# @mapmap/points reference

> Generated from the published **@mapmap/points 0.6.0** type declarations by `website/scripts/sdk-reference.mjs`. Do not edit by hand: change the JSDoc in the package, or the examples in `website/content/sdk-reference/points.examples.md`, and regenerate.

Every public export of the point-cloud SDK, as declared in the package. The guide, with the format, budgets and the streaming walkthrough, is [Point clouds in SDKs & installation](/docs/sdks#point-clouds-mapmappoints).

```bash
npm install @mapmap/points@0.6.0 @mapmap/maps@0.25.1 maplibre-gl three
```

Other SDK references: [@mapmap/maps](/docs/maps-reference), [@mapmap/core](/docs/core-reference), [@mapmap/tile](/docs/tile-reference), [@mapmap/react-native](/docs/react-native-reference), [@mapmap/voice](/docs/voice-reference), [MapMapKit](/docs/ios-reference), [ai.mapmap:core](/docs/android-reference).

101 exports across 2 entry points: 2 classes, 30 functions, 39 interfaces, 20 types, 10 constants.

## How to read this page

Each entry gives the declaration as published, the JSDoc description, and a table of parameters or properties with their types. The **Default** column quotes the default the source states (an `@default` tag or a "Default ..." sentence in the JSDoc). "required" means the property must be given; "not stated" means the source gives no default, so none is claimed here. Classes, factories and the main entry functions each carry a minimal example; examples marked *from the source* are the package's own `@example`.

## Contents

- [`@mapmap/points`](#mapmappoints): 99 exports
- [`@mapmap/points/worker`](#mapmappointsworker): 2 exports

## `@mapmap/points`

```ts
import { /* … */ } from "@mapmap/points";
```

### Classes (@mapmap/points)

[`CopcTransportError`](#copctransporterror), [`UnsupportedCrsError`](#unsupportedcrserror)

#### `CopcTransportError`

*Class*

```ts
class CopcTransportError extends Error { … }
```

Every error this layer throws, so a caller can `catch` one type.

Extends `Error`.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly code` | [`CopcTransportErrorCode`](#copctransporterrorcode) |  |  |
| `readonly status` | `number \| null` |  | HTTP status, when there was one. |
| `readonly url` | `string \| null` |  | The URL being read, when there was one. |

**Constructor and methods**

- `constructor(code: CopcTransportErrorCode, message: string, options?: { status?: number | null; url?: string | null; cause?: unknown; })`

**Example**

```ts
import { CopcTransportError, isAbortError, openCopc } from "@mapmap/points";

try {
  await openCopc("https://example.com/scan.copc.laz");
} catch (err) {
  if (isAbortError(err)) {
    // cancelled, not failed
  } else if (err instanceof CopcTransportError && err.code === "range-unsupported") {
    console.error("The server ignores Range requests:", err.url);
  } else {
    throw err;
  }
}
```

#### `UnsupportedCrsError`

*Class*

```ts
class UnsupportedCrsError extends Error { … }
```

Thrown when a CRS is outside the v1 set and no `crs` hook was supplied.

Extends `Error`.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly crsName` | `string` |  |  |
| `readonly epsg` | `number \| undefined` |  |  |
| `readonly family` | `string \| undefined` |  |  |

**Constructor and methods**

- `constructor(message: string, crsName: string, epsg?: number, family?: string)`

**Example**

```ts
import { UnsupportedCrsError, openCopc } from "@mapmap/points";

try {
  await openCopc("https://example.com/scan.copc.laz");
} catch (err) {
  if (err instanceof UnsupportedCrsError) {
    // Supply a crs hook (proj4 satisfies it) for this CRS.
    console.error(err.crsName, err.epsg);
  } else {
    throw err;
  }
}
```

### Functions (@mapmap/points)

[`absoluteHeightsFromMeta`](#absoluteheightsfrommeta), [`asprsClassLabel`](#asprsclasslabel), [`autoFogRange`](#autofogrange), [`cacheBytesForBudget`](#cachebytesforbudget), [`clampHeightRange`](#clampheightrange), [`classesFromMeta`](#classesfrommeta), [`classTableProblems`](#classtableproblems), [`createPointCloudLayer`](#createpointcloudlayer), [`decodePointCloud`](#decodepointcloud), [`defaultHeightRange`](#defaultheightrange), [`deviceMemoryGb`](#devicememorygb), [`encodePointCloud`](#encodepointcloud), [`eyePosition`](#eyeposition), [`formatClearance`](#formatclearance), [`formatLateral`](#formatlateral), [`isAbortError`](#isaborterror), [`isStreamingSource`](#isstreamingsource), [`localOffset`](#localoffset), [`metresPerPixel`](#metresperpixel), [`openCopc`](#opencopc), [`payloadBytes`](#payloadbytes), [`pointBudgetForDevice`](#pointbudgetfordevice), [`pointSizeScale`](#pointsizescale), [`reportToGeoJson`](#reporttogeojson), [`resolveClasses`](#resolveclasses), [`stickyElevation`](#stickyelevation), [`validateMeta`](#validatemeta), [`visiblePointCount`](#visiblepointcount)

#### `absoluteHeightsFromMeta`

*Function*

```ts
function absoluteHeightsFromMeta(meta: Pick<PointCloudMeta, "absoluteHeights" | "terrainRelative">): boolean;
```

Resolve a sidecar's height datum to the one boolean the layer needs.

`absoluteHeights` is the field; `terrainRelative` is the 0.1.0 spelling of
the same thing, kept as an alias so an existing sidecar renders
identically. They share a polarity, `terrainRelative: true` always
*meant* absolute heights, whatever its docstring claimed, so the alias
needs no inversion, only a rename. See #678.

Absent from both is `true`: that is what 0.1.0 did with an unset field,
and it is the right default anyway, since a cloud georeferenced well
enough to sit on a map is nearly always in absolute metres.

The deprecation warning fires once per process rather than per call: the
layer resolves this inside its render loop, and a warning on every frame
would bury the console it is trying to be seen in.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `meta` | `Pick<PointCloudMeta, "absoluteHeights" \| "terrainRelative">` | required |  |

Options and types: [`PointCloudMeta`](#pointcloudmeta).

#### `asprsClassLabel`

*Function*

```ts
function asprsClassLabel(value: number): string | undefined;
```

A label for a class byte, or `undefined` when the standard does not name
one. Never invents a name for a user-definable byte.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | required |  |

#### `autoFogRange`

*Function*

```ts
function autoFogRange(box: LocalBox): FogRange;
```

Fog distances derived from a cloud's horizontal extent.

`nearM` is 0.6 of the horizontal diagonal, floored at 120 m so a tiny
cloud is not fogged from the doorstep; `farM` is three times that, which
keeps the far end beyond the point where a viewer has pulled back far
enough to see the whole cloud. The vertical extent is deliberately not
in the calculation: fog is about how far away things are on the ground,
and a single tall mast should not push a street scene's fog out by 80 m.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `box` | [`LocalBox`](#localbox) | required |  |

Options and types: [`LocalBox`](#localbox).

#### `cacheBytesForBudget`

*Function*

```ts
function cacheBytesForBudget(pointBudget: number, override?: number): number;
```

Decoded-cache byte budget: 50 % headroom over the point budget, capped.

The headroom is what stops a small camera wobble evicting and immediately
refetching; the cap stops a 32 GB desktop filling a quarter of a gigabyte
for no visible benefit.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `pointBudget` | `number` | required |  |
| `override?` | `number` | not stated |  |

#### `clampHeightRange`

*Function*

```ts
function clampHeightRange(minM: number, maxM: number): MetreRange;
```

Put a caller's height range into a state the shader can divide by.

Throws on a non-finite bound, because there is no sane clamp for `NaN`
and silently substituting one hides the caller's own arithmetic bug.
An inverted or degenerate range is repaired rather than thrown: the
caller is usually a slider, and a slider whose handles meet should not
take the viewer's cloud down with it.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `minM` | `number` | required |  |
| `maxM` | `number` | required |  |

#### `classesFromMeta`

*Function*

```ts
function classesFromMeta(table: MetaClassTable | null | undefined): PointClass[];
```

Build a class table from a sidecar's `classes`, in either shape.

Two shapes are read because two shapes exist in the wild. A map of byte →
label is the compact one; a list of `{ byte, label, colour }` is what most
bakers emit, because a list survives a round trip through a language whose
object keys are not strings. Accepting only the map meant the list decoded
to NaN-valued rows that threw at first render, a long way from the sidecar
that caused it.

- **Map**: ordered by class byte, since a JSON object has no order worth
  trusting. Non-numeric keys are dropped rather than coerced to NaN.
- **List**: kept in the producer's own order, because they chose it and
  it drives the default palette. `colour` and `color` are both read.

Your byte values survive as your byte values in both: `40` stays `40`.
A table longer than [`MAX_CLASSES`](#max_classes) is truncated with a warning
rather than thrown, so a viewer handed a 20-class sidecar still draws
something, [`resolveClasses`](#resolveclasses) would refuse it outright.

Anything that is neither shape throws, naming both.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `table` | [`MetaClassTable \| null \| undefined`](#metaclasstable) | required |  |

Options and types: [`MetaClassTable`](#metaclasstable).

#### `classTableProblems`

*Function*

```ts
function classTableProblems(classes: unknown): string[];
```

Check a `classes` value against both accepted shapes.

Returns the problems rather than throwing, so [`validateMeta`](#validatemeta) can
report them alongside everything else it found. A wrong shape used to
surface as a NaN-keyed class table and a throw at first render, which is
a long way from the sidecar that caused it.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `classes` | `unknown` | required |  |

#### `createPointCloudLayer`

*Function*

```ts
function createPointCloudLayer(source: PointCloudSource, options?: PointCloudLayerOptions): PointCloudLayer;
```

Create the layer. Add it with `map.addLayer(...)` once the style is loaded.

`source` is either a decoded cloud or a `{ meta, raw }` pair, which is
decoded (and validated) here.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | [`PointCloudSource`](#pointcloudsource) | required |  |
| `options?` | [`PointCloudLayerOptions`](#pointcloudlayeroptions) | not stated |  |

Options and types: [`PointCloudSource`](#pointcloudsource), [`PointCloudLayerOptions`](#pointcloudlayeroptions).

**Example**

```ts
import "maplibre-gl/dist/maplibre-gl.css";
import { createMap } from "@mapmap/maps";
import { createPointCloudLayer, openCopc } from "@mapmap/points";

const map = createMap({ container: "map", apiKey: "snk_…", center: [-0.1276, 51.5072], zoom: 16 });
const source = await openCopc("https://example.com/scan.copc.laz");
const layer = createPointCloudLayer(source, { colourMode: "rgb", pointSizeM: 0.15 });
map.map.on("load", () => map.map.addLayer(layer));
```

#### `decodePointCloud`

*Function*

```ts
function decodePointCloud(meta: PointCloudMeta, raw: ArrayBuffer): DecodedPointCloud;
```

Point two typed arrays at a payload, after checking it is the right size.

Throws rather than returning a partial view: a truncated download that
silently renders its first 300 000 points is far harder to diagnose than
one that refuses.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `meta` | [`PointCloudMeta`](#pointcloudmeta) | required |  |
| `raw` | `ArrayBuffer` | required |  |

Options and types: [`PointCloudMeta`](#pointcloudmeta).

#### `defaultHeightRange`

*Function*

```ts
function defaultHeightRange(box: LocalBox): MetreRange;
```

The height range a cloud gets when the caller does not steer it: its own
vertical extent, widened if that extent is flatter than the ramp can use.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `box` | [`LocalBox`](#localbox) | required |  |

Options and types: [`LocalBox`](#localbox).

#### `deviceMemoryGb`

*Function*

```ts
function deviceMemoryGb(): number | undefined;
```

Read `navigator.deviceMemory` without assuming a DOM.

#### `encodePointCloud`

*Function*

```ts
function encodePointCloud(input: {
    anchor: [number, number];
    xyz: Float64Array | Float32Array | number[];
    rgb: Uint8Array | number[];
    classes?: Uint8Array | number[];
    quant?: number;
    sensor?: string;
}): {
    buffer: ArrayBuffer;
    meta: PointCloudMeta;
};
```

Encode points into the wire format, for producers written in TypeScript.

`xyz` is metres in the local frame; `rgb` is 0–255; `classes` is one byte
per point. Returns the buffer plus the `origin`, `quant` and `bboxLocal`
to write into the sidecar, so the two cannot disagree.

Most production baking happens offline in Python or Rust, this exists so
a small cloud can be built in the browser, and so the decoder has an
encoder to be tested against.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `input` | `{ anchor: [number, number]; xyz: Float64Array \| Float32Array \| number[]; rgb: Uint8Array \| number[]; classes?: Uint8Array \| number[]; quant?: number; sensor?: string; }` | required |  |

#### `eyePosition`

*Function*

```ts
function eyePosition(view: CameraView, centre: LocalCentre): EyePosition;
```

Where the eye is, in the cloud's local frame.

The camera sits `distanceM` behind the map centre, opposite the bearing,
lifted by the pitch, and, when terrain is on, raised by the terrain
height at the centre (see 1 in the module docs).

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `view` | [`CameraView`](#cameraview) | required |  |
| `centre` | [`LocalCentre`](#localcentre) | required |  |

Options and types: [`CameraView`](#cameraview), [`LocalCentre`](#localcentre).

#### `formatClearance`

*Function*

```ts
function formatClearance(c: MeasuredClearance): ClearanceText;
```

Format a measured clearance as `"4.62 m measured (safe bound 4.48 m,
surveyed Jan 2026)"`.

The bound is not optional, not appended by a caller and not dropped when
the string is short on space. If a surface has no room for the full
string it needs a shorter design, not a shorter truth.

A non-`structure` overhead class is named in the text, because a figure
limited by vegetation or by a wire behaves differently from one limited
by concrete: foliage is seasonal and compressible, and a wire is a thin
target measured from few returns. Both are still real obstructions, so
the figure stands; the reader is simply told what it is a figure about.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `c` | [`MeasuredClearance`](#measuredclearance) | required |  |

Options and types: [`MeasuredClearance`](#measuredclearance).

**Throws:** if any figure is non-finite, if a bound is negative, or if the
safe bound exceeds the measured value. Each of those means the
arithmetic upstream is wrong, and every one of them would otherwise
render as a confident, plausible label.

#### `formatLateral`

*Function*

```ts
function formatLateral(l: MeasuredLateral): ClearanceText;
```

Format a measured lateral clearance as `"2.90 m clear width measured
(safe bound 2.71 m, surveyed Jan 2026)"`.

A separate function rather than a flag on [`formatClearance`](#formatclearance),
because it takes a separate type. There is no argument anybody can get
wrong: a `MeasuredLateral` has no headroom to caption, and a
`MeasuredClearance` has no clear width.

The wording says "clear width" every time, including for a structure,
where the vertical formatter stays silent about the class. A figure with
no unit noun beside it defaults in a reader's head to headroom, because
that is what a clearance report is mostly about.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `l` | [`MeasuredLateral`](#measuredlateral) | required |  |

Options and types: [`MeasuredLateral`](#measuredlateral).

**Throws:** on the same three arithmetic impossibilities.

#### `isAbortError`

*Function*

```ts
function isAbortError(error: unknown): boolean;
```

True for the DOM `AbortError` and for our own aborted-code error.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `error` | `unknown` | required |  |

#### `isStreamingSource`

*Function*

```ts
function isStreamingSource(source: unknown): source is StreamingSource;
```

Narrow a layer source to a streamed one.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `unknown` | required |  |

#### `localOffset`

*Function*

```ts
function localOffset(anchor: [number, number], lngLat: [number, number]): {
    x: number;
    z: number;
};
```

Local offset in metres (x east, z south) of a lng/lat from an anchor.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `anchor` | `[number, number]` | required |  |
| `lngLat` | `[number, number]` | required |  |

#### `metresPerPixel`

*Function*

```ts
function metresPerPixel(lat: number, zoom: number): number;
```

Ground metres one **CSS** pixel covers at a given latitude and zoom.

512 is MapLibre's tile size in CSS pixels, so this is a CSS-pixel scale
and multiplying it by a device-pixel height is wrong by the device pixel
ratio. `camera.test.ts` pins it against MapLibre's own
`transform.cameraToCenterDistance`, which is likewise in CSS pixels.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `lat` | `number` | required |  |
| `zoom` | `number` | required |  |

#### `openCopc`

*Function*

```ts
function openCopc(source: string | RangeGetter, options?: CopcOpenOptions): Promise<CopcSource>;
```

Read a COPC's header and hierarchy root. Fetches no points.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `string \| RangeGetter` | required | a URL, or a [`RangeGetter`](#rangegetter) to read bytes yourself. |
| `options?` | [`CopcOpenOptions`](#copcopenoptions) | not stated |  |

Options and types: [`RangeGetter`](#rangegetter), [`CopcOpenOptions`](#copcopenoptions).

**Example**

```ts
import { openCopc } from "@mapmap/points";

const source = await openCopc("https://example.com/scan.copc.laz", {
  onWarn: (message) => console.warn(message),
});
console.log(source.meta.totalPoints, source.channels);
source.dispose(); // a source owns a worker pool: always dispose of it
```

#### `payloadBytes`

*Function*

```ts
function payloadBytes(count: number): number;
```

Byte length a payload with `count` points must have.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `count` | `number` | required |  |

#### `pointBudgetForDevice`

*Function*

```ts
function pointBudgetForDevice(deviceMemoryGb?: number, override?: number): number;
```

Resident point budget for a device.

At 10 bytes a point the three tiers are 120 / 60 / 30 MB of GPU buffer,
which leaves ample room beside a MapLibre basemap with terrain on the
weakest device in each tier.

`navigator.deviceMemory` is Chromium-only, so `undefined` takes the
**conservative** tier rather than the optimistic one. An app that knows
better passes `override`, and an explicit override always wins, including
over a device that reports plenty of memory.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `deviceMemoryGb?` | `number` | not stated |  |
| `override?` | `number` | not stated |  |

#### `pointSizeScale`

*Function*

```ts
function pointSizeScale(worldSizeM: number, bufferHeightPx: number, fovDeg?: number): number;
```

Device pixels a point of `worldSizeM` metres subtends at one metre.

The shader divides this by the distance to the point, which is what makes
points shrink with depth like real surfaces instead of staying a fixed
screen size and turning distant geometry into a solid slab.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `worldSizeM` | `number` | required |  |
| `bufferHeightPx` | `number` | required |  |
| `fovDeg?` | `number` | not stated |  |

#### `reportToGeoJson`

*Function*

```ts
function reportToGeoJson(report: RouteClearanceReport): ClearanceGeoJson;
```

Turn a route clearance report into map geometry.

The collection contains, in order:

1. the route ribbon, one LineString decoded from
   `route.geometry_polyline6`, statused by the height verdict. Omitted
   when the report carries no drawable shape;
2. coverage gaps, `not_surveyed` then `insufficient_data`, for the
   height axis and then the width axis;
3. advisory runs;
4. measurement points, height axis then width axis.

The width axis contributes nothing when it reports `not_assessed`: there
is no lateral geometry to draw, and the reason belongs in the panel
beside the report rather than on the map as an unplaced label.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `report` | [`RouteClearanceReport`](#routeclearancereport) | required |  |

Options and types: [`RouteClearanceReport`](#routeclearancereport).

**Throws:** if a coverage or advisory section carries fewer than two
positions, or if the encoded route or section geometry is malformed.

#### `resolveClasses`

*Function*

```ts
function resolveClasses(classes: readonly PointClass[]): ResolvedClasses;
```

Turn a caller's class table into the flat arrays the shader wants.

Throws on a table the shader cannot represent, duplicate class bytes, a
byte outside 0–255, or more classes than [`MAX_CLASSES`](#max_classes). These are
all producer mistakes that would otherwise render as points quietly
taking a neighbour's colour.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `classes` | [`readonly PointClass[]`](#pointclass) | required |  |

Options and types: [`PointClass`](#pointclass).

#### `stickyElevation`

*Function*

```ts
function stickyElevation(sampled: number | null | undefined, remembered: number | null): number | null;
```

Carry the last known terrain elevation across frames the DEM has not
answered for yet.

`map.queryTerrainElevation` returns `null` until the DEM tile covering
the point has loaded, on the first frames after terrain is switched on,
and again after a pan into un-cached tiles. Defaulting those frames to 0
puts the eye at sea level, so on a hill the point sizes and the fog
distance jump the moment the tile arrives: a first-paint pop with no
visible cause. Holding the previous value is wrong by however far the
camera has moved since, which is a metre or two, instead of by the height
of the hill.

Returns `null` only when nothing has ever been sampled, there is no real
elevation to hold yet, and the caller falls back to 0 for those frames
alone. Once a real value has been seen this never returns to `null`, so
the eye never snaps back to sea level mid-session.

Pure, so the layer's memory is one variable it passes in and stores back.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `sampled` | `number \| null \| undefined` | required |  |
| `remembered` | `number \| null` | required |  |

#### `validateMeta`

*Function*

```ts
function validateMeta(meta: PointCloudMeta): string[];
```

Validate a sidecar. Returns the problems; an empty array means usable.

Separate from [`decodePointCloud`](#decodepointcloud) so a build pipeline can check a
sidecar it has just written without holding the (large) binary.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `meta` | [`PointCloudMeta`](#pointcloudmeta) | required |  |

Options and types: [`PointCloudMeta`](#pointcloudmeta).

#### `visiblePointCount`

*Function*

```ts
function visiblePointCount(classes: ResolvedClasses, classCounts: Record<string, number> | undefined): number | null;
```

How many points the currently visible classes account for, or `null` if
that cannot be known from the sidecar.

`null` rather than a guess, and rather than a count of our own: the only
other way to answer is to walk the class byte of every point in the
payload, which is a multi-megabyte scan on the main thread for a number
a producer could have written into `classCounts` at bake time. A readout
that shows nothing is honest; one that stalls a viewer for a second per
toggle, or quietly reports the wrong figure, is not.

A visible class the producer did not count contributes nothing, so a
partial `classCounts` under-reports rather than throwing. Non-finite and
negative counts are ignored for the same reason.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `classes` | [`ResolvedClasses`](#resolvedclasses) | required |  |
| `classCounts` | `Record<string, number> \| undefined` | required |  |

Options and types: [`ResolvedClasses`](#resolvedclasses).

### Interfaces (@mapmap/points)

[`AdaptiveBudget`](#adaptivebudget), [`CameraView`](#cameraview), [`ChangeEvent`](#changeevent), [`ChangeReport`](#changereport), [`ClearanceEnforcement`](#clearanceenforcement), [`ClearanceFieldSidecar`](#clearancefieldsidecar), [`ClearanceGeoJson`](#clearancegeojson), [`CopcCloudMeta`](#copccloudmeta), [`CopcDataStats`](#copcdatastats), [`CopcOpenOptions`](#copcopenoptions), [`CopcSource`](#copcsource), [`DatumBlock`](#datumblock), [`DecodedPointCloud`](#decodedpointcloud), [`EpochRegistration`](#epochregistration), [`EyePosition`](#eyeposition-1), [`FogRange`](#fogrange), [`LateralPoint`](#lateralpoint), [`LimitingPoint`](#limitingpoint), [`LocalBox`](#localbox), [`LocalCentre`](#localcentre), [`MeasuredClearance`](#measuredclearance), [`MeasuredLateral`](#measuredlateral), [`MetaClassEntry`](#metaclassentry), [`MetreRange`](#metrerange), [`ObservedClass`](#observedclass), [`OsmDraftEdit`](#osmdraftedit), [`PointClass`](#pointclass), [`PointCloudLayer`](#pointcloudlayer), [`PointCloudLayerOptions`](#pointcloudlayeroptions), [`PointCloudMeta`](#pointcloudmeta), [`ResolvedClasses`](#resolvedclasses), [`RouteClearanceReport`](#routeclearancereport), [`RouteSection`](#routesection), [`StreamCloudMeta`](#streamcloudmeta), [`StreamingSource`](#streamingsource), [`StreamStats`](#streamstats), [`TagAuditReport`](#tagauditreport), [`TagComparison`](#tagcomparison), [`VehicleQuery`](#vehiclequery)

#### `AdaptiveBudget`

*Interface*

```ts
interface AdaptiveBudget
```

Turn the adaptive-budget option on, and bound it.

See `PointCloudLayerOptions.adaptiveBudget`. Off by default: a library
must not govern a caller's frame budget silently. `true` is a safe,
PROTECTIVE default, the budget may shrink to a floor to hold the frame rate
but never grows above where it started (`allowGrow: false`), so turning it on
can only ever make a struggling machine smoother, never fetch more than the
app already asked for. The object form is the real control: a floor, a hard
ceiling and an optional target frame rate. GROW is enabled only when the
ceiling is above the starting budget, that is the one gate that keeps a phone
(which the demo hands `max === its tier budget`) protective-only while a
desktop (`max` well above its start) may earn more detail.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `min` | `number` | required | Floor. The budget never shrinks below this. |
| `max` | `number` | required | Hard ceiling. The budget never grows above this, whatever the frame rate. |
| `targetFps?` | `number` | `DEFAULT_TARGET_FPS` (~50) | Frame rate to hold. Default `DEFAULT_TARGET_FPS` (~50). |

#### `CameraView`

*Interface*

```ts
interface CameraView
```

The parts of a MapLibre map this module reads.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `centre` | `{ lng: number; lat: number; }` | required | Map centre in degrees. |
| `zoom` | `number` | required |  |
| `bearing` | `number` | required | Degrees, clockwise from north. |
| `pitch` | `number` | required | Degrees from vertical. |
| `fovDeg?` | `number` | [`DEFAULT_FOV_DEG`](#default_fov_deg) | Vertical field of view in degrees. Defaults to [`DEFAULT_FOV_DEG`](#default_fov_deg). |
| `bufferHeightPx` | `number` | required | Drawing-buffer height in DEVICE pixels, not the CSS height. |
| `cssHeightPx?` | `number` | `bufferHeightPx`, which is right on a 1x display and is what every caller before 0.3.0 effectively passed | Canvas height in **CSS** pixels, for the eye distance. Two heights, and they are not interchangeable. [`pointSizeScale`](#pointsizescale) wants DEVICE pixels, because `gl_PointSize` is in device pixels. [`eyePosition`](#eyeposition) wants CSS pixels, because the metres-per-pixel it multiplies by is MapLibre's own 512-CSS-pixel tile scale. Passing the device height to both is the trap, and it is silent: it puts the eye at exactly `devicePixelRatio` times its true distance, which on a 2x display doubles every fog depth and halves every point's screen size. Defaults to `bufferHeightPx`, which is right on a 1x display and is what every caller before 0.3.0 effectively passed. |
| `centreElevationM?` | `number` | not stated | Terrain elevation at the centre in metres, or 0 when terrain is off. |

#### `ChangeEvent`

*Interface*

```ts
interface ChangeEvent
```

One clustered difference between two epochs.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `kind` | [`ChangeKind`](#changekind) | required |  |
| `location` | `{ lng: number; lat: number; }` | required |  |
| `footprint_m2` | `number` | required |  |
| `dz_m` | `number \| null` | required | Height change, metres, or `null` where the change is not a height. |
| `threshold_m` | `number` | required | The threshold actually applied: both cells' sigmas combined with the registration residual. Reported so a reader can tell a real change from one the alignment could not have resolved. |
| `overhead_class` | [`OverheadClass`](#overheadclass) | required |  |
| `view_url` | `string` | required |  |

#### `ChangeReport`

*Interface*

```ts
interface ChangeReport
```

A whole-corridor comparison between two epochs.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `registration` | [`EpochRegistration`](#epochregistration) | required |  |
| `events` | [`ChangeEvent[]`](#changeevent) | required |  |
| `compared_fraction` | `number` | required | Fraction of the corridor both epochs actually covered. |
| `explanation` | `string` | required |  |

#### `ClearanceEnforcement`

*Interface*

```ts
interface ClearanceEnforcement
```

The enforcement block, present on every response including, especially, the clear one, because the clear answer is the one that reads as a
certificate.

It mirrors the tunnel enforcement block the routing API already ships on
every truck route, and for the same reason: `basis` and `caveat` are
constants defined once in the `sn-clearance` crate, so the gateway, the
MCP tools and the docs cannot describe the same guarantee three different
ways. This SDK deliberately does **not** define its own copy of the
caveat text. A second definition is a second thing to drift; the literal
types below pin the fields that must not vary, and the prose arrives
from the one place that owns it.

**Asymmetry with the Rust side, stated so nobody assumes otherwise.**
There, `route_certified` is a type with exactly one inhabitant and
`basis` is a literal tag, so `true` is not writable and deserialising a
report that claims otherwise fails at the boundary. Here the literal
types below give the same guarantee at compile time and **none at all at
runtime**: TypeScript types are erased, so a JSON document parsed into
one of these can carry `route_certified: true` and nothing will notice.
Anything consuming an enforcement block from the wire, rather than from
code that typechecked, has to check it itself.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `basis` | `"surveyed_pointcloud_within_survey_difference"` | required |  |
| `route_certified` | `false` | required | Always `false`, and typed as the literal so it cannot be written otherwise in code. A clearance report is a measurement with a bound, never a certificate about a route. |
| `vertical_datum` | `string` | required | The vertical frame the figures live in, spelled out for a reader. |
| `caveat` | `string` | required | What this answer does and does not prove, in one paragraph. |

#### `ClearanceFieldSidecar`

*Interface*

```ts
interface ClearanceFieldSidecar
```

The `.clr.json` sidecar written beside the binary field.

`sources` exists for staleness: the dataset manifest holds the same
hashes, and a field baked from a source that has since been republished
is served with `stale: true` and can never carry a `pass`.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `schema_version` | `1` | required |  |
| `dataset` | `string` | required | The dataset id, as the point-cloud manifest spells it. |
| `datum` | [`DatumBlock`](#datumblock) | required |  |
| `cell_size_m` | `number` | required |  |
| `grid` | `{ min_e: number; min_n: number; max_e: number; max_n: number; cols: number; rows: number; }` | required |  |
| `survey` | `{ captured_from: string; captured_to: string; sensor: string; trajectory_sigma_source: "per_epoch_sd" \| "declared_constant"; sigma_rel_georef_m: number; passes: "per_point" \| "unavailable"; }` | required |  |
| `classes` | `{ source: "standard_las" \| "custom_dimension" \| "none"; dimension?: string; map_file?: string; }` | required | Where the class prior came from. `custom_dimension` is the San Jose case, where semantics live in a `Type` extra-byte dimension and the standard LAS classification is all zeros. The standard ASPRS table must never be applied to those codes; `map_file` carries the provenance of the decode that was used instead. |
| `sources` | `{ file: string; sha256: string; }[]` | required | Content hashes of the source tiles, for staleness detection. |
| `footprint?` | `Polygon` | not stated | WGS84 survey footprint, where the bake emitted one. |
| `bake` | `{ tool_version: string; parameters: Record<string, number \| string \| boolean>; qa: { cells_total: number; cells_road: number; cells_overhead: number; road_z_mad_m: number; synthetic_checks_passed: boolean; }; }` | required |  |

#### `ClearanceGeoJson`

*Interface*

```ts
interface ClearanceGeoJson
```

What [`reportToGeoJson`](#reporttogeojson) returns: a `FeatureCollection`, narrowed so
a consumer gets the property names checked instead of `any`.

Assignable to GeoJSON's own `FeatureCollection`, which `invariants.ts` asserts
at compile time, so anything expecting the standard type still takes it.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `"FeatureCollection"` | required |  |
| `features` | [`ClearanceFeature[]`](#clearancefeature) | required |  |

#### `CopcCloudMeta`

*Interface*

```ts
interface CopcCloudMeta extends StreamCloudMeta
```

Everything a COPC file tells us about itself, in `PointCloudMeta` shape.

Extends [`StreamCloudMeta`](#streamcloudmeta).

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `streaming` | `true` | required | Always true. How a consumer tells a streamed source from a baked one. |
| `totalPoints` | `number` | required | Points in the file, from the LAS header. `count` mirrors this. |
| `maxLevel` | `number` | required | Deepest level present in the hierarchy **loaded so far**. Live. |
| `spacingM` | `number` | required | Root point spacing, metres. The geometric error at level 0. |
| `pdrf` | `Pdrf` | required | LAS point data record format: 6, 7 or 8. |
| `hasRgb` | `boolean` | required | True when the PDRF carries RGB (7 or 8). |
| `hasNir` | `boolean` | required | True when the PDRF carries NIR (8). |
| `wkt?` | `string` | not stated | The file's CRS, verbatim from the `LASF_Projection` WKT VLR. |
| `projection` | `"geographic" \| "webMercator" \| "transverseMercator" \| "lambertCC" \| "caller"` | required | How the SDK is reprojecting: which family, or `"caller"` for a hook. |

#### `CopcDataStats`

*Interface*

```ts
interface CopcDataStats
```

Counters for `getStats()` and for a proof run's request ledger.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `requests` | `number` | required | Range requests actually issued, node data and hierarchy pages alike. |
| `nodeRequests` | `number` | required |  |
| `pageRequests` | `number` | required |  |
| `bytes` | `number` | required | Compressed bytes pulled over the wire. |
| `wastedBytes` | `number` | required | Bytes inside a coalesced span that nobody asked for. |
| `nodesDecoded` | `number` | required |  |
| `nodesFailed` | `number` | required |  |
| `cacheHits` | `number` | required | Nodes served from the decoded LRU with no request at all. |
| `decodedCacheBytes` | `number` | required |  |
| `compressedCacheBytes` | `number` | required |  |
| `requestsInFlight` | `number` | required | Coalesced requests on the wire right now, and the most there have ever been. The high-water mark is the one number that says whether admission is actually reaching its cap, the failure this pair was added to make visible was a 10-slot budget running at an effective two, which no byte or request total shows. |
| `maxObservedRequestsInFlight` | `number` | required |  |
| `pendingDecodeBytes` | `number` | required | Compressed bytes fetched and waiting for a decode worker. |
| `maxObservedPendingDecodeBytes` | `number` | required | Its high-water mark, against `maxPendingDecodeBytes`. |

#### `CopcOpenOptions`

*Interface*

```ts
interface CopcOpenOptions
```

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `anchor?` | `[number, number]` | the info VLR's cube centre, reprojected | Override the anchor. Default: the info VLR's cube centre, reprojected. |
| `crs?` | `CrsHook` | not stated | Reproject from the file's CRS yourself. Supply this when the CRS is outside the built-in set (geographic, Web Mercator, Transverse Mercator, Lambert Conformal Conic 2SP), or when you need a datum shift the SDK deliberately does not do. `proj4` fits this signature directly: ```ts const from = proj4("EPSG:2056"); openCopc(url, { crs: { toWgs84: (x, y) => from.inverse([x, y]) } }); ``` One consequence worth knowing: a hook is a **function**, so it cannot cross into a Web Worker. Nodes whose affine fit fails its residual gate keep the fitted affine instead of falling back to the exact path, and the SDK warns once naming the residual. This affects the coarsest two or three levels only, and the error shrinks fourfold per level. |
| `heightOffsetM?` | `number` | not stated | Metres added to every Z. For a geoid or datum offset the file omits. |
| `channel?` | [`CopcChannel`](#copcchannel) | `"rgb"` if the PDRF has it, else `"intensity"` | Default: `"rgb"` if the PDRF has it, else `"intensity"`. |
| `workers?` | `number` | `clamp(hardwareConcurrency - 2, 2, 4)` | Worker count. Default: `clamp(hardwareConcurrency - 2, 2, 4)`. |
| `createWorker?` | `(index: number) => PoolWorker` | The default is `new Worker(new URL("./worker.js", import.meta.url), { type: "module" })`, which every bundler this SDK supports understands | Build the decode workers yourself. The default is `new Worker(new URL("./worker.js", import.meta.url), { type: "module" })`, which every bundler this SDK supports understands. Pass your own when yours does not, or to run the pool outside a browser. |
| `lazPerfWasmUrl?` | `string` | Emscripten resolves it next to its own glue by default, which is right from `node_modules` and wrong from a bundle: a build that emits the decode worker as an asset leaves the glue looking for a sibling `.wasm` that was never copied there, and the whole decode path then fails on every node with a module-resolution error | URL of the `laz-perf` `.wasm`. Emscripten resolves it next to its own glue by default, which is right from `node_modules` and wrong from a bundle: a build that emits the decode worker as an asset leaves the glue looking for a sibling `.wasm` that was never copied there, and the whole decode path then fails on every node with a module-resolution error. Serve the file (it is `laz-perf/lib/worker/laz-perf.wasm`, 214 KB, unmodified) and name it here. |
| `cacheBytes?` | `number` | derived from the point budget | Decoded-cache byte budget. Default: derived from the point budget. |
| `compressedCacheBytes?` | `number` | not stated | Compressed-node cache, which makes a channel switch a re-decode. |
| `coalesceGapBytes?` | `number` | 32 KB (§16) | Largest gap two wanted byte ranges may have and still be joined into one request. Default 32 KB (§16). Raise it behind a fast link, lower it where bandwidth costs more than latency. |
| `coalesceMaxBytes?` | `number` | 4 MB (§16), which caps how long a single request can monopolise a connection | Largest span one coalesced request may cover. Default 4 MB (§16), which caps how long a single request can monopolise a connection. A single range wider than this is still issued whole: a node is one complete LAZ chunk and a prefix of it decodes to nothing. |
| `requestInit?` | `RequestInit` | not stated | Passed to every fetch: auth headers, credentials, a cache mode. |
| `requestStallMs?` | `number` | 15 s (`REQUEST_STALL_MS`) | How long one attempt may deliver nothing at all before it is abandoned and retried. Default 15 s (`REQUEST_STALL_MS`). A SILENCE deadline, re-armed on every chunk of body, so a slow download is never killed for being slow. It exists because `fetch` has none of its own: a request black-holed by a cellular handover never rejects, and a promise that never settles holds a scheduler slot for the life of the page. Only ever set this to 0 to disable it in a test that means to hold a request open. |
| `maxConcurrent?` | `number` | 6 | Total concurrent range requests to the file's origin. Default 6. Six is the browsers' per-origin connection cap for HTTP/1.1, and plain S3 is HTTP/1.1: asking for more just queues the surplus inside the browser, where the SDK can neither see it nor reprioritise it (design §8.3). Behind an **HTTP/2** origin, a CDN, or a proxy that terminates h2, that cap is lifted, one connection multiplexes many streams, and raising this is safe and worthwhile: on a dense city COPC a refine to street level is round-trip-bound, and more concurrency fills it faster. This is the one lever a library cannot pick a good default for, because it is a fact about the *caller's transport*, not about the file. So the default stays at the conservative HTTP/1.1 figure and an app that knows its origin speaks h2 opts in. Two of the total stay reserved for hierarchy pages however high this goes (`RESERVED_PAGE_SLOTS`), so the node lane and the COPC data source's own in-flight cap both track `maxConcurrent - 2`. Values below 3 leave the node lane with no room and are rejected. Ignored when you supply your own `getRange`. |
| `getRange?` | [`RangeGetter`](#rangegetter) | not stated | Replace the transport entirely. This is the seam the tests use. |
| `getPageRange?` | [`RangeGetter`](#rangegetter) | `getRange` | Hierarchy pages, if they want their own lane. Defaults to `getRange`. |
| `source?` | `string` | `"COPC <url>"` | What `meta.source` says. Default: `"COPC <url>"`. Put the credit here. |
| `sensor?` | `string` | not stated | Override `meta.sensor`, which otherwise comes from the LAS header. |
| `onWarn?` | `(message: string) => void` | `console.warn`, once per distinct message | Where warnings go. Default: `console.warn`, once per distinct message. |
| `signal?` | `AbortSignal` | not stated | Aborts the two opening reads. |

#### `CopcSource`

*Interface*

```ts
interface CopcSource extends StreamingSource
```

A streamed COPC, as `createPointCloudLayer` consumes it.

Extends [`StreamingSource`](#streamingsource).

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly kind` | `"copc"` | required |  |
| `readonly url` | `string` | required |  |
| `readonly meta` | [`CopcCloudMeta`](#copccloudmeta) | required |  |
| `readonly channels` | [`readonly CopcChannel[]`](#copcchannel) | required | What the file can be coloured by, given its PDRF. |
| `readonly channel` | [`CopcChannel`](#copcchannel) | required | Which of them is in the colour bytes right now. |
| `readonly stats` | [`CopcDataStats`](#copcdatastats) | required | Request and cache counters, for a readout or a proof run. |
| `readonly header` | `LasHeader` | required | The parsed LAS public header, for anything this meta does not carry. |
| `readonly info` | `CopcInfo` | required | The COPC info VLR: cube, spacing, root page. |
| `readonly descriptor` | `ProjectionDescriptor` | required | The §6.2 descriptor, including the CRS's own name. |

**Methods**

- `setChannel(channel: CopcChannel): Promise<void>`: Switch it. Re-decodes; no new range requests with the cache warm.
- `dispose(): void`: Release workers, abort in-flight requests, drop caches.

#### `DatumBlock`

*Interface*

```ts
interface DatumBlock
```

The mandatory datum block. A bake without one must not exist.

`crs.wkt_source` records where the CRS came from, and the ordering is a
quality ranking rather than a preference: a CRS read from the data's own
WKT VLR is a fact, one from a sidecar is a claim, and one declared by an
operator is a hope. It is never taken from a filename. The Montreal
delivery that was MTM and looked like UTM is why.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `crs` | `{ epsg: number; wkt_source: "las_vlr" \| "sidecar" \| "declared"; }` | required | CRS as read from the data (WKT VLR), never from a filename. |
| `vertical_basis` | [`VerticalBasis`](#verticalbasis) | required |  |
| `within_survey_differences` | `true` | required | The load-bearing invariant, restated where a reader will see it: headroom values are WITHIN-SURVEY differences, so the absolute basis above never enters the clearance arithmetic. Typed as the literal `true` because there is no other permissible value: an artefact whose numbers are not within-survey differences is not a clearance field. |

#### `DecodedPointCloud`

*Interface*

```ts
interface DecodedPointCloud
```

Two zero-copy views over one payload, ready to hand to the GPU.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `positions` | `Uint16Array` | required | `count * 3` integers; multiply by `quant` and add `origin` for metres. |
| `colours` | `Uint8Array` | required | `count * 4` bytes: R, G, B, class. |
| `meta` | [`PointCloudMeta`](#pointcloudmeta) | required |  |

#### `EpochRegistration`

*Interface*

```ts
interface EpochRegistration
```

The alignment applied to one epoch before it can be compared to another.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `from_epoch` | `string` | required |  |
| `to_epoch` | `string` | required |  |
| `transform` | `{ bearing_deg: number; t_e: number; t_n: number; t_z: number; }` | required | Four degrees of freedom: a bearing about the vertical plus a translation, applied to the `from` epoch. The bearing is **grid** north. |
| `residual` | `{ median_m: number; p90_m: number; method: "ncc_icp_4dof"; }` | required |  |
| `claim` | `"fit_not_survey"` | required | Always `fit_not_survey`. An alignment is not a measurement. |

#### `EyePosition`

*Interface*

```ts
interface EyePosition
```

Eye position in the cloud's local east/up/south frame, in metres.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `number` | required |  |
| `y` | `number` | required |  |
| `z` | `number` | required |  |
| `distanceM` | `number` | required | Ground distance from eye to map centre, metres. |

#### `FogRange`

*Interface*

```ts
interface FogRange
```

Fog distances in metres from the eye.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `nearM` | `number` | required | Where fog starts. |
| `farM` | `number` | required | Where fog is total. |

#### `LateralPoint`

*Interface*

```ts
interface LateralPoint
```

A place on the route where a lateral measurement constrains the answer.

The measurement is on `lateral`, not `clearance`. Two field names for
two quantities: code that reaches for `.clearance` on one of these does
not compile, which is the point.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `location` | `{ lng: number; lat: number; }` | required |  |
| `route_distance_m` | `number` | required |  |
| `lateral` | [`MeasuredLateral`](#measuredlateral) | required |  |
| `dataset` | `string` | required |  |
| `view_url` | `string` | required |  |

#### `LimitingPoint`

*Interface*

```ts
interface LimitingPoint
```

A place on the route where an overhead measurement constrains the answer.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `location` | `{ lng: number; lat: number; }` | required |  |
| `route_distance_m` | `number` | required |  |
| `clearance` | [`MeasuredClearance`](#measuredclearance) | required |  |
| `dataset` | `string` | required | Which survey the figure came from. |
| `view_url` | `string` | required | Deep link into the viewer, posed at this exact spot. |

#### `LocalBox`

*Interface*

```ts
interface LocalBox
```

Local bounding box in metres, in the cloud's own east/up/south frame.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `[number, number]` | required | East, metres from the anchor. |
| `y` | `[number, number]` | required | Up, metres. |
| `z` | `[number, number]` | required | South, metres from the anchor. |

#### `LocalCentre`

*Interface*

```ts
interface LocalCentre
```

Where the centre of the map sits in the cloud's local frame.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `number` | required | East, metres. |
| `z` | `number` | required | South, metres. |

#### `MeasuredClearance`

*Interface*

```ts
interface MeasuredClearance
```

One measured clearance. The uncertainty is not optional, and that is the
point of the type.

A headroom figure without an error bound is the number somebody drives a
lorry at a bridge on. Making `sigma_m` and `sampling_gap_m` required
means the bound cannot be dropped in transit, and pairing that with a
single formatter means it cannot be dropped in display either.

The two bounds are different animals and are kept apart deliberately:

- `sigma_m` is the combined fit residual of the road estimate, the
  overhead estimate and the differential georeferencing between them. It
  is a spread.
- `sampling_gap_m` is a **one-sided bias bound**, not a sigma. A sample
  spacing of *g* on a structure's underside means the true low point can
  hang up to about *g* below the lowest sample that was actually caught.
  No amount of averaging recovers it, so it is subtracted rather than
  combined in quadrature.

`safe_headroom_m` is the verdict input: `headroom_m` less a multiple of
`sigma_m` less `sampling_gap_m`. Both figures are reported, because a
caller is entitled to see what was measured as well as what it is safe
to conclude.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `headroom_m` | `number` | required | `overhead_z - road_z`, a within-survey difference. |
| `sigma_m` | `number` | required |  |
| `sampling_gap_m` | `number` | required | One-sided bias bound from the sample spacing on the overhead surface. |
| `safe_headroom_m` | `number` | required | `headroom_m - K*sigma_m - sampling_gap_m`; the verdict input. |
| `overhead_class` | [`OverheadClass`](#overheadclass) | required |  |
| `support_m2` | `number` | required | Connected-component area of the overhead cluster, the noise gate. |
| `surveyed_on` | `string` | required | ISO date the constraining geometry was captured. |

#### `MeasuredLateral`

*Interface*

```ts
interface MeasuredLateral
```

One measured lateral clearance. A separate type from
[`MeasuredClearance`](#measuredclearance), with no headroom field anywhere on it.

The obvious alternative was one measurement type with an axis
discriminant, and it is worse: a discriminant is something a reader has
to remember to check, and the whole discipline of this module is that
dangerous states are unrepresentable rather than merely detectable. A
clear width of 2.90 m sitting in a field called `headroom_m` is a
mislabelled safety figure one careless line away from a bridge strike,
so the field does not exist to be misread. The Rust crate makes the same
choice and asserts that a serialised width answer contains the string
"headroom" nowhere; `invariants.ts` asserts the same against the type,
and `format.test.ts` and `geojson.test.ts` against the values, so the
guarantee holds for every width answer rather than for the one a test
happened to build.

The uncertainty is derived laterally rather than borrowed from the
vertical, because it comes from different geometry:

- `sigma_m` is `sqrt(2) * sigma_rel_georef_m`. Both corridor edges are
  measured, and a width is the difference between them, so the
  differential georeferencing term enters twice.
- `sampling_gap_m` is `2 * cell_size`, one cell of ambiguity at each
  edge. It is a one-sided bias bound like its vertical counterpart: the
  true edge can sit up to a cell inboard of the last sample that caught
  it, and no averaging recovers that.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `clear_width_m` | `number` | required | Free width between the surveyed corridor edges, metres. |
| `sigma_m` | `number` | required | `sqrt(2) * sigma_rel_georef_m`: both edges carry the term. |
| `sampling_gap_m` | `number` | required | `2 * cell_size`: one cell of one-sided bias at each edge. |
| `safe_clear_width_m` | `number` | required | `clear_width_m - K*sigma_m - sampling_gap_m`; the verdict input. |
| `limited_by` | [`ObstructionClass`](#obstructionclass) | required | What the narrower edge was: a wall, a hedge, a pole. |
| `support_m2` | `number` | required | Connected-component area of the limiting cluster, the noise gate. |
| `surveyed_on` | `string` | required | ISO date the constraining geometry was captured. |

#### `MetaClassEntry`

*Interface*

```ts
interface MetaClassEntry
```

One class as an array-shaped sidecar writes it.

Bakers that emit a list rather than a map tend to spell the field `byte`
and, being written in Python or against a US style guide, `color`. Both
spellings are accepted; neither is preferred, and `colour` wins if a
producer somehow writes both.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `byte` | `number` | required | The class byte written in the alpha channel. Integer 0–255. |
| `label` | `string` | required | Label for your own UI. |
| `colour?` | `string \| [number, number, number]` | not stated | Optional colour, `#rgb`, `#rrggbb` or `[r, g, b]` in 0–1. |
| `color?` | `string \| [number, number, number]` | not stated | US spelling of `MetaClassEntry.colour`, accepted verbatim. |

#### `MetreRange`

*Interface*

```ts
interface MetreRange
```

A closed range in metres.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `minM` | `number` | required |  |
| `maxM` | `number` | required |  |

#### `ObservedClass`

*Interface*

```ts
interface ObservedClass
```

One class byte and how many resident points carry it.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | required | The classification byte, unmasked, exactly as the producer wrote it. |
| `count` | `number` | required | Points of this class in the **resident** set, right now. |

#### `OsmDraftEdit`

*Interface*

```ts
interface OsmDraftEdit
```

A proposed OSM edit, in draft.

`set` is typed to the single tag this product is ever allowed to draft.
Widening it is not a refactor: `maxheight` is a legal claim and we do not
hold the evidence to make one.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `element` | `"way" \| "node"` | required |  |
| `id` | `number` | required |  |
| `set` | `{ "maxheight:physical": string; }` | required | The only tag this product ever drafts. |
| `provenance` | `{ survey: string; surveyed_on: string; sigma_m: number; }` | required |  |
| `status` | `"draft"` | required | Always `draft`. A human reviews and submits; nothing auto-uploads. |

#### `PointClass`

*Interface*

```ts
interface PointClass
```

One entry in the class table.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | required | The byte written in the alpha channel for points of this class. |
| `label` | `string` | required | Label for your own UI. The SDK renders no text. |
| `colour?` | `string \| [number, number, number]` | the palette entry for this class's position in the table | Colour in class-colour mode, as `#rrggbb` or `[r, g, b]` in 0–1. Defaults to the palette entry for this class's position in the table. |
| `visible?` | `boolean` | true | Whether it starts visible. Defaults to true. |

#### `PointCloudLayer`

*Interface*

```ts
interface PointCloudLayer extends CustomLayerInterface
```

Extends `CustomLayerInterface`.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly meta` | [`PointCloudMeta`](#pointcloudmeta) | required |  |

**Methods**

- `setClassVisible(value: number, visible: boolean): void`: Show or hide one class, by its byte value.
- `setAllClassesVisible(visible: boolean): void`: Show or hide every class at once.
- `setColourMode(mode: ColourMode): void`
- `setPointSize(metres: number, maxPx?: number): void`: World size in metres; `maxPx` optionally lifts the screen-size cap.
- `setHeightRange(minM: number, maxM: number): void`: Steer the `"height"` colour ramp to a band of local metres, live. The bounds are sorted if they arrive the wrong way round, and a collapsed range is widened rather than dividing by zero in the shader. Non-finite bounds throw. Takes effect on the next frame whatever the current colour mode.
- `resetHeightRange(): void`: Put the ramp back to the cloud's own vertical extent.
- `getHeightRange(): MetreRange`: The band the ramp currently spans, for a slider to start from.
- `getFps(): number`: Frames per second, averaged over the last second.
- `getClasses(): ResolvedClasses | null`: The class table as resolved, for building a legend. `visible` is live: it is the array the shader itself reads, so it always reflects the last `PointCloudLayer.setClassVisible` or `PointCloudLayer.setAllClassesVisible` call rather than the state the table declared at construction. A legend can therefore read its checked state off the layer instead of shadowing it. `null` when the layer was created without a `classes` table.
- `getVisiblePointCount(): number | null`: How many points are currently drawn, for a stat readout. `meta.count` when the layer has no class table, since nothing is being culled. Otherwise the sum of `meta.classCounts` over the visible classes, or `null` when the sidecar carries no `classCounts`: the only other way to answer is to walk the class byte of every point in the payload on the main thread, which is too expensive to do per toggle for a number the producer could have baked in.
- `getStats(): StreamStats | null`: Streaming counters, for a loading readout. `null` for a static payload. `residentPoints / totalPoints` is what the loading UI shows. It never reaches 1 and it should not pretend to: the honest label is "2.1M of 940M points drawn", not a progress bar to 100 %.
- `observedClasses(): ObservedClass[]`: Classes actually seen in the resident set, by descending count. Empty for a static payload, whose class table is declared rather than discovered. For a streamed source this is a **sample** and a viewer should say so: it describes the nodes decoded so far, not the file. COPC carries no class histogram and LAS 1.4 defines no statistics VLR, so the alternative is not a better number, it is no number at all.
- `setClasses(classes: readonly PointClass[]): void`: Replace the class table live. Streamed sources only. This is what makes `PointCloudLayer.observedClasses` usable rather than merely interesting: discover which bytes the file actually uses, then name and colour them. Throws above `MAX_CLASSES`, and throws for a static payload, whose shader was compiled for exactly the table it was given. The table is read, never mutated, so a `readonly` array such as the exported `ASPRS_LAS14_CLASSES` is accepted as it stands: `cloud.setClasses(ASPRS_LAS14_CLASSES)` typechecks under strict TypeScript with no cast.
- `setChannel(channel: string): Promise<void>`: Switch the scalar in the colour bytes. Streamed sources only. Re-decodes the resident set, and issues **no** new range requests while the compressed-node cache still holds the bytes. Resolves once the re-decode is done; the map repaints as nodes arrive, so the scene never blanks.
- `setTargetSse(px: number): void`: Change the target screen-space error live, for a quality slider.
- `setPointBudget(points: number): void`: Change the resident point budget live.

#### `PointCloudLayerOptions`

*Interface*

```ts
interface PointCloudLayerOptions
```

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `id?` | `string` | `"mapmap-point-cloud"` | Layer id. Defaults to `"mapmap-point-cloud"`. |
| `classes?` | [`readonly PointClass[]`](#pointclass) | not stated | The classes this cloud declares. Omit to colour by RGB and height only; `setColourMode("class")` then throws rather than inventing a taxonomy. Read, never mutated, and typed `readonly` so a frozen table such as `ASPRS_LAS14_CLASSES` passes without a cast under strict TypeScript. |
| `colourMode?` | [`ColourMode`](#colourmode) | `"rgb"` | Starting colour mode. Defaults to `"rgb"`. |
| `pointSizeM?` | `number` | 0.15 | World size of one point in metres before perspective scaling. Default 0.15. |
| `maxPointPx?` | `number` | 10 | Screen-size cap in device pixels. Default 10. Raise it for offline video bakes: at the default, near-field points separate into distinct marbles that a video encoder reads as noise and spends its whole bitrate on. A higher cap lets them overlap into a surface, which encodes cleanly. |
| `fog?` | `{ colour?: string; nearM?: number; farM?: number; }` | not stated | Distance fog. Set `colour` to your basemap's fog colour so they agree. `nearM` and `farM` are metres from the eye, and each defaults on its own to a distance derived from the cloud's horizontal extent (0.6 of the diagonal, floored at 120 m, with `farM` three times `nearM`), so `{ colour }` alone still gets extent-scaled distances, and anything you pass explicitly wins. Fixed defaults suit an aerial capture and fog a 340 m street to flat grey the moment the camera pulls back, which presents as a broken renderer rather than as fog. |
| `heightRange?` | `{ minM: number; maxM: number; }` | the cloud's own vertical extent | Height range the `"height"` ramp spans, in local metres. Defaults to the cloud's own vertical extent; override it when a handful of below-datum returns or one tall roof would otherwise squeeze the interesting band into a slice of the ramp. Steerable later with `PointCloudLayer.setHeightRange`. |
| `pointBudget?` | `number` | a `navigator.deviceMemory` tier: 12M / 6M / 3M points at `>= 8 GB` / `>= 4 GB` / below or unknown | Resident point budget. Streamed sources only. Defaults to a `navigator.deviceMemory` tier: 12M / 6M / 3M points at `>= 8 GB` / `>= 4 GB` / below or unknown. `deviceMemory` is Chromium-only, so an unknown device takes the conservative tier and an app that knows better passes this. |
| `cacheBytes?` | `number` | not stated | Decoded-cache byte budget. Streamed sources only. |
| `targetSsePx?` | `number` | 2.0 | Target screen-space error in device pixels. Default 2.0. |
| `budgetRamp?` | `boolean` | Off by default | Open a large streamed scene through a budget ramp, for a fast first view. Streamed sources only. With this set, a scene whose resolved budget is large (a desktop tier) opens at a smaller budget so the near field sharpens in a second or two, then grows to the full budget once that near field is resident. The destination is unchanged; only the time to a usable near view improves. The policy, the size gate and the start budget, lives in `resolveBudgetRamp`, so a budget below the gate (every phone and mid tier) ignores the flag and keeps today's single-budget path. Off by default. |
| `adaptiveBudget?` | `AdaptiveBudget \| boolean` | Off by default, a library must not govern a caller's frame budget silently | Adaptive, fps-governed resident budget. Streamed sources only. Off by default, a library must not govern a caller's frame budget silently. With this set, the layer samples the real frame cadence (rAF deltas) and scales the resident budget within the given bounds to hold a target frame rate: a capable machine earns more detail, a weak one settles lower, and both stay smooth. It governs only on steady-state RENDER cost, foreground, camera-still, not-loading, post-settle frames, and moves the budget slowly, in modest multiplicative steps behind a wide deadband, a sustained-signal requirement and a cooldown, so a user never sees points hunting in and out. It converges to a stable budget and then stops. `true` is a PROTECTIVE default: the budget may shrink to hold the frame rate but never grows above where it started. The object form, `{ min, max, targetFps? }`, is the real control; GROW is enabled only when `max` is above the starting budget, which is how a phone (handed `max === its tier budget`) stays protective-only while a desktop may earn more. Composes with `budgetRamp`: the governor stays dormant until the ramp has grown, then takes over the steady-state budget. See [`AdaptiveBudget`](#adaptivebudget). |

#### `PointCloudMeta`

*Interface*

```ts
interface PointCloudMeta
```

The sidecar that describes a cloud. Written alongside the binary, usually
as `<name>.json` next to `<name>.bin`.

Deliberately small: it carries what is needed to *draw* the cloud, plus
two free-form provenance strings (`source`, `sensor`) that the SDK never
renders. Licence terms, survey dates and processing history belong in
your own metadata, not in a rendering payload.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `anchor` | `[number, number]` | required | `[lng, lat]` the local frame is measured from. |
| `quant` | `number` | required | Metres per position unit. 0.02 (2 cm) is the usual choice. |
| `origin` | `[number, number, number]` | required | Local-frame offset in metres added after dequantisation. |
| `count` | `number` | required | Number of points in the binary. |
| `bboxLocal` | [`LocalBox`](#localbox) | required | Local extent, used for the height colour ramp and for framing. |
| `bboxWgs?` | `[number, number, number, number]` | not stated | `[west, south, east, north]` in degrees, for `fitBounds`. |
| `classes?` | [`MetaClassTable`](#metaclasstable) | not stated | The cloud's class table, in either accepted shape: a map of class byte → label, e.g. `{ "0": "ground", "1": "building" }`, or a list of `{ byte, label, colour }` entries. The labels are yours: this format has no opinion about a classification scheme, and does not assume any particular standard's numbering. |
| `classCounts?` | `Record<string, number>` | not stated | Points per class, if the producer counted them. |
| `absoluteHeights?` | `boolean` | `true` | Whether the cloud's `y` values are absolute metres. Defaults to `true`. This flag moves the **camera**, never the points. Nothing in this SDK displaces a cloud: the vertex shader draws each point at `position * quant + origin` and that is the end of it. What the flag decides is the altitude the eye is reconstructed at, see `#678`, which is where the field got its proper name. - `true` (the default): the eye's altitude includes the terrain elevation at the map centre, because the cloud's own heights are absolute and MapLibre's terrain is displaced by absolute metres too. Leave it alone unless you know otherwise. - `false`: the eye sits at altitude 0. Correct only for a cloud on a flat z = 0 datum with terrain off. Either way this affects perspective point sizing and fog distance only, because those are the two things that need a real eye position. It cannot rescue a payload whose heights are wrong, see `PointCloudMeta.terrainRelative`. |
| `terrainRelative?` | `boolean` | not stated |  |
| `source?` | `string` | not stated | Free-form attribution string for the data. Not rendered by the SDK. |
| `sensor?` | `string` | not stated | What captured this cloud, free-form, e.g. `"Ouster OS-1-256-RGB @ 2048x10"`. Structured enough to put in a viewer's info panel without parsing it back out of `PointCloudMeta.source`, free-form enough that the SDK never interprets it: it is carried through encode, decode and validation untouched, and nothing is rendered from it. |

#### `ResolvedClasses`

*Interface*

```ts
interface ResolvedClasses
```

A class table normalised for the shader: dense arrays, resolved colours.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `number[]` | required | Class byte for each table row, in table order. |
| `labels` | `string[]` | required |  |
| `colours` | `number[]` | required | Flat `rgb` triples in 0–1, `values.length * 3` long. |
| `visible` | `number[]` | required | 1 or 0 per row, and the layer's single source of truth for what is drawn: the shader's `uClassOn` uniform is this very array, not a copy of it, so what a layer returns from `getClasses()` is always what it is currently rendering. Steer it through the layer's `setClassVisible`, which also asks the map to repaint; writing into it directly changes the next frame the map happens to draw, but does not schedule one. |
| `lookup` | `Int8Array` | required | Row index for a class byte, or -1. Length 256. |

#### `RouteClearanceReport`

*Interface*

```ts
interface RouteClearanceReport
```

The full report for one route and one vehicle.

`width` is either a verdict of its own or an explicit refusal. Lateral
obstruction distance can only be measured where both corridor edges have
adequate return density, and where they do not the width axis says
`not_assessed` while the height axis still answers. It does not guess.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `basis` | `"surveyed_geometry_not_signage"` | required |  |
| `clearance_enforcement` | [`ClearanceEnforcement`](#clearanceenforcement) | required |  |
| `route` | `{ distance_m: number; geometry_polyline6: string; }` | required |  |
| `vehicle` | [`VehicleQuery`](#vehiclequery) | required |  |
| `height` | [`RouteVerdict`](#routeverdict) | required |  |
| `width` | `WidthVerdict \| { verdict: "not_assessed"; reason: string; }` | required |  |
| `datasets` | `{ dataset: string; survey_dates: string; stale: boolean; }[]` | required |  |
| `advisories` | `{ section: RouteSection; note: string; }[]` | required | Vegetation age, seasonal compressibility and the like. |
| `explanation` | `string` | required | One-line honest summary: what was measured, when, with what bound, and what the answer stops short of concluding. |

#### `RouteSection`

*Interface*

```ts
interface RouteSection
```

A contiguous run of route stations, by distance along the route.

The gateway sends the run's shape as `polyline6`, a six-digit encoded
polyline (latitude first, like `route.geometry_polyline6`), and for one
release also as the older `polyline` array of `[lng, lat]` pairs, which
it has marked deprecated. A report from a gateway older than
`polyline6` carries only the array. `reportToGeoJson` reads
`polyline6` when it is present and non-empty and falls back to
`polyline` otherwise, so both shapes draw the same line.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `start_m` | `number` | required |  |
| `end_m` | `number` | required |  |
| `polyline6?` | `string` | not stated | The run's shape, encoded at precision 6, latitude first. |
| `polyline?` | `[number, number][]` | not stated | The same run as `[lng, lat]` pairs (GeoJSON order). |

#### `StreamCloudMeta`

*Interface*

```ts
interface StreamCloudMeta extends PointCloudMeta
```

Metadata a streamed source synthesises. Task E fills this from the file.

Extends [`PointCloudMeta`](#pointcloudmeta).

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `streaming` | `true` | required | Always true. How a consumer tells a streamed source from a baked one. |
| `totalPoints` | `number` | required | Points in the file, from the LAS header. `count` mirrors it. |
| `spacingM` | `number` | required | Root point spacing in metres: the geometric error at level 0. |
| `maxLevel` | `number` | required | Deepest level present in the hierarchy. |

#### `StreamingSource`

*Interface*

```ts
interface StreamingSource
```

A streamed point cloud, as `createPointCloudLayer` consumes it.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly kind` | `"stream" \| "copc"` | required | `"copc"` for a COPC file over HTTP range requests, which is the only container v1 reads. `"stream"` is the generic spelling, kept because this interface is the *concept*, an octree plus a way to get its nodes, and EPT (the v2 item) will be a second container behind the same one. Anything the layer does here is true of both. |
| `readonly meta` | [`StreamCloudMeta`](#streamcloudmeta) | required |  |
| `readonly octree` | `OctreeView` | required | The node store, adapted into local metres. |
| `readonly data` | `NodeDataSource` | required | The I/O side. |
| `readonly spacingPerQuant` | `number` | required | `65534 * spacing / (2 * halfsize)`. One constant for the whole file, and the reason the shader can derive a per-node point size from `length(modelMatrix[0].xyz)` alone. |
| `readonly channels?` | `readonly string[]` | not stated | Which scalars this container can put in the colour bytes. A *channel* is what is in the three colour bytes and changing it means re-decoding the resident set; a *colour mode* is how the shader paints them and changing it is a uniform write. Keeping the two apart is the split the existing viewer already got right, and it survives here. |

**Methods**

- `setChannel?(channel: string): Promise<void>`: Switch the channel. Absent when the container has only one.

#### `StreamStats`

*Interface*

```ts
interface StreamStats
```

Counters a loading readout can show without lying.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `residentPoints` | `number` | required | Drawn right now. |
| `residentNodes` | `number` | required |  |
| `selectedNodes` | `number` | required | Including nodes selected but not yet resident. |
| `totalPoints` | `number` | required | The file's own count, from the LAS header. |
| `pendingRequests` | `number` | required |  |
| `cacheBytes` | `number` | required |  |
| `cacheBudgetBytes` | `number` | required |  |
| `failedNodes` | `number` | required |  |
| `maxLevelResident` | `number` | required |  |
| `busy` | `boolean` | required | True while anything is outstanding: the repaint condition. |
| `pointBudget` | `number` | required | The resident point budget in force right now. Constant unless the adaptive governor is on, in which case this is the live, fps-scaled figure, so a "pts" readout can report the budget the scene is actually being held to rather than the number it was created with. |

#### `TagAuditReport`

*Interface*

```ts
interface TagAuditReport
```

The audit over a dataset, or over one route corridor.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `basis` | `"surveyed_geometry_not_signage"` | required |  |
| `clearance_enforcement` | [`ClearanceEnforcement`](#clearanceenforcement) | required |  |
| `area` | `{ dataset: string; } \| { route_polyline6: string; }` | required |  |
| `comparisons` | [`TagComparison[]`](#tagcomparison) | required |  |
| `osm_drafts` | [`OsmDraftEdit[]`](#osmdraftedit) | required |  |
| `explanation` | `string` | required |  |

#### `TagComparison`

*Interface*

```ts
interface TagComparison
```

One structure's tagged height against its measured clearance.

`osm.value_raw` is kept verbatim, and `osm.value_m` is `null` when the
value could not be parsed. That pairing is the same doctrine the
dimension parser already applies elsewhere in the stack: a tag reading
`13'6"` normalises cleanly, a tag reading `about 4m` does not, and the
second one is reported as unparseable rather than turned into a number
somebody might act on.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `osm` | `{ element: "way" \| "node"; id: number; tag: "maxheight" \| "maxheight:physical"; value_raw: string; value_m: number \| null; }` | required |  |
| `measured` | [`MeasuredClearance`](#measuredclearance) | required |  |
| `location` | `{ lng: number; lat: number; }` | required |  |
| `finding` | `"consistent" \| "tag_understates" \| "tag_overstates" \| "untagged_structure"` | required | What the comparison found, about the tag: - `consistent`, within the combined tolerance. - `tag_understates`, tagged below the measured physical clearance, so the map is over-restrictive and the router is turning away vehicles that would fit. - `tag_overstates`, tagged above the measured physical clearance. The dangerous direction. For a legal `maxheight` this is a signage and authority matter, not an edit. - `untagged_structure`, the survey found a constraining structure the map says nothing about. |
| `tolerance_m` | `number` | required | The tolerance actually applied, metres. |
| `view_url` | `string` | required | Deep link into the viewer at this structure. |

#### `VehicleQuery`

*Interface*

```ts
interface VehicleQuery
```

The vehicle the question was asked about.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `height_m` | `number` | required |  |
| `width_m?` | `number` | not stated | The width axis is assessed only when a width was given. |
| `margin_m` | `number` | 0 and is echoed back: we surface both the measured figure and the safe bound rather than baking anybody's compliance policy into the arithmetic | The caller's operating margin, added to the height before comparison. Defaults to 0 and is echoed back: we surface both the measured figure and the safe bound rather than baking anybody's compliance policy into the arithmetic. |

### Types (@mapmap/points)

[`ChangeKind`](#changekind), [`ClearanceFeature`](#clearancefeature), [`ClearanceLineProperties`](#clearancelineproperties), [`ClearancePointProperties`](#clearancepointproperties), [`ClearanceStatus`](#clearancestatus), [`ClearanceText`](#clearancetext), [`ColourMode`](#colourmode), [`CopcChannel`](#copcchannel), [`CopcTransportErrorCode`](#copctransporterrorcode), [`Coverage`](#coverage), [`IndeterminateReason`](#indeterminatereason), [`LateralPointProperties`](#lateralpointproperties), [`MetaClassTable`](#metaclasstable), [`ObstructionClass`](#obstructionclass), [`OverheadClass`](#overheadclass), [`PointCloudSource`](#pointcloudsource), [`RangeGetter`](#rangegetter), [`RouteVerdict`](#routeverdict), [`VerticalBasis`](#verticalbasis), [`WidthVerdict`](#widthverdict)

#### `ChangeKind`

*Type*

```ts
type ChangeKind = "appeared" | "vanished" | "moved" | "grew" | "shrank";
```

What happened to a cluster of cells between two epochs.

#### `ClearanceFeature`

*Type*

```ts
type ClearanceFeature = {
    type: "Feature";
    geometry: LineString;
    properties: ClearanceLineProperties;
} | {
    type: "Feature";
    geometry: Point;
    properties: ClearancePointProperties;
} | {
    type: "Feature";
    geometry: Point;
    properties: LateralPointProperties;
};
```

One feature of a clearance collection.

#### `ClearanceLineProperties`

*Type*

```ts
type ClearanceLineProperties = {
    kind: "route" | "coverage_gap" | "advisory";
    axis: "height" | "width" | null;
    status: ClearanceStatus;
    start_m: number;
    end_m: number;
    note: string | null;
};
```

Properties on the line features: the route ribbon, coverage gaps and
advisory runs.

Written as a type alias rather than an interface so it keeps its
implicit index signature and stays assignable to GeoJSON's
`GeoJsonProperties`. An interface here compiles until the moment someone
passes the collection to a typed GeoJSON API, and then does not.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `kind` | `"route" \| "coverage_gap" \| "advisory"` | required |  |
| `axis` | `"height" \| "width" \| null` | required | The axis this run reports on; `null` on the whole-route ribbon. |
| `status` | [`ClearanceStatus`](#clearancestatus) | required |  |
| `start_m` | `number` | required |  |
| `end_m` | `number` | required |  |
| `note` | `string \| null` | required | Advisory text, verbatim from the report. `null` on other kinds. |

#### `ClearancePointProperties`

*Type*

```ts
type ClearancePointProperties = MeasurementPointCommon & {
    axis: "height";
    headroom_m: number;
    safe_headroom_m: number;
    overhead_class: ObstructionClass;
};
```

Properties on an overhead measurement point.

`axis` is still here, and still useful: a style expression filters on it
to give the two axes different symbols. What it is no longer doing is
telling a reader which of two possible meanings `headroom_m` has, because
the width features do not have a `headroom_m` to disambiguate.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `axis` | `"height"` | required |  |
| `headroom_m` | `number` | required |  |
| `safe_headroom_m` | `number` | required |  |
| `overhead_class` | [`ObstructionClass`](#obstructionclass) | required |  |

#### `ClearanceStatus`

*Type*

```ts
type ClearanceStatus = "pass" | "fail" | "indeterminate" | "no_verdict" | "not_surveyed" | "insufficient_data" | "advisory";
```

What a style expression keys colour off.

`not_surveyed` and `insufficient_data` are separate from every verdict
on purpose. They are not weaker verdicts; they are the absence of one,
and a palette that renders them as a paler version of `pass` has
reintroduced by styling exactly the confusion the report types spend
their whole existence preventing.

#### `ClearanceText`

*Type*

```ts
type ClearanceText = string & {
    readonly __clearanceText: "a measured value and its bound, together";
};
```

Display text for a clearance measurement, carrying its bound.

A branded string: it *is* a `string` and can be assigned to one, passed
to a DOM API or put in a GeoJSON property, but a plain string cannot be
assigned back to it. That asymmetry is the whole point. Produced only by
[`formatClearance`](#formatclearance) and [`formatLateral`](#formatlateral).

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `readonly __clearanceText` | `"a measured value and its bound, together"` | required |  |

#### `ColourMode`

*Type*

```ts
type ColourMode = "rgb" | "class" | "height";
```

How points are coloured.

#### `CopcChannel`

*Type*

```ts
type CopcChannel = "rgb" | "intensity";
```

Which scalar lands in the three colour bytes.

#### `CopcTransportErrorCode`

*Type*

```ts
type CopcTransportErrorCode = "range-unsupported" | "network" | "stalled" | "http-status" | "retries-exhausted" | "aborted" | "laz-perf-missing" | "invalid-request";
```

Machine-readable discriminator on [`CopcTransportError`](#copctransporterror).

#### `Coverage`

*Type*

```ts
type Coverage = {
    kind: "complete";
    assessed_m: number;
} | {
    kind: "partial";
    assessed_m: number;
    not_surveyed: RouteSection[];
    insufficient_data: RouteSection[];
};
```

How much of the route was actually assessed.

The two variants are distinct shapes on purpose. `complete` has no field
for gaps, so a consumer holding a `complete` coverage cannot ask about
unsurveyed ground and get a plausible-looking empty array back; it has
to narrow first, and narrowing is where it learns that gaps exist.

Open sky is not a gap. A station over a cell with adequate road density
and no validated overhead cluster is a *measurement* of no obstruction,
and it counts toward `assessed_m`. A station outside every field, or
over cells flagged `NO_ROAD`, is the absence of a measurement and lands
in `not_surveyed`.

#### `IndeterminateReason`

*Type*

```ts
type IndeterminateReason = "inside_uncertainty_band" | "vegetation_age_exceeded" | "artefact_stale" | "artefact_freshness_unchecked";
```

Why an answer stopped short of a verdict, as a machine-readable set.

It travels **beside** the human sentence, not instead of it. A caller
that wants to branch on the cause should not be parsing prose, and a
person reading the answer should not be handed an enum variant. The list
is sorted and de-duplicated by the producer, so two identical answers
serialise identically.

- `inside_uncertainty_band`, the margin landed between the measured
  value and the safe bound. The measurement is fine; the question is
  too close to call.
- `vegetation_age_exceeded`, the limiting return is vegetation on a
  survey old enough that foliage has had time to move.
- `artefact_stale`, the clearance field is behind the sources it was
  baked from. A pass is refused rather than served with a warning.
- `artefact_freshness_unchecked`, staleness could not be established
  either way, which is not the same as fresh.

#### `LateralPointProperties`

*Type*

```ts
type LateralPointProperties = MeasurementPointCommon & {
    axis: "width";
    clear_width_m: number;
    safe_clear_width_m: number;
    limited_by: ObstructionClass;
};
```

Properties on a lateral measurement point.

No headroom field, matching `MeasuredLateral`. The Rust crate asserts
that a serialised width answer contains the string "headroom" nowhere;
`geojson.test.ts` asserts the same of this collection, so the guarantee
survives the trip through the SDK and onto the map.

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `axis` | `"width"` | required |  |
| `clear_width_m` | `number` | required |  |
| `safe_clear_width_m` | `number` | required |  |
| `limited_by` | [`ObstructionClass`](#obstructionclass) | required |  |

#### `MetaClassTable`

*Type*

```ts
type MetaClassTable = Record<string, string> | MetaClassEntry[];
```

The two class-table shapes a sidecar may use.

A map keyed by the class byte is the compact form; a list of entries is
what most bakers produce, because it survives a round trip through a
language whose object keys are not strings. Both mean the same thing, so
both are read, see [`classesFromMeta`](#classesfrommeta).

#### `ObstructionClass`

*Type*

```ts
type ObstructionClass = "structure" | "vegetation" | "wire" | "unknown";
```

What the survey found in the way, as a class prior rather than a
certainty.

`vegetation` and `wire` are labelled, never dropped: a branch across a
lane is a real obstruction, and a catenary is a real obstruction to a
high load. What the label buys is honesty about what the figure will do
next, vegetation is compressible and seasonal, and a wire is a thin
target measured from few returns.

This taxonomy IS shared between the two axes, deliberately, and that is
not a contradiction of the rule that keeps their measurements apart. A
headroom metre and a clear-width metre are different physical
quantities; "vegetation" is the same material whether it is above the
vehicle or beside it.

#### `OverheadClass`

*Type*

```ts
type OverheadClass = ObstructionClass;
```

The obstruction class at its vertical use site, where "overhead" reads
correctly. An alias, not a copy: one definition, two names, so the two
axes cannot drift apart on what `vegetation` means.

#### `PointCloudSource`

*Type*

```ts
type PointCloudSource = DecodedPointCloud | {
    meta: PointCloudMeta;
    raw: ArrayBuffer;
} | StreamingSource;
```

Where a layer's points come from.

The first two are the existing signature, so adding the third is a
widening and not a break.

#### `RangeGetter`

*Type*

```ts
type RangeGetter = (begin: number, end: number, signal?: AbortSignal) => Promise<Uint8Array>;
```

The byte-range reader every COPC read goes through. The whole I/O seam.

`end` is exclusive, matching `Uint8Array.prototype.subarray` and the
public API in the streaming design §11.2. HTTP's `Range` header is
inclusive at both ends, so the fetch implementation subtracts one; that
conversion happens in exactly one place (`:transport/http`).

#### `RouteVerdict`

*Type*

```ts
type RouteVerdict = {
    verdict: "pass";
    coverage: Extract<Coverage, {
        kind: "complete";
    }>;
    tightest?: LimitingPoint;
} | {
    verdict: "fail";
    coverage: Coverage;
    limiting: LimitingPoint;
    all_failures: LimitingPoint[];
} | {
    verdict: "indeterminate";
    coverage: Coverage;
    contested: LimitingPoint[];
    reasons: IndeterminateReason[];
    resolution_hint: string;
} | {
    verdict: "no_verdict";
    coverage: Extract<Coverage, {
        kind: "partial";
    }>;
    reason: "no_survey_coverage" | "insufficient_data";
};
```

The verdict on the vehicle's height.

A passing route CANNOT carry coverage gaps: `pass` narrows `Coverage` to
its `complete` variant, and `no_verdict` narrows it to `partial`. Those
two `Extract`s are the invariant. Everything else about the honesty of a
clearance answer is prose that somebody can ignore; this is arithmetic
the compiler does.

`tightest` is **optional**, and that is not a loosening. A fully
surveyed stretch of open road has no tightest point, because nothing
overhead was ever measured on it: `overhead_z` is NaN across the whole
corridor, which is a measurement of clear sky rather than a very large
headroom. The alternatives were both worse than an absent field, fabricate a number for a safety report, or refuse to pass the safest
possible route. Where a constraint does exist it is always named,
because "yes" is not an answer a dispatcher can act on and "yes, with
0.11 m to spare under the railway bridge at 4.2 km" is.

#### `VerticalBasis`

*Type*

```ts
type VerticalBasis = {
    kind: "ellipsoidal";
    ellipsoid: "WGS84";
    undulation_applied_m: number | null;
} | {
    kind: "orthometric";
    geoid: string;
} | {
    kind: "dem_referenced";
    dem: "terrarium_z14";
};
```

How a survey's heights relate to a physical vertical datum.

Recorded so a reader can say what the absolute numbers mean, never so the
clearance arithmetic can use them. `dem_referenced` is listed because
reheighted payloads exist and their basis has to be stateable; it is not
a clearance source, and a bake reading one is a bug rather than a
degraded mode.

#### `WidthVerdict`

*Type*

```ts
type WidthVerdict = {
    verdict: "pass";
    coverage: Extract<Coverage, {
        kind: "complete";
    }>;
    tightest?: LateralPoint;
} | {
    verdict: "fail";
    coverage: Coverage;
    limiting: LateralPoint;
    all_failures: LateralPoint[];
} | {
    verdict: "indeterminate";
    coverage: Coverage;
    contested: LateralPoint[];
    reasons: IndeterminateReason[];
    resolution_hint: string;
} | {
    verdict: "no_verdict";
    coverage: Extract<Coverage, {
        kind: "partial";
    }>;
    reason: "no_survey_coverage" | "insufficient_data";
};
```

The verdict on the vehicle's width. Structurally the shape of
[`RouteVerdict`](#routeverdict), deliberately not the same type.

It carries [`LateralPoint`](#lateralpoint)s, which carry [`MeasuredLateral`](#measuredlateral)s,
which have no headroom field. Sharing one verdict type between the axes
would have put a clear width inside a `MeasuredClearance` and undone the
separation at the only level that matters.

The same coverage invariant applies, for the same reason: a width axis
that passes over ground with no surveyed corridor edges is exactly as
dangerous as a height axis that does.

### Constants (@mapmap/points)

[`ASPRS_LAS14_CLASS_LABELS`](#asprs_las14_class_labels), [`ASPRS_LAS14_CLASSES`](#asprs_las14_classes), [`BYTES_PER_POINT`](#bytes_per_point), [`DEFAULT_FOV_DEG`](#default_fov_deg), [`DEFAULT_PALETTE`](#default_palette), [`FOG_FAR_MULTIPLE`](#fog_far_multiple), [`FOG_NEAR_DIAGONAL_FRACTION`](#fog_near_diagonal_fraction), [`MAX_CLASSES`](#max_classes), [`MIN_FOG_NEAR_M`](#min_fog_near_m), [`MIN_HEIGHT_SPAN_M`](#min_height_span_m)

#### `ASPRS_LAS14_CLASS_LABELS`

*Constant*

```ts
const ASPRS_LAS14_CLASS_LABELS: Readonly<Record<number, string>>;
```

Every ASPRS name for PDRF 6–10, by class byte (LAS 1.4 R15 Table 17).

For labelling a byte [`ASPRS_LAS14_CLASSES`](#asprs_las14_classes) leaves out, which is
what `observedClasses()` will hand you the moment a file uses one. Bytes
23–63 are reserved and 64–255 are user-definable, so anything absent from
this map is the producer's to name, not ours.

#### `ASPRS_LAS14_CLASSES`

*Constant*

```ts
const ASPRS_LAS14_CLASSES: readonly PointClass[];
```

The curated 14. Colours are chosen to read as a landscape rather than as a
palette: ground is earth, vegetation greens by height, buildings are the
one saturated colour, noise is a warning red, wires are pale.

#### `BYTES_PER_POINT`

*Constant*

```ts
const BYTES_PER_POINT = 10;
```

Bytes one point occupies: 3 * Uint16 + 4 * Uint8.

#### `DEFAULT_FOV_DEG`

*Constant*

```ts
const DEFAULT_FOV_DEG = 36.87;
```

MapLibre's default vertical field of view, in degrees.

#### `DEFAULT_PALETTE`

*Constant*

```ts
const DEFAULT_PALETTE: readonly string[];
```

Distinguishable, colour-blind-aware default palette.

Ordered so that the first few stay separable for viewers with the common
forms of colour-vision deficiency: the early entries differ in lightness
as well as hue, which is what survives a deuteranopic simulation.

#### `FOG_FAR_MULTIPLE`

*Constant*

```ts
const FOG_FAR_MULTIPLE = 3;
```

Fog is total at this multiple of where it starts.

#### `FOG_NEAR_DIAGONAL_FRACTION`

*Constant*

```ts
const FOG_NEAR_DIAGONAL_FRACTION = 0.6;
```

Fraction of the horizontal diagonal at which fog starts.

#### `MAX_CLASSES`

*Constant*

```ts
const MAX_CLASSES = 16;
```

Most classes any one cloud may declare.

The cap is a uniform-budget limit, not an arbitrary round number. The
shader carries a `vec3` colour, a visibility float and the class byte per
class in uniform arrays, and WebGL1 only guarantees 128 vertex uniform
vectors, array elements are not reliably packed, so each one can cost a
full slot. Sixteen classes is 48 slots, which leaves comfortable room for
everything else the vertex shader needs on the weakest conforming device.

It is also more than a legend can usefully hold: past roughly a dozen
flat colours a viewer cannot tell them apart anyway.

#### `MIN_FOG_NEAR_M`

*Constant*

```ts
const MIN_FOG_NEAR_M = 120;
```

Fog never starts closer than this, however small the cloud.

#### `MIN_HEIGHT_SPAN_M`

*Constant*

```ts
const MIN_HEIGHT_SPAN_M = 8;
```

Shortest height range the ramp is allowed to span.

A zero-height range divides by zero in the ramp and paints every point
the same colour, so a flat cloud (or a caller asking for min === max) is
widened to this instead. Eight metres is roughly two storeys: enough that
the ramp still says something about a nominally flat scene.

## `@mapmap/points/worker`

```ts
import { /* … */ } from "@mapmap/points/worker";
```

### Functions (@mapmap/points/worker)

[`onDecodeMessage`](#ondecodemessage), [`setLazPerfImporter`](#setlazperfimporter)

#### `onDecodeMessage`

*Function*

```ts
function onDecodeMessage(scope: WorkerScope, data: unknown): Promise<void>;
```

Handle one message. Exported so a test can drive it without a `Worker`.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `scope` | `WorkerScope` | required |  |
| `data` | `unknown` | required |  |

#### `setLazPerfImporter`

*Function*

```ts
function setLazPerfImporter(fn: (() => Promise<unknown>) | null): void;
```

Hand the loader your own `import()` of `laz-perf`.

`LoadLazPerfOptions.importModule` says the same thing, and cannot be used
where it is most needed: a worker is constructed from a URL and given no
options, so there is no way to pass a **function** into one. This setter is
that channel, called from inside the worker's own module scope, which is
why `worker.ts` re-exports it.

The case it exists for is real and is not hypothetical. A bundler that
processes this package's pre-built `dist/worker.js` may fail to map the
bare specifier in its dynamic import, and the failure mode is the worst
kind: the import neither resolves nor rejects, so every decode hangs, no
error is ever logged, and the scene stays empty while the network, the
octree and the georeferencing all work perfectly. An app-owned worker
module can resolve `laz-perf` in its own graph, where its bundler knows
what `node_modules` is, and hand the result over here.

```ts
// the app's own worker entry
import { setLazPerfImporter } from "@mapmap/points/worker";
setLazPerfImporter(() => import("laz-perf/lib/worker/index.js"));
```

Must be called before the first decode message is handled. Module scope in
the worker entry is exactly that moment.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `fn` | `(() => Promise<unknown>) \| null` | required |  |
