@mailwoman/codex 7.2.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 +81 -37
  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
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Canadian postal codes: the branded type, the shape, normalization, and the FSA-letter →
7
+ * province/territory prior — the only ALPHANUMERIC postcode of the systems the codex models.
8
+ *
9
+ * The informative contrast across `us/zipcode.ts`, `de/postleitzahl.ts`, `fr/code-postal.ts`, and
10
+ * here:
11
+ *
12
+ * - A US ZIP is numeric; its first digit maps to a loose BAND of states.
13
+ * - A German PLZ is numeric; its first digit maps to a Leitzone that CROSSES Bundesland borders.
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
16
+ * first LETTER pins the province or territory directly (`M` → Ontario, `H` → Quebec, `V` →
17
+ * British Columbia). So like the French prefix it is a clean admin prior, but it does the job
18
+ * with a single ALPHA character rather than digits.
19
+ *
20
+ * The clean rule has two wrinkles worth knowing. `X` is SHARED by the Northwest Territories and
21
+ * Nunavut (no single letter splits them), so `provinceOfPostalCode` returns an array there. And
22
+ * the large provinces span SEVERAL letters: Ontario alone owns `K L M N P`, Quebec owns `G H J`.
23
+ * The first three characters form the FSA (Forward Sortation Area); the last three are the LDU
24
+ * (Local Delivery Unit). A FSA whose SECOND character (the first digit) is `0` is a RURAL area —
25
+ * the bridge to the wider, lower-density delivery zones.
26
+ */
27
+
28
+ import type { Tagged } from "type-fest"
29
+
30
+ import type { CanadianProvinceCode } from "./province.ts"
31
+
32
+ /**
33
+ * A Canadian postal code: `A1A 1A1`. Six alphanumeric characters in a strict Letter-Digit-Letter-Digit-Letter-Digit
34
+ * pattern, conventionally written with a single space after the third. Unlike the other systems' bare five digits, the
35
+ * shape alone already says "Canada".
36
+ *
37
+ * @category Postal
38
+ * @type string
39
+ * @title Postal Code
40
+ * @pattern ^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$
41
+ */
42
+ export type PostalCode = Tagged<string, "CaPostalCode">
43
+
44
+ /**
45
+ * The Canadian postal-code shape. The valid FIRST letters exclude `D F I O Q U W Z` (never used to open a postcode);
46
+ * the interior letters additionally exclude `D F I O Q U` (the visually ambiguous ones). The space between FSA and LDU
47
+ * is optional in the raw form.
48
+ */
49
+ export const CA_POSTAL_CODE_PATTERN = /^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$/i
50
+
51
+ /**
52
+ * Normalize a postal-code surface form to canonical `A1A 1A1`: uppercase and ensure exactly one space between the FSA
53
+ * (first three chars) and the LDU (last three) — `K1A0B1` → `K1A 0B1`, `k1a 0b1` → `K1A 0B1`. Returns null if the input
54
+ * is not a valid Canadian postal code.
55
+ */
56
+ export function normalizeCaPostalCode(raw: unknown): PostalCode | null {
57
+ if (typeof raw !== "string") return null
58
+ const compact = raw.trim().toUpperCase().replace(/\s+/g, "")
59
+
60
+ if (compact.length !== 6) return null
61
+ const spaced = `${compact.slice(0, 3)} ${compact.slice(3)}`
62
+
63
+ return CA_POSTAL_CODE_PATTERN.test(spaced) ? (spaced as PostalCode) : null
64
+ }
65
+
66
+ /** Type-predicate for a Canadian postal code (accepts the spaced or unspaced surface form). */
67
+ export function isCaPostalCode(input: unknown): input is PostalCode {
68
+ return typeof input === "string" && CA_POSTAL_CODE_PATTERN.test(input)
69
+ }
70
+
71
+ /**
72
+ * FSA first letter → province/territory. Clean (one province per letter) except `X`, shared by the Northwest
73
+ * Territories and Nunavut, and the large provinces that span several letters: Ontario owns `K L M N P` and Quebec owns
74
+ * `G H J`. Letters `D F I O Q U W Z` never open a Canadian postcode and so do not appear here.
75
+ */
76
+ export const FSA_LETTER_TO_PROVINCE: Record<string, CanadianProvinceCode | CanadianProvinceCode[]> = {
77
+ A: "NL",
78
+ B: "NS",
79
+ C: "PE",
80
+ E: "NB",
81
+ G: "QC",
82
+ H: "QC",
83
+ J: "QC",
84
+ K: "ON",
85
+ L: "ON",
86
+ M: "ON",
87
+ N: "ON",
88
+ P: "ON",
89
+ R: "MB",
90
+ S: "SK",
91
+ T: "AB",
92
+ V: "BC",
93
+ X: ["NT", "NU"],
94
+ Y: "YT",
95
+ }
96
+
97
+ /**
98
+ * The province/territory a postal code belongs to, via its FSA first letter. Returns the single code for the clean
99
+ * letters, the `["NT", "NU"]` pair for the shared `X`, and null if the input is not a valid Canadian postal code (or
100
+ * its first letter has no province, which the pattern already forbids).
101
+ */
102
+ export function provinceOfPostalCode(postalCode: unknown): CanadianProvinceCode | CanadianProvinceCode[] | null {
103
+ const normalized = normalizeCaPostalCode(postalCode)
104
+
105
+ if (!normalized) return null
106
+
107
+ return FSA_LETTER_TO_PROVINCE[normalized[0]!] ?? null
108
+ }
109
+
110
+ /**
111
+ * True when a postal code is RURAL: its SECOND character (the FSA's first digit) is `0`. Canada Post uses a `0` in that
112
+ * position to mark the lower-density delivery zones (rural routes, small communities) — the contrast with the urban
113
+ * `1`–`9` FSAs. Returns false for a non-code.
114
+ */
115
+ export function isRuralPostalCode(pc: unknown): boolean {
116
+ const normalized = normalizeCaPostalCode(pc)
117
+
118
+ if (!normalized) return false
119
+
120
+ return normalized[1] === "0"
121
+ }
package/ca/province.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The 13 Canadian provinces and territories (10 provinces + 3 territories), keyed by their ISO
7
+ * 3166-2:CA code.
8
+ *
9
+ * The informative contrast with `de/bundesland.ts` and `us/state.ts`: a Canadian subdivision is
10
+ * officially BILINGUAL, so each unit carries two equally-canonical names — an English one and a
11
+ * French one — and the gap between them is wide (`Nova Scotia` / `Nouvelle-Écosse`, `British
12
+ * Columbia` / `Colombie-Britannique`). That mirrors the German English-exonym pattern (`Bavaria`
13
+ * / `Bayern`), except here the French name is not a foreign exonym but a co-official form a real
14
+ * address can be written in. And like a US two-letter state code, the ISO code (`ON`, `QC`, `BC`)
15
+ * is the abbreviation people actually write on the address line — so unlike German or French
16
+ * regions, the Canadian code IS a surface form, not just a resolver key.
17
+ */
18
+
19
+ /** Per-province record: ISO 3166-2:CA code, English name, and the co-official French name. */
20
+ export interface CanadianProvinceInfo {
21
+ /** ISO 3166-2:CA subdivision code without the `CA-` prefix (e.g. `ON` for `CA-ON`). */
22
+ code: string
23
+ /** English name (e.g. `Quebec`). */
24
+ name: string
25
+ /** Co-official French name (e.g. `Québec`). */
26
+ french: string
27
+ }
28
+
29
+ /**
30
+ * ISO 3166-2:CA code → province/territory info, for all 13 subdivisions (10 provinces + 3 territories). Codes are the
31
+ * official subdivision codes minus the `CA-` prefix.
32
+ */
33
+ export const CA_PROVINCES = {
34
+ AB: { code: "AB", name: "Alberta", french: "Alberta" },
35
+ BC: { code: "BC", name: "British Columbia", french: "Colombie-Britannique" },
36
+ MB: { code: "MB", name: "Manitoba", french: "Manitoba" },
37
+ NB: { code: "NB", name: "New Brunswick", french: "Nouveau-Brunswick" },
38
+ NL: { code: "NL", name: "Newfoundland and Labrador", french: "Terre-Neuve-et-Labrador" },
39
+ NS: { code: "NS", name: "Nova Scotia", french: "Nouvelle-Écosse" },
40
+ NT: { code: "NT", name: "Northwest Territories", french: "Territoires du Nord-Ouest" },
41
+ NU: { code: "NU", name: "Nunavut", french: "Nunavut" },
42
+ ON: { code: "ON", name: "Ontario", french: "Ontario" },
43
+ PE: { code: "PE", name: "Prince Edward Island", french: "Île-du-Prince-Édouard" },
44
+ QC: { code: "QC", name: "Quebec", french: "Québec" },
45
+ SK: { code: "SK", name: "Saskatchewan", french: "Saskatchewan" },
46
+ YT: { code: "YT", name: "Yukon", french: "Yukon" },
47
+ } as const satisfies Record<string, CanadianProvinceInfo>
48
+
49
+ /** An ISO 3166-2:CA province/territory code (`AB`, `ON`, `QC`, …). */
50
+ export type CanadianProvinceCode = keyof typeof CA_PROVINCES
51
+
52
+ const PROVINCE_CODE_SET: ReadonlySet<string> = new Set(Object.keys(CA_PROVINCES))
53
+
54
+ /** Type-predicate for an ISO 3166-2:CA province/territory code. Case-insensitive. */
55
+ export function isCanadianProvinceCode(input: unknown): input is CanadianProvinceCode {
56
+ return typeof input === "string" && PROVINCE_CODE_SET.has(input.toUpperCase())
57
+ }
58
+
59
+ /** Strip diacritics + lowercase so `Québec`, `Quebec`, and `quebec` all key alike. */
60
+ function foldName(s: string): string {
61
+ return s
62
+ .toLowerCase()
63
+ .normalize("NFD")
64
+ .replace(/[\u0300-\u036f]/g, "")
65
+ .replace(/[^a-z0-9]+/g, " ")
66
+ .trim()
67
+ }
68
+
69
+ /**
70
+ * Folded province name (English or French) / code → ISO 3166-2:CA code. Built diacritic-insensitive so both co-official
71
+ * names map regardless of accents: `Québec`, `Quebec`, and an unaccented `Nouvelle-Ecosse` all resolve. The two-name
72
+ * design is the Canadian wrinkle — unlike the German lookup's English exonym, the French form here is a name a real
73
+ * address may legitimately use.
74
+ */
75
+ export const CA_PROVINCE_NAME_TO_CODE: ReadonlyMap<string, CanadianProvinceCode> = (() => {
76
+ const out = new Map<string, CanadianProvinceCode>()
77
+
78
+ for (const code of Object.keys(CA_PROVINCES) as CanadianProvinceCode[]) {
79
+ const info = CA_PROVINCES[code]
80
+ out.set(foldName(info.name), code)
81
+ out.set(foldName(info.french), code)
82
+ out.set(code.toLowerCase(), code)
83
+ }
84
+
85
+ return out
86
+ })()
87
+
88
+ /**
89
+ * Resolve a Canadian province/territory surface form (ISO code, English name, or French name, accents optional) to its
90
+ * ISO code; null if unknown.
91
+ */
92
+ export function lookupCanadianProvince(input: string | null | undefined): CanadianProvinceCode | null {
93
+ if (!input || typeof input !== "string") return null
94
+ const upper = input.trim().toUpperCase()
95
+
96
+ if (PROVINCE_CODE_SET.has(upper)) return upper as CanadianProvinceCode
97
+
98
+ return CA_PROVINCE_NAME_TO_CODE.get(foldName(input)) ?? null
99
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Canadian street types and directionals — BILINGUAL, because Canada Post recognizes both English
7
+ * and French forms on the same national network.
8
+ *
9
+ * The informative contrast with `us/street-suffix.ts`, `de/street-type.ts`, and `fr/voie.ts`:
10
+ *
11
+ * - US — a trailing word with a USPS-standardized abbreviation (`Main Street` → `ST`).
12
+ * - German — a fused TRAILING suffix (`Hauptstraße`).
13
+ * - French — a LEADING standalone word (`Rue de la Paix`).
14
+ * - Canadian — BOTH at once. An English street puts the type LAST (`Maple Avenue`, `Sunset
15
+ * Crescent`); a French street puts it FIRST (`Rue Sainte-Catherine`, `Boulevard
16
+ * René-Lévesque`). So {@link isCanadianStreetWord} matches a whole token against EITHER
17
+ * vocabulary and stays position-agnostic — it cannot assume a side the way the
18
+ * single-language files do.
19
+ *
20
+ * The directionals carry the same bilingual twist, and one trap inside it: French `Ouest`
21
+ * abbreviates to `O`, not `W`. A naive English-only matcher reading `Rue Sherbrooke O` would miss
22
+ * the quadrant entirely. {@link CA_DIRECTIONALS} spells out the French abbreviations so `O` =
23
+ * Ouest = West is recognized.
24
+ */
25
+
26
+ /**
27
+ * English Canadian street-type words (Canada Post's recognized set, lowercase). Appear as the TRAILING token of an
28
+ * English street name (`Maple Avenue`, `Sunset Crescent`).
29
+ */
30
+ export const CA_STREET_TYPES_EN: ReadonlySet<string> = new Set([
31
+ "street",
32
+ "avenue",
33
+ "boulevard",
34
+ "drive",
35
+ "road",
36
+ "crescent",
37
+ "court",
38
+ "place",
39
+ "lane",
40
+ "way",
41
+ "trail",
42
+ "terrace",
43
+ "close",
44
+ "circle",
45
+ "square",
46
+ "heights",
47
+ "grove",
48
+ "gardens",
49
+ "hill",
50
+ "park",
51
+ "row",
52
+ "walk",
53
+ "green",
54
+ "bay",
55
+ "cove",
56
+ "gate",
57
+ "point",
58
+ "ridge",
59
+ "view",
60
+ ])
61
+
62
+ /**
63
+ * French Canadian street-type words (Canada Post's recognized set, lowercase, accent-bearing). Appear as the LEADING
64
+ * token of a French street name (`Rue Sainte-Catherine`, `Chemin du Roy`). Folded for matching in
65
+ * {@link isCanadianStreetWord}, so `Côte`/`cote` and `Allée`/`allee` key alike.
66
+ */
67
+ export const CA_STREET_TYPES_FR: ReadonlySet<string> = new Set([
68
+ "rue",
69
+ "avenue",
70
+ "boulevard",
71
+ "chemin",
72
+ "côte",
73
+ "place",
74
+ "impasse",
75
+ "allée",
76
+ "croissant",
77
+ "montée",
78
+ "rang",
79
+ "ruelle",
80
+ "voie",
81
+ "passage",
82
+ "carré",
83
+ "terrasse",
84
+ "promenade",
85
+ "sentier",
86
+ ])
87
+
88
+ /** Strip diacritics + lowercase so `Côte`/`cote`, `Allée`/`allee`, `Crescent`/`crescent` key alike. */
89
+ function foldToken(s: string): string {
90
+ return s
91
+ .toLowerCase()
92
+ .normalize("NFD")
93
+ .replace(/[\u0300-\u036f]/g, "")
94
+ .replace(/[^a-z]/g, "")
95
+ }
96
+
97
+ /** The English + French street-type vocabularies, both folded, for one position-agnostic lookup. */
98
+ const STREET_WORD_SET: ReadonlySet<string> = (() => {
99
+ const out = new Set<string>()
100
+
101
+ for (const w of CA_STREET_TYPES_EN) {
102
+ out.add(foldToken(w))
103
+ }
104
+
105
+ for (const w of CA_STREET_TYPES_FR) {
106
+ out.add(foldToken(w))
107
+ }
108
+
109
+ return out
110
+ })()
111
+
112
+ /**
113
+ * True when a token is a Canadian street-type word in EITHER language (case- and accent-insensitive) — `Street`,
114
+ * `Crescent`, `Rue`, `Chemin`, `Côte`. Position-agnostic, because an English type trails the name and a French type
115
+ * leads it; the matcher cannot lean on a side.
116
+ */
117
+ export function isCanadianStreetWord(token: unknown): boolean {
118
+ if (typeof token !== "string") return false
119
+ const t = foldToken(token)
120
+
121
+ return t.length > 0 && STREET_WORD_SET.has(t)
122
+ }
123
+
124
+ /**
125
+ * Bilingual directional words → canonical compass letter. Covers the bare letters (`N S E W`), the full English words
126
+ * (`North`/`South`/`East`/`West`), and the full French words (`Nord`/`Sud`/`Est`/`Ouest`). The bilingual twist is `O`:
127
+ * French `Ouest` abbreviates to `O`, NOT `W`, so an English-only matcher silently drops the quadrant on a French
128
+ * address line. Keys are folded (lowercase, accent-free); values are the English compass letter.
129
+ */
130
+ export const CA_DIRECTIONALS: Record<string, "N" | "S" | "E" | "W"> = {
131
+ n: "N",
132
+ north: "N",
133
+ nord: "N",
134
+ s: "S",
135
+ south: "S",
136
+ sud: "S",
137
+ e: "E",
138
+ east: "E",
139
+ est: "E",
140
+ w: "W",
141
+ west: "W",
142
+ ouest: "W",
143
+ o: "W", // French Ouest abbreviates to O, not W — the bilingual trap.
144
+ }
145
+
146
+ /**
147
+ * True when a token is a Canadian directional in either language (case- and accent-insensitive) — `N`, `NW`, `Nord`,
148
+ * `Ouest`, `O`. Compound English quadrants (`NW`, `SE`) are accepted by decomposing into their single-letter halves;
149
+ * the lone French `O` resolves to West.
150
+ */
151
+ export function isCanadianDirectional(token: unknown): boolean {
152
+ if (typeof token !== "string") return false
153
+ const t = foldToken(token)
154
+
155
+ if (t.length === 0) return false
156
+
157
+ if (t in CA_DIRECTIONALS) return true
158
+
159
+ // Compound English quadrants like NW / SE / NE / SW: each half must be a single-letter directional.
160
+ if (t.length === 2) {
161
+ const isLetter = (h: string): boolean => h === "n" || h === "s" || h === "e" || h === "w"
162
+
163
+ return isLetter(t[0]!) && isLetter(t[1]!)
164
+ }
165
+
166
+ return false
167
+ }