mailwoman 9.3.0 → 9.4.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 +6 -4
- package/lib/api-engine.ts +3 -3
- package/lib/browser-runtime/classify.ts +262 -0
- package/lib/browser-runtime/fetch.ts +89 -0
- package/lib/browser-runtime/index.ts +12 -0
- package/lib/browser-runtime/load-assets.ts +218 -0
- package/lib/browser-runtime/manifest.ts +104 -0
- package/lib/browser-runtime/resources.ts +358 -0
- package/lib/browser-runtime/types.ts +121 -0
- package/lib/cli-kit/dropin.ts +2 -7
- package/lib/cli-kit/index.ts +0 -1
- package/lib/cli-kit/shared.ts +4 -3
- package/lib/commands/corpus/build.tsx +2 -7
- package/lib/commands/corpus/fetch.tsx +27 -0
- package/lib/commands/corpus/run.tsx +1 -1
- package/lib/commands/corpus/slice/index.tsx +1 -2
- package/lib/commands/data/pull.tsx +1 -1
- package/lib/commands/gazetteer/build/poi-coverage.tsx +1 -1
- package/lib/commands/gazetteer/build/postcode-locality.tsx +3 -7
- package/lib/commands/gazetteer/build/tw-districts.tsx +70 -0
- package/lib/commands/gazetteer/conventions.tsx +2 -7
- package/lib/commands/gazetteer/importance.tsx +5 -9
- package/lib/commands/gazetteer/inspect/graph.tsx +2 -8
- package/lib/commands/gazetteer/inspect/mermaid.tsx +2 -8
- package/lib/commands/gazetteer/inspect/placetype-stats.tsx +2 -8
- package/lib/commands/gazetteer/inspect/sync.tsx +1 -2
- package/lib/commands/gazetteer/inspect/tree.tsx +2 -8
- package/lib/commands/gazetteer/overture-ingest.tsx +1 -2
- package/lib/commands/gazetteer/polygons.tsx +1 -2
- package/lib/commands/gazetteer/postcode-intl.tsx +1 -1
- package/lib/commands/gazetteer/publish.tsx +2 -2
- package/lib/commands/parse.tsx +30 -9
- package/lib/commands/poi.tsx +3 -3
- package/lib/commands/registry/run.tsx +2 -8
- package/lib/commands/registry/scorer-eval.tsx +1 -1
- package/lib/commands/situs/address-points.tsx +127 -34
- package/lib/commands/situs/interpolation-database.tsx +2 -8
- package/lib/commands/situs/interpolation.tsx +1 -2
- package/lib/commands/skill/install.tsx +1 -1
- package/lib/commands/tiger/fetch.tsx +2 -7
- package/lib/commands/tiger/redistricting.tsx +2 -7
- package/lib/commands/tiles/publish.tsx +6 -73
- package/lib/data/bundles.ts +3 -3
- package/lib/debug-view/DebugSessionApp.tsx +1 -1
- package/lib/debug-view/command.tsx +2 -1
- package/lib/debug-view/view-policy.ts +2 -1
- package/lib/dev-tools/fr-admin-split-selfvalidation.run.ts +1 -2
- package/lib/eval-harness/conformance/run.ts +4 -4
- package/lib/eval-harness/demo-cascade-smoke.ts +3 -13
- package/lib/eval-harness/gauntlet/cases/jp/regression.jsonl +4 -3
- package/lib/eval-harness/gauntlet/harness.ts +2 -7
- package/lib/eval-harness/oa-resolver/coordinate-tiers.ts +1 -1
- package/lib/eval-harness/oa-resolver/row-score.ts +1 -1
- package/lib/eval-harness/poi-board.ts +4 -3
- package/lib/eval-harness/premise-linkage/adapter.ts +1 -1
- package/lib/eval-harness/v0-tree-adapter.ts +1 -2
- package/lib/gazetteer-pipeline/admin/index.ts +1 -2
- package/lib/gazetteer-pipeline/index.ts +6 -3
- package/lib/gazetteer-pipeline/nsul-layer.ts +1 -2
- package/lib/gazetteer-pipeline/poi/build-poi.ts +1 -1
- package/lib/gazetteer-pipeline/poi/exclusion-coverage.ts +2 -3
- package/lib/gazetteer-pipeline/postcode/geonames-tail.ts +0 -2
- package/lib/gazetteer-pipeline/postcode/index.ts +1 -1
- package/lib/gazetteer-pipeline/tw-districts.ts +369 -0
- package/lib/gazetteer-pipeline/uprn-layer.ts +1 -2
- package/lib/geocode/classifier.ts +73 -0
- package/lib/geocode/core.ts +38 -60
- package/lib/geocode/index.ts +1 -0
- package/lib/geocode/national-overture.ts +145 -0
- package/lib/geocode/regions.ts +1 -1
- package/lib/geocode/session.ts +41 -22
- package/lib/geocode/worker.ts +1 -1
- package/lib/index.ts +0 -1
- package/lib/intent-refusal.ts +1 -1
- package/lib/observations/absence-route.ts +2 -9
- package/lib/observations/layer-record.ts +2 -1
- package/lib/poi/index.ts +0 -1
- package/lib/release-tools/publish-hf.ts +2 -2
- package/lib/resolver-backend.ts +0 -2
- package/lib/runtime-pipeline.ts +12 -27
- package/lib/tiles/publish.ts +120 -0
- package/lib/tools/generate-clients.ts +2 -2
- package/man/mailwoman.1 +1 -1
- package/out/api-engine.js +2 -1
- package/out/api-engine.js.map +1 -1
- package/out/browser-runtime/classify.d.ts +154 -0
- package/out/browser-runtime/classify.d.ts.map +1 -0
- package/out/browser-runtime/classify.js +146 -0
- package/out/browser-runtime/classify.js.map +1 -0
- package/out/browser-runtime/fetch.d.ts +18 -0
- package/out/browser-runtime/fetch.d.ts.map +1 -0
- package/out/browser-runtime/fetch.js +74 -0
- package/out/browser-runtime/fetch.js.map +1 -0
- package/out/browser-runtime/index.d.ts +12 -0
- package/out/browser-runtime/index.d.ts.map +1 -0
- package/out/browser-runtime/index.js +12 -0
- package/out/browser-runtime/index.js.map +1 -0
- package/out/browser-runtime/load-assets.d.ts +73 -0
- package/out/browser-runtime/load-assets.d.ts.map +1 -0
- package/out/browser-runtime/load-assets.js +120 -0
- package/out/browser-runtime/load-assets.js.map +1 -0
- package/out/browser-runtime/manifest.d.ts +76 -0
- package/out/browser-runtime/manifest.d.ts.map +1 -0
- package/out/browser-runtime/manifest.js +46 -0
- package/out/browser-runtime/manifest.js.map +1 -0
- package/out/browser-runtime/resources.d.ts +148 -0
- package/out/browser-runtime/resources.d.ts.map +1 -0
- package/out/browser-runtime/resources.js +323 -0
- package/out/browser-runtime/resources.js.map +1 -0
- package/out/browser-runtime/types.d.ts +130 -0
- package/out/browser-runtime/types.d.ts.map +1 -0
- package/out/browser-runtime/types.js +12 -0
- package/out/browser-runtime/types.js.map +1 -0
- package/out/cli-kit/dropin.d.ts.map +1 -1
- package/out/cli-kit/dropin.js +2 -1
- package/out/cli-kit/dropin.js.map +1 -1
- package/out/cli-kit/index.d.ts +0 -1
- package/out/cli-kit/index.d.ts.map +1 -1
- package/out/cli-kit/index.js +0 -1
- package/out/cli-kit/index.js.map +1 -1
- package/out/cli-kit/shared.d.ts +2 -2
- package/out/cli-kit/shared.d.ts.map +1 -1
- package/out/cli-kit/shared.js +2 -1
- package/out/cli-kit/shared.js.map +1 -1
- package/out/commands/corpus/build.d.ts.map +1 -1
- package/out/commands/corpus/build.js +17 -1
- package/out/commands/corpus/build.js.map +1 -1
- package/out/commands/corpus/fetch.d.ts +11 -1
- package/out/commands/corpus/fetch.d.ts.map +1 -1
- package/out/commands/corpus/fetch.js +18 -1
- package/out/commands/corpus/fetch.js.map +1 -1
- package/out/commands/corpus/run.d.ts.map +1 -1
- package/out/commands/corpus/run.js +2 -1
- package/out/commands/corpus/run.js.map +1 -1
- package/out/commands/corpus/slice/index.d.ts.map +1 -1
- package/out/commands/corpus/slice/index.js +2 -1
- package/out/commands/corpus/slice/index.js.map +1 -1
- package/out/commands/data/pull.d.ts.map +1 -1
- package/out/commands/data/pull.js +2 -1
- package/out/commands/data/pull.js.map +1 -1
- package/out/commands/gazetteer/build/poi-coverage.js +1 -1
- package/out/commands/gazetteer/build/poi-coverage.js.map +1 -1
- package/out/commands/gazetteer/build/postcode-locality.d.ts.map +1 -1
- package/out/commands/gazetteer/build/postcode-locality.js +2 -1
- package/out/commands/gazetteer/build/postcode-locality.js.map +1 -1
- package/out/commands/gazetteer/build/tw-districts.d.ts +50 -0
- package/out/commands/gazetteer/build/tw-districts.d.ts.map +1 -0
- package/out/commands/gazetteer/build/tw-districts.js +54 -0
- package/out/commands/gazetteer/build/tw-districts.js.map +1 -0
- package/out/commands/gazetteer/conventions.d.ts.map +1 -1
- package/out/commands/gazetteer/conventions.js +2 -1
- package/out/commands/gazetteer/conventions.js.map +1 -1
- package/out/commands/gazetteer/importance.d.ts.map +1 -1
- package/out/commands/gazetteer/importance.js +4 -2
- package/out/commands/gazetteer/importance.js.map +1 -1
- package/out/commands/gazetteer/inspect/graph.d.ts.map +1 -1
- package/out/commands/gazetteer/inspect/graph.js +2 -1
- package/out/commands/gazetteer/inspect/graph.js.map +1 -1
- package/out/commands/gazetteer/inspect/mermaid.d.ts.map +1 -1
- package/out/commands/gazetteer/inspect/mermaid.js +2 -1
- package/out/commands/gazetteer/inspect/mermaid.js.map +1 -1
- package/out/commands/gazetteer/inspect/placetype-stats.d.ts.map +1 -1
- package/out/commands/gazetteer/inspect/placetype-stats.js +2 -1
- package/out/commands/gazetteer/inspect/placetype-stats.js.map +1 -1
- package/out/commands/gazetteer/inspect/sync.d.ts.map +1 -1
- package/out/commands/gazetteer/inspect/sync.js +2 -1
- package/out/commands/gazetteer/inspect/sync.js.map +1 -1
- package/out/commands/gazetteer/inspect/tree.d.ts.map +1 -1
- package/out/commands/gazetteer/inspect/tree.js +2 -1
- package/out/commands/gazetteer/inspect/tree.js.map +1 -1
- package/out/commands/gazetteer/overture-ingest.d.ts.map +1 -1
- package/out/commands/gazetteer/overture-ingest.js +2 -1
- package/out/commands/gazetteer/overture-ingest.js.map +1 -1
- package/out/commands/gazetteer/polygons.d.ts.map +1 -1
- package/out/commands/gazetteer/polygons.js +2 -1
- package/out/commands/gazetteer/polygons.js.map +1 -1
- package/out/commands/gazetteer/postcode-intl.d.ts.map +1 -1
- package/out/commands/gazetteer/postcode-intl.js +2 -1
- package/out/commands/gazetteer/postcode-intl.js.map +1 -1
- package/out/commands/gazetteer/publish.js +2 -2
- package/out/commands/gazetteer/publish.js.map +1 -1
- package/out/commands/parse.d.ts.map +1 -1
- package/out/commands/parse.js +20 -6
- package/out/commands/parse.js.map +1 -1
- package/out/commands/poi.d.ts.map +1 -1
- package/out/commands/poi.js +3 -2
- package/out/commands/poi.js.map +1 -1
- package/out/commands/registry/run.d.ts.map +1 -1
- package/out/commands/registry/run.js +2 -1
- package/out/commands/registry/run.js.map +1 -1
- package/out/commands/registry/scorer-eval.js +1 -1
- package/out/commands/registry/scorer-eval.js.map +1 -1
- package/out/commands/situs/address-points.d.ts +22 -3
- package/out/commands/situs/address-points.d.ts.map +1 -1
- package/out/commands/situs/address-points.js +100 -26
- package/out/commands/situs/address-points.js.map +1 -1
- package/out/commands/situs/interpolation-database.d.ts.map +1 -1
- package/out/commands/situs/interpolation-database.js +2 -1
- package/out/commands/situs/interpolation-database.js.map +1 -1
- package/out/commands/situs/interpolation.d.ts.map +1 -1
- package/out/commands/situs/interpolation.js +2 -1
- package/out/commands/situs/interpolation.js.map +1 -1
- package/out/commands/skill/install.d.ts.map +1 -1
- package/out/commands/skill/install.js +2 -1
- package/out/commands/skill/install.js.map +1 -1
- package/out/commands/tiger/fetch.d.ts.map +1 -1
- package/out/commands/tiger/fetch.js +2 -1
- package/out/commands/tiger/fetch.js.map +1 -1
- package/out/commands/tiger/redistricting.d.ts.map +1 -1
- package/out/commands/tiger/redistricting.js +2 -1
- package/out/commands/tiger/redistricting.js.map +1 -1
- package/out/commands/tiles/publish.d.ts +5 -27
- package/out/commands/tiles/publish.d.ts.map +1 -1
- package/out/commands/tiles/publish.js +5 -49
- package/out/commands/tiles/publish.js.map +1 -1
- package/out/data/bundles.d.ts +3 -3
- package/out/data/bundles.js +3 -3
- package/out/debug-view/DebugSessionApp.d.ts.map +1 -1
- package/out/debug-view/DebugSessionApp.js +1 -1
- package/out/debug-view/DebugSessionApp.js.map +1 -1
- package/out/debug-view/command.d.ts.map +1 -1
- package/out/debug-view/command.js +2 -1
- package/out/debug-view/command.js.map +1 -1
- package/out/debug-view/view-policy.d.ts.map +1 -1
- package/out/debug-view/view-policy.js +1 -1
- package/out/debug-view/view-policy.js.map +1 -1
- package/out/dev-tools/fr-admin-split-selfvalidation.run.js.map +1 -1
- package/out/eval-harness/conformance/run.d.ts +4 -4
- package/out/eval-harness/demo-cascade-smoke.d.ts +2 -12
- package/out/eval-harness/demo-cascade-smoke.d.ts.map +1 -1
- package/out/eval-harness/demo-cascade-smoke.js +3 -13
- package/out/eval-harness/demo-cascade-smoke.js.map +1 -1
- package/out/eval-harness/gauntlet/harness.d.ts.map +1 -1
- package/out/eval-harness/gauntlet/harness.js +2 -1
- package/out/eval-harness/gauntlet/harness.js.map +1 -1
- package/out/eval-harness/oa-resolver/assembled-arm.d.ts +1 -1
- package/out/eval-harness/oa-resolver/coordinate-tiers.d.ts +1 -1
- package/out/eval-harness/oa-resolver/coordinate-tiers.d.ts.map +1 -1
- package/out/eval-harness/oa-resolver/parse-rig.d.ts +1 -1
- package/out/eval-harness/oa-resolver/row-score.js +1 -1
- package/out/eval-harness/oa-resolver/row-score.js.map +1 -1
- package/out/eval-harness/poi-board.d.ts.map +1 -1
- package/out/eval-harness/poi-board.js +2 -1
- package/out/eval-harness/poi-board.js.map +1 -1
- package/out/eval-harness/premise-linkage/adapter.d.ts.map +1 -1
- package/out/eval-harness/premise-linkage/adapter.js.map +1 -1
- package/out/eval-harness/v0-tree-adapter.d.ts +1 -1
- package/out/eval-harness/v0-tree-adapter.d.ts.map +1 -1
- package/out/eval-harness/v0-tree-adapter.js.map +1 -1
- package/out/gazetteer-pipeline/admin/index.d.ts.map +1 -1
- package/out/gazetteer-pipeline/admin/index.js +1 -2
- package/out/gazetteer-pipeline/admin/index.js.map +1 -1
- package/out/gazetteer-pipeline/index.d.ts +1 -1
- package/out/gazetteer-pipeline/index.d.ts.map +1 -1
- package/out/gazetteer-pipeline/index.js +5 -2
- package/out/gazetteer-pipeline/index.js.map +1 -1
- package/out/gazetteer-pipeline/nsul-layer.d.ts.map +1 -1
- package/out/gazetteer-pipeline/nsul-layer.js +2 -1
- package/out/gazetteer-pipeline/nsul-layer.js.map +1 -1
- package/out/gazetteer-pipeline/poi/build-poi.d.ts +2 -1
- package/out/gazetteer-pipeline/poi/build-poi.d.ts.map +1 -1
- package/out/gazetteer-pipeline/poi/build-poi.js +2 -1
- package/out/gazetteer-pipeline/poi/build-poi.js.map +1 -1
- package/out/gazetteer-pipeline/poi/exclusion-coverage.d.ts +1 -1
- package/out/gazetteer-pipeline/poi/exclusion-coverage.d.ts.map +1 -1
- package/out/gazetteer-pipeline/poi/exclusion-coverage.js +2 -3
- package/out/gazetteer-pipeline/poi/exclusion-coverage.js.map +1 -1
- package/out/gazetteer-pipeline/postcode/geonames-tail.d.ts +0 -1
- package/out/gazetteer-pipeline/postcode/geonames-tail.d.ts.map +1 -1
- package/out/gazetteer-pipeline/postcode/geonames-tail.js.map +1 -1
- package/out/gazetteer-pipeline/postcode/index.d.ts.map +1 -1
- package/out/gazetteer-pipeline/postcode/index.js +1 -1
- package/out/gazetteer-pipeline/postcode/index.js.map +1 -1
- package/out/gazetteer-pipeline/tw-districts.d.ts +143 -0
- package/out/gazetteer-pipeline/tw-districts.d.ts.map +1 -0
- package/out/gazetteer-pipeline/tw-districts.js +233 -0
- package/out/gazetteer-pipeline/tw-districts.js.map +1 -0
- package/out/gazetteer-pipeline/uprn-layer.d.ts.map +1 -1
- package/out/gazetteer-pipeline/uprn-layer.js +2 -1
- package/out/gazetteer-pipeline/uprn-layer.js.map +1 -1
- package/out/geocode/classifier.d.ts +56 -0
- package/out/geocode/classifier.d.ts.map +1 -0
- package/out/geocode/classifier.js +29 -0
- package/out/geocode/classifier.js.map +1 -0
- package/out/geocode/core.d.ts +4 -31
- package/out/geocode/core.d.ts.map +1 -1
- package/out/geocode/core.js +21 -18
- package/out/geocode/core.js.map +1 -1
- package/out/geocode/index.d.ts +1 -0
- package/out/geocode/index.d.ts.map +1 -1
- package/out/geocode/index.js +1 -0
- package/out/geocode/index.js.map +1 -1
- package/out/geocode/national-overture.d.ts +43 -0
- package/out/geocode/national-overture.d.ts.map +1 -0
- package/out/geocode/national-overture.js +114 -0
- package/out/geocode/national-overture.js.map +1 -0
- package/out/geocode/regions.d.ts +1 -1
- package/out/geocode/regions.d.ts.map +1 -1
- package/out/geocode/session.d.ts +1 -1
- package/out/geocode/session.d.ts.map +1 -1
- package/out/geocode/session.js +33 -14
- package/out/geocode/session.js.map +1 -1
- package/out/geocode/worker.js +1 -1
- package/out/geocode/worker.js.map +1 -1
- package/out/index.d.ts +0 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js +0 -1
- package/out/index.js.map +1 -1
- package/out/intent-refusal.d.ts +1 -1
- package/out/intent-refusal.js +1 -1
- package/out/observations/absence-route.d.ts +2 -1
- package/out/observations/absence-route.d.ts.map +1 -1
- package/out/observations/absence-route.js +2 -1
- package/out/observations/absence-route.js.map +1 -1
- package/out/observations/layer-record.d.ts +2 -1
- package/out/observations/layer-record.d.ts.map +1 -1
- package/out/observations/layer-record.js.map +1 -1
- package/out/poi/index.d.ts +0 -1
- package/out/poi/index.d.ts.map +1 -1
- package/out/poi/index.js +0 -1
- package/out/poi/index.js.map +1 -1
- package/out/release-tools/publish-hf.js +2 -2
- package/out/release-tools/publish-hf.js.map +1 -1
- package/out/resolver-backend.d.ts +1 -1
- package/out/resolver-backend.d.ts.map +1 -1
- package/out/resolver-backend.js +0 -1
- package/out/resolver-backend.js.map +1 -1
- package/out/runtime-pipeline.d.ts +0 -2
- package/out/runtime-pipeline.d.ts.map +1 -1
- package/out/runtime-pipeline.js +11 -4
- package/out/runtime-pipeline.js.map +1 -1
- package/out/tiles/publish.d.ts +70 -0
- package/out/tiles/publish.d.ts.map +1 -0
- package/out/tiles/publish.js +71 -0
- package/out/tiles/publish.js.map +1 -0
- package/out/tools/generate-clients.js +2 -2
- package/out/tools/generate-clients.js.map +1 -1
- package/package.json +117 -37
- package/lib/gazetteer-pipeline/poi/coverage-region.ts +0 -21
- package/lib/poi/overpass.ts +0 -11
- package/out/gazetteer-pipeline/poi/coverage-region.d.ts +0 -21
- package/out/gazetteer-pipeline/poi/coverage-region.d.ts.map +0 -1
- package/out/gazetteer-pipeline/poi/coverage-region.js +0 -21
- package/out/gazetteer-pipeline/poi/coverage-region.js.map +0 -1
- package/out/poi/overpass.d.ts +0 -11
- package/out/poi/overpass.d.ts.map +0 -1
- package/out/poi/overpass.js +0 -11
- package/out/poi/overpass.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,14 +14,15 @@ npx mailwoman parse "1600 Amphitheatre Parkway, Mountain View, CA 94043"
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
// Library — parse programmatically
|
|
17
|
-
import {
|
|
17
|
+
import { decodeAsJSON } from "@mailwoman/core/decoder"
|
|
18
|
+
import { createRuntimePipeline } from "mailwoman"
|
|
18
19
|
import { NeuralAddressClassifier } from "@mailwoman/neural"
|
|
19
20
|
|
|
20
21
|
const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" })
|
|
21
22
|
const parse = createRuntimePipeline({ classifier })
|
|
22
23
|
|
|
23
24
|
const { tree } = await parse("1600 Amphitheatre Parkway, Mountain View, CA 94043")
|
|
24
|
-
console.log(
|
|
25
|
+
console.log(decodeAsJSON(tree))
|
|
25
26
|
// { region: "CA", locality: "Mountain View", street: "Amphitheatre",
|
|
26
27
|
// house_number: "1600", street_suffix: "Parkway", postcode: "94043" }
|
|
27
28
|
```
|
|
@@ -89,7 +90,8 @@ up normalization, locale detection, kind classification, phrase grouping, and to
|
|
|
89
90
|
classification with production-ready defaults.
|
|
90
91
|
|
|
91
92
|
```ts
|
|
92
|
-
import {
|
|
93
|
+
import { decodeAsJSON, decodeAsTuples } from "@mailwoman/core/decoder"
|
|
94
|
+
import { createRuntimePipeline } from "mailwoman"
|
|
93
95
|
import { NeuralAddressClassifier } from "@mailwoman/neural"
|
|
94
96
|
|
|
95
97
|
const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" })
|
|
@@ -111,7 +113,7 @@ result.timing // per-stage wall-clock breakdown
|
|
|
111
113
|
Project the tree into the shape you need:
|
|
112
114
|
|
|
113
115
|
```ts
|
|
114
|
-
|
|
116
|
+
decodeAsJSON(result.tree)
|
|
115
117
|
// { region: "NY", locality: "New York", street: "5th",
|
|
116
118
|
// house_number: "350", street_suffix: "Ave", postcode: "10118" }
|
|
117
119
|
|
package/lib/api-engine.ts
CHANGED
|
@@ -40,9 +40,11 @@ import { pathExists, readDirectory, readLocalTextFile } from "@mailwoman/core/fs
|
|
|
40
40
|
import { tryParsingJSON } from "@mailwoman/core/json"
|
|
41
41
|
import { resolveModulePath } from "@mailwoman/core/module/resolvers"
|
|
42
42
|
import { deriveInputMode } from "@mailwoman/core/pipeline"
|
|
43
|
+
import type { Resolver, ResolveOpts } from "@mailwoman/core/resolver"
|
|
44
|
+
import { mailwomanDataRoot } from "@mailwoman/core/utils"
|
|
43
45
|
import { classifyKindSync } from "@mailwoman/kind-classifier"
|
|
44
46
|
import { computeQueryShape } from "@mailwoman/query-shape"
|
|
45
|
-
import { createWOFResolver
|
|
47
|
+
import { createWOFResolver } from "@mailwoman/resolver"
|
|
46
48
|
|
|
47
49
|
import { readReleaseManifest } from "#data/release"
|
|
48
50
|
import { $public } from "#env"
|
|
@@ -53,10 +55,8 @@ import {
|
|
|
53
55
|
buildNoGazetteerMessage,
|
|
54
56
|
createResolverBackend,
|
|
55
57
|
existingWOFDatabasePaths,
|
|
56
|
-
mailwomanDataRoot,
|
|
57
58
|
resolveCandidateDBPath,
|
|
58
59
|
} from "#resolver-backend"
|
|
59
|
-
|
|
60
60
|
/**
|
|
61
61
|
* Default per-state database root + interp calibration — mirrors the express server's defaults (`GeocodeRouter.ts`).
|
|
62
62
|
*/
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The classify stage of the browser runtime: query shape, kind, the neural pipeline and the tree flatten, with the
|
|
7
|
+
* two front-half timings captured, plus the dual-role resolution and the projections a renderer takes. The caller
|
|
8
|
+
* owns resolution (`runCascade`, a street tier, an anchor fallback) and the staged progress ticks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// STATIC on purpose: a dynamic-import destructure of this barrel gets tree-shaken by webpack's
|
|
12
|
+
// usedExports analysis, which once shipped the WOF cascade as `TypeError: i is not a function`.
|
|
13
|
+
// Static named imports are fully analyzable; do not re-dynamize.
|
|
14
|
+
import { type FlatTreeNode, flattenTreeNodes } from "@mailwoman/core/decoder"
|
|
15
|
+
import type { AddressTree } from "@mailwoman/core/decoder/types"
|
|
16
|
+
import type { ParseResult, ResolvedPlaceView } from "@mailwoman/core/pipeline/client-result"
|
|
17
|
+
import type { DualRole, MailwomanLookupLike } from "@mailwoman/resolver-wof-wasm/browser-cascade"
|
|
18
|
+
|
|
19
|
+
import type { FSTMatcherLike, MailwomanClassifierLike } from "#browser-runtime/types"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Project the cascade's hits onto the package's {@link ResolvedPlaceView} — the shape `useParsePipeline` and the result
|
|
23
|
+
* panels consume. Shared by both demo parse paths so the projection cannot drift.
|
|
24
|
+
*/
|
|
25
|
+
export function projectCascadeHits(
|
|
26
|
+
hits: ReadonlyArray<{ id: number; name: string; placetype: string; lat: number; lon: number; score: number }>
|
|
27
|
+
): ResolvedPlaceView[] {
|
|
28
|
+
return hits.map((c) => ({ id: c.id, name: c.name, placetype: c.placetype, lat: c.lat, lon: c.lon, score: c.score }))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-parse progress labels, keyed on whether a gazetteer lookup is wired for the selected release.
|
|
33
|
+
*/
|
|
34
|
+
export function parseStageLabelsFor(hasResolver: boolean): string[] {
|
|
35
|
+
return hasResolver
|
|
36
|
+
? ["Analyzing input shape…", "Running neural classifier…", "Resolving in gazetteer…"]
|
|
37
|
+
: ["Analyzing input shape…", "Running neural classifier…"]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#region Constants
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Locale the demo opens on.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_LOCALE = "en-us"
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Address the demo opens on — it exercises house number, street, directional, locality and region in one line.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_ADDRESS = "1600 Pennsylvania Ave NW, Washington, DC 20500"
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Demo preset addresses. Each carries its `country` (ISO code) — the placetype-pair-prior country pin (#1278 phase 2's
|
|
54
|
+
* `{country}` override): structural routing (locale-check) genuinely can't detect every locale from text shape (NZ's
|
|
55
|
+
* 4-digit postcode isn't distinctive), so a preset PINS its country and {@link pairCountryForInput} hands it to
|
|
56
|
+
* `selectPairIndexForText` when the input still equals the preset text. Free-typed input (no exact preset match) falls
|
|
57
|
+
* back to structural detection — GB-with-postcode auto-fires; NZ free-text stays unfired (the accepted consequence of
|
|
58
|
+
* structural routing, the #1308-sibling reality).
|
|
59
|
+
*
|
|
60
|
+
* See also `PIPELINE_PRESETS` in `@mailwoman/react` (pipeline/presets.ts) — the package carries its own preset list;
|
|
61
|
+
* reconciling the two lists is tracked outside this file.
|
|
62
|
+
*/
|
|
63
|
+
export const EXAMPLE_ADDRESSES: Array<{ label: string; address: string; country: string }> = [
|
|
64
|
+
{ label: "White House", address: "1600 Pennsylvania Ave NW, Washington, DC 20500", country: "us" },
|
|
65
|
+
{ label: "Apple Park", address: "1 Apple Park Way, Cupertino, CA 95014", country: "us" },
|
|
66
|
+
{ label: "30 Rockefeller Plaza", address: "30 Rockefeller Plaza, New York, NY 10112", country: "us" },
|
|
67
|
+
{ label: "Pier 39 SF", address: "Pier 39, San Francisco, CA 94133", country: "us" },
|
|
68
|
+
{ label: "Wrigley Field", address: "1060 W Addison St, Chicago, IL 60613", country: "us" },
|
|
69
|
+
{ label: "Space Needle", address: "400 Broad St, Seattle, WA 98109", country: "us" },
|
|
70
|
+
{ label: "ZIP only", address: "90210", country: "us" },
|
|
71
|
+
{ label: "Berlin (native order)", address: "Straußstraße 27, 12623 Berlin", country: "de" },
|
|
72
|
+
{ label: "Berlin city-state (int'l order)", address: "5 Hauptstraße, Berlin, Berlin 10115", country: "de" },
|
|
73
|
+
{ label: "Paris (street fall-through)", address: "181 Rue du Chevaleret, Paris", country: "fr" },
|
|
74
|
+
// GB dependent_locality (placetype-pair-prior arc) — a verified `gb-golden` board row
|
|
75
|
+
// (mailwoman/eval-harness/fixtures/gb-golden.jsonl). "Henbury" flips to dependent_locality via the en-gb pair-index
|
|
76
|
+
// prior; the `country: "gb"` pin selects it even if the user edits away the postcode (and structural detection also
|
|
77
|
+
// picks gb while the UK postcode is present).
|
|
78
|
+
{
|
|
79
|
+
label: "Macclesfield (GB dependent_locality)",
|
|
80
|
+
address: "41 Hightree Drive, Henbury, Macclesfield, SK11 9PD",
|
|
81
|
+
country: "gb",
|
|
82
|
+
},
|
|
83
|
+
// NZ dependent_locality (en-nz pair-prior arc) — Plimmerton is a suburb (dependent_locality) of Porirua. Postcode
|
|
84
|
+
// DELIBERATELY OMITTED: a trailing "Porirua 5026" puts the postcode in the parent's comma-field, so segment mode
|
|
85
|
+
// folds "porirua 5026" and misses the index's bare "porirua" key (the shipped GB artifact misses the same way) —
|
|
86
|
+
// tracked as #1308. The `country: "nz"` pin is required here: locale-check can't structurally detect NZ (4-digit
|
|
87
|
+
// postcode isn't a distinctive format), so ONLY the preset pin selects the nz index — free-typed NZ stays unfired.
|
|
88
|
+
{ label: "Plimmerton (NZ dependent_locality)", address: "35 Steyne Avenue, Plimmerton, Porirua", country: "nz" },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The placetype-pair country PIN for one input (#1278 phase 2's `{country}` override). Returns a preset's `country`
|
|
93
|
+
* when `input` still exactly equals that preset's text (trimmed), else `undefined` → the caller lets structural
|
|
94
|
+
* detection decide. This is the stale-pin rule: the pin is tied to the preset's IDENTITY (its text), so the instant the
|
|
95
|
+
* user edits the input it no longer matches and the parse drops to structural locale-check detection — clean and
|
|
96
|
+
* stateless (no "active preset" tracking to drift). GB-with-postcode still auto-fires structurally; NZ free-text stays
|
|
97
|
+
* unfired.
|
|
98
|
+
*/
|
|
99
|
+
export function pairCountryForInput(input: string): string | undefined {
|
|
100
|
+
const trimmed = input.trim()
|
|
101
|
+
|
|
102
|
+
return EXAMPLE_ADDRESSES.find((ex) => ex.address.trim() === trimmed)?.country
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
|
|
107
|
+
//#region Parse orchestration
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A source-order parsed node, as {@link flattenTreeNodes} yields it.
|
|
111
|
+
*/
|
|
112
|
+
export type FlatNode = FlatTreeNode
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What both demo parse paths need out of the neural classify stage before resolution.
|
|
116
|
+
*/
|
|
117
|
+
export interface ClassifyStageResult {
|
|
118
|
+
/**
|
|
119
|
+
* The decoded solver tree (opaque to the caller beyond `runCascade` / `flattenTreeNodes`).
|
|
120
|
+
*/
|
|
121
|
+
tree: AddressTree
|
|
122
|
+
/**
|
|
123
|
+
* Source-order flattened nodes.
|
|
124
|
+
*/
|
|
125
|
+
nodes: FlatNode[]
|
|
126
|
+
/**
|
|
127
|
+
* The query-shape kind hypothesis (`postcode_only` / `structured_address` / …).
|
|
128
|
+
*/
|
|
129
|
+
kindResult: ParseResult["kindResult"]
|
|
130
|
+
/**
|
|
131
|
+
* Wall-clock timing for the two front-half stages, in ms.
|
|
132
|
+
*/
|
|
133
|
+
timing: { shape: number; classify: number }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The neural classify front-half, shared by BOTH demo parse paths (the `/demo` map's `runParseWithBias` and the
|
|
138
|
+
* MDX-embed `PipelineExplorer`'s `runParse`): the 4-way pipeline import, the query-shape + kind pass, the neural
|
|
139
|
+
* `runPipeline`, and the tree flatten — with the two front-half timings captured. The caller owns resolution
|
|
140
|
+
* (`runCascade`, plus the map path's street tier / anchor fallback) and the staged `onStage` progress ticks.
|
|
141
|
+
*
|
|
142
|
+
* #1278: this is the single point the locale-check pre-parse plugs into. A future per-parse country / conventions hint
|
|
143
|
+
* is threaded through {@link ClassifyStageDeps} into the `runPipeline` call HERE — one insertion point for both paths.
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Per-parse placetype-pair prior selector (placetype-pair-prior arc, #1278), the shape the web loader's
|
|
147
|
+
* `LoadResult.selectPairIndexForText` exposes. Given the input text it runs locale-check over the text SHAPE (postcode
|
|
148
|
+
* format / script, never place names) and returns the matching loaded index wrapped as an opaque `placetypePair` opt —
|
|
149
|
+
* or `undefined` when no loaded index matches (byte-stable no-prior). Typed opaquely here because the docs bundle
|
|
150
|
+
* carries no neural type dependency; `runClassifyStage` threads the result verbatim into the pipeline's `placetypePair`
|
|
151
|
+
* opt.
|
|
152
|
+
*/
|
|
153
|
+
export type SelectPairIndex = (text: string, opts?: { country?: string }) => object | undefined
|
|
154
|
+
|
|
155
|
+
export interface ClassifyStageDeps {
|
|
156
|
+
/**
|
|
157
|
+
* The loaded neural classifier (must be ready — the caller guards `null`).
|
|
158
|
+
*/
|
|
159
|
+
classifier: MailwomanClassifierLike
|
|
160
|
+
/**
|
|
161
|
+
* The optional FST gazetteer prior.
|
|
162
|
+
*/
|
|
163
|
+
fst?: FSTMatcherLike | null
|
|
164
|
+
/**
|
|
165
|
+
* The optional street-morphology matcher — the #1315 street-context check's signal source. The check only fires when
|
|
166
|
+
* BOTH this and `fst` are wired (core's `streetContextRequirementFor`), matching the node runtime pipeline's default;
|
|
167
|
+
* a `null`/omitted matcher parses with the check off, exactly the pre-artifact demo behavior.
|
|
168
|
+
*/
|
|
169
|
+
streetMorphology?: FSTMatcherLike | null
|
|
170
|
+
/**
|
|
171
|
+
* The per-parse placetype-pair prior selector (#1278) — the loaded {@link SelectPairIndex}, or `null`/omitted when no
|
|
172
|
+
* pair index was staged for this release. When present, `runClassifyStage` calls it on the input and passes the
|
|
173
|
+
* result as the pipeline's `placetypePair` opt; the GB/NZ dependent_locality prior fires while US/FR inputs stay
|
|
174
|
+
* byte-stable.
|
|
175
|
+
*/
|
|
176
|
+
selectPairIndex?: SelectPairIndex | null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface ClassifyStageHooks {
|
|
180
|
+
/**
|
|
181
|
+
* Fired after the (synchronous) query-shape + kind pass, immediately before the neural pipeline runs. Drives the
|
|
182
|
+
* staged progress UI's "Running neural classifier…" transition. Both callers wire it to their `onStage(1)`.
|
|
183
|
+
*/
|
|
184
|
+
onClassifierStart?: () => void
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Run the shared classify front-half. See {@link ClassifyStageResult} / {@link ClassifyStageDeps}.
|
|
189
|
+
*/
|
|
190
|
+
export async function runClassifyStage(
|
|
191
|
+
input: string,
|
|
192
|
+
deps: ClassifyStageDeps,
|
|
193
|
+
hooks: ClassifyStageHooks = {}
|
|
194
|
+
): Promise<ClassifyStageResult> {
|
|
195
|
+
const [{ computeQueryShape }, { classifyKindSync }, { runPipeline }, { groupPhrases }] = await Promise.all([
|
|
196
|
+
import("@mailwoman/query-shape"),
|
|
197
|
+
import("@mailwoman/kind-classifier"),
|
|
198
|
+
import("@mailwoman/core/pipeline"),
|
|
199
|
+
import("@mailwoman/phrase-grouper"),
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
const tStart = performance.now()
|
|
203
|
+
const queryShape = computeQueryShape(input)
|
|
204
|
+
const kindResult = classifyKindSync({ raw: input, normalized: input }, queryShape)
|
|
205
|
+
const tShape = performance.now()
|
|
206
|
+
|
|
207
|
+
hooks.onClassifierStart?.()
|
|
208
|
+
|
|
209
|
+
// Placetype-pair prior (#1278): pick the per-parse index. A preset PINS its country (pairCountryForInput) — the
|
|
210
|
+
// phase-2 `{country}` override — so a locale structural routing can't detect from text (NZ: 4-digit postcode isn't
|
|
211
|
+
// distinctive) still fires while the input equals the preset text; the moment the user edits, the pin drops and we
|
|
212
|
+
// fall back to structural locale-check detection (GB-with-postcode auto-fires; NZ free-text stays unfired, the
|
|
213
|
+
// accepted #1308-sibling consequence of structural routing). No index / no match → undefined → byte-stable.
|
|
214
|
+
const pinnedCountry = pairCountryForInput(input)
|
|
215
|
+
const placetypePair = deps.selectPairIndex?.(input, pinnedCountry ? { country: pinnedCountry } : undefined)
|
|
216
|
+
|
|
217
|
+
const { tree } = await runPipeline(
|
|
218
|
+
input,
|
|
219
|
+
{
|
|
220
|
+
computeQueryShape,
|
|
221
|
+
groupPhrases,
|
|
222
|
+
classifier: deps.classifier as Parameters<typeof runPipeline>[1]["classifier"],
|
|
223
|
+
fst: (deps.fst ?? undefined) as Parameters<typeof runPipeline>[1]["fst"],
|
|
224
|
+
streetMorphology: (deps.streetMorphology ?? undefined) as Parameters<typeof runPipeline>[1]["streetMorphology"],
|
|
225
|
+
},
|
|
226
|
+
// Decision A endpoint default: the demo search box is a human typing fragments — the
|
|
227
|
+
// fragmented register, so the evidence-bundle channels feed (once a bundle model ships).
|
|
228
|
+
{ inputMode: "fragmented", ...(placetypePair !== undefined ? { placetypePair } : {}) }
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
const tClassify = performance.now()
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
tree,
|
|
235
|
+
nodes: flattenTreeNodes(tree),
|
|
236
|
+
kindResult: kindResult as ParseResult["kindResult"],
|
|
237
|
+
timing: { shape: tShape - tStart, classify: tClassify - tShape },
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Dual-role (#402) resolution, shared by both parse paths: whether the resolved pin doubles as another admin tier.
|
|
243
|
+
* Best-effort + optional — a placeless pin (`id === 0`, e.g. the map path's anchor/street synthesized hits), a lookup
|
|
244
|
+
* with no `coincidentRolesFor`, an empty relation, or a failed query all resolve to `undefined` (no dual-role badge).
|
|
245
|
+
*/
|
|
246
|
+
export async function resolveDualRoles(
|
|
247
|
+
lookup: MailwomanLookupLike,
|
|
248
|
+
primaryHit: { id: number } | undefined
|
|
249
|
+
): Promise<DualRole[] | undefined> {
|
|
250
|
+
if (!primaryHit || !primaryHit.id || !lookup.coincidentRolesFor) return undefined
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const roles = await lookup.coincidentRolesFor(primaryHit.id)
|
|
254
|
+
|
|
255
|
+
return roles.length ? roles : undefined
|
|
256
|
+
} catch {
|
|
257
|
+
// relation absent / query failed → no dual-role badge
|
|
258
|
+
return undefined
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The fetch the browser runtime loads its artifacts through. The data origin resets a long download now and then
|
|
7
|
+
* (measured: 1 of 18 cold loads lost the 38 MB model to `net::ERR_CONNECTION_RESET` 4.9 s in, and 2 of 16 loads
|
|
8
|
+
* failed under a two-attempt retry that covered the connection alone), and a reset that far in rejects the BODY
|
|
9
|
+
* read, not the `fetch()` call. So the body is buffered here, inside the retry, and the loader receives a response
|
|
10
|
+
* whose bytes are already complete. An HTTP status is never retried: a 404 is an answer, and the loaders decide what
|
|
11
|
+
* an absent artifact means.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The number of attempts a network failure gets, the first included.
|
|
16
|
+
*/
|
|
17
|
+
const ATTEMPTS = 3
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The pause before the second attempt; each later attempt doubles it.
|
|
21
|
+
*/
|
|
22
|
+
const FIRST_RETRY_DELAY_MS = 500
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `fetch` rejects with a `TypeError` for a network failure (a reset, a refused connection, a CORS refusal) and never
|
|
26
|
+
* for an HTTP status; a body read that loses its connection rejects the same way. Anything else (an abort, a bad URL)
|
|
27
|
+
* is not retried.
|
|
28
|
+
*/
|
|
29
|
+
function isNetworkFailure(error: unknown): boolean {
|
|
30
|
+
return error instanceof TypeError
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One attempt: the request, and for a successful status the whole body, so a connection lost mid-download is this
|
|
35
|
+
* attempt's failure and not the caller's.
|
|
36
|
+
*/
|
|
37
|
+
async function fetchComplete(
|
|
38
|
+
input: Parameters<typeof fetch>[0],
|
|
39
|
+
init?: Parameters<typeof fetch>[1]
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
const response = await fetch(input, init)
|
|
42
|
+
|
|
43
|
+
if (!response.ok || !response.body) return response
|
|
44
|
+
|
|
45
|
+
return new Response(await response.arrayBuffer(), {
|
|
46
|
+
status: response.status,
|
|
47
|
+
statusText: response.statusText,
|
|
48
|
+
headers: response.headers,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pause(ms: number, signal: AbortSignal | null | undefined): Promise<void> {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
signal?.removeEventListener("abort", onAbort)
|
|
56
|
+
resolve()
|
|
57
|
+
}, ms)
|
|
58
|
+
|
|
59
|
+
function onAbort(): void {
|
|
60
|
+
clearTimeout(timer)
|
|
61
|
+
reject(signal?.reason instanceof Error ? signal.reason : new DOMException("aborted", "AbortError"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
signal?.addEventListener("abort", onAbort, { once: true })
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `fetch` with the body already read, retried on a network failure. Same signature, so a loader takes it as its
|
|
70
|
+
* `fetchImpl`; the response it answers with can be read as bytes, text or JSON exactly as a live one.
|
|
71
|
+
*/
|
|
72
|
+
export async function fetchWithRetry(
|
|
73
|
+
input: Parameters<typeof fetch>[0],
|
|
74
|
+
init?: Parameters<typeof fetch>[1]
|
|
75
|
+
): Promise<Response> {
|
|
76
|
+
let attempt = 1
|
|
77
|
+
|
|
78
|
+
for (;;) {
|
|
79
|
+
try {
|
|
80
|
+
return await fetchComplete(input, init)
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (attempt >= ATTEMPTS || !isNetworkFailure(error) || init?.signal?.aborted) throw error
|
|
83
|
+
|
|
84
|
+
await pause(FIRST_RETRY_DELAY_MS * 2 ** (attempt - 1), init?.signal)
|
|
85
|
+
|
|
86
|
+
attempt++
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The browser runtime: what a host needs to load one published release into a working geocoder in a browser, from
|
|
7
|
+
* the public data origin. The Node counterpart is `createRuntimePipeline`; this composes the same packages for the
|
|
8
|
+
* client, and it is the one module here that a bundler must be able to build under the `browser` condition.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { loadReleaseAssets } from "#browser-runtime/load-assets"
|
|
12
|
+
export { fetchReleasesManifest } from "#browser-runtime/manifest"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The per-release asset loader: the classifier, the calibration table, the FST gazetteer and street-morphology
|
|
7
|
+
* matchers, the pair indexes, and the byte-range gazetteer lookup for one published release, reporting staged
|
|
8
|
+
* progress through {@link AssetLoadProgress}. The host owns the terminal ready/error state and reveals the returned
|
|
9
|
+
* bundle atomically. The onnxruntime-web and sql.js-httpvfs imports stay dynamic so a host that never loads a
|
|
10
|
+
* release never pays for them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { CalibrationTable, Calibrator } from "@mailwoman/core/decoder/calibration"
|
|
14
|
+
import { createCalibrator } from "@mailwoman/core/decoder/calibration"
|
|
15
|
+
import type { MailwomanLookupLike } from "@mailwoman/resolver-wof-wasm/browser-cascade"
|
|
16
|
+
|
|
17
|
+
import type { SelectPairIndex } from "#browser-runtime/classify"
|
|
18
|
+
import { DEFAULT_LOCALE } from "#browser-runtime/classify"
|
|
19
|
+
import { fetchWithRetry } from "#browser-runtime/fetch"
|
|
20
|
+
import type { ReleaseInfo } from "#browser-runtime/manifest"
|
|
21
|
+
import {
|
|
22
|
+
adminGazetteerURL,
|
|
23
|
+
assetURL,
|
|
24
|
+
loadFSTGazetteer,
|
|
25
|
+
loadStreetMorphologyFST,
|
|
26
|
+
neuralClassifierLoadURLs,
|
|
27
|
+
PAIR_INDEX_VERSION,
|
|
28
|
+
pairIndexBaseURL,
|
|
29
|
+
pairIndexURLs,
|
|
30
|
+
} from "#browser-runtime/resources"
|
|
31
|
+
import type {
|
|
32
|
+
AssetLoadProgress,
|
|
33
|
+
FSTMatcherLike,
|
|
34
|
+
FSTProvenanceLike,
|
|
35
|
+
MailwomanClassifierLike,
|
|
36
|
+
} from "#browser-runtime/types"
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What one release loads to: every field a host may read. A host that resolves admin-only ignores `anchorLookup`, and
|
|
40
|
+
* loading it is free because the anchor binaries are already fetched by the classifier load for an anchor-trained
|
|
41
|
+
* bundle.
|
|
42
|
+
*/
|
|
43
|
+
export interface ReleaseAssets {
|
|
44
|
+
classifier: MailwomanClassifierLike
|
|
45
|
+
/**
|
|
46
|
+
* Postcode-anchor centroid lookup (US ZIP → real centroid), for the postcode-only dead-end fallback.
|
|
47
|
+
*/
|
|
48
|
+
anchorLookup: Map<string, { lat: number; lon: number }> | null
|
|
49
|
+
fstMatcher: FSTMatcherLike | null
|
|
50
|
+
fstProvenance: FSTProvenanceLike | null
|
|
51
|
+
/**
|
|
52
|
+
* The street-morphology matcher — the #1315 street-context check's signal source, the node/browser parity fix (SCOPE
|
|
53
|
+
* invariant 2: node runtimes wire this by default; the browser previously never could). Loaded with the FST gazetteer
|
|
54
|
+
* because the check needs BOTH (core's `streetContextRequirementFor` only fires when the two stages are present).
|
|
55
|
+
* `null` when the release ships no `fst-street-morphology.bin` (pre-artifact bundles) — the demo then parses without
|
|
56
|
+
* the check, byte-identical to before.
|
|
57
|
+
*/
|
|
58
|
+
streetMorphologyMatcher: FSTMatcherLike | null
|
|
59
|
+
/**
|
|
60
|
+
* The byte-range gazetteer lookup. `null` when the release ships no gazetteer, when the host asked for none, or when
|
|
61
|
+
* the load failed.
|
|
62
|
+
*/
|
|
63
|
+
lookup: MailwomanLookupLike | null
|
|
64
|
+
calibrator: Calibrator | null
|
|
65
|
+
/**
|
|
66
|
+
* Per-parse placetype-pair prior selection (placetype-pair-prior arc, #1278). Runs locale-check over the input text
|
|
67
|
+
* and returns the loaded index whose header country matches (or `undefined` → byte-stable no-prior). Both demo parse
|
|
68
|
+
* paths thread this into `runClassifyStage` so a GB/NZ input gets its dependent_locality-resurrecting prior. `null`
|
|
69
|
+
* when no pair index was staged/loaded for this release (older bundles) — the loader then behaves exactly as before.
|
|
70
|
+
*/
|
|
71
|
+
selectPairIndex: SelectPairIndex | null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LoadReleaseAssetsOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Load the byte-range gazetteer lookup, given the same-origin base for the sql.js-httpvfs worker and wasm (for
|
|
77
|
+
* example `/mailwoman/sqljs`). Omit it and `lookup` is `null` without a gazetteer step.
|
|
78
|
+
*/
|
|
79
|
+
gazetteer?: { sqljsBaseURL: string }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load the classifier, calibration, FST and gazetteer bundle for one release. Reports staged progress through
|
|
84
|
+
* `progress`; the host owns the terminal ready/error state and reveals the returned bundle atomically.
|
|
85
|
+
*
|
|
86
|
+
* @param release The selected release descriptor (drives which optional assets are fetched).
|
|
87
|
+
* @param progress The host's progress and abort surface.
|
|
88
|
+
* @param options Which optional assets the host wants beyond the release's own.
|
|
89
|
+
*/
|
|
90
|
+
export async function loadReleaseAssets(
|
|
91
|
+
release: ReleaseInfo,
|
|
92
|
+
progress: AssetLoadProgress,
|
|
93
|
+
options: LoadReleaseAssetsOptions = {}
|
|
94
|
+
): Promise<ReleaseAssets> {
|
|
95
|
+
const gazetteer = release.hasWOFDB ? options.gazetteer : undefined
|
|
96
|
+
|
|
97
|
+
progress.setProgress(`Loading ${release.version} model (~${release.modelSize ?? "?"})…`)
|
|
98
|
+
|
|
99
|
+
// Build staged step labels based on what this release includes.
|
|
100
|
+
const steps: string[] = ["Loading classifier"]
|
|
101
|
+
|
|
102
|
+
if (release.hasFST) {
|
|
103
|
+
steps.push("Loading FST gazetteer")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (gazetteer) {
|
|
107
|
+
steps.push("Loading WOF database")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
progress.setStepLabels(steps)
|
|
111
|
+
|
|
112
|
+
// The pair indexes live on the public bucket beside every other model asset, under a dated generation: the objects
|
|
113
|
+
// ship an immutable Cache-Control, so a rebuilt index is readable only from a fresh path.
|
|
114
|
+
const pairIndexBase = pairIndexBaseURL(PAIR_INDEX_VERSION)
|
|
115
|
+
|
|
116
|
+
// Dynamic so the onnxruntime-web chunk loads only when a release does. The result is narrowed to the structural
|
|
117
|
+
// classifier contract this module exposes, so the neural package's own classifier type never enters a host bundle.
|
|
118
|
+
const { loadNeuralClassifierFromURLs } = await import("@mailwoman/neural/web-loader")
|
|
119
|
+
|
|
120
|
+
const { classifier, diagnostics, postcodeAnchorLookup, selectPairIndexForText } = (await loadNeuralClassifierFromURLs(
|
|
121
|
+
{
|
|
122
|
+
...neuralClassifierLoadURLs(DEFAULT_LOCALE, release.version, {
|
|
123
|
+
hasAnchor: release.hasAnchor,
|
|
124
|
+
forceWASM: progress.forceWASM,
|
|
125
|
+
}),
|
|
126
|
+
fetchImpl: fetchWithRetry,
|
|
127
|
+
// Every published pair index is loaded; the loader keeps each live and `selectPairIndexForText` picks per
|
|
128
|
+
// parse. Fetched tolerantly: a 404 is skipped, so a missing binary means no prior, never a failed load.
|
|
129
|
+
pairIndexURLs: pairIndexURLs(pairIndexBase),
|
|
130
|
+
}
|
|
131
|
+
)) as {
|
|
132
|
+
classifier: MailwomanClassifierLike
|
|
133
|
+
diagnostics?: { backend: string; modelBytes: number } | null
|
|
134
|
+
postcodeAnchorLookup?: Map<string, { lat: number; lon: number }> | null
|
|
135
|
+
selectPairIndexForText?: SelectPairIndex | null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
progress.setBackend(
|
|
139
|
+
diagnostics ? `${diagnostics.backend} (${(diagnostics.modelBytes / 1024 / 1024).toFixed(0)} MB int8)` : "unknown"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
progress.setStepIndex(0)
|
|
143
|
+
|
|
144
|
+
// The calibration table is the model's own held-out reliability, so it must match the loaded version. A release
|
|
145
|
+
// without one leaves `calibrator` null and the host shows raw softmax scores.
|
|
146
|
+
let calibrator: Calibrator | null = null
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const calRes = await fetchWithRetry(assetURL(DEFAULT_LOCALE, release.version, "calibration.json"))
|
|
150
|
+
|
|
151
|
+
if (calRes.ok) {
|
|
152
|
+
calibrator = createCalibrator((await calRes.json()) as CalibrationTable)
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
// No calibration table for this version — raw scores it is.
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let fstMatcher: FSTMatcherLike | null = null
|
|
159
|
+
let fstProvenance: FSTProvenanceLike | null = null
|
|
160
|
+
let streetMorphologyMatcher: FSTMatcherLike | null = null
|
|
161
|
+
|
|
162
|
+
if (release.hasFST) {
|
|
163
|
+
try {
|
|
164
|
+
const fstResult = await loadFSTGazetteer(DEFAULT_LOCALE, release.version)
|
|
165
|
+
fstMatcher = fstResult.matcher
|
|
166
|
+
fstProvenance = fstResult.provenance ?? null
|
|
167
|
+
} catch {
|
|
168
|
+
// FST not available for this version.
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The street-context check needs both matchers, so the morphology matcher is loaded only once the gazetteer FST
|
|
172
|
+
// is; a release that predates the artifact answers null and the parse runs with the check off.
|
|
173
|
+
if (fstMatcher) {
|
|
174
|
+
try {
|
|
175
|
+
streetMorphologyMatcher = await loadStreetMorphologyFST(DEFAULT_LOCALE, release.version)
|
|
176
|
+
} catch {
|
|
177
|
+
// Corrupt/unfetchable artifact — treat as absent (check off).
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
progress.setStepIndex(1)
|
|
183
|
+
|
|
184
|
+
let lookup: MailwomanLookupLike | null = null
|
|
185
|
+
|
|
186
|
+
if (gazetteer) {
|
|
187
|
+
try {
|
|
188
|
+
const { loadHTTPVFSDatabase, WOFCandidateTableLookup } =
|
|
189
|
+
await import("@mailwoman/resolver-wof-wasm/httpvfs/resolver")
|
|
190
|
+
|
|
191
|
+
const worker = await loadHTTPVFSDatabase(adminGazetteerURL(), gazetteer.sqljsBaseURL)
|
|
192
|
+
|
|
193
|
+
if (!progress.signal.aborted) {
|
|
194
|
+
const wofLookup = new WOFCandidateTableLookup(worker)
|
|
195
|
+
// Fire-and-forget: pull the schema/FTS/dual-role pages through the VFS now so the first interactive query
|
|
196
|
+
// starts warm. The worker serializes execs, so a user query issued mid-warm-up simply queues behind pages
|
|
197
|
+
// it was going to need anyway.
|
|
198
|
+
void wofLookup.warmUp().catch(() => {})
|
|
199
|
+
lookup = wofLookup
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// WOF DB not available for this version.
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
progress.setStepIndex(2)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
classifier,
|
|
210
|
+
anchorLookup: postcodeAnchorLookup ?? null,
|
|
211
|
+
fstMatcher,
|
|
212
|
+
fstProvenance,
|
|
213
|
+
streetMorphologyMatcher,
|
|
214
|
+
lookup,
|
|
215
|
+
calibrator,
|
|
216
|
+
selectPairIndex: selectPairIndexForText ?? null,
|
|
217
|
+
}
|
|
218
|
+
}
|