@mailwoman/resolver-wof-sqlite 9.1.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (501) hide show
  1. package/README.md +36 -20
  2. package/lib/address/index.ts +9 -0
  3. package/{address-point-interpolation.ts → lib/address/point-interpolation.ts} +40 -28
  4. package/{address-point-schema.ts → lib/address/point-schema.ts} +34 -8
  5. package/lib/address/point.ts +280 -0
  6. package/{ancestry-backfill.ts → lib/ancestry/backfill.ts} +14 -19
  7. package/{ancestry.ts → lib/ancestry/index.ts} +13 -8
  8. package/{build-candidate.ts → lib/build-candidate.ts} +243 -199
  9. package/{build-slim.ts → lib/build-slim.ts} +153 -168
  10. package/lib/candidate/alias-bags.ts +56 -0
  11. package/lib/candidate/ancestors-sidecar.ts +204 -0
  12. package/lib/candidate/country-display-names.ts +79 -0
  13. package/lib/candidate/extract-fold.ts +138 -0
  14. package/lib/candidate/name-roles.ts +238 -0
  15. package/lib/candidate/own-name.ts +146 -0
  16. package/lib/candidate/place-attrs.ts +44 -0
  17. package/lib/candidate-ancestors-schema.ts +195 -0
  18. package/{candidate-fts.ts → lib/candidate-fts.ts} +7 -5
  19. package/{candidate-importance.ts → lib/candidate-importance.ts} +54 -57
  20. package/lib/candidate-lookup.ts +976 -0
  21. package/{candidate-schema.ts → lib/candidate-schema.ts} +34 -6
  22. package/lib/candidate-scoring.ts +261 -0
  23. package/lib/capital-schema.ts +89 -0
  24. package/lib/capitals.ts +148 -0
  25. package/{coincident-roles.ts → lib/coincident-roles.ts} +75 -19
  26. package/{convention.ts → lib/convention/index.ts} +5 -5
  27. package/lib/convention/schema.ts +72 -0
  28. package/{coverage-manifest-schema.ts → lib/coverage-manifest-schema.ts} +15 -16
  29. package/lib/currency-backfill.ts +334 -0
  30. package/lib/env.ts +50 -0
  31. package/lib/exact-match.ts +113 -0
  32. package/{sharding.ts → lib/extracts.ts} +63 -60
  33. package/lib/fst/autocomplete.ts +176 -0
  34. package/{fst-builder.ts → lib/fst/builder.ts} +30 -27
  35. package/{fst-deserialize-web.ts → lib/fst/deserialize-web.ts} +27 -87
  36. package/lib/fst/format.ts +106 -0
  37. package/{fst-freshness.ts → lib/fst/freshness.ts} +42 -75
  38. package/lib/fst/index.ts +14 -0
  39. package/{fst-matcher.ts → lib/fst/matcher.ts} +1 -1
  40. package/{fst-serialize.ts → lib/fst/serialize.ts} +37 -115
  41. package/{fst-types.ts → lib/fst/types.ts} +4 -2
  42. package/{fts.ts → lib/fts/index.ts} +28 -23
  43. package/{fts-query.ts → lib/fts/query.ts} +2 -2
  44. package/{geonames-aliases.ts → lib/geonames/aliases.ts} +13 -13
  45. package/lib/geonames/index.ts +8 -0
  46. package/{geonames-postal.ts → lib/geonames/postal.ts} +16 -15
  47. package/{index.ts → lib/index.ts} +52 -48
  48. package/{interpolation.ts → lib/interpolation.ts} +137 -41
  49. package/lib/lookup.ts +861 -0
  50. package/{name-score.ts → lib/name-score.ts} +6 -4
  51. package/lib/nsul/index.ts +8 -0
  52. package/lib/nsul/lookup.ts +144 -0
  53. package/lib/nsul/schema.ts +130 -0
  54. package/{place-importance-schema.ts → lib/place-importance-schema.ts} +70 -22
  55. package/lib/poi/index.ts +8 -0
  56. package/{poi-lookup.ts → lib/poi/lookup.ts} +32 -39
  57. package/{poi-schema.ts → lib/poi/schema.ts} +10 -6
  58. package/lib/polygon-schema.ts +47 -0
  59. package/{postal-city-alias-lookup.ts → lib/postal/city-alias-lookup.ts} +15 -21
  60. package/{postal-city-candidate-schema.ts → lib/postal/city-candidate-schema.ts} +4 -2
  61. package/lib/postal/index.ts +9 -0
  62. package/{postcode-point-lookup.ts → lib/postcode-point-lookup.ts} +17 -15
  63. package/lib/primary-preference.ts +226 -0
  64. package/lib/proximity-rerank.ts +120 -0
  65. package/{ranking-weights.ts → lib/ranking-weights.ts} +30 -4
  66. package/lib/region-keys.ts +144 -0
  67. package/{reverse.ts → lib/reverse.ts} +45 -56
  68. package/{schema.ts → lib/schema.ts} +11 -1
  69. package/lib/search-fetch.ts +257 -0
  70. package/{sqlite-convention-source.ts → lib/sqlite-convention-source.ts} +9 -10
  71. package/lib/sqlite-utils.ts +83 -0
  72. package/{street-centroid-schema.ts → lib/street/centroid-schema.ts} +12 -6
  73. package/{street-centroid.ts → lib/street/centroid.ts} +26 -28
  74. package/lib/street/index.ts +13 -0
  75. package/{street-morphology-fst-builder.ts → lib/street/morphology-fst-builder.ts} +24 -14
  76. package/{street-morphology-fst-loader.ts → lib/street/morphology-fst-loader.ts} +14 -12
  77. package/{street-name-lookup.ts → lib/street/name-lookup.ts} +14 -30
  78. package/lib/street/normalize.ts +572 -0
  79. package/{street-segment-schema.ts → lib/street/segment-schema.ts} +13 -8
  80. package/{types.ts → lib/types.ts} +49 -9
  81. package/{unified-schema.ts → lib/unified-schema.ts} +21 -25
  82. package/lib/uprn/existence.ts +84 -0
  83. package/lib/uprn/index.ts +9 -0
  84. package/lib/uprn/lookup.ts +206 -0
  85. package/lib/uprn/schema.ts +124 -0
  86. package/lib/weights-overlay-linker.ts +878 -0
  87. package/out/address/index.d.ts +9 -0
  88. package/out/address/index.d.ts.map +1 -0
  89. package/out/address/index.js +9 -0
  90. package/out/address/index.js.map +1 -0
  91. package/out/{address-point-interpolation.d.ts → address/point-interpolation.d.ts} +11 -10
  92. package/out/address/point-interpolation.d.ts.map +1 -0
  93. package/out/{address-point-interpolation.js → address/point-interpolation.js} +29 -23
  94. package/out/address/point-interpolation.js.map +1 -0
  95. package/out/{address-point-schema.d.ts → address/point-schema.d.ts} +30 -10
  96. package/out/address/point-schema.d.ts.map +1 -0
  97. package/out/{address-point-schema.js → address/point-schema.js} +7 -3
  98. package/out/address/point-schema.js.map +1 -0
  99. package/out/{address-point.d.ts → address/point.d.ts} +16 -8
  100. package/out/address/point.d.ts.map +1 -0
  101. package/out/address/point.js +191 -0
  102. package/out/address/point.js.map +1 -0
  103. package/out/{ancestry-backfill.d.ts → ancestry/backfill.d.ts} +7 -5
  104. package/out/ancestry/backfill.d.ts.map +1 -0
  105. package/out/{ancestry-backfill.js → ancestry/backfill.js} +12 -16
  106. package/out/ancestry/backfill.js.map +1 -0
  107. package/out/{ancestry.d.ts → ancestry/index.d.ts} +6 -5
  108. package/out/ancestry/index.d.ts.map +1 -0
  109. package/out/{ancestry.js → ancestry/index.js} +7 -7
  110. package/out/ancestry/index.js.map +1 -0
  111. package/out/build-candidate.d.ts +88 -7
  112. package/out/build-candidate.d.ts.map +1 -1
  113. package/out/build-candidate.js +145 -149
  114. package/out/build-candidate.js.map +1 -1
  115. package/out/build-slim.d.ts +5 -5
  116. package/out/build-slim.d.ts.map +1 -1
  117. package/out/build-slim.js +127 -142
  118. package/out/build-slim.js.map +1 -1
  119. package/out/candidate/alias-bags.d.ts +19 -0
  120. package/out/candidate/alias-bags.d.ts.map +1 -0
  121. package/out/candidate/alias-bags.js +39 -0
  122. package/out/candidate/alias-bags.js.map +1 -0
  123. package/out/candidate/ancestors-sidecar.d.ts +32 -0
  124. package/out/candidate/ancestors-sidecar.d.ts.map +1 -0
  125. package/out/candidate/ancestors-sidecar.js +140 -0
  126. package/out/candidate/ancestors-sidecar.js.map +1 -0
  127. package/out/candidate/country-display-names.d.ts +35 -0
  128. package/out/candidate/country-display-names.d.ts.map +1 -0
  129. package/out/candidate/country-display-names.js +59 -0
  130. package/out/candidate/country-display-names.js.map +1 -0
  131. package/out/candidate/extract-fold.d.ts +32 -0
  132. package/out/candidate/extract-fold.d.ts.map +1 -0
  133. package/out/candidate/extract-fold.js +104 -0
  134. package/out/candidate/extract-fold.js.map +1 -0
  135. package/out/candidate/name-roles.d.ts +57 -0
  136. package/out/candidate/name-roles.d.ts.map +1 -0
  137. package/out/candidate/name-roles.js +166 -0
  138. package/out/candidate/name-roles.js.map +1 -0
  139. package/out/candidate/own-name.d.ts +50 -0
  140. package/out/candidate/own-name.d.ts.map +1 -0
  141. package/out/candidate/own-name.js +132 -0
  142. package/out/candidate/own-name.js.map +1 -0
  143. package/out/candidate/place-attrs.d.ts +43 -0
  144. package/out/candidate/place-attrs.d.ts.map +1 -0
  145. package/out/candidate/place-attrs.js +15 -0
  146. package/out/candidate/place-attrs.js.map +1 -0
  147. package/out/candidate-ancestors-schema.d.ts +150 -0
  148. package/out/candidate-ancestors-schema.d.ts.map +1 -0
  149. package/out/candidate-ancestors-schema.js +123 -0
  150. package/out/candidate-ancestors-schema.js.map +1 -0
  151. package/out/candidate-fts.d.ts +7 -5
  152. package/out/candidate-fts.d.ts.map +1 -1
  153. package/out/candidate-fts.js +5 -3
  154. package/out/candidate-fts.js.map +1 -1
  155. package/out/candidate-importance.d.ts +16 -16
  156. package/out/candidate-importance.d.ts.map +1 -1
  157. package/out/candidate-importance.js +47 -52
  158. package/out/candidate-importance.js.map +1 -1
  159. package/out/candidate-lookup.d.ts +31 -52
  160. package/out/candidate-lookup.d.ts.map +1 -1
  161. package/out/candidate-lookup.js +380 -172
  162. package/out/candidate-lookup.js.map +1 -1
  163. package/out/candidate-schema.d.ts +31 -7
  164. package/out/candidate-schema.d.ts.map +1 -1
  165. package/out/candidate-schema.js +3 -0
  166. package/out/candidate-schema.js.map +1 -1
  167. package/out/candidate-scoring.d.ts +34 -0
  168. package/out/candidate-scoring.d.ts.map +1 -0
  169. package/out/candidate-scoring.js +196 -0
  170. package/out/candidate-scoring.js.map +1 -0
  171. package/out/capital-schema.d.ts +51 -0
  172. package/out/capital-schema.d.ts.map +1 -0
  173. package/out/capital-schema.js +63 -0
  174. package/out/capital-schema.js.map +1 -0
  175. package/out/capitals.d.ts +69 -0
  176. package/out/capitals.d.ts.map +1 -0
  177. package/out/capitals.js +98 -0
  178. package/out/capitals.js.map +1 -0
  179. package/out/coincident-roles.d.ts +12 -4
  180. package/out/coincident-roles.d.ts.map +1 -1
  181. package/out/coincident-roles.js +43 -11
  182. package/out/coincident-roles.js.map +1 -1
  183. package/out/{convention.d.ts → convention/index.d.ts} +5 -5
  184. package/out/convention/index.d.ts.map +1 -0
  185. package/out/{convention.js → convention/index.js} +4 -4
  186. package/out/convention/index.js.map +1 -0
  187. package/out/convention/schema.d.ts +51 -0
  188. package/out/convention/schema.d.ts.map +1 -0
  189. package/out/convention/schema.js +34 -0
  190. package/out/convention/schema.js.map +1 -0
  191. package/out/coverage-manifest-schema.d.ts +8 -8
  192. package/out/coverage-manifest-schema.d.ts.map +1 -1
  193. package/out/coverage-manifest-schema.js +6 -10
  194. package/out/coverage-manifest-schema.js.map +1 -1
  195. package/out/currency-backfill.d.ts +90 -0
  196. package/out/currency-backfill.d.ts.map +1 -0
  197. package/out/currency-backfill.js +196 -0
  198. package/out/currency-backfill.js.map +1 -0
  199. package/out/env.d.ts +43 -0
  200. package/out/env.d.ts.map +1 -0
  201. package/out/env.js +48 -0
  202. package/out/env.js.map +1 -0
  203. package/out/exact-match.d.ts +25 -0
  204. package/out/exact-match.d.ts.map +1 -0
  205. package/out/exact-match.js +88 -0
  206. package/out/exact-match.js.map +1 -0
  207. package/out/extracts.d.ts +114 -0
  208. package/out/extracts.d.ts.map +1 -0
  209. package/out/{sharding.js → extracts.js} +38 -38
  210. package/out/extracts.js.map +1 -0
  211. package/out/{fst-autocomplete.d.ts → fst/autocomplete.d.ts} +13 -13
  212. package/out/fst/autocomplete.d.ts.map +1 -0
  213. package/out/fst/autocomplete.js +113 -0
  214. package/out/fst/autocomplete.js.map +1 -0
  215. package/out/{fst-builder.d.ts → fst/builder.d.ts} +5 -5
  216. package/out/fst/builder.d.ts.map +1 -0
  217. package/out/{fst-builder.js → fst/builder.js} +24 -24
  218. package/out/fst/builder.js.map +1 -0
  219. package/out/{fst-deserialize-web.d.ts → fst/deserialize-web.d.ts} +3 -3
  220. package/out/fst/deserialize-web.d.ts.map +1 -0
  221. package/out/{fst-deserialize-web.js → fst/deserialize-web.js} +10 -74
  222. package/out/fst/deserialize-web.js.map +1 -0
  223. package/out/fst/format.d.ts +80 -0
  224. package/out/fst/format.d.ts.map +1 -0
  225. package/out/fst/format.js +91 -0
  226. package/out/fst/format.js.map +1 -0
  227. package/out/{fst-freshness.d.ts → fst/freshness.d.ts} +11 -14
  228. package/out/fst/freshness.d.ts.map +1 -0
  229. package/out/{fst-freshness.js → fst/freshness.js} +42 -74
  230. package/out/fst/freshness.js.map +1 -0
  231. package/out/fst/index.d.ts +14 -0
  232. package/out/fst/index.d.ts.map +1 -0
  233. package/out/fst/index.js +14 -0
  234. package/out/fst/index.js.map +1 -0
  235. package/out/{fst-matcher.d.ts → fst/matcher.d.ts} +2 -2
  236. package/out/fst/matcher.d.ts.map +1 -0
  237. package/out/{fst-matcher.js → fst/matcher.js} +1 -1
  238. package/out/fst/matcher.js.map +1 -0
  239. package/out/{fst-serialize.d.ts → fst/serialize.d.ts} +5 -10
  240. package/out/fst/serialize.d.ts.map +1 -0
  241. package/out/{fst-serialize.js → fst/serialize.js} +18 -98
  242. package/out/fst/serialize.js.map +1 -0
  243. package/out/{fst-types.d.ts → fst/types.d.ts} +4 -3
  244. package/out/fst/types.d.ts.map +1 -0
  245. package/out/{fst-types.js → fst/types.js} +1 -1
  246. package/out/fst/types.js.map +1 -0
  247. package/out/{fts.d.ts → fts/index.d.ts} +20 -13
  248. package/out/fts/index.d.ts.map +1 -0
  249. package/out/{fts.js → fts/index.js} +20 -15
  250. package/out/fts/index.js.map +1 -0
  251. package/out/{fts-query.d.ts → fts/query.d.ts} +2 -2
  252. package/out/fts/query.d.ts.map +1 -0
  253. package/out/{fts-query.js → fts/query.js} +2 -2
  254. package/out/fts/query.js.map +1 -0
  255. package/out/{geonames-aliases.d.ts → geonames/aliases.d.ts} +8 -6
  256. package/out/geonames/aliases.d.ts.map +1 -0
  257. package/out/{geonames-aliases.js → geonames/aliases.js} +7 -7
  258. package/out/geonames/aliases.js.map +1 -0
  259. package/out/geonames/index.d.ts +8 -0
  260. package/out/geonames/index.d.ts.map +1 -0
  261. package/out/geonames/index.js +8 -0
  262. package/out/geonames/index.js.map +1 -0
  263. package/out/{geonames-postal.d.ts → geonames/postal.d.ts} +12 -10
  264. package/out/geonames/postal.d.ts.map +1 -0
  265. package/out/{geonames-postal.js → geonames/postal.js} +11 -11
  266. package/out/geonames/postal.js.map +1 -0
  267. package/out/index.d.ts +32 -31
  268. package/out/index.d.ts.map +1 -1
  269. package/out/index.js +24 -24
  270. package/out/index.js.map +1 -1
  271. package/out/interpolation.d.ts +18 -9
  272. package/out/interpolation.d.ts.map +1 -1
  273. package/out/interpolation.js +108 -36
  274. package/out/interpolation.js.map +1 -1
  275. package/out/lookup.d.ts +25 -25
  276. package/out/lookup.d.ts.map +1 -1
  277. package/out/lookup.js +174 -558
  278. package/out/lookup.js.map +1 -1
  279. package/out/name-score.d.ts +0 -10
  280. package/out/name-score.d.ts.map +1 -1
  281. package/out/name-score.js +6 -4
  282. package/out/name-score.js.map +1 -1
  283. package/out/nsul/index.d.ts +8 -0
  284. package/out/nsul/index.d.ts.map +1 -0
  285. package/out/nsul/index.js +8 -0
  286. package/out/nsul/index.js.map +1 -0
  287. package/out/nsul/lookup.d.ts +83 -0
  288. package/out/nsul/lookup.d.ts.map +1 -0
  289. package/out/nsul/lookup.js +80 -0
  290. package/out/nsul/lookup.js.map +1 -0
  291. package/out/nsul/schema.d.ts +100 -0
  292. package/out/nsul/schema.d.ts.map +1 -0
  293. package/out/nsul/schema.js +78 -0
  294. package/out/nsul/schema.js.map +1 -0
  295. package/out/place-importance-schema.d.ts +47 -10
  296. package/out/place-importance-schema.d.ts.map +1 -1
  297. package/out/place-importance-schema.js +56 -10
  298. package/out/place-importance-schema.js.map +1 -1
  299. package/out/poi/index.d.ts +8 -0
  300. package/out/poi/index.d.ts.map +1 -0
  301. package/out/poi/index.js +8 -0
  302. package/out/poi/index.js.map +1 -0
  303. package/out/{poi-lookup.d.ts → poi/lookup.d.ts} +8 -10
  304. package/out/poi/lookup.d.ts.map +1 -0
  305. package/out/{poi-lookup.js → poi/lookup.js} +24 -29
  306. package/out/poi/lookup.js.map +1 -0
  307. package/out/{poi-schema.d.ts → poi/schema.d.ts} +10 -6
  308. package/out/poi/schema.d.ts.map +1 -0
  309. package/out/{poi-schema.js → poi/schema.js} +1 -1
  310. package/out/poi/schema.js.map +1 -0
  311. package/out/polygon-schema.d.ts +37 -0
  312. package/out/polygon-schema.d.ts.map +1 -0
  313. package/out/polygon-schema.js +23 -0
  314. package/out/polygon-schema.js.map +1 -0
  315. package/out/{postal-city-alias-lookup.d.ts → postal/city-alias-lookup.d.ts} +7 -6
  316. package/out/postal/city-alias-lookup.d.ts.map +1 -0
  317. package/out/{postal-city-alias-lookup.js → postal/city-alias-lookup.js} +12 -16
  318. package/out/postal/city-alias-lookup.js.map +1 -0
  319. package/out/{postal-city-alias-schema.d.ts → postal/city-alias-schema.d.ts} +1 -1
  320. package/out/{postal-city-alias-schema.d.ts.map → postal/city-alias-schema.d.ts.map} +1 -1
  321. package/out/{postal-city-alias-schema.js → postal/city-alias-schema.js} +1 -1
  322. package/out/{postal-city-alias-schema.js.map → postal/city-alias-schema.js.map} +1 -1
  323. package/out/{postal-city-candidate-schema.d.ts → postal/city-candidate-schema.d.ts} +4 -3
  324. package/out/postal/city-candidate-schema.d.ts.map +1 -0
  325. package/out/{postal-city-candidate-schema.js → postal/city-candidate-schema.js} +2 -2
  326. package/out/{postal-city-candidate-schema.js.map → postal/city-candidate-schema.js.map} +1 -1
  327. package/out/postal/index.d.ts +9 -0
  328. package/out/postal/index.d.ts.map +1 -0
  329. package/out/postal/index.js +9 -0
  330. package/out/postal/index.js.map +1 -0
  331. package/out/postcode-point-lookup.d.ts +10 -10
  332. package/out/postcode-point-lookup.d.ts.map +1 -1
  333. package/out/postcode-point-lookup.js +13 -13
  334. package/out/postcode-point-lookup.js.map +1 -1
  335. package/out/primary-preference.d.ts +133 -0
  336. package/out/primary-preference.d.ts.map +1 -0
  337. package/out/primary-preference.js +146 -0
  338. package/out/primary-preference.js.map +1 -0
  339. package/out/proximity-rerank.d.ts +77 -0
  340. package/out/proximity-rerank.d.ts.map +1 -0
  341. package/out/proximity-rerank.js +86 -0
  342. package/out/proximity-rerank.js.map +1 -0
  343. package/out/ranking-weights.d.ts +15 -3
  344. package/out/ranking-weights.d.ts.map +1 -1
  345. package/out/ranking-weights.js +21 -3
  346. package/out/ranking-weights.js.map +1 -1
  347. package/out/region-keys.d.ts +47 -0
  348. package/out/region-keys.d.ts.map +1 -0
  349. package/out/region-keys.js +121 -0
  350. package/out/region-keys.js.map +1 -0
  351. package/out/reverse.d.ts +6 -6
  352. package/out/reverse.d.ts.map +1 -1
  353. package/out/reverse.js +23 -35
  354. package/out/reverse.js.map +1 -1
  355. package/out/schema.d.ts +10 -1
  356. package/out/schema.d.ts.map +1 -1
  357. package/out/schema.js.map +1 -1
  358. package/out/search-fetch.d.ts +59 -0
  359. package/out/search-fetch.d.ts.map +1 -0
  360. package/out/search-fetch.js +184 -0
  361. package/out/search-fetch.js.map +1 -0
  362. package/out/sqlite-convention-source.d.ts +7 -7
  363. package/out/sqlite-convention-source.d.ts.map +1 -1
  364. package/out/sqlite-convention-source.js +5 -5
  365. package/out/sqlite-convention-source.js.map +1 -1
  366. package/out/sqlite-utils.d.ts +24 -6
  367. package/out/sqlite-utils.d.ts.map +1 -1
  368. package/out/sqlite-utils.js +24 -7
  369. package/out/sqlite-utils.js.map +1 -1
  370. package/out/{street-centroid-schema.d.ts → street/centroid-schema.d.ts} +12 -7
  371. package/out/street/centroid-schema.d.ts.map +1 -0
  372. package/out/{street-centroid-schema.js → street/centroid-schema.js} +5 -5
  373. package/out/{street-centroid-schema.js.map → street/centroid-schema.js.map} +1 -1
  374. package/out/{street-centroid.d.ts → street/centroid.d.ts} +6 -6
  375. package/out/street/centroid.d.ts.map +1 -0
  376. package/out/{street-centroid.js → street/centroid.js} +18 -24
  377. package/out/street/centroid.js.map +1 -0
  378. package/out/street/index.d.ts +13 -0
  379. package/out/street/index.d.ts.map +1 -0
  380. package/out/street/index.js +13 -0
  381. package/out/street/index.js.map +1 -0
  382. package/out/{street-morphology-fst-builder.d.ts → street/morphology-fst-builder.d.ts} +4 -4
  383. package/out/street/morphology-fst-builder.d.ts.map +1 -0
  384. package/out/{street-morphology-fst-builder.js → street/morphology-fst-builder.js} +18 -13
  385. package/out/street/morphology-fst-builder.js.map +1 -0
  386. package/out/{street-morphology-fst-loader.d.ts → street/morphology-fst-loader.d.ts} +5 -5
  387. package/out/street/morphology-fst-loader.d.ts.map +1 -0
  388. package/out/{street-morphology-fst-loader.js → street/morphology-fst-loader.js} +11 -10
  389. package/out/street/morphology-fst-loader.js.map +1 -0
  390. package/out/{street-name-lookup.d.ts → street/name-lookup.d.ts} +4 -7
  391. package/out/street/name-lookup.d.ts.map +1 -0
  392. package/out/{street-name-lookup.js → street/name-lookup.js} +9 -23
  393. package/out/street/name-lookup.js.map +1 -0
  394. package/out/street/normalize.d.ts +179 -0
  395. package/out/street/normalize.d.ts.map +1 -0
  396. package/out/street/normalize.js +457 -0
  397. package/out/street/normalize.js.map +1 -0
  398. package/out/{street-segment-schema.d.ts → street/segment-schema.d.ts} +13 -9
  399. package/out/street/segment-schema.d.ts.map +1 -0
  400. package/out/{street-segment-schema.js → street/segment-schema.js} +4 -4
  401. package/out/{street-segment-schema.js.map → street/segment-schema.js.map} +1 -1
  402. package/out/types.d.ts +49 -9
  403. package/out/types.d.ts.map +1 -1
  404. package/out/types.js.map +1 -1
  405. package/out/unified-schema.d.ts +6 -5
  406. package/out/unified-schema.d.ts.map +1 -1
  407. package/out/unified-schema.js +16 -19
  408. package/out/unified-schema.js.map +1 -1
  409. package/out/uprn/existence.d.ts +53 -0
  410. package/out/uprn/existence.d.ts.map +1 -0
  411. package/out/uprn/existence.js +61 -0
  412. package/out/uprn/existence.js.map +1 -0
  413. package/out/uprn/index.d.ts +9 -0
  414. package/out/uprn/index.d.ts.map +1 -0
  415. package/out/uprn/index.js +9 -0
  416. package/out/uprn/index.js.map +1 -0
  417. package/out/uprn/lookup.d.ts +85 -0
  418. package/out/uprn/lookup.d.ts.map +1 -0
  419. package/out/uprn/lookup.js +149 -0
  420. package/out/uprn/lookup.js.map +1 -0
  421. package/out/uprn/schema.d.ts +93 -0
  422. package/out/uprn/schema.d.ts.map +1 -0
  423. package/out/uprn/schema.js +78 -0
  424. package/out/uprn/schema.js.map +1 -0
  425. package/out/weights-overlay-linker.d.ts +299 -0
  426. package/out/weights-overlay-linker.d.ts.map +1 -0
  427. package/out/weights-overlay-linker.js +511 -0
  428. package/out/weights-overlay-linker.js.map +1 -0
  429. package/package.json +622 -154
  430. package/address-point.ts +0 -125
  431. package/candidate-lookup.ts +0 -720
  432. package/fst-autocomplete.ts +0 -204
  433. package/geo.ts +0 -121
  434. package/lookup.ts +0 -1354
  435. package/out/address-point-interpolation.d.ts.map +0 -1
  436. package/out/address-point-interpolation.js.map +0 -1
  437. package/out/address-point-schema.d.ts.map +0 -1
  438. package/out/address-point-schema.js.map +0 -1
  439. package/out/address-point.d.ts.map +0 -1
  440. package/out/address-point.js +0 -84
  441. package/out/address-point.js.map +0 -1
  442. package/out/ancestry-backfill.d.ts.map +0 -1
  443. package/out/ancestry-backfill.js.map +0 -1
  444. package/out/ancestry.d.ts.map +0 -1
  445. package/out/ancestry.js.map +0 -1
  446. package/out/convention.d.ts.map +0 -1
  447. package/out/convention.js.map +0 -1
  448. package/out/fst-autocomplete.d.ts.map +0 -1
  449. package/out/fst-autocomplete.js +0 -130
  450. package/out/fst-autocomplete.js.map +0 -1
  451. package/out/fst-builder.d.ts.map +0 -1
  452. package/out/fst-builder.js.map +0 -1
  453. package/out/fst-deserialize-web.d.ts.map +0 -1
  454. package/out/fst-deserialize-web.js.map +0 -1
  455. package/out/fst-freshness.d.ts.map +0 -1
  456. package/out/fst-freshness.js.map +0 -1
  457. package/out/fst-matcher.d.ts.map +0 -1
  458. package/out/fst-matcher.js.map +0 -1
  459. package/out/fst-serialize.d.ts.map +0 -1
  460. package/out/fst-serialize.js.map +0 -1
  461. package/out/fst-types.d.ts.map +0 -1
  462. package/out/fst-types.js.map +0 -1
  463. package/out/fts-query.d.ts.map +0 -1
  464. package/out/fts-query.js.map +0 -1
  465. package/out/fts.d.ts.map +0 -1
  466. package/out/fts.js.map +0 -1
  467. package/out/geo.d.ts +0 -74
  468. package/out/geo.d.ts.map +0 -1
  469. package/out/geo.js +0 -71
  470. package/out/geo.js.map +0 -1
  471. package/out/geonames-aliases.d.ts.map +0 -1
  472. package/out/geonames-aliases.js.map +0 -1
  473. package/out/geonames-postal.d.ts.map +0 -1
  474. package/out/geonames-postal.js.map +0 -1
  475. package/out/poi-lookup.d.ts.map +0 -1
  476. package/out/poi-lookup.js.map +0 -1
  477. package/out/poi-schema.d.ts.map +0 -1
  478. package/out/poi-schema.js.map +0 -1
  479. package/out/postal-city-alias-lookup.d.ts.map +0 -1
  480. package/out/postal-city-alias-lookup.js.map +0 -1
  481. package/out/postal-city-candidate-schema.d.ts.map +0 -1
  482. package/out/sharding.d.ts +0 -114
  483. package/out/sharding.d.ts.map +0 -1
  484. package/out/sharding.js.map +0 -1
  485. package/out/street-centroid-schema.d.ts.map +0 -1
  486. package/out/street-centroid.d.ts.map +0 -1
  487. package/out/street-centroid.js.map +0 -1
  488. package/out/street-morphology-fst-builder.d.ts.map +0 -1
  489. package/out/street-morphology-fst-builder.js.map +0 -1
  490. package/out/street-morphology-fst-loader.d.ts.map +0 -1
  491. package/out/street-morphology-fst-loader.js.map +0 -1
  492. package/out/street-name-lookup.d.ts.map +0 -1
  493. package/out/street-name-lookup.js.map +0 -1
  494. package/out/street-normalize.d.ts +0 -95
  495. package/out/street-normalize.d.ts.map +0 -1
  496. package/out/street-normalize.js +0 -272
  497. package/out/street-normalize.js.map +0 -1
  498. package/out/street-segment-schema.d.ts.map +0 -1
  499. package/sqlite-utils.ts +0 -46
  500. package/street-normalize.ts +0 -318
  501. /package/{postal-city-alias-schema.ts → lib/postal/city-alias-schema.ts} +0 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The FTS5 candidate fetch behind the fuzzy name match: the schema-qualified `place_search`
7
+ * MATCH, its population-ordered companion fetch, and the raw row shape both of them return.
8
+ *
9
+ * Bbox and near-with-radius narrow at the SQL level through SQLite's built-in `rtree`, whose index name and schema
10
+ * live in `fts.ts` beside the FTS5 build. That is why this package pulls neither SpatiaLite nor turf: the R*Tree does
11
+ * the narrowing, and the passes downstream of it operate on ≤ a few hundred candidates per query rather than the
12
+ * whole corpus, so an exact haversine over the survivors is cheap.
13
+ */
14
+
15
+ import { bboxAround } from "@mailwoman/spatial"
16
+ import type { DatabaseClient, SQLInputValue } from "@mailwoman/sqlite/client"
17
+
18
+ import { PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE } from "#fts/index"
19
+ import type { RankingWeights } from "#ranking-weights"
20
+ import { allRows } from "#sqlite-utils"
21
+ import type { FindPlaceQuery, WOFPlacetype } from "#types"
22
+
23
+ /**
24
+ * Query length at or below which the FTS window is widened. A two- or three-character query is almost always a region
25
+ * abbreviation, where the exact match can otherwise fall outside the window behind higher-bm25 partial hits — "NY"
26
+ * losing to "New York".
27
+ */
28
+ const SHORT_QUERY_MAX_LENGTH = 3
29
+
30
+ /**
31
+ * Over-fetch floor for SHORT (≤3-char) queries — region abbreviations like "NY"/"VT". An exact-abbrev holder's BM25 is
32
+ * poor (long multilingual alt-name document), so the normal `limit * 4` window can drop it before `exactMatchTiering`
33
+ * promotes it. 200 comfortably covers every same-abbrev region across the 12-country gazetteer (a 2-letter token
34
+ * matches a few dozen regions at most) while staying a cheap region-placetype fetch. See the `#fuzzyNameMatch`
35
+ * over-fetch comment.
36
+ */
37
+ const SHORT_QUERY_OVERFETCH = 200
38
+
39
+ /**
40
+ * How many rows the population-ordered companion fetch (#905) adds to the candidate pool. Small on purpose: its only
41
+ * job is to guarantee the FAMOUS holders of a name enter the pool at all — for "Paris"-class floods the bm25 window is
42
+ * saturated by thousands of tiny same-name rows and no boost inside the bm25-based ORDER BY can rescue a candidate
43
+ * whose bm25 is length-poisoned by ~15 points (see the fetch-site comment).
44
+ */
45
+ const POPULATION_FETCH_LIMIT = 15
46
+
47
+ export interface RawSearchRow {
48
+ id: number
49
+ name: string
50
+ placetype: string
51
+ country: string | null
52
+ parent_id: number | null
53
+ rank: number // BM25 (lower = better in SQLite); we negate to get higher-is-better
54
+ lat: number | null
55
+ lon: number | null
56
+ min_latitude: number | null
57
+ max_latitude: number | null
58
+ min_longitude: number | null
59
+ max_longitude: number | null
60
+ population: number | null // from the place_population aux table; null when missing
61
+ /**
62
+ * From `place_importance.encyclopedic` when the extract's table carries the two-score split columns. NULL means the
63
+ * place has no Wikipedia article, or the extract predates the split — absence either way, and never 0 (ROAD_TO_V9
64
+ * §2).
65
+ */
66
+ encyclopedic: number | null
67
+ }
68
+
69
+ /**
70
+ * Fetch the raw candidate rows for a name match on one extract: the BM25-ordered window over `place_search` (widened
71
+ * for short queries), plus the population-ordered companion fetch that keeps the prominent holders of a name
72
+ * pool-complete. `schemaName` is the routed extract's bare schema name — validated at construction, so it is
73
+ * interpolated directly.
74
+ */
75
+ export function fetchSearchRows<DB>(options: {
76
+ db: DatabaseClient<DB>
77
+ schemaName: string
78
+ query: FindPlaceQuery
79
+ placetypes: WOFPlacetype[] | null
80
+ ftsQuery: string
81
+ limit: number
82
+ hasBboxIndex: ReadonlyMap<string, boolean>
83
+ hasPopulationIndex: ReadonlyMap<string, boolean>
84
+ encyclopedicClauses: ReadonlyMap<string, { select: string; join: string }>
85
+ weights: RankingWeights
86
+ }): RawSearchRow[] {
87
+ const {
88
+ db,
89
+ schemaName: sch,
90
+ query,
91
+ placetypes,
92
+ ftsQuery,
93
+ limit,
94
+ hasBboxIndex,
95
+ hasPopulationIndex,
96
+ encyclopedicClauses,
97
+ weights,
98
+ } = options
99
+
100
+ // Over-fetch so post-scoring + exact-match tiering have room to re-rank. SHORT queries (a 2–3-char
101
+ // region abbreviation like "NY"/"VT") are the danger case the `exactMatchTiering` docstring flags:
102
+ // the exact-abbrev holder's BM25 is poor (its long multilingual alt-name document tanks the score),
103
+ // so under the normal `limit * 4` window it drops OUT of the candidate pool BEFORE tiering can
104
+ // promote it — "NY" then resolves to a token-matching foreign region (Highland, GB) instead of New
105
+ // York. Widen the window for short queries so the exact match is always present to be tiered.
106
+ // (Cross-country abbrev collisions — "VT" is BOTH Vermont and Viterbo — still need a country/
107
+ // postcode signal to disambiguate; this only rescues the window-drop class, not genuine ambiguity.
108
+ // With a `country` hint every abbrev resolves; bare + no-context lifts 7→10/15 US states.)
109
+ const ftsLimit =
110
+ query.text.trim().length <= SHORT_QUERY_MAX_LENGTH ? Math.max(limit * 4, SHORT_QUERY_OVERFETCH) : limit * 4
111
+
112
+ // Filter out historical / superseded / deprecated places by default — they live in the same
113
+ // spr table but should never win a contemporary lookup. `is_current = 0` is the only WOF
114
+ // value that means "not current"; both `-1` (modern) and `1` (legacy) mean current. See #91.
115
+ // Note: with schema-qualified FROM the bare `place_search` reference in MATCH resolves to
116
+ // the FROM table — required by FTS5 parser, see extracts.ts header comment.
117
+ const where: string[] = ["place_search MATCH ?", "spr.is_current != 0", "spr.is_deprecated = 0"]
118
+ const params: SQLInputValue[] = [ftsQuery]
119
+
120
+ if (placetypes && placetypes.length) {
121
+ where.push(`spr.placetype IN (${placetypes.map(() => "?").join(", ")})`)
122
+ params.push(...placetypes)
123
+ }
124
+
125
+ if (query.country) {
126
+ where.push("spr.country = ?")
127
+ params.push(query.country)
128
+ }
129
+
130
+ if (query.parentID !== undefined) {
131
+ where.push(`(spr.parent_id = ? OR spr.id IN (SELECT id FROM ${sch}.ancestors WHERE ancestor_id = ?))`)
132
+ params.push(query.parentID, query.parentID)
133
+ }
134
+
135
+ // Bbox + near-with-radius are SQL-level filters via the R*Tree. We only emit the JOIN when
136
+ // the active extract has the R*Tree; missing-but-requested is silently treated as no-bbox-
137
+ // filter so legacy DBs / extracts-without-bbox don't crash.
138
+ const extractHasBbox = hasBboxIndex.get(sch) === true
139
+ const useBboxJoin = (query.bbox || query.near?.maxDistanceKm !== undefined) && extractHasBbox
140
+ let joinClause = `JOIN ${sch}.spr ON spr.id = place_search.wof_id`
141
+
142
+ if (useBboxJoin) {
143
+ joinClause += ` JOIN ${sch}.${PLACE_BBOX_TABLE} bbox ON bbox.id = spr.id`
144
+ // AABB intersection — both bbox sides must overlap. R*Tree handles this in O(log n).
145
+ const filterBox = query.bbox || bboxAround(query.near!.lat, query.near!.lon, query.near!.maxDistanceKm!)
146
+ where.push("bbox.min_lat <= ? AND bbox.max_lat >= ?", "bbox.min_lon <= ? AND bbox.max_lon >= ?")
147
+ params.push(filterBox.maxLat, filterBox.minLat, filterBox.maxLon, filterBox.minLon)
148
+ }
149
+
150
+ // LEFT JOIN the population aux table when present. Missing-on-this-extract means the SELECT
151
+ // just doesn't include the population column; the post-scoring loop treats it as 0.
152
+ const extractHasPopulation = hasPopulationIndex.get(sch) === true
153
+
154
+ const populationSelect = extractHasPopulation
155
+ ? `${PLACE_POPULATION_TABLE}.population AS population`
156
+ : `NULL AS population`
157
+
158
+ const populationJoin = extractHasPopulation
159
+ ? `LEFT JOIN ${sch}.${PLACE_POPULATION_TABLE} ON ${PLACE_POPULATION_TABLE}.id = spr.id`
160
+ : ""
161
+
162
+ // The encyclopedic score is CARRIED, never ranked on (ROAD_TO_V9 §2, ratified 2026-08-06) — it
163
+ // appears in the SELECT and in no ORDER BY, here or in the companion fetch below. Conditioned on the
164
+ // split column, so a pre-split extract emits a literal NULL and builds no join at all.
165
+ const { select: encyclopedicSelect, join: encyclopedicJoin } = encyclopedicClauses.get(sch)!
166
+
167
+ // Push the population boost into the ORDER BY when the index is available, so famous places
168
+ // (whose long alt-name lists hurt BM25) actually make it into the over-fetch window. The TS
169
+ // post-scoring will still compute the same boost for the final score; this just ensures the
170
+ // candidate set is right.
171
+ //
172
+ // Formula: rank_adjusted = bm25 - populationBoost * min(1.0, log10(1 + pop) / scaleLog10)
173
+ // Lower rank_adjusted = better (matches SQLite's bm25 convention of "more negative = better").
174
+ //
175
+ // #905 — do NOT reach for bm25 column weights here. Measured falsification (2026-07-02): FTS5's
176
+ // bm25 length normalization is polluted by the row's TOTAL document size, so identical 1-token
177
+ // `name` docs read −16.0 (empty alt_names) vs −0.43 (2.7 KB alt_names) EVEN with the alt_names
178
+ // column weighted to zero — no weighting isolates name relevance in this schema. The famous-
179
+ // holder guarantee lives in the population-ordered companion fetch below instead, and the
180
+ // exact tier breaks ties by population in the post-scoring sort.
181
+ const orderByExpr = extractHasPopulation
182
+ ? `(bm25(place_search) - ? * MIN(1.0, COALESCE(log10(1.0 + ${PLACE_POPULATION_TABLE}.population), 0) / ?))`
183
+ : "bm25(place_search)"
184
+
185
+ // Schema-qualified FROM with bare-name MATCH — required syntax for FTS5 on attached schemas.
186
+ // See extracts.ts header for the failure mode that drove this design.
187
+ const stmt = db.prepare(`
188
+ SELECT
189
+ spr.id AS id,
190
+ spr.name,
191
+ spr.placetype,
192
+ spr.country,
193
+ spr.parent_id,
194
+ bm25(place_search) AS rank,
195
+ spr.latitude AS lat,
196
+ spr.longitude AS lon,
197
+ spr.min_latitude, spr.max_latitude, spr.min_longitude, spr.max_longitude,
198
+ ${populationSelect},
199
+ ${encyclopedicSelect}
200
+ FROM ${sch}.place_search
201
+ ${joinClause}
202
+ ${populationJoin}
203
+ ${encyclopedicJoin}
204
+ WHERE ${where.join(" AND ")}
205
+ ORDER BY ${orderByExpr} ASC
206
+ LIMIT ?
207
+ `)
208
+
209
+ if (extractHasPopulation) {
210
+ params.push(weights.populationBoost, weights.populationScaleLog10)
211
+ }
212
+
213
+ params.push(ftsLimit)
214
+
215
+ const rawRows = allRows<RawSearchRow>(stmt, ...params)
216
+
217
+ // #905 companion fetch: the same MATCH, ordered by population alone. For name floods
218
+ // ("Paris" matches thousands of gap-fill villages) the bm25-based window above cannot admit
219
+ // the famous holder — its bm25 is length-poisoned by the row's alias bulk (measured ~15 pts,
220
+ // vs a +4.0 boost cap), so FR Paris never even reaches post-scoring. This fetch makes the
221
+ // prominent holders of a name pool-complete BY CONSTRUCTION; the exact-tier sort below
222
+ // decides whether they win. Skipped without a population index (nothing to order by).
223
+ if (extractHasPopulation) {
224
+ const popStmt = db.prepare(`
225
+ SELECT
226
+ spr.id AS id,
227
+ spr.name,
228
+ spr.placetype,
229
+ spr.country,
230
+ spr.parent_id,
231
+ bm25(place_search) AS rank,
232
+ spr.latitude AS lat,
233
+ spr.longitude AS lon,
234
+ spr.min_latitude, spr.max_latitude, spr.min_longitude, spr.max_longitude,
235
+ ${populationSelect},
236
+ ${encyclopedicSelect}
237
+ FROM ${sch}.place_search
238
+ ${joinClause}
239
+ ${populationJoin}
240
+ ${encyclopedicJoin}
241
+ WHERE ${where.join(" AND ")}
242
+ ORDER BY COALESCE(${PLACE_POPULATION_TABLE}.population, 0) DESC
243
+ LIMIT ?
244
+ `)
245
+
246
+ const popParams = params.slice(0, -3) // drop the two boost params + ftsLimit
247
+ const seen = new Set(rawRows.map((r) => r.id))
248
+
249
+ for (const row of allRows<RawSearchRow>(popStmt, ...popParams, POPULATION_FETCH_LIMIT)) {
250
+ if (!seen.has(row.id)) {
251
+ rawRows.push(row)
252
+ }
253
+ }
254
+ }
255
+
256
+ return rawRows
257
+ }
@@ -8,21 +8,20 @@
8
8
  * table keyed by WOF polygon id; this source queries them ON DEMAND by id (one indexed lookup,
9
9
  * memoized) rather than paging the whole table into memory as a code constant — the deliberate
10
10
  * counter to the Pelias "giant dictionary in RAM, no provenance" pattern (see the operator design
11
- * value in memory `feedback-no-load-bearing-trivia`).
11
+ * value in memory `feedback-no-irrelevant-trivia`).
12
12
  *
13
13
  * The asset is the queryable, distributable artifact; the strategy IMPLEMENTATIONS stay in code. An
14
14
  * unknown strategy NAME is surfaced loudly at dispatch (see `lookup.ts`), not silently
15
15
  * swallowed.
16
16
  */
17
17
 
18
- import type { DatabaseSync } from "node:sqlite"
18
+ import { tryParsingJSON } from "@mailwoman/core/json"
19
+ import type { DatabaseClient } from "@mailwoman/sqlite/client"
19
20
 
20
- import { tryParsingJSON } from "@mailwoman/core/objects"
21
+ import { ADDRESS_CONVENTION_TABLE, type Convention, type ConventionSource } from "#convention/index"
21
22
 
22
- import { ADDRESS_CONVENTION_TABLE, type Convention, type ConventionSource } from "./convention.ts"
23
-
24
- export class SqliteConventionSource implements ConventionSource {
25
- readonly #db: DatabaseSync
23
+ export class SqliteConventionSource<DB> implements ConventionSource {
24
+ readonly #db: DatabaseClient<DB>
26
25
  readonly #schema: string
27
26
  /**
28
27
  * Memoize per-id lookups (including misses, as `null`) so a hot ancestor chain is queried once.
@@ -31,10 +30,10 @@ export class SqliteConventionSource implements ConventionSource {
31
30
 
32
31
  /**
33
32
  * @param db An open handle to a DB that has the convention asset attached (or is it).
34
- * @param schema The schema name the `address_convention` table lives under (`main` or an ATTACHed shard name —
35
- * `WOFSqlitePlaceLookup` auto-detects which shard carries the table).
33
+ * @param schema The schema name the `address_convention` table lives under (`main` or an ATTACHed extract name —
34
+ * `WOFSQLitePlaceLookup` auto-detects which extract carries the table).
36
35
  */
37
- constructor(db: DatabaseSync, schema: string) {
36
+ constructor(db: DatabaseClient<DB>, schema: string) {
38
37
  this.#db = db
39
38
  this.#schema = schema
40
39
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Small shared helpers for the SQLite-backed lookups.
7
+ */
8
+
9
+ import { allRows, getRow } from "@mailwoman/core/utils"
10
+ import type { DatabaseClient, SQLInputValue } from "@mailwoman/sqlite/client"
11
+ import { hasColumn as columnExists, tableExists } from "@mailwoman/sqlite/introspection"
12
+
13
+ // The row-shape assertion itself lives in `core` so the readers that cannot depend on this package reach the same
14
+ // helper; re-exported here because this module is where this package's readers already look for it.
15
+ export { allRows, getRow } from "@mailwoman/core/utils"
16
+
17
+ /**
18
+ * A prepared single-row query whose parameter tuple remains visible to TypeScript. `StatementSync` accepts only the
19
+ * broad `SQLInputValue[]`, which otherwise erases tagged key types before they reach SQLite.
20
+ */
21
+ export type PreparedGet<Parameters extends SQLInputValue[], Row> = (...parameters: Parameters) => Row | undefined
22
+
23
+ /**
24
+ * Prepare a single-row query while preserving its exact parameter tuple at every call site.
25
+ */
26
+ export function prepareGet<Parameters extends SQLInputValue[], Row, DB>(
27
+ db: DatabaseClient<DB>,
28
+ sql: string
29
+ ): PreparedGet<Parameters, Row> {
30
+ const statement = db.prepare(sql)
31
+
32
+ return (...parameters) => getRow<Row>(statement, ...parameters)
33
+ }
34
+
35
+ /**
36
+ * Multi-row counterpart to {@link PreparedGet}.
37
+ */
38
+ export type PreparedAll<Parameters extends SQLInputValue[], Row> = (...parameters: Parameters) => Row[]
39
+
40
+ /**
41
+ * Prepare a multi-row query while preserving its exact parameter tuple at every call site.
42
+ */
43
+ export function prepareAll<Parameters extends SQLInputValue[], Row, DB>(
44
+ db: DatabaseClient<DB>,
45
+ sql: string
46
+ ): PreparedAll<Parameters, Row> {
47
+ const statement = db.prepare(sql)
48
+
49
+ return (...parameters) => allRows<Row>(statement, ...parameters)
50
+ }
51
+
52
+ /**
53
+ * True when `name` is a table in the open database. The street-level lookups use this to degrade gracefully on an
54
+ * empty/tableless extract — an interrupted `build-*-extract.ts`, or a stray 0-byte file (e.g. `sqlite3 <missing>.db
55
+ * "…"` CREATES one) — rather than throwing `no such table` at construction and taking down a whole state's geocode
56
+ * (#568). A missing table makes the lookup a no-op miss.
57
+ */
58
+ export function hasTable<DB>(db: DatabaseClient<DB>, name: string): boolean {
59
+ try {
60
+ return tableExists(db, name)
61
+ } catch {
62
+ return false
63
+ }
64
+ }
65
+
66
+ /**
67
+ * True when `table` exists in the open database AND carries `column`.
68
+ *
69
+ * The column-level sibling of {@link hasTable}, and it exists for the same reason one layer down: an artifact built
70
+ * before a column was added is still a VALID artifact, and a reader that unconditionally names the new column in its
71
+ * `SELECT` turns "this gazetteer is a build behind" into `no such column` at the first keystroke. Probe once at
72
+ * construction and shape the query — `table_info` is a PRAGMA, so it must not sit on a per-query path.
73
+ *
74
+ * Note the interpolation: PRAGMA does not take bound parameters, so `table` is spliced. Every caller passes a
75
+ * module-level constant; never pass user input.
76
+ */
77
+ export function hasColumn<DB>(db: DatabaseClient<DB>, table: string, column: string): boolean {
78
+ try {
79
+ return columnExists(db, table, column)
80
+ } catch {
81
+ return false
82
+ }
83
+ }
@@ -3,10 +3,10 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Typed schema for the DERIVED STREET-CENTROID shard (`street-centroids-<cc>.db`, built by
7
- * `ban/scripts/build-street-centroid-shard.ts` — the #1042 street-level tier behind "street-only
8
- * FR queries deserve a street-level answer"). The shard is a `GROUP BY street` roll-up of the
9
- * sealed rooftop address-point shard: one row per (street, postcode, commune) carrying the street's
6
+ * Typed schema for the DERIVED STREET-CENTROID extract (`street-centroids-<cc>.db`, built by
7
+ * `ban/scripts/build-street-centroid-extract.ts` — the #1042 street-level tier behind "street-only
8
+ * FR queries deserve a street-level answer"). The extract is a `GROUP BY street` roll-up of the
9
+ * sealed rooftop address-point extract: one row per (street, postcode, commune) carrying the street's
10
10
  * CENTROID + bounding-box EXTENT + member-point count. No new data source — a derived artifact.
11
11
  *
12
12
  * Single source of truth for the columns shared by the BUILDER (a positional prepared INSERT for
@@ -22,6 +22,8 @@
22
22
 
23
23
  import type { Kysely } from "kysely"
24
24
 
25
+ import type { NameKey, StreetKey } from "#street/normalize"
26
+
25
27
  /**
26
28
  * One street roll-up. `(street_norm, postcode, locality_base)` is unique. `lat`/`lon` are the UNWEIGHTED mean of the
27
29
  * group's member address points (each source row = one point), so a cross-group weighted mean (`SUM(lat*point_count) /
@@ -32,7 +34,7 @@ export interface StreetCentroidTable {
32
34
  /**
33
35
  * Shared `normalizeStreetForKeyLocale` of the street — the build/query-consistent probe key.
34
36
  */
35
- street_norm: string
37
+ street_norm: StreetKey
36
38
  /**
37
39
  * The 5-digit postcode of this group, or null when the source row carried none.
38
40
  */
@@ -40,7 +42,7 @@ export interface StreetCentroidTable {
40
42
  /**
41
43
  * Arrondissement-stripped commune (`stripArrondissement(normalizeLocalityForKey(commune))`) — the fallback scope.
42
44
  */
43
- locality_base: string
45
+ locality_base: NameKey
44
46
  /**
45
47
  * Weighted-mean centroid latitude of the street's member points.
46
48
  */
@@ -75,6 +77,10 @@ export interface StreetCentroidTable {
75
77
  * name-evidence rerank folds the model's street surface with the SAME `foldStreetSurface` used to build this column
76
78
  * (the fold-parity contract), so it must not drift from `street_norm`'s richer normalizer. Indexed (`idx_sc_name`)
77
79
  * for a direct seek.
80
+ *
81
+ * Deliberately UNBRANDED despite the `name_key` column name: `foldStreetSurface` is a different fold from the
82
+ * {@link NameKey} one every other `name_key` column carries, and giving it that brand would invite exactly the
83
+ * cross-fold probe the brands exist to stop.
78
84
  */
79
85
  name_key: string
80
86
  }
@@ -8,7 +8,7 @@
8
8
  * number) plus a postcode/commune scope, it returns the street's CENTROID + an honest extent-derived
9
9
  * uncertainty from the derived `street-centroids-<cc>.db` roll-up.
10
10
  *
11
- * Query-side normalization is THE shared normalizer (`street-normalize.ts`), selected per the shard's
11
+ * Query-side normalization is THE shared normalizer (`street-normalize.ts`), selected per the extract's
12
12
  * `streetLocale`, so build-side and probe-side keys agree by construction. The commune scope folds
13
13
  * through `normalizeLocalityForKey` + `stripArrondissement` (BAN names Paris/Lyon/Marseille per
14
14
  * arrondissement; a query names the base commune).
@@ -19,17 +19,21 @@
19
19
  * exact-after-normalization only (no fuzzy street matching in this tier).
20
20
  */
21
21
 
22
- import { DatabaseSync } from "node:sqlite"
23
-
24
22
  import type { StreetCentroidHit, StreetCentroidLookup } from "@mailwoman/resolver"
23
+ import { haversineKm } from "@mailwoman/spatial"
24
+ import { DatabaseClient } from "@mailwoman/sqlite/client"
25
25
 
26
- import { hasTable } from "./sqlite-utils.ts"
26
+ import { hasTable, prepareGet, type PreparedGet } from "#sqlite-utils"
27
+ import type { StreetCentroidDatabase } from "#street/centroid-schema"
27
28
  import {
28
29
  normalizeLocalityForKey,
29
30
  normalizeStreetForKeyLocale,
31
+ type NameKey,
32
+ type StreetKey,
33
+ streetLocaleForSurface,
30
34
  type StreetLocale,
31
35
  stripArrondissement,
32
- } from "./street-normalize.ts"
36
+ } from "#street/normalize"
33
37
 
34
38
  /**
35
39
  * The weighted-centroid + extent + provenance an aggregate probe projects. `lat` is null when nothing matched.
@@ -58,40 +62,34 @@ const AGG_SELECT =
58
62
  * Half the bbox diagonal, in METERS — an honest coarse radius for a street centroid.
59
63
  */
60
64
  function extentRadiusM(minLat: number, maxLat: number, minLon: number, maxLon: number): number {
61
- const R = 6_371_000
62
- const toRad = (d: number): number => (d * Math.PI) / 180
63
- const dLat = toRad(maxLat - minLat)
64
- const dLon = toRad(maxLon - minLon)
65
- const midLat = toRad((minLat + maxLat) / 2)
66
- const a = Math.sin(dLat / 2) ** 2 + Math.cos(midLat) ** 2 * Math.sin(dLon / 2) ** 2
67
- const diag = 2 * R * Math.asin(Math.min(1, Math.sqrt(a)))
68
-
69
- return Math.round(diag / 2)
65
+ return Math.round(haversineKm(minLat, minLon, maxLat, maxLon) * 500)
70
66
  }
71
67
 
72
68
  export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
73
- readonly #db: DatabaseSync
69
+ readonly #db: DatabaseClient<StreetCentroidDatabase>
74
70
  readonly #locale: StreetLocale
75
- readonly #byPostcode: ReturnType<DatabaseSync["prepare"]> | undefined
76
- readonly #byLocality: ReturnType<DatabaseSync["prepare"]> | undefined
71
+ readonly #byPostcode: PreparedGet<[postcode: string, street: StreetKey], AggRow> | undefined
72
+ readonly #byLocality: PreparedGet<[locality: NameKey, street: StreetKey], AggRow> | undefined
77
73
 
78
74
  /**
79
- * @param dbPath Shard path.
80
- * @param opts.streetLocale The street-normalization locale this shard was BUILT with — must match, or every key
75
+ * @param dbPath Extract path.
76
+ * @param opts.streetLocale The street-normalization locale this extract was BUILT with — must match, or every key
81
77
  * misses. Defaults to `"fr"` (BAN is the French national register; the tier is FR-only today).
82
78
  */
83
79
  constructor(dbPath: string, opts: { streetLocale?: StreetLocale } = {}) {
84
- this.#db = new DatabaseSync(dbPath, { readOnly: true })
80
+ this.#db = new DatabaseClient<StreetCentroidDatabase>(dbPath, { readOnly: true })
85
81
  this.#locale = opts.streetLocale ?? "fr"
86
82
 
87
- // Degrade gracefully on an empty/tableless shard (interrupted build, stray 0-byte file): with no
83
+ // Degrade gracefully on an empty/tableless extract (interrupted build, stray 0-byte file): with no
88
84
  // `street_centroid` table this lookup is a no-op miss, not a crash (mirrors the address-point reader).
89
85
  if (hasTable(this.#db, "street_centroid")) {
90
- this.#byPostcode = this.#db.prepare(
86
+ this.#byPostcode = prepareGet(
87
+ this.#db,
91
88
  `SELECT ${AGG_SELECT} FROM street_centroid WHERE postcode = ? AND street_norm = ?`
92
89
  )
93
90
 
94
- this.#byLocality = this.#db.prepare(
91
+ this.#byLocality = prepareGet(
92
+ this.#db,
95
93
  `SELECT ${AGG_SELECT} FROM street_centroid WHERE locality_base = ? AND street_norm = ?`
96
94
  )
97
95
  }
@@ -99,19 +97,19 @@ export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
99
97
 
100
98
  find(query: { street: string; postcode?: string; locality?: string }): StreetCentroidHit | null {
101
99
  if (!this.#byPostcode || !this.#byLocality) return null
102
- const streetNorm = normalizeStreetForKeyLocale(query.street, this.#locale)
100
+ const streetNorm = normalizeStreetForKeyLocale(query.street, streetLocaleForSurface(query.street, this.#locale))
103
101
 
104
102
  if (!streetNorm) return null
105
103
 
106
104
  let row: AggRow | undefined
107
105
 
108
106
  if (query.postcode?.trim()) {
109
- row = this.#byPostcode.get(query.postcode.trim(), streetNorm) as AggRow | undefined
107
+ row = this.#byPostcode(query.postcode.trim(), streetNorm)
110
108
  }
111
109
 
112
110
  if ((!row || row.lat == null) && query.locality?.trim()) {
113
111
  const base = stripArrondissement(normalizeLocalityForKey(query.locality))
114
- row = this.#byLocality.get(base, streetNorm) as AggRow | undefined
112
+ row = this.#byLocality(base, streetNorm)
115
113
  }
116
114
 
117
115
  if (!row || row.lat == null || row.lon == null) return null
@@ -125,7 +123,7 @@ export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
125
123
  }
126
124
  }
127
125
 
128
- close(): void {
129
- this.#db.close()
126
+ [Symbol.dispose](): void {
127
+ this.#db[Symbol.dispose]()
130
128
  }
131
129
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "#street/centroid"
8
+ export * from "#street/centroid-schema"
9
+ export * from "#street/morphology-fst-builder"
10
+ export * from "#street/morphology-fst-loader"
11
+ export * from "#street/name-lookup"
12
+ export * from "#street/normalize"
13
+ export * from "#street/segment-schema"
@@ -23,14 +23,13 @@
23
23
  * `fst-street-morphology.bin`.
24
24
  */
25
25
 
26
- import { readdirSync, readFileSync, statSync } from "node:fs"
27
- import { join } from "node:path"
28
-
26
+ import { isDirectory, readDirectory, readLocalTextFile, statPath } from "@mailwoman/core/fs/readers"
27
+ import { join } from "path-ts"
29
28
  import { TextSpliterator } from "spliterator"
30
29
 
31
- import type { FSTNode } from "./fst-matcher.ts"
32
- import { FSTMatcher, normalizeTokens } from "./fst-matcher.ts"
33
- import type { FSTProvenance, PlaceEntry } from "./fst-types.ts"
30
+ import type { FSTNode } from "#fst/matcher"
31
+ import { FSTMatcher, normalizeTokens } from "#fst/matcher"
32
+ import type { FSTProvenance, PlaceEntry } from "#fst/types"
34
33
 
35
34
  /**
36
35
  * Reserved synthetic wofID base for street-morphology entries. 32-bit unsigned, well above any realistic WOF
@@ -96,7 +95,9 @@ function parseLine(line: string): { canonical: string; variants: string[] } | nu
96
95
  return { canonical: parts[0]!, variants: parts }
97
96
  }
98
97
 
99
- export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): BuildStreetMorphologyFSTResult {
98
+ export async function buildStreetMorphologyFST(
99
+ opts: BuildStreetMorphologyFSTOpts
100
+ ): Promise<BuildStreetMorphologyFSTResult> {
100
101
  const progress = opts.onProgress ?? (() => {})
101
102
  const minVariantLength = opts.minVariantLength ?? 3
102
103
 
@@ -107,19 +108,28 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
107
108
  if (opts.locales && opts.locales.length) {
108
109
  locales = opts.locales
109
110
  } else {
110
- locales = readdirSync(opts.dictionariesDir).filter((entry) => {
111
+ const entries = await readDirectory(opts.dictionariesDir)
112
+ const localeProbes: Array<[string, boolean]> = []
113
+
114
+ for (const entry of entries) {
111
115
  const localePath = join(opts.dictionariesDir, entry)
112
116
 
113
- if (!statSync(localePath).isDirectory()) return false
117
+ if (!(await isDirectory(localePath))) {
118
+ localeProbes.push([entry, false])
119
+
120
+ continue
121
+ }
114
122
 
115
123
  try {
116
- statSync(join(localePath, STREET_TYPES_FILENAME))
124
+ await statPath(join(localePath, STREET_TYPES_FILENAME))
117
125
 
118
- return true
126
+ localeProbes.push([entry, true])
119
127
  } catch {
120
- return false
128
+ localeProbes.push([entry, false])
121
129
  }
122
- })
130
+ }
131
+
132
+ locales = localeProbes.filter(([, hasFile]) => hasFile).map(([entry]) => entry)
123
133
  }
124
134
 
125
135
  progress("discover", `Found ${locales.length} locales with ${STREET_TYPES_FILENAME}`)
@@ -130,7 +140,7 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
130
140
 
131
141
  for (const locale of locales) {
132
142
  const filePath = join(opts.dictionariesDir, locale, STREET_TYPES_FILENAME)
133
- const content = readFileSync(filePath, "utf8")
143
+ const content = await readLocalTextFile(filePath)
134
144
 
135
145
  for (const line of TextSpliterator.from(content)) {
136
146
  const parsed = parseLine(line)