@osmix/router 0.0.11 → 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.
Files changed (62) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +138 -53
  4. package/dist/algorithms/bidirectional.d.ts +6 -9
  5. package/dist/algorithms/bidirectional.d.ts.map +1 -1
  6. package/dist/algorithms/bidirectional.js +84 -104
  7. package/dist/algorithms/bidirectional.js.map +1 -1
  8. package/dist/algorithms/index.d.ts +2 -2
  9. package/dist/algorithms/index.d.ts.map +1 -1
  10. package/dist/algorithms/index.js +1 -1
  11. package/dist/algorithms/index.js.map +1 -1
  12. package/dist/algorithms/shortest-path.d.ts +2 -1
  13. package/dist/algorithms/shortest-path.d.ts.map +1 -1
  14. package/dist/algorithms/shortest-path.js +9 -7
  15. package/dist/algorithms/shortest-path.js.map +1 -1
  16. package/dist/binary-heap.d.ts +5 -0
  17. package/dist/binary-heap.d.ts.map +1 -1
  18. package/dist/binary-heap.js +11 -4
  19. package/dist/binary-heap.js.map +1 -1
  20. package/dist/graph.d.ts +35 -7
  21. package/dist/graph.d.ts.map +1 -1
  22. package/dist/graph.js +161 -119
  23. package/dist/graph.js.map +1 -1
  24. package/dist/index.d.ts +9 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/router.d.ts +1 -1
  29. package/dist/router.d.ts.map +1 -1
  30. package/dist/router.js +6 -3
  31. package/dist/router.js.map +1 -1
  32. package/dist/topology-stats.d.ts +37 -0
  33. package/dist/topology-stats.d.ts.map +1 -0
  34. package/dist/topology-stats.js +62 -0
  35. package/dist/topology-stats.js.map +1 -0
  36. package/dist/types.d.ts +15 -2
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils.d.ts +1 -1
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/utils.js +1 -7
  41. package/dist/utils.js.map +1 -1
  42. package/package.json +32 -29
  43. package/src/algorithms/bidirectional.ts +124 -136
  44. package/src/algorithms/index.ts +10 -10
  45. package/src/algorithms/shortest-path.ts +96 -92
  46. package/src/binary-heap.ts +133 -129
  47. package/src/graph.ts +389 -355
  48. package/src/index.ts +14 -9
  49. package/src/router.ts +221 -233
  50. package/src/topology-stats.ts +84 -0
  51. package/src/types.ts +102 -87
  52. package/src/utils.ts +67 -74
  53. package/test/algorithms.test.ts +396 -353
  54. package/test/binary-heap.test.ts +27 -0
  55. package/test/limits.test.ts +82 -0
  56. package/test/no-shared-array-buffer.test.ts +37 -0
  57. package/test/router.bench.ts +171 -188
  58. package/test/router.test.ts +685 -726
  59. package/test/spatial-capability.test.ts +23 -0
  60. package/test/topology-stats.test.ts +82 -0
  61. package/tsconfig.build.json +6 -3
  62. package/tsconfig.json +7 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @osmix/router
2
2
 
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 66eb7cd: `bidirectional` is now bidirectional Dijkstra: it searches backward over incoming edges, returns an optimal path and respects one-way edges. It was a breadth-first search that could return a longer path. Routing algorithms take an optional `RoutingAlgorithmContext` (`reverseGraph`, `maxSpeedMps`), which `Router` supplies; `RoutingGraph` adds `getIncomingEdges` and `maxSpeedMps`, and A*'s time heuristic uses the graph's maximum speed.
8
+ - 66eb7cd: Add `routingTopologyStats`, which counts a routing network's nodes, edges and components from its ways without building a `RoutingGraph`.
9
+
10
+ ### Patch Changes
11
+
12
+ - 466adc0: Share one-way normalization between exact reconciliation, fuzzy way matching, and routing. Respect explicit `no`, `false`, and `0` on roundabouts, recognize equivalent supported aliases, and compare reversed geometry consistently. Prevent unsupported values from being treated as direction-equivalent during matching while retaining the router's documented fallback behavior.
13
+
14
+ Block fuzzy matches when endpoint geometry cannot establish the orientation needed to compare one-way travel, including closed one-way roundabouts. Bidirectional ways without recognized direction-sensitive tags remain eligible.
15
+
16
+ - 8be5c80: Allow Osmix to use its ArrayBuffer fallback when SharedArrayBuffer is unavailable, so browser
17
+ applications no longer need to install a global SharedArrayBuffer shim.
18
+ - cd987f5: Preserve input topology during merges, conservatively reconcile compatible patch entities with the base,
19
+ validate routing-sensitive references, and insert multiple intersections in way order. Within-file duplicate
20
+ scans in the Merge app are now diagnostic only; regenerate older merged PBFs from their source inputs. Correct
21
+ the router priority queue so shortest-path searches visit lower-cost states first, and honor the one-way
22
+ direction implied by OSM roundabouts plus reverse one-way (`oneway=-1`) tags.
23
+
24
+ Restore the original 1-meter matching behavior as explicit, cross-dataset fuzzy conflation for imported data.
25
+ Callers select transferable properties independently from patch-network attachment; exact merge behavior
26
+ remains the default. Unique, high-confidence pedestrian and one-to-one-way matches can apply automatically,
27
+ while routing properties, motor roads, ambiguity, relation involvement, and uncertain geometry require review.
28
+ Grade conflicts, restrictions, protected tags, dangling references, way collapse, and base-topology rewrites
29
+ remain blocked. Matching candidates and their evidence are inspectable with `discoverConflationCandidates`;
30
+ merge plans carry the CAR/WALK topology diagnostics, and the Merge app reviews the plan before applying it.
31
+
32
+ - Updated dependencies [e4785fe]
33
+ - Updated dependencies [df04f92]
34
+ - Updated dependencies [e4785fe]
35
+ - Updated dependencies [66eb7cd]
36
+ - Updated dependencies [466adc0]
37
+ - Updated dependencies [66eb7cd]
38
+ - Updated dependencies [66eb7cd]
39
+ - Updated dependencies [8be5c80]
40
+ - @osmix/shared@0.2.0
41
+ - @osmix/core@0.3.0
42
+ - @osmix/types@0.2.0
43
+ - @osmix/geo@0.1.1
44
+
45
+ ## 0.0.14
46
+
47
+ ### Patch Changes
48
+
49
+ - c7a5a35: Harden parsing, loading, changeset generation, worker orchestration, and package verification across Osmix.
50
+
51
+ - `@osmix/pbf`, `@osmix/json`, and `@osmix/load` now validate PBF framing and decompression limits, normalize every supported input form, apply timestamp granularity consistently, translate tags before filtering, remove dangling filtered references, and build every requested spatial index.
52
+ - `@osmix/core` now returns `null` for unknown node-coordinate lookups, safely handles incomplete geometry, caches relation-to-way membership, and avoids repeated ID lookups and per-entity object allocation during sorted iteration.
53
+ - `@osmix/change` now preserves relation references during deduplication, escapes OSC XML attributes, applies changesets non-destructively, allocates collision-free IDs for empty or unsorted data, and resolves pending intersection geometry safely. Its intersection runtime is now vendored and strictly typed, removing the upstream test/build dependency graph.
54
+ - `osmix` now preserves sliced typed-array views during transfer, disposes and terminates owned workers reliably, uses collision-safe worker registries, and supports all documented PBF input forms across local and remote APIs.
55
+ - `@osmix/geojson`, `@osmix/vt`, and `@osmix/shortbread` now consume nullable or read-only geometry data without aliasing or mutation hazards.
56
+ - `@osmix/gtfs` and `@osmix/router` documentation now uses the current archive, routing, transfer, and distance APIs.
57
+ - `@osmix/shared` and `@osmix/test-utils` add executable package smoke coverage for byte streams and checked-in fixtures.
58
+ - Public package examples are classified as compilable or schematic and are checked against current exports and argument types.
59
+
60
+ - Updated dependencies [368d103]
61
+ - Updated dependencies [368d103]
62
+ - Updated dependencies [c7a5a35]
63
+ - @osmix/core@0.2.0
64
+ - @osmix/geo@0.1.0
65
+ - @osmix/types@0.1.0
66
+
67
+ ## 0.0.13
68
+
69
+ ### Patch Changes
70
+
71
+ - Updated dependencies [d67e38d]
72
+ - @osmix/core@0.1.10
73
+
74
+ ## 0.0.12
75
+
76
+ ### Patch Changes
77
+
78
+ - Updated dependencies [6144903]
79
+ - @osmix/core@0.1.9
80
+
3
81
  ## 0.0.11
4
82
 
5
83
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Conveyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,8 +5,9 @@
5
5
  ## Highlights
6
6
 
7
7
  - Builds a directed graph from OSM ways and nodes
8
+ - Respects explicit one-way tags and the one-way direction implied by `junction=roundabout`
8
9
  - Configurable highway type filtering
9
- - Multiple routing algorithms (Dijkstra, A*, bidirectional search)
10
+ - Multiple routing algorithms (Dijkstra, A\*, bidirectional search)
10
11
  - Support for both distance and time-based routing
11
12
  - Returns detailed route information including coordinates, way IDs, and node IDs
12
13
  - Serializable graph format for Web Worker support
@@ -14,35 +15,34 @@
14
15
  ## Installation
15
16
 
16
17
  ```sh
17
- bun install @osmix/router
18
+ pnpm add @osmix/router
18
19
  ```
19
20
 
20
21
  ## Usage
21
22
 
22
- ```ts
23
- import { RoutingGraph, Router, findNearestNodeOnGraph } from "@osmix/router"
24
- import { Osm } from "@osmix/core"
23
+ ```ts check-docs
24
+ import { Osm, Router, RoutingGraph } from "osmix";
25
25
 
26
- const osm = new Osm()
26
+ const osm = new Osm();
27
27
  // ... load OSM data into osm ...
28
28
 
29
29
  // Build routing graph
30
- const graph = new RoutingGraph(osm)
30
+ const graph = new RoutingGraph(osm);
31
31
 
32
32
  // Snap coordinates to nearest routable nodes
33
- const from = findNearestNodeOnGraph(osm, graph, [-73.989, 40.733], 0.5)
34
- const to = findNearestNodeOnGraph(osm, graph, [-73.988, 40.734], 0.5)
33
+ const from = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500);
34
+ const to = graph.findNearestRoutableNode(osm, [-73.988, 40.734], 500);
35
35
 
36
36
  if (from && to) {
37
- const router = new Router(osm, graph)
38
- const path = router.route(from.nodeIndex, to.nodeIndex)
39
-
40
- if (path) {
41
- const result = router.buildResult(path)
42
- console.log(result.coordinates) // Array of [lon, lat] coordinates
43
- console.log(result.wayIndexes) // Array of way indexes used
44
- console.log(result.nodeIndexes) // Array of node indexes for turns
45
- }
37
+ const router = new Router(osm, graph);
38
+ const path = router.route(from.nodeIndex, to.nodeIndex);
39
+
40
+ if (path) {
41
+ const result = router.buildResult(path);
42
+ console.log(result.coordinates); // Array of [lon, lat] coordinates
43
+ console.log(result.wayIndexes); // Array of way indexes used
44
+ console.log(result.nodeIndexes); // Array of node indexes for turns
45
+ }
46
46
  }
47
47
  ```
48
48
 
@@ -52,53 +52,101 @@ if (from && to) {
52
52
 
53
53
  Build and manage a routing graph from OSM data. The graph uses a CSR (Compressed Sparse Row) format for efficient memory usage and cache locality.
54
54
 
55
- ```ts
56
- import { RoutingGraph, defaultHighwayFilter } from "@osmix/router"
55
+ ```ts check-docs osm
56
+ import { RoutingGraph, defaultHighwayFilter } from "osmix";
57
57
 
58
58
  // Build from OSM data
59
- const graph = new RoutingGraph(osm, defaultHighwayFilter)
59
+ const graph = new RoutingGraph(osm, defaultHighwayFilter);
60
+ const nodeIndex = 0;
60
61
 
61
62
  // Properties
62
- graph.size // Number of routable nodes
63
- graph.edges // Total edge count
64
- graph.isRouteable(nodeIndex) // Check if node is routable
65
- graph.isIntersection(nodeIndex) // Check if node is an intersection
66
- graph.getEdges(nodeIndex) // Get outgoing edges from node
63
+ graph.size; // Number of node slots: every node in the dataset, routable or not
64
+ graph.edges; // Total edge count
65
+ graph.isRoutable(nodeIndex); // Check if node is routable
66
+ graph.isIntersection(nodeIndex); // Check if node is an intersection
67
+ graph.getEdges(nodeIndex); // Get outgoing edges from node
68
+ graph.getIncomingEdges(nodeIndex); // Get incoming edges (targetNodeIndex is the source)
69
+ graph.maxSpeedMps; // Fastest edge speed, used to bound the A* time heuristic
67
70
  ```
68
71
 
69
- #### `findNearestRoutableNode(osm, point, maxKm)`
72
+ #### `findNearestRoutableNode(osm, point, maxDistanceM)`
70
73
 
71
- Snap a geographic coordinate to the nearest routable node.
74
+ Snap a geographic coordinate to the nearest routable node. The radius is in meters. It snaps to graph nodes only, not to points along an edge. It needs the all-node spatial index (the Full load profile) and throws `SpatialIndexNotBuiltError` without it.
72
75
 
73
- ```ts
74
- const nearest = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 0.5)
76
+ ```ts check-docs router-context
77
+ const nearest = graph.findNearestRoutableNode(osm, [-73.989, 40.733], 500);
75
78
  if (nearest) {
76
- console.log(nearest.nodeIndex) // Internal node index
77
- console.log(nearest.coordinates) // Snapped [lon, lat]
78
- console.log(nearest.distance) // Distance from input point (meters)
79
+ console.log(nearest.nodeIndex); // Internal node index
80
+ console.log(nearest.coordinates); // Snapped [lon, lat]
81
+ console.log(nearest.distance); // Distance from input point (meters)
79
82
  }
80
83
  ```
81
84
 
82
85
  **Constructor parameters:**
83
86
 
84
87
  - `osm` - The `@osmix/core` dataset.
85
- - `filter` - Optional function `(tags?) => boolean` to select routable ways. Default: common vehicle highways.
86
- - `defaultSpeeds` - Optional speed limits (km/h) by highway type.
88
+ - `filter` - Optional function `(tags?) => boolean` to select routable ways. Default: `defaultHighwayFilter` (motorway to service roads). `defaultPedestrianFilter` selects footways, paths, cycleways, bridleways and steps.
89
+ - `defaultSpeeds` - Optional speed limits (km/h) by highway type. Default: `DEFAULT_SPEEDS`. A way's `maxspeed` tag takes priority (`parseMaxSpeed` reads numbers, `mph`, `walk` and `none`). Unknown types use 50 km/h.
90
+
91
+ Ways with a ref to a node that is not in the dataset (for example, after a bbox extract) keep their other segments. The segments that touch the missing node are not added.
92
+
93
+ Way directionality is currently graph-wide: custom filters can select pedestrian ways, but they
94
+ do not disable `oneway` or implicit roundabout direction. A custom filter does not supply a complete pedestrian or turn-restriction policy. The [optional local R5 comparison](../osmix/test/r5/README.md) evaluates declared fixture checks against a specific R5 checkout; coordinate linking and diagnostic observations do not establish complete modal legality.
95
+
96
+ #### Way direction
97
+
98
+ Graph construction and exact/fuzzy way matching share `normalizedWayDirection(tags)`, available from
99
+ `osmix` or `@osmix/types/way-direction`. It returns `OsmWayDirection`: `forward`, `reverse`, `both`, or
100
+ `unsupported`. Forward follows the way's ordered node references; reverse travels against that order.
101
+
102
+ | `oneway` value | Normalized direction |
103
+ | ------------------------------------------- | -------------------------------- |
104
+ | `yes`, `true`, `1` | `forward` |
105
+ | `reverse`, `-1` | `reverse` |
106
+ | `no`, `false`, `0` | `both`, including on roundabouts |
107
+ | Absent or empty, with `junction=roundabout` | `forward` |
108
+ | Absent or empty, on other ways | `both` |
109
+ | Any other nonempty value | `unsupported` |
110
+
111
+ One-way aliases are case-insensitive, and numeric values normalize like their string equivalents. Values
112
+ are not trimmed: `" yes "` is unsupported. The roundabout implication requires the literal
113
+ `junction=roundabout` value.
114
+
115
+ ```ts check-docs
116
+ import { normalizedWayDirection } from "osmix";
117
+
118
+ const direction = normalizedWayDirection({ junction: "roundabout", oneway: "0" });
119
+ console.log(direction); // "both"
120
+ ```
121
+
122
+ Unsupported values such as `reversible` and `alternating` prevent exact or fuzzy way matching from treating
123
+ two ways as direction-equivalent, even when their values are identical. The routing graph retains its existing
124
+ approximation for such values: forward on roundabouts and both directions on other ways. This fallback does
125
+ not model their actual rules. Conditional, time-dependent, lane-specific, and mode-specific one-way rules are
126
+ not evaluated by this normalization.
127
+
128
+ Fuzzy matching also needs a reliable geometry orientation before comparing one-way travel. It blocks one-way
129
+ candidates whose endpoints fit equally well in either order, including closed one-way loops. This matching
130
+ limit does not change their routing graph: graph edges always follow the stored references and normalized
131
+ tag direction.
87
132
 
88
133
  #### Serialization (Web Worker support)
89
134
 
90
135
  `RoutingGraph` can be serialized and transferred between Web Workers:
91
136
 
92
- ```ts
137
+ ```ts check-docs router-transfer
138
+ import { getTransferableBuffers, RoutingGraph } from "@osmix/router";
139
+
93
140
  // Build graph and get transferables
94
- const graph = new RoutingGraph(osm)
95
- const transferables = graph.transferables()
141
+ const transferables = graph.transferables();
142
+ const buffers = getTransferableBuffers(transferables);
96
143
 
97
- // Transfer to worker (zero-copy with SharedArrayBuffer)
98
- worker.postMessage(transferables)
144
+ // Transfer to worker without cloning ArrayBuffers
145
+ worker.postMessage(transferables, buffers);
99
146
 
100
147
  // Reconstruct in worker
101
- const reconstructed = new RoutingGraph(transferables)
148
+ const reconstructed = new RoutingGraph(transferables);
149
+ console.log(reconstructed.size);
102
150
  ```
103
151
 
104
152
  The `transferables()` method returns an object containing:
@@ -112,38 +160,75 @@ The `transferables()` method returns an object containing:
112
160
 
113
161
  High-level routing interface.
114
162
 
115
- ```ts
116
- const router = new Router(osm, graph, { algorithm: "astar", metric: "time" })
163
+ Schematic construction with route options:
164
+
165
+ ```ts schematic
166
+ const router = new Router(osm, graph, { algorithm: "astar", metric: "time" });
117
167
  ```
118
168
 
119
169
  **Methods:**
120
170
 
121
171
  - `route(fromNodeIndex, toNodeIndex, options?)` - Find path between nodes. Returns `PathSegment[]` or `null`.
122
- - `buildResult(path)` - Convert path to `RouteResult` with coordinates and metadata.
172
+ - `buildResult(path, options?)` - Convert path to `RouteResult` with `coordinates`, `wayIndexes` and `nodeIndexes`.
173
+ - `getRouteStatistics(path)` - Total `distance` (meters) and `time` (seconds).
174
+ - `getRoutePathInfo(path)` - `segments` (consecutive ways with the same name merged) and `turnPoints`.
123
175
 
124
176
  **Options:**
125
177
 
126
178
  - `algorithm` - `"dijkstra"` | `"astar"` | `"bidirectional"` (default: `"astar"`)
127
179
  - `metric` - `"distance"` | `"time"` (default: `"distance"`)
180
+ - `includeStats` - Add `distance` and `time` to the `RouteResult` (default: `false`)
181
+ - `includePathInfo` - Add `segments` and `turnPoints` to the `RouteResult` (default: `false`)
182
+
183
+ ### `routingTopologyStats(source, filter)`
184
+
185
+ Gives the counts a `RoutingGraph` built with `filter` would have (`nodes`, `routableNodes`, directed `edges` and weakly connected `components`) without building the graph. `source` is anything with a `nodeCount` and a `ways()` iterable of `{ tags, refs }`, so an `Osm` or a view of pending changes both work. Direction follows the graph, including the roundabout fallback.
186
+
187
+ ```ts schematic
188
+ const stats = routingTopologyStats({ nodeCount: osm.nodes.size, ways: () => osm.ways }, filter);
189
+ ```
128
190
 
129
191
  ### Algorithms
130
192
 
131
- | Algorithm | Optimal? | Speed | Best For |
132
- | --------------- | -------- | ------- | ------------------------------------- |
133
- | `dijkstra` | Yes | Slower | When you need guaranteed shortest path |
134
- | `astar` | Yes | Fast | Point-to-point queries (default) |
135
- | `bidirectional` | No | Fastest | Quick connectivity checks |
193
+ | Algorithm | Optimal? | Search |
194
+ | --------------- | -------- | ----------------------------------------------------------------- |
195
+ | `dijkstra` | Yes | Expands in all directions from the start |
196
+ | `astar` | Yes | Guided toward the end by straight-line distance (default) |
197
+ | `bidirectional` | Yes | Dijkstra from both ends; the backward half follows incoming edges |
198
+
199
+ For the `time` metric, A* divides the straight-line distance by `graph.maxSpeedMps`, so the heuristic stays admissible when a `maxspeed` is above 130 km/h. `bidirectional` builds an index of incoming edges on its first use (4 bytes for each node plus 8 bytes for each edge).
200
+
201
+ A custom algorithm has the `RoutingAlgorithmFn` signature. Its last argument, `RoutingAlgorithmContext`, carries `reverseGraph` and `maxSpeedMps`.
202
+
203
+ ## Limitations
204
+
205
+ The router reads only the `highway`, `oneway`, `junction` and `maxspeed` tags. It does not model:
206
+
207
+ - Turn restrictions (relations)
208
+ - Access tags (`access`, `motor_vehicle`, `foot`, `bicycle` and more)
209
+ - Mode-specific, conditional or lane-specific one-way tags
210
+ - Turn costs or elevation
211
+ - A bicycle profile (`defaultPedestrianFilter` includes cycleways)
212
+
213
+ Memory and search:
214
+
215
+ - The graph uses 16 bytes for each directed edge, plus about 4.25 bytes for each node in the dataset, routable or not. The build reads the routable ways twice and writes straight into these arrays, so its peak is the final size plus one 4-byte cursor for each node.
216
+ - Node, edge and way indexes are `Uint32`. Distances and times are `Float32`.
217
+ - A search has no distance or time limit, and you cannot cancel it. If no route exists, the search examines the whole connected network before it returns `null`.
218
+ - The worker keeps one graph (one filter) for each dataset.
219
+
220
+ See [docs/limits.md](../../docs/limits.md#router-limits) for measured graph sizes.
136
221
 
137
222
  ## Related Packages
138
223
 
139
224
  - [`@osmix/core`](../core/README.md) - In-memory OSM index with spatial queries.
225
+ - [`@osmix/load`](../load/README.md) - Load PBF data into `Osm` indexes (`fromPbf`).
140
226
  - [`@osmix/shared`](../shared/README.md) - Haversine distance and coordinate utilities.
141
227
  - [`osmix`](../osmix/README.md) - High-level API with worker support for routing.
142
228
 
143
229
  ## Development
144
230
 
145
- - `bun run test packages/router`
146
- - `bun run lint packages/router`
147
- - `bun run typecheck packages/router`
231
+ - `pnpm run test packages/router`
232
+ - `pnpm exec oxlint packages/router`
148
233
 
149
- Run `bun run check` at the repo root before publishing to ensure formatting, lint, and type coverage.
234
+ Run `pnpm run verify` at the repo root before publishing.
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Bidirectional breadth-first search algorithm.
2
+ * Bidirectional Dijkstra.
3
3
  *
4
- * Searches from both start and end simultaneously, terminating when the
5
- * frontiers meet. Very fast for finding any path, but does not guarantee
6
- * the shortest path. Use Dijkstra or A* when optimality is required.
4
+ * Runs Dijkstra forward from the start over outgoing edges and backward from
5
+ * the end over incoming edges, and stops when neither search can improve the
6
+ * best meeting point. Returns an optimal path and respects one-way edges.
7
7
  *
8
8
  * @module
9
9
  */
10
10
  import type { RoutingAlgorithmFn } from "../types.ts";
11
11
  /**
12
- * Bidirectional BFS - fast path finding from both ends.
13
- *
14
- * Uses breadth-first search from both start and end, terminating when
15
- * the frontiers meet. Very fast for finding any path, though not always
16
- * the optimal shortest path. Use Dijkstra or A* if optimality is required.
12
+ * Bidirectional Dijkstra. Requires `context.reverseGraph`, which `Router`
13
+ * supplies from `RoutingGraph.getIncomingEdges`.
17
14
  */
18
15
  export declare const bidirectional: RoutingAlgorithmFn;
19
16
  //# sourceMappingURL=bidirectional.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bidirectional.d.ts","sourceRoot":"","sources":["../../src/algorithms/bidirectional.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAe,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,kBA+H3B,CAAA"}
1
+ {"version":3,"file":"bidirectional.d.ts","sourceRoot":"","sources":["../../src/algorithms/bidirectional.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAA0B,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAQ9E;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,kBA+G3B,CAAC"}
@@ -1,128 +1,108 @@
1
1
  /**
2
- * Bidirectional breadth-first search algorithm.
2
+ * Bidirectional Dijkstra.
3
3
  *
4
- * Searches from both start and end simultaneously, terminating when the
5
- * frontiers meet. Very fast for finding any path, but does not guarantee
6
- * the shortest path. Use Dijkstra or A* when optimality is required.
4
+ * Runs Dijkstra forward from the start over outgoing edges and backward from
5
+ * the end over incoming edges, and stops when neither search can improve the
6
+ * best meeting point. Returns an optimal path and respects one-way edges.
7
7
  *
8
8
  * @module
9
9
  */
10
+ import { BinaryHeap } from "../binary-heap.js";
10
11
  /**
11
- * Bidirectional BFS - fast path finding from both ends.
12
- *
13
- * Uses breadth-first search from both start and end, terminating when
14
- * the frontiers meet. Very fast for finding any path, though not always
15
- * the optimal shortest path. Use Dijkstra or A* if optimality is required.
12
+ * Bidirectional Dijkstra. Requires `context.reverseGraph`, which `Router`
13
+ * supplies from `RoutingGraph.getIncomingEdges`.
16
14
  */
17
- export const bidirectional = (graph, start, end, getWeight) => {
15
+ export const bidirectional = (graph, start, end, getWeight, _getCoord, _metric, context) => {
18
16
  if (start === end)
19
17
  return [{ nodeIndex: start, cost: 0 }];
20
- // Forward search state
21
- const fDist = new Map();
18
+ const reverseGraph = context?.reverseGraph;
19
+ if (!reverseGraph) {
20
+ throw Error("Bidirectional routing needs context.reverseGraph (incoming edges). " +
21
+ "Router passes RoutingGraph.getIncomingEdges.");
22
+ }
23
+ const fDist = new Map([[start, 0]]);
24
+ const bDist = new Map([[end, 0]]);
22
25
  const fPrev = new Map();
23
- const fQueue = [start];
24
- // Backward search state
25
- const bDist = new Map();
26
- const bPrev = new Map();
27
- const bQueue = [end];
28
- fDist.set(start, 0);
29
- bDist.set(end, 0);
30
- let meetNode = null;
31
- while (fQueue.length > 0 && bQueue.length > 0) {
32
- // Expand forward
33
- if (fQueue.length > 0) {
34
- const current = fQueue.shift();
35
- const currentD = fDist.get(current);
36
- // Check if backward search reached this node
37
- if (bDist.has(current)) {
38
- meetNode = current;
39
- break;
26
+ const bNext = new Map();
27
+ const fHeap = new BinaryHeap();
28
+ const bHeap = new BinaryHeap();
29
+ const fClosed = new Set();
30
+ const bClosed = new Set();
31
+ fHeap.push(start, 0);
32
+ bHeap.push(end, 0);
33
+ let best = Number.POSITIVE_INFINITY;
34
+ let meet = -1;
35
+ const relax = (current, edges, dist, otherDist, closed, heap, record) => {
36
+ const currentD = dist.get(current);
37
+ for (const edge of edges) {
38
+ const neighbor = edge.targetNodeIndex;
39
+ if (closed.has(neighbor))
40
+ continue;
41
+ const cost = currentD + getWeight(edge);
42
+ const existing = dist.get(neighbor);
43
+ if (existing !== undefined && cost >= existing)
44
+ continue;
45
+ dist.set(neighbor, cost);
46
+ record(neighbor, edge, cost);
47
+ heap.push(neighbor, cost);
48
+ const other = otherDist.get(neighbor);
49
+ if (other !== undefined && cost + other < best) {
50
+ best = cost + other;
51
+ meet = neighbor;
40
52
  }
41
- for (const edge of graph(current)) {
42
- const neighbor = edge.targetNodeIndex;
43
- if (fDist.has(neighbor))
44
- continue;
45
- const newD = currentD + getWeight(edge);
46
- fDist.set(neighbor, newD);
53
+ }
54
+ };
55
+ while (fHeap.size > 0 && bHeap.size > 0) {
56
+ // Stop once no unsettled node can lie on a path shorter than `best`.
57
+ if (fHeap.peekPriority() + bHeap.peekPriority() >= best)
58
+ break;
59
+ if (fHeap.peekPriority() <= bHeap.peekPriority()) {
60
+ const current = fHeap.pop();
61
+ if (fClosed.has(current))
62
+ continue;
63
+ fClosed.add(current);
64
+ relax(current, graph(current), fDist, bDist, fClosed, fHeap, (neighbor, edge, cost) => {
47
65
  fPrev.set(neighbor, {
48
66
  nodeIndex: neighbor,
49
67
  wayIndex: edge.wayIndex,
50
68
  previousNodeIndex: current,
51
- cost: newD,
69
+ cost,
52
70
  });
53
- fQueue.push(neighbor);
54
- if (bDist.has(neighbor)) {
55
- meetNode = neighbor;
56
- break;
57
- }
58
- }
59
- if (meetNode !== null)
60
- break;
71
+ });
61
72
  }
62
- // Expand backward
63
- if (bQueue.length > 0) {
64
- const current = bQueue.shift();
65
- const currentD = bDist.get(current);
66
- // Check if forward search reached this node
67
- if (fDist.has(current)) {
68
- meetNode = current;
69
- break;
70
- }
71
- for (const edge of graph(current)) {
72
- const neighbor = edge.targetNodeIndex;
73
- if (bDist.has(neighbor))
74
- continue;
75
- const newD = currentD + getWeight(edge);
76
- bDist.set(neighbor, newD);
77
- bPrev.set(neighbor, {
78
- nodeIndex: neighbor,
79
- wayIndex: edge.wayIndex,
80
- previousNodeIndex: current,
81
- cost: newD,
82
- });
83
- bQueue.push(neighbor);
84
- if (fDist.has(neighbor)) {
85
- meetNode = neighbor;
86
- break;
87
- }
88
- }
89
- if (meetNode !== null)
90
- break;
73
+ else {
74
+ const current = bHeap.pop();
75
+ if (bClosed.has(current))
76
+ continue;
77
+ bClosed.add(current);
78
+ relax(current, reverseGraph(current), bDist, fDist, bClosed, bHeap, (neighbor, edge) => {
79
+ bNext.set(neighbor, { nextNodeIndex: current, wayIndex: edge.wayIndex });
80
+ });
91
81
  }
92
82
  }
93
- if (meetNode === null)
83
+ if (meet === -1)
94
84
  return null;
95
- // Build forward path: start -> meet
96
- const forward = [];
97
- let curr = meetNode;
98
- while (curr !== undefined && curr !== start) {
99
- const seg = fPrev.get(curr);
100
- if (!seg)
101
- break;
102
- forward.unshift(seg);
103
- curr = seg.previousNodeIndex;
85
+ // Forward half: start → meet.
86
+ const path = [];
87
+ let node = meet;
88
+ while (node !== start) {
89
+ const segment = fPrev.get(node);
90
+ path.unshift(segment);
91
+ node = segment.previousNodeIndex;
104
92
  }
105
- forward.unshift({ nodeIndex: start, cost: 0 });
106
- // Build backward path: meet -> end (reverse direction)
107
- // bDist represents distance FROM end TO node, so it decreases as we approach end.
108
- // Correct cost = forward cost to meet + (backward dist at meet - backward dist at curr)
109
- const meetForwardCost = fDist.get(meetNode);
110
- const meetBackwardCost = bDist.get(meetNode);
111
- curr = meetNode;
112
- while (curr !== undefined && curr !== end) {
113
- const seg = bPrev.get(curr);
114
- if (!seg)
115
- break;
116
- curr = seg.previousNodeIndex;
117
- if (curr !== undefined) {
118
- forward.push({
119
- nodeIndex: curr,
120
- wayIndex: seg.wayIndex,
121
- previousNodeIndex: seg.nodeIndex,
122
- cost: meetForwardCost + meetBackwardCost - (bDist.get(curr) ?? 0),
123
- });
124
- }
93
+ path.unshift({ nodeIndex: start, cost: 0 });
94
+ // Backward half: meet → end, with costs measured from the start.
95
+ node = meet;
96
+ while (node !== end) {
97
+ const step = bNext.get(node);
98
+ path.push({
99
+ nodeIndex: step.nextNodeIndex,
100
+ wayIndex: step.wayIndex,
101
+ previousNodeIndex: node,
102
+ cost: best - bDist.get(step.nextNodeIndex),
103
+ });
104
+ node = step.nextNodeIndex;
125
105
  }
126
- return forward;
106
+ return path;
127
107
  };
128
108
  //# sourceMappingURL=bidirectional.js.map