@mailwoman/resolver-wof-sqlite 9.1.0 → 9.3.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 (501) hide show
  1. package/README.md +36 -20
  2. package/lib/address/index.ts +9 -0
  3. package/{address-point-interpolation.ts → lib/address/point-interpolation.ts} +40 -28
  4. package/{address-point-schema.ts → lib/address/point-schema.ts} +34 -8
  5. package/lib/address/point.ts +280 -0
  6. package/{ancestry-backfill.ts → lib/ancestry/backfill.ts} +14 -19
  7. package/{ancestry.ts → lib/ancestry/index.ts} +13 -8
  8. package/{build-candidate.ts → lib/build-candidate.ts} +243 -199
  9. package/{build-slim.ts → lib/build-slim.ts} +153 -168
  10. package/lib/candidate/alias-bags.ts +56 -0
  11. package/lib/candidate/ancestors-sidecar.ts +204 -0
  12. package/lib/candidate/country-display-names.ts +79 -0
  13. package/lib/candidate/extract-fold.ts +138 -0
  14. package/lib/candidate/name-roles.ts +238 -0
  15. package/lib/candidate/own-name.ts +146 -0
  16. package/lib/candidate/place-attrs.ts +44 -0
  17. package/lib/candidate-ancestors-schema.ts +195 -0
  18. package/{candidate-fts.ts → lib/candidate-fts.ts} +7 -5
  19. package/{candidate-importance.ts → lib/candidate-importance.ts} +54 -57
  20. package/lib/candidate-lookup.ts +976 -0
  21. package/{candidate-schema.ts → lib/candidate-schema.ts} +34 -6
  22. package/lib/candidate-scoring.ts +261 -0
  23. package/lib/capital-schema.ts +89 -0
  24. package/lib/capitals.ts +148 -0
  25. package/{coincident-roles.ts → lib/coincident-roles.ts} +75 -19
  26. package/{convention.ts → lib/convention/index.ts} +5 -5
  27. package/lib/convention/schema.ts +72 -0
  28. package/{coverage-manifest-schema.ts → lib/coverage-manifest-schema.ts} +15 -16
  29. package/lib/currency-backfill.ts +334 -0
  30. package/lib/env.ts +50 -0
  31. package/lib/exact-match.ts +113 -0
  32. package/{sharding.ts → lib/extracts.ts} +63 -60
  33. package/lib/fst/autocomplete.ts +176 -0
  34. package/{fst-builder.ts → lib/fst/builder.ts} +30 -27
  35. package/{fst-deserialize-web.ts → lib/fst/deserialize-web.ts} +27 -87
  36. package/lib/fst/format.ts +106 -0
  37. package/{fst-freshness.ts → lib/fst/freshness.ts} +42 -75
  38. package/lib/fst/index.ts +14 -0
  39. package/{fst-matcher.ts → lib/fst/matcher.ts} +1 -1
  40. package/{fst-serialize.ts → lib/fst/serialize.ts} +37 -115
  41. package/{fst-types.ts → lib/fst/types.ts} +4 -2
  42. package/{fts.ts → lib/fts/index.ts} +28 -23
  43. package/{fts-query.ts → lib/fts/query.ts} +2 -2
  44. package/{geonames-aliases.ts → lib/geonames/aliases.ts} +13 -13
  45. package/lib/geonames/index.ts +8 -0
  46. package/{geonames-postal.ts → lib/geonames/postal.ts} +16 -15
  47. package/{index.ts → lib/index.ts} +52 -48
  48. package/{interpolation.ts → lib/interpolation.ts} +137 -41
  49. package/lib/lookup.ts +861 -0
  50. package/{name-score.ts → lib/name-score.ts} +6 -4
  51. package/lib/nsul/index.ts +8 -0
  52. package/lib/nsul/lookup.ts +144 -0
  53. package/lib/nsul/schema.ts +130 -0
  54. package/{place-importance-schema.ts → lib/place-importance-schema.ts} +70 -22
  55. package/lib/poi/index.ts +8 -0
  56. package/{poi-lookup.ts → lib/poi/lookup.ts} +32 -39
  57. package/{poi-schema.ts → lib/poi/schema.ts} +10 -6
  58. package/lib/polygon-schema.ts +47 -0
  59. package/{postal-city-alias-lookup.ts → lib/postal/city-alias-lookup.ts} +15 -21
  60. package/{postal-city-candidate-schema.ts → lib/postal/city-candidate-schema.ts} +4 -2
  61. package/lib/postal/index.ts +9 -0
  62. package/{postcode-point-lookup.ts → lib/postcode-point-lookup.ts} +17 -15
  63. package/lib/primary-preference.ts +226 -0
  64. package/lib/proximity-rerank.ts +120 -0
  65. package/{ranking-weights.ts → lib/ranking-weights.ts} +30 -4
  66. package/lib/region-keys.ts +144 -0
  67. package/{reverse.ts → lib/reverse.ts} +45 -56
  68. package/{schema.ts → lib/schema.ts} +11 -1
  69. package/lib/search-fetch.ts +257 -0
  70. package/{sqlite-convention-source.ts → lib/sqlite-convention-source.ts} +9 -10
  71. package/lib/sqlite-utils.ts +83 -0
  72. package/{street-centroid-schema.ts → lib/street/centroid-schema.ts} +12 -6
  73. package/{street-centroid.ts → lib/street/centroid.ts} +26 -28
  74. package/lib/street/index.ts +13 -0
  75. package/{street-morphology-fst-builder.ts → lib/street/morphology-fst-builder.ts} +24 -14
  76. package/{street-morphology-fst-loader.ts → lib/street/morphology-fst-loader.ts} +14 -12
  77. package/{street-name-lookup.ts → lib/street/name-lookup.ts} +14 -30
  78. package/lib/street/normalize.ts +572 -0
  79. package/{street-segment-schema.ts → lib/street/segment-schema.ts} +13 -8
  80. package/{types.ts → lib/types.ts} +49 -9
  81. package/{unified-schema.ts → lib/unified-schema.ts} +21 -25
  82. package/lib/uprn/existence.ts +84 -0
  83. package/lib/uprn/index.ts +9 -0
  84. package/lib/uprn/lookup.ts +206 -0
  85. package/lib/uprn/schema.ts +124 -0
  86. package/lib/weights-overlay-linker.ts +878 -0
  87. package/out/address/index.d.ts +9 -0
  88. package/out/address/index.d.ts.map +1 -0
  89. package/out/address/index.js +9 -0
  90. package/out/address/index.js.map +1 -0
  91. package/out/{address-point-interpolation.d.ts → address/point-interpolation.d.ts} +11 -10
  92. package/out/address/point-interpolation.d.ts.map +1 -0
  93. package/out/{address-point-interpolation.js → address/point-interpolation.js} +29 -23
  94. package/out/address/point-interpolation.js.map +1 -0
  95. package/out/{address-point-schema.d.ts → address/point-schema.d.ts} +30 -10
  96. package/out/address/point-schema.d.ts.map +1 -0
  97. package/out/{address-point-schema.js → address/point-schema.js} +7 -3
  98. package/out/address/point-schema.js.map +1 -0
  99. package/out/{address-point.d.ts → address/point.d.ts} +16 -8
  100. package/out/address/point.d.ts.map +1 -0
  101. package/out/address/point.js +191 -0
  102. package/out/address/point.js.map +1 -0
  103. package/out/{ancestry-backfill.d.ts → ancestry/backfill.d.ts} +7 -5
  104. package/out/ancestry/backfill.d.ts.map +1 -0
  105. package/out/{ancestry-backfill.js → ancestry/backfill.js} +12 -16
  106. package/out/ancestry/backfill.js.map +1 -0
  107. package/out/{ancestry.d.ts → ancestry/index.d.ts} +6 -5
  108. package/out/ancestry/index.d.ts.map +1 -0
  109. package/out/{ancestry.js → ancestry/index.js} +7 -7
  110. package/out/ancestry/index.js.map +1 -0
  111. package/out/build-candidate.d.ts +88 -7
  112. package/out/build-candidate.d.ts.map +1 -1
  113. package/out/build-candidate.js +145 -149
  114. package/out/build-candidate.js.map +1 -1
  115. package/out/build-slim.d.ts +5 -5
  116. package/out/build-slim.d.ts.map +1 -1
  117. package/out/build-slim.js +127 -142
  118. package/out/build-slim.js.map +1 -1
  119. package/out/candidate/alias-bags.d.ts +19 -0
  120. package/out/candidate/alias-bags.d.ts.map +1 -0
  121. package/out/candidate/alias-bags.js +39 -0
  122. package/out/candidate/alias-bags.js.map +1 -0
  123. package/out/candidate/ancestors-sidecar.d.ts +32 -0
  124. package/out/candidate/ancestors-sidecar.d.ts.map +1 -0
  125. package/out/candidate/ancestors-sidecar.js +140 -0
  126. package/out/candidate/ancestors-sidecar.js.map +1 -0
  127. package/out/candidate/country-display-names.d.ts +35 -0
  128. package/out/candidate/country-display-names.d.ts.map +1 -0
  129. package/out/candidate/country-display-names.js +59 -0
  130. package/out/candidate/country-display-names.js.map +1 -0
  131. package/out/candidate/extract-fold.d.ts +32 -0
  132. package/out/candidate/extract-fold.d.ts.map +1 -0
  133. package/out/candidate/extract-fold.js +104 -0
  134. package/out/candidate/extract-fold.js.map +1 -0
  135. package/out/candidate/name-roles.d.ts +57 -0
  136. package/out/candidate/name-roles.d.ts.map +1 -0
  137. package/out/candidate/name-roles.js +166 -0
  138. package/out/candidate/name-roles.js.map +1 -0
  139. package/out/candidate/own-name.d.ts +50 -0
  140. package/out/candidate/own-name.d.ts.map +1 -0
  141. package/out/candidate/own-name.js +132 -0
  142. package/out/candidate/own-name.js.map +1 -0
  143. package/out/candidate/place-attrs.d.ts +43 -0
  144. package/out/candidate/place-attrs.d.ts.map +1 -0
  145. package/out/candidate/place-attrs.js +15 -0
  146. package/out/candidate/place-attrs.js.map +1 -0
  147. package/out/candidate-ancestors-schema.d.ts +150 -0
  148. package/out/candidate-ancestors-schema.d.ts.map +1 -0
  149. package/out/candidate-ancestors-schema.js +123 -0
  150. package/out/candidate-ancestors-schema.js.map +1 -0
  151. package/out/candidate-fts.d.ts +7 -5
  152. package/out/candidate-fts.d.ts.map +1 -1
  153. package/out/candidate-fts.js +5 -3
  154. package/out/candidate-fts.js.map +1 -1
  155. package/out/candidate-importance.d.ts +16 -16
  156. package/out/candidate-importance.d.ts.map +1 -1
  157. package/out/candidate-importance.js +47 -52
  158. package/out/candidate-importance.js.map +1 -1
  159. package/out/candidate-lookup.d.ts +31 -52
  160. package/out/candidate-lookup.d.ts.map +1 -1
  161. package/out/candidate-lookup.js +380 -172
  162. package/out/candidate-lookup.js.map +1 -1
  163. package/out/candidate-schema.d.ts +31 -7
  164. package/out/candidate-schema.d.ts.map +1 -1
  165. package/out/candidate-schema.js +3 -0
  166. package/out/candidate-schema.js.map +1 -1
  167. package/out/candidate-scoring.d.ts +34 -0
  168. package/out/candidate-scoring.d.ts.map +1 -0
  169. package/out/candidate-scoring.js +196 -0
  170. package/out/candidate-scoring.js.map +1 -0
  171. package/out/capital-schema.d.ts +51 -0
  172. package/out/capital-schema.d.ts.map +1 -0
  173. package/out/capital-schema.js +63 -0
  174. package/out/capital-schema.js.map +1 -0
  175. package/out/capitals.d.ts +69 -0
  176. package/out/capitals.d.ts.map +1 -0
  177. package/out/capitals.js +98 -0
  178. package/out/capitals.js.map +1 -0
  179. package/out/coincident-roles.d.ts +12 -4
  180. package/out/coincident-roles.d.ts.map +1 -1
  181. package/out/coincident-roles.js +43 -11
  182. package/out/coincident-roles.js.map +1 -1
  183. package/out/{convention.d.ts → convention/index.d.ts} +5 -5
  184. package/out/convention/index.d.ts.map +1 -0
  185. package/out/{convention.js → convention/index.js} +4 -4
  186. package/out/convention/index.js.map +1 -0
  187. package/out/convention/schema.d.ts +51 -0
  188. package/out/convention/schema.d.ts.map +1 -0
  189. package/out/convention/schema.js +34 -0
  190. package/out/convention/schema.js.map +1 -0
  191. package/out/coverage-manifest-schema.d.ts +8 -8
  192. package/out/coverage-manifest-schema.d.ts.map +1 -1
  193. package/out/coverage-manifest-schema.js +6 -10
  194. package/out/coverage-manifest-schema.js.map +1 -1
  195. package/out/currency-backfill.d.ts +90 -0
  196. package/out/currency-backfill.d.ts.map +1 -0
  197. package/out/currency-backfill.js +196 -0
  198. package/out/currency-backfill.js.map +1 -0
  199. package/out/env.d.ts +43 -0
  200. package/out/env.d.ts.map +1 -0
  201. package/out/env.js +48 -0
  202. package/out/env.js.map +1 -0
  203. package/out/exact-match.d.ts +25 -0
  204. package/out/exact-match.d.ts.map +1 -0
  205. package/out/exact-match.js +88 -0
  206. package/out/exact-match.js.map +1 -0
  207. package/out/extracts.d.ts +114 -0
  208. package/out/extracts.d.ts.map +1 -0
  209. package/out/{sharding.js → extracts.js} +38 -38
  210. package/out/extracts.js.map +1 -0
  211. package/out/{fst-autocomplete.d.ts → fst/autocomplete.d.ts} +13 -13
  212. package/out/fst/autocomplete.d.ts.map +1 -0
  213. package/out/fst/autocomplete.js +113 -0
  214. package/out/fst/autocomplete.js.map +1 -0
  215. package/out/{fst-builder.d.ts → fst/builder.d.ts} +5 -5
  216. package/out/fst/builder.d.ts.map +1 -0
  217. package/out/{fst-builder.js → fst/builder.js} +24 -24
  218. package/out/fst/builder.js.map +1 -0
  219. package/out/{fst-deserialize-web.d.ts → fst/deserialize-web.d.ts} +3 -3
  220. package/out/fst/deserialize-web.d.ts.map +1 -0
  221. package/out/{fst-deserialize-web.js → fst/deserialize-web.js} +10 -74
  222. package/out/fst/deserialize-web.js.map +1 -0
  223. package/out/fst/format.d.ts +80 -0
  224. package/out/fst/format.d.ts.map +1 -0
  225. package/out/fst/format.js +91 -0
  226. package/out/fst/format.js.map +1 -0
  227. package/out/{fst-freshness.d.ts → fst/freshness.d.ts} +11 -14
  228. package/out/fst/freshness.d.ts.map +1 -0
  229. package/out/{fst-freshness.js → fst/freshness.js} +42 -74
  230. package/out/fst/freshness.js.map +1 -0
  231. package/out/fst/index.d.ts +14 -0
  232. package/out/fst/index.d.ts.map +1 -0
  233. package/out/fst/index.js +14 -0
  234. package/out/fst/index.js.map +1 -0
  235. package/out/{fst-matcher.d.ts → fst/matcher.d.ts} +2 -2
  236. package/out/fst/matcher.d.ts.map +1 -0
  237. package/out/{fst-matcher.js → fst/matcher.js} +1 -1
  238. package/out/fst/matcher.js.map +1 -0
  239. package/out/{fst-serialize.d.ts → fst/serialize.d.ts} +5 -10
  240. package/out/fst/serialize.d.ts.map +1 -0
  241. package/out/{fst-serialize.js → fst/serialize.js} +18 -98
  242. package/out/fst/serialize.js.map +1 -0
  243. package/out/{fst-types.d.ts → fst/types.d.ts} +4 -3
  244. package/out/fst/types.d.ts.map +1 -0
  245. package/out/{fst-types.js → fst/types.js} +1 -1
  246. package/out/fst/types.js.map +1 -0
  247. package/out/{fts.d.ts → fts/index.d.ts} +20 -13
  248. package/out/fts/index.d.ts.map +1 -0
  249. package/out/{fts.js → fts/index.js} +20 -15
  250. package/out/fts/index.js.map +1 -0
  251. package/out/{fts-query.d.ts → fts/query.d.ts} +2 -2
  252. package/out/fts/query.d.ts.map +1 -0
  253. package/out/{fts-query.js → fts/query.js} +2 -2
  254. package/out/fts/query.js.map +1 -0
  255. package/out/{geonames-aliases.d.ts → geonames/aliases.d.ts} +8 -6
  256. package/out/geonames/aliases.d.ts.map +1 -0
  257. package/out/{geonames-aliases.js → geonames/aliases.js} +7 -7
  258. package/out/geonames/aliases.js.map +1 -0
  259. package/out/geonames/index.d.ts +8 -0
  260. package/out/geonames/index.d.ts.map +1 -0
  261. package/out/geonames/index.js +8 -0
  262. package/out/geonames/index.js.map +1 -0
  263. package/out/{geonames-postal.d.ts → geonames/postal.d.ts} +12 -10
  264. package/out/geonames/postal.d.ts.map +1 -0
  265. package/out/{geonames-postal.js → geonames/postal.js} +11 -11
  266. package/out/geonames/postal.js.map +1 -0
  267. package/out/index.d.ts +32 -31
  268. package/out/index.d.ts.map +1 -1
  269. package/out/index.js +24 -24
  270. package/out/index.js.map +1 -1
  271. package/out/interpolation.d.ts +18 -9
  272. package/out/interpolation.d.ts.map +1 -1
  273. package/out/interpolation.js +108 -36
  274. package/out/interpolation.js.map +1 -1
  275. package/out/lookup.d.ts +25 -25
  276. package/out/lookup.d.ts.map +1 -1
  277. package/out/lookup.js +174 -558
  278. package/out/lookup.js.map +1 -1
  279. package/out/name-score.d.ts +0 -10
  280. package/out/name-score.d.ts.map +1 -1
  281. package/out/name-score.js +6 -4
  282. package/out/name-score.js.map +1 -1
  283. package/out/nsul/index.d.ts +8 -0
  284. package/out/nsul/index.d.ts.map +1 -0
  285. package/out/nsul/index.js +8 -0
  286. package/out/nsul/index.js.map +1 -0
  287. package/out/nsul/lookup.d.ts +83 -0
  288. package/out/nsul/lookup.d.ts.map +1 -0
  289. package/out/nsul/lookup.js +80 -0
  290. package/out/nsul/lookup.js.map +1 -0
  291. package/out/nsul/schema.d.ts +100 -0
  292. package/out/nsul/schema.d.ts.map +1 -0
  293. package/out/nsul/schema.js +78 -0
  294. package/out/nsul/schema.js.map +1 -0
  295. package/out/place-importance-schema.d.ts +47 -10
  296. package/out/place-importance-schema.d.ts.map +1 -1
  297. package/out/place-importance-schema.js +56 -10
  298. package/out/place-importance-schema.js.map +1 -1
  299. package/out/poi/index.d.ts +8 -0
  300. package/out/poi/index.d.ts.map +1 -0
  301. package/out/poi/index.js +8 -0
  302. package/out/poi/index.js.map +1 -0
  303. package/out/{poi-lookup.d.ts → poi/lookup.d.ts} +8 -10
  304. package/out/poi/lookup.d.ts.map +1 -0
  305. package/out/{poi-lookup.js → poi/lookup.js} +24 -29
  306. package/out/poi/lookup.js.map +1 -0
  307. package/out/{poi-schema.d.ts → poi/schema.d.ts} +10 -6
  308. package/out/poi/schema.d.ts.map +1 -0
  309. package/out/{poi-schema.js → poi/schema.js} +1 -1
  310. package/out/poi/schema.js.map +1 -0
  311. package/out/polygon-schema.d.ts +37 -0
  312. package/out/polygon-schema.d.ts.map +1 -0
  313. package/out/polygon-schema.js +23 -0
  314. package/out/polygon-schema.js.map +1 -0
  315. package/out/{postal-city-alias-lookup.d.ts → postal/city-alias-lookup.d.ts} +7 -6
  316. package/out/postal/city-alias-lookup.d.ts.map +1 -0
  317. package/out/{postal-city-alias-lookup.js → postal/city-alias-lookup.js} +12 -16
  318. package/out/postal/city-alias-lookup.js.map +1 -0
  319. package/out/{postal-city-alias-schema.d.ts → postal/city-alias-schema.d.ts} +1 -1
  320. package/out/{postal-city-alias-schema.d.ts.map → postal/city-alias-schema.d.ts.map} +1 -1
  321. package/out/{postal-city-alias-schema.js → postal/city-alias-schema.js} +1 -1
  322. package/out/{postal-city-alias-schema.js.map → postal/city-alias-schema.js.map} +1 -1
  323. package/out/{postal-city-candidate-schema.d.ts → postal/city-candidate-schema.d.ts} +4 -3
  324. package/out/postal/city-candidate-schema.d.ts.map +1 -0
  325. package/out/{postal-city-candidate-schema.js → postal/city-candidate-schema.js} +2 -2
  326. package/out/{postal-city-candidate-schema.js.map → postal/city-candidate-schema.js.map} +1 -1
  327. package/out/postal/index.d.ts +9 -0
  328. package/out/postal/index.d.ts.map +1 -0
  329. package/out/postal/index.js +9 -0
  330. package/out/postal/index.js.map +1 -0
  331. package/out/postcode-point-lookup.d.ts +10 -10
  332. package/out/postcode-point-lookup.d.ts.map +1 -1
  333. package/out/postcode-point-lookup.js +13 -13
  334. package/out/postcode-point-lookup.js.map +1 -1
  335. package/out/primary-preference.d.ts +133 -0
  336. package/out/primary-preference.d.ts.map +1 -0
  337. package/out/primary-preference.js +146 -0
  338. package/out/primary-preference.js.map +1 -0
  339. package/out/proximity-rerank.d.ts +77 -0
  340. package/out/proximity-rerank.d.ts.map +1 -0
  341. package/out/proximity-rerank.js +86 -0
  342. package/out/proximity-rerank.js.map +1 -0
  343. package/out/ranking-weights.d.ts +15 -3
  344. package/out/ranking-weights.d.ts.map +1 -1
  345. package/out/ranking-weights.js +21 -3
  346. package/out/ranking-weights.js.map +1 -1
  347. package/out/region-keys.d.ts +47 -0
  348. package/out/region-keys.d.ts.map +1 -0
  349. package/out/region-keys.js +121 -0
  350. package/out/region-keys.js.map +1 -0
  351. package/out/reverse.d.ts +6 -6
  352. package/out/reverse.d.ts.map +1 -1
  353. package/out/reverse.js +23 -35
  354. package/out/reverse.js.map +1 -1
  355. package/out/schema.d.ts +10 -1
  356. package/out/schema.d.ts.map +1 -1
  357. package/out/schema.js.map +1 -1
  358. package/out/search-fetch.d.ts +59 -0
  359. package/out/search-fetch.d.ts.map +1 -0
  360. package/out/search-fetch.js +184 -0
  361. package/out/search-fetch.js.map +1 -0
  362. package/out/sqlite-convention-source.d.ts +7 -7
  363. package/out/sqlite-convention-source.d.ts.map +1 -1
  364. package/out/sqlite-convention-source.js +5 -5
  365. package/out/sqlite-convention-source.js.map +1 -1
  366. package/out/sqlite-utils.d.ts +24 -6
  367. package/out/sqlite-utils.d.ts.map +1 -1
  368. package/out/sqlite-utils.js +24 -7
  369. package/out/sqlite-utils.js.map +1 -1
  370. package/out/{street-centroid-schema.d.ts → street/centroid-schema.d.ts} +12 -7
  371. package/out/street/centroid-schema.d.ts.map +1 -0
  372. package/out/{street-centroid-schema.js → street/centroid-schema.js} +5 -5
  373. package/out/{street-centroid-schema.js.map → street/centroid-schema.js.map} +1 -1
  374. package/out/{street-centroid.d.ts → street/centroid.d.ts} +6 -6
  375. package/out/street/centroid.d.ts.map +1 -0
  376. package/out/{street-centroid.js → street/centroid.js} +18 -24
  377. package/out/street/centroid.js.map +1 -0
  378. package/out/street/index.d.ts +13 -0
  379. package/out/street/index.d.ts.map +1 -0
  380. package/out/street/index.js +13 -0
  381. package/out/street/index.js.map +1 -0
  382. package/out/{street-morphology-fst-builder.d.ts → street/morphology-fst-builder.d.ts} +4 -4
  383. package/out/street/morphology-fst-builder.d.ts.map +1 -0
  384. package/out/{street-morphology-fst-builder.js → street/morphology-fst-builder.js} +18 -13
  385. package/out/street/morphology-fst-builder.js.map +1 -0
  386. package/out/{street-morphology-fst-loader.d.ts → street/morphology-fst-loader.d.ts} +5 -5
  387. package/out/street/morphology-fst-loader.d.ts.map +1 -0
  388. package/out/{street-morphology-fst-loader.js → street/morphology-fst-loader.js} +11 -10
  389. package/out/street/morphology-fst-loader.js.map +1 -0
  390. package/out/{street-name-lookup.d.ts → street/name-lookup.d.ts} +4 -7
  391. package/out/street/name-lookup.d.ts.map +1 -0
  392. package/out/{street-name-lookup.js → street/name-lookup.js} +9 -23
  393. package/out/street/name-lookup.js.map +1 -0
  394. package/out/street/normalize.d.ts +179 -0
  395. package/out/street/normalize.d.ts.map +1 -0
  396. package/out/street/normalize.js +457 -0
  397. package/out/street/normalize.js.map +1 -0
  398. package/out/{street-segment-schema.d.ts → street/segment-schema.d.ts} +13 -9
  399. package/out/street/segment-schema.d.ts.map +1 -0
  400. package/out/{street-segment-schema.js → street/segment-schema.js} +4 -4
  401. package/out/{street-segment-schema.js.map → street/segment-schema.js.map} +1 -1
  402. package/out/types.d.ts +49 -9
  403. package/out/types.d.ts.map +1 -1
  404. package/out/types.js.map +1 -1
  405. package/out/unified-schema.d.ts +6 -5
  406. package/out/unified-schema.d.ts.map +1 -1
  407. package/out/unified-schema.js +16 -19
  408. package/out/unified-schema.js.map +1 -1
  409. package/out/uprn/existence.d.ts +53 -0
  410. package/out/uprn/existence.d.ts.map +1 -0
  411. package/out/uprn/existence.js +61 -0
  412. package/out/uprn/existence.js.map +1 -0
  413. package/out/uprn/index.d.ts +9 -0
  414. package/out/uprn/index.d.ts.map +1 -0
  415. package/out/uprn/index.js +9 -0
  416. package/out/uprn/index.js.map +1 -0
  417. package/out/uprn/lookup.d.ts +85 -0
  418. package/out/uprn/lookup.d.ts.map +1 -0
  419. package/out/uprn/lookup.js +149 -0
  420. package/out/uprn/lookup.js.map +1 -0
  421. package/out/uprn/schema.d.ts +93 -0
  422. package/out/uprn/schema.d.ts.map +1 -0
  423. package/out/uprn/schema.js +78 -0
  424. package/out/uprn/schema.js.map +1 -0
  425. package/out/weights-overlay-linker.d.ts +299 -0
  426. package/out/weights-overlay-linker.d.ts.map +1 -0
  427. package/out/weights-overlay-linker.js +511 -0
  428. package/out/weights-overlay-linker.js.map +1 -0
  429. package/package.json +622 -154
  430. package/address-point.ts +0 -125
  431. package/candidate-lookup.ts +0 -720
  432. package/fst-autocomplete.ts +0 -204
  433. package/geo.ts +0 -121
  434. package/lookup.ts +0 -1354
  435. package/out/address-point-interpolation.d.ts.map +0 -1
  436. package/out/address-point-interpolation.js.map +0 -1
  437. package/out/address-point-schema.d.ts.map +0 -1
  438. package/out/address-point-schema.js.map +0 -1
  439. package/out/address-point.d.ts.map +0 -1
  440. package/out/address-point.js +0 -84
  441. package/out/address-point.js.map +0 -1
  442. package/out/ancestry-backfill.d.ts.map +0 -1
  443. package/out/ancestry-backfill.js.map +0 -1
  444. package/out/ancestry.d.ts.map +0 -1
  445. package/out/ancestry.js.map +0 -1
  446. package/out/convention.d.ts.map +0 -1
  447. package/out/convention.js.map +0 -1
  448. package/out/fst-autocomplete.d.ts.map +0 -1
  449. package/out/fst-autocomplete.js +0 -130
  450. package/out/fst-autocomplete.js.map +0 -1
  451. package/out/fst-builder.d.ts.map +0 -1
  452. package/out/fst-builder.js.map +0 -1
  453. package/out/fst-deserialize-web.d.ts.map +0 -1
  454. package/out/fst-deserialize-web.js.map +0 -1
  455. package/out/fst-freshness.d.ts.map +0 -1
  456. package/out/fst-freshness.js.map +0 -1
  457. package/out/fst-matcher.d.ts.map +0 -1
  458. package/out/fst-matcher.js.map +0 -1
  459. package/out/fst-serialize.d.ts.map +0 -1
  460. package/out/fst-serialize.js.map +0 -1
  461. package/out/fst-types.d.ts.map +0 -1
  462. package/out/fst-types.js.map +0 -1
  463. package/out/fts-query.d.ts.map +0 -1
  464. package/out/fts-query.js.map +0 -1
  465. package/out/fts.d.ts.map +0 -1
  466. package/out/fts.js.map +0 -1
  467. package/out/geo.d.ts +0 -74
  468. package/out/geo.d.ts.map +0 -1
  469. package/out/geo.js +0 -71
  470. package/out/geo.js.map +0 -1
  471. package/out/geonames-aliases.d.ts.map +0 -1
  472. package/out/geonames-aliases.js.map +0 -1
  473. package/out/geonames-postal.d.ts.map +0 -1
  474. package/out/geonames-postal.js.map +0 -1
  475. package/out/poi-lookup.d.ts.map +0 -1
  476. package/out/poi-lookup.js.map +0 -1
  477. package/out/poi-schema.d.ts.map +0 -1
  478. package/out/poi-schema.js.map +0 -1
  479. package/out/postal-city-alias-lookup.d.ts.map +0 -1
  480. package/out/postal-city-alias-lookup.js.map +0 -1
  481. package/out/postal-city-candidate-schema.d.ts.map +0 -1
  482. package/out/sharding.d.ts +0 -114
  483. package/out/sharding.d.ts.map +0 -1
  484. package/out/sharding.js.map +0 -1
  485. package/out/street-centroid-schema.d.ts.map +0 -1
  486. package/out/street-centroid.d.ts.map +0 -1
  487. package/out/street-centroid.js.map +0 -1
  488. package/out/street-morphology-fst-builder.d.ts.map +0 -1
  489. package/out/street-morphology-fst-builder.js.map +0 -1
  490. package/out/street-morphology-fst-loader.d.ts.map +0 -1
  491. package/out/street-morphology-fst-loader.js.map +0 -1
  492. package/out/street-name-lookup.d.ts.map +0 -1
  493. package/out/street-name-lookup.js.map +0 -1
  494. package/out/street-normalize.d.ts +0 -95
  495. package/out/street-normalize.d.ts.map +0 -1
  496. package/out/street-normalize.js +0 -272
  497. package/out/street-normalize.js.map +0 -1
  498. package/out/street-segment-schema.d.ts.map +0 -1
  499. package/sqlite-utils.ts +0 -46
  500. package/street-normalize.ts +0 -318
  501. /package/{postal-city-alias-schema.ts → lib/postal/city-alias-schema.ts} +0 -0
@@ -0,0 +1,878 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * Shared development tooling for weights overlays: the symlink primitives every overlay linker uses, the
6
+ * placetype-pair index build, and the manifest form ({@link materializeDevOverlay}) each locale's
7
+ * `scripts/link-dev-weights.ts` declares itself in.
8
+ */
9
+
10
+ import { dataRootPath } from "@mailwoman/core/data-root"
11
+ import {
12
+ pathExists,
13
+ readLocalJSONFile,
14
+ readLocalTextFile,
15
+ statPath,
16
+ readLocalBuffer,
17
+ statLink,
18
+ } from "@mailwoman/core/fs/readers"
19
+ import {
20
+ createSymbolicLink,
21
+ makeDirectories,
22
+ movePath,
23
+ removePath,
24
+ writeLocalTextFile,
25
+ } from "@mailwoman/core/fs/writers"
26
+ import { md5File } from "@mailwoman/core/hash"
27
+ import { parseJSONStrict } from "@mailwoman/core/json"
28
+ import { repoRootPath, workspacePath } from "@mailwoman/core/paths"
29
+ import { spawnProcessSync } from "@mailwoman/core/process"
30
+ import { readReleaseConfig, repoCommittedSoftFeedSources } from "@mailwoman/core/release-config"
31
+ import { weightsOverlayPath } from "@mailwoman/core/utils"
32
+ import { resolvePath } from "path-ts"
33
+
34
+ import { $public } from "#env"
35
+ import { fstFreshnessWarning } from "#fst/freshness"
36
+
37
+ /**
38
+ * Replicate `ln -sf SRC DEST` ATOMICALLY: symlink under a temp name, then rename over the destination. A plain
39
+ * unlink-then-symlink leaves a no-file window that concurrent vitest workers can hit mid-suite — bit CI on 2026-07-24.
40
+ * rename(2) replaces the destination atomically.
41
+ */
42
+ export async function linkForce(src: string, dest: string): Promise<void> {
43
+ const tmp = `${dest}.tmp-link`
44
+
45
+ if (await pathExists(tmp)) {
46
+ await removePath(tmp)
47
+ }
48
+
49
+ await createSymbolicLink(src, tmp)
50
+ await movePath(tmp, dest)
51
+ }
52
+
53
+ /**
54
+ * Remove a leftover local file/symlink so the #1179 base-weights fallback engages.
55
+ */
56
+ export async function removeIfPresent(dest: string): Promise<void> {
57
+ try {
58
+ await statLink(dest)
59
+ } catch {
60
+ return
61
+ }
62
+
63
+ await removePath(dest)
64
+
65
+ console.log(`removed stale local ${dest} (base fallback to en-us engages)`)
66
+ }
67
+
68
+ /**
69
+ * Symlink one soft-feed sibling into an overlay, warning rather than failing when the source is absent.
70
+ *
71
+ * Every one of these artifacts is OPTIONAL by design — the runtime has a fallback for each, so a fresh worktree that
72
+ * has not built the gazetteer still geocodes. That is why the miss prints the consequence instead of throwing: the
73
+ * operator needs to know which channel just resolved OFF, not to have the link step abort.
74
+ */
75
+ export async function linkSoftFeedSibling(
76
+ source: string,
77
+ destination: string,
78
+ consequenceIfMissing: string
79
+ ): Promise<boolean> {
80
+ if (!(await pathExists(source))) {
81
+ console.error(`WARNING: missing ${source} — ${consequenceIfMissing}`)
82
+
83
+ return false
84
+ }
85
+
86
+ await linkForce(source, destination)
87
+
88
+ console.log(`linked ${destination} \u2190 ${source}`)
89
+
90
+ return true
91
+ }
92
+
93
+ /**
94
+ * The calibrated pair-index emission bias every shipped artifact is built at today — the R5/R6/R9/R10/R11 bars were all
95
+ * measured at δ=10 (each linker's own docstring carries its locale's receipt).
96
+ */
97
+ export const PAIR_INDEX_DELTA = 10
98
+
99
+ /**
100
+ * The decoder transition-entry bonus (TRANSITION-BETA build, 2026-07-24 — operator-approved β=5 from the
101
+ * transition-level probe). en-nz deliberately builds WITHOUT one (unmeasured there): the two magnitudes are calibrated
102
+ * independently, and a locale earning one says nothing about the other.
103
+ */
104
+ export const PAIR_INDEX_TRANSITION_BETA = 5
105
+
106
+ /**
107
+ * The WHOLE-EDGE parent-bias magnitude (#46, default-on 2026-08-04) at the verdict's recommended δ=5 — see
108
+ * `docs/records/evals/2026-08-04-pix1-whole-edge-verdict.md`. Only the measured locales (us/gb/nz/fr) pass it; the
109
+ * D-rule's answer to an unmeasured locale is a per-locale absence, not an inherited magnitude.
110
+ */
111
+ export const PAIR_INDEX_PARENT_DELTA = 5
112
+
113
+ /**
114
+ * What one pair-index build has to say about itself. Everything else is shared.
115
+ */
116
+ export interface PairIndexOverlay {
117
+ /**
118
+ * Workspace directory name, e.g. `neural-weights-de-de`.
119
+ */
120
+ packageDir: string
121
+ /**
122
+ * ISO country code passed to `gazetteer pair-index --country`, and the suffix of the built artifact (`de` →
123
+ * `pair-index-de.bin`). NOT the locale tag: `en-in` builds `pair-index-in.bin`.
124
+ */
125
+ country: string
126
+ /**
127
+ * Calibrated magnitudes the locale's bars were measured at. Baked into the artifact's PIX1 header, which is how the
128
+ * freshness check notices a change to any of them. An ABSENT `transitionBeta`/`parentDelta` means the flag is not
129
+ * passed and the header carries no such key — a real state, distinct from zero (see `PairIndexHeader.parentDelta`).
130
+ * The whole calibration feeds both the build FLAGS and the staleness EXPECTATION, so the two cannot disagree.
131
+ */
132
+ delta: number
133
+ transitionBeta?: number
134
+ parentDelta?: number
135
+ /**
136
+ * Source FILES whose md5s the build records, in the order `gazetteer pair-index` records them. The freshness guard
137
+ * compares EVERY one against the existing header (#1734): a partial comparison leaves the guard blind to the rest,
138
+ * and a stale artifact then keeps reporting itself fresh while the data it was built from has moved. Empty means the
139
+ * build's source cannot be file-hashed (fr's BAN directory) and freshness rests on the magnitudes alone. Default: the
140
+ * WOF admin DB.
141
+ */
142
+ sources?: string[]
143
+ /**
144
+ * Inputs that must exist before a build is attempted — a missing one warns and returns, because a fresh clone has no
145
+ * data root and `yarn test` invokes the linkers to verify auto-resolve. Default: `sources`.
146
+ */
147
+ inputs?: string[]
148
+ /**
149
+ * Extra CLI args naming the build's sources (`--source`, `--borough-db`, `--pairs-jsonl`, `--ban-dir`). Default:
150
+ * `--borough-db <admin db>`.
151
+ */
152
+ extraArgs?: string[]
153
+ /**
154
+ * Refuse to trust an existing artifact smaller than this. fr-fr's guard: a pair index built from the WRONG source can
155
+ * carry matching magnitudes, and size is the one signal the header cannot fake.
156
+ */
157
+ minimumPlausibleBytes?: number
158
+ }
159
+
160
+ /**
161
+ * The header fields a dev-weights freshness guard reads.
162
+ */
163
+ export interface PairIndexHeaderFields {
164
+ delta: number
165
+ transitionBeta: number | undefined
166
+ parentDelta: number | undefined
167
+ schemaVersion: number
168
+ /**
169
+ * One md5 per source the build read, in fold order. Empty on a header that recorded none.
170
+ */
171
+ sourceMD5s: string[]
172
+ }
173
+
174
+ /**
175
+ * Minimal PIX1 header reader — magic + header only, reimplemented so a data-only weights package gains no dependency on
176
+ * `@mailwoman/neural` (which pulls onnxruntime-node) to read a few fields. `neural/pair-index-resolver.ts`'s own header
177
+ * parse is the source of truth this must follow.
178
+ *
179
+ * Shared by the overlay build below AND by the four hand-written base linkers
180
+ * (`neural-weights-{en-us,en-gb,en-nz,fr-fr}/scripts/link-dev-weights.ts`), which each carried their own near-copy
181
+ * before 2026-08-04 — the ×5 clone the taste audit named, and the reason three of them were schema-blind while this one
182
+ * was not.
183
+ */
184
+ export async function peekPairIndexHeaderFields(path: string): Promise<PairIndexHeaderFields> {
185
+ const bytes = await readLocalBuffer(path)
186
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
187
+ // "PIX1" little-endian.
188
+ const MAGIC = 0x31_58_49_50
189
+
190
+ if (view.getUint32(0, true) !== MAGIC) {
191
+ throw new Error(`pair index: bad magic reading ${path}`)
192
+ }
193
+
194
+ const headerLen = view.getUint32(4, true)
195
+
196
+ const header = parseJSONStrict<{
197
+ delta: number
198
+ transitionBeta?: number
199
+ parentDelta?: number
200
+ schemaVersion: number
201
+ sourceMD5s?: string[]
202
+ }>(Buffer.from(bytes.subarray(8, 8 + headerLen)).toString("utf8"))
203
+
204
+ return {
205
+ delta: header.delta,
206
+ transitionBeta: header.transitionBeta,
207
+ parentDelta: header.parentDelta,
208
+ schemaVersion: header.schemaVersion,
209
+ sourceMD5s: header.sourceMD5s ?? [],
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Hex characters in an md5 digest.
215
+ */
216
+ const MD5_HEX_LENGTH = 32
217
+
218
+ /**
219
+ * Md5 of `path`, cached in a standard `md5sum`-format sidecar (`<hash> <filename>`) beside it so a multi-gigabyte
220
+ * source is hashed once per change rather than once per linker run. The sidecar is trusted only when at least as new as
221
+ * the source; a missing or stale sidecar recomputes and rewrites, so the cache self-heals.
222
+ *
223
+ * The shared home for the copies the base linkers (`en-us`, `en-gb`, `en-nz`) each carry — new callers import this one.
224
+ */
225
+ export async function md5FileWithSidecar(path: string): Promise<string> {
226
+ const sidecarPath = `${path}.md5`
227
+ const sourceStats = await statPath(path)
228
+
229
+ if (await pathExists(sidecarPath)) {
230
+ try {
231
+ if ((await statPath(sidecarPath)).mtime >= sourceStats.mtime) {
232
+ const [hash] = (await readLocalTextFile(sidecarPath)).trim().split(/\s+/)
233
+
234
+ if (hash && hash.length === MD5_HEX_LENGTH) return hash
235
+ }
236
+ } catch {
237
+ // Unreadable sidecar — recompute below.
238
+ }
239
+ }
240
+
241
+ const hash = await md5File(path)
242
+ const filename = path.split(/[/\\]/).pop() || path
243
+
244
+ await writeLocalTextFile(`${hash} ${filename}\n`, sidecarPath)
245
+
246
+ return hash
247
+ }
248
+
249
+ /**
250
+ * The calibrated magnitudes a linker bakes into its artifact. `undefined` means the flag is NOT passed and the header
251
+ * carries no such key — a real state, distinct from zero (see `PairIndexHeader.parentDelta`), so the comparison below
252
+ * is `!==` against `undefined` rather than a truthiness test.
253
+ */
254
+ export interface PairIndexCalibration {
255
+ delta: number
256
+ transitionBeta?: number
257
+ parentDelta?: number
258
+ }
259
+
260
+ /**
261
+ * Why an existing `pair-index-*.bin` is stale against `expected`, or `undefined` when its header matches. Covers the
262
+ * FORMAT (schemaVersion) and every calibrated magnitude; source-md5 freshness stays with the caller, because each base
263
+ * linker passes a different set of sources and only it knows what they are.
264
+ *
265
+ * One place so a magnitude added to the header cannot be checked by some linkers and not others — which is exactly how
266
+ * three of the four base linkers ended up unable to notice a schema bump.
267
+ */
268
+ export function pairIndexStaleReason(
269
+ header: PairIndexHeaderFields,
270
+ expected: PairIndexCalibration
271
+ ): string | undefined {
272
+ if (header.schemaVersion !== REQUIRED_PAIR_INDEX_SCHEMA) {
273
+ return `schemaVersion ${header.schemaVersion} → ${REQUIRED_PAIR_INDEX_SCHEMA}`
274
+ }
275
+
276
+ if (header.delta !== expected.delta) return `delta ${header.delta} → ${expected.delta}`
277
+
278
+ if (header.transitionBeta !== expected.transitionBeta) {
279
+ return `transitionBeta ${header.transitionBeta ?? "(absent)"} → ${expected.transitionBeta ?? "(absent)"}`
280
+ }
281
+
282
+ if (header.parentDelta !== expected.parentDelta) {
283
+ return `parentDelta ${header.parentDelta ?? "(absent)"} → ${expected.parentDelta ?? "(absent)"}`
284
+ }
285
+
286
+ return undefined
287
+ }
288
+
289
+ /**
290
+ * The PIX1 schema this tree's reader requires. MUST equal `KNOWN_SCHEMA_VERSION` in `neural/pair-index-resolver.ts` —
291
+ * they are two ends of one fact, and this copy exists only because a data-only overlay must not gain a dependency on
292
+ * `@mailwoman/neural` (onnxruntime-node) to read one header field. Bump BOTH in the same commit; a schema bump that
293
+ * leaves this behind makes every dev checkout rebuild-loop or serve an artifact the runtime refuses.
294
+ *
295
+ * The freshness guard must compare it: a guard that checks only delta + source md5 reads a format-obsolete binary as
296
+ * "current" and leaves every dev checkout with artifacts the runtime refuses — the R5 freshness-guard lesson, format
297
+ * edition. Also compared by the four hand-written base linkers (`neural-weights-{en-us,en-gb,en-nz,fr-fr}`), which
298
+ * import this constant rather than re-typing the number.
299
+ */
300
+ export const REQUIRED_PAIR_INDEX_SCHEMA = 3
301
+
302
+ /**
303
+ * Warn when the per-locale FST a linker just symlinked was built from a DIFFERENT admin database than the one on disk
304
+ * now.
305
+ *
306
+ * WHY IT WARNS RATHER THAN REBUILDS, unlike its pair-index sibling above. A pair index is seconds of work and the
307
+ * linker owns its whole recipe. A locale FST is a multi-minute build whose output goes to a STAGING dir on purpose —
308
+ * the swap into `fst-per-locale/` is operator-approved after the battery, because an FST changes decoder behaviour and
309
+ * the D-rule does not let that land unmeasured. So the guard's job is to make the drift impossible to miss, and to name
310
+ * the command that starts fixing it. It is also why a stale FST is never fatal: the artifact is a decode-time bias
311
+ * list, and a dev tree must still run.
312
+ *
313
+ * The source-side half of the comparison lives here rather than in `fst-freshness.ts` for the same reason
314
+ * `pairIndexStaleReason` splits: only the caller knows which database it built against. All three FST-linking base
315
+ * packages build against the same one, so they share this rather than each pinning it.
316
+ */
317
+ export async function warnIfFSTStale(fstPath: string, locale: string): Promise<void> {
318
+ const warning = await fstFreshnessWarning({
319
+ fstPath,
320
+ sourceDBPath: String(dataRootPath("wof", "admin-global-priority.db")),
321
+ rebuildCommand: `node packages/mailwoman/out/cli.js gazetteer build fst --locales ${locale} (writes to a staging dir; swap is operator-conditional)`,
322
+ })
323
+
324
+ if (warning) {
325
+ console.error(warning)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Symlink the per-locale FST gazetteer (`fst-<locale>.bin`) from the shared build area
331
+ * (`$MAILWOMAN_DATA_ROOT/wof/fst-per-locale/`) into an overlay so `resolveWeights` surfaces `fstPath` in dev and the
332
+ * runtime pipeline can auto-wire the gazetteer + street-context check, then run {@link warnIfFSTStale} on the linked
333
+ * artifact. The publish flow stages the real binary (release-sequenced).
334
+ */
335
+ export async function linkLocaleFST(destDir: string, locale: string): Promise<void> {
336
+ const source = String(dataRootPath("wof", "fst-per-locale", `fst-${locale}.bin`))
337
+
338
+ const linked = await linkSoftFeedSibling(
339
+ source,
340
+ resolvePath(destDir, `fst-${locale}.bin`),
341
+ "the FST gazetteer default will resolve OFF for this locale."
342
+ )
343
+
344
+ if (linked) {
345
+ await warnIfFSTStale(source, locale)
346
+ }
347
+ }
348
+
349
+ /**
350
+ * Symlink the sealed locale-general street-morphology FST (`$MAILWOMAN_DATA_ROOT/wof/fst-street-morphology.bin`,
351
+ * `mailwoman gazetteer build street-morphology`) into an overlay so `resolveWeights` surfaces `streetMorphologyPath` in
352
+ * dev and the street-context check (#1315) deserializes the artifact instead of rebuilding from the libpostal
353
+ * dictionaries per process. Missing is non-fatal — the runtime loader's dictionary-build fallback covers it.
354
+ */
355
+ export async function linkStreetMorphologyFST(destDir: string): Promise<void> {
356
+ await linkSoftFeedSibling(
357
+ String(dataRootPath("wof", "fst-street-morphology.bin")),
358
+ resolvePath(destDir, "fst-street-morphology.bin"),
359
+ "the street-context check falls back to the per-process dictionary build."
360
+ )
361
+ }
362
+
363
+ /**
364
+ * Why an existing artifact may be TRUSTED without a rebuild, or `undefined` when it must be rebuilt (with the reason
365
+ * already printed). The skip requires both halves (#1734): the header magnitudes + format (`pairIndexStaleReason`) AND
366
+ * the header's `sourceMD5s` against the current sources' md5s. Magnitudes alone read a source change as "current"
367
+ * whenever pair counts happen not to move the calibrated numbers — the R5 freshness-guard lesson, which resurfaced in
368
+ * the 2026-08-18 admin swap. A source that is not on disk to re-hash leaves the magnitudes as the best available answer
369
+ * (the "missing source, can't build" branch below would fire anyway if a rebuild were needed).
370
+ */
371
+ async function pairIndexIsFresh(
372
+ dest: string,
373
+ artifact: string,
374
+ expected: PairIndexCalibration,
375
+ sources: string[],
376
+ minimumPlausibleBytes: number | undefined
377
+ ): Promise<boolean> {
378
+ try {
379
+ const header = await peekPairIndexHeaderFields(dest)
380
+ const staleReason = pairIndexStaleReason(header, expected)
381
+
382
+ if (staleReason) {
383
+ console.log(`rebuilding ${artifact} — ${staleReason}`)
384
+
385
+ return false
386
+ }
387
+
388
+ if (minimumPlausibleBytes !== undefined && (await statPath(dest)).size < minimumPlausibleBytes) {
389
+ console.log(
390
+ `rebuilding ${artifact} — ${(await statPath(dest)).size.toLocaleString()} bytes is implausibly small ` +
391
+ `for this recipe (wrong-source clobber)`
392
+ )
393
+
394
+ return false
395
+ }
396
+
397
+ if (!sources.length) {
398
+ console.log(`skipped ${artifact} build — ${dest} is current (magnitudes match; no file-hashable source)`)
399
+
400
+ return true
401
+ }
402
+
403
+ for (const source of sources) {
404
+ if (!(await pathExists(source))) {
405
+ console.log(
406
+ `skipped ${artifact} build — ${dest} has matching header magnitudes (${source} absent, md5 freshness unverifiable)`
407
+ )
408
+
409
+ return true
410
+ }
411
+ }
412
+
413
+ if (header.sourceMD5s.length !== sources.length) {
414
+ console.log(
415
+ `rebuilding ${artifact} — header records ${header.sourceMD5s.length} source md5s ` +
416
+ `[${header.sourceMD5s.join(", ") || "(none recorded)"}], but this build reads ${sources.length}`
417
+ )
418
+
419
+ return false
420
+ }
421
+
422
+ const currentMD5s: string[] = []
423
+
424
+ for (const source of sources) {
425
+ currentMD5s.push(await md5FileWithSidecar(source))
426
+ }
427
+
428
+ if (currentMD5s.every((md5, i) => md5 === header.sourceMD5s[i])) {
429
+ console.log(
430
+ `skipped ${artifact} build — ${dest} is current (magnitudes + all ${sources.length} source md5s match)`
431
+ )
432
+
433
+ return true
434
+ }
435
+
436
+ console.log(
437
+ `rebuilding ${artifact} — header source md5s [${header.sourceMD5s.join(", ")}] != current [${currentMD5s.join(", ")}]`
438
+ )
439
+
440
+ return false
441
+ } catch (error) {
442
+ console.log(`rebuilding ${artifact} — freshness unverifiable (${(error as Error).message})`)
443
+
444
+ return false
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Build `pair-index-<country>.bin` into the overlay, skipping the work when the artifact on disk was already built at
450
+ * these magnitudes FROM the sources on disk (see {@link pairIndexIsFresh}). The calibration object drives both the
451
+ * staleness expectation and the CLI flags, so a magnitude cannot be checked by the guard and dropped from the build.
452
+ *
453
+ * Exits non-zero on a failed build. Missing INPUTS (an unbuilt CLI, an absent source) warn and return instead: a fresh
454
+ * clone has neither, and `yarn test` invokes this to verify auto-resolve, so a hard failure there would be a failure to
455
+ * have run a build yet rather than a real fault.
456
+ */
457
+ export async function buildPairIndexOverlay(overlay: PairIndexOverlay): Promise<void> {
458
+ const { packageDir, country, delta, transitionBeta, parentDelta } = overlay
459
+ const CLI = String(workspacePath("mailwoman", "out", "cli.js"))
460
+ const ARTIFACT = `pair-index-${country}.bin`
461
+ // Built into the data-root OVERLAY, not into the tracked package. The locale is recovered from the
462
+ // workspace name (`neural-weights-en-gb` → `en-gb`) so callers keep passing the one identifier they
463
+ // already had.
464
+ const PKG_DIR = String(weightsOverlayPath(packageDir.replace(/^neural-weights-/, "")))
465
+ const DEST = resolvePath(PKG_DIR, ARTIFACT)
466
+
467
+ await makeDirectories(PKG_DIR)
468
+
469
+ /**
470
+ * Checked-in WOF-derived admin pairs — the default source, and the whole source list for the small overlays.
471
+ */
472
+ const WOF_ADMIN_DB = String(dataRootPath("wof", "admin-global-priority.db"))
473
+ const sources = overlay.sources ?? [WOF_ADMIN_DB]
474
+ const inputs = overlay.inputs ?? sources
475
+ const extraArgs = overlay.extraArgs ?? ["--borough-db", WOF_ADMIN_DB]
476
+
477
+ if (!(await pathExists(CLI))) {
478
+ console.error(`WARNING: ${CLI} not built — run \`yarn compile\` first, then re-run for ${ARTIFACT}.`)
479
+
480
+ return
481
+ }
482
+
483
+ if (
484
+ (await pathExists(DEST)) &&
485
+ (await pairIndexIsFresh(
486
+ DEST,
487
+ ARTIFACT,
488
+ { delta, transitionBeta, parentDelta },
489
+ sources,
490
+ overlay.minimumPlausibleBytes
491
+ ))
492
+ ) {
493
+ return
494
+ }
495
+
496
+ for (const input of inputs) {
497
+ if (await pathExists(input)) continue
498
+
499
+ console.error(
500
+ `WARNING: missing ${input} — ${ARTIFACT} not built; the placetype-pair prior stays inert for ${country.toUpperCase()}.`
501
+ )
502
+
503
+ return
504
+ }
505
+
506
+ const result = spawnProcessSync(
507
+ process.execPath,
508
+ [
509
+ CLI,
510
+ "gazetteer",
511
+ "pair-index",
512
+ "--out",
513
+ PKG_DIR,
514
+ "--country",
515
+ country,
516
+ "--delta",
517
+ String(delta),
518
+ ...(transitionBeta === undefined ? [] : ["--transition-beta", String(transitionBeta)]),
519
+ ...(parentDelta === undefined ? [] : ["--parent-delta", String(parentDelta)]),
520
+ ...extraArgs,
521
+ ],
522
+ { stdio: "inherit" }
523
+ )
524
+
525
+ if (result.status !== 0 || !(await pathExists(DEST))) {
526
+ console.error(`FAILED: gazetteer pair-index --country ${country} (exit ${result.status})`)
527
+
528
+ process.exit(1)
529
+ }
530
+
531
+ console.log(`built ${ARTIFACT}`)
532
+ }
533
+
534
+ // --- the manifest form ----------------------------------------------------------------------------------------------
535
+
536
+ /**
537
+ * A soft-feed sibling an overlay links: where it comes from, the name it takes in the overlay, and the consequence line
538
+ * printed when the source is missing (the link is warn-and-continue; the channel resolves OFF).
539
+ */
540
+ export interface SoftFeedLink {
541
+ source: string
542
+ name: string
543
+ consequenceIfMissing: string
544
+ }
545
+
546
+ /**
547
+ * The committed soft-feed lexicons as links, by channel, read from `release.config.json` so the filename a manifest
548
+ * links is the one the release ships. `streetType` is offered for completeness; the manifests link the evidence
549
+ * lexicons by the generation their CARD names instead (`evidenceLexiconsFromCard`).
550
+ */
551
+ export async function committedSoftFeedLinks(): Promise<{
552
+ anchor: SoftFeedLink
553
+ country: SoftFeedLink
554
+ streetType?: SoftFeedLink
555
+ }> {
556
+ const config = await readReleaseConfig()
557
+ const sources = repoCommittedSoftFeedSources(repoRootPath(), config.softFeed ?? {})
558
+
559
+ const link = (name: string, consequenceIfMissing: string): SoftFeedLink | undefined => {
560
+ const source = sources.get(name)
561
+
562
+ return source ? { source, name, consequenceIfMissing } : undefined
563
+ }
564
+
565
+ const anchor = link("anchor-lexicon-v1.json", "gazetteer channel will resolve OFF in this worktree.")
566
+ const country = link("country-surface-lexicon-v1.json", "country channel will resolve OFF in this worktree.")
567
+
568
+ if (!anchor || !country) {
569
+ throw new Error(
570
+ "release.config.json names no softFeed.gazetteerLexicon / softFeed.countryLexicon — the overlays cannot link them."
571
+ )
572
+ }
573
+
574
+ const streetType = link("street-type-lexicon-v3.json", "the street_type channel will resolve OFF in this worktree.")
575
+
576
+ return { anchor, country, ...(streetType ? { streetType } : {}) }
577
+ }
578
+
579
+ /**
580
+ * The slice of a weights package's committed `model-card.json` the dev materialization reads: the shipped digests the
581
+ * release re-verifies against the published tarball, and the channel requirements that name evidence lexicons.
582
+ */
583
+ export interface WeightsCard {
584
+ files_md5?: Record<string, string>
585
+ requires?: Record<string, { lexicon?: string; span_mode?: string } | undefined>
586
+ }
587
+
588
+ /**
589
+ * What one locale's `scripts/link-dev-weights.ts` declares, so an overlay is a manifest plus a call rather than a
590
+ * script cloned from a sibling. Every step is optional and runs only when named, in a fixed order: model pair,
591
+ * soft-feed siblings, card-named evidence lexicons, postcode binary, pair index, FSTs.
592
+ */
593
+ export interface DevOverlayManifest {
594
+ /**
595
+ * The overlay's locale tag, lower-case. The workspace is `neural-weights-<locale>` and the artifacts land in
596
+ * `$MAILWOMAN_DATA_ROOT/weights/<locale>/`, never in the tracked package: the binaries are not in git, so
597
+ * materializing them there made a fresh worktree unable to geocode, made `yarn test` mutate a tracked directory, and
598
+ * put a symlink into a publish tarball (`YN0035`).
599
+ */
600
+ locale: string
601
+ /**
602
+ * What happens to `model.onnx` + `tokenizer.model`. `link`: the pair is symlinked from the paths
603
+ * `release.config.json`'s `weights` block names under the data root (`$MAILWOMAN_DEV_MODEL` /
604
+ * `$MAILWOMAN_DEV_TOKENIZER` override them), and when `digestCard` names a workspace the linked default bytes must
605
+ * match that workspace's card `files_md5` — the #397 drift guard, which fails loud instead of grading the wrong
606
+ * model; an override skips the check and says so. `inherit`: the package declares `mailwoman.baseWeights`, so any
607
+ * local pair is REMOVED — a stale local file shadows the base fallback and silently serves outdated bytes. Omitted:
608
+ * the pair is left to `packages/release-kit/lib/weights/link-weights-overlay.ts`, the recipe writer.
609
+ */
610
+ model?: { kind: "link"; digestCard?: string } | { kind: "inherit" } | { kind: "char"; family: string }
611
+ /**
612
+ * Soft-feed siblings linked warn-and-continue, in order.
613
+ */
614
+ softFeed?: ReadonlyArray<SoftFeedLink>
615
+ /**
616
+ * Link the evidence lexicons the overlay's own card names under `requires.<channel>.lexicon`, so the generation comes
617
+ * from the card the loader reads and a card bump moves the artifact with it. `street-type-lexicon-v*.json` is
618
+ * committed under `data/gazetteer/`; `locality-surface-lexicon-v*.json` is 7–13 MB and lives in the data root.
619
+ */
620
+ evidenceLexiconsFromCard?: boolean
621
+ /**
622
+ * Build `postcode-<country>.bin` from a WOF postcode extract through the compiled `gazetteer postcode-binary` CLI,
623
+ * skipped when already present (it rebuilds in seconds, and the extract is versionless on disk).
624
+ */
625
+ postcodeBinary?: { country: string; database: string }
626
+ /**
627
+ * The placetype-pair index build, minus `packageDir`, which follows from `locale`.
628
+ */
629
+ pairIndex?: Omit<PairIndexOverlay, "packageDir">
630
+ /**
631
+ * Link `fst-<locale>.bin` with its freshness warning.
632
+ */
633
+ localeFST?: boolean
634
+ /**
635
+ * Link the shared street-morphology FST.
636
+ */
637
+ streetMorphologyFST?: boolean
638
+ }
639
+
640
+ /**
641
+ * What a materialized overlay hands back for the locale-specific step a manifest cannot express (en-gb's
642
+ * card-conditional postcode binary is the one that exists).
643
+ */
644
+ export interface DevOverlay {
645
+ destDir: string
646
+ cli: string
647
+ card: WeightsCard | undefined
648
+ }
649
+
650
+ /**
651
+ * Where each evidence channel's lexicon is found, by the name the card declares.
652
+ */
653
+ const EVIDENCE_LEXICON_SOURCES: ReadonlyArray<{
654
+ channel: "street_type" | "locality_surface"
655
+ source: (name: string) => string
656
+ }> = [
657
+ { channel: "street_type", source: (name) => String(repoRootPath("data", "gazetteer", name)) },
658
+ { channel: "locality_surface", source: (name) => String(dataRootPath("gazetteer", name)) },
659
+ ]
660
+
661
+ /**
662
+ * Read a weights workspace's committed card, or `undefined` when the workspace carries none.
663
+ */
664
+ async function readWeightsCard(workspace: string): Promise<WeightsCard | undefined> {
665
+ const path = resolvePath(String(workspacePath(workspace)), "model-card.json")
666
+
667
+ if (!(await pathExists(path))) return undefined
668
+
669
+ return readLocalJSONFile<WeightsCard>(path)
670
+ }
671
+
672
+ /**
673
+ * Link the base model pair from the release recipe and, when a digest card is named, hold the linked default bytes to
674
+ * that card's `files_md5`. The recipe and the card are the two registers a ship bumps in lockstep; a path bumped
675
+ * without the card, or the reverse, fails here rather than after an eval shift graded against the wrong weights.
676
+ */
677
+ /**
678
+ * A char-path base (#2164): the graph and its sealed vocabulary from `release.config.json`'s `charWeights[family]`, no
679
+ * tokenizer. The dev overlay carries both so `resolveWeights` finds the package whole.
680
+ */
681
+ async function linkCharModel(destDir: string, family: string): Promise<void> {
682
+ const recipe = (await readReleaseConfig()).charWeights?.[family]
683
+
684
+ if (!recipe) {
685
+ throw new Error(`release.config.json declares no charWeights.${family} — nothing to link for the ${family} base`)
686
+ }
687
+
688
+ const dataRoot = String(dataRootPath())
689
+
690
+ for (const [name, relative] of [
691
+ ["model.onnx", recipe.model],
692
+ ["char-vocab.json", recipe.charVocab],
693
+ ] as const) {
694
+ const source = resolvePath(dataRoot, relative)
695
+
696
+ if (!(await pathExists(source))) {
697
+ throw new Error(`missing char-path source ${name} for ${family}: ${source}`)
698
+ }
699
+
700
+ await linkForce(source, resolvePath(destDir, name))
701
+
702
+ console.log(`linked ${resolvePath(destDir, name)} ← ${source}`)
703
+ }
704
+
705
+ // The card is what tells `resolveWeights` this package owes a vocabulary rather than a tokenizer (its `encoder`
706
+ // block), so the overlay carries the workspace's committed card beside the two binaries.
707
+ const card = resolvePath(String(workspacePath(`neural-weights-${family}`)), "model-card.json")
708
+
709
+ await linkForce(card, resolvePath(destDir, "model-card.json"))
710
+ await removeIfPresent(resolvePath(destDir, "tokenizer.model"))
711
+ }
712
+
713
+ async function linkBaseModelPair(destDir: string, digestCard: string | undefined): Promise<void> {
714
+ const recipe = await readReleaseConfig()
715
+
716
+ const dataRoot = String(dataRootPath())
717
+ const digests = digestCard ? (await readWeightsCard(digestCard))?.files_md5 : undefined
718
+
719
+ const pair = [
720
+ { label: "model", name: "model.onnx", override: $public.MAILWOMAN_DEV_MODEL, recipe: recipe.weights.model },
721
+ {
722
+ label: "tokenizer",
723
+ name: "tokenizer.model",
724
+ override: $public.MAILWOMAN_DEV_TOKENIZER,
725
+ recipe: recipe.weights.tokenizer,
726
+ },
727
+ ]
728
+
729
+ for (const { label, name, override, recipe: recipePath } of pair) {
730
+ const source = override || resolvePath(dataRoot, recipePath)
731
+
732
+ if (!(await pathExists(source))) {
733
+ throw new Error(`missing source ${label}: ${source} — set MAILWOMAN_DEV_${label.toUpperCase()} to override`)
734
+ }
735
+
736
+ const dest = resolvePath(destDir, name)
737
+
738
+ await linkForce(source, dest)
739
+
740
+ console.log(`linked ${dest} ← ${source}`)
741
+
742
+ if (!digestCard) continue
743
+
744
+ if (override) {
745
+ console.error(` (${label} override active — skipping the #397 default-digest check)`)
746
+
747
+ continue
748
+ }
749
+
750
+ const expected = digests?.[name]
751
+
752
+ if (!expected) {
753
+ throw new Error(
754
+ `#397 guard: ${digestCard}/model-card.json has no files_md5 entry for ${name} — cannot verify the dev pin.`
755
+ )
756
+ }
757
+
758
+ const actual = await md5File(dest)
759
+
760
+ if (actual !== expected) {
761
+ throw new Error(
762
+ `#397 guard: linked default ${label} md5 ${actual} != shipped ${expected} (${digestCard} model-card files_md5). ` +
763
+ `The dev link has drifted from the shipped default: bump release.config.json weights.${label} and the card's ` +
764
+ `files_md5 in lockstep.`
765
+ )
766
+ }
767
+
768
+ console.log(` ${name} digest ok`)
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Build a postcode binary from a WOF postcode extract, skip-if-present. A missing CLI or extract is a warning (the
774
+ * anchor channel resolves OFF for that country); a build that runs and fails is an error.
775
+ */
776
+ async function buildPostcodeBinary(
777
+ destDir: string,
778
+ cli: string,
779
+ { country, database }: { country: string; database: string }
780
+ ): Promise<void> {
781
+ const artifact = `postcode-${country.toLowerCase()}.bin`
782
+ const dest = resolvePath(destDir, artifact)
783
+
784
+ if (await pathExists(dest)) {
785
+ console.log(`skipped ${artifact} build — ${dest} already present`)
786
+
787
+ return
788
+ }
789
+
790
+ if (!(await pathExists(cli))) {
791
+ console.error(
792
+ `WARNING: ${cli} not built — run \`yarn compile\` first, then re-run this script to build ${artifact}.`
793
+ )
794
+
795
+ return
796
+ }
797
+
798
+ if (!(await pathExists(database))) {
799
+ console.error(
800
+ `WARNING: missing ${database} — ${artifact} not built; the anchor channel will resolve OFF for ${country.toUpperCase()}.`
801
+ )
802
+
803
+ return
804
+ }
805
+
806
+ const result = spawnProcessSync(
807
+ process.execPath,
808
+ [cli, "gazetteer", "postcode-binary", "--out", destDir, "--locale", `${country.toUpperCase()}:${database}`],
809
+ { stdio: "inherit" }
810
+ )
811
+
812
+ if (result.status !== 0 || !(await pathExists(dest))) {
813
+ throw new Error(`failed to build ${dest} (exit ${result.status})`)
814
+ }
815
+
816
+ console.log(`built ${dest}`)
817
+ }
818
+
819
+ /**
820
+ * Materialize one locale's dev overlay from its manifest. The steps run in the order {@link DevOverlayManifest}
821
+ * documents, each independent on disk, and the result carries what a locale-specific step needs afterwards.
822
+ */
823
+ export async function materializeDevOverlay(manifest: DevOverlayManifest): Promise<DevOverlay> {
824
+ const destDir = String(weightsOverlayPath(manifest.locale))
825
+ const cli = String(workspacePath("mailwoman", "out", "cli.js"))
826
+ const card = await readWeightsCard(`neural-weights-${manifest.locale}`)
827
+
828
+ await makeDirectories(destDir)
829
+
830
+ if (manifest.model?.kind === "inherit") {
831
+ await removeIfPresent(resolvePath(destDir, "model.onnx"))
832
+ await removeIfPresent(resolvePath(destDir, "tokenizer.model"))
833
+ } else if (manifest.model?.kind === "link") {
834
+ await linkBaseModelPair(destDir, manifest.model.digestCard)
835
+ } else if (manifest.model?.kind === "char") {
836
+ await linkCharModel(destDir, manifest.model.family)
837
+ }
838
+
839
+ for (const { source, name, consequenceIfMissing } of manifest.softFeed ?? []) {
840
+ await linkSoftFeedSibling(source, resolvePath(destDir, name), consequenceIfMissing)
841
+ }
842
+
843
+ if (manifest.evidenceLexiconsFromCard) {
844
+ for (const { channel, source } of EVIDENCE_LEXICON_SOURCES) {
845
+ const declared = card?.requires?.[channel]?.lexicon
846
+
847
+ if (!declared) {
848
+ console.error(`WARNING: model-card declares no requires.${channel}.lexicon — the ${channel} channel stays OFF.`)
849
+
850
+ continue
851
+ }
852
+
853
+ await linkSoftFeedSibling(
854
+ source(declared),
855
+ resolvePath(destDir, declared),
856
+ `the ${channel} channel will resolve OFF in this worktree.`
857
+ )
858
+ }
859
+ }
860
+
861
+ if (manifest.postcodeBinary) {
862
+ await buildPostcodeBinary(destDir, cli, manifest.postcodeBinary)
863
+ }
864
+
865
+ if (manifest.pairIndex) {
866
+ await buildPairIndexOverlay({ packageDir: `neural-weights-${manifest.locale}`, ...manifest.pairIndex })
867
+ }
868
+
869
+ if (manifest.localeFST) {
870
+ await linkLocaleFST(destDir, manifest.locale)
871
+ }
872
+
873
+ if (manifest.streetMorphologyFST) {
874
+ await linkStreetMorphologyFST(destDir)
875
+ }
876
+
877
+ return { destDir, cli, card }
878
+ }