also looking at this
fix(dashboard): the regions panel says California, not CA
#63
4 files
+145
-9
| @@ -27,6 +27,8 @@ | ||
| 27 | 27 | */ |
| 28 | 28 | |
| 29 | 29 | /** What a grouped region query hands back, before folding. */ |
| 30 | import SUBDIVISIONS from './subdivisions.json' | |
| 31 | ||
| 30 | 32 | export interface RegionCount { |
| 31 | 33 | region: string | null |
| 32 | 34 | views: number | string |
| @@ -109,3 +111,78 @@ export function foldRegions(rows: readonly RegionCount[], floor: number, limit: | ||
| 109 | 111 | withheld, |
| 110 | 112 | } |
| 111 | 113 | } |
| 114 | ||
| 115 | /** | |
| 116 | * `US-CA` -> `California`, `PH-07` -> `Central Visayas`. | |
| 117 | * | |
| 118 | * ## Where the names come from | |
| 119 | * | |
| 120 | * `subdivisions.json` is a name -> code table per country, generated from the | |
| 121 | * DB-IP City Lite file so `geo.ts` can turn the English name that database | |
| 122 | * carries into an ISO code on the way in. Read the other way it is a code -> | |
| 123 | * name table, which is what a reader needs on the way out. | |
| 124 | * | |
| 125 | * The panel used to print the bare code, and the comment where it did explained | |
| 126 | * why: spelling a subdivision out "would mean shipping a table of every ISO | |
| 127 | * 3166-2 subdivision name — some four thousand of them". That table is already | |
| 128 | * shipped, and has been since region geo started working at all. `US-CA` at | |
| 129 | * least reads as California by luck; `PH-07` reads as nothing, and `CA` beside a | |
| 130 | * flag is equally the code for Canada. | |
| 131 | * | |
| 132 | * ## Why a name can be ambiguous, and which one wins | |
| 133 | * | |
| 134 | * DB-IP carries aliases, so 384 codes have more than one spelling — `AL-03` is | |
| 135 | * both `Elbasan` and `Elbasan County`, `DE-BW` both `Baden-Württemberg` and the | |
| 136 | * unaccented `Baden-Wurttemberg`. Reversing the map therefore needs a rule, and | |
| 137 | * the obvious one — shortest wins — picks the mangled German. | |
| 138 | * | |
| 139 | * So: the spelling with the most non-ASCII characters wins, and the shortest | |
| 140 | * breaks the tie. That is "prefer the properly accented name, then the plainest | |
| 141 | * form of it", which gives Baden-Württemberg, Berlin over State of Berlin, | |
| 142 | * Elbasan over Elbasan County, and Bicol over Bicol Region. | |
| 143 | * | |
| 144 | * Built once on first use: 194 countries and 3,234 subdivisions is not work to | |
| 145 | * repeat per row, and a dashboard that never opens the panel never pays for it. | |
| 146 | */ | |
| 147 | let subdivisionNames: Map<string, string> | null = null | |
| 148 | ||
| 149 | function namesByCode(): Map<string, string> { | |
| 150 | if (subdivisionNames) | |
| 151 | return subdivisionNames | |
| 152 | ||
| 153 | // Accented spelling first, then the shortest, so the winner is deterministic | |
| 154 | // whatever order the table is in. | |
| 155 | const preferred = (a: string, b: string): string => { | |
| 156 | const accents = (s: string) => [...s].filter(c => c.charCodeAt(0) > 127).length | |
| 157 | if (accents(a) !== accents(b)) | |
| 158 | return accents(a) > accents(b) ? a : b | |
| 159 | return a.length <= b.length ? a : b | |
| 160 | } | |
| 161 | ||
| 162 | const table = SUBDIVISIONS as Record<string, Record<string, string>> | |
| 163 | const built = new Map<string, string>() | |
| 164 | for (const [country, names] of Object.entries(table)) { | |
| 165 | for (const [name, code] of Object.entries(names)) { | |
| 166 | const key = `${country}-${code}` | |
| 167 | const held = built.get(key) | |
| 168 | built.set(key, held ? preferred(held, name) : name) | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | subdivisionNames = built | |
| 173 | return built | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * The English name for an ISO 3166-2 code, or null when the table has none. | |
| 178 | * | |
| 179 | * Null rather than a guess, and the caller keeps showing the code: a subdivision | |
| 180 | * the table does not know is still a real place the visitor came from, and a row | |
| 181 | * reading `PH-XX` is honest where an invented name would not be. | |
| 182 | */ | |
| 183 | export function subdivisionName(code: string | null | undefined): string | null { | |
| 184 | const parts = splitRegion(code) | |
| 185 | if (!parts) | |
| 186 | return null | |
| 187 | return namesByCode().get(`${parts.country}-${parts.subdivision}`) ?? null | |
| 188 | } | |