@mailwoman/codex 9.4.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -75
- package/lib/address/format.ts +144 -0
- package/lib/address/key.ts +134 -0
- package/lib/address/layout.ts +189 -0
- package/lib/address/layouts/generated.ts +1576 -0
- package/lib/address/layouts/index.ts +213 -0
- package/lib/address/render.ts +176 -0
- package/lib/{address-system-conventions.ts → address/system-conventions.ts} +6 -5
- package/lib/component.ts +96 -0
- package/lib/country/index.ts +3 -2
- package/lib/country/{reference.ts → reference/index.ts} +1 -1
- package/lib/country/region-languages.ts +46 -0
- package/lib/es/co-official-languages.ts +62 -0
- package/lib/es/index.ts +1 -0
- package/lib/gb/index.ts +2 -2
- package/lib/index.ts +4 -2
- package/lib/placetype-map.ts +157 -0
- package/lib/postcode/shapes.json +80 -0
- package/lib/postcode/shapes.ts +56 -0
- package/lib/{postcode-systems.ts → postcode/systems.ts} +8 -1
- package/lib/us/index.ts +2 -2
- package/lib/us/po-box.ts +37 -10
- package/lib/us/{street-directional.ts → street/directional.ts} +1 -1
- package/lib/us/{street-suffix.ts → street/suffix.ts} +1 -1
- package/out/address/format.d.ts +87 -0
- package/out/address/format.d.ts.map +1 -0
- package/out/address/format.js +85 -0
- package/out/address/format.js.map +1 -0
- package/out/address/key.d.ts +67 -0
- package/out/address/key.d.ts.map +1 -0
- package/out/address/key.js +91 -0
- package/out/address/key.js.map +1 -0
- package/out/address/layout.d.ts +104 -0
- package/out/address/layout.d.ts.map +1 -0
- package/out/address/layout.js +123 -0
- package/out/address/layout.js.map +1 -0
- package/out/address/layouts/generated.d.ts +21 -0
- package/out/address/layouts/generated.d.ts.map +1 -0
- package/out/address/layouts/generated.js +1380 -0
- package/out/address/layouts/generated.js.map +1 -0
- package/out/address/layouts/index.d.ts +81 -0
- package/out/address/layouts/index.d.ts.map +1 -0
- package/out/address/layouts/index.js +184 -0
- package/out/address/layouts/index.js.map +1 -0
- package/out/address/render.d.ts +64 -0
- package/out/address/render.d.ts.map +1 -0
- package/out/address/render.js +115 -0
- package/out/address/render.js.map +1 -0
- package/out/{address-system-conventions.d.ts → address/system-conventions.d.ts} +6 -5
- package/out/address/system-conventions.d.ts.map +1 -0
- package/out/{address-system-conventions.js → address/system-conventions.js} +2 -2
- package/out/address/system-conventions.js.map +1 -0
- package/out/au/delivery-service.d.ts.map +1 -1
- package/out/au/level-designator.d.ts.map +1 -1
- package/out/au/state.d.ts.map +1 -1
- package/out/ca/province.d.ts.map +1 -1
- package/out/component.d.ts +50 -0
- package/out/component.d.ts.map +1 -0
- package/out/component.js +84 -0
- package/out/component.js.map +1 -0
- package/out/country/codes.d.ts +2 -2
- package/out/country/codes.d.ts.map +1 -1
- package/out/country/display-names.d.ts.map +1 -1
- package/out/country/display-names.js.map +1 -1
- package/out/country/index.d.ts +3 -2
- package/out/country/index.d.ts.map +1 -1
- package/out/country/index.js +3 -2
- package/out/country/index.js.map +1 -1
- package/out/country/{reference-data.d.ts → reference/data.d.ts} +1 -1
- package/out/country/reference/data.d.ts.map +1 -0
- package/out/country/{reference-data.js → reference/data.js} +1 -1
- package/out/country/reference/data.js.map +1 -0
- package/out/country/{reference.d.ts → reference/index.d.ts} +1 -1
- package/out/country/reference/index.d.ts.map +1 -0
- package/out/country/{reference.js → reference/index.js} +2 -2
- package/out/country/reference/index.js.map +1 -0
- package/out/country/region-languages.d.ts +25 -0
- package/out/country/region-languages.d.ts.map +1 -0
- package/out/country/region-languages.js +41 -0
- package/out/country/region-languages.js.map +1 -0
- package/out/de/bundesland.d.ts.map +1 -1
- package/out/de/postleitzahl.d.ts.map +1 -1
- package/out/de/street-type.d.ts.map +1 -1
- package/out/es/co-official-languages.d.ts +37 -0
- package/out/es/co-official-languages.d.ts.map +1 -0
- package/out/es/co-official-languages.js +61 -0
- package/out/es/co-official-languages.js.map +1 -0
- package/out/es/index.d.ts +1 -0
- package/out/es/index.d.ts.map +1 -1
- package/out/es/index.js +1 -0
- package/out/es/index.js.map +1 -1
- package/out/fr/departement.d.ts.map +1 -1
- package/out/fr/region.d.ts.map +1 -1
- package/out/gb/country.d.ts.map +1 -1
- package/out/gb/index.d.ts +2 -2
- package/out/gb/index.d.ts.map +1 -1
- package/out/gb/index.js +2 -2
- package/out/gb/index.js.map +1 -1
- package/out/gb/{postcode-area.d.ts → postcode/area.d.ts} +1 -1
- package/out/gb/postcode/area.d.ts.map +1 -0
- package/out/gb/{postcode-area.js → postcode/area.js} +1 -1
- package/out/gb/postcode/area.js.map +1 -0
- package/out/gb/{postcode.d.ts → postcode/index.d.ts} +1 -1
- package/out/gb/postcode/index.d.ts.map +1 -0
- package/out/gb/{postcode.js → postcode/index.js} +1 -1
- package/out/gb/postcode/index.js.map +1 -0
- package/out/gb/street-type.d.ts.map +1 -1
- package/out/index.d.ts +4 -2
- package/out/index.d.ts.map +1 -1
- package/out/index.js +4 -2
- package/out/index.js.map +1 -1
- package/out/jp/address-unit.d.ts.map +1 -1
- package/out/jp/prefecture.d.ts.map +1 -1
- package/out/level-semantics.d.ts.map +1 -1
- package/out/level-semantics.js.map +1 -1
- package/out/nz/delivery-service.d.ts.map +1 -1
- package/out/placetype-map.d.ts +83 -0
- package/out/placetype-map.d.ts.map +1 -0
- package/out/placetype-map.js +130 -0
- package/out/placetype-map.js.map +1 -0
- package/out/postcode/shapes.d.ts +47 -0
- package/out/postcode/shapes.d.ts.map +1 -0
- package/out/postcode/shapes.js +38 -0
- package/out/postcode/shapes.js.map +1 -0
- package/out/postcode/shapes.json +80 -0
- package/out/{postcode-systems.d.ts → postcode/systems.d.ts} +8 -1
- package/out/postcode/systems.d.ts.map +1 -0
- package/out/{postcode-systems.js → postcode/systems.js} +9 -2
- package/out/postcode/systems.js.map +1 -0
- package/out/us/floor-designator.d.ts.map +1 -1
- package/out/us/index.d.ts +2 -2
- package/out/us/index.js +2 -2
- package/out/us/military-address.d.ts.map +1 -1
- package/out/us/po-box.d.ts +6 -0
- package/out/us/po-box.d.ts.map +1 -1
- package/out/us/po-box.js +35 -10
- package/out/us/po-box.js.map +1 -1
- package/out/us/state.d.ts.map +1 -1
- package/out/us/{street-directional.d.ts → street/directional.d.ts} +1 -1
- package/out/us/street/directional.d.ts.map +1 -0
- package/out/us/{street-directional.js → street/directional.js} +2 -2
- package/out/us/street/directional.js.map +1 -0
- package/out/us/{street-suffix.d.ts → street/suffix.d.ts} +1 -1
- package/out/us/street/suffix.d.ts.map +1 -0
- package/out/us/{street-suffix.js → street/suffix.js} +2 -2
- package/out/us/street/suffix.js.map +1 -0
- package/out/us/zipcode.d.ts +1 -1
- package/out/us/zipcode.d.ts.map +1 -1
- package/package.json +110 -30
- package/lib/tools/build-country-surface-lexicon.ts +0 -177
- package/lib/tools/export-country-surfaces.ts +0 -48
- package/lib/tools/generate-country-population.ts +0 -142
- package/lib/tools/generate-country-reference.ts +0 -168
- package/lib/tools/generate-official-languages.ts +0 -210
- package/lib/tools/index.ts +0 -12
- package/out/address-system-conventions.d.ts.map +0 -1
- package/out/address-system-conventions.js.map +0 -1
- package/out/country/reference-data.d.ts.map +0 -1
- package/out/country/reference-data.js.map +0 -1
- package/out/country/reference.d.ts.map +0 -1
- package/out/country/reference.js.map +0 -1
- package/out/gb/postcode-area.d.ts.map +0 -1
- package/out/gb/postcode-area.js.map +0 -1
- package/out/gb/postcode.d.ts.map +0 -1
- package/out/gb/postcode.js.map +0 -1
- package/out/postcode-systems.d.ts.map +0 -1
- package/out/postcode-systems.js.map +0 -1
- package/out/tools/build-country-surface-lexicon.d.ts +0 -43
- package/out/tools/build-country-surface-lexicon.d.ts.map +0 -1
- package/out/tools/build-country-surface-lexicon.js +0 -149
- package/out/tools/build-country-surface-lexicon.js.map +0 -1
- package/out/tools/export-country-surfaces.d.ts +0 -15
- package/out/tools/export-country-surfaces.d.ts.map +0 -1
- package/out/tools/export-country-surfaces.js +0 -34
- package/out/tools/export-country-surfaces.js.map +0 -1
- package/out/tools/generate-country-population.d.ts +0 -42
- package/out/tools/generate-country-population.d.ts.map +0 -1
- package/out/tools/generate-country-population.js +0 -106
- package/out/tools/generate-country-population.js.map +0 -1
- package/out/tools/generate-country-reference.d.ts +0 -36
- package/out/tools/generate-country-reference.d.ts.map +0 -1
- package/out/tools/generate-country-reference.js +0 -107
- package/out/tools/generate-country-reference.js.map +0 -1
- package/out/tools/generate-official-languages.d.ts +0 -50
- package/out/tools/generate-official-languages.d.ts.map +0 -1
- package/out/tools/generate-official-languages.js +0 -149
- package/out/tools/generate-official-languages.js.map +0 -1
- package/out/tools/index.d.ts +0 -12
- package/out/tools/index.d.ts.map +0 -1
- package/out/tools/index.js +0 -12
- package/out/tools/index.js.map +0 -1
- package/out/us/street-directional.d.ts.map +0 -1
- package/out/us/street-directional.js.map +0 -1
- package/out/us/street-suffix.d.ts.map +0 -1
- package/out/us/street-suffix.js.map +0 -1
- /package/lib/country/{reference-data.ts → reference/data.ts} +0 -0
- /package/lib/gb/{postcode-area.ts → postcode/area.ts} +0 -0
- /package/lib/gb/{postcode.ts → postcode/index.ts} +0 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Component-tag → resolver-placetype mapping, the equivalence groups a query expands into, and the test
|
|
7
|
+
* that tells a widened match from an exact one.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ComponentTag } from "#component"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mapping from mailwoman's address-component tags to the resolver's placetype taxonomy.
|
|
14
|
+
*
|
|
15
|
+
* PARTIAL on purpose: a tag absent from the map is NOT queried, and the resolver pass leaves its classifier attribution
|
|
16
|
+
* untouched. Omission is therefore a routing decision, not an oversight.
|
|
17
|
+
*/
|
|
18
|
+
export type PlacetypeMap = Partial<Record<ComponentTag, string>>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The map used when a backend does not supply its own.
|
|
22
|
+
*
|
|
23
|
+
* `street` and `house_number` are absent because WOF admin has no rows for them — they resolve through the situs
|
|
24
|
+
* extracts instead, which are keyed by street, not by placetype.
|
|
25
|
+
*
|
|
26
|
+
* The JP tiers are present because the candidate gazetteer keys them: 91.3% of Japanese records carry a kanji or kana
|
|
27
|
+
* key (49,255 of 53,920), a prefecture is a WOF `region`, a municipality a `locality` (its filter group admits the
|
|
28
|
+
* `borough` wards and `localadmin`), and a district (大字 / 町名) sits in the `locality` band beside the neighbourhoods.
|
|
29
|
+
* Measured on 300 JP board rows: with these entries and the JP rungs on the admin ladder, 271 resolve within 15 km;
|
|
30
|
+
* without them, 0. Only the character-path CJK model emits the tags, so no Latin parse reaches these rows.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_PLACETYPE_MAP: PlacetypeMap = {
|
|
33
|
+
country: "country",
|
|
34
|
+
region: "region",
|
|
35
|
+
locality: "locality",
|
|
36
|
+
dependent_locality: "locality",
|
|
37
|
+
subregion: "county",
|
|
38
|
+
prefecture: "region",
|
|
39
|
+
municipality: "locality",
|
|
40
|
+
district: "locality",
|
|
41
|
+
// `postcode` (mailwoman tag) maps to WOF's `postalcode` placetype. Resolves only when the
|
|
42
|
+
// backend has the postcode extract available — `WOFSQLitePlaceLookup` auto-routes `postalcode`
|
|
43
|
+
// queries to a `postalcode_us` (or similarly-named) extract, falling back to main if absent.
|
|
44
|
+
postcode: "postalcode",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where a country's WOF typing of a tier departs from {@link DEFAULT_PLACETYPE_MAP}, the entries that differ. The map
|
|
49
|
+
* names the tier by tag; WOF names it by placetype, and the two agree for most countries and not for all.
|
|
50
|
+
*
|
|
51
|
+
* - **TW** — 鄉鎮市區, the tier below the 縣市 that the parse tags `subregion`, is `locality` or `localadmin` in WOF for 164 of
|
|
52
|
+
* the 178 held-out districts the candidate table carries and `county` for 14 (the census in
|
|
53
|
+
* `docs/records/evals/2026-09-08-v8-cjk-regs.md` §5). Under the default `county` band `臺北市中正區` reached nothing and
|
|
54
|
+
* the admin tier fell to the 縣市. The `locality` group admits `localadmin` too, the rule the JP `municipality` tag
|
|
55
|
+
* already relies on, and the exact-type preference in ranking keeps a `locality` row ahead of a same-name
|
|
56
|
+
* `localadmin` row.
|
|
57
|
+
*/
|
|
58
|
+
const COUNTRY_PLACETYPE_OVERRIDES: Readonly<Record<string, PlacetypeMap>> = {
|
|
59
|
+
tw: { subregion: "locality" },
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The placetype map for a resolve scoped to `countryCode` (ISO alpha-2, any case): the default map with the country's
|
|
64
|
+
* overrides applied, or the default map itself when the country has none or is unknown. The identity of the default
|
|
65
|
+
* object is preserved in that case, so a caller comparing maps to decide whether a second resolve is needed reads
|
|
66
|
+
* "unchanged" correctly.
|
|
67
|
+
*/
|
|
68
|
+
export function placetypeMapForCountry(countryCode: string | null | undefined): PlacetypeMap {
|
|
69
|
+
const overrides = countryCode ? COUNTRY_PLACETYPE_OVERRIDES[countryCode.toLowerCase()] : undefined
|
|
70
|
+
|
|
71
|
+
return overrides ? { ...DEFAULT_PLACETYPE_MAP, ...overrides } : DEFAULT_PLACETYPE_MAP
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Placetype-equivalence groups for lookup FILTERING. WOF splits a single addressing tier across several placetypes, but
|
|
76
|
+
* an address's span can name ANY of them. A backend that filters to the one "obvious" placetype makes the equivalents
|
|
77
|
+
* unreachable, so a fuzzy same-name place in the wrong tier wins instead.
|
|
78
|
+
*
|
|
79
|
+
* Three tiers are affected (the value of each entry is the set the SQL filter should accept; the FIRST entry is the
|
|
80
|
+
* canonical/requested type, which extract routing keys off):
|
|
81
|
+
*
|
|
82
|
+
* - **`locality`** — `locality` (most cities), `borough` (Brooklyn, the Paris arrondissements, the London boroughs), and
|
|
83
|
+
* `localadmin` (FR communes, US towns/townships in New England). Without the group, Brooklyn-the-borough (pop 2.5M)
|
|
84
|
+
* was unreachable and the fuzzy "Brooklyn Park, MN" won.
|
|
85
|
+
* - **`region`** — `region` + `macroregion` (#718). WOF does NOT model every country's top-level civil division as
|
|
86
|
+
* `region`: Italian regions (Lombardia, Veneto, Toscana…) are `macroregion` (their PROVINCES are `region`), and the
|
|
87
|
+
* post-2016 French régions (Île-de-France) are `macroregion` too. An address's `region` span names exactly those, so
|
|
88
|
+
* a `region`-only filter resolved them to NOTHING (confirmed against the IT/FR eval rows). US states / DE
|
|
89
|
+
* Bundesländer / ES provincias are genuine `region`, so the EXACT-type match is preferred in ranking (see the
|
|
90
|
+
* resolve.ts fallback-quality annotation) — the macro is the recall safety net, not a demotion.
|
|
91
|
+
* - **`county`** — `county` + `macrocounty` (#718). The `subregion` ComponentTag maps to `county` via
|
|
92
|
+
* {@link DEFAULT_PLACETYPE_MAP}; WOF carries `macrocounty` for FR départements-grouping / DE / GB tiers above the
|
|
93
|
+
* county. Proactive (no eval row exercises `subregion` today) but symmetric with `region` — biasing to inclusion,
|
|
94
|
+
* since a missed resolution costs more than a too-broad candidate (which is QA-visible). Same exact-type preference
|
|
95
|
+
* applies.
|
|
96
|
+
*
|
|
97
|
+
* This table is the single source of truth for that expansion, shared by every lookup backend
|
|
98
|
+
* (`@mailwoman/core/resolver-wof-sqlite`, `@mailwoman/core/resolver-wof-wasm`, and the demo's httpvfs lookup) so the
|
|
99
|
+
* Node and browser resolvers can't drift. Keyed by the REQUESTED placetype. Placetypes without an entry pass through
|
|
100
|
+
* unchanged — an explicit `placetype: "borough"` query stays narrow.
|
|
101
|
+
*/
|
|
102
|
+
export const PLACETYPE_FILTER_GROUPS: Readonly<Record<string, readonly string[]>> = {
|
|
103
|
+
locality: ["locality", "borough", "localadmin"],
|
|
104
|
+
region: ["region", "macroregion"],
|
|
105
|
+
county: ["county", "macrocounty"],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Expand a placetype filter through {@link PLACETYPE_FILTER_GROUPS}, deduplicated and order-preserving (the first entry
|
|
110
|
+
* stays first — extract routing keys off it). `null`/`undefined` (no filter) passes through untouched.
|
|
111
|
+
*/
|
|
112
|
+
export function expandPlacetypeFilter(placetypes: null): null
|
|
113
|
+
export function expandPlacetypeFilter(placetypes: readonly string[]): string[]
|
|
114
|
+
export function expandPlacetypeFilter(placetypes: readonly string[] | null): string[] | null
|
|
115
|
+
|
|
116
|
+
export function expandPlacetypeFilter(placetypes: readonly string[] | null): string[] | null {
|
|
117
|
+
if (!placetypes) return null
|
|
118
|
+
const out: string[] = []
|
|
119
|
+
|
|
120
|
+
for (const placetype of placetypes) {
|
|
121
|
+
for (const expanded of PLACETYPE_FILTER_GROUPS[placetype] ?? [placetype]) {
|
|
122
|
+
if (!out.includes(expanded)) {
|
|
123
|
+
out.push(expanded)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Macro/broader-tier members of {@link PLACETYPE_FILTER_GROUPS} — the recall safety net a query may fall through to
|
|
133
|
+
* when no candidate of the EXACT requested placetype exists (#718). DELIBERATELY scoped to the `macro*` tiers only: the
|
|
134
|
+
* `locality` group's `borough`/`localadmin` are genuine peers (Brooklyn-the-borough is a first-class locality answer,
|
|
135
|
+
* #404-class), NOT fallbacks — so they must NOT be deprioritized or annotated. Only `macroregion`/`macrocounty` are a
|
|
136
|
+
* broader admin tier standing in for a true `region`/`county`.
|
|
137
|
+
*/
|
|
138
|
+
const MACRO_FALLBACK_PLACETYPES: ReadonlySet<string> = new Set(["macroregion", "macrocounty"])
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Did `candidatePlacetype` resolve `requestedPlacetype` only via a BROADER admin tier (a macro-type fallback within the
|
|
142
|
+
* {@link PLACETYPE_FILTER_GROUPS} expansion), rather than the exact type (#718)?
|
|
143
|
+
*
|
|
144
|
+
* `region` → `region` is exact (false); `region` → `macroregion` is a fallback (true). Scoped to the `macro*` tiers
|
|
145
|
+
* (see {@link MACRO_FALLBACK_PLACETYPES}) so the `locality` group's borough/localadmin peers stay exact. The resolver
|
|
146
|
+
* uses this to (a) prefer an exact-type candidate in ranking and (b) annotate `resolutionQuality: "fallback"` when only
|
|
147
|
+
* a macro-type matched. A placetype outside the requested group, or any non-macro member, is treated as exact (false).
|
|
148
|
+
*/
|
|
149
|
+
export function isPlacetypeFallback(requestedPlacetype: string, candidatePlacetype: string): boolean {
|
|
150
|
+
const group = PLACETYPE_FILTER_GROUPS[requestedPlacetype]
|
|
151
|
+
|
|
152
|
+
if (!group) return false
|
|
153
|
+
|
|
154
|
+
if (candidatePlacetype === requestedPlacetype) return false
|
|
155
|
+
|
|
156
|
+
return MACRO_FALLBACK_PLACETYPES.has(candidatePlacetype) && group.includes(candidatePlacetype)
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "postcode-shapes-v1",
|
|
3
|
+
"note": "Postcode SHAPE patterns for detecting postcode-like spans in free text, ordered most-specific to least. Priority IS the array index: a lower index wins an overlap. This is a SHAPE test, not gazetteer membership — see postcode-systems.ts for the per-system membership question. Alphanumeric patterns require UPPERCASE letters, which keeps them off lowercase prose. The regex bodies are written in the subset both JavaScript RegExp and Python re accept, because two runtimes read this file; a row that needs different source text in the two dialects needs a second field and a stated reason, not a loosened comparison.",
|
|
4
|
+
"shapes": [
|
|
5
|
+
{
|
|
6
|
+
"label": "JP-marked",
|
|
7
|
+
"kind": "designated",
|
|
8
|
+
"pattern": "(?<=〒\\s?)\\d{3}-\\d{4}\\b",
|
|
9
|
+
"unicode": true,
|
|
10
|
+
"javascriptOnly": "Python's `re` refuses a variable-width lookbehind, and `\\s?` makes this one variable-width. Nothing on the Python side compiles this row.",
|
|
11
|
+
"note": "The digits behind 〒, optionally spaced (〒506-0025, 〒 100-0001). The character path keeps the mark in the text, and on a venue-led line the model has read the digits as a house number. A designated match may overwrite any existing label."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"label": "GB",
|
|
15
|
+
"kind": "alnum",
|
|
16
|
+
"pattern": "\\b[A-Z]{1,2}\\d[A-Z\\d]?\\s+\\d[A-Z]{2}\\b",
|
|
17
|
+
"note": "Outward + space + inward: SW1A 1AA, EH8 9YL, W1J 9PN, IP13 6SU, B12 8QX."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"label": "CA",
|
|
21
|
+
"kind": "alnum",
|
|
22
|
+
"pattern": "\\b[A-Z]\\d[A-Z]\\s?\\d[A-Z]\\d\\b",
|
|
23
|
+
"note": "A1A 1A1, space optional: M5V 2T6, H2X 2T6, H3B 1A3."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"label": "IE",
|
|
27
|
+
"kind": "alnum",
|
|
28
|
+
"pattern": "\\b(?:[A-Z]\\d{2}|D6W)\\s+[A-Z\\d]{4}\\b",
|
|
29
|
+
"note": "Eircode: routing key (letter + 2 digits, or the D6W special) + a 4-alnum unique part — D02 AF30, T12 X70A, F91 Y5CY. Space REQUIRED: a glued 7-alnum blob is too forgeable for the ADD path. No GB collision, because a letter+2-digit GB outward always has a 3-character inward (B12 8QX), never 4."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"label": "DE",
|
|
33
|
+
"kind": "alnum",
|
|
34
|
+
"pattern": "\\bD-\\d{5}\\b",
|
|
35
|
+
"note": "The D-prefixed form: D-68161."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"label": "NL",
|
|
39
|
+
"kind": "alnum",
|
|
40
|
+
"pattern": "\\b\\d{4}\\s?[A-Z]{2}\\b",
|
|
41
|
+
"note": "1234 AB / 1234AB — space optional, glued is common. The US \"2737 CA\" collision (a ZIP+4 tail followed by a state) is settled by longest-match-wins, which lets the ZIP+4 claim it."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"label": "ZIP4",
|
|
45
|
+
"kind": "numeric",
|
|
46
|
+
"pattern": "\\b\\d{5}-\\d{4}\\b",
|
|
47
|
+
"note": "US ZIP+4. A numeric match is eligible only for snapping, never for adding a postcode where the model emitted none."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"label": "BR",
|
|
51
|
+
"kind": "numeric",
|
|
52
|
+
"pattern": "\\b\\d{5}-\\d{3}\\b",
|
|
53
|
+
"note": "CEP: 70390-100, 95090-020. Without it the generic NUM5 below matched the five-digit head and the trailing-smear clip discarded the sector suffix, so the repair pass truncated a parse the model had got right (repair OFF \"95090-020\", repair ON \"95090\"). The trailing word boundary keeps it off a ZIP+4's first nine characters, since \"94610-2737\" has a digit after \"273\"."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"label": "JP",
|
|
57
|
+
"kind": "numeric",
|
|
58
|
+
"pattern": "\\b\\d{3}-\\d{4}\\b",
|
|
59
|
+
"note": "100-0001."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"label": "PT",
|
|
63
|
+
"kind": "numeric",
|
|
64
|
+
"pattern": "\\b\\d{4}-\\d{3}\\b",
|
|
65
|
+
"note": "3060-187."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"label": "PL",
|
|
69
|
+
"kind": "numeric",
|
|
70
|
+
"pattern": "\\b\\d{2}-\\d{3}\\b",
|
|
71
|
+
"note": "47-400."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"label": "NUM5",
|
|
75
|
+
"kind": "numeric",
|
|
76
|
+
"pattern": "\\b\\d{5}\\b",
|
|
77
|
+
"note": "The bare 5-digit shape US, FR, DE and ES all share. Last, so every more-specific row above claims its overlap first."
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Postcode SHAPE patterns — which substrings of a line LOOK like a postcode, ordered most-specific
|
|
7
|
+
* to least. Priority IS the index: a lower index wins an overlap.
|
|
8
|
+
*
|
|
9
|
+
* The DATA lives in `./shapes.json` so non-TS consumers read the identical record, the
|
|
10
|
+
* same arrangement `us/street-suffix.json` has. Two runtimes need this table and both used to
|
|
11
|
+
* carry their own typed copy: `@mailwoman/neural`'s postcode repair, and the Python trainer's
|
|
12
|
+
* `features/postcode_shapes.py`, which paints the train-side anchor on the spans inference paints.
|
|
13
|
+
* Hand-mirrored, they drifted twice — the IE Eircode row was TS-only for a month, the BR CEP row
|
|
14
|
+
* for five weeks — and each time the trainer painted one fewer shape than inference, silently.
|
|
15
|
+
*
|
|
16
|
+
* This is a SHAPE test, not a gazetteer-membership test. A bare `68161` matches the US, German,
|
|
17
|
+
* French, Spanish and Italian 5-digit shapes; `./systems.ts` answers the membership
|
|
18
|
+
* question, and neither module reads the other.
|
|
19
|
+
*
|
|
20
|
+
* REGEX DIALECT. The bodies are written in the subset both JavaScript `RegExp` and Python `re`
|
|
21
|
+
* accept, which is what lets one file serve both. A row needing different source text in the two
|
|
22
|
+
* dialects needs a second field and a stated reason, not a loosened comparison on either side.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import postcodeShapeData from "./shapes.json" with { type: "json" }
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What a match is eligible to do. `designated` may overwrite any existing label, `alnum` may add a postcode where the
|
|
29
|
+
* model emitted none, and `numeric` may only snap an existing one — so a numeric shape can never invent a postcode over
|
|
30
|
+
* a hyphenated house number.
|
|
31
|
+
*/
|
|
32
|
+
export type PostcodeShapeKind = "alnum" | "numeric" | "designated"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One shape: the label it reports, what it is eligible to do, and the compiled pattern.
|
|
36
|
+
*/
|
|
37
|
+
export interface PostcodeShape {
|
|
38
|
+
readonly label: string
|
|
39
|
+
readonly kind: PostcodeShapeKind
|
|
40
|
+
readonly re: RegExp
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Every postcode shape, in priority order. Compiled once at module load; each `RegExp` carries the `g` flag because
|
|
45
|
+
* callers scan a whole line, and `u` as well where the row declares it.
|
|
46
|
+
*/
|
|
47
|
+
export const POSTCODE_SHAPES: readonly PostcodeShape[] = postcodeShapeData.shapes.map((shape) => ({
|
|
48
|
+
label: shape.label,
|
|
49
|
+
kind: shape.kind as PostcodeShapeKind,
|
|
50
|
+
re: new RegExp(shape.pattern, "unicode" in shape && shape.unicode ? "gu" : "g"),
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The record's own version string, so a consumer that vendors a copy can say which revision it holds.
|
|
55
|
+
*/
|
|
56
|
+
export const POSTCODE_SHAPES_VERSION: string = postcodeShapeData.version
|
|
@@ -186,8 +186,15 @@ export function isUnitGradePostcodeHit(parsed: string, resolverName: string | un
|
|
|
186
186
|
* **The JP row is the JP board through the served path** (`jp-served-resolve.run.ts`, 2,000 rows, seed 42): the 586
|
|
187
187
|
* rows carrying a postcode the candidate table keys, graded on the row's own entrance point — the locality-first answer
|
|
188
188
|
* is the municipality centroid, the postcode answer the code's 町域 centroid from the WOF extract.
|
|
189
|
+
*
|
|
190
|
+
* **SG** is the limiting case of the tier: a six-digit Singapore postcode names ONE building, so the code's point IS
|
|
191
|
+
* the address, and the only locality above it is the city-state itself. Measured on a 300-row seeded draw of the
|
|
192
|
+
* Overture-SG register (`postalcode-sg-overture.db` folded, each row geocoded as `<number> <street> Singapore
|
|
193
|
+
* <postcode>` and graded on its own point): the postcode point answers 300 of 300 within 1 km (p99 0.16 km);
|
|
194
|
+
* locality-first answers 185, and the other 115 take the "Singapore" locality centroid (p75 6.30 km, p90 11.43 km, p99
|
|
195
|
+
* 24.18 km). The postcode point is closer on those 115 and equal on the rest; it is never farther.
|
|
189
196
|
*/
|
|
190
|
-
export const AREA_POSTCODE_FINER_THAN_LOCALITY: ReadonlySet<string> = new Set(["DE", "JP"])
|
|
197
|
+
export const AREA_POSTCODE_FINER_THAN_LOCALITY: ReadonlySet<string> = new Set(["DE", "JP", "SG"])
|
|
191
198
|
|
|
192
199
|
/**
|
|
193
200
|
* True when this country's area-grade postal code outranks its locality. Absent or unknown country → false, so the
|
package/lib/us/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ export * from "#us/floor-designator"
|
|
|
13
13
|
export * from "#us/military-address"
|
|
14
14
|
export * from "#us/po-box"
|
|
15
15
|
export * from "#us/state"
|
|
16
|
-
export * from "#us/street
|
|
17
|
-
export * from "#us/street
|
|
16
|
+
export * from "#us/street/directional"
|
|
17
|
+
export * from "#us/street/suffix"
|
|
18
18
|
export * from "#us/unit-designator"
|
|
19
19
|
export * from "#us/zipcode"
|
package/lib/us/po-box.ts
CHANGED
|
@@ -31,11 +31,37 @@ export const US_PO_BOX_DESIGNATORS = [
|
|
|
31
31
|
export type USPoBoxDesignator = (typeof US_PO_BOX_DESIGNATORS)[number]
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Recognition patterns for {@link US_PO_BOX_DESIGNATORS}. Keeping the surface grammar next to the canonical designator
|
|
35
|
+
* prevents the exported lexicon and matcher from drifting apart.
|
|
36
36
|
*/
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const PO_BOX_DESIGNATOR_PATTERNS: ReadonlyArray<readonly [USPoBoxDesignator, string]> = [
|
|
38
|
+
["POST OFFICE BOX", String.raw`post\s+office\s+box`],
|
|
39
|
+
["PO BOX", String.raw`p\.?\s*o\.?\s*box`],
|
|
40
|
+
["P O BOX", String.raw`p\.?\s*o\.?\s*box`],
|
|
41
|
+
["FIRM CALLER", String.raw`firm\s+caller`],
|
|
42
|
+
["CALLER", "caller"],
|
|
43
|
+
["DRAWER", "drawer"],
|
|
44
|
+
["LOCKBOX", "lockbox"],
|
|
45
|
+
["BOX", "box"],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One standalone matcher per USPS designator. The id is alphanumeric with optional dashes (USPS caller/firm ids exist).
|
|
50
|
+
*/
|
|
51
|
+
const PO_BOX_MATCHERS = PO_BOX_DESIGNATOR_PATTERNS.map(([designator, pattern]) => ({
|
|
52
|
+
designator,
|
|
53
|
+
designatorRe: new RegExp(String.raw`^\s*${pattern}\s*$`, "i"),
|
|
54
|
+
phraseRe: new RegExp(String.raw`^\s*(${pattern})\s*#?\s*([\dA-Za-z][\dA-Za-z-]*)\s*$`, "i"),
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* True when `input` is a USPS PO-box designator without its box identifier. This is useful to consumers that compose a
|
|
59
|
+
* phrase and need to distinguish USPS vocabulary from their own locale-specific aliases (for example, the corpus's
|
|
60
|
+
* `POB` training variant).
|
|
61
|
+
*/
|
|
62
|
+
export function isUSPoBoxDesignator(input: unknown): input is string {
|
|
63
|
+
return typeof input === "string" && PO_BOX_MATCHERS.some(({ designatorRe }) => designatorRe.test(input))
|
|
64
|
+
}
|
|
39
65
|
|
|
40
66
|
/**
|
|
41
67
|
* Type-predicate: does the input look like a standalone PO Box address? Case-insensitive and tolerant of "P.O. Box",
|
|
@@ -43,7 +69,7 @@ const PO_BOX_RE =
|
|
|
43
69
|
* all-caps "PO BOX 123".)
|
|
44
70
|
*/
|
|
45
71
|
export function isPOBox(input: unknown): boolean {
|
|
46
|
-
return
|
|
72
|
+
return matchPOBox(input) !== null
|
|
47
73
|
}
|
|
48
74
|
|
|
49
75
|
/**
|
|
@@ -67,13 +93,14 @@ export interface PoBoxMatch {
|
|
|
67
93
|
*/
|
|
68
94
|
export function matchPOBox(input: unknown): PoBoxMatch | null {
|
|
69
95
|
if (typeof input !== "string") return null
|
|
70
|
-
const m = PO_BOX_RE.exec(input)
|
|
71
96
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
for (const { phraseRe } of PO_BOX_MATCHERS) {
|
|
98
|
+
const match = phraseRe.exec(input)
|
|
99
|
+
|
|
100
|
+
if (match) return { matched: match[1]!.trim(), id: match[2]! }
|
|
101
|
+
}
|
|
75
102
|
|
|
76
|
-
return
|
|
103
|
+
return null
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
/**
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @see {@link https://pe.usps.com/text/pub28/28apc_002.htm USPS Pub 28 Appendix C1}
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { matchCase } from "#us/street
|
|
13
|
+
import { matchCase } from "#us/street/suffix"
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* The 8 directional abbreviations accepted by the USPS. The USPS prefers the abbreviation over the fully-spelled-out
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* @see {@link https://pe.usps.com/text/pub28/28apc_002.htm USPS Street Suffix Abbreviations}
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import streetSuffixData from "
|
|
27
|
+
import streetSuffixData from "../street-suffix.json" with { type: "json" }
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Canonical USPS street suffix → list of recognized variants. The first variant in each list is the preferred USPS
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Render a `ComponentTag`-keyed dict into a country-localized string — the inverse of the parser.
|
|
7
|
+
*
|
|
8
|
+
* The order lives in `@mailwoman/codex/address-layouts`, as data. This module is the public surface over
|
|
9
|
+
* {@linkcode renderAddress}: {@linkcode formatAddress} is the join, and {@linkcode formatAddressRow} is the join plus
|
|
10
|
+
* the tags the layout printed, which is what every corpus adapter actually wants.
|
|
11
|
+
*
|
|
12
|
+
* IT USED TO WRAP A THIRD-PARTY ENGINE, and 229 of this file's 438 lines existed to work around templates written
|
|
13
|
+
* against OpenStreetMap's tag vocabulary rather than this project's: a pass that parsed 295 mustache templates at
|
|
14
|
+
* module load to discover which of them could render a sub-locality, a second that spliced a missing line back in
|
|
15
|
+
* afterwards, a third that removed a connector the template wrote between two slots when one was empty, and a
|
|
16
|
+
* translation layer between the two vocabularies. Owning the layouts deletes all four — a layout that declares a
|
|
17
|
+
* `dependent_locality` slot needs no interrogation about whether it has one, and a line assembled from present values
|
|
18
|
+
* never writes a connector around an absent one.
|
|
19
|
+
*/
|
|
20
|
+
import { type ComponentDict } from "#address/render";
|
|
21
|
+
import type { ComponentTag } from "#component";
|
|
22
|
+
export type { ComponentDict } from "#address/render";
|
|
23
|
+
/**
|
|
24
|
+
* Options accepted by {@linkcode formatAddress} and {@linkcode formatAddressRow}.
|
|
25
|
+
*/
|
|
26
|
+
export interface FormatAddressOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Replace the layout's line breaks with this separator. Default `"\n"`: the envelope form.
|
|
29
|
+
*/
|
|
30
|
+
separator?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Join the lines the way the COUNTRY does, for the single-line form a query or a corpus row takes — `", "` for most,
|
|
33
|
+
* `" "` for Japan and Korea, and nothing at all for the Chinese-script systems, whose admin run is unseparated.
|
|
34
|
+
*
|
|
35
|
+
* It is an option rather than each caller's literal because the literal is wrong outside the anglophone systems:
|
|
36
|
+
* joining Japan's lines with a comma gives `1-9-1, 丸の内, 千代田区, 東京都 100-0005`, which is the romanized convention
|
|
37
|
+
* printed backwards. `separator` wins when both are given.
|
|
38
|
+
*/
|
|
39
|
+
singleLine?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Render a component dict into an idiomatic per-country address string.
|
|
43
|
+
*
|
|
44
|
+
* Returns an empty string when the dict is empty, and when no layout names `country` — 55 of the 252 shipped country
|
|
45
|
+
* records carry no usable skeleton, and answering nothing for one of those reports absence rather than inventing an
|
|
46
|
+
* order. Throws nothing; a partial dict degrades to the parts the layout can print.
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatAddress(components: ComponentDict, country: string, opts?: FormatAddressOptions): string;
|
|
49
|
+
/**
|
|
50
|
+
* A rendered address and the components that survived the render.
|
|
51
|
+
*/
|
|
52
|
+
export interface AddressRow {
|
|
53
|
+
/**
|
|
54
|
+
* The rendered string.
|
|
55
|
+
*/
|
|
56
|
+
readonly raw: string;
|
|
57
|
+
/**
|
|
58
|
+
* The subset of the input dict the layout PRINTED, with the caller's original values. This is the half a corpus row
|
|
59
|
+
* needs: a label whose text is not in `raw` cannot be aligned against it.
|
|
60
|
+
*/
|
|
61
|
+
readonly components: ComponentDict;
|
|
62
|
+
/**
|
|
63
|
+
* Tags the dict carried a value for that the layout has no slot for, NAMED rather than silently dropped. France
|
|
64
|
+
* absorbing a region into its postcode line is the common case.
|
|
65
|
+
*/
|
|
66
|
+
readonly unplaced: readonly ComponentTag[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render `components` for `country` and report what the layout printed, in one pass.
|
|
70
|
+
*
|
|
71
|
+
* Returns null when nothing rendered — an empty dict, a country with no layout, or a dict whose every value falls in a
|
|
72
|
+
* slot this country omits. Every corpus adapter asked both questions and paid for two renders to get them, then
|
|
73
|
+
* recovered the alignment by searching the output string for each value; that search cannot tell a component the layout
|
|
74
|
+
* dropped from one whose value happens to sit inside another — `Paris` inside `Rue de Paris`. The render knows, so the
|
|
75
|
+
* answer is read rather than inferred.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatAddressRow(components: ComponentDict, country: string, opts?: FormatAddressOptions): AddressRow | null;
|
|
78
|
+
/**
|
|
79
|
+
* Which of `components` occur verbatim in `raw`, case- and whitespace-insensitively.
|
|
80
|
+
*
|
|
81
|
+
* This is a question about a string somebody else built — a committed golden fixture, a source's own address line — and
|
|
82
|
+
* it is the WEAKER of the two reconciliations: a substring test cannot tell a component the renderer dropped from one
|
|
83
|
+
* whose value happens to sit inside another. Anything rendered through a layout should read
|
|
84
|
+
* {@linkcode formatAddressRow}'s `components` instead, which the render knows rather than infers.
|
|
85
|
+
*/
|
|
86
|
+
export declare function componentsPresentIn(components: ComponentDict, raw: string): ComponentDict;
|
|
87
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../lib/address/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAgC,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACpB;AAQD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,MAAM,CAEjH;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAA;IAClC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC/B,UAAU,EAAE,aAAa,EACzB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,oBAAyB,GAC7B,UAAU,GAAG,IAAI,CAwBnB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAazF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Render a `ComponentTag`-keyed dict into a country-localized string — the inverse of the parser.
|
|
7
|
+
*
|
|
8
|
+
* The order lives in `@mailwoman/codex/address-layouts`, as data. This module is the public surface over
|
|
9
|
+
* {@linkcode renderAddress}: {@linkcode formatAddress} is the join, and {@linkcode formatAddressRow} is the join plus
|
|
10
|
+
* the tags the layout printed, which is what every corpus adapter actually wants.
|
|
11
|
+
*
|
|
12
|
+
* IT USED TO WRAP A THIRD-PARTY ENGINE, and 229 of this file's 438 lines existed to work around templates written
|
|
13
|
+
* against OpenStreetMap's tag vocabulary rather than this project's: a pass that parsed 295 mustache templates at
|
|
14
|
+
* module load to discover which of them could render a sub-locality, a second that spliced a missing line back in
|
|
15
|
+
* afterwards, a third that removed a connector the template wrote between two slots when one was empty, and a
|
|
16
|
+
* translation layer between the two vocabularies. Owning the layouts deletes all four — a layout that declares a
|
|
17
|
+
* `dependent_locality` slot needs no interrogation about whether it has one, and a line assembled from present values
|
|
18
|
+
* never writes a connector around an absent one.
|
|
19
|
+
*/
|
|
20
|
+
import { layoutForCountry, lineJoinForCountry } from "#address/layouts/index";
|
|
21
|
+
import { joinRendering, renderAddress } from "#address/render";
|
|
22
|
+
function separatorFor(country, opts) {
|
|
23
|
+
if (opts.separator !== undefined)
|
|
24
|
+
return opts.separator;
|
|
25
|
+
return opts.singleLine ? lineJoinForCountry(country) : "\n";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Render a component dict into an idiomatic per-country address string.
|
|
29
|
+
*
|
|
30
|
+
* Returns an empty string when the dict is empty, and when no layout names `country` — 55 of the 252 shipped country
|
|
31
|
+
* records carry no usable skeleton, and answering nothing for one of those reports absence rather than inventing an
|
|
32
|
+
* order. Throws nothing; a partial dict degrades to the parts the layout can print.
|
|
33
|
+
*/
|
|
34
|
+
export function formatAddress(components, country, opts = {}) {
|
|
35
|
+
return formatAddressRow(components, country, opts)?.raw ?? "";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render `components` for `country` and report what the layout printed, in one pass.
|
|
39
|
+
*
|
|
40
|
+
* Returns null when nothing rendered — an empty dict, a country with no layout, or a dict whose every value falls in a
|
|
41
|
+
* slot this country omits. Every corpus adapter asked both questions and paid for two renders to get them, then
|
|
42
|
+
* recovered the alignment by searching the output string for each value; that search cannot tell a component the layout
|
|
43
|
+
* dropped from one whose value happens to sit inside another — `Paris` inside `Rue de Paris`. The render knows, so the
|
|
44
|
+
* answer is read rather than inferred.
|
|
45
|
+
*/
|
|
46
|
+
export function formatAddressRow(components, country, opts = {}) {
|
|
47
|
+
const layout = layoutForCountry(country);
|
|
48
|
+
if (!layout)
|
|
49
|
+
return null;
|
|
50
|
+
const rendering = renderAddress(layout, components);
|
|
51
|
+
if (!rendering.placed.length)
|
|
52
|
+
return null;
|
|
53
|
+
const raw = joinRendering(rendering, separatorFor(country, opts));
|
|
54
|
+
if (!raw)
|
|
55
|
+
return null;
|
|
56
|
+
const placed = {};
|
|
57
|
+
for (const tag of rendering.placed) {
|
|
58
|
+
const value = components[tag];
|
|
59
|
+
if (value) {
|
|
60
|
+
placed[tag] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { raw, components: placed, unplaced: rendering.unplaced };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Which of `components` occur verbatim in `raw`, case- and whitespace-insensitively.
|
|
67
|
+
*
|
|
68
|
+
* This is a question about a string somebody else built — a committed golden fixture, a source's own address line — and
|
|
69
|
+
* it is the WEAKER of the two reconciliations: a substring test cannot tell a component the renderer dropped from one
|
|
70
|
+
* whose value happens to sit inside another. Anything rendered through a layout should read
|
|
71
|
+
* {@linkcode formatAddressRow}'s `components` instead, which the render knows rather than infers.
|
|
72
|
+
*/
|
|
73
|
+
export function componentsPresentIn(components, raw) {
|
|
74
|
+
const haystack = raw.toLowerCase().replaceAll(/\s+/g, " ");
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [tag, value] of Object.entries(components)) {
|
|
77
|
+
if (!value)
|
|
78
|
+
continue;
|
|
79
|
+
if (haystack.includes(value.toLowerCase().replaceAll(/\s+/g, " "))) {
|
|
80
|
+
out[tag] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../lib/address/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,aAAa,EAAsB,MAAM,iBAAiB,CAAA;AAyBlF,SAAS,YAAY,CAAC,OAAe,EAAE,IAA0B;IAChE,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,SAAS,CAAA;IAEvD,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,UAAyB,EAAE,OAAe,EAAE,IAAI,GAAyB,EAAE;IACxG,OAAO,gBAAgB,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAA;AAC9D,CAAC;AAsBD;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC/B,UAAyB,EACzB,OAAe,EACf,IAAI,GAAyB,EAAE;IAE/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAExC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAEnD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAEzC,MAAM,GAAG,GAAG,aAAa,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAA;IAEjE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IAErB,MAAM,MAAM,GAAkB,EAAE,CAAA;IAEhC,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;QAE7B,IAAI,KAAK,EAAE,CAAC;YACX,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QACpB,CAAC;IACF,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,CAAA;AACjE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAyB,EAAE,GAAW;IACzE,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC1D,MAAM,GAAG,GAAkB,EAAE,CAAA;IAE7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC,KAAK;YAAE,SAAQ;QAEpB,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YACpE,GAAG,CAAC,GAAmB,CAAC,GAAG,KAAK,CAAA;QACjC,CAAC;IACF,CAAC;IAED,OAAO,GAAG,CAAA;AACX,CAAC"}
|