@osmix/core 0.1.10 → 0.2.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 (70) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +86 -69
  4. package/dist/contracts.d.ts +26 -0
  5. package/dist/contracts.d.ts.map +1 -0
  6. package/dist/contracts.js +1 -0
  7. package/dist/contracts.js.map +1 -0
  8. package/dist/entities.d.ts +1 -1
  9. package/dist/entities.d.ts.map +1 -1
  10. package/dist/entities.js +2 -7
  11. package/dist/entities.js.map +1 -1
  12. package/dist/ids.d.ts +2 -0
  13. package/dist/ids.d.ts.map +1 -1
  14. package/dist/ids.js +23 -15
  15. package/dist/ids.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +0 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/mocks.d.ts.map +1 -1
  21. package/dist/mocks.js.map +1 -1
  22. package/dist/nodes.d.ts +9 -3
  23. package/dist/nodes.d.ts.map +1 -1
  24. package/dist/nodes.js +6 -13
  25. package/dist/nodes.js.map +1 -1
  26. package/dist/osm.d.ts +1 -1
  27. package/dist/osm.d.ts.map +1 -1
  28. package/dist/osm.js +1 -4
  29. package/dist/osm.js.map +1 -1
  30. package/dist/relations.d.ts +3 -2
  31. package/dist/relations.d.ts.map +1 -1
  32. package/dist/relations.js +15 -9
  33. package/dist/relations.js.map +1 -1
  34. package/dist/stringtable.d.ts.map +1 -1
  35. package/dist/stringtable.js +4 -13
  36. package/dist/stringtable.js.map +1 -1
  37. package/dist/tags.d.ts +1 -1
  38. package/dist/tags.d.ts.map +1 -1
  39. package/dist/tags.js.map +1 -1
  40. package/dist/typed-arrays.d.ts.map +1 -1
  41. package/dist/typed-arrays.js.map +1 -1
  42. package/dist/ways.d.ts +2 -1
  43. package/dist/ways.d.ts.map +1 -1
  44. package/dist/ways.js +18 -18
  45. package/dist/ways.js.map +1 -1
  46. package/package.json +46 -31
  47. package/src/contracts.ts +28 -0
  48. package/src/entities.ts +179 -194
  49. package/src/ids.ts +228 -235
  50. package/src/index.ts +12 -9
  51. package/src/mocks.ts +114 -113
  52. package/src/nodes.ts +364 -397
  53. package/src/osm.ts +196 -207
  54. package/src/relations.ts +530 -558
  55. package/src/stringtable.ts +174 -183
  56. package/src/tags.ts +263 -271
  57. package/src/typed-arrays.ts +184 -194
  58. package/src/types/geoflatbush.d.ts +28 -33
  59. package/src/types/geokdbush.d.ts +28 -33
  60. package/src/types/kdbush.d.ts +34 -34
  61. package/src/ways.ts +381 -405
  62. package/test/ids.bench.ts +29 -0
  63. package/test/ids.test.ts +84 -0
  64. package/test/memory.test.ts +29 -30
  65. package/test/relations.bench.ts +27 -0
  66. package/test/relations.test.ts +101 -0
  67. package/test/tags-search.test.ts +76 -56
  68. package/test/typed-arrays.test.ts +100 -99
  69. package/tsconfig.build.json +6 -6
  70. package/tsconfig.json +7 -7
package/src/ways.ts CHANGED
@@ -1,410 +1,386 @@
1
- import type { OsmPbfWay } from "@osmix/pbf"
2
- import type { ContentHasher } from "@osmix/shared/content-hasher"
3
- import type { GeoBbox2D, LonLat, OsmTags, OsmWay } from "@osmix/shared/types"
4
- import Flatbush from "flatbush"
5
- import { around as geoAround } from "geoflatbush"
6
- import { Entities, type EntitiesTransferables } from "./entities.ts"
7
- import { type IdOrIndex, Ids } from "./ids.ts"
8
- import type { Nodes } from "./nodes.ts"
9
- import type StringTable from "./stringtable.ts"
10
- import { Tags } from "./tags.ts"
1
+ import type { OsmPbfWay } from "@osmix/pbf";
2
+ import type { ContentHasher } from "@osmix/shared/content-hasher";
3
+ import type { GeoBbox2D, LonLat, OsmTags, OsmWay } from "@osmix/types";
4
+ import Flatbush from "flatbush";
5
+ import { around as geoAround } from "geoflatbush";
6
+
7
+ import { Entities, type EntitiesTransferables } from "./entities.ts";
8
+ import { type IdOrIndex, Ids } from "./ids.ts";
9
+ import type { Nodes } from "./nodes.ts";
10
+ import type StringTable from "./stringtable.ts";
11
+ import { Tags } from "./tags.ts";
11
12
  import {
12
- BufferConstructor,
13
- type BufferType,
14
- IdArrayType,
15
- ResizeableTypedArray as RTA,
16
- } from "./typed-arrays.ts"
17
-
18
- export interface WaysTransferables<T extends BufferType = BufferType>
19
- extends EntitiesTransferables<T> {
20
- refStart: T
21
- refCount: T
22
- refs: T
23
- bbox: T
24
- /** Optional - can be rebuilt via buildSpatialIndex() */
25
- spatialIndex?: T
13
+ BufferConstructor,
14
+ type BufferType,
15
+ IdArrayType,
16
+ ResizeableTypedArray as RTA,
17
+ } from "./typed-arrays.ts";
18
+
19
+ export interface WaysTransferables<
20
+ T extends BufferType = BufferType,
21
+ > extends EntitiesTransferables<T> {
22
+ refStart: T;
23
+ refCount: T;
24
+ refs: T;
25
+ bbox: T;
26
+ /** Optional - can be rebuilt via buildSpatialIndex() */
27
+ spatialIndex?: T;
26
28
  }
27
29
 
28
30
  export class Ways extends Entities<OsmWay> {
29
- private spatialIndex: Flatbush = new Flatbush(1)
30
- // Track if spatial index was properly built (vs default empty)
31
- private spatialIndexBuilt = false
32
-
33
- private refStart: RTA<Uint32Array>
34
- private refCount: RTA<Uint16Array> // Maximum 2,000 nodes per way
35
-
36
- // Node IDs
37
- private refs: RTA<Float64Array>
38
-
39
- // Bounding box of the way in geographic coordinates
40
- private bbox: RTA<Float64Array>
41
-
42
- // Node reference index
43
- private nodes: Nodes
44
-
45
- /**
46
- * Create a new Ways index.
47
- */
48
- constructor(
49
- stringTable: StringTable,
50
- nodes: Nodes,
51
- transferables?: WaysTransferables,
52
- ) {
53
- if (transferables) {
54
- super("way", new Ids(transferables), new Tags(stringTable, transferables))
55
- this.refStart = RTA.from(Uint32Array, transferables.refStart)
56
- this.refCount = RTA.from(Uint16Array, transferables.refCount)
57
- this.refs = RTA.from(IdArrayType, transferables.refs)
58
- this.bbox = RTA.from(Float64Array, transferables.bbox)
59
- // Only load spatial index if provided (not stored in IndexedDB)
60
- if (transferables.spatialIndex?.byteLength) {
61
- this.spatialIndex = Flatbush.from(transferables.spatialIndex)
62
- this.spatialIndexBuilt = true
63
- }
64
- this.indexBuilt = true
65
- } else {
66
- super("way", new Ids(), new Tags(stringTable))
67
- this.refStart = new RTA(Uint32Array)
68
- this.refCount = new RTA(Uint16Array)
69
- this.refs = new RTA(IdArrayType)
70
- this.bbox = new RTA(Float64Array)
71
- }
72
- this.nodes = nodes
73
- }
74
-
75
- /**
76
- * Add a single way to the index.
77
- */
78
- addWay(way: OsmWay) {
79
- const wayIndex = this.addEntity(way.id, way.tags ?? {})
80
- this.refStart.push(this.refs.length)
81
- this.refCount.push(way.refs.length)
82
- for (const ref of way.refs) this.refs.push(ref)
83
- return wayIndex
84
- }
85
-
86
- /**
87
- * Bulk add ways directly from a PBF PrimitiveBlock.
88
- */
89
- addWays(
90
- ways: OsmPbfWay[],
91
- blockStringIndexMap: Uint32Array,
92
- filter?: (way: OsmWay) => OsmWay | null,
93
- ) {
94
- let added = 0
95
- for (const way of ways) {
96
- let prevRefId = 0
97
- const refs = way.refs.map((refId) => {
98
- prevRefId += refId
99
- return prevRefId
100
- })
101
- const filteredWay = filter
102
- ? filter({
103
- id: way.id,
104
- refs,
105
- tags: this.tags.getTagsFromIndices(way.keys, way.vals),
106
- })
107
- : null
108
- if (filter && filteredWay === null) continue
109
-
110
- const tagKeys: number[] = way.keys.map((key) => {
111
- const index = blockStringIndexMap[key]
112
- if (index === undefined) throw Error("Tag key not found")
113
- return index
114
- })
115
- const tagValues: number[] = way.vals.map((val) => {
116
- const index = blockStringIndexMap[val]
117
- if (index === undefined) throw Error("Tag value not found")
118
- return index
119
- })
120
-
121
- this.addEntity(way.id, tagKeys, tagValues)
122
- this.refStart.push(this.refs.length)
123
- this.refCount.push(filteredWay?.refs.length ?? refs.length)
124
- this.refs.pushMany(filteredWay?.refs ?? refs)
125
- added++
126
- }
127
- return added
128
- }
129
-
130
- /**
131
- * Compact the internal arrays to free up memory.
132
- */
133
- buildEntityIndex() {
134
- this.refStart.compact()
135
- this.refCount.compact()
136
- this.refs.compact()
137
- }
138
-
139
- /**
140
- * Build the spatial index for ways.
141
- * If bbox data already exists (e.g., loaded from storage), reuses it.
142
- */
143
- buildSpatialIndex() {
144
- if (!this.nodes.isReady()) throw Error("Node index is not ready.")
145
- if (this.size === 0) return this.spatialIndex
146
-
147
- this.spatialIndex = new Flatbush(
148
- this.size,
149
- 128,
150
- Float64Array,
151
- BufferConstructor,
152
- )
153
-
154
- // If bbox already has data (loaded from storage), use it directly
155
- const hasBboxData = this.bbox.length >= this.size * 4
156
- for (let i = 0; i < this.size; i++) {
157
- let minX: number
158
- let minY: number
159
- let maxX: number
160
- let maxY: number
161
-
162
- if (hasBboxData) {
163
- // Use stored bbox values
164
- minX = this.bbox.at(i * 4)
165
- minY = this.bbox.at(i * 4 + 1)
166
- maxX = this.bbox.at(i * 4 + 2)
167
- maxY = this.bbox.at(i * 4 + 3)
168
- } else {
169
- // Calculate bbox from node coordinates
170
- minX = Number.POSITIVE_INFINITY
171
- minY = Number.POSITIVE_INFINITY
172
- maxX = Number.NEGATIVE_INFINITY
173
- maxY = Number.NEGATIVE_INFINITY
174
- const start = this.refStart.at(i)
175
- const count = this.refCount.at(i)
176
- for (let j = start; j < start + count; j++) {
177
- const refId = this.refs.at(j)
178
- const [lon, lat] = this.nodes.getNodeLonLat({ id: refId })
179
- if (lon < minX) minX = lon
180
- if (lon > maxX) maxX = lon
181
- if (lat < minY) minY = lat
182
- if (lat > maxY) maxY = lat
183
- }
184
- this.bbox.push(minX)
185
- this.bbox.push(minY)
186
- this.bbox.push(maxX)
187
- this.bbox.push(maxY)
188
- }
189
- this.spatialIndex.add(minX, minY, maxX, maxY)
190
- }
191
- // Compact bbox if we just calculated it
192
- if (!hasBboxData) {
193
- this.bbox.compact()
194
- }
195
- this.spatialIndex.finish()
196
- this.spatialIndexBuilt = true
197
- return this.spatialIndex
198
- }
199
-
200
- /**
201
- * Check if the spatial index has been built.
202
- */
203
- hasSpatialIndex(): boolean {
204
- return this.spatialIndexBuilt
205
- }
206
-
207
- /**
208
- * Get the full way entity.
209
- */
210
- getFullEntity(index: number, id: number, tags?: OsmTags): OsmWay {
211
- return {
212
- id,
213
- refs: [...this.getRefIds(index)],
214
- tags,
215
- }
216
- }
217
-
218
- /**
219
- * Get the node IDs referenced by a way.
220
- */
221
- getRefIds(index: number): number[] {
222
- const start = this.refStart.at(index)
223
- const count = this.refCount.at(index)
224
- return Array.from(this.refs.slice(start, start + count))
225
- }
226
-
227
- /**
228
- * Get the bounding box of a way.
229
- */
230
- getEntityBbox(idOrIndex: IdOrIndex): GeoBbox2D {
231
- const index =
232
- "index" in idOrIndex ? idOrIndex.index : this.ids.idOrIndex(idOrIndex)[0]
233
- return [
234
- this.bbox.at(index * 4),
235
- this.bbox.at(index * 4 + 1),
236
- this.bbox.at(index * 4 + 2),
237
- this.bbox.at(index * 4 + 3),
238
- ]
239
- }
240
-
241
- /**
242
- * Get the coordinates of a way as a flat array.
243
- */
244
- getLine(index: number) {
245
- const count = this.refCount.at(index)
246
- const start = this.refStart.at(index)
247
- const line = new Float64Array(count * 2)
248
- for (let i = 0; i < count; i++) {
249
- const ref = this.refs.at(start + i)
250
- const [lon, lat] = this.nodes.getNodeLonLat({ id: ref })
251
- line[i * 2] = lon
252
- line[i * 2 + 1] = lat
253
- }
254
- return line
255
- }
256
-
257
- /**
258
- * Get the coordinates of a way as an array of [lon, lat] pairs.
259
- */
260
- getCoordinates(index: number): LonLat[] {
261
- const count = this.refCount.at(index)
262
- const start = this.refStart.at(index)
263
- const coords: [number, number][] = []
264
- for (let refIndex = start; refIndex < start + count; refIndex++) {
265
- const ref = this.refs.at(refIndex)
266
- const coord = this.nodes.getNodeLonLat({ id: ref })
267
- if (
268
- coord === undefined ||
269
- coord[0] === undefined ||
270
- coord[1] === undefined
271
- ) {
272
- throw Error(
273
- `Invalid coordinate for way id ${this.ids.at(index)}, index ${index}, node ref ${ref}, ref index ${refIndex}`,
274
- )
275
- }
276
- coords.push(coord)
277
- }
278
- return coords
279
- }
280
-
281
- /**
282
- * Find way indexes that intersect a bounding box.
283
- */
284
- intersects(bbox: GeoBbox2D, filterFn?: (index: number) => boolean): number[] {
285
- if (this.size === 0) return []
286
- return this.spatialIndex.search(
287
- bbox[0],
288
- bbox[1],
289
- bbox[2],
290
- bbox[3],
291
- filterFn,
292
- )
293
- }
294
-
295
- /**
296
- * Find way indexes near a point using great-circle distance.
297
- * @param lon - Longitude in degrees.
298
- * @param lat - Latitude in degrees.
299
- * @param maxResults - Maximum number of results to return.
300
- * @param maxDistanceKm - Maximum distance in kilometers.
301
- * @returns Array of way indexes sorted by distance.
302
- */
303
- neighbors(
304
- lon: number,
305
- lat: number,
306
- maxResults?: number,
307
- maxDistanceKm?: number,
308
- ): number[] {
309
- if (this.size === 0) return []
310
- // Use geoflatbush for proper geographic distance calculations
311
- return geoAround(this.spatialIndex, lon, lat, maxResults, maxDistanceKm)
312
- }
313
-
314
- /**
315
- * Get ways within a bounding box.
316
- */
317
- withinBbox(
318
- bbox: GeoBbox2D,
319
- include?: (index: number) => boolean,
320
- ): {
321
- ids: Float64Array
322
- positions: Float64Array
323
- startIndices: Uint32Array
324
- } {
325
- const wayCandidates = this.intersects(bbox, include)
326
- const ids = new Float64Array(wayCandidates.length)
327
- const wayPositions: Float64Array[] = []
328
- const wayStartIndices = new Uint32Array(wayCandidates.length + 1)
329
- wayStartIndices[0] = 0
330
-
331
- let size = 0
332
- wayCandidates.forEach((wayIndex, i) => {
333
- ids[i] = this.ids.at(wayIndex)
334
- const way = this.getLine(wayIndex)
335
- size += way.length
336
- wayPositions.push(way)
337
- const prevIndex = wayStartIndices[i]
338
- if (prevIndex === undefined) throw Error("Previous index is undefined")
339
- wayStartIndices[i + 1] = prevIndex + way.length / 2
340
- })
341
- const wayPositionsArray = new Float64Array(size)
342
- let pIndex = 0
343
- for (const way of wayPositions) {
344
- wayPositionsArray.set(way, pIndex)
345
- pIndex += way.length
346
- }
347
-
348
- return {
349
- ids,
350
- positions: wayPositionsArray,
351
- startIndices: wayStartIndices,
352
- }
353
- }
354
-
355
- /**
356
- * Get transferable objects for passing to another thread.
357
- * Only includes spatialIndex if it has been built.
358
- */
359
- override transferables(): WaysTransferables {
360
- const base = {
361
- ...super.transferables(),
362
- refStart: this.refStart.array.buffer,
363
- refCount: this.refCount.array.buffer,
364
- refs: this.refs.array.buffer,
365
- bbox: this.bbox.array.buffer,
366
- }
367
- // Only include spatial index if it was built
368
- if (this.spatialIndexBuilt) {
369
- return { ...base, spatialIndex: this.spatialIndex.data }
370
- }
371
- return base
372
- }
373
-
374
- /**
375
- * Get the approximate memory requirements for a given number of ways in bytes.
376
- */
377
- static getBytesRequired(count: number) {
378
- if (count === 0) return 0
379
- // Approximate nodes per way
380
- let numNodes = count
381
- let n = count
382
- while (n !== 1) {
383
- n = Math.ceil(n / 128)
384
- numNodes += n
385
- }
386
- const indexBytes = (numNodes < 16384 ? 2 : 4) * numNodes
387
- const boxesBytes = numNodes * 4 * Float64Array.BYTES_PER_ELEMENT
388
- const spatialIndexBytes = 8 + indexBytes + boxesBytes
389
-
390
- return (
391
- Ids.getBytesRequired(count) +
392
- Tags.getBytesRequired(count) +
393
- count * Uint32Array.BYTES_PER_ELEMENT + // refStart
394
- count * Uint16Array.BYTES_PER_ELEMENT + // refCount
395
- count * 4 * Float64Array.BYTES_PER_ELEMENT + // bbox
396
- spatialIndexBytes
397
- )
398
- }
399
-
400
- /**
401
- * Update a ContentHasher with way-specific data (node references).
402
- */
403
- override updateHash(hasher: ContentHasher): ContentHasher {
404
- return super
405
- .updateHash(hasher)
406
- .update(this.refStart.array)
407
- .update(this.refCount.array)
408
- .update(this.refs.array)
409
- }
31
+ private spatialIndex: Flatbush = new Flatbush(1);
32
+ // Track if spatial index was properly built (vs default empty)
33
+ private spatialIndexBuilt = false;
34
+
35
+ private refStart: RTA<Uint32Array>;
36
+ private refCount: RTA<Uint16Array>; // Maximum 2,000 nodes per way
37
+
38
+ // Node IDs
39
+ private refs: RTA<Float64Array>;
40
+
41
+ // Bounding box of the way in geographic coordinates
42
+ private bbox: RTA<Float64Array>;
43
+
44
+ // Node reference index
45
+ private nodes: Nodes;
46
+
47
+ private getNodeLonLat(id: number): [number, number] {
48
+ const coordinate = this.nodes.getNodeLonLat({ id });
49
+ if (!coordinate) throw Error(`Node ${id} not found for way geometry`);
50
+ return coordinate;
51
+ }
52
+
53
+ /**
54
+ * Create a new Ways index.
55
+ */
56
+ constructor(stringTable: StringTable, nodes: Nodes, transferables?: WaysTransferables) {
57
+ if (transferables) {
58
+ super("way", new Ids(transferables), new Tags(stringTable, transferables));
59
+ this.refStart = RTA.from(Uint32Array, transferables.refStart);
60
+ this.refCount = RTA.from(Uint16Array, transferables.refCount);
61
+ this.refs = RTA.from(IdArrayType, transferables.refs);
62
+ this.bbox = RTA.from(Float64Array, transferables.bbox);
63
+ // Only load spatial index if provided (not stored in IndexedDB)
64
+ if (transferables.spatialIndex?.byteLength) {
65
+ this.spatialIndex = Flatbush.from(transferables.spatialIndex);
66
+ this.spatialIndexBuilt = true;
67
+ }
68
+ this.indexBuilt = true;
69
+ } else {
70
+ super("way", new Ids(), new Tags(stringTable));
71
+ this.refStart = new RTA(Uint32Array);
72
+ this.refCount = new RTA(Uint16Array);
73
+ this.refs = new RTA(IdArrayType);
74
+ this.bbox = new RTA(Float64Array);
75
+ }
76
+ this.nodes = nodes;
77
+ }
78
+
79
+ /**
80
+ * Add a single way to the index.
81
+ */
82
+ addWay(way: OsmWay) {
83
+ const wayIndex = this.addEntity(way.id, way.tags ?? {});
84
+ this.refStart.push(this.refs.length);
85
+ this.refCount.push(way.refs.length);
86
+ for (const ref of way.refs) this.refs.push(ref);
87
+ return wayIndex;
88
+ }
89
+
90
+ /**
91
+ * Bulk add ways directly from a PBF PrimitiveBlock.
92
+ */
93
+ addWays(
94
+ ways: OsmPbfWay[],
95
+ blockStringIndexMap: Uint32Array,
96
+ filter?: (way: OsmWay) => OsmWay | null,
97
+ ) {
98
+ let added = 0;
99
+ for (const way of ways) {
100
+ let prevRefId = 0;
101
+ const refs = way.refs.map((refId) => {
102
+ prevRefId += refId;
103
+ return prevRefId;
104
+ });
105
+ const tagKeys: number[] = way.keys.map((key) => {
106
+ const index = blockStringIndexMap[key];
107
+ if (index === undefined) throw Error("Tag key not found");
108
+ return index;
109
+ });
110
+ const tagValues: number[] = way.vals.map((val) => {
111
+ const index = blockStringIndexMap[val];
112
+ if (index === undefined) throw Error("Tag value not found");
113
+ return index;
114
+ });
115
+ const filteredWay = filter
116
+ ? filter({
117
+ id: way.id,
118
+ refs,
119
+ tags: this.tags.getTagsFromIndices(tagKeys, tagValues),
120
+ })
121
+ : null;
122
+ if (filter && filteredWay === null) continue;
123
+
124
+ this.addEntity(way.id, tagKeys, tagValues);
125
+ this.refStart.push(this.refs.length);
126
+ this.refCount.push(filteredWay?.refs.length ?? refs.length);
127
+ this.refs.pushMany(filteredWay?.refs ?? refs);
128
+ added++;
129
+ }
130
+ return added;
131
+ }
132
+
133
+ /**
134
+ * Compact the internal arrays to free up memory.
135
+ */
136
+ buildEntityIndex() {
137
+ this.refStart.compact();
138
+ this.refCount.compact();
139
+ this.refs.compact();
140
+ }
141
+
142
+ /**
143
+ * Build the spatial index for ways.
144
+ * If bbox data already exists (e.g., loaded from storage), reuses it.
145
+ */
146
+ buildSpatialIndex() {
147
+ if (!this.nodes.isReady()) throw Error("Node index is not ready.");
148
+ if (this.size === 0) return this.spatialIndex;
149
+
150
+ this.spatialIndex = new Flatbush(this.size, 128, Float64Array, BufferConstructor);
151
+
152
+ // If bbox already has data (loaded from storage), use it directly
153
+ const hasBboxData = this.bbox.length >= this.size * 4;
154
+ for (let i = 0; i < this.size; i++) {
155
+ let minX: number;
156
+ let minY: number;
157
+ let maxX: number;
158
+ let maxY: number;
159
+
160
+ if (hasBboxData) {
161
+ // Use stored bbox values
162
+ minX = this.bbox.at(i * 4);
163
+ minY = this.bbox.at(i * 4 + 1);
164
+ maxX = this.bbox.at(i * 4 + 2);
165
+ maxY = this.bbox.at(i * 4 + 3);
166
+ } else {
167
+ // Calculate bbox from node coordinates
168
+ minX = Number.POSITIVE_INFINITY;
169
+ minY = Number.POSITIVE_INFINITY;
170
+ maxX = Number.NEGATIVE_INFINITY;
171
+ maxY = Number.NEGATIVE_INFINITY;
172
+ const start = this.refStart.at(i);
173
+ const count = this.refCount.at(i);
174
+ for (let j = start; j < start + count; j++) {
175
+ const refId = this.refs.at(j);
176
+ const [lon, lat] = this.getNodeLonLat(refId);
177
+ if (lon < minX) minX = lon;
178
+ if (lon > maxX) maxX = lon;
179
+ if (lat < minY) minY = lat;
180
+ if (lat > maxY) maxY = lat;
181
+ }
182
+ this.bbox.push(minX);
183
+ this.bbox.push(minY);
184
+ this.bbox.push(maxX);
185
+ this.bbox.push(maxY);
186
+ }
187
+ this.spatialIndex.add(minX, minY, maxX, maxY);
188
+ }
189
+ // Compact bbox if we just calculated it
190
+ if (!hasBboxData) {
191
+ this.bbox.compact();
192
+ }
193
+ this.spatialIndex.finish();
194
+ this.spatialIndexBuilt = true;
195
+ return this.spatialIndex;
196
+ }
197
+
198
+ /**
199
+ * Check if the spatial index has been built.
200
+ */
201
+ hasSpatialIndex(): boolean {
202
+ return this.spatialIndexBuilt;
203
+ }
204
+
205
+ /**
206
+ * Get the full way entity.
207
+ */
208
+ getFullEntity(index: number, id: number, tags?: OsmTags): OsmWay {
209
+ return {
210
+ id,
211
+ refs: [...this.getRefIds(index)],
212
+ tags,
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Get the node IDs referenced by a way.
218
+ */
219
+ getRefIds(index: number): number[] {
220
+ const start = this.refStart.at(index);
221
+ const count = this.refCount.at(index);
222
+ return Array.from(this.refs.slice(start, start + count));
223
+ }
224
+
225
+ /**
226
+ * Get the bounding box of a way.
227
+ */
228
+ getEntityBbox(idOrIndex: IdOrIndex): GeoBbox2D {
229
+ const index = "index" in idOrIndex ? idOrIndex.index : this.ids.idOrIndex(idOrIndex)[0];
230
+ return [
231
+ this.bbox.at(index * 4),
232
+ this.bbox.at(index * 4 + 1),
233
+ this.bbox.at(index * 4 + 2),
234
+ this.bbox.at(index * 4 + 3),
235
+ ];
236
+ }
237
+
238
+ /**
239
+ * Get the coordinates of a way as a flat array.
240
+ */
241
+ getLine(index: number) {
242
+ const count = this.refCount.at(index);
243
+ const start = this.refStart.at(index);
244
+ const line = new Float64Array(count * 2);
245
+ for (let i = 0; i < count; i++) {
246
+ const ref = this.refs.at(start + i);
247
+ const [lon, lat] = this.getNodeLonLat(ref);
248
+ line[i * 2] = lon;
249
+ line[i * 2 + 1] = lat;
250
+ }
251
+ return line;
252
+ }
253
+
254
+ /**
255
+ * Get the coordinates of a way as an array of [lon, lat] pairs.
256
+ */
257
+ getCoordinates(index: number): LonLat[] {
258
+ const count = this.refCount.at(index);
259
+ const start = this.refStart.at(index);
260
+ const coords: [number, number][] = [];
261
+ for (let refIndex = start; refIndex < start + count; refIndex++) {
262
+ const ref = this.refs.at(refIndex);
263
+ coords.push(this.getNodeLonLat(ref));
264
+ }
265
+ return coords;
266
+ }
267
+
268
+ /**
269
+ * Find way indexes that intersect a bounding box.
270
+ */
271
+ intersects(bbox: GeoBbox2D, filterFn?: (index: number) => boolean): number[] {
272
+ if (this.size === 0) return [];
273
+ return this.spatialIndex.search(bbox[0], bbox[1], bbox[2], bbox[3], filterFn);
274
+ }
275
+
276
+ /**
277
+ * Find way indexes near a point using great-circle distance.
278
+ * @param lon - Longitude in degrees.
279
+ * @param lat - Latitude in degrees.
280
+ * @param maxResults - Maximum number of results to return.
281
+ * @param maxDistanceKm - Maximum distance in kilometers.
282
+ * @returns Array of way indexes sorted by distance.
283
+ */
284
+ neighbors(lon: number, lat: number, maxResults?: number, maxDistanceKm?: number): number[] {
285
+ if (this.size === 0) return [];
286
+ // Use geoflatbush for proper geographic distance calculations
287
+ return geoAround(this.spatialIndex, lon, lat, maxResults, maxDistanceKm);
288
+ }
289
+
290
+ /**
291
+ * Get ways within a bounding box.
292
+ */
293
+ withinBbox(
294
+ bbox: GeoBbox2D,
295
+ include?: (index: number) => boolean,
296
+ ): {
297
+ ids: Float64Array;
298
+ positions: Float64Array;
299
+ startIndices: Uint32Array;
300
+ } {
301
+ const wayCandidates = this.intersects(bbox, include);
302
+ const ids = new Float64Array(wayCandidates.length);
303
+ const wayPositions: Float64Array[] = [];
304
+ const wayStartIndices = new Uint32Array(wayCandidates.length + 1);
305
+ wayStartIndices[0] = 0;
306
+
307
+ let size = 0;
308
+ wayCandidates.forEach((wayIndex, i) => {
309
+ ids[i] = this.ids.at(wayIndex);
310
+ const way = this.getLine(wayIndex);
311
+ size += way.length;
312
+ wayPositions.push(way);
313
+ const prevIndex = wayStartIndices[i];
314
+ if (prevIndex === undefined) throw Error("Previous index is undefined");
315
+ wayStartIndices[i + 1] = prevIndex + way.length / 2;
316
+ });
317
+ const wayPositionsArray = new Float64Array(size);
318
+ let pIndex = 0;
319
+ for (const way of wayPositions) {
320
+ wayPositionsArray.set(way, pIndex);
321
+ pIndex += way.length;
322
+ }
323
+
324
+ return {
325
+ ids,
326
+ positions: wayPositionsArray,
327
+ startIndices: wayStartIndices,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Get transferable objects for passing to another thread.
333
+ * Only includes spatialIndex if it has been built.
334
+ */
335
+ override transferables(): WaysTransferables {
336
+ const base = {
337
+ ...super.transferables(),
338
+ refStart: this.refStart.array.buffer,
339
+ refCount: this.refCount.array.buffer,
340
+ refs: this.refs.array.buffer,
341
+ bbox: this.bbox.array.buffer,
342
+ };
343
+ // Only include spatial index if it was built
344
+ if (this.spatialIndexBuilt) {
345
+ return { ...base, spatialIndex: this.spatialIndex.data };
346
+ }
347
+ return base;
348
+ }
349
+
350
+ /**
351
+ * Get the approximate memory requirements for a given number of ways in bytes.
352
+ */
353
+ static getBytesRequired(count: number) {
354
+ if (count === 0) return 0;
355
+ // Approximate nodes per way
356
+ let numNodes = count;
357
+ let n = count;
358
+ while (n !== 1) {
359
+ n = Math.ceil(n / 128);
360
+ numNodes += n;
361
+ }
362
+ const indexBytes = (numNodes < 16384 ? 2 : 4) * numNodes;
363
+ const boxesBytes = numNodes * 4 * Float64Array.BYTES_PER_ELEMENT;
364
+ const spatialIndexBytes = 8 + indexBytes + boxesBytes;
365
+
366
+ return (
367
+ Ids.getBytesRequired(count) +
368
+ Tags.getBytesRequired(count) +
369
+ count * Uint32Array.BYTES_PER_ELEMENT + // refStart
370
+ count * Uint16Array.BYTES_PER_ELEMENT + // refCount
371
+ count * 4 * Float64Array.BYTES_PER_ELEMENT + // bbox
372
+ spatialIndexBytes
373
+ );
374
+ }
375
+
376
+ /**
377
+ * Update a ContentHasher with way-specific data (node references).
378
+ */
379
+ override updateHash(hasher: ContentHasher): ContentHasher {
380
+ return super
381
+ .updateHash(hasher)
382
+ .update(this.refStart.array)
383
+ .update(this.refCount.array)
384
+ .update(this.refs.array);
385
+ }
410
386
  }