@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
@@ -1,720 +0,0 @@
1
- /**
2
- * @copyright Sister Software
3
- * @license AGPL-3.0
4
- * @author Teffen Ellis, et al.
5
- *
6
- * Node-side {@link PlaceLookup} over the byte-range CANDIDATE table (`build-candidate.ts`) — the
7
- * SAME gazetteer the browser demo resolves against ({@link WOFCandidateTableLookup} in
8
- * `docs/src/shared/httpvfs-resolver.ts`), but reading a LOCAL `candidate.db` via `node:sqlite`
9
- * instead of sql.js-httpvfs. This is what makes the server/CLI resolver match the demo: one
10
- * lookup surface, one artifact, one ranking.
11
- *
12
- * The query is a single contiguous probe on the `WITHOUT ROWID` B-tree keyed `(name_key,
13
- * country_id, region_id, placetype_id, neg_rank, spr_id)`. `name_key` is the SHARED
14
- * {@link normalizeLocalityForKey} (build- and query-consistent), each row is denormalized (display
15
- * `name`, centroid, bbox), and population rank is precomputed into `neg_rank` — so the result is
16
- * POPULATION-FIRST and COUNTRY-AGNOSTIC (when no `country` filter is given), exactly like the
17
- * demo. That's the deliberate divergence from {@link WOFSqlitePlaceLookup}'s FTS/bm25 ranking: a
18
- * bare "Moscow" resolves to the 10.4 M-pop Russian city, not whichever same-name US township bm25
19
- * floats to the top.
20
- *
21
- * Disambiguation rides the same mechanism the cascade already uses: a parsed region resolves to its
22
- * stored bbox and the locality query is point-in-bbox-filtered on the candidate centroid (the
23
- * `bbox` field on {@link FindPlaceQuery}).
24
- */
25
-
26
- import { DatabaseSync } from "node:sqlite"
27
-
28
- import { expandPlacetypeFilter, type GazetteerArtifactCoverage } from "@mailwoman/resolver"
29
-
30
- import { CANDIDATE_FTS_TABLE } from "./candidate-fts.ts"
31
- import type { CandidateTable, CountryCodeTable, PlacetypeCodeTable } from "./candidate-schema.ts"
32
- import { readGazetteerCoverageManifest } from "./coverage-manifest-schema.ts"
33
- import { haversineKm } from "./geo.ts"
34
- import { trigramJaccard } from "./lookup.ts"
35
- import { referentialFromPopulation } from "./place-importance-schema.ts"
36
- import { POSTAL_CITY_CANDIDATE_TABLE, type PostalCityCandidateTable } from "./postal-city-candidate-schema.ts"
37
- import { hasColumn, hasTable } from "./sqlite-utils.ts"
38
- import { normalizeLocalityForKey, stripLocalityQualifier } from "./street-normalize.ts"
39
- import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "./types.ts"
40
-
41
- export interface WOFCandidateTableLookupOpts {
42
- /**
43
- * Path to a `candidate.db` built by `build-candidate.ts`. Opened read-only.
44
- */
45
- databasePath?: string
46
- /**
47
- * Pre-opened handle (tests / shared connections). Mutually exclusive with `databasePath`.
48
- */
49
- database?: DatabaseSync
50
- }
51
-
52
- /**
53
- * The candidate columns this lookup probes — a typed projection of the SHARED {@link CandidateTable}, so a column rename
54
- * in `build-candidate` (the writer) is a compile error here (the reader).
55
- */
56
- type CandidateRow = Pick<
57
- CandidateTable,
58
- | "spr_id"
59
- | "name"
60
- | "country_id"
61
- | "placetype_id"
62
- | "latitude"
63
- | "longitude"
64
- | "min_lat"
65
- | "min_lon"
66
- | "max_lat"
67
- | "max_lon"
68
- | "neg_rank"
69
- | "is_primary"
70
- | "population"
71
- > &
72
- // `importance` is OPTIONAL on the row rather than `number | null`, because whether the SELECT names it
73
- // depends on the artifact: a candidate.db built before #28 has no such column and the probe leaves it
74
- // out (see `#importanceSelect`). `undefined` therefore means "this build cannot tell you", which is the
75
- // same answer as `null`'s "the score source had no measurement" — both are UNMEASURED, and the emit
76
- // below collapses them into the one thing the consumer understands: no `importance` field at all.
77
- Partial<Pick<CandidateTable, "importance">>
78
-
79
- /**
80
- * FTS5-trigram over-fetch before the trigram-Jaccard re-rank, and the minimum similarity to count as a fuzzy hit (below
81
- * it the trigram overlap is noise, e.g. unrelated same-trigram names). Tunable.
82
- */
83
- const FUZZY_FETCH = 40
84
- const FUZZY_MIN = 0.34
85
-
86
- /**
87
- * Postcode-containment re-rank gate radius (km) — the SAME value the resolver's country pass measures at
88
- * (`POSTCODE_COUNTRY_COHERENCE_GATE_KM`, resolver/postcode-country-coherence.ts): a locality within this distance of
89
- * the postcode's own centroid counts as "containing" it. One number, two passes — a divergence here would make the two
90
- * mechanisms disagree about what is proximal.
91
- */
92
- const POSTCODE_CONTAINMENT_GATE_KM = 25
93
-
94
- /**
95
- * Bounded PRIMARY-NAME preference across a CROSS-COUNTRY name collision (the `is_primary` ranking signal).
96
- *
97
- * The raw candidate order is population-first (`neg_rank ASC`) and treats an ALIAS row (a place's alt-name / exonym,
98
- * `is_primary=0`) and a PRIMARY-name row (`is_primary=1`) on equal footing. So a foreign place whose transliterated
99
- * exonym coincidentally normalizes to a query — Changchun CN stores the Turkish exonym "Çançun" (`name_key="cancun"`),
100
- * 4.19 M pop — outranks the PRIMARY-name place the query actually means (Cancún MX, 0.89 M pop). This penalty makes a
101
- * same-key alias have to clear a population MARGIN over a foreign primary before it wins.
102
- *
103
- * It is deliberately NOT a dominant sort key (no `ORDER BY is_primary DESC`, which would make every primary outrank
104
- * every alias and break the alt-names users depend on — "NYC"→New York, "LA"→Los Angeles, "Frisco"→San Francisco). Two
105
- * bounds keep it a soft prior:
106
- *
107
- * 1. **Cross-country only.** The penalty applies to an alias ONLY when the top-population primary sharing the key is in a
108
- * DIFFERENT country. A SAME-country nickname contest (San Francisco's alias "Frisco" vs the primary Frisco, TX —
109
- * both US) is left on pure population, so the legitimate alias still wins.
110
- * 2. **Population-bounded.** The penalty is {@link PRIMARY_PREFERENCE_LOG10} in log10-population units — an alias must be
111
- * at least 10x more populous than the foreign primary to still win. So a genuinely dominant alias keeps winning
112
- * ("Los Angeles" over La, Ghana — gap 1.6; "Las Vegas" over Vegas, Cuba — gap 2.4) while a near-tie coincidental
113
- * collision defers to the primary (Cancún over Changchun — gap 0.7).
114
- */
115
- const PRIMARY_PREFERENCE_LOG10 = 1
116
-
117
- /**
118
- * Over-fetch cap for {@link rankByPrimaryPreference}: the candidate rows for one `name_key` (all same-name places
119
- * worldwide) are re-ranked in-process, so the probe fetches this many (population-ordered) before the re-rank rather
120
- * than the caller's small `limit`, ensuring the intended primary isn't cut below the fold by a cluster of more-populous
121
- * foreign aliases. Bounded and small — a single contiguous B-tree scan.
122
- */
123
- const RERANK_FETCH = 64
124
-
125
- /**
126
- * A candidate row annotated with the {@link rankByPrimaryPreference} effective rank + the exact-tier demotion flag.
127
- */
128
- export type RankedRow<R> = R & {
129
- /**
130
- * `neg_rank` plus the bounded cross-country alias penalty — the value the row is ORDERED by, and the base the emitted
131
- * `prominence` is derived from (so the resolver walk's `prominence ?? score` sort, `resolve.ts`, agrees with this
132
- * order; the raw `score`/`neg_rank` is left intact for the walk's `minWinningScore` gate).
133
- */
134
- effectiveNegRank: number
135
- /**
136
- * True when this row is a cross-country alias that LOST the bounded population contest to the same-key primary — a
137
- * coincidental foreign exonym (Changchun's "Çançun" for "Cancun"). Such a row is dropped out of the exact-match tier
138
- * (`exactMatch=false`) so the resolver walk's country pin — the model's `anchorPosterior`, which "never crosses the
139
- * exact/partial boundary" (`resolve.ts`) — can't ride a spurious posterior (CN 0.86 for "Cancun") back over the
140
- * primary. Only the LOSING foreign alias is demoted; a dominant alias (Los Angeles over La, Ghana) keeps its exact
141
- * tier, and a same-country nickname (San Francisco's "Frisco") is never touched.
142
- */
143
- demoted: boolean
144
- /**
145
- * True when this row came from the TYPO-CORRECTOR tier — the FTS5-trigram fallback that fires only after the exact
146
- * and qualifier-strip probes both missed. Such a row answers a query the gazetteer does not contain, so it is a fuzzy
147
- * match by construction and must not claim `exactMatch` (#17). Recall is unaffected: the row is still returned, still
148
- * ranked, still resolvable — it just stops asserting a match quality it does not have, which is what the FTS backend
149
- * has always done and what every `exactMatch`-filtering consumer assumed.
150
- */
151
- fuzzy?: boolean
152
- }
153
-
154
- /**
155
- * Bounded cross-country primary-name preference (see {@link PRIMARY_PREFERENCE_LOG10}). Pure + total-ordered so
156
- * `candidate-lookup.test.ts` can exercise it on synthetic rows. `rows` arrive population-ordered (`neg_rank ASC`); an
157
- * alias (`is_primary=0`) is pushed back by `delta` in log10-population units ONLY when the top-population primary
158
- * sharing the key is in a different country, and is `demoted` out of the exact tier when that penalty leaves it BEHIND
159
- * the primary. Returns the top `limit` after the re-rank, each annotated.
160
- */
161
- export function rankByPrimaryPreference<R extends Pick<CandidateRow, "neg_rank" | "is_primary" | "country_id">>(
162
- rows: readonly R[],
163
- limit: number,
164
- delta = PRIMARY_PREFERENCE_LOG10
165
- ): Array<RankedRow<R>> {
166
- // The primary the alias actually competes with for the top slot: highest population (min neg_rank). Undefined
167
- // when the set has no primary → nothing to prefer, penalty is 0, order stays population-first (today's behavior).
168
- let topPrimary: R | undefined
169
-
170
- for (const r of rows) {
171
- if (r.is_primary === 1 && (topPrimary === undefined || r.neg_rank < topPrimary.neg_rank)) {
172
- topPrimary = r
173
- }
174
- }
175
-
176
- const topCountry = topPrimary?.country_id
177
-
178
- // A cross-country alias (different country than the top primary) is penalized; it is DEMOTED when even after — i.e.
179
- // the penalty leaves its effective rank behind the primary's raw rank (it lost the bounded population contest).
180
- const isCrossCountryAlias = (r: R): boolean =>
181
- topCountry !== undefined && r.is_primary !== 1 && r.country_id !== topCountry
182
-
183
- const annotate = (r: R): RankedRow<R> => {
184
- const penalized = isCrossCountryAlias(r)
185
- const effectiveNegRank = r.neg_rank + (penalized ? delta : 0)
186
-
187
- return { ...r, effectiveNegRank, demoted: penalized && effectiveNegRank > topPrimary!.neg_rank }
188
- }
189
-
190
- return (
191
- rows
192
- .map((r, i) => ({ row: annotate(r), i }))
193
- // Effective rank ASC; ties keep population order, then original index (stable).
194
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
195
- .sort((a, b) => a.row.effectiveNegRank - b.row.effectiveNegRank || a.row.neg_rank - b.row.neg_rank || a.i - b.i)
196
- .slice(0, limit)
197
- .map((x) => x.row)
198
- )
199
- }
200
-
201
- /**
202
- * Unpadded character-trigrams of `s`, OR'd into an FTS5 trigram MATCH query (each quoted so FTS treats it as a literal
203
- * term). Returns "" when `s` is shorter than a trigram or yields no clean grams — the caller then skips the fuzzy
204
- * probe.
205
- */
206
- function ftsTrigramQuery(s: string): string {
207
- const grams = new Set<string>()
208
-
209
- for (let i = 0; i + 3 <= s.length; i++) {
210
- const g = s.slice(i, i + 3)
211
-
212
- if (/^[\p{L}\p{N} ]{3}$/u.test(g)) {
213
- grams.add(g)
214
- }
215
- }
216
-
217
- return [...grams].map((g) => `"${g}"`).join(" OR ")
218
- }
219
-
220
- /**
221
- * Node {@link PlaceLookup} over `candidate.db`. Drop-in for {@link WOFSqlitePlaceLookup} in `createWOFResolver(backend)`
222
- * — same `findPlace` contract, population-first ranking.
223
- */
224
- export class WOFCandidateTableLookup implements PlaceLookup {
225
- #db: DatabaseSync
226
- #ownsDB: boolean
227
- readonly #countryToID = new Map<string, number>()
228
- readonly #idToCountry = new Map<number, string>()
229
- readonly #placetypeToID = new Map<string, number>()
230
- readonly #idToPlacetype = new Map<number, string>()
231
- /**
232
- * Prepared `(name_key, postcode)` probe for the #741 postal-city side-index — `undefined` when the
233
- * `postal_city_candidate` table isn't present, so a candidate.db built without it is byte-stable.
234
- */
235
- readonly #postalCityProbe: ReturnType<DatabaseSync["prepare"]> | undefined
236
- /**
237
- * Prepared FTS5-trigram MATCH probe for the typo-tolerant fallback — `undefined` when the `candidate_fts` index isn't
238
- * present, so a candidate.db built without it is byte-stable (the fuzzy path is skipped, exactly like today).
239
- */
240
- readonly #ftsProbe: ReturnType<DatabaseSync["prepare"]> | undefined
241
- /**
242
- * Prepared UNFILTERED existence probe (`name_key` present anywhere, ignoring country/placetype/bbox). Gates the fuzzy
243
- * fallback: fuzzy is a TYPO corrector, so it engages only when the name doesn't exist in the gazetteer at all. A name
244
- * that DOES exist but missed under the active filter is a filter miss (e.g. a placer misroute "Vienna, Austria"→IT),
245
- * not a spelling miss — fuzzing it would scrape an unrelated same-country place and defeat the cascade's
246
- * country-agnostic retry. Prepared only alongside `#ftsProbe`.
247
- */
248
- readonly #nameKeyExistsProbe: ReturnType<DatabaseSync["prepare"]> | undefined
249
- /**
250
- * Facts this candidate DB declares about itself — the coverage manifest (`country_coverage` + `country_bbox`) the
251
- * gazetteer build emits, read once at open. `undefined` when the artifact predates the manifest, so every consumer
252
- * (the hard-country coverage gate, guard-B plausibility) falls back to its code constants byte-identically.
253
- */
254
- readonly artifactCoverage: GazetteerArtifactCoverage | undefined
255
- /**
256
- * `", importance"` when this artifact carries the #28 fame column, `""` when it does not — spliced into the probe's
257
- * SELECT list. Existence-gated exactly like `#ftsProbe` and `#postalCityProbe` above, and for the same reason: a
258
- * candidate.db built before the column is a valid artifact, and naming a column it lacks would turn a stale gazetteer
259
- * into `no such column` on the first keystroke rather than into "no fame signal", which is what it is.
260
- */
261
- readonly #importanceSelect: string
262
-
263
- constructor(opts: WOFCandidateTableLookupOpts) {
264
- if (opts.database) {
265
- this.#db = opts.database
266
- this.#ownsDB = false
267
- } else if (opts.databasePath) {
268
- this.#db = new DatabaseSync(opts.databasePath, { readOnly: true })
269
- this.#ownsDB = true
270
- } else {
271
- throw new Error("WOFCandidateTableLookup needs `databasePath` or `database`")
272
- }
273
-
274
- // The code tables are tiny (country/placetype dictionaries) — load them once at construction so
275
- // `findPlace` is a single B-tree probe with no dictionary round-trip.
276
- for (const r of this.#db.prepare("SELECT id, code FROM country_codes").all() as unknown as CountryCodeTable[]) {
277
- const code = String(r.code).toUpperCase()
278
- this.#countryToID.set(code, Number(r.id))
279
- this.#idToCountry.set(Number(r.id), code)
280
- }
281
-
282
- for (const r of this.#db
283
- .prepare("SELECT id, placetype FROM placetype_codes")
284
- .all() as unknown as PlacetypeCodeTable[]) {
285
- this.#placetypeToID.set(String(r.placetype), Number(r.id))
286
- this.#idToPlacetype.set(Number(r.id), String(r.placetype))
287
- }
288
-
289
- // #741 postal-city side-index: prepare the exact probe only if the table is present. Absent →
290
- // `#postalCityProbe` stays undefined → findPlace skips the postal-city path → byte-stable.
291
- if (hasTable(this.#db, POSTAL_CITY_CANDIDATE_TABLE)) {
292
- this.#postalCityProbe = this.#db.prepare(
293
- `SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`
294
- )
295
- }
296
-
297
- // FTS5-trigram fuzzy fallback: prepare the MATCH probe only if the index is present (the unified
298
- // gazetteer carries it; an older candidate.db doesn't → the fuzzy path is skipped, byte-stable).
299
- if (hasTable(this.#db, CANDIDATE_FTS_TABLE)) {
300
- this.#ftsProbe = this.#db.prepare(
301
- `SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`
302
- )
303
-
304
- this.#nameKeyExistsProbe = this.#db.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1")
305
- }
306
-
307
- // #28 fame column: probed ONCE here (it runs a PRAGMA, and `findPlace` is per-keystroke hot).
308
- this.#importanceSelect = hasColumn(this.#db, "candidate", "importance") ? ", importance" : ""
309
-
310
- // Coverage manifest (survey candidate #2): the artifact's own coverage facts, existence-gated like
311
- // the probes above — a candidate.db built before the manifest reads `undefined` and consumers keep
312
- // their code-constant fallbacks byte-identically.
313
- this.artifactCoverage = readGazetteerCoverageManifest(this.#db)
314
- }
315
-
316
- /**
317
- * Does this query want a locality-tier place? Postal-city aliases (#741) are all localities.
318
- */
319
- #wantsLocality(placetype: FindPlaceQuery["placetype"]): boolean {
320
- if (!placetype) return true
321
- const want = Array.isArray(placetype) ? placetype : [placetype]
322
-
323
- return expandPlacetypeFilter(want as readonly string[]).includes("locality")
324
- }
325
-
326
- /**
327
- * The postcode-containment anchor: the postcode's own centroid row in the candidate table, keyed whitespace-stripped
328
- * (#920 — the same fold the build applies to postcode rows), country-scoped when the query is, first
329
- * coordinate-bearing row wins. null when the candidate table carries no such postcode — the re-rank then abstains,
330
- * because a recall gap is not evidence for the name match. Meaning-of-zero: a 0,0 row is the build's unlocated
331
- * sentinel, never a real centroid.
332
- */
333
- #postcodeAnchor(postcode: string, country?: string): { lat: number; lon: number } | null {
334
- const placetypeID = this.#placetypeToID.get("postalcode")
335
-
336
- if (placetypeID === undefined) return null
337
-
338
- const conds = ["name_key = ?", "placetype_id = ?"]
339
- const params: Array<string | number> = [postcode.replaceAll(/\s+/g, ""), placetypeID]
340
-
341
- if (country) {
342
- const countryID = this.#countryToID.get(country.toUpperCase())
343
-
344
- if (countryID === undefined) return null // a country the candidate table doesn't carry
345
- conds.push("country_id = ?")
346
- params.push(countryID)
347
- }
348
-
349
- const row = this.#db
350
- .prepare(`SELECT latitude, longitude FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT 1`)
351
- .get(...params) as { latitude: number; longitude: number } | undefined
352
-
353
- if (!row || (Number(row.latitude) === 0 && Number(row.longitude) === 0)) return null
354
-
355
- return { lat: Number(row.latitude), lon: Number(row.longitude) }
356
- }
357
-
358
- async findPlace(query: FindPlaceQuery): Promise<PlaceCandidate[]> {
359
- let text = (query.text ?? "").trim()
360
-
361
- if (!text) return []
362
-
363
- // #920 name law, candidate-key edition: postcode rows are keyed by their whitespace-stripped
364
- // form at build (the GeoNames fold normalizes '624 66' → '62466'), so a postcode-typed query
365
- // strips internal whitespace before keying. Postcode-only — locality names keep their spaces.
366
- const wantsPostcode = [query.placetype].flat().includes("postalcode")
367
-
368
- if (wantsPostcode) {
369
- text = text.replaceAll(/\s+/g, "")
370
- }
371
-
372
- const nameKey = normalizeLocalityForKey(text)
373
-
374
- if (!nameKey) return []
375
-
376
- // #741: postcode-keyed postal-city alias. An exact `(name_key, postcode)` hit resolves a
377
- // user-typed POSTAL city ("Antioch", 37013) to the geographic locality the postcode sits in
378
- // ("Nashville"), bypassing the population/region ranking that can't see the postcode. Gated on
379
- // the side-index being present, a postcode in the query, and a locality-tier request — so the
380
- // common (no-postcode / non-locality) path is untouched. A hit short-circuits: the postcode is
381
- // an exact, high-confidence disambiguator, so we return the single geographic locality.
382
- if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
383
- const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim()) as
384
- | Pick<PostalCityCandidateTable, "spr_id" | "name" | "latitude" | "longitude">
385
- | undefined
386
-
387
- if (hit) {
388
- return [
389
- {
390
- id: Number(hit.spr_id),
391
- name: String(hit.name ?? ""),
392
- placetype: "locality" as WOFPlacetype,
393
- country: query.country?.toUpperCase() ?? "",
394
- lat: Number(hit.latitude),
395
- lon: Number(hit.longitude),
396
- score: 1,
397
- exactMatch: true,
398
- },
399
- ]
400
- }
401
- }
402
-
403
- const limit = Math.max(1, query.limit ?? 10)
404
-
405
- // Filter conds shared by the exact-key + strip-fallback probes (everything but name_key).
406
- const filters: string[] = []
407
- const filterParams: Array<string | number> = []
408
-
409
- if (query.country) {
410
- const cid = this.#countryToID.get(query.country.toUpperCase())
411
-
412
- if (cid === undefined) return [] // a country the candidate table doesn't carry
413
- filters.push("country_id = ?")
414
- filterParams.push(cid)
415
- }
416
-
417
- if (query.placetype) {
418
- // Shared placetype-equivalence expansion (a `locality` query must also reach borough /
419
- // localadmin). `postalcode` maps to no admin placetype here → empty → no rows.
420
- const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype]
421
-
422
- const ids = expandPlacetypeFilter(want as readonly string[])
423
- .map((t) => this.#placetypeToID.get(t))
424
- .filter((v): v is number => v !== undefined)
425
-
426
- if (!ids.length) return []
427
- filters.push(`placetype_id IN (${ids.map(() => "?").join(",")})`)
428
- filterParams.push(...ids)
429
- }
430
-
431
- if (query.bbox) {
432
- const b = query.bbox
433
- filters.push("latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?")
434
- filterParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon)
435
- }
436
-
437
- // Region scope: when the cascade resolves a region and passes it down as `parentID` (the walk sets
438
- // `query.parentID = parentResolved.id`), the candidate build stamps each place's region-tier ancestor
439
- // id into `region_id` (build-candidate.ts `regionOf`), and that id equals the resolved region's WOF id
440
- // — so `region_id = parentID` scopes the probe to in-region rows. Without it a bare same-name probe is
441
- // population-first and "Springfield, IL" (parentID = Illinois) drops to the larger Springfield, MO.
442
- // Kept OUT of the shared `filters` so a region MISS falls back to the unscoped cascade below: a
443
- // country/non-region parent (no `region_id` match), a `region_id=0` row (place with no region
444
- // ancestor), or a wrong parent degrades to today's behavior — never worse, recall-safe by construction.
445
- const regionParentID = query.parentID || undefined
446
-
447
- const probe = (nk: string, regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
448
- const conds = ["name_key = ?", ...filters]
449
- const params: Array<string | number> = [nk, ...filterParams]
450
-
451
- if (regionID !== undefined) {
452
- conds.push("region_id = ?")
453
- params.push(regionID)
454
- }
455
-
456
- // Fetch population-ordered (the clustered-key order — a cheap ordered scan), over-fetching to
457
- // RERANK_FETCH so the bounded cross-country primary-preference re-rank (below) can promote the
458
- // intended primary even when a cluster of more-populous foreign aliases sits ahead of it. `is_primary`
459
- // + `country_id` feed that re-rank. A single-country probe (a country filter, or all rows same
460
- // country) re-ranks to the identical population order, so the common path is untouched.
461
- // `population` rides along for the REFERENTIAL score on the result (ROAD_TO_V9 §2) — one more
462
- // column off a clustered row the probe already reads, and it is NOT what the probe orders by:
463
- // `neg_rank` remains the sort key, so this changes no ordering, only what the result reports.
464
- // `importance` (#28) rides along on the same terms, and there is deliberately NO `ORDER BY` on it:
465
- // the fame prior is applied by the RESOLVER (`resolver/toponym-prior.ts`), which alone knows
466
- // whether the query was bare enough to deserve it. A backend that pre-sorted by fame would apply
467
- // it to every lookup, including the qualified addresses the D-rule guard exists to protect.
468
- const sql =
469
- "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
470
- `${this.#importanceSelect} FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`
471
-
472
- const fetched = this.#db.prepare(sql).all(...params, Math.max(limit, RERANK_FETCH)) as unknown as CandidateRow[]
473
-
474
- return rankByPrimaryPreference(fetched, limit)
475
- }
476
-
477
- // The exact → qualifier-strip → typo-fuzzy probe cascade, run at a fixed region scope. Region scoping
478
- // only tightens an already-population-first pick, so a region MISS re-runs the whole cascade unscoped
479
- // (below) rather than dropping a place that has no in-region row.
480
- const cascade = (regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
481
- let rows = probe(nameKey, regionID)
482
-
483
- if (!rows.length) {
484
- // Query-side qualifier-strip fallback: an OA locality with a qualifier the gazetteer's
485
- // canonical name omits ("Lenk im Simmental" → "Lenk", "Roche VD"). Tried ONLY on an exact
486
- // miss; the cascade's region bbox disambiguates any base-name ambiguity.
487
- const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text))
488
-
489
- if (strippedKey && strippedKey !== nameKey) {
490
- rows = probe(strippedKey, regionID)
491
- }
492
- }
493
-
494
- // Typo-tolerant fallback (the unified gazetteer's fuzzy mode): an exact + strip miss may be a
495
- // misspelling the normalized key can't reach. FTS5-trigram fetches a loose set; we re-rank by
496
- // trigram-Jaccard (the admin backend's measure) and probe the best name_keys, so a typo resolves
497
- // the same on either backend. The country/placetype/bbox/region filters still apply via `probe`.
498
- // Skipped when the index is absent (byte-stable for an older candidate.db).
499
- //
500
- // Gate: only when the name doesn't exist in the gazetteer AT ALL (unfiltered). A name that exists
501
- // but missed under the active country/placetype/bbox filter is a FILTER miss, not a spelling miss
502
- // — fuzzing it scrapes an unrelated same-filter place ("Vienna, Austria" misrouted to IT would
503
- // pull a tiny Italian name_key near Siena) and masks the cascade's country-agnostic retry that
504
- // correctly lands population-first Vienna AT. The exact/strip probes already covered the real name.
505
- //
506
- // NEVER for postcodes: fuzzy is a typo corrector for place NAMES, and a "corrected" postcode is a
507
- // DIFFERENT postcode. The 2026-08-05 Code-Point swap exposed the trap at scale: Northern Ireland's
508
- // `BT3 9QQ` (absent — no permissive NI source) trigram-matched Sheffield's `S3 9QQ` (Jaccard 0.4
509
- // on {39q, 9qq}) and resolved 200+ km wrong with full confidence. An unknown postcode must abstain.
510
- if (
511
- !rows.length &&
512
- !wantsPostcode &&
513
- this.#ftsProbe &&
514
- this.#nameKeyExistsProbe &&
515
- !this.#nameKeyExistsProbe.get(nameKey)
516
- ) {
517
- const match = ftsTrigramQuery(nameKey)
518
-
519
- if (match) {
520
- const hits = this.#ftsProbe.all(match, FUZZY_FETCH) as unknown as Array<{ name_key: string }>
521
-
522
- const ranked = hits
523
- .map((h) => ({ nk: String(h.name_key), s: trigramJaccard(nameKey, String(h.name_key)) }))
524
- .filter((h) => h.s >= FUZZY_MIN)
525
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
526
- .sort((a, b) => b.s - a.s)
527
-
528
- const seen = new Set<string>()
529
-
530
- for (const h of ranked) {
531
- if (seen.has(h.nk)) continue
532
- seen.add(h.nk)
533
- // #17: stamp the tier. These rows answer a name the gazetteer does not carry, so they are
534
- // fuzzy matches and `exactMatch` below must say so — see `RankedRow.fuzzy`.
535
- rows.push(...probe(h.nk, regionID).map((r) => ({ ...r, fuzzy: true })))
536
-
537
- if (rows.length >= limit) break
538
- }
539
-
540
- rows = rows.slice(0, limit)
541
- }
542
- }
543
-
544
- return rows
545
- }
546
-
547
- let rows = cascade(regionParentID)
548
-
549
- // Region-scope fallback: if scoping to the parent region found nothing across the whole cascade, retry
550
- // unscoped so a place with no in-region row (missing ancestry, or a country/non-region parent) still
551
- // resolves exactly as it does today. Only when a region scope was actually applied.
552
- if (!rows.length && regionParentID !== undefined) {
553
- rows = cascade(undefined)
554
- }
555
-
556
- // Postcode-containment coherence (#31, Mechanism 2): re-rank the rows by proximity to the postcode's
557
- // own centroid, so the locality that CONTAINS the postcode wins the name-match tie (the "Paris" that
558
- // holds 75001, not the one that holds a 75001-free namesake). The resolver sends this flag on locality
559
- // lookups when `ResolveOpts.postcodeContainmentCoherence` is on. Strictly beneath the #741 postal-city
560
- // short-circuit above — an exact (name, postcode) hit IS the answer and outranks any re-rank — and after
561
- // the region-scope fallback, so it sees the final row set. Rows within the gate sort by distance first;
562
- // the out-gate tail keeps its original population-first order. No in-gate row, or no postcode row in
563
- // the candidate table → unchanged (byte-identical to the flag-off path).
564
- if (
565
- query.postcode &&
566
- query.postcodeContainmentCoherence === true &&
567
- this.#wantsLocality(query.placetype) &&
568
- rows.length > 1
569
- ) {
570
- const anchor = this.#postcodeAnchor(query.postcode, query.country)
571
-
572
- if (anchor) {
573
- const inGate: Array<{ row: RankedRow<CandidateRow>; distanceKm: number }> = []
574
- const outGate: RankedRow<CandidateRow>[] = []
575
-
576
- for (const row of rows) {
577
- const distanceKm = haversineKm(anchor.lat, anchor.lon, Number(row.latitude), Number(row.longitude))
578
-
579
- if (distanceKm <= POSTCODE_CONTAINMENT_GATE_KM) {
580
- inGate.push({ row, distanceKm })
581
- } else {
582
- outGate.push(row)
583
- }
584
- }
585
-
586
- if (inGate.length) {
587
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
588
- inGate.sort((a, b) => a.distanceKm - b.distanceKm)
589
- rows = [...inGate.map(({ row }) => row), ...outGate]
590
- }
591
- }
592
- }
593
-
594
- const candidates = rows.map((row): PlaceCandidate => {
595
- const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
596
-
597
- return {
598
- id: Number(row.spr_id),
599
- name: String(row.name ?? ""),
600
- placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? "") as WOFPlacetype,
601
- // Surfaced so the cascade can country-gate a postcode by the resolved locality (an ambiguous
602
- // international postcode like 10115 = Berlin DE AND New York US must not out-resolve the city).
603
- country: this.#idToCountry.get(Number(row.country_id)) ?? "",
604
- lat: Number(row.latitude),
605
- lon: Number(row.longitude),
606
- // `score` stays the RAW population rank (`-neg_rank`) — it feeds the resolver walk's absolute
607
- // `minWinningScore` gate (`resolve.ts`), which must see real prominence, never a penalized value.
608
- score: -Number(row.neg_rank),
609
- // `prominence` carries the bounded cross-country primary preference (the effective, penalty-adjusted
610
- // rank). The walk ORDERS candidates by `prominence ?? score` (`resolve.ts`), so this is what makes the
611
- // re-rank actually stick through resolution — without it the walk re-sorts by raw `score` and a
612
- // more-populous foreign alias (Changchun for "Cancun") wins back the node. Equals `score` for every
613
- // un-penalized row (primaries + same-country aliases), so the common ordering is unchanged.
614
- prominence: -Number(row.effectiveNegRank),
615
- // Every candidate row IS an exact normalized-name (or alias/abbrev) match — the cascade's exact tier
616
- // accepts alias-exact hits ("New York City" → New York) the same as canonical — EXCEPT a cross-country
617
- // alias that lost the bounded contest to a same-key primary (`demoted`): it drops to the partial tier so
618
- // the walk's country posterior can't cross back over the primary (see `RankedRow.demoted`) — and
619
- // EXCEPT a row the typo-corrector produced (`fuzzy`), which by definition answers a name the
620
- // gazetteer does not carry (see `RankedRow.fuzzy`).
621
- exactMatch: !row.demoted && !row.fuzzy,
622
- // The two-score split's carry (ROAD_TO_V9 §2). `referential` names the prominence this
623
- // backend has always ordered by — `neg_rank` IS `-log10(population + 1)`, so the score and
624
- // the sort key are two readings of the same number.
625
- ...(row.population === null || row.population <= 0
626
- ? {}
627
- : { population: row.population, referential: referentialFromPopulation(row.population) }),
628
- // #28: the fame prior, from the `importance` column the candidate build joins in. Emitted ONLY
629
- // when the artifact measured this place — an absent field is what `rankByImportance` reads as
630
- // "does not participate", and a 0 would be a claim nobody made (meaning-of-zero).
631
- //
632
- // The field name matches the column because they hold the same thing: the score source's
633
- // BLENDED prior — the concordance's encyclopedia-derived channel where a concordance matched, a
634
- // population-derived proxy everywhere else. It is NOT the strict `encyclopedic` channel
635
- // `place-importance-schema.ts` defines, and it deliberately does not land in that field: the
636
- // strict channel was measured on 2026-08-10 and covers eleven countries, none of them CA/AU/RU,
637
- // which makes it inert on three of the four homonym contests the prior exists to settle.
638
- // `PlaceCandidate.encyclopedic` stays reserved for a strict-channel source (the FTS backend's
639
- // clauses are strict and today emit NULL for everything — no shipped admin DB has the split
640
- // table at all). See `candidate-schema.ts` → {@link CandidateTable.importance}.
641
- ...(typeof row.importance === "number" && Number.isFinite(row.importance)
642
- ? { importance: row.importance }
643
- : {}),
644
- ...(hasBbox
645
- ? {
646
- bbox: {
647
- minLat: Number(row.min_lat),
648
- maxLat: Number(row.max_lat),
649
- minLon: Number(row.min_lon),
650
- maxLon: Number(row.max_lon),
651
- },
652
- }
653
- : {}),
654
- }
655
- })
656
-
657
- // Proximity re-rank (#938): with bias hints (the demo's map viewport / user location), re-sort the
658
- // exact-match candidates by the SAME prominence the FTS server uses (lookup.ts) — population and
659
- // nearness in one additive scale — so an in-view namesake wins a tie without a hard filter. Byte-
660
- // identical to the plain population order when no bias is passed. `score` here is -neg_rank =
661
- // log10(population + 1), so popTerm is the server formula read straight off it. Constants MIRROR
662
- // lookup.ts's DEFAULT_WEIGHTS (biasBoost 4, populationBoost 4, populationScaleLog10 6,
663
- // proximityScaleKm 100) — the #861 server↔demo parity contract; keep them in lockstep.
664
- if (query.bias && query.bias.length) {
665
- const BIAS_BOOST = 4
666
- const POP_BOOST = 4
667
- const POP_SCALE_LOG10 = 6
668
- // SHARPER than lookup.ts's 100 km on purpose: this backend's `score` is log-population ALONE
669
- // (no bm25 document term), so the population signal is weaker relative to the bias and the
670
- // gentle 100 km decay let a 230 km-distant alias-exact township ("Paris Township", OH) edge
671
- // out a global city ("Paris", FR) from a nearby view. A ~30 km scale keeps the boost to
672
- // candidates the user is actually LOOKING at — an in-view namesake still wins (Dublin, OH from
673
- // an Ohio view), a distant one no longer does (Paris stays FR from a Michigan view).
674
- const PROX_SCALE_KM = 30
675
-
676
- const combinedProminence = (c: PlaceCandidate): number => {
677
- // Population base is the PENALIZED `prominence` (set above = -effectiveNegRank), not raw `score`, so
678
- // the cross-country primary preference carries into the bias-weighted order too — a coincidental
679
- // foreign alias doesn't ride population back over a primary just because a viewport hint is present.
680
- const popBase = c.prominence ?? c.score
681
- const popTerm = POP_BOOST * Math.min(1, Math.max(0, popBase) / POP_SCALE_LOG10)
682
- let proxTerm = 0
683
-
684
- if (!(c.lat === 0 && c.lon === 0)) {
685
- for (const b of query.bias!) {
686
- const d = haversineKm(b.lat, b.lon, c.lat, c.lon)
687
- const term = (BIAS_BOOST * (b.weight ?? 1)) / (1 + d / PROX_SCALE_KM)
688
-
689
- if (term > proxTerm) {
690
- proxTerm = term
691
- }
692
- }
693
- }
694
-
695
- return popTerm + proxTerm
696
- }
697
-
698
- // Persist the combined value into `prominence` so the resolver walk's `prominence ?? score` sort (and any
699
- // other node consumer) honors the bias order — then sort. Stable within equal prominence (preserves the
700
- // population order the B-tree already gave).
701
- candidates
702
- .map((c, i) => {
703
- c.prominence = combinedProminence(c)
704
-
705
- return { c, i, p: c.prominence }
706
- })
707
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
708
- .sort((a, b) => b.p - a.p || a.i - b.i)
709
- .forEach((x, j) => (candidates[j] = x.c))
710
- }
711
-
712
- return candidates
713
- }
714
-
715
- close(): void {
716
- if (this.#ownsDB) {
717
- this.#db.close()
718
- }
719
- }
720
- }