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.
Files changed (348) hide show
  1. package/README.md +6 -4
  2. package/lib/api-engine.ts +3 -3
  3. package/lib/browser-runtime/classify.ts +262 -0
  4. package/lib/browser-runtime/fetch.ts +89 -0
  5. package/lib/browser-runtime/index.ts +12 -0
  6. package/lib/browser-runtime/load-assets.ts +218 -0
  7. package/lib/browser-runtime/manifest.ts +104 -0
  8. package/lib/browser-runtime/resources.ts +358 -0
  9. package/lib/browser-runtime/types.ts +121 -0
  10. package/lib/cli-kit/dropin.ts +2 -7
  11. package/lib/cli-kit/index.ts +0 -1
  12. package/lib/cli-kit/shared.ts +4 -3
  13. package/lib/commands/corpus/build.tsx +2 -7
  14. package/lib/commands/corpus/fetch.tsx +27 -0
  15. package/lib/commands/corpus/run.tsx +1 -1
  16. package/lib/commands/corpus/slice/index.tsx +1 -2
  17. package/lib/commands/data/pull.tsx +1 -1
  18. package/lib/commands/gazetteer/build/poi-coverage.tsx +1 -1
  19. package/lib/commands/gazetteer/build/postcode-locality.tsx +3 -7
  20. package/lib/commands/gazetteer/build/tw-districts.tsx +70 -0
  21. package/lib/commands/gazetteer/conventions.tsx +2 -7
  22. package/lib/commands/gazetteer/importance.tsx +5 -9
  23. package/lib/commands/gazetteer/inspect/graph.tsx +2 -8
  24. package/lib/commands/gazetteer/inspect/mermaid.tsx +2 -8
  25. package/lib/commands/gazetteer/inspect/placetype-stats.tsx +2 -8
  26. package/lib/commands/gazetteer/inspect/sync.tsx +1 -2
  27. package/lib/commands/gazetteer/inspect/tree.tsx +2 -8
  28. package/lib/commands/gazetteer/overture-ingest.tsx +1 -2
  29. package/lib/commands/gazetteer/polygons.tsx +1 -2
  30. package/lib/commands/gazetteer/postcode-intl.tsx +1 -1
  31. package/lib/commands/gazetteer/publish.tsx +2 -2
  32. package/lib/commands/parse.tsx +30 -9
  33. package/lib/commands/poi.tsx +3 -3
  34. package/lib/commands/registry/run.tsx +2 -8
  35. package/lib/commands/registry/scorer-eval.tsx +1 -1
  36. package/lib/commands/situs/address-points.tsx +127 -34
  37. package/lib/commands/situs/interpolation-database.tsx +2 -8
  38. package/lib/commands/situs/interpolation.tsx +1 -2
  39. package/lib/commands/skill/install.tsx +1 -1
  40. package/lib/commands/tiger/fetch.tsx +2 -7
  41. package/lib/commands/tiger/redistricting.tsx +2 -7
  42. package/lib/commands/tiles/publish.tsx +6 -73
  43. package/lib/data/bundles.ts +3 -3
  44. package/lib/debug-view/DebugSessionApp.tsx +1 -1
  45. package/lib/debug-view/command.tsx +2 -1
  46. package/lib/debug-view/view-policy.ts +2 -1
  47. package/lib/dev-tools/fr-admin-split-selfvalidation.run.ts +1 -2
  48. package/lib/eval-harness/conformance/run.ts +4 -4
  49. package/lib/eval-harness/demo-cascade-smoke.ts +3 -13
  50. package/lib/eval-harness/gauntlet/cases/jp/regression.jsonl +4 -3
  51. package/lib/eval-harness/gauntlet/harness.ts +2 -7
  52. package/lib/eval-harness/oa-resolver/coordinate-tiers.ts +1 -1
  53. package/lib/eval-harness/oa-resolver/row-score.ts +1 -1
  54. package/lib/eval-harness/poi-board.ts +4 -3
  55. package/lib/eval-harness/premise-linkage/adapter.ts +1 -1
  56. package/lib/eval-harness/v0-tree-adapter.ts +1 -2
  57. package/lib/gazetteer-pipeline/admin/index.ts +1 -2
  58. package/lib/gazetteer-pipeline/index.ts +6 -3
  59. package/lib/gazetteer-pipeline/nsul-layer.ts +1 -2
  60. package/lib/gazetteer-pipeline/poi/build-poi.ts +1 -1
  61. package/lib/gazetteer-pipeline/poi/exclusion-coverage.ts +2 -3
  62. package/lib/gazetteer-pipeline/postcode/geonames-tail.ts +0 -2
  63. package/lib/gazetteer-pipeline/postcode/index.ts +1 -1
  64. package/lib/gazetteer-pipeline/tw-districts.ts +369 -0
  65. package/lib/gazetteer-pipeline/uprn-layer.ts +1 -2
  66. package/lib/geocode/classifier.ts +73 -0
  67. package/lib/geocode/core.ts +38 -60
  68. package/lib/geocode/index.ts +1 -0
  69. package/lib/geocode/national-overture.ts +145 -0
  70. package/lib/geocode/regions.ts +1 -1
  71. package/lib/geocode/session.ts +41 -22
  72. package/lib/geocode/worker.ts +1 -1
  73. package/lib/index.ts +0 -1
  74. package/lib/intent-refusal.ts +1 -1
  75. package/lib/observations/absence-route.ts +2 -9
  76. package/lib/observations/layer-record.ts +2 -1
  77. package/lib/poi/index.ts +0 -1
  78. package/lib/release-tools/publish-hf.ts +2 -2
  79. package/lib/resolver-backend.ts +0 -2
  80. package/lib/runtime-pipeline.ts +12 -27
  81. package/lib/tiles/publish.ts +120 -0
  82. package/lib/tools/generate-clients.ts +2 -2
  83. package/man/mailwoman.1 +1 -1
  84. package/out/api-engine.js +2 -1
  85. package/out/api-engine.js.map +1 -1
  86. package/out/browser-runtime/classify.d.ts +154 -0
  87. package/out/browser-runtime/classify.d.ts.map +1 -0
  88. package/out/browser-runtime/classify.js +146 -0
  89. package/out/browser-runtime/classify.js.map +1 -0
  90. package/out/browser-runtime/fetch.d.ts +18 -0
  91. package/out/browser-runtime/fetch.d.ts.map +1 -0
  92. package/out/browser-runtime/fetch.js +74 -0
  93. package/out/browser-runtime/fetch.js.map +1 -0
  94. package/out/browser-runtime/index.d.ts +12 -0
  95. package/out/browser-runtime/index.d.ts.map +1 -0
  96. package/out/browser-runtime/index.js +12 -0
  97. package/out/browser-runtime/index.js.map +1 -0
  98. package/out/browser-runtime/load-assets.d.ts +73 -0
  99. package/out/browser-runtime/load-assets.d.ts.map +1 -0
  100. package/out/browser-runtime/load-assets.js +120 -0
  101. package/out/browser-runtime/load-assets.js.map +1 -0
  102. package/out/browser-runtime/manifest.d.ts +76 -0
  103. package/out/browser-runtime/manifest.d.ts.map +1 -0
  104. package/out/browser-runtime/manifest.js +46 -0
  105. package/out/browser-runtime/manifest.js.map +1 -0
  106. package/out/browser-runtime/resources.d.ts +148 -0
  107. package/out/browser-runtime/resources.d.ts.map +1 -0
  108. package/out/browser-runtime/resources.js +323 -0
  109. package/out/browser-runtime/resources.js.map +1 -0
  110. package/out/browser-runtime/types.d.ts +130 -0
  111. package/out/browser-runtime/types.d.ts.map +1 -0
  112. package/out/browser-runtime/types.js +12 -0
  113. package/out/browser-runtime/types.js.map +1 -0
  114. package/out/cli-kit/dropin.d.ts.map +1 -1
  115. package/out/cli-kit/dropin.js +2 -1
  116. package/out/cli-kit/dropin.js.map +1 -1
  117. package/out/cli-kit/index.d.ts +0 -1
  118. package/out/cli-kit/index.d.ts.map +1 -1
  119. package/out/cli-kit/index.js +0 -1
  120. package/out/cli-kit/index.js.map +1 -1
  121. package/out/cli-kit/shared.d.ts +2 -2
  122. package/out/cli-kit/shared.d.ts.map +1 -1
  123. package/out/cli-kit/shared.js +2 -1
  124. package/out/cli-kit/shared.js.map +1 -1
  125. package/out/commands/corpus/build.d.ts.map +1 -1
  126. package/out/commands/corpus/build.js +17 -1
  127. package/out/commands/corpus/build.js.map +1 -1
  128. package/out/commands/corpus/fetch.d.ts +11 -1
  129. package/out/commands/corpus/fetch.d.ts.map +1 -1
  130. package/out/commands/corpus/fetch.js +18 -1
  131. package/out/commands/corpus/fetch.js.map +1 -1
  132. package/out/commands/corpus/run.d.ts.map +1 -1
  133. package/out/commands/corpus/run.js +2 -1
  134. package/out/commands/corpus/run.js.map +1 -1
  135. package/out/commands/corpus/slice/index.d.ts.map +1 -1
  136. package/out/commands/corpus/slice/index.js +2 -1
  137. package/out/commands/corpus/slice/index.js.map +1 -1
  138. package/out/commands/data/pull.d.ts.map +1 -1
  139. package/out/commands/data/pull.js +2 -1
  140. package/out/commands/data/pull.js.map +1 -1
  141. package/out/commands/gazetteer/build/poi-coverage.js +1 -1
  142. package/out/commands/gazetteer/build/poi-coverage.js.map +1 -1
  143. package/out/commands/gazetteer/build/postcode-locality.d.ts.map +1 -1
  144. package/out/commands/gazetteer/build/postcode-locality.js +2 -1
  145. package/out/commands/gazetteer/build/postcode-locality.js.map +1 -1
  146. package/out/commands/gazetteer/build/tw-districts.d.ts +50 -0
  147. package/out/commands/gazetteer/build/tw-districts.d.ts.map +1 -0
  148. package/out/commands/gazetteer/build/tw-districts.js +54 -0
  149. package/out/commands/gazetteer/build/tw-districts.js.map +1 -0
  150. package/out/commands/gazetteer/conventions.d.ts.map +1 -1
  151. package/out/commands/gazetteer/conventions.js +2 -1
  152. package/out/commands/gazetteer/conventions.js.map +1 -1
  153. package/out/commands/gazetteer/importance.d.ts.map +1 -1
  154. package/out/commands/gazetteer/importance.js +4 -2
  155. package/out/commands/gazetteer/importance.js.map +1 -1
  156. package/out/commands/gazetteer/inspect/graph.d.ts.map +1 -1
  157. package/out/commands/gazetteer/inspect/graph.js +2 -1
  158. package/out/commands/gazetteer/inspect/graph.js.map +1 -1
  159. package/out/commands/gazetteer/inspect/mermaid.d.ts.map +1 -1
  160. package/out/commands/gazetteer/inspect/mermaid.js +2 -1
  161. package/out/commands/gazetteer/inspect/mermaid.js.map +1 -1
  162. package/out/commands/gazetteer/inspect/placetype-stats.d.ts.map +1 -1
  163. package/out/commands/gazetteer/inspect/placetype-stats.js +2 -1
  164. package/out/commands/gazetteer/inspect/placetype-stats.js.map +1 -1
  165. package/out/commands/gazetteer/inspect/sync.d.ts.map +1 -1
  166. package/out/commands/gazetteer/inspect/sync.js +2 -1
  167. package/out/commands/gazetteer/inspect/sync.js.map +1 -1
  168. package/out/commands/gazetteer/inspect/tree.d.ts.map +1 -1
  169. package/out/commands/gazetteer/inspect/tree.js +2 -1
  170. package/out/commands/gazetteer/inspect/tree.js.map +1 -1
  171. package/out/commands/gazetteer/overture-ingest.d.ts.map +1 -1
  172. package/out/commands/gazetteer/overture-ingest.js +2 -1
  173. package/out/commands/gazetteer/overture-ingest.js.map +1 -1
  174. package/out/commands/gazetteer/polygons.d.ts.map +1 -1
  175. package/out/commands/gazetteer/polygons.js +2 -1
  176. package/out/commands/gazetteer/polygons.js.map +1 -1
  177. package/out/commands/gazetteer/postcode-intl.d.ts.map +1 -1
  178. package/out/commands/gazetteer/postcode-intl.js +2 -1
  179. package/out/commands/gazetteer/postcode-intl.js.map +1 -1
  180. package/out/commands/gazetteer/publish.js +2 -2
  181. package/out/commands/gazetteer/publish.js.map +1 -1
  182. package/out/commands/parse.d.ts.map +1 -1
  183. package/out/commands/parse.js +20 -6
  184. package/out/commands/parse.js.map +1 -1
  185. package/out/commands/poi.d.ts.map +1 -1
  186. package/out/commands/poi.js +3 -2
  187. package/out/commands/poi.js.map +1 -1
  188. package/out/commands/registry/run.d.ts.map +1 -1
  189. package/out/commands/registry/run.js +2 -1
  190. package/out/commands/registry/run.js.map +1 -1
  191. package/out/commands/registry/scorer-eval.js +1 -1
  192. package/out/commands/registry/scorer-eval.js.map +1 -1
  193. package/out/commands/situs/address-points.d.ts +22 -3
  194. package/out/commands/situs/address-points.d.ts.map +1 -1
  195. package/out/commands/situs/address-points.js +100 -26
  196. package/out/commands/situs/address-points.js.map +1 -1
  197. package/out/commands/situs/interpolation-database.d.ts.map +1 -1
  198. package/out/commands/situs/interpolation-database.js +2 -1
  199. package/out/commands/situs/interpolation-database.js.map +1 -1
  200. package/out/commands/situs/interpolation.d.ts.map +1 -1
  201. package/out/commands/situs/interpolation.js +2 -1
  202. package/out/commands/situs/interpolation.js.map +1 -1
  203. package/out/commands/skill/install.d.ts.map +1 -1
  204. package/out/commands/skill/install.js +2 -1
  205. package/out/commands/skill/install.js.map +1 -1
  206. package/out/commands/tiger/fetch.d.ts.map +1 -1
  207. package/out/commands/tiger/fetch.js +2 -1
  208. package/out/commands/tiger/fetch.js.map +1 -1
  209. package/out/commands/tiger/redistricting.d.ts.map +1 -1
  210. package/out/commands/tiger/redistricting.js +2 -1
  211. package/out/commands/tiger/redistricting.js.map +1 -1
  212. package/out/commands/tiles/publish.d.ts +5 -27
  213. package/out/commands/tiles/publish.d.ts.map +1 -1
  214. package/out/commands/tiles/publish.js +5 -49
  215. package/out/commands/tiles/publish.js.map +1 -1
  216. package/out/data/bundles.d.ts +3 -3
  217. package/out/data/bundles.js +3 -3
  218. package/out/debug-view/DebugSessionApp.d.ts.map +1 -1
  219. package/out/debug-view/DebugSessionApp.js +1 -1
  220. package/out/debug-view/DebugSessionApp.js.map +1 -1
  221. package/out/debug-view/command.d.ts.map +1 -1
  222. package/out/debug-view/command.js +2 -1
  223. package/out/debug-view/command.js.map +1 -1
  224. package/out/debug-view/view-policy.d.ts.map +1 -1
  225. package/out/debug-view/view-policy.js +1 -1
  226. package/out/debug-view/view-policy.js.map +1 -1
  227. package/out/dev-tools/fr-admin-split-selfvalidation.run.js.map +1 -1
  228. package/out/eval-harness/conformance/run.d.ts +4 -4
  229. package/out/eval-harness/demo-cascade-smoke.d.ts +2 -12
  230. package/out/eval-harness/demo-cascade-smoke.d.ts.map +1 -1
  231. package/out/eval-harness/demo-cascade-smoke.js +3 -13
  232. package/out/eval-harness/demo-cascade-smoke.js.map +1 -1
  233. package/out/eval-harness/gauntlet/harness.d.ts.map +1 -1
  234. package/out/eval-harness/gauntlet/harness.js +2 -1
  235. package/out/eval-harness/gauntlet/harness.js.map +1 -1
  236. package/out/eval-harness/oa-resolver/assembled-arm.d.ts +1 -1
  237. package/out/eval-harness/oa-resolver/coordinate-tiers.d.ts +1 -1
  238. package/out/eval-harness/oa-resolver/coordinate-tiers.d.ts.map +1 -1
  239. package/out/eval-harness/oa-resolver/parse-rig.d.ts +1 -1
  240. package/out/eval-harness/oa-resolver/row-score.js +1 -1
  241. package/out/eval-harness/oa-resolver/row-score.js.map +1 -1
  242. package/out/eval-harness/poi-board.d.ts.map +1 -1
  243. package/out/eval-harness/poi-board.js +2 -1
  244. package/out/eval-harness/poi-board.js.map +1 -1
  245. package/out/eval-harness/premise-linkage/adapter.d.ts.map +1 -1
  246. package/out/eval-harness/premise-linkage/adapter.js.map +1 -1
  247. package/out/eval-harness/v0-tree-adapter.d.ts +1 -1
  248. package/out/eval-harness/v0-tree-adapter.d.ts.map +1 -1
  249. package/out/eval-harness/v0-tree-adapter.js.map +1 -1
  250. package/out/gazetteer-pipeline/admin/index.d.ts.map +1 -1
  251. package/out/gazetteer-pipeline/admin/index.js +1 -2
  252. package/out/gazetteer-pipeline/admin/index.js.map +1 -1
  253. package/out/gazetteer-pipeline/index.d.ts +1 -1
  254. package/out/gazetteer-pipeline/index.d.ts.map +1 -1
  255. package/out/gazetteer-pipeline/index.js +5 -2
  256. package/out/gazetteer-pipeline/index.js.map +1 -1
  257. package/out/gazetteer-pipeline/nsul-layer.d.ts.map +1 -1
  258. package/out/gazetteer-pipeline/nsul-layer.js +2 -1
  259. package/out/gazetteer-pipeline/nsul-layer.js.map +1 -1
  260. package/out/gazetteer-pipeline/poi/build-poi.d.ts +2 -1
  261. package/out/gazetteer-pipeline/poi/build-poi.d.ts.map +1 -1
  262. package/out/gazetteer-pipeline/poi/build-poi.js +2 -1
  263. package/out/gazetteer-pipeline/poi/build-poi.js.map +1 -1
  264. package/out/gazetteer-pipeline/poi/exclusion-coverage.d.ts +1 -1
  265. package/out/gazetteer-pipeline/poi/exclusion-coverage.d.ts.map +1 -1
  266. package/out/gazetteer-pipeline/poi/exclusion-coverage.js +2 -3
  267. package/out/gazetteer-pipeline/poi/exclusion-coverage.js.map +1 -1
  268. package/out/gazetteer-pipeline/postcode/geonames-tail.d.ts +0 -1
  269. package/out/gazetteer-pipeline/postcode/geonames-tail.d.ts.map +1 -1
  270. package/out/gazetteer-pipeline/postcode/geonames-tail.js.map +1 -1
  271. package/out/gazetteer-pipeline/postcode/index.d.ts.map +1 -1
  272. package/out/gazetteer-pipeline/postcode/index.js +1 -1
  273. package/out/gazetteer-pipeline/postcode/index.js.map +1 -1
  274. package/out/gazetteer-pipeline/tw-districts.d.ts +143 -0
  275. package/out/gazetteer-pipeline/tw-districts.d.ts.map +1 -0
  276. package/out/gazetteer-pipeline/tw-districts.js +233 -0
  277. package/out/gazetteer-pipeline/tw-districts.js.map +1 -0
  278. package/out/gazetteer-pipeline/uprn-layer.d.ts.map +1 -1
  279. package/out/gazetteer-pipeline/uprn-layer.js +2 -1
  280. package/out/gazetteer-pipeline/uprn-layer.js.map +1 -1
  281. package/out/geocode/classifier.d.ts +56 -0
  282. package/out/geocode/classifier.d.ts.map +1 -0
  283. package/out/geocode/classifier.js +29 -0
  284. package/out/geocode/classifier.js.map +1 -0
  285. package/out/geocode/core.d.ts +4 -31
  286. package/out/geocode/core.d.ts.map +1 -1
  287. package/out/geocode/core.js +21 -18
  288. package/out/geocode/core.js.map +1 -1
  289. package/out/geocode/index.d.ts +1 -0
  290. package/out/geocode/index.d.ts.map +1 -1
  291. package/out/geocode/index.js +1 -0
  292. package/out/geocode/index.js.map +1 -1
  293. package/out/geocode/national-overture.d.ts +43 -0
  294. package/out/geocode/national-overture.d.ts.map +1 -0
  295. package/out/geocode/national-overture.js +114 -0
  296. package/out/geocode/national-overture.js.map +1 -0
  297. package/out/geocode/regions.d.ts +1 -1
  298. package/out/geocode/regions.d.ts.map +1 -1
  299. package/out/geocode/session.d.ts +1 -1
  300. package/out/geocode/session.d.ts.map +1 -1
  301. package/out/geocode/session.js +33 -14
  302. package/out/geocode/session.js.map +1 -1
  303. package/out/geocode/worker.js +1 -1
  304. package/out/geocode/worker.js.map +1 -1
  305. package/out/index.d.ts +0 -1
  306. package/out/index.d.ts.map +1 -1
  307. package/out/index.js +0 -1
  308. package/out/index.js.map +1 -1
  309. package/out/intent-refusal.d.ts +1 -1
  310. package/out/intent-refusal.js +1 -1
  311. package/out/observations/absence-route.d.ts +2 -1
  312. package/out/observations/absence-route.d.ts.map +1 -1
  313. package/out/observations/absence-route.js +2 -1
  314. package/out/observations/absence-route.js.map +1 -1
  315. package/out/observations/layer-record.d.ts +2 -1
  316. package/out/observations/layer-record.d.ts.map +1 -1
  317. package/out/observations/layer-record.js.map +1 -1
  318. package/out/poi/index.d.ts +0 -1
  319. package/out/poi/index.d.ts.map +1 -1
  320. package/out/poi/index.js +0 -1
  321. package/out/poi/index.js.map +1 -1
  322. package/out/release-tools/publish-hf.js +2 -2
  323. package/out/release-tools/publish-hf.js.map +1 -1
  324. package/out/resolver-backend.d.ts +1 -1
  325. package/out/resolver-backend.d.ts.map +1 -1
  326. package/out/resolver-backend.js +0 -1
  327. package/out/resolver-backend.js.map +1 -1
  328. package/out/runtime-pipeline.d.ts +0 -2
  329. package/out/runtime-pipeline.d.ts.map +1 -1
  330. package/out/runtime-pipeline.js +11 -4
  331. package/out/runtime-pipeline.js.map +1 -1
  332. package/out/tiles/publish.d.ts +70 -0
  333. package/out/tiles/publish.d.ts.map +1 -0
  334. package/out/tiles/publish.js +71 -0
  335. package/out/tiles/publish.js.map +1 -0
  336. package/out/tools/generate-clients.js +2 -2
  337. package/out/tools/generate-clients.js.map +1 -1
  338. package/package.json +117 -37
  339. package/lib/gazetteer-pipeline/poi/coverage-region.ts +0 -21
  340. package/lib/poi/overpass.ts +0 -11
  341. package/out/gazetteer-pipeline/poi/coverage-region.d.ts +0 -21
  342. package/out/gazetteer-pipeline/poi/coverage-region.d.ts.map +0 -1
  343. package/out/gazetteer-pipeline/poi/coverage-region.js +0 -21
  344. package/out/gazetteer-pipeline/poi/coverage-region.js.map +0 -1
  345. package/out/poi/overpass.d.ts +0 -11
  346. package/out/poi/overpass.d.ts.map +0 -1
  347. package/out/poi/overpass.js +0 -11
  348. 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 { createRuntimePipeline, decodeAsJson } from "mailwoman"
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(decodeAsJson(tree))
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 { createRuntimePipeline, decodeAsJson, decodeAsTuples } from "mailwoman"
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
- decodeAsJson(result.tree)
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, type Resolver, type ResolveOpts } from "@mailwoman/resolver"
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
+ }