@mailwoman/codex 9.1.0 → 9.3.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.
- package/README.md +51 -8
- package/{address-system-conventions.ts → lib/address-system-conventions.ts} +9 -9
- package/{au → lib/au}/index.ts +4 -4
- package/{ca → lib/ca}/index.ts +4 -4
- package/{ca → lib/ca}/postal-code.ts +12 -12
- package/{ca → lib/ca}/province.ts +1 -1
- package/{ca → lib/ca}/street-type.ts +10 -12
- package/{country → lib/country}/codes.ts +1 -1
- package/{country → lib/country}/country.ts +43 -14
- package/lib/country/display-names.ts +150 -0
- package/lib/country/index.ts +17 -0
- package/lib/country/population.ts +265 -0
- package/{country → lib/country}/reference.ts +2 -2
- package/{country → lib/country}/subdivision.ts +62 -3
- package/{de → lib/de}/index.ts +4 -4
- package/{de → lib/de}/postleitzahl.ts +3 -3
- package/{es → lib/es}/codigo-postal.ts +24 -6
- package/{es → lib/es}/index.ts +1 -1
- package/{fr → lib/fr}/cedex.ts +1 -1
- package/{fr → lib/fr}/code-postal.ts +8 -8
- package/{fr → lib/fr}/departement.ts +5 -2
- package/{fr → lib/fr}/index.ts +6 -6
- package/{fr → lib/fr}/region.ts +1 -1
- package/{fr → lib/fr}/voie.ts +5 -13
- package/{gb → lib/gb}/country.ts +2 -10
- package/{gb → lib/gb}/index.ts +4 -4
- package/{gb → lib/gb}/postcode-area.ts +1 -1
- package/{gb → lib/gb}/street-type.ts +1 -1
- package/{index.ts → lib/index.ts} +22 -12
- package/{it → lib/it}/cap.ts +23 -5
- package/{it → lib/it}/index.ts +1 -1
- package/{jp → lib/jp}/index.ts +4 -4
- package/{jp → lib/jp}/postal-code.ts +11 -11
- package/{jp → lib/jp}/prefecture.ts +7 -5
- package/{normalize.ts → lib/normalize.ts} +15 -1
- package/{nz → lib/nz}/delivery-service.ts +1 -1
- package/{nz → lib/nz}/index.ts +2 -2
- package/lib/postcode-systems.ts +198 -0
- package/{tools → lib/tools}/build-country-surface-lexicon.ts +10 -9
- package/{tools → lib/tools}/export-country-surfaces.ts +8 -8
- package/lib/tools/generate-country-population.ts +142 -0
- package/{tools → lib/tools}/generate-country-reference.ts +8 -7
- package/{tools → lib/tools}/generate-official-languages.ts +15 -13
- package/{tools → lib/tools}/index.ts +4 -4
- package/{us → lib/us}/index.ts +8 -8
- package/{us → lib/us}/po-box.ts +1 -1
- package/{us → lib/us}/street-directional.ts +2 -2
- package/{us → lib/us}/street-suffix.ts +1 -1
- package/{us → lib/us}/unit-designator.ts +1 -1
- package/{us → lib/us}/zipcode.ts +2 -2
- package/out/address-system-conventions.d.ts +1 -1
- package/out/address-system-conventions.d.ts.map +1 -1
- package/out/address-system-conventions.js +8 -8
- package/out/address-system-conventions.js.map +1 -1
- package/out/au/delivery-service.d.ts.map +1 -1
- package/out/au/delivery-service.js.map +1 -1
- package/out/au/index.d.ts +4 -4
- package/out/au/index.d.ts.map +1 -1
- package/out/au/index.js +4 -4
- package/out/au/index.js.map +1 -1
- package/out/au/level-designator.d.ts.map +1 -1
- package/out/au/level-designator.js.map +1 -1
- package/out/au/postcode.d.ts.map +1 -1
- package/out/au/postcode.js.map +1 -1
- package/out/au/state.d.ts.map +1 -1
- package/out/au/state.js.map +1 -1
- package/out/ca/index.d.ts +4 -4
- package/out/ca/index.d.ts.map +1 -1
- package/out/ca/index.js +4 -4
- package/out/ca/index.js.map +1 -1
- package/out/ca/postal-code.d.ts +11 -11
- package/out/ca/postal-code.d.ts.map +1 -1
- package/out/ca/postal-code.js +9 -9
- package/out/ca/postal-code.js.map +1 -1
- package/out/ca/province.d.ts.map +1 -1
- package/out/ca/province.js +1 -1
- package/out/ca/province.js.map +1 -1
- package/out/ca/street-type.d.ts.map +1 -1
- package/out/ca/street-type.js +9 -12
- package/out/ca/street-type.js.map +1 -1
- package/out/country/codes.d.ts.map +1 -1
- package/out/country/codes.js.map +1 -1
- package/out/country/country.d.ts +13 -11
- package/out/country/country.d.ts.map +1 -1
- package/out/country/country.js +35 -13
- package/out/country/country.js.map +1 -1
- package/out/country/display-names.d.ts +67 -0
- package/out/country/display-names.d.ts.map +1 -0
- package/out/country/display-names.js +118 -0
- package/out/country/display-names.js.map +1 -0
- package/out/country/index.d.ts +10 -8
- package/out/country/index.d.ts.map +1 -1
- package/out/country/index.js +10 -8
- package/out/country/index.js.map +1 -1
- package/out/country/names.d.ts.map +1 -1
- package/out/country/names.js.map +1 -1
- package/out/country/official-languages.d.ts.map +1 -1
- package/out/country/official-languages.js.map +1 -1
- package/out/country/population.d.ts +16 -0
- package/out/country/population.d.ts.map +1 -0
- package/out/country/population.js +265 -0
- package/out/country/population.js.map +1 -0
- package/out/country/reference-data.d.ts.map +1 -1
- package/out/country/reference-data.js.map +1 -1
- package/out/country/reference.d.ts +1 -1
- package/out/country/reference.d.ts.map +1 -1
- package/out/country/reference.js +2 -2
- package/out/country/reference.js.map +1 -1
- package/out/country/subdivision.d.ts +7 -0
- package/out/country/subdivision.d.ts.map +1 -1
- package/out/country/subdivision.js +52 -3
- package/out/country/subdivision.js.map +1 -1
- package/out/de/bundesland.d.ts.map +1 -1
- package/out/de/bundesland.js.map +1 -1
- package/out/de/index.d.ts +4 -4
- package/out/de/index.d.ts.map +1 -1
- package/out/de/index.js +4 -4
- package/out/de/index.js.map +1 -1
- package/out/de/postleitzahl.d.ts +3 -3
- package/out/de/postleitzahl.d.ts.map +1 -1
- package/out/de/postleitzahl.js +2 -2
- package/out/de/postleitzahl.js.map +1 -1
- package/out/de/street-type.d.ts.map +1 -1
- package/out/de/street-type.js.map +1 -1
- package/out/es/codigo-postal.d.ts +13 -2
- package/out/es/codigo-postal.d.ts.map +1 -1
- package/out/es/codigo-postal.js +21 -4
- package/out/es/codigo-postal.js.map +1 -1
- package/out/es/index.d.ts +1 -1
- package/out/es/index.d.ts.map +1 -1
- package/out/es/index.js +1 -1
- package/out/es/index.js.map +1 -1
- package/out/fr/cedex.d.ts +1 -1
- package/out/fr/cedex.d.ts.map +1 -1
- package/out/fr/cedex.js +1 -1
- package/out/fr/cedex.js.map +1 -1
- package/out/fr/code-postal.d.ts +7 -7
- package/out/fr/code-postal.d.ts.map +1 -1
- package/out/fr/code-postal.js +8 -8
- package/out/fr/code-postal.js.map +1 -1
- package/out/fr/departement.d.ts +2 -2
- package/out/fr/departement.d.ts.map +1 -1
- package/out/fr/departement.js +3 -0
- package/out/fr/departement.js.map +1 -1
- package/out/fr/index.d.ts +6 -6
- package/out/fr/index.d.ts.map +1 -1
- package/out/fr/index.js +6 -6
- package/out/fr/index.js.map +1 -1
- package/out/fr/region.d.ts.map +1 -1
- package/out/fr/region.js +1 -1
- package/out/fr/region.js.map +1 -1
- package/out/fr/voie.d.ts.map +1 -1
- package/out/fr/voie.js +4 -11
- package/out/fr/voie.js.map +1 -1
- package/out/gb/country.d.ts.map +1 -1
- package/out/gb/country.js +1 -9
- package/out/gb/country.js.map +1 -1
- package/out/gb/index.d.ts +4 -4
- package/out/gb/index.d.ts.map +1 -1
- package/out/gb/index.js +4 -4
- package/out/gb/index.js.map +1 -1
- package/out/gb/postcode-area.d.ts +1 -1
- package/out/gb/postcode-area.d.ts.map +1 -1
- package/out/gb/postcode-area.js.map +1 -1
- package/out/gb/postcode.d.ts.map +1 -1
- package/out/gb/postcode.js.map +1 -1
- package/out/gb/street-type.d.ts +1 -1
- package/out/gb/street-type.d.ts.map +1 -1
- package/out/gb/street-type.js +1 -1
- package/out/gb/street-type.js.map +1 -1
- package/out/index.d.ts +12 -12
- package/out/index.d.ts.map +1 -1
- package/out/index.js +12 -12
- package/out/index.js.map +1 -1
- package/out/it/cap.d.ts +13 -2
- package/out/it/cap.d.ts.map +1 -1
- package/out/it/cap.js +20 -3
- package/out/it/cap.js.map +1 -1
- package/out/it/index.d.ts +1 -1
- package/out/it/index.d.ts.map +1 -1
- package/out/it/index.js +1 -1
- package/out/it/index.js.map +1 -1
- package/out/jp/address-unit.d.ts.map +1 -1
- package/out/jp/address-unit.js.map +1 -1
- package/out/jp/index.d.ts +4 -4
- package/out/jp/index.d.ts.map +1 -1
- package/out/jp/index.js +4 -4
- package/out/jp/index.js.map +1 -1
- package/out/jp/postal-code.d.ts +11 -11
- package/out/jp/postal-code.d.ts.map +1 -1
- package/out/jp/postal-code.js +8 -8
- package/out/jp/postal-code.js.map +1 -1
- package/out/jp/prefecture.d.ts +4 -1
- package/out/jp/prefecture.d.ts.map +1 -1
- package/out/jp/prefecture.js +6 -5
- package/out/jp/prefecture.js.map +1 -1
- package/out/level-semantics.d.ts.map +1 -1
- package/out/level-semantics.js.map +1 -1
- package/out/normalize.d.ts +8 -0
- package/out/normalize.d.ts.map +1 -1
- package/out/normalize.js +14 -1
- package/out/normalize.js.map +1 -1
- package/out/nz/delivery-service.d.ts +1 -1
- package/out/nz/delivery-service.d.ts.map +1 -1
- package/out/nz/delivery-service.js +1 -1
- package/out/nz/delivery-service.js.map +1 -1
- package/out/nz/index.d.ts +2 -2
- package/out/nz/index.d.ts.map +1 -1
- package/out/nz/index.js +2 -2
- package/out/nz/index.js.map +1 -1
- package/out/nz/postcode.d.ts.map +1 -1
- package/out/nz/postcode.js.map +1 -1
- package/out/postcode-systems.d.ts +99 -4
- package/out/postcode-systems.d.ts.map +1 -1
- package/out/postcode-systems.js +132 -11
- package/out/postcode-systems.js.map +1 -1
- package/out/tools/build-country-surface-lexicon.d.ts +1 -1
- package/out/tools/build-country-surface-lexicon.d.ts.map +1 -1
- package/out/tools/build-country-surface-lexicon.js +10 -9
- package/out/tools/build-country-surface-lexicon.js.map +1 -1
- package/out/tools/export-country-surfaces.d.ts +1 -1
- package/out/tools/export-country-surfaces.d.ts.map +1 -1
- package/out/tools/export-country-surfaces.js +7 -7
- package/out/tools/export-country-surfaces.js.map +1 -1
- package/out/tools/generate-country-population.d.ts +42 -0
- package/out/tools/generate-country-population.d.ts.map +1 -0
- package/out/tools/generate-country-population.js +106 -0
- package/out/tools/generate-country-population.js.map +1 -0
- package/out/tools/generate-country-reference.d.ts.map +1 -1
- package/out/tools/generate-country-reference.js +8 -8
- package/out/tools/generate-country-reference.js.map +1 -1
- package/out/tools/generate-official-languages.d.ts.map +1 -1
- package/out/tools/generate-official-languages.js +14 -11
- package/out/tools/generate-official-languages.js.map +1 -1
- package/out/tools/index.d.ts +4 -4
- package/out/tools/index.d.ts.map +1 -1
- package/out/tools/index.js +4 -4
- package/out/tools/index.js.map +1 -1
- package/out/us/floor-designator.d.ts.map +1 -1
- package/out/us/floor-designator.js.map +1 -1
- package/out/us/index.d.ts +8 -8
- package/out/us/index.d.ts.map +1 -1
- package/out/us/index.js +8 -8
- package/out/us/index.js.map +1 -1
- package/out/us/military-address.d.ts.map +1 -1
- package/out/us/military-address.js.map +1 -1
- package/out/us/po-box.d.ts +1 -1
- package/out/us/po-box.d.ts.map +1 -1
- package/out/us/po-box.js +1 -1
- package/out/us/po-box.js.map +1 -1
- package/out/us/state.d.ts.map +1 -1
- package/out/us/state.js.map +1 -1
- package/out/us/street-directional.d.ts.map +1 -1
- package/out/us/street-directional.js +2 -2
- package/out/us/street-directional.js.map +1 -1
- package/out/us/street-suffix.d.ts +1 -1
- package/out/us/street-suffix.d.ts.map +1 -1
- package/out/us/street-suffix.js +1 -1
- package/out/us/street-suffix.js.map +1 -1
- package/out/us/unit-designator.d.ts +1 -1
- package/out/us/unit-designator.d.ts.map +1 -1
- package/out/us/unit-designator.js +1 -1
- package/out/us/unit-designator.js.map +1 -1
- package/out/us/zipcode.d.ts +2 -2
- package/out/us/zipcode.d.ts.map +1 -1
- package/out/us/zipcode.js +2 -2
- package/out/us/zipcode.js.map +1 -1
- package/package.json +129 -6
- package/country/index.ts +0 -15
- package/postcode-systems.ts +0 -70
- /package/{au → lib/au}/delivery-service.ts +0 -0
- /package/{au → lib/au}/level-designator.ts +0 -0
- /package/{au → lib/au}/postcode.ts +0 -0
- /package/{au → lib/au}/state.ts +0 -0
- /package/{country → lib/country}/names.ts +0 -0
- /package/{country → lib/country}/official-languages.ts +0 -0
- /package/{country → lib/country}/reference-data.ts +0 -0
- /package/{de → lib/de}/bundesland.ts +0 -0
- /package/{de → lib/de}/street-type.ts +0 -0
- /package/{gb → lib/gb}/postcode.ts +0 -0
- /package/{jp → lib/jp}/address-unit.ts +0 -0
- /package/{level-semantics.ts → lib/level-semantics.ts} +0 -0
- /package/{nz → lib/nz}/postcode.ts +0 -0
- /package/{us → lib/us}/floor-designator.ts +0 -0
- /package/{us → lib/us}/military-address.ts +0 -0
- /package/{us → lib/us}/state.ts +0 -0
- /package/{us → lib/us}/street-suffix.json +0 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ fr.postcodePattern // → /^\d{5}$/
|
|
|
23
23
|
|
|
24
24
|
// US ZIP code branded type
|
|
25
25
|
import { us } from "@mailwoman/codex"
|
|
26
|
-
const zip: us.ZipCode = "94043" // branded, not
|
|
26
|
+
const zip: us.ZipCode = "94043" // branded, not string alone
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Supported address systems
|
|
@@ -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,22 +49,64 @@ import { lookupStreetSuffix } from "@mailwoman/codex/us"
|
|
|
49
49
|
```ts
|
|
50
50
|
import { candidateSystemsForPostcode } from "@mailwoman/codex"
|
|
51
51
|
|
|
52
|
-
// Which systems could
|
|
53
|
-
candidateSystemsForPostcode("94043") // → ["us"]
|
|
54
|
-
candidateSystemsForPostcode("
|
|
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 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,
|
|
64
106
|
no network. Suitable for bundling into browser and edge environments.
|
|
65
107
|
- **Branded types.** ZIP codes, postcodes, and abbreviations carry nominal types
|
|
66
108
|
so the type system catches locale mismatches at compile time.
|
|
67
|
-
- **
|
|
109
|
+
- **One definition.** The resolver, the decoder's convention masks, the
|
|
68
110
|
corpus synthesis layer, and the matcher all import from `@mailwoman/codex`.
|
|
69
111
|
|
|
70
112
|
## The normative tier (codex vs the libpostal dictionaries)
|
|
@@ -78,7 +120,7 @@ be merged:
|
|
|
78
120
|
invariance transforms, and formatting (rendering `N` vs `North` requires knowing
|
|
79
121
|
which form the authority prints).
|
|
80
122
|
- **The libpostal dictionaries** (`core/data/libpostal/dictionaries/`, Pelias
|
|
81
|
-
lineage) **are descriptive**: everything people
|
|
123
|
+
lineage) **are descriptive**: everything people write, including forms no
|
|
82
124
|
authority recognizes (`en/directionals.txt` lists `lower`/`upper`/`central`).
|
|
83
125
|
Their consumers are recall-shaped — evidence-lexicon curation laws, street
|
|
84
126
|
decomposition for training gold, the street-morphology FST. See the README in
|
|
@@ -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
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* by the caller) — an undetected system parses exactly as before.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { CODE_POSTAL_PATTERN } from "
|
|
22
|
-
import { UK_POSTCODE_PATTERN } from "
|
|
23
|
-
import type { SystemCode } from "
|
|
21
|
+
import { CODE_POSTAL_PATTERN } from "#fr/code-postal"
|
|
22
|
+
import { UK_POSTCODE_PATTERN } from "#gb/postcode"
|
|
23
|
+
import type { SystemCode } from "#postcode-systems"
|
|
24
24
|
|
|
25
25
|
export interface AddressSystemConventions {
|
|
26
26
|
/**
|
|
@@ -48,11 +48,11 @@ export const ADDRESS_SYSTEM_CONVENTIONS: Partial<Record<SystemCode, AddressSyste
|
|
|
48
48
|
* street-suffix class; Pub-28's suffix decomposition has no French counterpart).
|
|
49
49
|
*
|
|
50
50
|
* Provenance / why this is NOT a blanket prefix+suffix forbid (#719, 2026-06-18): an earlier model mis-tagged the
|
|
51
|
-
* leading "Rue" as a US-style `street_suffix` (RUE is a Pub-28 suffix variant) — the 2026-06-10 v1.1.0
|
|
52
|
-
* forbade BOTH affix tags to stop that leakage. That forbid was correct for THAT model but became a live
|
|
53
|
-
* bug for the current one: the shipped model (v1.5.0) emits the FR `street_prefix` correctly, but the
|
|
54
|
-
* mask was a hard −1e9 on every B-/I-street_prefix emission, so the detected-FR parse could never KEEP a
|
|
55
|
-
* destroyed `street_prefix` wholesale (measured on data/eval/external/ fr-street-prefix-real.jsonl at
|
|
51
|
+
* leading "Rue" as a US-style `street_suffix` (RUE is a Pub-28 suffix variant) — the 2026-06-10 v1.1.0 promotion eval
|
|
52
|
+
* — so #511 forbade BOTH affix tags to stop that leakage. That forbid was correct for THAT model but became a live
|
|
53
|
+
* production bug for the current one: the shipped model (v1.5.0) emits the FR `street_prefix` correctly, but the
|
|
54
|
+
* conventions mask was a hard −1e9 on every B-/I-street_prefix emission, so the detected-FR parse could never KEEP a
|
|
55
|
+
* prefix — it destroyed `street_prefix` wholesale (measured on data/eval/external/ fr-street-prefix-real.jsonl at
|
|
56
56
|
* conventions=auto: F1 0.0 with the forbid on → 80.0 with it off; the larger real-FR eval reported the same collapse,
|
|
57
57
|
* ~96 → ~0.6). We keep ONLY `street_suffix` forbidden: the current model with the forbid OFF shows zero FR
|
|
58
58
|
* street_suffix leakage (fp=0 on that same slice) and FR has no trailing street suffix, so the constraint costs
|
|
@@ -72,7 +72,7 @@ export const ADDRESS_SYSTEM_CONVENTIONS: Partial<Record<SystemCode, AddressSyste
|
|
|
72
72
|
* the raw text — exactly the shape-INVALID class `postcodePattern` exists to flag.
|
|
73
73
|
*
|
|
74
74
|
* Provenance (#1275, 2026-07-24): on the GB golden board's 106 postcode rows under the en-gb bundle, the clip class
|
|
75
|
-
* (parsed postcode = proper suffix of the truth) was 44/106 with this row absent — the repair
|
|
75
|
+
* (parsed postcode = proper suffix of the truth) was 44/106 with this row absent — the repair check never opened
|
|
76
76
|
* because `conventionsForSystem("gb")` returned null. With the repair reachable, exact 26 → 83 and the clip class
|
|
77
77
|
* goes to zero. No `forbiddenTags`: no measured GB-ungrammatical tag class exists (the FR street_suffix forbid's
|
|
78
78
|
* lesson — a forbid needs measured zero-cost receipts, and GB street grammar shares the trailing-suffix family with
|
package/{au → lib/au}/index.ts
RENAMED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* territories, and the 4-digit postcode.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
export * from "
|
|
13
|
-
export * from "
|
|
14
|
-
export * from "
|
|
15
|
-
export * from "
|
|
12
|
+
export * from "#au/delivery-service"
|
|
13
|
+
export * from "#au/level-designator"
|
|
14
|
+
export * from "#au/postcode"
|
|
15
|
+
export * from "#au/state"
|
package/{ca → lib/ca}/index.ts
RENAMED
|
@@ -4,9 +4,9 @@
|
|
|
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
|
-
*
|
|
7
|
+
* postcodes, and the provinces and territories.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export * from "
|
|
11
|
-
export * from "
|
|
12
|
-
export * from "
|
|
10
|
+
export * from "#ca/postal-code"
|
|
11
|
+
export * from "#ca/province"
|
|
12
|
+
export * from "#ca/street-type"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* Canadian
|
|
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
|
|
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.
|
|
@@ -27,21 +27,21 @@
|
|
|
27
27
|
|
|
28
28
|
import type { Tagged } from "type-fest"
|
|
29
29
|
|
|
30
|
-
import type { CanadianProvinceCode } from "
|
|
30
|
+
import type { CanadianProvinceCode } from "#ca/province"
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Characters in a Canadian
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
106
|
-
* letters, the `["NT", "NU"]` pair for the shared `X`, and null if the input is not a valid Canadian
|
|
107
|
-
*
|
|
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
|
|
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
|
*/
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* regions, the Canadian code IS a surface form, not just a resolver key.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { foldName } from "
|
|
19
|
+
import { foldName } from "#normalize"
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Per-province record: ISO 3166-2:CA code, English name, and the co-official French name.
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* Ouest = West is recognized.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { foldToken } from "#normalize"
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* English Canadian street-type words (Canada Post's recognized set, lowercase). Appear as the TRAILING token of an
|
|
28
30
|
* English street name (`Maple Avenue`, `Sunset Crescent`).
|
|
@@ -53,7 +55,7 @@ export const CA_STREET_TYPES_EN: ReadonlySet<string> = new Set([
|
|
|
53
55
|
"green",
|
|
54
56
|
"bay",
|
|
55
57
|
"cove",
|
|
56
|
-
"
|
|
58
|
+
"check",
|
|
57
59
|
"point",
|
|
58
60
|
"ridge",
|
|
59
61
|
"view",
|
|
@@ -86,14 +88,10 @@ export const CA_STREET_TYPES_FR: ReadonlySet<string> = new Set([
|
|
|
86
88
|
])
|
|
87
89
|
|
|
88
90
|
/**
|
|
89
|
-
*
|
|
91
|
+
* {@link foldToken}, letters only, so `Côte`/`cote`, `Allée`/`allee`, `Crescent`/`crescent` key alike.
|
|
90
92
|
*/
|
|
91
|
-
function
|
|
92
|
-
return s
|
|
93
|
-
.toLowerCase()
|
|
94
|
-
.normalize("NFD")
|
|
95
|
-
.replaceAll(/[\u0300-\u036F]/g, "")
|
|
96
|
-
.replaceAll(/[^a-z]/g, "")
|
|
93
|
+
function foldLetters(s: string): string {
|
|
94
|
+
return foldToken(s).replaceAll(/[^a-z]/g, "")
|
|
97
95
|
}
|
|
98
96
|
|
|
99
97
|
/**
|
|
@@ -103,11 +101,11 @@ const STREET_WORD_SET: ReadonlySet<string> = (() => {
|
|
|
103
101
|
const out = new Set<string>()
|
|
104
102
|
|
|
105
103
|
for (const w of CA_STREET_TYPES_EN) {
|
|
106
|
-
out.add(
|
|
104
|
+
out.add(foldLetters(w))
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
for (const w of CA_STREET_TYPES_FR) {
|
|
110
|
-
out.add(
|
|
108
|
+
out.add(foldLetters(w))
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
return out
|
|
@@ -120,7 +118,7 @@ const STREET_WORD_SET: ReadonlySet<string> = (() => {
|
|
|
120
118
|
*/
|
|
121
119
|
export function isCanadianStreetWord(token: unknown): boolean {
|
|
122
120
|
if (typeof token !== "string") return false
|
|
123
|
-
const t =
|
|
121
|
+
const t = foldLetters(token)
|
|
124
122
|
|
|
125
123
|
return t.length > 0 && STREET_WORD_SET.has(t)
|
|
126
124
|
}
|
|
@@ -154,7 +152,7 @@ export const CA_DIRECTIONALS: Record<string, "N" | "S" | "E" | "W"> = {
|
|
|
154
152
|
*/
|
|
155
153
|
export function isCanadianDirectional(token: unknown): boolean {
|
|
156
154
|
if (typeof token !== "string") return false
|
|
157
|
-
const t =
|
|
155
|
+
const t = foldLetters(token)
|
|
158
156
|
|
|
159
157
|
if (!t.length) return false
|
|
160
158
|
|
|
@@ -3,18 +3,19 @@
|
|
|
3
3
|
* @license AGPL-3.0
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*
|
|
6
|
-
* Country recognition for the `country` parity
|
|
6
|
+
* Country recognition for the `country` parity change. The ISO 3166-1 base (names + alpha-2/alpha-3)
|
|
7
7
|
* is salvaged from isp-nexus `spatial/countries` ({@link ./names.ts}, {@link ./codes.ts}); this
|
|
8
8
|
* adds the layer ISO doesn't carry — the **surface forms** addresses actually use (endonyms +
|
|
9
9
|
* common abbreviations: "USA"/"United States"/"U.S."; "Deutschland"/"Germany"; "España"/"Spain")
|
|
10
|
-
* — plus a {@link matchCountry} resolver the corpus country-
|
|
10
|
+
* — plus a {@link matchCountry} resolver the corpus country-extract + parsing reuse. Same shape as
|
|
11
11
|
* the other codex matchers (street-suffix, directional, po-box).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { Alpha3ToCountryRecord, CountryISO2 } from "
|
|
15
|
-
import type { CountryName } from "
|
|
14
|
+
import { Alpha3ToCountryRecord, CountryISO2 } from "#country/codes"
|
|
15
|
+
import type { CountryName } from "#country/names"
|
|
16
|
+
import { foldName } from "#normalize"
|
|
16
17
|
|
|
17
|
-
export { Alpha3ToCountryRecord, CountryISO2, type CountryISO3 } from "
|
|
18
|
+
export { Alpha3ToCountryRecord, CountryISO2, type CountryISO3 } from "#country/codes"
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Common real-address surface forms per ISO 3166-1 alpha-2, **canonical English name first** then endonym +
|
|
@@ -51,9 +52,11 @@ export const ISO2_TO_NAME: ReadonlyMap<string, CountryName> = new Map(
|
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
* Any recognized country surface form / canonical name / alpha-2 / alpha-3 → alpha-2 code. Built once at module load
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* Any recognized country surface form / canonical name / alpha-2 / alpha-3 → alpha-2 code. Built once at module load.
|
|
56
|
+
* Each surface contributes its lowercased key AND its {@link foldName}-folded key when the fold leaves anything — a
|
|
57
|
+
* non-Latin surface like `日本` survives only as its lowercased self — so accented and punctuated variants resolve.
|
|
58
|
+
* Canonical names + codes from the ISO base, plus the curated surface forms (surface forms win on collision — they're
|
|
59
|
+
* the address-facing spellings).
|
|
57
60
|
*/
|
|
58
61
|
export const COUNTRY_LOOKUP: ReadonlyMap<string, string> = (() => {
|
|
59
62
|
const out = new Map<string, string>()
|
|
@@ -64,6 +67,12 @@ export const COUNTRY_LOOKUP: ReadonlyMap<string, string> = (() => {
|
|
|
64
67
|
if (key && !out.has(key)) {
|
|
65
68
|
out.set(key, iso2)
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
const folded = foldName(k)
|
|
72
|
+
|
|
73
|
+
if (folded && !out.has(folded)) {
|
|
74
|
+
out.set(folded, iso2)
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
// ISO base: canonical name + alpha-2 + alpha-3.
|
|
@@ -88,12 +97,32 @@ export const COUNTRY_LOOKUP: ReadonlyMap<string, string> = (() => {
|
|
|
88
97
|
for (const [iso2, forms] of Object.entries(COUNTRY_SURFACE_FORMS)) {
|
|
89
98
|
for (const f of forms) {
|
|
90
99
|
out.set(f.trim().toLowerCase(), iso2)
|
|
100
|
+
|
|
101
|
+
const folded = foldName(f)
|
|
102
|
+
|
|
103
|
+
if (folded) {
|
|
104
|
+
out.set(folded, iso2)
|
|
105
|
+
}
|
|
91
106
|
}
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
return out
|
|
95
110
|
})()
|
|
96
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Probe the lookup the way it is keyed: the lowercased surface first, then the {@link foldName} fold when it is
|
|
114
|
+
* non-empty.
|
|
115
|
+
*/
|
|
116
|
+
function probeCountry(token: string): string | undefined {
|
|
117
|
+
const direct = COUNTRY_LOOKUP.get(token.trim().toLowerCase())
|
|
118
|
+
|
|
119
|
+
if (direct) return direct
|
|
120
|
+
|
|
121
|
+
const folded = foldName(token)
|
|
122
|
+
|
|
123
|
+
return folded ? COUNTRY_LOOKUP.get(folded) : undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
/**
|
|
98
127
|
* Result of a country match: the alpha-2 code, the canonical English name, and the matched surface.
|
|
99
128
|
*/
|
|
@@ -104,13 +133,13 @@ export interface CountryMatch {
|
|
|
104
133
|
}
|
|
105
134
|
|
|
106
135
|
/**
|
|
107
|
-
* Resolve a token (surface form, canonical name, alpha-2, or alpha-3) to a country. Case
|
|
108
|
-
* unrecognized. Multi-word names ("United States", "Great Britain") must be
|
|
109
|
-
* decides the span; this matches it.
|
|
136
|
+
* Resolve a token (surface form, canonical name, alpha-2, or alpha-3) to a country. Case-, accent-, and
|
|
137
|
+
* punctuation-insensitive. Returns null if unrecognized. Multi-word names ("United States", "Great Britain") must be
|
|
138
|
+
* passed as the whole phrase — the caller decides the span; this matches it.
|
|
110
139
|
*/
|
|
111
140
|
export function matchCountry(token: string | null | undefined): CountryMatch | null {
|
|
112
141
|
if (!token || typeof token !== "string") return null
|
|
113
|
-
const iso2 =
|
|
142
|
+
const iso2 = probeCountry(token)
|
|
114
143
|
|
|
115
144
|
if (!iso2) return null
|
|
116
145
|
|
|
@@ -121,11 +150,11 @@ export function matchCountry(token: string | null | undefined): CountryMatch | n
|
|
|
121
150
|
* Case-insensitive check: is the token any recognized country form?
|
|
122
151
|
*/
|
|
123
152
|
export function isCountryToken(token: unknown): boolean {
|
|
124
|
-
return typeof token === "string" &&
|
|
153
|
+
return typeof token === "string" && probeCountry(token) !== undefined
|
|
125
154
|
}
|
|
126
155
|
|
|
127
156
|
/**
|
|
128
|
-
* The preferred render forms for an alpha-2 (canonical first), for synth
|
|
157
|
+
* The preferred render forms for an alpha-2 (canonical first), for synth extracts. Empty if none curated.
|
|
129
158
|
*/
|
|
130
159
|
export function countrySurfaceForms(iso2: string): readonly string[] {
|
|
131
160
|
return (COUNTRY_SURFACE_FORMS as Record<string, readonly string[]>)[iso2.toUpperCase()] ?? []
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* ISO 3166-1 country reference + surface-form recognition (the `country` parity change).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from "#country/codes"
|
|
10
|
+
export * from "#country/country"
|
|
11
|
+
export * from "#country/display-names"
|
|
12
|
+
export * from "#country/names"
|
|
13
|
+
export * from "#country/official-languages"
|
|
14
|
+
export * from "#country/population"
|
|
15
|
+
export * from "#country/reference-data"
|
|
16
|
+
export * from "#country/reference"
|
|
17
|
+
export * from "#country/subdivision"
|