@mailwoman/resolver 4.13.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/out/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/resolver` — the address resolver implementation, lifted out of `@mailwoman/core`
7
+ * (#215) so it can depend on `@mailwoman/spatial` (haversine) + `@mailwoman/codex` (USPS
8
+ * directionals) instead of reinventing them. The TYPE contract stays in
9
+ * `@mailwoman/core/resolver` (so the `core/pipeline` composes the resolver structurally without a
10
+ * package cycle); this barrel re-exports it, so `@mailwoman/resolver` is a complete drop-in for
11
+ * what used to be `@mailwoman/core/resolver`.
12
+ */
13
+ export { RemoteResolver, serializableResolveOpts } from "./remote-resolver.js";
14
+ export type { RemoteResolverOpts, ResolveTreeRequest, ResolveTreeResponse, SerializableResolveOpts, } from "./remote-resolver.js";
15
+ export { createWofResolver } from "./resolve.js";
16
+ export { findRescoreCandidate, hasResolvedPlace } from "./span-rescore.js";
17
+ export type { RescoreCandidate, SpanRescoreOptions } from "./span-rescore.js";
18
+ export * from "@mailwoman/core/resolver";
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,YAAY,EACX,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,GACvB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAC1E,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAI7E,cAAc,0BAA0B,CAAA"}
package/out/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/resolver` — the address resolver implementation, lifted out of `@mailwoman/core`
7
+ * (#215) so it can depend on `@mailwoman/spatial` (haversine) + `@mailwoman/codex` (USPS
8
+ * directionals) instead of reinventing them. The TYPE contract stays in
9
+ * `@mailwoman/core/resolver` (so the `core/pipeline` composes the resolver structurally without a
10
+ * package cycle); this barrel re-exports it, so `@mailwoman/resolver` is a complete drop-in for
11
+ * what used to be `@mailwoman/core/resolver`.
12
+ */
13
+ export { RemoteResolver, serializableResolveOpts } from "./remote-resolver.js";
14
+ export { createWofResolver } from "./resolve.js";
15
+ export { findRescoreCandidate, hasResolvedPlace } from "./span-rescore.js";
16
+ // The type contract + placetype helpers live in core (pure types, keep core a leaf). Re-export so
17
+ // consumers get the whole surface from `@mailwoman/resolver`.
18
+ export * from "@mailwoman/core/resolver";
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAO9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAG1E,kGAAkG;AAClG,8DAA8D;AAC9D,cAAc,0BAA0B,CAAA"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `RemoteResolver` — the `Resolver` interface (`resolveTree`) over HTTP. The adapter the interface
7
+ * docstring anticipated (Phase 4.4): a client POSTs a parsed `AddressTree` + the serializable
8
+ * `ResolveOpts` to a resolver service, which owns the gazetteer + situs/interpolation shards,
9
+ * runs the cascade, and returns the resolved tree. Two payoffs:
10
+ *
11
+ * 1. **Multi-instance** — stateless parser nodes (the ~30 MB ONNX model) talk to ONE resolver service
12
+ * (the multi-GB gazetteer + shards). `parse` locally, `new RemoteResolver(...).resolveTree`
13
+ * remotely — same interface the in-process `WofResolver` satisfies, so it's a drop-in.
14
+ * 2. **Canary** — point it at a second resolver build (or an adapter fronting Pelias/Nominatim/BAN)
15
+ * and diff the resolved trees through the identical contract.
16
+ *
17
+ * Pure transport: `fetch` only, no node-specific deps (runs in the browser too). The
18
+ * `addressPoints` / `interpolation` opts are LIVE SQLite handles — not serializable — so they're
19
+ * stripped before the POST; the resolver service supplies its own from the tree's region (the
20
+ * data lives server-side, which is the whole point). All other opts (defaultCountry, calibration,
21
+ * hierarchyCompletion, …) ride along.
22
+ */
23
+ import type { AddressTree } from "@mailwoman/core/decoder";
24
+ import type { ResolveOpts, Resolver } from "@mailwoman/core/resolver";
25
+ /** `ResolveOpts` minus the non-serializable live lookup handles. What actually crosses the wire. */
26
+ export type SerializableResolveOpts = Omit<ResolveOpts, "addressPoints" | "interpolation">;
27
+ /** Strip the live lookup handles from `ResolveOpts` so the rest can be JSON-serialized over HTTP. */
28
+ export declare function serializableResolveOpts(opts?: ResolveOpts): SerializableResolveOpts | undefined;
29
+ export interface RemoteResolverOpts {
30
+ /**
31
+ * Full URL of the resolver service's resolve-tree endpoint, e.g.
32
+ * `http://resolver:7081/api/resolve-tree`.
33
+ */
34
+ endpoint: string;
35
+ /** Injectable fetch (tests / custom agents). Defaults to the global `fetch`. */
36
+ fetch?: typeof fetch;
37
+ /** Per-request timeout in ms. Default 10000. */
38
+ timeoutMs?: number;
39
+ /** Extra headers (auth, tracing). `Content-Type: application/json` is always set. */
40
+ headers?: Record<string, string>;
41
+ }
42
+ /** The wire request body `POST <endpoint>` expects (and the matching server handler parses). */
43
+ export interface ResolveTreeRequest {
44
+ tree: AddressTree;
45
+ opts?: SerializableResolveOpts;
46
+ }
47
+ /** The wire response body the server returns. */
48
+ export interface ResolveTreeResponse {
49
+ tree: AddressTree;
50
+ }
51
+ export declare class RemoteResolver implements Resolver {
52
+ #private;
53
+ constructor(opts: RemoteResolverOpts);
54
+ resolveTree(tree: AddressTree, opts?: ResolveOpts): Promise<AddressTree>;
55
+ }
56
+ //# sourceMappingURL=remote-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-resolver.d.ts","sourceRoot":"","sources":["../remote-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAErE,oGAAoG;AACpG,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,eAAe,CAAC,CAAA;AAE1F,qGAAqG;AACrG,wBAAgB,uBAAuB,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,uBAAuB,GAAG,SAAS,CAI/F;AAED,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,gFAAgF;IAChF,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;IACpB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,gGAAgG;AAChG,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,CAAC,EAAE,uBAAuB,CAAA;CAC9B;AAED,iDAAiD;AACjD,MAAM,WAAW,mBAAmB;IACnC,IAAI,EAAE,WAAW,CAAA;CACjB;AAED,qBAAa,cAAe,YAAW,QAAQ;;gBAMlC,IAAI,EAAE,kBAAkB;IAQ9B,WAAW,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;CAuB9E"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `RemoteResolver` — the `Resolver` interface (`resolveTree`) over HTTP. The adapter the interface
7
+ * docstring anticipated (Phase 4.4): a client POSTs a parsed `AddressTree` + the serializable
8
+ * `ResolveOpts` to a resolver service, which owns the gazetteer + situs/interpolation shards,
9
+ * runs the cascade, and returns the resolved tree. Two payoffs:
10
+ *
11
+ * 1. **Multi-instance** — stateless parser nodes (the ~30 MB ONNX model) talk to ONE resolver service
12
+ * (the multi-GB gazetteer + shards). `parse` locally, `new RemoteResolver(...).resolveTree`
13
+ * remotely — same interface the in-process `WofResolver` satisfies, so it's a drop-in.
14
+ * 2. **Canary** — point it at a second resolver build (or an adapter fronting Pelias/Nominatim/BAN)
15
+ * and diff the resolved trees through the identical contract.
16
+ *
17
+ * Pure transport: `fetch` only, no node-specific deps (runs in the browser too). The
18
+ * `addressPoints` / `interpolation` opts are LIVE SQLite handles — not serializable — so they're
19
+ * stripped before the POST; the resolver service supplies its own from the tree's region (the
20
+ * data lives server-side, which is the whole point). All other opts (defaultCountry, calibration,
21
+ * hierarchyCompletion, …) ride along.
22
+ */
23
+ /** Strip the live lookup handles from `ResolveOpts` so the rest can be JSON-serialized over HTTP. */
24
+ export function serializableResolveOpts(opts) {
25
+ if (!opts)
26
+ return undefined;
27
+ const { addressPoints: _ap, interpolation: _ip, ...rest } = opts;
28
+ return rest;
29
+ }
30
+ export class RemoteResolver {
31
+ #endpoint;
32
+ #fetch;
33
+ #timeoutMs;
34
+ #headers;
35
+ constructor(opts) {
36
+ if (!opts.endpoint)
37
+ throw new Error("RemoteResolver: `endpoint` is required");
38
+ this.#endpoint = opts.endpoint;
39
+ this.#fetch = opts.fetch ?? globalThis.fetch;
40
+ this.#timeoutMs = opts.timeoutMs ?? 10_000;
41
+ this.#headers = opts.headers ?? {};
42
+ }
43
+ async resolveTree(tree, opts) {
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
46
+ try {
47
+ const body = { tree, opts: serializableResolveOpts(opts) };
48
+ const res = await this.#fetch(this.#endpoint, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json", ...this.#headers },
51
+ body: JSON.stringify(body),
52
+ signal: controller.signal,
53
+ });
54
+ if (!res.ok) {
55
+ throw new Error(`RemoteResolver: ${this.#endpoint} → HTTP ${res.status} ${res.statusText}`);
56
+ }
57
+ const json = (await res.json());
58
+ if (!json || !json.tree || !Array.isArray(json.tree.roots)) {
59
+ throw new Error("RemoteResolver: malformed response (missing `tree.roots`)");
60
+ }
61
+ return json.tree;
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=remote-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-resolver.js","sourceRoot":"","sources":["../remote-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,qGAAqG;AACrG,MAAM,UAAU,uBAAuB,CAAC,IAAkB;IACzD,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAA;IAC3B,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAChE,OAAO,IAAI,CAAA;AACZ,CAAC;AA2BD,MAAM,OAAO,cAAc;IACjB,SAAS,CAAQ;IACjB,MAAM,CAAc;IACpB,UAAU,CAAQ;IAClB,QAAQ,CAAwB;IAEzC,YAAY,IAAwB;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAA;QAC7E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC9B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAA;QAC5C,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,IAAI,MAAM,CAAA;QAC1C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,IAAiB,EAAE,IAAkB;QACtD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAA;QACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACnE,IAAI,CAAC;YACJ,MAAM,IAAI,GAAuB,EAAE,IAAI,EAAE,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC,EAAE,CAAA;YAC9E,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE;gBAC7C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE;gBACjE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAC1B,MAAM,EAAE,UAAU,CAAC,MAAM;aACzB,CAAC,CAAA;YACF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACb,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,SAAS,WAAW,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;YAC5F,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAiC,CAAA;YAC/D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5D,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAA;YAC7E,CAAC;YACD,OAAO,IAAI,CAAC,IAAI,CAAA;QACjB,CAAC;gBAAS,CAAC;YACV,YAAY,CAAC,KAAK,CAAC,CAAA;QACpB,CAAC;IACF,CAAC;CACD"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `resolveTree` — walk an `AddressTree` top-down and decorate matched nodes with resolver- supplied
7
+ * attribution + coordinates.
8
+ *
9
+ * The walk is parent-constraint-aware: when a parent node resolves to a place id, its children's
10
+ * lookups are scoped to descendants of that parent. This dramatically narrows the search space
11
+ * for ambiguous names — `Springfield` under a resolved `Illinois` parent resolves to the IL one,
12
+ * not the MA one.
13
+ */
14
+ import { type Resolver, type ResolverBackend } from "@mailwoman/core/resolver";
15
+ /**
16
+ * Build a `Resolver` backed by a `ResolverBackend`. The backend can be any concrete impl
17
+ * structurally compatible with `PlaceLookup` — e.g. `new WofSqlitePlaceLookup({ databasePath
18
+ * }).asResolverBackend()` or a fake for tests.
19
+ */
20
+ export declare function createWofResolver(backend: ResolverBackend): Resolver;
21
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,EASN,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,MAAM,0BAA0B,CAAA;AAIjC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,QAAQ,CAEpE"}
package/out/resolve.js ADDED
@@ -0,0 +1,570 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `resolveTree` — walk an `AddressTree` top-down and decorate matched nodes with resolver- supplied
7
+ * attribution + coordinates.
8
+ *
9
+ * The walk is parent-constraint-aware: when a parent node resolves to a place id, its children's
10
+ * lookups are scoped to descendants of that parent. This dramatically narrows the search space
11
+ * for ambiguous names — `Springfield` under a resolved `Illinois` parent resolves to the IL one,
12
+ * not the MA one.
13
+ */
14
+ import { isStreetDirectionalToken } from "@mailwoman/codex/us";
15
+ import { DEFAULT_PLACETYPE_MAP, isPlacetypeFallback, } from "@mailwoman/core/resolver";
16
+ import { haversineKm } from "@mailwoman/spatial";
17
+ import { findRescoreCandidate, hasResolvedPlace } from "./span-rescore.js";
18
+ /**
19
+ * Build a `Resolver` backed by a `ResolverBackend`. The backend can be any concrete impl
20
+ * structurally compatible with `PlaceLookup` — e.g. `new WofSqlitePlaceLookup({ databasePath
21
+ * }).asResolverBackend()` or a fake for tests.
22
+ */
23
+ export function createWofResolver(backend) {
24
+ return new WofResolver(backend);
25
+ }
26
+ /**
27
+ * Pick the completion locality when an admin maps to several coincident same-name candidates
28
+ * (#405). Population is the PRIMARY signal — the principal city is the populous one, and it can sit
29
+ * FARTHER from the admin centroid than a tiny same-name hamlet (the Niigata case from #403).
30
+ * Nearest centroid breaks a population tie; a genuine tie (same population AND distance) ABSTAINS
31
+ * rather than guess.
32
+ */
33
+ function pickCompletion(candidates) {
34
+ if (candidates.length === 0)
35
+ return null;
36
+ if (candidates.length === 1)
37
+ return candidates[0];
38
+ const ranked = [...candidates].sort((a, b) => b.population - a.population || a.distanceKm - b.distanceKm);
39
+ const [first, second] = ranked;
40
+ if (first.population === second.population && first.distanceKm === second.distanceKm)
41
+ return null;
42
+ return first;
43
+ }
44
+ /**
45
+ * Find the first postcode value anywhere in the tree (a one-shot pre-scan; postcode and locality
46
+ * are siblings, so the top-down walk wouldn't otherwise let the locality lookup see it).
47
+ */
48
+ function firstPostcodeValue(roots) {
49
+ const stack = [...roots];
50
+ while (stack.length > 0) {
51
+ const n = stack.pop();
52
+ if (n.tag === "postcode" && n.value.trim().length > 0)
53
+ return n.value.trim();
54
+ stack.push(...n.children);
55
+ }
56
+ return undefined;
57
+ }
58
+ /** Street-name component tags that, with the street node itself, reconstruct the full street string. */
59
+ const STREET_NAME_TAGS = new Set(["street", "street_prefix", "street_prefix_particle", "street_suffix"]);
60
+ /**
61
+ * Reassemble the full street string from the street node's subtree (#483 coverage fix). The parser
62
+ * nests the directional/suffix as `street_prefix`/`street_suffix` CHILDREN of `street`
63
+ * (containment.ts), so `street.value` alone is the bare base name ("Sheldon" for "East Sheldon Rd")
64
+ * — which misses the coordinate shards keyed on the FULL normalized name. Collect street + its
65
+ * prefix/particle/suffix descendants (NOT house_number/unit, which also nest under street), order
66
+ * by span offset, and join.
67
+ */
68
+ function assembleStreetValue(streetNode, directionalUnit) {
69
+ const parts = [];
70
+ const stack = [streetNode];
71
+ while (stack.length > 0) {
72
+ const n = stack.pop();
73
+ if (STREET_NAME_TAGS.has(n.tag) && n.value.trim())
74
+ parts.push(n);
75
+ stack.push(...n.children);
76
+ }
77
+ // #718 admin-tail: a directional quadrant the model mis-tagged `unit` ("1532 Taylor Street NE" →
78
+ // [unit] "NE") folds back into the street key by span order, so the situs/interp lookup matches the
79
+ // shard's "taylor street northeast" (the lookup normalizer expands the abbreviation). Lookup-key
80
+ // only — the parse output and admin resolution are untouched. Byte-stable when absent (undefined).
81
+ if (directionalUnit && directionalUnit.value.trim())
82
+ parts.push(directionalUnit);
83
+ parts.sort((a, b) => a.start - b.start);
84
+ return parts.map((n) => n.value.trim()).join(" ");
85
+ }
86
+ /**
87
+ * Directional quadrant values the model sometimes emits as a `unit` node instead of inside the
88
+ * street subtree (#718 admin-tail diagnostic: ~19% of the admin-fallback tail, 83% of DC). Folded
89
+ * into the street lookup key by {@link assembleStreetValue}; the situs/interp lookup normalizer
90
+ * expands the abbreviation ("ne" → "northeast") so the shard's full street name matches.
91
+ */
92
+ // The 8 USPS cardinals/intercardinals (abbrev or name) — @codex/us owns the canonical table (#215).
93
+ const isDirectionalUnit = (value) => isStreetDirectionalToken(value.replace(/\./g, ""));
94
+ /**
95
+ * Address-point tier (#476): find `street` + `house_number` in the tree (first occurrence,
96
+ * depth-first), scope by the tree's postcode/locality values, and on an exact hit stamp the point
97
+ * onto the STREET node's metadata. Additive only — admin resolution is never altered.
98
+ */
99
+ function applyAddressPoint(roots, lookup) {
100
+ let street;
101
+ let houseNumber;
102
+ let directionalUnit;
103
+ let locality;
104
+ let postcode;
105
+ const stack = [...roots];
106
+ while (stack.length > 0) {
107
+ const n = stack.pop();
108
+ if (n.tag === "street" && !street)
109
+ street = n;
110
+ if (n.tag === "house_number" && !houseNumber)
111
+ houseNumber = n;
112
+ if (n.tag === "unit" && !directionalUnit && isDirectionalUnit(n.value))
113
+ directionalUnit = n;
114
+ if (n.tag === "locality" && !locality && n.value.trim())
115
+ locality = n.value.trim();
116
+ if (n.tag === "postcode" && !postcode && n.value.trim())
117
+ postcode = n.value.trim();
118
+ stack.push(...n.children);
119
+ }
120
+ if (!street || !houseNumber)
121
+ return;
122
+ const hit = lookup.find({
123
+ street: assembleStreetValue(street, directionalUnit),
124
+ number: houseNumber.value,
125
+ postcode,
126
+ locality,
127
+ });
128
+ if (!hit)
129
+ return;
130
+ street.metadata = {
131
+ ...street.metadata,
132
+ address_point: { lat: hit.lat, lon: hit.lon, source: hit.source, release: hit.release },
133
+ resolution_tier: "address_point",
134
+ };
135
+ }
136
+ /**
137
+ * House-number interpolation tier (#483): the third rung, consulted ONLY when the exact
138
+ * address-point tier ({@link applyAddressPoint}) did NOT already stamp the street node
139
+ * (`resolution_tier === "address_point"`). That gate IS the "after the exact-point fall-through" —
140
+ * an estimate never overwrites a real situs point. Postcode-scoped (no locality — the interpolators
141
+ * abstain statewide without a postcode). Stamps a DISTINCT metadata key (`interpolated_point`,
142
+ * never `address_point`). Additive only — admin resolution is untouched.
143
+ */
144
+ function applyInterpolation(roots, lookup, radiusCalibration) {
145
+ let street;
146
+ let houseNumber;
147
+ let directionalUnit;
148
+ let postcode;
149
+ const stack = [...roots];
150
+ while (stack.length > 0) {
151
+ const n = stack.pop();
152
+ if (n.tag === "street" && !street)
153
+ street = n;
154
+ if (n.tag === "house_number" && !houseNumber)
155
+ houseNumber = n;
156
+ if (n.tag === "unit" && !directionalUnit && isDirectionalUnit(n.value))
157
+ directionalUnit = n;
158
+ if (n.tag === "postcode" && !postcode && n.value.trim())
159
+ postcode = n.value.trim();
160
+ stack.push(...n.children);
161
+ }
162
+ if (!street || !houseNumber)
163
+ return;
164
+ // The fall-through gate: an exact situs point already won — never override it with an estimate.
165
+ if (street.metadata?.["resolution_tier"] === "address_point")
166
+ return;
167
+ const hit = lookup.find({ street: assembleStreetValue(street, directionalUnit), number: houseNumber.value, postcode });
168
+ if (!hit)
169
+ return;
170
+ // Conformal-calibrated radius when the caller supplies a multiplier (#374): the raw half-segment
171
+ // heuristic underestimates the true spread (~72% coverage on Travis); ×1.70 → a 90% bound. Default
172
+ // (no multiplier) keeps the raw value, byte-stable. Preserve the raw radius for transparency.
173
+ const calibrated = radiusCalibration ? Math.round(hit.uncertaintyM * radiusCalibration) : hit.uncertaintyM;
174
+ street.metadata = {
175
+ ...street.metadata,
176
+ interpolated_point: { lat: hit.lat, lon: hit.lon, source: hit.source, release: hit.release },
177
+ resolution_tier: "interpolated",
178
+ uncertainty_m: calibrated,
179
+ ...(radiusCalibration ? { uncertainty_raw_m: hit.uncertaintyM, uncertainty_calibration: radiusCalibration } : {}),
180
+ interpolation_method: hit.method,
181
+ ...(hit.parityMatched !== undefined ? { parity_matched: hit.parityMatched } : {}),
182
+ ...(hit.bracket !== undefined ? { interpolation_bracket: hit.bracket } : {}),
183
+ };
184
+ }
185
+ /**
186
+ * Span-rescore tier (#370): opt-in last-resort locality recovery. Runs ONLY when the tree resolved
187
+ * NOTHING (the #685 brake — never disturb a working coordinate). Enumerates raw-token spans, exact-
188
+ * matches the same-country gazetteer (longest-wins + postcode-consistency gate; see
189
+ * `span-rescore.ts`), and on a hit INJECTS a resolved `locality` node decorated exactly like a
190
+ * normally-resolved one. Byte-stable when `opts.spanRescore` is unset. Async (it queries the
191
+ * backend), so it's awaited.
192
+ */
193
+ async function applySpanRescore(roots, raw, backend, opts) {
194
+ if (hasResolvedPlace(roots))
195
+ return; // already resolved — never second-guess a working coordinate
196
+ const hit = await findRescoreCandidate(raw, roots, backend, {
197
+ country: opts.defaultCountry,
198
+ postcode: firstPostcodeValue(roots),
199
+ gateKm: opts.spanRescoreGateKm,
200
+ });
201
+ if (!hit)
202
+ return;
203
+ const node = {
204
+ tag: "locality",
205
+ value: hit.text,
206
+ start: hit.start,
207
+ end: hit.end,
208
+ // No model confidence for a post-hoc recovery; a mid-tier value marks it as recovered, not asserted.
209
+ confidence: 0.5,
210
+ children: [],
211
+ };
212
+ decorateNode(node, hit.place, []);
213
+ // `rescore_gated` carries the gate's precision signal as an EXPLICIT handle — NOT folded into the
214
+ // calibrated `confidence`, which would break the isotonic guarantee (a true calibrated 0.83 must not
215
+ // be confused with a rescore plug-in estimate; DeepSeek 2026-06-23). true = postcode gate fired
216
+ // (high-precision); false = ungated (no postcode→point coverage for this country, ~83%-precision).
217
+ node.metadata = { ...(node.metadata ?? {}), span_rescore: true, rescore_gated: hit.gated };
218
+ roots.push(node);
219
+ }
220
+ /** A resolved node carries a real coordinate (placeId set + non-zero lat/lon). */
221
+ function isResolvedWithCoord(n) {
222
+ return !!(n.placeId && typeof n.lat === "number" && typeof n.lon === "number" && (n.lat !== 0 || n.lon !== 0));
223
+ }
224
+ /**
225
+ * Postcode-disambiguated locality selection (#370 "Lever A"). The single biggest miss on the EU/AU
226
+ * panel is a same-named town resolved to the WRONG instance — "06260 Saint-Pierre" lands 617 km off
227
+ * — while the postcode that would disambiguate it (06260 → Alpes-Maritimes) sits resolved in the
228
+ * same tree, discarded because the coordinate-picker prefers the (wrong) locality node and never
229
+ * cross- checks it. This post-walk pass closes that loop, backend-agnostically and with no extra
230
+ * query:
231
+ *
232
+ * 1. Find the resolved postcode's coordinate (the trustworthy anchor — a postcode is unambiguous
233
+ * within a country in a way a town name is not).
234
+ * 2. For each resolved locality node farther than `gateKm` from it: re-pick the same-named candidate
235
+ * from the node's already-captured `alternatives` that is NEAREST the postcode and within the
236
+ * gate. This keeps locality granularity at the CORRECT instance.
237
+ * 3. If no alternative reconciles, the locality instance is unreliable — fall its coordinate back to
238
+ * the postcode point (right area, the safe answer) and flag `postcode_city_mismatch`.
239
+ *
240
+ * Only fires where the postcode resolved to a point, so it composes with postcode coverage (#193) —
241
+ * add a country's postcodes and this immediately disambiguates its same-named towns. Default-off
242
+ * via `opts.postcodeConsistency`; byte-stable when unset.
243
+ */
244
+ function applyPostcodeConsistency(roots, gateKm) {
245
+ // The resolved postcode anchor (first one with a real coordinate).
246
+ let anchor = null;
247
+ const findAnchor = [...roots];
248
+ while (findAnchor.length > 0) {
249
+ const n = findAnchor.pop();
250
+ if (n.tag === "postcode" && isResolvedWithCoord(n)) {
251
+ anchor = { lat: n.lat, lon: n.lon };
252
+ break;
253
+ }
254
+ findAnchor.push(...n.children);
255
+ }
256
+ if (!anchor)
257
+ return; // no postcode→point — nothing to disambiguate against (gate can't fire)
258
+ const stack = [...roots];
259
+ while (stack.length > 0) {
260
+ const node = stack.pop();
261
+ stack.push(...node.children);
262
+ if ((node.tag !== "locality" && node.tag !== "dependent_locality") || !isResolvedWithCoord(node))
263
+ continue;
264
+ if (haversineKm(anchor.lat, anchor.lon, node.lat, node.lon) <= gateKm)
265
+ continue; // already consistent
266
+ // Re-pick: the same-named candidate nearest the postcode, within the gate. `alternatives` is
267
+ // typed `unknown[]` on the node (decoder/types.ts can't import resolver types) — they ARE the
268
+ // `ResolvedPlace` runner-ups decorateNode attached, so the cast is sound.
269
+ const alts = node.alternatives ?? [];
270
+ const reconciling = alts
271
+ .filter((a) => a.lat !== 0 || a.lon !== 0)
272
+ .map((a) => ({ a, d: haversineKm(anchor.lat, anchor.lon, a.lat, a.lon) }))
273
+ .filter((x) => x.d <= gateKm)
274
+ .sort((x, y) => x.d - y.d)[0];
275
+ if (reconciling) {
276
+ // Swap to the consistent instance; the displaced winner becomes an alternative.
277
+ const displaced = {
278
+ id: 0,
279
+ name: String(node.metadata?.["resolver_name"] ?? node.value),
280
+ placetype: "locality",
281
+ country: reconciling.a.country,
282
+ lat: node.lat,
283
+ lon: node.lon,
284
+ score: 0,
285
+ };
286
+ const rest = alts.filter((a) => a !== reconciling.a);
287
+ decorateNode(node, reconciling.a, [displaced, ...rest]);
288
+ node.metadata = { ...(node.metadata ?? {}), postcode_repicked: true };
289
+ continue;
290
+ }
291
+ // No same-named instance near the postcode → the town is unreliable; trust the postcode's area.
292
+ node.lat = anchor.lat;
293
+ node.lon = anchor.lon;
294
+ node.metadata = { ...(node.metadata ?? {}), postcode_city_mismatch: true, coordinate_source: "postcode_fallback" };
295
+ }
296
+ }
297
+ class WofResolver {
298
+ #backend;
299
+ constructor(backend) {
300
+ this.#backend = backend;
301
+ }
302
+ async resolveTree(tree, opts = {}) {
303
+ const state = {
304
+ lookupsRemaining: opts.maxLookups ?? 10,
305
+ // Full replacement when `placetypeMap` is supplied — callers that want to extend rather
306
+ // than replace should spread DEFAULT_PLACETYPE_MAP themselves.
307
+ placetypeMap: opts.placetypeMap ?? DEFAULT_PLACETYPE_MAP,
308
+ minWinningScore: opts.minWinningScore ?? 0,
309
+ candidatesPerLookup: opts.candidatesPerLookup ?? 5,
310
+ defaultCountry: opts.defaultCountry,
311
+ parentFallback: opts.parentFallback ?? true,
312
+ postcode: firstPostcodeValue(tree.roots),
313
+ anchorPosterior: opts.anchorPosterior,
314
+ anchorWeight: opts.anchorWeight ?? 2.0,
315
+ hardCountry: opts.hardCountry,
316
+ // Default-ON (#402): completion only fires for a dual-role region whose locality the parser
317
+ // dropped, and no-ops entirely when the backend has no relation (the browser WASM resolver, or
318
+ // a gazetteer without `coincident_roles`). Pass `hierarchyCompletion: false` to opt out.
319
+ // `cityStateFallback` is the #387 alias that #405 generalized — still honored.
320
+ hierarchyCompletion: opts.hierarchyCompletion ?? opts.cityStateFallback ?? true,
321
+ includeAncestors: opts.includeAncestors ?? false,
322
+ localityNodePresent: false,
323
+ resolvedRegion: null,
324
+ resolvedRegionNode: null,
325
+ };
326
+ const newRoots = [];
327
+ for (const root of tree.roots) {
328
+ newRoots.push(await this.#walk(root, /* parentResolved */ null, state));
329
+ }
330
+ // Dual-role hierarchy completion (#405/#415). Only when enabled, a region resolved, and the parser
331
+ // emitted NO locality — record the dropped locality as a SECONDARY ROLE (an interpretation) on the
332
+ // resolved region node, from the backend's precomputed coincident-roles relation (#403). One node,
333
+ // one span, two roles — no synthesized sibling. See ResolveOpts.hierarchyCompletion.
334
+ if (state.hierarchyCompletion && state.resolvedRegion && state.resolvedRegionNode && !state.localityNodePresent) {
335
+ this.#completeRegionRole(state.resolvedRegion, state.resolvedRegionNode);
336
+ }
337
+ // Postcode-consistency (#370 "Lever A"): opt-in. After the admin walk (needs both the locality
338
+ // and the postcode resolved) and before the street tiers (which key off the postcode/street, not
339
+ // the locality coordinate this adjusts). Byte-stable when opts.postcodeConsistency is unset.
340
+ if (opts.postcodeConsistency) {
341
+ applyPostcodeConsistency(newRoots, opts.postcodeConsistencyGateKm ?? 50);
342
+ }
343
+ // Address-point tier (#476): opt-in street-level exact match. After the admin walk so the
344
+ // tier can never disturb admin attribution — it only ADDS the precise coordinate. Byte-stable
345
+ // when opts.addressPoints is absent.
346
+ if (opts.addressPoints) {
347
+ applyAddressPoint(newRoots, opts.addressPoints);
348
+ }
349
+ // Interpolation tier (#483): strictly AFTER the exact-point block so an estimate can never
350
+ // override a real situs point (applyInterpolation also gates on resolution_tier). Opt-in;
351
+ // byte-stable when opts.interpolation is absent.
352
+ if (opts.interpolation) {
353
+ applyInterpolation(newRoots, opts.interpolation, opts.interpolationRadiusCalibration);
354
+ }
355
+ // Span-rescore tier (#370): opt-in, last (so it only fires when every other tier left the tree
356
+ // unresolved). Byte-stable when opts.spanRescore is unset.
357
+ if (opts.spanRescore) {
358
+ await applySpanRescore(newRoots, tree.raw, this.#backend, opts);
359
+ }
360
+ return { raw: tree.raw, roots: newRoots };
361
+ }
362
+ /**
363
+ * Record a dropped dual-role locality as a `locality` INTERPRETATION on the resolved region node
364
+ * (#415, generalizes #405's synthesized node). Consults `coincidentLocalitiesFor(regionId)` (O(1)
365
+ * map lookup — no distance math, no backend query), picks the principal city
366
+ * ({@link pickCompletion}: population-primary, distance tiebreak, abstain on a genuine tie), and
367
+ * appends an interpretation to `regionNode.interpretations`. No-op when the backend has no
368
+ * relation, the region isn't a dual-role place, or it abstains. The region node's primary role
369
+ * stays `region`; the locality rides alongside.
370
+ */
371
+ #completeRegionRole(region, regionNode) {
372
+ if (typeof region.id !== "number" || !this.#backend.coincidentLocalitiesFor)
373
+ return;
374
+ const loc = pickCompletion(this.#backend.coincidentLocalitiesFor(region.id));
375
+ if (!loc)
376
+ return;
377
+ const interpretation = {
378
+ tag: "locality",
379
+ placeId: `wof:${loc.id}`,
380
+ sourceId: `${loc.placetype}:${loc.id}`,
381
+ lat: loc.lat,
382
+ lon: loc.lon,
383
+ confidence: 0,
384
+ metadata: { relationship_type: loc.relationshipType, resolver_completed: true, resolver_name: loc.name },
385
+ };
386
+ regionNode.interpretations = [...(regionNode.interpretations ?? []), interpretation];
387
+ }
388
+ async #walk(node, parentResolved, state) {
389
+ // Always clone — never mutate input nodes.
390
+ const decorated = { ...node, children: [] };
391
+ const placetype = state.placetypeMap[node.tag];
392
+ // Track locality presence for hierarchy completion (#405): completion must NOT fire if the parser
393
+ // already emitted a locality node (even one that failed to resolve) — it only fills a genuine
394
+ // gap. Cheap and always-on; only consulted when hierarchyCompletion is set.
395
+ if (placetype === "locality")
396
+ state.localityNodePresent = true;
397
+ let resolved = null;
398
+ if (placetype && state.lookupsRemaining > 0 && node.value.trim().length > 0) {
399
+ const picked = await this.#lookupAndPick(node, placetype, parentResolved, state);
400
+ if (picked) {
401
+ resolved = picked.top;
402
+ decorateNode(decorated, picked.top, picked.alternatives);
403
+ // Lineage attachment (#404): stamp the resolved place's ancestor chain onto metadata. Opt-in
404
+ // + only when the backend supplies it, so the default stays byte-identical (no extra query).
405
+ if (state.includeAncestors && this.#backend.ancestors) {
406
+ decorated.metadata = { ...(decorated.metadata ?? {}), ancestors: this.#backend.ancestors(picked.top.id) };
407
+ }
408
+ // Capture the first resolved region (place + node) for hierarchy completion — the locality
409
+ // interpretation is pushed onto this node in the post-walk pass.
410
+ if (placetype === "region" && state.resolvedRegion === null) {
411
+ state.resolvedRegion = picked.top;
412
+ state.resolvedRegionNode = decorated;
413
+ }
414
+ }
415
+ }
416
+ const carryParent = resolved ?? parentResolved;
417
+ for (const child of node.children) {
418
+ decorated.children.push(await this.#walk(child, carryParent, state));
419
+ }
420
+ return decorated;
421
+ }
422
+ async #lookupAndPick(node, placetype, parentResolved, state) {
423
+ state.lookupsRemaining--;
424
+ const query = {
425
+ text: node.value,
426
+ placetype,
427
+ limit: state.candidatesPerLookup,
428
+ };
429
+ // Pass the inherited parent constraint to the backend when available — `parentId` scopes to
430
+ // the resolved parent's descendants. For `country`: a resolved parent's country wins, else
431
+ // fall back to the caller's `defaultCountry`. Without this top-level hint a bare "IL" over a
432
+ // multi-country gazetteer fuzzy-matches a foreign place (e.g. a French region) — see the
433
+ // Direction-C resolver eval.
434
+ if (parentResolved && typeof parentResolved.id === "number")
435
+ query.parentId = parentResolved.id;
436
+ // #194: a resolved parent's country wins, then the caller's `defaultCountry`, then the confident
437
+ // placer `hardCountry`. All three are a HARD candidate filter. The placer's `hardCountry` is gated
438
+ // upstream on high confidence (so it only fires when the model is sure), and on a miss the node is
439
+ // left UNRESOLVED rather than re-resolved globally: the off-continent rows are precisely the ones
440
+ // whose locality isn't in the country's gazetteer slice, so a global retry would just re-admit the
441
+ // wrong-continent guess the hard filter exists to drop ("in-region or unresolved"). Measured: a
442
+ // global fallback collapses back to the soft-prior baseline (FI p90 3050, PL p90 1078); pure-hard
443
+ // collapses the tail (FI 18 km, PL p99 8172→494) at a coverage-bounded recall cost.
444
+ const country = parentResolved?.country ?? state.defaultCountry ?? state.hardCountry;
445
+ if (country)
446
+ query.country = country;
447
+ // Coordinate-first: hand the sibling postcode to locality lookups so the backend can inject
448
+ // postcode-proximal candidates the name-match would miss. Only for locality (the placetype both
449
+ // `locality` and `dependent_locality` map to); other placetypes ignore it.
450
+ if (placetype === "locality" && state.postcode)
451
+ query.postcode = state.postcode;
452
+ let candidates;
453
+ try {
454
+ candidates = await this.#backend.findPlace(query);
455
+ // Parent soft-gating: `parentId` is a HARD descendant filter in the backend, which wrongly
456
+ // zeroes the result when the parent resolved wrong OR the gazetteer hierarchy is incomplete
457
+ // (a real locality whose `ancestors` chain is missing its region). Rather than turn a
458
+ // resolvable node into an unresolved one, retry once WITHOUT the parent constraint — we
459
+ // prefer a parent-scoped hit but never sacrifice recall. The country constraint is kept, so
460
+ // this still can't wander to a foreign place. Same logical resolution → no extra budget.
461
+ if (candidates.length === 0 && state.parentFallback && query.parentId !== undefined) {
462
+ delete query.parentId;
463
+ candidates = await this.#backend.findPlace(query);
464
+ }
465
+ }
466
+ catch {
467
+ // Defensive: a backend failure should not abort the whole tree walk. Leave the node with
468
+ // its classifier attribution intact.
469
+ return null;
470
+ }
471
+ if (candidates.length === 0)
472
+ return null;
473
+ // Postcode-anchor re-rank (#369): when a country posterior is supplied (from the address's
474
+ // postcode), boost candidates by `anchorWeight * posterior[candidate.country]` and re-sort, so a
475
+ // postcode that pins the country pulls the right-country place over a higher-BM25 foreign namesake
476
+ // (the "Berlin DE vs Berlin US" class the #59 harness measured). No-op when `anchorPosterior` is
477
+ // undefined (the default) → byte-identical resolution.
478
+ //
479
+ // Applied to BOTH region and locality — the two placetypes that suffer cross-country namesake/
480
+ // abbreviation collisions a country posterior can break. The region case is the one #447's window
481
+ // fix couldn't reach: a bare 2-letter abbreviation is shared across countries ("VT" is
482
+ // both Vermont and Viterbo; "ME" both Maine and Messina), so with no country signal the score
483
+ // picks the wrong one — and because resolveTree resolves region FIRST and inherits its country
484
+ // down, a wrong region poisons the locality too. The postcode posterior breaks the tie at the
485
+ // region, and the right country then flows to the locality. (Country/macroregion/county are
486
+ // excluded: they don't exhibit this collision class and carry country via `parentId` when nested.)
487
+ //
488
+ // Tier-SAFE ordering: the candidate's exact-match flag is the PRIMARY key, so the country pin
489
+ // never crosses the exact/partial boundary. WITHIN a tier, `score + anchorWeight * posterior`
490
+ // applies the (soft) country boost. So a confident US postcode keeps the US EXACT region
491
+ // ("ME" → Maine) ahead of a more-populous US PARTIAL match (Missouri) AND, within the exact
492
+ // tier, ahead of a foreign exact match (Messina IT); a soft posterior still blends with score.
493
+ // (A plain additive re-rank loses the tier — it isn't encoded in `score` — and flips
494
+ // "ME" → Missouri / "PA" → Alabama. Backends that don't set `exactMatch` degrade to additive.)
495
+ const anchorEligible = placetype === "region" || placetype === "locality";
496
+ let ranked = candidates;
497
+ if (state.anchorPosterior && anchorEligible && candidates.length > 1) {
498
+ const post = state.anchorPosterior;
499
+ const w = state.anchorWeight;
500
+ ranked = [...candidates].sort((a, b) => Number(b.exactMatch ?? false) - Number(a.exactMatch ?? false) ||
501
+ b.score + w * (post[b.country] ?? 0) - (a.score + w * (post[a.country] ?? 0)));
502
+ }
503
+ // Exact-type preference (#718): when the placetype-equivalence group let a broader admin tier
504
+ // (`macroregion`/`macrocounty`) into the candidate pool, prefer a candidate of the EXACT
505
+ // requested type over the macro fallback — a real `region` (US state, DE Bundesland, ES
506
+ // provincia) must win over a same-name macroregion namesake, so no real region silently
507
+ // downgrades to a macro. STABLE partition: exact-type candidates keep their (already-ranked)
508
+ // relative order ahead of fallbacks, so the score / anchor re-rank survives WITHIN each tier.
509
+ // No-op for placetypes without a macro fallback (the byte-stable default) and when every
510
+ // candidate is the same tier.
511
+ const hasFallbackCandidate = ranked.some((c) => isPlacetypeFallback(placetype, c.placetype));
512
+ if (hasFallbackCandidate && ranked.length > 1) {
513
+ ranked = [
514
+ ...ranked.filter((c) => !isPlacetypeFallback(placetype, c.placetype)),
515
+ ...ranked.filter((c) => isPlacetypeFallback(placetype, c.placetype)),
516
+ ];
517
+ }
518
+ const top = ranked[0];
519
+ if (top.score < state.minWinningScore)
520
+ return null;
521
+ // Fallback-observability (#718): if the winner is a macro-type AND no exact-type candidate
522
+ // existed for this span, annotate that a broader tier stood in for the true one. Additive —
523
+ // identity/coordinate are unchanged; only `metadata.resolution_quality` is stamped downstream.
524
+ if (isPlacetypeFallback(placetype, top.placetype)) {
525
+ top.resolutionQuality = "fallback";
526
+ }
527
+ return { top, alternatives: ranked.slice(1) };
528
+ }
529
+ }
530
+ /**
531
+ * Stamp a node with resolver-supplied attribution. Displaces any prior classifier `source` /
532
+ * `sourceId` into `metadata.classifier_source` / `metadata.classifier_source_id` so debugging tools
533
+ * can still see who made the original assertion. Surfaces the runner-up candidates on
534
+ * `alternatives` so callers can disambiguate (Springfield-class failures, [#8 in the failure
535
+ * catalogue]).
536
+ */
537
+ function decorateNode(node, resolved, alternatives) {
538
+ if (node.source !== undefined || node.sourceId !== undefined) {
539
+ const meta = { ...(node.metadata ?? {}) };
540
+ if (node.source !== undefined)
541
+ meta["classifier_source"] = node.source;
542
+ if (node.sourceId !== undefined)
543
+ meta["classifier_source_id"] = node.sourceId;
544
+ node.metadata = meta;
545
+ }
546
+ node.source = "resolver";
547
+ node.sourceId = `${resolved.placetype}:${resolved.id}`;
548
+ node.lat = resolved.lat;
549
+ node.lon = resolved.lon;
550
+ node.placeId = `wof:${resolved.id}`; // v1: only WOF resolvers; the URI scheme stays this simple
551
+ // Record the resolver's ranking score AND the resolved place's CANONICAL name. The name is the
552
+ // gazetteer's truth for the place we picked — distinct from `node.value` (the raw input span). It
553
+ // lets consumers display the canonical name and lets the end-to-end eval check the resolver chose
554
+ // the right PLACE (gazetteer-name vs ground-truth) rather than merely echoing the parser's text.
555
+ node.metadata = { ...(node.metadata ?? {}), resolver_score: resolved.score, resolver_name: resolved.name };
556
+ // The postcode/locality conflict flag (the falsehood differentiator): the postcode pointed to a
557
+ // geographically different place than the parsed city name. Surface it so callers can warn rather
558
+ // than silently trust the resolved point.
559
+ if (resolved.mismatch)
560
+ node.metadata["postcode_city_mismatch"] = true;
561
+ // Fallback-observability (#718): a broader admin tier (macroregion/macrocounty) stood in for the
562
+ // true region/county because no exact-type candidate existed. Additive annotation only — the
563
+ // resolved coordinate/identity above is untouched; this just lets a consumer / QA pass see it.
564
+ if (resolved.resolutionQuality)
565
+ node.metadata["resolution_quality"] = resolved.resolutionQuality;
566
+ if (alternatives.length > 0) {
567
+ node.alternatives = alternatives;
568
+ }
569
+ }
570
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * #370 span-rescore — recover a dropped/fragmented locality from the RAW text when a parse fails to
7
+ * resolve. The model sometimes fragments an accented or non-ASCII locality token ("Grudziądz"
8
+ * splits into "Grudzi" + "dz" on the ą combining mark, #555), so neither fragment resolves and
9
+ * the address comes back with no coordinate. But the whole word sits intact in the raw input — a
10
+ * whitespace tokenizer sees it where the model's subword tokenizer didn't.
11
+ *
12
+ * This module is the PURE, backend-agnostic core: enumerate contiguous raw-token spans, exact-match
13
+ * them against the same-country gazetteer, and return the best locality candidate. The resolver
14
+ * (`resolve.ts` → `applySpanRescore`) owns the integration: it runs this ONLY on an unresolved
15
+ * tree (the #685 brake — never second-guess a working coordinate) and injects the recovered
16
+ * locality as a resolved node. Opt-in via `ResolveOpts.spanRescore`; default-off + byte-stable
17
+ * when unset.
18
+ *
19
+ * The design + thresholds are validated on a 7-locale coordinate panel
20
+ * (`scripts/eval/span-rescore-validate.ts`, eval
21
+ * `docs/articles/evals/2026-06-23-370-span-rescore.mdx`): longest-exact-match-wins (the gold
22
+ * locality is usually the LONGER, more-specific name — shortest- wins grabs the ambiguous prefix
23
+ * "Tomaszów" of "Tomaszów Mazowiecki", 135 km off), and a postcode- consistency gate that rejects
24
+ * a match far from where the postcode resolves (kills coverage-gap false-positives where the
25
+ * backend has postcode coverage).
26
+ */
27
+ import type { AddressNode } from "@mailwoman/core/decoder";
28
+ import type { ResolvedPlace, ResolverBackend } from "@mailwoman/core/resolver";
29
+ export interface SpanRescoreOptions {
30
+ /**
31
+ * ISO-3166 alpha-2 country to constrain the gazetteer match (the parse's detected/ default
32
+ * country).
33
+ */
34
+ country?: string;
35
+ /**
36
+ * Sibling postcode — used both as the backend disambiguation hint AND the consistency-gate
37
+ * anchor.
38
+ */
39
+ postcode?: string;
40
+ /**
41
+ * Reject a candidate whose coordinate is farther than this (km) from the postcode anchor. The
42
+ * gate only fires when the postcode resolves to a point in the backend; otherwise it can't and
43
+ * the match is accepted (so it never penalizes a backend without postcode coverage). 0 disables.
44
+ * Default 50.
45
+ */
46
+ gateKm?: number;
47
+ /** Max contiguous raw tokens to treat as one locality span. Default 4. */
48
+ maxSpanTokens?: number;
49
+ /**
50
+ * Min confidence for a street/house_number/postcode node to count as a span-blocking constituent.
51
+ * Default 0.7.
52
+ */
53
+ confidentThreshold?: number;
54
+ }
55
+ /** The recovered locality: the raw span and the gazetteer place it resolved to. */
56
+ export interface RescoreCandidate {
57
+ /** The raw text of the winning span. */
58
+ text: string;
59
+ /** Char offsets of the span in the raw input. */
60
+ start: number;
61
+ end: number;
62
+ /** The resolved gazetteer place (decorate a node with this). */
63
+ place: ResolvedPlace;
64
+ /**
65
+ * Whether the postcode-consistency gate FIRED for this recovery — i.e. the postcode resolved to a
66
+ * point and the match was validated within `gateKm` of it. `true` = high-precision (postcode-
67
+ * consistent); `false` = ungated (no postcode→point coverage for this country, so the match
68
+ * wasn't geo-validated — the ~83%-precision case). The caller surfaces this as
69
+ * `metadata.rescore_gated` so a consumer can threshold on it WITHOUT a hidden per-country
70
+ * coverage map. Deliberately NOT folded into the calibrated `confidence` — that would break the
71
+ * isotonic guarantee (a true calibrated 0.83 must not be confused with a rescore plug-in
72
+ * estimate).
73
+ */
74
+ gated: boolean;
75
+ }
76
+ /** True if any node in the tree already carries a resolved place id — the #685 brake. */
77
+ export declare function hasResolvedPlace(roots: readonly AddressNode[]): boolean;
78
+ /**
79
+ * Find the best locality the raw text exact-matches in the gazetteer. Returns null when nothing
80
+ * matches (or the postcode gate rejects every match). Callers gate on `hasResolvedPlace` first.
81
+ */
82
+ export declare function findRescoreCandidate(raw: string, roots: readonly AddressNode[], backend: ResolverBackend, opts?: SpanRescoreOptions): Promise<RescoreCandidate | null>;
83
+ //# sourceMappingURL=span-rescore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"span-rescore.d.ts","sourceRoot":"","sources":["../span-rescore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAG9E,MAAM,WAAW,kBAAkB;IAClC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,gEAAgE;IAChE,KAAK,EAAE,aAAa,CAAA;IACpB;;;;;;;;;OASG;IACH,KAAK,EAAE,OAAO,CAAA;CACd;AAyBD,yFAAyF;AACzF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAQvE;AAwBD;;;GAGG;AACH,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,EAAE,eAAe,EACxB,IAAI,GAAE,kBAAuB,GAC3B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAoDlC"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * #370 span-rescore — recover a dropped/fragmented locality from the RAW text when a parse fails to
7
+ * resolve. The model sometimes fragments an accented or non-ASCII locality token ("Grudziądz"
8
+ * splits into "Grudzi" + "dz" on the ą combining mark, #555), so neither fragment resolves and
9
+ * the address comes back with no coordinate. But the whole word sits intact in the raw input — a
10
+ * whitespace tokenizer sees it where the model's subword tokenizer didn't.
11
+ *
12
+ * This module is the PURE, backend-agnostic core: enumerate contiguous raw-token spans, exact-match
13
+ * them against the same-country gazetteer, and return the best locality candidate. The resolver
14
+ * (`resolve.ts` → `applySpanRescore`) owns the integration: it runs this ONLY on an unresolved
15
+ * tree (the #685 brake — never second-guess a working coordinate) and injects the recovered
16
+ * locality as a resolved node. Opt-in via `ResolveOpts.spanRescore`; default-off + byte-stable
17
+ * when unset.
18
+ *
19
+ * The design + thresholds are validated on a 7-locale coordinate panel
20
+ * (`scripts/eval/span-rescore-validate.ts`, eval
21
+ * `docs/articles/evals/2026-06-23-370-span-rescore.mdx`): longest-exact-match-wins (the gold
22
+ * locality is usually the LONGER, more-specific name — shortest- wins grabs the ambiguous prefix
23
+ * "Tomaszów" of "Tomaszów Mazowiecki", 135 km off), and a postcode- consistency gate that rejects
24
+ * a match far from where the postcode resolves (kills coverage-gap false-positives where the
25
+ * backend has postcode coverage).
26
+ */
27
+ import { haversineKm } from "@mailwoman/spatial";
28
+ /** Normalize for exact comparison: lowercase, strip diacritics + punctuation, collapse whitespace. */
29
+ const norm = (s) => s
30
+ .toLowerCase()
31
+ .normalize("NFD")
32
+ .replace(/[^a-z0-9 ]/g, " ")
33
+ .replace(/\s+/g, " ")
34
+ .trim();
35
+ /** Whitespace/punctuation tokenization of the raw input, char offsets preserved, diacritics intact. */
36
+ function tokenizeRaw(raw) {
37
+ const toks = [];
38
+ const re = /[^\s,;/]+/g;
39
+ let m;
40
+ while ((m = re.exec(raw)) !== null)
41
+ toks.push({ text: m[0], start: m.index, end: m.index + m[0].length });
42
+ return toks;
43
+ }
44
+ /** True if any node in the tree already carries a resolved place id — the #685 brake. */
45
+ export function hasResolvedPlace(roots) {
46
+ const stack = [...roots];
47
+ while (stack.length) {
48
+ const n = stack.pop();
49
+ if (n.placeId)
50
+ return true;
51
+ if (n.children?.length)
52
+ stack.push(...n.children);
53
+ }
54
+ return false;
55
+ }
56
+ /**
57
+ * Char ranges of confident street/house_number/postcode constituents — a locality span must not
58
+ * overlap them.
59
+ */
60
+ function confidentRanges(roots, threshold) {
61
+ const out = [];
62
+ const stack = [...roots];
63
+ while (stack.length) {
64
+ const n = stack.pop();
65
+ if ((n.tag === "postcode" || n.tag === "house_number" || n.tag === "street") &&
66
+ (n.confidence ?? 0) >= threshold &&
67
+ Number.isFinite(n.start) &&
68
+ Number.isFinite(n.end)) {
69
+ out.push([n.start, n.end]);
70
+ }
71
+ if (n.children?.length)
72
+ stack.push(...n.children);
73
+ }
74
+ return out;
75
+ }
76
+ /**
77
+ * Find the best locality the raw text exact-matches in the gazetteer. Returns null when nothing
78
+ * matches (or the postcode gate rejects every match). Callers gate on `hasResolvedPlace` first.
79
+ */
80
+ export async function findRescoreCandidate(raw, roots, backend, opts = {}) {
81
+ const gateKm = opts.gateKm ?? 50;
82
+ const maxSpan = opts.maxSpanTokens ?? 4;
83
+ const threshold = opts.confidentThreshold ?? 0.7;
84
+ const country = opts.country;
85
+ const postcode = opts.postcode?.trim() || undefined;
86
+ // Postcode-consistency anchor: where does the postcode itself resolve? (No-op when the backend has
87
+ // no postcode coverage — findPlace returns nothing → no anchor → gate can't fire → match accepted.)
88
+ let anchor = null;
89
+ if (postcode && gateKm > 0) {
90
+ const pcHits = await backend.findPlace({ text: postcode, country, limit: 2 });
91
+ const a = pcHits.find((h) => h.lat !== 0 || h.lon !== 0);
92
+ if (a)
93
+ anchor = { lat: a.lat, lon: a.lon };
94
+ }
95
+ const toks = tokenizeRaw(raw);
96
+ const avoid = confidentRanges(roots, threshold);
97
+ const overlapsAvoid = (s, e) => avoid.some(([as, ae]) => s < ae && as < e);
98
+ const spans = [];
99
+ for (let len = Math.min(maxSpan, toks.length); len >= 1; len--) {
100
+ for (let i = 0; i + len <= toks.length; i++) {
101
+ const start = toks[i].start;
102
+ const end = toks[i + len - 1].end;
103
+ if (overlapsAvoid(start, end))
104
+ continue;
105
+ spans.push({ text: raw.slice(start, end), start, end, len });
106
+ }
107
+ }
108
+ spans.sort((a, b) => b.len - a.len);
109
+ for (const sp of spans) {
110
+ const key = norm(sp.text);
111
+ if (key.length < 2 || /^\d+$/.test(key))
112
+ continue; // skip bare numbers / empties
113
+ const hits = await backend.findPlace({ text: sp.text, country, postcode, placetype: "locality", limit: 5 });
114
+ const exact = hits.filter((h) => h.exactMatch && norm(h.name) === key && (h.lat !== 0 || h.lon !== 0));
115
+ for (const h of exact) {
116
+ if (anchor && gateKm > 0 && haversineKm(anchor.lat, anchor.lon, h.lat, h.lon) > gateKm)
117
+ continue;
118
+ // gated = the postcode anchor existed AND validated this match (within gateKm). When no anchor
119
+ // (no postcode→point coverage), the match is ungated — returned, but flagged lower-precision.
120
+ return { text: sp.text, start: sp.start, end: sp.end, place: h, gated: anchor !== null };
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ //# sourceMappingURL=span-rescore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"span-rescore.js","sourceRoot":"","sources":["../span-rescore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Per-package vitest config for @mailwoman/resolver. The resolver's OWN files run from source
7
+ * (vitest transpiles ./resolve.ts etc.); its workspace deps — @mailwoman/core, /spatial, /codex —
8
+ * resolve to their compiled `out/` (built by `tsc -b`). vitest's default resolution doesn't
9
+ * traverse a sibling workspace's package-exports for a transitive import (e.g. spatial →
10
+ * @mailwoman/core/ objects), so the aliases below pin them explicitly. Most subpaths are
11
+ * directories (→ `<dir>/ index.js`); the handful that are bare files (`objects`) get a
12
+ * more-specific alias FIRST.
13
+ */
14
+ declare const _default: import("vite").UserConfig;
15
+ export default _default;
16
+ //# sourceMappingURL=vitest.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;;AASH,wBAiBE"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Per-package vitest config for @mailwoman/resolver. The resolver's OWN files run from source
7
+ * (vitest transpiles ./resolve.ts etc.); its workspace deps — @mailwoman/core, /spatial, /codex —
8
+ * resolve to their compiled `out/` (built by `tsc -b`). vitest's default resolution doesn't
9
+ * traverse a sibling workspace's package-exports for a transitive import (e.g. spatial →
10
+ * @mailwoman/core/ objects), so the aliases below pin them explicitly. Most subpaths are
11
+ * directories (→ `<dir>/ index.js`); the handful that are bare files (`objects`) get a
12
+ * more-specific alias FIRST.
13
+ */
14
+ import { resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { defineConfig } from "vitest/config";
17
+ const here = fileURLToPath(new URL(".", import.meta.url));
18
+ const out = (pkg, sub) => resolve(here, `../${pkg}/out/${sub}`);
19
+ export default defineConfig({
20
+ resolve: {
21
+ alias: [
22
+ // Order matters — file-subpaths (objects.ts → objects.js) before the directory regex.
23
+ { find: /^@mailwoman\/core\/objects$/, replacement: out("core", "objects.js") },
24
+ { find: /^@mailwoman\/core\/(.+)$/, replacement: resolve(here, "../core/out/$1/index.js") },
25
+ { find: /^@mailwoman\/core$/, replacement: out("core", "index.js") },
26
+ { find: /^@mailwoman\/spatial\/(.+)$/, replacement: resolve(here, "../spatial/out/$1/index.js") },
27
+ { find: /^@mailwoman\/spatial$/, replacement: out("spatial", "index.js") },
28
+ { find: /^@mailwoman\/codex\/(.+)$/, replacement: resolve(here, "../codex/out/$1/index.js") },
29
+ { find: /^@mailwoman\/codex$/, replacement: out("codex", "index.js") },
30
+ ],
31
+ },
32
+ test: {
33
+ isolate: true,
34
+ exclude: ["**/node_modules/**", "**/out/**", "**/dist/**"],
35
+ },
36
+ });
37
+ //# sourceMappingURL=vitest.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAE5C,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACzD,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,EAAE,CAAC,CAAA;AAE/E,eAAe,YAAY,CAAC;IAC3B,OAAO,EAAE;QACR,KAAK,EAAE;YACN,sFAAsF;YACtF,EAAE,IAAI,EAAE,6BAA6B,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE;YAC/E,EAAE,IAAI,EAAE,0BAA0B,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,yBAAyB,CAAC,EAAE;YAC3F,EAAE,IAAI,EAAE,oBAAoB,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE;YACpE,EAAE,IAAI,EAAE,6BAA6B,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,4BAA4B,CAAC,EAAE;YACjG,EAAE,IAAI,EAAE,uBAAuB,EAAE,WAAW,EAAE,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE;YAC1E,EAAE,IAAI,EAAE,2BAA2B,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,EAAE,0BAA0B,CAAC,EAAE;YAC7F,EAAE,IAAI,EAAE,qBAAqB,EAAE,WAAW,EAAE,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE;SACtE;KACD;IACD,IAAI,EAAE;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,oBAAoB,EAAE,WAAW,EAAE,YAAY,CAAC;KAC1D;CACD,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@mailwoman/resolver",
3
+ "version": "4.13.0",
4
+ "description": "The address resolver: walk an AddressTree, decorate nodes with gazetteer-supplied coordinates + attribution. Backend-agnostic (any ResolverBackend), with the span-rescore + postcode-consistency levers. Lifted out of @mailwoman/core so it can depend on @mailwoman/spatial (haversine) + @mailwoman/codex (USPS directionals) instead of reinventing them; the type contract stays in core so the pipeline composes it without a cycle.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sister-software/mailwoman.git",
9
+ "directory": "resolver"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": "./out/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@mailwoman/codex": "4.13.0",
18
+ "@mailwoman/core": "4.13.0",
19
+ "@mailwoman/spatial": "4.13.0"
20
+ },
21
+ "files": [
22
+ "out/**/*.js",
23
+ "out/**/*.js.map",
24
+ "out/**/*.d.ts",
25
+ "out/**/*.d.ts.map"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ }
30
+ }