@mailwoman/resolver-wof-sqlite 9.4.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (548) hide show
  1. package/README.md +8 -8
  2. package/lib/address/{point-interpolation.ts → point/interpolation.ts} +40 -38
  3. package/lib/address/point/schema.ts +132 -0
  4. package/lib/address/point.ts +68 -81
  5. package/lib/{address/index.ts → address.ts} +2 -2
  6. package/lib/ancestry/backfill.ts +24 -88
  7. package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
  8. package/lib/build/candidate.ts +608 -0
  9. package/lib/build/population-corrections.ts +64 -0
  10. package/lib/build/slim.ts +338 -0
  11. package/lib/candidate/alias-bags.ts +74 -14
  12. package/lib/candidate/ancestors/schema.ts +211 -0
  13. package/lib/candidate/{ancestors-sidecar.ts → ancestors/sidecar.ts} +17 -24
  14. package/lib/candidate/country-display-names.ts +6 -20
  15. package/lib/candidate/extract-fold.ts +12 -57
  16. package/lib/candidate/fts.ts +28 -0
  17. package/lib/candidate/importance.ts +221 -0
  18. package/lib/candidate/lookup.ts +669 -0
  19. package/lib/candidate/name-roles.ts +16 -34
  20. package/lib/candidate/own-name.ts +34 -31
  21. package/lib/candidate/place-attrs.ts +20 -13
  22. package/lib/candidate/schema.ts +211 -0
  23. package/lib/candidate/scoring.ts +222 -0
  24. package/lib/capital-schema.ts +21 -17
  25. package/lib/capitals.ts +26 -24
  26. package/lib/coincident-roles.ts +74 -55
  27. package/lib/convention/schema.ts +13 -7
  28. package/lib/convention.ts +144 -0
  29. package/lib/coverage-manifest-schema.ts +34 -39
  30. package/lib/currency-backfill.ts +24 -69
  31. package/lib/env.ts +1 -1
  32. package/lib/exact-match.ts +31 -20
  33. package/lib/extracts.ts +96 -73
  34. package/lib/fst/autocomplete.ts +52 -35
  35. package/lib/fst/builder.ts +30 -36
  36. package/lib/fst/deserialize-web.ts +8 -9
  37. package/lib/fst/format.ts +41 -23
  38. package/lib/fst/freshness.ts +17 -95
  39. package/lib/fst/matcher.ts +2 -2
  40. package/lib/fst/serialize.ts +45 -60
  41. package/lib/fst/types.ts +71 -59
  42. package/lib/fts/query.ts +5 -36
  43. package/lib/fts.ts +280 -0
  44. package/lib/geonames/aliases.ts +72 -149
  45. package/lib/geonames/postal.ts +162 -77
  46. package/lib/index.ts +20 -27
  47. package/lib/interpolation.ts +43 -96
  48. package/lib/lookup.ts +137 -257
  49. package/lib/name-score.ts +9 -7
  50. package/lib/nsul/lookup.ts +36 -53
  51. package/lib/nsul/schema.ts +16 -43
  52. package/lib/paths.ts +159 -0
  53. package/lib/place-importance-schema.ts +57 -164
  54. package/lib/poi/lookup.ts +100 -109
  55. package/lib/poi/schema.ts +42 -26
  56. package/lib/polygon-schema.ts +10 -6
  57. package/lib/postal/city/alias/lookup.ts +79 -0
  58. package/lib/postal/{city-alias-schema.ts → city/alias/schema.ts} +14 -10
  59. package/lib/postal/{city-candidate-schema.ts → city/candidate-schema.ts} +26 -20
  60. package/lib/postal.ts +9 -0
  61. package/lib/postcode-point-lookup.ts +9 -19
  62. package/lib/primary-preference.ts +45 -125
  63. package/lib/proximity-rerank.ts +43 -32
  64. package/lib/ranking-weights.ts +98 -76
  65. package/lib/region-keys.ts +68 -40
  66. package/lib/reverse.ts +108 -72
  67. package/lib/schema.ts +74 -42
  68. package/lib/search-fetch.ts +90 -68
  69. package/lib/sqlite/convention-source.ts +57 -0
  70. package/lib/sqlite/utils.ts +79 -0
  71. package/lib/street/centroid/schema.ts +149 -0
  72. package/lib/street/centroid.ts +16 -25
  73. package/lib/street/{morphology-fst-builder.ts → morphology/fst/builder.ts} +56 -41
  74. package/lib/street/{morphology-fst-loader.ts → morphology/fst/loader.ts} +36 -29
  75. package/lib/street/name-lookup.ts +43 -35
  76. package/lib/street/normalize.ts +85 -216
  77. package/lib/street/segment-schema.ts +116 -43
  78. package/lib/{street/index.ts → street.ts} +3 -3
  79. package/lib/types.ts +94 -140
  80. package/lib/unified-schema.ts +29 -29
  81. package/lib/uprn/existence.ts +30 -19
  82. package/lib/uprn/lookup.ts +28 -67
  83. package/lib/uprn/schema.ts +18 -40
  84. package/lib/weights-overlay-linker.ts +204 -232
  85. package/out/address/point/interpolation.d.ts +45 -0
  86. package/out/address/point/interpolation.d.ts.map +1 -0
  87. package/out/address/{point-interpolation.js → point/interpolation.js} +38 -38
  88. package/out/address/point/interpolation.js.map +1 -0
  89. package/out/address/point/schema.d.ts +74 -0
  90. package/out/address/point/schema.d.ts.map +1 -0
  91. package/out/address/{point-schema.js → point/schema.js} +4 -20
  92. package/out/address/point/schema.js.map +1 -0
  93. package/out/address/point.d.ts +24 -21
  94. package/out/address/point.d.ts.map +1 -1
  95. package/out/address/point.js +60 -77
  96. package/out/address/point.js.map +1 -1
  97. package/out/{address/index.js → address.d.ts} +3 -3
  98. package/out/address.d.ts.map +1 -0
  99. package/out/{address/index.d.ts → address.js} +3 -3
  100. package/out/address.js.map +1 -0
  101. package/out/ancestry/backfill.d.ts +11 -64
  102. package/out/ancestry/backfill.d.ts.map +1 -1
  103. package/out/ancestry/backfill.js +18 -76
  104. package/out/ancestry/backfill.js.map +1 -1
  105. package/out/ancestry.d.ts +49 -0
  106. package/out/ancestry.d.ts.map +1 -0
  107. package/out/ancestry.js +59 -0
  108. package/out/ancestry.js.map +1 -0
  109. package/out/build/candidate.d.ts +170 -0
  110. package/out/build/candidate.d.ts.map +1 -0
  111. package/out/{build-candidate.js → build/candidate.js} +33 -102
  112. package/out/build/candidate.js.map +1 -0
  113. package/out/build/population-corrections.d.ts +52 -0
  114. package/out/build/population-corrections.d.ts.map +1 -0
  115. package/out/build/population-corrections.js +40 -0
  116. package/out/build/population-corrections.js.map +1 -0
  117. package/out/build/slim.d.ts +44 -0
  118. package/out/build/slim.d.ts.map +1 -0
  119. package/out/{build-slim.js → build/slim.js} +33 -94
  120. package/out/build/slim.js.map +1 -0
  121. package/out/candidate/alias-bags.d.ts +13 -5
  122. package/out/candidate/alias-bags.d.ts.map +1 -1
  123. package/out/candidate/alias-bags.js +55 -12
  124. package/out/candidate/alias-bags.js.map +1 -1
  125. package/out/candidate/ancestors/schema.d.ts +166 -0
  126. package/out/candidate/ancestors/schema.d.ts.map +1 -0
  127. package/out/candidate/ancestors/schema.js +131 -0
  128. package/out/candidate/ancestors/schema.js.map +1 -0
  129. package/out/candidate/ancestors/sidecar.d.ts +27 -0
  130. package/out/candidate/ancestors/sidecar.d.ts.map +1 -0
  131. package/out/candidate/{ancestors-sidecar.js → ancestors/sidecar.js} +17 -24
  132. package/out/candidate/ancestors/sidecar.js.map +1 -0
  133. package/out/candidate/country-display-names.d.ts +5 -15
  134. package/out/candidate/country-display-names.d.ts.map +1 -1
  135. package/out/candidate/country-display-names.js +6 -20
  136. package/out/candidate/country-display-names.js.map +1 -1
  137. package/out/candidate/extract-fold.d.ts +8 -16
  138. package/out/candidate/extract-fold.d.ts.map +1 -1
  139. package/out/candidate/extract-fold.js +6 -48
  140. package/out/candidate/extract-fold.js.map +1 -1
  141. package/out/candidate/fts.d.ts +19 -0
  142. package/out/candidate/fts.d.ts.map +1 -0
  143. package/out/candidate/fts.js +22 -0
  144. package/out/candidate/fts.js.map +1 -0
  145. package/out/candidate/importance.d.ts +127 -0
  146. package/out/candidate/importance.d.ts.map +1 -0
  147. package/out/candidate/importance.js +162 -0
  148. package/out/candidate/importance.js.map +1 -0
  149. package/out/candidate/lookup.d.ts +36 -0
  150. package/out/candidate/lookup.d.ts.map +1 -0
  151. package/out/candidate/lookup.js +456 -0
  152. package/out/candidate/lookup.js.map +1 -0
  153. package/out/candidate/name-roles.d.ts +10 -22
  154. package/out/candidate/name-roles.d.ts.map +1 -1
  155. package/out/candidate/name-roles.js +15 -33
  156. package/out/candidate/name-roles.js.map +1 -1
  157. package/out/candidate/own-name.d.ts +23 -25
  158. package/out/candidate/own-name.d.ts.map +1 -1
  159. package/out/candidate/own-name.js +34 -31
  160. package/out/candidate/own-name.js.map +1 -1
  161. package/out/candidate/place-attrs.d.ts +20 -13
  162. package/out/candidate/place-attrs.d.ts.map +1 -1
  163. package/out/candidate/place-attrs.js +4 -5
  164. package/out/candidate/place-attrs.js.map +1 -1
  165. package/out/candidate/schema.d.ts +120 -0
  166. package/out/candidate/schema.d.ts.map +1 -0
  167. package/out/{candidate-schema.js → candidate/schema.js} +11 -23
  168. package/out/candidate/schema.js.map +1 -0
  169. package/out/{candidate-scoring.d.ts → candidate/scoring.d.ts} +8 -9
  170. package/out/candidate/scoring.d.ts.map +1 -0
  171. package/out/candidate/scoring.js +157 -0
  172. package/out/candidate/scoring.js.map +1 -0
  173. package/out/capital-schema.d.ts +21 -17
  174. package/out/capital-schema.d.ts.map +1 -1
  175. package/out/capital-schema.js +17 -15
  176. package/out/capital-schema.js.map +1 -1
  177. package/out/capitals.d.ts +26 -24
  178. package/out/capitals.d.ts.map +1 -1
  179. package/out/capitals.js +23 -21
  180. package/out/capitals.js.map +1 -1
  181. package/out/coincident-roles.d.ts +60 -43
  182. package/out/coincident-roles.d.ts.map +1 -1
  183. package/out/coincident-roles.js +61 -50
  184. package/out/coincident-roles.js.map +1 -1
  185. package/out/convention/schema.d.ts +12 -6
  186. package/out/convention/schema.d.ts.map +1 -1
  187. package/out/convention/schema.js +6 -4
  188. package/out/convention/schema.js.map +1 -1
  189. package/out/convention.d.ts +97 -0
  190. package/out/convention.d.ts.map +1 -0
  191. package/out/convention.js +81 -0
  192. package/out/convention.js.map +1 -0
  193. package/out/coverage-manifest-schema.d.ts +33 -38
  194. package/out/coverage-manifest-schema.d.ts.map +1 -1
  195. package/out/coverage-manifest-schema.js +20 -29
  196. package/out/coverage-manifest-schema.js.map +1 -1
  197. package/out/currency-backfill.d.ts +13 -35
  198. package/out/currency-backfill.d.ts.map +1 -1
  199. package/out/currency-backfill.js +12 -39
  200. package/out/currency-backfill.js.map +1 -1
  201. package/out/env.d.ts +2 -2
  202. package/out/env.js +1 -1
  203. package/out/exact-match.d.ts +19 -10
  204. package/out/exact-match.d.ts.map +1 -1
  205. package/out/exact-match.js +31 -20
  206. package/out/exact-match.js.map +1 -1
  207. package/out/extracts.d.ts +65 -54
  208. package/out/extracts.d.ts.map +1 -1
  209. package/out/extracts.js +63 -55
  210. package/out/extracts.js.map +1 -1
  211. package/out/fst/autocomplete.d.ts +28 -20
  212. package/out/fst/autocomplete.d.ts.map +1 -1
  213. package/out/fst/autocomplete.js +38 -27
  214. package/out/fst/autocomplete.js.map +1 -1
  215. package/out/fst/builder.d.ts.map +1 -1
  216. package/out/fst/builder.js +30 -36
  217. package/out/fst/builder.js.map +1 -1
  218. package/out/fst/deserialize-web.d.ts +3 -3
  219. package/out/fst/deserialize-web.d.ts.map +1 -1
  220. package/out/fst/deserialize-web.js +8 -9
  221. package/out/fst/deserialize-web.js.map +1 -1
  222. package/out/fst/format.d.ts +41 -23
  223. package/out/fst/format.d.ts.map +1 -1
  224. package/out/fst/format.js +41 -23
  225. package/out/fst/format.js.map +1 -1
  226. package/out/fst/freshness.d.ts +11 -73
  227. package/out/fst/freshness.d.ts.map +1 -1
  228. package/out/fst/freshness.js +12 -76
  229. package/out/fst/freshness.js.map +1 -1
  230. package/out/fst/matcher.d.ts +1 -1
  231. package/out/fst/matcher.d.ts.map +1 -1
  232. package/out/fst/matcher.js +2 -2
  233. package/out/fst/matcher.js.map +1 -1
  234. package/out/fst/serialize.d.ts +30 -26
  235. package/out/fst/serialize.d.ts.map +1 -1
  236. package/out/fst/serialize.js +44 -58
  237. package/out/fst/serialize.js.map +1 -1
  238. package/out/fst/types.d.ts +71 -59
  239. package/out/fst/types.d.ts.map +1 -1
  240. package/out/fst/types.js +1 -4
  241. package/out/fst/types.js.map +1 -1
  242. package/out/{fst/index.js → fst.d.ts} +1 -1
  243. package/out/fst.d.ts.map +1 -0
  244. package/out/{fst/index.d.ts → fst.js} +1 -1
  245. package/out/fst.js.map +1 -0
  246. package/out/fts/query.d.ts +2 -27
  247. package/out/fts/query.d.ts.map +1 -1
  248. package/out/fts/query.js +5 -36
  249. package/out/fts/query.js.map +1 -1
  250. package/out/fts.d.ts +126 -0
  251. package/out/fts.d.ts.map +1 -0
  252. package/out/fts.js +188 -0
  253. package/out/fts.js.map +1 -0
  254. package/out/geonames/aliases.d.ts +26 -67
  255. package/out/geonames/aliases.d.ts.map +1 -1
  256. package/out/geonames/aliases.js +44 -121
  257. package/out/geonames/aliases.js.map +1 -1
  258. package/out/geonames/postal.d.ts +84 -50
  259. package/out/geonames/postal.d.ts.map +1 -1
  260. package/out/geonames/postal.js +115 -78
  261. package/out/geonames/postal.js.map +1 -1
  262. package/out/{geonames/index.js → geonames.d.ts} +1 -1
  263. package/out/geonames.d.ts.map +1 -0
  264. package/out/{geonames/index.d.ts → geonames.js} +1 -1
  265. package/out/geonames.js.map +1 -0
  266. package/out/index.d.ts +20 -21
  267. package/out/index.d.ts.map +1 -1
  268. package/out/index.js +14 -15
  269. package/out/index.js.map +1 -1
  270. package/out/interpolation.d.ts +15 -52
  271. package/out/interpolation.d.ts.map +1 -1
  272. package/out/interpolation.js +25 -68
  273. package/out/interpolation.js.map +1 -1
  274. package/out/lookup.d.ts +24 -53
  275. package/out/lookup.d.ts.map +1 -1
  276. package/out/lookup.js +117 -223
  277. package/out/lookup.js.map +1 -1
  278. package/out/name-score.d.ts +2 -2
  279. package/out/name-score.d.ts.map +1 -1
  280. package/out/name-score.js +9 -7
  281. package/out/name-score.js.map +1 -1
  282. package/out/nsul/lookup.d.ts +33 -34
  283. package/out/nsul/lookup.d.ts.map +1 -1
  284. package/out/nsul/lookup.js +26 -40
  285. package/out/nsul/lookup.js.map +1 -1
  286. package/out/nsul/schema.d.ts +16 -43
  287. package/out/nsul/schema.d.ts.map +1 -1
  288. package/out/nsul/schema.js +9 -35
  289. package/out/nsul/schema.js.map +1 -1
  290. package/out/{nsul/index.d.ts → nsul.d.ts} +1 -1
  291. package/out/nsul.d.ts.map +1 -0
  292. package/out/{nsul/index.js → nsul.js} +1 -1
  293. package/out/nsul.js.map +1 -0
  294. package/out/paths.d.ts +124 -0
  295. package/out/paths.d.ts.map +1 -0
  296. package/out/paths.js +104 -0
  297. package/out/paths.js.map +1 -0
  298. package/out/place-importance-schema.d.ts +49 -141
  299. package/out/place-importance-schema.d.ts.map +1 -1
  300. package/out/place-importance-schema.js +25 -140
  301. package/out/place-importance-schema.js.map +1 -1
  302. package/out/poi/lookup.d.ts +38 -42
  303. package/out/poi/lookup.d.ts.map +1 -1
  304. package/out/poi/lookup.js +67 -90
  305. package/out/poi/lookup.js.map +1 -1
  306. package/out/poi/schema.d.ts +41 -25
  307. package/out/poi/schema.d.ts.map +1 -1
  308. package/out/poi/schema.js +25 -17
  309. package/out/poi/schema.js.map +1 -1
  310. package/out/{poi/index.js → poi.d.ts} +1 -1
  311. package/out/poi.d.ts.map +1 -0
  312. package/out/{poi/index.d.ts → poi.js} +1 -1
  313. package/out/poi.js.map +1 -0
  314. package/out/polygon-schema.d.ts +10 -6
  315. package/out/polygon-schema.d.ts.map +1 -1
  316. package/out/polygon-schema.js +2 -2
  317. package/out/polygon-schema.js.map +1 -1
  318. package/out/postal/{city-alias-lookup.d.ts → city/alias/lookup.d.ts} +22 -25
  319. package/out/postal/city/alias/lookup.d.ts.map +1 -0
  320. package/out/postal/city/alias/lookup.js +49 -0
  321. package/out/postal/city/alias/lookup.js.map +1 -0
  322. package/out/postal/{city-alias-schema.d.ts → city/alias/schema.d.ts} +15 -11
  323. package/out/postal/city/alias/schema.d.ts.map +1 -0
  324. package/out/postal/{city-alias-schema.js → city/alias/schema.js} +11 -9
  325. package/out/postal/city/alias/schema.js.map +1 -0
  326. package/out/postal/city/candidate-schema.d.ts +72 -0
  327. package/out/postal/city/candidate-schema.d.ts.map +1 -0
  328. package/out/postal/city/candidate-schema.js +60 -0
  329. package/out/postal/city/candidate-schema.js.map +1 -0
  330. package/out/postal.d.ts +9 -0
  331. package/out/postal.d.ts.map +1 -0
  332. package/out/postal.js +9 -0
  333. package/out/postal.js.map +1 -0
  334. package/out/postcode-point-lookup.d.ts +9 -19
  335. package/out/postcode-point-lookup.d.ts.map +1 -1
  336. package/out/postcode-point-lookup.js +5 -17
  337. package/out/postcode-point-lookup.js.map +1 -1
  338. package/out/primary-preference.d.ts +39 -93
  339. package/out/primary-preference.d.ts.map +1 -1
  340. package/out/primary-preference.js +13 -97
  341. package/out/primary-preference.js.map +1 -1
  342. package/out/proximity-rerank.d.ts +40 -29
  343. package/out/proximity-rerank.d.ts.map +1 -1
  344. package/out/proximity-rerank.js +38 -29
  345. package/out/proximity-rerank.js.map +1 -1
  346. package/out/ranking-weights.d.ts +91 -62
  347. package/out/ranking-weights.d.ts.map +1 -1
  348. package/out/ranking-weights.js +26 -25
  349. package/out/ranking-weights.js.map +1 -1
  350. package/out/region-keys.d.ts +48 -29
  351. package/out/region-keys.d.ts.map +1 -1
  352. package/out/region-keys.js +68 -40
  353. package/out/region-keys.js.map +1 -1
  354. package/out/reverse.d.ts +75 -50
  355. package/out/reverse.d.ts.map +1 -1
  356. package/out/reverse.js +61 -49
  357. package/out/reverse.js.map +1 -1
  358. package/out/schema.d.ts +74 -42
  359. package/out/schema.d.ts.map +1 -1
  360. package/out/schema.js +4 -5
  361. package/out/schema.js.map +1 -1
  362. package/out/search-fetch.d.ts +16 -12
  363. package/out/search-fetch.d.ts.map +1 -1
  364. package/out/search-fetch.js +83 -63
  365. package/out/search-fetch.js.map +1 -1
  366. package/out/sqlite/convention-source.d.ts +25 -0
  367. package/out/sqlite/convention-source.d.ts.map +1 -0
  368. package/out/sqlite/convention-source.js +48 -0
  369. package/out/sqlite/convention-source.js.map +1 -0
  370. package/out/sqlite/utils.d.ts +45 -0
  371. package/out/sqlite/utils.d.ts.map +1 -0
  372. package/out/sqlite/utils.js +56 -0
  373. package/out/sqlite/utils.js.map +1 -0
  374. package/out/street/centroid/schema.d.ts +100 -0
  375. package/out/street/centroid/schema.d.ts.map +1 -0
  376. package/out/street/centroid/schema.js +66 -0
  377. package/out/street/centroid/schema.js.map +1 -0
  378. package/out/street/centroid.d.ts +9 -19
  379. package/out/street/centroid.d.ts.map +1 -1
  380. package/out/street/centroid.js +11 -22
  381. package/out/street/centroid.js.map +1 -1
  382. package/out/street/{morphology-fst-builder.d.ts → morphology/fst/builder.d.ts} +21 -11
  383. package/out/street/morphology/fst/builder.d.ts.map +1 -0
  384. package/out/street/{morphology-fst-builder.js → morphology/fst/builder.js} +40 -34
  385. package/out/street/morphology/fst/builder.js.map +1 -0
  386. package/out/street/morphology/fst/loader.d.ts +74 -0
  387. package/out/street/morphology/fst/loader.d.ts.map +1 -0
  388. package/out/street/{morphology-fst-loader.js → morphology/fst/loader.js} +21 -21
  389. package/out/street/morphology/fst/loader.js.map +1 -0
  390. package/out/street/name-lookup.d.ts +24 -18
  391. package/out/street/name-lookup.d.ts.map +1 -1
  392. package/out/street/name-lookup.js +32 -31
  393. package/out/street/name-lookup.js.map +1 -1
  394. package/out/street/normalize.d.ts +35 -136
  395. package/out/street/normalize.d.ts.map +1 -1
  396. package/out/street/normalize.js +74 -191
  397. package/out/street/normalize.js.map +1 -1
  398. package/out/street/segment-schema.d.ts +79 -39
  399. package/out/street/segment-schema.d.ts.map +1 -1
  400. package/out/street/segment-schema.js +71 -21
  401. package/out/street/segment-schema.js.map +1 -1
  402. package/out/{street/index.d.ts → street.d.ts} +4 -4
  403. package/out/street.d.ts.map +1 -0
  404. package/out/{street/index.js → street.js} +4 -4
  405. package/out/street.js.map +1 -0
  406. package/out/types.d.ts +69 -138
  407. package/out/types.d.ts.map +1 -1
  408. package/out/types.js +0 -6
  409. package/out/types.js.map +1 -1
  410. package/out/unified-schema.d.ts +8 -12
  411. package/out/unified-schema.d.ts.map +1 -1
  412. package/out/unified-schema.js +29 -29
  413. package/out/unified-schema.js.map +1 -1
  414. package/out/uprn/existence.d.ts +28 -17
  415. package/out/uprn/existence.d.ts.map +1 -1
  416. package/out/uprn/existence.js +26 -16
  417. package/out/uprn/existence.js.map +1 -1
  418. package/out/uprn/lookup.d.ts +14 -44
  419. package/out/uprn/lookup.d.ts.map +1 -1
  420. package/out/uprn/lookup.js +20 -56
  421. package/out/uprn/lookup.js.map +1 -1
  422. package/out/uprn/schema.d.ts +18 -40
  423. package/out/uprn/schema.d.ts.map +1 -1
  424. package/out/uprn/schema.js +10 -32
  425. package/out/uprn/schema.js.map +1 -1
  426. package/out/{uprn/index.d.ts → uprn.d.ts} +1 -1
  427. package/out/uprn.d.ts.map +1 -0
  428. package/out/{uprn/index.js → uprn.js} +1 -1
  429. package/out/uprn.js.map +1 -0
  430. package/out/weights-overlay-linker.d.ts +132 -141
  431. package/out/weights-overlay-linker.d.ts.map +1 -1
  432. package/out/weights-overlay-linker.js +86 -154
  433. package/out/weights-overlay-linker.js.map +1 -1
  434. package/package.json +94 -599
  435. package/lib/address/point-schema.ts +0 -155
  436. package/lib/build-candidate.ts +0 -632
  437. package/lib/build-slim.ts +0 -408
  438. package/lib/candidate-ancestors-schema.ts +0 -195
  439. package/lib/candidate-fts.ts +0 -46
  440. package/lib/candidate-importance.ts +0 -224
  441. package/lib/candidate-lookup.ts +0 -977
  442. package/lib/candidate-schema.ts +0 -247
  443. package/lib/candidate-scoring.ts +0 -261
  444. package/lib/convention/index.ts +0 -154
  445. package/lib/fts/index.ts +0 -361
  446. package/lib/postal/city-alias-lookup.ts +0 -97
  447. package/lib/postal/index.ts +0 -9
  448. package/lib/sqlite-convention-source.ts +0 -65
  449. package/lib/sqlite-utils.ts +0 -82
  450. package/lib/street/centroid-schema.ts +0 -154
  451. package/out/address/index.d.ts.map +0 -1
  452. package/out/address/index.js.map +0 -1
  453. package/out/address/point-interpolation.d.ts +0 -49
  454. package/out/address/point-interpolation.d.ts.map +0 -1
  455. package/out/address/point-interpolation.js.map +0 -1
  456. package/out/address/point-schema.d.ts +0 -97
  457. package/out/address/point-schema.d.ts.map +0 -1
  458. package/out/address/point-schema.js.map +0 -1
  459. package/out/ancestry/index.d.ts +0 -45
  460. package/out/ancestry/index.d.ts.map +0 -1
  461. package/out/ancestry/index.js +0 -55
  462. package/out/ancestry/index.js.map +0 -1
  463. package/out/build-candidate.d.ts +0 -184
  464. package/out/build-candidate.d.ts.map +0 -1
  465. package/out/build-candidate.js.map +0 -1
  466. package/out/build-slim.d.ts +0 -80
  467. package/out/build-slim.d.ts.map +0 -1
  468. package/out/build-slim.js.map +0 -1
  469. package/out/candidate/ancestors-sidecar.d.ts +0 -32
  470. package/out/candidate/ancestors-sidecar.d.ts.map +0 -1
  471. package/out/candidate/ancestors-sidecar.js.map +0 -1
  472. package/out/candidate-ancestors-schema.d.ts +0 -150
  473. package/out/candidate-ancestors-schema.d.ts.map +0 -1
  474. package/out/candidate-ancestors-schema.js +0 -123
  475. package/out/candidate-ancestors-schema.js.map +0 -1
  476. package/out/candidate-fts.d.ts +0 -37
  477. package/out/candidate-fts.d.ts.map +0 -1
  478. package/out/candidate-fts.js +0 -40
  479. package/out/candidate-fts.js.map +0 -1
  480. package/out/candidate-importance.d.ts +0 -132
  481. package/out/candidate-importance.d.ts.map +0 -1
  482. package/out/candidate-importance.js +0 -169
  483. package/out/candidate-importance.js.map +0 -1
  484. package/out/candidate-lookup.d.ts +0 -76
  485. package/out/candidate-lookup.d.ts.map +0 -1
  486. package/out/candidate-lookup.js +0 -768
  487. package/out/candidate-lookup.js.map +0 -1
  488. package/out/candidate-schema.d.ts +0 -155
  489. package/out/candidate-schema.d.ts.map +0 -1
  490. package/out/candidate-schema.js.map +0 -1
  491. package/out/candidate-scoring.d.ts.map +0 -1
  492. package/out/candidate-scoring.js +0 -196
  493. package/out/candidate-scoring.js.map +0 -1
  494. package/out/convention/index.d.ts +0 -106
  495. package/out/convention/index.d.ts.map +0 -1
  496. package/out/convention/index.js +0 -93
  497. package/out/convention/index.js.map +0 -1
  498. package/out/fst/index.d.ts.map +0 -1
  499. package/out/fst/index.js.map +0 -1
  500. package/out/fts/index.d.ts +0 -184
  501. package/out/fts/index.d.ts.map +0 -1
  502. package/out/fts/index.js +0 -277
  503. package/out/fts/index.js.map +0 -1
  504. package/out/geonames/index.d.ts.map +0 -1
  505. package/out/geonames/index.js.map +0 -1
  506. package/out/nsul/index.d.ts.map +0 -1
  507. package/out/nsul/index.js.map +0 -1
  508. package/out/poi/index.d.ts.map +0 -1
  509. package/out/poi/index.js.map +0 -1
  510. package/out/postal/city-alias-lookup.d.ts.map +0 -1
  511. package/out/postal/city-alias-lookup.js +0 -62
  512. package/out/postal/city-alias-lookup.js.map +0 -1
  513. package/out/postal/city-alias-schema.d.ts.map +0 -1
  514. package/out/postal/city-alias-schema.js.map +0 -1
  515. package/out/postal/city-candidate-schema.d.ts +0 -66
  516. package/out/postal/city-candidate-schema.d.ts.map +0 -1
  517. package/out/postal/city-candidate-schema.js +0 -56
  518. package/out/postal/city-candidate-schema.js.map +0 -1
  519. package/out/postal/index.d.ts +0 -9
  520. package/out/postal/index.d.ts.map +0 -1
  521. package/out/postal/index.js +0 -9
  522. package/out/postal/index.js.map +0 -1
  523. package/out/sqlite-convention-source.d.ts +0 -29
  524. package/out/sqlite-convention-source.d.ts.map +0 -1
  525. package/out/sqlite-convention-source.js +0 -57
  526. package/out/sqlite-convention-source.js.map +0 -1
  527. package/out/sqlite-utils.d.ts +0 -45
  528. package/out/sqlite-utils.d.ts.map +0 -1
  529. package/out/sqlite-utils.js +0 -57
  530. package/out/sqlite-utils.js.map +0 -1
  531. package/out/street/centroid-schema.d.ts +0 -105
  532. package/out/street/centroid-schema.d.ts.map +0 -1
  533. package/out/street/centroid-schema.js +0 -78
  534. package/out/street/centroid-schema.js.map +0 -1
  535. package/out/street/index.d.ts.map +0 -1
  536. package/out/street/index.js.map +0 -1
  537. package/out/street/morphology-fst-builder.d.ts.map +0 -1
  538. package/out/street/morphology-fst-builder.js.map +0 -1
  539. package/out/street/morphology-fst-loader.d.ts +0 -67
  540. package/out/street/morphology-fst-loader.d.ts.map +0 -1
  541. package/out/street/morphology-fst-loader.js.map +0 -1
  542. package/out/uprn/index.d.ts.map +0 -1
  543. package/out/uprn/index.js.map +0 -1
  544. /package/lib/{fst/index.ts → fst.ts} +0 -0
  545. /package/lib/{geonames/index.ts → geonames.ts} +0 -0
  546. /package/lib/{nsul/index.ts → nsul.ts} +0 -0
  547. /package/lib/{poi/index.ts → poi.ts} +0 -0
  548. /package/lib/{uprn/index.ts → uprn.ts} +0 -0
@@ -0,0 +1,669 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
8
+ import { type Ancestor, type GazetteerArtifactCoverage, referentialFromPopulation } from "@mailwoman/core/resolver"
9
+ import { allRows } from "@mailwoman/core/utils"
10
+ import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators"
11
+ import { partitionByContainment } from "@mailwoman/resolver"
12
+ import { haversineKm } from "@mailwoman/spatial"
13
+ import type { DatabaseClient } from "@mailwoman/sqlite/client"
14
+ import { SQLiteLookup, type SQLiteLookupOptions } from "@mailwoman/sqlite/lookup"
15
+
16
+ import {
17
+ CANDIDATE_ANCESTOR_TABLE,
18
+ CANDIDATE_INTERVAL_TABLE,
19
+ intervalContains,
20
+ type CandidateAncestorTable,
21
+ type IntervalLabel,
22
+ } from "#candidate/ancestors/schema"
23
+ import { CANDIDATE_FTS_TABLE } from "#candidate/fts"
24
+ import type { CandidateDatabase, CandidateTable, CountryCodeTable, PlacetypeCodeTable } from "#candidate/schema"
25
+ import { readGazetteerCoverageManifest } from "#coverage-manifest-schema"
26
+ import { POSTAL_CITY_CANDIDATE_TABLE, type PostalCityCandidateTable } from "#postal/city/candidate-schema"
27
+ import { rankByPrimaryPreference, type RankedRow, RERANK_FETCH } from "#primary-preference"
28
+ import { applyProximityRerank } from "#proximity-rerank"
29
+ import { REGION_CLASS_PLACETYPES, regionQualifierProbeKeys } from "#region-keys"
30
+ import { hasColumn, hasTable } from "#sqlite-utils"
31
+ import { type NameKey, normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize"
32
+ import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "#types"
33
+
34
+ export { rankByPrimaryPreference } from "#primary-preference"
35
+ export type { RankedRow } from "#primary-preference"
36
+
37
+ /**
38
+ * Where a {@link WOFCandidateTableLookup} reads from
39
+ * (a read-only `candidate.db` or a caller-held connection) and how it ranks.
40
+ */
41
+ export interface WOFCandidateTableLookupOpts extends SQLiteLookupOptions<CandidateDatabase> {
42
+ /**
43
+ * Exempt `name_role = 'variant'` aliases from the cross-country primary-preference penalty.
44
+ * No-ops without the role column and is off by default.
45
+ */
46
+ variantAliasExemption?: boolean
47
+ }
48
+
49
+ type CandidateRow = Pick<
50
+ CandidateTable,
51
+ | "spr_id"
52
+ | "name"
53
+ | "country_id"
54
+ | "placetype_id"
55
+ | "latitude"
56
+ | "longitude"
57
+ | "min_lat"
58
+ | "min_lon"
59
+ | "max_lat"
60
+ | "max_lon"
61
+ | "neg_rank"
62
+ | "is_primary"
63
+ | "population"
64
+ > &
65
+ // Optional because older artifacts may not include `importance`.
66
+ Partial<Pick<CandidateTable, "importance">>
67
+
68
+ const FUZZY_FETCH = 40
69
+
70
+ const WORD_FUZZY_MIN = 0.85
71
+
72
+ function wordFuzzySimilarity(a: string, b: string): number {
73
+ return Math.max(jaroWinkler(a, b), levenshteinSimilarity(a, b))
74
+ }
75
+
76
+ const POSTCODE_CONTAINMENT_THRESHOLD_KM = 25
77
+
78
+ function ftsTrigramQuery(s: string): string {
79
+ const grams = new Set<string>()
80
+
81
+ for (let i = 0; i + 3 <= s.length; i++) {
82
+ const g = s.slice(i, i + 3)
83
+
84
+ if (/^[\p{L}\p{N} ]{3}$/u.test(g)) {
85
+ grams.add(g)
86
+ }
87
+ }
88
+
89
+ return [...grams].map((g) => `"${g}"`).join(" OR ")
90
+ }
91
+
92
+ /**
93
+ * Node {@link PlaceLookup} over `candidate.db`.
94
+ */
95
+ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> implements PlaceLookup {
96
+ readonly #countryToID = new Map<string, number>()
97
+ readonly #idToCountry = new Map<number, string>()
98
+ readonly #placetypeToID = new Map<string, number>()
99
+ readonly #idToPlacetype = new Map<number, string>()
100
+
101
+ readonly #postalCityProbe: ReturnType<DatabaseClient["prepare"]> | undefined
102
+
103
+ readonly #ftsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
104
+ /**
105
+ * Prepared unfiltered `name_key` existence probe, so fuzzy fallback runs only for true name misses.
106
+ */
107
+ readonly #nameKeyExistsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
108
+
109
+ readonly artifactCoverage: GazetteerArtifactCoverage | undefined
110
+
111
+ readonly #importanceSelect: string
112
+
113
+ readonly #hasNameRole: boolean
114
+ readonly #variantAliasExemption: boolean
115
+
116
+ readonly #roleSelect: string
117
+
118
+ readonly #ancestorsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
119
+ readonly #ancestorsCache = new Map<number, Ancestor[]>()
120
+
121
+ readonly #intervalProbe: ReturnType<DatabaseClient["prepare"]> | undefined
122
+ readonly #intervalCache = new Map<number, IntervalLabel | null>()
123
+
124
+ readonly #qualifierProbe: ReturnType<DatabaseClient["prepare"]> | undefined
125
+ /**
126
+ * Ancestor lineage accessor, nearest-first, present only when the artifact supports it.
127
+ */
128
+ readonly ancestors: ((id: number | string) => Ancestor[]) | undefined
129
+
130
+ constructor(opts: WOFCandidateTableLookupOpts) {
131
+ super(opts)
132
+
133
+ for (const r of allRows<CountryCodeTable>(this.database.prepare("SELECT id, code FROM country_codes"))) {
134
+ const code = String(r.code).toUpperCase()
135
+ this.#countryToID.set(code, Number(r.id))
136
+ this.#idToCountry.set(Number(r.id), code)
137
+ }
138
+
139
+ for (const r of allRows<PlacetypeCodeTable>(this.database.prepare("SELECT id, placetype FROM placetype_codes"))) {
140
+ this.#placetypeToID.set(String(r.placetype), Number(r.id))
141
+ this.#idToPlacetype.set(Number(r.id), String(r.placetype))
142
+ }
143
+
144
+ if (hasTable(this.database, POSTAL_CITY_CANDIDATE_TABLE)) {
145
+ this.#postalCityProbe = this.database.prepare(
146
+ `SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`
147
+ )
148
+ }
149
+
150
+ if (hasTable(this.database, CANDIDATE_FTS_TABLE)) {
151
+ this.#ftsProbe = this.database.prepare(
152
+ `SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`
153
+ )
154
+
155
+ this.#nameKeyExistsProbe = this.database.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1")
156
+ }
157
+
158
+ // Optional columns are detected once here for hot-path reads.
159
+ this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : ""
160
+ this.#hasNameRole = hasColumn(this.database, "candidate", "name_role")
161
+ this.#variantAliasExemption = opts.variantAliasExemption === true
162
+ this.#roleSelect = this.#hasNameRole ? ", name_role" : ""
163
+
164
+ if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
165
+ this.#ancestorsProbe = this.database.prepare(
166
+ `SELECT parent_spr_id, parent_placetype_id, parent_name FROM ${CANDIDATE_ANCESTOR_TABLE}` +
167
+ " WHERE spr_id = ? ORDER BY depth ASC"
168
+ )
169
+
170
+ this.ancestors = (id) => this.#ancestorLineage(id)
171
+ }
172
+
173
+ if (this.#ancestorsProbe && hasTable(this.database, CANDIDATE_INTERVAL_TABLE)) {
174
+ this.#intervalProbe = this.database.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`)
175
+
176
+ const bandIDs = [...REGION_CLASS_PLACETYPES, "country"]
177
+ .map((placetype) => this.#placetypeToID.get(placetype))
178
+ .filter((id): id is number => id !== undefined)
179
+
180
+ if (bandIDs.length) {
181
+ this.#qualifierProbe = this.database.prepare(
182
+ `SELECT DISTINCT spr_id FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.join(",")}) LIMIT 8`
183
+ )
184
+ }
185
+ }
186
+
187
+ this.artifactCoverage = readGazetteerCoverageManifest(this.database)
188
+ }
189
+
190
+ #ancestorLineage(id: number | string): Ancestor[] {
191
+ const pid = typeof id === "number" ? id : Number(id)
192
+
193
+ if (!Number.isFinite(pid) || !this.#ancestorsProbe) return []
194
+
195
+ const cached = this.#ancestorsCache.get(pid)
196
+
197
+ if (cached) return cached
198
+
199
+ const rows = allRows<Pick<CandidateAncestorTable, "parent_spr_id" | "parent_placetype_id" | "parent_name">>(
200
+ this.#ancestorsProbe,
201
+ pid
202
+ )
203
+
204
+ const lineage: Ancestor[] = rows.map((r) => ({
205
+ id: Number(r.parent_spr_id),
206
+ placetype: this.#idToPlacetype.get(Number(r.parent_placetype_id)) ?? "",
207
+ name: String(r.parent_name ?? ""),
208
+ }))
209
+
210
+ this.#ancestorsCache.set(pid, lineage)
211
+
212
+ return lineage
213
+ }
214
+
215
+ #intervalLabel(sprID: number): IntervalLabel | null {
216
+ if (!this.#intervalProbe) return null
217
+
218
+ const cached = this.#intervalCache.get(sprID)
219
+
220
+ if (cached !== undefined) return cached
221
+
222
+ const row = this.#intervalProbe.get(sprID) as { pre: number; post: number } | undefined
223
+ const label = row ? { pre: Number(row.pre), post: Number(row.post) } : null
224
+
225
+ this.#intervalCache.set(sprID, label)
226
+
227
+ return label
228
+ }
229
+
230
+ #qualifierRegionIDs(qualifier: string, country: string | undefined): Set<number> {
231
+ const ids = new Set<number>()
232
+
233
+ if (!this.#qualifierProbe) return ids
234
+
235
+ for (const key of regionQualifierProbeKeys(qualifier, country)) {
236
+ if (!key) continue
237
+
238
+ for (const row of allRows<{ spr_id: number }>(this.#qualifierProbe, key)) {
239
+ ids.add(Number(row.spr_id))
240
+ }
241
+ }
242
+
243
+ return ids
244
+ }
245
+
246
+ /**
247
+ * Whether `sprID` is contained by any qualifier row, using interval labels first
248
+ * and the ancestor chain as fallback.
249
+ */
250
+ #containedByQualifier(sprID: number, qualifierIDs: ReadonlySet<number>, qualifierLabels: IntervalLabel[]): boolean {
251
+ if (qualifierIDs.has(sprID)) return true
252
+
253
+ const label = this.#intervalLabel(sprID)
254
+
255
+ if (label && qualifierLabels.some((outer) => intervalContains(outer, label))) return true
256
+
257
+ return this.#ancestorLineage(sprID).some((ancestor) => qualifierIDs.has(Number(ancestor.id)))
258
+ }
259
+
260
+ /**
261
+ * Apply admin-containment re-rank: stamp containment, inject contained misses,
262
+ * then partition contained-first.
263
+ */
264
+ #applyAdminContainment(
265
+ rows: Array<RankedRow<CandidateRow>>,
266
+ qualifier: string,
267
+ country: string | undefined,
268
+ opts: {
269
+ nameKey: NameKey
270
+ strippedKey: NameKey
271
+ shapeFilters: string[]
272
+ shapeParams: Array<string | number>
273
+ limit: number
274
+ }
275
+ ): Array<RankedRow<CandidateRow>> {
276
+ const qualifierIDs = this.#qualifierRegionIDs(qualifier, country)
277
+
278
+ if (!qualifierIDs.size) {
279
+ for (const row of rows) {
280
+ row.containedByQualifier = false
281
+ }
282
+
283
+ return rows
284
+ }
285
+
286
+ const qualifierLabels = [...qualifierIDs]
287
+ .map((id) => this.#intervalLabel(id))
288
+ .filter((label): label is IntervalLabel => label !== null)
289
+
290
+ const contained = (sprID: number): boolean => this.#containedByQualifier(sprID, qualifierIDs, qualifierLabels)
291
+
292
+ for (const row of rows) {
293
+ row.containedByQualifier = contained(Number(row.spr_id))
294
+ }
295
+
296
+ const present = new Set(rows.map((row) => Number(row.spr_id)))
297
+ const injected: Array<RankedRow<CandidateRow>> = []
298
+
299
+ const injectSQL = (primaryOnly: boolean): string =>
300
+ "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
301
+ `${this.#importanceSelect} FROM candidate WHERE ${["name_key = ?", ...opts.shapeFilters, ...(primaryOnly ? ["is_primary = 1"] : [])].join(" AND ")} ` +
302
+ "ORDER BY neg_rank ASC LIMIT ?"
303
+
304
+ const injectFrom = (key: string, primaryOnly: boolean): void => {
305
+ const fetched = allRows<CandidateRow>(
306
+ this.database.prepare(injectSQL(primaryOnly)),
307
+ key,
308
+ ...opts.shapeParams,
309
+ RERANK_FETCH
310
+ )
311
+
312
+ for (const row of fetched) {
313
+ const sprID = Number(row.spr_id)
314
+
315
+ if (present.has(sprID) || !contained(sprID)) continue
316
+ present.add(sprID)
317
+
318
+ injected.push({ ...row, effectiveNegRank: row.neg_rank, demoted: false, containedByQualifier: true })
319
+ }
320
+ }
321
+
322
+ injectFrom(opts.nameKey, false)
323
+
324
+ // Also inject contained neighborhood-band namesakes (primary-keyed only).
325
+ const bandIDs = ["neighbourhood", "macrohood", "microhood"]
326
+ .map((placetype) => this.#placetypeToID.get(placetype))
327
+ .filter((id): id is number => id !== undefined)
328
+
329
+ if (bandIDs.length) {
330
+ const bandSQL =
331
+ "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
332
+ `${this.#importanceSelect} FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.map(() => "?").join(",")}) AND is_primary = 1 ` +
333
+ "ORDER BY neg_rank ASC LIMIT ?"
334
+
335
+ const fetched = allRows<CandidateRow>(this.database.prepare(bandSQL), opts.nameKey, ...bandIDs, RERANK_FETCH)
336
+
337
+ for (const row of fetched) {
338
+ const sprID = Number(row.spr_id)
339
+
340
+ if (present.has(sprID) || !contained(sprID)) continue
341
+ present.add(sprID)
342
+
343
+ injected.push({ ...row, effectiveNegRank: row.neg_rank, demoted: false, containedByQualifier: true })
344
+ }
345
+ }
346
+
347
+ // Try stripped-key injection only when exact-key containment found no row.
348
+ if (
349
+ !injected.length &&
350
+ !rows.some((row) => row.containedByQualifier) &&
351
+ opts.strippedKey &&
352
+ opts.strippedKey !== opts.nameKey
353
+ ) {
354
+ injectFrom(opts.strippedKey, true)
355
+ }
356
+
357
+ if (!injected.length && !rows.some((row) => row.containedByQualifier)) return rows
358
+
359
+ return partitionByContainment(
360
+ [...rows, ...injected],
361
+ (row) => row.containedByQualifier === true,
362
+ (row) => !row.demoted && !row.fuzzy
363
+ ).slice(0, opts.limit)
364
+ }
365
+
366
+ #wantsLocality(placetype: FindPlaceQuery["placetype"]): boolean {
367
+ if (!placetype) return true
368
+ const want = Array.isArray(placetype) ? placetype : [placetype]
369
+
370
+ return expandPlacetypeFilter(want as readonly string[]).includes("locality")
371
+ }
372
+
373
+ #postcodeAnchor(postcode: string, country?: string): { lat: number; lon: number } | null {
374
+ const placetypeID = this.#placetypeToID.get("postalcode")
375
+
376
+ if (placetypeID === undefined) return null
377
+
378
+ const conds = ["name_key = ?", "placetype_id = ?"]
379
+ const params: Array<string | number> = [postcode.replaceAll(/\s+/g, ""), placetypeID]
380
+
381
+ if (country) {
382
+ const countryID = this.#countryToID.get(country.toUpperCase())
383
+
384
+ if (countryID === undefined) return null // Unknown country in this artifact.
385
+ conds.push("country_id = ?")
386
+ params.push(countryID)
387
+ }
388
+
389
+ const row = this.database
390
+ .prepare(`SELECT latitude, longitude FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT 1`)
391
+ .get(...params) as { latitude: number; longitude: number } | undefined
392
+
393
+ if (!row || (Number(row.latitude) === 0 && Number(row.longitude) === 0)) return null
394
+
395
+ return { lat: Number(row.latitude), lon: Number(row.longitude) }
396
+ }
397
+
398
+ async findPlace(query: FindPlaceQuery): Promise<PlaceCandidate[]> {
399
+ let text = (query.text ?? "").trim()
400
+
401
+ if (!text) return []
402
+
403
+ // Postcode queries use whitespace-stripped keying.
404
+ const wantsPostcode = [query.placetype].flat().includes("postalcode")
405
+
406
+ if (wantsPostcode) {
407
+ text = text.replaceAll(/\s+/g, "")
408
+ }
409
+
410
+ const nameKey = normalizeLocalityForKey(text)
411
+
412
+ if (!nameKey) return []
413
+
414
+ if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
415
+ const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim()) as
416
+ | Pick<PostalCityCandidateTable, "spr_id" | "name" | "latitude" | "longitude">
417
+ | undefined
418
+
419
+ if (hit) {
420
+ return [
421
+ {
422
+ id: Number(hit.spr_id),
423
+ name: String(hit.name ?? ""),
424
+ placetype: "locality" as WOFPlacetype,
425
+ country: query.country?.toUpperCase() ?? "",
426
+ lat: Number(hit.latitude),
427
+ lon: Number(hit.longitude),
428
+ score: 1,
429
+ exactMatch: true,
430
+ },
431
+ ]
432
+ }
433
+ }
434
+
435
+ const limit = Math.max(1, query.limit ?? 10)
436
+
437
+ // Shape filters are tracked separately from the shared country scope for exact and stripped probes.
438
+ const filters: string[] = []
439
+ const filterParams: Array<string | number> = []
440
+ const shapeFilters: string[] = []
441
+ const shapeParams: Array<string | number> = []
442
+
443
+ if (query.country) {
444
+ const cid = this.#countryToID.get(query.country.toUpperCase())
445
+
446
+ if (cid === undefined) return [] // Unknown country in this artifact.
447
+ filters.push("country_id = ?")
448
+ filterParams.push(cid)
449
+ }
450
+
451
+ if (query.placetype) {
452
+ // Expand placetype equivalents (e.g. locality includes borough/localadmin).
453
+ const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype]
454
+
455
+ const ids = expandPlacetypeFilter(want as readonly string[])
456
+ .map((t) => this.#placetypeToID.get(t))
457
+ .filter((v): v is number => v !== undefined)
458
+
459
+ if (!ids.length) return []
460
+ shapeFilters.push(`placetype_id IN (${ids.map(() => "?").join(",")})`)
461
+ shapeParams.push(...ids)
462
+ }
463
+
464
+ if (query.bbox) {
465
+ const b = query.bbox
466
+ shapeFilters.push("latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?")
467
+ shapeParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon)
468
+ }
469
+
470
+ // Optional guard: restrict to primary rows for token-extracted probes.
471
+ if (query.primaryOnly) {
472
+ shapeFilters.push("is_primary = 1")
473
+ }
474
+
475
+ // The role filter is skipped on an artifact lacking `name_role`.
476
+ if (query.excludeNameRoles?.length && this.#hasNameRole) {
477
+ shapeFilters.push(`(name_role IS NULL OR name_role NOT IN (${query.excludeNameRoles.map(() => "?").join(",")}))`)
478
+ shapeParams.push(...query.excludeNameRoles)
479
+ }
480
+
481
+ filters.push(...shapeFilters)
482
+ filterParams.push(...shapeParams)
483
+
484
+ // Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
485
+ const regionParentID = query.parentID || undefined
486
+
487
+ const probe = (nk: string, regionID: number | undefined, countryID?: number): Array<RankedRow<CandidateRow>> => {
488
+ const conds = ["name_key = ?", ...filters]
489
+ const params: Array<string | number> = [nk, ...filterParams]
490
+
491
+ if (regionID !== undefined) {
492
+ conds.push("region_id = ?")
493
+ params.push(regionID)
494
+ }
495
+
496
+ if (typeof countryID === "number") {
497
+ conds.push("country_id = ?")
498
+ params.push(countryID)
499
+ }
500
+
501
+ // Population-ordered fetch over-fetches for the bounded primary-preference rerank.
502
+ const sql =
503
+ "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
504
+ `${this.#importanceSelect}${this.#roleSelect} FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`
505
+
506
+ const fetched = allRows<CandidateRow>(this.database.prepare(sql), ...params, Math.max(limit, RERANK_FETCH))
507
+
508
+ return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption)
509
+ }
510
+
511
+ const cascade = (regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
512
+ let rows = probe(nameKey, regionID)
513
+
514
+ if (!rows.length) {
515
+ const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text))
516
+
517
+ if (strippedKey && strippedKey !== nameKey) {
518
+ // Stripped probes only accept primary rows to avoid alias-scrape matches.
519
+ rows = probe(strippedKey, regionID).filter((r) => r.is_primary === 1)
520
+ }
521
+ }
522
+
523
+ // Typo-tolerant fallback for true name misses only, skipping postcodes and respecting the fuzzy-country scope.
524
+ const fuzzyCountryID =
525
+ !query.country && query.fuzzyCountry ? this.#countryToID.get(query.fuzzyCountry.toUpperCase()) : undefined
526
+
527
+ const fuzzyScopedOut = !query.country && !!query.fuzzyCountry && typeof fuzzyCountryID !== "number"
528
+
529
+ if (
530
+ !rows.length &&
531
+ !wantsPostcode &&
532
+ !fuzzyScopedOut &&
533
+ this.#ftsProbe &&
534
+ this.#nameKeyExistsProbe &&
535
+ !this.#nameKeyExistsProbe.get(nameKey)
536
+ ) {
537
+ const match = ftsTrigramQuery(nameKey)
538
+
539
+ if (match) {
540
+ const hits = allRows<{ name_key: string }>(this.#ftsProbe, match, FUZZY_FETCH)
541
+
542
+ const ranked = hits
543
+ .map((h) => ({ nk: String(h.name_key), s: wordFuzzySimilarity(nameKey, String(h.name_key)) }))
544
+ .filter((h) => h.s >= WORD_FUZZY_MIN)
545
+ // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
546
+ .sort((a, b) => b.s - a.s)
547
+
548
+ const seen = new Set<string>()
549
+
550
+ for (const h of ranked) {
551
+ if (seen.has(h.nk)) continue
552
+ seen.add(h.nk)
553
+
554
+ rows.push(...probe(h.nk, regionID, fuzzyCountryID).map((r) => ({ ...r, fuzzy: true })))
555
+
556
+ if (rows.length >= limit) break
557
+ }
558
+
559
+ rows = rows.slice(0, limit)
560
+ }
561
+ }
562
+
563
+ return rows
564
+ }
565
+
566
+ let rows = cascade(regionParentID)
567
+
568
+ let regionScopeMiss = false
569
+
570
+ if (!rows.length && regionParentID !== undefined) {
571
+ rows = cascade(undefined)
572
+ regionScopeMiss = rows.length > 0
573
+ }
574
+
575
+ if (
576
+ query.postcode &&
577
+ query.postcodeContainmentCoherence === true &&
578
+ this.#wantsLocality(query.placetype) &&
579
+ rows.length > 1
580
+ ) {
581
+ const anchor = this.#postcodeAnchor(query.postcode, query.country)
582
+
583
+ if (anchor) {
584
+ const insideThreshold: Array<{ row: RankedRow<CandidateRow>; distanceKm: number }> = []
585
+ const outsideThreshold: RankedRow<CandidateRow>[] = []
586
+
587
+ for (const row of rows) {
588
+ const distanceKm = haversineKm(anchor.lat, anchor.lon, Number(row.latitude), Number(row.longitude))
589
+
590
+ if (distanceKm <= POSTCODE_CONTAINMENT_THRESHOLD_KM) {
591
+ insideThreshold.push({ row, distanceKm })
592
+ } else {
593
+ outsideThreshold.push(row)
594
+ }
595
+ }
596
+
597
+ if (insideThreshold.length) {
598
+ // oxlint-disable-next-line unicorn/no-array-sort -- hot-path in-place sort
599
+ insideThreshold.sort((a, b) => a.distanceKm - b.distanceKm)
600
+ rows = [...insideThreshold.map(({ row }) => row), ...outsideThreshold]
601
+ }
602
+ }
603
+ }
604
+
605
+ // Admin-containment re-rank runs last.
606
+ if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
607
+ rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
608
+ nameKey,
609
+ strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
610
+ shapeFilters,
611
+ shapeParams,
612
+ limit,
613
+ })
614
+ }
615
+
616
+ const candidates = rows.map((row): PlaceCandidate => {
617
+ const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
618
+ const parent = this.#ancestorLineage(Number(row.spr_id))[0]
619
+
620
+ return {
621
+ id: Number(row.spr_id),
622
+ name: String(row.name ?? ""),
623
+ placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? "") as WOFPlacetype,
624
+
625
+ country: this.#idToCountry.get(Number(row.country_id)) ?? "",
626
+ lat: Number(row.latitude),
627
+ lon: Number(row.longitude),
628
+
629
+ ...(parent ? { parent_id: Number(parent.id) } : {}),
630
+
631
+ score: -Number(row.neg_rank),
632
+
633
+ prominence: -Number(row.effectiveNegRank),
634
+
635
+ exactMatch: !row.demoted && !row.fuzzy,
636
+
637
+ ...(regionScopeMiss ? { regionScopeMiss: true } : {}),
638
+
639
+ ...(row.containedByQualifier === undefined ? {} : { containedByQualifier: row.containedByQualifier }),
640
+
641
+ ...(row.variantExempted ? { variantAliasExempted: true as const } : {}),
642
+
643
+ ...(row.population === null || row.population <= 0
644
+ ? {}
645
+ : { population: row.population, referential: referentialFromPopulation(row.population) }),
646
+
647
+ ...(typeof row.importance === "number" && Number.isFinite(row.importance)
648
+ ? { importance: row.importance }
649
+ : {}),
650
+ ...(hasBbox
651
+ ? {
652
+ bbox: {
653
+ minLat: Number(row.min_lat),
654
+ maxLat: Number(row.max_lat),
655
+ minLon: Number(row.min_lon),
656
+ maxLon: Number(row.max_lon),
657
+ },
658
+ }
659
+ : {}),
660
+ }
661
+ })
662
+
663
+ if (query.bias && query.bias.length) {
664
+ applyProximityRerank(candidates, query.bias)
665
+ }
666
+
667
+ return candidates
668
+ }
669
+ }