← back to Nationalrealestate
src/ingest/zip_county.ts
51 lines
/**
* zip_county — national ZIP (ZCTA) -> primary county FIPS crosswalk that powers ZIP
* lookup in the top location search (/api/geo-search). Source: Census 2020 ZCTA-to-county
* relationship file (pipe-delimited, keyless, free). A ZIP can span counties; we keep the
* county with the largest land overlap (AREALAND_PART). Idempotent — rebuilds each run.
* Run: `npm run ingest:zipcounty` (local) or on prod after deploy.
*/
import { query, pool } from '../../db/pool.ts';
const SRC = 'https://www2.census.gov/geo/docs/maps-data/data/rel2020/zcta520/tab20_zcta520_county20_natl.txt';
async function main() {
console.log('[zip_county] downloading Census ZCTA→county relationship file…');
const res = await fetch(SRC);
if (!res.ok) throw new Error('download failed: HTTP ' + res.status);
const text = await res.text();
const lines = text.split('\n');
// fields (0-based): 1=GEOID_ZCTA5 (zip), 9=GEOID_COUNTY (fips), 16=AREALAND_PART
const best = new Map<string, { fips: string; area: number }>();
for (let i = 1; i < lines.length; i++) {
const f = lines[i].split('|');
if (f.length < 17) continue;
const zip = (f[1] || '').trim();
const fips = (f[9] || '').trim();
const area = Number(f[16] || 0);
if (!/^\d{5}$/.test(zip) || !/^\d{5}$/.test(fips)) continue;
const cur = best.get(zip);
if (!cur || area > cur.area) best.set(zip, { fips, area });
}
console.log(`[zip_county] parsed ${best.size} unique ZIPs`);
await query(`CREATE TABLE IF NOT EXISTS zip_county (zip text PRIMARY KEY, county_fips text NOT NULL)`);
await query(`TRUNCATE zip_county`);
const entries = [...best.entries()];
const CHUNK = 1000;
for (let i = 0; i < entries.length; i += CHUNK) {
const slice = entries.slice(i, i + CHUNK);
const vals: string[] = [];
const params: string[] = [];
slice.forEach(([zip, v], j) => { vals.push(`($${j * 2 + 1},$${j * 2 + 2})`); params.push(zip, v.fips); });
await query(
`INSERT INTO zip_county (zip, county_fips) VALUES ${vals.join(',')}
ON CONFLICT (zip) DO UPDATE SET county_fips = EXCLUDED.county_fips`,
params,
);
}
const c = await query<{ n: string }>(`SELECT count(*)::text AS n FROM zip_county`);
console.log(`[zip_county] loaded ${c.rows[0].n} rows`);
await pool.end();
}
main().catch(e => { console.error(e); process.exit(1); });