@mailwoman/resolver-wof-sqlite 8.0.0 → 8.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 (219) hide show
  1. package/address-point-interpolation.ts +9 -3
  2. package/address-point-schema.ts +32 -10
  3. package/address-point.ts +3 -0
  4. package/ancestry-backfill.ts +18 -5
  5. package/ancestry.ts +7 -2
  6. package/build-candidate.ts +29 -6
  7. package/build-slim.ts +40 -11
  8. package/candidate-fts.ts +1 -0
  9. package/candidate-lookup.ts +49 -15
  10. package/candidate-schema.ts +35 -11
  11. package/coincident-roles.ts +28 -6
  12. package/convention.ts +3 -1
  13. package/coverage-manifest-schema.ts +243 -0
  14. package/fst-autocomplete.ts +15 -9
  15. package/fst-builder.ts +65 -9
  16. package/fst-deserialize-web.ts +46 -9
  17. package/fst-matcher.ts +12 -5
  18. package/fst-serialize.ts +83 -9
  19. package/fst-types.ts +43 -0
  20. package/fts-query.ts +84 -0
  21. package/fts.ts +35 -9
  22. package/geo.ts +9 -3
  23. package/geonames-aliases.ts +116 -79
  24. package/geonames-postal.ts +25 -5
  25. package/index.ts +19 -0
  26. package/interpolation.ts +59 -55
  27. package/lookup.ts +103 -292
  28. package/name-score.ts +76 -0
  29. package/out/address-point-interpolation.d.ts.map +1 -1
  30. package/out/address-point-interpolation.js +4 -2
  31. package/out/address-point-interpolation.js.map +1 -1
  32. package/out/address-point-schema.d.ts +30 -10
  33. package/out/address-point-schema.d.ts.map +1 -1
  34. package/out/address-point-schema.js +6 -2
  35. package/out/address-point-schema.js.map +1 -1
  36. package/out/address-point.d.ts.map +1 -1
  37. package/out/address-point.js.map +1 -1
  38. package/out/ancestry-backfill.d.ts +6 -2
  39. package/out/ancestry-backfill.d.ts.map +1 -1
  40. package/out/ancestry-backfill.js +7 -3
  41. package/out/ancestry-backfill.js.map +1 -1
  42. package/out/ancestry.d.ts +6 -2
  43. package/out/ancestry.d.ts.map +1 -1
  44. package/out/ancestry.js +3 -1
  45. package/out/ancestry.js.map +1 -1
  46. package/out/build-candidate.d.ts +9 -3
  47. package/out/build-candidate.d.ts.map +1 -1
  48. package/out/build-candidate.js +5 -3
  49. package/out/build-candidate.js.map +1 -1
  50. package/out/build-slim.d.ts +15 -5
  51. package/out/build-slim.d.ts.map +1 -1
  52. package/out/build-slim.js +11 -5
  53. package/out/build-slim.js.map +1 -1
  54. package/out/candidate-fts.d.ts.map +1 -1
  55. package/out/candidate-fts.js.map +1 -1
  56. package/out/candidate-lookup.d.ts +16 -3
  57. package/out/candidate-lookup.d.ts.map +1 -1
  58. package/out/candidate-lookup.js +27 -11
  59. package/out/candidate-lookup.js.map +1 -1
  60. package/out/candidate-schema.d.ts +33 -11
  61. package/out/candidate-schema.d.ts.map +1 -1
  62. package/out/candidate-schema.js.map +1 -1
  63. package/out/coincident-roles.d.ts +16 -4
  64. package/out/coincident-roles.d.ts.map +1 -1
  65. package/out/coincident-roles.js +9 -3
  66. package/out/coincident-roles.js.map +1 -1
  67. package/out/convention.d.ts +3 -1
  68. package/out/convention.d.ts.map +1 -1
  69. package/out/convention.js.map +1 -1
  70. package/out/coverage-manifest-schema.d.ts +112 -0
  71. package/out/coverage-manifest-schema.d.ts.map +1 -0
  72. package/out/coverage-manifest-schema.js +154 -0
  73. package/out/coverage-manifest-schema.js.map +1 -0
  74. package/out/fst-autocomplete.d.ts +1 -1
  75. package/out/fst-autocomplete.d.ts.map +1 -1
  76. package/out/fst-autocomplete.js +11 -9
  77. package/out/fst-autocomplete.js.map +1 -1
  78. package/out/fst-builder.d.ts.map +1 -1
  79. package/out/fst-builder.js +42 -9
  80. package/out/fst-builder.js.map +1 -1
  81. package/out/fst-deserialize-web.d.ts.map +1 -1
  82. package/out/fst-deserialize-web.js +34 -9
  83. package/out/fst-deserialize-web.js.map +1 -1
  84. package/out/fst-matcher.d.ts +6 -2
  85. package/out/fst-matcher.d.ts.map +1 -1
  86. package/out/fst-matcher.js +9 -5
  87. package/out/fst-matcher.js.map +1 -1
  88. package/out/fst-serialize.d.ts.map +1 -1
  89. package/out/fst-serialize.js +62 -9
  90. package/out/fst-serialize.js.map +1 -1
  91. package/out/fst-types.d.ts +43 -0
  92. package/out/fst-types.d.ts.map +1 -1
  93. package/out/fts-query.d.ts +41 -0
  94. package/out/fts-query.d.ts.map +1 -0
  95. package/out/fts-query.js +75 -0
  96. package/out/fts-query.js.map +1 -0
  97. package/out/fts.d.ts +21 -7
  98. package/out/fts.d.ts.map +1 -1
  99. package/out/fts.js +10 -4
  100. package/out/fts.js.map +1 -1
  101. package/out/geo.d.ts +6 -2
  102. package/out/geo.d.ts.map +1 -1
  103. package/out/geo.js +3 -1
  104. package/out/geo.js.map +1 -1
  105. package/out/geonames-aliases.d.ts +12 -4
  106. package/out/geonames-aliases.d.ts.map +1 -1
  107. package/out/geonames-aliases.js +72 -67
  108. package/out/geonames-aliases.js.map +1 -1
  109. package/out/geonames-postal.d.ts +9 -3
  110. package/out/geonames-postal.d.ts.map +1 -1
  111. package/out/geonames-postal.js +7 -2
  112. package/out/geonames-postal.js.map +1 -1
  113. package/out/index.d.ts +2 -0
  114. package/out/index.d.ts.map +1 -1
  115. package/out/index.js +1 -0
  116. package/out/index.js.map +1 -1
  117. package/out/interpolation.d.ts +24 -6
  118. package/out/interpolation.d.ts.map +1 -1
  119. package/out/interpolation.js +32 -40
  120. package/out/interpolation.js.map +1 -1
  121. package/out/lookup.d.ts +3 -97
  122. package/out/lookup.d.ts.map +1 -1
  123. package/out/lookup.js +52 -184
  124. package/out/lookup.js.map +1 -1
  125. package/out/name-score.d.ts +28 -0
  126. package/out/name-score.d.ts.map +1 -0
  127. package/out/name-score.js +67 -0
  128. package/out/name-score.js.map +1 -0
  129. package/out/poi-lookup.d.ts +24 -8
  130. package/out/poi-lookup.d.ts.map +1 -1
  131. package/out/poi-lookup.js +27 -13
  132. package/out/poi-lookup.js.map +1 -1
  133. package/out/poi-schema.d.ts +42 -13
  134. package/out/poi-schema.d.ts.map +1 -1
  135. package/out/poi-schema.js +12 -3
  136. package/out/poi-schema.js.map +1 -1
  137. package/out/postal-city-alias-lookup.d.ts +18 -6
  138. package/out/postal-city-alias-lookup.d.ts.map +1 -1
  139. package/out/postal-city-alias-lookup.js.map +1 -1
  140. package/out/postal-city-alias-schema.d.ts +27 -9
  141. package/out/postal-city-alias-schema.d.ts.map +1 -1
  142. package/out/postal-city-alias-schema.js +3 -1
  143. package/out/postal-city-alias-schema.js.map +1 -1
  144. package/out/postal-city-candidate-schema.d.ts +15 -5
  145. package/out/postal-city-candidate-schema.d.ts.map +1 -1
  146. package/out/postal-city-candidate-schema.js +3 -1
  147. package/out/postal-city-candidate-schema.js.map +1 -1
  148. package/out/postcode-point-lookup.d.ts +6 -2
  149. package/out/postcode-point-lookup.d.ts.map +1 -1
  150. package/out/postcode-point-lookup.js +6 -2
  151. package/out/postcode-point-lookup.js.map +1 -1
  152. package/out/ranking-weights.d.ts +118 -0
  153. package/out/ranking-weights.d.ts.map +1 -0
  154. package/out/ranking-weights.js +44 -0
  155. package/out/ranking-weights.js.map +1 -0
  156. package/out/reverse.d.ts +9 -3
  157. package/out/reverse.d.ts.map +1 -1
  158. package/out/reverse.js +20 -6
  159. package/out/reverse.js.map +1 -1
  160. package/out/sharding.d.ts +3 -1
  161. package/out/sharding.d.ts.map +1 -1
  162. package/out/sharding.js +7 -5
  163. package/out/sharding.js.map +1 -1
  164. package/out/sqlite-convention-source.d.ts.map +1 -1
  165. package/out/sqlite-convention-source.js +3 -1
  166. package/out/sqlite-convention-source.js.map +1 -1
  167. package/out/street-centroid-schema.d.ts +33 -11
  168. package/out/street-centroid-schema.d.ts.map +1 -1
  169. package/out/street-centroid-schema.js +3 -1
  170. package/out/street-centroid-schema.js.map +1 -1
  171. package/out/street-centroid.d.ts.map +1 -1
  172. package/out/street-centroid.js +6 -2
  173. package/out/street-centroid.js.map +1 -1
  174. package/out/street-morphology-fst-builder.d.ts +6 -2
  175. package/out/street-morphology-fst-builder.d.ts.map +1 -1
  176. package/out/street-morphology-fst-builder.js +8 -7
  177. package/out/street-morphology-fst-builder.js.map +1 -1
  178. package/out/street-morphology-fst-loader.d.ts +67 -0
  179. package/out/street-morphology-fst-loader.d.ts.map +1 -0
  180. package/out/street-morphology-fst-loader.js +59 -0
  181. package/out/street-morphology-fst-loader.js.map +1 -0
  182. package/out/street-name-lookup.d.ts +9 -3
  183. package/out/street-name-lookup.d.ts.map +1 -1
  184. package/out/street-name-lookup.js +9 -7
  185. package/out/street-name-lookup.js.map +1 -1
  186. package/out/street-normalize.d.ts +3 -1
  187. package/out/street-normalize.d.ts.map +1 -1
  188. package/out/street-normalize.js +23 -13
  189. package/out/street-normalize.js.map +1 -1
  190. package/out/street-segment-schema.d.ts +68 -13
  191. package/out/street-segment-schema.d.ts.map +1 -1
  192. package/out/street-segment-schema.js +21 -2
  193. package/out/street-segment-schema.js.map +1 -1
  194. package/out/types.d.ts +18 -6
  195. package/out/types.d.ts.map +1 -1
  196. package/out/unified-schema.d.ts +1 -1
  197. package/out/unified-schema.d.ts.map +1 -1
  198. package/out/unified-schema.js +2 -2
  199. package/out/unified-schema.js.map +1 -1
  200. package/package.json +13 -5
  201. package/poi-lookup.ts +53 -21
  202. package/poi-schema.ts +43 -13
  203. package/postal-city-alias-lookup.ts +20 -6
  204. package/postal-city-alias-schema.ts +28 -9
  205. package/postal-city-candidate-schema.ts +15 -5
  206. package/postcode-point-lookup.ts +6 -2
  207. package/ranking-weights.ts +148 -0
  208. package/reverse.ts +47 -10
  209. package/sharding.ts +13 -6
  210. package/sqlite-convention-source.ts +4 -1
  211. package/street-centroid-schema.ts +35 -11
  212. package/street-centroid.ts +10 -3
  213. package/street-morphology-fst-builder.ts +25 -9
  214. package/street-morphology-fst-loader.ts +103 -0
  215. package/street-name-lookup.ts +19 -7
  216. package/street-normalize.ts +28 -13
  217. package/street-segment-schema.ts +83 -13
  218. package/types.ts +18 -6
  219. package/unified-schema.ts +11 -2
package/out/lookup.d.ts CHANGED
@@ -13,6 +13,7 @@ import { DatabaseSync } from "node:sqlite";
13
13
  import { type Ancestor, type CoincidentLocality } from "@mailwoman/resolver";
14
14
  import { type Convention, type ConventionSource } from "./convention.ts";
15
15
  import type { WOFPostalCityAliasLookup } from "./postal-city-alias-lookup.ts";
16
+ import { type RankingWeights } from "./ranking-weights.ts";
16
17
  import { type ShardConfig } from "./sharding.ts";
17
18
  import type { FindPlaceQuery, PlaceCandidate, PlaceLookup } from "./types.ts";
18
19
  export interface WOFSqlitePlaceLookupOpts {
@@ -61,103 +62,6 @@ export interface WOFSqlitePlaceLookupOpts {
61
62
  */
62
63
  postalCityAliases?: WOFPostalCityAliasLookup;
63
64
  }
64
- /**
65
- * Ranking weights for `findPlace`. Tweakable per-instance but defaults match the values declared in the Phase 4.2 plan
66
- * doc.
67
- */
68
- export interface RankingWeights {
69
- /** Boost when the candidate's placetype matches an explicit `placetype` filter. */
70
- placetypeMatchBoost: number;
71
- /** Boost when the candidate is a locality and no explicit placetype was requested. */
72
- localityImplicitBoost: number;
73
- /** Boost when the candidate's country matches an explicit `country` filter. */
74
- countryMatchBoost: number;
75
- /** Boost when the candidate is a direct child of the requested `parentID`. */
76
- directChildBoost: number;
77
- /** Boost when the candidate is a transitive descendant of the requested `parentID`. */
78
- descendantBoost: number;
79
- /** Multiplier on the length-penalty term (penalizes much-longer-than-query names). */
80
- lengthPenaltyWeight: number;
81
- /**
82
- * Magnitude of the proximity boost when the query carries `near`. The contribution is `proximityBoost / (1 +
83
- * distanceKm / proximityScaleKm)` — at distance 0 the boost is full magnitude, at `proximityScaleKm` it's half,
84
- * decaying further with distance. Default tuned so proximity can overcome a typical FTS rank tie but not dominate a
85
- * strong text match.
86
- */
87
- proximityBoost: number;
88
- /**
89
- * Magnitude of the bias-hint term inside the exact-tier PROMINENCE sort (the `bias`/viewport path). Deliberately
90
- * population-scale (default = populationBoost) so a candidate near the map view / the user beats a distant-but-bigger
91
- * namesake — "the map view wins" is the feature; same-region ties (all candidates far from every hint) still fall to
92
- * population.
93
- */
94
- biasBoost: number;
95
- /** Distance (km) at which the proximity boost halves. Tune to the typical query radius. */
96
- proximityScaleKm: number;
97
- /**
98
- * Magnitude of the population boost when the candidate has a known `wof:population`. The contribution is
99
- * `populationBoost * log10(1 + population) / populationScaleLog10`, capped at `populationBoost`. WOF only carries
100
- * population for ~15% of localities (mostly larger ones); places without it get +0 (never a penalty). Default tuned
101
- * so the famous Springfield, IL (pop ~112k) gets ~0.42 boost — enough to nudge past tiny same-name peers.
102
- */
103
- populationBoost: number;
104
- /**
105
- * Population (in log10) at which the boost reaches its full magnitude. Default 6 — i.e. a population of 1,000,000
106
- * gives `populationBoost` exactly. Larger populations cap at the same value (no compounding effect for megacities).
107
- */
108
- populationScaleLog10: number;
109
- /**
110
- * Tier candidates with an EXACT name/alias match above candidates that only match partially, BEFORE the weighted-sum
111
- * score is consulted. Default true.
112
- *
113
- * Why this is needed (and why it ALIGNS with — rather than overrides — the population/importance signal): the
114
- * weighted sum adds population as a large additive boost (`populationBoost`, up to +4) so that famous places surface
115
- * for unambiguous full-name queries. But population is a _prominence prior_ — its job is to break ties among
116
- * candidates that match the query EQUALLY WELL (e.g. "Springfield" → Springfield IL over Springfield MA, both exact
117
- * name matches). It was never meant to promote a place that matches the query WORSE. For a 2-letter region
118
- * abbreviation that backfires: querying "ME" returns Maine (which has the exact alias `ME`) AND Missouri/
119
- * Michigan/etc. (which do not), and Missouri's larger population (+4) overcomes Maine's bm25 edge — so "Portland, ME"
120
- * resolves its region to Missouri and the locality then cascades to the wrong state. Tiering restores the intended
121
- * ordering: **match quality is the primary key, prominence (population) the secondary key WITHIN a tier.**
122
- * Springfield-IL-over-MA still works (both exact → same tier → population decides); ME→Maine now works (only Maine is
123
- * exact → higher tier → population never gets to override it). See
124
- * docs/articles/evals/resolver-geo/2026-05-30-resolver-exact-match.md.
125
- *
126
- * Note: tiering re-ranks within the over-fetched candidate window (`limit * 4`); a pathological exact match that
127
- * falls outside that window is not rescued. For the region-abbrev case the window is comfortably sufficient (a
128
- * handful of states match a 2-letter query).
129
- */
130
- exactMatchTiering: boolean;
131
- /**
132
- * #936 option 3 — official-language names ARE names. When true, a candidate holding the query as an OFFICIAL name
133
- * (`names.official = 1`: a preferred-form name in an official language of its country, stamped at ingest) joins the
134
- * NAME-exact sub-tier rather than the alias-exact one, provided its population clears {@link officialNameExactFloor}.
135
- * Fixes unscoped "Åbo" → Turku (its official Swedish name) over a hamlet literally named Åbo; population still orders
136
- * within the sub-tier, so Paris → Paris FR is untouched.
137
- *
138
- * Default true (operator-promoted 2026-07-03 after the pre-registered gate battery: four intended exonym flips —
139
- * Berne→Bern, Bruges→Brugge, Roma→Rome, Åbo→Turku — with the namesake/abbreviation rows and the US/FI panels
140
- * byte-identical). Requires a gazetteer carrying the #940 ingest bit — on older DBs without the `official` column the
141
- * probe fails soft and behavior is identical to the flag being off.
142
- */
143
- officialNameExact: boolean;
144
- /**
145
- * Minimum population for a candidate's official names to join the name-exact sub-tier. The #936 review's no-floor
146
- * census measured the boundary: ≥100k holders are the famous-exonym class (757 flips, intent-correct; 7 collisions,
147
- * none harmful) while 10k–100k holders are junk-dominated (3,481 flips led by short-form mis-tags — Villeneuve-Loubet
148
- * carrying "villeneuve" would bury five real villages of that name). Rank-time knob: tunable without re-ingest;
149
- * below-floor official names simply stay alias-tier (today's behavior).
150
- */
151
- officialNameExactFloor: number;
152
- }
153
- /** Padded character-trigram set (a leading/trailing space pads short tokens). */
154
- export declare function trigrams(s: string): Set<string>;
155
- /**
156
- * Character-trigram Jaccard ∈ [0,1] — tolerant of the swallowed-leading-char fragments ("auen" vs "plauen") and minor
157
- * misspellings without a heavyweight edit-distance pass. Shared with the candidate backend's FTS5-trigram fuzzy
158
- * fallback so both lookups rank typos identically.
159
- */
160
- export declare function trigramJaccard(a: string, b: string): number;
161
65
  export declare class WOFSqlitePlaceLookup implements PlaceLookup, Disposable {
162
66
  #private;
163
67
  constructor(opts: WOFSqlitePlaceLookupOpts, weights?: Partial<RankingWeights>);
@@ -182,4 +86,6 @@ export declare class WOFSqlitePlaceLookup implements PlaceLookup, Disposable {
182
86
  close(): void;
183
87
  [Symbol.dispose](): void;
184
88
  }
89
+ export { trigramJaccard, trigrams } from "./name-score.ts";
90
+ export type { RankingWeights } from "./ranking-weights.ts";
185
91
  //# sourceMappingURL=lookup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAA;AAG9D,OAAO,EAAyB,KAAK,QAAQ,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAKnG,OAAO,EAIN,KAAK,UAAU,EACf,KAAK,gBAAgB,EAGrB,MAAM,iBAAiB,CAAA;AAWxB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAE7E,OAAO,EAKN,KAAK,WAAW,EAChB,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAgB,MAAM,YAAY,CAAA;AAE3F,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,WAAW,CAAC,CAAA;IAC3D;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC3D;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;CAC5C;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,mFAAmF;IACnF,mBAAmB,EAAE,MAAM,CAAA;IAC3B,sFAAsF;IACtF,qBAAqB,EAAE,MAAM,CAAA;IAC7B,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,CAAA;IACzB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAA;IACxB,uFAAuF;IACvF,eAAe,EAAE,MAAM,CAAA;IACvB,sFAAsF;IACtF,mBAAmB,EAAE,MAAM,CAAA;IAC3B;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,2FAA2F;IAC3F,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,iBAAiB,EAAE,OAAO,CAAA;IAC1B;;;;;;;;;;;OAWG;IACH,iBAAiB,EAAE,OAAO,CAAA;IAC1B;;;;;;OAMG;IACH,sBAAsB,EAAE,MAAM,CAAA;CAC9B;AAkGD,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAS/C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa3D;AAqBD,qBAAa,oBAAqB,YAAW,WAAW,EAAE,UAAU;;gBAuDvD,IAAI,EAAE,wBAAwB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IAwHvE,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAyDjE;;;;;OAKG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IAyDvE;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAAE;IA8vB1C,KAAK,IAAI,IAAI;IAUb,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAgBxB"}
1
+ {"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAA;AAG9D,OAAO,EAAyB,KAAK,QAAQ,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAKnG,OAAO,EAIN,KAAK,UAAU,EACf,KAAK,gBAAgB,EAGrB,MAAM,iBAAiB,CAAA;AAaxB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC7E,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAE3E,OAAO,EAKN,KAAK,WAAW,EAChB,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAgB,MAAM,YAAY,CAAA;AAS3F,MAAM,WAAW,wBAAwB;IACxC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,WAAW,CAAC,CAAA;IAC3D;;;OAGG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC3D;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;CAC5C;AAyDD,qBAAa,oBAAqB,YAAW,WAAW,EAAE,UAAU;;gBA6DvD,IAAI,EAAE,wBAAwB,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC;IA8HvE,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA6DjE;;;;;OAKG;IACH,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE;IA2DvE;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAAE;IA6xB1C,KAAK,IAAI,IAAI;IAUb,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAkBxB;AAED,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE1D,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
package/out/lookup.js CHANGED
@@ -16,40 +16,19 @@ import { Kysely } from "kysely";
16
16
  import { ancestorLineage } from "./ancestry.js";
17
17
  import { COINCIDENT_ROLES_TABLE, coincidentRolesExists } from "./coincident-roles.js";
18
18
  import { ADDRESS_CONVENTION_TABLE, resolveConvention, SeedConventionSource, } from "./convention.js";
19
+ import { normalizePlacetypes, sanitizeFTSQuery } from "./fts-query.js";
19
20
  import { aliasBagExactMatch, buildPlaceSearchFTS, PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE, placeBboxExists, placePopulationExists, placeSearchFTSExists, } from "./fts.js";
20
21
  import { bboxAround, haversineKm } from "./geo.js";
22
+ import { cfNormalize, softNameScore, trigramJaccard } from "./name-score.js";
23
+ import { DEFAULT_WEIGHTS } from "./ranking-weights.js";
21
24
  import { pickShardForPlacetype, pickShardsForPlacetype, resolveShards, } from "./sharding.js";
22
25
  import { SqliteConventionSource } from "./sqlite-convention-source.js";
23
- const DEFAULT_WEIGHTS = {
24
- placetypeMatchBoost: 0.5,
25
- localityImplicitBoost: 0.2,
26
- countryMatchBoost: 0.3,
27
- directChildBoost: 0.5,
28
- descendantBoost: 0.2,
29
- lengthPenaltyWeight: 0.1,
30
- proximityBoost: 0.8,
31
- proximityScaleKm: 100,
32
- biasBoost: 4.0,
33
- // populationBoost is intentionally large — empirical tuning against real WOF showed BM25 gaps
34
- // of 1.5-3.0 between famous places and tiny same-name peers (because the famous ones have
35
- // hundreds of alt-name entries that hurt their FTS document score). To consistently surface
36
- // "the famous one" for unambiguous queries like "New York" or "Chicago", the population signal
37
- // needs to dominate. Callers wanting a more conservative balance can drop this in the
38
- // RankingWeights override.
39
- //
40
- // Note: this resolver uses `place_population` directly. The separate `place_importance` table
41
- // (Wikipedia-derived) is consumed by the FST layer, not here. See
42
- // docs/articles/concepts/importance-vs-population.md for the two-signal contract.
43
- populationBoost: 4.0,
44
- populationScaleLog10: 6,
45
- // Exact name/alias match outranks partial match before the weighted sum (incl. population) is
46
- // consulted — keeps population as an intra-tier prominence tiebreaker, not a cross-tier promoter.
47
- // Fixes the 2-letter-region-abbrev bug ("ME" → Maine, not the more-populous Missouri).
48
- exactMatchTiering: true,
49
- // #936 option 3 — promoted default-ON 2026-07-03 (gate battery PASS; see the RankingWeights docstring).
50
- officialNameExact: true,
51
- officialNameExactFloor: 100_000,
52
- };
26
+ /**
27
+ * Query length at or below which the FTS window is widened. A two- or three-character query is almost always a region
28
+ * abbreviation, where the exact match can otherwise fall outside the window behind higher-bm25 partial hits — "NY"
29
+ * losing to "New York".
30
+ */
31
+ const SHORT_QUERY_MAX_LENGTH = 3;
53
32
  /**
54
33
  * Over-fetch floor for SHORT (≤3-char) queries — region abbreviations like "NY"/"VT". An exact-abbrev holder's BM25 is
55
34
  * poor (long multilingual alt-name document), so the normal `limit * 4` window can drop it before `exactMatchTiering`
@@ -85,57 +64,6 @@ const CF_PC_DECAY_KM = 8;
85
64
  */
86
65
  const CF_MISMATCH_KM = 50;
87
66
  const CF_MISMATCH_DELTA = 0.5;
88
- /** Case-fold + strip diacritics + collapse punctuation — for the coord-first soft name match. */
89
- function cfNormalize(s) {
90
- return s
91
- .toLowerCase()
92
- .normalize("NFD")
93
- .replace(/[\u0300-\u036f]/g, "") // combining diacritical marks
94
- .replace(/[^a-z0-9]+/g, " ")
95
- .trim();
96
- }
97
- /** Padded character-trigram set (a leading/trailing space pads short tokens). */
98
- export function trigrams(s) {
99
- const t = ` ${s} `;
100
- const out = new Set();
101
- for (let i = 0; i + 3 <= t.length; i++) {
102
- out.add(t.slice(i, i + 3));
103
- }
104
- return out;
105
- }
106
- /**
107
- * Character-trigram Jaccard ∈ [0,1] — tolerant of the swallowed-leading-char fragments ("auen" vs "plauen") and minor
108
- * misspellings without a heavyweight edit-distance pass. Shared with the candidate backend's FTS5-trigram fuzzy
109
- * fallback so both lookups rank typos identically.
110
- */
111
- export function trigramJaccard(a, b) {
112
- const A = trigrams(a);
113
- const B = trigrams(b);
114
- if (A.size === 0 || B.size === 0)
115
- return 0;
116
- let inter = 0;
117
- for (const x of A)
118
- if (B.has(x)) {
119
- inter++;
120
- }
121
- return inter / (A.size + B.size - inter);
122
- }
123
- /** Soft name-match score ∈ [0,1]: exact (normalized) name/alias → 1, else best trigram-Jaccard. */
124
- function softNameScore(text, name, aliases) {
125
- const q = cfNormalize(text);
126
- if (!q)
127
- return 0;
128
- let best = 0;
129
- for (const raw of [name, ...aliases]) {
130
- const n = cfNormalize(raw);
131
- if (!n)
132
- continue;
133
- if (n === q)
134
- return 1;
135
- best = Math.max(best, trigramJaccard(q, n));
136
- }
137
- return best;
138
- }
139
67
  export class WOFSqlitePlaceLookup {
140
68
  #db;
141
69
  #ownsDB;
@@ -165,7 +93,9 @@ export class WOFSqlitePlaceLookup {
165
93
  * override) schema names.
166
94
  */
167
95
  #shards;
168
- /** #920: per-schema probed country sets for country-aware shard routing (non-main shards only). */
96
+ /**
97
+ * #920: per-schema probed country sets for country-aware shard routing (non-main shards only).
98
+ */
169
99
  #shardCountries;
170
100
  /**
171
101
  * The Geographic Rule Engine (Direction E, #289). `#conventionSource` supplies per-WOF-polygon resolution profiles;
@@ -177,13 +107,17 @@ export class WOFSqlitePlaceLookup {
177
107
  #conventionSource;
178
108
  #strategies;
179
109
  #countryWOFIdCache = new Map();
180
- /** Strategy names already warned about — so an unknown name surfaces once, not once per query. */
110
+ /**
111
+ * Strategy names already warned about — so an unknown name surfaces once, not once per query.
112
+ */
181
113
  #warnedUnknownStrategies = new Set();
182
114
  /**
183
115
  * Lazily-built `admin_id → coincident localities` map from the #403 relation (null until first use).
184
116
  */
185
117
  #coincidentRolesCache = null;
186
- /** Per-id memoized ancestor lineages (#404) — a hot chain is queried once. */
118
+ /**
119
+ * Per-id memoized ancestor lineages (#404) — a hot chain is queried once.
120
+ */
187
121
  #ancestorsCache = new Map();
188
122
  /**
189
123
  * Opt-in postal-city alias reader (#475). `null` unless `opts.postalCityAliases` was supplied — every alias code path
@@ -216,7 +150,7 @@ export class WOFSqlitePlaceLookup {
216
150
  // ATTACH each non-main shard. Schema names were validated by resolveShards, so safe to
217
151
  // interpolate directly (SQLite ATTACH doesn't accept parameters for the schema name).
218
152
  for (const s of shards.slice(1)) {
219
- this.#db.exec(`ATTACH DATABASE '${s.path.replace(/'/g, "''")}' AS ${s.schemaName}`);
153
+ this.#db.exec(`ATTACH DATABASE '${s.path.replaceAll("'", "''")}' AS ${s.schemaName}`);
220
154
  }
221
155
  }
222
156
  // node:sqlite has no .pragma() helper; pragmas are executed as plain SQL.
@@ -319,7 +253,7 @@ export class WOFSqlitePlaceLookup {
319
253
  break;
320
254
  }
321
255
  }
322
- if (outcome.length > 0)
256
+ if (outcome.length)
323
257
  return outcome;
324
258
  // #924: NL postcode retry ladder. The WOF NL postalcode repo stores full codes UNSPACED
325
259
  // ('1012LG') plus 4-digit stems ('1012'), while Dutch addresses carry the spaced form
@@ -333,10 +267,10 @@ export class WOFSqlitePlaceLookup {
333
267
  (normalizePlacetypes(query.placetype)?.includes("postalcode") ?? false) &&
334
268
  /^\d{4}\s?[A-Za-z]{2}$/.test(query.text.trim())) {
335
269
  const trimmed = query.text.trim();
336
- const joined = trimmed.replace(/\s+/g, "");
270
+ const joined = trimmed.replaceAll(/\s+/g, "");
337
271
  if (joined !== trimmed) {
338
272
  const full = await this.findPlace({ ...query, text: joined });
339
- if (full.length > 0)
273
+ if (full.length)
340
274
  return full;
341
275
  }
342
276
  const stem = trimmed.slice(0, 4);
@@ -456,7 +390,7 @@ export class WOFSqlitePlaceLookup {
456
390
  // (Cross-country abbrev collisions — "VT" is BOTH Vermont and Viterbo — still need a country/
457
391
  // postcode signal to disambiguate; this only rescues the window-drop class, not genuine ambiguity.
458
392
  // With a `country` hint every abbrev resolves; bare + no-context lifts 7→10/15 US states.)
459
- const ftsLimit = query.text.trim().length <= 3 ? Math.max(limit * 4, SHORT_QUERY_OVERFETCH) : limit * 4;
393
+ const ftsLimit = query.text.trim().length <= SHORT_QUERY_MAX_LENGTH ? Math.max(limit * 4, SHORT_QUERY_OVERFETCH) : limit * 4;
460
394
  // Expand the placetype filter through the shared equivalence table (core/resolver): a
461
395
  // `locality` query must also reach `borough` / `localadmin` rows — Brooklyn-the-borough
462
396
  // (pop 2.5M) is a borough, not a locality, and a strict filter made it unreachable so the
@@ -511,7 +445,7 @@ export class WOFSqlitePlaceLookup {
511
445
  // the FROM table — required by FTS5 parser, see sharding.ts header comment.
512
446
  const where = ["place_search MATCH ?", "spr.is_current != 0", "spr.is_deprecated = 0"];
513
447
  const params = [ftsQuery];
514
- if (placetypes && placetypes.length > 0) {
448
+ if (placetypes && placetypes.length) {
515
449
  where.push(`spr.placetype IN (${placetypes.map(() => "?").join(", ")})`);
516
450
  params.push(...placetypes);
517
451
  }
@@ -532,9 +466,7 @@ export class WOFSqlitePlaceLookup {
532
466
  if (useBboxJoin) {
533
467
  joinClause += ` JOIN ${sch}.${PLACE_BBOX_TABLE} bbox ON bbox.id = spr.id`;
534
468
  // AABB intersection — both bbox sides must overlap. R*Tree handles this in O(log n).
535
- const filterBox = query.bbox
536
- ? query.bbox
537
- : bboxAround(query.near.lat, query.near.lon, query.near.maxDistanceKm);
469
+ const filterBox = query.bbox || bboxAround(query.near.lat, query.near.lon, query.near.maxDistanceKm);
538
470
  where.push("bbox.min_lat <= ? AND bbox.max_lat >= ?", "bbox.min_lon <= ? AND bbox.max_lon >= ?");
539
471
  params.push(filterBox.maxLat, filterBox.minLat, filterBox.maxLon, filterBox.minLon);
540
472
  }
@@ -616,7 +548,7 @@ export class WOFSqlitePlaceLookup {
616
548
  ORDER BY COALESCE(${PLACE_POPULATION_TABLE}.population, 0) DESC
617
549
  LIMIT ?
618
550
  `);
619
- const popParams = params.slice(0, params.length - 3); // drop the two boost params + ftsLimit
551
+ const popParams = params.slice(0, -3); // drop the two boost params + ftsLimit
620
552
  const seen = new Set(rawRows.map((r) => r.id));
621
553
  for (const row of popStmt.all(...popParams, POPULATION_FETCH_LIMIT)) {
622
554
  if (!seen.has(row.id)) {
@@ -629,7 +561,7 @@ export class WOFSqlitePlaceLookup {
629
561
  // SQLite's bm25() returns a lower-is-better score (negative for matches). Negate so we
630
562
  // start from a higher-is-better baseline.
631
563
  let score = -row.rank;
632
- if (placetypes && placetypes.length > 0 && placetypes.includes(row.placetype)) {
564
+ if (placetypes && placetypes.length && placetypes.includes(row.placetype)) {
633
565
  score += this.#weights.placetypeMatchBoost;
634
566
  }
635
567
  if (!placetypes && row.placetype === "locality") {
@@ -639,12 +571,7 @@ export class WOFSqlitePlaceLookup {
639
571
  score += this.#weights.countryMatchBoost;
640
572
  }
641
573
  if (query.parentID !== undefined) {
642
- if (row.parent_id === query.parentID) {
643
- score += this.#weights.directChildBoost;
644
- }
645
- else {
646
- score += this.#weights.descendantBoost;
647
- }
574
+ score += row.parent_id === query.parentID ? this.#weights.directChildBoost : this.#weights.descendantBoost;
648
575
  }
649
576
  const extraLen = Math.max(0, row.name.length - queryLen - 3);
650
577
  score -= (this.#weights.lengthPenaltyWeight * extraLen) / 10;
@@ -731,7 +658,7 @@ export class WOFSqlitePlaceLookup {
731
658
  // Runs even for a SINGLE candidate so `exactMatch` is stamped consistently (parity with the
732
659
  // WASM lookup) — a sole alias hit ("New York City" → New York) must still carry the flag the
733
660
  // demo cascade / #369 re-rank read.
734
- if (this.#weights.exactMatchTiering && candidates.length > 0) {
661
+ if (this.#weights.exactMatchTiering && candidates.length) {
735
662
  const exactIds = this.#exactMatchIds(sch, candidates.map((c) => c.id), query.text);
736
663
  // Stamp the tier onto every candidate (not just when the tiering sort fires) so a downstream
737
664
  // re-rank — #369's postcode-anchor country pin in `resolveTree` — can keep the country pin from
@@ -739,7 +666,7 @@ export class WOFSqlitePlaceLookup {
739
666
  for (const c of candidates) {
740
667
  c.exactMatch = exactIds.has(c.id);
741
668
  }
742
- if (exactIds.size > 0) {
669
+ if (exactIds.size) {
743
670
  // #905: WITHIN the exact tier, population is the PRIMARY key and the weighted score
744
671
  // only breaks population ties. Exactness saturates text relevance, and the bm25
745
672
  // residue inside `score` is length-noise (see the fetch-site comment), so letting it
@@ -753,7 +680,7 @@ export class WOFSqlitePlaceLookup {
753
680
  // widen recall, not to tie primaries. ME→Maine is untouched: 'ME' name-exact-matches
754
681
  // nothing, so the alias sub-tier still decides there. Population orders within each
755
682
  // sub-tier as before.
756
- const norm = (v) => v.toLowerCase().trim().replace(/\s+/g, " ");
683
+ const norm = (v) => v.toLowerCase().trim().replaceAll(/\s+/g, " ");
757
684
  const needle = norm(query.text);
758
685
  // #936 option 3: an OFFICIAL name (preferred form in an official language of the place's
759
686
  // country, `names.official = 1`) counts as the place's own name for the sub-tier — "Åbo" is
@@ -789,11 +716,11 @@ export class WOFSqlitePlaceLookup {
789
716
  }
790
717
  return b.score - a.score;
791
718
  });
792
- return Promise.resolve(candidates.slice(0, limit));
719
+ return candidates.slice(0, limit);
793
720
  }
794
721
  }
795
722
  candidates.sort((a, b) => b.score - a.score);
796
- return Promise.resolve(candidates.slice(0, limit));
723
+ return candidates.slice(0, limit);
797
724
  }
798
725
  #isLocalityQuery(query) {
799
726
  const pts = normalizePlacetypes(query.placetype);
@@ -824,7 +751,7 @@ export class WOFSqlitePlaceLookup {
824
751
  const cached = this.#countryWOFIdCache.get(code);
825
752
  if (cached !== undefined)
826
753
  return cached;
827
- let id = null;
754
+ let id;
828
755
  try {
829
756
  const row = this.#db
830
757
  .prepare(`SELECT id FROM main.spr WHERE placetype = 'country' AND country = ? AND is_current != 0 LIMIT 1`)
@@ -854,7 +781,7 @@ export class WOFSqlitePlaceLookup {
854
781
  .prepare(`SELECT locality_id AS id, aliases, distance_km AS dist, is_containing AS containing
855
782
  FROM ${sch}.${POSTCODE_LOCALITY_TABLE} WHERE ${pcWhere}`)
856
783
  .all(...pcParams);
857
- if (pcRows.length === 0)
784
+ if (!pcRows.length)
858
785
  return null;
859
786
  const limit = query.limit ?? 10;
860
787
  // Name-match candidates via the normal FTS path (postcode cleared → no recursion).
@@ -898,7 +825,7 @@ export class WOFSqlitePlaceLookup {
898
825
  // (#475). `postalAliasByGeo` is empty unless the opt-in reader was supplied, so when off this
899
826
  // reduces to the original `info?.aliases ?? []` and the score is unchanged.
900
827
  const wofAliases = info?.aliases ?? [];
901
- const aliases = postalAliasByGeo.size > 0
828
+ const aliases = postalAliasByGeo.size
902
829
  ? [...wofAliases, ...(postalAliasByGeo.get(cfNormalize(cand.name)) ?? [])]
903
830
  : wofAliases;
904
831
  const sName = softNameScore(query.text, cand.name, aliases);
@@ -922,12 +849,13 @@ export class WOFSqlitePlaceLookup {
922
849
  // every locality polygon still anchor to the closest town.
923
850
  const anchorRow = pcRows
924
851
  .filter((r) => merged.has(r.id))
852
+ // oxlint-disable-next-line unicorn/no-array-sort -- sorts a freshly-built array; toSorted would double-allocate on a hot path
925
853
  .sort((a, b) => b.containing - a.containing || a.dist - b.dist)[0];
926
854
  const anchor = anchorRow ? merged.get(anchorRow.id) : undefined;
927
- if (anchor && top.id !== anchorRow.id) {
928
- if (haversineKm(top.lat, top.lon, anchor.lat, anchor.lon) > CF_MISMATCH_KM) {
929
- top.mismatch = true;
930
- }
855
+ if (anchor &&
856
+ top.id !== anchorRow.id &&
857
+ haversineKm(top.lat, top.lon, anchor.lat, anchor.lon) > CF_MISMATCH_KM) {
858
+ top.mismatch = true;
931
859
  }
932
860
  }
933
861
  return scored.slice(0, limit).map(({ exact, ...c }) => {
@@ -935,9 +863,11 @@ export class WOFSqlitePlaceLookup {
935
863
  return c;
936
864
  });
937
865
  }
938
- /** Fetch locality spr rows (from main) for the postcode-injected candidate ids the FTS set missed. */
866
+ /**
867
+ * Fetch locality spr rows (from main) for the postcode-injected candidate ids the FTS set missed.
868
+ */
939
869
  #fetchLocalitiesByID(ids) {
940
- if (ids.length === 0)
870
+ if (!ids.length)
941
871
  return [];
942
872
  const hasPop = this.#hasPopulationIndex.get("main") === true;
943
873
  const popSelect = hasPop ? `pp.population AS population` : `NULL AS population`;
@@ -976,7 +906,7 @@ export class WOFSqlitePlaceLookup {
976
906
  #exactMatchIds(schemaName, ids, text) {
977
907
  const out = new Set();
978
908
  const trimmed = text.trim();
979
- if (ids.length === 0 || !trimmed)
909
+ if (!ids.length || !trimmed)
980
910
  return out;
981
911
  const placeholders = ids.map(() => "?").join(", ");
982
912
  try {
@@ -995,7 +925,7 @@ export class WOFSqlitePlaceLookup {
995
925
  const rows = this.#db
996
926
  .prepare(`SELECT wof_id AS id, name, alt_names FROM ${schemaName}.place_search WHERE wof_id IN (${placeholders})`)
997
927
  .all(...ids);
998
- const norm = (s) => s.toLowerCase().trim().replace(/\s+/g, " ");
928
+ const norm = (s) => s.toLowerCase().trim().replaceAll(/\s+/g, " ");
999
929
  const needle = norm(trimmed);
1000
930
  for (const r of rows) {
1001
931
  if (r.name !== null && norm(r.name) === needle) {
@@ -1029,7 +959,7 @@ export class WOFSqlitePlaceLookup {
1029
959
  #officialNameIds(schemaName, ids, text) {
1030
960
  const out = new Set();
1031
961
  const trimmed = text.trim();
1032
- if (ids.length === 0 || !trimmed)
962
+ if (!ids.length || !trimmed)
1033
963
  return out;
1034
964
  const placeholders = ids.map(() => "?").join(", ");
1035
965
  try {
@@ -1056,7 +986,9 @@ export class WOFSqlitePlaceLookup {
1056
986
  [Symbol.dispose]() {
1057
987
  this.close();
1058
988
  }
1059
- /** Build the FTS5 virtual table from the `names` + `places` tables. */
989
+ /**
990
+ * Build the FTS5 virtual table from the `names` + `places` tables.
991
+ */
1060
992
  #ensureFTS() {
1061
993
  buildPlaceSearchFTS(this.#db);
1062
994
  }
@@ -1066,69 +998,5 @@ export class WOFSqlitePlaceLookup {
1066
998
  }
1067
999
  }
1068
1000
  }
1069
- function normalizePlacetypes(p) {
1070
- if (!p)
1071
- return null;
1072
- return Array.isArray(p) ? p : [p];
1073
- }
1074
- /**
1075
- * Make an arbitrary user-typed string safe for FTS5 MATCH.
1076
- *
1077
- * FTS5 has its own query syntax (`"phrase"`, `term1 OR term2`, `prefix*`, NEAR/N, etc.). Letting raw user input through
1078
- * means a user typing `Paris's` or `St. (Petersburg)` causes a syntax error.
1079
- *
1080
- * Per-token rules:
1081
- *
1082
- * - Strip all punctuation except trailing `*` from each whitespace-separated token.
1083
- * - **Trailing `*`** is preserved as FTS5 **prefix syntax** — `627*` becomes the literal `627*` (unquoted). The caller
1084
- * signaled they want a prefix; respect that.
1085
- * - All other tokens are wrapped in `"..."` as a single-word phrase. Conservative — handles apostrophes, parens, accented
1086
- * input, etc. safely.
1087
- * - Multiple tokens join with implicit AND.
1088
- *
1089
- * Examples:
1090
- *
1091
- * - `"Paris"` → `"Paris"` (phrase)
1092
- * - `"627*"` → `627*` (prefix)
1093
- * - `"St. (Petersburg)"` → `"St" "Petersburg"` (two phrases, AND-joined)
1094
- * - `"Thiron-Gardais"` → `"Thiron" "Gardais"` (intra-token punctuation SPLITS — #945; fusing to `ThironGardais` matched
1095
- * nothing because the FTS doc tokenizes the hyphenated name as two terms)
1096
- * - `"110 00"` with `fuseTokens` (postcode-typed) → `"110" "00"` per-token fused — the #920 name law
1097
- * - `"Pari* TX"` → `Pari* "TX"` (mixed prefix + phrase)
1098
- * - `"*"` alone → `""` (no body → drop)
1099
- */
1100
- function sanitizeFTSQuery(text, opts) {
1101
- const out = [];
1102
- for (const rawToken of text.normalize("NFKC").split(/\s+/u)) {
1103
- const trimmed = rawToken.trim();
1104
- if (!trimmed)
1105
- continue;
1106
- const hasPrefixStar = trimmed.endsWith("*");
1107
- // #920 name law (postcode-typed queries ONLY): delete intra-token punctuation and FUSE the
1108
- // remainder — postal names are stored in this collapsed shape ("SW1A" stays one term).
1109
- if (opts?.fuseTokens) {
1110
- const body = trimmed.replace(/[^\p{L}\p{N}]/gu, "");
1111
- if (!body)
1112
- continue;
1113
- out.push(hasPrefixStar ? `${body}*` : `"${body.replace(/"/g, '""')}"`);
1114
- continue;
1115
- }
1116
- // Everything else SPLITS on intra-token punctuation — the behavior the docstring always
1117
- // promised ("St. (Petersburg)" → two phrases). The old code DELETED punctuation instead,
1118
- // fusing "Thiron-Gardais" into the unmatchable single term `ThironGardais` while the FTS
1119
- // doc holds two terms (#945 — the entire hyphenated-name class missed at the raw lookup;
1120
- // masked for years because pre-splice tokenizers never emitted hyphen-preserved values).
1121
- const parts = trimmed.split(/[^\p{L}\p{N}]+/u).filter(Boolean);
1122
- if (parts.length === 0)
1123
- continue;
1124
- for (let i = 0; i < parts.length; i++) {
1125
- const body = parts[i].replace(/\*/g, "");
1126
- if (!body)
1127
- continue;
1128
- // The caller's trailing `*` applies to the FINAL part ("Thiron-Gard*" → "Thiron" Gard*).
1129
- out.push(hasPrefixStar && i === parts.length - 1 ? `${body}*` : `"${body.replace(/"/g, '""')}"`);
1130
- }
1131
- }
1132
- return out.join(" ");
1133
- }
1001
+ export { trigramJaccard, trigrams } from "./name-score.js";
1134
1002
  //# sourceMappingURL=lookup.js.map