Theme
Design your own
On this page (9)

§A position's cell

s2Cell([lon, lat], level) returns { id, token, level }: the id as a BigInt (64 bits do not fit a JavaScript number), the token as the canonical hex string with trailing zeros dropped. A string argument is always a token - pass a decimal id as BigInt(decimalString). On the map, cells are a layer: their tokens in cells, drawn with great-circle edges and pickable like regions (s2:<token>).

§The cell of a GPS position, its neighbors and its parent

Move the level: the cell holding the position shrinks four times per level; the four edge neighbors are drawn around it, the parent's center is the square marker. Click a cell to pick it.

§The hierarchy

Every cell has exactly four children and one parent; a cell's id contains its parent's as a prefix, which is why s2Parent and s2Children are bit operations. The six faces are level 0 - here they are, colored, with their children at level 1.

§The cube faces and their children

§Drill down the hierarchy

A cell can be the map's focus, like a region: data-focus="s2:<token>" - or a double click on any drawn cell - frames the cell and draws its four children in place of the layer's cells; the countries stay as they are. Double click a child to go one level deeper; up, Escape or df$.shadcn.geoMap.up(el) step back to the parent, and from a face to the cells the page set. Each step is a geo-map-focus event, and the level readout names the level drawn. The camera stops at 1024x: on this map that frames a cell down to level 8, and from level 9 on each step halves the children - about 20 px across at level 12.

§From a cube face down through the levels

Double click a cell to drill into it, or press Into Munich's cell to go down one level toward Munich's GPS position. Up steps out; World goes back to the six faces.

§S2 and Morton are different curves

S2 orders cells along a Hilbert curve on the cube's faces - neighbors along the curve are always neighbors on the ground. The routing page's Morton index orders a plain longitude / latitude grid along a Z curve instead: simpler, and enough to cut a rectangle query down to a few key ranges. Neither is a road network, and S2 cells are spherical, not ellipsoidal: they are equal in neither area nor shape, only within a factor of about two.

§CSS view file

The library lives in the Geo Map component - the stylesheet is the map's.

@layer components {
  .geo-map {
    /* the map's own colors: derived from the theme, literal where the theme has no token (water, routes, cells) */
    --geo-map-ocean: color-mix(in oklab, var(--background) 90%, #6c9bab);
    --geo-map-grid: color-mix(in oklab, var(--foreground) 13%, transparent);
    --geo-map-border: color-mix(in oklab, var(--foreground) 38%, transparent);
    --geo-map-route: #c14e2e;
    --geo-map-cell-fill: color-mix(in oklab, #4e958b 32%, transparent);
    --geo-map-cell-border: #347871;
    /* a pick: a coral that reads on every palette (the theme's primary is near-black in the default theme) */
    --geo-map-select: oklch(0.6 0.19 25);
    --geo-map-mix: 100%;
    /* a tone's drawn lightness = l0 + l * lk, its chroma c * ck: identity in light mode; dark mode maps the pastels
       into the dark range instead of mixing them toward the background (which greyed them) */
    --geo-map-l0: 0;
    --geo-map-lk: 1;
    --geo-map-ck: 1;
    --geo-map-callout-width: 10rem;
    --geo-map-callout-size: 0.875rem;
    --geo-map-tone-0: oklch(0.91 0.055 150);
    --geo-map-tone-1: oklch(0.93 0.06 95);
    --geo-map-tone-2: oklch(0.89 0.06 60);
    --geo-map-tone-3: oklch(0.88 0.05 18);
    --geo-map-tone-4: oklch(0.88 0.045 318);
    --geo-map-tone-5: oklch(0.88 0.045 260);
    --geo-map-tone-6: oklch(0.9 0.05 205);
    --geo-map-tone: var(--geo-map-tone-0);
    position: relative;
    isolation: isolate;
    overflow: hidden;
    block-size: var(--geo-map-height, 28rem);
    min-block-size: 16rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--card);
    color: var(--card-foreground);
    font-family: var(--font-sans);
    &[data-palette="ocean"] {
      --geo-map-tone-0: oklch(0.93 0.035 215);
      --geo-map-tone-1: oklch(0.89 0.05 225);
      --geo-map-tone-2: oklch(0.85 0.06 235);
      --geo-map-tone-3: oklch(0.81 0.07 245);
      --geo-map-tone-4: oklch(0.87 0.05 200);
      --geo-map-tone-5: oklch(0.83 0.06 255);
      --geo-map-tone-6: oklch(0.91 0.04 240);
    }
    &[data-palette="forest"] {
      --geo-map-tone-0: oklch(0.93 0.045 135);
      --geo-map-tone-1: oklch(0.89 0.07 135);
      --geo-map-tone-2: oklch(0.85 0.075 145);
      --geo-map-tone-3: oklch(0.81 0.075 158);
      --geo-map-tone-4: oklch(0.87 0.06 170);
      --geo-map-tone-5: oklch(0.83 0.065 140);
      --geo-map-tone-6: oklch(0.91 0.05 125);
    }
    &[data-palette="sunset"] {
      --geo-map-tone-0: oklch(0.92 0.06 82);
      --geo-map-tone-1: oklch(0.88 0.075 65);
      --geo-map-tone-2: oklch(0.84 0.085 50);
      --geo-map-tone-3: oklch(0.8 0.085 32);
      --geo-map-tone-4: oklch(0.86 0.065 15);
      --geo-map-tone-5: oklch(0.82 0.07 350);
      --geo-map-tone-6: oklch(0.9 0.06 40);
    }
    &[data-palette="heat"] {
      --geo-map-tone-0: oklch(0.96 0.055 95);
      --geo-map-tone-1: oklch(0.92 0.08 85);
      --geo-map-tone-2: oklch(0.88 0.09 72);
      --geo-map-tone-3: oklch(0.84 0.095 58);
      --geo-map-tone-4: oklch(0.8 0.1 45);
      --geo-map-tone-5: oklch(0.76 0.1 35);
      --geo-map-tone-6: oklch(0.72 0.1 27);
    }
    &[data-palette="mono"] {
      --geo-map-tone-0: color-mix(in oklab, var(--foreground) 5%, var(--card));
      --geo-map-tone-1: color-mix(in oklab, var(--foreground) 9%, var(--card));
      --geo-map-tone-2: color-mix(in oklab, var(--foreground) 13%, var(--card));
      --geo-map-tone-3: color-mix(in oklab, var(--foreground) 17%, var(--card));
      --geo-map-tone-4: color-mix(in oklab, var(--foreground) 7%, var(--card));
      --geo-map-tone-5: color-mix(in oklab, var(--foreground) 11%, var(--card));
      --geo-map-tone-6: color-mix(in oklab, var(--foreground) 15%, var(--card));
    }
    &[data-palette="chart"] {
      --geo-map-tone-0: color-mix(in oklab, var(--chart-1) 45%, var(--card));
      --geo-map-tone-1: color-mix(in oklab, var(--chart-2) 40%, var(--card));
      --geo-map-tone-2: color-mix(in oklab, var(--chart-3) 35%, var(--card));
      --geo-map-tone-3: color-mix(in oklab, var(--chart-4) 30%, var(--card));
      --geo-map-tone-4: color-mix(in oklab, var(--chart-5) 25%, var(--card));
      --geo-map-tone-5: color-mix(in oklab, var(--chart-1) 25%, var(--card));
      --geo-map-tone-6: color-mix(in oklab, var(--chart-3) 20%, var(--card));
    }
    &[data-variant="plain"] {
      border: 0;
      border-radius: 0;
      background: transparent;
    }
  }
  .dark .geo-map {
    --geo-map-ocean: color-mix(in oklab, var(--background) 92%, #5a9fb3);
    --geo-map-border: color-mix(in oklab, var(--background) 70%, transparent);
    --geo-map-grid: color-mix(in oklab, var(--foreground) 10%, transparent);
    --geo-map-route: #ff9c76;
    --geo-map-cell-fill: color-mix(in oklab, #54bbaf 25%, transparent);
    --geo-map-cell-border: #8adbd2;
    --geo-map-mix: 58%;
    --geo-map-select: oklch(0.72 0.16 28);
    --geo-map-l0: 0.2;
    --geo-map-lk: 0.33;
    --geo-map-ck: 0.9;
    &:is([data-palette="mono"], [data-palette="chart"]) {
      --geo-map-l0: 0;
      --geo-map-lk: 1;
      --geo-map-ck: 1;
    }
  }
  .geo-map-svg {
    display: block;
    inline-size: 100%;
    block-size: 100%;
    touch-action: none;
    cursor: grab;
    user-select: none;
    &:active {
      cursor: grabbing;
    }
    &:focus-visible {
      outline: 2px solid var(--ring);
      outline-offset: -3px;
    }
  }
  .geo-map-sphere path {
    fill: var(--geo-map-ocean);
    stroke: var(--geo-map-grid);
  }
  .geo-map-graticule path {
    fill: none;
    stroke: var(--geo-map-grid);
    stroke-width: 0.6;
    pointer-events: none;
  }
  /* a region's fill (and its lifted copy's): its tone, light as written; dark through the lightness map above. The
     context, the pick and the cells set their own fill on a more specific selector */
  .geo-map-region,
  .geo-map-lifted {
    fill: color-mix(in oklab, var(--geo-map-tone) var(--geo-map-mix), var(--background));
    @supports (color: oklch(from red l c h)) {
      fill: oklch(
        from var(--geo-map-tone) calc(var(--geo-map-l0) + l * var(--geo-map-lk)) calc(c * var(--geo-map-ck)) h
      );
    }
    &[data-tone="0"] {
      --geo-map-tone: var(--geo-map-tone-0);
    }
    &[data-tone="1"] {
      --geo-map-tone: var(--geo-map-tone-1);
    }
    &[data-tone="2"] {
      --geo-map-tone: var(--geo-map-tone-2);
    }
    &[data-tone="3"] {
      --geo-map-tone: var(--geo-map-tone-3);
    }
    &[data-tone="4"] {
      --geo-map-tone: var(--geo-map-tone-4);
    }
    &[data-tone="5"] {
      --geo-map-tone: var(--geo-map-tone-5);
    }
    &[data-tone="6"] {
      --geo-map-tone: var(--geo-map-tone-6);
    }
  }
  .geo-map-region {
    stroke: var(--geo-map-border);
    stroke-width: 0.65;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    transition:
      fill 180ms ease,
      opacity 180ms ease,
      stroke 180ms ease;
    outline: none;
    &:focus-visible {
      stroke: var(--ring);
      stroke-width: 2.5;
    }
  }
  .geo-map-regions .geo-map-region:hover,
  .geo-map-cells .geo-map-region:hover {
    stroke: var(--foreground);
    stroke-width: 1.2;
  }
  /* around a focus: the rest of the world, dimmed and inert */
  .geo-map-context {
    pointer-events: none;
    & .geo-map-region {
      fill: color-mix(in oklab, var(--muted) 70%, var(--geo-map-ocean));
      opacity: 0.55;
    }
  }
  .geo-map[data-border="none"] :is(.geo-map-regions, .geo-map-context) .geo-map-region {
    stroke: none;
  }
  .geo-map[data-border="dashed"] :is(.geo-map-regions, .geo-map-context) .geo-map-region {
    stroke-dasharray: 3 2;
  }
  .geo-map[data-border="cased"] :is(.geo-map-regions, .geo-map-context) .geo-map-region {
    stroke: var(--background);
    stroke-width: 2;
    paint-order: stroke fill;
  }
  /* the selection: fill (default), outline, pulse, lift */
  .geo-map-regions .geo-map-region[aria-pressed="true"],
  .geo-map-cells .geo-map-region[aria-pressed="true"] {
    fill: color-mix(in oklab, var(--geo-map-select) 62%, var(--card));
    stroke: var(--geo-map-select);
    stroke-width: 1.4;
  }
  .geo-map[data-highlight="outline"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] {
    fill: color-mix(in oklab, var(--geo-map-select) 14%, transparent);
    stroke: var(--geo-map-select);
    stroke-width: 2.4;
    stroke-dasharray: 6 4;
    animation: geo-map-march 900ms linear infinite;
  }
  .geo-map[data-highlight="pulse"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] {
    animation: geo-map-pulse 1.6s ease-in-out infinite;
  }
  .geo-map[data-highlight="lift"] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"] {
    fill: color-mix(in oklab, var(--foreground) 22%, var(--geo-map-ocean));
    stroke: none;
  }
  @keyframes geo-map-march {
    to {
      stroke-dashoffset: -20;
    }
  }
  @keyframes geo-map-pulse {
    50% {
      fill: color-mix(in oklab, var(--geo-map-select) 28%, var(--card));
      stroke-width: 3;
    }
  }
  /* hover (and data-highlight="lift"): a copy of the region raised a little, its shadow on the map below */
  .geo-map-lift {
    pointer-events: none;
  }
  .geo-map-lifted {
    stroke: var(--foreground);
    stroke-width: 1;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    translate: 0 -3px;
    filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.28));
    transition:
      translate 160ms ease-out,
      filter 160ms ease-out;
    @starting-style {
      translate: 0 0;
      filter: drop-shadow(0 0 0 rgb(0 0 0 / 0));
    }
    &[aria-pressed="true"] {
      fill: color-mix(in oklab, var(--geo-map-select) 62%, var(--card));
      stroke: var(--geo-map-select);
    }
    &.geo-map-cell {
      fill: var(--geo-map-cell-fill);
      stroke: var(--geo-map-cell-border);
    }
  }
  .geo-map[data-highlight="lift"] .geo-map-lifted[aria-pressed="true"] {
    translate: 0 -5px;
    filter: drop-shadow(0 7px 5px rgb(0 0 0 / 0.32));
  }
  .geo-map-cells .geo-map-region {
    fill: var(--geo-map-cell-fill);
    stroke: var(--geo-map-cell-border);
    stroke-width: 1.2;
  }
  .geo-map-lines {
    pointer-events: none;
  }
  .geo-map-line {
    fill: none;
    stroke: var(--geo-map-route);
    stroke-width: 2.5;
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
  }
  .geo-map-markers {
    pointer-events: none;
  }
  .geo-map-marker {
    & > :not(text) {
      fill: var(--geo-map-route);
      stroke: var(--background);
      stroke-width: 2;
    }
    & text {
      font: 500 11px var(--font-sans);
      fill: var(--foreground);
      stroke: var(--background);
      stroke-width: 3px;
      paint-order: stroke;
      stroke-linejoin: round;
    }
  }
  .geo-map-hatch {
    fill: none;
    stroke: var(--muted-foreground);
    stroke-width: 1;
  }
  /* callouts: a leader line from each region to its name outside the map, the name written on the line */
  .geo-map-callout {
    cursor: pointer;
  }
  .geo-map-callout-line {
    fill: none;
    stroke: color-mix(in oklab, var(--foreground) 45%, transparent);
    stroke-width: 0.8;
    vector-effect: non-scaling-stroke;
  }
  .geo-map-callout-dot {
    fill: var(--foreground);
  }
  /* the script spaces the rows and shortens the names by this size (--geo-map-callout-size) */
  .geo-map-callout-label {
    font: 500 var(--geo-map-callout-size) var(--font-sans);
    fill: var(--foreground);
  }
  .geo-map-callout:is([data-hover], [data-selected]) {
    & .geo-map-callout-line {
      stroke: var(--geo-map-select);
      stroke-width: 1.6;
    }
    & .geo-map-callout-dot {
      fill: var(--geo-map-select);
    }
    & .geo-map-callout-label {
      fill: var(--geo-map-select);
      font-weight: 600;
    }
  }
  .geo-map-tooltip {
    position: absolute;
    z-index: 3;
    max-inline-size: 14rem;
    padding: 0.35rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--popover-foreground);
    background: var(--popover);
    box-shadow: var(--shadow-md);
    pointer-events: none;
    font-size: 0.75rem;
    white-space: nowrap;
    &[hidden] {
      display: none;
    }
  }
  .geo-map-error {
    position: absolute;
    inset: auto 1rem 1rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--destructive);
    border-radius: var(--radius-md);
    color: var(--destructive);
    background: var(--card);
    font-size: 0.8125rem;
  }
  .geo-map[data-geo-status="loading"] .geo-map-svg {
    animation: geo-map-loading 1.2s ease-in-out infinite alternate;
  }
  @keyframes geo-map-loading {
    to {
      opacity: 0.55;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .geo-map-region,
    .geo-map-lifted {
      transition: none;
    }
    .geo-map[data-highlight] :is(.geo-map-regions, .geo-map-cells) .geo-map-region[aria-pressed="true"],
    .geo-map[data-geo-status="loading"] .geo-map-svg {
      animation: none;
    }
  }
  @media (prefers-contrast: more) {
    .geo-map {
      --geo-map-border: var(--foreground);
    }
    .geo-map-region {
      stroke-width: 1;
    }
    .geo-map-callout-line {
      stroke: var(--foreground);
    }
  }
  @media (forced-colors: active) {
    .geo-map-sphere path {
      fill: Canvas;
      stroke: CanvasText;
    }
    .geo-map-region,
    .geo-map-lifted {
      fill: Canvas;
      stroke: CanvasText;
    }
    .geo-map-regions .geo-map-region[aria-pressed="true"],
    .geo-map-lifted[aria-pressed="true"] {
      fill: Highlight;
      stroke: CanvasText;
    }
    .geo-map-line,
    .geo-map-callout-line {
      stroke: CanvasText;
    }
    .geo-map-callout-label,
    .geo-map-marker text {
      fill: CanvasText;
    }
  }
}

§JS view file

// -- Geo Map (geo-map) -------------------------------------------------------
// Vector geography in SVG: countries, continents and first-order administrative
// regions (Natural Earth v5.1.2) on five projections, plus the library the map
// is made of - WGS84 geodesics, S2 cells, a Morton spatial index and weighted
// routing over application-supplied segments (df$.shadcn.geo).
//
// - Lazy: the official engines arrive on the first map or the first
//   df$.shadcn.geo.load() - d3-geo (projections, clipping, paths; with its
//   d3-array helper), GeographicLib (ellipsoidal geodesics) and defuss-dson
//   (the DGEO metadata), each from its pinned jsDelivr build (GEO_VENDOR_URLS,
//   or configure({ urls }) to self-host). S2, Morton, routing and the DGEO
//   decoder are this file's own code: a vendor would cost more than it buys -
//   VERIFIED: s2js, the reference tests/e2e/site/geo-lib.e2e.ts compares this
//   S2 against, bundles to 168,690 bytes minified; Morton and Dijkstra are a
//   few dozen lines each.
// - Data: a manifest of DGEO chunks (data/ beside this file - the world
//   overview stays resident, a country's detail and its states load when they
//   come into view, the original 10m states from 24x). data-src, a
//   <meta name="geo-map-data">, configure({ data }) or the release on jsDelivr.
// - Regions are state: the selection (one or several, picked by click or set by
//   setState) and the focus (a region framed in view, its children shown -
//   Europe shows its countries, Germany its states) are attributes on the
//   element, so CSS animates them and render() reproduces them.
// - Callouts: a leader line from every region in view to its name outside the
//   map (data-callouts).
//
// State lives ON the element (data-* + the store); the bound `api` is the only
// state mutator (AGENTS.md "State API").
import { defussGlobals, defussQuery, componentState, bindComponent } from '../../../shared/state-api.js';
const df$ = defussGlobals();
const dfDollar = defussQuery();
// VERIFIED: (verify's component types ratchet - tsc -p tsconfig.components.json) every type this file's API docs
// state - arguments, return values, event details - holds against its code.
// 'default' = nothing picked, 'selected' = one or more regions picked, 'focused' = a region framed with its children
const geoMapStates = ['default', 'selected', 'focused'];
// -- types ----------------------------------------------------------------------
/** A position in WGS84 degrees, longitude first (GeoJSON order): [longitude, latitude]. */
export type LonLat = [number, number];
/** A rectangle in degrees: [west, south, east, north]; west > east crosses the antimeridian. */
export type GeoBounds = [number, number, number, number];
/** A boundary level: continents, countries (and territories), first-order administrative regions. */
export type GeoLevel = 'continent' | 'country' | 'state';
/** A map projection. */
export type GeoProjectionName = 'equalEarth' | 'equalArea' | 'mercator' | 'equirectangular' | 'orthographic';
/** A fill palette for the regions. */
export type GeoPalette = 'atlas' | 'ocean' | 'forest' | 'sunset' | 'heat' | 'mono' | 'chart';
/** How region borders are drawn. */
export type GeoBorder = 'natural' | 'cased' | 'dashed' | 'none';
/** What a region is: its identity and place in the hierarchy. */
export interface GeoRegionProperties {
  /** the id within its level: a continent's name, a country's ADM0_A3 code, a state's adm1_code */
  id: string;
  /** the display name (English) */
  name: string;
  /** its level */
  level: GeoLevel;
  /** a state's country (ADM0_A3) */
  parent?: string;
  /** a country's continent */
  continent?: string;
}
/** A region's outline: a polygon or several, rings in D3 winding (the smaller spherical area is the inside). */
export interface GeoGeometry {
  /** 'Polygon' or 'MultiPolygon' */
  type: 'Polygon' | 'MultiPolygon';
  /** rings of [longitude, latitude] positions (a MultiPolygon: one list of rings per polygon) */
  coordinates: LonLat[][] | LonLat[][][];
}
/** A region as a GeoJSON feature. */
export interface GeoRegion {
  /** always 'Feature' */
  type: 'Feature';
  /** identity, name, level and parent */
  properties: GeoRegionProperties;
  /** the outline */
  geometry: GeoGeometry;
}
/** A reference ellipsoid: equatorial radius and flattening. */
export interface GeoEllipsoid {
  /** the equatorial radius in metres */
  a: number;
  /** the flattening (0 = a sphere) */
  f: number;
}
/** The inverse geodesic problem solved: how far apart two positions are and in which direction. */
export interface GeoInverse {
  /** the surface distance along the geodesic, in metres */
  meters: number;
  /** the bearing leaving the first position, degrees clockwise from north */
  initialBearing: number;
  /** the bearing arriving at the second position */
  finalBearing: number;
}
/** An S2 cell: its 64-bit id, its hexadecimal token and its level. */
export interface S2Cell {
  /** the exact cell id (never a Number - 64 bits do not fit) */
  id: bigint;
  /** the canonical lowercase hex token (trailing zeros dropped) */
  token: string;
  /** 0 (a cube face) to 30 (about 1 cm²) */
  level: number;
}
/** A node of a route graph. */
export interface GeoRouteNode {
  /** a unique id */
  id: string;
  /** where it is */
  position: LonLat;
  /** a display name */
  name?: string;
}
/** A segment between two nodes, with the length the application measured. */
export interface GeoRouteSegment {
  /** a unique id */
  id: string;
  /** the start node's id */
  from: string;
  /** the end node's id */
  to: string;
  /** the network length in metres - authoritative, independent of the drawn geometry */
  lengthMeters: number;
  /** usable in both directions (default: only from the from node to the to node) */
  bidirectional?: boolean;
  /** the drawn path; its first and last positions equal the nodes' (default: the straight pair) */
  coordinates?: LonLat[];
}
/** A position inside a segment, as a fraction of its length. */
export interface GeoSegmentPosition {
  /** the segment's id */
  segment: string;
  /** 0 (its from node) to 1 (its to node) */
  fraction: number;
}
/** One segment a route travels. */
export interface GeoRouteStep {
  /** the segment's id */
  segment: string;
  /** the node it leaves */
  from: string;
  /** the node it reaches */
  to: string;
  /** travelled against its direction */
  reversed: boolean;
  /** the metres it contributes */
  lengthMeters: number;
  /** its path, in travel order */
  coordinates: LonLat[];
}
/** A shortest route. */
export interface GeoRoute {
  /** the summed segment lengths in metres */
  lengthMeters: number;
  /** the nodes passed, in order */
  nodeIds: string[];
  /** the segments travelled */
  steps: GeoRouteStep[];
  /** the whole path */
  coordinates: LonLat[];
}
/** The nearest node to a position. */
export interface GeoNearestNode {
  /** the node */
  node: GeoRouteNode;
  /** its WGS84 distance from the position, in metres */
  meters: number;
}
/** A weighted directed graph: Dijkstra over the supplied lengths, a Morton index over the node positions. */
export interface GeoRouteGraph {
  /** the shortest route between two nodes, or null when none connects them */
  route: (from: string, to: string) => GeoRoute | null;
  /** the shortest route between two positions inside segments (each cut proportionally) */
  routeAt: (from: GeoSegmentPosition, to: GeoSegmentPosition) => GeoRoute | null;
  /** the node closest to a position (WGS84), optionally among the nodes in a rectangle */
  nearestNode: (position: LonLat, bounds?: GeoBounds) => GeoNearestNode | null;
  /** the nodes inside a rectangle (Morton prefix ranges, then the exact test) */
  nodesInBounds: (bounds: GeoBounds) => GeoRouteNode[];
  /** the node ids in Morton (Z) order */
  mortonOrder: () => string[];
}
/** A Morton (Z-order) index over positions. */
export interface GeoMortonIndex {
  /** how many items it holds */
  size: number;
  /** the items in Morton order */
  ordered: () => unknown[];
  /** the items inside a rectangle */
  query: (bounds: GeoBounds) => unknown[];
}
/** A quantized TopoJSON topology (what a DGEO chunk decodes to). */
export interface GeoTopology {
  /** always 'Topology' */
  type: 'Topology';
  /** the named geometry collections */
  objects: Record<string, unknown>;
  /** the delta-encoded arcs */
  arcs: number[][][];
  /** the quantization transform */
  transform?: { scale: [number, number]; translate: [number, number] };
}
/** A marker on the map. */
export interface GeoMarker {
  /** a unique id */
  id: string;
  /** a label beside it (and its accessible name) */
  label?: string;
  /** where: [longitude, latitude], or an S2 cell's center */
  position: LonLat | { s2: string };
  /** a CSS color (default: the route color) */
  color?: string;
  /** circle (default), diamond, square or pin */
  shape?: 'circle' | 'diamond' | 'square' | 'pin';
}
/** A line on the map: given positions, or the geodesic between two. */
export interface GeoLine {
  /** a unique id */
  id: string;
  /** the positions it passes */
  coordinates?: LonLat[];
  /** with `to` instead of coordinates: the WGS84 geodesic from here */
  from?: LonLat;
  /** the geodesic's end */
  to?: LonLat;
  /** a CSS color */
  color?: string;
  /** the stroke width in px */
  width?: number;
  /** an SVG dash array ("4 3") */
  dash?: string;
}
/** A region's or cell's own look, over its palette fill. */
export interface GeoTileStyle {
  /** a CSS fill (a color, a token, the hatch url) */
  fill?: string;
  /** a CSS stroke */
  stroke?: string;
  /** the stroke width in px */
  strokeWidth?: number;
  /** an SVG dash array */
  dash?: string;
  /** 0 to 1 */
  opacity?: number;
}
/** What a map draws over its boundaries (the script.geo-map-layers JSON has the same shape). */
export interface GeoLayers {
  /** markers */
  markers?: GeoMarker[];
  /** lines */
  lines?: GeoLine[];
  /** S2 cells, by token */
  cells?: string[];
  /** per-region looks, by key (country:DEU, state:DEU-1591, continent:Europe, s2:47a1) */
  styles?: Record<string, GeoTileStyle>;
}
/** A map's state: every field optional in setState() (merged into the current one); getState() reports all. */
export interface GeoMapConfig {
  /** the projection (default equalEarth) */
  projection?: GeoProjectionName;
  /** the boundary level, or 'auto' by zoom: continents below 2x, countries below 8x, states above (default country) */
  level?: GeoLevel | 'auto';
  /** the fill palette (default atlas) */
  palette?: GeoPalette;
  /** the border style (default natural) */
  border?: GeoBorder;
  /** the latitude / longitude grid (default true) */
  graticule?: boolean;
  /** leader lines from the regions in view to their names outside the map (default false) */
  callouts?: boolean;
  /** the picked regions' keys (level:id - country:FRA); a string may list them separated by spaces or commas */
  selected?: string[] | string;
  /** the framed key (a region, or an S2 cell as s2:<token>), or null; its children are shown, the rest dimmed */
  focus?: string | null;
  /** the view's center */
  center?: LonLat;
  /** the zoom factor, 1 to 1024 */
  zoom?: number;
}
/** setState() configs per state. */
export interface GeoMapStateConfigs {
  /** Nothing picked, nothing framed - the selection and the focus are cleared. */
  default: GeoMapConfig;
  /** One or more regions picked (`config.selected`): highlighted the way data-highlight says. */
  selected: GeoMapConfig;
  /** A region framed in view (`config.focus`) with its children shown - a continent's countries, a country's states. */
  focused: GeoMapConfig;
}
/** One search result. */
export interface GeoSearchHit {
  /** the region's key (country:DEU) */
  key: string;
  /** its name */
  name: string;
  /** its level */
  level: GeoLevel;
  /** what a list shows: the name, a state's country after a comma */
  label: string;
}
/** A region drawn in the current view. */
export interface GeoVisibleRegion {
  /** its key */
  key: string;
  /** its name */
  name: string;
  /** its level */
  level: GeoLevel;
  /** its label point in SVG pixels */
  anchor: [number, number] | null;
}
/** What the last render drew and loaded. */
export interface GeoRenderStats {
  /** regions drawn */
  visibleRegions: number;
  /** context regions drawn dimmed around a focus */
  contextRegions: number;
  /** S2 cells drawn */
  visibleCells: number;
  /** markers drawn */
  visibleMarkers: number;
  /** callouts drawn */
  callouts: number;
  /** milliseconds the render took */
  renderMs: number;
  /** detail chunks in the cache */
  cachedChunks: number;
  /** encoded bytes of those chunks */
  loadedBytes: number;
  /** chunks still loading */
  loading: number;
}
/** The `geo-map-select` event's detail: the picked regions. */
export interface GeoMapSelectDetail {
  /** their keys */
  keys: string[];
  /** their names (a key whose region is not loaded names itself) */
  names: string[];
}
/** The `geo-map-focus` event's detail: the framed region. */
export interface GeoMapFocusDetail {
  /** its key, or null when the focus was cleared */
  key: string | null;
  /** its name, or null */
  name: string | null;
}
/** The `geo-map-view` event's detail: where the camera came to rest. */
export interface GeoMapViewDetail {
  /** the center */
  center: LonLat;
  /** the zoom factor */
  zoom: number;
  /** the level drawn */
  level: GeoLevel;
}
/** The `geo-map-error` event's detail: what could not be loaded. */
export interface GeoMapErrorDetail {
  /** the message */
  message: string;
  /** the URL that failed, or '' */
  url: string;
}
/** The engines' URLs (the pinned official builds). */
export interface GeoVendorUrls {
  /** d3-array (UMD; d3-geo's helpers) */
  d3Array: string;
  /** d3-geo (UMD) */
  d3Geo: string;
  /** GeographicLib geodesic (UMD) */
  geodesic: string;
  /** defuss-dson (ESM) */
  dson: string;
}
/** What configure() sets for every map and the library. */
export interface GeoConfigureOptions {
  /** the default data manifest URL (a map's data-src wins) */
  data?: string;
  /** engine URLs to load instead of the pinned jsDelivr builds (the same versions, self-hosted) */
  urls?: Partial<GeoVendorUrls>;
  /** the bounds a focus frames, by region key, over the built-in ones (Europe, North America, Oceania) */
  views?: Record<string, GeoBounds>;
}
// -- engines -------------------------------------------------------------------------------
/** The pinned official builds the component loads (never @latest). */
export const GEO_VENDOR_URLS: GeoVendorUrls = {
  d3Array: 'https://cdn.jsdelivr.net/npm/d3-array@3.2.4/dist/d3-array.min.js',
  d3Geo: 'https://cdn.jsdelivr.net/npm/d3-geo@3.1.1/dist/d3-geo.min.js',
  geodesic: 'https://cdn.jsdelivr.net/npm/geographiclib-geodesic@2.2.0/geographiclib-geodesic.min.js',
  dson: 'https://cdn.jsdelivr.net/npm/defuss-dson@1.1.2/dist/index.mjs',
};
/**
 * the data the release ships: its npm package on jsDelivr (pinned to the release core reports). Not the GitHub route
 * the docs load the bundles from - VERIFIED (curl, 2026-11-02): the repository is past jsDelivr's 50 MB limit for
 * /gh/, which answers a file's first fetch with 403 "Package size exceeded" now and then (3 of 31 fresh chunks), and
 * a map fetches hundreds of them; /npm/ (72 MB unpacked, within its limit) served 41 of 41
 */
const releaseData = (): string =>
  `https://cdn.jsdelivr.net/npm/defuss-shadcn@${df$.shared?.abi ?? '0.0.0'}/dist/components/geo-map/data/manifest.json`;
interface Engines {
  d3: any;
  geodesic: any;
  DSON: { parse(text: string): unknown; stringify(value: unknown): string };
}
let engines: Engines | null = null;
let enginesPromise: Promise<Engines> | null = null;
const settings: { data: string; urls: GeoVendorUrls; views: Record<string, GeoBounds> } = {
  data: '',
  urls: { ...GEO_VENDOR_URLS },
  views: {},
};
const LOAD_MS = 20_000;
const metaContent = (name: string): string =>
  ((dfDollar(`meta[name="${name}"]`).get(0) ?? null) as HTMLMetaElement | null)?.content ?? '';
/** the one vendor import site (verify: VENDOR_IMPORTS) - the official engines, never our code */
function vendor(vendorUrl: string): Promise<any> {
  // a microtask later: a failing import is a rejection, never a synchronous throw out of the caller
  return Promise.resolve().then(() => import(/* @vite-ignore */ vendorUrl));
}
/**
 * Load the engines once: d3-geo with d3-array, GeographicLib and defuss-dson (the pinned builds, or the configured
 * URLs). Every map calls it; call it yourself before using the geodesic functions. A failed load can be retried.
 * @returns resolves when every engine is ready
 */
function load(): Promise<void> {
  if (engines) return Promise.resolve();
  if (!enginesPromise) {
    const urls = settings.urls;
    const present = (globalThis as any).d3;
    // a page that already has d3 (v7 bundles d3-geo) keeps it - the UMD builds install on that global
    const d3 =
      present?.geoPath && present?.geoEqualEarth
        ? Promise.resolve()
        : vendor(urls.d3Array).then(() => vendor(urls.d3Geo));
    const pending = Promise.all([d3, vendor(urls.geodesic), vendor(urls.dson)]).then(([, , dson]) => {
      const g = globalThis as any;
      if (!g.d3?.geoPath) throw new Error('d3-geo did not install globalThis.d3');
      if (!g.geodesic?.Geodesic) throw new Error('GeographicLib did not install globalThis.geodesic');
      const DSON = dson?.DSON ?? dson?.default?.DSON;
      if (!DSON) throw new Error('defuss-dson exports no DSON');
      engines = { d3: g.d3, geodesic: g.geodesic, DSON };
      return engines;
    });
    pending.catch(() => {
      if (enginesPromise === pending) enginesPromise = null;
    });
    enginesPromise = pending;
  }
  let timer = 0;
  const deadline = new Promise<never>((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`the geography engines did not load within ${LOAD_MS / 1000} s`)),
      LOAD_MS,
    ) as unknown as number;
  });
  return Promise.race([enginesPromise, deadline])
    .then(() => undefined)
    .finally(() => clearTimeout(timer));
}
const ready = (): Engines => {
  if (!engines) throw new Error('the geography engines are not loaded - await df$.shadcn.geo.load() first');
  return engines;
};
/**
 * Set the data URL, the engine URLs or the focus views for every map on the page (maps already showing keep their
 * data). Engine URLs only take effect before the engines first load.
 * @param options - `{ data, urls, views }`
 */
function configure(options: GeoConfigureOptions): void {
  if (options.data !== undefined) settings.data = String(options.data);
  if (options.urls) settings.urls = { ...settings.urls, ...options.urls };
  if (options.views) settings.views = { ...settings.views, ...options.views };
}
// -- validation ----------------------------------------------------------------------------
function assertLonLat(point: unknown): asserts point is LonLat {
  const p = point as number[];
  if (!Array.isArray(p) || p.length !== 2 || !p.every(Number.isFinite) || Math.abs(p[0]!) > 180 || Math.abs(p[1]!) > 90)
    throw new RangeError('expected [longitude, latitude] in [-180, 180] x [-90, 90] degrees');
}
function assertBounds(b: GeoBounds): void {
  if (!Array.isArray(b) || b.length !== 4) throw new RangeError('expected [west, south, east, north]');
  assertLonLat([b[0], b[1]]);
  assertLonLat([b[2], b[3]]);
  if (b[1] > b[3]) throw new RangeError('south must not exceed north');
}
/**
 * Whether a position lies inside a rectangle (one whose west exceeds its east crosses the antimeridian).
 * @param point - [longitude, latitude]
 * @param bounds - [west, south, east, north]
 * @returns true inside or on the edge
 */
function inBounds(point: LonLat, bounds: GeoBounds): boolean {
  return (
    point[1] >= bounds[1] &&
    point[1] <= bounds[3] &&
    (bounds[0] <= bounds[2]
      ? point[0] >= bounds[0] && point[0] <= bounds[2]
      : point[0] >= bounds[0] || point[0] <= bounds[2])
  );
}
const wrapLon = (lon: number): number => ((((lon + 180) % 360) + 360) % 360) - 180;
const clampZoom = (z: number): number => Math.max(1, Math.min(1024, z));
const clamp = (v: number, lo: number, hi: number): number => Math.max(lo, Math.min(hi, v));
// -- geodesy (GeographicLib) ------------------------------------------------------------------
/** WGS84: the GPS reference ellipsoid. */
const WGS84: GeoEllipsoid = Object.freeze({ a: 6378137, f: 1 / 298.257223563 });
const solvers = new Map<string, any>();
function solver(ellipsoid: GeoEllipsoid = WGS84): any {
  const key = `${ellipsoid.a}/${ellipsoid.f}`;
  let s = solvers.get(key);
  if (!s) {
    if (
      !Number.isFinite(ellipsoid.a) ||
      ellipsoid.a <= 0 ||
      !Number.isFinite(ellipsoid.f) ||
      ellipsoid.f < 0 ||
      ellipsoid.f >= 1 / 50
    )
      throw new RangeError('an ellipsoid needs a > 0 and 0 <= f < 1/50');
    s = new (ready().geodesic.Geodesic.Geodesic)(ellipsoid.a, ellipsoid.f);
    solvers.set(key, s);
  }
  return s;
}
/**
 * The distance and bearings between two positions along the ellipsoid's surface (Karney's algorithm - nearly
 * antipodal points included). Excludes altitude, terrain and road detours.
 * @param a - [longitude, latitude] of the start
 * @param b - [longitude, latitude] of the end
 * @param ellipsoid - the reference ellipsoid (default WGS84)
 * @returns metres and the bearings at both ends
 */
function inverse(a: LonLat, b: LonLat, ellipsoid?: GeoEllipsoid): GeoInverse {
  assertLonLat(a);
  assertLonLat(b);
  const r = solver(ellipsoid).Inverse(a[1], a[0], b[1], b[0]);
  return { meters: r.s12, initialBearing: r.azi1, finalBearing: r.azi2 };
}
/**
 * Where a geodesic leads: the position a given distance away from a start in a given bearing.
 * @param origin - [longitude, latitude] of the start
 * @param bearing - degrees clockwise from north
 * @param meters - the distance along the surface
 * @param ellipsoid - the reference ellipsoid (default WGS84)
 * @returns the end position
 */
function direct(origin: LonLat, bearing: number, meters: number, ellipsoid?: GeoEllipsoid): LonLat {
  assertLonLat(origin);
  if (!Number.isFinite(bearing) || !Number.isFinite(meters)) throw new RangeError('a finite bearing and distance');
  const p = solver(ellipsoid).Direct(origin[1], origin[0], bearing, meters);
  return [p.lon2, p.lat2];
}
/**
 * The geodesic between two positions as positions at most `maxStepMeters` apart - a line the map draws as the
 * shortest surface path.
 * @param a - the start
 * @param b - the end
 * @param maxStepMeters - the largest spacing between samples (default 20000)
 * @param ellipsoid - the reference ellipsoid (default WGS84)
 * @returns the positions, a first and b last
 */
function geodesicPath(a: LonLat, b: LonLat, maxStepMeters: number = 20_000, ellipsoid?: GeoEllipsoid): LonLat[] {
  if (!Number.isFinite(maxStepMeters) || maxStepMeters <= 0) throw new RangeError('maxStepMeters must be positive');
  const { meters } = inverse(a, b, ellipsoid);
  const n = Math.max(1, Math.ceil(meters / maxStepMeters));
  if (n > 100_000) throw new RangeError('too many geodesic samples (at most 100000 segments)');
  const line = solver(ellipsoid).InverseLine(a[1], a[0], b[1], b[0]);
  const points: LonLat[] = [[a[0], a[1]]];
  for (let i = 1; i < n; i++) {
    const p = line.Position((meters * i) / n);
    points.push([p.lon2, p.lat2]);
  }
  points.push([b[0], b[1]]);
  return points;
}
/**
 * The length of a polyline along the ellipsoid: the sum of its segments' geodesic distances.
 * @param points - two or more positions
 * @param ellipsoid - the reference ellipsoid (default WGS84)
 * @returns metres
 */
function polylineLength(points: LonLat[], ellipsoid?: GeoEllipsoid): number {
  let sum = 0;
  for (let i = 1; i < points.length; i++) sum += inverse(points[i - 1]!, points[i]!, ellipsoid).meters;
  return sum;
}
// -- S2 (cube faces, Hilbert curve, 64-bit ids) ----------------------------------------------
// VERIFIED: (tests/e2e/geography/geo-lib.e2e.ts) tokens, levels, centers, vertices, edge neighbors, parents and
// children equal s2js 1.44.0 (a port of Google's S2 library) on random positions and levels.
const S2_MAX = 2 ** 30;
const SWAP = 1;
const INVERT = 2;
const POS_TO_IJ = [
  [0, 1, 3, 2],
  [0, 2, 3, 1],
  [3, 2, 0, 1],
  [3, 1, 0, 2],
];
const POS_TO_ORIENTATION = [SWAP, 0, 0, INVERT | SWAP];
const LOOKUP_POS = new Int32Array(1024);
const LOOKUP_IJ = new Int32Array(1024);
function initLookup(level: number, i: number, j: number, orig: number, pos: number, orientation: number): void {
  if (level === 4) {
    const ij = (i << 4) + j;
    LOOKUP_POS[(ij << 2) + orig] = (pos << 2) + orientation;
    LOOKUP_IJ[(pos << 2) + orig] = (ij << 2) + orientation;
    return;
  }
  const r = POS_TO_IJ[orientation]!;
  for (let k = 0; k < 4; k++)
    initLookup(
      level + 1,
      (i << 1) + (r[k]! >> 1),
      (j << 1) + (r[k]! & 1),
      orig,
      (pos << 2) + k,
      orientation ^ POS_TO_ORIENTATION[k]!,
    );
}
for (const o of [0, SWAP, INVERT, SWAP | INVERT]) initLookup(0, 0, 0, o, 0, o);
const RAD = 180 / Math.PI;
function lonLatToXyz(p: LonLat): [number, number, number] {
  const phi = p[1] / RAD;
  const theta = p[0] / RAD;
  const c = Math.cos(phi);
  return [Math.cos(theta) * c, Math.sin(theta) * c, Math.sin(phi)];
}
function xyzToLonLat(x: number, y: number, z: number): LonLat {
  return [Math.atan2(y, x) * RAD, Math.atan2(z, Math.hypot(x, y)) * RAD];
}
function xyzToFaceUV(x: number, y: number, z: number): [number, number, number] {
  const ax = Math.abs(x);
  const ay = Math.abs(y);
  const az = Math.abs(z);
  let f = ax > ay ? (ax > az ? 0 : 2) : ay > az ? 1 : 2;
  if ((f === 0 && x < 0) || (f === 1 && y < 0) || (f === 2 && z < 0)) f += 3;
  switch (f) {
    case 0:
      return [0, y / x, z / x];
    case 1:
      return [1, -x / y, z / y];
    case 2:
      return [2, -x / z, -y / z];
    case 3:
      return [3, z / x, y / x];
    case 4:
      return [4, z / y, -x / y];
    default:
      return [5, -y / z, -x / z];
  }
}
function faceUVToXyz(f: number, u: number, v: number): [number, number, number] {
  switch (f) {
    case 0:
      return [1, u, v];
    case 1:
      return [-u, 1, v];
    case 2:
      return [-u, -v, 1];
    case 3:
      return [-1, -v, -u];
    case 4:
      return [v, -1, -u];
    default:
      return [v, u, -1];
  }
}
const stToUV = (s: number): number => (s >= 0.5 ? (4 * s * s - 1) / 3 : (1 - 4 * (1 - s) * (1 - s)) / 3);
const uvToST = (u: number): number => (u >= 0 ? 0.5 * Math.sqrt(1 + 3 * u) : 1 - 0.5 * Math.sqrt(1 - 3 * u));
const stToIJ = (s: number): number => clamp(Math.floor(S2_MAX * s), 0, S2_MAX - 1);
function fromFaceIJ(f: number, i: number, j: number): bigint {
  let n = BigInt(f) << 60n;
  let bits = f & SWAP;
  for (let k = 7; k >= 0; k--) {
    bits += ((i >> (k * 4)) & 15) << 6;
    bits += ((j >> (k * 4)) & 15) << 2;
    bits = LOOKUP_POS[bits]!;
    n |= BigInt(bits >> 2) << BigInt(k * 8);
    bits &= SWAP | INVERT;
  }
  return n * 2n + 1n;
}
function faceIJ(id: bigint): [number, number, number] {
  const f = Number(id >> 61n);
  let orientation = f & SWAP;
  let i = 0;
  let j = 0;
  let nbits = 2;
  for (let k = 7; k >= 0; k--) {
    orientation += Number((id >> BigInt(k * 8 + 1)) & BigInt((1 << (2 * nbits)) - 1)) << 2;
    orientation = LOOKUP_IJ[orientation]!;
    i += (orientation >> 6) * 2 ** (k * 4);
    j += ((orientation >> 2) & 15) * 2 ** (k * 4);
    orientation &= SWAP | INVERT;
    nbits = 4;
  }
  return [f, i, j];
}
const lsb = (id: bigint): bigint => id & -id;
const lsbForLevel = (level: number): bigint => 1n << BigInt(2 * (30 - level));
function levelOf(id: bigint): number {
  let tz = 0;
  while (tz < 64 && ((id >> BigInt(tz)) & 1n) === 0n) tz++;
  return 30 - (tz >> 1);
}
const parentAt = (id: bigint, level: number): bigint => {
  const l = lsbForLevel(level);
  return (id & -l) | l;
};
const tokenOf = (id: bigint): string => id.toString(16).padStart(16, '0').replace(/0+$/, '') || 'X';
/**
 * An S2 cell id from a hexadecimal token or a bigint, validated. A string is ALWAYS a hex token - pass a decimal id
 * as BigInt(decimalString).
 * @param cell - a token ('47a1') or an id
 * @returns the 64-bit id
 */
function s2Id(cell: string | bigint): bigint {
  if (typeof cell === 'string' && !/^[0-9a-fA-F]{1,16}$/.test(cell))
    throw new RangeError('expected a 1-16 digit hexadecimal S2 token');
  const id = typeof cell === 'bigint' ? cell : BigInt(`0x${cell.padEnd(16, '0')}`);
  if (id <= 0n || id >= 1n << 64n || Number(id >> 61n) > 5 || (lsb(id) & 0x1555555555555555n) === 0n)
    throw new RangeError('invalid S2 cell id');
  return id;
}
/**
 * The S2 cell holding a position at a level.
 * @param point - [longitude, latitude]
 * @param level - 0 to 30 (default 12, about 2 km across)
 * @returns its id, token and level
 */
function s2Cell(point: LonLat, level: number = 12): S2Cell {
  assertLonLat(point);
  if (!Number.isInteger(level) || level < 0 || level > 30) throw new RangeError('an S2 level is 0 to 30');
  const [f, u, v] = xyzToFaceUV(...lonLatToXyz(point));
  const id = parentAt(fromFaceIJ(f, stToIJ(uvToST(u)), stToIJ(uvToST(v))), level);
  return { id, token: tokenOf(id), level };
}
/**
 * A cell's canonical token.
 * @param cell - a token or an id
 * @returns the lowercase hex token
 */
function s2Token(cell: string | bigint): string {
  return tokenOf(s2Id(cell));
}
/**
 * A cell's level.
 * @param cell - a token or an id
 * @returns 0 to 30
 */
function s2Level(cell: string | bigint): number {
  return levelOf(s2Id(cell));
}
/** the cell's (s, t) square on its face: face, lower i and j, size in leaf cells */
function cellSquare(id: bigint): [number, number, number, number] {
  const [f, i, j] = faceIJ(id);
  const size = 2 ** (30 - levelOf(id));
  return [f, Math.floor(i / size) * size, Math.floor(j / size) * size, size];
}
/**
 * The center of a cell (in its face's (s, t) square - not the original GPS fix a cell was made from).
 * @param cell - a token or an id
 * @returns [longitude, latitude]
 */
function s2Center(cell: string | bigint): LonLat {
  const [f, i, j, size] = cellSquare(s2Id(cell));
  return xyzToLonLat(...faceUVToXyz(f, stToUV((i + size / 2) / S2_MAX), stToUV((j + size / 2) / S2_MAX)));
}
/**
 * A cell's four corners (counterclockwise, as S2 orders them).
 * @param cell - a token or an id
 * @returns four [longitude, latitude] positions
 */
function s2Vertices(cell: string | bigint): LonLat[] {
  const [f, i, j, size] = cellSquare(s2Id(cell));
  const u0 = stToUV(i / S2_MAX);
  const u1 = stToUV((i + size) / S2_MAX);
  const v0 = stToUV(j / S2_MAX);
  const v1 = stToUV((j + size) / S2_MAX);
  return [
    [u0, v0],
    [u1, v0],
    [u1, v1],
    [u0, v1],
  ].map(([u, v]) => xyzToLonLat(...faceUVToXyz(f, u!, v!)));
}
/**
 * A cell as a region the map can draw (great-circle edges; its ring wound for D3).
 * @param cell - a token or an id
 * @returns a GeoJSON polygon feature (properties.id = the token)
 */
function s2Polygon(cell: string | bigint): GeoRegion {
  const token = s2Token(cell);
  // S2 gives counterclockwise vertices; D3 takes the clockwise ring as the small region
  const ring = s2Vertices(cell).reverse();
  ring.push([ring[0]![0], ring[0]![1]]);
  return {
    type: 'Feature',
    properties: { id: token, name: `S2 ${token}`, level: 'state' },
    geometry: { type: 'Polygon', coordinates: [ring] },
  };
}
function fromFaceIJWrap(f: number, i: number, j: number): bigint {
  i = clamp(i, -1, S2_MAX);
  j = clamp(j, -1, S2_MAX);
  const limit = 1 + Number.EPSILON;
  const u = clamp((2 * i + 1 - S2_MAX) / S2_MAX, -limit, limit);
  const v = clamp((2 * j + 1 - S2_MAX) / S2_MAX, -limit, limit);
  const [g, u2, v2] = xyzToFaceUV(...faceUVToXyz(f, u, v));
  return fromFaceIJ(g, stToIJ(0.5 * (u2 + 1)), stToIJ(0.5 * (v2 + 1)));
}
/**
 * The four cells sharing an edge with a cell, at its level (below, right, above, left in its face's frame).
 * @param cell - a token or an id
 * @returns four tokens
 */
function s2Neighbors(cell: string | bigint): string[] {
  const id = s2Id(cell);
  const level = levelOf(id);
  const size = 2 ** (30 - level);
  const [f, i, j] = faceIJ(id);
  return [
    [i, j - size],
    [i + size, j],
    [i, j + size],
    [i - size, j],
  ].map(([a, b]) => tokenOf(parentAt(fromFaceIJWrap(f, a!, b!), level)));
}
/**
 * A cell's four children, one level finer (none at level 30).
 * @param cell - a token or an id
 * @returns their tokens in Hilbert order
 */
function s2Children(cell: string | bigint): string[] {
  const id = s2Id(cell);
  if (levelOf(id) === 30) return [];
  const low = lsb(id);
  const begin = id - low + (low >> 2n);
  const step = low >> 1n;
  return [0n, 1n, 2n, 3n].map((k) => tokenOf(begin + k * step));
}
/**
 * The cell containing a cell at a coarser level.
 * @param cell - a token or an id
 * @param level - 0 to the cell's own level (default one coarser)
 * @returns the parent's token
 */
function s2Parent(cell: string | bigint, level?: number): string {
  const id = s2Id(cell);
  const own = levelOf(id);
  const target = level ?? own - 1;
  if (!Number.isInteger(target) || target < 0 || target > own) throw new RangeError(`a parent level is 0 to ${own}`);
  return tokenOf(parentAt(id, target));
}
/**
 * A position from either form a marker takes: [longitude, latitude] as given, or an S2 cell's center.
 * @param value - a position or `{ s2: token }`
 * @returns [longitude, latitude]
 */
function resolvePosition(value: LonLat | { s2: string | bigint }): LonLat {
  if (Array.isArray(value)) {
    assertLonLat(value);
    return [value[0], value[1]];
  }
  return s2Center((value as { s2: string | bigint }).s2);
}
// -- Morton (Z-order over a longitude / latitude grid - not S2's Hilbert curve) ------------------
function checkBits(bits: number): void {
  if (!Number.isInteger(bits) || bits < 1 || bits > 26) throw new RangeError('Morton bits are 1 to 26');
}
/**
 * Interleave two grid coordinates into a Morton key (x on the even bits, y on the odd).
 * @param x - 0 to 2^bits - 1
 * @param y - 0 to 2^bits - 1
 * @param bits - bits per coordinate, 1 to 26 (default 16)
 * @returns the key
 */
function mortonEncode(x: number, y: number, bits: number = 16): bigint {
  checkBits(bits);
  if (![x, y].every((v) => Number.isInteger(v) && v >= 0 && v < 2 ** bits))
    throw new RangeError('grid coordinate out of range');
  const bx = BigInt(x);
  const by = BigInt(y);
  let key = 0n;
  for (let b = 0; b < bits; b++) {
    const s = BigInt(b);
    key |= (((bx >> s) & 1n) << (2n * s)) | (((by >> s) & 1n) << (2n * s + 1n));
  }
  return key;
}
/**
 * Split a Morton key back into its grid coordinates.
 * @param key - the key
 * @param bits - bits per coordinate (default 16)
 * @returns [x, y]
 */
function mortonDecode(key: bigint, bits: number = 16): [number, number] {
  checkBits(bits);
  if (key < 0n || key >= 1n << BigInt(2 * bits)) throw new RangeError('Morton key out of range');
  let x = 0n;
  let y = 0n;
  for (let b = 0; b < bits; b++) {
    const s = BigInt(b);
    x |= ((key >> (2n * s)) & 1n) << s;
    y |= ((key >> (2n * s + 1n)) & 1n) << s;
  }
  return [Number(x), Number(y)];
}
function mortonGrid(p: LonLat, bits: number): [number, number] {
  const n = 2 ** bits;
  return [Math.min(n - 1, Math.floor(((p[0] + 180) / 360) * n)), Math.min(n - 1, Math.floor(((p[1] + 90) / 180) * n))];
}
/**
 * A position's Morton key on a 2^bits x 2^bits longitude / latitude grid.
 * @param point - [longitude, latitude]
 * @param bits - bits per coordinate (default 16)
 * @returns the key
 */
function mortonKey(point: LonLat, bits: number = 16): bigint {
  assertLonLat(point);
  checkBits(bits);
  return mortonEncode(...mortonGrid(point, bits), bits);
}
/**
 * A Morton index over items with positions: sorted keys, rectangle queries by recursive prefix ranges, then the exact
 * position test (no grid false positives).
 * @param items - the items
 * @param position - an item's [longitude, latitude]
 * @param bits - bits per coordinate (default 16)
 * @returns the index
 */
function mortonIndex<T>(items: readonly T[], position: (item: T) => LonLat, bits: number = 16): GeoMortonIndex {
  checkBits(bits);
  const entries = items
    .map((item) => {
      const point = position(item);
      assertLonLat(point);
      return { key: mortonKey(point, bits), point: [point[0], point[1]] as LonLat, item };
    })
    .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
  const lowerBound = (key: bigint): number => {
    let lo = 0;
    let hi = entries.length;
    while (lo < hi) {
      const m = (lo + hi) >>> 1;
      if (entries[m]!.key < key) lo = m + 1;
      else hi = m;
    }
    return lo;
  };
  const query = (bounds: GeoBounds): T[] => {
    assertBounds(bounds);
    if (bounds[0] > bounds[2])
      return [...query([bounds[0], bounds[1], 180, bounds[3]]), ...query([-180, bounds[1], bounds[2], bounds[3]])];
    const [x0, y0] = mortonGrid([bounds[0], bounds[1]], bits);
    const [x1, y1] = mortonGrid([bounds[2], bounds[3]], bits);
    const out: T[] = [];
    const walk = (x: number, y: number, size: number, depth: number, prefix: bigint): void => {
      if (x > x1 || y > y1 || x + size - 1 < x0 || y + size - 1 < y0) return;
      const shift = BigInt(2 * (bits - depth));
      const begin = lowerBound(prefix << shift);
      const end = lowerBound((prefix + 1n) << shift);
      if (begin === end) return;
      // a small range is cheaper to test directly; pruning stays exact
      if (end - begin <= 32 || depth === bits || (x >= x0 && y >= y0 && x + size - 1 <= x1 && y + size - 1 <= y1)) {
        for (let i = begin; i < end; i++) if (inBounds(entries[i]!.point, bounds)) out.push(entries[i]!.item);
        return;
      }
      const half = size / 2;
      for (let q = 0; q < 4; q++)
        walk(x + (q & 1) * half, y + (q >> 1) * half, half, depth + 1, prefix * 4n + BigInt(q));
    };
    walk(0, 0, 2 ** bits, 0, 0n);
    return out;
  };
  return { size: entries.length, ordered: () => entries.map((e) => e.item), query };
}
// -- routing (Dijkstra over supplied lengths) ---------------------------------------------------
interface RouteArc {
  to: string;
  segment: Required<Pick<GeoRouteSegment, 'coordinates'>> & GeoRouteSegment;
  reversed: boolean;
}
/** a binary min-heap; stale Dijkstra entries are skipped on pop */
class Queue {
  private values: { id: string; cost: number }[] = [];
  push(value: { id: string; cost: number }): void {
    let i = this.values.length;
    this.values.push(value);
    while (i > 0) {
      const p = (i - 1) >>> 1;
      if (this.values[p]!.cost <= value.cost) break;
      this.values[i] = this.values[p]!;
      i = p;
    }
    this.values[i] = value;
  }
  pop(): { id: string; cost: number } | undefined {
    if (!this.values.length) return undefined;
    const first = this.values[0]!;
    const last = this.values.pop()!;
    if (this.values.length) {
      let i = 0;
      while (i * 2 + 1 < this.values.length) {
        let c = i * 2 + 1;
        if (c + 1 < this.values.length && this.values[c + 1]!.cost < this.values[c]!.cost) c++;
        if (this.values[c]!.cost >= last.cost) break;
        this.values[i] = this.values[c]!;
        i = c;
      }
      this.values[i] = last;
    }
    return first;
  }
}
function pointOnLine(points: LonLat[], fraction: number): LonLat {
  if (fraction <= 0) return [points[0]![0], points[0]![1]];
  const last = points[points.length - 1]!;
  if (fraction >= 1) return [last[0], last[1]];
  const lengths = points.slice(1).map((p, i) => inverse(points[i]!, p).meters);
  const total = lengths.reduce((a, b) => a + b, 0);
  if (!total) return [points[0]![0], points[0]![1]];
  const target = total * fraction;
  let before = 0;
  for (let i = 0; i < lengths.length; i++) {
    if (before + lengths[i]! >= target)
      return direct(points[i]!, inverse(points[i]!, points[i + 1]!).initialBearing, target - before);
    before += lengths[i]!;
  }
  return [last[0], last[1]];
}
function sliceLine(points: LonLat[], from: number, to: number): LonLat[] {
  const total = polylineLength(points);
  const out = [pointOnLine(points, from)];
  let before = 0;
  for (let i = 1; i < points.length - 1; i++) {
    before += inverse(points[i - 1]!, points[i]!).meters;
    const f = total ? before / total : 0;
    if (f > from && f < to) out.push([points[i]![0], points[i]![1]]);
  }
  out.push(pointOnLine(points, to));
  return out;
}
/**
 * A route graph over nodes and the segments between them. Dijkstra minimizes the SUPPLIED lengths (no A*: an
 * application's lengths may undercut the surface distance, so a geodesic heuristic would not be admissible); a
 * Morton index finds nodes by position. Needs the engines (df$.shadcn.geo.load()) for nearestNode and routeAt.
 * @param nodes - the nodes (unique ids)
 * @param segments - the segments (unique ids; lengths finite and >= 0)
 * @returns the graph
 */
function routeGraph(nodes: GeoRouteNode[], segments: GeoRouteSegment[]): GeoRouteGraph {
  const byId = new Map<string, GeoRouteNode>();
  const segs = new Map<string, RouteArc['segment']>();
  const adjacency = new Map<string, RouteArc[]>();
  for (const n of nodes) {
    if (!n.id || byId.has(n.id)) throw new Error(`duplicate or empty node id: ${n.id}`);
    assertLonLat(n.position);
    byId.set(n.id, { ...n, position: [n.position[0], n.position[1]] });
    adjacency.set(n.id, []);
  }
  for (const edge of segments) {
    if (!edge.id || segs.has(edge.id)) throw new Error(`duplicate or empty segment id: ${edge.id}`);
    if (!byId.has(edge.from) || !byId.has(edge.to)) throw new Error(`unknown endpoint of segment ${edge.id}`);
    if (!Number.isFinite(edge.lengthMeters) || edge.lengthMeters < 0)
      throw new RangeError(`segment ${edge.id}: the length must be finite and >= 0`);
    const a = byId.get(edge.from)!.position;
    const b = byId.get(edge.to)!.position;
    const points = edge.coordinates?.map((p) => {
      assertLonLat(p);
      return [p[0], p[1]] as LonLat;
    }) ?? [a, b];
    const end = points[points.length - 1]!;
    if (points.length < 2 || points[0]![0] !== a[0] || points[0]![1] !== a[1] || end[0] !== b[0] || end[1] !== b[1])
      throw new Error(`segment ${edge.id}: its geometry must start and end at its nodes`);
    const segment = { ...edge, coordinates: points };
    segs.set(edge.id, segment);
    adjacency.get(edge.from)!.push({ to: edge.to, segment, reversed: false });
    if (edge.bidirectional) adjacency.get(edge.to)!.push({ to: edge.from, segment, reversed: true });
  }
  const index = mortonIndex([...byId.values()], (n) => n.position);
  const copy = (n: GeoRouteNode): GeoRouteNode => ({ ...n, position: [n.position[0], n.position[1]] });
  const route = (from: string, to: string): GeoRoute | null => {
    if (!byId.has(from) || !byId.has(to)) throw new Error('unknown route node');
    const costs = new Map([[from, 0]]);
    const previous = new Map<string, { from: string; arc: RouteArc }>();
    const queue = new Queue();
    queue.push({ id: from, cost: 0 });
    for (let current = queue.pop(); current; current = queue.pop()) {
      if (current.cost !== costs.get(current.id)) continue;
      if (current.id === to) break;
      for (const arc of adjacency.get(current.id)!) {
        const cost = current.cost + arc.segment.lengthMeters;
        if (!Number.isFinite(cost)) throw new RangeError('route cost overflow');
        if (cost < (costs.get(arc.to) ?? Infinity)) {
          costs.set(arc.to, cost);
          previous.set(arc.to, { from: current.id, arc });
          queue.push({ id: arc.to, cost });
        }
      }
    }
    if (!costs.has(to)) return null;
    const steps: GeoRouteStep[] = [];
    const ids = [to];
    for (let id = to; id !== from;) {
      const p = previous.get(id)!;
      const coordinates = p.arc.segment.coordinates.map((q) => [q[0], q[1]] as LonLat);
      if (p.arc.reversed) coordinates.reverse();
      steps.push({
        segment: p.arc.segment.id,
        from: p.from,
        to: id,
        reversed: p.arc.reversed,
        lengthMeters: p.arc.segment.lengthMeters,
        coordinates,
      });
      id = p.from;
      ids.push(id);
    }
    steps.reverse();
    ids.reverse();
    const coordinates = steps.flatMap((step, i) => (i ? step.coordinates.slice(1) : step.coordinates));
    if (!steps.length) {
      const p = byId.get(from)!.position;
      coordinates.push([p[0], p[1]], [p[0], p[1]]);
    }
    return { lengthMeters: costs.get(to)!, nodeIds: ids, steps, coordinates };
  };
  const routeAt = (from: GeoSegmentPosition, to: GeoSegmentPosition): GeoRoute | null => {
    const allNodes = [...byId.values()];
    const cuts = new Map<string, Map<number, string>>();
    const originals = new Map<string, string>();
    let sequence = 0;
    const unique = (): string => {
      let id: string;
      do id = `__geo_cut_${sequence++}`;
      while (byId.has(id) || segs.has(id));
      return id;
    };
    const resolve = (p: GeoSegmentPosition): string => {
      const edge = segs.get(p.segment);
      if (!edge) throw new Error(`unknown segment: ${p.segment}`);
      if (!Number.isFinite(p.fraction) || p.fraction < 0 || p.fraction > 1)
        throw new RangeError('a fraction is 0 to 1');
      if (p.fraction === 0) return edge.from;
      if (p.fraction === 1) return edge.to;
      if (!cuts.has(edge.id)) cuts.set(edge.id, new Map());
      const map = cuts.get(edge.id)!;
      if (!map.has(p.fraction)) {
        const id = unique();
        map.set(p.fraction, id);
        allNodes.push({ id, position: pointOnLine(edge.coordinates, p.fraction) });
      }
      return map.get(p.fraction)!;
    };
    const a = resolve(from);
    const b = resolve(to);
    const positionOf = (id: string): LonLat => allNodes.find((n) => n.id === id)!.position;
    const split: GeoRouteSegment[] = [];
    for (const edge of segs.values()) {
      const map = cuts.get(edge.id);
      if (!map) {
        split.push(edge);
        continue;
      }
      const parts: [number, string][] = [[0, edge.from], ...map.entries(), [1, edge.to]];
      parts.sort((x, y) => x[0] - y[0]);
      for (let i = 1; i < parts.length; i++) {
        const prev = parts[i - 1]!;
        const next = parts[i]!;
        const coordinates = sliceLine(edge.coordinates, prev[0], next[0]);
        coordinates[0] = [...positionOf(prev[1])] as LonLat;
        coordinates[coordinates.length - 1] = [...positionOf(next[1])] as LonLat;
        const id = unique();
        originals.set(id, edge.id);
        // the interior cut keeps the recorded length, proportionally
        split.push({
          ...edge,
          id,
          from: prev[1],
          to: next[1],
          coordinates,
          lengthMeters: edge.lengthMeters * (next[0] - prev[0]),
        });
      }
    }
    const result = routeGraph(allNodes, split).route(a, b);
    if (result) for (const step of result.steps) step.segment = originals.get(step.segment) ?? step.segment;
    return result;
  };
  const nearestNode = (position: LonLat, bounds?: GeoBounds): GeoNearestNode | null => {
    assertLonLat(position);
    let best: GeoNearestNode | null = null;
    for (const node of (bounds ? index.query(bounds) : [...byId.values()]) as GeoRouteNode[]) {
      const meters = inverse(position, node.position).meters;
      if (!best || meters < best.meters) best = { node: copy(node), meters };
    }
    return best;
  };
  return {
    route,
    routeAt,
    nearestNode,
    nodesInBounds: (bounds) => (index.query(bounds) as GeoRouteNode[]).map(copy),
    mortonOrder: () => (index.ordered() as GeoRouteNode[]).map((n) => n.id),
  };
}
/**
 * The position a given distance along a route, measured in its own (supplied) metres; within a step its geometry is
 * parametrized by its ellipsoidal arc length. Needs the engines.
 * @param route - a route from a graph
 * @param meters - 0 to route.lengthMeters
 * @returns [longitude, latitude]
 */
function positionAlongRoute(route: GeoRoute, meters: number): LonLat {
  if (!Number.isFinite(meters) || meters < 0 || meters > route.lengthMeters)
    throw new RangeError('a distance on the route');
  let remaining = meters;
  for (const step of route.steps) {
    if (remaining <= step.lengthMeters && step.lengthMeters > 0)
      return pointOnLine(step.coordinates, remaining / step.lengthMeters);
    remaining -= step.lengthMeters;
  }
  const last = route.coordinates[route.coordinates.length - 1]!;
  return [last[0], last[1]];
}
// -- DGEO / TopoJSON decoding ---------------------------------------------------------------------
const DGEO_HEADER = 20;
const DGEO_MAX = 256 * 1024 * 1024;
const MAX_ARCS = 2_000_000;
const utf8 = new TextDecoder('utf-8', { fatal: true });
function readVarint(data: Uint8Array, cursor: { i: number }, end: number): number {
  let value = 0;
  let scale = 1;
  for (let k = 0; k < 5; k++) {
    if (cursor.i >= end) throw new Error('DGEO: truncated varint');
    const b = data[cursor.i++]!;
    value += (b & 127) * scale;
    if (value > 0xffffffff) throw new Error('DGEO: varint overflow');
    if (b < 128) {
      if (k && b === 0) throw new Error('DGEO: noncanonical varint');
      return value;
    }
    scale *= 128;
  }
  throw new Error('DGEO: oversized varint');
}
function checkTopology(t: GeoTopology, arcCount: number): void {
  if (!t || t.type !== 'Topology' || !t.objects || typeof t.objects !== 'object') throw new Error('invalid topology');
  const tr = t.transform;
  if (
    !tr ||
    tr.scale.length !== 2 ||
    tr.translate.length !== 2 ||
    ![...tr.scale, ...tr.translate].every(Number.isFinite)
  )
    throw new Error('a quantized topology needs its transform');
  let refs = 0;
  const indices = (value: unknown, depth: number): void => {
    if (depth > 8 || ++refs > 10_000_000) throw new Error('excessive arc references');
    if (Array.isArray(value)) {
      for (const x of value) indices(x, depth + 1);
      return;
    }
    if (!Number.isInteger(value) || ((value as number) < 0 ? -(value as number) - 1 : (value as number)) >= arcCount)
      throw new Error('arc reference out of range');
  };
  const visit = (o: any, depth: number): void => {
    if (!o || depth > 32) throw new Error('invalid geometry tree');
    if (o.type === 'GeometryCollection') (o.geometries ?? []).forEach((g: unknown) => visit(g, depth + 1));
    else if (['Polygon', 'MultiPolygon', 'LineString', 'MultiLineString'].includes(o.type)) indices(o.arcs, 0);
  };
  Object.values(t.objects).forEach((o) => visit(o, 0));
}
/**
 * Decode a topology: DGEO bytes (DSON metadata + varint arcs) or quantized TopoJSON as UTF-8 JSON - for first-party
 * data (DSON is no sandbox for hostile metadata). Needs the engines.
 * @param bytes - the file's bytes
 * @returns the topology, arcs delta-encoded as in TopoJSON
 */
function decode(bytes: Uint8Array): GeoTopology {
  if (bytes.length > DGEO_MAX) throw new Error('topology size limit exceeded');
  if (bytes[0] !== 68) {
    const t = JSON.parse(utf8.decode(bytes)) as GeoTopology;
    if (!Array.isArray(t.arcs) || t.arcs.length > MAX_ARCS) throw new Error('invalid arcs');
    checkTopology(t, t.arcs.length);
    return t;
  }
  if (bytes.length < DGEO_HEADER || bytes[1] !== 71 || bytes[2] !== 69 || bytes[3] !== 79)
    throw new Error('invalid DGEO header');
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  if (view.getUint16(4, true) !== 1 || view.getUint16(6, true) !== 0) throw new Error('unsupported DGEO version');
  const m = view.getUint32(8, true);
  const count = view.getUint32(12, true);
  const rawLength = view.getUint32(16, true);
  const base = DGEO_HEADER + m + (count + 1) * 4;
  if (m > 64 * 1024 * 1024 || count > MAX_ARCS || base + rawLength !== bytes.length)
    throw new Error('invalid DGEO lengths');
  const meta = ready().DSON.parse(utf8.decode(bytes.subarray(DGEO_HEADER, DGEO_HEADER + m))) as GeoTopology;
  checkTopology(meta, count);
  const offset = (k: number): number => view.getUint32(DGEO_HEADER + m + k * 4, true);
  if (offset(0) !== 0 || offset(count) !== rawLength) throw new Error('invalid DGEO offset table');
  const arcs: number[][][] = [];
  for (let k = 0; k < count; k++) {
    const start = offset(k);
    const stop = offset(k + 1);
    if (stop < start || stop > rawLength) throw new Error('non-monotonic DGEO offsets');
    const cursor = { i: base + start };
    const end = base + stop;
    const arc: number[][] = [];
    while (cursor.i < end) {
      const x = readVarint(bytes, cursor, end);
      const y = readVarint(bytes, cursor, end);
      arc.push([x % 2 ? -(x + 1) / 2 : x / 2, y % 2 ? -(y + 1) / 2 : y / 2]);
    }
    if (arc.length < 2) throw new Error('a DGEO arc needs two points');
    arcs.push(arc);
  }
  return { ...meta, arcs };
}
/**
 * A topology object's geometries as GeoJSON features (TopoJSON's feature(): arcs stitched, shared points dropped,
 * reversed arcs turned).
 * @param topology - a decoded topology
 * @param object - the object's name (world.dgeo: 'countries', 'continents'; a chunk: 'regions')
 * @returns the features
 */
function features(topology: GeoTopology, object: string): GeoRegion[] {
  const o = topology.objects[object] as any;
  if (!o) throw new Error(`the topology has no object "${object}"`);
  const [kx, ky] = topology.transform?.scale ?? [1, 1];
  const [dx, dy] = topology.transform?.translate ?? [0, 0];
  // the arcs, absolute and transformed once
  const arcs = topology.arcs.map((arc) => {
    let x = 0;
    let y = 0;
    return arc.map((p) => {
      x += p[0]!;
      y += p[1]!;
      return [x * kx + dx, y * ky + dy] as LonLat;
    });
  });
  const line = (indices: number[]): LonLat[] => {
    const points: LonLat[] = [];
    for (const i of indices) {
      if (points.length) points.pop();
      const start = points.length;
      for (const p of arcs[i < 0 ? ~i : i]!) points.push([p[0], p[1]]);
      if (i < 0) {
        const tail = points.splice(start).reverse();
        points.push(...tail);
      }
    }
    return points;
  };
  const ring = (indices: number[]): LonLat[] => {
    const points = line(indices);
    while (points.length < 4) points.push([points[0]![0], points[0]![1]]);
    return points;
  };
  const geometry = (g: any): any => {
    switch (g.type) {
      case 'Polygon':
        return { type: 'Polygon', coordinates: g.arcs.map(ring) };
      case 'MultiPolygon':
        return { type: 'MultiPolygon', coordinates: g.arcs.map((p: number[][]) => p.map(ring)) };
      case 'LineString': {
        const points = line(g.arcs);
        if (points.length < 2) points.push([points[0]![0], points[0]![1]]);
        return { type: 'LineString', coordinates: points };
      }
      case 'MultiLineString':
        return { type: 'MultiLineString', coordinates: g.arcs.map(line) };
      case 'Point':
        return { type: 'Point', coordinates: [g.coordinates[0] * kx + dx, g.coordinates[1] * ky + dy] };
      case 'MultiPoint':
        return {
          type: 'MultiPoint',
          coordinates: g.coordinates.map((c: number[]) => [c[0]! * kx + dx, c[1]! * ky + dy]),
        };
      case 'GeometryCollection':
        return { type: 'GeometryCollection', geometries: g.geometries.map(geometry) };
      default:
        return null;
    }
  };
  const feature = (g: any): GeoRegion =>
    ({
      type: 'Feature',
      ...(g.id != null ? { id: g.id } : {}),
      properties: g.properties ?? {},
      geometry: geometry(g),
    }) as GeoRegion;
  return o.type === 'GeometryCollection' ? o.geometries.map(feature) : [feature(o)];
}
/**
 * A region from any GeoJSON polygon feature, its rings turned to D3's winding where needed (each ring by its
 * spherical area: an outer ring encloses the smaller side) - so RFC 7946 and D3-wound input both draw. The input is
 * not changed. Needs the engines.
 * @param region - a Polygon / MultiPolygon feature with properties `{ id, name, level, parent? }`
 * @returns the region, ready for a map
 */
function rewind(region: GeoRegion): GeoRegion {
  const { d3 } = ready();
  const g = region.geometry;
  if (!g || (g.type !== 'Polygon' && g.type !== 'MultiPolygon'))
    throw new Error('a region is a Polygon or MultiPolygon');
  const fix = (polygon: LonLat[][]): LonLat[][] =>
    polygon.map((ring, i) => {
      if (ring.length < 4) throw new Error('a ring needs four positions (closed)');
      ring.forEach(assertLonLat);
      const copy = ring.map((p) => [p[0], p[1]] as LonLat);
      const big = d3.geoArea({ type: 'Polygon', coordinates: [copy] }) > 2 * Math.PI;
      // the outer ring encloses the small side, a hole the large one
      return (i === 0 ? big : !big) ? copy.reverse() : copy;
    });
  const coordinates =
    g.type === 'Polygon' ? fix(g.coordinates as LonLat[][]) : (g.coordinates as LonLat[][][]).map(fix);
  return {
    type: 'Feature',
    properties: { ...region.properties },
    geometry: { type: g.type, coordinates } as GeoGeometry,
  };
}
/** conservative rectangles for the broad phase: split strips, sampled parallels, a 0.1° latitude pad */
function boundsProxy(id: string, bounds: GeoBounds): GeoRegion {
  const [west, south, east, north] = bounds;
  const intervals =
    west > east
      ? [
          [west, 180],
          [-180, east],
        ]
      : [[west, east]];
  const polygons: LonLat[][][] = [];
  const bottom = Math.max(-90, south - 0.1);
  const top = Math.min(90, north + 0.1);
  for (const [w, e] of intervals as [number, number][]) {
    const count = Math.max(1, Math.ceil((e - w) / 60));
    for (let part = 0; part < count; part++) {
      const a = w + ((e - w) * part) / count;
      const b = w + ((e - w) * (part + 1)) / count;
      const steps = Math.max(1, Math.ceil((b - a) / 5));
      const ring: LonLat[] = [
        [a, bottom],
        [a, top],
      ];
      for (let j = 1; j <= steps; j++) ring.push([a + ((b - a) * j) / steps, top]);
      ring.push([b, bottom]);
      for (let j = steps - 1; j >= 0; j--) ring.push([a + ((b - a) * j) / steps, bottom]);
      polygons.push([ring]);
    }
  }
  return {
    type: 'Feature',
    properties: { id, name: id, level: 'country' },
    geometry: { type: 'MultiPolygon', coordinates: polygons },
  };
}
// -- data -------------------------------------------------------------------------------------------
interface GeoManifest {
  version: number;
  initial: string;
  details: Record<string, string>;
  states: Record<string, string>;
  stateDetails?: Record<string, string>;
  bounds?: Record<string, GeoBounds>;
  names?: string;
}
interface Dataset {
  url: string;
  manifest: GeoManifest | null;
  countries: GeoRegion[];
  continents: GeoRegion[];
  /** a GeoJSON source's states (memory - no chunks) */
  memoryStates: GeoRegion[];
  countryIds: Set<string>;
  proxies: Map<string, GeoRegion>;
  byKey: Map<string, GeoRegion>;
  chunks: Map<string, { regions: GeoRegion[]; bytes: number }>;
  loading: Set<string>;
  failed: Map<string, { until: number; attempts: number }>;
  bytes: number;
  maps: Set<HTMLElement>;
  names: Promise<SearchEntry[]> | null;
  /** state key → parent country id (names.json) */
  parents: Map<string, string>;
}
interface SearchEntry {
  key: string;
  name: string;
  level: GeoLevel;
  label: string;
  norm: string;
}
const datasets = new Map<string, Promise<Dataset>>();
const CACHE_BYTES = 24 * 1024 * 1024;
const MAX_LOADS = 4;
let activeLoads = 0;
const loadQueue: { ds: Dataset; path: string }[] = [];
const keyOf = (r: GeoRegion): string => `${r.properties.level}:${r.properties.id}`;
async function fetchBytes(url: string): Promise<Uint8Array> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
  return new Uint8Array(await response.arrayBuffer());
}
function index(ds: Dataset, regions: GeoRegion[]): void {
  for (const r of regions) ds.byKey.set(keyOf(r), r);
}
function openDataset(url: string): Promise<Dataset> {
  let known = datasets.get(url);
  if (!known) {
    known = (async () => {
      await load();
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`);
      const json = (await response.json()) as any;
      const ds: Dataset = {
        url,
        manifest: null,
        countries: [],
        continents: [],
        memoryStates: [],
        countryIds: new Set(),
        proxies: new Map(),
        byKey: new Map(),
        chunks: new Map(),
        loading: new Set(),
        failed: new Map(),
        bytes: 0,
        maps: new Set(),
        names: null,
        parents: new Map(),
      };
      if (json?.type === 'FeatureCollection') {
        // an application's own regions (GeoJSON): every level resident, wound for D3
        const regions = (json.features as GeoRegion[]).map(rewind);
        ds.continents = regions.filter((r) => r.properties.level === 'continent');
        ds.countries = regions.filter((r) => r.properties.level === 'country');
        ds.memoryStates = regions.filter((r) => r.properties.level === 'state');
        for (const s of ds.memoryStates) if (s.properties.parent) ds.parents.set(keyOf(s), s.properties.parent);
      } else {
        const manifest = json as GeoManifest;
        if (manifest.version !== 1 || !manifest.initial || !manifest.details || !manifest.states)
          throw new Error(`${url} is no geo-map manifest (version 1)`);
        ds.manifest = manifest;
        const topology = decode(await fetchBytes(new URL(manifest.initial, url).href));
        ds.countries = features(topology, 'countries');
        ds.continents = features(topology, 'continents');
        for (const [id, b] of Object.entries(manifest.bounds ?? {})) ds.proxies.set(id, boundsProxy(id, b));
      }
      ds.countryIds = new Set(ds.countries.map((r) => r.properties.id));
      index(ds, ds.countries);
      index(ds, ds.continents);
      index(ds, ds.memoryStates);
      return ds;
    })();
    known.catch(() => {
      if (datasets.get(url) === known) datasets.delete(url);
    });
    datasets.set(url, known);
  }
  return known;
}
/** the chunk's regions when cached (touched for the LRU), else undefined - and wanted */
function chunk(ds: Dataset, path: string, wanted: Set<string>): GeoRegion[] | undefined {
  wanted.add(path);
  const entry = ds.chunks.get(path);
  if (entry) {
    ds.chunks.delete(path);
    ds.chunks.set(path, entry);
    return entry.regions;
  }
  if (
    !ds.loading.has(path) &&
    (ds.failed.get(path)?.until ?? 0) <= Date.now() &&
    !loadQueue.some((q) => q.ds === ds && q.path === path)
  )
    loadQueue.push({ ds, path });
  return undefined;
}
/** the maps on the page that read a dataset: a map that left it (an SPA swapped it out) lets go of its chunks - it
 *  registers again on its next render if it comes back */
function liveMaps(ds: Dataset): HTMLElement[] {
  for (const el of ds.maps) if (!el.isConnected) ds.maps.delete(el);
  return [...ds.maps];
}
const wantedBySomeMap = (ds: Dataset, path: string): boolean => liveMaps(ds).some((el) => el._geo?.wanted.has(path));
function pump(): void {
  while (activeLoads < MAX_LOADS && loadQueue.length) {
    // the newest request first: the view the reader looks at now
    const next = loadQueue.pop()!;
    if (next.ds.chunks.has(next.path) || next.ds.loading.has(next.path) || !wantedBySomeMap(next.ds, next.path))
      continue;
    activeLoads++;
    next.ds.loading.add(next.path);
    void loadChunk(next.ds, next.path);
  }
}
async function loadChunk(ds: Dataset, path: string): Promise<void> {
  const url = new URL(path, ds.url).href;
  try {
    const bytes = await fetchBytes(url);
    const regions = features(decode(bytes), 'regions');
    ds.chunks.set(path, { regions, bytes: bytes.length });
    ds.bytes += bytes.length;
    ds.failed.delete(path);
    index(ds, regions);
    // the budget counts encoded bytes (decoded heap is larger); what a map shows is never evicted
    for (const [key, entry] of ds.chunks) {
      if (ds.bytes <= CACHE_BYTES || ds.chunks.size <= 1) break;
      if (!wantedBySomeMap(ds, key)) {
        ds.chunks.delete(key);
        ds.bytes -= entry.bytes;
      }
    }
  } catch (err) {
    const attempts = (ds.failed.get(path)?.attempts ?? 0) + 1;
    ds.failed.set(path, { attempts, until: Date.now() + Math.min(60_000, 1000 * 2 ** attempts) });
    for (const el of liveMaps(ds)) reportError(el, err, url);
  } finally {
    ds.loading.delete(path);
    activeLoads--;
    for (const el of liveMaps(ds)) requestRender(el);
    pump();
  }
}
const normalize = (s: string): string =>
  s
    .normalize('NFD')
    .replace(/[̀-ͯ]/g, '')
    .toLowerCase()
    .trim();
/** the search entries: continents and countries now, every state once names.json arrived */
function searchEntries(ds: Dataset): Promise<SearchEntry[]> {
  if (!ds.names) {
    const resident: SearchEntry[] = [...ds.continents, ...ds.countries, ...ds.memoryStates].map((r) => {
      const parent = r.properties.parent ? ds.byKey.get(`country:${r.properties.parent}`)?.properties.name : '';
      const label = parent ? `${r.properties.name}, ${parent}` : r.properties.name;
      return { key: keyOf(r), name: r.properties.name, level: r.properties.level, label, norm: normalize(label) };
    });
    const namesPath = ds.manifest ? (ds.manifest.names ?? 'names.json') : '';
    const pending: Promise<SearchEntry[]> = !namesPath
      ? Promise.resolve(resident)
      : fetch(new URL(namesPath, ds.url).href)
          .then((r) => (r.ok ? r.json() : { states: [] }))
          .catch(() => ({ states: [] }))
          .then((json: { states?: [string, string, string][] }) => {
            const states = (json.states ?? []).map(([id, name, parent]) => {
              ds.parents.set(`state:${id}`, parent);
              const country = ds.byKey.get(`country:${parent}`)?.properties.name ?? parent;
              const label = `${name}, ${country}`;
              return { key: `state:${id}`, name, level: 'state' as GeoLevel, label, norm: normalize(label) };
            });
            return [...resident, ...states];
          });
    ds.names = pending;
  }
  return ds.names;
}
function rank(entries: SearchEntry[], query: string, limit: number): GeoSearchHit[] {
  const q = normalize(query);
  if (!q) return [];
  const order: GeoLevel[] = ['continent', 'country', 'state'];
  const scored: [number, SearchEntry][] = [];
  for (const e of entries) {
    const name = normalize(e.name);
    const score =
      name === q || e.norm === q
        ? 0
        : name.startsWith(q)
          ? 1
          : name.split(/[\s-]+/).some((w) => w.startsWith(q))
            ? 2
            : e.norm.includes(q)
              ? 3
              : -1;
    if (score >= 0) scored.push([score * 10 + order.indexOf(e.level), e]);
  }
  scored.sort((a, b) => a[0] - b[0] || a[1].label.localeCompare(b[1].label));
  return scored.slice(0, limit).map(([, e]) => ({ key: e.key, name: e.name, level: e.level, label: e.label }));
}
// -- projection + framing ----------------------------------------------------------------------------
const PROJECTIONS: GeoProjectionName[] = ['equalEarth', 'equalArea', 'mercator', 'equirectangular', 'orthographic'];
const LEVELS: (GeoLevel | 'auto')[] = ['auto', 'continent', 'country', 'state'];
const PALETTES: GeoPalette[] = ['atlas', 'ocean', 'forest', 'sunset', 'heat', 'mono', 'chart'];
const BORDERS: GeoBorder[] = ['natural', 'cased', 'dashed', 'none'];
const HOME: LonLat = [0, 12];
interface Extent {
  x0: number;
  y0: number;
  x1: number;
  y1: number;
}
function makeProjection(name: GeoProjectionName, box: Extent, center: LonLat, zoom: number, clip = true): any {
  const { d3 } = ready();
  const make = {
    equalEarth: d3.geoEqualEarth,
    // Lambert's azimuthal equal-area, turned to the camera: every area true, like Equal Earth, and the shapes true
    // at the center too - a framed country is drawn at its real size and in its real shape, wherever it lies
    equalArea: d3.geoAzimuthalEqualArea,
    mercator: d3.geoMercator,
    equirectangular: d3.geoEquirectangular,
    orthographic: d3.geoOrthographic,
  }[name];
  const p = make().precision(0.35);
  p.fitExtent(
    [
      [box.x0 + 12, box.y0 + 12],
      [Math.max(box.x0 + 13, box.x1 - 12), Math.max(box.y0 + 13, box.y1 - 12)],
    ],
    { type: 'Sphere' },
  );
  const scale = p.scale();
  if (name === 'orthographic') p.rotate([-center[0], -center[1], 0]).clipAngle(90);
  else if (name === 'equalArea') p.rotate([-center[0], -center[1], 0]);
  else p.rotate([-center[0], 0, 0]).center([0, clamp(center[1], -85, 85)]);
  p.scale(scale * zoom).translate([(box.x0 + box.x1) / 2, (box.y0 + box.y1) / 2]);
  // a flat projection zoomed in never shows the empty band past a pole (the card's background over the Arctic, as a
  // wide Europe frame did): once the map is taller than its box the view stops at the pole's edge; zoomed out, the
  // whole outline stays where the camera puts it. Only the drawn view - frame() measures unclamped (clip false)
  if (clip && name !== 'orthographic' && name !== 'equalArea') {
    const [[, top], [, bottom]] = d3.geoPath(p).bounds({ type: 'Sphere' }) as [[number, number], [number, number]];
    const dy =
      bottom - top <= box.y1 - box.y0 ? 0 : top > box.y0 ? box.y0 - top : bottom < box.y1 ? box.y1 - bottom : 0;
    if (dy) p.translate([(box.x0 + box.x1) / 2, (box.y0 + box.y1) / 2 + dy]);
  }
  if (clip)
    p.clipExtent([
      [box.x0, box.y0],
      [box.x1, box.y1],
    ]);
  return p;
}
/** the built-in focus frames - where a continent's outline (Russia in Europe, the Aleutians) frames poorly */
const VIEWS: Record<string, GeoBounds> = {
  'continent:Europe': [-25, 34, 45, 71],
  'continent:North America': [-168, 7, -20, 72],
  'continent:Oceania': [110, -48, -175, 5],
};
/**
 * The part of a region a focus frames: its largest polygon plus every polygon near the growing cluster - France
 * frames the mainland and Corsica, not French Guiana; an archipelago chains together.
 */
function mainBody(region: GeoRegion): GeoRegion {
  const { d3 } = ready();
  const g = region.geometry;
  if (g.type !== 'MultiPolygon' || g.coordinates.length < 2) return region;
  const parts = (g.coordinates as LonLat[][][]).map((coordinates) => {
    const poly = { type: 'Polygon', coordinates };
    return { coordinates, area: d3.geoArea(poly) as number, centroid: d3.geoCentroid(poly) as LonLat };
  });
  parts.sort((a, b) => b.area - a.area);
  const included = [parts[0]!];
  let rest = parts.slice(1);
  for (let changed = true; changed;) {
    changed = false;
    const area = included.reduce((s, p) => s + p.area, 0);
    const center = d3.geoCentroid({ type: 'MultiPolygon', coordinates: included.map((p) => p.coordinates) }) as LonLat;
    const radius = Math.sqrt(area / Math.PI);
    const next = rest.filter((p) => {
      const near = d3.geoDistance(center, p.centroid) <= 2 * (radius + Math.sqrt(p.area / Math.PI)) + 0.02;
      if (near) {
        included.push(p);
        changed = true;
      }
      return !near;
    });
    rest = next;
  }
  return { ...region, geometry: { type: 'MultiPolygon', coordinates: included.map((p) => p.coordinates) } };
}
/** the camera that frames a region (or bounds) inside a box */
function frame(target: GeoRegion, box: Extent, projection: GeoProjectionName): { center: LonLat; zoom: number } {
  const { d3 } = ready();
  const center = d3.geoCentroid(target) as LonLat;
  const c: LonLat = [wrapLon(center[0]), clamp(center[1], -85, 85)];
  const p = makeProjection(projection, box, c, 1, false);
  const b = d3.geoPath(p).bounds(target);
  const w = b[1][0] - b[0][0];
  const h = b[1][1] - b[0][1];
  const pad = 28;
  // the target's size at zoom 1 may be far below a pixel (an S2 cell from level 8, a microstate): only a point (no
  // size) is guarded - a 1px floor capped every such frame at the box's width (390x on a 28rem map)
  const zoom = clampZoom(
    Math.min((box.x1 - box.x0 - pad * 2) / Math.max(1e-9, w), (box.y1 - box.y0 - pad * 2) / Math.max(1e-9, h)),
  );
  return { center: c, zoom: Number.isFinite(zoom) ? zoom : 1 };
}
// -- the map instance ---------------------------------------------------------------------------------
interface GeoView {
  projection: GeoProjectionName;
  level: GeoLevel | 'auto';
  palette: GeoPalette;
  border: GeoBorder;
  graticule: boolean;
  callouts: boolean;
  selected: string[];
  focus: string | null;
  center: LonLat;
  zoom: number;
}
interface GeoInstance {
  view: GeoView;
  ds: Dataset | null;
  status: 'loading' | 'ready' | 'error';
  ready: Promise<void>;
  svg: SVGSVGElement;
  layers: Record<string, SVGGElement>;
  tooltip: HTMLOutputElement;
  hatch: string;
  width: number;
  height: number;
  projection: any;
  box: Extent;
  frameId: number;
  flight: { id: number; target: { center: LonLat; zoom: number }; done: () => void } | null;
  home: { center: LonLat; zoom: number };
  /** the selection and focus the markup declared - what 'selected' / 'focused' fall back to without a config */
  authored: { focus: string | null; selected: string[] };
  restTimer: number;
  pendingFit: string | null;
  pointers: Map<number, [number, number]>;
  moved: boolean;
  drawn: Map<string, GeoRegion>;
  anchors: Map<string, [number, number]>;
  hover: string | null;
  roving: string | null;
  layersData: Required<GeoLayers>;
  cellRegions: GeoRegion[];
  wanted: Set<string>;
  stats: GeoRenderStats;
  abort: AbortController;
  resize: ResizeObserver;
  attrs: MutationObserver;
  lastView: string;
}
const NS = 'http://www.w3.org/2000/svg';
let nextId = 0;
function svgEl<K extends keyof SVGElementTagNameMap>(
  tag: K,
  attrs: Record<string, string> = {},
): SVGElementTagNameMap[K] {
  const el = document.createElementNS(NS, tag);
  for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
  return el;
}
/** a layer's children replaced - through df$ (AGENTS.md "DOM through df$") */
function setChildren(parent: Element, nodes: Element[]): void {
  dfDollar(parent).empty().append(nodes);
}
/** the one path of a layer (the sphere, the graticule): made once, its d updated */
function layerPath(layer: SVGGElement, d: string): void {
  let p = layer.firstElementChild as SVGPathElement | null;
  if (!p) {
    p = svgEl('path');
    dfDollar(layer).append(p);
  }
  if (p.getAttribute('d') !== d) p.setAttribute('d', d);
}
function hash(s: string): number {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
  return h >>> 0;
}
const reducedMotion = (): boolean =>
  typeof globalThis.matchMedia === 'function' && globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches;
/** the keys of a selection config: an array, or a string separated by spaces or commas */
function keyList(value: unknown): string[] {
  const list = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(/[\s,]+/) : [];
  return [...new Set(list.map((k) => k.trim()).filter(Boolean))];
}
function parseCenter(value: string | undefined): LonLat | null {
  if (!value) return null;
  const parts = value.split(',').map((v) => Number(v.trim()));
  if (parts.length !== 2 || !parts.every(Number.isFinite)) return null;
  return [wrapLon(parts[0]!), clamp(parts[1]!, -85, 85)];
}
/** the view the element's attributes declare (the camera from the instance - it is no markup) */
function viewFromAttributes(el: HTMLElement, camera: { center: LonLat; zoom: number }): GeoView {
  const d = el.dataset;
  const pick = <T extends string>(value: string | undefined, allowed: T[], fallback: T): T =>
    allowed.includes(value as T) ? (value as T) : fallback;
  return {
    projection: pick(d.projection, PROJECTIONS, 'equalEarth'),
    level: pick(d.level, LEVELS, 'country'),
    palette: pick(d.palette, PALETTES, 'atlas'),
    border: pick(d.border, BORDERS, 'natural'),
    graticule: d.graticule !== 'false',
    callouts: d.callouts !== undefined && d.callouts !== 'false',
    selected: keyList(d.selected ?? ''),
    focus: d.focus || null,
    center: camera.center,
    zoom: camera.zoom,
  };
}
const stateNameOf = (v: Pick<GeoView, 'focus' | 'selected'>): string =>
  v.focus ? 'focused' : v.selected.length ? 'selected' : 'default';
/** a map's state as getState() reports it */
function configOf(geo: GeoInstance): Required<Omit<GeoMapConfig, 'selected'>> & { selected: string[] } {
  const v = geo.view;
  const camera = geo.flight?.target ?? { center: v.center, zoom: v.zoom };
  return {
    projection: v.projection,
    level: v.level,
    palette: v.palette,
    border: v.border,
    graticule: v.graticule,
    callouts: v.callouts,
    selected: [...v.selected],
    focus: v.focus,
    center: [camera.center[0], camera.center[1]],
    zoom: camera.zoom,
  };
}
function requestRender(el: HTMLElement): void {
  const geo = el._geo as GeoInstance | undefined;
  if (!geo || geo.frameId) return;
  geo.frameId = requestAnimationFrame(() => {
    geo.frameId = 0;
    renderMap(el);
  });
}
function reportError(el: HTMLElement, err: unknown, url: string): void {
  const message = err instanceof Error ? err.message : String(err);
  // a chunk or the data could not be loaded - the map keeps what it has
  el.dispatchEvent(new CustomEvent<GeoMapErrorDetail>('geo-map-error', { detail: { message, url }, bubbles: true }));
}
/** the level drawn: the configured one ('auto' by zoom) */
function levelFor(view: GeoView): GeoLevel {
  return view.level === 'auto' ? (view.zoom >= 8 ? 'state' : view.zoom >= 2 ? 'country' : 'continent') : view.level;
}
/** the token of the S2 cell in focus (s2:<token>), or null - also for a focus that names no valid cell */
function cellFocus(view: GeoView): string | null {
  if (!view.focus?.startsWith('s2:')) return null;
  try {
    return s2Token(view.focus.slice(3));
  } catch {
    return null;
  }
}
/** the cells drawn: the focused cell's four children - the S2 hierarchy one level down - else the layer's cells */
function cellsDrawn(geo: GeoInstance): GeoRegion[] {
  const token = cellFocus(geo.view);
  return token ? s2Children(token).map(s2Polygon) : geo.cellRegions;
}
/** the regions drawn - and, around a focus, the dimmed context */
function selectRegions(geo: GeoInstance, path: any): { regions: GeoRegion[]; context: GeoRegion[] } {
  const ds = geo.ds!;
  const v = geo.view;
  const m = ds.manifest;
  const zoom = v.zoom;
  geo.wanted = new Set();
  const visible = (r: GeoRegion): boolean => !!path(zoom >= 3 ? (ds.proxies.get(r.properties.id) ?? r) : r);
  const statesOf = (id: string): GeoRegion[] | undefined => {
    if (!m) return ds.memoryStates.filter((s) => s.properties.parent === id);
    const p = zoom >= 24 ? (m.stateDetails?.[id] ?? m.states[id]) : m.states[id];
    return p ? chunk(ds, p, geo.wanted) : [];
  };
  const countryShape = (r: GeoRegion): GeoRegion[] => {
    const p = zoom >= 3 && m ? m.details[r.properties.id] : undefined;
    return (p && chunk(ds, p, geo.wanted)) || [r];
  };
  const focus = v.focus ? ds.byKey.get(v.focus) : undefined;
  const focusKey = v.focus;
  // an S2 cell in focus frames a place, not a region: the regions are drawn as without one
  if (focusKey && !focusKey.startsWith('s2:')) {
    const [level, ...rest] = focusKey.split(':');
    const id = rest.join(':');
    const countries = ds.countries.filter(visible);
    if (level === 'continent') {
      const inside = countries.filter((c) => c.properties.continent === id);
      return {
        regions: inside.flatMap(countryShape),
        context: countries.filter((c) => c.properties.continent !== id).flatMap(countryShape),
      };
    }
    const countryId = level === 'country' ? id : (focus?.properties.parent ?? ds.parents.get(focusKey) ?? '');
    const states = countryId ? statesOf(countryId) : undefined;
    const home = ds.byKey.get(`country:${countryId}`);
    return {
      regions: states?.length ? states : home ? countryShape(home) : [],
      context: countries.filter((c) => c.properties.id !== countryId).flatMap(countryShape),
    };
  }
  const level = levelFor(v);
  if (level === 'continent') return { regions: ds.continents, context: [] };
  const regions: GeoRegion[] = [];
  for (const c of ds.countries) {
    if (!visible(c)) continue;
    if (level === 'state' && zoom >= 3) {
      const states = statesOf(c.properties.id);
      regions.push(...(states && states.length ? states : countryShape(c)));
    } else regions.push(...countryShape(c));
  }
  // parents with states but no 50m country (Gibraltar, Akrotiri...): their states, by the bounds index
  if (level === 'state' && zoom >= 3 && m)
    for (const id of Object.keys(m.states)) {
      if (ds.countryIds.has(id)) continue;
      const proxy = ds.proxies.get(id);
      if (proxy && path(proxy)) regions.push(...(statesOf(id) ?? []));
    }
  return { regions, context: [] };
}
/** a region's label point: the projected centroid of its largest polygon, moved inside when it falls out */
function anchorOf(path: any, projection: any, region: GeoRegion): [number, number] | null {
  const { d3 } = ready();
  const g = region.geometry;
  let largest: any = g;
  if (g.type === 'MultiPolygon') {
    let best = -1;
    for (const coordinates of g.coordinates as LonLat[][][]) {
      const poly = { type: 'Polygon', coordinates };
      const a = path.area(poly);
      if (a > best) {
        best = a;
        largest = poly;
      }
    }
  }
  const c = path.centroid(largest) as [number, number];
  if (!c.every(Number.isFinite)) return null;
  const inside = (pt: [number, number]): boolean => {
    const ll = projection.invert?.(pt);
    return !!ll && d3.geoContains(largest, ll);
  };
  if (inside(c)) return c;
  const [[x0, y0], [x1, y1]] = path.bounds(largest);
  let best: [number, number] | null = null;
  let dist = Infinity;
  for (let i = 1; i < 8; i++)
    for (let j = 1; j < 8; j++) {
      const pt: [number, number] = [x0 + ((x1 - x0) * i) / 8, y0 + ((y1 - y0) * j) / 8];
      const d = Math.hypot(pt[0] - c[0], pt[1] - c[1]);
      if (d < dist && inside(pt)) {
        best = pt;
        dist = d;
      }
    }
  return best ?? c;
}
/** a length custom property of the map in px (px, rem or em), else the fallback */
function cssPx(el: HTMLElement, name: string, fallback: number): number {
  const raw = getComputedStyle(el).getPropertyValue(name).trim();
  const n = parseFloat(raw);
  if (!Number.isFinite(n)) return fallback;
  if (raw.endsWith('rem')) return n * parseFloat(getComputedStyle(document.documentElement).fontSize || '16');
  if (raw.endsWith('em')) return n * parseFloat(getComputedStyle(el).fontSize || '16');
  return n;
}
/** the callouts' gutter width in px (--geo-map-callout-width, default 10rem) */
const gutterOf = (el: HTMLElement): number => cssPx(el, '--geo-map-callout-width', 160);
let measure: OffscreenCanvasRenderingContext2D | null | undefined;
/** a name's width in px in a font - canvas text metrics, an estimate where there is no canvas */
function textWidth(text: string, font: string, size: number): number {
  if (measure === undefined)
    measure = typeof OffscreenCanvas === 'undefined' ? null : new OffscreenCanvas(1, 1).getContext('2d');
  if (!measure) return text.length * size * 0.58;
  measure.font = font;
  return measure.measureText(text).width;
}
function renderMap(el: HTMLElement): void {
  const geo = el._geo as GeoInstance | undefined;
  if (!geo || geo.status !== 'ready' || !geo.ds) return;
  const { d3 } = ready();
  const start = performance.now();
  const v = geo.view;
  const W = geo.width;
  const H = geo.height;
  const gutter = v.callouts ? Math.min(gutterOf(el), W / 3) : 0;
  geo.box = { x0: gutter, y0: 0, x1: W - gutter, y1: H };
  geo.svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
  geo.projection = makeProjection(v.projection, geo.box, v.center, v.zoom);
  // the camera follows a view stopped at a pole (makeProjection), so a pan toward it does not wind up unseen
  if (!geo.flight && v.projection !== 'orthographic' && v.projection !== 'equalArea') {
    const seen = geo.projection.invert?.([(geo.box.x0 + geo.box.x1) / 2, (geo.box.y0 + geo.box.y1) / 2]);
    if (seen && Number.isFinite(seen[1]) && Math.abs(seen[1] - v.center[1]) > 1e-6)
      v.center = [v.center[0], clamp(seen[1], -85, 85)];
  }
  const path = d3.geoPath(geo.projection);
  const { regions, context } = selectRegions(geo, path);
  geo.ds.maps.add(el);
  pump();
  const L = geo.layers;
  layerPath(L.sphere!, path({ type: 'Sphere' }) ?? '');
  layerPath(L.graticule!, v.graticule ? (path(d3.geoGraticule10()) ?? '') : '');
  const selected = new Set(v.selected);
  const styles = geo.layersData.styles;
  geo.drawn = new Map();
  let visibleRegions = 0;
  let contextRegions = 0;
  let visibleCells = 0;
  const reconcile = (layer: SVGGElement, items: GeoRegion[], kind: 'region' | 'context' | 'cell'): void => {
    const existing = new Map<string, SVGPathElement>();
    for (const child of [...layer.children])
      existing.set(child.getAttribute('data-key') ?? '', child as SVGPathElement);
    const keep = new Set<string>();
    for (const region of items) {
      const key = kind === 'cell' ? `s2:${region.properties.id}` : keyOf(region);
      if (keep.has(key)) continue;
      const d = path(region);
      if (!d) continue;
      keep.add(key);
      if (kind === 'context') contextRegions++;
      else {
        geo.drawn.set(key, region);
        if (kind === 'cell') visibleCells++;
        else visibleRegions++;
      }
      let p = existing.get(key);
      if (!p) {
        p = svgEl('path', {
          'data-key': key,
          class: kind === 'cell' ? 'geo-map-region geo-map-cell' : 'geo-map-region',
        });
        if (kind !== 'context') {
          p.setAttribute('role', 'button');
          p.setAttribute('tabindex', '-1');
          p.setAttribute(
            'aria-label',
            `${region.properties.name} (${kind === 'cell' ? 'S2 cell' : region.properties.level})`,
          );
        }
        layer.append(p);
      }
      if (p.getAttribute('d') !== d) p.setAttribute('d', d);
      if (kind !== 'cell') p.setAttribute('data-tone', String(hash(region.properties.id) % 7));
      if (kind !== 'context') p.setAttribute('aria-pressed', selected.has(key) ? 'true' : 'false');
      const style = styles[key];
      p.removeAttribute('style');
      if (style) {
        if (style.fill !== undefined) p.style.fill = style.fill;
        if (style.stroke !== undefined) p.style.stroke = style.stroke;
        if (style.strokeWidth !== undefined) p.style.strokeWidth = String(style.strokeWidth);
        if (style.opacity !== undefined) p.style.opacity = String(style.opacity);
        if (style.dash !== undefined) p.style.strokeDasharray = style.dash;
      }
    }
    for (const [key, p] of existing) if (!keep.has(key)) p.remove();
  };
  reconcile(L.context!, context, 'context');
  reconcile(L.regions!, regions, 'region');
  reconcile(L.cells!, cellsDrawn(geo), 'cell');
  // the roving tab stop: the selected region in view, else the first drawn
  const stops = [...L.regions!.children, ...L.cells!.children] as SVGPathElement[];
  const stop =
    stops.find((p) => p.getAttribute('data-key') === geo.roving) ??
    stops.find((p) => p.getAttribute('aria-pressed') === 'true') ??
    stops[0];
  geo.roving = stop?.getAttribute('data-key') ?? null;
  for (const p of stops) {
    const t = p === stop ? '0' : '-1';
    if (p.getAttribute('tabindex') !== t) p.setAttribute('tabindex', t);
  }
  // lines
  setChildren(
    L.lines!,
    geo.layersData.lines.flatMap((line) => {
      let coordinates = line.coordinates;
      if (!coordinates && line.from && line.to) coordinates = geodesicPath(line.from, line.to);
      const d = coordinates ? path({ type: 'LineString', coordinates }) : null;
      if (!d) return [];
      const p = svgEl('path', { d, class: 'geo-map-line', 'data-line': line.id });
      if (line.color) p.style.stroke = line.color;
      if (line.width !== undefined) p.style.strokeWidth = String(line.width);
      if (line.dash) p.style.strokeDasharray = line.dash;
      return [p];
    }),
  );
  // markers
  let visibleMarkers = 0;
  setChildren(
    L.markers!,
    geo.layersData.markers.flatMap((marker) => {
      const pos = resolvePosition(marker.position);
      if (!path({ type: 'Point', coordinates: pos })) return [];
      const xy = geo.projection(pos) as [number, number];
      visibleMarkers++;
      const g = svgEl('g', {
        class: 'geo-map-marker',
        transform: `translate(${xy[0].toFixed(2)},${xy[1].toFixed(2)})`,
        'data-marker': marker.id,
        'data-shape': marker.shape ?? 'circle',
        role: 'img',
        'aria-label': marker.label ?? marker.id,
      });
      const shape =
        marker.shape === 'diamond'
          ? svgEl('path', { d: 'M0,-7L7,0L0,7L-7,0Z' })
          : marker.shape === 'square'
            ? svgEl('rect', { x: '-5', y: '-5', width: '10', height: '10', rx: '2' })
            : marker.shape === 'pin'
              ? svgEl('path', { d: 'M0,1C-18,-14 -5,-22 0,-22C5,-22 18,-14 0,1Z' })
              : svgEl('circle', { r: '5' });
      if (marker.color) shape.style.fill = marker.color;
      g.append(shape);
      if (marker.label) {
        const text = svgEl('text', { x: '10', y: marker.shape === 'pin' ? '-12' : '-6' });
        text.textContent = marker.label;
        g.append(text);
      }
      return [g];
    }),
  );
  // anchors (callouts + keyboard movement) for the regions drawn
  geo.anchors = new Map();
  const callable = [...geo.drawn].filter(([k]) => !k.startsWith('s2:'));
  if (callable.length <= 400)
    for (const [key, region] of callable) {
      const a = anchorOf(path, geo.projection, region);
      if (a && a[0] >= geo.box.x0 && a[0] <= geo.box.x1 && a[1] >= 0 && a[1] <= H) geo.anchors.set(key, a);
    }
  let calloutCount = 0;
  if (v.callouts) calloutCount = drawCallouts(el, geo, W, H, gutter, path);
  else setChildren(L.callouts!, []);
  drawLift(geo);
  geo.stats = {
    visibleRegions,
    contextRegions,
    visibleCells,
    visibleMarkers,
    callouts: calloutCount,
    renderMs: performance.now() - start,
    cachedChunks: geo.ds.chunks.size,
    loadedBytes: geo.ds.bytes,
    // this map's chunks still on their way (a failed one waits for its retry)
    loading: [...geo.wanted].filter((p) => !geo.ds!.chunks.has(p) && !geo.ds!.failed.has(p)).length,
  };
  if (el.hasAttribute('aria-busy') !== geo.stats.loading > 0)
    dfDollar(el).attr('aria-busy', geo.stats.loading ? 'true' : null);
  // the pending fit of a focused state: its chunk has arrived
  if (geo.pendingFit && geo.ds.byKey.has(geo.pendingFit) && !geo.flight) {
    const key = geo.pendingFit;
    geo.pendingFit = null;
    flyTo(el, targetFor(geo, key), false);
  }
  writeOutputs(el);
  // a render finished: what it drew and what is still loading
  el.dispatchEvent(new CustomEvent<GeoRenderStats>('geo-map-render', { detail: { ...geo.stats } }));
}
function drawCallouts(el: HTMLElement, geo: GeoInstance, W: number, H: number, gutter: number, path: any): number {
  const layer = geo.layers.callouts!;
  const focus = geo.view.focus;
  const entries = [...geo.anchors].map(([key, anchor]) => {
    const region = geo.drawn.get(key)!;
    return { key, anchor, name: region.properties.name, area: path.area(region) as number };
  });
  // the rows and the names follow the label's size (--geo-map-callout-size, default 0.875rem)
  const size = cssPx(el, '--geo-map-callout-size', 14);
  const row = Math.max(18, Math.round(size * 1.6));
  // measured in the hover weight, so a highlighted name still fits
  const font = `600 ${size}px ${getComputedStyle(el).fontFamily}`;
  const room = gutter - 12;
  const fit = (name: string): string => {
    if (textWidth(name, font, size) <= room) return name;
    let n = name.length - 1;
    while (n > 1 && textWidth(`${name.slice(0, n)}…`, font, size) > room) n--;
    return `${name.slice(0, n).trimEnd()}…`;
  };
  const capacity = Math.max(1, Math.floor((H - 16) / row));
  const mid = (geo.box.x0 + geo.box.x1) / 2;
  // the largest regions when there are more than the gutters hold; then west to the left gutter, east to the right -
  // a full side gives its regions nearest the middle to the other one, so lines do not cross the map
  const shown = entries.sort((a, b) => b.area - a.area).slice(0, capacity * 2);
  shown.sort((a, b) => a.anchor[0] - b.anchor[0]);
  const west = shown.filter((e) => e.anchor[0] < mid).length;
  const split = clamp(west, shown.length - capacity, capacity);
  const sides: Record<'left' | 'right', typeof entries> = { left: shown.slice(0, split), right: shown.slice(split) };
  const groups: SVGGElement[] = [];
  for (const side of ['left', 'right'] as const) {
    const list = sides[side].sort((a, b) => a.anchor[1] - b.anchor[1]);
    const top = Math.round(size) + row;
    const bottom = H - 8;
    const ys = list.map((e) => clamp(e.anchor[1], top, bottom));
    for (let i = 1; i < ys.length; i++) ys[i] = Math.max(ys[i]!, ys[i - 1]! + row);
    if (ys.length && ys[ys.length - 1]! > bottom) {
      ys[ys.length - 1] = bottom;
      for (let i = ys.length - 2; i >= 0; i--) ys[i] = Math.min(ys[i]!, ys[i + 1]! - row);
    }
    list.forEach((e, i) => {
      const y = ys[i]!;
      const edge = side === 'left' ? gutter : W - gutter;
      const outer = side === 'left' ? 6 : W - 6;
      const g = svgEl('g', { class: 'geo-map-callout', 'data-key': e.key, 'data-side': side });
      if (focus && e.key === focus) g.setAttribute('data-focus', '');
      g.append(
        svgEl('polyline', {
          class: 'geo-map-callout-line',
          points: `${e.anchor[0].toFixed(1)},${e.anchor[1].toFixed(1)} ${edge.toFixed(1)},${y.toFixed(1)} ${outer.toFixed(1)},${y.toFixed(1)}`,
        }),
        svgEl('circle', {
          class: 'geo-map-callout-dot',
          cx: e.anchor[0].toFixed(1),
          cy: e.anchor[1].toFixed(1),
          r: '2.5',
        }),
      );
      const text = svgEl('text', {
        class: 'geo-map-callout-label',
        x: String(outer),
        y: (y - Math.max(4, Math.round(size * 0.3))).toFixed(1),
        'text-anchor': side === 'left' ? 'start' : 'end',
      });
      text.textContent = fit(e.name);
      g.append(text);
      if (geo.view.selected.includes(e.key)) g.setAttribute('data-selected', '');
      groups.push(g);
    });
  }
  setChildren(layer, groups);
  return groups.length;
}
/** the lifted copies: the hovered region, and the selection when data-highlight="lift" */
function drawLift(geo: GeoInstance): void {
  const el = dfDollar(geo.svg).closest('.geo-map').get(0) as HTMLElement | undefined;
  const keys = new Set<string>();
  if (el?.dataset.highlight === 'lift') for (const k of geo.view.selected) keys.add(k);
  if (geo.hover) keys.add(geo.hover);
  const existing = new Map<string, SVGPathElement>();
  for (const c of [...geo.layers.lift!.children]) existing.set(c.getAttribute('data-key') ?? '', c as SVGPathElement);
  const keep: SVGPathElement[] = [];
  for (const key of keys) {
    const source = (dfDollar(geo.svg)
      .find(`.geo-map-regions [data-key="${CSS.escape(key)}"], .geo-map-cells [data-key="${CSS.escape(key)}"]`)
      .get(0) ?? null) as unknown as SVGPathElement | null;
    if (!source) continue;
    let copy = existing.get(key);
    if (!copy) copy = svgEl('path', { 'data-key': key, class: 'geo-map-lifted' });
    copy.setAttribute('d', source.getAttribute('d') ?? '');
    for (const a of ['data-tone', 'style', 'aria-pressed']) {
      const value = source.getAttribute(a);
      if (value === null) copy.removeAttribute(a);
      else copy.setAttribute(a, value);
    }
    if (key.startsWith('s2:')) copy.classList.add('geo-map-cell');
    keep.push(copy);
  }
  setChildren(geo.layers.lift!, keep);
}
/** the camera a key frames: a built-in or configured view, else the region's main body */
function targetFor(geo: GeoInstance, key: string): { center: LonLat; zoom: number } {
  const box = geo.box.x1 > geo.box.x0 ? geo.box : { x0: 0, y0: 0, x1: geo.width, y1: geo.height };
  const bounds = settings.views[key] ?? VIEWS[key];
  if (bounds) return frame(boundsProxy(key, bounds), box, geo.view.projection);
  const cell = cellFocus({ ...geo.view, focus: key });
  if (cell) return frame(s2Polygon(cell), box, geo.view.projection);
  const region = geo.ds?.byKey.get(key);
  if (region)
    return frame(region.properties.level === 'continent' ? region : mainBody(region), box, geo.view.projection);
  // a state whose chunk is not here yet: its country first, the state once it arrives
  const parent = geo.ds?.parents.get(key);
  const home = parent ? geo.ds?.byKey.get(`country:${parent}`) : undefined;
  geo.pendingFit = key;
  return home ? frame(mainBody(home), box, geo.view.projection) : { center: geo.view.center, zoom: geo.view.zoom };
}
/** move the camera - a flight (great-circle center, logarithmic zoom) unless motion is reduced or `animate` is false */
function flyTo(el: HTMLElement, target: { center: LonLat; zoom: number }, animate = true): Promise<void> {
  const geo = el._geo as GeoInstance;
  const { d3 } = ready();
  cancelFlight(geo);
  const from = { center: geo.view.center, zoom: geo.view.zoom };
  const to = {
    center: [wrapLon(target.center[0]), clamp(target.center[1], -85, 85)] as LonLat,
    zoom: clampZoom(target.zoom),
  };
  if (!animate || reducedMotion() || !geo.width) {
    geo.view.center = to.center;
    geo.view.zoom = to.zoom;
    requestRender(el);
    announceView(el);
    return Promise.resolve();
  }
  const interpolate = d3.geoInterpolate(from.center, to.center);
  const distance = d3.geoDistance(from.center, to.center);
  const ms = clamp(420 + distance * 260 + Math.abs(Math.log2(to.zoom / from.zoom)) * 90, 450, 1400);
  let t0 = -1;
  return new Promise<void>((resolve) => {
    const step = (now: number): void => {
      // timed by the frames: the first frame's timestamp may come before performance.now() read at the start
      if (t0 < 0) t0 = now;
      const t = Math.min(1, (now - t0) / ms);
      const e = t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
      const c = interpolate(e) as LonLat;
      geo.view.center = [wrapLon(c[0]), clamp(c[1], -85, 85)];
      geo.view.zoom = from.zoom * (to.zoom / from.zoom) ** e;
      renderMap(el);
      if (t < 1 && geo.flight) {
        geo.flight.id = requestAnimationFrame(step);
        return;
      }
      geo.view.center = to.center;
      geo.view.zoom = to.zoom;
      geo.flight = null;
      renderMap(el);
      announceView(el);
      resolve();
    };
    geo.flight = { id: requestAnimationFrame(step), target: to, done: resolve };
  });
}
/** stop a flight where it is (a pan, a zoom, another flight took over); its promise resolves */
function cancelFlight(geo: GeoInstance): void {
  if (!geo.flight) return;
  cancelAnimationFrame(geo.flight.id);
  const done = geo.flight.done;
  geo.flight = null;
  done();
}
function announceView(el: HTMLElement): void {
  const geo = el._geo as GeoInstance;
  const v = geo.view;
  const key = `${v.center[0].toFixed(4)},${v.center[1].toFixed(4)},${v.zoom.toFixed(4)}`;
  if (key === geo.lastView) return;
  geo.lastView = key;
  // the camera came to rest (a pan, a zoom, a flight)
  el.dispatchEvent(
    new CustomEvent<GeoMapViewDetail>('geo-map-view', {
      detail: { center: [v.center[0], v.center[1]], zoom: v.zoom, level: levelFor(v) },
      bubbles: true,
    }),
  );
}
// -- markup + state ---------------------------------------------------------------------------------
/** set an attribute only when it says something other than the element already says (absent = the default) */
function reflect(el: HTMLElement, name: string, value: string | null, fallback: string | null): void {
  const current = el.getAttribute(name);
  if ((current ?? fallback) === (value ?? fallback)) return;
  dfDollar(el).attr(name, value === fallback && current === null ? null : value);
}
/**
 * The markup of a state, on the live element and on render()'s detached copy: the view options, the selection
 * and the focus as data-* attributes. The SVG, the tooltip and the camera are the runtime's (no markup).
 */
function applyMarkup(el: HTMLElement, name: string, config: GeoMapConfig): void {
  if (config.projection !== undefined) reflect(el, 'data-projection', config.projection, 'equalEarth');
  if (config.level !== undefined) reflect(el, 'data-level', config.level, 'country');
  if (config.palette !== undefined) reflect(el, 'data-palette', config.palette, 'atlas');
  if (config.border !== undefined) reflect(el, 'data-border', config.border, 'natural');
  if (config.graticule !== undefined) reflect(el, 'data-graticule', config.graticule ? null : 'false', null);
  if (config.callouts !== undefined) reflect(el, 'data-callouts', config.callouts ? 'true' : null, null);
  const selected = name === 'default' ? [] : keyList(config.selected ?? '');
  const now = keyList(el.getAttribute('data-selected') ?? '');
  if (selected.join(' ') !== now.join(' '))
    dfDollar(el).attr('data-selected', selected.length ? selected.join(' ') : null);
  const focus = name === 'focused' ? config.focus || null : null;
  if ((el.getAttribute('data-focus') || null) !== focus) dfDollar(el).attr('data-focus', focus);
}
function checkConfig(config: GeoMapConfig): void {
  if (config.projection !== undefined && !PROJECTIONS.includes(config.projection))
    throw new RangeError(`geo-map: unknown projection "${config.projection}" (${PROJECTIONS.join(', ')})`);
  if (config.level !== undefined && !LEVELS.includes(config.level))
    throw new RangeError(`geo-map: unknown level "${config.level}" (${LEVELS.join(', ')})`);
  if (config.palette !== undefined && !PALETTES.includes(config.palette))
    throw new RangeError(`geo-map: unknown palette "${config.palette}" (${PALETTES.join(', ')})`);
  if (config.border !== undefined && !BORDERS.includes(config.border))
    throw new RangeError(`geo-map: unknown border "${config.border}" (${BORDERS.join(', ')})`);
  if (config.center !== undefined) assertLonLat(config.center);
  if (
    config.zoom !== undefined &&
    (!Number.isFinite(Number(config.zoom)) || Number(config.zoom) < 1 || Number(config.zoom) > 1024)
  )
    throw new RangeError('geo-map: zoom is 1 to 1024');
}
/** the state's DOM work: the markup, then the view (a new focus flies there unless the config names a camera) */
function triggerStateChange(
  el: HTMLElement,
  name: string,
  config: GeoMapConfig,
  incoming: GeoMapConfig,
): Promise<void> {
  const geo = el._geo as GeoInstance;
  checkConfig(config);
  // a cell passed as the focus must name one; a focus already on the element (written from outside) is not
  // re-checked, so a map that holds an invalid one can still be reset
  if (incoming.focus?.startsWith('s2:')) s2Token(incoming.focus.slice(3));
  // entered without saying what (nothing, or an empty value - the State tab's cleared field): the selection /
  // focus the markup declared
  if (name === 'focused' && !config.focus && geo.authored.focus) config = { ...config, focus: geo.authored.focus };
  if (name === 'selected' && !keyList(config.selected ?? '').length)
    config = { ...config, selected: geo.authored.selected };
  // the state the config amounts to: 'focused' without a focus is a selection (or nothing)
  let landed = name;
  if (name === 'focused' && !config.focus) landed = keyList(config.selected ?? '').length ? 'selected' : 'default';
  if (name === 'selected' && !keyList(config.selected ?? '').length) landed = 'default';
  applyMarkup(el, landed, config);
  if (landed !== name) el.dataset.stateName = landed;
  return geo.ready.then(() => {
    const before = geo.view.focus;
    syncView(el, false);
    if (geo.status !== 'ready') return undefined; // nothing drawn: the error alert stays, the attributes are the state
    const camera = incoming.center !== undefined || incoming.zoom !== undefined;
    if (camera)
      return flyTo(
        el,
        { center: (incoming.center as LonLat) ?? geo.view.center, zoom: Number(incoming.zoom ?? geo.view.zoom) },
        false,
      );
    if (geo.view.focus && geo.view.focus !== before) return flyTo(el, targetFor(geo, geo.view.focus));
    requestRender(el);
    return undefined;
  });
}
/** take the element's attributes into the view; a focus that changed flies there (when `fly`) */
function syncView(el: HTMLElement, fly: boolean): void {
  const geo = el._geo as GeoInstance | undefined;
  if (!geo) return;
  const previous = geo.view;
  const next = viewFromAttributes(el, { center: previous.center, zoom: previous.zoom });
  const changed =
    JSON.stringify({ ...next, center: 0, zoom: 0 }) !== JSON.stringify({ ...previous, center: 0, zoom: 0 });
  if (!changed) return;
  geo.view = next;
  if (next.selected.join(' ') !== previous.selected.join(' ')) announceSelection(el);
  if (next.focus !== previous.focus) {
    announceFocus(el);
    if (fly && next.focus && geo.status === 'ready') void flyTo(el, targetFor(geo, next.focus));
  }
  requestRender(el);
}
function nameOfKey(geo: GeoInstance, key: string): string {
  return geo.ds?.byKey.get(key)?.properties.name ?? (key.startsWith('s2:') ? `S2 ${key.slice(3)}` : key);
}
function announceSelection(el: HTMLElement): void {
  const geo = el._geo as GeoInstance;
  const keys = [...geo.view.selected];
  // the selection changed - by a click, a key or setState
  el.dispatchEvent(
    new CustomEvent<GeoMapSelectDetail>('geo-map-select', {
      detail: { keys, names: keys.map((k) => nameOfKey(geo, k)) },
      bubbles: true,
    }),
  );
}
function announceFocus(el: HTMLElement): void {
  const geo = el._geo as GeoInstance;
  const key = geo.view.focus;
  // the focus changed: a region framed (its children shown), or the focus cleared
  el.dispatchEvent(
    new CustomEvent<GeoMapFocusDetail>('geo-map-focus', {
      detail: { key, name: key ? nameOfKey(geo, key) : null },
      bubbles: true,
    }),
  );
}
/** Registry-level API; pass the element explicitly. Unknown names throw. */
export const geoMapApi = componentState({
  component: 'geo-map',
  states: geoMapStates,
  mergeConfig: true,
  apply: (el, state, _previous, incoming) =>
    triggerStateChange(el as HTMLElement, state.name, state.config as GeoMapConfig, incoming as GeoMapConfig),
  // the state the attributes and the camera show now - a click or a pan changes them without setState
  read: (el) => {
    const geo = (el as HTMLElement)._geo as GeoInstance | undefined;
    if (!geo) return { name: (el as HTMLElement).dataset.stateName || 'default', config: {} };
    const view = viewFromAttributes(el as HTMLElement, { center: geo.view.center, zoom: geo.view.zoom });
    const config = configOf({ ...geo, view: { ...view, center: geo.view.center, zoom: geo.view.zoom } });
    return { name: stateNameOf(view), config: config as unknown as Record<string, unknown> };
  },
  markup: (el, state) => applyMarkup(el as HTMLElement, state.name, state.config as GeoMapConfig),
});
/** commit what the user did (a pick, a focus by double click) the way setState would have written it */
function userChange(el: HTMLElement, patch: { selected?: string[]; focus?: string | null }): void {
  const geo = el._geo as GeoInstance;
  const config = { ...configOf(geo), ...patch };
  const name = stateNameOf({ focus: config.focus ?? null, selected: keyList(config.selected) });
  if (patch.focus !== undefined && patch.focus !== geo.view.focus) {
    act(el, Promise.resolve(geoMapApi.setState(el, name, { selected: config.selected, focus: config.focus ?? null })));
    return;
  }
  applyMarkup(el, name, config);
  syncView(el, true);
  geoMapApi.commit(el, name, configOf(geo) as unknown as Record<string, unknown>);
}
// -- interaction -------------------------------------------------------------------------------------
function pixelOf(geo: GeoInstance, e: { clientX: number; clientY: number }): [number, number] {
  const r = geo.svg.getBoundingClientRect();
  return [((e.clientX - r.left) * geo.width) / (r.width || 1), ((e.clientY - r.top) * geo.height) / (r.height || 1)];
}
function unprojectVisible(projection: any, pixel: [number, number]): LonLat | null {
  const { d3 } = ready();
  const p = projection.invert?.(pixel);
  if (!p || !p.every(Number.isFinite) || Math.abs(p[1]) > 90) return null;
  if (!d3.geoPath(projection)({ type: 'Point', coordinates: p })) return null;
  const back = projection(p);
  if (!back || Math.hypot(back[0] - pixel[0], back[1] - pixel[1]) > 0.1) return null;
  return [p[0], p[1]];
}
function pan(el: HTMLElement, dx: number, dy: number): void {
  const geo = el._geo as GeoInstance;
  if (!geo.projection) return;
  cancelFlight(geo);
  const delta = RAD / geo.projection.scale();
  geo.view.center = [wrapLon(geo.view.center[0] - dx * delta), clamp(geo.view.center[1] + dy * delta, -85, 85)];
  requestRender(el);
}
function zoomAt(el: HTMLElement, factor: number, pixel?: [number, number]): void {
  const geo = el._geo as GeoInstance;
  if (!geo.projection) return;
  cancelFlight(geo);
  const zoom = clampZoom(geo.view.zoom * factor);
  let center: LonLat = [...geo.view.center];
  const before = pixel ? unprojectVisible(geo.projection, pixel) : null;
  // keep the place under the cursor: the projections are not linear in their center, so the correction repeats
  for (let i = 0; pixel && before && i < 4; i++) {
    const after = unprojectVisible(makeProjection(geo.view.projection, geo.box, center, zoom), pixel);
    if (!after) break;
    const dLon = wrapLon(before[0] - after[0]);
    const dLat = before[1] - after[1];
    center = [wrapLon(center[0] + dLon), clamp(center[1] + dLat, -85, 85)];
    if (Math.abs(dLon) + Math.abs(dLat) < 1e-6) break;
  }
  geo.view.center = center;
  geo.view.zoom = zoom;
  requestRender(el);
}
function settleSoon(el: HTMLElement): void {
  const geo = el._geo as GeoInstance;
  clearTimeout(geo.restTimer);
  geo.restTimer = setTimeout(() => announceView(el), 160) as unknown as number;
}
const keyAt = (target: EventTarget | null): string | null => {
  if (!(target instanceof Element)) return null;
  const hit = dfDollar(target).closest('[data-key]').get(0) as Element | undefined;
  return hit && !dfDollar(hit).closest('.geo-map-context').get(0) ? hit.getAttribute('data-key') : null;
};
function pick(el: HTMLElement, key: string, additive: boolean): void {
  const geo = el._geo as GeoInstance;
  const mode = el.dataset.select ?? 'single';
  if (mode === 'none') return;
  const now = geo.view.selected;
  let next: string[];
  if (mode === 'multiple' || additive) next = now.includes(key) ? now.filter((k) => k !== key) : [...now, key];
  else next = now.length === 1 && now[0] === key ? [] : [key];
  geo.roving = key;
  userChange(el, { selected: next });
}
function showTooltip(geo: GeoInstance, key: string | null, pixel?: [number, number]): void {
  const t = geo.tooltip;
  if (!key) {
    t.hidden = true;
    return;
  }
  const region = geo.drawn.get(key);
  if (!region) return;
  const parent = region.properties.parent
    ? geo.ds?.byKey.get(`country:${region.properties.parent}`)?.properties.name
    : '';
  dfDollar(t).text(parent ? `${region.properties.name}, ${parent}` : region.properties.name);
  t.hidden = false;
  if (pixel) {
    t.style.left = `${Math.min(pixel[0] + 14, geo.width - 8)}px`;
    t.style.top = `${pixel[1] + 12}px`;
  }
}
function setHover(el: HTMLElement, key: string | null): void {
  const geo = el._geo as GeoInstance;
  if (geo.hover === key) return;
  geo.hover = key;
  for (const c of dfDollar(geo.svg).find('.geo-map-callout').toArray() as Element[])
    c.toggleAttribute('data-hover', c.getAttribute('data-key') === key);
  drawLift(geo);
  writeOutputs(el);
}
/** the region nearest in an arrow's direction from the roving one (by label points) */
function moveRoving(el: HTMLElement, key: string, dir: [number, number]): void {
  const geo = el._geo as GeoInstance;
  const from = geo.anchors.get(key);
  if (!from) return;
  let best: string | null = null;
  let score = Infinity;
  for (const [k, a] of geo.anchors) {
    if (k === key) continue;
    const dx = a[0] - from[0];
    const dy = a[1] - from[1];
    const along = dx * dir[0] + dy * dir[1];
    if (along <= 0) continue;
    const across = Math.abs(dx * dir[1] - dy * dir[0]);
    const s = along + across * 2;
    if (s < score) {
      score = s;
      best = k;
    }
  }
  if (!best) return;
  geo.roving = best;
  renderMap(el);
  const target = dfDollar(geo.svg)
    .find(`[data-key="${CSS.escape(best)}"][role="button"]`)
    .get(0) as unknown as SVGElement | undefined;
  target?.focus();
}
function bindInteractions(el: HTMLElement): void {
  const geo = el._geo as GeoInstance;
  const svg = geo.svg;
  const signal = geo.abort.signal;
  svg.addEventListener(
    'pointerdown',
    (e) => {
      if (e.button !== 0) return;
      geo.pointers.set(e.pointerId, pixelOf(geo, e));
      geo.moved = false;
    },
    { signal },
  );
  svg.addEventListener(
    'pointermove',
    (e) => {
      const next = pixelOf(geo, e);
      const previous = geo.pointers.get(e.pointerId);
      if (!previous) {
        const key = keyAt(e.target);
        setHover(el, key);
        showTooltip(geo, key, next);
        return;
      }
      if (Math.hypot(next[0] - previous[0], next[1] - previous[1]) < 0.5) return;
      if (!svg.hasPointerCapture(e.pointerId)) svg.setPointerCapture(e.pointerId);
      const others = [...geo.pointers.entries()].filter(([id]) => id !== e.pointerId);
      if (others.length) {
        const other = others[0]![1];
        const a = Math.hypot(previous[0] - other[0], previous[1] - other[1]);
        const b = Math.hypot(next[0] - other[0], next[1] - other[1]);
        if (a > 2 && b > 2) zoomAt(el, b / a);
      } else pan(el, next[0] - previous[0], next[1] - previous[1]);
      if (Math.hypot(next[0] - previous[0], next[1] - previous[1]) > 1) geo.moved = true;
      geo.pointers.set(e.pointerId, next);
      showTooltip(geo, null);
      settleSoon(el);
    },
    { signal },
  );
  const release = (e: PointerEvent): void => {
    geo.pointers.delete(e.pointerId);
  };
  svg.addEventListener('pointerup', release, { signal });
  svg.addEventListener('pointercancel', release, { signal });
  svg.addEventListener('lostpointercapture', release, { signal });
  svg.addEventListener(
    'pointerleave',
    () => {
      setHover(el, null);
      showTooltip(geo, null);
    },
    { signal },
  );
  svg.addEventListener(
    'click',
    (e) => {
      if (geo.moved) return;
      const key = keyAt(e.target);
      if (key) pick(el, key, e.shiftKey || e.metaKey || e.ctrlKey);
    },
    { signal },
  );
  svg.addEventListener(
    'dblclick',
    (e) => {
      e.preventDefault();
      const key = keyAt(e.target);
      // a region or an S2 cell: drill into it (its children); a state, a leaf cell, the background: zoom in there
      const drill =
        key &&
        el.dataset.drill !== 'none' &&
        !key.startsWith('state:') &&
        !(key.startsWith('s2:') && s2Level(key.slice(3)) === 30);
      if (drill) userChange(el, { focus: key, selected: [] });
      else zoomAt(el, 2, pixelOf(geo, e));
      settleSoon(el);
    },
    { signal },
  );
  svg.addEventListener(
    'wheel',
    (e) => {
      e.preventDefault();
      const delta = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? geo.height : 1);
      zoomAt(el, Math.exp(-delta * 0.0015), pixelOf(geo, e));
      settleSoon(el);
    },
    { signal, passive: false },
  );
  svg.addEventListener(
    'keydown',
    (e) => {
      const key = keyAt(e.target);
      const arrows: Record<string, [number, number]> = {
        ArrowLeft: [-1, 0],
        ArrowRight: [1, 0],
        ArrowUp: [0, -1],
        ArrowDown: [0, 1],
      };
      if (key && (e.key === 'Enter' || e.key === ' ')) {
        e.preventDefault();
        pick(el, key, e.shiftKey);
        return;
      }
      if (key && arrows[e.key] && !e.shiftKey) {
        e.preventDefault();
        moveRoving(el, key, arrows[e.key]!);
        return;
      }
      if (e.key === 'Escape') {
        if (geo.view.selected.length) userChange(el, { selected: [] });
        else if (geo.view.focus) act(el, focusUp(el));
        return;
      }
      if (!['+', '=', '-', 'Home', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
      e.preventDefault();
      if (e.key === '+' || e.key === '=') zoomAt(el, 1.5);
      else if (e.key === '-') zoomAt(el, 1 / 1.5);
      else if (e.key === 'Home') act(el, reset(el));
      else {
        const [x, y] = arrows[e.key]!;
        pan(el, -x * 50, -y * 50);
      }
      settleSoon(el);
    },
    { signal },
  );
  svg.addEventListener(
    'focusin',
    (e) => {
      const key = keyAt(e.target);
      if (key) {
        geo.roving = key;
        setHover(el, key);
      }
    },
    { signal },
  );
  svg.addEventListener('focusout', () => setHover(el, null), { signal });
}
// -- outside controls + outputs ------------------------------------------------------------------
function writeOutputs(el: HTMLElement): void {
  if (!el.id) return;
  const geo = el._geo as GeoInstance;
  for (const out of dfDollar(`output[data-geo-for="${CSS.escape(el.id)}"]`).toArray() as HTMLOutputElement[]) {
    const kind = out.dataset.geoOutput ?? 'selection';
    const v = geo.view;
    // the level of an S2 focus's children; a focus that names no valid cell has none (and must not throw here)
    const cell = cellFocus(v);
    const text =
      kind === 'focus'
        ? v.focus
          ? nameOfKey(geo, v.focus)
          : 'World'
        : kind === 'zoom'
          ? `${v.zoom < 10 ? v.zoom.toFixed(1) : Math.round(v.zoom)}x`
          : kind === 'level'
            ? v.focus
              ? v.focus.startsWith('s2:')
                ? cell
                  ? `S2 level ${s2Level(cell) + 1}`
                  : ''
                : v.focus.startsWith('continent:')
                  ? 'country'
                  : 'state'
              : levelFor(v)
            : kind === 'hover'
              ? geo.hover
                ? nameOfKey(geo, geo.hover)
                : ''
              : kind === 'center'
                ? `${v.center[1].toFixed(2)}°, ${v.center[0].toFixed(2)}°`
                : kind === 'regions'
                  ? String(geo.stats.visibleRegions)
                  : v.selected.map((k) => nameOfKey(geo, k)).join(', ');
    if (out.textContent !== text) dfDollar(out).text(text);
  }
}
const mapFor = (control: Element): HTMLElement | null => {
  const id = control.getAttribute('data-geo-for') ?? '';
  const el = (id ? dfDollar(`#${CSS.escape(id)}`).get(0) : null) as HTMLElement | null;
  return el?.matches('.geo-map') && el._geo ? el : null;
};
/** a datalist of the best matches under a search input */
async function suggest(input: HTMLInputElement, el: HTMLElement): Promise<GeoSearchHit[]> {
  const hits = await search(el, input.value, 12);
  let list = (input.list ?? null) as HTMLDataListElement | null;
  if (!list) {
    list = document.createElement('datalist');
    list.id = `${el.id}-geo-names-${nextId++}`;
    list.setAttribute('data-ce-chrome', '');
    dfDollar(input).after(list);
    input.setAttribute('list', list.id);
  }
  setChildren(
    list,
    hits.map((h) => {
      const o = document.createElement('option');
      o.value = h.label;
      return o;
    }),
  );
  return hits;
}
async function runSearch(input: HTMLInputElement, el: HTMLElement): Promise<void> {
  const value = input.value.trim();
  if (!value) return;
  const hits = await search(el, value, 12);
  const hit = hits.find((h) => normalize(h.label) === normalize(value)) ?? hits[0];
  input.setCustomValidity(hit ? '' : `No region named "${value}"`);
  input.toggleAttribute('aria-invalid', !hit);
  if (!hit) {
    input.reportValidity();
    return;
  }
  await focusRegion(el, hit.key);
}
/** a control's action, run without a caller to await it: a failure is reported as the map's geo-map-error */
function act(el: HTMLElement, action: Promise<unknown>): void {
  action.catch((err) => reportError(el, err, ''));
}
function bindControls(): void {
  if (document.__geoMapInit) return;
  document.__geoMapInit = true;
  dfDollar(document).on('click', (e: Event) => {
    const control =
      e.target instanceof Element
        ? (dfDollar(e.target).closest('[data-geo-for][data-geo-action]').get(0) as HTMLElement | undefined)
        : undefined;
    if (!control || control.tagName === 'INPUT') return;
    const el = mapFor(control);
    if (!el) return;
    const value = control.dataset.value ?? '';
    const geo = el._geo as GeoInstance;
    switch (control.dataset.geoAction) {
      case 'zoom-in':
        zoomAt(el, 1.6);
        settleSoon(el);
        break;
      case 'zoom-out':
        zoomAt(el, 1 / 1.6);
        settleSoon(el);
        break;
      case 'reset':
        act(el, reset(el));
        break;
      case 'up':
        act(el, focusUp(el));
        break;
      case 'focus':
        act(el, focusRegion(el, value || geo.view.selected[0] || null));
        break;
      case 'select':
        act(el, select(el, keyList(value)));
        break;
      case 'clear':
        act(el, select(el, []));
        break;
    }
  });
  dfDollar(document).on('change', (e: Event) => {
    const control = e.target instanceof HTMLElement ? e.target : null;
    if (!control?.matches('[data-geo-for]')) return;
    const el = mapFor(control);
    if (!el) return;
    if (control.matches('input[data-geo-action="search"]')) {
      act(el, runSearch(control as HTMLInputElement, el));
      return;
    }
    const param = control.dataset.geoParam;
    if (!param) return;
    const value =
      control instanceof HTMLInputElement && control.type === 'checkbox'
        ? control.checked
        : (control as HTMLInputElement).value;
    const current = el.api!.getState();
    act(el, Promise.resolve(geoMapApi.setState(el, current.name, { [param]: value })));
  });
  dfDollar(document).on('input', (e: Event) => {
    const input = e.target instanceof HTMLInputElement ? e.target : null;
    if (!input?.matches('[data-geo-for][data-geo-action="search"]')) return;
    const el = mapFor(input);
    if (!el) return;
    input.setCustomValidity('');
    input.removeAttribute('aria-invalid');
    act(el, suggest(input, el));
  });
  dfDollar(document).on('keydown', (e: Event) => {
    const ev = e as KeyboardEvent;
    const input = ev.target instanceof HTMLInputElement ? ev.target : null;
    if (ev.key !== 'Enter' || !input?.matches('[data-geo-for][data-geo-action="search"]')) return;
    const el = mapFor(input);
    if (!el) return;
    ev.preventDefault();
    act(el, runSearch(input, el));
  });
}
// -- the public map API ---------------------------------------------------------------------------
const mapOf = (el: HTMLElement | string): HTMLElement => {
  const node = (typeof el === 'string' ? dfDollar(el).get(0) : el) as HTMLElement | undefined;
  if (!node?.matches?.('.geo-map') || !node._geo) throw new Error('not an initialized .geo-map');
  return node;
};
/**
 * Pick regions (an empty list clears the selection); they are highlighted the way data-highlight says.
 * @param el - the .geo-map element, or a selector for it
 * @param keys - region keys: country:FRA, state:DEU-1591, continent:Europe, s2:47a1
 * @returns resolves when the map shows them
 */
function select(el: HTMLElement | string, keys: string[]): Promise<void> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  const name = geo.view.focus ? 'focused' : keys.length ? 'selected' : 'default';
  return Promise.resolve(geoMapApi.setState(node, name, { selected: keys }) as Promise<void>);
}
/**
 * Frame a region and show its children - a continent's countries, a country's states; everything else is dimmed.
 * An S2 cell (s2:<token>) frames the cell and shows its four children. A name works too (the best search hit). null
 * clears the focus.
 * @param el - the .geo-map element, or a selector for it
 * @param region - a key (continent:Europe, country:DEU, s2:479f), a name ('Germany'), or null
 * @returns resolves when the camera has arrived
 */
async function focusRegion(el: HTMLElement | string, region: string | null): Promise<void> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  await geo.ready;
  let key = region;
  if (key?.startsWith('s2:')) key = `s2:${s2Token(key.slice(3))}`;
  else if (key && !/^(continent|country|state):/.test(key)) key = (await search(node, key, 1))[0]?.key ?? null;
  if (region && !key) throw new Error(`geo-map: no region named "${region}"`);
  if (key?.startsWith('state:') && geo.ds && !geo.ds.parents.has(key) && !geo.ds.byKey.has(key))
    await searchEntries(geo.ds);
  await geoMapApi.setState(node, key ? 'focused' : 'default', { focus: key, selected: [] });
}
/**
 * Step out of the focus: a state to its country, a country to its continent, a continent to the world; an S2 cell
 * to its parent, a cube face to the world.
 * @param el - the .geo-map element, or a selector for it
 * @returns resolves when the camera has arrived
 */
async function focusUp(el: HTMLElement | string): Promise<void> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  const key = geo.view.focus;
  if (!key) return reset(node);
  const cell = cellFocus(geo.view);
  if (cell) return s2Level(cell) ? focusRegion(node, `s2:${s2Parent(cell)}`) : reset(node);
  const region = geo.ds?.byKey.get(key);
  const parent = key.startsWith('state:')
    ? `country:${region?.properties.parent ?? geo.ds?.parents.get(key) ?? ''}`
    : key.startsWith('country:') && region?.properties.continent
      ? `continent:${region.properties.continent}`
      : null;
  if (parent && geo.ds?.byKey.has(parent)) return focusRegion(node, parent);
  return reset(node);
}
/**
 * Back to the whole world: no focus, no selection, the authored camera.
 * @param el - the .geo-map element, or a selector for it
 * @returns resolves when the camera has arrived
 */
async function reset(el: HTMLElement | string): Promise<void> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  await geo.ready;
  await geoMapApi.setState(node, 'default', {});
  if (geo.status === 'ready') await flyTo(node, geo.home);
}
/**
 * Find regions by name: continents and countries at once, every state once the names index has loaded. Accents and
 * case are ignored; whole names rank before prefixes, prefixes before parts.
 * @param el - the .geo-map element, or a selector for it
 * @param query - what was typed
 * @param limit - the most hits (default 10)
 * @returns the hits, best first
 */
async function search(el: HTMLElement | string, query: string, limit: number = 10): Promise<GeoSearchHit[]> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  await geo.ready;
  if (!geo.ds) return [];
  return rank(await searchEntries(geo.ds), query, limit);
}
/**
 * Zoom by a factor around the center (2 = twice as close).
 * @param el - the .geo-map element, or a selector for it
 * @param factor - greater than 0
 */
function zoomBy(el: HTMLElement | string, factor: number): void {
  if (!Number.isFinite(factor) || factor <= 0) throw new RangeError('a positive zoom factor');
  const node = mapOf(el);
  zoomAt(node, factor);
  settleSoon(node);
}
/**
 * Move the camera - flying there unless `animate` is false or motion is reduced.
 * @param el - the .geo-map element, or a selector for it
 * @param center - [longitude, latitude]
 * @param zoom - 1 to 1024 (default: the current zoom)
 * @param animate - fly (default true)
 * @returns resolves when the camera has arrived
 */
async function setView(
  el: HTMLElement | string,
  center: LonLat,
  zoom?: number,
  animate: boolean = true,
): Promise<void> {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  assertLonLat(center);
  await geo.ready;
  await flyTo(node, { center, zoom: zoom ?? geo.view.zoom }, animate);
}
/**
 * Replace what the map draws over its boundaries - markers, lines, S2 cells, per-region looks (each one given
 * replaces that layer; leave one out to keep it).
 * @param el - the .geo-map element, or a selector for it
 * @param layers - `{ markers, lines, cells, styles }`
 */
function setLayers(el: HTMLElement | string, layers: GeoLayers): void {
  const node = mapOf(el);
  const geo = node._geo as GeoInstance;
  const next = { ...geo.layersData };
  if (layers.markers) {
    for (const m of layers.markers) resolvePosition(m.position);
    next.markers = layers.markers.map((m) => ({ ...m }));
  }
  if (layers.lines) {
    for (const l of layers.lines) {
      if (l.coordinates) {
        if (l.coordinates.length < 2) throw new RangeError(`line ${l.id}: two positions at least`);
        l.coordinates.forEach(assertLonLat);
      } else if (!l.from || !l.to) throw new RangeError(`line ${l.id}: coordinates, or from and to`);
    }
    next.lines = layers.lines.map((l) => ({ ...l }));
  }
  if (layers.cells) {
    next.cells = layers.cells.map((c) => s2Token(c));
    geo.cellRegions = next.cells.map(s2Polygon);
  }
  if (layers.styles) next.styles = { ...layers.styles };
  geo.layersData = next;
  requestRender(node);
}
/**
 * The regions drawn now, with their label points.
 * @param el - the .geo-map element, or a selector for it
 * @returns one entry per region in view
 */
function visible(el: HTMLElement | string): GeoVisibleRegion[] {
  const geo = mapOf(el)._geo as GeoInstance;
  return [...geo.drawn].map(([key, r]) => ({
    key,
    name: r.properties.name,
    level: r.properties.level,
    anchor: geo.anchors.get(key) ?? null,
  }));
}
/**
 * A region by key, when the map has it (resident, or its chunk loaded).
 * @param el - the .geo-map element, or a selector for it
 * @param key - country:DEU, state:DEU-1591, continent:Europe
 * @returns the region, or null
 */
function region(el: HTMLElement | string, key: string): GeoRegion | null {
  return (mapOf(el)._geo as GeoInstance).ds?.byKey.get(key) ?? null;
}
/**
 * Where a position is drawn in the last rendered view (SVG pixels), or null when it is out of view or behind the
 * globe.
 * @param el - the .geo-map element, or a selector for it
 * @param point - [longitude, latitude]
 * @returns [x, y] or null
 */
function project(el: HTMLElement | string, point: LonLat): [number, number] | null {
  const geo = mapOf(el)._geo as GeoInstance;
  assertLonLat(point);
  if (!geo.projection || !ready().d3.geoPath(geo.projection)({ type: 'Point', coordinates: point })) return null;
  return geo.projection(point) as [number, number];
}
/**
 * The position drawn at an SVG pixel of the last rendered view, or null off the globe.
 * @param el - the .geo-map element, or a selector for it
 * @param pixel - [x, y]
 * @returns [longitude, latitude] or null
 */
function unproject(el: HTMLElement | string, pixel: [number, number]): LonLat | null {
  const geo = mapOf(el)._geo as GeoInstance;
  return geo.projection ? unprojectVisible(geo.projection, pixel) : null;
}
/**
 * What the last render drew and loaded.
 * @param el - the .geo-map element, or a selector for it
 * @returns the counts and timings
 */
function stats(el: HTMLElement | string): GeoRenderStats {
  return { ...(mapOf(el)._geo as GeoInstance).stats };
}
/**
 * The fill that hatches a region (an SVG pattern of the map) - for `styles: { 'country:DEU': { fill } }`.
 * @param el - the .geo-map element, or a selector for it
 * @returns a CSS url() fill
 */
function hatch(el: HTMLElement | string): string {
  return (mapOf(el)._geo as GeoInstance).hatch;
}
df$.geoMapApi = geoMapApi;
df$.geoMapStates = geoMapStates;
// public imperative APIs (AGENTS.md "No window globals": df$.shadcn.geoMap, df$.shadcn.geo)
df$.geoMap = {
  select,
  focus: focusRegion,
  up: focusUp,
  reset,
  search,
  zoomBy,
  setView,
  setLayers,
  visible,
  region,
  project,
  unproject,
  stats,
  hatch,
};
df$.geo = {
  load,
  configure,
  urls: GEO_VENDOR_URLS,
  WGS84,
  inverse,
  direct,
  path: geodesicPath,
  length: polylineLength,
  s2Cell,
  s2Id,
  s2Token,
  s2Level,
  s2Center,
  s2Vertices,
  s2Polygon,
  s2Neighbors,
  s2Children,
  s2Parent,
  resolvePosition,
  inBounds,
  mortonEncode,
  mortonDecode,
  mortonKey,
  mortonIndex,
  routeGraph,
  positionAlongRoute,
  decode,
  features,
  rewind,
};
// -- init ----------------------------------------------------------------------------------------------
function layersFrom(el: HTMLElement): Required<GeoLayers> {
  const script = dfDollar(el).children('script.geo-map-layers').get(0) as HTMLScriptElement | undefined;
  const empty = { markers: [], lines: [], cells: [], styles: {} };
  if (!script) return empty;
  const json = JSON.parse(script.textContent || '{}') as GeoLayers;
  return { ...empty, ...json } as Required<GeoLayers>;
}
function dataUrl(el: HTMLElement): string {
  const raw = el.dataset.src || metaContent('geo-map-data') || settings.data || releaseData();
  return new URL(raw, document.baseURI).href;
}
function mount(el: HTMLElement): void {
  const camera = {
    center: parseCenter(el.dataset.center) ?? HOME,
    zoom: clampZoom(Number(el.dataset.zoom) || 1),
  };
  // runtime chrome (data-ce-chrome): the HTML Preview Editor never writes it into a source
  const svg = svgEl('svg', {
    'data-ce-chrome': '',
    class: 'geo-map-svg',
    role: 'group',
    tabindex: '0',
    'aria-roledescription': 'map',
    'aria-label': el.getAttribute('aria-label') || 'Interactive map. Arrow keys pan, plus and minus zoom, Home resets.',
  });
  const pattern = svgEl('pattern', {
    id: `geo-map-hatch-${nextId++}`,
    width: '8',
    height: '8',
    patternUnits: 'userSpaceOnUse',
  });
  pattern.append(svgEl('path', { d: 'M-2,2L2,-2M0,8L8,0M6,10L10,6', class: 'geo-map-hatch' }));
  const defs = svgEl('defs');
  defs.append(pattern);
  svg.append(defs);
  const layers: Record<string, SVGGElement> = {};
  for (const name of ['sphere', 'graticule', 'context', 'regions', 'cells', 'lines', 'lift', 'markers', 'callouts']) {
    layers[name] = svgEl('g', { class: `geo-map-${name}` });
    if (['sphere', 'graticule', 'context', 'lift', 'lines'].includes(name))
      layers[name]!.setAttribute('aria-hidden', 'true');
    svg.append(layers[name]!);
  }
  const tooltip = document.createElement('output');
  tooltip.className = 'geo-map-tooltip';
  tooltip.setAttribute('data-ce-chrome', '');
  tooltip.hidden = true;
  tooltip.setAttribute('aria-hidden', 'true');
  let layersData: Required<GeoLayers>;
  let layerError = '';
  try {
    layersData = layersFrom(el);
  } catch (err) {
    layersData = { markers: [], lines: [], cells: [], styles: {} };
    layerError = `script.geo-map-layers is no valid JSON: ${err instanceof Error ? err.message : err}`;
  }
  const geo: GeoInstance = {
    view: viewFromAttributes(el, camera),
    ds: null,
    status: 'loading',
    ready: Promise.resolve(),
    svg,
    layers,
    tooltip,
    hatch: `url(#${pattern.id})`,
    width: el.clientWidth || 800,
    height: el.clientHeight || 480,
    projection: null,
    box: { x0: 0, y0: 0, x1: 0, y1: 0 },
    frameId: 0,
    flight: null,
    pendingFit: null,
    pointers: new Map(),
    moved: false,
    drawn: new Map(),
    anchors: new Map(),
    hover: null,
    roving: null,
    layersData,
    cellRegions: [],
    wanted: new Set(),
    stats: {
      visibleRegions: 0,
      contextRegions: 0,
      visibleCells: 0,
      visibleMarkers: 0,
      callouts: 0,
      renderMs: 0,
      cachedChunks: 0,
      loadedBytes: 0,
      loading: 0,
    },
    abort: new AbortController(),
    resize: new ResizeObserver((entries) => {
      const r = entries[0]!.contentRect;
      if (r.width > 0 && r.height > 0 && (r.width !== geo.width || r.height !== geo.height)) {
        geo.width = r.width;
        geo.height = r.height;
        requestRender(el);
      }
    }),
    attrs: new MutationObserver(() => syncView(el, true)),
    lastView: '',
    home: camera,
    authored: { focus: el.dataset.focus || null, selected: keyList(el.dataset.selected ?? '') },
    restTimer: 0,
  };
  el._geo = geo;
  dfDollar(el).append(svg).append(tooltip);
  el.dataset.geoStatus = 'loading';
  dfDollar(el).attr('aria-busy', 'true');
  geo.ready = (async () => {
    try {
      await load();
      try {
        layersData.cells = layersData.cells.map((c) => s2Token(c));
        geo.cellRegions = layersData.cells.map(s2Polygon);
        for (const m of layersData.markers) resolvePosition(m.position);
      } catch (err) {
        layersData.cells = [];
        layersData.markers = layersData.markers.filter((m) => {
          try {
            resolvePosition(m.position);
            return true;
          } catch {
            return false;
          }
        });
        layerError ||= `script.geo-map-layers: ${err instanceof Error ? err.message : err}`;
      }
      geo.ds = await openDataset(dataUrl(el));
      geo.ds.maps.add(el);
      geo.status = 'ready';
      el.dataset.geoStatus = 'ready';
      // a focused map without an authored camera opens framed on its focus
      if (geo.view.focus && !el.dataset.center && !el.dataset.zoom) {
        geo.box = { x0: 0, y0: 0, x1: geo.width, y1: geo.height };
        const target = targetFor(geo, geo.view.focus);
        geo.view.center = target.center;
        geo.view.zoom = target.zoom;
      }
      renderMap(el);
      announceView(el);
      if (layerError) {
        // the layers block is broken: the map draws without what it could not read, and says so
        dfDollar(el).append(
          dfDollar('<output class="geo-map-error" role="alert" data-ce-chrome></output>').text(layerError),
        );
        reportError(el, new Error(layerError), '');
      }
    } catch (err) {
      geo.status = 'error';
      el.dataset.geoStatus = 'error';
      dfDollar(el).attr('aria-busy', null);
      const message = err instanceof Error ? err.message : String(err);
      dfDollar(el).children('.geo-map-error').remove();
      dfDollar(el).append(
        dfDollar('<output class="geo-map-error" role="alert" data-ce-chrome></output>').text(
          `The map could not be loaded. ${message}`,
        ),
      );
      reportError(el, err, '');
    }
  })();
  geo.resize.observe(el);
  geo.attrs.observe(el, {
    attributes: true,
    attributeFilter: [
      'data-projection',
      'data-level',
      'data-palette',
      'data-border',
      'data-graticule',
      'data-callouts',
      'data-selected',
      'data-focus',
      'data-highlight',
    ],
  });
  bindInteractions(el);
}
function init(): void {
  (dfDollar('.geo-map:not([data-init])').toArray() as HTMLElement[]).forEach((el) => {
    el.dataset.init = '';
    bindControls();
    mount(el);
    const geo = el._geo as GeoInstance;
    // el.store + el.api (AGENTS.md "State through stores"): the state the attributes declare
    const name = stateNameOf(geo.view);
    bindComponent(el, geoMapApi, { name, config: configOf(geo) as unknown as Record<string, unknown> });
    // the first state change waits for the engines and the data: el.api.settled() resolves once the map is drawn
    void geoMapApi.setState(el, name, {});
  });
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });

Comments, ideas or improvements? Edit this page's source on GitHub