{"_id":"@ateliercartographie/geoarrow-deck-stream","_rev":"2-b572e5c031b566eb9334e7e6125674a2","name":"@ateliercartographie/geoarrow-deck-stream","dist-tags":{"latest":"1.2.0"},"versions":{"1.1.3":{"name":"@ateliercartographie/geoarrow-deck-stream","version":"1.1.3","keywords":["geoarrow","deck.gl","d3-geo","webgl","geospatial","projection","binary","high-performance"],"author":{"name":"Thomas Ansart — Atelier de cartographie de Sciences Po"},"license":"ISC","_id":"@ateliercartographie/geoarrow-deck-stream@1.1.3","maintainers":[{"name":"tombor","email":"thomas2ansart@gmail.com"}],"homepage":"https://github.com/AtelierCartographie/geoarrow-deck-stream#readme","bugs":{"url":"https://github.com/AtelierCartographie/geoarrow-deck-stream/issues"},"dist":{"shasum":"2e8fe8b12e049634e069f73a78740bfc55b1064f","tarball":"https://registry.npmjs.org/@ateliercartographie/geoarrow-deck-stream/-/geoarrow-deck-stream-1.1.3.tgz","fileCount":67,"integrity":"sha512-1woBDq78l8TY8cqAIC1YGmkClW8QzznyMTcawjppAETeKeFJHIW9Mv3BbtskffhfMQyvexkxyI3JGT1R0cKdZA==","signatures":[{"sig":"MEQCIHMwJ50wF1gH/NBOsg1OHuGC3Sj+3SY6C6oiuAsfXuwhAiAkqDzpzR317tyhQ4qUuUZjB1THtZVNBjl2FnXzdD8IWw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1403054},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"83b58991ec6ff9a388f1c59cbc503f3a9dad0dad","scripts":{"dev":"tsc --watch","lint":"eslint src --ext .ts","test":"vitest run --watch=false","build":"tsc","prepare":"npm run build","preview":"vite examples --port 8000","typecheck":"tsc --noEmit","build:bundle":"rollup -c"},"_npmUser":{"name":"tombor","email":"thomas2ansart@gmail.com"},"repository":{"url":"git+https://github.com/AtelierCartographie/geoarrow-deck-stream.git","type":"git"},"_npmVersion":"10.9.2","description":"High-performance GeoArrow to Deck.gl binary parser using d3-geo streaming","directories":{},"sideEffects":false,"_nodeVersion":"22.22.2","dependencies":{"d3-geo":"^3.1.1","earcut":"^3.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rollup":"^4.55.1","vitest":"^4.0.0","typescript":"^5.9.3","apache-arrow":"^21.0.0","@deck.gl/core":"^9.0.0","@types/d3-geo":"^3.1.0","@types/earcut":"^3.0.0","@deck.gl/layers":"^9.0.0","@geoarrow/geoarrow-js":"^0.3.3","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.3.0","@rollup/plugin-node-resolve":"^16.0.3"},"peerDependencies":{"apache-arrow":">=21.0.0","@deck.gl/core":">=9.0.0","@deck.gl/layers":">=9.0.0","@geoarrow/geoarrow-js":">=0.3.0"},"_npmOperationalInternal":{"tmp":"tmp/geoarrow-deck-stream_1.1.3_1783506039911_0.7498039378850108","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@ateliercartographie/geoarrow-deck-stream","version":"1.2.0","description":"High-performance GeoArrow to Deck.gl binary parser using d3-geo streaming","type":"module","main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./worker":{"types":"./dist/worker.d.ts","import":"./dist/worker.js"},"./parse-worker":{"types":"./dist/parse-worker.d.ts","import":"./dist/parse-worker.js"}},"scripts":{"build":"tsc","build:bundle":"rollup -c","prepare":"npm run build","dev":"tsc --watch","preview":"vite examples --port 8000","test":"vitest run --watch=false","lint":"eslint src --ext .ts","typecheck":"tsc --noEmit"},"keywords":["geoarrow","deck.gl","d3-geo","webgl","geospatial","projection","binary","high-performance"],"author":{"name":"Thomas Ansart — Atelier de cartographie de Sciences Po"},"license":"ISC","repository":{"type":"git","url":"git+https://github.com/AtelierCartographie/geoarrow-deck-stream.git"},"bugs":{"url":"https://github.com/AtelierCartographie/geoarrow-deck-stream/issues"},"homepage":"https://github.com/AtelierCartographie/geoarrow-deck-stream#readme","publishConfig":{"access":"public"},"dependencies":{"d3-geo":"^3.1.1","earcut":"^3.2.0"},"devDependencies":{"@deck.gl/core":"^9.0.0","@deck.gl/layers":"^9.0.0","@rollup/plugin-node-resolve":"^16.0.3","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.3.0","@types/d3-geo":"^3.1.0","@types/earcut":"^3.0.0","apache-arrow":"^21.0.0","@geoarrow/geoarrow-js":"^0.3.3","rollup":"^4.55.1","typescript":"^5.9.3","vitest":"^4.0.0"},"peerDependencies":{"apache-arrow":">=21.0.0","@deck.gl/core":">=9.0.0","@deck.gl/layers":">=9.0.0","@geoarrow/geoarrow-js":">=0.3.0"},"_id":"@ateliercartographie/geoarrow-deck-stream@1.2.0","gitHead":"f722ddeca507d8e49c8d25de35ad235c2aee632f","_nodeVersion":"22.22.2","_npmVersion":"10.9.2","dist":{"integrity":"sha512-R1joc9KfGehmVu2yPWQ8mHSx2rFLFIGJEl8vaL0H3iOBfZZJTV6vzj4qFrXkGzwMNX+1ZTEIbhULU4lxoD3KLw==","shasum":"c6636231733b89a8614c1d9f59c39adade214d5a","tarball":"https://registry.npmjs.org/@ateliercartographie/geoarrow-deck-stream/-/geoarrow-deck-stream-1.2.0.tgz","fileCount":97,"unpackedSize":1498645,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD1VhbI98XPSCkJr3sYpdwZXfwZbxRLTRJJTbqjmQrqCQIgA/vD4EtJ6kkPmhGobFB7Q04AuIVhKOwqWjxH4jPpfcg="}]},"_npmUser":{"name":"tombor","email":"thomas2ansart@gmail.com"},"directories":{},"maintainers":[{"name":"tombor","email":"thomas2ansart@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/geoarrow-deck-stream_1.2.0_1784182243476_0.30015665251512336"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-08T10:20:39.741Z","modified":"2026-07-16T06:10:43.809Z","1.1.3":"2026-07-08T10:20:40.153Z","1.2.0":"2026-07-16T06:10:43.655Z"},"bugs":{"url":"https://github.com/AtelierCartographie/geoarrow-deck-stream/issues"},"author":{"name":"Thomas Ansart — Atelier de cartographie de Sciences Po"},"license":"ISC","homepage":"https://github.com/AtelierCartographie/geoarrow-deck-stream#readme","keywords":["geoarrow","deck.gl","d3-geo","webgl","geospatial","projection","binary","high-performance"],"repository":{"type":"git","url":"git+https://github.com/AtelierCartographie/geoarrow-deck-stream.git"},"description":"High-performance GeoArrow to Deck.gl binary parser using d3-geo streaming","maintainers":[{"name":"tombor","email":"thomas2ansart@gmail.com"}],"readme":"# GeoArrow Deck Stream\n\nHigh-performance GeoArrow to Deck.gl binary parser using d3-geo streaming.\n\n## Overview\n\nThis module provides a **zero-serialization pipeline** for transforming GeoArrow geometries into Deck.gl-ready binary buffers. It handles both:\n\n1. **Reprojection**: Transform WGS84 (lon/lat) data to any d3-geo projection\n2. **Pass-through**: Standardize already-projected data (Lambert 93, UTM, etc.) for Deck.gl\n\n### Key Features\n\n- ⚡ **Zero-copy Arrow access**: Reads directly from Arrow binary buffers\n- 🚀 **No object allocation**: No `{x, y}` objects in the hot path\n- 🔄 **Unified code path**: Same API for reprojection and identity transforms\n- ✂️ **Automatic feature splitting**: Handles antimeridian/clipping correctly\n- 🎯 **Feature ID mapping**: Split geometries maintain reference to source data\n- 🌐 **Full Geometry Support**: Points, LineStrings, Polygons (with holes), Multi-geometries\n- 🌪️ **Spherical Winding**: Automatic correction of ring direction (Rewind / Right-Hand Rule) using `d3-geo`\n- 🔺 **Integrated Triangulation**: Uses `earcut` internally to perform triangulation for `SolidPolygonLayer`\n- 🧵 **Off-main-thread parsing**: Web Worker API with serializable projection specs and transferable outputs — see [Off-Main-Thread Parsing](#off-main-thread-parsing-web-worker)\n\n## Supported Input Formats\n\nThe library accepts Apache Arrow tables with geometry columns in three formats:\n\n| Format                   | Extension Name                                | Description                                                                                    |\n| ------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------- |\n| **GeoArrow Interleaved** | `geoarrow.point`, `geoarrow.linestring`, etc. | Coordinates as `[x,y,x,y,...]` in a single `Float64Array`. Default in DuckDB-WASM, GeoParquet. |\n| **GeoArrow Separated**   | Same as above                                 | Coordinates as separate `x[]` and `y[]` arrays (Apache Arrow Struct encoding).                 |\n| **GeoArrow WKB**         | `geoarrow.wkb`                                | Well-Known Binary blobs in a `Binary` column. Auto-decoded to native GeoArrow before parsing.  |\n\nAll geometry types are supported: **Point**, **MultiPoint**, **LineString**, **MultiLineString**, **Polygon** (with holes), **MultiPolygon**.\n\n### WKB Support\n\nWKB input (`geoarrow.wkb`) is **transparently decoded** — all parse functions (`parseGeometry`, `parsePoints`, `parsePolygonsToSolid`) detect WKB columns and convert them to native GeoArrow automatically. No additional code is needed.\n\nFor explicit control, you can also decode WKB manually:\n\n```typescript\nimport { decodeWkbColumn, isWkbGeometryColumn } from \"@ateliercartographie/geoarrow-deck-stream\";\n\n// Check if table has WKB geometry\nif (isWkbGeometryColumn(table)) {\n  const { table: nativeTable, geometryType } = decodeWkbColumn(table);\n  console.log(`Decoded WKB → ${geometryType}`); // e.g. \"multipolygon\"\n}\n```\n\nSupported WKB features:\n\n- Little-endian and big-endian byte order\n- 2D, 3D (Z), and 4D (ZM) coordinates (Z/M values are dropped for Deck.gl)\n- ISO WKB and EWKB (PostGIS) variants with SRID\n- NULL geometry handling (validity bitmap)\n- Mixed type promotion (e.g., Polygon + MultiPolygon → MultiPolygon)\n\n## CRS Detection & Projection Strategy\n\nGeoArrow inputs may be in WGS84 (requiring reprojection) or already projected (requiring pass-through). This library provides utilities to detect the CRS and choose the correct strategy, compliant with GeoArrow and GeoParquet specifications.\n\n```typescript\nimport {\n  isWGS84,\n  getProjectionStrategy,\n  extractCRSFromArrow,\n} from \"@ateliercartographie/geoarrow-deck-stream\";\nimport { geoMercator, geoIdentity } from \"d3-geo\";\n\n// 1. Quick Check (Most Common)\nconst projection = isWGS84(table)\n  ? geoMercator().fitSize([width, height], bounds)\n  : geoIdentity(); // Pass-through for pre-projected data\n\n// 2. Advanced Strategy\nconst strategy = getProjectionStrategy(table);\n// returns: 'reproject' | 'passthrough' | 'unknown'\n\nif (strategy === \"unknown\") {\n  console.warn(\"CRS unknown - assuming WGS84 or asking user\");\n}\n\n// 3. Detailed CRS Info\nconst info = extractCRSFromArrow(table);\nif (info.crsInfo?.isWGS84) {\n  console.log(\"Data is WGS84 (EPSG:4326/OGC:CRS84)\");\n}\n```\n\n## Installation\n\n```bash\nnpm install @ateliercartographie/geoarrow-deck-stream d3-geo earcut\n```\n\n## Quick Start\n\n```typescript\nimport {\n  parsePolygonsToSolid,\n  createSolidPolygonLayerProps,\n} from \"@ateliercartographie/geoarrow-deck-stream\";\nimport { geoOrthographic } from \"d3-geo\";\nimport { SolidPolygonLayer } from \"@deck.gl/layers\";\n\n// From DuckDB-WASM query result\nconst result = await conn.query(`SELECT geometry FROM my_countries`);\nconst table = result; // Pass the Arrow Table directly\n\n// Reproject to globe, fix winding, and triangulate\nconst data = parsePolygonsToSolid(table, {\n  projection: geoOrthographic().rotate([-10, -40]),\n  rewind: true, // Default: true (Fixes Right-Hand Rule for spherical rendering)\n});\n\n// Create Deck.gl layer\nconst layer = new SolidPolygonLayer({\n  id: \"countries\",\n  ...createSolidPolygonLayerProps(data),\n  getFillColor: [0, 150, 200],\n  getLineColor: [255, 255, 255],\n});\n```\n\n### DuckDB-WASM Integration\n\n#### DuckDB-WASM ≥ 1.33 (transparent `geoarrow.wkb`)\n\nSince [v1.33.1-dev41](https://github.com/duckdb/duckdb-wasm/pull/2200), DuckDB-WASM natively returns geometry columns as `geoarrow.wkb`. The library handles this transparently — no special handling needed:\n\n```typescript\nimport { parsePolygonsToSolid } from \"@ateliercartographie/geoarrow-deck-stream\";\nimport { geoEqualEarth } from \"d3-geo\";\n\nconst db = await AsyncDuckDB.create(/* ... */);\nconst conn = await db.connect();\n\n// Load the spatial extension\nawait conn.query(`INSTALL spatial; LOAD spatial;`);\n\n// Query geometry directly — DuckDB returns geoarrow.wkb\nconst table = await conn.query(`\n  SELECT geometry, name, population\n  FROM my_countries\n`);\n\n// geoarrow-deck-stream detects WKB and decodes it automatically\nconst data = parsePolygonsToSolid(table, {\n  projection: geoEqualEarth().translate([512, 384]).scale(200),\n});\n```\n\n#### DuckDB-WASM < 1.33 (explicit `ST_AsWKB`)\n\nOlder versions of DuckDB-WASM return geometry as an opaque BLOB that is not standard WKB. Use `ST_AsWKB()` to convert it explicitly:\n\n```typescript\n// Older DuckDB-WASM: must call ST_AsWKB() to produce valid WKB\nconst table = await conn.query(`\n  SELECT ST_AsWKB(geometry) as geometry, name, population\n  FROM my_countries\n`);\n\n// Same API — WKB is auto-decoded\nconst data = parsePolygonsToSolid(table, {\n  projection: geoEqualEarth().translate([512, 384]).scale(200),\n});\n```\n\n## Composite Projections\n\nSupport for composite projections (like `geoAlbersUsa` or France with DOM-TOM) where distant territories are displayed in insets.\n\n```typescript\nimport {\n  buildCompositeProjection,\n  PRESET_LAYOUTS,\n  TERRITORY_BOUNDS,\n  createInsetBorderData,\n} from \"@ateliercartographie/geoarrow-deck-stream\";\nimport { geoConicConformal, geoMercator } from \"d3-geo\";\n\n// 1. Build a composite projection\nconst franceProjection = buildCompositeProjection({\n  width: 960,\n  height: 600,\n  entries: [\n    // Main territory\n    {\n      id: \"mainland\",\n      projection: geoConicConformal().parallels([44, 49]).rotate([-3, 0]),\n      bounds: TERRITORY_BOUNDS.FRANCE_MAINLAND,\n      layout: PRESET_LAYOUTS.FRANCE_DOM_TOM.mainland,\n    },\n    // Inset territory (Guadeloupe)\n    {\n      id: \"guadeloupe\",\n      projection: geoMercator(),\n      bounds: TERRITORY_BOUNDS.GUADELOUPE,\n      layout: PRESET_LAYOUTS.FRANCE_DOM_TOM.guadeloupe,\n      scaleMultiplier: 1.5, // Make small islands visible\n    },\n    // ... add other DOM-TOMs using PRESET_LAYOUTS.FRANCE_DOM_TOM\n  ],\n});\n\n// 2. Use in parser\nconst data = parseGeometry(table, { projection: franceProjection });\n\n// 3. (Optional) Render inset borders\nconst borderData = createInsetBorderData(franceProjection);\nconst borderLayer = new PathLayer({\n  data: borderData,\n  getPath: (d) => d.path,\n  getColor: [0, 0, 0, 128],\n});\n```\n\n## Off-Main-Thread Parsing (Web Worker)\n\nThe whole pipeline — Arrow IPC decode, d3 projection stream, binary sink and\nearcut triangulation — can run in a Web Worker so the main thread (and your\nUI) never freezes. On a demanding basemap (France, 34 879 communes, 849k\ncoordinates), the longest main-thread block drops from ~2 s to ~15 ms, with\nbit-identical output.\n\nInputs (Arrow IPC bytes) cross the thread boundary as a single structured\nclone (or a zero-copy transfer with `transferInput: true`); output TypedArrays\nare always **transferred** back, never copied.\n\nBecause d3 projections are closures, they cannot be posted to a worker.\nInstead the worker API takes a **serializable projection spec** — the factory\n*name* plus its parameters, aligned with the `D3Usage` format of\n[`proj-suggest`](https://github.com/AtelierCartographie/proj-suggest):\n\n```typescript\n// Main thread\nimport { createParseWorkerClient } from \"@ateliercartographie/geoarrow-deck-stream/worker\";\n\nconst worker = new Worker(\n  new URL(\"./parse.worker.ts\", import.meta.url),\n  { type: \"module\" },\n);\nconst client = createParseWorkerClient(worker);\n\nconst solid = await client.parsePolygonsToSolid(ipcBytes, {\n  projection: \"geoConicConformal\",\n  rotate: [-3, 0],\n  center: [0, 46.5],\n  parallels: [44, 49],\n  fitExtent: { extent: [[0, 0], [960, 600]], bbox: [-5.5, 41, 10, 51.5] },\n});\n// → feed straight to SolidPolygonLayer via createSolidPolygonLayerProps(solid)\n```\n\n```typescript\n// parse.worker.ts — standard d3-geo projections only? Two lines:\nimport { setupParseWorker } from \"@ateliercartographie/geoarrow-deck-stream/worker\";\nsetupParseWorker();\n```\n\nIf you only need d3-geo projections you can skip the worker file entirely and\npoint the Worker at the shipped entry\n`@ateliercartographie/geoarrow-deck-stream/parse-worker`.\n\n### Exotic projections (d3-geo-projection, d3-geo-polygon, custom)\n\nExtend the worker-side registry — any factory name becomes valid in specs:\n\n```typescript\n// parse.worker.ts\nimport * as d3GeoProjection from \"d3-geo-projection\";\nimport { geoInterrupt } from \"d3-geo-polygon\";\nimport { geoMollweideRaw } from \"d3-geo-projection\";\nimport {\n  setupParseWorker,\n  createProjectionRegistry,\n} from \"@ateliercartographie/geoarrow-deck-stream/worker\";\n\nsetupParseWorker({\n  projections: createProjectionRegistry(d3GeoProjection, {\n    // custom assembly under a custom id\n    mollweideOceans: () => geoInterrupt(geoMollweideRaw, [/* lobes */]).rotate([-200, 0]),\n  }),\n});\n\n// main thread: { projection: \"geoBertin1953\" } or { projection: \"mollweideOceans\" }\n```\n\n### Composite projections in a worker\n\nPass a `type: 'composite'` spec — same shape as `buildCompositeProjection`\nconfig, with each entry's projection described as a spec:\n\n```typescript\nconst spec = {\n  type: \"composite\",\n  width: 960,\n  height: 600,\n  entries: [\n    {\n      id: \"mainland\",\n      projection: { projection: \"geoConicConformal\", parallels: [44, 49], rotate: [-3, 0] },\n      bounds: [-5.5, 41, 10, 51.5],\n      layout: { x: 0.2, y: 0, width: 0.8, height: 1 },\n    },\n    // ... insets\n  ],\n} as const;\n\nconst solid = await client.parsePolygonsToSolid(ipcBytes, spec);\nconst borders = await client.insetBorders(spec); // PathLayer-ready frames\n```\n\nThe client also exposes `parseGeometry`, `parsePoints` and `parseSphere`.\n`resolveProjectionSpec(spec, registry?)` is available on both sides if you\nwant to resolve a spec to a live projection yourself.\n\nSee [examples/worker-usage.html](examples/worker-usage.html) for a runnable\nbefore/after demo.\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                    INPUT (GeoArrow / WKB)                           │\n│       Reads Float64Array coordinates directly (native)              │\n│       or decodes WKB → native GeoArrow first                       │\n└─────────────────────────────────────────────────────────────────────┘\n                                 │\n                                 ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                         DRIVER (Orchestrator)                       │\n│  • Iterates over Arrow geometries                                   │\n│  • Manages featureId tracking                                       │\n└─────────────────────────────────────────────────────────────────────┘\n                                 │\n                                 ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                    REWIND STREAM (Optional)                         │\n│  • buffers rings -> checks spherical containment (d3-geo)           │\n│  • reverses rings if needed (Right-Hand Rule)                       │\n└─────────────────────────────────────────────────────────────────────┘\n                                 │\n                                 ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                    D3 PROJECTION STREAM                             │\n│  ┌───────────────┐     ┌──────────────────┐     ┌──────────────┐   │\n│  │ Rotation      │ ──▶ │ Projection Math  │ ──▶ │ Clipping     │   │\n│  └───────────────┘     └──────────────────┘     └──────────────┘   │\n└─────────────────────────────────────────────────────────────────────┘\n                                 │\n                                 ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                      BINARY SINK (Custom GeoStream)                 │\n│  • Receives point(x, y) calls                                       │\n│  • Writes directly to Float32Array buffers                          │\n│  • For Polygons: Runs Earcut triangulation on the fly               │\n└─────────────────────────────────────────────────────────────────────┘\n```\n\n## \"Zero-Copy\" Philosophy & Data Flow\n\nWhile \"Zero-Copy\" is the guiding principle, strict zero-copy is impossible when transforming Double-Precision data for the GPU. Here is the honest breakdown of the data lifecycle:\n\n### 1. Arrow Input (True Zero-Copy)\n\nWe read directly from the underlying `Float64Array` buffers of the Apache Arrow table.\n\n- **No deserialization**: We do not parse JSON. WKB input is decoded once upfront into native GeoArrow (a necessary pre-processing step), then the rest of the pipeline reads the resulting buffers with zero-copy.\n- **No object allocation**: We never create `{x: 10, y: 20}` objects. We pass raw numbers `(x, y)` through the pipeline.\n- **Batched Access**: Coordinates are read sequentially, optimizing CPU cache usage.\n\n### 2. The D3-Geo Pipeline (Streaming)\n\nThe streaming architecture acts as a \"pipe\", processing one coordinate at a time.\n\n- **Projection**: Mathematical transformations happen on the stack or registers. No intermediate arrays are allocated for total projected points.\n- **Topology (Rewind/Clipping)**: These steps **are not zero-copy**. To fix winding order (Right-Hand Rule) or clip polygons against the antimeridian/globe, the pipeline _must_ temporarily buffer the current geometry.\n  - _Impact_: Allocations are limited to the **single feature** being processed, not the entire dataset. Peak memory usage remains minimal.\n\n### 3. Binary Output (Transcoding)\n\nThe final Sink writes data into `Float32Array` buffers for Deck.gl.\n\n- **Necessary Copy**: GPUs require `Float32`. GeoArrow is usually `Float64`. We must copy and cast values.\n- **Interleaving**: We write `[x,y, x,y]` contiguously, which is the native format for WebGL attributes.\n- **Triangulation**: For `SolidPolygonLayer`, `earcut` generates indices. This is a CPU-intensive step but essential for rendering filled polygons without tessellation artifacts.\n\n**Why use d3-geo instead of a custom loop?**\n\nDespite the overhead of function calls (virtual dispatch), `d3-geo` offers mathematically robust handling of spherical coordinates, antimeridian cutting, and clipping that is notoriously difficult to implement correctly from scratch. The streaming approach ensures that **Peak Memory Usage** remains `O(Output + Single Feature)` rather than `O(Input + Output)`.\n\n## API Reference\n\n### `parseLineStrings(table, options)`\n\n### `parsePolygonsToSolid(table, options)`\n\n### `parsePoints(table, options)`\n\nMain parsing functions. Transform GeoArrow geometries to Deck.gl binary format.\n\n```typescript\nfunction parsePolygonsToSolid(\n  table: Table,\n  options: {\n    projection: GeoProjection; // d3-geo projection\n    capacityMultiplier?: number; // Buffer growth factor (default: 1.5)\n    rewind?: boolean; // Fix spherical winding order (default: true)\n  },\n): BinaryPolygonData;\n```\n\n### `BinaryPolygonData`\n\nOutput format compatible with Deck.gl binary attributes for `SolidPolygonLayer`.\n\n```typescript\ninterface BinaryPolygonData {\n  length: number; // Number of indices (triangles * 3)\n  positions: Float32Array; // [x0,y0, x1,y1, ...] projected coords\n  indices: Uint32Array; // Triangulation indices for WebGL\n  featureIds: Uint32Array; // Maps output vertex → input feature\n  startIndices: Uint32Array; // Range for each polygon (mostly for outlines)\n}\n```\n\n### `createSolidPolygonLayerProps(data)`\n\nCreates props for Deck.gl `SolidPolygonLayer`.\n\n```typescript\nconst props = createSolidPolygonLayerProps(binaryData);\n\nnew SolidPolygonLayer({\n  id: \"my-polygons\",\n  ...props,\n  getFillColor: [255, 0, 0],\n});\n```\n\n## Attribute Accessors & Performance\n\nBecause this parser splits geometries (clipping/wrapping), the number of output vertices often exceeds input rows. You cannot simply pass an Arrow column as a WebGL attribute. We provide two patterns to handle attributes like Color, Width, or Elevation.\n\n### 1. Binary Expansion (Fastest - GPU)\n\nUse `createColorAttribute` (or similar helpers) to generate a pre-expanded TypedArray that matches the output geometry. This offers maximum rendering performance (pure GPU) but costs some CPU time upfront to build the buffer.\n\n```typescript\n// Good for static data\nconst colors = createColorAttribute(binaryData, (featureId) => {\n  // Access Arrow column directly by row index (featureId)\n  const val = populationColumn.get(featureId);\n  return scale(val); // Returns [r, g, b]\n});\n\nnew PathLayer({\n  ...props,\n  getColor: colors, // Passing { value: Uint8Array, size: ... }\n});\n```\n\n### 2. Indexed Accessor (Flexible - CPU/Dynamic)\n\nIf you need dynamic updates (hover, highlighting) without rebuilding buffers, use the `featureIds` mapping inside a standard Deck.gl accessor.\n\n**Note**: Do not use the `d` object. It is undefined in binary mode. Use the `index` argument.\n\n```typescript\n// Good for interactive/dynamic data\nnew PathLayer({\n  ...props,\n  // Warning: 'd' is null/undefined in binary mode!\n  getFillColor: (_, { index }) => {\n    // 1. Get original Row ID\n    const rowId = binaryData.featureIds[index];\n\n    // 2. Lookup in your original data source (Arrow, Array, etc.)\n    const isHovered = rowId === hoveredId;\n    return isHovered ? [255, 255, 0] : [0, 0, 255];\n  },\n  updateTriggers: {\n    getFillColor: [hoveredId], // Only re-evaluate when this changes\n  },\n});\n```\n\n### 3. TextLayer Hybrid Pattern\n\nFor `TextLayer`, we use a \"Proxy Accessor\" pattern. `createTextLayerProps` is currently internal but the pattern is as follows:\n\n1.  Use `parsePoints` to get binary positions.\n2.  Pass a virtual array (`new Array(n).fill(null)`) to `data` to trigger accessors.\n3.  Read positions manually from the binary buffer in `getPosition`.\n\n```typescript\nimport { TextLayer } from \"@deck.gl/layers\";\nconst data = parsePoints(table, { projection });\n\nnew TextLayer({\n  id: \"text-labels\",\n\n  // 1. DATA: Virtual array to trigger JS accessors\n  data: new Array(data.featureIds.length).fill(null),\n\n  // 2. POSITION: Read manually from binary buffer\n  getPosition: (_, { index }) => {\n    const i = index * 2;\n    return [data.positions[i], data.positions[i + 1]];\n  },\n\n  // 3. TEXT: Dynamic lookup via Feature ID\n  getText: (_, { index }) => {\n    const featureId = data.featureIds[index];\n    const nameVector = table.getChild(\"name\");\n    return String(nameVector.get(featureId));\n  },\n\n  getSize: 14,\n  getColor: [255, 255, 255],\n\n  // CRITICAL: Ensure updates when data changes\n  updateTriggers: {\n    getPosition: [data.positions],\n    getText: [data.featureIds],\n  },\n});\n```\n\n## Recipe: Joining External Data (BYOD)\n\nA common scenario: You have static \"Basemaps\" (e.g., French Communes geometry) and dynamic \"User Data\" (e.g., CSV with Population by Commune Code). You want to join them without recreating the geometry.\n\n**The Strategy: \"Logical Join\" via Accessors**\n\n1.  **Parse Geometry once**: Keep the binary result in memory.\n2.  **Extract Keys**: Cache the \"Business ID\" column (e.g., `insee_code`) from the Arrow table.\n3.  **Map User Data**: Index your external data by that Key.\n4.  **Render**: Chain the lookups in the accessor.\n\n```javascript\n// 1. Setup Phase (Run once)\nconst binaryData = parsePolygonsToSolid(arrowTable, { projection });\n\n// Extract IDs to a native array for O(1) access\n// Note: We access the Arrow Vector directly\nconst geoKeys = arrowTable.getChild(\"code_insee\");\n\n// 2. User Data Phase (Run when user uploads CSV/JSON)\n// Index user data: Map<Code, Value>\nconst userDataMap = new Map();\nuserRows.forEach((row) => {\n  userDataMap.set(row.code, row.value);\n});\n\n// 3. Render Phase\nnew SolidPolygonLayer({\n  ...createSolidPolygonLayerProps(binaryData),\n\n  // The Data Chain:\n  // Binary Vertex Index -> Feature ID (Row Index) -> Business Key (INSEE) -> User Value\n  getFillColor: (_, { index }) => {\n    // A. Get Arrow Row Index\n    const rowId = binaryData.featureIds[index];\n\n    // B. Get Business Key (Zero-copy read from Arrow)\n    const key = geoKeys.get(rowId);\n\n    // C. Get User Value\n    const value = userDataMap.get(key);\n\n    return value ? colorScale(value) : [200, 200, 200]; // Fallback color\n  },\n\n  // CRITICAL: Tell Deck.gl to redraw when user data map changes\n  updateTriggers: {\n    getFillColor: [userDataMap],\n  },\n});\n```\n\n## Understanding Feature Splitting\n\nWhen geometries cross projection boundaries (antimeridian, globe edge), they get split:\n\n```\nInput Arrow Data:\n┌─────────────────────────────────────────┐\n│ Row 0: France (LineString)              │\n│ Row 1: USA (LineString)                 │\n│ Row 2: Russia (LineString - crosses 180°)│\n└─────────────────────────────────────────┘\n\nOutput Binary Data:\n┌─────────────────────────────────────────┐\n│ Path 0: France      → featureId: 0      │\n│ Path 1: USA         → featureId: 1      │\n│ Path 2: Russia West → featureId: 2      │  ← Same featureId!\n│ Path 3: Russia East → featureId: 2      │  ← Split at antimeridian\n└─────────────────────────────────────────┘\n```\n\nThis means:\n\n- `binaryData.length` may be > `arrowColumn.length`\n- Use `featureIds` to map back to original attributes\n- All split paths get the same color/style from the source feature\n\n## Performance Tips\n\n1. **Use OrthographicView**: For projected data, not MapView\n2. **Batch updates**: Avoid re-parsing on every frame if data is static\n3. **Pre-allocate buffers**: Set `capacityMultiplier` based on expected clipping\n4. **Disable Rewind for Projected Data**: If using `geoIdentity` (pure pass-through), set `rewind: false` to skip unnecessary spherical calculations.\n\n## Development & Building\n\n### Standard Build (NPM)\n\n```bash\npnpm build\n```\n\nUses `tsc` to compile TypeScript to `dist/`, keeping the file structure. This is best for internal usage or when consuming via a bundler that wants individually importable modules for maximum tree-shaking.\n\n### Production Bundle (CDN/Standalone)\n\n```bash\npnpm build:bundle\n```\n\nUses `rollup` to generate a single ESM file (`dist/geoarrow-deck-stream.min.mjs`) containing the library and core dependencies (`d3-geo`, `earcut`), excluding peer dependencies (`apache-arrow`, `deck.gl`).\n\n**Bundle Stats:**\n\n- Minified: ~68 KB\n- Gzipped: ~22 KB\n\n## License\n\nISC\n","readmeFilename":"README.md"}