@mailwoman/codex 9.0.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +48 -5
  2. package/ca/index.ts +1 -1
  3. package/ca/postal-code.ts +11 -11
  4. package/country/display-names.ts +150 -0
  5. package/country/index.ts +2 -0
  6. package/country/population.ts +265 -0
  7. package/country/subdivision.ts +59 -0
  8. package/de/index.ts +1 -1
  9. package/de/postleitzahl.ts +3 -3
  10. package/es/codigo-postal.ts +2 -2
  11. package/fr/code-postal.ts +6 -6
  12. package/fr/index.ts +1 -1
  13. package/index.ts +10 -1
  14. package/it/cap.ts +2 -2
  15. package/jp/index.ts +1 -1
  16. package/jp/postal-code.ts +11 -11
  17. package/out/ca/index.d.ts +1 -1
  18. package/out/ca/index.js +1 -1
  19. package/out/ca/postal-code.d.ts +10 -10
  20. package/out/ca/postal-code.js +9 -9
  21. package/out/country/display-names.d.ts +67 -0
  22. package/out/country/display-names.d.ts.map +1 -0
  23. package/out/country/display-names.js +118 -0
  24. package/out/country/display-names.js.map +1 -0
  25. package/out/country/index.d.ts +2 -0
  26. package/out/country/index.d.ts.map +1 -1
  27. package/out/country/index.js +2 -0
  28. package/out/country/index.js.map +1 -1
  29. package/out/country/population.d.ts +16 -0
  30. package/out/country/population.d.ts.map +1 -0
  31. package/out/country/population.js +265 -0
  32. package/out/country/population.js.map +1 -0
  33. package/out/country/subdivision.d.ts +7 -0
  34. package/out/country/subdivision.d.ts.map +1 -1
  35. package/out/country/subdivision.js +49 -0
  36. package/out/country/subdivision.js.map +1 -1
  37. package/out/de/index.d.ts +1 -1
  38. package/out/de/index.js +1 -1
  39. package/out/de/postleitzahl.d.ts +3 -3
  40. package/out/de/postleitzahl.js +2 -2
  41. package/out/es/codigo-postal.d.ts +2 -2
  42. package/out/es/codigo-postal.js +1 -1
  43. package/out/fr/code-postal.d.ts +5 -5
  44. package/out/fr/code-postal.js +6 -6
  45. package/out/fr/index.d.ts +1 -1
  46. package/out/fr/index.js +1 -1
  47. package/out/index.d.ts +1 -1
  48. package/out/index.d.ts.map +1 -1
  49. package/out/index.js +1 -1
  50. package/out/index.js.map +1 -1
  51. package/out/it/cap.d.ts +2 -2
  52. package/out/it/cap.js +1 -1
  53. package/out/jp/index.d.ts +1 -1
  54. package/out/jp/index.js +1 -1
  55. package/out/jp/postal-code.d.ts +11 -11
  56. package/out/jp/postal-code.js +8 -8
  57. package/out/postcode-systems.d.ts +84 -0
  58. package/out/postcode-systems.d.ts.map +1 -1
  59. package/out/postcode-systems.js +106 -0
  60. package/out/postcode-systems.js.map +1 -1
  61. package/out/tools/build-country-surface-lexicon.js +1 -1
  62. package/out/tools/build-country-surface-lexicon.js.map +1 -1
  63. package/out/tools/export-country-surfaces.js +1 -1
  64. package/out/tools/export-country-surfaces.js.map +1 -1
  65. package/out/tools/generate-country-population.d.ts +42 -0
  66. package/out/tools/generate-country-population.d.ts.map +1 -0
  67. package/out/tools/generate-country-population.js +106 -0
  68. package/out/tools/generate-country-population.js.map +1 -0
  69. package/out/tools/generate-country-reference.d.ts.map +1 -1
  70. package/out/tools/generate-country-reference.js +4 -4
  71. package/out/tools/generate-country-reference.js.map +1 -1
  72. package/out/tools/generate-official-languages.d.ts.map +1 -1
  73. package/out/tools/generate-official-languages.js +7 -4
  74. package/out/tools/generate-official-languages.js.map +1 -1
  75. package/out/tools/index.d.ts +2 -2
  76. package/out/tools/index.d.ts.map +1 -1
  77. package/out/tools/index.js +2 -2
  78. package/out/tools/index.js.map +1 -1
  79. package/out/us/street-suffix.d.ts +432 -418
  80. package/out/us/street-suffix.d.ts.map +1 -1
  81. package/out/us/street-suffix.js +28 -216
  82. package/out/us/street-suffix.js.map +1 -1
  83. package/out/us/street-suffix.json +273 -0
  84. package/out/us/zipcode.d.ts +1 -1
  85. package/out/us/zipcode.js +1 -1
  86. package/package.json +328 -4
  87. package/postcode-systems.ts +112 -0
  88. package/tools/build-country-surface-lexicon.ts +1 -1
  89. package/tools/export-country-surfaces.ts +1 -1
  90. package/tools/generate-country-population.ts +143 -0
  91. package/tools/generate-country-reference.ts +5 -3
  92. package/tools/generate-official-languages.ts +8 -4
  93. package/tools/index.ts +2 -2
  94. package/us/street-suffix.json +273 -0
  95. package/us/street-suffix.ts +32 -217
  96. package/us/zipcode.ts +1 -1
package/README.md CHANGED
@@ -41,7 +41,7 @@ import { lookupStreetSuffix } from "@mailwoman/codex/us"
41
41
  | **`fr`** | La Poste postcode format, CEDEX conventions, département codes |
42
42
  | **`gb`** | Royal Mail postcode format, post town conventions |
43
43
  | **`de`** | Deutsche Post postcode format, Bundesland abbreviations |
44
- | **`ca`** | Canada Post postcode format, province abbreviations |
44
+ | **`ca`** | Canada Post postcode format, province abbreviations, the urban/rural FSA split |
45
45
  | **`au`** | Australia Post postcode format, state abbreviations |
46
46
 
47
47
  ## Cross-system utilities
@@ -49,15 +49,57 @@ import { lookupStreetSuffix } from "@mailwoman/codex/us"
49
49
  ```ts
50
50
  import { candidateSystemsForPostcode } from "@mailwoman/codex"
51
51
 
52
- // Which systems could "94043" belong to?
53
- candidateSystemsForPostcode("94043") // → ["us"]
54
- candidateSystemsForPostcode("75008") // → ["fr"]
55
- candidateSystemsForPostcode("10115") // → ["de"]
52
+ // Which systems could this shape belong to? A five-digit run is not evidence of one country.
53
+ candidateSystemsForPostcode("94043") // → ["us", "de", "fr"]
54
+ candidateSystemsForPostcode("SW1A 1AA") // → ["gb"]
56
55
 
57
56
  // Address system conventions (forbidden tags, expected shapes, etc.)
58
57
  import { ADDRESS_SYSTEM_CONVENTIONS, conventionsForSystem } from "@mailwoman/codex"
59
58
  ```
60
59
 
60
+ Note the first answer. This is a **shape** test, not a gazetteer membership test, and
61
+ returning all three is the correct answer rather than a hedge — the caller's country
62
+ scope is what narrows it. Picking one locale here would be a guess wearing a fact's
63
+ clothes.
64
+
65
+ ## Postcode granularity: three tiers, each earned by measurement
66
+
67
+ One `postalcode` placetype covers systems that are not comparable. An Irish Eircode
68
+ names a single address; an Australian postcode names a locality. Between them sit
69
+ most of the world, and the distinction that actually changes an answer is narrower:
70
+ **is this code finer than the locality that contains it?**
71
+
72
+ That is a fact about a country's _administrative_ geography, not its postal system,
73
+ and code length does not predict it. France and Germany are both five digits and land
74
+ on opposite sides.
75
+
76
+ ```ts
77
+ import { isUnitGradePostcodeHit, areaPostcodeLeadsLocality } from "@mailwoman/codex"
78
+
79
+ isUnitGradePostcodeHit("N7 0BT", "n70bt") // → true (GB unit, and the resolver hit the FULL code)
80
+ isUnitGradePostcodeHit("N7 0BT", "n7") // → false (the resolver answered with the outward district)
81
+ areaPostcodeLeadsLocality("DE") // → true (a Gemeinde can be the size of Berlin)
82
+ areaPostcodeLeadsLocality("FR") // → false (one code postal often spans several communes)
83
+ ```
84
+
85
+ | Tier | Members | What earned it |
86
+ | --------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- |
87
+ | **unit-grade** (`UNIT_GRADE_POSTCODE`) | NL PC6, GB unit, CA **urban** LDU | measured against rooftop truth — GB 38 m median, CA urban 78 m |
88
+ | **area, but still finer than the locality** (`AREA_POSTCODE_FINER_THAN_LOCALITY`) | DE | full-panel measurement: 5.84 km → 1.24 km p50, better on every percentile |
89
+ | **area** (the default) | everything else | the locality-first convention |
90
+
91
+ Membership is earned by a measurement, never by a shape that looks tight. Canada is
92
+ the case that shows why: its urban LDU is unit-grade, its **rural** LDU measures
93
+ 2.08 km against the locality's 929 m and is excluded — and Canada Post already marks
94
+ the difference with a `0` in the second character, so the code says which before any
95
+ lookup runs. The pooled Canadian number reads 0.10 km and looks like a uniform win;
96
+ it is two populations, and a tier claim that averages two granularities is exactly
97
+ what these tables exist to prevent.
98
+
99
+ `@mailwoman/resolver`'s `admin-winner` consumes both predicates, and so does the
100
+ browser demo's pin ranking — one definition, because the two once disagreed and
101
+ nothing noticed.
102
+
61
103
  ## Design
62
104
 
63
105
  - **Zero runtime dependencies.** Pure TypeScript data tables — no database, no I/O,
@@ -92,6 +134,7 @@ questions, different tables.
92
134
 
93
135
  - [`@mailwoman/core`](../core) — `ComponentTag` schema, pipeline infrastructure
94
136
  - [`@mailwoman/address-id`](../address-id) — uses codex for stable address primary keys
137
+ - [`@mailwoman/resolver`](../resolver) — consumes the granularity tiers to order a resolved tree
95
138
  - [Address system conventions](https://github.com/sister-software/mailwoman/blob/main/docs/engineering/reference/SCHEMA.mdx)
96
139
 
97
140
  ## License
package/ca/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
6
  * The Canadian address system (Canada Post / ISO 3166-2:CA): bilingual street types, alphanumeric
7
- * postal codes, and the provinces and territories.
7
+ * postcodes, and the provinces and territories.
8
8
  */
9
9
 
10
10
  export * from "./postal-code.ts"
package/ca/postal-code.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Canadian postal codes: the branded type, the shape, normalization, and the FSA-letter →
6
+ * Canadian postcodes: the branded type, the shape, normalization, and the FSA-letter →
7
7
  * province/territory prior — the only ALPHANUMERIC postcode of the systems the codex models.
8
8
  *
9
9
  * The informative contrast across `us/zipcode.ts`, `de/postleitzahl.ts`, `fr/code-postal.ts`, and
@@ -12,7 +12,7 @@
12
12
  * - A US ZIP is numeric; its first digit maps to a loose BAND of states.
13
13
  * - A German PLZ is numeric; its first digit maps to a Leitzone that CROSSES Bundesland borders.
14
14
  * - A French code postal is numeric; its first TWO digits ARE the département.
15
- * - A Canadian postal code is `A1A 1A1` — Letter Digit Letter, then Digit Letter Digit — and its
15
+ * - A Canadian postcode is `A1A 1A1` — Letter Digit Letter, then Digit Letter Digit — and its
16
16
  * first LETTER pins the province or territory directly (`M` → Ontario, `H` → Quebec, `V` →
17
17
  * British Columbia). So like the French prefix it is a clean admin prior, but it does the job
18
18
  * with a single ALPHA character rather than digits.
@@ -30,18 +30,18 @@ import type { Tagged } from "type-fest"
30
30
  import type { CanadianProvinceCode } from "./province.ts"
31
31
 
32
32
  /**
33
- * Characters in a Canadian postal code once spaces are stripped: `A1A1A1`.
33
+ * Characters in a Canadian postcode once spaces are stripped: `A1A1A1`.
34
34
  */
35
35
  const POSTAL_CODE_LENGTH = 6
36
36
 
37
37
  /**
38
- * A Canadian postal code: `A1A 1A1`. Six alphanumeric characters in a strict Letter-Digit-Letter-Digit-Letter-Digit
38
+ * A Canadian postcode: `A1A 1A1`. Six alphanumeric characters in a strict Letter-Digit-Letter-Digit-Letter-Digit
39
39
  * pattern, conventionally written with a single space after the third. Unlike the other systems' bare five digits, the
40
40
  * shape alone already says "Canada".
41
41
  *
42
42
  * @category Postal
43
43
  * @type string
44
- * @title Postal Code
44
+ * @title Postcode
45
45
  * @pattern ^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$
46
46
  */
47
47
  export type PostalCode = Tagged<string, "CaPostalCode">
@@ -56,7 +56,7 @@ export const CA_POSTAL_CODE_PATTERN = /^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?
56
56
  /**
57
57
  * Normalize a postal-code surface form to canonical `A1A 1A1`: uppercase and ensure exactly one space between the FSA
58
58
  * (first three chars) and the LDU (last three) — `K1A0B1` → `K1A 0B1`, `k1a 0b1` → `K1A 0B1`. Returns null if the input
59
- * is not a valid Canadian postal code.
59
+ * is not a valid Canadian postcode.
60
60
  */
61
61
  export function normalizeCaPostalCode(raw: unknown): PostalCode | null {
62
62
  if (typeof raw !== "string") return null
@@ -69,7 +69,7 @@ export function normalizeCaPostalCode(raw: unknown): PostalCode | null {
69
69
  }
70
70
 
71
71
  /**
72
- * Type-predicate for a Canadian postal code (accepts the spaced or unspaced surface form).
72
+ * Type-predicate for a Canadian postcode (accepts the spaced or unspaced surface form).
73
73
  */
74
74
  export function isCaPostalCode(input: unknown): input is PostalCode {
75
75
  return typeof input === "string" && CA_POSTAL_CODE_PATTERN.test(input)
@@ -102,9 +102,9 @@ export const FSA_LETTER_TO_PROVINCE: Record<string, CanadianProvinceCode | Canad
102
102
  }
103
103
 
104
104
  /**
105
- * The province/territory a postal code belongs to, via its FSA first letter. Returns the single code for the clean
106
- * letters, the `["NT", "NU"]` pair for the shared `X`, and null if the input is not a valid Canadian postal code (or
107
- * its first letter has no province, which the pattern already forbids).
105
+ * The province/territory a postcode belongs to, via its FSA first letter. Returns the single code for the clean
106
+ * letters, the `["NT", "NU"]` pair for the shared `X`, and null if the input is not a valid Canadian postcode (or its
107
+ * first letter has no province, which the pattern already forbids).
108
108
  */
109
109
  export function provinceOfPostalCode(postalCode: unknown): CanadianProvinceCode | CanadianProvinceCode[] | null {
110
110
  const normalized = normalizeCaPostalCode(postalCode)
@@ -115,7 +115,7 @@ export function provinceOfPostalCode(postalCode: unknown): CanadianProvinceCode
115
115
  }
116
116
 
117
117
  /**
118
- * True when a postal code is RURAL: its SECOND character (the FSA's first digit) is `0`. Canada Post uses a `0` in that
118
+ * True when a postcode is RURAL: its SECOND character (the FSA's first digit) is `0`. Canada Post uses a `0` in that
119
119
  * position to mark the lower-density delivery zones (rural routes, small communities) — the contrast with the urban
120
120
  * `1`–`9` FSAs. Returns false for a non-code.
121
121
  */
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Country surface forms across scripts, enumerated from the RUNTIME's own ICU via `Intl.DisplayNames`.
7
+ *
8
+ * The gazetteer holds country names in English and little else, which is why a bare `格鲁吉亚` (Georgia the country,
9
+ * Chinese), `沙特阿拉伯` or `巴布亚新几内亚` resolves to nothing while `佐治亚州` (Georgia the US state) resolves
10
+ * correctly — the state is a real WOF record carrying multilingual names, and 140 of the 237 country rows are
11
+ * synthetic and carry only a canonical English name. Measured 2026-08-15; WOF has no Chinese country names at all,
12
+ * and `geonames-aliases.ts` filters every alias through a Latin-script regex.
13
+ *
14
+ * ICU already knows all of it. No download, no vendored corpus, no licence question, and no drift against a snapshot
15
+ * we would otherwise have to refresh: the names come from the same ICU the runtime uses for every other
16
+ * locale-sensitive operation. Measured coverage at time of writing: **280 regions, 5,244 distinct surfaces** across
17
+ * the locale × style grid below.
18
+ *
19
+ * This module ENUMERATES. It does not decide what the gazetteer stores — see the candidate build for that, and note
20
+ * that a surface here is a NAME THE WORLD USES, never an authority's designation.
21
+ */
22
+
23
+ /**
24
+ * The locales whose display names are enumerated. Chosen for script coverage rather than speaker count: each entry
25
+ * either contributes a distinct script (Han simplified/traditional, Kana, Hangul, Arabic, Cyrillic, Devanagari, Hebrew,
26
+ * Greek, Thai) or a major Latin-script exonym set that diverges from English.
27
+ *
28
+ * Adding a locale is additive and safe — surfaces are deduplicated — but every addition grows the candidate table, so
29
+ * it earns its place by contributing surfaces a user would plausibly type.
30
+ */
31
+ export const DISPLAY_NAME_LOCALES = [
32
+ "en",
33
+ "zh-Hans",
34
+ "zh-Hant",
35
+ "ja",
36
+ "ko",
37
+ "ar",
38
+ "ru",
39
+ "es",
40
+ "fr",
41
+ "de",
42
+ "hi",
43
+ "pt",
44
+ "it",
45
+ "nl",
46
+ "pl",
47
+ "tr",
48
+ "vi",
49
+ "th",
50
+ "id",
51
+ "fa",
52
+ "he",
53
+ "uk",
54
+ "sv",
55
+ "el",
56
+ ] as const
57
+
58
+ /**
59
+ * `long` is the ordinary name, `short` supplies the abbreviations people actually type (`UK`, `US`, `アメリカ`), and
60
+ * `narrow` occasionally differs again. All three are enumerated because the query register is whatever the user wrote.
61
+ */
62
+ export const DISPLAY_NAME_STYLES = ["long", "short", "narrow"] as const
63
+
64
+ /**
65
+ * One country surface and where it came from.
66
+ */
67
+ export interface CountryDisplayName {
68
+ /**
69
+ * ISO 3166-1 alpha-2.
70
+ */
71
+ iso2: string
72
+ /**
73
+ * The surface as ICU renders it, unmodified — normalisation is the consumer's job, and the raw form is what a
74
+ * provenance record needs to be auditable.
75
+ */
76
+ name: string
77
+ /**
78
+ * BCP-47 tag this surface came from. Carried so a consumer can scope by locale rather than accepting every script for
79
+ * every query.
80
+ */
81
+ locale: string
82
+ }
83
+
84
+ /**
85
+ * Two-letter sequences that are not ISO 3166-1 regions. `Intl.DisplayNames.of` echoes its input for an unknown code, so
86
+ * the echo IS the miss signal — no separate region list to keep in sync.
87
+ */
88
+ /**
89
+ * The AA–ZZ sweep bounds. ISO 3166-1 alpha-2 is exactly two uppercase ASCII letters, so enumerating the whole square
90
+ * and keeping what ICU recognises avoids carrying a region list that would need its own upkeep.
91
+ */
92
+ const ASCII_A = 65
93
+ const ASCII_Z = 90
94
+
95
+ function isEcho(code: string, rendered: string | undefined): boolean {
96
+ return !rendered || rendered === code
97
+ }
98
+
99
+ /**
100
+ * Enumerate every AA–ZZ code against the locale × style grid, keeping what ICU recognises.
101
+ *
102
+ * Deduplicated per (iso2, name): the same surface reached from several locales is one row, and the FIRST locale that
103
+ * produced it wins the attribution — deterministic because {@link DISPLAY_NAME_LOCALES} is ordered.
104
+ */
105
+ export function* enumerateCountryDisplayNames(
106
+ locales: readonly string[] = DISPLAY_NAME_LOCALES
107
+ ): Generator<CountryDisplayName> {
108
+ const formatters = locales.flatMap((locale) =>
109
+ DISPLAY_NAME_STYLES.map((style) => {
110
+ try {
111
+ return { locale, formatter: new Intl.DisplayNames([locale], { type: "region", style }) }
112
+ } catch {
113
+ // A runtime without this locale's data degrades to fewer surfaces, never to an error.
114
+ return undefined
115
+ }
116
+ }).filter((f) => f !== null && f !== undefined)
117
+ )
118
+
119
+ for (let a = ASCII_A; a <= ASCII_Z; a++) {
120
+ for (let b = ASCII_A; b <= ASCII_Z; b++) {
121
+ const iso2 = String.fromCharCode(a, b)
122
+ const seen = new Set<string>()
123
+
124
+ for (const { locale, formatter } of formatters) {
125
+ let rendered: string | undefined
126
+
127
+ try {
128
+ rendered = formatter.of(iso2)
129
+ } catch {
130
+ continue
131
+ }
132
+
133
+ if (isEcho(iso2, rendered) || seen.has(rendered!)) continue
134
+ seen.add(rendered!)
135
+
136
+ yield { iso2, name: rendered!, locale }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Every surface ICU knows for one country. Convenience over {@link enumerateCountryDisplayNames} for a single lookup;
144
+ * the generator is the bulk path.
145
+ */
146
+ export function countryDisplayNames(iso2: string, locales?: readonly string[]): string[] {
147
+ const upper = iso2.toUpperCase()
148
+
149
+ return [...enumerateCountryDisplayNames(locales)].filter((n) => n.iso2 === upper).map((n) => n.name)
150
+ }
package/country/index.ts CHANGED
@@ -8,8 +8,10 @@
8
8
 
9
9
  export * from "./codes.ts"
10
10
  export * from "./country.ts"
11
+ export * from "./display-names.ts"
11
12
  export * from "./names.ts"
12
13
  export * from "./official-languages.ts"
14
+ export * from "./population.ts"
13
15
  export * from "./reference-data.ts"
14
16
  export * from "./reference.ts"
15
17
  export * from "./subdivision.ts"
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * GENERATED — do not edit by hand. Per-country population, derived from GeoNames countryInfo.txt
7
+ * (https://download.geonames.org/export/dump/countryInfo.txt, CC-BY-4.0). Estimates are
8
+ * census-vintage; a prominence race reads the magnitude, not the currency. Countries GeoNames
9
+ * declines to estimate are ABSENT, never zero.
10
+ * Regenerate with: mailwoman dev generate country-population
11
+ */
12
+
13
+ /**
14
+ * ISO 3166-1 alpha-2 → population estimate.
15
+ */
16
+ export const COUNTRY_POPULATION: Readonly<Record<string, number>> = {
17
+ AD: 77_006,
18
+ AE: 9_630_959,
19
+ AF: 37_172_386,
20
+ AG: 96_286,
21
+ AI: 13_254,
22
+ AL: 2_866_376,
23
+ AM: 3_090_500,
24
+ AN: 300_000,
25
+ AO: 30_809_762,
26
+ AR: 44_494_502,
27
+ AS: 55_465,
28
+ AT: 8_847_037,
29
+ AU: 24_992_369,
30
+ AW: 105_845,
31
+ AX: 26_711,
32
+ AZ: 10_224_900,
33
+ BA: 3_323_929,
34
+ BB: 286_641,
35
+ BD: 161_356_039,
36
+ BE: 11_422_068,
37
+ BF: 19_751_535,
38
+ BG: 7_000_039,
39
+ BH: 1_569_439,
40
+ BI: 11_175_378,
41
+ BJ: 11_485_048,
42
+ BL: 8450,
43
+ BM: 63_968,
44
+ BN: 428_962,
45
+ BO: 11_353_142,
46
+ BQ: 18_012,
47
+ BR: 209_469_333,
48
+ BS: 385_640,
49
+ BT: 754_394,
50
+ BW: 2_254_126,
51
+ BY: 9_485_386,
52
+ BZ: 383_071,
53
+ CA: 37_058_856,
54
+ CC: 628,
55
+ CD: 84_068_091,
56
+ CF: 4_666_377,
57
+ CG: 5_244_363,
58
+ CH: 8_516_543,
59
+ CI: 25_069_229,
60
+ CK: 21_388,
61
+ CL: 18_729_160,
62
+ CM: 25_216_237,
63
+ CN: 1_411_778_724,
64
+ CO: 49_648_685,
65
+ CR: 4_999_441,
66
+ CS: 10_829_175,
67
+ CU: 11_338_138,
68
+ CV: 543_767,
69
+ CW: 159_849,
70
+ CX: 1500,
71
+ CY: 1_189_265,
72
+ CZ: 10_625_695,
73
+ DE: 82_927_922,
74
+ DJ: 958_920,
75
+ DK: 5_797_446,
76
+ DM: 71_625,
77
+ DO: 10_627_165,
78
+ DZ: 42_228_429,
79
+ EC: 17_084_357,
80
+ EE: 1_320_884,
81
+ EG: 98_423_595,
82
+ EH: 273_008,
83
+ ER: 6_209_262,
84
+ ES: 46_723_749,
85
+ ET: 109_224_559,
86
+ FI: 5_518_050,
87
+ FJ: 883_483,
88
+ FK: 2638,
89
+ FM: 112_640,
90
+ FO: 48_497,
91
+ FR: 66_987_244,
92
+ GA: 2_119_275,
93
+ GB: 66_488_991,
94
+ GD: 111_454,
95
+ GE: 3_704_500,
96
+ GF: 195_506,
97
+ GG: 65_228,
98
+ GH: 29_767_108,
99
+ GI: 33_718,
100
+ GL: 56_025,
101
+ GM: 2_280_102,
102
+ GN: 12_414_318,
103
+ GP: 443_000,
104
+ GQ: 1_308_974,
105
+ GR: 10_727_668,
106
+ GS: 30,
107
+ GT: 17_247_807,
108
+ GU: 165_768,
109
+ GW: 1_874_309,
110
+ GY: 779_004,
111
+ HK: 7_396_076,
112
+ HN: 9_587_522,
113
+ HR: 3_871_833,
114
+ HT: 11_123_176,
115
+ HU: 9_768_785,
116
+ ID: 267_663_435,
117
+ IE: 4_853_506,
118
+ IL: 8_883_800,
119
+ IM: 84_077,
120
+ IN: 1_352_617_328,
121
+ IO: 4000,
122
+ IQ: 38_433_600,
123
+ IR: 81_800_269,
124
+ IS: 353_574,
125
+ IT: 60_431_283,
126
+ JE: 90_812,
127
+ JM: 2_934_855,
128
+ JO: 9_956_011,
129
+ JP: 126_529_100,
130
+ KE: 51_393_010,
131
+ KG: 6_315_800,
132
+ KH: 16_249_798,
133
+ KI: 115_847,
134
+ KM: 832_322,
135
+ KN: 52_441,
136
+ KP: 25_549_819,
137
+ KR: 51_635_256,
138
+ KW: 4_137_309,
139
+ KY: 64_174,
140
+ KZ: 18_276_499,
141
+ LA: 7_061_507,
142
+ LB: 6_848_925,
143
+ LC: 181_889,
144
+ LI: 37_910,
145
+ LK: 21_670_000,
146
+ LR: 4_818_977,
147
+ LS: 2_108_132,
148
+ LT: 2_789_533,
149
+ LU: 607_728,
150
+ LV: 1_926_542,
151
+ LY: 6_678_567,
152
+ MA: 36_029_138,
153
+ MC: 38_682,
154
+ MD: 3_545_883,
155
+ ME: 622_345,
156
+ MF: 37_264,
157
+ MG: 26_262_368,
158
+ MH: 58_413,
159
+ MK: 2_082_958,
160
+ ML: 19_077_690,
161
+ MM: 53_708_395,
162
+ MN: 3_170_208,
163
+ MO: 631_636,
164
+ MP: 56_882,
165
+ MQ: 432_900,
166
+ MR: 4_403_319,
167
+ MS: 9341,
168
+ MT: 483_530,
169
+ MU: 1_265_303,
170
+ MV: 515_696,
171
+ MW: 17_563_749,
172
+ MX: 126_190_788,
173
+ MY: 31_528_585,
174
+ MZ: 29_495_962,
175
+ NA: 2_448_255,
176
+ NC: 284_060,
177
+ NE: 22_442_948,
178
+ NF: 1828,
179
+ NG: 195_874_740,
180
+ NI: 6_465_513,
181
+ NL: 17_231_017,
182
+ NO: 5_314_336,
183
+ NP: 28_087_871,
184
+ NR: 12_704,
185
+ NU: 2166,
186
+ NZ: 4_885_500,
187
+ OM: 4_829_483,
188
+ PA: 4_176_873,
189
+ PE: 31_989_256,
190
+ PF: 277_679,
191
+ PG: 8_606_316,
192
+ PH: 106_651_922,
193
+ PK: 212_215_030,
194
+ PL: 37_978_548,
195
+ PM: 7012,
196
+ PN: 46,
197
+ PR: 3_195_153,
198
+ PS: 4_569_087,
199
+ PT: 10_281_762,
200
+ PW: 17_907,
201
+ PY: 6_956_071,
202
+ QA: 2_781_677,
203
+ RE: 776_948,
204
+ RO: 19_473_936,
205
+ RS: 6_982_084,
206
+ RU: 144_478_050,
207
+ RW: 12_301_939,
208
+ SA: 33_699_947,
209
+ SB: 652_858,
210
+ SC: 96_762,
211
+ SD: 41_801_533,
212
+ SE: 10_183_175,
213
+ SG: 5_638_676,
214
+ SH: 7460,
215
+ SI: 2_067_372,
216
+ SJ: 2550,
217
+ SK: 5_447_011,
218
+ SL: 7_650_154,
219
+ SM: 33_785,
220
+ SN: 15_854_360,
221
+ SO: 15_008_154,
222
+ SR: 575_991,
223
+ SS: 8_260_490,
224
+ ST: 197_700,
225
+ SV: 6_420_744,
226
+ SX: 40_654,
227
+ SY: 16_906_283,
228
+ SZ: 1_136_191,
229
+ TC: 37_665,
230
+ TD: 15_477_751,
231
+ TF: 140,
232
+ TG: 7_889_094,
233
+ TH: 69_428_524,
234
+ TJ: 9_100_837,
235
+ TK: 1466,
236
+ TL: 1_267_972,
237
+ TM: 5_850_908,
238
+ TN: 11_565_204,
239
+ TO: 103_197,
240
+ TR: 82_319_724,
241
+ TT: 1_389_858,
242
+ TV: 11_508,
243
+ TW: 23_451_837,
244
+ TZ: 56_318_348,
245
+ UA: 40_000_000,
246
+ UG: 42_723_139,
247
+ US: 327_167_434,
248
+ UY: 3_449_299,
249
+ UZ: 32_955_400,
250
+ VA: 921,
251
+ VC: 110_211,
252
+ VE: 28_870_195,
253
+ VG: 29_802,
254
+ VI: 106_977,
255
+ VN: 95_540_395,
256
+ VU: 292_680,
257
+ WF: 16_025,
258
+ WS: 196_130,
259
+ XK: 1_845_300,
260
+ YE: 28_498_687,
261
+ YT: 279_471,
262
+ ZA: 57_779_622,
263
+ ZM: 17_351_822,
264
+ ZW: 16_868_409,
265
+ }
@@ -26,6 +26,7 @@
26
26
  * re-keys those two existing tables into one subdivision→country view.
27
27
  */
28
28
 
29
+ import { AU_STATE_ABBREVIATIONS } from "../au/state.ts"
29
30
  import { CA_PROVINCES } from "../ca/province.ts"
30
31
  import { foldName } from "../normalize.ts"
31
32
  import { US_STATE_BY_ABBREVIATION } from "../us/state.ts"
@@ -91,3 +92,61 @@ export function matchSubdivision(token: string | null | undefined): SubdivisionM
91
92
 
92
93
  return SUBDIVISION_LOOKUP.get(foldName(token)) ?? null
93
94
  }
95
+
96
+ /**
97
+ * Per-country subdivision lookups for callers that already KNOW the country. Kept separate from
98
+ * {@link SUBDIVISION_LOOKUP} on purpose: the combined map is only unambiguous because the US and CA sets are disjoint,
99
+ * and Australia breaks that property twice — `WA` collides with Washington and `NT` with the Northwest Territories. A
100
+ * country-scoped caller (the admin-coherence check reads the winner's resolver-stamped country) dissolves the collision
101
+ * instead of arbitrating it.
102
+ */
103
+ const SCOPED_SUBDIVISION_LOOKUP: ReadonlyMap<string, ReadonlyMap<string, SubdivisionMatch>> = (() => {
104
+ const byCountry = new Map<string, Map<string, SubdivisionMatch>>()
105
+
106
+ const put = (country: string, key: string, match: SubdivisionMatch): void => {
107
+ const folded = foldName(key)
108
+
109
+ if (!folded.length) return
110
+
111
+ const scoped = byCountry.get(country) ?? new Map<string, SubdivisionMatch>()
112
+
113
+ byCountry.set(country, scoped)
114
+
115
+ if (!scoped.has(folded)) {
116
+ scoped.set(folded, match)
117
+ }
118
+ }
119
+
120
+ for (const [code, name] of Object.entries(US_STATE_BY_ABBREVIATION)) {
121
+ const match: SubdivisionMatch = { code, name, country: "US" }
122
+ put("US", code, match)
123
+ put("US", name, match)
124
+ }
125
+
126
+ for (const info of Object.values(CA_PROVINCES)) {
127
+ const match: SubdivisionMatch = { code: info.code, name: info.name, country: "CA" }
128
+ put("CA", info.code, match)
129
+ put("CA", info.name, match)
130
+ put("CA", info.french, match)
131
+ }
132
+
133
+ for (const [code, name] of Object.entries(AU_STATE_ABBREVIATIONS)) {
134
+ const match: SubdivisionMatch = { code, name, country: "AU" }
135
+ put("AU", code, match)
136
+ put("AU", name, match)
137
+ }
138
+
139
+ return byCountry
140
+ })()
141
+
142
+ /**
143
+ * Resolve a subdivision surface form WITHIN one country. Same folding and return shape as {@link matchSubdivision}, but
144
+ * scoped: `WA` under `AU` is Western Australia, under `US` Washington, and under any other country null. Use this
145
+ * whenever the country is already established; the unscoped lookup exists for the address-line case where the
146
+ * subdivision itself is the country evidence.
147
+ */
148
+ export function matchSubdivisionIn(countryAlpha2: string, token: string | null | undefined): SubdivisionMatch | null {
149
+ if (!token || typeof token !== "string") return null
150
+
151
+ return SCOPED_SUBDIVISION_LOOKUP.get(countryAlpha2.toUpperCase())?.get(foldName(token)) ?? null
152
+ }
package/de/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * The German address system (Deutsche Post / ISO 3166-2:DE): street types, postal codes
6
+ * The German address system (Deutsche Post / ISO 3166-2:DE): street types, postcodes
7
7
  * (Postleitzahl), and the federal states (Bundesländer).
8
8
  */
9
9