@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 +40 -0
- package/out/cli.d.ts +17 -0
- package/out/cli.d.ts.map +1 -0
- package/out/cli.js +110 -0
- package/out/cli.js.map +1 -0
- package/out/index.d.ts +92 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +103 -0
- package/out/index.js.map +1 -0
- package/package.json +34 -0
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
|
package/out/cli.d.ts.map
ADDED
|
@@ -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
|
package/out/index.js.map
ADDED
|
@@ -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
|
+
}
|