@mailwoman/nominatim 4.15.1

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 ADDED
@@ -0,0 +1,54 @@
1
+ # @mailwoman/nominatim
2
+
3
+ A **Nominatim-compatible HTTP geocoding API** over the [Mailwoman](https://mailwoman.sister.software) engine. Point an existing Nominatim client at it and forward + reverse geocoding work — no PostgreSQL, no `osm2pgsql` import.
4
+
5
+ ```bash
6
+ npx @mailwoman/nominatim serve --port 8080 --data <gazetteer-or-bundle>
7
+ ```
8
+
9
+ ```python
10
+ from geopy.geocoders import Nominatim
11
+
12
+ geo = Nominatim(domain="localhost:8080", scheme="http")
13
+ geo.geocode("1600 Pennsylvania Ave NW, Washington DC", addressdetails=True)
14
+ geo.reverse((38.8977, -77.0365))
15
+ ```
16
+
17
+ ## Endpoints
18
+
19
+ | Endpoint | Nominatim contract | Status |
20
+ | ---------- | -------------------------------------------------------- | ------- |
21
+ | `/search` | free-text `q` + structured forward geocoding | ✓ |
22
+ | `/reverse` | `lat`/`lon` → nearest address (`WofReverseGeocoder` PIP) | ✓ |
23
+ | `/status` | health + data version | ✓ |
24
+ | `/lookup` | resolve known place ids | planned |
25
+
26
+ ## Library use
27
+
28
+ The package is engine-agnostic — embed it in your own server:
29
+
30
+ ```ts
31
+ import express from "express"
32
+ import { createNominatimRouter, type NominatimEngine } from "@mailwoman/nominatim"
33
+
34
+ const engine: NominatimEngine = {
35
+ /* search, reverse, lookup, status — backed by your Mailwoman pipeline */
36
+ }
37
+ express().use(createNominatimRouter(engine)).listen(8080)
38
+ ```
39
+
40
+ ## Annotations
41
+
42
+ Every result carries an OpenCage-style `annotations` block — coordinate formats (DMS, MGRS, geohash,
43
+ Maidenhead, Mercator), qibla bearing, sun times, country flag, calling code, and currency, plus the IANA
44
+ timezone, UN/LOCODE, and EU NUTS codes when their data bundles are present. Plain Nominatim returns none
45
+ of these.
46
+
47
+ ## Status
48
+
49
+ Shipped. `/search` and `/reverse` resolve over the live engine and return the enriched block; `/lookup`
50
+ is not yet implemented (returns `501`). `addressdetails` goes down to the house number and road when the
51
+ query carries them — `1600 Pennsylvania Avenue NW, Washington, DC 20500` resolves to the rooftop
52
+ (`38.897, -77.037`) with `house_number`, `road`, `city`, `state`, `postcode`, and `country_code`.
53
+
54
+ For autocomplete / type-ahead, see the companion [`@mailwoman/photon`](../photon).
package/out/cli.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-nominatim` — boot a Nominatim-compatible endpoint via the `serve` command. Usage +
8
+ * examples live in the package README.
9
+ *
10
+ * Wires the real engine: `/search` over `geocodeAddress` (parse → resolve), `/reverse` over
11
+ * `WofReverseGeocoder` (point-in-polygon over WOF admin polygons), reusing the same
12
+ * resolver-backend selector GeocodeRouter uses. Results carry the OpenCage-style `annotations`
13
+ * block — coordinate formats, flag, calling code, currency, and (when their DBs are present)
14
+ * timezone, UN/LOCODE, NUTS — composed from the `@mailwoman/*` annotators.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;GAaG"}
package/out/cli.js ADDED
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-nominatim` — boot a Nominatim-compatible endpoint via the `serve` command. Usage +
8
+ * examples live in the package README.
9
+ *
10
+ * Wires the real engine: `/search` over `geocodeAddress` (parse → resolve), `/reverse` over
11
+ * `WofReverseGeocoder` (point-in-polygon over WOF admin polygons), reusing the same
12
+ * resolver-backend selector GeocodeRouter uses. Results carry the OpenCage-style `annotations`
13
+ * block — coordinate formats, flag, calling code, currency, and (when their DBs are present)
14
+ * timezone, UN/LOCODE, NUTS — composed from the `@mailwoman/*` annotators.
15
+ */
16
+ import { composeAnnotators, toOpenCage } from "@mailwoman/annotations";
17
+ import { countryReferenceAnnotator, matchCountry } from "@mailwoman/codex/country";
18
+ import { NeuralAddressClassifier } from "@mailwoman/neural";
19
+ import { makeNutsAnnotator, NutsLookup } from "@mailwoman/nuts-lookup";
20
+ import { createWofResolver } from "@mailwoman/resolver";
21
+ import { coordinateFormatAnnotator } from "@mailwoman/spatial";
22
+ import { makeTimezoneAnnotator, TimezoneLookup } from "@mailwoman/timezone-lookup";
23
+ import { makeUnLocodeAnnotator, UnLocodeLookup } from "@mailwoman/un-locode-lookup";
24
+ import express from "express";
25
+ import { createAddressParser } from "mailwoman";
26
+ import { geocodeAddress, ShardProvider } from "mailwoman/geocode-core";
27
+ import { createResolverBackend, mailwomanDataRoot, resolveCandidateDbPath, wofShardPaths, } from "mailwoman/resolver-backend";
28
+ import { existsSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { parseArgs } from "node:util";
31
+ import { createNominatimRouter, toNominatimResult, } from "./index.js";
32
+ /** WOF placetype → Nominatim address key. */
33
+ const PLACETYPE_TO_KEY = {
34
+ venue: "road",
35
+ street: "road",
36
+ locality: "city",
37
+ localadmin: "city",
38
+ borough: "city_district",
39
+ neighbourhood: "suburb",
40
+ county: "county",
41
+ region: "state",
42
+ macroregion: "state",
43
+ country: "country",
44
+ };
45
+ /**
46
+ * A real address fits comfortably; anything longer is malformed input (and would exceed the model's
47
+ * input window). Cap defensively so a giant query returns no results instead of faulting.
48
+ */
49
+ const MAX_QUERY_LEN = 512;
50
+ function joinNonEmpty(...parts) {
51
+ return parts.filter(Boolean).join(", ");
52
+ }
53
+ /**
54
+ * The resolver returns admin labels + a coordinate (often rooftop/interpolated via the situs
55
+ * shards) but drops the street. Recover house_number + road from the parse so the result carries
56
+ * the full address — Nominatim populates both `addressdetails` and `display_name` down to the house
57
+ * number.
58
+ */
59
+ async function streetParts(parser, query) {
60
+ const solution = (await parser.parse(query, { verbose: true })).solutions[0];
61
+ const matches = solution?.toJSON()?.matches ?? [];
62
+ return {
63
+ houseNumber: matches.find((m) => m.classification === "house_number")?.value,
64
+ road: matches.find((m) => m.classification === "street")?.value,
65
+ };
66
+ }
67
+ /** Map a forward geocode result (admin + coordinate) into the formatter's neutral shape. */
68
+ function forwardToResolved(r) {
69
+ const address = {};
70
+ if (r.locality)
71
+ address.city = r.locality;
72
+ if (r.region)
73
+ address.state = r.region;
74
+ if (r.postcode)
75
+ address.postcode = r.postcode;
76
+ for (const h of r.hierarchy) {
77
+ if (h.tag === "country")
78
+ address.country = h.value;
79
+ }
80
+ return {
81
+ lat: r.lat,
82
+ lon: r.lon,
83
+ address,
84
+ displayName: joinNonEmpty(address.city, address.state, address.postcode, address.country) || r.input,
85
+ };
86
+ }
87
+ async function serve() {
88
+ const { values } = parseArgs({
89
+ options: {
90
+ port: { type: "string", default: "8080" },
91
+ host: { type: "string", default: "0.0.0.0" },
92
+ data: { type: "string" },
93
+ },
94
+ allowPositionals: true,
95
+ });
96
+ const port = Number(values.port) || 8080;
97
+ const host = values.host ?? "0.0.0.0";
98
+ const resolverMod = await import("@mailwoman/resolver-wof-sqlite");
99
+ const wofPaths = wofShardPaths().filter(existsSync);
100
+ const adminDbPath = wofPaths[0];
101
+ const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" });
102
+ const parser = createAddressParser();
103
+ const backend = createResolverBackend(resolverMod, { wofPaths });
104
+ const resolver = createWofResolver(backend);
105
+ const shards = new ShardProvider(resolverMod, mailwomanDataRoot());
106
+ // NOT a geocode country constraint. The default-on #244 placer already routes the query's country
107
+ // (Berlin→DE, Boston→US) and `defaultCountry` is a HARD override that beats it (geocode-core.ts:102),
108
+ // so forcing "US" resolved every non-US query to its US namesake (Berlin→Berlin NH). We let the
109
+ // placer decide instead. This is the fallback used ONLY to annotate the flag/currency/calling-code
110
+ // when the resolved hierarchy omits the country tag — which on US-centric data (no candidate DB)
111
+ // happens for US results, where "US" is the right guess. Non-US results carry the country tag, so
112
+ // the fallback never mislabels them.
113
+ const annotationCountryFallback = resolveCandidateDbPath() ? undefined : "US";
114
+ const reverseGeo = adminDbPath ? new resolverMod.WofReverseGeocoder({ adminDbPath }) : undefined;
115
+ const annotators = [coordinateFormatAnnotator, countryReferenceAnnotator];
116
+ const tzDbPath = join(mailwomanDataRoot(), "timezone", "timezone.db");
117
+ if (existsSync(tzDbPath))
118
+ annotators.push(makeTimezoneAnnotator(new TimezoneLookup({ databasePath: tzDbPath })));
119
+ const ulDbPath = join(mailwomanDataRoot(), "un-locode", "un-locode.db");
120
+ if (existsSync(ulDbPath))
121
+ annotators.push(makeUnLocodeAnnotator(new UnLocodeLookup({ databasePath: ulDbPath })));
122
+ const nutsDbPath = join(mailwomanDataRoot(), "nuts", "nuts.db");
123
+ if (existsSync(nutsDbPath))
124
+ annotators.push(makeNutsAnnotator(new NutsLookup({ databasePath: nutsDbPath })));
125
+ const annotate = composeAnnotators(annotators);
126
+ const engine = {
127
+ async search(params) {
128
+ const query = (params.q ?? joinNonEmpty(params.street, params.city, params.state, params.postalcode, params.country))?.trim();
129
+ // Empty/whitespace → no query; absurdly long → not an address (and would blow the model's input).
130
+ if (!query || query.length > MAX_QUERY_LEN)
131
+ return [];
132
+ // A caller-supplied `countrycodes` is an explicit hard restriction (Nominatim semantics): honor
133
+ // it as the country constraint, even to the point of no result. It doubles as the manual override
134
+ // for the #822 placer frontier — `countrycodes=au` lands Sydney in Australia. One country is the
135
+ // common (geopy) case; for a list we apply the first.
136
+ const userCountry = params.countrycodes?.[0]?.toUpperCase();
137
+ const result = await geocodeAddress(query, {
138
+ classifier,
139
+ resolver,
140
+ shards: shards.for,
141
+ defaultCountry: userCountry,
142
+ });
143
+ if (result.lat == null || result.lon == null)
144
+ return [];
145
+ const resolved = forwardToResolved(result);
146
+ // Recover the street the resolver drops, so addressdetails + display_name carry it.
147
+ const { houseNumber, road } = await streetParts(parser, query);
148
+ if (houseNumber)
149
+ resolved.address.house_number = houseNumber;
150
+ if (road)
151
+ resolved.address.road = road;
152
+ // The country tag isn't always in the hierarchy (US admin results omit it); backfill from the
153
+ // US-centric-data default so the address, display_name, and flag/currency/calling-code agree.
154
+ const countryName = result.hierarchy.find((h) => h.tag === "country")?.value ?? annotationCountryFallback;
155
+ const country = matchCountry(countryName);
156
+ if (country) {
157
+ if (!resolved.address.country)
158
+ resolved.address.country = country.canonical;
159
+ resolved.address.country_code = country.iso2.toLowerCase();
160
+ }
161
+ if (houseNumber || road) {
162
+ resolved.displayName =
163
+ joinNonEmpty(houseNumber, road, resolved.address.city, resolved.address.state, resolved.address.postcode, resolved.address.country) || resolved.displayName;
164
+ }
165
+ const out = toNominatimResult(resolved, { addressdetails: params.addressdetails });
166
+ out.annotations = toOpenCage(await annotate({
167
+ lat: result.lat,
168
+ lon: result.lon,
169
+ countryCode: country?.iso2,
170
+ placeName: result.locality ?? undefined,
171
+ }));
172
+ return [out].slice(0, params.limit);
173
+ },
174
+ async reverse(params) {
175
+ if (!reverseGeo)
176
+ return null;
177
+ const { hierarchy } = await reverseGeo.reverseGeocode(params.lat, params.lon);
178
+ if (hierarchy.length === 0)
179
+ return null;
180
+ const address = {};
181
+ for (const place of hierarchy) {
182
+ const key = PLACETYPE_TO_KEY[place.placetype];
183
+ if (key && !address[key])
184
+ address[key] = place.name;
185
+ }
186
+ const deepest = hierarchy[0];
187
+ if (deepest.country)
188
+ address.country_code = deepest.country.toLowerCase();
189
+ const resolved = {
190
+ lat: params.lat,
191
+ lon: params.lon,
192
+ address,
193
+ displayName: hierarchy.map((p) => p.name).join(", "),
194
+ placeId: deepest.id,
195
+ boundingbox: deepest.bbox
196
+ ? [
197
+ String(deepest.bbox.minLat),
198
+ String(deepest.bbox.maxLat),
199
+ String(deepest.bbox.minLon),
200
+ String(deepest.bbox.maxLon),
201
+ ]
202
+ : undefined,
203
+ };
204
+ const out = toNominatimResult(resolved, { addressdetails: params.addressdetails });
205
+ out.annotations = toOpenCage(await annotate({ lat: params.lat, lon: params.lon, countryCode: address.country_code }));
206
+ return out;
207
+ },
208
+ async status() {
209
+ return { status: 0, message: "OK" };
210
+ },
211
+ };
212
+ express()
213
+ .use(createNominatimRouter(engine))
214
+ .listen(port, host, () => {
215
+ console.error(`[@mailwoman/nominatim] listening on http://${host}:${port}`);
216
+ console.error(` wof: ${adminDbPath ?? "(none found — set MAILWOMAN_WOF_DB)"}`);
217
+ console.error(` endpoints: GET /search GET /reverse GET /lookup GET /status`);
218
+ });
219
+ }
220
+ const command = process.argv[2];
221
+ switch (command) {
222
+ case "serve":
223
+ await serve();
224
+ break;
225
+ default:
226
+ console.error("Usage: mailwoman-nominatim serve [--port 8080] [--host 0.0.0.0] [--data <path>]");
227
+ process.exit(command ? 1 : 0);
228
+ }
229
+ //# sourceMappingURL=cli.js.map
package/out/cli.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":"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"}
package/out/index.d.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/nominatim` — a Nominatim-compatible HTTP geocoding API over the Mailwoman engine.
7
+ *
8
+ * The package is intentionally engine-agnostic: {@link createNominatimRouter} takes a
9
+ * {@link NominatimEngine} (the thing that actually parses + resolves) and exposes it under the
10
+ * endpoint shapes + response format a Nominatim client expects. The CLI (`./cli.ts`) wires the
11
+ * real Mailwoman engine; tests can inject a fake. This keeps the compat surface isolated from the
12
+ * resolver wiring.
13
+ *
14
+ * Implementation is staged across the epic (#801): #804 the result formatter, #802 `/search`, #803
15
+ * `/reverse`, #805 `/lookup` + `/status`. Routes whose engine method is absent answer `501`.
16
+ */
17
+ import type { OpenCageAnnotations } from "@mailwoman/annotations";
18
+ import { Router } from "express";
19
+ /** Output serialization formats Nominatim supports. `jsonv2` is the modern default. */
20
+ export type NominatimFormat = "jsonv2" | "json" | "geojson";
21
+ /**
22
+ * The structured address breakdown returned under `address` when `addressdetails=1`. Keys mirror
23
+ * Nominatim's OSM-derived tag names; populated from Mailwoman's `ComponentTag` / resolved ancestor
24
+ * lineage (mapping owned by #804).
25
+ */
26
+ export interface NominatimAddressDetails {
27
+ house_number?: string;
28
+ road?: string;
29
+ neighbourhood?: string;
30
+ suburb?: string;
31
+ city?: string;
32
+ town?: string;
33
+ village?: string;
34
+ county?: string;
35
+ state?: string;
36
+ postcode?: string;
37
+ country?: string;
38
+ country_code?: string;
39
+ [key: string]: string | undefined;
40
+ }
41
+ /** A single Nominatim result object (the shape geopy and friends parse). */
42
+ export interface NominatimResult {
43
+ place_id: number | string;
44
+ licence: string;
45
+ osm_type?: string;
46
+ osm_id?: number | string;
47
+ lat: string;
48
+ lon: string;
49
+ display_name: string;
50
+ /** `[south, north, west, east]` as strings, per Nominatim. */
51
+ boundingbox?: [string, string, string, string];
52
+ class?: string;
53
+ type?: string;
54
+ importance?: number;
55
+ place_rank?: number;
56
+ address?: NominatimAddressDetails;
57
+ /** Present when `format=geojson` or `polygon_geojson=1`. */
58
+ geojson?: unknown;
59
+ /** OpenCage-style enrichment block (timezone, coordinate formats, …); attached by the engine. */
60
+ annotations?: OpenCageAnnotations;
61
+ }
62
+ /** Parsed `/search` parameters (free-text OR structured; never both). */
63
+ export interface NominatimSearchParams {
64
+ q?: string;
65
+ street?: string;
66
+ city?: string;
67
+ county?: string;
68
+ state?: string;
69
+ country?: string;
70
+ postalcode?: string;
71
+ countrycodes?: string[];
72
+ limit: number;
73
+ viewbox?: [number, number, number, number];
74
+ bounded?: boolean;
75
+ addressdetails?: boolean;
76
+ format: NominatimFormat;
77
+ acceptLanguage?: string;
78
+ }
79
+ /** Parsed `/reverse` parameters. */
80
+ export interface NominatimReverseParams {
81
+ lat: number;
82
+ lon: number;
83
+ zoom?: number;
84
+ addressdetails?: boolean;
85
+ format: NominatimFormat;
86
+ acceptLanguage?: string;
87
+ }
88
+ /** Parsed `/lookup` parameters. */
89
+ export interface NominatimLookupParams {
90
+ osmIds: string[];
91
+ addressdetails?: boolean;
92
+ format: NominatimFormat;
93
+ }
94
+ /** Nominatim `/status` payload. */
95
+ export interface NominatimStatus {
96
+ status: number;
97
+ message: string;
98
+ data_updated?: string;
99
+ }
100
+ /**
101
+ * The geocoding engine the router delegates to. Each method is optional; a route whose method is
102
+ * not provided answers `501 Not Implemented`. The real implementation (Mailwoman parse → resolve,
103
+ * plus `WofReverseGeocoder`) is wired by the CLI and fleshed out across #802–#805.
104
+ */
105
+ export interface NominatimEngine {
106
+ search?(params: NominatimSearchParams): Promise<NominatimResult[]>;
107
+ reverse?(params: NominatimReverseParams): Promise<NominatimResult | null>;
108
+ lookup?(params: NominatimLookupParams): Promise<NominatimResult[]>;
109
+ status?(): Promise<NominatimStatus>;
110
+ }
111
+ /**
112
+ * Build the Nominatim-compatible router around an injected {@link NominatimEngine}. Query-param
113
+ * parsing lives here; the result _formatting_ (jsonv2 vs geojson envelope, `address` projection) is
114
+ * #804 and currently passes the engine's results through verbatim.
115
+ */
116
+ export declare function createNominatimRouter(engine: NominatimEngine): Router;
117
+ /**
118
+ * A resolved address in a neutral shape, the input to {@link toNominatimResult}. The engine maps its
119
+ * native geocode/reverse result into this; the formatter renders it as a Nominatim result. This is
120
+ * the #804 mapping seam, kept dependency-free (no `@mailwoman/*` import) so it stays
121
+ * unit-testable.
122
+ */
123
+ export interface ResolvedAddress {
124
+ lat: number | null;
125
+ lon: number | null;
126
+ address: NominatimAddressDetails;
127
+ /** Pre-rendered display name; falls back to the address values joined by ", ". */
128
+ displayName?: string;
129
+ category?: string;
130
+ type?: string;
131
+ importance?: number;
132
+ placeRank?: number;
133
+ boundingbox?: [string, string, string, string];
134
+ /** A stable id from the resolver (WOF/GERS); a deterministic hash is used when absent. */
135
+ placeId?: string | number;
136
+ }
137
+ /** The attribution string emitted as `licence` (the data sources Mailwoman resolves over). */
138
+ export declare const MAILWOMAN_LICENCE = "Data \u00A9 Who's On First, Overture Maps, OpenAddresses, US Census TIGER";
139
+ /**
140
+ * Render a {@link ResolvedAddress} as a Nominatim result. `addressdetails` gates the `address`
141
+ * block, matching Nominatim. The `annotations` block is attached by the caller (empty until the
142
+ * annotations layer lands).
143
+ */
144
+ export declare function toNominatimResult(r: ResolvedAddress, opts?: {
145
+ addressdetails?: boolean;
146
+ }): NominatimResult;
147
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/out/index.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/nominatim` — a Nominatim-compatible HTTP geocoding API over the Mailwoman engine.
7
+ *
8
+ * The package is intentionally engine-agnostic: {@link createNominatimRouter} takes a
9
+ * {@link NominatimEngine} (the thing that actually parses + resolves) and exposes it under the
10
+ * endpoint shapes + response format a Nominatim client expects. The CLI (`./cli.ts`) wires the
11
+ * real Mailwoman engine; tests can inject a fake. This keeps the compat surface isolated from the
12
+ * resolver wiring.
13
+ *
14
+ * Implementation is staged across the epic (#801): #804 the result formatter, #802 `/search`, #803
15
+ * `/reverse`, #805 `/lookup` + `/status`. Routes whose engine method is absent answer `501`.
16
+ */
17
+ import { Router } from "express";
18
+ const DEFAULT_LIMIT = 10;
19
+ function parseFormat(raw) {
20
+ return raw === "geojson" || raw === "json" ? raw : "jsonv2";
21
+ }
22
+ function parseBool(raw) {
23
+ return raw === "1" || raw === "true";
24
+ }
25
+ function asString(raw) {
26
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
27
+ }
28
+ /**
29
+ * Build the Nominatim-compatible router around an injected {@link NominatimEngine}. Query-param
30
+ * parsing lives here; the result _formatting_ (jsonv2 vs geojson envelope, `address` projection) is
31
+ * #804 and currently passes the engine's results through verbatim.
32
+ */
33
+ export function createNominatimRouter(engine) {
34
+ const router = Router();
35
+ const search = async (req, res) => {
36
+ if (!engine.search) {
37
+ res.status(501).json({ error: "search not implemented (see #802)" });
38
+ return;
39
+ }
40
+ const q = req.query;
41
+ const params = {
42
+ q: asString(q["q"]),
43
+ street: asString(q["street"]),
44
+ city: asString(q["city"]),
45
+ county: asString(q["county"]),
46
+ state: asString(q["state"]),
47
+ country: asString(q["country"]),
48
+ postalcode: asString(q["postalcode"]),
49
+ countrycodes: asString(q["countrycodes"])?.split(","),
50
+ limit: Number(q["limit"] ?? DEFAULT_LIMIT) || DEFAULT_LIMIT,
51
+ bounded: parseBool(q["bounded"]),
52
+ addressdetails: parseBool(q["addressdetails"]),
53
+ format: parseFormat(q["format"]),
54
+ acceptLanguage: asString(q["accept-language"]),
55
+ };
56
+ res.json(await engine.search(params));
57
+ };
58
+ const reverse = async (req, res) => {
59
+ if (!engine.reverse) {
60
+ res.status(501).json({ error: "reverse not implemented (see #803)" });
61
+ return;
62
+ }
63
+ const q = req.query;
64
+ const lat = Number(q["lat"]);
65
+ const lon = Number(q["lon"]);
66
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
67
+ res.status(400).json({ error: "lat and lon are required" });
68
+ return;
69
+ }
70
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
71
+ res.status(400).json({ error: "lat must be in [-90, 90] and lon in [-180, 180]" });
72
+ return;
73
+ }
74
+ const params = {
75
+ lat,
76
+ lon,
77
+ zoom: q["zoom"] != null ? Number(q["zoom"]) : undefined,
78
+ addressdetails: parseBool(q["addressdetails"]),
79
+ format: parseFormat(q["format"]),
80
+ acceptLanguage: asString(q["accept-language"]),
81
+ };
82
+ res.json(await engine.reverse(params));
83
+ };
84
+ const lookup = async (req, res) => {
85
+ if (!engine.lookup) {
86
+ res.status(501).json({ error: "lookup not implemented (see #805)" });
87
+ return;
88
+ }
89
+ const params = {
90
+ osmIds: asString(req.query["osm_ids"])?.split(",") ?? [],
91
+ addressdetails: parseBool(req.query["addressdetails"]),
92
+ format: parseFormat(req.query["format"]),
93
+ };
94
+ res.json(await engine.lookup(params));
95
+ };
96
+ const status = async (_req, res) => {
97
+ if (!engine.status) {
98
+ res.json({ status: 0, message: "OK" });
99
+ return;
100
+ }
101
+ res.json(await engine.status());
102
+ };
103
+ // Safety net: a malformed query (whitespace-only, absurdly long, control chars) or an engine fault
104
+ // must never crash the process into a stack-trace 500. Wrap every handler so an unexpected throw
105
+ // becomes a clean JSON error.
106
+ const safe = (fn) => async (req, res, next) => {
107
+ try {
108
+ await fn(req, res, next);
109
+ }
110
+ catch {
111
+ if (!res.headersSent)
112
+ res.status(500).json({ error: "internal error" });
113
+ }
114
+ };
115
+ router.get("/search", safe(search));
116
+ router.get("/reverse", safe(reverse));
117
+ router.get("/lookup", safe(lookup));
118
+ router.get("/status", safe(status));
119
+ return router;
120
+ }
121
+ /** The attribution string emitted as `licence` (the data sources Mailwoman resolves over). */
122
+ export const MAILWOMAN_LICENCE = "Data © Who's On First, Overture Maps, OpenAddresses, US Census TIGER";
123
+ function stableId(seed) {
124
+ let h = 5381;
125
+ for (let i = 0; i < seed.length; i++)
126
+ h = (h * 33) ^ seed.charCodeAt(i);
127
+ return h >>> 0;
128
+ }
129
+ /**
130
+ * Render a {@link ResolvedAddress} as a Nominatim result. `addressdetails` gates the `address`
131
+ * block, matching Nominatim. The `annotations` block is attached by the caller (empty until the
132
+ * annotations layer lands).
133
+ */
134
+ export function toNominatimResult(r, opts = {}) {
135
+ const displayName = r.displayName ?? Object.values(r.address).filter(Boolean).join(", ");
136
+ const lat = r.lat != null ? String(r.lat) : "";
137
+ const lon = r.lon != null ? String(r.lon) : "";
138
+ const result = {
139
+ place_id: r.placeId ?? stableId(`${lat},${lon},${displayName}`),
140
+ licence: MAILWOMAN_LICENCE,
141
+ lat,
142
+ lon,
143
+ display_name: displayName,
144
+ };
145
+ if (r.category != null)
146
+ result.class = r.category;
147
+ if (r.type != null)
148
+ result.type = r.type;
149
+ if (r.importance != null)
150
+ result.importance = r.importance;
151
+ if (r.placeRank != null)
152
+ result.place_rank = r.placeRank;
153
+ if (r.boundingbox)
154
+ result.boundingbox = r.boundingbox;
155
+ if (opts.addressdetails)
156
+ result.address = r.address;
157
+ return result;
158
+ }
159
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@mailwoman/nominatim",
3
+ "version": "4.15.1",
4
+ "description": "Nominatim drop-in — a Nominatim-compatible HTTP geocoding API (search / reverse / lookup / status) over the Mailwoman engine. Run it with `npx @mailwoman/nominatim serve`.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sister-software/mailwoman.git",
9
+ "directory": "nominatim"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": "./out/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@mailwoman/annotations": "4.15.1",
18
+ "@mailwoman/codex": "4.15.0",
19
+ "@mailwoman/neural": "4.15.0",
20
+ "@mailwoman/nuts-lookup": "4.15.1",
21
+ "@mailwoman/resolver": "4.15.0",
22
+ "@mailwoman/resolver-wof-sqlite": "4.15.0",
23
+ "@mailwoman/spatial": "4.15.0",
24
+ "@mailwoman/timezone-lookup": "4.15.1",
25
+ "@mailwoman/un-locode-lookup": "4.15.1",
26
+ "express": "^5.2.1",
27
+ "mailwoman": "4.15.0"
28
+ },
29
+ "files": [
30
+ "out/**/*.js",
31
+ "out/**/*.js.map",
32
+ "out/**/*.d.ts",
33
+ "out/**/*.d.ts.map",
34
+ "README.md"
35
+ ],
36
+ "bin": "./out/cli.js",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }