@osmix/core 0.0.1

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 ADDED
@@ -0,0 +1,11 @@
1
+ # @osmix/core
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Initial release
8
+ - Updated dependencies
9
+ - @osmix/json@0.0.1
10
+ - @osmix/pbf@0.0.1
11
+ - @osmix/shared@0.0.1
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # @osmix/core
2
+
3
+ @osmix/core wraps the `Osmix` index: a typed-array OpenStreetMap engine that reads `.osm.pbf` streams, builds spatial indexes, and emits JSON, PBF, or raster tiles without leaving modern JavaScript runtimes.
4
+
5
+ ## Highlights
6
+
7
+ - Ingest `.osm.pbf` sources into node, way, and relation stores backed by transferable typed arrays using readers from [`@osmix/pbf`](../pbf/README.md).
8
+ - Run fast bounding-box searches with KDBush/Flatbush and convert matches straight to GeoJSON.
9
+ - Trim extracts, write new PBF buffers, or stream entities to downstream tooling with [`@osmix/json`](../json/README.md) or [`@osmix/pbf`](../pbf/README.md).
10
+ - Pair with [`@osmix/change`](../change/README.md) when you need deduplication, intersection, or merge pipelines.
11
+ - Ship fully indexed datasets across workers via `transferables()` + `Osmix.from`.
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install @osmix/core
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Load a PBF in one call
22
+
23
+ ```ts
24
+ import { Osmix } from "@osmix/core"
25
+ import { readFile } from "node:fs/promises"
26
+
27
+ const osm = await Osmix.fromPbf(readFile("example.osm.pbf"))
28
+
29
+ console.log(osm.nodes.size, osm.ways.size, osm.relations.size)
30
+ ```
31
+
32
+ `fromPbf` accepts `ArrayBufferLike`, async (iterable) chunks, or Web `ReadableStream` inputs.
33
+
34
+ ### Stream with progress and extraction
35
+
36
+ Pass a logger or set one later to receive throttled progress messages. Provide `extractBbox` to keep only features intersecting the desired bounds while parsing.
37
+
38
+ ```ts
39
+ import { Osmix } from "@osmix/core"
40
+ import { createReadStream } from "node:fs"
41
+
42
+ const osm = new Osmix({ id: "planet-seattle" })
43
+ osm.setLogger((message) => console.log(message))
44
+
45
+ await osm.readPbf(createReadStream("planet.osm.pbf"), {
46
+ extractBbox: [-122.5, 47.45, -122.2, 47.75],
47
+ })
48
+ ```
49
+
50
+ ### Build synthetic fixtures
51
+
52
+ When generating test data, insert entities directly and call `buildIndexes()` before querying.
53
+
54
+ ```ts
55
+ const osm = new Osmix({ id: "fixture" })
56
+ osm.nodes.addNode({ id: 1, lon: -0.1, lat: 51.5, tags: { amenity: "cafe" } })
57
+ osm.ways.addWay({ id: 10, refs: [1], tags: { highway: "service" } })
58
+ osm.buildIndexes()
59
+ ```
60
+
61
+ ## Query the index
62
+
63
+ - `osm.get("node", 123)` or `osm.getById("n123")` fetches specific entities.
64
+ - `osm.nodes.sorted()` / `osm.ways.sorted()` iterate ids in ascending order.
65
+ - `osm.getEntityGeoJson(entity)` returns a GeoJSON feature with coordinates resolved from the
66
+ indexed nodes.
67
+
68
+ Spatial lookups emit compact typed arrays that can be transferred across Web Workers and used directly in `Deck.gl`:
69
+
70
+ ```ts
71
+ const bbox = [-122.34, 47.57, -122.30, 47.61]
72
+ const { ids, positions } = osm.getNodesInBbox(bbox)
73
+
74
+ const { ids: wayIds, positions: wayCoords, startIndices } = osm.getWaysInBbox(bbox)
75
+ ```
76
+
77
+ `positions` stores `[lon, lat]` pairs; `startIndices` splits `wayCoords` into per-way sequences.
78
+
79
+ ## Extract and export data
80
+
81
+ ```ts
82
+ const downtown = osm.extract([-122.35, 47.60, -122.32, 47.62])
83
+
84
+ const entityStream = downtown.toEntityStream() // header + entities
85
+ const pbfStream = downtown.toPbfStream() // Uint8Array chunks
86
+ const pbfBuffer = await downtown.toPbfBuffer()
87
+ ```
88
+
89
+ `extract` returns a new, fully indexed `Osmix` instance with the header bbox updated.
90
+
91
+ ## Changesets and merging
92
+
93
+ Change orchestration now lives in the dedicated [`@osmix/change`](../change/README.md) package. Install it alongside
94
+ `@osmix/core` when you want to deduplicate entities, generate direct merges, or script intersection creation:
95
+
96
+ ```ts
97
+ import { Osmix } from "@osmix/core"
98
+ import { merge, OsmixChangeset } from "@osmix/change"
99
+ import { readFile } from "node:fs/promises"
100
+
101
+ const base = await Osmix.fromPbf(readFile("base.osm.pbf"))
102
+ const patch = await Osmix.fromPbf(readFile("patch.osm.pbf"))
103
+
104
+ const changeset = new OsmixChangeset(base)
105
+ changeset.deduplicateWays(base.ways)
106
+
107
+ const merged = await merge(base, patch, { directMerge: true })
108
+ ```
109
+
110
+ Refer to the [`@osmix/change` README](../change/README.md) for the full API surface.
111
+
112
+ ## Transfer between threads
113
+
114
+ Typed arrays keep the index transferable.
115
+
116
+ ```ts
117
+ // main thread
118
+ const payload = osm.transferables()
119
+ const buffers = []
120
+ const collect = (value: unknown) => {
121
+ if (value instanceof ArrayBuffer) buffers.push(value)
122
+ else if (ArrayBuffer.isView(value)) buffers.push(value.buffer)
123
+ else if (Array.isArray(value)) value.forEach(collect)
124
+ else if (value && typeof value === "object")
125
+ Object.values(value).forEach(collect)
126
+ return buffers
127
+ }
128
+
129
+ worker.postMessage(payload, { transfer: collect(payload) })
130
+
131
+ // worker
132
+ import { Osmix } from "@osmix/core"
133
+
134
+ self.addEventListener("message", ({ data }) => {
135
+ const osm = Osmix.from(data)
136
+ // ready for queries immediately
137
+ })
138
+ ```
139
+
140
+ ## API overview
141
+
142
+ - `Osmix` – ingest PBF sources, build indexes, query entities, extract subsets, and emit JSON/PBF.
143
+ - `Nodes` / `Ways` / `Relations` – typed-array backed stores exposed for advanced workflows.
144
+ - `OsmixRasterTile` – rasterize indexed data into tile-sized buffers with lon/lat helpers.
145
+ - Utilities – `throttle`, plus the shared type definitions used across the package.
146
+ - Changesets and merge helpers now live in [`@osmix/change`](../change/README.md).
147
+
148
+ ## Environment and limitations
149
+
150
+ - Requires runtimes with Web Streams, `TextEncoder`/`TextDecoder`, and zlib-compatible `CompressionStream`/`DecompressionStream` support (Bun, Node 20+, modern browsers).
151
+ - `readPbf` expects dense-node blocks; PBFs that omit dense encodings are currently unsupported.
152
+ - Filtering during ingest depends on node membership; emit nodes, then ways, then relations when supplying custom entity generators.
153
+
154
+ ## Development
155
+
156
+ - `bun run test packages/core`
157
+ - `bun run lint packages/core`
158
+ - `bun run typecheck packages/core`
159
+
160
+ Run `bun run check` at the repo root before publishing to ensure formatting, lint, and type coverage.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package",
3
+ "name": "@osmix/core",
4
+ "type": "module",
5
+ "version": "0.0.1",
6
+ "description": "An OSM PBF reader, writer, and merge tool written in TypeScript",
7
+ "main": "./src/index.ts",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ]
23
+ },
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/conveyal/osmix.git"
28
+ },
29
+ "homepage": "https://github.com/conveyal/osmix#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/conveyal/osmix/issues"
32
+ },
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsc",
36
+ "bench": "vitest bench",
37
+ "test": "vitest",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "devDependencies": {
41
+ "@types/geojson": "catalog:",
42
+ "typescript": "catalog:",
43
+ "vitest": "catalog:"
44
+ },
45
+ "dependencies": {
46
+ "@osmix/json": "workspace:*",
47
+ "@osmix/pbf": "workspace:*",
48
+ "@osmix/shared": "workspace:*",
49
+ "flatbush": "^4.4.1",
50
+ "kdbush": "mourner/kdbush#16416cd",
51
+ "sweepline-intersections": "^1.5.0"
52
+ }
53
+ }
@@ -0,0 +1,95 @@
1
+ import type { OsmEntity, OsmEntityType, OsmTags } from "@osmix/json"
2
+ import { type IdOrIndex, Ids, type IdsTransferables } from "./ids"
3
+ import type StringTable from "./stringtable"
4
+ import { Tags, type TagsTransferables } from "./tags"
5
+
6
+ export interface EntitiesTransferables
7
+ extends IdsTransferables,
8
+ TagsTransferables {}
9
+
10
+ export abstract class Entities<T extends OsmEntity> {
11
+ indexType: OsmEntityType
12
+ stringTable: StringTable
13
+ ids: Ids
14
+ tags: Tags
15
+
16
+ private indexBuilt = false
17
+
18
+ constructor(
19
+ indexType: OsmEntityType,
20
+ stringTable: StringTable,
21
+ ids?: Ids,
22
+ tags?: Tags,
23
+ ) {
24
+ this.stringTable = stringTable
25
+ this.indexType = indexType
26
+ this.ids = ids ?? new Ids()
27
+ this.tags = tags ?? new Tags(stringTable)
28
+ }
29
+
30
+ get isReady() {
31
+ return this.ids.isReady && this.tags.isReady && this.indexBuilt
32
+ }
33
+
34
+ get size() {
35
+ return this.ids.size
36
+ }
37
+
38
+ abstract buildEntityIndex(): void
39
+
40
+ buildIndex() {
41
+ console.time(`${this.indexType}Index.finish`)
42
+ this.ids.buildIndex()
43
+ this.tags.buildIndex()
44
+ this.buildEntityIndex()
45
+ this.indexBuilt = true
46
+ console.timeEnd(`${this.indexType}Index.finish`)
47
+ }
48
+
49
+ abstract getFullEntity(index: number, id: number, tags?: OsmTags): T
50
+
51
+ get(idOrIndex: IdOrIndex): T | null {
52
+ const [index, id] = this.ids.idOrIndex(idOrIndex)
53
+ if (index === -1) return null
54
+ return this.getFullEntity(index, id, this.tags.getTags(index))
55
+ }
56
+
57
+ getByIndex(index: number): T {
58
+ return this.getFullEntity(
59
+ index,
60
+ this.ids.at(index),
61
+ this.tags.getTags(index),
62
+ )
63
+ }
64
+
65
+ getEntitiesByIndex(indexes: number[]): T[] {
66
+ const entities: T[] = []
67
+ for (const index of indexes) {
68
+ const entity = this.getByIndex(index)
69
+ if (entity) entities.push(entity)
70
+ else throw Error(`Entity not found at index ${index}`)
71
+ }
72
+ return entities
73
+ }
74
+
75
+ *[Symbol.iterator](): Generator<T> {
76
+ for (let i = 0; i < this.size; i++) {
77
+ yield this.getFullEntity(i, this.ids.at(i), this.tags.getTags(i))
78
+ }
79
+ }
80
+
81
+ getById(id: number): T | null {
82
+ const index = this.ids.getIndexFromId(id)
83
+ if (index !== -1)
84
+ return this.getFullEntity(index, id, this.tags.getTags(index))
85
+ return null
86
+ }
87
+
88
+ *sorted(): Generator<T> {
89
+ for (const id of this.ids.sorted) {
90
+ const index = this.ids.getIndexFromId(id)
91
+ if (index === -1) throw Error(`Entity not found at id ${id}`)
92
+ yield this.getFullEntity(index, id, this.tags.getTags(index))
93
+ }
94
+ }
95
+ }
package/src/ids.ts ADDED
@@ -0,0 +1,210 @@
1
+ import { assertValue } from "@osmix/shared/assert"
2
+ import {
3
+ BufferConstructor,
4
+ type BufferType,
5
+ ResizeableTypedArray,
6
+ } from "./typed-arrays"
7
+
8
+ export type IdOrIndex = { id: number } | { index: number }
9
+
10
+ const BLOCK_SIZE = 256
11
+
12
+ export interface IdsTransferables {
13
+ ids: BufferType
14
+ sortedIds: BufferType
15
+ sortedIdPositionToIndex: BufferType
16
+ anchors: BufferType
17
+ idsAreSorted: boolean
18
+ }
19
+
20
+ /**
21
+ * Efficiently store and lookup IDs.
22
+ *
23
+ * This is used to store IDs in a sorted array and then use binary search to find the index of an ID.
24
+ * Maps max out at 2^32 IDs.
25
+ */
26
+ export class Ids {
27
+ private ids: ResizeableTypedArray<Float64Array>
28
+ private indexBuilt = false
29
+ private idsAreSorted = true
30
+ private idsSorted: Float64Array
31
+ private sortedIdPositionToIndex: Uint32Array
32
+ private anchors: Float64Array
33
+
34
+ static from({
35
+ ids,
36
+ sortedIds,
37
+ sortedIdPositionToIndex,
38
+ anchors,
39
+ idsAreSorted,
40
+ }: IdsTransferables) {
41
+ const idIndex = new Ids()
42
+ idIndex.ids = ResizeableTypedArray.from(Float64Array, ids)
43
+ idIndex.idsSorted = new Float64Array(sortedIds)
44
+ idIndex.sortedIdPositionToIndex = new Uint32Array(sortedIdPositionToIndex)
45
+ idIndex.anchors = new Float64Array(anchors)
46
+ idIndex.indexBuilt = true
47
+ idIndex.idsAreSorted = idsAreSorted
48
+ return idIndex
49
+ }
50
+
51
+ constructor() {
52
+ this.ids = new ResizeableTypedArray(Float64Array)
53
+ this.idsSorted = new Float64Array(new BufferConstructor(0))
54
+ this.sortedIdPositionToIndex = new Uint32Array(new BufferConstructor(0))
55
+ this.anchors = new Float64Array(new BufferConstructor(0))
56
+ }
57
+
58
+ transferables(): IdsTransferables {
59
+ return {
60
+ ids: this.ids.array.buffer,
61
+ sortedIds: this.idsSorted.buffer,
62
+ sortedIdPositionToIndex: this.sortedIdPositionToIndex.buffer,
63
+ anchors: this.anchors.buffer,
64
+ idsAreSorted: this.idsAreSorted,
65
+ }
66
+ }
67
+
68
+ get size() {
69
+ return this.ids.length
70
+ }
71
+
72
+ get isReady() {
73
+ return this.indexBuilt
74
+ }
75
+
76
+ isSorted() {
77
+ return this.idsAreSorted
78
+ }
79
+
80
+ add(id: number): void {
81
+ if (this.indexBuilt) throw Error("ID index already built.")
82
+ if (this.ids.length > 0 && id < this.ids.at(-1)) this.idsAreSorted = false
83
+ this.ids.push(id)
84
+ }
85
+
86
+ at(index: number): number {
87
+ return this.ids.at(index)
88
+ }
89
+
90
+ has(id: number): boolean {
91
+ return this.getIndexFromId(id) !== -1
92
+ }
93
+
94
+ /**
95
+ * Build the index of IDs to positions.
96
+ *
97
+ * If the IDs are not sorted, we need to sort them and build a new index.
98
+ * If the IDs are sorted, we can use the existing index.
99
+ */
100
+ buildIndex() {
101
+ if (this.indexBuilt) throw Error("ID index already build.")
102
+ this.ids.compact()
103
+ if (!this.idsAreSorted) {
104
+ console.warn("IDs were not sorted. Sorting now...")
105
+ // Build the sorted index
106
+ const idsBuffer = new BufferConstructor(
107
+ this.size * Float64Array.BYTES_PER_ELEMENT,
108
+ )
109
+ const posBuffer = new BufferConstructor(
110
+ this.size * Uint32Array.BYTES_PER_ELEMENT,
111
+ )
112
+ this.idsSorted = new Float64Array(idsBuffer)
113
+ this.sortedIdPositionToIndex = new Uint32Array(posBuffer)
114
+
115
+ // Fill and sort with positions.
116
+ for (let i = 0; i < this.size; i++) {
117
+ this.idsSorted[i] = this.ids.at(i)
118
+ this.sortedIdPositionToIndex[i] = i
119
+ }
120
+
121
+ // Sort by id, carrying position; use native sort on chunks or a custom radix/merge for stability.
122
+ // For simplicity:
123
+ const tmp: { id: number; pos: number }[] = Array.from(
124
+ { length: this.size },
125
+ (_, i) => ({
126
+ id: this.idsSorted[i] as number,
127
+ pos: this.sortedIdPositionToIndex[i] as number,
128
+ }),
129
+ )
130
+ tmp.sort((a, b) => a.id - b.id)
131
+ tmp.forEach(({ id, pos }, i) => {
132
+ this.idsSorted[i] = id
133
+ this.sortedIdPositionToIndex[i] = pos
134
+ })
135
+ } else {
136
+ // Point to the same array
137
+ this.idsSorted = this.ids.array
138
+ // Create the sortedIdPositionToIndex array
139
+ const posBuffer = new BufferConstructor(
140
+ this.size * Uint32Array.BYTES_PER_ELEMENT,
141
+ )
142
+ this.sortedIdPositionToIndex = new Uint32Array(posBuffer)
143
+ for (let i = 0; i < this.size; i++) {
144
+ this.sortedIdPositionToIndex[i] = i
145
+ }
146
+ }
147
+
148
+ // Build anchors (every blockSize-th key)
149
+ const aLen = Math.ceil(this.size / BLOCK_SIZE)
150
+ const sab = new BufferConstructor(aLen * Float64Array.BYTES_PER_ELEMENT)
151
+ this.anchors = new Float64Array(sab, 0, aLen)
152
+ for (let j = 0; j < aLen; j++) {
153
+ const id = this.idsSorted[Math.min(j * BLOCK_SIZE, this.size - 1)]
154
+ assertValue(id, "ID is undefined")
155
+ this.anchors[j] = id
156
+ }
157
+
158
+ this.indexBuilt = true
159
+ }
160
+
161
+ // Lookup id → index
162
+ getIndexFromId(id: number): number {
163
+ if (!this.indexBuilt) throw Error("IdIndex not built.")
164
+
165
+ // binary search anchors
166
+ let lo = 0
167
+ let hi = this.anchors.length - 1
168
+ while (lo < hi) {
169
+ const mid = (lo + hi + 1) >>> 1
170
+ const anchor = this.anchors[mid]
171
+ assertValue(anchor, "Anchor is undefined")
172
+ if (anchor <= id) lo = mid
173
+ else hi = mid - 1
174
+ }
175
+ const start = lo * BLOCK_SIZE
176
+ const end = Math.min(start + BLOCK_SIZE, this.idsSorted.length)
177
+
178
+ // binary search within block
179
+ let l = start
180
+ let r = end - 1
181
+ while (l <= r) {
182
+ const m = (l + r) >>> 1
183
+ const v = this.idsSorted[m]
184
+ assertValue(v, "Value is undefined")
185
+ if (v === id) {
186
+ if (this.idsAreSorted) return m
187
+ const index = this.sortedIdPositionToIndex[m]
188
+ assertValue(index, "Position is undefined")
189
+ return index
190
+ }
191
+ if (v < id) l = m + 1
192
+ else r = m - 1
193
+ }
194
+
195
+ // ID not found
196
+ return -1
197
+ }
198
+
199
+ /**
200
+ * Pass an ID or an index, get both.
201
+ */
202
+ idOrIndex(i: IdOrIndex): [index: number, id: number] {
203
+ if ("id" in i) return [this.getIndexFromId(i.id), i.id]
204
+ return [i.index, this.at(i.index)]
205
+ }
206
+
207
+ get sorted() {
208
+ return this.idsSorted
209
+ }
210
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference path="./types/kdbush.d.ts" />
2
+
3
+ export { Nodes } from "./nodes"
4
+ export { Osmix } from "./osmix"
5
+ export { Relations } from "./relations"
6
+ export { throttle } from "./utils"
7
+ export { Ways } from "./ways"