Join a people group to its UPG-100 zone, with admin names folded

The UPG-100 lists are defined by admin region and the people-group tables are
keyed by group, so the join runs through the province assignment. The two files
disagree on spelling: the zone table writes `gujarat`, the province table writes
`Gujarāt`, and the same for Karnātaka, Tamil Nādu, Uttarākhand, Jammu and
Kashmīr and Haryāna.

Matching on toLowerCase alone joins 514 of 1009 rows across 9 zones. Folding the
combining marks joins 1009 of 1009 across 12 zones.

The 50.9% is the number worth naming: a legend built on it would have shown half
the groups and three fewer zones, and nothing on screen would have said so —
every zone still looks populated, only smaller. The fold lives at the lookup
rather than as extra spellings in the zone table, so a future source spelling
cannot reopen it.
This commit is contained in:
Doxa Maps 2026-09-28 13:27:27 +00:00
parent dece2df47e
commit d6253b34d0

View file

@ -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
}