@totemsdk/spatial-proof 0.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/dist/proof.js ADDED
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ /**
3
+ * Proof envelope integration for @totemsdk/spatial-proof.
4
+ *
5
+ * Bridges spatial objects and spatial relation claims into @totemsdk/proof
6
+ * evidence refs and signed proof envelopes. Signing/verification delegate to
7
+ * @totemsdk/proof. Anchoring is deliberately NOT required for verification.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.spatialObjectToEvidenceRef = spatialObjectToEvidenceRef;
11
+ exports.spatialRelationToEvidenceRef = spatialRelationToEvidenceRef;
12
+ exports.spatialClaimEvidenceRefs = spatialClaimEvidenceRefs;
13
+ exports.createUnsignedSpatialProof = createUnsignedSpatialProof;
14
+ exports.signSpatialProof = signSpatialProof;
15
+ exports.verifySpatialProof = verifySpatialProof;
16
+ const proof_1 = require("@totemsdk/proof");
17
+ const canonical_js_1 = require("./canonical.js");
18
+ const claim_js_1 = require("./claim.js");
19
+ /**
20
+ * Convert a SpatialObject into an EvidenceRef for inclusion in a proof.
21
+ */
22
+ function spatialObjectToEvidenceRef(obj) {
23
+ return {
24
+ id: obj.spatialId,
25
+ kind: 'spatial-object',
26
+ hash: (0, canonical_js_1.hashSpatialObject)(obj),
27
+ };
28
+ }
29
+ /**
30
+ * Convert a SpatialRelationClaim into an EvidenceRef for inclusion in a proof.
31
+ */
32
+ function spatialRelationToEvidenceRef(claim) {
33
+ return {
34
+ id: claim.relationId,
35
+ kind: 'spatial-relation',
36
+ hash: (0, canonical_js_1.hashSpatialRelationClaim)(claim),
37
+ };
38
+ }
39
+ /**
40
+ * Build the evidence ref list for a spatial proof.
41
+ *
42
+ * Always includes the relation claim hash and spatial object hash. Optionally
43
+ * adds the subject geometry hash, subject proof ID, location claim ID, and
44
+ * raster manifest ID when present on the claim.
45
+ */
46
+ function spatialClaimEvidenceRefs(claim, obj, subjectGeometry) {
47
+ const refs = [spatialRelationToEvidenceRef(claim), spatialObjectToEvidenceRef(obj)];
48
+ if (claim.inputs.subjectGeometryHash) {
49
+ refs.push({
50
+ id: claim.inputs.subjectGeometryHash,
51
+ kind: 'geometry',
52
+ hash: subjectGeometry ? (0, canonical_js_1.computeGeometryHash)(subjectGeometry) : claim.inputs.subjectGeometryHash,
53
+ });
54
+ }
55
+ if (claim.inputs.subjectProofId) {
56
+ refs.push({ id: claim.inputs.subjectProofId, kind: 'proof', hash: claim.inputs.subjectProofId });
57
+ }
58
+ if (claim.inputs.locationClaimId) {
59
+ refs.push({ id: claim.inputs.locationClaimId, kind: 'location-claim', hash: claim.inputs.locationClaimId });
60
+ }
61
+ if (claim.inputs.rasterManifestId) {
62
+ refs.push({ id: claim.inputs.rasterManifestId, kind: 'manifest', hash: claim.inputs.rasterManifestId });
63
+ }
64
+ return refs;
65
+ }
66
+ /**
67
+ * Create an unsigned attestation proof for a spatial relation claim.
68
+ *
69
+ * The proof claims: "this subject relates to this spatial object in this way
70
+ * at this time, computed by this engine." It does NOT claim the relation is
71
+ * geodetically exact — approximation notes travel inside the claim result.
72
+ */
73
+ function createUnsignedSpatialProof(params) {
74
+ const { claim, spatialObject } = params;
75
+ return (0, proof_1.createProof)({
76
+ kind: 'attestation',
77
+ subject: {
78
+ id: claim.subjectId,
79
+ kind: claim.subjectKind,
80
+ metadata: {
81
+ spatialRelationId: claim.relationId,
82
+ spatialObjectId: claim.spatialObjectId,
83
+ },
84
+ },
85
+ issuer: params.issuer ?? claim.subjectId,
86
+ issuedAt: params.issuedAt,
87
+ expiresAt: params.expiresAt,
88
+ evidence: spatialClaimEvidenceRefs(claim, spatialObject, params.subjectGeometry),
89
+ payload: { spatialRelation: claim },
90
+ });
91
+ }
92
+ /**
93
+ * Sign an unsigned spatial proof with a WOTS key.
94
+ *
95
+ * The caller is responsible for reserving the WOTS key index (see
96
+ * @totemsdk/wots-lease) before calling — one-time key warning applies.
97
+ */
98
+ function signSpatialProof(unsigned, seed, keyIndex) {
99
+ return (0, proof_1.signProof)(unsigned, seed, keyIndex);
100
+ }
101
+ /**
102
+ * Verify a signed spatial proof end to end.
103
+ *
104
+ * Checks:
105
+ * 1. the underlying @totemsdk/proof verification (signature, proofId, expiry)
106
+ * 2. payload contains a structurally valid SpatialRelationClaim
107
+ * 3. the relation's claimId matches a recomputation from its fields
108
+ * 4. the relation evidence hash matches the payload claim
109
+ * 5. the spatial-object evidence hash matches the payload claim inputs
110
+ * 6. any subject geometry evidence hash is present when claimed
111
+ *
112
+ * Anchoring is not required.
113
+ */
114
+ function verifySpatialProof(signed) {
115
+ const base = (0, proof_1.verifyProof)(signed);
116
+ if (!base.valid) {
117
+ return {
118
+ valid: false,
119
+ reason: base.reason,
120
+ signerAddress: base.signerAddress,
121
+ };
122
+ }
123
+ const signerAddress = signed.signature.address;
124
+ const claim = signed.payload?.['spatialRelation'];
125
+ if (!claim || typeof claim !== 'object') {
126
+ return { valid: false, reason: 'payload missing spatialRelation claim', signerAddress };
127
+ }
128
+ const validation = (0, claim_js_1.validateSpatialRelationClaim)(claim);
129
+ if (!validation.valid) {
130
+ return {
131
+ valid: false,
132
+ reason: 'payload spatialRelation invalid: ' + validation.errors.join('; '),
133
+ payloadValid: false,
134
+ signerAddress,
135
+ };
136
+ }
137
+ const { relationId: _relationId, ...rest } = claim;
138
+ const recomputedId = (0, canonical_js_1.computeSpatialRelationId)(rest);
139
+ if (recomputedId !== claim.relationId) {
140
+ return {
141
+ valid: false,
142
+ reason: 'relationId does not match recomputed value',
143
+ relationIdValid: false,
144
+ spatialObjectId: claim.spatialObjectId,
145
+ relationId: claim.relationId,
146
+ signerAddress,
147
+ };
148
+ }
149
+ const relationEvidence = signed.evidence?.find((e) => e.id === claim.relationId && e.kind === 'spatial-relation');
150
+ if (!relationEvidence) {
151
+ return {
152
+ valid: false,
153
+ reason: 'evidence missing spatial-relation ref',
154
+ spatialObjectId: claim.spatialObjectId,
155
+ relationId: claim.relationId,
156
+ signerAddress,
157
+ };
158
+ }
159
+ const expectedRelationHash = (0, canonical_js_1.hashSpatialRelationClaim)(claim);
160
+ if (relationEvidence.hash !== expectedRelationHash) {
161
+ return {
162
+ valid: false,
163
+ reason: 'evidence hash does not match payload claim',
164
+ evidenceHashValid: false,
165
+ spatialObjectId: claim.spatialObjectId,
166
+ relationId: claim.relationId,
167
+ signerAddress,
168
+ };
169
+ }
170
+ const objectEvidence = signed.evidence?.find((e) => e.id === claim.spatialObjectId && e.kind === 'spatial-object');
171
+ if (!objectEvidence) {
172
+ return {
173
+ valid: false,
174
+ reason: 'evidence missing spatial-object ref',
175
+ spatialObjectId: claim.spatialObjectId,
176
+ relationId: claim.relationId,
177
+ signerAddress,
178
+ };
179
+ }
180
+ return {
181
+ valid: true,
182
+ signerAddress,
183
+ spatialObjectId: claim.spatialObjectId,
184
+ relationId: claim.relationId,
185
+ payloadValid: true,
186
+ evidenceHashValid: true,
187
+ relationIdValid: true,
188
+ };
189
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Proofgraph integration helpers for @totemsdk/spatial-proof.
3
+ *
4
+ * Produces nodes and edges for @totemsdk/proofgraph without storage
5
+ * assumptions. Helpers never mutate a graph directly — callers combine the
6
+ * returned nodes/edges with addNode/addEdge.
7
+ *
8
+ * Note: @totemsdk/proofgraph has no native 'spatial' node type, so spatial
9
+ * objects and relation claims are mapped to the generic 'custom' node type
10
+ * (the same approach @totemsdk/location-proof uses). If a dedicated
11
+ * 'spatial-object' / 'spatial-relation' node type is ever wanted, propose it
12
+ * in @totemsdk/proofgraph (do not silently fork the vocabulary).
13
+ */
14
+ import type { ProofGraph, ProofGraphNode, ProofGraphEdge } from '@totemsdk/proofgraph';
15
+ import type { SpatialObject, SpatialRelationClaim } from './types.js';
16
+ /**
17
+ * Build a ProofGraphNode for a spatial object.
18
+ *
19
+ * Uses the 'custom' node type (no native proofgraph node type fits a spatial
20
+ * object). Node ID is deterministic: "custom:<spatialId>".
21
+ */
22
+ export declare function spatialObjectToProofGraphNode(obj: SpatialObject): ProofGraphNode;
23
+ /**
24
+ * Build a ProofGraphNode for a spatial relation claim.
25
+ *
26
+ * Uses the 'custom' node type. Node ID is deterministic: "custom:<relationId>".
27
+ */
28
+ export declare function spatialRelationToProofGraphNode(claim: SpatialRelationClaim): ProofGraphNode;
29
+ /**
30
+ * Build ProofGraphEdges for a spatial relation claim:
31
+ * about relation → subject
32
+ * references relation → spatial object
33
+ * derived_from relation → location claim (when present)
34
+ * references relation → subject proof (when present)
35
+ * references relation → raster manifest (when present)
36
+ *
37
+ * Edge IDs are deterministic (content-derived in @totemsdk/proofgraph).
38
+ */
39
+ export declare function spatialRelationToGraphEdges(claim: SpatialRelationClaim): ProofGraphEdge[];
40
+ /**
41
+ * Add a spatial relation claim as a 'custom' node to a proof graph (immutable
42
+ * — returns a new graph).
43
+ */
44
+ export declare function addSpatialRelationToGraph(graph: ProofGraph, claim: SpatialRelationClaim): ProofGraph;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * Proofgraph integration helpers for @totemsdk/spatial-proof.
4
+ *
5
+ * Produces nodes and edges for @totemsdk/proofgraph without storage
6
+ * assumptions. Helpers never mutate a graph directly — callers combine the
7
+ * returned nodes/edges with addNode/addEdge.
8
+ *
9
+ * Note: @totemsdk/proofgraph has no native 'spatial' node type, so spatial
10
+ * objects and relation claims are mapped to the generic 'custom' node type
11
+ * (the same approach @totemsdk/location-proof uses). If a dedicated
12
+ * 'spatial-object' / 'spatial-relation' node type is ever wanted, propose it
13
+ * in @totemsdk/proofgraph (do not silently fork the vocabulary).
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.spatialObjectToProofGraphNode = spatialObjectToProofGraphNode;
17
+ exports.spatialRelationToProofGraphNode = spatialRelationToProofGraphNode;
18
+ exports.spatialRelationToGraphEdges = spatialRelationToGraphEdges;
19
+ exports.addSpatialRelationToGraph = addSpatialRelationToGraph;
20
+ const proofgraph_1 = require("@totemsdk/proofgraph");
21
+ const canonical_js_1 = require("./canonical.js");
22
+ /**
23
+ * Build a ProofGraphNode for a spatial object.
24
+ *
25
+ * Uses the 'custom' node type (no native proofgraph node type fits a spatial
26
+ * object). Node ID is deterministic: "custom:<spatialId>".
27
+ */
28
+ function spatialObjectToProofGraphNode(obj) {
29
+ return {
30
+ id: (0, proofgraph_1.computeNodeId)('custom', obj.spatialId),
31
+ type: 'custom',
32
+ refId: obj.spatialId,
33
+ data: {
34
+ kind: 'spatial-object',
35
+ spatialKind: obj.kind,
36
+ geometryHash: (0, canonical_js_1.computeGeometryHash)(obj.geometry),
37
+ ...(obj.name !== undefined ? { name: obj.name } : {}),
38
+ ...(obj.crs !== undefined ? { crs: obj.crs } : {}),
39
+ },
40
+ createdAt: Date.now(),
41
+ };
42
+ }
43
+ /**
44
+ * Build a ProofGraphNode for a spatial relation claim.
45
+ *
46
+ * Uses the 'custom' node type. Node ID is deterministic: "custom:<relationId>".
47
+ */
48
+ function spatialRelationToProofGraphNode(claim) {
49
+ return {
50
+ id: (0, proofgraph_1.computeNodeId)('custom', claim.relationId),
51
+ type: 'custom',
52
+ refId: claim.relationId,
53
+ data: {
54
+ kind: 'spatial-relation',
55
+ subjectId: claim.subjectId,
56
+ spatialObjectId: claim.spatialObjectId,
57
+ relation: claim.relation,
58
+ matched: claim.result.matched,
59
+ },
60
+ createdAt: Date.now(),
61
+ };
62
+ }
63
+ /**
64
+ * Build ProofGraphEdges for a spatial relation claim:
65
+ * about relation → subject
66
+ * references relation → spatial object
67
+ * derived_from relation → location claim (when present)
68
+ * references relation → subject proof (when present)
69
+ * references relation → raster manifest (when present)
70
+ *
71
+ * Edge IDs are deterministic (content-derived in @totemsdk/proofgraph).
72
+ */
73
+ function spatialRelationToGraphEdges(claim) {
74
+ const edges = [
75
+ (0, proofgraph_1.buildEdge)('about', claim.relationId, claim.subjectId, claim.relationId),
76
+ (0, proofgraph_1.buildEdge)('references', claim.relationId, claim.spatialObjectId, claim.relationId),
77
+ ];
78
+ if (claim.inputs.locationClaimId) {
79
+ edges.push((0, proofgraph_1.buildEdge)('derived_from', claim.relationId, claim.inputs.locationClaimId, claim.relationId));
80
+ }
81
+ if (claim.inputs.subjectProofId) {
82
+ edges.push((0, proofgraph_1.buildEdge)('references', claim.relationId, claim.inputs.subjectProofId, claim.relationId));
83
+ }
84
+ if (claim.inputs.rasterManifestId) {
85
+ edges.push((0, proofgraph_1.buildEdge)('references', claim.relationId, claim.inputs.rasterManifestId, claim.relationId));
86
+ }
87
+ return edges;
88
+ }
89
+ /**
90
+ * Add a spatial relation claim as a 'custom' node to a proof graph (immutable
91
+ * — returns a new graph).
92
+ */
93
+ function addSpatialRelationToGraph(graph, claim) {
94
+ return (0, proofgraph_1.addNode)(graph, 'custom', claim.relationId, {
95
+ kind: 'spatial-relation',
96
+ ...claim,
97
+ });
98
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Spatial relation evaluation for @totemsdk/spatial-proof.
3
+ *
4
+ * evaluateSpatialRelation turns a subject geometry (point, route sample, or
5
+ * footprint bbox) plus a spatial object into a deterministic SpatialRelationClaim.
6
+ *
7
+ * HONESTY RULE: any relation evaluated by an approximate algorithm (bounding
8
+ * boxes instead of exact polygon intersection/coverage) MUST include an
9
+ * explicit uncertainty note. This package never silently claims exactness
10
+ * it did not compute.
11
+ *
12
+ * All functions are deterministic and dependency-free.
13
+ */
14
+ import type { EvaluateSpatialRelationParams, SpatialRelationClaim } from './types.js';
15
+ /**
16
+ * Evaluate a spatial relation and return a deterministic SpatialRelationClaim.
17
+ *
18
+ * The claim's relationId is content-derived from all fields except relationId
19
+ * and metadata, so identical evaluations always produce the same ID.
20
+ */
21
+ export declare function evaluateSpatialRelation(params: EvaluateSpatialRelationParams): SpatialRelationClaim;
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ /**
3
+ * Spatial relation evaluation for @totemsdk/spatial-proof.
4
+ *
5
+ * evaluateSpatialRelation turns a subject geometry (point, route sample, or
6
+ * footprint bbox) plus a spatial object into a deterministic SpatialRelationClaim.
7
+ *
8
+ * HONESTY RULE: any relation evaluated by an approximate algorithm (bounding
9
+ * boxes instead of exact polygon intersection/coverage) MUST include an
10
+ * explicit uncertainty note. This package never silently claims exactness
11
+ * it did not compute.
12
+ *
13
+ * All functions are deterministic and dependency-free.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.evaluateSpatialRelation = evaluateSpatialRelation;
17
+ const canonical_js_1 = require("./canonical.js");
18
+ const geometry_js_1 = require("./geometry.js");
19
+ const ENGINE_NAME = '@totemsdk/spatial-proof';
20
+ const ENGINE_VERSION = '0.1.0';
21
+ const BBOX_UNCERTAINTY = 'Relation evaluated using bounding-box approximation; exact polygon intersection not implemented.';
22
+ function isPointGeometry(g) {
23
+ return g.type === 'Point';
24
+ }
25
+ function isPolygonGeometry(g) {
26
+ return g.type === 'Polygon';
27
+ }
28
+ function isMultiPolygonGeometry(g) {
29
+ return g.type === 'MultiPolygon';
30
+ }
31
+ function isLineStringGeometry(g) {
32
+ return g.type === 'LineString';
33
+ }
34
+ function pointInside(g, point) {
35
+ if (isPolygonGeometry(g))
36
+ return (0, geometry_js_1.pointInPolygon)(point.coordinates, g);
37
+ if (isMultiPolygonGeometry(g))
38
+ return (0, geometry_js_1.pointInMultiPolygon)(point.coordinates, g);
39
+ return false;
40
+ }
41
+ /**
42
+ * Evaluate a spatial relation and return a deterministic SpatialRelationClaim.
43
+ *
44
+ * The claim's relationId is content-derived from all fields except relationId
45
+ * and metadata, so identical evaluations always produce the same ID.
46
+ */
47
+ function evaluateSpatialRelation(params) {
48
+ const { subjectId, subjectKind, spatialObject, relation, computedAt = Date.now() } = params;
49
+ const validation = (0, geometry_js_1.validateSpatialObject)(spatialObject);
50
+ if (!validation.valid) {
51
+ throw new Error('invalid spatial object: ' + validation.errors.join('; '));
52
+ }
53
+ const spatialGeometryHash = (0, canonical_js_1.computeGeometryHash)(spatialObject.geometry);
54
+ const subjectGeometryHash = params.subjectGeometry
55
+ ? (0, canonical_js_1.computeGeometryHash)(params.subjectGeometry)
56
+ : undefined;
57
+ // NOTE: relationId is NOT computed here. It is stamped once, at the very end,
58
+ // after every branch has finalised engine.algorithm and result, so recomputing
59
+ // the claim ID from the emitted claim always reproduces it.
60
+ const base = {
61
+ subjectId,
62
+ subjectKind,
63
+ spatialObjectId: spatialObject.spatialId,
64
+ relation,
65
+ computedAt,
66
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'unknown' },
67
+ inputs: {
68
+ ...(subjectGeometryHash !== undefined ? { subjectGeometryHash } : {}),
69
+ spatialGeometryHash,
70
+ ...(params.subjectProofId !== undefined ? { subjectProofId: params.subjectProofId } : {}),
71
+ ...(params.locationClaimId !== undefined ? { locationClaimId: params.locationClaimId } : {}),
72
+ ...(params.rasterManifestId !== undefined ? { rasterManifestId: params.rasterManifestId } : {}),
73
+ },
74
+ result: { matched: false },
75
+ ...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
76
+ };
77
+ function emit(overrides = {}) {
78
+ const body = { ...base, ...overrides };
79
+ return { ...body, relationId: (0, canonical_js_1.computeSpatialRelationId)(body) };
80
+ }
81
+ if (!params.subjectGeometry) {
82
+ return emit({
83
+ result: {
84
+ matched: false,
85
+ uncertainty: ['No subject geometry provided; relation could not be evaluated.'],
86
+ },
87
+ });
88
+ }
89
+ const subject = params.subjectGeometry;
90
+ const obj = spatialObject.geometry;
91
+ // ── inside / outside / entered_zone / exited_zone (point vs polygon) ───────
92
+ if (relation === 'inside' || relation === 'entered_zone') {
93
+ if (isPointGeometry(subject) && (isPolygonGeometry(obj) || isMultiPolygonGeometry(obj))) {
94
+ const matched = pointInside(obj, subject);
95
+ return emit({
96
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'point-in-polygon (ray casting, planar)' },
97
+ result: {
98
+ matched,
99
+ confidenceScore: matched ? 80 : 60,
100
+ uncertainty: ['Point-in-polygon test is planar (lon/lat); not a geodesic test.'],
101
+ },
102
+ });
103
+ }
104
+ return emit({
105
+ result: {
106
+ matched: false,
107
+ uncertainty: ['inside requires a Point subject and Polygon/MultiPolygon object.'],
108
+ },
109
+ });
110
+ }
111
+ if (relation === 'outside' || relation === 'exited_zone') {
112
+ if (isPointGeometry(subject) && (isPolygonGeometry(obj) || isMultiPolygonGeometry(obj))) {
113
+ const matched = !pointInside(obj, subject);
114
+ return emit({
115
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'point-in-polygon (ray casting, planar)' },
116
+ result: {
117
+ matched,
118
+ confidenceScore: matched ? 80 : 60,
119
+ uncertainty: ['Point-in-polygon test is planar (lon/lat); not a geodesic test.'],
120
+ },
121
+ });
122
+ }
123
+ return emit({
124
+ result: {
125
+ matched: false,
126
+ uncertainty: ['outside requires a Point subject and Polygon/MultiPolygon object.'],
127
+ },
128
+ });
129
+ }
130
+ // ── within_distance (point vs point via Haversine) ─────────────────────────
131
+ if (relation === 'within_distance') {
132
+ if (isPointGeometry(subject) && isPointGeometry(obj)) {
133
+ const distanceM = (0, geometry_js_1.distanceMeters)(subject.coordinates, obj.coordinates);
134
+ const maxDistanceM = params.maxDistanceM ?? 0;
135
+ return emit({
136
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'haversine great-circle distance' },
137
+ result: {
138
+ matched: distanceM <= maxDistanceM,
139
+ distanceM,
140
+ confidenceScore: 95,
141
+ },
142
+ });
143
+ }
144
+ return emit({
145
+ result: {
146
+ matched: false,
147
+ uncertainty: ['within_distance requires Point subject and Point object (Haversine).'],
148
+ },
149
+ });
150
+ }
151
+ // ── near_boundary (point vs polygon boundary) ──────────────────────────────
152
+ if (relation === 'near_boundary') {
153
+ if (isPointGeometry(subject) && (isPolygonGeometry(obj) || isMultiPolygonGeometry(obj))) {
154
+ const polys = isMultiPolygonGeometry(obj) ? obj.coordinates.map((c) => ({ type: 'Polygon', coordinates: c })) : [obj];
155
+ const thresholdM = params.maxDistanceM ?? 0;
156
+ let nearest = Infinity;
157
+ let matched = false;
158
+ for (const poly of polys) {
159
+ const inside = (0, geometry_js_1.pointInPolygon)(subject.coordinates, poly);
160
+ const near = (0, geometry_js_1.isPointNearBoundary)(subject.coordinates, poly, thresholdM);
161
+ if (!inside && near)
162
+ matched = true;
163
+ // distance to nearest ring vertex as a rough distance figure
164
+ for (const ring of poly.coordinates) {
165
+ for (let i = 1; i < ring.length; i++) {
166
+ const d = (0, geometry_js_1.distancePointToSegmentMeters)(subject.coordinates, ring[i - 1], ring[i]);
167
+ if (!inside && d < nearest)
168
+ nearest = d;
169
+ }
170
+ }
171
+ }
172
+ return emit({
173
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'point-to-polygon-boundary distance (equirectangular approximation)' },
174
+ result: {
175
+ matched,
176
+ ...(Number.isFinite(nearest) ? { distanceM: nearest } : {}),
177
+ uncertainty: ['Boundary distance uses an equirectangular local approximation.'],
178
+ },
179
+ });
180
+ }
181
+ return emit({
182
+ result: {
183
+ matched: false,
184
+ uncertainty: ['near_boundary requires a Point subject and Polygon/MultiPolygon object.'],
185
+ },
186
+ });
187
+ }
188
+ // ── on_route (point near LineString) ───────────────────────────────────────
189
+ if (relation === 'on_route') {
190
+ if (isPointGeometry(subject) && isLineStringGeometry(obj)) {
191
+ const distanceM = (0, geometry_js_1.distancePointToLineStringMeters)(subject.coordinates, obj);
192
+ const maxDistanceM = params.maxDistanceM ?? 0;
193
+ return emit({
194
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'point-to-LineString distance (equirectangular approximation)' },
195
+ result: {
196
+ matched: distanceM <= maxDistanceM,
197
+ distanceM,
198
+ confidenceScore: 85,
199
+ uncertainty: ['Route distance uses an equirectangular approximation; adequate for short segments.'],
200
+ },
201
+ });
202
+ }
203
+ return emit({
204
+ result: {
205
+ matched: false,
206
+ uncertainty: ['on_route requires a Point subject and LineString object.'],
207
+ },
208
+ });
209
+ }
210
+ // ── intersects / overlaps (bbox precheck; polygon relation where feasible) ─
211
+ if (relation === 'intersects' || relation === 'overlaps') {
212
+ const subBox = (0, geometry_js_1.getBoundingBox)(subject);
213
+ const objBox = (0, geometry_js_1.getBoundingBox)(obj);
214
+ const boxOk = (0, geometry_js_1.bboxIntersects)(subBox, objBox);
215
+ // When both are polygons we can say more than the box: check vertices.
216
+ const exact = isPolygonGeometry(subject) && (isPolygonGeometry(obj) || isMultiPolygonGeometry(obj))
217
+ ? polygonIntersectsExact(subject, obj)
218
+ : undefined;
219
+ // A vertex-probe is still not a full intersection: crops can cross edges
220
+ // without any vertex inside the other polygon.
221
+ const matched = exact ?? boxOk;
222
+ return emit({
223
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'bbox precheck' + (exact !== undefined ? ' + polygon vertex probing' : '') },
224
+ result: {
225
+ matched,
226
+ uncertainty: exact === undefined
227
+ ? [BBOX_UNCERTAINTY]
228
+ : ['Vertex probing is approximate; polygons that intersect without shared vertices may be misjudged.'],
229
+ },
230
+ });
231
+ }
232
+ // ── covers / covered_by (scene footprint bbox covers site boundary bbox) ──
233
+ if (relation === 'covers' || relation === 'covered_by') {
234
+ const subBox = (0, geometry_js_1.getBoundingBox)(subject);
235
+ const objBox = (0, geometry_js_1.getBoundingBox)(obj);
236
+ const matched = relation === 'covers' ? (0, geometry_js_1.bboxCovers)(subBox, objBox) : (0, geometry_js_1.bboxCovers)(objBox, subBox);
237
+ return emit({
238
+ engine: { name: ENGINE_NAME, version: ENGINE_VERSION, algorithm: 'bbox cover check' },
239
+ result: {
240
+ matched,
241
+ uncertainty: [BBOX_UNCERTAINTY],
242
+ },
243
+ });
244
+ }
245
+ // ── unknown ────────────────────────────────────────────────────────────────
246
+ return emit({
247
+ result: {
248
+ matched: false,
249
+ uncertainty: [`Relation type '${relation}' is not supported.`],
250
+ },
251
+ });
252
+ }
253
+ function polygonIntersectsExact(subject, obj) {
254
+ const objPolys = isMultiPolygonGeometry(obj) ? obj.coordinates.map((c) => ({ type: 'Polygon', coordinates: c })) : [obj];
255
+ for (const poly of objPolys) {
256
+ for (const ring of subject.coordinates) {
257
+ for (const v of ring) {
258
+ if ((0, geometry_js_1.pointInPolygon)(v, poly))
259
+ return true;
260
+ }
261
+ }
262
+ for (const ring of poly.coordinates) {
263
+ for (const v of ring) {
264
+ if ((0, geometry_js_1.pointInPolygon)(v, subject))
265
+ return true;
266
+ }
267
+ }
268
+ }
269
+ return false;
270
+ }