@mailwoman/codex 7.2.0 → 7.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/address-system-conventions.ts +68 -0
- package/au/delivery-service.ts +179 -0
- package/au/index.ts +15 -0
- package/au/level-designator.ts +209 -0
- package/au/postcode.ts +51 -0
- package/au/state.ts +35 -0
- package/ca/index.ts +12 -0
- package/ca/postal-code.ts +121 -0
- package/ca/province.ts +99 -0
- package/ca/street-type.ts +167 -0
- package/country/codes.ts +534 -0
- package/country/country.ts +125 -0
- package/country/index.ts +15 -0
- package/country/names.ts +274 -0
- package/country/official-languages.ts +397 -0
- package/country/reference-data.ts +267 -0
- package/country/reference.ts +47 -0
- package/country/subdivision.ts +93 -0
- package/de/bundesland.ts +102 -0
- package/de/index.ts +12 -0
- package/de/postleitzahl.ts +91 -0
- package/de/street-type.ts +83 -0
- package/fr/cedex.ts +56 -0
- package/fr/code-postal.ts +105 -0
- package/fr/departement.ts +142 -0
- package/fr/index.ts +14 -0
- package/fr/region.ts +93 -0
- package/fr/voie.ts +98 -0
- package/gb/country.ts +74 -0
- package/gb/index.ts +14 -0
- package/gb/postcode-area.ts +107 -0
- package/gb/postcode.ts +109 -0
- package/gb/street-type.ts +90 -0
- package/index.ts +38 -0
- package/jp/address-unit.ts +87 -0
- package/jp/index.ts +13 -0
- package/jp/postal-code.ts +93 -0
- package/jp/prefecture.ts +173 -0
- package/level-semantics.ts +623 -0
- package/nz/delivery-service.ts +211 -0
- package/nz/index.ts +12 -0
- package/nz/postcode.ts +42 -0
- package/out/country/index.d.ts +1 -0
- package/out/country/index.d.ts.map +1 -1
- package/out/country/index.js +1 -0
- package/out/country/index.js.map +1 -1
- package/out/country/subdivision.d.ts +44 -0
- package/out/country/subdivision.d.ts.map +1 -0
- package/out/country/subdivision.js +76 -0
- package/out/country/subdivision.js.map +1 -0
- package/package.json +81 -37
- package/postcode-systems.ts +68 -0
- package/tools/build-country-surface-lexicon.ts +166 -0
- package/tools/export-country-surfaces.ts +46 -0
- package/tools/generate-country-reference.ts +153 -0
- package/tools/generate-official-languages.ts +188 -0
- package/tools/index.ts +12 -0
- package/us/floor-designator.ts +119 -0
- package/us/index.ts +19 -0
- package/us/military-address.ts +199 -0
- package/us/po-box.ts +82 -0
- package/us/state.ts +156 -0
- package/us/street-directional.ts +220 -0
- package/us/street-suffix.ts +345 -0
- package/us/unit-designator.ts +223 -0
- package/us/zipcode.ts +212 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Per-address-system parsing conventions (#478's rules-as-constraints slice, started as the #511
|
|
7
|
+
* Tier A corrective). The model's locale head detects WHICH address system a string belongs to;
|
|
8
|
+
* this table says what is and isn't grammatical in that system, so the decoder can obey the
|
|
9
|
+
* detection instead of merely being nudged by it.
|
|
10
|
+
*
|
|
11
|
+
* Every row is a provenance-carrying claim about a national addressing convention — the same
|
|
12
|
+
* provenance-first discipline as the rest of the codex. Add rows with a source, not from vibes;
|
|
13
|
+
* an absent row means "no constraints known", never "no constraints exist".
|
|
14
|
+
*
|
|
15
|
+
* First consumer: `@mailwoman/neural`'s decoder applies `forbiddenTags` as a hard emission mask
|
|
16
|
+
* before Viterbi and treats `postcodePattern` as the system's canonical shape for the snap-only
|
|
17
|
+
* postcode repair pass. Both triggered only when the system is detected confidently (or supplied
|
|
18
|
+
* by the caller) — an undetected system parses exactly as before.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { CODE_POSTAL_PATTERN } from "./fr/code-postal.ts"
|
|
22
|
+
import type { SystemCode } from "./postcode-systems.ts"
|
|
23
|
+
|
|
24
|
+
export interface AddressSystemConventions {
|
|
25
|
+
/**
|
|
26
|
+
* Component tags that are NOT grammatical in this address system (names from the core `ComponentTag` union; codex
|
|
27
|
+
* stays dependency-free so they are plain strings here).
|
|
28
|
+
*/
|
|
29
|
+
readonly forbiddenTags?: readonly string[]
|
|
30
|
+
/**
|
|
31
|
+
* The system's canonical postcode shape. A decoded postcode span that is a strict sub-match of a pattern-valid string
|
|
32
|
+
* in the raw text is shape-INVALID for this system and eligible for the snap-only repair (extend/clip to the valid
|
|
33
|
+
* match — never invent a span).
|
|
34
|
+
*/
|
|
35
|
+
readonly postcodePattern?: RegExp
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ADDRESS_SYSTEM_CONVENTIONS: Partial<Record<SystemCode, AddressSystemConventions>> = {
|
|
39
|
+
/**
|
|
40
|
+
* France (La Poste / AFNOR NF Z 10-011): the street TYPE is a LEADING particle of the street name ("Rue de Rivoli",
|
|
41
|
+
* "Avenue des Champs-Élysées", "Cours Lafayette") and is labeled `street_prefix` — French addresses DO carry a
|
|
42
|
+
* street_prefix, just never a trailing USPS-style street_suffix (the libpostal French dictionaries have no trailing
|
|
43
|
+
* street-suffix class; Pub-28's suffix decomposition has no French counterpart).
|
|
44
|
+
*
|
|
45
|
+
* Provenance / why this is NOT a blanket prefix+suffix forbid (#719, 2026-06-18): an earlier model mis-tagged the
|
|
46
|
+
* leading "Rue" as a US-style `street_suffix` (RUE is a Pub-28 suffix variant) — the 2026-06-10 v1.1.0 gate — so #511
|
|
47
|
+
* forbade BOTH affix tags to stop that leakage. That forbid was correct for THAT model but became a live production
|
|
48
|
+
* bug for the current one: the shipped model (v1.5.0) emits the FR `street_prefix` correctly, but the conventions
|
|
49
|
+
* mask was a hard −1e9 on every B-/I-street_prefix emission, so the detected-FR parse could never KEEP a prefix — it
|
|
50
|
+
* destroyed `street_prefix` wholesale (measured on data/eval/external/ fr-street-prefix-real.jsonl at
|
|
51
|
+
* conventions=auto: F1 0.0 with the forbid on → 80.0 with it off; the larger real-FR eval reported the same collapse,
|
|
52
|
+
* ~96 → ~0.6). We keep ONLY `street_suffix` forbidden: the current model with the forbid OFF shows zero FR
|
|
53
|
+
* street_suffix leakage (fp=0 on that same slice) and FR has no trailing street suffix, so the constraint costs
|
|
54
|
+
* nothing while still guarding against any future suffix mis-tag. Postcode: exactly five digits (NF Z 10-011; see
|
|
55
|
+
* fr/code-postal).
|
|
56
|
+
*/
|
|
57
|
+
fr: {
|
|
58
|
+
forbiddenTags: ["street_suffix"],
|
|
59
|
+
postcodePattern: CODE_POSTAL_PATTERN,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Look up conventions for a system. Absent row = no constraints KNOWN (parse unconstrained). */
|
|
64
|
+
export function conventionsForSystem(system: SystemCode | null | undefined): AddressSystemConventions | null {
|
|
65
|
+
if (!system) return null
|
|
66
|
+
|
|
67
|
+
return ADDRESS_SYSTEM_CONVENTIONS[system] ?? null
|
|
68
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Australia Post delivery-service designators (Postal Delivery Types) — the Commonwealth po_box
|
|
7
|
+
* vocabulary the US-only `us/po-box.ts` cannot see: `GPO Box 2890`, `Locked Bag 1797`, `Private
|
|
8
|
+
* Bag 7`, plus the rural/community legacy tail (`RMB 4600`, `RSD`, `CMB`).
|
|
9
|
+
*
|
|
10
|
+
* Sourcing (accessed 2026-06-11; the underlying urban/rural addressing standard is AS/NZS 4819,
|
|
11
|
+
* which governs street addressing — the delivery-service designators below are Australia Post's
|
|
12
|
+
* own, from its addressing guidance):
|
|
13
|
+
*
|
|
14
|
+
* - The complete Postal Delivery Type table comes verbatim from Australia Post's barcode addressing
|
|
15
|
+
* booklet ("Hints and tips to get a higher address match rate", SAP 8838883): CARE OF POST
|
|
16
|
+
* OFFICE→CARE PO, COMMUNITY MAIL AGENT→CMA, COMMUNITY MAIL BAG→CMB, GENERAL POST OFFICE
|
|
17
|
+
* BOX→GPO BOX, LOCKED MAIL BAG SERVICE→LOCKED BAG, MAIL SERVICE→MS, POST OFFICE BOX→PO BOX,
|
|
18
|
+
* POSTE RESTANTE→CARE PO, PRIVATE MAIL BAG SERVICE→PRIVATE BAG, ROADSIDE DELIVERY→RSD,
|
|
19
|
+
* ROADSIDE MAIL BAG→RMB, ROADSIDE MAIL BOX→RMB, ROADSIDE MAIL SERVICE→RMS, COMMUNITY POSTAL
|
|
20
|
+
* AGENT→CPA. The same booklet states: "With the exception of Care of Post Office, Community
|
|
21
|
+
* Mail Agent, Community Postal Agent, and Community Mail Bag, all Postal Delivery Types must
|
|
22
|
+
* have an associated number for a match to occur. e.g. PO Box 112", and "'PRIVATE BOX' is not
|
|
23
|
+
* a valid type" (so PRIVATE BOX is deliberately NOT in this table).
|
|
24
|
+
* - Which designators are CURRENT retail products (vs. AMAS-recognized legacy forms) comes from the
|
|
25
|
+
* live auspost.com.au pages: the addressing guidelines ("Line 2 should contain the street
|
|
26
|
+
* number and name, or PO Box or Locked Bag number"), the Correct Addressing brochure (SAP
|
|
27
|
+
* 8833878, Nov 2022 — `GPO Box 123 / SYDNEY NSW 2000` example), the personal "PO Boxes and
|
|
28
|
+
* Private Bags" page (Private Bag: "If you live in a rural or remote area of Australia, you
|
|
29
|
+
* can manage your mail securely with a Private Bag"), and the business "PO Boxes and Locked
|
|
30
|
+
* Bags" page (GPO Box: "Lease a single GPO Box, or the same box number in each capital city
|
|
31
|
+
* with our Common Box service"; Common Box numbers run 9800–9999). PO Box, GPO Box, Locked
|
|
32
|
+
* Bag, and Private Bag appear on those current pages; the rural/community types (RSD, RMB,
|
|
33
|
+
* RMS, MS, CMB, CMA, CPA, Care PO) appear ONLY in the AMAS table and are flagged `legacy` —
|
|
34
|
+
* the parser must still recognize them on older addresses.
|
|
35
|
+
*
|
|
36
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/Barcode_hints_tips.pdf Australia Post barcode addressing booklet (Postal Delivery Type table)}
|
|
37
|
+
* @see {@link https://auspost.com.au/sending/guidelines/addressing-guidelines Australia Post addressing guidelines}
|
|
38
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/correct-addressing.pdf Australia Post Correct Addressing brochure (Nov 2022)}
|
|
39
|
+
* @see {@link https://auspost.com.au/receiving/manage-your-mail/po-boxes-and-private-bags Australia Post — PO Boxes and Private Bags}
|
|
40
|
+
* @see {@link https://auspost.com.au/business/business-admin/po-boxes-and-locked-bags Australia Post — business PO Boxes, GPO Boxes and Locked Bags}
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** One Postal Delivery Type row from the Australia Post AMAS abbreviation table. */
|
|
44
|
+
export interface AuDeliveryServiceDesignator {
|
|
45
|
+
/** The full Postal Delivery Type name, verbatim from the table (uppercase as published). */
|
|
46
|
+
name: string
|
|
47
|
+
/** The standard abbreviation — the surface form written on mail ("GPO BOX", "LOCKED BAG"). */
|
|
48
|
+
abbreviation: string
|
|
49
|
+
/**
|
|
50
|
+
* Whether the designator "must have an associated number for a match to occur" (AMAS rule; exceptions are Care of
|
|
51
|
+
* Post Office, Community Mail Agent, Community Postal Agent, and Community Mail Bag).
|
|
52
|
+
*/
|
|
53
|
+
requiresNumber: boolean
|
|
54
|
+
/**
|
|
55
|
+
* True when the designator is recognized by the AMAS Postal Delivery Type table but absent from every current
|
|
56
|
+
* auspost.com.au addressing/product page (accessed 2026-06-11) — the rural and community forms superseded by rural
|
|
57
|
+
* street addressing under AS/NZS 4819. The parser must still RECOGNIZE these on old addresses; synthesis should
|
|
58
|
+
* weight them low.
|
|
59
|
+
*/
|
|
60
|
+
legacy: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The verbatim Postal Delivery Type table (see the module header for the per-row provenance). Multiple names can share
|
|
65
|
+
* an abbreviation (ROADSIDE MAIL BAG and ROADSIDE MAIL BOX are both RMB; POSTE RESTANTE is addressed as CARE PO).
|
|
66
|
+
*/
|
|
67
|
+
export const AU_DELIVERY_SERVICE_DESIGNATORS = [
|
|
68
|
+
{ name: "GENERAL POST OFFICE BOX", abbreviation: "GPO BOX", requiresNumber: true, legacy: false },
|
|
69
|
+
{ name: "POST OFFICE BOX", abbreviation: "PO BOX", requiresNumber: true, legacy: false },
|
|
70
|
+
{ name: "LOCKED MAIL BAG SERVICE", abbreviation: "LOCKED BAG", requiresNumber: true, legacy: false },
|
|
71
|
+
{ name: "PRIVATE MAIL BAG SERVICE", abbreviation: "PRIVATE BAG", requiresNumber: true, legacy: false },
|
|
72
|
+
{ name: "COMMUNITY MAIL BAG", abbreviation: "CMB", requiresNumber: false, legacy: true },
|
|
73
|
+
{ name: "COMMUNITY MAIL AGENT", abbreviation: "CMA", requiresNumber: false, legacy: true },
|
|
74
|
+
{ name: "COMMUNITY POSTAL AGENT", abbreviation: "CPA", requiresNumber: false, legacy: true },
|
|
75
|
+
{ name: "CARE OF POST OFFICE", abbreviation: "CARE PO", requiresNumber: false, legacy: true },
|
|
76
|
+
{ name: "POSTE RESTANTE", abbreviation: "CARE PO", requiresNumber: false, legacy: true },
|
|
77
|
+
{ name: "MAIL SERVICE", abbreviation: "MS", requiresNumber: true, legacy: true },
|
|
78
|
+
{ name: "ROADSIDE DELIVERY", abbreviation: "RSD", requiresNumber: true, legacy: true },
|
|
79
|
+
{ name: "ROADSIDE MAIL BAG", abbreviation: "RMB", requiresNumber: true, legacy: true },
|
|
80
|
+
{ name: "ROADSIDE MAIL BOX", abbreviation: "RMB", requiresNumber: true, legacy: true },
|
|
81
|
+
{ name: "ROADSIDE MAIL SERVICE", abbreviation: "RMS", requiresNumber: true, legacy: true },
|
|
82
|
+
] as const satisfies readonly AuDeliveryServiceDesignator[]
|
|
83
|
+
|
|
84
|
+
/** A canonical Australia Post Postal Delivery Type abbreviation. */
|
|
85
|
+
export type AuDeliveryServiceAbbreviation = (typeof AU_DELIVERY_SERVICE_DESIGNATORS)[number]["abbreviation"]
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Per-designator surface patterns (designator phrase only, no anchor, no id). Ordered longest / most-specific first so
|
|
89
|
+
* the matcher prefers "GPO Box" over "PO Box" and "RMS" over "MS". Each pattern tolerates the punctuation AMAS tells
|
|
90
|
+
* mailers to strip ("the full stops and commas in R.M.B and P.O.") — recognition must accept what deliverable mail
|
|
91
|
+
* actually carries.
|
|
92
|
+
*
|
|
93
|
+
* MS is special-cased in {@link matchAuDeliveryService}: its identifier must start with a digit so the bare two-letter
|
|
94
|
+
* designator cannot swallow an honorific ("Ms Smith").
|
|
95
|
+
*/
|
|
96
|
+
const DESIGNATOR_PATTERNS: ReadonlyArray<readonly [AuDeliveryServiceAbbreviation, string]> = [
|
|
97
|
+
["GPO BOX", String.raw`general\s+post\s+office\s+box|g\.?\s*p\.?\s*o\.?\s*box`],
|
|
98
|
+
["PO BOX", String.raw`post\s+office\s+box|p\.?\s*o\.?\s*box`],
|
|
99
|
+
["LOCKED BAG", String.raw`locked\s+(?:mail\s+)?bag(?:\s+service)?`],
|
|
100
|
+
["PRIVATE BAG", String.raw`private\s+(?:mail\s+)?bag(?:\s+service)?`],
|
|
101
|
+
["CARE PO", String.raw`care\s+of\s+post\s+office|poste\s+restante|care\s+po`],
|
|
102
|
+
["CMB", String.raw`community\s+mail\s+bag|cmb`],
|
|
103
|
+
["CMA", String.raw`community\s+mail\s+agent|cma`],
|
|
104
|
+
["CPA", String.raw`community\s+postal\s+agent|cpa`],
|
|
105
|
+
["RSD", String.raw`roadside\s+delivery|r\.?\s*s\.?\s*d\.?`],
|
|
106
|
+
["RMB", String.raw`roadside\s+mail\s+(?:bag|box)|r\.?\s*m\.?\s*b\.?`],
|
|
107
|
+
["RMS", String.raw`roadside\s+mail\s+service|rms`],
|
|
108
|
+
["MS", String.raw`mail\s+service|ms`],
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
const DESIGNATOR_INFO = new Map<AuDeliveryServiceAbbreviation, { requiresNumber: boolean; legacy: boolean }>(
|
|
112
|
+
AU_DELIVERY_SERVICE_DESIGNATORS.map((d) => [d.abbreviation, { requiresNumber: d.requiresNumber, legacy: d.legacy }])
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// One anchored regex per designator: phrase + (required|optional) identifier. The id shape matches
|
|
116
|
+
// the US slice ([\dA-Za-z][\dA-Za-z-]*); MS additionally requires a digit-leading id (see above).
|
|
117
|
+
const MATCHERS: ReadonlyArray<{ abbreviation: AuDeliveryServiceAbbreviation; re: RegExp }> = DESIGNATOR_PATTERNS.map(
|
|
118
|
+
([abbreviation, src]) => {
|
|
119
|
+
const { requiresNumber } = DESIGNATOR_INFO.get(abbreviation)!
|
|
120
|
+
const id = abbreviation === "MS" ? String.raw`(\d[\dA-Za-z-]*)` : String.raw`([\dA-Za-z][\dA-Za-z-]*)`
|
|
121
|
+
const tail = requiresNumber ? String.raw`\s*#?\s*${id}` : String.raw`(?:\s*#?\s*${id})?`
|
|
122
|
+
|
|
123
|
+
return { abbreviation, re: new RegExp(String.raw`^\s*(${src})${tail}\s*$`, "i") }
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
/** Result of an AU delivery-service parse. */
|
|
128
|
+
export interface AuDeliveryServiceMatch {
|
|
129
|
+
/** The designator phrase as it appeared ("G.P.O. Box", "Locked Bag"). */
|
|
130
|
+
matched: string
|
|
131
|
+
/** The canonical Postal Delivery Type abbreviation ("GPO BOX", "LOCKED BAG"). */
|
|
132
|
+
designator: AuDeliveryServiceAbbreviation
|
|
133
|
+
/** The delivery-service number when present ("9999", "4600"). */
|
|
134
|
+
id?: string
|
|
135
|
+
/** True when the designator is an AMAS-only legacy form (see the table). */
|
|
136
|
+
legacy: boolean
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* If `input` is a standalone Australia Post delivery-service phrase ("GPO Box 2890", "Locked Bag 1797", "RMB 4600",
|
|
141
|
+
* bare "CMB"), return the canonical designator, the id, and the legacy flag. Null otherwise — including for "Private
|
|
142
|
+
* Box", which Australia Post explicitly calls out as not a valid type.
|
|
143
|
+
*/
|
|
144
|
+
export function matchAuDeliveryService(input: unknown): AuDeliveryServiceMatch | null {
|
|
145
|
+
if (typeof input !== "string") return null
|
|
146
|
+
|
|
147
|
+
for (const { abbreviation, re } of MATCHERS) {
|
|
148
|
+
const m = re.exec(input)
|
|
149
|
+
|
|
150
|
+
if (!m) continue
|
|
151
|
+
const info = DESIGNATOR_INFO.get(abbreviation)!
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
matched: m[1]!.trim(),
|
|
155
|
+
designator: abbreviation,
|
|
156
|
+
...(m[2] ? { id: m[2] } : {}),
|
|
157
|
+
legacy: info.legacy,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Type-predicate: does the input look like a standalone AU delivery-service address line? */
|
|
165
|
+
export function isAuDeliveryService(input: unknown): boolean {
|
|
166
|
+
return matchAuDeliveryService(input) !== null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Normalize a recognized delivery-service phrase to the canonical AMAS form (`"g.p.o. box 123"` → `"GPO BOX 123"`).
|
|
171
|
+
* Returns the input unchanged if it isn't one.
|
|
172
|
+
*/
|
|
173
|
+
export function normalizeAuDeliveryService(input: string): string {
|
|
174
|
+
const m = matchAuDeliveryService(input)
|
|
175
|
+
|
|
176
|
+
if (!m) return input
|
|
177
|
+
|
|
178
|
+
return m.id ? `${m.designator} ${m.id.toUpperCase()}` : m.designator
|
|
179
|
+
}
|
package/au/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The Australian address system (Australia Post / ISO 3166-2:AU; street addressing per AS/NZS
|
|
7
|
+
* 4819): delivery-service designators (GPO Box, Locked Bag, Private Bag, the rural legacy tail),
|
|
8
|
+
* AMAS / AS 4590.1 floor/level designators (Level 3, Ground Floor, Mezzanine), states and
|
|
9
|
+
* territories, and the 4-digit postcode.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./delivery-service.ts"
|
|
13
|
+
export * from "./level-designator.ts"
|
|
14
|
+
export * from "./postcode.ts"
|
|
15
|
+
export * from "./state.ts"
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Australia Post AMAS floor/level designators — the sub-premise vocabulary that names a FLOOR of a
|
|
7
|
+
* building rather than a numbered unit on that floor: `Level 3`, `L 12`, `Ground Floor`,
|
|
8
|
+
* `Mezzanine`.
|
|
9
|
+
*
|
|
10
|
+
* Sourcing (accessed 2026-06-12):
|
|
11
|
+
*
|
|
12
|
+
* - **Australia Post AMAS** (Address Matching Approval System) defines the Level Designator type and
|
|
13
|
+
* its approved abbreviation. The verbatim AMAS description: "LEVEL" is the full word; "L" is
|
|
14
|
+
* the approved abbreviation. The standard phrase format is `LEVEL <number>`, e.g. "LEVEL 3"
|
|
15
|
+
* or "L 3" (abbreviation always uppercase in AMAS output). Australia Post's own barcode
|
|
16
|
+
* addressing booklet (SAP 8838883) lists "LEVEL" and "L" as the level type; the Correct
|
|
17
|
+
* Addressing brochure (SAP 8833878, Nov 2022) gives the example `LEVEL 3 / 60 MARGARET ST /
|
|
18
|
+
* SYDNEY NSW 2000`. The AMAS Data Extract Format document (v4.2) confirms "LEVEL" → "L" as
|
|
19
|
+
* the sole level abbreviation pair.
|
|
20
|
+
* - **Ground floor** is treated by AMAS as `LEVEL G` (with the identifier "G"). Australia Post's
|
|
21
|
+
* addressing guidelines state that ground floor should be written as "LEVEL G"; the full word
|
|
22
|
+
* "GROUND" is a recognized alias for the identifier, not a separate designator type.
|
|
23
|
+
* - **Mezzanine**, **Lower Ground**, and **Upper Ground** appear in AS 4590.1-2017 (the Australian
|
|
24
|
+
* Standard for interchange of client information) as recognized level-type values alongside
|
|
25
|
+
* LEVEL and GROUND. AS 4590.1-2017 Table 3 "Level type": B (Basement), G (Ground), MEZZANINE
|
|
26
|
+
* (M), LG (Lower Ground), UG (Upper Ground), L (Level), OD (Observation Deck), P (Parking /
|
|
27
|
+
* Podium), RT (Rooftop). These are the values a Geocoded National Address File (GNAF) record
|
|
28
|
+
* may carry in the LEVEL_TYPE_CODE column, which mirrors the AP AMAS level-type vocabulary.
|
|
29
|
+
* - "LVL" and "LG" appear as widely-recognized surface variants in real AU addresses (Open Addresses
|
|
30
|
+
* AU export, accessed via OpenAddresses) though AS 4590.1-2017 and AMAS canonicalize to "L"
|
|
31
|
+
* and "LG" respectively; the variants are included in {@link AU_LEVEL_DESIGNATOR_VARIANTS} so
|
|
32
|
+
* the parser can RECOGNIZE them without synthesizing them.
|
|
33
|
+
*
|
|
34
|
+
* @see {@link https://auspost.com.au/sending/guidelines/addressing-guidelines Australia Post addressing guidelines}
|
|
35
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/correct-addressing.pdf Australia Post Correct Addressing brochure (Nov 2022)}
|
|
36
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/Barcode_hints_tips.pdf Australia Post barcode addressing booklet}
|
|
37
|
+
* @see {@link https://www.iso.org/standard/67840.html AS 4590.1-2017 — Interchange of client information}
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One AMAS / AS 4590.1 level-type row.
|
|
42
|
+
*
|
|
43
|
+
* The `type` is the AS 4590.1 LEVEL_TYPE_CODE value (what GNAF and AMAS use internally); the `abbreviation` is the
|
|
44
|
+
* approved surface form used in formatted mail; the `requiresNumber` flag distinguishes designators that take a floor
|
|
45
|
+
* identifier from standalone ones.
|
|
46
|
+
*/
|
|
47
|
+
export interface AuLevelDesignator {
|
|
48
|
+
/** AS 4590.1 LEVEL_TYPE_CODE (the GNAF / AMAS internal code). */
|
|
49
|
+
code: string
|
|
50
|
+
/** Full descriptive name (AMAS table label). */
|
|
51
|
+
name: string
|
|
52
|
+
/** The approved AMAS surface abbreviation written on mail ("L", "B", "M"). */
|
|
53
|
+
abbreviation: string
|
|
54
|
+
/**
|
|
55
|
+
* True when the designator takes a numeric or alphanumeric floor identifier after it (`LEVEL 3`, `BASEMENT 2`). False
|
|
56
|
+
* for standalone types (`GROUND`, `MEZZANINE`, `ROOFTOP`) that name a specific well-known floor by vocabulary alone.
|
|
57
|
+
*/
|
|
58
|
+
requiresNumber: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* AMAS / AS 4590.1-2017 level-type table (Table 3). Verbatim codes; see the module header for provenance. Ordered with
|
|
63
|
+
* the most-common forms first for match priority.
|
|
64
|
+
*/
|
|
65
|
+
export const AU_LEVEL_DESIGNATORS = [
|
|
66
|
+
{ code: "L", name: "LEVEL", abbreviation: "L", requiresNumber: true },
|
|
67
|
+
{ code: "G", name: "GROUND", abbreviation: "G", requiresNumber: false },
|
|
68
|
+
{ code: "B", name: "BASEMENT", abbreviation: "B", requiresNumber: true },
|
|
69
|
+
{ code: "M", name: "MEZZANINE", abbreviation: "M", requiresNumber: false },
|
|
70
|
+
{ code: "LG", name: "LOWER GROUND", abbreviation: "LG", requiresNumber: false },
|
|
71
|
+
{ code: "UG", name: "UPPER GROUND", abbreviation: "UG", requiresNumber: false },
|
|
72
|
+
{ code: "OD", name: "OBSERVATION DECK", abbreviation: "OD", requiresNumber: false },
|
|
73
|
+
{ code: "P", name: "PARKING", abbreviation: "P", requiresNumber: true },
|
|
74
|
+
{ code: "RT", name: "ROOFTOP", abbreviation: "RT", requiresNumber: false },
|
|
75
|
+
] as const satisfies readonly AuLevelDesignator[]
|
|
76
|
+
|
|
77
|
+
/** A canonical AS 4590.1 LEVEL_TYPE_CODE. */
|
|
78
|
+
export type AuLevelCode = (typeof AU_LEVEL_DESIGNATORS)[number]["code"]
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Recognized surface variants for each AMAS level code — the canonical code/abbreviation pair PLUS additional forms
|
|
82
|
+
* found in real AU addresses (Open Addresses export) that the parser must RECOGNIZE but the synthesis layer should not
|
|
83
|
+
* favor over the canonical form.
|
|
84
|
+
*
|
|
85
|
+
* Synthesis uses only the first element (the AMAS canonical surface). Recognition accepts all.
|
|
86
|
+
*/
|
|
87
|
+
export const AU_LEVEL_DESIGNATOR_VARIANTS: Readonly<Record<AuLevelCode, readonly string[]>> = {
|
|
88
|
+
L: ["L", "LEVEL", "LVL", "LEVL", "LEV"],
|
|
89
|
+
G: ["G", "GROUND", "GRD", "GF", "GROUND FLOOR"],
|
|
90
|
+
B: ["B", "BASEMENT", "BSMT", "LOWER LEVEL"],
|
|
91
|
+
M: ["M", "MEZZANINE", "MEZZ", "MEZZANINE LEVEL"],
|
|
92
|
+
LG: ["LG", "LOWER GROUND", "LOWER GROUND FLOOR"],
|
|
93
|
+
UG: ["UG", "UPPER GROUND", "UPPER GROUND FLOOR"],
|
|
94
|
+
OD: ["OD", "OBSERVATION DECK"],
|
|
95
|
+
P: ["P", "PARKING", "PODIUM"],
|
|
96
|
+
RT: ["RT", "ROOFTOP", "ROOF"],
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Inverse lookup: every variant (abbreviation or surface form) → the canonical AMAS code. Lowercase-keyed for
|
|
101
|
+
* case-insensitive matching (`"level"` → `"L"`, `"bsmt"` → `"B"`).
|
|
102
|
+
*/
|
|
103
|
+
export const AU_LEVEL_DESIGNATOR_LOOKUP: ReadonlyMap<string, AuLevelCode> = (() => {
|
|
104
|
+
// Structural integrity check: every code must have at least one non-empty variant. Throw at
|
|
105
|
+
// module load time so a malformed table entry fails loud rather than silently producing an empty
|
|
106
|
+
// lexicon (the "builder must round-trip loud" rule from the task contract).
|
|
107
|
+
for (const { code } of AU_LEVEL_DESIGNATORS) {
|
|
108
|
+
const variants = AU_LEVEL_DESIGNATOR_VARIANTS[code]
|
|
109
|
+
|
|
110
|
+
if (!variants || variants.length === 0) {
|
|
111
|
+
throw new Error(`[codex/au/level-designator] code "${code}" has no variants in AU_LEVEL_DESIGNATOR_VARIANTS`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const v of variants) {
|
|
115
|
+
if (!v || !v.trim()) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`[codex/au/level-designator] code "${code}" has an empty or blank variant in AU_LEVEL_DESIGNATOR_VARIANTS`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const out = new Map<string, AuLevelCode>()
|
|
123
|
+
|
|
124
|
+
for (const { code } of AU_LEVEL_DESIGNATORS) {
|
|
125
|
+
for (const variant of AU_LEVEL_DESIGNATOR_VARIANTS[code]) {
|
|
126
|
+
const key = variant.toLowerCase()
|
|
127
|
+
|
|
128
|
+
if (!out.has(key)) {
|
|
129
|
+
out.set(key, code)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return out
|
|
135
|
+
})()
|
|
136
|
+
|
|
137
|
+
/** Result of an AU level designator parse. */
|
|
138
|
+
export interface AuLevelDesignatorMatch {
|
|
139
|
+
/** The code as it appeared in the input ("Level", "L", "lvl"). */
|
|
140
|
+
matched: string
|
|
141
|
+
/** The canonical AS 4590.1 LEVEL_TYPE_CODE ("L", "B", "M"). */
|
|
142
|
+
code: AuLevelCode
|
|
143
|
+
/** The floor identifier when present ("3", "G", "B2"). */
|
|
144
|
+
identifier?: string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// One regex per level code. Multi-word variants ("LOWER GROUND", "GROUND FLOOR") are matched
|
|
148
|
+
// before their shorter constituents by ordering the variant list longest-first within each code.
|
|
149
|
+
const LEVEL_MATCHERS: ReadonlyArray<{ code: AuLevelCode; requiresNumber: boolean; re: RegExp }> = (() => {
|
|
150
|
+
const rows: Array<{ code: AuLevelCode; requiresNumber: boolean; re: RegExp }> = []
|
|
151
|
+
|
|
152
|
+
for (const { code, requiresNumber } of AU_LEVEL_DESIGNATORS) {
|
|
153
|
+
const variants = [...AU_LEVEL_DESIGNATOR_VARIANTS[code]]
|
|
154
|
+
.sort((a, b) => b.length - a.length)
|
|
155
|
+
.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, String.raw`\s+`))
|
|
156
|
+
const alts = variants.join("|")
|
|
157
|
+
// Identifier: optional alphanumeric (B2, 12, G). requiresNumber=true → identifier required.
|
|
158
|
+
const tail = requiresNumber
|
|
159
|
+
? String.raw`\s+([A-Za-z]?\d[\dA-Za-z-]*|\d[\dA-Za-z-]*)`
|
|
160
|
+
: String.raw`(?:\s+([A-Za-z]?\d[\dA-Za-z-]*|\d[\dA-Za-z-]*))?`
|
|
161
|
+
rows.push({ code, requiresNumber, re: new RegExp(String.raw`^\s*(${alts})${tail}\s*$`, "i") })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return rows
|
|
165
|
+
})()
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* If `input` is a standalone AU level designator phrase ("Level 3", "L 12", "Ground Floor", "Mezzanine", "B 2"), return
|
|
169
|
+
* the canonical code and identifier. Null otherwise. Malformed entries (a requires-number designator with no
|
|
170
|
+
* identifier, e.g. bare "Level") return null — the builder throws loudly when a row in a table violates this
|
|
171
|
+
* constraint.
|
|
172
|
+
*/
|
|
173
|
+
export function matchAuLevelDesignator(input: unknown): AuLevelDesignatorMatch | null {
|
|
174
|
+
if (typeof input !== "string") return null
|
|
175
|
+
|
|
176
|
+
for (const { code, re } of LEVEL_MATCHERS) {
|
|
177
|
+
const m = re.exec(input)
|
|
178
|
+
|
|
179
|
+
if (!m) continue
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
matched: m[1]!.trim(),
|
|
183
|
+
code,
|
|
184
|
+
...(m[2] ? { identifier: m[2] } : {}),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Type-predicate: does the input look like a standalone AU level designator phrase? */
|
|
192
|
+
export function isAuLevelDesignator(input: unknown): boolean {
|
|
193
|
+
return matchAuLevelDesignator(input) !== null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Normalize a recognized level phrase to the AMAS canonical form (`"level 3"` → `"L 3"`, `"ground floor"` → `"G"`).
|
|
198
|
+
* Returns the input unchanged if it isn't a level designator phrase. Throws if a row in {@link AU_LEVEL_DESIGNATORS} is
|
|
199
|
+
* malformed (requires-number entry with no abbreviation or empty name) — the builder must surface structural defects
|
|
200
|
+
* loudly.
|
|
201
|
+
*/
|
|
202
|
+
export function normalizeAuLevelDesignator(input: string): string {
|
|
203
|
+
const m = matchAuLevelDesignator(input)
|
|
204
|
+
|
|
205
|
+
if (!m) return input
|
|
206
|
+
const row = AU_LEVEL_DESIGNATORS.find((r) => r.code === m.code)!
|
|
207
|
+
|
|
208
|
+
return m.identifier ? `${row.abbreviation} ${m.identifier.toUpperCase()}` : row.abbreviation
|
|
209
|
+
}
|
package/au/postcode.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Australian postcodes: four digits, written at the end of the last address line after the locality
|
|
7
|
+
* and state (`SYDNEY NSW 2000`). Sourcing (accessed 2026-06-11):
|
|
8
|
+
*
|
|
9
|
+
* - Australia Post's Correct Addressing brochure (SAP 8833878, Nov 2022) — every example carries a
|
|
10
|
+
* 4-digit postcode and the brochure references envelopes "with preprinted four postcode
|
|
11
|
+
* squares".
|
|
12
|
+
* - The barcode addressing booklet documents the coarse first-digit → state prior: "if the Postcode
|
|
13
|
+
* falls in the range 3000-3999 the State abbreviation will be VIC; 4000-4999 will be QLD,
|
|
14
|
+
* etc. … Exceptions to this include ACT Postcodes and Postcodes located on State borders."
|
|
15
|
+
* Because the booklet only enumerates VIC and QLD and flags exceptions, this module
|
|
16
|
+
* deliberately does NOT ship a full first-digit → state table — the shape is the contract,
|
|
17
|
+
* the geographic prior is the gazetteer's job.
|
|
18
|
+
*
|
|
19
|
+
* Note the shape collides with New Zealand's (also 4 digits) — `candidateSystemsForPostcode`
|
|
20
|
+
* returns both, and that ambiguity is by design (shape test, not membership test).
|
|
21
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/correct-addressing.pdf Australia Post Correct Addressing brochure (Nov 2022)}
|
|
22
|
+
* @see {@link https://auspost.com.au/content/dam/auspost_corp/media/documents/Barcode_hints_tips.pdf Australia Post barcode addressing booklet}
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Tagged } from "type-fest"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* An Australian postcode: four digits.
|
|
29
|
+
*
|
|
30
|
+
* @category Postal
|
|
31
|
+
* @type string
|
|
32
|
+
* @title Australian postcode
|
|
33
|
+
* @pattern ^\d{4}$
|
|
34
|
+
*/
|
|
35
|
+
export type AuPostcode = Tagged<string, "AuPostcode">
|
|
36
|
+
|
|
37
|
+
/** The AU postcode shape: exactly four digits. */
|
|
38
|
+
export const AU_POSTCODE_PATTERN = /^\d{4}$/
|
|
39
|
+
|
|
40
|
+
/** Normalize a postcode surface form (trim only — AU has no country-prefix courtesy form). */
|
|
41
|
+
export function normalizeAuPostcode(raw: unknown): AuPostcode | null {
|
|
42
|
+
if (typeof raw !== "string") return null
|
|
43
|
+
const s = raw.trim()
|
|
44
|
+
|
|
45
|
+
return AU_POSTCODE_PATTERN.test(s) ? (s as AuPostcode) : null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Type-predicate for a (normalized) Australian postcode. */
|
|
49
|
+
export function isAuPostcode(input: unknown): input is AuPostcode {
|
|
50
|
+
return typeof input === "string" && AU_POSTCODE_PATTERN.test(input)
|
|
51
|
+
}
|
package/au/state.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Australian states and territories (ISO 3166-2:AU). Australia Post's addressing guidelines put the
|
|
7
|
+
* state abbreviation on the last line between the locality and the 4-digit postcode ("Line 3
|
|
8
|
+
* should contain the locality or suburb, state and postcode and be written in capital letters" —
|
|
9
|
+
* addressing guidelines, accessed 2026-06-11), e.g. `SYDNEY NSW 2000`, `BUNBURY WA 6230`, `EUROA
|
|
10
|
+
* VIC 3664`. The abbreviation set is the ISO 3166-2:AU subdivision codes, which are the same
|
|
11
|
+
* codes Australia Post's own examples use (NSW, VIC, WA, QLD, ACT appear across the addressing
|
|
12
|
+
* guidelines and the barcode booklet).
|
|
13
|
+
* @see {@link https://auspost.com.au/sending/guidelines/addressing-guidelines Australia Post addressing guidelines}
|
|
14
|
+
* @see {@link https://www.iso.org/obp/ui/#iso:code:3166:AU ISO 3166-2:AU}
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** State/territory abbreviation → full name (ISO 3166-2:AU subdivision set). */
|
|
18
|
+
export const AU_STATE_ABBREVIATIONS = {
|
|
19
|
+
ACT: "Australian Capital Territory",
|
|
20
|
+
NSW: "New South Wales",
|
|
21
|
+
NT: "Northern Territory",
|
|
22
|
+
QLD: "Queensland",
|
|
23
|
+
SA: "South Australia",
|
|
24
|
+
TAS: "Tasmania",
|
|
25
|
+
VIC: "Victoria",
|
|
26
|
+
WA: "Western Australia",
|
|
27
|
+
} as const satisfies Record<string, string>
|
|
28
|
+
|
|
29
|
+
/** An Australian state/territory abbreviation as written on the last address line. */
|
|
30
|
+
export type AuStateAbbreviation = keyof typeof AU_STATE_ABBREVIATIONS
|
|
31
|
+
|
|
32
|
+
/** Type-predicate for an AU state/territory abbreviation (case-insensitive). */
|
|
33
|
+
export function isAuStateAbbreviation(input: unknown): input is AuStateAbbreviation {
|
|
34
|
+
return typeof input === "string" && Object.hasOwn(AU_STATE_ABBREVIATIONS, input.toUpperCase())
|
|
35
|
+
}
|
package/ca/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The Canadian address system (Canada Post / ISO 3166-2:CA): bilingual street types, alphanumeric
|
|
7
|
+
* postal codes, and the provinces and territories.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from "./postal-code.ts"
|
|
11
|
+
export * from "./province.ts"
|
|
12
|
+
export * from "./street-type.ts"
|