@mailwoman/resolver-wof-sqlite 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +250 -0
  2. package/out/address-point-interpolation.d.ts +48 -0
  3. package/out/address-point-interpolation.d.ts.map +1 -0
  4. package/out/address-point-interpolation.js +164 -0
  5. package/out/address-point-interpolation.js.map +1 -0
  6. package/out/address-point-schema.d.ts +58 -0
  7. package/out/address-point-schema.d.ts.map +1 -0
  8. package/out/address-point-schema.js +67 -0
  9. package/out/address-point-schema.js.map +1 -0
  10. package/out/address-point.d.ts +29 -0
  11. package/out/address-point.d.ts.map +1 -0
  12. package/out/address-point.js +62 -0
  13. package/out/address-point.js.map +1 -0
  14. package/out/ancestry.d.ts +40 -0
  15. package/out/ancestry.d.ts.map +1 -0
  16. package/out/ancestry.js +53 -0
  17. package/out/ancestry.js.map +1 -0
  18. package/out/build-candidate-cli.d.ts +16 -0
  19. package/out/build-candidate-cli.d.ts.map +1 -0
  20. package/out/build-candidate-cli.js +80 -0
  21. package/out/build-candidate-cli.js.map +1 -0
  22. package/out/build-candidate.d.ts +54 -0
  23. package/out/build-candidate.d.ts.map +1 -0
  24. package/out/build-candidate.js +230 -0
  25. package/out/build-candidate.js.map +1 -0
  26. package/out/build-coincident-roles-cli.d.ts +16 -0
  27. package/out/build-coincident-roles-cli.d.ts.map +1 -0
  28. package/out/build-coincident-roles-cli.js +94 -0
  29. package/out/build-coincident-roles-cli.js.map +1 -0
  30. package/out/build-fts-cli.d.ts +23 -0
  31. package/out/build-fts-cli.d.ts.map +1 -0
  32. package/out/build-fts-cli.js +117 -0
  33. package/out/build-fts-cli.js.map +1 -0
  34. package/out/build-slim-cli.d.ts +14 -0
  35. package/out/build-slim-cli.d.ts.map +1 -0
  36. package/out/build-slim-cli.js +130 -0
  37. package/out/build-slim-cli.js.map +1 -0
  38. package/out/build-slim.d.ts +71 -0
  39. package/out/build-slim.d.ts.map +1 -0
  40. package/out/build-slim.js +267 -0
  41. package/out/build-slim.js.map +1 -0
  42. package/out/candidate-lookup.d.ts +43 -0
  43. package/out/candidate-lookup.d.ts.map +1 -0
  44. package/out/candidate-lookup.js +191 -0
  45. package/out/candidate-lookup.js.map +1 -0
  46. package/out/candidate-schema.d.ts +86 -0
  47. package/out/candidate-schema.d.ts.map +1 -0
  48. package/out/candidate-schema.js +109 -0
  49. package/out/candidate-schema.js.map +1 -0
  50. package/out/coincident-roles.d.ts +86 -0
  51. package/out/coincident-roles.d.ts.map +1 -0
  52. package/out/coincident-roles.js +160 -0
  53. package/out/coincident-roles.js.map +1 -0
  54. package/out/convention.d.ts +109 -0
  55. package/out/convention.d.ts.map +1 -0
  56. package/out/convention.js +94 -0
  57. package/out/convention.js.map +1 -0
  58. package/out/fst-autocomplete.d.ts +49 -0
  59. package/out/fst-autocomplete.d.ts.map +1 -0
  60. package/out/fst-autocomplete.js +124 -0
  61. package/out/fst-autocomplete.js.map +1 -0
  62. package/out/fst-builder.d.ts +20 -0
  63. package/out/fst-builder.d.ts.map +1 -0
  64. package/out/fst-builder.js +219 -0
  65. package/out/fst-builder.js.map +1 -0
  66. package/out/fst-deserialize-web.d.ts +16 -0
  67. package/out/fst-deserialize-web.d.ts.map +1 -0
  68. package/out/fst-deserialize-web.js +133 -0
  69. package/out/fst-deserialize-web.js.map +1 -0
  70. package/out/fst-matcher.d.ts +33 -0
  71. package/out/fst-matcher.d.ts.map +1 -0
  72. package/out/fst-matcher.js +117 -0
  73. package/out/fst-matcher.js.map +1 -0
  74. package/out/fst-serialize.d.ts +30 -0
  75. package/out/fst-serialize.d.ts.map +1 -0
  76. package/out/fst-serialize.js +261 -0
  77. package/out/fst-serialize.js.map +1 -0
  78. package/out/fst-types.d.ts +60 -0
  79. package/out/fst-types.d.ts.map +1 -0
  80. package/out/fst-types.js +11 -0
  81. package/out/fst-types.js.map +1 -0
  82. package/out/fts.d.ts +158 -0
  83. package/out/fts.d.ts.map +1 -0
  84. package/out/fts.js +261 -0
  85. package/out/fts.js.map +1 -0
  86. package/out/geo.d.ts +74 -0
  87. package/out/geo.d.ts.map +1 -0
  88. package/out/geo.js +88 -0
  89. package/out/geo.js.map +1 -0
  90. package/out/index.d.ts +27 -0
  91. package/out/index.d.ts.map +1 -0
  92. package/out/index.js +22 -0
  93. package/out/index.js.map +1 -0
  94. package/out/interpolation.d.ts +84 -0
  95. package/out/interpolation.d.ts.map +1 -0
  96. package/out/interpolation.js +150 -0
  97. package/out/interpolation.js.map +1 -0
  98. package/out/lookup.d.ts +156 -0
  99. package/out/lookup.d.ts.map +1 -0
  100. package/out/lookup.js +876 -0
  101. package/out/lookup.js.map +1 -0
  102. package/out/postal-city-alias-lookup.d.ts +50 -0
  103. package/out/postal-city-alias-lookup.d.ts.map +1 -0
  104. package/out/postal-city-alias-lookup.js +66 -0
  105. package/out/postal-city-alias-lookup.js.map +1 -0
  106. package/out/postal-city-alias-schema.d.ts +51 -0
  107. package/out/postal-city-alias-schema.d.ts.map +1 -0
  108. package/out/postal-city-alias-schema.js +47 -0
  109. package/out/postal-city-alias-schema.js.map +1 -0
  110. package/out/postal-city-candidate-schema.d.ts +58 -0
  111. package/out/postal-city-candidate-schema.d.ts.map +1 -0
  112. package/out/postal-city-candidate-schema.js +56 -0
  113. package/out/postal-city-candidate-schema.js.map +1 -0
  114. package/out/postcode-point-lookup.d.ts +38 -0
  115. package/out/postcode-point-lookup.d.ts.map +1 -0
  116. package/out/postcode-point-lookup.js +46 -0
  117. package/out/postcode-point-lookup.js.map +1 -0
  118. package/out/reverse.d.ts +99 -0
  119. package/out/reverse.d.ts.map +1 -0
  120. package/out/reverse.js +290 -0
  121. package/out/reverse.js.map +1 -0
  122. package/out/schema.d.ts +163 -0
  123. package/out/schema.d.ts.map +1 -0
  124. package/out/schema.js +18 -0
  125. package/out/schema.js.map +1 -0
  126. package/out/sharding.d.ts +96 -0
  127. package/out/sharding.d.ts.map +1 -0
  128. package/out/sharding.js +129 -0
  129. package/out/sharding.js.map +1 -0
  130. package/out/sqlite-convention-source.d.ts +29 -0
  131. package/out/sqlite-convention-source.d.ts.map +1 -0
  132. package/out/sqlite-convention-source.js +53 -0
  133. package/out/sqlite-convention-source.js.map +1 -0
  134. package/out/sqlite-utils.d.ts +17 -0
  135. package/out/sqlite-utils.d.ts.map +1 -0
  136. package/out/sqlite-utils.js +24 -0
  137. package/out/sqlite-utils.js.map +1 -0
  138. package/out/street-morphology-fst-builder.d.ts +59 -0
  139. package/out/street-morphology-fst-builder.d.ts.map +1 -0
  140. package/out/street-morphology-fst-builder.js +174 -0
  141. package/out/street-morphology-fst-builder.js.map +1 -0
  142. package/out/street-normalize.d.ts +66 -0
  143. package/out/street-normalize.d.ts.map +1 -0
  144. package/out/street-normalize.js +176 -0
  145. package/out/street-normalize.js.map +1 -0
  146. package/out/street-segment-schema.d.ts +61 -0
  147. package/out/street-segment-schema.d.ts.map +1 -0
  148. package/out/street-segment-schema.js +64 -0
  149. package/out/street-segment-schema.js.map +1 -0
  150. package/out/types.d.ts +137 -0
  151. package/out/types.d.ts.map +1 -0
  152. package/out/types.js +13 -0
  153. package/out/types.js.map +1 -0
  154. package/out/unified-schema.d.ts +25 -0
  155. package/out/unified-schema.d.ts.map +1 -0
  156. package/out/unified-schema.js +142 -0
  157. package/out/unified-schema.js.map +1 -0
  158. package/package.json +54 -0
package/out/reverse.js ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reverse geocoding (#484): `(lat, lon)` → the containing admin hierarchy. Assembly over existing
7
+ * machinery, per the 2026-06-11 scoping notes:
8
+ *
9
+ * 1. **Candidate fetch** — the admin DB's `place_bbox` R*Tree (built by `fts.ts`) for places whose
10
+ * bbox contains the point, smallest-area-first (so the FIRST polygon confirmation is the
11
+ * deepest).
12
+ * 2. **PIP confirmation** — ray-cast (geo.ts, the canonical TS port of
13
+ * `scripts/eval/pip-containment.py`) against the polygon sidecar DB (`wof-polygons.db`,
14
+ * `polygons(id, geom)` with GeoJSON text — built by `scripts/build-wof-polygons.mjs` for the
15
+ * demo map). A candidate whose polygon EXISTS but rejects the point is a bbox false positive
16
+ * and is dropped entirely; a candidate with no polygon row stays eligible for the
17
+ * approximate fallback.
18
+ * 3. **Approximate descent** — WOF carries point geometry for most localities (#292: ~99% of JP
19
+ * municipalities; ~half of US localities have degenerate bboxes too), so the polygon walk
20
+ * usually bottoms out at county level. We then descend tier-by-tier (county → localadmin →
21
+ * locality → …) through the winner's DESCENDANTS (the `ancestors` table, reversed), taking
22
+ * the PIP-confirmed child when a polygon exists and the nearest-centroid child otherwise —
23
+ * the latter flagged `containment: "approximate"`, the demo's honesty convention.
24
+ * 4. **Hierarchy assembly** — the deepest place's ancestor chain via the SAME walk forward resolution
25
+ * uses (`ancestry.ts`, #404), so consumers get a symmetric tree.
26
+ *
27
+ * Reverse quality is country-dependent (polygon coverage: see the #292 JP finding); `containment`
28
+ * says so per result rather than pretending.
29
+ */
30
+ import { DatabaseSync } from "node:sqlite";
31
+ import { ancestorLineage, placetypeDepth } from "./ancestry.js";
32
+ import { PLACE_BBOX_TABLE } from "./fts.js";
33
+ import { geometryContains, haversineKm } from "./geo.js";
34
+ const DEFAULT_MAX_CANDIDATES = 128;
35
+ const DEFAULT_MAX_APPROXIMATE_KM = 25;
36
+ /**
37
+ * The tier ladder for the approximate descent, coarsest-first. Each tier is attempted among the
38
+ * CURRENT winner's descendants; a tier with no rows is skipped (e.g. counties without localadmins
39
+ * jump straight to locality).
40
+ */
41
+ const DESCENT_TIERS = [
42
+ "county",
43
+ "localadmin",
44
+ "locality",
45
+ "borough",
46
+ "neighbourhood",
47
+ "microhood",
48
+ ];
49
+ function toPlaceCandidate(row, distanceKm) {
50
+ const c = {
51
+ id: row.id,
52
+ name: row.name,
53
+ placetype: row.placetype,
54
+ country: row.country ?? "",
55
+ lat: row.lat,
56
+ lon: row.lon,
57
+ parent_id: row.parent_id ?? undefined,
58
+ score: 0,
59
+ };
60
+ if (distanceKm !== undefined)
61
+ c.distanceKm = distanceKm;
62
+ return c;
63
+ }
64
+ export class WofReverseGeocoder {
65
+ #admin;
66
+ #ownsAdmin;
67
+ #polygons;
68
+ #ownsPolygons;
69
+ /**
70
+ * Parsed-geometry cache. Reverse queries cluster geographically (an eval run hits the same ~15
71
+ * county polygons 1400 times), so caching the JSON.parse pays for itself immediately. Bounded —
72
+ * cleared wholesale at the cap rather than LRU-tracked; the polygons are DP-simplified and small,
73
+ * the cap exists only to keep a long-lived server process honest.
74
+ */
75
+ #geometryCache = new Map();
76
+ static #GEOMETRY_CACHE_CAP = 4096;
77
+ constructor(opts) {
78
+ if (opts.adminDatabase && opts.adminDbPath) {
79
+ throw new Error("WofReverseGeocoder: pass either `adminDatabase` or `adminDbPath`, not both");
80
+ }
81
+ if (!opts.adminDatabase && !opts.adminDbPath) {
82
+ throw new Error("WofReverseGeocoder: one of `adminDatabase` or `adminDbPath` is required");
83
+ }
84
+ if (opts.polygonDatabase && opts.polygonDbPath) {
85
+ throw new Error("WofReverseGeocoder: pass either `polygonDatabase` or `polygonDbPath`, not both");
86
+ }
87
+ this.#admin = opts.adminDatabase ?? new DatabaseSync(opts.adminDbPath, { readOnly: true });
88
+ this.#ownsAdmin = !opts.adminDatabase;
89
+ this.#polygons =
90
+ opts.polygonDatabase ?? (opts.polygonDbPath ? new DatabaseSync(opts.polygonDbPath, { readOnly: true }) : null);
91
+ this.#ownsPolygons = !opts.polygonDatabase && Boolean(opts.polygonDbPath);
92
+ // Fail loudly up front — the R*Tree is a build artifact, not part of the upstream WOF
93
+ // distribution, and a missing index would otherwise surface as an opaque SQL error per query.
94
+ const hasBbox = this.#admin
95
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
96
+ .get(PLACE_BBOX_TABLE);
97
+ if (!hasBbox) {
98
+ throw new Error(`WofReverseGeocoder: the admin DB has no \`${PLACE_BBOX_TABLE}\` R*Tree. Build it with ` +
99
+ "`mailwoman-wof-build-fts <path-to-wof.db>` (see resolver-wof-sqlite/README.md).");
100
+ }
101
+ if (this.#polygons) {
102
+ const hasPolygons = this.#polygons
103
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'polygons'`)
104
+ .get();
105
+ if (!hasPolygons) {
106
+ throw new Error("WofReverseGeocoder: the polygon DB has no `polygons` table. Expected a `wof-polygons.db` " +
107
+ "built by scripts/build-wof-polygons.mjs.");
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Resolve a WGS-84 point to its containing admin hierarchy. Async for symmetry with
113
+ * `PlaceLookup.findPlace` (the work is sync `node:sqlite` underneath — same convention).
114
+ */
115
+ async reverseGeocode(lat, lon, opts = {}) {
116
+ if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) {
117
+ throw new RangeError(`WofReverseGeocoder.reverseGeocode: (${lat}, ${lon}) is not a WGS-84 coordinate`);
118
+ }
119
+ const maxApproximateKm = opts.maxApproximateKm ?? DEFAULT_MAX_APPROXIMATE_KM;
120
+ const candidates = this.#bboxCandidates(lat, lon, opts);
121
+ // PIP walk, smallest-bbox-first: the first polygon that contains the point is the deepest
122
+ // polygon-confirmable place. Polygon-rejected candidates are bbox false positives — dropped.
123
+ let winner = null;
124
+ let winnerConfirmed = false;
125
+ const pointOnly = [];
126
+ for (const c of candidates) {
127
+ const contains = geometryContains(this.#geometry(c.id), lon, lat);
128
+ if (contains === true) {
129
+ winner = c;
130
+ winnerConfirmed = true;
131
+ break;
132
+ }
133
+ if (contains === null)
134
+ pointOnly.push(c);
135
+ }
136
+ if (!winner) {
137
+ // No polygon confirmed anywhere — nearest centroid among the polygon-less bbox candidates.
138
+ let bestKm = Infinity;
139
+ for (const c of pointOnly) {
140
+ const km = haversineKm(lat, lon, c.lat, c.lon);
141
+ if (km < bestKm) {
142
+ bestKm = km;
143
+ winner = c;
144
+ }
145
+ }
146
+ if (!winner)
147
+ return { hierarchy: [], containment: "approximate" };
148
+ }
149
+ // Approximate descent into finer tiers than the winner.
150
+ let current = winner;
151
+ let currentConfirmed = winnerConfirmed;
152
+ let currentDistanceKm = currentConfirmed ? undefined : haversineKm(lat, lon, current.lat, current.lon);
153
+ for (const tier of DESCENT_TIERS) {
154
+ if (placetypeDepth(tier) <= placetypeDepth(current.placetype))
155
+ continue;
156
+ if (opts.placetypes && !opts.placetypes.includes(tier))
157
+ continue;
158
+ const kids = this.#descendants(current.id, tier, lat, lon, maxApproximateKm);
159
+ let next = null;
160
+ let nextConfirmed = false;
161
+ let nextKm;
162
+ let bestKm = Infinity;
163
+ for (const k of kids) {
164
+ const contains = geometryContains(this.#geometry(k.id), lon, lat);
165
+ if (contains === true) {
166
+ next = k;
167
+ nextConfirmed = true;
168
+ nextKm = undefined;
169
+ break;
170
+ }
171
+ if (contains === false)
172
+ continue; // known not-here — polygon rejected
173
+ const km = haversineKm(lat, lon, k.lat, k.lon);
174
+ if (km <= maxApproximateKm && km < bestKm) {
175
+ bestKm = km;
176
+ next = k;
177
+ nextConfirmed = false;
178
+ nextKm = km;
179
+ }
180
+ }
181
+ if (next) {
182
+ current = next;
183
+ currentConfirmed = nextConfirmed;
184
+ currentDistanceKm = nextKm;
185
+ }
186
+ // An empty tier is NOT terminal — counties without localadmins jump straight to locality.
187
+ }
188
+ // Hierarchy assembly via the shared ancestor walk. If the descent crossed an ancestry gap
189
+ // (the deepest place's recorded lineage misses the PIP root), merge the root's own chain so
190
+ // region/country are always present when a polygon confirmed them.
191
+ const byId = new Map();
192
+ byId.set(current.id, toPlaceCandidate(current, currentDistanceKm));
193
+ for (const a of ancestorLineage(this.#admin, current.id)) {
194
+ if (!byId.has(a.id)) {
195
+ byId.set(a.id, { ...a, placetype: a.placetype, country: a.country ?? "", score: 0 });
196
+ }
197
+ }
198
+ if (!byId.has(winner.id)) {
199
+ byId.set(winner.id, toPlaceCandidate(winner));
200
+ for (const a of ancestorLineage(this.#admin, winner.id)) {
201
+ if (!byId.has(a.id)) {
202
+ byId.set(a.id, { ...a, placetype: a.placetype, country: a.country ?? "", score: 0 });
203
+ }
204
+ }
205
+ }
206
+ const hierarchy = [...byId.values()];
207
+ if (opts.placetypes) {
208
+ const allowed = new Set(opts.placetypes);
209
+ for (let i = hierarchy.length - 1; i >= 0; i--) {
210
+ if (!allowed.has(hierarchy[i].placetype))
211
+ hierarchy.splice(i, 1);
212
+ }
213
+ }
214
+ hierarchy.sort((a, b) => placetypeDepth(b.placetype) - placetypeDepth(a.placetype));
215
+ return { hierarchy, containment: currentConfirmed ? "polygon" : "approximate" };
216
+ }
217
+ /** Bbox candidates containing the point, smallest-area-first, via the `place_bbox` R*Tree. */
218
+ #bboxCandidates(lat, lon, opts) {
219
+ const where = [
220
+ "bbox.min_lat <= ?",
221
+ "bbox.max_lat >= ?",
222
+ "bbox.min_lon <= ?",
223
+ "bbox.max_lon >= ?",
224
+ "spr.is_current != 0",
225
+ "spr.is_deprecated = 0",
226
+ ];
227
+ const params = [lat, lat, lon, lon];
228
+ if (opts.placetypes && opts.placetypes.length > 0) {
229
+ where.push(`spr.placetype IN (${opts.placetypes.map(() => "?").join(", ")})`);
230
+ params.push(...opts.placetypes);
231
+ }
232
+ params.push(opts.maxCandidates ?? DEFAULT_MAX_CANDIDATES);
233
+ return this.#admin
234
+ .prepare(`SELECT spr.id AS id, spr.name AS name, spr.placetype AS placetype, spr.country AS country,
235
+ spr.parent_id AS parent_id, spr.latitude AS lat, spr.longitude AS lon
236
+ FROM ${PLACE_BBOX_TABLE} bbox JOIN spr ON spr.id = bbox.id
237
+ WHERE ${where.join(" AND ")}
238
+ ORDER BY (bbox.max_lat - bbox.min_lat) * (bbox.max_lon - bbox.min_lon) ASC
239
+ LIMIT ?`)
240
+ .all(...params);
241
+ }
242
+ /**
243
+ * Descendants of `parentId` at one placetype tier, pre-filtered to a centroid window around the
244
+ * query point (a generous 4× the approximate cap — polygon-holding children may legitimately have
245
+ * far centroids, e.g. a sprawling consolidated city; the precise cap is applied per-candidate in
246
+ * the caller, and only to centroid-fallback steps).
247
+ */
248
+ #descendants(parentId, placetype, lat, lon, maxApproximateKm) {
249
+ const windowDeg = (maxApproximateKm * 4) / 111;
250
+ return this.#admin
251
+ .prepare(`SELECT s.id AS id, s.name AS name, s.placetype AS placetype, s.country AS country,
252
+ s.parent_id AS parent_id, s.latitude AS lat, s.longitude AS lon
253
+ FROM ancestors a JOIN spr s ON s.id = a.id
254
+ WHERE a.ancestor_id = ? AND s.placetype = ? AND s.is_current != 0 AND s.is_deprecated = 0
255
+ AND s.latitude BETWEEN ? AND ? AND s.longitude BETWEEN ? AND ?`)
256
+ .all(parentId, placetype, lat - windowDeg, lat + windowDeg, lon - windowDeg, lon + windowDeg);
257
+ }
258
+ /** Parsed GeoJSON geometry for a WOF id, or null when absent / unparseable / no polygon DB. */
259
+ #geometry(id) {
260
+ if (!this.#polygons)
261
+ return null;
262
+ const cached = this.#geometryCache.get(id);
263
+ if (cached !== undefined)
264
+ return cached;
265
+ if (this.#geometryCache.size >= WofReverseGeocoder.#GEOMETRY_CACHE_CAP)
266
+ this.#geometryCache.clear();
267
+ const row = this.#polygons.prepare(`SELECT geom FROM polygons WHERE id = ?`).get(id);
268
+ let geometry = null;
269
+ if (row) {
270
+ try {
271
+ geometry = JSON.parse(row.geom);
272
+ }
273
+ catch {
274
+ geometry = null; // malformed row — treat as no-polygon rather than failing the query
275
+ }
276
+ }
277
+ this.#geometryCache.set(id, geometry);
278
+ return geometry;
279
+ }
280
+ close() {
281
+ if (this.#ownsAdmin)
282
+ this.#admin.close();
283
+ if (this.#ownsPolygons)
284
+ this.#polygons?.close();
285
+ }
286
+ [Symbol.dispose]() {
287
+ this.close();
288
+ }
289
+ }
290
+ //# sourceMappingURL=reverse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverse.js","sourceRoot":"","sources":["../reverse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Kysely table types for the subset of the Who's On First SQLite schema we touch in Phase 4.2.
7
+ *
8
+ * The full upstream distribution at data.geocode.earth/wof/dist/sqlite/ ships ~7 tables; this file
9
+ * models only the ones we read. Pretending to model the others would be misleading — we haven't
10
+ * verified their shapes and they're not part of the resolver's contract.
11
+ *
12
+ * Authoritative schema docs:
13
+ *
14
+ * - Whosonfirst SQLite README: https://github.com/whosonfirst/go-whosonfirst-sqlite
15
+ * - Per-table sources under https://github.com/whosonfirst/go-whosonfirst-sqlite-features
16
+ */
17
+ /**
18
+ * The FTS5 virtual table built by this package on first open (NOT shipped by upstream WOF).
19
+ *
20
+ * `content` is unindexed — it's there so we can roundtrip the original name back to the caller
21
+ * without a second SELECT. The actual FTS rebuild happens in `fts.ts::buildPlaceSearchFts`.
22
+ */
23
+ export interface PlaceSearchTable {
24
+ rowid: number;
25
+ wof_id: number;
26
+ name: string;
27
+ alt_names: string | null;
28
+ }
29
+ /**
30
+ * `spr` — the Who's On First "Standard Places Response": a denormalized lightweight summary of one
31
+ * row per place. The resolver's main lookup table.
32
+ *
33
+ * Lifecycle flags carry TWO conventions, both meaning "currently valid": `is_current = -1` (modern
34
+ * Who's On First) and `is_current = 1` (legacy Mapzen-era). Only `is_current = 0` means "not
35
+ * current". Filters in `lookup.ts` and `fts.ts` use `is_current != 0 AND is_deprecated = 0` — see
36
+ * #91 for the diagnostic that uncovered the mixed-convention reality.
37
+ *
38
+ * Lat/lon live directly on this row — no GeoJSON extraction needed for centroid resolution. `min_*`
39
+ * / `max_*` form a bounding box if callers want one (Phase 4.3 candidate).
40
+ */
41
+ export interface SprTable {
42
+ id: number;
43
+ parent_id: number | null;
44
+ name: string | null;
45
+ placetype: string | null;
46
+ country: string | null;
47
+ latitude: number;
48
+ longitude: number;
49
+ min_latitude: number;
50
+ min_longitude: number;
51
+ max_latitude: number;
52
+ max_longitude: number;
53
+ is_current: number;
54
+ is_deprecated: number;
55
+ is_ceased: number;
56
+ is_superseded: number;
57
+ is_superseding: number;
58
+ superseded_by: string | null;
59
+ supersedes: string | null;
60
+ lastmodified: number;
61
+ }
62
+ /**
63
+ * Alternate names per place, keyed by language tag subfields (BCP-47 components). Joins back to
64
+ * `spr.id` via `id` (NOT `place_id` — the real WOF schema uses the same column name as the spr
65
+ * primary key; this is a normal join across two tables with the same FK column name).
66
+ *
67
+ * No `kind` column in real WOF — the FTS build just concatenates ALL names per id.
68
+ */
69
+ export interface NamesTable {
70
+ id: number;
71
+ placetype: string | null;
72
+ country: string | null;
73
+ language: string | null;
74
+ extlang: string | null;
75
+ script: string | null;
76
+ region: string | null;
77
+ variant: string | null;
78
+ extension: string | null;
79
+ privateuse: string | null;
80
+ name: string;
81
+ lastmodified: number;
82
+ }
83
+ /**
84
+ * Per-place GeoJSON blob. Centroid lat/lon are already exposed via `spr.{latitude,longitude}` so
85
+ * the resolver doesn't need to parse this; we keep the table modeled in case Phase 4.3 wants the
86
+ * full geometry for bbox / polygon work.
87
+ */
88
+ export interface GeojsonTable {
89
+ id: number;
90
+ body: string;
91
+ source: string | null;
92
+ alt_label: string | null;
93
+ is_alt: number;
94
+ lastmodified: number;
95
+ }
96
+ /**
97
+ * Adjacency table for ancestor relationships. One row per (place, ancestor) pair, including
98
+ * transitive ancestors. Used to implement `FindPlaceQuery.parentId` (descendant lookup).
99
+ */
100
+ export interface AncestorsTable {
101
+ id: number;
102
+ ancestor_id: number;
103
+ ancestor_placetype: string | null;
104
+ lastmodified: number;
105
+ }
106
+ /**
107
+ * `place_population` — `id → wof:population`, split off `spr` so a population-rank join is a single
108
+ * indexed probe. Written by the build/augment ingest + the GeoNames backfill; read by the candidate
109
+ * build's `neg_rank`. WOF carries population for ~15% of localities; absent = unknown, not zero.
110
+ */
111
+ export interface PlacePopulationTable {
112
+ id: number;
113
+ population: number;
114
+ }
115
+ /**
116
+ * `place_abbr` — `id → abbreviation` (e.g. `IL → Illinois`), derived from `names` rows whose
117
+ * `language = 'abbr'`. Lets the resolver accept a 2-letter region abbreviation as an exact match.
118
+ */
119
+ export interface PlaceAbbrTable {
120
+ id: number;
121
+ abbr: string;
122
+ }
123
+ /**
124
+ * `concordances` — external-id cross-references per place (`id → (other_source, other_id)`), e.g. a
125
+ * GeoNames or Overture GERS id. Metadata only; not part of the resolve path.
126
+ */
127
+ export interface ConcordancesTable {
128
+ id: number;
129
+ other_id: string;
130
+ other_source: string;
131
+ lastmodified: number;
132
+ }
133
+ /**
134
+ * `coincident_roles` (#402) — the dual-role relation: a place that is BOTH an admin region AND a
135
+ * locality (Berlin the city-state). One row per (admin, locality) pair the resolver can complete a
136
+ * hierarchy with. Surfaced by {@link MailwomanLookupLike.coincidentRolesFor}.
137
+ */
138
+ export interface CoincidentRolesTable {
139
+ admin_id: number;
140
+ locality_id: number;
141
+ relationship_type: string;
142
+ admin_placetype: string;
143
+ distance_km: number;
144
+ locality_population: number;
145
+ }
146
+ /**
147
+ * The full schema we hand to `Kysely<WofDatabase>` / `new DatabaseClient<WofDatabase>(...)`. Tables
148
+ * not listed here will fail type-checked queries — by design. The reader
149
+ * ({@link WofSqlitePlaceLookup}) already consumes this; the build/augment WRITERS adopt it so a
150
+ * column rename is a compile error on both sides (the drift that bit the corpus TIGER adapter).
151
+ */
152
+ export interface WofDatabase {
153
+ place_search: PlaceSearchTable;
154
+ spr: SprTable;
155
+ names: NamesTable;
156
+ geojson: GeojsonTable;
157
+ ancestors: AncestorsTable;
158
+ place_population: PlacePopulationTable;
159
+ place_abbr: PlaceAbbrTable;
160
+ concordances: ConcordancesTable;
161
+ coincident_roles: CoincidentRolesTable;
162
+ }
163
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../schema.ts"],"names":[],"mappings":"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"}
package/out/schema.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Kysely table types for the subset of the Who's On First SQLite schema we touch in Phase 4.2.
7
+ *
8
+ * The full upstream distribution at data.geocode.earth/wof/dist/sqlite/ ships ~7 tables; this file
9
+ * models only the ones we read. Pretending to model the others would be misleading — we haven't
10
+ * verified their shapes and they're not part of the resolver's contract.
11
+ *
12
+ * Authoritative schema docs:
13
+ *
14
+ * - Whosonfirst SQLite README: https://github.com/whosonfirst/go-whosonfirst-sqlite
15
+ * - Per-table sources under https://github.com/whosonfirst/go-whosonfirst-sqlite-features
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Multi-shard support for `WofSqlitePlaceLookup` — opens multiple WOF SQLite distributions on one
7
+ * connection via `ATTACH DATABASE`, and routes queries to the right shard based on placetype.
8
+ *
9
+ * ## The FTS5 syntax rule that drove this design
10
+ *
11
+ * The naive `SELECT … FROM pc.place_search WHERE pc.place_search MATCH ?` fails — SQLite parses the
12
+ * schema-qualified table on the left of MATCH as "column place_search of table pc". Discovered in
13
+ * the spike at PR review time; documented as `_SHARD_RULE.md` should it ever bite again.
14
+ *
15
+ * The working form: schema-qualified in FROM, bare table name in MATCH:
16
+ *
17
+ * ```sql
18
+ * SELECT … FROM pc.place_search WHERE place_search MATCH ?
19
+ * ```
20
+ *
21
+ * Identical table names across attached shards (which is what we have — every shard ships its own
22
+ * `place_search` + `place_bbox`) are fine because the bare-name MATCH resolves against FROM
23
+ * scope.
24
+ */
25
+ /**
26
+ * Derive a SQL-safe schema name from a WOF distribution filename. Used by `ATTACH DATABASE … AS
27
+ * <name>` so each shard gets a stable, predictable handle.
28
+ *
29
+ * Convention strips the `whosonfirst-data-` prefix and the `-latest.db` (or just `.db`) suffix,
30
+ * then replaces `-` with `_` for SQL identifier safety.
31
+ *
32
+ * Examples:
33
+ *
34
+ * - `whosonfirst-data-admin-us-latest.db` → `admin_us`
35
+ * - `whosonfirst-data-postalcode-us-latest.db` → `postalcode_us`
36
+ * - `whosonfirst-data-admin-latest.db` → `admin`
37
+ * - `my-custom.db` → `my_custom`
38
+ *
39
+ * Callers can override the derived name explicitly via `ShardConfig.schemaName` when the filename
40
+ * doesn't follow WOF convention.
41
+ */
42
+ export declare function deriveSchemaName(path: string): string;
43
+ /**
44
+ * Per-shard configuration. The simple form is just a path string — the schema name is derived from
45
+ * it. The object form lets callers override the derived schema name (useful when a filename doesn't
46
+ * follow WOF convention) or attach an extra hint about which placetypes route here.
47
+ */
48
+ export interface ShardConfig {
49
+ path: string;
50
+ /**
51
+ * Override the auto-derived schema name. Useful when the filename doesn't match WOF convention or
52
+ * when you want a memorable handle. Must be a valid SQLite identifier —
53
+ * `[a-zA-Z_][a-zA-Z0-9_]*`.
54
+ */
55
+ schemaName?: string;
56
+ /**
57
+ * Optional explicit list of placetypes this shard serves. When set, queries against any listed
58
+ * placetype are routed to this shard. When omitted, routing falls back to a name-match heuristic:
59
+ * a shard whose `schemaName` contains the placetype as a substring (e.g. `postalcode_us` for
60
+ * `postalcode` queries) is preferred for that placetype.
61
+ */
62
+ placetypes?: readonly string[];
63
+ }
64
+ /**
65
+ * Resolved post-derivation: paired path + chosen schema name + (possibly empty) placetypes hint.
66
+ * Used internally by `WofSqlitePlaceLookup` so the routing logic operates on uniform structures.
67
+ */
68
+ export interface ResolvedShard {
69
+ path: string;
70
+ schemaName: string;
71
+ placetypes: readonly string[];
72
+ }
73
+ /**
74
+ * Normalize the user-provided `databasePath` opt (which may be a single string, an array of
75
+ * strings, or an array of `ShardConfig` objects) into a uniform `ResolvedShard[]`.
76
+ *
77
+ * The first shard becomes `main` regardless of its derived schema name — that's the SQLite
78
+ * convention. Subsequent shards keep their derived (or override) schema name.
79
+ */
80
+ export declare function resolveShards(input: string | ReadonlyArray<string | ShardConfig>): ResolvedShard[];
81
+ /**
82
+ * Pick the shard to route a query to given the requested placetype(s).
83
+ *
84
+ * Routing rules, in order:
85
+ *
86
+ * 1. If any shard has explicit `placetypes` that includes the requested placetype, use it.
87
+ * 2. Otherwise, if a non-main shard's `schemaName` matches the placetype (e.g. `postalcode_us` matches
88
+ * `postalcode`), use it.
89
+ * 3. Otherwise, fall back to `main`.
90
+ *
91
+ * This deliberately doesn't UNION across shards — BM25 scores aren't comparable across separately-
92
+ * indexed corpora, and the typical mailwoman query has a single placetype anyway. If a caller needs
93
+ * cross-shard results they can issue two `findPlace` calls.
94
+ */
95
+ export declare function pickShardForPlacetype(shards: ResolvedShard[], placetype: string | undefined): ResolvedShard;
96
+ //# sourceMappingURL=sharding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharding.d.ts","sourceRoot":"","sources":["../sharding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAKD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,aAAa,EAAE,CAkClG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAmB3G"}