terra-route 0.0.12 → 0.0.14

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.
@@ -6,6 +6,7 @@ import { FourAryHeap } from "./heap/four-ary-heap";
6
6
 
7
7
  interface Router {
8
8
  buildRouteGraph(network: FeatureCollection<LineString>): void;
9
+ expandRouteGraph(network: FeatureCollection<LineString>): void;
9
10
  getRoute(start: Feature<Point>, end: Feature<Point>): Feature<LineString> | null;
10
11
  }
11
12
 
@@ -31,6 +32,11 @@ class TerraRoute implements Router {
31
32
  private cameFromScratch: Int32Array | null = null; // Predecessor per node for path reconstruction
32
33
  private visitedScratch: Uint8Array | null = null; // Visited set to avoid reprocessing
33
34
  private hScratch: Float64Array | null = null; // Per-node heuristic cache for the current query (lazy compute)
35
+ // Reverse-direction scratch for bidirectional search
36
+ private gScoreRevScratch: Float64Array | null = null; // gScore per node from the end
37
+ private cameFromRevScratch: Int32Array | null = null; // Successor per node (next step toward the end)
38
+ private visitedRevScratch: Uint8Array | null = null; // Visited set for reverse search
39
+ private hRevScratch: Float64Array | null = null; // Heuristic cache for reverse direction per query
34
40
  private scratchCapacity = 0; // Current capacity of scratch arrays
35
41
 
36
42
  constructor(options?: {
@@ -63,52 +69,15 @@ class TerraRoute implements Router {
63
69
  const coordsLocal = this.coordinates; // Local alias for coordinates array
64
70
  const measureDistance = this.distanceMeasurement; // Local alias for distance function
65
71
 
66
- const features = network.features; // All LineString features
67
-
68
72
  // Pass 1: assign indices and count degrees per node
69
73
  const degree: number[] = []; // Dynamic degree array; grows as nodes are discovered
70
- for (let f = 0, fLen = features.length; f < fLen; f++) { // Iterate features
71
- const feature = features[f]; // Current feature
72
- const lineCoords = feature.geometry.coordinates; // Coordinates for this LineString
73
-
74
- for (let i = 0, len = lineCoords.length - 1; i < len; i++) { // Iterate segment pairs
75
- const a = lineCoords[i]; // Segment start coord
76
- const b = lineCoords[i + 1]; // Segment end coord
77
-
78
- const lngA = a[0], latA = a[1]; // Keys for A
79
- const lngB = b[0], latB = b[1]; // Keys for B
80
-
81
- // Index A
82
- let latMapA = coordIndexMapLocal.get(lngA);
83
- if (latMapA === undefined) {
84
- latMapA = new Map<number, number>();
85
- coordIndexMapLocal.set(lngA, latMapA);
86
- }
87
- let indexA = latMapA.get(latA);
88
- if (indexA === undefined) {
89
- indexA = coordsLocal.length;
90
- coordsLocal.push(a);
91
- latMapA.set(latA, indexA);
92
- }
93
-
94
- // Index B
95
- let latMapB = coordIndexMapLocal.get(lngB);
96
- if (latMapB === undefined) {
97
- latMapB = new Map<number, number>();
98
- coordIndexMapLocal.set(lngB, latMapB);
99
- }
100
- let indexB = latMapB.get(latB);
101
- if (indexB === undefined) {
102
- indexB = coordsLocal.length;
103
- coordsLocal.push(b);
104
- latMapB.set(latB, indexB);
105
- }
74
+ this.forEachSegment(network, (a, b) => {
75
+ const indexA = this.indexCoordinate(a, coordIndexMapLocal, coordsLocal);
76
+ const indexB = this.indexCoordinate(b, coordIndexMapLocal, coordsLocal);
106
77
 
107
- // Count degree for both directions
108
- degree[indexA] = (degree[indexA] ?? 0) + 1;
109
- degree[indexB] = (degree[indexB] ?? 0) + 1;
110
- }
111
- }
78
+ degree[indexA] = (degree[indexA] ?? 0) + 1;
79
+ degree[indexB] = (degree[indexB] ?? 0) + 1;
80
+ });
112
81
 
113
82
  // Build CSR arrays from degree counts
114
83
  const nodeCount = this.coordinates.length; // Total nodes discovered
@@ -124,32 +93,20 @@ class TerraRoute implements Router {
124
93
 
125
94
  // Pass 2: fill CSR arrays using a write cursor per node
126
95
  const cursor = offsets.slice(); // Current write positions per node
127
- for (let f = 0, fLen = features.length; f < fLen; f++) {
128
- const feature = features[f];
129
- const lineCoords = feature.geometry.coordinates;
130
- for (let i = 0, len = lineCoords.length - 1; i < len; i++) {
131
- const a = lineCoords[i];
132
- const b = lineCoords[i + 1];
133
-
134
- const lngA = a[0], latA = a[1];
135
- const lngB = b[0], latB = b[1];
136
-
137
- // Read back indices (guaranteed to exist from pass 1)
138
- const indexA = this.coordinateIndexMap.get(lngA)!.get(latA)!;
139
- const indexB = this.coordinateIndexMap.get(lngB)!.get(latB)!;
140
-
141
- const segmentDistance = measureDistance(a, b); // Edge weight once
142
-
143
- // Write A → B
144
- let pos = cursor[indexA]++;
145
- indices[pos] = indexB;
146
- distances[pos] = segmentDistance;
147
- // Write B → A
148
- pos = cursor[indexB]++;
149
- indices[pos] = indexA;
150
- distances[pos] = segmentDistance;
151
- }
152
- }
96
+ this.forEachSegment(network, (a, b) => {
97
+ // Read back indices (guaranteed to exist from pass 1)
98
+ const indexA = this.coordinateIndexMap.get(a[0])!.get(a[1])!;
99
+ const indexB = this.coordinateIndexMap.get(b[0])!.get(b[1])!;
100
+
101
+ const segmentDistance = measureDistance(a, b); // Edge weight once
102
+
103
+ let pos = cursor[indexA]++;
104
+ indices[pos] = indexB;
105
+ distances[pos] = segmentDistance;
106
+ pos = cursor[indexB]++;
107
+ indices[pos] = indexA;
108
+ distances[pos] = segmentDistance;
109
+ });
153
110
 
154
111
  // Commit CSR to instance
155
112
  this.csrOffsets = offsets;
@@ -160,6 +117,122 @@ class TerraRoute implements Router {
160
117
  this.adjacencyList = new Array(nodeCount);
161
118
  }
162
119
 
120
+ /**
121
+ * Expands (merges) the existing graph with an additional LineString FeatureCollection.
122
+ */
123
+ public expandRouteGraph(network: FeatureCollection<LineString>): void {
124
+ if (this.network === null) {
125
+ throw new Error("Network not built. Please call buildRouteGraph(network) first.");
126
+ }
127
+
128
+ // Merge the feature arrays for reference/debugging. We avoid copying properties deeply.
129
+ this.network = {
130
+ type: "FeatureCollection",
131
+ features: [...this.network.features, ...network.features],
132
+ };
133
+
134
+ const coordIndexMapLocal = this.coordinateIndexMap;
135
+ const coordsLocal = this.coordinates;
136
+ const measureDistance = this.distanceMeasurement;
137
+ const adj = this.adjacencyList;
138
+
139
+ // Ensure we have adjacency arrays for any existing CSR-only nodes.
140
+ // (During buildRouteGraph, adjacencyList is sized but entries are undefined.)
141
+ for (let i = 0; i < adj.length; i++) {
142
+ if (adj[i] === undefined) adj[i] = [];
143
+ }
144
+
145
+ // Add new edges into the adjacency list (sparse), then rebuild CSR from adjacency.
146
+ this.forEachSegment(network, (a, b) => {
147
+ const indexA = this.indexCoordinate(a, coordIndexMapLocal, coordsLocal, (idx) => { adj[idx] = []; });
148
+ const indexB = this.indexCoordinate(b, coordIndexMapLocal, coordsLocal, (idx) => { adj[idx] = []; });
149
+
150
+ const segmentDistance = measureDistance(a, b);
151
+
152
+ adj[indexA].push({ node: indexB, distance: segmentDistance });
153
+ adj[indexB].push({ node: indexA, distance: segmentDistance });
154
+ });
155
+
156
+ this.rebuildCsrFromAdjacency();
157
+ }
158
+
159
+ /**
160
+ * Rebuild CSR arrays for the full node set, using:
161
+ * - Existing CSR edges (from the last build/expand)
162
+ * - Any additional edges stored in `adjacencyList`
163
+ */
164
+ private rebuildCsrFromAdjacency(): void {
165
+ const nodeCount = this.coordinates.length;
166
+ const adj = this.adjacencyList;
167
+
168
+ // Compute degree using CSR degree + adjacency degree
169
+ const degree = new Int32Array(nodeCount);
170
+
171
+ if (this.csrOffsets && this.csrIndices && this.csrDistances) {
172
+ const csrOffsets = this.csrOffsets;
173
+ const covered = Math.min(this.csrNodeCount, nodeCount);
174
+ for (let i = 0; i < covered; i++) {
175
+ degree[i] += (csrOffsets[i + 1] - csrOffsets[i]);
176
+ }
177
+ }
178
+
179
+ for (let i = 0; i < nodeCount; i++) {
180
+ const neighbors = adj[i];
181
+ if (neighbors && neighbors.length) degree[i] += neighbors.length;
182
+ }
183
+
184
+ const offsets = new Int32Array(nodeCount + 1);
185
+ for (let i = 0; i < nodeCount; i++) {
186
+ offsets[i + 1] = offsets[i] + degree[i];
187
+ }
188
+ const totalEdges = offsets[nodeCount];
189
+ const indices = new Int32Array(totalEdges);
190
+ const distances = new Float64Array(totalEdges);
191
+ const cursor = offsets.slice();
192
+
193
+ // Copy existing CSR edges first
194
+ if (this.csrOffsets && this.csrIndices && this.csrDistances) {
195
+ const csrOffsets = this.csrOffsets;
196
+ const csrIndices = this.csrIndices;
197
+ const csrDistances = this.csrDistances;
198
+ const covered = Math.min(this.csrNodeCount, nodeCount);
199
+ for (let n = 0; n < covered; n++) {
200
+ const startOff = csrOffsets[n];
201
+ const endOff = csrOffsets[n + 1];
202
+ let pos = cursor[n];
203
+ for (let i = startOff; i < endOff; i++) {
204
+ indices[pos] = csrIndices[i];
205
+ distances[pos] = csrDistances[i];
206
+ pos++;
207
+ }
208
+ cursor[n] = pos;
209
+ }
210
+ }
211
+
212
+ // Append adjacency edges
213
+ for (let n = 0; n < nodeCount; n++) {
214
+ const neighbors = adj[n];
215
+ if (!neighbors || neighbors.length === 0) continue;
216
+ let pos = cursor[n];
217
+ for (let i = 0, len = neighbors.length; i < len; i++) {
218
+ const nb = neighbors[i];
219
+ indices[pos] = nb.node;
220
+ distances[pos] = nb.distance;
221
+ pos++;
222
+ }
223
+ cursor[n] = pos;
224
+ }
225
+
226
+ // Commit and reset adjacency (we've absorbed edges into CSR)
227
+ this.csrOffsets = offsets;
228
+ this.csrIndices = indices;
229
+ this.csrDistances = distances;
230
+ this.csrNodeCount = nodeCount;
231
+
232
+ // Keep adjacency list for *future* dynamic additions, but clear existing edges to avoid duplication.
233
+ this.adjacencyList = new Array(nodeCount);
234
+ }
235
+
163
236
  /**
164
237
  * Computes the shortest route between two points in the network using the A* algorithm.
165
238
  *
@@ -189,112 +262,202 @@ class TerraRoute implements Router {
189
262
  // Local aliases
190
263
  const coords = this.coordinates; // Alias to coordinates array
191
264
  const adj = this.adjacencyList; // Alias to sparse adjacency list (for dynamic nodes)
192
- const measureDistance = this.distanceMeasurement; // Alias to distance function
193
265
 
194
266
  // Ensure and init scratch buffers
195
267
  const nodeCount = coords.length; // Current number of nodes (may be >= csrNodeCount if new nodes added)
196
268
  this.ensureScratch(nodeCount); // Allocate scratch arrays if needed
197
269
 
198
270
  // Non-null after ensure
199
- const gScore = this.gScoreScratch!; // gScore pointer
200
- const cameFrom = this.cameFromScratch!; // cameFrom pointer
201
- const visited = this.visitedScratch!; // visited pointer
202
- const hCache = this.hScratch!; // heuristic cache pointer
203
-
204
- // Reset only the used range for speed
205
- gScore.fill(Number.POSITIVE_INFINITY, 0, nodeCount); // Init gScore to +∞
206
- cameFrom.fill(-1, 0, nodeCount); // Init predecessors to -1 (unknown)
207
- visited.fill(0, 0, nodeCount); // Init visited flags to 0
208
- hCache.fill(-1, 0, nodeCount); // Init heuristic cache with sentinel (-1 means unknown)
209
-
210
- // Create min-heap (priority queue)
211
- const openSet = new this.heapConstructor();
212
-
213
- // Precompute heuristic for start to prime the queue cost
214
- const endCoord = coords[endIndex]; // Cache end coordinate for heuristic
215
- let hStart = hCache[startIndex];
216
-
217
- if (hStart < 0) {
218
- hStart = measureDistance(coords[startIndex], endCoord);
219
- hCache[startIndex] = hStart;
220
- }
221
-
222
- openSet.insert(hStart, startIndex); // Insert start with f = g(0) + h(start)
223
- gScore[startIndex] = 0; // g(start) = 0
224
-
225
- while (openSet.size() > 0) { // Main A* loop until queue empty
226
- const current = openSet.extractMin()!; // Pop node with smallest f
227
- if (visited[current] !== 0) { // Skip if already finalized
228
- continue;
229
- }
230
- if (current === endIndex) { // Early exit if reached goal
271
+ const gF = this.gScoreScratch!; // forward gScore (from start)
272
+ const gR = this.gScoreRevScratch!; // reverse gScore (from end)
273
+ const prevF = this.cameFromScratch!; // predecessor in forward search
274
+ const nextR = this.cameFromRevScratch!; // successor in reverse search (toward end)
275
+ const visF = this.visitedScratch!;
276
+ const visR = this.visitedRevScratch!;
277
+ const hF = this.hScratch!;
278
+ const hR = this.hRevScratch!;
279
+
280
+ gF.fill(Number.POSITIVE_INFINITY, 0, nodeCount);
281
+ gR.fill(Number.POSITIVE_INFINITY, 0, nodeCount);
282
+ prevF.fill(-1, 0, nodeCount);
283
+ nextR.fill(-1, 0, nodeCount);
284
+ visF.fill(0, 0, nodeCount);
285
+ visR.fill(0, 0, nodeCount);
286
+ hF.fill(-1, 0, nodeCount);
287
+ hR.fill(-1, 0, nodeCount);
288
+
289
+ const openF = new this.heapConstructor();
290
+ const openR = new this.heapConstructor();
291
+
292
+ const startCoord = coords[startIndex];
293
+ const endCoord = coords[endIndex];
294
+
295
+ gF[startIndex] = 0;
296
+ gR[endIndex] = 0;
297
+
298
+ // Bidirectional Dijkstra (A* with zero heuristic). This keeps correctness simple and matches the
299
+ // reference pathfinder while still saving work by meeting in the middle.
300
+ openF.insert(0, startIndex);
301
+ openR.insert(0, endIndex);
302
+
303
+ // Best meeting point found so far
304
+ let bestPathCost = Number.POSITIVE_INFINITY;
305
+ let meetingNode = -1;
306
+
307
+ // Main bidirectional loop: expand alternately.
308
+ // Without a heap peek, a safe and effective stopping rule is based on the last extracted keys:
309
+ // once min_g_forward + min_g_reverse >= bestPathCost, no shorter path can still be found.
310
+ let lastExtractedGForward = 0;
311
+ let lastExtractedGReverse = 0;
312
+ while (openF.size() > 0 && openR.size() > 0) {
313
+ if (meetingNode >= 0 && (lastExtractedGForward + lastExtractedGReverse) >= bestPathCost) {
231
314
  break;
232
315
  }
233
- visited[current] = 1; // Mark as visited
234
-
235
- // Prefer CSR neighbors if available for this node, fall back to sparse list for dynamically added nodes
236
-
237
- if (this.csrOffsets && current < this.csrNodeCount) { // Use CSR fast path
238
- const csrOffsets = this.csrOffsets!; // Local CSR offsets (non-null here)
239
- const csrIndices = this.csrIndices!; // Local CSR neighbors
240
- const csrDistances = this.csrDistances!; // Local CSR weights
241
- const startOff = csrOffsets[current]; // Row start for current
242
- const endOff = csrOffsets[current + 1]; // Row end for current
243
-
244
- for (let i = startOff; i < endOff; i++) { // Iterate neighbors in CSR slice
245
- const nbNode = csrIndices[i]; // Neighbor node id
246
- const tentativeG = gScore[current] + csrDistances[i]; // g' = g(current) + w(current, nb)
247
- if (tentativeG < gScore[nbNode]) { // Relaxation check
248
- gScore[nbNode] = tentativeG; // Update best g
249
- cameFrom[nbNode] = current; // Track predecessor
250
- // A* priority = g + h (cache h per node for this query)
251
- let hVal = hCache[nbNode];
252
- if (hVal < 0) { hVal = measureDistance(coords[nbNode], endCoord); hCache[nbNode] = hVal; }
253
- openSet.insert(tentativeG + hVal, nbNode); // Push/update neighbor into open set
316
+
317
+ // Expand one step from each side, prioritizing the smaller frontier.
318
+ const expandForward = openF.size() <= openR.size();
319
+
320
+ if (expandForward) {
321
+ const current = openF.extractMin()!;
322
+ if (visF[current] !== 0) continue;
323
+ lastExtractedGForward = gF[current];
324
+ visF[current] = 1;
325
+
326
+ // If reverse has finalized this node, we have a candidate meeting.
327
+ if (visR[current] !== 0) {
328
+ const total = gF[current] + gR[current];
329
+ if (total < bestPathCost) {
330
+ bestPathCost = total;
331
+ meetingNode = current;
254
332
  }
255
333
  }
256
- }
257
- // Fallback: use sparse adjacency (only for nodes added after CSR build)
258
- else {
259
-
260
- const neighbors = adj[current]; // Neighbor list for current
261
- if (!neighbors || neighbors.length === 0) continue; // No neighbors
262
- for (let i = 0, n = neighbors.length; i < n; i++) { // Iterate neighbors
263
- const nb = neighbors[i]; // Neighbor entry
264
- const nbNode = nb.node; // Neighbor id
265
-
266
- const tentativeG = gScore[current] + nb.distance; // g' via current
267
- if (tentativeG < gScore[nbNode]) { // Relax if better
268
- gScore[nbNode] = tentativeG; // Update best g
269
- cameFrom[nbNode] = current; // Track predecessor
270
-
271
- // A* priority = g + h (cached)
272
- let hVal = hCache[nbNode];
273
- if (hVal < 0) { hVal = measureDistance(coords[nbNode], endCoord); hCache[nbNode] = hVal; }
274
- openSet.insert(tentativeG + hVal, nbNode); // Enqueue neighbor
334
+
335
+ // Relax neighbors and push newly improved ones
336
+ if (this.csrOffsets && current < this.csrNodeCount) {
337
+ const csrOffsets = this.csrOffsets!;
338
+ const csrIndices = this.csrIndices!;
339
+ const csrDistances = this.csrDistances!;
340
+ for (let i = csrOffsets[current], endOff = csrOffsets[current + 1]; i < endOff; i++) {
341
+ const nbNode = csrIndices[i];
342
+ const tentativeG = gF[current] + csrDistances[i];
343
+ if (tentativeG < gF[nbNode]) {
344
+ gF[nbNode] = tentativeG;
345
+ prevF[nbNode] = current;
346
+ const otherG = gR[nbNode];
347
+ if (otherG !== Number.POSITIVE_INFINITY) {
348
+ const total = tentativeG + otherG;
349
+ if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
350
+ }
351
+ openF.insert(tentativeG, nbNode);
352
+ }
353
+ }
354
+ } else {
355
+ const neighbors = adj[current];
356
+ if (neighbors && neighbors.length) {
357
+ for (let i = 0, n = neighbors.length; i < n; i++) {
358
+ const nb = neighbors[i];
359
+ const nbNode = nb.node;
360
+ const tentativeG = gF[current] + nb.distance;
361
+ if (tentativeG < gF[nbNode]) {
362
+ gF[nbNode] = tentativeG;
363
+ prevF[nbNode] = current;
364
+ const otherG = gR[nbNode];
365
+ if (otherG !== Number.POSITIVE_INFINITY) {
366
+ const total = tentativeG + otherG;
367
+ if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
368
+ }
369
+ openF.insert(tentativeG, nbNode);
370
+ }
371
+ }
372
+ }
373
+ }
374
+ } else {
375
+ const current = openR.extractMin()!;
376
+ if (visR[current] !== 0) continue;
377
+ lastExtractedGReverse = gR[current];
378
+ visR[current] = 1;
379
+
380
+ if (visF[current] !== 0) {
381
+ const total = gF[current] + gR[current];
382
+ if (total < bestPathCost) {
383
+ bestPathCost = total;
384
+ meetingNode = current;
385
+ }
386
+ }
387
+
388
+ // Reverse direction: same neighbor iteration because graph is undirected.
389
+ // Store successor pointer (next step toward end) i.e. nextR[neighbor] = current.
390
+ if (this.csrOffsets && current < this.csrNodeCount) {
391
+ const csrOffsets = this.csrOffsets!;
392
+ const csrIndices = this.csrIndices!;
393
+ const csrDistances = this.csrDistances!;
394
+ for (let i = csrOffsets[current], endOff = csrOffsets[current + 1]; i < endOff; i++) {
395
+ const nbNode = csrIndices[i];
396
+ const tentativeG = gR[current] + csrDistances[i];
397
+ if (tentativeG < gR[nbNode]) {
398
+ gR[nbNode] = tentativeG;
399
+ nextR[nbNode] = current;
400
+ const otherG = gF[nbNode];
401
+ if (otherG !== Number.POSITIVE_INFINITY) {
402
+ const total = tentativeG + otherG;
403
+ if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
404
+ }
405
+ openR.insert(tentativeG, nbNode);
406
+ }
407
+ }
408
+ } else {
409
+ const neighbors = adj[current];
410
+ if (neighbors && neighbors.length) {
411
+ for (let i = 0, n = neighbors.length; i < n; i++) {
412
+ const nb = neighbors[i];
413
+ const nbNode = nb.node;
414
+ const tentativeG = gR[current] + nb.distance;
415
+ if (tentativeG < gR[nbNode]) {
416
+ gR[nbNode] = tentativeG;
417
+ nextR[nbNode] = current;
418
+ const otherG = gF[nbNode];
419
+ if (otherG !== Number.POSITIVE_INFINITY) {
420
+ const total = tentativeG + otherG;
421
+ if (total < bestPathCost) { bestPathCost = total; meetingNode = nbNode; }
422
+ }
423
+ openR.insert(tentativeG, nbNode);
424
+ }
425
+ }
275
426
  }
276
427
  }
277
428
  }
278
429
  }
279
430
 
280
- // If goal was never reached/relaxed, return null
281
- if (cameFrom[endIndex] < 0) {
431
+ if (meetingNode < 0) {
282
432
  return null;
283
433
  }
284
434
 
285
- // Reconstruct path (push + reverse to avoid O(n^2) unshift)
435
+ // Reconstruct path: start -> meeting using prevF, then meeting -> end using nextR
286
436
  const path: Position[] = [];
287
- let cur = endIndex;
288
- while (cur !== startIndex) {
437
+
438
+ // Walk back from meeting to start, collecting nodes
439
+ let cur = meetingNode;
440
+ while (cur !== startIndex && cur >= 0) {
289
441
  path.push(coords[cur]);
290
- cur = cameFrom[cur];
442
+ cur = prevF[cur];
443
+ }
444
+ if (cur !== startIndex) {
445
+ // Forward tree doesn't connect start to meeting (shouldn't happen if meeting is valid)
446
+ return null;
291
447
  }
292
- // Include start coordinate
293
448
  path.push(coords[startIndex]);
294
-
295
- // Reverse to get start→end order
296
449
  path.reverse();
297
450
 
451
+ // Walk from meeting to end (skip meeting node because it's already included)
452
+ cur = meetingNode;
453
+ while (cur !== endIndex) {
454
+ cur = nextR[cur];
455
+ if (cur < 0) {
456
+ return null;
457
+ }
458
+ path.push(coords[cur]);
459
+ }
460
+
298
461
  return {
299
462
  type: "Feature",
300
463
  geometry: { type: "LineString", coordinates: path },
@@ -353,7 +516,11 @@ class TerraRoute implements Router {
353
516
  && this.gScoreScratch
354
517
  && this.cameFromScratch
355
518
  && this.visitedScratch
356
- && this.hScratch;
519
+ && this.hScratch
520
+ && this.gScoreRevScratch
521
+ && this.cameFromRevScratch
522
+ && this.visitedRevScratch
523
+ && this.hRevScratch;
357
524
 
358
525
  if (ifAlreadyBigEnough) {
359
526
  return; // Nothing to do
@@ -363,8 +530,58 @@ class TerraRoute implements Router {
363
530
  this.cameFromScratch = new Int32Array(capacity);
364
531
  this.visitedScratch = new Uint8Array(capacity);
365
532
  this.hScratch = new Float64Array(capacity);
533
+ this.gScoreRevScratch = new Float64Array(capacity);
534
+ this.cameFromRevScratch = new Int32Array(capacity);
535
+ this.visitedRevScratch = new Uint8Array(capacity);
536
+ this.hRevScratch = new Float64Array(capacity);
366
537
  this.scratchCapacity = capacity;
367
538
  }
539
+
540
+
541
+ // Iterate all consecutive segment pairs in a LineString FeatureCollection.
542
+ // Kept as a simple loop helper to avoid repeating nested iteration logic.
543
+ private forEachSegment(
544
+ network: FeatureCollection<LineString>,
545
+ fn: (a: Position, b: Position) => void,
546
+ ): void {
547
+ const features = network.features;
548
+ for (let f = 0, fLen = features.length; f < fLen; f++) {
549
+ const lineCoords = features[f].geometry.coordinates;
550
+ for (let i = 0, len = lineCoords.length - 1; i < len; i++) {
551
+ // GeoJSON coordinates are compatible with Position (number[]), and the project assumes [lng,lat]
552
+ fn(lineCoords[i] as Position, lineCoords[i + 1] as Position);
553
+ }
554
+ }
555
+ }
556
+
557
+ // Hot-path coordinate indexer used by both build/expand.
558
+ // Accepts explicit maps/arrays so callers can hoist them once.
559
+ private indexCoordinate(
560
+ coord: Position,
561
+ coordIndexMapLocal: Map<number, Map<number, number>>,
562
+ coordsLocal: Position[],
563
+ onNewIndex?: (index: number) => void,
564
+ ): number {
565
+ const lng = coord[0];
566
+ const lat = coord[1];
567
+
568
+ let latMap = coordIndexMapLocal.get(lng);
569
+ if (latMap === undefined) {
570
+ latMap = new Map<number, number>();
571
+ coordIndexMapLocal.set(lng, latMap);
572
+ }
573
+
574
+ let idx = latMap.get(lat);
575
+ if (idx === undefined) {
576
+ idx = coordsLocal.length;
577
+ coordsLocal.push(coord);
578
+ latMap.set(lat, idx);
579
+ if (onNewIndex) onNewIndex(idx);
580
+ }
581
+
582
+ return idx;
583
+ }
584
+
368
585
  }
369
586
 
370
587
  export { TerraRoute, createCheapRuler, haversineDistance }