@mailwoman/resolver-wof-sqlite 10.0.0 → 10.2.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 (477) hide show
  1. package/README.md +9 -8
  2. package/lib/address/point/interpolation.ts +45 -39
  3. package/lib/address/point/schema.ts +17 -40
  4. package/lib/address/point.ts +316 -0
  5. package/lib/{address/index.ts → address.ts} +1 -1
  6. package/lib/ancestry/backfill.ts +20 -86
  7. package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
  8. package/lib/build/candidate.ts +122 -174
  9. package/lib/build/population-corrections.ts +64 -0
  10. package/lib/build/slim.ts +44 -114
  11. package/lib/candidate/alias-bags.ts +16 -17
  12. package/lib/candidate/ancestors/schema.ts +77 -61
  13. package/lib/candidate/ancestors/sidecar.ts +15 -22
  14. package/lib/candidate/country-display-names.ts +6 -20
  15. package/lib/candidate/extract-fold.ts +10 -55
  16. package/lib/candidate/fts.ts +5 -23
  17. package/lib/candidate/importance.ts +65 -68
  18. package/lib/candidate/lookup.ts +124 -415
  19. package/lib/candidate/name-roles.ts +15 -33
  20. package/lib/candidate/own-name.ts +35 -32
  21. package/lib/candidate/place-attrs.ts +20 -13
  22. package/lib/candidate/schema.ts +29 -65
  23. package/lib/candidate/scoring.ts +54 -93
  24. package/lib/capital-schema.ts +21 -17
  25. package/lib/capitals.ts +29 -27
  26. package/lib/coincident-roles.ts +85 -55
  27. package/lib/convention/schema.ts +15 -16
  28. package/lib/convention.ts +144 -0
  29. package/lib/coverage-manifest-schema.ts +40 -45
  30. package/lib/currency-backfill.ts +34 -79
  31. package/lib/env.ts +1 -1
  32. package/lib/exact-match.ts +32 -21
  33. package/lib/extracts.ts +97 -74
  34. package/lib/fst/autocomplete.ts +54 -37
  35. package/lib/fst/builder.ts +37 -41
  36. package/lib/fst/deserialize-web.ts +16 -17
  37. package/lib/fst/format.ts +41 -23
  38. package/lib/fst/freshness.ts +43 -120
  39. package/lib/fst/matcher.ts +7 -5
  40. package/lib/fst/serialize.ts +54 -74
  41. package/lib/fst/types.ts +78 -75
  42. package/lib/{fst/index.ts → fst.ts} +1 -0
  43. package/lib/fts/query.ts +5 -36
  44. package/lib/fts.ts +280 -0
  45. package/lib/geonames/aliases.ts +73 -144
  46. package/lib/geonames/postal.ts +162 -71
  47. package/lib/index.ts +9 -8
  48. package/lib/interpolation.ts +48 -100
  49. package/lib/lookup.ts +155 -270
  50. package/lib/name-score.ts +9 -7
  51. package/lib/nsul/lookup.ts +37 -54
  52. package/lib/nsul/schema.ts +16 -43
  53. package/lib/paths.ts +159 -0
  54. package/lib/place-importance-schema.ts +62 -169
  55. package/lib/poi/lookup.ts +104 -121
  56. package/lib/poi/schema.ts +42 -26
  57. package/lib/polygon-schema.ts +10 -6
  58. package/lib/postal/city/alias/lookup.ts +22 -40
  59. package/lib/postal/city/alias/schema.ts +14 -10
  60. package/lib/postal/city/candidate-schema.ts +26 -20
  61. package/lib/postcode-point-lookup.ts +8 -27
  62. package/lib/primary-preference.ts +46 -126
  63. package/lib/proximity-rerank.ts +44 -33
  64. package/lib/ranking-weights.ts +105 -78
  65. package/lib/region-keys.ts +70 -42
  66. package/lib/reverse.ts +149 -86
  67. package/lib/schema.ts +73 -50
  68. package/lib/search-fetch.ts +90 -68
  69. package/lib/sqlite/convention-source.ts +15 -23
  70. package/lib/sqlite/utils.ts +19 -22
  71. package/lib/street/centroid/schema.ts +37 -42
  72. package/lib/street/{centroid/index.ts → centroid.ts} +16 -25
  73. package/lib/street/morphology/fst/builder.ts +60 -42
  74. package/lib/street/morphology/fst/loader.ts +40 -32
  75. package/lib/street/name-lookup.ts +45 -41
  76. package/lib/street/normalize.ts +88 -219
  77. package/lib/street/segment-schema.ts +116 -43
  78. package/lib/{street/index.ts → street.ts} +1 -1
  79. package/lib/types.ts +109 -150
  80. package/lib/unified-schema.ts +29 -29
  81. package/lib/uprn/existence.ts +30 -19
  82. package/lib/uprn/lookup.ts +29 -68
  83. package/lib/uprn/schema.ts +18 -40
  84. package/lib/weights-overlay-linker.ts +236 -252
  85. package/out/address/point/interpolation.d.ts +20 -23
  86. package/out/address/point/interpolation.d.ts.map +1 -1
  87. package/out/address/point/interpolation.js +42 -39
  88. package/out/address/point/interpolation.js.map +1 -1
  89. package/out/address/point/schema.d.ts +12 -35
  90. package/out/address/point/schema.d.ts.map +1 -1
  91. package/out/address/point/schema.js +3 -19
  92. package/out/address/point/schema.js.map +1 -1
  93. package/out/address/point.d.ts +57 -0
  94. package/out/address/point.d.ts.map +1 -0
  95. package/out/address/point.js +206 -0
  96. package/out/address/point.js.map +1 -0
  97. package/out/{address/index.js → address.d.ts} +2 -2
  98. package/out/address.d.ts.map +1 -0
  99. package/out/{address/index.d.ts → address.js} +2 -2
  100. package/out/address.js.map +1 -0
  101. package/out/ancestry/backfill.d.ts +11 -64
  102. package/out/ancestry/backfill.d.ts.map +1 -1
  103. package/out/ancestry/backfill.js +14 -74
  104. package/out/ancestry/backfill.js.map +1 -1
  105. package/out/ancestry.d.ts +49 -0
  106. package/out/ancestry.d.ts.map +1 -0
  107. package/out/ancestry.js +59 -0
  108. package/out/ancestry.js.map +1 -0
  109. package/out/build/candidate.d.ts +82 -112
  110. package/out/build/candidate.d.ts.map +1 -1
  111. package/out/build/candidate.js +17 -100
  112. package/out/build/candidate.js.map +1 -1
  113. package/out/build/population-corrections.d.ts +52 -0
  114. package/out/build/population-corrections.d.ts.map +1 -0
  115. package/out/build/population-corrections.js +40 -0
  116. package/out/build/population-corrections.js.map +1 -0
  117. package/out/build/slim.d.ts +8 -44
  118. package/out/build/slim.d.ts.map +1 -1
  119. package/out/build/slim.js +33 -94
  120. package/out/build/slim.js.map +1 -1
  121. package/out/candidate/alias-bags.d.ts +6 -6
  122. package/out/candidate/alias-bags.d.ts.map +1 -1
  123. package/out/candidate/alias-bags.js +16 -17
  124. package/out/candidate/alias-bags.js.map +1 -1
  125. package/out/candidate/ancestors/schema.d.ts +73 -57
  126. package/out/candidate/ancestors/schema.d.ts.map +1 -1
  127. package/out/candidate/ancestors/schema.js +56 -48
  128. package/out/candidate/ancestors/schema.js.map +1 -1
  129. package/out/candidate/ancestors/sidecar.d.ts +3 -8
  130. package/out/candidate/ancestors/sidecar.d.ts.map +1 -1
  131. package/out/candidate/ancestors/sidecar.js +15 -22
  132. package/out/candidate/ancestors/sidecar.js.map +1 -1
  133. package/out/candidate/country-display-names.d.ts +5 -15
  134. package/out/candidate/country-display-names.d.ts.map +1 -1
  135. package/out/candidate/country-display-names.js +6 -20
  136. package/out/candidate/country-display-names.js.map +1 -1
  137. package/out/candidate/extract-fold.d.ts +7 -15
  138. package/out/candidate/extract-fold.d.ts.map +1 -1
  139. package/out/candidate/extract-fold.js +5 -47
  140. package/out/candidate/extract-fold.js.map +1 -1
  141. package/out/candidate/fts.d.ts +5 -23
  142. package/out/candidate/fts.d.ts.map +1 -1
  143. package/out/candidate/fts.js +5 -23
  144. package/out/candidate/fts.js.map +1 -1
  145. package/out/candidate/importance.d.ts +60 -65
  146. package/out/candidate/importance.d.ts.map +1 -1
  147. package/out/candidate/importance.js +56 -63
  148. package/out/candidate/importance.js.map +1 -1
  149. package/out/candidate/lookup.d.ts +18 -55
  150. package/out/candidate/lookup.d.ts.map +1 -1
  151. package/out/candidate/lookup.js +82 -385
  152. package/out/candidate/lookup.js.map +1 -1
  153. package/out/candidate/name-roles.d.ts +9 -21
  154. package/out/candidate/name-roles.d.ts.map +1 -1
  155. package/out/candidate/name-roles.js +15 -33
  156. package/out/candidate/name-roles.js.map +1 -1
  157. package/out/candidate/own-name.d.ts +23 -25
  158. package/out/candidate/own-name.d.ts.map +1 -1
  159. package/out/candidate/own-name.js +35 -32
  160. package/out/candidate/own-name.js.map +1 -1
  161. package/out/candidate/place-attrs.d.ts +20 -13
  162. package/out/candidate/place-attrs.d.ts.map +1 -1
  163. package/out/candidate/place-attrs.js +4 -5
  164. package/out/candidate/place-attrs.js.map +1 -1
  165. package/out/candidate/schema.d.ts +27 -62
  166. package/out/candidate/schema.d.ts.map +1 -1
  167. package/out/candidate/schema.js +10 -22
  168. package/out/candidate/schema.js.map +1 -1
  169. package/out/candidate/scoring.d.ts +7 -8
  170. package/out/candidate/scoring.d.ts.map +1 -1
  171. package/out/candidate/scoring.js +52 -89
  172. package/out/candidate/scoring.js.map +1 -1
  173. package/out/capital-schema.d.ts +21 -17
  174. package/out/capital-schema.d.ts.map +1 -1
  175. package/out/capital-schema.js +17 -15
  176. package/out/capital-schema.js.map +1 -1
  177. package/out/capitals.d.ts +27 -25
  178. package/out/capitals.d.ts.map +1 -1
  179. package/out/capitals.js +23 -21
  180. package/out/capitals.js.map +1 -1
  181. package/out/coincident-roles.d.ts +60 -43
  182. package/out/coincident-roles.d.ts.map +1 -1
  183. package/out/coincident-roles.js +72 -50
  184. package/out/coincident-roles.js.map +1 -1
  185. package/out/convention/schema.d.ts +14 -14
  186. package/out/convention/schema.d.ts.map +1 -1
  187. package/out/convention/schema.js +6 -4
  188. package/out/convention/schema.js.map +1 -1
  189. package/out/convention.d.ts +97 -0
  190. package/out/convention.d.ts.map +1 -0
  191. package/out/convention.js +81 -0
  192. package/out/convention.js.map +1 -0
  193. package/out/coverage-manifest-schema.d.ts +34 -39
  194. package/out/coverage-manifest-schema.d.ts.map +1 -1
  195. package/out/coverage-manifest-schema.js +25 -34
  196. package/out/coverage-manifest-schema.js.map +1 -1
  197. package/out/currency-backfill.d.ts +16 -38
  198. package/out/currency-backfill.d.ts.map +1 -1
  199. package/out/currency-backfill.js +19 -46
  200. package/out/currency-backfill.js.map +1 -1
  201. package/out/env.d.ts +1 -1
  202. package/out/env.js +1 -1
  203. package/out/exact-match.d.ts +19 -10
  204. package/out/exact-match.d.ts.map +1 -1
  205. package/out/exact-match.js +32 -21
  206. package/out/exact-match.js.map +1 -1
  207. package/out/extracts.d.ts +66 -55
  208. package/out/extracts.d.ts.map +1 -1
  209. package/out/extracts.js +63 -55
  210. package/out/extracts.js.map +1 -1
  211. package/out/fst/autocomplete.d.ts +29 -21
  212. package/out/fst/autocomplete.d.ts.map +1 -1
  213. package/out/fst/autocomplete.js +39 -28
  214. package/out/fst/autocomplete.js.map +1 -1
  215. package/out/fst/builder.d.ts.map +1 -1
  216. package/out/fst/builder.js +37 -41
  217. package/out/fst/builder.js.map +1 -1
  218. package/out/fst/deserialize-web.d.ts +4 -4
  219. package/out/fst/deserialize-web.d.ts.map +1 -1
  220. package/out/fst/deserialize-web.js +15 -16
  221. package/out/fst/deserialize-web.js.map +1 -1
  222. package/out/fst/format.d.ts +41 -23
  223. package/out/fst/format.d.ts.map +1 -1
  224. package/out/fst/format.js +41 -23
  225. package/out/fst/format.js.map +1 -1
  226. package/out/fst/freshness.d.ts +19 -81
  227. package/out/fst/freshness.d.ts.map +1 -1
  228. package/out/fst/freshness.js +31 -94
  229. package/out/fst/freshness.js.map +1 -1
  230. package/out/fst/matcher.d.ts +5 -4
  231. package/out/fst/matcher.d.ts.map +1 -1
  232. package/out/fst/matcher.js +2 -2
  233. package/out/fst/matcher.js.map +1 -1
  234. package/out/fst/serialize.d.ts +28 -28
  235. package/out/fst/serialize.d.ts.map +1 -1
  236. package/out/fst/serialize.js +53 -71
  237. package/out/fst/serialize.js.map +1 -1
  238. package/out/fst/types.d.ts +78 -74
  239. package/out/fst/types.d.ts.map +1 -1
  240. package/out/fst/types.js +1 -4
  241. package/out/fst/types.js.map +1 -1
  242. package/out/{fst/index.js → fst.d.ts} +2 -1
  243. package/out/fst.d.ts.map +1 -0
  244. package/out/{fst/index.d.ts → fst.js} +2 -1
  245. package/out/fst.js.map +1 -0
  246. package/out/fts/query.d.ts +2 -27
  247. package/out/fts/query.d.ts.map +1 -1
  248. package/out/fts/query.js +5 -36
  249. package/out/fts/query.js.map +1 -1
  250. package/out/fts.d.ts +126 -0
  251. package/out/fts.d.ts.map +1 -0
  252. package/out/fts.js +188 -0
  253. package/out/fts.js.map +1 -0
  254. package/out/geonames/aliases.d.ts +26 -62
  255. package/out/geonames/aliases.d.ts.map +1 -1
  256. package/out/geonames/aliases.js +45 -117
  257. package/out/geonames/aliases.js.map +1 -1
  258. package/out/geonames/postal.d.ts +84 -44
  259. package/out/geonames/postal.d.ts.map +1 -1
  260. package/out/geonames/postal.js +115 -73
  261. package/out/geonames/postal.js.map +1 -1
  262. package/out/{geonames/index.js → geonames.d.ts} +1 -1
  263. package/out/geonames.d.ts.map +1 -0
  264. package/out/{geonames/index.d.ts → geonames.js} +1 -1
  265. package/out/geonames.js.map +1 -0
  266. package/out/index.d.ts +9 -8
  267. package/out/index.d.ts.map +1 -1
  268. package/out/index.js +6 -5
  269. package/out/index.js.map +1 -1
  270. package/out/interpolation.d.ts +18 -55
  271. package/out/interpolation.d.ts.map +1 -1
  272. package/out/interpolation.js +27 -69
  273. package/out/interpolation.js.map +1 -1
  274. package/out/lookup.d.ts +23 -51
  275. package/out/lookup.d.ts.map +1 -1
  276. package/out/lookup.js +136 -234
  277. package/out/lookup.js.map +1 -1
  278. package/out/name-score.d.ts +2 -2
  279. package/out/name-score.d.ts.map +1 -1
  280. package/out/name-score.js +9 -7
  281. package/out/name-score.js.map +1 -1
  282. package/out/nsul/lookup.d.ts +33 -34
  283. package/out/nsul/lookup.d.ts.map +1 -1
  284. package/out/nsul/lookup.js +26 -40
  285. package/out/nsul/lookup.js.map +1 -1
  286. package/out/nsul/schema.d.ts +16 -43
  287. package/out/nsul/schema.d.ts.map +1 -1
  288. package/out/nsul/schema.js +9 -35
  289. package/out/nsul/schema.js.map +1 -1
  290. package/out/{nsul/index.d.ts → nsul.d.ts} +1 -1
  291. package/out/nsul.d.ts.map +1 -0
  292. package/out/{nsul/index.js → nsul.js} +1 -1
  293. package/out/nsul.js.map +1 -0
  294. package/out/paths.d.ts +124 -0
  295. package/out/paths.d.ts.map +1 -0
  296. package/out/paths.js +104 -0
  297. package/out/paths.js.map +1 -0
  298. package/out/place-importance-schema.d.ts +51 -143
  299. package/out/place-importance-schema.d.ts.map +1 -1
  300. package/out/place-importance-schema.js +28 -143
  301. package/out/place-importance-schema.js.map +1 -1
  302. package/out/poi/lookup.d.ts +40 -47
  303. package/out/poi/lookup.d.ts.map +1 -1
  304. package/out/poi/lookup.js +69 -96
  305. package/out/poi/lookup.js.map +1 -1
  306. package/out/poi/schema.d.ts +41 -25
  307. package/out/poi/schema.d.ts.map +1 -1
  308. package/out/poi/schema.js +25 -17
  309. package/out/poi/schema.js.map +1 -1
  310. package/out/{poi/index.js → poi.d.ts} +1 -1
  311. package/out/poi.d.ts.map +1 -0
  312. package/out/{poi/index.d.ts → poi.js} +1 -1
  313. package/out/poi.js.map +1 -0
  314. package/out/polygon-schema.d.ts +10 -6
  315. package/out/polygon-schema.d.ts.map +1 -1
  316. package/out/polygon-schema.js +2 -2
  317. package/out/polygon-schema.js.map +1 -1
  318. package/out/postal/city/alias/lookup.d.ts +20 -23
  319. package/out/postal/city/alias/lookup.d.ts.map +1 -1
  320. package/out/postal/city/alias/lookup.js +17 -30
  321. package/out/postal/city/alias/lookup.js.map +1 -1
  322. package/out/postal/city/alias/schema.d.ts +14 -10
  323. package/out/postal/city/alias/schema.d.ts.map +1 -1
  324. package/out/postal/city/alias/schema.js +10 -8
  325. package/out/postal/city/alias/schema.js.map +1 -1
  326. package/out/postal/city/candidate-schema.d.ts +25 -19
  327. package/out/postal/city/candidate-schema.d.ts.map +1 -1
  328. package/out/postal/city/candidate-schema.js +21 -17
  329. package/out/postal/city/candidate-schema.js.map +1 -1
  330. package/out/{postal/index.d.ts → postal.d.ts} +1 -1
  331. package/out/postal.d.ts.map +1 -0
  332. package/out/{postal/index.js → postal.js} +1 -1
  333. package/out/postal.js.map +1 -0
  334. package/out/postcode-point-lookup.d.ts +6 -24
  335. package/out/postcode-point-lookup.d.ts.map +1 -1
  336. package/out/postcode-point-lookup.js +5 -17
  337. package/out/postcode-point-lookup.js.map +1 -1
  338. package/out/primary-preference.d.ts +38 -92
  339. package/out/primary-preference.d.ts.map +1 -1
  340. package/out/primary-preference.js +15 -99
  341. package/out/primary-preference.js.map +1 -1
  342. package/out/proximity-rerank.d.ts +41 -30
  343. package/out/proximity-rerank.d.ts.map +1 -1
  344. package/out/proximity-rerank.js +38 -29
  345. package/out/proximity-rerank.js.map +1 -1
  346. package/out/ranking-weights.d.ts +98 -64
  347. package/out/ranking-weights.d.ts.map +1 -1
  348. package/out/ranking-weights.js +26 -25
  349. package/out/ranking-weights.js.map +1 -1
  350. package/out/region-keys.d.ts +50 -31
  351. package/out/region-keys.d.ts.map +1 -1
  352. package/out/region-keys.js +68 -40
  353. package/out/region-keys.js.map +1 -1
  354. package/out/reverse.d.ts +78 -50
  355. package/out/reverse.d.ts.map +1 -1
  356. package/out/reverse.js +98 -61
  357. package/out/reverse.js.map +1 -1
  358. package/out/schema.d.ts +72 -49
  359. package/out/schema.d.ts.map +1 -1
  360. package/out/schema.js +4 -5
  361. package/out/schema.js.map +1 -1
  362. package/out/search-fetch.d.ts +16 -12
  363. package/out/search-fetch.d.ts.map +1 -1
  364. package/out/search-fetch.js +83 -63
  365. package/out/search-fetch.js.map +1 -1
  366. package/out/sqlite/convention-source.d.ts +12 -16
  367. package/out/sqlite/convention-source.d.ts.map +1 -1
  368. package/out/sqlite/convention-source.js +13 -22
  369. package/out/sqlite/convention-source.js.map +1 -1
  370. package/out/sqlite/utils.d.ts +19 -19
  371. package/out/sqlite/utils.d.ts.map +1 -1
  372. package/out/sqlite/utils.js +14 -15
  373. package/out/sqlite/utils.js.map +1 -1
  374. package/out/street/centroid/schema.d.ts +37 -42
  375. package/out/street/centroid/schema.d.ts.map +1 -1
  376. package/out/street/centroid/schema.js +9 -21
  377. package/out/street/centroid/schema.js.map +1 -1
  378. package/out/street/centroid.d.ts +30 -0
  379. package/out/street/centroid.d.ts.map +1 -0
  380. package/out/street/{centroid/index.js → centroid.js} +13 -24
  381. package/out/street/centroid.js.map +1 -0
  382. package/out/street/morphology/fst/builder.d.ts +20 -10
  383. package/out/street/morphology/fst/builder.d.ts.map +1 -1
  384. package/out/street/morphology/fst/builder.js +44 -34
  385. package/out/street/morphology/fst/builder.js.map +1 -1
  386. package/out/street/morphology/fst/loader.d.ts +33 -26
  387. package/out/street/morphology/fst/loader.d.ts.map +1 -1
  388. package/out/street/morphology/fst/loader.js +23 -22
  389. package/out/street/morphology/fst/loader.js.map +1 -1
  390. package/out/street/name-lookup.d.ts +24 -18
  391. package/out/street/name-lookup.d.ts.map +1 -1
  392. package/out/street/name-lookup.js +34 -35
  393. package/out/street/name-lookup.js.map +1 -1
  394. package/out/street/normalize.d.ts +35 -136
  395. package/out/street/normalize.d.ts.map +1 -1
  396. package/out/street/normalize.js +77 -194
  397. package/out/street/normalize.js.map +1 -1
  398. package/out/street/segment-schema.d.ts +79 -39
  399. package/out/street/segment-schema.d.ts.map +1 -1
  400. package/out/street/segment-schema.js +71 -21
  401. package/out/street/segment-schema.js.map +1 -1
  402. package/out/{street/index.js → street.d.ts} +2 -2
  403. package/out/street.d.ts.map +1 -0
  404. package/out/{street/index.d.ts → street.js} +2 -2
  405. package/out/street.js.map +1 -0
  406. package/out/types.d.ts +84 -149
  407. package/out/types.d.ts.map +1 -1
  408. package/out/types.js +0 -6
  409. package/out/types.js.map +1 -1
  410. package/out/unified-schema.d.ts +8 -12
  411. package/out/unified-schema.d.ts.map +1 -1
  412. package/out/unified-schema.js +29 -29
  413. package/out/unified-schema.js.map +1 -1
  414. package/out/uprn/existence.d.ts +28 -17
  415. package/out/uprn/existence.d.ts.map +1 -1
  416. package/out/uprn/existence.js +26 -16
  417. package/out/uprn/existence.js.map +1 -1
  418. package/out/uprn/lookup.d.ts +14 -44
  419. package/out/uprn/lookup.d.ts.map +1 -1
  420. package/out/uprn/lookup.js +21 -57
  421. package/out/uprn/lookup.js.map +1 -1
  422. package/out/uprn/schema.d.ts +18 -40
  423. package/out/uprn/schema.d.ts.map +1 -1
  424. package/out/uprn/schema.js +10 -32
  425. package/out/uprn/schema.js.map +1 -1
  426. package/out/{uprn/index.d.ts → uprn.d.ts} +1 -1
  427. package/out/uprn.d.ts.map +1 -0
  428. package/out/{uprn/index.js → uprn.js} +1 -1
  429. package/out/uprn.js.map +1 -0
  430. package/out/weights-overlay-linker.d.ts +138 -144
  431. package/out/weights-overlay-linker.d.ts.map +1 -1
  432. package/out/weights-overlay-linker.js +105 -159
  433. package/out/weights-overlay-linker.js.map +1 -1
  434. package/package.json +91 -596
  435. package/lib/address/point/index.ts +0 -328
  436. package/lib/convention/index.ts +0 -154
  437. package/lib/fts/index.ts +0 -361
  438. package/out/address/index.d.ts.map +0 -1
  439. package/out/address/index.js.map +0 -1
  440. package/out/address/point/index.d.ts +0 -53
  441. package/out/address/point/index.d.ts.map +0 -1
  442. package/out/address/point/index.js +0 -222
  443. package/out/address/point/index.js.map +0 -1
  444. package/out/ancestry/index.d.ts +0 -45
  445. package/out/ancestry/index.d.ts.map +0 -1
  446. package/out/ancestry/index.js +0 -55
  447. package/out/ancestry/index.js.map +0 -1
  448. package/out/convention/index.d.ts +0 -106
  449. package/out/convention/index.d.ts.map +0 -1
  450. package/out/convention/index.js +0 -93
  451. package/out/convention/index.js.map +0 -1
  452. package/out/fst/index.d.ts.map +0 -1
  453. package/out/fst/index.js.map +0 -1
  454. package/out/fts/index.d.ts +0 -184
  455. package/out/fts/index.d.ts.map +0 -1
  456. package/out/fts/index.js +0 -277
  457. package/out/fts/index.js.map +0 -1
  458. package/out/geonames/index.d.ts.map +0 -1
  459. package/out/geonames/index.js.map +0 -1
  460. package/out/nsul/index.d.ts.map +0 -1
  461. package/out/nsul/index.js.map +0 -1
  462. package/out/poi/index.d.ts.map +0 -1
  463. package/out/poi/index.js.map +0 -1
  464. package/out/postal/index.d.ts.map +0 -1
  465. package/out/postal/index.js.map +0 -1
  466. package/out/street/centroid/index.d.ts +0 -40
  467. package/out/street/centroid/index.d.ts.map +0 -1
  468. package/out/street/centroid/index.js.map +0 -1
  469. package/out/street/index.d.ts.map +0 -1
  470. package/out/street/index.js.map +0 -1
  471. package/out/uprn/index.d.ts.map +0 -1
  472. package/out/uprn/index.js.map +0 -1
  473. /package/lib/{geonames/index.ts → geonames.ts} +0 -0
  474. /package/lib/{nsul/index.ts → nsul.ts} +0 -0
  475. /package/lib/{poi/index.ts → poi.ts} +0 -0
  476. /package/lib/{postal/index.ts → postal.ts} +0 -0
  477. /package/lib/{uprn/index.ts → uprn.ts} +0 -0
@@ -3,74 +3,41 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Does an `fst-*.bin` still match the gazetteer it was built from?
7
- *
8
- * WHY THIS EXISTS. Every FST artifact is a projection of one WOF admin database, and the admin
9
- * database is a sealed readonly artifact that a rebuild REPLACES. Nothing mechanically tied the two
10
- * together: the 2026-08-04 admin swap (4.87M rows, ancestry repaired, macrohood/microhood ingested)
11
- * left `fst-global-priority.bin` at its 2026-05-28 build and the per-locale set at 2026-07-26, and
12
- * the only way to notice was to compare mtimes by hand. The artifacts kept loading, kept answering
13
- * queries, and answered them from a gazetteer that no longer exists.
14
- *
15
- * `FSTProvenance` already recorded `sourceDB` — the source's PATH, which is exactly the field that
16
- * cannot change when the bytes behind it do. So the stamp gains the source's IDENTITY (md5 + byte
17
- * size) and this module compares it. The shape deliberately mirrors
18
- * `@mailwoman/resolver-wof-sqlite/weights-overlay-linker`'s `pairIndexStaleReason`: one function returning a reason
19
- * string or `undefined`, so a fact added to the stamp cannot be checked by some callers and not
20
- * others — which is how three of the four base linkers ended up unable to notice a PIX1 schema bump.
21
- *
22
- * FORMAT IS PART OF FRESHNESS. The check compares the serializer version too, not just the source
23
- * md5. A guard that checks only the source reads a format-obsolete binary as "current" (the R5
24
- * freshness-guard lesson, format edition), and a file below {@link MIN_STAMPED_FORMAT_VERSION}
25
- * cannot carry a stamp at all — reported as its own reason rather than silently passing.
26
- *
27
- * STALE IS A WARNING, NOT A FAULT. A dev tree with an old FST must still run; the artifact is a
28
- * decode-time bias list, not a correctness dependency. Callers print {@link formatFSTStaleWarning}
29
- * and continue.
6
+ * Does an `fst-*.bin` still match the gazetteer it was built from?
30
7
  */
31
8
 
32
- import { pathExists, readFileRange, readLocalTextFile, statPath } from "@mailwoman/core/fs/readers"
9
+ import { readFileRange, readLocalTextFile } from "@mailwoman/core/fs/readers"
10
+ import { pathExists, statPath } from "@mailwoman/core/fs/readers/stat"
33
11
  import { writeLocalTextFile } from "@mailwoman/core/fs/writers"
34
12
  import { md5File } from "@mailwoman/core/hash"
35
13
  import { tryParsingJSON } from "@mailwoman/core/json"
36
14
  import { basename, resolvePath, type PathBuilderLike } from "path-ts"
37
15
 
38
- import { FST_FORMAT_VERSION } from "#fst/serialize"
16
+ import { FST_FORMAT_VERSION } from "#fst/format"
39
17
  import type { FSTProvenance } from "#fst/types"
40
18
 
41
19
  /**
42
- * Fixed header size in bytes — mirrors `fst-serialize.ts`'s `HEADER_SIZE`. Duplicated rather than exported across
43
- * because this module reads the header by SEEK (never buffering the file), and the serializer's constant is private to
44
- * its own read/write pair.
20
+ * Fixed header size in bytes, mirroring `fst-serialize.ts`'s private `HEADER_SIZE`.
45
21
  */
46
22
  const HEADER_SIZE = 32
47
23
 
48
24
  /**
49
- * `"FST\0"` little-endian, the four magic bytes every artifact opens with.
25
+ * `"FST\0"` little-endian, the four bytes every artifact opens with.
50
26
  */
51
27
  const MAGIC = 0x00_54_53_46
52
28
 
53
- /**
54
- * Byte offset of the u32 trailer offset inside the header (the last of its eight fields).
55
- */
56
29
  const PROVENANCE_OFFSET_FIELD = 28
57
30
 
58
31
  /**
59
- * First serializer version carrying the trailing provenance block. Below this a file has no place to put a stamp, so
60
- * "unstamped" is a statement about the FORMAT, not about the builder.
32
+ * First serializer version carrying the trailing provenance block. below it
33
+ * a file has no place to put a stamp.
61
34
  */
62
35
  export const MIN_STAMPED_FORMAT_VERSION = 3
63
36
 
64
- /**
65
- * Hex characters in an md5 digest.
66
- */
67
37
  const MD5_HEX_LENGTH = 32
68
38
 
69
39
  /**
70
40
  * What an FST was built from, recorded so a later reader can tell whether that thing still exists.
71
- *
72
- * `bytes` is not redundant with `md5` — it is the field that survives a truncated or half-written source and makes the
73
- * mismatch legible in the warning ("5,273,722,880 → 5,372,076,032" names the rebuild; a hex delta does not).
74
41
  */
75
42
  export interface FSTSourceIdentity {
76
43
  md5: string
@@ -79,85 +46,55 @@ export interface FSTSourceIdentity {
79
46
 
80
47
  /**
81
48
  * The stamp fields a freshness guard reads off an artifact, plus the format version it was written at.
82
- *
83
- * Every field is optional because every one of them can be legitimately absent on an artifact that predates it, and the
84
- * meaning-of-zero rule applies to all of them: absent is a distinct state from "matches", and the reasons below say so
85
- * in different words.
86
49
  */
87
50
  export interface FSTStampFields {
88
51
  formatVersion: number
89
- provenance: FSTProvenance | undefined
52
+ provenance: FSTProvenance | null
90
53
  }
91
54
 
92
55
  /**
93
56
  * What a caller expects the artifact to have been built from.
94
- *
95
- * `exclusionPolicy` is optional and caller-supplied on purpose. The policy id lives in
96
- * `mailwoman/gazetteer-pipeline/fst.ts` (which depends on this package, not the other way round), so only the caller
97
- * knows which policy it means — the same split as the pair-index guard, where the format+magnitude half is shared and
98
- * the source-md5 half stays with the script that knows its sources.
99
57
  */
100
58
  export interface FSTExpectation {
101
59
  source: FSTSourceIdentity
102
60
  /**
103
- * Serializer version to require. Defaults to {@link FST_FORMAT_VERSION} — the version this tree writes — so a caller
104
- * cannot forget the format half of the comparison. Override only to check against a specific older floor.
61
+ * Serializer version to require, defaulting to {@link FST_FORMAT_VERSION}.
105
62
  */
106
- formatVersion?: number
107
- exclusionPolicy?: string
63
+ formatVersion?: number | null
64
+ exclusionPolicy?: string | null
108
65
  }
109
66
 
110
67
  /**
111
- * Read an artifact's stamp WITHOUT deserializing it — a header seek plus the trailer, three reads totalling a few
112
- * kilobytes. The distinction matters: `fst-global-priority.bin` is 317 MB and this runs on every `yarn test` via the
113
- * weights linkers, so `readFileSync` + `readFSTProvenance` would trade a freshness guard for a slower test suite and
114
- * nobody would keep it.
115
- *
116
- * Returns `undefined` for a file that is absent, too small, or not an FST at all — none of which is this function's
117
- * business to diagnose.
68
+ * Read an artifact's stamp without deserializing it.
118
69
  */
119
- export async function peekFSTStampFields(path: string): Promise<FSTStampFields | undefined> {
120
- if (!(await pathExists(path))) return undefined
70
+ export async function peekFSTStampFields(path: PathBuilderLike): Promise<FSTStampFields | null> {
71
+ if (!(await pathExists(path))) return null
121
72
  const size = (await statPath(path)).size
122
73
 
123
- if (size < HEADER_SIZE) return undefined
74
+ if (size < HEADER_SIZE) return null
124
75
  const header = await readFileRange(path, 0, HEADER_SIZE)
125
76
 
126
- if (header.readUInt32LE(0) !== MAGIC) return undefined
77
+ if (header.readUInt32LE(0) !== MAGIC) return null
127
78
  const formatVersion = header.readUInt16LE(4)
128
79
 
129
- if (formatVersion < MIN_STAMPED_FORMAT_VERSION) return { formatVersion, provenance: undefined }
80
+ if (formatVersion < MIN_STAMPED_FORMAT_VERSION) return { formatVersion, provenance: null }
130
81
  const trailerStart = header.readUInt32LE(PROVENANCE_OFFSET_FIELD)
131
82
 
132
- // 0 = "this build wrote no trailer" (the serializer's own encoding); anything past EOF is a
133
- // truncated file. Both read as "no stamp", which is what the caller does with them anyway.
134
- if (trailerStart === 0 || trailerStart + 4 > size) return { formatVersion, provenance: undefined }
83
+ // 0 means this build wrote no trailer and past-EOF means truncated.
84
+ // Both read as no stamp.
85
+ if (trailerStart === 0 || trailerStart + 4 > size) return { formatVersion, provenance: null }
135
86
 
136
87
  const jsonLength = (await readFileRange(path, trailerStart, 4)).readUInt32LE(0)
137
88
 
138
- if (jsonLength === 0 || trailerStart + 4 + jsonLength > size) return { formatVersion, provenance: undefined }
89
+ if (jsonLength === 0 || trailerStart + 4 + jsonLength > size) return { formatVersion, provenance: null }
139
90
 
140
91
  const json = await readFileRange(path, trailerStart + 4, jsonLength)
141
92
 
142
- return { formatVersion, provenance: tryParsingJSON<FSTProvenance>(json.toString("utf8")) ?? undefined }
93
+ return { formatVersion, provenance: tryParsingJSON<FSTProvenance>(json.toString("utf8")) ?? null }
143
94
  }
144
95
 
145
- /**
146
- * Re-exported from `@mailwoman/core/hash`. This package's own test imports it from here, and the sidecar convention
147
- * below is what it is for.
148
- */
149
-
150
96
  /**
151
97
  * The source identity an FST build should stamp, or a check should compare against.
152
- *
153
- * Uses the `.md5` sidecar convention the weights linkers already established on this exact file: `<path>.md5` in
154
- * md5sum(1) format (`<hash> <filename>`), trusted only while its mtime is at least the source's. An older sidecar is
155
- * recomputed. Without it the admin DB costs 7.3 s per call and the guard would be quietly disabled by whoever noticed
156
- * `yarn test` got slower.
157
- *
158
- * `refreshSidecar` writes the recomputed digest back. Best-effort: a sealed data root or a read-only mount fails the
159
- * write and the caller still gets its answer, because refusing to check freshness on a read-only tree would be the
160
- * wrong trade.
161
98
  */
162
99
  export async function readWOFSourceIdentity(
163
100
  source: PathBuilderLike,
@@ -170,7 +107,7 @@ export async function readWOFSourceIdentity(
170
107
 
171
108
  if (hit) return hit
172
109
  const sidecarPath = `${path}.md5`
173
- let md5: string | undefined
110
+ let md5: string | null = null
174
111
 
175
112
  if (await pathExists(sidecarPath)) {
176
113
  const sidecarStats = await statPath(sidecarPath)
@@ -191,7 +128,7 @@ export async function readWOFSourceIdentity(
191
128
  try {
192
129
  await writeLocalTextFile(`${md5} ${basename(path)}\n`, sidecarPath)
193
130
  } catch {
194
- // Read-only data root — the digest is still correct, it just isn't cached.
131
+ // Read-only data root: the digest is still correct, only the cache write failed.
195
132
  }
196
133
  }
197
134
  }
@@ -202,21 +139,12 @@ export async function readWOFSourceIdentity(
202
139
  return identity
203
140
  }
204
141
 
205
- /**
206
- * Memo for {@link readWOFSourceIdentity}, keyed on (path, mtimeMs, size) — NOT on path alone, for the same reason
207
- * `computeSurfaceCountryCounts` isn't: the admin DB is a sealed artifact that a rebuild REPLACES, so a path-only memo
208
- * would serve a stale digest against a new file for the life of the process.
209
- */
210
142
  const sourceIdentityMemo = new Map<string, FSTSourceIdentity>()
211
143
 
212
144
  /**
213
- * Why an FST artifact is stale against `expected`, or `undefined` when it still matches.
214
- *
215
- * The order is deliberate: FORMAT first (a version-obsolete file is stale whatever its source says), then the presence
216
- * of a stamp, then the source identity, then the build policy. Each returns prose a reader can act on — the reasons are
217
- * printed verbatim into {@link formatFSTStaleWarning}.
145
+ * Why an FST artifact is stale against `expected`, or `null` when it still matches.
218
146
  */
219
- export function fstStaleReason(fields: FSTStampFields | undefined, expected: FSTExpectation): string | undefined {
147
+ export function fstStaleReason(fields: FSTStampFields | null, expected: FSTExpectation): string | null {
220
148
  if (!fields) return "unreadable or not an FST artifact"
221
149
  const requiredFormat = expected.formatVersion ?? FST_FORMAT_VERSION
222
150
 
@@ -240,24 +168,19 @@ export function fstStaleReason(fields: FSTStampFields | undefined, expected: FST
240
168
  return `source db ${provenance.sourceDBMD5.slice(0, 8)} → ${expected.source.md5.slice(0, 8)} (built ${provenance.builtAt})`
241
169
  }
242
170
 
243
- // Reached only when the md5s agree, so a size disagreement means one of the two was recorded
244
- // against a different file than it was hashed from. Cheap to check, and it never fires by accident.
245
- if (provenance.sourceDBBytes !== undefined && provenance.sourceDBBytes !== expected.source.bytes) {
171
+ if (provenance.sourceDBBytes != null && provenance.sourceDBBytes !== expected.source.bytes) {
246
172
  return `source db size ${provenance.sourceDBBytes} → ${expected.source.bytes} at a matching md5 — one of the two is misrecorded`
247
173
  }
248
174
 
249
- if (expected.exclusionPolicy !== undefined && provenance.exclusionPolicy !== expected.exclusionPolicy) {
175
+ if (expected.exclusionPolicy && provenance.exclusionPolicy !== expected.exclusionPolicy) {
250
176
  return `exclusion policy ${provenance.exclusionPolicy ?? "(none)"} → ${expected.exclusionPolicy}`
251
177
  }
252
178
 
253
- return undefined
179
+ return null
254
180
  }
255
181
 
256
182
  /**
257
- * The whole check, for a caller that has a path and a source DB and wants a warning string or nothing.
258
- *
259
- * Returns `undefined` when the artifact is current OR when it is absent — an absent artifact is a different problem
260
- * with a different message, and every existing caller already reports it in place.
183
+ * The whole check, for a caller with a path and a source DB that wants a warning string or none.
261
184
  */
262
185
  export async function fstFreshnessWarning({
263
186
  fstPath,
@@ -266,34 +189,34 @@ export async function fstFreshnessWarning({
266
189
  exclusionPolicy,
267
190
  rebuildCommand,
268
191
  }: {
269
- fstPath: string
270
- sourceDBPath: string
271
- formatVersion?: number
272
- exclusionPolicy?: string
192
+ fstPath: PathBuilderLike
193
+ sourceDBPath: PathBuilderLike
194
+ formatVersion?: number | null
195
+ exclusionPolicy?: string | null
273
196
  rebuildCommand: string
274
- }): Promise<string | undefined> {
275
- if (!(await pathExists(fstPath)) || !(await pathExists(sourceDBPath))) return undefined
197
+ }): Promise<string | null> {
198
+ if (!(await pathExists(fstPath)) || !(await pathExists(sourceDBPath))) return null
276
199
 
277
200
  const reason = fstStaleReason(await peekFSTStampFields(fstPath), {
278
201
  source: await readWOFSourceIdentity(sourceDBPath),
279
- ...(formatVersion === undefined ? {} : { formatVersion }),
280
- ...(exclusionPolicy === undefined ? {} : { exclusionPolicy }),
202
+ formatVersion: formatVersion ?? null,
203
+ exclusionPolicy: exclusionPolicy ?? null,
281
204
  })
282
205
 
283
- return reason === undefined ? undefined : formatFSTStaleWarning({ fstPath, reason, rebuildCommand })
206
+ return reason ? formatFSTStaleWarning({ fstPath, reason, rebuildCommand }) : null
284
207
  }
285
208
 
286
209
  /**
287
- * The one warning format, so `grep -r "FST STALE"` finds every site that can emit one.
210
+ * The one warning format, so `grep -r "FST stale"` finds every site that emits one.
288
211
  */
289
212
  export function formatFSTStaleWarning({
290
213
  fstPath,
291
214
  reason,
292
215
  rebuildCommand,
293
216
  }: {
294
- fstPath: string
217
+ fstPath: PathBuilderLike
295
218
  reason: string
296
219
  rebuildCommand: string
297
220
  }): string {
298
- return `WARNING: FST STALE — ${fstPath}: ${reason}. Rebuild with: ${rebuildCommand}`
221
+ return `WARNING: FST STALE — ${fstPath.toString()}: ${reason}. Rebuild with: ${rebuildCommand}`
299
222
  }
@@ -8,7 +8,9 @@
8
8
  * arrays at accepting states.
9
9
  */
10
10
 
11
- import type { FSTContinuation, FSTMatchResult, FSTQueryResult, PlaceEntry } from "#fst/types"
11
+ import type { AncestrieMatch } from "@mailwoman/ancestrie"
12
+
13
+ import type { FSTContinuation, FSTQueryResult, PlaceEntry } from "#fst/types"
12
14
 
13
15
  interface FSTNode {
14
16
  edges: Map<string, number>
@@ -36,7 +38,7 @@ export class FSTMatcher {
36
38
  return count
37
39
  }
38
40
 
39
- walk(tokens: string[]): FSTMatchResult | null {
41
+ walk(tokens: string[]): AncestrieMatch | null {
40
42
  let stateID = 0
41
43
 
42
44
  for (const token of tokens) {
@@ -54,7 +56,7 @@ export class FSTMatcher {
54
56
  return { stateID, accepted: node.places.length > 0, depth: tokens.length }
55
57
  }
56
58
 
57
- walkFrom(prev: FSTMatchResult, token: string): FSTMatchResult | null {
59
+ walkFrom(prev: AncestrieMatch, token: string): AncestrieMatch | null {
58
60
  const node = this.nodes[prev.stateID]
59
61
 
60
62
  if (!node) return null
@@ -143,7 +145,7 @@ export class FSTMatcher {
143
145
  }
144
146
 
145
147
  /**
146
- * Normalize text into FST tokens: lowercase, NFKC, strip punctuation, split on whitespace.
148
+ * Normalize text into FST tokens: lowercase, nfkc, strip punctuation, split on whitespace.
147
149
  */
148
150
  export function normalizeTokens(text: string): string[] {
149
151
  return text
@@ -151,7 +153,7 @@ export function normalizeTokens(text: string): string[] {
151
153
  .toLowerCase()
152
154
  .replaceAll(/[\p{P}\p{S}]/gu, "")
153
155
  .split(/\s+/)
154
- .filter((t) => t.length > 0)
156
+ .filter((t) => t.length)
155
157
  }
156
158
 
157
159
  export type { FSTNode }
@@ -2,38 +2,27 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
+ * @file Binary serialization for the FST gazetteer.
5
6
  *
6
- * Binary serialization for the FST gazetteer. Format:
7
+ * All integers are little-endian. The writer emits the current version. The reader accepts every earlier version.
7
8
  *
8
- * HEADER (32 bytes) magic [u8; 4] "FST\0" version u16 1 flags u16 0 (reserved) stateCount u32
9
- * edgeCount u32 total edges across all states placeCount u32 total place entries across all
10
- * states stringCount u32 unique strings in string table stringBytes u32 total bytes of string
11
- * data _reserved u32
9
+ * - Header, 32 bytes: magic `FST\0`, version u16, flags u16 (bit 0 means `crossCountryBranches` is present),
10
+ * The header stores five u32 counts (stateCount, edgeCount, placeCount, stringCount, stringBytes).
11
+ * Its provenance offset is u32. The value 0 means the trailer is absent.
12
+ * - The string table contains `stringCount + 1` u32 offsets, with the final offset as a sentinel, followed by the UTF-8 data.
13
+ * - State table, 16 bytes per state: edgeStart u32, placeStart u32, edgeCount u32, placeCount u32. Before version
14
+ * 4 the entry is 12 bytes with u16 counts.
15
+ * - Edge table, 8 bytes per edge: stringIdx u32, targetState u32.
16
+ * - Place table, 60 bytes per place: wofID u32, placetypeIdx u8, chainLen u8, crossCountryBranches u8,
17
+ * placeFlags u8 (bit 0 means `encyclopedic` is present), nameIdx u32, referential f32, lat f32, lon f32.
18
+ * Each entry then stores eight u32 parent ids. Version 5 added encyclopedic f32. Earlier entries contain 56 bytes.
19
+ * Those entries have no encyclopedic score.
20
+ * - Provenance trailer: a u32 length followed by JSON.
12
21
  *
13
- * STRING TABLE offsets [u32; stringCount + 1] byte offset into data (last = sentinel) data [u8;
14
- * stringBytes] concatenated UTF-8
15
- *
16
- * STATE TABLE [stateCount × 12 bytes] edgeStart u32 index into edge table placeStart u32 index into
17
- * place table edgeCount u16 placeCount u16
18
- *
19
- * EDGE TABLE [edgeCount × 8 bytes] stringIdx u32 index into string table targetState u32
20
- *
21
- * PLACE TABLE [placeCount × 60 bytes at V5, 56 below] wofID u32 placetypeIdx u8 index into
22
- * PLACETYPE_ORDER chainLen u8 0..8 crossCountryBranches u8 (header flags bit0 enables the read)
23
- * placeFlags u8 (V5; bit0 = encyclopedic present) nameIdx u32 index into string table referential f32
24
- * population-anchored likelihood [0,1] — was the conflated `importance` (V2–V4), was population u32
25
- * (V1) lat f32 lon f32 chain [u32; 8] parent chain (unused slots = 0) encyclopedic f32 (V5 only;
26
- * read only when placeFlags bit0 is set)
27
- *
28
- * THE V5 BUMP IS THE TWO-SCORE SPLIT (ROAD_TO_V9 §2 R1). Through V4 one float carried whichever score
29
- * the source database held, and nothing in the bytes said which — so a reader could not tell a
30
- * population proxy from a Wikipedia score. V5 names the ranking score `referential` and gives the
31
- * encyclopedic signal its own slot plus a PER-PLACE presence bit, because most places have no
32
- * Wikipedia article and a 0 there would be a fact nobody recorded. `fst-freshness.ts` reports every
33
- * V4-and-below artifact as format-stale for exactly this reason: its single float is unattributable.
22
+ * Presence bits keep a missing value distinct from a stored zero.
34
23
  */
35
24
 
36
- import { tryParsingJSON } from "@mailwoman/core/json"
25
+ import { tryParsingJSON, stringifyJSON } from "@mailwoman/core/json"
37
26
 
38
27
  import {
39
28
  EDGE_ENTRY_SIZE,
@@ -55,16 +44,12 @@ import type { FSTNode } from "#fst/matcher"
55
44
  import { FSTMatcher } from "#fst/matcher"
56
45
  import type { FSTProvenance, PlaceEntry } from "#fst/types"
57
46
 
58
- export { FST_FORMAT_VERSION } from "#fst/format"
59
-
60
- /**
61
- * File magic as a Buffer, for the Node-side prefix comparison.
62
- */
63
47
  const MAGIC = Buffer.from(FST_MAGIC_BYTES)
64
48
 
65
49
  /**
66
- * Longest ancestry chain stored per place. Deeper hierarchies are truncated at the leaf end, since the specific end of
67
- * the chain is what disambiguates and the country end is recoverable anyway.
50
+ * The longest parent chain stored per place.
51
+ *
52
+ * The writer keeps the first eight valid parent ids and drops the rest.
68
53
  */
69
54
  const MAX_CHAIN_LEN = 8
70
55
 
@@ -74,10 +59,12 @@ for (let i = 0; i < PLACETYPE_ORDER.length; i++) {
74
59
  placetypeToIdx.set(PLACETYPE_ORDER[i]!, i)
75
60
  }
76
61
 
62
+ /**
63
+ * Serializes a matcher, with optional provenance, into the current FST binary format.
64
+ */
77
65
  export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): Buffer {
78
66
  const nodes = matcher.toNodes() as FSTNode[]
79
67
 
80
- // Intern state-edge strings before serializing their table.
81
68
  const stringMap = new Map<string, number>()
82
69
  const strings: string[] = []
83
70
 
@@ -106,7 +93,6 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
106
93
  const encodedStrings = strings.map((s) => Buffer.from(s, "utf8"))
107
94
  const stringBytes = encodedStrings.reduce((sum, b) => sum + b.length, 0)
108
95
 
109
- // Count each serialized record class.
110
96
  let totalEdges = 0
111
97
  let totalPlaces = 0
112
98
 
@@ -115,23 +101,20 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
115
101
  totalPlaces += node.places.length
116
102
  }
117
103
 
118
- // Allocate the complete binary buffer.
119
104
  const stringTableSize = (strings.length + 1) * 4 + stringBytes
120
105
  const stateTableSize = nodes.length * WIDE_STATE_ENTRY_SIZE
121
106
  const edgeTableSize = totalEdges * EDGE_ENTRY_SIZE
122
107
  const placeTableSize = totalPlaces * SPLIT_PLACE_ENTRY_SIZE
123
- const provenanceJson = provenance ? Buffer.from(JSON.stringify(provenance), "utf8") : null
108
+ const provenanceJson = provenance ? Buffer.from(stringifyJSON(provenance), "utf8") : null
124
109
  const provenanceSize = provenanceJson ? 4 + provenanceJson.length : 0
125
110
  const binarySize = HEADER_SIZE + stringTableSize + stateTableSize + edgeTableSize + placeTableSize
126
111
  const totalSize = binarySize + provenanceSize
127
112
  const buf = Buffer.alloc(totalSize)
128
113
  let pos = 0
129
114
 
130
- // Read the versioned binary header.
131
- // flags bit0 (survey #4, 2026-07-27): place rows carry surface-ambiguity data in the former _pad
132
- // byte (pp+6 = crossCountryBranches u8, pp+7 reserved). Presence-signaled here so VERSION stays
133
- // put: pre-ambiguity artifacts read flags=0 → readers expose `undefined`, never a fake 0.
134
- const hasAmbiguity = nodes.some((n) => n.places.some((p) => p.crossCountryBranches !== undefined))
115
+ // Flag bit 0 marks that place rows include `crossCountryBranches`.
116
+ // Readers of a file without it report `undefined`.
117
+ const hasAmbiguity = nodes.some((n) => n.places.some((p) => p.crossCountryBranches != null))
135
118
  MAGIC.copy(buf, pos)
136
119
  pos += 4
137
120
  buf.writeUInt16LE(FST_FORMAT_VERSION, pos)
@@ -151,7 +134,6 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
151
134
  buf.writeUInt32LE(provenanceJson ? binarySize : 0, pos)
152
135
  pos += 4
153
136
 
154
- // Read the interned string table.
155
137
  let strOffset = 0
156
138
 
157
139
  for (const encoded of encodedStrings) {
@@ -160,17 +142,15 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
160
142
  strOffset += encoded.length
161
143
  }
162
144
 
145
+ // The final offset is the sentinel that ends the last string.
163
146
  buf.writeUInt32LE(strOffset, pos)
164
147
  pos += 4
165
148
 
166
- // sentinel
167
-
168
149
  for (const encoded of encodedStrings) {
169
150
  encoded.copy(buf, pos)
170
151
  pos += encoded.length
171
152
  }
172
153
 
173
- // Read state, edge, and place tables.
174
154
  const stateTableStart = pos
175
155
  const edgeTableStart = stateTableStart + stateTableSize
176
156
  const placeTableStart = edgeTableStart + edgeTableSize
@@ -197,17 +177,16 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
197
177
 
198
178
  for (const place of node.places) {
199
179
  const pp = placeTableStart + placeIdx * SPLIT_PLACE_ENTRY_SIZE
200
- // Filter out WOF sentinel parent IDs (negative values like -1, -4).
180
+ // WOF uses negative parent ids such as -1 as sentinels.
201
181
  const validChain = place.parentChain.filter((id) => id > 0)
202
182
  const chainLen = Math.min(validChain.length, MAX_CHAIN_LEN)
203
183
  buf.writeUInt32LE(place.wofID, pp)
204
184
  buf.writeUInt8(placetypeToIdx.get(place.placetype) ?? 0, pp + 4)
205
185
  buf.writeUInt8(chainLen, pp + 5)
206
- // Former _pad: byte 0 = crossCountryBranches (header flags bit0 enables the read), byte 1 = v5 placeFlags.
207
186
  buf.writeUInt8(hasAmbiguity ? Math.min(place.crossCountryBranches ?? 0, 255) : 0, pp + 6)
208
- // An absent encyclopedic score writes flag 0 AND a 0.0 float. The float is unread in that
209
- // state, so absence can never surface as a score — the meaning-of-zero rule, in bytes.
210
- const hasEncyclopedic = place.encyclopedic !== undefined
187
+ // An absent encyclopedic score writes flag 0 and a 0.0 float.
188
+ // Readers skip the float when the flag is 0.
189
+ const hasEncyclopedic = place.encyclopedic != null
211
190
  buf.writeUInt8(hasEncyclopedic ? PLACE_FLAG_HAS_ENCYCLOPEDIC : 0, pp + 7)
212
191
  buf.writeUInt32LE(intern(place.name), pp + 8)
213
192
  buf.writeFloatLE(place.referential, pp + 12)
@@ -218,7 +197,7 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
218
197
  buf.writeUInt32LE(ci < chainLen ? validChain[ci]! : 0, pp + 24 + ci * 4)
219
198
  }
220
199
 
221
- buf.writeFloatLE(hasEncyclopedic ? place.encyclopedic! : 0, pp + ENCYCLOPEDIC_OFFSET)
200
+ buf.writeFloatLE(place.encyclopedic ?? 0, pp + ENCYCLOPEDIC_OFFSET)
222
201
 
223
202
  placeIdx++
224
203
  }
@@ -233,8 +212,12 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
233
212
  return buf
234
213
  }
235
214
 
215
+ /**
216
+ * Deserializes an FST binary of any supported version into a matcher.
217
+ *
218
+ * @throws When the buffer is too small, has the wrong magic, or has an unsupported version.
219
+ */
236
220
  export function deserializeFST(buf: Buffer): FSTMatcher {
237
- // Verify the versioned binary header.
238
221
  if (buf.length < HEADER_SIZE) throw new Error("FST buffer too small for header")
239
222
 
240
223
  if (!buf.subarray(0, 4).equals(MAGIC)) throw new Error("FST magic mismatch")
@@ -246,7 +229,6 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
246
229
 
247
230
  const isV2 = version >= 2
248
231
  const isSplit = version >= VERSION_TWO_SCORE_SPLIT
249
- // flags bit0 (survey #4): place rows carry surface-ambiguity data in the former _pad byte.
250
232
  const hasAmbiguity = (buf.readUInt16LE(6) & 1) === 1
251
233
 
252
234
  const stateCount = buf.readUInt32LE(8)
@@ -257,7 +239,6 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
257
239
 
258
240
  let pos = HEADER_SIZE
259
241
 
260
- // Verify string-table offsets and contents.
261
242
  const strOffsets = new Uint32Array(stringCount + 1)
262
243
 
263
244
  for (let i = 0; i <= stringCount; i++) {
@@ -276,9 +257,7 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
276
257
 
277
258
  pos += stringBytes
278
259
 
279
- // Verify state-table offsets and transitions.
280
260
  const stateEntrySize = version >= VERSION_WIDE_STATE_COUNTERS ? WIDE_STATE_ENTRY_SIZE : NARROW_STATE_ENTRY_SIZE
281
- // v5 grew the place entry by the encyclopedic float; v4-and-below files are read at the old stride.
282
261
  const placeEntrySize = version >= VERSION_TWO_SCORE_SPLIT ? SPLIT_PLACE_ENTRY_SIZE : LEGACY_PLACE_ENTRY_SIZE
283
262
  const stateTableStart = pos
284
263
  const edgeTableStart = stateTableStart + stateCount * stateEntrySize
@@ -317,16 +296,14 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
317
296
  parentChain.push(buf.readUInt32LE(pp + 24 + ci * 4))
318
297
  }
319
298
 
320
- // v1 stored a raw population u32 here; v2–v4 the conflated `importance` float; v5 the
321
- // referential score. A v1 file's population is mapped through the SAME curve
322
- // `referentialFromPopulation` uses, so its value is genuinely referential — the only
323
- // generation of this format for which that can be said without reading the source database.
299
+ // Version 1 stored a raw population u32 here.
300
+ // This version maps it through the `referentialFromPopulation` curve.
301
+ // Later versions store an f32 score.
324
302
  const referential = isV2
325
303
  ? buf.readFloatLE(pp + 12)
326
304
  : Math.min(1, Math.log2(1 + buf.readUInt32LE(pp + 12) / 1000) / 14)
327
305
 
328
- // Per-place presence bit (v5+). A v4-and-below file has no encyclopedic channel at all, so
329
- // the field stays undefined rather than reading the reserved byte as a flag.
306
+ // Files before version 5 have no encyclopedic score, so their reserved byte is not read as a flag.
330
307
  const hasEncyclopedic =
331
308
  isSplit && (buf.readUInt8(pp + 7) & PLACE_FLAG_HAS_ENCYCLOPEDIC) === PLACE_FLAG_HAS_ENCYCLOPEDIC
332
309
 
@@ -338,9 +315,8 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
338
315
  lat: buf.readFloatLE(pp + 16),
339
316
  lon: buf.readFloatLE(pp + 20),
340
317
  parentChain,
341
- // Header flags bit0 enables the read (survey #4): pre-ambiguity artifacts expose undefined.
342
- ...(hasAmbiguity ? { crossCountryBranches: buf.readUInt8(pp + 6) } : {}),
343
- ...(hasEncyclopedic ? { encyclopedic: buf.readFloatLE(pp + ENCYCLOPEDIC_OFFSET) } : {}),
318
+ crossCountryBranches: hasAmbiguity ? buf.readUInt8(pp + 6) : null,
319
+ encyclopedic: hasEncyclopedic ? buf.readFloatLE(pp + ENCYCLOPEDIC_OFFSET) : null,
344
320
  }
345
321
  }
346
322
 
@@ -350,23 +326,27 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
350
326
  return FSTMatcher.fromNodes(nodes)
351
327
  }
352
328
 
353
- export function readFSTProvenance(buf: Buffer): FSTProvenance | undefined {
354
- if (buf.length < HEADER_SIZE) return undefined
329
+ /**
330
+ * Reads the provenance trailer from an FST binary, or returns `undefined`
331
+ * when the file has none or it cannot be parsed.
332
+ */
333
+ export function readFSTProvenance(buf: Buffer): FSTProvenance | null {
334
+ if (buf.length < HEADER_SIZE) return null
355
335
 
356
- if (!buf.subarray(0, 4).equals(MAGIC)) return undefined
336
+ if (!buf.subarray(0, 4).equals(MAGIC)) return null
357
337
  const version = buf.readUInt16LE(4)
358
338
 
359
- if (version < VERSION_WITH_METADATA) return undefined
339
+ if (version < VERSION_WITH_METADATA) return null
360
340
  const provenanceOffset = buf.readUInt32LE(28)
361
341
 
362
- if (provenanceOffset === 0 || provenanceOffset >= buf.length) return undefined
342
+ if (provenanceOffset === 0 || provenanceOffset >= buf.length) return null
363
343
 
364
344
  try {
365
345
  const jsonLen = buf.readUInt32LE(provenanceOffset)
366
346
  const jsonStr = buf.toString("utf8", provenanceOffset + 4, provenanceOffset + 4 + jsonLen)
367
347
 
368
- return tryParsingJSON<FSTProvenance>(jsonStr) ?? undefined
348
+ return tryParsingJSON<FSTProvenance>(jsonStr) ?? null
369
349
  } catch {
370
- return undefined
350
+ return null
371
351
  }
372
352
  }