@osmix/core 0.1.10 → 0.3.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 (97) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +136 -72
  4. package/dist/contracts.d.ts +27 -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 +22 -2
  9. package/dist/entities.d.ts.map +1 -1
  10. package/dist/entities.js +54 -10
  11. package/dist/entities.js.map +1 -1
  12. package/dist/ids.d.ts +20 -5
  13. package/dist/ids.d.ts.map +1 -1
  14. package/dist/ids.js +87 -29
  15. package/dist/ids.js.map +1 -1
  16. package/dist/index.d.ts +7 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +6 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/indirect-kd-index.d.ts +28 -0
  21. package/dist/indirect-kd-index.d.ts.map +1 -0
  22. package/dist/indirect-kd-index.js +163 -0
  23. package/dist/indirect-kd-index.js.map +1 -0
  24. package/dist/limits.d.ts +30 -0
  25. package/dist/limits.d.ts.map +1 -0
  26. package/dist/limits.js +51 -0
  27. package/dist/limits.js.map +1 -0
  28. package/dist/mocks.d.ts.map +1 -1
  29. package/dist/mocks.js.map +1 -1
  30. package/dist/nodes.d.ts +36 -15
  31. package/dist/nodes.d.ts.map +1 -1
  32. package/dist/nodes.js +185 -62
  33. package/dist/nodes.js.map +1 -1
  34. package/dist/osm.d.ts +60 -4
  35. package/dist/osm.d.ts.map +1 -1
  36. package/dist/osm.js +32 -6
  37. package/dist/osm.js.map +1 -1
  38. package/dist/relations.d.ts +11 -4
  39. package/dist/relations.d.ts.map +1 -1
  40. package/dist/relations.js +73 -29
  41. package/dist/relations.js.map +1 -1
  42. package/dist/renumber.d.ts +23 -0
  43. package/dist/renumber.d.ts.map +1 -0
  44. package/dist/renumber.js +64 -0
  45. package/dist/renumber.js.map +1 -0
  46. package/dist/stringtable.d.ts +2 -1
  47. package/dist/stringtable.d.ts.map +1 -1
  48. package/dist/stringtable.js +9 -14
  49. package/dist/stringtable.js.map +1 -1
  50. package/dist/tags.d.ts +45 -12
  51. package/dist/tags.d.ts.map +1 -1
  52. package/dist/tags.js +153 -27
  53. package/dist/tags.js.map +1 -1
  54. package/dist/typed-arrays.d.ts +63 -48
  55. package/dist/typed-arrays.d.ts.map +1 -1
  56. package/dist/typed-arrays.js +184 -107
  57. package/dist/typed-arrays.js.map +1 -1
  58. package/dist/ways.d.ts +52 -3
  59. package/dist/ways.d.ts.map +1 -1
  60. package/dist/ways.js +223 -46
  61. package/dist/ways.js.map +1 -1
  62. package/package.json +45 -31
  63. package/src/contracts.ts +29 -0
  64. package/src/entities.ts +242 -194
  65. package/src/ids.ts +278 -235
  66. package/src/index.ts +22 -13
  67. package/src/indirect-kd-index.ts +212 -0
  68. package/src/limits.ts +73 -0
  69. package/src/mocks.ts +114 -113
  70. package/src/nodes.ts +538 -397
  71. package/src/osm.ts +282 -207
  72. package/src/relations.ts +580 -558
  73. package/src/renumber.ts +80 -0
  74. package/src/stringtable.ts +189 -184
  75. package/src/tags.ts +406 -272
  76. package/src/typed-arrays.ts +307 -205
  77. package/src/types/geoflatbush.d.ts +28 -33
  78. package/src/ways.ts +577 -406
  79. package/test/entity-index-errors.test.ts +41 -0
  80. package/test/ids.bench.ts +33 -0
  81. package/test/ids.test.ts +122 -0
  82. package/test/limits.test.ts +83 -0
  83. package/test/memory.test.ts +29 -30
  84. package/test/no-shared-array-buffer.test.ts +41 -0
  85. package/test/nodes-spatial-index.test.ts +261 -0
  86. package/test/osm-bbox.test.ts +30 -0
  87. package/test/relations.bench.ts +32 -0
  88. package/test/relations.test.ts +124 -0
  89. package/test/renumber.test.ts +54 -0
  90. package/test/spatial-index-rebuild.test.ts +33 -0
  91. package/test/storage-compaction.test.ts +269 -0
  92. package/test/tags-search.test.ts +89 -56
  93. package/test/typed-arrays.test.ts +252 -132
  94. package/tsconfig.build.json +6 -6
  95. package/tsconfig.json +7 -7
  96. package/src/types/geokdbush.d.ts +0 -37
  97. package/src/types/kdbush.d.ts +0 -47
package/src/osm.ts CHANGED
@@ -1,222 +1,297 @@
1
- import type { OsmPbfHeaderBlock } from "@osmix/pbf"
2
- import { ContentHasher } from "@osmix/shared/content-hasher"
3
- import type { GeoBbox2D } from "@osmix/shared/types"
4
- import { Nodes, type NodesTransferables } from "./nodes.ts"
5
- import { Relations, type RelationsTransferables } from "./relations.ts"
6
- import StringTable, { type StringTableTransferables } from "./stringtable.ts"
7
- import type { BufferType } from "./typed-arrays.ts"
8
- import { Ways, type WaysTransferables } from "./ways.ts"
1
+ import type { OsmPbfHeaderBlock } from "@osmix/pbf";
2
+ import { ContentHasher } from "@osmix/shared/content-hasher";
3
+ import type { GeoBbox2D } from "@osmix/types";
4
+
5
+ import { Nodes, type NodesTransferables } from "./nodes.ts";
6
+ import { Relations, type RelationsTransferables } from "./relations.ts";
7
+ import StringTable, { type StringTableTransferables } from "./stringtable.ts";
8
+ import type { BufferType } from "./typed-arrays.ts";
9
+ import { Ways, type WaysTransferables } from "./ways.ts";
10
+
11
+ export const OSM_TRANSFER_VERSION = 2 as const;
12
+ export const OSM_CONTENT_HASH_VERSION = 2 as const;
9
13
 
10
14
  export interface OsmTransferables<T extends BufferType = BufferType> {
11
- id: string
12
- header: OsmPbfHeaderBlock
13
- contentHash: string
14
- stringTable: StringTableTransferables<T>
15
- nodes: NodesTransferables<T>
16
- ways: WaysTransferables<T>
17
- relations: RelationsTransferables<T>
15
+ transferVersion: typeof OSM_TRANSFER_VERSION;
16
+ contentHashVersion: typeof OSM_CONTENT_HASH_VERSION;
17
+ id: string;
18
+ header: OsmPbfHeaderBlock;
19
+ contentHash: string;
20
+ loadDiagnostics?: OsmLoadDiagnostics;
21
+ stringTable: StringTableTransferables<T>;
22
+ nodes: NodesTransferables<T>;
23
+ ways: WaysTransferables<T>;
24
+ relations: RelationsTransferables<T>;
25
+ }
26
+
27
+ export type OsmLoadProfile = "auto" | "full" | "view";
28
+ export type ResolvedOsmLoadProfile = Exclude<OsmLoadProfile, "auto">;
29
+
30
+ export interface OsmLoadDiagnosticReason {
31
+ code: string;
32
+ level: "info" | "warning";
33
+ message: string;
34
+ }
35
+
36
+ /** Plain-data diagnostics attached by loaders and safe to transfer between workers. */
37
+ export interface OsmLoadDiagnostics {
38
+ requestedProfile: OsmLoadProfile;
39
+ selectedProfile: ResolvedOsmLoadProfile;
40
+ reasons: OsmLoadDiagnosticReason[];
41
+ bytes: {
42
+ residentTypedBuffers: number;
43
+ projectedTypedBufferPeak: number;
44
+ largestPlannedAllocation: number;
45
+ storageBytes?: number;
46
+ };
47
+ budgets: {
48
+ workingSet?: number;
49
+ singleAllocation?: number;
50
+ allNodeSpatialIndex?: number;
51
+ arrayBufferCeiling?: number;
52
+ sharedArrayBufferCeiling?: number;
53
+ };
54
+ phaseTimingsMs: Record<string, number>;
55
+ counters: {
56
+ taggedNodes?: number;
57
+ taggedWays?: number;
58
+ taggedRelations?: number;
59
+ nodeTagPairs?: number;
60
+ wayTagPairs?: number;
61
+ relationTagPairs?: number;
62
+ wayReferences?: number;
63
+ missingWayReferences?: number;
64
+ relationMembers?: number;
65
+ };
18
66
  }
19
67
 
20
68
  export interface OsmInfo {
21
- id: string
22
- bbox: GeoBbox2D
23
- header: OsmPbfHeaderBlock
24
- stats: {
25
- nodes: number
26
- ways: number
27
- relations: number
28
- }
69
+ id: string;
70
+ /** Bounding box of all nodes, or `null` when the dataset has no nodes. */
71
+ bbox: GeoBbox2D | null;
72
+ header: OsmPbfHeaderBlock;
73
+ stats: {
74
+ nodes: number;
75
+ ways: number;
76
+ relations: number;
77
+ };
78
+ spatialIndexes: {
79
+ nodes: { all: boolean; tagged: boolean };
80
+ ways: boolean;
81
+ relations: boolean;
82
+ };
83
+ loadDiagnostics?: OsmLoadDiagnostics;
29
84
  }
30
85
 
31
86
  export interface OsmOptions {
32
- id: string
33
- header: OsmPbfHeaderBlock
87
+ id: string;
88
+ header: OsmPbfHeaderBlock;
34
89
  }
35
90
 
36
91
  /**
37
92
  * OSM Entity Index.
38
93
  */
39
94
  export class Osm {
40
- // Filename or ID of this OSM Entity index.
41
- readonly id: string
42
- readonly header: OsmPbfHeaderBlock
43
-
44
- // Shared string lookup table for all nodes, ways, and relations
45
- readonly stringTable: StringTable
46
- readonly nodes: Nodes
47
- readonly ways: Ways
48
- readonly relations: Relations
49
-
50
- private indexBuilt = false
51
- private _contentHash = ""
52
-
53
- /**
54
- * Create a new OSM Entity index.
55
- */
56
- constructor(opts?: Partial<OsmOptions> | OsmTransferables | Osm) {
57
- this.id = opts?.id ?? "unknown"
58
- this.header = opts?.header ?? {
59
- required_features: [],
60
- optional_features: [],
61
- }
62
- if (opts && "stringTable" in opts) {
63
- if (opts instanceof Osm) {
64
- this.stringTable = new StringTable(opts.stringTable.transferables())
65
- this.nodes = new Nodes(this.stringTable, opts.nodes.transferables())
66
- this.ways = new Ways(
67
- this.stringTable,
68
- this.nodes,
69
- opts.ways.transferables(),
70
- )
71
- this.relations = new Relations(
72
- this.stringTable,
73
- this.nodes,
74
- this.ways,
75
- opts.relations.transferables(),
76
- )
77
- this._contentHash = opts._contentHash
78
- this.indexBuilt = true
79
- } else {
80
- this.stringTable = new StringTable(opts.stringTable)
81
- this.nodes = new Nodes(this.stringTable, opts.nodes)
82
- this.ways = new Ways(this.stringTable, this.nodes, opts.ways)
83
- this.relations = new Relations(
84
- this.stringTable,
85
- this.nodes,
86
- this.ways,
87
- opts.relations,
88
- )
89
- this._contentHash = opts.contentHash
90
- this.indexBuilt = true
91
- }
92
- } else {
93
- this.stringTable = new StringTable()
94
- this.nodes = new Nodes(this.stringTable)
95
- this.ways = new Ways(this.stringTable, this.nodes)
96
- this.relations = new Relations(this.stringTable, this.nodes, this.ways)
97
- }
98
- }
99
-
100
- /**
101
- * Build the internal indexes for all entities.
102
- * Also computes the content hash after indexes are built.
103
- */
104
- buildIndexes() {
105
- this.stringTable.buildIndex()
106
- this.nodes.buildIndex()
107
- this.ways.buildIndex()
108
- this.relations.buildIndex()
109
- this.indexBuilt = true
110
- // Compute content hash after indexes are built
111
- this._contentHash = this.computeContentHash()
112
- }
113
-
114
- /**
115
- * Check if the index is built and ready for use.
116
- */
117
- isReady() {
118
- return (
119
- this.nodes.isReady() &&
120
- this.ways.isReady() &&
121
- this.relations.isReady() &&
122
- this.indexBuilt
123
- )
124
- }
125
-
126
- /**
127
- * Build spatial indexes for all entities.
128
- */
129
- buildSpatialIndexes() {
130
- this.nodes.buildSpatialIndex()
131
- this.ways.buildSpatialIndex()
132
- this.relations.buildSpatialIndex()
133
- }
134
-
135
- /**
136
- * Check if spatial indexes have been built for all entity types.
137
- */
138
- hasSpatialIndexes(): boolean {
139
- return (
140
- this.nodes.hasSpatialIndex() &&
141
- this.ways.hasSpatialIndex() &&
142
- this.relations.hasSpatialIndex()
143
- )
144
- }
145
-
146
- /**
147
- * Get the bounding box of all entities in the OSM index.
148
- */
149
- bbox(): GeoBbox2D {
150
- return this.nodes.getBbox()
151
- }
152
-
153
- /**
154
- * Get information about the OSM index.
155
- */
156
- info(): OsmInfo {
157
- return {
158
- id: this.id,
159
- bbox: this.bbox(),
160
- header: this.header,
161
- stats: {
162
- nodes: this.nodes.size,
163
- ways: this.ways.size,
164
- relations: this.relations.size,
165
- },
166
- }
167
- }
168
-
169
- /**
170
- * Get transferable objects for passing to another thread.
171
- */
172
- transferables(): OsmTransferables {
173
- return {
174
- id: this.id,
175
- header: this.header,
176
- contentHash: this._contentHash,
177
- stringTable: this.stringTable.transferables(),
178
- nodes: this.nodes.transferables(),
179
- ways: this.ways.transferables(),
180
- relations: this.relations.transferables(),
181
- }
182
- }
183
-
184
- /**
185
- * Get the content hash of this OSM dataset.
186
- * The hash is computed when indexes are built.
187
- *
188
- * @returns A hex string hash uniquely identifying the content.
189
- */
190
- contentHash(): string {
191
- return this._contentHash
192
- }
193
-
194
- /**
195
- * Check if this OSM dataset has identical content to another.
196
- * Uses the pre-computed content hash for fast comparison.
197
- *
198
- * @param other - The other Osm instance to compare with.
199
- * @returns True if both datasets have identical content.
200
- */
201
- isEqual(other: Osm | null | undefined): boolean {
202
- if (!other) return false
203
- return this._contentHash === other._contentHash
204
- }
205
-
206
- /**
207
- * Compute a content hash of all underlying data.
208
- * This hash uniquely identifies the dataset content regardless of metadata.
209
- *
210
- * @returns A hex string hash of the content.
211
- */
212
- private computeContentHash(): string {
213
- const hasher = new ContentHasher()
214
- // Hash string table first (shared by all entities)
215
- this.stringTable.updateHash(hasher)
216
- // Hash each entity collection
217
- this.nodes.updateHash(hasher)
218
- this.ways.updateHash(hasher)
219
- this.relations.updateHash(hasher)
220
- return hasher.digest()
221
- }
95
+ // Filename or ID of this OSM Entity index.
96
+ readonly id: string;
97
+ readonly header: OsmPbfHeaderBlock;
98
+
99
+ // Shared string lookup table for all nodes, ways, and relations
100
+ readonly stringTable: StringTable;
101
+ readonly nodes: Nodes;
102
+ readonly ways: Ways;
103
+ readonly relations: Relations;
104
+
105
+ private indexBuilt = false;
106
+ private _contentHash = "";
107
+ private _loadDiagnostics?: OsmLoadDiagnostics;
108
+
109
+ /**
110
+ * Create a new OSM Entity index.
111
+ */
112
+ constructor(opts?: Partial<OsmOptions> | OsmTransferables | Osm) {
113
+ this.id = opts?.id ?? "unknown";
114
+ this.header = opts?.header ?? {
115
+ required_features: [],
116
+ optional_features: [],
117
+ };
118
+ if (opts && "stringTable" in opts) {
119
+ if (opts instanceof Osm) {
120
+ this.stringTable = new StringTable(opts.stringTable.transferables());
121
+ this.nodes = new Nodes(this.stringTable, opts.nodes.transferables());
122
+ this.ways = new Ways(this.stringTable, this.nodes, opts.ways.transferables());
123
+ this.relations = new Relations(
124
+ this.stringTable,
125
+ this.nodes,
126
+ this.ways,
127
+ opts.relations.transferables(),
128
+ );
129
+ this._contentHash = opts._contentHash;
130
+ this._loadDiagnostics = opts._loadDiagnostics;
131
+ this.indexBuilt = true;
132
+ } else {
133
+ if (opts.transferVersion !== OSM_TRANSFER_VERSION) {
134
+ throw Error(
135
+ `Unsupported OSM transfer version: ${String(opts.transferVersion)}. Expected ${String(OSM_TRANSFER_VERSION)}.`,
136
+ );
137
+ }
138
+ if (opts.contentHashVersion !== OSM_CONTENT_HASH_VERSION) {
139
+ throw Error(
140
+ `Unsupported OSM content hash version: ${String(opts.contentHashVersion)}. Expected ${String(OSM_CONTENT_HASH_VERSION)}.`,
141
+ );
142
+ }
143
+ this.stringTable = new StringTable(opts.stringTable);
144
+ this.nodes = new Nodes(this.stringTable, opts.nodes);
145
+ this.ways = new Ways(this.stringTable, this.nodes, opts.ways);
146
+ this.relations = new Relations(this.stringTable, this.nodes, this.ways, opts.relations);
147
+ this._contentHash = opts.contentHash;
148
+ this._loadDiagnostics = opts.loadDiagnostics;
149
+ this.indexBuilt = true;
150
+ }
151
+ } else {
152
+ this.stringTable = new StringTable();
153
+ this.nodes = new Nodes(this.stringTable);
154
+ this.ways = new Ways(this.stringTable, this.nodes);
155
+ this.relations = new Relations(this.stringTable, this.nodes, this.ways);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Build the internal indexes for all entities.
161
+ * Also computes the content hash after indexes are built.
162
+ */
163
+ buildIndexes() {
164
+ this.stringTable.buildIndex();
165
+ this.nodes.buildIndex();
166
+ this.ways.buildIndex();
167
+ this.relations.buildIndex();
168
+ this.indexBuilt = true;
169
+ // Compute content hash after indexes are built
170
+ this._contentHash = this.computeContentHash();
171
+ }
172
+
173
+ /**
174
+ * Check if the index is built and ready for use.
175
+ */
176
+ isReady() {
177
+ return (
178
+ this.nodes.isReady() && this.ways.isReady() && this.relations.isReady() && this.indexBuilt
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Build spatial indexes for all entities.
184
+ */
185
+ buildSpatialIndexes() {
186
+ this.nodes.buildSpatialIndex("tagged");
187
+ this.nodes.buildSpatialIndex();
188
+ this.ways.buildSpatialIndex();
189
+ this.relations.buildSpatialIndex();
190
+ }
191
+
192
+ /**
193
+ * Check if spatial indexes have been built for all entity types.
194
+ */
195
+ hasSpatialIndexes(): boolean {
196
+ return (
197
+ this.nodes.hasSpatialIndex() &&
198
+ this.nodes.hasSpatialIndex("tagged") &&
199
+ this.ways.hasSpatialIndex() &&
200
+ this.relations.hasSpatialIndex()
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Get the bounding box of all entities in the OSM index, or `null` when there are no nodes.
206
+ */
207
+ bbox(): GeoBbox2D | null {
208
+ return this.nodes.getBbox();
209
+ }
210
+
211
+ /**
212
+ * Get information about the OSM index.
213
+ */
214
+ info(): OsmInfo {
215
+ return {
216
+ id: this.id,
217
+ bbox: this.bbox(),
218
+ header: this.header,
219
+ stats: {
220
+ nodes: this.nodes.size,
221
+ ways: this.ways.size,
222
+ relations: this.relations.size,
223
+ },
224
+ spatialIndexes: {
225
+ nodes: {
226
+ all: this.nodes.hasSpatialIndex("all"),
227
+ tagged: this.nodes.hasSpatialIndex("tagged"),
228
+ },
229
+ ways: this.ways.hasSpatialIndex(),
230
+ relations: this.relations.hasSpatialIndex(),
231
+ },
232
+ ...(this._loadDiagnostics ? { loadDiagnostics: this._loadDiagnostics } : {}),
233
+ };
234
+ }
235
+
236
+ /** Attach loader-provided diagnostics for `info()` and worker transfer. */
237
+ setLoadDiagnostics(loadDiagnostics: OsmLoadDiagnostics | undefined): void {
238
+ this._loadDiagnostics = loadDiagnostics;
239
+ }
240
+
241
+ /**
242
+ * Get transferable objects for passing to another thread.
243
+ */
244
+ transferables(): OsmTransferables {
245
+ return {
246
+ transferVersion: OSM_TRANSFER_VERSION,
247
+ contentHashVersion: OSM_CONTENT_HASH_VERSION,
248
+ id: this.id,
249
+ header: this.header,
250
+ contentHash: this._contentHash,
251
+ ...(this._loadDiagnostics ? { loadDiagnostics: this._loadDiagnostics } : {}),
252
+ stringTable: this.stringTable.transferables(),
253
+ nodes: this.nodes.transferables(),
254
+ ways: this.ways.transferables(),
255
+ relations: this.relations.transferables(),
256
+ };
257
+ }
258
+
259
+ /**
260
+ * Get the content hash of this OSM dataset.
261
+ * The hash is computed when indexes are built.
262
+ *
263
+ * @returns A hex string hash uniquely identifying the content.
264
+ */
265
+ contentHash(): string {
266
+ return this._contentHash;
267
+ }
268
+
269
+ /**
270
+ * Check if this OSM dataset has identical content to another.
271
+ * Uses the pre-computed content hash for fast comparison.
272
+ *
273
+ * @param other - The other Osm instance to compare with.
274
+ * @returns True if both datasets have identical content.
275
+ */
276
+ isEqual(other: Osm | null | undefined): boolean {
277
+ if (!other) return false;
278
+ return this._contentHash === other._contentHash;
279
+ }
280
+
281
+ /**
282
+ * Compute a content hash of all underlying data.
283
+ * This hash uniquely identifies the dataset content regardless of metadata.
284
+ *
285
+ * @returns A hex string hash of the content.
286
+ */
287
+ private computeContentHash(): string {
288
+ const hasher = new ContentHasher().updateNumber(OSM_CONTENT_HASH_VERSION);
289
+ // Hash string table first (shared by all entities)
290
+ this.stringTable.updateHash(hasher);
291
+ // Hash each entity collection
292
+ this.nodes.updateHash(hasher);
293
+ this.ways.updateHash(hasher);
294
+ this.relations.updateHash(hasher);
295
+ return hasher.digest();
296
+ }
222
297
  }