@mailwoman/codex 7.1.0 → 7.2.1

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 (57) hide show
  1. package/address-system-conventions.ts +68 -0
  2. package/au/delivery-service.ts +179 -0
  3. package/au/index.ts +15 -0
  4. package/au/level-designator.ts +209 -0
  5. package/au/postcode.ts +51 -0
  6. package/au/state.ts +35 -0
  7. package/ca/index.ts +12 -0
  8. package/ca/postal-code.ts +121 -0
  9. package/ca/province.ts +99 -0
  10. package/ca/street-type.ts +167 -0
  11. package/country/codes.ts +534 -0
  12. package/country/country.ts +125 -0
  13. package/country/index.ts +14 -0
  14. package/country/names.ts +274 -0
  15. package/country/official-languages.ts +397 -0
  16. package/country/reference-data.ts +267 -0
  17. package/country/reference.ts +47 -0
  18. package/de/bundesland.ts +102 -0
  19. package/de/index.ts +12 -0
  20. package/de/postleitzahl.ts +91 -0
  21. package/de/street-type.ts +83 -0
  22. package/fr/cedex.ts +56 -0
  23. package/fr/code-postal.ts +105 -0
  24. package/fr/departement.ts +142 -0
  25. package/fr/index.ts +14 -0
  26. package/fr/region.ts +93 -0
  27. package/fr/voie.ts +98 -0
  28. package/gb/country.ts +74 -0
  29. package/gb/index.ts +14 -0
  30. package/gb/postcode-area.ts +107 -0
  31. package/gb/postcode.ts +109 -0
  32. package/gb/street-type.ts +90 -0
  33. package/index.ts +38 -0
  34. package/jp/address-unit.ts +87 -0
  35. package/jp/index.ts +13 -0
  36. package/jp/postal-code.ts +93 -0
  37. package/jp/prefecture.ts +173 -0
  38. package/level-semantics.ts +623 -0
  39. package/nz/delivery-service.ts +211 -0
  40. package/nz/index.ts +12 -0
  41. package/nz/postcode.ts +42 -0
  42. package/package.json +13 -5
  43. package/postcode-systems.ts +68 -0
  44. package/tools/build-country-surface-lexicon.ts +166 -0
  45. package/tools/export-country-surfaces.ts +46 -0
  46. package/tools/generate-country-reference.ts +153 -0
  47. package/tools/generate-official-languages.ts +188 -0
  48. package/tools/index.ts +12 -0
  49. package/us/floor-designator.ts +119 -0
  50. package/us/index.ts +19 -0
  51. package/us/military-address.ts +199 -0
  52. package/us/po-box.ts +82 -0
  53. package/us/state.ts +156 -0
  54. package/us/street-directional.ts +220 -0
  55. package/us/street-suffix.ts +345 -0
  56. package/us/unit-designator.ts +223 -0
  57. package/us/zipcode.ts +212 -0
package/fr/cedex.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * CEDEX — Courrier d'Entreprise à Distribution EXceptionnelle (La Poste business routing, NF Z
7
+ * 10-011 §3.4). A CEDEX line replaces the ordinary delivery line for high-volume business
8
+ * recipients: `75008 PARIS CEDEX 08` — the word CEDEX after the distribution office name,
9
+ * optionally followed by a 1–2 digit office number. The component is the `CEDEX [NN]` phrase
10
+ * itself (the schema's `cedex` tag); the preceding postcode/locality keep their own tags.
11
+ *
12
+ * This slice closes the gap PR #516 documented: the shard builder sourced the shape from SCHEMA.mdx
13
+ * prose because codex had no cedex home. Now it does — the builder and any future consumer import
14
+ * from here (the provenance-first discipline: one provenanced source).
15
+ */
16
+
17
+ /** Matches a CEDEX phrase: the keyword plus an optional 1–2 digit office number. */
18
+ export const CEDEX_PATTERN = /\bCEDEX(?:\s+(\d{1,2}))?\b/i
19
+
20
+ /** A matched CEDEX phrase with its char range and optional office number. */
21
+ export interface CedexMatch {
22
+ /** The full matched phrase as it appears ("CEDEX 08", "Cedex"). */
23
+ matched: string
24
+ start: number
25
+ end: number
26
+ /** The office number when present ("08"), undefined for bare CEDEX. */
27
+ office?: string
28
+ }
29
+
30
+ /**
31
+ * Find the CEDEX phrase in a line, if any. Returns the LAST match — a CEDEX line places the phrase terminally (NF Z
32
+ * 10-011), and any earlier occurrence in pathological input is more likely a venue name fragment.
33
+ */
34
+ export function matchCedex(text: string): CedexMatch | null {
35
+ let match: CedexMatch | null = null
36
+ const re = new RegExp(CEDEX_PATTERN.source, "gi")
37
+
38
+ for (const m of text.matchAll(re)) {
39
+ match = {
40
+ matched: m[0],
41
+ start: m.index,
42
+ end: m.index + m[0].length,
43
+ ...(m[1] ? { office: m[1] } : {}),
44
+ }
45
+ }
46
+
47
+ return match
48
+ }
49
+
50
+ /** True when the string is exactly a CEDEX phrase (the component-value validator). */
51
+ export function isCedex(input: unknown): boolean {
52
+ if (typeof input !== "string") return false
53
+ const m = input.trim().match(CEDEX_PATTERN)
54
+
55
+ return m !== null && m[0].length === input.trim().length
56
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * French postal codes (code postal): the branded type, the shape, normalization, and the
7
+ * first-two-digits → département mapping — the cleanest postcode→admin prior of the three
8
+ * systems.
9
+ *
10
+ * The informative contrast across `us/zipcode.ts`, `de/postleitzahl.ts`, and here:
11
+ *
12
+ * - A US ZIP's first digit maps to a loose BAND of states.
13
+ * - A German PLZ's first digit maps to a Leitzone that CROSSES Bundesland borders.
14
+ * - A French code postal's first TWO digits ARE the département number directly (`75008` → 75, Paris;
15
+ * `13001` → 13, Bouches-du-Rhône). So the French prefix pins the actual admin unit, and the
16
+ * région follows from the département. It is the tightest of the three.
17
+ *
18
+ * The exceptions are the interesting part: Corsica shares prefix `20` across two départements (`2A`
19
+ * Corse-du-Sud / `2B` Haute-Corse, resolved by the rest of the code), and the overseas DOM use a
20
+ * THREE-digit prefix (`971`–`976`). `departementOfCodePostal` handles both. Two further
21
+ * real-world caveats it does NOT try to model: a handful of communes sit under a neighbouring
22
+ * département's code (e.g. some `05`/`04` border villages), and a CEDEX code can carry a
23
+ * large-volume-mail prefix that differs from the geographic one — both rare enough to leave to
24
+ * the gazetteer.
25
+ */
26
+
27
+ import type { Tagged } from "type-fest"
28
+
29
+ import { departementInfo, type DepartementCode, type DepartementInfo } from "./departement.ts"
30
+ import { FR_REGIONS, type FrenchRegionInfo } from "./region.ts"
31
+
32
+ /**
33
+ * A French postal code: five digits (`75008`). Same shape as a US ZIP or a German PLZ — the shape alone does not
34
+ * disambiguate the country.
35
+ *
36
+ * @category Postal
37
+ * @type string
38
+ * @title Code postal
39
+ * @pattern ^\d{5}$
40
+ */
41
+ export type CodePostal = Tagged<string, "CodePostal">
42
+
43
+ /** The code-postal shape: exactly five digits. */
44
+ export const CODE_POSTAL_PATTERN = /^\d{5}$/
45
+
46
+ /**
47
+ * Normalize a code-postal surface form to the bare five digits: strip an `F-` country courtesy prefix and surrounding
48
+ * whitespace (`F-75008` → `75008`). Returns null if the result is not five digits.
49
+ */
50
+ export function normalizeCodePostal(raw: unknown): CodePostal | null {
51
+ if (typeof raw !== "string") return null
52
+ const s = raw.trim().toUpperCase().replace(/^F-/, "")
53
+
54
+ return CODE_POSTAL_PATTERN.test(s) ? (s as CodePostal) : null
55
+ }
56
+
57
+ /** Type-predicate for a (normalized) French postal code. */
58
+ export function isCodePostal(input: unknown): input is CodePostal {
59
+ return typeof input === "string" && CODE_POSTAL_PATTERN.test(input)
60
+ }
61
+
62
+ /**
63
+ * The département code a postal code belongs to. The clean rule plus its two exceptions:
64
+ *
65
+ * - `20xxx` → Corsica. The split is by the rest of the code: roughly `20000`–`20199` → `2A` (Ajaccio side), `20200`+ →
66
+ * `2B` (Bastia side). Approximate at the boundary, exact for the bulk.
67
+ * - `970`–`976`xx → an overseas DOM, keyed by the three-digit prefix (`971`–`974`, `976`).
68
+ * - Otherwise the first two digits are the département number.
69
+ *
70
+ * Returns null for a prefix with no département (e.g. `975`/`977`/`98x` collectivities, or a malformed code).
71
+ */
72
+ export function departementOfCodePostal(codePostal: unknown): DepartementCode | null {
73
+ const cp = normalizeCodePostal(codePostal)
74
+
75
+ if (!cp) return null
76
+
77
+ if (cp.startsWith("20")) {
78
+ // Corsica: prefix 20 covers both départements; the numeric value splits them.
79
+ return Number(cp) < 20200 ? "2A" : "2B"
80
+ }
81
+
82
+ if (cp.startsWith("97") || cp.startsWith("98")) {
83
+ // Overseas: three-digit prefix. Only the five DOM are départements.
84
+ const dom = cp.slice(0, 3)
85
+
86
+ return departementInfo(dom) ? (dom as DepartementCode) : null
87
+ }
88
+ const dd = cp.slice(0, 2)
89
+
90
+ return departementInfo(dd) ? (dd as DepartementCode) : null
91
+ }
92
+
93
+ /** The full département record a postal code resolves to (name + région), or null. */
94
+ export function departementForCodePostal(codePostal: unknown): DepartementInfo | null {
95
+ return departementInfo(departementOfCodePostal(codePostal))
96
+ }
97
+
98
+ /**
99
+ * The région a postal code resolves to, via its département; null if the code maps to no département.
100
+ */
101
+ export function regionForCodePostal(codePostal: unknown): FrenchRegionInfo | null {
102
+ const dep = departementForCodePostal(codePostal)
103
+
104
+ return dep ? FR_REGIONS[dep.region] : null
105
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The 101 French départements (96 metropolitan including Corsica's 2A/2B, plus the 5 overseas DOM),
7
+ * each mapped to its région.
8
+ *
9
+ * The département is the key admin unit for French postal geography: a code postal's first two
10
+ * digits ARE the département number (see `code-postal.ts`), and the région is derived from the
11
+ * département. This table is therefore the hinge between `code-postal.ts` and `region.ts`.
12
+ */
13
+
14
+ import type { FrenchRegionCode } from "./region.ts"
15
+
16
+ /** Per-département record: code (2-digit, or `2A`/`2B`, or 3-digit DOM) + name + its région. */
17
+ export interface DepartementInfo {
18
+ /** Département code: `01`–`95` (metropolitan), `2A`/`2B` (Corsica), or `971`–`976` (overseas). */
19
+ code: string
20
+ /** French name (e.g. `Bouches-du-Rhône`). */
21
+ name: string
22
+ /** The ISO 3166-2:FR code of the région this département belongs to. */
23
+ region: FrenchRegionCode
24
+ }
25
+
26
+ /** Département code → info. 96 metropolitan (incl. 2A/2B) + 5 overseas = 101. */
27
+ export const FR_DEPARTEMENTS = {
28
+ "01": { code: "01", name: "Ain", region: "ARA" },
29
+ "02": { code: "02", name: "Aisne", region: "HDF" },
30
+ "03": { code: "03", name: "Allier", region: "ARA" },
31
+ "04": { code: "04", name: "Alpes-de-Haute-Provence", region: "PAC" },
32
+ "05": { code: "05", name: "Hautes-Alpes", region: "PAC" },
33
+ "06": { code: "06", name: "Alpes-Maritimes", region: "PAC" },
34
+ "07": { code: "07", name: "Ardèche", region: "ARA" },
35
+ "08": { code: "08", name: "Ardennes", region: "GES" },
36
+ "09": { code: "09", name: "Ariège", region: "OCC" },
37
+ "10": { code: "10", name: "Aube", region: "GES" },
38
+ "11": { code: "11", name: "Aude", region: "OCC" },
39
+ "12": { code: "12", name: "Aveyron", region: "OCC" },
40
+ "13": { code: "13", name: "Bouches-du-Rhône", region: "PAC" },
41
+ "14": { code: "14", name: "Calvados", region: "NOR" },
42
+ "15": { code: "15", name: "Cantal", region: "ARA" },
43
+ "16": { code: "16", name: "Charente", region: "NAQ" },
44
+ "17": { code: "17", name: "Charente-Maritime", region: "NAQ" },
45
+ "18": { code: "18", name: "Cher", region: "CVL" },
46
+ "19": { code: "19", name: "Corrèze", region: "NAQ" },
47
+ "2A": { code: "2A", name: "Corse-du-Sud", region: "COR" },
48
+ "2B": { code: "2B", name: "Haute-Corse", region: "COR" },
49
+ "21": { code: "21", name: "Côte-d'Or", region: "BFC" },
50
+ "22": { code: "22", name: "Côtes-d'Armor", region: "BRE" },
51
+ "23": { code: "23", name: "Creuse", region: "NAQ" },
52
+ "24": { code: "24", name: "Dordogne", region: "NAQ" },
53
+ "25": { code: "25", name: "Doubs", region: "BFC" },
54
+ "26": { code: "26", name: "Drôme", region: "ARA" },
55
+ "27": { code: "27", name: "Eure", region: "NOR" },
56
+ "28": { code: "28", name: "Eure-et-Loir", region: "CVL" },
57
+ "29": { code: "29", name: "Finistère", region: "BRE" },
58
+ "30": { code: "30", name: "Gard", region: "OCC" },
59
+ "31": { code: "31", name: "Haute-Garonne", region: "OCC" },
60
+ "32": { code: "32", name: "Gers", region: "OCC" },
61
+ "33": { code: "33", name: "Gironde", region: "NAQ" },
62
+ "34": { code: "34", name: "Hérault", region: "OCC" },
63
+ "35": { code: "35", name: "Ille-et-Vilaine", region: "BRE" },
64
+ "36": { code: "36", name: "Indre", region: "CVL" },
65
+ "37": { code: "37", name: "Indre-et-Loire", region: "CVL" },
66
+ "38": { code: "38", name: "Isère", region: "ARA" },
67
+ "39": { code: "39", name: "Jura", region: "BFC" },
68
+ "40": { code: "40", name: "Landes", region: "NAQ" },
69
+ "41": { code: "41", name: "Loir-et-Cher", region: "CVL" },
70
+ "42": { code: "42", name: "Loire", region: "ARA" },
71
+ "43": { code: "43", name: "Haute-Loire", region: "ARA" },
72
+ "44": { code: "44", name: "Loire-Atlantique", region: "PDL" },
73
+ "45": { code: "45", name: "Loiret", region: "CVL" },
74
+ "46": { code: "46", name: "Lot", region: "OCC" },
75
+ "47": { code: "47", name: "Lot-et-Garonne", region: "NAQ" },
76
+ "48": { code: "48", name: "Lozère", region: "OCC" },
77
+ "49": { code: "49", name: "Maine-et-Loire", region: "PDL" },
78
+ "50": { code: "50", name: "Manche", region: "NOR" },
79
+ "51": { code: "51", name: "Marne", region: "GES" },
80
+ "52": { code: "52", name: "Haute-Marne", region: "GES" },
81
+ "53": { code: "53", name: "Mayenne", region: "PDL" },
82
+ "54": { code: "54", name: "Meurthe-et-Moselle", region: "GES" },
83
+ "55": { code: "55", name: "Meuse", region: "GES" },
84
+ "56": { code: "56", name: "Morbihan", region: "BRE" },
85
+ "57": { code: "57", name: "Moselle", region: "GES" },
86
+ "58": { code: "58", name: "Nièvre", region: "BFC" },
87
+ "59": { code: "59", name: "Nord", region: "HDF" },
88
+ "60": { code: "60", name: "Oise", region: "HDF" },
89
+ "61": { code: "61", name: "Orne", region: "NOR" },
90
+ "62": { code: "62", name: "Pas-de-Calais", region: "HDF" },
91
+ "63": { code: "63", name: "Puy-de-Dôme", region: "ARA" },
92
+ "64": { code: "64", name: "Pyrénées-Atlantiques", region: "NAQ" },
93
+ "65": { code: "65", name: "Hautes-Pyrénées", region: "OCC" },
94
+ "66": { code: "66", name: "Pyrénées-Orientales", region: "OCC" },
95
+ "67": { code: "67", name: "Bas-Rhin", region: "GES" },
96
+ "68": { code: "68", name: "Haut-Rhin", region: "GES" },
97
+ "69": { code: "69", name: "Rhône", region: "ARA" },
98
+ "70": { code: "70", name: "Haute-Saône", region: "BFC" },
99
+ "71": { code: "71", name: "Saône-et-Loire", region: "BFC" },
100
+ "72": { code: "72", name: "Sarthe", region: "PDL" },
101
+ "73": { code: "73", name: "Savoie", region: "ARA" },
102
+ "74": { code: "74", name: "Haute-Savoie", region: "ARA" },
103
+ "75": { code: "75", name: "Paris", region: "IDF" },
104
+ "76": { code: "76", name: "Seine-Maritime", region: "NOR" },
105
+ "77": { code: "77", name: "Seine-et-Marne", region: "IDF" },
106
+ "78": { code: "78", name: "Yvelines", region: "IDF" },
107
+ "79": { code: "79", name: "Deux-Sèvres", region: "NAQ" },
108
+ "80": { code: "80", name: "Somme", region: "HDF" },
109
+ "81": { code: "81", name: "Tarn", region: "OCC" },
110
+ "82": { code: "82", name: "Tarn-et-Garonne", region: "OCC" },
111
+ "83": { code: "83", name: "Var", region: "PAC" },
112
+ "84": { code: "84", name: "Vaucluse", region: "PAC" },
113
+ "85": { code: "85", name: "Vendée", region: "PDL" },
114
+ "86": { code: "86", name: "Vienne", region: "NAQ" },
115
+ "87": { code: "87", name: "Haute-Vienne", region: "NAQ" },
116
+ "88": { code: "88", name: "Vosges", region: "GES" },
117
+ "89": { code: "89", name: "Yonne", region: "BFC" },
118
+ "90": { code: "90", name: "Territoire de Belfort", region: "BFC" },
119
+ "91": { code: "91", name: "Essonne", region: "IDF" },
120
+ "92": { code: "92", name: "Hauts-de-Seine", region: "IDF" },
121
+ "93": { code: "93", name: "Seine-Saint-Denis", region: "IDF" },
122
+ "94": { code: "94", name: "Val-de-Marne", region: "IDF" },
123
+ "95": { code: "95", name: "Val-d'Oise", region: "IDF" },
124
+ "971": { code: "971", name: "Guadeloupe", region: "GUA" },
125
+ "972": { code: "972", name: "Martinique", region: "MTQ" },
126
+ "973": { code: "973", name: "Guyane", region: "GUF" },
127
+ "974": { code: "974", name: "La Réunion", region: "LRE" },
128
+ "976": { code: "976", name: "Mayotte", region: "MAY" },
129
+ } as const satisfies Record<string, DepartementInfo>
130
+
131
+ /** A French département code (`01`–`95`, `2A`/`2B`, or `971`–`976`). */
132
+ export type DepartementCode = keyof typeof FR_DEPARTEMENTS
133
+
134
+ /**
135
+ * Look up a département by code (case-insensitive for the Corsica `2A`/`2B` letters); null if unknown.
136
+ */
137
+ export function departementInfo(code: string | null | undefined): DepartementInfo | null {
138
+ if (!code || typeof code !== "string") return null
139
+ const key = code.trim().toUpperCase()
140
+
141
+ return (FR_DEPARTEMENTS as Record<string, DepartementInfo>)[key] ?? null
142
+ }
package/fr/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The French address system (La Poste / ISO 3166-2:FR): street types (voie), postal codes (code
7
+ * postal), and the admin hierarchy of départements and régions.
8
+ */
9
+
10
+ export * from "./cedex.ts"
11
+ export * from "./code-postal.ts"
12
+ export * from "./departement.ts"
13
+ export * from "./region.ts"
14
+ export * from "./voie.ts"
package/fr/region.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The 18 French régions (13 metropolitan + 5 overseas), keyed by their ISO 3166-2:FR code.
7
+ *
8
+ * The contrast with `de/bundesland.ts` and `us/state.ts`: France's regions were redrawn in the 2016
9
+ * reform that merged 22 metropolitan régions into 13 (Aquitaine + Limousin + Poitou-Charentes →
10
+ * Nouvelle-Aquitaine, etc.). So a French region is a large, recent amalgamation, and — like a
11
+ * German Bundesland — it is almost never written on an address line, which reads `code-postal
12
+ * commune`. The region is inferred from the département, which is inferred from the postcode (see
13
+ * `code-postal.ts`).
14
+ */
15
+
16
+ /** Per-region record: ISO 3166-2:FR code (sans `FR-` prefix) + French name. */
17
+ export interface FrenchRegionInfo {
18
+ /** ISO 3166-2:FR region code without the `FR-` prefix (e.g. `IDF` for `FR-IDF`). */
19
+ code: string
20
+ /** French name (e.g. `Île-de-France`). */
21
+ name: string
22
+ }
23
+
24
+ /** ISO 3166-2:FR region code → info, for all 18 régions (13 metropolitan + 5 overseas). */
25
+ export const FR_REGIONS = {
26
+ ARA: { code: "ARA", name: "Auvergne-Rhône-Alpes" },
27
+ BFC: { code: "BFC", name: "Bourgogne-Franche-Comté" },
28
+ BRE: { code: "BRE", name: "Bretagne" },
29
+ CVL: { code: "CVL", name: "Centre-Val de Loire" },
30
+ COR: { code: "COR", name: "Corse" },
31
+ GES: { code: "GES", name: "Grand Est" },
32
+ HDF: { code: "HDF", name: "Hauts-de-France" },
33
+ IDF: { code: "IDF", name: "Île-de-France" },
34
+ NOR: { code: "NOR", name: "Normandie" },
35
+ NAQ: { code: "NAQ", name: "Nouvelle-Aquitaine" },
36
+ OCC: { code: "OCC", name: "Occitanie" },
37
+ PDL: { code: "PDL", name: "Pays de la Loire" },
38
+ PAC: { code: "PAC", name: "Provence-Alpes-Côte d'Azur" },
39
+ GUA: { code: "GUA", name: "Guadeloupe" },
40
+ MTQ: { code: "MTQ", name: "Martinique" },
41
+ GUF: { code: "GUF", name: "Guyane" },
42
+ LRE: { code: "LRE", name: "La Réunion" },
43
+ MAY: { code: "MAY", name: "Mayotte" },
44
+ } as const satisfies Record<string, FrenchRegionInfo>
45
+
46
+ /** An ISO 3166-2:FR region code (`ARA`, `IDF`, `PAC`, …). */
47
+ export type FrenchRegionCode = keyof typeof FR_REGIONS
48
+
49
+ const REGION_CODE_SET: ReadonlySet<string> = new Set(Object.keys(FR_REGIONS))
50
+
51
+ /** Type-predicate for an ISO 3166-2:FR region code. Case-insensitive. */
52
+ export function isFrenchRegionCode(input: unknown): input is FrenchRegionCode {
53
+ return typeof input === "string" && REGION_CODE_SET.has(input.toUpperCase())
54
+ }
55
+
56
+ /** Strip diacritics + lowercase so `Île-de-France`, `ile-de-france`, `Ile de France` all key alike. */
57
+ function foldName(s: string): string {
58
+ return s
59
+ .toLowerCase()
60
+ .normalize("NFD")
61
+ .replace(/[\u0300-\u036f]/g, "")
62
+ .replace(/[^a-z0-9]+/g, " ")
63
+ .trim()
64
+ }
65
+
66
+ /**
67
+ * Folded region name / code → ISO 3166-2:FR code. Built diacritic-insensitive so the resolver's surface form
68
+ * (`Île-de-France`, or an unaccented `Ile-de-France`) maps regardless of accents. Mirrors `de/bundesland.ts`'s
69
+ * `lookupGermanState`, the same role: fold a region surface form to one code so a resolver eval can compare
70
+ * like-for-like without a US-USPS-shaped matcher.
71
+ */
72
+ export const FR_REGION_NAME_TO_CODE: ReadonlyMap<string, FrenchRegionCode> = (() => {
73
+ const out = new Map<string, FrenchRegionCode>()
74
+
75
+ for (const code of Object.keys(FR_REGIONS) as FrenchRegionCode[]) {
76
+ out.set(foldName(FR_REGIONS[code].name), code)
77
+ out.set(code.toLowerCase(), code)
78
+ }
79
+
80
+ return out
81
+ })()
82
+
83
+ /**
84
+ * Resolve a French region surface form (ISO code or name, accents optional) to its ISO code; null if unknown.
85
+ */
86
+ export function lookupFrenchRegion(input: string | null | undefined): FrenchRegionCode | null {
87
+ if (!input || typeof input !== "string") return null
88
+ const upper = input.trim().toUpperCase()
89
+
90
+ if (REGION_CODE_SET.has(upper)) return upper as FrenchRegionCode
91
+
92
+ return FR_REGION_NAME_TO_CODE.get(foldName(input)) ?? null
93
+ }
package/fr/voie.ts ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * French street types (types de voie).
7
+ *
8
+ * The third point on the morphology spectrum the codex now spans:
9
+ *
10
+ * - US — the type is a TRAILING word with a standardized abbreviation (`Main Street` → `ST`).
11
+ * - German — the type is a fused TRAILING suffix (`Hauptstraße`).
12
+ * - French — the type is a LEADING standalone word (`Rue de la Paix`, `Avenue des Champs-Élysées`).
13
+ * It carries common abbreviations (`bd`, `av`, `pl`) but no single national standard like
14
+ * USPS Pub-28.
15
+ *
16
+ * So French detection is "is this token a known voie word", position-first — which is why
17
+ * {@link isFrenchStreetWord} matches a whole token rather than a suffix.
18
+ */
19
+
20
+ /**
21
+ * Canonical French voie type → common written abbreviations. The leading word of a French street name. The first entry
22
+ * of each list is the most common abbreviation where one exists.
23
+ */
24
+ export const FR_VOIE_TYPES = {
25
+ rue: ["r"],
26
+ avenue: ["av", "ave"],
27
+ boulevard: ["bd", "boul"],
28
+ place: ["pl"],
29
+ impasse: ["imp"],
30
+ allée: ["all"],
31
+ allées: [],
32
+ chemin: ["ch", "che"],
33
+ quai: [],
34
+ cours: ["crs"],
35
+ passage: ["pass", "psg"],
36
+ square: ["sq"],
37
+ route: ["rte"],
38
+ sentier: ["sen"],
39
+ villa: [],
40
+ cité: [],
41
+ esplanade: ["espl"],
42
+ faubourg: ["fbg", "fg"],
43
+ mail: [],
44
+ promenade: ["prom"],
45
+ "rond-point": ["rpt"],
46
+ voie: [],
47
+ chaussée: ["chée"],
48
+ ruelle: [],
49
+ venelle: [],
50
+ traverse: ["trav"],
51
+ montée: ["mtée"],
52
+ clos: [],
53
+ hameau: ["ham"],
54
+ résidence: ["rés"],
55
+ lotissement: ["lot"],
56
+ } as const satisfies Record<string, readonly string[]>
57
+
58
+ /** A canonical French voie type (e.g. `rue`, `avenue`, `boulevard`). */
59
+ export type FrenchVoieType = keyof typeof FR_VOIE_TYPES
60
+
61
+ /**
62
+ * Set of every French voie token — each canonical type plus each abbreviation — folded to lowercase/diacritic-free for
63
+ * matching. `Allée`/`allee`/`all` all resolve here.
64
+ */
65
+ const VOIE_TOKEN_SET: ReadonlySet<string> = (() => {
66
+ const fold = (s: string): string =>
67
+ s
68
+ .toLowerCase()
69
+ .normalize("NFD")
70
+ .replace(/[\u0300-\u036f]/g, "")
71
+ const out = new Set<string>()
72
+
73
+ for (const canonical of Object.keys(FR_VOIE_TYPES) as FrenchVoieType[]) {
74
+ out.add(fold(canonical))
75
+
76
+ for (const abbr of FR_VOIE_TYPES[canonical]) {
77
+ out.add(fold(abbr))
78
+ }
79
+ }
80
+
81
+ return out
82
+ })()
83
+
84
+ /**
85
+ * True when a token is a French voie type word or abbreviation (case- and accent-insensitive) — `Rue`, `BD`, `Allée`,
86
+ * `impasse`. Matches the WHOLE token (French types lead the street name, they are not fused suffixes), so a city or
87
+ * surname is not caught the way a suffix test might.
88
+ */
89
+ export function isFrenchStreetWord(token: unknown): boolean {
90
+ if (typeof token !== "string") return false
91
+ const t = token
92
+ .toLowerCase()
93
+ .normalize("NFD")
94
+ .replace(/[\u0300-\u036f]/g, "")
95
+ .replace(/[^a-z-]/g, "")
96
+
97
+ return t.length > 0 && VOIE_TOKEN_SET.has(t)
98
+ }
package/gb/country.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The four constituent countries of the United Kingdom, keyed by their ISO 3166-2:GB code: England
7
+ * (`ENG`), Scotland (`SCT`), Wales (`WLS`), Northern Ireland (`NIR`).
8
+ *
9
+ * The UK's top-level admin tier is itself the first oddity of this system. France has régions,
10
+ * Germany has Bundesländer, the US has states — a single flat layer. The UK has _countries_
11
+ * inside a country, and an address almost never names which one: a line reads `street, town,
12
+ * postcode`, and the constituent country is inferred — usually, but not always cleanly, from the
13
+ * postcode area (see `postcode-area.ts`). So this file is the coarse admin label, and the
14
+ * postcode is the thing that actually carries the geography.
15
+ */
16
+
17
+ /** Per-country record: ISO 3166-2:GB code (sans `GB-` prefix) + English name. */
18
+ export interface UkCountryInfo {
19
+ /** ISO 3166-2:GB country code without the `GB-` prefix (e.g. `ENG` for `GB-ENG`). */
20
+ code: string
21
+ /** English name (e.g. `Scotland`). */
22
+ name: string
23
+ }
24
+
25
+ /** ISO 3166-2:GB country code → info, for all four constituent countries. */
26
+ export const GB_COUNTRIES = {
27
+ ENG: { code: "ENG", name: "England" },
28
+ SCT: { code: "SCT", name: "Scotland" },
29
+ WLS: { code: "WLS", name: "Wales" },
30
+ NIR: { code: "NIR", name: "Northern Ireland" },
31
+ } as const satisfies Record<string, UkCountryInfo>
32
+
33
+ /** An ISO 3166-2:GB constituent-country code (`ENG`, `SCT`, `WLS`, `NIR`). */
34
+ export type UkCountryCode = keyof typeof GB_COUNTRIES
35
+
36
+ const COUNTRY_CODE_SET: ReadonlySet<string> = new Set(Object.keys(GB_COUNTRIES))
37
+
38
+ /** Type-predicate for an ISO 3166-2:GB country code. Case-insensitive. */
39
+ export function isUkCountryCode(input: unknown): input is UkCountryCode {
40
+ return typeof input === "string" && COUNTRY_CODE_SET.has(input.toUpperCase())
41
+ }
42
+
43
+ /** Lowercase + collapse non-alphanumerics so `Northern Ireland`, `northern-ireland` key alike. */
44
+ function foldName(s: string): string {
45
+ return s
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, " ")
48
+ .trim()
49
+ }
50
+
51
+ /** Folded country name / code → ISO 3166-2:GB code, so a surface form maps regardless of casing. */
52
+ const COUNTRY_NAME_TO_CODE: ReadonlyMap<string, UkCountryCode> = (() => {
53
+ const out = new Map<string, UkCountryCode>()
54
+
55
+ for (const code of Object.keys(GB_COUNTRIES) as UkCountryCode[]) {
56
+ out.set(foldName(GB_COUNTRIES[code].name), code)
57
+ out.set(code.toLowerCase(), code)
58
+ }
59
+
60
+ return out
61
+ })()
62
+
63
+ /**
64
+ * Resolve a UK constituent-country surface form (ISO code or English name) to its ISO code; null if unknown. Accepts
65
+ * `ENG`, `England`, `Northern Ireland`, `scotland`, etc.
66
+ */
67
+ export function lookupUkCountry(input: string | null | undefined): UkCountryCode | null {
68
+ if (!input || typeof input !== "string") return null
69
+ const upper = input.trim().toUpperCase()
70
+
71
+ if (COUNTRY_CODE_SET.has(upper)) return upper as UkCountryCode
72
+
73
+ return COUNTRY_NAME_TO_CODE.get(foldName(input)) ?? null
74
+ }
package/gb/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The United Kingdom address system (Royal Mail / ISO 3166-2:GB): the variable-length alphanumeric
7
+ * postcode and its Royal-Mail postcode areas, the four constituent countries, and British street
8
+ * vocabulary.
9
+ */
10
+
11
+ export * from "./country.ts"
12
+ export * from "./postcode-area.ts"
13
+ export * from "./postcode.ts"
14
+ export * from "./street-type.ts"