@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,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* codex → corpus-python bridge: emit the authoritative country surface forms as JSON so the Python
|
|
7
|
+
* shard generators can synthesize address tails ("…, USA" / "…, United States of America") without
|
|
8
|
+
* re-deriving the country name/alias data. `@mailwoman/codex` stays the single source of truth
|
|
9
|
+
* (COUNTRY_SURFACE_FORMS + ISO2_TO_NAME, salvaged from isp-nexus spatial/countries); this writes a
|
|
10
|
+
* snapshot the language boundary can't import directly.
|
|
11
|
+
*
|
|
12
|
+
* Regenerate: `node codex/tools/export-country-surfaces.ts` (writes the corpus-python data file).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFileSync } from "node:fs"
|
|
16
|
+
import { resolve } from "node:path"
|
|
17
|
+
|
|
18
|
+
import { COUNTRY_SURFACE_FORMS, ISO2_TO_NAME } from "../country/country.ts"
|
|
19
|
+
|
|
20
|
+
// Merge: rich surface forms where the codex curates them, else the canonical English name for every
|
|
21
|
+
// ISO 3166-1 alpha-2. Canonical-name-first (the codex's own ordering) so the common form leads.
|
|
22
|
+
const surfaces: Record<string, string[]> = {}
|
|
23
|
+
|
|
24
|
+
for (const [iso2, forms] of Object.entries(COUNTRY_SURFACE_FORMS)) {
|
|
25
|
+
surfaces[iso2] = [...forms]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [iso2, name] of ISO2_TO_NAME) {
|
|
29
|
+
if (!surfaces[iso2]) {
|
|
30
|
+
surfaces[iso2] = [name]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const out = resolve(import.meta.dirname, "../../corpus-python/src/mailwoman_train/data/country-surfaces.json")
|
|
35
|
+
writeFileSync(
|
|
36
|
+
out,
|
|
37
|
+
JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
_generated: "codex/tools/export-country-surfaces.ts from @mailwoman/codex COUNTRY_SURFACE_FORMS + ISO2_TO_NAME",
|
|
40
|
+
surfaces,
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
2
|
|
44
|
+
) + "\n"
|
|
45
|
+
)
|
|
46
|
+
process.stderr.write(`wrote ${Object.keys(surfaces).length} countries → ${out}\n`)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Regenerate `codex/country/reference-data.ts` — the per-country calling code (E.164) + currency
|
|
7
|
+
* (ISO 4217) table — from mledoze/countries (https://github.com/mledoze/countries, ODbL). The
|
|
8
|
+
* output is committed; this tool makes it reproducible (provenance), not a hand-typed dictionary.
|
|
9
|
+
*
|
|
10
|
+
* Calling-code rule: mledoze splits the code as `idd.root` + `idd.suffixes`. For most countries a
|
|
11
|
+
* single suffix completes the code (GB `+4` + `4` = 44); NANP members share root `+1` with their
|
|
12
|
+
* area code as the suffix, so they map to 1.
|
|
13
|
+
*
|
|
14
|
+
* Usage: mailwoman dev generate country-reference
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { writeFileSync } from "node:fs"
|
|
18
|
+
import { fileURLToPath } from "node:url"
|
|
19
|
+
|
|
20
|
+
const SOURCE = "https://raw.githubusercontent.com/mledoze/countries/master/countries.json"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The committed output path, resolved relative to this module (codex/tools/ → codex/country/). The codegen is
|
|
24
|
+
* repo-only, and in the repo `@mailwoman/codex/tools` always loads from source via the `node` exports condition, so
|
|
25
|
+
* `import.meta.url` points at the source tree. (`@mailwoman/core`'s `repoRootPath` would also work, but codex is
|
|
26
|
+
* zero-runtime-dep and `core` already references `codex` — importing core here would cycle the project graph.)
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_OUT = fileURLToPath(new URL("../country/reference-data.ts", import.meta.url))
|
|
29
|
+
|
|
30
|
+
/** A single country record from mledoze/countries, narrowed to the fields this tool reads. */
|
|
31
|
+
interface MledozeCountry {
|
|
32
|
+
cca2?: string
|
|
33
|
+
idd?: { root?: string; suffixes?: string[] }
|
|
34
|
+
currencies?: Record<string, { name?: string; symbol?: string }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The emitted per-country reference row. */
|
|
38
|
+
interface CountryReferenceEntry {
|
|
39
|
+
callingCode?: number
|
|
40
|
+
currency?: { isoCode: string; name?: string; symbol?: string }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Options for {@linkcode generateCountryReference}. */
|
|
44
|
+
export interface GenerateCountryReferenceOptions {
|
|
45
|
+
/** Output path override. Default: `codex/country/reference-data.ts` (the committed table). */
|
|
46
|
+
out?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Summary returned by {@linkcode generateCountryReference}. */
|
|
50
|
+
export interface GenerateCountryReferenceSummary {
|
|
51
|
+
countries: number
|
|
52
|
+
outPath: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function callingCode(country: MledozeCountry): number | undefined {
|
|
56
|
+
const root = (country.idd?.root ?? "").replace("+", "")
|
|
57
|
+
const suffixes = country.idd?.suffixes ?? []
|
|
58
|
+
|
|
59
|
+
if (!root) return undefined
|
|
60
|
+
|
|
61
|
+
if (root === "1") return 1
|
|
62
|
+
|
|
63
|
+
if (suffixes.length === 1) {
|
|
64
|
+
const n = Number(root + suffixes[0])
|
|
65
|
+
|
|
66
|
+
return Number.isFinite(n) ? n : undefined
|
|
67
|
+
}
|
|
68
|
+
const n = Number(root)
|
|
69
|
+
|
|
70
|
+
return Number.isFinite(n) ? n : undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const serialize = (o: CountryReferenceEntry): string =>
|
|
74
|
+
JSON.stringify(o, null, 0)
|
|
75
|
+
.replace(/"isoCode"/g, "isoCode")
|
|
76
|
+
.replace(/"callingCode"/g, "callingCode")
|
|
77
|
+
.replace(/"currency"/g, "currency")
|
|
78
|
+
.replace(/"name"/g, "name")
|
|
79
|
+
.replace(/"symbol"/g, "symbol")
|
|
80
|
+
|
|
81
|
+
/** Fetch mledoze/countries and regenerate the committed `COUNTRY_REFERENCE` table. */
|
|
82
|
+
export async function generateCountryReference(
|
|
83
|
+
options: GenerateCountryReferenceOptions = {},
|
|
84
|
+
report?: (line: string) => void
|
|
85
|
+
): Promise<GenerateCountryReferenceSummary> {
|
|
86
|
+
const outPath = options.out ?? DEFAULT_OUT
|
|
87
|
+
const response = await fetch(SOURCE)
|
|
88
|
+
|
|
89
|
+
if (!response.ok) throw new Error(`fetch ${SOURCE} failed: ${response.status}`)
|
|
90
|
+
const countries = (await response.json()) as MledozeCountry[]
|
|
91
|
+
|
|
92
|
+
const rows: Record<string, CountryReferenceEntry> = {}
|
|
93
|
+
|
|
94
|
+
for (const country of countries) {
|
|
95
|
+
const alpha2 = country.cca2
|
|
96
|
+
|
|
97
|
+
if (!alpha2) continue
|
|
98
|
+
const entry: CountryReferenceEntry = {}
|
|
99
|
+
const cc = callingCode(country)
|
|
100
|
+
|
|
101
|
+
if (cc != null) {
|
|
102
|
+
entry.callingCode = cc
|
|
103
|
+
}
|
|
104
|
+
const currencyCodes = Object.keys(country.currencies ?? {}).sort()
|
|
105
|
+
|
|
106
|
+
if (currencyCodes.length) {
|
|
107
|
+
const code = currencyCodes[0]!
|
|
108
|
+
const info = country.currencies![code] ?? {}
|
|
109
|
+
entry.currency = { isoCode: code }
|
|
110
|
+
|
|
111
|
+
if (info.name) {
|
|
112
|
+
entry.currency.name = info.name
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (info.symbol) {
|
|
116
|
+
entry.currency.symbol = info.symbol
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Object.keys(entry).length) {
|
|
121
|
+
rows[alpha2] = entry
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = Object.keys(rows)
|
|
126
|
+
.sort()
|
|
127
|
+
.map((k) => `\t${k}: ${serialize(rows[k]!)},`)
|
|
128
|
+
.join("\n")
|
|
129
|
+
|
|
130
|
+
const header = `/**
|
|
131
|
+
* @copyright Sister Software
|
|
132
|
+
* @license AGPL-3.0
|
|
133
|
+
* @author Teffen Ellis, et al.
|
|
134
|
+
*
|
|
135
|
+
* GENERATED — do not edit by hand. Country calling codes (E.164) + currencies (ISO 4217), derived
|
|
136
|
+
* from mledoze/countries (https://github.com/mledoze/countries, ODbL). NANP members map to 1.
|
|
137
|
+
* Regenerate with: mailwoman dev generate country-reference
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
/** Static per-country reference: calling code + currency. */
|
|
141
|
+
export interface CountryReference {
|
|
142
|
+
callingCode?: number
|
|
143
|
+
currency?: { isoCode: string; name?: string; symbol?: string }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** ISO 3166-1 alpha-2 → reference. */
|
|
147
|
+
export const COUNTRY_REFERENCE: Record<string, CountryReference> = {`
|
|
148
|
+
|
|
149
|
+
writeFileSync(outPath, `${header}\n${body}\n}\n`)
|
|
150
|
+
report?.(`wrote ${outPath} (${Object.keys(rows).length} countries)`)
|
|
151
|
+
|
|
152
|
+
return { countries: Object.keys(rows).length, outPath }
|
|
153
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Regenerates `codex/country/official-languages.ts` from Unicode CLDR supplemental data
|
|
7
|
+
* (territoryInfo `_officialStatus` + languageAlias). The emitted table is the #936 ingest bit's
|
|
8
|
+
* authority for "is this name row in an official language of its country?" — consumed by the
|
|
9
|
+
* gazetteer builders (`mailwoman gazetteer build`, `@mailwoman/resolver-wof-sqlite`'s GeoNames
|
|
10
|
+
* fold), never at query time.
|
|
11
|
+
*
|
|
12
|
+
* Each language is emitted under EVERY ISO-639 spelling CLDR aliases to it (fi + fin, sv + swe)
|
|
13
|
+
* so consumers can test WOF's 639-3 tags, Overture's BCP-47 keys, and GeoNames' mixed 2/3-letter
|
|
14
|
+
* codes without a mapping step.
|
|
15
|
+
*
|
|
16
|
+
* Usage: mailwoman dev generate official-languages [--cldr-dir <dir>] [--cldr-version 47.0.0]
|
|
17
|
+
*
|
|
18
|
+
* With `cldrDir`, reads cldr-territoryInfo.json + cldr-aliases.json from disk; otherwise fetches
|
|
19
|
+
* the pinned cldr-core release from jsdelivr.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
23
|
+
import { join } from "node:path"
|
|
24
|
+
import { fileURLToPath } from "node:url"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The committed output path, resolved relative to this module (codex/tools/ → codex/country/). The codegen is
|
|
28
|
+
* repo-only, and in the repo `@mailwoman/codex/tools` always loads from source via the `node` exports condition, so
|
|
29
|
+
* `import.meta.url` points at the source tree.
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_OUT = fileURLToPath(new URL("../country/official-languages.ts", import.meta.url))
|
|
32
|
+
|
|
33
|
+
/** Options for {@linkcode generateOfficialLanguages}. */
|
|
34
|
+
export interface GenerateOfficialLanguagesOptions {
|
|
35
|
+
/** Read cldr-territoryInfo.json + cldr-aliases.json from this directory instead of fetching. */
|
|
36
|
+
cldrDir?: string
|
|
37
|
+
/** Pinned cldr-core release fetched from jsdelivr when {@linkcode GenerateOfficialLanguagesOptions.cldrDir} is absent. */
|
|
38
|
+
cldrVersion?: string
|
|
39
|
+
/** Output path override. Default: `codex/country/official-languages.ts` (the committed table). */
|
|
40
|
+
out?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Summary returned by {@linkcode generateOfficialLanguages}. */
|
|
44
|
+
export interface GenerateOfficialLanguagesSummary {
|
|
45
|
+
territories: number
|
|
46
|
+
cldrVersion: string
|
|
47
|
+
outPath: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface LanguagePopulation {
|
|
51
|
+
_officialStatus?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function loadCLDR(file: string, cldrDir: string | undefined, cldrVersion: string): Promise<unknown> {
|
|
55
|
+
if (cldrDir) return JSON.parse(readFileSync(join(cldrDir, `cldr-${file}.json`), "utf8"))
|
|
56
|
+
const url = `https://cdn.jsdelivr.net/npm/cldr-core@${cldrVersion}/supplemental/${file}.json`
|
|
57
|
+
const res = await fetch(url)
|
|
58
|
+
|
|
59
|
+
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`)
|
|
60
|
+
|
|
61
|
+
return res.json()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Regenerate the committed `OFFICIAL_LANGUAGES` table from CLDR supplemental data. */
|
|
65
|
+
export async function generateOfficialLanguages(
|
|
66
|
+
options: GenerateOfficialLanguagesOptions = {},
|
|
67
|
+
report?: (line: string) => void
|
|
68
|
+
): Promise<GenerateOfficialLanguagesSummary> {
|
|
69
|
+
const cldrVersion = options.cldrVersion ?? "47.0.0"
|
|
70
|
+
const outPath = options.out ?? DEFAULT_OUT
|
|
71
|
+
|
|
72
|
+
const territoryInfo = (
|
|
73
|
+
(await loadCLDR("territoryInfo", options.cldrDir, cldrVersion)) as Record<
|
|
74
|
+
string,
|
|
75
|
+
Record<string, Record<string, unknown>>
|
|
76
|
+
>
|
|
77
|
+
).supplemental!.territoryInfo as Record<string, { languagePopulation?: Record<string, LanguagePopulation> }>
|
|
78
|
+
const aliasesDoc = (await loadCLDR("aliases", options.cldrDir, cldrVersion)) as {
|
|
79
|
+
supplemental: { metadata: { alias: { languageAlias: Record<string, { _replacement?: string }> } } }
|
|
80
|
+
}
|
|
81
|
+
const languageAlias = aliasesDoc.supplemental.metadata.alias.languageAlias
|
|
82
|
+
|
|
83
|
+
// canonical code → every plain 2-3 letter alias spelling that maps to it (fi gains "fin")
|
|
84
|
+
const spellingsOf = new Map<string, Set<string>>()
|
|
85
|
+
|
|
86
|
+
for (const [alias, entry] of Object.entries(languageAlias)) {
|
|
87
|
+
const canon = entry._replacement
|
|
88
|
+
|
|
89
|
+
if (!canon || !/^[a-z]{2,3}$/.test(alias)) continue
|
|
90
|
+
let set = spellingsOf.get(canon)
|
|
91
|
+
|
|
92
|
+
if (!set) {
|
|
93
|
+
spellingsOf.set(canon, (set = new Set()))
|
|
94
|
+
}
|
|
95
|
+
set.add(alias)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const table: Record<string, { official: string[]; regional?: string[] }> = {}
|
|
99
|
+
|
|
100
|
+
for (const territory of Object.keys(territoryInfo).sort()) {
|
|
101
|
+
if (!/^[A-Z]{2}$/.test(territory)) continue
|
|
102
|
+
const pops = territoryInfo[territory]!.languagePopulation
|
|
103
|
+
|
|
104
|
+
if (!pops) continue
|
|
105
|
+
const official = new Set<string>()
|
|
106
|
+
const regional = new Set<string>()
|
|
107
|
+
|
|
108
|
+
for (const [lang, data] of Object.entries(pops)) {
|
|
109
|
+
const status = data._officialStatus
|
|
110
|
+
|
|
111
|
+
if (!status) continue
|
|
112
|
+
// CLDR keys can carry script subtags ("zh_Hant") — name tags use the base language.
|
|
113
|
+
const base = lang.split("_")[0]!
|
|
114
|
+
const spellings = [base, ...(spellingsOf.get(base) ?? [])].sort()
|
|
115
|
+
|
|
116
|
+
if (status === "official" || status === "de_facto_official") {
|
|
117
|
+
for (const s of spellings) {
|
|
118
|
+
official.add(s)
|
|
119
|
+
}
|
|
120
|
+
} else if (status === "official_regional") {
|
|
121
|
+
for (const s of spellings) {
|
|
122
|
+
regional.add(s)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (official.size === 0 && regional.size === 0) continue
|
|
128
|
+
table[territory] = { official: [...official].sort() }
|
|
129
|
+
|
|
130
|
+
if (regional.size > 0) {
|
|
131
|
+
table[territory]!.regional = [...regional].sort()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entries = Object.entries(table)
|
|
136
|
+
.map(([cc, v]) => {
|
|
137
|
+
const reg = v.regional ? `, regional: [${v.regional.map((l) => `"${l}"`).join(", ")}]` : ""
|
|
138
|
+
|
|
139
|
+
return `\t${cc}: { official: [${v.official.map((l) => `"${l}"`).join(", ")}]${reg} },`
|
|
140
|
+
})
|
|
141
|
+
.join("\n")
|
|
142
|
+
|
|
143
|
+
const header = `/**
|
|
144
|
+
* @copyright Sister Software
|
|
145
|
+
* @license AGPL-3.0
|
|
146
|
+
* @author Teffen Ellis, et al.
|
|
147
|
+
*
|
|
148
|
+
* GENERATED — do not edit by hand. Official languages per ISO 3166-1 territory, derived from
|
|
149
|
+
* Unicode CLDR ${cldrVersion} supplemental territoryInfo (\`_officialStatus\`). \`official\` merges
|
|
150
|
+
* CLDR's \`official\` + \`de_facto_official\`; \`regional\` is \`official_regional\` (kept separate —
|
|
151
|
+
* the #936 probe showed it pulls in cross-border quirks like Korean-in-CN, so consumers opt in).
|
|
152
|
+
* Every language appears under each ISO-639 spelling CLDR aliases to it (fi AND fin) so WOF
|
|
153
|
+
* 639-3 tags, Overture BCP-47 keys, and GeoNames codes all match without mapping.
|
|
154
|
+
* Regenerate with: mailwoman dev generate official-languages
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
/** Official-language spellings for one territory. */
|
|
158
|
+
export interface OfficialLanguageEntry {
|
|
159
|
+
/** CLDR \`official\` + \`de_facto_official\`, in every ISO-639 spelling. */
|
|
160
|
+
official: readonly string[]
|
|
161
|
+
/** CLDR \`official_regional\` (e.g. Catalan in ES) — opt-in for consumers. */
|
|
162
|
+
regional?: readonly string[]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** ISO 3166-1 alpha-2 → official languages. */
|
|
166
|
+
export const OFFICIAL_LANGUAGES: Record<string, OfficialLanguageEntry> = {
|
|
167
|
+
${entries}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Is \`language\` (any ISO-639 spelling: "sv", "swe", …) an official language of \`country\` (ISO
|
|
172
|
+
* 3166-1 alpha-2)? Regional-official languages count only with \`includeRegional\`.
|
|
173
|
+
*/
|
|
174
|
+
export function isOfficialLanguage(country: string, language: string, includeRegional = false): boolean {
|
|
175
|
+
const entry = OFFICIAL_LANGUAGES[country.toUpperCase()]
|
|
176
|
+
|
|
177
|
+
if (!entry) return false
|
|
178
|
+
const lang = language.toLowerCase()
|
|
179
|
+
|
|
180
|
+
return entry.official.includes(lang) || (includeRegional && (entry.regional?.includes(lang) ?? false))
|
|
181
|
+
}
|
|
182
|
+
`
|
|
183
|
+
|
|
184
|
+
writeFileSync(outPath, header)
|
|
185
|
+
report?.(`Wrote ${outPath}: ${Object.keys(table).length} territories (CLDR ${cldrVersion})`)
|
|
186
|
+
|
|
187
|
+
return { territories: Object.keys(table).length, cldrVersion, outPath }
|
|
188
|
+
}
|
package/tools/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Codex operator tools — the `run()`-style modules behind `mailwoman dev generate …` commands. No
|
|
7
|
+
* argv, no `process.exit`: commands own parsing, rendering, and exit codes (see the 2026-07-09
|
|
8
|
+
* scripts→Pastel spec).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export * from "./generate-country-reference.ts"
|
|
12
|
+
export * from "./generate-official-languages.ts"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* USPS Publication 28, Appendix C2 — Floor-class Secondary Unit Designators.
|
|
7
|
+
*
|
|
8
|
+
* The sibling of {@link ./unit-designator.ts}: where the unit table covers the full secondary-unit
|
|
9
|
+
* vocabulary (APT, STE, RM, …), this module extracts the floor-class subset — designators that
|
|
10
|
+
* name a FLOOR or LEVEL of the building rather than a specific addressable unit on that floor.
|
|
11
|
+
* USPS Pub 28 Appendix C2 identifies these designators as requiring a secondary number: "FL" (the
|
|
12
|
+
* approved abbreviation for FLOOR). The publication gives `FLOOR` as the canonical designator
|
|
13
|
+
* with approved abbreviation `FL` and variant `FLR`; `BASEMENT` (`BSMT`), `PENTHOUSE` (`PH`), and
|
|
14
|
+
* `LOBBY` (`LBBY`) are the standalone-or-numbered floor-adjacent types also listed in Appendix
|
|
15
|
+
* C2.
|
|
16
|
+
*
|
|
17
|
+
* Appendix C2 explicitly marks FLOOR, BASEMENT as requiring a secondary number (alongside APT,
|
|
18
|
+
* BLDG, etc.) while PENTHOUSE and LOBBY may stand alone. PH and LBBY are kept here (not just in
|
|
19
|
+
* {@link ./unit-designator.ts}) because the span proposer treats them as level-class hints —
|
|
20
|
+
* "LOBBY" and "PH" name a specific floor-analog, not a numbered unit, and the prior map routes
|
|
21
|
+
* `LEVEL_PHRASE` → `unit` (the schema carries no separate `level` tag).
|
|
22
|
+
*
|
|
23
|
+
* This table drives the `levelDesignators` set in the span-proposer lexicon. The full
|
|
24
|
+
* secondary-unit designators (APT, STE, RM, …) remain in {@link ./unit-designator.ts}.
|
|
25
|
+
*
|
|
26
|
+
* Data is verbatim USPS Pub 28 Appendix C2.
|
|
27
|
+
* @see {@link https://pe.usps.com/text/pub28/28apc_003.htm USPS Publication 28 — Appendix C2: Secondary Unit Designators}
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One USPS Pub 28 C2 floor-class designator row.
|
|
32
|
+
*
|
|
33
|
+
* `requiresNumber` mirrors the Appendix C2 classification: FLOOR and BASEMENT must be followed by a secondary number;
|
|
34
|
+
* PENTHOUSE and LOBBY may stand alone.
|
|
35
|
+
*/
|
|
36
|
+
export interface USFloorDesignator {
|
|
37
|
+
/** Full canonical designator (uppercase per the publication). */
|
|
38
|
+
name: string
|
|
39
|
+
/** Approved USPS abbreviation (what the post office prints on standardized mail). */
|
|
40
|
+
abbreviation: string
|
|
41
|
+
/** Additional recognized surface variants from Appendix C2. */
|
|
42
|
+
variants: readonly string[]
|
|
43
|
+
/**
|
|
44
|
+
* True when Appendix C2 marks this designator as "Requires a Secondary Number" (FLOOR, BASEMENT). False for
|
|
45
|
+
* standalone types (PENTHOUSE, LOBBY) that name a specific floor-analog without an identifier.
|
|
46
|
+
*/
|
|
47
|
+
requiresNumber: boolean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* USPS Pub 28 C2 floor-class secondary unit designators. Verbatim from the publication; see the module header for the
|
|
52
|
+
* per-row provenance. Ordered with the most-common numbered form first.
|
|
53
|
+
*/
|
|
54
|
+
export const US_FLOOR_DESIGNATORS = [
|
|
55
|
+
{ name: "FLOOR", abbreviation: "FL", variants: ["FLR"], requiresNumber: true },
|
|
56
|
+
{ name: "BASEMENT", abbreviation: "BSMT", variants: [], requiresNumber: true },
|
|
57
|
+
{ name: "PENTHOUSE", abbreviation: "PH", variants: [], requiresNumber: false },
|
|
58
|
+
{ name: "LOBBY", abbreviation: "LBBY", variants: [], requiresNumber: false },
|
|
59
|
+
] as const satisfies readonly USFloorDesignator[]
|
|
60
|
+
|
|
61
|
+
/** A canonical USPS floor-class designator name. */
|
|
62
|
+
export type USFloorDesignatorName = (typeof US_FLOOR_DESIGNATORS)[number]["name"]
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Inverse lookup: every surface form (canonical name, approved abbreviation, or Appendix C2 variant) → its canonical
|
|
66
|
+
* designator name. Lowercase-keyed for case-insensitive matching: `"fl"` → `"FLOOR"`, `"bsmt"` → `"BASEMENT"`, `"ph"` →
|
|
67
|
+
* `"PENTHOUSE"`.
|
|
68
|
+
*/
|
|
69
|
+
export const US_FLOOR_DESIGNATOR_LOOKUP: ReadonlyMap<string, USFloorDesignatorName> = (() => {
|
|
70
|
+
const out = new Map<string, USFloorDesignatorName>()
|
|
71
|
+
|
|
72
|
+
for (const row of US_FLOOR_DESIGNATORS) {
|
|
73
|
+
out.set(row.name.toLowerCase(), row.name)
|
|
74
|
+
out.set(row.abbreviation.toLowerCase(), row.name)
|
|
75
|
+
|
|
76
|
+
for (const v of row.variants) {
|
|
77
|
+
if (!out.has(v.toLowerCase())) {
|
|
78
|
+
out.set(v.toLowerCase(), row.name)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return out
|
|
84
|
+
})()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* All lowercase surface tokens for the floor-class designators — the set the span proposer populates `levelDesignators`
|
|
88
|
+
* with when wiring the US codex slice. Includes canonical names, approved abbreviations, and Appendix C2 variants.
|
|
89
|
+
*/
|
|
90
|
+
export const US_FLOOR_DESIGNATOR_TOKENS: ReadonlySet<string> = new Set(US_FLOOR_DESIGNATOR_LOOKUP.keys())
|
|
91
|
+
|
|
92
|
+
/** Approved USPS abbreviation per canonical floor designator name. */
|
|
93
|
+
export const US_FLOOR_DESIGNATOR_PREFERRED_ABBR: Readonly<Record<USFloorDesignatorName, string>> = Object.fromEntries(
|
|
94
|
+
US_FLOOR_DESIGNATORS.map((r) => [r.name, r.abbreviation])
|
|
95
|
+
) as Readonly<Record<USFloorDesignatorName, string>>
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Look up a USPS floor-class designator (by canonical name, abbreviation, or any Appendix C2 variant) and return the
|
|
99
|
+
* canonical name + approved abbreviation. Returns null if the token isn't a recognized floor-class designator.
|
|
100
|
+
*/
|
|
101
|
+
export function lookupFloorDesignator(input: string | null | undefined): {
|
|
102
|
+
designator: USFloorDesignatorName
|
|
103
|
+
abbreviation: string
|
|
104
|
+
} | null {
|
|
105
|
+
if (!input || typeof input !== "string") return null
|
|
106
|
+
const designator = US_FLOOR_DESIGNATOR_LOOKUP.get(input.trim().toLowerCase())
|
|
107
|
+
|
|
108
|
+
if (!designator) return null
|
|
109
|
+
|
|
110
|
+
return { designator, abbreviation: US_FLOOR_DESIGNATOR_PREFERRED_ABBR[designator] }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True when a token is a recognized USPS floor-class secondary unit designator (case-insensitive) — `"Floor"`, `"FL"`,
|
|
115
|
+
* `"flr"`, `"bsmt"`, `"ph"`, `"lbby"`.
|
|
116
|
+
*/
|
|
117
|
+
export function isFloorDesignatorToken(input: unknown): boolean {
|
|
118
|
+
return typeof input === "string" && US_FLOOR_DESIGNATOR_LOOKUP.has(input.trim().toLowerCase())
|
|
119
|
+
}
|
package/us/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The United States address system (USPS): street suffixes, secondary unit designators, floor-class
|
|
7
|
+
* designators (USPS Pub 28 C2 floor/level subset), military/diplomatic post office designators
|
|
8
|
+
* (USPS Pub 28 Chapter 7: APO/FPO/DPO + PSC/CMR/UNIT), ZIP codes, and the state abbreviations
|
|
9
|
+
* they hang off of.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export * from "./floor-designator.ts"
|
|
13
|
+
export * from "./military-address.ts"
|
|
14
|
+
export * from "./po-box.ts"
|
|
15
|
+
export * from "./state.ts"
|
|
16
|
+
export * from "./street-directional.ts"
|
|
17
|
+
export * from "./street-suffix.ts"
|
|
18
|
+
export * from "./unit-designator.ts"
|
|
19
|
+
export * from "./zipcode.ts"
|