/**
 * The built city, plus the handful of landmarks placed by hand.
 *
 * Two things make this read as a city rather than as noise:
 *
 *   - **Buildings step along a block lattice** in each district's own grid
 *     bearing. An earlier version rejection-sampled uniformly inside each
 *     district and it looked like rubble, because a city is not a Poisson
 *     process. In San Francisco the lattice also reproduces the 46° between
 *     the grid north of Market and the grid south of it, and Market Street
 *     falls out as a seam rather than having to be drawn.
 *   - **Buildings stand on the terrain.** Every base is sampled from
 *     `world.groundAt`, so Nob Hill's low-rises tower over taller blocks in the
 *     flats below — which is true of that city, and which a flat map gets
 *     exactly backwards.
 *
 * Everything is instanced: one draw call for the whole city.
 */

import * as THREE from "three";
import { deviceProfile } from "./stage.ts";
import type { District } from "./types.ts";
import { seededRandom, type World } from "./world.ts";

/** Lot size in scene units, and how many lots sit between cross-streets. */
const LOT = 0.42; // ~40 m at SF's scale
const BLOCK_LOTS = 4; // 3 made streets a third of the city's surface

/**
 * The lot a **detail** district is built on, in true metres — a ladder now,
 * not a number, and which rung the board stands on is decided by the camera.
 *
 * `LOT` above is fixed in scene units, and the note under it already says what
 * that means: a lot is 40 m in San Francisco, 164 m in Southern California and
 * **806 m** on the statewide board. That is the right trade while a board is
 * looked at from its own stand-off — and exactly the wrong one once
 * `cities/unify.ts` puts San Francisco's fifty-two districts *on* the statewide
 * board, because they are then lotted at 806 m. The arithmetic is brutal: lots
 * go as the inverse square of their size, so the Bay Area's 83,137 buildings
 * become about **two hundred**, and a city renders as a handful of grey slabs.
 *
 * So a detail district measures its lot in metres. **160 m was that number, and
 * one number is what this replaces**, because the merged board stopped being
 * looked at from one distance and 160 m is wrong at both ends of the range it
 * is now looked at from. Measured with `scripts/cost-at.mjs` against the
 * 400,000-triangle cap, on the board with the frustum culling already in it:
 *
 * | pose | triangles at a flat 160 m |
 * |---|---|
 * | San Francisco, 2.5 km | 216,292 — 184,000 spare, and the city is 1,987 lots |
 * | San Francisco, 7.7 km | 222,286 |
 * | Los Angeles, 2.5 km | **415,780 — over** |
 * | Los Angeles, 7.7 km | **621,866 — over by half again** |
 *
 * Both ends are the same fault. Over San Francisco 160 m buys 37 buildings in
 * the Financial District, the city reads as scattered cubes, and a third of the
 * budget goes unspent; over Los Angeles the same 160 m is 40,000 lots genuinely
 * in frustum — you are looking down a ninety-kilometre basin — and the board is
 * half again over its cap. Frustum culling cannot reach either one: at San
 * Francisco there is nothing to remove, and at Los Angeles the lots it would
 * have to remove are on screen.
 *
 * Every rung from 40 to 250 is **below** `NEIGHBOURHOOD_LOT_METRES`, and the
 * ladder's working range stops at 250 for that reason rather than going on to
 * 320. Above 260 m the street lattice switches off, so coverage rises by
 * 1/(3/4)² = 1.778× and a 320 m rung is *dearer* than a 250 m one: at the Los
 * Angeles 7.7 km pose 250 m packs about 17,100 lots and 320 m packs 18,656. It
 * is also a look change and not only a count change — the same city with no
 * streets in it.
 *
 * **400 is the exception and it is a floor, not a working rung.** There is a
 * band of stand-off just below the one where `DETAIL_STANDOFF_M` switches the
 * metros off entirely — a seek of 12.5 to 13.3 km over Los Angeles, which is
 * 113 to 120 km of engine stand-off — where the frustum holds essentially the
 * whole Southland and 250 m is 19,600 to 20,000 lots. Measured, that band is
 * 400,548 and 404,828 triangles: over the cap on the coarsest rung the ladder
 * had, with nowhere left to go. 400 m takes those poses to about 293,000. It
 * loses the street lattice and that is the price, and at 118 km of stand-off it
 * is a price of nothing: the frame is 90 km of ground across a thousand pixels,
 * so a 400 m lot is four pixels and a street is one.
 *
 * 160 m stays on the ladder, at `DETAIL_LOT_REFERENCE`, and its rung is drawn
 * from the unshifted seed. So the board as it shipped is still one of the rungs
 * exactly, rather than something near it.
 *
 * **A rung change pops, and there is no version of this that does not.** Each
 * rung is an independent survey of the same ground — a different lattice, a
 * different set of addresses, different heights and colours drawn from a stream
 * of its own — so stepping from 250 m to 200 m does not subdivide the city, it
 * replaces it. Cross-fading between two rungs would cost the one thing this
 * board has none of, which is a draw call, because both instance sets would
 * have to be non-empty in the same frame. What makes it liveable is where the
 * steps fall: a rung changes when the visible set changes, which on the merged
 * board is on the order of once a journey, and the change lands during camera
 * motion rather than on a still frame. Held still, the board never re-lots.
 */
const DETAIL_LOT_TIERS = [40, 80, 160, 200, 250, 400] as const;

/**
 * The rung the board shipped on, and the one every other rung is priced
 * against.
 *
 * Two jobs, and they are both about not moving what already works. It is the
 * tier built **first** for a district, so its measured lot count is what the
 * finer rungs' cost is predicted from before any of them is walked; and it is
 * the tier that keeps the district's own unshifted seed, so every building on
 * it stands exactly where it stood.
 */
const DETAIL_LOT_REFERENCE = 2;

/**
 * How far apart two rungs' random streams sit.
 *
 * Each rung is an independent walk of the same district and needs its own
 * stream, or the 80 m lattice would be the 160 m lattice's first quarter with
 * the same heights and colours in it — which reads as a city that grew denser
 * by subdivision rather than as a finer survey of the same city. A prime stride
 * off the district's own seed, offset by the rung's distance from
 * `DETAIL_LOT_REFERENCE`, keeps the reference rung on the unshifted seed and
 * gives every other rung a stream nothing else uses.
 */
const TIER_SEED_STRIDE = 104_729;

/**
 * The most lots one district may contribute to a rung finer than the reference,
 * or that rung is not built for it at all.
 *
 * A finer rung is only ever *chosen* when the whole visible set fits
 * `LOT_BUDGET`, and the visible set always contains the district being
 * asked about — so a district that alone is a large fraction of the budget can
 * never see its fine rung drawn, and building one for it is dead weight in the
 * store and dead time in the boot. That is the whole of this constant: it is
 * not an aesthetic threshold, it is a "would this ever be reachable" one.
 *
 * The reason it has to be there at all is that the fine rungs are enormous if
 * left ungated. Measured on the merged board: the 89 detail districts build
 * 56,327 lots at 160 m, 225,308 at 80 m and **901,502 at 40 m** — 3,786 ms of
 * the boot's main thread and 72 MiB, against 264 ms and 4.7 MiB. The shape of
 * the distribution is what makes the gate cheap: the Bay's downtown districts
 * are 37 to 278 lots at 160 m, while the Southland's outer basin districts are
 * 2,140 (Northridge and Sylmar), 2,848 (Ontario) and 4,009 (West Covina and
 * Pomona). At 2,500 the sprawl gets no fine rung and the downtowns get both.
 *
 * A district that is refused a rung simply draws its finest built one when the
 * board asks for a finer; see the coarse-fill in `createBlocks`.
 *
 * **Finer than the reference is not walked at boot.** The gate still decides
 * whether a district *may* have a 40 m or 80 m rung; the walk itself waits for
 * a pose whose budget would pick that rung. A district you never stand over at
 * 2 km is a district that never spends the second and the megabytes.
 */
const TIER_DISTRICT_LOTS = 2_500;

/**
 * How many lots the whole board may pack.
 *
 * **This is the lever, and it is a lot count rather than a lot size**, because
 * a lot size is the wrong thing to hang on the camera. The merged board's two
 * metros are not the same bill at the same distance: measured at the same
 * engine stand-off of 22.6 km, San Francisco has 1,987 lots in frustum at 160 m
 * and Los Angeles has about 23,000, an eleven-fold difference that comes from
 * the districts themselves — the Bay's are one to three kilometres across and
 * the Southland's outer ones are ten to fifteen. Any rule written in metres of
 * lot, or in kilometres of reach, is therefore a very different bill in the two
 * cities and has to be sized for the worse one. A rule written in *lots you may
 * pack* is the same rule in both, and it spends San Francisco's spare budget
 * without spending Los Angeles's.
 *
 * **It is one number and not a ramp, and that is a measurement rather than a
 * simplification.** The obvious shape for this was a budget that shrinks as the
 * camera pulls back, on the reasoning that more of the state is in frame and
 * the terrain costs more. The reasoning is right and the effect is not there:
 * everything in the frame that is *not* the city layer — terrain, water, roads,
 * structures — measured across the whole band in which any of this runs comes
 * to
 *
 * | pose | engine stand-off | everything but the city |
 * |---|---|---|
 * | San Francisco, 2.5 km seek | 22.6 km | 195,468 |
 * | Los Angeles, 2.5 km seek | 22.6 km | 184,256 |
 * | San Francisco, 7.7 km seek | 69.7 km | 196,502 |
 * | Los Angeles, 7.7 km seek | 69.7 km | 203,822 |
 * | Los Angeles, 9 km seek | 81.5 km | 204,268 |
 * | Los Angeles, 13 km seek | 118 km | 204,448 |
 *
 * — flat to within 10%, because the terrain is chunked and frustum-culled and
 * what leaves the frame at the near end arrives at the far end. A ramp built on
 * that would be fitting noise, and it did real harm while it was in: sized to
 * be safe at 120 km it coarsened Los Angeles at 9 and 11 km, poses that measure
 * 380,698 and 390,538 and have nothing to give back.
 *
 * So: 400,000 minus 205,000 is 195,000 triangles for the city, and ten
 * triangles to an instance makes that 19,500 lots. 19,000 is that with a
 * margin, and it is what holds the guarantee — a pose whose non-city cost is at
 * or under the 204,448 this was priced against is at most
 * `19,000 x 10 + 204,448 = 394,448`.
 *
 * **That is a bound on the city and not on the frame**, and the difference is
 * worth stating because the sentence here used to claim the latter. The
 * non-city cost is not a constant and nothing enforces it: measured, it reaches
 * 288,365 at Los Angeles from 110 km, a pose where metro detail is off entirely
 * and terrain, corridor and sky are the whole of the frame. The city is bounded;
 * everything else is measured, and the worst combined pose anywhere in either
 * metro's descent is **390,730 at LA 10.5-11.5 km**, which is 2.3% under the
 * cap. Nothing in CI stands there — see `TODO.md`.
 */
export const LOT_BUDGET = 19_000;


/**
 * The lot, in true metres, at a given rung of the ladder.
 *
 * The ladder itself stays private — nothing outside this file gets to reason
 * about which rungs exist — but a harness has to be able to *report* the rung
 * the board settled on, and a rung index alone is not a fact anyone can read.
 * It describes the detail districts only; a base district has one lattice at
 * the scene-unit `LOT` and every rung is that.
 */
export function detailLotMetres(tier: number): number {
  const at = Math.min(DETAIL_LOT_TIERS.length - 1, Math.max(0, Math.round(tier)));
  return DETAIL_LOT_TIERS[at]!;
}


/**
 * The ground size at which a lot stops being a city block.
 *
 * `LOT` is fixed in **scene units**, which is right — the three boards are 1003,
 * 308 and 284 units across and are looked at from comparable standoffs, so a
 * lot that is legible on one is legible on the others. But it means a lot is
 * 40 m in San Francisco, 164 m in Southern California and **806 m** on the
 * statewide California board, and two things that are correct for a city are
 * wrong at 806 m:
 *
 *   - **The street lattice.** Skipping every fourth row and column leaves 44%
 *     of a district unbuilt. At 40 m those gaps are streets. At 806 m they are
 *     eight-hundred-metre voids, and Los Angeles came out as a chequerboard of
 *     separate white squares rather than as a city — the one thing the state
 *     board most needed it to be. Above the threshold the lots tile, and the
 *     district's `coverage` roll does all the thinning, which reads as urban
 *     fabric because its gaps are irregular.
 *   - **Casting shadows.** A shadow caster pays for itself twice, once in the
 *     shadow pass and once in the beauty pass. A 40 m building on a San
 *     Francisco hillside throws a shadow you can see; a 60 m building on a
 *     806 m lot throws about one pixel, and paying a second pass over a hundred
 *     thousand triangles for it — on the board with the tightest budget of the
 *     three — is not a trade anyone would make on purpose. Lambert still shades
 *     the four walls, which is all the state camera can resolve anyway.
 *   - **The underside.** Same measurement, same argument; see the geometry.
 *
 * 260 m is comfortably above Southern California's 164 and far below
 * California's 806, so neither of the detailed boards changes at all.
 */
export const NEIGHBOURHOOD_LOT_METRES = 260;

/**
 * Whether the anonymous city goes into the shadow map, given the lot size and
 * the device.
 *
 * Two independent reasons to say no, and they are answering different
 * questions, which is why this is one function and not one flag.
 *
 * **The lot.** See `NEIGHBOURHOOD_LOT_METRES`: a 60 m building on an 806 m lot
 * throws about a pixel of shadow, and paying a whole second pass over a hundred
 * thousand triangles for a pixel is not a trade anyone would make on purpose.
 *
 * **The device.** This is the new half, and it is the only lever in the engine
 * that removes about 44% of a phone's per-frame geometry. Measured on this
 * repo's Radeon with `EXT_disjoint_timer_query_webgl2`, at the whole-board pose:
 *
 * | board | triangles with | without | GPU frame |
 * |---|---|---|---|
 * | bay-area | 2,265,056 | 1,267,448 | 1.22 ms → 0.93 ms |
 * | socal | 1,417,648 | 772,444 | 0.91 ms → 0.69 ms |
 *
 * — because the shadow pass costs *geometry submission*, not rasterisation. The
 * same measurement is why the shadow map's **size** is not the lever it looks
 * like: 4096, 2048, 1024 and 256 texel maps all render the bay-area frame in
 * 1.21–1.31 ms, inside the run-to-run noise of a 1.2 ms frame. On a tile-based
 * mobile GPU, where binning cost tracks geometry and parameter-buffer traffic,
 * dropping a million triangles is worth strictly more than it is here.
 *
 * What it costs, measured as a picture pair at 16:30 PDT rather than argued:
 * at the whole-board pose it is **invisible** — mean frame luminance 120.23 with
 * and 120.27 without, 0.03% — because `scene.ts` hands the kit a shadow extent
 * of 0.75 board spans, which for the Bay Area's 1003 units is a 1504-unit box,
 * about 69 m to a texel at 2048, and a building footprint is one texel or less.
 * At the FiDi chapter pose it is visible (105.32 → 108.64, +3.2%): the towers
 * stop shadowing the ground and each other. So this is a *pose-dependent*
 * saving, which is precisely why it is gated on the handheld and not shipped
 * flat — a phone is the device that cannot afford the geometry and the device
 * whose screen is least able to resolve what the geometry bought.
 *
 * Terrain keeps self-shadowing on a phone either way: its caster is a decimated
 * copy swung in by `drawRange`, a quarter of its triangles, and it measured free
 * (1.24 ms against 1.22). Landmarks keep casting too — there are a handful of
 * them and their silhouettes are the point. Office scenes are untouched: at
 * 1 unit = 1 m the desk shadows are the whole read of depth, and `stage.ts`
 * already argues that.
 *
 * The `handheld` argument is injectable so a test can ask both questions
 * without a `window`; the default is the live device.
 */
export function blocksCastShadow(lotIsABlock: boolean, handheld = isHandheld()): boolean {
  return lotIsABlock && !handheld;
}

/** `deviceProfile()` reads `window`; the typecheck and the tests do not have one. */
function isHandheld(): boolean {
  if (typeof window === "undefined") return false;
  return deviceProfile().handheld;
}

const PALETTES = {
  downtown: [0xb9c3cc, 0xa8b4c0, 0xc7cfd6, 0x9dabb8, 0xd2d8dd, 0x8f9eaa],
  residential: [0xe8e2d6, 0xdcd3c4, 0xefe9dd, 0xd6cdbc, 0xe3d9c8, 0xcfc4b2, 0xf0ece2],
  industrial: [0xbdb5a8, 0xa89f92, 0xcac2b4, 0xb0a89a, 0x9c9488],
} satisfies Record<District["palette"], number[]>;

/**
 * How commercial each palette's buildings are, 0..1.
 *
 * Read only by `nightlights.ts`, and the reason a night city looks like a city
 * rather than like a uniform field of dots: an office floor is a continuous
 * band of large windows with half of them left on all night, and a house is two
 * small warm rectangles that go out. The number is the same fact the palette
 * already encodes, which is why it is derived from it rather than authored
 * again per district.
 */
const COMMERCIAL = {
  downtown: 1,
  residential: 0.12,
  industrial: 0.45,
} satisfies Record<District["palette"], number>;

/**
 * The name of the per-instance attribute `createBlocks` leaves on its geometry:
 * `[commercial, seed]`.
 *
 * A vertex attribute rather than a field on `userData` because the only
 * consumer is a shader, and this puts the data where the GPU already wants it.
 * `createBlocks` writes it because `createBlocks` is what knows which district
 * a given instance came out of; nothing else can recover that from the mesh.
 */
export const FACADE_ATTRIBUTE = "aFacade";

/** An independent stream for the facades; see where it is drawn from. */
const FACADE_SEED = 20_261;

interface Box {
  x: number;
  z: number;
  y: number;
  w: number;
  d: number;
  h: number;
  rot: number;
  color: THREE.Color;
  commercial: number;
}

/**
 * A named building's claim on the anonymous city scatter, in scene units.
 *
 * Circles are deliberately conservative. Anonymous buildings rotate with
 * their districts and named glyphs rotate with their streets; a circle is the
 * one cheap overlap test that cannot leave a corner poking through, and there
 * are only a handful of reservations to test.
 */
export interface BuildingReservation {
  x: number;
  z: number;
  radius: number;
}

function polygonBounds(poly: [number, number][]) {
  let minLat = Infinity;
  let maxLat = -Infinity;
  let minLng = Infinity;
  let maxLng = -Infinity;
  for (const [lat, lng] of poly) {
    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lng < minLng) minLng = lng;
    if (lng > maxLng) maxLng = lng;
  }
  return { minLat, maxLat, minLng, maxLng };
}

export function createBlocks(
  world: World,
  reservations: readonly BuildingReservation[] = [],
): THREE.InstancedMesh {
  const boxes: Box[] = [];
  /**
   * Where each district's lots sit in `boxes`, how far that district reaches,
   * and whether it is metro detail — so the level of detail can draw the city
   * you are near and not the one four hundred kilometres away, and so the
   * frustum can drop the one behind you. See `updateBlocksDetail`.
   *
   * **Every** district that emitted a lot is recorded here, base districts
   * included, and that is the change that made a close pose cheap. While only
   * the detail districts were listed the base set was a fixed prefix of the
   * instance buffer and was therefore drawn at every pose: standing 2.5 km over
   * San Francisco the merged board still paid for California's own 2,839
   * state-scale lots, none of which were on screen. A base range costs eight
   * entries in a list and buys the whole prefix back.
   */
  const districtRanges: DetailRange[] = [];
  let seedBase = 1337;

  // See `NEIGHBOURHOOD_LOT_METRES`. One measurement, two decisions, and both of
  // them are about how much ground a lot covers rather than about which board
  // this is — a self-hoster's pack gets the same treatment without naming it.
  const lotMetres = LOT * world.metresPerUnit;
  const lotIsABlock = lotMetres <= NEIGHBOURHOOD_LOT_METRES;

  /*
   * Base districts first, detail districts last.
   *
   * This used to be the whole of the layer's level of detail — `InstancedMesh.
   * count` draws the first N instances, so a merged board dropped every metro
   * building by lowering one number. It is no longer that: `updateBlocksDetail`
   * packs *every* district, base ones included, so what is drawn is a set and
   * not a prefix, and one number cannot express it.
   *
   * The sort stays for two reasons that are both still true. It is what makes
   * `base` — the count published as `userData.baseCount` — a meaningful number
   * at all, and it decides the order the seeded streams are drawn in, so
   * removing it would reshuffle every anonymous building on every board. There
   * is nothing here for `nightlights.ts` to learn either way: it reads this
   * mesh exactly as it is returned.
   *
   * A pack with no `detail` districts sorts to itself and pays nothing.
   */
  const ordered = [...world.city.districts].sort(
    (a, b) => Number(a.detail ?? false) - Number(b.detail ?? false),
  );
  /**
   * A district's centre in scene space, and how far its lattice has to reach.
   *
   * Lot-dependent only in the last term — the pad that keeps the rotated
   * lattice covering the corners once it is turned — so the range's own reach
   * is taken at the coarsest rung, which is the largest of them.
   */
  function frame(district: District, lot: number) {
    const b = polygonBounds(district.polygon);
    const corners = [
      world.project(b.minLat, b.minLng),
      world.project(b.minLat, b.maxLng),
      world.project(b.maxLat, b.minLng),
      world.project(b.maxLat, b.maxLng),
    ];
    const xs = corners.map((c) => c[0]);
    const zs = corners.map((c) => c[1]);
    const cx = (Math.min(...xs) + Math.max(...xs)) / 2;
    const cz = (Math.min(...zs) + Math.max(...zs)) / 2;
    return { cx, cz, reach: Math.hypot(Math.max(...xs) - cx, Math.max(...zs) - cz) + lot };
  }

  /**
   * One rung's walk of one district: step the lattice, keep what lands on
   * buildable ground, and return what survives.
   *
   * This is the loop that has always been here, lifted out of it — every line
   * in it is about the lattice and none of them is about which rung it is
   * walking. Boot calls it for the reference rung and every coarser one;
   * `updateBlocksDetail` calls it the first time a pose would pick a finer.
   * `lot` and `seed` are the whole difference between two calls.
   */
  function walkLots(district: District, lot: number, seed: number): Box[] {
    const lotBoxes: Box[] = [];
    const lotIsABlockHere = lot * world.metresPerUnit <= NEIGHBOURHOOD_LOT_METRES;
    const rand = seededRandom(seed);

    const palette = PALETTES[district.palette];
    const commercial = COMMERCIAL[district.palette];
    const angle = district.gridAngle;
    const coverage = district.coverage ?? 0.88;

    // The district's extent in scene space, padded so the rotated lattice
    // still covers the corners once it is turned.
    const { cx, cz, reach } = frame(district, lot);

    const cos = Math.cos(angle);
    const sin = Math.sin(angle);
    const steps = Math.ceil(reach / lot);

    for (let iu = -steps; iu <= steps; iu++) {
      if (lotIsABlockHere && ((iu % BLOCK_LOTS) + BLOCK_LOTS) % BLOCK_LOTS === 0) continue; // street
      for (let iv = -steps; iv <= steps; iv++) {
        if (lotIsABlockHere && ((iv % BLOCK_LOTS) + BLOCK_LOTS) % BLOCK_LOTS === 0) continue; // street

        const u = (iu + (rand() - 0.5) * 0.34) * lot;
        const v = (iv + (rand() - 0.5) * 0.34) * lot;
        const x = cx + u * cos - v * sin;
        const z = cz + u * sin + v * cos;

        const [lat, lng] = world.unproject(x, z);
        if (!world.pointInPolygon(lat, lng, district.polygon)) continue;
        /**
         * Land and parks come off the lattice; the district polygon does not.
         *
         * The three tests used to be three exhaustive polygon walks each, and
         * on the Bay Area's 186k candidate lots that was 240 ms of the boot's
         * main thread — the largest single item in it, spent re-deriving what
         * the heightfield Worker had already worked out for the whole board.
         * `isLandSampled` and `inParkSampled` read that answer and fall through
         * to the exact test only on a lattice cell that straddles the edge, so
         * the coastline and the park boundaries are still decided by the
         * polygons; see `World.sampled`. Same 186k lots, 19 ms.
         *
         * The district stays exact because there is no mask for it: districts
         * are not a property of the lattice, they overlap, and San Francisco
         * declares fifty-two of them. It is also the cheap one — the polygons
         * are a dozen vertices and the bounding box rejects almost everything,
         * which is 47 ms against the coastline's 235.
         */
        if (!world.isLandSampled(lat, lng)) continue;
        if (world.inParkSampled(lat, lng)) continue;
        if (rand() > coverage) continue; // yards, car parks, the unbuilt lots

        // Cubed, so tall buildings stay rare and the skyline keeps a
        // silhouette instead of turning into a plateau.
        const roll = rand();
        const isTower = rand() < district.towerChance;
        const t = isTower ? 0.55 + roll * 0.45 : roll ** 3;
        const heightM = district.minHeight + t * (district.maxHeight - district.minHeight);

        // Towers take several lots. A 260 m tower on one 40 m lot is a 25:1
        // needle, and downtown came out looking like a bed of nails; real
        // towers assemble their sites, and Salesforce Tower is about 5:1.
        let fill = isTower ? 1.5 + rand() * 0.7 : 0.78 + rand() * 0.18;
        /*
         * Those multipliers assemble a tower site from a neighbourhood lot.
         * On a 400 m lot they draw a 600–880 m cube — photographed at FiDi
         * from 2.5 km as the unidentified 700 m slab, rotated onto the
         * street grid, covering the buildings under it. A coarse lot is a
         * parcel: the building sits on it, it is not the building. Fine
         * rungs (40 / 80 m) are unchanged, because the cap is larger than
         * the fill they already use. Depth is capped too: the 0.85–1.15
         * stretch after fill would otherwise put a 260 m cap at 300 m.
         */
        const cap = NEIGHBOURHOOD_LOT_METRES / world.metresPerUnit;
        const width = Math.min(lot * fill, cap);
        const depth = Math.min(lot * fill * (0.85 + rand() * 0.3), cap);
        const rotation = angle + (rand() - 0.5) * 0.03;
        const color = new THREE.Color(
          palette[Math.floor(rand() * palette.length)] ?? 0xd9d3c6,
        );

        /**
         * Keep a named building legible instead of drawing it inside a random
         * one at the same address.
         *
         * Every random property is drawn before this test. The sequence is
         * load-bearing: skipping those calls for one reserved lot would
         * reshuffle every anonymous building after it and turn a local change
         * into a whole new skyline.
         */
        const radius = Math.hypot(width, depth) / 2;
        if (
          reservations.some(
            (reserved) => Math.hypot(x - reserved.x, z - reserved.z) < radius + reserved.radius,
          )
        ) {
          continue;
        }

        lotBoxes.push({
          x,
          z,
          y: world.groundAt(lat, lng),
          w: width,
          d: depth,
          h: world.metres(heightM),
          rot: rotation,
          color,
          // A tower is an office whatever district it landed in.
          commercial: Math.min(1, commercial + (isTower ? 0.4 : 0)),
        });
      }
    }
    return lotBoxes;
  }

  function emit(district: District, lot: number, seed: number): DetailTier {
    const start = boxes.length;
    const lotBoxes = walkLots(district, lot, seed);
    for (const box of lotBoxes) boxes.push(box);
    return { start, count: lotBoxes.length };
  }

  const coarsestTier = DETAIL_LOT_TIERS.length - 1;
  let baseBoxes = -1;
  for (const district of ordered) {
    if (baseBoxes < 0 && district.detail === true) baseBoxes = boxes.length;
    const districtStart = boxes.length;
    const seed = seedBase;
    seedBase += 7919;

    /*
     * A detail district is walked at the reference rung and every coarser one;
     * everything else keeps the scene-unit `LOT` it has always had, in the one
     * slot the coarse-fill below then hands to every rung, so no existing
     * board moves by a lot. Fine rungs wait for a pose that would pick them.
     */
    const tiers: (DetailTier | null)[] = DETAIL_LOT_TIERS.map(() => null);
    if (district.detail === true) {
      const metres = (t: number) => DETAIL_LOT_TIERS[t]! / world.metresPerUnit;
      const stream = (t: number) => seed + (t - DETAIL_LOT_REFERENCE) * TIER_SEED_STRIDE;
      /*
       * The reference rung first, because it is what the finer rungs are priced
       * against, and on its own unshifted seed, because it is the board as it
       * shipped and nothing about it may move.
       */
      const reference = emit(district, metres(DETAIL_LOT_REFERENCE), seed);
      tiers[DETAIL_LOT_REFERENCE] = reference;
      for (let t = DETAIL_LOT_REFERENCE + 1; t <= coarsestTier; t++) {
        tiers[t] = emit(district, metres(t), stream(t));
      }
      /*
       * Finer than the reference is priced the same way it always was, and
       * **not walked**. Lots go as the inverse square of their size — 160 m
       * builds 56,327 and 40 m would build 901,502 ungated — so the reference
       * count is enough to decide whether a district may ever have a 40 m
       * rung. Walking that rung for every downtown at boot is the second of
       * boot this file used to spend. `updateBlocksDetail` walks it the first
       * time the budget would pick it; a district that never comes that close
       * never spends it.
       */
    } else {
      tiers[coarsestTier] = emit(district, LOT, seed);
    }

    if (boxes.length > districtStart) {
      /*
       * **Coarse-fill.** A district that was refused a fine rung still has to
       * answer when the board asks for one, and the answer is its own finest
       * built rung. So the ladder is flattened here, once, into an array with
       * an entry at every index: walking from the coarse end inwards, each
       * index takes the nearest built rung that is no finer than it, and a
       * second pass fine-ward covers the district whose coarse rungs all came
       * out empty. `updateBlocksDetail` then indexes it directly and never has
       * to know which rungs a district owns.
       *
       * That is also what makes a mixed frame legal: standing over downtown Los
       * Angeles at the 80 m rung, Downtown and Koreatown are drawn at 80 m and
       * West Covina — which has no 80 m rung, being 4,009 lots at 160 — is drawn
       * at 160. It is fifteen kilometres away and the difference does not read.
       */
      const filled = fillTiers(tiers, districtStart);

      /*
       * The vertical extent, walked here because the horizontal reach is not it.
       *
       * `reach` is the half-diagonal of the district's own lat/lng bounding box
       * plus a lot, and it is a **plan** radius: measured over all 59,166
       * instances, every lot on the merged board is inside its district's
       * `reach` of the centre with metres to spare. It was used as a 3-D radius
       * on a sphere pinned at y=0, and that is a different claim — one this
       * board does not support. A lot's local y is the ground it stands on
       * times `verticalExaggeration`, which is 15 here, so a district in the San
       * Gabriel foothills sits kilometres of scene height above the plane its
       * cull sphere was centred on. **39 of 97 districts had lots outside their
       * own sphere**, the worst by 4.78 units — 9.2 km — and the failure that
       * produces is buildings on a hillside vanishing while they are on screen.
       *
       * So the test is an axis-aligned box and not a sphere. It costs the same
       * — `Frustum.intersectsBox` is the same six plane tests as
       * `intersectsSphere` — and it is *exact* for the volume being described
       * rather than conservative in x/z and wrong in y. The walk covers every
       * rung of the district at once, which is what it has to do: the box is
       * tested before the rung is chosen, so it has to contain all of them.
       */
      let y0 = Infinity;
      let y1 = -Infinity;
      for (let k = districtStart; k < boxes.length; k++) {
        const box = boxes[k]!;
        if (box.y < y0) y0 = box.y;
        if (box.y + box.h > y1) y1 = box.y + box.h;
      }
      const widest = district.detail === true
        ? DETAIL_LOT_TIERS[coarsestTier]! / world.metresPerUnit
        : LOT;
      const { cx, cz, reach } = frame(district, widest);
      districtRanges.push({
        x: cx,
        z: cz,
        r: reach,
        y0,
        y1,
        detail: district.detail === true,
        tiers: filled,
        ...(district.detail === true
          ? {
              owned: tiers.slice(),
              district,
              seed,
              referenceCount: tiers[DETAIL_LOT_REFERENCE]?.count ?? 0,
            }
          : {}),
      });
    }
  }

  const geometry = new THREE.BoxGeometry(1, 1, 1);
  geometry.translate(0, 0.5, 0); // pivot at the base, so y is ground level
  if (!lotIsABlock) {
    /**
     * Drop the underside at neighbourhood scale.
     *
     * `BoxGeometry` lays its groups out px, nx, py, ny, pz, nz, two triangles
     * each, so the six indices from 18 are the floor. A building sits on the
     * ground and that face is never visible — except on San Francisco's
     * steepest blocks, where a 40 m lot spanning a 3.6×-exaggerated hillside can
     * leave a corner clear of the terrain and you would see straight through the
     * hole. So this is tied to the same measurement as the street lattice and
     * the shadow pass: at 806 m to the lot the ground under a building is flat
     * to within a hair and nothing can get beneath it, and one sixth of the
     * board's largest triangle consumer goes back to the budget.
     *
     * **The ladder does not reach this decision and must not.** Both of the
     * board-wide calls — this one and the shadow pass below — are made from
     * `LOT × metresPerUnit`, which is 806 m on the merged board and does not
     * move when a district is re-lotted; a detail district's rung has never
     * been what decides them. That is deliberate rather than incidental: the
     * finest rung on the ladder is 40 m, and deriving the shadow pass from the
     * rung in force would put a hundred thousand triangles into a second pass
     * the moment the camera came in over San Francisco — the one pose with the
     * least budget to spare. What the finer rungs *do* change is the overhang
     * argument above, and they change it in the safe direction: a smaller lot
     * is a smaller building and a shorter span over the slope, so if 160 m
     * never showed daylight under a wall, 80 m and 40 m cannot start.
     */
    const index = geometry.getIndex();
    if (index) {
      const kept = Array.from(index.array).filter((_, at) => at < 18 || at >= 24);
      geometry.setIndex(kept);
    }
  }

  /*
   * Where the base set ends in the *source* order. `boxes.length` when the pack
   * declares no detail districts, which is what makes "show everything" the
   * behaviour of every board that is not the merged one — such a board gets no
   * store below and its `count` is never touched again.
   *
   * It is no longer a draw boundary: what is drawn is `DetailStore.ranges`
   * filtered by reach and frustum, each district at whichever rung the board
   * chose. Published because it is still the honest answer to "how much of this
   * board is the base pack's own", which is the first thing anyone asks of a
   * merged board; nothing in the engine reads it any more.
   */
  const base = baseBoxes < 0 ? boxes.length : baseBoxes;
  const hasDetail = base < boxes.length;

  /**
   * How many instances the mesh has room for — **and it is no longer
   * `boxes.length`**, because only one rung of each district is ever drawn
   * and the fine rungs are not in `boxes` at boot.
   *
   * `InstancedMesh` fixes its capacity at construction. A packed set is at
   * most `LOT_BUDGET`, except when even the coarsest rung is over that, in
   * which case the coarsest rung is the fallback and must fit. Fine rungs
   * append to the store later, but they still pack through the budget, so
   * the live buffer does not grow.
   */
  let capacity = boxes.length;
  if (hasDetail) {
    /*
     * Fine rungs are not in `boxes` yet. A packed set is still at most
     * `LOT_BUDGET` (or the whole coarsest rung if even that is over), so the
     * mesh has to be able to hold the budget even though boot never walked
     * 40 m. On the merged board this is still 19,000 — the coarsest rung is
     * 18,742 and the budget is the larger number — same allocation as before.
     */
    const coarsestLots = districtRanges.reduce(
      (n, r) => n + r.tiers[coarsestTier]!.count,
      0,
    );
    capacity = Math.max(coarsestLots, LOT_BUDGET);
  }

  // The per-instance facade data, drawn from a stream of its own.
  //
  // The obvious place for the seed is inside the placement loop, next to every
  // other `rand()` — and putting it there would have been a mistake, because a
  // scatter's draw sequence is load-bearing. One extra call shifts every
  // subsequent draw, and the whole city would have rebuilt itself the first
  // time anyone lit a window. A second stream costs nothing, is just as
  // deterministic across reloads, and leaves the skyline exactly where it was.
  const windows = seededRandom(FACADE_SEED);
  const srcFacade = new Float32Array(boxes.length * 2);
  boxes.forEach((b, i) => {
    srcFacade[i * 2] = b.commercial;
    srcFacade[i * 2 + 1] = windows();
  });

  /*
   * The three per-instance attributes, composed once over every rung.
   *
   * They used to be composed straight into the mesh and the store copied back
   * out of it, which was the honest way round while the mesh held exactly the
   * lots that existed. It cannot be that way round now: `boxes` is several
   * times the size of the buffers the mesh is allowed to allocate. So the
   * source arrays are the primary copy and the mesh is filled *from* them —
   * wholesale on a board with no ladder, and a district at a time by
   * `updateBlocksDetail` on one with.
   */
  const srcMatrix = new Float32Array(boxes.length * 16);
  const srcColor = new Float32Array(boxes.length * 3);
  {
    const matrix = new THREE.Matrix4();
    const quat = new THREE.Quaternion();
    const pos = new THREE.Vector3();
    const scl = new THREE.Vector3();
    const up = new THREE.Vector3(0, 1, 0);
    boxes.forEach((b, i) => {
      pos.set(b.x, b.y, b.z);
      quat.setFromAxisAngle(up, b.rot);
      scl.set(b.w, b.h, b.d);
      matrix.compose(pos, quat, scl);
      matrix.toArray(srcMatrix, i * 16);
      srcColor[i * 3] = b.color.r;
      srcColor[i * 3 + 1] = b.color.g;
      srcColor[i * 3 + 2] = b.color.b;
    });
  }

  const facade = new THREE.InstancedBufferAttribute(new Float32Array(capacity * 2), 2);
  geometry.setAttribute(FACADE_ATTRIBUTE, facade);

  const mesh = new THREE.InstancedMesh(geometry, new THREE.MeshLambertMaterial(), capacity);
  mesh.name = "blocks";
  mesh.castShadow = blocksCastShadow(lotIsABlock);
  mesh.receiveShadow = true;
  // What `setColorAt` allocates on its first call, allocated up front because
  // the fill below is a buffer copy and not a per-instance write.
  mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(capacity * 3), 3);

  mesh.userData.baseCount = base;

  if (!hasDetail) {
    (mesh.instanceMatrix.array as Float32Array).set(srcMatrix);
    (mesh.instanceColor.array as Float32Array).set(srcColor);
    (facade.array as Float32Array).set(srcFacade);
    mesh.instanceMatrix.needsUpdate = true;
    mesh.instanceColor.needsUpdate = true;
    facade.needsUpdate = true;
    return mesh;
  }

  /*
   * Every lot of every rung, kept so the live buffers can be re-packed by
   * district and by rung.
   *
   * **Why a copy rather than a prefix.** `InstancedMesh.count` draws the first N
   * instances, so an ordered "base first, detail last" layout can express "no
   * cities" and "every city" and nothing in between. On the merged board that
   * is the difference between 348,271 triangles and 925,854 against a 430,000
   * cap: driving into Los Angeles revealed San Francisco's fifty-two districts
   * as well, four hundred kilometres away and behind the camera.
   *
   * So the visible districts are copied into the front of the live buffers when
   * the visible *set* changes — not per frame, and it changes on the order of
   * once a journey. Three attributes move together and they must: the matrix,
   * the colour and `FACADE_ATTRIBUTE`, which is per-instance and is what decides
   * which windows are lit. Moving the matrices alone would light a tower's
   * windows on a warehouse.
   *
   * **The copy holds the rungs boot walked**, which is the reference and every
   * coarser one. Fine rungs append here the first time a pose would pick them,
   * so a district you never stand over at 2 km never spends the walk. The live
   * mesh is still packed from this copy; only the copy grows.
   */
  const store: DetailStore = {
    ranges: districtRanges,
    srcMatrix,
    srcColor,
    srcFacade,
    key: null,
    tier: coarsestTier,
    // Nothing has been packed yet, so nothing is being drawn at any rung. `null`
    // rather than `coarsestTier` for the same reason the field exists.
    detailTier: null,
    walkLots,
    windows,
    metresPerUnit: world.metresPerUnit,
  };
  mesh.userData.detail = store;
  /*
   * **Born packed.** `InstancedMesh`'s constructor sets `count` to its
   * capacity, so between this function returning and the scene's first
   * `applyDetailLod` the mesh draws *every* detail lot — and the budget
   * harness reports `maxTriangles`, a max over its whole sample window, so it
   * caught that as 911,541 triangles against a 440,000 cap while the steady
   * state was a correct 348,271. A level of detail that is right on every
   * frame but the first is not a level of detail; it is a spike with a good
   * explanation.
   *
   * Zero rather than `base` since the base lots stopped being a prefix: the
   * first `applyDetailLod` packs them like any other district. Zero is still
   * strictly below the capacity the constructor set, which is the only
   * property this line has ever needed.
   */
  mesh.count = 0;

  return mesh;
}

/** How many lots the store is holding — every walked rung, not the packed set. */
export function storedLotCount(mesh: THREE.InstancedMesh): number {
  const store = mesh.userData.detail as DetailStore | undefined;
  if (store === undefined) return mesh.count;
  return (store.srcMatrix.length / 16) | 0;
}

/**
 * One district's lots, and the box that contains them.
 *
 * `r` is the **plan** radius: the half-diagonal of the district's own lat/lng
 * bounding box plus a lot. Measured across the 89 detail districts that emit
 * lots it runs from **1.06 km to 15.53 km, median 3.82 km**, and every lot on
 * the board is inside its own district's `r` of `(x, z)`. (The pack declares 99
 * detail districts; ten emit no lot at all — every candidate falls in water, in
 * a park or outside the coverage roll — and carry no range. The eight base
 * districts are far larger again, which is why a whole-board pose still draws
 * everything.)
 *
 * `y0`/`y1` are the vertical extent, and they exist because `r` is not it. A
 * lot's local y is the ground under it times `verticalExaggeration` — 15 on the
 * merged board — so `r` describes the district's footprint and says nothing
 * about how far up the mountain it is. The two together are an axis-aligned box
 * and that box is what the frustum test runs against; see the walk in
 * `createBlocks` for the measurement that made this necessary. It is tested
 * *before* a rung is chosen, so it contains every rung of the district and not
 * only the one that will be drawn.
 */
interface DetailRange {
  x: number;
  z: number;
  r: number;
  /** Lowest ground any lot stands on, in local scene units. */
  y0: number;
  /** Highest roof, in local scene units. */
  y1: number;
  /** Metro detail, revealed by stand-off; `false` for the base pack's own. */
  detail: boolean;
  /**
   * What this district draws at each rung of `DETAIL_LOT_TIERS`, indexed by
   * rung and **dense** — a district that was refused a fine rung repeats its
   * finest built one there, so this can be indexed without a check. See the
   * coarse-fill in `createBlocks`.
   *
   * A base district has one lattice and every index is it.
   */
  tiers: DetailTier[];
  /**
   * The rungs this district has actually walked, sparse. Missing fine rungs
   * are walked on demand; `tiers` is the coarse-fill of this. Absent on a
   * base district, which has one lattice.
   */
  owned?: (DetailTier | null)[];
  district?: District;
  seed?: number;
  /** Lot count at `DETAIL_LOT_REFERENCE`, for pricing a finer rung. */
  referenceCount?: number;
}

/** Where one rung of one district's lots sit in the store's arrays. */
interface DetailTier {
  start: number;
  count: number;
}

/**
 * Flatten a sparse owned-rung list into a dense ladder.
 *
 * A district that was refused a fine rung still has to answer when the board
 * asks for one, and the answer is its own finest built rung. Walking from the
 * coarse end inwards, each index takes the nearest built rung that is no
 * finer than it; a second pass fine-ward covers a district whose coarse rungs
 * all came out empty.
 */
function fillTiers(owned: (DetailTier | null)[], fallbackStart: number): DetailTier[] {
  const coarsest = DETAIL_LOT_TIERS.length - 1;
  const filled: DetailTier[] = [];
  let carry: DetailTier | null = null;
  for (let t = coarsest; t >= 0; t--) {
    const own = owned[t] ?? null;
    if (own !== null && own.count > 0) carry = own;
    filled[t] = carry ?? { start: fallbackStart, count: 0 };
  }
  let back: DetailTier | null = null;
  for (let t = 0; t <= coarsest; t++) {
    const here = filled[t]!;
    if (here.count > 0) back = here;
    else if (back !== null) filled[t] = back;
  }
  return filled;
}

function growFloat32(src: Float32Array, extra: number): Float32Array {
  const next = new Float32Array(src.length + extra);
  next.set(src);
  return next;
}

function composeLots(store: DetailStore, more: readonly Box[]): DetailTier {
  const start = (store.srcMatrix.length / 16) | 0;
  const n = more.length;
  if (n === 0) return { start, count: 0 };
  const srcMatrix = growFloat32(store.srcMatrix, n * 16);
  const srcColor = growFloat32(store.srcColor, n * 3);
  const srcFacade = growFloat32(store.srcFacade, n * 2);
  const windows = store.windows ?? (() => 0);
  const matrix = new THREE.Matrix4();
  const quat = new THREE.Quaternion();
  const pos = new THREE.Vector3();
  const scl = new THREE.Vector3();
  const up = new THREE.Vector3(0, 1, 0);
  more.forEach((b, i) => {
    const at = start + i;
    pos.set(b.x, b.y, b.z);
    quat.setFromAxisAngle(up, b.rot);
    scl.set(b.w, b.h, b.d);
    matrix.compose(pos, quat, scl);
    matrix.toArray(srcMatrix, at * 16);
    srcColor[at * 3] = b.color.r;
    srcColor[at * 3 + 1] = b.color.g;
    srcColor[at * 3 + 2] = b.color.b;
    srcFacade[at * 2] = b.commercial;
    srcFacade[at * 2 + 1] = windows();
  });
  store.srcMatrix = srcMatrix;
  store.srcColor = srcColor;
  store.srcFacade = srcFacade;
  return { start, count: n };
}

interface DetailStore {
  ranges: DetailRange[];
  srcMatrix: Float32Array;
  srcColor: Float32Array;
  srcFacade: Float32Array;
  /**
   * The visible set the buffers currently hold, as a key — and `null` until the
   * first pack, which is not the same as "empty".
   *
   * It was `""` and that was a bug with a 563,000-triangle blast radius: an
   * empty visible set produces the empty string too, so the very first call —
   * the whole-board pose, where nothing should be drawn — compared equal, took
   * the early return, and left `count` at the value the `InstancedMesh`
   * constructor gave it, which is *every* instance. The board measured 911,541
   * triangles against a 440,000 cap while the code that was supposed to prevent
   * exactly that ran and did nothing.
   *
   * The key is built from the packed rungs' `start` offsets, which are unique
   * across the whole store, so it identifies the rung as well as the district
   * and a change of rung alone re-packs.
   */
  key: string | null;
  /**
   * The rung the board settled on last time it packed. Nothing reads it back:
   * the choice is a pure function of the visible set and the budget, so this is
   * a readout rather than carried state — `__teraCamera.lots()` reports it, and
   * "did the board coarsen when I pulled back" is not a question a triangle
   * total can answer, because at these poses the terrain is more than half of
   * that total.
   */
  tier: number;
  /**
   * The rung, but only when a **detail** district is actually being drawn at it.
   *
   * `tier` is an index into every range's `tiers` array and is always valid,
   * because the coarse fill gives base districts an entry at every rung. That
   * makes it the right thing to index with and the wrong thing to report: at a
   * pose where the stand-off has switched metro detail off entirely, the budget
   * scan still finds that the handful of visible base districts fit at the
   * finest rung, sets `tier` to 0, and the instrument printed "40 m" for a board
   * drawing nothing but 806 m base lots. The engine was right and the readout
   * was not — which is worse than the reverse, because the readout is what the
   * next decision gets made on.
   */
  detailTier: number | null;
  /**
   * Enough to walk a finer rung after boot: the world the lots stand on, the
   * lattice walker, and the facade stream continued from where boot left it.
   * Absent on a board with no detail districts.
   */
  walkLots?: (district: District, lot: number, seed: number) => Box[];
  windows?: () => number;
  metresPerUnit?: number;
}

/**
 * How much further than its own reach a district is kept packed for, as a
 * **fraction of the camera's stand-off**.
 *
 * It exists to cover one frame of lag and nothing else: `scene.ts` calls
 * `applyDetailLod` before `kit.tick`, so the frustum this function is handed is
 * the one from the pose the camera held last frame, and a district entering
 * frame at speed would otherwise pop in a frame late.
 *
 * A *fraction* rather than a distance because the two cases it has to cover are
 * four hundred kilometres apart: a chapter flight crossing the state at 400 km
 * of stand-off moves a long way per frame, and an orbit at 2.5 km moves almost
 * nothing. One proportional number covers both; a fixed metre pad has to be
 * sized for the first and is then enormous at the second. Measured against a
 * fixed 4 km pad at 2.5 km over Los Angeles: the fixed pad packs **8,079 extra
 * lots** — 80,790 triangles — for nothing, because the median district reach is
 * 3.82 km, while 2% of the stand-off packs the same set as no pad at all. At
 * 7.7 km over the same city the 2% pad is worth 966 instances against no pad,
 * which is what it is for.
 */
const DETAIL_FRUSTUM_PAD = 0.02;

function estimatedCount(range: DetailRange, t: number): number {
  const owned = range.owned?.[t];
  if (owned) return owned.count;
  if (range.owned && t < DETAIL_LOT_REFERENCE && range.referenceCount !== undefined) {
    const finer = DETAIL_LOT_TIERS[DETAIL_LOT_REFERENCE]! / DETAIL_LOT_TIERS[t]!;
    const estimate = range.referenceCount * finer * finer;
    if (estimate > TIER_DISTRICT_LOTS) return range.tiers[t]!.count;
    return estimate;
  }
  return range.tiers[t]!.count;
}

/**
 * Walk any finer rung the budget is about to pick, for the districts in frame.
 *
 * Boot left those rungs sparse. The first pose whose visible sum would fit at
 * 40 m or 80 m pays the walk; every later pose on the same districts reuses it.
 */
function ensureFineRungs(store: DetailStore, visible: DetailRange[], wanted: number): void {
  if (wanted >= DETAIL_LOT_REFERENCE) return;
  const walk = store.walkLots;
  const mpu = store.metresPerUnit;
  if (!walk || mpu === undefined || mpu <= 0) return;
  const coarsest = DETAIL_LOT_TIERS.length - 1;
  for (const range of visible) {
    const owned = range.owned;
    const district = range.district;
    if (!owned || !district || range.seed === undefined || range.referenceCount === undefined) {
      continue;
    }
    let grew = false;
    for (let t = DETAIL_LOT_REFERENCE - 1; t >= wanted; t--) {
      if (owned[t]) continue;
      const finer = DETAIL_LOT_TIERS[DETAIL_LOT_REFERENCE]! / DETAIL_LOT_TIERS[t]!;
      if (range.referenceCount * finer * finer > TIER_DISTRICT_LOTS) break;
      const more = walk(
        district,
        DETAIL_LOT_TIERS[t]! / mpu,
        range.seed + (t - DETAIL_LOT_REFERENCE) * TIER_SEED_STRIDE,
      );
      owned[t] = composeLots(store, more);
      for (const box of more) {
        if (box.y < range.y0) range.y0 = box.y;
        if (box.y + box.h > range.y1) range.y1 = box.y + box.h;
      }
      grew = true;
    }
    if (grew) {
      const fallback = owned[coarsest]?.start ?? range.tiers[coarsest]!.start;
      range.tiers = fillTiers(owned, fallback);
    }
  }
}

/** Held rather than allocated: the frustum test runs once per district. */
const REACH_BOX = new THREE.Box3();

/**
 * Draw the cities near `(x, z)` and inside `view`, and no others.
 *
 * Called from the scene's frame loop, and cheap on every frame that changes
 * nothing: it decides the visible set, compares it to the last one as a string
 * key, and returns before touching a buffer if they match. A journey across
 * California changes that set a handful of times.
 *
 * When it does change, the visible districts' lots are copied into the front of
 * the instance buffers and `count` is moved. That is a `Float32Array.set` per
 * district — about 59,000 instances at the very most, which is a few
 * milliseconds on the one frame it happens, against a saving of over half a
 * million triangles on every frame in between.
 *
 * **`view` is the frustum culling, and it is free.** three culls per *object*,
 * and this whole layer is one `InstancedMesh` whose bounding sphere contains
 * California, so nothing about it is ever rejected at any pose. Testing each
 * district's own sphere here removes the same geometry the renderer would have
 * removed if the city had been split into meshes — and removes it for **zero
 * extra draw calls**, where a split would have cost one per chunk.
 * `WebGLIndexedBufferRenderer` returns immediately on `primcount === 0`, so a
 * repack down to nothing is *cheaper* than a draw call, not equal to one.
 *
 * Measured on the merged board, at 2.5 km of stand-off:
 *
 * | pose | detail lots | base lots |
 * |---|---|---|
 * | San Francisco, reach only | 3,546 | 2,839 |
 * | San Francisco, reach ∧ frustum | **1,987** | **0** |
 * | Los Angeles, reach only | 48,081 | 2,839 |
 * | Los Angeles, reach ∧ frustum | **15,067** | **0** |
 *
 * The Los Angeles row is the one that matters: 48,081 lots is 480,810 triangles
 * against a 400,000 cap, on a pose no budget cell stands at, which is why
 * nothing had ever measured it. It was a live breach and this closes it.
 *
 * `view` is optional and its absence means today's behaviour exactly — reach
 * alone, every base district drawn. That is what every board without detail
 * districts gets, and what the one up-front call at scene build gets, so the
 * opening frame cannot be short of a district because of where the camera
 * happened to be pointing before the first tick.
 *
 * `standoffUnits` only scales `DETAIL_FRUSTUM_PAD`; at 0 the pad is 0.
 *
 * **`lotBudget` is the second half of the level of detail and the newer one.**
 * The frustum decides *which* districts are drawn; the budget decides how
 * finely each of them is drawn, by picking the finest rung of
 * `DETAIL_LOT_TIERS` whose visible sum fits in it. It defaults to
 * `LOT_BUDGET` and is clamped to it, because that is the number the mesh's
 * capacity was allocated against and no caller may spend past it. It is a
 * parameter at all so a test can drive the ladder without staging a pose that
 * happens to land on the rung it wants to see.
 */
export function updateBlocksDetail(
  mesh: THREE.InstancedMesh,
  x: number,
  z: number,
  reachUnits: number,
  view?: THREE.Frustum,
  standoffUnits = 0,
  lotBudget = LOT_BUDGET,
): void {
  const store = mesh.userData.detail as DetailStore | undefined;
  if (store === undefined) return;
  const reach2 = reachUnits * reachUnits;
  const pad = DETAIL_FRUSTUM_PAD * standoffUnits;
  const visible = store.ranges.filter((r) => {
    if (r.detail && (reachUnits <= 0 || (x - r.x) ** 2 + (z - r.z) ** 2 >= reach2)) return false;
    if (view === undefined) return true;
    /*
     * The pad goes on every axis, including the vertical, because the vertical
     * is where the camera's own motion is least constrained: a descent changes
     * altitude far faster than it changes the ground point being looked at.
     */
    REACH_BOX.min.set(r.x - r.r - pad, r.y0 - pad, r.z - r.r - pad);
    REACH_BOX.max.set(r.x + r.r + pad, r.y1 + pad, r.z + r.r + pad);
    return view.intersectsBox(REACH_BOX);
  });
  /**
   * Which rung of the ladder the whole board stands on: the finest one whose
   * visible lots fit the budget, and the coarsest rung if none of them do.
   *
   * One rung for the board rather than one per district, and that is a
   * deliberate choice about *what* the level of detail is. A per-district rung
   * chosen from the district's own distance is the textbook answer and it is
   * the wrong one here, because the poses this exists for are not close: the
   * camera at "2.5 km over San Francisco" is a lifted, tilted seat 22.6 km from
   * what it is looking at, and every district in frame is that same 22.6 km
   * away. Distance cannot tell those districts apart, and it cannot tell San
   * Francisco from Los Angeles either — the two cities are the same stand-off
   * and an eleven-fold difference in bill. How much city is in the frame is
   * what tells them apart, and the visible sum is exactly that.
   *
   * **No hysteresis, and that is worth saying because there was some.** A dead
   * band is the reflex here, since stepping down a rung replaces every building
   * in frame; and with the budget a constant there is nothing for it to damp.
   * The rung is a pure function of the visible set, the visible set changes
   * discretely, and a change of visible set already re-packs the buffers — so
   * the only thing a margin bought was a bias towards the rung the board
   * happened to start on. It cost real ground while it was in: at 15% it left
   * Los Angeles at 400 m lots on poses that measure 375,132 and 380,698
   * triangles at 250 m, because 17,131 lots is under the 19,000 budget and over
   * 85% of it. What is left is honest about its own pop; see the note in
   * `DETAIL_LOT_TIERS`.
   *
   * The sums are recomputed rather than cached: it is one addition per visible
   * district per rung — under six hundred on the merged board's worst pose —
   * and only on a frame where the visible set already changed.
   */
  const budget = Math.min(lotBudget, LOT_BUDGET);
  const coarsest = DETAIL_LOT_TIERS.length - 1;
  const sumEst = (t: number) => {
    let n = 0;
    for (const r of visible) n += estimatedCount(r, t);
    return n;
  };
  let want = coarsest;
  for (let t = 0; t < coarsest; t++) {
    if (sumEst(t) <= budget) {
      want = t;
      break;
    }
  }
  ensureFineRungs(store, visible, want);
  const sumAt = (t: number) => {
    let n = 0;
    for (const r of visible) n += r.tiers[t]!.count;
    return n;
  };
  let tier = coarsest;
  for (let t = 0; t < coarsest; t++) {
    if (sumAt(t) <= budget) {
      tier = t;
      break;
    }
  }
  store.tier = tier;
  store.detailTier = visible.some((r) => r.detail) ? tier : null;

  /*
   * `start` is unique across the whole store — every rung of every district has
   * its own run of `boxes` — so the packed rungs' offsets identify the rung as
   * well as the district, and a change of rung with the same districts in frame
   * produces a different key and re-packs.
   */
  const packed = visible.map((r) => r.tiers[tier]!);
  const key = packed.map((t) => t.start).join(",");
  if (key === store.key) return;
  store.key = key;

  const matrix = mesh.instanceMatrix.array as Float32Array;
  const colour = mesh.instanceColor?.array as Float32Array | undefined;
  const facade = mesh.geometry.getAttribute(FACADE_ATTRIBUTE);
  const facadeArray = facade?.array as Float32Array | undefined;

  let at = 0;
  for (const r of packed) {
    matrix.set(store.srcMatrix.subarray(r.start * 16, (r.start + r.count) * 16), at * 16);
    if (colour !== undefined) {
      colour.set(store.srcColor.subarray(r.start * 3, (r.start + r.count) * 3), at * 3);
    }
    if (facadeArray !== undefined) {
      facadeArray.set(store.srcFacade.subarray(r.start * 2, (r.start + r.count) * 2), at * 2);
    }
    at += r.count;
  }
  mesh.count = at;
  /**
   * **Not hygiene: the sphere is a hazard now that the packing moves.**
   *
   * `Frustum.intersectsObject` sees `InstancedMesh.boundingSphere` as defined
   * (it is `null`), computes it **once** over whatever `count` held at that
   * moment, and never recomputes. While the base set was a fixed prefix the
   * first sphere covered the state and was harmlessly conservative forever.
   * With a repacked set, a sphere computed while the camera stood over San
   * Francisco would be a sphere around San Francisco — and would then hide Los
   * Angeles the entire way down the state. Nulling it restores lazy recompute.
   */
  mesh.boundingSphere = null;
  /*
   * Only the packed prefix is uploaded. three merges adjacent ranges in
   * `WebGLAttributes.updateBuffer` and clears them after the upload, so an
   * orbit that re-packs repeatedly moves `at` instances rather than the whole
   * 3.8 MB matrix buffer every time the visible set changes.
   */
  mesh.instanceMatrix.addUpdateRange(0, at * 16);
  mesh.instanceMatrix.needsUpdate = true;
  if (mesh.instanceColor) {
    mesh.instanceColor.addUpdateRange(0, at * 3);
    mesh.instanceColor.needsUpdate = true;
  }
  if (facade) {
    // `createBlocks` sets this as an `InstancedBufferAttribute`; the narrowing
    // is for the interleaved case the getter's type admits and this file never
    // builds, which has no partial upload of its own.
    if (facade instanceof THREE.BufferAttribute) facade.addUpdateRange(0, at * 2);
    facade.needsUpdate = true;
  }
}

/**
 * The named buildings. Separate meshes because the eye goes looking for these
 * specific silhouettes — a pyramid at Montgomery, a white finger on Telegraph
 * Hill, the red tripod on the ridge — and a box would not do.
 */
export function createLandmarks(
  world: World,
  reservations: readonly BuildingReservation[] = [],
): THREE.Group {
  const group = new THREE.Group();
  group.name = "landmarks";

  for (const lm of world.city.landmarks) {
    // Tagged rather than sorted: landmarks are one mesh each, so the lever here
    // is visibility and not a count. `scene.ts` reads `userData.detail`.
    const [x, z] = world.project(lm.lat, lm.lng);
    // A richer stable glyph at this address supersedes the coarse landmark
    // primitive. Drawing both would hide the glyph inside the old mesh and
    // leave two different sources claiming the same real building.
    if (
      reservations.some(
        (reserved) => Math.hypot(x - reserved.x, z - reserved.z) < reserved.radius,
      )
    ) {
      continue;
    }
    const base = world.groundAt(lm.lat, lm.lng);
    const h = world.metres(lm.height);
    const w = lm.footprint * world.lngScale * 2;

    let geo: THREE.BufferGeometry;
    switch (lm.shape) {
      case "pyramid":
        geo = new THREE.ConeGeometry(w * 0.72, h, 4);
        geo.translate(0, h / 2, 0);
        geo.rotateY(Math.PI / 4);
        break;
      case "cylinder":
        geo = new THREE.CylinderGeometry(w * 0.6, w * 0.68, h, 20);
        geo.translate(0, h / 2, 0);
        break;
      case "tower":
        geo = new THREE.CylinderGeometry(w * 0.42, w * 0.62, h, 4);
        geo.rotateY(Math.PI / 4);
        geo.translate(0, h / 2, 0);
        break;
      default:
        geo = new THREE.BoxGeometry(w, h, w);
        geo.translate(0, h / 2, 0);
    }

    const mesh = new THREE.Mesh(
      geo,
      new THREE.MeshLambertMaterial({ color: lm.color ?? 0xaebac6 }),
    );
    mesh.position.set(x, base, z);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    mesh.userData.landmark = lm;
    if (lm.detail === true) mesh.userData.detail = true;
    group.add(mesh);
  }

  return group;
}
