@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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS Publication 28, Appendix C — Postal Service Standard Suffix Abbreviations.
|
|
7
|
+
*
|
|
8
|
+
* For each canonical suffix the value lists every recognized variant in USPS-published order; the
|
|
9
|
+
* first variant is the preferred USPS abbreviation (e.g. `AVENUE → ["AVE", "AV", "AVEN", "AVENU",
|
|
10
|
+
* "AVN", "AVNUE"]` — `AVE` is what the post office prints).
|
|
11
|
+
*
|
|
12
|
+
* This module is the single home for the USPS suffix table. It carries both the synthesis-layer
|
|
13
|
+
* helpers (`US_STREET_SUFFIX_PREFERRED_ABBR`, `matchCase`, `matchTrailingSuffix` — used by
|
|
14
|
+
* `@mailwoman/corpus`) and the richer branded-type lookup (`StreetSuffix`, `lookupStreetSuffix`,
|
|
15
|
+
* `isStreetSuffix`) The data is verbatim USPS Pub-28; the two APIs share one underlying record.
|
|
16
|
+
* @see {@link https://pe.usps.com/text/pub28/28apc_002.htm USPS Street Suffix Abbreviations}
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canonical USPS street suffix → list of recognized variants. The first variant in each list is the preferred USPS
|
|
21
|
+
* abbreviation. Keys + values are uppercase per the publication.
|
|
22
|
+
*/
|
|
23
|
+
export const US_STREET_SUFFIX_VARIANTS = {
|
|
24
|
+
ALLEY: ["ALY", "ALLEE", "ALLY"],
|
|
25
|
+
ANEX: ["ANX", "ANNEX", "ANNX"],
|
|
26
|
+
ARCADE: ["ARC"],
|
|
27
|
+
AVENUE: ["AVE", "AV", "AVEN", "AVENU", "AVN", "AVNUE"],
|
|
28
|
+
BAYOU: ["BYU", "BAYOO"],
|
|
29
|
+
BEACH: ["BCH"],
|
|
30
|
+
BEND: ["BND"],
|
|
31
|
+
BLUFF: ["BLF", "BLUF"],
|
|
32
|
+
BLUFFS: ["BLFS"],
|
|
33
|
+
BOTTOM: ["BTM", "BOT", "BOTTM"],
|
|
34
|
+
BOULEVARD: ["BLVD", "BOUL", "BOULV"],
|
|
35
|
+
BRANCH: ["BR", "BRNCH"],
|
|
36
|
+
BRIDGE: ["BRG", "BRDGE"],
|
|
37
|
+
BROOK: ["BRK"],
|
|
38
|
+
BROOKS: ["BRKS"],
|
|
39
|
+
BURG: ["BG"],
|
|
40
|
+
BURGS: ["BGS"],
|
|
41
|
+
BYPASS: ["BYP", "BYPA", "BYPAS", "BYPS"],
|
|
42
|
+
CAMP: ["CP", "CMP"],
|
|
43
|
+
CANYON: ["CYN", "CANYN", "CNYN"],
|
|
44
|
+
CAPE: ["CPE"],
|
|
45
|
+
CAUSEWAY: ["CSWY", "CAUSWA"],
|
|
46
|
+
CENTER: ["CTR", "CEN", "CENT", "CENTR", "CENTRE", "CNTER", "CNTR"],
|
|
47
|
+
CENTERS: ["CTRS"],
|
|
48
|
+
CIRCLE: ["CIR", "CIRC", "CIRCL", "CRCL", "CRCLE"],
|
|
49
|
+
CIRCLES: ["CIRS"],
|
|
50
|
+
CLIFF: ["CLF"],
|
|
51
|
+
CLIFFS: ["CLFS"],
|
|
52
|
+
CLUB: ["CLB"],
|
|
53
|
+
COMMON: ["CMN"],
|
|
54
|
+
COMMONS: ["CMNS"],
|
|
55
|
+
CORNER: ["COR"],
|
|
56
|
+
CORNERS: ["CORS"],
|
|
57
|
+
COURSE: ["CRSE"],
|
|
58
|
+
COURT: ["CT"],
|
|
59
|
+
COURTS: ["CTS"],
|
|
60
|
+
COVE: ["CV"],
|
|
61
|
+
COVES: ["CVS"],
|
|
62
|
+
CREEK: ["CRK"],
|
|
63
|
+
CRESCENT: ["CRES", "CRSENT", "CRSNT"],
|
|
64
|
+
CREST: ["CRST"],
|
|
65
|
+
CROSSING: ["XING", "CRSSNG"],
|
|
66
|
+
CROSSROAD: ["XRD"],
|
|
67
|
+
CROSSROADS: ["XRDS"],
|
|
68
|
+
CURVE: ["CURV"],
|
|
69
|
+
DALE: ["DL"],
|
|
70
|
+
DAM: ["DM"],
|
|
71
|
+
DIVIDE: ["DV", "DIV", "DVD"],
|
|
72
|
+
DRIVE: ["DR", "DRIV", "DRV"],
|
|
73
|
+
DRIVES: ["DRS"],
|
|
74
|
+
ESTATE: ["EST"],
|
|
75
|
+
ESTATES: ["ESTS"],
|
|
76
|
+
EXPRESSWAY: ["EXPY", "EXP", "EXPR", "EXPRESS", "EXPW"],
|
|
77
|
+
EXTENSION: ["EXT", "EXTN", "EXTNSN"],
|
|
78
|
+
EXTENSIONS: ["EXTS"],
|
|
79
|
+
FALL: ["FALL"],
|
|
80
|
+
FALLS: ["FLS"],
|
|
81
|
+
FERRY: ["FRY", "FRRY"],
|
|
82
|
+
FIELD: ["FLD"],
|
|
83
|
+
FIELDS: ["FLDS"],
|
|
84
|
+
FLAT: ["FLT"],
|
|
85
|
+
FLATS: ["FLTS"],
|
|
86
|
+
FORD: ["FRD"],
|
|
87
|
+
FORDS: ["FRDS"],
|
|
88
|
+
FOREST: ["FRST", "FORESTS"],
|
|
89
|
+
FORGE: ["FRG", "FORG"],
|
|
90
|
+
FORGES: ["FRGS"],
|
|
91
|
+
FORK: ["FRK"],
|
|
92
|
+
FORKS: ["FRKS"],
|
|
93
|
+
FORT: ["FT", "FRT"],
|
|
94
|
+
FREEWAY: ["FWY", "FREEWY", "FRWAY", "FRWY"],
|
|
95
|
+
GARDEN: ["GDN", "GARDN", "GRDEN", "GRDN"],
|
|
96
|
+
GARDENS: ["GDNS", "GRDNS"],
|
|
97
|
+
GATEWAY: ["GTWY", "GATEWY", "GATWAY", "GTWAY"],
|
|
98
|
+
GLEN: ["GLN"],
|
|
99
|
+
GLENS: ["GLNS"],
|
|
100
|
+
GREEN: ["GRN"],
|
|
101
|
+
GREENS: ["GRNS"],
|
|
102
|
+
GROVE: ["GRV", "GROV"],
|
|
103
|
+
GROVES: ["GRVS"],
|
|
104
|
+
HARBOR: ["HBR", "HARB", "HARBR", "HRBOR"],
|
|
105
|
+
HARBORS: ["HBRS"],
|
|
106
|
+
HAVEN: ["HVN"],
|
|
107
|
+
HEIGHTS: ["HTS", "HT"],
|
|
108
|
+
HIGHWAY: ["HWY", "HIGHWY", "HIWAY", "HIWY", "HWAY"],
|
|
109
|
+
HILL: ["HL"],
|
|
110
|
+
HILLS: ["HLS"],
|
|
111
|
+
HOLLOW: ["HOLW", "HLLW", "HOLLOWS", "HOLWS"],
|
|
112
|
+
INLET: ["INLT"],
|
|
113
|
+
ISLAND: ["IS", "ISLND"],
|
|
114
|
+
ISLANDS: ["ISS", "ISLNDS"],
|
|
115
|
+
ISLE: ["ISLE", "ISLES"],
|
|
116
|
+
JUNCTION: ["JCT", "JCTION", "JCTN", "JUNCTN", "JUNCTON"],
|
|
117
|
+
JUNCTIONS: ["JCTS", "JCTNS"],
|
|
118
|
+
KEY: ["KY"],
|
|
119
|
+
KEYS: ["KYS"],
|
|
120
|
+
KNOLL: ["KNL", "KNOL"],
|
|
121
|
+
KNOLLS: ["KNLS"],
|
|
122
|
+
LAKE: ["LK"],
|
|
123
|
+
LAKES: ["LKS"],
|
|
124
|
+
LAND: ["LAND"],
|
|
125
|
+
LANDING: ["LNDG", "LNDNG"],
|
|
126
|
+
LANE: ["LN"],
|
|
127
|
+
LIGHT: ["LGT"],
|
|
128
|
+
LIGHTS: ["LGTS"],
|
|
129
|
+
LOAF: ["LF"],
|
|
130
|
+
LOCK: ["LCK"],
|
|
131
|
+
LOCKS: ["LCKS"],
|
|
132
|
+
LODGE: ["LDG", "LDGE", "LODG"],
|
|
133
|
+
LOOP: ["LOOP", "LOOPS"],
|
|
134
|
+
MALL: ["MALL"],
|
|
135
|
+
MANOR: ["MNR"],
|
|
136
|
+
MANORS: ["MNRS"],
|
|
137
|
+
MEADOW: ["MDW"],
|
|
138
|
+
MEADOWS: ["MDWS", "MDW", "MEDOWS"],
|
|
139
|
+
MEWS: ["MEWS"],
|
|
140
|
+
MILL: ["ML"],
|
|
141
|
+
MILLS: ["MLS"],
|
|
142
|
+
MISSION: ["MSN", "MISSN", "MSSN"],
|
|
143
|
+
MOTORWAY: ["MTWY"],
|
|
144
|
+
MOUNT: ["MT", "MNT"],
|
|
145
|
+
MOUNTAIN: ["MTN", "MNTAIN", "MNTN", "MOUNTIN", "MTIN"],
|
|
146
|
+
MOUNTAINS: ["MTNS", "MNTNS"],
|
|
147
|
+
NECK: ["NCK"],
|
|
148
|
+
ORCHARD: ["ORCH", "ORCHRD"],
|
|
149
|
+
OVAL: ["OVAL", "OVL"],
|
|
150
|
+
OVERPASS: ["OPAS"],
|
|
151
|
+
PARK: ["PARK", "PRK", "PARKS"],
|
|
152
|
+
PARKWAY: ["PKWY", "PARKWY", "PKWAY", "PKY"],
|
|
153
|
+
PARKWAYS: ["PKWY", "PKWYS"],
|
|
154
|
+
PASS: ["PASS"],
|
|
155
|
+
PASSAGE: ["PSGE"],
|
|
156
|
+
PATH: ["PATH", "PATHS"],
|
|
157
|
+
PIKE: ["PIKE", "PIKES"],
|
|
158
|
+
PINE: ["PNE"],
|
|
159
|
+
PINES: ["PNES"],
|
|
160
|
+
PLACE: ["PL"],
|
|
161
|
+
PLAIN: ["PLN"],
|
|
162
|
+
PLAINS: ["PLNS"],
|
|
163
|
+
PLAZA: ["PLZ", "PLZA"],
|
|
164
|
+
POINT: ["PT"],
|
|
165
|
+
POINTS: ["PTS"],
|
|
166
|
+
PORT: ["PRT"],
|
|
167
|
+
PORTS: ["PRTS"],
|
|
168
|
+
PRAIRIE: ["PR", "PRR"],
|
|
169
|
+
RADIAL: ["RADL", "RAD", "RADIEL"],
|
|
170
|
+
RAMP: ["RAMP"],
|
|
171
|
+
RANCH: ["RNCH", "RANCHES", "RNCHS"],
|
|
172
|
+
RAPID: ["RPD"],
|
|
173
|
+
RAPIDS: ["RPDS"],
|
|
174
|
+
REST: ["RST"],
|
|
175
|
+
RIDGE: ["RDG", "RDGE"],
|
|
176
|
+
RIDGES: ["RDGS"],
|
|
177
|
+
RIVER: ["RIV", "RVR", "RIVR"],
|
|
178
|
+
ROAD: ["RD"],
|
|
179
|
+
ROADS: ["RDS"],
|
|
180
|
+
ROUTE: ["RTE"],
|
|
181
|
+
ROW: ["ROW"],
|
|
182
|
+
RUE: ["RUE"],
|
|
183
|
+
RUN: ["RUN"],
|
|
184
|
+
SHOAL: ["SHL"],
|
|
185
|
+
SHOALS: ["SHLS"],
|
|
186
|
+
SHORE: ["SHR", "SHOAR"],
|
|
187
|
+
SHORES: ["SHRS", "SHOARS"],
|
|
188
|
+
SKYWAY: ["SKWY"],
|
|
189
|
+
SPRING: ["SPG", "SPNG", "SPRNG"],
|
|
190
|
+
SPRINGS: ["SPGS", "SPNGS", "SPRNGS"],
|
|
191
|
+
SPUR: ["SPUR"],
|
|
192
|
+
SPURS: ["SPUR"],
|
|
193
|
+
SQUARE: ["SQ", "SQR", "SQRE", "SQU"],
|
|
194
|
+
SQUARES: ["SQS", "SQRS"],
|
|
195
|
+
STATION: ["STA", "STATN", "STN"],
|
|
196
|
+
STRAVENUE: ["STRA", "STRAV", "STRAVEN", "STRAVN", "STRVN", "STRVNUE"],
|
|
197
|
+
STREAM: ["STRM", "STREME"],
|
|
198
|
+
STREET: ["ST", "STRT", "STR"],
|
|
199
|
+
STREETS: ["STS"],
|
|
200
|
+
SUMMIT: ["SMT", "SUMIT", "SUMITT"],
|
|
201
|
+
TERRACE: ["TER", "TERR"],
|
|
202
|
+
THROUGHWAY: ["TRWY"],
|
|
203
|
+
TRACE: ["TRCE", "TRACES"],
|
|
204
|
+
TRACK: ["TRAK", "TRACKS", "TRK", "TRKS"],
|
|
205
|
+
TRAFFICWAY: ["TRFY"],
|
|
206
|
+
TRAIL: ["TRL", "TRAILS", "TRLS"],
|
|
207
|
+
TRAILER: ["TRLR", "TRLRS"],
|
|
208
|
+
TUNNEL: ["TUNL", "TUNEL", "TUNLS", "TUNNELS", "TUNNL"],
|
|
209
|
+
TURNPIKE: ["TPKE", "TRNPK", "TURNPK"],
|
|
210
|
+
UNDERPASS: ["UPAS"],
|
|
211
|
+
UNION: ["UN"],
|
|
212
|
+
UNIONS: ["UNS"],
|
|
213
|
+
VALLEY: ["VLY", "VALLY", "VLLY"],
|
|
214
|
+
VALLEYS: ["VLYS"],
|
|
215
|
+
VIADUCT: ["VIA", "VDCT", "VIADCT"],
|
|
216
|
+
VIEW: ["VW"],
|
|
217
|
+
VIEWS: ["VWS"],
|
|
218
|
+
VILLAGE: ["VLG", "VILL", "VILLAG", "VILLG", "VILLIAGE"],
|
|
219
|
+
VILLAGES: ["VLGS"],
|
|
220
|
+
VILLE: ["VL"],
|
|
221
|
+
VISTA: ["VIS", "VIST", "VST", "VSTA"],
|
|
222
|
+
WALK: ["WALK"],
|
|
223
|
+
WALKS: ["WALK"],
|
|
224
|
+
WALL: ["WALL"],
|
|
225
|
+
WAY: ["WAY", "WY"],
|
|
226
|
+
WAYS: ["WAYS"],
|
|
227
|
+
WELL: ["WL"],
|
|
228
|
+
WELLS: ["WLS"],
|
|
229
|
+
} as const satisfies Record<string, readonly string[]>
|
|
230
|
+
|
|
231
|
+
/** Canonical USPS suffix (full word, uppercase per the publication). */
|
|
232
|
+
export type USStreetSuffix = keyof typeof US_STREET_SUFFIX_VARIANTS
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inverse lookup: every variant abbreviation OR full canonical word → its canonical key. Built once at module load,
|
|
236
|
+
* lowercase-keyed for case-insensitive matching (`street` → `"STREET"`, `st` → `"STREET"`, `strt` → `"STREET"`, …).
|
|
237
|
+
*/
|
|
238
|
+
export const US_STREET_SUFFIX_LOOKUP: ReadonlyMap<string, USStreetSuffix> = (() => {
|
|
239
|
+
const out = new Map<string, USStreetSuffix>()
|
|
240
|
+
|
|
241
|
+
for (const canonical of Object.keys(US_STREET_SUFFIX_VARIANTS) as USStreetSuffix[]) {
|
|
242
|
+
out.set(canonical.toLowerCase(), canonical)
|
|
243
|
+
|
|
244
|
+
for (const variant of US_STREET_SUFFIX_VARIANTS[canonical]) {
|
|
245
|
+
// Don't overwrite — first canonical that claims a variant wins (matches USPS Pub-28's
|
|
246
|
+
// ordering). E.g. "WALK" and "WALKS" both list "WALK" as a variant; "WALK" wins because it
|
|
247
|
+
// sorts first in `Object.keys`.
|
|
248
|
+
if (!out.has(variant.toLowerCase())) {
|
|
249
|
+
out.set(variant.toLowerCase(), canonical)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return out
|
|
255
|
+
})()
|
|
256
|
+
|
|
257
|
+
/** Preferred USPS abbreviation per canonical (`AVENUE → "AVE"`, `STREET → "ST"`). */
|
|
258
|
+
export const US_STREET_SUFFIX_PREFERRED_ABBR: Readonly<Record<USStreetSuffix, string>> = Object.fromEntries(
|
|
259
|
+
(Object.keys(US_STREET_SUFFIX_VARIANTS) as USStreetSuffix[]).map((k) => [k, US_STREET_SUFFIX_VARIANTS[k][0]])
|
|
260
|
+
) as Readonly<Record<USStreetSuffix, string>>
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Apply `target`'s letters in the same case-pattern as `reference`. Three patterns covered:
|
|
264
|
+
*
|
|
265
|
+
* - All-uppercase reference (`"AVE"`) → uppercase target (`"AVENUE"`).
|
|
266
|
+
* - All-lowercase reference (`"ave"`) → lowercase target (`"avenue"`).
|
|
267
|
+
* - Anything else (`"Ave"`, `"aVe"`) → title-case target (`"Avenue"`).
|
|
268
|
+
*/
|
|
269
|
+
export function matchCase(target: string, reference: string): string {
|
|
270
|
+
if (!reference) return target
|
|
271
|
+
|
|
272
|
+
if (reference === reference.toUpperCase()) return target.toUpperCase()
|
|
273
|
+
|
|
274
|
+
if (reference === reference.toLowerCase()) return target.toLowerCase()
|
|
275
|
+
|
|
276
|
+
return target.charAt(0).toUpperCase() + target.slice(1).toLowerCase()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* If the last whitespace-separated word of `street` is a known USPS suffix variant, return the canonical key and the
|
|
281
|
+
* matched word. Returns null if the trailing word isn't a known suffix.
|
|
282
|
+
*/
|
|
283
|
+
export function matchTrailingSuffix(street: string): { canonical: USStreetSuffix; matched: string } | null {
|
|
284
|
+
const trimmed = street.trim()
|
|
285
|
+
|
|
286
|
+
if (!trimmed) return null
|
|
287
|
+
const parts = trimmed.split(/\s+/)
|
|
288
|
+
const last = parts[parts.length - 1]!
|
|
289
|
+
const canonical = US_STREET_SUFFIX_LOOKUP.get(last.toLowerCase())
|
|
290
|
+
|
|
291
|
+
if (!canonical) return null
|
|
292
|
+
|
|
293
|
+
return { canonical, matched: last }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The USPS suffix record, under its original isp-nexus name. Aliases {@link US_STREET_SUFFIX_VARIANTS}.
|
|
298
|
+
*/
|
|
299
|
+
export const StreetSuffixAbbreviationRecord = US_STREET_SUFFIX_VARIANTS
|
|
300
|
+
export type StreetSuffixAbbreviationRecord = typeof US_STREET_SUFFIX_VARIANTS
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* A canonical USPS street suffix, i.e. "STREET", "AVENUE", "BOULEVARD". Aliases {@link USStreetSuffix}.
|
|
304
|
+
*/
|
|
305
|
+
export type StreetSuffix = USStreetSuffix
|
|
306
|
+
|
|
307
|
+
/** A standardized USPS street suffix abbreviation (the preferred form), i.e. "ST", "AVE", "BLVD". */
|
|
308
|
+
export type USPSStandardSuffixAbbreviation = StreetSuffixAbbreviationRecord[StreetSuffix][0]
|
|
309
|
+
|
|
310
|
+
/** Any USPS-recognized suffix variant or abbreviation. */
|
|
311
|
+
export type StreetSuffixAbbreviation = StreetSuffixAbbreviationRecord[StreetSuffix][number]
|
|
312
|
+
|
|
313
|
+
/** Result of a successful USPS street suffix lookup. */
|
|
314
|
+
export interface StreetSuffixMatch<S extends StreetSuffix = StreetSuffix> {
|
|
315
|
+
/** The matched canonical USPS street suffix, i.e. "STREET", "AVENUE". */
|
|
316
|
+
suffix: S
|
|
317
|
+
/** The preferred USPS street suffix abbreviation, i.e. "ST", "AVE". */
|
|
318
|
+
abbreviation: StreetSuffixAbbreviationRecord[S][0]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Look up a USPS street suffix (by canonical word, abbreviation, or any variant) and its preferred abbreviation.
|
|
323
|
+
*/
|
|
324
|
+
export function lookupStreetSuffix<S extends StreetSuffix>(suffix: S): StreetSuffixMatch<S>
|
|
325
|
+
export function lookupStreetSuffix(input: string | null | undefined): StreetSuffixMatch | null
|
|
326
|
+
export function lookupStreetSuffix(input: string | null | undefined): StreetSuffixMatch | null {
|
|
327
|
+
if (!input || typeof input !== "string") return null
|
|
328
|
+
const suffix = US_STREET_SUFFIX_LOOKUP.get(input.trim().toLowerCase())
|
|
329
|
+
|
|
330
|
+
if (!suffix) return null
|
|
331
|
+
|
|
332
|
+
return { suffix, abbreviation: US_STREET_SUFFIX_VARIANTS[suffix][0] }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Type-predicate: is the input a canonical USPS street suffix (uppercase full word, e.g. "STREET")? */
|
|
336
|
+
export function isStreetSuffix(input: unknown): input is StreetSuffix {
|
|
337
|
+
return typeof input === "string" && Object.hasOwn(US_STREET_SUFFIX_VARIANTS, input)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* True when a token is any USPS street suffix or abbreviation (case-insensitive) — `"St"`, `"BLVD"`, `"trail"`.
|
|
342
|
+
*/
|
|
343
|
+
export function isStreetSuffixToken(input: unknown): boolean {
|
|
344
|
+
return typeof input === "string" && US_STREET_SUFFIX_LOOKUP.has(input.trim().toLowerCase())
|
|
345
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS Publication 28, Appendix C2 — Secondary Unit Designators.
|
|
7
|
+
*
|
|
8
|
+
* The sibling of {@link ./street-suffix.ts}: where that table standardizes the trailing street
|
|
9
|
+
* _type_ (AVENUE → AVE), this one standardizes the _secondary unit_ designator that introduces an
|
|
10
|
+
* apartment / suite / floor / room (APARTMENT → APT, SUITE → STE). For each canonical designator
|
|
11
|
+
* the value lists recognized variants in USPS order; the first is the approved USPS abbreviation
|
|
12
|
+
* (what the post office prints).
|
|
13
|
+
*
|
|
14
|
+
* Used by `@mailwoman/corpus`'s synthesis layer (the `unit-{expand,abbreviate}` augmentations) to
|
|
15
|
+
* vary the designator in a `unit` component while preserving the identifier — the data-generation
|
|
16
|
+
* counterpart to the runtime `UnitDesignatorClassifier` (which matches the broader libpostal
|
|
17
|
+
* `unit_types` lexicon). Designators are LEADING ("Apt 4B"), unlike street suffixes which trail.
|
|
18
|
+
*
|
|
19
|
+
* `US_UNIT_DESIGNATOR_REQUIRES_RANGE` (added for #1100, the secondary-address epic; retrieved from
|
|
20
|
+
* Appendix C2 2026-07-13) is Pub-28's own "Requires a Secondary Number" column: APT, BLDG, DEPT,
|
|
21
|
+
* FL, HNGR, KEY, LOT, PIER, RM, SLIP, SPC, STOP, STE, TRLR, and UNIT must be followed by an
|
|
22
|
+
* identifier ("Apt 4B", never bare "Apt"); BSMT, FRNT, LBBY, LOWR, OFC, PH, REAR, SIDE, and UPPR
|
|
23
|
+
* may stand alone. This formalizes, as provenance-tracked reference data, the split that
|
|
24
|
+
* `corpus/src/shard-recipes/unit.ts` previously hand-rolled (and only partially covered) as
|
|
25
|
+
* in-file `ID_DESIGNATORS`/`STANDALONE_DESIGNATORS` arrays for synthesis weighting. A SEPARATE,
|
|
26
|
+
* not-yet-built deliverable of #1100 is the per-locale *level-semantics* table (étage/RDC, EG/OG/UG,
|
|
27
|
+
* planta/piso/bajo, piano/terra, 階/F/B1, …) — this module stays US/Pub-28 only.
|
|
28
|
+
*
|
|
29
|
+
* Data is verbatim USPS Pub-28 C2.
|
|
30
|
+
* @see {@link https://pe.usps.com/text/pub28/28apc_003.htm USPS Secondary Unit Designators}
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical USPS secondary unit designator → recognized variants. The first variant is the approved USPS abbreviation.
|
|
35
|
+
* Keys + values uppercase per the publication. The designators marked by USPS as "requires a secondary number" (APT,
|
|
36
|
+
* BLDG, FL, …) and the standalone ones (BSMT, LBBY, PH, …) are both included — synthesis treats them uniformly.
|
|
37
|
+
*/
|
|
38
|
+
export const US_UNIT_DESIGNATOR_VARIANTS = {
|
|
39
|
+
APARTMENT: ["APT", "APRT", "APMT"],
|
|
40
|
+
BASEMENT: ["BSMT"],
|
|
41
|
+
BUILDING: ["BLDG", "BLD"],
|
|
42
|
+
DEPARTMENT: ["DEPT"],
|
|
43
|
+
FLOOR: ["FL", "FLR"],
|
|
44
|
+
FRONT: ["FRNT"],
|
|
45
|
+
HANGAR: ["HNGR"],
|
|
46
|
+
KEY: ["KEY"],
|
|
47
|
+
LOBBY: ["LBBY"],
|
|
48
|
+
LOT: ["LOT"],
|
|
49
|
+
LOWER: ["LOWR"],
|
|
50
|
+
OFFICE: ["OFC"],
|
|
51
|
+
PENTHOUSE: ["PH"],
|
|
52
|
+
PIER: ["PIER"],
|
|
53
|
+
REAR: ["REAR"],
|
|
54
|
+
ROOM: ["RM"],
|
|
55
|
+
SIDE: ["SIDE"],
|
|
56
|
+
SLIP: ["SLIP"],
|
|
57
|
+
SPACE: ["SPC"],
|
|
58
|
+
STOP: ["STOP"],
|
|
59
|
+
SUITE: ["STE", "SUIT"],
|
|
60
|
+
TRAILER: ["TRLR"],
|
|
61
|
+
UNIT: ["UNIT"],
|
|
62
|
+
UPPER: ["UPPR"],
|
|
63
|
+
} as const satisfies Record<string, readonly string[]>
|
|
64
|
+
|
|
65
|
+
/** Canonical USPS secondary unit designator (full word, uppercase per the publication). */
|
|
66
|
+
export type USUnitDesignator = keyof typeof US_UNIT_DESIGNATOR_VARIANTS
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Inverse lookup: every variant abbreviation OR full canonical word → its canonical key, built once at module load,
|
|
70
|
+
* lowercase-keyed for case-insensitive matching (`apt` → `"APARTMENT"`, `ste` → `"SUITE"`, `suite` → `"SUITE"`).
|
|
71
|
+
*/
|
|
72
|
+
export const US_UNIT_DESIGNATOR_LOOKUP: ReadonlyMap<string, USUnitDesignator> = (() => {
|
|
73
|
+
const out = new Map<string, USUnitDesignator>()
|
|
74
|
+
|
|
75
|
+
for (const canonical of Object.keys(US_UNIT_DESIGNATOR_VARIANTS) as USUnitDesignator[]) {
|
|
76
|
+
out.set(canonical.toLowerCase(), canonical)
|
|
77
|
+
|
|
78
|
+
for (const variant of US_UNIT_DESIGNATOR_VARIANTS[canonical]) {
|
|
79
|
+
// First canonical that claims a variant wins (matches the publication's ordering).
|
|
80
|
+
if (!out.has(variant.toLowerCase())) {
|
|
81
|
+
out.set(variant.toLowerCase(), canonical)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return out
|
|
87
|
+
})()
|
|
88
|
+
|
|
89
|
+
/** Approved USPS abbreviation per canonical (`APARTMENT → "APT"`, `SUITE → "STE"`). */
|
|
90
|
+
export const US_UNIT_DESIGNATOR_PREFERRED_ABBR: Readonly<Record<USUnitDesignator, string>> = Object.fromEntries(
|
|
91
|
+
(Object.keys(US_UNIT_DESIGNATOR_VARIANTS) as USUnitDesignator[]).map((k) => [k, US_UNIT_DESIGNATOR_VARIANTS[k][0]])
|
|
92
|
+
) as Readonly<Record<USUnitDesignator, string>>
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Canonical designators Appendix C2 marks as "Requires a Secondary Number" — the designator must be followed by an
|
|
96
|
+
* identifier ("Apt 4B", "Rm 12"), never appearing bare. The remaining designators (BASEMENT, FRONT, LOBBY, LOWER,
|
|
97
|
+
* OFFICE, PENTHOUSE, REAR, SIDE, UPPER) may stand alone with no trailing identifier. Verbatim from USPS Pub-28 C2; see
|
|
98
|
+
* the module header for provenance (#1100).
|
|
99
|
+
*/
|
|
100
|
+
export const US_UNIT_DESIGNATOR_REQUIRES_RANGE: Readonly<Record<USUnitDesignator, boolean>> = {
|
|
101
|
+
APARTMENT: true,
|
|
102
|
+
BASEMENT: false,
|
|
103
|
+
BUILDING: true,
|
|
104
|
+
DEPARTMENT: true,
|
|
105
|
+
FLOOR: true,
|
|
106
|
+
FRONT: false,
|
|
107
|
+
HANGAR: true,
|
|
108
|
+
KEY: true,
|
|
109
|
+
LOBBY: false,
|
|
110
|
+
LOT: true,
|
|
111
|
+
LOWER: false,
|
|
112
|
+
OFFICE: false,
|
|
113
|
+
PENTHOUSE: false,
|
|
114
|
+
PIER: true,
|
|
115
|
+
REAR: false,
|
|
116
|
+
ROOM: true,
|
|
117
|
+
SIDE: false,
|
|
118
|
+
SLIP: true,
|
|
119
|
+
SPACE: true,
|
|
120
|
+
STOP: true,
|
|
121
|
+
SUITE: true,
|
|
122
|
+
TRAILER: true,
|
|
123
|
+
UNIT: true,
|
|
124
|
+
UPPER: false,
|
|
125
|
+
} as const satisfies Record<USUnitDesignator, boolean>
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* If the FIRST whitespace-separated word of `unit` is a known USPS designator variant, return the canonical key and the
|
|
129
|
+
* matched word. Returns null if the leading word isn't a known designator (e.g. a bare `"4B"` or `"#210"`).
|
|
130
|
+
* Leading-word-only — designators introduce the unit, unlike street suffixes which trail.
|
|
131
|
+
*/
|
|
132
|
+
export function matchLeadingDesignator(unit: string): { canonical: USUnitDesignator; matched: string } | null {
|
|
133
|
+
const trimmed = unit.trim()
|
|
134
|
+
|
|
135
|
+
if (!trimmed) return null
|
|
136
|
+
const first = trimmed.split(/\s+/)[0]!
|
|
137
|
+
const canonical = US_UNIT_DESIGNATOR_LOOKUP.get(first.toLowerCase())
|
|
138
|
+
|
|
139
|
+
if (!canonical) return null
|
|
140
|
+
|
|
141
|
+
return { canonical, matched: first }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Result of {@link matchLeadingDesignatorWithRange}: the leading designator plus its optional secondary range. */
|
|
145
|
+
export interface UnitDesignatorRangeMatch {
|
|
146
|
+
/** The matched canonical designator, i.e. "APARTMENT", "SUITE". */
|
|
147
|
+
canonical: USUnitDesignator
|
|
148
|
+
/** The designator's own matched surface form, i.e. "Apt". */
|
|
149
|
+
matched: string
|
|
150
|
+
/**
|
|
151
|
+
* The secondary range/identifier token immediately following the designator, i.e. "4B" in "Apt 4B". Undefined when
|
|
152
|
+
* the designator appears standalone (e.g. bare "Basement"). This module does not validate the range's own shape —
|
|
153
|
+
* numeric, letter, or alphanumeric ranges are all USPS-valid.
|
|
154
|
+
*/
|
|
155
|
+
range: string | undefined
|
|
156
|
+
/**
|
|
157
|
+
* Whether USPS Pub-28 Appendix C2 marks this designator as requiring a secondary range (see
|
|
158
|
+
* {@link US_UNIT_DESIGNATOR_REQUIRES_RANGE}). Informational only — not enforced by this matcher.
|
|
159
|
+
*/
|
|
160
|
+
requiresRange: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Like {@link matchLeadingDesignator}, but also captures the secondary range/identifier token immediately following the
|
|
165
|
+
* designator, if present ("Apt 4B" → designator "APARTMENT", range "4B"; "Basement" → range `undefined`). Mirrors
|
|
166
|
+
* `street-suffix`/`street-directional`'s designator+adjacent-token matchers.
|
|
167
|
+
*/
|
|
168
|
+
export function matchLeadingDesignatorWithRange(unit: string): UnitDesignatorRangeMatch | null {
|
|
169
|
+
const trimmed = unit.trim()
|
|
170
|
+
|
|
171
|
+
if (!trimmed) return null
|
|
172
|
+
const parts = trimmed.split(/\s+/)
|
|
173
|
+
const first = parts[0]!
|
|
174
|
+
const canonical = US_UNIT_DESIGNATOR_LOOKUP.get(first.toLowerCase())
|
|
175
|
+
|
|
176
|
+
if (!canonical) return null
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
canonical,
|
|
180
|
+
matched: first,
|
|
181
|
+
range: parts[1],
|
|
182
|
+
requiresRange: US_UNIT_DESIGNATOR_REQUIRES_RANGE[canonical],
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Result of a successful USPS secondary-unit designator lookup. */
|
|
187
|
+
export interface UnitDesignatorMatch<D extends USUnitDesignator = USUnitDesignator> {
|
|
188
|
+
/** The matched canonical designator, i.e. "APARTMENT", "SUITE". */
|
|
189
|
+
designator: D
|
|
190
|
+
/** The approved USPS abbreviation, i.e. "APT", "STE". */
|
|
191
|
+
abbreviation: (typeof US_UNIT_DESIGNATOR_VARIANTS)[D][0]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Look up a USPS secondary unit designator (by canonical word, abbreviation, or any variant) and its approved
|
|
196
|
+
* abbreviation.
|
|
197
|
+
*/
|
|
198
|
+
export function lookupUnitDesignator<D extends USUnitDesignator>(designator: D): UnitDesignatorMatch<D>
|
|
199
|
+
export function lookupUnitDesignator(input: string | null | undefined): UnitDesignatorMatch | null
|
|
200
|
+
export function lookupUnitDesignator(input: string | null | undefined): UnitDesignatorMatch | null {
|
|
201
|
+
if (!input || typeof input !== "string") return null
|
|
202
|
+
const designator = US_UNIT_DESIGNATOR_LOOKUP.get(input.trim().toLowerCase())
|
|
203
|
+
|
|
204
|
+
if (!designator) return null
|
|
205
|
+
|
|
206
|
+
return { designator, abbreviation: US_UNIT_DESIGNATOR_VARIANTS[designator][0] }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* True when a token is any USPS secondary unit designator or abbreviation (case-insensitive) — `"Apt"`, `"STE"`,
|
|
211
|
+
* `"floor"`.
|
|
212
|
+
*/
|
|
213
|
+
export function isUnitDesignatorToken(input: unknown): boolean {
|
|
214
|
+
return typeof input === "string" && US_UNIT_DESIGNATOR_LOOKUP.has(input.trim().toLowerCase())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Alias of {@link isUnitDesignatorToken} under Pub-28's own term ("secondary unit designator"). Added for #1100 so
|
|
219
|
+
* secondary-address call sites can spell the predicate after the publication's vocabulary.
|
|
220
|
+
*/
|
|
221
|
+
export function isSecondaryUnitDesignatorToken(input: unknown): boolean {
|
|
222
|
+
return isUnitDesignatorToken(input)
|
|
223
|
+
}
|