/** * scripts/extract-admin0-pop.mjs — one-time extraction of the population * SOURCE for the region-geo manifest: `public/ne-admin0-pop.json`. * * Output = `{ "": , ... }` — 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)`, );