@unmap/geocoding 0.27.0 → 0.28.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.
@@ -0,0 +1,65 @@
1
+ import { OUTSIDE_COVERAGE_CODE } from './canada-service-area.js';
2
+
3
+ /**
4
+ * Imported as `@unmap/geocoding/along`, not from the package root: `checkAlongLine` tests every
5
+ * vertex against the Canada service-area polygon, and re-exporting it from the root put that
6
+ * polygon in the SDK's browser bundle (+38 KB gzip, over its ceiling) for a check only the gateway
7
+ * and the container run.
8
+ *
9
+ * The contract for `POST /places/along`: places within a few metres of a route, in the order the
10
+ * route reaches them.
11
+ *
12
+ * One module, three readers. The gateway validates the public request with it, the geocode
13
+ * container re-checks the line it is handed, and the SDK documents the same numbers. Bounds that
14
+ * live as literals on two sides drift (docs/decisions/2026-09-20-one-source-for-request-contracts.md
15
+ * found three such cases), so these exist once.
16
+ */
17
+ declare const PLACES_ALONG: {
18
+ /** Vertices in the route line. A 100 km drive from `/route` is well under this. */
19
+ readonly maxVertices: 2000;
20
+ /**
21
+ * Total line length. Walks, rides and "coffee on the way" across a city drive; a provincial
22
+ * highway trip is a different question (stops near exits), not a longer corridor.
23
+ */
24
+ readonly maxLengthMeters: 100000;
25
+ /** Metres either side of the line. Out-of-range values are clamped, as `/places/nearby` clamps. */
26
+ readonly radius: {
27
+ readonly min: 10;
28
+ readonly max: 500;
29
+ readonly default: 50;
30
+ };
31
+ /** Results returned. Out-of-range values are a 400, as `/places/nearby`'s `limit` is. */
32
+ readonly limit: {
33
+ readonly min: 1;
34
+ readonly max: 200;
35
+ readonly default: 50;
36
+ };
37
+ };
38
+ type AlongLine = [number, number][];
39
+ type AlongLineCheck = {
40
+ ok: true;
41
+ line: AlongLine;
42
+ lengthMeters: number;
43
+ } | {
44
+ ok: false;
45
+ status: 400 | 422;
46
+ code: "bad_request" | typeof OUTSIDE_COVERAGE_CODE;
47
+ error: string;
48
+ };
49
+ /** Great-circle length of a `[lng, lat]` line, in metres. */
50
+ declare function lineLengthMeters(line: AlongLine): number;
51
+ /**
52
+ * Validate a route line: a GeoJSON LineString, which is exactly what `/route` returns as
53
+ * `geometry`, so a caller passes one response straight into the next request.
54
+ *
55
+ * Only a LineString. A Feature or a MultiLineString would each be a second spelling of the same
56
+ * input, and the error names the field to pass instead. A third coordinate (elevation) is
57
+ * accepted and dropped, because GeoJSON allows it and `/elevation`-enriched lines carry it.
58
+ */
59
+ declare function checkAlongLine(value: unknown): AlongLineCheck;
60
+ /** `radius`, clamped. Undefined means the default; anything that is not a finite number is null. */
61
+ declare function alongRadius(raw: unknown): number | null;
62
+ /** `limit`, checked. Undefined means the default; out of range or not an integer is null. */
63
+ declare function alongLimit(raw: unknown): number | null;
64
+
65
+ export { type AlongLine, type AlongLineCheck, PLACES_ALONG, alongLimit, alongRadius, checkAlongLine, lineLengthMeters };
package/dist/along.js ADDED
@@ -0,0 +1,86 @@
1
+ import {
2
+ OUTSIDE_COVERAGE_CODE,
3
+ OUTSIDE_COVERAGE_MESSAGE,
4
+ WGS84_RANGE_MESSAGE,
5
+ inCanadianServiceArea,
6
+ inWgs84
7
+ } from "./chunk-EACPOBHF.js";
8
+
9
+ // src/along.ts
10
+ var PLACES_ALONG = {
11
+ /** Vertices in the route line. A 100 km drive from `/route` is well under this. */
12
+ maxVertices: 2e3,
13
+ /**
14
+ * Total line length. Walks, rides and "coffee on the way" across a city drive; a provincial
15
+ * highway trip is a different question (stops near exits), not a longer corridor.
16
+ */
17
+ maxLengthMeters: 1e5,
18
+ /** Metres either side of the line. Out-of-range values are clamped, as `/places/nearby` clamps. */
19
+ radius: { min: 10, max: 500, default: 50 },
20
+ /** Results returned. Out-of-range values are a 400, as `/places/nearby`'s `limit` is. */
21
+ limit: { min: 1, max: 200, default: 50 }
22
+ };
23
+ var R = 63710088e-1;
24
+ function lineLengthMeters(line) {
25
+ let total = 0;
26
+ for (let i = 1; i < line.length; i++) {
27
+ const [lng1, lat1] = line[i - 1];
28
+ const [lng2, lat2] = line[i];
29
+ const p1 = lat1 * Math.PI / 180;
30
+ const p2 = lat2 * Math.PI / 180;
31
+ const dp = p2 - p1;
32
+ const dl = (lng2 - lng1) * Math.PI / 180;
33
+ const a = Math.sin(dp / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2;
34
+ total += 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));
35
+ }
36
+ return total;
37
+ }
38
+ function bad(error) {
39
+ return { ok: false, status: 400, code: "bad_request", error };
40
+ }
41
+ function checkAlongLine(value) {
42
+ const shape = "route must be a GeoJSON LineString, e.g. the geometry /route returns";
43
+ if (typeof value !== "object" || value === null) return bad(shape);
44
+ const g = value;
45
+ if (g.type !== "LineString" || !Array.isArray(g.coordinates)) return bad(shape);
46
+ if (g.coordinates.length < 2) return bad("route needs at least 2 coordinates");
47
+ if (g.coordinates.length > PLACES_ALONG.maxVertices) {
48
+ return bad(`route has ${g.coordinates.length} coordinates; at most ${PLACES_ALONG.maxVertices}. Simplify the line first.`);
49
+ }
50
+ const line = [];
51
+ for (const c of g.coordinates) {
52
+ if (!Array.isArray(c) || c.length < 2 || c.length > 3 || !c.every((n) => typeof n === "number" && Number.isFinite(n))) {
53
+ return bad("every route coordinate must be [lng, lat] with finite numbers");
54
+ }
55
+ const [lng, lat] = c;
56
+ if (!inWgs84(lng, lat)) return { ok: false, status: 400, code: "bad_request", error: WGS84_RANGE_MESSAGE };
57
+ line.push([lng, lat]);
58
+ }
59
+ const lengthMeters = lineLengthMeters(line);
60
+ if (lengthMeters > PLACES_ALONG.maxLengthMeters) {
61
+ return bad(`route is ${Math.round(lengthMeters / 1e3)} km; at most ${PLACES_ALONG.maxLengthMeters / 1e3} km`);
62
+ }
63
+ if (!line.every(([lng, lat]) => inCanadianServiceArea(lng, lat))) {
64
+ return { ok: false, status: 422, code: OUTSIDE_COVERAGE_CODE, error: OUTSIDE_COVERAGE_MESSAGE };
65
+ }
66
+ return { ok: true, line, lengthMeters };
67
+ }
68
+ function alongRadius(raw) {
69
+ if (raw === void 0) return PLACES_ALONG.radius.default;
70
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
71
+ return Math.min(PLACES_ALONG.radius.max, Math.max(PLACES_ALONG.radius.min, raw));
72
+ }
73
+ function alongLimit(raw) {
74
+ if (raw === void 0) return PLACES_ALONG.limit.default;
75
+ if (typeof raw !== "number" || !Number.isInteger(raw)) return null;
76
+ if (raw < PLACES_ALONG.limit.min || raw > PLACES_ALONG.limit.max) return null;
77
+ return raw;
78
+ }
79
+ export {
80
+ PLACES_ALONG,
81
+ alongLimit,
82
+ alongRadius,
83
+ checkAlongLine,
84
+ lineLengthMeters
85
+ };
86
+ //# sourceMappingURL=along.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/along.ts"],"sourcesContent":["import {\n inCanadianServiceArea,\n inWgs84,\n OUTSIDE_COVERAGE_CODE,\n OUTSIDE_COVERAGE_MESSAGE,\n WGS84_RANGE_MESSAGE,\n} from \"./canada-service-area\";\n\n/**\n * Imported as `@unmap/geocoding/along`, not from the package root: `checkAlongLine` tests every\n * vertex against the Canada service-area polygon, and re-exporting it from the root put that\n * polygon in the SDK's browser bundle (+38 KB gzip, over its ceiling) for a check only the gateway\n * and the container run.\n *\n * The contract for `POST /places/along`: places within a few metres of a route, in the order the\n * route reaches them.\n *\n * One module, three readers. The gateway validates the public request with it, the geocode\n * container re-checks the line it is handed, and the SDK documents the same numbers. Bounds that\n * live as literals on two sides drift (docs/decisions/2026-09-20-one-source-for-request-contracts.md\n * found three such cases), so these exist once.\n */\nexport const PLACES_ALONG = {\n /** Vertices in the route line. A 100 km drive from `/route` is well under this. */\n maxVertices: 2000,\n /**\n * Total line length. Walks, rides and \"coffee on the way\" across a city drive; a provincial\n * highway trip is a different question (stops near exits), not a longer corridor.\n */\n maxLengthMeters: 100_000,\n /** Metres either side of the line. Out-of-range values are clamped, as `/places/nearby` clamps. */\n radius: { min: 10, max: 500, default: 50 },\n /** Results returned. Out-of-range values are a 400, as `/places/nearby`'s `limit` is. */\n limit: { min: 1, max: 200, default: 50 },\n} as const;\n\nexport type AlongLine = [number, number][];\n\nexport type AlongLineCheck =\n | { ok: true; line: AlongLine; lengthMeters: number }\n | { ok: false; status: 400 | 422; code: \"bad_request\" | typeof OUTSIDE_COVERAGE_CODE; error: string };\n\nconst R = 6_371_008.8;\n\n/** Great-circle length of a `[lng, lat]` line, in metres. */\nexport function lineLengthMeters(line: AlongLine): number {\n let total = 0;\n for (let i = 1; i < line.length; i++) {\n const [lng1, lat1] = line[i - 1]!;\n const [lng2, lat2] = line[i]!;\n const p1 = (lat1 * Math.PI) / 180;\n const p2 = (lat2 * Math.PI) / 180;\n const dp = p2 - p1;\n const dl = ((lng2 - lng1) * Math.PI) / 180;\n const a = Math.sin(dp / 2) ** 2 + Math.cos(p1) * Math.cos(p2) * Math.sin(dl / 2) ** 2;\n total += 2 * R * Math.asin(Math.min(1, Math.sqrt(a)));\n }\n return total;\n}\n\nfunction bad(error: string): AlongLineCheck {\n return { ok: false, status: 400, code: \"bad_request\", error };\n}\n\n/**\n * Validate a route line: a GeoJSON LineString, which is exactly what `/route` returns as\n * `geometry`, so a caller passes one response straight into the next request.\n *\n * Only a LineString. A Feature or a MultiLineString would each be a second spelling of the same\n * input, and the error names the field to pass instead. A third coordinate (elevation) is\n * accepted and dropped, because GeoJSON allows it and `/elevation`-enriched lines carry it.\n */\nexport function checkAlongLine(value: unknown): AlongLineCheck {\n const shape = \"route must be a GeoJSON LineString, e.g. the geometry /route returns\";\n if (typeof value !== \"object\" || value === null) return bad(shape);\n const g = value as { type?: unknown; coordinates?: unknown };\n if (g.type !== \"LineString\" || !Array.isArray(g.coordinates)) return bad(shape);\n if (g.coordinates.length < 2) return bad(\"route needs at least 2 coordinates\");\n if (g.coordinates.length > PLACES_ALONG.maxVertices) {\n return bad(`route has ${g.coordinates.length} coordinates; at most ${PLACES_ALONG.maxVertices}. Simplify the line first.`);\n }\n const line: AlongLine = [];\n for (const c of g.coordinates) {\n if (!Array.isArray(c) || c.length < 2 || c.length > 3 || !c.every((n) => typeof n === \"number\" && Number.isFinite(n))) {\n return bad(\"every route coordinate must be [lng, lat] with finite numbers\");\n }\n const [lng, lat] = c as [number, number];\n if (!inWgs84(lng, lat)) return { ok: false, status: 400, code: \"bad_request\", error: WGS84_RANGE_MESSAGE };\n line.push([lng, lat]);\n }\n const lengthMeters = lineLengthMeters(line);\n if (lengthMeters > PLACES_ALONG.maxLengthMeters) {\n return bad(`route is ${Math.round(lengthMeters / 1000)} km; at most ${PLACES_ALONG.maxLengthMeters / 1000} km`);\n }\n // Every vertex, not just the ends: a line from Windsor to Montréal through Michigan and New\n // York has Canadian endpoints and an American middle the corpus cannot answer for.\n if (!line.every(([lng, lat]) => inCanadianServiceArea(lng, lat))) {\n return { ok: false, status: 422, code: OUTSIDE_COVERAGE_CODE, error: OUTSIDE_COVERAGE_MESSAGE };\n }\n return { ok: true, line, lengthMeters };\n}\n\n/** `radius`, clamped. Undefined means the default; anything that is not a finite number is null. */\nexport function alongRadius(raw: unknown): number | null {\n if (raw === undefined) return PLACES_ALONG.radius.default;\n if (typeof raw !== \"number\" || !Number.isFinite(raw)) return null;\n return Math.min(PLACES_ALONG.radius.max, Math.max(PLACES_ALONG.radius.min, raw));\n}\n\n/** `limit`, checked. Undefined means the default; out of range or not an integer is null. */\nexport function alongLimit(raw: unknown): number | null {\n if (raw === undefined) return PLACES_ALONG.limit.default;\n if (typeof raw !== \"number\" || !Number.isInteger(raw)) return null;\n if (raw < PLACES_ALONG.limit.min || raw > PLACES_ALONG.limit.max) return null;\n return raw;\n}\n"],"mappings":"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","names":[]}