@mailwoman/resolver-wof-sqlite 10.0.0 → 10.2.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 (477) hide show
  1. package/README.md +9 -8
  2. package/lib/address/point/interpolation.ts +45 -39
  3. package/lib/address/point/schema.ts +17 -40
  4. package/lib/address/point.ts +316 -0
  5. package/lib/{address/index.ts → address.ts} +1 -1
  6. package/lib/ancestry/backfill.ts +20 -86
  7. package/lib/{ancestry/index.ts → ancestry.ts} +19 -15
  8. package/lib/build/candidate.ts +122 -174
  9. package/lib/build/population-corrections.ts +64 -0
  10. package/lib/build/slim.ts +44 -114
  11. package/lib/candidate/alias-bags.ts +16 -17
  12. package/lib/candidate/ancestors/schema.ts +77 -61
  13. package/lib/candidate/ancestors/sidecar.ts +15 -22
  14. package/lib/candidate/country-display-names.ts +6 -20
  15. package/lib/candidate/extract-fold.ts +10 -55
  16. package/lib/candidate/fts.ts +5 -23
  17. package/lib/candidate/importance.ts +65 -68
  18. package/lib/candidate/lookup.ts +124 -415
  19. package/lib/candidate/name-roles.ts +15 -33
  20. package/lib/candidate/own-name.ts +35 -32
  21. package/lib/candidate/place-attrs.ts +20 -13
  22. package/lib/candidate/schema.ts +29 -65
  23. package/lib/candidate/scoring.ts +54 -93
  24. package/lib/capital-schema.ts +21 -17
  25. package/lib/capitals.ts +29 -27
  26. package/lib/coincident-roles.ts +85 -55
  27. package/lib/convention/schema.ts +15 -16
  28. package/lib/convention.ts +144 -0
  29. package/lib/coverage-manifest-schema.ts +40 -45
  30. package/lib/currency-backfill.ts +34 -79
  31. package/lib/env.ts +1 -1
  32. package/lib/exact-match.ts +32 -21
  33. package/lib/extracts.ts +97 -74
  34. package/lib/fst/autocomplete.ts +54 -37
  35. package/lib/fst/builder.ts +37 -41
  36. package/lib/fst/deserialize-web.ts +16 -17
  37. package/lib/fst/format.ts +41 -23
  38. package/lib/fst/freshness.ts +43 -120
  39. package/lib/fst/matcher.ts +7 -5
  40. package/lib/fst/serialize.ts +54 -74
  41. package/lib/fst/types.ts +78 -75
  42. package/lib/{fst/index.ts → fst.ts} +1 -0
  43. package/lib/fts/query.ts +5 -36
  44. package/lib/fts.ts +280 -0
  45. package/lib/geonames/aliases.ts +73 -144
  46. package/lib/geonames/postal.ts +162 -71
  47. package/lib/index.ts +9 -8
  48. package/lib/interpolation.ts +48 -100
  49. package/lib/lookup.ts +155 -270
  50. package/lib/name-score.ts +9 -7
  51. package/lib/nsul/lookup.ts +37 -54
  52. package/lib/nsul/schema.ts +16 -43
  53. package/lib/paths.ts +159 -0
  54. package/lib/place-importance-schema.ts +62 -169
  55. package/lib/poi/lookup.ts +104 -121
  56. package/lib/poi/schema.ts +42 -26
  57. package/lib/polygon-schema.ts +10 -6
  58. package/lib/postal/city/alias/lookup.ts +22 -40
  59. package/lib/postal/city/alias/schema.ts +14 -10
  60. package/lib/postal/city/candidate-schema.ts +26 -20
  61. package/lib/postcode-point-lookup.ts +8 -27
  62. package/lib/primary-preference.ts +46 -126
  63. package/lib/proximity-rerank.ts +44 -33
  64. package/lib/ranking-weights.ts +105 -78
  65. package/lib/region-keys.ts +70 -42
  66. package/lib/reverse.ts +149 -86
  67. package/lib/schema.ts +73 -50
  68. package/lib/search-fetch.ts +90 -68
  69. package/lib/sqlite/convention-source.ts +15 -23
  70. package/lib/sqlite/utils.ts +19 -22
  71. package/lib/street/centroid/schema.ts +37 -42
  72. package/lib/street/{centroid/index.ts → centroid.ts} +16 -25
  73. package/lib/street/morphology/fst/builder.ts +60 -42
  74. package/lib/street/morphology/fst/loader.ts +40 -32
  75. package/lib/street/name-lookup.ts +45 -41
  76. package/lib/street/normalize.ts +88 -219
  77. package/lib/street/segment-schema.ts +116 -43
  78. package/lib/{street/index.ts → street.ts} +1 -1
  79. package/lib/types.ts +109 -150
  80. package/lib/unified-schema.ts +29 -29
  81. package/lib/uprn/existence.ts +30 -19
  82. package/lib/uprn/lookup.ts +29 -68
  83. package/lib/uprn/schema.ts +18 -40
  84. package/lib/weights-overlay-linker.ts +236 -252
  85. package/out/address/point/interpolation.d.ts +20 -23
  86. package/out/address/point/interpolation.d.ts.map +1 -1
  87. package/out/address/point/interpolation.js +42 -39
  88. package/out/address/point/interpolation.js.map +1 -1
  89. package/out/address/point/schema.d.ts +12 -35
  90. package/out/address/point/schema.d.ts.map +1 -1
  91. package/out/address/point/schema.js +3 -19
  92. package/out/address/point/schema.js.map +1 -1
  93. package/out/address/point.d.ts +57 -0
  94. package/out/address/point.d.ts.map +1 -0
  95. package/out/address/point.js +206 -0
  96. package/out/address/point.js.map +1 -0
  97. package/out/{address/index.js → address.d.ts} +2 -2
  98. package/out/address.d.ts.map +1 -0
  99. package/out/{address/index.d.ts → address.js} +2 -2
  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 +14 -74
  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 +82 -112
  110. package/out/build/candidate.d.ts.map +1 -1
  111. package/out/build/candidate.js +17 -100
  112. package/out/build/candidate.js.map +1 -1
  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 +8 -44
  118. package/out/build/slim.d.ts.map +1 -1
  119. package/out/build/slim.js +33 -94
  120. package/out/build/slim.js.map +1 -1
  121. package/out/candidate/alias-bags.d.ts +6 -6
  122. package/out/candidate/alias-bags.d.ts.map +1 -1
  123. package/out/candidate/alias-bags.js +16 -17
  124. package/out/candidate/alias-bags.js.map +1 -1
  125. package/out/candidate/ancestors/schema.d.ts +73 -57
  126. package/out/candidate/ancestors/schema.d.ts.map +1 -1
  127. package/out/candidate/ancestors/schema.js +56 -48
  128. package/out/candidate/ancestors/schema.js.map +1 -1
  129. package/out/candidate/ancestors/sidecar.d.ts +3 -8
  130. package/out/candidate/ancestors/sidecar.d.ts.map +1 -1
  131. package/out/candidate/ancestors/sidecar.js +15 -22
  132. package/out/candidate/ancestors/sidecar.js.map +1 -1
  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 +7 -15
  138. package/out/candidate/extract-fold.d.ts.map +1 -1
  139. package/out/candidate/extract-fold.js +5 -47
  140. package/out/candidate/extract-fold.js.map +1 -1
  141. package/out/candidate/fts.d.ts +5 -23
  142. package/out/candidate/fts.d.ts.map +1 -1
  143. package/out/candidate/fts.js +5 -23
  144. package/out/candidate/fts.js.map +1 -1
  145. package/out/candidate/importance.d.ts +60 -65
  146. package/out/candidate/importance.d.ts.map +1 -1
  147. package/out/candidate/importance.js +56 -63
  148. package/out/candidate/importance.js.map +1 -1
  149. package/out/candidate/lookup.d.ts +18 -55
  150. package/out/candidate/lookup.d.ts.map +1 -1
  151. package/out/candidate/lookup.js +82 -385
  152. package/out/candidate/lookup.js.map +1 -1
  153. package/out/candidate/name-roles.d.ts +9 -21
  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 +35 -32
  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 +27 -62
  166. package/out/candidate/schema.d.ts.map +1 -1
  167. package/out/candidate/schema.js +10 -22
  168. package/out/candidate/schema.js.map +1 -1
  169. package/out/candidate/scoring.d.ts +7 -8
  170. package/out/candidate/scoring.d.ts.map +1 -1
  171. package/out/candidate/scoring.js +52 -89
  172. package/out/candidate/scoring.js.map +1 -1
  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 +27 -25
  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 +72 -50
  184. package/out/coincident-roles.js.map +1 -1
  185. package/out/convention/schema.d.ts +14 -14
  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 +34 -39
  194. package/out/coverage-manifest-schema.d.ts.map +1 -1
  195. package/out/coverage-manifest-schema.js +25 -34
  196. package/out/coverage-manifest-schema.js.map +1 -1
  197. package/out/currency-backfill.d.ts +16 -38
  198. package/out/currency-backfill.d.ts.map +1 -1
  199. package/out/currency-backfill.js +19 -46
  200. package/out/currency-backfill.js.map +1 -1
  201. package/out/env.d.ts +1 -1
  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 +32 -21
  206. package/out/exact-match.js.map +1 -1
  207. package/out/extracts.d.ts +66 -55
  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 +29 -21
  212. package/out/fst/autocomplete.d.ts.map +1 -1
  213. package/out/fst/autocomplete.js +39 -28
  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 +37 -41
  217. package/out/fst/builder.js.map +1 -1
  218. package/out/fst/deserialize-web.d.ts +4 -4
  219. package/out/fst/deserialize-web.d.ts.map +1 -1
  220. package/out/fst/deserialize-web.js +15 -16
  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 +19 -81
  227. package/out/fst/freshness.d.ts.map +1 -1
  228. package/out/fst/freshness.js +31 -94
  229. package/out/fst/freshness.js.map +1 -1
  230. package/out/fst/matcher.d.ts +5 -4
  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 +28 -28
  235. package/out/fst/serialize.d.ts.map +1 -1
  236. package/out/fst/serialize.js +53 -71
  237. package/out/fst/serialize.js.map +1 -1
  238. package/out/fst/types.d.ts +78 -74
  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} +2 -1
  243. package/out/fst.d.ts.map +1 -0
  244. package/out/{fst/index.d.ts → fst.js} +2 -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 -62
  255. package/out/geonames/aliases.d.ts.map +1 -1
  256. package/out/geonames/aliases.js +45 -117
  257. package/out/geonames/aliases.js.map +1 -1
  258. package/out/geonames/postal.d.ts +84 -44
  259. package/out/geonames/postal.d.ts.map +1 -1
  260. package/out/geonames/postal.js +115 -73
  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 +9 -8
  267. package/out/index.d.ts.map +1 -1
  268. package/out/index.js +6 -5
  269. package/out/index.js.map +1 -1
  270. package/out/interpolation.d.ts +18 -55
  271. package/out/interpolation.d.ts.map +1 -1
  272. package/out/interpolation.js +27 -69
  273. package/out/interpolation.js.map +1 -1
  274. package/out/lookup.d.ts +23 -51
  275. package/out/lookup.d.ts.map +1 -1
  276. package/out/lookup.js +136 -234
  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 +51 -143
  299. package/out/place-importance-schema.d.ts.map +1 -1
  300. package/out/place-importance-schema.js +28 -143
  301. package/out/place-importance-schema.js.map +1 -1
  302. package/out/poi/lookup.d.ts +40 -47
  303. package/out/poi/lookup.d.ts.map +1 -1
  304. package/out/poi/lookup.js +69 -96
  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 +20 -23
  319. package/out/postal/city/alias/lookup.d.ts.map +1 -1
  320. package/out/postal/city/alias/lookup.js +17 -30
  321. package/out/postal/city/alias/lookup.js.map +1 -1
  322. package/out/postal/city/alias/schema.d.ts +14 -10
  323. package/out/postal/city/alias/schema.d.ts.map +1 -1
  324. package/out/postal/city/alias/schema.js +10 -8
  325. package/out/postal/city/alias/schema.js.map +1 -1
  326. package/out/postal/city/candidate-schema.d.ts +25 -19
  327. package/out/postal/city/candidate-schema.d.ts.map +1 -1
  328. package/out/postal/city/candidate-schema.js +21 -17
  329. package/out/postal/city/candidate-schema.js.map +1 -1
  330. package/out/{postal/index.d.ts → postal.d.ts} +1 -1
  331. package/out/postal.d.ts.map +1 -0
  332. package/out/{postal/index.js → postal.js} +1 -1
  333. package/out/postal.js.map +1 -0
  334. package/out/postcode-point-lookup.d.ts +6 -24
  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 +38 -92
  339. package/out/primary-preference.d.ts.map +1 -1
  340. package/out/primary-preference.js +15 -99
  341. package/out/primary-preference.js.map +1 -1
  342. package/out/proximity-rerank.d.ts +41 -30
  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 +98 -64
  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 +50 -31
  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 +78 -50
  355. package/out/reverse.d.ts.map +1 -1
  356. package/out/reverse.js +98 -61
  357. package/out/reverse.js.map +1 -1
  358. package/out/schema.d.ts +72 -49
  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 +12 -16
  367. package/out/sqlite/convention-source.d.ts.map +1 -1
  368. package/out/sqlite/convention-source.js +13 -22
  369. package/out/sqlite/convention-source.js.map +1 -1
  370. package/out/sqlite/utils.d.ts +19 -19
  371. package/out/sqlite/utils.d.ts.map +1 -1
  372. package/out/sqlite/utils.js +14 -15
  373. package/out/sqlite/utils.js.map +1 -1
  374. package/out/street/centroid/schema.d.ts +37 -42
  375. package/out/street/centroid/schema.d.ts.map +1 -1
  376. package/out/street/centroid/schema.js +9 -21
  377. package/out/street/centroid/schema.js.map +1 -1
  378. package/out/street/centroid.d.ts +30 -0
  379. package/out/street/centroid.d.ts.map +1 -0
  380. package/out/street/{centroid/index.js → centroid.js} +13 -24
  381. package/out/street/centroid.js.map +1 -0
  382. package/out/street/morphology/fst/builder.d.ts +20 -10
  383. package/out/street/morphology/fst/builder.d.ts.map +1 -1
  384. package/out/street/morphology/fst/builder.js +44 -34
  385. package/out/street/morphology/fst/builder.js.map +1 -1
  386. package/out/street/morphology/fst/loader.d.ts +33 -26
  387. package/out/street/morphology/fst/loader.d.ts.map +1 -1
  388. package/out/street/morphology/fst/loader.js +23 -22
  389. package/out/street/morphology/fst/loader.js.map +1 -1
  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 +34 -35
  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 +77 -194
  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.js → street.d.ts} +2 -2
  403. package/out/street.d.ts.map +1 -0
  404. package/out/{street/index.d.ts → street.js} +2 -2
  405. package/out/street.js.map +1 -0
  406. package/out/types.d.ts +84 -149
  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 +21 -57
  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 +138 -144
  431. package/out/weights-overlay-linker.d.ts.map +1 -1
  432. package/out/weights-overlay-linker.js +105 -159
  433. package/out/weights-overlay-linker.js.map +1 -1
  434. package/package.json +91 -596
  435. package/lib/address/point/index.ts +0 -328
  436. package/lib/convention/index.ts +0 -154
  437. package/lib/fts/index.ts +0 -361
  438. package/out/address/index.d.ts.map +0 -1
  439. package/out/address/index.js.map +0 -1
  440. package/out/address/point/index.d.ts +0 -53
  441. package/out/address/point/index.d.ts.map +0 -1
  442. package/out/address/point/index.js +0 -222
  443. package/out/address/point/index.js.map +0 -1
  444. package/out/ancestry/index.d.ts +0 -45
  445. package/out/ancestry/index.d.ts.map +0 -1
  446. package/out/ancestry/index.js +0 -55
  447. package/out/ancestry/index.js.map +0 -1
  448. package/out/convention/index.d.ts +0 -106
  449. package/out/convention/index.d.ts.map +0 -1
  450. package/out/convention/index.js +0 -93
  451. package/out/convention/index.js.map +0 -1
  452. package/out/fst/index.d.ts.map +0 -1
  453. package/out/fst/index.js.map +0 -1
  454. package/out/fts/index.d.ts +0 -184
  455. package/out/fts/index.d.ts.map +0 -1
  456. package/out/fts/index.js +0 -277
  457. package/out/fts/index.js.map +0 -1
  458. package/out/geonames/index.d.ts.map +0 -1
  459. package/out/geonames/index.js.map +0 -1
  460. package/out/nsul/index.d.ts.map +0 -1
  461. package/out/nsul/index.js.map +0 -1
  462. package/out/poi/index.d.ts.map +0 -1
  463. package/out/poi/index.js.map +0 -1
  464. package/out/postal/index.d.ts.map +0 -1
  465. package/out/postal/index.js.map +0 -1
  466. package/out/street/centroid/index.d.ts +0 -40
  467. package/out/street/centroid/index.d.ts.map +0 -1
  468. package/out/street/centroid/index.js.map +0 -1
  469. package/out/street/index.d.ts.map +0 -1
  470. package/out/street/index.js.map +0 -1
  471. package/out/uprn/index.d.ts.map +0 -1
  472. package/out/uprn/index.js.map +0 -1
  473. /package/lib/{geonames/index.ts → geonames.ts} +0 -0
  474. /package/lib/{nsul/index.ts → nsul.ts} +0 -0
  475. /package/lib/{poi/index.ts → poi.ts} +0 -0
  476. /package/lib/{postal/index.ts → postal.ts} +0 -0
  477. /package/lib/{uprn/index.ts → uprn.ts} +0 -0
@@ -2,25 +2,6 @@
2
2
  * @copyright Sister Software
3
3
  * @license AGPL-3.0
4
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 runtime resolves against ({@link WOFCandidateTableLookup} in
8
- * `packages/resolver-wof-wasm/lib/httpvfs/resolver.ts`), but reading a LOCAL `candidate.db` via
9
- * `node:sqlite` instead of sql.js-httpvfs. This is what makes the server/CLI resolver match the
10
- * browser: one 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
5
  */
25
6
  import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map";
26
7
  import { referentialFromPopulation } from "@mailwoman/core/resolver";
@@ -28,7 +9,7 @@ import { allRows } from "@mailwoman/core/utils";
28
9
  import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators";
29
10
  import { partitionByContainment } from "@mailwoman/resolver";
30
11
  import { haversineKm } from "@mailwoman/spatial";
31
- import { DatabaseClient } from "@mailwoman/sqlite/client";
12
+ import { SQLiteLookup } from "@mailwoman/sqlite/lookup";
32
13
  import { CANDIDATE_ANCESTOR_TABLE, CANDIDATE_INTERVAL_TABLE, intervalContains, } from "#candidate/ancestors/schema";
33
14
  import { CANDIDATE_FTS_TABLE } from "#candidate/fts";
34
15
  import { readGazetteerCoverageManifest } from "#coverage-manifest-schema";
@@ -38,41 +19,12 @@ import { applyProximityRerank } from "#proximity-rerank";
38
19
  import { REGION_CLASS_PLACETYPES, regionQualifierProbeKeys } from "#region-keys";
39
20
  import { hasColumn, hasTable } from "#sqlite-utils";
40
21
  import { normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize";
41
- export { rankByPrimaryPreference } from "#primary-preference";
42
- /**
43
- * FTS5-trigram over-fetch before the WORD-LEVEL re-rank. The trigram index stays the candidate GENERATOR (it is what
44
- * the artifact carries); the scoring moved off trigram-Jaccard on 2026-08-12 (#1614), which the aucklnad receipts
45
- * falsified as a typo measure: it scored the true transposition correction 'auckland' at 0.333 — below its own 0.34 bar
46
- * — while 'auckley' scored 0.375 and the 'gore bay' scrape 0.455, because shared generic suffixes count as trigram
47
- * evidence and transpositions count against it.
48
- */
49
22
  const FUZZY_FETCH = 40;
50
- /**
51
- * Minimum WORD-LEVEL similarity (max of Jaro-Winkler and normalized edit similarity — the `match/comparators`
52
- * primitives, deliberately WITHOUT `nameSimilarity`'s token-subset floor, which is a person-name rule that would hand
53
- * 'stanmore bay' to a place named 'Bay') for a fuzzy correction to count. Measured on the #1614 receipts:
54
- * 'aucklnad'→'auckland' 0.975 (in), →'auckley' 0.868 (in, but outranked), 'stanmore bay'→'gore bay' ~0.70 (out),
55
- * 'sacremento'→'sacramento' ~0.97 (in).
56
- */
57
23
  const WORD_FUZZY_MIN = 0.85;
58
- /**
59
- * The word-level correction similarity — see {@link WORD_FUZZY_MIN}.
60
- */
61
24
  function wordFuzzySimilarity(a, b) {
62
25
  return Math.max(jaroWinkler(a, b), levenshteinSimilarity(a, b));
63
26
  }
64
- /**
65
- * Postcode-containment re-rank check radius (km) — the SAME value the resolver's country pass measures at
66
- * (`POSTCODE_COUNTRY_COHERENCE_THRESHOLD_KM`, resolver/postcode-country-coherence.ts): a locality within this distance
67
- * of the postcode's own centroid counts as "containing" it. One number, two passes — a divergence here would make the
68
- * two mechanisms disagree about what is proximal.
69
- */
70
27
  const POSTCODE_CONTAINMENT_THRESHOLD_KM = 25;
71
- /**
72
- * Unpadded character-trigrams of `s`, OR'd into an FTS5 trigram MATCH query (each quoted so FTS treats it as a literal
73
- * term). Returns "" when `s` is shorter than a trigram or yields no clean grams — the caller then skips the fuzzy
74
- * probe.
75
- */
76
28
  function ftsTrigramQuery(s) {
77
29
  const grams = new Set();
78
30
  for (let i = 0; i + 3 <= s.length; i++) {
@@ -84,160 +36,72 @@ function ftsTrigramQuery(s) {
84
36
  return [...grams].map((g) => `"${g}"`).join(" OR ");
85
37
  }
86
38
  /**
87
- * Node {@link PlaceLookup} over `candidate.db`. Drop-in for {@link WOFSQLitePlaceLookup} in `createWOFResolver(backend)`
88
- * — same `findPlace` contract, population-first ranking.
39
+ * Node {@link PlaceLookup} over `candidate.db`.
89
40
  */
90
- export class WOFCandidateTableLookup {
91
- #db;
92
- /**
93
- * Resources this instance opened. A connection handed in by a caller is NOT in here, so disposal cannot reach it —
94
- * ownership is membership rather than a flag a later branch has to check.
95
- */
96
- #resources = new DisposableStack();
41
+ export class WOFCandidateTableLookup extends SQLiteLookup {
97
42
  #countryToID = new Map();
98
43
  #idToCountry = new Map();
99
44
  #placetypeToID = new Map();
100
45
  #idToPlacetype = new Map();
101
- /**
102
- * Prepared `(name_key, postcode)` probe for the #741 postal-city side-index — `undefined` when the
103
- * `postal_city_candidate` table isn't present, so a candidate.db built without it is byte-stable.
104
- */
105
46
  #postalCityProbe;
106
- /**
107
- * Prepared FTS5-trigram MATCH probe for the typo-tolerant fallback — `undefined` when the `candidate_fts` index isn't
108
- * present, so a candidate.db built without it is byte-stable (the fuzzy path is skipped, exactly like today).
109
- */
110
47
  #ftsProbe;
111
48
  /**
112
- * Prepared UNFILTERED existence probe (`name_key` present anywhere, ignoring country/placetype/bbox). Checks the
113
- * fuzzy fallback: fuzzy is a TYPO corrector, so it engages only when the name doesn't exist in the gazetteer at all.
114
- * A name that DOES exist but missed under the active filter is a filter miss (e.g. a placer misroute "Vienna,
115
- * Austria"→IT), not a spelling miss — fuzzing it would scrape an unrelated same-country place and defeat the
116
- * cascade's country-agnostic retry. Prepared only alongside `#ftsProbe`.
49
+ * Prepared unfiltered `name_key` existence probe, so fuzzy fallback runs only for true name misses.
117
50
  */
118
51
  #nameKeyExistsProbe;
119
- /**
120
- * Facts this candidate DB declares about itself — the coverage manifest (`country_coverage` + `country_bbox`) the
121
- * gazetteer build emits, read once at open. `undefined` when the artifact predates the manifest, so every consumer
122
- * (the hard-country coverage check, guard-B plausibility) falls back to its code constants byte-identically.
123
- */
124
52
  artifactCoverage;
125
- /**
126
- * `", importance"` when this artifact carries the #28 fame column, `""` when it does not — spliced into the probe's
127
- * SELECT list. Existence-restricted exactly like `#ftsProbe` and `#postalCityProbe` above, and for the same reason: a
128
- * candidate.db built before the column is a valid artifact, and naming a column it lacks would turn a stale gazetteer
129
- * into `no such column` on the first keystroke rather than into "no fame signal", which is what it is.
130
- */
131
53
  #importanceSelect;
132
- /**
133
- * Whether the artifact carries the #1730 `name_role` column — absent on pre-role builds, where the `excludeNameRoles`
134
- * filter degrades to a no-op rather than erroring on a missing column.
135
- */
136
54
  #hasNameRole;
137
55
  #variantAliasExemption;
138
- /**
139
- * `", name_role"` when the artifact carries the column — the probe SELECT rides it so the #1882 exemption can read
140
- * the stamp off the row; empty on a pre-role build.
141
- */
142
56
  #roleSelect;
143
- /**
144
- * Prepared chain probe over the `candidate_ancestor` sidecar — `undefined` when the artifact predates it.
145
- */
146
57
  #ancestorsProbe;
147
58
  #ancestorsCache = new Map();
148
- /**
149
- * Prepared interval-label probe over `candidate_interval` — `undefined` when the artifact predates the sidecar, which
150
- * is what makes the admin-containment re-rank (#1717 stage 2) capability-restricted: without it,
151
- * `FindPlaceQuery.regionQualifier` is ignored, no candidate carries a `containedByQualifier` stamp, and the resolver
152
- * walk reports the change `unavailable` instead of silently dead.
153
- */
154
59
  #intervalProbe;
155
60
  #intervalCache = new Map();
156
- /**
157
- * Prepared qualifier probe: the region-band rows (plus `country` — the region SLOT can hold a mislabeled country
158
- * name, "Moscow, Russia" parses region="Russia") for one folded qualifier key. `undefined` when the artifact lacks
159
- * the sidecar or the placetype dictionary lacks the band entirely.
160
- */
161
61
  #qualifierProbe;
162
62
  /**
163
- * The ancestor lineage of a resolved place — nearest-first (locality-tier → county → region → … → country), the same
164
- * order the FTS backend's `ancestorLineage` serves, read from the `candidate_ancestor` sidecar in one clustered
165
- * probe. Backs `ResolveOpts.includeAncestors` (#404) on this backend, which is what puts region-class ancestry in
166
- * front of the admin-coherence check (#1717).
167
- *
168
- * A PROPERTY, not a method, and assigned only when the artifact carries the sidecar: capability probes (`typeof
169
- * backend.ancestors === "function"` — the resolver's gap report) then read the ARTIFACT truthfully. A candidate.db
170
- * built before the sidecar reports the capability absent instead of presenting a method that answers `[]` for every
171
- * place, which would be an absence dressed as a negative answer.
63
+ * Ancestor lineage accessor, nearest-first, present only when the artifact supports it.
172
64
  */
173
65
  ancestors;
174
66
  constructor(opts) {
175
- if (opts.database) {
176
- this.#db = opts.database;
177
- }
178
- else if (opts.databasePath) {
179
- this.#db = this.#resources.use(new DatabaseClient(opts.databasePath, { readOnly: true }));
180
- }
181
- else {
182
- throw new Error("WOFCandidateTableLookup needs `databasePath` or `database`");
183
- }
184
- // The code tables are tiny (country/placetype dictionaries) — load them once at construction so
185
- // `findPlace` is a single B-tree probe with no dictionary round-trip.
186
- for (const r of allRows(this.#db.prepare("SELECT id, code FROM country_codes"))) {
67
+ super(opts);
68
+ for (const r of allRows(this.database.prepare("SELECT id, code FROM country_codes"))) {
187
69
  const code = String(r.code).toUpperCase();
188
70
  this.#countryToID.set(code, Number(r.id));
189
71
  this.#idToCountry.set(Number(r.id), code);
190
72
  }
191
- for (const r of allRows(this.#db.prepare("SELECT id, placetype FROM placetype_codes"))) {
73
+ for (const r of allRows(this.database.prepare("SELECT id, placetype FROM placetype_codes"))) {
192
74
  this.#placetypeToID.set(String(r.placetype), Number(r.id));
193
75
  this.#idToPlacetype.set(Number(r.id), String(r.placetype));
194
76
  }
195
- // #741 postal-city side-index: prepare the exact probe only if the table is present. Absent →
196
- // `#postalCityProbe` stays undefined → findPlace skips the postal-city path → byte-stable.
197
- if (hasTable(this.#db, POSTAL_CITY_CANDIDATE_TABLE)) {
198
- this.#postalCityProbe = this.#db.prepare(`SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`);
77
+ if (hasTable(this.database, POSTAL_CITY_CANDIDATE_TABLE)) {
78
+ this.#postalCityProbe = this.database.prepare(`SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`);
199
79
  }
200
- // FTS5-trigram fuzzy fallback: prepare the MATCH probe only if the index is present (the unified
201
- // gazetteer carries it; an older candidate.db doesn't → the fuzzy path is skipped, byte-stable).
202
- if (hasTable(this.#db, CANDIDATE_FTS_TABLE)) {
203
- this.#ftsProbe = this.#db.prepare(`SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`);
204
- this.#nameKeyExistsProbe = this.#db.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1");
80
+ if (hasTable(this.database, CANDIDATE_FTS_TABLE)) {
81
+ this.#ftsProbe = this.database.prepare(`SELECT name_key FROM ${CANDIDATE_FTS_TABLE} WHERE ${CANDIDATE_FTS_TABLE} MATCH ? ORDER BY bm25(${CANDIDATE_FTS_TABLE}) LIMIT ?`);
82
+ this.#nameKeyExistsProbe = this.database.prepare("SELECT 1 FROM candidate WHERE name_key = ? LIMIT 1");
205
83
  }
206
- // #28 fame column: probed ONCE here (it runs a PRAGMA, and `findPlace` is per-keystroke hot).
207
- this.#importanceSelect = hasColumn(this.#db, "candidate", "importance") ? ", importance" : "";
208
- this.#hasNameRole = hasColumn(this.#db, "candidate", "name_role");
209
- this.#variantAliasExemption = opts.variantAliasExemption === true;
84
+ // Optional columns are detected once here for hot-path reads.
85
+ this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : "";
86
+ this.#hasNameRole = hasColumn(this.database, "candidate", "name_role");
87
+ this.#variantAliasExemption = (opts.variantAliasExemption ?? "applied") === "applied";
210
88
  this.#roleSelect = this.#hasNameRole ? ", name_role" : "";
211
- // Ancestors sidecar (#1717): existence-restricted like the probes above, and the CAPABILITY checks with
212
- // it — see the `ancestors` property doc for why an older artifact must read as "no ancestors()"
213
- // rather than as a method that answers [] everywhere.
214
- if (hasTable(this.#db, CANDIDATE_ANCESTOR_TABLE)) {
215
- this.#ancestorsProbe = this.#db.prepare(`SELECT parent_spr_id, parent_placetype_id, parent_name FROM ${CANDIDATE_ANCESTOR_TABLE}` +
89
+ if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
90
+ this.#ancestorsProbe = this.database.prepare(`SELECT parent_spr_id, parent_placetype_id, parent_name FROM ${CANDIDATE_ANCESTOR_TABLE}` +
216
91
  " WHERE spr_id = ? ORDER BY depth ASC");
217
92
  this.ancestors = (id) => this.#ancestorLineage(id);
218
93
  }
219
- // Admin-containment re-rank (#1717 stage 2): conditioned on the interval half of the sidecar (built in
220
- // the same pass as the closure rows; probed separately so a hand-degraded artifact degrades
221
- // truthfully) AND on the placetype dictionary carrying the qualifier band at all.
222
- if (this.#ancestorsProbe && hasTable(this.#db, CANDIDATE_INTERVAL_TABLE)) {
223
- this.#intervalProbe = this.#db.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`);
94
+ if (this.#ancestorsProbe && hasTable(this.database, CANDIDATE_INTERVAL_TABLE)) {
95
+ this.#intervalProbe = this.database.prepare(`SELECT pre, post FROM ${CANDIDATE_INTERVAL_TABLE} WHERE spr_id = ?`);
224
96
  const bandIDs = [...REGION_CLASS_PLACETYPES, "country"]
225
97
  .map((placetype) => this.#placetypeToID.get(placetype))
226
98
  .filter((id) => id !== undefined);
227
99
  if (bandIDs.length) {
228
- this.#qualifierProbe = this.#db.prepare(`SELECT DISTINCT spr_id FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.join(",")}) LIMIT 8`);
100
+ this.#qualifierProbe = this.database.prepare(`SELECT DISTINCT spr_id FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.join(",")}) LIMIT 8`);
229
101
  }
230
102
  }
231
- // Coverage manifest (survey candidate #2): the artifact's own coverage facts, existence-restricted like
232
- // the probes above — a candidate.db built before the manifest reads `undefined` and consumers keep
233
- // their code-constant fallbacks byte-identically.
234
- this.artifactCoverage = readGazetteerCoverageManifest(this.#db);
103
+ this.artifactCoverage = readGazetteerCoverageManifest(this.database);
235
104
  }
236
- /**
237
- * The memoized chain read behind {@link ancestors}. Sync raw `.prepare()` on purpose — the backend contract's
238
- * `ancestors()` is synchronous (the sync-by-interface resolver-reader rule), and the sidecar row already carries the
239
- * parent's name and placetype, so this is one clustered probe with no join.
240
- */
241
105
  #ancestorLineage(id) {
242
106
  const pid = typeof id === "number" ? id : Number(id);
243
107
  if (!Number.isFinite(pid) || !this.#ancestorsProbe)
@@ -254,10 +118,6 @@ export class WOFCandidateTableLookup {
254
118
  this.#ancestorsCache.set(pid, lineage);
255
119
  return lineage;
256
120
  }
257
- /**
258
- * The interval label for one place, memoized. `null` is a real answer — the place has no recorded ancestry in the
259
- * source (absence semantics: UNVERIFIABLE, never a containment verdict) — and is cached as such.
260
- */
261
121
  #intervalLabel(sprID) {
262
122
  if (!this.#intervalProbe)
263
123
  return null;
@@ -269,13 +129,6 @@ export class WOFCandidateTableLookup {
269
129
  this.#intervalCache.set(sprID, label);
270
130
  return label;
271
131
  }
272
- /**
273
- * The qualifier's own rows in the candidate table: every region-band (+ country) place whose `name_key` matches one
274
- * of the qualifier's {@link regionQualifierProbeKeys} expansions. Alias keys participate — `Thüringen` finds the row
275
- * stored as `Thuringia` through the artifact's own alias keying, which is precisely the variant-form bridge the
276
- * admin-coherence verdicts' fold-equality bound cannot offer (its stated v1 bound). Empty = the qualifier names
277
- * nothing the artifact knows; the caller then stamps `false` everywhere and reorders nothing.
278
- */
279
132
  #qualifierRegionIDs(qualifier, country) {
280
133
  const ids = new Set();
281
134
  if (!this.#qualifierProbe)
@@ -290,10 +143,8 @@ export class WOFCandidateTableLookup {
290
143
  return ids;
291
144
  }
292
145
  /**
293
- * Is `sprID` contained by ANY of the qualifier's rows? Interval first — {@link intervalContains}, O(1), reflexive —
294
- * then the closure rows where intervals abstain: the interval forest encodes only the CANONICAL parent per place, so
295
- * a `false` there means "not contained along the canonical hierarchy", and the chain probe (one clustered read of
296
- * ≤{@link MAX_ANCESTOR_DEPTH} rows) is the complete record that determines the result.
146
+ * Whether `sprID` is contained by any qualifier row, using interval labels first
147
+ * and the ancestor chain as fallback.
297
148
  */
298
149
  #containedByQualifier(sprID, qualifierIDs, qualifierLabels) {
299
150
  if (qualifierIDs.has(sprID))
@@ -304,23 +155,8 @@ export class WOFCandidateTableLookup {
304
155
  return this.#ancestorLineage(sprID).some((ancestor) => qualifierIDs.has(Number(ancestor.id)));
305
156
  }
306
157
  /**
307
- * The #1717 stage-2 re-rank over one lookup's final row set. Three steps, each additive:
308
- *
309
- * 1. Resolve the qualifier to its region-band rows ({@link #qualifierRegionIDs}) and stamp every existing row's
310
- * `containedByQualifier` — the stamp is the trace surface, written even when nothing reorders.
311
- * 2. INJECT contained same-key candidates the country scope hid: the deciding-site measurement (2026-08-18, the #1729
312
- * lesson re-confirmed) showed `Weimar, Thüringen` under the en-US locale probes `country_id = US`, so the DE row
313
- * is not IN the list and no reorder of the list can reach it. The injection probe runs the same exact fold (and,
314
- * on a contained-miss, the qualifier-strip variant restricted to primary keys — the #1626 alias-scrape guard)
315
- * under the SHAPE conds only, appends contained rows not already present, and never removes anything — recall can
316
- * only widen. The typo-fuzzy tier is deliberately not probed: a qualifier cannot vouch for a name the gazetteer
317
- * does not carry.
318
- * 3. Partition contained-first — the SHARED {@link partitionByContainment} (tier-safe, stable; the resolver walk runs
319
- * the same function after its fame re-rank, one function at both deciding sites per the #861 rule) — then
320
- * re-window to `limit`.
321
- *
322
- * A qualifier that matches nothing stamps `false` everywhere and reorders nothing — byte-identical answers, and the
323
- * walk's verdict reads `no_contained_candidate` rather than `unavailable` (the question WAS asked).
158
+ * Apply admin-containment re-rank: stamp containment, inject contained misses,
159
+ * then partition contained-first.
324
160
  */
325
161
  #applyAdminContainment(rows, qualifier, country, opts) {
326
162
  const qualifierIDs = this.#qualifierRegionIDs(qualifier, country);
@@ -343,7 +179,7 @@ export class WOFCandidateTableLookup {
343
179
  `${this.#importanceSelect} FROM candidate WHERE ${["name_key = ?", ...opts.shapeFilters, ...(primaryOnly ? ["is_primary = 1"] : [])].join(" AND ")} ` +
344
180
  "ORDER BY neg_rank ASC LIMIT ?";
345
181
  const injectFrom = (key, primaryOnly) => {
346
- const fetched = allRows(this.#db.prepare(injectSQL(primaryOnly)), key, ...opts.shapeParams, RERANK_FETCH);
182
+ const fetched = allRows(this.database.prepare(injectSQL(primaryOnly)), key, ...opts.shapeParams, RERANK_FETCH);
347
183
  for (const row of fetched) {
348
184
  const sprID = Number(row.spr_id);
349
185
  if (present.has(sprID) || !contained(sprID))
@@ -353,16 +189,7 @@ export class WOFCandidateTableLookup {
353
189
  }
354
190
  };
355
191
  injectFrom(opts.nameKey, false);
356
- // #1731: the dependent-locality band. A locality query's filter group (locality/borough/localadmin)
357
- // cannot reach a neighbourhood-tier namesake, so a CONTAINED one is structurally invisible no matter
358
- // how the list reorders — the Astoria class: Queens' Astoria is a WOF neighbourhood, and the walk
359
- // answered the Oregon locality under `qualifier="NY"` because nothing in the pool sat under NY. The
360
- // widening is injection-only and triple-conditioned: the band is explicit (neighbourhood/macrohood/microhood
361
- // — never region or country tiers), admission still requires the sidecar's containment proof, and only
362
- // primary-keyed rows enter (an alias-keyed neighbourhood is the #1626 scrape class). Recall can only
363
- // widen, and only toward rows the qualifier vouches for. `opts.shapeFilters`' bbox clause is
364
- // deliberately not carried: containment is the stronger constraint, and the two co-occurring is not a
365
- // measured shape.
192
+ // Also inject contained neighborhood-band namesakes (primary-keyed only).
366
193
  const bandIDs = ["neighbourhood", "macrohood", "microhood"]
367
194
  .map((placetype) => this.#placetypeToID.get(placetype))
368
195
  .filter((id) => id !== undefined);
@@ -370,7 +197,7 @@ export class WOFCandidateTableLookup {
370
197
  const bandSQL = "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
371
198
  `${this.#importanceSelect} FROM candidate WHERE name_key = ? AND placetype_id IN (${bandIDs.map(() => "?").join(",")}) AND is_primary = 1 ` +
372
199
  "ORDER BY neg_rank ASC LIMIT ?";
373
- const fetched = allRows(this.#db.prepare(bandSQL), opts.nameKey, ...bandIDs, RERANK_FETCH);
200
+ const fetched = allRows(this.database.prepare(bandSQL), opts.nameKey, ...bandIDs, RERANK_FETCH);
374
201
  for (const row of fetched) {
375
202
  const sprID = Number(row.spr_id);
376
203
  if (present.has(sprID) || !contained(sprID))
@@ -379,8 +206,7 @@ export class WOFCandidateTableLookup {
379
206
  injected.push({ ...row, effectiveNegRank: row.neg_rank, demoted: false, containedByQualifier: true });
380
207
  }
381
208
  }
382
- // The strip variant mirrors the cascade's discipline: tried only when the exact fold vouched for
383
- // nothing, and primary-keyed only (a stripped surface never named an alias — #1626).
209
+ // Try stripped-key injection only when exact-key containment found no row.
384
210
  if (!injected.length &&
385
211
  !rows.some((row) => row.containedByQualifier) &&
386
212
  opts.strippedKey &&
@@ -391,22 +217,12 @@ export class WOFCandidateTableLookup {
391
217
  return rows;
392
218
  return partitionByContainment([...rows, ...injected], (row) => row.containedByQualifier === true, (row) => !row.demoted && !row.fuzzy).slice(0, opts.limit);
393
219
  }
394
- /**
395
- * Does this query want a locality-tier place? Postal-city aliases (#741) are all localities.
396
- */
397
220
  #wantsLocality(placetype) {
398
221
  if (!placetype)
399
222
  return true;
400
223
  const want = Array.isArray(placetype) ? placetype : [placetype];
401
224
  return expandPlacetypeFilter(want).includes("locality");
402
225
  }
403
- /**
404
- * The postcode-containment anchor: the postcode's own centroid row in the candidate table, keyed whitespace-stripped
405
- * (#920 — the same fold the build applies to postcode rows), country-scoped when the query is, first
406
- * coordinate-bearing row wins. null when the candidate table carries no such postcode — the re-rank then abstains,
407
- * because a recall gap is not evidence for the name match. Meaning-of-zero: a 0,0 row is the build's unlocated
408
- * sentinel, never a real centroid.
409
- */
410
226
  #postcodeAnchor(postcode, country) {
411
227
  const placetypeID = this.#placetypeToID.get("postalcode");
412
228
  if (placetypeID === undefined)
@@ -416,11 +232,11 @@ export class WOFCandidateTableLookup {
416
232
  if (country) {
417
233
  const countryID = this.#countryToID.get(country.toUpperCase());
418
234
  if (countryID === undefined)
419
- return null; // a country the candidate table doesn't carry
235
+ return null; // Unknown country in this artifact.
420
236
  conds.push("country_id = ?");
421
237
  params.push(countryID);
422
238
  }
423
- const row = this.#db
239
+ const row = this.database
424
240
  .prepare(`SELECT latitude, longitude FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT 1`)
425
241
  .get(...params);
426
242
  if (!row || (Number(row.latitude) === 0 && Number(row.longitude) === 0))
@@ -431,9 +247,7 @@ export class WOFCandidateTableLookup {
431
247
  let text = (query.text ?? "").trim();
432
248
  if (!text)
433
249
  return [];
434
- // #920 name law, candidate-key edition: postcode rows are keyed by their whitespace-stripped
435
- // form at build (the GeoNames fold normalizes '624 66' → '62466'), so a postcode-typed query
436
- // strips internal whitespace before keying. Postcode-only — locality names keep their spaces.
250
+ // Postcode queries use whitespace-stripped keying.
437
251
  const wantsPostcode = [query.placetype].flat().includes("postalcode");
438
252
  if (wantsPostcode) {
439
253
  text = text.replaceAll(/\s+/g, "");
@@ -441,12 +255,6 @@ export class WOFCandidateTableLookup {
441
255
  const nameKey = normalizeLocalityForKey(text);
442
256
  if (!nameKey)
443
257
  return [];
444
- // #741: postcode-keyed postal-city alias. An exact `(name_key, postcode)` hit resolves a
445
- // user-typed POSTAL city ("Antioch", 37013) to the geographic locality the postcode sits in
446
- // ("Nashville"), bypassing the population/region ranking that can't see the postcode. Conditioned on
447
- // the side-index being present, a postcode in the query, and a locality-tier request — so the
448
- // common (no-postcode / non-locality) path is untouched. A hit short-circuits: the postcode is
449
- // an exact, high-confidence disambiguator, so we return the single geographic locality.
450
258
  if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
451
259
  const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim());
452
260
  if (hit) {
@@ -458,18 +266,26 @@ export class WOFCandidateTableLookup {
458
266
  country: query.country?.toUpperCase() ?? "",
459
267
  lat: Number(hit.latitude),
460
268
  lon: Number(hit.longitude),
269
+ parent_id: null,
461
270
  score: 1,
271
+ distanceKm: null,
462
272
  exactMatch: true,
273
+ prominence: null,
274
+ population: null,
275
+ referential: null,
276
+ encyclopedic: null,
277
+ importance: null,
278
+ bbox: null,
279
+ mismatch: null,
280
+ containedByQualifier: null,
281
+ regionScopeMiss: null,
282
+ variantAliasExempted: null,
463
283
  },
464
284
  ];
465
285
  }
466
286
  }
467
287
  const limit = Math.max(1, query.limit ?? 10);
468
- // Filter conds shared by the exact-key + strip-fallback probes (everything but name_key). The
469
- // SHAPE subset (placetype/bbox/primary — everything but the country scope) is kept separately
470
- // because the admin-containment injection probe (#1717 stage 2) runs under the shape conds
471
- // WITHOUT the country: bypassing a locale-inferred country scope for a qualifier-vouched
472
- // candidate is the change's whole point and the one filter injection may cross.
288
+ // Shape filters are tracked separately from the shared country scope for exact and stripped probes.
473
289
  const filters = [];
474
290
  const filterParams = [];
475
291
  const shapeFilters = [];
@@ -477,13 +293,12 @@ export class WOFCandidateTableLookup {
477
293
  if (query.country) {
478
294
  const cid = this.#countryToID.get(query.country.toUpperCase());
479
295
  if (cid === undefined)
480
- return []; // a country the candidate table doesn't carry
296
+ return []; // Unknown country in this artifact.
481
297
  filters.push("country_id = ?");
482
298
  filterParams.push(cid);
483
299
  }
484
300
  if (query.placetype) {
485
- // Shared placetype-equivalence expansion (a `locality` query must also reach borough /
486
- // localadmin). `postalcode` maps to no admin placetype here → empty → no rows.
301
+ // Expand placetype equivalents (e.g. locality includes borough/localadmin).
487
302
  const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype];
488
303
  const ids = expandPlacetypeFilter(want)
489
304
  .map((t) => this.#placetypeToID.get(t))
@@ -498,102 +313,46 @@ export class WOFCandidateTableLookup {
498
313
  shapeFilters.push("latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?");
499
314
  shapeParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon);
500
315
  }
501
- // The re-reading guard (#1632, the #1626 rationale generalized to the caller): a probe whose surface
502
- // is a token taken out of a longer classified span never NAMED an alias, so alias-keyed rows must not
503
- // answer it — 'Savile Row''s token 'Row' resolved Rhu, Scotland (585 km) through the village's
504
- // historical-name alias key. Whole-input bare probes never set this, keeping the exonym recall the
505
- // #1546 note protects (Москва's alias rows answer 'Moscow').
316
+ // Optional guard: restrict to primary rows for token-extracted probes.
506
317
  if (query.primaryOnly) {
507
318
  shapeFilters.push("is_primary = 1");
508
319
  }
509
- // The role guard (#1730): a probe may refuse abbreviation/gloss alias rows while keeping the
510
- // role-NULL exonym tier open — the distinction `primaryOnly` cannot express. Degrades to a no-op
511
- // on an artifact without the column.
320
+ // The role filter is skipped on an artifact lacking `name_role`.
512
321
  if (query.excludeNameRoles?.length && this.#hasNameRole) {
513
322
  shapeFilters.push(`(name_role IS NULL OR name_role NOT IN (${query.excludeNameRoles.map(() => "?").join(",")}))`);
514
323
  shapeParams.push(...query.excludeNameRoles);
515
324
  }
516
- // The main-probe conds are country-then-shape, exactly the order they have always been.
517
325
  filters.push(...shapeFilters);
518
326
  filterParams.push(...shapeParams);
519
- // Region scope: when the cascade resolves a region and passes it down as `parentID` (the walk sets
520
- // `query.parentID = parentResolved.id`), the candidate build stamps each place's region-tier ancestor
521
- // id into `region_id` (build-candidate.ts `regionOf`), and that id equals the resolved region's WOF id
522
- // — so `region_id = parentID` scopes the probe to in-region rows. Without it a bare same-name probe is
523
- // population-first and "Springfield, IL" (parentID = Illinois) drops to the larger Springfield, MO.
524
- // Kept OUT of the shared `filters` so a region MISS falls back to the unscoped cascade below: a
525
- // country/non-region parent (no `region_id` match), a `region_id=0` row (place with no region
526
- // ancestor), or a wrong parent degrades to today's behavior — never worse, recall-safe by construction.
527
- const regionParentID = query.parentID || undefined;
327
+ // Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
328
+ const regionParentID = query.parentID || null;
528
329
  const probe = (nk, regionID, countryID) => {
529
330
  const conds = ["name_key = ?", ...filters];
530
331
  const params = [nk, ...filterParams];
531
- if (regionID !== undefined) {
332
+ if (regionID !== null) {
532
333
  conds.push("region_id = ?");
533
334
  params.push(regionID);
534
335
  }
535
- // #1585: the fuzzy tier's country scope — only the corrected-key probes pass this.
536
336
  if (typeof countryID === "number") {
537
337
  conds.push("country_id = ?");
538
338
  params.push(countryID);
539
339
  }
540
- // Fetch population-ordered (the clustered-key order — a cheap ordered scan), over-fetching to
541
- // RERANK_FETCH so the bounded cross-country primary-preference re-rank (below) can promote the
542
- // intended primary even when a cluster of more-populous foreign aliases sits ahead of it. `is_primary`
543
- // + `country_id` feed that re-rank. A single-country probe (a country filter, or all rows same
544
- // country) re-ranks to the identical population order, so the common path is untouched.
545
- // `population` rides along for the REFERENTIAL score on the result (ROAD_TO_V9 §2) — one more
546
- // column off a clustered row the probe already reads, and it is NOT what the probe orders by:
547
- // `neg_rank` remains the sort key, so this changes no ordering, only what the result reports.
548
- // `importance` (#28) rides along on the same terms, and there is deliberately NO `ORDER BY` on it:
549
- // the fame prior is applied by the RESOLVER (`resolver/toponym-prior.ts`), which alone knows
550
- // whether the query was bare enough to deserve it. A backend that pre-sorted by fame would apply
551
- // it to every lookup, including the qualified addresses the D-rule guard exists to protect.
340
+ // Population-ordered fetch over-fetches for the bounded primary-preference rerank.
552
341
  const sql = "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank, is_primary, population" +
553
342
  `${this.#importanceSelect}${this.#roleSelect} FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`;
554
- const fetched = allRows(this.#db.prepare(sql), ...params, Math.max(limit, RERANK_FETCH));
343
+ const fetched = allRows(this.database.prepare(sql), ...params, Math.max(limit, RERANK_FETCH));
555
344
  return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption);
556
345
  };
557
- // The exact → qualifier-strip → typo-fuzzy probe cascade, run at a fixed region scope. Region scoping
558
- // only tightens an already-population-first pick, so a region MISS re-runs the whole cascade unscoped
559
- // (below) rather than dropping a place that has no in-region row.
560
346
  const cascade = (regionID) => {
561
347
  let rows = probe(nameKey, regionID);
562
348
  if (!rows.length) {
563
- // Query-side qualifier-strip fallback: an OA locality with a qualifier the gazetteer's
564
- // canonical name omits ("Lenk im Simmental" → "Lenk", "Roche VD"). Tried ONLY on an exact
565
- // miss; the cascade's region bbox disambiguates any base-name ambiguity.
566
349
  const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text));
567
350
  if (strippedKey && strippedKey !== nameKey) {
568
- // #1626: a stripped probe may answer only through a NON-PRIMARY alias key, which is a
569
- // scrape, not a qualifier match — 'Savile Row' stripped to 'row' resolved Rhu, Scotland
570
- // (585 km) through the village's historical-name alias. The legitimate qualifier class
571
- // matches the place's own primary key ('Lenk im Simmental' → the Lenk row keyed 'lenk',
572
- // is_primary=1), so refusing alias-keyed rows keeps every intended case and kills the
573
- // scrape. The alias tier remains fully available to EXACT queries — only the stripped
574
- // RETRY loses it, because the query's own surface never named the alias.
351
+ // Stripped probes only accept primary rows to avoid alias-scrape matches.
575
352
  rows = probe(strippedKey, regionID).filter((r) => r.is_primary === 1);
576
353
  }
577
354
  }
578
- // Typo-tolerant fallback (the unified gazetteer's fuzzy mode): an exact + strip miss may be a
579
- // misspelling the normalized key can't reach. FTS5-trigram fetches a loose set; we re-rank by
580
- // trigram-Jaccard (the admin backend's measure) and probe the best name_keys, so a typo resolves
581
- // the same on either backend. The country/placetype/bbox/region filters still apply via `probe`.
582
- // Skipped when the index is absent (byte-stable for an older candidate.db).
583
- //
584
- // Condition: only when the name doesn't exist in the gazetteer AT ALL (unfiltered). A name that exists
585
- // but missed under the active country/placetype/bbox filter is a FILTER miss, not a spelling miss
586
- // — fuzzing it scrapes an unrelated same-filter place ("Vienna, Austria" misrouted to IT would
587
- // pull a tiny Italian name_key near Siena) and masks the cascade's country-agnostic retry that
588
- // correctly lands population-first Vienna AT. The exact/strip probes already covered the real name.
589
- //
590
- // NEVER for postcodes: fuzzy is a typo corrector for place NAMES, and a "corrected" postcode is a
591
- // DIFFERENT postcode. The 2026-08-05 Code-Point swap exposed the trap at scale: Northern Ireland's
592
- // `BT3 9QQ` (absent — no permissive NI source) trigram-matched Sheffield's `S3 9QQ` (Jaccard 0.4
593
- // on {39q, 9qq}) and resolved 200+ km wrong with full confidence. An unknown postcode must abstain.
594
- // #1585: a locale HINT scopes the typo tier to its country. Only when no hard `country`
595
- // filter is active (that is already narrower); a scope naming a country the table doesn't
596
- // carry is a SCOPED-EMPTY — the fuzzy tier abstains rather than falling through worldwide.
355
+ // Typo-tolerant fallback for true name misses only, skipping postcodes and respecting the fuzzy-country scope.
597
356
  const fuzzyCountryID = !query.country && query.fuzzyCountry ? this.#countryToID.get(query.fuzzyCountry.toUpperCase()) : undefined;
598
357
  const fuzzyScopedOut = !query.country && !!query.fuzzyCountry && typeof fuzzyCountryID !== "number";
599
358
  if (!rows.length &&
@@ -608,15 +367,13 @@ export class WOFCandidateTableLookup {
608
367
  const ranked = hits
609
368
  .map((h) => ({ nk: String(h.name_key), s: wordFuzzySimilarity(nameKey, String(h.name_key)) }))
610
369
  .filter((h) => h.s >= WORD_FUZZY_MIN)
611
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
370
+ // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array. toSorted would double-allocate on a hot path
612
371
  .sort((a, b) => b.s - a.s);
613
372
  const seen = new Set();
614
373
  for (const h of ranked) {
615
374
  if (seen.has(h.nk))
616
375
  continue;
617
376
  seen.add(h.nk);
618
- // #17: stamp the tier. These rows answer a name the gazetteer does not carry, so they are
619
- // fuzzy matches and `exactMatch` below must say so — see `RankedRow.fuzzy`.
620
377
  rows.push(...probe(h.nk, regionID, fuzzyCountryID).map((r) => ({ ...r, fuzzy: true })));
621
378
  if (rows.length >= limit)
622
379
  break;
@@ -627,25 +384,11 @@ export class WOFCandidateTableLookup {
627
384
  return rows;
628
385
  };
629
386
  let rows = cascade(regionParentID);
630
- // #1731: whether the rows the caller receives came from the UNSCOPED fallback below — the backend's
631
- // interior check the resolver-side trace (#1721) cannot otherwise see. Stamped onto every returned
632
- // place, because the re-admission path is exactly where a wrong-instance namesake enters.
633
387
  let regionScopeMiss = false;
634
- // Region-scope fallback: if scoping to the parent region found nothing across the whole cascade, retry
635
- // unscoped so a place with no in-region row (missing ancestry, or a country/non-region parent) still
636
- // resolves exactly as it does today. Only when a region scope was actually applied.
637
- if (!rows.length && regionParentID !== undefined) {
638
- rows = cascade(undefined);
388
+ if (!rows.length && regionParentID !== null) {
389
+ rows = cascade(null);
639
390
  regionScopeMiss = rows.length > 0;
640
391
  }
641
- // Postcode-containment coherence (#31, Mechanism 2): re-rank the rows by proximity to the postcode's
642
- // own centroid, so the locality that CONTAINS the postcode wins the name-match tie (the "Paris" that
643
- // holds 75001, not the one that holds a 75001-free namesake). The resolver sends this flag on locality
644
- // lookups when `ResolveOpts.postcodeContainmentCoherence` is on. Strictly beneath the #741 postal-city
645
- // short-circuit above — an exact (name, postcode) hit IS the answer and outranks any re-rank — and after
646
- // the region-scope fallback, so it sees the final row set. Rows within the radius sort by distance first;
647
- // the out-of-radius tail keeps its original population-first order. No in-radius row, or no postcode row in
648
- // the candidate table → unchanged (byte-identical to the flag-off path).
649
392
  if (query.postcode &&
650
393
  query.postcodeContainmentCoherence === true &&
651
394
  this.#wantsLocality(query.placetype) &&
@@ -664,19 +407,15 @@ export class WOFCandidateTableLookup {
664
407
  }
665
408
  }
666
409
  if (insideThreshold.length) {
667
- // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
410
+ // oxlint-disable-next-line unicorn/no-array-sort -- hot-path in-place sort
668
411
  insideThreshold.sort((a, b) => a.distanceKm - b.distanceKm);
669
412
  rows = [...insideThreshold.map(({ row }) => row), ...outsideThreshold];
670
413
  }
671
414
  }
672
415
  }
673
- // Admin-containment re-rank (#1717 stage 2): LAST, on the final row set — the qualifier is the
674
- // address's outermost explicit statement, so its partition outranks the postcode-proximity order
675
- // above (contained rows keep that order among themselves). Capability-conditioned on the sidecar
676
- // (`#qualifierProbe`); when the artifact predates it, `regionQualifier` is ignored, no stamp is
677
- // written, and the resolver walk reports the change `unavailable`.
416
+ // Admin-containment re-rank runs last.
678
417
  if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
679
- rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
418
+ rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country ?? null, {
680
419
  nameKey,
681
420
  strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
682
421
  shapeFilters,
@@ -686,84 +425,42 @@ export class WOFCandidateTableLookup {
686
425
  }
687
426
  const candidates = rows.map((row) => {
688
427
  const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null;
428
+ const parent = this.#ancestorLineage(Number(row.spr_id))[0];
429
+ const hasPopulation = row.population !== null && row.population > 0;
689
430
  return {
690
431
  id: Number(row.spr_id),
691
432
  name: String(row.name ?? ""),
692
433
  placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? ""),
693
- // Surfaced so the cascade can country-restrict a postcode by the resolved locality (an ambiguous
694
- // international postcode like 10115 = Berlin DE AND New York US must not out-resolve the city).
695
434
  country: this.#idToCountry.get(Number(row.country_id)) ?? "",
696
435
  lat: Number(row.latitude),
697
436
  lon: Number(row.longitude),
698
- // `score` stays the RAW population rank (`-neg_rank`) — it feeds the resolver walk's absolute
699
- // `minWinningScore` floor (`resolve.ts`), which must see real prominence, never a penalized value.
437
+ parent_id: parent ? Number(parent.id) : null,
700
438
  score: -Number(row.neg_rank),
701
- // `prominence` carries the bounded cross-country primary preference (the effective, penalty-adjusted
702
- // rank). The walk ORDERS candidates by `prominence ?? score` (`resolve.ts`), so this is what makes the
703
- // re-rank actually stick through resolution — without it the walk re-sorts by raw `score` and a
704
- // more-populous foreign alias (Changchun for "Cancun") wins back the node. Equals `score` for every
705
- // un-penalized row (primaries + same-country aliases), so the common ordering is unchanged.
439
+ distanceKm: null,
706
440
  prominence: -Number(row.effectiveNegRank),
707
- // Every candidate row IS an exact normalized-name (or alias/abbrev) match — the cascade's exact tier
708
- // accepts alias-exact hits ("New York City" → New York) the same as canonical — EXCEPT a cross-country
709
- // alias that lost the bounded contest to a same-key primary (`demoted`): it drops to the partial tier so
710
- // the walk's country posterior can't cross back over the primary (see `RankedRow.demoted`) — and
711
- // EXCEPT a row the typo-corrector produced (`fuzzy`), which by definition answers a name the
712
- // gazetteer does not carry (see `RankedRow.fuzzy`).
713
441
  exactMatch: !row.demoted && !row.fuzzy,
714
- // #1731: emitted ONLY when a region scope was applied, missed, and the unscoped fallback
715
- // produced this row — the re-admission path. Absence means the question never arose.
716
- ...(regionScopeMiss ? { regionScopeMiss: true } : {}),
717
- // #1717 stage 2 — the containment stamp, tri-state: emitted ONLY when the question was
718
- // asked (a `regionQualifier` query over a sidecar-bearing artifact); its absence is what
719
- // the resolver walk reports as `unavailable` (meaning-of-zero).
720
- ...(row.containedByQualifier === undefined ? {} : { containedByQualifier: row.containedByQualifier }),
721
- // #1893 — the exemption's firing mark, carried only when the ranker actually spared this row
722
- // the cross-country penalty (see RankedRow.variantExempted).
723
- ...(row.variantExempted ? { variantAliasExempted: true } : {}),
724
- // The two-score split's carry (ROAD_TO_V9 §2). `referential` names the prominence this
725
- // backend has always ordered by — `neg_rank` IS `-log10(population + 1)`, so the score and
726
- // the sort key are two readings of the same number.
727
- ...(row.population === null || row.population <= 0
728
- ? {}
729
- : { population: row.population, referential: referentialFromPopulation(row.population) }),
730
- // #28: the fame prior, from the `importance` column the candidate build joins in. Emitted ONLY
731
- // when the artifact measured this place — an absent field is what `rankByImportance` reads as
732
- // "does not participate", and a 0 would be a claim nobody made (meaning-of-zero).
733
- //
734
- // The field name matches the column because they hold the same thing: the score source's
735
- // BLENDED prior — the concordance's encyclopedia-derived channel where a concordance matched, a
736
- // population-derived proxy everywhere else. It is NOT the strict `encyclopedic` channel
737
- // `place-importance-schema.ts` defines, and it deliberately does not land in that field: the
738
- // strict channel was measured on 2026-08-10 and covers eleven countries, none of them CA/AU/RU,
739
- // which makes it inert on three of the four homonym contests the prior exists to settle.
740
- // `PlaceCandidate.encyclopedic` stays reserved for a strict-channel source (the FTS backend's
741
- // clauses are strict and today emit NULL for everything — no shipped admin DB has the split
742
- // table at all). See `candidate-schema.ts` → {@link CandidateTable.importance}.
743
- ...(typeof row.importance === "number" && Number.isFinite(row.importance)
744
- ? { importance: row.importance }
745
- : {}),
746
- ...(hasBbox
442
+ regionScopeMiss: regionScopeMiss ? true : null,
443
+ containedByQualifier: row.containedByQualifier ?? null,
444
+ variantAliasExempted: row.variantExempted ? true : null,
445
+ population: hasPopulation ? row.population : null,
446
+ referential: hasPopulation ? referentialFromPopulation(row.population) : null,
447
+ encyclopedic: null,
448
+ importance: typeof row.importance === "number" && Number.isFinite(row.importance) ? row.importance : null,
449
+ bbox: hasBbox
747
450
  ? {
748
- bbox: {
749
- minLat: Number(row.min_lat),
750
- maxLat: Number(row.max_lat),
751
- minLon: Number(row.min_lon),
752
- maxLon: Number(row.max_lon),
753
- },
451
+ minLat: Number(row.min_lat),
452
+ maxLat: Number(row.max_lat),
453
+ minLon: Number(row.min_lon),
454
+ maxLon: Number(row.max_lon),
754
455
  }
755
- : {}),
456
+ : null,
457
+ mismatch: null,
756
458
  };
757
459
  });
758
- // Proximity re-rank (#938) — the shared implementation, so the browser byte-range twin runs the same
759
- // code rather than the same constants. See proximity-rerank.ts for why that distinction mattered.
760
460
  if (query.bias && query.bias.length) {
761
461
  applyProximityRerank(candidates, query.bias);
762
462
  }
763
463
  return candidates;
764
464
  }
765
- [Symbol.dispose]() {
766
- this.#resources[Symbol.dispose]();
767
- }
768
465
  }
769
466
  //# sourceMappingURL=lookup.js.map