diff --git a/plugin-designs/gospel-internet/src/upgZones.js b/plugin-designs/gospel-internet/src/upgZones.js new file mode 100644 index 000000000..705264324 --- /dev/null +++ b/plugin-designs/gospel-internet/src/upgZones.js @@ -0,0 +1,57 @@ +/** + * upgZones.js — join a people group to its UPG-100 zone. + * + * THE CHAIN, and why it needs three files. UPG-100 lists are defined by ADMIN REGION + * ("Tibet Autonomous Region", "Gujarat"); the people-group tables are keyed by group. + * They share no id, so the bridge is the province assignment: + * + * admin-to-list.json admin name -> zone id + * gospel-by-province-groups.json group -> province name + * + * ★ ADMIN NAMES ARE MATCHED WITH DIACRITICS FOLDED, and that is load-bearing rather than + * defensive. The two files disagree on spelling: the zone table writes `gujarat`, the + * province table writes `Gujarāt`; likewise Karnātaka, Tamil Nādu, Uttarākhand, Jammu and + * Kashmīr, Haryāna. Matching on `.toLowerCase()` alone resolves 514 of 1009 rows across 9 + * zones. Folding the combining marks resolves 1009 of 1009 across 12 zones. + * + * That 50.9% is the dangerous number, not the 100%: a legend built on it would have shown + * half the groups and three fewer zones with NOTHING ON SCREEN SAYING SO — every zone would + * still have looked populated, just smaller. Fold here rather than adding spellings to the + * zone table, so a future source spelling cannot reopen it. + */ + +/** Lowercase and strip combining marks, so `Gujarāt` and `gujarat` are the same key. */ +export function foldAdmin(name) { + return String(name ?? '') + .normalize('NFD') + .replace(/\p{Mn}/gu, '') + .trim() + .toLowerCase() +} + +/** Build a folded lookup from the zone table's `admin name -> zone id` map. */ +export function zoneIndex(adminToList) { + const ix = new Map() + for (const [admin, zone] of Object.entries(adminToList || {})) ix.set(foldAdmin(admin), zone) + return ix +} + +/** The zone id for a province name, or null when the province is in no UPG-100 list. */ +export function zoneFor(ix, province) { + return ix.get(foldAdmin(province)) ?? null +} + +/** + * group key -> zone id, from the province bridge rows. + * `key` decides the join key into whatever table the caller holds; the default + * `(name, iso3)` is the pair that matches near-neighbors at 994/1009. + */ +export function zonesByGroup(adminToList, provinceRows, key = (r) => `${r.people_group}|${r.iso3}`) { + const ix = zoneIndex(adminToList) + const out = new Map() + for (const r of provinceRows || []) { + const z = zoneFor(ix, r.province) + if (z) out.set(key(r), z) + } + return out +}