@mailwoman/codex 9.3.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/jp/index.ts +1 -0
- package/lib/jp/municipality-register.ts +62 -0
- package/lib/kr/index.ts +10 -0
- package/lib/kr/sigungu-register.ts +294 -0
- 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/register-completion.ts +24 -0
- 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/index.d.ts +1 -0
- package/out/jp/index.d.ts.map +1 -1
- package/out/jp/index.js +1 -0
- package/out/jp/index.js.map +1 -1
- package/out/jp/municipality-register.d.ts +48 -0
- package/out/jp/municipality-register.d.ts.map +1 -0
- package/out/jp/municipality-register.js +41 -0
- package/out/jp/municipality-register.js.map +1 -0
- package/out/jp/prefecture.d.ts.map +1 -1
- package/out/kr/index.d.ts +10 -0
- package/out/kr/index.d.ts.map +1 -0
- package/out/kr/index.js +10 -0
- package/out/kr/index.js.map +1 -0
- package/out/kr/sigungu-register.d.ts +37 -0
- package/out/kr/sigungu-register.d.ts.map +1 -0
- package/out/kr/sigungu-register.js +283 -0
- package/out/kr/sigungu-register.js.map +1 -0
- 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/register-completion.d.ts +14 -0
- package/out/register-completion.d.ts.map +1 -0
- package/out/register-completion.js +23 -0
- package/out/register-completion.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 +120 -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,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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The one read every name register offers a decode-time repair: given the surface a model closed a span on and the
|
|
7
|
+
* text that follows, the characters that would extend the surface to a register name. A surface that already IS a
|
|
8
|
+
* name and is not the prefix of a longer one answers null.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The remainder of the first register name that begins with `surface` and continues into `following`, or null.
|
|
13
|
+
*/
|
|
14
|
+
export function completeFromRegister(names: readonly string[], surface: string, following: string): string | null {
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
if (name.length > surface.length && name.startsWith(surface)) {
|
|
17
|
+
const remainder = name.slice(surface.length)
|
|
18
|
+
|
|
19
|
+
if (following.startsWith(remainder)) return remainder
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null
|
|
24
|
+
}
|
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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The canonical match key — a normalized, deterministic string derived from address components,
|
|
7
|
+
* distinct from the human-readable formatted string.
|
|
8
|
+
*
|
|
9
|
+
* Where `format.ts` produces something for a person to read, this produces something for a
|
|
10
|
+
* _machine_ to collide on: lowercased, diacritic-stripped, punctuation-flattened, whitespace-
|
|
11
|
+
* collapsed, fields in a fixed canonical order. Two records for the same address that differ only
|
|
12
|
+
* in spelling, case, or punctuation produce the same key — which is exactly what the matcher's
|
|
13
|
+
* blocking stage wants as one cheap, high-precision candidate signal (alongside geographic
|
|
14
|
+
* proximity, which carries the real weight — see the geocode-first record-matching concept doc).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT done yet (follow-ups, all conditioned on `@mailwoman/codex`): expanding street
|
|
17
|
+
* suffixes (`Ave` → `avenue`) and directionals (`N` → `north`) to a canonical form, and
|
|
18
|
+
* USPS-style standardization. This first version is pure normalization with no dictionary expansion,
|
|
19
|
+
* so the key is stable and explainable; expansion is an additive refinement, not a rewrite.
|
|
20
|
+
*/
|
|
21
|
+
import type { ComponentDict } from "#address/format";
|
|
22
|
+
/**
|
|
23
|
+
* Options accepted by {@linkcode canonicalKey}.
|
|
24
|
+
*/
|
|
25
|
+
export interface CanonicalKeyOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Field separator in the emitted key. Default `"|"` — preserves field boundaries for blocking.
|
|
28
|
+
*/
|
|
29
|
+
separator?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Options for {@linkcode foldForKey} — the two points where the formatter's address-token fold and the record package's
|
|
33
|
+
* fragment fold legitimately differ.
|
|
34
|
+
*/
|
|
35
|
+
export interface FoldForKeyOptions {
|
|
36
|
+
/**
|
|
37
|
+
* How connective punctuation folds. `"space"` turns `&`, `+`, and `/` into word boundaries (`"A&B"` → `"a b"`);
|
|
38
|
+
* `"and"` spells `&` and `+` out as the word `and` (`"AT&T"` → `"at and t"`), leaving `/` to the punctuation
|
|
39
|
+
* catch-all (still a word boundary).
|
|
40
|
+
*/
|
|
41
|
+
ampersand: "space" | "and";
|
|
42
|
+
/**
|
|
43
|
+
* Intra-token deletion set. When true, periods join the apostrophes as intra-token noise and are deleted (`"S.A."` →
|
|
44
|
+
* `"sa"`), while a backtick falls to the punctuation catch-all. When false or omitted, backticks are deleted
|
|
45
|
+
* alongside the apostrophes and periods become word boundaries (`"S.A."` → `"s a"`).
|
|
46
|
+
*/
|
|
47
|
+
dropPeriods?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The shared fold behind every match key: NFKD-decompose and strip combining marks (so `é` → `e`), lowercase, delete
|
|
51
|
+
* intra-token punctuation, expand or flatten connective punctuation per {@linkcode FoldForKeyOptions}, space every
|
|
52
|
+
* remaining non-alphanumeric, and collapse whitespace. Deterministic — the same input and options always yield the same
|
|
53
|
+
* output.
|
|
54
|
+
*/
|
|
55
|
+
export declare function foldForKey(input: string, options: FoldForKeyOptions): string;
|
|
56
|
+
/**
|
|
57
|
+
* Normalize a single token for matching: {@linkcode foldForKey} with connective punctuation flattened to spaces (so
|
|
58
|
+
* `"A&B"` → `"a b"`, not `"ab"`). Deterministic and reversible-free — the same input always yields the same output.
|
|
59
|
+
*/
|
|
60
|
+
export declare function normalizeAddressToken(input: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Derive the canonical match key from an address component dict: each present, address-identifying field normalized via
|
|
63
|
+
* {@linkcode normalizeAddressToken}, in fixed order, joined by the separator. Empty / whitespace-only fields are
|
|
64
|
+
* skipped. Returns an empty string if nothing identifying remains.
|
|
65
|
+
*/
|
|
66
|
+
export declare function canonicalKey(components: ComponentDict, opts?: CanonicalKeyOptions): string;
|
|
67
|
+
//# sourceMappingURL=key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.d.ts","sourceRoot":"","sources":["../../lib/address/key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAyBpD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;OAIG;IACH,SAAS,EAAE,OAAO,GAAG,KAAK,CAAA;IAC1B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAqB5E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,aAAa,EAAE,IAAI,GAAE,mBAAwB,GAAG,MAAM,CAgB9F"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The canonical match key — a normalized, deterministic string derived from address components,
|
|
7
|
+
* distinct from the human-readable formatted string.
|
|
8
|
+
*
|
|
9
|
+
* Where `format.ts` produces something for a person to read, this produces something for a
|
|
10
|
+
* _machine_ to collide on: lowercased, diacritic-stripped, punctuation-flattened, whitespace-
|
|
11
|
+
* collapsed, fields in a fixed canonical order. Two records for the same address that differ only
|
|
12
|
+
* in spelling, case, or punctuation produce the same key — which is exactly what the matcher's
|
|
13
|
+
* blocking stage wants as one cheap, high-precision candidate signal (alongside geographic
|
|
14
|
+
* proximity, which carries the real weight — see the geocode-first record-matching concept doc).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT done yet (follow-ups, all conditioned on `@mailwoman/codex`): expanding street
|
|
17
|
+
* suffixes (`Ave` → `avenue`) and directionals (`N` → `north`) to a canonical form, and
|
|
18
|
+
* USPS-style standardization. This first version is pure normalization with no dictionary expansion,
|
|
19
|
+
* so the key is stable and explainable; expansion is an additive refinement, not a rewrite.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The address-identifying components, in canonical key order. Venue / attention are intentionally excluded — those
|
|
23
|
+
* carry organization identity, which the record layer keys separately.
|
|
24
|
+
*/
|
|
25
|
+
const KEY_FIELD_ORDER = [
|
|
26
|
+
"po_box",
|
|
27
|
+
"house_number",
|
|
28
|
+
"street_prefix",
|
|
29
|
+
"street_prefix_particle",
|
|
30
|
+
"street",
|
|
31
|
+
"street_suffix",
|
|
32
|
+
"intersection_a",
|
|
33
|
+
"intersection_b",
|
|
34
|
+
"unit",
|
|
35
|
+
"dependent_locality",
|
|
36
|
+
"locality",
|
|
37
|
+
"subregion",
|
|
38
|
+
"region",
|
|
39
|
+
"postcode",
|
|
40
|
+
"country",
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* The shared fold behind every match key: NFKD-decompose and strip combining marks (so `é` → `e`), lowercase, delete
|
|
44
|
+
* intra-token punctuation, expand or flatten connective punctuation per {@linkcode FoldForKeyOptions}, space every
|
|
45
|
+
* remaining non-alphanumeric, and collapse whitespace. Deterministic — the same input and options always yield the same
|
|
46
|
+
* output.
|
|
47
|
+
*/
|
|
48
|
+
export function foldForKey(input, options) {
|
|
49
|
+
const folded = input
|
|
50
|
+
.normalize("NFKD")
|
|
51
|
+
// strip combining marks (U+0300–U+036F) left by NFKD decomposition, so "é" → "e"
|
|
52
|
+
.replaceAll(/[\u0300-\u036F]/g, "")
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
// apostrophes are intra-word (possessives, "O'Brien") — delete so the token stays whole
|
|
55
|
+
.replaceAll(options.dropPeriods ? /[.'’]/g : /['’`]/g, "");
|
|
56
|
+
const connected = options.ampersand === "and"
|
|
57
|
+
? folded.replaceAll("&", " and ").replaceAll("+", " and ")
|
|
58
|
+
: folded.replaceAll(/[&+/]/g, " ");
|
|
59
|
+
return (connected
|
|
60
|
+
// everything else non-alphanumeric (keep spaces) is noise
|
|
61
|
+
.replaceAll(/[^a-z0-9\s]/g, " ")
|
|
62
|
+
.replaceAll(/\s+/g, " ")
|
|
63
|
+
.trim());
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Normalize a single token for matching: {@linkcode foldForKey} with connective punctuation flattened to spaces (so
|
|
67
|
+
* `"A&B"` → `"a b"`, not `"ab"`). Deterministic and reversible-free — the same input always yields the same output.
|
|
68
|
+
*/
|
|
69
|
+
export function normalizeAddressToken(input) {
|
|
70
|
+
return foldForKey(input, { ampersand: "space" });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Derive the canonical match key from an address component dict: each present, address-identifying field normalized via
|
|
74
|
+
* {@linkcode normalizeAddressToken}, in fixed order, joined by the separator. Empty / whitespace-only fields are
|
|
75
|
+
* skipped. Returns an empty string if nothing identifying remains.
|
|
76
|
+
*/
|
|
77
|
+
export function canonicalKey(components, opts = {}) {
|
|
78
|
+
const separator = opts.separator ?? "|";
|
|
79
|
+
const parts = [];
|
|
80
|
+
for (const tag of KEY_FIELD_ORDER) {
|
|
81
|
+
const value = components[tag];
|
|
82
|
+
if (!value)
|
|
83
|
+
continue;
|
|
84
|
+
const normalized = normalizeAddressToken(value);
|
|
85
|
+
if (normalized) {
|
|
86
|
+
parts.push(normalized);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return parts.join(separator);
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../../lib/address/key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAKH;;;GAGG;AACH,MAAM,eAAe,GAAG;IACvB,QAAQ;IACR,cAAc;IACd,eAAe;IACf,wBAAwB;IACxB,QAAQ;IACR,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,MAAM;IACN,oBAAoB;IACpB,UAAU;IACV,WAAW;IACX,QAAQ;IACR,UAAU;IACV,SAAS;CACkC,CAAA;AA+B5C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,OAA0B;IACnE,MAAM,MAAM,GAAG,KAAK;SAClB,SAAS,CAAC,MAAM,CAAC;QAClB,iFAAiF;SAChF,UAAU,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAClC,WAAW,EAAE;QACd,wFAAwF;SACvF,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;IAE3D,MAAM,SAAS,GACd,OAAO,CAAC,SAAS,KAAK,KAAK;QAC1B,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC;QAC1D,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;IAEpC,OAAO,CACN,SAAS;QACR,0DAA0D;SACzD,UAAU,CAAC,cAAc,EAAE,GAAG,CAAC;SAC/B,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC;SACvB,IAAI,EAAE,CACR,CAAA;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa;IAClD,OAAO,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAA;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,UAAyB,EAAE,IAAI,GAAwB,EAAE;IACrF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,CAAA;IACvC,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;QAE7B,IAAI,CAAC,KAAK;YAAE,SAAQ;QACpB,MAAM,UAAU,GAAG,qBAAqB,CAAC,KAAK,CAAC,CAAA;QAE/C,IAAI,UAAU,EAAE,CAAC;YAChB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACvB,CAAC;IACF,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;AAC7B,CAAC"}
|