import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { getDbPool } from '../../server/src/db/pool.js'; import { getFeatureGeometry, getStringProperty, normalizePostalCode, readFeatureCollection, type PostalDatasetConfig } from './postal-import-utils.js'; const currentDir = path.dirname(fileURLToPath(import.meta.url)); const datasetsRoot = path.resolve(currentDir, '../datasets/postal'); const datasetConfigs: PostalDatasetConfig[] = [ { countryCode: 'US', label: 'US ZIP/ZCTA', filePath: process.env.POSTAL_US_DATASET_PATH || path.join(datasetsRoot, 'us_zcta.geojson'), postalCodeKeys: ['postal_code', 'zip', 'zcta', 'GEOID20', 'ZCTA5CE20', 'ZCTA5CE10'], displayNameKeys: ['display_name', 'name', 'NAMELSAD20', 'GEOID20', 'postal_code'], }, { countryCode: 'CA', label: 'Canada FSA', filePath: process.env.POSTAL_CA_DATASET_PATH || path.join(datasetsRoot, 'ca_fsa.geojson'), postalCodeKeys: ['postal_code', 'fsa', 'CFSAUID', 'CFSAUID24'], displayNameKeys: ['display_name', 'name', 'CFSAUID', 'postal_code'], }, ]; async function importDataset(config: PostalDatasetConfig) { const pool = getDbPool(); const client = await pool.connect(); try { const collection = await readFeatureCollection(config.filePath); let insertedCount = 0; let skippedCount = 0; await client.query('begin'); for (const [index, feature] of collection.features.entries()) { const rawPostalCode = getStringProperty(feature.properties, config.postalCodeKeys); if (!rawPostalCode) { skippedCount += 1; continue; } const normalizedPostalCode = normalizePostalCode(config.countryCode, rawPostalCode); if (!normalizedPostalCode) { skippedCount += 1; continue; } const displayName = getStringProperty(feature.properties, config.displayNameKeys) || normalizedPostalCode; const geometry = getFeatureGeometry(feature, config.filePath, index); await client.query( ` insert into public.postal_areas ( country_code, postal_code, display_name, normalized_postal_code, geom, centroid, search_radius_m, metadata_json, created_at, updated_at ) values ( $1, $2, $3, $4, ST_Multi(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326)), ST_Centroid(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326))::geography, greatest(1000, round(sqrt(ST_Area(ST_SetSRID(ST_GeomFromGeoJSON($5), 4326)::geography) / pi()))::integer), $6::jsonb, now(), now() ) on conflict (country_code, normalized_postal_code) do update set postal_code = excluded.postal_code, display_name = excluded.display_name, geom = excluded.geom, centroid = excluded.centroid, search_radius_m = excluded.search_radius_m, metadata_json = excluded.metadata_json, updated_at = now() `, [ config.countryCode, rawPostalCode.trim(), displayName, normalizedPostalCode, JSON.stringify(geometry), JSON.stringify(feature.properties ?? {}), ], ); insertedCount += 1; } await client.query('commit'); console.log(`Imported ${insertedCount} ${config.label} areas from ${config.filePath}. Skipped ${skippedCount}.`); } catch (error) { await client.query('rollback'); throw error; } finally { client.release(); } } async function run() { for (const config of datasetConfigs) { await importDataset(config); } await getDbPool().end(); } await run();