@mailwoman/resolver-wof-sqlite 9.4.0 → 10.1.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 (548) hide show
  1. package/README.md +8 -8
  2. package/lib/address/{point-interpolation.ts → point/interpolation.ts} +40 -38
  3. package/lib/address/point/schema.ts +132 -0
  4. package/lib/address/point.ts +68 -81
  5. package/lib/{address/index.ts → address.ts} +2 -2
  6. package/lib/ancestry/backfill.ts +24 -88
  7. package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
  8. package/lib/build/candidate.ts +608 -0
  9. package/lib/build/population-corrections.ts +64 -0
  10. package/lib/build/slim.ts +338 -0
  11. package/lib/candidate/alias-bags.ts +74 -14
  12. package/lib/candidate/ancestors/schema.ts +211 -0
  13. package/lib/candidate/{ancestors-sidecar.ts → ancestors/sidecar.ts} +17 -24
  14. package/lib/candidate/country-display-names.ts +6 -20
  15. package/lib/candidate/extract-fold.ts +12 -57
  16. package/lib/candidate/fts.ts +28 -0
  17. package/lib/candidate/importance.ts +221 -0
  18. package/lib/candidate/lookup.ts +669 -0
  19. package/lib/candidate/name-roles.ts +16 -34
  20. package/lib/candidate/own-name.ts +34 -31
  21. package/lib/candidate/place-attrs.ts +20 -13
  22. package/lib/candidate/schema.ts +211 -0
  23. package/lib/candidate/scoring.ts +222 -0
  24. package/lib/capital-schema.ts +21 -17
  25. package/lib/capitals.ts +26 -24
  26. package/lib/coincident-roles.ts +74 -55
  27. package/lib/convention/schema.ts +13 -7
  28. package/lib/convention.ts +144 -0
  29. package/lib/coverage-manifest-schema.ts +34 -39
  30. package/lib/currency-backfill.ts +24 -69
  31. package/lib/env.ts +1 -1
  32. package/lib/exact-match.ts +31 -20
  33. package/lib/extracts.ts +96 -73
  34. package/lib/fst/autocomplete.ts +52 -35
  35. package/lib/fst/builder.ts +30 -36
  36. package/lib/fst/deserialize-web.ts +8 -9
  37. package/lib/fst/format.ts +41 -23
  38. package/lib/fst/freshness.ts +17 -95
  39. package/lib/fst/matcher.ts +2 -2
  40. package/lib/fst/serialize.ts +45 -60
  41. package/lib/fst/types.ts +71 -59
  42. package/lib/fts/query.ts +5 -36
  43. package/lib/fts.ts +280 -0
  44. package/lib/geonames/aliases.ts +72 -149
  45. package/lib/geonames/postal.ts +162 -77
  46. package/lib/index.ts +20 -27
  47. package/lib/interpolation.ts +43 -96
  48. package/lib/lookup.ts +137 -257
  49. package/lib/name-score.ts +9 -7
  50. package/lib/nsul/lookup.ts +36 -53
  51. package/lib/nsul/schema.ts +16 -43
  52. package/lib/paths.ts +159 -0
  53. package/lib/place-importance-schema.ts +57 -164
  54. package/lib/poi/lookup.ts +100 -109
  55. package/lib/poi/schema.ts +42 -26
  56. package/lib/polygon-schema.ts +10 -6
  57. package/lib/postal/city/alias/lookup.ts +79 -0
  58. package/lib/postal/{city-alias-schema.ts → city/alias/schema.ts} +14 -10
  59. package/lib/postal/{city-candidate-schema.ts → city/candidate-schema.ts} +26 -20
  60. package/lib/postal.ts +9 -0
  61. package/lib/postcode-point-lookup.ts +9 -19
  62. package/lib/primary-preference.ts +45 -125
  63. package/lib/proximity-rerank.ts +43 -32
  64. package/lib/ranking-weights.ts +98 -76
  65. package/lib/region-keys.ts +68 -40
  66. package/lib/reverse.ts +108 -72
  67. package/lib/schema.ts +74 -42
  68. package/lib/search-fetch.ts +90 -68
  69. package/lib/sqlite/convention-source.ts +57 -0
  70. package/lib/sqlite/utils.ts +79 -0
  71. package/lib/street/centroid/schema.ts +149 -0
  72. package/lib/street/centroid.ts +16 -25
  73. package/lib/street/{morphology-fst-builder.ts → morphology/fst/builder.ts} +56 -41
  74. package/lib/street/{morphology-fst-loader.ts → morphology/fst/loader.ts} +36 -29
  75. package/lib/street/name-lookup.ts +43 -35
  76. package/lib/street/normalize.ts +85 -216
  77. package/lib/street/segment-schema.ts +116 -43
  78. package/lib/{street/index.ts → street.ts} +3 -3
  79. package/lib/types.ts +94 -140
  80. package/lib/unified-schema.ts +29 -29
  81. package/lib/uprn/existence.ts +30 -19
  82. package/lib/uprn/lookup.ts +28 -67
  83. package/lib/uprn/schema.ts +18 -40
  84. package/lib/weights-overlay-linker.ts +204 -232
  85. package/out/address/point/interpolation.d.ts +45 -0
  86. package/out/address/point/interpolation.d.ts.map +1 -0
  87. package/out/address/{point-interpolation.js → point/interpolation.js} +38 -38
  88. package/out/address/point/interpolation.js.map +1 -0
  89. package/out/address/point/schema.d.ts +74 -0
  90. package/out/address/point/schema.d.ts.map +1 -0
  91. package/out/address/{point-schema.js → point/schema.js} +4 -20
  92. package/out/address/point/schema.js.map +1 -0
  93. package/out/address/point.d.ts +24 -21
  94. package/out/address/point.d.ts.map +1 -1
  95. package/out/address/point.js +60 -77
  96. package/out/address/point.js.map +1 -1
  97. package/out/{address/index.js → address.d.ts} +3 -3
  98. package/out/address.d.ts.map +1 -0
  99. package/out/{address/index.d.ts → address.js} +3 -3
  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 +18 -76
  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 +170 -0
  110. package/out/build/candidate.d.ts.map +1 -0
  111. package/out/{build-candidate.js → build/candidate.js} +33 -102
  112. package/out/build/candidate.js.map +1 -0
  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 +44 -0
  118. package/out/build/slim.d.ts.map +1 -0
  119. package/out/{build-slim.js → build/slim.js} +33 -94
  120. package/out/build/slim.js.map +1 -0
  121. package/out/candidate/alias-bags.d.ts +13 -5
  122. package/out/candidate/alias-bags.d.ts.map +1 -1
  123. package/out/candidate/alias-bags.js +55 -12
  124. package/out/candidate/alias-bags.js.map +1 -1
  125. package/out/candidate/ancestors/schema.d.ts +166 -0
  126. package/out/candidate/ancestors/schema.d.ts.map +1 -0
  127. package/out/candidate/ancestors/schema.js +131 -0
  128. package/out/candidate/ancestors/schema.js.map +1 -0
  129. package/out/candidate/ancestors/sidecar.d.ts +27 -0
  130. package/out/candidate/ancestors/sidecar.d.ts.map +1 -0
  131. package/out/candidate/{ancestors-sidecar.js → ancestors/sidecar.js} +17 -24
  132. package/out/candidate/ancestors/sidecar.js.map +1 -0
  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 +8 -16
  138. package/out/candidate/extract-fold.d.ts.map +1 -1
  139. package/out/candidate/extract-fold.js +6 -48
  140. package/out/candidate/extract-fold.js.map +1 -1
  141. package/out/candidate/fts.d.ts +19 -0
  142. package/out/candidate/fts.d.ts.map +1 -0
  143. package/out/candidate/fts.js +22 -0
  144. package/out/candidate/fts.js.map +1 -0
  145. package/out/candidate/importance.d.ts +127 -0
  146. package/out/candidate/importance.d.ts.map +1 -0
  147. package/out/candidate/importance.js +162 -0
  148. package/out/candidate/importance.js.map +1 -0
  149. package/out/candidate/lookup.d.ts +36 -0
  150. package/out/candidate/lookup.d.ts.map +1 -0
  151. package/out/candidate/lookup.js +456 -0
  152. package/out/candidate/lookup.js.map +1 -0
  153. package/out/candidate/name-roles.d.ts +10 -22
  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 +34 -31
  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 +120 -0
  166. package/out/candidate/schema.d.ts.map +1 -0
  167. package/out/{candidate-schema.js → candidate/schema.js} +11 -23
  168. package/out/candidate/schema.js.map +1 -0
  169. package/out/{candidate-scoring.d.ts → candidate/scoring.d.ts} +8 -9
  170. package/out/candidate/scoring.d.ts.map +1 -0
  171. package/out/candidate/scoring.js +157 -0
  172. package/out/candidate/scoring.js.map +1 -0
  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 +26 -24
  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 +61 -50
  184. package/out/coincident-roles.js.map +1 -1
  185. package/out/convention/schema.d.ts +12 -6
  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 +33 -38
  194. package/out/coverage-manifest-schema.d.ts.map +1 -1
  195. package/out/coverage-manifest-schema.js +20 -29
  196. package/out/coverage-manifest-schema.js.map +1 -1
  197. package/out/currency-backfill.d.ts +13 -35
  198. package/out/currency-backfill.d.ts.map +1 -1
  199. package/out/currency-backfill.js +12 -39
  200. package/out/currency-backfill.js.map +1 -1
  201. package/out/env.d.ts +2 -2
  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 +31 -20
  206. package/out/exact-match.js.map +1 -1
  207. package/out/extracts.d.ts +65 -54
  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 +28 -20
  212. package/out/fst/autocomplete.d.ts.map +1 -1
  213. package/out/fst/autocomplete.js +38 -27
  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 +30 -36
  217. package/out/fst/builder.js.map +1 -1
  218. package/out/fst/deserialize-web.d.ts +3 -3
  219. package/out/fst/deserialize-web.d.ts.map +1 -1
  220. package/out/fst/deserialize-web.js +8 -9
  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 +11 -73
  227. package/out/fst/freshness.d.ts.map +1 -1
  228. package/out/fst/freshness.js +12 -76
  229. package/out/fst/freshness.js.map +1 -1
  230. package/out/fst/matcher.d.ts +1 -1
  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 +30 -26
  235. package/out/fst/serialize.d.ts.map +1 -1
  236. package/out/fst/serialize.js +44 -58
  237. package/out/fst/serialize.js.map +1 -1
  238. package/out/fst/types.d.ts +71 -59
  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} +1 -1
  243. package/out/fst.d.ts.map +1 -0
  244. package/out/{fst/index.d.ts → fst.js} +1 -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 -67
  255. package/out/geonames/aliases.d.ts.map +1 -1
  256. package/out/geonames/aliases.js +44 -121
  257. package/out/geonames/aliases.js.map +1 -1
  258. package/out/geonames/postal.d.ts +84 -50
  259. package/out/geonames/postal.d.ts.map +1 -1
  260. package/out/geonames/postal.js +115 -78
  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 +20 -21
  267. package/out/index.d.ts.map +1 -1
  268. package/out/index.js +14 -15
  269. package/out/index.js.map +1 -1
  270. package/out/interpolation.d.ts +15 -52
  271. package/out/interpolation.d.ts.map +1 -1
  272. package/out/interpolation.js +25 -68
  273. package/out/interpolation.js.map +1 -1
  274. package/out/lookup.d.ts +24 -53
  275. package/out/lookup.d.ts.map +1 -1
  276. package/out/lookup.js +117 -223
  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 +49 -141
  299. package/out/place-importance-schema.d.ts.map +1 -1
  300. package/out/place-importance-schema.js +25 -140
  301. package/out/place-importance-schema.js.map +1 -1
  302. package/out/poi/lookup.d.ts +38 -42
  303. package/out/poi/lookup.d.ts.map +1 -1
  304. package/out/poi/lookup.js +67 -90
  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 → city/alias/lookup.d.ts} +22 -25
  319. package/out/postal/city/alias/lookup.d.ts.map +1 -0
  320. package/out/postal/city/alias/lookup.js +49 -0
  321. package/out/postal/city/alias/lookup.js.map +1 -0
  322. package/out/postal/{city-alias-schema.d.ts → city/alias/schema.d.ts} +15 -11
  323. package/out/postal/city/alias/schema.d.ts.map +1 -0
  324. package/out/postal/{city-alias-schema.js → city/alias/schema.js} +11 -9
  325. package/out/postal/city/alias/schema.js.map +1 -0
  326. package/out/postal/city/candidate-schema.d.ts +72 -0
  327. package/out/postal/city/candidate-schema.d.ts.map +1 -0
  328. package/out/postal/city/candidate-schema.js +60 -0
  329. package/out/postal/city/candidate-schema.js.map +1 -0
  330. package/out/postal.d.ts +9 -0
  331. package/out/postal.d.ts.map +1 -0
  332. package/out/postal.js +9 -0
  333. package/out/postal.js.map +1 -0
  334. package/out/postcode-point-lookup.d.ts +9 -19
  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 +39 -93
  339. package/out/primary-preference.d.ts.map +1 -1
  340. package/out/primary-preference.js +13 -97
  341. package/out/primary-preference.js.map +1 -1
  342. package/out/proximity-rerank.d.ts +40 -29
  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 +91 -62
  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 +48 -29
  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 +75 -50
  355. package/out/reverse.d.ts.map +1 -1
  356. package/out/reverse.js +61 -49
  357. package/out/reverse.js.map +1 -1
  358. package/out/schema.d.ts +74 -42
  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 +25 -0
  367. package/out/sqlite/convention-source.d.ts.map +1 -0
  368. package/out/sqlite/convention-source.js +48 -0
  369. package/out/sqlite/convention-source.js.map +1 -0
  370. package/out/sqlite/utils.d.ts +45 -0
  371. package/out/sqlite/utils.d.ts.map +1 -0
  372. package/out/sqlite/utils.js +56 -0
  373. package/out/sqlite/utils.js.map +1 -0
  374. package/out/street/centroid/schema.d.ts +100 -0
  375. package/out/street/centroid/schema.d.ts.map +1 -0
  376. package/out/street/centroid/schema.js +66 -0
  377. package/out/street/centroid/schema.js.map +1 -0
  378. package/out/street/centroid.d.ts +9 -19
  379. package/out/street/centroid.d.ts.map +1 -1
  380. package/out/street/centroid.js +11 -22
  381. package/out/street/centroid.js.map +1 -1
  382. package/out/street/{morphology-fst-builder.d.ts → morphology/fst/builder.d.ts} +21 -11
  383. package/out/street/morphology/fst/builder.d.ts.map +1 -0
  384. package/out/street/{morphology-fst-builder.js → morphology/fst/builder.js} +40 -34
  385. package/out/street/morphology/fst/builder.js.map +1 -0
  386. package/out/street/morphology/fst/loader.d.ts +74 -0
  387. package/out/street/morphology/fst/loader.d.ts.map +1 -0
  388. package/out/street/{morphology-fst-loader.js → morphology/fst/loader.js} +21 -21
  389. package/out/street/morphology/fst/loader.js.map +1 -0
  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 +32 -31
  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 +74 -191
  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.d.ts → street.d.ts} +4 -4
  403. package/out/street.d.ts.map +1 -0
  404. package/out/{street/index.js → street.js} +4 -4
  405. package/out/street.js.map +1 -0
  406. package/out/types.d.ts +69 -138
  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 +20 -56
  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 +132 -141
  431. package/out/weights-overlay-linker.d.ts.map +1 -1
  432. package/out/weights-overlay-linker.js +86 -154
  433. package/out/weights-overlay-linker.js.map +1 -1
  434. package/package.json +94 -599
  435. package/lib/address/point-schema.ts +0 -155
  436. package/lib/build-candidate.ts +0 -632
  437. package/lib/build-slim.ts +0 -408
  438. package/lib/candidate-ancestors-schema.ts +0 -195
  439. package/lib/candidate-fts.ts +0 -46
  440. package/lib/candidate-importance.ts +0 -224
  441. package/lib/candidate-lookup.ts +0 -977
  442. package/lib/candidate-schema.ts +0 -247
  443. package/lib/candidate-scoring.ts +0 -261
  444. package/lib/convention/index.ts +0 -154
  445. package/lib/fts/index.ts +0 -361
  446. package/lib/postal/city-alias-lookup.ts +0 -97
  447. package/lib/postal/index.ts +0 -9
  448. package/lib/sqlite-convention-source.ts +0 -65
  449. package/lib/sqlite-utils.ts +0 -82
  450. package/lib/street/centroid-schema.ts +0 -154
  451. package/out/address/index.d.ts.map +0 -1
  452. package/out/address/index.js.map +0 -1
  453. package/out/address/point-interpolation.d.ts +0 -49
  454. package/out/address/point-interpolation.d.ts.map +0 -1
  455. package/out/address/point-interpolation.js.map +0 -1
  456. package/out/address/point-schema.d.ts +0 -97
  457. package/out/address/point-schema.d.ts.map +0 -1
  458. package/out/address/point-schema.js.map +0 -1
  459. package/out/ancestry/index.d.ts +0 -45
  460. package/out/ancestry/index.d.ts.map +0 -1
  461. package/out/ancestry/index.js +0 -55
  462. package/out/ancestry/index.js.map +0 -1
  463. package/out/build-candidate.d.ts +0 -184
  464. package/out/build-candidate.d.ts.map +0 -1
  465. package/out/build-candidate.js.map +0 -1
  466. package/out/build-slim.d.ts +0 -80
  467. package/out/build-slim.d.ts.map +0 -1
  468. package/out/build-slim.js.map +0 -1
  469. package/out/candidate/ancestors-sidecar.d.ts +0 -32
  470. package/out/candidate/ancestors-sidecar.d.ts.map +0 -1
  471. package/out/candidate/ancestors-sidecar.js.map +0 -1
  472. package/out/candidate-ancestors-schema.d.ts +0 -150
  473. package/out/candidate-ancestors-schema.d.ts.map +0 -1
  474. package/out/candidate-ancestors-schema.js +0 -123
  475. package/out/candidate-ancestors-schema.js.map +0 -1
  476. package/out/candidate-fts.d.ts +0 -37
  477. package/out/candidate-fts.d.ts.map +0 -1
  478. package/out/candidate-fts.js +0 -40
  479. package/out/candidate-fts.js.map +0 -1
  480. package/out/candidate-importance.d.ts +0 -132
  481. package/out/candidate-importance.d.ts.map +0 -1
  482. package/out/candidate-importance.js +0 -169
  483. package/out/candidate-importance.js.map +0 -1
  484. package/out/candidate-lookup.d.ts +0 -76
  485. package/out/candidate-lookup.d.ts.map +0 -1
  486. package/out/candidate-lookup.js +0 -768
  487. package/out/candidate-lookup.js.map +0 -1
  488. package/out/candidate-schema.d.ts +0 -155
  489. package/out/candidate-schema.d.ts.map +0 -1
  490. package/out/candidate-schema.js.map +0 -1
  491. package/out/candidate-scoring.d.ts.map +0 -1
  492. package/out/candidate-scoring.js +0 -196
  493. package/out/candidate-scoring.js.map +0 -1
  494. package/out/convention/index.d.ts +0 -106
  495. package/out/convention/index.d.ts.map +0 -1
  496. package/out/convention/index.js +0 -93
  497. package/out/convention/index.js.map +0 -1
  498. package/out/fst/index.d.ts.map +0 -1
  499. package/out/fst/index.js.map +0 -1
  500. package/out/fts/index.d.ts +0 -184
  501. package/out/fts/index.d.ts.map +0 -1
  502. package/out/fts/index.js +0 -277
  503. package/out/fts/index.js.map +0 -1
  504. package/out/geonames/index.d.ts.map +0 -1
  505. package/out/geonames/index.js.map +0 -1
  506. package/out/nsul/index.d.ts.map +0 -1
  507. package/out/nsul/index.js.map +0 -1
  508. package/out/poi/index.d.ts.map +0 -1
  509. package/out/poi/index.js.map +0 -1
  510. package/out/postal/city-alias-lookup.d.ts.map +0 -1
  511. package/out/postal/city-alias-lookup.js +0 -62
  512. package/out/postal/city-alias-lookup.js.map +0 -1
  513. package/out/postal/city-alias-schema.d.ts.map +0 -1
  514. package/out/postal/city-alias-schema.js.map +0 -1
  515. package/out/postal/city-candidate-schema.d.ts +0 -66
  516. package/out/postal/city-candidate-schema.d.ts.map +0 -1
  517. package/out/postal/city-candidate-schema.js +0 -56
  518. package/out/postal/city-candidate-schema.js.map +0 -1
  519. package/out/postal/index.d.ts +0 -9
  520. package/out/postal/index.d.ts.map +0 -1
  521. package/out/postal/index.js +0 -9
  522. package/out/postal/index.js.map +0 -1
  523. package/out/sqlite-convention-source.d.ts +0 -29
  524. package/out/sqlite-convention-source.d.ts.map +0 -1
  525. package/out/sqlite-convention-source.js +0 -57
  526. package/out/sqlite-convention-source.js.map +0 -1
  527. package/out/sqlite-utils.d.ts +0 -45
  528. package/out/sqlite-utils.d.ts.map +0 -1
  529. package/out/sqlite-utils.js +0 -57
  530. package/out/sqlite-utils.js.map +0 -1
  531. package/out/street/centroid-schema.d.ts +0 -105
  532. package/out/street/centroid-schema.d.ts.map +0 -1
  533. package/out/street/centroid-schema.js +0 -78
  534. package/out/street/centroid-schema.js.map +0 -1
  535. package/out/street/index.d.ts.map +0 -1
  536. package/out/street/index.js.map +0 -1
  537. package/out/street/morphology-fst-builder.d.ts.map +0 -1
  538. package/out/street/morphology-fst-builder.js.map +0 -1
  539. package/out/street/morphology-fst-loader.d.ts +0 -67
  540. package/out/street/morphology-fst-loader.d.ts.map +0 -1
  541. package/out/street/morphology-fst-loader.js.map +0 -1
  542. package/out/uprn/index.d.ts.map +0 -1
  543. package/out/uprn/index.js.map +0 -1
  544. /package/lib/{fst/index.ts → fst.ts} +0 -0
  545. /package/lib/{geonames/index.ts → geonames.ts} +0 -0
  546. /package/lib/{nsul/index.ts → nsul.ts} +0 -0
  547. /package/lib/{poi/index.ts → poi.ts} +0 -0
  548. /package/lib/{uprn/index.ts → uprn.ts} +0 -0
package/README.md CHANGED
@@ -40,7 +40,7 @@ Databases are `ATTACH`ed by a schema name **derived from the filename**, and que
40
40
  - The name does not route. `postcode-ca-overture.db` derives `postcode_ca_overture`, and the router tests `startsWith("postalcode_")` — **"postcode" is not "postalcode"**. It held 843,739 Canadian codes and answered every query with zero hits, which is indistinguishable from "this country has no places".
41
41
  - The database carries `spr` but no `place_search`. It routes, then dies mid-`SELECT`.
42
42
 
43
- The predicate is the **table, not the filename**: a database carrying `spr` is claiming to be a place database, and every lookup path here reaches the FTS index. A relation-table database like `postcode-locality-<cc>.db` carries no `spr`, never makes that claim, and is exempt — which is what keeps the documented default database list working.
43
+ The predicate is the **table rather than the filename**: a database carrying `spr` is claiming to be a place database, and every lookup path here reaches the FTS index. A relation-table database like `postcode-locality-<cc>.db` carries no `spr`, never makes that claim, and is exempt — which is what keeps the documented default database list working.
44
44
 
45
45
  ```
46
46
  WOFSQLitePlaceLookup: …/postcode-ca-overture.db carries "spr" but no "place_search" table, so it
@@ -81,7 +81,7 @@ Cross-database `UNION` queries are not supported in one `findPlace` call — BM2
81
81
  comparable across separately-indexed corpora. Issue two `findPlace` calls and merge in your
82
82
  caller if you need that.
83
83
 
84
- ## Getting the WOF SQLite distribution
84
+ ## Get the WOF SQLite distribution
85
85
 
86
86
  The Geocode Earth team mirrors WOF SQLite distributions at <https://data.geocode.earth/wof/dist/sqlite/>. The two relevant distributions for v1:
87
87
 
@@ -137,7 +137,7 @@ Builds a trimmed WOF SQLite distribution sized for browser-side deployments (Pat
137
137
  # Defaults: --top 1000 localities, --countries US, drops geojson after building aux tables
138
138
  # The slim wof-hot.db distribution is RETIRED (2026-06-20): the demo byte-range-resolves
139
139
  # against the global candidate table. `buildSlimWOFDatabase` remains importable from
140
- # `@mailwoman/resolver-wof-sqlite/build-slim` for test fixtures. Historical invocation:
140
+ # `@mailwoman/resolver-wof-sqlite/build/slim` for test fixtures. Historical invocation:
141
141
  # mailwoman-wof-build-slim \
142
142
  --in /path/to/whosonfirst-data-admin-us-latest.db \
143
143
  --in /path/to/whosonfirst-data-postalcode-us-latest.db \
@@ -175,7 +175,7 @@ const { created, indexedRows, durationMs } = buildPlaceSearchFts(db, {
175
175
  })
176
176
  ```
177
177
 
178
- ## Ranking
178
+ ## Rank places
179
179
 
180
180
  The resolver scores candidates by:
181
181
 
@@ -237,11 +237,11 @@ are all stripped safely before going to FTS5. Per-token rules:
237
237
 
238
238
  - **Bare tokens** (`"Paris"`, `"62701"`) become FTS5 **phrase matches**: `"Paris"` matches places
239
239
  named exactly "Paris", `"62701"` matches the postcode 62701 exactly.
240
- - **Trailing `*`** (`"627*"`, `"Pari*"`) becomes FTS5 **prefix syntax**: `627*` matches every
240
+ - **A trailing `*`** (`"627*"`, `"Pari*"`) becomes FTS5 **prefix syntax**: `627*` matches every
241
241
  postcode starting with 627, `Pari*` matches Paris / Parishville / etc. The caller explicitly
242
242
  signals "prefix"; bare tokens stay phrase-matched for safety.
243
- - **Multiple tokens** join with implicit AND: `"Pari* TX"` matches places whose name contains
244
- both a `Pari*`-prefixed word AND the word `TX`.
243
+ - **Multiple tokens** join with an implicit `AND`: `"Pari* TX"` matches places whose name contains
244
+ both a `Pari*`-prefixed word and the word `TX`.
245
245
 
246
246
  Example: `findPlace({ text: "902*", placetype: "postalcode" })` returns 90201, 90210, 90211, …
247
247
  matching the Los Angeles ZIP corridor.
@@ -256,7 +256,7 @@ This package itself is AGPL-3.0; the WOF data it indexes is CC-BY 4.0. The two l
256
256
 
257
257
  ## Integration tests
258
258
 
259
- `resolver-wof-sqlite/integration.test.ts` exercises the resolver against a real WOF SQLite distribution. The suite is **skipped** when no DB is present — set `MAILWOMAN_WOF_DB` to override the lookup path, otherwise it defaults to `/mnt/playpen/mailwoman-data/wof/whosonfirst-data-admin-us-latest.db` (the canonical lab location). CI runs against the fixture-only suites; operators with real WOF data locally get an extra layer of validation.
259
+ `resolver-wof-sqlite/integration.test.ts` exercises the resolver against a real WOF SQLite distribution. The suite is **skipped** when no DB is present — set `MAILWOMAN_WOF_DB` to override the lookup path, otherwise it defaults to `$MAILWOMAN_DATA_ROOT/db/wof/whosonfirst-data-admin-us-latest.db` (the canonical lab location). CI runs against the fixture-only suites; operators with real WOF data locally get an extra layer of validation.
260
260
 
261
261
  Coverage includes: placetype filtering, country filtering, the empty-result case, FTS5 special-character sanitization, Japanese alt-name resolution, parent-constrained lookup, and a performance budget (`findPlace` < 250 ms against the 142 k-row US admin database).
262
262
 
@@ -3,47 +3,45 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Address-point interpolation — "Method 2" of the resolution ladder (#483, Phase 1 of
7
- * `docs/articles/plan/2026-06-11-resolution-ladder.md`): when the exact address-point tier (#476)
8
- * misses a house number, bracket the number with REAL neighbor points on the same street from the
9
- * same #476 extract and interpolate linearly in house-number space between them. Real occupancy
10
- * replaces TIGER's uniform-spacing assumption — the dominant error term of the TIGER pilot's check
11
- * miss; TIGER range interpolation (`StreetInterpolator`) demotes to the fallback for streets too
12
- * sparse to bracket.
6
+ * Address-point interpolation, the second method of the resolution ladder. When the exact
7
+ * address-point tier misses a house number, bracket the number with real neighbor points on the
8
+ * same street and interpolate linearly in house-number space between them. Real occupancy
9
+ * replaces the tiger range tier's uniform-spacing assumption. Tiger range interpolation
10
+ * (`StreetInterpolator`) is the fallback for streets too sparse to bracket.
13
11
  *
14
- * Matching key is `street_key` — THE shared normalizer plus the route fold
15
- * (`canonicalizeRouteKey`), identical at build time (`scripts/build-address-point-extract.ts`) and
16
- * query time, by construction. Scope is postcode-first like the segment tier; a query without a
17
- * postcode goes straight to the fallback (which carries its own statewide-ambiguity abstention).
12
+ * The matching key is `street_key`, the shared normalizer plus the route fold
13
+ * (`canonicalizeRouteKey`), identical at build time (`mailwoman situs address-points`) and query
14
+ * time by construction. Scope is postcode-first like the segment tier. A query without a postcode
15
+ * goes straight to the fallback. The fallback has its own statewide-ambiguity abstention.
18
16
  *
19
- * Bracketing contract:
17
+ * Neighbor candidates never include the queried number itself, so production never overrides an
18
+ * on-file number and grading against the same extract is non-circular.
20
19
  *
21
- * - Neighbor candidates NEVER include the queried number itself (any unit/duplicate row of it) — in
22
- * production the exact tier would already have answered an on-file number, and in the eval
23
- * this is what makes grading against the same extract non-circular by construction.
24
20
  * - Both-sided bracket (`bracket: "both"`): linear interpolation between the nearest known number
25
- * below and above; `uncertaintyM` = half the distance between them.
26
- * - Single-sided (`bracket: "single"`): linear extrapolation along the two nearest known numbers on
27
- * that side, capped at one pair-span beyond the nearest point (`t ≤ 2` — beyond that the line
28
- * carries no evidence and the query falls through); `uncertaintyM` = the pair distance plus
29
- * the extrapolated overshoot, explicitly larger than the both-sided radius.
30
- * - No bracket (no neighbors, a single known number, or past the extrapolation cap): fall through to
31
- * the TIGER fallback when configured, else null.
21
+ * below and above. `uncertaintyM` is half the distance between them.
22
+ * - Single-sided (`bracket: "single"`): linear extrapolation along the two nearest known numbers
23
+ * on that side, capped at one pair-span beyond the nearest point (`t ≤ 2`, past which the line
24
+ * provides no evidence and the query falls through). `uncertaintyM` is the pair distance plus
25
+ * the extrapolated overshoot, larger than the both-sided radius.
26
+ * - No bracket, meaning no neighbors, a single known number, or past the extrapolation cap: fall
27
+ * through to the tiger fallback when configured, else null.
32
28
  *
33
- * Standalone like the segment tier — core wiring rides the Phase 2 ordered `spatialTiers` list.
29
+ * Standalone like the segment tier, wired through the ordered `spatialTiers` list.
34
30
  */
35
31
 
36
32
  import type { InterpolationLookup } from "@mailwoman/core/resolver"
37
33
  import { haversineKm } from "@mailwoman/spatial"
38
34
  import { DatabaseClient } from "@mailwoman/sqlite/client"
39
35
 
40
- import type { AddressPointDatabase } from "#address/point-schema"
36
+ import type { AddressPointDatabase } from "#address/point/schema"
41
37
  import type { InterpolatedHit, InterpolationQuery, StreetInterpolator } from "#interpolation"
42
38
  import { hasTable, prepareAll, type PreparedAll } from "#sqlite-utils"
43
39
  import { canonicalizeRouteKey, type RouteKey, streetKeyVariants } from "#street/normalize"
44
40
  /**
45
- * Extrapolation cap for a single-sided bracket: at most one pair-span beyond the nearest known point (`t = 2`). Past
46
- * it, the two-point line carries no evidence about the query number.
41
+ * Extrapolation cap for a single-sided bracket: at most one pair-span beyond
42
+ * the nearest known point (`t = 2`).
43
+ *
44
+ * Past it, the two-point line provides no evidence about the query number.
47
45
  */
48
46
  const MAX_EXTRAPOLATION_T = 2
49
47
 
@@ -71,29 +69,31 @@ export class AddressPointInterpolator<
71
69
  > implements InterpolationLookup {
72
70
  readonly #db: DatabaseClient<DB>
73
71
  /**
74
- * Resources this instance opened. A connection handed in by a caller is NOT in here, so disposal cannot reach it —
75
- * ownership is membership rather than a flag a later branch has to check.
72
+ * Resources this instance opened.
73
+ *
74
+ * A connection handed in by a caller is not in here, so disposal cannot reach it.
75
+ * Ownership is membership rather than a flag a later branch checks.
76
76
  */
77
- readonly #resources = new DisposableStack()
77
+ readonly #resources: DisposableStack
78
78
  readonly #fallback: StreetInterpolator | undefined
79
79
  readonly #byPostcode: PreparedAll<[postcode: string, street: RouteKey, number: number], PointRow> | undefined
80
80
 
81
81
  constructor(opts: { dbPath?: string; database?: DatabaseClient<DB>; fallback?: StreetInterpolator }) {
82
+ using resources = new DisposableStack()
83
+
82
84
  if (opts.database) {
83
85
  this.#db = opts.database
84
86
  } else if (opts.dbPath) {
85
- this.#db = this.#resources.use(new DatabaseClient<DB>(opts.dbPath, { readOnly: true }))
87
+ this.#db = resources.use(new DatabaseClient<DB>(opts.dbPath, { readOnly: true }))
86
88
  } else {
87
89
  throw new Error("AddressPointInterpolator: one of dbPath or database is required")
88
90
  }
89
91
 
90
92
  this.#fallback = opts.fallback
91
93
 
92
- // Degrade gracefully on an empty/tableless extract (#568): with no `address_point` table this tier
93
- // is skipped, deferring to the segment fallback rather than crashing at construction.
94
+ // Degrade gracefully on an extract without an `address_point` table.
95
+ // The tier is skipped and defers to the segment fallback rather than crashing at construction.
94
96
  if (hasTable(this.#db, "address_point")) {
95
- // Strictly-numeric neighbor numbers on the route-folded street key within the ZIP. The
96
- // queried number itself is excluded HERE (see module doc: non-circular by construction).
97
97
  this.#byPostcode = prepareAll(
98
98
  this.#db,
99
99
  `SELECT CAST(number AS INTEGER) AS n, lat, lon, source, release
@@ -103,6 +103,8 @@ export class AddressPointInterpolator<
103
103
  AND CAST(number AS INTEGER) != ?`
104
104
  )
105
105
  }
106
+
107
+ this.#resources = resources.move()
106
108
  }
107
109
 
108
110
  find(query: InterpolationQuery): InterpolatedHit | null {
@@ -111,10 +113,9 @@ export class AddressPointInterpolator<
111
113
  if (!/^\d+$/.test(numberRaw)) return null
112
114
  const n = Number(numberRaw)
113
115
 
114
- // No own table (empty extract) or no postcode → defer to the segment fallback rather than query.
115
116
  if (!this.#byPostcode || !query.postcode) return this.#fallback?.find(query) ?? null
116
117
 
117
- // Key-variant ladder (see `streetKeyVariants`) — same probe order as the exact-point reader.
118
+ // Key-variant ladder (see `streetKeyVariants`), the same probe order as the exact-point reader.
118
119
  let rows: PointRow[] = []
119
120
 
120
121
  for (const variant of streetKeyVariants(query.street)) {
@@ -165,7 +166,8 @@ function anchorsByNumber(rows: readonly PointRow[]): NumberAnchor[] {
165
166
  function interpolateFromNeighbors(rows: readonly PointRow[], n: number): InterpolatedHit | null {
166
167
  const anchors = anchorsByNumber(rows)
167
168
 
168
- // Nearest known number below and above the query (the rows never contain n itself).
169
+ // Nearest known number below and above the query.
170
+ // The rows never contain n itself.
169
171
  let below: NumberAnchor | undefined
170
172
  let above: NumberAnchor | undefined
171
173
 
@@ -196,7 +198,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
196
198
  }
197
199
 
198
200
  // Single-sided: extrapolate along the two nearest known numbers on the populated side.
199
- // `near` is the anchor closest to n, `far` the next one out; t > 1 by construction.
201
+ // `t > 1` by construction because n lies outside the span.
200
202
  const side = below ? anchors.slice(-2) : anchors.slice(0, 2)
201
203
 
202
204
  if (side.length < 2) return null
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import type { Kysely } from "kysely"
8
+
9
+ import type { NameKey, RouteKey, StreetKey } from "#street/normalize"
10
+
11
+ /**
12
+ * One rooftop address point; `(street_norm, number)` within a `postcode`
13
+ * or `locality_norm` scope is the lookup.
14
+ */
15
+ export interface AddressPointTable {
16
+ /**
17
+ * Shared {@link normalizeStreetForKey} of the street — the build/query-consistent probe key.
18
+ */
19
+ street_norm: StreetKey
20
+ /**
21
+ * `canonicalizeRouteKey(street_norm)` — the route-fold key, branded
22
+ * so it cannot be interchanged with `street_norm`.
23
+ */
24
+ street_key: RouteKey
25
+ /**
26
+ * House number, normalized lower-case (kept text — "123-A", "12 1/2" must survive).
27
+ */
28
+ number: string
29
+ unit: string | null
30
+ postcode: string | null
31
+ /**
32
+ * Shared {@link normalizeLocalityForKey} of the locality — the fallback scope.
33
+ */
34
+ locality_norm: NameKey | null
35
+
36
+ street_raw: string
37
+ lat: number
38
+ lon: number
39
+ /**
40
+ * Provenance: the dataset this point came from (e.g. `overture:us`, `openaddresses`).
41
+ */
42
+ source: string
43
+
44
+ release: string
45
+ /**
46
+ * The source register's stable administrative key for the point's commune or municipality —
47
+ * BAN's `code_insee`; the coverage basis is computed per this key.
48
+ */
49
+ admin_code: string | null
50
+ /**
51
+ * The register's own certification flag for the point
52
+ * (BAN `certification_commune`: 1 certified, 0 not), never inferred from a share.
53
+ */
54
+ certified: number | null
55
+ }
56
+
57
+ /**
58
+ * The address-point database schema for `new DatabaseClient<AddressPointDatabase>(...)`.
59
+ */
60
+ export interface AddressPointDatabase {
61
+ address_point: AddressPointTable
62
+ }
63
+
64
+ /**
65
+ * The subset of a Kysely handle the DDL touches, narrowed to `schema`
66
+ * because Kysely is invariant in its schema parameter.
67
+ */
68
+ export type AddressPointSchemaHandle = Pick<Kysely<AddressPointDatabase>, "schema">
69
+
70
+ /**
71
+ * The `address_point` columns in insert order, from which the builder derives its positional placeholders.
72
+ */
73
+ export const ADDRESS_POINT_COLUMNS = [
74
+ "street_norm",
75
+ "street_key",
76
+ "number",
77
+ "unit",
78
+ "postcode",
79
+ "locality_norm",
80
+ "street_raw",
81
+ "lat",
82
+ "lon",
83
+ "source",
84
+ "release",
85
+ "admin_code",
86
+ "certified",
87
+ ] as const
88
+
89
+ /**
90
+ * Create the `address_point` table — called before the streaming bulk load.
91
+ */
92
+ export async function createAddressPointTable(db: AddressPointSchemaHandle): Promise<void> {
93
+ await db.schema
94
+ .createTable("address_point")
95
+ .addColumn("street_norm", "text", (c) => c.notNull())
96
+
97
+ .addColumn("street_key", "text", (c) => c.notNull())
98
+ .addColumn("number", "text", (c) => c.notNull())
99
+ .addColumn("unit", "text")
100
+ .addColumn("postcode", "text")
101
+ .addColumn("locality_norm", "text")
102
+ .addColumn("street_raw", "text", (c) => c.notNull())
103
+ .addColumn("lat", "real", (c) => c.notNull())
104
+ .addColumn("lon", "real", (c) => c.notNull())
105
+ .addColumn("source", "text", (c) => c.notNull())
106
+ .addColumn("release", "text", (c) => c.notNull())
107
+ .addColumn("admin_code", "text")
108
+ .addColumn("certified", "integer")
109
+ .execute()
110
+ }
111
+
112
+ /**
113
+ * Create the three probe indexes the reader relies on (postcode-scope, locality-scope, route-key).
114
+ */
115
+ export async function createAddressPointIndexes(db: AddressPointSchemaHandle): Promise<void> {
116
+ await db.schema
117
+ .createIndex("idx_ap_postcode")
118
+ .on("address_point")
119
+ .columns(["postcode", "street_norm", "number"])
120
+ .execute()
121
+
122
+ await db.schema
123
+ .createIndex("idx_ap_locality")
124
+ .on("address_point")
125
+ .columns(["locality_norm", "street_norm", "number"])
126
+ .execute()
127
+
128
+ await db.schema.createIndex("idx_ap_streetkey").on("address_point").columns(["postcode", "street_key"]).execute()
129
+ // Street-first index for the bbox scope: OSM points often have no postcode or locality,
130
+ // so the reader scopes a `(street_norm, number)` probe by the resolved locality's bbox.
131
+ await db.schema.createIndex("idx_ap_street").on("address_point").columns(["street_norm", "number"]).execute()
132
+ }
@@ -2,23 +2,14 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
- *
6
- * SQLite implementation of core's `AddressPointLookup` (#476): exact `(street, number)` within a
7
- * postcode (preferred), locality, or — for extracts whose points carry no scope tag (OSM, #247) —
8
- * the resolved locality's BBOX. Query-side normalization is THE shared normalizer
9
- * (`street-normalize.ts`), selected per the extract's `streetLocale` so build-side and probe-side
10
- * stay identical by construction (US delegates to the USPS pipeline; FR/DE/NL use the locale rules).
11
- *
12
- * Matching is exact-after-normalization only — no fuzzy street matching in this tier (measure how
13
- * far exact gets first; fuzz is a later, separate decision). Scope order is most-selective first:
14
- * postcode, then locality, then the bbox fall-through (only when a bbox is supplied AND the prior
15
- * scopes missed). Multiple hits return the first by rowid — unit siblings share the building coord.
5
+ * @file SQLite implementation of core's `AddressPointLookup`.
16
6
  */
17
7
 
18
8
  import type { AddressPointHit, AddressPointLookup } from "@mailwoman/core/resolver"
19
9
  import { DatabaseClient } from "@mailwoman/sqlite/client"
10
+ import type { PathBuilderLike } from "path-ts"
20
11
 
21
- import type { AddressPointDatabase, AddressPointTable } from "#address/point-schema"
12
+ import type { AddressPointDatabase, AddressPointTable } from "#address/point/schema"
22
13
  import { hasTable, prepareGet, type PreparedGet } from "#sqlite-utils"
23
14
  import {
24
15
  normalizeHouseNumberForKey,
@@ -33,17 +24,28 @@ import {
33
24
  } from "#street/normalize"
34
25
 
35
26
  /**
36
- * The columns this lookup projects — a typed slice of the SHARED {@link AddressPointTable}, so a column rename in
37
- * `build-address-point-extract.ts` (the writer) is a compile error here (the reader).
27
+ * The columns this lookup reads, typed from the shared {@link AddressPointTable}
28
+ * so a writer-side rename fails to compile here.
38
29
  */
39
30
  type AddressPointRow = Pick<AddressPointTable, "lat" | "lon" | "source" | "release" | "locality_norm" | "postcode">
40
31
 
41
32
  /**
42
- * The 4 columns the reader SELECTs, in the schema's order — referenced by the prepared SELECTs so the projected
43
- * `AddressPointRow` stays in lockstep with the shared schema.
33
+ * The column list every prepared SELECT uses.
34
+ * It must match the fields of `AddressPointRow`.
44
35
  */
45
36
  const SELECT_COLS = "lat, lon, source, release, locality_norm, postcode"
46
37
 
38
+ /**
39
+ * Finds an address point by exact normalized `(street, number)` within a postcode,
40
+ * a locality, or a bounding box.
41
+ *
42
+ * Query normalization uses the extract's `streetLocale`, which must match the
43
+ * locale the extract was built with.
44
+ * Street matching is exact after normalization.
45
+ *
46
+ * Scopes are tried from most to least selective: postcode, locality, locality suffix, then bounding box.
47
+ * When several rows match, any one of them can answer, because unit siblings share the building coordinate.
48
+ */
47
49
  export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressPointDatabase>
48
50
  implements AddressPointLookup, Disposable
49
51
  {
@@ -56,8 +58,9 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
56
58
  | undefined
57
59
  readonly #byLocality: PreparedGet<[locality: NameKey, street: StreetKey, number: string], AddressPointRow> | undefined
58
60
  /**
59
- * The scope key matched by its TAIL — a `zh` query that names the 鄉鎮市區 without its 縣市. Narrowed by the (street,
60
- * number) index first, so the LIKE walks the handful of rows that share the pair.
61
+ * This query matches the locality key by its suffix, for a `zh` query that gives the 鄉鎮市區 without its 縣市.
62
+ *
63
+ * The (street, number) index narrows the rows first, so the `LIKE` scans only rows that share the pair.
61
64
  */
62
65
  readonly #byLocalityTail:
63
66
  | PreparedGet<[street: StreetKey, number: string, tailPattern: string], AddressPointRow>
@@ -71,22 +74,25 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
71
74
 
72
75
  /**
73
76
  * @param dbPath Extract path.
74
- * @param opts.streetLocale The street-normalization locale this extract was BUILT with — must match, or every key
75
- * misses. Defaults to `"us"` (the situs tier), so existing callers are unchanged.
76
- * @param opts.localityKeys Whether the extract's `locality_norm` is a FULL place name a query can be held to. The BAN
77
- * and OSM extracts write the commune or `addr:city` in full; the US situs extract writes the NAD city field, which
78
- * several counties abbreviate (`addi` for Addison on 5,174 Texas rows, 327,264 Texas rows at four characters or
79
- * fewer) or give as the parent town (`easton` for North Easton). A key like that can steer WHICH row answers but
80
- * cannot refuse one, so it never contradicts. Defaults from the street locale: `"us"` is abbreviated, the rest
81
- * full.
77
+ * @param opts.streetLocale The street-normalization locale the extract was built with.
78
+ * A mismatch makes every key miss.
79
+ * It defaults to `"us"`.
80
+ * @param opts.localityKeys Whether the extract's `locality_norm` holds full place names.
81
+ * With `"abbreviated"`, a row's locality can select a row but never rejects one.
82
+ * The US situs extract uses NAD city fields.
83
+ * Some counties abbreviate them (`addi` for Addison).
84
+ * Other counties replace them with the parent town.
85
+ * It defaults to `"abbreviated"` for `"us"` and `"full"` for every other locale.
82
86
  */
83
- constructor(dbPath: string, opts: { streetLocale?: StreetLocale; localityKeys?: "full" | "abbreviated" } = {}) {
87
+ constructor(
88
+ dbPath: PathBuilderLike,
89
+ opts: { streetLocale?: StreetLocale; localityKeys?: "full" | "abbreviated" } = {}
90
+ ) {
84
91
  this.#db = new DatabaseClient<DB>(dbPath, { readOnly: true })
85
92
  this.#locale = opts.streetLocale ?? "us"
86
93
  this.#localityKeys = opts.localityKeys ?? (this.#locale === "us" ? "abbreviated" : "full")
87
94
 
88
- // Degrade gracefully on an empty/tableless extract (interrupted build, stray 0-byte file): with no
89
- // `address_point` table this lookup is a no-op miss, not a crash that loses the whole state (#568).
95
+ // An extract without an `address_point` table, such as an interrupted build, makes every lookup miss.
90
96
  if (hasTable(this.#db, "address_point")) {
91
97
  this.#byPostcode = prepareGet(
92
98
  this.#db,
@@ -134,10 +140,10 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
134
140
 
135
141
  if (!number) return null
136
142
 
137
- // A `zh` extract scopes a point by 縣市 + 鄉鎮市區, the pair the parse tags `region` + `subregion`; the Taiwanese
138
- // register carries no postcode and the parse no `locality`. The pair becomes the locality key here, on the
139
- // reader that was built with the same fold, and a Latin extract never sees the two fields. A line that names
140
- // only the 鄉鎮市區 (`中和區中興街281號`, 14.7% of the TW board) matches the stored pair by its tail instead.
143
+ // A `zh` extract keys locality as 縣市 + 鄉鎮市區.
144
+ // The parser tags 縣市 as `region` and 鄉鎮市區 as `subregion`.
145
+ // The Taiwanese register has no postcode, so this pair becomes the locality key.
146
+ // A query with only the 鄉鎮市區 matches the stored key by its suffix.
141
147
  const scoped =
142
148
  this.#locale === "zh" && !query.locality && query.subregion
143
149
  ? query.region
@@ -145,9 +151,9 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
145
151
  : { ...query, localityTail: normalizeLocalityForKeyLocale(query.subregion, "zh") }
146
152
  : query
147
153
 
148
- // Key-variant ladder (see `streetKeyVariants`): the literal key first, then the doubled-type
149
- // collapse and the saint↔st register swap — each variant runs the FULL number ladder below, and
150
- // the first variant to answer wins, so an attested literal key is never second-guessed.
154
+ // `streetKeyVariants` yields the literal key first.
155
+ // Each variant runs the full number fallback sequence.
156
+ // The first variant with a match wins.
151
157
  let row: AddressPointRow | undefined
152
158
 
153
159
  for (const streetNorm of streetKeyVariants(query.street, streetLocaleForSurface(query.street, this.#locale))) {
@@ -169,8 +175,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
169
175
  }
170
176
 
171
177
  /**
172
- * The full number ladder for ONE normalized street key: exact, then the range low-end, then letter-suffix spacing
173
- * with the base-number fall — see each rung's note in place.
178
+ * Tries one normalized street key with the exact number, then fallback number forms.
179
+ * Each fallback runs only after the previous forms missed.
174
180
  */
175
181
  #findForKey(
176
182
  streetNorm: StreetKey,
@@ -186,10 +192,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
186
192
  ) {
187
193
  let row = this.#probe(streetNorm, number, query)
188
194
 
189
- // Range-surface fallback: every register this reader serves stores ONE number per point
190
- // (G-NAF `NUMBER_FIRST`, BAN, OA, OSM `addr:housenumber`), but the attested surface is often
191
- // a range — "385-387 Esplanade" keys `385`. Null-only: an exact range key that matched above
192
- // (some OSM points DO carry "385-387" verbatim) is never second-guessed.
195
+ // The registers store one number per point, so a range such as "385-387" falls back to its low end.
193
196
  if (!row) {
194
197
  const low = /^(\d+[a-z]?)-\d+[a-z]?$/.exec(number)?.[1]
195
198
 
@@ -198,12 +201,9 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
198
201
  }
199
202
  }
200
203
 
201
- // Letter-suffix spacing fallback: the registers disagree on the joint — BAN stores "3 a"
202
- // (space-separated), G-NAF and most OA sources store "3a" — and the parsed surface can arrive
203
- // either way. On a miss, retry the OTHER spacing; on a double miss, the BASE number (the
204
- // register attests no 3A but does attest 3 — the adjacent-parcel approximation, priced the
205
- // same as the range fallback's low end). Null-only throughout, and only for the
206
- // digits+single-letter shape (never touches "12 1/2" or unit-bearing forms).
204
+ // BAN stores a letter suffix as "3 a", while G-NAF and most OA sources store "3a".
205
+ // For a number of digits plus one letter, the lookup retries the other spacing
206
+ // and then the bare number.
207
207
  if (!row) {
208
208
  const joined = /^(\d+)\s+([a-z])$/.exec(number)
209
209
  const spaced = /^(\d+)([a-z])$/.exec(number)
@@ -215,11 +215,10 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
215
215
  }
216
216
  }
217
217
 
218
- // Sub-number fallback for the Taiwanese register: `14之12` is building 12 off number 14, `30附40` an attached
219
- // number, stored as number `14` / `30` with the rest in `unit`. A query that kept the pair on the number span
220
- // falls to the base number — the same adjacent-parcel approximation the letter-suffix rung makes, priced the
221
- // same: on the 2,000-row served read the rows answered this way sit a median 256 m and at most 1.0 km from the
222
- // building.
218
+ // The Taiwanese register stores `14之12` and `30附40` as numbers `14` and `30`.
219
+ // It stores the rest in `unit`.
220
+ // A query number that still includes the sub-number falls back to the base number.
221
+ // That result is approximate.
223
222
  if (!row && this.#locale === "zh") {
224
223
  const base = /^(\d+)(?:[之附]\d+)+$/u.exec(number)?.[1]
225
224
 
@@ -232,8 +231,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
232
231
  }
233
232
 
234
233
  /**
235
- * The scope ladder for one (street, number) key: postcode, then locality, then the bbox fall-through — each rung only
236
- * when the prior missed.
234
+ * Probes one (street, number) key by postcode, then locality, then locality suffix, then bounding box.
235
+ * Each scope runs only when the previous ones missed.
237
236
  */
238
237
  #probe(
239
238
  streetNorm: StreetKey,
@@ -251,11 +250,9 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
251
250
  const postcode = query.postcode.trim()
252
251
  const localityKey = query.locality ? this.#localityKey(query.locality) : undefined
253
252
 
254
- // A postcode can span several places — DE 04509 covers Schönwölkau and Werlitzsch, both with a Teichstraße 3 —
255
- // so when the query names a locality the row whose own locality agrees is asked for FIRST. Only when no such
256
- // row exists does the postcode-only row answer, and then only if its locality does not name a different place
257
- // (see `#scopeContradicts` for what "different" tolerates): a query naming a third village under the postcode
258
- // falls through to the locality rung rather than answering the wrong rooftop.
253
+ // One postcode can cover several places with the same street and number,
254
+ // so a row whose locality agrees with the query is tried first.
255
+ // The postcode-only row answers only if `#scopeContradicts` accepts it.
259
256
  const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) : undefined
260
257
  const candidate = agreeing ?? this.#byPostcode!(postcode, streetNorm, number)
261
258
 
@@ -263,27 +260,20 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
263
260
  }
264
261
 
265
262
  if (!row && query.locality) {
266
- // FR extracts key arrondissement communes at the base city (both-sides fold, see the BAN
267
- // builder + stripArrondissement) — fold the probe too so "Paris 13e Arrondissement" and
268
- // "Paris" both hit. No-op for "us" extracts and every non-arrondissement commune.
269
263
  row = this.#byLocality!(this.#localityKey(query.locality), streetNorm, number)
270
264
  }
271
265
 
272
266
  if (!row && query.localityTail && this.#byLocalityTail) {
273
- // A Han key carries no `%` or `_`, so the tail is a literal suffix pattern.
267
+ // A Han key contains no `%` or `_`, so the pattern matches a literal suffix.
274
268
  row = this.#byLocalityTail(streetNorm, number, `%${query.localityTail}`)
275
269
  }
276
270
 
277
- // Bbox fall-through (#247): the point carries no postcode/locality of its own, but its coordinate falls
278
- // inside the resolved locality's box. Only reached when the scoped probes missed AND a bbox was supplied.
271
+ // The bounding box serves extracts whose points have no postcode or locality, such as OSM.
279
272
  if (!row && query.bbox) {
280
273
  const b = query.bbox
281
274
  const candidate = this.#byBbox!(streetNorm, number, b.minLat, b.maxLat, b.minLon, b.maxLon)
282
275
 
283
- // A register row that carries its own scope and was NOT found by the scoped rungs is a different address that
284
- // happens to share the street and number inside the box: `10 rue de la République, 75008 Paris` reached
285
- // Servon's `10 rue de la République` (postcode 77170) 26 km away this way, at rooftop tier and 1 m uncertainty.
286
- // The rung exists for points with no scope of their own; a point whose scope disagrees with the query is a miss.
276
+ // A row with its own scope that the scoped probes missed is a different address in the same box.
287
277
  row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : undefined
288
278
  }
289
279
 
@@ -291,9 +281,10 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
291
281
  }
292
282
 
293
283
  /**
294
- * The query's locality folded the way the extract's builder folded its `locality_norm`. FR extracts key
295
- * arrondissement communes at the base city (both-sides fold, see the BAN builder + stripArrondissement), so "Paris
296
- * 13e Arrondissement" and "Paris" both hit; a no-op for every other locale.
284
+ * Folds the query's locality the way the extract's builder folded `locality_norm`.
285
+ *
286
+ * FR extracts key arrondissements at the base city, so "Paris 13e Arrondissement"
287
+ * and "Paris" produce the same key.
297
288
  */
298
289
  #localityKey(locality: string): NameKey {
299
290
  if (this.#locale === "fr") return stripArrondissement(normalizeLocalityForKey(locality))
@@ -302,15 +293,11 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
302
293
  }
303
294
 
304
295
  /**
305
- * Whether a row's OWN postcode or locality names a different place than the query did. Absent scope on the row is not
306
- * a contradiction — it is the case the bbox rung was built for — and a rung that matched ON a field cannot contradict
307
- * it, so at the postcode rung only the locality can disagree and at the bbox rung either can.
296
+ * Returns whether the row's own postcode or locality differs from the query's.
308
297
  *
309
- * The locality is consulted only on an extract whose keys are full names (the constructor's `localityKeys`). Under
310
- * exact comparison against the US extract's abbreviated keys, the postcode rung refused `4900 Airport Pkwy, Addison
311
- * TX 75001`'s own rooftop row (`addi`) and `678 Depot St, North Easton, MA 02356`'s (`easton`), and both `pass` board
312
- * rows fell to interpolation 144–198 m away — invisible to a 1 km grade (#2155). `servon` against `paris` and
313
- * `werlitzsch` against `krensitz`, on the BAN and OSM extracts, are different places and still refuse.
298
+ * A row field that is empty never contradicts.
299
+ * The locality is compared only when the extract's `localityKeys` is `"full"`,
300
+ * because abbreviated keys would reject correct rows.
314
301
  */
315
302
  #scopeContradicts(row: AddressPointRow, query: { postcode?: string; locality?: string }): boolean {
316
303
  if (query.postcode && row.postcode && row.postcode.trim() !== query.postcode.trim()) return true
@@ -5,5 +5,5 @@
5
5
  */
6
6
 
7
7
  export * from "#address/point"
8
- export * from "#address/point-interpolation"
9
- export * from "#address/point-schema"
8
+ export * from "#address/point/interpolation"
9
+ export * from "#address/point/schema"