@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.
- package/README.md +250 -0
- package/out/address-point-interpolation.d.ts +48 -0
- package/out/address-point-interpolation.d.ts.map +1 -0
- package/out/address-point-interpolation.js +164 -0
- package/out/address-point-interpolation.js.map +1 -0
- package/out/address-point-schema.d.ts +58 -0
- package/out/address-point-schema.d.ts.map +1 -0
- package/out/address-point-schema.js +67 -0
- package/out/address-point-schema.js.map +1 -0
- package/out/address-point.d.ts +29 -0
- package/out/address-point.d.ts.map +1 -0
- package/out/address-point.js +62 -0
- package/out/address-point.js.map +1 -0
- package/out/ancestry.d.ts +40 -0
- package/out/ancestry.d.ts.map +1 -0
- package/out/ancestry.js +53 -0
- package/out/ancestry.js.map +1 -0
- package/out/build-candidate-cli.d.ts +16 -0
- package/out/build-candidate-cli.d.ts.map +1 -0
- package/out/build-candidate-cli.js +80 -0
- package/out/build-candidate-cli.js.map +1 -0
- package/out/build-candidate.d.ts +54 -0
- package/out/build-candidate.d.ts.map +1 -0
- package/out/build-candidate.js +230 -0
- package/out/build-candidate.js.map +1 -0
- package/out/build-coincident-roles-cli.d.ts +16 -0
- package/out/build-coincident-roles-cli.d.ts.map +1 -0
- package/out/build-coincident-roles-cli.js +94 -0
- package/out/build-coincident-roles-cli.js.map +1 -0
- package/out/build-fts-cli.d.ts +23 -0
- package/out/build-fts-cli.d.ts.map +1 -0
- package/out/build-fts-cli.js +117 -0
- package/out/build-fts-cli.js.map +1 -0
- package/out/build-slim-cli.d.ts +14 -0
- package/out/build-slim-cli.d.ts.map +1 -0
- package/out/build-slim-cli.js +130 -0
- package/out/build-slim-cli.js.map +1 -0
- package/out/build-slim.d.ts +71 -0
- package/out/build-slim.d.ts.map +1 -0
- package/out/build-slim.js +267 -0
- package/out/build-slim.js.map +1 -0
- package/out/candidate-lookup.d.ts +43 -0
- package/out/candidate-lookup.d.ts.map +1 -0
- package/out/candidate-lookup.js +191 -0
- package/out/candidate-lookup.js.map +1 -0
- package/out/candidate-schema.d.ts +86 -0
- package/out/candidate-schema.d.ts.map +1 -0
- package/out/candidate-schema.js +109 -0
- package/out/candidate-schema.js.map +1 -0
- package/out/coincident-roles.d.ts +86 -0
- package/out/coincident-roles.d.ts.map +1 -0
- package/out/coincident-roles.js +160 -0
- package/out/coincident-roles.js.map +1 -0
- package/out/convention.d.ts +109 -0
- package/out/convention.d.ts.map +1 -0
- package/out/convention.js +94 -0
- package/out/convention.js.map +1 -0
- package/out/fst-autocomplete.d.ts +49 -0
- package/out/fst-autocomplete.d.ts.map +1 -0
- package/out/fst-autocomplete.js +124 -0
- package/out/fst-autocomplete.js.map +1 -0
- package/out/fst-builder.d.ts +20 -0
- package/out/fst-builder.d.ts.map +1 -0
- package/out/fst-builder.js +219 -0
- package/out/fst-builder.js.map +1 -0
- package/out/fst-deserialize-web.d.ts +16 -0
- package/out/fst-deserialize-web.d.ts.map +1 -0
- package/out/fst-deserialize-web.js +133 -0
- package/out/fst-deserialize-web.js.map +1 -0
- package/out/fst-matcher.d.ts +33 -0
- package/out/fst-matcher.d.ts.map +1 -0
- package/out/fst-matcher.js +117 -0
- package/out/fst-matcher.js.map +1 -0
- package/out/fst-serialize.d.ts +30 -0
- package/out/fst-serialize.d.ts.map +1 -0
- package/out/fst-serialize.js +261 -0
- package/out/fst-serialize.js.map +1 -0
- package/out/fst-types.d.ts +60 -0
- package/out/fst-types.d.ts.map +1 -0
- package/out/fst-types.js +11 -0
- package/out/fst-types.js.map +1 -0
- package/out/fts.d.ts +158 -0
- package/out/fts.d.ts.map +1 -0
- package/out/fts.js +261 -0
- package/out/fts.js.map +1 -0
- package/out/geo.d.ts +74 -0
- package/out/geo.d.ts.map +1 -0
- package/out/geo.js +88 -0
- package/out/geo.js.map +1 -0
- package/out/index.d.ts +27 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +22 -0
- package/out/index.js.map +1 -0
- package/out/interpolation.d.ts +84 -0
- package/out/interpolation.d.ts.map +1 -0
- package/out/interpolation.js +150 -0
- package/out/interpolation.js.map +1 -0
- package/out/lookup.d.ts +156 -0
- package/out/lookup.d.ts.map +1 -0
- package/out/lookup.js +876 -0
- package/out/lookup.js.map +1 -0
- package/out/postal-city-alias-lookup.d.ts +50 -0
- package/out/postal-city-alias-lookup.d.ts.map +1 -0
- package/out/postal-city-alias-lookup.js +66 -0
- package/out/postal-city-alias-lookup.js.map +1 -0
- package/out/postal-city-alias-schema.d.ts +51 -0
- package/out/postal-city-alias-schema.d.ts.map +1 -0
- package/out/postal-city-alias-schema.js +47 -0
- package/out/postal-city-alias-schema.js.map +1 -0
- package/out/postal-city-candidate-schema.d.ts +58 -0
- package/out/postal-city-candidate-schema.d.ts.map +1 -0
- package/out/postal-city-candidate-schema.js +56 -0
- package/out/postal-city-candidate-schema.js.map +1 -0
- package/out/postcode-point-lookup.d.ts +38 -0
- package/out/postcode-point-lookup.d.ts.map +1 -0
- package/out/postcode-point-lookup.js +46 -0
- package/out/postcode-point-lookup.js.map +1 -0
- package/out/reverse.d.ts +99 -0
- package/out/reverse.d.ts.map +1 -0
- package/out/reverse.js +290 -0
- package/out/reverse.js.map +1 -0
- package/out/schema.d.ts +163 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +18 -0
- package/out/schema.js.map +1 -0
- package/out/sharding.d.ts +96 -0
- package/out/sharding.d.ts.map +1 -0
- package/out/sharding.js +129 -0
- package/out/sharding.js.map +1 -0
- package/out/sqlite-convention-source.d.ts +29 -0
- package/out/sqlite-convention-source.d.ts.map +1 -0
- package/out/sqlite-convention-source.js +53 -0
- package/out/sqlite-convention-source.js.map +1 -0
- package/out/sqlite-utils.d.ts +17 -0
- package/out/sqlite-utils.d.ts.map +1 -0
- package/out/sqlite-utils.js +24 -0
- package/out/sqlite-utils.js.map +1 -0
- package/out/street-morphology-fst-builder.d.ts +59 -0
- package/out/street-morphology-fst-builder.d.ts.map +1 -0
- package/out/street-morphology-fst-builder.js +174 -0
- package/out/street-morphology-fst-builder.js.map +1 -0
- package/out/street-normalize.d.ts +66 -0
- package/out/street-normalize.d.ts.map +1 -0
- package/out/street-normalize.js +176 -0
- package/out/street-normalize.js.map +1 -0
- package/out/street-segment-schema.d.ts +61 -0
- package/out/street-segment-schema.d.ts.map +1 -0
- package/out/street-segment-schema.js +64 -0
- package/out/street-segment-schema.js.map +1 -0
- package/out/types.d.ts +137 -0
- package/out/types.d.ts.map +1 -0
- package/out/types.js +13 -0
- package/out/types.js.map +1 -0
- package/out/unified-schema.d.ts +25 -0
- package/out/unified-schema.d.ts.map +1 -0
- package/out/unified-schema.js +142 -0
- package/out/unified-schema.js.map +1 -0
- 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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"}
|
package/out/schema.d.ts
ADDED
|
@@ -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"}
|