@mailwoman/resolver-wof-sqlite 9.2.0 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (493) hide show
  1. package/README.md +16 -19
  2. package/lib/address/index.ts +9 -0
  3. package/{address-point-interpolation.ts → lib/address/point-interpolation.ts} +25 -24
  4. package/{address-point-schema.ts → lib/address/point-schema.ts} +18 -4
  5. package/lib/address/point.ts +328 -0
  6. package/{ancestry-backfill.ts → lib/ancestry/backfill.ts} +14 -19
  7. package/{ancestry.ts → lib/ancestry/index.ts} +5 -4
  8. package/{build-candidate.ts → lib/build-candidate.ts} +99 -79
  9. package/{build-slim.ts → lib/build-slim.ts} +150 -165
  10. package/{candidate → lib/candidate}/alias-bags.ts +8 -6
  11. package/{candidate → lib/candidate}/ancestors-sidecar.ts +13 -15
  12. package/{candidate → lib/candidate}/country-display-names.ts +2 -2
  13. package/lib/candidate/extract-fold.ts +219 -0
  14. package/{candidate → lib/candidate}/name-roles.ts +17 -16
  15. package/{candidate → lib/candidate}/own-name.ts +2 -1
  16. package/{candidate → lib/candidate}/place-attrs.ts +3 -3
  17. package/{candidate-ancestors-schema.ts → lib/candidate-ancestors-schema.ts} +4 -4
  18. package/{candidate-fts.ts → lib/candidate-fts.ts} +3 -3
  19. package/{candidate-importance.ts → lib/candidate-importance.ts} +53 -57
  20. package/{candidate-lookup.ts → lib/candidate-lookup.ts} +84 -80
  21. package/{candidate-schema.ts → lib/candidate-schema.ts} +6 -6
  22. package/{candidate-scoring.ts → lib/candidate-scoring.ts} +15 -22
  23. package/{capital-schema.ts → lib/capital-schema.ts} +5 -6
  24. package/{capitals.ts → lib/capitals.ts} +2 -2
  25. package/{coincident-roles.ts → lib/coincident-roles.ts} +9 -13
  26. package/{convention.ts → lib/convention/index.ts} +3 -3
  27. package/{convention-schema.ts → lib/convention/schema.ts} +2 -2
  28. package/{coverage-manifest-schema.ts → lib/coverage-manifest-schema.ts} +10 -10
  29. package/{currency-backfill.ts → lib/currency-backfill.ts} +126 -41
  30. package/lib/env.ts +50 -0
  31. package/{exact-match.ts → lib/exact-match.ts} +23 -14
  32. package/{sharding.ts → lib/extracts.ts} +61 -58
  33. package/{fst-autocomplete.ts → lib/fst/autocomplete.ts} +4 -4
  34. package/{fst-builder.ts → lib/fst/builder.ts} +17 -16
  35. package/{fst-deserialize-web.ts → lib/fst/deserialize-web.ts} +27 -87
  36. package/lib/fst/format.ts +106 -0
  37. package/{fst-freshness.ts → lib/fst/freshness.ts} +40 -74
  38. package/lib/fst/index.ts +14 -0
  39. package/{fst-matcher.ts → lib/fst/matcher.ts} +1 -1
  40. package/{fst-serialize.ts → lib/fst/serialize.ts} +37 -115
  41. package/{fst-types.ts → lib/fst/types.ts} +4 -2
  42. package/{fts.ts → lib/fts/index.ts} +24 -19
  43. package/{fts-query.ts → lib/fts/query.ts} +1 -1
  44. package/{geonames-aliases.ts → lib/geonames/aliases.ts} +13 -13
  45. package/lib/geonames/index.ts +8 -0
  46. package/{geonames-postal.ts → lib/geonames/postal.ts} +14 -13
  47. package/{index.ts → lib/index.ts} +37 -37
  48. package/{interpolation.ts → lib/interpolation.ts} +28 -26
  49. package/{lookup.ts → lib/lookup.ts} +130 -142
  50. package/lib/nsul/index.ts +8 -0
  51. package/lib/nsul/lookup.ts +144 -0
  52. package/lib/nsul/schema.ts +130 -0
  53. package/{place-importance-schema.ts → lib/place-importance-schema.ts} +7 -16
  54. package/lib/poi/index.ts +8 -0
  55. package/{poi-lookup.ts → lib/poi/lookup.ts} +22 -29
  56. package/{poi-schema.ts → lib/poi/schema.ts} +3 -4
  57. package/{postal-city-alias-lookup.ts → lib/postal/city-alias-lookup.ts} +14 -20
  58. package/{postal-city-candidate-schema.ts → lib/postal/city-candidate-schema.ts} +2 -2
  59. package/lib/postal/index.ts +9 -0
  60. package/{postcode-point-lookup.ts → lib/postcode-point-lookup.ts} +17 -15
  61. package/{primary-preference.ts → lib/primary-preference.ts} +43 -14
  62. package/{proximity-rerank.ts → lib/proximity-rerank.ts} +2 -2
  63. package/{ranking-weights.ts → lib/ranking-weights.ts} +30 -4
  64. package/{region-keys.ts → lib/region-keys.ts} +9 -9
  65. package/{reverse.ts → lib/reverse.ts} +34 -47
  66. package/{schema.ts → lib/schema.ts} +10 -0
  67. package/{search-fetch.ts → lib/search-fetch.ts} +29 -29
  68. package/{sqlite-convention-source.ts → lib/sqlite-convention-source.ts} +9 -10
  69. package/{sqlite-utils.ts → lib/sqlite-utils.ts} +14 -19
  70. package/{street-centroid-schema.ts → lib/street/centroid-schema.ts} +5 -5
  71. package/{street-centroid.ts → lib/street/centroid.ts} +15 -22
  72. package/lib/street/index.ts +13 -0
  73. package/{street-morphology-fst-builder.ts → lib/street/morphology-fst-builder.ts} +24 -14
  74. package/{street-morphology-fst-loader.ts → lib/street/morphology-fst-loader.ts} +14 -12
  75. package/{street-name-lookup.ts → lib/street/name-lookup.ts} +14 -30
  76. package/{street-normalize.ts → lib/street/normalize.ts} +86 -20
  77. package/{street-segment-schema.ts → lib/street/segment-schema.ts} +7 -7
  78. package/{types.ts → lib/types.ts} +18 -12
  79. package/{unified-schema.ts → lib/unified-schema.ts} +20 -24
  80. package/lib/uprn/existence.ts +84 -0
  81. package/lib/uprn/index.ts +9 -0
  82. package/{uprn-lookup.ts → lib/uprn/lookup.ts} +14 -19
  83. package/{uprn-schema.ts → lib/uprn/schema.ts} +3 -3
  84. package/lib/weights-overlay-linker.ts +879 -0
  85. package/out/address/index.d.ts +9 -0
  86. package/out/address/index.d.ts.map +1 -0
  87. package/out/address/index.js +9 -0
  88. package/out/address/index.js.map +1 -0
  89. package/out/{address-point-interpolation.d.ts → address/point-interpolation.d.ts} +12 -11
  90. package/out/address/point-interpolation.d.ts.map +1 -0
  91. package/out/{address-point-interpolation.js → address/point-interpolation.js} +18 -18
  92. package/out/address/point-interpolation.js.map +1 -0
  93. package/out/{address-point-schema.d.ts → address/point-schema.d.ts} +16 -6
  94. package/out/address/point-schema.d.ts.map +1 -0
  95. package/out/{address-point-schema.js → address/point-schema.js} +7 -3
  96. package/out/address/point-schema.js.map +1 -0
  97. package/out/{address-point.d.ts → address/point.d.ts} +19 -9
  98. package/out/address/point.d.ts.map +1 -0
  99. package/out/address/point.js +222 -0
  100. package/out/address/point.js.map +1 -0
  101. package/out/{ancestry-backfill.d.ts → ancestry/backfill.d.ts} +7 -5
  102. package/out/ancestry/backfill.d.ts.map +1 -0
  103. package/out/{ancestry-backfill.js → ancestry/backfill.js} +12 -16
  104. package/out/ancestry/backfill.js.map +1 -0
  105. package/out/{ancestry.d.ts → ancestry/index.d.ts} +4 -3
  106. package/out/ancestry/index.d.ts.map +1 -0
  107. package/out/{ancestry.js → ancestry/index.js} +3 -2
  108. package/out/ancestry/index.js.map +1 -0
  109. package/out/build-candidate.d.ts +32 -24
  110. package/out/build-candidate.d.ts.map +1 -1
  111. package/out/build-candidate.js +60 -54
  112. package/out/build-candidate.js.map +1 -1
  113. package/out/build-slim.d.ts +4 -4
  114. package/out/build-slim.d.ts.map +1 -1
  115. package/out/build-slim.js +124 -139
  116. package/out/build-slim.js.map +1 -1
  117. package/out/candidate/alias-bags.d.ts +5 -3
  118. package/out/candidate/alias-bags.d.ts.map +1 -1
  119. package/out/candidate/alias-bags.js +2 -2
  120. package/out/candidate/alias-bags.js.map +1 -1
  121. package/out/candidate/ancestors-sidecar.d.ts +9 -10
  122. package/out/candidate/ancestors-sidecar.d.ts.map +1 -1
  123. package/out/candidate/ancestors-sidecar.js +7 -7
  124. package/out/candidate/ancestors-sidecar.js.map +1 -1
  125. package/out/candidate/country-display-names.d.ts +1 -1
  126. package/out/candidate/country-display-names.d.ts.map +1 -1
  127. package/out/candidate/country-display-names.js +1 -1
  128. package/out/candidate/country-display-names.js.map +1 -1
  129. package/out/candidate/extract-fold.d.ts +41 -0
  130. package/out/candidate/extract-fold.d.ts.map +1 -0
  131. package/out/candidate/extract-fold.js +153 -0
  132. package/out/candidate/extract-fold.js.map +1 -0
  133. package/out/candidate/name-roles.d.ts +13 -11
  134. package/out/candidate/name-roles.d.ts.map +1 -1
  135. package/out/candidate/name-roles.js +11 -10
  136. package/out/candidate/name-roles.js.map +1 -1
  137. package/out/candidate/own-name.d.ts +1 -1
  138. package/out/candidate/own-name.d.ts.map +1 -1
  139. package/out/candidate/own-name.js +2 -1
  140. package/out/candidate/own-name.js.map +1 -1
  141. package/out/candidate/place-attrs.d.ts +3 -3
  142. package/out/candidate/place-attrs.d.ts.map +1 -1
  143. package/out/candidate/place-attrs.js +1 -1
  144. package/out/candidate/place-attrs.js.map +1 -1
  145. package/out/candidate-ancestors-schema.d.ts +4 -4
  146. package/out/candidate-ancestors-schema.d.ts.map +1 -1
  147. package/out/candidate-ancestors-schema.js +2 -2
  148. package/out/candidate-ancestors-schema.js.map +1 -1
  149. package/out/candidate-fts.d.ts +3 -3
  150. package/out/candidate-fts.d.ts.map +1 -1
  151. package/out/candidate-fts.js +1 -1
  152. package/out/candidate-fts.js.map +1 -1
  153. package/out/candidate-importance.d.ts +16 -16
  154. package/out/candidate-importance.d.ts.map +1 -1
  155. package/out/candidate-importance.js +46 -51
  156. package/out/candidate-importance.js.map +1 -1
  157. package/out/candidate-lookup.d.ts +16 -14
  158. package/out/candidate-lookup.d.ts.map +1 -1
  159. package/out/candidate-lookup.js +67 -63
  160. package/out/candidate-lookup.js.map +1 -1
  161. package/out/candidate-schema.d.ts +6 -6
  162. package/out/candidate-schema.d.ts.map +1 -1
  163. package/out/candidate-schema.js +1 -1
  164. package/out/candidate-schema.js.map +1 -1
  165. package/out/candidate-scoring.d.ts +7 -7
  166. package/out/candidate-scoring.d.ts.map +1 -1
  167. package/out/candidate-scoring.js +10 -14
  168. package/out/candidate-scoring.js.map +1 -1
  169. package/out/capital-schema.d.ts +3 -3
  170. package/out/capital-schema.d.ts.map +1 -1
  171. package/out/capital-schema.js +2 -2
  172. package/out/capital-schema.js.map +1 -1
  173. package/out/capitals.d.ts +1 -1
  174. package/out/capitals.d.ts.map +1 -1
  175. package/out/capitals.js +2 -2
  176. package/out/capitals.js.map +1 -1
  177. package/out/coincident-roles.d.ts +7 -6
  178. package/out/coincident-roles.d.ts.map +1 -1
  179. package/out/coincident-roles.js +2 -4
  180. package/out/coincident-roles.js.map +1 -1
  181. package/out/{convention.d.ts → convention/index.d.ts} +4 -4
  182. package/out/convention/index.d.ts.map +1 -0
  183. package/out/{convention.js → convention/index.js} +2 -2
  184. package/out/convention/index.js.map +1 -0
  185. package/out/{convention-schema.d.ts → convention/schema.d.ts} +2 -2
  186. package/out/convention/schema.d.ts.map +1 -0
  187. package/out/{convention-schema.js → convention/schema.js} +3 -3
  188. package/out/convention/schema.js.map +1 -0
  189. package/out/coverage-manifest-schema.d.ts +8 -8
  190. package/out/coverage-manifest-schema.d.ts.map +1 -1
  191. package/out/coverage-manifest-schema.js +5 -4
  192. package/out/coverage-manifest-schema.js.map +1 -1
  193. package/out/currency-backfill.d.ts +51 -7
  194. package/out/currency-backfill.d.ts.map +1 -1
  195. package/out/currency-backfill.js +65 -49
  196. package/out/currency-backfill.js.map +1 -1
  197. package/out/env.d.ts +43 -0
  198. package/out/env.d.ts.map +1 -0
  199. package/out/env.js +48 -0
  200. package/out/env.js.map +1 -0
  201. package/out/exact-match.d.ts +5 -5
  202. package/out/exact-match.d.ts.map +1 -1
  203. package/out/exact-match.js +10 -11
  204. package/out/exact-match.js.map +1 -1
  205. package/out/extracts.d.ts +114 -0
  206. package/out/extracts.d.ts.map +1 -0
  207. package/out/{sharding.js → extracts.js} +38 -38
  208. package/out/extracts.js.map +1 -0
  209. package/out/{fst-autocomplete.d.ts → fst/autocomplete.d.ts} +2 -2
  210. package/out/fst/autocomplete.d.ts.map +1 -0
  211. package/out/{fst-autocomplete.js → fst/autocomplete.js} +3 -3
  212. package/out/fst/autocomplete.js.map +1 -0
  213. package/out/{fst-builder.d.ts → fst/builder.d.ts} +5 -5
  214. package/out/fst/builder.d.ts.map +1 -0
  215. package/out/{fst-builder.js → fst/builder.js} +14 -13
  216. package/out/fst/builder.js.map +1 -0
  217. package/out/{fst-deserialize-web.d.ts → fst/deserialize-web.d.ts} +3 -3
  218. package/out/fst/deserialize-web.d.ts.map +1 -0
  219. package/out/{fst-deserialize-web.js → fst/deserialize-web.js} +10 -74
  220. package/out/fst/deserialize-web.js.map +1 -0
  221. package/out/fst/format.d.ts +80 -0
  222. package/out/fst/format.d.ts.map +1 -0
  223. package/out/fst/format.js +91 -0
  224. package/out/fst/format.js.map +1 -0
  225. package/out/{fst-freshness.d.ts → fst/freshness.d.ts} +9 -13
  226. package/out/fst/freshness.d.ts.map +1 -0
  227. package/out/{fst-freshness.js → fst/freshness.js} +40 -73
  228. package/out/fst/freshness.js.map +1 -0
  229. package/out/fst/index.d.ts +14 -0
  230. package/out/fst/index.d.ts.map +1 -0
  231. package/out/fst/index.js +14 -0
  232. package/out/fst/index.js.map +1 -0
  233. package/out/{fst-matcher.d.ts → fst/matcher.d.ts} +2 -2
  234. package/out/fst/matcher.d.ts.map +1 -0
  235. package/out/{fst-matcher.js → fst/matcher.js} +1 -1
  236. package/out/fst/matcher.js.map +1 -0
  237. package/out/{fst-serialize.d.ts → fst/serialize.d.ts} +5 -10
  238. package/out/fst/serialize.d.ts.map +1 -0
  239. package/out/{fst-serialize.js → fst/serialize.js} +18 -98
  240. package/out/fst/serialize.js.map +1 -0
  241. package/out/{fst-types.d.ts → fst/types.d.ts} +4 -3
  242. package/out/fst/types.d.ts.map +1 -0
  243. package/out/{fst-types.js → fst/types.js} +1 -1
  244. package/out/fst/types.js.map +1 -0
  245. package/out/{fts.d.ts → fts/index.d.ts} +16 -9
  246. package/out/fts/index.d.ts.map +1 -0
  247. package/out/{fts.js → fts/index.js} +16 -11
  248. package/out/fts/index.js.map +1 -0
  249. package/out/{fts-query.d.ts → fts/query.d.ts} +2 -2
  250. package/out/fts/query.d.ts.map +1 -0
  251. package/out/{fts-query.js → fts/query.js} +1 -1
  252. package/out/fts/query.js.map +1 -0
  253. package/out/{geonames-aliases.d.ts → geonames/aliases.d.ts} +8 -6
  254. package/out/geonames/aliases.d.ts.map +1 -0
  255. package/out/{geonames-aliases.js → geonames/aliases.js} +7 -7
  256. package/out/geonames/aliases.js.map +1 -0
  257. package/out/geonames/index.d.ts +8 -0
  258. package/out/geonames/index.d.ts.map +1 -0
  259. package/out/geonames/index.js +8 -0
  260. package/out/geonames/index.js.map +1 -0
  261. package/out/{geonames-postal.d.ts → geonames/postal.d.ts} +10 -8
  262. package/out/geonames/postal.d.ts.map +1 -0
  263. package/out/{geonames-postal.js → geonames/postal.js} +9 -9
  264. package/out/geonames/postal.js.map +1 -0
  265. package/out/index.d.ts +32 -32
  266. package/out/index.d.ts.map +1 -1
  267. package/out/index.js +24 -24
  268. package/out/index.js.map +1 -1
  269. package/out/interpolation.d.ts +11 -10
  270. package/out/interpolation.d.ts.map +1 -1
  271. package/out/interpolation.js +20 -20
  272. package/out/interpolation.js.map +1 -1
  273. package/out/lookup.d.ts +22 -21
  274. package/out/lookup.d.ts.map +1 -1
  275. package/out/lookup.js +103 -113
  276. package/out/lookup.js.map +1 -1
  277. package/out/name-score.d.ts.map +1 -1
  278. package/out/name-score.js.map +1 -1
  279. package/out/nsul/index.d.ts +8 -0
  280. package/out/nsul/index.d.ts.map +1 -0
  281. package/out/nsul/index.js +8 -0
  282. package/out/nsul/index.js.map +1 -0
  283. package/out/nsul/lookup.d.ts +83 -0
  284. package/out/nsul/lookup.d.ts.map +1 -0
  285. package/out/nsul/lookup.js +80 -0
  286. package/out/nsul/lookup.js.map +1 -0
  287. package/out/nsul/schema.d.ts +100 -0
  288. package/out/nsul/schema.d.ts.map +1 -0
  289. package/out/nsul/schema.js +78 -0
  290. package/out/nsul/schema.js.map +1 -0
  291. package/out/place-importance-schema.d.ts +5 -6
  292. package/out/place-importance-schema.d.ts.map +1 -1
  293. package/out/place-importance-schema.js +3 -4
  294. package/out/place-importance-schema.js.map +1 -1
  295. package/out/poi/index.d.ts +8 -0
  296. package/out/poi/index.d.ts.map +1 -0
  297. package/out/poi/index.js +8 -0
  298. package/out/poi/index.js.map +1 -0
  299. package/out/{poi-lookup.d.ts → poi/lookup.d.ts} +8 -10
  300. package/out/poi/lookup.d.ts.map +1 -0
  301. package/out/{poi-lookup.js → poi/lookup.js} +14 -18
  302. package/out/poi/lookup.js.map +1 -0
  303. package/out/{poi-schema.d.ts → poi/schema.d.ts} +4 -4
  304. package/out/poi/schema.d.ts.map +1 -0
  305. package/out/{poi-schema.js → poi/schema.js} +1 -1
  306. package/out/poi/schema.js.map +1 -0
  307. package/out/polygon-schema.d.ts.map +1 -1
  308. package/out/polygon-schema.js.map +1 -1
  309. package/out/{postal-city-alias-lookup.d.ts → postal/city-alias-lookup.d.ts} +6 -5
  310. package/out/postal/city-alias-lookup.d.ts.map +1 -0
  311. package/out/{postal-city-alias-lookup.js → postal/city-alias-lookup.js} +11 -15
  312. package/out/postal/city-alias-lookup.js.map +1 -0
  313. package/out/{postal-city-alias-schema.d.ts → postal/city-alias-schema.d.ts} +1 -1
  314. package/out/{postal-city-alias-schema.d.ts.map → postal/city-alias-schema.d.ts.map} +1 -1
  315. package/out/{postal-city-alias-schema.js → postal/city-alias-schema.js} +1 -1
  316. package/out/{postal-city-alias-schema.js.map → postal/city-alias-schema.js.map} +1 -1
  317. package/out/{postal-city-candidate-schema.d.ts → postal/city-candidate-schema.d.ts} +3 -3
  318. package/out/{postal-city-candidate-schema.d.ts.map → postal/city-candidate-schema.d.ts.map} +1 -1
  319. package/out/{postal-city-candidate-schema.js → postal/city-candidate-schema.js} +2 -2
  320. package/out/{postal-city-candidate-schema.js.map → postal/city-candidate-schema.js.map} +1 -1
  321. package/out/postal/index.d.ts +9 -0
  322. package/out/postal/index.d.ts.map +1 -0
  323. package/out/postal/index.js +9 -0
  324. package/out/postal/index.js.map +1 -0
  325. package/out/postcode-point-lookup.d.ts +10 -10
  326. package/out/postcode-point-lookup.d.ts.map +1 -1
  327. package/out/postcode-point-lookup.js +13 -13
  328. package/out/postcode-point-lookup.js.map +1 -1
  329. package/out/primary-preference.d.ts +15 -7
  330. package/out/primary-preference.d.ts.map +1 -1
  331. package/out/primary-preference.js +28 -12
  332. package/out/primary-preference.js.map +1 -1
  333. package/out/proximity-rerank.d.ts +2 -2
  334. package/out/proximity-rerank.d.ts.map +1 -1
  335. package/out/proximity-rerank.js +2 -2
  336. package/out/proximity-rerank.js.map +1 -1
  337. package/out/ranking-weights.d.ts +15 -3
  338. package/out/ranking-weights.d.ts.map +1 -1
  339. package/out/ranking-weights.js +21 -3
  340. package/out/ranking-weights.js.map +1 -1
  341. package/out/region-keys.d.ts +8 -8
  342. package/out/region-keys.d.ts.map +1 -1
  343. package/out/region-keys.js +9 -9
  344. package/out/region-keys.js.map +1 -1
  345. package/out/reverse.d.ts +6 -6
  346. package/out/reverse.d.ts.map +1 -1
  347. package/out/reverse.js +20 -29
  348. package/out/reverse.js.map +1 -1
  349. package/out/schema.d.ts +9 -0
  350. package/out/schema.d.ts.map +1 -1
  351. package/out/schema.js.map +1 -1
  352. package/out/search-fetch.d.ts +12 -10
  353. package/out/search-fetch.d.ts.map +1 -1
  354. package/out/search-fetch.js +21 -20
  355. package/out/search-fetch.js.map +1 -1
  356. package/out/sqlite-convention-source.d.ts +7 -7
  357. package/out/sqlite-convention-source.d.ts.map +1 -1
  358. package/out/sqlite-convention-source.js +5 -5
  359. package/out/sqlite-convention-source.js.map +1 -1
  360. package/out/sqlite-utils.d.ts +8 -9
  361. package/out/sqlite-utils.d.ts.map +1 -1
  362. package/out/sqlite-utils.js +6 -10
  363. package/out/sqlite-utils.js.map +1 -1
  364. package/out/{street-centroid-schema.d.ts → street/centroid-schema.d.ts} +6 -6
  365. package/out/{street-centroid-schema.d.ts.map → street/centroid-schema.d.ts.map} +1 -1
  366. package/out/{street-centroid-schema.js → street/centroid-schema.js} +5 -5
  367. package/out/{street-centroid-schema.js.map → street/centroid-schema.js.map} +1 -1
  368. package/out/{street-centroid.d.ts → street/centroid.d.ts} +7 -7
  369. package/out/street/centroid.d.ts.map +1 -0
  370. package/out/{street-centroid.js → street/centroid.js} +13 -19
  371. package/out/street/centroid.js.map +1 -0
  372. package/out/street/index.d.ts +13 -0
  373. package/out/street/index.d.ts.map +1 -0
  374. package/out/street/index.js +13 -0
  375. package/out/street/index.js.map +1 -0
  376. package/out/{street-morphology-fst-builder.d.ts → street/morphology-fst-builder.d.ts} +4 -4
  377. package/out/street/morphology-fst-builder.d.ts.map +1 -0
  378. package/out/{street-morphology-fst-builder.js → street/morphology-fst-builder.js} +18 -13
  379. package/out/street/morphology-fst-builder.js.map +1 -0
  380. package/out/{street-morphology-fst-loader.d.ts → street/morphology-fst-loader.d.ts} +5 -5
  381. package/out/street/morphology-fst-loader.d.ts.map +1 -0
  382. package/out/{street-morphology-fst-loader.js → street/morphology-fst-loader.js} +11 -10
  383. package/out/street/morphology-fst-loader.js.map +1 -0
  384. package/out/{street-name-lookup.d.ts → street/name-lookup.d.ts} +4 -7
  385. package/out/street/name-lookup.d.ts.map +1 -0
  386. package/out/{street-name-lookup.js → street/name-lookup.js} +9 -23
  387. package/out/street/name-lookup.js.map +1 -0
  388. package/out/{street-normalize.d.ts → street/normalize.d.ts} +38 -15
  389. package/out/street/normalize.d.ts.map +1 -0
  390. package/out/{street-normalize.js → street/normalize.js} +73 -17
  391. package/out/street/normalize.js.map +1 -0
  392. package/out/{street-segment-schema.d.ts → street/segment-schema.d.ts} +8 -8
  393. package/out/{street-segment-schema.d.ts.map → street/segment-schema.d.ts.map} +1 -1
  394. package/out/{street-segment-schema.js → street/segment-schema.js} +4 -4
  395. package/out/{street-segment-schema.js.map → street/segment-schema.js.map} +1 -1
  396. package/out/types.d.ts +18 -12
  397. package/out/types.d.ts.map +1 -1
  398. package/out/types.js.map +1 -1
  399. package/out/unified-schema.d.ts +5 -4
  400. package/out/unified-schema.d.ts.map +1 -1
  401. package/out/unified-schema.js +15 -18
  402. package/out/unified-schema.js.map +1 -1
  403. package/out/uprn/existence.d.ts +53 -0
  404. package/out/uprn/existence.d.ts.map +1 -0
  405. package/out/uprn/existence.js +61 -0
  406. package/out/uprn/existence.js.map +1 -0
  407. package/out/uprn/index.d.ts +9 -0
  408. package/out/uprn/index.d.ts.map +1 -0
  409. package/out/uprn/index.js +9 -0
  410. package/out/uprn/index.js.map +1 -0
  411. package/out/{uprn-lookup.d.ts → uprn/lookup.d.ts} +4 -4
  412. package/out/uprn/lookup.d.ts.map +1 -0
  413. package/out/{uprn-lookup.js → uprn/lookup.js} +11 -14
  414. package/out/uprn/lookup.js.map +1 -0
  415. package/out/{uprn-schema.d.ts → uprn/schema.d.ts} +4 -4
  416. package/out/uprn/schema.d.ts.map +1 -0
  417. package/out/{uprn-schema.js → uprn/schema.js} +4 -4
  418. package/out/uprn/schema.js.map +1 -0
  419. package/out/weights-overlay-linker.d.ts +184 -26
  420. package/out/weights-overlay-linker.d.ts.map +1 -1
  421. package/out/weights-overlay-linker.js +339 -86
  422. package/out/weights-overlay-linker.js.map +1 -1
  423. package/package.json +465 -269
  424. package/address-point.ts +0 -218
  425. package/candidate/shard-fold.ts +0 -137
  426. package/out/address-point-interpolation.d.ts.map +0 -1
  427. package/out/address-point-interpolation.js.map +0 -1
  428. package/out/address-point-schema.d.ts.map +0 -1
  429. package/out/address-point-schema.js.map +0 -1
  430. package/out/address-point.d.ts.map +0 -1
  431. package/out/address-point.js +0 -140
  432. package/out/address-point.js.map +0 -1
  433. package/out/ancestry-backfill.d.ts.map +0 -1
  434. package/out/ancestry-backfill.js.map +0 -1
  435. package/out/ancestry.d.ts.map +0 -1
  436. package/out/ancestry.js.map +0 -1
  437. package/out/candidate/shard-fold.d.ts +0 -31
  438. package/out/candidate/shard-fold.d.ts.map +0 -1
  439. package/out/candidate/shard-fold.js +0 -104
  440. package/out/candidate/shard-fold.js.map +0 -1
  441. package/out/convention-schema.d.ts.map +0 -1
  442. package/out/convention-schema.js.map +0 -1
  443. package/out/convention.d.ts.map +0 -1
  444. package/out/convention.js.map +0 -1
  445. package/out/fst-autocomplete.d.ts.map +0 -1
  446. package/out/fst-autocomplete.js.map +0 -1
  447. package/out/fst-builder.d.ts.map +0 -1
  448. package/out/fst-builder.js.map +0 -1
  449. package/out/fst-deserialize-web.d.ts.map +0 -1
  450. package/out/fst-deserialize-web.js.map +0 -1
  451. package/out/fst-freshness.d.ts.map +0 -1
  452. package/out/fst-freshness.js.map +0 -1
  453. package/out/fst-matcher.d.ts.map +0 -1
  454. package/out/fst-matcher.js.map +0 -1
  455. package/out/fst-serialize.d.ts.map +0 -1
  456. package/out/fst-serialize.js.map +0 -1
  457. package/out/fst-types.d.ts.map +0 -1
  458. package/out/fst-types.js.map +0 -1
  459. package/out/fts-query.d.ts.map +0 -1
  460. package/out/fts-query.js.map +0 -1
  461. package/out/fts.d.ts.map +0 -1
  462. package/out/fts.js.map +0 -1
  463. package/out/geonames-aliases.d.ts.map +0 -1
  464. package/out/geonames-aliases.js.map +0 -1
  465. package/out/geonames-postal.d.ts.map +0 -1
  466. package/out/geonames-postal.js.map +0 -1
  467. package/out/poi-lookup.d.ts.map +0 -1
  468. package/out/poi-lookup.js.map +0 -1
  469. package/out/poi-schema.d.ts.map +0 -1
  470. package/out/poi-schema.js.map +0 -1
  471. package/out/postal-city-alias-lookup.d.ts.map +0 -1
  472. package/out/postal-city-alias-lookup.js.map +0 -1
  473. package/out/sharding.d.ts +0 -114
  474. package/out/sharding.d.ts.map +0 -1
  475. package/out/sharding.js.map +0 -1
  476. package/out/street-centroid.d.ts.map +0 -1
  477. package/out/street-centroid.js.map +0 -1
  478. package/out/street-morphology-fst-builder.d.ts.map +0 -1
  479. package/out/street-morphology-fst-builder.js.map +0 -1
  480. package/out/street-morphology-fst-loader.d.ts.map +0 -1
  481. package/out/street-morphology-fst-loader.js.map +0 -1
  482. package/out/street-name-lookup.d.ts.map +0 -1
  483. package/out/street-name-lookup.js.map +0 -1
  484. package/out/street-normalize.d.ts.map +0 -1
  485. package/out/street-normalize.js.map +0 -1
  486. package/out/uprn-lookup.d.ts.map +0 -1
  487. package/out/uprn-lookup.js.map +0 -1
  488. package/out/uprn-schema.d.ts.map +0 -1
  489. package/out/uprn-schema.js.map +0 -1
  490. package/weights-overlay-linker.ts +0 -377
  491. /package/{name-score.ts → lib/name-score.ts} +0 -0
  492. /package/{polygon-schema.ts → lib/polygon-schema.ts} +0 -0
  493. /package/{postal-city-alias-schema.ts → lib/postal/city-alias-schema.ts} +0 -0
package/README.md CHANGED
@@ -17,7 +17,7 @@ Requires Node 22+ for built-in `node:sqlite`.
17
17
  ```ts
18
18
  import { WOFSQLitePlaceLookup } from "@mailwoman/resolver-wof-sqlite"
19
19
 
20
- const lookup = new WOFSQLitePlaceLookup({
20
+ using lookup = new WOFSQLitePlaceLookup({
21
21
  databasePath: "/path/to/whosonfirst-data-admin-us-latest.db",
22
22
  buildFTS: true, // build the FTS5 index on first open (one-time cost)
23
23
  })
@@ -31,18 +31,16 @@ const candidates = await lookup.findPlace({
31
31
  for (const c of candidates) {
32
32
  console.log(c.id, c.name, c.country, c.lat, c.lon, "score:", c.score)
33
33
  }
34
-
35
- lookup.close()
36
34
  ```
37
35
 
38
- ## A shard that cannot answer says so on construction
36
+ ## A database that cannot answer says so on construction
39
37
 
40
- Shards are `ATTACH`ed by a schema name **derived from the filename**, and queries route to them by matching that name against the requested placetype — `postalcode_us` serves `postalcode`. Two ways that used to fail quietly, and both now throw when you build the lookup:
38
+ Databases are `ATTACH`ed by a schema name **derived from the filename**, and queries route to them by matching that name against the requested placetype — `postalcode_us` serves `postalcode`. Two ways that used to fail without logging, and both now throw when you build the lookup:
41
39
 
42
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".
43
- - The shard carries `spr` but no `place_search`. It routes, then dies mid-`SELECT`.
41
+ - The database carries `spr` but no `place_search`. It routes, then dies mid-`SELECT`.
44
42
 
45
- The predicate is the **table, not the filename**: a shard carrying `spr` is claiming to be a place shard, and every lookup path here reaches the FTS index. A relation-table shard like `postcode-locality-<cc>.db` carries no `spr`, never makes that claim, and is exempt — which is what keeps the documented default shard list working.
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.
46
44
 
47
45
  ```
48
46
  WOFSQLitePlaceLookup: …/postcode-ca-overture.db carries "spr" but no "place_search" table, so it
@@ -54,21 +52,21 @@ locality, region, county, country, venue), so it would never have been queried e
54
52
 
55
53
  The second sentence appears only when the name routes nowhere. It is the half that turns zero hits into a diagnosis.
56
54
 
57
- ## Multi-shard (admin + postcode in one connection)
55
+ ## Multiple databases (admin + postcode in one connection)
58
56
 
59
- Pass an array of paths to open multiple WOF shards on a single connection — each is opened as a
57
+ Pass an array of paths to open multiple WOF databases on a single connection — each is opened as a
60
58
  separate SQLite schema via `ATTACH DATABASE`. Schema names auto-derive from filenames
61
59
  (`whosonfirst-data-admin-us-latest.db` → `admin_us`, `whosonfirst-data-postalcode-us-latest.db` →
62
60
  `postalcode_us`). Queries route by `placetype` — a `postalcode` query goes to the
63
- `postalcode_us` shard automatically, everything else hits main.
61
+ `postalcode_us` database automatically, everything else hits main.
64
62
 
65
63
  ```ts
66
64
  const lookup = new WOFSQLitePlaceLookup({
67
65
  databasePath: ["/data/wof/whosonfirst-data-admin-us-latest.db", "/data/wof/whosonfirst-data-postalcode-us-latest.db"],
68
66
  })
69
67
 
70
- await lookup.findPlace({ text: "Springfield", placetype: "locality" }) // → admin shard
71
- await lookup.findPlace({ text: "62701", placetype: "postalcode" }) // → postcode shard
68
+ await lookup.findPlace({ text: "Springfield", placetype: "locality" }) // → admin extract
69
+ await lookup.findPlace({ text: "62701", placetype: "postalcode" }) // → postcode extract
72
70
  ```
73
71
 
74
72
  Override schema names or routing explicitly when needed:
@@ -79,13 +77,13 @@ new WOFSQLitePlaceLookup({
79
77
  })
80
78
  ```
81
79
 
82
- Cross-shard `UNION` queries are not supported in one `findPlace` call — BM25 scores aren't
80
+ Cross-database `UNION` queries are not supported in one `findPlace` call — BM25 scores aren't
83
81
  comparable across separately-indexed corpora. Issue two `findPlace` calls and merge in your
84
82
  caller if you need that.
85
83
 
86
84
  ## Getting the WOF SQLite distribution
87
85
 
88
- The Geocode Earth team mirrors WOF SQLite distributions at <https://data.geocode.earth/wof/dist/sqlite/>. The two relevant shards for v1:
86
+ The Geocode Earth team mirrors WOF SQLite distributions at <https://data.geocode.earth/wof/dist/sqlite/>. The two relevant distributions for v1:
89
87
 
90
88
  | Distribution | Size (bz2) | Use |
91
89
  | ---------------------------------------------- | ---------- | ----------------------------------------------------------------- |
@@ -102,7 +100,7 @@ bunzip2 whosonfirst-data-admin-us-latest.db.bz2
102
100
 
103
101
  Upstream WOF SQLite distributions ship a `places` table but **not** an FTS5 index. The resolver needs FTS5 to do fast prefix + token-bag matching. Two options:
104
102
 
105
- 1. **`buildFTS: true` on construction** — builds the index lazily on first open. Cost is one-time but expensive (~minutes on the full US admin shard). Use for prototyping.
103
+ 1. **`buildFTS: true` on construction** — builds the index lazily on first open. Cost is one-time but expensive (~minutes on the full US admin database). Use for prototyping.
106
104
  2. **Pre-build the index with `mailwoman gazetteer build fts`** — ship the DB with the index included so first-open is fast. Recommended for production.
107
105
 
108
106
  ### `mailwoman gazetteer build fts`
@@ -170,12 +168,11 @@ You can also build the index programmatically via the package's `./fts` subpath:
170
168
  import { DatabaseSync } from "node:sqlite"
171
169
  import { buildPlaceSearchFts } from "@mailwoman/resolver-wof-sqlite/fts"
172
170
 
173
- const db = new DatabaseSync("/path/to/wof.db")
171
+ using db = new DatabaseSync("/path/to/wof.db")
174
172
  const { created, indexedRows, durationMs } = buildPlaceSearchFts(db, {
175
173
  drop: false,
176
174
  onProgress: (phase, detail) => console.log(phase, detail),
177
175
  })
178
- db.close()
179
176
  ```
180
177
 
181
178
  ## Ranking
@@ -261,11 +258,11 @@ This package itself is AGPL-3.0; the WOF data it indexes is CC-BY 4.0. The two l
261
258
 
262
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.
263
260
 
264
- 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 shard).
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).
265
262
 
266
263
  ## Concurrency model
267
264
 
268
- This package opens a single `node:sqlite` connection per `WOFSQLitePlaceLookup` instance. SQLite is single-writer / many-reader; the Kysely wrapper around the connection serializes all queries through a mutex. For high-concurrency HTTP servers, instantiate one resolver per request handler or per pool slot — sharing a single instance across concurrent requests is fine (queries queue) but won't parallelize across cores.
265
+ This package opens a single `node:sqlite` connection per `WOFSQLitePlaceLookup` instance. SQLite is single-writer / multiple-reader; the Kysely wrapper around the connection serializes all queries through a mutex. For high-concurrency HTTP servers, instantiate one resolver per request handler or per pool slot — sharing a single instance across concurrent requests is safe (queries queue) but won't parallelize across cores.
269
266
 
270
267
  ## License
271
268
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "#address/point"
8
+ export * from "#address/point-interpolation"
9
+ export * from "#address/point-schema"
@@ -6,13 +6,13 @@
6
6
  * Address-point interpolation — "Method 2" of the resolution ladder (#483, Phase 1 of
7
7
  * `docs/articles/plan/2026-06-11-resolution-ladder.md`): when the exact address-point tier (#476)
8
8
  * misses a house number, bracket the number with REAL neighbor points on the same street from the
9
- * same #476 shard 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 gate
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
11
  * miss; TIGER range interpolation (`StreetInterpolator`) demotes to the fallback for streets too
12
12
  * sparse to bracket.
13
13
  *
14
14
  * Matching key is `street_key` — THE shared normalizer plus the route fold
15
- * (`canonicalizeRouteKey`), identical at build time (`scripts/build-address-point-shard.ts`) and
15
+ * (`canonicalizeRouteKey`), identical at build time (`scripts/build-address-point-extract.ts`) and
16
16
  * query time, by construction. Scope is postcode-first like the segment tier; a query without a
17
17
  * postcode goes straight to the fallback (which carries its own statewide-ambiguity abstention).
18
18
  *
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * - Neighbor candidates NEVER include the queried number itself (any unit/duplicate row of it) — in
22
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 shard non-circular by construction.
23
+ * this is what makes grading against the same extract non-circular by construction.
24
24
  * - Both-sided bracket (`bracket: "both"`): linear interpolation between the nearest known number
25
25
  * below and above; `uncertaintyM` = half the distance between them.
26
26
  * - Single-sided (`bracket: "single"`): linear extrapolation along the two nearest known numbers on
@@ -33,15 +33,14 @@
33
33
  * Standalone like the segment tier — core wiring rides the Phase 2 ordered `spatialTiers` list.
34
34
  */
35
35
 
36
- import { DatabaseSync } from "node:sqlite"
37
-
38
- import type { InterpolationLookup } from "@mailwoman/resolver"
36
+ import type { InterpolationLookup } from "@mailwoman/core/resolver"
39
37
  import { haversineKm } from "@mailwoman/spatial"
38
+ import { DatabaseClient } from "@mailwoman/sqlite/client"
40
39
 
41
- import type { InterpolatedHit, InterpolationQuery, StreetInterpolator } from "./interpolation.ts"
42
- import { hasTable, prepareAll, type PreparedAll } from "./sqlite-utils.ts"
43
- import { canonicalizeRouteKey, type RouteKey, streetKeyVariants } from "./street-normalize.ts"
44
-
40
+ import type { AddressPointDatabase } from "#address/point-schema"
41
+ import type { InterpolatedHit, InterpolationQuery, StreetInterpolator } from "#interpolation"
42
+ import { hasTable, prepareAll, type PreparedAll } from "#sqlite-utils"
43
+ import { canonicalizeRouteKey, type RouteKey, streetKeyVariants } from "#street/normalize"
45
44
  /**
46
45
  * Extrapolation cap for a single-sided bracket: at most one pair-span beyond the nearest known point (`t = 2`). Past
47
46
  * it, the two-point line carries no evidence about the query number.
@@ -67,26 +66,30 @@ interface NumberAnchor {
67
66
  release: string
68
67
  }
69
68
 
70
- export class AddressPointInterpolator implements InterpolationLookup {
71
- readonly #db: DatabaseSync
72
- readonly #ownsDB: boolean
69
+ export class AddressPointInterpolator<
70
+ DB extends AddressPointDatabase = AddressPointDatabase,
71
+ > implements InterpolationLookup {
72
+ readonly #db: DatabaseClient<DB>
73
+ /**
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.
76
+ */
77
+ readonly #resources = new DisposableStack()
73
78
  readonly #fallback: StreetInterpolator | undefined
74
79
  readonly #byPostcode: PreparedAll<[postcode: string, street: RouteKey, number: number], PointRow> | undefined
75
80
 
76
- constructor(opts: { dbPath?: string; database?: DatabaseSync; fallback?: StreetInterpolator }) {
81
+ constructor(opts: { dbPath?: string; database?: DatabaseClient<DB>; fallback?: StreetInterpolator }) {
77
82
  if (opts.database) {
78
83
  this.#db = opts.database
79
- this.#ownsDB = false
80
84
  } else if (opts.dbPath) {
81
- this.#db = new DatabaseSync(opts.dbPath, { readOnly: true })
82
- this.#ownsDB = true
85
+ this.#db = this.#resources.use(new DatabaseClient<DB>(opts.dbPath, { readOnly: true }))
83
86
  } else {
84
87
  throw new Error("AddressPointInterpolator: one of dbPath or database is required")
85
88
  }
86
89
 
87
90
  this.#fallback = opts.fallback
88
91
 
89
- // Degrade gracefully on an empty/tableless shard (#568): with no `address_point` table this tier
92
+ // Degrade gracefully on an empty/tableless extract (#568): with no `address_point` table this tier
90
93
  // is skipped, deferring to the segment fallback rather than crashing at construction.
91
94
  if (hasTable(this.#db, "address_point")) {
92
95
  // Strictly-numeric neighbor numbers on the route-folded street key within the ZIP. The
@@ -108,7 +111,7 @@ export class AddressPointInterpolator implements InterpolationLookup {
108
111
  if (!/^\d+$/.test(numberRaw)) return null
109
112
  const n = Number(numberRaw)
110
113
 
111
- // No own table (empty shard) or no postcode → defer to the segment fallback rather than query.
114
+ // No own table (empty extract) or no postcode → defer to the segment fallback rather than query.
112
115
  if (!this.#byPostcode || !query.postcode) return this.#fallback?.find(query) ?? null
113
116
 
114
117
  // Key-variant ladder (see `streetKeyVariants`) — same probe order as the exact-point reader.
@@ -127,10 +130,8 @@ export class AddressPointInterpolator implements InterpolationLookup {
127
130
  return hit ?? this.#fallback?.find(query) ?? null
128
131
  }
129
132
 
130
- close(): void {
131
- if (this.#ownsDB) {
132
- this.#db.close()
133
- }
133
+ [Symbol.dispose](): void {
134
+ this.#resources[Symbol.dispose]()
134
135
  }
135
136
  }
136
137
 
@@ -3,8 +3,8 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Typed schema for the SITUS / rooftop ADDRESS-POINT shards (`address-points-<cc>-<slug>.db`, built
7
- * by `scripts/build-address-point-shard.ts` — the #476/#567 national rooftop tier behind the
6
+ * Typed schema for the SITUS / rooftop ADDRESS-POINT extracts (`address-points-<cc>-<slug>.db`, built
7
+ * by `scripts/build-address-point-extract.ts` — the #476/#567 national rooftop tier behind the
8
8
  * demo's "type any US address, get the building"). Single source of truth for the columns shared
9
9
  * by the BUILDER and the READER ({@link AddressPointSqliteLookup}), so a column rename in one is a
10
10
  * compile error in the other.
@@ -18,7 +18,7 @@
18
18
 
19
19
  import type { Kysely } from "kysely"
20
20
 
21
- import type { NameKey, RouteKey, StreetKey } from "./street-normalize.ts"
21
+ import type { NameKey, RouteKey, StreetKey } from "#street/normalize"
22
22
 
23
23
  /**
24
24
  * One rooftop address point. `(street_norm, number)` within a `postcode` (preferred) or `locality_norm` scope is the
@@ -59,6 +59,16 @@ export interface AddressPointTable {
59
59
  * The pinned data release the point was ingested from.
60
60
  */
61
61
  release: string
62
+ /**
63
+ * The source register's stable administrative key for the point's commune or municipality — BAN's `code_insee`. A
64
+ * display name (`locality_norm`) is not a key; the coverage basis is computed per THIS.
65
+ */
66
+ admin_code: string | null
67
+ /**
68
+ * The register's own certification flag for the point (BAN `certification_commune`: 1 certified by the commune, 0
69
+ * not), or null for a source that states none. A basis is never inferred from a share of these.
70
+ */
71
+ certified: number | null
62
72
  }
63
73
 
64
74
  /**
@@ -71,7 +81,7 @@ export interface AddressPointDatabase {
71
81
  /**
72
82
  * The slice of a Kysely handle the `address_point` DDL touches — the parameter type its builders take.
73
83
  *
74
- * Kysely is invariant in its schema parameter (the incompatibility is in `transaction()`), so a shard that EXTENDS
84
+ * Kysely is invariant in its schema parameter (the incompatibility is in `transaction()`), so a extract that EXTENDS
75
85
  * `AddressPointTable` — OSM adds `h3_cell` — cannot pass its own handle to a `Kysely<AddressPointDatabase>` parameter.
76
86
  * Naming only `schema` lets it, and the DDL below needs nothing else.
77
87
  */
@@ -93,6 +103,8 @@ export const ADDRESS_POINT_COLUMNS = [
93
103
  "lon",
94
104
  "source",
95
105
  "release",
106
+ "admin_code",
107
+ "certified",
96
108
  ] as const
97
109
 
98
110
  /**
@@ -113,6 +125,8 @@ export async function createAddressPointTable(db: AddressPointSchemaHandle): Pro
113
125
  .addColumn("lon", "real", (c) => c.notNull())
114
126
  .addColumn("source", "text", (c) => c.notNull())
115
127
  .addColumn("release", "text", (c) => c.notNull())
128
+ .addColumn("admin_code", "text")
129
+ .addColumn("certified", "integer")
116
130
  .execute()
117
131
  }
118
132
 
@@ -0,0 +1,328 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
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.
16
+ */
17
+
18
+ import type { AddressPointHit, AddressPointLookup } from "@mailwoman/core/resolver"
19
+ import { DatabaseClient } from "@mailwoman/sqlite/client"
20
+
21
+ import type { AddressPointDatabase, AddressPointTable } from "#address/point-schema"
22
+ import { hasTable, prepareGet, type PreparedGet } from "#sqlite-utils"
23
+ import {
24
+ normalizeHouseNumberForKey,
25
+ normalizeLocalityForKey,
26
+ normalizeLocalityForKeyLocale,
27
+ type NameKey,
28
+ type StreetKey,
29
+ streetKeyVariants,
30
+ streetLocaleForSurface,
31
+ stripArrondissement,
32
+ type StreetLocale,
33
+ } from "#street/normalize"
34
+
35
+ /**
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).
38
+ */
39
+ type AddressPointRow = Pick<AddressPointTable, "lat" | "lon" | "source" | "release" | "locality_norm" | "postcode">
40
+
41
+ /**
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.
44
+ */
45
+ const SELECT_COLS = "lat, lon, source, release, locality_norm, postcode"
46
+
47
+ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressPointDatabase>
48
+ implements AddressPointLookup, Disposable
49
+ {
50
+ readonly #db: DatabaseClient<DB>
51
+ readonly #locale: StreetLocale
52
+ readonly #localityKeys: "full" | "abbreviated"
53
+ readonly #byPostcode: PreparedGet<[postcode: string, street: StreetKey, number: string], AddressPointRow> | undefined
54
+ readonly #byPostcodeLocality:
55
+ | PreparedGet<[postcode: string, locality: NameKey, street: StreetKey, number: string], AddressPointRow>
56
+ | undefined
57
+ readonly #byLocality: PreparedGet<[locality: NameKey, street: StreetKey, number: string], AddressPointRow> | undefined
58
+ /**
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
+ */
62
+ readonly #byLocalityTail:
63
+ | PreparedGet<[street: StreetKey, number: string, tailPattern: string], AddressPointRow>
64
+ | undefined
65
+ readonly #byBbox:
66
+ | PreparedGet<
67
+ [street: StreetKey, number: string, minLat: number, maxLat: number, minLon: number, maxLon: number],
68
+ AddressPointRow
69
+ >
70
+ | undefined
71
+
72
+ /**
73
+ * @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.
82
+ */
83
+ constructor(dbPath: string, opts: { streetLocale?: StreetLocale; localityKeys?: "full" | "abbreviated" } = {}) {
84
+ this.#db = new DatabaseClient<DB>(dbPath, { readOnly: true })
85
+ this.#locale = opts.streetLocale ?? "us"
86
+ this.#localityKeys = opts.localityKeys ?? (this.#locale === "us" ? "abbreviated" : "full")
87
+
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).
90
+ if (hasTable(this.#db, "address_point")) {
91
+ this.#byPostcode = prepareGet(
92
+ this.#db,
93
+ `SELECT ${SELECT_COLS} FROM address_point
94
+ WHERE postcode = ? AND street_norm = ? AND number = ? LIMIT 1`
95
+ )
96
+
97
+ this.#byPostcodeLocality = prepareGet(
98
+ this.#db,
99
+ `SELECT ${SELECT_COLS} FROM address_point
100
+ WHERE postcode = ? AND locality_norm = ? AND street_norm = ? AND number = ? LIMIT 1`
101
+ )
102
+
103
+ this.#byLocality = prepareGet(
104
+ this.#db,
105
+ `SELECT ${SELECT_COLS} FROM address_point
106
+ WHERE locality_norm = ? AND street_norm = ? AND number = ? LIMIT 1`
107
+ )
108
+
109
+ this.#byBbox = prepareGet(
110
+ this.#db,
111
+ `SELECT ${SELECT_COLS} FROM address_point
112
+ WHERE street_norm = ? AND number = ? AND lat BETWEEN ? AND ? AND lon BETWEEN ? AND ? LIMIT 1`
113
+ )
114
+
115
+ this.#byLocalityTail = prepareGet(
116
+ this.#db,
117
+ `SELECT ${SELECT_COLS} FROM address_point
118
+ WHERE street_norm = ? AND number = ? AND locality_norm LIKE ? LIMIT 1`
119
+ )
120
+ }
121
+ }
122
+
123
+ find(query: {
124
+ street: string
125
+ number: string
126
+ postcode?: string
127
+ locality?: string
128
+ region?: string
129
+ subregion?: string
130
+ bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
131
+ }): AddressPointHit | null {
132
+ if (!this.#byPostcode || !this.#byLocality || !this.#byBbox) return null
133
+ const number = normalizeHouseNumberForKey(query.number, this.#locale)
134
+
135
+ if (!number) return null
136
+
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.
141
+ const scoped =
142
+ this.#locale === "zh" && !query.locality && query.subregion
143
+ ? query.region
144
+ ? { ...query, locality: `${query.region}${query.subregion}` }
145
+ : { ...query, localityTail: normalizeLocalityForKeyLocale(query.subregion, "zh") }
146
+ : query
147
+
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.
151
+ let row: AddressPointRow | undefined
152
+
153
+ for (const streetNorm of streetKeyVariants(query.street, streetLocaleForSurface(query.street, this.#locale))) {
154
+ row = this.#findForKey(streetNorm, number, scoped)
155
+
156
+ if (row) break
157
+ }
158
+
159
+ if (!row) return null
160
+
161
+ return {
162
+ lat: row.lat,
163
+ lon: row.lon,
164
+ source: row.source,
165
+ release: row.release,
166
+ ...(row.locality_norm ? { localityNorm: row.locality_norm } : {}),
167
+ ...(row.postcode ? { postcode: row.postcode } : {}),
168
+ }
169
+ }
170
+
171
+ /**
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.
174
+ */
175
+ #findForKey(
176
+ streetNorm: StreetKey,
177
+ number: string,
178
+ query: {
179
+ street: string
180
+ number: string
181
+ postcode?: string
182
+ locality?: string
183
+ localityTail?: NameKey
184
+ bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
185
+ }
186
+ ) {
187
+ let row = this.#probe(streetNorm, number, query)
188
+
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.
193
+ if (!row) {
194
+ const low = /^(\d+[a-z]?)-\d+[a-z]?$/.exec(number)?.[1]
195
+
196
+ if (low) {
197
+ row = this.#probe(streetNorm, low, query)
198
+ }
199
+ }
200
+
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).
207
+ if (!row) {
208
+ const joined = /^(\d+)\s+([a-z])$/.exec(number)
209
+ const spaced = /^(\d+)([a-z])$/.exec(number)
210
+
211
+ if (joined) {
212
+ row = this.#probe(streetNorm, `${joined[1]}${joined[2]}`, query) ?? this.#probe(streetNorm, joined[1]!, query)
213
+ } else if (spaced) {
214
+ row = this.#probe(streetNorm, `${spaced[1]} ${spaced[2]}`, query) ?? this.#probe(streetNorm, spaced[1]!, query)
215
+ }
216
+ }
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.
223
+ if (!row && this.#locale === "zh") {
224
+ const base = /^(\d+)(?:[之附]\d+)+$/u.exec(number)?.[1]
225
+
226
+ if (base) {
227
+ row = this.#probe(streetNorm, base, query)
228
+ }
229
+ }
230
+
231
+ return row
232
+ }
233
+
234
+ /**
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.
237
+ */
238
+ #probe(
239
+ streetNorm: StreetKey,
240
+ number: string,
241
+ query: {
242
+ postcode?: string
243
+ locality?: string
244
+ localityTail?: NameKey
245
+ bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
246
+ }
247
+ ): AddressPointRow | undefined {
248
+ let row: AddressPointRow | undefined
249
+
250
+ if (query.postcode) {
251
+ const postcode = query.postcode.trim()
252
+ const localityKey = query.locality ? this.#localityKey(query.locality) : undefined
253
+
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.
259
+ const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) : undefined
260
+ const candidate = agreeing ?? this.#byPostcode!(postcode, streetNorm, number)
261
+
262
+ row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : undefined
263
+ }
264
+
265
+ 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
+ row = this.#byLocality!(this.#localityKey(query.locality), streetNorm, number)
270
+ }
271
+
272
+ if (!row && query.localityTail && this.#byLocalityTail) {
273
+ // A Han key carries no `%` or `_`, so the tail is a literal suffix pattern.
274
+ row = this.#byLocalityTail(streetNorm, number, `%${query.localityTail}`)
275
+ }
276
+
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.
279
+ if (!row && query.bbox) {
280
+ const b = query.bbox
281
+ const candidate = this.#byBbox!(streetNorm, number, b.minLat, b.maxLat, b.minLon, b.maxLon)
282
+
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.
287
+ row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : undefined
288
+ }
289
+
290
+ return row
291
+ }
292
+
293
+ /**
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.
297
+ */
298
+ #localityKey(locality: string): NameKey {
299
+ if (this.#locale === "fr") return stripArrondissement(normalizeLocalityForKey(locality))
300
+
301
+ return normalizeLocalityForKeyLocale(locality, this.#locale)
302
+ }
303
+
304
+ /**
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.
308
+ *
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.
314
+ */
315
+ #scopeContradicts(row: AddressPointRow, query: { postcode?: string; locality?: string }): boolean {
316
+ if (query.postcode && row.postcode && row.postcode.trim() !== query.postcode.trim()) return true
317
+
318
+ if (this.#localityKeys === "full" && query.locality && row.locality_norm) {
319
+ return row.locality_norm !== this.#localityKey(query.locality)
320
+ }
321
+
322
+ return false
323
+ }
324
+
325
+ [Symbol.dispose](): void {
326
+ this.#db[Symbol.dispose]()
327
+ }
328
+ }