2026-03-16 23:19:40 +00:00
|
|
|
const { Pool } = require('pg');
|
|
|
|
|
const { execSync } = require('child_process');
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const readline = require('readline');
|
2026-03-16 20:32:22 +00:00
|
|
|
require('dotenv').config();
|
|
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
const DATA_URL = 'https://download.geonames.org/export/dump/cities5000.zip';
|
|
|
|
|
const ZIP_FILE = '/tmp/cities.zip';
|
|
|
|
|
const TXT_FILE = '/tmp/cities5000.txt';
|
2026-03-16 20:32:22 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
async function importGeoNames() {
|
|
|
|
|
const pool = new Pool({
|
2026-03-16 20:50:35 +00:00
|
|
|
connectionString: process.env.DATABASE_URL || 'postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight'
|
2026-03-16 20:32:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-16 23:19:40 +00:00
|
|
|
console.log('Downloading GeoNames cities5000 (Pop > 5000)...');
|
|
|
|
|
execSync(`wget -q ${DATA_URL} -O ${ZIP_FILE}`);
|
|
|
|
|
|
|
|
|
|
console.log('Extracting data...');
|
|
|
|
|
execSync(`unzip -o ${ZIP_FILE} -d /tmp`);
|
|
|
|
|
|
2026-03-16 20:32:22 +00:00
|
|
|
console.log('Connecting to database...');
|
2026-03-16 23:19:40 +00:00
|
|
|
const client = await pool.connect();
|
2026-03-16 20:32:22 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
// Ensure table is clean
|
|
|
|
|
await client.query('TRUNCATE TABLE cities');
|
2026-03-16 20:32:22 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
console.log('Starting stream import...');
|
|
|
|
|
|
|
|
|
|
const fileStream = fs.createReadStream(TXT_FILE);
|
|
|
|
|
const rl = readline.createInterface({
|
|
|
|
|
input: fileStream,
|
|
|
|
|
crlfDelay: Infinity
|
|
|
|
|
});
|
2026-03-16 20:32:22 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
let batch = [];
|
|
|
|
|
const batchSize = 500;
|
|
|
|
|
let count = 0;
|
2026-03-16 20:50:35 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
for await (const line of rl) {
|
|
|
|
|
const parts = line.split('\t');
|
|
|
|
|
if (parts.length < 15) continue;
|
2026-03-16 20:32:22 +00:00
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
const name = parts[1]; // name
|
|
|
|
|
const lat = parseFloat(parts[4]);
|
|
|
|
|
const lon = parseFloat(parts[5]);
|
|
|
|
|
const country = parts[8]; // country code
|
|
|
|
|
const population = parseInt(parts[14]) || 0;
|
|
|
|
|
|
|
|
|
|
batch.push({ name, lat, lon, country, population });
|
|
|
|
|
|
|
|
|
|
if (batch.length >= batchSize) {
|
|
|
|
|
await insertBatch(client, batch);
|
|
|
|
|
count += batch.length;
|
|
|
|
|
if (count % 5000 === 0) console.log(`Imported ${count} cities...`);
|
|
|
|
|
batch = [];
|
2026-03-16 20:32:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
if (batch.length > 0) {
|
|
|
|
|
await insertBatch(client, batch);
|
|
|
|
|
count += batch.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`SUCCESS: Imported ${count} cities and towns.`);
|
|
|
|
|
client.release();
|
2026-03-16 20:32:22 +00:00
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('ERROR during import:', err);
|
|
|
|
|
} finally {
|
2026-03-16 23:19:40 +00:00
|
|
|
// Cleanup
|
|
|
|
|
if (fs.existsSync(ZIP_FILE)) fs.unlinkSync(ZIP_FILE);
|
|
|
|
|
if (fs.existsSync(TXT_FILE)) fs.unlinkSync(TXT_FILE);
|
|
|
|
|
await pool.end();
|
2026-03-16 20:32:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 23:19:40 +00:00
|
|
|
async function insertBatch(client, batch) {
|
|
|
|
|
const queryParts = [];
|
|
|
|
|
const values = [];
|
|
|
|
|
|
|
|
|
|
batch.forEach((city, index) => {
|
|
|
|
|
const base = index * 5;
|
|
|
|
|
queryParts.push(`($${base + 1}, $${base + 2}, $${base + 3}, ST_SetSRID(ST_MakePoint($${base + 4}, $${base + 5}), 4326)::geography)`);
|
|
|
|
|
values.push(city.name, city.population, city.country, city.lon, city.lat);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await client.query(
|
|
|
|
|
`INSERT INTO cities (name, population, country, geom) VALUES ${queryParts.join(',')}`,
|
|
|
|
|
values
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
importGeoNames();
|