@mailwoman/codex 7.1.0 → 7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +13 -5
- 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,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Per-locale LEVEL semantics — the fix for "Flr 1 means something different depending on where you
|
|
7
|
+
* are" (#1100, the secondary-address epic's second data deliverable). {@link "./us/unit-designator.ts"},
|
|
8
|
+
* {@link "./us/floor-designator.ts"}, and {@link "./au/level-designator.ts"} each standardize ONE
|
|
9
|
+
* address system's floor/level VOCABULARY — which surface tokens exist ("FL", "L 3", "Ground
|
|
10
|
+
* Floor"). This module standardizes the cross-locale ORDINAL SEMANTICS those tokens carry: the fact
|
|
11
|
+
* that "1st floor" names the SAME physical storey as "ground floor" in the United States
|
|
12
|
+
* (`firstNumberedIsGround: true`) but ONE STOREY ABOVE it in France, Germany, and most of
|
|
13
|
+
* continental Europe (`firstNumberedIsGround: false`), where ground already has its own name (RDC,
|
|
14
|
+
* EG, PLANTA BAJA, …) and claims ordinal 0 on its own.
|
|
15
|
+
*
|
|
16
|
+
* This is inherently a MULTI-LOCALE module — unlike `us/`, `au/`, `fr/`, … it doesn't belong to one
|
|
17
|
+
* address system, so it lives at the codex root and is exported only from the root barrel
|
|
18
|
+
* (mirroring `address-system-conventions.ts` and `postcode-systems.ts`, the other cross-system root
|
|
19
|
+
* modules). It does not get its own `@mailwoman/codex/<x>` subpath.
|
|
20
|
+
*
|
|
21
|
+
* Two tables do the work:
|
|
22
|
+
*
|
|
23
|
+
* 1. {@link LEVEL_DESIGNATORS_BY_FAMILY} — the designator LEXICON per LANGUAGE family (the surface
|
|
24
|
+
* vocabulary: FL/FLOOR/LVL/LEVEL, ÉTAGE/ÉT, OG/OBERGESCHOSS, …). Keyed by a bare language tag
|
|
25
|
+
* ("en", "fr", "de", …) because the WORDS don't vary by country — American and British English
|
|
26
|
+
* both say "floor", "basement", "penthouse".
|
|
27
|
+
* 2. {@link LEVEL_ORDINAL_CONVENTIONS} — the ORDINAL CONVENTION per full locale ("en-US", "en-GB",
|
|
28
|
+
* …), because the NUMBERING varies by country even within one language: American and Canadian
|
|
29
|
+
* buildings both call the ground floor "the 1st floor"; British buildings, like the rest of the
|
|
30
|
+
* IMDF/continental-European convention locales, do not.
|
|
31
|
+
*
|
|
32
|
+
* {@link levelToOrdinal} composes both: look up the designator's KIND in the locale's language
|
|
33
|
+
* family (ground / basement / numbered / fractional / special / fixed), then — for numbered
|
|
34
|
+
* designators only — apply the locale's numbering convention. IMDF (Apple's Indoor Mapping Data
|
|
35
|
+
* Format) is the schema precedent for encoding a level as a signed integer `ordinal` where ground is
|
|
36
|
+
* always 0; this table supplies the locale-aware mapping from a raw (designator, number) pair into
|
|
37
|
+
* that same ordinal space.
|
|
38
|
+
*
|
|
39
|
+
* Data is convention encoded from common postal/building usage, not a single postal authority
|
|
40
|
+
* publication — level-numbering conventions are cultural, not regulatory, so no Pub-28-style single
|
|
41
|
+
* source exists for most of these locales. Retrieved/encoded 2026-07-13, epic #1100.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately-excluded ambiguities (handled by an explicit, documented rounding rule below, never a
|
|
44
|
+
* silent guess):
|
|
45
|
+
*
|
|
46
|
+
* - **Spanish PRINCIPAL / ENTRESUELO**: pre-metric Spanish buildings run BAJO (0) → ENTRESUELO
|
|
47
|
+
* (~0.5) → PRINCIPAL (1) → PISO 1/2 (2), but the exact offset varies by city and building age.
|
|
48
|
+
* ENTRESUELO's true position (0.5) isn't representable as an integer ordinal; it floors to 0
|
|
49
|
+
* (grouped with ground) — a documented approximation, not an empirical claim. PRINCIPAL is a
|
|
50
|
+
* fixed, always-ordinal-1 designator (it names a specific floor by convention, not by a number the
|
|
51
|
+
* caller supplies).
|
|
52
|
+
* - **English LOWER GROUND / UPPER GROUND** (UK mixed-use buildings): sit at roughly -0.5 and +0.5
|
|
53
|
+
* relative to ground. Both round DOWN (floor): LOWER GROUND → -1 (grouped with the first basement
|
|
54
|
+
* level), UPPER GROUND → 0 (grouped with ground). Convention choices, not measurements.
|
|
55
|
+
* - **PENTHOUSE / ROOF / ATTIC / DACHGESCHOSS / ÁTICO / ATTICO**: named by relationship to the TOP of
|
|
56
|
+
* a SPECIFIC building, not by a fixed distance from ground — there is no locale-independent integer
|
|
57
|
+
* to assign. {@link levelToOrdinal} returns `undefined` for this designator kind rather than
|
|
58
|
+
* guessing.
|
|
59
|
+
* - **Nordic ground-floor vocabulary**: Danish STUEN/STUEETAGE is a well-attested standard term
|
|
60
|
+
* (the "st." you see on Danish addresses). Norwegian has no equally standard, universally-agreed
|
|
61
|
+
* single word for "ground floor" distinct from "1. etasje" in everyday use; GATEPLAN is included
|
|
62
|
+
* here for structural parity with the other Nordic tables but is a lower-confidence, regional
|
|
63
|
+
* inclusion — flagged in-line, not asserted as authoritative.
|
|
64
|
+
*
|
|
65
|
+
* @see {@link https://register.apple.com/resources/imdf/Level/ IMDF Level — `ordinal` (Apple Indoor Mapping Data Format)}
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* How a level designator's ordinal is derived. See the module header for the rationale behind each non-obvious kind.
|
|
70
|
+
*
|
|
71
|
+
* - `"ground"` — always ordinal 0 (RDC, EG, PLANTA BAJA, …).
|
|
72
|
+
* - `"basement"` — ordinal is the negation of the trailing number, defaulting to 1 when the designator appears bare
|
|
73
|
+
* ("Basement" alone → -1, same as "B1").
|
|
74
|
+
* - `"numbered"` — ordinal depends on the locale's {@link LevelOrdinalConvention} (US/CA/JP-style vs
|
|
75
|
+
* continental-European/IMDF-style); requires a number.
|
|
76
|
+
* - `"fractionalAboveGround"` — conceptually between ground and the first numbered level (mezzanine, entresol/entresuelo,
|
|
77
|
+
* upper ground, German Zwischengeschoss); floors to ordinal 0.
|
|
78
|
+
* - `"fractionalBelowGround"` — conceptually between the first basement level and ground (UK lower ground, Italian
|
|
79
|
+
* seminterrato); floors to ordinal -1.
|
|
80
|
+
* - `"special"` — named by relationship to a SPECIFIC building's top (penthouse, roof, attic); no locale-independent
|
|
81
|
+
* ordinal exists. {@link levelToOrdinal} returns `undefined`.
|
|
82
|
+
* - `"fixedOrdinal"` — a specific named floor with its own fixed ordinal, independent of any number the caller supplies
|
|
83
|
+
* (Spanish PRINCIPAL is always ordinal 1).
|
|
84
|
+
*/
|
|
85
|
+
export type LevelDesignatorKind =
|
|
86
|
+
| "ground"
|
|
87
|
+
| "basement"
|
|
88
|
+
| "numbered"
|
|
89
|
+
| "fractionalAboveGround"
|
|
90
|
+
| "fractionalBelowGround"
|
|
91
|
+
| "special"
|
|
92
|
+
| "fixedOrdinal"
|
|
93
|
+
|
|
94
|
+
/** One row of a per-language-family level-designator lexicon. */
|
|
95
|
+
export interface LevelDesignatorRow {
|
|
96
|
+
/** Canonical designator key (the language's native canonical spelling, uppercase). */
|
|
97
|
+
code: string
|
|
98
|
+
/** Human-readable name — the native word plus an English gloss in parentheses. */
|
|
99
|
+
name: string
|
|
100
|
+
/** Recognized surface variants, including the canonical code itself and common ASCII-folded / abbreviated spellings. */
|
|
101
|
+
variants: readonly string[]
|
|
102
|
+
/** How this designator maps to an ordinal — see {@link LevelDesignatorKind}. */
|
|
103
|
+
kind: LevelDesignatorKind
|
|
104
|
+
/** True when a secondary number typically follows ("FL 3", "B 2"); false for standalone designators ("EG", "RDC"). */
|
|
105
|
+
requiresNumber: boolean
|
|
106
|
+
/**
|
|
107
|
+
* Only present when `kind` is `"fixedOrdinal"` — the designator's own fixed ordinal, independent of any passed
|
|
108
|
+
* number.
|
|
109
|
+
*/
|
|
110
|
+
fixedOrdinal?: number
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* English (American, British, Canadian, Australian, …) floor/level vocabulary. This is the GENERIC English lexicon for
|
|
115
|
+
* the ordinal-semantics table; it doesn't replace the more detailed per-system lexicons in
|
|
116
|
+
* {@link "./us/floor-designator.ts"} (USPS Pub-28 C2) or {@link "./au/level-designator.ts"} (AS 4590.1 / AMAS) — those
|
|
117
|
+
* drive span-proposer/synthesis vocabulary for their own address system. This table exists to answer a narrower
|
|
118
|
+
* question for ANY English-speaking locale: given a designator + number, what ordinal does it name.
|
|
119
|
+
*/
|
|
120
|
+
export const EN_LEVEL_DESIGNATORS = [
|
|
121
|
+
{
|
|
122
|
+
code: "FLOOR",
|
|
123
|
+
name: "Floor",
|
|
124
|
+
variants: ["FLOOR", "FL", "FLR", "LEVEL", "LVL"],
|
|
125
|
+
kind: "numbered",
|
|
126
|
+
requiresNumber: true,
|
|
127
|
+
},
|
|
128
|
+
{ code: "BASEMENT", name: "Basement", variants: ["BASEMENT", "BSMT", "B"], kind: "basement", requiresNumber: true },
|
|
129
|
+
{ code: "PENTHOUSE", name: "Penthouse", variants: ["PENTHOUSE", "PH"], kind: "special", requiresNumber: false },
|
|
130
|
+
{
|
|
131
|
+
code: "GROUND",
|
|
132
|
+
name: "Ground",
|
|
133
|
+
variants: ["GROUND", "G", "GROUND FLOOR", "GF"],
|
|
134
|
+
kind: "ground",
|
|
135
|
+
requiresNumber: false,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
code: "LOWER GROUND",
|
|
139
|
+
name: "Lower Ground",
|
|
140
|
+
variants: ["LOWER GROUND", "LG"],
|
|
141
|
+
kind: "fractionalBelowGround",
|
|
142
|
+
requiresNumber: false,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
code: "UPPER GROUND",
|
|
146
|
+
name: "Upper Ground",
|
|
147
|
+
variants: ["UPPER GROUND", "UG"],
|
|
148
|
+
kind: "fractionalAboveGround",
|
|
149
|
+
requiresNumber: false,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
code: "MEZZANINE",
|
|
153
|
+
name: "Mezzanine",
|
|
154
|
+
variants: ["MEZZANINE", "MEZZ", "M"],
|
|
155
|
+
kind: "fractionalAboveGround",
|
|
156
|
+
requiresNumber: false,
|
|
157
|
+
},
|
|
158
|
+
{ code: "ROOF", name: "Roof", variants: ["ROOF", "RF"], kind: "special", requiresNumber: false },
|
|
159
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
160
|
+
|
|
161
|
+
/** French (France, and — for the vocabulary, not the numbering convention — Francophone Canada) floor/level vocabulary. */
|
|
162
|
+
export const FR_LEVEL_DESIGNATORS = [
|
|
163
|
+
{
|
|
164
|
+
code: "ÉTAGE",
|
|
165
|
+
name: "Étage (Floor)",
|
|
166
|
+
variants: ["ÉTAGE", "ETAGE", "ÉT", "ET"],
|
|
167
|
+
kind: "numbered",
|
|
168
|
+
requiresNumber: true,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
code: "RDC",
|
|
172
|
+
name: "Rez-de-chaussée (Ground floor)",
|
|
173
|
+
variants: ["RDC", "REZ-DE-CHAUSSÉE", "REZ-DE-CHAUSSEE", "REZ DE CHAUSSEE"],
|
|
174
|
+
kind: "ground",
|
|
175
|
+
requiresNumber: false,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
code: "SOUS-SOL",
|
|
179
|
+
name: "Sous-sol (Basement)",
|
|
180
|
+
variants: ["SOUS-SOL", "SOUS SOL", "SS"],
|
|
181
|
+
kind: "basement",
|
|
182
|
+
requiresNumber: true,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
code: "ENTRESOL",
|
|
186
|
+
name: "Entresol (Mezzanine)",
|
|
187
|
+
variants: ["ENTRESOL"],
|
|
188
|
+
kind: "fractionalAboveGround",
|
|
189
|
+
requiresNumber: false,
|
|
190
|
+
},
|
|
191
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
192
|
+
|
|
193
|
+
/** German floor/level vocabulary (the -geschoss family). */
|
|
194
|
+
export const DE_LEVEL_DESIGNATORS = [
|
|
195
|
+
{
|
|
196
|
+
code: "OBERGESCHOSS",
|
|
197
|
+
name: "Obergeschoss (Upper floor)",
|
|
198
|
+
variants: ["OBERGESCHOSS", "OG"],
|
|
199
|
+
kind: "numbered",
|
|
200
|
+
requiresNumber: true,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
code: "ERDGESCHOSS",
|
|
204
|
+
name: "Erdgeschoss (Ground floor)",
|
|
205
|
+
variants: ["ERDGESCHOSS", "EG"],
|
|
206
|
+
kind: "ground",
|
|
207
|
+
requiresNumber: false,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
code: "UNTERGESCHOSS",
|
|
211
|
+
name: "Untergeschoss (Basement)",
|
|
212
|
+
variants: ["UNTERGESCHOSS", "UG"],
|
|
213
|
+
kind: "basement",
|
|
214
|
+
requiresNumber: true,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
code: "DACHGESCHOSS",
|
|
218
|
+
name: "Dachgeschoss (Attic/roof floor)",
|
|
219
|
+
variants: ["DACHGESCHOSS", "DG"],
|
|
220
|
+
kind: "special",
|
|
221
|
+
requiresNumber: false,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
code: "ZWISCHENGESCHOSS",
|
|
225
|
+
name: "Zwischengeschoss (Mezzanine)",
|
|
226
|
+
variants: ["ZWISCHENGESCHOSS", "ZG"],
|
|
227
|
+
kind: "fractionalAboveGround",
|
|
228
|
+
requiresNumber: false,
|
|
229
|
+
},
|
|
230
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Spanish floor/level vocabulary. PRINCIPAL and ENTRESUELO offsets vary by city and building age (see the module
|
|
234
|
+
* header) — encoded here as a single convention, not an empirical universal.
|
|
235
|
+
*/
|
|
236
|
+
export const ES_LEVEL_DESIGNATORS = [
|
|
237
|
+
{ code: "PLANTA", name: "Planta/Piso (Floor)", variants: ["PLANTA", "PISO"], kind: "numbered", requiresNumber: true },
|
|
238
|
+
{
|
|
239
|
+
code: "PLANTA BAJA",
|
|
240
|
+
name: "Planta Baja (Ground floor)",
|
|
241
|
+
variants: ["PLANTA BAJA", "BAJO", "PB"],
|
|
242
|
+
kind: "ground",
|
|
243
|
+
requiresNumber: false,
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
code: "ENTRESUELO",
|
|
247
|
+
name: "Entresuelo (Mezzanine)",
|
|
248
|
+
variants: ["ENTRESUELO"],
|
|
249
|
+
kind: "fractionalAboveGround",
|
|
250
|
+
requiresNumber: false,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
code: "PRINCIPAL",
|
|
254
|
+
name: "Principal",
|
|
255
|
+
variants: ["PRINCIPAL"],
|
|
256
|
+
kind: "fixedOrdinal",
|
|
257
|
+
requiresNumber: false,
|
|
258
|
+
fixedOrdinal: 1,
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
code: "SÓTANO",
|
|
262
|
+
name: "Sótano (Basement)",
|
|
263
|
+
variants: ["SÓTANO", "SOTANO"],
|
|
264
|
+
kind: "basement",
|
|
265
|
+
requiresNumber: true,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
code: "ÁTICO",
|
|
269
|
+
name: "Ático (Attic/penthouse)",
|
|
270
|
+
variants: ["ÁTICO", "ATICO"],
|
|
271
|
+
kind: "special",
|
|
272
|
+
requiresNumber: false,
|
|
273
|
+
},
|
|
274
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
275
|
+
|
|
276
|
+
/** Italian floor/level vocabulary. */
|
|
277
|
+
export const IT_LEVEL_DESIGNATORS = [
|
|
278
|
+
{ code: "PIANO", name: "Piano (Floor)", variants: ["PIANO"], kind: "numbered", requiresNumber: true },
|
|
279
|
+
{
|
|
280
|
+
code: "PIANO TERRA",
|
|
281
|
+
name: "Piano Terra (Ground floor)",
|
|
282
|
+
variants: ["PIANO TERRA", "PT"],
|
|
283
|
+
kind: "ground",
|
|
284
|
+
requiresNumber: false,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
code: "SEMINTERRATO",
|
|
288
|
+
name: "Seminterrato (Semi-basement)",
|
|
289
|
+
variants: ["SEMINTERRATO"],
|
|
290
|
+
kind: "fractionalBelowGround",
|
|
291
|
+
requiresNumber: false,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
code: "ATTICO",
|
|
295
|
+
name: "Attico (Attic/penthouse)",
|
|
296
|
+
variants: ["ATTICO"],
|
|
297
|
+
kind: "special",
|
|
298
|
+
requiresNumber: false,
|
|
299
|
+
},
|
|
300
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
301
|
+
|
|
302
|
+
/** Portuguese floor/level vocabulary. */
|
|
303
|
+
export const PT_LEVEL_DESIGNATORS = [
|
|
304
|
+
{ code: "ANDAR", name: "Andar (Floor)", variants: ["ANDAR"], kind: "numbered", requiresNumber: true },
|
|
305
|
+
{
|
|
306
|
+
code: "RÉS-DO-CHÃO",
|
|
307
|
+
name: "Rés-do-chão (Ground floor)",
|
|
308
|
+
variants: ["RÉS-DO-CHÃO", "RES-DO-CHAO", "RC"],
|
|
309
|
+
kind: "ground",
|
|
310
|
+
requiresNumber: false,
|
|
311
|
+
},
|
|
312
|
+
{ code: "CAVE", name: "Cave (Basement)", variants: ["CAVE"], kind: "basement", requiresNumber: true },
|
|
313
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
314
|
+
|
|
315
|
+
/** Dutch floor/level vocabulary. */
|
|
316
|
+
export const NL_LEVEL_DESIGNATORS = [
|
|
317
|
+
{
|
|
318
|
+
code: "VERDIEPING",
|
|
319
|
+
name: "Verdieping (Floor)",
|
|
320
|
+
variants: ["VERDIEPING", "VERD"],
|
|
321
|
+
kind: "numbered",
|
|
322
|
+
requiresNumber: true,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
code: "BEGANE GROND",
|
|
326
|
+
name: "Begane Grond (Ground floor)",
|
|
327
|
+
variants: ["BEGANE GROND", "BG"],
|
|
328
|
+
kind: "ground",
|
|
329
|
+
requiresNumber: false,
|
|
330
|
+
},
|
|
331
|
+
{ code: "KELDER", name: "Kelder (Basement)", variants: ["KELDER"], kind: "basement", requiresNumber: true },
|
|
332
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Japanese (and generic CJK numeral+letter) floor/level vocabulary. Japanese addresses write the numbered floor as a
|
|
336
|
+
* trailing "F" suffix on the number ("2F", "地下1F"/"B1F") or the kanji "階" ("2階"); there is no distinct bare word for
|
|
337
|
+
* "ground floor" the way RDC/EG/PLANTA BAJA exist in Europe — "1F"/"1階" already IS ground (handled by the `"numbered"`
|
|
338
|
+
* kind + the ja-JP `firstNumberedIsGround: true` convention, not a separate `"ground"` row). "B" (and "地下", literally
|
|
339
|
+
* "underground") name the basement count the same way English "B1" does.
|
|
340
|
+
*/
|
|
341
|
+
export const JA_LEVEL_DESIGNATORS = [
|
|
342
|
+
{ code: "F", name: "階 (Floor)", variants: ["F", "階"], kind: "numbered", requiresNumber: true },
|
|
343
|
+
{ code: "B", name: "地下 (Basement)", variants: ["B", "地下"], kind: "basement", requiresNumber: true },
|
|
344
|
+
{ code: "RF", name: "屋上 (Rooftop)", variants: ["RF", "屋上", "ROOFTOP"], kind: "special", requiresNumber: false },
|
|
345
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
346
|
+
|
|
347
|
+
/** Swedish floor/level vocabulary. */
|
|
348
|
+
export const SV_LEVEL_DESIGNATORS = [
|
|
349
|
+
{ code: "VÅNING", name: "Våning (Floor)", variants: ["VÅNING", "VANING"], kind: "numbered", requiresNumber: true },
|
|
350
|
+
{
|
|
351
|
+
code: "BOTTENVÅNING",
|
|
352
|
+
name: "Bottenvåning (Ground floor)",
|
|
353
|
+
variants: ["BOTTENVÅNING", "BOTTENVANING", "BV"],
|
|
354
|
+
kind: "ground",
|
|
355
|
+
requiresNumber: false,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
code: "KÄLLARE",
|
|
359
|
+
name: "Källare (Basement)",
|
|
360
|
+
variants: ["KÄLLARE", "KALLARE"],
|
|
361
|
+
kind: "basement",
|
|
362
|
+
requiresNumber: true,
|
|
363
|
+
},
|
|
364
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Norwegian floor/level vocabulary. GATEPLAN ("street level") is a lower-confidence, regional inclusion for the
|
|
368
|
+
* ground-floor row — see the module header's Nordic-vocabulary caveat.
|
|
369
|
+
*/
|
|
370
|
+
export const NO_LEVEL_DESIGNATORS = [
|
|
371
|
+
{ code: "ETASJE", name: "Etasje (Floor)", variants: ["ETASJE"], kind: "numbered", requiresNumber: true },
|
|
372
|
+
{
|
|
373
|
+
code: "GATEPLAN",
|
|
374
|
+
name: "Gateplan (Street/ground level)",
|
|
375
|
+
variants: ["GATEPLAN"],
|
|
376
|
+
kind: "ground",
|
|
377
|
+
requiresNumber: false,
|
|
378
|
+
},
|
|
379
|
+
{ code: "KJELLER", name: "Kjeller (Basement)", variants: ["KJELLER"], kind: "basement", requiresNumber: true },
|
|
380
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
381
|
+
|
|
382
|
+
/** Danish floor/level vocabulary. STUEN/STUEETAGE ("st.") is the standard ground-floor term seen on Danish addresses. */
|
|
383
|
+
export const DA_LEVEL_DESIGNATORS = [
|
|
384
|
+
{ code: "ETAGE", name: "Etage (Floor)", variants: ["ETAGE"], kind: "numbered", requiresNumber: true },
|
|
385
|
+
{
|
|
386
|
+
code: "STUEN",
|
|
387
|
+
name: "Stuen/Stueetage (Ground floor)",
|
|
388
|
+
variants: ["STUEN", "STUEETAGE", "ST"],
|
|
389
|
+
kind: "ground",
|
|
390
|
+
requiresNumber: false,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
code: "KÆLDER",
|
|
394
|
+
name: "Kælder (Basement)",
|
|
395
|
+
variants: ["KÆLDER", "KAELDER"],
|
|
396
|
+
kind: "basement",
|
|
397
|
+
requiresNumber: true,
|
|
398
|
+
},
|
|
399
|
+
] as const satisfies readonly LevelDesignatorRow[]
|
|
400
|
+
|
|
401
|
+
/** A bare language-family tag — the key into {@link LEVEL_DESIGNATORS_BY_FAMILY}. */
|
|
402
|
+
export type LevelLocaleFamily = "en" | "fr" | "de" | "es" | "it" | "pt" | "nl" | "ja" | "sv" | "no" | "da"
|
|
403
|
+
|
|
404
|
+
/** Every language family's level-designator lexicon, keyed by bare language tag. */
|
|
405
|
+
export const LEVEL_DESIGNATORS_BY_FAMILY: Readonly<Record<LevelLocaleFamily, readonly LevelDesignatorRow[]>> = {
|
|
406
|
+
en: EN_LEVEL_DESIGNATORS,
|
|
407
|
+
fr: FR_LEVEL_DESIGNATORS,
|
|
408
|
+
de: DE_LEVEL_DESIGNATORS,
|
|
409
|
+
es: ES_LEVEL_DESIGNATORS,
|
|
410
|
+
it: IT_LEVEL_DESIGNATORS,
|
|
411
|
+
pt: PT_LEVEL_DESIGNATORS,
|
|
412
|
+
nl: NL_LEVEL_DESIGNATORS,
|
|
413
|
+
ja: JA_LEVEL_DESIGNATORS,
|
|
414
|
+
sv: SV_LEVEL_DESIGNATORS,
|
|
415
|
+
no: NO_LEVEL_DESIGNATORS,
|
|
416
|
+
da: DA_LEVEL_DESIGNATORS,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Per-family inverse lookup (lowercase variant → its designator row), built once at module load. Structural integrity
|
|
421
|
+
* check runs here: every row must carry at least one non-blank variant, and no variant may repeat WITHIN a family
|
|
422
|
+
* (case-insensitive) — a malformed table throws loudly at import time rather than silently producing an ambiguous
|
|
423
|
+
* lexicon. Collisions ACROSS families are expected and fine (that's the entire point of this module: "UG" is Upper
|
|
424
|
+
* Ground in English but Untergeschoss in German — same token, different family, different meaning).
|
|
425
|
+
*/
|
|
426
|
+
const LEVEL_DESIGNATOR_LOOKUP_BY_FAMILY: ReadonlyMap<
|
|
427
|
+
LevelLocaleFamily,
|
|
428
|
+
ReadonlyMap<string, LevelDesignatorRow>
|
|
429
|
+
> = (() => {
|
|
430
|
+
const byFamily = new Map<LevelLocaleFamily, ReadonlyMap<string, LevelDesignatorRow>>()
|
|
431
|
+
|
|
432
|
+
for (const family of Object.keys(LEVEL_DESIGNATORS_BY_FAMILY) as LevelLocaleFamily[]) {
|
|
433
|
+
const rows = LEVEL_DESIGNATORS_BY_FAMILY[family]
|
|
434
|
+
const lookup = new Map<string, LevelDesignatorRow>()
|
|
435
|
+
|
|
436
|
+
for (const row of rows) {
|
|
437
|
+
if (row.variants.length === 0) {
|
|
438
|
+
throw new Error(`[codex/level-semantics] family "${family}" designator "${row.code}" has no variants`)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const variant of row.variants) {
|
|
442
|
+
if (!variant || !variant.trim()) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`[codex/level-semantics] family "${family}" designator "${row.code}" has an empty or blank variant`
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const key = variant.toLowerCase()
|
|
449
|
+
const existing = lookup.get(key)
|
|
450
|
+
|
|
451
|
+
if (existing) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`[codex/level-semantics] family "${family}" has a duplicate variant "${variant}" (designators "${existing.code}" and "${row.code}")`
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
lookup.set(key, row)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
byFamily.set(family, lookup)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return byFamily
|
|
465
|
+
})()
|
|
466
|
+
|
|
467
|
+
const LEVEL_LOCALE_FAMILIES: ReadonlySet<LevelLocaleFamily> = new Set(
|
|
468
|
+
Object.keys(LEVEL_DESIGNATORS_BY_FAMILY) as LevelLocaleFamily[]
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
/** BCP-47-ish language tags that fold into the Norwegian family (Bokmål, Nynorsk, and the deprecated macrolanguage tag). */
|
|
472
|
+
const NORWEGIAN_LANGUAGE_TAGS: ReadonlySet<string> = new Set(["no", "nb", "nn"])
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Split `locale` into `{ language, region }`, lowercasing the language and uppercasing the region. Tolerant of a bare
|
|
476
|
+
* language tag (no region).
|
|
477
|
+
*/
|
|
478
|
+
function splitLocaleTag(locale: string): { language: string; region: string | undefined } {
|
|
479
|
+
const [language, region] = locale.split("-")
|
|
480
|
+
|
|
481
|
+
return { language: (language ?? "").toLowerCase(), region: region?.toUpperCase() }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Resolve a BCP-47-ish locale tag to its {@link LevelLocaleFamily}, or `undefined` if this module has no lexicon for it. */
|
|
485
|
+
function localeFamily(locale: string): LevelLocaleFamily | undefined {
|
|
486
|
+
const { language } = splitLocaleTag(locale)
|
|
487
|
+
const family = NORWEGIAN_LANGUAGE_TAGS.has(language) ? "no" : language
|
|
488
|
+
|
|
489
|
+
return LEVEL_LOCALE_FAMILIES.has(family as LevelLocaleFamily) ? (family as LevelLocaleFamily) : undefined
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The locale's level-numbering convention: does the FIRST numbered level ("1st floor", "1F", "étage 1", …) coincide
|
|
494
|
+
* with ground (ordinal 0), or sit one storey above it?
|
|
495
|
+
*/
|
|
496
|
+
export interface LevelOrdinalConvention {
|
|
497
|
+
/**
|
|
498
|
+
* True for the US/Canada/Japan-style convention, where the first numbered level IS ground ("1st floor" = ground floor
|
|
499
|
+
* = ordinal 0, so ordinal = number - 1). False for the continental-European / IMDF-style convention, where ground has
|
|
500
|
+
* its own designator (RDC, EG, PLANTA BAJA, …) and the first NUMBERED level sits one storey above it (ordinal =
|
|
501
|
+
* number) — also the convention in the UK.
|
|
502
|
+
*/
|
|
503
|
+
readonly firstNumberedIsGround: boolean
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Per-FULL-LOCALE ordinal convention overrides. Keyed by full locale (not bare language family) because English splits
|
|
508
|
+
* by country even though the vocabulary doesn't: American and Canadian buildings number the ground floor "1"; British
|
|
509
|
+
* buildings do not. "CA" (English or French) buckets with the US/Japan convention per real-world North American
|
|
510
|
+
* building-code practice, though individual Quebec buildings can and do vary.
|
|
511
|
+
*/
|
|
512
|
+
export const LEVEL_ORDINAL_CONVENTIONS: Readonly<Record<string, LevelOrdinalConvention>> = {
|
|
513
|
+
"en-US": { firstNumberedIsGround: true },
|
|
514
|
+
"en-CA": { firstNumberedIsGround: true },
|
|
515
|
+
"en-GB": { firstNumberedIsGround: false },
|
|
516
|
+
"fr-CA": { firstNumberedIsGround: true },
|
|
517
|
+
"ja-JP": { firstNumberedIsGround: true },
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Default convention per language family, used when {@link levelToOrdinal} is given a bare-language locale ("fr" with no
|
|
522
|
+
* country) or a country this table doesn't specifically override. Every entry here follows the
|
|
523
|
+
* continental-European/IMDF convention (ground is its own designator; numbered floors start at 1 for the storey above)
|
|
524
|
+
* except Japanese, which follows the US/CA convention. English has NO family-wide default — American/Canadian and
|
|
525
|
+
* British buildings disagree, so a bare "en" locale intentionally resolves to `undefined` rather than guessing.
|
|
526
|
+
*/
|
|
527
|
+
const FAMILY_DEFAULT_ORDINAL_CONVENTION: Partial<Record<LevelLocaleFamily, LevelOrdinalConvention>> = {
|
|
528
|
+
fr: { firstNumberedIsGround: false },
|
|
529
|
+
de: { firstNumberedIsGround: false },
|
|
530
|
+
es: { firstNumberedIsGround: false },
|
|
531
|
+
it: { firstNumberedIsGround: false },
|
|
532
|
+
pt: { firstNumberedIsGround: false },
|
|
533
|
+
nl: { firstNumberedIsGround: false },
|
|
534
|
+
sv: { firstNumberedIsGround: false },
|
|
535
|
+
no: { firstNumberedIsGround: false },
|
|
536
|
+
da: { firstNumberedIsGround: false },
|
|
537
|
+
ja: { firstNumberedIsGround: true },
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Resolve the ordinal convention for `locale`: an exact full-locale override, else the language family's default, else
|
|
542
|
+
* `undefined`.
|
|
543
|
+
*/
|
|
544
|
+
function resolveOrdinalConvention(locale: string): LevelOrdinalConvention | undefined {
|
|
545
|
+
const { language, region } = splitLocaleTag(locale)
|
|
546
|
+
const normalized = region ? `${language}-${region}` : language
|
|
547
|
+
|
|
548
|
+
if (LEVEL_ORDINAL_CONVENTIONS[normalized]) {
|
|
549
|
+
return LEVEL_ORDINAL_CONVENTIONS[normalized]
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const family = localeFamily(locale)
|
|
553
|
+
|
|
554
|
+
return family ? FAMILY_DEFAULT_ORDINAL_CONVENTION[family] : undefined
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Look up a level designator (by canonical code, abbreviation, or any recognized variant) within a locale's language
|
|
559
|
+
* family. Case-insensitive. Returns `undefined` when the locale's family is unknown to this module, or the token isn't
|
|
560
|
+
* a recognized designator in that family.
|
|
561
|
+
*/
|
|
562
|
+
export function lookupLevelDesignator(designator: string, locale: string): LevelDesignatorRow | undefined {
|
|
563
|
+
if (!designator || typeof designator !== "string") return undefined
|
|
564
|
+
const family = localeFamily(locale)
|
|
565
|
+
|
|
566
|
+
if (!family) return undefined
|
|
567
|
+
|
|
568
|
+
return LEVEL_DESIGNATOR_LOOKUP_BY_FAMILY.get(family)?.get(designator.trim().toLowerCase())
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** True when `input` is a recognized level designator (case-insensitive) in `locale`'s language family. */
|
|
572
|
+
export function isLevelDesignatorToken(input: unknown, locale: string): boolean {
|
|
573
|
+
return typeof input === "string" && lookupLevelDesignator(input, locale) !== undefined
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Map a (designator, number) pair to an IMDF-style signed integer ordinal, given the semantics of `locale`. Ground is
|
|
578
|
+
* always 0. Returns `undefined` when:
|
|
579
|
+
*
|
|
580
|
+
* - `locale`'s language family has no lexicon in this module,
|
|
581
|
+
* - `designator` isn't a recognized token in that family,
|
|
582
|
+
* - The designator is `"special"` (penthouse/roof/attic — no locale-independent ordinal exists), or
|
|
583
|
+
* - The designator is `"numbered"` but either `number` is missing or the locale has no resolvable ordinal convention (a
|
|
584
|
+
* bare "en" locale, for example).
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* levelToOrdinal("FL", 1, "en-US") // → 0 (US: 1st floor IS ground)
|
|
588
|
+
* levelToOrdinal("étage", 1, "fr-FR") // → 1 (FR: 1st étage is one storey above ground)
|
|
589
|
+
* levelToOrdinal("EG", undefined, "de-DE") // → 0 (ground, number ignored)
|
|
590
|
+
* levelToOrdinal("B", 1, "en-US") // → -1 (basement 1)
|
|
591
|
+
* levelToOrdinal("F", 1, "ja-JP") // → 0 (JP: 1F IS ground)
|
|
592
|
+
* levelToOrdinal("B", 1, "ja-JP") // → -1 (JP: B1F)
|
|
593
|
+
*/
|
|
594
|
+
export function levelToOrdinal(designator: string, number: number | undefined, locale: string): number | undefined {
|
|
595
|
+
const row = lookupLevelDesignator(designator, locale)
|
|
596
|
+
|
|
597
|
+
if (!row) return undefined
|
|
598
|
+
|
|
599
|
+
switch (row.kind) {
|
|
600
|
+
case "ground":
|
|
601
|
+
return 0
|
|
602
|
+
case "fractionalAboveGround":
|
|
603
|
+
return 0
|
|
604
|
+
case "fractionalBelowGround":
|
|
605
|
+
return -1
|
|
606
|
+
case "special":
|
|
607
|
+
return undefined
|
|
608
|
+
case "fixedOrdinal":
|
|
609
|
+
return row.fixedOrdinal
|
|
610
|
+
case "basement":
|
|
611
|
+
return -Math.abs(number ?? 1)
|
|
612
|
+
case "numbered": {
|
|
613
|
+
if (number === undefined) return undefined
|
|
614
|
+
const convention = resolveOrdinalConvention(locale)
|
|
615
|
+
|
|
616
|
+
if (!convention) return undefined
|
|
617
|
+
|
|
618
|
+
return convention.firstNumberedIsGround ? number - 1 : number
|
|
619
|
+
}
|
|
620
|
+
default:
|
|
621
|
+
return undefined
|
|
622
|
+
}
|
|
623
|
+
}
|