@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
package/out/lookup.js CHANGED
@@ -3,22 +3,18 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `WOFSQLitePlaceLookup` — the resolver implementation backed by `node:sqlite` + a Kysely-typed
7
- * query layer where the queries are non-trivial, and raw SQL where they aren't (FTS5 MATCH, the
8
- * FTS index build).
9
- *
10
- * See `docs/plan/phases/PHASE_4_2_wof_sqlite.md` for the design rationale.
6
+ * `WOFSQLitePlaceLookup` — the resolver implementation backed by `node:sqlite` plus a Kysely-typed query layer where the queries are non-trivial and raw SQL where they are not.
11
7
  */
12
8
  import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map";
13
9
  import { allRows } from "@mailwoman/core/utils";
14
10
  import { haversineKm } from "@mailwoman/spatial";
15
- import { DatabaseClient } from "@mailwoman/sqlite/client";
16
- import { ancestorLineage } from "#ancestry/index";
11
+ import { SQLiteLookup } from "@mailwoman/sqlite/lookup";
12
+ import { ancestorLineage } from "#ancestry";
17
13
  import { candidateFromSearchRow, rankCandidates } from "#candidate/scoring";
18
14
  import { loadCoincidentLocalities } from "#coincident-roles";
19
- import { ADDRESS_CONVENTION_TABLE, resolveConvention, SeedConventionSource, } from "#convention/index";
15
+ import { ADDRESS_CONVENTION_TABLE, resolveConvention, SeedConventionSource, } from "#convention";
20
16
  import { pickExtractForPlacetype, pickExtractsForPlacetype, resolveExtracts, } from "#extracts";
21
- import { buildPlaceSearchFTS, PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE, PLACE_SEARCH_TABLE, placeBboxExists, placePopulationExists, placeSearchFTSExists, } from "#fts/index";
17
+ import { buildPlaceSearchFTS, PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE, PLACE_SEARCH_TABLE, placeBboxExists, placePopulationExists, placeSearchFTSExists, } from "#fts";
22
18
  import { normalizePlacetypes, sanitizeFTSQuery } from "#fts/query";
23
19
  import { cfNormalize, softNameScore } from "#name-score";
24
20
  import { encyclopedicClauses } from "#place-importance-schema";
@@ -26,14 +22,8 @@ import { DEFAULT_WEIGHTS, populationScaleTerm } from "#ranking-weights";
26
22
  import { fetchSearchRows } from "#search-fetch";
27
23
  import { SqliteConventionSource } from "#sqlite/convention-source";
28
24
  /**
29
- * The coordinate-first candidate table (scripts/build-postcode-locality.ts): postcode → containing
30
- *
31
- * - Nearby localities with WOF alt-name aliases.
32
- */
33
- /**
34
- * The placetypes `pickExtractsForPlacetype`'s substring rule can route by name. Not every WOF placetype — only the ones
35
- * a purpose-built extract is ever named for — so the diagnostic below can say "this name routes nowhere" without
36
- * claiming to enumerate the gazetteer.
25
+ * The placetypes `pickExtractsForPlacetype`'s substring rule can route by name,
26
+ * a subset of the WOF placetypes.
37
27
  */
38
28
  const KNOWN_ROUTED_PLACETYPES = [
39
29
  "postalcode",
@@ -45,85 +35,70 @@ const KNOWN_ROUTED_PLACETYPES = [
45
35
  ];
46
36
  const POSTCODE_LOCALITY_TABLE = "postcode_locality";
47
37
  /**
48
- * Tunables for the coordinate-first locality soft-score `Score = pc·S_pc + name·S_name + pop·S_pop` (each S in [0,1]).
49
- * The pc/name/pop WEIGHTS now come from the resolved convention's `scoringWeights` (`WORLD_DEFAULT` = 0.6/0.3/0.1 — the
50
- * EU values), so a locale can retune them as data. PC_DECAY_KM sets how fast S_pc falls with distance.
38
+ * Tunables for the coordinate-first locality soft-score
39
+ * `Score = pc·S_pc + name·S_name + pop·S_pop` (each S in [0,1]).
51
40
  */
52
41
  const CF_PC_DECAY_KM = 8;
53
42
  /**
54
- * The chosen locality must be within this distance of the postcode's containing locality, else the postcode and the
55
- * parsed city name are judged to disagree (a transposed / wrong-for-the-city postcode) and the `mismatch` flag fires.
56
- * Generous enough that a city-state Ortsteil (~15km from the city centroid) and an abutting town (~few km) are NOT
57
- * flagged, tight enough to catch a wrong city (hundreds of km).
43
+ * The chosen locality must be within this distance of the postcode's containing
44
+ * locality or the `mismatch` flag fires.
58
45
  */
59
46
  const CF_MISMATCH_KM = 50;
60
- export class WOFSQLitePlaceLookup {
61
- #db;
62
- /**
63
- * Resources this instance opened. A connection handed in by a caller is NOT in here, so disposal cannot reach it —
64
- * ownership is membership rather than a flag a later branch has to check.
65
- */
66
- #resources = new DisposableStack();
47
+ export class WOFSQLitePlaceLookup extends SQLiteLookup {
67
48
  #weights;
68
49
  /**
69
- * Cached at construction so we don't `sqlite_master` query on every findPlace call. Bbox + near- with-radius queries
70
- * fall back to no-filter when this is false, preserving compatibility with DBs that were FTS-built before the R*Tree
71
- * shipped.
50
+ * Cached at construction.
72
51
  *
73
- * Per-extract: a extract is only considered to have the bbox index if its own R*Tree table exists.
52
+ * An extract is considered to have the bbox index only if its own R*Tree table exists.
74
53
  */
75
54
  #hasBboxIndex;
76
55
  /**
77
- * Per-extract probe for the `place_population` aux table. When false, the LEFT JOIN is omitted from the SELECT and
78
- * population boost is 0 for every row — preserves compatibility with DBs built before this feature shipped.
56
+ * Per-extract probe for the `place_population` aux table.
57
+ *
58
+ * When false, the left join is omitted and the population boost is 0 for every row.
79
59
  */
80
60
  #hasPopulationIndex;
81
61
  /**
82
- * Per-extract SELECT term + LEFT JOIN for the two-score split's `encyclopedic` carry (ROAD_TO_V9 §2 R1), probed and
83
- * built once at construction. Degrades to `NULL AS encyclopedic` with no join on a pre-split extract — every shipped
84
- * extract today. See {@link encyclopedicClauses} for why the probe is a column and not a table.
62
+ * Per-extract select term and left join for the two-score split's `encyclopedic` value,
63
+ * probed and built once at construction.
85
64
  */
86
65
  #encyclopedicClauses;
87
66
  /**
88
- * Per-extract probe for the `postcode_locality` table (the coordinate-first candidate table, built by
89
- * scripts/build-postcode-locality.ts). Cached at construction; null'd out when absent so the coord-first path
90
- * silently no-ops on a deployment that didn't ship the table.
67
+ * Per-extract probe for the `postcode_locality` table, cached at construction
68
+ * and null when absent so the coord-first path no-ops.
91
69
  */
92
70
  #postcodeLocalityExtract;
93
71
  /**
94
- * Resolved extract list. Always at least one entry; first is `main`. Multi-extract adds extras with their own derived
95
- * (or override) schema names.
72
+ * Resolved extract list, always at least one entry with `main` first.
96
73
  */
97
74
  #extracts;
98
75
  /**
99
- * #920: per-schema probed country sets for country-aware extract routing (non-main extracts only).
76
+ * Per-schema probed country sets for country-aware extract routing, non-main extracts only.
100
77
  */
101
78
  #extractCountries;
102
79
  /**
103
- * The Geographic Rule Engine (Direction E, #289). `#conventionSource` supplies per-WOF-polygon resolution profiles;
104
- * `#strategies` is the named-primitive registry the merged convention dispatches. Empty source → every query resolves
105
- * to `WORLD_DEFAULT` → byte-identical to the pre-engine coordinate-first path. `#countryWOFIdCache` memoizes the
106
- * country-code → country-WOF-id lookup that seeds the convention ancestor chain (one query per country, then
107
- * cached).
80
+ * `#conventionSource` supplies per-WOF-polygon profiles for the Geographic Rule Engine.
81
+ *
82
+ * `#strategies` maps primitive names to strategies.
83
+ * `#countryWOFIdCache` memoizes country-code to country-WOF-id lookups.
108
84
  */
109
85
  #conventionSource;
110
86
  #strategies;
111
87
  #countryWOFIdCache = new Map();
112
88
  /**
113
- * Strategy names already warned about — so an unknown name surfaces once, not once per query.
89
+ * Strategy names already warned about, so an unknown name surfaces once.
114
90
  */
115
91
  #warnedUnknownStrategies = new Set();
116
92
  /**
117
- * Lazily-built `admin_id → coincident localities` map from the #403 relation (null until first use).
93
+ * Lazily-built `admin_id → coincident localities` map, null until first use.
118
94
  */
119
95
  #coincidentRolesCache = null;
120
96
  /**
121
- * Per-id memoized ancestor lineages (#404) — a hot chain is queried once.
97
+ * Per-id memoized ancestor lineages, queried once per chain.
122
98
  */
123
99
  #ancestorsCache = new Map();
124
100
  /**
125
- * Opt-in postal-city alias reader (#475). `null` unless `opts.postalCityAliases` was supplied — every alias code path
126
- * is conditioned on this, so the default resolver is byte-identical.
101
+ * Opt-in postal-city alias reader, `null` unless supplied, so every alias path is skipped by default.
127
102
  */
128
103
  #postalCityAliases;
129
104
  constructor(opts, weights) {
@@ -133,28 +108,22 @@ export class WOFSQLitePlaceLookup {
133
108
  if (!opts.database && !opts.databasePath) {
134
109
  throw new Error("WOFSQLitePlaceLookup: one of `database` or `databasePath` is required");
135
110
  }
136
- if (opts.database) {
137
- this.#db = opts.database;
138
- this.#extracts = [{ path: ":memory:", schemaName: "main", placetypes: [] }];
139
- }
140
- else {
141
- const extracts = resolveExtracts(opts.databasePath);
142
- this.#extracts = extracts;
143
- // Read-only by default — shipped gazetteer extracts are sealed 0444 and Docker `:ro` mounts
144
- // forbid write-mode opens, so a writable open fails there. The only code path that writes to
145
- // the main extract is `#ensureFTS()` (FTS5 index build), conditioned on `opts.buildFTS`; open writable
146
- // ONLY when that build was explicitly requested. Every read query (FTS5 MATCH, the aux-table
147
- // SELECTs, ATTACH, and the `busy_timeout` PRAGMA) works read-only. See the docker read-only
148
- // mount limitation (#1213).
149
- this.#db = this.#resources.use(new DatabaseClient(extracts[0].path, { readOnly: !opts.buildFTS }));
150
- // ATTACH each non-main extract. Schema names were validated by resolveExtracts, so safe to
151
- // interpolate directly (SQLite ATTACH doesn't accept parameters for the schema name).
152
- for (const s of extracts.slice(1)) {
153
- this.#db.exec(`ATTACH DATABASE '${s.path.replaceAll("'", "''")}' AS ${s.schemaName}`);
154
- }
111
+ const extracts = opts.database
112
+ ? [{ path: ":memory:", schemaName: "main", placetypes: [] }]
113
+ : resolveExtracts(opts.databasePath);
114
+ // Read-only by default: shipped extracts are sealed 0444 and Docker `:ro`
115
+ // mounts forbid a write-mode open.
116
+ // Only `buildFTS` opens writable.
117
+ super(opts.database ? { database: opts.database } : { databasePath: extracts[0].path }, {
118
+ readOnly: !opts.buildFTS,
119
+ });
120
+ this.#extracts = extracts;
121
+ // Schema names were validated by resolveExtracts, so interpolating them is safe.
122
+ // SQLite attach accepts no parameter for a schema name.
123
+ for (const s of extracts.slice(1)) {
124
+ this.database.exec(`ATTACH DATABASE '${s.path.replaceAll("'", "''")}' AS ${s.schemaName}`);
155
125
  }
156
- // node:sqlite has no .pragma() helper; pragmas are executed as plain SQL.
157
- this.#db.exec("PRAGMA busy_timeout = 5000");
126
+ this.database.exec("PRAGMA busy_timeout = 5000");
158
127
  if (opts.buildFTS) {
159
128
  this.#ensureFTS();
160
129
  }
@@ -162,32 +131,17 @@ export class WOFSQLitePlaceLookup {
162
131
  this.#assertFTSExists();
163
132
  }
164
133
  this.#weights = { ...DEFAULT_WEIGHTS, ...weights };
165
- // Probe each extract's aux-table presence — driven by per-extract table existence in
166
- // sqlite_master. Cached at construction so findPlace doesn't query sqlite_master per call.
167
134
  this.#hasBboxIndex = new Map();
168
135
  this.#hasPopulationIndex = new Map();
169
136
  this.#encyclopedicClauses = new Map();
170
137
  for (const s of this.#extracts) {
171
138
  this.#hasBboxIndex.set(s.schemaName, this.#extractHasTable(s.schemaName, PLACE_BBOX_TABLE));
172
139
  this.#hasPopulationIndex.set(s.schemaName, this.#extractHasTable(s.schemaName, PLACE_POPULATION_TABLE));
173
- this.#encyclopedicClauses.set(s.schemaName, encyclopedicClauses(this.#db, s.schemaName));
140
+ this.#encyclopedicClauses.set(s.schemaName, encyclopedicClauses(this.database, s.schemaName));
174
141
  }
175
- // Every lookup path here reaches `place_search`, and a extract without it fails in one of two ways
176
- // that are both hard to read: an unroutable name returns zero hits (indistinguishable from "this
177
- // country has no places") and a routable one throws mid-query from deep inside a SELECT. The
178
- // unroutable half is the worse of the two — a extract reaches routing only through the name
179
- // `deriveSchemaName` derives from its FILENAME, so a file spelled one letter off the placetype it
180
- // serves answers with nothing while holding every row that was asked for.
181
- //
182
- // Two independent things bring a extract under the guard, and it needs both. Carrying `spr` is a
183
- // CLAIM to be a place extract. Carrying a name that routes is an INVITATION to be queried as one, and
184
- // it is made by the filename alone — so a database with no tables at all still gets picked, still
185
- // answers no query, and still dies inside a SELECT. Testing only the claim lets an empty or
186
- // truncated file past construction; testing only the name would exempt a correctly-named build
187
- // input. A extract needs to fail neither test to be exempt.
188
- //
189
- // Exempt by design: `postcode-locality-<cc>.db` carries a relation table and nothing else, matches
190
- // no routed placetype, and is part of the documented default extract list.
142
+ // An extract is guarded when its path identifies a routed placetype
143
+ // or it contains `spr`, and must then contain `place_search`; testing only one of
144
+ // those would let an empty file through or exempt a build input.
191
145
  for (const s of this.#extracts) {
192
146
  if (s.schemaName === "main")
193
147
  continue;
@@ -208,45 +162,34 @@ export class WOFSQLitePlaceLookup {
208
162
  : ` Its schema name "${s.schemaName}" also matches no routed placetype (${KNOWN_ROUTED_PLACETYPES.join(", ")}), ` +
209
163
  `so it would never have been queried even with the table — check the filename's spelling.`));
210
164
  }
211
- // #920 country-aware extract routing: probe each NON-MAIN extract's country set once at
212
- // construction (they're small, purpose-built extracts — postcode/locality slices; main is the
213
- // multi-GB admin DB and is the fallback anyway, so it is deliberately NOT scanned). Feeds
214
- // pickExtractForPlacetype so two postcode extracts (postalcode-us + postalcode-geonames-tail)
215
- // route by the query's country instead of first-match starving the second extract.
165
+ // Probe each non-main extract's country set once at construction so two postcode
166
+ // extracts route by the query's country instead of first-match starving the second.
216
167
  this.#extractCountries = new Map();
217
168
  for (const sh of this.#extracts) {
218
169
  if (sh.schemaName === "main")
219
170
  continue;
220
171
  try {
221
- const rows = this.#db
172
+ const rows = this.database
222
173
  .prepare(`SELECT DISTINCT country FROM ${sh.schemaName}.spr WHERE country != ''`)
223
174
  .all();
224
175
  this.#extractCountries.set(sh.schemaName, new Set(rows.map((r) => r.country)));
225
176
  }
226
177
  catch {
227
- // A extract without spr (or an attach oddity) just doesn't participate in country routing.
178
+ // An extract without spr (or an attach oddity) just doesn't participate in country routing.
228
179
  }
229
180
  }
230
- // The postcode_locality table can live on any attached extract (typically its own
231
- // `postcode-locality-<cc>.db`). Find the first extract that has it; null = coord-first disabled.
232
181
  this.#postcodeLocalityExtract =
233
182
  this.#extracts.find((s) => this.#extractHasTable(s.schemaName, POSTCODE_LOCALITY_TABLE))?.schemaName ?? null;
234
- // Opt-in postal-city alias reader (#475). Construction-time present-or-not is the check: null
235
- // keeps the coordinate-first scorer byte-identical to pre-#475.
236
183
  this.#postalCityAliases = opts.postalCityAliases ?? null;
237
- // The Geographic Rule Engine convention source. Precedence: an explicit `opts.conventions`
238
- // (a ready source or a seed map) wins; else the build-from-source convention asset if one is
239
- // attached (auto-detected, like the postcode_locality extract — adding conventions.db to
240
- // databasePath enables it; queried on demand, not paged into memory); else empty, so EU rides
241
- // WORLD_DEFAULT. The registry binds strategy NAMES to the SQL-bound primitives — adding a
242
- // strategy is registering it here.
184
+ // Precedence: explicit `opts.conventions` wins, else an attached build-from-source
185
+ // convention asset, else empty so EU uses `WORLD_DEFAULT`.
243
186
  const conventionExtract = this.#extracts.find((s) => this.#extractHasTable(s.schemaName, ADDRESS_CONVENTION_TABLE))?.schemaName ?? null;
244
187
  this.#conventionSource = opts.conventions
245
188
  ? "get" in opts.conventions && typeof opts.conventions.get === "function"
246
189
  ? opts.conventions
247
190
  : new SeedConventionSource(opts.conventions)
248
191
  : conventionExtract
249
- ? new SqliteConventionSource(this.#db, conventionExtract)
192
+ ? new SqliteConventionSource(this.database, conventionExtract)
250
193
  : new SeedConventionSource();
251
194
  this.#strategies = new Map([
252
195
  ["postcode_area_resolution", (q, c) => this.#postcodeAreaResolution(q, c)],
@@ -254,28 +197,24 @@ export class WOFSQLitePlaceLookup {
254
197
  ]);
255
198
  }
256
199
  #extractHasTable(schemaName, tableName) {
257
- // For main, the existing helpers work directly. For attached extracts we have to ask via the
258
- // schema-qualified `sqlite_master` view.
200
+ // Main uses the existing helpers.
201
+ // Attached extracts need the schema-qualified `sqlite_master`.
259
202
  if (schemaName === "main") {
260
203
  if (tableName === PLACE_BBOX_TABLE)
261
- return placeBboxExists(this.#db);
204
+ return placeBboxExists(this.database);
262
205
  if (tableName === PLACE_POPULATION_TABLE)
263
- return placePopulationExists(this.#db);
206
+ return placePopulationExists(this.database);
264
207
  }
265
- const row = this.#db
208
+ const row = this.database
266
209
  .prepare(`SELECT name FROM ${schemaName}.sqlite_master WHERE type = 'table' AND name = ?`)
267
210
  .get(tableName);
268
211
  return Boolean(row);
269
212
  }
270
213
  async findPlace(query) {
271
- // Geographic Rule Engine dispatch (#289). Resolve the effective convention for this query
272
- // (WORLD_DEFAULT for the EU locales — the seed source is empty) and run its candidate strategies
273
- // in order; the first to return a non-null result wins. The default list,
274
- // [postcode_area_resolution, fallback_fuzzy_name_match], reproduces the pre-engine coordinate-
275
- // first → FTS fall-through exactly. Unknown strategy names are skipped, so a convention may name
276
- // a primitive a future phase will register.
214
+ // Run the effective convention's candidate strategies in order.
215
+ // The first non-null result wins and unknown strategy names are skipped.
277
216
  const convention = this.#conventionFor(query);
278
- let outcome = [];
217
+ let found = [];
279
218
  for (const name of convention.candidateStrategies) {
280
219
  const strategy = this.#strategies.get(name);
281
220
  if (!strategy) {
@@ -283,21 +222,15 @@ export class WOFSQLitePlaceLookup {
283
222
  continue;
284
223
  }
285
224
  const result = await strategy(query, convention);
286
- if (result !== null) {
287
- outcome = result;
225
+ if (result) {
226
+ found = result;
288
227
  break;
289
228
  }
290
229
  }
291
- if (outcome.length)
292
- return outcome;
293
- // #924: NL postcode retry ladder. The WOF NL postalcode repo stores full codes UNSPACED
294
- // ('1012LG') plus 4-digit stems ('1012'), while Dutch addresses carry the spaced form
295
- // ('1012 LG') — two FTS tokens that can never match the one-token doc (the #920 name law,
296
- // resurfacing in a WOF-built extract). On a postcode-typed NL-shape miss, retry ONCE with the
297
- // whitespace-joined form (block-level precision when the full-code row exists), then the
298
- // 4-digit stem (area-level). Country-restricted to NL — the same digits+letters shape elsewhere
299
- // must not silently coarsen to a different system's code. Each retry only fires when its
300
- // text differs from the current one, so the ladder terminates by construction.
230
+ if (found.length)
231
+ return found;
232
+ // On a postcode-typed NL-shape miss, retry with the whitespace-joined form then the 4-digit stem,
233
+ // restricted to NL so the same shape elsewhere cannot coarsen to another system's code.
301
234
  if (query.country?.toUpperCase() === "NL" &&
302
235
  (normalizePlacetypes(query.placetype)?.includes("postalcode") ?? false) &&
303
236
  /^\d{4}\s?[A-Za-z]{2}$/.test(query.text.trim())) {
@@ -312,31 +245,23 @@ export class WOFSQLitePlaceLookup {
312
245
  if (stem !== trimmed)
313
246
  return this.findPlace({ ...query, text: stem });
314
247
  }
315
- return outcome;
248
+ return found;
316
249
  }
317
250
  /**
318
- * Dual-role localities coincident with an admin id, from the precomputed `coincident_roles` relation (#403). Backs
319
- * {@link ResolveOpts.hierarchyCompletion} (#405): O(1) once the relation is loaded. Returns `[]` when the relation
320
- * table is absent (older DB) or the admin isn't a dual-role place, so completion degrades gracefully. The relation +
321
- * `spr` join is loaded once and memoized.
251
+ * Dual-role localities coincident with an admin id, or `[]` when the relation table is absent.
322
252
  */
323
253
  coincidentLocalitiesFor(adminID) {
324
254
  const id = typeof adminID === "number" ? adminID : Number(adminID);
325
255
  if (!Number.isFinite(id))
326
256
  return [];
327
257
  if (!this.#coincidentRolesCache) {
328
- this.#coincidentRolesCache = loadCoincidentLocalities(this.#db);
258
+ this.#coincidentRolesCache = loadCoincidentLocalities(this.database);
329
259
  }
330
260
  return this.#coincidentRolesCache.get(id) ?? [];
331
261
  }
332
262
  /**
333
- * The ancestor lineage of a place — its containment chain joined with `spr` for canonical names, ordered
334
- * NEAREST-FIRST (localadmin → county → region → … → country). Backs {@link ResolveOpts.includeAncestors} (#404). Self
335
- * is excluded; memoized per id. Returns `[]` when the place has no recorded ancestry.
336
- *
337
- * The walk itself lives in `ancestry.ts` (shared with the reverse geocoder, #484); the ordering is its
338
- * `PLACETYPE_DEPTH` table — same ranking as the previous inline SQL CASE, extended below `localadmin` so
339
- * locality/neighbourhood ancestors order correctly instead of sorting last.
263
+ * The ancestor lineage of a place, ordered nearest-first with self excluded,
264
+ * or `[]` when it has no recorded ancestry.
340
265
  */
341
266
  ancestors(id) {
342
267
  const pid = typeof id === "number" ? id : Number(id);
@@ -345,7 +270,7 @@ export class WOFSQLitePlaceLookup {
345
270
  const cached = this.#ancestorsCache.get(pid);
346
271
  if (cached)
347
272
  return cached;
348
- const lineage = ancestorLineage(this.#db, pid).map((r) => ({
273
+ const lineage = ancestorLineage(this.database, pid).map((r) => ({
349
274
  id: r.id,
350
275
  placetype: r.placetype,
351
276
  name: r.name,
@@ -354,10 +279,8 @@ export class WOFSQLitePlaceLookup {
354
279
  return lineage;
355
280
  }
356
281
  /**
357
- * Surface an unknown strategy name LOUDLY (once per name) rather than swallowing it silently — an invisible no-op is
358
- * exactly the hidden-dependency failure mode we avoid (see the provenance-first design value). We warn rather than
359
- * throw so a convention asset built against a newer code revision (one that adds a strategy) degrades gracefully on
360
- * an older build instead of taking down resolution.
282
+ * Warn once per unknown strategy name rather than throwing, so a convention built
283
+ * against a newer revision degrades instead of stopping resolution.
361
284
  */
362
285
  #warnUnknownStrategy(name) {
363
286
  if (this.#warnedUnknownStrategies.has(name))
@@ -368,10 +291,8 @@ export class WOFSQLitePlaceLookup {
368
291
  `against a newer code revision, rebuild the asset for this one.`);
369
292
  }
370
293
  /**
371
- * Strategy `postcode_area_resolution` — the coordinate-first locality path, strictly conditioned (a sibling postcode
372
- * AND a postcode_locality table AND a locality query). Returns `null` — so the dispatcher falls through to the next
373
- * strategy — when the condition is unmet or the postcode isn't in the table; otherwise the soft-scored postcode∪name
374
- * candidate set.
294
+ * The coordinate-first locality strategy, returning `null` when its postcode,
295
+ * table, or locality conditions are unmet.
375
296
  */
376
297
  #postcodeAreaResolution(query, convention) {
377
298
  if (!(query.postcode && this.#postcodeLocalityExtract && this.#isLocalityQuery(query))) {
@@ -380,31 +301,27 @@ export class WOFSQLitePlaceLookup {
380
301
  return this.#findLocalityCoordFirst(query, this.#postcodeLocalityExtract, convention);
381
302
  }
382
303
  /**
383
- * Strategy `fallback_fuzzy_name_match` — the BM25 FTS name-match over the gazetteer, the universal fallback. Always
384
- * returns an array (never null), so it terminates the dispatch chain.
304
+ * The BM25 FTS name-match fallback, always returning an array so it terminates the dispatch chain.
385
305
  */
386
306
  async #fuzzyNameMatch(query, forceExtract) {
387
307
  const limit = query.limit ?? 10;
388
- // Expand the placetype filter through the shared equivalence table (core/resolver): a
389
- // `locality` query must also reach `borough` / `localadmin` rows — Brooklyn-the-borough
390
- // (pop 2.5M) is a borough, not a locality, and a strict filter made it unreachable so the
391
- // fuzzy "Brooklyn Park, MN" won instead. Order-preserving: the FIRST entry stays the
392
- // requested placetype, which is what extract routing keys off below.
308
+ // Expand the placetype filter through the shared equivalence table so a `locality`
309
+ // query also reaches `borough` and `localadmin` rows.
393
310
  const placetypes = expandPlacetypeFilter(normalizePlacetypes(query.placetype));
394
- // Postcode-typed queries keep the #920 fused name-law shape; everything else splits on
395
- // intra-token punctuation so hyphenated names reach the FTS as their real terms (#945).
311
+ // Postcode-typed queries keep the fused name-law shape.
312
+ // Everything else splits on intra-token punctuation so hyphenated names
313
+ // reach the FTS as their real terms.
396
314
  const ftsQuery = sanitizeFTSQuery(query.text, { fuseTokens: placetypes?.includes("postalcode") ?? false });
397
315
  if (!ftsQuery)
398
316
  return [];
399
- // Pick the extract for this query. Multi-extract routing is placetype-driven; a query without
400
- // `placetype` always goes to main. (Mixed-placetype queries with multiple extracts aren't
401
- // supported in v1 — caller can issue two findPlace calls and merge in TS if needed.)
317
+ // Multi-extract routing uses placetype.
318
+ // Queries without `placetype` go to main.
319
+ // Mixed-placetype spread across extracts is unsupported.
402
320
  const firstPlacetype = placetypes?.[0];
403
- // Bias fan-out (#58/proximity-bias): a country-less query WITH proximity hints must see the
404
- // cross-extract ambiguity the hints exist to resolve — "48026" lives in postalcode-us AND
405
- // postalcode-intl, and single-extract routing would hide one side. Query every matching extract
406
- // (self-recursion with a extract pin), merge by id, and re-sort by the same (exact, prominence)
407
- // keys the per-extract tier sort used. Bounded: hints + no country + >1 matching extract only.
321
+ // A country-less query with proximity hints queries every matching extract
322
+ // so cross-extract ambiguity is visible.
323
+ // The bound requires hints and an absent country.
324
+ // It also requires more than one matching extract.
408
325
  const hasBiasHints = !!query.near || (query.bias?.length ?? 0) > 0;
409
326
  if (!forceExtract && hasBiasHints && !query.country) {
410
327
  const matching = pickExtractsForPlacetype(this.#extracts, firstPlacetype);
@@ -427,14 +344,14 @@ export class WOFSQLitePlaceLookup {
427
344
  }
428
345
  }
429
346
  const extract = forceExtract ??
430
- pickExtractForPlacetype(this.#extracts, firstPlacetype, {
347
+ pickExtractForPlacetype(this.#extracts, firstPlacetype ?? null, {
431
348
  country: query.country,
432
349
  countriesBySchema: this.#extractCountries,
433
350
  });
434
- // bare schema name; safe to interpolate (validated at construction)
351
+ // Validated at construction, so bare interpolation is safe.
435
352
  const sch = extract.schemaName;
436
353
  const rawRows = fetchSearchRows({
437
- db: this.#db,
354
+ db: this.database,
438
355
  schemaName: sch,
439
356
  query,
440
357
  placetypes,
@@ -453,7 +370,7 @@ export class WOFSQLitePlaceLookup {
453
370
  };
454
371
  const candidates = rawRows.map((row) => candidateFromSearchRow(row, scoring));
455
372
  rankCandidates(candidates, {
456
- db: this.#db,
373
+ db: this.database,
457
374
  schemaName: sch,
458
375
  query,
459
376
  weights: this.#weights,
@@ -465,11 +382,7 @@ export class WOFSQLitePlaceLookup {
465
382
  return !pts || pts.includes("locality");
466
383
  }
467
384
  /**
468
- * Resolve the effective convention for a query (the Geographic Rule Engine entry point). The ancestor chain is keyed
469
- * by WOF polygon id; for #289 it carries just the country level — resolved from `query.country` via the cached
470
- * code→WOF-id lookup — so the EU locales, which have no override rows, resolve to `WORLD_DEFAULT` and dispatch is
471
- * byte-identical to the pre-engine path. E4 (JP) extends the chain with the resolved locality's `ancestors` row, so a
472
- * region/locality-level convention (e.g. Sapporo's grid) deep-merges over the country one.
385
+ * Resolve the effective convention for a query, keyed by the country's WOF polygon id.
473
386
  */
474
387
  #conventionFor(query) {
475
388
  const chain = [];
@@ -482,8 +395,7 @@ export class WOFSQLitePlaceLookup {
482
395
  return resolveConvention(this.#conventionSource, chain);
483
396
  }
484
397
  /**
485
- * Country ISO code → its WOF polygon id (the coarsest convention key). Cached — one indexed `spr` query per distinct
486
- * country, then memoized (including a not-found `null`) so findPlace never pays for it twice.
398
+ * Country ISO code to its WOF polygon id, memoized including the not-found `null`.
487
399
  */
488
400
  #countryWOFId(code) {
489
401
  const cached = this.#countryWOFIdCache.get(code);
@@ -491,7 +403,7 @@ export class WOFSQLitePlaceLookup {
491
403
  return cached;
492
404
  let id;
493
405
  try {
494
- const row = this.#db
406
+ const row = this.database
495
407
  .prepare(`SELECT id FROM main.spr WHERE placetype = 'country' AND country = ? AND is_current != 0 LIMIT 1`)
496
408
  .get(code);
497
409
  id = row?.id ?? null;
@@ -503,33 +415,28 @@ export class WOFSQLitePlaceLookup {
503
415
  return id;
504
416
  }
505
417
  /**
506
- * Coordinate-first locality resolution. The postcode_locality table maps the sibling postcode to the locality whose
507
- * polygon contains the postcode centroid (+ a few nearby ones for the abutting- postcode case). We union those
508
- * COORDINATE candidates with the FTS NAME candidates and soft-score the union `0.6·S_pc + 0.3·S_name + 0.1·S_pop` —
509
- * so a small town the name-match never finds is recovered by the postcode, while an unambiguous name (Berlin) still
510
- * wins on name + population. Returns null when the postcode isn't in the table (→ caller falls back to the FTS
511
- * path).
418
+ * Coordinate-first locality resolution: union the postcode's containing
419
+ * and nearby localities with the FTS name candidates and soft-score them,
420
+ * returning null when the postcode is not in the table.
512
421
  */
513
422
  async #findLocalityCoordFirst(query, sch, convention) {
514
423
  const w = convention.scoringWeights;
515
424
  const pc = query.postcode.trim();
516
425
  const pcWhere = query.country ? "postcode = ? AND country = ?" : "postcode = ?";
517
426
  const pcParams = query.country ? [pc, query.country] : [pc];
518
- const pcRows = allRows(this.#db.prepare(`SELECT locality_id AS id, aliases, distance_km AS dist, is_containing AS containing
427
+ const pcRows = allRows(this.database.prepare(`SELECT locality_id AS id, aliases, distance_km AS dist, is_containing AS containing
519
428
  FROM ${sch}.${POSTCODE_LOCALITY_TABLE} WHERE ${pcWhere}`), ...pcParams);
520
429
  if (!pcRows.length)
521
430
  return null;
522
431
  const limit = query.limit ?? 10;
523
- // Name-match candidates via the normal FTS path (postcode cleared → no recursion).
432
+ // Name-match candidates via the normal FTS path with the postcode cleared, so this cannot recurse.
524
433
  const ftsCands = await this.findPlace({ ...query, postcode: undefined, limit: Math.max(limit, 10) });
525
434
  const pcInfo = new Map();
526
435
  for (const r of pcRows) {
527
436
  pcInfo.set(r.id, { dist: r.dist, containing: r.containing === 1, aliases: r.aliases ? r.aliases.split("|") : [] });
528
437
  }
529
- // #475 (opt-in): observed postal-city aliases for this postcode, keyed by the geographic
530
- // locality name they map to. A user-typed postal city ("Antioch", 37013) becomes a name-match
531
- // alias for the geographic locality the postcode sits in ("Nashville"). Empty when the reader
532
- // isn't supplied → the scoring loop below is byte-identical to pre-#475.
438
+ // Observed postal-city aliases for this postcode keyed by geographic locality name,
439
+ // empty when the reader is not supplied.
533
440
  const postalAliasByGeo = new Map();
534
441
  if (this.#postalCityAliases) {
535
442
  for (const a of await this.#postalCityAliases.getDivergentAliases(pc)) {
@@ -557,35 +464,27 @@ export class WOFSQLitePlaceLookup {
557
464
  for (const cand of merged.values()) {
558
465
  const info = pcInfo.get(cand.id);
559
466
  const sPc = info ? (info.containing ? 1 : Math.exp(-info.dist / CF_PC_DECAY_KM)) : 0;
560
- // Fold any postal-city aliases for this candidate's geographic name into the soft name match
561
- // (#475). `postalAliasByGeo` is empty unless the opt-in reader was supplied, so when off this
562
- // reduces to the original `info?.aliases ?? []` and the score is unchanged.
467
+ // Fold postal-city aliases into the soft name match.
468
+ // The map is empty unless the opt-in reader was supplied, so scoring is unchanged when off.
563
469
  const wofAliases = info?.aliases ?? [];
564
470
  const aliases = postalAliasByGeo.size
565
471
  ? [...wofAliases, ...(postalAliasByGeo.get(cfNormalize(cand.name)) ?? [])]
566
472
  : wofAliases;
567
473
  const sName = softNameScore(query.text, cand.name, aliases);
568
- const sPop = populationScaleTerm(cand.population, this.#weights);
474
+ const sPop = populationScaleTerm(cand.population ?? null, this.#weights);
569
475
  scored.push({ ...cand, score: w.pc * sPc + w.name * sName + w.pop * sPop, exact: sName >= 1 });
570
476
  }
571
- // Exact-name tiering (same philosophy as the FTS path): an EXACT name/alias match tiers above
572
- // coordinate-only candidates, with the soft-score breaking ties WITHIN a tier. This keeps an
573
- // unambiguous city ("Berlin", exact + huge population) ahead of the fine-grained Ortsteil its
574
- // postcode centroid lands in, while a small town the name-match never finds (no exact tier) is
575
- // still recovered by its postcode's containing locality.
477
+ // An exact name or alias match tiers above coordinate-only candidates,
478
+ // with the soft score breaking ties within a tier.
576
479
  scored.sort((a, b) => Number(b.exact) - Number(a.exact) || b.score - a.score);
577
- // Conflict flag: if the chosen locality is NOT the postcode's containing locality and sits far
578
- // from it, the postcode and the city name disagree (a transposed / wrong-for-the-city postcode).
579
- // We keep the name-chosen locality but flag it — the falsehood signal a BM25 geocoder can't give.
480
+ // When the chosen locality is not the postcode's containing locality and sits far
481
+ // from it, flag `mismatch` rather than overriding the name.
580
482
  const top = scored[0];
581
483
  if (top) {
582
- // The postcode's geographic anchor: among the postcode's candidate localities that actually
583
- // resolved (some — e.g. unnamed Ortsteile — are in the postcode table but not the admin DB),
584
- // prefer the containing one, else the nearest. Postcodes whose centroid falls just outside
585
- // every locality polygon still anchor to the closest town.
484
+ // Among the postcode's candidate localities that resolved, prefer the containing one, else the nearest.
586
485
  const anchorRow = pcRows
587
486
  .filter((r) => merged.has(r.id))
588
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
487
+ // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
589
488
  .sort((a, b) => b.containing - a.containing || a.dist - b.dist)[0];
590
489
  const anchor = anchorRow ? merged.get(anchorRow.id) : undefined;
591
490
  if (anchor &&
@@ -600,7 +499,7 @@ export class WOFSQLitePlaceLookup {
600
499
  });
601
500
  }
602
501
  /**
603
- * Fetch locality spr rows (from main) for the postcode-injected candidate ids the FTS set missed.
502
+ * Fetch locality spr rows from main for the postcode-injected candidate ids the FTS set missed.
604
503
  */
605
504
  #fetchLocalitiesByID(ids) {
606
505
  if (!ids.length)
@@ -609,40 +508,43 @@ export class WOFSQLitePlaceLookup {
609
508
  const popSelect = hasPop ? `pp.population AS population` : `NULL AS population`;
610
509
  const popJoin = hasPop ? `LEFT JOIN main.${PLACE_POPULATION_TABLE} pp ON pp.id = s.id` : "";
611
510
  const ph = ids.map(() => "?").join(", ");
612
- const rows = allRows(this.#db.prepare(`SELECT s.id AS id, s.name AS name, s.country AS country, s.parent_id AS parent_id,
511
+ const rows = allRows(this.database.prepare(`SELECT s.id AS id, s.name AS name, s.country AS country, s.parent_id AS parent_id,
613
512
  s.latitude AS lat, s.longitude AS lon, s.placetype AS placetype, ${popSelect}
614
513
  FROM main.spr s ${popJoin}
615
514
  WHERE s.id IN (${ph}) AND s.is_current != 0`), ...ids);
616
515
  return rows.map((row) => {
617
- const c = {
516
+ return {
618
517
  id: row.id,
619
518
  name: row.name,
620
519
  placetype: row.placetype,
621
520
  country: row.country ?? "",
622
521
  lat: row.lat ?? 0,
623
522
  lon: row.lon ?? 0,
624
- parent_id: row.parent_id ?? undefined,
523
+ parent_id: row.parent_id ?? null,
625
524
  score: 0,
525
+ population: row.population !== null && row.population > 0 ? row.population : null,
526
+ distanceKm: null,
527
+ exactMatch: null,
528
+ prominence: null,
529
+ referential: null,
530
+ encyclopedic: null,
531
+ importance: null,
532
+ bbox: null,
533
+ mismatch: null,
534
+ containedByQualifier: null,
535
+ regionScopeMiss: null,
536
+ variantAliasExempted: null,
626
537
  };
627
- if (row.population !== null && row.population > 0) {
628
- c.population = row.population;
629
- }
630
- return c;
631
538
  });
632
539
  }
633
- [Symbol.dispose]() {
634
- // Only when we opened it. A caller who passed a pre-opened client keeps using it after this returns — the FTS
635
- // build this lookup performed lives on their connection, and closing it would take that with us.
636
- this.#resources[Symbol.dispose]();
637
- }
638
540
  /**
639
- * Build the FTS5 virtual table from the `names` + `places` tables.
541
+ * Build the FTS5 virtual table from the `names` and `places` tables.
640
542
  */
641
543
  #ensureFTS() {
642
- buildPlaceSearchFTS(this.#db);
544
+ buildPlaceSearchFTS(this.database);
643
545
  }
644
546
  #assertFTSExists() {
645
- if (!placeSearchFTSExists(this.#db)) {
547
+ if (!placeSearchFTSExists(this.database)) {
646
548
  throw new Error("WOFSQLitePlaceLookup: `place_search` FTS5 table is missing. Pass `buildFTS: true` to build it on open, or run `mailwoman gazetteer build fts <path-to-wof.db>` ahead of time (see resolver-wof-sqlite/README.md).");
647
549
  }
648
550
  }