@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.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +136 -72
- package/dist/contracts.d.ts +27 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -0
- package/dist/entities.d.ts +22 -2
- package/dist/entities.d.ts.map +1 -1
- package/dist/entities.js +54 -10
- package/dist/entities.js.map +1 -1
- package/dist/ids.d.ts +20 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +87 -29
- package/dist/ids.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/indirect-kd-index.d.ts +28 -0
- package/dist/indirect-kd-index.d.ts.map +1 -0
- package/dist/indirect-kd-index.js +163 -0
- package/dist/indirect-kd-index.js.map +1 -0
- package/dist/limits.d.ts +30 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +51 -0
- package/dist/limits.js.map +1 -0
- package/dist/mocks.d.ts.map +1 -1
- package/dist/mocks.js.map +1 -1
- package/dist/nodes.d.ts +36 -15
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +185 -62
- package/dist/nodes.js.map +1 -1
- package/dist/osm.d.ts +60 -4
- package/dist/osm.d.ts.map +1 -1
- package/dist/osm.js +32 -6
- package/dist/osm.js.map +1 -1
- package/dist/relations.d.ts +11 -4
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +73 -29
- package/dist/relations.js.map +1 -1
- package/dist/renumber.d.ts +23 -0
- package/dist/renumber.d.ts.map +1 -0
- package/dist/renumber.js +64 -0
- package/dist/renumber.js.map +1 -0
- package/dist/stringtable.d.ts +2 -1
- package/dist/stringtable.d.ts.map +1 -1
- package/dist/stringtable.js +9 -14
- package/dist/stringtable.js.map +1 -1
- package/dist/tags.d.ts +45 -12
- package/dist/tags.d.ts.map +1 -1
- package/dist/tags.js +153 -27
- package/dist/tags.js.map +1 -1
- package/dist/typed-arrays.d.ts +63 -48
- package/dist/typed-arrays.d.ts.map +1 -1
- package/dist/typed-arrays.js +184 -107
- package/dist/typed-arrays.js.map +1 -1
- package/dist/ways.d.ts +52 -3
- package/dist/ways.d.ts.map +1 -1
- package/dist/ways.js +223 -46
- package/dist/ways.js.map +1 -1
- package/package.json +45 -31
- package/src/contracts.ts +29 -0
- package/src/entities.ts +242 -194
- package/src/ids.ts +278 -235
- package/src/index.ts +22 -13
- package/src/indirect-kd-index.ts +212 -0
- package/src/limits.ts +73 -0
- package/src/mocks.ts +114 -113
- package/src/nodes.ts +538 -397
- package/src/osm.ts +282 -207
- package/src/relations.ts +580 -558
- package/src/renumber.ts +80 -0
- package/src/stringtable.ts +189 -184
- package/src/tags.ts +406 -272
- package/src/typed-arrays.ts +307 -205
- package/src/types/geoflatbush.d.ts +28 -33
- package/src/ways.ts +577 -406
- package/test/entity-index-errors.test.ts +41 -0
- package/test/ids.bench.ts +33 -0
- package/test/ids.test.ts +122 -0
- package/test/limits.test.ts +83 -0
- package/test/memory.test.ts +29 -30
- package/test/no-shared-array-buffer.test.ts +41 -0
- package/test/nodes-spatial-index.test.ts +261 -0
- package/test/osm-bbox.test.ts +30 -0
- package/test/relations.bench.ts +32 -0
- package/test/relations.test.ts +124 -0
- package/test/renumber.test.ts +54 -0
- package/test/spatial-index-rebuild.test.ts +33 -0
- package/test/storage-compaction.test.ts +269 -0
- package/test/tags-search.test.ts +89 -56
- package/test/typed-arrays.test.ts +252 -132
- package/tsconfig.build.json +6 -6
- package/tsconfig.json +7 -7
- package/src/types/geokdbush.d.ts +0 -37
- package/src/types/kdbush.d.ts +0 -47
package/src/tags.ts
CHANGED
|
@@ -8,289 +8,423 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type { ContentHasher } from "@osmix/shared/content-hasher"
|
|
12
|
-
import type { OsmTags } from "@osmix/
|
|
13
|
-
|
|
14
|
-
import
|
|
11
|
+
import type { ContentHasher } from "@osmix/shared/content-hasher";
|
|
12
|
+
import type { OsmTags } from "@osmix/types";
|
|
13
|
+
|
|
14
|
+
import StringTable from "./stringtable.ts";
|
|
15
|
+
import { BufferConstructor, type BufferType, ResizeableTypedArray as RTA } from "./typed-arrays.ts";
|
|
16
|
+
|
|
17
|
+
const TAG_RANK_BLOCK_SIZE = 256;
|
|
18
|
+
const TAG_BITS_PER_WORD = 32;
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Serializable state for worker transfer.
|
|
18
22
|
*/
|
|
19
23
|
export interface TagsTransferables<T extends BufferType = BufferType> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
/** Exact number of entities represented by this tag index. */
|
|
25
|
+
tagEntityCount: number;
|
|
26
|
+
/** One presence bit per entity. */
|
|
27
|
+
taggedEntityBits: T;
|
|
28
|
+
/** Tagged-entity prefix count at each 256-entity boundary. */
|
|
29
|
+
tagRankCheckpoints: T;
|
|
30
|
+
/** Tag offsets for tagged entities, including a final sentinel. */
|
|
31
|
+
tagOffsets: T;
|
|
32
|
+
/** Flattened tag key indices. */
|
|
33
|
+
tagKeys: T;
|
|
34
|
+
/** Flattened tag value indices. */
|
|
35
|
+
tagVals: T;
|
|
36
|
+
|
|
37
|
+
/** Flattened entity indices for reverse key lookup. */
|
|
38
|
+
keyEntities: T;
|
|
39
|
+
/** Maps key index → start position in keyEntities. */
|
|
40
|
+
keyIndexStart: T;
|
|
41
|
+
/** Maps key index → count of entities with that key. */
|
|
42
|
+
keyIndexCount: T;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
46
|
* Bidirectional tag storage.
|
|
39
47
|
*
|
|
40
|
-
* Limits: Max 255 tags per entity.
|
|
41
48
|
* Note: String indices reference a shared `StringTable`.
|
|
42
49
|
*/
|
|
43
50
|
export class Tags {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
51
|
+
/** Reference to the shared string table for key/value storage */
|
|
52
|
+
private stringTable: StringTable = new StringTable();
|
|
53
|
+
|
|
54
|
+
// ─── Entity → Tag Lookup ───────────────────────────────────────────────────
|
|
55
|
+
/** Exact number of entities represented by this tag index. */
|
|
56
|
+
private entityCount = 0;
|
|
57
|
+
/** One presence bit per entity. */
|
|
58
|
+
private taggedEntityBits: Uint32Array;
|
|
59
|
+
/** Tagged-entity prefix count at each 256-entity boundary. */
|
|
60
|
+
private tagRankCheckpoints: Uint32Array;
|
|
61
|
+
/** Tag offsets for tagged entities, including a final sentinel. */
|
|
62
|
+
private tagOffsets: RTA<Uint32Array>;
|
|
63
|
+
/** All tag key string indices, concatenated */
|
|
64
|
+
private tagKeys: RTA<Uint32Array>;
|
|
65
|
+
/** All tag value string indices, concatenated (parallel to tagKeys) */
|
|
66
|
+
private tagVals: RTA<Uint32Array>;
|
|
67
|
+
|
|
68
|
+
// ─── Key → Entity Reverse Lookup ───────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Flattened array of entity indices that have each tag key.
|
|
71
|
+
* Indexed via keyIndexStart and keyIndexCount.
|
|
72
|
+
*/
|
|
73
|
+
private keyEntities: RTA<Uint32Array>;
|
|
74
|
+
|
|
75
|
+
/** Maps key string index → start position in keyEntities */
|
|
76
|
+
private keyIndexStart: RTA<Uint32Array>;
|
|
77
|
+
/** Maps key string index → count of entities with that key */
|
|
78
|
+
private keyIndexCount: RTA<Uint32Array>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Temporary map used during ingestion to collect entity indices per key.
|
|
82
|
+
* Converted to flat arrays during buildIndex() and then cleared.
|
|
83
|
+
*/
|
|
84
|
+
private keyEntityIndexBuilder = new Map<number, number[]>();
|
|
85
|
+
|
|
86
|
+
/** Tagged entity indexes collected during ingestion and released on finalization. */
|
|
87
|
+
private taggedEntityIndexBuilder: RTA<Uint32Array> | null;
|
|
88
|
+
|
|
89
|
+
/** Largest tagged entity index seen so far; enforces ascending ingestion order. */
|
|
90
|
+
private lastTaggedEntityIndex = -1;
|
|
91
|
+
|
|
92
|
+
/** Whether buildIndex() has been called */
|
|
93
|
+
private indexBuilt = false;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a new Tags index.
|
|
97
|
+
*/
|
|
98
|
+
constructor(stringTable: StringTable, transferables?: TagsTransferables) {
|
|
99
|
+
this.stringTable = stringTable;
|
|
100
|
+
if (transferables) {
|
|
101
|
+
this.entityCount = transferables.tagEntityCount;
|
|
102
|
+
this.taggedEntityBits = new Uint32Array(transferables.taggedEntityBits);
|
|
103
|
+
this.tagRankCheckpoints = new Uint32Array(transferables.tagRankCheckpoints);
|
|
104
|
+
this.tagOffsets = RTA.from(Uint32Array, transferables.tagOffsets);
|
|
105
|
+
this.tagKeys = RTA.from(Uint32Array, transferables.tagKeys);
|
|
106
|
+
this.tagVals = RTA.from(Uint32Array, transferables.tagVals);
|
|
107
|
+
this.keyEntities = RTA.from(Uint32Array, transferables.keyEntities);
|
|
108
|
+
this.keyIndexStart = RTA.from(Uint32Array, transferables.keyIndexStart);
|
|
109
|
+
this.keyIndexCount = RTA.from(Uint32Array, transferables.keyIndexCount);
|
|
110
|
+
this.taggedEntityIndexBuilder = null;
|
|
111
|
+
this.indexBuilt = true;
|
|
112
|
+
} else {
|
|
113
|
+
this.taggedEntityBits = new Uint32Array(new BufferConstructor(0));
|
|
114
|
+
this.tagRankCheckpoints = new Uint32Array(new BufferConstructor(0));
|
|
115
|
+
this.tagOffsets = new RTA(Uint32Array);
|
|
116
|
+
this.tagKeys = new RTA(Uint32Array);
|
|
117
|
+
this.tagVals = new RTA(Uint32Array);
|
|
118
|
+
this.keyEntities = new RTA(Uint32Array);
|
|
119
|
+
this.keyIndexStart = new RTA(Uint32Array);
|
|
120
|
+
this.keyIndexCount = new RTA(Uint32Array);
|
|
121
|
+
this.taggedEntityIndexBuilder = new RTA(Uint32Array);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Add tags to an entity.
|
|
127
|
+
*/
|
|
128
|
+
addTags(index: number, tags?: OsmTags): [number[], number[]] {
|
|
129
|
+
const tagKeys: number[] = [];
|
|
130
|
+
const tagValues: number[] = [];
|
|
131
|
+
|
|
132
|
+
if (tags) {
|
|
133
|
+
for (const [key, value] of Object.entries(tags)) {
|
|
134
|
+
tagKeys.push(this.stringTable.add(key));
|
|
135
|
+
tagValues.push(this.stringTable.add(String(value)));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.addTagKeysAndValues(index, tagKeys, tagValues);
|
|
140
|
+
|
|
141
|
+
return [tagKeys, tagValues];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add tags to an entity using key and value indexes.
|
|
146
|
+
*
|
|
147
|
+
* Tagged entities must be added in strictly ascending entity-index order and
|
|
148
|
+
* at most once per entity: the rank/popcount lookup orders tagged entities by
|
|
149
|
+
* index while `tagOffsets` records them in call order, so an out-of-order or
|
|
150
|
+
* repeated tagged index would silently misalign every later entity's tags.
|
|
151
|
+
* `Entities.addEntity` (append-only) satisfies this naturally.
|
|
152
|
+
*/
|
|
153
|
+
addTagKeysAndValues(index: number, keys: number[], values: number[]) {
|
|
154
|
+
if (this.indexBuilt) throw Error("Tag index already built.");
|
|
155
|
+
if (keys.length !== values.length) throw Error("Tag keys and values must have equal length.");
|
|
156
|
+
if (index < 0) throw Error(`Invalid entity index: ${index}`);
|
|
157
|
+
this.entityCount = Math.max(this.entityCount, index + 1);
|
|
158
|
+
if (keys.length === 0) return;
|
|
159
|
+
|
|
160
|
+
if (index <= this.lastTaggedEntityIndex) {
|
|
161
|
+
throw Error(
|
|
162
|
+
`Tagged entity ${index} added out of order (last tagged entity: ${this.lastTaggedEntityIndex}). Tagged entities must be added once, in ascending index order.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
this.lastTaggedEntityIndex = index;
|
|
166
|
+
this.taggedEntityIndexBuilder?.push(index);
|
|
167
|
+
this.tagOffsets.push(this.tagKeys.length);
|
|
168
|
+
this.tagKeys.pushMany(keys);
|
|
169
|
+
this.tagVals.pushMany(values);
|
|
170
|
+
|
|
171
|
+
keys.forEach((key) => {
|
|
172
|
+
const keyEntities = this.keyEntityIndexBuilder.get(key);
|
|
173
|
+
if (keyEntities) {
|
|
174
|
+
keyEntities.push(index);
|
|
175
|
+
} else {
|
|
176
|
+
this.keyEntityIndexBuilder.set(key, [index]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Finalize the tag index.
|
|
183
|
+
*
|
|
184
|
+
* Compacts arrays and builds the reverse key→entity index.
|
|
185
|
+
* Must be called before `hasKey()`.
|
|
186
|
+
*/
|
|
187
|
+
buildIndex() {
|
|
188
|
+
if (this.indexBuilt) return;
|
|
189
|
+
|
|
190
|
+
const taggedEntityIndexes = this.taggedEntityIndexBuilder;
|
|
191
|
+
if (!taggedEntityIndexes) throw Error("Tagged entity builder is unavailable.");
|
|
192
|
+
const bitWordCount = Math.ceil(this.entityCount / TAG_BITS_PER_WORD);
|
|
193
|
+
this.taggedEntityBits = new Uint32Array(
|
|
194
|
+
new BufferConstructor(bitWordCount * Uint32Array.BYTES_PER_ELEMENT),
|
|
195
|
+
);
|
|
196
|
+
for (let i = 0; i < taggedEntityIndexes.length; i++) {
|
|
197
|
+
const entityIndex = taggedEntityIndexes.at(i);
|
|
198
|
+
const wordIndex = entityIndex >>> 5;
|
|
199
|
+
this.taggedEntityBits[wordIndex] =
|
|
200
|
+
(this.taggedEntityBits[wordIndex] ?? 0) | (1 << (entityIndex & 31));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const checkpointCount = Math.ceil(this.entityCount / TAG_RANK_BLOCK_SIZE) + 1;
|
|
204
|
+
this.tagRankCheckpoints = new Uint32Array(
|
|
205
|
+
new BufferConstructor(checkpointCount * Uint32Array.BYTES_PER_ELEMENT),
|
|
206
|
+
);
|
|
207
|
+
let taggedIndex = 0;
|
|
208
|
+
for (let checkpoint = 0; checkpoint < checkpointCount; checkpoint++) {
|
|
209
|
+
const entityBoundary = checkpoint * TAG_RANK_BLOCK_SIZE;
|
|
210
|
+
while (
|
|
211
|
+
taggedIndex < taggedEntityIndexes.length &&
|
|
212
|
+
taggedEntityIndexes.at(taggedIndex) < entityBoundary
|
|
213
|
+
) {
|
|
214
|
+
taggedIndex++;
|
|
215
|
+
}
|
|
216
|
+
this.tagRankCheckpoints[checkpoint] = taggedIndex;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.tagOffsets.push(this.tagKeys.length);
|
|
220
|
+
this.tagOffsets.compact();
|
|
221
|
+
this.tagKeys.compact();
|
|
222
|
+
this.tagVals.compact();
|
|
223
|
+
|
|
224
|
+
// Convert the builder map to flat arrays for the reverse index
|
|
225
|
+
for (const [keyIndex, entityIndexes] of this.keyEntityIndexBuilder) {
|
|
226
|
+
this.keyIndexStart.set(keyIndex, this.keyEntities.length);
|
|
227
|
+
this.keyIndexCount.set(keyIndex, entityIndexes.length);
|
|
228
|
+
this.keyEntities.pushMany(entityIndexes);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.keyEntities.compact();
|
|
232
|
+
this.keyIndexStart.compact();
|
|
233
|
+
this.keyIndexCount.compact();
|
|
234
|
+
this.keyEntityIndexBuilder.clear();
|
|
235
|
+
this.taggedEntityIndexBuilder = null;
|
|
236
|
+
|
|
237
|
+
this.indexBuilt = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if the index is built and ready for use.
|
|
242
|
+
*/
|
|
243
|
+
isReady() {
|
|
244
|
+
return this.indexBuilt;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get the number of tags for an entity.
|
|
249
|
+
*/
|
|
250
|
+
cardinality(index: number): number {
|
|
251
|
+
const taggedIndex = this.getTaggedEntityIndex(index);
|
|
252
|
+
if (taggedIndex === -1) return 0;
|
|
253
|
+
return this.tagOffsets.at(taggedIndex + 1) - this.tagOffsets.at(taggedIndex);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Number of entities that have at least one tag. */
|
|
257
|
+
get taggedEntityCount(): number {
|
|
258
|
+
return this.indexBuilt
|
|
259
|
+
? Math.max(0, this.tagOffsets.length - 1)
|
|
260
|
+
: (this.taggedEntityIndexBuilder?.length ?? 0);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Iterate tagged entity indexes without allocating an intermediate array. */
|
|
264
|
+
*taggedEntityIndexes(): Generator<number> {
|
|
265
|
+
if (!this.indexBuilt) throw Error("Tag index not built.");
|
|
266
|
+
for (let wordIndex = 0; wordIndex < this.taggedEntityBits.length; wordIndex++) {
|
|
267
|
+
let word = this.taggedEntityBits[wordIndex] ?? 0;
|
|
268
|
+
while (word !== 0) {
|
|
269
|
+
const lowestBit = word & -word;
|
|
270
|
+
const bit = 31 - Math.clz32(lowestBit);
|
|
271
|
+
const entityIndex = wordIndex * TAG_BITS_PER_WORD + bit;
|
|
272
|
+
if (entityIndex < this.entityCount) yield entityIndex;
|
|
273
|
+
word = (word & (word - 1)) >>> 0;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get the tags for an entity.
|
|
280
|
+
*/
|
|
281
|
+
getTags(index: number): OsmTags | undefined {
|
|
282
|
+
const taggedIndex = this.getTaggedEntityIndex(index);
|
|
283
|
+
if (taggedIndex === -1) return;
|
|
284
|
+
const tagStart = this.tagOffsets.at(taggedIndex);
|
|
285
|
+
const tagCount = this.tagOffsets.at(taggedIndex + 1) - tagStart;
|
|
286
|
+
const tagKeyIndexes = this.tagKeys.array.slice(tagStart, tagStart + tagCount);
|
|
287
|
+
const tagValIndexes = this.tagVals.array.slice(tagStart, tagStart + tagCount);
|
|
288
|
+
const tags: OsmTags = {};
|
|
289
|
+
for (let i = 0; i < tagCount; i++) {
|
|
290
|
+
const keyIndex = tagKeyIndexes[i];
|
|
291
|
+
const valIndex = tagValIndexes[i];
|
|
292
|
+
if (keyIndex === undefined || valIndex === undefined)
|
|
293
|
+
throw Error("Tag key or value not found");
|
|
294
|
+
tags[this.stringTable.get(keyIndex)] = this.stringTable.get(valIndex);
|
|
295
|
+
}
|
|
296
|
+
return tags;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get tags from key and value indexes.
|
|
301
|
+
*/
|
|
302
|
+
getTagsFromIndices(keys: number[], values: number[]): OsmTags {
|
|
303
|
+
const tags: OsmTags = {};
|
|
304
|
+
for (let i = 0; i < keys.length; i++) {
|
|
305
|
+
const keyIndex = keys[i];
|
|
306
|
+
const valIndex = values[i];
|
|
307
|
+
if (keyIndex === undefined || valIndex === undefined)
|
|
308
|
+
throw Error("Tag key or value not found");
|
|
309
|
+
tags[this.stringTable.get(keyIndex)] = this.stringTable.get(valIndex);
|
|
310
|
+
}
|
|
311
|
+
return tags;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Find the index of a tag key.
|
|
316
|
+
*/
|
|
317
|
+
find(key: string): number {
|
|
318
|
+
return this.stringTable.find(key);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get all entity indexes that have a specific tag key.
|
|
323
|
+
*/
|
|
324
|
+
hasKey(keyIndex: number): number[] {
|
|
325
|
+
if (
|
|
326
|
+
keyIndex < 0 ||
|
|
327
|
+
keyIndex >= this.keyIndexStart.length ||
|
|
328
|
+
keyIndex >= this.keyIndexCount.length
|
|
329
|
+
)
|
|
330
|
+
return [];
|
|
331
|
+
const start = this.keyIndexStart.at(keyIndex) ?? 0;
|
|
332
|
+
const count = this.keyIndexCount.at(keyIndex) ?? 0;
|
|
333
|
+
return Array.from(this.keyEntities.array.subarray(start, start + count));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Create a unique composite index for a key=value pair.
|
|
338
|
+
* Uses row-major indexing: `key * width + val`.
|
|
339
|
+
*/
|
|
340
|
+
kvToIndex(key: number, val: number) {
|
|
341
|
+
const width = this.stringTable.length;
|
|
342
|
+
return key * width + val;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get transferable objects for passing to another thread.
|
|
347
|
+
*/
|
|
348
|
+
transferables(): TagsTransferables {
|
|
349
|
+
return {
|
|
350
|
+
tagEntityCount: this.entityCount,
|
|
351
|
+
taggedEntityBits: this.taggedEntityBits.buffer,
|
|
352
|
+
tagRankCheckpoints: this.tagRankCheckpoints.buffer,
|
|
353
|
+
tagOffsets: this.tagOffsets.array.buffer,
|
|
354
|
+
tagKeys: this.tagKeys.array.buffer,
|
|
355
|
+
tagVals: this.tagVals.array.buffer,
|
|
356
|
+
keyEntities: this.keyEntities.array.buffer,
|
|
357
|
+
keyIndexStart: this.keyIndexStart.array.buffer,
|
|
358
|
+
keyIndexCount: this.keyIndexCount.array.buffer,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Get the approximate memory requirements for a tag index in bytes.
|
|
364
|
+
*
|
|
365
|
+
* @param tagCount - Total key/value pairs. Each costs 12 bytes: key, value and the
|
|
366
|
+
* reverse key → entity index. The small per-key index arrays are not counted.
|
|
367
|
+
*/
|
|
368
|
+
static getBytesRequired(entityCount: number, taggedEntityCount = entityCount, tagCount = 0) {
|
|
369
|
+
return (
|
|
370
|
+
Math.ceil(entityCount / TAG_BITS_PER_WORD) * Uint32Array.BYTES_PER_ELEMENT +
|
|
371
|
+
(Math.ceil(entityCount / TAG_RANK_BLOCK_SIZE) + 1) * Uint32Array.BYTES_PER_ELEMENT +
|
|
372
|
+
(taggedEntityCount + 1) * Uint32Array.BYTES_PER_ELEMENT +
|
|
373
|
+
tagCount * 3 * Uint32Array.BYTES_PER_ELEMENT // tagKeys + tagVals + keyEntities
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Reconstruct a Tags index from transferable objects.
|
|
379
|
+
*/
|
|
380
|
+
static fromTransferables(stringTable: StringTable, transferables: TagsTransferables) {
|
|
381
|
+
const tagIndex = new Tags(stringTable, transferables);
|
|
382
|
+
tagIndex.indexBuilt = true;
|
|
383
|
+
return tagIndex;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Update a ContentHasher with the tags data.
|
|
388
|
+
* Hashes tag keys and values for each entity.
|
|
389
|
+
*/
|
|
390
|
+
updateHash(hasher: ContentHasher): ContentHasher {
|
|
391
|
+
return hasher
|
|
392
|
+
.updateNumber(this.entityCount)
|
|
393
|
+
.update(this.taggedEntityBits)
|
|
394
|
+
.update(this.tagOffsets.array)
|
|
395
|
+
.update(this.tagKeys.array)
|
|
396
|
+
.update(this.tagVals.array);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Return the compact tagged-entity ordinal, or -1 when the entity has no tags.
|
|
401
|
+
*
|
|
402
|
+
* The ordinal is the entity's rank among tagged entities: read the cached
|
|
403
|
+
* prefix count at the nearest 256-entity checkpoint, popcount the presence
|
|
404
|
+
* words between the checkpoint and the entity's word, then popcount the bits
|
|
405
|
+
* below the entity within its own word. The result indexes `tagOffsets`.
|
|
406
|
+
*/
|
|
407
|
+
private getTaggedEntityIndex(index: number): number {
|
|
408
|
+
if (index < 0 || index >= this.entityCount) return -1;
|
|
409
|
+
const wordIndex = index >>> 5;
|
|
410
|
+
const word = this.taggedEntityBits[wordIndex] ?? 0;
|
|
411
|
+
const bit = index & 31;
|
|
412
|
+
if ((word & (1 << bit)) === 0) return -1;
|
|
413
|
+
|
|
414
|
+
const checkpoint = index >>> 8;
|
|
415
|
+
let rank = this.tagRankCheckpoints[checkpoint] ?? 0;
|
|
416
|
+
const checkpointWord = checkpoint * (TAG_RANK_BLOCK_SIZE / TAG_BITS_PER_WORD);
|
|
417
|
+
for (let i = checkpointWord; i < wordIndex; i++) {
|
|
418
|
+
rank += popcount(this.taggedEntityBits[i] ?? 0);
|
|
419
|
+
}
|
|
420
|
+
const lowerBitMask = bit === 0 ? 0 : 0xffffffff >>> (TAG_BITS_PER_WORD - bit);
|
|
421
|
+
return rank + popcount(word & lowerBitMask);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Count set bits in an unsigned 32-bit integer. */
|
|
426
|
+
function popcount(value: number): number {
|
|
427
|
+
const pairs = value - ((value >>> 1) & 0x55555555);
|
|
428
|
+
const nibbles = (pairs & 0x33333333) + ((pairs >>> 2) & 0x33333333);
|
|
429
|
+
return (((nibbles + (nibbles >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
|
|
296
430
|
}
|