← back to Nationalrealestate
src/ingest/cbsa_crosswalk.ts
63 lines
/**
* Backfills region.cbsa_code for COUNTY rows from the Census CBSA delineation
* file (list1_2023.xlsx) — county FIPS -> CBSA code. Fixes the null metro name
* on property/location panels (M-P1 report nit).
*/
import { query, pool } from '../../db/pool.ts';
import { openRun, closeRun, download } from './run.ts';
import { createRequire } from 'node:module';
const require_ = createRequire(import.meta.url);
const XLSX = require_('xlsx');
const URL_ = 'https://www2.census.gov/programs-surveys/metro-micro/geographies/reference-files/2023/delineation-files/list1_2023.xlsx';
async function main() {
const runId = await openRun('cbsa_crosswalk', URL_);
try {
const { path, sha256 } = await download(URL_, 'list1_2023.xlsx', { reuseCached: true });
const wb = XLSX.readFile(path);
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows: any[][] = XLSX.utils.sheet_to_json(sheet, { header: 1 });
// header row is a few rows down in this file — find it by the 'CBSA Code' cell
const hIdx = rows.findIndex(r => Array.isArray(r) && r.some(c => String(c).trim() === 'CBSA Code'));
if (hIdx < 0) throw new Error('header row not found — file format drift?');
const header = rows[hIdx].map((h: any) => String(h).trim());
const col = (name: string) => header.indexOf(name);
const cCbsa = col('CBSA Code'), cStF = col('FIPS State Code'), cCoF = col('FIPS County Code');
if (cCbsa < 0 || cStF < 0 || cCoF < 0) throw new Error('expected columns missing: ' + header.join('|'));
const pairs: [string, string][] = [];
for (const r of rows.slice(hIdx + 1)) {
if (!Array.isArray(r) || r[cCbsa] == null || r[cStF] == null || r[cCoF] == null) continue;
const cbsa = String(r[cCbsa]).trim();
const fips = String(r[cStF]).padStart(2, '0') + String(r[cCoF]).padStart(3, '0');
if (/^\d{5}$/.test(cbsa) && /^\d{5}$/.test(fips)) pairs.push([fips, cbsa]);
}
if (pairs.length < 1500) throw new Error(`only ${pairs.length} county-CBSA pairs parsed — drift?`);
let updated = 0;
for (let i = 0; i < pairs.length; i += 500) {
const batch = pairs.slice(i, i + 500);
const params: unknown[] = [];
const values = batch.map(([f, c], j) => { params.push(f, c); return `($${j * 2 + 1},$${j * 2 + 2})`; });
const r = await query(
`UPDATE region rg SET cbsa_code = v.cbsa
FROM (VALUES ${values.join(',')}) AS v(fips, cbsa)
WHERE rg.region_type = 'county' AND rg.fips = v.fips`,
params,
);
updated += r.rowCount ?? 0;
}
await closeRun(runId, 'ok', { upserted: updated, fileHash: sha256, notes: `${pairs.length} county-CBSA pairs` });
console.log(`[cbsa_crosswalk] ok: ${updated} county regions updated from ${pairs.length} pairs`);
} catch (e: any) {
await closeRun(runId, 'failed', { notes: String(e.message || e) });
throw e;
} finally {
await pool.end();
}
}
main().catch(e => { console.error(e); process.exit(1); });