@mailwoman/photon 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,40 @@
1
+ # @mailwoman/photon
2
+
3
+ A **Photon-compatible autocomplete geocoding API** over the [Mailwoman](https://mailwoman.sister.software) engine — search-as-you-type, returning GeoJSON `FeatureCollection`s. Where [`@mailwoman/nominatim`](../nominatim) is structured lookup, this is the type-ahead front door. No Elasticsearch.
4
+
5
+ ```bash
6
+ npx @mailwoman/photon serve --port 2322 --data <gazetteer-or-bundle>
7
+ ```
8
+
9
+ ```bash
10
+ curl "http://localhost:2322/api?q=1600 penn&limit=5&lat=38.9&lon=-77"
11
+ curl "http://localhost:2322/reverse?lat=38.8977&lon=-77.0365"
12
+ ```
13
+
14
+ ## Endpoints
15
+
16
+ | Endpoint | Photon contract |
17
+ | ---------- | -------------------------------------------------- |
18
+ | `/api` | forward / autocomplete → GeoJSON FeatureCollection |
19
+ | `/reverse` | `lat`/`lon` → GeoJSON FeatureCollection |
20
+
21
+ Backed by Mailwoman's FST autocomplete tier (#190/#587) + parse → resolve.
22
+
23
+ ## Library use
24
+
25
+ ```ts
26
+ import express from "express"
27
+ import { createPhotonRouter, type PhotonEngine } from "@mailwoman/photon"
28
+
29
+ const engine: PhotonEngine = {
30
+ /* search, reverse — backed by your Mailwoman pipeline */
31
+ }
32
+ express().use(createPhotonRouter(engine)).listen(2322)
33
+ ```
34
+
35
+ ## Status
36
+
37
+ Shipped. `/api` and `/reverse` resolve over the live engine and return Photon GeoJSON. `/api` runs the
38
+ query through the geocoder today; the dedicated prefix-first FST front (the last bit of Photon's tuned
39
+ type-ahead ordering) is a refinement. Pairs with [`@mailwoman/nominatim`](../nominatim) for the
40
+ structured-lookup shape and the OpenCage-style annotations block.
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-photon` — boot a Photon-compatible autocomplete endpoint via the `serve` command.
8
+ * Usage
9
+ *
10
+ * - Examples live in the package README.
11
+ *
12
+ * Wires the real engine: `/api` over `geocodeAddress` (parse → resolve), `/reverse` over
13
+ * `WofReverseGeocoder`, projecting results into Photon's GeoJSON FeatureCollection. The FST
14
+ * autocomplete tier is the eventual front for `/api`; geocode resolution is the MVP path.
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,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman-photon` — boot a Photon-compatible autocomplete endpoint via the `serve` command.
8
+ * Usage
9
+ *
10
+ * - Examples live in the package README.
11
+ *
12
+ * Wires the real engine: `/api` over `geocodeAddress` (parse → resolve), `/reverse` over
13
+ * `WofReverseGeocoder`, projecting results into Photon's GeoJSON FeatureCollection. The FST
14
+ * autocomplete tier is the eventual front for `/api`; geocode resolution is the MVP path.
15
+ */
16
+ import { NeuralAddressClassifier } from "@mailwoman/neural";
17
+ import { createWofResolver } from "@mailwoman/resolver";
18
+ import { geocodeAddress, ShardProvider } from "mailwoman/geocode-core";
19
+ import { createResolverBackend, mailwomanDataRoot, wofShardPaths } from "mailwoman/resolver-backend";
20
+ import { existsSync } from "node:fs";
21
+ import { parseArgs } from "node:util";
22
+ import { createPhotonRouter, photonCollection, photonFeature, } from "./index.js";
23
+ /** WOF placetype → Photon property key. */
24
+ const PLACETYPE_TO_KEY = {
25
+ street: "street",
26
+ locality: "city",
27
+ localadmin: "city",
28
+ county: "county",
29
+ region: "state",
30
+ country: "country",
31
+ };
32
+ /** A real address fits comfortably; longer is malformed input (and would exceed the model's window). */
33
+ const MAX_QUERY_LEN = 512;
34
+ async function serve() {
35
+ const { values } = parseArgs({
36
+ options: {
37
+ port: { type: "string", default: "2322" },
38
+ host: { type: "string", default: "0.0.0.0" },
39
+ data: { type: "string" },
40
+ },
41
+ allowPositionals: true,
42
+ });
43
+ const port = Number(values.port) || 2322;
44
+ const host = values.host ?? "0.0.0.0";
45
+ const resolverMod = await import("@mailwoman/resolver-wof-sqlite");
46
+ const wofPaths = wofShardPaths().filter(existsSync);
47
+ const adminDbPath = wofPaths[0];
48
+ const classifier = await NeuralAddressClassifier.loadFromWeights({ locale: "en-US" });
49
+ const backend = createResolverBackend(resolverMod, { wofPaths });
50
+ const resolver = createWofResolver(backend);
51
+ const shards = new ShardProvider(resolverMod, mailwomanDataRoot());
52
+ const reverseGeo = adminDbPath ? new resolverMod.WofReverseGeocoder({ adminDbPath }) : undefined;
53
+ const engine = {
54
+ async search(params) {
55
+ // Empty/whitespace → no query; absurdly long → not an address (and would blow the model's input).
56
+ const query = params.q?.trim();
57
+ if (!query || query.length > MAX_QUERY_LEN)
58
+ return photonCollection([]);
59
+ // No country constraint: the default-on #244 placer routes the query's country (Berlin→DE,
60
+ // Boston→US). Forcing "US" here is a HARD override (geocode-core.ts:102) that resolved every
61
+ // non-US query to its US namesake — wrong for a global autocomplete front.
62
+ const result = await geocodeAddress(query, { classifier, resolver, shards: shards.for });
63
+ if (result.lat == null || result.lon == null)
64
+ return photonCollection([]);
65
+ const properties = {
66
+ name: result.locality ?? result.region ?? undefined,
67
+ city: result.locality ?? undefined,
68
+ state: result.region ?? undefined,
69
+ postcode: result.postcode ?? undefined,
70
+ };
71
+ for (const h of result.hierarchy)
72
+ if (h.tag === "country")
73
+ properties.country = h.value;
74
+ return photonCollection([photonFeature(result.lon, result.lat, properties)]);
75
+ },
76
+ async reverse(params) {
77
+ if (!reverseGeo)
78
+ return photonCollection([]);
79
+ const { hierarchy } = await reverseGeo.reverseGeocode(params.lat, params.lon);
80
+ if (hierarchy.length === 0)
81
+ return photonCollection([]);
82
+ const deepest = hierarchy[0];
83
+ const properties = { name: deepest.name, countrycode: deepest.country?.toLowerCase() };
84
+ for (const place of hierarchy) {
85
+ const key = PLACETYPE_TO_KEY[place.placetype];
86
+ if (key && properties[key] == null)
87
+ properties[key] = place.name;
88
+ }
89
+ return photonCollection([photonFeature(deepest.lon, deepest.lat, properties)]);
90
+ },
91
+ };
92
+ const express = (await import("express")).default;
93
+ express()
94
+ .use(createPhotonRouter(engine))
95
+ .listen(port, host, () => {
96
+ console.error(`[@mailwoman/photon] listening on http://${host}:${port}`);
97
+ console.error(` wof: ${adminDbPath ?? "(none found — set MAILWOMAN_WOF_DB)"}`);
98
+ console.error(` endpoints: GET /api GET /reverse`);
99
+ });
100
+ }
101
+ const command = process.argv[2];
102
+ switch (command) {
103
+ case "serve":
104
+ await serve();
105
+ break;
106
+ default:
107
+ console.error("Usage: mailwoman-photon serve [--port 2322] [--host 0.0.0.0] [--data <path>]");
108
+ process.exit(command ? 1 : 0);
109
+ }
110
+ //# 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,92 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/photon` — a Photon-compatible autocomplete / type-ahead geocoding API over the
7
+ * Mailwoman engine. Where `@mailwoman/nominatim` is structured lookup, Photon is
8
+ * search-as-you-type: a GeoJSON `FeatureCollection` per query, biased by location, ranked for
9
+ * prefixes. It maps onto Mailwoman's shipped FST autocomplete tier (#190/#587) + parse →
10
+ * resolve.
11
+ *
12
+ * Like its sibling, the package is engine-agnostic: {@link createPhotonRouter} takes a
13
+ * {@link PhotonEngine}; the CLI wires the real engine. Implementation is staged on the epic (#801
14
+ * / the Photon child); routes whose engine method is absent answer `501`.
15
+ */
16
+ import { Router } from "express";
17
+ /**
18
+ * Photon feature properties — OSM-derived tag names, populated from Mailwoman's `ComponentTag` /
19
+ * resolved place. `extent` is `[minLon, maxLat, maxLon, minLat]` per Photon's convention.
20
+ */
21
+ export interface PhotonProperties {
22
+ osm_id?: number | string;
23
+ osm_type?: string;
24
+ osm_key?: string;
25
+ osm_value?: string;
26
+ type?: string;
27
+ name?: string;
28
+ housenumber?: string;
29
+ street?: string;
30
+ postcode?: string;
31
+ city?: string;
32
+ district?: string;
33
+ county?: string;
34
+ state?: string;
35
+ country?: string;
36
+ countrycode?: string;
37
+ extent?: [number, number, number, number];
38
+ [key: string]: unknown;
39
+ }
40
+ /** A Photon result feature: a GeoJSON Point with {@link PhotonProperties}. */
41
+ export interface PhotonFeature {
42
+ type: "Feature";
43
+ geometry: {
44
+ type: "Point";
45
+ coordinates: [number, number];
46
+ };
47
+ properties: PhotonProperties;
48
+ }
49
+ /** The Photon response envelope — a GeoJSON FeatureCollection. */
50
+ export interface PhotonFeatureCollection {
51
+ type: "FeatureCollection";
52
+ features: PhotonFeature[];
53
+ }
54
+ /** Parsed `/api` (forward / autocomplete) parameters. */
55
+ export interface PhotonSearchParams {
56
+ q: string;
57
+ limit: number;
58
+ lang?: string;
59
+ /** Location bias. */
60
+ lat?: number;
61
+ lon?: number;
62
+ bbox?: [number, number, number, number];
63
+ osmTag?: string[];
64
+ layer?: string[];
65
+ }
66
+ /** Parsed `/reverse` parameters. */
67
+ export interface PhotonReverseParams {
68
+ lat: number;
69
+ lon: number;
70
+ limit: number;
71
+ lang?: string;
72
+ radius?: number;
73
+ }
74
+ /**
75
+ * The engine the router delegates to. Each method is optional; a missing one answers `501`. The
76
+ * real implementation backs `/api` with the FST autocomplete tier + parse→resolve, and `/reverse`
77
+ * with the `WofReverseGeocoder`.
78
+ */
79
+ export interface PhotonEngine {
80
+ search?(params: PhotonSearchParams): Promise<PhotonFeatureCollection>;
81
+ reverse?(params: PhotonReverseParams): Promise<PhotonFeatureCollection>;
82
+ }
83
+ /**
84
+ * Build the Photon-compatible router around an injected {@link PhotonEngine}. Param parsing lives
85
+ * here; the feature _projection_ (resolved place → {@link PhotonProperties}) is the staged work.
86
+ */
87
+ export declare function createPhotonRouter(engine: PhotonEngine): Router;
88
+ /** Build a Photon `Feature` from a coordinate + properties. */
89
+ export declare function photonFeature(lon: number, lat: number, properties: PhotonProperties): PhotonFeature;
90
+ /** Wrap features in a Photon `FeatureCollection`. */
91
+ export declare function photonCollection(features: PhotonFeature[]): PhotonFeatureCollection;
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAuB,MAAM,EAAE,MAAM,SAAS,CAAA;AAErD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAChC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACzC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,SAAS,CAAA;IACf,QAAQ,EAAE;QACT,IAAI,EAAE,OAAO,CAAA;QACb,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAC7B,CAAA;IACD,UAAU,EAAE,gBAAgB,CAAA;CAC5B;AAED,kEAAkE;AAClE,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,mBAAmB,CAAA;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAA;CACzB;AAED,yDAAyD;AACzD,MAAM,WAAW,kBAAkB;IAClC,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qBAAqB;IACrB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,oCAAoC;AACpC,MAAM,WAAW,mBAAmB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B,MAAM,CAAC,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;IACrE,OAAO,CAAC,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAA;CACvE;AAgBD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAmE/D;AAED,+DAA+D;AAC/D,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,GAAG,aAAa,CAEnG;AAED,qDAAqD;AACrD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,uBAAuB,CAEnF"}
package/out/index.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `@mailwoman/photon` — a Photon-compatible autocomplete / type-ahead geocoding API over the
7
+ * Mailwoman engine. Where `@mailwoman/nominatim` is structured lookup, Photon is
8
+ * search-as-you-type: a GeoJSON `FeatureCollection` per query, biased by location, ranked for
9
+ * prefixes. It maps onto Mailwoman's shipped FST autocomplete tier (#190/#587) + parse →
10
+ * resolve.
11
+ *
12
+ * Like its sibling, the package is engine-agnostic: {@link createPhotonRouter} takes a
13
+ * {@link PhotonEngine}; the CLI wires the real engine. Implementation is staged on the epic (#801
14
+ * / the Photon child); routes whose engine method is absent answer `501`.
15
+ */
16
+ import { Router } from "express";
17
+ const DEFAULT_LIMIT = 15;
18
+ function asString(raw) {
19
+ return typeof raw === "string" && raw.length > 0 ? raw : undefined;
20
+ }
21
+ function asStringArray(raw) {
22
+ if (Array.isArray(raw))
23
+ return raw.filter((v) => typeof v === "string");
24
+ const s = asString(raw);
25
+ return s ? [s] : undefined;
26
+ }
27
+ const EMPTY = { type: "FeatureCollection", features: [] };
28
+ /**
29
+ * Build the Photon-compatible router around an injected {@link PhotonEngine}. Param parsing lives
30
+ * here; the feature _projection_ (resolved place → {@link PhotonProperties}) is the staged work.
31
+ */
32
+ export function createPhotonRouter(engine) {
33
+ const router = Router();
34
+ const search = async (req, res) => {
35
+ if (!engine.search) {
36
+ res.status(501).json({ ...EMPTY, message: "search not implemented" });
37
+ return;
38
+ }
39
+ const q = req.query;
40
+ const query = asString(q["q"]);
41
+ if (!query) {
42
+ res.status(400).json({ ...EMPTY, message: "q is required" });
43
+ return;
44
+ }
45
+ const params = {
46
+ q: query,
47
+ limit: Number(q["limit"] ?? DEFAULT_LIMIT) || DEFAULT_LIMIT,
48
+ lang: asString(q["lang"]),
49
+ lat: q["lat"] != null ? Number(q["lat"]) : undefined,
50
+ lon: q["lon"] != null ? Number(q["lon"]) : undefined,
51
+ osmTag: asStringArray(q["osm_tag"]),
52
+ layer: asStringArray(q["layer"]),
53
+ };
54
+ res.json(await engine.search(params));
55
+ };
56
+ const reverse = async (req, res) => {
57
+ if (!engine.reverse) {
58
+ res.status(501).json({ ...EMPTY, message: "reverse not implemented" });
59
+ return;
60
+ }
61
+ const q = req.query;
62
+ const lat = Number(q["lat"]);
63
+ const lon = Number(q["lon"]);
64
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
65
+ res.status(400).json({ ...EMPTY, message: "lat and lon are required" });
66
+ return;
67
+ }
68
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
69
+ res.status(400).json({ ...EMPTY, message: "lat must be in [-90, 90] and lon in [-180, 180]" });
70
+ return;
71
+ }
72
+ const params = {
73
+ lat,
74
+ lon,
75
+ limit: Number(q["limit"] ?? DEFAULT_LIMIT) || DEFAULT_LIMIT,
76
+ lang: asString(q["lang"]),
77
+ radius: q["radius"] != null ? Number(q["radius"]) : undefined,
78
+ };
79
+ res.json(await engine.reverse(params));
80
+ };
81
+ // Safety net: malformed input or an engine fault returns an empty FeatureCollection, never a crash.
82
+ const safe = (fn) => async (req, res, next) => {
83
+ try {
84
+ await fn(req, res, next);
85
+ }
86
+ catch {
87
+ if (!res.headersSent)
88
+ res.status(500).json({ ...EMPTY, message: "internal error" });
89
+ }
90
+ };
91
+ router.get("/api", safe(search));
92
+ router.get("/reverse", safe(reverse));
93
+ return router;
94
+ }
95
+ /** Build a Photon `Feature` from a coordinate + properties. */
96
+ export function photonFeature(lon, lat, properties) {
97
+ return { type: "Feature", geometry: { type: "Point", coordinates: [lon, lat] }, properties };
98
+ }
99
+ /** Wrap features in a Photon `FeatureCollection`. */
100
+ export function photonCollection(features) {
101
+ return { type: "FeatureCollection", features };
102
+ }
103
+ //# 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,34 @@
1
+ {
2
+ "name": "@mailwoman/photon",
3
+ "version": "4.15.1",
4
+ "description": "Photon drop-in — a Photon-compatible autocomplete/type-ahead geocoding API (GeoJSON FeatureCollection over /api + /reverse) on the Mailwoman engine. Run it with `npx @mailwoman/photon serve`.",
5
+ "license": "AGPL-3.0-only",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sister-software/mailwoman.git",
9
+ "directory": "photon"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ "./package.json": "./package.json",
14
+ ".": "./out/index.js"
15
+ },
16
+ "dependencies": {
17
+ "@mailwoman/neural": "4.15.0",
18
+ "@mailwoman/resolver": "4.15.0",
19
+ "@mailwoman/resolver-wof-sqlite": "4.15.0",
20
+ "express": "^5.2.1",
21
+ "mailwoman": "4.15.0"
22
+ },
23
+ "files": [
24
+ "out/**/*.js",
25
+ "out/**/*.js.map",
26
+ "out/**/*.d.ts",
27
+ "out/**/*.d.ts.map",
28
+ "README.md"
29
+ ],
30
+ "bin": "./out/cli.js",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }