79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
/**
|
|
* scripts/extract-admin0-pop.mjs — one-time extraction of the population
|
|
* SOURCE for the region-geo manifest: `public/ne-admin0-pop.json`.
|
|
*
|
|
* Output = `{ "<ADM0_A3>": <POP_EST>, ... }` — the estimated population
|
|
* (POP_EST) of every Natural Earth 1:10m **admin-0 country** code, keeping
|
|
* the **maximum** POP_EST seen for each code (Natural Earth ships a few
|
|
* duplicated / broken-out features per code, e.g. the `TLC` stubs, whose
|
|
* POP_EST is 0; the real country always wins by having the larger value).
|
|
* Only codes with a finite, positive POP_EST are recorded (a 0/null POP_EST
|
|
* has nothing to distribute). Keys are sorted; the file is written with
|
|
* 2-space indent + trailing newline (deterministic, committed).
|
|
*
|
|
* Why a separate committed file: `public/region-geo.json` (built by
|
|
* `build-region-geo.mjs`) needs a per-country population, but the raw
|
|
* Natural Earth GeoJSON (13 MB) is a *source* file we deliberately do NOT
|
|
* commit (see the AGENTS.md data-pipeline note). This script distils it
|
|
* into a tiny (a few KB) derived table that IS committed, so the region-geo
|
|
* builder stays fully reproducible in CI without the source present.
|
|
* Re-run only when the underlying Natural Earth data changes.
|
|
*
|
|
* The 1:10m admin-1 file (the one the region generator uses) carries NO
|
|
* population; the 1:10m admin-0 file does (uppercase `POP_EST` / `ADM0_A3`).
|
|
*
|
|
* Usage:
|
|
* NE_ADMIN0=/path/to/ne_10m_admin_0_countries.geojson node scripts/extract-admin0-pop.mjs
|
|
* (default source: /tmp/ne10m_admin0.geojson)
|
|
*/
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(here, '..');
|
|
|
|
const src = process.env.NE_ADMIN0 || '/tmp/ne10m_admin0.geojson';
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(src, 'utf8');
|
|
} catch (err) {
|
|
console.error(
|
|
`[extract-admin0-pop] could not read the Natural Earth admin-0 source at ${src}. ` +
|
|
`Download it (ne_10m_admin_0_countries.geojson) and re-run with NE_ADMIN0 pointing at it.`,
|
|
);
|
|
throw err;
|
|
}
|
|
|
|
const geo = JSON.parse(raw);
|
|
|
|
/** ADM0_A3 -> max finite, positive POP_EST. */
|
|
const byCode = new Map();
|
|
for (const f of geo.features ?? []) {
|
|
const p = f.properties ?? {};
|
|
const code = typeof p.ADM0_A3 === 'string' ? p.ADM0_A3 : '';
|
|
const pop = typeof p.POP_EST === 'number' ? p.POP_EST : NaN;
|
|
if (!code || !Number.isFinite(pop) || pop <= 0) continue;
|
|
const prev = byCode.get(code);
|
|
if (prev === undefined || pop > prev) byCode.set(code, Math.round(pop));
|
|
}
|
|
|
|
if (byCode.size === 0) {
|
|
throw new Error('[extract-admin0-pop] no finite positive POP_EST found in the source — wrong file?');
|
|
}
|
|
|
|
// Deterministic: sorted keys, 2-space indent, trailing newline.
|
|
const keys = [...byCode.keys()].sort();
|
|
const manifest = {};
|
|
for (const k of keys) manifest[k] = byCode.get(k);
|
|
|
|
const out = path.join(root, 'public', 'ne-admin0-pop.json');
|
|
writeFileSync(out, JSON.stringify(manifest, null, 2) + '\n');
|
|
|
|
let sum = 0;
|
|
for (const v of byCode.values()) sum += v;
|
|
console.log(
|
|
`[extract-admin0-pop] wrote public/ne-admin0-pop.json — ${byCode.size} country codes, ` +
|
|
`total population ${sum.toLocaleString('en-US')} (Natural Earth 1:10m admin-0, max POP_EST per code)`,
|
|
);
|