@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.
- 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 +14 -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/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/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,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS Publication 28 — Military / Diplomatic Post Office designators.
|
|
7
|
+
*
|
|
8
|
+
* Military and diplomatic overseas addresses use a distinct addressing scheme: instead of a city
|
|
9
|
+
* name, a standardized location-class code (APO, FPO, DPO) appears on the city line, followed by
|
|
10
|
+
* the armed-forces "state" code (AA, AE, AP) and a 09xxx ZIP code. The unit/PSC/CMR line
|
|
11
|
+
* appearing above the city line identifies the specific installation unit, postal service center,
|
|
12
|
+
* or community mail room.
|
|
13
|
+
*
|
|
14
|
+
* Sourcing (accessed 2026-06-12):
|
|
15
|
+
*
|
|
16
|
+
* - **USPS Publication 28, Chapter 7** ("Armed Forces and Diplomatic Post Offices") defines the three
|
|
17
|
+
* armed-forces location codes and three state-code regions, and states: "Use APO with AA
|
|
18
|
+
* (Americas), AE (Europe/Middle East/Africa/Canada), or AP (Pacific)"; "FPO (Fleet Post
|
|
19
|
+
* Office) for Navy ships and shore installations"; "DPO (Diplomatic Post Office) for US
|
|
20
|
+
* embassies and missions." Full URL: https://pe.usps.com/text/pub28/28c7_001.htm
|
|
21
|
+
* - **USPS Publication 28, Appendix B** gives the complete list of accepted unit-line formats: `UNIT
|
|
22
|
+
* <id>`, `PSC <id> BOX <box>`, `CMR <id> BOX <box>`, and `UNIT <id> BOX <box>`. The same
|
|
23
|
+
* appendix notes the two-digit unit ranges for PSC/CMR/UNIT assignment by theater. Full URL:
|
|
24
|
+
* https://pe.usps.com/text/pub28/28apb_001.htm
|
|
25
|
+
* - The **Armed Forces "state" codes** (AA, AE, AP) are defined in the same USPS appendix and are
|
|
26
|
+
* also the official USPS abbreviations for the three Armed Forces addressing regions. See:
|
|
27
|
+
* https://pe.usps.com/text/pub28/28apb_002.htm
|
|
28
|
+
* - "DPO" for Diplomatic Post Offices was added as a distinct code in 2011 (USPS Customer/ Industry
|
|
29
|
+
* Notice 61). It does NOT replace APO in diplomatic mail — both exist, with DPO used
|
|
30
|
+
* specifically for State Department overseas posts and APO/FPO retained for DoD.
|
|
31
|
+
*
|
|
32
|
+
* @see {@link https://pe.usps.com/text/pub28/28c7_001.htm USPS Pub 28 Chapter 7 — Military Addresses}
|
|
33
|
+
* @see {@link https://pe.usps.com/text/pub28/28apb_001.htm USPS Pub 28 Appendix B — Armed Forces Addresses}
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** USPS military / diplomatic post-office location codes (the "city" substitute on the city line). */
|
|
37
|
+
export const US_MILITARY_POST_OFFICE_CODES = [
|
|
38
|
+
/**
|
|
39
|
+
* Army Post Office — domestic USPS gateway for Army and Air Force overseas mail; also used for some diplomatic
|
|
40
|
+
* addresses (DPO is preferred for State Dept posts since 2011).
|
|
41
|
+
*/
|
|
42
|
+
{ code: "APO", name: "Army Post Office", armedForces: true },
|
|
43
|
+
/** Fleet Post Office — Navy ships and shore installations. */
|
|
44
|
+
{ code: "FPO", name: "Fleet Post Office", armedForces: true },
|
|
45
|
+
/** Diplomatic Post Office — US embassies and missions (added 2011). */
|
|
46
|
+
{ code: "DPO", name: "Diplomatic Post Office", armedForces: false },
|
|
47
|
+
] as const
|
|
48
|
+
|
|
49
|
+
export type USMilitaryPostOfficeCode = (typeof US_MILITARY_POST_OFFICE_CODES)[number]["code"]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* USPS Armed Forces "state" codes used in place of state names on military/diplomatic addresses. These appear where a
|
|
53
|
+
* US state abbreviation (NY, CA, …) would appear in a civilian address.
|
|
54
|
+
*/
|
|
55
|
+
export const US_ARMED_FORCES_REGIONS = [
|
|
56
|
+
{ code: "AA", name: "Armed Forces Americas", description: "Americas (excluding Canada)" },
|
|
57
|
+
{ code: "AE", name: "Armed Forces Europe", description: "Europe, Middle East, Africa, and Canada" },
|
|
58
|
+
{ code: "AP", name: "Armed Forces Pacific", description: "Pacific" },
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
export type USArmedForcesRegionCode = (typeof US_ARMED_FORCES_REGIONS)[number]["code"]
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* USPS Pub 28 Appendix B unit-line designators for military/diplomatic overseas addresses. Each designator introduces
|
|
65
|
+
* an installation identifier and optionally a box number.
|
|
66
|
+
*
|
|
67
|
+
* Format rules per Appendix B:
|
|
68
|
+
*
|
|
69
|
+
* - `PSC <id> BOX <box>` — Postal Service Center
|
|
70
|
+
* - `CMR <id> BOX <box>` — Community Mail Room
|
|
71
|
+
* - `UNIT <id> BOX <box>` — numbered unit (battalion/company); UNIT may stand alone with just an id and no BOX when the
|
|
72
|
+
* unit has direct mail delivery
|
|
73
|
+
*
|
|
74
|
+
* BOX is required for PSC and CMR; UNIT may omit BOX.
|
|
75
|
+
*/
|
|
76
|
+
export const US_MILITARY_UNIT_DESIGNATORS = [
|
|
77
|
+
{
|
|
78
|
+
code: "PSC",
|
|
79
|
+
name: "Postal Service Center",
|
|
80
|
+
requiresBox: true,
|
|
81
|
+
description: "Installation-level postal service center; format: PSC <id> BOX <box>",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: "CMR",
|
|
85
|
+
name: "Community Mail Room",
|
|
86
|
+
requiresBox: true,
|
|
87
|
+
description: "Sub-installation mail room; format: CMR <id> BOX <box>",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
code: "UNIT",
|
|
91
|
+
name: "Unit",
|
|
92
|
+
requiresBox: false,
|
|
93
|
+
description: "Numbered military unit (battalion/company); format: UNIT <id> [BOX <box>]",
|
|
94
|
+
},
|
|
95
|
+
] as const
|
|
96
|
+
|
|
97
|
+
export type USMilitaryUnitDesignatorCode = (typeof US_MILITARY_UNIT_DESIGNATORS)[number]["code"]
|
|
98
|
+
|
|
99
|
+
/** Result of a military address line parse (the unit line: PSC/CMR/UNIT). */
|
|
100
|
+
export interface USMilitaryUnitMatch {
|
|
101
|
+
/** The designator as it appeared ("PSC", "CMR", "Unit"). */
|
|
102
|
+
matched: string
|
|
103
|
+
/** The canonical designator code ("PSC", "CMR", "UNIT"). */
|
|
104
|
+
code: USMilitaryUnitDesignatorCode
|
|
105
|
+
/** The installation identifier ("1520", "453"). */
|
|
106
|
+
id: string
|
|
107
|
+
/** The box number when present ("4620", "1234A"). */
|
|
108
|
+
box?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Unit-line regex: PSC/CMR/UNIT <id> [BOX <box>]
|
|
112
|
+
// Identifiers are numeric; box numbers are alphanumeric. UNIT may stand without BOX.
|
|
113
|
+
const UNIT_LINE_RE = /^\s*(psc|cmr|unit)\s+(\d+)(?:\s+box\s+([\dA-Za-z]+))?\s*$/i
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* If `input` is a USPS military unit-line ("PSC 1520 BOX 4620", "CMR 453 BOX 100", "UNIT 7 BOX 234A", "UNIT 7"), return
|
|
117
|
+
* the canonical designator, installation id, and optional box. Null otherwise. Throws on a PSC or CMR line without a
|
|
118
|
+
* BOX component (per Appendix B, BOX is required for PSC/CMR; a bare "PSC 1520" is malformed).
|
|
119
|
+
*/
|
|
120
|
+
export function matchMilitaryUnitLine(input: unknown): USMilitaryUnitMatch | null {
|
|
121
|
+
if (typeof input !== "string") return null
|
|
122
|
+
const m = UNIT_LINE_RE.exec(input)
|
|
123
|
+
|
|
124
|
+
if (!m) return null
|
|
125
|
+
const code = m[1]!.toUpperCase() as USMilitaryUnitDesignatorCode
|
|
126
|
+
const id = m[2]!
|
|
127
|
+
const box = m[3]
|
|
128
|
+
|
|
129
|
+
const row = US_MILITARY_UNIT_DESIGNATORS.find((r) => r.code === code)!
|
|
130
|
+
|
|
131
|
+
if (row.requiresBox && !box) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`[codex/us/military-address] ${code} line requires a BOX component per USPS Pub 28 Appendix B; got bare "${input.trim()}"`
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { matched: m[1]!, code, id, ...(box ? { box } : {}) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Type-predicate: does the input look like a USPS military unit line (PSC/CMR/UNIT)? */
|
|
141
|
+
export function isMilitaryUnitLine(input: unknown): boolean {
|
|
142
|
+
try {
|
|
143
|
+
return matchMilitaryUnitLine(input) !== null
|
|
144
|
+
} catch {
|
|
145
|
+
return false // PSC/CMR without BOX is structurally malformed (not a false negative)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Result of a military city-line parse (APO/FPO/DPO + region code + ZIP). */
|
|
150
|
+
export interface USMilitaryCityMatch {
|
|
151
|
+
/** The post-office code as it appeared ("APO", "FPO", "DPO"). */
|
|
152
|
+
matched: string
|
|
153
|
+
/** The canonical post-office code. */
|
|
154
|
+
code: USMilitaryPostOfficeCode
|
|
155
|
+
/** The Armed Forces region code ("AA", "AE", "AP"). */
|
|
156
|
+
region: USArmedForcesRegionCode
|
|
157
|
+
/**
|
|
158
|
+
* The 5-digit or 9-digit ZIP code. Typical ranges per Pub 28: 09xxx (AE), 34xxx (AA), 96xxx (AP) — range validation
|
|
159
|
+
* per region is caller responsibility.
|
|
160
|
+
*/
|
|
161
|
+
zip: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// City-line regex: APO/FPO/DPO <region> <zip>
|
|
165
|
+
// USPS military ZIP assignment per Pub 28 and the Armed Forces zip code list:
|
|
166
|
+
// - AA (Americas): 340xx range
|
|
167
|
+
// - AE (Europe/ME/Africa/Canada): 09xxx range
|
|
168
|
+
// - AP (Pacific): 962xx-966xx range
|
|
169
|
+
// The regex accepts any 5-digit or 9-digit ZIP code in combination with a valid region code —
|
|
170
|
+
// validating the specific numeric range for each region is left to the caller (region+ZIP
|
|
171
|
+
// co-validation is operational policy, not structural syntax).
|
|
172
|
+
const CITY_LINE_RE = /^\s*(apo|fpo|dpo)\s+(aa|ae|ap)\s+(\d{5}(?:-\d{4})?)\s*$/i
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* If `input` is a USPS military city line ("APO AE 09165", "FPO AP 96602-1254", "DPO AE 09498", "APO AA 34022", "APO AP
|
|
176
|
+
* 96525"), return the canonical code, region, and ZIP. Null otherwise.
|
|
177
|
+
*
|
|
178
|
+
* ZIP ranges per Pub 28: AE (Europe/ME/Africa/Canada) → 09xxx; AP (Pacific) → 96xxx; AA (Americas) → 34xxx. Range
|
|
179
|
+
* validation per region is left to the caller; the matcher accepts any 5. or 9-digit ZIP paired with a valid region
|
|
180
|
+
* code.
|
|
181
|
+
*/
|
|
182
|
+
export function matchMilitaryCityLine(input: unknown): USMilitaryCityMatch | null {
|
|
183
|
+
if (typeof input !== "string") return null
|
|
184
|
+
const m = CITY_LINE_RE.exec(input)
|
|
185
|
+
|
|
186
|
+
if (!m) return null
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
matched: m[1]!.toUpperCase(),
|
|
190
|
+
code: m[1]!.toUpperCase() as USMilitaryPostOfficeCode,
|
|
191
|
+
region: m[2]!.toUpperCase() as USArmedForcesRegionCode,
|
|
192
|
+
zip: m[3]!,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Type-predicate: does the input look like a USPS military city line (APO/FPO/DPO + region + ZIP)? */
|
|
197
|
+
export function isMilitaryCityLine(input: unknown): boolean {
|
|
198
|
+
return matchMilitaryCityLine(input) !== null
|
|
199
|
+
}
|
package/us/po-box.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS PO Box recognition + normalization to the surface forms that actually occur
|
|
7
|
+
* (case-insensitive, punctuated "P.O. Box", spelled-out "Post Office Box", bare "Box"). A PO box
|
|
8
|
+
* isn't a closed vocabulary like street suffixes — it's a designator phrase + a box id — so the
|
|
9
|
+
* API is a detector ({@link isPOBox}), a normalizer ({@link normalizePOBox}), and an extractor
|
|
10
|
+
* ({@link matchPOBox}) the corpus po_box synth/parsing can reuse instead of re-deriving the
|
|
11
|
+
* regex.
|
|
12
|
+
* @see {@link https://pe.usps.com/text/pub28/28c2_012.htm USPS Pub 28 §29 (PO Box / Caller service)}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* USPS designator phrases that introduce a post-office-box identifier, longest-first so the matcher prefers the most
|
|
17
|
+
* specific phrase ("Post Office Box" before "Box"). Each is matched case-insensitively with flexible internal
|
|
18
|
+
* punctuation/spacing.
|
|
19
|
+
*/
|
|
20
|
+
export const US_PO_BOX_DESIGNATORS = [
|
|
21
|
+
"POST OFFICE BOX",
|
|
22
|
+
"PO BOX",
|
|
23
|
+
"P O BOX",
|
|
24
|
+
"FIRM CALLER",
|
|
25
|
+
"CALLER",
|
|
26
|
+
"DRAWER",
|
|
27
|
+
"LOCKBOX",
|
|
28
|
+
"BOX",
|
|
29
|
+
] as const satisfies readonly string[]
|
|
30
|
+
|
|
31
|
+
export type USPoBoxDesignator = (typeof US_PO_BOX_DESIGNATORS)[number]
|
|
32
|
+
|
|
33
|
+
// Matches a leading PO-box designator + its identifier. Allows "P.O. Box", "PO BOX", "Post Office
|
|
34
|
+
// Box", "Box 12-A", etc. The id is alphanumeric with optional dashes (USPS caller/firm ids exist).
|
|
35
|
+
const PO_BOX_RE =
|
|
36
|
+
/^\s*(?:(p\.?\s*o\.?\s*box)|(post\s+office\s+box)|(firm\s+caller)|(caller)|(drawer)|(lockbox)|(box))\s*#?\s*([\dA-Za-z][\dA-Za-z-]*)\s*$/i
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type-predicate: does the input look like a standalone PO Box address? Case-insensitive and tolerant of "P.O. Box",
|
|
40
|
+
* "Post Office Box", "Box 12", "PMB"-style ids. (Widens the original isp-nexus `/^PO BOX [\d-]+$/`, which only matched
|
|
41
|
+
* all-caps "PO BOX 123".)
|
|
42
|
+
*/
|
|
43
|
+
export function isPOBox(input: unknown): boolean {
|
|
44
|
+
return typeof input === "string" && PO_BOX_RE.test(input)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Result of a PO-box parse: the matched designator phrase and the box identifier. */
|
|
48
|
+
export interface PoBoxMatch {
|
|
49
|
+
/** The designator phrase as it appeared, e.g. "P.O. Box", "Post Office Box". */
|
|
50
|
+
matched: string
|
|
51
|
+
/** The box identifier, e.g. "123", "12-A". */
|
|
52
|
+
id: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* If `input` is a PO-box phrase ("PO Box 123", "P.O. Box 12-A", "Post Office Box 7"), return the designator phrase and
|
|
57
|
+
* the id. Null otherwise. Useful for the corpus po_box shard (split the designator from the number) and for
|
|
58
|
+
* resolver/parsing reuse.
|
|
59
|
+
*/
|
|
60
|
+
export function matchPOBox(input: unknown): PoBoxMatch | null {
|
|
61
|
+
if (typeof input !== "string") return null
|
|
62
|
+
const m = PO_BOX_RE.exec(input)
|
|
63
|
+
|
|
64
|
+
if (!m) return null
|
|
65
|
+
const matched = (m[1] ?? m[2] ?? m[3] ?? m[4] ?? m[5] ?? m[6] ?? m[7] ?? "").trim()
|
|
66
|
+
const id = m[8]!
|
|
67
|
+
|
|
68
|
+
return { matched, id }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalize any recognized PO-box phrase to the canonical USPS "PO BOX <id>" form. Returns the input unchanged if it
|
|
73
|
+
* isn't a PO box. (Widens the original isp-nexus normalizer, which only collapsed the "P.O. BOX" spelling and left the
|
|
74
|
+
* id/casing alone.)
|
|
75
|
+
*/
|
|
76
|
+
export function normalizePOBox(input: string): string {
|
|
77
|
+
const m = matchPOBox(input)
|
|
78
|
+
|
|
79
|
+
if (!m) return input
|
|
80
|
+
|
|
81
|
+
return `PO BOX ${m.id.toUpperCase()}`
|
|
82
|
+
}
|
package/us/state.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* US two-letter postal abbreviations for the 50 states, DC, and the five primary territories.
|
|
7
|
+
*
|
|
8
|
+
* This is the minimal state primitive `zipcode.ts` needs (a closed abbreviation set + predicate)
|
|
9
|
+
* without pulling in `@mailwoman/tiger`'s FIPS enums and their `shapefile-parser` dependency —
|
|
10
|
+
* the point of the codex being a zero-runtime-dep package the browser-pure parser can consume.
|
|
11
|
+
* The full FIPS-keyed table still lives in `@mailwoman/tiger` (`tiger/state.ts`) and in the
|
|
12
|
+
* corpus TIGER adapter; folding those onto this set is a deliberate follow-up, not a v1 concern.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* USPS two-letter abbreviations: the 50 states, the District of Columbia, and the five primary territories (Puerto
|
|
17
|
+
* Rico, Guam, US Virgin Islands, Northern Mariana Islands, American Samoa).
|
|
18
|
+
*/
|
|
19
|
+
export const US_STATE_ABBREVIATIONS = [
|
|
20
|
+
"AL",
|
|
21
|
+
"AK",
|
|
22
|
+
"AZ",
|
|
23
|
+
"AR",
|
|
24
|
+
"CA",
|
|
25
|
+
"CO",
|
|
26
|
+
"CT",
|
|
27
|
+
"DE",
|
|
28
|
+
"DC",
|
|
29
|
+
"FL",
|
|
30
|
+
"GA",
|
|
31
|
+
"HI",
|
|
32
|
+
"ID",
|
|
33
|
+
"IL",
|
|
34
|
+
"IN",
|
|
35
|
+
"IA",
|
|
36
|
+
"KS",
|
|
37
|
+
"KY",
|
|
38
|
+
"LA",
|
|
39
|
+
"ME",
|
|
40
|
+
"MD",
|
|
41
|
+
"MA",
|
|
42
|
+
"MI",
|
|
43
|
+
"MN",
|
|
44
|
+
"MS",
|
|
45
|
+
"MO",
|
|
46
|
+
"MT",
|
|
47
|
+
"NE",
|
|
48
|
+
"NV",
|
|
49
|
+
"NH",
|
|
50
|
+
"NJ",
|
|
51
|
+
"NM",
|
|
52
|
+
"NY",
|
|
53
|
+
"NC",
|
|
54
|
+
"ND",
|
|
55
|
+
"OH",
|
|
56
|
+
"OK",
|
|
57
|
+
"OR",
|
|
58
|
+
"PA",
|
|
59
|
+
"RI",
|
|
60
|
+
"SC",
|
|
61
|
+
"SD",
|
|
62
|
+
"TN",
|
|
63
|
+
"TX",
|
|
64
|
+
"UT",
|
|
65
|
+
"VT",
|
|
66
|
+
"VA",
|
|
67
|
+
"WA",
|
|
68
|
+
"WV",
|
|
69
|
+
"WI",
|
|
70
|
+
"WY",
|
|
71
|
+
"PR",
|
|
72
|
+
"GU",
|
|
73
|
+
"VI",
|
|
74
|
+
"MP",
|
|
75
|
+
"AS",
|
|
76
|
+
] as const satisfies readonly string[]
|
|
77
|
+
|
|
78
|
+
/** A USPS two-letter state-or-territory abbreviation. */
|
|
79
|
+
export type USStateAbbreviation = (typeof US_STATE_ABBREVIATIONS)[number]
|
|
80
|
+
|
|
81
|
+
const STATE_ABBREVIATION_SET: ReadonlySet<string> = new Set(US_STATE_ABBREVIATIONS)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type-predicate for a USPS state-or-territory abbreviation. Case-insensitive (`"ca"` and `"CA"` both pass), since the
|
|
85
|
+
* abbreviation arrives from raw address text.
|
|
86
|
+
*/
|
|
87
|
+
export function isUSStateAbbreviation(input: unknown): input is USStateAbbreviation {
|
|
88
|
+
return typeof input === "string" && STATE_ABBREVIATION_SET.has(input.toUpperCase())
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Full names for the 50 states, DC, and the five primary territories, keyed by USPS abbreviation. The names half of the
|
|
93
|
+
* closed region vocabulary — where the country/state homographs live ("Georgia", "Washington"), which the
|
|
94
|
+
* gazetteer-anchor lexicon needs to flag candidate-tag collisions. Source: USPS Publication 28, Appendix B.
|
|
95
|
+
*/
|
|
96
|
+
export const US_STATE_BY_ABBREVIATION = {
|
|
97
|
+
AL: "Alabama",
|
|
98
|
+
AK: "Alaska",
|
|
99
|
+
AZ: "Arizona",
|
|
100
|
+
AR: "Arkansas",
|
|
101
|
+
CA: "California",
|
|
102
|
+
CO: "Colorado",
|
|
103
|
+
CT: "Connecticut",
|
|
104
|
+
DE: "Delaware",
|
|
105
|
+
DC: "District of Columbia",
|
|
106
|
+
FL: "Florida",
|
|
107
|
+
GA: "Georgia",
|
|
108
|
+
HI: "Hawaii",
|
|
109
|
+
ID: "Idaho",
|
|
110
|
+
IL: "Illinois",
|
|
111
|
+
IN: "Indiana",
|
|
112
|
+
IA: "Iowa",
|
|
113
|
+
KS: "Kansas",
|
|
114
|
+
KY: "Kentucky",
|
|
115
|
+
LA: "Louisiana",
|
|
116
|
+
ME: "Maine",
|
|
117
|
+
MD: "Maryland",
|
|
118
|
+
MA: "Massachusetts",
|
|
119
|
+
MI: "Michigan",
|
|
120
|
+
MN: "Minnesota",
|
|
121
|
+
MS: "Mississippi",
|
|
122
|
+
MO: "Missouri",
|
|
123
|
+
MT: "Montana",
|
|
124
|
+
NE: "Nebraska",
|
|
125
|
+
NV: "Nevada",
|
|
126
|
+
NH: "New Hampshire",
|
|
127
|
+
NJ: "New Jersey",
|
|
128
|
+
NM: "New Mexico",
|
|
129
|
+
NY: "New York",
|
|
130
|
+
NC: "North Carolina",
|
|
131
|
+
ND: "North Dakota",
|
|
132
|
+
OH: "Ohio",
|
|
133
|
+
OK: "Oklahoma",
|
|
134
|
+
OR: "Oregon",
|
|
135
|
+
PA: "Pennsylvania",
|
|
136
|
+
RI: "Rhode Island",
|
|
137
|
+
SC: "South Carolina",
|
|
138
|
+
SD: "South Dakota",
|
|
139
|
+
TN: "Tennessee",
|
|
140
|
+
TX: "Texas",
|
|
141
|
+
UT: "Utah",
|
|
142
|
+
VT: "Vermont",
|
|
143
|
+
VA: "Virginia",
|
|
144
|
+
WA: "Washington",
|
|
145
|
+
WV: "West Virginia",
|
|
146
|
+
WI: "Wisconsin",
|
|
147
|
+
WY: "Wyoming",
|
|
148
|
+
PR: "Puerto Rico",
|
|
149
|
+
GU: "Guam",
|
|
150
|
+
VI: "US Virgin Islands",
|
|
151
|
+
MP: "Northern Mariana Islands",
|
|
152
|
+
AS: "American Samoa",
|
|
153
|
+
} as const satisfies Record<USStateAbbreviation, string>
|
|
154
|
+
|
|
155
|
+
/** The full state/territory names, derived from {@link US_STATE_BY_ABBREVIATION}. */
|
|
156
|
+
export const US_STATE_NAMES: readonly string[] = Object.values(US_STATE_BY_ABBREVIATION)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS street **directionals** — the 8 cardinal/intercardinal prefixes (N, S, E, W, NE, NW, SE,
|
|
7
|
+
* SW). The leading-directional counterpart to {@link ./street-suffix.ts} (trailing street type):
|
|
8
|
+
* "N Main St" splits into a `street_prefix` directional ("N"), a `street` name ("Main"), and a
|
|
9
|
+
* `street_suffix` type ("St").
|
|
10
|
+
* @see {@link https://pe.usps.com/text/pub28/28apc_002.htm USPS Pub 28 Appendix C1}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { matchCase } from "./street-suffix.ts"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The 8 directional abbreviations accepted by the USPS. The USPS prefers the abbreviation over the fully-spelled-out
|
|
17
|
+
* name.
|
|
18
|
+
*/
|
|
19
|
+
export const DirectionalAbbreviation = {
|
|
20
|
+
NORTH: "N",
|
|
21
|
+
EAST: "E",
|
|
22
|
+
SOUTH: "S",
|
|
23
|
+
WEST: "W",
|
|
24
|
+
NORTHEAST: "NE",
|
|
25
|
+
NORTHWEST: "NW",
|
|
26
|
+
SOUTHEAST: "SE",
|
|
27
|
+
SOUTHWEST: "SW",
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
export type DirectionalAbbreviation = (typeof DirectionalAbbreviation)[keyof typeof DirectionalAbbreviation]
|
|
31
|
+
|
|
32
|
+
/** The 8 directional names accepted by the USPS (intercardinals spaced, per the publication). */
|
|
33
|
+
export const DirectionalNames = [
|
|
34
|
+
"NORTH",
|
|
35
|
+
"EAST",
|
|
36
|
+
"SOUTH",
|
|
37
|
+
"WEST",
|
|
38
|
+
"NORTH EAST",
|
|
39
|
+
"NORTH WEST",
|
|
40
|
+
"SOUTH EAST",
|
|
41
|
+
"SOUTH WEST",
|
|
42
|
+
] as const satisfies readonly string[]
|
|
43
|
+
|
|
44
|
+
export type DirectionalName = (typeof DirectionalNames)[number]
|
|
45
|
+
|
|
46
|
+
export const DirectionalNameVariations = [
|
|
47
|
+
...DirectionalNames,
|
|
48
|
+
// Without spaces (the common US street form: "Northeast Main St")…
|
|
49
|
+
"NORTHEAST",
|
|
50
|
+
"NORTHWEST",
|
|
51
|
+
"SOUTHEAST",
|
|
52
|
+
"SOUTHWEST",
|
|
53
|
+
// Title-case…
|
|
54
|
+
"North",
|
|
55
|
+
"East",
|
|
56
|
+
"South",
|
|
57
|
+
"West",
|
|
58
|
+
"North East",
|
|
59
|
+
"North West",
|
|
60
|
+
"South East",
|
|
61
|
+
"South West",
|
|
62
|
+
"Northeast",
|
|
63
|
+
"Northwest",
|
|
64
|
+
"Southeast",
|
|
65
|
+
"Southwest",
|
|
66
|
+
// Lower-case…
|
|
67
|
+
"north east",
|
|
68
|
+
"north west",
|
|
69
|
+
"south east",
|
|
70
|
+
"south west",
|
|
71
|
+
"northeast",
|
|
72
|
+
"northwest",
|
|
73
|
+
"southeast",
|
|
74
|
+
"southwest",
|
|
75
|
+
] as const satisfies readonly string[]
|
|
76
|
+
|
|
77
|
+
export type DirectionalNameVariation = (typeof DirectionalNameVariations)[number]
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Abbreviation → full name.
|
|
81
|
+
*/
|
|
82
|
+
export const DirectionalAbbreviationRecord = {
|
|
83
|
+
N: "NORTH",
|
|
84
|
+
E: "EAST",
|
|
85
|
+
S: "SOUTH",
|
|
86
|
+
W: "WEST",
|
|
87
|
+
NE: "NORTH EAST",
|
|
88
|
+
NW: "NORTH WEST",
|
|
89
|
+
SE: "SOUTH EAST",
|
|
90
|
+
SW: "SOUTH WEST",
|
|
91
|
+
} as const satisfies Record<DirectionalAbbreviation, DirectionalName>
|
|
92
|
+
|
|
93
|
+
export type DirectionalAbbreviationRecord = typeof DirectionalAbbreviationRecord
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Name (and its variations) → abbreviation.
|
|
97
|
+
*/
|
|
98
|
+
export const DirectionAbbreviationRecord = {
|
|
99
|
+
NORTH: DirectionalAbbreviation.NORTH,
|
|
100
|
+
EAST: DirectionalAbbreviation.EAST,
|
|
101
|
+
SOUTH: DirectionalAbbreviation.SOUTH,
|
|
102
|
+
WEST: DirectionalAbbreviation.WEST,
|
|
103
|
+
NORTHEAST: DirectionalAbbreviation.NORTHEAST,
|
|
104
|
+
NORTHWEST: DirectionalAbbreviation.NORTHWEST,
|
|
105
|
+
SOUTHEAST: DirectionalAbbreviation.SOUTHEAST,
|
|
106
|
+
SOUTHWEST: DirectionalAbbreviation.SOUTHWEST,
|
|
107
|
+
"NORTH EAST": DirectionalAbbreviation.NORTHEAST,
|
|
108
|
+
"NORTH WEST": DirectionalAbbreviation.NORTHWEST,
|
|
109
|
+
"SOUTH EAST": DirectionalAbbreviation.SOUTHEAST,
|
|
110
|
+
"SOUTH WEST": DirectionalAbbreviation.SOUTHWEST,
|
|
111
|
+
} as const satisfies Partial<Record<string, DirectionalAbbreviation>>
|
|
112
|
+
|
|
113
|
+
export type DirectionAbbreviationRecord = typeof DirectionAbbreviationRecord
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Abbreviation (verbatim or normalized) → full name.
|
|
117
|
+
*/
|
|
118
|
+
export const AbbreviationToDirectional: ReadonlyMap<string, DirectionalName> = new Map(
|
|
119
|
+
Object.entries(DirectionalAbbreviationRecord)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Name (verbatim or normalized) → abbreviation.
|
|
124
|
+
*/
|
|
125
|
+
export const DirectionalToAbbreviationMap: ReadonlyMap<string, DirectionalAbbreviation> = new Map(
|
|
126
|
+
Object.entries(DirectionAbbreviationRecord)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Given a possible directional abbreviation, return the corresponding full name (or null).
|
|
131
|
+
*/
|
|
132
|
+
export function pluckDirectionalName(input: unknown): DirectionalName | null {
|
|
133
|
+
if (!input || typeof input !== "string") return null
|
|
134
|
+
|
|
135
|
+
return AbbreviationToDirectional.get(input) || AbbreviationToDirectional.get(input.trim().toUpperCase()) || null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Given a possible directional name, return the corresponding abbreviation (or null).
|
|
140
|
+
*/
|
|
141
|
+
export function lookupDirectionalAbbreviation(input: unknown): DirectionalAbbreviation | null {
|
|
142
|
+
if (!input || typeof input !== "string") return null
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
DirectionalToAbbreviationMap.get(input) ||
|
|
146
|
+
DirectionalToAbbreviationMap.get(input.trim().toUpperCase().replace(/\s+/g, " ")) ||
|
|
147
|
+
null
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Result of a directional lookup: the canonical full name + its preferred abbreviation.
|
|
153
|
+
*/
|
|
154
|
+
export interface DirectionalMatch {
|
|
155
|
+
/** The matched directional name, e.g. "NORTH", "NORTH EAST". */
|
|
156
|
+
directional: DirectionalName
|
|
157
|
+
/** The corresponding USPS abbreviation, e.g. "N", "NE". */
|
|
158
|
+
abbreviation: DirectionalAbbreviation
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Look up a directional by abbreviation OR name (any variation), returning both forms.
|
|
163
|
+
*/
|
|
164
|
+
export function lookupDirectional(input: unknown): DirectionalMatch | null {
|
|
165
|
+
if (!input || typeof input !== "string") return null
|
|
166
|
+
const abbreviation = lookupDirectionalAbbreviation(input)
|
|
167
|
+
|
|
168
|
+
if (abbreviation) return { directional: DirectionalAbbreviationRecord[abbreviation], abbreviation }
|
|
169
|
+
const directional = pluckDirectionalName(input)
|
|
170
|
+
|
|
171
|
+
if (directional) return { directional, abbreviation: DirectionalToAbbreviationMap.get(directional)! }
|
|
172
|
+
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Codex shard-facing helpers (mirror street-suffix's matchTrailingSuffix) ───────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* If the FIRST whitespace-separated word of `street` is a known USPS directional (abbrev or name), return the canonical
|
|
180
|
+
* name, its abbreviation, and the matched surface word. Null otherwise. (The leading-end counterpart of
|
|
181
|
+
* {@link matchTrailingSuffix}; mirrors unit-designator's `matchLeadingDesignator`.) Single-word only — the spaced "NORTH
|
|
182
|
+
* EAST" form is normalized to its one-word variant in real US streets, which this matches via the lookup.
|
|
183
|
+
*/
|
|
184
|
+
export function matchLeadingDirectional(
|
|
185
|
+
street: string
|
|
186
|
+
): { canonical: DirectionalName; abbreviation: DirectionalAbbreviation; matched: string } | null {
|
|
187
|
+
const trimmed = street.trim()
|
|
188
|
+
|
|
189
|
+
if (!trimmed) return null
|
|
190
|
+
const first = trimmed.split(/\s+/)[0]!
|
|
191
|
+
const m = lookupDirectional(first)
|
|
192
|
+
|
|
193
|
+
if (!m) return null
|
|
194
|
+
|
|
195
|
+
return { canonical: m.directional, abbreviation: m.abbreviation, matched: first }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Render a directional in the requested surface form, in `reference`'s case pattern:
|
|
200
|
+
*
|
|
201
|
+
* - `"abbr"` → the USPS abbreviation ("N", "NE").
|
|
202
|
+
* - `"full"` → the one-word spelled-out form ("North", "Northeast") — the common US street form, not the publication's
|
|
203
|
+
* spaced "NORTH EAST".
|
|
204
|
+
*/
|
|
205
|
+
export function renderDirectional(
|
|
206
|
+
match: { canonical: DirectionalName; abbreviation: DirectionalAbbreviation },
|
|
207
|
+
form: "abbr" | "full",
|
|
208
|
+
reference: string
|
|
209
|
+
): string {
|
|
210
|
+
const target = form === "abbr" ? match.abbreviation : match.canonical.replace(/\s+/g, "")
|
|
211
|
+
|
|
212
|
+
return matchCase(target, reference)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Case-insensitive check: is the token any USPS directional or abbreviation (`"N"`, `"north"`, `"NW"`)?
|
|
217
|
+
*/
|
|
218
|
+
export function isStreetDirectionalToken(input: unknown): boolean {
|
|
219
|
+
return lookupDirectional(typeof input === "string" ? input.trim() : input) !== null
|
|
220
|
+
}
|