diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,100 @@
 The serialization format carries its own version tag, independent of the package
 version; any change to it is recorded here explicitly.
 
-## Unreleased
+## 1.1.0.0 - 2026-09-23
 
+* Require `moonlight-algebra >= 0.1.1`, the first release carrying the
+  public `join` sublibrary the construction kernels depend on.
+* `tested-with` names GHC 9.14.1 alone. Since 1.0.0.0 the package has not
+  solved on 9.8, 9.10 or 9.12: ungated components require `base >= 4.22`,
+  and so does every `moonlight-algebra` library.
+
+* Nearest queries minimize exact squared distance and then resident handle.
+  Constrained meshes use complete site selection; Delaunay ties no longer stop
+  at a merely local minimum handle. Real-constructor regressions cover hidden
+  constrained minima, cocircular ties and rounded binary64 near-ties.
+* Move convex admission, hulls, observations and errors to `Moonlight.Planar.Convex`
+  in the focused DCEL component. Remove morphology-owned convex authoring and the
+  duplicate generator dual carrier; the generator now owns `PowerDualEdge`.
+  Foreign raw convex admission has its own obstruction code 207.
+* Add restricted exact prepared power-mass sections, with quadratic areas,
+  complete strict cell/window certificates, empty-cell witnesses and exact open
+  interval expiry. Existing regular-site dispositions remain unchanged.
+* Encode V6 through storage-owned logical page folds without gathering shared
+  stores. Wire bytes, default payloads and field-major topology order are unchanged.
+* Compute sign-only lifted predicates using positive integer row scaling.
+  Rational determinant magnitudes remain authoritative for volume ordering.
+
+* Finish the package rename inside the package (2026-09-04). The module
+  namespace `Moonlight.Triangulation` and everything beneath it is now
+  `Moonlight.Planar`, and every test suite, benchmark, and executable
+  component named `moonlight-triangulation-*` is now `moonlight-planar-*`.
+  The C ABI is untouched: the `moonlight-triangulation-c` foreign library,
+  the `moonlight_triangulation.h` header, the exported symbol names, and the
+  Python, TypeScript, and Rust binding package names all keep their names,
+  because the bindings consume them by name.
+
+* Surgery on repeated validation (2026-09-03). A `Triangulation` is
+  admitted once, at its trust boundary: `refine` no longer re-validates
+  admitted input, local refinement no longer re-walks its closure after
+  the transaction, overlay residency no longer re-checks what the
+  constrained value certifies, and the region valuation sweep plans its
+  events once. The closure statistics are renamed for what they count
+  (`ClosureStats`, `closureFaces`, `refinementClosureStats`).
+* `CanonicalAdmission` is a lineage witness carried on the value: the
+  normal form's owner mints it, decode verifies it in one linear pass
+  over the arenas, and canonical publication of a value already in normal
+  form is the identity. No serialization format change.
+* The fixed-width orientation worker had never run (`MachDeps.h` was not
+  included, so the width test silently fell back). It runs now, and a
+  fixed-width in-circle worker joins it, cross-checked against the
+  arbitrary-precision determinant on 15,000 quadrilaterals.
+* `segmentRelationWith` decides collinearity before shared endpoints:
+  collinear segments overlapping for a positive length are an overlap
+  whether or not they share an endpoint; a straight continuation stays a
+  shared endpoint. The frozen relation oracle gains eight contact families.
+* `ExactLengthExpression` is a radical normal form: rational squares fold
+  into the coefficient, and radicands in one square class merge onto their
+  common kernel. The term field is `lengthRadicand :: Integer`
+  (was `squaredLength :: ExactRational`).
+* The facade is narrowed to the value algebra; statistics, sessions,
+  incremental bulk insertion, hint-seeded location and interpolation move
+  to `Moonlight.Planar.Telemetry`. `polygonInset` is retired in
+  favour of `erodeBy`; three never-raised `OverlayCellWitness`
+  constructors and `HexPlanarCoverageInvalid` are removed, and full-cell
+  hex coverage is total.
+* Local refinement's closure statistics and receipt are counted from the
+  domain's own faces and interface pairs; no closure set is materialized
+  on the publication path. The set-derived counts survive as the test-side
+  oracle (`topologyClosureSelectionStats`) with a law holding the two equal.
+* Receipts: refinement allocation down 23 to 30 percent, the local-refine
+  publication lane down about 70 percent end to end, hex intersection 65
+  to 26 ms, decode of a 20k-site mesh down 23 MB; power, minkowski and
+  overlay lanes at or below their previous numbers.
+
+* Add packed native hex dilation, right-adjoint erosion, opening, closing,
+  frontiers, connected-component labels, and multi-source shortest distances.
+  These operate in the existing `HexRegion` owner without boxed coordinate or
+  polygon intermediates.
+* Add three exact `PlanarRegion` restrictions onto a caller-supplied
+  `HexLayout`: closed center membership, full-cell coverage, and closed
+  intersection. Their boundary semantics are distinct in the names and types.
+* Centralize pure, effectful, and topology-derived packed construction beneath
+  the private `HexRegion` representation owner. Exact full-cell coverage now
+  uses `hexRegionGenerateM` and no longer exposes an impossible packed-word
+  admission failure; frontier kernels publish their final words without a
+  throwaway dilation or erosion vector.
+* Replace the package-local annotation semilattice with the canonical
+  dependency-light `Moonlight.Algebra.Pure.Join` owner, and hide the raw
+  `RefinementParameters` constructor behind checked authoring operations.
+* Admit canonical packed hex words without copying, decode versioned hex bytes
+  in bulk instead of rebuilding membership cell by cell, and restrict compatible
+  gluing to the word spans covered by each local section.
+* Move the canonical hex byte interpreter into the dependency-light
+  `hex-serialize` component; native hex consumers no longer build the DCEL.
+* Lower hex regions into Homology directly from the coordinate fold, without an
+  intermediate boxed vector.
+
 ## 1.0.0.0 - 2026-08-31
 
 * Rename the planar package from `moonlight-triangulation` to
@@ -213,9 +305,9 @@
   `constraintBatchStats`; `constrainedExtensionBuildStats` is unchanged. The
   batch is the same value `recoverConstraints` returns, so extension and
   standalone recovery are now read through one accessor set instead of two.
-* Withdraw the aggregator modules `Moonlight.Triangulation.Handles`,
-  `Moonlight.Triangulation.Handles.Iterators` and
-  `Moonlight.Triangulation.Internal.DcelOperations` from the exposed module
+* Withdraw the aggregator modules `Moonlight.Planar.Handles`,
+  `Moonlight.Planar.Handles.Iterators` and
+  `Moonlight.Planar.Internal.DcelOperations` from the exposed module
   list. Every leaf they re-exported remains exposed under its own name; import
   the leaf.
 * Add `BoundaryOrientation`, `boundaryLoopOrientation` and
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -31,11 +31,20 @@
 
 | Operation | Use when | Inputs | Result |
 | --- | --- | --- | --- |
+| `curveStep` / `openTrail` / `closeWith` / `locate` | Curves must retain editable controls rather than sampled points | Relative line, quadratic, cubic, or positive-weight rational-conic steps and an anchor | Exact open/closed trail with structural endpoint continuity |
+| `lowerOpenTrail` / `lowerClosedTrail` / `lowerSimpleRegion` | Authored curves need bounded polygonal observations | Explicit error metric and subdivision budgets, plus curves / outer-hole components | Source-local samples and error receipts / admitted polygonal region, or typed refusal |
+| `fill` / `stroke` / `renderSvg` | Curves need ordered illustration and publication | Canonical curves, typed paint, and fixed SVG options | Pure `Picture part` / SVG document, or `SvgError` |
+| `motif` / `attachMotif` / `transformMotif` | Reusable parts must retain semantic attachments | Picture, total typed port frames, destination frame | Picture and ports transformed coherently |
+| `geometryEnvelope` / `alignMotif` / `arrangeMotifs` | Parts need conservative geometric layout | Canonical picture / motifs, axis, fraction or signed gap | Exact control support / coherently moved motifs; not ink bounds |
 | `hexLayout` / `hexNeighbourCoord` | A bounded native hexagonal world needs dense identity and arithmetic adjacency | Axial origin and extents / coordinate and direction | `HexLayout` / optional neighbour / typed layout obstruction |
+| `hexRegionFromCoords` / `hexRegionFromPackedWords` / `hexRegionGenerateM` | Sparse, packed, or effectful dense authoring | Coordinates, canonical words, or a coordinate predicate plus layout | Opaque canonical `HexRegion` / caller or representation obstruction |
 | `hexRegionUnion` / `hexRegionIntersection` / `hexRegionDifference` / `hexRegionSymmetricDifference` | Native cell selections must combine without triangulation or polygon construction | Two same-layout `HexRegion`s | Packed `HexRegion` / layout mismatch |
+| `hexRegionDilate` / `hexRegionErode` / `hexRegionOpening` / `hexRegionClosing` | A bounded hex selection needs lawful neighbourhood morphology | `HexRegion` | Packed `HexRegion`; clipped dilation and its right-adjoint erosion |
+| `hexRegionComponentLabels` / `hexRegionDistancesWithin` | Gameplay and analysis need connected sections and shortest hex steps without boxed graphs | Selected domain and sources | Opaque layout-indexed labels or distance map / typed source obstruction |
 | `restrictHexRegion` / `reframeHexRegion` | A local section changes context | Target layout and region | Packed restricted/reframed region / typed restriction obstruction |
 | `glueCompatibleHexRegions` | Local sections must descend only after agreeing on overlaps | Nonempty local-region family | Glued `HexRegion` / exact overlap witness |
 | `hexRegionPlanarRegion` | Native selected cells need exact polygonal operations | `HexRegion` | Existing `PlanarRegion` / typed boundary or publication obstruction |
+| `hexRegionByCenterInPlanarRegion` / `hexRegionFullyCoveredByPlanarRegion` / `hexRegionIntersectingPlanarRegion` | Exact geometry must restrict onto a supplied hex context without ambiguous boundary semantics | `HexLayout` and `PlanarRegion` | Native packed `HexRegion` |
 | `delaunayGeometry` | Coordinates own the input | `Vector Point` | Geometry-only mesh / `BuildError` |
 | `delaunayFromCoordinates` | Payloads have a separate plane | Defaults, points, payloads, duplicate policy | `BuildResult` / `BuildError` |
 | `insert` / `insertAt` / `insertMany` | An immutable mesh gains sites or payload replacements | Mesh plus payloads or explicit points | `InsertionResult` / `BuildResult` / `BuildError` |
@@ -60,10 +69,11 @@
 | `verticesInCircle` / `verticesInRectangle` | A metric window selects resident sites | Mesh and circle / rectangle | Handles / typed metric error |
 | `exactClipRetainedPolygon` | A convex exact polygon must meet closed half-planes while retaining source lines | Retained polygon and half-plane list | Empty, point, segment, or polygon section plus receipt / `ExactClipError` |
 | `overlayLayers` | Layers need one exact arrangement | Two `PlanarLayer`s | `OverlayResult` / `OverlayError` |
-| `overlayAll` | A nonempty layer family needs one tuple-labelled common refinement | `NonEmpty (PlanarLayer label)` | `PlanarLayer (NonEmpty label)` / `OverlayError` |
+| `overlayAll` | A nonempty layer family needs one fused common refinement | `NonEmpty (PlanarLayer label)` | `OverlayResult (NonEmpty label)` / `OverlayError` |
 | `overlayClosedUnion` / `overlayClosedIntersection` / `overlayRegularizedDifference` | Boolean output may retain lower cells | Two label predicates, `OverlayResult` | `ExactCellSet` / `OverlaySelectionError` |
-| `overlaySelectedRegion` | Selected faces must publish as polygons | Label-pair predicate, `OverlayResult` | `PlanarRegion` / `RegionPublicationError` |
-| `overlayMass` / `overlayConfusion` | Only exact selected area or the finite label-pair matrix is needed | Predicate / `OverlayResult` | `ExactArea` / exact mass map |
+| `overlaySelectedRegion` / `overlayPlanarLayer` | Exact faces must publish as polygons | Label predicate / `OverlayResult labels` | `PlanarRegion` / `PlanarLayer labels`, or `RegionPublicationError` |
+| `overlayMass` / `overlayConfusion` | Only exact selected area or finite label masses are needed | Predicate / `OverlayResult labels` | Exact area / mass map, or typed observation obstruction |
+| `foldBoundedOverlayCells` | Direct exact moments or another boundary fold is required | Accumulator, labels and oriented exact edge pairs | Accumulator / `OverlayCellWitness` |
 | `layerCovers` | A labelled layer must cover an admitted polygonal window | `PlanarLayer`, `PolygonComponent` | `()` or `LayerCoverageError` carrying the exact gap |
 | `cellValuations` / `regionValuations` | Intrinsic measures are required | `ExactCellSet` / `PlanarRegion` | `PlanarValuations` / `ValuationError` |
 | `minkowskiSum` / `polygonOffset` | Regions must add / expand | Two regions / element and region | Region plus receipt / `MinkowskiError` |
@@ -77,12 +87,32 @@
 | `alphaShapeContainsFace` | One face at one radius is enough | `RadiusSquared`, mesh, `FaceId` | `Bool` |
 | `alphaFiltration` | Every critical radius matters | Delaunay mesh | `AlphaFiltration` / `AlphaFiltrationError` |
 | `regularAlphaFiltration` | Weighted regular topology needs signed exact births | `RegularTriangulation` | `RegularAlphaFiltration` / `RegularAlphaError` |
-| `fromExactCellSet` | Exact cells need a generic complex view | `ExactCellSet` | `DCELComplex` |
+| `fromExactCellSet` | Exact cells need a cellular chain view | `ExactCellSet` | `DCELComplex` / `DCELError` |
 | `filteredAlphaComplex` | Alpha births must enter persistence | `AlphaFiltration` | Filtered chain complex / `DCELError` |
 | `activationAlphaZigzag` | Non-nested labelled depths need checked correspondence | `NonEmpty (ActivationSlice depth label)` | Admitted complexes, adjacent-union witnesses, and checked zigzag / `ActivationZigzagError` |
 | `activationZigzagIntervals` / `activationAlphaPersistence` | An admitted zigzag / labelled depth family needs its exact barcode | `ActivationZigzag` / `NonEmpty (ActivationSlice depth label)` | Stage-labelled exact zigzag intervals / `ActivationZigzagError` |
 | `canonicalize` | Numbering must ignore construction history | Mesh with unit edge/face payloads | Canonical mesh / `BuildError` |
 
+Exact overlay owns geometry-free incidence, exact coordinates, source-ordinal
+provenance and face labels. One segment-event plan handles the entire source
+family. Distinct rational vertices may coincide in binary64 without obstructing
+construction, closed selections or exact valuations. Polygons are fallible
+derived observations: a connected pinch may refuse publication while its exact
+closed-cell selection and chains remain meaningful.
+`Functor` and `Traversable` relabel only the result's label plane; the binary
+and n-ary authoring entrances use that same traversal to decode their sources.
+
+Face boundaries retain every connected boundary walk and isolated boundary
+vertex. Area and moments sum oriented edges directly. Cellular lowering adds
+abstract, face-owned cuts for disconnected inner boundary components; it does
+not invent geometric diagonals. Native alpha retains its dense triangulation
+witness and ordered-column lowering. Nonconvex morphology uses exact convex
+slab sections, and generated-piece unions use the same fused overlay owner.
+
+Hex restrictions derive exact center spans in oblique lattice coordinates and
+pack whole runs. Coverage and intersection invoke their detailed classifiers
+only on an exact boundary supercover; contact semantics are unchanged.
+
 [Interpret cell sets as incidence categories.](docs/category-observatory/README.md#cell-complex-and-category-interpretation)
 · [Compute persistent alpha topology.](docs/persistence-rose/README.md#exact-alpha-filtration-and-persistent-topology)
 · [Track topology across non-nested activation depths.](docs/activation-zigzag.md)
@@ -107,6 +137,32 @@
 When a regular value is already available, `boundedPowerDiagramFromRegular`
 reuses its normalized lifted hull.
 
+`Moonlight.Planar.Convex` owns convex admission, hull construction, boundary
+observations, exact convex-slab decomposition and `ConvexError`, available
+through the focused `dcel` component. Morphology
+owns structuring-element origin containment and Minkowski operations, not convex
+geometry. Raw foreign convex admission reports obstruction 207 independently of
+morphology failure.
+
+For repeated rational weight changes at fixed positions, import
+`Moonlight.Planar.PowerDiagram` and prepare an opaque power-mass section:
+
+```haskell
+section <- preparePowerMassSection domain sites weightRates
+masses <- evaluatePowerMasses parameter section
+```
+
+`weightRates` supplies exactly one rational rate per site label. The section
+retains its window, sites, direction, quadratic area coefficients and a sufficient
+open validity interval. Complete cell/window slack certificates and strict empty
+witnesses justify reuse; interval endpoints are refused. Expiry requests fresh
+preparation, not an epsilon step or an inferred topology transition. This
+restricted observation refuses coincident positions, lower-dimensional seeds
+and multiply incident seed vertices; the existing regular and bounded power
+APIs retain their broader contracts. No event-transition engine is implied.
+Preparation has a real cost: use this observation to amortize repeated queries,
+not as an assumed faster replacement for a one-shot bounded diagram.
+
 `upperEnvelope` maps exact `c0 + cx*x + cy*y` forms into the same owner. Its
 planar result contains only two-dimensional winning regions, with `Nothing`
 outside the window; nonconvex windows and holes use the canonical overlay.
@@ -203,27 +259,40 @@
 combined <- hexRegionUnion left right
 ```
 
-The pure Boolean kernels visit one `Word64` per 64 cells. Exact polygons,
-Homology chains, and bytes are explicit derived interpretations in the default,
-`cell-complex`, and `serialize` components respectively.
+The pure Boolean kernels visit one `Word64` per 64 cells. Packed input is
+admitted without copying after its length and final-word padding are checked.
+Exact polygons, Homology chains, and bytes are explicit derived interpretations
+in the default, `cell-complex`, and `hex-serialize` components respectively.
 
-Use the broad default library with explicit `Vector` inputs. It exposes the
-facade and the safe leaf modules; construction and set algebra retain their
-typed obstruction in `Either`:
+Use the default library for exact overlay, power geometry and morphology.
+Native construction and set algebra live in the explicit `build` component;
+import their owning modules rather than an umbrella:
 
 ```cabal
 build-depends:
   base >= 4.19 && < 5,
-  moonlight-planar >= 1.0 && < 1.1,
+  moonlight-planar:dcel >= 1.0 && < 1.1,
+  moonlight-planar:build >= 1.0 && < 1.1,
   vector >= 0.13 && < 0.14
 ```
 
+```haskell
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Point (Point (..))
+```
+
+There is no `Moonlight.Planar` umbrella module. An exact-only consumer does not
+inherit native construction, legalization or refinement through the default
+library. Region, convex and query-point admissions expose read-only
+observations; changing their invariant-bearing structure requires their checked
+constructors.
+
 Use `withScopedTriangulation` for allocation-free local incidence traversal.
 Use `Handles.Dynamic` when a handle must escape. Fixed `VertexId`/edge/face
 values are unchecked resident indices and belong only with the mesh that issued
 them.
 
-[`SetAlgebra.hs`](docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs)
+[`SetAlgebra.hs`](docs/examples/Moonlight/Planar/Example/SetAlgebra.hs)
 is the smallest compile-checked workflow. The
 [example index](docs/examples/README.md) adds constrained interiors,
 alpha-boundary descent, exact overlay and valuations, and polygonal morphology.
@@ -244,6 +313,240 @@
 complements. Exact predicates use bounded machine-word evaluation and fall
 through to aligned `Integer` arithmetic when required.
 
+## Curves and illustration
+
+Author shape intent in ordinary Haskell records and pure functions. Keep the
+curves as the source of truth; neither SVG strings nor triangulation vertices
+are the editing interface.
+
+This is a strong foundation, not a finished illustration language. The
+[curve-authoring status](docs/curve-authoring-status.md) records the missing
+measurement, offset, intersection, relational, repetition, and diagnostic
+algebras required before making a broader readiness claim.
+
+`Moonlight.Planar.Curve` separates relative steps from their absolute
+`Located` anchor. `curveStep` supplies a displacement and a shape made with
+`line`, `quadratic`, `cubic`, or `rationalQuadratic`; control vectors are relative
+to the step's start. `openTrail` composes steps, while `closeWith` derives the
+last displacement and retains its explicitly chosen closing shape. Endpoint
+continuity is structural, not a promise of smoothness. Use `hermiteStep`,
+`startJet`, `endJet`, and `joinContinuity` for explicit tangent obligations.
+`circle` and `ellipse` author rational conics, not sampled polygons.
+`Moonlight.Planar.Affine` supplies exact local placement and reflection;
+`composeAffine2 outer inner` means apply `inner`, then `outer`.
+`inverseAffineIso2` reverses an admitted nonsingular frame without repeating
+singularity checks. The curve actions `transformStep`, `transformTrail`,
+`transformClosedTrail`, `transformLocatedTrail`, `transformLocatedClosedTrail`,
+and `transformPath` accept one `Affine2`; callers cannot accidentally pair
+incompatible point and vector maps. Translation acts only on located anchors.
+
+### Author landmarks and profiles, not Bezier handles
+
+Prefer `Moonlight.Planar.Curve.Authoring` for illustration work:
+
+- `cardinalOpen` / `cardinalClosed` interpolate `NonEmpty Knot` landmarks.
+  `Interpolating point` derives its tangent from neighboring landmarks;
+  `ExplicitJets point incoming outgoing` expresses an intentional derivative
+  or corner. Tension is an admitted `UnitInterval`: zero gives uniform
+  Catmull-Rom tangents, one zeros the automatic tangents. Explicit jets are
+  independent of tension. Spacing is by knot index, not arc length.
+- `profileOutline` / `profileRails` derive a silhouette or two open rails from
+  `ProfileStation center halfSpan`. Reuse this law for horns, petals, leaves,
+  ribbons and blades. Zero half-span makes a pointed end; bending the centers
+  bends both rails together. The transverse vector is not a normalized normal.
+- `polygonTrail` connects corner landmarks with straight lines.
+- `bowedTrail bow start end` derives a bowed stroke. Its midpoint displacement
+  is `bow * perpendicular (end-start)`, so the parameter is dimensionless.
+  It is a quadratic bow, not a circular arc. Author it locally and transform
+  the resulting curve: fixed bow parameters do not commute with arbitrary
+  shears, and reflections reverse the bow sign.
+
+These constructors return the existing exact Curve values immediately. There
+is no second spline evaluator, shape cache or scene registry. Cardinal/profile
+construction commutes with affine transformation. It is interpolation of
+landmarks, not a guarantee against overshoot or self-intersection.
+
+The optional `Moonlight.Planar.Illustration` library paints these same curves.
+`Picture part` is a monoid: `behind <> inFront` paints left to right. `fill`
+requires closed contours and an explicit `NonZero` or `EvenOdd` rule; `stroke`
+takes a `Path` of open or closed subpaths. Stroke width, local/output units,
+cap, and join are explicit. `miterLimit` admits ratios of at least one.
+`Solid`, `LinearGradient`, and `RadialGradient` are typed paint values;
+`gradientStops` stably sorts admitted offsets, preserving the author's order
+at coincident stops. `clip`, `place`, and `opacity` are scopes; group opacity
+does not distribute across overlapping children. `annotate` attaches an
+author-owned part value, and `fmap` embeds motif labels into a larger part type.
+
+This complete pure authoring module needs `base`,
+`moonlight-planar:dcel`, and `moonlight-planar:illustration`:
+
+```haskell
+module Leaf (Part (..), leaf, foliage) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Planar.Affine (Affine2)
+import Moonlight.Planar.Curve.Authoring (ProfileStation (..), profileOutline)
+import Moonlight.Planar.Exact
+  ( ExactRational, ExactVector (..), PositiveExact, exactPoint
+  , positiveExactValue, positiveTwo, unitHalf )
+import Moonlight.Planar.Illustration
+  ( Color (..), FillRule (..), Paint (..), Picture, annotate, fill, place )
+
+data Part = Leaf deriving (Eq, Show)
+
+leaf :: ExactRational -> PositiveExact -> Picture Part
+leaf bend breadth = annotate Leaf $
+  fill NonZero (Solid (RGB 53 132 94)) (outline :| [])
+ where
+  outline = profileOutline unitHalf $
+    ProfileStation (exactPoint 0 0) (ExactVector 0 0) :|
+      [ ProfileStation (exactPoint bend (-30))
+          (ExactVector (positiveExactValue breadth) 0)
+      , ProfileStation (exactPoint (2*bend) (-70)) (ExactVector 0 0)
+      ]
+
+foliage :: [Affine2] -> Picture Part
+foliage frames = foldMap (\frame -> place frame motif) frames
+ where
+  motif = leaf 8 positiveTwo
+```
+
+For an agent, the workflow is: name parts and attachment frames; choose a
+profile or interpolating contour; express its widths and landmarks as equations
+of a small control record; reuse the value with `place` and `foldMap`; paint in
+explicit back-to-front order. Inspect the fixed-camera render and diagnostics
+after editing a meaningful control. Do not replace the profile with a traced
+list of cubic handles merely to match one rendered sample.
+
+`Moonlight.Planar.Illustration.Svg` publishes with
+`renderSvg :: (part -> String) -> SvgOptions -> Picture part -> Either SvgError String`.
+Construct options once using `svgViewport` (pixel dimensions, view-box origin
+and positive extents), `svgPrecision` (decimal places and positive scalar-error
+limit), then `svgOptions` (viewport, precision, positive conic pixel tolerance,
+maximum subdivision depth, maximum leaves). `renderDiagnosticSvg` uses the same
+camera and adds control polygons, anchors, local axes, and escaped part labels.
+The viewport uses `preserveAspectRatio="none"`; choose matching pixel/view-box
+aspect ratios unless stretching is intentional. Writing the result is the
+caller's effect boundary.
+
+### Typed attachment and geometric layout
+
+Import `Moonlight.Planar.Illustration.Motif` and
+`Moonlight.Planar.Illustration.Layout` directly from the optional `illustration`
+component. A `Motif port part` holds a Picture and a total `port -> AffineIso2`.
+Port constructors belong to the author; no registry, string lookup or serialized
+scene is involved. Frames express origin, axes and scale. `attachMotif` computes
+`destination . inverse source` and moves every child port with the picture.
+Raw matrices are admitted once with `affineIso2`; return a named author error
+on singular input, never an identity fallback. `translationAffineIso2` is total.
+
+Using the `leaf` and `Part` definitions above:
+
+```haskell
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( AffineIso2, identityAffineIso2, translationAffineIso2 )
+import Moonlight.Planar.Illustration.Motif
+  ( Motif, motif, motifPicture, attachMotif )
+import Moonlight.Planar.Illustration.Layout
+  ( LayoutAxis (Horizontal), arrangeMotifs )
+
+data LeafPort = LeafRoot
+
+mountedLeaf :: AffineIso2 -> Motif LeafPort Part
+mountedLeaf socket = attachMotif LeafRoot socket $
+  motif (leaf 8 positiveTwo) (const identityAffineIso2)
+
+leafRow :: Picture Part
+leafRow = foldMap motifPicture $ arrangeMotifs Horizontal 12 $ Seq.fromList
+  [ mountedLeaf (translationAffineIso2 (ExactVector 30 90))
+  , mountedLeaf (translationAffineIso2 (ExactVector 30 90))
+  , mountedLeaf (translationAffineIso2 (ExactVector 30 90)) ]
+```
+
+Layout returns motifs so their ports cannot be left behind. `alignMotif` selects
+the minimum, midpoint or maximum using `unitZero`, `unitHalf` or `unitOne`, then
+translates it to a requested coordinate. `arrangeMotifs` measures each child once,
+preserves the first nonempty position, and places subsequent intervals at the
+previous edge plus gap. Negative gaps intentionally overlap; empty geometry
+consumes no gap. It does not rescan the growing scene or retain a layout cache.
+
+`geometryEnvelope` retains exact finite control support; `geometrySupport` and
+`geometryBounds` are observations, not new curve evaluators. Bounds include
+stroke centerlines but **not stroke ink**, ignore clipping/opacity shrinkage,
+and may be loose. They cannot certify culling, painted clearance, pixel coverage
+or topology. Affine attachment permits shear/reflection; it is not a rigid joint
+or a smooth-join guarantee. These limits are intentional, not hidden fallback
+behaviour.
+
+### Semantic editing study
+
+The [Knight and crescent source](docs/art-common/Moonlight/Planar/Exhibit/IllustrationStudy.hs)
+provides `studyPicture`, `defaultControls`, and checked absolute edits:
+`editControl LeftHornSweep 30 defaultControls` changes the left horn without
+moving the eyes or unrelated geometry. Other controls are `EyeSpacing`,
+`EyeTilt`, `CloakFullness`, and `FullerWidth`. These are exhibit-owned controls,
+not a library scene registry. The blade's fuller is an actual unpainted hole,
+not a background-colored patch.
+
+The [equational garden](docs/art-common/Moonlight/Planar/Exhibit/EquationalGarden.hs)
+is a second full-scene example: shared leaf/petal profiles, framed flowers,
+bowed stems and veins, clipped highlights and layered botanical silhouettes.
+Its `gardenPlants` retains typed stem sockets and attached leaves/blossoms;
+`gardenPicture` returns a typed authoring error if a socket is singular.
+The Knight's `partPort` derives actual horn roots and eye centers from controls,
+replacing the old fixed diagnostic-coordinate table.
+
+The standalone [space sword](docs/art-common/Moonlight/Planar/Exhibit/SpaceSword.hs)
+is an illustration-scale composition of a swept metal profile, attached hilt,
+recessed energy channel, repeated typed inscriptions and a single affine pose.
+`defaultSpaceSwordControls` and `overchargedSpaceSwordControls` change the blade
+system without perturbing the crossguard, grip, counterweight or star field.
+It is source-authored mathematics, not traced output wearing a lab coat.
+Both illustration authors use the reusable authoring operations rather than
+constructing Bezier control handles. Low-level curve construction remains
+available for algorithms that actually need it.
+
+From the repository root, render the fixed-camera baseline, combined edited
+variant, and diagnostics using the isolated build directory:
+
+```console
+scripts/safe-cabal.sh run moonlight-planar:exe:moonlight-planar-illustration-study \
+  --project-file=cabal.project.planar-dev \
+  --builddir=dist-newstyle-planar-illustration --enable-benchmarks -j1 -- \
+  /Volumes/Sirius/work/pm-artifacts/planar-illustration-20260911/attachments-layout-20260912/candidate
+```
+
+The output directory must be absolute. Knight outputs are `knight-crescent.svg`,
+`knight-crescent-edited.svg`, and `knight-crescent-diagnostic.svg`; botanical
+outputs are `equational-garden.svg`, `equational-garden-edited.svg`, and
+`equational-garden-diagnostic.svg`. `equational-garden-layout.svg` and
+`equational-garden-layout-edited.svg` show the same leaf motifs with ports and
+geometry bounds: widening the middle leaf moves its successor automatically.
+The same run writes `space-sword.svg` and `space-sword-overcharged.svg`.
+Compare an
+edited crop and the whole picture without changing the camera; unchanged
+geometry can still acquire different visible pixels through occlusion.
+
+### Approximation and topology limits
+
+SVG keeps native line/quadratic/cubic commands. Rational conics use the canonical
+lowerer with error measured in the exact accumulated affine and viewport pixel
+metric. SVG subdivision budgets apply per source conic; exhaustion refuses
+publication. Decimal rounding is checked **per emitted scalar**, separately
+from the conic approximation bound; it is not a combined browser-pixel bound
+or a proof about accumulated transform rounding. Unsupported numeric range,
+insufficient precision, and positive dimensions rounded to zero also refuse.
+
+`Moonlight.Planar.Curve.Lowering` returns source-local samples and opaque span
+receipts; its affine argument changes the error metric, not the returned
+coordinates. The bound is Hausdorff distance, not tangent accuracy or topology.
+`Moonlight.Planar.Curve.Region.lowerSimpleRegion` additionally checks the
+resulting polygon through the existing region owner: outer contours are CCW,
+holes CW. Admission proves the **sampled polygon's** simplicity, containment,
+and component compatibility, not that the original curves have equivalent
+topology. SVG winding/clipping likewise does not constitute region admission.
+
 ## Consumer libraries
 
 The Cabal manifest owns component visibility and dependencies; this is its
@@ -252,15 +555,21 @@
 
 | Dependency | Imports | Purpose |
 | --- | --- | --- |
-| `moonlight-planar:hex` | `Moonlight.Hex.Coordinate`, `.Element`, `.Region` | Native axial elements, arithmetic neighbours, packed finite-set algebra, restriction, and compatible gluing; depends only on `base`, `deepseq`, `transformers`, and `vector`. |
-| `moonlight-planar` | `Moonlight.Triangulation` plus the public `dcel`, `build`, and `dual` modules | Broad construction, observation, set algebra, CDT, refinement, alpha, Voronoi, interpolation, and exact planar surface. |
-| `moonlight-planar:core` | `.Scalar`, `.LineSideInfo` | Low-level exact scalars and paged storage primitives. |
-| `moonlight-planar:dcel` | `.Types`, `.Dcel`, `.Handles.*`, `.PointLocation`, `.FloodFillIterator`, `.IntersectionIterator`, `.Alpha`, `.Exact`, `.Region`, `.Valuation` | Immutable mesh vocabulary, exact geometry, scoped/owning observations, and the explicit fixed-index kernel; no construction kernel. |
-| `moonlight-planar:build` | `.BulkLoad`, `.Session`, `.Removal`, `.Cdt`, `.Refinement`, `.SetAlgebra` | Construction and persistent editing over the DCEL; no dual, serialization, concurrency, or Homology dependency. |
+| `moonlight-planar:hex` | `Moonlight.Hex.Coordinate`, `.Element`, `.Region`, `.Topology` | Native axial elements, arithmetic neighbours, packed finite-set algebra and morphology, components, distances, restriction, and compatible gluing; depends only on `base`, `deepseq`, `transformers`, and `vector`. |
+| `moonlight-planar` | `.Overlay`, `.PowerDiagram`, `.RegularAlpha`, `.Minkowski`, `Moonlight.Hex.Planar` and read-only `dcel` modules | Exact planar algebra and native-hex interpretation without native triangulation construction. |
+| `moonlight-planar:dcel` | `.Scalar`, `.LineSideInfo`, `.Types`, `.Dcel`, `.Handles.*`, `.PointLocation`, `.FloodFillIterator`, `.IntersectionIterator`, `.Alpha`, `.Exact`, `.Affine`, `.Curve`, `.Curve.Authoring`, `.Curve.Lowering`, `.Curve.Region`, `.Convex`, `.Region`, `.Valuation` | Immutable mesh vocabulary, exact geometry and curve authoring/lowering, scoped/owning observations, and the explicit fixed-index kernel; no construction kernel. |
+| `moonlight-planar:illustration` | `.Illustration`, `.Illustration.Svg` | Optional ordered painting of canonical curves and pure SVG/diagnostic publication; adds no second geometry representation. |
+| `moonlight-planar:build` | `.BulkLoad`, `.Session`, `.Removal`, `.Cdt`, `.Refinement`, `.SetAlgebra`, `.Canonical`, `.HintGenerator`, `.Telemetry` | Construction, persistent editing and canonical rebuilding; dual observations support operational hints and telemetry, without serialization, concurrency or Homology. |
 | `moonlight-planar:dual` | `.Voronoi`, `.Voronoi.Handles`, `.Interpolation` | Voronoi observations and natural-neighbour interpolation. |
 | `moonlight-planar:parallel` | `.Parallel` | Bounded concurrent union; adds `async`. |
-| `moonlight-planar:serialize` | `Moonlight.Hex.Serialization`, `Moonlight.Triangulation.Serialization` | Versioned binary envelopes; adds `binary`, `bytestring`, and `transformers`. |
-| `moonlight-planar:cell-complex` (GHC 9.14+) | `Moonlight.Hex.CellComplex`, `Moonlight.Triangulation.CellComplex` | Native-hex and exact-DCEL chain interpretations; adds Homology. |
+| `moonlight-planar:hex-serialize` | `Moonlight.Hex.Serialization` | Versioned packed-region bytes without the DCEL dependency cone. |
+| `moonlight-planar:serialize` | `Moonlight.Planar.Serialization` | Versioned triangulation bytes; adds `binary`, `bytestring`, and `transformers`. |
+| `moonlight-planar:cell-complex` (GHC 9.14+) | `Moonlight.Hex.CellComplex`, `Moonlight.Planar.CellComplex` | Native-hex and exact-DCEL chain interpretations; adds Homology. |
 | `moonlight-planar:zigzag` (GHC 9.14+) | `.Zigzag` | Labelled activation alpha complexes, adjacent-union witnesses, and stage-labelled zigzag intervals; adds Homology and construction. |
+
+The scalar `core` component is package-private: its trusted rational bridges
+assume normalization already proved by internal arithmetic. Public callers use
+the checked rational operations in `Moonlight.Planar.Exact`, not an internal
+conversion accepting arbitrary `Ratio` representations.
 
 [Build and validate package components locally.](docs/development.md)
diff --git a/bench/aggregate/Main.hs b/bench/aggregate/Main.hs
--- a/bench/aggregate/Main.hs
+++ b/bench/aggregate/Main.hs
@@ -4,11 +4,11 @@
 -- slices instead of importing them drifts the moment a slice grows.
 module Main (main) where
 
-import qualified Moonlight.Triangulation.BuildBench as BuildBench
-import qualified Moonlight.Triangulation.DcelBench as DcelBench
-import qualified Moonlight.Triangulation.DualBench as DualBench
-import qualified Moonlight.Triangulation.JoinBench as JoinBench
-import qualified Moonlight.Triangulation.RegionBench as RegionBench
+import qualified Moonlight.Planar.BuildBench as BuildBench
+import qualified Moonlight.Planar.DcelBench as DcelBench
+import qualified Moonlight.Planar.DualBench as DualBench
+import qualified Moonlight.Planar.JoinBench as JoinBench
+import qualified Moonlight.Planar.RegionBench as RegionBench
 
 main :: IO ()
 main = do
diff --git a/bench/alpha/Main.hs b/bench/alpha/Main.hs
--- a/bench/alpha/Main.hs
+++ b/bench/alpha/Main.hs
@@ -31,14 +31,14 @@
   , mod2PersistentPairsWithCriticalBettiTable
   )
 import Moonlight.Homology.Topology (freeBettiVector)
-import Moonlight.Triangulation.Alpha
+import Moonlight.Planar.Alpha
   ( AlphaBirth
   , alphaFiltration
   )
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellComplex (filteredAlphaComplex)
-import Moonlight.Triangulation.LabelledComplex (filteredPlanarComplex)
-import Moonlight.Triangulation.PowerDiagram
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.CellComplex (filteredAlphaComplex)
+import Moonlight.Planar.LabelledComplex (filteredPlanarComplex)
+import Moonlight.Planar.PowerDiagram
   ( PowerSite
   , RegularTriangulation
   , powerSite
@@ -46,13 +46,14 @@
   , regularTriangulation
   , regularTriangulationReceipt
   )
-import Moonlight.Triangulation.RegularAlpha
+import Moonlight.Planar.RegularAlpha
   ( PowerAlphaBirth
   , regularAlphaBirths
   , regularAlphaComplex
   , regularAlphaFiltration
   )
-import Moonlight.Triangulation.Types (DelaunayTriangulation, Point)
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (Point)
 
 type PreparedAlpha = FilteredFiniteChainComplex AlphaBirth Int
 
diff --git a/bench/build/Main.hs b/bench/build/Main.hs
--- a/bench/build/Main.hs
+++ b/bench/build/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.BuildBench as BuildBench
+import qualified Moonlight.Planar.BuildBench as BuildBench
 
 main :: IO ()
 main = BuildBench.benchmarks
diff --git a/bench/build/Moonlight/Planar/BuildBench.hs b/bench/build/Moonlight/Planar/BuildBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/build/Moonlight/Planar/BuildBench.hs
@@ -0,0 +1,293 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | The construction side: circle-sweep bulk load against the arrival-order
+-- session kernel, persistent single insertion, constraint recovery and Ruppert
+-- refinement. Each reports the library's own work counters alongside the time,
+-- because the claim being measured is about work done rather than seconds.
+module Moonlight.Planar.BuildBench (benchmarks) where
+
+import BenchMeasure (requireRight, timedValue)
+import BenchSupport (randomPoints)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad (forM_, unless)
+import Data.Foldable (traverse_)
+import Data.List (sort)
+import qualified Data.List as List
+import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (delaunay, delaunayGeometry, empty, insert)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.Cdt (CdtError (..), ConstraintRecoveryResult (..), ConstraintResult, addConstraintEdge, canAddConstraint, constrainedDelaunay, constraintBatchStats, constraintBatchTriangulation, fromDelaunay, recoverConstraints)
+import Moonlight.Planar.Dcel (destination, numUndirectedEdges, numVertices, origin, undirectedEndpoints, vertexPoint)
+import Moonlight.Planar.Handles.HandleDefs (VertexId, normalizedDirected)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (undirectedEdges, vertices)
+import Moonlight.Planar.Refinement (refine, withAdditionalVertexBudget, withConstraintPreservation, withMaximumArea, withMaximumRadiusEdgeRatio, withOuterFaceExclusion)
+import Moonlight.Planar.Session (insertVertex, insertVertexAt, withSession)
+import Moonlight.Planar.Types (BuildError (..), BuildResult, ConstrainedDelaunayTriangulation, ConstraintMode (..), DelaunayTriangulation, RefinementParameters, Triangulation, buildInputVertices, buildTriangulation, defaultRefinementParameters, insertionTriangulation, refinementAddedVertices, refinementStats, unitElementDefaults)
+import Moonlight.Planar.BuildStats (BuildMetric (EdgeFlips, LocationWalkSteps, RefinementFaceChecks, RefinementQueuePops), buildStat)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Validation (validateTriangulation)
+import Moonlight.Planar.Foreign.Mesh (insertGeometryBatch)
+-- The public handle module hides this constructor. The benchmark indexes the builder's
+-- own input mapping, so every handle it forges is one the builder issued, and
+-- it owns that obligation explicitly by naming the module that grants it.
+import Moonlight.Planar.Internal.HandleDefs (VertexId (VertexId))
+import Moonlight.Planar.Internal.Session (withLocalSession)
+
+benchmarks :: IO ()
+benchmarks = do
+  putStrLn "moonlight-planar native construction benchmark"
+  forM_ [1_000, 10_000, 50_000] benchmarkConstruction
+  forM_ [1_000, 10_000, 50_000, 100_000, 1_000_000] benchmarkSingletonInsertionCrossover
+  traverse_
+    benchmarkGeometryBatchInsertion
+    [(0, 0), (0, 1_000), (50_000, 0), (50_000, 1), (50_000, 8), (50_000, 64), (50_000, 1_000), (50_000, 10_000), (50_000, 50_000)]
+  benchmarkDuplicateGeometryBatches
+  benchmarkConstraints
+  benchmarkRefinement 2_500
+
+-- | Compare the geometry batch entrance with its arrival-order session
+-- control. Both include coordinate admission and publication. Small, empty,
+-- and duplicate-heavy sections must not disappear behind a large fresh batch:
+-- a resident hash or radial sort may cost more than the local work it saves.
+benchmarkGeometryBatchInsertion :: (Int, Int) -> IO ()
+benchmarkGeometryBatchInsertion (baseCount, addedCount) =
+  benchmarkGeometryBatchFixture
+    (show baseCount <> "+" <> show addedCount)
+    (V.fromList (randomPoints 0x9e3779b97f4a7c15 baseCount))
+    (V.fromList (randomPoints 0xbf58476d1ce4e5b9 addedCount))
+
+benchmarkDuplicateGeometryBatches :: IO ()
+benchmarkDuplicateGeometryBatches =
+  traverse_
+    (\(label, points) -> benchmarkGeometryBatchFixture label basePoints points)
+    [ ("resident-duplicates/50000+1000", V.take 1_000 basePoints)
+    , ("mixed-duplicates/50000+1000", V.take 900 basePoints <> V.replicate 100 freshPoint)
+    , ("fresh-duplicates/50000+10000", V.replicate 10_000 freshPoint)
+    ]
+ where
+  basePoints :: V.Vector Point
+  basePoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 50_000)
+  freshPoint :: Point
+  freshPoint = Point 0.3141592653589793 0.2718281828459045
+
+benchmarkGeometryBatchFixture :: String -> V.Vector Point -> V.Vector Point -> IO ()
+benchmarkGeometryBatchFixture fixtureLabel basePoints addedPoints = do
+  let label suffix = "geometry-batch-" <> suffix <> "/" <> fixtureLabel
+  base <- evaluate . force =<< requireRight (delaunayGeometry basePoints)
+  _ <- evaluate (force addedPoints)
+  admitted <- timedValue (label "abi") $ requireRight (insertGeometryBatch base addedPoints)
+  (_, sessioned, _) <- timedValue (label "session") $
+    requireRight
+      ( withSession base (V.length addedPoints) $
+          V.mapM_ (\point -> () <$ insertVertexAt point ()) addedPoints
+      )
+  admittedCanonical <- requireRight (canonicalize admitted)
+  sessionedCanonical <- requireRight (canonicalize sessioned)
+  equal <- evaluate (force (admittedCanonical == sessionedCanonical))
+  unless equal $
+    fail (label "witness" <> ": the admitted and session arms disagree")
+  unless
+    ( numVertices admitted >= numVertices base
+        && numVertices sessioned >= numVertices base
+        && all
+          (\vertex -> vertexPoint admitted vertex == vertexPoint base vertex && vertexPoint sessioned vertex == vertexPoint base vertex)
+          (vertices base)
+    )
+    (fail (label "witness" <> ": a resident vertex handle changed identity"))
+  putStrLn (label "witness" <> ": ok")
+
+benchmarkConstruction :: Int -> IO ()
+benchmarkConstruction count = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
+  swept <- timedValue ("circle-sweep/" <> show count) $ requireRight (delaunay unitElementDefaults points)
+  (_, sessioned, _) <- timedValue ("session/" <> show count) $
+    requireRight
+      ( withSession (empty unitElementDefaults) (V.length points) $
+          V.mapM_ insertVertex points
+      )
+  evaluate (force (canonicalEdges (buildTriangulation swept) == canonicalEdges sessioned)) >>= \equal ->
+    if equal then pure () else fail "circle-sweep and session construction disagree"
+
+benchmarkSingletonInsertionCrossover :: Int -> IO ()
+benchmarkSingletonInsertionCrossover count = do
+  let points = V.fromList (randomPoints 0xd1b54a32d192ed03 count)
+  built <- requireRight (delaunay unitElementDefaults points)
+  let query = Point 0.000_123_456_7 (-0.000_765_432_1)
+      base = buildTriangulation built
+  scheduled <- timedValue ("singleton-scheduled-public-insert/" <> show count) $ requireRight (insert base query)
+  (_, local, _) <- timedValue ("singleton-local-session-insert/" <> show count) $
+    requireRight (withLocalSession base 1 (insertVertex query))
+  let scheduledMesh = insertionTriangulation scheduled
+  unless (null (validateTriangulation scheduledMesh)) $
+    fail ("scheduled singleton insertion produced an invalid triangulation at " <> show count <> " sites")
+  unless (null (validateTriangulation local)) $
+    fail ("local singleton insertion produced an invalid triangulation at " <> show count <> " sites")
+  scheduledCanonical <- requireRight (canonicalize scheduledMesh)
+  localCanonical <- requireRight (canonicalize local)
+  equal <- evaluate (force (scheduledCanonical == localCanonical))
+  unless equal $
+    fail ("scheduled and local singleton insertion disagree semantically at " <> show count <> " sites")
+  putStrLn ("singleton-insertion-crossover/" <> show count <> "-semantic-witness: ok")
+
+benchmarkConstraints :: IO ()
+benchmarkConstraints = do
+  benchmarkConstraintFixture "cdt/recovery" 8_000 (randomChords 800)
+  benchmarkConstraintFixture "cdt/recovery/20000+4000" 20_000 (randomChords 4_000)
+  benchmarkConstraintFixture "cdt/recovery/edges-8000+2000" 8_000 (delaunayEdges 2_000)
+  benchmarkConstraintFixture "cdt/recovery/edges-20000+6000" 20_000 (delaunayEdges 6_000)
+
+-- | Chords between pseudo-random input positions: long, so most cross an
+-- earlier constraint and the resident constraint set stays sparse.
+randomChords :: Int -> DelaunayTriangulation Point -> BuildResult 'Unconstrained Point () () () -> IO (V.Vector (VertexId, VertexId))
+randomChords constraintCount _ built =
+  V.mapM
+    (\(fromIndex, toIndex) ->
+      let len = sizeofPrimArray inputMapping
+          mFrom = if fromIndex >= 0 && fromIndex < len then Just (VertexId (indexPrimArray inputMapping fromIndex)) else Nothing
+          mTo = if toIndex >= 0 && toIndex < len then Just (VertexId (indexPrimArray inputMapping toIndex)) else Nothing
+      in case (mFrom, mTo) of
+        (Just from, Just to) -> pure (from, to)
+        _ -> fail "constraint benchmark endpoint is out of range"
+    )
+    requestIndices
+ where
+  inputMapping = buildInputVertices built
+  pointCount = sizeofPrimArray inputMapping
+  requestIndices =
+    V.fromList
+      ( take constraintCount
+          [ (a, b)
+          | index <- [0 .. pointCount * constraintCount - 1]
+          , let a = index `mod` pointCount
+                b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
+          , a /= b
+          ]
+      )
+
+-- | Every k-th Delaunay edge: each is accepted without a corridor, so the
+-- resident constraint set is as dense as the request count.
+delaunayEdges :: Int -> DelaunayTriangulation Point -> BuildResult 'Unconstrained Point () () () -> IO (V.Vector (VertexId, VertexId))
+delaunayEdges constraintCount mesh _ =
+  pure
+    ( V.fromList
+        ( take constraintCount
+            [ undirectedEndpoints mesh edge
+            | (rank, edge) <- zip [0 :: Int ..] (undirectedEdges mesh)
+            , rank `mod` stride == 0
+            ]
+        )
+    )
+ where
+  stride = max 1 (numUndirectedEdges mesh `quot` constraintCount)
+
+-- | Batch recovery on a fixture, then singleton admission against the
+-- recovered mesh: the same number of accepted and rejected requests, so the
+-- admission prescan and the corridor walk are measured on both outcomes.
+benchmarkConstraintFixture
+  :: String
+  -> Int
+  -> (DelaunayTriangulation Point -> BuildResult 'Unconstrained Point () () () -> IO (V.Vector (VertexId, VertexId)))
+  -> IO ()
+benchmarkConstraintFixture label pointCount requestsOf = do
+  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x94d049bb133111eb pointCount)))
+  let mesh = buildTriangulation built
+      cdt = fromDelaunay mesh
+  pairs <- requestsOf mesh built
+  batch <- timedValue label (requireRight (recoverConstraints cdt pairs))
+  putStrLn (label <> "-stats: " <> show (constraintBatchStats batch))
+  let constrained = constraintBatchTriangulation batch
+      resident = numVertices constrained
+      singletonCount = 200
+      candidates =
+        [ (VertexId (fromIntegral a), VertexId (fromIntegral b))
+        | index <- [0 .. 40 * singletonCount * 50 - 1 :: Int]
+        , let a = (index * 7919 + 17) `mod` resident
+              b = (index * 104_729 + resident `quot` 3) `mod` resident
+        , a /= b
+        ]
+      (admitted, blocked) = List.partition (uncurry (canAddConstraint constrained)) candidates
+      acceptedPairs = take singletonCount admitted
+      rejectedPairs = take singletonCount blocked
+      pointOf = vertexPoint constrained
+      publishedEdges :: Either CdtError (ConstraintResult Point () () ()) -> Int
+      publishedEdges = either (const 0) (numUndirectedEdges . constraintRecoveryTriangulation)
+      refusals :: Either CdtError (ConstraintResult Point () () ()) -> Int
+      refusals = either (const 1) (const 0)
+  unless (length acceptedPairs == singletonCount && length rejectedPairs == singletonCount) $
+    fail (label <> " could not draw enough admitted and blocked singleton requests")
+  _ <- evaluate (force (fmap (\(a, b) -> (pointOf a, pointOf b)) (acceptedPairs <> rejectedPairs)))
+  acceptedEdges <-
+    timedValue (label <> "/singleton-add-accepted") $
+      traverse
+        (\(a, b) -> evaluate (publishedEdges (addConstraintEdge constrained (pointOf a) (pointOf b))))
+        acceptedPairs
+  rejectedRefusals <-
+    timedValue (label <> "/singleton-add-rejected") $
+      traverse
+        (\(a, b) -> evaluate (refusals (addConstraintEdge constrained (pointOf a) (pointOf b))))
+        rejectedPairs
+  putStrLn
+    ( label
+        <> "/singleton-outcomes: accepted-published="
+        <> show (length (filter (> 0) acceptedEdges))
+        <> "/"
+        <> show singletonCount
+        <> " rejected-refused="
+        <> show (sum rejectedRefusals)
+        <> "/"
+        <> show singletonCount
+    )
+
+-- Ruppert refinement on a constrained square. The Steiner budget is the
+-- variable of interest: both the encroachment search and the outer-region
+-- classification are per-insertion costs, so their growth shows as a widening
+-- gap between the two budgets rather than in either figure alone.
+benchmarkRefinement :: Int -> IO ()
+benchmarkRefinement steinerBudget = do
+  cdtBuild <- requireRight $ constrainedDelaunay
+    unitElementDefaults
+    (V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64, Point 20 20, Point 44 44] :: V.Vector (Point))
+    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5)])
+  let cdt :: ConstrainedDelaunayTriangulation (Point)
+      cdt = buildTriangulation cdtBuild
+      parameters :: Int -> Either BuildError RefinementParameters
+      parameters budget =
+        fmap
+          (withOuterFaceExclusion True . withConstraintPreservation False)
+          ( withAdditionalVertexBudget budget defaultRefinementParameters
+              >>= withMaximumArea 0.5
+              >>= withMaximumRadiusEdgeRatio 1.0
+          )
+  forM_ [steinerBudget `quot` 4, steinerBudget] $ \budget -> do
+    budgeted <- requireRight (parameters budget)
+    refined <- timedValue ("refine/steiner-" <> show budget) (requireRight (refine id budgeted cdt))
+    let stats = refinementStats refined
+    putStrLn ("refine-added/" <> show budget <> ": " <> show (refinementAddedVertices refined))
+    putStrLn
+      ( "refine-work/"
+          <> show budget
+          <> ": location-steps="
+          <> show ((buildStat LocationWalkSteps) stats)
+          <> ", face-checks="
+          <> show ((buildStat RefinementFaceChecks) stats)
+          <> ", queue-pops="
+          <> show ((buildStat RefinementQueuePops) stats)
+          <> ", flips="
+          <> show ((buildStat EdgeFlips) stats)
+      )
+
+canonicalEdges :: Triangulation mode vertex directed undirected face -> [(Point, Point)]
+canonicalEdges triangulation =
+  sort
+    [ ordered (vertexPoint triangulation (origin triangulation edge)) (vertexPoint triangulation (destination triangulation edge))
+    | undirected <- undirectedEdges triangulation
+    , let edge = normalizedDirected undirected
+    ]
+ where
+  ordered :: Ord value => value -> value -> (value, value)
+  ordered left right = if left <= right then (left, right) else (right, left)
diff --git a/bench/build/Moonlight/Triangulation/BuildBench.hs b/bench/build/Moonlight/Triangulation/BuildBench.hs
deleted file mode 100644
--- a/bench/build/Moonlight/Triangulation/BuildBench.hs
+++ /dev/null
@@ -1,185 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | The construction side: circle-sweep bulk load against the arrival-order
--- session kernel, persistent single insertion, constraint recovery and Ruppert
--- refinement. Each reports the library's own work counters alongside the time,
--- because the claim being measured is about work done rather than seconds.
-module Moonlight.Triangulation.BuildBench (benchmarks) where
-
-import BenchMeasure (requireRight, timedValue)
-import BenchSupport (randomPoints)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad (forM_, unless)
-import Data.List (sort)
-import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad (empty, insert)
-import Moonlight.Triangulation.Cdt (recoverConstraints)
-import Moonlight.Triangulation.Foreign.Mesh (insertGeometryBatch)
--- The facade withholds this constructor. The benchmark indexes the builder's
--- own input mapping, so every handle it forges is one the builder issued, and
--- it owns that obligation explicitly by naming the module that grants it.
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (VertexId))
-import Moonlight.Triangulation.Internal.Session (insertVertex, insertVertexAt, withLocalSession, withSession)
-import Moonlight.Triangulation.Types
-  ( InsertionResult (insertionTriangulation)
-  , refinementStats
-  , statEdgeFlips
-  , statLocationWalkSteps
-  , statRefinementFaceChecks
-  , statRefinementQueuePops
-  )
-
-benchmarks :: IO ()
-benchmarks = do
-  putStrLn "moonlight-triangulation native construction benchmark"
-  forM_ [1_000, 10_000, 50_000] benchmarkConstruction
-  forM_ [1_000, 10_000, 50_000, 100_000, 1_000_000] benchmarkSingletonInsertionCrossover
-  forM_ [(50_000, 1_000), (50_000, 10_000), (50_000, 50_000)] benchmarkGeometryBatchInsertion
-  benchmarkConstraints
-  benchmarkRefinement 2_500
-
--- | What the geometry-only foreign batch entrance costs against the session
--- insertion it is built out of. The two arms differ by exactly one thing: the
--- ABI route validates every point into an intermediate boxed vector before it
--- opens the session, and the session arm receives the same points with that
--- pass already paid. Their difference is therefore the price of admission,
--- and it is the number that decides whether a fused geometry entrance is worth
--- designing at all. Inserting into an existing mesh is not the law of fresh
--- circle sweep, so nothing here transfers from the construction lanes above.
-benchmarkGeometryBatchInsertion :: (Int, Int) -> IO ()
-benchmarkGeometryBatchInsertion (baseCount, addedCount) = do
-  let label suffix = "geometry-batch-" <> suffix <> "/" <> show baseCount <> "+" <> show addedCount
-      basePoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 baseCount)
-      addedPoints = V.fromList (randomPoints 0xbf58476d1ce4e5b9 addedCount)
-  base <- evaluate . force =<< requireRight (delaunayGeometry basePoints)
-  _ <- evaluate (force addedPoints)
-  admitted <- timedValue (label "abi") $ requireRight (insertGeometryBatch base addedPoints)
-  (_, sessioned, _) <- timedValue (label "session") $
-    requireRight
-      ( withSession base (V.length addedPoints) $
-          V.mapM_ (\point -> () <$ insertVertexAt point ()) addedPoints
-      )
-  admittedCanonical <- requireRight (canonicalize admitted)
-  sessionedCanonical <- requireRight (canonicalize sessioned)
-  equal <- evaluate (force (admittedCanonical == sessionedCanonical))
-  unless equal $
-    fail (label "witness" <> ": the admitted and session arms disagree")
-  putStrLn (label "witness" <> ": ok")
-
-benchmarkConstruction :: Int -> IO ()
-benchmarkConstruction count = do
-  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
-  swept <- timedValue ("circle-sweep/" <> show count) $ requireRight (delaunay unitElementDefaults points)
-  (_, sessioned, _) <- timedValue ("session/" <> show count) $
-    requireRight
-      ( withSession (empty unitElementDefaults) (V.length points) $
-          V.mapM_ insertVertex points
-      )
-  evaluate (force (canonicalEdges (buildTriangulation swept) == canonicalEdges sessioned)) >>= \equal ->
-    if equal then pure () else fail "circle-sweep and session construction disagree"
-
-benchmarkSingletonInsertionCrossover :: Int -> IO ()
-benchmarkSingletonInsertionCrossover count = do
-  let points = V.fromList (randomPoints 0xd1b54a32d192ed03 count)
-  built <- requireRight (delaunay unitElementDefaults points)
-  let query = Point 0.000_123_456_7 (-0.000_765_432_1)
-      base = buildTriangulation built
-  scheduled <- timedValue ("singleton-scheduled-public-insert/" <> show count) $ requireRight (insert base query)
-  (_, local, _) <- timedValue ("singleton-local-session-insert/" <> show count) $
-    requireRight (withLocalSession base 1 (insertVertex query))
-  let scheduledMesh = insertionTriangulation scheduled
-  unless (null (validateTriangulation scheduledMesh)) $
-    fail ("scheduled singleton insertion produced an invalid triangulation at " <> show count <> " sites")
-  unless (null (validateTriangulation local)) $
-    fail ("local singleton insertion produced an invalid triangulation at " <> show count <> " sites")
-  scheduledCanonical <- requireRight (canonicalize scheduledMesh)
-  localCanonical <- requireRight (canonicalize local)
-  equal <- evaluate (force (scheduledCanonical == localCanonical))
-  unless equal $
-    fail ("scheduled and local singleton insertion disagree semantically at " <> show count <> " sites")
-  putStrLn ("singleton-insertion-crossover/" <> show count <> "-semantic-witness: ok")
-
-benchmarkConstraints :: IO ()
-benchmarkConstraints = do
-  let pointCount = 8_000
-      constraintCount = 800
-  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x94d049bb133111eb pointCount)))
-  let cdt = fromDelaunay (buildTriangulation built)
-      inputMapping = buildInputVertices built
-      requestIndices =
-        V.fromList
-          ( take constraintCount
-              [ (a, b)
-              | index <- [0 .. pointCount * constraintCount - 1]
-              , let a = index `mod` pointCount
-                    b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
-              , a /= b
-              ]
-          )
-  pairs <-
-    V.mapM
-      (\(fromIndex, toIndex) ->
-        let len = sizeofPrimArray inputMapping
-            mFrom = if fromIndex >= 0 && fromIndex < len then Just (VertexId (indexPrimArray inputMapping fromIndex)) else Nothing
-            mTo = if toIndex >= 0 && toIndex < len then Just (VertexId (indexPrimArray inputMapping toIndex)) else Nothing
-        in case (mFrom, mTo) of
-          (Just from, Just to) -> pure (from, to)
-          _ -> fail "constraint benchmark endpoint is out of range"
-      )
-      requestIndices
-  batch <- timedValue "cdt/recovery" (requireRight (recoverConstraints cdt pairs))
-  putStrLn ("cdt/recovery-stats: " <> show (constraintBatchStats batch))
-
--- Ruppert refinement on a constrained square. The Steiner budget is the
--- variable of interest: both the encroachment search and the outer-region
--- classification are per-insertion costs, so their growth shows as a widening
--- gap between the two budgets rather than in either figure alone.
-benchmarkRefinement :: Int -> IO ()
-benchmarkRefinement steinerBudget = do
-  cdtBuild <- requireRight $ constrainedDelaunay
-    unitElementDefaults
-    (V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64, Point 20 20, Point 44 44] :: V.Vector (Point))
-    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5)])
-  let cdt :: ConstrainedDelaunayTriangulation (Point)
-      cdt = buildTriangulation cdtBuild
-      parameters :: Int -> RefinementParameters
-      parameters budget = defaultRefinementParameters
-        { refineMaxAdditionalVertices = Just budget
-        , refineMaxArea = Just 0.5
-        , refineMaxRadiusEdgeRatio = Just 1.0
-        , refineExcludeOuterFaces = True
-        , refineKeepConstraintEdges = False
-        }
-  forM_ [steinerBudget `quot` 4, steinerBudget] $ \budget -> do
-    refined <- timedValue ("refine/steiner-" <> show budget) (requireRight (refine id (parameters budget) cdt))
-    let stats = refinementStats refined
-    putStrLn ("refine-added/" <> show budget <> ": " <> show (refinementAddedVertices refined))
-    putStrLn
-      ( "refine-work/"
-          <> show budget
-          <> ": location-steps="
-          <> show (statLocationWalkSteps stats)
-          <> ", face-checks="
-          <> show (statRefinementFaceChecks stats)
-          <> ", queue-pops="
-          <> show (statRefinementQueuePops stats)
-          <> ", flips="
-          <> show (statEdgeFlips stats)
-      )
-
-canonicalEdges :: Triangulation mode vertex directed undirected face -> [(Point, Point)]
-canonicalEdges triangulation =
-  sort
-    [ ordered (vertexPoint triangulation (origin triangulation edge)) (vertexPoint triangulation (destination triangulation edge))
-    | undirected <- undirectedEdges triangulation
-    , let edge = normalizedDirected undirected
-    ]
- where
-  ordered :: Ord value => value -> value -> (value, value)
-  ordered left right = if left <= right then (left, right) else (right, left)
diff --git a/bench/curve/Main.hs b/bench/curve/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/curve/Main.hs
@@ -0,0 +1,7 @@
+module Main (main) where
+
+import qualified Moonlight.Planar.CurveBench as Curve
+import qualified Moonlight.Planar.AuthoringBench as Authoring
+
+main :: IO ()
+main = Curve.benchmarks >> Authoring.benchmarks
diff --git a/bench/curve/Moonlight/Planar/AuthoringBench.hs b/bench/curve/Moonlight/Planar/AuthoringBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/curve/Moonlight/Planar/AuthoringBench.hs
@@ -0,0 +1,123 @@
+-- | Matched attachment fixture. Admission is outside the timed action;
+-- ordered world curves and every requested port are fully forced inside it.
+module Moonlight.Planar.AuthoringBench (benchmarks) where
+
+import BenchMeasure (timedProjection)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (toList, traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+import Moonlight.Planar.Curve
+import Moonlight.Planar.Exact
+import Moonlight.Planar.Illustration
+import Moonlight.Planar.Illustration.Motif
+import Moonlight.Planar.Illustration.Layout
+
+data Port = Root | Tip
+
+type Observation = ([Path], [AffineIso2])
+
+type Instance = Motif Port ()
+
+benchmarks :: IO ()
+benchmarks = do
+  source <- admitted (affine2 (ExactVector 2 1) (ExactVector 1 2) (ExactVector 7 (-3)))
+  tip <- admitted (affine2 (ExactVector 1 0) (ExactVector 1 1) (ExactVector 8 12))
+  targets <- traverse (admitted . target) [1 .. 4096]
+  inputs <- evaluate (force (source, tip, targets))
+  instances <- timedProjection "attachment-authoring-n4096" observe
+    (evaluate (assemble inputs))
+  let (curves, ports) = observe instances
+      checksum = sum (pathChecksum <$> curves) + sum (frameChecksum <$> ports)
+  putStrLn ("attachment-authoring-receipt: instances=4096 paths=" <> show (length curves)
+    <> " ports=" <> show (length ports) <> " exact-checksum=" <> show checksum)
+  putStrLn "attachment-authoring-contract: admitted-inputs-outside; full-world-path-and-port-NF-inside"
+  traverse_ (benchmarkLayout source tip) [1024,2048]
+
+admitted :: Affine2 -> IO AffineIso2
+admitted = maybe (fail "benchmark frame is singular") pure . affineIso2
+
+target :: Integer -> Affine2
+target index =
+  let n = fromInteger index
+   in affine2 (ExactVector 1 (n * exactThird)) (ExactVector 0 2) (ExactVector (25 * n) (3 * n))
+
+assemble :: (AffineIso2, AffineIso2, [AffineIso2]) -> [Instance]
+assemble (source, tip, targets) = fmap instantiate targets
+ where
+  ports :: Port -> AffineIso2
+  ports Root = source
+  ports Tip = tip
+  instantiate :: AffineIso2 -> Instance
+  instantiate destination = attachMotif Root destination (motif artwork ports)
+{-# NOINLINE assemble #-}
+
+artwork :: Picture ()
+artwork = fill NonZero (Solid (RGB 80 150 110))
+  (ellipse (ExactVector 8 2) (ExactVector (-1) 12) :| [])
+
+observe :: [Instance] -> Observation
+observe instances =
+  (concatMap (worldPaths . motifPicture) instances, concatMap (\value -> fmap (motifPort value) [Root,Tip]) instances)
+
+-- New capability costs, not comparisons with a reference envelope/layout engine.
+benchmarkLayout :: AffineIso2 -> AffineIso2 -> Int -> IO ()
+benchmarkLayout source tip count = do
+  targets <- traverse (admitted . target) [1 .. fromIntegral count]
+  inputs <- evaluate (force (source, tip, targets))
+  let instances = assemble inputs
+  _ <- evaluate (force (observe instances))
+  envelope <- timedProjection ("geometry-envelope-n" <> show count) observeEnvelope
+    (evaluate (foldMap (geometryEnvelope . motifPicture) instances))
+  putStrLn ("geometry-envelope-receipt: instances=" <> show count <> " observations=" <> show (observeEnvelope envelope))
+  arranged <- timedProjection ("motif-layout-n" <> show count) (observe . toList)
+    (evaluate (arrangeMotifs Horizontal 3 (Seq.fromList instances)))
+  let (curves,ports) = observe (toList arranged)
+  putStrLn ("motif-layout-receipt: instances=" <> show count
+    <> " paths=" <> show (length curves) <> " ports=" <> show (length ports)
+    <> " exact-checksum=" <> show (sum (pathChecksum <$> curves) + sum (frameChecksum <$> ports)))
+  putStrLn "envelope-layout-contract: new-capability; prepared-inputs-outside; envelope-support-or-full-world-path-and-port-NF-inside"
+
+observeEnvelope
+  :: GeometryEnvelope
+  -> (Maybe (ExactRational, ExactRational, ExactRational, ExactRational), [Maybe ExactRational])
+observeEnvelope envelope =
+  (fmap (\bounds -> (boundsMinimumX bounds, boundsMinimumY bounds, boundsMaximumX bounds, boundsMaximumY bounds))
+    (geometryBounds envelope), fmap (`geometrySupport` envelope)
+    [ExactVector 1 0, ExactVector 0 1, ExactVector (-1) 0, ExactVector 0 (-1), ExactVector 2 3])
+
+worldPaths :: Picture part -> [Path]
+worldPaths picture = foldPicture algebra picture identityAffine2
+ where
+  algebra :: PictureAlgebra part (Affine2 -> [Path])
+  algebra = PictureAlgebra
+    { paintSequence = \children frame -> concatMap ($ frame) (toList children)
+    , paintFill = \_ _ contours frame -> [transformPath frame (path (Seq.fromList (ClosedSubpath <$> toList contours)))]
+    , paintStroke = \_ curve frame -> [transformPath frame curve]
+    , paintClip = \_ contours child frame ->
+        transformPath frame (path (Seq.fromList (ClosedSubpath <$> toList contours))) : child frame
+    , paintPlace = \placement child frame -> child (composeAffine2 frame placement)
+    , paintOpacity = \_ child -> child
+    , paintAnnotation = \_ child -> child
+    }
+
+pathChecksum :: Path -> ExactRational
+pathChecksum = sum . fmap subpathChecksum . toList . pathSubpaths
+ where
+  subpathChecksum :: Subpath -> ExactRational
+  subpathChecksum (OpenSubpath value) = trailChecksum (location value) (toList (trailSteps (locatedValue value)))
+  subpathChecksum (ClosedSubpath value) = trailChecksum (location value) (toList (closedTrailSteps (locatedValue value)))
+  trailChecksum :: ExactPoint -> [CurveStep] -> ExactRational
+  trailChecksum origin steps =
+    let (x,y) = exactPointCoordinates origin
+     in x + y + sum (fmap (sum . fmap vectorChecksum . toList . stepControlPoints) steps)
+
+frameChecksum :: AffineIso2 -> ExactRational
+frameChecksum frame =
+  let (x,y,offset) = affineColumns (affineIsoMap frame)
+   in vectorChecksum x + vectorChecksum y + vectorChecksum offset
+
+vectorChecksum :: ExactVector -> ExactRational
+vectorChecksum (ExactVector x y) = x + y
diff --git a/bench/curve/Moonlight/Planar/CurveBench.hs b/bench/curve/Moonlight/Planar/CurveBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/curve/Moonlight/Planar/CurveBench.hs
@@ -0,0 +1,159 @@
+-- | New-capability receipts, not a comparison with fixed-sample artwork.
+-- Timings include exact leaf-bound observation and complete output forcing.
+module Moonlight.Planar.CurveBench (benchmarks) where
+
+import BenchMeasure (requireRight, timedProjection)
+import Control.DeepSeq (NFData (..), force)
+import Control.Exception (evaluate)
+import Data.Foldable (toList, traverse_)
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( Affine2, affine2, identityAffine2 )
+import Moonlight.Planar.Curve
+  ( Subpath (..), CurveStep, ClosedTrail, Located, Path
+  , closeWith, closedTrailSteps, cubic, curveStep, ellipse, hermiteStep, line
+  , locate, locatedValue, location, transformPath, openTrail, path
+  , pathSubpaths, stepControlPoints, trailSteps )
+import Moonlight.Planar.Curve.Lowering
+  ( LoweredPath, LoweringError, LoweringPolicy, loweredSpans, loweredTolerance
+  , lowerClosedTrail, lowerOpenTrail, loweringPolicy, spanFrom, spanParameterFrom
+  , spanParameterTo, spanSquaredBound, spanTo )
+import Moonlight.Planar.Exact
+  ( ExactRational, ExactVector (..), exactPoint, exactPointBitWidth
+  , exactRational, exactRationalBitWidth, positiveExact, positiveExactValue )
+
+data CurveFamily
+  = HornSpans
+  | ExactCircle
+  | SkewEllipse
+  | RepeatedMotifs
+  | AnisotropicHorn
+  | CuspsAndOvershoot
+  deriving stock (Eq, Show)
+
+data CurveReceipt = CurveReceipt
+  { emittedLeaves :: !Int
+  , maximumCoordinateBits :: !Int
+  , maximumParameterBits :: !Int
+  , checkedLeafBounds :: !Int
+  , allLeafBoundsPass :: !Bool
+  }
+  deriving stock (Show)
+
+instance NFData CurveReceipt where
+  rnf receipt = rnf (emittedLeaves receipt)
+    `seq` rnf (maximumCoordinateBits receipt)
+    `seq` rnf (maximumParameterBits receipt)
+    `seq` rnf (checkedLeafBounds receipt)
+    `seq` rnf (allLeafBoundsPass receipt)
+
+instance Semigroup CurveReceipt where
+  a <> b = CurveReceipt
+    (emittedLeaves a + emittedLeaves b)
+    (max (maximumCoordinateBits a) (maximumCoordinateBits b))
+    (max (maximumParameterBits a) (maximumParameterBits b))
+    (checkedLeafBounds a + checkedLeafBounds b)
+    (allLeafBoundsPass a && allLeafBoundsPass b)
+
+instance Monoid CurveReceipt where
+  mempty = CurveReceipt 0 0 0 0 True
+
+benchmarks :: IO ()
+benchmarks = do
+  putStrLn "curve-benchmark: new capability; no fixed-sample speedup or topology claim"
+  putStrLn "curve-benchmark: epsilon=1/8 in declared affine metric; max-depth=16; max-leaves=8192 per subpath"
+  putStrLn "curve-benchmark: elapsed includes exact leaf-bound observations and full output forcing; process max-live is cumulative"
+  traverse_ benchmarkFamily
+    [HornSpans, ExactCircle, SkewEllipse, RepeatedMotifs, AnisotropicHorn, CuspsAndOvershoot]
+
+benchmarkFamily :: CurveFamily -> IO ()
+benchmarkFamily family = do
+  tolerance <- requireRight (exactRational 1 8) >>= requireRight . positiveExact
+  policy <- requireRight (loweringPolicy tolerance (metric family) 16 8192)
+  authored <- evaluate (force (geometry family))
+  let sourceBits = inputBits authored
+      sourceSteps = sum (length . stepsOf <$> toList (pathSubpaths authored))
+  result <- timedProjection ("curve-" <> show family) observe
+    (requireRight (traverse (lowerSubpath policy) (toList (pathSubpaths authored))))
+  let receipt = observe result
+  putStrLn ("curve-" <> show family <> "-receipt: input-steps=" <> show sourceSteps
+    <> " input-coordinate-bits=" <> show sourceBits
+    <> " observed-coordinate-bit-growth=" <> show (max 0 (maximumCoordinateBits receipt - sourceBits))
+    <> " " <> show receipt)
+  if allLeafBoundsPass receipt
+    then pure ()
+    else fail ("curve leaf bound failed for " <> show family)
+
+observe :: [LoweredPath] -> CurveReceipt
+observe = foldMap observePath
+
+observePath :: LoweredPath -> CurveReceipt
+observePath lowered = foldMap observeSpan (loweredSpans lowered)
+ where
+  epsilon = positiveExactValue (loweredTolerance lowered)
+  observeSpan spanValue = CurveReceipt 1
+    (max (exactPointBitWidth (spanFrom spanValue)) (exactPointBitWidth (spanTo spanValue)))
+    (max (exactRationalBitWidth (spanParameterFrom spanValue))
+      (exactRationalBitWidth (spanParameterTo spanValue)))
+    1 (spanSquaredBound spanValue >= 0 && spanSquaredBound spanValue <= epsilon * epsilon)
+
+lowerSubpath :: LoweringPolicy -> Subpath -> Either LoweringError LoweredPath
+lowerSubpath policy subpath = case subpath of
+  OpenSubpath value -> lowerOpenTrail policy value
+  ClosedSubpath value -> lowerClosedTrail policy value
+
+inputBits :: Path -> Int
+inputBits = foldr max 0 . fmap subpathBits . toList . pathSubpaths
+ where
+  subpathBits :: Subpath -> Int
+  subpathBits subpath = foldr max 0
+    (anchorBits subpath : fmap vectorBits (concatMap (toList . stepControlPoints) (stepsOf subpath)))
+  anchorBits :: Subpath -> Int
+  anchorBits (OpenSubpath value) = exactPointBitWidth (location value)
+  anchorBits (ClosedSubpath value) = exactPointBitWidth (location value)
+  vectorBits :: ExactVector -> Int
+  vectorBits (ExactVector x y) = max (exactRationalBitWidth x) (exactRationalBitWidth y)
+
+stepsOf :: Subpath -> [CurveStep]
+stepsOf (OpenSubpath value) = toList (trailSteps (locatedValue value))
+stepsOf (ClosedSubpath value) = toList (closedTrailSteps (locatedValue value))
+
+metric :: CurveFamily -> Affine2
+metric AnisotropicHorn = affine2 (ExactVector 12 2) (ExactVector 0 1) (ExactVector 0 0)
+metric _ = identityAffine2
+
+geometry :: CurveFamily -> Path
+geometry HornSpans = closedPath horn
+geometry ExactCircle = closedPath (ellipse (ExactVector 40 0) (ExactVector 0 40))
+geometry SkewEllipse = closedPath (ellipse (ExactVector 70 15) (ExactVector (-12) 28))
+geometry RepeatedMotifs = foldMap motif [0 .. 11]
+geometry AnisotropicHorn = closedPath horn
+geometry CuspsAndOvershoot = path (Seq.fromList (OpenSubpath . locate (exactPoint 0 0)
+  . openTrail . Seq.singleton <$> pathologicalSteps))
+
+closedPath :: Located ClosedTrail -> Path
+closedPath = path . Seq.singleton . ClosedSubpath
+
+-- Two Hermite rails share root and tip; changing a middle jet remains local.
+horn :: Located ClosedTrail
+horn = locate (exactPoint 0 0) (closeWith line (openTrail (Seq.fromList
+  [ hermiteStep (ExactVector 28 (-50)) (ExactVector 15 (-65)) (ExactVector 40 (-30))
+  , hermiteStep (ExactVector 22 (-30)) (ExactVector 40 (-30)) (ExactVector 8 (-35))
+  , hermiteStep (ExactVector (-32) 35) (ExactVector (-45) 10) (ExactVector (-35) 25)
+  , hermiteStep (ExactVector (-18) 45) (ExactVector (-35) 25) (ExactVector (-8) 60)
+  ])))
+
+motif :: Integer -> Path
+motif index = transformPath placement
+  (closedPath (ellipse (ExactVector 10 3) (ExactVector (-2) 4)))
+ where
+  n :: ExactRational
+  n = fromInteger index
+  placement = affine2 (ExactVector 1 n) (ExactVector 0 1) (ExactVector (25 * n) (3 * n))
+
+pathologicalSteps :: [CurveStep]
+pathologicalSteps =
+  [ curveStep (cubic (ExactVector 60 80) (ExactVector (-60) 80)) (ExactVector 0 0)
+  , curveStep (cubic (ExactVector 100 0) (ExactVector (-100) 0)) (ExactVector 1 0)
+  , curveStep (cubic (ExactVector 1 80) (ExactVector (-1) (-80))) (ExactVector 2 0)
+  ]
diff --git a/bench/dcel/Main.hs b/bench/dcel/Main.hs
--- a/bench/dcel/Main.hs
+++ b/bench/dcel/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.DcelBench as DcelBench
+import qualified Moonlight.Planar.DcelBench as DcelBench
 
 main :: IO ()
 main = DcelBench.benchmarks
diff --git a/bench/dcel/Moonlight/Planar/DcelBench.hs b/bench/dcel/Moonlight/Planar/DcelBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/dcel/Moonlight/Planar/DcelBench.hs
@@ -0,0 +1,213 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Read-side traversal over a finished mesh: ordered line intersection and the
+-- circle shape query. Construction here is fixture cost, not the subject.
+module Moonlight.Planar.DcelBench (benchmarks) where
+
+import BenchSupport
+  ( latticeFaceBand
+  , latticePoints
+  , randomPoints
+  )
+import BenchMeasure (requireRight, timedValue)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import Data.Word (Word64)
+import qualified Data.List as List
+import qualified Data.Vector as V
+import Moonlight.Planar.Alpha (alphaShapeContainsFace)
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Dcel (incidentFace, numFaces, numVertices, origin, destination, vertexPoint)
+import Moonlight.Planar.HintGenerator (buildHierarchyHint, hierarchyHint)
+import Moonlight.Planar.Internal.HandleDefs (FaceId (..), VertexId (..), normalizedDirected, unDirectedEdgeId, unFaceId, unVertexId)
+import Moonlight.Planar.PointLocation (locatePointWithHint)
+import Moonlight.Planar.FloodFillIterator (BoundaryObstruction (..), FaceComponent, RegionBoundary, boundaryLoopVertices, componentBoundary, edgesInCircle, faceComponentFaces, faceComponents, regionBoundaryHoleLoops, regionBoundaryOuterLoop)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces, undirectedEdges, vertices)
+import Moonlight.Planar.IntersectionIterator (lineIntersections)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Types (DelaunayTriangulation, Location (..), LocationHint (..), LocationStats (..), buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (Point (..), QueryPoint)
+import Moonlight.Planar.Scalar (mkRadiusSquared)
+
+benchmarks :: IO ()
+benchmarks = do
+  benchmarkPointLocation 20_000 10_000
+  benchmarkQueries 20_000 10_000
+  benchmarkRegionWorkload 440 272 22
+
+benchmarkQueries :: Int -> Int -> IO ()
+benchmarkQueries pointCount queryCount = do
+  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0xbf58476d1ce4e5b9 pointCount)))
+  circleEdges <- requireRight (edgesInCircle (buildTriangulation built) (Point 0 0) 0.25)
+  queries <-
+    requireRight
+      (traverse mkQueryPoint (V.fromList (take (2 * queryCount) (randomPoints 0x632be59bd9b4e019 (2 * queryCount)))))
+  let triangulation = buildTriangulation built
+      total = V.ifoldl' (lineCount triangulation queries queryCount) 0 (V.take queryCount queries)
+      shapeTotal = length circleEdges
+  _ <- timedValue "line-and-shape-queries" (pure (total, shapeTotal))
+  pure ()
+ where
+  lineCount
+    :: DelaunayTriangulation (Point)
+    -> V.Vector (QueryPoint)
+    -> Int
+    -> Int
+    -> Int
+    -> QueryPoint
+    -> Int
+  lineCount triangulation queries stride !accumulator index from =
+    accumulator + length (lineIntersections triangulation from (queries V.! (index + stride)))
+
+-- | The workload that motivated region extraction: 239,360 bounded faces.
+-- Construction is shared fixture cost and is forced before either timed lane.
+benchmarkRegionWorkload :: Int -> Int -> Int -> IO ()
+benchmarkRegionWorkload widthInCells heightInCells expectedBandCount = do
+  built <-
+    requireRight
+      (delaunay unitElementDefaults (latticePoints widthInCells heightInCells))
+  triangulation <- evaluate (force (buildTriangulation built))
+  benchmarkRegionBoundaries triangulation expectedBandCount
+  benchmarkAlphaFaceMembership triangulation
+
+benchmarkRegionBoundaries :: DelaunayTriangulation Point -> Int -> IO ()
+benchmarkRegionBoundaries triangulation expectedBandCount = do
+  analysed <-
+    timedValue
+      "face-components-and-boundaries"
+      (evaluate (regionAnalysis triangulation))
+  (components, boundaries) <- requireRight analysed
+  let faceCount = sum (fmap (length . faceComponentFaces . snd) components)
+      outerLoopCount = length boundaries
+      holeLoopCount =
+        sum (fmap (length . regionBoundaryHoleLoops) boundaries)
+      boundaryVertexCount =
+        sum
+          ( fmap
+              (length . boundaryLoopVertices . regionBoundaryOuterLoop)
+              boundaries
+          )
+  if
+    ( faceCount
+    , length components
+    , outerLoopCount
+    , holeLoopCount
+    , boundaryVertexCount
+    )
+      == (239_360, expectedBandCount, expectedBandCount, 0, 88)
+    then
+      putStrLn
+        "face-components-and-boundaries-receipt: faces=239360 components=22 outer-loops=22 hole-loops=0 boundary-vertices=88"
+    else
+      fail
+        ( "region benchmark receipt mismatch: "
+            <> show
+              ( faceCount
+              , length components
+              , outerLoopCount
+              , holeLoopCount
+              , boundaryVertexCount
+              )
+        )
+ where
+  regionAnalysis
+    :: DelaunayTriangulation Point
+    -> Either
+        BoundaryObstruction
+        ([(Int, FaceComponent)], [RegionBoundary])
+  regionAnalysis mesh = do
+    let components = faceComponents mesh (latticeFaceBand mesh)
+    boundaries <-
+      traverse
+        (componentBoundary mesh . snd)
+        components
+    pure (components, boundaries)
+
+benchmarkAlphaFaceMembership :: DelaunayTriangulation Point -> IO ()
+benchmarkAlphaFaceMembership triangulation = do
+  threshold <- requireRight (mkRadiusSquared 0.5)
+  let containsFace = alphaShapeContainsFace threshold triangulation
+  admittedCount <-
+    timedValue
+      "alpha-face-membership"
+      ( evaluate
+          ( List.foldl'
+              (\count face -> if containsFace face then count + 1 else count)
+              (0 :: Int)
+              (innerFaces triangulation)
+          )
+      )
+  if admittedCount == 239_360
+    then putStrLn "alpha-face-membership-receipt: admitted=239360"
+    else fail ("alpha face membership receipt mismatch: " <> show admittedCount)
+
+-- | Force meshes and admitted queries before timing only the existing location
+-- owner. The digest retains constructor and resident identifier; step and
+-- fallback totals pin the path as well as the geometric answer.
+benchmarkPointLocation :: Int -> Int -> IO ()
+benchmarkPointLocation pointCount queryCount = do
+  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x1234_5678 pointCount)))
+  mesh <- evaluate (force (buildTriangulation built))
+  queries <- requireRight (traverse mkQueryPoint (V.fromList (randomPoints 0xdead_beef queryCount))) >>= evaluate . force
+  hierarchy <- requireRight (buildHierarchyHint 16 mesh) >>= evaluate . force
+  let noHints :: V.Vector (Maybe LocationHint, QueryPoint)
+      noHints = V.map ((,) Nothing) queries
+      hierarchyHints = V.map (\query -> (hierarchyHint hierarchy query, query)) queries
+      staleVertex = Just (VertexHint (VertexId (fromIntegral (numVertices mesh))))
+      staleFace = Just (FaceHint (FaceId (fromIntegral (numFaces mesh))))
+  traverse_
+    (\(label, section) -> evaluate (force section) >>= reportPointLocation mesh label)
+    [ ("point-location/random/no-hint", noHints)
+    , ("point-location/random/hierarchy-hint", hierarchyHints)
+    , ("point-location/random/stale-vertex", V.map ((,) staleVertex) queries)
+    , ("point-location/random/stale-face", V.map ((,) staleFace) queries)
+    ]
+  lattice <- requireRight (delaunay unitElementDefaults (latticePoints 64 64))
+  boundaryMesh <- evaluate (force (buildTriangulation lattice))
+  vertexQueries <- requireRight
+    (traverse (\vertex -> (,) (Just (VertexHint vertex)) <$> mkQueryPoint (vertexPoint boundaryMesh vertex)) (V.fromList (vertices boundaryMesh)))
+  edgeQueries <- requireRight
+    (traverse
+      (\edge ->
+        let forward = normalizedDirected edge
+            Point ax ay = vertexPoint boundaryMesh (origin boundaryMesh forward)
+            Point bx by = vertexPoint boundaryMesh (destination boundaryMesh forward)
+         in (,) (Just (FaceHint (incidentFace boundaryMesh forward)))
+              <$> mkQueryPoint (Point ((ax + bx) / 2) ((ay + by) / 2)))
+      (V.fromList (undirectedEdges boundaryMesh)))
+  traverse_
+    (\(label, section) -> evaluate (force section) >>= reportPointLocation boundaryMesh label)
+    [ ("point-location/boundary/vertices", vertexQueries)
+    , ("point-location/boundary/edges", edgeQueries)
+    ]
+
+reportPointLocation
+  :: DelaunayTriangulation Point
+  -> String
+  -> V.Vector (Maybe LocationHint, QueryPoint)
+  -> IO ()
+reportPointLocation mesh label queries = do
+  (digest, steps, fallbacks) <- timedValue label (evaluate (V.foldl' observe (0, 0, 0) queries))
+  putStrLn
+    (label <> "-receipt: queries=" <> show (V.length queries)
+      <> " semantic-digest=" <> show digest
+      <> " walk-steps=" <> show steps
+      <> " fallbacks=" <> show fallbacks)
+ where
+  observe :: (Word64, Int, Int) -> (Maybe LocationHint, QueryPoint) -> (Word64, Int, Int)
+  observe (!digest, !steps, !fallbacks) (hint, query) =
+    let (location, stats) = locatePointWithHint mesh hint query
+        (!tag, !resident) = case location of
+          EmptyTriangulation -> (0, 0)
+          OnVertex vertex -> (1, fromIntegral (unVertexId vertex))
+          OnEdge edge -> (2, fromIntegral (unDirectedEdgeId edge))
+          InFace face -> (3, fromIntegral (unFaceId face))
+          OutsideConvexHull Nothing -> (4, 0)
+          OutsideConvexHull (Just edge) -> (5, fromIntegral (unDirectedEdgeId edge))
+     in ( digest * 1099511628211 + tag * 4294967296 + resident
+        , steps + locationWalkSteps stats
+        , fallbacks + if locationUsedFallback stats then 1 else 0
+        )
diff --git a/bench/dcel/Moonlight/Triangulation/DcelBench.hs b/bench/dcel/Moonlight/Triangulation/DcelBench.hs
deleted file mode 100644
--- a/bench/dcel/Moonlight/Triangulation/DcelBench.hs
+++ /dev/null
@@ -1,132 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | Read-side traversal over a finished mesh: ordered line intersection and the
--- circle shape query. Construction here is fixture cost, not the subject.
-module Moonlight.Triangulation.DcelBench (benchmarks) where
-
-import BenchSupport
-  ( latticeFaceBand
-  , latticePoints
-  , randomPoints
-  )
-import BenchMeasure (requireRight, timedValue)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import qualified Data.List as List
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.FloodFillIterator (edgesInCircle)
-import Moonlight.Triangulation.IntersectionIterator (lineIntersections)
-
-benchmarks :: IO ()
-benchmarks = do
-  benchmarkQueries 20_000 10_000
-  benchmarkRegionWorkload 440 272 22
-
-benchmarkQueries :: Int -> Int -> IO ()
-benchmarkQueries pointCount queryCount = do
-  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0xbf58476d1ce4e5b9 pointCount)))
-  circleEdges <- requireRight (edgesInCircle (buildTriangulation built) (Point 0 0) 0.25)
-  queries <-
-    requireRight
-      (traverse mkQueryPoint (V.fromList (take (2 * queryCount) (randomPoints 0x632be59bd9b4e019 (2 * queryCount)))))
-  let triangulation = buildTriangulation built
-      total = V.ifoldl' (lineCount triangulation queries queryCount) 0 (V.take queryCount queries)
-      shapeTotal = length circleEdges
-  _ <- timedValue "line-and-shape-queries" (pure (total, shapeTotal))
-  pure ()
- where
-  lineCount
-    :: DelaunayTriangulation (Point)
-    -> V.Vector (QueryPoint)
-    -> Int
-    -> Int
-    -> Int
-    -> QueryPoint
-    -> Int
-  lineCount triangulation queries stride !accumulator index from =
-    accumulator + length (lineIntersections triangulation from (queries V.! (index + stride)))
-
--- | The workload that motivated region extraction: 239,360 bounded faces.
--- Construction is shared fixture cost and is forced before either timed lane.
-benchmarkRegionWorkload :: Int -> Int -> Int -> IO ()
-benchmarkRegionWorkload widthInCells heightInCells expectedBandCount = do
-  built <-
-    requireRight
-      (delaunay unitElementDefaults (latticePoints widthInCells heightInCells))
-  triangulation <- evaluate (force (buildTriangulation built))
-  benchmarkRegionBoundaries triangulation expectedBandCount
-  benchmarkAlphaFaceMembership triangulation
-
-benchmarkRegionBoundaries :: DelaunayTriangulation Point -> Int -> IO ()
-benchmarkRegionBoundaries triangulation expectedBandCount = do
-  analysed <-
-    timedValue
-      "face-components-and-boundaries"
-      (evaluate (regionAnalysis triangulation))
-  (components, boundaries) <- requireRight analysed
-  let faceCount = sum (fmap (length . faceComponentFaces . snd) components)
-      outerLoopCount = length boundaries
-      holeLoopCount =
-        sum (fmap (length . regionBoundaryHoleLoops) boundaries)
-      boundaryVertexCount =
-        sum
-          ( fmap
-              (length . boundaryLoopVertices . regionBoundaryOuterLoop)
-              boundaries
-          )
-  if
-    ( faceCount
-    , length components
-    , outerLoopCount
-    , holeLoopCount
-    , boundaryVertexCount
-    )
-      == (239_360, expectedBandCount, expectedBandCount, 0, 88)
-    then
-      putStrLn
-        "face-components-and-boundaries-receipt: faces=239360 components=22 outer-loops=22 hole-loops=0 boundary-vertices=88"
-    else
-      fail
-        ( "region benchmark receipt mismatch: "
-            <> show
-              ( faceCount
-              , length components
-              , outerLoopCount
-              , holeLoopCount
-              , boundaryVertexCount
-              )
-        )
- where
-  regionAnalysis
-    :: DelaunayTriangulation Point
-    -> Either
-        BoundaryObstruction
-        ([(Int, FaceComponent)], [RegionBoundary])
-  regionAnalysis mesh = do
-    let components = faceComponents mesh (latticeFaceBand mesh)
-    boundaries <-
-      traverse
-        (componentBoundary mesh . snd)
-        components
-    pure (components, boundaries)
-
-benchmarkAlphaFaceMembership :: DelaunayTriangulation Point -> IO ()
-benchmarkAlphaFaceMembership triangulation = do
-  threshold <- requireRight (mkRadiusSquared 0.5)
-  let containsFace = alphaShapeContainsFace threshold triangulation
-  admittedCount <-
-    timedValue
-      "alpha-face-membership"
-      ( evaluate
-          ( List.foldl'
-              (\count face -> if containsFace face then count + 1 else count)
-              (0 :: Int)
-              (innerFaces triangulation)
-          )
-      )
-  if admittedCount == 239_360
-    then putStrLn "alpha-face-membership-receipt: admitted=239360"
-    else fail ("alpha face membership receipt mismatch: " <> show admittedCount)
diff --git a/bench/dual/Main.hs b/bench/dual/Main.hs
--- a/bench/dual/Main.hs
+++ b/bench/dual/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.DualBench as DualBench
+import qualified Moonlight.Planar.DualBench as DualBench
 
 main :: IO ()
 main = DualBench.benchmarks
diff --git a/bench/dual/Moonlight/Planar/DualBench.hs b/bench/dual/Moonlight/Planar/DualBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/dual/Moonlight/Planar/DualBench.hs
@@ -0,0 +1,139 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | The dual side: Delaunay-hierarchy hints against an unhinted walk, and
+-- natural-neighbour interpolation over a reused workspace. Both report
+-- allocation, because the claim in each case is about work avoided rather than
+-- time taken.
+module Moonlight.Planar.DualBench (benchmarks) where
+
+import BenchMeasure (requireRight, timedValue)
+import BenchSupport (randomPoints)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad.ST (stToIO)
+import qualified Data.Vector as V
+import GHC.Exts (RealWorld)
+import GHC.Stats (RTSStats (allocated_bytes), getRTSStats, getRTSStatsEnabled)
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Dcel (vertexPoint)
+import Moonlight.Planar.Handles.HandleDefs (VertexId)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Types (ConstraintMode (..), DelaunayTriangulation, buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (Point (..), QueryPoint)
+import Moonlight.Planar.Telemetry
+  ( LocationHint (VertexHint)
+  , NearestStats (nearestWalkSteps)
+  , nearestNeighbor
+  )
+import Moonlight.Planar.HintGenerator
+  ( HierarchyHint
+  , buildHierarchyHint
+  , defaultHierarchyBranchFactor
+  , hierarchyHint
+  , hierarchyLevelCount
+  , hierarchyVertexCount
+  )
+import Moonlight.Planar.Interpolation
+  ( NaturalNeighborWorkspace
+  , interpolateNaturalNeighbor
+  , newNaturalNeighborWorkspace
+  , workspaceBytes
+  )
+import System.Mem (performGC)
+
+benchmarks :: IO ()
+benchmarks = do
+  benchmarkHierarchy 20_000 5_000
+  benchmarkSibson 10_000 5_000
+
+benchmarkHierarchy :: Int -> Int -> IO ()
+benchmarkHierarchy pointCount queryCount = do
+  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x123456789abcdef pointCount)))
+  queries <-
+    requireRight
+      (traverse mkQueryPoint (V.fromList (take queryCount (randomPoints 0x3141592653589793 queryCount))))
+  let triangulation = buildTriangulation built
+  hierarchy <- requireRight (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+  (_, baselineSteps) <- timedValue "nearest/no-hierarchy" (evaluate (force (walkTotal triangulation Nothing queries)))
+  (_, hierarchySteps) <- timedValue "nearest/delaunay-hierarchy" (evaluate (force (walkHierarchyTotal triangulation hierarchy queries)))
+  putStrLn ("hierarchy-levels: " <> show (hierarchyLevelCount hierarchy))
+  putStrLn ("hierarchy-vertices: " <> show (hierarchyVertexCount hierarchy))
+  putStrLn ("nearest-walk-steps/no-hierarchy: " <> show baselineSteps)
+  putStrLn ("nearest-walk-steps/hierarchy: " <> show hierarchySteps)
+ where
+  walkTotal
+    :: DelaunayTriangulation (Point)
+    -> Maybe VertexId
+    -> V.Vector (QueryPoint)
+    -> (Int, Int)
+  walkTotal triangulation hint queries = V.foldl' step (0 :: Int, 0 :: Int) queries
+   where
+    step
+      :: (Int, Int)
+      -> QueryPoint
+      -> (Int, Int)
+    step (!count, !steps) query =
+      case nearestNeighbor triangulation hint query of
+        Nothing -> (count, steps)
+        Just (_, stats) -> (count + 1, steps + nearestWalkSteps stats)
+
+  walkHierarchyTotal
+    :: DelaunayTriangulation (Point)
+    -> HierarchyHint
+    -> V.Vector (QueryPoint)
+    -> (Int, Int)
+  walkHierarchyTotal triangulation hierarchy queries = V.foldl' step (0 :: Int, 0 :: Int) queries
+   where
+    step
+      :: (Int, Int)
+      -> QueryPoint
+      -> (Int, Int)
+    step (!count, !steps) query =
+      let hint = case hierarchyHint hierarchy query of
+            Just (VertexHint vertex) -> Just vertex
+            _ -> Nothing
+       in case nearestNeighbor triangulation hint query of
+            Nothing -> (count, steps)
+            Just (_, stats) -> (count + 1, steps + nearestWalkSteps stats)
+
+benchmarkSibson :: Int -> Int -> IO ()
+benchmarkSibson pointCount queryCount = do
+  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x8cb92baa3f3d8dd7 pointCount)))
+  queries <-
+    requireRight
+      (traverse mkQueryPoint (V.fromList (take queryCount (randomPoints 0xdb4f0b9175ae2165 queryCount))))
+  let triangulation = buildTriangulation built
+      height vertex =
+        let Point x y = vertexPoint triangulation vertex
+         in x * x + 0.5 * y
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  putStrLn ("sibson-workspace-bytes: " <> show (workspaceBytes workspace))
+  statsEnabled <- getRTSStatsEnabled
+  if statsEnabled then performGC else pure ()
+  before <- if statsEnabled then Just <$> getRTSStats else pure Nothing
+  _ <- timedValue "sibson/reused-workspace" (queryLoop workspace height queries)
+  after <- if statsEnabled then Just <$> getRTSStats else pure Nothing
+  case (before, after) of
+    (Just left, Just right) -> do
+      let allocated = allocated_bytes right - allocated_bytes left
+          perQuery = fromIntegral allocated / fromIntegral queryCount :: Double
+      putStrLn ("sibson-allocated-bytes-total: " <> show allocated)
+      putStrLn ("sibson-allocated-bytes/query: " <> show perQuery)
+    _ -> putStrLn "sibson allocation counters unavailable; run with +RTS -T"
+ where
+  queryLoop
+    :: NaturalNeighborWorkspace RealWorld 'Unconstrained (Point) () () ()
+    -> (VertexId -> Double)
+    -> V.Vector (QueryPoint)
+    -> IO Double
+  queryLoop workspace height queries = go 0 0
+   where
+    !count = V.length queries
+    go !index !total
+      | index >= count = evaluate total
+      | otherwise = do
+          (value, _) <- stToIO (interpolateNaturalNeighbor height workspace Nothing (queries V.! index))
+          go (index + 1) (total + maybe 0 id value)
diff --git a/bench/dual/Moonlight/Triangulation/DualBench.hs b/bench/dual/Moonlight/Triangulation/DualBench.hs
deleted file mode 100644
--- a/bench/dual/Moonlight/Triangulation/DualBench.hs
+++ /dev/null
@@ -1,121 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | The dual side: Delaunay-hierarchy hints against an unhinted walk, and
--- natural-neighbour interpolation over a reused workspace. Both report
--- allocation, because the claim in each case is about work avoided rather than
--- time taken.
-module Moonlight.Triangulation.DualBench (benchmarks) where
-
-import BenchMeasure (requireRight, timedValue)
-import BenchSupport (randomPoints)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad.ST (stToIO)
-import qualified Data.Vector as V
-import GHC.Exts (RealWorld)
-import GHC.Stats (RTSStats (allocated_bytes), getRTSStats, getRTSStatsEnabled)
-import Moonlight.Triangulation
-import Moonlight.Triangulation.HintGenerator
-  ( HierarchyHint
-  , buildHierarchyHint
-  , defaultHierarchyBranchFactor
-  , hierarchyHint
-  , hierarchyLevelCount
-  , hierarchyVertexCount
-  )
-import Moonlight.Triangulation.Interpolation
-  ( NaturalNeighborWorkspace
-  , interpolateNaturalNeighbor
-  , newNaturalNeighborWorkspace
-  , workspaceBytes
-  )
-import System.Mem (performGC)
-
-benchmarks :: IO ()
-benchmarks = do
-  benchmarkHierarchy 20_000 5_000
-  benchmarkSibson 10_000 5_000
-
-benchmarkHierarchy :: Int -> Int -> IO ()
-benchmarkHierarchy pointCount queryCount = do
-  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x123456789abcdef pointCount)))
-  queries <-
-    requireRight
-      (traverse mkQueryPoint (V.fromList (take queryCount (randomPoints 0x3141592653589793 queryCount))))
-  let triangulation = buildTriangulation built
-  hierarchy <- requireRight (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
-  (_, baselineSteps) <- timedValue "nearest/no-hierarchy" (evaluate (force (walkTotal triangulation Nothing queries)))
-  (_, hierarchySteps) <- timedValue "nearest/delaunay-hierarchy" (evaluate (force (walkHierarchyTotal triangulation hierarchy queries)))
-  putStrLn ("hierarchy-levels: " <> show (hierarchyLevelCount hierarchy))
-  putStrLn ("hierarchy-vertices: " <> show (hierarchyVertexCount hierarchy))
-  putStrLn ("nearest-walk-steps/no-hierarchy: " <> show baselineSteps)
-  putStrLn ("nearest-walk-steps/hierarchy: " <> show hierarchySteps)
- where
-  walkTotal
-    :: DelaunayTriangulation (Point)
-    -> Maybe VertexId
-    -> V.Vector (QueryPoint)
-    -> (Int, Int)
-  walkTotal triangulation hint queries = V.foldl' step (0 :: Int, 0 :: Int) queries
-   where
-    step (!count, !steps) query =
-      case nearestNeighbor triangulation hint query of
-        Nothing -> (count, steps)
-        Just (_, stats) -> (count + 1, steps + nearestWalkSteps stats)
-
-  walkHierarchyTotal
-    :: DelaunayTriangulation (Point)
-    -> HierarchyHint
-    -> V.Vector (QueryPoint)
-    -> (Int, Int)
-  walkHierarchyTotal triangulation hierarchy queries = V.foldl' step (0 :: Int, 0 :: Int) queries
-   where
-    step (!count, !steps) query =
-      let hint = case hierarchyHint hierarchy query of
-            Just (VertexHint vertex) -> Just vertex
-            _ -> Nothing
-       in case nearestNeighbor triangulation hint query of
-            Nothing -> (count, steps)
-            Just (_, stats) -> (count + 1, steps + nearestWalkSteps stats)
-
-benchmarkSibson :: Int -> Int -> IO ()
-benchmarkSibson pointCount queryCount = do
-  built <- requireRight (delaunay unitElementDefaults (V.fromList (randomPoints 0x8cb92baa3f3d8dd7 pointCount)))
-  queries <-
-    requireRight
-      (traverse mkQueryPoint (V.fromList (take queryCount (randomPoints 0xdb4f0b9175ae2165 queryCount))))
-  let triangulation = buildTriangulation built
-      height vertex =
-        let Point x y = vertexPoint triangulation vertex
-         in x * x + 0.5 * y
-  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
-  putStrLn ("sibson-workspace-bytes: " <> show (workspaceBytes workspace))
-  statsEnabled <- getRTSStatsEnabled
-  if statsEnabled then performGC else pure ()
-  before <- if statsEnabled then Just <$> getRTSStats else pure Nothing
-  _ <- timedValue "sibson/reused-workspace" (queryLoop workspace height queries)
-  after <- if statsEnabled then Just <$> getRTSStats else pure Nothing
-  case (before, after) of
-    (Just left, Just right) -> do
-      let allocated = allocated_bytes right - allocated_bytes left
-          perQuery = fromIntegral allocated / fromIntegral queryCount :: Double
-      putStrLn ("sibson-allocated-bytes-total: " <> show allocated)
-      putStrLn ("sibson-allocated-bytes/query: " <> show perQuery)
-    _ -> putStrLn "sibson allocation counters unavailable; run with +RTS -T"
- where
-  queryLoop
-    :: NaturalNeighborWorkspace RealWorld 'Unconstrained (Point) () () ()
-    -> (VertexId -> Double)
-    -> V.Vector (QueryPoint)
-    -> IO Double
-  queryLoop workspace height queries = go 0 0
-   where
-    !count = V.length queries
-    go !index !total
-      | index >= count = evaluate total
-      | otherwise = do
-          (value, _) <- stToIO (interpolateNaturalNeighbor height workspace Nothing (queries V.! index))
-          go (index + 1) (total + maybe 0 id value)
diff --git a/bench/hex/Main.hs b/bench/hex/Main.hs
--- a/bench/hex/Main.hs
+++ b/bench/hex/Main.hs
@@ -6,19 +6,101 @@
 import BenchMeasure (requireRight, timedValue)
 import Control.DeepSeq (force)
 import Control.Exception (evaluate)
+import Control.Monad (foldM)
+import Data.ByteString.Lazy qualified as BL
+import Data.Foldable (traverse_)
+import Data.List (sort)
 import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Hex.Coordinate (HexCoord (..), HexDirection (HexNorthEast))
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Sequence (Seq (..), ViewL (..), (|>))
+import Data.Sequence qualified as Seq
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Moonlight.Hex.Coordinate
+  ( HexCoord (..)
+  , HexDirection (HexNorthEast)
+  , allHexDirections
+  )
+import Moonlight.Hex.Planar
+  ( hexRegionFullyCoveredByPlanarRegion
+  , hexRegionIntersectingPlanarRegion
+  )
 import Moonlight.Hex.Region
+import Moonlight.Hex.Topology
+import Moonlight.Hex.Serialization
+import Moonlight.Planar.Region
+  ( planarRegionComponents
+  , polygonHoleLoops
+  )
+import PlanarSelectionBaseline qualified as Baseline
+import PlanarSelectionBench (holedRegion, runPlanarRepeated)
+import System.Environment (getArgs, withArgs)
 
 main :: IO ()
 main = do
-  benchmarkPackedAlgebra "hex-4096" 1024 4
-  benchmarkPackedAlgebra "hex-1048576" 1024 1024
-  benchmarkPackedAlgebra "hex-16777216" 4096 4096
+  arguments <- getArgs
+  case arguments of
+    ["--planar-only"] -> do
+      benchmarkPlanarSelection "hex-planar-1" 1 1
+      benchmarkPlanarSelection "hex-planar-85" 17 5
+      benchmarkPlanarSelection "hex-planar-4096" 64 64
+    "--planar-repeated" : benchmarkArguments ->
+      withArgs benchmarkArguments runPlanarRepeated
+    _ -> benchmarkAll
 
-benchmarkPackedAlgebra :: String -> Int -> Int -> IO ()
-benchmarkPackedAlgebra label width height = do
+benchmarkAll :: IO ()
+benchmarkAll = do
+  benchmarkPackedAlgebra Nothing "hex-4096" 1024 4
+  benchmarkPackedAlgebra Nothing "hex-1048576" 1024 1024
+  benchmarkPackedAlgebra (Just 16) "hex-16777216" 4096 4096
+  benchmarkSeparatedGluing "hex-256-separated-strips" 256 16 1024
+  benchmarkReferenceTopology
+  benchmarkPlanarSelection "hex-planar-4096" 64 64
+
+-- | Both exact lattice-against-region selections over one layout: full
+-- coverage and closed intersection against a region whose outer loop avoids
+-- the lattice and whose holes include one lattice-aligned cell, so both the
+-- generic and the boundary-touching cases are exercised. The selected
+-- coordinate lists are printed in full; they are the identity receipt.
+benchmarkPlanarSelection :: String -> Int -> Int -> IO ()
+benchmarkPlanarSelection label width height = do
   layout <- requireRight (hexLayout (HexCoord 0 0) width height)
+  region <- holedRegion >>= evaluate . force
+  putStrLn
+    ( label
+        <> "-layout: cells="
+        <> show (hexLayoutCellCount layout)
+        <> " holes="
+        <> show (sum (fmap (length . polygonHoleLoops) (planarRegionComponents region)))
+    )
+  baselineIntersecting <-
+    timedValue (label <> "-baseline-intersection") (evaluate (force (Baseline.hexRegionIntersectingPlanarRegion layout region)))
+  intersecting <-
+    timedValue (label <> "-intersection") (evaluate (force (hexRegionIntersectingPlanarRegion layout region)))
+  if baselineIntersecting == intersecting
+    then pure ()
+    else fail (label <> ": baseline/candidate intersection disagreement")
+  putStrLn (label <> "-intersection-cells: " <> show (selectedCoordinates intersecting))
+  baselineCovered <-
+    timedValue (label <> "-baseline-coverage") (evaluate (force (Baseline.hexRegionFullyCoveredByPlanarRegion layout region)))
+  covered <-
+    timedValue (label <> "-coverage") (evaluate (force (hexRegionFullyCoveredByPlanarRegion layout region)))
+  if baselineCovered == covered
+    then pure ()
+    else fail (label <> ": baseline/candidate coverage disagreement")
+  putStrLn (label <> "-coverage-cells: " <> show (selectedCoordinates covered))
+  putStrLn
+    ( label
+        <> "-receipt: cardinalities="
+        <> show (hexRegionCardinality covered, hexRegionCardinality intersecting)
+    )
+ where
+  selectedCoordinates = sort . foldHexRegionCoords (flip (:)) []
+
+benchmarkPackedAlgebra :: Maybe Int -> String -> Int -> Int -> IO ()
+benchmarkPackedAlgebra repetitions label width height = do
+  layout <- requireRight (hexLayout (HexCoord 0 0) width height)
   left <- evaluate (force (hexRegionGenerate layout leftPredicate))
   right <- evaluate (force (hexRegionGenerate layout rightPredicate))
   let cells = hexLayoutCellCount layout
@@ -36,6 +118,13 @@
   symmetricRegion <- timedValue (label <> "-symmetric-difference") (requireRight (hexRegionSymmetricDifference left right))
   complemented <- timedValue (label <> "-complement") (evaluate (complementHexRegion left))
   generated <- timedValue (label <> "-generate") (evaluate (hexRegionGenerate layout generatedPredicate))
+  encoded <- timedValue (label <> "-encode") (evaluate (encodeHexRegion left))
+  let decodingBudget =
+        HexDecodingBudget
+          { hexDecodingMaximumInputBytes = fromIntegral (BL.length encoded)
+          , hexDecodingMaximumCells = fromIntegral cells
+          }
+  decoded <- timedValue (label <> "-decode") (requireRight (decodeHexRegion decodingBudget encoded))
   neighbourCount <-
     timedValue
       (label <> "-neighbour")
@@ -56,13 +145,53 @@
           , hexRegionCardinality symmetricRegion
           , hexRegionCardinality complemented
           , hexRegionCardinality generated
+          , hexRegionCardinality decoded
           , hexRegionCardinality restricted
           , hexRegionCardinality glued
           )
         <> " neighbours="
         <> show neighbourCount
     )
+  traverse_
+    (\count -> benchmarkSteadyPackedAlgebra label count layout left right)
+    repetitions
 
+benchmarkSteadyPackedAlgebra :: String -> Int -> HexLayout -> HexRegion -> HexRegion -> IO ()
+benchmarkSteadyPackedAlgebra label repetitions layout left right = do
+  _ <-
+    timedValue
+      (label <> "-steady-union-" <> show repetitions)
+      (foldM forceUnion (emptyHexRegion layout) operands)
+  _ <-
+    timedValue
+      (label <> "-steady-generate-" <> show repetitions)
+      (foldM forceGeneration (emptyHexRegion layout) [1 .. repetitions])
+  (localLeft, localRight) <- prepareGluing (hexLayoutWidth layout) (hexLayoutHeight layout)
+  initialGluing <- evaluate . force =<< requireRight (glueCompatibleHexRegions (localLeft :| [localRight]))
+  _ <-
+    timedValue
+      (label <> "-steady-gluing-" <> show repetitions)
+      (foldM (forceGluing localLeft localRight) initialGluing [1 .. repetitions])
+  pure ()
+ where
+  operands = fmap (\index -> if even index then left else right) [1 .. repetitions]
+  forceUnion :: HexRegion -> HexRegion -> IO HexRegion
+  forceUnion accumulated operand =
+    evaluate . force =<< requireRight (hexRegionUnion accumulated operand)
+  forceGeneration :: HexRegion -> Int -> IO HexRegion
+  forceGeneration _ salt =
+    evaluate
+      ( force
+          ( hexRegionGenerate layout
+              (\(HexCoord q r) -> (mixedCoordinate q r + salt) `mod` 11 == 0)
+          )
+      )
+  forceGluing :: HexRegion -> HexRegion -> HexRegion -> Int -> IO HexRegion
+  forceGluing localLeft localRight accumulated _ =
+    evaluate . force
+      =<< requireRight
+        (glueCompatibleHexRegions (accumulated :| [localLeft, localRight]))
+
 benchmarkRestriction :: String -> HexLayout -> HexRegion -> IO HexRegion
 benchmarkRestriction label layout source = do
   let targetHeight = max 1 (hexLayoutHeight layout `quot` 2)
@@ -71,6 +200,13 @@
 
 benchmarkGluing :: String -> Int -> Int -> IO HexRegion
 benchmarkGluing label width height = do
+  (left, right) <- prepareGluing width height
+  timedValue
+    (label <> "-gluing")
+    (requireRight (glueCompatibleHexRegions (left :| [right])))
+
+prepareGluing :: Int -> Int -> IO (HexRegion, HexRegion)
+prepareGluing width height = do
   let quarter = max 1 (width `quot` 4)
       localWidth = width - quarter
       membership (HexCoord q r) = (q + 3 * r) `mod` 7 <= 2
@@ -78,10 +214,30 @@
   rightLayout <- requireRight (hexLayout (HexCoord quarter 0) localWidth height)
   left <- evaluate (force (hexRegionGenerate leftLayout membership))
   right <- evaluate (force (hexRegionGenerate rightLayout membership))
-  timedValue
-    (label <> "-gluing")
-    (requireRight (glueCompatibleHexRegions (left :| [right])))
+  pure (left, right)
 
+benchmarkSeparatedGluing :: String -> Int -> Int -> Int -> IO ()
+benchmarkSeparatedGluing label sectionCount sectionWidth sectionHeight = do
+  sections <- traverse section [0 .. sectionCount - 1]
+  case sections of
+    firstSection : remainingSections -> do
+      glued <-
+        timedValue
+          (label <> "-gluing")
+          (requireRight (glueCompatibleHexRegions (firstSection :| remainingSections)))
+      putStrLn
+        ( label
+            <> "-receipt: cells="
+            <> show (hexLayoutCellCount (hexRegionLayout glued))
+            <> " cardinality="
+            <> show (hexRegionCardinality glued)
+        )
+    [] -> fail (label <> ": expected a nonempty section family")
+ where
+  section index = do
+    layout <- requireRight (hexLayout (HexCoord (index * sectionWidth) 0) sectionWidth sectionHeight)
+    evaluate (force (hexRegionGenerate layout leftPredicate))
+
 leftPredicate :: HexCoord -> Bool
 leftPredicate (HexCoord q r) = (q + r) `mod` 3 /= 0
 
@@ -104,3 +260,150 @@
         descend
           (remaining - 1)
           (maybe count (const (count + 1)) (hexNeighbourCoord layout coordinate HexNorthEast))
+
+benchmarkReferenceTopology :: IO ()
+benchmarkReferenceTopology = do
+  layout <- requireRight (hexLayout (HexCoord 0 0) 256 256)
+  let domainPredicate (HexCoord q r) = q `mod` 29 /= 0 || r `mod` 31 == 0
+  domainRegion <-
+    evaluate
+      ( force
+          ( hexRegionGenerate
+              layout
+              domainPredicate
+          )
+      )
+  sourceRegion <-
+    evaluate
+      ( force
+          ( hexRegionGenerate
+              layout
+              (\coordinate@(HexCoord q r) ->
+                 domainPredicate coordinate && q `mod` 97 == 1 && r `mod` 89 == 2)
+          )
+      )
+  let domain = Set.fromList (foldHexRegionCoords (flip (:)) [] domainRegion)
+      sources = Set.fromList (foldHexRegionCoords (flip (:)) [] sourceRegion) `Set.intersection` domain
+  dilated <- timedValue "hex-reference-dilation" (evaluate (referenceDilation layout domain))
+  eroded <- timedValue "hex-reference-erosion" (evaluate (referenceErosion layout domain))
+  componentCount <- timedValue "hex-reference-components" (evaluate (referenceComponentCount layout domain))
+  distances <- timedValue "hex-reference-distances" (evaluate (referenceDistances layout domain sources))
+  nativeDilated <- timedValue "hex-native-dilation" (evaluate (hexRegionDilate domainRegion))
+  nativeEroded <- timedValue "hex-native-erosion" (evaluate (hexRegionErode domainRegion))
+  nativeInnerFrontier <- timedValue "hex-native-inner-frontier" (evaluate (hexRegionInnerFrontier domainRegion))
+  nativeOuterFrontier <- timedValue "hex-native-outer-frontier" (evaluate (hexRegionOuterFrontier domainRegion))
+  nativeComponents <- timedValue "hex-native-components" (evaluate (hexRegionComponentLabels domainRegion))
+  nativeDistances <-
+    timedValue
+      "hex-native-distances"
+      (requireRight (hexRegionDistancesWithin domainRegion sourceRegion))
+  putStrLn
+    ( "hex-reference-receipt: cardinalities="
+        <> show (Set.size domain, Set.size dilated, Set.size eroded)
+        <> " components="
+        <> show componentCount
+        <> " reached="
+        <> show (Map.size distances)
+        <> " maximum-distance="
+        <> show (maximumDistance distances)
+    )
+  putStrLn
+    ( "hex-native-receipt: cardinalities="
+        <> show
+          ( hexRegionCardinality domainRegion
+          , hexRegionCardinality nativeDilated
+          , hexRegionCardinality nativeEroded
+          , hexRegionCardinality nativeInnerFrontier
+          , hexRegionCardinality nativeOuterFrontier
+          )
+        <> " components="
+        <> show (hexComponentCount nativeComponents)
+        <> " reached="
+        <> show (hexRegionCardinality (hexDistanceReachableRegion nativeDistances))
+        <> " maximum-distance="
+        <> show (hexDistanceMaximum nativeDistances)
+    )
+
+referenceDilation :: HexLayout -> Set HexCoord -> Set HexCoord
+referenceDilation layout selected =
+  Set.foldl'
+    (\expanded coordinate -> foldl' (insertNeighbour coordinate) expanded allHexDirections)
+    selected
+    selected
+ where
+  insertNeighbour coordinate expanded direction =
+    maybe expanded (`Set.insert` expanded) (hexNeighbourCoord layout coordinate direction)
+
+referenceErosion :: HexLayout -> Set HexCoord -> Set HexCoord
+referenceErosion layout selected =
+  Set.filter
+    (\coordinate -> all (maybe True (`Set.member` selected) . hexNeighbourCoord layout coordinate) allHexDirections)
+    selected
+
+referenceComponentCount :: HexLayout -> Set HexCoord -> Int
+referenceComponentCount layout = descend 0
+ where
+  descend :: Int -> Set HexCoord -> Int
+  descend count remaining =
+    case Set.minView remaining of
+      Nothing -> count
+      Just (seed, withoutSeed) ->
+        descend (count + 1) (referenceFlood layout remaining (Seq.singleton seed) withoutSeed)
+
+referenceFlood :: HexLayout -> Set HexCoord -> Seq HexCoord -> Set HexCoord -> Set HexCoord
+referenceFlood layout domain frontier remaining =
+  case Seq.viewl frontier of
+    EmptyL -> remaining
+    coordinate :< rest ->
+      let neighbours =
+            foldl'
+              (\found direction ->
+                 maybe found
+                   (\candidate ->
+                      if Set.member candidate domain && Set.member candidate remaining
+                        then Set.insert candidate found
+                        else found)
+                   (hexNeighbourCoord layout coordinate direction))
+              Set.empty
+              allHexDirections
+       in referenceFlood
+            layout
+            domain
+            (foldl' (|>) rest neighbours)
+            (remaining `Set.difference` neighbours)
+
+referenceDistances :: HexLayout -> Set HexCoord -> Set HexCoord -> Map HexCoord Int
+referenceDistances layout domain sources = descend initialFrontier initialDistances
+ where
+  initialFrontier = Seq.fromList (Set.toAscList sources)
+  initialDistances = Map.fromSet (const 0) sources
+
+  descend :: Seq HexCoord -> Map HexCoord Int -> Map HexCoord Int
+  descend frontier distances =
+    case Seq.viewl frontier of
+      EmptyL -> distances
+      coordinate :< rest ->
+        let nextDistance = maybe 1 (+ 1) (Map.lookup coordinate distances)
+            (nextFrontier, nextDistances) =
+              foldl'
+                (admitNeighbour coordinate nextDistance)
+                (rest, distances)
+                allHexDirections
+         in descend nextFrontier nextDistances
+
+  admitNeighbour
+    :: HexCoord
+    -> Int
+    -> (Seq HexCoord, Map HexCoord Int)
+    -> HexDirection
+    -> (Seq HexCoord, Map HexCoord Int)
+  admitNeighbour coordinate distance (frontier, distances) direction =
+    case hexNeighbourCoord layout coordinate direction of
+      Just candidate
+        | Set.member candidate domain
+        , Map.notMember candidate distances ->
+            (frontier |> candidate, Map.insert candidate distance distances)
+      _ -> (frontier, distances)
+
+maximumDistance :: Map HexCoord Int -> Int
+maximumDistance = Map.foldl' max 0
diff --git a/bench/hex/PlanarSelectionBaseline.hs b/bench/hex/PlanarSelectionBaseline.hs
new file mode 100644
--- /dev/null
+++ b/bench/hex/PlanarSelectionBaseline.hs
@@ -0,0 +1,187 @@
+{-# OPTIONS_GHC -O1 -fexpose-all-unfoldings #-}
+
+-- | Frozen test oracle from commit 39029d4b84. Production never imports this
+-- module. Both schedules receive the same fully forced admitted input.
+module PlanarSelectionBaseline
+  ( hexRegionByCenterInPlanarRegion
+  , hexRegionFullyCoveredByPlanarRegion
+  , hexRegionIntersectingPlanarRegion
+  ) where
+
+import Data.List qualified as List
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Moonlight.Hex.Coordinate (HexCoord (..))
+import Moonlight.Hex.Element (HexVertex, hexCellVertices, hexVertexCoordinates)
+import Moonlight.Hex.Region (HexLayout, HexRegion, hexRegionGenerate)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , SegmentRelation (SegmentsDisjoint)
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , exactSegmentRelation
+  )
+import Moonlight.Planar.Region
+  ( PlanarRegion
+  , RegionPointLocation (RegionExterior)
+  , exactLoopPoints
+  , planarRegionComponents
+  , polygonHoleLoops
+  , polygonOuterLoop
+  , regionPointLocation
+  )
+
+hexRegionByCenterInPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionByCenterInPlanarRegion layout region =
+  hexRegionGenerate layout ((/= RegionExterior) . regionPointLocation region . exactCellCentre)
+
+-- | Select cells whose entire closed hexagon is covered by the closed planar
+-- region. A closed convex cell lies in the region exactly when its centre
+-- does and no boundary segment of the region meets the cell's interior: the
+-- interior is connected, so once the boundary is known to miss it, it lies
+-- wholly on the centre's side. Boundary-only contact is therefore coverage,
+-- and no overlay or gap publication is involved.
+hexRegionFullyCoveredByPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionFullyCoveredByPlanarRegion layout region =
+  let boundary = planarBoundary region
+   in hexRegionGenerate layout (cellCoveredByRegion region boundary)
+
+-- | Select cells whose closed hexagon has any point in common with the closed
+-- planar region. The region boundary is prepared once as an ephemeral view;
+-- the admitted 'PlanarRegion' remains the sole geometry authority.
+hexRegionIntersectingPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionIntersectingPlanarRegion layout region =
+  let boundary = planarBoundary region
+   in hexRegionGenerate layout (cellIntersectsRegion region boundary)
+
+exactCellCentre :: HexCoord -> ExactPoint
+exactCellCentre (HexCoord q r) =
+  exactPoint
+    (fromInteger (3 * toInteger q))
+    (fromInteger (2 * toInteger r + toInteger q))
+
+-- | Closed axis-aligned extent, the cheap separation test in front of every
+-- exact predicate: two closed sets with disjoint extents are disjoint.
+data Extent = Extent !ExactRational !ExactRational !ExactRational !ExactRational
+
+pointExtent :: ExactPoint -> Extent
+pointExtent point =
+  let (x, y) = exactPointCoordinates point
+   in Extent x x y y
+
+pointsExtent :: NonEmpty ExactPoint -> Extent
+pointsExtent = foldr1 joinExtent . fmap pointExtent
+ where
+  joinExtent (Extent leftLow leftHigh leftBottom leftTop) (Extent rightLow rightHigh rightBottom rightTop) =
+    Extent (min leftLow rightLow) (max leftHigh rightHigh) (min leftBottom rightBottom) (max leftTop rightTop)
+
+extentsMeet :: Extent -> Extent -> Bool
+extentsMeet (Extent leftLow leftHigh leftBottom leftTop) (Extent rightLow rightHigh rightBottom rightTop) =
+  leftLow <= rightHigh && rightLow <= leftHigh && leftBottom <= rightTop && rightBottom <= leftTop
+
+-- | The region boundary as an ephemeral view: every boundary vertex and every
+-- boundary segment, each with its extent.
+data PlanarBoundary = PlanarBoundary
+  { boundaryPoints :: ![(Extent, ExactPoint)]
+  , boundarySegments :: ![(Extent, ExactPoint, ExactPoint)]
+  }
+
+planarBoundary :: PlanarRegion -> PlanarBoundary
+planarBoundary region =
+  let loops = planarRegionComponents region >>= componentLoops
+      points = loops >>= NonEmpty.toList . exactLoopPoints
+   in PlanarBoundary
+        { boundaryPoints = fmap (\point -> (pointExtent point, point)) points
+        , boundarySegments =
+            [ (pointsExtent (from :| [to]), from, to)
+            | loop <- loops
+            , (from, to) <- exactPointCycleSegments (NonEmpty.toList (exactLoopPoints loop))
+            ]
+        }
+ where
+  componentLoops component =
+    polygonOuterLoop component : polygonHoleLoops component
+
+cellCoveredByRegion :: PlanarRegion -> PlanarBoundary -> HexCoord -> Bool
+cellCoveredByRegion region boundary coordinate =
+  regionPointLocation region (exactCellCentre coordinate) /= RegionExterior
+    && not
+      ( any
+          (\(extent, from, to) -> extentsMeet cellExtent extent && segmentMeetsConvexInterior cellPoints (from, to))
+          (boundarySegments boundary)
+      )
+ where
+  cellVertices = fmap exactVertexPoint (hexCellVertices coordinate)
+  cellPoints = NonEmpty.toList cellVertices
+  cellExtent = pointsExtent cellVertices
+
+-- | Whether a segment has a point strictly inside the convex polygon whose
+-- vertices are listed counter-clockwise. A segment on a side's line never
+-- enters; otherwise it enters when an endpoint is strictly inside, when it
+-- properly crosses a side, or when it touches the boundary at two distinct
+-- points, since the chord between them is interior to a convex polygon.
+segmentMeetsConvexInterior :: [ExactPoint] -> (ExactPoint, ExactPoint) -> Bool
+segmentMeetsConvexInterior vertices (from, to)
+  | any onSideLine sides = False
+  | strictlyInside fromSides || strictlyInside toSides = True
+  | any properlyCrosses sides = True
+  | otherwise = length (List.nub contacts) >= 2
+ where
+  edges = exactPointCycleSegments vertices
+  fromSides = fmap (\(a, b) -> exactOrient2d a b from) edges
+  toSides = fmap (\(a, b) -> exactOrient2d a b to) edges
+  vertexSides = fmap (exactOrient2d from to) vertices
+  sides = zip (zip fromSides toSides) (zip vertexSides (drop 1 (cycle vertexSides)))
+  onSideLine :: ((Ordering, Ordering), (Ordering, Ordering)) -> Bool
+  onSideLine ((fromSide, toSide), _) = fromSide == EQ && toSide == EQ
+  strictlyInside :: [Ordering] -> Bool
+  strictlyInside = all (== GT)
+  onBoundary :: [Ordering] -> Bool
+  onBoundary orientations = all (/= LT) orientations && elem EQ orientations
+  properlyCrosses :: ((Ordering, Ordering), (Ordering, Ordering)) -> Bool
+  properlyCrosses ((fromSide, toSide), (startSide, endSide)) =
+    opposite fromSide toSide && opposite startSide endSide
+  opposite :: Ordering -> Ordering -> Bool
+  opposite left right = (left == GT && right == LT) || (left == LT && right == GT)
+  contacts =
+    [point | (point, orientations) <- [(from, fromSides), (to, toSides)], onBoundary orientations]
+      <> [vertex | (vertex, side) <- zip vertices vertexSides, side == EQ, withinSpan vertex]
+  withinSpan :: ExactPoint -> Bool
+  withinSpan vertex = extentsMeet (pointExtent vertex) (pointsExtent (from :| [to]))
+
+cellIntersectsRegion :: PlanarRegion -> PlanarBoundary -> HexCoord -> Bool
+cellIntersectsRegion region boundary coordinate =
+  any ((/= RegionExterior) . regionPointLocation region) cellPoints
+    || any
+      (\(extent, point) -> extentsMeet cellExtent extent && pointInClosedHex cellSegments point)
+      (boundaryPoints boundary)
+    || any
+      (\(extent, regionFrom, regionTo) ->
+         extentsMeet cellExtent extent
+           && any
+             (\(cellFrom, cellTo) ->
+                exactSegmentRelation cellFrom cellTo regionFrom regionTo /= SegmentsDisjoint)
+             cellSegments)
+      (boundarySegments boundary)
+ where
+  cellVertices = fmap exactVertexPoint (hexCellVertices coordinate)
+  cellPoints = NonEmpty.toList cellVertices
+  cellSegments = exactPointCycleSegments cellPoints
+  cellExtent = pointsExtent cellVertices
+
+pointInClosedHex :: [(ExactPoint, ExactPoint)] -> ExactPoint -> Bool
+pointInClosedHex cellSegments query =
+  all (\(from, to) -> exactOrient2d from to query /= LT) cellSegments
+
+exactPointCycleSegments :: [ExactPoint] -> [(ExactPoint, ExactPoint)]
+exactPointCycleSegments points =
+  case points of
+    [] -> []
+    firstPoint : remaining -> zip points (remaining <> [firstPoint])
+
+exactVertexPoint :: HexVertex -> ExactPoint
+exactVertexPoint vertex =
+  let (x, y) = hexVertexCoordinates vertex
+   in exactPoint (fromInteger x) (fromInteger y)
diff --git a/bench/hex/PlanarSelectionBench.hs b/bench/hex/PlanarSelectionBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/hex/PlanarSelectionBench.hs
@@ -0,0 +1,216 @@
+module PlanarSelectionBench
+  ( holedRegion
+  , runPlanarRepeated
+  ) where
+
+import BenchMeasure (requireRight, timedValue)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Ratio ((%), denominator, numerator)
+import Moonlight.Hex.Coordinate (HexCoord (..))
+import Moonlight.Hex.Element (hexCellVertices, hexVertexCoordinates)
+import Moonlight.Hex.Planar
+  ( hexRegionByCenterInPlanarRegion
+  , hexRegionFullyCoveredByPlanarRegion
+  , hexRegionIntersectingPlanarRegion
+  , hexRegionPlanarRegion
+  )
+import Moonlight.Hex.Region
+  ( HexLayout
+  , HexRegion
+  , fullHexRegion
+  , hexLayout
+  , hexLayoutCellCount
+  , hexLayoutWordCount
+  , hexRegionCardinality
+  )
+import Moonlight.Planar.Exact (ExactPoint, exactPoint, exactRational)
+import Moonlight.Planar.Region
+  ( ExactLoop
+  , PlanarRegion
+  , exactLoop
+  , planarRegion
+  , polygonComponent
+  )
+import PlanarSelectionBaseline qualified as Baseline
+import System.Mem (performGC)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)
+
+-- Every function application below receives the same fully evaluated pair.
+-- The region builders, output-agreement checks, and one-shot allocation
+-- receipts are outside the repeated Tasty timing distribution.
+runPlanarRepeated :: IO ()
+runPlanarRepeated = do
+  fixtures <- planarFixtures >>= evaluate . force
+  traverse_ reportFixture fixtures
+  defaultMain (fmap benchmarkFixture fixtures)
+
+type PlanarFixture = (String, (HexLayout, PlanarRegion))
+
+benchmarkFixture :: PlanarFixture -> Benchmark
+benchmarkFixture (label, input) =
+  bgroup label
+    [ bgroup "centre"
+        [ bench "baseline" (nf (uncurry Baseline.hexRegionByCenterInPlanarRegion) input)
+        , bench "candidate" (nf (uncurry hexRegionByCenterInPlanarRegion) input)
+        ]
+    , bgroup "coverage"
+        [ bench "baseline" (nf (uncurry Baseline.hexRegionFullyCoveredByPlanarRegion) input)
+        , bench "candidate" (nf (uncurry hexRegionFullyCoveredByPlanarRegion) input)
+        ]
+    , bgroup "intersection"
+        [ bench "baseline" (nf (uncurry Baseline.hexRegionIntersectingPlanarRegion) input)
+        , bench "candidate" (nf (uncurry hexRegionIntersectingPlanarRegion) input)
+        ]
+    ]
+
+reportFixture :: PlanarFixture -> IO ()
+reportFixture (label, input@(layout, _)) = do
+  baselineCentre <- measured "centre/baseline" Baseline.hexRegionByCenterInPlanarRegion
+  candidateCentre <- measured "centre/candidate" hexRegionByCenterInPlanarRegion
+  baselineCoverage <- measured "coverage/baseline" Baseline.hexRegionFullyCoveredByPlanarRegion
+  candidateCoverage <- measured "coverage/candidate" hexRegionFullyCoveredByPlanarRegion
+  baselineIntersection <- measured "intersection/baseline" Baseline.hexRegionIntersectingPlanarRegion
+  candidateIntersection <- measured "intersection/candidate" hexRegionIntersectingPlanarRegion
+  if baselineCentre == candidateCentre && baselineCoverage == candidateCoverage && baselineIntersection == candidateIntersection
+    then putStrLn
+      ( label <> "-matched-receipt: cells=" <> show (hexLayoutCellCount layout)
+          <> " words=" <> show (hexLayoutWordCount layout)
+          <> " selected=" <> show (hexRegionCardinality candidateCentre, hexRegionCardinality candidateCoverage, hexRegionCardinality candidateIntersection)
+          <> " centre-coverage-intersection=equal"
+      )
+    else fail (label <> ": baseline/candidate packed outputs disagree")
+ where
+  measured :: String -> (HexLayout -> PlanarRegion -> HexRegion) -> IO HexRegion
+  measured lane classify = do
+    performGC
+    timedValue (label <> "/" <> lane) (evaluate (force (uncurry classify input)))
+
+planarFixtures :: IO [PlanarFixture]
+planarFixtures = do
+  holed <- holedRegion
+  holedFixtures <- traverse (holedFixture holed) [("holed/1", 1, 1), ("holed/85", 17, 5), ("holed/4096", 64, 64)]
+  alignedFixtures <- traverse alignedFixture [("aligned/1", HexCoord 0 0, 1, 1), ("aligned/85", HexCoord (-8) (-2), 17, 5)]
+  contactFixtures <-
+    concat <$> traverse boundaryFixtures
+      [ ("vertex-contact", [rectangle 2 0 3 1], [])
+      , ("side-contact", [rectangle (-1) 1 1 2], [])
+      , ("tiny-island", [rectangle (2 % 5) (1 % 10) (3 % 5) (3 % 10)], [])
+      , ("tiny-hole", [rectangle (-12) (-12) 12 12], [rectangle (2 % 5) (1 % 10) (3 % 5) (3 % 10)])
+      , ("sliver", [sliver], [])
+      , ("empty", [], [])
+      ]
+  translated <- rationalComponents [fmap (translatePoint largeOrigin) sliver] []
+  translatedLayout <- requireRight (hexLayout (offsetOrigin largeOrigin (-8) (-2)) 17 5)
+  negative <- rationalComponents [fmap (translatePoint (HexCoord (-31) (-17))) sliver] []
+  negativeLayout <- requireRight (hexLayout (HexCoord (-39) (-19)) 17 5)
+  dense <- rationalComponents [fmap (\(q, r) -> (3 * q, 2 * r + q)) [(-2, -2), (514, -2), (514, 514), (-2, 514)]] []
+  denseLayout <- requireRight (hexLayout (HexCoord 0 0) 512 512)
+  pure
+    ( holedFixtures <> alignedFixtures <> contactFixtures
+        <> [ ("large-translation/85", (translatedLayout, translated))
+           , ("negative-translation/85", (negativeLayout, negative))
+           , ("dense/262144", (denseLayout, dense))
+           ]
+    )
+ where
+  holedFixture :: PlanarRegion -> (String, Int, Int) -> IO PlanarFixture
+  holedFixture region (label, width, height) = do
+    layout <- requireRight (hexLayout (HexCoord 0 0) width height)
+    pure (label, (layout, region))
+
+  alignedFixture :: (String, HexCoord, Int, Int) -> IO PlanarFixture
+  alignedFixture (label, origin, width, height) = do
+    layout <- requireRight (hexLayout origin width height)
+    region <- requireRight (hexRegionPlanarRegion (fullHexRegion layout))
+    pure (label, (layout, region))
+
+  boundaryFixtures :: (String, [[RationalPoint]], [[RationalPoint]]) -> IO [PlanarFixture]
+  boundaryFixtures (label, outers, holes) = do
+    region <- rationalComponents outers holes
+    traverse
+      (\(suffix, origin, width, height) -> do
+          layout <- requireRight (hexLayout origin width height)
+          pure (label <> suffix, (layout, region)))
+      [("/1", HexCoord 0 0, 1, 1), ("/85", HexCoord (-8) (-2), 17, 5)]
+
+  rectangle :: Rational -> Rational -> Rational -> Rational -> [RationalPoint]
+  rectangle left bottom right top =
+    [(left, bottom), (right, bottom), (right, top), (left, top)]
+
+  sliver :: [RationalPoint]
+  sliver = [(-22, -7), (22, 7), (22, 7 + 1 % 1024), (-22, -7 + 1 % 1024)]
+
+  largeOrigin :: HexCoord
+  largeOrigin = HexCoord (2 ^ (55 :: Int)) (negate (2 ^ (54 :: Int)))
+
+  offsetOrigin :: HexCoord -> Int -> Int -> HexCoord
+  offsetOrigin (HexCoord q r) dq dr = HexCoord (q + dq) (r + dr)
+
+type RationalPoint = (Rational, Rational)
+
+translatePoint :: HexCoord -> RationalPoint -> RationalPoint
+translatePoint (HexCoord q r) (x, y) =
+  ( x + fromInteger (3 * toInteger q)
+  , y + fromInteger (2 * toInteger r + toInteger q)
+  )
+
+rationalComponents :: [[RationalPoint]] -> [[RationalPoint]] -> IO PlanarRegion
+rationalComponents outers holes = do
+  admittedHoles <- traverse (admitLoop . reverse) holes
+  components <- traverse
+    (\outer -> admitLoop outer >>= \loop -> requireRight (polygonComponent loop admittedHoles))
+    outers
+  requireRight (planarRegion components)
+ where
+  admitLoop :: [RationalPoint] -> IO ExactLoop
+  admitLoop points = do
+    exactPoints <- traverse pointOf points
+    maybe (fail "planar benchmark loop is empty") (requireRight . exactLoop) (NonEmpty.nonEmpty exactPoints)
+
+  pointOf :: RationalPoint -> IO ExactPoint
+  pointOf (x, y) =
+    exactPoint
+      <$> requireRight (exactRational (numerator x) (denominator x))
+      <*> requireRight (exactRational (numerator y) (denominator y))
+
+holedRegion :: IO PlanarRegion
+holedRegion = do
+  outer <-
+    loopOf
+      [ (5, 2, 7, 3)
+      , (541, 3, 29, 7)
+      , (191, 1, 201, 2)
+      , (852, 5, 556, 3)
+      , (181, 3, 1711, 9)
+      , (31, 3, 452, 3)
+      ]
+  square <- loopOf [(121, 3, 121, 3), (121, 3, 211, 3), (211, 3, 211, 3), (211, 3, 121, 3)]
+  triangle <- loopOf [(241, 2, 241, 2), (241, 2, 321, 2), (451, 3, 281, 2)]
+  cellHole <-
+    requireRight
+      ( exactLoop
+          ( NonEmpty.reverse
+              ( fmap
+                  (\vertex -> let (x, y) = hexVertexCoordinates vertex in exactPoint (fromInteger x) (fromInteger y))
+                  (hexCellVertices (HexCoord 30 20))
+              )
+          )
+      )
+  component <- requireRight (polygonComponent outer [square, cellHole, triangle])
+  requireRight (planarRegion [component])
+ where
+  loopOf :: [(Integer, Integer, Integer, Integer)] -> IO ExactLoop
+  loopOf fractions = do
+    points <- traverse pointOf fractions
+    case points of
+      firstPoint : rest -> requireRight (exactLoop (firstPoint :| rest))
+      [] -> fail "holedRegion: empty loop"
+  pointOf :: (Integer, Integer, Integer, Integer) -> IO ExactPoint
+  pointOf (xNumerator, xDenominator, yNumerator, yDenominator) =
+    exactPoint
+      <$> requireRight (exactRational xNumerator xDenominator)
+      <*> requireRight (exactRational yNumerator yDenominator)
diff --git a/bench/join/Main.hs b/bench/join/Main.hs
--- a/bench/join/Main.hs
+++ b/bench/join/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.JoinBench as JoinBench
+import qualified Moonlight.Planar.JoinBench as JoinBench
 
 main :: IO ()
 main = JoinBench.benchmarks
diff --git a/bench/join/Moonlight/Planar/JoinBench.hs b/bench/join/Moonlight/Planar/JoinBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/join/Moonlight/Planar/JoinBench.hs
@@ -0,0 +1,905 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | What the join costs, and against what.
+--
+-- These lanes compare the public union schedules with rebuild, local insertion,
+-- and explicit canonical observation. Schedule claims live or die by these
+-- measurements rather than by asymptotic theatre.
+module Moonlight.Planar.JoinBench
+  ( benchmarks
+  , publicationBenchmarks
+  ) where
+
+import BenchMeasure (requireRight, timedValue)
+import BenchSupport (randomPoints)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad (foldM, unless)
+import Data.List (sort, sortBy)
+import Data.Ord (comparing)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Data.Word (Word64)
+import Moonlight.Planar.BulkLoad (delaunay, delaunayGeometry, insertMany)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.Cdt (ConstrainedSeamResult, constrainedDelaunay, constrainedSeamCachedFrontierPointReads, constrainedSeamFaceFirstPoint, constrainedSeamFaceSecondPoint, constrainedSeamFaceThirdPoint, constrainedSeamJoinFaces, constrainedSeamLeftConstraintCount, constrainedSeamPublicationStats, constrainedSeamResultTriangulation, constrainedSeamRightFaceEvidence, constrainedSeamTargetFace, constraintSegments, joinSeparatedConstrained, segmentEnd, segmentStart)
+import Moonlight.Planar.Dcel (numFaces)
+import Moonlight.Planar.Handles.HandleDefs (FaceId, UndirectedEdgeId, asUndirected, reverseEdge)
+import Moonlight.Planar.Payload (geometryOnlyPublication)
+import Moonlight.Planar.Refinement (refineWithinDomain, withAdditionalVertexBudget, withConstraintPreservation, withMaximumArea, withoutMaximumRadiusEdgeRatio)
+import Moonlight.Planar.SetAlgebra (difference, intersection, intersectionWith, siteRelation, symmetricDifference, union, unions)
+import Moonlight.Planar.Types (BuildError (..), ClosureStats (..), ConstrainedDelaunayTriangulation, DelaunayTriangulation, buildTriangulation, defaultRefinementParameters, refinedTriangulation, refinementAddedVertices, refinementAttemptedBoundaryCrossings, refinementClosureStats, refinementComplete, refinementCreatedFaces, refinementDomainReceipt, refinementDomainResult, refinementFinalPermittedFaces, refinementInterfaceBoundaryReads, refinementPublicationStats, refinementStats, refinementTouchedEdges, unitElementDefaults)
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Validation (validateTriangulation)
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.BoxedPaged (boxedMaterializedPageCount)
+import Moonlight.Planar.Internal.Paged (PublicationStats (..), pagedOverlayPageCount)
+import Moonlight.Planar.Internal.Representation
+  ( Triangulation
+      ( triConstraint
+      , triDirectedData
+      , triFaceData
+      , triFaceEdge
+      , triHalfTopology
+      , triPointX
+      , triPointY
+      , triUndirectedData
+      , triVertexData
+      , triVertexOut
+      )
+  )
+
+type Mesh = DelaunayTriangulation ()
+type SiteMesh = DelaunayTriangulation (Point)
+type ConstrainedMesh = ConstrainedDelaunayTriangulation ()
+
+data ExtensionSignature = ExtensionSignature
+  { extensionOmegaGeometry :: !(Set.Set (Point, Point, Point))
+  , extensionGammaGeometry :: !(Set.Set (Point, Point))
+  , extensionSeamGeometry :: !(Set.Set (Point, Point, Point))
+  , extensionConstraintGeometry :: !(Set.Set (Point, Point))
+  }
+  deriving stock (Eq, Show)
+
+data PreparedSeparatedExtension = PreparedSeparatedExtension
+  { preparedBase :: !ConstrainedMesh
+  , preparedExtension :: !ConstrainedMesh
+  , preparedSignature :: !ExtensionSignature
+  }
+
+-- | The scale comparison retains only observations emitted by the local
+-- interpreters.  Resident face count is deliberately absent: it is the
+-- independent variable, not permission to normalize local work by A.
+data ExtensionLocalityReceipt = ExtensionLocalityReceipt
+  { extensionLocalitySeamPublication :: !PublicationStats
+  , extensionLocalityRefinementPublication :: !PublicationStats
+  , extensionLocalityCachedFrontierPointReads :: !Int
+  , extensionLocalityValidationClosure :: !ClosureStats
+  , extensionLocalityRefinementStats :: !BuildStats
+  , extensionLocalityFinalOmegaGeometry :: !(Set.Set (Point, Point, Point))
+  , extensionLocalityAddedVertices :: !Int
+  , extensionLocalityFinalOmegaFaces :: !Int
+  , extensionLocalityTouchedEdges :: !Int
+  , extensionLocalityCreatedFaces :: !Int
+  , extensionLocalityInterfaceReads :: !Int
+  , extensionLocalityBoundaryCrossings :: !Int
+  }
+  deriving stock (Eq, Show)
+
+rectangleBoundarySites :: Double -> Double -> [Point]
+rectangleBoundarySites left right =
+  [ Point left (-1)
+  , Point right (-1)
+  ]
+    <> fmap (Point right) [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]
+    <> [Point right 1, Point left 1]
+
+rectangleCollarSites :: Double -> Double -> [Point]
+rectangleCollarSites left right =
+  let ys = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]
+      width = right - left
+   in fmap (Point (left + 0.2 * width)) ys
+        <> fmap (Point (right - 0.2 * width)) ys
+
+rectangleWorldSites :: Word64 -> Int -> Double -> Double -> [Point]
+rectangleWorldSites seed count left right =
+  rectangleFixedSites left right
+    <> fmap
+      (\(Point x y) ->
+         Point
+           (left + 0.35 * (right - left) + 0.15 * (right - left) * (x + 1))
+           (-0.6 + 0.6 * (y + 1)))
+      (randomPoints seed (max 0 (count - length (rectangleFixedSites left right))))
+
+rectangleFixedSites :: Double -> Double -> [Point]
+rectangleFixedSites left right = rectangleBoundarySites left right <> rectangleCollarSites left right
+
+rectangleContour :: Int -> [(Int, Int)]
+rectangleContour count =
+  [ (index, (index + 1) `mod` count)
+  | index <- [0 .. count - 1]
+  ]
+
+rectangleConstraintPairs :: [(Int, Int)]
+rectangleConstraintPairs =
+  rectangleContour boundaryCount
+    <> chain collarLeftStart
+    <> chain collarRightStart
+ where
+  boundaryCount = length (rectangleBoundarySites 0 1)
+  collarLength = length (rectangleCollarSites 0 1) `quot` 2
+  collarLeftStart = boundaryCount
+  collarRightStart = collarLeftStart + collarLength
+  chain start =
+    [ (start + index, start + index + 1)
+    | index <- [0 .. collarLength - 2]
+    ]
+
+contourPairs :: [Point] -> [(Point, Point)]
+contourPairs points =
+  case points of
+    [] -> []
+    first : rest -> zip points (rest <> [first])
+
+orderedPointPair :: (Point, Point) -> (Point, Point)
+orderedPointPair (first, second)
+  | first <= second = (first, second)
+  | otherwise = (second, first)
+
+contourGeometry :: [Point] -> Set.Set (Point, Point)
+contourGeometry = Set.fromList . fmap orderedPointPair . contourPairs
+
+rectangleConstraintGeometry :: Double -> Double -> Set.Set (Point, Point)
+rectangleConstraintGeometry left right =
+  let boundary = rectangleBoundarySites left right
+      collar = rectangleCollarSites left right
+      collarLength = length collar `quot` 2
+      (leftCollar, rightCollar) = splitAt collarLength collar
+   in Set.unions
+        [ contourGeometry boundary
+        , openChainGeometry leftCollar
+        , openChainGeometry rightCollar
+        ]
+
+openChainGeometry :: [Point] -> Set.Set (Point, Point)
+openChainGeometry points =
+  Set.fromList
+    [ orderedPointPair pair
+    | pair <- zip points (drop 1 points)
+    ]
+
+constraintGeometry :: ConstrainedMesh -> Set.Set (Point, Point)
+constraintGeometry mesh =
+  Set.fromList
+    [ orderedPointPair (segmentStart segment, segmentEnd segment)
+    | segment <- V.toList (constraintSegments mesh)
+    ]
+
+constrainedGeometryMesh :: [Point] -> IO ConstrainedMesh
+constrainedGeometryMesh points =
+  geometryOnlyPublication . buildTriangulation
+    <$> requireRight
+      ( constrainedDelaunay
+          unitElementDefaults
+          (V.fromList points)
+          (V.fromList rectangleConstraintPairs)
+      )
+
+boundaryOfPermittedFaces
+  :: ConstrainedMesh
+  -> Set.Set FaceId
+  -> Set.Set UndirectedEdgeId
+boundaryOfPermittedFaces mesh faces =
+  Set.fromList
+    [ asUndirected edge
+    | face <- Set.toList faces
+    , edge <- Dcel.faceDirectedEdges mesh face
+    , let adjacent = Dcel.incidentFace mesh (reverseEdge edge)
+    , adjacent /= Dcel.outerFace
+    , Set.notMember adjacent faces
+    ]
+
+faceGeometry :: ConstrainedMesh -> FaceId -> Maybe (Point, Point, Point)
+faceGeometry mesh face =
+  case sort (fmap (Dcel.vertexPoint mesh) (Dcel.faceVertices mesh face)) of
+    [first, second, third] -> Just (first, second, third)
+    _ -> Nothing
+
+faceGeometrySet
+  :: ConstrainedMesh
+  -> Set.Set FaceId
+  -> Either String (Set.Set (Point, Point, Point))
+faceGeometrySet mesh faces =
+  Set.fromList
+    <$> traverse
+      (\face ->
+         maybe
+           (Left ("local refinement receipt named a non-triangular face: " <> show face))
+           Right
+           (faceGeometry mesh face))
+      (Set.toList faces)
+
+edgeGeometry :: ConstrainedMesh -> UndirectedEdgeId -> (Point, Point)
+edgeGeometry mesh edge =
+  let (fromVertex, toVertex) = Dcel.undirectedEndpoints mesh edge
+   in orderedPointPair
+        (Dcel.vertexPoint mesh fromVertex, Dcel.vertexPoint mesh toVertex)
+
+extensionSignature :: ConstrainedSeamResult () -> Either String ExtensionSignature
+extensionSignature receipt =
+  let joined = constrainedSeamResultTriangulation receipt
+      permitted =
+        Set.union
+          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence receipt))))
+          (Set.fromList (V.toList (constrainedSeamJoinFaces receipt)))
+      interface = boundaryOfPermittedFaces joined permitted
+   in do
+        seamFaces <-
+          Set.fromList
+            <$> traverse
+              (\face ->
+                 case faceGeometry joined face of
+                   Just geometry -> Right geometry
+                   Nothing -> Left "seam receipt named a non-triangular face")
+              (V.toList (constrainedSeamJoinFaces receipt))
+        let omegaFaces =
+              Set.union
+                ( Set.fromList
+                    [ ( constrainedSeamFaceFirstPoint evidence
+                      , constrainedSeamFaceSecondPoint evidence
+                      , constrainedSeamFaceThirdPoint evidence
+                      )
+                    | evidence <- V.toList (constrainedSeamRightFaceEvidence receipt)
+                    ]
+                )
+                seamFaces
+        pure
+          ExtensionSignature
+            { extensionOmegaGeometry = omegaFaces
+            , extensionGammaGeometry = Set.fromList (fmap (edgeGeometry joined) (Set.toList interface))
+            , extensionSeamGeometry = seamFaces
+            , extensionConstraintGeometry = constraintGeometry joined
+            }
+
+refinementInterfaceGeometry :: ConstrainedMesh -> Set.Set FaceId -> Set.Set (Point, Point)
+refinementInterfaceGeometry mesh faces =
+  Set.fromList
+    [ edgeGeometry mesh edge
+    | edge <- Set.toList (boundaryOfPermittedFaces mesh faces)
+    ]
+
+assertLocalPublicationStats :: String -> PublicationStats -> IO ()
+assertLocalPublicationStats label stats = do
+  unless (publicationUnboxedBasePageEnumerations stats == 0) $
+    fail (label <> " enumerated resident unboxed base pages")
+  unless (publicationUnboxedBasePageFreezes stats == 0) $
+    fail (label <> " froze resident unboxed base pages")
+  unless (publicationBoxedBasePageEnumerations stats == 0) $
+    fail (label <> " enumerated resident boxed base pages")
+  unless (publicationBoxedBasePageFreezes stats == 0) $
+    fail (label <> " froze resident boxed base pages")
+
+-- These are post-publication representation observations. They do not count
+-- base-page opens or freeze enumeration and therefore are not a proof that
+-- those events were absent from the timed action.
+publicationPageReceipt :: ConstrainedMesh -> (Int, Int)
+publicationPageReceipt mesh =
+  ( sum
+      [ pagedOverlayPageCount (triPointX mesh)
+      , pagedOverlayPageCount (triPointY mesh)
+      , pagedOverlayPageCount (triVertexOut mesh)
+      , pagedOverlayPageCount (triHalfTopology mesh)
+      , pagedOverlayPageCount (triFaceEdge mesh)
+      , pagedOverlayPageCount (triConstraint mesh)
+      ]
+  , sum
+      [ boxedMaterializedPageCount (triVertexData mesh)
+      , boxedMaterializedPageCount (triDirectedData mesh)
+      , boxedMaterializedPageCount (triUndirectedData mesh)
+      , boxedMaterializedPageCount (triFaceData mesh)
+      ]
+  )
+
+benchmarks :: IO ()
+benchmarks = do
+  benchmarkBalanced 20_000
+  benchmarkSeparated 20_000
+  benchmarkSkew 20_000 200
+  benchmarkOverlap 20_000
+  benchmarkTournament 20_000 16
+  benchmarkSpatialTournament 20_000 16
+  benchmarkTournamentScaling 20_000
+  benchmarkCanonicalize 20_000
+  benchmarkSetAlgebra 20_000
+
+publicationBenchmarks :: IO ()
+publicationBenchmarks = do
+  -- These are face bands, not site-count labels.  A planar Delaunay mesh is
+  -- approximately twice as many faces as sites, so the resident fixtures use
+  -- 125k and 250k sites to exercise the requested 250k/500k-face lanes, and
+  -- the incoming small world uses 15k sites for its approximately 30k faces.
+  prepared250k <- preparePersistentSeparatedExtension 125_000 15_000
+  prepared500k <- preparePersistentSeparatedExtension 250_000 15_000
+  assertFaceBand "250k-faces" 245_000 255_000 (preparedBase prepared250k)
+  assertFaceBand "500k-faces" 490_000 510_000 (preparedBase prepared500k)
+  assertFaceBand "30k-extension-faces" 29_000 31_000 (preparedExtension prepared250k)
+  assertFaceBand "30k-extension-faces-repeat" 29_000 31_000 (preparedExtension prepared500k)
+  unless (preparedSignature prepared250k == preparedSignature prepared500k) $
+    fail "separated extension changed its fixed collar, Gamma, or seam geometry across A sizes"
+  receipt250k <- benchmarkPersistentSeparatedExtension "250k-faces" prepared250k
+  receipt500k <- benchmarkPersistentSeparatedExtension "500k-faces" prepared500k
+  assertIdenticalFrontierLocality receipt250k receipt500k
+
+assertIdenticalFrontierLocality
+  :: ExtensionLocalityReceipt
+  -> ExtensionLocalityReceipt
+  -> IO ()
+assertIdenticalFrontierLocality smaller larger = do
+  assertPublicationAccounting "250k seam" (extensionLocalitySeamPublication smaller)
+  assertPublicationAccounting "500k seam" (extensionLocalitySeamPublication larger)
+  assertPublicationAccounting "250k refinement" (extensionLocalityRefinementPublication smaller)
+  assertPublicationAccounting "500k refinement" (extensionLocalityRefinementPublication larger)
+  assertPageAlignedPublicationLocality
+    "seam"
+    (extensionLocalitySeamPublication smaller)
+    (extensionLocalitySeamPublication larger)
+  assertPageAlignedPublicationLocality
+    "refinement"
+    (extensionLocalityRefinementPublication smaller)
+    (extensionLocalityRefinementPublication larger)
+  unless
+    ( extensionLocalityCachedFrontierPointReads smaller
+        == extensionLocalityCachedFrontierPointReads larger
+    ) $
+    fail "identical-frontier cached-read work grew with resident A"
+  unless
+    ( extensionLocalityValidationClosure smaller
+        == extensionLocalityValidationClosure larger
+    ) $
+    fail "identical local refinement produced an A-dependent closure"
+  unless
+    ( and
+        [ extensionLocalityRefinementStats smaller == extensionLocalityRefinementStats larger
+        , extensionLocalityFinalOmegaGeometry smaller == extensionLocalityFinalOmegaGeometry larger
+        , extensionLocalityAddedVertices smaller == extensionLocalityAddedVertices larger
+        , extensionLocalityFinalOmegaFaces smaller == extensionLocalityFinalOmegaFaces larger
+        , extensionLocalityTouchedEdges smaller == extensionLocalityTouchedEdges larger
+        , extensionLocalityCreatedFaces smaller == extensionLocalityCreatedFaces larger
+        , extensionLocalityInterfaceReads smaller == extensionLocalityInterfaceReads larger
+        , extensionLocalityBoundaryCrossings smaller == extensionLocalityBoundaryCrossings larger
+        ]
+    ) $
+    fail "identical-frontier refinement changed its canonical local work receipt"
+
+-- The receipt counts page opens and copied cells at the storage boundary.  It
+-- intentionally does not update a counter for every logical cell write: that
+-- instrumentation used to put a strict fourteen-field STRef update on every
+-- dense build mutation.  Local page accounting is exact and remains the
+-- observation used by the extension lane.
+assertPublicationAccounting :: String -> PublicationStats -> IO ()
+assertPublicationAccounting label stats = do
+  unless
+    ( publicationUnboxedBasePageOpens stats
+        == publicationUnboxedDirtyBasePages stats
+        && publicationBoxedBasePageOpens stats
+          == publicationBoxedDirtyBasePages stats
+        && publicationUnboxedCopiedCells stats
+          <= 1_024 * publicationUnboxedBasePageOpens stats
+        && publicationBoxedCopiedCells stats
+          <= 256 * publicationBoxedBasePageOpens stats
+    ) $
+    fail (label <> " page publication accounting is inconsistent")
+
+-- A larger resident shifts the same local write intervals within fixed-size
+-- pages.  At most two interval boundaries per store may therefore change page
+-- classification or copy one extra page; the semantic local-work receipt below
+-- remains exact.
+assertPageAlignedPublicationLocality :: String -> PublicationStats -> PublicationStats -> IO ()
+assertPageAlignedPublicationLocality label smaller larger = do
+  let unboxedPageSlack :: Int
+      unboxedPageSlack = 2 * 6
+      boxedPageSlack :: Int
+      boxedPageSlack = 2 * 4
+      dirtyUnboxed :: PublicationStats -> Int
+      dirtyUnboxed stats =
+        publicationUnboxedDirtyBasePages stats
+          + publicationUnboxedDirtyAppendedPages stats
+      dirtyBoxed :: PublicationStats -> Int
+      dirtyBoxed stats =
+        publicationBoxedDirtyBasePages stats
+          + publicationBoxedDirtyAppendedPages stats
+      within :: Int -> Int -> Int -> Bool
+      within slack left right = abs (left - right) <= slack
+  unless
+    ( and
+        [ within unboxedPageSlack (dirtyUnboxed smaller) (dirtyUnboxed larger)
+        , within (1_024 * unboxedPageSlack) (publicationUnboxedCopiedCells smaller) (publicationUnboxedCopiedCells larger)
+        , within boxedPageSlack (dirtyBoxed smaller) (dirtyBoxed larger)
+        , within (256 * boxedPageSlack) (publicationBoxedCopiedCells smaller) (publicationBoxedCopiedCells larger)
+        ]
+    ) $
+    fail ("identical-frontier " <> label <> " publication escaped its fixed page-alignment envelope")
+
+assertFaceBand :: String -> Int -> Int -> ConstrainedMesh -> IO ()
+assertFaceBand label lower upper mesh = do
+  let actual = numFaces mesh
+  unless (lower <= actual && actual <= upper) $
+    fail
+      ( "publication-separated-"
+          <> label
+          <> " expected base faces in ["
+          <> show lower
+          <> ","
+          <> show upper
+          <> "], got "
+          <> show actual
+      )
+
+-- | Two halves of one point set, joined.
+--
+-- The balanced pair rebuilds from its combined site set. The input-order and
+-- ranked lanes distinguish ordinary construction from construction whose
+-- vertex numbering is already canonical.
+benchmarkBalanced :: Int -> IO ()
+benchmarkBalanced total = do
+  let sites = randomPoints 0x9e3779b97f4a7c15 total
+      (left, right) = splitAt (total `div` 2) sites
+  leftMesh <- geometryMesh left
+  rightMesh <- geometryMesh right
+  _ <- evaluate (force (leftMesh, rightMesh))
+  _ <- timedValue "join-balanced" (requireRight (union leftMesh rightMesh))
+  _ <- timedValue "join-balanced-rebuild-input-order" (geometryMesh sites)
+  _ <- timedValue "join-balanced-rebuild-ranked-order" (geometryMesh (canonical sites))
+  pure ()
+
+-- | Two operands whose sites are separated by a vertical line.
+--
+-- This is the stratum a seam merge is defined on, and the number here is the
+-- one it has to beat: the reference schedule does not know the operands are
+-- separated and rebuilds the union regardless. A linear-time merge wins
+-- asymptotically over an @O(n log n)@ rebuild; whether it wins at the sizes
+-- anything actually merges at is this measurement and not an argument.
+--
+-- Three gap widths, because the seam's work is the cross-edge chain and the
+-- deletions it drives, and how far the two clouds stand apart decides how much
+-- of each interior the chain disturbs. A distant pair is the easy case — the
+-- chain is short and nothing inside either operand dies. An abutting pair is
+-- the hard one.
+benchmarkSeparated :: Int -> IO ()
+benchmarkSeparated total = do
+  let half = total `div` 2
+      sites = randomPoints 0xd1b54a32d192ed03 half
+      extent = 2 * maximum [abs x | Point x _ <- sites]
+  leftMesh <- geometryMesh sites
+  _ <- evaluate (force leftMesh)
+  mapM_
+    ( \(name, gap) -> do
+        let shifted = [Point (x + gap * extent) y | Point x y <- sites]
+        rightMesh <- geometryMesh shifted
+        _ <- evaluate (force rightMesh)
+        _ <- timedValue ("join-separated-" <> name) (requireRight (union leftMesh rightMesh))
+        pure ()
+    )
+    [("distant" :: String, 8), ("near", 2), ("abutting", 1.02)]
+
+-- | A large mesh joined with a small one. Rebuilding costs the whole union;
+-- inserting the small operand's sites into the large mesh costs only the
+-- insertions. This is the ratio that says whether a skewed lane is worth
+-- having, and it needs no new algorithm — 'insertMany' is already the
+-- one-transaction batch path.
+--
+-- Both lanes carry the same vertex payload so the comparison is of the
+-- schedules and not of the stores.
+benchmarkSkew :: Int -> Int -> IO ()
+benchmarkSkew large small = do
+  let bulk = randomPoints 0xbf58476d1ce4e5b9 large
+      addition = randomPoints 0x94d049bb133111eb small
+  bulkMesh <- siteMesh bulk
+  _ <- evaluate (force bulkMesh)
+  _ <-
+    timedValue
+      "join-skew-rebuild"
+      (siteMesh (canonical (bulk <> addition)))
+  _ <-
+    timedValue
+      "join-skew-insert-many"
+      (buildTriangulation <$> requireRight (insertMany bulkMesh (V.fromList addition)))
+  pure ()
+
+-- | Prepare the fixed-frontier workload once. The preview join is outside the
+-- timed lanes: it certifies that changing only the resident interior did not
+-- alter the B/J geometry or the exact Gamma section.
+preparePersistentSeparatedExtension :: Int -> Int -> IO PreparedSeparatedExtension
+preparePersistentSeparatedExtension baseCount extensionCount = do
+  let baseSites = rectangleWorldSites 0x243f6a8885a308d3 baseCount (-1) 0
+      extensionSites = rectangleWorldSites 0x13198a2e03707344 extensionCount 4 5
+      expectedBaseConstraints = rectangleConstraintGeometry (-1) 0
+      expectedExtensionConstraints = rectangleConstraintGeometry 4 5
+  base <- constrainedGeometryMesh baseSites
+  extension <- constrainedGeometryMesh extensionSites
+  _ <- evaluate (force (base, extension))
+  unless (constraintGeometry base == expectedBaseConstraints) $
+    fail "fixed A collar constraints were not retained by the source build"
+  unless (constraintGeometry extension == expectedExtensionConstraints) $
+    fail "fixed B collar constraints were not retained by the source build"
+  preview <- requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension)
+  signature <- requireRight (extensionSignature preview)
+  let expectedConstraints = Set.union expectedBaseConstraints expectedExtensionConstraints
+      actualConstraints = extensionConstraintGeometry signature
+  unless (expectedConstraints `Set.isSubsetOf` actualConstraints) $
+    fail
+      ( "source-preserving seam did not retain the fixed A/B contour constraints; missing="
+          <> show (Set.toAscList (Set.difference expectedConstraints actualConstraints))
+      )
+  pure
+    PreparedSeparatedExtension
+      { preparedBase = base
+      , preparedExtension = extension
+      , preparedSignature = signature
+      }
+
+-- | The extension lane is the operation the persistent-world plan actually
+-- promises: geometry-only publication prepares the two frontier indexes once,
+-- the caller-left constrained source stays resident, and refinement is
+-- restricted to the right-source faces plus the seam faces emitted by that
+-- interpretation. Construction, frontier preparation, receipt descent, and
+-- the final validity observation are outside the timed extension actions;
+-- neither action invokes generic union, canonical numbering, or global
+-- validation.
+benchmarkPersistentSeparatedExtension :: String -> PreparedSeparatedExtension -> IO ExtensionLocalityReceipt
+benchmarkPersistentSeparatedExtension label prepared = do
+  let base = preparedBase prepared
+      extension = preparedExtension prepared
+  joinedReceipt <-
+    timedValue
+      ("publication-separated-" <> label <> "-join")
+      (requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension))
+  let joined = constrainedSeamResultTriangulation joinedReceipt
+      permitted =
+        Set.union
+          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence joinedReceipt))))
+          (Set.fromList (V.toList (constrainedSeamJoinFaces joinedReceipt)))
+      interface = boundaryOfPermittedFaces joined permitted
+  parameters <-
+    requireRight
+      ( fmap
+          (withConstraintPreservation True . withoutMaximumRadiusEdgeRatio)
+          ( withAdditionalVertexBudget 4_096 defaultRefinementParameters
+              >>= withMaximumArea 0.0002
+          )
+      )
+  signature <- requireRight (extensionSignature joinedReceipt)
+  unless (signature == preparedSignature prepared) $
+    fail ("publication-separated-" <> label <> " changed its prepared Omega/Gamma/J geometry")
+  unless (refinementInterfaceGeometry joined permitted == extensionGammaGeometry signature) $
+    fail ("publication-separated-" <> label <> " changed its certified Gamma geometry")
+  refined <-
+    timedValue
+      ("publication-separated-" <> label <> "-local-refine")
+      ( requireRight
+          ( refineWithinDomain
+              (const ())
+              parameters
+              permitted
+              joined
+          )
+      )
+  let result = refinementDomainResult refined
+      receipt = refinementDomainReceipt refined
+      seamPublication = constrainedSeamPublicationStats joinedReceipt
+      refinementPublication = refinementPublicationStats receipt
+      validationClosure = refinementClosureStats receipt
+      finalFaces = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))
+  finalOmegaGeometry <- requireRight (faceGeometrySet (refinedTriangulation result) finalFaces)
+  assertLocalPublicationStats
+    ("publication-separated-" <> label <> " seam")
+    seamPublication
+  assertLocalPublicationStats
+    ("publication-separated-" <> label <> " refinement")
+    refinementPublication
+  unless
+    ( publicationUnboxedDirtyBasePages seamPublication
+        + publicationUnboxedDirtyAppendedPages seamPublication
+        > 0
+    ) $
+    fail ("publication-separated-" <> label <> " seam published no appended unboxed writes")
+  unless
+    ( publicationUnboxedDirtyBasePages refinementPublication
+        + publicationUnboxedDirtyAppendedPages refinementPublication
+        > 0
+    ) $
+    fail ("publication-separated-" <> label <> " refinement published no appended unboxed writes")
+  unless (refinementComplete result) $
+    fail ("publication-separated-" <> label <> " exhausted its finite local refinement budget")
+  unless (refinementInterfaceBoundaryReads receipt > 0) $
+    fail ("publication-separated-" <> label <> " performed no positive Gamma-boundary descent")
+  unless (refinementAddedVertices result > 0) $
+    fail ("publication-separated-" <> label <> " performed no positive local refinement")
+  case validateTriangulation (refinedTriangulation result) of
+    [] -> pure ()
+    violations -> fail ("publication-separated-" <> label <> " invalid: " <> show violations)
+  putStrLn
+    ( "publication-separated-"
+        <> label
+        <> "-base-faces="
+        <> show (numFaces base)
+        <> " extension-faces="
+        <> show (numFaces extension)
+        <> " receipt: omega="
+        <> show (Set.size permitted)
+        <> " b-faces="
+        <> show (V.length (constrainedSeamRightFaceEvidence joinedReceipt))
+        <> " j-faces="
+        <> show (V.length (constrainedSeamJoinFaces joinedReceipt))
+        <> " a-constraints="
+        <> show (constrainedSeamLeftConstraintCount joinedReceipt)
+        <> " gamma="
+        <> show (Set.size interface)
+        <> " final-omega="
+        <> show (V.length (refinementFinalPermittedFaces receipt))
+        <> " steiner="
+        <> show (refinementAddedVertices result)
+    )
+  renderPublicationStats
+    ("publication-separated-" <> label <> "-seam-publication")
+    seamPublication
+  putStrLn
+    ( "publication-separated-"
+        <> label
+        <> "-seam-cached-frontier-point-reads="
+        <> show (constrainedSeamCachedFrontierPointReads joinedReceipt)
+    )
+  renderPublicationStats
+    ("publication-separated-" <> label <> "-refinement-publication")
+    refinementPublication
+  putStrLn
+    ( "publication-separated-"
+        <> label
+        <> "-closure: faces="
+        <> show (closureFaces validationClosure)
+        <> " directed-edges="
+        <> show (closureDirectedEdges validationClosure)
+        <> " vertices="
+        <> show (closureVertices validationClosure)
+        <> " interface-pairs="
+        <> show (closureInterfacePairs validationClosure)
+        <> " constraint-pairs="
+        <> show (closureConstraintPairs validationClosure)
+    )
+  let (unboxedOverlayPages, boxedMaterializedPages) = publicationPageReceipt (refinedTriangulation result)
+  putStrLn
+    ( "publication-separated-"
+        <> label
+        <> "-observed-final-pages: unboxed-overlay="
+        <> show unboxedOverlayPages
+        <> " boxed-materialized="
+        <> show boxedMaterializedPages
+        <> " local-touched-edges="
+        <> show (V.length (refinementTouchedEdges receipt))
+        <> " local-created-faces="
+        <> show (V.length (refinementCreatedFaces receipt))
+    )
+  pure
+    ExtensionLocalityReceipt
+      { extensionLocalitySeamPublication = seamPublication
+      , extensionLocalityRefinementPublication = refinementPublication
+      , extensionLocalityCachedFrontierPointReads = constrainedSeamCachedFrontierPointReads joinedReceipt
+      , extensionLocalityValidationClosure = validationClosure
+      , extensionLocalityRefinementStats = refinementStats result
+      , extensionLocalityFinalOmegaGeometry = finalOmegaGeometry
+      , extensionLocalityAddedVertices = refinementAddedVertices result
+      , extensionLocalityFinalOmegaFaces = Set.size finalFaces
+      , extensionLocalityTouchedEdges = V.length (refinementTouchedEdges receipt)
+      , extensionLocalityCreatedFaces = V.length (refinementCreatedFaces receipt)
+      , extensionLocalityInterfaceReads = refinementInterfaceBoundaryReads receipt
+      , extensionLocalityBoundaryCrossings = refinementAttemptedBoundaryCrossings receipt
+      }
+
+renderPublicationStats :: String -> PublicationStats -> IO ()
+renderPublicationStats label stats =
+  putStrLn
+    ( label
+        <> ": unboxed-base-enumerations="
+        <> show (publicationUnboxedBasePageEnumerations stats)
+        <> " unboxed-base-opens="
+        <> show (publicationUnboxedBasePageOpens stats)
+        <> " unboxed-base-freezes="
+        <> show (publicationUnboxedBasePageFreezes stats)
+        <> " unboxed-dirty-base-pages="
+        <> show (publicationUnboxedDirtyBasePages stats)
+        <> " unboxed-dirty-appended-pages="
+        <> show (publicationUnboxedDirtyAppendedPages stats)
+        <> " unboxed-copied-cells="
+        <> show (publicationUnboxedCopiedCells stats)
+        <> " boxed-base-enumerations="
+        <> show (publicationBoxedBasePageEnumerations stats)
+        <> " boxed-base-opens="
+        <> show (publicationBoxedBasePageOpens stats)
+        <> " boxed-base-freezes="
+        <> show (publicationBoxedBasePageFreezes stats)
+        <> " boxed-dirty-base-pages="
+        <> show (publicationBoxedDirtyBasePages stats)
+        <> " boxed-dirty-appended-pages="
+        <> show (publicationBoxedDirtyAppendedPages stats)
+        <> " boxed-copied-cells="
+        <> show (publicationBoxedCopiedCells stats)
+    )
+
+-- | The same operand sizes at three overlap fractions. A join is sized by the
+-- union, so wholly overlapping operands must cost what one of them costs.
+benchmarkOverlap :: Int -> IO ()
+benchmarkOverlap total = do
+  let sites = randomPoints 0x2545f4914f6cdd1d total
+      half = total `div` 2
+  disjointLeft <- geometryMesh (take half sites)
+  disjointRight <- geometryMesh (drop half sites)
+  halfLeft <- geometryMesh (take half sites)
+  halfRight <- geometryMesh (drop (half `div` 2) (take (half + half `div` 2) sites))
+  sameLeft <- geometryMesh (take half sites)
+  sameRight <- geometryMesh (reverse (take half sites))
+  _ <- evaluate (force (disjointLeft, disjointRight, halfLeft, halfRight, sameLeft, sameRight))
+  _ <- timedValue "join-overlap-000" (requireRight (union disjointLeft disjointRight))
+  _ <- timedValue "join-overlap-050" (requireRight (union halfLeft halfRight))
+  _ <- timedValue "join-overlap-100" (requireRight (union sameLeft sameRight))
+  pure ()
+
+-- | 'unions' is a balanced tournament and not a fold, which is a cost claim
+-- and therefore has to be measured rather than asserted. A fold republishes an
+-- accumulator that grows by one shard per step.
+--
+-- This lane once reported the fold as the faster of the two, which was true and
+-- was not a fact about the schedules: @joinBalanced@ carried no specialization,
+-- so every join inside the tournament ran through a dictionary while
+-- the left-associated schedule at a known element type ran specialized. The tournament was
+-- paying twice for arithmetic, and that swamped the asymptotic gap it was
+-- supposed to be demonstrating.
+--
+-- The shards are dealt round-robin, so every one of them spans the whole extent
+-- and no join in the tournament is separable. That is deliberate: it is the
+-- adversarial sharding, and it measures the operator with no structure to
+-- exploit. 'benchmarkSpatialTournament' is the same tournament over the
+-- sharding a caller who wanted it to be fast would actually choose.
+benchmarkTournament :: Int -> Int -> IO ()
+benchmarkTournament total shardCount = do
+  let sites = randomPoints 0x14057b7ef767814f total
+      indexed = zip [0 :: Int ..] sites
+  shards <-
+    traverse
+      (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])
+      [0 .. shardCount - 1]
+  _ <- evaluate (force shards)
+  _ <- timedValue "join-tournament" (requireRight (unions shards))
+  _ <- timedValue "join-left-fold" (requireRight (unionsLeftAssociated shards))
+  pure ()
+
+-- | Where the tournament's advantage over the fold actually appears.
+--
+-- A fold republishes an accumulator that grows by one shard per step, so it
+-- rebuilds @Θ(nk)@ sites over @k@ shards where halving rebuilds @Θ(n log k)@.
+-- That is a statement about @k@, and at the sixteen shards the lane above uses
+-- the predicted factor is barely two — small enough to be swamped by the
+-- per-join costs both schedules pay fifteen times each. This sweep is here
+-- because a cost claim that only holds asymptotically has to say at what size
+-- it starts holding, and the answer has to be measured rather than asserted.
+benchmarkTournamentScaling :: Int -> IO ()
+benchmarkTournamentScaling total =
+  mapM_
+    ( \shardCount -> do
+        let sites = randomPoints 0x9e3779b97f4a7c15 total
+            indexed = zip [0 :: Int ..] sites
+        shards <-
+          traverse
+            (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])
+            [0 .. shardCount - 1]
+        _ <- evaluate (force shards)
+        _ <- timedValue ("join-shards-" <> show shardCount <> "-tournament") (requireRight (unions shards))
+        _ <- timedValue ("join-shards-" <> show shardCount <> "-fold") (requireRight (unionsLeftAssociated shards))
+        pure ()
+    )
+    [4 :: Int, 16, 64]
+
+
+-- | The same tournament over shards cut by abscissa rather than dealt.
+--
+-- This is the workload a seam schedule exists for, and the only one where it
+-- can pay off more than once. Shards cut into contiguous x-ranges are pairwise
+-- separated; so is every intermediate result, because the union of two adjacent
+-- ranges is a range. Every one of the fifteen joins in the tournament is
+-- therefore separable — the tournament /is/ the divide-and-conquer recursion,
+-- entered from the leaves.
+--
+-- Against the reference schedule this must cost about what the dealt
+-- tournament costs, since a rebuild cannot tell the two shardings apart. That
+-- agreement is the baseline; the gap that opens between these two lanes is the
+-- whole return on a merge kernel.
+benchmarkSpatialTournament :: Int -> Int -> IO ()
+benchmarkSpatialTournament total shardCount = do
+  let sites = randomPoints 0x3c6ef372fe94f82a total
+      ordered = sortBy (comparing (\(Point x _) -> x)) sites
+      width = (total + shardCount - 1) `div` shardCount
+  shards <-
+    traverse
+      (\shard -> geometryMesh (take width (drop (shard * width) ordered)))
+      [0 .. shardCount - 1]
+  _ <- evaluate (force shards)
+  _ <- timedValue "join-spatial-tournament" (requireRight (unions shards))
+  pure ()
+
+-- | The renumbering pass on its own, against the construction it follows.
+benchmarkCanonicalize :: Int -> IO ()
+benchmarkCanonicalize total = do
+  let sites = randomPoints 0x27d4eb2f165667c5 total
+  mesh <- geometryMesh sites
+  _ <- evaluate (force mesh)
+  _ <- timedValue "canonicalize-alone" (evaluate . force =<< requireRight (canonicalize mesh))
+  pure ()
+
+-- | The shared canonical rebuild boundary under half overlap, on both its
+-- geometry-only specializations and its annotation-preserving surface. Setup
+-- and source publication are forced before every clock; the measurements are
+-- therefore the exact site classification, rebuild, canonical publication and
+-- payload transport the public operations own.
+benchmarkSetAlgebra :: Int -> IO ()
+benchmarkSetAlgebra total = do
+  let common = total `quot` 2
+      sites = randomPoints 0x6A09E667F3BCC909 (total + common)
+      leftPoints = take total sites
+      rightPoints = drop common sites
+  leftGeometry <- geometryMesh leftPoints
+  rightGeometry <- geometryMesh rightPoints
+  emptyGeometry <- requireRight (unions [])
+  _ <- evaluate (force (leftGeometry, rightGeometry, emptyGeometry))
+  _ <- timedValue "set-intersection-unit" (requireRight (intersection leftGeometry rightGeometry))
+  _ <- timedValue "set-difference-unit" (requireRight (difference leftGeometry rightGeometry))
+  _ <-
+    timedValue
+      "set-symmetric-difference-unit"
+      (requireRight (symmetricDifference leftGeometry rightGeometry))
+  _ <-
+    timedValue
+      "set-difference-right-empty"
+      (requireRight (difference leftGeometry emptyGeometry))
+  _ <-
+    timedValue
+      "set-symmetric-difference-left-empty"
+      (requireRight (symmetricDifference emptyGeometry leftGeometry))
+  _ <- timedValue "set-relation-half-overlap" (evaluate (siteRelation leftGeometry rightGeometry))
+  leftAnnotated <- siteMesh leftPoints
+  rightAnnotated <- siteMesh rightPoints
+  _ <- evaluate (force (leftAnnotated, rightAnnotated))
+  _ <-
+    timedValue
+      "set-intersection-annotated"
+      (requireRight (intersectionWith (,) leftAnnotated rightAnnotated))
+  _ <-
+    timedValue
+      "set-difference-annotated"
+      (requireRight (difference leftAnnotated rightAnnotated))
+  _ <-
+    timedValue
+      "set-symmetric-difference-annotated"
+      (requireRight (symmetricDifference leftAnnotated rightAnnotated))
+  pure ()
+
+
+canonical :: [Point] -> [Point]
+canonical points = [Point x y | (x, y) <- dropAdjacentDuplicates (sort [(x, y) | Point x y <- points])]
+
+dropAdjacentDuplicates :: Eq a => [a] -> [a]
+dropAdjacentDuplicates (first : second : rest)
+  | first == second = dropAdjacentDuplicates (second : rest)
+  | otherwise = first : dropAdjacentDuplicates (second : rest)
+dropAdjacentDuplicates rest = rest
+
+geometryMesh :: [Point] -> IO Mesh
+geometryMesh points = requireRight (delaunayGeometry (V.fromList points))
+
+siteMesh :: [Point] -> IO SiteMesh
+siteMesh points =
+  buildTriangulation <$> requireRight (delaunay unitElementDefaults (V.fromList points))
+
+unionsLeftAssociated :: [Mesh] -> Either BuildError Mesh
+unionsLeftAssociated meshes = unions [] >>= \identity -> foldM union identity meshes
diff --git a/bench/join/Moonlight/Triangulation/JoinBench.hs b/bench/join/Moonlight/Triangulation/JoinBench.hs
deleted file mode 100644
--- a/bench/join/Moonlight/Triangulation/JoinBench.hs
+++ /dev/null
@@ -1,1009 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | What the join costs, and against what.
---
--- These lanes compare the public union schedules with rebuild, local insertion,
--- and explicit canonical observation. Schedule claims live or die by these
--- measurements rather than by asymptotic theatre.
-module Moonlight.Triangulation.JoinBench
-  ( benchmarks
-  , publicationBenchmarks
-  ) where
-
-import BenchMeasure (requireRight, timedValue)
-import BenchSupport (randomPoints)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad (foldM, unless)
-import Data.List (sort, sortBy)
-import Data.Ord (comparing)
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Data.Word (Word64)
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad (insertMany)
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Types (BuildStats, RefinementResult (refinementStats))
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedMaterializedPageCount)
-import Moonlight.Triangulation.Internal.Paged (pagedOverlayPageCount)
-import Moonlight.Triangulation.Internal.PointIndex (lookupPointIndex)
-import Moonlight.Triangulation.Internal.Representation
-  ( Triangulation
-      ( triConstraint
-      , triDirectedData
-      , triFaceData
-      , triFaceEdge
-      , triHalfTopology
-      , triPointIndex
-      , triPointX
-      , triPointY
-      , triUndirectedData
-      , triVertexData
-      , triVertexOut
-      )
-  )
-import System.Mem (performGC)
-
-type Mesh = DelaunayTriangulation ()
-type SiteMesh = DelaunayTriangulation (Point)
-type ConstrainedMesh = ConstrainedDelaunayTriangulation ()
-
-data ExtensionSignature = ExtensionSignature
-  { extensionOmegaGeometry :: !(Set.Set (Point, Point, Point))
-  , extensionGammaGeometry :: !(Set.Set (Point, Point))
-  , extensionSeamGeometry :: !(Set.Set (Point, Point, Point))
-  , extensionConstraintGeometry :: !(Set.Set (Point, Point))
-  }
-  deriving stock (Eq, Show)
-
-data PreparedSeparatedExtension = PreparedSeparatedExtension
-  { preparedBase :: !ConstrainedMesh
-  , preparedExtension :: !ConstrainedMesh
-  , preparedSignature :: !ExtensionSignature
-  }
-
--- | The scale comparison retains only observations emitted by the local
--- interpreters.  Resident face count is deliberately absent: it is the
--- independent variable, not permission to normalize local work by A.
-data ExtensionLocalityReceipt = ExtensionLocalityReceipt
-  { extensionLocalitySeamPublication :: !PublicationStats
-  , extensionLocalityRefinementPublication :: !PublicationStats
-  , extensionLocalityCachedFrontierPointReads :: !Int
-  , extensionLocalityValidationClosure :: !ValidationClosureStats
-  , extensionLocalityRefinementStats :: !BuildStats
-  , extensionLocalityFinalOmegaGeometry :: !(Set.Set (Point, Point, Point))
-  , extensionLocalityAddedVertices :: !Int
-  , extensionLocalityFinalOmegaFaces :: !Int
-  , extensionLocalityTouchedEdges :: !Int
-  , extensionLocalityCreatedFaces :: !Int
-  , extensionLocalityInterfaceReads :: !Int
-  , extensionLocalityBoundaryCrossings :: !Int
-  }
-  deriving stock (Eq, Show)
-
-rectangleBoundarySites :: Double -> Double -> [Point]
-rectangleBoundarySites left right =
-  [ Point left (-1)
-  , Point right (-1)
-  ]
-    <> fmap (Point right) [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]
-    <> [Point right 1, Point left 1]
-
-rectangleCollarSites :: Double -> Double -> [Point]
-rectangleCollarSites left right =
-  let ys = [-0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75]
-      width = right - left
-   in fmap (Point (left + 0.2 * width)) ys
-        <> fmap (Point (right - 0.2 * width)) ys
-
-rectangleWorldSites :: Word64 -> Int -> Double -> Double -> [Point]
-rectangleWorldSites seed count left right =
-  rectangleFixedSites left right
-    <> fmap
-      (\(Point x y) ->
-         Point
-           (left + 0.35 * (right - left) + 0.15 * (right - left) * (x + 1))
-           (-0.6 + 0.6 * (y + 1)))
-      (randomPoints seed (max 0 (count - length (rectangleFixedSites left right))))
-
-rectangleFixedSites :: Double -> Double -> [Point]
-rectangleFixedSites left right = rectangleBoundarySites left right <> rectangleCollarSites left right
-
-rectangleContour :: Int -> [(Int, Int)]
-rectangleContour count =
-  [ (index, (index + 1) `mod` count)
-  | index <- [0 .. count - 1]
-  ]
-
-rectangleConstraintPairs :: [(Int, Int)]
-rectangleConstraintPairs =
-  rectangleContour boundaryCount
-    <> chain collarLeftStart
-    <> chain collarRightStart
- where
-  boundaryCount = length (rectangleBoundarySites 0 1)
-  collarLength = length (rectangleCollarSites 0 1) `quot` 2
-  collarLeftStart = boundaryCount
-  collarRightStart = collarLeftStart + collarLength
-  chain start =
-    [ (start + index, start + index + 1)
-    | index <- [0 .. collarLength - 2]
-    ]
-
-contourPairs :: [Point] -> [(Point, Point)]
-contourPairs points =
-  case points of
-    [] -> []
-    first : rest -> zip points (rest <> [first])
-
-orderedPointPair :: (Point, Point) -> (Point, Point)
-orderedPointPair (first, second)
-  | first <= second = (first, second)
-  | otherwise = (second, first)
-
-contourGeometry :: [Point] -> Set.Set (Point, Point)
-contourGeometry = Set.fromList . fmap orderedPointPair . contourPairs
-
-rectangleConstraintGeometry :: Double -> Double -> Set.Set (Point, Point)
-rectangleConstraintGeometry left right =
-  let boundary = rectangleBoundarySites left right
-      collar = rectangleCollarSites left right
-      collarLength = length collar `quot` 2
-      (leftCollar, rightCollar) = splitAt collarLength collar
-   in Set.unions
-        [ contourGeometry boundary
-        , openChainGeometry leftCollar
-        , openChainGeometry rightCollar
-        ]
-
-openChainGeometry :: [Point] -> Set.Set (Point, Point)
-openChainGeometry points =
-  Set.fromList
-    [ orderedPointPair pair
-    | pair <- zip points (drop 1 points)
-    ]
-
-constraintGeometry :: ConstrainedMesh -> Set.Set (Point, Point)
-constraintGeometry mesh =
-  Set.fromList
-    [ orderedPointPair (segmentStart segment, segmentEnd segment)
-    | segment <- V.toList (constraintSegments mesh)
-    ]
-
-constrainedGeometryMesh :: [Point] -> IO ConstrainedMesh
-constrainedGeometryMesh points =
-  geometryOnlyPublication . buildTriangulation
-    <$> requireRight
-      ( constrainedDelaunay
-          unitElementDefaults
-          (V.fromList points)
-          (V.fromList rectangleConstraintPairs)
-      )
-
-boundaryOfPermittedFaces
-  :: ConstrainedMesh
-  -> Set.Set FaceId
-  -> Set.Set UndirectedEdgeId
-boundaryOfPermittedFaces mesh faces =
-  Set.fromList
-    [ asUndirected edge
-    | face <- Set.toList faces
-    , edge <- Dcel.faceDirectedEdges mesh face
-    , let adjacent = Dcel.incidentFace mesh (reverseEdge edge)
-    , adjacent /= Dcel.outerFace
-    , Set.notMember adjacent faces
-    ]
-
-faceGeometry :: ConstrainedMesh -> FaceId -> Maybe (Point, Point, Point)
-faceGeometry mesh face =
-  case sort (fmap (Dcel.vertexPoint mesh) (Dcel.faceVertices mesh face)) of
-    [first, second, third] -> Just (first, second, third)
-    _ -> Nothing
-
-faceGeometrySet
-  :: ConstrainedMesh
-  -> Set.Set FaceId
-  -> Either String (Set.Set (Point, Point, Point))
-faceGeometrySet mesh faces =
-  Set.fromList
-    <$> traverse
-      (\face ->
-         maybe
-           (Left ("local refinement receipt named a non-triangular face: " <> show face))
-           Right
-           (faceGeometry mesh face))
-      (Set.toList faces)
-
-edgeGeometry :: ConstrainedMesh -> UndirectedEdgeId -> (Point, Point)
-edgeGeometry mesh edge =
-  let (fromVertex, toVertex) = Dcel.undirectedEndpoints mesh edge
-   in orderedPointPair
-        (Dcel.vertexPoint mesh fromVertex, Dcel.vertexPoint mesh toVertex)
-
-extensionSignature :: ConstrainedSeamResult () -> Either String ExtensionSignature
-extensionSignature receipt =
-  let joined = constrainedSeamResultTriangulation receipt
-      permitted =
-        Set.union
-          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence receipt))))
-          (Set.fromList (V.toList (constrainedSeamJoinFaces receipt)))
-      interface = boundaryOfPermittedFaces joined permitted
-   in do
-        seamFaces <-
-          Set.fromList
-            <$> traverse
-              (\face ->
-                 case faceGeometry joined face of
-                   Just geometry -> Right geometry
-                   Nothing -> Left "seam receipt named a non-triangular face")
-              (V.toList (constrainedSeamJoinFaces receipt))
-        let omegaFaces =
-              Set.union
-                ( Set.fromList
-                    [ ( constrainedSeamFaceFirstPoint evidence
-                      , constrainedSeamFaceSecondPoint evidence
-                      , constrainedSeamFaceThirdPoint evidence
-                      )
-                    | evidence <- V.toList (constrainedSeamRightFaceEvidence receipt)
-                    ]
-                )
-                seamFaces
-        pure
-          ExtensionSignature
-            { extensionOmegaGeometry = omegaFaces
-            , extensionGammaGeometry = Set.fromList (fmap (edgeGeometry joined) (Set.toList interface))
-            , extensionSeamGeometry = seamFaces
-            , extensionConstraintGeometry = constraintGeometry joined
-            }
-
-refinementInterfaceGeometry :: ConstrainedMesh -> Set.Set FaceId -> Set.Set (Point, Point)
-refinementInterfaceGeometry mesh faces =
-  Set.fromList
-    [ edgeGeometry mesh edge
-    | edge <- Set.toList (boundaryOfPermittedFaces mesh faces)
-    ]
-
-assertLocalPublicationStats :: String -> PublicationStats -> IO ()
-assertLocalPublicationStats label stats = do
-  unless (publicationUnboxedBasePageEnumerations stats == 0) $
-    fail (label <> " enumerated resident unboxed base pages")
-  unless (publicationUnboxedBasePageFreezes stats == 0) $
-    fail (label <> " froze resident unboxed base pages")
-  unless (publicationBoxedBasePageEnumerations stats == 0) $
-    fail (label <> " enumerated resident boxed base pages")
-  unless (publicationBoxedBasePageFreezes stats == 0) $
-    fail (label <> " froze resident boxed base pages")
-
--- These are post-publication representation observations. They do not count
--- base-page opens or freeze enumeration and therefore are not a proof that
--- those events were absent from the timed action.
-publicationPageReceipt :: ConstrainedMesh -> (Int, Int)
-publicationPageReceipt mesh =
-  ( sum
-      [ pagedOverlayPageCount (triPointX mesh)
-      , pagedOverlayPageCount (triPointY mesh)
-      , pagedOverlayPageCount (triVertexOut mesh)
-      , pagedOverlayPageCount (triHalfTopology mesh)
-      , pagedOverlayPageCount (triFaceEdge mesh)
-      , pagedOverlayPageCount (triConstraint mesh)
-      ]
-  , sum
-      [ boxedMaterializedPageCount (triVertexData mesh)
-      , boxedMaterializedPageCount (triDirectedData mesh)
-      , boxedMaterializedPageCount (triUndirectedData mesh)
-      , boxedMaterializedPageCount (triFaceData mesh)
-      ]
-  )
-
-benchmarks :: IO ()
-benchmarks = do
-  benchmarkBalanced 20_000
-  benchmarkSeparated 20_000
-  benchmarkSkew 20_000 200
-  benchmarkOverlap 20_000
-  benchmarkTournament 20_000 16
-  benchmarkSpatialTournament 20_000 16
-  benchmarkTournamentScaling 20_000
-  benchmarkCanonicalize 20_000
-  benchmarkSetAlgebra 20_000
-
-publicationBenchmarks :: IO ()
-publicationBenchmarks = do
-  -- These are face bands, not site-count labels.  A planar Delaunay mesh is
-  -- approximately twice as many faces as sites, so the resident fixtures use
-  -- 125k and 250k sites to exercise the requested 250k/500k-face lanes, and
-  -- the incoming small world uses 15k sites for its approximately 30k faces.
-  prepared250k <- preparePersistentSeparatedExtension 125_000 15_000
-  prepared500k <- preparePersistentSeparatedExtension 250_000 15_000
-  assertFaceBand "250k-faces" 245_000 255_000 (preparedBase prepared250k)
-  assertFaceBand "500k-faces" 490_000 510_000 (preparedBase prepared500k)
-  assertFaceBand "30k-extension-faces" 29_000 31_000 (preparedExtension prepared250k)
-  assertFaceBand "30k-extension-faces-repeat" 29_000 31_000 (preparedExtension prepared500k)
-  unless (preparedSignature prepared250k == preparedSignature prepared500k) $
-    fail "separated extension changed its fixed collar, Gamma, or seam geometry across A sizes"
-  receipt250k <- benchmarkPersistentSeparatedExtension "250k-faces" prepared250k
-  receipt500k <- benchmarkPersistentSeparatedExtension "500k-faces" prepared500k
-  assertIdenticalFrontierLocality receipt250k receipt500k
-
-assertIdenticalFrontierLocality
-  :: ExtensionLocalityReceipt
-  -> ExtensionLocalityReceipt
-  -> IO ()
-assertIdenticalFrontierLocality smaller larger = do
-  assertPublicationAccounting "250k seam" (extensionLocalitySeamPublication smaller)
-  assertPublicationAccounting "500k seam" (extensionLocalitySeamPublication larger)
-  assertPublicationAccounting "250k refinement" (extensionLocalityRefinementPublication smaller)
-  assertPublicationAccounting "500k refinement" (extensionLocalityRefinementPublication larger)
-  assertPageAlignedPublicationLocality
-    "seam"
-    (extensionLocalitySeamPublication smaller)
-    (extensionLocalitySeamPublication larger)
-  assertPageAlignedPublicationLocality
-    "refinement"
-    (extensionLocalityRefinementPublication smaller)
-    (extensionLocalityRefinementPublication larger)
-  unless
-    ( extensionLocalityCachedFrontierPointReads smaller
-        == extensionLocalityCachedFrontierPointReads larger
-    ) $
-    fail "identical-frontier cached-read work grew with resident A"
-  unless
-    ( extensionLocalityValidationClosure smaller
-        == extensionLocalityValidationClosure larger
-    ) $
-    fail "identical local refinement produced an A-dependent validation closure"
-  unless
-    ( and
-        [ extensionLocalityRefinementStats smaller == extensionLocalityRefinementStats larger
-        , extensionLocalityFinalOmegaGeometry smaller == extensionLocalityFinalOmegaGeometry larger
-        , extensionLocalityAddedVertices smaller == extensionLocalityAddedVertices larger
-        , extensionLocalityFinalOmegaFaces smaller == extensionLocalityFinalOmegaFaces larger
-        , extensionLocalityTouchedEdges smaller == extensionLocalityTouchedEdges larger
-        , extensionLocalityCreatedFaces smaller == extensionLocalityCreatedFaces larger
-        , extensionLocalityInterfaceReads smaller == extensionLocalityInterfaceReads larger
-        , extensionLocalityBoundaryCrossings smaller == extensionLocalityBoundaryCrossings larger
-        ]
-    ) $
-    fail "identical-frontier refinement changed its canonical local work receipt"
-
--- The receipt counts page opens and copied cells at the storage boundary.  It
--- intentionally does not update a counter for every logical cell write: that
--- instrumentation used to put a strict fourteen-field STRef update on every
--- dense build mutation.  Local page accounting is exact and remains the
--- observation used by the extension lane.
-assertPublicationAccounting :: String -> PublicationStats -> IO ()
-assertPublicationAccounting label stats = do
-  unless
-    ( publicationUnboxedBasePageOpens stats
-        == publicationUnboxedDirtyBasePages stats
-        && publicationBoxedBasePageOpens stats
-          == publicationBoxedDirtyBasePages stats
-        && publicationUnboxedCopiedCells stats
-          <= 1_024 * publicationUnboxedBasePageOpens stats
-        && publicationBoxedCopiedCells stats
-          <= 256 * publicationBoxedBasePageOpens stats
-    ) $
-    fail (label <> " page publication accounting is inconsistent")
-
--- A larger resident shifts the same local write intervals within fixed-size
--- pages.  At most two interval boundaries per store may therefore change page
--- classification or copy one extra page; the semantic local-work receipt below
--- remains exact.
-assertPageAlignedPublicationLocality :: String -> PublicationStats -> PublicationStats -> IO ()
-assertPageAlignedPublicationLocality label smaller larger = do
-  let unboxedPageSlack :: Int
-      unboxedPageSlack = 2 * 6
-      boxedPageSlack :: Int
-      boxedPageSlack = 2 * 4
-      dirtyUnboxed :: PublicationStats -> Int
-      dirtyUnboxed stats =
-        publicationUnboxedDirtyBasePages stats
-          + publicationUnboxedDirtyAppendedPages stats
-      dirtyBoxed :: PublicationStats -> Int
-      dirtyBoxed stats =
-        publicationBoxedDirtyBasePages stats
-          + publicationBoxedDirtyAppendedPages stats
-      within :: Int -> Int -> Int -> Bool
-      within slack left right = abs (left - right) <= slack
-  unless
-    ( and
-        [ within unboxedPageSlack (dirtyUnboxed smaller) (dirtyUnboxed larger)
-        , within (1_024 * unboxedPageSlack) (publicationUnboxedCopiedCells smaller) (publicationUnboxedCopiedCells larger)
-        , within boxedPageSlack (dirtyBoxed smaller) (dirtyBoxed larger)
-        , within (256 * boxedPageSlack) (publicationBoxedCopiedCells smaller) (publicationBoxedCopiedCells larger)
-        ]
-    ) $
-    fail ("identical-frontier " <> label <> " publication escaped its fixed page-alignment envelope")
-
-assertFaceBand :: String -> Int -> Int -> ConstrainedMesh -> IO ()
-assertFaceBand label lower upper mesh = do
-  let actual = numFaces mesh
-  unless (lower <= actual && actual <= upper) $
-    fail
-      ( "publication-separated-"
-          <> label
-          <> " expected base faces in ["
-          <> show lower
-          <> ","
-          <> show upper
-          <> "], got "
-          <> show actual
-      )
-
-benchmarkIndexedSupportContexts :: Int -> Int -> IO ()
-benchmarkIndexedSupportContexts baseCount deltaCount = do
-  let baseSites = randomPoints 0xcbbb9d5dc1059ed8 baseCount
-      retainedSites = drop deltaCount baseSites
-  benchmarkColdRelationWarmIntersection baseSites retainedSites
-  performGC
-  benchmarkColdIntersectionWarmRelation baseSites retainedSites
-  performGC
-
-benchmarkColdRelationWarmIntersection :: [Point] -> [Point] -> IO ()
-benchmarkColdRelationWarmIntersection baseSites retainedSites = do
-  base <- geometryMesh baseSites
-  retained <- geometryMesh retainedSites
-  _ <- evaluate (force (base, retained))
-  relation <- timedValue "set-relation-near-full-cold-index" (evaluate (force (siteRelation base retained)))
-  unless (relation == RightProperSubset) $
-    fail "cold indexed relation misclassified the retained operand"
-  forceExactPointIndex base baseSites
-  benchmarkIndexedIntersection "set-intersection-near-full-warm-index" base retained
-
-benchmarkColdIntersectionWarmRelation :: [Point] -> [Point] -> IO ()
-benchmarkColdIntersectionWarmRelation baseSites retainedSites = do
-  base <- geometryMesh baseSites
-  retained <- geometryMesh retainedSites
-  _ <- evaluate (force (base, retained))
-  benchmarkIndexedIntersection "set-intersection-near-full-cold-index" base retained
-  forceExactPointIndex base baseSites
-  relation <- timedValue "set-relation-near-full-warm-index" (evaluate (force (siteRelation base retained)))
-  unless (relation == RightProperSubset) $
-    fail "warm indexed relation misclassified the retained operand"
-
-benchmarkIndexedIntersection :: String -> Mesh -> Mesh -> IO ()
-benchmarkIndexedIntersection label base retained = do
-  result <- benchmarkValidatedSetOperation label (intersection base retained)
-  observedCanonical <- evaluate . force =<< requireRight (canonicalize result)
-  expectedCanonical <- evaluate . force =<< requireRight (canonicalize retained)
-  unless (observedCanonical == expectedCanonical) $
-    fail (label <> " disagreed with the retained operand")
-
-forceExactPointIndex :: Mesh -> [Point] -> IO ()
-forceExactPointIndex triangulation points =
-  case points of
-    [] -> fail "cannot warm a point index without a witness"
-    witness : _ ->
-      case
-          lookupPointIndex
-            (triPointX triangulation)
-            (triPointY triangulation)
-            (triPointIndex triangulation)
-            witness
-        of
-          Nothing -> fail "point-index warmup missed its exact witness"
-          Just vertex -> () <$ evaluate (force vertex)
-
--- | Two halves of one point set, joined.
---
--- The balanced pair rebuilds from its combined site set. The input-order and
--- ranked lanes distinguish ordinary construction from construction whose
--- vertex numbering is already canonical.
-benchmarkBalanced :: Int -> IO ()
-benchmarkBalanced total = do
-  let sites = randomPoints 0x9e3779b97f4a7c15 total
-      (left, right) = splitAt (total `div` 2) sites
-  leftMesh <- geometryMesh left
-  rightMesh <- geometryMesh right
-  _ <- evaluate (force (leftMesh, rightMesh))
-  _ <- timedValue "join-balanced" (requireRight (union leftMesh rightMesh))
-  _ <- timedValue "join-balanced-rebuild-input-order" (geometryMesh sites)
-  _ <- timedValue "join-balanced-rebuild-ranked-order" (geometryMesh (canonical sites))
-  pure ()
-
--- | Two operands whose sites are separated by a vertical line.
---
--- This is the stratum a seam merge is defined on, and the number here is the
--- one it has to beat: the reference schedule does not know the operands are
--- separated and rebuilds the union regardless. A linear-time merge wins
--- asymptotically over an @O(n log n)@ rebuild; whether it wins at the sizes
--- anything actually merges at is this measurement and not an argument.
---
--- Three gap widths, because the seam's work is the cross-edge chain and the
--- deletions it drives, and how far the two clouds stand apart decides how much
--- of each interior the chain disturbs. A distant pair is the easy case — the
--- chain is short and nothing inside either operand dies. An abutting pair is
--- the hard one.
-benchmarkSeparated :: Int -> IO ()
-benchmarkSeparated total = do
-  let half = total `div` 2
-      sites = randomPoints 0xd1b54a32d192ed03 half
-      extent = 2 * maximum [abs x | Point x _ <- sites]
-  leftMesh <- geometryMesh sites
-  _ <- evaluate (force leftMesh)
-  mapM_
-    ( \(name, gap) -> do
-        let shifted = [Point (x + gap * extent) y | Point x y <- sites]
-        rightMesh <- geometryMesh shifted
-        _ <- evaluate (force rightMesh)
-        _ <- timedValue ("join-separated-" <> name) (requireRight (union leftMesh rightMesh))
-        pure ()
-    )
-    [("distant" :: String, 8), ("near", 2), ("abutting", 1.02)]
-
--- | A large mesh joined with a small one. Rebuilding costs the whole union;
--- inserting the small operand's sites into the large mesh costs only the
--- insertions. This is the ratio that says whether a skewed lane is worth
--- having, and it needs no new algorithm — 'insertMany' is already the
--- one-transaction batch path.
---
--- Both lanes carry the same vertex payload so the comparison is of the
--- schedules and not of the stores.
-benchmarkSkew :: Int -> Int -> IO ()
-benchmarkSkew large small = do
-  let bulk = randomPoints 0xbf58476d1ce4e5b9 large
-      addition = randomPoints 0x94d049bb133111eb small
-  bulkMesh <- siteMesh bulk
-  _ <- evaluate (force bulkMesh)
-  _ <-
-    timedValue
-      "join-skew-rebuild"
-      (siteMesh (canonical (bulk <> addition)))
-  _ <-
-    timedValue
-      "join-skew-insert-many"
-      (buildTriangulation <$> requireRight (insertMany bulkMesh (V.fromList addition)))
-  pure ()
-
--- | Prepare the fixed-frontier workload once. The preview join is outside the
--- timed lanes: it certifies that changing only the resident interior did not
--- alter the B/J geometry or the exact Gamma section.
-preparePersistentSeparatedExtension :: Int -> Int -> IO PreparedSeparatedExtension
-preparePersistentSeparatedExtension baseCount extensionCount = do
-  let baseSites = rectangleWorldSites 0x243f6a8885a308d3 baseCount (-1) 0
-      extensionSites = rectangleWorldSites 0x13198a2e03707344 extensionCount 4 5
-      expectedBaseConstraints = rectangleConstraintGeometry (-1) 0
-      expectedExtensionConstraints = rectangleConstraintGeometry 4 5
-  base <- constrainedGeometryMesh baseSites
-  extension <- constrainedGeometryMesh extensionSites
-  _ <- evaluate (force (base, extension))
-  unless (constraintGeometry base == expectedBaseConstraints) $
-    fail "fixed A collar constraints were not retained by the source build"
-  unless (constraintGeometry extension == expectedExtensionConstraints) $
-    fail "fixed B collar constraints were not retained by the source build"
-  preview <- requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension)
-  signature <- requireRight (extensionSignature preview)
-  let expectedConstraints = Set.union expectedBaseConstraints expectedExtensionConstraints
-      actualConstraints = extensionConstraintGeometry signature
-  unless (expectedConstraints `Set.isSubsetOf` actualConstraints) $
-    fail
-      ( "source-preserving seam did not retain the fixed A/B contour constraints; missing="
-          <> show (Set.toAscList (Set.difference expectedConstraints actualConstraints))
-      )
-  pure
-    PreparedSeparatedExtension
-      { preparedBase = base
-      , preparedExtension = extension
-      , preparedSignature = signature
-      }
-
--- | The extension lane is the operation the persistent-world plan actually
--- promises: geometry-only publication prepares the two frontier indexes once,
--- the caller-left constrained source stays resident, and refinement is
--- restricted to the right-source faces plus the seam faces emitted by that
--- interpretation. Construction, frontier preparation, receipt descent, and
--- the final validity observation are outside the timed extension actions;
--- neither action invokes generic union, canonical numbering, or global
--- validation.
-benchmarkPersistentSeparatedExtension :: String -> PreparedSeparatedExtension -> IO ExtensionLocalityReceipt
-benchmarkPersistentSeparatedExtension label prepared = do
-  let base = preparedBase prepared
-      extension = preparedExtension prepared
-  joinedReceipt <-
-    timedValue
-      ("publication-separated-" <> label <> "-join")
-      (requireRight (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters base extension))
-  let joined = constrainedSeamResultTriangulation joinedReceipt
-      permitted =
-        Set.union
-          (Set.fromList (fmap constrainedSeamTargetFace (V.toList (constrainedSeamRightFaceEvidence joinedReceipt))))
-          (Set.fromList (V.toList (constrainedSeamJoinFaces joinedReceipt)))
-      interface = boundaryOfPermittedFaces joined permitted
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 4_096
-          , refineMaxArea = Just 0.0002
-          , refineMaxRadiusEdgeRatio = Nothing
-          , refineKeepConstraintEdges = True
-          }
-  signature <- requireRight (extensionSignature joinedReceipt)
-  unless (signature == preparedSignature prepared) $
-    fail ("publication-separated-" <> label <> " changed its prepared Omega/Gamma/J geometry")
-  unless (refinementInterfaceGeometry joined permitted == extensionGammaGeometry signature) $
-    fail ("publication-separated-" <> label <> " changed its certified Gamma geometry")
-  refined <-
-    timedValue
-      ("publication-separated-" <> label <> "-local-refine")
-      ( requireRight
-          ( refineWithinDomain
-              (const ())
-              parameters
-              permitted
-              joined
-          )
-      )
-  let result = refinementDomainResult refined
-      receipt = refinementDomainReceipt refined
-      seamPublication = constrainedSeamPublicationStats joinedReceipt
-      refinementPublication = refinementPublicationStats receipt
-      validationClosure = refinementValidationClosureStats receipt
-      finalFaces = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))
-  finalOmegaGeometry <- requireRight (faceGeometrySet (refinedTriangulation result) finalFaces)
-  assertLocalPublicationStats
-    ("publication-separated-" <> label <> " seam")
-    seamPublication
-  assertLocalPublicationStats
-    ("publication-separated-" <> label <> " refinement")
-    refinementPublication
-  unless
-    ( publicationUnboxedDirtyBasePages seamPublication
-        + publicationUnboxedDirtyAppendedPages seamPublication
-        > 0
-    ) $
-    fail ("publication-separated-" <> label <> " seam published no appended unboxed writes")
-  unless
-    ( publicationUnboxedDirtyBasePages refinementPublication
-        + publicationUnboxedDirtyAppendedPages refinementPublication
-        > 0
-    ) $
-    fail ("publication-separated-" <> label <> " refinement published no appended unboxed writes")
-  unless (refinementComplete result) $
-    fail ("publication-separated-" <> label <> " exhausted its finite local refinement budget")
-  unless (refinementInterfaceBoundaryReads receipt > 0) $
-    fail ("publication-separated-" <> label <> " performed no positive Gamma-boundary descent")
-  unless (refinementAddedVertices result > 0) $
-    fail ("publication-separated-" <> label <> " performed no positive local refinement")
-  case validateTriangulation (refinedTriangulation result) of
-    [] -> pure ()
-    violations -> fail ("publication-separated-" <> label <> " invalid: " <> show violations)
-  putStrLn
-    ( "publication-separated-"
-        <> label
-        <> "-base-faces="
-        <> show (numFaces base)
-        <> " extension-faces="
-        <> show (numFaces extension)
-        <> " receipt: omega="
-        <> show (Set.size permitted)
-        <> " b-faces="
-        <> show (V.length (constrainedSeamRightFaceEvidence joinedReceipt))
-        <> " j-faces="
-        <> show (V.length (constrainedSeamJoinFaces joinedReceipt))
-        <> " a-constraints="
-        <> show (constrainedSeamLeftConstraintCount joinedReceipt)
-        <> " gamma="
-        <> show (Set.size interface)
-        <> " final-omega="
-        <> show (V.length (refinementFinalPermittedFaces receipt))
-        <> " steiner="
-        <> show (refinementAddedVertices result)
-    )
-  renderPublicationStats
-    ("publication-separated-" <> label <> "-seam-publication")
-    seamPublication
-  putStrLn
-    ( "publication-separated-"
-        <> label
-        <> "-seam-cached-frontier-point-reads="
-        <> show (constrainedSeamCachedFrontierPointReads joinedReceipt)
-    )
-  renderPublicationStats
-    ("publication-separated-" <> label <> "-refinement-publication")
-    refinementPublication
-  putStrLn
-    ( "publication-separated-"
-        <> label
-        <> "-validation-closure: faces="
-        <> show (validationClosureFaces validationClosure)
-        <> " directed-edges="
-        <> show (validationClosureDirectedEdges validationClosure)
-        <> " vertices="
-        <> show (validationClosureVertices validationClosure)
-        <> " interface-pairs="
-        <> show (validationClosureInterfacePairs validationClosure)
-        <> " constraint-pairs="
-        <> show (validationClosureConstraintPairs validationClosure)
-    )
-  let (unboxedOverlayPages, boxedMaterializedPages) = publicationPageReceipt (refinedTriangulation result)
-  putStrLn
-    ( "publication-separated-"
-        <> label
-        <> "-observed-final-pages: unboxed-overlay="
-        <> show unboxedOverlayPages
-        <> " boxed-materialized="
-        <> show boxedMaterializedPages
-        <> " local-touched-edges="
-        <> show (V.length (refinementTouchedEdges receipt))
-        <> " local-created-faces="
-        <> show (V.length (refinementCreatedFaces receipt))
-    )
-  pure
-    ExtensionLocalityReceipt
-      { extensionLocalitySeamPublication = seamPublication
-      , extensionLocalityRefinementPublication = refinementPublication
-      , extensionLocalityCachedFrontierPointReads = constrainedSeamCachedFrontierPointReads joinedReceipt
-      , extensionLocalityValidationClosure = validationClosure
-      , extensionLocalityRefinementStats = refinementStats result
-      , extensionLocalityFinalOmegaGeometry = finalOmegaGeometry
-      , extensionLocalityAddedVertices = refinementAddedVertices result
-      , extensionLocalityFinalOmegaFaces = Set.size finalFaces
-      , extensionLocalityTouchedEdges = V.length (refinementTouchedEdges receipt)
-      , extensionLocalityCreatedFaces = V.length (refinementCreatedFaces receipt)
-      , extensionLocalityInterfaceReads = refinementInterfaceBoundaryReads receipt
-      , extensionLocalityBoundaryCrossings = refinementAttemptedBoundaryCrossings receipt
-      }
-
-renderPublicationStats :: String -> PublicationStats -> IO ()
-renderPublicationStats label stats =
-  putStrLn
-    ( label
-        <> ": unboxed-base-enumerations="
-        <> show (publicationUnboxedBasePageEnumerations stats)
-        <> " unboxed-base-opens="
-        <> show (publicationUnboxedBasePageOpens stats)
-        <> " unboxed-base-freezes="
-        <> show (publicationUnboxedBasePageFreezes stats)
-        <> " unboxed-dirty-base-pages="
-        <> show (publicationUnboxedDirtyBasePages stats)
-        <> " unboxed-dirty-appended-pages="
-        <> show (publicationUnboxedDirtyAppendedPages stats)
-        <> " unboxed-copied-cells="
-        <> show (publicationUnboxedCopiedCells stats)
-        <> " boxed-base-enumerations="
-        <> show (publicationBoxedBasePageEnumerations stats)
-        <> " boxed-base-opens="
-        <> show (publicationBoxedBasePageOpens stats)
-        <> " boxed-base-freezes="
-        <> show (publicationBoxedBasePageFreezes stats)
-        <> " boxed-dirty-base-pages="
-        <> show (publicationBoxedDirtyBasePages stats)
-        <> " boxed-dirty-appended-pages="
-        <> show (publicationBoxedDirtyAppendedPages stats)
-        <> " boxed-copied-cells="
-        <> show (publicationBoxedCopiedCells stats)
-    )
-
-benchmarkPersistentSetOperations :: Int -> Int -> IO ()
-benchmarkPersistentSetOperations baseCount deltaCount = do
-  let baseSites = randomPoints 0xcbbb9d5dc1059ed8 baseCount
-      removedSites = take deltaCount baseSites
-      retainedSites = drop deltaCount baseSites
-      extensionSites =
-        fmap
-          (\(Point x y) -> Point (1.2 + 0.1 * x) y)
-          (randomPoints 0x629a292a367cd507 deltaCount)
-  base <- geometryMesh baseSites
-  removed <- geometryMesh removedSites
-  retained <- geometryMesh retainedSites
-  extension <- geometryMesh extensionSites
-  empty <- requireRight (unions [])
-  _ <- evaluate (force (base, removed, retained, extension, empty))
-  putStrLn ("set-publication-base-sites: " <> show (numVertices base))
-  putStrLn ("set-publication-delta-sites: " <> show (numVertices removed))
-  _ <- benchmarkValidatedSetOperation "set-publication-difference-right-empty" (difference base empty)
-  _ <- benchmarkValidatedSetOperation "set-publication-symmetric-difference-left-empty" (symmetricDifference empty base)
-  _ <- benchmarkValidatedSetOperation "set-publication-symmetric-difference-right-empty" (symmetricDifference base empty)
-  benchmarkPublishedSetOperation
-    "set-publication-difference-skew"
-    (difference base removed)
-    (pure retained)
-  benchmarkPublishedSetOperation
-    "set-publication-intersection-skew"
-    (intersection base retained)
-    (pure retained)
-  benchmarkPublishedSetOperation
-    "set-publication-symmetric-difference-disjoint-skew"
-    (symmetricDifference base extension)
-    (geometryMesh (baseSites <> extensionSites))
-  benchmarkPublishedSetOperation
-    "set-publication-symmetric-difference-small-output"
-    (symmetricDifference base retained)
-    (pure removed)
-
-benchmarkPublishedSetOperation :: String -> Either BuildError Mesh -> IO Mesh -> IO ()
-benchmarkPublishedSetOperation label operation expectedWitness = do
-  result <- benchmarkValidatedSetOperation label operation
-  observedCanonical <-
-    timedValue
-      (label <> "-explicit-canonicalize")
-      (evaluate . force =<< requireRight (canonicalize result))
-  expected <- expectedWitness
-  expectedCanonical <- evaluate . force =<< requireRight (canonicalize expected)
-  unless (observedCanonical == expectedCanonical) $
-    fail (label <> " disagreed with the independently rebuilt witness")
-
-benchmarkValidatedSetOperation :: String -> Either BuildError Mesh -> IO Mesh
-benchmarkValidatedSetOperation label operation = do
-  result <- timedValue label (evaluate . force =<< requireRight operation)
-  case validateTriangulation result of
-    [] -> pure ()
-    violations -> fail (label <> " invalid: " <> show violations)
-  pure result
-
--- | The same operand sizes at three overlap fractions. A join is sized by the
--- union, so wholly overlapping operands must cost what one of them costs.
-benchmarkOverlap :: Int -> IO ()
-benchmarkOverlap total = do
-  let sites = randomPoints 0x2545f4914f6cdd1d total
-      half = total `div` 2
-  disjointLeft <- geometryMesh (take half sites)
-  disjointRight <- geometryMesh (drop half sites)
-  halfLeft <- geometryMesh (take half sites)
-  halfRight <- geometryMesh (drop (half `div` 2) (take (half + half `div` 2) sites))
-  sameLeft <- geometryMesh (take half sites)
-  sameRight <- geometryMesh (reverse (take half sites))
-  _ <- evaluate (force (disjointLeft, disjointRight, halfLeft, halfRight, sameLeft, sameRight))
-  _ <- timedValue "join-overlap-000" (requireRight (union disjointLeft disjointRight))
-  _ <- timedValue "join-overlap-050" (requireRight (union halfLeft halfRight))
-  _ <- timedValue "join-overlap-100" (requireRight (union sameLeft sameRight))
-  pure ()
-
--- | 'unions' is a balanced tournament and not a fold, which is a cost claim
--- and therefore has to be measured rather than asserted. A fold republishes an
--- accumulator that grows by one shard per step.
---
--- This lane once reported the fold as the faster of the two, which was true and
--- was not a fact about the schedules: @joinBalanced@ carried no specialization,
--- so every join inside the tournament ran through a dictionary while
--- the left-associated schedule at a known element type ran specialized. The tournament was
--- paying twice for arithmetic, and that swamped the asymptotic gap it was
--- supposed to be demonstrating.
---
--- The shards are dealt round-robin, so every one of them spans the whole extent
--- and no join in the tournament is separable. That is deliberate: it is the
--- adversarial sharding, and it measures the operator with no structure to
--- exploit. 'benchmarkSpatialTournament' is the same tournament over the
--- sharding a caller who wanted it to be fast would actually choose.
-benchmarkTournament :: Int -> Int -> IO ()
-benchmarkTournament total shardCount = do
-  let sites = randomPoints 0x14057b7ef767814f total
-      indexed = zip [0 :: Int ..] sites
-  shards <-
-    traverse
-      (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])
-      [0 .. shardCount - 1]
-  _ <- evaluate (force shards)
-  _ <- timedValue "join-tournament" (requireRight (unions shards))
-  _ <- timedValue "join-left-fold" (requireRight (unionsLeftAssociated shards))
-  pure ()
-
--- | Where the tournament's advantage over the fold actually appears.
---
--- A fold republishes an accumulator that grows by one shard per step, so it
--- rebuilds @Θ(nk)@ sites over @k@ shards where halving rebuilds @Θ(n log k)@.
--- That is a statement about @k@, and at the sixteen shards the lane above uses
--- the predicted factor is barely two — small enough to be swamped by the
--- per-join costs both schedules pay fifteen times each. This sweep is here
--- because a cost claim that only holds asymptotically has to say at what size
--- it starts holding, and the answer has to be measured rather than asserted.
-benchmarkTournamentScaling :: Int -> IO ()
-benchmarkTournamentScaling total =
-  mapM_
-    ( \shardCount -> do
-        let sites = randomPoints 0x9e3779b97f4a7c15 total
-            indexed = zip [0 :: Int ..] sites
-        shards <-
-          traverse
-            (\shard -> geometryMesh [site | (index, site) <- indexed, index `mod` shardCount == shard])
-            [0 .. shardCount - 1]
-        _ <- evaluate (force shards)
-        _ <- timedValue ("join-shards-" <> show shardCount <> "-tournament") (requireRight (unions shards))
-        _ <- timedValue ("join-shards-" <> show shardCount <> "-fold") (requireRight (unionsLeftAssociated shards))
-        pure ()
-    )
-    [4 :: Int, 16, 64]
-
-
--- | The same tournament over shards cut by abscissa rather than dealt.
---
--- This is the workload a seam schedule exists for, and the only one where it
--- can pay off more than once. Shards cut into contiguous x-ranges are pairwise
--- separated; so is every intermediate result, because the union of two adjacent
--- ranges is a range. Every one of the fifteen joins in the tournament is
--- therefore separable — the tournament /is/ the divide-and-conquer recursion,
--- entered from the leaves.
---
--- Against the reference schedule this must cost about what the dealt
--- tournament costs, since a rebuild cannot tell the two shardings apart. That
--- agreement is the baseline; the gap that opens between these two lanes is the
--- whole return on a merge kernel.
-benchmarkSpatialTournament :: Int -> Int -> IO ()
-benchmarkSpatialTournament total shardCount = do
-  let sites = randomPoints 0x3c6ef372fe94f82a total
-      ordered = sortBy (comparing (\(Point x _) -> x)) sites
-      width = (total + shardCount - 1) `div` shardCount
-  shards <-
-    traverse
-      (\shard -> geometryMesh (take width (drop (shard * width) ordered)))
-      [0 .. shardCount - 1]
-  _ <- evaluate (force shards)
-  _ <- timedValue "join-spatial-tournament" (requireRight (unions shards))
-  pure ()
-
--- | The renumbering pass on its own, against the construction it follows.
-benchmarkCanonicalize :: Int -> IO ()
-benchmarkCanonicalize total = do
-  let sites = randomPoints 0x27d4eb2f165667c5 total
-  mesh <- geometryMesh sites
-  _ <- evaluate (force mesh)
-  _ <- timedValue "canonicalize-alone" (evaluate . force =<< requireRight (canonicalize mesh))
-  pure ()
-
--- | The shared canonical rebuild boundary under half overlap, on both its
--- geometry-only specializations and its annotation-preserving surface. Setup
--- and source publication are forced before every clock; the measurements are
--- therefore the exact site classification, rebuild, canonical publication and
--- payload transport the public operations own.
-benchmarkSetAlgebra :: Int -> IO ()
-benchmarkSetAlgebra total = do
-  let common = total `quot` 2
-      sites = randomPoints 0x6A09E667F3BCC909 (total + common)
-      leftPoints = take total sites
-      rightPoints = drop common sites
-  leftGeometry <- geometryMesh leftPoints
-  rightGeometry <- geometryMesh rightPoints
-  emptyGeometry <- requireRight (unions [])
-  _ <- evaluate (force (leftGeometry, rightGeometry, emptyGeometry))
-  _ <- timedValue "set-intersection-unit" (requireRight (intersection leftGeometry rightGeometry))
-  _ <- timedValue "set-difference-unit" (requireRight (difference leftGeometry rightGeometry))
-  _ <-
-    timedValue
-      "set-symmetric-difference-unit"
-      (requireRight (symmetricDifference leftGeometry rightGeometry))
-  _ <-
-    timedValue
-      "set-difference-right-empty"
-      (requireRight (difference leftGeometry emptyGeometry))
-  _ <-
-    timedValue
-      "set-symmetric-difference-left-empty"
-      (requireRight (symmetricDifference emptyGeometry leftGeometry))
-  _ <- timedValue "set-relation-half-overlap" (evaluate (siteRelation leftGeometry rightGeometry))
-  leftAnnotated <- siteMesh leftPoints
-  rightAnnotated <- siteMesh rightPoints
-  _ <- evaluate (force (leftAnnotated, rightAnnotated))
-  _ <-
-    timedValue
-      "set-intersection-annotated"
-      (requireRight (intersectionWith (,) leftAnnotated rightAnnotated))
-  _ <-
-    timedValue
-      "set-difference-annotated"
-      (requireRight (difference leftAnnotated rightAnnotated))
-  _ <-
-    timedValue
-      "set-symmetric-difference-annotated"
-      (requireRight (symmetricDifference leftAnnotated rightAnnotated))
-  pure ()
-
-
-canonical :: [Point] -> [Point]
-canonical points = [Point x y | (x, y) <- dropAdjacentDuplicates (sort [(x, y) | Point x y <- points])]
-
-dropAdjacentDuplicates :: Eq a => [a] -> [a]
-dropAdjacentDuplicates (first : second : rest)
-  | first == second = dropAdjacentDuplicates (second : rest)
-  | otherwise = first : dropAdjacentDuplicates (second : rest)
-dropAdjacentDuplicates rest = rest
-
-geometryMesh :: [Point] -> IO Mesh
-geometryMesh points = requireRight (delaunayGeometry (V.fromList points))
-
-siteMesh :: [Point] -> IO SiteMesh
-siteMesh points =
-  buildTriangulation <$> requireRight (delaunay unitElementDefaults (V.fromList points))
-
-unionsLeftAssociated :: [Mesh] -> Either BuildError Mesh
-unionsLeftAssociated meshes = unions [] >>= \identity -> foldM union identity meshes
diff --git a/bench/layer/Main.hs b/bench/layer/Main.hs
--- a/bench/layer/Main.hs
+++ b/bench/layer/Main.hs
@@ -1,8 +1,10 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.LayerBench as LayerBench
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import qualified Moonlight.Planar.LayerBench as LayerBench
 
 main :: IO ()
 main = do
-  fixtures <- LayerBench.prepareFixtures
+  fixtures <- LayerBench.prepareFixtures >>= evaluate . force
   LayerBench.runBenchmarks fixtures
diff --git a/bench/layer/Moonlight/Planar/LayerBench.hs b/bench/layer/Moonlight/Planar/LayerBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/layer/Moonlight/Planar/LayerBench.hs
@@ -0,0 +1,277 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+-- | Timing and allocation evidence for exact affine envelopes and layer
+-- composition. Manual lanes use only the pre-existing public operations a
+-- caller otherwise has to compose and discard.
+module Moonlight.Planar.LayerBench
+  ( LayerBenchFixtures
+  , prepareFixtures
+  , runBenchmarks
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad (foldM)
+import Data.Bifunctor (first)
+import Data.Ratio (numerator, denominator)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact (ExactPoint, exactPoint, exactRational)
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import Moonlight.Planar.Overlay (OverlayResult, layerCovers, overlayAll, overlayConfusion, overlayLayers, overlayMass, overlayPlanarLayer, overlaySelectedRegion, foldBoundedOverlayCells)
+import Moonlight.Planar.PowerDiagram (AffineForm (..), upperEnvelope)
+import Moonlight.Planar.Region (PlanarLayer, PlanarRegion, PolygonComponent, exactLoop, planarLayer, planarLayerOutsideLabel, planarLayerRegions, planarRegion, planarRegionComponents, polygonComponent)
+import Moonlight.Planar.Valuation (ExactArea, exactAreaValue, regionValuations, valuationArea, ExactPlanarMoments, orientedBoundaryMoments, polygonComponentMoments)
+import Test.Tasty.Bench (bench, bgroup, defaultMain, nf)
+
+data LayerBenchFixtures = LayerBenchFixtures
+  { envelopeWindow :: !PolygonComponent
+  , envelopeForms16 :: !(Map.Map Int AffineForm)
+  , envelopeForms64 :: !(Map.Map Int AffineForm)
+  , envelopeForms169 :: !(Map.Map Int AffineForm)
+  , overlayLayers8 :: !(NonEmpty (PlanarLayer Bool))
+  , overlayLayers16 :: !(NonEmpty (PlanarLayer Bool))
+  , measuredOverlay :: !(OverlayResult (Bool, Bool))
+  , coverageLayer :: !(PlanarLayer Bool)
+  , momentBoundaries :: ![(String, [(ExactPoint, ExactPoint)])]
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+prepareFixtures :: IO LayerBenchFixtures
+prepareFixtures = do
+  window <- rectangleComponent (-20) (-20) 20 20
+  layers8 <- prepareLayerFamily 8
+  layers16 <- prepareLayerFamily 16
+  left <- rectangleLayer 0 0 60 100
+  right <- rectangleLayer 40 0 100 100
+  massOverlay <- requireRight "mass benchmark overlay" (overlayLayers left right)
+  windowRegion <- requireRight "coverage benchmark region" (planarRegion [window])
+  completeCoverage <-
+    requireRight
+      "coverage benchmark layer"
+      (planarLayer False (Map.singleton True windowRegion))
+  boundaries <- traverse (traverse (traverse admitMomentEdge)) rationalMomentBoundaries
+  pure
+    LayerBenchFixtures
+      { envelopeWindow = window
+      , envelopeForms16 = affineForms 16
+      , envelopeForms64 = affineForms 64
+      , envelopeForms169 = affineForms 169
+      , overlayLayers8 = layers8
+      , overlayLayers16 = layers16
+      , measuredOverlay = massOverlay
+      , coverageLayer = completeCoverage
+      , momentBoundaries = boundaries
+      }
+
+runBenchmarks :: LayerBenchFixtures -> IO ()
+runBenchmarks fixtures =
+  defaultMain
+    [ bgroup
+        "exact affine upper envelope"
+        [ bench "n=16" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms16 fixtures))
+        , bench "n=64" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms64 fixtures))
+        , bench "n=169" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms169 fixtures))
+        ]
+    , bgroup
+        "n-ary overlay"
+        [ bench "fused/publish/n=8" (nf fusedPublishedOverlay (overlayLayers8 fixtures))
+        , bench "left-associated/publish/n=8" (nf leftAssociatedOverlay (overlayLayers8 fixtures))
+        , bench "fused/publish/n=16" (nf fusedPublishedOverlay (overlayLayers16 fixtures))
+        , bench "left-associated/publish/n=16" (nf leftAssociatedOverlay (overlayLayers16 fixtures))
+        ]
+    , bgroup "oriented edge moments"
+        [ bench label (nf orientedBoundaryMoments boundary)
+        | (label, boundary) <- momentBoundaries fixtures
+        ]
+    , bgroup
+        "overlay analytics"
+        [ bench "mass/direct" (nf directMass (measuredOverlay fixtures))
+        , bench "mass/publish-and-value" (nf publishedMass (measuredOverlay fixtures))
+        , bench "confusion/direct" (nf directConfusion (measuredOverlay fixtures))
+        , bench "confusion/repeated-publication" (nf publishedConfusion (measuredOverlay fixtures))
+        , bench "moments/direct" (nf directMoments (measuredOverlay fixtures))
+        , bench "moments/publish-and-value" (nf publishedMoments (measuredOverlay fixtures))
+        , bench
+            "coverage/complete"
+            (nf (coverageResult (coverageLayer fixtures)) (envelopeWindow fixtures))
+        ]
+    ]
+
+fusedPublishedOverlay
+  :: NonEmpty (PlanarLayer Bool)
+  -> Either String (PlanarLayer (NonEmpty Bool))
+fusedPublishedOverlay sources = do
+  result <- first show (overlayAll sources)
+  first show (overlayPlanarLayer result)
+
+leftAssociatedOverlay
+  :: NonEmpty (PlanarLayer Bool)
+  -> Either String (PlanarLayer (NonEmpty Bool))
+leftAssociatedOverlay (firstLayer :| remainingLayers) = do
+  initial <- singletonLabelLayer firstLayer
+  liftedRemaining <- traverse singletonLabelLayer remainingLayers
+  foldM overlayNext initial liftedRemaining
+ where
+  overlayNext left right = do
+    result <- first show (overlayLayers left right)
+    published <- first show (overlayPlanarLayer result)
+    flattenPublicLayer published
+
+singletonLabelLayer
+  :: PlanarLayer Bool
+  -> Either String (PlanarLayer (NonEmpty Bool))
+singletonLabelLayer layer =
+  first show
+    ( planarLayer
+        (planarLayerOutsideLabel layer :| [])
+        ( Map.fromList
+            [ (label :| [], region)
+            | (label, region) <- Map.toAscList (planarLayerRegions layer)
+            ]
+        )
+    )
+
+flattenPublicLayer
+  :: PlanarLayer (NonEmpty Bool, NonEmpty Bool)
+  -> Either String (PlanarLayer (NonEmpty Bool))
+flattenPublicLayer layer =
+  first show
+    ( planarLayer
+        (uncurry (<>) (planarLayerOutsideLabel layer))
+        ( Map.fromList
+            [ (leftLabels <> rightLabels, region)
+            | ((leftLabels, rightLabels), region) <- Map.toAscList (planarLayerRegions layer)
+            ]
+        )
+    )
+
+directMass :: OverlayResult (Bool, Bool) -> Either String ExactArea
+directMass = first show . overlayMass (== (True, True))
+
+publishedMass :: OverlayResult (Bool, Bool) -> Either String ExactArea
+publishedMass result = do
+  region <- first show (overlaySelectedRegion (== (True, True)) result)
+  valuationArea <$> first show (regionValuations region)
+
+directConfusion :: OverlayResult (Bool, Bool) -> Either String (Map.Map (Bool, Bool) ExactRational)
+directConfusion = fmap (fmap exactAreaValue) . first show . overlayConfusion
+
+publishedConfusion
+  :: OverlayResult (Bool, Bool)
+  -> Either String (Map.Map (Bool, Bool) ExactRational)
+publishedConfusion result =
+  Map.fromList
+    <$> traverse
+      (\labels -> (labels,) . exactAreaValue <$> publishedPairMass labels)
+      [(False, True), (True, False), (True, True)]
+ where
+  publishedPairMass labels = do
+    region <- first show (overlaySelectedRegion (== labels) result)
+    valuationArea <$> first show (regionValuations region)
+
+directMoments :: OverlayResult (Bool, Bool) -> Either String ExactPlanarMoments
+directMoments = first show . foldBoundedOverlayCells
+  (\moments labels boundary -> if labels == (True, True) then moments <> orientedBoundaryMoments boundary else moments)
+  mempty
+
+publishedMoments :: OverlayResult (Bool, Bool) -> Either String ExactPlanarMoments
+publishedMoments result = do
+  region <- first show (overlaySelectedRegion (== (True, True)) result)
+  pure (foldMap polygonComponentMoments (planarRegionComponents region))
+
+coverageResult
+  :: PlanarLayer Bool
+  -> PolygonComponent
+  -> Either String ()
+coverageResult layer = first show . layerCovers layer
+
+prepareLayerFamily :: Int -> IO (NonEmpty (PlanarLayer Bool))
+prepareLayerFamily count = do
+  layers <- traverse prepareLayer [0 .. count - 1]
+  case layers of
+    firstLayer : remaining -> pure (firstLayer :| remaining)
+    [] -> fail "layer benchmark requires a positive family size"
+ where
+  prepareLayer :: Int -> IO (PlanarLayer Bool)
+  prepareLayer index =
+    rectangleLayer
+      (-64 + fromIntegral ((7 * index) `mod` 31))
+      (-64 + fromIntegral ((11 * index) `mod` 29))
+      (32 + fromIntegral ((13 * index) `mod` 31))
+      (32 + fromIntegral ((17 * index) `mod` 29))
+
+rectangleLayer :: Integer -> Integer -> Integer -> Integer -> IO (PlanarLayer Bool)
+rectangleLayer minimumX minimumY maximumX maximumY = do
+  region <- rectangleRegion minimumX minimumY maximumX maximumY
+  requireRight "benchmark rectangle layer" (planarLayer False (Map.singleton True region))
+
+rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
+rectangleRegion minimumX minimumY maximumX maximumY = do
+  component <- rectangleComponent minimumX minimumY maximumX maximumY
+  requireRight "benchmark rectangle region" (planarRegion [component])
+
+rectangleComponent :: Integer -> Integer -> Integer -> Integer -> IO PolygonComponent
+rectangleComponent minimumX minimumY maximumX maximumY = do
+  loop <-
+    requireRight
+      "benchmark rectangle loop"
+      ( exactLoop
+          ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
+              :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
+                 , exactPoint (fromInteger maximumX) (fromInteger maximumY)
+                 , exactPoint (fromInteger minimumX) (fromInteger maximumY)
+                 ]
+          )
+      )
+  requireRight "benchmark rectangle component" (polygonComponent loop [])
+
+affineForms :: Int -> Map.Map Int AffineForm
+affineForms count =
+  Map.fromAscList
+    [ (index, tangentForm index)
+    | index <- [0 .. count - 1]
+    ]
+ where
+  tangentForm :: Int -> AffineForm
+  tangentForm index =
+    let coordinateX = fromIntegral (index `mod` 17 - 8)
+        coordinateY = fromIntegral (index `div` 17 - 5)
+     in AffineForm
+          (negate (coordinateX * coordinateX + coordinateY * coordinateY))
+          (2 * coordinateX)
+          (2 * coordinateY)
+
+requireRight :: Show failure => String -> Either failure value -> IO value
+requireRight label = either (fail . ((label <> ": ") <>) . show) pure
+
+-- Identical fixture covers are prepared before both copied benchmark binaries.
+-- These lanes time the existing public six-moment kernel, not input admission.
+-- The independent Rational formula oracle lives in the existing ValuationSpec.
+rationalMomentBoundaries :: [(String, [((Rational, Rational), (Rational, Rational))])]
+rationalMomentBoundaries =
+  [ ("integer/n=64", [((n, 2*n+1), (n+3, 1-n)) | n <- [-32 .. 31]])
+  , ("common-denominator/n=64", [((n/64, (2*n+1)/64), ((n+3)/64, (1-n)/64)) | n <- [-32 .. 31]])
+  , ("mixed-coprime/n=64", [((n/101, (2*n+1)/103), ((n+3)/107, (1-n)/109)) | n <- [-32 .. 31]])
+  , ("high-bitwidth/n=64", [(((huge+n)/p, (huge-n)/q), ((huge+2*n+1)/r, (n-huge)/s)) | n <- [-32 .. 31]])
+  , ("zero-cross/n=64", [((n/101, 2*n/101), ((n+3)/107, 2*(n+3)/107)) | n <- [-32 .. 31]])
+  ]
+ where
+  huge = fromInteger (2 ^ (191 :: Int))
+  p = fromInteger (2 ^ (127 :: Int) - 1)
+  q = fromInteger (2 ^ (107 :: Int) - 1)
+  r = fromInteger (2 ^ (89 :: Int) - 1)
+  s = fromInteger (2 ^ (61 :: Int) - 1)
+
+admitMomentEdge :: ((Rational, Rational), (Rational, Rational)) -> IO (ExactPoint, ExactPoint)
+admitMomentEdge (from, to) = (,) <$> admitMomentPoint from <*> admitMomentPoint to
+
+admitMomentPoint :: (Rational, Rational) -> IO ExactPoint
+admitMomentPoint (x, y) =
+  exactPoint <$> admit x <*> admit y
+ where
+  admit :: Rational -> IO ExactRational
+  admit value = requireRight "moment benchmark coordinate"
+    (exactRational (numerator value) (denominator value))
diff --git a/bench/layer/Moonlight/Triangulation/LayerBench.hs b/bench/layer/Moonlight/Triangulation/LayerBench.hs
deleted file mode 100644
--- a/bench/layer/Moonlight/Triangulation/LayerBench.hs
+++ /dev/null
@@ -1,244 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-
--- | Timing and allocation evidence for exact affine envelopes and layer
--- composition. Manual lanes use only the pre-existing public operations a
--- caller otherwise has to compose and discard.
-module Moonlight.Triangulation.LayerBench
-  ( LayerBenchFixtures
-  , prepareFixtures
-  , runBenchmarks
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad (foldM)
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import GHC.Generics (Generic)
-import Moonlight.Triangulation
-  ( AffineForm (..)
-  , ExactArea
-  , ExactRational
-  , OverlayResult
-  , PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , exactAreaValue
-  , exactLoop
-  , exactPoint
-  , layerCovers
-  , overlayAll
-  , overlayConfusion
-  , overlayLayers
-  , overlayMass
-  , overlayPlanarLayer
-  , overlaySelectedRegion
-  , planarLayer
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarRegion
-  , polygonComponent
-  , regionValuations
-  , upperEnvelope
-  , valuationArea
-  )
-import Test.Tasty.Bench (bench, bgroup, defaultMain, nf)
-
-data LayerBenchFixtures = LayerBenchFixtures
-  { envelopeWindow :: !PolygonComponent
-  , envelopeForms16 :: !(Map.Map Int AffineForm)
-  , envelopeForms64 :: !(Map.Map Int AffineForm)
-  , envelopeForms169 :: !(Map.Map Int AffineForm)
-  , overlayLayers8 :: !(NonEmpty (PlanarLayer Bool))
-  , overlayLayers16 :: !(NonEmpty (PlanarLayer Bool))
-  , measuredOverlay :: !(OverlayResult Bool Bool)
-  , coverageLayer :: !(PlanarLayer Bool)
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-prepareFixtures :: IO LayerBenchFixtures
-prepareFixtures = do
-  window <- rectangleComponent (-20) (-20) 20 20
-  layers8 <- prepareLayerFamily 8
-  layers16 <- prepareLayerFamily 16
-  left <- rectangleLayer 0 0 60 100
-  right <- rectangleLayer 40 0 100 100
-  massOverlay <- requireRight "mass benchmark overlay" (overlayLayers left right)
-  windowRegion <- requireRight "coverage benchmark region" (planarRegion [window])
-  completeCoverage <-
-    requireRight
-      "coverage benchmark layer"
-      (planarLayer False (Map.singleton True windowRegion))
-  pure
-    LayerBenchFixtures
-      { envelopeWindow = window
-      , envelopeForms16 = affineForms 16
-      , envelopeForms64 = affineForms 64
-      , envelopeForms169 = affineForms 169
-      , overlayLayers8 = layers8
-      , overlayLayers16 = layers16
-      , measuredOverlay = massOverlay
-      , coverageLayer = completeCoverage
-      }
-
-runBenchmarks :: LayerBenchFixtures -> IO ()
-runBenchmarks fixtures =
-  defaultMain
-    [ bgroup
-        "exact affine upper envelope"
-        [ bench "n=16" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms16 fixtures))
-        , bench "n=64" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms64 fixtures))
-        , bench "n=169" (nf (upperEnvelope (envelopeWindow fixtures)) (envelopeForms169 fixtures))
-        ]
-    , bgroup
-        "n-ary overlay"
-        [ bench "balanced/n=8" (nf balancedOverlay (overlayLayers8 fixtures))
-        , bench "left-associated/n=8" (nf leftAssociatedOverlay (overlayLayers8 fixtures))
-        , bench "balanced/n=16" (nf balancedOverlay (overlayLayers16 fixtures))
-        , bench "left-associated/n=16" (nf leftAssociatedOverlay (overlayLayers16 fixtures))
-        ]
-    , bgroup
-        "overlay analytics"
-        [ bench "mass/direct" (nf directMass (measuredOverlay fixtures))
-        , bench "mass/publish-and-value" (nf publishedMass (measuredOverlay fixtures))
-        , bench "confusion/direct" (nf directConfusion (measuredOverlay fixtures))
-        , bench "confusion/repeated-publication" (nf publishedConfusion (measuredOverlay fixtures))
-        , bench
-            "coverage/complete"
-            (nf (coverageResult (coverageLayer fixtures)) (envelopeWindow fixtures))
-        ]
-    ]
-
-balancedOverlay
-  :: NonEmpty (PlanarLayer Bool)
-  -> Either String (PlanarLayer (NonEmpty Bool))
-balancedOverlay = first show . overlayAll
-
-leftAssociatedOverlay
-  :: NonEmpty (PlanarLayer Bool)
-  -> Either String (PlanarLayer (NonEmpty Bool))
-leftAssociatedOverlay (firstLayer :| remainingLayers) = do
-  initial <- singletonLabelLayer firstLayer
-  liftedRemaining <- traverse singletonLabelLayer remainingLayers
-  foldM overlayNext initial liftedRemaining
- where
-  overlayNext left right = do
-    result <- first show (overlayLayers left right)
-    flattenPublicLayer (overlayPlanarLayer result)
-
-singletonLabelLayer
-  :: PlanarLayer Bool
-  -> Either String (PlanarLayer (NonEmpty Bool))
-singletonLabelLayer layer =
-  first show
-    ( planarLayer
-        (planarLayerOutsideLabel layer :| [])
-        ( Map.fromList
-            [ (label :| [], region)
-            | (label, region) <- Map.toAscList (planarLayerRegions layer)
-            ]
-        )
-    )
-
-flattenPublicLayer
-  :: PlanarLayer (NonEmpty Bool, NonEmpty Bool)
-  -> Either String (PlanarLayer (NonEmpty Bool))
-flattenPublicLayer layer =
-  first show
-    ( planarLayer
-        (uncurry (<>) (planarLayerOutsideLabel layer))
-        ( Map.fromList
-            [ (leftLabels <> rightLabels, region)
-            | ((leftLabels, rightLabels), region) <- Map.toAscList (planarLayerRegions layer)
-            ]
-        )
-    )
-
-directMass :: OverlayResult Bool Bool -> Either String ExactArea
-directMass = first show . overlayMass (== (True, True))
-
-publishedMass :: OverlayResult Bool Bool -> Either String ExactArea
-publishedMass result = do
-  region <- first show (overlaySelectedRegion (== (True, True)) result)
-  valuationArea <$> first show (regionValuations region)
-
-directConfusion :: OverlayResult Bool Bool -> Map.Map (Bool, Bool) ExactRational
-directConfusion = fmap exactAreaValue . overlayConfusion
-
-publishedConfusion
-  :: OverlayResult Bool Bool
-  -> Either String (Map.Map (Bool, Bool) ExactRational)
-publishedConfusion result =
-  Map.fromList
-    <$> traverse
-      (\labels -> (labels,) . exactAreaValue <$> publishedPairMass labels)
-      [(False, True), (True, False), (True, True)]
- where
-  publishedPairMass labels = do
-    region <- first show (overlaySelectedRegion (== labels) result)
-    valuationArea <$> first show (regionValuations region)
-
-coverageResult
-  :: PlanarLayer Bool
-  -> PolygonComponent
-  -> Either String ()
-coverageResult layer = first show . layerCovers layer
-
-prepareLayerFamily :: Int -> IO (NonEmpty (PlanarLayer Bool))
-prepareLayerFamily count = do
-  layers <- traverse prepareLayer [0 .. count - 1]
-  case layers of
-    firstLayer : remaining -> pure (firstLayer :| remaining)
-    [] -> fail "layer benchmark requires a positive family size"
- where
-  prepareLayer index =
-    rectangleLayer
-      (-64 + fromIntegral ((7 * index) `mod` 31))
-      (-64 + fromIntegral ((11 * index) `mod` 29))
-      (32 + fromIntegral ((13 * index) `mod` 31))
-      (32 + fromIntegral ((17 * index) `mod` 29))
-
-rectangleLayer :: Integer -> Integer -> Integer -> Integer -> IO (PlanarLayer Bool)
-rectangleLayer minimumX minimumY maximumX maximumY = do
-  region <- rectangleRegion minimumX minimumY maximumX maximumY
-  requireRight "benchmark rectangle layer" (planarLayer False (Map.singleton True region))
-
-rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
-rectangleRegion minimumX minimumY maximumX maximumY = do
-  component <- rectangleComponent minimumX minimumY maximumX maximumY
-  requireRight "benchmark rectangle region" (planarRegion [component])
-
-rectangleComponent :: Integer -> Integer -> Integer -> Integer -> IO PolygonComponent
-rectangleComponent minimumX minimumY maximumX maximumY = do
-  loop <-
-    requireRight
-      "benchmark rectangle loop"
-      ( exactLoop
-          ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
-              :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
-                 , exactPoint (fromInteger maximumX) (fromInteger maximumY)
-                 , exactPoint (fromInteger minimumX) (fromInteger maximumY)
-                 ]
-          )
-      )
-  requireRight "benchmark rectangle component" (polygonComponent loop [])
-
-affineForms :: Int -> Map.Map Int AffineForm
-affineForms count =
-  Map.fromAscList
-    [ (index, tangentForm index)
-    | index <- [0 .. count - 1]
-    ]
- where
-  tangentForm index =
-    let coordinateX = fromIntegral (index `mod` 17 - 8)
-        coordinateY = fromIntegral (index `div` 17 - 5)
-     in AffineForm
-          (negate (coordinateX * coordinateX + coordinateY * coordinateY))
-          (2 * coordinateX)
-          (2 * coordinateY)
-
-requireRight :: Show failure => String -> Either failure value -> IO value
-requireRight label = either (fail . ((label <> ": ") <>) . show) pure
diff --git a/bench/lifted-sign/Main.hs b/bench/lifted-sign/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/lifted-sign/Main.hs
@@ -0,0 +1,37 @@
+module Main (main) where
+
+import BenchMeasure (requireRight, timedValue)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import Moonlight.Planar.LiftedSignCases
+import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)
+
+main :: IO ()
+main = do
+  cases <- requireRight liftedSignCases >>= evaluate . force
+  traverse_ reportFamily cases
+  defaultMain (map familyBenchmarks cases)
+
+familyBenchmarks :: (LiftedSignFamily, [LiftedSignInput]) -> Benchmark
+familyBenchmarks (family, inputs) =
+  bgroup
+    (show family)
+    [ bench "normalized-rational-magnitude-sign" (nf magnitudeSigns inputs)
+    , bench "positive-row-scaled-integer-sign" (nf candidateSigns inputs)
+    ]
+
+reportFamily :: (LiftedSignFamily, [LiftedSignInput]) -> IO ()
+reportFamily (family, inputs) = do
+  putStrLn (show family <> "-predicates: " <> show (length inputs))
+  _ <- timedValue (show family <> "-magnitude-sign") (evaluate (magnitudeSigns inputs))
+  _ <- timedValue (show family <> "-integer-sign") (evaluate (candidateSigns inputs))
+  pure ()
+
+magnitudeSigns :: [LiftedSignInput] -> [Ordering]
+magnitudeSigns = map magnitudeSign
+{-# NOINLINE magnitudeSigns #-}
+
+candidateSigns :: [LiftedSignInput] -> [Ordering]
+candidateSigns = map candidateSign
+{-# NOINLINE candidateSigns #-}
diff --git a/bench/power/Main.hs b/bench/power/Main.hs
--- a/bench/power/Main.hs
+++ b/bench/power/Main.hs
@@ -1,9 +1,17 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.PowerBench as PowerBench
+import qualified Moonlight.Planar.PowerBench as PowerBench
+import qualified Moonlight.Planar.PowerMassBench as PowerMassBench
+import System.Environment (getArgs, withArgs)
+import Test.Tasty.Bench (defaultMain)
 
 main :: IO ()
 main = do
-  cases <- PowerBench.prepareCases
-  PowerBench.reportCases cases
-  PowerBench.runBenchmarks cases
+  arguments <- getArgs
+  case arguments of
+    "--mass-only" : remaining ->
+      withArgs remaining (PowerMassBench.prepareBenchmarks >>= defaultMain)
+    _ -> do
+      cases <- PowerBench.prepareCases
+      PowerBench.reportCases cases
+      PowerBench.runBenchmarks cases
diff --git a/bench/power/Moonlight/Planar/PowerBench.hs b/bench/power/Moonlight/Planar/PowerBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/power/Moonlight/Planar/PowerBench.hs
@@ -0,0 +1,487 @@
+-- | Tasty-bench lanes for the exact bounded power-diagram owner.  The timed
+-- lanes force the public result, while the one-shot report keeps the
+-- structural receipt and a shared incremental allocation observation outside
+-- the timing sample.  One construction is revealed, its diagram is forced,
+-- and only then is the additional receipt-reachable section measured.
+module Moonlight.Planar.PowerBench
+  ( PowerBenchCase
+  , prepareCases
+  , reportCases
+  , runBenchmarks
+  ) where
+
+import BenchMeasure (requireRight)
+import BenchSupport (randomPoints)
+import qualified Moonlight.Planar.PowerMassBench as PowerMassBench
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as Vector
+import GHC.Conc.Sync (getAllocationCounter, setAllocationCounter)
+import Moonlight.Planar.Convex (ConvexPolygon, convexPolygon)
+import Moonlight.Planar.Exact (exactPoint)
+import Moonlight.Planar.PowerDiagram (BoundedPowerDiagram, PowerDiagramError, PowerDiagramReceipt, PowerSite, PowerWeight, RegularEditError, RegularEditResult, RegularTriangulation, RegularTriangulationReceipt, boundedPowerDiagram, boundedPowerDiagramFromRegular, powerDiagramActiveBoundaries, powerDiagramCoincidentDominatedCells, powerDiagramCoincidentEquivalentCells, powerDiagramDomainVertices, powerDiagramBoundaryCompatibilityChecks, powerDiagramEmptyCells, powerDiagramExactIntersections, powerDiagramFinalCoordinateBitGrowth, powerDiagramFinalCoordinateBits, powerDiagramFinalDenominatorBits, powerDiagramInputSites, powerDiagramLowerDimensionalCells, powerDiagramMaximumAffineCoefficientBits, powerDiagramMaximumInputBits, powerDiagramPeakIntermediateBitGrowth, powerDiagramPeakIntermediateCoordinateBits, powerDiagramMaximumCellConstraints, powerDiagramPublishedCells, powerDiagramOracleCells, powerDiagramRegularEdges, powerDiagramRegularFaces, powerDiagramSubmittedSiteConstraints, powerSite, powerSiteLabel, powerSitePosition, powerSiteWeight, powerWeight, powerWeightExact, powerWeightFromExact, insertRegularSite, removeRegularSite, reweightRegularSites, regularTriangulation, regularEditChangedSites, regularEditTriangulation, regularEditTransitions, regularSiteDisposition, regularSites, RegularSiteDisposition (RegularSiteHidden))
+import Moonlight.Planar.Point (Point (Point))
+import System.Mem (performGC)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)
+
+data PowerBenchFamily
+  = WellConditioned
+  | NearParallel
+  | CollinearSlopes
+  deriving stock (Eq, Ord, Show)
+
+data PowerBenchCase = PowerBenchCase
+  { powerBenchLabel :: !String
+  , powerBenchDomain :: !ConvexPolygon
+  , powerBenchSites :: !(NonEmpty (PowerSite Int))
+  , powerBenchRegular :: !(RegularTriangulation Int)
+  , powerBenchInsertionBase :: !(RegularTriangulation Int)
+  , powerBenchInsertedSite :: !(PowerSite Int)
+  , powerBenchReweight :: !PowerWeight
+  , powerBenchCommonShift :: !(Map.Map Int PowerWeight)
+  , powerBenchCoincidentLabel :: !Int
+  , powerBenchCoincidentRegular :: !(RegularTriangulation Int)
+  , powerBenchCoincidentReweight :: !PowerWeight
+  , powerBenchHiddenEdit :: !(Maybe HiddenEditFixture)
+  }
+
+data HiddenEditFixture = HiddenEditFixture
+  { hiddenEditLabel :: !Int
+  , hiddenEditLowerWeight :: !PowerWeight
+  }
+
+type PowerResult =
+  Either (PowerDiagramError Int) (BoundedPowerDiagram Int, PowerDiagramReceipt)
+
+type DiagramResult =
+  Either (PowerDiagramError Int) (BoundedPowerDiagram Int)
+
+type RegularResult =
+  Either
+    (PowerDiagramError Int)
+    (RegularTriangulation Int, RegularTriangulationReceipt)
+
+type RegularEditBenchmarkResult =
+  Either (RegularEditError Int) (RegularEditResult Int)
+
+type RegularEditObservation =
+  Either (RegularEditError Int) (Int, Int, Maybe (RegularSiteDisposition Int))
+
+siteCounts :: [Int]
+siteCounts = [16, 64, 128, 169, 256, 512, 2048]
+
+families :: [PowerBenchFamily]
+families = [WellConditioned, NearParallel, CollinearSlopes]
+
+-- | Deterministic, fixed-domain fixture bands include the exhibit scale and a
+-- larger scaling point.  Ordinary and nearly parallel families expose exact
+-- clipping cost; the collinear family exposes upper-chain classification.
+prepareCases :: IO [PowerBenchCase]
+prepareCases =
+  traverse prepareCase [(family, count) | family <- families, count <- siteCounts]
+
+prepareCase :: (PowerBenchFamily, Int) -> IO PowerBenchCase
+prepareCase (family, siteCount) = do
+  domain <-
+    requireRight
+      ( convexPolygon
+          ( exactPoint (-1) (-1)
+              :| [exactPoint 1 (-1), exactPoint 1 1, exactPoint (-1) 1]
+          )
+      )
+  sites <-
+    traverse (prepareSite family) (zip [0 ..] (fixturePoints family siteCount))
+  case NonEmpty.nonEmpty sites of
+    Nothing -> fail "power benchmark fixture requires at least one site"
+    Just nonEmptySites -> do
+      regular <- requireRight (regularTriangulation nonEmptySites)
+      insertionBaseSites <-
+        case NonEmpty.nonEmpty (NonEmpty.init nonEmptySites) of
+          Nothing -> fail "power edit benchmark requires at least two sites"
+          Just baseSites -> pure baseSites
+      insertionBase <- requireRight (regularTriangulation insertionBaseSites)
+      replacementWeight <- requireRight (powerWeight 17)
+      subordinateWeight <- requireRight (powerWeight (-1048576))
+      subordinateReweight <- requireRight (powerWeight (-2097152))
+      let representativeSite = NonEmpty.head nonEmptySites
+          commonShift =
+            Map.fromList
+              [ ( powerSiteLabel site
+                , powerWeightFromExact (powerWeightExact (powerSiteWeight site) + 1)
+                )
+              | site <- NonEmpty.toList nonEmptySites
+              ]
+      subordinateSite <-
+        requireRight
+          (powerSite siteCount (powerSitePosition representativeSite) subordinateWeight)
+      subordinateInsertion <-
+        requireRight (insertRegularSite subordinateSite (fst regular))
+      let preparedRegular = fst regular
+          hiddenEdit =
+            fmap
+              (\site ->
+                 HiddenEditFixture
+                   { hiddenEditLabel = powerSiteLabel site
+                   , hiddenEditLowerWeight = subordinateWeight
+                   })
+              ( List.find
+                  (\site ->
+                     regularSiteDisposition (powerSiteLabel site) preparedRegular
+                       == Just RegularSiteHidden)
+                  (regularSites preparedRegular)
+              )
+      pure
+        PowerBenchCase
+          { powerBenchLabel = familyLabel family <> "/n=" <> show siteCount
+          , powerBenchDomain = domain
+          , powerBenchSites = nonEmptySites
+          , powerBenchRegular = preparedRegular
+          , powerBenchInsertionBase = fst insertionBase
+          , powerBenchInsertedSite = NonEmpty.last nonEmptySites
+          , powerBenchReweight = replacementWeight
+          , powerBenchCommonShift = commonShift
+          , powerBenchCoincidentLabel = powerSiteLabel subordinateSite
+          , powerBenchCoincidentRegular = regularEditTriangulation subordinateInsertion
+          , powerBenchCoincidentReweight = subordinateReweight
+          , powerBenchHiddenEdit = hiddenEdit
+          }
+
+fixturePoints :: PowerBenchFamily -> Int -> [Point]
+fixturePoints family siteCount =
+  case family of
+    WellConditioned -> randomPoints 0x9e3779b97f4a7c15 siteCount
+    NearParallel -> nearParallelPoints siteCount
+    CollinearSlopes -> collinearSlopePoints siteCount
+
+nearParallelPoints :: Int -> [Point]
+nearParallelPoints siteCount =
+  [ Point
+      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8 - 0.9)
+      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8e-6 - 0.9e-6)
+  | index <- [1 .. siteCount]
+  ]
+
+collinearSlopePoints :: Int -> [Point]
+collinearSlopePoints siteCount =
+  [ Point (fromIntegral index / 1024 - 0.5) 0
+  | index <- [0 .. siteCount - 1]
+  ]
+
+familyLabel :: PowerBenchFamily -> String
+familyLabel WellConditioned = "power-well-conditioned"
+familyLabel NearParallel = "power-near-parallel"
+familyLabel CollinearSlopes = "power-collinear-slopes"
+
+prepareSite :: PowerBenchFamily -> (Int, Point) -> IO (PowerSite Int)
+prepareSite family (label, point) = do
+  let weightValue =
+        case family of
+          CollinearSlopes
+            | odd label -> -(2 / 1048576)
+            | otherwise -> 0
+          WellConditioned -> fromIntegral (label `mod` 9 - 4) / 256
+          NearParallel -> fromIntegral (label `mod` 9 - 4) / 256
+  weight <- requireRight (powerWeight weightValue)
+  requireRight (powerSite label point weight)
+
+-- | Print one receipt and one allocation probe per fixture.  Tasty-bench
+-- remains the authority for timing; this report is the derived structural
+-- view that timing samples cannot carry.
+reportCases :: [PowerBenchCase] -> IO ()
+reportCases cases = do
+  putStrLn "moonlight-planar exact bounded power-diagram benchmark"
+  putStrLn
+    "receipt-overhead-method: reveal one shared construction, force its diagram, then measure only the additional receipt-reachable allocation; strict work incurred while revealing the pair is not attributed"
+  traverse_ reportCase cases
+
+reportCase :: PowerBenchCase -> IO ()
+reportCase benchmarkCase = do
+  (sharedResult, allocation) <- measureSharedAllocation benchmarkCase
+  case sharedResult of
+    Left failure -> fail (powerBenchLabel benchmarkCase <> ": " <> show failure)
+    Right (_, receipt) -> do
+      putStrLn (renderReceipt (powerBenchLabel benchmarkCase) receipt)
+      putStrLn
+        ( powerBenchLabel benchmarkCase
+            <> "-allocated-bytes/shared-full: "
+            <> show (sharedFullAllocation allocation)
+        )
+      putStrLn
+        ( powerBenchLabel benchmarkCase
+            <> "-allocated-bytes/shared-through-diagram: "
+            <> show (sharedDiagramAllocation allocation)
+        )
+      putStrLn
+        ( powerBenchLabel benchmarkCase
+            <> "-allocated-bytes/incremental-receipt-reachable-overhead: "
+            <> show (sharedReceiptReachableAllocation allocation)
+        )
+
+runBenchmarks :: [PowerBenchCase] -> IO ()
+runBenchmarks cases = do
+  massBenchmarks <- PowerMassBench.prepareBenchmarks
+  defaultMain (bgroup "exact bounded power diagram" (concatMap caseBenchmarks cases) : massBenchmarks)
+
+caseBenchmarks :: PowerBenchCase -> [Benchmark]
+caseBenchmarks benchmarkCase =
+  [ bench
+      (powerBenchLabel benchmarkCase <> "/regular-topology")
+      (nf constructRegularTopology benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/construct-with-receipt")
+      (nf construct benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/construct-diagram-only")
+      (nf constructDiagramOnly benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/clip-prepared-regular")
+      (nf constructFromRegular benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/insert-local-observation")
+      (nf observeInsertion benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/insert-and-normalize")
+      (nf insertAndNormalize benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/remove-local-observation")
+      (nf observeRemoval benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/remove-and-normalize")
+      (nf removeAndNormalize benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/reweight-local-observation")
+      (nf observeReweight benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/reweight-and-normalize")
+      (nf reweightAndNormalize benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/common-weight-shift")
+      (nf shiftEveryWeight benchmarkCase)
+  ]
+    <> topologyPreservingBenchmarks benchmarkCase
+
+topologyPreservingBenchmarks :: PowerBenchCase -> [Benchmark]
+topologyPreservingBenchmarks benchmarkCase =
+  [ bench
+      (powerBenchLabel benchmarkCase <> "/remove-coincident-subordinate")
+      (nf removeCoincidentSubordinate benchmarkCase)
+  , bench
+      (powerBenchLabel benchmarkCase <> "/reweight-coincident-subordinate")
+      (nf reweightCoincidentSubordinate benchmarkCase)
+  ]
+    <> maybe [] hiddenBenchmarks (powerBenchHiddenEdit benchmarkCase)
+ where
+  hiddenBenchmarks hidden =
+    [ bench
+        (powerBenchLabel benchmarkCase <> "/remove-hidden")
+        (nf (removeHidden benchmarkCase) hidden)
+    , bench
+        (powerBenchLabel benchmarkCase <> "/reweight-hidden-downward")
+        (nf (reweightHiddenDownward benchmarkCase) hidden)
+    ]
+
+construct :: PowerBenchCase -> PowerResult
+construct benchmarkCase =
+  boundedPowerDiagram
+    (powerBenchDomain benchmarkCase)
+    (powerBenchSites benchmarkCase)
+
+constructDiagramOnly :: PowerBenchCase -> DiagramResult
+constructDiagramOnly = fmap fst . construct
+
+constructRegularTopology :: PowerBenchCase -> RegularResult
+constructRegularTopology = regularTriangulation . powerBenchSites
+
+constructFromRegular :: PowerBenchCase -> PowerResult
+constructFromRegular benchmarkCase =
+  boundedPowerDiagramFromRegular
+    (powerBenchDomain benchmarkCase)
+    (powerBenchRegular benchmarkCase)
+
+insertAndNormalize
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+insertAndNormalize benchmarkCase =
+  insertRegularSite
+    (powerBenchInsertedSite benchmarkCase)
+    (powerBenchInsertionBase benchmarkCase)
+
+observeInsertion
+  :: PowerBenchCase
+  -> RegularEditObservation
+observeInsertion benchmarkCase =
+  observeRegularEdit
+    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
+    (insertAndNormalize benchmarkCase)
+
+removeAndNormalize
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+removeAndNormalize benchmarkCase =
+  removeRegularSite
+    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
+    (powerBenchRegular benchmarkCase)
+
+observeRemoval
+  :: PowerBenchCase
+  -> RegularEditObservation
+observeRemoval benchmarkCase =
+  observeRegularEdit
+    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
+    (removeAndNormalize benchmarkCase)
+
+reweightAndNormalize
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+reweightAndNormalize benchmarkCase =
+  reweightRegularSites
+    ( Map.singleton
+        (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
+        (powerBenchReweight benchmarkCase)
+    )
+    (powerBenchRegular benchmarkCase)
+
+observeReweight
+  :: PowerBenchCase
+  -> RegularEditObservation
+observeReweight benchmarkCase =
+  observeRegularEdit
+    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
+    (reweightAndNormalize benchmarkCase)
+
+observeRegularEdit
+  :: Int
+  -> RegularEditBenchmarkResult
+  -> RegularEditObservation
+observeRegularEdit focus =
+  fmap
+    (\editResult ->
+       ( Set.size (regularEditChangedSites editResult)
+       , Vector.length (regularEditTransitions editResult)
+       , regularSiteDisposition
+           focus
+           (regularEditTriangulation editResult)
+       ))
+
+shiftEveryWeight
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+shiftEveryWeight benchmarkCase =
+  reweightRegularSites
+    (powerBenchCommonShift benchmarkCase)
+    (powerBenchRegular benchmarkCase)
+
+removeCoincidentSubordinate
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+removeCoincidentSubordinate benchmarkCase =
+  removeRegularSite
+    (powerBenchCoincidentLabel benchmarkCase)
+    (powerBenchCoincidentRegular benchmarkCase)
+
+reweightCoincidentSubordinate
+  :: PowerBenchCase
+  -> RegularEditBenchmarkResult
+reweightCoincidentSubordinate benchmarkCase =
+  reweightRegularSites
+    ( Map.singleton
+        (powerBenchCoincidentLabel benchmarkCase)
+        (powerBenchCoincidentReweight benchmarkCase)
+    )
+    (powerBenchCoincidentRegular benchmarkCase)
+
+removeHidden
+  :: PowerBenchCase
+  -> HiddenEditFixture
+  -> RegularEditBenchmarkResult
+removeHidden benchmarkCase fixture =
+  removeRegularSite (hiddenEditLabel fixture) (powerBenchRegular benchmarkCase)
+
+reweightHiddenDownward
+  :: PowerBenchCase
+  -> HiddenEditFixture
+  -> RegularEditBenchmarkResult
+reweightHiddenDownward benchmarkCase fixture =
+  reweightRegularSites
+    (Map.singleton (hiddenEditLabel fixture) (hiddenEditLowerWeight fixture))
+    (powerBenchRegular benchmarkCase)
+
+data SharedPowerAllocation = SharedPowerAllocation
+  { sharedFullAllocation :: !Integer
+  , sharedDiagramAllocation :: !Integer
+  , sharedReceiptReachableAllocation :: !Integer
+  }
+
+measureSharedAllocation
+  :: PowerBenchCase
+  -> IO (PowerResult, SharedPowerAllocation)
+measureSharedAllocation benchmarkCase = do
+  performGC
+  setAllocationCounter maxBound
+  allocationStart <- getAllocationCounter
+  sharedResult <- evaluate (construct benchmarkCase)
+  case sharedResult of
+    Left failure -> do
+      forcedFailure <- evaluate (force failure)
+      allocationEnd <- getAllocationCounter
+      let total = allocationDifference allocationStart allocationEnd
+      pure
+        ( Left forcedFailure
+        , SharedPowerAllocation total total 0
+        )
+    Right (diagram, receipt) -> do
+      forcedDiagram <- evaluate (force diagram)
+      allocationAfterDiagram <- getAllocationCounter
+      forcedReceipt <- evaluate (force receipt)
+      allocationAfterReceipt <- getAllocationCounter
+      pure
+        ( Right (forcedDiagram, forcedReceipt)
+        , SharedPowerAllocation
+            { sharedFullAllocation =
+                allocationDifference allocationStart allocationAfterReceipt
+            , sharedDiagramAllocation =
+                allocationDifference allocationStart allocationAfterDiagram
+            , sharedReceiptReachableAllocation =
+                allocationDifference allocationAfterDiagram allocationAfterReceipt
+            }
+        )
+
+allocationDifference :: Integral amount => amount -> amount -> Integer
+allocationDifference before after = toInteger (max 0 (before - after))
+
+renderReceipt :: String -> PowerDiagramReceipt -> String
+renderReceipt label receipt =
+  unwords
+    [ label <> "-receipt:"
+    , field "input-sites" powerDiagramInputSites
+    , field "domain-vertices" powerDiagramDomainVertices
+    , field "submitted-site-constraints" powerDiagramSubmittedSiteConstraints
+    , field "active-boundaries" powerDiagramActiveBoundaries
+    , field "boundary-compatibility-checks" powerDiagramBoundaryCompatibilityChecks
+    , field "exact-intersections" powerDiagramExactIntersections
+    , field "published" powerDiagramPublishedCells
+    , field "lower-dimensional" powerDiagramLowerDimensionalCells
+    , field "empty" powerDiagramEmptyCells
+    , field "coincident-equivalent" powerDiagramCoincidentEquivalentCells
+    , field "coincident-dominated" powerDiagramCoincidentDominatedCells
+    , field "regular-faces" powerDiagramRegularFaces
+    , field "regular-edges" powerDiagramRegularEdges
+    , field "oracle-cells" powerDiagramOracleCells
+    , field "maximum-cell-constraints" powerDiagramMaximumCellConstraints
+    , field "input-bits" powerDiagramMaximumInputBits
+    , field "affine-bits" powerDiagramMaximumAffineCoefficientBits
+    , field "peak-coordinate-bits" powerDiagramPeakIntermediateCoordinateBits
+    , field "final-coordinate-bits" powerDiagramFinalCoordinateBits
+    , field "final-denominator-bits" powerDiagramFinalDenominatorBits
+    , field "peak-growth-bits" powerDiagramPeakIntermediateBitGrowth
+    , field "final-growth-bits" powerDiagramFinalCoordinateBitGrowth
+    ]
+ where
+  field :: Show value => String -> (PowerDiagramReceipt -> value) -> String
+  field name project = name <> "=" <> show (project receipt)
diff --git a/bench/power/Moonlight/Planar/PowerMassBench.hs b/bench/power/Moonlight/Planar/PowerMassBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/power/Moonlight/Planar/PowerMassBench.hs
@@ -0,0 +1,114 @@
+-- | Preparation and repeated observation are separate measured workloads.
+-- This inspectable three-site fixture is a microbenchmark, not a production
+-- speed claim. Reconstruction shares the canonical power-site encoding.
+module Moonlight.Planar.PowerMassBench (prepareBenchmarks) where
+
+import BenchMeasure (requireRight)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Moonlight.Planar.Convex (convexPolygon, convexPolygonPoints, retainConvexPolygon)
+import Moonlight.Planar.Exact (ExactPoint, ExactRational, exactPoint, exactPointCross, exactRational)
+import Moonlight.Planar.PowerDiagram
+  ( PowerCellDisposition (..)
+  , PowerDiagramError
+  , PowerMassError
+  , PowerSite
+  , boundedPowerDiagram
+  , evaluatePowerMasses
+  , powerCellDispositions
+  , powerSite
+  , powerSiteLabel
+  , powerSitePosition
+  , powerWeightFromExact
+  , preparePowerMassSection
+  )
+import Moonlight.Planar.Point (Point (..))
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf)
+
+prepareBenchmarks :: IO [Benchmark]
+prepareBenchmarks = do
+  window <- requireRight
+    (convexPolygon (exactPoint (-2) (-2) :| [exactPoint 2 (-2), exactPoint 2 2, exactPoint (-2) 2]))
+  sites <- requireRight
+    (traverse (\(label, position) -> powerSite label position (powerWeightFromExact 0))
+      ((0 :: Int, Point (-1) (-1)) :| [(1, Point 1 (-1)), (2, Point 0 1)]))
+  let trajectories = NonEmpty.zip sites (1 :| [0, 0])
+      direction = Map.fromList (fmap (\(site, rate) -> (powerSiteLabel site, rate)) (NonEmpty.toList trajectories))
+      prepare = preparePowerMassSection window sites
+  section <- requireRight (prepare direction) >>= evaluate . force
+  parameters <- requireRight (traverse (`exactRational` 16) [-112 .. 64])
+  half <- requireRight (exactRational 1 2)
+  expiryBenchmarks <- prepareExpiryBenchmarks
+  retentionBenchmarks <- traverse prepareRetentionBenchmark [4, 32, 128]
+  let observations :: [ExactRational] -> Either (PowerMassError Int) [Map.Map Int ExactRational]
+      observations queryParameters = traverse (`evaluatePowerMasses` section) queryParameters
+      prepareAndObserve
+        :: (Map.Map Int ExactRational, [ExactRational])
+        -> Either (PowerMassError Int) [Map.Map Int ExactRational]
+      prepareAndObserve (queryDirection, queryParameters) = do
+        prepared <- prepare queryDirection
+        traverse (`evaluatePowerMasses` prepared) queryParameters
+      reconstruction parameter = do
+        reweighted <- traverse
+          (\(site, rate) -> powerSite (powerSiteLabel site) (powerSitePosition site) (powerWeightFromExact (parameter * rate)))
+          trajectories
+        (diagram, _) <- boundedPowerDiagram window reweighted
+        pure (Map.fromList (fmap (\(label, cell) -> (label, cellArea cell)) (powerCellDispositions diagram)))
+      cellArea :: PowerCellDisposition Int -> ExactRational
+      cellArea disposition = case disposition of
+        PublishedPowerCell polygon ->
+          let points@(initial :| remaining) = convexPolygonPoints polygon
+           in half * sum (zipWith exactPointCross (NonEmpty.toList points) (remaining <> [initial]))
+        LowerDimensionalPowerCell _ -> 0
+        EmptyPowerCell -> 0
+        CoincidentEquivalentTo _ -> 0
+        CoincidentDominatedBy _ -> 0
+      amortizedBenchmarks queryCount =
+        let queryParameters = take queryCount parameters
+         in bgroup ("queries-" <> show queryCount)
+              [ bench "prepare-and-evaluate" (nf prepareAndObserve (direction, queryParameters))
+              , bench "reconstruct" (nf (traverse reconstruction) queryParameters)
+              ]
+  pure
+    ([ bgroup "certified power mass/three-site"
+        ( [ bench "prepare" (nf prepare direction)
+          , bench "reuse-only-177-exact-parameters" (nf observations parameters)
+          ]
+          <> fmap amortizedBenchmarks [1, 2, 8, 32, 128]
+        )
+    , expiryBenchmarks
+    ] <> retentionBenchmarks)
+
+-- The admitted window is fixture cost. This measures only its conversion into
+-- retained affine boundaries, over the same exact convex family at each size.
+prepareRetentionBenchmark :: Int -> IO Benchmark
+prepareRetentionBenchmark count = do
+  let pointAt :: Int -> ExactPoint
+      pointAt index = exactPoint (fromIntegral index) (fromIntegral (index * index))
+  window <- requireRight (convexPolygon (pointAt 0 :| fmap pointAt [1 .. count - 1])) >>= evaluate . force
+  pure (bench ("convex-retention/" <> show count)
+    (nf retainConvexPolygon window))
+
+-- The next seed is an explicitly supplied exact interior value, not an
+-- epsilon-derived event transition. The benchmark includes the refusal at
+-- the old endpoint and complete preparation at the new seed.
+prepareExpiryBenchmarks :: IO Benchmark
+prepareExpiryBenchmarks = do
+  window <- requireRight
+    (convexPolygon (exactPoint (-1) (-1) :| [exactPoint 1 (-1), exactPoint 1 1, exactPoint (-1) 1]))
+  let siteAt :: Int -> Point -> ExactRational -> Either (PowerDiagramError Int) (PowerSite Int)
+      siteAt label position weight = powerSite label position (powerWeightFromExact weight)
+      direction = Map.fromList [(0 :: Int, 0), (1, 4)]
+  firstSite <- requireRight (siteAt 0 (Point 0 0) 0)
+  hiddenSite <- requireRight (siteAt 1 (Point 2 0) (-4))
+  visibleSite <- requireRight (siteAt 1 (Point 2 0) 2)
+  initial <- requireRight (preparePowerMassSection window (firstSite :| [hiddenSite]) direction) >>= evaluate . force
+  let expireAndRecertify (parameter, updatedSites) =
+        ( evaluatePowerMasses parameter initial
+        , preparePowerMassSection window updatedSites direction
+        )
+  pure (bench "certified power mass/window-contact/expiry-and-recertify-at-3-over-2"
+    (nf expireAndRecertify (1, firstSite :| [visibleSite])))
diff --git a/bench/power/Moonlight/Triangulation/PowerBench.hs b/bench/power/Moonlight/Triangulation/PowerBench.hs
deleted file mode 100644
--- a/bench/power/Moonlight/Triangulation/PowerBench.hs
+++ /dev/null
@@ -1,536 +0,0 @@
--- | Tasty-bench lanes for the exact bounded power-diagram owner.  The timed
--- lanes force the public result, while the one-shot report keeps the
--- structural receipt and a shared incremental allocation observation outside
--- the timing sample.  One construction is revealed, its diagram is forced,
--- and only then is the additional receipt-reachable section measured.
-module Moonlight.Triangulation.PowerBench
-  ( PowerBenchCase
-  , prepareCases
-  , reportCases
-  , runBenchmarks
-  ) where
-
-import BenchMeasure (requireRight)
-import BenchSupport (randomPoints)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Data.Foldable (traverse_)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Vector as Vector
-import GHC.Conc.Sync (getAllocationCounter, setAllocationCounter)
-import Moonlight.Triangulation
-  ( BoundedPowerDiagram
-  , ConvexPolygon
-  , Point (Point)
-  , PowerDiagramError
-  , PowerDiagramReceipt
-  , PowerSite
-  , PowerWeight
-  , RegularEditError
-  , RegularEditResult
-  , RegularTriangulation
-  , RegularTriangulationReceipt
-  , boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , convexPolygon
-  , exactPoint
-  , powerDiagramActiveBoundaries
-  , powerDiagramCoincidentDominatedCells
-  , powerDiagramCoincidentEquivalentCells
-  , powerDiagramDomainVertices
-  , powerDiagramBoundaryCompatibilityChecks
-  , powerDiagramEmptyCells
-  , powerDiagramExactIntersections
-  , powerDiagramFinalCoordinateBitGrowth
-  , powerDiagramFinalCoordinateBits
-  , powerDiagramFinalDenominatorBits
-  , powerDiagramInputSites
-  , powerDiagramLowerDimensionalCells
-  , powerDiagramMaximumAffineCoefficientBits
-  , powerDiagramMaximumInputBits
-  , powerDiagramPeakIntermediateBitGrowth
-  , powerDiagramPeakIntermediateCoordinateBits
-  , powerDiagramMaximumCellConstraints
-  , powerDiagramPublishedCells
-  , powerDiagramOracleCells
-  , powerDiagramRegularEdges
-  , powerDiagramRegularFaces
-  , powerDiagramSubmittedSiteConstraints
-  , powerSite
-  , powerSiteLabel
-  , powerSitePosition
-  , powerSiteWeight
-  , powerWeight
-  , powerWeightExact
-  , powerWeightFromExact
-  , insertRegularSite
-  , removeRegularSite
-  , reweightRegularSites
-  , regularTriangulation
-  , regularEditChangedSites
-  , regularEditTriangulation
-  , regularEditTransitions
-  , regularSiteDisposition
-  , regularSites
-  , RegularSiteDisposition (RegularSiteHidden)
-  )
-import System.Mem (performGC)
-import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)
-
-data PowerBenchFamily
-  = WellConditioned
-  | NearParallel
-  | CollinearSlopes
-  deriving stock (Eq, Ord, Show)
-
-data PowerBenchCase = PowerBenchCase
-  { powerBenchLabel :: !String
-  , powerBenchDomain :: !ConvexPolygon
-  , powerBenchSites :: !(NonEmpty (PowerSite Int))
-  , powerBenchRegular :: !(RegularTriangulation Int)
-  , powerBenchInsertionBase :: !(RegularTriangulation Int)
-  , powerBenchInsertedSite :: !(PowerSite Int)
-  , powerBenchReweight :: !PowerWeight
-  , powerBenchCommonShift :: !(Map.Map Int PowerWeight)
-  , powerBenchCoincidentLabel :: !Int
-  , powerBenchCoincidentRegular :: !(RegularTriangulation Int)
-  , powerBenchCoincidentReweight :: !PowerWeight
-  , powerBenchHiddenEdit :: !(Maybe HiddenEditFixture)
-  }
-
-data HiddenEditFixture = HiddenEditFixture
-  { hiddenEditLabel :: !Int
-  , hiddenEditLowerWeight :: !PowerWeight
-  }
-
-type PowerResult =
-  Either (PowerDiagramError Int) (BoundedPowerDiagram Int, PowerDiagramReceipt)
-
-type DiagramResult =
-  Either (PowerDiagramError Int) (BoundedPowerDiagram Int)
-
-type RegularResult =
-  Either
-    (PowerDiagramError Int)
-    (RegularTriangulation Int, RegularTriangulationReceipt)
-
-type RegularEditBenchmarkResult =
-  Either (RegularEditError Int) (RegularEditResult Int)
-
-type RegularEditObservation =
-  Either (RegularEditError Int) (Int, Int, Maybe (RegularSiteDisposition Int))
-
-siteCounts :: [Int]
-siteCounts = [16, 64, 128, 169, 256, 512, 2048]
-
-families :: [PowerBenchFamily]
-families = [WellConditioned, NearParallel, CollinearSlopes]
-
--- | Deterministic, fixed-domain fixture bands include the exhibit scale and a
--- larger scaling point.  Ordinary and nearly parallel families expose exact
--- clipping cost; the collinear family exposes upper-chain classification.
-prepareCases :: IO [PowerBenchCase]
-prepareCases =
-  traverse prepareCase [(family, count) | family <- families, count <- siteCounts]
-
-prepareCase :: (PowerBenchFamily, Int) -> IO PowerBenchCase
-prepareCase (family, siteCount) = do
-  domain <-
-    requireRight
-      ( convexPolygon
-          ( exactPoint (-1) (-1)
-              :| [exactPoint 1 (-1), exactPoint 1 1, exactPoint (-1) 1]
-          )
-      )
-  sites <-
-    traverse (prepareSite family) (zip [0 ..] (fixturePoints family siteCount))
-  case NonEmpty.nonEmpty sites of
-    Nothing -> fail "power benchmark fixture requires at least one site"
-    Just nonEmptySites -> do
-      regular <- requireRight (regularTriangulation nonEmptySites)
-      insertionBaseSites <-
-        case NonEmpty.nonEmpty (NonEmpty.init nonEmptySites) of
-          Nothing -> fail "power edit benchmark requires at least two sites"
-          Just baseSites -> pure baseSites
-      insertionBase <- requireRight (regularTriangulation insertionBaseSites)
-      replacementWeight <- requireRight (powerWeight 17)
-      subordinateWeight <- requireRight (powerWeight (-1048576))
-      subordinateReweight <- requireRight (powerWeight (-2097152))
-      let representativeSite = NonEmpty.head nonEmptySites
-          commonShift =
-            Map.fromList
-              [ ( powerSiteLabel site
-                , powerWeightFromExact (powerWeightExact (powerSiteWeight site) + 1)
-                )
-              | site <- NonEmpty.toList nonEmptySites
-              ]
-      subordinateSite <-
-        requireRight
-          (powerSite siteCount (powerSitePosition representativeSite) subordinateWeight)
-      subordinateInsertion <-
-        requireRight (insertRegularSite subordinateSite (fst regular))
-      let preparedRegular = fst regular
-          hiddenEdit =
-            fmap
-              (\site ->
-                 HiddenEditFixture
-                   { hiddenEditLabel = powerSiteLabel site
-                   , hiddenEditLowerWeight = subordinateWeight
-                   })
-              ( List.find
-                  (\site ->
-                     regularSiteDisposition (powerSiteLabel site) preparedRegular
-                       == Just RegularSiteHidden)
-                  (regularSites preparedRegular)
-              )
-      pure
-        PowerBenchCase
-          { powerBenchLabel = familyLabel family <> "/n=" <> show siteCount
-          , powerBenchDomain = domain
-          , powerBenchSites = nonEmptySites
-          , powerBenchRegular = preparedRegular
-          , powerBenchInsertionBase = fst insertionBase
-          , powerBenchInsertedSite = NonEmpty.last nonEmptySites
-          , powerBenchReweight = replacementWeight
-          , powerBenchCommonShift = commonShift
-          , powerBenchCoincidentLabel = powerSiteLabel subordinateSite
-          , powerBenchCoincidentRegular = regularEditTriangulation subordinateInsertion
-          , powerBenchCoincidentReweight = subordinateReweight
-          , powerBenchHiddenEdit = hiddenEdit
-          }
-
-fixturePoints :: PowerBenchFamily -> Int -> [Point]
-fixturePoints family siteCount =
-  case family of
-    WellConditioned -> randomPoints 0x9e3779b97f4a7c15 siteCount
-    NearParallel -> nearParallelPoints siteCount
-    CollinearSlopes -> collinearSlopePoints siteCount
-
-nearParallelPoints :: Int -> [Point]
-nearParallelPoints siteCount =
-  [ Point
-      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8 - 0.9)
-      (fromIntegral index / fromIntegral (siteCount + 1) * 1.8e-6 - 0.9e-6)
-  | index <- [1 .. siteCount]
-  ]
-
-collinearSlopePoints :: Int -> [Point]
-collinearSlopePoints siteCount =
-  [ Point (fromIntegral index / 1024 - 0.5) 0
-  | index <- [0 .. siteCount - 1]
-  ]
-
-familyLabel :: PowerBenchFamily -> String
-familyLabel WellConditioned = "power-well-conditioned"
-familyLabel NearParallel = "power-near-parallel"
-familyLabel CollinearSlopes = "power-collinear-slopes"
-
-prepareSite :: PowerBenchFamily -> (Int, Point) -> IO (PowerSite Int)
-prepareSite family (label, point) = do
-  let weightValue =
-        case family of
-          CollinearSlopes
-            | odd label -> -(2 / 1048576)
-            | otherwise -> 0
-          WellConditioned -> fromIntegral (label `mod` 9 - 4) / 256
-          NearParallel -> fromIntegral (label `mod` 9 - 4) / 256
-  weight <- requireRight (powerWeight weightValue)
-  requireRight (powerSite label point weight)
-
--- | Print one receipt and one allocation probe per fixture.  Tasty-bench
--- remains the authority for timing; this report is the derived structural
--- view that timing samples cannot carry.
-reportCases :: [PowerBenchCase] -> IO ()
-reportCases cases = do
-  putStrLn "moonlight-triangulation exact bounded power-diagram benchmark"
-  putStrLn
-    "receipt-overhead-method: reveal one shared construction, force its diagram, then measure only the additional receipt-reachable allocation; strict work incurred while revealing the pair is not attributed"
-  traverse_ reportCase cases
-
-reportCase :: PowerBenchCase -> IO ()
-reportCase benchmarkCase = do
-  (sharedResult, allocation) <- measureSharedAllocation benchmarkCase
-  case sharedResult of
-    Left failure -> fail (powerBenchLabel benchmarkCase <> ": " <> show failure)
-    Right (_, receipt) -> do
-      putStrLn (renderReceipt (powerBenchLabel benchmarkCase) receipt)
-      putStrLn
-        ( powerBenchLabel benchmarkCase
-            <> "-allocated-bytes/shared-full: "
-            <> show (sharedFullAllocation allocation)
-        )
-      putStrLn
-        ( powerBenchLabel benchmarkCase
-            <> "-allocated-bytes/shared-through-diagram: "
-            <> show (sharedDiagramAllocation allocation)
-        )
-      putStrLn
-        ( powerBenchLabel benchmarkCase
-            <> "-allocated-bytes/incremental-receipt-reachable-overhead: "
-            <> show (sharedReceiptReachableAllocation allocation)
-        )
-
-runBenchmarks :: [PowerBenchCase] -> IO ()
-runBenchmarks cases = defaultMain [bgroup "exact bounded power diagram" (concatMap caseBenchmarks cases)]
-
-caseBenchmarks :: PowerBenchCase -> [Benchmark]
-caseBenchmarks benchmarkCase =
-  [ bench
-      (powerBenchLabel benchmarkCase <> "/regular-topology")
-      (nf constructRegularTopology benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/construct-with-receipt")
-      (nf construct benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/construct-diagram-only")
-      (nf constructDiagramOnly benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/clip-prepared-regular")
-      (nf constructFromRegular benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/insert-local-observation")
-      (nf observeInsertion benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/insert-and-normalize")
-      (nf insertAndNormalize benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/remove-local-observation")
-      (nf observeRemoval benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/remove-and-normalize")
-      (nf removeAndNormalize benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/reweight-local-observation")
-      (nf observeReweight benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/reweight-and-normalize")
-      (nf reweightAndNormalize benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/common-weight-shift")
-      (nf shiftEveryWeight benchmarkCase)
-  ]
-    <> topologyPreservingBenchmarks benchmarkCase
-
-topologyPreservingBenchmarks :: PowerBenchCase -> [Benchmark]
-topologyPreservingBenchmarks benchmarkCase =
-  [ bench
-      (powerBenchLabel benchmarkCase <> "/remove-coincident-subordinate")
-      (nf removeCoincidentSubordinate benchmarkCase)
-  , bench
-      (powerBenchLabel benchmarkCase <> "/reweight-coincident-subordinate")
-      (nf reweightCoincidentSubordinate benchmarkCase)
-  ]
-    <> maybe [] hiddenBenchmarks (powerBenchHiddenEdit benchmarkCase)
- where
-  hiddenBenchmarks hidden =
-    [ bench
-        (powerBenchLabel benchmarkCase <> "/remove-hidden")
-        (nf (removeHidden benchmarkCase) hidden)
-    , bench
-        (powerBenchLabel benchmarkCase <> "/reweight-hidden-downward")
-        (nf (reweightHiddenDownward benchmarkCase) hidden)
-    ]
-
-construct :: PowerBenchCase -> PowerResult
-construct benchmarkCase =
-  boundedPowerDiagram
-    (powerBenchDomain benchmarkCase)
-    (powerBenchSites benchmarkCase)
-
-constructDiagramOnly :: PowerBenchCase -> DiagramResult
-constructDiagramOnly = fmap fst . construct
-
-constructRegularTopology :: PowerBenchCase -> RegularResult
-constructRegularTopology = regularTriangulation . powerBenchSites
-
-constructFromRegular :: PowerBenchCase -> PowerResult
-constructFromRegular benchmarkCase =
-  boundedPowerDiagramFromRegular
-    (powerBenchDomain benchmarkCase)
-    (powerBenchRegular benchmarkCase)
-
-insertAndNormalize
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-insertAndNormalize benchmarkCase =
-  insertRegularSite
-    (powerBenchInsertedSite benchmarkCase)
-    (powerBenchInsertionBase benchmarkCase)
-
-observeInsertion
-  :: PowerBenchCase
-  -> RegularEditObservation
-observeInsertion benchmarkCase =
-  observeRegularEdit
-    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
-    (insertAndNormalize benchmarkCase)
-
-removeAndNormalize
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-removeAndNormalize benchmarkCase =
-  removeRegularSite
-    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
-    (powerBenchRegular benchmarkCase)
-
-observeRemoval
-  :: PowerBenchCase
-  -> RegularEditObservation
-observeRemoval benchmarkCase =
-  observeRegularEdit
-    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
-    (removeAndNormalize benchmarkCase)
-
-reweightAndNormalize
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-reweightAndNormalize benchmarkCase =
-  reweightRegularSites
-    ( Map.singleton
-        (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
-        (powerBenchReweight benchmarkCase)
-    )
-    (powerBenchRegular benchmarkCase)
-
-observeReweight
-  :: PowerBenchCase
-  -> RegularEditObservation
-observeReweight benchmarkCase =
-  observeRegularEdit
-    (powerSiteLabel (powerBenchInsertedSite benchmarkCase))
-    (reweightAndNormalize benchmarkCase)
-
-observeRegularEdit
-  :: Int
-  -> RegularEditBenchmarkResult
-  -> RegularEditObservation
-observeRegularEdit focus =
-  fmap
-    (\editResult ->
-       ( Set.size (regularEditChangedSites editResult)
-       , Vector.length (regularEditTransitions editResult)
-       , regularSiteDisposition
-           focus
-           (regularEditTriangulation editResult)
-       ))
-
-shiftEveryWeight
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-shiftEveryWeight benchmarkCase =
-  reweightRegularSites
-    (powerBenchCommonShift benchmarkCase)
-    (powerBenchRegular benchmarkCase)
-
-removeCoincidentSubordinate
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-removeCoincidentSubordinate benchmarkCase =
-  removeRegularSite
-    (powerBenchCoincidentLabel benchmarkCase)
-    (powerBenchCoincidentRegular benchmarkCase)
-
-reweightCoincidentSubordinate
-  :: PowerBenchCase
-  -> RegularEditBenchmarkResult
-reweightCoincidentSubordinate benchmarkCase =
-  reweightRegularSites
-    ( Map.singleton
-        (powerBenchCoincidentLabel benchmarkCase)
-        (powerBenchCoincidentReweight benchmarkCase)
-    )
-    (powerBenchCoincidentRegular benchmarkCase)
-
-removeHidden
-  :: PowerBenchCase
-  -> HiddenEditFixture
-  -> RegularEditBenchmarkResult
-removeHidden benchmarkCase fixture =
-  removeRegularSite (hiddenEditLabel fixture) (powerBenchRegular benchmarkCase)
-
-reweightHiddenDownward
-  :: PowerBenchCase
-  -> HiddenEditFixture
-  -> RegularEditBenchmarkResult
-reweightHiddenDownward benchmarkCase fixture =
-  reweightRegularSites
-    (Map.singleton (hiddenEditLabel fixture) (hiddenEditLowerWeight fixture))
-    (powerBenchRegular benchmarkCase)
-
-data SharedPowerAllocation = SharedPowerAllocation
-  { sharedFullAllocation :: !Integer
-  , sharedDiagramAllocation :: !Integer
-  , sharedReceiptReachableAllocation :: !Integer
-  }
-
-measureSharedAllocation
-  :: PowerBenchCase
-  -> IO (PowerResult, SharedPowerAllocation)
-measureSharedAllocation benchmarkCase = do
-  performGC
-  setAllocationCounter maxBound
-  allocationStart <- getAllocationCounter
-  sharedResult <- evaluate (construct benchmarkCase)
-  case sharedResult of
-    Left failure -> do
-      forcedFailure <- evaluate (force failure)
-      allocationEnd <- getAllocationCounter
-      let total = allocationDifference allocationStart allocationEnd
-      pure
-        ( Left forcedFailure
-        , SharedPowerAllocation total total 0
-        )
-    Right (diagram, receipt) -> do
-      forcedDiagram <- evaluate (force diagram)
-      allocationAfterDiagram <- getAllocationCounter
-      forcedReceipt <- evaluate (force receipt)
-      allocationAfterReceipt <- getAllocationCounter
-      pure
-        ( Right (forcedDiagram, forcedReceipt)
-        , SharedPowerAllocation
-            { sharedFullAllocation =
-                allocationDifference allocationStart allocationAfterReceipt
-            , sharedDiagramAllocation =
-                allocationDifference allocationStart allocationAfterDiagram
-            , sharedReceiptReachableAllocation =
-                allocationDifference allocationAfterDiagram allocationAfterReceipt
-            }
-        )
-
-allocationDifference :: Integral amount => amount -> amount -> Integer
-allocationDifference before after = toInteger (max 0 (before - after))
-
-renderReceipt :: String -> PowerDiagramReceipt -> String
-renderReceipt label receipt =
-  unwords
-    [ label <> "-receipt:"
-    , field "input-sites" powerDiagramInputSites
-    , field "domain-vertices" powerDiagramDomainVertices
-    , field "submitted-site-constraints" powerDiagramSubmittedSiteConstraints
-    , field "active-boundaries" powerDiagramActiveBoundaries
-    , field "boundary-compatibility-checks" powerDiagramBoundaryCompatibilityChecks
-    , field "exact-intersections" powerDiagramExactIntersections
-    , field "published" powerDiagramPublishedCells
-    , field "lower-dimensional" powerDiagramLowerDimensionalCells
-    , field "empty" powerDiagramEmptyCells
-    , field "coincident-equivalent" powerDiagramCoincidentEquivalentCells
-    , field "coincident-dominated" powerDiagramCoincidentDominatedCells
-    , field "regular-faces" powerDiagramRegularFaces
-    , field "regular-edges" powerDiagramRegularEdges
-    , field "oracle-cells" powerDiagramOracleCells
-    , field "maximum-cell-constraints" powerDiagramMaximumCellConstraints
-    , field "input-bits" powerDiagramMaximumInputBits
-    , field "affine-bits" powerDiagramMaximumAffineCoefficientBits
-    , field "peak-coordinate-bits" powerDiagramPeakIntermediateCoordinateBits
-    , field "final-coordinate-bits" powerDiagramFinalCoordinateBits
-    , field "final-denominator-bits" powerDiagramFinalDenominatorBits
-    , field "peak-growth-bits" powerDiagramPeakIntermediateBitGrowth
-    , field "final-growth-bits" powerDiagramFinalCoordinateBitGrowth
-    ]
- where
-  field :: Show value => String -> (PowerDiagramReceipt -> value) -> String
-  field name project = name <> "=" <> show (project receipt)
diff --git a/bench/publication/PublicationMain.hs b/bench/publication/PublicationMain.hs
--- a/bench/publication/PublicationMain.hs
+++ b/bench/publication/PublicationMain.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.JoinBench as JoinBench
+import qualified Moonlight.Planar.JoinBench as JoinBench
 
 main :: IO ()
 main = JoinBench.publicationBenchmarks
diff --git a/bench/region/Main.hs b/bench/region/Main.hs
--- a/bench/region/Main.hs
+++ b/bench/region/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.RegionBench as RegionBench
+import qualified Moonlight.Planar.RegionBench as RegionBench
 
 main :: IO ()
 main = RegionBench.benchmarks
diff --git a/bench/region/Moonlight/Planar/RegionBench.hs b/bench/region/Moonlight/Planar/RegionBench.hs
new file mode 100644
--- /dev/null
+++ b/bench/region/Moonlight/Planar/RegionBench.hs
@@ -0,0 +1,501 @@
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Exact segment-event, overlay, and grouped-region publication receipts.
+-- The source families are closed data; measurement is the only effect.
+module Moonlight.Planar.RegionBench (benchmarks) where
+
+import BenchSupport
+  ( latticeFaceBand
+  , latticePoints
+  )
+import BenchMeasure (requireRight, timedValue)
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import qualified Data.Map.Strict as Map
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Types (buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.CellSet
+  ( exactCellSet
+  , exactCellSetEdgeCount
+  , exactCellSetFaceCount
+  , exactCellSetVertexCount
+  )
+import Moonlight.Planar.Dcel (numInnerFaces)
+import Moonlight.Planar.Exact (ExactPoint, exactPoint)
+import Moonlight.Planar.Convex (convexPolygon)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Overlay
+  ( OverlayReceipt (..)
+  , overlayClosedIntersection
+  , overlayLayers
+  , overlayReceipt
+  )
+import Moonlight.Planar.Minkowski
+  ( MinkowskiReceipt
+  , convexMinkowskiSum
+  , erodeBy
+  , minkowskiExactCoordinateBitGrowth
+  , minkowskiExactCrossings
+  , minkowskiGeneratedConvolutionEdges
+  , minkowskiGeneratedPieces
+  , minkowskiOutputCells
+  , minkowskiOverlayPasses
+  , minkowskiSum
+  , structuringElement
+  )
+import Moonlight.Planar.Region
+  ( PlanarLayer
+  , PolygonComponent
+  , RegionValidationError
+  , exactLoop
+  , exactLoopPoints
+  , labelledPlanarLayer
+  , planarLayer
+  , planarLayerRegions
+  , planarRegion
+  , planarRegionComponents
+  , polygonComponent
+  , polygonHoleLoops
+  , polygonOuterLoop
+  )
+import Moonlight.Planar.Valuation
+  ( cellValuations
+  , eulerCharacteristicValue
+  , exactAreaValue
+  , exactLengthTerms
+  , lengthCoefficient
+  , regionValuations
+  , lengthRadicand
+  , valuationArea
+  , valuationEuler
+  , valuationIntrinsic1
+  , exactLengthExpression
+  )
+
+data OverlayFamily
+  = DisjointFamily
+  | GridCrossingFamily
+  | CollinearOverlapFamily
+  deriving stock (Eq, Ord, Show)
+
+benchmarks :: IO ()
+benchmarks = do
+  traverse_
+    (\family -> traverse_ (benchmarkOverlayFamily family) [2, 4, 8])
+    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
+  benchmarkPublicationReceipt
+  traverse_ benchmarkRegionAuthoring [64, 256, 1_024]
+  traverse_ benchmarkRegionValuation [64, 256, 1_024]
+  traverse_ benchmarkRegionTouchingValuation [8, 16, 32]
+  traverse_ benchmarkConvexMinkowski [8, 32, 128, 512]
+  traverse_
+    (\family -> traverse_ (benchmarkOverlaySelectorFamily family) [2, 4, 8])
+    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
+  benchmarkGeneralMorphology
+
+benchmarkOverlayFamily :: OverlayFamily -> Int -> IO ()
+benchmarkOverlayFamily family size = do
+  layers <- requireRight (familyLayers family size)
+  result <-
+    timedValue
+      (familyName family <> "-n" <> show size)
+      (requireRight (uncurry overlayLayers layers))
+  let receipt = overlayReceipt result
+      inputAndOutput = overlayInputSegments receipt + overlayRelationEvents receipt
+      logarithmicScale = max 1 (ceilingLog2 (overlayInputSegments receipt + 1))
+      totalLimit = 128 * inputAndOutput * logarithmicScale
+  putStrLn
+    ( familyName family
+        <> "-receipt: n="
+        <> show size
+        <> " source-segments="
+        <> show (overlayInputSegments receipt)
+        <> " relation-events-k="
+        <> show (overlayRelationEvents receipt)
+        <> " exact-relation-checks="
+        <> show (overlayTotalRelationChecks receipt)
+        <> " atomic-edges="
+        <> show (overlayAtomicEdges receipt)
+        <> " arrangement-cells="
+        <> show (overlayArrangementCells receipt)
+        <> " boundary-orbits="
+        <> show (overlayBoundaryOrbits receipt)
+        <> " avl-height="
+        <> show (overlaySweepMaximumHeight receipt)
+    )
+  if overlayTotalRelationChecks receipt <= totalLimit
+    then pure ()
+    else
+      fail
+        ( familyName family
+            <> " retained superlinear total relation work: "
+            <> show (overlayTotalRelationChecks receipt, totalLimit)
+        )
+
+benchmarkOverlaySelectorFamily :: OverlayFamily -> Int -> IO ()
+benchmarkOverlaySelectorFamily family size = do
+  layers <- requireRight (familyLayers family size)
+  result <- requireRight (uncurry overlayLayers layers)
+  selectedReceipt <-
+    timedValue
+      (familyName family <> "-selector-n" <> show size)
+      (do
+         selected <-
+           requireRight
+             (overlayClosedIntersection (== 1) (== 1) result)
+         valuations <- requireRight (cellValuations selected)
+         pure
+           ( exactCellSetVertexCount selected
+           , exactCellSetEdgeCount selected
+           , exactCellSetFaceCount selected
+           , eulerCharacteristicValue (valuationEuler valuations)
+           , length
+               ( exactLengthTerms
+                   (exactLengthExpression (valuationIntrinsic1 valuations))
+               )
+           ))
+  putStrLn
+    ( familyName family
+        <> "-selector-receipt: n="
+        <> show size
+        <> " cells="
+        <> show selectedReceipt
+    )
+
+familyName :: OverlayFamily -> String
+familyName DisjointFamily = "overlay-disjoint"
+familyName GridCrossingFamily = "overlay-grid-crossing"
+familyName CollinearOverlapFamily = "overlay-collinear-overlap"
+
+benchmarkRegionAuthoring :: Int -> IO ()
+benchmarkRegionAuthoring size = do
+  componentCount <-
+    timedValue
+      ("planar-region-disjoint-authoring-n" <> show size)
+      (requireRight $ do
+         components <-
+           traverse
+             rectangleComponent
+             (disjointRectangles size)
+         length . planarRegionComponents <$> planarRegion components)
+  if componentCount == size
+    then
+      putStrLn
+        ( "planar-region-disjoint-authoring-receipt: n="
+            <> show size
+            <> " components="
+            <> show componentCount
+        )
+    else fail ("planar region authoring lost components: " <> show componentCount)
+
+benchmarkRegionValuation :: Int -> IO ()
+benchmarkRegionValuation size = do
+  components <-
+    requireRight
+      ( traverse
+          rectangleComponent
+          (disjointRectangles size)
+      )
+  region <- requireRight (planarRegion components)
+  valuations <-
+    timedValue
+      ("planar-region-valuations-n" <> show size)
+      (requireRight (regionValuations region))
+  let receipt =
+        ( eulerCharacteristicValue (valuationEuler valuations)
+        , length
+            ( exactLengthTerms
+                (exactLengthExpression (valuationIntrinsic1 valuations))
+            )
+        )
+  if receipt == (size, 1)
+    then
+      putStrLn
+        ( "planar-region-valuations-receipt: n="
+            <> show size
+            <> " euler="
+            <> show size
+            <> " radical-terms=1"
+        )
+    else fail ("planar region valuation receipt mismatch: " <> show receipt)
+
+-- | Valuations over a family whose component boundaries meet: unit cells in
+-- even rows, double-width cells in odd rows, tiling one square of the given
+-- side. Neighbours in a row share an edge exactly (duplicate segments), a
+-- double cell's edge covers two unit edges (collinear overlaps), and every
+-- corner is a shared endpoint, so the contact path of 'regionValuations' is
+-- what this lane times. The union is one simply connected square, so the
+-- receipt is closed-form: Euler 1, area side², intrinsic one-volume 2·side.
+-- Every edge length is an integer, so the normal form folds the whole
+-- expression into the single radicand-1 term.
+benchmarkRegionTouchingValuation :: Int -> IO ()
+benchmarkRegionTouchingValuation side = do
+  let cells = touchingCells side
+      count = length cells
+  components <- requireRight (traverse rectangleComponent cells)
+  region <- requireRight (planarRegion components)
+  valuations <-
+    timedValue
+      ("planar-region-valuations-touching-n" <> show count)
+      (requireRight (regionValuations region))
+  let euler = eulerCharacteristicValue (valuationEuler valuations)
+      area = exactAreaValue (valuationArea valuations)
+      terms =
+        [ (lengthCoefficient term, lengthRadicand term)
+        | term <- exactLengthTerms (exactLengthExpression (valuationIntrinsic1 valuations))
+        ]
+      intrinsic1 = case terms of
+        [(coefficient, 1)] -> Just coefficient
+        _ -> Nothing
+  if euler == 1 && area == fromIntegral (side * side) && intrinsic1 == Just (fromIntegral (2 * side))
+    then
+      putStrLn
+        ( "planar-region-valuations-touching-receipt: n="
+            <> show count
+            <> " side="
+            <> show side
+            <> " euler=1 area="
+            <> show (side * side)
+            <> " intrinsic1="
+            <> show (2 * side)
+            <> " radical-terms="
+            <> show (length terms)
+        )
+    else fail ("planar region touching valuation receipt mismatch: " <> show (euler, area, terms))
+
+benchmarkConvexMinkowski :: Int -> IO ()
+benchmarkConvexMinkowski halfSize = do
+  left <- requireRight (convexPolygon (convexLens halfSize))
+  right <- requireRight (convexPolygon (convexLens halfSize))
+  result <-
+    timedValue
+      ("convex-minkowski-n" <> show (2 * halfSize))
+      (evaluate (convexMinkowskiSum left right))
+  let outputVertices =
+        sum
+          [ length (exactLoopPoints (polygonOuterLoop component))
+          | component <- planarRegionComponents result
+          ]
+      inputVertices = 4 * halfSize
+  if outputVertices <= inputVertices
+    then
+      putStrLn
+        ( "convex-minkowski-receipt: input-vertices="
+            <> show inputVertices
+            <> " output-vertices="
+            <> show outputVertices
+        )
+    else fail ("convex Minkowski output exceeded n+m: " <> show (outputVertices, inputVertices))
+
+benchmarkGeneralMorphology :: IO ()
+benchmarkGeneralMorphology = do
+  concaveLoop <-
+    requireRight
+      ( exactLoop
+          ( integerPoint 0 0
+              :| [ integerPoint 6 0
+                 , integerPoint 6 2
+                 , integerPoint 2 2
+                 , integerPoint 2 6
+                 , integerPoint 0 6
+                 ]
+          )
+      )
+  concaveComponent <- requireRight (polygonComponent concaveLoop [])
+  concaveRegion <- requireRight (planarRegion [concaveComponent])
+  kernelComponent <- requireRight (rectangleComponent (-1, -1, 1, 1))
+  kernelRegion <- requireRight (planarRegion [kernelComponent])
+  kernelPolygon <-
+    requireRight
+      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
+  element <- requireRight (structuringElement kernelPolygon)
+  (sumComponents, sumReceipt) <-
+    timedValue
+      "general-minkowski-concave"
+      (do
+         (result, receipt) <- requireRight (minkowskiSum concaveRegion kernelRegion)
+         pure (length (planarRegionComponents result), receipt))
+  printMorphologyReceipt "general-minkowski-concave" sumComponents sumReceipt
+  erosionSourceComponents <-
+    requireRight
+      ( traverse
+          rectangleComponent
+          [(0, 0, 6, 6), (9, 0, 15, 6)]
+      )
+  erosionSource <- requireRight (planarRegion erosionSourceComponents)
+  (erosionComponents, erosionReceipt) <-
+    timedValue
+      "general-erosion-disconnected"
+      (do
+         (result, receipt) <- requireRight (erodeBy element erosionSource)
+         pure (length (planarRegionComponents result), receipt))
+  printMorphologyReceipt
+    "general-erosion-disconnected"
+    erosionComponents
+    erosionReceipt
+  if minkowskiOverlayPasses sumReceipt > 0
+      && minkowskiGeneratedPieces sumReceipt > 0
+      && minkowskiOverlayPasses erosionReceipt > 0
+    then pure ()
+    else fail "general morphology bypassed its declared decomposition/overlay work"
+
+printMorphologyReceipt :: String -> Int -> MinkowskiReceipt -> IO ()
+printMorphologyReceipt label outputComponents receipt =
+  putStrLn
+    ( label
+        <> "-receipt: output-components="
+        <> show outputComponents
+        <> " generated-pieces="
+        <> show (minkowskiGeneratedPieces receipt)
+        <> " convolution-edges="
+        <> show (minkowskiGeneratedConvolutionEdges receipt)
+        <> " overlay-passes="
+        <> show (minkowskiOverlayPasses receipt)
+        <> " exact-crossings="
+        <> show (minkowskiExactCrossings receipt)
+        <> " output-cells="
+        <> show (minkowskiOutputCells receipt)
+        <> " coordinate-bit-growth="
+        <> show (minkowskiExactCoordinateBitGrowth receipt)
+    )
+
+convexLens :: Int -> NonEmpty ExactPoint
+convexLens halfSize =
+  let maximumIndex = max 1 (halfSize - 1)
+      height = 2 * maximumIndex * maximumIndex + 1
+      lower =
+        [ integerPoint index (index * index)
+        | index <- [1 .. maximumIndex]
+        ]
+      upper =
+        [ integerPoint index (height - index * index)
+        | index <- reverse [0 .. maximumIndex]
+        ]
+   in integerPoint 0 0 :| (lower <> upper)
+
+familyLayers
+  :: OverlayFamily
+  -> Int
+  -> Either RegionValidationError (PlanarLayer Int, PlanarLayer Int)
+familyLayers family size =
+  case family of
+    DisjointFamily ->
+      (,)
+        <$> layerFromRectangles (disjointRectangles size)
+        <*> planarLayer 0 Map.empty
+    GridCrossingFamily ->
+      (,)
+        <$> layerFromRectangles
+          [ (3 * index, 0, 3 * index + 1, 3 * size - 1)
+          | index <- [0 .. size - 1]
+          ]
+        <*> layerFromRectangles
+          [ (0, 3 * index, 3 * size - 1, 3 * index + 1)
+          | index <- [0 .. size - 1]
+          ]
+    CollinearOverlapFamily ->
+      (,)
+        <$> layerFromRectangles
+          [ (3 * index, 0, 3 * index + 2, 2)
+          | index <- [0 .. size - 1]
+          ]
+        <*> layerFromRectangles
+          [ (3 * index + 1, 0, 3 * index + 3, 1)
+          | index <- [0 .. size - 1]
+          ]
+
+disjointRectangles :: Int -> [(Int, Int, Int, Int)]
+disjointRectangles size =
+  [ (3 * index, 0, 3 * index + 1, 1)
+  | index <- [0 .. size - 1]
+  ]
+{-# INLINE disjointRectangles #-}
+
+touchingCells :: Int -> [(Int, Int, Int, Int)]
+touchingCells side =
+  [ (x, y, x + 1, y + 1)
+  | y <- [0, 2 .. side - 1]
+  , x <- [0 .. side - 1]
+  ]
+    ++ [ (x, y, x + 2, y + 1)
+       | y <- [1, 3 .. side - 1]
+       , x <- [0, 2 .. side - 2]
+       ]
+{-# INLINE touchingCells #-}
+
+layerFromRectangles
+  :: [(Int, Int, Int, Int)]
+  -> Either RegionValidationError (PlanarLayer Int)
+layerFromRectangles rectangles = do
+  components <- traverse rectangleComponent rectangles
+  region <- planarRegion components
+  planarLayer 0 (Map.singleton 1 region)
+
+rectangleComponent
+  :: (Int, Int, Int, Int)
+  -> Either RegionValidationError PolygonComponent
+rectangleComponent (minimumX, minimumY, maximumX, maximumY) = do
+  loop <-
+    exactLoop
+      ( integerPoint minimumX minimumY
+          :| [ integerPoint maximumX minimumY
+             , integerPoint maximumX maximumY
+             , integerPoint minimumX maximumY
+             ]
+      )
+  polygonComponent loop []
+
+integerPoint :: Int -> Int -> ExactPoint
+integerPoint x y =
+  exactPoint
+    (fromIntegral x)
+    (fromIntegral y)
+
+benchmarkPublicationReceipt :: IO ()
+benchmarkPublicationReceipt = do
+  built <-
+    requireRight (delaunay unitElementDefaults (latticePoints 440 272))
+  triangulation <- evaluate (force (buildTriangulation built))
+  published <-
+    timedValue
+      "labelled-planar-layer-publication"
+      (requireRight (labelledPlanarLayer (-1) triangulation (latticeFaceBand triangulation)))
+  let components =
+        concatMap planarRegionComponents (Map.elems (planarLayerRegions published))
+      holeCount = sum (map (length . polygonHoleLoops) components)
+      exactCoordinateCount =
+        sum
+          [ length (exactLoopPoints (polygonOuterLoop component))
+              + sum (map (length . exactLoopPoints) (polygonHoleLoops component))
+          | component <- components
+          ]
+      receipt =
+        ( numInnerFaces triangulation
+        , length components
+        , length components
+        , holeCount
+        , exactCoordinateCount
+        )
+  if receipt == (239_360, 22, 22, 0, 88)
+    then
+      putStrLn
+        "labelled-planar-layer-publication-receipt: faces=239360 components=22 outer-loops=22 holes=0 exact-coordinates=88"
+    else fail ("labelled planar layer receipt mismatch: " <> show receipt)
+  sparseReceipt <-
+    timedValue
+      "exact-cell-set-sparse-selection"
+      (do
+         selected <- requireRight (exactCellSet triangulation [VertexId 0] [] [])
+         pure
+           ( exactCellSetVertexCount selected
+           , exactCellSetEdgeCount selected
+           , exactCellSetFaceCount selected
+           ))
+  if sparseReceipt == (1, 0, 0)
+    then putStrLn "exact-cell-set-sparse-selection-receipt: vertices=1 edges=0 faces=0"
+    else fail ("sparse exact cell selection receipt mismatch: " <> show sparseReceipt)
+
+ceilingLog2 :: Int -> Int
+ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))
diff --git a/bench/region/Moonlight/Triangulation/RegionBench.hs b/bench/region/Moonlight/Triangulation/RegionBench.hs
deleted file mode 100644
--- a/bench/region/Moonlight/Triangulation/RegionBench.hs
+++ /dev/null
@@ -1,443 +0,0 @@
-{-# LANGUAGE NumericUnderscores #-}
-
--- | Exact segment-event, overlay, and grouped-region publication receipts.
--- The source families are closed data; measurement is the only effect.
-module Moonlight.Triangulation.RegionBench (benchmarks) where
-
-import BenchSupport
-  ( latticeFaceBand
-  , latticePoints
-  )
-import BenchMeasure (requireRight, timedValue)
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Data.Foldable (traverse_)
-import qualified Data.Map.Strict as Map
-import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Triangulation
-  ( buildTriangulation
-  , delaunay
-  , unitElementDefaults
-  )
-import Moonlight.Triangulation.CellSet
-  ( exactCellSet
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , exactCellSetVertexCount
-  )
-import Moonlight.Triangulation.Dcel (numInnerFaces)
-import Moonlight.Triangulation.Exact (ExactPoint, exactPoint)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Overlay
-  ( OverlayReceipt (..)
-  , overlayClosedIntersection
-  , overlayLayers
-  , overlayReceipt
-  )
-import Moonlight.Triangulation.Minkowski
-  ( MinkowskiReceipt
-  , convexMinkowskiSum
-  , convexPolygon
-  , erodeBy
-  , minkowskiExactCoordinateBitGrowth
-  , minkowskiExactCrossings
-  , minkowskiGeneratedConvolutionEdges
-  , minkowskiGeneratedPieces
-  , minkowskiOutputCells
-  , minkowskiOverlayPasses
-  , minkowskiSum
-  , structuringElement
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PolygonComponent
-  , RegionValidationError
-  , exactLoop
-  , exactLoopPoints
-  , labelledPlanarLayer
-  , planarLayer
-  , planarLayerRegions
-  , planarRegion
-  , planarRegionComponents
-  , polygonComponent
-  , polygonHoleLoops
-  , polygonOuterLoop
-  )
-import Moonlight.Triangulation.Valuation
-  ( cellValuations
-  , eulerCharacteristicValue
-  , exactLengthTerms
-  , regionValuations
-  , valuationEuler
-  , valuationIntrinsic1
-  , exactLengthExpression
-  )
-
-data OverlayFamily
-  = DisjointFamily
-  | GridCrossingFamily
-  | CollinearOverlapFamily
-  deriving stock (Eq, Ord, Show)
-
-benchmarks :: IO ()
-benchmarks = do
-  traverse_
-    (\family -> traverse_ (benchmarkOverlayFamily family) [2, 4, 8])
-    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
-  benchmarkPublicationReceipt
-  traverse_ benchmarkRegionAuthoring [64, 256, 1_024]
-  traverse_ benchmarkRegionValuation [64, 256, 1_024]
-  traverse_ benchmarkConvexMinkowski [8, 32, 128, 512]
-  traverse_
-    (\family -> traverse_ (benchmarkOverlaySelectorFamily family) [2, 4, 8])
-    [DisjointFamily, GridCrossingFamily, CollinearOverlapFamily]
-  benchmarkGeneralMorphology
-
-benchmarkOverlayFamily :: OverlayFamily -> Int -> IO ()
-benchmarkOverlayFamily family size = do
-  layers <- requireRight (familyLayers family size)
-  result <-
-    timedValue
-      (familyName family <> "-n" <> show size)
-      (requireRight (uncurry overlayLayers layers))
-  let receipt = overlayReceipt result
-      inputAndOutput = overlayInputSegments receipt + overlayRelationEvents receipt
-      logarithmicScale = max 1 (ceilingLog2 (overlayInputSegments receipt + 1))
-      totalLimit = 128 * inputAndOutput * logarithmicScale
-  putStrLn
-    ( familyName family
-        <> "-receipt: n="
-        <> show size
-        <> " source-segments="
-        <> show (overlayInputSegments receipt)
-        <> " relation-events-k="
-        <> show (overlayRelationEvents receipt)
-        <> " exact-relation-checks="
-        <> show (overlayTotalRelationChecks receipt)
-        <> " atomic-edges="
-        <> show (overlayAtomicEdges receipt)
-        <> " arrangement-cells="
-        <> show (overlayArrangementCells receipt)
-        <> " resident-faces="
-        <> show (overlayResidentFaces receipt)
-        <> " avl-height="
-        <> show (overlaySweepMaximumHeight receipt)
-    )
-  if overlayTotalRelationChecks receipt <= totalLimit
-    then pure ()
-    else
-      fail
-        ( familyName family
-            <> " retained superlinear total relation work: "
-            <> show (overlayTotalRelationChecks receipt, totalLimit)
-        )
-
-benchmarkOverlaySelectorFamily :: OverlayFamily -> Int -> IO ()
-benchmarkOverlaySelectorFamily family size = do
-  layers <- requireRight (familyLayers family size)
-  result <- requireRight (uncurry overlayLayers layers)
-  selectedReceipt <-
-    timedValue
-      (familyName family <> "-selector-n" <> show size)
-      (do
-         selected <-
-           requireRight
-             (overlayClosedIntersection (== 1) (== 1) result)
-         valuations <- requireRight (cellValuations selected)
-         pure
-           ( exactCellSetVertexCount selected
-           , exactCellSetEdgeCount selected
-           , exactCellSetFaceCount selected
-           , eulerCharacteristicValue (valuationEuler valuations)
-           , length
-               ( exactLengthTerms
-                   (exactLengthExpression (valuationIntrinsic1 valuations))
-               )
-           ))
-  putStrLn
-    ( familyName family
-        <> "-selector-receipt: n="
-        <> show size
-        <> " cells="
-        <> show selectedReceipt
-    )
-
-familyName :: OverlayFamily -> String
-familyName DisjointFamily = "overlay-disjoint"
-familyName GridCrossingFamily = "overlay-grid-crossing"
-familyName CollinearOverlapFamily = "overlay-collinear-overlap"
-
-benchmarkRegionAuthoring :: Int -> IO ()
-benchmarkRegionAuthoring size = do
-  componentCount <-
-    timedValue
-      ("planar-region-disjoint-authoring-n" <> show size)
-      (requireRight $ do
-         components <-
-           traverse
-             rectangleComponent
-             (disjointRectangles size)
-         length . planarRegionComponents <$> planarRegion components)
-  if componentCount == size
-    then
-      putStrLn
-        ( "planar-region-disjoint-authoring-receipt: n="
-            <> show size
-            <> " components="
-            <> show componentCount
-        )
-    else fail ("planar region authoring lost components: " <> show componentCount)
-
-benchmarkRegionValuation :: Int -> IO ()
-benchmarkRegionValuation size = do
-  components <-
-    requireRight
-      ( traverse
-          rectangleComponent
-          (disjointRectangles size)
-      )
-  region <- requireRight (planarRegion components)
-  valuations <-
-    timedValue
-      ("planar-region-valuations-n" <> show size)
-      (requireRight (regionValuations region))
-  let receipt =
-        ( eulerCharacteristicValue (valuationEuler valuations)
-        , length
-            ( exactLengthTerms
-                (exactLengthExpression (valuationIntrinsic1 valuations))
-            )
-        )
-  if receipt == (size, 1)
-    then
-      putStrLn
-        ( "planar-region-valuations-receipt: n="
-            <> show size
-            <> " euler="
-            <> show size
-            <> " radical-terms=1"
-        )
-    else fail ("planar region valuation receipt mismatch: " <> show receipt)
-
-benchmarkConvexMinkowski :: Int -> IO ()
-benchmarkConvexMinkowski halfSize = do
-  left <- requireRight (convexPolygon (convexLens halfSize))
-  right <- requireRight (convexPolygon (convexLens halfSize))
-  result <-
-    timedValue
-      ("convex-minkowski-n" <> show (2 * halfSize))
-      (evaluate (convexMinkowskiSum left right))
-  let outputVertices =
-        sum
-          [ length (exactLoopPoints (polygonOuterLoop component))
-          | component <- planarRegionComponents result
-          ]
-      inputVertices = 4 * halfSize
-  if outputVertices <= inputVertices
-    then
-      putStrLn
-        ( "convex-minkowski-receipt: input-vertices="
-            <> show inputVertices
-            <> " output-vertices="
-            <> show outputVertices
-        )
-    else fail ("convex Minkowski output exceeded n+m: " <> show (outputVertices, inputVertices))
-
-benchmarkGeneralMorphology :: IO ()
-benchmarkGeneralMorphology = do
-  concaveLoop <-
-    requireRight
-      ( exactLoop
-          ( integerPoint 0 0
-              :| [ integerPoint 6 0
-                 , integerPoint 6 2
-                 , integerPoint 2 2
-                 , integerPoint 2 6
-                 , integerPoint 0 6
-                 ]
-          )
-      )
-  concaveComponent <- requireRight (polygonComponent concaveLoop [])
-  concaveRegion <- requireRight (planarRegion [concaveComponent])
-  kernelComponent <- requireRight (rectangleComponent (-1, -1, 1, 1))
-  kernelRegion <- requireRight (planarRegion [kernelComponent])
-  kernelPolygon <-
-    requireRight
-      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
-  element <- requireRight (structuringElement kernelPolygon)
-  (sumComponents, sumReceipt) <-
-    timedValue
-      "general-minkowski-concave"
-      (do
-         (result, receipt) <- requireRight (minkowskiSum concaveRegion kernelRegion)
-         pure (length (planarRegionComponents result), receipt))
-  printMorphologyReceipt "general-minkowski-concave" sumComponents sumReceipt
-  erosionSourceComponents <-
-    requireRight
-      ( traverse
-          rectangleComponent
-          [(0, 0, 6, 6), (9, 0, 15, 6)]
-      )
-  erosionSource <- requireRight (planarRegion erosionSourceComponents)
-  (erosionComponents, erosionReceipt) <-
-    timedValue
-      "general-erosion-disconnected"
-      (do
-         (result, receipt) <- requireRight (erodeBy element erosionSource)
-         pure (length (planarRegionComponents result), receipt))
-  printMorphologyReceipt
-    "general-erosion-disconnected"
-    erosionComponents
-    erosionReceipt
-  if minkowskiOverlayPasses sumReceipt > 0
-      && minkowskiGeneratedPieces sumReceipt > 0
-      && minkowskiOverlayPasses erosionReceipt > 0
-    then pure ()
-    else fail "general morphology bypassed its declared decomposition/overlay work"
-
-printMorphologyReceipt :: String -> Int -> MinkowskiReceipt -> IO ()
-printMorphologyReceipt label outputComponents receipt =
-  putStrLn
-    ( label
-        <> "-receipt: output-components="
-        <> show outputComponents
-        <> " generated-pieces="
-        <> show (minkowskiGeneratedPieces receipt)
-        <> " convolution-edges="
-        <> show (minkowskiGeneratedConvolutionEdges receipt)
-        <> " overlay-passes="
-        <> show (minkowskiOverlayPasses receipt)
-        <> " exact-crossings="
-        <> show (minkowskiExactCrossings receipt)
-        <> " output-cells="
-        <> show (minkowskiOutputCells receipt)
-        <> " coordinate-bit-growth="
-        <> show (minkowskiExactCoordinateBitGrowth receipt)
-    )
-
-convexLens :: Int -> NonEmpty ExactPoint
-convexLens halfSize =
-  let maximumIndex = max 1 (halfSize - 1)
-      height = 2 * maximumIndex * maximumIndex + 1
-      lower =
-        [ integerPoint index (index * index)
-        | index <- [1 .. maximumIndex]
-        ]
-      upper =
-        [ integerPoint index (height - index * index)
-        | index <- reverse [0 .. maximumIndex]
-        ]
-   in integerPoint 0 0 :| (lower <> upper)
-
-familyLayers
-  :: OverlayFamily
-  -> Int
-  -> Either RegionValidationError (PlanarLayer Int, PlanarLayer Int)
-familyLayers family size =
-  case family of
-    DisjointFamily ->
-      (,)
-        <$> layerFromRectangles (disjointRectangles size)
-        <*> planarLayer 0 Map.empty
-    GridCrossingFamily ->
-      (,)
-        <$> layerFromRectangles
-          [ (3 * index, 0, 3 * index + 1, 3 * size - 1)
-          | index <- [0 .. size - 1]
-          ]
-        <*> layerFromRectangles
-          [ (0, 3 * index, 3 * size - 1, 3 * index + 1)
-          | index <- [0 .. size - 1]
-          ]
-    CollinearOverlapFamily ->
-      (,)
-        <$> layerFromRectangles
-          [ (3 * index, 0, 3 * index + 2, 2)
-          | index <- [0 .. size - 1]
-          ]
-        <*> layerFromRectangles
-          [ (3 * index + 1, 0, 3 * index + 3, 1)
-          | index <- [0 .. size - 1]
-          ]
-
-disjointRectangles :: Int -> [(Int, Int, Int, Int)]
-disjointRectangles size =
-  [ (3 * index, 0, 3 * index + 1, 1)
-  | index <- [0 .. size - 1]
-  ]
-{-# INLINE disjointRectangles #-}
-
-layerFromRectangles
-  :: [(Int, Int, Int, Int)]
-  -> Either RegionValidationError (PlanarLayer Int)
-layerFromRectangles rectangles = do
-  components <- traverse rectangleComponent rectangles
-  region <- planarRegion components
-  planarLayer 0 (Map.singleton 1 region)
-
-rectangleComponent
-  :: (Int, Int, Int, Int)
-  -> Either RegionValidationError PolygonComponent
-rectangleComponent (minimumX, minimumY, maximumX, maximumY) = do
-  loop <-
-    exactLoop
-      ( integerPoint minimumX minimumY
-          :| [ integerPoint maximumX minimumY
-             , integerPoint maximumX maximumY
-             , integerPoint minimumX maximumY
-             ]
-      )
-  polygonComponent loop []
-
-integerPoint :: Int -> Int -> ExactPoint
-integerPoint x y =
-  exactPoint
-    (fromIntegral x)
-    (fromIntegral y)
-
-benchmarkPublicationReceipt :: IO ()
-benchmarkPublicationReceipt = do
-  built <-
-    requireRight (delaunay unitElementDefaults (latticePoints 440 272))
-  triangulation <- evaluate (force (buildTriangulation built))
-  published <-
-    timedValue
-      "labelled-planar-layer-publication"
-      (requireRight (labelledPlanarLayer (-1) triangulation (latticeFaceBand triangulation)))
-  let components =
-        concatMap planarRegionComponents (Map.elems (planarLayerRegions published))
-      holeCount = sum (map (length . polygonHoleLoops) components)
-      exactCoordinateCount =
-        sum
-          [ length (exactLoopPoints (polygonOuterLoop component))
-              + sum (map (length . exactLoopPoints) (polygonHoleLoops component))
-          | component <- components
-          ]
-      receipt =
-        ( numInnerFaces triangulation
-        , length components
-        , length components
-        , holeCount
-        , exactCoordinateCount
-        )
-  if receipt == (239_360, 22, 22, 0, 88)
-    then
-      putStrLn
-        "labelled-planar-layer-publication-receipt: faces=239360 components=22 outer-loops=22 holes=0 exact-coordinates=88"
-    else fail ("labelled planar layer receipt mismatch: " <> show receipt)
-  sparseReceipt <-
-    timedValue
-      "exact-cell-set-sparse-selection"
-      (do
-         selected <- requireRight (exactCellSet triangulation [VertexId 0] [] [])
-         pure
-           ( exactCellSetVertexCount selected
-           , exactCellSetEdgeCount selected
-           , exactCellSetFaceCount selected
-           ))
-  if sparseReceipt == (1, 0, 0)
-    then putStrLn "exact-cell-set-sparse-selection-receipt: vertices=1 edges=0 faces=0"
-    else fail ("sparse exact cell selection receipt mismatch: " <> show sparseReceipt)
-
-ceilingLog2 :: Int -> Int
-ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))
diff --git a/bench/serialization/Main.hs b/bench/serialization/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/serialization/Main.hs
@@ -0,0 +1,78 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Main (main) where
+
+import BenchMeasure (requireRight, timedValue)
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import Control.Monad (unless)
+import Data.Binary (Binary)
+import Data.Bifunctor (first)
+import qualified Data.ByteString.Lazy as BL
+import Data.Foldable (traverse_)
+import Moonlight.Planar.Internal.Paged (pagedOverlayPageCount)
+import Moonlight.Planar.Internal.Representation
+  ( Triangulation (..), mapVertices, mapDirectedEdges, mapUndirectedEdges, mapFaces )
+import Moonlight.Planar.Internal.Types (KnownConstraintMode)
+import Moonlight.Planar.Internal.Validation (validateTopology)
+import Moonlight.Planar.Serialization
+  ( DecodingBudget (..), decodeTriangulation, encodeTriangulation, trustedBinaryPayloadDecoders )
+import Moonlight.Planar.SerializationFixtures
+  ( SerializationFixture, serializationFixtures, serializationGridFixtures )
+import Moonlight.Planar.SerializationV6Oracle (encodeV6Gathered)
+import System.Mem (performGC)
+import Test.Tasty.Bench (Benchmark, bench, bgroup, defaultMain, nf)
+
+main :: IO ()
+main = do
+  collinearFixtures <- concat <$> traverse prepare [10242, 32770]
+  gridFixtures <- requireRight (serializationGridFixtures 10242)
+  fixtures <- evaluate (force (collinearFixtures <> [("grid/10242/" <> label, mesh) | (label, mesh) <- gridFixtures]))
+  unitFixtures <-
+    evaluate . force $
+      [ (label <> "/unit", mapVertices (const ()) . mapDirectedEdges (const ()) . mapUndirectedEdges (const ()) . mapFaces (const ()) $ mesh)
+      | (label, mesh) <- fixtures
+      ]
+  traverse_ report fixtures
+  traverse_ report unitFixtures
+  defaultMain (map benchmarkPair fixtures <> map benchmarkPair unitFixtures)
+ where
+  prepare :: Int -> IO [(String, SerializationFixture)]
+  prepare count = do
+    fixtures <- requireRight (serializationFixtures count)
+    evaluate (force [(show count <> "/" <> label, mesh) | (label, mesh) <- fixtures])
+
+  benchmarkPair
+    :: forall mode vertex directed undirected face.
+       ( KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face
+       , NFData vertex, NFData directed, NFData undirected, NFData face )
+    => (String, Triangulation mode vertex directed undirected face)
+    -> Benchmark
+  benchmarkPair (label, mesh) =
+    bgroup label
+      [ bench "gathered-v6-oracle" (nf encodeV6Gathered mesh)
+      , bench "pagewise-v6" (nf encodeTriangulation mesh)
+      , bench "decode-v6"
+          (nf (first show . decodeTriangulation @mode @vertex @directed @undirected @face
+            (DecodingBudget 100_000_000 100_000_000) trustedBinaryPayloadDecoders)
+            (encodeTriangulation mesh))
+      , bench "validate-topology" (nf validateTopology mesh)
+      , bench "map-vertices" (nf (mapVertices (\value -> (value, True))) mesh)
+      , bench "map-faces" (nf (mapFaces (\value -> (value, True))) mesh)
+      ]
+
+  report
+    :: (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
+    => (String, Triangulation mode vertex directed undirected face)
+    -> IO ()
+  report (label, mesh) = do
+    performGC
+    gathered <- timedValue (label <> "/gathered-v6-oracle") (pure (encodeV6Gathered mesh))
+    performGC
+    pagewise <- timedValue (label <> "/pagewise-v6") (pure (encodeTriangulation mesh))
+    unless (gathered == pagewise) (fail (label <> ": V6 bytes differ"))
+    putStrLn
+      ( label <> " bytes=" <> show (BL.length pagewise)
+          <> " shared-topology-pages=" <> show (pagedOverlayPageCount (triHalfTopology mesh))
+      )
diff --git a/bench/spade-compare/hs-alpha/Main.hs b/bench/spade-compare/hs-alpha/Main.hs
--- a/bench/spade-compare/hs-alpha/Main.hs
+++ b/bench/spade-compare/hs-alpha/Main.hs
@@ -4,19 +4,19 @@
 import Control.DeepSeq (force)
 import Control.Exception (evaluate)
 import Data.Vector qualified as Vector
-import Moonlight.Triangulation.Bench.SpadeCompare.Alpha
+import Moonlight.Planar.Bench.SpadeCompare.Alpha
   ( alphaPersistenceReceipt
   , renderAlphaPersistenceReceipt
   )
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-import Moonlight.Triangulation.Bench.SpadeCompare.Lane
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Planar.Bench.SpadeCompare.Lane
   ( LaneKind (AlphaPersistenceLane)
   , LaneObstruction
   , LaneRequest (..)
   , parseLaneRequest
   , renderLaneObstruction
   )
-import Moonlight.Triangulation.Bench.SpadeCompare.Timing (measureLane)
+import Moonlight.Planar.Bench.SpadeCompare.Timing (measureLane)
 import System.Environment (getArgs)
 import System.Exit (ExitCode (ExitFailure), exitWith)
 import System.IO (hPutStrLn, stderr)
@@ -62,5 +62,5 @@
 
 refuseUsage :: IO value
 refuseUsage = do
-  hPutStrLn stderr "usage: moonlight-triangulation-alpha-spade-referent (alpha-receipt COUNT | bench-one alpha-persistence COUNT 0)"
+  hPutStrLn stderr "usage: moonlight-planar-alpha-spade-referent (alpha-receipt COUNT | bench-one alpha-persistence COUNT 0)"
   exitWith (ExitFailure 2)
diff --git a/bench/spade-compare/hs/Main.hs b/bench/spade-compare/hs/Main.hs
--- a/bench/spade-compare/hs/Main.hs
+++ b/bench/spade-compare/hs/Main.hs
@@ -1,6 +1,6 @@
 -- | Haskell half of the spade external referent.
 --
--- @moonlight-triangulation@ is a port of the Rust @spade@ crate, so spade is the
+-- @moonlight-planar@ is a port of the Rust @spade@ crate, so spade is the
 -- referent that grades it. Both halves generate their inputs from the same LCG
 -- with the same seeds and emit their gate output in the same encoding, so
 -- neither side can reformat a disagreement into agreement.
@@ -16,32 +16,36 @@
 import Control.Exception (evaluate)
 import Control.Monad.ST (runST)
 import Data.Word (Word64)
-import Moonlight.Triangulation.Internal.Mutable (freezeTriangulation, thawTriangulation)
+import Moonlight.Planar.Internal.Mutable (freezeTriangulation, thawTriangulation)
 import System.Environment (getArgs)
 import System.Exit (exitWith, ExitCode (ExitFailure))
 import System.IO (hPutStrLn, stderr)
 import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad
-import Moonlight.Triangulation.Cdt
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.HintGenerator
-import Moonlight.Triangulation.Interpolation
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Bench.SpadeCompare.Constraint
-import Moonlight.Triangulation.Bench.SpadeCompare.Dcel
-import Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
-import Moonlight.Triangulation.Bench.SpadeCompare.Gates
-import Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
-import Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
-import Moonlight.Triangulation.Bench.SpadeCompare.Intersection
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-import Moonlight.Triangulation.Bench.SpadeCompare.Lane
-import Moonlight.Triangulation.Bench.SpadeCompare.Removal
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Timing (measureLane)
-import Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
+import Moonlight.Planar.BulkLoad
+import Moonlight.Planar.Cdt
+import Moonlight.Planar.Dcel (numVertices)
+import Moonlight.Planar.Handles.HandleDefs (VertexId)
+import Moonlight.Planar.PointLocation
+import Moonlight.Planar.Refinement (refine)
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Telemetry (LocationHint (..), insertMany, nearestNeighbor)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.HintGenerator
+import Moonlight.Planar.Interpolation
+import Moonlight.Planar.Bench.SpadeCompare.Constraint
+import Moonlight.Planar.Bench.SpadeCompare.Dcel
+import Moonlight.Planar.Bench.SpadeCompare.Delaunay
+import Moonlight.Planar.Bench.SpadeCompare.Gates
+import Moonlight.Planar.Bench.SpadeCompare.Hierarchy
+import Moonlight.Planar.Bench.SpadeCompare.Interpolation
+import Moonlight.Planar.Bench.SpadeCompare.Intersection
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Planar.Bench.SpadeCompare.Lane
+import Moonlight.Planar.Bench.SpadeCompare.Removal
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Timing (measureLane)
+import Moonlight.Planar.Bench.SpadeCompare.Voronoi
 
 main :: IO ()
 main = do
@@ -66,7 +70,7 @@
     ["interp-locate-audit", count, queries] -> reportInterpolationLocateAudit (read count) (read queries)
     ["interp-cavity-audit", count, queries] -> reportInterpolationCavityAudit (read count) (read queries)
     _ -> do
-      hPutStrLn stderr "usage: moonlight-triangulation-spade-referent (gate DIR | divergence FILE | inventory-csv | inventory-human | lane-specs CLASS | snapshot-specs | bench-one LANE A B | counters BUDGET | sweep-stats COUNT | removal-audit COUNT REMOVALS | removal-context COUNT INDEX | fresh-suffix COUNT DROPPED PATH | interp-audit COUNT QUERIES | interp-context COUNT [INDEX])"
+      hPutStrLn stderr "usage: moonlight-planar-spade-referent (gate DIR | divergence FILE | inventory-csv | inventory-human | lane-specs CLASS | snapshot-specs | bench-one LANE A B | counters BUDGET | sweep-stats COUNT | removal-audit COUNT REMOVALS | removal-context COUNT INDEX | fresh-suffix COUNT DROPPED PATH | interp-audit COUNT QUERIES | interp-context COUNT [INDEX])"
       exitWith (ExitFailure 2)
 
 refuseLane :: LaneObstruction -> IO value
@@ -237,13 +241,7 @@
   RefineLane -> do
     let (points, constraints) = refinementInput
     cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
-    let parameters = defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just first
-          , refineMaxArea = Just 0.5
-          , refineMaxRadiusEdgeRatio = Just 1.0
-          , refineExcludeOuterFaces = True
-          , refineKeepConstraintEdges = False
-          }
+    parameters <- require (refinementParameters first)
     measure (refinementAddedVertices <$> require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point)))
  where
   reopenTriangulation state _ = do
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Alpha.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Alpha.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Alpha.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Matched end-to-end alpha-persistence work and its canonical receipt.
+-- Input generation remains outside the timed action; Delaunay construction,
+-- exact alpha births, cellular lowering, one reduction, and all critical-birth
+-- Betti queries remain inside it.
+module Moonlight.Planar.Bench.SpadeCompare.Alpha
+  ( AlphaPersistenceObstruction (..)
+  , AlphaPersistenceReceipt (..)
+  , alphaPersistenceReceipt
+  , renderAlphaPersistenceReceipt
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.Bits (xor)
+import Data.List (intercalate)
+import Data.Vector (Vector)
+import Data.Vector.Unboxed qualified as UnboxedVector
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+import Moonlight.Homology.Boundary
+  ( degreeCardinality
+  )
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..)
+  , HomologyFailure
+  , PersistencePair (..)
+  )
+import Moonlight.Homology.Persistence
+  ( criticalBettiTableValues
+  , filteredBaseComplex
+  , filteredCriticalValues
+  , mod2PersistentPairsWithCriticalBettiTable
+  )
+import Moonlight.Planar.Alpha
+  ( AlphaFiltrationError
+  , alphaFiltration
+  )
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.CellComplex
+  ( DCELError
+  , filteredAlphaComplex
+  )
+import Moonlight.Planar.Types (BuildError)
+import Moonlight.Planar.Point (Point)
+
+data AlphaPersistenceObstruction
+  = AlphaPersistenceBuildRefused !BuildError
+  | AlphaPersistenceBirthRefused !AlphaFiltrationError
+  | AlphaPersistenceLoweringRefused !DCELError
+  | AlphaPersistenceReductionRefused !HomologyFailure
+  deriving stock (Eq, Show)
+
+data AlphaPersistenceReceipt = AlphaPersistenceReceipt
+  { alphaReceiptCriticalBirths :: !Int
+  , alphaReceiptVertexCells :: !Int
+  , alphaReceiptEdgeCells :: !Int
+  , alphaReceiptFaceCells :: !Int
+  , alphaReceiptFinitePairs :: !Int
+  , alphaReceiptEssentialPairs :: !Int
+  , alphaReceiptProfileChecksum :: !Word64
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+alphaPersistenceReceipt
+  :: Vector Point
+  -> Either AlphaPersistenceObstruction AlphaPersistenceReceipt
+alphaPersistenceReceipt points = do
+  triangulation <-
+    first AlphaPersistenceBuildRefused (delaunayGeometry points)
+  filtration <-
+    first AlphaPersistenceBirthRefused (alphaFiltration triangulation)
+  filtered <-
+    first AlphaPersistenceLoweringRefused (filteredAlphaComplex filtration)
+  (pairs, criticalBettiTable) <-
+    first AlphaPersistenceReductionRefused
+      (mod2PersistentPairsWithCriticalBettiTable filtered)
+  let criticalBirths = filteredCriticalValues filtered
+      finite = filteredBaseComplex filtered
+      isFinitePair = maybe False (const True) . persistenceDeath
+  pure
+    AlphaPersistenceReceipt
+      { alphaReceiptCriticalBirths = length criticalBirths
+      , alphaReceiptVertexCells = degreeCardinality finite (HomologicalDegree 0)
+      , alphaReceiptEdgeCells = degreeCardinality finite (HomologicalDegree 1)
+      , alphaReceiptFaceCells = degreeCardinality finite (HomologicalDegree 2)
+      , alphaReceiptFinitePairs = length (filter isFinitePair pairs)
+      , alphaReceiptEssentialPairs = length (filter (not . isFinitePair) pairs)
+      , alphaReceiptProfileChecksum =
+          UnboxedVector.foldl'
+            mixChecksum
+            fnvOffset
+            (criticalBettiTableValues criticalBettiTable)
+      }
+
+mixChecksum :: Word64 -> Int -> Word64
+mixChecksum accumulator value =
+  (accumulator `xor` fromIntegral value) * fnvPrime
+
+fnvOffset :: Word64
+fnvOffset = 14695981039346656037
+
+fnvPrime :: Word64
+fnvPrime = 1099511628211
+
+renderAlphaPersistenceReceipt :: AlphaPersistenceReceipt -> String
+renderAlphaPersistenceReceipt receipt =
+  intercalate
+    ","
+    ( fmap
+        show
+        [ fromIntegral (alphaReceiptCriticalBirths receipt) :: Word64
+        , fromIntegral (alphaReceiptVertexCells receipt)
+        , fromIntegral (alphaReceiptEdgeCells receipt)
+        , fromIntegral (alphaReceiptFaceCells receipt)
+        , fromIntegral (alphaReceiptFinitePairs receipt)
+        , fromIntegral (alphaReceiptEssentialPairs receipt)
+        , alphaReceiptProfileChecksum receipt
+        ]
+    )
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Constraint.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Constraint.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Constraint.hs
@@ -0,0 +1,170 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Constraint recovery and splitting lanes and their gates.
+module Moonlight.Planar.Bench.SpadeCompare.Constraint where
+
+import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Cdt
+import Moonlight.Planar.Dcel (previous)
+import Moonlight.Planar.Handles.HandleDefs (VertexId)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.SetAlgebra (intersection)
+import Moonlight.Planar.Types (ConstrainedDelaunayTriangulation, buildInputVertices, buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+-- | Disjoint split cells, stacked in y: the points, the vertical constraints
+-- the build starts from, and the crossing requests. Band @i@ carries a
+-- constraint from (16, 4i) to (16, 4i + 2) and a crossing request from
+-- (0, 4i + a) to (32, 4i + b), with a and b jittered inside the band, so every
+-- request crosses exactly one constraint and no request reaches another band.
+--
+-- The geometry is chosen so the split point is not merely close on the two
+-- sides but identical. spade solves the two line equations by Cramer's rule and
+-- this side walks the parametric form; here every coefficient either vanishes
+-- or is a power of two, both reduce to one rounding of @u + (v - u) / 2@, and
+-- neither can round it differently. A generic crossing would leave the two
+-- formulas free to disagree in the last bit and there would be nothing to gate.
+splitBands :: Int -> (V.Vector Point, V.Vector (Int, Int), [(Int, Int)])
+splitBands bandCount =
+  ( V.fromList (concatMap bandPoints (zip [0 ..] jitter))
+  , V.fromList [(4 * index, 4 * index + 1) | index <- [0 .. bandCount - 1]]
+  , [(4 * index + 2, 4 * index + 3) | index <- [0 .. bandCount - 1]]
+  )
+ where
+  jitter = randomPoints 0x51ed270b7c1fd1a3 bandCount
+
+  bandPoints (index, Point offsetX offsetY) =
+    let base = 4 * fromIntegral (index :: Int)
+     in [ Point 16 base
+        , Point 16 (base + 2)
+        , Point 0 (base + 1 + 0.5 * offsetX)
+        , Point 32 (base + 1 + 0.5 * offsetY)
+        ]
+
+-- | The same constraint program the batch recovery runs, one request at a time
+-- through the persistent singleton entry point. A request whose corridor is
+-- blocked by an existing constraint is refused and the previous triangulation
+-- stands, which is what the strict side's 'can_add_constraint' guard arranges.
+addConstraintsIncrementally
+  :: ConstrainedDelaunayTriangulation Point
+  -> [(Point, Point)]
+  -> (ConstrainedDelaunayTriangulation Point, [Int])
+addConstraintsIncrementally base requests =
+  case foldl' step (base, []) (zip [0 ..] requests) of
+    (triangulation, accepted) -> (triangulation, reverse accepted)
+ where
+  step (triangulation, accepted) (index, (from, to)) =
+    case addConstraintEdge triangulation from to of
+      Left (ConstraintIntersection _) -> (triangulation, accepted)
+      Left failure -> error (show failure)
+      Right result -> (constraintRecoveryTriangulation result, index : accepted)
+
+-- | Splitting requests, one per band, carried by the batch verb: one
+-- transaction for every corridor, published once -- the shape of the strict
+-- side's fold over one mutable CDT, which publishes nothing between splits.
+-- Every crossed constraint is split at the intersection rather than refused,
+-- so every request is accepted. Splitting appends vertices and never removes
+-- one, so the handles resolved before the first request stay valid through
+-- the last.
+splitConstraints
+  :: ConstrainedDelaunayTriangulation Point
+  -> [(VertexId, VertexId)]
+  -> ConstrainedDelaunayTriangulation Point
+splitConstraints base requests =
+  case addConstraintsAndSplit id base (V.fromList requests) of
+    Left failure -> error (show failure)
+    Right result -> constraintRecoveryTriangulation result
+
+-- | The singleton constraint program's accepted request indices and final
+-- constrained edge set. A blocked request must be refused at the same index on
+-- both sides, or the two are not running the same program.
+writeConstraintIncrementalGate :: FilePath -> Int -> Int -> IO ()
+writeConstraintIncrementalGate directory pointCount constraintCount = do
+  let points = V.fromList (randomPoints 0x94d049bb133111eb pointCount)
+      requests =
+        [ (points V.! fromIndex, points V.! toIndex)
+        | (fromIndex, toIndex) <- constraintPairs pointCount constraintCount
+        ]
+  base <- fromDelaunay . buildTriangulation <$> require (delaunay unitElementDefaults points)
+  let (recovered, accepted) = addConstraintsIncrementally base requests
+      prefix =
+        directory
+          </> ("constraint-incremental-" <> show pointCount <> "-" <> show constraintCount)
+  writeFile (prefix <> "-accepted.txt") (unlines (map show accepted))
+  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges recovered))
+
+-- | Both the split vertices and the constrained edges they carve. The edge set
+-- names every vertex by coordinate, so a split point that landed one ulp away
+-- on one side shows up here rather than hiding behind a matching edge count.
+writeConstraintSplitGate :: FilePath -> Int -> IO ()
+writeConstraintSplitGate directory bandCount = do
+  (base, handles) <- splitBandCdt bandCount
+  let split = splitConstraints base handles
+      prefix = directory </> ("constraint-split-" <> show bandCount)
+  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges split))
+  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges split))
+
+-- | The banded CDT and the crossing requests named by handle. Handle resolution
+-- goes through the build's own input mapping rather than a locate, which is the
+-- constraint gate's convention; the strict side has no such mapping and locates
+-- instead. Either way both name the vertex standing at the same coordinate.
+splitBandCdt
+  :: Int
+  -> IO (ConstrainedDelaunayTriangulation Point, [(VertexId, VertexId)])
+splitBandCdt bandCount = do
+  let (points, constraints, crossings) = splitBands bandCount
+  built <- require (constrainedDelaunay unitElementDefaults points constraints)
+  let mapping = buildInputVertices built
+      len = sizeofPrimArray mapping
+      resolve index
+        | index >= 0 && index < len = VertexId (indexPrimArray mapping index)
+        | otherwise = error ("split band endpoint is out of range: " <> show index)
+  pure
+    ( buildTriangulation built
+    , [(resolve fromIndex, resolve toIndex) | (fromIndex, toIndex) <- crossings]
+    )
+
+writeConstraintGate :: FilePath -> IO ()
+writeConstraintGate directory = do
+  let points = V.fromList (randomPoints 0x94d049bb133111eb 8000)
+      requestIndices = V.fromList (constraintPairs 8000 800)
+  built <- require (delaunay unitElementDefaults points)
+  let mapping = buildInputVertices built
+      len = sizeofPrimArray mapping
+  requests <-
+    V.mapM
+      (\(fromIndex, toIndex) ->
+        let mFrom = if fromIndex >= 0 && fromIndex < len then Just (VertexId (indexPrimArray mapping fromIndex)) else Nothing
+            mTo = if toIndex >= 0 && toIndex < len then Just (VertexId (indexPrimArray mapping toIndex)) else Nothing
+        in case (mFrom, mTo) of
+          (Just from, Just to) -> pure (from, to)
+          _ -> fail "constraint gate endpoint is out of range"
+      )
+      requestIndices
+  batch <-
+    require
+      ( recoverConstraints
+          (fromDelaunay (buildTriangulation built))
+          requests
+      )
+  let accepted =
+        V.ifoldr
+          (\index outcome indices ->
+            case outcome of
+              ConstraintAccepted _ _ -> index : indices
+              ConstraintRejected _ -> indices
+          )
+          []
+          (constraintBatchOutcomes batch)
+  writeFile
+    (directory </> "cdt-accepted-8000-800.txt")
+    (unlines (map show accepted))
+  writeFile
+    (directory </> "cdt-constraints-8000-800.txt")
+    (unlines (canonicalConstraintEdges (constraintBatchTriangulation batch)))
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Dcel.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Dcel.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Dcel.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The primal DCEL traversal gate and the walk lane.
+module Moonlight.Planar.Bench.SpadeCompare.Dcel where
+
+import Data.List (sort)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (allFaces, directedEdges, innerFaces, undirectedEdges, vertices)
+import Moonlight.Planar.Handles.Iterators.HullIterator (hullEdges)
+import System.FilePath ((</>))
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Bench.SpadeCompare.Support
+
+-- | Every iterator over the mesh that spade also offers. The hull cycle and
+-- each vertex's counterclockwise link are cycles with no determined starting
+-- point and are rotated; the inner faces are a set and are sorted. The
+-- enumerator lengths are stated because they are the only cross-comparable
+-- claim the fixed and dynamic iterators make: their elements are handles, and
+-- handles do not cross implementations.
+writeDcelWalkGate :: FilePath -> Int -> IO ()
+writeDcelWalkGate directory count = do
+  triangulation <- delaunayOf count
+  writeFile
+    (directory </> ("dcel-hull-" <> show count <> ".txt"))
+    (unlines (hullGateLines triangulation))
+  writeFile
+    (directory </> ("dcel-circular-" <> show count <> ".txt"))
+    (unlines (circularGateLines triangulation))
+  writeFile
+    (directory </> ("dcel-faces-" <> show count <> ".txt"))
+    (unlines (innerFaceGateLines triangulation))
+
+hullGateLines :: DelaunayTriangulation Point -> [String]
+hullGateLines triangulation =
+  ("hull-size " <> show (length ring)) : rotateToSmallest ring
+ where
+  ring =
+    [ coordHex (vertexPoint triangulation (origin triangulation edge))
+        <> coordHex (vertexPoint triangulation (destination triangulation edge))
+    | edge <- hullEdges triangulation
+    ]
+
+circularGateLines :: DelaunayTriangulation Point -> [String]
+circularGateLines triangulation =
+  [ "vertices " <> show (length (vertices triangulation))
+  , "directed-edges " <> show (length (directedEdges triangulation))
+  , "undirected-edges " <> show (length (undirectedEdges triangulation))
+  , "all-faces " <> show (length (allFaces triangulation))
+  , "inner-faces " <> show (length (innerFaces triangulation))
+  ]
+    <> sort
+      [ coordHex (vertexPoint triangulation vertex) <> concat (rotateToSmallest ring)
+      | vertex <- vertices triangulation
+      , let ring =
+              [ coordHex (vertexPoint triangulation (destination triangulation edge))
+              | edge <- vertexOutgoingEdges triangulation vertex
+              ]
+      ]
+
+innerFaceGateLines :: DelaunayTriangulation Point -> [String]
+innerFaceGateLines triangulation =
+  sort
+    [ concat (sort (map (coordHex . vertexPoint triangulation) [a, b, c]))
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    ]
+
+-- | Every traversal the DCEL offers, in one pass: the hull cycle, each vertex's
+-- counterclockwise link, and each inner face's vertex triple. The coordinates
+-- are summed so that no step can be satisfied by a thunk.
+dcelWalk :: DelaunayTriangulation Point -> (Double, Int)
+dcelWalk triangulation = overFaces (overVertices (overHull (0, 0)))
+ where
+  overHull accumulator =
+    foldl' (\acc edge -> charge acc (origin triangulation edge)) accumulator (hullEdges triangulation)
+
+  overVertices accumulator = foldl' overLink accumulator (vertices triangulation)
+
+  overLink accumulator vertex =
+    foldl'
+      (\acc edge -> charge acc (destination triangulation edge))
+      accumulator
+      (vertexOutgoingEdges triangulation vertex)
+
+  overFaces accumulator = foldl' overCorners accumulator (innerFaces triangulation)
+
+  overCorners accumulator face =
+    case innerFaceVertices triangulation face of
+      Nothing -> accumulator
+      Just (a, b, c) -> foldl' charge accumulator [a, b, c]
+
+  charge (!total, !count) vertex =
+    let Point x y = vertexPoint triangulation vertex
+     in (total + x + y, count + 1)
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Delaunay.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Delaunay.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Delaunay.hs
@@ -0,0 +1,265 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Bulk-load, incremental, nearest, and refinement lanes and their gates.
+module Moonlight.Planar.Bench.SpadeCompare.Delaunay where
+
+import Control.Exception (evaluate)
+import Data.Foldable (traverse_)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad
+import Moonlight.Planar.Cdt (constrainedDelaunay)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Refinement (refine, withAdditionalVertexBudget, withConstraintPreservation, withMaximumArea, withMaximumRadiusEdgeRatio, withOuterFaceExclusion)
+import Moonlight.Planar.SetAlgebra (difference)
+import Moonlight.Planar.Types
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, DuplicatePoints, SpatialSeedPoints, HullInsertions, EdgeFlips, LocationWalkSteps, LegalizationMaxStack, SteinerPoints, RefinementFaceChecks, RefinementQueuePops, SweepFastPoints, SweepSkippedPoints), buildStat)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Telemetry (buildStats, insertVertex, refinementStats, withSession)
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, DuplicatePoints, SpatialSeedPoints, HullInsertions, EdgeFlips, LocationWalkSteps, LegalizationMaxStack, SteinerPoints, RefinementFaceChecks, RefinementQueuePops, SweepFastPoints, SweepSkippedPoints), buildStat)
+import Moonlight.Planar.HintGenerator
+import Moonlight.Planar.Session
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+writeBulkLoadGate :: FilePath -> Int -> IO ()
+writeBulkLoadGate directory count = do
+  triangulation <-
+    require
+      ( delaunayGeometry
+          (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+      )
+  writeFile
+    (directory </> ("bulk-load-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeIncrementalGate :: FilePath -> Int -> IO ()
+writeIncrementalGate directory count = do
+  triangulation <-
+    insertAllIncrementally
+      (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+  writeFile
+    (directory </> ("incremental-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeNearestGate :: FilePath -> IO ()
+writeNearestGate directory = do
+  let pointCount = 20000
+      queryCount = 5000
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.fromList (randomPoints 0x0123456789abcdef pointCount))
+        )
+  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+  queries <- requireQueryPoints (randomPoints 0x3141592653589793 queryCount)
+  writeFile
+    (directory </> "nearest-20000-5000.txt")
+    (unlines (map (nearestAnswer triangulation hierarchy) queries))
+
+-- | The recipe every refinement lane and gate shares: a Steiner budget,
+-- maximum area 0.5, radius-edge ratio 1.0, outer faces excluded, constraint
+-- edges free to split.
+refinementParameters :: Int -> Either BuildError RefinementParameters
+refinementParameters steinerBudget =
+  withOuterFaceExclusion True . withConstraintPreservation False
+    <$> ( withAdditionalVertexBudget steinerBudget defaultRefinementParameters
+            >>= withMaximumArea 0.5
+            >>= withMaximumRadiusEdgeRatio 1.0
+        )
+
+writeRefinementGate :: FilePath -> Int -> IO ()
+writeRefinementGate directory steinerBudget = do
+  let (points, constraints) = refinementInput
+  parameters <- require (refinementParameters steinerBudget)
+  cdtBuild <- require (constrainedDelaunay unitElementDefaults points constraints)
+  refined <-
+    require
+      ( refine
+          id
+          parameters
+          (buildTriangulation cdtBuild :: ConstrainedDelaunayTriangulation Point)
+      )
+  let triangulation = refinedTriangulation refined
+      prefix = directory </> ("refine-" <> show steinerBudget)
+      completion = if refinementComplete refined then "1" else "0"
+      summary =
+        [ "added " <> show (refinementAddedVertices refined)
+        , "vertices " <> show (numVertices triangulation)
+        , "edges " <> show (numUndirectedEdges triangulation)
+        , "inner-faces " <> show (numInnerFaces triangulation)
+        , "excluded " <> show (V.length (refinementExcludedFaces refined))
+        , "complete " <> completion
+        ]
+  writeFile (prefix <> "-summary.txt") (unlines summary)
+  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges triangulation))
+  writeFile
+    (prefix <> "-constraints.txt")
+    (unlines (canonicalConstraintEdges triangulation))
+
+-- | Arrival-order insertion through immutable snapshots. The artifact proves
+-- final-state agreement with Spade; it is not a cross-language timing row.
+writePersistentGate :: FilePath -> Int -> IO ()
+writePersistentGate directory count = do
+  triangulation <-
+    insertAllPersistently (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
+  writeFile
+    (directory </> ("persistent-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+writeSweepAngleGate :: FilePath -> Int -> IO ()
+writeSweepAngleGate directory count = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.fromList (nearCollinearPoints 0x9e3779b97f4a7c15 count))
+        )
+  writeFile
+    (directory </> ("sweep-angle-collapse-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+-- | The degenerate chain, pinned by the only thing it has: its edges.
+--
+-- There are no faces to compare and no circumcentres to argue about, so unlike
+-- every other gate here this one has no per-side half and no exclusion. The
+-- answer is @n - 1@ consecutive segments and the two sides either both produce
+-- it or one of them is wrong.
+writeDegenerateLineGate :: FilePath -> Int -> IO ()
+writeDegenerateLineGate directory count = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        (delaunay unitElementDefaults (V.fromList (exactlyCollinearPoints count)))
+  writeFile
+    (directory </> ("degenerate-line-" <> show count <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+
+-- Arrival-order insertion with one publication: the transactional comparison
+-- against Spade's @&mut self@ insertion. The per-operation snapshot fold is
+-- measured separately as Moonlight publication stress.
+insertAllIncrementally
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+insertAllIncrementally points =
+  evaluate
+    ( case either (error . show) id
+        ( withSession
+            (empty unitElementDefaults)
+            (V.length points)
+            (V.mapM_ insertVertex points)
+        ) of
+        (_, frozen, _) -> frozen
+    )
+
+insertAllPersistently
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+insertAllPersistently =
+  V.foldM'
+    (\triangulation point ->
+      insertionTriangulation <$> require (insert triangulation point)
+    )
+    (empty unitElementDefaults)
+
+-- | The refinement lane's own counters, so the worklist question is answered by
+-- measurement rather than by assertion.
+--
+-- @faceChecks@ is not a worklist counter: the terminal audit runs the complete
+-- face evaluation over every final inner face without popping anything. So the
+-- amplification ratio is @pops / steiner@, and @faceChecks - pops@ should equal
+-- the final inner-face count less any pop discarded as excluded.
+-- | The circle sweep's placement split, on the same generator the bulk lanes
+-- use. A point takes the fast path when the hull edge its own angle selected
+-- has the point strictly to its left; otherwise it is deferred to a generic
+-- insertion after the sweep finishes. spade takes the identical branch on
+-- @is_on_right_side_or_on_line@ and its own source calls that fallback "very
+-- slow", so a materially non-zero skip count is wasted work in the bulk lane
+-- rather than a curiosity. @seed + fast + skipped@ must exhaust the unique
+-- points; this reports the identity rather than asserting it silently.
+reportSweepStats :: Int -> IO ()
+reportSweepStats count = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
+  built <- require (delaunay unitElementDefaults points)
+  let stats = buildStats (built :: BuildResult 'Unconstrained Point () () ())
+      seeded = (buildStat SpatialSeedPoints) stats
+      fast = (buildStat SweepFastPoints) stats
+      skippedPoints = (buildStat SweepSkippedPoints) stats
+      unique = (buildStat UniquePoints) stats
+      accounted = seeded + fast + skippedPoints
+      share :: Int -> String
+      share value
+        | unique == 0 = "n/a"
+        | otherwise = show (100 * fromIntegral value / fromIntegral unique :: Double) <> "%"
+  traverse_
+    putStrLn
+    [ "input-points " <> show ((buildStat InputPoints) stats)
+    , "unique-points " <> show unique
+    , "duplicate-points " <> show ((buildStat DuplicatePoints) stats)
+    , "seed-points " <> show seeded
+    , "sweep-fast-points " <> show fast
+    , "sweep-skipped-points " <> show skippedPoints
+    , "skipped-share " <> share skippedPoints
+    , "accounted " <> show accounted
+    , "identity-holds " <> show (accounted == unique)
+    , "edge-flips " <> show ((buildStat EdgeFlips) stats)
+    , "hull-insertions " <> show ((buildStat HullInsertions) stats)
+    , "legalization-max-stack " <> show ((buildStat LegalizationMaxStack) stats)
+    ]
+  if accounted == unique
+    then pure ()
+    else do
+      hPutStrLn
+        stderr
+        ( "sweep-stats: seed + fast + skipped = "
+            <> show accounted
+            <> " but unique = "
+            <> show unique
+        )
+      exitWith (ExitFailure 1)
+
+reportRefinementCounters :: Int -> IO ()
+reportRefinementCounters budget = do
+  let (points, constraints) = refinementInput
+  parameters <- require (refinementParameters budget)
+  cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
+  result <-
+    require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point))
+  let stats = refinementStats result
+      steiner = (buildStat SteinerPoints) stats
+      pops = (buildStat RefinementQueuePops) stats
+      checks = (buildStat RefinementFaceChecks) stats
+      inner = numInnerFaces (refinedTriangulation result)
+      excluded = V.length (refinementExcludedFaces result)
+      ratio :: Int -> Int -> String
+      ratio numerator denominator
+        | denominator == 0 = "n/a"
+        | otherwise =
+            show (fromIntegral numerator / fromIntegral denominator :: Double)
+  traverse_
+    putStrLn
+    [ "budget " <> show budget
+    , "steiner " <> show steiner
+    , "queue-pops " <> show pops
+    , "face-checks " <> show checks
+    , "inner-faces " <> show inner
+    , "excluded " <> show excluded
+    , "edge-flips " <> show ((buildStat EdgeFlips) stats)
+    , "location-walk-steps " <> show ((buildStat LocationWalkSteps) stats)
+    , "complete " <> show (refinementComplete result)
+    , "pops/steiner " <> ratio pops steiner
+    , "checks/steiner " <> ratio checks steiner
+    , "flips/steiner " <> ratio ((buildStat EdgeFlips) stats) steiner
+    , "walk-steps/steiner " <> ratio ((buildStat LocationWalkSteps) stats) steiner
+    , "checks-minus-pops " <> show (checks - pops)
+    -- Every face check is now a queue pop. The terminal audit used to make the
+    -- difference equal the final inner-face count; if this ever reads False
+    -- again, a second whole-mesh quality pass has come back.
+    , "no-second-pass " <> show (checks == pops)
+    ]
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Gates.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Gates.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Gates.hs
@@ -0,0 +1,273 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The artifact rosters: every gate the binary writes, and the divergence set.
+module Moonlight.Planar.Bench.SpadeCompare.Gates where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import System.Directory (createDirectoryIfMissing)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad
+import Moonlight.Planar.Cdt (constrainedDelaunay)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Refinement (refine, withAdditionalVertexBudget, withOuterFaceExclusion)
+import Moonlight.Planar.Removal
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Bench.SpadeCompare.Constraint
+import Moonlight.Planar.Bench.SpadeCompare.Dcel
+import Moonlight.Planar.Bench.SpadeCompare.Delaunay
+import Moonlight.Planar.Bench.SpadeCompare.Hierarchy
+import Moonlight.Planar.Bench.SpadeCompare.Interpolation
+import Moonlight.Planar.Bench.SpadeCompare.Intersection
+import Moonlight.Planar.Bench.SpadeCompare.Removal
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Planar.Bench.SpadeCompare.Voronoi
+
+-- | The exact observable mesh egress retained by the persistent witnesses.
+--
+-- These artifacts validate immutable predecessor and final-state agreement;
+-- they are not Spade timing ratios. Snapshot publication is measured only on
+-- the Haskell side against its session reference.
+--
+-- The cross-language gate already compares the canonical-edge projection.
+-- This witness additionally retains the geometry-labelled circular DCEL
+-- section, so an isolated or face-less predecessor cannot change unseen.  It
+-- deliberately observes geometric topology rather than internal storage, and
+-- remains a gate-only sink outside every timed lane.
+data PersistentEndpoint = PersistentEndpoint
+  { persistentEndpointVertices :: !Int
+  , persistentEndpointUndirectedEdges :: !Int
+  , persistentEndpointInnerFaces :: !Int
+  , persistentEndpointCanonicalEdges :: ![String]
+  , persistentEndpointCircularDcel :: ![String]
+  }
+  deriving (Eq, Show)
+
+-- | Every retained section is named by its operation and completed edit count,
+-- never by a formatting-only string.
+data PersistentWitnessLabel
+  = PersistentInsertionWitness !Int
+  | PersistentRemovalWitness !Int
+  deriving (Eq, Show)
+
+-- | A preserved immutable section together with its authoritative egress at
+-- capture time.  Insert and removal each supply one concrete use, so this is
+-- the shared gate algebra rather than a second triangulation representation.
+data PersistentSnapshotWitness = PersistentSnapshotWitness
+  { persistentSnapshotLabel :: !PersistentWitnessLabel
+  , persistentSnapshotEndpoint :: !PersistentEndpoint
+  , persistentSnapshotTriangulation :: !(DelaunayTriangulation Point)
+  }
+
+-- | Complete typed refusal surface for the persistence gate.  The public
+-- operations already return 'BuildError'; the witness transports it instead
+-- of manufacturing a stringly failure.  A changed snapshot carries its exact
+-- observed endpoint so the gate failure is inspectable without a unilateral
+-- artifact.
+data PersistentSnapshotObstruction
+  = PersistentInitialBuildRefused !BuildError
+  | PersistentInsertionRefused !Int !BuildError
+  | PersistentRemovalRefused !Int !BuildError
+  | PersistentRemovalTargetMissing !Int
+  | PersistentSnapshotChanged
+      !PersistentWitnessLabel
+      !PersistentEndpoint
+      !PersistentEndpoint
+  deriving Show
+
+persistentEndpoint
+  :: DelaunayTriangulation Point
+  -> IO PersistentEndpoint
+persistentEndpoint triangulation = do
+  edges <- evaluate (force (canonicalEdges triangulation))
+  circular <- evaluate (force (circularGateLines triangulation))
+  pure $!
+    PersistentEndpoint
+      { persistentEndpointVertices = numVertices triangulation
+      , persistentEndpointUndirectedEdges = numUndirectedEdges triangulation
+      , persistentEndpointInnerFaces = numInnerFaces triangulation
+      , persistentEndpointCanonicalEdges = edges
+      , persistentEndpointCircularDcel = circular
+      }
+
+capturePersistentSnapshot
+  :: PersistentWitnessLabel
+  -> DelaunayTriangulation Point
+  -> IO PersistentSnapshotWitness
+capturePersistentSnapshot label triangulation = do
+  endpoint <- persistentEndpoint triangulation
+  pure
+    PersistentSnapshotWitness
+      { persistentSnapshotLabel = label
+      , persistentSnapshotEndpoint = endpoint
+      , persistentSnapshotTriangulation = triangulation
+      }
+
+verifyPersistentSnapshot
+  :: PersistentSnapshotWitness
+  -> IO (Either PersistentSnapshotObstruction ())
+verifyPersistentSnapshot witness = do
+  observed <- persistentEndpoint (persistentSnapshotTriangulation witness)
+  pure
+    ( if observed == persistentSnapshotEndpoint witness
+        then Right ()
+        else
+          Left
+            ( PersistentSnapshotChanged
+                (persistentSnapshotLabel witness)
+                (persistentSnapshotEndpoint witness)
+                observed
+            )
+    )
+
+verifyPersistentSnapshots :: [PersistentSnapshotWitness] -> IO (Either PersistentSnapshotObstruction ())
+verifyPersistentSnapshots = fmap sequence_ . traverse verifyPersistentSnapshot
+
+assertPersistentSnapshot
+  :: Either PersistentSnapshotObstruction value
+  -> IO value
+assertPersistentSnapshot = require
+
+-- | Fixed sparse checkpoints of a longer immutable chain.  Each one has later
+-- edits behind it, so the test distinguishes persistence from merely producing
+-- the correct final mesh.
+persistentCheckpoint :: Int -> Bool
+persistentCheckpoint index = index `elem` [0, 1, 2, 7, 31, 63]
+
+verifyPersistentInsertionSnapshots :: IO ()
+verifyPersistentInsertionSnapshots = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 96)
+      initial = empty unitElementDefaults
+  initialWitness <- capturePersistentSnapshot (PersistentInsertionWitness 0) initial
+  (final, witnesses) <-
+    V.ifoldM'
+      (\(current, preserved) index point -> do
+        nextTriangulation <-
+          assertPersistentSnapshot
+            ( insertionTriangulation
+                <$> first (PersistentInsertionRefused index) (insert current point)
+            )
+        if persistentCheckpoint index
+          then do
+            witness <- capturePersistentSnapshot (PersistentInsertionWitness (index + 1)) nextTriangulation
+            pure (nextTriangulation, preserved <> [witness])
+          else pure (nextTriangulation, preserved)
+      )
+      (initial, [initialWitness])
+      points
+  -- Force the terminal endpoint as an out-of-clock comparable sink before
+  -- checking predecessors; its edge projection is the same projection the
+  -- Rust gate emits for the timed lanes.
+  _ <- persistentEndpoint final
+  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
+
+verifyPersistentRemovalSnapshots :: IO ()
+verifyPersistentRemovalSnapshots = do
+  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 128)
+      removals = V.take 96 points
+  initial <-
+    buildTriangulation
+      <$> assertPersistentSnapshot
+        (first PersistentInitialBuildRefused (delaunay unitElementDefaults points))
+  initialWitness <- capturePersistentSnapshot (PersistentRemovalWitness 0) initial
+  (final, witnesses) <-
+    V.ifoldM'
+      (\(current, preserved) index point -> do
+        outcome <-
+          assertPersistentSnapshot
+            (first (PersistentRemovalRefused index) (locateAndRemove current point))
+        nextTriangulation <-
+          case outcome of
+            Nothing -> assertPersistentSnapshot (Left (PersistentRemovalTargetMissing index))
+            Just removal -> pure (removalTriangulation removal)
+        if persistentCheckpoint index
+          then do
+            witness <- capturePersistentSnapshot (PersistentRemovalWitness (index + 1)) nextTriangulation
+            pure (nextTriangulation, preserved <> [witness])
+          else pure (nextTriangulation, preserved)
+      )
+      (initial, [initialWitness])
+      removals
+  _ <- persistentEndpoint final
+  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
+
+-- | The Haskell side's immutable-contract gate.  It emits no extra artifact:
+-- adding a unilateral file would make the cross-language artifact diff fail.
+-- Its selected predecessor sections are instead verified before the existing
+-- shared endpoint artifacts are written.
+verifyPersistentSnapshotWitnesses :: IO ()
+verifyPersistentSnapshotWitnesses = do
+  verifyPersistentInsertionSnapshots
+  verifyPersistentRemovalSnapshots
+
+-- | Pin the exact output of every timed workload before comparing its cost.
+-- Persistent witness artifacts above are final-state agreement gates, not
+-- cross-language timing rows.
+writeGate :: FilePath -> IO ()
+writeGate directory = do
+  createDirectoryIfMissing True directory
+  verifyPersistentSnapshotWitnesses
+  traverse_ (writeBulkLoadGate directory) [1000, 10000]
+  traverse_ (writeIncrementalGate directory) [1000, 10000]
+  writeNearestGate directory
+  writeConstraintGate directory
+  traverse_ (writeRefinementGate directory) [625, 2500]
+  traverse_ (uncurry (writeRemovalGate directory)) [(1000, 250), (10000, 2500)]
+  traverse_ (uncurry (writeInterpolationGate directory)) [(10000, 1000), (100000, 2000)]
+  writeVoronoiGate directory 1000
+  writeDcelWalkGate directory 2000
+  writeIntersectionGate directory 10000 500
+  writeIntersectionVertexGate directory 2000
+  -- Persistent artifacts validate final-state agreement only; they are not
+  -- Spade timing rows. Their canonical egress is retained for the hard gate.
+  writePersistentGate directory 1000
+  writePersistentRemovalGate directory 10000 2500
+  -- The remaining diagnostic gates. Timing is meaningful only after both
+  -- sides have been shown to compute the same canonical result.
+  writeConstraintIncrementalGate directory 8000 800
+  writeConstraintSplitGate directory 1000
+  writeSweepAngleGate directory 2000
+  writeDegenerateLineGate directory 2000
+  writeHierarchyIncrementalGate directory 1000
+  writeHierarchyDuplicateGate directory 10000 500
+  writeHierarchyRemovalGate directory 10000 250
+  writeIntersectionOutsideGate directory 2000 100
+
+-- | The shapes on which the two implementations classify the constrained
+-- domain. They do not agree, so this is a characterization rather than a gate:
+-- the committed baselines lock in the known divergence and fail if either side
+-- moves. See @README.md@ for the compatibility rule.
+divergenceShapes :: [(String, [Point], [(Int, Int)])]
+divergenceShapes =
+  [ ("flush", square 8 <> [Point 4 4], loop4)
+  , ("notched", square 8 <> [Point 13 4, Point 4 4], loop4)
+  , ("flush-plus-dangling-segment", square 8 <> [Point 2 2, Point 6 6], loop4 <> [(4, 5)])
+  , ("notched-plus-dangling-segment", square 8 <> [Point 13 4, Point 2 2, Point 6 6], loop4 <> [(5, 6)])
+  , ( "annulus"
+    , square 12 <> [Point 4 4, Point 8 4, Point 8 8, Point 4 8]
+    , loop4 <> [(4, 5), (5, 6), (6, 7), (7, 4)]
+    )
+  ]
+ where
+  square scale = [Point 0 0, Point scale 0, Point scale scale, Point 0 scale]
+  loop4 = [(0, 1), (1, 2), (2, 3), (3, 0)]
+
+writeDivergence :: FilePath -> IO ()
+writeDivergence path = do
+  rows <- mapM row divergenceShapes
+  writeFile path (unlines rows)
+ where
+  row (label, points, constraints) = do
+    built <- require (constrainedDelaunay unitElementDefaults (V.fromList points) (V.fromList constraints))
+    let cdt = buildTriangulation built
+    parameters <- require (withOuterFaceExclusion True <$> withAdditionalVertexBudget 0 defaultRefinementParameters)
+    classified <- require (refine id parameters cdt)
+    pure
+      ( label
+          <> ": inner " <> show (numInnerFaces cdt)
+          <> ", excluded " <> show (V.length (refinementExcludedFaces classified))
+      )
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Hierarchy.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Hierarchy.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Hierarchy.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Hierarchy-hint lanes, their gates, and the maintained-hint step.
+module Moonlight.Planar.Bench.SpadeCompare.Hierarchy where
+
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad
+import Moonlight.Planar.Types (DelaunayTriangulation, InsertionDisposition (..), unitElementDefaults)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Telemetry (LocationHint (..), Session, insertVertexAt, insertVertexAtNearVertex, refuse, withSession)
+import Moonlight.Planar.HintGenerator
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Session
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The step both insertion lanes run: one insert, one hierarchy update, and
+-- nothing between them. It is what spade's @insert@ does internally through
+-- @notify_vertex_inserted@, and both sides carry branch factor 16.
+--
+-- The point handed to 'updateHierarchyAfterInsertion' is the same binding
+-- handed to 'insertVertexAt', never one read back out of the harness's input
+-- vector. The mesh stores 'canonicalPoint' of what it was given, so a
+-- harness-side copy would part company from it on a signed zero, seed a
+-- different nearest neighbour, and move the gate against spade. Same function,
+-- same argument, same bits.
+hierarchyStep
+  :: HierarchyHint
+  -> Point
+  -> Session s Point () () () HierarchyHint
+hierarchyStep hierarchy point = do
+  query <- either refuse pure (validatePoint Nothing point)
+  (vertex, disposition) <-
+    case hierarchyHint hierarchy query of
+      Just (VertexHint seed) -> insertVertexAtNearVertex seed point point
+      _ -> insertVertexAt point point
+  either refuse pure (updateHierarchyAfterInsertion hierarchy point vertex disposition)
+
+-- | Arrival-order insertion with the hierarchy maintained alongside it.
+--
+-- One session spans the whole run. The hierarchy no longer asks for a
+-- triangulation, so the per-point publication that existed only to hand it one
+-- is gone: the mesh is thawed once and published once for the program, where
+-- before it was once per point. The measured operation is unchanged — still an
+-- insert per point with the hint maintained against it, which is spade's loop.
+insertAllWithHierarchy
+  :: V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+insertAllWithHierarchy points = do
+  let base = empty unitElementDefaults
+  initial <- require (buildHierarchyHint defaultHierarchyBranchFactor base)
+  (hierarchy, mesh, _) <-
+    require (withSession base (V.length points) (V.foldM' hierarchyStep initial points))
+  pure (mesh, hierarchy)
+
+-- | The same step over a mesh that already holds every point, which is the
+-- duplicate lane. Each insert locates a site that is already there and answers
+-- 'AlreadyPresent', so the hierarchy comes back unexamined and the level walk
+-- is never entered. What remains on the clock is the locate and the
+-- disposition test, with no rebuild standing behind either.
+reinsertAllWithHierarchy
+  :: (DelaunayTriangulation Point, HierarchyHint)
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+reinsertAllWithHierarchy (triangulation, hierarchy) points =
+  case withSession triangulation (V.length points) (V.foldM' hierarchyStep hierarchy points) of
+    Left refusal -> fail (show refusal)
+    Right (updated, mesh, _) -> pure (mesh, updated)
+
+-- | Remove a program of coordinates through the hierarchy: every locate
+-- starts at the hierarchy's nearest sample, one base session publishes once,
+-- and the hierarchy of the resulting value is published once.
+--
+-- No query observes an intermediate hierarchy in this lane. A triangulation
+-- and its hierarchy denote their current values rather than the edit history,
+-- so the lawful physical schedule is one base session followed by one
+-- hierarchy publication — the shape of the strict side's
+-- @HierarchyTriangulation@, whose removals also locate through the hierarchy.
+removeAllWithHierarchy
+  :: (DelaunayTriangulation Point, HierarchyHint)
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+removeAllWithHierarchy (triangulation, hierarchy) points =
+  case removeManyWithHierarchy hierarchy triangulation points of
+    Left refusal -> fail (show refusal)
+    Right (outcomes, surviving, repaired) -> do
+      V.imapM_ checkRemoved outcomes
+      pure (surviving, repaired)
+ where
+  checkRemoved index outcome = case outcome of
+    Nothing ->
+      error
+        ( "removal target is not a vertex: index="
+            <> show index
+            <> " point="
+            <> ( case points V.! index of
+                  Point x y -> hex64 x <> "," <> hex64 y
+               )
+        )
+    Just _ -> pure ()
+
+-- | The hierarchy lanes' gate, in two parts. The base mesh alone would not see
+-- a hierarchy whose nesting law has drifted, because a search started from a
+-- bad hint still walks to the right answer; a hint naming a vertex that does
+-- not exist, or one the search cannot walk out of, moves the answers. Both are
+-- pinned.
+hierarchyGateQueries :: Int
+hierarchyGateQueries = 1000
+
+writeHierarchyGate
+  :: FilePath
+  -> String
+  -> (DelaunayTriangulation Point, HierarchyHint)
+  -> IO ()
+writeHierarchyGate directory label (triangulation, hierarchy) = do
+  queries <- requireQueryPoints (randomPoints 0x3141592653589793 hierarchyGateQueries)
+  writeFile
+    (directory </> (label <> "-edges.txt"))
+    (unlines (canonicalEdges triangulation))
+  writeFile
+    (directory </> (label <> "-nearest.txt"))
+    ( unlines
+        ( map
+            (nearestAnswer triangulation hierarchy)
+            queries
+        )
+    )
+
+writeHierarchyIncrementalGate :: FilePath -> Int -> IO ()
+writeHierarchyIncrementalGate directory pointCount = do
+  built <- insertAllWithHierarchy (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount))
+  writeHierarchyGate directory ("hierarchy-incremental-" <> show pointCount) built
+
+writeHierarchyDuplicateGate :: FilePath -> Int -> Int -> IO ()
+writeHierarchyDuplicateGate directory pointCount duplicateCount = do
+  base <- hierarchyOf pointCount
+  built <-
+    reinsertAllWithHierarchy
+      base
+      (V.fromList (randomPoints 0x9e3779b97f4a7c15 duplicateCount))
+  writeHierarchyGate
+    directory
+    ("hierarchy-duplicate-" <> show pointCount <> "-" <> show duplicateCount)
+    built
+
+writeHierarchyRemovalGate :: FilePath -> Int -> Int -> IO ()
+writeHierarchyRemovalGate directory pointCount removalCount = do
+  base <- hierarchyOf pointCount
+  built <-
+    removeAllWithHierarchy
+      base
+      (V.fromList (take removalCount (randomPoints 0x9e3779b97f4a7c15 pointCount)))
+  writeHierarchyGate
+    directory
+    ("hierarchy-removal-" <> show pointCount <> "-" <> show removalCount)
+    built
+
+-- | A bulk-loaded mesh and the hierarchy over it, which is where the duplicate
+-- and removal lanes start. The strict side reaches the same pair through
+-- @HierarchyTriangulation::bulk_load@.
+hierarchyOf
+  :: Int
+  -> IO (DelaunayTriangulation Point, HierarchyHint)
+hierarchyOf count = do
+  triangulation <- delaunayOf count
+  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
+  pure (triangulation, hierarchy)
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Input.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Input.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Input.hs
@@ -0,0 +1,20 @@
+-- | Cross-language benchmark input programs. Both Haskell interpreters import
+-- this owner directly; neither keeps a private copy of the LCG stream.
+module Moonlight.Planar.Bench.SpadeCompare.Input
+  ( randomPoints
+  ) where
+
+import Data.Word (Word64)
+import Moonlight.Planar.Point (Point (..))
+
+-- | The Rust generator, bit for bit.
+randomPoints :: Word64 -> Int -> [Point]
+randomPoints seed count = take count (go seed)
+ where
+  go :: Word64 -> [Point]
+  go state =
+    let state1 = state * 6364136223846793005 + 1442695040888963407
+        state2 = state1 * 6364136223846793005 + 1442695040888963407
+        unit :: Word64 -> Double
+        unit value = fromIntegral (value `div` 2048) / 9007199254740992
+     in Point (2 * unit state1 - 1) (2 * unit state2 - 1) : go state2
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Interpolation.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Interpolation.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Interpolation.hs
@@ -0,0 +1,424 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Sibson interpolation lanes, gates, and diagnostics.
+module Moonlight.Planar.Bench.SpadeCompare.Interpolation where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad.ST (stToIO)
+import GHC.Exts (RealWorld)
+import Data.Foldable (traverse_)
+import Data.List (sort, sortOn)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (delaunay, empty)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Handles.HandleDefs (asUndirected, reverseEdge)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Math (circumcenter, inCircle)
+import Moonlight.Planar.Scalar (isFinite)
+import Moonlight.Planar.PointLocation
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+import Moonlight.Planar.Telemetry (LocationStats (..), locatePointWithHint)
+import Moonlight.Planar.Interpolation
+import Moonlight.Planar.Internal.Predicates qualified as Predicates
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The Sibson query reports its own cavity size, so the distribution of
+-- (cavity faces, neighbours returned) says which step degenerates: a cavity of
+-- one face means 'discoverCavity' never expanded, while a larger cavity paired
+-- with three neighbours means the cell or the normalization was rejected.
+reportInterpolationAudit :: Int -> Int -> IO ()
+reportInterpolationAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  queries <- requireQueryPoints (randomPoints 0x2718281828459045 queryCount)
+  pairs <-
+    traverse
+      ( \query -> do
+          (count, _, stats) <-
+            stToIO (foldNaturalNeighborWeights (\total _ _ -> total + (1 :: Int)) 0 workspace Nothing query)
+          pure ((interpolationCavityFaces stats, count), interpolationUsedFallback stats)
+      )
+      queries
+  let tally =
+        [ (representative, 1 + length rest)
+        | representative : rest <- groupSorted (sort (map fst pairs))
+        ]
+      fallbacks = length (filter snd pairs)
+  putStrLn ("queries " <> show (length pairs))
+  putStrLn ("fallbacks " <> show fallbacks)
+  traverse_
+    (\((cavity, neighbors), n) -> putStrLn ("cavityFaces=" <> show cavity <> " neighbors=" <> show neighbors <> " count=" <> show n))
+    tally
+ where
+  groupSorted [] = []
+  groupSorted (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupSorted rest
+
+-- | Isolates the cavity flood of the Sibson pipeline: per query, the BFS
+-- over 'inCircle' and nothing else. The margin tally counts tests whose
+-- determinant is inside the filtered predicate's error bound — the exact
+-- dyadic path, which allocates Integer arithmetic per call.
+reportInterpolationCavityAudit :: Int -> Int -> IO ()
+reportInterpolationCavityAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (faces, tests, margins) <-
+    V.foldM'
+      ( \(faceAcc, testAcc, marginAcc) query ->
+          case locatePoint triangulation query of
+            InFace start -> do
+              let (!cavitySize, !testCount, !marginCount) = flood triangulation (queryPointValue query) start
+              pure (faceAcc + cavitySize, testAcc + testCount, marginAcc + marginCount)
+            _ -> pure (faceAcc, testAcc, marginAcc)
+      )
+      (0 :: Int, 0 :: Int, 0 :: Int)
+      queries
+  putStrLn ("queries " <> show queryCount)
+  putStrLn ("cavity-faces " <> show faces)
+  putStrLn ("incircle-tests " <> show tests)
+  putStrLn ("exact-path-margins " <> show margins)
+ where
+  flood triangulation query start = go [start] [] 0 0
+   where
+    go [] seen tests margins = (length seen, tests, margins)
+    go (face : rest) seen tests margins
+      | face `elem` seen = go rest seen tests margins
+      | otherwise =
+          case innerFaceVertices triangulation face of
+            Nothing -> go rest seen tests margins
+            Just (a, b, c) ->
+              let !pa = vertexPoint triangulation a
+                  !pb = vertexPoint triangulation b
+                  !pc = vertexPoint triangulation c
+                  !verdict = Predicates.inCircle pa pb pc query
+                  !margins' = if insideMargin pa pb pc query then margins + 1 else margins
+               in if verdict == GT
+                    then go (adjacent triangulation face ++ rest) (face : seen) (tests + 1) margins'
+                    else go rest seen (tests + 1) margins'
+
+  adjacent triangulation face =
+    case innerFaceDirectedEdges triangulation face of
+      Nothing -> []
+      Just (e0, e1, e2) ->
+        [ opposite
+        | edge <- [e0, e1, e2]
+        , let opposite = incidentFace triangulation (reverseEdge edge)
+        , opposite /= outerFace
+        ]
+
+  -- The branch condition of the filtered Double inCircle, recomputed over
+  -- public coordinates: inside the error bound means the exact dyadic path.
+  insideMargin (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
+    abs determinant <= 1.1102230246251577e-15 * permanent
+   where
+    !adx = ax - dx
+    !ady = ay - dy
+    !bdx = bx - dx
+    !bdy = by - dy
+    !cdx = cx - dx
+    !cdy = cy - dy
+    !abdet = adx * bdy - bdx * ady
+    !bcdet = bdx * cdy - cdx * bdy
+    !cadet = cdx * ady - adx * cdy
+    !alift = adx * adx + ady * ady
+    !blift = bdx * bdx + bdy * bdy
+    !clift = cdx * cdx + cdy * cdy
+    !determinant = alift * bcdet + blift * cadet + clift * abdet
+    !permanent =
+      (abs (bdx * cdy) + abs (cdx * bdy)) * alift
+        + (abs (cdx * ady) + abs (adx * cdy)) * blift
+        + (abs (adx * bdy) + abs (bdx * ady)) * clift
+
+-- | Isolates the locate half of the interpolation lane: same triangulation,
+-- same queries, but only 'locatePointWithHint' runs — no cavity, no Sibson.
+-- With the allocation of this in hand against the full lane's, the owner of
+-- the per-query cost is named by subtraction rather than by guesswork.
+reportInterpolationLocateAudit :: Int -> Int -> IO ()
+reportInterpolationLocateAudit pointCount queryCount = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (steps, fallbacks) <-
+    V.foldM'
+      ( \(stepAcc, fallbackAcc) query -> do
+          let (!location, !stats) = locatePointWithHint triangulation Nothing query
+          _ <- evaluate (force (show location))
+          pure (stepAcc + locationWalkSteps stats, fallbackAcc + (if locationUsedFallback stats then 1 else 0))
+      )
+      (0 :: Int, 0 :: Int)
+      queries
+  putStrLn ("queries " <> show queryCount)
+  putStrLn ("walk-steps " <> show steps)
+  putStrLn ("fallbacks " <> show fallbacks)
+
+-- | The Sibson pipeline re-implemented over public API only, stage by stage,
+-- mirroring the library's 'sibsonQuery': cavity flood, boundary collection,
+-- loop ordering, insertion cell, stolen-area walks, normalization. The stage
+-- sets are order-independent, so where the replica succeeds while the library
+-- falls back the workspace layer is convicted, and where it fails the stage's
+-- own algorithm is. With no index it tallies the first 1000 gate queries by
+-- failing stage; with an index it dumps one query's full geometry.
+reportInterpolationContext :: Int -> Maybe Int -> IO ()
+reportInterpolationContext pointCount maybeIndex = do
+  triangulation <-
+    buildTriangulation
+      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 1000))
+  case maybeIndex of
+    Just index -> case queries V.!? index of
+      Nothing -> do
+        hPutStrLn stderr "interp-context: query index out of range"
+        exitWith (ExitFailure 2)
+      Just query -> do
+        let (_, detail) = replicateQuery triangulation query
+        putStrLn ("query " <> pointHex (queryPointValue query))
+        traverse_ putStrLn detail
+    Nothing -> do
+      let reports = map (fst . replicateQuery triangulation) (V.toList queries)
+          tally =
+            [ (representative, 1 + length rest)
+            | representative : rest <- groupByHead (sort reports)
+            ]
+      putStrLn ("queries " <> show (length reports))
+      traverse_ (\(stage, count) -> putStrLn (stage <> " count=" <> show count)) tally
+ where
+  groupByHead [] = []
+  groupByHead (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupByHead rest
+
+  replicateQuery triangulation queryPoint =
+    let query = queryPointValue queryPoint
+     in case locatePoint triangulation queryPoint of
+      OnVertex _ -> ("special-on-vertex", ["on-vertex"])
+      OnEdge edge
+        | isBoundaryEdge triangulation (asUndirected edge) -> ("special-boundary-edge", ["boundary-edge"])
+        | otherwise ->
+            let !left = incidentFace triangulation edge
+                !right = incidentFace triangulation (reverseEdge edge)
+                !start = if left /= outerFace then left else right
+             in sibson triangulation query start
+      InFace face -> sibson triangulation query face
+      OutsideConvexHull _ -> ("special-outside-hull", ["outside-hull"])
+      EmptyTriangulation -> ("special-empty", ["empty"])
+
+  sibson triangulation query startFace =
+    let cavity = flood [startFace] []
+        boundary =
+          [ edge
+          | face <- cavity
+          , Just (e0, e1, e2) <- [innerFaceDirectedEdges triangulation face]
+          , edge <- [e0, e1, e2]
+          , let opposite = incidentFace triangulation (reverseEdge edge)
+          , opposite == outerFace || not (opposite `elem` cavity)
+          ]
+        header =
+          [ "cavity " <> show (length cavity)
+          , "boundary " <> show (length boundary)
+          ]
+     in case orderLoop boundary of
+          Left reason -> ("fail-order", header ++ ["order-failure " <> reason])
+          Right ordered
+            | length ordered < 3 -> ("fail-order-small", header ++ ["ordered " <> show (length ordered)])
+            | otherwise ->
+                case insertionCell ordered of
+                  Left reason -> ("fail-cell", header ++ ["ordered " <> show (length ordered), "cell-failure " <> reason])
+                  Right cell ->
+                    let walks = stolenAreas ordered cell
+                     in case sequence walks of
+                          Left reason -> ("fail-stolen", header ++ ["ordered " <> show (length ordered), "stolen-failure " <> reason])
+                          Right areas ->
+                            let !total = sum areas
+                                !minimumWeight = minimum areas
+                                !tolerance = max 1.0e-12 (128 * 2.220446049250313e-16)
+                                !clampedTotal = sum (map (max 0) areas)
+                                verdict
+                                  | any (not . isFinite) areas || total == 0 || minimumWeight < negate tolerance =
+                                      Left ("normalize total=" <> show total <> " min=" <> show minimumWeight)
+                                  | clampedTotal <= 0 || not (isFinite clampedTotal) =
+                                      Left ("clamped-total " <> show clampedTotal)
+                                  | otherwise = Right ()
+                             in case verdict of
+                                  Left reason ->
+                                    ( "fail-normalize"
+                                    , header
+                                        ++ ["ordered " <> show (length ordered)]
+                                        ++ ["area " <> show area | area <- areas]
+                                        ++ ["normalize-failure " <> reason]
+                                    )
+                                  Right () ->
+                                    ( "sibson-success"
+                                    , header
+                                        ++ ["ordered " <> show (length ordered)]
+                                        ++ zipWith areaLine ordered areas
+                                        ++ ["total " <> show total <> " min " <> show minimumWeight]
+                                    )
+   where
+    areaLine edge area =
+      "  vertex "
+        <> pointHex (vertexPoint triangulation (origin triangulation edge))
+        <> " area "
+        <> show area
+
+    contains face =
+      case innerFaceVertices triangulation face of
+        Nothing -> False
+        Just (a, b, c) ->
+          Predicates.inCircle
+            (vertexPoint triangulation a)
+            (vertexPoint triangulation b)
+            (vertexPoint triangulation c)
+            query
+            /= LT
+
+    adjacent face =
+      case innerFaceDirectedEdges triangulation face of
+        Nothing -> []
+        Just (e0, e1, e2) ->
+          [ opposite
+          | edge <- [e0, e1, e2]
+          , let opposite = incidentFace triangulation (reverseEdge edge)
+          , opposite /= outerFace
+          ]
+
+    flood [] seen = seen
+    flood (face : rest) seen
+      | face `elem` seen = flood rest seen
+      | not (contains face) = flood rest seen
+      | otherwise = flood (adjacent face ++ rest) (face : seen)
+
+    orderLoop [] = Left "empty-boundary"
+    orderLoop boundary@(first : _) = chain first first [] 0
+     where
+      !count = length boundary
+      origins = [(origin triangulation edge, edge) | edge <- boundary]
+      chain start current acc index
+        | index >= count =
+            if current == start then Right (reverse acc) else Left "no-closure"
+        | otherwise =
+            case lookup (destination triangulation current) origins of
+              Nothing ->
+                Left
+                  ( "broken-chain "
+                      <> pointHex (vertexPoint triangulation (destination triangulation current))
+                  )
+              Just nextEdge
+                | nextEdge == start && index + 1 == count -> Right (reverse (current : acc))
+                | nextEdge == start -> Left "premature-closure"
+                | otherwise -> chain start nextEdge (current : acc) (index + 1)
+
+    relative point = case point of Point x y -> Point (x - qx) (y - qy)
+    (qx, qy) = case query of Point x y -> (x, y)
+
+    insertionCell ordered = traverse cellCenter ordered
+     where
+      cellCenter edge =
+        let !from = vertexPoint triangulation (origin triangulation edge)
+            !to = vertexPoint triangulation (destination triangulation edge)
+         in case circumcenter (relative to) (relative from) (Point 0 0) of
+              Nothing ->
+                Left
+                  ( "edge "
+                      <> pointHex from
+                      <> " "
+                      <> pointHex to
+                  )
+              Just center -> Right center
+
+    stolenAreas ordered cell =
+      let !count = length ordered
+          pairs = zip ordered cell
+          predecessors = drop (count - 1) pairs ++ take (count - 1) pairs
+       in zipWith walk pairs predecessors
+     where
+      walk (stopEdge, first) (initialEdge, initialPoint) = go initialEdge initialPoint 0 initialPositive initialNegative
+       where
+        !target = reverseEdge stopEdge
+        !limit = numDirectedEdges triangulation + 1
+        !initialPositive = px first * py initialPoint
+        !initialNegative = py first * px initialPoint
+        !vertex = vertexPoint triangulation (origin triangulation stopEdge)
+        go lastEdge lastPoint steps positive negative
+          | steps >= limit = Left "step-limit"
+          | incidentFace triangulation lastEdge == outerFace =
+              Left ("outer-face vertex=" <> pointHex vertex <> " steps=" <> show steps)
+          | otherwise =
+              case innerFaceVertices triangulation (incidentFace triangulation lastEdge) of
+                Nothing -> Left "no-face-vertices"
+                Just (a, b, c) ->
+                  case circumcenter
+                    (relative (vertexPoint triangulation a))
+                    (relative (vertexPoint triangulation b))
+                    (relative (vertexPoint triangulation c)) of
+                    Nothing ->
+                      Left ("circumcenter vertex=" <> pointHex vertex <> " steps=" <> show steps)
+                    Just current ->
+                      let !positive' = positive + px lastPoint * py current
+                          !negative' = negative + py lastPoint * px current
+                          !nextEdge = reverseEdge (next triangulation lastEdge)
+                       in if nextEdge == target
+                            then Right ((negative' + py current * px first) - (positive' + px current * py first))
+                            else go nextEdge current (steps + 1) positive' negative'
+      px (Point x _) = x
+      py (Point _ y) = y
+
+-- | The interpolation gate, two artifacts per query. The neighbour set is
+-- exact-predicate determined over triangulations that already agree
+-- bit-for-bit, so it crosses implementations. The weights cannot: each
+-- neighbour's stolen area uses the same operation sequence on both sides,
+-- but the normalization total accumulates those areas in each side's own
+-- neighbour order, and floating-point addition is not associative. The
+-- weights are pinned per side in the driver instead.
+writeInterpolationGate :: FilePath -> Int -> Int -> IO ()
+writeInterpolationGate directory pointCount queryCount = do
+  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
+  (neighborLines, weightLines) <-
+    V.foldM'
+      ( \(neighborAcc, weightAcc) query -> do
+          (entries, _, _) <- stToIO (foldNaturalNeighborWeights (\acc vertex weight -> (vertex, weight) : acc) [] workspace Nothing query)
+          let sortedEntries =
+                sortOn
+                  (\(x, y, _) -> (x, y))
+                  [ case vertexPoint triangulation vertex of
+                      Point x y -> (x, y, weight)
+                  | (vertex, weight) <- entries
+                  ]
+              queryHex = case queryPointValue query of Point x y -> hex64 x <> hex64 y
+          pure
+            ( queryHex <> concatMap (\(x, y, _) -> hex64 x <> hex64 y) sortedEntries : neighborAcc
+            , queryHex <> concatMap (\(x, y, weight) -> hex64 x <> hex64 y <> hex64 weight) sortedEntries : weightAcc
+            )
+      )
+      ([], [])
+      queries
+  writeFile
+    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-neighbors.txt"))
+    (unlines (reverse neighborLines))
+  writeFile
+    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-weights.txt"))
+    (unlines (reverse weightLines))
+
+-- | The sum of one query's Sibson weights through the reusable workspace —
+-- the allocation-free path, matching what the strict side's buffer reuse
+-- does inside its measured region.
+interpolationWeightSum
+  :: NaturalNeighborWorkspace RealWorld 'Unconstrained Point () () ()
+  -> V.Vector QueryPoint
+  -> IO Double
+interpolationWeightSum workspace =
+  V.foldM'
+    ( \total query -> do
+        (weightSum, _, _) <- stToIO (foldNaturalNeighborWeights (\acc _ weight -> acc + weight) 0 workspace Nothing query)
+        pure (total + weightSum)
+    )
+    0
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Intersection.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Intersection.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Intersection.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Corridor-walk lanes, their inputs, and their gates.
+module Moonlight.Planar.Bench.SpadeCompare.Intersection where
+
+import Data.List (sortOn)
+import Data.Word (Word64)
+import System.FilePath ((</>))
+import qualified Data.Vector as V
+import Moonlight.Planar.Dcel (destination, origin, undirectedEndpoints, vertexPoint)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (undirectedEdges, vertices)
+import Moonlight.Planar.IntersectionIterator
+import Moonlight.Planar.SetAlgebra (intersection)
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+-- | Query endpoints pulled inward. The generator fills [-1, 1]^2, so a point
+-- with both coordinates inside [-0.5, 0.5] is strictly inside the hull of a
+-- few thousand of them and locating it lands on a face. A chord that starts
+-- OUTSIDE the hull makes endpoint choice and local hull entry observable, so
+-- those chords remain the separate "intersection-outside" cliff lane.
+interiorQueryPoints :: Word64 -> Int -> [Point]
+interiorQueryPoints seed count =
+  [Point (0.5 * x) (0.5 * y) | Point x y <- randomPoints seed count]
+
+-- | Query origins pushed away from the dense centre so the corpus repeatedly
+-- exercises outside-hull entry rather than only face-to-face corridors.
+exteriorQueryPoints :: Word64 -> Int -> [Point]
+exteriorQueryPoints seed count =
+  [Point (4 * x) (4 * y) | Point x y <- randomPoints seed count]
+
+intersectionChords :: Int -> [(Point, Point)]
+intersectionChords queryCount =
+  zip
+    (interiorQueryPoints 0x3141592653589793 queryCount)
+    (interiorQueryPoints 0x2718281828459045 queryCount)
+
+outsideChords :: Int -> [(Point, Point)]
+outsideChords queryCount =
+  zip
+    (exteriorQueryPoints 0x243f6a8885a308d3 queryCount)
+    (interiorQueryPoints 0x2718281828459045 queryCount)
+
+-- | One line per query: the line's own endpoints, then its intersection
+-- sequence in the order the walk reports it. Every branch of the walk turns on
+-- an orientation predicate over a mesh that already agrees bit for bit, so the
+-- whole sequence is exactly determined and crosses implementations — order
+-- included, which is why nothing here is sorted.
+writeIntersectionGate :: FilePath -> Int -> Int -> IO ()
+writeIntersectionGate directory pointCount queryCount = do
+  triangulation <- delaunayOf pointCount
+  chords <- requireQueryChords (intersectionChords queryCount)
+  writeFile
+    (directory </> ("intersection-" <> show pointCount <> "-" <> show queryCount <> "-chords.txt"))
+    (unlines (map (intersectionLine triangulation) chords))
+
+writeIntersectionOutsideGate :: FilePath -> Int -> Int -> IO ()
+writeIntersectionOutsideGate directory pointCount queryCount = do
+  triangulation <- delaunayOf pointCount
+  chords <- requireQueryChords (outsideChords queryCount)
+  writeFile
+    ( directory
+        </> ("intersection-outside-" <> show pointCount <> "-" <> show queryCount <> ".txt")
+    )
+    (unlines (map (intersectionLine triangulation) chords))
+
+-- | Lines whose endpoints are mesh vertices, which is the only way to reach the
+-- vertex and overlap branches of the walk at all: over generated doubles no
+-- chord lands exactly on a site, so a gate built from chords alone exercises
+-- one branch of three. The first family joins vertices the constraint
+-- generator's index pairs select, which starts and ends the walk on a vertex;
+-- the second runs along an existing edge, which is the only shape that produces
+-- an overlap. Both name their endpoints by coordinate, the second from a list
+-- both sides sort by the same encoding, because vertex handles do not cross.
+writeIntersectionVertexGate :: FilePath -> Int -> IO ()
+writeIntersectionVertexGate directory pointCount = do
+  triangulation <- delaunayOf pointCount
+  let sites = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+      joins =
+        [ (sites V.! fromIndex, sites V.! toIndex)
+        | (fromIndex, toIndex) <- constraintPairs pointCount intersectionVertexLines
+        ]
+      alongEdges = take intersectionVertexLines (sortedEdgeEndpoints triangulation)
+  queryLines <- requireQueryChords (joins <> alongEdges)
+  writeFile
+    (directory </> ("intersection-vertexlines-" <> show pointCount <> ".txt"))
+    (unlines (map (intersectionLine triangulation) queryLines))
+
+intersectionVertexLines :: Int
+intersectionVertexLines = 100
+
+sortedEdgeEndpoints
+  :: DelaunayTriangulation Point
+  -> [(Point, Point)]
+sortedEdgeEndpoints triangulation =
+  map snd
+    ( sortOn
+        fst
+        [ (coordHex first <> coordHex second, (first, second))
+        | edge <- undirectedEdges triangulation
+        , let (from, to) = undirectedEndpoints triangulation edge
+              p = vertexPoint triangulation from
+              q = vertexPoint triangulation to
+              (first, second) = if pointKey p <= pointKey q then (p, q) else (q, p)
+        ]
+    )
+ where
+  pointKey (Point x y) = (x, y)
+
+intersectionLine
+  :: DelaunayTriangulation Point
+  -> (QueryPoint, QueryPoint)
+  -> String
+intersectionLine triangulation (from, to) =
+  coordHex (queryPointValue from)
+    <> coordHex (queryPointValue to)
+    <> concatMap (encodeIntersection triangulation) (lineIntersections triangulation from to)
+
+encodeIntersection
+  :: DelaunayTriangulation Point
+  -> Intersection
+  -> String
+encodeIntersection triangulation event = case event of
+  VertexIntersection vertex -> "V" <> coordHex (vertexPoint triangulation vertex)
+  EdgeIntersection edge -> "E" <> directedHex edge
+  EdgeOverlap edge -> "O" <> directedHex edge
+ where
+  directedHex edge =
+    coordHex (vertexPoint triangulation (origin triangulation edge))
+      <> coordHex (vertexPoint triangulation (destination triangulation edge))
+
+-- | The corridor walk, counted. 'lineIntersections' materializes its answer, so
+-- forcing the spine forces every step that produced it; spade's iterator is
+-- lazy and is consumed by the same count.
+intersectionWalk
+  :: DelaunayTriangulation Point
+  -> [(QueryPoint, QueryPoint)]
+  -> Int
+intersectionWalk triangulation =
+  foldl' (\total (from, to) -> total + length (lineIntersections triangulation from to)) 0
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Lane.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Lane.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Lane.hs
@@ -0,0 +1,302 @@
+-- | The closed comparison vocabulary and its boundary projections.
+--
+-- The driver executes these requests, the report orders and names them, and
+-- the Haskell runner interprets their command constructors. None of those
+-- boundaries authors another lane universe.
+module Moonlight.Planar.Bench.SpadeCompare.Lane
+  ( LaneClass (..)
+  , LaneKind (..)
+  , LaneRequest (..)
+  , LaneObstruction (..)
+  , laneKindName
+  , laneRequestLabel
+  , parseLaneRequest
+  , renderInventoryCsv
+  , renderInventoryHuman
+  , renderLaneSpecs
+  , renderLaneObstruction
+  , renderSnapshotSpecs
+  ) where
+
+import Data.List (find, intercalate)
+import Text.Read (readMaybe)
+
+data LaneClass
+  = ParityLane
+  | CliffLane
+  deriving stock (Eq, Show)
+
+-- | Every benchmark command understood by the Moonlight runner. The board is
+-- a closed subset of this vocabulary; the remaining constructors are focused
+-- diagnostics rather than Spade comparison rows.
+data LaneKind
+  = BulkLoadLane
+  | IncrementalLane
+  | SnapshotInsertLane
+  | SweepAngleCollapseLane
+  | DegenerateLineLane
+  | BatchSweepLane
+  | NearestLane
+  | CdtRecoveryLane
+  | ConstraintIncrementalLane
+  | ConstraintSplitLane
+  | PublicationFloorLane
+  | RemovalLane
+  | SnapshotRemovalLane
+  | HierarchyIncrementalLane
+  | HierarchyDuplicateLane
+  | HierarchyRemovalLane
+  | HierarchyRemovalOnlyLane
+  | HierarchyRebuildOnlyLane
+  | InterpolationLane
+  | VoronoiSweepLane
+  | DcelWalkLane
+  | AlphaPersistenceLane
+  | IntersectionLane
+  | IntersectionOutsideLane
+  | RefineLane
+  deriving stock (Bounded, Enum, Eq, Show)
+
+data LaneRequest = LaneRequest
+  { laneRequestKind :: !LaneKind
+  , laneRequestFirst :: !Int
+  , laneRequestSecond :: !Int
+  }
+  deriving stock (Eq, Show)
+
+data LaneObstruction
+  = UnknownLane !String
+  | UnknownLaneClass !String
+  | MalformedWorkSize !String
+  | NegativeWorkSize !Int
+  | AlphaPersistenceRequiresSpecializedRunner
+  deriving stock (Eq, Show)
+
+data LaneSpec = LaneSpec
+  { laneSpecClass :: !LaneClass
+  , laneSpecRequest :: !LaneRequest
+  , laneSpecDisplay :: !String
+  }
+
+data SnapshotStressSpec = SnapshotStressSpec
+  { snapshotSessionRequest :: !LaneRequest
+  , snapshotPublicationRequest :: !LaneRequest
+  , snapshotDisplay :: !String
+  }
+
+laneKindName :: LaneKind -> String
+laneKindName = \case
+  BulkLoadLane -> "bulk-load"
+  IncrementalLane -> "incremental"
+  SnapshotInsertLane -> "snapshot-insert"
+  SweepAngleCollapseLane -> "sweep-angle-collapse"
+  DegenerateLineLane -> "degenerate-line"
+  BatchSweepLane -> "batch-sweep"
+  NearestLane -> "nearest"
+  CdtRecoveryLane -> "cdt-recovery"
+  ConstraintIncrementalLane -> "constraint-incremental"
+  ConstraintSplitLane -> "constraint-split"
+  PublicationFloorLane -> "publication-floor"
+  RemovalLane -> "removal"
+  SnapshotRemovalLane -> "snapshot-removal"
+  HierarchyIncrementalLane -> "hierarchy-incremental"
+  HierarchyDuplicateLane -> "hierarchy-duplicate"
+  HierarchyRemovalLane -> "hierarchy-removal"
+  HierarchyRemovalOnlyLane -> "hierarchy-removal-only"
+  HierarchyRebuildOnlyLane -> "hierarchy-rebuild-only"
+  InterpolationLane -> "interpolation"
+  VoronoiSweepLane -> "voronoi-sweep"
+  DcelWalkLane -> "dcel-walk"
+  AlphaPersistenceLane -> "alpha-persistence"
+  IntersectionLane -> "intersection"
+  IntersectionOutsideLane -> "intersection-outside"
+  RefineLane -> "refine"
+
+laneRequestLabel :: LaneRequest -> String
+laneRequestLabel request =
+  intercalate
+    "-"
+    [ laneKindName (laneRequestKind request)
+    , show (laneRequestFirst request)
+    , show (laneRequestSecond request)
+    ]
+
+parseLaneRequest :: String -> String -> String -> Either LaneObstruction LaneRequest
+parseLaneRequest rawKind rawFirst rawSecond =
+  LaneRequest
+    <$> parseLaneKind rawKind
+    <*> parseWorkSize rawFirst
+    <*> parseWorkSize rawSecond
+
+renderLaneObstruction :: LaneObstruction -> String
+renderLaneObstruction = \case
+  UnknownLane lane -> "unknown benchmark lane: " <> lane
+  UnknownLaneClass laneClass -> "unknown benchmark lane class: " <> laneClass
+  MalformedWorkSize raw -> "benchmark work size is not an integer: " <> raw
+  NegativeWorkSize size -> "benchmark work size is negative: " <> show size
+  AlphaPersistenceRequiresSpecializedRunner ->
+    "alpha-persistence is owned by moonlight-planar-alpha-spade-referent"
+
+renderInventoryCsv :: String
+renderInventoryCsv =
+  unlines
+    ( fmap renderBoardLane boardLaneSpecs
+        <> fmap renderSnapshotLane snapshotStressSpecs
+    )
+ where
+  renderBoardLane specification =
+    csvRow
+      [ "lane"
+      , laneClassName (laneSpecClass specification)
+      , laneRequestLabel (laneSpecRequest specification)
+      , laneSpecDisplay specification
+      ]
+
+  renderSnapshotLane specification =
+    csvRow
+      [ "lane"
+      , "snapshot"
+      , laneRequestLabel (snapshotPublicationRequest specification)
+      , snapshotDisplay specification
+      , laneRequestLabel (snapshotSessionRequest specification)
+      ]
+
+renderInventoryHuman :: String
+renderInventoryHuman =
+  concatMap renderClass [ParityLane, CliffLane]
+    <> "snapshot lanes ("
+    <> show (length snapshotStressSpecs)
+    <> ")\n"
+    <> concatMap renderSnapshot snapshotStressSpecs
+ where
+  renderClass laneClass =
+    let specifications = laneSpecs laneClass
+     in laneClassName laneClass
+          <> " lanes ("
+          <> show (length specifications)
+          <> ")\n"
+          <> concatMap
+            ((<> "\n") . ("  " <>) . laneRequestLabel . laneSpecRequest)
+            specifications
+
+  renderSnapshot specification =
+    "  "
+      <> laneRequestLabel (snapshotPublicationRequest specification)
+      <> " against "
+      <> laneRequestLabel (snapshotSessionRequest specification)
+      <> "\n"
+
+renderLaneSpecs :: String -> Either LaneObstruction String
+renderLaneSpecs rawClass =
+  unlines . fmap (renderRequestWords . laneSpecRequest) . laneSpecs
+    <$> parseLaneClass rawClass
+
+renderSnapshotSpecs :: String
+renderSnapshotSpecs =
+  unlines
+    ( fmap
+        ( \specification ->
+            renderRequestWords (snapshotSessionRequest specification)
+              <> " "
+              <> renderRequestWords (snapshotPublicationRequest specification)
+        )
+        snapshotStressSpecs
+    )
+
+parseLaneKind :: String -> Either LaneObstruction LaneKind
+parseLaneKind raw =
+  maybe
+    (Left (UnknownLane raw))
+    Right
+    (find ((== raw) . laneKindName) [minBound .. maxBound])
+
+parseLaneClass :: String -> Either LaneObstruction LaneClass
+parseLaneClass = \case
+  "parity" -> Right ParityLane
+  "cliff" -> Right CliffLane
+  other -> Left (UnknownLaneClass other)
+
+parseWorkSize :: String -> Either LaneObstruction Int
+parseWorkSize raw =
+  maybe
+    (Left (MalformedWorkSize raw))
+    (\size -> if size < 0 then Left (NegativeWorkSize size) else Right size)
+    (readMaybe raw)
+
+laneClassName :: LaneClass -> String
+laneClassName = \case
+  ParityLane -> "parity"
+  CliffLane -> "cliff"
+
+laneSpecs :: LaneClass -> [LaneSpec]
+laneSpecs laneClass = filter ((== laneClass) . laneSpecClass) boardLaneSpecs
+
+renderRequestWords :: LaneRequest -> String
+renderRequestWords request =
+  unwords
+    [ laneKindName (laneRequestKind request)
+    , show (laneRequestFirst request)
+    , show (laneRequestSecond request)
+    ]
+
+csvRow :: [String] -> String
+csvRow = intercalate "," . fmap csvField
+
+csvField :: String -> String
+csvField field
+  | any (`elem` [',', '"', '\n', '\r']) field =
+      '"' : concatMap escapeQuote field <> "\""
+  | otherwise = field
+ where
+  escapeQuote '"' = "\"\""
+  escapeQuote character = [character]
+
+boardLaneSpecs :: [LaneSpec]
+boardLaneSpecs =
+  [ parity BulkLoadLane 1000 0 "Bulk load, 1k points"
+  , parity BulkLoadLane 10000 0 "Bulk load, 10k"
+  , parity IncrementalLane 1000 0 "Incremental insert, 1k"
+  , parity IncrementalLane 10000 0 "Incremental insert, 10k"
+  , parity IncrementalLane 500000 0 "Incremental insert, 500k"
+  , parity NearestLane 20000 5000 "Nearest, 5k queries / 20k points"
+  , parity CdtRecoveryLane 8000 800 "CDT recovery, 800 / 8k"
+  , parity RefineLane 625 0 "Refinement, 625 Steiner"
+  , parity RefineLane 2500 0 "Refinement, 2,500 Steiner"
+  , parity RemovalLane 1000 250 "Removal, 250 / 1k"
+  , parity RemovalLane 10000 2500 "Removal, 2.5k / 10k"
+  , parity RemovalLane 100000 25000 "Removal, 25k / 100k"
+  , parity InterpolationLane 10000 1000 "Interpolation, 1k queries / 10k"
+  , parity InterpolationLane 100000 2000 "Interpolation, 2k queries / 100k"
+  , parity VoronoiSweepLane 1000 0 "Voronoi dual sweep, 1k sites"
+  , parity DcelWalkLane 2000 0 "DCEL traversal, 2k points"
+  , parity IntersectionLane 10000 500 "Line intersection, 500 chords / 10k"
+  , parity DegenerateLineLane 2000 0 "Exactly collinear bulk chain, 2k"
+  , cliff ConstraintIncrementalLane 8000 800 "Constraint one at a time, 800 / 8k"
+  , cliff ConstraintSplitLane 1000 0 "Constraint split, 1k crossings"
+  -- This deliberately stays at 1k while its parity twin also reaches 10k.
+  , cliff HierarchyIncrementalLane 1000 0 "Hierarchy insert, 1k"
+  , cliff HierarchyDuplicateLane 10000 500 "Hierarchy duplicate insert, 500 / 10k"
+  , cliff HierarchyRemovalLane 10000 250 "Hierarchy removal, 250 / 10k"
+  , cliff SweepAngleCollapseLane 2000 0 "Sweep hull index at one angle, 2k"
+  , cliff IntersectionOutsideLane 2000 100 "Line intersection from outside the hull, 100 / 2k"
+  ]
+ where
+  parity :: LaneKind -> Int -> Int -> String -> LaneSpec
+  parity kind first second display =
+    LaneSpec ParityLane (LaneRequest kind first second) display
+
+  cliff :: LaneKind -> Int -> Int -> String -> LaneSpec
+  cliff kind first second display =
+    LaneSpec CliffLane (LaneRequest kind first second) display
+
+snapshotStressSpecs :: [SnapshotStressSpec]
+snapshotStressSpecs =
+  [ SnapshotStressSpec
+      (LaneRequest IncrementalLane 1000 0)
+      (LaneRequest SnapshotInsertLane 1000 0)
+      "Insert snapshots, 1k points"
+  , SnapshotStressSpec
+      (LaneRequest RemovalLane 10000 2500)
+      (LaneRequest SnapshotRemovalLane 10000 2500)
+      "Removal snapshots, 2.5k / 10k"
+  ]
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Removal.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Removal.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Removal.hs
@@ -0,0 +1,434 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Removal lanes, their gates, and the removal diagnostics.
+module Moonlight.Planar.Bench.SpadeCompare.Removal where
+
+import Control.Monad (when)
+import Data.Foldable (traverse_)
+import Data.List (sortOn)
+import System.Exit (exitWith, ExitCode (ExitFailure))
+import System.FilePath ((</>))
+import System.IO (hPutStrLn, stderr)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (delaunay, empty)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Handles.HandleDefs (VertexId, reverseEdge)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces, undirectedEdges, vertices)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.PointLocation
+import Moonlight.Planar.Removal
+import Moonlight.Planar.SetAlgebra (difference)
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Telemetry (removeManyAt, withSession)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Predicates qualified as Predicates
+import Moonlight.Planar.Session
+import Moonlight.Planar.Bench.SpadeCompare.Support
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+
+-- | The removal gate: a deterministic prefix of the input, removed by
+-- coordinate, and the surviving canonical edge set. Delaunay triangulations
+-- of point sets in general position are unique, so the survivor is forced
+-- and both implementations must agree bit-for-bit.
+writeRemovalGate :: FilePath -> Int -> Int -> IO ()
+writeRemovalGate directory pointCount removalCount = do
+  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
+  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
+  surviving <- removeAllByCoordinate triangulation (V.fromList (take removalCount points))
+  writeFile
+    (directory </> ("removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt"))
+    (unlines (canonicalEdges surviving))
+
+-- | Remove vertices by coordinate inside one session: one O(n) dense thaw and
+-- publication for the whole program, then O(degree) topology work per removal.
+-- The snapshot 'locateAndRemove' fold pays that O(n) boundary per removal.
+removeAllByCoordinate
+  :: DelaunayTriangulation Point
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+removeAllByCoordinate triangulation removals =
+  case withSession triangulation 0 (removeManyAt removals) of
+    Left refusal -> fail (show refusal)
+    Right (outcomes, surviving, _) -> do
+      V.imapM_ checkRemoved outcomes
+      pure surviving
+ where
+  checkRemoved index outcome = case outcome of
+    Nothing ->
+      error
+        ( "removal target is not a vertex: index="
+            <> show index
+            <> " point="
+            <> ( case removals V.! index of
+                  Point x y -> hex64 x <> "," <> hex64 y
+               )
+        )
+    Just _ -> pure ()
+
+-- | Removal by coordinate through the persistent entry point: a thaw and a
+-- publication per removal, against the session fold's one of each for the whole
+-- program. Same removals, same order, same survivor — only the price differs.
+removeAllPersistently
+  :: DelaunayTriangulation Point
+  -> V.Vector Point
+  -> IO (DelaunayTriangulation Point)
+removeAllPersistently =
+  V.foldM'
+    (\current point -> do
+      outcome <- require (locateAndRemove current point)
+      case outcome of
+        Nothing -> fail ("removal target is not a vertex: " <> pointHex point)
+        Just result -> pure (removalTriangulation result)
+    )
+
+writePersistentRemovalGate :: FilePath -> Int -> Int -> IO ()
+writePersistentRemovalGate directory pointCount removalCount = do
+  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
+  base <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
+  surviving <- removeAllPersistently base (V.fromList (take removalCount points))
+  writeFile
+    ( directory
+        </> ("persistent-removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt")
+    )
+    (unlines (canonicalEdges surviving))
+
+-- | Removal audited against the one oracle that needs no second implementation:
+-- the Delaunay triangulation of a point set in general position is unique, so
+-- removing a prefix must leave exactly the triangulation a fresh bulk load of
+-- the survivors produces. Reports the first removal at which that identity
+-- breaks, so a divergence is dated rather than merely observed.
+--
+-- Every removal also reports the handle swap-compaction relocated into the
+-- freed slot, and that handle is the caller's only re-anchor after a removal
+-- invalidates the rest. An out-of-range or misnamed handle is invisible to the
+-- edge-set oracle, so it is checked here against the vertex the mesh moved.
+reportRemovalAudit :: Int -> Int -> IO ()
+reportRemovalAudit pointCount removalCount = do
+  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+  base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
+  let step current index
+        | index >= removalCount = putStrLn ("clean-through " <> show removalCount)
+        | otherwise =
+            case allPoints V.!? index of
+              Nothing -> putStrLn ("input-exhausted-at-removal " <> show index)
+              Just point -> do
+                outcome <- require (locateAndRemove current point)
+                case outcome of
+                  Nothing -> putStrLn ("located-nothing-at-removal " <> show index)
+                  Just result -> do
+                    let nextTriangulation = removalTriangulation result
+                        movedPoint =
+                          vertexPoint current (VertexId (fromIntegral (numVertices current - 1)))
+                    case removalOutcomeSwap (removalOutcome result) of
+                      Nothing
+                        | vertexPoint current (VertexId (fromIntegral (numVertices current - 1))) /= point ->
+                            putStrLn ("swap-handle-missing-at-removal " <> show index)
+                        | otherwise -> pure ()
+                      Just (swappedIn@(VertexId rawSwappedIn), _)
+                        | toInteger rawSwappedIn >= toInteger (numVertices nextTriangulation) ->
+                            putStrLn
+                              ( "swap-handle-out-of-range-at-removal "
+                                  <> show index
+                                  <> " handle="
+                                  <> show rawSwappedIn
+                                  <> " vertices="
+                                  <> show (numVertices nextTriangulation)
+                              )
+                        | vertexPoint nextTriangulation swappedIn /= movedPoint ->
+                            putStrLn
+                              ( "swap-handle-misnamed-at-removal "
+                                  <> show index
+                                  <> " handle="
+                                  <> show rawSwappedIn
+                              )
+                        | otherwise -> pure ()
+                    fresh <-
+                      buildTriangulation
+                        <$> require (delaunay unitElementDefaults (V.drop (index + 1) allPoints))
+                    let removalEdges = canonicalEdges nextTriangulation
+                        freshEdges = canonicalEdges fresh
+                    if removalEdges == freshEdges
+                      then step nextTriangulation (index + 1)
+                      else do
+                        let (onlyRemoval, onlyFresh) = sortedDifference removalEdges freshEdges
+                        putStrLn ("diverged-at-removal " <> show index)
+                        putStrLn
+                          ( "removed-point "
+                              <> case point of
+                                Point x y -> hex64 x <> "," <> hex64 y
+                          )
+                        putStrLn ("removal-vertices " <> show (numVertices nextTriangulation))
+                        putStrLn ("fresh-vertices " <> show (numVertices fresh))
+                        putStrLn ("removal-edges " <> show (length removalEdges))
+                        putStrLn ("fresh-edges " <> show (length freshEdges))
+                        putStrLn ("only-in-removal " <> show (length onlyRemoval))
+                        putStrLn ("only-in-fresh " <> show (length onlyFresh))
+                        let (invertedRemoval, illegalRemoval) = meshHealth nextTriangulation
+                            (invertedFresh, illegalFresh) = meshHealth fresh
+                        putStrLn ("removal-inverted-faces " <> show invertedRemoval)
+                        putStrLn ("removal-illegal-half-edges " <> show illegalRemoval)
+                        putStrLn ("fresh-inverted-faces " <> show invertedFresh)
+                        putStrLn ("fresh-illegal-half-edges " <> show illegalFresh)
+                        traverse_ (putStrLn . ("  removal-only " <>)) (take 8 onlyRemoval)
+                        traverse_ (putStrLn . ("  fresh-only " <>)) (take 8 onlyFresh)
+  step base 0
+
+-- | The geometry around one removal, reconstructed with public API only. For
+-- an interior removal the cavity border is the cyclic link of the removed
+-- vertex, and nothing but a drain flip can destroy one of those non-star
+-- edges: cleanup removes only the star, and swap-remove touches only the
+-- removed vertex. The reported hull predicate guards that interpretation.
+-- The fan origin 'remeshRing' would pick is also public: 'collectOutgoing'
+-- starts at the stored out-edge, so the origin is the destination of that
+-- edge.
+reportRemovalContext :: Int -> Int -> IO ()
+reportRemovalContext pointCount target = do
+  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
+  case allPoints V.!? target of
+    Nothing -> do
+      hPutStrLn stderr "removal-context: target index out of range"
+      exitWith (ExitFailure 2)
+    Just removedPoint -> do
+      base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
+      before <- removeAllByCoordinate base (V.take target allPoints)
+      after <- removeAllByCoordinate before (V.singleton removedPoint)
+      fresh <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop (target + 1) allPoints))
+      lab <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop target allPoints))
+      removedQuery <- require (mkQueryPoint removedPoint)
+      putStrLn ("lab-clean " <> show (canonicalEdges before == canonicalEdges lab))
+      case locatePoint before removedQuery of
+        OnVertex removedVertex -> do
+          let ring = ringOf before removedVertex
+              ringPoints = map snd ring
+              ringMember point = any (== point) ringPoints
+              rOnHull = any (\(edge, _) -> incidentFace before edge == outerFace) ring
+              afterPairs = canonicalEdgePairs after
+              freshPairs = canonicalEdgePairs fresh
+              borderPairs = cyclicPairs ringPoints
+              destroyedBorder = [pair | pair <- borderPairs, not (any (== pair) afterPairs)]
+              removalOnly = [pair | pair <- afterPairs, not (any (== pair) freshPairs)]
+              freshOnly = [pair | pair <- freshPairs, not (any (== pair) afterPairs)]
+          putStrLn ("r-on-hull " <> show rOnHull)
+          putStrLn ("ring-size " <> show (length ringPoints))
+          traverse_
+            (\(index, (_, point)) -> putStrLn ("  ring " <> show index <> " " <> pointHex point))
+            (zip [(0 :: Int) ..] ring)
+          putStrLn ("border-edges-before " <> show (length borderPairs))
+          putStrLn ("border-edges-destroyed " <> show (length destroyedBorder))
+          traverse_
+            (\(p, q) -> putStrLn ("  destroyed " <> pointHex p <> " " <> pointHex q))
+            destroyedBorder
+          case ringPoints of
+            [] -> putStrLn "fan-unavailable empty-ring"
+            fanOriginPoint : ringTail ->
+              if rOnHull
+                then putStrLn "fan-unavailable hull-removal"
+                else do
+                  let fanOrientations =
+                        zipWith3
+                          (\index p q -> (index, p, q, Predicates.orient2d fanOriginPoint p q))
+                          [(1 :: Int) ..]
+                          ringTail
+                          (drop 1 ringTail)
+                      fanCandidates =
+                        zipWith3
+                          ( \(index, priorPoint) current following ->
+                              ( index
+                              , current
+                              , Predicates.orient2d priorPoint following fanOriginPoint == GT
+                                  && Predicates.orient2d following priorPoint current == GT
+                              , Predicates.inCircle current fanOriginPoint priorPoint following
+                              )
+                          )
+                          (zip [(2 :: Int) ..] ringTail)
+                          (drop 1 ringTail)
+                          (drop 2 ringTail)
+                  putStrLn ("fan-origin " <> pointHex fanOriginPoint)
+                  putStrLn
+                    ( "fan-inverted "
+                        <> show (length [() | (_, _, _, verdict) <- fanOrientations, verdict == LT])
+                        <> " of "
+                        <> show (length fanOrientations)
+                    )
+                  traverse_
+                    ( \(index, p, q, verdict) ->
+                        when (verdict == LT) $
+                          putStrLn
+                            ( "  fan-inverted-at "
+                                <> show index
+                                <> " "
+                                <> pointHex fanOriginPoint
+                                <> " "
+                                <> pointHex p
+                                <> " "
+                                <> pointHex q
+                            )
+                    )
+                    fanOrientations
+                  traverse_
+                    ( \(index, current, convex, circle) ->
+                        putStrLn
+                          ( "  fan-candidate "
+                              <> show index
+                              <> " "
+                              <> pointHex current
+                              <> "->"
+                              <> pointHex fanOriginPoint
+                              <> " convex="
+                              <> show convex
+                              <> " incircle="
+                              <> show circle
+                          )
+                    )
+                    fanCandidates
+          putStrLn ("only-in-removal " <> show (length removalOnly))
+          traverse_
+            ( \(p, q) ->
+                putStrLn
+                  ( "  removal-only "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " ring="
+                      <> show (ringMember p, ringMember q)
+                  )
+            )
+            removalOnly
+          putStrLn ("only-in-fresh " <> show (length freshOnly))
+          traverse_
+            ( \(p, q) ->
+                putStrLn
+                  ( "  fresh-only "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " ring="
+                      <> show (ringMember p, ringMember q)
+                  )
+            )
+            freshOnly
+          let inverted = invertedFaces after
+          putStrLn ("inverted-faces " <> show (length inverted))
+          traverse_
+            ( \(p, q, r) ->
+                putStrLn
+                  ( "  inverted-face "
+                      <> pointHex p
+                      <> " "
+                      <> pointHex q
+                      <> " "
+                      <> pointHex r
+                      <> " ring="
+                      <> show (ringMember p, ringMember q, ringMember r)
+                  )
+            )
+            inverted
+        other -> putStrLn ("locate-unexpected " <> show other)
+ where
+  ringOf triangulation vertex =
+    case vertexOutEdge triangulation vertex of
+      Nothing -> []
+      Just start -> walk start start []
+   where
+    walk start current acc =
+      let ringVertex = destination triangulation current
+          nextEdge = counterClockwise triangulation current
+          acc' = (current, vertexPoint triangulation ringVertex) : acc
+       in if nextEdge == start
+            then reverse acc'
+            else walk start nextEdge acc'
+
+  cyclicPairs (first : second : rest) =
+    zipWith canonicalPair (first : second : rest) ((second : rest) ++ [first])
+  cyclicPairs _ = []
+
+  canonicalEdgePairs triangulation =
+    sortOn
+      (\(p, q) -> (pointKey p, pointKey q))
+      [ canonicalPair p q
+      | edge <- undirectedEdges triangulation
+      , let (from, to) = undirectedEndpoints triangulation edge
+            p = vertexPoint triangulation from
+            q = vertexPoint triangulation to
+      ]
+
+  canonicalPair p q =
+    if pointKey p <= pointKey q then (p, q) else (q, p)
+
+  pointKey (Point x y) = (x, y)
+
+  invertedFaces triangulation =
+    [ (vertexPoint triangulation a, vertexPoint triangulation b, vertexPoint triangulation c)
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    , Predicates.orient2d
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        == LT
+    ]
+
+-- | Two independent health measures, which together decide what a divergence
+-- means. Delaunay's theorem says a /valid/ triangulation with no locally
+-- illegal edge is globally Delaunay — so a mesh that differs from the fresh
+-- build while carrying no illegal edge cannot be a valid triangulation, and
+-- one carrying illegal edges was simply left under-legalized.
+meshHealth
+  :: Triangulation mode vertex directed undirected face
+  -> (Int, Int)
+meshHealth triangulation =
+  ( length [() | face <- innerFaces triangulation, not (isCounterClockwise face)]
+  , length
+      [ ()
+      | face <- innerFaces triangulation
+      , edge <- faceEdgeList face
+      , edgeIsIllegal edge
+      ]
+  )
+ where
+  faceEdgeList face = case innerFaceDirectedEdges triangulation face of
+    Nothing -> []
+    Just (e0, e1, e2) -> [e0, e1, e2]
+
+  isCounterClockwise face = case innerFaceVertices triangulation face of
+    Nothing -> True
+    Just (a, b, c) ->
+      Predicates.orient2d
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        == GT
+
+  edgeIsIllegal edge =
+    let twin = reverseEdge edge
+     in incidentFace triangulation twin /= outerFace
+          && Predicates.inCircle
+            (vertexPoint triangulation (origin triangulation edge))
+            (vertexPoint triangulation (destination triangulation edge))
+            (vertexPoint triangulation (destination triangulation (next triangulation edge)))
+            (vertexPoint triangulation (destination triangulation (next triangulation twin)))
+            == GT
+
+-- | The Delaunay triangulation of the survivors, built from scratch. Diffed
+-- against a removal result it decides whether that result is the Delaunay
+-- triangulation it is required to be.
+writeFreshSuffix :: Int -> Int -> FilePath -> IO ()
+writeFreshSuffix pointCount dropped path = do
+  triangulation <-
+    buildTriangulation
+      <$> require
+        ( delaunay
+            unitElementDefaults
+            (V.drop dropped (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)))
+        )
+  writeFile path (unlines (canonicalEdges triangulation))
+
+-- | Symmetric difference of two ascending lists, by merge.
+sortedDifference :: Ord a => [a] -> [a] -> ([a], [a])
+sortedDifference [] right = ([], right)
+sortedDifference left [] = (left, [])
+sortedDifference left@(l : ls) right@(r : rs)
+  | l == r = sortedDifference ls rs
+  | l < r = let (onlyLeft, onlyRight) = sortedDifference ls right in (l : onlyLeft, onlyRight)
+  | otherwise = let (onlyLeft, onlyRight) = sortedDifference left rs in (onlyLeft, r : onlyRight)
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Support.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Support.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Support.hs
@@ -0,0 +1,172 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Input generation, canonical encoding, and the shared build helpers.
+module Moonlight.Planar.Bench.SpadeCompare.Support where
+
+import Data.List (sort)
+import Data.Word (Word64)
+import GHC.Float (castDoubleToWord64)
+import Numeric (showHex)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Cdt (constraintEdges)
+import Moonlight.Planar.Dcel (undirectedEndpoints, vertexPoint)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (undirectedEdges)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Math (orient2d)
+import Moonlight.Planar.Types (ConstrainedDelaunayTriangulation, DelaunayTriangulation, Triangulation, buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (HasPosition (..), Point (..), QueryPoint)
+import Moonlight.Planar.Telemetry (LocationHint (..), nearestNeighbor)
+import Moonlight.Planar.Bench.SpadeCompare.Input (randomPoints)
+import Moonlight.Planar.HintGenerator
+
+-- | The same generated points flattened onto a sliver a picometre thick. No
+-- coordinate becomes equal to another and no four points become cocircular, so
+-- the input stays in general position and the load stays on the ordinary
+-- face-building path; what collapses is the angular spread the bulk loaders
+-- index their hulls by. That collapse is the whole of what this input
+-- measures, and it does not reach the face-less location path — that one needs
+-- exact collinearity, which 'exactlyCollinearPoints' supplies and gates.
+nearCollinearPoints :: Word64 -> Int -> [Point]
+nearCollinearPoints seed count =
+  [Point x (y * 1.0e-12) | Point x y <- randomPoints seed count]
+
+-- | Points on one exact line: @(i, 0)@, every coordinate an integer that a
+-- 'Double' holds exactly. No three are in general position, so 'orient2d'
+-- returns an exact zero on both sides and no face is ever built.
+--
+-- Uniqueness does not need general position, only one valid answer. A
+-- collinear set admits exactly one triangulation — the chain of consecutive
+-- segments, @n - 1@ of them and no triangle — so the canonical edge set is
+-- fully determined and gates bit for bit against the strict side. Both
+-- implementations state that same count as their own degenerate Euler
+-- invariant, which is what makes this a shared answer rather than a
+-- coincidence.
+exactlyCollinearPoints :: Int -> [Point]
+exactlyCollinearPoints count = [Point (fromIntegral index) 0 | index <- [0 .. count - 1]]
+
+hex64 :: Double -> String
+hex64 value =
+  let raw = showHex (castDoubleToWord64 value) ""
+   in replicate (16 - length raw) '0' <> raw
+
+-- | Canonical edge set: each undirected edge as its two endpoint coordinates in
+-- IEEE-754 bit patterns, endpoint-ordered then globally sorted. Formatting
+-- cannot introduce a spurious disagreement and rounding cannot hide a real one.
+canonicalEdges :: Triangulation mode vertex directed undirected face -> [String]
+canonicalEdges triangulation =
+  sort
+    [ encodeCanonicalEdge first second
+    | edge <- undirectedEdges triangulation
+    , let (from, to) = undirectedEndpoints triangulation edge
+          p = vertexPoint triangulation from
+          q = vertexPoint triangulation to
+          (first, second) = if key p <= key q then (p, q) else (q, p)
+    ]
+ where
+  key (Point x y) = (x, y)
+
+canonicalConstraintEdges
+  :: ConstrainedDelaunayTriangulation vertex
+  -> [String]
+canonicalConstraintEdges triangulation =
+  sort
+    [ encodeCanonicalEdge first second
+    | edge <- constraintEdges triangulation
+    , let (from, to) = undirectedEndpoints triangulation edge
+          p = vertexPoint triangulation from
+          q = vertexPoint triangulation to
+          (first, second) =
+            if pointKey p <= pointKey q
+              then (p, q)
+              else (q, p)
+    ]
+ where
+  pointKey (Point x y) = (x, y)
+
+encodeCanonicalEdge :: Point -> Point -> String
+encodeCanonicalEdge (Point ax ay) (Point bx by) =
+  concatMap hex64 [ax, ay, bx, by]
+
+coordHex :: Point -> String
+coordHex (Point x y) = hex64 x <> hex64 y
+
+-- | A cycle, rotated to begin at its smallest encoded element.
+--
+-- Both implementations keep one entry edge per vertex and per face — whichever
+-- their builder wrote last — so the two walk the same cycle from different
+-- places. Rotating pins the order, which the mesh determines, and declines to
+-- pin the starting point, which it does not. The rotation compares the encoded
+-- strings rather than the coordinates so that both sides order by the same
+-- comparison: the bit pattern of a negative double does not sort in numeric
+-- order, so a Haskell 'Ord' on 'Point' and a Rust @partial_cmp@ on a tuple
+-- would each have to be argued to agree where two byte strings simply do.
+rotateToSmallest :: [String] -> [String]
+rotateToSmallest [] = []
+rotateToSmallest ring =
+  let !smallest = minimum ring
+      (before, after) = break (== smallest) ring
+   in after <> before
+
+require :: Show failure => Either failure value -> IO value
+require (Left failure) = fail (show failure)
+require (Right value) = pure value
+
+requireQueryPoints
+  :: Traversable container
+  => container Point
+  -> IO (container QueryPoint)
+requireQueryPoints = require . traverse mkQueryPoint
+
+requireQueryChords
+  :: Traversable container
+  => container (Point, Point)
+  -> IO (container (QueryPoint, QueryPoint))
+requireQueryChords =
+  require
+    . traverse
+      (\(from, to) -> (,) <$> mkQueryPoint from <*> mkQueryPoint to)
+
+delaunayOf :: Int -> IO (DelaunayTriangulation Point)
+delaunayOf count =
+  buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x9e3779b97f4a7c15 count)))
+
+-- | One line per query: the coordinates of the nearest vertex the hierarchy
+-- routed the search to, or @none@.
+nearestAnswer
+  :: DelaunayTriangulation Point
+  -> HierarchyHint
+  -> QueryPoint
+  -> String
+nearestAnswer triangulation hierarchy query =
+  let hint = case hierarchyHint hierarchy query of
+        Just (VertexHint vertex) -> Just vertex
+        _ -> Nothing
+   in case nearestNeighbor triangulation hint query of
+        Nothing -> "none"
+        Just (vertex, _) ->
+          let Point x y = vertexPoint triangulation vertex
+           in hex64 x <> hex64 y
+
+pointHex :: Point -> String
+pointHex (Point x y) = hex64 x <> "," <> hex64 y
+
+constraintPairs :: Int -> Int -> [(Int, Int)]
+constraintPairs pointCount constraintCount =
+  take constraintCount
+    [ (a, b)
+    | index <- [0 ..]
+    , let a = index `mod` pointCount
+          b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
+    , a /= b
+    ]
+
+-- | A closed square with no dangling constraint. The interior segment used by
+-- the package's own benchmark is outside spade's documented "closed shape"
+-- contract and makes it refuse the whole domain — not a workload the two can be
+-- timed on.
+refinementInput :: (V.Vector Point, V.Vector (Int, Int))
+refinementInput =
+  ( V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64]
+  , V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
+  )
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Timing.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Timing.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Timing.hs
@@ -0,0 +1,23 @@
+-- | The single fresh-process timing boundary shared by both Haskell runners.
+module Moonlight.Planar.Bench.SpadeCompare.Timing
+  ( measureLane
+  ) where
+
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import GHC.Clock (getMonotonicTimeNSec)
+import Moonlight.Planar.Bench.SpadeCompare.Lane
+  ( LaneRequest
+  , laneRequestLabel
+  )
+import System.Mem (performGC)
+
+-- | Time exactly one fully forced iteration. Input construction belongs to
+-- each interpreter before this boundary; the driver supplies fresh processes.
+measureLane :: NFData value => LaneRequest -> IO value -> IO ()
+measureLane request work = do
+  performGC
+  start <- getMonotonicTimeNSec
+  _ <- work >>= evaluate . force
+  stop <- getMonotonicTimeNSec
+  putStrLn (laneRequestLabel request <> "," <> show (stop - start))
diff --git a/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Voronoi.hs b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Voronoi.hs
new file mode 100644
--- /dev/null
+++ b/bench/spade-compare/hs/Moonlight/Planar/Bench/SpadeCompare/Voronoi.hs
@@ -0,0 +1,160 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The Voronoi dual gate and the dual sweep lane.
+module Moonlight.Planar.Bench.SpadeCompare.Voronoi where
+
+import Data.List (sort)
+import System.FilePath ((</>))
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Handles.HandleDefs (DirectedEdgeId)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces, vertices)
+import Moonlight.Planar.SetAlgebra (difference)
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (HasPosition (..), Point (..))
+import Moonlight.Planar.Voronoi
+import Moonlight.Planar.Bench.SpadeCompare.Support
+
+-- | The dual, pinned by its combinatorics and by nothing else.
+--
+-- Every verb the dual offers resolves a primal element, so its answers are
+-- exact over a mesh the bulk-load gate already pins bit for bit. Voronoi vertex
+-- POSITIONS are the exception and are deliberately absent from every artifact
+-- here: a position is a circumcentre, and the two implementations do not form
+-- one the same way. spade takes the reciprocal of the determinant and
+-- multiplies by it; this side first rescales both difference vectors by their
+-- largest component and only then divides. Each rounds where the other does
+-- not, so the positions cannot be bit-identical, and a gate over them would be
+-- grading the arithmetic rather than the dual. What IS determined about a
+-- position is whether it exists at all, and the vertex artifact pins that.
+--
+-- Direction vectors are pinned in full: each component is one subtraction of
+-- two stored coordinates on both sides, so there is nothing left to round.
+writeVoronoiGate :: FilePath -> Int -> IO ()
+writeVoronoiGate directory count = do
+  triangulation <- delaunayOf count
+  let prefix = directory </> ("voronoi-" <> show count)
+  writeFile
+    (prefix <> "-dual.txt")
+    (unlines (sort (map (dualEdgeRecord triangulation) (directedVoronoiEdges triangulation))))
+  writeFile (prefix <> "-cells.txt") (unlines (voronoiCellLines triangulation))
+  writeFile (prefix <> "-vertices.txt") (unlines (voronoiVertexLines triangulation))
+
+-- | One directed dual edge: the primal edge it names, the site of the dual face
+-- to its left, the dual vertex at each end, the destinations its dual @next@
+-- and @prev@ reach, and its direction vector. @next@ and @prev@ rotate about
+-- the primal origin, so naming them by destination alone loses nothing.
+dualEdgeRecord
+  :: DelaunayTriangulation Point
+  -> DirectedVoronoiEdgeId
+  -> String
+dualEdgeRecord triangulation edge =
+  concat
+    [ coordHex (site (origin triangulation primal))
+    , coordHex (site (destination triangulation primal))
+    , coordHex (site (voronoiFaceSite (voronoiIncidentFace triangulation edge)))
+    , dualVertexName triangulation primal (voronoiFrom triangulation edge)
+    , dualVertexName triangulation primal (voronoiTo triangulation edge)
+    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiNext triangulation edge))))
+    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiPrevious triangulation edge))))
+    , coordHex (voronoiDirectionVector triangulation edge)
+    ]
+ where
+  primal = asDelaunayDirectedEdge edge
+  site = vertexPoint triangulation
+
+-- | An inner dual vertex named by the one corner of its dual face that is not
+-- an endpoint of the edge the question was asked about — the record already
+-- carries the other two, so the apex completes the face. An outer dual vertex
+-- is the edge itself and needs no name.
+--
+-- The @X@ tag cannot be reached: an inner dual vertex names an inner face by
+-- construction, and 'innerFaceVertices' refuses only the outer one. spade
+-- cannot emit it at all, its dual vertex carrying a face handle already typed
+-- inner, so an @X@ appearing here is a disagreement rather than a crash.
+dualVertexName
+  :: DelaunayTriangulation Point
+  -> DirectedEdgeId
+  -> VoronoiVertexId
+  -> String
+dualVertexName _ _ (OuterVoronoiVertex _) = "O"
+dualVertexName triangulation primal (InnerVoronoiVertex face) =
+  case innerFaceVertices triangulation face of
+    Nothing -> "X"
+    Just (a, b, c)
+      | isApex a -> named a
+      | isApex b -> named b
+      | otherwise -> named c
+ where
+  from = origin triangulation primal
+  to = destination triangulation primal
+  isApex vertex = vertex /= from && vertex /= to
+  named vertex = "I" <> coordHex (vertexPoint triangulation vertex)
+
+-- | The dual faces, one line each, plus the three enumerator lengths. The
+-- undirected dual carries no artifact of its own: it is the primal undirected
+-- edge set relabelled, which every existing edge gate already pins, so its
+-- length is the only claim left to make about it.
+voronoiCellLines :: DelaunayTriangulation Point -> [String]
+voronoiCellLines triangulation =
+  [ "faces " <> show (length (voronoiFaces triangulation))
+  , "directed " <> show (length (directedVoronoiEdges triangulation))
+  , "undirected " <> show (length (undirectedVoronoiEdges triangulation))
+  ]
+    <> sort
+      [ coordHex (vertexPoint triangulation (voronoiFaceSite face)) <> concat (rotateToSmallest ring)
+      | face <- voronoiFaces triangulation
+      , let ring =
+              [ coordHex
+                  ( vertexPoint
+                      triangulation
+                      (destination triangulation (asDelaunayDirectedEdge edge))
+                  )
+              | edge <- voronoiFaceAdjacentEdges triangulation face
+              ]
+      ]
+
+-- | The inner dual vertices, keyed by their dual face's three corners. Each
+-- line carries whether the vertex has a position — the only exactly determined
+-- thing about a circumcentre — and the outgoing triple the dual reports.
+voronoiVertexLines :: DelaunayTriangulation Point -> [String]
+voronoiVertexLines triangulation =
+  sort
+    [ concat (sort (map coordHex [site a, site b, site c]))
+        <> existence dual
+        <> concat (rotateToSmallest (outgoing dual))
+    | face <- innerFaces triangulation
+    , Just (a, b, c) <- [innerFaceVertices triangulation face]
+    , let dual = InnerVoronoiVertex face
+    ]
+ where
+  site = vertexPoint triangulation
+
+  existence dual = case voronoiVertexPosition triangulation dual of
+    Nothing -> "N"
+    Just _ -> "J"
+
+  outgoing dual = case voronoiVertexOutgoingEdges triangulation dual of
+    Nothing -> []
+    Just edges ->
+      [ coordHex (site (origin triangulation primal)) <> coordHex (site (destination triangulation primal))
+      | edge <- edges
+      , let primal = asDelaunayDirectedEdge edge
+      ]
+
+-- | The whole dual, walked the way spade's own documentation walks it: every
+-- dual face, its adjacent dual edges, and the dual vertex each of those runs
+-- to. The position of an inner dual vertex is summed rather than merely
+-- reached, because a 'Maybe' scrutinised only to its outer constructor can wrap
+-- a thunk and the lane would then be timing the allocation of a circumcentre
+-- instead of the circumcentre. Unbounded cells are counted, so a dual that
+-- stopped classifying hull edges as outer moves the answer rather than the cost.
+voronoiSweep :: DelaunayTriangulation Point -> (Double, Int)
+voronoiSweep triangulation = foldl' overCell (0, 0) (voronoiFaces triangulation)
+ where
+  overCell accumulator face =
+    foldl' overEdge accumulator (voronoiFaceAdjacentEdges triangulation face)
+
+  overEdge (!total, !unbounded) edge =
+    case voronoiVertexPosition triangulation (voronoiTo triangulation edge) of
+      Nothing -> (total, unbounded + 1)
+      Just (Point x y) -> (total + x + y, unbounded)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Alpha.hs
+++ /dev/null
@@ -1,126 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Matched end-to-end alpha-persistence work and its canonical receipt.
--- Input generation remains outside the timed action; Delaunay construction,
--- exact alpha births, cellular lowering, one reduction, and all critical-birth
--- Betti queries remain inside it.
-module Moonlight.Triangulation.Bench.SpadeCompare.Alpha
-  ( AlphaPersistenceObstruction (..)
-  , AlphaPersistenceReceipt (..)
-  , alphaPersistenceReceipt
-  , renderAlphaPersistenceReceipt
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.Bits (xor)
-import Data.List (intercalate)
-import Data.Vector (Vector)
-import Data.Vector.Unboxed qualified as UnboxedVector
-import Data.Word (Word64)
-import GHC.Generics (Generic)
-import Moonlight.Homology.Boundary
-  ( degreeCardinality
-  )
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..)
-  , HomologyFailure
-  , PersistencePair (..)
-  )
-import Moonlight.Homology.Persistence
-  ( criticalBettiTableValues
-  , filteredBaseComplex
-  , filteredCriticalValues
-  , mod2PersistentPairsWithCriticalBettiTable
-  )
-import Moonlight.Triangulation.Alpha
-  ( AlphaFiltrationError
-  , alphaFiltration
-  )
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellComplex
-  ( DCELError
-  , filteredAlphaComplex
-  )
-import Moonlight.Triangulation.Types
-  ( BuildError
-  , Point
-  )
-
-data AlphaPersistenceObstruction
-  = AlphaPersistenceBuildRefused !BuildError
-  | AlphaPersistenceBirthRefused !AlphaFiltrationError
-  | AlphaPersistenceLoweringRefused !DCELError
-  | AlphaPersistenceReductionRefused !HomologyFailure
-  deriving stock (Eq, Show)
-
-data AlphaPersistenceReceipt = AlphaPersistenceReceipt
-  { alphaReceiptCriticalBirths :: !Int
-  , alphaReceiptVertexCells :: !Int
-  , alphaReceiptEdgeCells :: !Int
-  , alphaReceiptFaceCells :: !Int
-  , alphaReceiptFinitePairs :: !Int
-  , alphaReceiptEssentialPairs :: !Int
-  , alphaReceiptProfileChecksum :: !Word64
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-alphaPersistenceReceipt
-  :: Vector Point
-  -> Either AlphaPersistenceObstruction AlphaPersistenceReceipt
-alphaPersistenceReceipt points = do
-  triangulation <-
-    first AlphaPersistenceBuildRefused (delaunayGeometry points)
-  filtration <-
-    first AlphaPersistenceBirthRefused (alphaFiltration triangulation)
-  filtered <-
-    first AlphaPersistenceLoweringRefused (filteredAlphaComplex filtration)
-  (pairs, criticalBettiTable) <-
-    first AlphaPersistenceReductionRefused
-      (mod2PersistentPairsWithCriticalBettiTable filtered)
-  let criticalBirths = filteredCriticalValues filtered
-      finite = filteredBaseComplex filtered
-      isFinitePair = maybe False (const True) . persistenceDeath
-  pure
-    AlphaPersistenceReceipt
-      { alphaReceiptCriticalBirths = length criticalBirths
-      , alphaReceiptVertexCells = degreeCardinality finite (HomologicalDegree 0)
-      , alphaReceiptEdgeCells = degreeCardinality finite (HomologicalDegree 1)
-      , alphaReceiptFaceCells = degreeCardinality finite (HomologicalDegree 2)
-      , alphaReceiptFinitePairs = length (filter isFinitePair pairs)
-      , alphaReceiptEssentialPairs = length (filter (not . isFinitePair) pairs)
-      , alphaReceiptProfileChecksum =
-          UnboxedVector.foldl'
-            mixChecksum
-            fnvOffset
-            (criticalBettiTableValues criticalBettiTable)
-      }
-
-mixChecksum :: Word64 -> Int -> Word64
-mixChecksum accumulator value =
-  (accumulator `xor` fromIntegral value) * fnvPrime
-
-fnvOffset :: Word64
-fnvOffset = 14695981039346656037
-
-fnvPrime :: Word64
-fnvPrime = 1099511628211
-
-renderAlphaPersistenceReceipt :: AlphaPersistenceReceipt -> String
-renderAlphaPersistenceReceipt receipt =
-  intercalate
-    ","
-    ( fmap
-        show
-        [ fromIntegral (alphaReceiptCriticalBirths receipt) :: Word64
-        , fromIntegral (alphaReceiptVertexCells receipt)
-        , fromIntegral (alphaReceiptEdgeCells receipt)
-        , fromIntegral (alphaReceiptFaceCells receipt)
-        , fromIntegral (alphaReceiptFinitePairs receipt)
-        , fromIntegral (alphaReceiptEssentialPairs receipt)
-        , alphaReceiptProfileChecksum receipt
-        ]
-    )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Constraint.hs
+++ /dev/null
@@ -1,164 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Constraint recovery and splitting lanes and their gates.
-module Moonlight.Triangulation.Bench.SpadeCompare.Constraint where
-
-import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
-import System.FilePath ((</>))
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Cdt
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
--- | Disjoint split cells, stacked in y: the points, the vertical constraints
--- the build starts from, and the crossing requests. Band @i@ carries a
--- constraint from (16, 4i) to (16, 4i + 2) and a crossing request from
--- (0, 4i + a) to (32, 4i + b), with a and b jittered inside the band, so every
--- request crosses exactly one constraint and no request reaches another band.
---
--- The geometry is chosen so the split point is not merely close on the two
--- sides but identical. spade solves the two line equations by Cramer's rule and
--- this side walks the parametric form; here every coefficient either vanishes
--- or is a power of two, both reduce to one rounding of @u + (v - u) / 2@, and
--- neither can round it differently. A generic crossing would leave the two
--- formulas free to disagree in the last bit and there would be nothing to gate.
-splitBands :: Int -> (V.Vector Point, V.Vector (Int, Int), [(Int, Int)])
-splitBands bandCount =
-  ( V.fromList (concatMap bandPoints (zip [0 ..] jitter))
-  , V.fromList [(4 * index, 4 * index + 1) | index <- [0 .. bandCount - 1]]
-  , [(4 * index + 2, 4 * index + 3) | index <- [0 .. bandCount - 1]]
-  )
- where
-  jitter = randomPoints 0x51ed270b7c1fd1a3 bandCount
-
-  bandPoints (index, Point offsetX offsetY) =
-    let base = 4 * fromIntegral (index :: Int)
-     in [ Point 16 base
-        , Point 16 (base + 2)
-        , Point 0 (base + 1 + 0.5 * offsetX)
-        , Point 32 (base + 1 + 0.5 * offsetY)
-        ]
-
--- | The same constraint program the batch recovery runs, one request at a time
--- through the persistent singleton entry point. A request whose corridor is
--- blocked by an existing constraint is refused and the previous triangulation
--- stands, which is what the strict side's 'can_add_constraint' guard arranges.
-addConstraintsIncrementally
-  :: ConstrainedDelaunayTriangulation Point
-  -> [(Point, Point)]
-  -> (ConstrainedDelaunayTriangulation Point, [Int])
-addConstraintsIncrementally base requests =
-  case foldl' step (base, []) (zip [0 ..] requests) of
-    (triangulation, accepted) -> (triangulation, reverse accepted)
- where
-  step (triangulation, accepted) (index, (from, to)) =
-    case addConstraintEdge triangulation from to of
-      Left (ConstraintIntersection _) -> (triangulation, accepted)
-      Left failure -> error (show failure)
-      Right result -> (constraintRecoveryTriangulation result, index : accepted)
-
--- | Splitting requests, one per band, carried by the batch verb: one
--- transaction for every corridor, published once -- the shape of the strict
--- side's fold over one mutable CDT, which publishes nothing between splits.
--- Every crossed constraint is split at the intersection rather than refused,
--- so every request is accepted. Splitting appends vertices and never removes
--- one, so the handles resolved before the first request stay valid through
--- the last.
-splitConstraints
-  :: ConstrainedDelaunayTriangulation Point
-  -> [(VertexId, VertexId)]
-  -> ConstrainedDelaunayTriangulation Point
-splitConstraints base requests =
-  case addConstraintsAndSplit id base (V.fromList requests) of
-    Left failure -> error (show failure)
-    Right result -> constraintRecoveryTriangulation result
-
--- | The singleton constraint program's accepted request indices and final
--- constrained edge set. A blocked request must be refused at the same index on
--- both sides, or the two are not running the same program.
-writeConstraintIncrementalGate :: FilePath -> Int -> Int -> IO ()
-writeConstraintIncrementalGate directory pointCount constraintCount = do
-  let points = V.fromList (randomPoints 0x94d049bb133111eb pointCount)
-      requests =
-        [ (points V.! fromIndex, points V.! toIndex)
-        | (fromIndex, toIndex) <- constraintPairs pointCount constraintCount
-        ]
-  base <- fromDelaunay . buildTriangulation <$> require (delaunay unitElementDefaults points)
-  let (recovered, accepted) = addConstraintsIncrementally base requests
-      prefix =
-        directory
-          </> ("constraint-incremental-" <> show pointCount <> "-" <> show constraintCount)
-  writeFile (prefix <> "-accepted.txt") (unlines (map show accepted))
-  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges recovered))
-
--- | Both the split vertices and the constrained edges they carve. The edge set
--- names every vertex by coordinate, so a split point that landed one ulp away
--- on one side shows up here rather than hiding behind a matching edge count.
-writeConstraintSplitGate :: FilePath -> Int -> IO ()
-writeConstraintSplitGate directory bandCount = do
-  (base, handles) <- splitBandCdt bandCount
-  let split = splitConstraints base handles
-      prefix = directory </> ("constraint-split-" <> show bandCount)
-  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges split))
-  writeFile (prefix <> "-constraints.txt") (unlines (canonicalConstraintEdges split))
-
--- | The banded CDT and the crossing requests named by handle. Handle resolution
--- goes through the build's own input mapping rather than a locate, which is the
--- constraint gate's convention; the strict side has no such mapping and locates
--- instead. Either way both name the vertex standing at the same coordinate.
-splitBandCdt
-  :: Int
-  -> IO (ConstrainedDelaunayTriangulation Point, [(VertexId, VertexId)])
-splitBandCdt bandCount = do
-  let (points, constraints, crossings) = splitBands bandCount
-  built <- require (constrainedDelaunay unitElementDefaults points constraints)
-  let mapping = buildInputVertices built
-      len = sizeofPrimArray mapping
-      resolve index
-        | index >= 0 && index < len = VertexId (indexPrimArray mapping index)
-        | otherwise = error ("split band endpoint is out of range: " <> show index)
-  pure
-    ( buildTriangulation built
-    , [(resolve fromIndex, resolve toIndex) | (fromIndex, toIndex) <- crossings]
-    )
-
-writeConstraintGate :: FilePath -> IO ()
-writeConstraintGate directory = do
-  let points = V.fromList (randomPoints 0x94d049bb133111eb 8000)
-      requestIndices = V.fromList (constraintPairs 8000 800)
-  built <- require (delaunay unitElementDefaults points)
-  let mapping = buildInputVertices built
-      len = sizeofPrimArray mapping
-  requests <-
-    V.mapM
-      (\(fromIndex, toIndex) ->
-        let mFrom = if fromIndex >= 0 && fromIndex < len then Just (VertexId (indexPrimArray mapping fromIndex)) else Nothing
-            mTo = if toIndex >= 0 && toIndex < len then Just (VertexId (indexPrimArray mapping toIndex)) else Nothing
-        in case (mFrom, mTo) of
-          (Just from, Just to) -> pure (from, to)
-          _ -> fail "constraint gate endpoint is out of range"
-      )
-      requestIndices
-  batch <-
-    require
-      ( recoverConstraints
-          (fromDelaunay (buildTriangulation built))
-          requests
-      )
-  let accepted =
-        V.ifoldr
-          (\index outcome indices ->
-            case outcome of
-              ConstraintAccepted _ _ -> index : indices
-              ConstraintRejected _ -> indices
-          )
-          []
-          (constraintBatchOutcomes batch)
-  writeFile
-    (directory </> "cdt-accepted-8000-800.txt")
-    (unlines (map show accepted))
-  writeFile
-    (directory </> "cdt-constraints-8000-800.txt")
-    (unlines (canonicalConstraintEdges (constraintBatchTriangulation batch)))
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Dcel.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | The primal DCEL traversal gate and the walk lane.
-module Moonlight.Triangulation.Bench.SpadeCompare.Dcel where
-
-import Data.List (sort)
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (allFaces, directedEdges)
-import Moonlight.Triangulation.Handles.Iterators.HullIterator (hullEdges)
-import System.FilePath ((</>))
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-
--- | Every iterator over the mesh that spade also offers. The hull cycle and
--- each vertex's counterclockwise link are cycles with no determined starting
--- point and are rotated; the inner faces are a set and are sorted. The
--- enumerator lengths are stated because they are the only cross-comparable
--- claim the fixed and dynamic iterators make: their elements are handles, and
--- handles do not cross implementations.
-writeDcelWalkGate :: FilePath -> Int -> IO ()
-writeDcelWalkGate directory count = do
-  triangulation <- delaunayOf count
-  writeFile
-    (directory </> ("dcel-hull-" <> show count <> ".txt"))
-    (unlines (hullGateLines triangulation))
-  writeFile
-    (directory </> ("dcel-circular-" <> show count <> ".txt"))
-    (unlines (circularGateLines triangulation))
-  writeFile
-    (directory </> ("dcel-faces-" <> show count <> ".txt"))
-    (unlines (innerFaceGateLines triangulation))
-
-hullGateLines :: DelaunayTriangulation Point -> [String]
-hullGateLines triangulation =
-  ("hull-size " <> show (length ring)) : rotateToSmallest ring
- where
-  ring =
-    [ coordHex (vertexPoint triangulation (origin triangulation edge))
-        <> coordHex (vertexPoint triangulation (destination triangulation edge))
-    | edge <- hullEdges triangulation
-    ]
-
-circularGateLines :: DelaunayTriangulation Point -> [String]
-circularGateLines triangulation =
-  [ "vertices " <> show (length (vertices triangulation))
-  , "directed-edges " <> show (length (directedEdges triangulation))
-  , "undirected-edges " <> show (length (undirectedEdges triangulation))
-  , "all-faces " <> show (length (allFaces triangulation))
-  , "inner-faces " <> show (length (innerFaces triangulation))
-  ]
-    <> sort
-      [ coordHex (vertexPoint triangulation vertex) <> concat (rotateToSmallest ring)
-      | vertex <- vertices triangulation
-      , let ring =
-              [ coordHex (vertexPoint triangulation (destination triangulation edge))
-              | edge <- vertexOutgoingEdges triangulation vertex
-              ]
-      ]
-
-innerFaceGateLines :: DelaunayTriangulation Point -> [String]
-innerFaceGateLines triangulation =
-  sort
-    [ concat (sort (map (coordHex . vertexPoint triangulation) [a, b, c]))
-    | face <- innerFaces triangulation
-    , Just (a, b, c) <- [innerFaceVertices triangulation face]
-    ]
-
--- | Every traversal the DCEL offers, in one pass: the hull cycle, each vertex's
--- counterclockwise link, and each inner face's vertex triple. The coordinates
--- are summed so that no step can be satisfied by a thunk.
-dcelWalk :: DelaunayTriangulation Point -> (Double, Int)
-dcelWalk triangulation = overFaces (overVertices (overHull (0, 0)))
- where
-  overHull accumulator =
-    foldl' (\acc edge -> charge acc (origin triangulation edge)) accumulator (hullEdges triangulation)
-
-  overVertices accumulator = foldl' overLink accumulator (vertices triangulation)
-
-  overLink accumulator vertex =
-    foldl'
-      (\acc edge -> charge acc (destination triangulation edge))
-      accumulator
-      (vertexOutgoingEdges triangulation vertex)
-
-  overFaces accumulator = foldl' overCorners accumulator (innerFaces triangulation)
-
-  overCorners accumulator face =
-    case innerFaceVertices triangulation face of
-      Nothing -> accumulator
-      Just (a, b, c) -> foldl' charge accumulator [a, b, c]
-
-  charge (!total, !count) vertex =
-    let Point x y = vertexPoint triangulation vertex
-     in (total + x + y, count + 1)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Delaunay.hs
+++ /dev/null
@@ -1,261 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Bulk-load, incremental, nearest, and refinement lanes and their gates.
-module Moonlight.Triangulation.Bench.SpadeCompare.Delaunay where
-
-import Control.Exception (evaluate)
-import Data.Foldable (traverse_)
-import System.Exit (exitWith, ExitCode (ExitFailure))
-import System.FilePath ((</>))
-import System.IO (hPutStrLn, stderr)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.HintGenerator
-import Moonlight.Triangulation.Session
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
-writeBulkLoadGate :: FilePath -> Int -> IO ()
-writeBulkLoadGate directory count = do
-  triangulation <-
-    require
-      ( delaunayGeometry
-          (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
-      )
-  writeFile
-    (directory </> ("bulk-load-" <> show count <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-
-writeIncrementalGate :: FilePath -> Int -> IO ()
-writeIncrementalGate directory count = do
-  triangulation <-
-    insertAllIncrementally
-      (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
-  writeFile
-    (directory </> ("incremental-" <> show count <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-
-writeNearestGate :: FilePath -> IO ()
-writeNearestGate directory = do
-  let pointCount = 20000
-      queryCount = 5000
-  triangulation <-
-    buildTriangulation
-      <$> require
-        ( delaunay
-            unitElementDefaults
-            (V.fromList (randomPoints 0x0123456789abcdef pointCount))
-        )
-  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
-  queries <- requireQueryPoints (randomPoints 0x3141592653589793 queryCount)
-  writeFile
-    (directory </> "nearest-20000-5000.txt")
-    (unlines (map (nearestAnswer triangulation hierarchy) queries))
-
-writeRefinementGate :: FilePath -> Int -> IO ()
-writeRefinementGate directory steinerBudget = do
-  let (points, constraints) = refinementInput
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just steinerBudget
-          , refineMaxArea = Just 0.5
-          , refineMaxRadiusEdgeRatio = Just 1.0
-          , refineExcludeOuterFaces = True
-          , refineKeepConstraintEdges = False
-          }
-  cdtBuild <- require (constrainedDelaunay unitElementDefaults points constraints)
-  refined <-
-    require
-      ( refine
-          id
-          parameters
-          (buildTriangulation cdtBuild :: ConstrainedDelaunayTriangulation Point)
-      )
-  let triangulation = refinedTriangulation refined
-      prefix = directory </> ("refine-" <> show steinerBudget)
-      completion = if refinementComplete refined then "1" else "0"
-      summary =
-        [ "added " <> show (refinementAddedVertices refined)
-        , "vertices " <> show (numVertices triangulation)
-        , "edges " <> show (numUndirectedEdges triangulation)
-        , "inner-faces " <> show (numInnerFaces triangulation)
-        , "excluded " <> show (V.length (refinementExcludedFaces refined))
-        , "complete " <> completion
-        ]
-  writeFile (prefix <> "-summary.txt") (unlines summary)
-  writeFile (prefix <> "-edges.txt") (unlines (canonicalEdges triangulation))
-  writeFile
-    (prefix <> "-constraints.txt")
-    (unlines (canonicalConstraintEdges triangulation))
-
--- | Arrival-order insertion through immutable snapshots. The artifact proves
--- final-state agreement with Spade; it is not a cross-language timing row.
-writePersistentGate :: FilePath -> Int -> IO ()
-writePersistentGate directory count = do
-  triangulation <-
-    insertAllPersistently (V.fromList (randomPoints 0x9e3779b97f4a7c15 count))
-  writeFile
-    (directory </> ("persistent-" <> show count <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-
-writeSweepAngleGate :: FilePath -> Int -> IO ()
-writeSweepAngleGate directory count = do
-  triangulation <-
-    buildTriangulation
-      <$> require
-        ( delaunay
-            unitElementDefaults
-            (V.fromList (nearCollinearPoints 0x9e3779b97f4a7c15 count))
-        )
-  writeFile
-    (directory </> ("sweep-angle-collapse-" <> show count <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-
--- | The degenerate chain, pinned by the only thing it has: its edges.
---
--- There are no faces to compare and no circumcentres to argue about, so unlike
--- every other gate here this one has no per-side half and no exclusion. The
--- answer is @n - 1@ consecutive segments and the two sides either both produce
--- it or one of them is wrong.
-writeDegenerateLineGate :: FilePath -> Int -> IO ()
-writeDegenerateLineGate directory count = do
-  triangulation <-
-    buildTriangulation
-      <$> require
-        (delaunay unitElementDefaults (V.fromList (exactlyCollinearPoints count)))
-  writeFile
-    (directory </> ("degenerate-line-" <> show count <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-
--- Arrival-order insertion with one publication: the transactional comparison
--- against Spade's @&mut self@ insertion. The per-operation snapshot fold is
--- measured separately as Moonlight publication stress.
-insertAllIncrementally
-  :: V.Vector Point
-  -> IO (DelaunayTriangulation Point)
-insertAllIncrementally points =
-  evaluate
-    ( case either (error . show) id
-        ( withSession
-            (empty unitElementDefaults)
-            (V.length points)
-            (V.mapM_ insertVertex points)
-        ) of
-        (_, frozen, _) -> frozen
-    )
-
-insertAllPersistently
-  :: V.Vector Point
-  -> IO (DelaunayTriangulation Point)
-insertAllPersistently =
-  V.foldM'
-    (\triangulation point ->
-      insertionTriangulation <$> require (insert triangulation point)
-    )
-    (empty unitElementDefaults)
-
--- | The refinement lane's own counters, so the worklist question is answered by
--- measurement rather than by assertion.
---
--- @faceChecks@ is not a worklist counter: the terminal audit runs the complete
--- face evaluation over every final inner face without popping anything. So the
--- amplification ratio is @pops / steiner@, and @faceChecks - pops@ should equal
--- the final inner-face count less any pop discarded as excluded.
--- | The circle sweep's placement split, on the same generator the bulk lanes
--- use. A point takes the fast path when the hull edge its own angle selected
--- has the point strictly to its left; otherwise it is deferred to a generic
--- insertion after the sweep finishes. spade takes the identical branch on
--- @is_on_right_side_or_on_line@ and its own source calls that fallback "very
--- slow", so a materially non-zero skip count is wasted work in the bulk lane
--- rather than a curiosity. @seed + fast + skipped@ must exhaust the unique
--- points; this reports the identity rather than asserting it silently.
-reportSweepStats :: Int -> IO ()
-reportSweepStats count = do
-  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 count)
-  built <- require (delaunay unitElementDefaults points)
-  let stats = buildStats (built :: BuildResult 'Unconstrained Point () () ())
-      seeded = statSpatialSeedPoints stats
-      fast = statSweepFastPoints stats
-      skippedPoints = statSweepSkippedPoints stats
-      unique = statUniquePoints stats
-      accounted = seeded + fast + skippedPoints
-      share :: Int -> String
-      share value
-        | unique == 0 = "n/a"
-        | otherwise = show (100 * fromIntegral value / fromIntegral unique :: Double) <> "%"
-  traverse_
-    putStrLn
-    [ "input-points " <> show (statInputPoints stats)
-    , "unique-points " <> show unique
-    , "duplicate-points " <> show (statDuplicatePoints stats)
-    , "seed-points " <> show seeded
-    , "sweep-fast-points " <> show fast
-    , "sweep-skipped-points " <> show skippedPoints
-    , "skipped-share " <> share skippedPoints
-    , "accounted " <> show accounted
-    , "identity-holds " <> show (accounted == unique)
-    , "edge-flips " <> show (statEdgeFlips stats)
-    , "hull-insertions " <> show (statHullInsertions stats)
-    , "legalization-max-stack " <> show (statLegalizationMaxStack stats)
-    ]
-  if accounted == unique
-    then pure ()
-    else do
-      hPutStrLn
-        stderr
-        ( "sweep-stats: seed + fast + skipped = "
-            <> show accounted
-            <> " but unique = "
-            <> show unique
-        )
-      exitWith (ExitFailure 1)
-
-reportRefinementCounters :: Int -> IO ()
-reportRefinementCounters budget = do
-  let (points, constraints) = refinementInput
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just budget
-          , refineMaxArea = Just 0.5
-          , refineMaxRadiusEdgeRatio = Just 1.0
-          , refineExcludeOuterFaces = True
-          , refineKeepConstraintEdges = False
-          }
-  cdt <- buildTriangulation <$> require (constrainedDelaunay unitElementDefaults points constraints)
-  result <-
-    require (refine id parameters (cdt :: ConstrainedDelaunayTriangulation Point))
-  let stats = refinementStats result
-      steiner = statSteinerPoints stats
-      pops = statRefinementQueuePops stats
-      checks = statRefinementFaceChecks stats
-      inner = numInnerFaces (refinedTriangulation result)
-      excluded = V.length (refinementExcludedFaces result)
-      ratio :: Int -> Int -> String
-      ratio numerator denominator
-        | denominator == 0 = "n/a"
-        | otherwise =
-            show (fromIntegral numerator / fromIntegral denominator :: Double)
-  traverse_
-    putStrLn
-    [ "budget " <> show budget
-    , "steiner " <> show steiner
-    , "queue-pops " <> show pops
-    , "face-checks " <> show checks
-    , "inner-faces " <> show inner
-    , "excluded " <> show excluded
-    , "edge-flips " <> show (statEdgeFlips stats)
-    , "location-walk-steps " <> show (statLocationWalkSteps stats)
-    , "complete " <> show (refinementComplete result)
-    , "pops/steiner " <> ratio pops steiner
-    , "checks/steiner " <> ratio checks steiner
-    , "flips/steiner " <> ratio (statEdgeFlips stats) steiner
-    , "walk-steps/steiner " <> ratio (statLocationWalkSteps stats) steiner
-    , "checks-minus-pops " <> show (checks - pops)
-    -- Every face check is now a queue pop. The terminal audit used to make the
-    -- difference equal the final inner-face count; if this ever reads False
-    -- again, a second whole-mesh quality pass has come back.
-    , "no-second-pass " <> show (checks == pops)
-    ]
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Gates.hs
+++ /dev/null
@@ -1,275 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | The artifact rosters: every gate the binary writes, and the divergence set.
-module Moonlight.Triangulation.Bench.SpadeCompare.Gates where
-
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import System.Directory (createDirectoryIfMissing)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Removal
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Bench.SpadeCompare.Constraint
-import Moonlight.Triangulation.Bench.SpadeCompare.Dcel
-import Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
-import Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
-import Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
-import Moonlight.Triangulation.Bench.SpadeCompare.Intersection
-import Moonlight.Triangulation.Bench.SpadeCompare.Removal
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-import Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
-
--- | The exact observable mesh egress retained by the persistent witnesses.
---
--- These artifacts validate immutable predecessor and final-state agreement;
--- they are not Spade timing ratios. Snapshot publication is measured only on
--- the Haskell side against its session reference.
---
--- The cross-language gate already compares the canonical-edge projection.
--- This witness additionally retains the geometry-labelled circular DCEL
--- section, so an isolated or face-less predecessor cannot change unseen.  It
--- deliberately observes geometric topology rather than internal storage, and
--- remains a gate-only sink outside every timed lane.
-data PersistentEndpoint = PersistentEndpoint
-  { persistentEndpointVertices :: !Int
-  , persistentEndpointUndirectedEdges :: !Int
-  , persistentEndpointInnerFaces :: !Int
-  , persistentEndpointCanonicalEdges :: ![String]
-  , persistentEndpointCircularDcel :: ![String]
-  }
-  deriving (Eq, Show)
-
--- | Every retained section is named by its operation and completed edit count,
--- never by a formatting-only string.
-data PersistentWitnessLabel
-  = PersistentInsertionWitness !Int
-  | PersistentRemovalWitness !Int
-  deriving (Eq, Show)
-
--- | A preserved immutable section together with its authoritative egress at
--- capture time.  Insert and removal each supply one concrete use, so this is
--- the shared gate algebra rather than a second triangulation representation.
-data PersistentSnapshotWitness = PersistentSnapshotWitness
-  { persistentSnapshotLabel :: !PersistentWitnessLabel
-  , persistentSnapshotEndpoint :: !PersistentEndpoint
-  , persistentSnapshotTriangulation :: !(DelaunayTriangulation Point)
-  }
-
--- | Complete typed refusal surface for the persistence gate.  The public
--- operations already return 'BuildError'; the witness transports it instead
--- of manufacturing a stringly failure.  A changed snapshot carries its exact
--- observed endpoint so the gate failure is inspectable without a unilateral
--- artifact.
-data PersistentSnapshotObstruction
-  = PersistentInitialBuildRefused !BuildError
-  | PersistentInsertionRefused !Int !BuildError
-  | PersistentRemovalRefused !Int !BuildError
-  | PersistentRemovalTargetMissing !Int
-  | PersistentSnapshotChanged
-      !PersistentWitnessLabel
-      !PersistentEndpoint
-      !PersistentEndpoint
-  deriving Show
-
-persistentEndpoint
-  :: DelaunayTriangulation Point
-  -> IO PersistentEndpoint
-persistentEndpoint triangulation = do
-  edges <- evaluate (force (canonicalEdges triangulation))
-  circular <- evaluate (force (circularGateLines triangulation))
-  pure $!
-    PersistentEndpoint
-      { persistentEndpointVertices = numVertices triangulation
-      , persistentEndpointUndirectedEdges = numUndirectedEdges triangulation
-      , persistentEndpointInnerFaces = numInnerFaces triangulation
-      , persistentEndpointCanonicalEdges = edges
-      , persistentEndpointCircularDcel = circular
-      }
-
-capturePersistentSnapshot
-  :: PersistentWitnessLabel
-  -> DelaunayTriangulation Point
-  -> IO PersistentSnapshotWitness
-capturePersistentSnapshot label triangulation = do
-  endpoint <- persistentEndpoint triangulation
-  pure
-    PersistentSnapshotWitness
-      { persistentSnapshotLabel = label
-      , persistentSnapshotEndpoint = endpoint
-      , persistentSnapshotTriangulation = triangulation
-      }
-
-verifyPersistentSnapshot
-  :: PersistentSnapshotWitness
-  -> IO (Either PersistentSnapshotObstruction ())
-verifyPersistentSnapshot witness = do
-  observed <- persistentEndpoint (persistentSnapshotTriangulation witness)
-  pure
-    ( if observed == persistentSnapshotEndpoint witness
-        then Right ()
-        else
-          Left
-            ( PersistentSnapshotChanged
-                (persistentSnapshotLabel witness)
-                (persistentSnapshotEndpoint witness)
-                observed
-            )
-    )
-
-verifyPersistentSnapshots :: [PersistentSnapshotWitness] -> IO (Either PersistentSnapshotObstruction ())
-verifyPersistentSnapshots = fmap sequence_ . traverse verifyPersistentSnapshot
-
-assertPersistentSnapshot
-  :: Either PersistentSnapshotObstruction value
-  -> IO value
-assertPersistentSnapshot = require
-
--- | Fixed sparse checkpoints of a longer immutable chain.  Each one has later
--- edits behind it, so the test distinguishes persistence from merely producing
--- the correct final mesh.
-persistentCheckpoint :: Int -> Bool
-persistentCheckpoint index = index `elem` [0, 1, 2, 7, 31, 63]
-
-verifyPersistentInsertionSnapshots :: IO ()
-verifyPersistentInsertionSnapshots = do
-  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 96)
-      initial = empty unitElementDefaults
-  initialWitness <- capturePersistentSnapshot (PersistentInsertionWitness 0) initial
-  (final, witnesses) <-
-    V.ifoldM'
-      (\(current, preserved) index point -> do
-        nextTriangulation <-
-          assertPersistentSnapshot
-            ( insertionTriangulation
-                <$> first (PersistentInsertionRefused index) (insert current point)
-            )
-        if persistentCheckpoint index
-          then do
-            witness <- capturePersistentSnapshot (PersistentInsertionWitness (index + 1)) nextTriangulation
-            pure (nextTriangulation, preserved <> [witness])
-          else pure (nextTriangulation, preserved)
-      )
-      (initial, [initialWitness])
-      points
-  -- Force the terminal endpoint as an out-of-clock comparable sink before
-  -- checking predecessors; its edge projection is the same projection the
-  -- Rust gate emits for the timed lanes.
-  _ <- persistentEndpoint final
-  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
-
-verifyPersistentRemovalSnapshots :: IO ()
-verifyPersistentRemovalSnapshots = do
-  let points = V.fromList (randomPoints 0x9e3779b97f4a7c15 128)
-      removals = V.take 96 points
-  initial <-
-    buildTriangulation
-      <$> assertPersistentSnapshot
-        (first PersistentInitialBuildRefused (delaunay unitElementDefaults points))
-  initialWitness <- capturePersistentSnapshot (PersistentRemovalWitness 0) initial
-  (final, witnesses) <-
-    V.ifoldM'
-      (\(current, preserved) index point -> do
-        outcome <-
-          assertPersistentSnapshot
-            (first (PersistentRemovalRefused index) (locateAndRemove current point))
-        nextTriangulation <-
-          case outcome of
-            Nothing -> assertPersistentSnapshot (Left (PersistentRemovalTargetMissing index))
-            Just removal -> pure (removalTriangulation removal)
-        if persistentCheckpoint index
-          then do
-            witness <- capturePersistentSnapshot (PersistentRemovalWitness (index + 1)) nextTriangulation
-            pure (nextTriangulation, preserved <> [witness])
-          else pure (nextTriangulation, preserved)
-      )
-      (initial, [initialWitness])
-      removals
-  _ <- persistentEndpoint final
-  assertPersistentSnapshot =<< verifyPersistentSnapshots witnesses
-
--- | The Haskell side's immutable-contract gate.  It emits no extra artifact:
--- adding a unilateral file would make the cross-language artifact diff fail.
--- Its selected predecessor sections are instead verified before the existing
--- shared endpoint artifacts are written.
-verifyPersistentSnapshotWitnesses :: IO ()
-verifyPersistentSnapshotWitnesses = do
-  verifyPersistentInsertionSnapshots
-  verifyPersistentRemovalSnapshots
-
--- | Pin the exact output of every timed workload before comparing its cost.
--- Persistent witness artifacts above are final-state agreement gates, not
--- cross-language timing rows.
-writeGate :: FilePath -> IO ()
-writeGate directory = do
-  createDirectoryIfMissing True directory
-  verifyPersistentSnapshotWitnesses
-  traverse_ (writeBulkLoadGate directory) [1000, 10000]
-  traverse_ (writeIncrementalGate directory) [1000, 10000]
-  writeNearestGate directory
-  writeConstraintGate directory
-  traverse_ (writeRefinementGate directory) [625, 2500]
-  traverse_ (uncurry (writeRemovalGate directory)) [(1000, 250), (10000, 2500)]
-  traverse_ (uncurry (writeInterpolationGate directory)) [(10000, 1000), (100000, 2000)]
-  writeVoronoiGate directory 1000
-  writeDcelWalkGate directory 2000
-  writeIntersectionGate directory 10000 500
-  writeIntersectionVertexGate directory 2000
-  -- Persistent artifacts validate final-state agreement only; they are not
-  -- Spade timing rows. Their canonical egress is retained for the hard gate.
-  writePersistentGate directory 1000
-  writePersistentRemovalGate directory 10000 2500
-  -- The remaining diagnostic gates. Timing is meaningful only after both
-  -- sides have been shown to compute the same canonical result.
-  writeConstraintIncrementalGate directory 8000 800
-  writeConstraintSplitGate directory 1000
-  writeSweepAngleGate directory 2000
-  writeDegenerateLineGate directory 2000
-  writeHierarchyIncrementalGate directory 1000
-  writeHierarchyDuplicateGate directory 10000 500
-  writeHierarchyRemovalGate directory 10000 250
-  writeIntersectionOutsideGate directory 2000 100
-
--- | The shapes on which the two implementations classify the constrained
--- domain. They do not agree, so this is a characterization rather than a gate:
--- the committed baselines lock in the known divergence and fail if either side
--- moves. See @README.md@ for the compatibility rule.
-divergenceShapes :: [(String, [Point], [(Int, Int)])]
-divergenceShapes =
-  [ ("flush", square 8 <> [Point 4 4], loop4)
-  , ("notched", square 8 <> [Point 13 4, Point 4 4], loop4)
-  , ("flush-plus-dangling-segment", square 8 <> [Point 2 2, Point 6 6], loop4 <> [(4, 5)])
-  , ("notched-plus-dangling-segment", square 8 <> [Point 13 4, Point 2 2, Point 6 6], loop4 <> [(5, 6)])
-  , ( "annulus"
-    , square 12 <> [Point 4 4, Point 8 4, Point 8 8, Point 4 8]
-    , loop4 <> [(4, 5), (5, 6), (6, 7), (7, 4)]
-    )
-  ]
- where
-  square scale = [Point 0 0, Point scale 0, Point scale scale, Point 0 scale]
-  loop4 = [(0, 1), (1, 2), (2, 3), (3, 0)]
-
-writeDivergence :: FilePath -> IO ()
-writeDivergence path = do
-  rows <- mapM row divergenceShapes
-  writeFile path (unlines rows)
- where
-  row (label, points, constraints) = do
-    built <- require (constrainedDelaunay unitElementDefaults (V.fromList points) (V.fromList constraints))
-    let cdt = buildTriangulation built
-        parameters =
-          defaultRefinementParameters
-            { refineMaxAdditionalVertices = Just 0
-            , refineExcludeOuterFaces = True
-            }
-    classified <- require (refine id parameters cdt)
-    pure
-      ( label
-          <> ": inner " <> show (numInnerFaces cdt)
-          <> ", excluded " <> show (V.length (refinementExcludedFaces classified))
-      )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Hierarchy.hs
+++ /dev/null
@@ -1,166 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Hierarchy-hint lanes, their gates, and the maintained-hint step.
-module Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy where
-
-import System.FilePath ((</>))
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.BulkLoad
-import Moonlight.Triangulation.HintGenerator
-import Moonlight.Triangulation.Math (validatePoint)
-import Moonlight.Triangulation.Session
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
--- | The step both insertion lanes run: one insert, one hierarchy update, and
--- nothing between them. It is what spade's @insert@ does internally through
--- @notify_vertex_inserted@, and both sides carry branch factor 16.
---
--- The point handed to 'updateHierarchyAfterInsertion' is the same binding
--- handed to 'insertVertexAt', never one read back out of the harness's input
--- vector. The mesh stores 'canonicalPoint' of what it was given, so a
--- harness-side copy would part company from it on a signed zero, seed a
--- different nearest neighbour, and move the gate against spade. Same function,
--- same argument, same bits.
-hierarchyStep
-  :: HierarchyHint
-  -> Point
-  -> Session s Point () () () HierarchyHint
-hierarchyStep hierarchy point = do
-  query <- either refuse pure (validatePoint Nothing point)
-  (vertex, disposition) <-
-    case hierarchyHint hierarchy query of
-      Just (VertexHint seed) -> insertVertexAtNearVertex seed point point
-      _ -> insertVertexAt point point
-  either refuse pure (updateHierarchyAfterInsertion hierarchy point vertex disposition)
-
--- | Arrival-order insertion with the hierarchy maintained alongside it.
---
--- One session spans the whole run. The hierarchy no longer asks for a
--- triangulation, so the per-point publication that existed only to hand it one
--- is gone: the mesh is thawed once and published once for the program, where
--- before it was once per point. The measured operation is unchanged — still an
--- insert per point with the hint maintained against it, which is spade's loop.
-insertAllWithHierarchy
-  :: V.Vector Point
-  -> IO (DelaunayTriangulation Point, HierarchyHint)
-insertAllWithHierarchy points = do
-  let base = empty unitElementDefaults
-  initial <- require (buildHierarchyHint defaultHierarchyBranchFactor base)
-  (hierarchy, mesh, _) <-
-    require (withSession base (V.length points) (V.foldM' hierarchyStep initial points))
-  pure (mesh, hierarchy)
-
--- | The same step over a mesh that already holds every point, which is the
--- duplicate lane. Each insert locates a site that is already there and answers
--- 'AlreadyPresent', so the hierarchy comes back unexamined and the level walk
--- is never entered. What remains on the clock is the locate and the
--- disposition test, with no rebuild standing behind either.
-reinsertAllWithHierarchy
-  :: (DelaunayTriangulation Point, HierarchyHint)
-  -> V.Vector Point
-  -> IO (DelaunayTriangulation Point, HierarchyHint)
-reinsertAllWithHierarchy (triangulation, hierarchy) points =
-  case withSession triangulation (V.length points) (V.foldM' hierarchyStep hierarchy points) of
-    Left refusal -> fail (show refusal)
-    Right (updated, mesh, _) -> pure (mesh, updated)
-
--- | Remove a program of coordinates through the hierarchy: every locate
--- starts at the hierarchy's nearest sample, one base session publishes once,
--- and the hierarchy of the resulting value is published once.
---
--- No query observes an intermediate hierarchy in this lane. A triangulation
--- and its hierarchy denote their current values rather than the edit history,
--- so the lawful physical schedule is one base session followed by one
--- hierarchy publication — the shape of the strict side's
--- @HierarchyTriangulation@, whose removals also locate through the hierarchy.
-removeAllWithHierarchy
-  :: (DelaunayTriangulation Point, HierarchyHint)
-  -> V.Vector Point
-  -> IO (DelaunayTriangulation Point, HierarchyHint)
-removeAllWithHierarchy (triangulation, hierarchy) points =
-  case removeManyWithHierarchy hierarchy triangulation points of
-    Left refusal -> fail (show refusal)
-    Right (outcomes, surviving, repaired) -> do
-      V.imapM_ checkRemoved outcomes
-      pure (surviving, repaired)
- where
-  checkRemoved index outcome = case outcome of
-    Nothing ->
-      error
-        ( "removal target is not a vertex: index="
-            <> show index
-            <> " point="
-            <> ( case points V.! index of
-                  Point x y -> hex64 x <> "," <> hex64 y
-               )
-        )
-    Just _ -> pure ()
-
--- | The hierarchy lanes' gate, in two parts. The base mesh alone would not see
--- a hierarchy whose nesting law has drifted, because a search started from a
--- bad hint still walks to the right answer; a hint naming a vertex that does
--- not exist, or one the search cannot walk out of, moves the answers. Both are
--- pinned.
-hierarchyGateQueries :: Int
-hierarchyGateQueries = 1000
-
-writeHierarchyGate
-  :: FilePath
-  -> String
-  -> (DelaunayTriangulation Point, HierarchyHint)
-  -> IO ()
-writeHierarchyGate directory label (triangulation, hierarchy) = do
-  queries <- requireQueryPoints (randomPoints 0x3141592653589793 hierarchyGateQueries)
-  writeFile
-    (directory </> (label <> "-edges.txt"))
-    (unlines (canonicalEdges triangulation))
-  writeFile
-    (directory </> (label <> "-nearest.txt"))
-    ( unlines
-        ( map
-            (nearestAnswer triangulation hierarchy)
-            queries
-        )
-    )
-
-writeHierarchyIncrementalGate :: FilePath -> Int -> IO ()
-writeHierarchyIncrementalGate directory pointCount = do
-  built <- insertAllWithHierarchy (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount))
-  writeHierarchyGate directory ("hierarchy-incremental-" <> show pointCount) built
-
-writeHierarchyDuplicateGate :: FilePath -> Int -> Int -> IO ()
-writeHierarchyDuplicateGate directory pointCount duplicateCount = do
-  base <- hierarchyOf pointCount
-  built <-
-    reinsertAllWithHierarchy
-      base
-      (V.fromList (randomPoints 0x9e3779b97f4a7c15 duplicateCount))
-  writeHierarchyGate
-    directory
-    ("hierarchy-duplicate-" <> show pointCount <> "-" <> show duplicateCount)
-    built
-
-writeHierarchyRemovalGate :: FilePath -> Int -> Int -> IO ()
-writeHierarchyRemovalGate directory pointCount removalCount = do
-  base <- hierarchyOf pointCount
-  built <-
-    removeAllWithHierarchy
-      base
-      (V.fromList (take removalCount (randomPoints 0x9e3779b97f4a7c15 pointCount)))
-  writeHierarchyGate
-    directory
-    ("hierarchy-removal-" <> show pointCount <> "-" <> show removalCount)
-    built
-
--- | A bulk-loaded mesh and the hierarchy over it, which is where the duplicate
--- and removal lanes start. The strict side reaches the same pair through
--- @HierarchyTriangulation::bulk_load@.
-hierarchyOf
-  :: Int
-  -> IO (DelaunayTriangulation Point, HierarchyHint)
-hierarchyOf count = do
-  triangulation <- delaunayOf count
-  hierarchy <- require (buildHierarchyHint defaultHierarchyBranchFactor triangulation)
-  pure (triangulation, hierarchy)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Input.hs
+++ /dev/null
@@ -1,20 +0,0 @@
--- | Cross-language benchmark input programs. Both Haskell interpreters import
--- this owner directly; neither keeps a private copy of the LCG stream.
-module Moonlight.Triangulation.Bench.SpadeCompare.Input
-  ( randomPoints
-  ) where
-
-import Data.Word (Word64)
-import Moonlight.Triangulation.Types (Point (..))
-
--- | The Rust generator, bit for bit.
-randomPoints :: Word64 -> Int -> [Point]
-randomPoints seed count = take count (go seed)
- where
-  go :: Word64 -> [Point]
-  go state =
-    let state1 = state * 6364136223846793005 + 1442695040888963407
-        state2 = state1 * 6364136223846793005 + 1442695040888963407
-        unit :: Word64 -> Double
-        unit value = fromIntegral (value `div` 2048) / 9007199254740992
-     in Point (2 * unit state1 - 1) (2 * unit state2 - 1) : go state2
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Interpolation.hs
+++ /dev/null
@@ -1,419 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Sibson interpolation lanes, gates, and diagnostics.
-module Moonlight.Triangulation.Bench.SpadeCompare.Interpolation where
-
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad.ST (stToIO)
-import GHC.Exts (RealWorld)
-import Data.Foldable (traverse_)
-import Data.List (sort, sortOn)
-import System.Exit (exitWith, ExitCode (ExitFailure))
-import System.FilePath ((</>))
-import System.IO (hPutStrLn, stderr)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Interpolation
-import Moonlight.Triangulation.Internal.Predicates qualified as Predicates
-import Moonlight.Triangulation.Math (isFinite)
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
--- | The Sibson query reports its own cavity size, so the distribution of
--- (cavity faces, neighbours returned) says which step degenerates: a cavity of
--- one face means 'discoverCavity' never expanded, while a larger cavity paired
--- with three neighbours means the cell or the normalization was rejected.
-reportInterpolationAudit :: Int -> Int -> IO ()
-reportInterpolationAudit pointCount queryCount = do
-  triangulation <-
-    buildTriangulation
-      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
-  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
-  queries <- requireQueryPoints (randomPoints 0x2718281828459045 queryCount)
-  pairs <-
-    traverse
-      ( \query -> do
-          (count, _, stats) <-
-            stToIO (foldNaturalNeighborWeights (\total _ _ -> total + (1 :: Int)) 0 workspace Nothing query)
-          pure ((interpolationCavityFaces stats, count), interpolationUsedFallback stats)
-      )
-      queries
-  let tally =
-        [ (representative, 1 + length rest)
-        | representative : rest <- groupSorted (sort (map fst pairs))
-        ]
-      fallbacks = length (filter snd pairs)
-  putStrLn ("queries " <> show (length pairs))
-  putStrLn ("fallbacks " <> show fallbacks)
-  traverse_
-    (\((cavity, neighbors), n) -> putStrLn ("cavityFaces=" <> show cavity <> " neighbors=" <> show neighbors <> " count=" <> show n))
-    tally
- where
-  groupSorted [] = []
-  groupSorted (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupSorted rest
-
--- | Isolates the cavity flood of the Sibson pipeline: per query, the BFS
--- over 'inCircle' and nothing else. The margin tally counts tests whose
--- determinant is inside the filtered predicate's error bound — the exact
--- dyadic path, which allocates Integer arithmetic per call.
-reportInterpolationCavityAudit :: Int -> Int -> IO ()
-reportInterpolationCavityAudit pointCount queryCount = do
-  triangulation <-
-    buildTriangulation
-      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
-  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
-  (faces, tests, margins) <-
-    V.foldM'
-      ( \(faceAcc, testAcc, marginAcc) query ->
-          case locatePoint triangulation query of
-            InFace start -> do
-              let (!cavitySize, !testCount, !marginCount) = flood triangulation (queryPointValue query) start
-              pure (faceAcc + cavitySize, testAcc + testCount, marginAcc + marginCount)
-            _ -> pure (faceAcc, testAcc, marginAcc)
-      )
-      (0 :: Int, 0 :: Int, 0 :: Int)
-      queries
-  putStrLn ("queries " <> show queryCount)
-  putStrLn ("cavity-faces " <> show faces)
-  putStrLn ("incircle-tests " <> show tests)
-  putStrLn ("exact-path-margins " <> show margins)
- where
-  flood triangulation query start = go [start] [] 0 0
-   where
-    go [] seen tests margins = (length seen, tests, margins)
-    go (face : rest) seen tests margins
-      | face `elem` seen = go rest seen tests margins
-      | otherwise =
-          case innerFaceVertices triangulation face of
-            Nothing -> go rest seen tests margins
-            Just (a, b, c) ->
-              let !pa = vertexPoint triangulation a
-                  !pb = vertexPoint triangulation b
-                  !pc = vertexPoint triangulation c
-                  !verdict = Predicates.inCircle pa pb pc query
-                  !margins' = if insideMargin pa pb pc query then margins + 1 else margins
-               in if verdict == GT
-                    then go (adjacent triangulation face ++ rest) (face : seen) (tests + 1) margins'
-                    else go rest seen (tests + 1) margins'
-
-  adjacent triangulation face =
-    case innerFaceDirectedEdges triangulation face of
-      Nothing -> []
-      Just (e0, e1, e2) ->
-        [ opposite
-        | edge <- [e0, e1, e2]
-        , let opposite = incidentFace triangulation (reverseEdge edge)
-        , opposite /= outerFace
-        ]
-
-  -- The branch condition of the filtered Double inCircle, recomputed over
-  -- public coordinates: inside the error bound means the exact dyadic path.
-  insideMargin (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
-    abs determinant <= 1.1102230246251577e-15 * permanent
-   where
-    !adx = ax - dx
-    !ady = ay - dy
-    !bdx = bx - dx
-    !bdy = by - dy
-    !cdx = cx - dx
-    !cdy = cy - dy
-    !abdet = adx * bdy - bdx * ady
-    !bcdet = bdx * cdy - cdx * bdy
-    !cadet = cdx * ady - adx * cdy
-    !alift = adx * adx + ady * ady
-    !blift = bdx * bdx + bdy * bdy
-    !clift = cdx * cdx + cdy * cdy
-    !determinant = alift * bcdet + blift * cadet + clift * abdet
-    !permanent =
-      (abs (bdx * cdy) + abs (cdx * bdy)) * alift
-        + (abs (cdx * ady) + abs (adx * cdy)) * blift
-        + (abs (adx * bdy) + abs (bdx * ady)) * clift
-
--- | Isolates the locate half of the interpolation lane: same triangulation,
--- same queries, but only 'locatePointWithHint' runs — no cavity, no Sibson.
--- With the allocation of this in hand against the full lane's, the owner of
--- the per-query cost is named by subtraction rather than by guesswork.
-reportInterpolationLocateAudit :: Int -> Int -> IO ()
-reportInterpolationLocateAudit pointCount queryCount = do
-  triangulation <-
-    buildTriangulation
-      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
-  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
-  (steps, fallbacks) <-
-    V.foldM'
-      ( \(stepAcc, fallbackAcc) query -> do
-          let (!location, !stats) = locatePointWithHint triangulation Nothing query
-          _ <- evaluate (force (show location))
-          pure (stepAcc + locationWalkSteps stats, fallbackAcc + (if locationUsedFallback stats then 1 else 0))
-      )
-      (0 :: Int, 0 :: Int)
-      queries
-  putStrLn ("queries " <> show queryCount)
-  putStrLn ("walk-steps " <> show steps)
-  putStrLn ("fallbacks " <> show fallbacks)
-
--- | The Sibson pipeline re-implemented over public API only, stage by stage,
--- mirroring the library's 'sibsonQuery': cavity flood, boundary collection,
--- loop ordering, insertion cell, stolen-area walks, normalization. The stage
--- sets are order-independent, so where the replica succeeds while the library
--- falls back the workspace layer is convicted, and where it fails the stage's
--- own algorithm is. With no index it tallies the first 1000 gate queries by
--- failing stage; with an index it dumps one query's full geometry.
-reportInterpolationContext :: Int -> Maybe Int -> IO ()
-reportInterpolationContext pointCount maybeIndex = do
-  triangulation <-
-    buildTriangulation
-      <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
-  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 1000))
-  case maybeIndex of
-    Just index -> case queries V.!? index of
-      Nothing -> do
-        hPutStrLn stderr "interp-context: query index out of range"
-        exitWith (ExitFailure 2)
-      Just query -> do
-        let (_, detail) = replicateQuery triangulation query
-        putStrLn ("query " <> pointHex (queryPointValue query))
-        traverse_ putStrLn detail
-    Nothing -> do
-      let reports = map (fst . replicateQuery triangulation) (V.toList queries)
-          tally =
-            [ (representative, 1 + length rest)
-            | representative : rest <- groupByHead (sort reports)
-            ]
-      putStrLn ("queries " <> show (length reports))
-      traverse_ (\(stage, count) -> putStrLn (stage <> " count=" <> show count)) tally
- where
-  groupByHead [] = []
-  groupByHead (x : xs) = let (same, rest) = span (== x) xs in (x : same) : groupByHead rest
-
-  replicateQuery triangulation queryPoint =
-    let query = queryPointValue queryPoint
-     in case locatePoint triangulation queryPoint of
-      OnVertex _ -> ("special-on-vertex", ["on-vertex"])
-      OnEdge edge
-        | isBoundaryEdge triangulation (asUndirected edge) -> ("special-boundary-edge", ["boundary-edge"])
-        | otherwise ->
-            let !left = incidentFace triangulation edge
-                !right = incidentFace triangulation (reverseEdge edge)
-                !start = if left /= outerFace then left else right
-             in sibson triangulation query start
-      InFace face -> sibson triangulation query face
-      OutsideConvexHull _ -> ("special-outside-hull", ["outside-hull"])
-      EmptyTriangulation -> ("special-empty", ["empty"])
-
-  sibson triangulation query startFace =
-    let cavity = flood [startFace] []
-        boundary =
-          [ edge
-          | face <- cavity
-          , Just (e0, e1, e2) <- [innerFaceDirectedEdges triangulation face]
-          , edge <- [e0, e1, e2]
-          , let opposite = incidentFace triangulation (reverseEdge edge)
-          , opposite == outerFace || not (opposite `elem` cavity)
-          ]
-        header =
-          [ "cavity " <> show (length cavity)
-          , "boundary " <> show (length boundary)
-          ]
-     in case orderLoop boundary of
-          Left reason -> ("fail-order", header ++ ["order-failure " <> reason])
-          Right ordered
-            | length ordered < 3 -> ("fail-order-small", header ++ ["ordered " <> show (length ordered)])
-            | otherwise ->
-                case insertionCell ordered of
-                  Left reason -> ("fail-cell", header ++ ["ordered " <> show (length ordered), "cell-failure " <> reason])
-                  Right cell ->
-                    let walks = stolenAreas ordered cell
-                     in case sequence walks of
-                          Left reason -> ("fail-stolen", header ++ ["ordered " <> show (length ordered), "stolen-failure " <> reason])
-                          Right areas ->
-                            let !total = sum areas
-                                !minimumWeight = minimum areas
-                                !tolerance = max 1.0e-12 (128 * 2.220446049250313e-16)
-                                !clampedTotal = sum (map (max 0) areas)
-                                verdict
-                                  | any (not . isFinite) areas || total == 0 || minimumWeight < negate tolerance =
-                                      Left ("normalize total=" <> show total <> " min=" <> show minimumWeight)
-                                  | clampedTotal <= 0 || not (isFinite clampedTotal) =
-                                      Left ("clamped-total " <> show clampedTotal)
-                                  | otherwise = Right ()
-                             in case verdict of
-                                  Left reason ->
-                                    ( "fail-normalize"
-                                    , header
-                                        ++ ["ordered " <> show (length ordered)]
-                                        ++ ["area " <> show area | area <- areas]
-                                        ++ ["normalize-failure " <> reason]
-                                    )
-                                  Right () ->
-                                    ( "sibson-success"
-                                    , header
-                                        ++ ["ordered " <> show (length ordered)]
-                                        ++ zipWith areaLine ordered areas
-                                        ++ ["total " <> show total <> " min " <> show minimumWeight]
-                                    )
-   where
-    areaLine edge area =
-      "  vertex "
-        <> pointHex (vertexPoint triangulation (origin triangulation edge))
-        <> " area "
-        <> show area
-
-    contains face =
-      case innerFaceVertices triangulation face of
-        Nothing -> False
-        Just (a, b, c) ->
-          Predicates.inCircle
-            (vertexPoint triangulation a)
-            (vertexPoint triangulation b)
-            (vertexPoint triangulation c)
-            query
-            /= LT
-
-    adjacent face =
-      case innerFaceDirectedEdges triangulation face of
-        Nothing -> []
-        Just (e0, e1, e2) ->
-          [ opposite
-          | edge <- [e0, e1, e2]
-          , let opposite = incidentFace triangulation (reverseEdge edge)
-          , opposite /= outerFace
-          ]
-
-    flood [] seen = seen
-    flood (face : rest) seen
-      | face `elem` seen = flood rest seen
-      | not (contains face) = flood rest seen
-      | otherwise = flood (adjacent face ++ rest) (face : seen)
-
-    orderLoop [] = Left "empty-boundary"
-    orderLoop boundary@(first : _) = chain first first [] 0
-     where
-      !count = length boundary
-      origins = [(origin triangulation edge, edge) | edge <- boundary]
-      chain start current acc index
-        | index >= count =
-            if current == start then Right (reverse acc) else Left "no-closure"
-        | otherwise =
-            case lookup (destination triangulation current) origins of
-              Nothing ->
-                Left
-                  ( "broken-chain "
-                      <> pointHex (vertexPoint triangulation (destination triangulation current))
-                  )
-              Just nextEdge
-                | nextEdge == start && index + 1 == count -> Right (reverse (current : acc))
-                | nextEdge == start -> Left "premature-closure"
-                | otherwise -> chain start nextEdge (current : acc) (index + 1)
-
-    relative point = case point of Point x y -> Point (x - qx) (y - qy)
-    (qx, qy) = case query of Point x y -> (x, y)
-
-    insertionCell ordered = traverse cellCenter ordered
-     where
-      cellCenter edge =
-        let !from = vertexPoint triangulation (origin triangulation edge)
-            !to = vertexPoint triangulation (destination triangulation edge)
-         in case circumcenter (relative to) (relative from) (Point 0 0) of
-              Nothing ->
-                Left
-                  ( "edge "
-                      <> pointHex from
-                      <> " "
-                      <> pointHex to
-                  )
-              Just center -> Right center
-
-    stolenAreas ordered cell =
-      let !count = length ordered
-          pairs = zip ordered cell
-          predecessors = drop (count - 1) pairs ++ take (count - 1) pairs
-       in zipWith walk pairs predecessors
-     where
-      walk (stopEdge, first) (initialEdge, initialPoint) = go initialEdge initialPoint 0 initialPositive initialNegative
-       where
-        !target = reverseEdge stopEdge
-        !limit = numDirectedEdges triangulation + 1
-        !initialPositive = px first * py initialPoint
-        !initialNegative = py first * px initialPoint
-        !vertex = vertexPoint triangulation (origin triangulation stopEdge)
-        go lastEdge lastPoint steps positive negative
-          | steps >= limit = Left "step-limit"
-          | incidentFace triangulation lastEdge == outerFace =
-              Left ("outer-face vertex=" <> pointHex vertex <> " steps=" <> show steps)
-          | otherwise =
-              case innerFaceVertices triangulation (incidentFace triangulation lastEdge) of
-                Nothing -> Left "no-face-vertices"
-                Just (a, b, c) ->
-                  case circumcenter
-                    (relative (vertexPoint triangulation a))
-                    (relative (vertexPoint triangulation b))
-                    (relative (vertexPoint triangulation c)) of
-                    Nothing ->
-                      Left ("circumcenter vertex=" <> pointHex vertex <> " steps=" <> show steps)
-                    Just current ->
-                      let !positive' = positive + px lastPoint * py current
-                          !negative' = negative + py lastPoint * px current
-                          !nextEdge = reverseEdge (next triangulation lastEdge)
-                       in if nextEdge == target
-                            then Right ((negative' + py current * px first) - (positive' + px current * py first))
-                            else go nextEdge current (steps + 1) positive' negative'
-      px (Point x _) = x
-      py (Point _ y) = y
-
--- | The interpolation gate, two artifacts per query. The neighbour set is
--- exact-predicate determined over triangulations that already agree
--- bit-for-bit, so it crosses implementations. The weights cannot: each
--- neighbour's stolen area uses the same operation sequence on both sides,
--- but the normalization total accumulates those areas in each side's own
--- neighbour order, and floating-point addition is not associative. The
--- weights are pinned per side in the driver instead.
-writeInterpolationGate :: FilePath -> Int -> Int -> IO ()
-writeInterpolationGate directory pointCount queryCount = do
-  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x0123456789abcdef pointCount)))
-  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
-  queries <- requireQueryPoints (V.fromList (randomPoints 0x2718281828459045 queryCount))
-  (neighborLines, weightLines) <-
-    V.foldM'
-      ( \(neighborAcc, weightAcc) query -> do
-          (entries, _, _) <- stToIO (foldNaturalNeighborWeights (\acc vertex weight -> (vertex, weight) : acc) [] workspace Nothing query)
-          let sortedEntries =
-                sortOn
-                  (\(x, y, _) -> (x, y))
-                  [ case vertexPoint triangulation vertex of
-                      Point x y -> (x, y, weight)
-                  | (vertex, weight) <- entries
-                  ]
-              queryHex = case queryPointValue query of Point x y -> hex64 x <> hex64 y
-          pure
-            ( queryHex <> concatMap (\(x, y, _) -> hex64 x <> hex64 y) sortedEntries : neighborAcc
-            , queryHex <> concatMap (\(x, y, weight) -> hex64 x <> hex64 y <> hex64 weight) sortedEntries : weightAcc
-            )
-      )
-      ([], [])
-      queries
-  writeFile
-    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-neighbors.txt"))
-    (unlines (reverse neighborLines))
-  writeFile
-    (directory </> ("interpolation-" <> show pointCount <> "-" <> show queryCount <> "-weights.txt"))
-    (unlines (reverse weightLines))
-
--- | The sum of one query's Sibson weights through the reusable workspace —
--- the allocation-free path, matching what the strict side's buffer reuse
--- does inside its measured region.
-interpolationWeightSum
-  :: NaturalNeighborWorkspace RealWorld 'Unconstrained Point () () ()
-  -> V.Vector QueryPoint
-  -> IO Double
-interpolationWeightSum workspace =
-  V.foldM'
-    ( \total query -> do
-        (weightSum, _, _) <- stToIO (foldNaturalNeighborWeights (\acc _ weight -> acc + weight) 0 workspace Nothing query)
-        pure (total + weightSum)
-    )
-    0
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Intersection.hs
+++ /dev/null
@@ -1,138 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Corridor-walk lanes, their inputs, and their gates.
-module Moonlight.Triangulation.Bench.SpadeCompare.Intersection where
-
-import Data.List (sortOn)
-import Data.Word (Word64)
-import System.FilePath ((</>))
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.IntersectionIterator
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
--- | Query endpoints pulled inward. The generator fills [-1, 1]^2, so a point
--- with both coordinates inside [-0.5, 0.5] is strictly inside the hull of a
--- few thousand of them and locating it lands on a face. A chord that starts
--- OUTSIDE the hull makes endpoint choice and local hull entry observable, so
--- those chords remain the separate "intersection-outside" cliff lane.
-interiorQueryPoints :: Word64 -> Int -> [Point]
-interiorQueryPoints seed count =
-  [Point (0.5 * x) (0.5 * y) | Point x y <- randomPoints seed count]
-
--- | Query origins pushed away from the dense centre so the corpus repeatedly
--- exercises outside-hull entry rather than only face-to-face corridors.
-exteriorQueryPoints :: Word64 -> Int -> [Point]
-exteriorQueryPoints seed count =
-  [Point (4 * x) (4 * y) | Point x y <- randomPoints seed count]
-
-intersectionChords :: Int -> [(Point, Point)]
-intersectionChords queryCount =
-  zip
-    (interiorQueryPoints 0x3141592653589793 queryCount)
-    (interiorQueryPoints 0x2718281828459045 queryCount)
-
-outsideChords :: Int -> [(Point, Point)]
-outsideChords queryCount =
-  zip
-    (exteriorQueryPoints 0x243f6a8885a308d3 queryCount)
-    (interiorQueryPoints 0x2718281828459045 queryCount)
-
--- | One line per query: the line's own endpoints, then its intersection
--- sequence in the order the walk reports it. Every branch of the walk turns on
--- an orientation predicate over a mesh that already agrees bit for bit, so the
--- whole sequence is exactly determined and crosses implementations — order
--- included, which is why nothing here is sorted.
-writeIntersectionGate :: FilePath -> Int -> Int -> IO ()
-writeIntersectionGate directory pointCount queryCount = do
-  triangulation <- delaunayOf pointCount
-  chords <- requireQueryChords (intersectionChords queryCount)
-  writeFile
-    (directory </> ("intersection-" <> show pointCount <> "-" <> show queryCount <> "-chords.txt"))
-    (unlines (map (intersectionLine triangulation) chords))
-
-writeIntersectionOutsideGate :: FilePath -> Int -> Int -> IO ()
-writeIntersectionOutsideGate directory pointCount queryCount = do
-  triangulation <- delaunayOf pointCount
-  chords <- requireQueryChords (outsideChords queryCount)
-  writeFile
-    ( directory
-        </> ("intersection-outside-" <> show pointCount <> "-" <> show queryCount <> ".txt")
-    )
-    (unlines (map (intersectionLine triangulation) chords))
-
--- | Lines whose endpoints are mesh vertices, which is the only way to reach the
--- vertex and overlap branches of the walk at all: over generated doubles no
--- chord lands exactly on a site, so a gate built from chords alone exercises
--- one branch of three. The first family joins vertices the constraint
--- generator's index pairs select, which starts and ends the walk on a vertex;
--- the second runs along an existing edge, which is the only shape that produces
--- an overlap. Both name their endpoints by coordinate, the second from a list
--- both sides sort by the same encoding, because vertex handles do not cross.
-writeIntersectionVertexGate :: FilePath -> Int -> IO ()
-writeIntersectionVertexGate directory pointCount = do
-  triangulation <- delaunayOf pointCount
-  let sites = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
-      joins =
-        [ (sites V.! fromIndex, sites V.! toIndex)
-        | (fromIndex, toIndex) <- constraintPairs pointCount intersectionVertexLines
-        ]
-      alongEdges = take intersectionVertexLines (sortedEdgeEndpoints triangulation)
-  queryLines <- requireQueryChords (joins <> alongEdges)
-  writeFile
-    (directory </> ("intersection-vertexlines-" <> show pointCount <> ".txt"))
-    (unlines (map (intersectionLine triangulation) queryLines))
-
-intersectionVertexLines :: Int
-intersectionVertexLines = 100
-
-sortedEdgeEndpoints
-  :: DelaunayTriangulation Point
-  -> [(Point, Point)]
-sortedEdgeEndpoints triangulation =
-  map snd
-    ( sortOn
-        fst
-        [ (coordHex first <> coordHex second, (first, second))
-        | edge <- undirectedEdges triangulation
-        , let (from, to) = undirectedEndpoints triangulation edge
-              p = vertexPoint triangulation from
-              q = vertexPoint triangulation to
-              (first, second) = if pointKey p <= pointKey q then (p, q) else (q, p)
-        ]
-    )
- where
-  pointKey (Point x y) = (x, y)
-
-intersectionLine
-  :: DelaunayTriangulation Point
-  -> (QueryPoint, QueryPoint)
-  -> String
-intersectionLine triangulation (from, to) =
-  coordHex (queryPointValue from)
-    <> coordHex (queryPointValue to)
-    <> concatMap (encodeIntersection triangulation) (lineIntersections triangulation from to)
-
-encodeIntersection
-  :: DelaunayTriangulation Point
-  -> Intersection
-  -> String
-encodeIntersection triangulation event = case event of
-  VertexIntersection vertex -> "V" <> coordHex (vertexPoint triangulation vertex)
-  EdgeIntersection edge -> "E" <> directedHex edge
-  EdgeOverlap edge -> "O" <> directedHex edge
- where
-  directedHex edge =
-    coordHex (vertexPoint triangulation (origin triangulation edge))
-      <> coordHex (vertexPoint triangulation (destination triangulation edge))
-
--- | The corridor walk, counted. 'lineIntersections' materializes its answer, so
--- forcing the spine forces every step that produced it; spade's iterator is
--- lazy and is consumed by the same count.
-intersectionWalk
-  :: DelaunayTriangulation Point
-  -> [(QueryPoint, QueryPoint)]
-  -> Int
-intersectionWalk triangulation =
-  foldl' (\total (from, to) -> total + length (lineIntersections triangulation from to)) 0
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Lane.hs
+++ /dev/null
@@ -1,302 +0,0 @@
--- | The closed comparison vocabulary and its boundary projections.
---
--- The driver executes these requests, the report orders and names them, and
--- the Haskell runner interprets their command constructors. None of those
--- boundaries authors another lane universe.
-module Moonlight.Triangulation.Bench.SpadeCompare.Lane
-  ( LaneClass (..)
-  , LaneKind (..)
-  , LaneRequest (..)
-  , LaneObstruction (..)
-  , laneKindName
-  , laneRequestLabel
-  , parseLaneRequest
-  , renderInventoryCsv
-  , renderInventoryHuman
-  , renderLaneSpecs
-  , renderLaneObstruction
-  , renderSnapshotSpecs
-  ) where
-
-import Data.List (find, intercalate)
-import Text.Read (readMaybe)
-
-data LaneClass
-  = ParityLane
-  | CliffLane
-  deriving stock (Eq, Show)
-
--- | Every benchmark command understood by the Moonlight runner. The board is
--- a closed subset of this vocabulary; the remaining constructors are focused
--- diagnostics rather than Spade comparison rows.
-data LaneKind
-  = BulkLoadLane
-  | IncrementalLane
-  | SnapshotInsertLane
-  | SweepAngleCollapseLane
-  | DegenerateLineLane
-  | BatchSweepLane
-  | NearestLane
-  | CdtRecoveryLane
-  | ConstraintIncrementalLane
-  | ConstraintSplitLane
-  | PublicationFloorLane
-  | RemovalLane
-  | SnapshotRemovalLane
-  | HierarchyIncrementalLane
-  | HierarchyDuplicateLane
-  | HierarchyRemovalLane
-  | HierarchyRemovalOnlyLane
-  | HierarchyRebuildOnlyLane
-  | InterpolationLane
-  | VoronoiSweepLane
-  | DcelWalkLane
-  | AlphaPersistenceLane
-  | IntersectionLane
-  | IntersectionOutsideLane
-  | RefineLane
-  deriving stock (Bounded, Enum, Eq, Show)
-
-data LaneRequest = LaneRequest
-  { laneRequestKind :: !LaneKind
-  , laneRequestFirst :: !Int
-  , laneRequestSecond :: !Int
-  }
-  deriving stock (Eq, Show)
-
-data LaneObstruction
-  = UnknownLane !String
-  | UnknownLaneClass !String
-  | MalformedWorkSize !String
-  | NegativeWorkSize !Int
-  | AlphaPersistenceRequiresSpecializedRunner
-  deriving stock (Eq, Show)
-
-data LaneSpec = LaneSpec
-  { laneSpecClass :: !LaneClass
-  , laneSpecRequest :: !LaneRequest
-  , laneSpecDisplay :: !String
-  }
-
-data SnapshotStressSpec = SnapshotStressSpec
-  { snapshotSessionRequest :: !LaneRequest
-  , snapshotPublicationRequest :: !LaneRequest
-  , snapshotDisplay :: !String
-  }
-
-laneKindName :: LaneKind -> String
-laneKindName = \case
-  BulkLoadLane -> "bulk-load"
-  IncrementalLane -> "incremental"
-  SnapshotInsertLane -> "snapshot-insert"
-  SweepAngleCollapseLane -> "sweep-angle-collapse"
-  DegenerateLineLane -> "degenerate-line"
-  BatchSweepLane -> "batch-sweep"
-  NearestLane -> "nearest"
-  CdtRecoveryLane -> "cdt-recovery"
-  ConstraintIncrementalLane -> "constraint-incremental"
-  ConstraintSplitLane -> "constraint-split"
-  PublicationFloorLane -> "publication-floor"
-  RemovalLane -> "removal"
-  SnapshotRemovalLane -> "snapshot-removal"
-  HierarchyIncrementalLane -> "hierarchy-incremental"
-  HierarchyDuplicateLane -> "hierarchy-duplicate"
-  HierarchyRemovalLane -> "hierarchy-removal"
-  HierarchyRemovalOnlyLane -> "hierarchy-removal-only"
-  HierarchyRebuildOnlyLane -> "hierarchy-rebuild-only"
-  InterpolationLane -> "interpolation"
-  VoronoiSweepLane -> "voronoi-sweep"
-  DcelWalkLane -> "dcel-walk"
-  AlphaPersistenceLane -> "alpha-persistence"
-  IntersectionLane -> "intersection"
-  IntersectionOutsideLane -> "intersection-outside"
-  RefineLane -> "refine"
-
-laneRequestLabel :: LaneRequest -> String
-laneRequestLabel request =
-  intercalate
-    "-"
-    [ laneKindName (laneRequestKind request)
-    , show (laneRequestFirst request)
-    , show (laneRequestSecond request)
-    ]
-
-parseLaneRequest :: String -> String -> String -> Either LaneObstruction LaneRequest
-parseLaneRequest rawKind rawFirst rawSecond =
-  LaneRequest
-    <$> parseLaneKind rawKind
-    <*> parseWorkSize rawFirst
-    <*> parseWorkSize rawSecond
-
-renderLaneObstruction :: LaneObstruction -> String
-renderLaneObstruction = \case
-  UnknownLane lane -> "unknown benchmark lane: " <> lane
-  UnknownLaneClass laneClass -> "unknown benchmark lane class: " <> laneClass
-  MalformedWorkSize raw -> "benchmark work size is not an integer: " <> raw
-  NegativeWorkSize size -> "benchmark work size is negative: " <> show size
-  AlphaPersistenceRequiresSpecializedRunner ->
-    "alpha-persistence is owned by moonlight-triangulation-alpha-spade-referent"
-
-renderInventoryCsv :: String
-renderInventoryCsv =
-  unlines
-    ( fmap renderBoardLane boardLaneSpecs
-        <> fmap renderSnapshotLane snapshotStressSpecs
-    )
- where
-  renderBoardLane specification =
-    csvRow
-      [ "lane"
-      , laneClassName (laneSpecClass specification)
-      , laneRequestLabel (laneSpecRequest specification)
-      , laneSpecDisplay specification
-      ]
-
-  renderSnapshotLane specification =
-    csvRow
-      [ "lane"
-      , "snapshot"
-      , laneRequestLabel (snapshotPublicationRequest specification)
-      , snapshotDisplay specification
-      , laneRequestLabel (snapshotSessionRequest specification)
-      ]
-
-renderInventoryHuman :: String
-renderInventoryHuman =
-  concatMap renderClass [ParityLane, CliffLane]
-    <> "snapshot lanes ("
-    <> show (length snapshotStressSpecs)
-    <> ")\n"
-    <> concatMap renderSnapshot snapshotStressSpecs
- where
-  renderClass laneClass =
-    let specifications = laneSpecs laneClass
-     in laneClassName laneClass
-          <> " lanes ("
-          <> show (length specifications)
-          <> ")\n"
-          <> concatMap
-            ((<> "\n") . ("  " <>) . laneRequestLabel . laneSpecRequest)
-            specifications
-
-  renderSnapshot specification =
-    "  "
-      <> laneRequestLabel (snapshotPublicationRequest specification)
-      <> " against "
-      <> laneRequestLabel (snapshotSessionRequest specification)
-      <> "\n"
-
-renderLaneSpecs :: String -> Either LaneObstruction String
-renderLaneSpecs rawClass =
-  unlines . fmap (renderRequestWords . laneSpecRequest) . laneSpecs
-    <$> parseLaneClass rawClass
-
-renderSnapshotSpecs :: String
-renderSnapshotSpecs =
-  unlines
-    ( fmap
-        ( \specification ->
-            renderRequestWords (snapshotSessionRequest specification)
-              <> " "
-              <> renderRequestWords (snapshotPublicationRequest specification)
-        )
-        snapshotStressSpecs
-    )
-
-parseLaneKind :: String -> Either LaneObstruction LaneKind
-parseLaneKind raw =
-  maybe
-    (Left (UnknownLane raw))
-    Right
-    (find ((== raw) . laneKindName) [minBound .. maxBound])
-
-parseLaneClass :: String -> Either LaneObstruction LaneClass
-parseLaneClass = \case
-  "parity" -> Right ParityLane
-  "cliff" -> Right CliffLane
-  other -> Left (UnknownLaneClass other)
-
-parseWorkSize :: String -> Either LaneObstruction Int
-parseWorkSize raw =
-  maybe
-    (Left (MalformedWorkSize raw))
-    (\size -> if size < 0 then Left (NegativeWorkSize size) else Right size)
-    (readMaybe raw)
-
-laneClassName :: LaneClass -> String
-laneClassName = \case
-  ParityLane -> "parity"
-  CliffLane -> "cliff"
-
-laneSpecs :: LaneClass -> [LaneSpec]
-laneSpecs laneClass = filter ((== laneClass) . laneSpecClass) boardLaneSpecs
-
-renderRequestWords :: LaneRequest -> String
-renderRequestWords request =
-  unwords
-    [ laneKindName (laneRequestKind request)
-    , show (laneRequestFirst request)
-    , show (laneRequestSecond request)
-    ]
-
-csvRow :: [String] -> String
-csvRow = intercalate "," . fmap csvField
-
-csvField :: String -> String
-csvField field
-  | any (`elem` [',', '"', '\n', '\r']) field =
-      '"' : concatMap escapeQuote field <> "\""
-  | otherwise = field
- where
-  escapeQuote '"' = "\"\""
-  escapeQuote character = [character]
-
-boardLaneSpecs :: [LaneSpec]
-boardLaneSpecs =
-  [ parity BulkLoadLane 1000 0 "Bulk load, 1k points"
-  , parity BulkLoadLane 10000 0 "Bulk load, 10k"
-  , parity IncrementalLane 1000 0 "Incremental insert, 1k"
-  , parity IncrementalLane 10000 0 "Incremental insert, 10k"
-  , parity IncrementalLane 500000 0 "Incremental insert, 500k"
-  , parity NearestLane 20000 5000 "Nearest, 5k queries / 20k points"
-  , parity CdtRecoveryLane 8000 800 "CDT recovery, 800 / 8k"
-  , parity RefineLane 625 0 "Refinement, 625 Steiner"
-  , parity RefineLane 2500 0 "Refinement, 2,500 Steiner"
-  , parity RemovalLane 1000 250 "Removal, 250 / 1k"
-  , parity RemovalLane 10000 2500 "Removal, 2.5k / 10k"
-  , parity RemovalLane 100000 25000 "Removal, 25k / 100k"
-  , parity InterpolationLane 10000 1000 "Interpolation, 1k queries / 10k"
-  , parity InterpolationLane 100000 2000 "Interpolation, 2k queries / 100k"
-  , parity VoronoiSweepLane 1000 0 "Voronoi dual sweep, 1k sites"
-  , parity DcelWalkLane 2000 0 "DCEL traversal, 2k points"
-  , parity IntersectionLane 10000 500 "Line intersection, 500 chords / 10k"
-  , parity DegenerateLineLane 2000 0 "Exactly collinear bulk chain, 2k"
-  , cliff ConstraintIncrementalLane 8000 800 "Constraint one at a time, 800 / 8k"
-  , cliff ConstraintSplitLane 1000 0 "Constraint split, 1k crossings"
-  -- This deliberately stays at 1k while its parity twin also reaches 10k.
-  , cliff HierarchyIncrementalLane 1000 0 "Hierarchy insert, 1k"
-  , cliff HierarchyDuplicateLane 10000 500 "Hierarchy duplicate insert, 500 / 10k"
-  , cliff HierarchyRemovalLane 10000 250 "Hierarchy removal, 250 / 10k"
-  , cliff SweepAngleCollapseLane 2000 0 "Sweep hull index at one angle, 2k"
-  , cliff IntersectionOutsideLane 2000 100 "Line intersection from outside the hull, 100 / 2k"
-  ]
- where
-  parity :: LaneKind -> Int -> Int -> String -> LaneSpec
-  parity kind first second display =
-    LaneSpec ParityLane (LaneRequest kind first second) display
-
-  cliff :: LaneKind -> Int -> Int -> String -> LaneSpec
-  cliff kind first second display =
-    LaneSpec CliffLane (LaneRequest kind first second) display
-
-snapshotStressSpecs :: [SnapshotStressSpec]
-snapshotStressSpecs =
-  [ SnapshotStressSpec
-      (LaneRequest IncrementalLane 1000 0)
-      (LaneRequest SnapshotInsertLane 1000 0)
-      "Insert snapshots, 1k points"
-  , SnapshotStressSpec
-      (LaneRequest RemovalLane 10000 2500)
-      (LaneRequest SnapshotRemovalLane 10000 2500)
-      "Removal snapshots, 2.5k / 10k"
-  ]
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Removal.hs
+++ /dev/null
@@ -1,428 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Removal lanes, their gates, and the removal diagnostics.
-module Moonlight.Triangulation.Bench.SpadeCompare.Removal where
-
-import Control.Monad (when)
-import Data.Foldable (traverse_)
-import Data.List (sortOn)
-import System.Exit (exitWith, ExitCode (ExitFailure))
-import System.FilePath ((</>))
-import System.IO (hPutStrLn, stderr)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Predicates qualified as Predicates
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Removal
-import Moonlight.Triangulation.Session
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-
--- | The removal gate: a deterministic prefix of the input, removed by
--- coordinate, and the surviving canonical edge set. Delaunay triangulations
--- of point sets in general position are unique, so the survivor is forced
--- and both implementations must agree bit-for-bit.
-writeRemovalGate :: FilePath -> Int -> Int -> IO ()
-writeRemovalGate directory pointCount removalCount = do
-  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
-  triangulation <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
-  surviving <- removeAllByCoordinate triangulation (V.fromList (take removalCount points))
-  writeFile
-    (directory </> ("removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt"))
-    (unlines (canonicalEdges surviving))
-
--- | Remove vertices by coordinate inside one session: one O(n) dense thaw and
--- publication for the whole program, then O(degree) topology work per removal.
--- The snapshot 'locateAndRemove' fold pays that O(n) boundary per removal.
-removeAllByCoordinate
-  :: DelaunayTriangulation Point
-  -> V.Vector Point
-  -> IO (DelaunayTriangulation Point)
-removeAllByCoordinate triangulation removals =
-  case withSession triangulation 0 (removeManyAt removals) of
-    Left refusal -> fail (show refusal)
-    Right (outcomes, surviving, _) -> do
-      V.imapM_ checkRemoved outcomes
-      pure surviving
- where
-  checkRemoved index outcome = case outcome of
-    Nothing ->
-      error
-        ( "removal target is not a vertex: index="
-            <> show index
-            <> " point="
-            <> ( case removals V.! index of
-                  Point x y -> hex64 x <> "," <> hex64 y
-               )
-        )
-    Just _ -> pure ()
-
--- | Removal by coordinate through the persistent entry point: a thaw and a
--- publication per removal, against the session fold's one of each for the whole
--- program. Same removals, same order, same survivor — only the price differs.
-removeAllPersistently
-  :: DelaunayTriangulation Point
-  -> V.Vector Point
-  -> IO (DelaunayTriangulation Point)
-removeAllPersistently =
-  V.foldM'
-    (\current point -> do
-      outcome <- require (locateAndRemove current point)
-      case outcome of
-        Nothing -> fail ("removal target is not a vertex: " <> pointHex point)
-        Just result -> pure (removalTriangulation result)
-    )
-
-writePersistentRemovalGate :: FilePath -> Int -> Int -> IO ()
-writePersistentRemovalGate directory pointCount removalCount = do
-  let points = randomPoints 0x9e3779b97f4a7c15 pointCount
-  base <- buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList points))
-  surviving <- removeAllPersistently base (V.fromList (take removalCount points))
-  writeFile
-    ( directory
-        </> ("persistent-removal-" <> show pointCount <> "-" <> show removalCount <> "-edges.txt")
-    )
-    (unlines (canonicalEdges surviving))
-
--- | Removal audited against the one oracle that needs no second implementation:
--- the Delaunay triangulation of a point set in general position is unique, so
--- removing a prefix must leave exactly the triangulation a fresh bulk load of
--- the survivors produces. Reports the first removal at which that identity
--- breaks, so a divergence is dated rather than merely observed.
---
--- Every removal also reports the handle swap-compaction relocated into the
--- freed slot, and that handle is the caller's only re-anchor after a removal
--- invalidates the rest. An out-of-range or misnamed handle is invisible to the
--- edge-set oracle, so it is checked here against the vertex the mesh moved.
-reportRemovalAudit :: Int -> Int -> IO ()
-reportRemovalAudit pointCount removalCount = do
-  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
-  base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
-  let step current index
-        | index >= removalCount = putStrLn ("clean-through " <> show removalCount)
-        | otherwise =
-            case allPoints V.!? index of
-              Nothing -> putStrLn ("input-exhausted-at-removal " <> show index)
-              Just point -> do
-                outcome <- require (locateAndRemove current point)
-                case outcome of
-                  Nothing -> putStrLn ("located-nothing-at-removal " <> show index)
-                  Just result -> do
-                    let nextTriangulation = removalTriangulation result
-                        movedPoint =
-                          vertexPoint current (VertexId (fromIntegral (numVertices current - 1)))
-                    case removalOutcomeSwap (removalOutcome result) of
-                      Nothing
-                        | vertexPoint current (VertexId (fromIntegral (numVertices current - 1))) /= point ->
-                            putStrLn ("swap-handle-missing-at-removal " <> show index)
-                        | otherwise -> pure ()
-                      Just (swappedIn@(VertexId rawSwappedIn), _)
-                        | toInteger rawSwappedIn >= toInteger (numVertices nextTriangulation) ->
-                            putStrLn
-                              ( "swap-handle-out-of-range-at-removal "
-                                  <> show index
-                                  <> " handle="
-                                  <> show rawSwappedIn
-                                  <> " vertices="
-                                  <> show (numVertices nextTriangulation)
-                              )
-                        | vertexPoint nextTriangulation swappedIn /= movedPoint ->
-                            putStrLn
-                              ( "swap-handle-misnamed-at-removal "
-                                  <> show index
-                                  <> " handle="
-                                  <> show rawSwappedIn
-                              )
-                        | otherwise -> pure ()
-                    fresh <-
-                      buildTriangulation
-                        <$> require (delaunay unitElementDefaults (V.drop (index + 1) allPoints))
-                    let removalEdges = canonicalEdges nextTriangulation
-                        freshEdges = canonicalEdges fresh
-                    if removalEdges == freshEdges
-                      then step nextTriangulation (index + 1)
-                      else do
-                        let (onlyRemoval, onlyFresh) = sortedDifference removalEdges freshEdges
-                        putStrLn ("diverged-at-removal " <> show index)
-                        putStrLn
-                          ( "removed-point "
-                              <> case point of
-                                Point x y -> hex64 x <> "," <> hex64 y
-                          )
-                        putStrLn ("removal-vertices " <> show (numVertices nextTriangulation))
-                        putStrLn ("fresh-vertices " <> show (numVertices fresh))
-                        putStrLn ("removal-edges " <> show (length removalEdges))
-                        putStrLn ("fresh-edges " <> show (length freshEdges))
-                        putStrLn ("only-in-removal " <> show (length onlyRemoval))
-                        putStrLn ("only-in-fresh " <> show (length onlyFresh))
-                        let (invertedRemoval, illegalRemoval) = meshHealth nextTriangulation
-                            (invertedFresh, illegalFresh) = meshHealth fresh
-                        putStrLn ("removal-inverted-faces " <> show invertedRemoval)
-                        putStrLn ("removal-illegal-half-edges " <> show illegalRemoval)
-                        putStrLn ("fresh-inverted-faces " <> show invertedFresh)
-                        putStrLn ("fresh-illegal-half-edges " <> show illegalFresh)
-                        traverse_ (putStrLn . ("  removal-only " <>)) (take 8 onlyRemoval)
-                        traverse_ (putStrLn . ("  fresh-only " <>)) (take 8 onlyFresh)
-  step base 0
-
--- | The geometry around one removal, reconstructed with public API only. For
--- an interior removal the cavity border is the cyclic link of the removed
--- vertex, and nothing but a drain flip can destroy one of those non-star
--- edges: cleanup removes only the star, and swap-remove touches only the
--- removed vertex. The reported hull predicate guards that interpretation.
--- The fan origin 'remeshRing' would pick is also public: 'collectOutgoing'
--- starts at the stored out-edge, so the origin is the destination of that
--- edge.
-reportRemovalContext :: Int -> Int -> IO ()
-reportRemovalContext pointCount target = do
-  let allPoints = V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)
-  case allPoints V.!? target of
-    Nothing -> do
-      hPutStrLn stderr "removal-context: target index out of range"
-      exitWith (ExitFailure 2)
-    Just removedPoint -> do
-      base <- buildTriangulation <$> require (delaunay unitElementDefaults allPoints)
-      before <- removeAllByCoordinate base (V.take target allPoints)
-      after <- removeAllByCoordinate before (V.singleton removedPoint)
-      fresh <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop (target + 1) allPoints))
-      lab <- buildTriangulation <$> require (delaunay unitElementDefaults (V.drop target allPoints))
-      removedQuery <- require (mkQueryPoint removedPoint)
-      putStrLn ("lab-clean " <> show (canonicalEdges before == canonicalEdges lab))
-      case locatePoint before removedQuery of
-        OnVertex removedVertex -> do
-          let ring = ringOf before removedVertex
-              ringPoints = map snd ring
-              ringMember point = any (== point) ringPoints
-              rOnHull = any (\(edge, _) -> incidentFace before edge == outerFace) ring
-              afterPairs = canonicalEdgePairs after
-              freshPairs = canonicalEdgePairs fresh
-              borderPairs = cyclicPairs ringPoints
-              destroyedBorder = [pair | pair <- borderPairs, not (any (== pair) afterPairs)]
-              removalOnly = [pair | pair <- afterPairs, not (any (== pair) freshPairs)]
-              freshOnly = [pair | pair <- freshPairs, not (any (== pair) afterPairs)]
-          putStrLn ("r-on-hull " <> show rOnHull)
-          putStrLn ("ring-size " <> show (length ringPoints))
-          traverse_
-            (\(index, (_, point)) -> putStrLn ("  ring " <> show index <> " " <> pointHex point))
-            (zip [(0 :: Int) ..] ring)
-          putStrLn ("border-edges-before " <> show (length borderPairs))
-          putStrLn ("border-edges-destroyed " <> show (length destroyedBorder))
-          traverse_
-            (\(p, q) -> putStrLn ("  destroyed " <> pointHex p <> " " <> pointHex q))
-            destroyedBorder
-          case ringPoints of
-            [] -> putStrLn "fan-unavailable empty-ring"
-            fanOriginPoint : ringTail ->
-              if rOnHull
-                then putStrLn "fan-unavailable hull-removal"
-                else do
-                  let fanOrientations =
-                        zipWith3
-                          (\index p q -> (index, p, q, Predicates.orient2d fanOriginPoint p q))
-                          [(1 :: Int) ..]
-                          ringTail
-                          (drop 1 ringTail)
-                      fanCandidates =
-                        zipWith3
-                          ( \(index, priorPoint) current following ->
-                              ( index
-                              , current
-                              , Predicates.orient2d priorPoint following fanOriginPoint == GT
-                                  && Predicates.orient2d following priorPoint current == GT
-                              , Predicates.inCircle current fanOriginPoint priorPoint following
-                              )
-                          )
-                          (zip [(2 :: Int) ..] ringTail)
-                          (drop 1 ringTail)
-                          (drop 2 ringTail)
-                  putStrLn ("fan-origin " <> pointHex fanOriginPoint)
-                  putStrLn
-                    ( "fan-inverted "
-                        <> show (length [() | (_, _, _, verdict) <- fanOrientations, verdict == LT])
-                        <> " of "
-                        <> show (length fanOrientations)
-                    )
-                  traverse_
-                    ( \(index, p, q, verdict) ->
-                        when (verdict == LT) $
-                          putStrLn
-                            ( "  fan-inverted-at "
-                                <> show index
-                                <> " "
-                                <> pointHex fanOriginPoint
-                                <> " "
-                                <> pointHex p
-                                <> " "
-                                <> pointHex q
-                            )
-                    )
-                    fanOrientations
-                  traverse_
-                    ( \(index, current, convex, circle) ->
-                        putStrLn
-                          ( "  fan-candidate "
-                              <> show index
-                              <> " "
-                              <> pointHex current
-                              <> "->"
-                              <> pointHex fanOriginPoint
-                              <> " convex="
-                              <> show convex
-                              <> " incircle="
-                              <> show circle
-                          )
-                    )
-                    fanCandidates
-          putStrLn ("only-in-removal " <> show (length removalOnly))
-          traverse_
-            ( \(p, q) ->
-                putStrLn
-                  ( "  removal-only "
-                      <> pointHex p
-                      <> " "
-                      <> pointHex q
-                      <> " ring="
-                      <> show (ringMember p, ringMember q)
-                  )
-            )
-            removalOnly
-          putStrLn ("only-in-fresh " <> show (length freshOnly))
-          traverse_
-            ( \(p, q) ->
-                putStrLn
-                  ( "  fresh-only "
-                      <> pointHex p
-                      <> " "
-                      <> pointHex q
-                      <> " ring="
-                      <> show (ringMember p, ringMember q)
-                  )
-            )
-            freshOnly
-          let inverted = invertedFaces after
-          putStrLn ("inverted-faces " <> show (length inverted))
-          traverse_
-            ( \(p, q, r) ->
-                putStrLn
-                  ( "  inverted-face "
-                      <> pointHex p
-                      <> " "
-                      <> pointHex q
-                      <> " "
-                      <> pointHex r
-                      <> " ring="
-                      <> show (ringMember p, ringMember q, ringMember r)
-                  )
-            )
-            inverted
-        other -> putStrLn ("locate-unexpected " <> show other)
- where
-  ringOf triangulation vertex =
-    case vertexOutEdge triangulation vertex of
-      Nothing -> []
-      Just start -> walk start start []
-   where
-    walk start current acc =
-      let ringVertex = destination triangulation current
-          nextEdge = counterClockwise triangulation current
-          acc' = (current, vertexPoint triangulation ringVertex) : acc
-       in if nextEdge == start
-            then reverse acc'
-            else walk start nextEdge acc'
-
-  cyclicPairs (first : second : rest) =
-    zipWith canonicalPair (first : second : rest) ((second : rest) ++ [first])
-  cyclicPairs _ = []
-
-  canonicalEdgePairs triangulation =
-    sortOn
-      (\(p, q) -> (pointKey p, pointKey q))
-      [ canonicalPair p q
-      | edge <- undirectedEdges triangulation
-      , let (from, to) = undirectedEndpoints triangulation edge
-            p = vertexPoint triangulation from
-            q = vertexPoint triangulation to
-      ]
-
-  canonicalPair p q =
-    if pointKey p <= pointKey q then (p, q) else (q, p)
-
-  pointKey (Point x y) = (x, y)
-
-  invertedFaces triangulation =
-    [ (vertexPoint triangulation a, vertexPoint triangulation b, vertexPoint triangulation c)
-    | face <- innerFaces triangulation
-    , Just (a, b, c) <- [innerFaceVertices triangulation face]
-    , Predicates.orient2d
-        (vertexPoint triangulation a)
-        (vertexPoint triangulation b)
-        (vertexPoint triangulation c)
-        == LT
-    ]
-
--- | Two independent health measures, which together decide what a divergence
--- means. Delaunay's theorem says a /valid/ triangulation with no locally
--- illegal edge is globally Delaunay — so a mesh that differs from the fresh
--- build while carrying no illegal edge cannot be a valid triangulation, and
--- one carrying illegal edges was simply left under-legalized.
-meshHealth
-  :: Triangulation mode vertex directed undirected face
-  -> (Int, Int)
-meshHealth triangulation =
-  ( length [() | face <- innerFaces triangulation, not (isCounterClockwise face)]
-  , length
-      [ ()
-      | face <- innerFaces triangulation
-      , edge <- faceEdgeList face
-      , edgeIsIllegal edge
-      ]
-  )
- where
-  faceEdgeList face = case innerFaceDirectedEdges triangulation face of
-    Nothing -> []
-    Just (e0, e1, e2) -> [e0, e1, e2]
-
-  isCounterClockwise face = case innerFaceVertices triangulation face of
-    Nothing -> True
-    Just (a, b, c) ->
-      Predicates.orient2d
-        (vertexPoint triangulation a)
-        (vertexPoint triangulation b)
-        (vertexPoint triangulation c)
-        == GT
-
-  edgeIsIllegal edge =
-    let twin = reverseEdge edge
-     in incidentFace triangulation twin /= outerFace
-          && Predicates.inCircle
-            (vertexPoint triangulation (origin triangulation edge))
-            (vertexPoint triangulation (destination triangulation edge))
-            (vertexPoint triangulation (destination triangulation (next triangulation edge)))
-            (vertexPoint triangulation (destination triangulation (next triangulation twin)))
-            == GT
-
--- | The Delaunay triangulation of the survivors, built from scratch. Diffed
--- against a removal result it decides whether that result is the Delaunay
--- triangulation it is required to be.
-writeFreshSuffix :: Int -> Int -> FilePath -> IO ()
-writeFreshSuffix pointCount dropped path = do
-  triangulation <-
-    buildTriangulation
-      <$> require
-        ( delaunay
-            unitElementDefaults
-            (V.drop dropped (V.fromList (randomPoints 0x9e3779b97f4a7c15 pointCount)))
-        )
-  writeFile path (unlines (canonicalEdges triangulation))
-
--- | Symmetric difference of two ascending lists, by merge.
-sortedDifference :: Ord a => [a] -> [a] -> ([a], [a])
-sortedDifference [] right = ([], right)
-sortedDifference left [] = (left, [])
-sortedDifference left@(l : ls) right@(r : rs)
-  | l == r = sortedDifference ls rs
-  | l < r = let (onlyLeft, onlyRight) = sortedDifference ls right in (l : onlyLeft, onlyRight)
-  | otherwise = let (onlyLeft, onlyRight) = sortedDifference left rs in (onlyLeft, r : onlyRight)
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Support.hs
+++ /dev/null
@@ -1,164 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Input generation, canonical encoding, and the shared build helpers.
-module Moonlight.Triangulation.Bench.SpadeCompare.Support where
-
-import Data.List (sort)
-import Data.Word (Word64)
-import GHC.Float (castDoubleToWord64)
-import Numeric (showHex)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Bench.SpadeCompare.Input (randomPoints)
-import Moonlight.Triangulation.HintGenerator
-
--- | The same generated points flattened onto a sliver a picometre thick. No
--- coordinate becomes equal to another and no four points become cocircular, so
--- the input stays in general position and the load stays on the ordinary
--- face-building path; what collapses is the angular spread the bulk loaders
--- index their hulls by. That collapse is the whole of what this input
--- measures, and it does not reach the face-less location path — that one needs
--- exact collinearity, which 'exactlyCollinearPoints' supplies and gates.
-nearCollinearPoints :: Word64 -> Int -> [Point]
-nearCollinearPoints seed count =
-  [Point x (y * 1.0e-12) | Point x y <- randomPoints seed count]
-
--- | Points on one exact line: @(i, 0)@, every coordinate an integer that a
--- 'Double' holds exactly. No three are in general position, so 'orient2d'
--- returns an exact zero on both sides and no face is ever built.
---
--- Uniqueness does not need general position, only one valid answer. A
--- collinear set admits exactly one triangulation — the chain of consecutive
--- segments, @n - 1@ of them and no triangle — so the canonical edge set is
--- fully determined and gates bit for bit against the strict side. Both
--- implementations state that same count as their own degenerate Euler
--- invariant, which is what makes this a shared answer rather than a
--- coincidence.
-exactlyCollinearPoints :: Int -> [Point]
-exactlyCollinearPoints count = [Point (fromIntegral index) 0 | index <- [0 .. count - 1]]
-
-hex64 :: Double -> String
-hex64 value =
-  let raw = showHex (castDoubleToWord64 value) ""
-   in replicate (16 - length raw) '0' <> raw
-
--- | Canonical edge set: each undirected edge as its two endpoint coordinates in
--- IEEE-754 bit patterns, endpoint-ordered then globally sorted. Formatting
--- cannot introduce a spurious disagreement and rounding cannot hide a real one.
-canonicalEdges :: Triangulation mode vertex directed undirected face -> [String]
-canonicalEdges triangulation =
-  sort
-    [ encodeCanonicalEdge first second
-    | edge <- undirectedEdges triangulation
-    , let (from, to) = undirectedEndpoints triangulation edge
-          p = vertexPoint triangulation from
-          q = vertexPoint triangulation to
-          (first, second) = if key p <= key q then (p, q) else (q, p)
-    ]
- where
-  key (Point x y) = (x, y)
-
-canonicalConstraintEdges
-  :: ConstrainedDelaunayTriangulation vertex
-  -> [String]
-canonicalConstraintEdges triangulation =
-  sort
-    [ encodeCanonicalEdge first second
-    | edge <- constraintEdges triangulation
-    , let (from, to) = undirectedEndpoints triangulation edge
-          p = vertexPoint triangulation from
-          q = vertexPoint triangulation to
-          (first, second) =
-            if pointKey p <= pointKey q
-              then (p, q)
-              else (q, p)
-    ]
- where
-  pointKey (Point x y) = (x, y)
-
-encodeCanonicalEdge :: Point -> Point -> String
-encodeCanonicalEdge (Point ax ay) (Point bx by) =
-  concatMap hex64 [ax, ay, bx, by]
-
-coordHex :: Point -> String
-coordHex (Point x y) = hex64 x <> hex64 y
-
--- | A cycle, rotated to begin at its smallest encoded element.
---
--- Both implementations keep one entry edge per vertex and per face — whichever
--- their builder wrote last — so the two walk the same cycle from different
--- places. Rotating pins the order, which the mesh determines, and declines to
--- pin the starting point, which it does not. The rotation compares the encoded
--- strings rather than the coordinates so that both sides order by the same
--- comparison: the bit pattern of a negative double does not sort in numeric
--- order, so a Haskell 'Ord' on 'Point' and a Rust @partial_cmp@ on a tuple
--- would each have to be argued to agree where two byte strings simply do.
-rotateToSmallest :: [String] -> [String]
-rotateToSmallest [] = []
-rotateToSmallest ring =
-  let !smallest = minimum ring
-      (before, after) = break (== smallest) ring
-   in after <> before
-
-require :: Show failure => Either failure value -> IO value
-require (Left failure) = fail (show failure)
-require (Right value) = pure value
-
-requireQueryPoints
-  :: Traversable container
-  => container Point
-  -> IO (container QueryPoint)
-requireQueryPoints = require . traverse mkQueryPoint
-
-requireQueryChords
-  :: Traversable container
-  => container (Point, Point)
-  -> IO (container (QueryPoint, QueryPoint))
-requireQueryChords =
-  require
-    . traverse
-      (\(from, to) -> (,) <$> mkQueryPoint from <*> mkQueryPoint to)
-
-delaunayOf :: Int -> IO (DelaunayTriangulation Point)
-delaunayOf count =
-  buildTriangulation <$> require (delaunay unitElementDefaults (V.fromList (randomPoints 0x9e3779b97f4a7c15 count)))
-
--- | One line per query: the coordinates of the nearest vertex the hierarchy
--- routed the search to, or @none@.
-nearestAnswer
-  :: DelaunayTriangulation Point
-  -> HierarchyHint
-  -> QueryPoint
-  -> String
-nearestAnswer triangulation hierarchy query =
-  let hint = case hierarchyHint hierarchy query of
-        Just (VertexHint vertex) -> Just vertex
-        _ -> Nothing
-   in case nearestNeighbor triangulation hint query of
-        Nothing -> "none"
-        Just (vertex, _) ->
-          let Point x y = vertexPoint triangulation vertex
-           in hex64 x <> hex64 y
-
-pointHex :: Point -> String
-pointHex (Point x y) = hex64 x <> "," <> hex64 y
-
-constraintPairs :: Int -> Int -> [(Int, Int)]
-constraintPairs pointCount constraintCount =
-  take constraintCount
-    [ (a, b)
-    | index <- [0 ..]
-    , let a = index `mod` pointCount
-          b = (index * 6151 + pointCount `quot` 2) `mod` pointCount
-    , a /= b
-    ]
-
--- | A closed square with no dangling constraint. The interior segment used by
--- the package's own benchmark is outside spade's documented "closed shape"
--- contract and makes it refuse the whole domain — not a workload the two can be
--- timed on.
-refinementInput :: (V.Vector Point, V.Vector (Int, Int))
-refinementInput =
-  ( V.fromList [Point 0 0, Point 64 0, Point 64 64, Point 0 64]
-  , V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
-  )
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Timing.hs
+++ /dev/null
@@ -1,23 +0,0 @@
--- | The single fresh-process timing boundary shared by both Haskell runners.
-module Moonlight.Triangulation.Bench.SpadeCompare.Timing
-  ( measureLane
-  ) where
-
-import Control.DeepSeq (NFData, force)
-import Control.Exception (evaluate)
-import GHC.Clock (getMonotonicTimeNSec)
-import Moonlight.Triangulation.Bench.SpadeCompare.Lane
-  ( LaneRequest
-  , laneRequestLabel
-  )
-import System.Mem (performGC)
-
--- | Time exactly one fully forced iteration. Input construction belongs to
--- each interpreter before this boundary; the driver supplies fresh processes.
-measureLane :: NFData value => LaneRequest -> IO value -> IO ()
-measureLane request work = do
-  performGC
-  start <- getMonotonicTimeNSec
-  _ <- work >>= evaluate . force
-  stop <- getMonotonicTimeNSec
-  putStrLn (laneRequestLabel request <> "," <> show (stop - start))
diff --git a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs b/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs
deleted file mode 100644
--- a/bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs
+++ /dev/null
@@ -1,156 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | The Voronoi dual gate and the dual sweep lane.
-module Moonlight.Triangulation.Bench.SpadeCompare.Voronoi where
-
-import Data.List (sort)
-import System.FilePath ((</>))
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Voronoi
-import Moonlight.Triangulation.Bench.SpadeCompare.Support
-
--- | The dual, pinned by its combinatorics and by nothing else.
---
--- Every verb the dual offers resolves a primal element, so its answers are
--- exact over a mesh the bulk-load gate already pins bit for bit. Voronoi vertex
--- POSITIONS are the exception and are deliberately absent from every artifact
--- here: a position is a circumcentre, and the two implementations do not form
--- one the same way. spade takes the reciprocal of the determinant and
--- multiplies by it; this side first rescales both difference vectors by their
--- largest component and only then divides. Each rounds where the other does
--- not, so the positions cannot be bit-identical, and a gate over them would be
--- grading the arithmetic rather than the dual. What IS determined about a
--- position is whether it exists at all, and the vertex artifact pins that.
---
--- Direction vectors are pinned in full: each component is one subtraction of
--- two stored coordinates on both sides, so there is nothing left to round.
-writeVoronoiGate :: FilePath -> Int -> IO ()
-writeVoronoiGate directory count = do
-  triangulation <- delaunayOf count
-  let prefix = directory </> ("voronoi-" <> show count)
-  writeFile
-    (prefix <> "-dual.txt")
-    (unlines (sort (map (dualEdgeRecord triangulation) (directedVoronoiEdges triangulation))))
-  writeFile (prefix <> "-cells.txt") (unlines (voronoiCellLines triangulation))
-  writeFile (prefix <> "-vertices.txt") (unlines (voronoiVertexLines triangulation))
-
--- | One directed dual edge: the primal edge it names, the site of the dual face
--- to its left, the dual vertex at each end, the destinations its dual @next@
--- and @prev@ reach, and its direction vector. @next@ and @prev@ rotate about
--- the primal origin, so naming them by destination alone loses nothing.
-dualEdgeRecord
-  :: DelaunayTriangulation Point
-  -> DirectedVoronoiEdgeId
-  -> String
-dualEdgeRecord triangulation edge =
-  concat
-    [ coordHex (site (origin triangulation primal))
-    , coordHex (site (destination triangulation primal))
-    , coordHex (site (voronoiFaceSite (voronoiIncidentFace triangulation edge)))
-    , dualVertexName triangulation primal (voronoiFrom triangulation edge)
-    , dualVertexName triangulation primal (voronoiTo triangulation edge)
-    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiNext triangulation edge))))
-    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiPrevious triangulation edge))))
-    , coordHex (voronoiDirectionVector triangulation edge)
-    ]
- where
-  primal = asDelaunayDirectedEdge edge
-  site = vertexPoint triangulation
-
--- | An inner dual vertex named by the one corner of its dual face that is not
--- an endpoint of the edge the question was asked about — the record already
--- carries the other two, so the apex completes the face. An outer dual vertex
--- is the edge itself and needs no name.
---
--- The @X@ tag cannot be reached: an inner dual vertex names an inner face by
--- construction, and 'innerFaceVertices' refuses only the outer one. spade
--- cannot emit it at all, its dual vertex carrying a face handle already typed
--- inner, so an @X@ appearing here is a disagreement rather than a crash.
-dualVertexName
-  :: DelaunayTriangulation Point
-  -> DirectedEdgeId
-  -> VoronoiVertexId
-  -> String
-dualVertexName _ _ (OuterVoronoiVertex _) = "O"
-dualVertexName triangulation primal (InnerVoronoiVertex face) =
-  case innerFaceVertices triangulation face of
-    Nothing -> "X"
-    Just (a, b, c)
-      | isApex a -> named a
-      | isApex b -> named b
-      | otherwise -> named c
- where
-  from = origin triangulation primal
-  to = destination triangulation primal
-  isApex vertex = vertex /= from && vertex /= to
-  named vertex = "I" <> coordHex (vertexPoint triangulation vertex)
-
--- | The dual faces, one line each, plus the three enumerator lengths. The
--- undirected dual carries no artifact of its own: it is the primal undirected
--- edge set relabelled, which every existing edge gate already pins, so its
--- length is the only claim left to make about it.
-voronoiCellLines :: DelaunayTriangulation Point -> [String]
-voronoiCellLines triangulation =
-  [ "faces " <> show (length (voronoiFaces triangulation))
-  , "directed " <> show (length (directedVoronoiEdges triangulation))
-  , "undirected " <> show (length (undirectedVoronoiEdges triangulation))
-  ]
-    <> sort
-      [ coordHex (vertexPoint triangulation (voronoiFaceSite face)) <> concat (rotateToSmallest ring)
-      | face <- voronoiFaces triangulation
-      , let ring =
-              [ coordHex
-                  ( vertexPoint
-                      triangulation
-                      (destination triangulation (asDelaunayDirectedEdge edge))
-                  )
-              | edge <- voronoiFaceAdjacentEdges triangulation face
-              ]
-      ]
-
--- | The inner dual vertices, keyed by their dual face's three corners. Each
--- line carries whether the vertex has a position — the only exactly determined
--- thing about a circumcentre — and the outgoing triple the dual reports.
-voronoiVertexLines :: DelaunayTriangulation Point -> [String]
-voronoiVertexLines triangulation =
-  sort
-    [ concat (sort (map coordHex [site a, site b, site c]))
-        <> existence dual
-        <> concat (rotateToSmallest (outgoing dual))
-    | face <- innerFaces triangulation
-    , Just (a, b, c) <- [innerFaceVertices triangulation face]
-    , let dual = InnerVoronoiVertex face
-    ]
- where
-  site = vertexPoint triangulation
-
-  existence dual = case voronoiVertexPosition triangulation dual of
-    Nothing -> "N"
-    Just _ -> "J"
-
-  outgoing dual = case voronoiVertexOutgoingEdges triangulation dual of
-    Nothing -> []
-    Just edges ->
-      [ coordHex (site (origin triangulation primal)) <> coordHex (site (destination triangulation primal))
-      | edge <- edges
-      , let primal = asDelaunayDirectedEdge edge
-      ]
-
--- | The whole dual, walked the way spade's own documentation walks it: every
--- dual face, its adjacent dual edges, and the dual vertex each of those runs
--- to. The position of an inner dual vertex is summed rather than merely
--- reached, because a 'Maybe' scrutinised only to its outer constructor can wrap
--- a thunk and the lane would then be timing the allocation of a circumcentre
--- instead of the circumcentre. Unbounded cells are counted, so a dual that
--- stopped classifying hull edges as outer moves the answer rather than the cost.
-voronoiSweep :: DelaunayTriangulation Point -> (Double, Int)
-voronoiSweep triangulation = foldl' overCell (0, 0) (voronoiFaces triangulation)
- where
-  overCell accumulator face =
-    foldl' overEdge accumulator (voronoiFaceAdjacentEdges triangulation face)
-
-  overEdge (!total, !unbounded) edge =
-    case voronoiVertexPosition triangulation (voronoiTo triangulation edge) of
-      Nothing -> (total, unbounded + 1)
-      Just (Point x y) -> (total + x + y, unbounded)
diff --git a/bench/support/BenchSupport.hs b/bench/support/BenchSupport.hs
--- a/bench/support/BenchSupport.hs
+++ b/bench/support/BenchSupport.hs
@@ -10,16 +10,13 @@
 import Data.List qualified as List
 import Data.Word (Word64)
 import Data.Vector qualified as V
-import Moonlight.Triangulation.Dcel
+import Moonlight.Planar.Dcel
   ( faceVertices
   , vertexPoint
   )
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId)
-import Moonlight.Triangulation.Types
-  ( Point (Point)
-  , Triangulation
-  , pointX
-  )
+import Moonlight.Planar.Internal.HandleDefs (FaceId)
+import Moonlight.Planar.Point (Point (Point, pointX))
+import Moonlight.Planar.Types (Triangulation)
 
 randomPoints :: Word64 -> Int -> [Point]
 randomPoints seed count = take count (go seed)
diff --git a/docs/README.md b/docs/README.md
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,6 +8,9 @@
 | Which public operation should I call? | [Package operation index](../README.md#operations) |
 | Which library token should I depend on? | [Consumer libraries](../README.md#consumer-libraries) |
 | What algebraic laws do meshes satisfy? | [Package algebraic contract](../README.md#algebraic-contract) |
+| How do I author curves, paint illustrations, and make local semantic edits? | [Curve and illustration guide](../README.md#curves-and-illustration) |
+| How far is curve authoring from a complete illustration language? | [Present boundary and missing algebra](./curve-authoring-status.md) |
+| How do parts attach and lay out without losing their semantic ports? | [Typed attachment and geometric layout](../README.md#typed-attachment-and-geometric-layout) |
 | How are exact power cells and affine envelopes represented? | [Package affine-cell guide](../README.md#exact-power-cells-and-affine-envelopes) |
 | How do weighted sites survive insertion, removal, and reweighting? | [Persistent regular-site algebra](./regular-site-algebra.md) |
 | How do weighted regular cells become an exact alpha filtration? | [Exact weighted alpha filtration](./weighted-alpha.md) |
diff --git a/docs/activation-zigzag.md b/docs/activation-zigzag.md
--- a/docs/activation-zigzag.md
+++ b/docs/activation-zigzag.md
@@ -1,6 +1,6 @@
 # Activation zigzag persistence
 
-Use `Moonlight.Triangulation.Zigzag` when independently observed activation
+Use `Moonlight.Planar.Zigzag` when independently observed activation
 point clouds do not form a monotone filtration. Each point carries a stable
 label, each depth carries its own coordinates and exact squared-radius alpha
 threshold, and adjacent observations meet through the canonical cospan
@@ -38,7 +38,7 @@
 diagram or changing degree and multiplicity.
 
 The compile-checked
-[`squareLoopAcrossDepths`](./examples/Moonlight/Triangulation/Example/ActivationZigzag.hs)
+[`squareLoopAcrossDepths`](./examples/Moonlight/Planar/Example/ActivationZigzag.hs)
 example observes a square before its boundary appears, while it carries one
 loop, and after its Delaunay faces fill that loop. Its degree-one result is:
 
diff --git a/docs/art-common/Moonlight/Planar/Exhibit/EquationalGarden.hs b/docs/art-common/Moonlight/Planar/Exhibit/EquationalGarden.hs
new file mode 100644
--- /dev/null
+++ b/docs/art-common/Moonlight/Planar/Exhibit/EquationalGarden.hs
@@ -0,0 +1,404 @@
+-- | A moonlit botanical plate authored with profiles, interpolated stations,
+-- bowed lines and reusable local frames. No illustration-owned curve handles.
+module Moonlight.Planar.Exhibit.EquationalGarden
+  ( GardenControls (..)
+  , defaultGardenControls
+  , GardenPart (..)
+  , gardenPartName
+  , gardenPicture
+  , GardenAttachmentError (..)
+  , GardenPlant (..)
+  , StemPort (..), LeafPort (..), BlossomPort (..)
+  , gardenPlants, gardenLayoutPicture
+  , leafMotif
+  , petalOutline
+  , leafOutline
+  ) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( Affine2, AffineIso2, affine2, affineIso2, affineIsoMap, identityAffineIso2
+  , translationAffineIso2 )
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, OpenTrail, Subpath (..), circle, ellipse, path
+  , locate, openTrail, curveStep, line, transformLocatedClosedTrail )
+import Moonlight.Planar.Curve.Authoring
+  ( Knot (..), ProfileStation (..), bowedTrail, cardinalOpen, polygonTrail, profileOutline )
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactVector (..), PositiveExact, UnitInterval
+  , exactHalf, exactThird, exactPoint, exactPointCoordinates, positiveOne
+  , positiveTwo, unitHalf, unitOne, unitZero, unitIntervalValue
+  , boundsMinimumX, boundsMaximumX, boundsMinimumY, boundsMaximumY )
+import Moonlight.Planar.Illustration
+  ( Color (..), FillRule (..), GradientStop (..), LineCap (..), LineJoin (..)
+  , Paint (..), Picture, StrokeStyle (..), StrokeUnits (..)
+  , annotate, clip, fill, gradientStops, opacity, place, stroke )
+import Moonlight.Planar.Illustration.Motif
+  ( Motif, motif, motifPicture, motifPort, attachMotif, transformMotif )
+import Moonlight.Planar.Illustration.Layout
+  ( LayoutAxis (..), alignMotif, arrangeMotifs, geometryEnvelope, geometryBounds )
+
+-- | Ordinary semantic controls: opening changes petals; breeze bends stems
+-- and leaves. The unit interval is admitted once by its existing scalar owner.
+data GardenControls = GardenControls
+  { blossomOpening :: !UnitInterval
+  , gardenBreeze :: !UnitInterval
+  } deriving stock (Eq, Show)
+
+defaultGardenControls :: GardenControls
+defaultGardenControls = GardenControls unitHalf unitHalf
+
+data GardenPart = NightSky | Moon | Ground | Stems | Leaves | Blossoms | Ribbon | Fireflies
+  deriving stock (Eq, Ord, Show)
+
+gardenPartName :: GardenPart -> String
+gardenPartName part = case part of
+  NightSky -> "night-sky"
+  Moon -> "pierced-moon"
+  Ground -> "garden-ground"
+  Stems -> "interpolated-stems"
+  Leaves -> "profile-leaves"
+  Blossoms -> "repeated-profile-blossoms"
+  Ribbon -> "flowing-profile-ribbon"
+  Fireflies -> "fireflies"
+
+data Palette = Palette !Color !Color !Color
+
+data Plant = Plant
+  { plantRoot :: !ExactPoint
+  , plantFlower :: !ExactPoint
+  , plantScale :: !ExactRational
+  , plantTilt :: !ExactRational
+  , plantPalette :: !Palette
+  }
+
+pearl, blue, lilac, amber :: Palette
+pearl = Palette (RGB 40 100 114) (RGB 178 219 207) (RGB 249 246 213)
+blue = Palette (RGB 35 68 106) (RGB 115 173 194) (RGB 219 236 227)
+lilac = Palette (RGB 68 61 104) (RGB 162 159 194) (RGB 233 219 219)
+amber = Palette (RGB 106 85 77) (RGB 211 177 136) (RGB 250 233 181)
+
+plants :: [Plant]
+plants =
+  [ Plant (exactPoint 305 975) (exactPoint 237 447) (exactHalf + exactThird * exactHalf) (-exactThird) lilac
+  , Plant (exactPoint 938 960) (exactPoint 1014 548) (exactHalf + exactHalf ^ (3 :: Int)) exactThird blue
+  , Plant (exactPoint 626 971) (exactPoint 596 386) (1 + exactThird * exactHalf) (-exactHalf * exactThird) pearl
+  , Plant (exactPoint 429 981) (exactPoint 376 650) (1 - exactHalf ^ (3 :: Int)) exactThird blue
+  , Plant (exactPoint 806 973) (exactPoint 849 724) (1 - exactHalf ^ (2 :: Int)) (-exactThird) amber
+  ]
+
+-- | One tapered profile is reused for broad petals, narrow leaves and light
+-- ribbons. Stations express center/half-span, never Bezier control handles.
+lanceolate :: ExactRational -> ExactRational -> ExactRational -> Located ClosedTrail
+lanceolate height width bend = profileOutline unitHalf $
+  ProfileStation (exactPoint 0 0) (ExactVector 0 0) :|
+    [ ProfileStation (exactPoint (bend * exactHalf) (negate height * exactHalf ^ (2 :: Int)))
+        (ExactVector (width * exactHalf) 0)
+    , ProfileStation (exactPoint bend (negate height * (exactHalf + exactHalf ^ (3 :: Int))))
+        (ExactVector width 0)
+    , ProfileStation (exactPoint (bend * exactHalf) (negate height * (1 - exactHalf ^ (3 :: Int))))
+        (ExactVector (width * exactHalf) 0)
+    , ProfileStation (exactPoint 0 (negate height)) (ExactVector 0 0)
+    ]
+
+petalOutline :: UnitInterval -> Located ClosedTrail
+petalOutline opening =
+  lanceolate 142 (19 + 17 * unitIntervalValue opening) (8 * (1 - unitIntervalValue opening))
+
+leafOutline :: UnitInterval -> Located ClosedTrail
+leafOutline = leafContour 24
+
+leafContour :: ExactRational -> UnitInterval -> Located ClosedTrail
+leafContour width breeze = lanceolate 158 width (24 * (2 * unitIntervalValue breeze - 1))
+
+data StemPort = LowerLeafSocket | UpperLeafSocket | BlossomSocket
+  deriving stock (Eq, Ord, Show, Enum, Bounded)
+
+data LeafPort = LeafRoot
+  deriving stock (Eq, Ord, Show)
+
+data BlossomPort = BlossomRoot
+  deriving stock (Eq, Ord, Show)
+
+data GardenAttachmentError = SingularGardenSocket !ExactPoint !StemPort
+  deriving stock (Eq, Show)
+
+-- An authored heterogeneous assembly, not a generic connection graph. Each
+-- child retains its ports until the final ordered painting projection.
+data GardenPlant = GardenPlant
+  { gardenStem :: !(Motif StemPort GardenPart)
+  , gardenLowerLeaf :: !(Motif LeafPort GardenPart)
+  , gardenUpperLeaf :: !(Motif LeafPort GardenPart)
+  , gardenBlossom :: !(Motif BlossomPort GardenPart)
+  }
+
+gardenPlants :: GardenControls -> Either GardenAttachmentError [GardenPlant]
+gardenPlants controls = traverse assemble plants
+ where
+  leaf :: Motif LeafPort GardenPart
+  leaf = leafMotif 24 (gardenBreeze controls)
+  assemble :: Plant -> Either GardenAttachmentError GardenPlant
+  assemble plant = do
+    stem <- stemMotif controls plant
+    pure (GardenPlant stem
+      (attachMotif LeafRoot (motifPort stem LowerLeafSocket) leaf)
+      (attachMotif LeafRoot (motifPort stem UpperLeafSocket) leaf)
+      (attachMotif BlossomRoot (motifPort stem BlossomSocket)
+        (blossomMotif controls (plantPalette plant))))
+
+gardenPicture :: GardenControls -> Either GardenAttachmentError (Picture GardenPart)
+gardenPicture controls = do
+  assemblies <- gardenPlants controls
+  pure $ annotate NightSky sky
+    <> annotate Moon moon
+    <> annotate Ground ground
+    <> annotate Stems (foldMap (motifPicture . gardenStem) assemblies <> grass)
+    <> annotate Leaves (foldMap (\plant -> motifPicture (gardenLowerLeaf plant)
+      <> motifPicture (gardenUpperLeaf plant)) assemblies)
+    <> annotate Blossoms (foldMap (motifPicture . gardenBlossom) assemblies)
+    <> annotate Ribbon ribbon
+    <> annotate Fireflies fireflies
+
+-- The origin, two interpolated leaf attachments and flower center are the
+-- same values in stem and leaf authoring: no placement by sample index.
+stemStation :: GardenControls -> Plant -> ExactRational -> ExactPoint
+stemStation controls plant t =
+  let (rx, ry) = exactPointCoordinates (plantRoot plant)
+      (fx, fy) = exactPointCoordinates (plantFlower plant)
+      sway = 86 * (2 * unitIntervalValue (gardenBreeze controls) - 1)
+   in exactPoint (rx + t * (fx - rx) + 4 * t * (1 - t) * sway) (ry + t * (fy - ry))
+
+plantStem :: GardenControls -> Plant -> Picture GardenPart
+plantStem controls plant =
+  let stem = cardinalOpen unitHalf $ Interpolating (plantRoot plant) :|
+        (Interpolating . stemStation controls plant <$> [exactThird, 2 * exactThird, 1])
+   in ink positiveTwo (RGB 26 73 72) stem
+        <> ink positiveOne (RGB 100 148 116) stem
+
+stemMotif :: GardenControls -> Plant -> Either GardenAttachmentError (Motif StemPort GardenPart)
+stemMotif controls plant = do
+  lower <- admit LowerLeafSocket (leafSocket exactThird (-1) 1)
+  upper <- admit UpperLeafSocket (leafSocket (2 * exactThird) 1 (1 - exactThird))
+  flower <- admit BlossomSocket $ affine2
+    (ExactVector (plantScale plant) (plantTilt plant * plantScale plant * exactHalf))
+    (ExactVector 0 (plantScale plant * (1 - exactHalf ^ (3 :: Int))))
+    (pointVector (plantFlower plant))
+  pure $ motif (plantStem controls plant) $ \port -> case port of
+    LowerLeafSocket -> lower
+    UpperLeafSocket -> upper
+    BlossomSocket -> flower
+ where
+  admit :: StemPort -> Affine2 -> Either GardenAttachmentError AffineIso2
+  admit port = maybe (Left (SingularGardenSocket (plantRoot plant) port)) Right . affineIso2
+  leafSocket :: ExactRational -> ExactRational -> ExactRational -> Affine2
+  leafSocket t side size =
+    let scale = size * (exactHalf + plantScale plant * exactHalf)
+     in affine2
+          (ExactVector (scale * exactHalf) (side * scale))
+          (ExactVector (negate side * scale) (scale * exactHalf))
+          (pointVector (stemStation controls plant t))
+
+leafMotif :: ExactRational -> UnitInterval -> Motif LeafPort GardenPart
+leafMotif width breeze = motif picture (const identityAffineIso2)
+ where
+  outline :: Located ClosedTrail
+  outline = leafContour width breeze
+  vein :: Located OpenTrail
+  vein = bowedTrail ((2 * unitIntervalValue breeze - 1) * exactHalf ^ (3 :: Int))
+    (exactPoint 0 0) (exactPoint 0 (-158))
+  picture :: Picture GardenPart
+  picture = fill NonZero (gradient (exactPoint (negate width) 0) (exactPoint width (-138))
+            (Palette (RGB 13 44 52) (RGB 48 104 96) (RGB 114 162 130))) (outline :| [])
+          <> clip NonZero (outline :| [])
+            (ink positiveOne (RGB 151 180 133) vein
+              <> foldMap sideVein [1 .. 5 :: Int])
+  sideVein :: Int -> Picture GardenPart
+  sideVein index =
+    let y = negate (22 + 20 * fromIntegral index)
+     in opacity unitHalf $ foldMap
+          (\side -> ink positiveOne (RGB 96 150 120)
+            (bowedTrail (side * exactHalf ^ (3 :: Int)) (exactPoint 0 (y + 14)) (exactPoint (side * (width + 2)) y)))
+          [-1, 1]
+
+blossomMotif :: GardenControls -> Palette -> Motif BlossomPort GardenPart
+blossomMotif controls palette = motif picture (const identityAffineIso2)
+ where
+  picture :: Picture GardenPart
+  picture = opacity unitHalf (place (uniformAt 170 (exactPoint 0 8))
+    (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
+      (gradientStops (GradientStop unitZero (RGB 131 199 168) unitHalf :|
+        [GradientStop unitOne (RGB 57 130 136) unitZero])))
+      (circle positiveOne :| [])))
+    <> petals (blossomOpening controls) palette
+
+-- | The same leaves, with geometric bounds and real port axes. Widening the
+-- middle specimen displaces its successor; there is no authored successor x.
+gardenLayoutPicture :: UnitInterval -> Picture GardenPart
+gardenLayoutPicture opening =
+  sky <> annotate Leaves (foldMap (\value -> motifPicture value <> diagnostic value) specimens)
+ where
+  specimens :: Seq.Seq (Motif LeafPort GardenPart)
+  specimens = arrangeMotifs Horizontal 75 $ fmap
+    (alignMotif Vertical unitOne 640 . transformMotif (translationAffineIso2 (ExactVector 150 0))) $
+    Seq.fromList [leafMotif 24 unitHalf, leafMotif (24 + 70 * unitIntervalValue opening) unitHalf,
+      leafMotif 24 unitHalf]
+  diagnostic :: Motif LeafPort GardenPart -> Picture GardenPart
+  diagnostic value = portAxes (motifPort value LeafRoot) <>
+    case geometryBounds (geometryEnvelope (motifPicture value)) of
+      Nothing -> mempty
+      Just bounds -> stroke (StrokeStyle (Solid (RGB 182 164 109)) positiveOne
+          OutputUnits ButtCap BevelJoin) $ path $ Seq.singleton $ ClosedSubpath $
+        polygonTrail (exactPoint (boundsMinimumX bounds) (boundsMinimumY bounds) :|
+          [ exactPoint (boundsMaximumX bounds) (boundsMinimumY bounds)
+          , exactPoint (boundsMaximumX bounds) (boundsMaximumY bounds)
+          , exactPoint (boundsMinimumX bounds) (boundsMaximumY bounds) ])
+
+portAxes :: AffineIso2 -> Picture GardenPart
+portAxes frame = foldMap axis [ExactVector 16 0, ExactVector 0 (-16)]
+ where
+  axis :: ExactVector -> Picture GardenPart
+  axis direction = place (affineIsoMap frame) $
+    ink positiveTwo (RGB 239 157 120)
+      (locate (exactPoint 0 0) (openTrail (Seq.singleton (curveStep line direction))))
+
+-- Exact rational frames: cardinal quarter-turns and scaled diagonal rotations.
+-- One petal value supplies every spoke and both differently sized whorls.
+spokes :: [(ExactRational, ExactRational)]
+spokes =
+  [(0,-1),(diagonal,-diagonal),(1,0),(diagonal,diagonal)
+  ,(0,1),(-diagonal,diagonal),(-1,0),(-diagonal,-diagonal)]
+ where
+  diagonal :: ExactRational
+  diagonal = 3 * exactHalf ^ (2 :: Int)
+
+petals :: UnitInterval -> Palette -> Picture GardenPart
+petals opening palette =
+  whorl
+    <> place (affine2 (ExactVector exactHalf exactHalf) (ExactVector (-exactHalf) exactHalf) (ExactVector 0 0))
+      whorl
+    <> flowerHeart
+ where
+  whorl :: Picture GardenPart
+  whorl = foldMap (\direction -> place (spokeFrame direction) petal) spokes
+  outline :: Located ClosedTrail
+  outline = petalOutline opening
+  petal :: Picture GardenPart
+  petal = fill NonZero (gradient (exactPoint 0 6) (exactPoint 0 (-145)) palette) (outline :| [])
+    <> clip NonZero (outline :| [])
+      (opacity unitHalf
+        (fill NonZero (Solid (RGB 238 243 216)) (lanceolate 138 3 4 :| []))
+        <> ink positiveOne (RGB 137 184 170)
+          (bowedTrail (exactHalf ^ (5 :: Int)) (exactPoint 0 0) (exactPoint 0 (-136))))
+    <> stroke (StrokeStyle (Solid (RGB 172 207 189)) positiveOne OutputUnits RoundCap RoundJoin)
+      (path (Seq.singleton (ClosedSubpath outline)))
+
+flowerHeart :: Picture GardenPart
+flowerHeart =
+  fill NonZero (gradient (exactPoint (-20) (-20)) (exactPoint 20 20)
+    (Palette (RGB 234 209 141) (RGB 159 158 87) (RGB 69 108 92)))
+    (ellipse (ExactVector 25 0) (ExactVector 0 21) :| [])
+    <> foldMap pollen spokes
+    <> fill NonZero (Solid (RGB 230 223 161)) (circle positiveTwo :| [])
+ where
+  pollen :: (ExactRational, ExactRational) -> Picture GardenPart
+  pollen (x, y) = place (uniformAt 2 (exactPoint (15*x) (12*y))) $
+    fill NonZero (Solid (RGB 245 225 153)) (circle positiveOne :| [])
+
+sky :: Picture GardenPart
+sky = fill NonZero (gradient (exactPoint 150 0) (exactPoint 1050 1050)
+    (Palette (RGB 5 18 31) (RGB 19 46 58) (RGB 7 22 34)))
+    (polygonTrail (exactPoint 0 0 :| [exactPoint 1200 0, exactPoint 1200 1000, exactPoint 0 1000]) :| [])
+  <> place (affine2 (ExactVector 550 0) (ExactVector 0 410) (ExactVector 830 310))
+    (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
+      (gradientStops (GradientStop unitZero (RGB 61 99 103) unitHalf :|
+        [GradientStop unitOne (RGB 23 58 75) unitZero]))) (circle positiveOne :| []))
+  <> foldMap star [0 .. 71 :: Int]
+ where
+  star :: Int -> Picture GardenPart
+  star index =
+    let x = 48 + fromIntegral ((index * 173 + 41) `mod` 1100)
+        y = 44 + fromIntegral ((index * 97 + 17) `mod` 640)
+        size = exactHalf + fromIntegral (index `mod` 3) * exactHalf
+     in opacity unitHalf $ place (uniformAt size (exactPoint x y)) $
+          fill NonZero (Solid (RGB 182 207 189)) (spark :| [])
+
+moon :: Picture GardenPart
+moon = place (uniformAt 152 (exactPoint 943 219)) $
+  let outer = circle positiveOne
+      hole = transformLocatedClosedTrail
+        (uniformAt 1 (exactPoint exactHalf (-exactThird))) outer
+      -- Clip the overlapping even-odd circles to the outer disc: the cutout
+      -- is transparent and cannot create a second lobe outside the moon.
+      moonDisc :: Picture GardenPart
+      moonDisc = fill EvenOdd (gradient (exactPoint (-1) (-1)) (exactPoint 1 1)
+        (Palette (RGB 225 231 195) (RGB 150 183 168) (RGB 74 123 128))) (outer :| [hole])
+   in clip NonZero (outer :| []) moonDisc
+
+ground :: Picture GardenPart
+ground = place (uniformAt 1 (exactPoint 614 962)) $
+  fill NonZero (gradient (exactPoint 0 (-75)) (exactPoint 0 38)
+    (Palette (RGB 27 64 65) (RGB 13 39 48) (RGB 5 20 31)))
+    (ellipse (ExactVector 598 0) (ExactVector 0 83) :| [])
+
+grass :: Picture GardenPart
+grass = opacity unitHalf $ foldMap blade [0 .. 17 :: Int]
+ where
+  blade :: Int -> Picture GardenPart
+  blade index =
+    let x = 150 + fromIntegral (index * 53)
+        height = 90 + fromIntegral ((index * 71) `mod` 135)
+        lean = fromIntegral ((index * 31) `mod` 90) - 45
+     in ink positiveOne (RGB 76 124 111)
+          (bowedTrail (lean * exactHalf ^ (8 :: Int)) (exactPoint x 976) (exactPoint (x+lean) (976-height)))
+
+ribbon :: Picture GardenPart
+ribbon =
+  let stations = ProfileStation (exactPoint 205 904) (ExactVector 0 0) :|
+        [ProfileStation (exactPoint 380 925) (ExactVector (-4) 15)
+        ,ProfileStation (exactPoint 605 871) (ExactVector 0 21)
+        ,ProfileStation (exactPoint 829 895) (ExactVector 3 16)
+        ,ProfileStation (exactPoint 1023 852) (ExactVector 0 0)]
+      outline = profileOutline unitHalf stations
+   in fill NonZero (gradient (exactPoint 230 940) (exactPoint 1000 835)
+        (Palette (RGB 61 88 87) (RGB 147 168 136) (RGB 51 102 99))) (outline :| [])
+      <> clip NonZero (outline :| [])
+        (ink positiveOne (RGB 211 211 158)
+          (cardinalOpen unitHalf (Interpolating (exactPoint 205 904) :|
+            (Interpolating <$> [exactPoint 380 930,exactPoint 605 880,exactPoint 829 899,exactPoint 1023 852]))))
+
+fireflies :: Picture GardenPart
+fireflies = foldMap glow [(142,685),(525,195),(735,576),(1101,753),(618,754),(279,816)]
+ where
+  glow :: (ExactRational, ExactRational) -> Picture GardenPart
+  glow (x,y) = place (uniformAt 1 (exactPoint x y)) $
+    place (uniformAt 15 (exactPoint 0 0))
+      (fill NonZero (RadialGradient (exactPoint 0 0) positiveOne
+        (gradientStops (GradientStop unitZero (RGB 231 224 143) unitHalf :|
+          [GradientStop unitOne (RGB 163 198 157) unitZero]))) (circle positiveOne :| []))
+      <> fill NonZero (Solid (RGB 250 239 174)) (spark :| [])
+
+spark :: Located ClosedTrail
+spark = polygonTrail (exactPoint 0 (-3) :|
+  [exactPoint exactHalf (-exactHalf),exactPoint 3 0,exactPoint exactHalf exactHalf
+  ,exactPoint 0 3,exactPoint (-exactHalf) exactHalf,exactPoint (-3) 0,exactPoint (-exactHalf) (-exactHalf)])
+
+-- These local helpers are shared painting/placement expressions within this
+-- artwork, not another public rendering or geometry layer.
+gradient :: ExactPoint -> ExactPoint -> Palette -> Paint
+gradient a b (Palette dark middle light) = LinearGradient a b $
+  gradientStops (GradientStop unitZero dark unitOne :|
+    [GradientStop unitHalf middle unitOne, GradientStop unitOne light unitOne])
+
+ink :: PositiveExact -> Color -> Located OpenTrail -> Picture GardenPart
+ink width color curve = stroke (StrokeStyle (Solid color) width OutputUnits RoundCap RoundJoin)
+  (path (Seq.singleton (OpenSubpath curve)))
+
+uniformAt :: ExactRational -> ExactPoint -> Affine2
+uniformAt scale center = affine2 (ExactVector scale 0) (ExactVector 0 scale) (pointVector center)
+
+spokeFrame :: (ExactRational, ExactRational) -> Affine2
+spokeFrame (x,y) = affine2 (ExactVector (negate y) x) (ExactVector (negate x) (negate y)) (ExactVector 0 0)
+
+pointVector :: ExactPoint -> ExactVector
+pointVector = uncurry ExactVector . exactPointCoordinates
diff --git a/docs/art-common/Moonlight/Planar/Exhibit/Geometry.hs b/docs/art-common/Moonlight/Planar/Exhibit/Geometry.hs
new file mode 100644
--- /dev/null
+++ b/docs/art-common/Moonlight/Planar/Exhibit/Geometry.hs
@@ -0,0 +1,83 @@
+module Moonlight.Planar.Exhibit.Geometry
+  ( ShapeError (..)
+  , Triangle (..)
+  , triangulateShape
+  , solidObj
+  ) where
+
+import Data.Bifunctor (first)
+import Data.Function (on)
+import Data.List (groupBy, sortOn)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Vector as Vector
+import Moonlight.Planar.Cdt (CdtError, boundedRegionFaces, constrainedDelaunay)
+import Moonlight.Planar.Dcel (innerFaceVertices, vertexPoint)
+import Moonlight.Planar.Handles.HandleDefs (FaceId)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Types (buildTriangulation, unitElementDefaults)
+import Text.Printf (printf)
+
+data ShapeError = MeshRejected !CdtError | MissingTriangle !FaceId
+  deriving stock (Show)
+
+data Triangle = Triangle !Point !Point !Point
+
+-- Concave outlines and holes share the native constrained-domain builder.
+triangulateShape :: NonEmpty (NonEmpty Point) -> [Point] -> Either ShapeError [Triangle]
+triangulateShape loops interior = do
+  let boundaries = NonEmpty.toList loops
+      offsets = scanl (+) 0 (NonEmpty.length <$> boundaries)
+      constraints = concat (zipWith loopConstraints offsets boundaries)
+      points = concatMap NonEmpty.toList boundaries <> interior
+  result <- first MeshRejected $
+    constrainedDelaunay unitElementDefaults (Vector.fromList points) (Vector.fromList constraints)
+  let mesh = buildTriangulation result
+  traverse
+    (\face -> case innerFaceVertices mesh face of
+      Nothing -> Left (MissingTriangle face)
+      Just (a, b, c) -> Right (Triangle (vertexPoint mesh a) (vertexPoint mesh b) (vertexPoint mesh c)))
+    (boundedRegionFaces mesh)
+
+loopConstraints :: Int -> NonEmpty Point -> [(Int, Int)]
+loopConstraints offset points =
+  let indices = [offset .. offset + NonEmpty.length points - 1]
+   in zip indices (drop 1 indices <> take 1 indices)
+
+-- OBJ is a transport view. Coincident face vertices are welded on import;
+-- only edges incident to one selected triangle receive a side wall.
+solidObj :: String -> (Point -> Double) -> [Triangle] -> String
+solidObj name halfThickness triangles =
+  let oriented = orient <$> triangles
+      boundary = concat . filter ((== 1) . length)
+        . groupBy ((==) `on` edgeKey) . sortOn edgeKey
+        $ concatMap triangleEdges oriented
+   in "o " <> name <> "\n"
+      <> foldMap surfaces oriented
+      <> foldMap wall boundary
+ where
+  orient :: Triangle -> Triangle
+  orient triangle@(Triangle a@(Point ax ay) b@(Point bx by) c@(Point cx cy))
+    | (bx - ax) * (cy - ay) - (by - ay) * (cx - ax) >= 0 = triangle
+    | otherwise = Triangle a c b
+
+  triangleEdges :: Triangle -> [(Point, Point)]
+  triangleEdges (Triangle a b c) = [(a, b), (b, c), (c, a)]
+
+  edgeKey :: (Point, Point) -> ((Double, Double), (Double, Double))
+  edgeKey (Point ax ay, Point bx by) =
+    (min (ax, ay) (bx, by), max (ax, ay) (bx, by))
+
+  surfaces :: Triangle -> String
+  surfaces (Triangle a b c) = face (-1) [c, b, a] <> face 1 [a, b, c]
+
+  face :: Double -> [Point] -> String
+  face side points = foldMap (vertex side) points <> "f -3 -2 -1\n"
+
+  wall :: (Point, Point) -> String
+  wall (a, b) = vertex (-1) a <> vertex (-1) b <> vertex 1 b <> vertex 1 a
+    <> "f -4 -3 -2 -1\n"
+
+  vertex :: Double -> Point -> String
+  vertex side point@(Point x y) = printf "v %.9f %.9f %.9f\n"
+    (x / 750) (side * halfThickness point) (negate y / 750)
diff --git a/docs/art-common/Moonlight/Planar/Exhibit/IllustrationStudy.hs b/docs/art-common/Moonlight/Planar/Exhibit/IllustrationStudy.hs
new file mode 100644
--- /dev/null
+++ b/docs/art-common/Moonlight/Planar/Exhibit/IllustrationStudy.hs
@@ -0,0 +1,307 @@
+-- | Artistic intent lives here; curves, paint and SVG remain library-owned.
+module Moonlight.Planar.Exhibit.IllustrationStudy
+  ( StudyControls, StudyControl (..), StudyControlError (..), defaultControls
+  , editControl, StudyPart (..), partName, partContour, partPort
+  , studyPicture, diagnosticPicture
+  ) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( AffineIso2, affine2, affineIsoMap, translationAffineIso2, identityAffineIso2 )
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, OpenTrail, Subpath (..), ellipse, location, path, transformLocatedClosedTrail )
+import Moonlight.Planar.Curve.Authoring
+  ( Knot (Interpolating), ProfileStation (..), bowedTrail, cardinalClosed, cardinalOpen
+  , polygonTrail, profileOutline )
+import Moonlight.Planar.Exact
+import Moonlight.Planar.Illustration
+import Moonlight.Planar.Illustration.Motif
+  ( Motif, motif, motifPicture, motifPort, attachMotif )
+
+-- | Values are physical drawing units, not anonymous control-point indices.
+data StudyControls = StudyControls
+  { hornSweep :: !ExactRational
+  , eyeSpacing :: !ExactRational
+  , eyeTilt :: !ExactRational
+  , cloakFullness :: !ExactRational
+  , fullerWidth :: !ExactRational
+  } deriving stock (Eq, Show)
+
+data StudyControl = LeftHornSweep | EyeSpacing | EyeTilt | CloakFullness | FullerWidth
+  deriving stock (Eq, Ord, Show, Enum, Bounded)
+
+data StudyControlError = ControlOutsideRange !StudyControl !ExactRational
+  deriving stock (Eq, Show)
+
+defaultControls :: StudyControls
+defaultControls = StudyControls 0 57 7 0 12
+
+-- | Absolute semantic edits. Bounds keep the authored family intentional.
+editControl :: StudyControl -> ExactRational -> StudyControls -> Either StudyControlError StudyControls
+editControl control value controls
+  | value < low || value > high = Left (ControlOutsideRange control value)
+  | otherwise = Right $ case control of
+      LeftHornSweep -> controls { hornSweep = value }
+      EyeSpacing -> controls { eyeSpacing = value }
+      EyeTilt -> controls { eyeTilt = value }
+      CloakFullness -> controls { cloakFullness = value }
+      FullerWidth -> controls { fullerWidth = value }
+ where
+  (low, high) = case control of
+    LeftHornSweep -> (-45, 45)
+    EyeSpacing -> (40, 75)
+    EyeTilt -> (-15, 15)
+    CloakFullness -> (-25, 55)
+    FullerWidth -> (5, 24)
+
+data StudyPart = LeftHorn | RightHorn | Mask | LeftEye | RightEye | Cloak
+  | LeftFoot | RightFoot | Blade | Fuller | Guard | Grip
+  deriving stock (Eq, Ord, Show, Enum, Bounded)
+
+partName :: StudyPart -> String
+partName part = case part of
+  LeftHorn -> "knight-left-horn"
+  RightHorn -> "knight-right-horn"
+  Mask -> "knight-mask"
+  LeftEye -> "knight-left-eye"
+  RightEye -> "knight-right-eye"
+  Cloak -> "knight-cloak"
+  LeftFoot -> "knight-left-foot"
+  RightFoot -> "knight-right-foot"
+  Blade -> "crescent-blade"
+  Fuller -> "crescent-fuller"
+  Guard -> "crescent-guard"
+  Grip -> "crescent-grip"
+
+-- | Geometry and its semantic root are authored together. Roots need not be
+-- the first boundary point (notably horns and ellipses).
+data PartGeometry
+  = WorldContour !(Located ClosedTrail)
+  | AttachedContour !(Located ClosedTrail) !ExactVector
+
+data RootPort = RootPort
+
+partPort :: StudyControls -> StudyPart -> AffineIso2
+partPort controls part = motifPort (partMotif controls part (Solid ink)) RootPort
+
+partMotif :: StudyControls -> StudyPart -> Paint -> Motif RootPort StudyPart
+partMotif controls part paint = case partGeometry controls part of
+  WorldContour contour -> motif
+    (annotate part (fill NonZero paint (contour :| []))) (const (pointFrame (location contour)))
+  AttachedContour contour rootOffset -> attachMotif RootPort (translationAffineIso2 rootOffset) $
+    motif (annotate part (fill NonZero (localPaint rootOffset paint) (contour :| [])))
+      (const identityAffineIso2)
+
+partContour :: StudyControls -> StudyPart -> Located ClosedTrail
+partContour controls part = case partGeometry controls part of
+  WorldContour contour -> contour
+  AttachedContour contour rootOffset ->
+    transformLocatedClosedTrail (affineIsoMap (translationAffineIso2 rootOffset)) contour
+
+partGeometry :: StudyControls -> StudyPart -> PartGeometry
+partGeometry controls part = case part of
+  LeftHorn -> AttachedContour (horn (hornSweep controls)) (ExactVector 336 359)
+  RightHorn -> AttachedContour
+    (transformLocatedClosedTrail
+      (affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 0 0)) (horn 0))
+    (ExactVector 484 359)
+  LeftEye -> eyeGeometry (-1) controls
+  RightEye -> eyeGeometry 1 controls
+
+  Mask -> boundary $ cardinalClosed unitZero $ fmap (Interpolating . uncurry exactPoint)
+    ((298,344) :| [(410,326),(522,344),(535,409),(507,485),(473,519)
+      ,(410,529),(347,519),(313,485),(285,409)])
+  Cloak -> boundary $ let f = cloakFullness controls in cardinalClosed unitHalf $
+    fmap (Interpolating . uncurry exactPoint)
+      ((359,499) :| [(320-f*exactHalf,612),(257-f,759),(288-f,775),(317-f,804)
+        ,(350,798),(385,817),(420,805),(454,811),(488,795),(518+f,778)
+        ,(560+f,751),(507+f*exactHalf,612),(461,499),(410,515)])
+  LeftFoot -> boundary $ oval (357,825) (37,15) (-5)
+  RightFoot -> boundary $ oval (464,825) (38,15) 5
+  Blade -> boundary $ profileOutline unitHalf $
+    ProfileStation (exactPoint 844 735) (ExactVector (-32) 0) :|
+    [ ProfileStation (exactPoint 848 580) (ExactVector (-38) 0)
+    , ProfileStation (exactPoint 874 420) (ExactVector (-32) 0)
+    , ProfileStation (exactPoint 914 280) (ExactVector (-17) 0)
+    , ProfileStation (exactPoint 958 194) (ExactVector 0 0) ]
+  Fuller -> boundary $ let w=fullerWidth controls in profileOutline unitHalf $
+    ProfileStation (exactPoint 894 350) (ExactVector 0 0) :|
+    [ ProfileStation (exactPoint 871 440) (ExactVector (-w) 0)
+    , ProfileStation (exactPoint 850 550) (ExactVector (-w*exactHalf) 0)
+    , ProfileStation (exactPoint 843 630) (ExactVector 0 0) ]
+  Guard -> boundary $ profileOutline unitZero $
+    ProfileStation (exactPoint 743 736) (ExactVector 0 0) :|
+    [ ProfileStation (exactPoint 783 760) (ExactVector 0 (-10))
+    , ProfileStation (exactPoint 815 747) (ExactVector 0 (-10))
+    , ProfileStation (exactPoint 843 735) (ExactVector 0 (-15))
+    , ProfileStation (exactPoint 871 747) (ExactVector 0 (-10))
+    , ProfileStation (exactPoint 903 760) (ExactVector 0 (-10))
+    , ProfileStation (exactPoint 943 736) (ExactVector 0 0) ]
+  Grip -> boundary $ profileOutline unitHalf $
+    ProfileStation (exactPoint 843 753) (ExactVector (-14) 0) :|
+    [ ProfileStation (exactPoint 843 795) (ExactVector (-11) 0)
+    , ProfileStation (exactPoint 843 853) (ExactVector (-15) 0) ]
+
+ where
+  boundary :: Located ClosedTrail -> PartGeometry
+  boundary = WorldContour
+
+pointFrame :: ExactPoint -> AffineIso2
+pointFrame point = let (x,y) = exactPointCoordinates point
+                   in translationAffineIso2 (ExactVector x y)
+
+-- World-space paints must be pulled back by the attachment translation.
+-- All attached roots in this author are translations, so radii stay unchanged;
+-- this helper does not pretend to support arbitrary affine radial gradients.
+localPaint :: ExactVector -> Paint -> Paint
+localPaint (ExactVector x y) paint = case paint of
+  Solid color -> Solid color
+  LinearGradient start end stops -> LinearGradient (local start) (local end) stops
+  RadialGradient center radius stops -> RadialGradient (local center) radius stops
+ where
+  local :: ExactPoint -> ExactPoint
+  local point = translateExactPoint point (ExactVector (negate x) (negate y))
+
+eyeGeometry :: ExactRational -> StudyControls -> PartGeometry
+eyeGeometry side controls =
+  let center = exactPoint (410 + side * eyeSpacing controls) 420
+      (x,y) = exactPointCoordinates center
+      local = ellipse (ExactVector 27 (negate side * eyeTilt controls)) (ExactVector 0 43)
+   in AttachedContour local (ExactVector x y)
+
+type XY = (ExactRational, ExactRational)
+
+oval :: XY -> XY -> ExactRational -> Located ClosedTrail
+oval (x,y) (rx,ry) shear = transformLocatedClosedTrail
+  (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector x y))
+  (ellipse (ExactVector rx shear) (ExactVector 0 ry))
+
+rectangle :: XY -> XY -> Located ClosedTrail
+rectangle (x,y) (w,h) = polygonTrail $
+  fmap (uncurry exactPoint) ((x,y) :| [(x+w,y),(x+w,y+h),(x,y+h)])
+
+-- | One tapering horn profile; its mirror is the same authored shape. Sweep
+-- changes the upper spine, not a collection of unrelated tangent handles.
+horn :: ExactRational -> Located ClosedTrail
+horn sweep = profileOutline unitZero $
+  ProfileStation (exactPoint 0 0) (ExactVector (-18) 10) :|
+  [ ProfileStation (exactPoint (-34) (-78)) (ExactVector (-14) 0)
+  , ProfileStation (exactPoint (-55+sweep*exactHalf) (-146)) (ExactVector (-6) 0)
+  , ProfileStation (exactPoint (-42+sweep) (-213)) (ExactVector 0 0) ]
+
+
+ink, ivory, gold, blue :: Color
+ink = RGB 11 23 36
+ivory = RGB 227 242 236
+gold = RGB 194 162 108
+blue = RGB 95 169 194
+
+linear :: XY -> XY -> Color -> Color -> Color -> Paint
+linear a b lo mid hi = LinearGradient (uncurry exactPoint a) (uncurry exactPoint b)
+  (gradientStops (GradientStop unitZero lo unitOne :|
+    [GradientStop unitHalf mid unitOne, GradientStop unitOne hi unitOne]))
+
+studyPicture :: StudyControls -> Picture StudyPart
+studyPicture c = backdrop <> knight <> sword
+ where
+  shape :: StudyPart -> Paint -> Picture StudyPart
+  shape part paint = motifPicture (partMotif c part paint)
+  knight :: Picture StudyPart
+  knight = shape LeftFoot (Solid ink) <> shape RightFoot (Solid ink)
+    <> shape Cloak (linear (270,500) (540,810) (RGB 48 69 83) (RGB 24 43 57) (RGB 10 23 34))
+    <> cloakFolds c
+    <> shape LeftHorn porcelain <> shape RightHorn porcelain
+    <> shape Mask porcelain <> maskRim c
+    <> shape LeftEye (Solid ink) <> shape RightEye (Solid ink)
+    <> eyeGlints c
+  porcelain :: Paint
+  porcelain = linear (300,330) (520,500) (RGB 166 196 198) ivory (RGB 199 221 217)
+  bladeContours :: NonEmpty (Located ClosedTrail)
+  bladeContours = partContour c Blade :| [partContour c Fuller]
+  sword :: Picture StudyPart
+  sword = shape Grip (linear (827,750) (859,855) ink (RGB 62 81 86) ink)
+    <> gripBindings
+    <> annotate Blade (fill EvenOdd steel bladeContours)
+    <> annotate Blade (clip EvenOdd bladeContours (reflection <> bladeFacets))
+    <> annotate Blade (outline blue (partContour c Blade))
+    <> annotate Fuller (outline (RGB 161 208 211) (partContour c Fuller))
+    <> shape Guard (linear (740,725) (938,778) (RGB 92 82 68) (RGB 229 211 162) gold)
+    <> outline gold (partContour c Guard)
+    <> fill NonZero (Solid gold) (oval (843,862) (17,12) 0 :| [])
+    <> fill NonZero (linear (834,848) (850,870) blue ivory ink) (oval (843,859) (7,8) 0 :| [])
+  steel :: Paint
+  steel = linear (790,390) (912,590) (RGB 47 90 113) (RGB 178 220 223) (RGB 81 137 156)
+  reflection :: Picture StudyPart
+  reflection = opacity unitHalf $ fill NonZero
+    (linear (760,420) (925,590) (RGB 116 196 214) ivory (RGB 95 157 180))
+    (polygonTrail (fmap (uncurry exactPoint) ((760,530) :| [(928,413),(928,477),(760,594)])) :| [])
+  bladeFacets :: Picture StudyPart
+  bladeFacets = opacity unitHalf $ fill NonZero (Solid (RGB 226 247 240))
+    (polygonTrail (fmap (uncurry exactPoint) ((838,727) :| [(860,510),(958,194),(883,480)])) :| [])
+
+backdrop :: Picture StudyPart
+backdrop = fill NonZero (linear (0,0) (1200,1000) (RGB 9 20 31) (RGB 19 38 53) (RGB 6 17 28))
+  (rectangle (0,0) (1200,1000) :| [])
+  <> opacity unitHalf (fill NonZero (Solid (RGB 42 65 78)) (oval (815,335) (246,246) 0 :| []))
+  <> outline (RGB 65 87 96) (oval (815,335) (263,263) 0)
+  <> fill NonZero (Solid (RGB 8 20 29)) (oval (608,873) (403,27) 0 :| [])
+  <> foldMap star [(156,220),(210,387),(658,173),(1026,584),(690,706),(1060,230)]
+  <> outline (RGB 64 88 98) (rectangle (54,54) (1092,892))
+ where
+  star :: XY -> Picture StudyPart
+  star (x,y) = place (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector x y)) $
+    fill NonZero (Solid (RGB 134 165 176))
+      (polygonTrail (fmap (uncurry exactPoint)
+        ((0,-5) :| [(2,-1),(6,0),(2,1),(0,5),(-2,1),(-6,0),(-2,-1)])) :| [])
+
+outline :: Color -> Located ClosedTrail -> Picture part
+outline color closed = stroke (pen color) (path (Seq.singleton (ClosedSubpath closed)))
+
+pen :: Color -> StrokeStyle
+pen color = StrokeStyle (Solid color) positiveOne LocalUnits RoundCap RoundJoin
+
+-- | Decorative strokes are bowed chords or interpolated landmarks.
+inkLine :: Color -> Located OpenTrail -> Picture part
+inkLine color trail = stroke (pen color) (path (Seq.singleton (OpenSubpath trail)))
+
+cloakFolds :: StudyControls -> Picture StudyPart
+cloakFolds c = annotate Cloak $ clip NonZero (partContour c Cloak :| []) $
+  foldMap foldLine [(-2),(-1),0,1,2]
+ where
+  foldLine :: ExactRational -> Picture StudyPart
+  foldLine axis = inkLine (RGB 64 84 96) $
+    bowedTrail (axis*exactThird*exactThird*exactHalf)
+      (exactPoint (410+axis*19) 526)
+      (exactPoint (410+axis*(51+cloakFullness c*exactHalf)) 813)
+
+maskRim :: StudyControls -> Picture StudyPart
+maskRim c = annotate Mask $ outline (RGB 145 178 182) (partContour c Mask)
+  <> inkLine (RGB 244 252 242) (cardinalOpen unitHalf $
+    fmap (Interpolating . uncurry exactPoint) ((302,369) :| [(294,419),(320,480),(353,504)]))
+
+eyeGlints :: StudyControls -> Picture StudyPart
+eyeGlints c = foldMap glint [LeftEye,RightEye]
+ where
+  glint :: StudyPart -> Picture StudyPart
+  glint part = annotate part $ place (affineIsoMap (partPort c part)) $
+    opacity unitHalf $ fill NonZero (Solid (RGB 89 124 139))
+      (oval (-10,-17) (4,9) 0 :| [])
+
+gripBindings :: Picture StudyPart
+gripBindings = annotate Grip $ foldMap binding [0..6 :: Int]
+ where
+  binding :: Int -> Picture StudyPart
+  binding i = place
+    (affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector 0 (770 + fromIntegral i*11)))
+    (inkLine gold (bowedTrail (exactThird*exactHalf) (exactPoint 831 0) (exactPoint 855 0)))
+
+-- | Visible landmarks are annotation, never geometric identity or mesh indices.
+diagnosticPicture :: StudyControls -> Picture StudyPart
+diagnosticPicture controls = studyPicture controls <> foldMap landmark [minBound..maxBound]
+ where
+  landmark :: StudyPart -> Picture StudyPart
+  landmark part = annotate part $ outline (RGB 247 133 104)
+    (transformLocatedClosedTrail
+      (affineIsoMap (partPort controls part))
+      (ellipse (ExactVector 5 0) (ExactVector 0 5)))
diff --git a/docs/art-common/Moonlight/Planar/Exhibit/SpaceSword.hs b/docs/art-common/Moonlight/Planar/Exhibit/SpaceSword.hs
new file mode 100644
--- /dev/null
+++ b/docs/art-common/Moonlight/Planar/Exhibit/SpaceSword.hs
@@ -0,0 +1,386 @@
+-- | A standalone orbital sword authored from profiles, typed attachment frames,
+-- repeated inscriptions and one affine pose. No hand-traced Bezier handles.
+module Moonlight.Planar.Exhibit.SpaceSword
+  ( SpaceSwordControls (..)
+  , defaultSpaceSwordControls
+  , overchargedSpaceSwordControls
+  , SpaceSwordPart (..)
+  , spaceSwordPartName
+  , spaceSwordPicture
+  ) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( Affine2, affine2, identityAffineIso2, translationAffineIso2 )
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, OpenTrail, Subpath (..), circle, path )
+import Moonlight.Planar.Curve.Authoring
+  ( Knot (Interpolating), ProfileStation (..), bowedTrail, cardinalOpen
+  , polygonTrail, profileOutline )
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactVector (..), PositiveExact, UnitInterval
+  , exactHalf, exactPoint, exactPointCoordinates, exactThird, positiveOne, positiveTwo
+  , unitHalf, unitOne, unitZero, unitIntervalValue )
+import Moonlight.Planar.Illustration
+  ( Color (..), FillRule (..), GradientStop (..), LineCap (..), LineJoin (..)
+  , Paint (..), Picture, StrokeStyle (..), StrokeUnits (..), annotate, clip
+  , fill, gradientStops, opacity, place, stroke )
+import Moonlight.Planar.Illustration.Motif
+  ( Motif, attachMotif, motif, motifPicture, motifPort )
+
+-- | Charge widens the recessed energy channel and lengthens its trailing wake.
+-- Sweep bends the cutting edge without disturbing the hilt assembly.
+data SpaceSwordControls = SpaceSwordControls
+  { bladeCharge :: !UnitInterval
+  , bladeSweep :: !ExactRational
+  } deriving stock (Eq, Show)
+
+defaultSpaceSwordControls :: SpaceSwordControls
+defaultSpaceSwordControls = SpaceSwordControls unitHalf 28
+
+overchargedSpaceSwordControls :: SpaceSwordControls
+overchargedSpaceSwordControls = SpaceSwordControls unitOne 40
+
+data SpaceSwordPart
+  = VoidField
+  | NebulaVeil
+  | BladeAura
+  | BladeShell
+  | BladeCore
+  | BladeRunes
+  | Crossguard
+  | ReactorGrip
+  | Counterweight
+  | IonWake
+  deriving stock (Eq, Ord, Show, Enum, Bounded)
+
+spaceSwordPartName :: SpaceSwordPart -> String
+spaceSwordPartName part = case part of
+  VoidField -> "void-field"
+  NebulaVeil -> "nebula-veil"
+  BladeAura -> "blade-aura"
+  BladeShell -> "blade-shell"
+  BladeCore -> "blade-core"
+  BladeRunes -> "blade-runes"
+  Crossguard -> "crossguard"
+  ReactorGrip -> "reactor-grip"
+  Counterweight -> "counterweight"
+  IonWake -> "ion-wake"
+
+data BladePort = BladeRoot | BladeTip
+
+data RunePort = RuneRoot
+
+data HiltPort = BladeSocket
+
+spaceSwordPicture :: SpaceSwordControls -> Picture SpaceSwordPart
+spaceSwordPicture controls =
+  annotate VoidField voidField
+    <> annotate NebulaVeil nebulaVeil
+    <> swordAssembly controls
+
+-- | Blade and hilt are attached in their authoring frame, then the complete
+-- assembly receives one affine action. Geometry, clips, paint and ornament
+-- therefore cannot drift apart when the weapon is posed.
+swordAssembly :: SpaceSwordControls -> Picture SpaceSwordPart
+swordAssembly controls =
+  let hilt = hiltMotif
+      blade = attachMotif BladeRoot (motifPort hilt BladeSocket) (bladeMotif controls)
+      assembled = annotate IonWake (ionWake controls)
+        <> motifPicture blade
+        <> motifPicture hilt
+   in place swordPose assembled
+
+-- | A similarity pose: orthogonal columns of equal squared length. The socket
+-- at @(604,745)@ lands at @(350,710)@, sending the blade toward the upper right.
+swordPose :: Affine2
+swordPose = affine2
+  (ExactVector threeQuarters 1)
+  (ExactVector (-1) threeQuarters)
+  (ExactVector 642 (negate (452 + threeQuarters)))
+ where
+  threeQuarters = exactHalf + exactHalf * exactHalf
+
+bladeMotif :: SpaceSwordControls -> Motif BladePort SpaceSwordPart
+bladeMotif controls = motif picture port
+ where
+  charge = unitIntervalValue (bladeCharge controls)
+  sweep = bladeSweep controls
+  aura = bladeProfile (42 + 6 * charge) sweep
+  shell = bladeProfile 34 sweep
+  channel = channelProfile charge sweep
+  picture = annotate BladeAura
+      (opacity unitHalf (fill NonZero (Solid (RGB 27 118 151)) (aura :| [])))
+    <> annotate BladeShell
+      (fill NonZero shellPaint (shell :| [])
+        <> clip NonZero (shell :| [])
+          (bladeSpineFacet sweep <> cuttingBevel sweep <> bladeReflection sweep)
+        <> outline (RGB 118 154 166) shell)
+    <> annotate BladeCore
+      (fill NonZero corePaint (channel :| [])
+        <> outline (RGB 128 232 215) channel)
+    <> annotate BladeRunes (runeColumn controls)
+  port selected = case selected of
+    BladeRoot -> identityAffineIso2
+    BladeTip -> translationAffineIso2 (ExactVector sweep (-625))
+
+-- | The lower spine remains nearly straight while the cutting edge carries the
+-- changing width and sweep. The silhouette is still one editable profile.
+bladeProfile :: ExactRational -> ExactRational -> Located ClosedTrail
+bladeProfile width sweep = profileOutline unitHalf $
+  ProfileStation (exactPoint 0 0) (ExactVector width 0) :|
+  [ ProfileStation (exactPoint (width * exactThird) (-150))
+      (ExactVector (width + width * exactThird) 0)
+  , ProfileStation (exactPoint (width * exactThird + sweep * exactThird) (-345))
+      (ExactVector (width + 5) 0)
+  , ProfileStation (exactPoint (sweep - width * exactThird) (-530))
+      (ExactVector (width * exactHalf + 3) 0)
+  , ProfileStation (exactPoint sweep (-625)) (ExactVector 0 0)
+  ]
+
+channelProfile :: ExactRational -> ExactRational -> Located ClosedTrail
+channelProfile charge sweep = profileOutline unitHalf $
+  ProfileStation (exactPoint 8 (-28)) (ExactVector (4 + 2 * charge) 0) :|
+  [ ProfileStation (exactPoint 12 (-145)) (ExactVector (5 + 2 * charge) 0)
+  , ProfileStation (exactPoint 19 (-315)) (ExactVector (6 + 2 * charge) 0)
+  , ProfileStation (exactPoint (sweep - 7) (-490)) (ExactVector (4 + charge) 0)
+  , ProfileStation (exactPoint sweep (-579)) (ExactVector 0 0)
+  ]
+
+bladeSpineFacet :: ExactRational -> Picture SpaceSwordPart
+bladeSpineFacet sweep = fill NonZero (Solid (RGB 8 18 30)) $
+  polygonTrail (exactPoint (-34) 0 :|
+    [ exactPoint (-34) (-148)
+    , exactPoint (-19) (-345)
+    , exactPoint (sweep - 22) (-528)
+    , exactPoint (sweep - 5) (-602)
+    , exactPoint 2 (-340)
+    , exactPoint (-5) (-80)
+    ]) :| []
+
+cuttingBevel :: ExactRational -> Picture SpaceSwordPart
+cuttingBevel sweep = fill NonZero (Solid (RGB 148 182 190)) $
+  polygonTrail (exactPoint 34 0 :|
+    [ exactPoint 57 (-150)
+    , exactPoint 63 (-345)
+    , exactPoint (sweep + 20) (-530)
+    , exactPoint sweep (-625)
+    , exactPoint (sweep + 8) (-520)
+    , exactPoint 43 (-320)
+    , exactPoint 28 (-55)
+    ]) :| []
+
+bladeReflection :: ExactRational -> Picture SpaceSwordPart
+bladeReflection sweep = opacity unitHalf $ fill NonZero (Solid (RGB 220 239 224)) $
+  polygonTrail (exactPoint 27 (-302) :|
+    [ exactPoint 42 (-336)
+    , exactPoint (sweep + 13) (-485)
+    , exactPoint (sweep + 6) (-522)
+    , exactPoint 31 (-386)
+    ]) :| []
+
+runeColumn :: SpaceSwordControls -> Picture SpaceSwordPart
+runeColumn controls = foldMap placeRune runeStations
+ where
+  charge = unitIntervalValue (bladeCharge controls)
+  sweep = bladeSweep controls
+  rune = runeMotif charge
+  placeRune :: (ExactRational, ExactRational) -> Picture SpaceSwordPart
+  placeRune (x,y) = motifPicture $
+    attachMotif RuneRoot (translationAffineIso2 (ExactVector x y)) rune
+  runeStations :: [(ExactRational, ExactRational)]
+  runeStations = [(12,-136),(21,-322),(sweep-6,-474)]
+
+runeMotif :: ExactRational -> Motif RunePort SpaceSwordPart
+runeMotif charge = motif picture (const identityAffineIso2)
+ where
+  size = 5 + 2 * charge
+  rune = polygonTrail (exactPoint 0 (negate size) :|
+    [ exactPoint (size * exactHalf) 0
+    , exactPoint 0 size
+    , exactPoint (negate (size * exactHalf)) 0
+    ])
+  picture :: Picture SpaceSwordPart
+  picture = opacity unitHalf (fill NonZero (Solid (RGB 108 221 202)) (rune :| []))
+    <> outline (RGB 174 236 213) rune
+
+ionWake :: SpaceSwordControls -> Picture SpaceSwordPart
+ionWake controls = opacity unitHalf $
+  ink positiveTwo (RGB 39 146 184) outer
+    <> ink positiveOne (RGB 105 210 202) inner
+ where
+  charge = unitIntervalValue (bladeCharge controls)
+  sweep = bladeSweep controls
+  outer = cardinalOpen unitHalf (Interpolating (exactPoint 50 (-18)) :|
+    (Interpolating <$>
+      [ exactPoint (78 + 18 * charge) (-155)
+      , exactPoint (72 + 27 * charge) (-355)
+      , exactPoint (sweep + 38 + 36 * charge) (-574)
+      ]))
+  inner = bowedTrail (exactThird + charge * exactThird)
+    (exactPoint 46 (-42)) (exactPoint (sweep + 22) (-545))
+
+hiltMotif :: Motif HiltPort SpaceSwordPart
+hiltMotif = motif picture port
+ where
+  picture = annotate Crossguard crossguard
+    <> annotate ReactorGrip reactorGrip
+    <> annotate Counterweight counterweight
+  port BladeSocket = translationAffineIso2 (ExactVector 604 745)
+
+crossguard :: Picture SpaceSwordPart
+crossguard =
+  fill NonZero guardPaint (guardContour :| [])
+    <> fill NonZero (Solid (RGB 166 135 74)) (guardInlay :| [])
+    <> outline (RGB 177 151 91) guardContour
+    <> fill NonZero (Solid (RGB 12 29 39)) (socketContour :| [])
+    <> outline (RGB 91 143 148) socketContour
+
+guardContour :: Located ClosedTrail
+guardContour = polygonTrail (exactPoint 495 750 :|
+  [ exactPoint 552 730
+  , exactPoint 591 735
+  , exactPoint 604 721
+  , exactPoint 618 735
+  , exactPoint 663 728
+  , exactPoint 718 746
+  , exactPoint 658 765
+  , exactPoint 618 757
+  , exactPoint 604 770
+  , exactPoint 590 758
+  , exactPoint 550 766
+  ])
+
+guardInlay :: Located ClosedTrail
+guardInlay = polygonTrail (exactPoint 512 750 :|
+  [ exactPoint 556 738
+  , exactPoint 590 744
+  , exactPoint 590 750
+  , exactPoint 552 756
+  ])
+
+socketContour :: Located ClosedTrail
+socketContour = polygonTrail (exactPoint 584 733 :|
+  [ exactPoint 624 733
+  , exactPoint 632 758
+  , exactPoint 619 774
+  , exactPoint 589 774
+  , exactPoint 576 758
+  ])
+
+reactorGrip :: Picture SpaceSwordPart
+reactorGrip =
+  fill NonZero gripPaint (gripContour :| [])
+    <> clip NonZero (gripContour :| []) (foldMap gripBand [0..4 :: Int])
+    <> outline (RGB 73 105 113) gripContour
+    <> gripAxis
+
+gripContour :: Located ClosedTrail
+gripContour = profileOutline unitHalf $
+  ProfileStation (exactPoint 604 766) (ExactVector 19 0) :|
+  [ ProfileStation (exactPoint 604 815) (ExactVector 17 0)
+  , ProfileStation (exactPoint 604 884) (ExactVector 20 0)
+  , ProfileStation (exactPoint 604 922) (ExactVector 15 0)
+  ]
+
+gripBand :: Int -> Picture SpaceSwordPart
+gripBand index = opacity unitHalf $ fill NonZero (Solid (RGB 148 124 78)) $
+  polygonTrail (exactPoint 582 y :|
+    [ exactPoint 626 (y + 12)
+    , exactPoint 626 (y + 19)
+    , exactPoint 582 (y + 7)
+    ]) :| []
+ where
+  y = 780 + fromIntegral index * 27
+
+gripAxis :: Picture SpaceSwordPart
+gripAxis = opacity unitHalf $ ink positiveOne (RGB 81 159 158) $
+  cardinalOpen unitHalf (Interpolating (exactPoint 604 772) :|
+    (Interpolating <$> [exactPoint 600 822, exactPoint 607 875, exactPoint 604 916]))
+
+counterweight :: Picture SpaceSwordPart
+counterweight =
+  fill NonZero guardPaint (counterweightContour :| [])
+    <> outline (RGB 147 132 91) counterweightContour
+    <> fill NonZero (Solid (RGB 91 198 184)) (emitterSlit :| [])
+ where
+  counterweightContour = polygonTrail (exactPoint 604 913 :|
+    [ exactPoint 622 929
+    , exactPoint 619 954
+    , exactPoint 604 972
+    , exactPoint 589 954
+    , exactPoint 586 929
+    ])
+  emitterSlit = polygonTrail (exactPoint 599 939 :|
+    [exactPoint 609 939, exactPoint 611 949, exactPoint 604 956, exactPoint 597 949])
+
+voidField :: Picture SpaceSwordPart
+voidField = fill NonZero voidPaint
+    (polygonTrail (exactPoint 0 0 :|
+      [exactPoint 1200 0,exactPoint 1200 1000,exactPoint 0 1000]) :| [])
+  <> foldMap star stars
+  <> foldMap brightStar brightStars
+ where
+  star :: (ExactRational, ExactRational, ExactRational) -> Picture SpaceSwordPart
+  star (x,y,size) = place (uniformAt size (exactPoint x y)) $
+    opacity unitHalf (fill NonZero (Solid (RGB 139 166 184)) (circle positiveOne :| []))
+  brightStar :: (ExactRational, ExactRational, ExactRational) -> Picture SpaceSwordPart
+  brightStar (x,y,size) = place (uniformAt size (exactPoint x y)) $
+    opacity unitHalf (fill NonZero (Solid (RGB 173 205 207)) (spark :| []))
+  stars :: [(ExactRational, ExactRational, ExactRational)]
+  stars = [(82,132,1),(188,310,1),(298,91,2),(482,178,1),(699,85,1)
+    ,(884,152,1),(1094,88,1),(1057,416,2),(946,817,1),(1111,912,1)]
+  brightStars :: [(ExactRational, ExactRational, ExactRational)]
+  brightStars = [(229,655,2),(741,798,2),(1010,598,1)]
+
+nebulaVeil :: Picture SpaceSwordPart
+nebulaVeil = opacity unitHalf $
+  fill NonZero nebulaPaint (nebulaContour :| [])
+ where
+  nebulaContour = profileOutline unitHalf $
+    ProfileStation (exactPoint (-120) 940) (ExactVector 76 108) :|
+    [ ProfileStation (exactPoint 236 755) (ExactVector 66 101)
+    , ProfileStation (exactPoint 596 506) (ExactVector 54 88)
+    , ProfileStation (exactPoint 951 263) (ExactVector 42 67)
+    , ProfileStation (exactPoint 1280 72) (ExactVector 24 36)
+    ]
+
+spark :: Located ClosedTrail
+spark = polygonTrail (exactPoint 0 (-3) :|
+  [exactPoint 1 (-1),exactPoint 4 0,exactPoint 1 1,exactPoint 0 4
+  ,exactPoint (-1) 1,exactPoint (-4) 0,exactPoint (-1) (-1)])
+
+voidPaint, nebulaPaint, shellPaint, corePaint, guardPaint, gripPaint :: Paint
+voidPaint = linear (exactPoint 0 0) (exactPoint 1200 1000)
+  (RGB 2 5 13) (RGB 12 16 37) (RGB 4 9 22)
+nebulaPaint = linear (exactPoint 0 1000) (exactPoint 1200 0)
+  (RGB 39 17 57) (RGB 25 72 86) (RGB 8 20 43)
+shellPaint = linear (exactPoint (-40) 0) (exactPoint 70 (-625))
+  (RGB 13 27 43) (RGB 104 126 137) (RGB 15 49 68)
+corePaint = linear (exactPoint 8 (-28)) (exactPoint 40 (-579))
+  (RGB 26 78 89) (RGB 120 229 204) (RGB 31 113 139)
+guardPaint = linear (exactPoint 495 730) (exactPoint 718 766)
+  (RGB 18 28 39) (RGB 112 99 72) (RGB 20 42 53)
+gripPaint = linear (exactPoint 584 766) (exactPoint 624 922)
+  (RGB 7 14 24) (RGB 40 51 54) (RGB 8 18 27)
+
+linear :: ExactPoint -> ExactPoint -> Color -> Color -> Color -> Paint
+linear a b low middle high = LinearGradient a b $ gradientStops
+  (GradientStop unitZero low unitOne :|
+    [GradientStop unitHalf middle unitOne, GradientStop unitOne high unitOne])
+
+outline :: Color -> Located ClosedTrail -> Picture part
+outline color curve = stroke
+  (StrokeStyle (Solid color) positiveOne OutputUnits RoundCap RoundJoin)
+  (path (Seq.singleton (ClosedSubpath curve)))
+
+ink :: PositiveExact -> Color -> Located OpenTrail -> Picture part
+ink width color curve = stroke
+  (StrokeStyle (Solid color) width OutputUnits RoundCap RoundJoin)
+  (path (Seq.singleton (OpenSubpath curve)))
+
+uniformAt :: ExactRational -> ExactPoint -> Affine2
+uniformAt scale center = affine2
+  (ExactVector scale 0) (ExactVector 0 scale)
+  (uncurry ExactVector (exactPointCoordinates center))
diff --git a/docs/art-common/Moonlight/Planar/Exhibit/Svg.hs b/docs/art-common/Moonlight/Planar/Exhibit/Svg.hs
new file mode 100644
--- /dev/null
+++ b/docs/art-common/Moonlight/Planar/Exhibit/Svg.hs
@@ -0,0 +1,17 @@
+module Moonlight.Planar.Exhibit.Svg (polygon, circle, pointText, scalar) where
+
+import Moonlight.Planar.Point (Point (..))
+import Text.Printf (printf)
+
+polygon :: String -> [Point] -> String
+polygon attributes points = "<polygon points=\"" <> unwords (pointText <$> points) <> "\" " <> attributes <> "/>"
+
+circle :: Point -> Double -> String -> String
+circle (Point x y) radius attributes = "<circle cx=\"" <> scalar x <> "\" cy=\"" <> scalar y
+  <> "\" r=\"" <> scalar radius <> "\" " <> attributes <> "/>"
+
+pointText :: Point -> String
+pointText (Point x y) = scalar x <> "," <> scalar y
+
+scalar :: Double -> String
+scalar = printf "%.3f"
diff --git a/docs/category-observatory/Main.hs b/docs/category-observatory/Main.hs
--- a/docs/category-observatory/Main.hs
+++ b/docs/category-observatory/Main.hs
@@ -10,13 +10,13 @@
 import Data.Aeson qualified as Aeson
 import Data.Bifunctor (first)
 import Data.ByteString.Lazy.Char8 qualified as LazyByteString
-import Moonlight.Triangulation.Exhibit.CategoryObservatory
+import Moonlight.Planar.Exhibit.CategoryObservatory
   ( Exhibit,
     ExhibitError,
     buildExhibit,
     renderExhibitError,
   )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
+import Moonlight.Planar.Exhibit.CategoryObservatory.Picture
   ( PictureObstruction,
     categoryObservatoryPictureFileName,
     renderCategoryObservatoryPicture,
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory.hs
@@ -0,0 +1,97 @@
+module Moonlight.Planar.Exhibit.CategoryObservatory
+  ( Exhibit,
+    ExhibitError,
+    buildExhibit,
+    renderExhibitError,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.Vector qualified as Vector
+import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.CellComplex
+  ( DCELComplex,
+    fromExactCellSet,
+  )
+import Moonlight.Planar.CellSet
+  ( ExactCellSet,
+    closeFaceCellSet,
+    exactCellSetEdgeCount,
+    exactCellSetFaceCount,
+    exactCellSetVertexCount,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Category
+  ( projectCategory,
+    validateCategoryEnumeration,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Geometry (projectMesh)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( CellInventory (..),
+    Exhibit (..),
+    ExhibitError (..),
+    renderExhibitError,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Nerve
+  ( projectNerve,
+    validateNerveCount,
+  )
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces)
+import Moonlight.Planar.Point (Point (..))
+
+buildExhibit :: Either ExhibitError Exhibit
+buildExhibit = do
+  triangulation <- first ExhibitTriangulationBuildFailed (delaunayGeometry exhibitPoints)
+  selectedCells <-
+    first ExhibitCellSelectionFailed
+      (closeFaceCellSet triangulation (innerFaces triangulation))
+  complexValue <- first ExhibitCellComplexFailed (fromExactCellSet selectedCells)
+  let categoryValue = complexCategory complexValue
+  validateInventory selectedCells complexValue
+  validateCategoryEnumeration complexValue categoryValue
+  categoryProjection <- projectCategory triangulation categoryValue
+  nerveProjection <- projectNerve categoryValue
+  validateNerveCount complexValue nerveProjection
+  meshProjection <- projectMesh triangulation complexValue
+  pure
+    Exhibit
+      { schemaVersion = 2,
+        exhibitTitle = "Triangulation, incidence category, and flag nerve",
+        mesh = meshProjection,
+        category = categoryProjection,
+        nerve = nerveProjection
+      }
+
+validateInventory :: ExactCellSet -> DCELComplex -> Either ExhibitError ()
+validateInventory cellSet complexValue =
+  let expectedInventory =
+        CellInventory
+          { cellVertexCount = exactCellSetVertexCount cellSet,
+            cellEdgeCount = exactCellSetEdgeCount cellSet,
+            cellFaceCount = exactCellSetFaceCount cellSet
+          }
+      observedInventory =
+        CellInventory
+          { cellVertexCount = length (vertices complexValue),
+            cellEdgeCount = length (edges complexValue),
+            cellFaceCount = length (faces complexValue)
+          }
+   in if observedInventory == expectedInventory
+        then Right ()
+        else Left (ExhibitInventoryMismatch expectedInventory observedInventory)
+
+exhibitPoints :: Vector.Vector Point
+exhibitPoints =
+  Vector.fromList
+    [ Point 80 72,
+      Point 222 48,
+      Point 362 92,
+      Point 52 208,
+      Point 176 166,
+      Point 310 190,
+      Point 414 232,
+      Point 112 330,
+      Point 258 314,
+      Point 380 354
+    ]
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Category.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Category.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Category.hs
@@ -0,0 +1,127 @@
+module Moonlight.Planar.Exhibit.CategoryObservatory.Category
+  ( incidenceFlagCount,
+    projectCategory,
+    validateCategoryEnumeration,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.Containers.ListUtils (nubOrd)
+import Moonlight.Category
+  ( Category (..),
+    FiniteComposableCategory (..),
+  )
+import Moonlight.Homology.Pure.Topology.CellCategory
+  ( ComplexCat,
+    ComplexMor (..),
+    ComplexOb,
+  )
+import Moonlight.Homology.Pure.Topology.CellComplex
+  ( CellComplex2D (..),
+    CellTypes (..),
+    OrientedEdge (..),
+  )
+import Moonlight.Planar.CellComplex (DCELComplex)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Geometry (positionForObject)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Identifier
+  ( morphismKind,
+    objectIdentifier,
+    objectKind,
+    objectLabel,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( CategoryMorphism (..),
+    CategoryObject (..),
+    CategoryProjection (..),
+    ExhibitError (..),
+    MorphismEndpoint (..),
+    MorphismInventory (..),
+  )
+import Moonlight.Planar.Types (DelaunayTriangulation)
+
+projectCategory ::
+  DelaunayTriangulation () ->
+  ComplexCat DCELComplex ->
+  Either ExhibitError CategoryProjection
+projectCategory triangulation categoryValue =
+  CategoryProjection
+    <$> traverse
+      (projectCategoryObject triangulation)
+      (enumerateObjects categoryValue)
+    <*> traverse
+      (projectCategoryMorphism categoryValue)
+      (zip [0 ..] (enumerateMorphisms categoryValue))
+
+projectCategoryObject ::
+  DelaunayTriangulation () ->
+  ComplexOb DCELComplex ->
+  Either ExhibitError CategoryObject
+projectCategoryObject triangulation objectValue =
+  CategoryObject
+    (objectIdentifier objectValue)
+    (objectKind objectValue)
+    (objectLabel objectValue)
+    <$> positionForObject triangulation objectValue
+
+projectCategoryMorphism ::
+  ComplexCat DCELComplex ->
+  (Int, ComplexMor DCELComplex) ->
+  Either ExhibitError CategoryMorphism
+projectCategoryMorphism categoryValue (morphismIndex, morphismValue) =
+  CategoryMorphism
+    ("morphism-" <> show morphismIndex)
+    (morphismKind morphismValue)
+    <$> projectedEndpoint MorphismSource source
+    <*> projectedEndpoint MorphismTarget target
+  where
+    projectedEndpoint endpoint endpointProjection =
+      objectIdentifier
+        <$> first
+          (const (ExhibitMorphismEndpointUnavailable endpoint morphismIndex))
+          (endpointProjection categoryValue morphismValue)
+
+validateCategoryEnumeration ::
+  DCELComplex ->
+  ComplexCat DCELComplex ->
+  Either ExhibitError ()
+validateCategoryEnumeration complexValue categoryValue =
+  let morphisms = enumerateMorphisms categoryValue
+      expectedInventory =
+        MorphismInventory
+          { identityMorphismCount =
+              length (vertices complexValue)
+                + length (edges complexValue)
+                + length (faces complexValue),
+            faceEdgeMorphismCount =
+              sum (fmap (length . faceBoundary complexValue) (faces complexValue)),
+            edgeVertexMorphismCount =
+              sum (fmap (edgeEndpointCount complexValue) (edges complexValue)),
+            faceVertexMorphismCount =
+              sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
+          }
+      observedInventory =
+        foldMap morphismInventory morphisms
+   in if observedInventory == expectedInventory
+        then Right ()
+        else Left (ExhibitCategoryEnumerationMismatch expectedInventory observedInventory)
+
+edgeEndpointCount :: DCELComplex -> Edge DCELComplex -> Int
+edgeEndpointCount complexValue edgeValue =
+  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
+   in length (nubOrd [sourceVertex, targetVertex])
+
+incidenceFlagCount :: DCELComplex -> Face DCELComplex -> Int
+incidenceFlagCount complexValue faceValue =
+  sum
+    ( fmap
+        (edgeEndpointCount complexValue . orientedEdge)
+        (faceBoundary complexValue faceValue)
+    )
+
+morphismInventory :: ComplexMor DCELComplex -> MorphismInventory
+morphismInventory morphismValue =
+  case morphismValue of
+    IdentityMor _ -> MorphismInventory 1 0 0 0
+    FaceToEdge {} -> MorphismInventory 0 1 0 0
+    EdgeToVertex _ _ -> MorphismInventory 0 0 1 0
+    FaceToVertex {} -> MorphismInventory 0 0 0 1
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Geometry.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Geometry.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Geometry.hs
@@ -0,0 +1,131 @@
+module Moonlight.Planar.Exhibit.CategoryObservatory.Geometry
+  ( positionForObject,
+    projectMesh,
+  )
+where
+
+import Data.Foldable (foldMap')
+import Moonlight.Homology.Pure.Topology.CellCategory (ComplexOb (..))
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Planar.CellComplex (DCELComplex, DCELCellEdge (..), faceCutFace)
+import Moonlight.Planar.Dcel
+  ( faceVertices,
+    undirectedEndpoints,
+    vertexPoint,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Identifier
+  ( edgeIdentifier,
+    edgeObjectIdentifier,
+    faceIdentifier,
+    faceObjectIdentifier,
+    vertexIdentifier,
+    vertexObjectIdentifier,
+  )
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( ExhibitError (..),
+    MeshEdge (..),
+    MeshFace (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    Position (..),
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId,
+    VertexId,
+  )
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..))
+
+projectMesh :: DelaunayTriangulation () -> DCELComplex -> Either ExhibitError MeshProjection
+projectMesh triangulation complexValue = do
+  projectedEdges <- traverse (projectMeshEdge triangulation) (edges complexValue)
+  pure MeshProjection
+    { meshVertices = fmap (projectMeshVertex triangulation) (vertices complexValue),
+      meshEdges = projectedEdges,
+      meshFaces = fmap (projectMeshFace triangulation) (faces complexValue)
+    }
+
+projectMeshVertex :: DelaunayTriangulation () -> VertexId -> MeshVertex
+projectMeshVertex triangulation vertexValue =
+  MeshVertex
+    { meshVertexIdentifier = vertexIdentifier vertexValue,
+      meshVertexObjectIdentifier = vertexObjectIdentifier vertexValue,
+      meshVertexPosition = pointPosition (vertexPoint triangulation vertexValue)
+    }
+
+projectMeshEdge :: DelaunayTriangulation () -> DCELCellEdge -> Either ExhibitError MeshEdge
+projectMeshEdge triangulation edgeValue =
+  case edgeValue of
+    FaceBoundaryCut cut -> Left (ExhibitAbstractCutPositionUnavailable (faceCutFace cut))
+    GeometricCellEdge geometricEdge ->
+      let (sourceVertex, targetVertex) = undirectedEndpoints triangulation geometricEdge
+       in Right MeshEdge
+        { meshEdgeIdentifier = edgeIdentifier edgeValue,
+          meshEdgeObjectIdentifier = edgeObjectIdentifier edgeValue,
+          meshEdgeVertexIdentifiers = fmap vertexIdentifier [sourceVertex, targetVertex]
+        }
+
+projectMeshFace :: DelaunayTriangulation () -> FaceId -> MeshFace
+projectMeshFace triangulation faceValue =
+  MeshFace
+    { meshFaceIdentifier = faceIdentifier faceValue,
+      meshFaceObjectIdentifier = faceObjectIdentifier faceValue,
+      meshFaceVertexIdentifiers = fmap vertexIdentifier (faceVertices triangulation faceValue)
+    }
+
+positionForObject ::
+  DelaunayTriangulation () ->
+  ComplexOb DCELComplex ->
+  Either ExhibitError Position
+positionForObject triangulation objectValue =
+  case objectValue of
+    VertexOb vertexValue ->
+      Right (pointPosition (vertexPoint triangulation vertexValue))
+    EdgeOb edgeValue ->
+      case edgeValue of
+        FaceBoundaryCut cut -> Left (ExhibitAbstractCutPositionUnavailable (faceCutFace cut))
+        GeometricCellEdge geometricEdge ->
+          let (sourceVertex, targetVertex) = undirectedEndpoints triangulation geometricEdge
+           in Right
+                ( midpoint
+                    (pointPosition (vertexPoint triangulation sourceVertex))
+                    (pointPosition (vertexPoint triangulation targetVertex))
+                )
+    FaceOb faceValue ->
+      centroid faceValue
+        (fmap (pointPosition . vertexPoint triangulation) (faceVertices triangulation faceValue))
+
+data PositionSum = PositionSum Double Double
+
+instance Semigroup PositionSum where
+  PositionSum leftX leftY <> PositionSum rightX rightY =
+    PositionSum (leftX + rightX) (leftY + rightY)
+
+instance Monoid PositionSum where
+  mempty = PositionSum 0 0
+
+centroid :: FaceId -> [Position] -> Either ExhibitError Position
+centroid faceValue positions =
+  case positions of
+    [] -> Left (ExhibitFaceWithoutVertices faceValue)
+    _ ->
+      let PositionSum summedX summedY =
+            foldMap'
+              (\positionValue -> PositionSum (positionX positionValue) (positionY positionValue))
+              positions
+          positionCount = fromIntegral (length positions)
+       in Right (Position (summedX / positionCount) (summedY / positionCount))
+
+midpoint :: Position -> Position -> Position
+midpoint firstPosition secondPosition =
+  Position
+    { positionX = (positionX firstPosition + positionX secondPosition) / 2,
+      positionY = (positionY firstPosition + positionY secondPosition) / 2
+    }
+
+pointPosition :: Point -> Position
+pointPosition pointValue =
+  Position
+    { positionX = pointX pointValue,
+      positionY = pointY pointValue
+    }
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Identifier.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Identifier.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Identifier.hs
@@ -0,0 +1,89 @@
+module Moonlight.Planar.Exhibit.CategoryObservatory.Identifier
+  ( objectIdentifier,
+    objectKind,
+    objectLabel,
+    morphismKind,
+    vertexIdentifier,
+    edgeIdentifier,
+    faceIdentifier,
+    vertexObjectIdentifier,
+    edgeObjectIdentifier,
+    faceObjectIdentifier,
+  )
+where
+
+import Moonlight.Homology.Pure.Topology.CellCategory
+  ( ComplexMor (..),
+    ComplexOb (..),
+  )
+import Moonlight.Planar.CellComplex (DCELComplex, DCELCellEdge (..), faceCutFace, faceCutComponent)
+import Moonlight.Planar.Internal.Incidence (FaceBoundaryComponent (..))
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( CategoryMorphismKind (..),
+    CategoryObjectKind (..),
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId,
+    VertexId,
+    unDirectedEdgeId,
+    unFaceId,
+    unUndirectedEdgeId,
+    unVertexId,
+  )
+
+objectIdentifier :: ComplexOb DCELComplex -> String
+objectIdentifier objectValue =
+  case objectValue of
+    VertexOb vertexValue -> vertexObjectIdentifier vertexValue
+    EdgeOb edgeValue -> edgeObjectIdentifier edgeValue
+    FaceOb faceValue -> faceObjectIdentifier faceValue
+
+objectKind :: ComplexOb DCELComplex -> CategoryObjectKind
+objectKind objectValue =
+  case objectValue of
+    VertexOb _ -> VertexObject
+    EdgeOb _ -> EdgeObject
+    FaceOb _ -> FaceObject
+
+objectLabel :: ComplexOb DCELComplex -> String
+objectLabel objectValue =
+  case objectValue of
+    VertexOb vertexValue -> "v" <> show (unVertexId vertexValue)
+    EdgeOb edgeValue -> "e" <> edgeIdentitySuffix edgeValue
+    FaceOb faceValue -> "f" <> show (unFaceId faceValue)
+
+morphismKind :: ComplexMor DCELComplex -> CategoryMorphismKind
+morphismKind morphismValue =
+  case morphismValue of
+    IdentityMor _ -> IdentityMorphism
+    FaceToEdge {} -> FaceEdgeMorphism
+    EdgeToVertex _ _ -> EdgeVertexMorphism
+    FaceToVertex {} -> FaceVertexComposite
+
+vertexIdentifier :: VertexId -> String
+vertexIdentifier vertexValue = "vertex-" <> show (unVertexId vertexValue)
+
+edgeIdentifier :: DCELCellEdge -> String
+edgeIdentifier edgeValue = "edge-" <> edgeIdentitySuffix edgeValue
+
+edgeIdentitySuffix :: DCELCellEdge -> String
+edgeIdentitySuffix edgeValue =
+  case edgeValue of
+    GeometricCellEdge edge -> show (unUndirectedEdgeId edge)
+    FaceBoundaryCut cut ->
+      "cut-" <> show (unFaceId (faceCutFace cut)) <> "-" <>
+        (case faceCutComponent cut of
+          BoundaryCycleRoot root -> "cycle-" <> show (unDirectedEdgeId root)
+          IsolatedBoundaryVertex vertex -> "vertex-" <> show (unVertexId vertex))
+
+faceIdentifier :: FaceId -> String
+faceIdentifier faceValue = "face-" <> show (unFaceId faceValue)
+
+vertexObjectIdentifier :: VertexId -> String
+vertexObjectIdentifier vertexValue = "object-" <> vertexIdentifier vertexValue
+
+edgeObjectIdentifier :: DCELCellEdge -> String
+edgeObjectIdentifier edgeValue = "object-" <> edgeIdentifier edgeValue
+
+faceObjectIdentifier :: FaceId -> String
+faceObjectIdentifier faceValue = "object-" <> faceIdentifier faceValue
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Model.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Model.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Model.hs
@@ -0,0 +1,191 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( Exhibit (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    MeshEdge (..),
+    MeshFace (..),
+    Position (..),
+    CategoryProjection (..),
+    CategoryObject (..),
+    CategoryObjectKind (..),
+    CategoryMorphism (..),
+    CategoryMorphismKind (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+    CellInventory (..),
+    MorphismInventory (..),
+    ExhibitError (..),
+    MorphismEndpoint (..),
+    renderExhibitError,
+  )
+where
+
+import Data.Aeson (ToJSON)
+import GHC.Generics (Generic)
+import Moonlight.Planar.CellSet (CellSelectionError)
+import Moonlight.Planar.CellComplex (DCELError)
+import Moonlight.Planar.Internal.HandleDefs (FaceId)
+import Moonlight.Planar.Types (BuildError)
+import Numeric.Natural (Natural)
+
+data Exhibit = Exhibit
+  { schemaVersion :: Int,
+    exhibitTitle :: String,
+    mesh :: MeshProjection,
+    category :: CategoryProjection,
+    nerve :: NerveProjection
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshProjection = MeshProjection
+  { meshVertices :: [MeshVertex],
+    meshEdges :: [MeshEdge],
+    meshFaces :: [MeshFace]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshVertex = MeshVertex
+  { meshVertexIdentifier :: String,
+    meshVertexObjectIdentifier :: String,
+    meshVertexPosition :: Position
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshEdge = MeshEdge
+  { meshEdgeIdentifier :: String,
+    meshEdgeObjectIdentifier :: String,
+    meshEdgeVertexIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data MeshFace = MeshFace
+  { meshFaceIdentifier :: String,
+    meshFaceObjectIdentifier :: String,
+    meshFaceVertexIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data Position = Position
+  { positionX :: Double,
+    positionY :: Double
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryProjection = CategoryProjection
+  { categoryObjects :: [CategoryObject],
+    categoryMorphisms :: [CategoryMorphism]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryObject = CategoryObject
+  { categoryObjectIdentifier :: String,
+    categoryObjectKind :: CategoryObjectKind,
+    categoryObjectLabel :: String,
+    categoryObjectPosition :: Position
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryObjectKind
+  = VertexObject
+  | EdgeObject
+  | FaceObject
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryMorphism = CategoryMorphism
+  { categoryMorphismIdentifier :: String,
+    categoryMorphismKind :: CategoryMorphismKind,
+    categoryMorphismSourceObject :: String,
+    categoryMorphismTargetObject :: String
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CategoryMorphismKind
+  = IdentityMorphism
+  | FaceEdgeMorphism
+  | EdgeVertexMorphism
+  | FaceVertexComposite
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveProjection = NerveProjection
+  { nerveTwoSimplices :: [NerveTwoSimplex],
+    nerveSimplexCounts :: [NerveSimplexCount]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveSimplexCount = NerveSimplexCount
+  { nerveDimension :: Natural,
+    normalizedSimplexCount :: Int,
+    unnormalizedSimplexCount :: Int
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data NerveTwoSimplex = NerveTwoSimplex
+  { nerveSimplexIdentifier :: String,
+    nerveSimplexObjectIdentifiers :: [String]
+  }
+  deriving stock (Generic)
+  deriving anyclass (ToJSON)
+
+data CellInventory = CellInventory
+  { cellVertexCount :: Int,
+    cellEdgeCount :: Int,
+    cellFaceCount :: Int
+  }
+  deriving stock (Eq, Show)
+
+data MorphismInventory = MorphismInventory
+  { identityMorphismCount :: Int,
+    faceEdgeMorphismCount :: Int,
+    edgeVertexMorphismCount :: Int,
+    faceVertexMorphismCount :: Int
+  }
+  deriving stock (Eq, Show)
+
+instance Semigroup MorphismInventory where
+  MorphismInventory leftIdentities leftFaceEdges leftEdgeVertices leftFaceVertices
+    <> MorphismInventory rightIdentities rightFaceEdges rightEdgeVertices rightFaceVertices =
+      MorphismInventory
+        (leftIdentities + rightIdentities)
+        (leftFaceEdges + rightFaceEdges)
+        (leftEdgeVertices + rightEdgeVertices)
+        (leftFaceVertices + rightFaceVertices)
+
+instance Monoid MorphismInventory where
+  mempty = MorphismInventory 0 0 0 0
+
+data ExhibitError
+  = ExhibitTriangulationBuildFailed BuildError
+  | ExhibitCellSelectionFailed CellSelectionError
+  | ExhibitCellComplexFailed DCELError
+  | ExhibitAbstractCutPositionUnavailable FaceId
+  | ExhibitMorphismEndpointUnavailable MorphismEndpoint Int
+  | ExhibitFaceWithoutVertices FaceId
+  | ExhibitMalformedTwoSimplex Int
+  | ExhibitInventoryMismatch CellInventory CellInventory
+  | ExhibitCategoryEnumerationMismatch MorphismInventory MorphismInventory
+  | ExhibitNerveCountMismatch Int Int
+  deriving stock (Show)
+
+data MorphismEndpoint
+  = MorphismSource
+  | MorphismTarget
+  deriving stock (Show)
+
+renderExhibitError :: ExhibitError -> String
+renderExhibitError = show
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Nerve.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Nerve.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Nerve.hs
@@ -0,0 +1,84 @@
+module Moonlight.Planar.Exhibit.CategoryObservatory.Nerve
+  ( projectNerve,
+    validateNerveCount,
+  )
+where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Category (chainVertices)
+import Moonlight.Category.Simplicial
+  ( GeneratedSSet,
+    NerveSimplex,
+    TruncatedNormalizedSSet,
+    generatedSimplicesAtDimension,
+    nerveSimplexChain,
+    normalizedNerve,
+    simplicesAtDimension,
+    unnormalizedNerve,
+  )
+import Moonlight.Homology.Pure.Topology.CellCategory (ComplexCat)
+import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
+import Moonlight.Planar.CellComplex (DCELComplex)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Category (incidenceFlagCount)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Identifier (objectIdentifier)
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( ExhibitError (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+  )
+import Numeric.Natural (Natural)
+
+projectNerve :: ComplexCat DCELComplex -> Either ExhibitError NerveProjection
+projectNerve categoryValue =
+  let normalizedNerveValue = normalizedNerve categoryValue nerveObservationBound
+      unnormalizedNerveValue = unnormalizedNerve categoryValue nerveObservationBound
+   in NerveProjection
+        <$> traverse
+          projectTwoSimplex
+          (zip [0 ..] (simplicesAtDimension normalizedNerveValue nerveObservationBound))
+        <*> pure
+          ( fmap
+              (projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue)
+              nerveObservationDimensions
+          )
+
+validateNerveCount :: DCELComplex -> NerveProjection -> Either ExhibitError ()
+validateNerveCount complexValue nerveProjection =
+  let expectedCount = sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
+      observedCount = length (nerveTwoSimplices nerveProjection)
+   in if observedCount == expectedCount
+        then Right ()
+        else Left (ExhibitNerveCountMismatch expectedCount observedCount)
+
+projectNerveSimplexCount ::
+  TruncatedNormalizedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
+  GeneratedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
+  Natural ->
+  NerveSimplexCount
+projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue dimensionValue =
+  NerveSimplexCount
+    { nerveDimension = dimensionValue,
+      normalizedSimplexCount = length (simplicesAtDimension normalizedNerveValue dimensionValue),
+      unnormalizedSimplexCount = length (generatedSimplicesAtDimension unnormalizedNerveValue dimensionValue)
+    }
+
+projectTwoSimplex ::
+  (Int, NerveSimplex (ComplexCat DCELComplex)) ->
+  Either ExhibitError NerveTwoSimplex
+projectTwoSimplex (simplexIndex, simplexValue) =
+  case chainVertices (nerveSimplexChain simplexValue) of
+    faceObject :| [edgeObject, vertexObject] ->
+      Right
+        NerveTwoSimplex
+          { nerveSimplexIdentifier = "simplex-" <> show simplexIndex,
+            nerveSimplexObjectIdentifiers =
+              fmap objectIdentifier [faceObject, edgeObject, vertexObject]
+          }
+    _ -> Left (ExhibitMalformedTwoSimplex simplexIndex)
+
+nerveObservationBound :: Natural
+nerveObservationBound = 2
+
+nerveObservationDimensions :: [Natural]
+nerveObservationDimensions = [0, 1, 2]
diff --git a/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Picture.hs b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Picture.hs
new file mode 100644
--- /dev/null
+++ b/docs/category-observatory/Moonlight/Planar/Exhibit/CategoryObservatory/Picture.hs
@@ -0,0 +1,663 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Pure, compact SVG projection of the checked category-observatory exhibit.
+module Moonlight.Planar.Exhibit.CategoryObservatory.Picture
+  ( PictureObstruction (..),
+    categoryObservatoryPictureFileName,
+    renderCategoryObservatoryPicture,
+    renderPictureObstruction,
+  )
+where
+
+import Data.Map.Strict qualified as Map
+import Data.Set qualified as Set
+import Moonlight.Planar.Exhibit.CategoryObservatory.Model
+  ( CategoryObject (..),
+    CategoryObjectKind (..),
+    CategoryProjection (..),
+    Exhibit (..),
+    MeshEdge (..),
+    MeshFace (..),
+    MeshProjection (..),
+    MeshVertex (..),
+    NerveProjection (..),
+    NerveSimplexCount (..),
+    NerveTwoSimplex (..),
+    Position (..),
+  )
+import Numeric (showFFloat)
+
+data PictureObstruction
+  = PictureHasNoMeshVertices
+  | PictureHasNoNerveTwoSimplex
+  | PictureMissingMeshVertex !String
+  | PictureMissingCategoryObject !String
+  | PictureMalformedMeshEdge !String ![String]
+  | PictureMalformedMeshFace !String ![String]
+  | PictureMalformedNerveTwoSimplex !String ![String]
+  | PictureMalformedFlagKinds ![CategoryObjectKind]
+  deriving stock (Eq, Show)
+
+data PictureBounds = PictureBounds
+  { minimumX :: !Double,
+    minimumY :: !Double,
+    maximumX :: !Double,
+    maximumY :: !Double
+  }
+
+data PictureFrame = PictureFrame
+  { frameLeft :: !Double,
+    frameTop :: !Double,
+    frameWidth :: !Double,
+    frameHeight :: !Double
+  }
+
+data ResolvedMeshEdge = ResolvedMeshEdge
+  { resolvedEdge :: !MeshEdge,
+    resolvedEdgeSource :: !Position,
+    resolvedEdgeTarget :: !Position
+  }
+
+data ResolvedMeshFace = ResolvedMeshFace
+  { resolvedFace :: !MeshFace,
+    resolvedFaceVertices :: ![Position]
+  }
+
+data ResolvedNerveSimplex = ResolvedNerveSimplex
+  { resolvedSimplex :: !NerveTwoSimplex,
+    resolvedSimplexVertices :: ![Position]
+  }
+
+data FlagSelection = FlagSelection
+  { selectedSimplexIdentifier :: !String,
+    selectedFace :: !CategoryObject,
+    selectedEdge :: !CategoryObject,
+    selectedVertex :: !CategoryObject,
+    selectedObjectIdentifiers :: !(Set.Set String)
+  }
+
+data PictureContext = PictureContext
+  { contextExhibit :: !Exhibit,
+    contextBounds :: !PictureBounds,
+    contextEdges :: ![ResolvedMeshEdge],
+    contextFaces :: ![ResolvedMeshFace],
+    contextNerveSimplices :: ![ResolvedNerveSimplex],
+    contextFlag :: !FlagSelection
+  }
+
+categoryObservatoryPictureFileName :: FilePath
+categoryObservatoryPictureFileName =
+  "moonlight-triangulation-category-observatory.svg"
+
+renderCategoryObservatoryPicture :: Exhibit -> Either PictureObstruction String
+renderCategoryObservatoryPicture exhibit =
+  renderPicture <$> preparePicture exhibit
+
+renderPictureObstruction :: PictureObstruction -> String
+renderPictureObstruction = \case
+  PictureHasNoMeshVertices ->
+    "category-observatory picture requires at least one mesh vertex"
+  PictureHasNoNerveTwoSimplex ->
+    "category-observatory picture requires at least one admitted two-simplex"
+  PictureMissingMeshVertex identifier ->
+    "category-observatory picture cannot resolve mesh vertex " <> show identifier
+  PictureMissingCategoryObject identifier ->
+    "category-observatory picture cannot resolve category object " <> show identifier
+  PictureMalformedMeshEdge identifier vertices ->
+    "category-observatory mesh edge "
+      <> show identifier
+      <> " does not name exactly two vertices: "
+      <> show vertices
+  PictureMalformedMeshFace identifier vertices ->
+    "category-observatory mesh face "
+      <> show identifier
+      <> " does not name exactly three vertices: "
+      <> show vertices
+  PictureMalformedNerveTwoSimplex identifier objects ->
+    "category-observatory nerve simplex "
+      <> show identifier
+      <> " does not name one face, edge, and vertex: "
+      <> show objects
+  PictureMalformedFlagKinds kinds ->
+    "category-observatory selected flag has incompatible object kinds: " <> show kinds
+
+preparePicture :: Exhibit -> Either PictureObstruction PictureContext
+preparePicture exhibit = do
+  let meshProjection = mesh exhibit
+      categoryProjection = category exhibit
+      nerveProjection = nerve exhibit
+      meshVerticesValue = meshVertices meshProjection
+      vertexPositions =
+        Map.fromList
+          ( fmap
+              (\vertex -> (meshVertexIdentifier vertex, meshVertexPosition vertex))
+              meshVerticesValue
+          )
+      categoryObjectsByIdentifier =
+        Map.fromList
+          ( fmap
+              (\objectValue -> (categoryObjectIdentifier objectValue, objectValue))
+              (categoryObjects categoryProjection)
+          )
+  pictureBounds <- boundsFromPositions (meshVertexPosition <$> meshVerticesValue)
+  resolvedEdges <- traverse (resolveMeshEdge vertexPositions) (meshEdges meshProjection)
+  resolvedFaces <- traverse (resolveMeshFace vertexPositions) (meshFaces meshProjection)
+  resolvedSimplices <-
+    traverse
+      (resolveNerveSimplex categoryObjectsByIdentifier)
+      (nerveTwoSimplices nerveProjection)
+  flagSelection <- selectFlag categoryObjectsByIdentifier (nerveTwoSimplices nerveProjection)
+  pure
+    PictureContext
+      { contextExhibit = exhibit,
+        contextBounds = pictureBounds,
+        contextEdges = resolvedEdges,
+        contextFaces = resolvedFaces,
+        contextNerveSimplices = resolvedSimplices,
+        contextFlag = flagSelection
+      }
+
+boundsFromPositions :: [Position] -> Either PictureObstruction PictureBounds
+boundsFromPositions positions =
+  case positions of
+    [] -> Left PictureHasNoMeshVertices
+    firstPosition : remainingPositions ->
+      Right
+        ( foldl'
+            extendBounds
+            ( PictureBounds
+                (positionX firstPosition)
+                (positionY firstPosition)
+                (positionX firstPosition)
+                (positionY firstPosition)
+            )
+            remainingPositions
+        )
+
+extendBounds :: PictureBounds -> Position -> PictureBounds
+extendBounds bounds positionValue =
+  PictureBounds
+    { minimumX = min (minimumX bounds) (positionX positionValue),
+      minimumY = min (minimumY bounds) (positionY positionValue),
+      maximumX = max (maximumX bounds) (positionX positionValue),
+      maximumY = max (maximumY bounds) (positionY positionValue)
+    }
+
+resolveMeshEdge ::
+  Map.Map String Position ->
+  MeshEdge ->
+  Either PictureObstruction ResolvedMeshEdge
+resolveMeshEdge vertexPositions edgeValue =
+  case meshEdgeVertexIdentifiers edgeValue of
+    [sourceIdentifier, targetIdentifier] ->
+      ResolvedMeshEdge edgeValue
+        <$> lookupMeshVertex vertexPositions sourceIdentifier
+        <*> lookupMeshVertex vertexPositions targetIdentifier
+    identifiers ->
+      Left (PictureMalformedMeshEdge (meshEdgeIdentifier edgeValue) identifiers)
+
+resolveMeshFace ::
+  Map.Map String Position ->
+  MeshFace ->
+  Either PictureObstruction ResolvedMeshFace
+resolveMeshFace vertexPositions faceValue =
+  case meshFaceVertexIdentifiers faceValue of
+    [firstIdentifier, secondIdentifier, thirdIdentifier] ->
+      ResolvedMeshFace faceValue
+        <$> traverse
+          (lookupMeshVertex vertexPositions)
+          [firstIdentifier, secondIdentifier, thirdIdentifier]
+    identifiers ->
+      Left (PictureMalformedMeshFace (meshFaceIdentifier faceValue) identifiers)
+
+lookupMeshVertex ::
+  Map.Map String Position ->
+  String ->
+  Either PictureObstruction Position
+lookupMeshVertex vertexPositions identifier =
+  maybe
+    (Left (PictureMissingMeshVertex identifier))
+    Right
+    (Map.lookup identifier vertexPositions)
+
+resolveNerveSimplex ::
+  Map.Map String CategoryObject ->
+  NerveTwoSimplex ->
+  Either PictureObstruction ResolvedNerveSimplex
+resolveNerveSimplex categoryObjectsByIdentifier simplexValue =
+  case nerveSimplexObjectIdentifiers simplexValue of
+    [faceIdentifier, edgeIdentifier, vertexIdentifier] ->
+      ResolvedNerveSimplex simplexValue
+        . fmap categoryObjectPosition
+        <$> traverse
+          (lookupCategoryObject categoryObjectsByIdentifier)
+          [faceIdentifier, edgeIdentifier, vertexIdentifier]
+    identifiers ->
+      Left
+        ( PictureMalformedNerveTwoSimplex
+            (nerveSimplexIdentifier simplexValue)
+            identifiers
+        )
+
+selectFlag ::
+  Map.Map String CategoryObject ->
+  [NerveTwoSimplex] ->
+  Either PictureObstruction FlagSelection
+selectFlag categoryObjectsByIdentifier simplices =
+  case simplices of
+    [] -> Left PictureHasNoNerveTwoSimplex
+    simplexValue : _ ->
+      case nerveSimplexObjectIdentifiers simplexValue of
+        identifiers@[faceIdentifier, edgeIdentifier, vertexIdentifier] -> do
+          faceObject <- lookupCategoryObject categoryObjectsByIdentifier faceIdentifier
+          edgeObject <- lookupCategoryObject categoryObjectsByIdentifier edgeIdentifier
+          vertexObject <- lookupCategoryObject categoryObjectsByIdentifier vertexIdentifier
+          let kinds = categoryObjectKind <$> [faceObject, edgeObject, vertexObject]
+          case kinds of
+            [FaceObject, EdgeObject, VertexObject] ->
+              Right
+                FlagSelection
+                  { selectedSimplexIdentifier = nerveSimplexIdentifier simplexValue,
+                    selectedFace = faceObject,
+                    selectedEdge = edgeObject,
+                    selectedVertex = vertexObject,
+                    selectedObjectIdentifiers = Set.fromList identifiers
+                  }
+            _ -> Left (PictureMalformedFlagKinds kinds)
+        identifiers ->
+          Left
+            ( PictureMalformedNerveTwoSimplex
+                (nerveSimplexIdentifier simplexValue)
+                identifiers
+            )
+
+lookupCategoryObject ::
+  Map.Map String CategoryObject ->
+  String ->
+  Either PictureObstruction CategoryObject
+lookupCategoryObject categoryObjectsByIdentifier identifier =
+  maybe
+    (Left (PictureMissingCategoryObject identifier))
+    Right
+    (Map.lookup identifier categoryObjectsByIdentifier)
+
+renderPicture :: PictureContext -> String
+renderPicture context =
+  unlines
+    ( renderDocumentOpening
+        <> renderHeader context
+        <> renderPanels context
+        <> renderNerveProfile context
+        <> ["</svg>"]
+    )
+
+renderDocumentOpening :: [String]
+renderDocumentOpening =
+  [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
+    "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 920\" role=\"img\" aria-labelledby=\"title description\">",
+    "<title id=\"title\">Moonlight category observatory</title>",
+    "<desc id=\"description\">One checked Delaunay section shown as geometry, a local face-edge-vertex incidence flag, and the full path-sensitive flag nerve.</desc>",
+    "<defs>",
+    "  <linearGradient id=\"background\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop stop-color=\"#10262a\"/><stop offset=\"0.48\" stop-color=\"#071217\"/><stop offset=\"1\" stop-color=\"#120f20\"/></linearGradient>",
+    "  <radialGradient id=\"cyan-glow\"><stop stop-color=\"#72d6c2\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#72d6c2\" stop-opacity=\"0\"/></radialGradient>",
+    "  <radialGradient id=\"violet-glow\"><stop stop-color=\"#bc8cff\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#bc8cff\" stop-opacity=\"0\"/></radialGradient>",
+    "  <pattern id=\"grid\" width=\"28\" height=\"28\" patternUnits=\"userSpaceOnUse\"><path d=\"M28 0H0V28\" fill=\"none\" stroke=\"#9bc7bd\" stroke-opacity=\"0.07\" stroke-width=\"1\"/></pattern>",
+    "  <marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0L10 5L0 10Z\" fill=\"#72d6c2\"/></marker>",
+    "  <filter id=\"soft-shadow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feDropShadow dx=\"0\" dy=\"8\" stdDeviation=\"12\" flood-color=\"#000\" flood-opacity=\"0.28\"/></filter>",
+    "  <style><![CDATA[",
+    "    .sans{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,sans-serif}",
+    "    .serif{font-family:Georgia,'Times New Roman',serif}",
+    "    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}",
+    "    .panel{fill:#0a181d;fill-opacity:.93;stroke:#8bbeb2;stroke-opacity:.22;stroke-width:1.2}",
+    "    .kicker{fill:#72d6c2;font-size:12px;font-weight:700;letter-spacing:2.8px}",
+    "    .title{fill:#f1eee7;font-size:48px;font-weight:400;letter-spacing:-1.4px}",
+    "    .subtitle{fill:#9ab0aa;font-size:16px}",
+    "    .panel-number{fill:#72d6c2;font-size:11px;font-weight:700;letter-spacing:1.6px}",
+    "    .panel-title{fill:#f1eee7;font-size:25px}",
+    "    .callout-title{fill:#f1eee7;font-size:21px}",
+    "    .panel-note{fill:#8ca49f;font-size:12px}",
+    "    .metric-value{fill:#f1eee7;font-size:18px;font-weight:700}",
+    "    .metric-label{fill:#8ca49f;font-size:9px;font-weight:700;letter-spacing:1.2px}",
+    "    .mesh-face{fill:#72d6c2;fill-opacity:.035;stroke:none}",
+    "    .mesh-face.selected{fill:#ffad66;fill-opacity:.28}",
+    "    .mesh-edge{stroke:#72d6c2;stroke-opacity:.58;stroke-width:1.7}",
+    "    .mesh-edge.selected{stroke:#ffad66;stroke-opacity:1;stroke-width:5}",
+    "    .mesh-vertex{fill:#071217;stroke:#f2e37e;stroke-width:2.2}",
+    "    .mesh-vertex.selected{fill:#f2e37e;stroke:#fff7b0;stroke-width:4}",
+    "    .nerve-simplex{fill:#bc8cff;fill-opacity:.065;stroke:#bc8cff;stroke-opacity:.42;stroke-width:1.1}",
+    "    .nerve-simplex.selected{fill:#ffad66;fill-opacity:.48;stroke:#ffd1a8;stroke-opacity:1;stroke-width:3}",
+    "    .object-face{fill:#ffad66}.object-edge{fill:#72d6c2}.object-vertex{fill:#f2e37e}",
+    "    .node-label{fill:#f1eee7;font-size:23px;font-weight:700}.node-kind{fill:#8ca49f;font-size:10px;font-weight:700;letter-spacing:1.4px}",
+    "    .profile-label{fill:#8ca49f;font-size:11px;font-weight:700;letter-spacing:1px}.profile-value{fill:#f1eee7;font-size:12px;font-weight:700}",
+    "  ]]></style>",
+    "</defs>",
+    "<rect width=\"1440\" height=\"920\" fill=\"url(#background)\"/>",
+    "<circle cx=\"180\" cy=\"110\" r=\"290\" fill=\"url(#cyan-glow)\"/>",
+    "<circle cx=\"1290\" cy=\"520\" r=\"360\" fill=\"url(#violet-glow)\"/>"
+  ]
+
+renderHeader :: PictureContext -> [String]
+renderHeader context =
+  [ svgText "mono kicker" 56 38 "start" "MOONLIGHT / FINITE DESCENT",
+    svgText "serif title" 56 94 "start" "One section, three faithful projections",
+    svgText
+      "sans subtitle"
+      57
+      127
+      "start"
+      "Delaunay geometry → incidence category → path-sensitive flag nerve"
+  ]
+    <> concat
+      ( zipWith
+          renderMetric
+          [56, 212, 368, 524, 680, 856]
+          (pictureMetrics context)
+      )
+    <> [ "<path d=\"M1040 163H1378\" stroke=\"#72d6c2\" stroke-opacity=\".24\"/>",
+         svgText "mono panel-note" 1378 168 "end" "ONE HIGHLIGHTED FLAG GLUES ALL THREE PANELS"
+       ]
+
+data PictureMetric = PictureMetric
+  { metricValue :: !Int,
+    metricLabel :: !String
+  }
+
+pictureMetrics :: PictureContext -> [PictureMetric]
+pictureMetrics context =
+  let exhibit = contextExhibit context
+      meshProjection = mesh exhibit
+      categoryProjection = category exhibit
+      nerveProjection = nerve exhibit
+   in [ PictureMetric (length (meshVertices meshProjection)) "VERTICES",
+        PictureMetric (length (meshEdges meshProjection)) "EDGES",
+        PictureMetric (length (meshFaces meshProjection)) "FACES",
+        PictureMetric (length (categoryObjects categoryProjection)) "OBJECTS",
+        PictureMetric (length (categoryMorphisms categoryProjection)) "MORPHISMS",
+        PictureMetric (length (nerveTwoSimplices nerveProjection)) "FLAGS"
+      ]
+
+renderMetric :: Double -> PictureMetric -> [String]
+renderMetric x metric =
+  [ "<g transform=\"translate(" <> coordinate x <> " 151)\">",
+    "  <rect width=\"136\" height=\"48\" rx=\"13\" fill=\"#0b1b20\" stroke=\"#8bbeb2\" stroke-opacity=\".17\"/>",
+    "  " <> svgText "sans metric-value" 16 22 "start" (show (metricValue metric)),
+    "  " <> svgText "mono metric-label" 16 38 "start" (metricLabel metric),
+    "</g>"
+  ]
+
+renderPanels :: PictureContext -> [String]
+renderPanels context =
+  renderPanelBackgrounds
+    <> renderGeometryPanel context
+    <> renderCategoryPanel (contextFlag context)
+    <> renderNervePanel context
+
+renderPanelBackgrounds :: [String]
+renderPanelBackgrounds =
+  concatMap
+    (\x -> ["<rect class=\"panel\" x=\"" <> coordinate x <> "\" y=\"222\" width=\"410\" height=\"470\" rx=\"22\" filter=\"url(#soft-shadow)\"/>"])
+    [50, 515, 980]
+
+renderGeometryPanel :: PictureContext -> [String]
+renderGeometryPanel context =
+  renderPanelHeader 50 "01" "Geometric section" "the admitted Delaunay cells"
+    <> ["<rect x=\"70\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
+    <> fmap (renderMeshFace context geometryFrame) (contextFaces context)
+    <> fmap (renderMeshEdge context geometryFrame) (contextEdges context)
+    <> fmap (renderMeshVertex context geometryFrame) (meshVertices (mesh (contextExhibit context)))
+    <> renderGeometrySelectionLabels context
+
+geometryFrame :: PictureFrame
+geometryFrame = PictureFrame 83 341 344 290
+
+renderMeshFace :: PictureContext -> PictureFrame -> ResolvedMeshFace -> String
+renderMeshFace context frame faceValue =
+  let selected =
+        Set.member
+          (meshFaceObjectIdentifier (resolvedFace faceValue))
+          (selectedObjectIdentifiers (contextFlag context))
+   in "<polygon class=\"mesh-face"
+        <> selectedClass selected
+        <> "\" points=\""
+        <> renderPointList (contextBounds context) frame (resolvedFaceVertices faceValue)
+        <> "\"/>"
+
+renderMeshEdge :: PictureContext -> PictureFrame -> ResolvedMeshEdge -> String
+renderMeshEdge context frame edgeValue =
+  let selected =
+        Set.member
+          (meshEdgeObjectIdentifier (resolvedEdge edgeValue))
+          (selectedObjectIdentifiers (contextFlag context))
+      (sourceX, sourceY) = projectPosition (contextBounds context) frame (resolvedEdgeSource edgeValue)
+      (targetX, targetY) = projectPosition (contextBounds context) frame (resolvedEdgeTarget edgeValue)
+   in "<line class=\"mesh-edge"
+        <> selectedClass selected
+        <> "\" x1=\""
+        <> coordinate sourceX
+        <> "\" y1=\""
+        <> coordinate sourceY
+        <> "\" x2=\""
+        <> coordinate targetX
+        <> "\" y2=\""
+        <> coordinate targetY
+        <> "\"/>"
+
+renderMeshVertex :: PictureContext -> PictureFrame -> MeshVertex -> String
+renderMeshVertex context frame vertexValue =
+  let selected =
+        Set.member
+          (meshVertexObjectIdentifier vertexValue)
+          (selectedObjectIdentifiers (contextFlag context))
+      (x, y) = projectPosition (contextBounds context) frame (meshVertexPosition vertexValue)
+   in "<circle class=\"mesh-vertex"
+        <> selectedClass selected
+        <> "\" cx=\""
+        <> coordinate x
+        <> "\" cy=\""
+        <> coordinate y
+        <> "\" r=\""
+        <> if selected then "7\"/>" else "4.5\"/>"
+
+renderGeometrySelectionLabels :: PictureContext -> [String]
+renderGeometrySelectionLabels context =
+  let flag = contextFlag context
+      labelledObjects = [selectedFace flag, selectedEdge flag, selectedVertex flag]
+   in fmap
+        (\objectValue ->
+           let (x, y) = projectPosition (contextBounds context) geometryFrame (categoryObjectPosition objectValue)
+            in svgText
+                 "mono profile-value"
+                 (x + 10)
+                 (y - 10)
+                 "start"
+                 (categoryObjectLabel objectValue)
+        )
+        labelledObjects
+
+renderCategoryPanel :: FlagSelection -> [String]
+renderCategoryPanel flag =
+  renderPanelHeader 515 "02" "Incidence category" "one local section; no arrow thicket"
+    <> [ svgText "mono panel-note" 720 334 "middle" "SELECTED NONDEGENERATE CHAIN",
+         "<path d=\"M621 463H686\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
+         "<path d=\"M754 463H817\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
+         "<path d=\"M609 421C665 365 794 365 850 421\" fill=\"none\" stroke=\"#bc8cff\" stroke-opacity=\".68\" stroke-width=\"1.6\" stroke-dasharray=\"6 7\" marker-end=\"url(#arrow)\"/>",
+         "<rect x=\"556\" y=\"430\" width=\"66\" height=\"66\" rx=\"14\" fill=\"#ffad66\" fill-opacity=\".18\" stroke=\"#ffad66\" stroke-width=\"2\"/>",
+         "<rect x=\"696\" y=\"439\" width=\"48\" height=\"48\" transform=\"rotate(45 720 463)\" fill=\"#72d6c2\" fill-opacity=\".16\" stroke=\"#72d6c2\" stroke-width=\"2\"/>",
+         "<circle cx=\"850\" cy=\"463\" r=\"31\" fill=\"#f2e37e\" fill-opacity=\".14\" stroke=\"#f2e37e\" stroke-width=\"2\"/>",
+         svgText "mono node-kind" 589 407 "middle" "FACE OBJECT",
+         svgText "sans node-label" 589 471 "middle" (categoryObjectLabel (selectedFace flag)),
+         svgText "mono node-kind" 720 407 "middle" "EDGE OBJECT",
+         svgText "sans node-label" 720 471 "middle" (categoryObjectLabel (selectedEdge flag)),
+         svgText "mono node-kind" 850 407 "middle" "VERTEX OBJECT",
+         svgText "sans node-label" 850 471 "middle" (categoryObjectLabel (selectedVertex flag)),
+         svgText "mono panel-note" 720 535 "middle" "face → edge → vertex",
+         "<rect x=\"552\" y=\"570\" width=\"336\" height=\"78\" rx=\"14\" fill=\"#071217\" stroke=\"#bc8cff\" stroke-opacity=\".28\"/>",
+         svgText "serif callout-title" 720 602 "middle" "The intermediate edge survives.",
+         svgText "sans panel-note" 720 628 "middle" "Same endpoints, different paths, distinct flags."
+       ]
+
+renderNervePanel :: PictureContext -> [String]
+renderNervePanel context =
+  renderPanelHeader 980 "03" "Flag nerve" "all 66 admitted two-simplices"
+    <> ["<rect x=\"1000\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
+    <> fmap (renderNerveSimplex context nerveFrame) (contextNerveSimplices context)
+    <> fmap (renderNerveObject context nerveFrame) (categoryObjects (category (contextExhibit context)))
+    <> renderNerveSelectionLabel context
+
+nerveFrame :: PictureFrame
+nerveFrame = PictureFrame 1013 341 344 290
+
+renderNerveSimplex :: PictureContext -> PictureFrame -> ResolvedNerveSimplex -> String
+renderNerveSimplex context frame simplexValue =
+  let selected =
+        nerveSimplexIdentifier (resolvedSimplex simplexValue)
+          == selectedSimplexIdentifier (contextFlag context)
+   in "<polygon class=\"nerve-simplex"
+        <> selectedClass selected
+        <> "\" points=\""
+        <> renderPointList (contextBounds context) frame (resolvedSimplexVertices simplexValue)
+        <> "\"/>"
+
+renderNerveObject :: PictureContext -> PictureFrame -> CategoryObject -> String
+renderNerveObject context frame objectValue =
+  let selected =
+        Set.member
+          (categoryObjectIdentifier objectValue)
+          (selectedObjectIdentifiers (contextFlag context))
+      (x, y) = projectPosition (contextBounds context) frame (categoryObjectPosition objectValue)
+      radius = if selected then 6 else 3.4
+   in "<circle class=\""
+        <> objectKindClass (categoryObjectKind objectValue)
+        <> "\" cx=\""
+        <> coordinate x
+        <> "\" cy=\""
+        <> coordinate y
+        <> "\" r=\""
+        <> coordinate radius
+        <> "\" opacity=\""
+        <> if selected then "1\" stroke=\"#fff\" stroke-width=\"2\"/>" else ".78\"/>"
+
+renderNerveSelectionLabel :: PictureContext -> [String]
+renderNerveSelectionLabel context =
+  let flag = contextFlag context
+      faceObject = selectedFace flag
+      (x, y) = projectPosition (contextBounds context) nerveFrame (categoryObjectPosition faceObject)
+   in [ svgText "mono profile-value" (x + 12) (y - 12) "start" "selected flag",
+        svgText "mono panel-note" 1352 642 "end" (selectedSimplexIdentifier flag)
+      ]
+
+renderPanelHeader :: Double -> String -> String -> String -> [String]
+renderPanelHeader x number titleText note =
+  [ svgText "mono panel-number" (x + 22) 254 "start" number,
+    svgText "serif panel-title" (x + 22) 282 "start" titleText,
+    svgText "sans panel-note" (x + 22) 304 "start" note,
+    "<path d=\"M"
+      <> coordinate (x + 20)
+      <> " 314H"
+      <> coordinate (x + 390)
+      <> "\" stroke=\"#8bbeb2\" stroke-opacity=\".14\"/>"
+  ]
+
+renderNerveProfile :: PictureContext -> [String]
+renderNerveProfile context =
+  let counts = nerveSimplexCounts (nerve (contextExhibit context))
+      maximumCount =
+        foldl'
+          (\current count -> max current (unnormalizedSimplexCount count))
+          1
+          counts
+   in [ "<rect x=\"50\" y=\"720\" width=\"1340\" height=\"156\" rx=\"22\" fill=\"#09171c\" stroke=\"#8bbeb2\" stroke-opacity=\".20\"/>",
+        svgText "mono kicker" 76 752 "start" "NORMALIZATION PROFILE",
+        svgText "serif panel-title" 76 789 "start" "Identity insertions are cost, not geometry.",
+        svgText "sans panel-note" 76 817 "start" "Identity insertions swell dimension two to 385.",
+        "<rect x=\"76\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#72d6c2\"/>",
+        svgText "mono profile-label" 96 845 "start" "NORMALIZED",
+        "<rect x=\"196\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#bc8cff\"/>",
+        svgText "mono profile-label" 216 845 "start" "UNNORMALIZED"
+      ]
+        <> concat
+          ( zipWith
+              (renderProfileRow maximumCount)
+              [770, 814, 858]
+              counts
+          )
+
+renderProfileRow :: Int -> Double -> NerveSimplexCount -> [String]
+renderProfileRow maximumCount y count =
+  let normalizedWidth = profileWidth maximumCount (normalizedSimplexCount count)
+      unnormalizedWidth = profileWidth maximumCount (unnormalizedSimplexCount count)
+   in [ svgText "mono profile-label" 590 (y + 5) "end" ("DIM " <> show (nerveDimension count)),
+        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"230\" height=\"11\" rx=\"5.5\" fill=\"#13272d\"/>",
+        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate normalizedWidth <> "\" height=\"11\" rx=\"5.5\" fill=\"#72d6c2\"/>",
+        svgText "mono profile-value" 858 (y + 3) "start" (show (normalizedSimplexCount count)),
+        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"310\" height=\"11\" rx=\"5.5\" fill=\"#1d1830\"/>",
+        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate (unnormalizedWidth * 310 / 230) <> "\" height=\"11\" rx=\"5.5\" fill=\"#bc8cff\"/>",
+        svgText "mono profile-value" 1288 (y + 3) "start" (show (unnormalizedSimplexCount count))
+      ]
+
+profileWidth :: Int -> Int -> Double
+profileWidth maximumCount count =
+  230 * fromIntegral count / fromIntegral maximumCount
+
+projectPosition :: PictureBounds -> PictureFrame -> Position -> (Double, Double)
+projectPosition bounds frame positionValue =
+  ( frameLeft frame
+      + frameWidth frame
+        * normalizedCoordinate (positionX positionValue) (minimumX bounds) (maximumX bounds),
+    frameTop frame
+      + frameHeight frame
+        * normalizedCoordinate (positionY positionValue) (minimumY bounds) (maximumY bounds)
+  )
+
+normalizedCoordinate :: Double -> Double -> Double -> Double
+normalizedCoordinate value minimumValue maximumValue =
+  if maximumValue == minimumValue
+    then 0.5
+    else (value - minimumValue) / (maximumValue - minimumValue)
+
+renderPointList :: PictureBounds -> PictureFrame -> [Position] -> String
+renderPointList bounds frame =
+  unwords
+    . fmap
+      (\positionValue ->
+         let (x, y) = projectPosition bounds frame positionValue
+          in coordinate x <> "," <> coordinate y
+      )
+
+selectedClass :: Bool -> String
+selectedClass selected = if selected then " selected" else ""
+
+objectKindClass :: CategoryObjectKind -> String
+objectKindClass = \case
+  FaceObject -> "object-face"
+  EdgeObject -> "object-edge"
+  VertexObject -> "object-vertex"
+
+svgText :: String -> Double -> Double -> String -> String -> String
+svgText classes x y anchor contents =
+  "<text class=\""
+    <> classes
+    <> "\" x=\""
+    <> coordinate x
+    <> "\" y=\""
+    <> coordinate y
+    <> "\" text-anchor=\""
+    <> anchor
+    <> "\">"
+    <> escapeXml contents
+    <> "</text>"
+
+coordinate :: Double -> String
+coordinate value = showFFloat (Just 1) value ""
+
+escapeXml :: String -> String
+escapeXml = concatMap escapeCharacter
+
+escapeCharacter :: Char -> String
+escapeCharacter = \case
+  '&' -> "&amp;"
+  '<' -> "&lt;"
+  '>' -> "&gt;"
+  '\"' -> "&quot;"
+  '\'' -> "&apos;"
+  character -> [character]
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory.hs
+++ /dev/null
@@ -1,96 +0,0 @@
-module Moonlight.Triangulation.Exhibit.CategoryObservatory
-  ( Exhibit,
-    ExhibitError,
-    buildExhibit,
-    renderExhibitError,
-  )
-where
-
-import Data.Bifunctor (first)
-import Data.Vector qualified as Vector
-import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
-import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellComplex
-  ( DCELComplex,
-    fromExactCellSet,
-  )
-import Moonlight.Triangulation.CellSet
-  ( ExactCellSet,
-    closeFaceCellSet,
-    exactCellSetEdgeCount,
-    exactCellSetFaceCount,
-    exactCellSetVertexCount,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
-  ( projectCategory,
-    validateCategoryEnumeration,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry (projectMesh)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( CellInventory (..),
-    Exhibit (..),
-    ExhibitError (..),
-    renderExhibitError,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
-  ( projectNerve,
-    validateNerveCount,
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
-import Moonlight.Triangulation.Types (Point (..))
-
-buildExhibit :: Either ExhibitError Exhibit
-buildExhibit = do
-  triangulation <- first ExhibitTriangulationBuildFailed (delaunayGeometry exhibitPoints)
-  selectedCells <-
-    first ExhibitCellSelectionFailed
-      (closeFaceCellSet triangulation (innerFaces triangulation))
-  let complexValue = fromExactCellSet selectedCells
-      categoryValue = complexCategory complexValue
-  validateInventory selectedCells complexValue
-  validateCategoryEnumeration complexValue categoryValue
-  categoryProjection <- projectCategory triangulation categoryValue
-  nerveProjection <- projectNerve categoryValue
-  validateNerveCount complexValue nerveProjection
-  pure
-    Exhibit
-      { schemaVersion = 2,
-        exhibitTitle = "Triangulation, incidence category, and flag nerve",
-        mesh = projectMesh triangulation complexValue,
-        category = categoryProjection,
-        nerve = nerveProjection
-      }
-
-validateInventory :: ExactCellSet -> DCELComplex -> Either ExhibitError ()
-validateInventory cellSet complexValue =
-  let expectedInventory =
-        CellInventory
-          { cellVertexCount = exactCellSetVertexCount cellSet,
-            cellEdgeCount = exactCellSetEdgeCount cellSet,
-            cellFaceCount = exactCellSetFaceCount cellSet
-          }
-      observedInventory =
-        CellInventory
-          { cellVertexCount = length (vertices complexValue),
-            cellEdgeCount = length (edges complexValue),
-            cellFaceCount = length (faces complexValue)
-          }
-   in if observedInventory == expectedInventory
-        then Right ()
-        else Left (ExhibitInventoryMismatch expectedInventory observedInventory)
-
-exhibitPoints :: Vector.Vector Point
-exhibitPoints =
-  Vector.fromList
-    [ Point 80 72,
-      Point 222 48,
-      Point 362 92,
-      Point 52 208,
-      Point 176 166,
-      Point 310 190,
-      Point 414 232,
-      Point 112 330,
-      Point 258 314,
-      Point 380 354
-    ]
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Category.hs
+++ /dev/null
@@ -1,127 +0,0 @@
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
-  ( incidenceFlagCount,
-    projectCategory,
-    validateCategoryEnumeration,
-  )
-where
-
-import Data.Bifunctor (first)
-import Data.Containers.ListUtils (nubOrd)
-import Moonlight.Category
-  ( Category (..),
-    FiniteComposableCategory (..),
-  )
-import Moonlight.Homology.Pure.Topology.CellCategory
-  ( ComplexCat,
-    ComplexMor (..),
-    ComplexOb,
-  )
-import Moonlight.Homology.Pure.Topology.CellComplex
-  ( CellComplex2D (..),
-    CellTypes (..),
-    OrientedEdge (..),
-  )
-import Moonlight.Triangulation.CellComplex (DCELComplex)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry (positionForObject)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
-  ( morphismKind,
-    objectIdentifier,
-    objectKind,
-    objectLabel,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( CategoryMorphism (..),
-    CategoryObject (..),
-    CategoryProjection (..),
-    ExhibitError (..),
-    MorphismEndpoint (..),
-    MorphismInventory (..),
-  )
-import Moonlight.Triangulation.Types (DelaunayTriangulation)
-
-projectCategory ::
-  DelaunayTriangulation () ->
-  ComplexCat DCELComplex ->
-  Either ExhibitError CategoryProjection
-projectCategory triangulation categoryValue =
-  CategoryProjection
-    <$> traverse
-      (projectCategoryObject triangulation)
-      (enumerateObjects categoryValue)
-    <*> traverse
-      (projectCategoryMorphism categoryValue)
-      (zip [0 ..] (enumerateMorphisms categoryValue))
-
-projectCategoryObject ::
-  DelaunayTriangulation () ->
-  ComplexOb DCELComplex ->
-  Either ExhibitError CategoryObject
-projectCategoryObject triangulation objectValue =
-  CategoryObject
-    (objectIdentifier objectValue)
-    (objectKind objectValue)
-    (objectLabel objectValue)
-    <$> positionForObject triangulation objectValue
-
-projectCategoryMorphism ::
-  ComplexCat DCELComplex ->
-  (Int, ComplexMor DCELComplex) ->
-  Either ExhibitError CategoryMorphism
-projectCategoryMorphism categoryValue (morphismIndex, morphismValue) =
-  CategoryMorphism
-    ("morphism-" <> show morphismIndex)
-    (morphismKind morphismValue)
-    <$> projectedEndpoint MorphismSource source
-    <*> projectedEndpoint MorphismTarget target
-  where
-    projectedEndpoint endpoint endpointProjection =
-      objectIdentifier
-        <$> first
-          (const (ExhibitMorphismEndpointUnavailable endpoint morphismIndex))
-          (endpointProjection categoryValue morphismValue)
-
-validateCategoryEnumeration ::
-  DCELComplex ->
-  ComplexCat DCELComplex ->
-  Either ExhibitError ()
-validateCategoryEnumeration complexValue categoryValue =
-  let morphisms = enumerateMorphisms categoryValue
-      expectedInventory =
-        MorphismInventory
-          { identityMorphismCount =
-              length (vertices complexValue)
-                + length (edges complexValue)
-                + length (faces complexValue),
-            faceEdgeMorphismCount =
-              sum (fmap (length . faceBoundary complexValue) (faces complexValue)),
-            edgeVertexMorphismCount =
-              sum (fmap (edgeEndpointCount complexValue) (edges complexValue)),
-            faceVertexMorphismCount =
-              sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
-          }
-      observedInventory =
-        foldMap morphismInventory morphisms
-   in if observedInventory == expectedInventory
-        then Right ()
-        else Left (ExhibitCategoryEnumerationMismatch expectedInventory observedInventory)
-
-edgeEndpointCount :: DCELComplex -> Edge DCELComplex -> Int
-edgeEndpointCount complexValue edgeValue =
-  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
-   in length (nubOrd [sourceVertex, targetVertex])
-
-incidenceFlagCount :: DCELComplex -> Face DCELComplex -> Int
-incidenceFlagCount complexValue faceValue =
-  sum
-    ( fmap
-        (edgeEndpointCount complexValue . orientedEdge)
-        (faceBoundary complexValue faceValue)
-    )
-
-morphismInventory :: ComplexMor DCELComplex -> MorphismInventory
-morphismInventory morphismValue =
-  case morphismValue of
-    IdentityMor _ -> MorphismInventory 1 0 0 0
-    FaceToEdge {} -> MorphismInventory 0 1 0 0
-    EdgeToVertex _ _ -> MorphismInventory 0 0 1 0
-    FaceToVertex {} -> MorphismInventory 0 0 0 1
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Geometry.hs
+++ /dev/null
@@ -1,127 +0,0 @@
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry
-  ( positionForObject,
-    projectMesh,
-  )
-where
-
-import Data.Foldable (foldMap')
-import Moonlight.Homology.Pure.Topology.CellCategory (ComplexOb (..))
-import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
-import Moonlight.Triangulation.CellComplex (DCELComplex)
-import Moonlight.Triangulation.Dcel
-  ( faceVertices,
-    undirectedEndpoints,
-    vertexPoint,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
-  ( edgeIdentifier,
-    edgeObjectIdentifier,
-    faceIdentifier,
-    faceObjectIdentifier,
-    vertexIdentifier,
-    vertexObjectIdentifier,
-  )
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( ExhibitError (..),
-    MeshEdge (..),
-    MeshFace (..),
-    MeshProjection (..),
-    MeshVertex (..),
-    Position (..),
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId,
-    UndirectedEdgeId,
-    VertexId,
-  )
-import Moonlight.Triangulation.Types
-  ( DelaunayTriangulation,
-    Point (..),
-  )
-
-projectMesh :: DelaunayTriangulation () -> DCELComplex -> MeshProjection
-projectMesh triangulation complexValue =
-  MeshProjection
-    { meshVertices = fmap (projectMeshVertex triangulation) (vertices complexValue),
-      meshEdges = fmap (projectMeshEdge triangulation) (edges complexValue),
-      meshFaces = fmap (projectMeshFace triangulation) (faces complexValue)
-    }
-
-projectMeshVertex :: DelaunayTriangulation () -> VertexId -> MeshVertex
-projectMeshVertex triangulation vertexValue =
-  MeshVertex
-    { meshVertexIdentifier = vertexIdentifier vertexValue,
-      meshVertexObjectIdentifier = vertexObjectIdentifier vertexValue,
-      meshVertexPosition = pointPosition (vertexPoint triangulation vertexValue)
-    }
-
-projectMeshEdge :: DelaunayTriangulation () -> UndirectedEdgeId -> MeshEdge
-projectMeshEdge triangulation edgeValue =
-  let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
-   in MeshEdge
-        { meshEdgeIdentifier = edgeIdentifier edgeValue,
-          meshEdgeObjectIdentifier = edgeObjectIdentifier edgeValue,
-          meshEdgeVertexIdentifiers = fmap vertexIdentifier [sourceVertex, targetVertex]
-        }
-
-projectMeshFace :: DelaunayTriangulation () -> FaceId -> MeshFace
-projectMeshFace triangulation faceValue =
-  MeshFace
-    { meshFaceIdentifier = faceIdentifier faceValue,
-      meshFaceObjectIdentifier = faceObjectIdentifier faceValue,
-      meshFaceVertexIdentifiers = fmap vertexIdentifier (faceVertices triangulation faceValue)
-    }
-
-positionForObject ::
-  DelaunayTriangulation () ->
-  ComplexOb DCELComplex ->
-  Either ExhibitError Position
-positionForObject triangulation objectValue =
-  case objectValue of
-    VertexOb vertexValue ->
-      Right (pointPosition (vertexPoint triangulation vertexValue))
-    EdgeOb edgeValue ->
-      let (sourceVertex, targetVertex) = undirectedEndpoints triangulation edgeValue
-       in Right
-            ( midpoint
-                (pointPosition (vertexPoint triangulation sourceVertex))
-                (pointPosition (vertexPoint triangulation targetVertex))
-            )
-    FaceOb faceValue ->
-      centroid faceValue
-        (fmap (pointPosition . vertexPoint triangulation) (faceVertices triangulation faceValue))
-
-data PositionSum = PositionSum Double Double
-
-instance Semigroup PositionSum where
-  PositionSum leftX leftY <> PositionSum rightX rightY =
-    PositionSum (leftX + rightX) (leftY + rightY)
-
-instance Monoid PositionSum where
-  mempty = PositionSum 0 0
-
-centroid :: FaceId -> [Position] -> Either ExhibitError Position
-centroid faceValue positions =
-  case positions of
-    [] -> Left (ExhibitFaceWithoutVertices faceValue)
-    _ ->
-      let PositionSum summedX summedY =
-            foldMap'
-              (\positionValue -> PositionSum (positionX positionValue) (positionY positionValue))
-              positions
-          positionCount = fromIntegral (length positions)
-       in Right (Position (summedX / positionCount) (summedY / positionCount))
-
-midpoint :: Position -> Position -> Position
-midpoint firstPosition secondPosition =
-  Position
-    { positionX = (positionX firstPosition + positionX secondPosition) / 2,
-      positionY = (positionY firstPosition + positionY secondPosition) / 2
-    }
-
-pointPosition :: Point -> Position
-pointPosition pointValue =
-  Position
-    { positionX = pointX pointValue,
-      positionY = pointY pointValue
-    }
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Identifier.hs
+++ /dev/null
@@ -1,78 +0,0 @@
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
-  ( objectIdentifier,
-    objectKind,
-    objectLabel,
-    morphismKind,
-    vertexIdentifier,
-    edgeIdentifier,
-    faceIdentifier,
-    vertexObjectIdentifier,
-    edgeObjectIdentifier,
-    faceObjectIdentifier,
-  )
-where
-
-import Moonlight.Homology.Pure.Topology.CellCategory
-  ( ComplexMor (..),
-    ComplexOb (..),
-  )
-import Moonlight.Triangulation.CellComplex (DCELComplex)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( CategoryMorphismKind (..),
-    CategoryObjectKind (..),
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId,
-    UndirectedEdgeId,
-    VertexId,
-    unFaceId,
-    unUndirectedEdgeId,
-    unVertexId,
-  )
-
-objectIdentifier :: ComplexOb DCELComplex -> String
-objectIdentifier objectValue =
-  case objectValue of
-    VertexOb vertexValue -> vertexObjectIdentifier vertexValue
-    EdgeOb edgeValue -> edgeObjectIdentifier edgeValue
-    FaceOb faceValue -> faceObjectIdentifier faceValue
-
-objectKind :: ComplexOb DCELComplex -> CategoryObjectKind
-objectKind objectValue =
-  case objectValue of
-    VertexOb _ -> VertexObject
-    EdgeOb _ -> EdgeObject
-    FaceOb _ -> FaceObject
-
-objectLabel :: ComplexOb DCELComplex -> String
-objectLabel objectValue =
-  case objectValue of
-    VertexOb vertexValue -> "v" <> show (unVertexId vertexValue)
-    EdgeOb edgeValue -> "e" <> show (unUndirectedEdgeId edgeValue)
-    FaceOb faceValue -> "f" <> show (unFaceId faceValue)
-
-morphismKind :: ComplexMor DCELComplex -> CategoryMorphismKind
-morphismKind morphismValue =
-  case morphismValue of
-    IdentityMor _ -> IdentityMorphism
-    FaceToEdge {} -> FaceEdgeMorphism
-    EdgeToVertex _ _ -> EdgeVertexMorphism
-    FaceToVertex {} -> FaceVertexComposite
-
-vertexIdentifier :: VertexId -> String
-vertexIdentifier vertexValue = "vertex-" <> show (unVertexId vertexValue)
-
-edgeIdentifier :: UndirectedEdgeId -> String
-edgeIdentifier edgeValue = "edge-" <> show (unUndirectedEdgeId edgeValue)
-
-faceIdentifier :: FaceId -> String
-faceIdentifier faceValue = "face-" <> show (unFaceId faceValue)
-
-vertexObjectIdentifier :: VertexId -> String
-vertexObjectIdentifier vertexValue = "object-" <> vertexIdentifier vertexValue
-
-edgeObjectIdentifier :: UndirectedEdgeId -> String
-edgeObjectIdentifier edgeValue = "object-" <> edgeIdentifier edgeValue
-
-faceObjectIdentifier :: FaceId -> String
-faceObjectIdentifier faceValue = "object-" <> faceIdentifier faceValue
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Model.hs
+++ /dev/null
@@ -1,188 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( Exhibit (..),
-    MeshProjection (..),
-    MeshVertex (..),
-    MeshEdge (..),
-    MeshFace (..),
-    Position (..),
-    CategoryProjection (..),
-    CategoryObject (..),
-    CategoryObjectKind (..),
-    CategoryMorphism (..),
-    CategoryMorphismKind (..),
-    NerveProjection (..),
-    NerveSimplexCount (..),
-    NerveTwoSimplex (..),
-    CellInventory (..),
-    MorphismInventory (..),
-    ExhibitError (..),
-    MorphismEndpoint (..),
-    renderExhibitError,
-  )
-where
-
-import Data.Aeson (ToJSON)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.CellSet (CellSelectionError)
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId)
-import Moonlight.Triangulation.Types (BuildError)
-import Numeric.Natural (Natural)
-
-data Exhibit = Exhibit
-  { schemaVersion :: Int,
-    exhibitTitle :: String,
-    mesh :: MeshProjection,
-    category :: CategoryProjection,
-    nerve :: NerveProjection
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshProjection = MeshProjection
-  { meshVertices :: [MeshVertex],
-    meshEdges :: [MeshEdge],
-    meshFaces :: [MeshFace]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshVertex = MeshVertex
-  { meshVertexIdentifier :: String,
-    meshVertexObjectIdentifier :: String,
-    meshVertexPosition :: Position
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshEdge = MeshEdge
-  { meshEdgeIdentifier :: String,
-    meshEdgeObjectIdentifier :: String,
-    meshEdgeVertexIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data MeshFace = MeshFace
-  { meshFaceIdentifier :: String,
-    meshFaceObjectIdentifier :: String,
-    meshFaceVertexIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data Position = Position
-  { positionX :: Double,
-    positionY :: Double
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryProjection = CategoryProjection
-  { categoryObjects :: [CategoryObject],
-    categoryMorphisms :: [CategoryMorphism]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryObject = CategoryObject
-  { categoryObjectIdentifier :: String,
-    categoryObjectKind :: CategoryObjectKind,
-    categoryObjectLabel :: String,
-    categoryObjectPosition :: Position
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryObjectKind
-  = VertexObject
-  | EdgeObject
-  | FaceObject
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryMorphism = CategoryMorphism
-  { categoryMorphismIdentifier :: String,
-    categoryMorphismKind :: CategoryMorphismKind,
-    categoryMorphismSourceObject :: String,
-    categoryMorphismTargetObject :: String
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CategoryMorphismKind
-  = IdentityMorphism
-  | FaceEdgeMorphism
-  | EdgeVertexMorphism
-  | FaceVertexComposite
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveProjection = NerveProjection
-  { nerveTwoSimplices :: [NerveTwoSimplex],
-    nerveSimplexCounts :: [NerveSimplexCount]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveSimplexCount = NerveSimplexCount
-  { nerveDimension :: Natural,
-    normalizedSimplexCount :: Int,
-    unnormalizedSimplexCount :: Int
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data NerveTwoSimplex = NerveTwoSimplex
-  { nerveSimplexIdentifier :: String,
-    nerveSimplexObjectIdentifiers :: [String]
-  }
-  deriving stock (Generic)
-  deriving anyclass (ToJSON)
-
-data CellInventory = CellInventory
-  { cellVertexCount :: Int,
-    cellEdgeCount :: Int,
-    cellFaceCount :: Int
-  }
-  deriving stock (Eq, Show)
-
-data MorphismInventory = MorphismInventory
-  { identityMorphismCount :: Int,
-    faceEdgeMorphismCount :: Int,
-    edgeVertexMorphismCount :: Int,
-    faceVertexMorphismCount :: Int
-  }
-  deriving stock (Eq, Show)
-
-instance Semigroup MorphismInventory where
-  MorphismInventory leftIdentities leftFaceEdges leftEdgeVertices leftFaceVertices
-    <> MorphismInventory rightIdentities rightFaceEdges rightEdgeVertices rightFaceVertices =
-      MorphismInventory
-        (leftIdentities + rightIdentities)
-        (leftFaceEdges + rightFaceEdges)
-        (leftEdgeVertices + rightEdgeVertices)
-        (leftFaceVertices + rightFaceVertices)
-
-instance Monoid MorphismInventory where
-  mempty = MorphismInventory 0 0 0 0
-
-data ExhibitError
-  = ExhibitTriangulationBuildFailed BuildError
-  | ExhibitCellSelectionFailed CellSelectionError
-  | ExhibitMorphismEndpointUnavailable MorphismEndpoint Int
-  | ExhibitFaceWithoutVertices FaceId
-  | ExhibitMalformedTwoSimplex Int
-  | ExhibitInventoryMismatch CellInventory CellInventory
-  | ExhibitCategoryEnumerationMismatch MorphismInventory MorphismInventory
-  | ExhibitNerveCountMismatch Int Int
-  deriving stock (Show)
-
-data MorphismEndpoint
-  = MorphismSource
-  | MorphismTarget
-  deriving stock (Show)
-
-renderExhibitError :: ExhibitError -> String
-renderExhibitError = show
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Nerve.hs
+++ /dev/null
@@ -1,84 +0,0 @@
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
-  ( projectNerve,
-    validateNerveCount,
-  )
-where
-
-import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Category (chainVertices)
-import Moonlight.Category.Simplicial
-  ( GeneratedSSet,
-    NerveSimplex,
-    TruncatedNormalizedSSet,
-    generatedSimplicesAtDimension,
-    nerveSimplexChain,
-    normalizedNerve,
-    simplicesAtDimension,
-    unnormalizedNerve,
-  )
-import Moonlight.Homology.Pure.Topology.CellCategory (ComplexCat)
-import Moonlight.Homology.Pure.Topology.CellComplex (CellComplex2D (..))
-import Moonlight.Triangulation.CellComplex (DCELComplex)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Category (incidenceFlagCount)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier (objectIdentifier)
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( ExhibitError (..),
-    NerveProjection (..),
-    NerveSimplexCount (..),
-    NerveTwoSimplex (..),
-  )
-import Numeric.Natural (Natural)
-
-projectNerve :: ComplexCat DCELComplex -> Either ExhibitError NerveProjection
-projectNerve categoryValue =
-  let normalizedNerveValue = normalizedNerve categoryValue nerveObservationBound
-      unnormalizedNerveValue = unnormalizedNerve categoryValue nerveObservationBound
-   in NerveProjection
-        <$> traverse
-          projectTwoSimplex
-          (zip [0 ..] (simplicesAtDimension normalizedNerveValue nerveObservationBound))
-        <*> pure
-          ( fmap
-              (projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue)
-              nerveObservationDimensions
-          )
-
-validateNerveCount :: DCELComplex -> NerveProjection -> Either ExhibitError ()
-validateNerveCount complexValue nerveProjection =
-  let expectedCount = sum (fmap (incidenceFlagCount complexValue) (faces complexValue))
-      observedCount = length (nerveTwoSimplices nerveProjection)
-   in if observedCount == expectedCount
-        then Right ()
-        else Left (ExhibitNerveCountMismatch expectedCount observedCount)
-
-projectNerveSimplexCount ::
-  TruncatedNormalizedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
-  GeneratedSSet (NerveSimplex (ComplexCat DCELComplex)) ->
-  Natural ->
-  NerveSimplexCount
-projectNerveSimplexCount normalizedNerveValue unnormalizedNerveValue dimensionValue =
-  NerveSimplexCount
-    { nerveDimension = dimensionValue,
-      normalizedSimplexCount = length (simplicesAtDimension normalizedNerveValue dimensionValue),
-      unnormalizedSimplexCount = length (generatedSimplicesAtDimension unnormalizedNerveValue dimensionValue)
-    }
-
-projectTwoSimplex ::
-  (Int, NerveSimplex (ComplexCat DCELComplex)) ->
-  Either ExhibitError NerveTwoSimplex
-projectTwoSimplex (simplexIndex, simplexValue) =
-  case chainVertices (nerveSimplexChain simplexValue) of
-    faceObject :| [edgeObject, vertexObject] ->
-      Right
-        NerveTwoSimplex
-          { nerveSimplexIdentifier = "simplex-" <> show simplexIndex,
-            nerveSimplexObjectIdentifiers =
-              fmap objectIdentifier [faceObject, edgeObject, vertexObject]
-          }
-    _ -> Left (ExhibitMalformedTwoSimplex simplexIndex)
-
-nerveObservationBound :: Natural
-nerveObservationBound = 2
-
-nerveObservationDimensions :: [Natural]
-nerveObservationDimensions = [0, 1, 2]
diff --git a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs b/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs
deleted file mode 100644
--- a/docs/category-observatory/Moonlight/Triangulation/Exhibit/CategoryObservatory/Picture.hs
+++ /dev/null
@@ -1,663 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-
--- | Pure, compact SVG projection of the checked category-observatory exhibit.
-module Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
-  ( PictureObstruction (..),
-    categoryObservatoryPictureFileName,
-    renderCategoryObservatoryPicture,
-    renderPictureObstruction,
-  )
-where
-
-import Data.Map.Strict qualified as Map
-import Data.Set qualified as Set
-import Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-  ( CategoryObject (..),
-    CategoryObjectKind (..),
-    CategoryProjection (..),
-    Exhibit (..),
-    MeshEdge (..),
-    MeshFace (..),
-    MeshProjection (..),
-    MeshVertex (..),
-    NerveProjection (..),
-    NerveSimplexCount (..),
-    NerveTwoSimplex (..),
-    Position (..),
-  )
-import Numeric (showFFloat)
-
-data PictureObstruction
-  = PictureHasNoMeshVertices
-  | PictureHasNoNerveTwoSimplex
-  | PictureMissingMeshVertex !String
-  | PictureMissingCategoryObject !String
-  | PictureMalformedMeshEdge !String ![String]
-  | PictureMalformedMeshFace !String ![String]
-  | PictureMalformedNerveTwoSimplex !String ![String]
-  | PictureMalformedFlagKinds ![CategoryObjectKind]
-  deriving stock (Eq, Show)
-
-data PictureBounds = PictureBounds
-  { minimumX :: !Double,
-    minimumY :: !Double,
-    maximumX :: !Double,
-    maximumY :: !Double
-  }
-
-data PictureFrame = PictureFrame
-  { frameLeft :: !Double,
-    frameTop :: !Double,
-    frameWidth :: !Double,
-    frameHeight :: !Double
-  }
-
-data ResolvedMeshEdge = ResolvedMeshEdge
-  { resolvedEdge :: !MeshEdge,
-    resolvedEdgeSource :: !Position,
-    resolvedEdgeTarget :: !Position
-  }
-
-data ResolvedMeshFace = ResolvedMeshFace
-  { resolvedFace :: !MeshFace,
-    resolvedFaceVertices :: ![Position]
-  }
-
-data ResolvedNerveSimplex = ResolvedNerveSimplex
-  { resolvedSimplex :: !NerveTwoSimplex,
-    resolvedSimplexVertices :: ![Position]
-  }
-
-data FlagSelection = FlagSelection
-  { selectedSimplexIdentifier :: !String,
-    selectedFace :: !CategoryObject,
-    selectedEdge :: !CategoryObject,
-    selectedVertex :: !CategoryObject,
-    selectedObjectIdentifiers :: !(Set.Set String)
-  }
-
-data PictureContext = PictureContext
-  { contextExhibit :: !Exhibit,
-    contextBounds :: !PictureBounds,
-    contextEdges :: ![ResolvedMeshEdge],
-    contextFaces :: ![ResolvedMeshFace],
-    contextNerveSimplices :: ![ResolvedNerveSimplex],
-    contextFlag :: !FlagSelection
-  }
-
-categoryObservatoryPictureFileName :: FilePath
-categoryObservatoryPictureFileName =
-  "moonlight-triangulation-category-observatory.svg"
-
-renderCategoryObservatoryPicture :: Exhibit -> Either PictureObstruction String
-renderCategoryObservatoryPicture exhibit =
-  renderPicture <$> preparePicture exhibit
-
-renderPictureObstruction :: PictureObstruction -> String
-renderPictureObstruction = \case
-  PictureHasNoMeshVertices ->
-    "category-observatory picture requires at least one mesh vertex"
-  PictureHasNoNerveTwoSimplex ->
-    "category-observatory picture requires at least one admitted two-simplex"
-  PictureMissingMeshVertex identifier ->
-    "category-observatory picture cannot resolve mesh vertex " <> show identifier
-  PictureMissingCategoryObject identifier ->
-    "category-observatory picture cannot resolve category object " <> show identifier
-  PictureMalformedMeshEdge identifier vertices ->
-    "category-observatory mesh edge "
-      <> show identifier
-      <> " does not name exactly two vertices: "
-      <> show vertices
-  PictureMalformedMeshFace identifier vertices ->
-    "category-observatory mesh face "
-      <> show identifier
-      <> " does not name exactly three vertices: "
-      <> show vertices
-  PictureMalformedNerveTwoSimplex identifier objects ->
-    "category-observatory nerve simplex "
-      <> show identifier
-      <> " does not name one face, edge, and vertex: "
-      <> show objects
-  PictureMalformedFlagKinds kinds ->
-    "category-observatory selected flag has incompatible object kinds: " <> show kinds
-
-preparePicture :: Exhibit -> Either PictureObstruction PictureContext
-preparePicture exhibit = do
-  let meshProjection = mesh exhibit
-      categoryProjection = category exhibit
-      nerveProjection = nerve exhibit
-      meshVerticesValue = meshVertices meshProjection
-      vertexPositions =
-        Map.fromList
-          ( fmap
-              (\vertex -> (meshVertexIdentifier vertex, meshVertexPosition vertex))
-              meshVerticesValue
-          )
-      categoryObjectsByIdentifier =
-        Map.fromList
-          ( fmap
-              (\objectValue -> (categoryObjectIdentifier objectValue, objectValue))
-              (categoryObjects categoryProjection)
-          )
-  pictureBounds <- boundsFromPositions (meshVertexPosition <$> meshVerticesValue)
-  resolvedEdges <- traverse (resolveMeshEdge vertexPositions) (meshEdges meshProjection)
-  resolvedFaces <- traverse (resolveMeshFace vertexPositions) (meshFaces meshProjection)
-  resolvedSimplices <-
-    traverse
-      (resolveNerveSimplex categoryObjectsByIdentifier)
-      (nerveTwoSimplices nerveProjection)
-  flagSelection <- selectFlag categoryObjectsByIdentifier (nerveTwoSimplices nerveProjection)
-  pure
-    PictureContext
-      { contextExhibit = exhibit,
-        contextBounds = pictureBounds,
-        contextEdges = resolvedEdges,
-        contextFaces = resolvedFaces,
-        contextNerveSimplices = resolvedSimplices,
-        contextFlag = flagSelection
-      }
-
-boundsFromPositions :: [Position] -> Either PictureObstruction PictureBounds
-boundsFromPositions positions =
-  case positions of
-    [] -> Left PictureHasNoMeshVertices
-    firstPosition : remainingPositions ->
-      Right
-        ( foldl'
-            extendBounds
-            ( PictureBounds
-                (positionX firstPosition)
-                (positionY firstPosition)
-                (positionX firstPosition)
-                (positionY firstPosition)
-            )
-            remainingPositions
-        )
-
-extendBounds :: PictureBounds -> Position -> PictureBounds
-extendBounds bounds positionValue =
-  PictureBounds
-    { minimumX = min (minimumX bounds) (positionX positionValue),
-      minimumY = min (minimumY bounds) (positionY positionValue),
-      maximumX = max (maximumX bounds) (positionX positionValue),
-      maximumY = max (maximumY bounds) (positionY positionValue)
-    }
-
-resolveMeshEdge ::
-  Map.Map String Position ->
-  MeshEdge ->
-  Either PictureObstruction ResolvedMeshEdge
-resolveMeshEdge vertexPositions edgeValue =
-  case meshEdgeVertexIdentifiers edgeValue of
-    [sourceIdentifier, targetIdentifier] ->
-      ResolvedMeshEdge edgeValue
-        <$> lookupMeshVertex vertexPositions sourceIdentifier
-        <*> lookupMeshVertex vertexPositions targetIdentifier
-    identifiers ->
-      Left (PictureMalformedMeshEdge (meshEdgeIdentifier edgeValue) identifiers)
-
-resolveMeshFace ::
-  Map.Map String Position ->
-  MeshFace ->
-  Either PictureObstruction ResolvedMeshFace
-resolveMeshFace vertexPositions faceValue =
-  case meshFaceVertexIdentifiers faceValue of
-    [firstIdentifier, secondIdentifier, thirdIdentifier] ->
-      ResolvedMeshFace faceValue
-        <$> traverse
-          (lookupMeshVertex vertexPositions)
-          [firstIdentifier, secondIdentifier, thirdIdentifier]
-    identifiers ->
-      Left (PictureMalformedMeshFace (meshFaceIdentifier faceValue) identifiers)
-
-lookupMeshVertex ::
-  Map.Map String Position ->
-  String ->
-  Either PictureObstruction Position
-lookupMeshVertex vertexPositions identifier =
-  maybe
-    (Left (PictureMissingMeshVertex identifier))
-    Right
-    (Map.lookup identifier vertexPositions)
-
-resolveNerveSimplex ::
-  Map.Map String CategoryObject ->
-  NerveTwoSimplex ->
-  Either PictureObstruction ResolvedNerveSimplex
-resolveNerveSimplex categoryObjectsByIdentifier simplexValue =
-  case nerveSimplexObjectIdentifiers simplexValue of
-    [faceIdentifier, edgeIdentifier, vertexIdentifier] ->
-      ResolvedNerveSimplex simplexValue
-        . fmap categoryObjectPosition
-        <$> traverse
-          (lookupCategoryObject categoryObjectsByIdentifier)
-          [faceIdentifier, edgeIdentifier, vertexIdentifier]
-    identifiers ->
-      Left
-        ( PictureMalformedNerveTwoSimplex
-            (nerveSimplexIdentifier simplexValue)
-            identifiers
-        )
-
-selectFlag ::
-  Map.Map String CategoryObject ->
-  [NerveTwoSimplex] ->
-  Either PictureObstruction FlagSelection
-selectFlag categoryObjectsByIdentifier simplices =
-  case simplices of
-    [] -> Left PictureHasNoNerveTwoSimplex
-    simplexValue : _ ->
-      case nerveSimplexObjectIdentifiers simplexValue of
-        identifiers@[faceIdentifier, edgeIdentifier, vertexIdentifier] -> do
-          faceObject <- lookupCategoryObject categoryObjectsByIdentifier faceIdentifier
-          edgeObject <- lookupCategoryObject categoryObjectsByIdentifier edgeIdentifier
-          vertexObject <- lookupCategoryObject categoryObjectsByIdentifier vertexIdentifier
-          let kinds = categoryObjectKind <$> [faceObject, edgeObject, vertexObject]
-          case kinds of
-            [FaceObject, EdgeObject, VertexObject] ->
-              Right
-                FlagSelection
-                  { selectedSimplexIdentifier = nerveSimplexIdentifier simplexValue,
-                    selectedFace = faceObject,
-                    selectedEdge = edgeObject,
-                    selectedVertex = vertexObject,
-                    selectedObjectIdentifiers = Set.fromList identifiers
-                  }
-            _ -> Left (PictureMalformedFlagKinds kinds)
-        identifiers ->
-          Left
-            ( PictureMalformedNerveTwoSimplex
-                (nerveSimplexIdentifier simplexValue)
-                identifiers
-            )
-
-lookupCategoryObject ::
-  Map.Map String CategoryObject ->
-  String ->
-  Either PictureObstruction CategoryObject
-lookupCategoryObject categoryObjectsByIdentifier identifier =
-  maybe
-    (Left (PictureMissingCategoryObject identifier))
-    Right
-    (Map.lookup identifier categoryObjectsByIdentifier)
-
-renderPicture :: PictureContext -> String
-renderPicture context =
-  unlines
-    ( renderDocumentOpening
-        <> renderHeader context
-        <> renderPanels context
-        <> renderNerveProfile context
-        <> ["</svg>"]
-    )
-
-renderDocumentOpening :: [String]
-renderDocumentOpening =
-  [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>",
-    "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 920\" role=\"img\" aria-labelledby=\"title description\">",
-    "<title id=\"title\">Moonlight category observatory</title>",
-    "<desc id=\"description\">One checked Delaunay section shown as geometry, a local face-edge-vertex incidence flag, and the full path-sensitive flag nerve.</desc>",
-    "<defs>",
-    "  <linearGradient id=\"background\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop stop-color=\"#10262a\"/><stop offset=\"0.48\" stop-color=\"#071217\"/><stop offset=\"1\" stop-color=\"#120f20\"/></linearGradient>",
-    "  <radialGradient id=\"cyan-glow\"><stop stop-color=\"#72d6c2\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#72d6c2\" stop-opacity=\"0\"/></radialGradient>",
-    "  <radialGradient id=\"violet-glow\"><stop stop-color=\"#bc8cff\" stop-opacity=\"0.22\"/><stop offset=\"1\" stop-color=\"#bc8cff\" stop-opacity=\"0\"/></radialGradient>",
-    "  <pattern id=\"grid\" width=\"28\" height=\"28\" patternUnits=\"userSpaceOnUse\"><path d=\"M28 0H0V28\" fill=\"none\" stroke=\"#9bc7bd\" stroke-opacity=\"0.07\" stroke-width=\"1\"/></pattern>",
-    "  <marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0L10 5L0 10Z\" fill=\"#72d6c2\"/></marker>",
-    "  <filter id=\"soft-shadow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feDropShadow dx=\"0\" dy=\"8\" stdDeviation=\"12\" flood-color=\"#000\" flood-opacity=\"0.28\"/></filter>",
-    "  <style><![CDATA[",
-    "    .sans{font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,sans-serif}",
-    "    .serif{font-family:Georgia,'Times New Roman',serif}",
-    "    .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}",
-    "    .panel{fill:#0a181d;fill-opacity:.93;stroke:#8bbeb2;stroke-opacity:.22;stroke-width:1.2}",
-    "    .kicker{fill:#72d6c2;font-size:12px;font-weight:700;letter-spacing:2.8px}",
-    "    .title{fill:#f1eee7;font-size:48px;font-weight:400;letter-spacing:-1.4px}",
-    "    .subtitle{fill:#9ab0aa;font-size:16px}",
-    "    .panel-number{fill:#72d6c2;font-size:11px;font-weight:700;letter-spacing:1.6px}",
-    "    .panel-title{fill:#f1eee7;font-size:25px}",
-    "    .callout-title{fill:#f1eee7;font-size:21px}",
-    "    .panel-note{fill:#8ca49f;font-size:12px}",
-    "    .metric-value{fill:#f1eee7;font-size:18px;font-weight:700}",
-    "    .metric-label{fill:#8ca49f;font-size:9px;font-weight:700;letter-spacing:1.2px}",
-    "    .mesh-face{fill:#72d6c2;fill-opacity:.035;stroke:none}",
-    "    .mesh-face.selected{fill:#ffad66;fill-opacity:.28}",
-    "    .mesh-edge{stroke:#72d6c2;stroke-opacity:.58;stroke-width:1.7}",
-    "    .mesh-edge.selected{stroke:#ffad66;stroke-opacity:1;stroke-width:5}",
-    "    .mesh-vertex{fill:#071217;stroke:#f2e37e;stroke-width:2.2}",
-    "    .mesh-vertex.selected{fill:#f2e37e;stroke:#fff7b0;stroke-width:4}",
-    "    .nerve-simplex{fill:#bc8cff;fill-opacity:.065;stroke:#bc8cff;stroke-opacity:.42;stroke-width:1.1}",
-    "    .nerve-simplex.selected{fill:#ffad66;fill-opacity:.48;stroke:#ffd1a8;stroke-opacity:1;stroke-width:3}",
-    "    .object-face{fill:#ffad66}.object-edge{fill:#72d6c2}.object-vertex{fill:#f2e37e}",
-    "    .node-label{fill:#f1eee7;font-size:23px;font-weight:700}.node-kind{fill:#8ca49f;font-size:10px;font-weight:700;letter-spacing:1.4px}",
-    "    .profile-label{fill:#8ca49f;font-size:11px;font-weight:700;letter-spacing:1px}.profile-value{fill:#f1eee7;font-size:12px;font-weight:700}",
-    "  ]]></style>",
-    "</defs>",
-    "<rect width=\"1440\" height=\"920\" fill=\"url(#background)\"/>",
-    "<circle cx=\"180\" cy=\"110\" r=\"290\" fill=\"url(#cyan-glow)\"/>",
-    "<circle cx=\"1290\" cy=\"520\" r=\"360\" fill=\"url(#violet-glow)\"/>"
-  ]
-
-renderHeader :: PictureContext -> [String]
-renderHeader context =
-  [ svgText "mono kicker" 56 38 "start" "MOONLIGHT / FINITE DESCENT",
-    svgText "serif title" 56 94 "start" "One section, three faithful projections",
-    svgText
-      "sans subtitle"
-      57
-      127
-      "start"
-      "Delaunay geometry → incidence category → path-sensitive flag nerve"
-  ]
-    <> concat
-      ( zipWith
-          renderMetric
-          [56, 212, 368, 524, 680, 856]
-          (pictureMetrics context)
-      )
-    <> [ "<path d=\"M1040 163H1378\" stroke=\"#72d6c2\" stroke-opacity=\".24\"/>",
-         svgText "mono panel-note" 1378 168 "end" "ONE HIGHLIGHTED FLAG GLUES ALL THREE PANELS"
-       ]
-
-data PictureMetric = PictureMetric
-  { metricValue :: !Int,
-    metricLabel :: !String
-  }
-
-pictureMetrics :: PictureContext -> [PictureMetric]
-pictureMetrics context =
-  let exhibit = contextExhibit context
-      meshProjection = mesh exhibit
-      categoryProjection = category exhibit
-      nerveProjection = nerve exhibit
-   in [ PictureMetric (length (meshVertices meshProjection)) "VERTICES",
-        PictureMetric (length (meshEdges meshProjection)) "EDGES",
-        PictureMetric (length (meshFaces meshProjection)) "FACES",
-        PictureMetric (length (categoryObjects categoryProjection)) "OBJECTS",
-        PictureMetric (length (categoryMorphisms categoryProjection)) "MORPHISMS",
-        PictureMetric (length (nerveTwoSimplices nerveProjection)) "FLAGS"
-      ]
-
-renderMetric :: Double -> PictureMetric -> [String]
-renderMetric x metric =
-  [ "<g transform=\"translate(" <> coordinate x <> " 151)\">",
-    "  <rect width=\"136\" height=\"48\" rx=\"13\" fill=\"#0b1b20\" stroke=\"#8bbeb2\" stroke-opacity=\".17\"/>",
-    "  " <> svgText "sans metric-value" 16 22 "start" (show (metricValue metric)),
-    "  " <> svgText "mono metric-label" 16 38 "start" (metricLabel metric),
-    "</g>"
-  ]
-
-renderPanels :: PictureContext -> [String]
-renderPanels context =
-  renderPanelBackgrounds
-    <> renderGeometryPanel context
-    <> renderCategoryPanel (contextFlag context)
-    <> renderNervePanel context
-
-renderPanelBackgrounds :: [String]
-renderPanelBackgrounds =
-  concatMap
-    (\x -> ["<rect class=\"panel\" x=\"" <> coordinate x <> "\" y=\"222\" width=\"410\" height=\"470\" rx=\"22\" filter=\"url(#soft-shadow)\"/>"])
-    [50, 515, 980]
-
-renderGeometryPanel :: PictureContext -> [String]
-renderGeometryPanel context =
-  renderPanelHeader 50 "01" "Geometric section" "the admitted Delaunay cells"
-    <> ["<rect x=\"70\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
-    <> fmap (renderMeshFace context geometryFrame) (contextFaces context)
-    <> fmap (renderMeshEdge context geometryFrame) (contextEdges context)
-    <> fmap (renderMeshVertex context geometryFrame) (meshVertices (mesh (contextExhibit context)))
-    <> renderGeometrySelectionLabels context
-
-geometryFrame :: PictureFrame
-geometryFrame = PictureFrame 83 341 344 290
-
-renderMeshFace :: PictureContext -> PictureFrame -> ResolvedMeshFace -> String
-renderMeshFace context frame faceValue =
-  let selected =
-        Set.member
-          (meshFaceObjectIdentifier (resolvedFace faceValue))
-          (selectedObjectIdentifiers (contextFlag context))
-   in "<polygon class=\"mesh-face"
-        <> selectedClass selected
-        <> "\" points=\""
-        <> renderPointList (contextBounds context) frame (resolvedFaceVertices faceValue)
-        <> "\"/>"
-
-renderMeshEdge :: PictureContext -> PictureFrame -> ResolvedMeshEdge -> String
-renderMeshEdge context frame edgeValue =
-  let selected =
-        Set.member
-          (meshEdgeObjectIdentifier (resolvedEdge edgeValue))
-          (selectedObjectIdentifiers (contextFlag context))
-      (sourceX, sourceY) = projectPosition (contextBounds context) frame (resolvedEdgeSource edgeValue)
-      (targetX, targetY) = projectPosition (contextBounds context) frame (resolvedEdgeTarget edgeValue)
-   in "<line class=\"mesh-edge"
-        <> selectedClass selected
-        <> "\" x1=\""
-        <> coordinate sourceX
-        <> "\" y1=\""
-        <> coordinate sourceY
-        <> "\" x2=\""
-        <> coordinate targetX
-        <> "\" y2=\""
-        <> coordinate targetY
-        <> "\"/>"
-
-renderMeshVertex :: PictureContext -> PictureFrame -> MeshVertex -> String
-renderMeshVertex context frame vertexValue =
-  let selected =
-        Set.member
-          (meshVertexObjectIdentifier vertexValue)
-          (selectedObjectIdentifiers (contextFlag context))
-      (x, y) = projectPosition (contextBounds context) frame (meshVertexPosition vertexValue)
-   in "<circle class=\"mesh-vertex"
-        <> selectedClass selected
-        <> "\" cx=\""
-        <> coordinate x
-        <> "\" cy=\""
-        <> coordinate y
-        <> "\" r=\""
-        <> if selected then "7\"/>" else "4.5\"/>"
-
-renderGeometrySelectionLabels :: PictureContext -> [String]
-renderGeometrySelectionLabels context =
-  let flag = contextFlag context
-      labelledObjects = [selectedFace flag, selectedEdge flag, selectedVertex flag]
-   in fmap
-        (\objectValue ->
-           let (x, y) = projectPosition (contextBounds context) geometryFrame (categoryObjectPosition objectValue)
-            in svgText
-                 "mono profile-value"
-                 (x + 10)
-                 (y - 10)
-                 "start"
-                 (categoryObjectLabel objectValue)
-        )
-        labelledObjects
-
-renderCategoryPanel :: FlagSelection -> [String]
-renderCategoryPanel flag =
-  renderPanelHeader 515 "02" "Incidence category" "one local section; no arrow thicket"
-    <> [ svgText "mono panel-note" 720 334 "middle" "SELECTED NONDEGENERATE CHAIN",
-         "<path d=\"M621 463H686\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
-         "<path d=\"M754 463H817\" stroke=\"#72d6c2\" stroke-width=\"2.5\" marker-end=\"url(#arrow)\"/>",
-         "<path d=\"M609 421C665 365 794 365 850 421\" fill=\"none\" stroke=\"#bc8cff\" stroke-opacity=\".68\" stroke-width=\"1.6\" stroke-dasharray=\"6 7\" marker-end=\"url(#arrow)\"/>",
-         "<rect x=\"556\" y=\"430\" width=\"66\" height=\"66\" rx=\"14\" fill=\"#ffad66\" fill-opacity=\".18\" stroke=\"#ffad66\" stroke-width=\"2\"/>",
-         "<rect x=\"696\" y=\"439\" width=\"48\" height=\"48\" transform=\"rotate(45 720 463)\" fill=\"#72d6c2\" fill-opacity=\".16\" stroke=\"#72d6c2\" stroke-width=\"2\"/>",
-         "<circle cx=\"850\" cy=\"463\" r=\"31\" fill=\"#f2e37e\" fill-opacity=\".14\" stroke=\"#f2e37e\" stroke-width=\"2\"/>",
-         svgText "mono node-kind" 589 407 "middle" "FACE OBJECT",
-         svgText "sans node-label" 589 471 "middle" (categoryObjectLabel (selectedFace flag)),
-         svgText "mono node-kind" 720 407 "middle" "EDGE OBJECT",
-         svgText "sans node-label" 720 471 "middle" (categoryObjectLabel (selectedEdge flag)),
-         svgText "mono node-kind" 850 407 "middle" "VERTEX OBJECT",
-         svgText "sans node-label" 850 471 "middle" (categoryObjectLabel (selectedVertex flag)),
-         svgText "mono panel-note" 720 535 "middle" "face → edge → vertex",
-         "<rect x=\"552\" y=\"570\" width=\"336\" height=\"78\" rx=\"14\" fill=\"#071217\" stroke=\"#bc8cff\" stroke-opacity=\".28\"/>",
-         svgText "serif callout-title" 720 602 "middle" "The intermediate edge survives.",
-         svgText "sans panel-note" 720 628 "middle" "Same endpoints, different paths, distinct flags."
-       ]
-
-renderNervePanel :: PictureContext -> [String]
-renderNervePanel context =
-  renderPanelHeader 980 "03" "Flag nerve" "all 66 admitted two-simplices"
-    <> ["<rect x=\"1000\" y=\"326\" width=\"370\" height=\"330\" rx=\"15\" fill=\"url(#grid)\"/>"]
-    <> fmap (renderNerveSimplex context nerveFrame) (contextNerveSimplices context)
-    <> fmap (renderNerveObject context nerveFrame) (categoryObjects (category (contextExhibit context)))
-    <> renderNerveSelectionLabel context
-
-nerveFrame :: PictureFrame
-nerveFrame = PictureFrame 1013 341 344 290
-
-renderNerveSimplex :: PictureContext -> PictureFrame -> ResolvedNerveSimplex -> String
-renderNerveSimplex context frame simplexValue =
-  let selected =
-        nerveSimplexIdentifier (resolvedSimplex simplexValue)
-          == selectedSimplexIdentifier (contextFlag context)
-   in "<polygon class=\"nerve-simplex"
-        <> selectedClass selected
-        <> "\" points=\""
-        <> renderPointList (contextBounds context) frame (resolvedSimplexVertices simplexValue)
-        <> "\"/>"
-
-renderNerveObject :: PictureContext -> PictureFrame -> CategoryObject -> String
-renderNerveObject context frame objectValue =
-  let selected =
-        Set.member
-          (categoryObjectIdentifier objectValue)
-          (selectedObjectIdentifiers (contextFlag context))
-      (x, y) = projectPosition (contextBounds context) frame (categoryObjectPosition objectValue)
-      radius = if selected then 6 else 3.4
-   in "<circle class=\""
-        <> objectKindClass (categoryObjectKind objectValue)
-        <> "\" cx=\""
-        <> coordinate x
-        <> "\" cy=\""
-        <> coordinate y
-        <> "\" r=\""
-        <> coordinate radius
-        <> "\" opacity=\""
-        <> if selected then "1\" stroke=\"#fff\" stroke-width=\"2\"/>" else ".78\"/>"
-
-renderNerveSelectionLabel :: PictureContext -> [String]
-renderNerveSelectionLabel context =
-  let flag = contextFlag context
-      faceObject = selectedFace flag
-      (x, y) = projectPosition (contextBounds context) nerveFrame (categoryObjectPosition faceObject)
-   in [ svgText "mono profile-value" (x + 12) (y - 12) "start" "selected flag",
-        svgText "mono panel-note" 1352 642 "end" (selectedSimplexIdentifier flag)
-      ]
-
-renderPanelHeader :: Double -> String -> String -> String -> [String]
-renderPanelHeader x number titleText note =
-  [ svgText "mono panel-number" (x + 22) 254 "start" number,
-    svgText "serif panel-title" (x + 22) 282 "start" titleText,
-    svgText "sans panel-note" (x + 22) 304 "start" note,
-    "<path d=\"M"
-      <> coordinate (x + 20)
-      <> " 314H"
-      <> coordinate (x + 390)
-      <> "\" stroke=\"#8bbeb2\" stroke-opacity=\".14\"/>"
-  ]
-
-renderNerveProfile :: PictureContext -> [String]
-renderNerveProfile context =
-  let counts = nerveSimplexCounts (nerve (contextExhibit context))
-      maximumCount =
-        foldl'
-          (\current count -> max current (unnormalizedSimplexCount count))
-          1
-          counts
-   in [ "<rect x=\"50\" y=\"720\" width=\"1340\" height=\"156\" rx=\"22\" fill=\"#09171c\" stroke=\"#8bbeb2\" stroke-opacity=\".20\"/>",
-        svgText "mono kicker" 76 752 "start" "NORMALIZATION PROFILE",
-        svgText "serif panel-title" 76 789 "start" "Identity insertions are cost, not geometry.",
-        svgText "sans panel-note" 76 817 "start" "Identity insertions swell dimension two to 385.",
-        "<rect x=\"76\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#72d6c2\"/>",
-        svgText "mono profile-label" 96 845 "start" "NORMALIZED",
-        "<rect x=\"196\" y=\"838\" width=\"12\" height=\"4\" rx=\"2\" fill=\"#bc8cff\"/>",
-        svgText "mono profile-label" 216 845 "start" "UNNORMALIZED"
-      ]
-        <> concat
-          ( zipWith
-              (renderProfileRow maximumCount)
-              [770, 814, 858]
-              counts
-          )
-
-renderProfileRow :: Int -> Double -> NerveSimplexCount -> [String]
-renderProfileRow maximumCount y count =
-  let normalizedWidth = profileWidth maximumCount (normalizedSimplexCount count)
-      unnormalizedWidth = profileWidth maximumCount (unnormalizedSimplexCount count)
-   in [ svgText "mono profile-label" 590 (y + 5) "end" ("DIM " <> show (nerveDimension count)),
-        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"230\" height=\"11\" rx=\"5.5\" fill=\"#13272d\"/>",
-        "<rect x=\"615\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate normalizedWidth <> "\" height=\"11\" rx=\"5.5\" fill=\"#72d6c2\"/>",
-        svgText "mono profile-value" 858 (y + 3) "start" (show (normalizedSimplexCount count)),
-        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"310\" height=\"11\" rx=\"5.5\" fill=\"#1d1830\"/>",
-        "<rect x=\"965\" y=\"" <> coordinate (y - 8) <> "\" width=\"" <> coordinate (unnormalizedWidth * 310 / 230) <> "\" height=\"11\" rx=\"5.5\" fill=\"#bc8cff\"/>",
-        svgText "mono profile-value" 1288 (y + 3) "start" (show (unnormalizedSimplexCount count))
-      ]
-
-profileWidth :: Int -> Int -> Double
-profileWidth maximumCount count =
-  230 * fromIntegral count / fromIntegral maximumCount
-
-projectPosition :: PictureBounds -> PictureFrame -> Position -> (Double, Double)
-projectPosition bounds frame positionValue =
-  ( frameLeft frame
-      + frameWidth frame
-        * normalizedCoordinate (positionX positionValue) (minimumX bounds) (maximumX bounds),
-    frameTop frame
-      + frameHeight frame
-        * normalizedCoordinate (positionY positionValue) (minimumY bounds) (maximumY bounds)
-  )
-
-normalizedCoordinate :: Double -> Double -> Double -> Double
-normalizedCoordinate value minimumValue maximumValue =
-  if maximumValue == minimumValue
-    then 0.5
-    else (value - minimumValue) / (maximumValue - minimumValue)
-
-renderPointList :: PictureBounds -> PictureFrame -> [Position] -> String
-renderPointList bounds frame =
-  unwords
-    . fmap
-      (\positionValue ->
-         let (x, y) = projectPosition bounds frame positionValue
-          in coordinate x <> "," <> coordinate y
-      )
-
-selectedClass :: Bool -> String
-selectedClass selected = if selected then " selected" else ""
-
-objectKindClass :: CategoryObjectKind -> String
-objectKindClass = \case
-  FaceObject -> "object-face"
-  EdgeObject -> "object-edge"
-  VertexObject -> "object-vertex"
-
-svgText :: String -> Double -> Double -> String -> String -> String
-svgText classes x y anchor contents =
-  "<text class=\""
-    <> classes
-    <> "\" x=\""
-    <> coordinate x
-    <> "\" y=\""
-    <> coordinate y
-    <> "\" text-anchor=\""
-    <> anchor
-    <> "\">"
-    <> escapeXml contents
-    <> "</text>"
-
-coordinate :: Double -> String
-coordinate value = showFFloat (Just 1) value ""
-
-escapeXml :: String -> String
-escapeXml = concatMap escapeCharacter
-
-escapeCharacter :: Char -> String
-escapeCharacter = \case
-  '&' -> "&amp;"
-  '<' -> "&lt;"
-  '>' -> "&gt;"
-  '\"' -> "&quot;"
-  '\'' -> "&apos;"
-  character -> [character]
diff --git a/docs/category-observatory/README.md b/docs/category-observatory/README.md
--- a/docs/category-observatory/README.md
+++ b/docs/category-observatory/README.md
@@ -17,8 +17,11 @@
 
 The `cell-complex` component interprets an `ExactCellSet` as Homology's generic
 `CellComplex2D`. The opaque carrier retains the authoritative selected handles
-and resident DCEL. `fromExactCellSet` is total because `ExactCellSet`
-construction has already discharged downward closure and handle validity.
+and immutable incidence. `fromExactCellSet` derives a cellular refinement:
+arrangement faces with multiple boundary components receive abstract cut arcs,
+while native triangulations retain their original cells. Missing bounded-face
+anchors are typed construction failures. This exhibit uses the native case;
+it does not assign fabricated straight-line geometry to abstract cuts.
 
 The interpretation is available on GHC 9.14 and newer. Existing Triangulation
 components retain the package's GHC 9.8–9.14 support range: below 9.14 Cabal
@@ -31,16 +34,16 @@
 -- the consumer package
 build-depends:
   base >= 4.22 && < 5,
-  moonlight-triangulation >= 1.4 && < 1.5,
+  moonlight-planar >= 1.0 && < 1.1,
   moonlight-planar:cell-complex >= 1.0 && < 1.1,
   moonlight-homology:cell-category >= 0.1 && < 0.2
 ```
 
 ```haskell
 import Moonlight.Homology.Pure.Topology.CellCategory (complexCategory)
-import Moonlight.Triangulation.CellComplex (fromExactCellSet)
+import Moonlight.Planar.CellComplex (fromExactCellSet)
 
-incidenceCategory = complexCategory . fromExactCellSet
+incidenceCategory = fmap complexCategory . fromExactCellSet
 ```
 
 ## What owns what
@@ -49,14 +52,14 @@
 
 ```text
 ExactCellSet
-  -> Moonlight.Triangulation.CellComplex.DCELComplex
+  -> Moonlight.Planar.CellComplex.DCELComplex
   -> Moonlight.Homology.Pure.Topology.CellCategory.ComplexCat
   -> Moonlight.Category.Simplicial.normalizedNerve
   -> checked schema-version-2 JSON
   -> Melusine's /category browser projection
 ```
 
-`moonlight-triangulation-category-observatory-export` owns only the checked
+`moonlight-planar-category-observatory-export` owns only the checked
 projection boundary. Geometry remains in Triangulation, the generic
 two-dimensional cell interface and incidence category remain in Homology, and
 the browser remains a derived view. There is no observatory-specific mesh,
@@ -92,7 +95,7 @@
 
 ```console
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-category-observatory-export \
+  moonlight-planar:exe:moonlight-planar-category-observatory-export \
   --project-file=cabal.project.planar-dev
 ```
 
@@ -101,11 +104,11 @@
 
 ```console
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-category-observatory-export \
+  moonlight-planar:exe:moonlight-planar-category-observatory-export \
   --project-file=cabal.project.planar-dev -- \
   picture compiler/foundation/moonlight-planar/docs/category-observatory/moonlight-triangulation-category-observatory.svg
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-category-observatory-export \
+  moonlight-planar:exe:moonlight-planar-category-observatory-export \
   --project-file=cabal.project.planar-dev -- \
   verify-artifacts compiler/foundation/moonlight-planar
 ```
@@ -129,7 +132,7 @@
 
 | module | responsibility |
 | --- | --- |
-| `Moonlight.Triangulation.Exhibit.CategoryObservatory` | deterministic witness and final descent |
+| `Moonlight.Planar.Exhibit.CategoryObservatory` | deterministic witness and final descent |
 | `.Model` | JSON projection types and typed obstructions |
 | `.Geometry` | mesh projection and geometric object placement |
 | `.Category` | finite object/morphism projection and inventory law |
diff --git a/docs/delaunay-compare/Main.hs b/docs/delaunay-compare/Main.hs
--- a/docs/delaunay-compare/Main.hs
+++ b/docs/delaunay-compare/Main.hs
@@ -1,9 +1,9 @@
 -- | Haskell owner of the upstream-compatible construction comparison.
 module Main (main) where
 
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (renderCompareObstruction)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Native (withNativeApi)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Suite
+import Moonlight.Planar.Bench.DelaunayCompare.Domain (renderCompareObstruction)
+import Moonlight.Planar.Bench.DelaunayCompare.Native (withNativeApi)
+import Moonlight.Planar.Bench.DelaunayCompare.Suite
   ( preflightSuite
   , suiteAgreementMessage
   , suiteBenchmarks
diff --git a/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Domain.hs b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Domain.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Domain.hs
@@ -0,0 +1,244 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | The closed case matrix and typed obstruction surface for the comparison.
+module Moonlight.Planar.Bench.DelaunayCompare.Domain
+  ( SizeBand (..)
+  , allSizeBands
+  , sizeBandLabel
+  , PointDistribution (..)
+  , allPointDistributions
+  , pointDistributionLabel
+  , Implementation (..)
+  , allImplementations
+  , implementationLabel
+  , NativeImplementation (..)
+  , nativeImplementationLabel
+  , implementationOfNative
+  , PointCount
+  , pointCountValue
+  , pointCountsFor
+  , FixtureSpec (..)
+  , allFixtureSpecs
+  , DelaunaySummary (..)
+  , NativeStatus (..)
+  , NativePreparationFailure (..)
+  , NativeRunFailure (..)
+  , CompareObstruction (..)
+  , renderCompareObstruction
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import GHC.Generics (Generic)
+import Moonlight.Planar.Types (BuildError)
+
+data SizeBand
+  = Small
+  | Big
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allSizeBands :: [SizeBand]
+allSizeBands = [Small, Big]
+
+sizeBandLabel :: SizeBand -> String
+sizeBandLabel = \case
+  Small -> "small"
+  Big -> "big"
+
+data PointDistribution
+  = LocalInsertion
+  | Uniform
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allPointDistributions :: [PointDistribution]
+allPointDistributions = [LocalInsertion, Uniform]
+
+pointDistributionLabel :: PointDistribution -> String
+pointDistributionLabel = \case
+  LocalInsertion -> "local insertion"
+  Uniform -> "uniform"
+
+data Implementation
+  = Spade
+  | SpadeHierarchy
+  | Cdt
+  | Delaunator
+  | Moonlight
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Upstream order, with Moonlight appended rather than interposed.
+allImplementations :: [Implementation]
+allImplementations = [Spade, SpadeHierarchy, Cdt, Delaunator, Moonlight]
+
+implementationLabel :: Implementation -> String
+implementationLabel = \case
+  Spade -> "spade 2"
+  SpadeHierarchy -> "spade 2 hierarchy"
+  Cdt -> "cdt"
+  Delaunator -> "delaunator"
+  Moonlight -> "moonlight-triangulation"
+
+data NativeImplementation
+  = NativeSpade
+  | NativeSpadeHierarchy
+  | NativeCdt
+  | NativeDelaunator
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+nativeImplementationLabel :: NativeImplementation -> String
+nativeImplementationLabel = implementationLabel . implementationOfNative
+
+implementationOfNative :: NativeImplementation -> Implementation
+implementationOfNative = \case
+  NativeSpade -> Spade
+  NativeSpadeHierarchy -> SpadeHierarchy
+  NativeCdt -> Cdt
+  NativeDelaunator -> Delaunator
+
+newtype PointCount = PointCount Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+pointCountValue :: PointCount -> Int
+pointCountValue (PointCount count) = count
+
+data FixtureSpec = FixtureSpec
+  { fixtureSizeBand :: !SizeBand
+  , fixturePointDistribution :: !PointDistribution
+  , fixturePointCount :: !PointCount
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+allFixtureSpecs :: [FixtureSpec]
+allFixtureSpecs =
+  concatMap
+    ( \sizeBand ->
+        liftA2
+          (FixtureSpec sizeBand)
+          allPointDistributions
+          (pointCountsFor sizeBand)
+    )
+    allSizeBands
+
+pointCountsFor :: SizeBand -> [PointCount]
+pointCountsFor = \case
+  Small -> PointCount <$> [2_000, 4_000, 6_000, 8_000, 10_000, 12_000, 14_000]
+  Big -> PointCount <$> [50_000, 100_000, 150_000, 200_000, 250_000]
+
+data DelaunaySummary = DelaunaySummary
+  { summaryVertexCount :: !Int
+  , summaryTriangleCount :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativeStatus
+  = NativeNullPointer
+  | NativeUnknownTag
+  | NativeCoordinateCountMismatch
+  | NativeDistributionConstructionFailed
+  | NativeTriangulationFailed
+  | NativePanicked
+  | NativeUnknownStatus !Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativePreparationFailure
+  = NativePreparationStatus !NativeStatus
+  | NativePreparationReturnedNull
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data NativeRunFailure
+  = NativeRunStatus !NativeStatus
+  | NativeCountExceedsHaskellInt !Integer
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data CompareObstruction
+  = RustManifestNotFound !(NonEmpty FilePath)
+  | CargoInvocationFailed !FilePath !String
+  | CargoBuildFailed !FilePath !Int !String
+  | UnsupportedDynamicLibraryHost !String
+  | DynamicLibraryOpenFailed !FilePath !String
+  | DynamicSymbolLoadFailed !FilePath !String !String
+  | FixtureGenerationFailed !FixtureSpec !NativeStatus
+  | FixturePreparationFailed !FixtureSpec !NativeImplementation !NativeStatus
+  | NativePreparedNull !FixtureSpec !NativeImplementation
+  | MoonlightConstructionFailed !FixtureSpec !BuildError
+  | NativeConstructionFailed !FixtureSpec !NativeImplementation !NativeRunFailure
+  | SummaryDisagreement !FixtureSpec !Implementation !DelaunaySummary !DelaunaySummary
+  | ComparisonObstructions !(NonEmpty CompareObstruction)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+renderCompareObstruction :: CompareObstruction -> String
+renderCompareObstruction = \case
+  RustManifestNotFound candidates ->
+    "Rust referent manifest not found; checked " <> show (NonEmpty.toList candidates)
+  CargoInvocationFailed manifest details ->
+    "could not invoke Cargo for " <> manifest <> ": " <> details
+  CargoBuildFailed manifest exitCode details ->
+    "Cargo failed for "
+      <> manifest
+      <> " with exit code "
+      <> show exitCode
+      <> if null details then "" else ":\n" <> details
+  UnsupportedDynamicLibraryHost host ->
+    "delaunay-compare does not know the dynamic-library suffix for host " <> show host
+  DynamicLibraryOpenFailed path details ->
+    "could not load Rust referent library " <> path <> ": " <> details
+  DynamicSymbolLoadFailed path symbol details ->
+    "could not load symbol " <> symbol <> " from " <> path <> ": " <> details
+  FixtureGenerationFailed fixture status ->
+    "upstream fixture generation failed for " <> renderFixture fixture <> ": " <> show status
+  FixturePreparationFailed fixture implementation status ->
+    "native preparation failed for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> ": "
+      <> show status
+  NativePreparedNull fixture implementation ->
+    "native preparation returned a null handle for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+  MoonlightConstructionFailed fixture buildError ->
+    "Moonlight construction failed for " <> renderFixture fixture <> ": " <> show buildError
+  NativeConstructionFailed fixture implementation failure ->
+    "native construction failed for "
+      <> nativeImplementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> ": "
+      <> show failure
+  SummaryDisagreement fixture implementation expected observed ->
+    "construction summary disagreement for "
+      <> implementationLabel implementation
+      <> " on "
+      <> renderFixture fixture
+      <> "; Moonlight="
+      <> show expected
+      <> ", observed="
+      <> show observed
+  ComparisonObstructions obstructions ->
+    unlines ("delaunay comparison obstructed:" : fmap (("  - " <>) . renderCompareObstruction) (NonEmpty.toList obstructions))
+
+renderFixture :: FixtureSpec -> String
+renderFixture fixture =
+  sizeBandLabel (fixtureSizeBand fixture)
+    <> "/"
+    <> pointDistributionLabel (fixturePointDistribution fixture)
+    <> "/"
+    <> show (pointCountValue (fixturePointCount fixture))
diff --git a/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Native.hs b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Native.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Native.hs
@@ -0,0 +1,307 @@
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Resource-safe dynamic boundary to the Rust crates under comparison.
+module Moonlight.Planar.Bench.DelaunayCompare.Native
+  ( NativeApi
+  , NativeFixture
+  , GeneratedCoordinates
+  , withNativeApi
+  , generateCoordinates
+  , generatedCoordinatePairs
+  , prepareNativeFixture
+  , runNativeFixture
+  , releaseNativeFixture
+  ) where
+
+import Control.Exception (IOException, bracket, displayException, try)
+import Control.Monad (filterM)
+import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT)
+import Data.Bifunctor (first)
+import Data.Int (Int32)
+import Data.List (unfoldr)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Bench.DelaunayCompare.Domain
+import Foreign.C.Types (CDouble, CInt (..), CSize (..))
+import Foreign.Marshal.Alloc (alloca)
+import Foreign.Ptr (FunPtr, Ptr, nullPtr)
+import Foreign.Storable (peek, poke)
+import qualified Data.Vector as Vector
+import qualified Data.Vector.Storable as StorableVector
+import qualified Data.Vector.Storable.Mutable as MutableStorableVector
+import System.Directory (doesFileExist, makeAbsolute)
+import System.Environment (lookupEnv)
+import System.Exit (ExitCode (..))
+import System.FilePath ((</>), takeDirectory)
+import System.Info (os)
+import System.Posix.DynamicLinker
+  ( DL
+  , RTLDFlags (RTLD_LOCAL, RTLD_NOW)
+  , dlclose
+  , dlopen
+  , dlsym
+  )
+import System.Process
+  ( CreateProcess (cwd)
+  , proc
+  , readCreateProcessWithExitCode
+  )
+
+type GenerateFunction = CInt -> CSize -> Ptr CDouble -> CSize -> IO CInt
+type PrepareFunction = CInt -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> IO CInt
+type RunFunction = Ptr () -> Ptr CSize -> Ptr CSize -> IO CInt
+type ReleaseFunction = Ptr () -> IO ()
+
+foreign import ccall safe "dynamic"
+  bindGenerateFunction :: FunPtr GenerateFunction -> GenerateFunction
+
+foreign import ccall safe "dynamic"
+  bindPrepareFunction :: FunPtr PrepareFunction -> PrepareFunction
+
+foreign import ccall safe "dynamic"
+  bindRunFunction :: FunPtr RunFunction -> RunFunction
+
+foreign import ccall unsafe "dynamic"
+  bindReleaseFunction :: FunPtr ReleaseFunction -> ReleaseFunction
+
+data NativeApi = NativeApi
+  { nativeGenerate :: !GenerateFunction
+  , nativePrepare :: !PrepareFunction
+  , nativeRun :: !RunFunction
+  , nativeRelease :: !ReleaseFunction
+  }
+
+newtype NativeFixture = NativeFixture (Ptr ())
+
+data GeneratedCoordinates = GeneratedCoordinates
+  { generatedPointCount :: !PointCount
+  , generatedCoordinateValues :: !(StorableVector.Vector CDouble)
+  }
+
+withNativeApi
+  :: (NativeApi -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withNativeApi action = do
+  manifestResult <- resolveRustManifest
+  case manifestResult of
+    Left obstruction -> pure (Left obstruction)
+    Right manifest -> do
+      libraryResult <- buildRustLibrary manifest
+      case libraryResult of
+        Left obstruction -> pure (Left obstruction)
+        Right libraryPath -> withLoadedNativeApi libraryPath action
+
+resolveRustManifest :: IO (Either CompareObstruction FilePath)
+resolveRustManifest = do
+  configured <- lookupEnv "MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST"
+  let candidates =
+        case configured of
+          Just path -> path :| []
+          Nothing ->
+            "docs/delaunay-compare/rust/Cargo.toml"
+              :| [ "foundation/moonlight-planar/docs/delaunay-compare/rust/Cargo.toml"
+                 , "compiler/foundation/moonlight-planar/docs/delaunay-compare/rust/Cargo.toml"
+                 ]
+  existing <- filterM doesFileExist (NonEmpty.toList candidates)
+  case existing of
+    manifest : _ -> Right <$> makeAbsolute manifest
+    [] -> pure (Left (RustManifestNotFound candidates))
+
+buildRustLibrary :: FilePath -> IO (Either CompareObstruction FilePath)
+buildRustLibrary manifest = do
+  libraryNameResult <- dynamicLibraryName
+  case libraryNameResult of
+    Left obstruction -> pure (Left obstruction)
+    Right libraryName -> do
+      let rustRoot = takeDirectory manifest
+          targetDirectory = rustRoot </> "target"
+          command =
+            ( proc
+                "cargo"
+                [ "build"
+                , "--quiet"
+                , "--release"
+                , "--lib"
+                , "--manifest-path"
+                , manifest
+                , "--target-dir"
+                , targetDirectory
+                ]
+            )
+              { cwd = Just rustRoot
+              }
+      invocation <- try @IOException (readCreateProcessWithExitCode command "")
+      pure $
+        case invocation of
+          Left exception ->
+            Left (CargoInvocationFailed manifest (displayException exception))
+          Right (ExitFailure exitCode, standardOutput, standardError) ->
+            Left
+              ( CargoBuildFailed
+                  manifest
+                  exitCode
+                  (standardError <> standardOutput)
+              )
+          Right (ExitSuccess, _, _) ->
+            Right (targetDirectory </> "release" </> libraryName)
+
+dynamicLibraryName :: IO (Either CompareObstruction FilePath)
+dynamicLibraryName =
+  pure $
+    case os of
+      "darwin" -> Right "libmoonlight_delaunay_compare_referents.dylib"
+      "linux" -> Right "libmoonlight_delaunay_compare_referents.so"
+      host -> Left (UnsupportedDynamicLibraryHost host)
+
+withLoadedNativeApi
+  :: FilePath
+  -> (NativeApi -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withLoadedNativeApi libraryPath action = do
+  opened <- try @IOException (dlopen libraryPath [RTLD_NOW, RTLD_LOCAL])
+  case opened of
+    Left exception ->
+      pure (Left (DynamicLibraryOpenFailed libraryPath (displayException exception)))
+    Right handle ->
+      bracket
+        (pure handle)
+        dlclose
+        ( \loadedHandle -> do
+            apiResult <- loadNativeApi libraryPath loadedHandle
+            case apiResult of
+              Left obstruction -> pure (Left obstruction)
+              Right api -> action api
+        )
+
+loadNativeApi :: FilePath -> DL -> IO (Either CompareObstruction NativeApi)
+loadNativeApi libraryPath handle =
+  runExceptT $
+    NativeApi
+      <$> loadSymbol libraryPath handle "delaunay_compare_generate" bindGenerateFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_prepare" bindPrepareFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_run" bindRunFunction
+      <*> loadSymbol libraryPath handle "delaunay_compare_release" bindReleaseFunction
+
+loadSymbol
+  :: FilePath
+  -> DL
+  -> String
+  -> (FunPtr function -> boundFunction)
+  -> ExceptT CompareObstruction IO boundFunction
+loadSymbol libraryPath handle symbol bind =
+  ExceptT $ do
+    loaded <- try @IOException (dlsym handle symbol)
+    pure $
+      first
+        (DynamicSymbolLoadFailed libraryPath symbol . displayException)
+        (bind <$> loaded)
+
+generateCoordinates
+  :: NativeApi
+  -> FixtureSpec
+  -> IO (Either NativeStatus GeneratedCoordinates)
+generateCoordinates api fixture = do
+  let pointCount = fixturePointCount fixture
+      coordinateCount = 2 * pointCountValue pointCount
+  mutableCoordinates <- MutableStorableVector.new coordinateCount
+  statusCode <-
+    MutableStorableVector.unsafeWith mutableCoordinates $ \coordinatePointer ->
+      nativeGenerate api
+        (distributionTag (fixturePointDistribution fixture))
+        (fromIntegral (pointCountValue pointCount))
+        coordinatePointer
+        (fromIntegral coordinateCount)
+  case decodeNativeStatus statusCode of
+    Left status -> pure (Left status)
+    Right () -> do
+      coordinates <- StorableVector.unsafeFreeze mutableCoordinates
+      pure (Right (GeneratedCoordinates pointCount coordinates))
+
+generatedCoordinatePairs :: GeneratedCoordinates -> Vector.Vector (Double, Double)
+generatedCoordinatePairs generated =
+  Vector.fromList
+    ( unfoldr
+        takeCoordinatePair
+        (realToFrac <$> StorableVector.toList (generatedCoordinateValues generated))
+    )
+ where
+  takeCoordinatePair :: [Double] -> Maybe ((Double, Double), [Double])
+  takeCoordinatePair = \case
+    x : y : remaining -> Just ((x, y), remaining)
+    _ -> Nothing
+
+prepareNativeFixture
+  :: NativeApi
+  -> NativeImplementation
+  -> GeneratedCoordinates
+  -> IO (Either NativePreparationFailure NativeFixture)
+prepareNativeFixture api implementation coordinates =
+  alloca $ \preparedOutput -> do
+    poke preparedOutput nullPtr
+    statusCode <-
+      StorableVector.unsafeWith (generatedCoordinateValues coordinates) $ \coordinatePointer ->
+        nativePrepare api
+          (implementationTag implementation)
+          coordinatePointer
+          (fromIntegral (pointCountValue (generatedPointCount coordinates)))
+          preparedOutput
+    case decodeNativeStatus statusCode of
+      Left status -> pure (Left (NativePreparationStatus status))
+      Right () -> do
+        prepared <- peek preparedOutput
+        pure $
+          if prepared == nullPtr
+            then Left NativePreparationReturnedNull
+            else Right (NativeFixture prepared)
+
+runNativeFixture
+  :: NativeApi
+  -> NativeFixture
+  -> IO (Either NativeRunFailure DelaunaySummary)
+runNativeFixture api (NativeFixture prepared) =
+  alloca $ \vertexCountOutput ->
+    alloca $ \triangleCountOutput -> do
+      statusCode <- nativeRun api prepared vertexCountOutput triangleCountOutput
+      case decodeNativeStatus statusCode of
+        Left status -> pure (Left (NativeRunStatus status))
+        Right () -> do
+          vertexCount <- peek vertexCountOutput
+          triangleCount <- peek triangleCountOutput
+          pure $ DelaunaySummary <$> cSizeToInt vertexCount <*> cSizeToInt triangleCount
+
+releaseNativeFixture :: NativeApi -> NativeFixture -> IO ()
+releaseNativeFixture api (NativeFixture prepared) = nativeRelease api prepared
+
+decodeNativeStatus :: CInt -> Either NativeStatus ()
+decodeNativeStatus statusCode =
+  case fromIntegral statusCode :: Int32 of
+    0 -> Right ()
+    1 -> Left NativeNullPointer
+    2 -> Left NativeUnknownTag
+    3 -> Left NativeCoordinateCountMismatch
+    4 -> Left NativeDistributionConstructionFailed
+    5 -> Left NativeTriangulationFailed
+    6 -> Left NativePanicked
+    unknown -> Left (NativeUnknownStatus (fromIntegral unknown))
+
+cSizeToInt :: CSize -> Either NativeRunFailure Int
+cSizeToInt value =
+  let integerValue = toInteger value
+   in if integerValue > toInteger (maxBound :: Int)
+        then Left (NativeCountExceedsHaskellInt integerValue)
+        else Right (fromInteger integerValue)
+
+distributionTag :: PointDistribution -> CInt
+distributionTag = \case
+  LocalInsertion -> 0
+  Uniform -> 1
+
+implementationTag :: NativeImplementation -> CInt
+implementationTag = \case
+  NativeSpade -> 0
+  NativeSpadeHierarchy -> 1
+  NativeCdt -> 2
+  NativeDelaunator -> 3
diff --git a/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Picture.hs b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Picture.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Picture.hs
@@ -0,0 +1,466 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Typed receipt parsing and pure Spade-style SVG projection.
+module Moonlight.Planar.Bench.DelaunayCompare.Picture
+  ( BenchmarkReceipt
+  , PictureObstruction (..)
+  , parseBenchmarkReceipt
+  , pictureFileName
+  , renderComparisonPicture
+  , renderPictureObstruction
+  ) where
+
+import Data.List (group, sort, sortOn)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (mapMaybe)
+import qualified Data.Set as Set
+import Moonlight.Planar.Bench.DelaunayCompare.Domain
+import Numeric (showFFloat)
+import Text.Read (readMaybe)
+
+data ObservationKey = ObservationKey
+  { observationFixture :: !FixtureSpec
+  , observationImplementation :: !Implementation
+  }
+  deriving stock (Eq, Ord, Show)
+
+data BenchmarkObservation = BenchmarkObservation
+  { benchmarkKey :: !ObservationKey
+  , benchmarkMeanPicoseconds :: !Double
+  , benchmarkSpreadPicoseconds :: !Double
+  }
+  deriving stock (Eq, Show)
+
+newtype BenchmarkReceipt = BenchmarkReceipt
+  { receiptObservations :: Map.Map ObservationKey BenchmarkObservation
+  }
+
+data PictureObstruction
+  = MissingCsvHeader
+  | UnexpectedCsvHeader !String
+  | MalformedCsvRow !Int !String
+  | UnknownBenchmarkName !Int !String
+  | InvalidBenchmarkScalar !Int !String !String
+  | DuplicateBenchmarkObservations ![ObservationKey]
+  | MissingBenchmarkObservations ![ObservationKey]
+  deriving stock (Eq, Show)
+
+parseBenchmarkReceipt :: String -> Either PictureObstruction BenchmarkReceipt
+parseBenchmarkReceipt source =
+  case lines source of
+    [] -> Left MissingCsvHeader
+    header : rows
+      | header /= expectedCsvHeader -> Left (UnexpectedCsvHeader header)
+      | otherwise -> do
+          observations <- traverse (uncurry parseObservationRow) (zip [2 ..] rows)
+          let keys = benchmarkKey <$> observations
+              duplicateKeys =
+                mapMaybe
+                  ( \case
+                      key : _ : _ -> Just key
+                      _ -> Nothing
+                  )
+                  (group (sort keys))
+              observationMap = Map.fromList ((\observation -> (benchmarkKey observation, observation)) <$> observations)
+              missingKeys = Set.toList (expectedObservationKeys `Set.difference` Map.keysSet observationMap)
+          case (duplicateKeys, missingKeys) of
+            (duplicate : duplicates, _) -> Left (DuplicateBenchmarkObservations (duplicate : duplicates))
+            ([], missing : remaining) -> Left (MissingBenchmarkObservations (missing : remaining))
+            ([], []) -> Right (BenchmarkReceipt observationMap)
+
+renderComparisonPicture
+  :: SizeBand
+  -> BenchmarkReceipt
+  -> String
+renderComparisonPicture sizeBand receipt =
+  renderSvg sizeBand pointCounts axisMaximum panels
+ where
+  pointCounts = pointCountValue <$> pointCountsFor sizeBand
+  panels = picturePanel receipt sizeBand <$> allPointDistributions
+  allObservations = concatMap (concatMap pictureSeriesObservations . picturePanelSeries) panels
+  maximumMilliseconds =
+    foldr
+      (max . observationUpperMilliseconds)
+      0
+      allObservations
+  axisMaximum = niceCeiling maximumMilliseconds
+
+pictureFileName :: SizeBand -> FilePath
+pictureFileName = \case
+  Small -> "moonlight-delaunay-compare-small.svg"
+  Big -> "moonlight-delaunay-compare-big.svg"
+
+renderPictureObstruction :: PictureObstruction -> String
+renderPictureObstruction = \case
+  MissingCsvHeader -> "benchmark CSV has no header"
+  UnexpectedCsvHeader header ->
+    "benchmark CSV header was " <> show header <> "; expected " <> show expectedCsvHeader
+  MalformedCsvRow lineNumber row ->
+    "benchmark CSV row " <> show lineNumber <> " is not a three-column tasty-bench row: " <> show row
+  UnknownBenchmarkName lineNumber name ->
+    "benchmark CSV row " <> show lineNumber <> " names an unknown comparison case: " <> show name
+  InvalidBenchmarkScalar lineNumber field value ->
+    "benchmark CSV row "
+      <> show lineNumber
+      <> " has invalid "
+      <> field
+      <> ": "
+      <> show value
+  DuplicateBenchmarkObservations keys ->
+    "benchmark CSV repeats comparison cases: " <> show keys
+  MissingBenchmarkObservations keys ->
+    "benchmark CSV omits comparison cases: " <> show keys
+
+expectedCsvHeader :: String
+expectedCsvHeader = "Name,Mean (ps),2*Stdev (ps)"
+
+expectedObservationKeys :: Set.Set ObservationKey
+expectedObservationKeys =
+  Set.fromList (liftA2 ObservationKey allFixtureSpecs allImplementations)
+
+expectedBenchmarkNames :: Map.Map String ObservationKey
+expectedBenchmarkNames =
+  Map.fromList ((\key -> (benchmarkName key, key)) <$> Set.toList expectedObservationKeys)
+
+benchmarkName :: ObservationKey -> String
+benchmarkName key =
+  "All.comparison: creation benchmark ("
+    <> sizeBandLabel (fixtureSizeBand fixture)
+    <> ")."
+    <> implementationLabel (observationImplementation key)
+    <> "."
+    <> pointDistributionLabel (fixturePointDistribution fixture)
+    <> "."
+    <> show (pointCountValue (fixturePointCount fixture))
+ where
+  fixture = observationFixture key
+
+parseObservationRow :: Int -> String -> Either PictureObstruction BenchmarkObservation
+parseObservationRow lineNumber row =
+  case csvTriple row of
+    Nothing -> Left (MalformedCsvRow lineNumber row)
+    Just (name, rawMean, rawSpread) -> do
+      key <- maybe (Left (UnknownBenchmarkName lineNumber name)) Right (Map.lookup name expectedBenchmarkNames)
+      meanPicoseconds <- parseNonNegativeScalar lineNumber "mean picoseconds" rawMean
+      spreadPicoseconds <- parseNonNegativeScalar lineNumber "two-standard-deviation spread" rawSpread
+      pure
+        BenchmarkObservation
+          { benchmarkKey = key
+          , benchmarkMeanPicoseconds = meanPicoseconds
+          , benchmarkSpreadPicoseconds = spreadPicoseconds
+          }
+
+csvTriple :: String -> Maybe (String, String, String)
+csvTriple row =
+  case break (== ',') row of
+    (firstColumn, ',' : remaining) ->
+      case break (== ',') remaining of
+        (secondColumn, ',' : thirdColumn)
+          | ',' `notElem` thirdColumn -> Just (firstColumn, secondColumn, thirdColumn)
+        _ -> Nothing
+    _ -> Nothing
+
+parseNonNegativeScalar :: Int -> String -> String -> Either PictureObstruction Double
+parseNonNegativeScalar lineNumber field rawValue =
+  case readMaybe rawValue of
+    Just value
+      | value >= 0 && not (isInfinite value) && not (isNaN value) -> Right value
+    _ -> Left (InvalidBenchmarkScalar lineNumber field rawValue)
+
+data PictureSeries = PictureSeries
+  { pictureSeriesImplementation :: !Implementation
+  , pictureSeriesObservations :: ![BenchmarkObservation]
+  }
+
+data PicturePanel = PicturePanel
+  { picturePanelDistribution :: !PointDistribution
+  , picturePanelSeries :: ![PictureSeries]
+  }
+
+picturePanel
+  :: BenchmarkReceipt
+  -> SizeBand
+  -> PointDistribution
+  -> PicturePanel
+picturePanel receipt sizeBand distribution =
+  PicturePanel distribution (pictureSeries receipt sizeBand distribution <$> allImplementations)
+
+pictureSeries
+  :: BenchmarkReceipt
+  -> SizeBand
+  -> PointDistribution
+  -> Implementation
+  -> PictureSeries
+pictureSeries receipt sizeBand distribution implementation =
+  PictureSeries implementation observations
+ where
+  observations =
+    sortOn (pointCountValue . fixturePointCount . observationFixture . benchmarkKey)
+      [ observation
+      | observation <- Map.elems (receiptObservations receipt)
+      , let key = benchmarkKey observation
+            fixture = observationFixture key
+      , fixtureSizeBand fixture == sizeBand
+      , fixturePointDistribution fixture == distribution
+      , observationImplementation key == implementation
+      ]
+
+data PictureLayout = PictureLayout
+  { pictureWidth :: !Double
+  , pictureHeight :: !Double
+  , picturePlotTop :: !Double
+  , picturePlotBottom :: !Double
+  , picturePanelWidth :: !Double
+  , picturePanelLefts :: ![Double]
+  }
+
+pictureLayout :: PictureLayout
+pictureLayout =
+  PictureLayout
+    { pictureWidth = 1280
+    , pictureHeight = 720
+    , picturePlotTop = 168
+    , picturePlotBottom = 610
+    , picturePanelWidth = 540
+    , picturePanelLefts = [70, 670]
+    }
+
+renderSvg :: SizeBand -> [Int] -> Double -> [PicturePanel] -> String
+renderSvg sizeBand pointCounts axisMaximum panels =
+  unlines
+    ( [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+      , "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "
+          <> coordinate (pictureWidth pictureLayout)
+          <> " "
+          <> coordinate (pictureHeight pictureLayout)
+          <> "\" role=\"img\" aria-labelledby=\"title description\">"
+      , "<title id=\"title\">" <> pictureTitle sizeBand <> "</title>"
+      , "<desc id=\"description\">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>"
+      , "<defs>"
+      , "  <pattern id=\"triangulation-mesh\" width=\"54\" height=\"46\" patternUnits=\"userSpaceOnUse\">"
+      , "    <path d=\"M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46\" fill=\"none\" stroke=\"rgb(255,140,0)\" stroke-width=\"0.7\"/>"
+      , "  </pattern>"
+      , "  <clipPath id=\"header-mesh-clip\"><rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" rx=\"20\"/></clipPath>"
+      , "</defs>"
+      , "<rect width=\""
+          <> coordinate (pictureWidth pictureLayout)
+          <> "\" height=\""
+          <> coordinate (pictureHeight pictureLayout)
+          <> "\" fill=\"white\"/>"
+      , "<rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" fill=\"url(#triangulation-mesh)\" opacity=\"0.10\" clip-path=\"url(#header-mesh-clip)\"/>"
+      , svgText 640 34 "middle" 24 "500" (pictureTitle sizeBand)
+      , svgText 640 61 "middle" 13 "400" "Moonlight against Spade's construction referents · CPU time · lower is faster"
+      ]
+        <> renderLegend
+        <> concat (zipWith (renderPanel pointCounts axisMaximum) (picturePanelLefts pictureLayout) panels)
+        <> [ svgText 640 704 "middle" 11 "400" "Whiskers show tasty-bench's reported 2× standard-deviation spread."
+           , "</svg>"
+           ]
+    )
+
+pictureTitle :: SizeBand -> String
+pictureTitle = \case
+  Small -> "Moonlight Delaunay construction · small point sets"
+  Big -> "Moonlight Delaunay construction · big point sets"
+
+renderLegend :: [String]
+renderLegend =
+  concat
+    ( zipWith
+        renderLegendItem
+        [105, 315, 560, 735, 940]
+        allImplementations
+    )
+
+renderLegendItem :: Double -> Implementation -> [String]
+renderLegendItem x implementation =
+  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
+  , "  <line x1=\"" <> coordinate x <> "\" y1=\"101\" x2=\"" <> coordinate (x + 34) <> "\" y2=\"101\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\"" <> seriesDash implementation <> "/>"
+  , "  " <> renderMarker implementation (x + 17) 101
+  , "  " <> svgText (x + 43) 105 "start" 12 "400" (implementationLabel implementation)
+  , "</g>"
+  ]
+
+renderPanel :: [Int] -> Double -> Double -> PicturePanel -> [String]
+renderPanel pointCounts axisMaximum panelLeft panel =
+  [ "<g aria-label=\"" <> panelLabel <> "\">"
+  , svgText (panelLeft + panelWidth / 2) 144 "middle" 15 "500" panelLabel
+  ]
+    <> concatMap (renderHorizontalGrid panelLeft panelWidth axisMaximum) [0 .. 5]
+    <> concatMap (renderVerticalTick panelLeft panelWidth pointCounts) pointCounts
+    <> [ "<rect x=\"" <> coordinate panelLeft <> "\" y=\"" <> coordinate plotTop <> "\" width=\"" <> coordinate panelWidth <> "\" height=\"" <> coordinate plotHeight <> "\" fill=\"none\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
+       , svgText (panelLeft + panelWidth / 2) 664 "middle" 12 "400" "input size (points)"
+       , "<text x=\"" <> coordinate (panelLeft - 55) <> "\" y=\"" <> coordinate (plotTop + plotHeight / 2) <> "\" text-anchor=\"middle\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"12\" fill=\"rgb(20,20,20)\" transform=\"rotate(-90 " <> coordinate (panelLeft - 55) <> " " <> coordinate (plotTop + plotHeight / 2) <> ")\">CPU time (ms)</text>"
+       ]
+    <> concatMap (renderSeries panelLeft panelWidth pointCounts axisMaximum) (picturePanelSeries panel)
+    <> ["</g>"]
+ where
+  panelLabel = pointDistributionLabel (picturePanelDistribution panel)
+  panelWidth = picturePanelWidth pictureLayout
+  plotTop = picturePlotTop pictureLayout
+  plotHeight = picturePlotBottom pictureLayout - plotTop
+
+renderHorizontalGrid :: Double -> Double -> Double -> Int -> [String]
+renderHorizontalGrid panelLeft panelWidth axisMaximum tickIndex =
+  [ "<line x1=\"" <> coordinate panelLeft <> "\" y1=\"" <> coordinate y <> "\" x2=\"" <> coordinate (panelLeft + panelWidth) <> "\" y2=\"" <> coordinate y <> "\" stroke=\"rgb(170,170,170)\" stroke-width=\"0.7\" stroke-dasharray=\"2 4\"/>"
+  , svgText (panelLeft - 10) (y + 4) "end" 11 "400" (axisLabel tickValue)
+  ]
+ where
+  tickValue = axisMaximum * fromIntegral tickIndex / 5
+  y = pictureY axisMaximum tickValue
+
+renderVerticalTick :: Double -> Double -> [Int] -> Int -> [String]
+renderVerticalTick panelLeft panelWidth pointCounts pointCount =
+  [ "<line x1=\"" <> coordinate x <> "\" y1=\"610\" x2=\"" <> coordinate x <> "\" y2=\"616\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
+  , svgText x 635 "middle" 11 "400" (pointCountLabel pointCount)
+  ]
+ where
+  x = pictureX panelLeft panelWidth pointCounts pointCount
+
+renderSeries :: Double -> Double -> [Int] -> Double -> PictureSeries -> [String]
+renderSeries panelLeft panelWidth pointCounts axisMaximum series =
+  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
+  , "  <polyline points=\"" <> unwords (observationPoint <$> observations) <> "\" fill=\"none\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\" stroke-linecap=\"round\" stroke-linejoin=\"round\"" <> seriesDash implementation <> "/>"
+  ]
+    <> concatMap renderObservation observations
+    <> ["</g>"]
+ where
+  implementation = pictureSeriesImplementation series
+  observations = pictureSeriesObservations series
+  observationPoint observation =
+    coordinate (observationX observation) <> "," <> coordinate (observationY observation)
+  observationX =
+    pictureX panelLeft panelWidth pointCounts
+      . pointCountValue
+      . fixturePointCount
+      . observationFixture
+      . benchmarkKey
+  observationY = pictureY axisMaximum . observationMeanMilliseconds
+  renderObservation observation =
+    [ "  <line x1=\"" <> coordinate x <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate x <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate upperY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate lowerY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
+    , "  " <> renderMarker implementation x y
+    ]
+   where
+    x = observationX observation
+    y = observationY observation
+    upperY = pictureY axisMaximum (observationUpperMilliseconds observation)
+    lowerY = pictureY axisMaximum (observationLowerMilliseconds observation)
+
+implementationColor :: Implementation -> String
+implementationColor = \case
+  Spade -> "rgb(0,0,139)"
+  SpadeHierarchy -> "rgb(139,0,139)"
+  Cdt -> "rgb(178,34,34)"
+  Delaunator -> "rgb(0,139,139)"
+  Moonlight -> "rgb(255,140,0)"
+
+seriesStrokeWidth :: Implementation -> String
+seriesStrokeWidth = \case
+  Moonlight -> "3.2"
+  _ -> "2.0"
+
+seriesDash :: Implementation -> String
+seriesDash = \case
+  SpadeHierarchy -> " stroke-dasharray=\"7 4\""
+  _ -> ""
+
+renderMarker :: Implementation -> Double -> Double -> String
+renderMarker implementation x y =
+  case implementation of
+    Spade -> circle 3.2
+    SpadeHierarchy ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.3) <> " L " <> coordinate (x - 4.1) <> " " <> coordinate (y + 3.5) <> " L " <> coordinate (x + 4.1) <> " " <> coordinate (y + 3.5) <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Cdt ->
+      "<rect x=\"" <> coordinate (x - 3.2) <> "\" y=\"" <> coordinate (y - 3.2) <> "\" width=\"6.4\" height=\"6.4\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Delaunator ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.2) <> " L " <> coordinate (x + 4.2) <> " " <> coordinate y <> " L " <> coordinate x <> " " <> coordinate (y + 4.2) <> " L " <> coordinate (x - 4.2) <> " " <> coordinate y <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+    Moonlight ->
+      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 5.4) <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y - 1.7) <> " L " <> coordinate (x + 5.4) <> " " <> coordinate y <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate x <> " " <> coordinate (y + 5.4) <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate (x - 5.4) <> " " <> coordinate y <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y - 1.7) <> " Z\" fill=\"" <> color <> "\" stroke=\"white\" stroke-width=\"1\"/>"
+ where
+  color = implementationColor implementation
+  circle radius =
+    "<circle cx=\"" <> coordinate x <> "\" cy=\"" <> coordinate y <> "\" r=\"" <> coordinate radius <> "\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
+
+pictureX :: Double -> Double -> [Int] -> Int -> Double
+pictureX panelLeft panelWidth pointCounts pointCount =
+  case pointCounts of
+    [] -> panelLeft
+    firstCount : remainingCounts ->
+      let minimumCount = firstCount
+          maximumCount = foldr max firstCount remainingCounts
+          countRange = maximumCount - minimumCount
+       in if countRange == 0
+            then panelLeft + panelWidth / 2
+            else
+              panelLeft
+                + panelWidth
+                  * fromIntegral (pointCount - minimumCount)
+                  / fromIntegral countRange
+
+pictureY :: Double -> Double -> Double
+pictureY axisMaximum milliseconds =
+  picturePlotBottom pictureLayout
+    - plotHeight * min axisMaximum (max 0 milliseconds) / axisMaximum
+ where
+  plotHeight = picturePlotBottom pictureLayout - picturePlotTop pictureLayout
+
+observationMeanMilliseconds :: BenchmarkObservation -> Double
+observationMeanMilliseconds = picosecondsToMilliseconds . benchmarkMeanPicoseconds
+
+observationUpperMilliseconds :: BenchmarkObservation -> Double
+observationUpperMilliseconds observation =
+  picosecondsToMilliseconds
+    (benchmarkMeanPicoseconds observation + benchmarkSpreadPicoseconds observation)
+
+observationLowerMilliseconds :: BenchmarkObservation -> Double
+observationLowerMilliseconds observation =
+  picosecondsToMilliseconds
+    (max 0 (benchmarkMeanPicoseconds observation - benchmarkSpreadPicoseconds observation))
+
+picosecondsToMilliseconds :: Double -> Double
+picosecondsToMilliseconds picoseconds = picoseconds / 1.0e9
+
+niceCeiling :: Double -> Double
+niceCeiling value
+  | value <= 0 = 1
+  | otherwise = niceFraction * magnitude
+ where
+  magnitude = 10 ** fromIntegral (floor (logBase 10 value) :: Int)
+  fraction = value / magnitude
+  niceFraction
+    | fraction <= 1 = 1
+    | fraction <= 2 = 2
+    | fraction <= 5 = 5
+    | otherwise = 10
+
+axisLabel :: Double -> String
+axisLabel value
+  | value >= 10 = showFFloat (Just 0) value ""
+  | value >= 1 = showFFloat (Just 1) value ""
+  | otherwise = showFFloat (Just 2) value ""
+
+pointCountLabel :: Int -> String
+pointCountLabel pointCount
+  | pointCount `mod` 1000 == 0 = show (pointCount `div` 1000) <> "k"
+  | otherwise = show pointCount
+
+coordinate :: Double -> String
+coordinate value = showFFloat (Just 1) value ""
+
+svgText :: Double -> Double -> String -> Int -> String -> String -> String
+svgText x y anchor fontSize fontWeight content =
+  "<text x=\""
+    <> coordinate x
+    <> "\" y=\""
+    <> coordinate y
+    <> "\" text-anchor=\""
+    <> anchor
+    <> "\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\""
+    <> show fontSize
+    <> "\" font-weight=\""
+    <> fontWeight
+    <> "\" fill=\"rgb(20,20,20)\">"
+    <> content
+    <> "</text>"
diff --git a/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Suite.hs b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Suite.hs
new file mode 100644
--- /dev/null
+++ b/docs/delaunay-compare/Moonlight/Planar/Bench/DelaunayCompare/Suite.hs
@@ -0,0 +1,259 @@
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
+module Moonlight.Planar.Bench.DelaunayCompare.Suite
+  ( PreparedSuite
+  , withPreparedSuite
+  , preflightSuite
+  , suiteBenchmarks
+  , suiteAgreementMessage
+  ) where
+
+import Control.DeepSeq (force)
+import Control.Exception (bracket, evaluate)
+import Data.Bifunctor (first)
+import Data.Either (lefts, rights)
+import Data.Foldable (toList, traverse_)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Vector as Vector
+import Moonlight.Planar.Bench.DelaunayCompare.Domain
+import Moonlight.Planar.Bench.DelaunayCompare.Native
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Dcel (numInnerFaces, numVertices)
+import Moonlight.Planar.Types (BuildError)
+import Moonlight.Planar.Point (Point (Point))
+import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)
+
+data NativeSections value = NativeSections
+  { nativeSpadeSection :: !value
+  , nativeSpadeHierarchySection :: !value
+  , nativeCdtSection :: !value
+  , nativeDelaunatorSection :: !value
+  }
+  deriving stock (Functor, Foldable, Traversable)
+
+nativeImplementationSections :: NativeSections NativeImplementation
+nativeImplementationSections =
+  NativeSections
+    { nativeSpadeSection = NativeSpade
+    , nativeSpadeHierarchySection = NativeSpadeHierarchy
+    , nativeCdtSection = NativeCdt
+    , nativeDelaunatorSection = NativeDelaunator
+    }
+
+nativeSectionAt :: NativeImplementation -> NativeSections value -> value
+nativeSectionAt implementation sections =
+  case implementation of
+    NativeSpade -> nativeSpadeSection sections
+    NativeSpadeHierarchy -> nativeSpadeHierarchySection sections
+    NativeCdt -> nativeCdtSection sections
+    NativeDelaunator -> nativeDelaunatorSection sections
+
+data PreparedFixture = PreparedFixture
+  { preparedSpec :: !FixtureSpec
+  , preparedMoonlightPoints :: !(Vector.Vector Point)
+  , preparedNativeFixtures :: !(NativeSections NativeFixture)
+  }
+
+newtype PreparedSuite = PreparedSuite [PreparedFixture]
+
+withPreparedSuite
+  :: NativeApi
+  -> (PreparedSuite -> IO (Either CompareObstruction result))
+  -> IO (Either CompareObstruction result)
+withPreparedSuite api action = do
+  prepared <- prepareSuite api
+  case prepared of
+    Left obstruction -> pure (Left obstruction)
+    Right suite -> bracket (pure suite) (releaseSuite api) action
+
+prepareSuite :: NativeApi -> IO (Either CompareObstruction PreparedSuite)
+prepareSuite api = do
+  outcomes <- traverse (prepareFixture api) allFixtureSpecs
+  case NonEmpty.nonEmpty (lefts outcomes) of
+    Just obstructions -> do
+      traverse_ (releasePreparedFixture api) (rights outcomes)
+      pure (Left (ComparisonObstructions obstructions))
+    Nothing -> pure (Right (PreparedSuite (rights outcomes)))
+
+prepareFixture :: NativeApi -> FixtureSpec -> IO (Either CompareObstruction PreparedFixture)
+prepareFixture api fixture = do
+  generated <- generateCoordinates api fixture
+  case generated of
+    Left status -> pure (Left (FixtureGenerationFailed fixture status))
+    Right coordinates -> do
+      nativeFixtures <- prepareNativeFixtures api fixture coordinates
+      pure $
+        PreparedFixture
+          fixture
+          (Vector.map (uncurry Point) (generatedCoordinatePairs coordinates))
+          <$> nativeFixtures
+
+prepareNativeFixtures
+  :: NativeApi
+  -> FixtureSpec
+  -> GeneratedCoordinates
+  -> IO (Either CompareObstruction (NativeSections NativeFixture))
+prepareNativeFixtures api fixture coordinates = do
+  outcomes <- traverse prepareSection nativeImplementationSections
+  case sequenceA outcomes of
+    Right nativeFixtures -> pure (Right nativeFixtures)
+    Left obstruction -> do
+      traverse_ (releaseNativeFixture api) (rights (toList outcomes))
+      pure (Left obstruction)
+ where
+  prepareSection implementation = do
+    prepared <- prepareNativeFixture api implementation coordinates
+    pure $
+      first
+        (preparationObstruction implementation)
+        prepared
+
+  preparationObstruction implementation failure =
+    case failure of
+      NativePreparationStatus status -> FixturePreparationFailed fixture implementation status
+      NativePreparationReturnedNull -> NativePreparedNull fixture implementation
+
+releaseSuite :: NativeApi -> PreparedSuite -> IO ()
+releaseSuite api (PreparedSuite fixtures) =
+  traverse_ (releasePreparedFixture api) fixtures
+
+releasePreparedFixture :: NativeApi -> PreparedFixture -> IO ()
+releasePreparedFixture api = releasePreparedNativeFixtures api . preparedNativeFixtures
+
+releasePreparedNativeFixtures :: NativeApi -> NativeSections NativeFixture -> IO ()
+releasePreparedNativeFixtures api = traverse_ (releaseNativeFixture api)
+
+preflightSuite :: NativeApi -> PreparedSuite -> IO (Either CompareObstruction ())
+preflightSuite api (PreparedSuite fixtures) = do
+  outcomes <- traverse (preflightFixture api) fixtures
+  pure $
+    case NonEmpty.nonEmpty (lefts outcomes) of
+      Just obstructions -> Left (ComparisonObstructions obstructions)
+      Nothing -> Right ()
+
+preflightFixture :: NativeApi -> PreparedFixture -> IO (Either CompareObstruction ())
+preflightFixture api fixture = do
+  moonlightResult <-
+    evaluate
+      ( force
+          (moonlightConstructionSummary (preparedMoonlightPoints fixture))
+      )
+  nativeOutcomes <- traverse runSection nativeImplementationSections
+  pure $ do
+    moonlightSummary <-
+      first
+        (MoonlightConstructionFailed spec)
+        moonlightResult
+    nativeSummaries <- sequenceA nativeOutcomes
+    traverse_
+      ( \implementation ->
+          requireSummaryAgreement
+            spec
+            moonlightSummary
+            ( implementationOfNative implementation
+            , nativeSectionAt implementation nativeSummaries
+            )
+      )
+      nativeImplementationSections
+ where
+  spec = preparedSpec fixture
+  nativeFixtures = preparedNativeFixtures fixture
+  runSection implementation =
+    first (NativeConstructionFailed spec implementation)
+      <$> runNativeFixture api (nativeSectionAt implementation nativeFixtures)
+
+requireSummaryAgreement
+  :: FixtureSpec
+  -> DelaunaySummary
+  -> (Implementation, DelaunaySummary)
+  -> Either CompareObstruction ()
+requireSummaryAgreement fixture expected (implementation, observed) =
+  if observed == expected
+    then Right ()
+    else Left (SummaryDisagreement fixture implementation expected observed)
+
+moonlightConstructionSummary
+  :: Vector.Vector Point
+  -> Either BuildError DelaunaySummary
+moonlightConstructionSummary points = do
+  triangulation <- delaunayGeometry points
+  pure
+    DelaunaySummary
+      { summaryVertexCount = numVertices triangulation
+      , summaryTriangleCount = numInnerFaces triangulation
+      }
+
+suiteBenchmarks :: NativeApi -> PreparedSuite -> [Benchmark]
+suiteBenchmarks api (PreparedSuite fixtures) =
+  fmap (sizeBandBenchmarks api fixtures) allSizeBands
+
+sizeBandBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Benchmark
+sizeBandBenchmarks api fixtures sizeBand =
+  bgroup
+    ("comparison: creation benchmark (" <> sizeBandLabel sizeBand <> ")")
+    (fmap (implementationBenchmarks api fixtures sizeBand) allImplementations)
+
+implementationBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Implementation
+  -> Benchmark
+implementationBenchmarks api fixtures sizeBand implementation =
+  bgroup
+    (implementationLabel implementation)
+    ( fmap
+        (distributionBenchmarks api fixtures sizeBand implementation)
+        allPointDistributions
+    )
+
+distributionBenchmarks
+  :: NativeApi
+  -> [PreparedFixture]
+  -> SizeBand
+  -> Implementation
+  -> PointDistribution
+  -> Benchmark
+distributionBenchmarks api fixtures sizeBand implementation distribution =
+  bgroup
+    (pointDistributionLabel distribution)
+    ( fmap
+        (fixtureBenchmark api implementation)
+        ( filter
+            (matchesFixture sizeBand distribution . preparedSpec)
+            fixtures
+        )
+    )
+
+matchesFixture :: SizeBand -> PointDistribution -> FixtureSpec -> Bool
+matchesFixture sizeBand distribution fixture =
+  fixtureSizeBand fixture == sizeBand
+    && fixturePointDistribution fixture == distribution
+
+fixtureBenchmark :: NativeApi -> Implementation -> PreparedFixture -> Benchmark
+fixtureBenchmark api implementation fixture =
+  bench (show (pointCountValue (fixturePointCount (preparedSpec fixture)))) $
+    case implementation of
+      Moonlight -> nf moonlightConstructionSummary (preparedMoonlightPoints fixture)
+      Spade -> nativeBenchmark NativeSpade
+      SpadeHierarchy -> nativeBenchmark NativeSpadeHierarchy
+      Cdt -> nativeBenchmark NativeCdt
+      Delaunator -> nativeBenchmark NativeDelaunator
+ where
+  nativeFixtures = preparedNativeFixtures fixture
+  nativeBenchmark nativeImplementation =
+    nfIO (runNativeFixture api (nativeSectionAt nativeImplementation nativeFixtures))
+
+suiteAgreementMessage :: String
+suiteAgreementMessage =
+  "delaunay-compare agreement: "
+    <> show (length allFixtureSpecs)
+    <> " fixtures agree on vertex and triangle counts across "
+    <> show (length allImplementations)
+    <> " implementations"
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
deleted file mode 100644
--- a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Domain.hs
+++ /dev/null
@@ -1,244 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE NumericUnderscores #-}
-
--- | The closed case matrix and typed obstruction surface for the comparison.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-  ( SizeBand (..)
-  , allSizeBands
-  , sizeBandLabel
-  , PointDistribution (..)
-  , allPointDistributions
-  , pointDistributionLabel
-  , Implementation (..)
-  , allImplementations
-  , implementationLabel
-  , NativeImplementation (..)
-  , nativeImplementationLabel
-  , implementationOfNative
-  , PointCount
-  , pointCountValue
-  , pointCountsFor
-  , FixtureSpec (..)
-  , allFixtureSpecs
-  , DelaunaySummary (..)
-  , NativeStatus (..)
-  , NativePreparationFailure (..)
-  , NativeRunFailure (..)
-  , CompareObstruction (..)
-  , renderCompareObstruction
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.List.NonEmpty as NonEmpty
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Types (BuildError)
-
-data SizeBand
-  = Small
-  | Big
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allSizeBands :: [SizeBand]
-allSizeBands = [Small, Big]
-
-sizeBandLabel :: SizeBand -> String
-sizeBandLabel = \case
-  Small -> "small"
-  Big -> "big"
-
-data PointDistribution
-  = LocalInsertion
-  | Uniform
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allPointDistributions :: [PointDistribution]
-allPointDistributions = [LocalInsertion, Uniform]
-
-pointDistributionLabel :: PointDistribution -> String
-pointDistributionLabel = \case
-  LocalInsertion -> "local insertion"
-  Uniform -> "uniform"
-
-data Implementation
-  = Spade
-  | SpadeHierarchy
-  | Cdt
-  | Delaunator
-  | Moonlight
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Upstream order, with Moonlight appended rather than interposed.
-allImplementations :: [Implementation]
-allImplementations = [Spade, SpadeHierarchy, Cdt, Delaunator, Moonlight]
-
-implementationLabel :: Implementation -> String
-implementationLabel = \case
-  Spade -> "spade 2"
-  SpadeHierarchy -> "spade 2 hierarchy"
-  Cdt -> "cdt"
-  Delaunator -> "delaunator"
-  Moonlight -> "moonlight-triangulation"
-
-data NativeImplementation
-  = NativeSpade
-  | NativeSpadeHierarchy
-  | NativeCdt
-  | NativeDelaunator
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-nativeImplementationLabel :: NativeImplementation -> String
-nativeImplementationLabel = implementationLabel . implementationOfNative
-
-implementationOfNative :: NativeImplementation -> Implementation
-implementationOfNative = \case
-  NativeSpade -> Spade
-  NativeSpadeHierarchy -> SpadeHierarchy
-  NativeCdt -> Cdt
-  NativeDelaunator -> Delaunator
-
-newtype PointCount = PointCount Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-pointCountValue :: PointCount -> Int
-pointCountValue (PointCount count) = count
-
-data FixtureSpec = FixtureSpec
-  { fixtureSizeBand :: !SizeBand
-  , fixturePointDistribution :: !PointDistribution
-  , fixturePointCount :: !PointCount
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-allFixtureSpecs :: [FixtureSpec]
-allFixtureSpecs =
-  concatMap
-    ( \sizeBand ->
-        liftA2
-          (FixtureSpec sizeBand)
-          allPointDistributions
-          (pointCountsFor sizeBand)
-    )
-    allSizeBands
-
-pointCountsFor :: SizeBand -> [PointCount]
-pointCountsFor = \case
-  Small -> PointCount <$> [2_000, 4_000, 6_000, 8_000, 10_000, 12_000, 14_000]
-  Big -> PointCount <$> [50_000, 100_000, 150_000, 200_000, 250_000]
-
-data DelaunaySummary = DelaunaySummary
-  { summaryVertexCount :: !Int
-  , summaryTriangleCount :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativeStatus
-  = NativeNullPointer
-  | NativeUnknownTag
-  | NativeCoordinateCountMismatch
-  | NativeDistributionConstructionFailed
-  | NativeTriangulationFailed
-  | NativePanicked
-  | NativeUnknownStatus !Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativePreparationFailure
-  = NativePreparationStatus !NativeStatus
-  | NativePreparationReturnedNull
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data NativeRunFailure
-  = NativeRunStatus !NativeStatus
-  | NativeCountExceedsHaskellInt !Integer
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data CompareObstruction
-  = RustManifestNotFound !(NonEmpty FilePath)
-  | CargoInvocationFailed !FilePath !String
-  | CargoBuildFailed !FilePath !Int !String
-  | UnsupportedDynamicLibraryHost !String
-  | DynamicLibraryOpenFailed !FilePath !String
-  | DynamicSymbolLoadFailed !FilePath !String !String
-  | FixtureGenerationFailed !FixtureSpec !NativeStatus
-  | FixturePreparationFailed !FixtureSpec !NativeImplementation !NativeStatus
-  | NativePreparedNull !FixtureSpec !NativeImplementation
-  | MoonlightConstructionFailed !FixtureSpec !BuildError
-  | NativeConstructionFailed !FixtureSpec !NativeImplementation !NativeRunFailure
-  | SummaryDisagreement !FixtureSpec !Implementation !DelaunaySummary !DelaunaySummary
-  | ComparisonObstructions !(NonEmpty CompareObstruction)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-renderCompareObstruction :: CompareObstruction -> String
-renderCompareObstruction = \case
-  RustManifestNotFound candidates ->
-    "Rust referent manifest not found; checked " <> show (NonEmpty.toList candidates)
-  CargoInvocationFailed manifest details ->
-    "could not invoke Cargo for " <> manifest <> ": " <> details
-  CargoBuildFailed manifest exitCode details ->
-    "Cargo failed for "
-      <> manifest
-      <> " with exit code "
-      <> show exitCode
-      <> if null details then "" else ":\n" <> details
-  UnsupportedDynamicLibraryHost host ->
-    "delaunay-compare does not know the dynamic-library suffix for host " <> show host
-  DynamicLibraryOpenFailed path details ->
-    "could not load Rust referent library " <> path <> ": " <> details
-  DynamicSymbolLoadFailed path symbol details ->
-    "could not load symbol " <> symbol <> " from " <> path <> ": " <> details
-  FixtureGenerationFailed fixture status ->
-    "upstream fixture generation failed for " <> renderFixture fixture <> ": " <> show status
-  FixturePreparationFailed fixture implementation status ->
-    "native preparation failed for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> ": "
-      <> show status
-  NativePreparedNull fixture implementation ->
-    "native preparation returned a null handle for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-  MoonlightConstructionFailed fixture buildError ->
-    "Moonlight construction failed for " <> renderFixture fixture <> ": " <> show buildError
-  NativeConstructionFailed fixture implementation failure ->
-    "native construction failed for "
-      <> nativeImplementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> ": "
-      <> show failure
-  SummaryDisagreement fixture implementation expected observed ->
-    "construction summary disagreement for "
-      <> implementationLabel implementation
-      <> " on "
-      <> renderFixture fixture
-      <> "; Moonlight="
-      <> show expected
-      <> ", observed="
-      <> show observed
-  ComparisonObstructions obstructions ->
-    unlines ("delaunay comparison obstructed:" : fmap (("  - " <>) . renderCompareObstruction) (NonEmpty.toList obstructions))
-
-renderFixture :: FixtureSpec -> String
-renderFixture fixture =
-  sizeBandLabel (fixtureSizeBand fixture)
-    <> "/"
-    <> pointDistributionLabel (fixturePointDistribution fixture)
-    <> "/"
-    <> show (pointCountValue (fixturePointCount fixture))
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
deleted file mode 100644
--- a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Native.hs
+++ /dev/null
@@ -1,307 +0,0 @@
-{-# LANGUAGE ForeignFunctionInterface #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Resource-safe dynamic boundary to the Rust crates under comparison.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Native
-  ( NativeApi
-  , NativeFixture
-  , GeneratedCoordinates
-  , withNativeApi
-  , generateCoordinates
-  , generatedCoordinatePairs
-  , prepareNativeFixture
-  , runNativeFixture
-  , releaseNativeFixture
-  ) where
-
-import Control.Exception (IOException, bracket, displayException, try)
-import Control.Monad (filterM)
-import Control.Monad.Trans.Except (ExceptT (ExceptT), runExceptT)
-import Data.Bifunctor (first)
-import Data.Int (Int32)
-import Data.List (unfoldr)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Foreign.C.Types (CDouble, CInt (..), CSize (..))
-import Foreign.Marshal.Alloc (alloca)
-import Foreign.Ptr (FunPtr, Ptr, nullPtr)
-import Foreign.Storable (peek, poke)
-import qualified Data.Vector as Vector
-import qualified Data.Vector.Storable as StorableVector
-import qualified Data.Vector.Storable.Mutable as MutableStorableVector
-import System.Directory (doesFileExist, makeAbsolute)
-import System.Environment (lookupEnv)
-import System.Exit (ExitCode (..))
-import System.FilePath ((</>), takeDirectory)
-import System.Info (os)
-import System.Posix.DynamicLinker
-  ( DL
-  , RTLDFlags (RTLD_LOCAL, RTLD_NOW)
-  , dlclose
-  , dlopen
-  , dlsym
-  )
-import System.Process
-  ( CreateProcess (cwd)
-  , proc
-  , readCreateProcessWithExitCode
-  )
-
-type GenerateFunction = CInt -> CSize -> Ptr CDouble -> CSize -> IO CInt
-type PrepareFunction = CInt -> Ptr CDouble -> CSize -> Ptr (Ptr ()) -> IO CInt
-type RunFunction = Ptr () -> Ptr CSize -> Ptr CSize -> IO CInt
-type ReleaseFunction = Ptr () -> IO ()
-
-foreign import ccall safe "dynamic"
-  bindGenerateFunction :: FunPtr GenerateFunction -> GenerateFunction
-
-foreign import ccall safe "dynamic"
-  bindPrepareFunction :: FunPtr PrepareFunction -> PrepareFunction
-
-foreign import ccall safe "dynamic"
-  bindRunFunction :: FunPtr RunFunction -> RunFunction
-
-foreign import ccall unsafe "dynamic"
-  bindReleaseFunction :: FunPtr ReleaseFunction -> ReleaseFunction
-
-data NativeApi = NativeApi
-  { nativeGenerate :: !GenerateFunction
-  , nativePrepare :: !PrepareFunction
-  , nativeRun :: !RunFunction
-  , nativeRelease :: !ReleaseFunction
-  }
-
-newtype NativeFixture = NativeFixture (Ptr ())
-
-data GeneratedCoordinates = GeneratedCoordinates
-  { generatedPointCount :: !PointCount
-  , generatedCoordinateValues :: !(StorableVector.Vector CDouble)
-  }
-
-withNativeApi
-  :: (NativeApi -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withNativeApi action = do
-  manifestResult <- resolveRustManifest
-  case manifestResult of
-    Left obstruction -> pure (Left obstruction)
-    Right manifest -> do
-      libraryResult <- buildRustLibrary manifest
-      case libraryResult of
-        Left obstruction -> pure (Left obstruction)
-        Right libraryPath -> withLoadedNativeApi libraryPath action
-
-resolveRustManifest :: IO (Either CompareObstruction FilePath)
-resolveRustManifest = do
-  configured <- lookupEnv "MOONLIGHT_DELAUNAY_COMPARE_RUST_MANIFEST"
-  let candidates =
-        case configured of
-          Just path -> path :| []
-          Nothing ->
-            "docs/delaunay-compare/rust/Cargo.toml"
-              :| [ "foundation/moonlight-planar/docs/delaunay-compare/rust/Cargo.toml"
-                 , "compiler/foundation/moonlight-planar/docs/delaunay-compare/rust/Cargo.toml"
-                 ]
-  existing <- filterM doesFileExist (NonEmpty.toList candidates)
-  case existing of
-    manifest : _ -> Right <$> makeAbsolute manifest
-    [] -> pure (Left (RustManifestNotFound candidates))
-
-buildRustLibrary :: FilePath -> IO (Either CompareObstruction FilePath)
-buildRustLibrary manifest = do
-  libraryNameResult <- dynamicLibraryName
-  case libraryNameResult of
-    Left obstruction -> pure (Left obstruction)
-    Right libraryName -> do
-      let rustRoot = takeDirectory manifest
-          targetDirectory = rustRoot </> "target"
-          command =
-            ( proc
-                "cargo"
-                [ "build"
-                , "--quiet"
-                , "--release"
-                , "--lib"
-                , "--manifest-path"
-                , manifest
-                , "--target-dir"
-                , targetDirectory
-                ]
-            )
-              { cwd = Just rustRoot
-              }
-      invocation <- try @IOException (readCreateProcessWithExitCode command "")
-      pure $
-        case invocation of
-          Left exception ->
-            Left (CargoInvocationFailed manifest (displayException exception))
-          Right (ExitFailure exitCode, standardOutput, standardError) ->
-            Left
-              ( CargoBuildFailed
-                  manifest
-                  exitCode
-                  (standardError <> standardOutput)
-              )
-          Right (ExitSuccess, _, _) ->
-            Right (targetDirectory </> "release" </> libraryName)
-
-dynamicLibraryName :: IO (Either CompareObstruction FilePath)
-dynamicLibraryName =
-  pure $
-    case os of
-      "darwin" -> Right "libmoonlight_delaunay_compare_referents.dylib"
-      "linux" -> Right "libmoonlight_delaunay_compare_referents.so"
-      host -> Left (UnsupportedDynamicLibraryHost host)
-
-withLoadedNativeApi
-  :: FilePath
-  -> (NativeApi -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withLoadedNativeApi libraryPath action = do
-  opened <- try @IOException (dlopen libraryPath [RTLD_NOW, RTLD_LOCAL])
-  case opened of
-    Left exception ->
-      pure (Left (DynamicLibraryOpenFailed libraryPath (displayException exception)))
-    Right handle ->
-      bracket
-        (pure handle)
-        dlclose
-        ( \loadedHandle -> do
-            apiResult <- loadNativeApi libraryPath loadedHandle
-            case apiResult of
-              Left obstruction -> pure (Left obstruction)
-              Right api -> action api
-        )
-
-loadNativeApi :: FilePath -> DL -> IO (Either CompareObstruction NativeApi)
-loadNativeApi libraryPath handle =
-  runExceptT $
-    NativeApi
-      <$> loadSymbol libraryPath handle "delaunay_compare_generate" bindGenerateFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_prepare" bindPrepareFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_run" bindRunFunction
-      <*> loadSymbol libraryPath handle "delaunay_compare_release" bindReleaseFunction
-
-loadSymbol
-  :: FilePath
-  -> DL
-  -> String
-  -> (FunPtr function -> boundFunction)
-  -> ExceptT CompareObstruction IO boundFunction
-loadSymbol libraryPath handle symbol bind =
-  ExceptT $ do
-    loaded <- try @IOException (dlsym handle symbol)
-    pure $
-      first
-        (DynamicSymbolLoadFailed libraryPath symbol . displayException)
-        (bind <$> loaded)
-
-generateCoordinates
-  :: NativeApi
-  -> FixtureSpec
-  -> IO (Either NativeStatus GeneratedCoordinates)
-generateCoordinates api fixture = do
-  let pointCount = fixturePointCount fixture
-      coordinateCount = 2 * pointCountValue pointCount
-  mutableCoordinates <- MutableStorableVector.new coordinateCount
-  statusCode <-
-    MutableStorableVector.unsafeWith mutableCoordinates $ \coordinatePointer ->
-      nativeGenerate api
-        (distributionTag (fixturePointDistribution fixture))
-        (fromIntegral (pointCountValue pointCount))
-        coordinatePointer
-        (fromIntegral coordinateCount)
-  case decodeNativeStatus statusCode of
-    Left status -> pure (Left status)
-    Right () -> do
-      coordinates <- StorableVector.unsafeFreeze mutableCoordinates
-      pure (Right (GeneratedCoordinates pointCount coordinates))
-
-generatedCoordinatePairs :: GeneratedCoordinates -> Vector.Vector (Double, Double)
-generatedCoordinatePairs generated =
-  Vector.fromList
-    ( unfoldr
-        takeCoordinatePair
-        (realToFrac <$> StorableVector.toList (generatedCoordinateValues generated))
-    )
- where
-  takeCoordinatePair :: [Double] -> Maybe ((Double, Double), [Double])
-  takeCoordinatePair = \case
-    x : y : remaining -> Just ((x, y), remaining)
-    _ -> Nothing
-
-prepareNativeFixture
-  :: NativeApi
-  -> NativeImplementation
-  -> GeneratedCoordinates
-  -> IO (Either NativePreparationFailure NativeFixture)
-prepareNativeFixture api implementation coordinates =
-  alloca $ \preparedOutput -> do
-    poke preparedOutput nullPtr
-    statusCode <-
-      StorableVector.unsafeWith (generatedCoordinateValues coordinates) $ \coordinatePointer ->
-        nativePrepare api
-          (implementationTag implementation)
-          coordinatePointer
-          (fromIntegral (pointCountValue (generatedPointCount coordinates)))
-          preparedOutput
-    case decodeNativeStatus statusCode of
-      Left status -> pure (Left (NativePreparationStatus status))
-      Right () -> do
-        prepared <- peek preparedOutput
-        pure $
-          if prepared == nullPtr
-            then Left NativePreparationReturnedNull
-            else Right (NativeFixture prepared)
-
-runNativeFixture
-  :: NativeApi
-  -> NativeFixture
-  -> IO (Either NativeRunFailure DelaunaySummary)
-runNativeFixture api (NativeFixture prepared) =
-  alloca $ \vertexCountOutput ->
-    alloca $ \triangleCountOutput -> do
-      statusCode <- nativeRun api prepared vertexCountOutput triangleCountOutput
-      case decodeNativeStatus statusCode of
-        Left status -> pure (Left (NativeRunStatus status))
-        Right () -> do
-          vertexCount <- peek vertexCountOutput
-          triangleCount <- peek triangleCountOutput
-          pure $ DelaunaySummary <$> cSizeToInt vertexCount <*> cSizeToInt triangleCount
-
-releaseNativeFixture :: NativeApi -> NativeFixture -> IO ()
-releaseNativeFixture api (NativeFixture prepared) = nativeRelease api prepared
-
-decodeNativeStatus :: CInt -> Either NativeStatus ()
-decodeNativeStatus statusCode =
-  case fromIntegral statusCode :: Int32 of
-    0 -> Right ()
-    1 -> Left NativeNullPointer
-    2 -> Left NativeUnknownTag
-    3 -> Left NativeCoordinateCountMismatch
-    4 -> Left NativeDistributionConstructionFailed
-    5 -> Left NativeTriangulationFailed
-    6 -> Left NativePanicked
-    unknown -> Left (NativeUnknownStatus (fromIntegral unknown))
-
-cSizeToInt :: CSize -> Either NativeRunFailure Int
-cSizeToInt value =
-  let integerValue = toInteger value
-   in if integerValue > toInteger (maxBound :: Int)
-        then Left (NativeCountExceedsHaskellInt integerValue)
-        else Right (fromInteger integerValue)
-
-distributionTag :: PointDistribution -> CInt
-distributionTag = \case
-  LocalInsertion -> 0
-  Uniform -> 1
-
-implementationTag :: NativeImplementation -> CInt
-implementationTag = \case
-  NativeSpade -> 0
-  NativeSpadeHierarchy -> 1
-  NativeCdt -> 2
-  NativeDelaunator -> 3
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
deleted file mode 100644
--- a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Picture.hs
+++ /dev/null
@@ -1,466 +0,0 @@
-{-# LANGUAGE LambdaCase #-}
-
--- | Typed receipt parsing and pure Spade-style SVG projection.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Picture
-  ( BenchmarkReceipt
-  , PictureObstruction (..)
-  , parseBenchmarkReceipt
-  , pictureFileName
-  , renderComparisonPicture
-  , renderPictureObstruction
-  ) where
-
-import Data.List (group, sort, sortOn)
-import qualified Data.Map.Strict as Map
-import Data.Maybe (mapMaybe)
-import qualified Data.Set as Set
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Numeric (showFFloat)
-import Text.Read (readMaybe)
-
-data ObservationKey = ObservationKey
-  { observationFixture :: !FixtureSpec
-  , observationImplementation :: !Implementation
-  }
-  deriving stock (Eq, Ord, Show)
-
-data BenchmarkObservation = BenchmarkObservation
-  { benchmarkKey :: !ObservationKey
-  , benchmarkMeanPicoseconds :: !Double
-  , benchmarkSpreadPicoseconds :: !Double
-  }
-  deriving stock (Eq, Show)
-
-newtype BenchmarkReceipt = BenchmarkReceipt
-  { receiptObservations :: Map.Map ObservationKey BenchmarkObservation
-  }
-
-data PictureObstruction
-  = MissingCsvHeader
-  | UnexpectedCsvHeader !String
-  | MalformedCsvRow !Int !String
-  | UnknownBenchmarkName !Int !String
-  | InvalidBenchmarkScalar !Int !String !String
-  | DuplicateBenchmarkObservations ![ObservationKey]
-  | MissingBenchmarkObservations ![ObservationKey]
-  deriving stock (Eq, Show)
-
-parseBenchmarkReceipt :: String -> Either PictureObstruction BenchmarkReceipt
-parseBenchmarkReceipt source =
-  case lines source of
-    [] -> Left MissingCsvHeader
-    header : rows
-      | header /= expectedCsvHeader -> Left (UnexpectedCsvHeader header)
-      | otherwise -> do
-          observations <- traverse (uncurry parseObservationRow) (zip [2 ..] rows)
-          let keys = benchmarkKey <$> observations
-              duplicateKeys =
-                mapMaybe
-                  ( \case
-                      key : _ : _ -> Just key
-                      _ -> Nothing
-                  )
-                  (group (sort keys))
-              observationMap = Map.fromList ((\observation -> (benchmarkKey observation, observation)) <$> observations)
-              missingKeys = Set.toList (expectedObservationKeys `Set.difference` Map.keysSet observationMap)
-          case (duplicateKeys, missingKeys) of
-            (duplicate : duplicates, _) -> Left (DuplicateBenchmarkObservations (duplicate : duplicates))
-            ([], missing : remaining) -> Left (MissingBenchmarkObservations (missing : remaining))
-            ([], []) -> Right (BenchmarkReceipt observationMap)
-
-renderComparisonPicture
-  :: SizeBand
-  -> BenchmarkReceipt
-  -> String
-renderComparisonPicture sizeBand receipt =
-  renderSvg sizeBand pointCounts axisMaximum panels
- where
-  pointCounts = pointCountValue <$> pointCountsFor sizeBand
-  panels = picturePanel receipt sizeBand <$> allPointDistributions
-  allObservations = concatMap (concatMap pictureSeriesObservations . picturePanelSeries) panels
-  maximumMilliseconds =
-    foldr
-      (max . observationUpperMilliseconds)
-      0
-      allObservations
-  axisMaximum = niceCeiling maximumMilliseconds
-
-pictureFileName :: SizeBand -> FilePath
-pictureFileName = \case
-  Small -> "moonlight-delaunay-compare-small.svg"
-  Big -> "moonlight-delaunay-compare-big.svg"
-
-renderPictureObstruction :: PictureObstruction -> String
-renderPictureObstruction = \case
-  MissingCsvHeader -> "benchmark CSV has no header"
-  UnexpectedCsvHeader header ->
-    "benchmark CSV header was " <> show header <> "; expected " <> show expectedCsvHeader
-  MalformedCsvRow lineNumber row ->
-    "benchmark CSV row " <> show lineNumber <> " is not a three-column tasty-bench row: " <> show row
-  UnknownBenchmarkName lineNumber name ->
-    "benchmark CSV row " <> show lineNumber <> " names an unknown comparison case: " <> show name
-  InvalidBenchmarkScalar lineNumber field value ->
-    "benchmark CSV row "
-      <> show lineNumber
-      <> " has invalid "
-      <> field
-      <> ": "
-      <> show value
-  DuplicateBenchmarkObservations keys ->
-    "benchmark CSV repeats comparison cases: " <> show keys
-  MissingBenchmarkObservations keys ->
-    "benchmark CSV omits comparison cases: " <> show keys
-
-expectedCsvHeader :: String
-expectedCsvHeader = "Name,Mean (ps),2*Stdev (ps)"
-
-expectedObservationKeys :: Set.Set ObservationKey
-expectedObservationKeys =
-  Set.fromList (liftA2 ObservationKey allFixtureSpecs allImplementations)
-
-expectedBenchmarkNames :: Map.Map String ObservationKey
-expectedBenchmarkNames =
-  Map.fromList ((\key -> (benchmarkName key, key)) <$> Set.toList expectedObservationKeys)
-
-benchmarkName :: ObservationKey -> String
-benchmarkName key =
-  "All.comparison: creation benchmark ("
-    <> sizeBandLabel (fixtureSizeBand fixture)
-    <> ")."
-    <> implementationLabel (observationImplementation key)
-    <> "."
-    <> pointDistributionLabel (fixturePointDistribution fixture)
-    <> "."
-    <> show (pointCountValue (fixturePointCount fixture))
- where
-  fixture = observationFixture key
-
-parseObservationRow :: Int -> String -> Either PictureObstruction BenchmarkObservation
-parseObservationRow lineNumber row =
-  case csvTriple row of
-    Nothing -> Left (MalformedCsvRow lineNumber row)
-    Just (name, rawMean, rawSpread) -> do
-      key <- maybe (Left (UnknownBenchmarkName lineNumber name)) Right (Map.lookup name expectedBenchmarkNames)
-      meanPicoseconds <- parseNonNegativeScalar lineNumber "mean picoseconds" rawMean
-      spreadPicoseconds <- parseNonNegativeScalar lineNumber "two-standard-deviation spread" rawSpread
-      pure
-        BenchmarkObservation
-          { benchmarkKey = key
-          , benchmarkMeanPicoseconds = meanPicoseconds
-          , benchmarkSpreadPicoseconds = spreadPicoseconds
-          }
-
-csvTriple :: String -> Maybe (String, String, String)
-csvTriple row =
-  case break (== ',') row of
-    (firstColumn, ',' : remaining) ->
-      case break (== ',') remaining of
-        (secondColumn, ',' : thirdColumn)
-          | ',' `notElem` thirdColumn -> Just (firstColumn, secondColumn, thirdColumn)
-        _ -> Nothing
-    _ -> Nothing
-
-parseNonNegativeScalar :: Int -> String -> String -> Either PictureObstruction Double
-parseNonNegativeScalar lineNumber field rawValue =
-  case readMaybe rawValue of
-    Just value
-      | value >= 0 && not (isInfinite value) && not (isNaN value) -> Right value
-    _ -> Left (InvalidBenchmarkScalar lineNumber field rawValue)
-
-data PictureSeries = PictureSeries
-  { pictureSeriesImplementation :: !Implementation
-  , pictureSeriesObservations :: ![BenchmarkObservation]
-  }
-
-data PicturePanel = PicturePanel
-  { picturePanelDistribution :: !PointDistribution
-  , picturePanelSeries :: ![PictureSeries]
-  }
-
-picturePanel
-  :: BenchmarkReceipt
-  -> SizeBand
-  -> PointDistribution
-  -> PicturePanel
-picturePanel receipt sizeBand distribution =
-  PicturePanel distribution (pictureSeries receipt sizeBand distribution <$> allImplementations)
-
-pictureSeries
-  :: BenchmarkReceipt
-  -> SizeBand
-  -> PointDistribution
-  -> Implementation
-  -> PictureSeries
-pictureSeries receipt sizeBand distribution implementation =
-  PictureSeries implementation observations
- where
-  observations =
-    sortOn (pointCountValue . fixturePointCount . observationFixture . benchmarkKey)
-      [ observation
-      | observation <- Map.elems (receiptObservations receipt)
-      , let key = benchmarkKey observation
-            fixture = observationFixture key
-      , fixtureSizeBand fixture == sizeBand
-      , fixturePointDistribution fixture == distribution
-      , observationImplementation key == implementation
-      ]
-
-data PictureLayout = PictureLayout
-  { pictureWidth :: !Double
-  , pictureHeight :: !Double
-  , picturePlotTop :: !Double
-  , picturePlotBottom :: !Double
-  , picturePanelWidth :: !Double
-  , picturePanelLefts :: ![Double]
-  }
-
-pictureLayout :: PictureLayout
-pictureLayout =
-  PictureLayout
-    { pictureWidth = 1280
-    , pictureHeight = 720
-    , picturePlotTop = 168
-    , picturePlotBottom = 610
-    , picturePanelWidth = 540
-    , picturePanelLefts = [70, 670]
-    }
-
-renderSvg :: SizeBand -> [Int] -> Double -> [PicturePanel] -> String
-renderSvg sizeBand pointCounts axisMaximum panels =
-  unlines
-    ( [ "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
-      , "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 "
-          <> coordinate (pictureWidth pictureLayout)
-          <> " "
-          <> coordinate (pictureHeight pictureLayout)
-          <> "\" role=\"img\" aria-labelledby=\"title description\">"
-      , "<title id=\"title\">" <> pictureTitle sizeBand <> "</title>"
-      , "<desc id=\"description\">CPU construction time for Spade, cdt, delaunator, and Moonlight over the upstream Delaunay comparison fixtures. Lower lines are faster.</desc>"
-      , "<defs>"
-      , "  <pattern id=\"triangulation-mesh\" width=\"54\" height=\"46\" patternUnits=\"userSpaceOnUse\">"
-      , "    <path d=\"M0 46L27 0L54 46ZM0 46L54 46M27 0L27 46\" fill=\"none\" stroke=\"rgb(255,140,0)\" stroke-width=\"0.7\"/>"
-      , "  </pattern>"
-      , "  <clipPath id=\"header-mesh-clip\"><rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" rx=\"20\"/></clipPath>"
-      , "</defs>"
-      , "<rect width=\""
-          <> coordinate (pictureWidth pictureLayout)
-          <> "\" height=\""
-          <> coordinate (pictureHeight pictureLayout)
-          <> "\" fill=\"white\"/>"
-      , "<rect x=\"955\" y=\"8\" width=\"290\" height=\"82\" fill=\"url(#triangulation-mesh)\" opacity=\"0.10\" clip-path=\"url(#header-mesh-clip)\"/>"
-      , svgText 640 34 "middle" 24 "500" (pictureTitle sizeBand)
-      , svgText 640 61 "middle" 13 "400" "Moonlight against Spade's construction referents · CPU time · lower is faster"
-      ]
-        <> renderLegend
-        <> concat (zipWith (renderPanel pointCounts axisMaximum) (picturePanelLefts pictureLayout) panels)
-        <> [ svgText 640 704 "middle" 11 "400" "Whiskers show tasty-bench's reported 2× standard-deviation spread."
-           , "</svg>"
-           ]
-    )
-
-pictureTitle :: SizeBand -> String
-pictureTitle = \case
-  Small -> "Moonlight Delaunay construction · small point sets"
-  Big -> "Moonlight Delaunay construction · big point sets"
-
-renderLegend :: [String]
-renderLegend =
-  concat
-    ( zipWith
-        renderLegendItem
-        [105, 315, 560, 735, 940]
-        allImplementations
-    )
-
-renderLegendItem :: Double -> Implementation -> [String]
-renderLegendItem x implementation =
-  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
-  , "  <line x1=\"" <> coordinate x <> "\" y1=\"101\" x2=\"" <> coordinate (x + 34) <> "\" y2=\"101\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\"" <> seriesDash implementation <> "/>"
-  , "  " <> renderMarker implementation (x + 17) 101
-  , "  " <> svgText (x + 43) 105 "start" 12 "400" (implementationLabel implementation)
-  , "</g>"
-  ]
-
-renderPanel :: [Int] -> Double -> Double -> PicturePanel -> [String]
-renderPanel pointCounts axisMaximum panelLeft panel =
-  [ "<g aria-label=\"" <> panelLabel <> "\">"
-  , svgText (panelLeft + panelWidth / 2) 144 "middle" 15 "500" panelLabel
-  ]
-    <> concatMap (renderHorizontalGrid panelLeft panelWidth axisMaximum) [0 .. 5]
-    <> concatMap (renderVerticalTick panelLeft panelWidth pointCounts) pointCounts
-    <> [ "<rect x=\"" <> coordinate panelLeft <> "\" y=\"" <> coordinate plotTop <> "\" width=\"" <> coordinate panelWidth <> "\" height=\"" <> coordinate plotHeight <> "\" fill=\"none\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
-       , svgText (panelLeft + panelWidth / 2) 664 "middle" 12 "400" "input size (points)"
-       , "<text x=\"" <> coordinate (panelLeft - 55) <> "\" y=\"" <> coordinate (plotTop + plotHeight / 2) <> "\" text-anchor=\"middle\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\"12\" fill=\"rgb(20,20,20)\" transform=\"rotate(-90 " <> coordinate (panelLeft - 55) <> " " <> coordinate (plotTop + plotHeight / 2) <> ")\">CPU time (ms)</text>"
-       ]
-    <> concatMap (renderSeries panelLeft panelWidth pointCounts axisMaximum) (picturePanelSeries panel)
-    <> ["</g>"]
- where
-  panelLabel = pointDistributionLabel (picturePanelDistribution panel)
-  panelWidth = picturePanelWidth pictureLayout
-  plotTop = picturePlotTop pictureLayout
-  plotHeight = picturePlotBottom pictureLayout - plotTop
-
-renderHorizontalGrid :: Double -> Double -> Double -> Int -> [String]
-renderHorizontalGrid panelLeft panelWidth axisMaximum tickIndex =
-  [ "<line x1=\"" <> coordinate panelLeft <> "\" y1=\"" <> coordinate y <> "\" x2=\"" <> coordinate (panelLeft + panelWidth) <> "\" y2=\"" <> coordinate y <> "\" stroke=\"rgb(170,170,170)\" stroke-width=\"0.7\" stroke-dasharray=\"2 4\"/>"
-  , svgText (panelLeft - 10) (y + 4) "end" 11 "400" (axisLabel tickValue)
-  ]
- where
-  tickValue = axisMaximum * fromIntegral tickIndex / 5
-  y = pictureY axisMaximum tickValue
-
-renderVerticalTick :: Double -> Double -> [Int] -> Int -> [String]
-renderVerticalTick panelLeft panelWidth pointCounts pointCount =
-  [ "<line x1=\"" <> coordinate x <> "\" y1=\"610\" x2=\"" <> coordinate x <> "\" y2=\"616\" stroke=\"rgb(25,25,25)\" stroke-width=\"1\"/>"
-  , svgText x 635 "middle" 11 "400" (pointCountLabel pointCount)
-  ]
- where
-  x = pictureX panelLeft panelWidth pointCounts pointCount
-
-renderSeries :: Double -> Double -> [Int] -> Double -> PictureSeries -> [String]
-renderSeries panelLeft panelWidth pointCounts axisMaximum series =
-  [ "<g aria-label=\"" <> implementationLabel implementation <> "\">"
-  , "  <polyline points=\"" <> unwords (observationPoint <$> observations) <> "\" fill=\"none\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"" <> seriesStrokeWidth implementation <> "\" stroke-linecap=\"round\" stroke-linejoin=\"round\"" <> seriesDash implementation <> "/>"
-  ]
-    <> concatMap renderObservation observations
-    <> ["</g>"]
- where
-  implementation = pictureSeriesImplementation series
-  observations = pictureSeriesObservations series
-  observationPoint observation =
-    coordinate (observationX observation) <> "," <> coordinate (observationY observation)
-  observationX =
-    pictureX panelLeft panelWidth pointCounts
-      . pointCountValue
-      . fixturePointCount
-      . observationFixture
-      . benchmarkKey
-  observationY = pictureY axisMaximum . observationMeanMilliseconds
-  renderObservation observation =
-    [ "  <line x1=\"" <> coordinate x <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate x <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate upperY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate upperY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  <line x1=\"" <> coordinate (x - 3) <> "\" y1=\"" <> coordinate lowerY <> "\" x2=\"" <> coordinate (x + 3) <> "\" y2=\"" <> coordinate lowerY <> "\" stroke=\"" <> implementationColor implementation <> "\" stroke-width=\"0.9\" opacity=\"0.48\"/>"
-    , "  " <> renderMarker implementation x y
-    ]
-   where
-    x = observationX observation
-    y = observationY observation
-    upperY = pictureY axisMaximum (observationUpperMilliseconds observation)
-    lowerY = pictureY axisMaximum (observationLowerMilliseconds observation)
-
-implementationColor :: Implementation -> String
-implementationColor = \case
-  Spade -> "rgb(0,0,139)"
-  SpadeHierarchy -> "rgb(139,0,139)"
-  Cdt -> "rgb(178,34,34)"
-  Delaunator -> "rgb(0,139,139)"
-  Moonlight -> "rgb(255,140,0)"
-
-seriesStrokeWidth :: Implementation -> String
-seriesStrokeWidth = \case
-  Moonlight -> "3.2"
-  _ -> "2.0"
-
-seriesDash :: Implementation -> String
-seriesDash = \case
-  SpadeHierarchy -> " stroke-dasharray=\"7 4\""
-  _ -> ""
-
-renderMarker :: Implementation -> Double -> Double -> String
-renderMarker implementation x y =
-  case implementation of
-    Spade -> circle 3.2
-    SpadeHierarchy ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.3) <> " L " <> coordinate (x - 4.1) <> " " <> coordinate (y + 3.5) <> " L " <> coordinate (x + 4.1) <> " " <> coordinate (y + 3.5) <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Cdt ->
-      "<rect x=\"" <> coordinate (x - 3.2) <> "\" y=\"" <> coordinate (y - 3.2) <> "\" width=\"6.4\" height=\"6.4\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Delaunator ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 4.2) <> " L " <> coordinate (x + 4.2) <> " " <> coordinate y <> " L " <> coordinate x <> " " <> coordinate (y + 4.2) <> " L " <> coordinate (x - 4.2) <> " " <> coordinate y <> " Z\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-    Moonlight ->
-      "<path d=\"M " <> coordinate x <> " " <> coordinate (y - 5.4) <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y - 1.7) <> " L " <> coordinate (x + 5.4) <> " " <> coordinate y <> " L " <> coordinate (x + 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate x <> " " <> coordinate (y + 5.4) <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y + 1.7) <> " L " <> coordinate (x - 5.4) <> " " <> coordinate y <> " L " <> coordinate (x - 1.7) <> " " <> coordinate (y - 1.7) <> " Z\" fill=\"" <> color <> "\" stroke=\"white\" stroke-width=\"1\"/>"
- where
-  color = implementationColor implementation
-  circle radius =
-    "<circle cx=\"" <> coordinate x <> "\" cy=\"" <> coordinate y <> "\" r=\"" <> coordinate radius <> "\" fill=\"white\" stroke=\"" <> color <> "\" stroke-width=\"1.6\"/>"
-
-pictureX :: Double -> Double -> [Int] -> Int -> Double
-pictureX panelLeft panelWidth pointCounts pointCount =
-  case pointCounts of
-    [] -> panelLeft
-    firstCount : remainingCounts ->
-      let minimumCount = firstCount
-          maximumCount = foldr max firstCount remainingCounts
-          countRange = maximumCount - minimumCount
-       in if countRange == 0
-            then panelLeft + panelWidth / 2
-            else
-              panelLeft
-                + panelWidth
-                  * fromIntegral (pointCount - minimumCount)
-                  / fromIntegral countRange
-
-pictureY :: Double -> Double -> Double
-pictureY axisMaximum milliseconds =
-  picturePlotBottom pictureLayout
-    - plotHeight * min axisMaximum (max 0 milliseconds) / axisMaximum
- where
-  plotHeight = picturePlotBottom pictureLayout - picturePlotTop pictureLayout
-
-observationMeanMilliseconds :: BenchmarkObservation -> Double
-observationMeanMilliseconds = picosecondsToMilliseconds . benchmarkMeanPicoseconds
-
-observationUpperMilliseconds :: BenchmarkObservation -> Double
-observationUpperMilliseconds observation =
-  picosecondsToMilliseconds
-    (benchmarkMeanPicoseconds observation + benchmarkSpreadPicoseconds observation)
-
-observationLowerMilliseconds :: BenchmarkObservation -> Double
-observationLowerMilliseconds observation =
-  picosecondsToMilliseconds
-    (max 0 (benchmarkMeanPicoseconds observation - benchmarkSpreadPicoseconds observation))
-
-picosecondsToMilliseconds :: Double -> Double
-picosecondsToMilliseconds picoseconds = picoseconds / 1.0e9
-
-niceCeiling :: Double -> Double
-niceCeiling value
-  | value <= 0 = 1
-  | otherwise = niceFraction * magnitude
- where
-  magnitude = 10 ** fromIntegral (floor (logBase 10 value) :: Int)
-  fraction = value / magnitude
-  niceFraction
-    | fraction <= 1 = 1
-    | fraction <= 2 = 2
-    | fraction <= 5 = 5
-    | otherwise = 10
-
-axisLabel :: Double -> String
-axisLabel value
-  | value >= 10 = showFFloat (Just 0) value ""
-  | value >= 1 = showFFloat (Just 1) value ""
-  | otherwise = showFFloat (Just 2) value ""
-
-pointCountLabel :: Int -> String
-pointCountLabel pointCount
-  | pointCount `mod` 1000 == 0 = show (pointCount `div` 1000) <> "k"
-  | otherwise = show pointCount
-
-coordinate :: Double -> String
-coordinate value = showFFloat (Just 1) value ""
-
-svgText :: Double -> Double -> String -> Int -> String -> String -> String
-svgText x y anchor fontSize fontWeight content =
-  "<text x=\""
-    <> coordinate x
-    <> "\" y=\""
-    <> coordinate y
-    <> "\" text-anchor=\""
-    <> anchor
-    <> "\" font-family=\"Helvetica, Arial, sans-serif\" font-size=\""
-    <> show fontSize
-    <> "\" font-weight=\""
-    <> fontWeight
-    <> "\" fill=\"rgb(20,20,20)\">"
-    <> content
-    <> "</text>"
diff --git a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs b/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
deleted file mode 100644
--- a/docs/delaunay-compare/Moonlight/Triangulation/Bench/DelaunayCompare/Suite.hs
+++ /dev/null
@@ -1,258 +0,0 @@
-{-# LANGUAGE DeriveTraversable #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Fixture descent, compatibility gluing, and the authoritative benchmark view.
-module Moonlight.Triangulation.Bench.DelaunayCompare.Suite
-  ( PreparedSuite
-  , withPreparedSuite
-  , preflightSuite
-  , suiteBenchmarks
-  , suiteAgreementMessage
-  ) where
-
-import Control.DeepSeq (force)
-import Control.Exception (bracket, evaluate)
-import Data.Bifunctor (first)
-import Data.Either (lefts, rights)
-import Data.Foldable (toList, traverse_)
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-import Moonlight.Triangulation.Bench.DelaunayCompare.Native
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.Dcel (numInnerFaces, numVertices)
-import Moonlight.Triangulation.Types (BuildError, Point (Point))
-import Test.Tasty.Bench (Benchmark, bench, bgroup, nf, nfIO)
-
-data NativeSections value = NativeSections
-  { nativeSpadeSection :: !value
-  , nativeSpadeHierarchySection :: !value
-  , nativeCdtSection :: !value
-  , nativeDelaunatorSection :: !value
-  }
-  deriving stock (Functor, Foldable, Traversable)
-
-nativeImplementationSections :: NativeSections NativeImplementation
-nativeImplementationSections =
-  NativeSections
-    { nativeSpadeSection = NativeSpade
-    , nativeSpadeHierarchySection = NativeSpadeHierarchy
-    , nativeCdtSection = NativeCdt
-    , nativeDelaunatorSection = NativeDelaunator
-    }
-
-nativeSectionAt :: NativeImplementation -> NativeSections value -> value
-nativeSectionAt implementation sections =
-  case implementation of
-    NativeSpade -> nativeSpadeSection sections
-    NativeSpadeHierarchy -> nativeSpadeHierarchySection sections
-    NativeCdt -> nativeCdtSection sections
-    NativeDelaunator -> nativeDelaunatorSection sections
-
-data PreparedFixture = PreparedFixture
-  { preparedSpec :: !FixtureSpec
-  , preparedMoonlightPoints :: !(Vector.Vector Point)
-  , preparedNativeFixtures :: !(NativeSections NativeFixture)
-  }
-
-newtype PreparedSuite = PreparedSuite [PreparedFixture]
-
-withPreparedSuite
-  :: NativeApi
-  -> (PreparedSuite -> IO (Either CompareObstruction result))
-  -> IO (Either CompareObstruction result)
-withPreparedSuite api action = do
-  prepared <- prepareSuite api
-  case prepared of
-    Left obstruction -> pure (Left obstruction)
-    Right suite -> bracket (pure suite) (releaseSuite api) action
-
-prepareSuite :: NativeApi -> IO (Either CompareObstruction PreparedSuite)
-prepareSuite api = do
-  outcomes <- traverse (prepareFixture api) allFixtureSpecs
-  case NonEmpty.nonEmpty (lefts outcomes) of
-    Just obstructions -> do
-      traverse_ (releasePreparedFixture api) (rights outcomes)
-      pure (Left (ComparisonObstructions obstructions))
-    Nothing -> pure (Right (PreparedSuite (rights outcomes)))
-
-prepareFixture :: NativeApi -> FixtureSpec -> IO (Either CompareObstruction PreparedFixture)
-prepareFixture api fixture = do
-  generated <- generateCoordinates api fixture
-  case generated of
-    Left status -> pure (Left (FixtureGenerationFailed fixture status))
-    Right coordinates -> do
-      nativeFixtures <- prepareNativeFixtures api fixture coordinates
-      pure $
-        PreparedFixture
-          fixture
-          (Vector.map (uncurry Point) (generatedCoordinatePairs coordinates))
-          <$> nativeFixtures
-
-prepareNativeFixtures
-  :: NativeApi
-  -> FixtureSpec
-  -> GeneratedCoordinates
-  -> IO (Either CompareObstruction (NativeSections NativeFixture))
-prepareNativeFixtures api fixture coordinates = do
-  outcomes <- traverse prepareSection nativeImplementationSections
-  case sequenceA outcomes of
-    Right nativeFixtures -> pure (Right nativeFixtures)
-    Left obstruction -> do
-      traverse_ (releaseNativeFixture api) (rights (toList outcomes))
-      pure (Left obstruction)
- where
-  prepareSection implementation = do
-    prepared <- prepareNativeFixture api implementation coordinates
-    pure $
-      first
-        (preparationObstruction implementation)
-        prepared
-
-  preparationObstruction implementation failure =
-    case failure of
-      NativePreparationStatus status -> FixturePreparationFailed fixture implementation status
-      NativePreparationReturnedNull -> NativePreparedNull fixture implementation
-
-releaseSuite :: NativeApi -> PreparedSuite -> IO ()
-releaseSuite api (PreparedSuite fixtures) =
-  traverse_ (releasePreparedFixture api) fixtures
-
-releasePreparedFixture :: NativeApi -> PreparedFixture -> IO ()
-releasePreparedFixture api = releasePreparedNativeFixtures api . preparedNativeFixtures
-
-releasePreparedNativeFixtures :: NativeApi -> NativeSections NativeFixture -> IO ()
-releasePreparedNativeFixtures api = traverse_ (releaseNativeFixture api)
-
-preflightSuite :: NativeApi -> PreparedSuite -> IO (Either CompareObstruction ())
-preflightSuite api (PreparedSuite fixtures) = do
-  outcomes <- traverse (preflightFixture api) fixtures
-  pure $
-    case NonEmpty.nonEmpty (lefts outcomes) of
-      Just obstructions -> Left (ComparisonObstructions obstructions)
-      Nothing -> Right ()
-
-preflightFixture :: NativeApi -> PreparedFixture -> IO (Either CompareObstruction ())
-preflightFixture api fixture = do
-  moonlightResult <-
-    evaluate
-      ( force
-          (moonlightConstructionSummary (preparedMoonlightPoints fixture))
-      )
-  nativeOutcomes <- traverse runSection nativeImplementationSections
-  pure $ do
-    moonlightSummary <-
-      first
-        (MoonlightConstructionFailed spec)
-        moonlightResult
-    nativeSummaries <- sequenceA nativeOutcomes
-    traverse_
-      ( \implementation ->
-          requireSummaryAgreement
-            spec
-            moonlightSummary
-            ( implementationOfNative implementation
-            , nativeSectionAt implementation nativeSummaries
-            )
-      )
-      nativeImplementationSections
- where
-  spec = preparedSpec fixture
-  nativeFixtures = preparedNativeFixtures fixture
-  runSection implementation =
-    first (NativeConstructionFailed spec implementation)
-      <$> runNativeFixture api (nativeSectionAt implementation nativeFixtures)
-
-requireSummaryAgreement
-  :: FixtureSpec
-  -> DelaunaySummary
-  -> (Implementation, DelaunaySummary)
-  -> Either CompareObstruction ()
-requireSummaryAgreement fixture expected (implementation, observed) =
-  if observed == expected
-    then Right ()
-    else Left (SummaryDisagreement fixture implementation expected observed)
-
-moonlightConstructionSummary
-  :: Vector.Vector Point
-  -> Either BuildError DelaunaySummary
-moonlightConstructionSummary points = do
-  triangulation <- delaunayGeometry points
-  pure
-    DelaunaySummary
-      { summaryVertexCount = numVertices triangulation
-      , summaryTriangleCount = numInnerFaces triangulation
-      }
-
-suiteBenchmarks :: NativeApi -> PreparedSuite -> [Benchmark]
-suiteBenchmarks api (PreparedSuite fixtures) =
-  fmap (sizeBandBenchmarks api fixtures) allSizeBands
-
-sizeBandBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Benchmark
-sizeBandBenchmarks api fixtures sizeBand =
-  bgroup
-    ("comparison: creation benchmark (" <> sizeBandLabel sizeBand <> ")")
-    (fmap (implementationBenchmarks api fixtures sizeBand) allImplementations)
-
-implementationBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Implementation
-  -> Benchmark
-implementationBenchmarks api fixtures sizeBand implementation =
-  bgroup
-    (implementationLabel implementation)
-    ( fmap
-        (distributionBenchmarks api fixtures sizeBand implementation)
-        allPointDistributions
-    )
-
-distributionBenchmarks
-  :: NativeApi
-  -> [PreparedFixture]
-  -> SizeBand
-  -> Implementation
-  -> PointDistribution
-  -> Benchmark
-distributionBenchmarks api fixtures sizeBand implementation distribution =
-  bgroup
-    (pointDistributionLabel distribution)
-    ( fmap
-        (fixtureBenchmark api implementation)
-        ( filter
-            (matchesFixture sizeBand distribution . preparedSpec)
-            fixtures
-        )
-    )
-
-matchesFixture :: SizeBand -> PointDistribution -> FixtureSpec -> Bool
-matchesFixture sizeBand distribution fixture =
-  fixtureSizeBand fixture == sizeBand
-    && fixturePointDistribution fixture == distribution
-
-fixtureBenchmark :: NativeApi -> Implementation -> PreparedFixture -> Benchmark
-fixtureBenchmark api implementation fixture =
-  bench (show (pointCountValue (fixturePointCount (preparedSpec fixture)))) $
-    case implementation of
-      Moonlight -> nf moonlightConstructionSummary (preparedMoonlightPoints fixture)
-      Spade -> nativeBenchmark NativeSpade
-      SpadeHierarchy -> nativeBenchmark NativeSpadeHierarchy
-      Cdt -> nativeBenchmark NativeCdt
-      Delaunator -> nativeBenchmark NativeDelaunator
- where
-  nativeFixtures = preparedNativeFixtures fixture
-  nativeBenchmark nativeImplementation =
-    nfIO (runNativeFixture api (nativeSectionAt nativeImplementation nativeFixtures))
-
-suiteAgreementMessage :: String
-suiteAgreementMessage =
-  "delaunay-compare agreement: "
-    <> show (length allFixtureSpecs)
-    <> " fixtures agree on vertex and triangle counts across "
-    <> show (length allImplementations)
-    <> " implementations"
diff --git a/docs/delaunay-compare/PictureMain.hs b/docs/delaunay-compare/PictureMain.hs
--- a/docs/delaunay-compare/PictureMain.hs
+++ b/docs/delaunay-compare/PictureMain.hs
@@ -8,8 +8,8 @@
 import Control.Monad.Trans.Except (ExceptT (ExceptT), except, runExceptT)
 import Data.Bifunctor (first)
 import Data.Foldable (traverse_)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Domain (allSizeBands)
-import Moonlight.Triangulation.Bench.DelaunayCompare.Picture
+import Moonlight.Planar.Bench.DelaunayCompare.Domain (allSizeBands)
+import Moonlight.Planar.Bench.DelaunayCompare.Picture
 import System.Directory (createDirectoryIfMissing)
 import System.Environment (getArgs)
 import System.Exit (die)
@@ -64,7 +64,7 @@
 renderPictureCommandFailure :: PictureCommandFailure -> String
 renderPictureCommandFailure = \case
   PictureCommandUsage ->
-    "usage: moonlight-triangulation-delaunay-pictures INPUT.csv OUTPUT_DIRECTORY"
+    "usage: moonlight-planar-delaunay-pictures INPUT.csv OUTPUT_DIRECTORY"
   PictureCommandReadFailed path details ->
     "could not read benchmark CSV " <> path <> ": " <> details
   PictureCommandCreateDirectoryFailed path details ->
diff --git a/docs/delaunay-compare/README.md b/docs/delaunay-compare/README.md
--- a/docs/delaunay-compare/README.md
+++ b/docs/delaunay-compare/README.md
@@ -2,7 +2,7 @@
 
 [Browse all exhibits.](../README.md)
 
-`moonlight-triangulation-delaunay-compare` is Moonlight's package-owned port of Spade's
+`moonlight-planar-delaunay-compare` is Moonlight's package-owned port of Spade's
 [`delaunay_compare`](https://github.com/Stoeoef/spade/tree/c8befc96bbbc1898a89cb19f9f3104a848936374/delaunay_compare)
 construction suite. It retains the upstream case matrix and appends
 `moonlight-triangulation` to the implementation list:
@@ -82,7 +82,7 @@
   `069d3bb1639b6d865fe4491b2fbbb9d0cdc8324173e30bebf690ed6592190c20`
 
 The Haskell projection in
-`Moonlight.Triangulation.Bench.DelaunayCompare.Picture` parses the tasty-bench
+`Moonlight.Planar.Bench.DelaunayCompare.Picture` parses the tasty-bench
 CSV against the exact closed 120-case registry and reports typed obstructions
 for malformed, unknown, duplicated, or missing observations before gluing
 either picture.
@@ -93,7 +93,7 @@
 
 ```console
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-delaunay-compare \
+  moonlight-planar:exe:moonlight-planar-delaunay-compare \
   --project-file=cabal.project.planar-dev \
   -- --list-tests
 ```
@@ -102,7 +102,7 @@
 
 ```console
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-delaunay-compare \
+  moonlight-planar:exe:moonlight-planar-delaunay-compare \
   --project-file=cabal.project.planar-dev \
   -- --time-mode wall -j1
 ```
@@ -121,7 +121,7 @@
 
 ```console
 caffeinate -i scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-delaunay-compare \
+  moonlight-planar:exe:moonlight-planar-delaunay-compare \
   --project-file=cabal.project.planar-dev -- \
   --stdev 5 --timeout 30s --time-mode cpu -j1 \
   --csv foundation/moonlight-planar/docs/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
@@ -133,7 +133,7 @@
 
 ```console
 scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-delaunay-pictures \
+  moonlight-planar:exe:moonlight-planar-delaunay-pictures \
   --project-file=cabal.project.planar-dev -- \
   foundation/moonlight-planar/docs/delaunay-compare/results/moonlight-delaunay-compare-YYYY-MM-DD.csv \
   foundation/moonlight-planar/docs/delaunay-compare/results
diff --git a/docs/examples/Moonlight/Planar/Example/ActivationZigzag.hs b/docs/examples/Moonlight/Planar/Example/ActivationZigzag.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Planar/Example/ActivationZigzag.hs
@@ -0,0 +1,57 @@
+module Moonlight.Planar.Example.ActivationZigzag
+  ( ActivationZigzagExampleError (..)
+  , squareLoopAcrossDepths
+  ) where
+
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Vector qualified as Vector
+import Moonlight.Homology.Chain (HomologicalDegree (..))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (RadiusSquaredError, mkRadiusSquared)
+import Moonlight.Planar.Zigzag
+  ( ActivationInterval
+  , ActivationPoint (..)
+  , ActivationSlice (..)
+  , ActivationZigzagError
+  , ZigzagInterval (..)
+  , activationAlphaPersistence
+  )
+
+data ActivationZigzagExampleError
+  = ActivationRadiusFailed !RadiusSquaredError
+  | ActivationPersistenceFailed !(ActivationZigzagError Int String)
+  deriving stock (Eq, Show)
+
+-- | Observe a square before its boundary appears, while it carries one loop,
+-- and after its two Delaunay faces fill that loop.  The returned interval uses
+-- the expanded observation/union stages rather than pretending the slices are
+-- a monotone filtration.
+squareLoopAcrossDepths :: Either ActivationZigzagExampleError [ActivationInterval Int]
+squareLoopAcrossDepths = do
+  let depths :: NonEmpty Int
+      depths = 0 :| [1, 2]
+  thresholds <-
+    traverse
+      (first ActivationRadiusFailed . mkRadiusSquared . fromIntegral)
+      depths
+  intervals <-
+    first ActivationPersistenceFailed
+      ( activationAlphaPersistence
+          ( NonEmpty.zipWith
+              (\depthValue threshold -> ActivationSlice depthValue threshold squarePoints)
+              depths
+              thresholds
+          )
+      )
+  pure (filter ((== HomologicalDegree 1) . zigzagIntervalDegree) intervals)
+
+squarePoints :: Vector.Vector (ActivationPoint String)
+squarePoints =
+  Vector.fromList
+    [ ActivationPoint "south-west" (Point (-1) (-1))
+    , ActivationPoint "south-east" (Point 1 (-1))
+    , ActivationPoint "north-east" (Point 1 1)
+    , ActivationPoint "north-west" (Point (-1) 1)
+    ]
diff --git a/docs/examples/Moonlight/Planar/Example/AlphaBoundary.hs b/docs/examples/Moonlight/Planar/Example/AlphaBoundary.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Planar/Example/AlphaBoundary.hs
@@ -0,0 +1,59 @@
+module Moonlight.Planar.Example.AlphaBoundary
+  ( AlphaBoundaryExampleError (..)
+  , BoundaryShape (..)
+  , alphaBoundaryShapes
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.Vector as Vector
+import Moonlight.Planar.Alpha (alphaShapeContainsFace)
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.FloodFillIterator (BoundaryObstruction (..), RegionBoundary, boundaryLoopVertices, componentBoundary, faceComponents, regionBoundaryHoleLoops, regionBoundaryOuterLoop)
+import Moonlight.Planar.Types (BuildError (..))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (RadiusSquaredError (..), mkRadiusSquared)
+
+data AlphaBoundaryExampleError
+  = AlphaBoundaryBuildFailed !BuildError
+  | AlphaBoundaryRadiusFailed !RadiusSquaredError
+  | AlphaBoundaryDescentFailed !BoundaryObstruction
+  deriving stock (Eq, Show)
+
+data BoundaryShape = BoundaryShape
+  { boundaryOuterVertexCount :: !Int
+  , boundaryHoleCount :: !Int
+  }
+  deriving stock (Eq, Show)
+
+-- | Descend the closed alpha-shape face section of one square to its boundary.
+alphaBoundaryShapes :: Either AlphaBoundaryExampleError [BoundaryShape]
+alphaBoundaryShapes = do
+  mesh <-
+    first AlphaBoundaryBuildFailed
+      ( delaunayGeometry
+          ( Vector.fromList
+              [ Point 0 0
+              , Point 2 0
+              , Point 2 2
+              , Point 0 2
+              ]
+          )
+      )
+  threshold <- first AlphaBoundaryRadiusFailed (mkRadiusSquared 2)
+  let selectedComponents =
+        fmap snd
+          (filter fst (faceComponents mesh (alphaShapeContainsFace threshold mesh)))
+  traverse
+    ( fmap boundaryShape
+        . first AlphaBoundaryDescentFailed
+        . componentBoundary mesh
+    )
+    selectedComponents
+
+boundaryShape :: RegionBoundary -> BoundaryShape
+boundaryShape boundary =
+  BoundaryShape
+    { boundaryOuterVertexCount =
+        length (boundaryLoopVertices (regionBoundaryOuterLoop boundary))
+    , boundaryHoleCount = length (regionBoundaryHoleLoops boundary)
+    }
diff --git a/docs/examples/Moonlight/Planar/Example/ConstrainedRegion.hs b/docs/examples/Moonlight/Planar/Example/ConstrainedRegion.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Planar/Example/ConstrainedRegion.hs
@@ -0,0 +1,40 @@
+module Moonlight.Planar.Example.ConstrainedRegion
+  ( constrainedRegionCounts
+  ) where
+
+import qualified Data.Vector as Vector
+import Moonlight.Planar.Cdt (CdtError (..), boundedRegionFaces, constrainedDelaunay)
+import Moonlight.Planar.Dcel (numFaces)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Types (buildTriangulation, unitElementDefaults)
+
+-- | Count every face and only the bounded odd-depth domain faces of an annulus.
+constrainedRegionCounts :: Either CdtError (Int, Int)
+constrainedRegionCounts = do
+  result <-
+    constrainedDelaunay
+      unitElementDefaults
+      (Vector.fromList annulusPoints)
+      (Vector.fromList (closedLoop 0 <> closedLoop 4))
+  let annulus = buildTriangulation result
+  pure (numFaces annulus, length (boundedRegionFaces annulus))
+
+annulusPoints :: [Point]
+annulusPoints =
+  [ Point (-2) (-2)
+  , Point 2 (-2)
+  , Point 2 2
+  , Point (-2) 2
+  , Point (-1) (-1)
+  , Point 1 (-1)
+  , Point 1 1
+  , Point (-1) 1
+  ]
+
+closedLoop :: Int -> [(Int, Int)]
+closedLoop offset =
+  [ (offset, offset + 1)
+  , (offset + 1, offset + 2)
+  , (offset + 2, offset + 3)
+  , (offset + 3, offset)
+  ]
diff --git a/docs/examples/Moonlight/Planar/Example/PlanarRegion.hs b/docs/examples/Moonlight/Planar/Example/PlanarRegion.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Planar/Example/PlanarRegion.hs
@@ -0,0 +1,145 @@
+module Moonlight.Planar.Example.PlanarRegion
+  ( ExactFractionSummary (..)
+  , CellValuationSummary (..)
+  , PlanarExampleError (..)
+  , PlanarExampleSummary (..)
+  , planarExample
+  ) where
+
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import Moonlight.Planar.CellSet (ExactCellSet)
+import Moonlight.Planar.Convex (ConvexError (..), convexPolygon)
+import Moonlight.Planar.Exact (exactPoint)
+import Moonlight.Planar.Internal.ExactRational (exactRationalDenominator, exactRationalNumerator)
+import Moonlight.Planar.Minkowski (MinkowskiError (..), closeWith, erodeBy, minkowskiSum, openWith, polygonOffset, structuringElement)
+import Moonlight.Planar.Overlay (OverlayError (..), OverlaySelectionError (..), overlayClosedIntersection, overlayClosedUnion, overlayLayers, overlayRegularizedDifference)
+import Moonlight.Planar.Region (ExactLoop, PlanarLayer, PlanarRegion, RegionValidationError (..), exactLoop, exactLoopPoints, planarLayer, planarRegion, polygonComponent)
+import Moonlight.Planar.Valuation (PlanarValuations, ValuationError (..), cellValuations, eulerCharacteristicValue, exactAreaValue, regionValuations, valuationArea, valuationEuler)
+
+data ExactFractionSummary = ExactFractionSummary
+  { fractionNumerator :: !Integer
+  , fractionDenominator :: !Integer
+  }
+  deriving stock (Eq, Show)
+
+data CellValuationSummary = CellValuationSummary
+  { cellEulerCharacteristic :: !Int
+  , cellExactArea :: !ExactFractionSummary
+  }
+  deriving stock (Eq, Show)
+
+data PlanarExampleSummary = PlanarExampleSummary
+  { booleanValuations :: ![CellValuationSummary]
+  , morphologyAreas :: ![ExactFractionSummary]
+  }
+  deriving stock (Eq, Show)
+
+data PlanarExampleError
+  = PlanarRegionFailed !RegionValidationError
+  | PlanarOverlayFailed !OverlayError
+  | PlanarSelectionFailed !OverlaySelectionError
+  | PlanarValuationFailed !ValuationError
+  | PlanarConvexFailed !ConvexError
+  | PlanarMinkowskiFailed !MinkowskiError
+  deriving stock (Eq, Show)
+
+-- | Compute exact Boolean valuations and morphology through the public facade.
+planarExample :: Either PlanarExampleError PlanarExampleSummary
+planarExample = do
+  left <- rectangleRegion 0 0 4 4
+  right <- rectangleRegion 2 1 6 3
+  leftLayer <- insideLayer left
+  rightLayer <- insideLayer right
+  overlay <- first PlanarOverlayFailed (overlayLayers leftLayer rightLayer)
+  unionCells <- first PlanarSelectionFailed (overlayClosedUnion id id overlay)
+  intersectionCells <-
+    first PlanarSelectionFailed (overlayClosedIntersection id id overlay)
+  differenceCells <-
+    first PlanarSelectionFailed (overlayRegularizedDifference id id overlay)
+  cellSummaries <-
+    traverse
+      cellValuationSummary
+      [unionCells, intersectionCells, differenceCells]
+
+  kernelLoop <- rectangleLoop (-1) (-1) 1 1
+  kernelPolygon <-
+    first PlanarConvexFailed (convexPolygon (exactLoopPoints kernelLoop))
+  kernel <- first PlanarMinkowskiFailed (structuringElement kernelPolygon)
+  (sumRegion, _) <- first PlanarMinkowskiFailed (minkowskiSum left right)
+  (offsetRegion, _) <- first PlanarMinkowskiFailed (polygonOffset kernel left)
+  (insetRegion, _) <- first PlanarMinkowskiFailed (erodeBy kernel left)
+  (openedRegion, _) <- first PlanarMinkowskiFailed (openWith kernel left)
+  (closedRegion, _) <- first PlanarMinkowskiFailed (closeWith kernel left)
+  areaSummaries <-
+    traverse
+      regionAreaSummary
+      [sumRegion, offsetRegion, insetRegion, openedRegion, closedRegion]
+
+  pure
+    PlanarExampleSummary
+      { booleanValuations = cellSummaries
+      , morphologyAreas = areaSummaries
+      }
+
+rectangleLoop
+  :: Integer
+  -> Integer
+  -> Integer
+  -> Integer
+  -> Either PlanarExampleError ExactLoop
+rectangleLoop minimumX minimumY maximumX maximumY =
+  first PlanarRegionFailed
+    ( exactLoop
+        ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
+            :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
+               , exactPoint (fromInteger maximumX) (fromInteger maximumY)
+               , exactPoint (fromInteger minimumX) (fromInteger maximumY)
+               ]
+        )
+    )
+
+rectangleRegion
+  :: Integer
+  -> Integer
+  -> Integer
+  -> Integer
+  -> Either PlanarExampleError PlanarRegion
+rectangleRegion minimumX minimumY maximumX maximumY = do
+  outer <- rectangleLoop minimumX minimumY maximumX maximumY
+  component <- first PlanarRegionFailed (polygonComponent outer [])
+  first PlanarRegionFailed (planarRegion [component])
+
+insideLayer :: PlanarRegion -> Either PlanarExampleError (PlanarLayer Bool)
+insideLayer =
+  first PlanarRegionFailed
+    . planarLayer False
+    . Map.singleton True
+
+cellValuationSummary
+  :: ExactCellSet
+  -> Either PlanarExampleError CellValuationSummary
+cellValuationSummary cells = do
+  valuations <- first PlanarValuationFailed (cellValuations cells)
+  pure
+    CellValuationSummary
+      { cellEulerCharacteristic =
+          eulerCharacteristicValue (valuationEuler valuations)
+      , cellExactArea = exactAreaSummary valuations
+      }
+
+regionAreaSummary
+  :: PlanarRegion
+  -> Either PlanarExampleError ExactFractionSummary
+regionAreaSummary region =
+  exactAreaSummary
+    <$> first PlanarValuationFailed (regionValuations region)
+
+exactAreaSummary :: PlanarValuations -> ExactFractionSummary
+exactAreaSummary valuations =
+  let area = exactAreaValue (valuationArea valuations)
+   in ExactFractionSummary
+        { fractionNumerator = exactRationalNumerator area
+        , fractionDenominator = exactRationalDenominator area
+        }
diff --git a/docs/examples/Moonlight/Planar/Example/SetAlgebra.hs b/docs/examples/Moonlight/Planar/Example/SetAlgebra.hs
new file mode 100644
--- /dev/null
+++ b/docs/examples/Moonlight/Planar/Example/SetAlgebra.hs
@@ -0,0 +1,30 @@
+module Moonlight.Planar.Example.SetAlgebra
+  ( siteCounts
+  ) where
+
+import qualified Data.Vector as Vector
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Dcel (numVertices)
+import Moonlight.Planar.SetAlgebra (intersection, union)
+import Moonlight.Planar.Types (BuildError (..), DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..))
+
+-- | Join and meet two overlapping site sets through the public facade.
+siteCounts :: Either BuildError (Int, Int)
+siteCounts = do
+  left <- unitSquareAt 0
+  right <- unitSquareAt 1
+  joined <- left `union` right
+  shared <- intersection left right
+  pure (numVertices joined, numVertices shared)
+
+unitSquareAt :: Double -> Either BuildError (DelaunayTriangulation ())
+unitSquareAt minimumX =
+  delaunayGeometry
+    ( Vector.fromList
+        [ Point minimumX 0
+        , Point (minimumX + 1) 0
+        , Point (minimumX + 1) 1
+        , Point minimumX 1
+        ]
+    )
diff --git a/docs/examples/Moonlight/Triangulation/Example/ActivationZigzag.hs b/docs/examples/Moonlight/Triangulation/Example/ActivationZigzag.hs
deleted file mode 100644
--- a/docs/examples/Moonlight/Triangulation/Example/ActivationZigzag.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-module Moonlight.Triangulation.Example.ActivationZigzag
-  ( ActivationZigzagExampleError (..)
-  , squareLoopAcrossDepths
-  ) where
-
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Vector qualified as Vector
-import Moonlight.Homology.Chain (HomologicalDegree (..))
-import Moonlight.Triangulation.Types
-  ( Point (..)
-  , RadiusSquaredError
-  , mkRadiusSquared
-  )
-import Moonlight.Triangulation.Zigzag
-  ( ActivationInterval
-  , ActivationPoint (..)
-  , ActivationSlice (..)
-  , ActivationZigzagError
-  , ZigzagInterval (..)
-  , activationAlphaPersistence
-  )
-
-data ActivationZigzagExampleError
-  = ActivationRadiusFailed !RadiusSquaredError
-  | ActivationPersistenceFailed !(ActivationZigzagError Int String)
-  deriving stock (Eq, Show)
-
--- | Observe a square before its boundary appears, while it carries one loop,
--- and after its two Delaunay faces fill that loop.  The returned interval uses
--- the expanded observation/union stages rather than pretending the slices are
--- a monotone filtration.
-squareLoopAcrossDepths :: Either ActivationZigzagExampleError [ActivationInterval Int]
-squareLoopAcrossDepths = do
-  let depths :: NonEmpty Int
-      depths = 0 :| [1, 2]
-  thresholds <-
-    traverse
-      (first ActivationRadiusFailed . mkRadiusSquared . fromIntegral)
-      depths
-  intervals <-
-    first ActivationPersistenceFailed
-      ( activationAlphaPersistence
-          ( NonEmpty.zipWith
-              (\depthValue threshold -> ActivationSlice depthValue threshold squarePoints)
-              depths
-              thresholds
-          )
-      )
-  pure (filter ((== HomologicalDegree 1) . zigzagIntervalDegree) intervals)
-
-squarePoints :: Vector.Vector (ActivationPoint String)
-squarePoints =
-  Vector.fromList
-    [ ActivationPoint "south-west" (Point (-1) (-1))
-    , ActivationPoint "south-east" (Point 1 (-1))
-    , ActivationPoint "north-east" (Point 1 1)
-    , ActivationPoint "north-west" (Point (-1) 1)
-    ]
diff --git a/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs b/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs
deleted file mode 100644
--- a/docs/examples/Moonlight/Triangulation/Example/AlphaBoundary.hs
+++ /dev/null
@@ -1,54 +0,0 @@
-module Moonlight.Triangulation.Example.AlphaBoundary
-  ( AlphaBoundaryExampleError (..)
-  , BoundaryShape (..)
-  , alphaBoundaryShapes
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation
-
-data AlphaBoundaryExampleError
-  = AlphaBoundaryBuildFailed !BuildError
-  | AlphaBoundaryRadiusFailed !RadiusSquaredError
-  | AlphaBoundaryDescentFailed !BoundaryObstruction
-  deriving stock (Eq, Show)
-
-data BoundaryShape = BoundaryShape
-  { boundaryOuterVertexCount :: !Int
-  , boundaryHoleCount :: !Int
-  }
-  deriving stock (Eq, Show)
-
--- | Descend the closed alpha-shape face section of one square to its boundary.
-alphaBoundaryShapes :: Either AlphaBoundaryExampleError [BoundaryShape]
-alphaBoundaryShapes = do
-  mesh <-
-    first AlphaBoundaryBuildFailed
-      ( delaunayGeometry
-          ( Vector.fromList
-              [ Point 0 0
-              , Point 2 0
-              , Point 2 2
-              , Point 0 2
-              ]
-          )
-      )
-  threshold <- first AlphaBoundaryRadiusFailed (mkRadiusSquared 2)
-  let selectedComponents =
-        fmap snd
-          (filter fst (faceComponents mesh (alphaShapeContainsFace threshold mesh)))
-  traverse
-    ( fmap boundaryShape
-        . first AlphaBoundaryDescentFailed
-        . componentBoundary mesh
-    )
-    selectedComponents
-
-boundaryShape :: RegionBoundary -> BoundaryShape
-boundaryShape boundary =
-  BoundaryShape
-    { boundaryOuterVertexCount =
-        length (boundaryLoopVertices (regionBoundaryOuterLoop boundary))
-    , boundaryHoleCount = length (regionBoundaryHoleLoops boundary)
-    }
diff --git a/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs b/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs
deleted file mode 100644
--- a/docs/examples/Moonlight/Triangulation/Example/ConstrainedRegion.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-module Moonlight.Triangulation.Example.ConstrainedRegion
-  ( constrainedRegionCounts
-  ) where
-
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation
-
--- | Count every face and only the bounded odd-depth domain faces of an annulus.
-constrainedRegionCounts :: Either CdtError (Int, Int)
-constrainedRegionCounts = do
-  result <-
-    constrainedDelaunay
-      unitElementDefaults
-      (Vector.fromList annulusPoints)
-      (Vector.fromList (closedLoop 0 <> closedLoop 4))
-  let annulus = buildTriangulation result
-  pure (numFaces annulus, length (boundedRegionFaces annulus))
-
-annulusPoints :: [Point]
-annulusPoints =
-  [ Point (-2) (-2)
-  , Point 2 (-2)
-  , Point 2 2
-  , Point (-2) 2
-  , Point (-1) (-1)
-  , Point 1 (-1)
-  , Point 1 1
-  , Point (-1) 1
-  ]
-
-closedLoop :: Int -> [(Int, Int)]
-closedLoop offset =
-  [ (offset, offset + 1)
-  , (offset + 1, offset + 2)
-  , (offset + 2, offset + 3)
-  , (offset + 3, offset)
-  ]
diff --git a/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs b/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs
deleted file mode 100644
--- a/docs/examples/Moonlight/Triangulation/Example/PlanarRegion.hs
+++ /dev/null
@@ -1,137 +0,0 @@
-module Moonlight.Triangulation.Example.PlanarRegion
-  ( ExactFractionSummary (..)
-  , CellValuationSummary (..)
-  , PlanarExampleError (..)
-  , PlanarExampleSummary (..)
-  , planarExample
-  ) where
-
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import Moonlight.Triangulation
-
-data ExactFractionSummary = ExactFractionSummary
-  { fractionNumerator :: !Integer
-  , fractionDenominator :: !Integer
-  }
-  deriving stock (Eq, Show)
-
-data CellValuationSummary = CellValuationSummary
-  { cellEulerCharacteristic :: !Int
-  , cellExactArea :: !ExactFractionSummary
-  }
-  deriving stock (Eq, Show)
-
-data PlanarExampleSummary = PlanarExampleSummary
-  { booleanValuations :: ![CellValuationSummary]
-  , morphologyAreas :: ![ExactFractionSummary]
-  }
-  deriving stock (Eq, Show)
-
-data PlanarExampleError
-  = PlanarRegionFailed !RegionValidationError
-  | PlanarOverlayFailed !(OverlayError Bool Bool)
-  | PlanarSelectionFailed !OverlaySelectionError
-  | PlanarValuationFailed !ValuationError
-  | PlanarMinkowskiFailed !MinkowskiError
-  deriving stock (Eq, Show)
-
--- | Compute exact Boolean valuations and morphology through the public facade.
-planarExample :: Either PlanarExampleError PlanarExampleSummary
-planarExample = do
-  left <- rectangleRegion 0 0 4 4
-  right <- rectangleRegion 2 1 6 3
-  leftLayer <- insideLayer left
-  rightLayer <- insideLayer right
-  overlay <- first PlanarOverlayFailed (overlayLayers leftLayer rightLayer)
-  unionCells <- first PlanarSelectionFailed (overlayClosedUnion id id overlay)
-  intersectionCells <-
-    first PlanarSelectionFailed (overlayClosedIntersection id id overlay)
-  differenceCells <-
-    first PlanarSelectionFailed (overlayRegularizedDifference id id overlay)
-  cellSummaries <-
-    traverse
-      cellValuationSummary
-      [unionCells, intersectionCells, differenceCells]
-
-  kernelLoop <- rectangleLoop (-1) (-1) 1 1
-  kernelPolygon <-
-    first PlanarMinkowskiFailed (convexPolygon (exactLoopPoints kernelLoop))
-  kernel <- first PlanarMinkowskiFailed (structuringElement kernelPolygon)
-  (sumRegion, _) <- first PlanarMinkowskiFailed (minkowskiSum left right)
-  (offsetRegion, _) <- first PlanarMinkowskiFailed (polygonOffset kernel left)
-  (insetRegion, _) <- first PlanarMinkowskiFailed (polygonInset kernel left)
-  (openedRegion, _) <- first PlanarMinkowskiFailed (openWith kernel left)
-  (closedRegion, _) <- first PlanarMinkowskiFailed (closeWith kernel left)
-  areaSummaries <-
-    traverse
-      regionAreaSummary
-      [sumRegion, offsetRegion, insetRegion, openedRegion, closedRegion]
-
-  pure
-    PlanarExampleSummary
-      { booleanValuations = cellSummaries
-      , morphologyAreas = areaSummaries
-      }
-
-rectangleLoop
-  :: Integer
-  -> Integer
-  -> Integer
-  -> Integer
-  -> Either PlanarExampleError ExactLoop
-rectangleLoop minimumX minimumY maximumX maximumY =
-  first PlanarRegionFailed
-    ( exactLoop
-        ( exactPoint (fromInteger minimumX) (fromInteger minimumY)
-            :| [ exactPoint (fromInteger maximumX) (fromInteger minimumY)
-               , exactPoint (fromInteger maximumX) (fromInteger maximumY)
-               , exactPoint (fromInteger minimumX) (fromInteger maximumY)
-               ]
-        )
-    )
-
-rectangleRegion
-  :: Integer
-  -> Integer
-  -> Integer
-  -> Integer
-  -> Either PlanarExampleError PlanarRegion
-rectangleRegion minimumX minimumY maximumX maximumY = do
-  outer <- rectangleLoop minimumX minimumY maximumX maximumY
-  component <- first PlanarRegionFailed (polygonComponent outer [])
-  first PlanarRegionFailed (planarRegion [component])
-
-insideLayer :: PlanarRegion -> Either PlanarExampleError (PlanarLayer Bool)
-insideLayer =
-  first PlanarRegionFailed
-    . planarLayer False
-    . Map.singleton True
-
-cellValuationSummary
-  :: ExactCellSet
-  -> Either PlanarExampleError CellValuationSummary
-cellValuationSummary cells = do
-  valuations <- first PlanarValuationFailed (cellValuations cells)
-  pure
-    CellValuationSummary
-      { cellEulerCharacteristic =
-          eulerCharacteristicValue (valuationEuler valuations)
-      , cellExactArea = exactAreaSummary valuations
-      }
-
-regionAreaSummary
-  :: PlanarRegion
-  -> Either PlanarExampleError ExactFractionSummary
-regionAreaSummary region =
-  exactAreaSummary
-    <$> first PlanarValuationFailed (regionValuations region)
-
-exactAreaSummary :: PlanarValuations -> ExactFractionSummary
-exactAreaSummary valuations =
-  let area = exactAreaValue (valuationArea valuations)
-   in ExactFractionSummary
-        { fractionNumerator = exactRationalNumerator area
-        , fractionDenominator = exactRationalDenominator area
-        }
diff --git a/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs b/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs
deleted file mode 100644
--- a/docs/examples/Moonlight/Triangulation/Example/SetAlgebra.hs
+++ /dev/null
@@ -1,26 +0,0 @@
-module Moonlight.Triangulation.Example.SetAlgebra
-  ( siteCounts
-  ) where
-
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation
-
--- | Join and meet two overlapping site sets through the public facade.
-siteCounts :: Either BuildError (Int, Int)
-siteCounts = do
-  left <- unitSquareAt 0
-  right <- unitSquareAt 1
-  joined <- left `union` right
-  shared <- intersection left right
-  pure (numVertices joined, numVertices shared)
-
-unitSquareAt :: Double -> Either BuildError (DelaunayTriangulation ())
-unitSquareAt minimumX =
-  delaunayGeometry
-    ( Vector.fromList
-        [ Point minimumX 0
-        , Point (minimumX + 1) 0
-        , Point (minimumX + 1) 1
-        , Point minimumX 1
-        ]
-    )
diff --git a/docs/examples/README.md b/docs/examples/README.md
--- a/docs/examples/README.md
+++ b/docs/examples/README.md
@@ -3,18 +3,19 @@
 [Return to the package overview.](../../README.md#use)
 · [Browse the documentation.](../README.md)
 
-The ordinary examples import only the public `Moonlight.Triangulation` facade.
+The ordinary examples import their public geometric and construction owners
+directly; there is no `Moonlight.Planar` umbrella.
 The activation example names its opt-in `zigzag` component explicitly. Package
 tests compile every module and own their deterministic expected results.
 
 | Workflow | Canonical source |
 | --- | --- |
-| Join and meet overlapping site sets | [`SetAlgebra.hs`](./Moonlight/Triangulation/Example/SetAlgebra.hs) |
-| Recover a constrained annulus interior | [`ConstrainedRegion.hs`](./Moonlight/Triangulation/Example/ConstrainedRegion.hs) |
-| Descend an alpha face section to its boundary | [`AlphaBoundary.hs`](./Moonlight/Triangulation/Example/AlphaBoundary.hs) |
-| Overlay, measure, and morph exact regions | [`PlanarRegion.hs`](./Moonlight/Triangulation/Example/PlanarRegion.hs) |
-| Track an alpha loop across non-nested depths | [`ActivationZigzag.hs`](./Moonlight/Triangulation/Example/ActivationZigzag.hs) |
+| Join and meet overlapping site sets | [`SetAlgebra.hs`](./Moonlight/Planar/Example/SetAlgebra.hs) |
+| Recover a constrained annulus interior | [`ConstrainedRegion.hs`](./Moonlight/Planar/Example/ConstrainedRegion.hs) |
+| Descend an alpha face section to its boundary | [`AlphaBoundary.hs`](./Moonlight/Planar/Example/AlphaBoundary.hs) |
+| Overlay, measure, and morph exact regions | [`PlanarRegion.hs`](./Moonlight/Planar/Example/PlanarRegion.hs) |
+| Track an alpha loop across non-nested depths | [`ActivationZigzag.hs`](./Moonlight/Planar/Example/ActivationZigzag.hs) |
 
-[The facade example test](../../test/examples/Main.hs) and
+[The public example test](../../test/examples/Main.hs) and
 [zigzag test](../../test/zigzag/Main.hs) check every published summary against
 these sources.
diff --git a/docs/illustration-study/Main.hs b/docs/illustration-study/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/illustration-study/Main.hs
@@ -0,0 +1,58 @@
+-- | The only effect boundary of the study: validate output location, render
+-- every fixed-camera variant, then publish ordinary SVG documents.
+module Main (main) where
+
+import Control.Monad (foldM)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import Moonlight.Planar.Exact
+import Moonlight.Planar.Exhibit.IllustrationStudy
+import Moonlight.Planar.Exhibit.EquationalGarden
+import Moonlight.Planar.Exhibit.SpaceSword
+import Moonlight.Planar.Illustration.Svg
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs)
+import System.Exit (die)
+import System.FilePath ((</>), isAbsolute)
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    [directory] | isAbsolute directory -> do
+      documents <- either die pure studyDocuments
+      createDirectoryIfMissing True directory
+      traverse_ (\(name,svg) -> writeFile (directory </> name) svg) documents
+      putStrLn ("Wrote fixed-camera illustration studies to " <> directory)
+    _ -> die "usage: moonlight-planar-illustration-study ABSOLUTE_OUTPUT_DIRECTORY"
+
+studyDocuments :: Either String [(FilePath,String)]
+studyDocuments = do
+  width <- first show (positiveExact 1200)
+  height <- first show (positiveExact 1000)
+  roundingValue <- first show (exactRational 1 100000)
+  toleranceValue <- first show (exactRational 1 4)
+  rounding <- first show (positiveExact roundingValue)
+  tolerance <- first show (positiveExact toleranceValue)
+  viewport <- first show (svgViewport 1200 1000 (exactPoint 0 0) width height)
+  precision <- first show (svgPrecision 5 rounding)
+  options <- first show (svgOptions viewport precision tolerance 24 100000)
+  edited <- first show $ foldM (\controls (control,value) -> editControl control value controls)
+    defaultControls [(LeftHornSweep,30),(EyeSpacing,66),(EyeTilt,12),(CloakFullness,30),(FullerWidth,20)]
+  let gardenEdited = defaultGardenControls { blossomOpening = unitOne, gardenBreeze = unitOne }
+  garden <- first show (gardenPicture defaultGardenControls)
+  editedGarden <- first show (gardenPicture gardenEdited)
+  traverse (\(name, rendered) -> (name,) <$> first show rendered)
+    [ ("knight-crescent.svg", renderSvg partName options (studyPicture defaultControls))
+    , ("knight-crescent-edited.svg", renderSvg partName options (studyPicture edited))
+    , ("knight-crescent-diagnostic.svg", renderDiagnosticSvg partName options (diagnosticPicture defaultControls))
+    , ("equational-garden.svg", renderSvg gardenPartName options garden)
+    , ("equational-garden-edited.svg", renderSvg gardenPartName options editedGarden)
+    , ("equational-garden-diagnostic.svg", renderDiagnosticSvg gardenPartName options garden)
+    , ("equational-garden-layout.svg", renderSvg gardenPartName options (gardenLayoutPicture unitZero))
+    , ("equational-garden-layout-edited.svg", renderSvg gardenPartName options (gardenLayoutPicture unitOne))
+    , ("space-sword.svg", renderSvg spaceSwordPartName options
+        (spaceSwordPicture defaultSpaceSwordControls))
+    , ("space-sword-overcharged.svg", renderSvg spaceSwordPartName options
+        (spaceSwordPicture overchargedSpaceSwordControls))
+    ]
diff --git a/docs/knight-rig/Main.hs b/docs/knight-rig/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/knight-rig/Main.hs
@@ -0,0 +1,147 @@
+module Main (main) where
+
+import Data.Bifunctor (first)
+import Data.Foldable (toList)
+import Data.List (mapAccumL)
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Planar.Affine (affine2)
+import qualified Moonlight.Planar.Curve as Curve
+import Moonlight.Planar.Exact
+import Moonlight.Planar.Exhibit.Geometry (ShapeError, solidObj, triangulateShape)
+import Moonlight.Planar.Point (Point (..), PointValidationError, queryPointValue)
+import System.Environment (getArgs)
+import System.Exit (die)
+
+type XZ = (Double, Double)
+data Part = Part String Double (NonEmpty XZ) [XZ]
+
+data KnightError = KnightGeometry ShapeError | KnightPoint PointValidationError
+  | KnightScalar ScalarRefinementError | KnightArithmetic ExactArithmeticError
+  deriving stock (Show)
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    [path] -> either (die . show) (writeFile path) knight
+    _ -> die "usage: moonlight-planar-knight-rig OUTPUT.obj"
+
+knight :: Either KnightError String
+knight = ("# Native Moonlight Planar CDT - layered Knight fan art\n" <>) . concat
+  <$> (parts >>= traverse build)
+ where
+  build :: Part -> Either KnightError String
+  build (Part name thickness outline seeds) =
+    solidObj name (const thickness) <$> first KnightGeometry (triangulateShape (fmap point outline :| []) (fmap point seeds))
+  point :: XZ -> Point
+  point (x,z) = Point (snap (750*x)) (snap (-750*z))
+  snap :: Double -> Double
+  snap v = fromIntegral (round (v*65536) :: Integer) / 65536
+
+-- A continuous mask-and-horn silhouette; the rigid mask is not an image card.
+mask :: Either KnightError (NonEmpty XZ)
+mask = ((0,0.98) :|) . concat <$> sequence
+  [ curve (0,0.98) (-0.31,0.96) (-0.41,1.10) (-0.39,1.32)
+  , curve (-0.39,1.32) (-0.40,1.53) (-0.49,1.72) (-0.38,1.88)
+  , curve (-0.38,1.88) (-0.40,1.68) (-0.20,1.64) (-0.22,1.45)
+  , curve (-0.22,1.45) (-0.12,1.40) (0.12,1.40) (0.22,1.45)
+  , curve (0.22,1.45) (0.20,1.64) (0.40,1.68) (0.38,1.88)
+  , curve (0.38,1.88) (0.49,1.72) (0.40,1.53) (0.39,1.32)
+  , curve (0.39,1.32) (0.41,1.10) (0.31,0.96) (0,0.98)
+  ]
+
+-- Existing sixteen samples per authored cubic, evaluated by the curve owner.
+curve :: XZ -> XZ -> XZ -> XZ -> Either KnightError [XZ]
+curve a b c d = do
+  start <- exactXZ a
+  controlA <- exactXZ b
+  controlB <- exactXZ c
+  end <- exactXZ d
+  let step = Curve.curveStep
+        (Curve.cubic (exactVectorFromPoints start controlA) (exactVectorFromPoints start controlB))
+        (exactVectorFromPoints start end)
+  parameters <- traverse (sampleParameter 16)
+    [i | i <- [1..16], i < 16 || d /= (0,0.98)]
+  traverse (embeddedXZ . translateExactPoint start . (`Curve.evaluateStep` step)) parameters
+
+-- The ellipse is the canonical four rational conics. Twelve parameter samples
+-- per quarter retain 48 vertices; rational parameter spacing is not uniform
+-- angular spacing. Trigonometry here specifies the tilted coordinate frame only.
+ellipse :: XZ -> XZ -> Double -> Either KnightError (NonEmpty XZ)
+ellipse (cx,cz) (rx,rz) tilt = do
+  center <- exactXZ (cx,cz)
+  axisX <- exactXZ (rx*cos tilt,rx*sin tilt)
+  axisY <- exactXZ (-rz*sin tilt,rz*cos tilt)
+  let origin = exactPoint 0 0
+      placement = affine2 (ExactVector 1 0) (ExactVector 0 1) (exactVectorFromPoints origin center)
+      outline = Curve.transformLocatedClosedTrail placement
+        (Curve.ellipse (exactVectorFromPoints origin axisX) (exactVectorFromPoints origin axisY))
+      start = Curve.location outline
+      (_, locatedSteps) = mapAccumL locateStep start
+        (toList (Curve.closedTrailSteps (Curve.locatedValue outline)))
+      locateStep :: ExactPoint -> Curve.CurveStep -> (ExactPoint,(ExactPoint,Curve.CurveStep))
+      locateStep point step = (translateExactPoint point (Curve.curveStepEnd step),(point,step))
+  parameters <- traverse (sampleParameter 12) [0..11]
+  let samples = concatMap (\(point,step) ->
+        map (translateExactPoint point . (`Curve.evaluateStep` step)) parameters) locatedSteps
+  traverse embeddedXZ (start :| drop 1 samples)
+
+sampleParameter :: Integer -> Integer -> Either KnightError UnitInterval
+sampleParameter count index = first KnightArithmetic (exactRational index count)
+  >>= first KnightScalar . unitInterval
+
+exactXZ :: XZ -> Either KnightError ExactPoint
+exactXZ (x,z) = first KnightPoint (exactPointFromPoint (Point x z))
+
+-- Binary64 projection is a transport candidate; the unchanged snapping and CDT
+-- admission below remain responsible for the actual mesh embedding.
+embeddedXZ :: ExactPoint -> Either KnightError XZ
+embeddedXZ point = toXZ . queryPointValue <$> first KnightPoint (exactPointToEmbeddingCandidate point)
+ where
+  toXZ :: Point -> XZ
+  toXZ (Point x z) = (x,z)
+
+ellipticPart :: String -> Double -> XZ -> XZ -> Double -> Either KnightError Part
+ellipticPart name thickness center axes tilt =
+  (\outline -> Part name thickness outline []) <$> ellipse center axes tilt
+
+strip :: (Double -> XZ) -> (Double -> Double) -> NonEmpty XZ
+strip center width = side (-1) 0 :|
+  ([side (-1) (fromIntegral i/24) | i <- [1..24 :: Int]]
+   <> [side 1 (fromIntegral i/24) | i <- [24,23..0 :: Int]])
+ where
+  side :: Double -> Double -> XZ
+  side sign t = let (x,z)=center t in (x+sign*width t,z)
+
+limb :: String -> Double -> Double -> Double -> Double -> Part
+limb name x top bottom width = Part name 0.026
+  (strip (\t -> (x + 0.022*sin(pi*t),top+(bottom-top)*t)) (\t -> width*(1-0.28*t)))
+  [(x+0.022*sin(pi*t),top+(bottom-top)*t) | i <- [1..23 :: Int], let t=fromIntegral i/24]
+
+cloak :: Int -> Part
+cloak i = Part ("Cloak_"<>show i) 0.014
+  (strip center width) [center (fromIntegral j/24) | j<-[1..23 :: Int]]
+ where
+  axis :: Double
+  axis=fromIntegral (i-2)
+  center :: Double -> XZ
+  center t = (axis*(0.064+0.102*t) + 0.008*sin(pi*t),1.025-(0.55+0.025*cos(axis*1.5))*t)
+  width :: Double -> Double
+  width t = (0.094+0.018*t)*(1-0.48*t^ (8::Int))
+
+parts :: Either KnightError [Part]
+parts = sequence $
+  [ (\outline -> Part "Mask" 0.052 outline []) <$> mask
+  , ellipticPart "Eye_L" 0.003 (-0.177,1.218) (0.072,0.103) (-0.12)
+  , ellipticPart "Eye_R" 0.003 (0.177,1.218) (0.072,0.103) 0.12
+  , ellipticPart "Body" 0.068 (0,0.665) (0.16,0.255) 0
+  , ellipticPart "Collar" 0.018 (0,0.991) (0.205,0.045) (-0.06)
+  , Right (limb "Leg_L" (-0.115) 0.57 0.075 0.047)
+  , Right (limb "Leg_R" 0.115 0.57 0.075 0.047)
+  , ellipticPart "Foot_L" 0.033 (-0.086,0.051) (0.079,0.033) 0
+  , ellipticPart "Foot_R" 0.033 (0.144,0.051) (0.079,0.033) 0
+  , Right (limb "Arm_L" (-0.23) 0.935 0.57 0.037)
+  , Right (limb "Arm_R" 0.23 0.935 0.57 0.037)
+  , ellipticPart "Hand_L" 0.027 (-0.23,0.565) (0.045,0.052) (-0.1)
+  , ellipticPart "Hand_R" 0.027 (0.23,0.565) (0.045,0.052) 0.1
+  ] <> fmap (Right . cloak) [0..4]
diff --git a/docs/knight-rig/README.md b/docs/knight-rig/README.md
new file mode 100644
--- /dev/null
+++ b/docs/knight-rig/README.md
@@ -0,0 +1,56 @@
+# Knight | native 2.5D character rig
+
+Fan art of the little Knight from [Hollow Knight](https://www.hollowknight.com/),
+original character by Team Cherry. No game asset or texture was extracted.
+
+Eighteen shallow solid layers are constructed once by Moonlight Planar's native
+constrained Delaunay builder. The mask and eye inlays remain rigid. Limbs use
+smooth normalized two-bone weights; five cloak panels have independent root/hem
+bones. Blender owns IK, skin evaluation, animation storage and export. The
+geometry is intended for front and modest three-quarter views, not a full turntable.
+
+## Regenerate
+
+From the repository root (replace the output directory if desired):
+
+```sh
+scripts/safe-cabal.sh run moonlight-planar:exe:moonlight-planar-knight-rig \
+  --project-file=cabal.project.planar-dev \
+  --builddir=/Volumes/Sirius/work/pm-artifacts/planar-collapse-20260907/build \
+  --offline -j6 -- /Volumes/Sirius/work/pm-artifacts/knight-rig-20260907/knight.obj
+blender --background --factory-startup --python-exit-code 1 \
+  --python compiler/foundation/moonlight-planar/docs/knight-rig/rig.py -- \
+  /Volumes/Sirius/work/pm-artifacts/knight-rig-20260907/knight.obj \
+  /Volumes/Sirius/work/pm-artifacts/knight-rig-20260907
+python3 compiler/foundation/moonlight-planar/docs/knight-rig/package.py \
+  /Volumes/Sirius/work/pm-artifacts/knight-rig-20260907
+```
+
+The interpreter targets Blender 5.1.1; preview packaging uses Pillow with WebP
+support. Outputs include `knight-rig.blend`, `knight-rig.glb`, still/skeleton
+previews, `knight-idle.webp`, `knight-walk.webp`, and measured rig/export checks.
+The shared native shape owner is `docs/art-common/.../Exhibit/Geometry.hs`;
+moonblade and this character import that same implementation, with no facade.
+
+## Pose and animation
+
+In Blender select **KNIGHT | 2.5D rig**, enter Pose Mode, and use ROOT, Hips,
+Chest, Head, foot/knee controls, arm/forearm FK bones and individual Cloak/Hem
+bones. Clear the active action to pose manually. Scale channels are locked to
+avoid accidental limb stretching. Foot IK uses two fixed-length links.
+
+The Action Editor contains **Knight | Idle** (3 s) and **Knight | Walk** (1.333 s),
+both sampled at 24 fps. Idle is stationary. Walk advances 0.468 m along +X per
+cycle; its stance trajectory cancels root travel, keeping the planted foot at
+one world position. Accumulate root displacement when repeating Walk. The
+preview camera follows the root so the looping display stays centered.
+
+GLB retains both sampled clips and linear skinning; Blender-only IK controls
+are not portable glTF constraints. There is no cloth simulation or motion
+capture: this is a deliberately authored stylized gait with cloak follow-through.
+
+Checks cover finite deformation, positive sampled face areas, weights summing
+to one, foot-control trajectories and two-bone reach over 34 poses, plus actual
+exported meshes, skin joints, clips and binary weights. They do not certify
+arbitrary poses, self-collision or anatomical realism. Regenerate from the
+source; do not patch generated meshes or pictures.
diff --git a/docs/knight-rig/package.py b/docs/knight-rig/package.py
new file mode 100644
--- /dev/null
+++ b/docs/knight-rig/package.py
@@ -0,0 +1,63 @@
+"""Encode existing native renders and verify the exported glTF payload."""
+from __future__ import annotations
+
+from itertools import chain
+from pathlib import Path
+import json
+import math
+import struct
+import sys
+from PIL import Image
+
+
+def animation(folder: Path,destination: Path) -> dict:
+    frames=tuple(map(lambda p:Image.open(p).convert('RGB'),sorted(folder.glob('frame-*.png'))))
+    frames[0].save(destination,save_all=True,append_images=frames[1:],duration=83,loop=0,quality=90,method=6)
+    return {'file':destination.name,'frames':len(frames),'width':frames[0].width,'height':frames[0].height,'fps':12}
+
+
+def glb_report(path: Path) -> dict:
+    payload=path.read_bytes()
+    n,kind=struct.unpack_from('<II',payload,12)
+    document=json.loads(payload[20:20+n])
+    binary=payload[28+n:]
+    component={5126:('f',4),5123:('H',2),5121:('B',1),5125:('I',4)}
+    arity={'SCALAR':1,'VEC2':2,'VEC3':3,'VEC4':4,'MAT4':16}
+    def accessor(index: int) -> tuple[tuple[float|int,...],...]:
+        a=document['accessors'][index]; view=document['bufferViews'][a['bufferView']]
+        fmt,size=component[a['componentType']]; count=arity[a['type']]
+        offset=view.get('byteOffset',0)+a.get('byteOffset',0)
+        stride=view.get('byteStride',size*count)
+        return tuple(map(lambda i:struct.unpack_from('<'+fmt*count,binary,offset+stride*i),range(a['count'])))
+    weights=tuple(chain.from_iterable(accessor(p['attributes']['WEIGHTS_0']) for mesh in document['meshes'] for p in mesh['primitives']))
+    clips=tuple({'name':a['name'],'duration_seconds':max(accessor(s['input'])[-1][0] for s in a['samplers']),
+                 'channels':len(a['channels'])} for a in document.get('animations',[]))
+    report={'meshes':len(document['meshes']),'skins':len(document.get('skins',[])),
+            'joints':tuple(document['nodes'][i]['name'] for i in document['skins'][0]['joints']),
+            'animations':clips,'max_weight_sum_error':max(abs(sum(w)-1) for w in weights),
+            'all_weights_finite':all(map(math.isfinite,chain.from_iterable(weights)))}
+    return {**report,'passed':report['skins']==1 and report['meshes']==18 and len(clips)==2
+            and report['max_weight_sum_error']<1e-6 and report['all_weights_finite']}
+
+
+def contact_sheet(root: Path) -> None:
+    rows=tuple(tuple(Image.open(root/name/f'frame-{i:04d}.png').convert('RGB').resize((260,312)) for i in indices)
+               for name,indices in (('idle',(1,19,37,55)),('walk',(1,9,17,25))))
+    canvas=Image.new('RGB',(1040,624),'#060e19')
+    tuple(map(lambda row:tuple(map(lambda cell:canvas.paste(cell[1],(cell[0]*260,row[0]*312)),enumerate(row[1]))),enumerate(rows)))
+    canvas.save(root/'motion-contact-sheet.jpg',quality=92)
+
+
+def main(root: Path) -> None:
+    report=glb_report(root/'knight-rig.glb')
+    (root/'export-checks.json').write_text(json.dumps(report,indent=2)+'\n')
+    if not report['passed']:
+        raise SystemExit('Exported skeleton/clip/weight checks failed')
+    previews=tuple(animation(root/clip,root/f'knight-{clip}.webp') for clip in ('idle','walk'))
+    contact_sheet(root)
+    (root/'previews.json').write_text(json.dumps(previews,indent=2)+'\n')
+    print(json.dumps({'export':report,'previews':previews},indent=2))
+
+
+if __name__=='__main__':
+    main(Path(sys.argv[1]))
diff --git a/docs/knight-rig/rig.py b/docs/knight-rig/rig.py
new file mode 100644
--- /dev/null
+++ b/docs/knight-rig/rig.py
@@ -0,0 +1,528 @@
+"""Native Planar layers -> editable 2.5D Knight fan-art rig.
+
+The closed skeleton, binding field and sampled motion are pure descriptions.
+Blender is only the mesh/constraint/keyframe/render/export interpreter.
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+from itertools import chain
+from math import cos, sin, pi, isfinite
+from pathlib import Path
+import json
+import sys
+
+import bmesh
+import bpy
+from mathutils import Vector
+
+Vec3 = tuple[float, float, float]
+
+
+class Bone(Enum):
+    ROOT = "ROOT"
+    HIPS = "Hips"
+    CHEST = "Chest"
+    HEAD = "Head"
+    THIGH_L = "Thigh.L"
+    SHIN_L = "Shin.L"
+    FOOT_L = "Foot.L"
+    THIGH_R = "Thigh.R"
+    SHIN_R = "Shin.R"
+    FOOT_R = "Foot.R"
+    ARM_L = "UpperArm.L"
+    FOREARM_L = "Forearm.L"
+    ARM_R = "UpperArm.R"
+    FOREARM_R = "Forearm.R"
+    IK_L = "CTRL_Foot.L"
+    IK_R = "CTRL_Foot.R"
+    POLE_L = "CTRL_Knee.L"
+    POLE_R = "CTRL_Knee.R"
+    CAPE_0 = "Cloak.Outer.L"
+    HEM_0 = "Hem.Outer.L"
+    CAPE_1 = "Cloak.Inner.L"
+    HEM_1 = "Hem.Inner.L"
+    CAPE_2 = "Cloak.Center"
+    HEM_2 = "Hem.Center"
+    CAPE_3 = "Cloak.Inner.R"
+    HEM_3 = "Hem.Inner.R"
+    CAPE_4 = "Cloak.Outer.R"
+    HEM_4 = "Hem.Outer.R"
+
+
+class Finish(Enum):
+    SHELL = "Porcelain | warm ivory"
+    VOID = "Void | velvet black"
+    CLOTH = "Cloak | blue graphite"
+    FOLD = "Cloak | silver fold"
+    EYE = "Eyes | absolute dark"
+
+
+class Clip(Enum):
+    IDLE = "Idle"
+    WALK = "Walk"
+
+    @property
+    def frames(self) -> int:
+        return 72 if self is Clip.IDLE else 32
+
+
+@dataclass(frozen=True)
+class BoneSpec:
+    bone: Bone
+    head: Vec3
+    tail: Vec3
+    parent: Bone | None
+    deform: bool = True
+
+
+@dataclass(frozen=True)
+class Rigid:
+    bone: Bone
+
+
+@dataclass(frozen=True)
+class Joint:
+    upper: Bone
+    lower: Bone
+    z: float
+    radius: float
+
+
+Skin = Rigid | Joint
+
+
+@dataclass(frozen=True)
+class Layer:
+    name: str
+    depth: float
+    finish: Finish
+    skin: Skin
+    tint: float = 1.0
+
+
+@dataclass(frozen=True)
+class Transform:
+    bone: Bone
+    location: Vec3 = (0.0, 0.0, 0.0)
+    roll: float = 0.0
+
+
+CAPE_PAIRS = ((Bone.CAPE_0, Bone.HEM_0), (Bone.CAPE_1, Bone.HEM_1),
+              (Bone.CAPE_2, Bone.HEM_2), (Bone.CAPE_3, Bone.HEM_3), (Bone.CAPE_4, Bone.HEM_4))
+LEG_SIDES = ((-0.115, 0.015, Bone.THIGH_L, Bone.SHIN_L, Bone.FOOT_L, Bone.IK_L, Bone.POLE_L),
+             (0.115, -0.025, Bone.THIGH_R, Bone.SHIN_R, Bone.FOOT_R, Bone.IK_R, Bone.POLE_R))
+
+
+def leg_bones(side: tuple[float, float, Bone, Bone, Bone, Bone, Bone]) -> tuple[BoneSpec, ...]:
+    x, y, thigh, shin, foot, ik, pole = side
+    hip, knee, ankle = (x,y,0.57), (x+0.022,y,0.305), (x,y,0.075)
+    return (BoneSpec(thigh,hip,knee,Bone.HIPS), BoneSpec(shin,knee,ankle,thigh),
+            BoneSpec(foot,ankle,(x+0.11,y,0.075),shin),
+            BoneSpec(ik,ankle,(x+0.11,y,0.075),Bone.ROOT,False),
+            BoneSpec(pole,(x+0.6,y,0.3),(x+0.6,y,0.4),Bone.ROOT,False))
+
+
+def cloak_bones(index: int, bones: tuple[Bone, Bone]) -> tuple[BoneSpec, ...]:
+    axis = index-2
+    root, hem = bones
+    return (BoneSpec(root,(axis*0.064,-0.11,1.025),(axis*0.12,-0.11,0.74),Bone.CHEST),
+            BoneSpec(hem,(axis*0.12,-0.11,0.74),(axis*0.166,-0.11,0.46),root))
+
+
+SKELETON = (
+    BoneSpec(Bone.ROOT,(0,0,0),(0,0,0.18),None,False),
+    BoneSpec(Bone.HIPS,(0,0,0.57),(0,0,0.74),Bone.ROOT),
+    BoneSpec(Bone.CHEST,(0,0,0.74),(0,0,1.025),Bone.HIPS),
+    BoneSpec(Bone.HEAD,(0,0,1.025),(0,0,1.50),Bone.CHEST),
+    BoneSpec(Bone.ARM_L,(-0.23,0.005,0.935),(-0.208,0.005,0.745),Bone.CHEST),
+    BoneSpec(Bone.FOREARM_L,(-0.208,0.005,0.745),(-0.23,0.005,0.57),Bone.ARM_L),
+    BoneSpec(Bone.ARM_R,(0.23,-0.04,0.935),(0.252,-0.04,0.745),Bone.CHEST),
+    BoneSpec(Bone.FOREARM_R,(0.252,-0.04,0.745),(0.23,-0.04,0.57),Bone.ARM_R),
+) + tuple(chain.from_iterable(map(leg_bones,LEG_SIDES))) + tuple(chain.from_iterable(
+    map(lambda pair: cloak_bones(*pair),enumerate(CAPE_PAIRS))))
+
+LAYERS = (
+    Layer("Mask",-0.15,Finish.SHELL,Rigid(Bone.HEAD)),
+    Layer("Eye_L",-0.207,Finish.EYE,Rigid(Bone.HEAD)),
+    Layer("Eye_R",-0.207,Finish.EYE,Rigid(Bone.HEAD)),
+    Layer("Body",0.045,Finish.VOID,Joint(Bone.CHEST,Bone.HIPS,0.73,0.10)),
+    Layer("Collar",-0.15,Finish.FOLD,Rigid(Bone.CHEST)),
+    Layer("Leg_L",0.015,Finish.VOID,Joint(Bone.THIGH_L,Bone.SHIN_L,0.305,0.07)),
+    Layer("Leg_R",-0.025,Finish.VOID,Joint(Bone.THIGH_R,Bone.SHIN_R,0.305,0.07)),
+    Layer("Foot_L",0.015,Finish.VOID,Rigid(Bone.FOOT_L)),
+    Layer("Foot_R",-0.025,Finish.VOID,Rigid(Bone.FOOT_R)),
+    Layer("Arm_L",0.005,Finish.VOID,Joint(Bone.ARM_L,Bone.FOREARM_L,0.745,0.065)),
+    Layer("Arm_R",-0.04,Finish.VOID,Joint(Bone.ARM_R,Bone.FOREARM_R,0.745,0.065)),
+    Layer("Hand_L",0.005,Finish.VOID,Rigid(Bone.FOREARM_L)),
+    Layer("Hand_R",-0.04,Finish.VOID,Rigid(Bone.FOREARM_R)),
+) + tuple(map(lambda pair: Layer(f"Cloak_{pair[0]}",-0.10-0.006*(2-abs(pair[0]-2)),
+    Finish.CLOTH,Joint(pair[1][0],pair[1][1],0.74,0.15), (0.78,1.10,0.95,1.18,0.82)[pair[0]]),enumerate(CAPE_PAIRS)))
+
+
+def smooth(t: float) -> float:
+    return t*t*(3-2*t)
+
+
+def weights(skin: Skin,z: float) -> tuple[tuple[Bone,float], ...]:
+    match skin:
+        case Rigid(bone): return ((bone,1.0),)
+        case Joint(upper,lower,center,radius):
+            high = smooth(max(0.0,min(1.0,(z-center+radius)/(2*radius))))
+            return tuple(filter(lambda pair: pair[1]>0,((upper,high),(lower,1-high))))
+
+
+DUTY = 0.62
+STRIDE = 0.29
+TRAVEL = STRIDE / DUTY
+
+
+def foot_path(phase: float) -> tuple[float,float]:
+    p = phase % 1.0
+    if p < DUTY:
+        return (STRIDE*(0.5-p/DUTY),0.0)
+    u = (p-DUTY)/(1-DUTY)
+    return (STRIDE*(-0.5+smooth(u)),0.092*sin(pi*u)**2)
+
+
+def motion(clip: Clip,t: float) -> tuple[Transform,...]:
+    phase = 2*pi*t
+    idle = clip is Clip.IDLE
+    root_x = 0 if idle else TRAVEL*(t-0.5)
+    bob = -0.022+0.004*sin(phase) if idle else -0.044+0.007*cos(2*phase)
+    sway = 0.018*sin(phase) if idle else 0.025*sin(phase)
+    feet = ((0.0,0.0),(0.0,0.0)) if idle else (foot_path(t),foot_path(t+0.5))
+    # Hips local Y points upward. Foot-control local Y points forward, Z upward.
+    return (
+        Transform(Bone.ROOT,(root_x,0,0)),
+        Transform(Bone.HIPS,(0,bob,0),sway),
+        Transform(Bone.CHEST,roll=-0.65*sway),
+        Transform(Bone.HEAD,roll=(-0.018*sin(phase-0.5) if idle else -0.028*sin(phase-0.45))),
+        Transform(Bone.IK_L,(0,feet[0][0],feet[0][1])),
+        Transform(Bone.IK_R,(0,feet[1][0],feet[1][1])),
+        Transform(Bone.ARM_L,roll=0.07+(0.02 if idle else 0.20)*sin(phase+pi)),
+        Transform(Bone.ARM_R,roll=-0.07+(0.02 if idle else 0.20)*sin(phase)),
+        Transform(Bone.FOREARM_L,roll=-0.12-0.025*sin(phase)),
+        Transform(Bone.FOREARM_R,roll=0.10+0.025*sin(phase)),
+    ) + tuple(chain.from_iterable(map(lambda pair: (
+        Transform(pair[1][0],roll=(0.010 if idle else 0.033)*sin(phase-0.35+pair[0]*0.25)),
+        Transform(pair[1][1],roll=(0.025 if idle else 0.080)*sin(phase-1.0+pair[0]*0.30)),
+    ),enumerate(CAPE_PAIRS))))
+
+
+# Blender interpreter: geometry is imported from the one native mesher.
+def clean_mesh(mesh: bpy.types.Mesh) -> None:
+    bm = bmesh.new()
+    bm.from_mesh(mesh)
+    bmesh.ops.remove_doubles(bm,verts=tuple(bm.verts),dist=1e-7)
+    bmesh.ops.recalc_face_normals(bm,faces=tuple(bm.faces))
+    bm.to_mesh(mesh)
+    bm.free()
+    mesh.update()
+
+
+def material(finish: Finish,tint: float=1.0) -> bpy.types.Material:
+    colors = {Finish.SHELL:(0.77,0.80,0.76,1), Finish.VOID:(0.006,0.009,0.017,1),
+              Finish.CLOTH:(0.07,0.095,0.14,1), Finish.FOLD:(0.13,0.17,0.23,1), Finish.EYE:(0.001,0.002,0.003,1)}
+    mat = bpy.data.materials.new(f"{finish.value} | {tint:.2f}")
+    mat.diffuse_color = tuple(map(lambda c:c*tint,colors[finish][:3]))+(1.0,)
+    mat.use_nodes=True
+    bsdf=mat.node_tree.nodes.get("Principled BSDF")
+    bsdf.inputs['Base Color'].default_value=mat.diffuse_color
+    bsdf.inputs['Roughness'].default_value=0.8 if finish is not Finish.SHELL else 0.56
+    bsdf.inputs['Specular IOR Level'].default_value=0.18 if finish is Finish.EYE else 0.28
+    return mat
+
+
+def prepare_layer(layer: Layer) -> bpy.types.Object:
+    obj=bpy.data.objects[layer.name]
+    clean_mesh(obj.data)
+    # Bake layer depth into the mesh so all bind transforms share one coordinate system.
+    obj.data.vertices.foreach_set('co',tuple(chain.from_iterable(map(
+        lambda v:(v.co.x,v.co.y+layer.depth,v.co.z),obj.data.vertices))))
+    obj.data.materials.append(material(layer.finish,layer.tint))
+    bevel=obj.modifiers.new("Soft cut edge","BEVEL")
+    bevel.width=0.005 if layer.finish is Finish.SHELL else 0.0015
+    bevel.segments=3
+    # Bevel before skinning: never smooth or move a rigged boundary after binding.
+    bpy.context.view_layer.objects.active=obj
+    bpy.ops.object.modifier_apply(modifier=bevel.name)
+    return obj
+
+
+def edit_bone(spec: BoneSpec,armature: bpy.types.Armature) -> None:
+    bone=armature.edit_bones.new(spec.bone.value)
+    bone.head,bone.tail=spec.head,spec.tail
+    bone.parent=armature.edit_bones[spec.parent.value] if spec.parent else None
+    bone.use_deform=spec.deform
+
+
+def constrain_leg(rig: bpy.types.Object,side: tuple[float,float,Bone,Bone,Bone,Bone,Bone]) -> None:
+    _,_,_,shin,foot,target,pole=side
+    ik=rig.pose.bones[shin.value].constraints.new('IK')
+    ik.name="Planted foot | two fixed-length links"
+    ik.target,ik.subtarget=rig,target.value
+    ik.pole_target,ik.pole_subtarget=rig,pole.value
+    ik.chain_count,ik.use_stretch=2,False
+    ik.pole_angle=0
+    copy=rig.pose.bones[foot.value].constraints.new('COPY_ROTATION')
+    copy.name="Sole orientation"
+    copy.target,copy.subtarget=rig,target.value
+    copy.target_space,copy.owner_space='WORLD','WORLD'
+
+
+def create_rig() -> bpy.types.Object:
+    data=bpy.data.armatures.new("Knight | articulated skeleton")
+    rig=bpy.data.objects.new("KNIGHT | 2.5D rig",data)
+    bpy.context.scene.collection.objects.link(rig)
+    bpy.context.view_layer.objects.active=rig
+    rig.select_set(True)
+    bpy.ops.object.mode_set(mode='EDIT')
+    tuple(map(lambda spec:edit_bone(spec,data),SKELETON))
+    bpy.ops.object.mode_set(mode='OBJECT')
+    rig.show_in_front=True
+    data.display_type='OCTAHEDRAL'
+    tuple(map(lambda bone:setattr(bone,'rotation_mode','XYZ'),rig.pose.bones))
+    tuple(map(lambda bone:setattr(bone,'lock_scale',(True,True,True)),rig.pose.bones))
+    tuple(map(lambda side:constrain_leg(rig,side),LEG_SIDES))
+    controls=data.collections.new('Controls | root, feet, knees')
+    deform=data.collections.new('Deform | head, limbs, cloak')
+    tuple(map(lambda spec:(controls if not spec.deform else deform).assign(data.bones[spec.bone.value]),SKELETON))
+    tuple(map(lambda spec:setattr(data.bones[spec.bone.value].color,'palette','THEME04' if not spec.deform else 'THEME03'),SKELETON))
+    return rig
+
+
+def bind(obj: bpy.types.Object,layer: Layer,rig: bpy.types.Object) -> None:
+    influences=tuple(map(lambda v:weights(layer.skin,v.co.z),obj.data.vertices))
+    bones=tuple(dict.fromkeys(map(lambda pair:pair[0],chain.from_iterable(influences))))
+    groups={bone:obj.vertex_groups.new(name=bone.value) for bone in bones}
+    tuple(map(lambda pair:tuple(map(lambda weight:groups[weight[0]].add((pair[0],),weight[1],'REPLACE'),pair[1])),enumerate(influences)))
+    mod=obj.modifiers.new("Skeleton | normalized analytic weights",'ARMATURE')
+    mod.object=rig
+    mod.use_deform_preserve_volume=False
+    obj.parent=rig
+
+
+def apply_pose(rig: bpy.types.Object,clip: Clip,t: float,key: int|None=None) -> None:
+    def apply(transform: Transform) -> None:
+        bone=rig.pose.bones[transform.bone.value]
+        bone.location=transform.location
+        bone.rotation_euler=(0,0,transform.roll)
+        if key is not None:
+            bone.keyframe_insert(data_path='location',frame=key,group=bone.name)
+            bone.keyframe_insert(data_path='rotation_euler',frame=key,group=bone.name)
+    tuple(map(apply,motion(clip,t)))
+    rig.update_tag()
+    bpy.context.view_layer.update()
+
+
+def make_action(rig: bpy.types.Object,clip: Clip) -> bpy.types.Action:
+    rig.animation_data_create()
+    rig.animation_data.action=None
+    tuple(map(lambda i:apply_pose(rig,clip,i/clip.frames,i+1),range(clip.frames+1)))
+    action=rig.animation_data.action
+    action.name=f"Knight | {clip.value}"
+    action.use_fake_user=True
+    return action
+
+
+def evaluated_points(obj: bpy.types.Object) -> tuple[Vec3,...]:
+    result=obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
+    return tuple(map(lambda v:tuple(v.co),result.data.vertices))
+
+
+def area(points: tuple[Vec3,...],indices: tuple[int,int,int]) -> float:
+    a,b,c=tuple(map(lambda i:Vector(points[i]),indices))
+    return (b-a).cross(c-a).length*0.5
+
+
+def validate(rig: bpy.types.Object,objects: tuple[bpy.types.Object,...]) -> dict:
+    def mesh_report(obj: bpy.types.Object) -> dict:
+        obj.data.calc_loop_triangles()
+        return {'name':obj.name,'vertices':len(obj.data.vertices),
+                'max_weight_error':max(map(lambda v:abs(sum(map(lambda w:w.weight,v.groups))-1),obj.data.vertices)),
+                'influences':max(map(lambda v:len(v.groups),obj.data.vertices))}
+    def pose_report(clip: Clip,t: float) -> dict:
+        apply_pose(rig,clip,t)
+        point_sets=tuple(map(evaluated_points,objects))
+        target_errors=tuple(map(lambda side:(rig.pose.bones[side[3].value].tail-rig.pose.bones[side[5].value].head).length,LEG_SIDES))
+        expected_feet=((0.0,0.0),(0.0,0.0)) if clip is Clip.IDLE else (foot_path(t),foot_path(t+0.5))
+        foot_path_errors=tuple(map(lambda pair:(rig.pose.bones[pair[0][5].value].head-Vector((
+            pair[0][0]+pair[1][0]+(0 if clip is Clip.IDLE else TRAVEL*(t-0.5)),pair[0][1],0.075+pair[1][1]))).length,
+            zip(LEG_SIDES,expected_feet)))
+        return {'clip':clip.value,'phase':t,'finite':all(map(isfinite,chain.from_iterable(chain.from_iterable(point_sets)))),
+                'minimum_area':min(chain.from_iterable(map(lambda pair:map(lambda tri:area(pair[1],tuple(tri.vertices)),pair[0].data.loop_triangles),zip(objects,point_sets)))),
+                'max_foot_target_error':max(target_errors),
+                'max_foot_path_error':max(foot_path_errors),
+                'knees':tuple(map(lambda side:tuple(rig.pose.bones[side[3].value].head),LEG_SIDES))}
+    meshes=tuple(map(mesh_report,objects))
+    poses=tuple(pose_report(clip,i/16) for clip in Clip for i in range(17))
+    return {'passed':all(map(lambda m:m['max_weight_error']<1e-6,meshes)) and all(map(
+        lambda p:p['finite'] and p['minimum_area']>1e-12 and p['max_foot_target_error']<0.003 and p['max_foot_path_error']<1e-5,poses)),
+        'meshes':meshes,'poses':poses,'scope':'34 sampled poses; foot IK reach and skin normalization, not all-pose collision certification.'}
+
+
+def aim(obj: bpy.types.Object,target: Vec3) -> None:
+    obj.rotation_euler=(Vector(target)-obj.location).to_track_quat('-Z','Y').to_euler()
+
+
+def light(name: str,position: Vec3,color: Vec3,power: float,size: float) -> None:
+    data=bpy.data.lights.new(name,'AREA')
+    data.energy,data.color,data.shape,data.size=power,color,'DISK',size
+    obj=bpy.data.objects.new(name,data)
+    bpy.context.scene.collection.objects.link(obj)
+    obj.location=position
+    aim(obj,(0,0,0.9))
+
+
+def stage(rig: bpy.types.Object) -> None:
+    scene=bpy.context.scene
+    scene.render.engine='CYCLES'
+    scene.cycles.samples=20
+    scene.cycles.use_denoising=True
+    scene.render.resolution_x,scene.render.resolution_y,scene.render.resolution_percentage=800,960,100
+    scene.render.image_settings.file_format='PNG'
+    scene.render.fps=24
+    scene.world=bpy.data.worlds.new('Deep blue | quiet cavern')
+    scene.world.use_nodes=True
+    scene.world.node_tree.nodes['Background'].inputs['Color'].default_value=(0.025,0.045,0.09,1)
+    scene.world.node_tree.nodes['Background'].inputs['Strength'].default_value=0.25
+    scene.view_settings.view_transform='AgX'
+    bpy.ops.mesh.primitive_plane_add(size=200,location=(0,0,0.017))
+    floor=bpy.context.object
+    floor.name='Stage | ground contact'
+    ground=material(Finish.CLOTH,0.3)
+    floor.data.materials.append(ground)
+    camera_data=bpy.data.cameras.new('Portrait | front three-quarter')
+    camera=bpy.data.objects.new('Portrait | front three-quarter',camera_data)
+    scene.collection.objects.link(camera)
+    camera.location=(0.45,-8,2.25)
+    camera_data.type,camera_data.ortho_scale='ORTHO',2.27
+    aim(camera,(0,0,0.93))
+    follow=camera.constraints.new('COPY_LOCATION')
+    follow.name='Preview follows accumulated root motion'
+    follow.target,follow.subtarget=rig,Bone.ROOT.value
+    follow.use_x,follow.use_y,follow.use_z,follow.use_offset=True,False,False,True
+    scene.camera=camera
+    tuple(map(lambda args:light(*args),(
+        ('Ivory softbox',(-2,-4,4),(0.78,0.87,1),240,3),
+        ('Moon rim',(1,1,3),(0.25,0.58,1),330,2),
+        ('Warm bounce',(3,-2,1),(1,0.77,0.56),70,2),
+    )))
+
+
+GUIDE = '''KNIGHT | 2.5D FAN-ART RIG
+
+Select KNIGHT | 2.5D rig and enter Pose Mode.
+ROOT places the character. Hips and Chest control weight/lean; Head remains rigid.
+CTRL_Foot.L / .R move the feet; CTRL_Knee.L / .R choose knee direction.
+UpperArm and Forearm are FK controls. Cloak and Hem bones shape each cloth panel.
+Clear the active action to pose manually; the feet remain IK-controlled.
+
+Action Editor: Knight | Idle (frames 1-73, 3 seconds), Knight | Walk (1-33, 1.333 seconds).
+The endpoint repeats the local pose; display frames 1-72 or 1-32 for looping playback.
+Walk carries +X root motion of 0.468 m per cycle. Accumulate that translation when
+repeating the action to retain planted world-space feet. Idle is stationary.
+The walk is authored, not motion capture; cloak motion is keyed, not cloth simulation.
+
+Native mesh layers were triangulated once by Moonlight Planar constrained Delaunay.
+These are shallow solid layers for front and modest three-quarter views, not a
+fully modeled all-angle character. The dark eyes are rigid mesh inlays.
+GLB contains skinning and sampled Idle/Walk; editable IK constraints stay in Blender.
+
+Fan art of the Knight from Hollow Knight. Original character: Team Cherry.
+https://www.hollowknight.com/ . No game asset or texture was extracted.
+'''
+
+
+def export(rig: bpy.types.Object,objects: tuple[bpy.types.Object,...],actions: tuple[bpy.types.Action,...],output: Path) -> None:
+    scene=bpy.context.scene
+    rig.animation_data.action=actions[0]
+    scene.frame_start,scene.frame_end=1,Clip.IDLE.frames
+    scene.frame_set(1)
+    bpy.ops.object.select_all(action='DESELECT')
+    tuple(map(lambda o:o.select_set(True),(rig,)+objects))
+    bpy.context.view_layer.objects.active=rig
+    bpy.ops.export_scene.gltf(filepath=str(output/'knight-rig.glb'),export_format='GLB',use_selection=True,
+        export_animations=True,export_animation_mode='ACTIONS',export_force_sampling=True,
+        export_bake_animation=True,export_anim_slide_to_zero=True,
+        export_skins=True,export_def_bones=True,export_extras=True)
+    bpy.data.texts.new('START HERE | Knight controls').write(GUIDE)
+    (output/'README.txt').write_text(GUIDE)
+    bpy.ops.object.select_all(action='DESELECT')
+    rig.select_set(True)
+    bpy.context.view_layer.objects.active=rig
+    bpy.ops.wm.save_as_mainfile(filepath=str(output/'knight-rig.blend'))
+    scene.render.filepath=str(output/'knight-rig.png')
+    bpy.ops.render.render(write_still=True)
+
+
+def render_clip(rig: bpy.types.Object,action: bpy.types.Action,clip: Clip,output: Path) -> None:
+    scene=bpy.context.scene
+    rig.animation_data.action=action
+    scene.frame_start,scene.frame_end,scene.frame_step=1,clip.frames,2
+    scene.render.resolution_percentage=65
+    scene.cycles.samples=12
+    folder=output/clip.value.lower()
+    folder.mkdir(exist_ok=True)
+    scene.render.filepath=str(folder/'frame-')
+    bpy.ops.render.render(animation=True)
+
+
+def render_skeleton(rig: bpy.types.Object,action: bpy.types.Action,output: Path) -> None:
+    """X-ray diagram derived from evaluated bones, not a separately authored rig."""
+    rig.animation_data.action=action
+    scene=bpy.context.scene
+    scene.frame_set(9)
+    scene.render.resolution_percentage=100
+    glow=bpy.data.materials.new('Diagram | joint cyan')
+    glow.diffuse_color=(0.10,0.85,0.80,1)
+    glow.use_nodes=True
+    node=glow.node_tree.nodes.get('Principled BSDF')
+    node.inputs['Base Color'].default_value=glow.diffuse_color
+    node.inputs['Emission Color'].default_value=glow.diffuse_color
+    node.inputs['Emission Strength'].default_value=0.5
+    def segment(spec: BoneSpec) -> bpy.types.Object:
+        bone=rig.pose.bones[spec.bone.value]
+        data=bpy.data.curves.new('Diagram | '+spec.bone.value,'CURVE')
+        data.dimensions,data.bevel_depth,data.bevel_resolution='3D',0.006,3
+        spline=data.splines.new('POLY')
+        spline.points.add(1)
+        spline.points.foreach_set('co',(bone.head.x,-0.42,bone.head.z,1,bone.tail.x,-0.42,bone.tail.z,1))
+        obj=bpy.data.objects.new(data.name,data)
+        scene.collection.objects.link(obj)
+        data.materials.append(glow)
+        return obj
+    segments=tuple(map(segment,filter(lambda spec:spec.deform,SKELETON)))
+    scene.render.filepath=str(output/'knight-skeleton.png')
+    bpy.ops.render.render(write_still=True)
+    tuple(map(lambda obj:bpy.data.objects.remove(obj,do_unlink=True),segments))
+
+
+def main() -> None:
+    args=tuple(sys.argv[sys.argv.index('--')+1:]) if '--' in sys.argv else ()
+    if len(args)!=2:
+        raise SystemExit('usage: blender -b --factory-startup --python rig.py -- INPUT.obj OUTPUT_DIRECTORY')
+    source,output=map(Path,args)
+    output.mkdir(parents=True,exist_ok=True)
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    bpy.ops.wm.obj_import(filepath=str(source),forward_axis='Y',up_axis='Z')
+    objects=tuple(map(prepare_layer,LAYERS))
+    bpy.ops.object.select_all(action='DESELECT')
+    rig=create_rig()
+    tuple(map(lambda pair:bind(pair[0],pair[1],rig),zip(objects,LAYERS)))
+    report=validate(rig,objects)
+    (output/'rig-checks.json').write_text(json.dumps(report,indent=2))
+    print(json.dumps({'passed':report['passed'],'poses':report['poses']}),flush=True)
+    if not report['passed']:
+        raise SystemExit('Rig admission failed; inspect rig-checks.json')
+    actions=tuple(map(lambda clip:make_action(rig,clip),Clip))
+    stage(rig)
+    export(rig,objects,actions,output)
+    tuple(map(lambda pair:render_clip(rig,pair[0],pair[1],output),zip(actions,Clip)))
+    render_skeleton(rig,actions[1],output)
+
+
+if __name__=='__main__':
+    main()
diff --git a/docs/mesh-publication.md b/docs/mesh-publication.md
--- a/docs/mesh-publication.md
+++ b/docs/mesh-publication.md
@@ -41,16 +41,16 @@
 readers continue observing the previous value while the next is built. A failed
 operation returns its obstruction with both operands unchanged.
 
-`Moonlight.Triangulation.Parallel.unionsConcurrently` interprets the same pure
+`Moonlight.Planar.Parallel.unionsConcurrently` interprets the same pure
 join tournament with bounded concurrency.
 
 ## Source authority
 
 The executable schedule decisions live in
-[`SetAlgebra`](../src-build/Moonlight/Triangulation/SetAlgebra.hs),
-[`BulkLoad`](../src-build/Moonlight/Triangulation/BulkLoad.hs),
-[`Join.Plan`](../src-build/Moonlight/Triangulation/Internal/Join/Plan.hs), and
-[`Cdt.Union`](../src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs). The
+[`SetAlgebra`](../src-build/Moonlight/Planar/SetAlgebra.hs),
+[`BulkLoad`](../src-build/Moonlight/Planar/BulkLoad.hs),
+[`Join.Plan`](../src-build/Moonlight/Planar/Internal/Join/Plan.hs), and
+[`Cdt.Union`](../src-build/Moonlight/Planar/Internal/Cdt/Union.hs). The
 focused measurements live in
-[`JoinBench`](../bench/join/Moonlight/Triangulation/JoinBench.hs); this page is
+[`JoinBench`](../bench/join/Moonlight/Planar/JoinBench.hs); this page is
 their consumer-facing projection.
diff --git a/docs/moonblade/Main.hs b/docs/moonblade/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/moonblade/Main.hs
@@ -0,0 +1,221 @@
+module Main (main) where
+
+import Data.Foldable (traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Moonlight.Planar.Exhibit.Moonblade.Crescent as Crescent
+import Moonlight.Planar.Exhibit.Geometry (ShapeError, Triangle (..), triangulateShape)
+import Moonlight.Planar.Exhibit.Svg (circle, polygon, pointText, scalar)
+import Moonlight.Planar.Point (Point (..))
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs)
+import System.Exit (die)
+import System.FilePath ((</>))
+import Text.Printf (printf)
+
+data Material = Moonsteel | PaleGold
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  case arguments of
+    [outputPath] -> either (die . show) (writeFile outputPath) moonblade
+    ["crescent-solid", outputPath] -> either (die . show) (writeFile outputPath) Crescent.solid
+    ["crescent-frames", outputDirectory] -> either (die . show)
+      (\frames -> createDirectoryIfMissing True outputDirectory
+        *> traverse_ (\(name, svg) -> writeFile (outputDirectory </> name) svg) frames)
+      Crescent.frames
+    _ -> die "usage: moonlight-planar-moonblade OUTPUT.svg | crescent-frames OUTPUT_DIRECTORY | crescent-solid OUTPUT.obj"
+
+moonblade :: Either ShapeError String
+moonblade = do
+  blade <- triangulateShape (bladeOutline :| []) bladeSeeds
+  guard <- triangulateShape (guardOutline :| []) guardSeeds
+  pure $
+    "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1400\" height=\"1600\" viewBox=\"0 0 1400 1600\">"
+      <> "<title>Vesper - the astral moonblade</title><desc>A straight broad moon-glass blade with a luminous turquoise edge, flowing constellations, a symmetric bronze star guard, and a leather-wrapped grip. "
+      <> show (length blade) <> " blade facets and " <> show (length guard)
+      <> " guard facets are native Moonlight Planar constrained Delaunay triangles. Subtle facets support the materials rather than dominating the design. Light, ornament and composition are authored SVG.</desc>"
+      <> definitions <> background
+      <> "<g transform=\"translate(700 1110)\">"
+      <> bladeAura
+      <> hilt
+      <> polygon "fill=\"url(#moonglass)\" stroke=\"#033c49\" stroke-width=\"11\" stroke-linejoin=\"round\"" (NonEmpty.toList bladeOutline)
+      <> foldMap (uncurry (renderFacet Moonsteel)) (zip [0 ..] blade)
+      <> bladeEdges
+      <> bladeEngraving
+      <> polygon "fill=\"url(#bronze)\" stroke=\"#241810\" stroke-width=\"9\" stroke-linejoin=\"round\"" (NonEmpty.toList guardOutline)
+      <> foldMap (uncurry (renderFacet PaleGold)) (zip [0 ..] guard)
+      <> polygon "fill=\"none\" stroke=\"#ebc493\" stroke-width=\"2.5\" stroke-linejoin=\"round\"" (NonEmpty.toList guardOutline)
+      <> guardFiligree
+      <> fittings
+      <> "</g>" <> typography <> "</svg>"
+
+bladePoint :: Double -> Double -> Point
+bladePoint side t = quantize $
+  Point (side * bladeWidth t) (-1000 * t)
+
+bladeWidth :: Double -> Double
+bladeWidth t = (100 - 6 * t) * if t <= 0.76 then 1 else cos (pi * (t - 0.76) / 0.48)
+
+bladeOutline :: NonEmpty Point
+bladeOutline = bladePoint (-1) 0 :|
+  (fmap (bladePoint (-1) . fraction) [1 .. 48] <> fmap (bladePoint 1 . fraction) [47, 46 .. 0])
+
+bladeSeeds :: [Point]
+bladeSeeds =
+  [ bladePoint (side * (0.2 + 0.5 * abs (sin (fromIntegral index * 1.71)))) ((fromIntegral index + 0.5) / 64)
+  | index <- [0 .. 63 :: Int], side <- [-1, 1]
+  ]
+
+guardOutline :: NonEmpty Point
+guardOutline = Point 0 (-138) :|
+  (rightSide <> [Point 0 76] <> fmap mirror (reverse rightSide))
+ where
+  rightSide :: [Point]
+  rightSide =
+    [ Point 18 (-115), Point 27 (-39), Point 81 (-49)
+    , Point 156 (-105), Point 174 (-98), Point 187 (-80), Point 173 (-63)
+    , Point 111 (-13), Point 183 (-20), Point 196 (-7), Point 196 18
+    , Point 182 29, Point 110 24, Point 171 77, Point 170 91
+    , Point 152 108, Point 136 103, Point 64 53, Point 25 57, Point 17 76
+    ]
+  mirror :: Point -> Point
+  mirror (Point x y) = Point (-x) y
+
+guardSeeds :: [Point]
+guardSeeds = [Point 0 0, Point 0 (-75), Point (-65) 0, Point 65 0, Point (-123) (-60), Point 123 (-60), Point (-120) 68, Point 120 68]
+
+fraction :: Int -> Double
+fraction index = fromIntegral index / 48
+
+quantize :: Point -> Point
+quantize (Point x y) = Point (snap x) (snap y)
+ where
+  snap :: Double -> Double
+  snap value = fromIntegral (round (1024 * value) :: Integer) / 1024
+
+renderFacet :: Material -> Int -> Triangle -> String
+renderFacet material index (Triangle a@(Point ax _) b@(Point bx _) c@(Point cx _)) =
+  let across = abs (ax + bx + cx) / 300
+      shimmer = 0.5 + 0.5 * sin (fromIntegral index * 1.719)
+      color = case material of
+        Moonsteel -> rgb (16 + 12 * shimmer) (66 + 40 * across) (80 + 32 * across)
+        PaleGold -> rgb (140 + 85 * shimmer) (94 + 78 * shimmer) (57 + 60 * shimmer)
+      opacity = case material of Moonsteel -> "0.24"; PaleGold -> "0.17"
+   in polygon ("fill=\"" <> color <> "\" fill-opacity=\"" <> opacity <> "\"") [a, b, c]
+
+rgb :: Double -> Double -> Double -> String
+rgb red green blue = printf "#%02x%02x%02x" (round red :: Int) (round green :: Int) (round blue :: Int)
+
+bladeEdges :: String
+bladeEdges =
+  foldMap bevel [-1, 1]
+  <> polygon "fill=\"none\" stroke=\"#43eadf\" stroke-width=\"7\" stroke-opacity=\"0.35\" stroke-linejoin=\"round\"" (NonEmpty.toList bladeOutline)
+  <> polygon "fill=\"none\" stroke=\"#b4fff4\" stroke-width=\"2.5\" stroke-linejoin=\"round\"" (NonEmpty.toList bladeOutline)
+ where
+  bevel :: Double -> String
+  bevel side = polygon "fill=\"url(#edge)\" fill-opacity=\"0.75\""
+    (fmap (bladePoint side . fraction) [0 .. 48] <> fmap (bladePoint (side * 0.91) . fraction) [47, 46 .. 0])
+
+bladeAura :: String
+bladeAura = foldMap halo [0 .. 19 :: Int]
+ where
+  halo :: Int -> String
+  halo index = polygon
+    ("fill=\"none\" stroke=\"#30efd8\" stroke-width=\"" <> scalar (10 + 4 * fromIntegral index)
+      <> "\" stroke-opacity=\"" <> scalar (0.05 * exp (negate (fromIntegral index) / 7))
+      <> "\" stroke-linejoin=\"round\"")
+    (NonEmpty.toList bladeOutline)
+
+bladeEngraving :: String
+bladeEngraving =
+  "<g fill=\"none\" stroke=\"#49d8d2\" stroke-linecap=\"round\">"
+  <> foldMap filament [0, 1, 2 :: Int]
+  <> "<path d=\"M 22 -715 A 54 65 0 1 0 22 -596 A 43 63 0 0 1 22 -715 Z\" fill=\"#8defe0\" fill-opacity=\"0.07\" stroke-opacity=\"0.33\" stroke-width=\"1\"/>"
+  <> "<path d=\"M 0 -927 L 8 -906 L 0 -883 L -8 -906 Z M 0 -941 V -930 M 0 -880 V -864\" stroke-opacity=\"0.58\" stroke-width=\"1.2\"/></g>"
+  <> foldMap mote [0 .. 22 :: Int]
+  <> "<g fill=\"none\" stroke=\"#8af4e5\" stroke-width=\"0.9\" stroke-opacity=\"0.36\"><path d=\"M -54 -309 L -17 -244 L 47 -356 M -54 -309 L 13 -417 L 47 -356\"/></g>"
+ where
+  filament :: Int -> String
+  filament index =
+    let phase = fromIntegral index * 1.9
+        points = [Point (51 * sin (8.5 * t + phase) * sin (pi * t)) (-1000 * t) | sample <- [0 .. 96 :: Int], let t = 0.16 + 0.69 * fromIntegral sample / 96]
+     in "<polyline points=\"" <> unwords (pointText <$> points) <> "\" stroke-width=\""
+        <> scalar (if index == 0 then 2 else 0.8) <> "\" stroke-opacity=\"0.3\"/>"
+  mote :: Int -> String
+  mote index =
+    let t = 0.16 + 0.68 * fromIntegral index / 23
+        point = Point (60 * sin (fromIntegral index * 2.399)) (-1000 * t)
+        radius = if index `mod` 4 == 0 then 3 else 1.5
+     in circle point (radius * 4) "fill=\"url(#mote)\""
+        <> circle point radius "fill=\"#b0fff0\""
+        <> circle point (radius * 0.4) "fill=\"#f0fff8\""
+
+hilt :: String
+hilt =
+  "<path d=\"M -31 49 H 31 L 27 248 L 34 263 H -34 L -27 248 Z\" fill=\"url(#grip)\" stroke=\"#3a261b\" stroke-width=\"4\"/>"
+  <> foldMap binding [0 .. 8 :: Int]
+  <> "<g fill=\"url(#bronze)\" stroke=\"#42291c\" stroke-width=\"2\"><rect x=\"-35\" y=\"72\" width=\"70\" height=\"13\" rx=\"3\"/><rect x=\"-32\" y=\"242\" width=\"64\" height=\"10\" rx=\"2\"/><rect x=\"-36\" y=\"258\" width=\"72\" height=\"11\" rx=\"3\"/></g>"
+  <> "<path d=\"M -31 75 H 31 M -29 245 H 29 M -31 261 H 31\" stroke=\"#ecc391\" stroke-width=\"1.4\"/>"
+ where
+  binding :: Int -> String
+  binding index =
+    let y = 89 + fromIntegral index * 17
+     in "<g transform=\"translate(0 " <> scalar y <> ")\"><path d=\"M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17\" stroke=\"#31221b\" stroke-width=\"4\" fill=\"none\"/>"
+        <> "<path d=\"M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16\" stroke=\"#be8960\" stroke-width=\"1.6\" fill=\"none\"/>"
+        <> "<path d=\"M 0 6 L 3 9 L 0 12 L -3 9 Z\" fill=\"#d3a268\"/></g>"
+
+guardFiligree :: String
+guardFiligree =
+  polygon "fill=\"none\" stroke=\"#784a30\" stroke-width=\"3\" stroke-linejoin=\"round\""
+    (fmap (\(Point x y) -> Point (0.94 * x) (0.88 * y)) (NonEmpty.toList guardOutline))
+  <> "<path d=\"M -157 -83 L -62 -6 L 0 10 L 62 -6 L 157 -83 M -149 86 L -50 25 L 0 10 L 50 25 L 149 86 M 0 -117 V 53 M -178 6 H -78 M 78 6 H 178\" fill=\"none\" stroke=\"#75432a\" stroke-width=\"11\" stroke-linejoin=\"round\"/>"
+  <> "<path d=\"M -157 -86 L -62 -9 L 0 7 L 62 -9 L 157 -86 M -149 83 L -50 22 L 0 7 L 50 22 L 149 83 M 0 -117 V 53 M -178 3 H -78 M 78 3 H 178\" fill=\"none\" stroke=\"#f3ce95\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/>"
+  <> "<path d=\"M -22 -33 L 0 -103 L 22 -33 L 0 7 Z\" fill=\"url(#bronze)\" stroke=\"#e5b47c\" stroke-width=\"1.2\"/>"
+
+fittings :: String
+fittings =
+  "<path d=\"M 0 -17 L 8 0 L 27 8 L 8 16 L 0 36 L -8 16 L -27 8 L -8 0 Z\" fill=\"#8b5632\" stroke=\"#f0c890\" stroke-width=\"1.5\"/>"
+  <> circle (Point 0 8) 17 "fill=\"url(#amber)\""
+  <> "<path d=\"M 0 -7 L 3 5 L 14 8 L 3 11 L 0 25 L -3 11 L -14 8 L -3 5 Z\" fill=\"#ffe4a4\"/>"
+  <> "<path d=\"M -32 270 H 32 L 42 284 V 324 L 29 340 H -29 L -42 324 V 284 Z\" fill=\"url(#bronze)\" stroke=\"#40291e\" stroke-width=\"4\"/>"
+  <> "<path d=\"M -27 279 H 27 L 32 288 V 319 L 24 330 H -24 L -32 319 V 288 Z\" fill=\"#6c4938\" stroke=\"#e5b17c\" stroke-width=\"1.2\"/>"
+  <> circle (Point 0 304) 25 "fill=\"url(#amber)\""
+  <> "<path d=\"M 0 289 L 5 301 L 0 316 L -5 301 Z\" fill=\"#eec67e\"/>"
+  <> "<ellipse cx=\"0\" cy=\"345\" rx=\"29\" ry=\"14\" fill=\"url(#gem)\" stroke=\"#4a3426\" stroke-width=\"3\"/><path d=\"M -43 334 H 43 L 38 344 H -38 Z\" fill=\"url(#bronze)\" stroke=\"#593925\" stroke-width=\"2\"/><path d=\"M -38 337 H 38\" stroke=\"#e7b982\" stroke-width=\"1.2\"/>"
+
+background :: String
+background =
+  "<rect width=\"1400\" height=\"1600\" fill=\"url(#night)\"/>"
+  <> "<ellipse cx=\"700\" cy=\"665\" rx=\"360\" ry=\"740\" fill=\"url(#atmosphere)\"/>"
+  <> "<g fill=\"none\" stroke=\"#597275\" stroke-width=\"0.7\" opacity=\"0.23\"><circle cx=\"700\" cy=\"655\" r=\"360\"/><circle cx=\"700\" cy=\"655\" r=\"368\" stroke-dasharray=\"1 12\"/><path d=\"M 302 655 H 345 M 1055 655 H 1098 M 700 256 V 300 M 700 1010 V 1055\"/></g>"
+  <> foldMap star [0 .. 139 :: Int]
+  <> "<ellipse cx=\"700\" cy=\"1490\" rx=\"109\" ry=\"11\" fill=\"#000706\" opacity=\"0.4\"/>"
+ where
+  star :: Int -> String
+  star index = circle
+    (Point (55 + fromIntegral ((index * 947 + 71) `mod` 1290)) (55 + fromIntegral ((index * 389 + 173) `mod` 1490)))
+    (if index `mod` 17 == 0 then 1 else 0.5) "fill=\"#94b9b0\" opacity=\"0.2\""
+
+definitions :: String
+definitions = "<defs>"
+  <> "<radialGradient id=\"night\" cx=\"50%\" cy=\"43%\" r=\"75%\"><stop stop-color=\"#163035\"/><stop offset=\"0.5\" stop-color=\"#101f25\"/><stop offset=\"1\" stop-color=\"#091218\"/></radialGradient>"
+  <> "<radialGradient id=\"atmosphere\"><stop stop-color=\"#126b66\" stop-opacity=\"0.14\"/><stop offset=\"1\" stop-color=\"#126b66\" stop-opacity=\"0\"/></radialGradient>"
+  <> "<linearGradient id=\"moonglass\"><stop stop-color=\"#117b80\"/><stop offset=\"0.10\" stop-color=\"#14535e\"/><stop offset=\"0.28\" stop-color=\"#183a49\"/><stop offset=\"0.55\" stop-color=\"#142e3e\"/><stop offset=\"0.85\" stop-color=\"#155563\"/><stop offset=\"1\" stop-color=\"#1ca1a1\"/></linearGradient>"
+  <> "<linearGradient id=\"edge\"><stop stop-color=\"#e0fff2\"/><stop offset=\"0.4\" stop-color=\"#4adccf\"/><stop offset=\"1\" stop-color=\"#a9fff2\"/></linearGradient>"
+  <> "<linearGradient id=\"bronze\" x1=\"0\" y1=\"0\" x2=\"0.7\" y2=\"1\"><stop stop-color=\"#775038\"/><stop offset=\"0.23\" stop-color=\"#c88f5e\"/><stop offset=\"0.46\" stop-color=\"#efc28b\"/><stop offset=\"0.53\" stop-color=\"#a46c46\"/><stop offset=\"0.80\" stop-color=\"#be875a\"/><stop offset=\"1\" stop-color=\"#6d4833\"/></linearGradient>"
+  <> "<linearGradient id=\"grip\"><stop stop-color=\"#37271e\"/><stop offset=\"0.45\" stop-color=\"#805637\"/><stop offset=\"0.68\" stop-color=\"#68482f\"/><stop offset=\"1\" stop-color=\"#2d211b\"/></linearGradient>"
+  <> "<radialGradient id=\"gem\" cx=\"35%\" cy=\"22%\"><stop stop-color=\"#81e4d1\"/><stop offset=\"0.25\" stop-color=\"#188e98\"/><stop offset=\"1\" stop-color=\"#122e3e\"/></radialGradient>"
+  <> "<radialGradient id=\"mote\"><stop stop-color=\"#69fce5\" stop-opacity=\"0.75\"/><stop offset=\"0.3\" stop-color=\"#39d5d0\" stop-opacity=\"0.3\"/><stop offset=\"1\" stop-color=\"#39d5d0\" stop-opacity=\"0\"/></radialGradient>"
+  <> "<radialGradient id=\"amber\"><stop stop-color=\"#ffd889\" stop-opacity=\"0.8\"/><stop offset=\"1\" stop-color=\"#f9b44d\" stop-opacity=\"0\"/></radialGradient>"
+  <> "</defs>"
+
+typography :: String
+typography =
+  "<g fill=\"#bfbaa8\"><text x=\"90\" y=\"108\" font-family=\"Helvetica, sans-serif\" font-size=\"11\" letter-spacing=\"4\">MOONLIGHT / RELIQUARY</text>"
+  <> "<text x=\"85\" y=\"249\" font-family=\"Georgia, serif\" font-size=\"73\" letter-spacing=\"-2\">Vesper</text>"
+  <> "<text x=\"91\" y=\"283\" font-family=\"Helvetica, sans-serif\" font-size=\"11\" letter-spacing=\"4\">THE ASTRAL MOONBLADE</text>"
+  <> "<path d=\"M 91 308 H 280\" stroke=\"#667d79\" stroke-width=\"0.7\"/></g>"
+  <> "<g fill=\"#839f9d\" font-family=\"Georgia, serif\" font-size=\"17\" font-style=\"italic\"><text x=\"91\" y=\"346\">A small sky, held in bronze.</text></g>"
+  <> "<g fill=\"#77918e\" font-family=\"Helvetica, sans-serif\" font-size=\"10\" letter-spacing=\"2\"><text x=\"91\" y=\"1536\">BRONZE / MOONGLASS / STARLIGHT</text><text x=\"1309\" y=\"1536\" text-anchor=\"end\">PLANAR STUDY 02</text></g>"
diff --git a/docs/moonblade/Moonlight/Planar/Exhibit/Moonblade/Crescent.hs b/docs/moonblade/Moonlight/Planar/Exhibit/Moonblade/Crescent.hs
new file mode 100644
--- /dev/null
+++ b/docs/moonblade/Moonlight/Planar/Exhibit/Moonblade/Crescent.hs
@@ -0,0 +1,219 @@
+module Moonlight.Planar.Exhibit.Moonblade.Crescent (frames, solid) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Exhibit.Geometry (ShapeError, Triangle (..), solidObj, triangulateShape)
+import Moonlight.Planar.Exhibit.Svg (circle, polygon, scalar)
+import Moonlight.Planar.Point (Point (..))
+import Text.Printf (printf)
+
+data Material = Moonsteel | PaleGold
+
+solid :: Either ShapeError String
+solid = do
+  blade <- triangulateShape (bladeOutline :| [fullerOutline]) bladeSeeds
+  guard <- triangulateShape (guardOutline :| []) guardSeeds
+  pure $ "# Vesper: native crescent blade and guard, metres, Z up.\n"
+    <> solidObj "Blade" bladeThickness blade
+    <> solidObj "Guard" (const 0.012) guard
+ where
+  bladeThickness :: Point -> Double
+  bladeThickness (Point x y) =
+    let t = max 0 (min 1 (negate y / 900))
+        across = abs ((x - bladeCenter t) / max 1 (bladeWidth t))
+        edge = 0.00035 + 0.00085 * min 1 ((1 - t) / 0.05)
+        tipTaper = min 1 ((1 - t) / 0.12)
+     in edge + 0.009 * (1 - min 1 across) * (1 - 0.4 * t) * tipTaper
+
+-- One immutable native mesh, sampled at 80 phases of a 6.4-second loop.
+-- Only the rigid pose and illumination vary; the silhouette never deforms.
+frames :: Either ShapeError [(FilePath, String)]
+frames = do
+  blade <- triangulateShape (bladeOutline :| [fullerOutline]) bladeSeeds
+  guard <- triangulateShape (guardOutline :| []) guardSeeds
+  let bladeFacets = foldMap (uncurry (renderFacet Moonsteel)) (zip [0 ..] blade)
+      guardFacets = foldMap (uncurry (renderFacet PaleGold)) (zip [0 ..] guard)
+      clip = "<defs><clipPath id=\"blade-clip\">"
+        <> foldMap (\(Triangle a b c) -> polygon "" [a, b, c]) blade
+        <> "</clipPath></defs>"
+      render phase =
+        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"900\" viewBox=\"0 0 1600 1200\">"
+        <> "<title>Vesper - the crescent moonblade</title><desc>A rigid hovering moonblade with pulsing runes and a traveling reflection. Native constrained Delaunay facets preserve the pierced fuller.</desc>"
+        <> definitions <> clip <> background phase
+        <> "<g transform=\"translate(" <> scalar (525 + 5 * sin (2 * pi * phase))
+        <> " " <> scalar (870 - 7 * (1 - cos (2 * pi * phase)))
+        <> ") rotate(" <> scalar (40 + 0.6 * sin (2 * pi * phase)) <> ")\">"
+        <> bladeAura phase <> hilt <> bladeFacets <> bladeEdges <> lightSweep phase
+        <> polygon "fill=\"none\" stroke=\"#a6dbe9\" stroke-width=\"1.1\"" (NonEmpty.toList fullerOutline)
+        <> bladeEngraving phase <> guardFacets
+        <> polygon "fill=\"none\" stroke=\"#f5e4b7\" stroke-width=\"1.6\"" (NonEmpty.toList guardOutline)
+        <> guardFiligree <> fittings
+        <> "</g>" <> typography <> "</svg>"
+  pure $ fmap (\index -> (printf "frame-%03d.svg" index, render (fromIntegral index / 80))) [0 .. 79 :: Int]
+
+bladePoint :: Double -> Double -> Point
+bladePoint side t = quantize $
+  Point (bladeCenter t + side * bladeWidth t) (-900 * t)
+
+bladeCenter :: Double -> Double
+bladeCenter t = 12 * t + 40 * t * t
+
+bladeWidth :: Double -> Double
+bladeWidth t = (62 - 22 * t) * if t <= 0.76 then 1 else ((1 - t) / 0.24) ** 0.72
+
+bladeOutline :: NonEmpty Point
+bladeOutline = bladePoint (-1) 0 :|
+  (fmap (bladePoint (-1) . fraction) [1 .. 48] <> fmap (bladePoint 1 . fraction) [47, 46 .. 0])
+
+fullerPoint :: Double -> Double -> Point
+fullerPoint side u =
+  let t = 0.12 + 0.22 * u
+   in quantize (Point (bladeCenter t + side * 7 * sin (pi * u)) (-900 * t))
+
+fullerOutline :: NonEmpty Point
+fullerOutline = fullerPoint (-1) 0 :|
+  (fmap (fullerPoint (-1) . fraction) [1 .. 48] <> fmap (fullerPoint 1 . fraction) [47, 46 .. 1])
+
+bladeSeeds :: [Point]
+bladeSeeds = fmap
+  (\index -> bladePoint (0.77 * sin (fromIntegral index * 2.39996)) ((fromIntegral index + 0.5) / 118))
+  [0 .. 117 :: Int]
+
+guardPoint :: Double -> Double -> Point
+guardPoint height t = quantize (Point (-168 + 336 * t) (-75 + height * sin (pi * t)))
+
+guardOutline :: NonEmpty Point
+guardOutline = guardPoint 135 0 :|
+  (fmap (guardPoint 135 . fraction) [1 .. 48] <> fmap (guardPoint 88 . fraction) [47, 46 .. 1])
+
+guardSeeds :: [Point]
+guardSeeds = fmap
+  (\index -> guardPoint (110 + 13 * sin (fromIntegral index * 2.4)) ((fromIntegral index + 0.5) / 35))
+  [0 .. 34 :: Int]
+
+fraction :: Int -> Double
+fraction index = fromIntegral index / 48
+
+quantize :: Point -> Point
+quantize (Point x y) = Point (snap x) (snap y)
+ where
+  snap :: Double -> Double
+  snap value = fromIntegral (round (1024 * value) :: Integer) / 1024
+
+renderFacet :: Material -> Int -> Triangle -> String
+renderFacet material index (Triangle a@(Point ax ay) b@(Point bx by) c@(Point cx cy)) =
+  let t = negate (ay + by + cy) / 2700
+      across = ((ax + bx + cx) / 3 - bladeCenter t) / max 1 (bladeWidth (max 0 (min 1 t)))
+      shimmer = 0.5 + 0.5 * sin (fromIntegral index * 1.719)
+      light = max 0 (min 1 (0.35 + 0.23 * shimmer + 0.3 * across))
+      color = case material of
+        Moonsteel -> rgb (18 + 114 * light) (42 + 162 * light) (64 + 180 * light)
+        PaleGold -> rgb (102 + 137 * shimmer) (92 + 128 * shimmer) (69 + 112 * shimmer)
+      stroke = case material of Moonsteel -> "#a6d9e8"; PaleGold -> "#e9d9af"
+   in polygon ("fill=\"" <> color <> "\" stroke=\"" <> stroke
+      <> "\" stroke-width=\"0.48\" stroke-opacity=\"0.24\"") [a, b, c]
+
+rgb :: Double -> Double -> Double -> String
+rgb red green blue = printf "#%02x%02x%02x" (round red :: Int) (round green :: Int) (round blue :: Int)
+
+bladeEdges :: String
+bladeEdges =
+  polygon "fill=\"url(#silver)\"" (fmap (bladePoint 1 . fraction) [0 .. 48] <> fmap (bladePoint 0.78 . fraction) [47, 46 .. 0])
+  <> polygon "fill=\"#d8f4f1\" fill-opacity=\"0.75\"" (fmap (bladePoint (-1) . fraction) [0 .. 48] <> fmap (bladePoint (-0.94) . fraction) [47, 46 .. 0])
+  <> polygon "fill=\"#d3f1ed\" fill-opacity=\"0.55\"" (fmap (bladePoint 0.08 . fraction) [18 .. 48] <> fmap (bladePoint 0.04 . fraction) [47, 46 .. 18])
+  <> polygon "fill=\"none\" stroke=\"#deffff\" stroke-width=\"1.4\"" (NonEmpty.toList bladeOutline)
+
+bladeAura :: Double -> String
+bladeAura phase = "<g filter=\"url(#aura)\" opacity=\""
+  <> scalar (0.85 + 0.15 * cos (2 * pi * phase)) <> "\">"
+  <> polygon "fill=\"none\" stroke=\"#56cbf7\" stroke-width=\"11\" stroke-opacity=\"0.45\"" (NonEmpty.toList bladeOutline)
+  <> "</g>"
+
+-- The light is clipped to the selected triangles, so the real hole stays empty.
+-- The band lies entirely outside the blade at both ends of the cycle.
+lightSweep :: Double -> String
+lightSweep phase = "<g clip-path=\"url(#blade-clip)\"><rect x=\"-100\" y=\""
+  <> scalar (100 - 1250 * phase)
+  <> "\" width=\"300\" height=\"200\" fill=\"url(#reflection)\"/></g>"
+
+bladeEngraving :: Double -> String
+bladeEngraving phase = foldMap rune [0 .. 8 :: Int]
+ where
+  rune :: Int -> String
+  rune index =
+    let t = 0.43 + fromIntegral index * 0.047
+        Point x y = bladePoint (-0.36) t
+        marking = case index `mod` 3 of
+          0 -> "M -5 -6 L 0 0 L -5 6 M 0 0 L 7 0 M 3 -4 L 7 0 L 3 4"
+          1 -> "M 0 -7 L 0 7 M -5 -3 L 0 2 L 5 -3 M -3 7 L 3 7"
+          _ -> "M -5 0 L 0 -7 L 5 0 L 0 7 Z M -7 0 L 7 0"
+     in "<path transform=\"translate(" <> scalar x <> " " <> scalar y <> ") rotate("
+        <> scalar (atan ((12 + 80 * t) / 900) * 180 / pi) <> ")\" d=\"" <> marking
+        <> "\" fill=\"none\" stroke=\"#e4e7bd\" stroke-width=\"1.2\" stroke-linecap=\"round\" opacity=\""
+        <> scalar (0.72 + 0.23 * cos (2 * pi * phase - fromIntegral index * 0.7)) <> "\"/>"
+
+hilt :: String
+hilt =
+  "<path d=\"M -24 21 L 24 21 L 20 193 L 0 214 L -20 193 Z\" fill=\"url(#grip)\" stroke=\"#899da7\" stroke-width=\"2\"/>"
+  <> foldMap binding [0 .. 11 :: Int]
+  <> "<path d=\"M -24 29 H 24 M -22 40 H 22 M -20 182 H 20 M -20 192 H 20\" stroke=\"#d7cba8\" stroke-width=\"5\"/>"
+ where
+  binding :: Int -> String
+  binding index =
+    let y = 48 + fromIntegral index * 11
+     in "<path d=\"M -22 " <> scalar y <> " L 21 " <> scalar (y + 13)
+        <> " M 21 " <> scalar y <> " L -22 " <> scalar (y + 13)
+        <> "\" stroke=\"#55666e\" stroke-width=\"2\"/><path d=\"M -20 " <> scalar y
+        <> " L 20 " <> scalar (y + 13) <> "\" stroke=\"#b4b1a0\" stroke-width=\"0.5\"/>"
+
+guardFiligree :: String
+guardFiligree =
+  "<path d=\"M -146 -48 Q -85 42 0 36 Q 85 42 146 -48 M -125 -26 Q -68 47 -22 35 M 125 -26 Q 68 47 22 35\" fill=\"none\" stroke=\"#f8edc7\" stroke-width=\"1\"/>"
+  <> foldMap (\index -> circle (guardPoint 112 (fromIntegral index / 16)) 1.8 "fill=\"#f5e3b2\" stroke=\"#6e664f\" stroke-width=\"0.5\"") [1 .. 15 :: Int]
+
+fittings :: String
+fittings =
+  "<path d=\"M 0 -43 L 31 2 L 0 50 L -31 2 Z\" fill=\"#132d3c\" stroke=\"#ddd0a9\" stroke-width=\"3\"/>"
+  <> "<path d=\"M 0 -28 L 18 2 L 0 32 L -18 2 Z\" fill=\"url(#gem)\" stroke=\"#b4e6ea\" stroke-width=\"1\"/>"
+  <> "<path d=\"M 0 -28 L 0 32 M -18 2 H 18\" stroke=\"#deffff\" stroke-opacity=\"0.5\" stroke-width=\"0.7\"/>"
+  <> circle (Point 0 226) 34 "fill=\"#101e2a\" stroke=\"#cec39e\" stroke-width=\"3\""
+  <> circle (Point 0 226) 28 "fill=\"url(#gem)\" stroke=\"#719bb0\" stroke-width=\"0.8\""
+  <> "<path d=\"M 9 202 A 25 25 0 1 0 9 250 A 27 27 0 0 1 9 202 Z\" fill=\"#e7e7cd\"/>"
+  <> "<path d=\"M 0 261 L 0 277 M -4 269 L 0 277 L 4 269\" stroke=\"#cec39e\" fill=\"none\" stroke-width=\"2\"/>"
+
+background :: Double -> String
+background phase =
+  "<rect width=\"1600\" height=\"1200\" fill=\"url(#night)\"/>"
+  <> "<g opacity=\"" <> scalar (0.85 + 0.15 * cos (2 * pi * phase)) <> "\">"
+  <> circle (Point 1040 337) 379 "fill=\"url(#moon-halo)\""
+  <> circle (Point 1040 337) 264 "fill=\"url(#moon)\" stroke=\"#8bafbd\" stroke-opacity=\"0.16\" stroke-width=\"0.8\""
+  <> "</g><g fill=\"none\" stroke=\"#7593a1\" stroke-width=\"0.55\" opacity=\"0.24\"><circle cx=\"1040\" cy=\"337\" r=\"302\"/><circle cx=\"1040\" cy=\"337\" r=\"310\" stroke-dasharray=\"1 9\"/><path d=\"M 718 139 A 379 379 0 0 1 1349 557\"/></g>"
+  <> foldMap star [0 .. 219 :: Int]
+  <> "<path d=\"M 1166 135 V 191 M 1138 163 H 1194\" stroke=\"#c4eff2\" stroke-width=\"0.8\" opacity=\""
+  <> scalar (0.65 + 0.15 * cos (2 * pi * phase)) <> "\"/>"
+ where
+  star :: Int -> String
+  star index = circle
+    (Point (45 + fromIntegral ((index * 947 + 71) `mod` 1510)) (45 + fromIntegral ((index * 389 + 173) `mod` 1110)))
+    (if index `mod` 17 == 0 then 1.25 else 0.6)
+    ("fill=\"#aec7d4\" opacity=\"" <> scalar (0.28 + 0.1 * cos (2 * pi * phase + fromIntegral index * 1.71)) <> "\"")
+
+definitions :: String
+definitions = "<defs>"
+  <> "<radialGradient id=\"night\" cx=\"68%\" cy=\"28%\" r=\"85%\"><stop stop-color=\"#1e394c\"/><stop offset=\"0.53\" stop-color=\"#0b1927\"/><stop offset=\"1\" stop-color=\"#050b14\"/></radialGradient>"
+  <> "<radialGradient id=\"moon-halo\"><stop stop-color=\"#86b2cf\" stop-opacity=\"0.15\"/><stop offset=\"1\" stop-color=\"#86b2cf\" stop-opacity=\"0\"/></radialGradient>"
+  <> "<radialGradient id=\"moon\" cx=\"33%\" cy=\"25%\" r=\"80%\"><stop stop-color=\"#7c9daa\" stop-opacity=\"0.30\"/><stop offset=\"0.6\" stop-color=\"#58778c\" stop-opacity=\"0.13\"/><stop offset=\"1\" stop-color=\"#182b3c\" stop-opacity=\"0.1\"/></radialGradient>"
+  <> "<linearGradient id=\"silver\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0.4\"><stop stop-color=\"#7193a4\"/><stop offset=\"0.35\" stop-color=\"#d5e7e7\"/><stop offset=\"0.65\" stop-color=\"#f0fcf0\"/><stop offset=\"1\" stop-color=\"#8ebccd\"/></linearGradient>"
+  <> "<linearGradient id=\"reflection\" x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"><stop stop-color=\"#d9ffff\" stop-opacity=\"0\"/><stop offset=\"0.35\" stop-color=\"#a4edff\" stop-opacity=\"0.08\"/><stop offset=\"0.5\" stop-color=\"#edffff\" stop-opacity=\"0.42\"/><stop offset=\"0.65\" stop-color=\"#a4edff\" stop-opacity=\"0.08\"/><stop offset=\"1\" stop-color=\"#d9ffff\" stop-opacity=\"0\"/></linearGradient>"
+  <> "<linearGradient id=\"grip\"><stop stop-color=\"#101822\"/><stop offset=\"0.45\" stop-color=\"#344455\"/><stop offset=\"1\" stop-color=\"#080f18\"/></linearGradient>"
+  <> "<radialGradient id=\"gem\" cx=\"30%\" cy=\"20%\"><stop stop-color=\"#cff6ed\"/><stop offset=\"0.35\" stop-color=\"#5397b7\"/><stop offset=\"1\" stop-color=\"#132a52\"/></radialGradient>"
+  <> "<filter id=\"aura\" x=\"-50%\" y=\"-20%\" width=\"200%\" height=\"140%\"><feGaussianBlur stdDeviation=\"10\"/></filter></defs>"
+
+typography :: String
+typography =
+  "<g fill=\"#d4dcd6\"><text x=\"86\" y=\"99\" font-family=\"Helvetica, sans-serif\" font-size=\"12\" letter-spacing=\"4\">MOONLIGHT / RELIQUARY No. 01</text>"
+  <> "<path d=\"M 86 123 H 416\" stroke=\"#758891\" stroke-width=\"0.7\"/>"
+  <> "<text x=\"79\" y=\"258\" font-family=\"Georgia, serif\" font-size=\"102\" letter-spacing=\"-4\">Vesper.</text>"
+  <> "<text x=\"88\" y=\"301\" font-family=\"Helvetica, sans-serif\" font-size=\"15\" letter-spacing=\"7\">THE MOONBLADE</text></g>"
+  <> "<g fill=\"#91a9b6\" font-family=\"Georgia, serif\" font-size=\"20\" font-style=\"italic\"><text x=\"88\" y=\"369\">An edge of borrowed light.</text><text x=\"88\" y=\"400\">A silence given shape.</text></g>"
+  <> "<g fill=\"#94a7ad\" font-family=\"Helvetica, sans-serif\" font-size=\"11\" letter-spacing=\"2\"><text x=\"88\" y=\"1138\">MOONSTEEL / PALE GOLD / MIDNIGHT GLASS</text><text x=\"1512\" y=\"1138\" text-anchor=\"end\">FORGED IN PLANAR GEOMETRY</text></g>"
diff --git a/docs/moonblade/README.md b/docs/moonblade/README.md
new file mode 100644
--- /dev/null
+++ b/docs/moonblade/README.md
@@ -0,0 +1,77 @@
+# Vesper, the moonblade
+
+A generative vector sword built through Moonlight Planar's public `build` and
+`dcel` components. Its straight broad blade and concave bronze star guard are
+constrained Delaunay domains. `boundedRegionFaces` selects their interiors.
+Every material facet comes from the native mesh, with intentionally quiet
+contrast instead of the earlier prominent triangular mosaic.
+
+The current design follows the supplied bronze-and-turquoise sword reference:
+symmetric guard, rigid silhouette, leather-wrapped grip, an illuminated pommel,
+and a small constellation held inside a luminous moon-glass blade.
+
+The supplied earlier crescent-guard illustration is also preserved as an animated
+variant: rigid hover, traveling blade reflection, and gently pulsing runes and
+moonlight. Its pierced fuller remains a real hole, including in the reflection
+clip. Both designs use the same native constrained-domain builder. Animation
+builds its meshes once and renders 80 immutable frames at 80 ms per frame.
+
+The point curves, materials, lighting, grip, engraving, and typography are art
+direction. The result is an illustration, not a manufacturing specification.
+Construction and rendering are pure; `main` only reads the path and writes SVG.
+The small SVG encoders are shared with the Noctilucent Garden exhibit.
+
+From the repository root:
+
+```sh
+scripts/safe-cabal.sh build moonlight-planar:lib:dcel \
+  --project-file=cabal.project.planar-dev --builddir=dist-newstyle-planar-dev --offline -j6
+scripts/safe-cabal.sh run moonlight-planar:exe:moonlight-planar-moonblade \
+  --project-file=cabal.project.planar-dev --builddir=dist-newstyle-planar-dev --offline -j6 -- \
+  /Users/bluerose/Developer/pale-meridian/compiler/foundation/moonlight-planar/docs/moonblade/moonblade.svg
+```
+
+Edit the source and regenerate; do not hand-edit the derived SVG.
+
+To render the crescent animation's 1200-by-900 SVG frames, replace the final
+output argument above with `crescent-frames /absolute/path/to/vesper-frames`.
+Rasterize those SVGs and encode them at 12.5 fps for a seamless 6.4-second loop.
+
+## Riggable 3D crescent
+
+`crescent-solid OUTPUT.obj` exports the same native blade and guard as solid
+meshes in metres, with a raised blade center, thin edge, and open fuller. The
+OBJ is a transport view; Blender welds coincident face vertices on import.
+No raster image or painted mask supplies the geometry.
+
+Build the rig with Blender 5.1:
+
+```sh
+blender --background --factory-startup --python-exit-code 1 \
+  --python compiler/foundation/moonlight-planar/docs/moonblade/rig.py -- \
+  /absolute/output/vesper.obj /absolute/output
+```
+
+This writes `vesper-rig.blend`, `vesper-rig.glb`, `vesper-rig.png`, control
+instructions and `deformation-checks.json`. In Pose Mode select `ROOT_Sword`:
+its custom properties give side bend ±25°, flat bend ±15°, and twist ±25°.
+Zero all three for a rigid weapon. The grip, fittings and blade base do not
+deform. Six connected blade bones use normalized smoothstep weights, at most
+two influences per vertex; no automatic weighting or simulation is involved.
+
+The blend preserves these bounded controls. GLB carries skinning and sampled
+animation, not Blender's custom-property drivers. The provided UVs are surface
+projections, not a unique baking atlas. These are artistic deformation controls,
+not a physical model of steel or collision certification.
+
+The checks exercise ten poses, including both signs of each axis, combined
+extremes and an out-of-range control value. They measure face-area ratios,
+finite coordinates and rigid-fitting drift, plus rest mesh manifoldness and
+weight normalization. The sample animation occupies frames 1–120 at 24 fps.
+To render a 12-fps preview from the saved scene:
+
+```sh
+blender --background /absolute/output/vesper-rig.blend \
+  --render-output /absolute/output/preview/frame- --render-format PNG \
+  --frame-start 1 --frame-end 120 --frame-jump 2 --render-anim
+```
diff --git a/docs/moonblade/moonblade.svg b/docs/moonblade/moonblade.svg
new file mode 100644
--- /dev/null
+++ b/docs/moonblade/moonblade.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="1600" viewBox="0 0 1400 1600"><title>Vesper - the astral moonblade</title><desc>A straight broad moon-glass blade with a luminous turquoise edge, flowing constellations, a symmetric bronze star guard, and a leather-wrapped grip. 351 blade facets and 56 guard facets are native Moonlight Planar constrained Delaunay triangles. Subtle facets support the materials rather than dominating the design. Light, ornament and composition are authored SVG.</desc><defs><radialGradient id="night" cx="50%" cy="43%" r="75%"><stop stop-color="#163035"/><stop offset="0.5" stop-color="#101f25"/><stop offset="1" stop-color="#091218"/></radialGradient><radialGradient id="atmosphere"><stop stop-color="#126b66" stop-opacity="0.14"/><stop offset="1" stop-color="#126b66" stop-opacity="0"/></radialGradient><linearGradient id="moonglass"><stop stop-color="#117b80"/><stop offset="0.10" stop-color="#14535e"/><stop offset="0.28" stop-color="#183a49"/><stop offset="0.55" stop-color="#142e3e"/><stop offset="0.85" stop-color="#155563"/><stop offset="1" stop-color="#1ca1a1"/></linearGradient><linearGradient id="edge"><stop stop-color="#e0fff2"/><stop offset="0.4" stop-color="#4adccf"/><stop offset="1" stop-color="#a9fff2"/></linearGradient><linearGradient id="bronze" x1="0" y1="0" x2="0.7" y2="1"><stop stop-color="#775038"/><stop offset="0.23" stop-color="#c88f5e"/><stop offset="0.46" stop-color="#efc28b"/><stop offset="0.53" stop-color="#a46c46"/><stop offset="0.80" stop-color="#be875a"/><stop offset="1" stop-color="#6d4833"/></linearGradient><linearGradient id="grip"><stop stop-color="#37271e"/><stop offset="0.45" stop-color="#805637"/><stop offset="0.68" stop-color="#68482f"/><stop offset="1" stop-color="#2d211b"/></linearGradient><radialGradient id="gem" cx="35%" cy="22%"><stop stop-color="#81e4d1"/><stop offset="0.25" stop-color="#188e98"/><stop offset="1" stop-color="#122e3e"/></radialGradient><radialGradient id="mote"><stop stop-color="#69fce5" stop-opacity="0.75"/><stop offset="0.3" stop-color="#39d5d0" stop-opacity="0.3"/><stop offset="1" stop-color="#39d5d0" stop-opacity="0"/></radialGradient><radialGradient id="amber"><stop stop-color="#ffd889" stop-opacity="0.8"/><stop offset="1" stop-color="#f9b44d" stop-opacity="0"/></radialGradient></defs><rect width="1400" height="1600" fill="url(#night)"/><ellipse cx="700" cy="665" rx="360" ry="740" fill="url(#atmosphere)"/><g fill="none" stroke="#597275" stroke-width="0.7" opacity="0.23"><circle cx="700" cy="655" r="360"/><circle cx="700" cy="655" r="368" stroke-dasharray="1 12"/><path d="M 302 655 H 345 M 1055 655 H 1098 M 700 256 V 300 M 700 1010 V 1055"/></g><circle cx="126.000" cy="228.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="1073.000" cy="617.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="730.000" cy="1006.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="387.000" cy="1395.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1334.000" cy="294.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="991.000" cy="683.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="648.000" cy="1072.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="305.000" cy="1461.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1252.000" cy="360.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="909.000" cy="749.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="566.000" cy="1138.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="223.000" cy="1527.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1170.000" cy="426.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="827.000" cy="815.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="484.000" cy="1204.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="141.000" cy="103.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1088.000" cy="492.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="745.000" cy="881.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="402.000" cy="1270.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="59.000" cy="169.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1006.000" cy="558.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="663.000" cy="947.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="320.000" cy="1336.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1267.000" cy="235.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="924.000" cy="624.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="581.000" cy="1013.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="238.000" cy="1402.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1185.000" cy="301.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="842.000" cy="690.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="499.000" cy="1079.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="156.000" cy="1468.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1103.000" cy="367.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="760.000" cy="756.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="417.000" cy="1145.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="74.000" cy="1534.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="1021.000" cy="433.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="678.000" cy="822.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="335.000" cy="1211.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1282.000" cy="110.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="939.000" cy="499.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="596.000" cy="888.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="253.000" cy="1277.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1200.000" cy="176.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="857.000" cy="565.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="514.000" cy="954.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="171.000" cy="1343.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1118.000" cy="242.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="775.000" cy="631.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="432.000" cy="1020.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="89.000" cy="1409.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1036.000" cy="308.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="693.000" cy="697.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="350.000" cy="1086.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1297.000" cy="1475.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="954.000" cy="374.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="611.000" cy="763.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="268.000" cy="1152.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1215.000" cy="1541.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="872.000" cy="440.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="529.000" cy="829.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="186.000" cy="1218.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1133.000" cy="117.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="790.000" cy="506.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="447.000" cy="895.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="104.000" cy="1284.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1051.000" cy="183.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="708.000" cy="572.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="365.000" cy="961.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1312.000" cy="1350.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="969.000" cy="249.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="626.000" cy="638.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="283.000" cy="1027.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1230.000" cy="1416.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="887.000" cy="315.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="544.000" cy="704.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="201.000" cy="1093.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1148.000" cy="1482.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="805.000" cy="381.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="462.000" cy="770.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="119.000" cy="1159.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1066.000" cy="58.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="723.000" cy="447.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="380.000" cy="836.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1327.000" cy="1225.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="984.000" cy="124.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="641.000" cy="513.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="298.000" cy="902.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1245.000" cy="1291.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="902.000" cy="190.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="559.000" cy="579.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="216.000" cy="968.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1163.000" cy="1357.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="820.000" cy="256.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="477.000" cy="645.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="134.000" cy="1034.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1081.000" cy="1423.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="738.000" cy="322.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="395.000" cy="711.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1342.000" cy="1100.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="999.000" cy="1489.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="656.000" cy="388.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="313.000" cy="777.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1260.000" cy="1166.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="917.000" cy="65.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="574.000" cy="454.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="231.000" cy="843.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1178.000" cy="1232.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="835.000" cy="131.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="492.000" cy="520.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="149.000" cy="909.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1096.000" cy="1298.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="753.000" cy="197.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="410.000" cy="586.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="67.000" cy="975.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1014.000" cy="1364.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="671.000" cy="263.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="328.000" cy="652.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1275.000" cy="1041.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="932.000" cy="1430.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="589.000" cy="329.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="246.000" cy="718.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1193.000" cy="1107.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="850.000" cy="1496.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="507.000" cy="395.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="164.000" cy="784.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1111.000" cy="1173.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="768.000" cy="72.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="425.000" cy="461.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="82.000" cy="850.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1029.000" cy="1239.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="686.000" cy="138.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="343.000" cy="527.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1290.000" cy="916.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="947.000" cy="1305.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="604.000" cy="204.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="261.000" cy="593.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="1208.000" cy="982.000" r="1.000" fill="#94b9b0" opacity="0.2"/><circle cx="865.000" cy="1371.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="522.000" cy="270.000" r="0.500" fill="#94b9b0" opacity="0.2"/><circle cx="179.000" cy="659.000" r="0.500" fill="#94b9b0" opacity="0.2"/><ellipse cx="700" cy="1490" rx="109" ry="11" fill="#000706" opacity="0.4"/><g transform="translate(700 1110)"><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="10.000" stroke-opacity="0.050" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="14.000" stroke-opacity="0.043" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="18.000" stroke-opacity="0.038" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="22.000" stroke-opacity="0.033" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="26.000" stroke-opacity="0.028" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="30.000" stroke-opacity="0.024" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="34.000" stroke-opacity="0.021" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="38.000" stroke-opacity="0.018" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="42.000" stroke-opacity="0.016" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="46.000" stroke-opacity="0.014" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="50.000" stroke-opacity="0.012" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="54.000" stroke-opacity="0.010" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="58.000" stroke-opacity="0.009" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="62.000" stroke-opacity="0.008" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="66.000" stroke-opacity="0.007" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="70.000" stroke-opacity="0.006" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="74.000" stroke-opacity="0.005" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="78.000" stroke-opacity="0.004" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="82.000" stroke-opacity="0.004" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#30efd8" stroke-width="86.000" stroke-opacity="0.003" stroke-linejoin="round"/><path d="M -31 49 H 31 L 27 248 L 34 263 H -34 L -27 248 Z" fill="url(#grip)" stroke="#3a261b" stroke-width="4"/><g transform="translate(0 89.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 106.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 123.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 140.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 157.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 174.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 191.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 208.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g transform="translate(0 225.000)"><path d="M -28 0 Q 0 12 28 17 M 28 0 Q 0 12 -28 17" stroke="#31221b" stroke-width="4" fill="none"/><path d="M -28 -1 Q 0 11 28 16 M 28 -1 Q 0 11 -28 16" stroke="#be8960" stroke-width="1.6" fill="none"/><path d="M 0 6 L 3 9 L 0 12 L -3 9 Z" fill="#d3a268"/></g><g fill="url(#bronze)" stroke="#42291c" stroke-width="2"><rect x="-35" y="72" width="70" height="13" rx="3"/><rect x="-32" y="242" width="64" height="10" rx="2"/><rect x="-36" y="258" width="72" height="11" rx="3"/></g><path d="M -31 75 H 31 M -29 245 H 29 M -31 261 H 31" stroke="#ecc391" stroke-width="1.4"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="url(#moonglass)" stroke="#033c49" stroke-width="11" stroke-linejoin="round"/><polygon points="-25.105,-523.438 25.105,-523.438 -38.175,-492.188" fill="#164754" fill-opacity="0.24"/><polygon points="-38.175,-492.188 25.105,-523.438 38.175,-492.188" fill="#1c4553" fill-opacity="0.24"/><polygon points="-38.175,-492.188 38.175,-492.188 -49.846,-460.938" fill="#144955" fill-opacity="0.24"/><polygon points="-49.846,-460.938 38.175,-492.188 49.846,-460.938" fill="#114754" fill-opacity="0.24"/><polygon points="25.105,-523.438 -25.105,-523.438 -27.022,-554.688" fill="#194653" fill-opacity="0.24"/><polygon points="25.105,-523.438 -27.022,-554.688 27.022,-554.688" fill="#1a4553" fill-opacity="0.24"/><polygon points="-38.175,-492.188 -66.265,-507.812 -25.105,-523.438" fill="#11535e" fill-opacity="0.24"/><polygon points="-61.180,-476.562 -38.175,-492.188 -49.846,-460.938" fill="#135660" fill-opacity="0.24"/><polygon points="49.846,-460.938 38.175,-492.188 61.180,-476.562" fill="#1c5660" fill-opacity="0.24"/><polygon points="-66.265,-507.812 -38.175,-492.188 -61.180,-476.562" fill="#175862" fill-opacity="0.24"/><polygon points="38.175,-492.188 25.105,-523.438 66.265,-507.812" fill="#10535e" fill-opacity="0.24"/><polygon points="61.180,-476.562 38.175,-492.188 66.265,-507.812" fill="#165862" fill-opacity="0.24"/><polygon points="-49.846,-460.938 -41.919,-414.062 -52.854,-445.312" fill="#1c555f" fill-opacity="0.24"/><polygon points="-49.846,-460.938 41.919,-414.062 -41.919,-414.062" fill="#144955" fill-opacity="0.24"/><polygon points="-25.105,-523.438 -66.265,-507.812 -67.726,-539.062" fill="#115761" fill-opacity="0.24"/><polygon points="-27.022,-554.688 -25.105,-523.438 -67.726,-539.062" fill="#1a525d" fill-opacity="0.24"/><polygon points="25.105,-523.438 27.022,-554.688 67.726,-539.062" fill="#1a525d" fill-opacity="0.24"/><polygon points="66.265,-507.812 25.105,-523.438 67.726,-539.062" fill="#115761" fill-opacity="0.24"/><polygon points="-52.854,-445.312 -41.919,-414.062 -59.217,-429.688" fill="#135760" fill-opacity="0.24"/><polygon points="41.919,-414.062 -49.846,-460.938 49.846,-460.938" fill="#1c4854" fill-opacity="0.24"/><polygon points="41.919,-414.062 49.846,-460.938 52.854,-445.312" fill="#17555f" fill-opacity="0.24"/><polygon points="41.919,-414.062 52.854,-445.312 59.217,-429.688" fill="#105760" fill-opacity="0.24"/><polygon points="-27.022,-554.688 -65.463,-570.312 -39.765,-585.938" fill="#17545e" fill-opacity="0.24"/><polygon points="27.022,-554.688 -39.765,-585.938 39.765,-585.938" fill="#1c4653" fill-opacity="0.24"/><polygon points="27.022,-554.688 65.463,-570.312 67.726,-539.062" fill="#145761" fill-opacity="0.24"/><polygon points="-39.765,-585.938 27.022,-554.688 -27.022,-554.688" fill="#114754" fill-opacity="0.24"/><polygon points="-66.265,-507.812 -61.180,-476.562 -97.000,-500.000" fill="#1a6068" fill-opacity="0.24"/><polygon points="61.180,-476.562 66.265,-507.812 97.000,-500.000" fill="#1a6068" fill-opacity="0.24"/><polygon points="-65.463,-570.312 -27.022,-554.688 -67.726,-539.062" fill="#115761" fill-opacity="0.24"/><polygon points="65.463,-570.312 27.022,-554.688 39.765,-585.938" fill="#14545e" fill-opacity="0.24"/><polygon points="-97.000,-500.000 -61.180,-476.562 -97.125,-479.167" fill="#1c646b" fill-opacity="0.24"/><polygon points="61.180,-476.562 97.000,-500.000 97.125,-479.167" fill="#17646b" fill-opacity="0.24"/><polygon points="-67.726,-539.062 -66.265,-507.812 -96.875,-520.833" fill="#106169" fill-opacity="0.24"/><polygon points="-96.875,-520.833 -66.265,-507.812 -97.000,-500.000" fill="#17656c" fill-opacity="0.24"/><polygon points="66.265,-507.812 67.726,-539.062 96.875,-520.833" fill="#1c6169" fill-opacity="0.24"/><polygon points="97.000,-500.000 66.265,-507.812 96.875,-520.833" fill="#13656c" fill-opacity="0.24"/><polygon points="-97.125,-479.167 -61.180,-476.562 -97.250,-458.333" fill="#11646b" fill-opacity="0.24"/><polygon points="61.180,-476.562 52.854,-445.312 49.846,-460.938" fill="#1a5861" fill-opacity="0.24"/><polygon points="52.854,-445.312 61.180,-476.562 97.250,-458.333" fill="#1a5e67" fill-opacity="0.24"/><polygon points="52.854,-445.312 97.375,-437.500 59.217,-429.688" fill="#115e66" fill-opacity="0.24"/><polygon points="97.250,-458.333 61.180,-476.562 97.125,-479.167" fill="#14646b" fill-opacity="0.24"/><polygon points="-67.726,-539.062 -96.875,-520.833 -96.750,-541.667" fill="#1c656c" fill-opacity="0.24"/><polygon points="-67.726,-539.062 -96.625,-562.500 -65.463,-570.312" fill="#166169" fill-opacity="0.24"/><polygon points="96.875,-520.833 67.726,-539.062 96.750,-541.667" fill="#10656c" fill-opacity="0.24"/><polygon points="67.726,-539.062 96.625,-562.500 96.750,-541.667" fill="#17656c" fill-opacity="0.24"/><polygon points="-52.854,-445.312 -97.250,-458.333 -61.180,-476.562" fill="#1c5e67" fill-opacity="0.24"/><polygon points="-97.250,-458.333 -52.854,-445.312 -97.375,-437.500" fill="#13636a" fill-opacity="0.24"/><polygon points="-52.854,-445.312 -61.180,-476.562 -49.846,-460.938" fill="#115861" fill-opacity="0.24"/><polygon points="-97.375,-437.500 -52.854,-445.312 -59.217,-429.688" fill="#1a5e66" fill-opacity="0.24"/><polygon points="97.375,-437.500 52.854,-445.312 97.250,-458.333" fill="#19636a" fill-opacity="0.24"/><polygon points="-96.625,-562.500 -67.726,-539.062 -96.750,-541.667" fill="#11656c" fill-opacity="0.24"/><polygon points="96.625,-562.500 67.726,-539.062 65.463,-570.312" fill="#146169" fill-opacity="0.24"/><polygon points="-41.919,-414.062 29.212,-382.812 -29.212,-382.812" fill="#1c4854" fill-opacity="0.24"/><polygon points="29.212,-382.812 -41.919,-414.062 41.919,-414.062" fill="#164653" fill-opacity="0.24"/><polygon points="-59.217,-429.688 -41.919,-414.062 -65.557,-398.438" fill="#105862" fill-opacity="0.24"/><polygon points="-65.557,-398.438 -41.919,-414.062 -29.212,-382.812" fill="#18545f" fill-opacity="0.24"/><polygon points="41.919,-414.062 59.217,-429.688 65.557,-398.438" fill="#1b5862" fill-opacity="0.24"/><polygon points="29.212,-382.812 41.919,-414.062 65.557,-398.438" fill="#13545f" fill-opacity="0.24"/><polygon points="-39.765,-585.938 -65.463,-570.312 -59.661,-601.562" fill="#125862" fill-opacity="0.24"/><polygon points="65.463,-570.312 39.765,-585.938 59.661,-601.562" fill="#1b5862" fill-opacity="0.24"/><polygon points="-97.375,-437.500 -59.217,-429.688 -97.500,-416.667" fill="#19646b" fill-opacity="0.24"/><polygon points="-97.500,-416.667 -59.217,-429.688 -65.557,-398.438" fill="#106068" fill-opacity="0.24"/><polygon points="59.217,-429.688 97.375,-437.500 97.500,-416.667" fill="#15646b" fill-opacity="0.24"/><polygon points="65.557,-398.438 59.217,-429.688 97.500,-416.667" fill="#1c6068" fill-opacity="0.24"/><polygon points="-65.463,-570.312 -96.625,-562.500 -96.500,-583.333" fill="#16646c" fill-opacity="0.24"/><polygon points="-59.661,-601.562 -65.463,-570.312 -96.500,-583.333" fill="#106068" fill-opacity="0.24"/><polygon points="96.625,-562.500 65.463,-570.312 96.500,-583.333" fill="#18646c" fill-opacity="0.24"/><polygon points="96.500,-583.333 65.463,-570.312 59.661,-601.562" fill="#1b6068" fill-opacity="0.24"/><polygon points="39.765,-585.938 -39.765,-585.938 -50.883,-617.188" fill="#124955" fill-opacity="0.24"/><polygon points="-50.883,-617.188 -39.765,-585.938 -59.661,-601.562" fill="#125660" fill-opacity="0.24"/><polygon points="39.765,-585.938 -50.883,-617.188 50.883,-617.188" fill="#1b4754" fill-opacity="0.24"/><polygon points="59.661,-601.562 39.765,-585.938 50.883,-617.188" fill="#195660" fill-opacity="0.24"/><polygon points="-97.500,-416.667 -65.557,-398.438 -97.625,-395.833" fill="#10656c" fill-opacity="0.24"/><polygon points="65.557,-398.438 97.500,-416.667 97.625,-395.833" fill="#15656c" fill-opacity="0.24"/><polygon points="-59.661,-601.562 -96.500,-583.333 -96.375,-604.167" fill="#1c646b" fill-opacity="0.24"/><polygon points="96.500,-583.333 59.661,-601.562 96.375,-604.167" fill="#15646b" fill-opacity="0.24"/><polygon points="-50.883,-617.188 50.778,-632.812 50.883,-617.188" fill="#104955" fill-opacity="0.24"/><polygon points="50.778,-632.812 -50.883,-617.188 -50.778,-632.812" fill="#184955" fill-opacity="0.24"/><polygon points="-65.557,-398.438 -29.212,-382.812 -68.369,-367.188" fill="#1b5861" fill-opacity="0.24"/><polygon points="-65.557,-398.438 -97.750,-375.000 -97.625,-395.833" fill="#12656c" fill-opacity="0.24"/><polygon points="29.212,-382.812 65.557,-398.438 68.369,-367.188" fill="#125861" fill-opacity="0.24"/><polygon points="65.557,-398.438 97.750,-375.000 68.369,-367.188" fill="#1b6169" fill-opacity="0.24"/><polygon points="-29.212,-382.812 23.446,-351.562 -23.446,-351.562" fill="#184653" fill-opacity="0.24"/><polygon points="-68.369,-367.188 -29.212,-382.812 -23.446,-351.562" fill="#10525d" fill-opacity="0.24"/><polygon points="23.446,-351.562 -29.212,-382.812 29.212,-382.812" fill="#154553" fill-opacity="0.24"/><polygon points="23.446,-351.562 29.212,-382.812 68.369,-367.188" fill="#1c525d" fill-opacity="0.24"/><polygon points="-97.750,-375.000 -65.557,-398.438 -68.369,-367.188" fill="#156169" fill-opacity="0.24"/><polygon points="97.750,-375.000 65.557,-398.438 97.625,-395.833" fill="#10656c" fill-opacity="0.24"/><polygon points="-59.661,-601.562 -96.375,-604.167 -96.250,-625.000" fill="#19646b" fill-opacity="0.24"/><polygon points="-50.883,-617.188 -59.661,-601.562 -96.250,-625.000" fill="#1b5e66" fill-opacity="0.24"/><polygon points="-50.778,-632.812 -50.883,-617.188 -96.250,-625.000" fill="#125c65" fill-opacity="0.24"/><polygon points="96.375,-604.167 59.661,-601.562 96.250,-625.000" fill="#12646b" fill-opacity="0.24"/><polygon points="96.250,-625.000 59.661,-601.562 50.883,-617.188" fill="#1b5e66" fill-opacity="0.24"/><polygon points="96.250,-625.000 50.883,-617.188 50.778,-632.812" fill="#185c65" fill-opacity="0.24"/><polygon points="-50.778,-632.812 -96.250,-625.000 -59.527,-648.438" fill="#105e66" fill-opacity="0.24"/><polygon points="96.250,-625.000 50.778,-632.812 59.527,-648.438" fill="#165e66" fill-opacity="0.24"/><polygon points="-50.778,-632.812 39.505,-664.062 50.778,-632.812" fill="#1c4754" fill-opacity="0.24"/><polygon points="-39.505,-664.062 -50.778,-632.812 -59.527,-648.438" fill="#155660" fill-opacity="0.24"/><polygon points="39.505,-664.062 -50.778,-632.812 -39.505,-664.062" fill="#104955" fill-opacity="0.24"/><polygon points="59.527,-648.438 50.778,-632.812 39.505,-664.062" fill="#195660" fill-opacity="0.24"/><polygon points="-97.750,-375.000 -68.369,-367.188 -97.875,-354.167" fill="#1b656c" fill-opacity="0.24"/><polygon points="68.369,-367.188 97.750,-375.000 97.875,-354.167" fill="#12656c" fill-opacity="0.24"/><polygon points="-68.369,-367.188 -23.446,-351.562 -67.426,-335.938" fill="#135761" fill-opacity="0.24"/><polygon points="-97.875,-354.167 -68.369,-367.188 -67.426,-335.938" fill="#1b6169" fill-opacity="0.24"/><polygon points="23.446,-351.562 68.369,-367.188 67.426,-335.938" fill="#185761" fill-opacity="0.24"/><polygon points="67.426,-335.938 68.369,-367.188 97.875,-354.167" fill="#106169" fill-opacity="0.24"/><polygon points="-59.527,-648.438 -96.250,-625.000 -96.125,-645.833" fill="#16646b" fill-opacity="0.24"/><polygon points="96.250,-625.000 59.527,-648.438 96.125,-645.833" fill="#1c646b" fill-opacity="0.24"/><polygon points="-67.426,-335.938 -23.446,-351.562 -36.777,-320.312" fill="#14535e" fill-opacity="0.24"/><polygon points="23.446,-351.562 67.426,-335.938 36.777,-320.312" fill="#11535e" fill-opacity="0.24"/><polygon points="-97.875,-354.167 -67.426,-335.938 -98.000,-333.333" fill="#19656c" fill-opacity="0.24"/><polygon points="67.426,-335.938 97.875,-354.167 98.000,-333.333" fill="#1a656c" fill-opacity="0.24"/><polygon points="-39.505,-664.062 -59.527,-648.438 -65.039,-679.688" fill="#115862" fill-opacity="0.24"/><polygon points="-59.527,-648.438 -96.000,-666.667 -65.039,-679.688" fill="#135f68" fill-opacity="0.24"/><polygon points="59.527,-648.438 39.505,-664.062 65.039,-679.688" fill="#1c5862" fill-opacity="0.24"/><polygon points="59.527,-648.438 96.000,-666.667 96.125,-645.833" fill="#17646b" fill-opacity="0.24"/><polygon points="-96.000,-666.667 -59.527,-648.438 -96.125,-645.833" fill="#10646b" fill-opacity="0.24"/><polygon points="96.000,-666.667 59.527,-648.438 65.039,-679.688" fill="#165f68" fill-opacity="0.24"/><polygon points="-39.505,-664.062 26.715,-695.312 39.505,-664.062" fill="#1c4653" fill-opacity="0.24"/><polygon points="-26.715,-695.312 -39.505,-664.062 -65.039,-679.688" fill="#14545e" fill-opacity="0.24"/><polygon points="26.715,-695.312 -39.505,-664.062 -26.715,-695.312" fill="#114754" fill-opacity="0.24"/><polygon points="65.039,-679.688 39.505,-664.062 26.715,-695.312" fill="#1a545e" fill-opacity="0.24"/><polygon points="-67.426,-335.938 -36.777,-320.312 -62.788,-304.688" fill="#1a5862" fill-opacity="0.24"/><polygon points="36.777,-320.312 67.426,-335.938 62.788,-304.688" fill="#115862" fill-opacity="0.24"/><polygon points="-98.000,-333.333 -67.426,-335.938 -98.125,-312.500" fill="#13656c" fill-opacity="0.24"/><polygon points="-98.125,-312.500 -67.426,-335.938 -62.788,-304.688" fill="#1c6068" fill-opacity="0.24"/><polygon points="67.426,-335.938 98.000,-333.333 98.125,-312.500" fill="#17656c" fill-opacity="0.24"/><polygon points="62.788,-304.688 67.426,-335.938 98.125,-312.500" fill="#106068" fill-opacity="0.24"/><polygon points="-65.039,-679.688 -96.000,-666.667 -95.875,-687.500" fill="#17646b" fill-opacity="0.24"/><polygon points="96.000,-666.667 65.039,-679.688 95.875,-687.500" fill="#1c646b" fill-opacity="0.24"/><polygon points="-62.788,-304.688 -36.777,-320.312 -48.834,-289.062" fill="#145660" fill-opacity="0.24"/><polygon points="36.777,-320.312 62.788,-304.688 48.834,-289.062" fill="#115660" fill-opacity="0.24"/><polygon points="-26.715,-695.312 -65.039,-679.688 -67.006,-710.938" fill="#1a5761" fill-opacity="0.24"/><polygon points="-67.006,-710.938 -65.039,-679.688 -95.875,-687.500" fill="#1a6068" fill-opacity="0.24"/><polygon points="65.039,-679.688 26.715,-695.312 67.006,-710.938" fill="#115761" fill-opacity="0.24"/><polygon points="95.875,-687.500 65.039,-679.688 67.006,-710.938" fill="#146068" fill-opacity="0.24"/><polygon points="-98.125,-312.500 -62.788,-304.688 -98.250,-291.667" fill="#1c656c" fill-opacity="0.24"/><polygon points="62.788,-304.688 98.125,-312.500 98.250,-291.667" fill="#17656c" fill-opacity="0.24"/><polygon points="-26.715,-695.312 24.861,-726.562 26.715,-695.312" fill="#104553" fill-opacity="0.24"/><polygon points="-24.861,-726.562 -26.715,-695.312 -67.006,-710.938" fill="#17525d" fill-opacity="0.24"/><polygon points="24.861,-726.562 -26.715,-695.312 -24.861,-726.562" fill="#1c4653" fill-opacity="0.24"/><polygon points="67.006,-710.938 26.715,-695.312 24.861,-726.562" fill="#13525d" fill-opacity="0.24"/><polygon points="-62.788,-304.688 -48.834,-289.062 -54.803,-273.438" fill="#115862" fill-opacity="0.24"/><polygon points="-98.250,-291.667 -62.788,-304.688 -54.803,-273.438" fill="#1a5f67" fill-opacity="0.24"/><polygon points="48.834,-289.062 62.788,-304.688 54.803,-273.438" fill="#1a5862" fill-opacity="0.24"/><polygon points="54.803,-273.438 62.788,-304.688 98.250,-291.667" fill="#115f67" fill-opacity="0.24"/><polygon points="-67.006,-710.938 -95.875,-687.500 -95.750,-708.333" fill="#14646c" fill-opacity="0.24"/><polygon points="95.875,-687.500 67.006,-710.938 95.750,-708.333" fill="#1c646c" fill-opacity="0.24"/><polygon points="-54.803,-273.438 -98.375,-270.833 -98.250,-291.667" fill="#16646b" fill-opacity="0.24"/><polygon points="54.803,-273.438 44.075,-242.188 48.834,-289.062" fill="#105660" fill-opacity="0.24"/><polygon points="-98.375,-270.833 -54.803,-273.438 -58.686,-257.812" fill="#175e67" fill-opacity="0.24"/><polygon points="54.803,-273.438 98.250,-291.667 98.375,-270.833" fill="#1c646b" fill-opacity="0.24"/><polygon points="58.686,-257.812 54.803,-273.438 98.375,-270.833" fill="#135e67" fill-opacity="0.24"/><polygon points="-67.006,-710.938 -95.750,-708.333 -95.625,-729.167" fill="#11646c" fill-opacity="0.24"/><polygon points="95.750,-708.333 67.006,-710.938 95.625,-729.167" fill="#1a646c" fill-opacity="0.24"/><polygon points="-24.861,-726.562 -67.006,-710.938 -65.284,-742.188" fill="#195761" fill-opacity="0.24"/><polygon points="-65.284,-742.188 -67.006,-710.938 -95.625,-729.167" fill="#116068" fill-opacity="0.24"/><polygon points="67.006,-710.938 24.861,-726.562 65.284,-742.188" fill="#145761" fill-opacity="0.24"/><polygon points="95.625,-729.167 67.006,-710.938 65.284,-742.188" fill="#1c6068" fill-opacity="0.24"/><polygon points="36.777,-320.312 -48.834,-289.062 -36.777,-320.312" fill="#164955" fill-opacity="0.24"/><polygon points="-48.834,-289.062 44.075,-242.188 -44.075,-242.188" fill="#104955" fill-opacity="0.24"/><polygon points="-54.803,-273.438 -48.834,-289.062 -44.075,-242.188" fill="#185660" fill-opacity="0.24"/><polygon points="-58.686,-257.812 -54.803,-273.438 -44.075,-242.188" fill="#1b5761" fill-opacity="0.24"/><polygon points="-36.777,-320.312 23.446,-351.562 36.777,-320.312" fill="#134553" fill-opacity="0.24"/><polygon points="-48.834,-289.062 48.834,-289.062 44.075,-242.188" fill="#124855" fill-opacity="0.24"/><polygon points="23.446,-351.562 -36.777,-320.312 -23.446,-351.562" fill="#1b4754" fill-opacity="0.24"/><polygon points="48.834,-289.062 -48.834,-289.062 36.777,-320.312" fill="#194754" fill-opacity="0.24"/><polygon points="44.075,-242.188 54.803,-273.438 58.686,-257.812" fill="#105761" fill-opacity="0.24"/><polygon points="-24.861,-726.562 -65.284,-742.188 -37.614,-757.812" fill="#15535e" fill-opacity="0.24"/><polygon points="65.284,-742.188 24.861,-726.562 37.614,-757.812" fill="#1c535e" fill-opacity="0.24"/><polygon points="-98.375,-270.833 -58.686,-257.812 -98.500,-250.000" fill="#16646b" fill-opacity="0.24"/><polygon points="58.686,-257.812 98.375,-270.833 98.500,-250.000" fill="#10646b" fill-opacity="0.24"/><polygon points="-65.284,-742.188 -95.625,-729.167 -95.500,-750.000" fill="#18646b" fill-opacity="0.24"/><polygon points="95.625,-729.167 65.284,-742.188 95.500,-750.000" fill="#1b646b" fill-opacity="0.24"/><polygon points="-98.500,-250.000 -58.686,-257.812 -65.565,-226.562" fill="#126068" fill-opacity="0.24"/><polygon points="-65.565,-226.562 -58.686,-257.812 -44.075,-242.188" fill="#125862" fill-opacity="0.24"/><polygon points="44.075,-242.188 58.686,-257.812 65.565,-226.562" fill="#1b5862" fill-opacity="0.24"/><polygon points="65.565,-226.562 58.686,-257.812 98.500,-250.000" fill="#196068" fill-opacity="0.24"/><polygon points="-37.614,-757.812 -65.284,-742.188 -59.789,-773.438" fill="#105861" fill-opacity="0.24"/><polygon points="-65.284,-742.188 -95.136,-770.833 -59.789,-773.438" fill="#155f67" fill-opacity="0.24"/><polygon points="65.284,-742.188 37.614,-757.812 59.789,-773.438" fill="#1c5861" fill-opacity="0.24"/><polygon points="65.284,-742.188 95.136,-770.833 95.500,-750.000" fill="#15646b" fill-opacity="0.24"/><polygon points="-98.500,-250.000 -65.565,-226.562 -98.625,-229.167" fill="#10656c" fill-opacity="0.24"/><polygon points="65.565,-226.562 98.500,-250.000 98.625,-229.167" fill="#18656c" fill-opacity="0.24"/><polygon points="-44.075,-242.188 31.427,-210.938 -31.427,-210.938" fill="#1b4855" fill-opacity="0.24"/><polygon points="-65.565,-226.562 -44.075,-242.188 -31.427,-210.938" fill="#12555f" fill-opacity="0.24"/><polygon points="31.427,-210.938 -44.075,-242.188 44.075,-242.188" fill="#124653" fill-opacity="0.24"/><polygon points="31.427,-210.938 44.075,-242.188 65.565,-226.562" fill="#1b555f" fill-opacity="0.24"/><polygon points="-95.136,-770.833 -65.284,-742.188 -95.500,-750.000" fill="#18646b" fill-opacity="0.24"/><polygon points="95.136,-770.833 65.284,-742.188 59.789,-773.438" fill="#105f67" fill-opacity="0.24"/><polygon points="-37.614,-757.812 -59.789,-773.438 -48.011,-789.062" fill="#155560" fill-opacity="0.24"/><polygon points="59.789,-773.438 37.614,-757.812 48.011,-789.062" fill="#1c5560" fill-opacity="0.24"/><polygon points="-98.625,-229.167 -65.565,-226.562 -98.750,-208.333" fill="#15656c" fill-opacity="0.24"/><polygon points="65.565,-226.562 98.625,-229.167 98.750,-208.333" fill="#10656c" fill-opacity="0.24"/><polygon points="-59.789,-773.438 -95.136,-770.833 -93.212,-791.667" fill="#19636a" fill-opacity="0.24"/><polygon points="95.136,-770.833 59.789,-773.438 93.212,-791.667" fill="#1b636a" fill-opacity="0.24"/><polygon points="-65.565,-226.562 -31.427,-210.938 -68.935,-195.312" fill="#125862" fill-opacity="0.24"/><polygon points="-98.750,-208.333 -65.565,-226.562 -68.935,-195.312" fill="#126169" fill-opacity="0.24"/><polygon points="31.427,-210.938 65.565,-226.562 68.935,-195.312" fill="#1b5862" fill-opacity="0.24"/><polygon points="68.935,-195.312 65.565,-226.562 98.750,-208.333" fill="#186169" fill-opacity="0.24"/><polygon points="-48.011,-789.062 37.614,-757.812 -37.614,-757.812" fill="#104855" fill-opacity="0.24"/><polygon points="49.438,-804.688 -49.438,-804.688 35.835,-835.938" fill="#164754" fill-opacity="0.24"/><polygon points="-31.427,-210.938 21.740,-179.688 -21.740,-179.688" fill="#1c4653" fill-opacity="0.24"/><polygon points="-68.935,-195.312 -31.427,-210.938 -21.740,-179.688" fill="#15525d" fill-opacity="0.24"/><polygon points="21.740,-179.688 -31.427,-210.938 31.427,-210.938" fill="#104552" fill-opacity="0.24"/><polygon points="21.740,-179.688 31.427,-210.938 68.935,-195.312" fill="#19525d" fill-opacity="0.24"/><polygon points="-98.750,-208.333 -68.935,-195.312 -98.875,-187.500" fill="#1b666c" fill-opacity="0.24"/><polygon points="68.935,-195.312 98.750,-208.333 98.875,-187.500" fill="#12666c" fill-opacity="0.24"/><polygon points="-93.212,-791.667 -49.438,-804.688 -48.011,-789.062" fill="#135b64" fill-opacity="0.24"/><polygon points="-49.438,-804.688 -89.564,-812.500 -53.386,-820.312" fill="#1b5c65" fill-opacity="0.24"/><polygon points="-93.212,-791.667 -48.011,-789.062 -59.789,-773.438" fill="#185d65" fill-opacity="0.24"/><polygon points="48.011,-789.062 93.212,-791.667 59.789,-773.438" fill="#105d65" fill-opacity="0.24"/><polygon points="-89.564,-812.500 -49.438,-804.688 -93.212,-791.667" fill="#166169" fill-opacity="0.24"/><polygon points="93.212,-791.667 49.438,-804.688 89.564,-812.500" fill="#1c6169" fill-opacity="0.24"/><polygon points="93.212,-791.667 48.011,-789.062 49.438,-804.688" fill="#145b64" fill-opacity="0.24"/><polygon points="89.564,-812.500 49.438,-804.688 53.386,-820.312" fill="#115c65" fill-opacity="0.24"/><polygon points="-37.614,-757.812 24.861,-726.562 -24.861,-726.562" fill="#194754" fill-opacity="0.24"/><polygon points="-49.438,-804.688 48.011,-789.062 -48.011,-789.062" fill="#1a4955" fill-opacity="0.24"/><polygon points="-35.835,-835.938 -49.438,-804.688 -53.386,-820.312" fill="#11545f" fill-opacity="0.24"/><polygon points="-37.614,-757.812 37.614,-757.812 24.861,-726.562" fill="#134553" fill-opacity="0.24"/><polygon points="37.614,-757.812 -48.011,-789.062 48.011,-789.062" fill="#1c4754" fill-opacity="0.24"/><polygon points="-49.438,-804.688 49.438,-804.688 48.011,-789.062" fill="#174855" fill-opacity="0.24"/><polygon points="53.386,-820.312 49.438,-804.688 35.835,-835.938" fill="#10545f" fill-opacity="0.24"/><polygon points="35.835,-835.938 -49.438,-804.688 -35.835,-835.938" fill="#164955" fill-opacity="0.24"/><polygon points="-68.935,-195.312 -21.740,-179.688 -68.522,-164.062" fill="#1c5761" fill-opacity="0.24"/><polygon points="-98.875,-187.500 -68.935,-195.312 -68.522,-164.062" fill="#146269" fill-opacity="0.24"/><polygon points="21.740,-179.688 68.935,-195.312 68.522,-164.062" fill="#115761" fill-opacity="0.24"/><polygon points="68.522,-164.062 68.935,-195.312 98.875,-187.500" fill="#1a6269" fill-opacity="0.24"/><polygon points="-98.875,-187.500 -68.522,-164.062 -99.000,-166.667" fill="#1a666c" fill-opacity="0.24"/><polygon points="68.522,-164.062 98.875,-187.500 99.000,-166.667" fill="#11666c" fill-opacity="0.24"/><polygon points="-53.386,-820.312 -89.564,-812.500 -84.266,-833.333" fill="#136068" fill-opacity="0.24"/><polygon points="89.564,-812.500 53.386,-820.312 84.266,-833.333" fill="#1c6068" fill-opacity="0.24"/><polygon points="-68.522,-164.062 -21.740,-179.688 -35.314,-148.438" fill="#17535d" fill-opacity="0.24"/><polygon points="21.740,-179.688 68.522,-164.062 35.314,-148.438" fill="#10535d" fill-opacity="0.24"/><polygon points="-35.835,-835.938 -53.386,-820.312 -52.646,-851.562" fill="#17555f" fill-opacity="0.24"/><polygon points="-52.646,-851.562 -53.386,-820.312 -84.266,-833.333" fill="#1c5b64" fill-opacity="0.24"/><polygon points="53.386,-820.312 35.835,-835.938 52.646,-851.562" fill="#14555f" fill-opacity="0.24"/><polygon points="84.266,-833.333 53.386,-820.312 52.646,-851.562" fill="#115b64" fill-opacity="0.24"/><polygon points="-52.646,-851.562 -84.266,-833.333 -77.419,-854.167" fill="#1a5f67" fill-opacity="0.24"/><polygon points="84.266,-833.333 52.646,-851.562 77.419,-854.167" fill="#1a5f67" fill-opacity="0.24"/><polygon points="-99.000,-166.667 -68.522,-164.062 -99.125,-145.833" fill="#11666c" fill-opacity="0.24"/><polygon points="68.522,-164.062 99.000,-166.667 99.125,-145.833" fill="#14666c" fill-opacity="0.24"/><polygon points="-35.835,-835.938 21.596,-867.188 35.835,-835.938" fill="#1c4552" fill-opacity="0.24"/><polygon points="-21.596,-867.188 -35.835,-835.938 -52.646,-851.562" fill="#17515c" fill-opacity="0.24"/><polygon points="21.596,-867.188 -35.835,-835.938 -21.596,-867.188" fill="#104754" fill-opacity="0.24"/><polygon points="52.646,-851.562 35.835,-835.938 21.596,-867.188" fill="#17515c" fill-opacity="0.24"/><polygon points="-68.522,-164.062 -35.314,-148.438 -64.349,-132.812" fill="#1c5862" fill-opacity="0.24"/><polygon points="-99.125,-145.833 -68.522,-164.062 -64.349,-132.812" fill="#136169" fill-opacity="0.24"/><polygon points="35.314,-148.438 68.522,-164.062 64.349,-132.812" fill="#115862" fill-opacity="0.24"/><polygon points="64.349,-132.812 68.522,-164.062 99.125,-145.833" fill="#1a6169" fill-opacity="0.24"/><polygon points="-52.646,-851.562 -77.419,-854.167 -69.154,-875.000" fill="#1a5d65" fill-opacity="0.24"/><polygon points="77.419,-854.167 52.646,-851.562 69.154,-875.000" fill="#115d65" fill-opacity="0.24"/><polygon points="-64.349,-132.812 -35.314,-148.438 -47.744,-117.188" fill="#145660" fill-opacity="0.24"/><polygon points="35.314,-148.438 64.349,-132.812 47.744,-117.188" fill="#1c5660" fill-opacity="0.24"/><polygon points="-99.125,-145.833 -64.349,-132.812 -99.250,-125.000" fill="#16656c" fill-opacity="0.24"/><polygon points="64.349,-132.812 99.125,-145.833 99.250,-125.000" fill="#10656c" fill-opacity="0.24"/><polygon points="-21.596,-867.188 -52.646,-851.562 -45.948,-882.812" fill="#17525d" fill-opacity="0.24"/><polygon points="-45.948,-882.812 -52.646,-851.562 -69.154,-875.000" fill="#1c5862" fill-opacity="0.24"/><polygon points="52.646,-851.562 21.596,-867.188 45.948,-882.812" fill="#13525d" fill-opacity="0.24"/><polygon points="69.154,-875.000 52.646,-851.562 45.948,-882.812" fill="#115862" fill-opacity="0.24"/><polygon points="-64.349,-132.812 -47.744,-117.188 -56.725,-101.562" fill="#1a5962" fill-opacity="0.24"/><polygon points="47.744,-117.188 64.349,-132.812 56.725,-101.562" fill="#195962" fill-opacity="0.24"/><polygon points="-21.596,-867.188 14.021,-898.438 21.596,-867.188" fill="#114451" fill-opacity="0.24"/><polygon points="-14.021,-898.438 -21.596,-867.188 -45.948,-882.812" fill="#144d59" fill-opacity="0.24"/><polygon points="14.021,-898.438 -21.596,-867.188 -14.021,-898.438" fill="#1c4552" fill-opacity="0.24"/><polygon points="45.948,-882.812 21.596,-867.188 14.021,-898.438" fill="#164d59" fill-opacity="0.24"/><polygon points="-45.948,-882.812 -69.154,-875.000 -59.630,-895.833" fill="#105963" fill-opacity="0.24"/><polygon points="69.154,-875.000 45.948,-882.812 59.630,-895.833" fill="#185963" fill-opacity="0.24"/><polygon points="-99.250,-125.000 -64.349,-132.812 -99.375,-104.167" fill="#1b656c" fill-opacity="0.24"/><polygon points="-99.375,-104.167 -64.349,-132.812 -56.725,-101.562" fill="#135f68" fill-opacity="0.24"/><polygon points="64.349,-132.812 99.250,-125.000 99.375,-104.167" fill="#12656c" fill-opacity="0.24"/><polygon points="56.725,-101.562 64.349,-132.812 99.375,-104.167" fill="#1b5f68" fill-opacity="0.24"/><polygon points="-56.725,-101.562 -46.229,-70.312 -58.069,-85.938" fill="#195761" fill-opacity="0.24"/><polygon points="-99.375,-104.167 -56.725,-101.562 -58.069,-85.938" fill="#105f67" fill-opacity="0.24"/><polygon points="56.725,-101.562 46.229,-70.312 47.744,-117.188" fill="#155660" fill-opacity="0.24"/><polygon points="58.069,-85.938 56.725,-101.562 99.375,-104.167" fill="#1c5f67" fill-opacity="0.24"/><polygon points="-14.021,-898.438 -45.948,-882.812 -34.675,-914.062" fill="#164f5a" fill-opacity="0.24"/><polygon points="-34.675,-914.062 -45.948,-882.812 -59.630,-895.833" fill="#10555f" fill-opacity="0.24"/><polygon points="45.948,-882.812 14.021,-898.438 34.675,-914.062" fill="#184f5a" fill-opacity="0.24"/><polygon points="59.630,-895.833 45.948,-882.812 34.675,-914.062" fill="#1b555f" fill-opacity="0.24"/><polygon points="-34.675,-914.062 -59.630,-895.833 -49.024,-916.667" fill="#12555f" fill-opacity="0.24"/><polygon points="59.630,-895.833 34.675,-914.062 49.024,-916.667" fill="#12555f" fill-opacity="0.24"/><polygon points="-99.375,-104.167 -58.069,-85.938 -99.500,-83.333" fill="#1b646b" fill-opacity="0.24"/><polygon points="58.069,-85.938 99.375,-104.167 99.500,-83.333" fill="#19646b" fill-opacity="0.24"/><polygon points="-47.744,-117.188 46.229,-70.312 -46.229,-70.312" fill="#104855" fill-opacity="0.24"/><polygon points="-46.229,-70.312 -56.725,-101.562 -47.744,-117.188" fill="#155660" fill-opacity="0.24"/><polygon points="47.744,-117.188 -47.744,-117.188 35.314,-148.438" fill="#1c4754" fill-opacity="0.24"/><polygon points="46.229,-70.312 56.725,-101.562 58.069,-85.938" fill="#155761" fill-opacity="0.24"/><polygon points="-14.021,-898.438 -34.675,-914.062 -15.754,-929.688" fill="#104b57" fill-opacity="0.24"/><polygon points="14.021,-898.438 -15.754,-929.688 15.754,-929.688" fill="#184451" fill-opacity="0.24"/><polygon points="34.675,-914.062 14.021,-898.438 15.754,-929.688" fill="#1b4b57" fill-opacity="0.24"/><polygon points="-15.754,-929.688 14.021,-898.438 -14.021,-898.438" fill="#124452" fill-opacity="0.24"/><polygon points="-34.675,-914.062 -49.024,-916.667 -37.537,-937.500" fill="#12525d" fill-opacity="0.24"/><polygon points="-15.754,-929.688 -34.675,-914.062 -37.537,-937.500" fill="#1b4e59" fill-opacity="0.24"/><polygon points="49.024,-916.667 34.675,-914.062 37.537,-937.500" fill="#18525d" fill-opacity="0.24"/><polygon points="37.537,-937.500 34.675,-914.062 15.754,-929.688" fill="#104e59" fill-opacity="0.24"/><polygon points="-99.500,-83.333 -65.487,-54.688 -99.625,-62.500" fill="#15656c" fill-opacity="0.24"/><polygon points="99.500,-83.333 65.487,-54.688 58.069,-85.938" fill="#1c6068" fill-opacity="0.24"/><polygon points="-65.487,-54.688 -99.500,-83.333 -58.069,-85.938" fill="#156068" fill-opacity="0.24"/><polygon points="-65.487,-54.688 -58.069,-85.938 -46.229,-70.312" fill="#105962" fill-opacity="0.24"/><polygon points="65.487,-54.688 99.500,-83.333 99.625,-62.500" fill="#19656c" fill-opacity="0.24"/><polygon points="46.229,-70.312 58.069,-85.938 65.487,-54.688" fill="#1b5962" fill-opacity="0.24"/><polygon points="-15.754,-929.688 -37.537,-937.500 -21.124,-945.312" fill="#124c58" fill-opacity="0.24"/><polygon points="37.537,-937.500 15.754,-929.688 21.124,-945.312" fill="#124c58" fill-opacity="0.24"/><polygon points="35.314,-148.438 -47.744,-117.188 -35.314,-148.438" fill="#1b4855" fill-opacity="0.24"/><polygon points="-46.229,-70.312 33.662,-39.062 -33.662,-39.062" fill="#184855" fill-opacity="0.24"/><polygon points="-65.487,-54.688 -46.229,-70.312 -33.662,-39.062" fill="#105560" fill-opacity="0.24"/><polygon points="-35.314,-148.438 21.740,-179.688 35.314,-148.438" fill="#164552" fill-opacity="0.24"/><polygon points="-46.229,-70.312 46.229,-70.312 33.662,-39.062" fill="#1c4654" fill-opacity="0.24"/><polygon points="21.740,-179.688 -35.314,-148.438 -21.740,-179.688" fill="#154754" fill-opacity="0.24"/><polygon points="46.229,-70.312 -47.744,-117.188 47.744,-117.188" fill="#104855" fill-opacity="0.24"/><polygon points="33.662,-39.062 46.229,-70.312 65.487,-54.688" fill="#195560" fill-opacity="0.24"/><polygon points="-21.124,-945.312 -37.537,-937.500 -25.386,-958.333" fill="#1b4d59" fill-opacity="0.24"/><polygon points="37.537,-937.500 21.124,-945.312 25.386,-958.333" fill="#124d59" fill-opacity="0.24"/><polygon points="-15.754,-929.688 11.857,-960.938 15.754,-929.688" fill="#134451" fill-opacity="0.24"/><polygon points="15.754,-929.688 11.857,-960.938 21.124,-945.312" fill="#1b4855" fill-opacity="0.24"/><polygon points="21.124,-945.312 11.857,-960.938 25.386,-958.333" fill="#184a56" fill-opacity="0.24"/><polygon points="-11.857,-960.938 -15.754,-929.688 -21.124,-945.312" fill="#104855" fill-opacity="0.24"/><polygon points="-11.857,-960.938 -21.124,-945.312 -25.386,-958.333" fill="#164a56" fill-opacity="0.24"/><polygon points="11.857,-960.938 -15.754,-929.688 -11.857,-960.938" fill="#1c4452" fill-opacity="0.24"/><polygon points="-99.625,-62.500 -65.487,-54.688 -99.750,-41.667" fill="#14656c" fill-opacity="0.24"/><polygon points="65.487,-54.688 99.625,-62.500 99.750,-41.667" fill="#11656c" fill-opacity="0.24"/><polygon points="-11.857,-960.938 8.007,-976.562 11.857,-960.938" fill="#194351" fill-opacity="0.24"/><polygon points="8.007,-976.562 -11.857,-960.938 -8.007,-976.562" fill="#1a4451" fill-opacity="0.24"/><polygon points="-99.750,-41.667 -65.487,-54.688 -69.419,-23.438" fill="#116169" fill-opacity="0.24"/><polygon points="-69.419,-23.438 -65.487,-54.688 -33.662,-39.062" fill="#135862" fill-opacity="0.24"/><polygon points="33.662,-39.062 65.487,-54.688 69.419,-23.438" fill="#1c5862" fill-opacity="0.24"/><polygon points="69.419,-23.438 65.487,-54.688 99.750,-41.667" fill="#176169" fill-opacity="0.24"/><polygon points="-11.857,-960.938 -25.386,-958.333 -12.795,-979.167" fill="#104955" fill-opacity="0.24"/><polygon points="-8.007,-976.562 -11.857,-960.938 -12.795,-979.167" fill="#164653" fill-opacity="0.24"/><polygon points="11.857,-960.938 8.007,-976.562 12.795,-979.167" fill="#1c4653" fill-opacity="0.24"/><polygon points="-99.750,-41.667 -69.419,-23.438 -99.875,-20.833" fill="#14666d" fill-opacity="0.24"/><polygon points="69.419,-23.438 99.750,-41.667 99.875,-20.833" fill="#11666d" fill-opacity="0.24"/><polygon points="-33.662,-39.062 19.990,-7.812 -19.990,-7.812" fill="#1a4654" fill-opacity="0.24"/><polygon points="19.990,-7.812 -33.662,-39.062 33.662,-39.062" fill="#1a4552" fill-opacity="0.24"/><polygon points="-8.007,-976.562 2.867,-992.188 8.007,-976.562" fill="#114250" fill-opacity="0.24"/><polygon points="-2.867,-992.188 -8.007,-976.562 -12.795,-979.167" fill="#134553" fill-opacity="0.24"/><polygon points="2.867,-992.188 -8.007,-976.562 -2.867,-992.188" fill="#1c4351" fill-opacity="0.24"/><polygon points="12.795,-979.167 8.007,-976.562 2.867,-992.188" fill="#174553" fill-opacity="0.24"/><polygon points="2.867,-992.188 -2.867,-992.188 0.000,-1000.000" fill="#104250" fill-opacity="0.24"/><polygon points="-99.875,-20.833 -69.419,-23.438 -100.000,0.000" fill="#17666d" fill-opacity="0.24"/><polygon points="69.419,-23.438 99.875,-20.833 100.000,0.000" fill="#1c666d" fill-opacity="0.24"/><polygon points="25.386,-958.333 11.857,-960.938 12.795,-979.167" fill="#144955" fill-opacity="0.24"/><polygon points="12.795,-979.167 2.867,-992.188 0.000,-1000.000" fill="#114452" fill-opacity="0.24"/><polygon points="0.000,-1000.000 -2.867,-992.188 -12.795,-979.167" fill="#1a4452" fill-opacity="0.24"/><polygon points="-69.419,-23.438 -19.990,-7.812 -100.000,0.000" fill="#1a5b64" fill-opacity="0.24"/><polygon points="-19.990,-7.812 -69.419,-23.438 -33.662,-39.062" fill="#11525d" fill-opacity="0.24"/><polygon points="-100.000,0.000 -19.990,-7.812 19.990,-7.812" fill="#144f5b" fill-opacity="0.24"/><polygon points="19.990,-7.812 33.662,-39.062 69.419,-23.438" fill="#1c525d" fill-opacity="0.24"/><polygon points="19.990,-7.812 69.419,-23.438 100.000,0.000" fill="#175b64" fill-opacity="0.24"/><polygon points="-100.000,0.000 19.990,-7.812 100.000,0.000" fill="#104552" fill-opacity="0.24"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 -11.643,-979.167 -23.101,-958.333 -34.159,-937.500 -44.612,-916.667 -54.263,-895.833 -62.931,-875.000 -70.451,-854.167 -76.682,-833.333 -81.504,-812.500 -84.822,-791.667 -86.573,-770.833 -86.905,-750.000 -87.019,-729.167 -87.133,-708.333 -87.246,-687.500 -87.360,-666.667 -87.474,-645.833 -87.588,-625.000 -87.701,-604.167 -87.815,-583.333 -87.929,-562.500 -88.043,-541.667 -88.156,-520.833 -88.270,-500.000 -88.384,-479.167 -88.497,-458.333 -88.611,-437.500 -88.725,-416.667 -88.839,-395.833 -88.952,-375.000 -89.066,-354.167 -89.180,-333.333 -89.294,-312.500 -89.407,-291.667 -89.521,-270.833 -89.635,-250.000 -89.749,-229.167 -89.862,-208.333 -89.977,-187.500 -90.090,-166.667 -90.204,-145.833 -90.317,-125.000 -90.432,-104.167 -90.545,-83.333 -90.659,-62.500 -90.772,-41.667 -90.887,-20.833 -91.000,0.000" fill="url(#edge)" fill-opacity="0.75"/><polygon points="100.000,0.000 99.875,-20.833 99.750,-41.667 99.625,-62.500 99.500,-83.333 99.375,-104.167 99.250,-125.000 99.125,-145.833 99.000,-166.667 98.875,-187.500 98.750,-208.333 98.625,-229.167 98.500,-250.000 98.375,-270.833 98.250,-291.667 98.125,-312.500 98.000,-333.333 97.875,-354.167 97.750,-375.000 97.625,-395.833 97.500,-416.667 97.375,-437.500 97.250,-458.333 97.125,-479.167 97.000,-500.000 96.875,-520.833 96.750,-541.667 96.625,-562.500 96.500,-583.333 96.375,-604.167 96.250,-625.000 96.125,-645.833 96.000,-666.667 95.875,-687.500 95.750,-708.333 95.625,-729.167 95.500,-750.000 95.136,-770.833 93.212,-791.667 89.564,-812.500 84.266,-833.333 77.419,-854.167 69.154,-875.000 59.630,-895.833 49.024,-916.667 37.537,-937.500 25.386,-958.333 12.795,-979.167 0.000,-1000.000 11.643,-979.167 23.101,-958.333 34.159,-937.500 44.612,-916.667 54.263,-895.833 62.931,-875.000 70.451,-854.167 76.682,-833.333 81.504,-812.500 84.822,-791.667 86.573,-770.833 86.905,-750.000 87.019,-729.167 87.133,-708.333 87.246,-687.500 87.360,-666.667 87.474,-645.833 87.588,-625.000 87.701,-604.167 87.815,-583.333 87.929,-562.500 88.043,-541.667 88.156,-520.833 88.270,-500.000 88.384,-479.167 88.497,-458.333 88.611,-437.500 88.725,-416.667 88.839,-395.833 88.952,-375.000 89.066,-354.167 89.180,-333.333 89.294,-312.500 89.407,-291.667 89.521,-270.833 89.635,-250.000 89.749,-229.167 89.862,-208.333 89.977,-187.500 90.090,-166.667 90.204,-145.833 90.317,-125.000 90.432,-104.167 90.545,-83.333 90.659,-62.500 90.772,-41.667 90.887,-20.833 91.000,0.000" fill="url(#edge)" fill-opacity="0.75"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#43eadf" stroke-width="7" stroke-opacity="0.35" stroke-linejoin="round"/><polygon points="-100.000,0.000 -99.875,-20.833 -99.750,-41.667 -99.625,-62.500 -99.500,-83.333 -99.375,-104.167 -99.250,-125.000 -99.125,-145.833 -99.000,-166.667 -98.875,-187.500 -98.750,-208.333 -98.625,-229.167 -98.500,-250.000 -98.375,-270.833 -98.250,-291.667 -98.125,-312.500 -98.000,-333.333 -97.875,-354.167 -97.750,-375.000 -97.625,-395.833 -97.500,-416.667 -97.375,-437.500 -97.250,-458.333 -97.125,-479.167 -97.000,-500.000 -96.875,-520.833 -96.750,-541.667 -96.625,-562.500 -96.500,-583.333 -96.375,-604.167 -96.250,-625.000 -96.125,-645.833 -96.000,-666.667 -95.875,-687.500 -95.750,-708.333 -95.625,-729.167 -95.500,-750.000 -95.136,-770.833 -93.212,-791.667 -89.564,-812.500 -84.266,-833.333 -77.419,-854.167 -69.154,-875.000 -59.630,-895.833 -49.024,-916.667 -37.537,-937.500 -25.386,-958.333 -12.795,-979.167 0.000,-1000.000 12.795,-979.167 25.386,-958.333 37.537,-937.500 49.024,-916.667 59.630,-895.833 69.154,-875.000 77.419,-854.167 84.266,-833.333 89.564,-812.500 93.212,-791.667 95.136,-770.833 95.500,-750.000 95.625,-729.167 95.750,-708.333 95.875,-687.500 96.000,-666.667 96.125,-645.833 96.250,-625.000 96.375,-604.167 96.500,-583.333 96.625,-562.500 96.750,-541.667 96.875,-520.833 97.000,-500.000 97.125,-479.167 97.250,-458.333 97.375,-437.500 97.500,-416.667 97.625,-395.833 97.750,-375.000 97.875,-354.167 98.000,-333.333 98.125,-312.500 98.250,-291.667 98.375,-270.833 98.500,-250.000 98.625,-229.167 98.750,-208.333 98.875,-187.500 99.000,-166.667 99.125,-145.833 99.250,-125.000 99.375,-104.167 99.500,-83.333 99.625,-62.500 99.750,-41.667 99.875,-20.833 100.000,0.000" fill="none" stroke="#b4fff4" stroke-width="2.5" stroke-linejoin="round"/><g fill="none" stroke="#49d8d2" stroke-linecap="round"><polyline points="24.026,-160.000 25.286,-167.188 26.458,-174.375 27.528,-181.563 28.484,-188.750 29.316,-195.938 30.013,-203.125 30.565,-210.312 30.963,-217.500 31.200,-224.687 31.270,-231.875 31.166,-239.062 30.884,-246.250 30.420,-253.437 29.774,-260.625 28.944,-267.812 27.930,-275.000 26.734,-282.188 25.360,-289.375 23.811,-296.562 22.093,-303.750 20.212,-310.938 18.176,-318.125 15.995,-325.312 13.677,-332.500 11.235,-339.688 8.679,-346.875 6.022,-354.062 3.278,-361.250 0.461,-368.438 -2.414,-375.625 -5.333,-382.812 -8.279,-390.000 -11.237,-397.188 -14.191,-404.375 -17.125,-411.562 -20.022,-418.750 -22.867,-425.937 -25.643,-433.125 -28.336,-440.312 -30.930,-447.500 -33.412,-454.688 -35.766,-461.875 -37.981,-469.062 -40.044,-476.250 -41.944,-483.437 -43.670,-490.625 -45.213,-497.812 -46.564,-505.000 -47.717,-512.187 -48.666,-519.375 -49.407,-526.562 -49.934,-533.750 -50.248,-540.938 -50.347,-548.125 -50.230,-555.312 -49.902,-562.500 -49.363,-569.688 -48.619,-576.875 -47.675,-584.062 -46.538,-591.250 -45.217,-598.438 -43.719,-605.625 -42.055,-612.812 -40.236,-620.000 -38.273,-627.187 -36.181,-634.375 -33.971,-641.562 -31.657,-648.750 -29.256,-655.938 -26.780,-663.125 -24.246,-670.312 -21.668,-677.500 -19.064,-684.688 -16.447,-691.875 -13.834,-699.062 -11.240,-706.250 -8.680,-713.438 -6.168,-720.625 -3.719,-727.812 -1.346,-735.000 0.938,-742.188 3.122,-749.375 5.193,-756.562 7.142,-763.750 8.959,-770.938 10.636,-778.125 12.166,-785.312 13.544,-792.500 14.763,-799.688 15.820,-806.875 16.714,-814.062 17.442,-821.250 18.006,-828.438 18.405,-835.625 18.643,-842.812 18.723,-850.000" stroke-width="2.000" stroke-opacity="0.3"/><polyline points="-2.902,-160.000 -4.566,-167.188 -6.329,-174.375 -8.183,-181.563 -10.114,-188.750 -12.112,-195.938 -14.163,-203.125 -16.254,-210.312 -18.371,-217.500 -20.499,-224.687 -22.625,-231.875 -24.733,-239.062 -26.808,-246.250 -28.836,-253.437 -30.802,-260.625 -32.691,-267.812 -34.489,-275.000 -36.183,-282.188 -37.760,-289.375 -39.206,-296.562 -40.510,-303.750 -41.660,-310.938 -42.648,-318.125 -43.462,-325.312 -44.096,-332.500 -44.542,-339.688 -44.794,-346.875 -44.847,-354.062 -44.698,-361.250 -44.344,-368.438 -43.785,-375.625 -43.020,-382.812 -42.051,-390.000 -40.880,-397.188 -39.513,-404.375 -37.953,-411.562 -36.208,-418.750 -34.286,-425.937 -32.195,-433.125 -29.944,-440.312 -27.546,-447.500 -25.012,-454.688 -22.354,-461.875 -19.587,-469.062 -16.723,-476.250 -13.779,-483.437 -10.770,-490.625 -7.711,-497.812 -4.618,-505.000 -1.508,-512.187 1.603,-519.375 4.699,-526.562 7.764,-533.750 10.780,-540.938 13.733,-548.125 16.608,-555.312 19.390,-562.500 22.063,-569.688 24.616,-576.875 27.036,-584.062 29.310,-591.250 31.428,-598.438 33.380,-605.625 35.157,-612.812 36.752,-620.000 38.159,-627.187 39.372,-634.375 40.388,-641.562 41.203,-648.750 41.816,-655.938 42.228,-663.125 42.439,-670.312 42.452,-677.500 42.270,-684.688 41.899,-691.875 41.345,-699.062 40.614,-706.250 39.715,-713.438 38.658,-720.625 37.452,-727.812 36.108,-735.000 34.638,-742.188 33.056,-749.375 31.373,-756.562 29.604,-763.750 27.762,-770.938 25.862,-778.125 23.919,-785.312 21.947,-792.500 19.960,-799.688 17.974,-806.875 16.002,-814.062 14.059,-821.250 12.159,-828.438 10.313,-835.625 8.535,-842.812 6.837,-850.000" stroke-width="0.800" stroke-opacity="0.3"/><polyline points="-22.149,-160.000 -22.334,-167.188 -22.365,-174.375 -22.237,-181.563 -21.945,-188.750 -21.485,-195.938 -20.855,-203.125 -20.055,-210.312 -19.085,-217.500 -17.946,-224.687 -16.641,-231.875 -15.174,-239.062 -13.550,-246.250 -11.776,-253.437 -9.858,-260.625 -7.806,-267.812 -5.630,-275.000 -3.339,-282.188 -0.945,-289.375 1.539,-296.562 4.100,-303.750 6.725,-310.938 9.399,-318.125 12.107,-325.312 14.834,-332.500 17.565,-339.688 20.284,-346.875 22.975,-354.062 25.623,-361.250 28.211,-368.438 30.725,-375.625 33.149,-382.812 35.468,-390.000 37.670,-397.188 39.739,-404.375 41.665,-411.562 43.434,-418.750 45.035,-425.937 46.459,-433.125 47.697,-440.312 48.741,-447.500 49.584,-454.688 50.220,-461.875 50.646,-469.062 50.857,-476.250 50.853,-483.437 50.633,-490.625 50.198,-497.812 49.550,-505.000 48.692,-512.187 47.630,-519.375 46.368,-526.562 44.915,-533.750 43.278,-540.938 41.467,-548.125 39.492,-555.312 37.365,-562.500 35.097,-569.688 32.703,-576.875 30.194,-584.062 27.587,-591.250 24.896,-598.438 22.136,-605.625 19.323,-612.812 16.472,-620.000 13.601,-627.187 10.723,-634.375 7.857,-641.562 5.017,-648.750 2.218,-655.938 -0.524,-663.125 -3.195,-670.312 -5.780,-677.500 -8.268,-684.688 -10.644,-691.875 -12.899,-699.062 -15.020,-706.250 -16.999,-713.438 -18.827,-720.625 -20.497,-727.812 -22.001,-735.000 -23.335,-742.188 -24.495,-749.375 -25.478,-756.562 -26.283,-763.750 -26.909,-770.938 -27.358,-778.125 -27.632,-785.312 -27.734,-792.500 -27.669,-799.688 -27.442,-806.875 -27.061,-814.062 -26.533,-821.250 -25.867,-828.438 -25.073,-835.625 -24.161,-842.812 -23.143,-850.000" stroke-width="0.800" stroke-opacity="0.3"/><path d="M 22 -715 A 54 65 0 1 0 22 -596 A 43 63 0 0 1 22 -715 Z" fill="#8defe0" fill-opacity="0.07" stroke-opacity="0.33" stroke-width="1"/><path d="M 0 -927 L 8 -906 L 0 -883 L -8 -906 Z M 0 -941 V -930 M 0 -880 V -864" stroke-opacity="0.58" stroke-width="1.2"/></g><circle cx="0.000" cy="-160.000" r="12.000" fill="url(#mote)"/><circle cx="0.000" cy="-160.000" r="3.000" fill="#b0fff0"/><circle cx="0.000" cy="-160.000" r="1.200" fill="#f0fff8"/><circle cx="40.572" cy="-189.565" r="6.000" fill="url(#mote)"/><circle cx="40.572" cy="-189.565" r="1.500" fill="#b0fff0"/><circle cx="40.572" cy="-189.565" r="0.600" fill="#f0fff8"/><circle cx="-59.780" cy="-219.130" r="6.000" fill="url(#mote)"/><circle cx="-59.780" cy="-219.130" r="1.500" fill="#b0fff0"/><circle cx="-59.780" cy="-219.130" r="0.600" fill="#f0fff8"/><circle cx="47.510" cy="-248.696" r="6.000" fill="url(#mote)"/><circle cx="47.510" cy="-248.696" r="1.500" fill="#b0fff0"/><circle cx="47.510" cy="-248.696" r="0.600" fill="#f0fff8"/><circle cx="-10.223" cy="-278.261" r="12.000" fill="url(#mote)"/><circle cx="-10.223" cy="-278.261" r="3.000" fill="#b0fff0"/><circle cx="-10.223" cy="-278.261" r="1.200" fill="#f0fff8"/><circle cx="-32.447" cy="-307.826" r="6.000" fill="url(#mote)"/><circle cx="-32.447" cy="-307.826" r="1.500" fill="#b0fff0"/><circle cx="-32.447" cy="-307.826" r="0.600" fill="#f0fff8"/><circle cx="58.032" cy="-337.391" r="6.000" fill="url(#mote)"/><circle cx="58.032" cy="-337.391" r="1.500" fill="#b0fff0"/><circle cx="58.032" cy="-337.391" r="0.600" fill="#f0fff8"/><circle cx="-53.059" cy="-366.957" r="6.000" fill="url(#mote)"/><circle cx="-53.059" cy="-366.957" r="1.500" fill="#b0fff0"/><circle cx="-53.059" cy="-366.957" r="0.600" fill="#f0fff8"/><circle cx="20.147" cy="-396.522" r="12.000" fill="url(#mote)"/><circle cx="20.147" cy="-396.522" r="3.000" fill="#b0fff0"/><circle cx="20.147" cy="-396.522" r="1.200" fill="#f0fff8"/><circle cx="23.373" cy="-426.087" r="6.000" fill="url(#mote)"/><circle cx="23.373" cy="-426.087" r="1.500" fill="#b0fff0"/><circle cx="23.373" cy="-426.087" r="0.600" fill="#f0fff8"/><circle cx="-54.586" cy="-455.652" r="6.000" fill="url(#mote)"/><circle cx="-54.586" cy="-455.652" r="1.500" fill="#b0fff0"/><circle cx="-54.586" cy="-455.652" r="0.600" fill="#f0fff8"/><circle cx="57.056" cy="-485.217" r="6.000" fill="url(#mote)"/><circle cx="57.056" cy="-485.217" r="1.500" fill="#b0fff0"/><circle cx="57.056" cy="-485.217" r="0.600" fill="#f0fff8"/><circle cx="-29.482" cy="-514.783" r="12.000" fill="url(#mote)"/><circle cx="-29.482" cy="-514.783" r="3.000" fill="#b0fff0"/><circle cx="-29.482" cy="-514.783" r="1.200" fill="#f0fff8"/><circle cx="-13.616" cy="-544.348" r="6.000" fill="url(#mote)"/><circle cx="-13.616" cy="-544.348" r="1.500" fill="#b0fff0"/><circle cx="-13.616" cy="-544.348" r="0.600" fill="#f0fff8"/><circle cx="49.545" cy="-573.913" r="6.000" fill="url(#mote)"/><circle cx="49.545" cy="-573.913" r="1.500" fill="#b0fff0"/><circle cx="49.545" cy="-573.913" r="0.600" fill="#f0fff8"/><circle cx="-59.385" cy="-603.478" r="6.000" fill="url(#mote)"/><circle cx="-59.385" cy="-603.478" r="1.500" fill="#b0fff0"/><circle cx="-59.385" cy="-603.478" r="0.600" fill="#f0fff8"/><circle cx="37.955" cy="-633.043" r="12.000" fill="url(#mote)"/><circle cx="37.955" cy="-633.043" r="3.000" fill="#b0fff0"/><circle cx="37.955" cy="-633.043" r="1.200" fill="#f0fff8"/><circle cx="3.460" cy="-662.609" r="6.000" fill="url(#mote)"/><circle cx="3.460" cy="-662.609" r="1.500" fill="#b0fff0"/><circle cx="3.460" cy="-662.609" r="0.600" fill="#f0fff8"/><circle cx="-43.054" cy="-692.174" r="6.000" fill="url(#mote)"/><circle cx="-43.054" cy="-692.174" r="1.500" fill="#b0fff0"/><circle cx="-43.054" cy="-692.174" r="0.600" fill="#f0fff8"/><circle cx="59.977" cy="-721.739" r="6.000" fill="url(#mote)"/><circle cx="59.977" cy="-721.739" r="1.500" fill="#b0fff0"/><circle cx="59.977" cy="-721.739" r="0.600" fill="#f0fff8"/><circle cx="-45.318" cy="-751.304" r="12.000" fill="url(#mote)"/><circle cx="-45.318" cy="-751.304" r="3.000" fill="#b0fff0"/><circle cx="-45.318" cy="-751.304" r="1.200" fill="#f0fff8"/><circle cx="6.796" cy="-780.870" r="6.000" fill="url(#mote)"/><circle cx="6.796" cy="-780.870" r="1.500" fill="#b0fff0"/><circle cx="6.796" cy="-780.870" r="0.600" fill="#f0fff8"/><circle cx="35.304" cy="-810.435" r="6.000" fill="url(#mote)"/><circle cx="35.304" cy="-810.435" r="1.500" fill="#b0fff0"/><circle cx="35.304" cy="-810.435" r="0.600" fill="#f0fff8"/><g fill="none" stroke="#8af4e5" stroke-width="0.9" stroke-opacity="0.36"><path d="M -54 -309 L -17 -244 L 47 -356 M -54 -309 L 13 -417 L 47 -356"/></g><polygon points="0.000,-138.000 18.000,-115.000 27.000,-39.000 81.000,-49.000 156.000,-105.000 174.000,-98.000 187.000,-80.000 173.000,-63.000 111.000,-13.000 183.000,-20.000 196.000,-7.000 196.000,18.000 182.000,29.000 110.000,24.000 171.000,77.000 170.000,91.000 152.000,108.000 136.000,103.000 64.000,53.000 25.000,57.000 17.000,76.000 0.000,76.000 -17.000,76.000 -25.000,57.000 -64.000,53.000 -136.000,103.000 -152.000,108.000 -170.000,91.000 -171.000,77.000 -110.000,24.000 -182.000,29.000 -196.000,18.000 -196.000,-7.000 -183.000,-20.000 -111.000,-13.000 -173.000,-63.000 -187.000,-80.000 -174.000,-98.000 -156.000,-105.000 -81.000,-49.000 -27.000,-39.000 -18.000,-115.000" fill="url(#bronze)" stroke="#241810" stroke-width="9" stroke-linejoin="round"/><polygon points="0.000,0.000 -27.000,-39.000 27.000,-39.000" fill="#b68557" fill-opacity="0.17"/><polygon points="0.000,0.000 -65.000,0.000 -27.000,-39.000" fill="#e1ac75" fill-opacity="0.17"/><polygon points="0.000,0.000 65.000,0.000 25.000,57.000" fill="#aa7a4e" fill-opacity="0.17"/><polygon points="-25.000,57.000 0.000,0.000 25.000,57.000" fill="#90623c" fill-opacity="0.17"/><polygon points="-65.000,0.000 0.000,0.000 -25.000,57.000" fill="#ce9b68" fill-opacity="0.17"/><polygon points="65.000,0.000 0.000,0.000 27.000,-39.000" fill="#d6a26d" fill-opacity="0.17"/><polygon points="27.000,-39.000 -27.000,-39.000 0.000,-75.000" fill="#956740" fill-opacity="0.17"/><polygon points="-25.000,57.000 25.000,57.000 0.000,76.000" fill="#a17148" fill-opacity="0.17"/><polygon points="-25.000,57.000 0.000,76.000 -17.000,76.000" fill="#dea973" fill-opacity="0.17"/><polygon points="0.000,76.000 25.000,57.000 17.000,76.000" fill="#c08e5e" fill-opacity="0.17"/><polygon points="-65.000,0.000 -25.000,57.000 -64.000,53.000" fill="#8c5e39" fill-opacity="0.17"/><polygon points="25.000,57.000 65.000,0.000 64.000,53.000" fill="#b98759" fill-opacity="0.17"/><polygon points="-27.000,-39.000 -65.000,0.000 -81.000,-49.000" fill="#e0ab74" fill-opacity="0.17"/><polygon points="65.000,0.000 27.000,-39.000 81.000,-49.000" fill="#a8774d" fill-opacity="0.17"/><polygon points="-81.000,-49.000 -65.000,0.000 -111.000,-13.000" fill="#91633d" fill-opacity="0.17"/><polygon points="65.000,0.000 81.000,-49.000 111.000,-13.000" fill="#d09d69" fill-opacity="0.17"/><polygon points="-65.000,0.000 -64.000,53.000 -110.000,24.000" fill="#d4a06c" fill-opacity="0.17"/><polygon points="-111.000,-13.000 -65.000,0.000 -110.000,24.000" fill="#94653f" fill-opacity="0.17"/><polygon points="64.000,53.000 65.000,0.000 110.000,24.000" fill="#a37349" fill-opacity="0.17"/><polygon points="110.000,24.000 65.000,0.000 111.000,-13.000" fill="#dfaa73" fill-opacity="0.17"/><polygon points="0.000,-75.000 -27.000,-39.000 -18.000,-115.000" fill="#be8c5c" fill-opacity="0.17"/><polygon points="27.000,-39.000 0.000,-75.000 18.000,-115.000" fill="#8c5e39" fill-opacity="0.17"/><polygon points="18.000,-115.000 0.000,-75.000 -18.000,-115.000" fill="#bc8a5b" fill-opacity="0.17"/><polygon points="-81.000,-49.000 -111.000,-13.000 -123.000,-60.000" fill="#dfab74" fill-opacity="0.17"/><polygon points="111.000,-13.000 81.000,-49.000 123.000,-60.000" fill="#a5754b" fill-opacity="0.17"/><polygon points="18.000,-115.000 -18.000,-115.000 0.000,-138.000" fill="#93643e" fill-opacity="0.17"/><polygon points="-110.000,24.000 -64.000,53.000 -120.000,68.000" fill="#d29e6b" fill-opacity="0.17"/><polygon points="64.000,53.000 110.000,24.000 120.000,68.000" fill="#d29e6b" fill-opacity="0.17"/><polygon points="-120.000,68.000 -64.000,53.000 -136.000,103.000" fill="#93643e" fill-opacity="0.17"/><polygon points="64.000,53.000 120.000,68.000 136.000,103.000" fill="#a5754b" fill-opacity="0.17"/><polygon points="-111.000,-13.000 -173.000,-63.000 -123.000,-60.000" fill="#e0ab74" fill-opacity="0.17"/><polygon points="173.000,-63.000 111.000,-13.000 123.000,-60.000" fill="#bc8a5b" fill-opacity="0.17"/><polygon points="-111.000,-13.000 -110.000,24.000 -182.000,29.000" fill="#8c5e39" fill-opacity="0.17"/><polygon points="-183.000,-20.000 -111.000,-13.000 -182.000,29.000" fill="#be8c5c" fill-opacity="0.17"/><polygon points="110.000,24.000 111.000,-13.000 182.000,29.000" fill="#dfaa73" fill-opacity="0.17"/><polygon points="110.000,24.000 171.000,77.000 120.000,68.000" fill="#a37349" fill-opacity="0.17"/><polygon points="182.000,29.000 111.000,-13.000 183.000,-20.000" fill="#94653f" fill-opacity="0.17"/><polygon points="-136.000,103.000 -171.000,77.000 -120.000,68.000" fill="#d4a06c" fill-opacity="0.17"/><polygon points="-171.000,77.000 -110.000,24.000 -120.000,68.000" fill="#d09d69" fill-opacity="0.17"/><polygon points="136.000,103.000 170.000,91.000 152.000,108.000" fill="#91633d" fill-opacity="0.17"/><polygon points="171.000,77.000 136.000,103.000 120.000,68.000" fill="#a8774d" fill-opacity="0.17"/><polygon points="-156.000,-105.000 -81.000,-49.000 -123.000,-60.000" fill="#e0ab74" fill-opacity="0.17"/><polygon points="-156.000,-105.000 -123.000,-60.000 -173.000,-63.000" fill="#b98759" fill-opacity="0.17"/><polygon points="81.000,-49.000 156.000,-105.000 123.000,-60.000" fill="#8c5e39" fill-opacity="0.17"/><polygon points="173.000,-63.000 123.000,-60.000 156.000,-105.000" fill="#c18e5e" fill-opacity="0.17"/><polygon points="-136.000,103.000 -170.000,91.000 -171.000,77.000" fill="#dea973" fill-opacity="0.17"/><polygon points="-170.000,91.000 -136.000,103.000 -152.000,108.000" fill="#a17148" fill-opacity="0.17"/><polygon points="170.000,91.000 136.000,103.000 171.000,77.000" fill="#966740" fill-opacity="0.17"/><polygon points="-183.000,-20.000 -182.000,29.000 -196.000,-7.000" fill="#d6a26d" fill-opacity="0.17"/><polygon points="182.000,29.000 183.000,-20.000 196.000,-7.000" fill="#ce9b68" fill-opacity="0.17"/><polygon points="-196.000,-7.000 -182.000,29.000 -196.000,18.000" fill="#90623c" fill-opacity="0.17"/><polygon points="182.000,29.000 196.000,-7.000 196.000,18.000" fill="#aa7a4e" fill-opacity="0.17"/><polygon points="-156.000,-105.000 -173.000,-63.000 -174.000,-98.000" fill="#e1ac75" fill-opacity="0.17"/><polygon points="173.000,-63.000 156.000,-105.000 174.000,-98.000" fill="#b68557" fill-opacity="0.17"/><polygon points="-174.000,-98.000 -173.000,-63.000 -187.000,-80.000" fill="#8c5e39" fill-opacity="0.17"/><polygon points="173.000,-63.000 174.000,-98.000 187.000,-80.000" fill="#c39060" fill-opacity="0.17"/><polygon points="0.000,-138.000 18.000,-115.000 27.000,-39.000 81.000,-49.000 156.000,-105.000 174.000,-98.000 187.000,-80.000 173.000,-63.000 111.000,-13.000 183.000,-20.000 196.000,-7.000 196.000,18.000 182.000,29.000 110.000,24.000 171.000,77.000 170.000,91.000 152.000,108.000 136.000,103.000 64.000,53.000 25.000,57.000 17.000,76.000 0.000,76.000 -17.000,76.000 -25.000,57.000 -64.000,53.000 -136.000,103.000 -152.000,108.000 -170.000,91.000 -171.000,77.000 -110.000,24.000 -182.000,29.000 -196.000,18.000 -196.000,-7.000 -183.000,-20.000 -111.000,-13.000 -173.000,-63.000 -187.000,-80.000 -174.000,-98.000 -156.000,-105.000 -81.000,-49.000 -27.000,-39.000 -18.000,-115.000" fill="none" stroke="#ebc493" stroke-width="2.5" stroke-linejoin="round"/><polygon points="0.000,-121.440 16.920,-101.200 25.380,-34.320 76.140,-43.120 146.640,-92.400 163.560,-86.240 175.780,-70.400 162.620,-55.440 104.340,-11.440 172.020,-17.600 184.240,-6.160 184.240,15.840 171.080,25.520 103.400,21.120 160.740,67.760 159.800,80.080 142.880,95.040 127.840,90.640 60.160,46.640 23.500,50.160 15.980,66.880 0.000,66.880 -15.980,66.880 -23.500,50.160 -60.160,46.640 -127.840,90.640 -142.880,95.040 -159.800,80.080 -160.740,67.760 -103.400,21.120 -171.080,25.520 -184.240,15.840 -184.240,-6.160 -172.020,-17.600 -104.340,-11.440 -162.620,-55.440 -175.780,-70.400 -163.560,-86.240 -146.640,-92.400 -76.140,-43.120 -25.380,-34.320 -16.920,-101.200" fill="none" stroke="#784a30" stroke-width="3" stroke-linejoin="round"/><path d="M -157 -83 L -62 -6 L 0 10 L 62 -6 L 157 -83 M -149 86 L -50 25 L 0 10 L 50 25 L 149 86 M 0 -117 V 53 M -178 6 H -78 M 78 6 H 178" fill="none" stroke="#75432a" stroke-width="11" stroke-linejoin="round"/><path d="M -157 -86 L -62 -9 L 0 7 L 62 -9 L 157 -86 M -149 83 L -50 22 L 0 7 L 50 22 L 149 83 M 0 -117 V 53 M -178 3 H -78 M 78 3 H 178" fill="none" stroke="#f3ce95" stroke-width="2.5" stroke-linejoin="round"/><path d="M -22 -33 L 0 -103 L 22 -33 L 0 7 Z" fill="url(#bronze)" stroke="#e5b47c" stroke-width="1.2"/><path d="M 0 -17 L 8 0 L 27 8 L 8 16 L 0 36 L -8 16 L -27 8 L -8 0 Z" fill="#8b5632" stroke="#f0c890" stroke-width="1.5"/><circle cx="0.000" cy="8.000" r="17.000" fill="url(#amber)"/><path d="M 0 -7 L 3 5 L 14 8 L 3 11 L 0 25 L -3 11 L -14 8 L -3 5 Z" fill="#ffe4a4"/><path d="M -32 270 H 32 L 42 284 V 324 L 29 340 H -29 L -42 324 V 284 Z" fill="url(#bronze)" stroke="#40291e" stroke-width="4"/><path d="M -27 279 H 27 L 32 288 V 319 L 24 330 H -24 L -32 319 V 288 Z" fill="#6c4938" stroke="#e5b17c" stroke-width="1.2"/><circle cx="0.000" cy="304.000" r="25.000" fill="url(#amber)"/><path d="M 0 289 L 5 301 L 0 316 L -5 301 Z" fill="#eec67e"/><ellipse cx="0" cy="345" rx="29" ry="14" fill="url(#gem)" stroke="#4a3426" stroke-width="3"/><path d="M -43 334 H 43 L 38 344 H -38 Z" fill="url(#bronze)" stroke="#593925" stroke-width="2"/><path d="M -38 337 H 38" stroke="#e7b982" stroke-width="1.2"/></g><g fill="#bfbaa8"><text x="90" y="108" font-family="Helvetica, sans-serif" font-size="11" letter-spacing="4">MOONLIGHT / RELIQUARY</text><text x="85" y="249" font-family="Georgia, serif" font-size="73" letter-spacing="-2">Vesper</text><text x="91" y="283" font-family="Helvetica, sans-serif" font-size="11" letter-spacing="4">THE ASTRAL MOONBLADE</text><path d="M 91 308 H 280" stroke="#667d79" stroke-width="0.7"/></g><g fill="#839f9d" font-family="Georgia, serif" font-size="17" font-style="italic"><text x="91" y="346">A small sky, held in bronze.</text></g><g fill="#77918e" font-family="Helvetica, sans-serif" font-size="10" letter-spacing="2"><text x="91" y="1536">BRONZE / MOONGLASS / STARLIGHT</text><text x="1309" y="1536" text-anchor="end">PLANAR STUDY 02</text></g></svg>
diff --git a/docs/moonblade/rig.py b/docs/moonblade/rig.py
new file mode 100644
--- /dev/null
+++ b/docs/moonblade/rig.py
@@ -0,0 +1,520 @@
+"""Native crescent OBJ -> solid, analytically weighted Blender sword.
+
+Pure geometry/weight descriptions precede the Blender effect boundary.
+Run: blender -b --factory-startup --python rig.py -- INPUT.obj OUTPUT_DIRECTORY
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import Enum
+from itertools import chain, product
+from math import cos, floor, isfinite, pi, sin
+from pathlib import Path
+import json
+import sys
+
+import bmesh
+import bpy
+from mathutils import Vector
+
+
+Vec3 = tuple[float, float, float]
+Face = tuple[int, ...]
+BLADE_LENGTH = 1.2
+BONE_COUNT = 6
+ROOT = "ROOT_Sword"
+
+
+class Finish(Enum):
+    STEEL = "Moonsteel"
+    GOLD = "Pale gold"
+    LEATHER = "Midnight leather"
+    GLASS = "Moon glass"
+    INLAY = "Ivory moon"
+
+
+class Control(Enum):
+    SIDE = "bend_side_deg"
+    FLAT = "bend_flat_deg"
+    TWIST = "twist_deg"
+
+
+@dataclass(frozen=True)
+class Drive:
+    control: Control
+    axis: int
+    limit: float
+
+
+DRIVES = (Drive(Control.SIDE, 2, 25.0), Drive(Control.FLAT, 0, 15.0), Drive(Control.TWIST, 1, 25.0))
+
+
+@dataclass(frozen=True)
+class Pose:
+    name: str
+    side: float = 0.0
+    flat: float = 0.0
+    twist: float = 0.0
+
+    def value(self, control: Control) -> float:
+        match control:
+            case Control.SIDE: return self.side
+            case Control.FLAT: return self.flat
+            case Control.TWIST: return self.twist
+
+
+@dataclass(frozen=True)
+class MeshSpec:
+    name: str
+    vertices: tuple[Vec3, ...]
+    faces: tuple[Face, ...]
+    finish: Finish
+
+
+@dataclass(frozen=True)
+class Weight:
+    bone: str
+    value: float
+
+
+def bone_name(index: int) -> str:
+    return f"BLADE_{index + 1:02d}"
+
+
+def skin_weights(z: float) -> tuple[Weight, ...]:
+    """Adjacent-bone smoothstep partition of unity; the base is rigid."""
+    u = max(0.0, min(BONE_COUNT - 1.0, z * BONE_COUNT / BLADE_LENGTH - 0.5))
+    lower = floor(u)
+    upper = min(BONE_COUNT - 1, lower + 1)
+    t = u - lower
+    mix = t * t * (3.0 - 2.0 * t)
+    return (Weight(bone_name(lower), 1.0),) if lower == upper else tuple(filter(
+        lambda weight: weight.value > 0.0,
+        (Weight(bone_name(lower), 1.0 - mix), Weight(bone_name(upper), mix)),
+    ))
+
+
+def lathe(name: str, profile: tuple[tuple[float, float], ...], finish: Finish, sides: int = 32,
+          flatten: float = 1.0) -> MeshSpec:
+    vertices = tuple(map(lambda sample: (
+        profile[sample[0]][1] * cos(2 * pi * sample[1] / sides),
+        flatten * profile[sample[0]][1] * sin(2 * pi * sample[1] / sides),
+        profile[sample[0]][0],
+    ), product(range(len(profile)), range(sides))))
+    walls = tuple(map(lambda cell: (
+        cell[0] * sides + cell[1], cell[0] * sides + (cell[1] + 1) % sides,
+        (cell[0] + 1) * sides + (cell[1] + 1) % sides, (cell[0] + 1) * sides + cell[1],
+    ), product(range(len(profile) - 1), range(sides))))
+    caps = (tuple(reversed(range(sides))), tuple(range((len(profile) - 1) * sides, len(profile) * sides)))
+    return MeshSpec(name, vertices, walls + caps, finish)
+
+
+def torus(name: str, center: Vec3, radius: float, thickness: float, finish: Finish) -> MeshSpec:
+    major, minor = 64, 8
+    vertices = tuple(map(lambda sample: (
+        center[0] + (radius + thickness * cos(2 * pi * sample[1] / minor)) * cos(2 * pi * sample[0] / major),
+        center[1] + thickness * sin(2 * pi * sample[1] / minor),
+        center[2] + (radius + thickness * cos(2 * pi * sample[1] / minor)) * sin(2 * pi * sample[0] / major),
+    ), product(range(major), range(minor))))
+    faces = tuple(map(lambda cell: (
+        cell[0] * minor + cell[1], ((cell[0] + 1) % major) * minor + cell[1],
+        ((cell[0] + 1) % major) * minor + (cell[1] + 1) % minor,
+        cell[0] * minor + (cell[1] + 1) % minor,
+    ), product(range(major), range(minor))))
+    return MeshSpec(name, vertices, faces, finish)
+
+
+def diamond(name: str, width: float, height: float, front: float, finish: Finish) -> MeshSpec:
+    return MeshSpec(name,
+        ((0.0, -0.023, height), (width, -0.023, 0.0), (0.0, -0.023, -height),
+         (-width, -0.023, 0.0), (0.0, front, 0.0), (0.0, 0.016, 0.0)),
+        tuple(chain.from_iterable(map(lambda index: (
+            (index, (index + 1) % 4, 4), ((index + 1) % 4, index, 5),
+        ), range(4)))), finish)
+
+
+def binding(index: int) -> MeshSpec:
+    z = -0.068 - index * 0.0155
+    return lathe(f"Grip binding {index + 1:02d}",
+        ((z - 0.002, 0.028), (z, 0.0295), (z + 0.002, 0.028)), Finish.GOLD, flatten=0.78)
+
+
+def pommel_disc() -> MeshSpec:
+    disc = lathe("Pommel moon glass", ((-0.016, 0.042), (0.0, 0.046), (0.016, 0.042)), Finish.GLASS)
+    return MeshSpec(disc.name, tuple(map(lambda v: (v[0], v[2], v[1] - 226 / 750), disc.vertices)), disc.faces, disc.finish)
+
+
+def crescent_inlay() -> MeshSpec:
+    """A shallow closed crescent ribbon, not a texture painted on the pommel."""
+    steps = 40
+    # Two meridian curves meet at their tips; interior cross-sections remain quads.
+    rows = tuple(map(lambda index: index / steps, range(1, steps)))
+    tip_top, tip_bottom = (0.015, -0.021, -0.269), (0.015, -0.021, -0.334)
+    ribbon = tuple(chain.from_iterable(map(lambda t: (
+        (0.015 - 0.047 * sin(pi * t), -0.021, -0.269 - 0.065 * t),
+        (0.015 - 0.028 * sin(pi * t), -0.021, -0.269 - 0.065 * t),
+    ), rows)))
+    front = (tip_top,) + ribbon + (tip_bottom,)
+    n = len(front)
+    front_faces = ((0, 1, 2),) + tuple(map(lambda row: (
+        1 + 2 * row, 3 + 2 * row, 4 + 2 * row, 2 + 2 * row,
+    ), range(len(rows) - 1))) + ((n - 3, n - 1, n - 2),)
+    boundary = (0,) + tuple(range(1, n - 1, 2)) + (n - 1,) + tuple(reversed(range(2, n - 1, 2)))
+    back = tuple(map(lambda v: (v[0], v[1] + 0.002, v[2]), front))
+    walls = tuple(map(lambda edge: (edge[0], edge[1], edge[1] + n, edge[0] + n),
+        zip(boundary, boundary[1:] + boundary[:1])))
+    return MeshSpec("Pommel crescent inlay", front + back,
+        front_faces + tuple(map(lambda face: tuple(map(lambda i: i + n, reversed(face))), front_faces)) + walls,
+        Finish.INLAY)
+
+
+def fittings() -> tuple[MeshSpec, ...]:
+    return (
+        lathe("Leather grip", ((-0.27, 0.022), (-0.25, 0.027), (-0.06, 0.03), (-0.025, 0.032)), Finish.LEATHER, flatten=0.78),
+        lathe("Grip lower ferrule", ((-0.271, 0.024), (-0.261, 0.03), (-0.252, 0.03)), Finish.GOLD, flatten=0.8),
+        lathe("Grip upper ferrule", ((-0.056, 0.032), (-0.032, 0.034)), Finish.GOLD, flatten=0.8),
+        diamond("Guard gemstone setting", 0.043, 0.072, -0.037, Finish.GOLD),
+        diamond("Guard moon glass", 0.031, 0.053, -0.052, Finish.GLASS),
+        pommel_disc(), torus("Pommel rim", (0.0, -0.012, -226 / 750), 0.045, 0.0028, Finish.GOLD),
+        crescent_inlay(),
+    ) + tuple(map(binding, range(12)))
+
+
+POSE_TESTS = (
+    Pose("neutral"), Pose("side positive", side=25), Pose("side negative", side=-25),
+    Pose("flat positive", flat=15), Pose("flat negative", flat=-15),
+    Pose("twist positive", twist=25), Pose("twist negative", twist=-25),
+    Pose("combined positive", 25, 15, 25), Pose("combined negative", -25, -15, -25),
+    Pose("above limit", 250, 150, 250),
+)
+
+
+# Blender effect boundary: construct data blocks, bind, evaluate, save and render.
+def finish_material(finish: Finish, shade: int = 0) -> bpy.types.Material:
+    material = bpy.data.materials.new(f"{finish.value} {shade:02d}")
+    match finish:
+        case Finish.STEEL: color, metal, rough = (0.10 + shade * 0.018, 0.27 + shade * 0.027, 0.38 + shade * 0.031, 1.0), 0.78, 0.3
+        case Finish.GOLD: color, metal, rough = (0.48 + shade * 0.035, 0.36 + shade * 0.032, 0.16 + shade * 0.024, 1.0), 0.83, 0.3
+        case Finish.LEATHER: color, metal, rough = (0.009, 0.018, 0.028, 1.0), 0.08, 0.68
+        case Finish.GLASS: color, metal, rough = (0.028, 0.27, 0.40, 1.0), 0.55, 0.16
+        case Finish.INLAY: color, metal, rough = (0.75, 0.84, 0.68, 1.0), 0.4, 0.27
+    material.diffuse_color = color
+    material.use_nodes = True
+    shader = material.node_tree.nodes.get("Principled BSDF")
+    shader.inputs["Base Color"].default_value = color
+    shader.inputs["Metallic"].default_value = metal
+    shader.inputs["Roughness"].default_value = rough
+    return material
+
+
+def clean_mesh(mesh: bpy.types.Mesh, weld: bool = False) -> None:
+    editable = bmesh.new()
+    editable.from_mesh(mesh)
+    if weld:
+        bmesh.ops.remove_doubles(editable, verts=tuple(editable.verts), dist=1e-8)
+    bmesh.ops.recalc_face_normals(editable, faces=tuple(editable.faces))
+    editable.to_mesh(mesh)
+    editable.free()
+    mesh.update()
+
+
+def assign_materials(obj: bpy.types.Object, materials: tuple[bpy.types.Material, ...]) -> None:
+    tuple(map(obj.data.materials.append, materials))
+    obj.data.polygons.foreach_set("material_index", tuple(map(
+        lambda face: int((0.5 + 0.5 * sin(face.index * 1.719)) * (len(materials) - 1)), obj.data.polygons)))
+
+
+def add_uvs(mesh: bpy.types.Mesh) -> None:
+    lower_x, upper_x = min(map(lambda v: v.co.x, mesh.vertices)), max(map(lambda v: v.co.x, mesh.vertices))
+    lower_z, upper_z = min(map(lambda v: v.co.z, mesh.vertices)), max(map(lambda v: v.co.z, mesh.vertices))
+    uv = mesh.uv_layers.new(name="Surface projection")
+    values = tuple(chain.from_iterable(map(lambda loop: (
+        (mesh.vertices[loop.vertex_index].co.x - lower_x) / max(1e-8, upper_x - lower_x),
+        (mesh.vertices[loop.vertex_index].co.z - lower_z) / max(1e-8, upper_z - lower_z),
+    ), mesh.loops)))
+    uv.data.foreach_set("uv", values)
+
+
+def create_mesh(spec: MeshSpec) -> bpy.types.Object:
+    mesh = bpy.data.meshes.new(spec.name)
+    mesh.from_pydata(spec.vertices, (), spec.faces)
+    clean_mesh(mesh)
+    obj = bpy.data.objects.new(spec.name, mesh)
+    bpy.context.scene.collection.objects.link(obj)
+    assign_materials(obj, (finish_material(spec.finish),))
+    add_uvs(mesh)
+    return obj
+
+
+def import_native(path: Path) -> tuple[bpy.types.Object, bpy.types.Object]:
+    bpy.ops.wm.obj_import(filepath=str(path), forward_axis="Y", up_axis="Z")
+    blade, guard = bpy.data.objects["Blade"], bpy.data.objects["Guard"]
+    tuple(map(lambda obj: clean_mesh(obj.data, weld=True), (blade, guard)))
+    assign_materials(blade, tuple(map(lambda index: finish_material(Finish.STEEL, index), range(8))))
+    assign_materials(guard, tuple(map(lambda index: finish_material(Finish.GOLD, index), range(6))))
+    tuple(map(lambda obj: add_uvs(obj.data), (blade, guard)))
+    return blade, guard
+
+
+def edit_blade_bone(armature: bpy.types.Armature, index: int) -> None:
+    bone = armature.edit_bones.new(bone_name(index))
+    bone.head = (0.0, 0.0, index * BLADE_LENGTH / BONE_COUNT)
+    bone.tail = (0.0, 0.0, (index + 1) * BLADE_LENGTH / BONE_COUNT)
+    bone.parent = armature.edit_bones[ROOT if index == 0 else bone_name(index - 1)]
+    bone.use_connect = True
+
+
+def add_control(root: bpy.types.PoseBone, drive: Drive) -> None:
+    root[drive.control.value] = 0.0
+    root.id_properties_ui(drive.control.value).update(
+        min=-drive.limit, max=drive.limit, soft_min=-drive.limit, soft_max=drive.limit,
+        description=f"Total {drive.control.value.replace('_', ' ')}. Clamped; base, grip and guard stay rigid.")
+
+
+def bone_driver(rig: bpy.types.Object, bone: bpy.types.PoseBone, drive: Drive) -> None:
+    driver = bone.driver_add("rotation_euler", drive.axis).driver
+    variable = driver.variables.new()
+    variable.name = "angle"
+    variable.type = "SINGLE_PROP"
+    variable.targets[0].id = rig
+    variable.targets[0].data_path = f'pose.bones["{ROOT}"]["{drive.control.value}"]'
+    driver.expression = f"min(max(angle,{-drive.limit}),{drive.limit})*{pi / 180 / (BONE_COUNT - 1):.16f}"
+
+
+def configure_blade_bone(rig: bpy.types.Object, index: int) -> None:
+    bone = rig.pose.bones[bone_name(index)]
+    bone.rotation_mode = "XYZ"
+    bone.lock_location = (True, True, True)
+    bone.lock_scale = (True, True, True)
+    bone.lock_rotation = (True, True, True)
+    bone.bone.color.palette = "THEME04"
+    if index > 0:
+        tuple(map(lambda drive: bone_driver(rig, bone, drive), DRIVES))
+
+
+def create_rig() -> bpy.types.Object:
+    armature = bpy.data.armatures.new("Vesper skeleton")
+    rig = bpy.data.objects.new("VESPER | Rig", armature)
+    bpy.context.scene.collection.objects.link(rig)
+    bpy.context.view_layer.objects.active = rig
+    rig.select_set(True)
+    bpy.ops.object.mode_set(mode="EDIT")
+    root = armature.edit_bones.new(ROOT)
+    root.head, root.tail = (0.0, 0.0, -0.28), (0.0, 0.0, 0.0)
+    tuple(map(lambda index: edit_blade_bone(armature, index), range(BONE_COUNT)))
+    bpy.ops.object.mode_set(mode="OBJECT")
+    rig.show_in_front = True
+    armature.display_type = "BBONE"
+    root_pose = rig.pose.bones[ROOT]
+    root_pose.bone.color.palette = "THEME03"
+    root_pose.lock_scale = (True, True, True)
+    tuple(map(lambda drive: add_control(root_pose, drive), DRIVES))
+    tuple(map(lambda index: configure_blade_bone(rig, index), range(BONE_COUNT)))
+    return rig
+
+
+def bind_mesh(obj: bpy.types.Object, rig: bpy.types.Object, deform: bool) -> None:
+    names = tuple(map(bone_name, range(BONE_COUNT))) if deform else (ROOT,)
+    groups = dict(map(lambda name: (name, obj.vertex_groups.new(name=name)), names))
+    weights = tuple(map(lambda vertex: (vertex.index, skin_weights(vertex.co.z) if deform else (Weight(ROOT, 1.0),)), obj.data.vertices))
+    tuple(map(lambda vertex: tuple(map(lambda weight: groups[weight.bone].add((vertex[0],), weight.value, "REPLACE"), vertex[1])), weights))
+    modifier = obj.modifiers.new("Analytic skin | six bones", "ARMATURE")
+    modifier.object = rig
+    modifier.use_deform_preserve_volume = False
+    modifier.use_bone_envelopes = False
+    obj.parent = rig
+
+
+def apply_pose(rig: bpy.types.Object, pose: Pose) -> None:
+    root = rig.pose.bones[ROOT]
+    tuple(map(lambda drive: root.__setitem__(drive.control.value, pose.value(drive.control)), DRIVES))
+    rig.update_tag()
+    bpy.context.view_layer.update()
+
+
+def coordinates(obj: bpy.types.Object) -> tuple[Vec3, ...]:
+    evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
+    return tuple(map(lambda vertex: tuple(vertex.co), evaluated.data.vertices))
+
+
+def topology_report(obj: bpy.types.Object) -> dict[str, object]:
+    editable = bmesh.new()
+    editable.from_mesh(obj.data)
+    report = {"object": obj.name, "vertices": len(editable.verts), "faces": len(editable.faces),
+              "nonmanifold_edges": sum(map(lambda edge: not edge.is_manifold, editable.edges)),
+              "zero_area_faces": sum(map(lambda face: face.calc_area() <= 1e-16, editable.faces)),
+              "max_weight_error": max(map(lambda vertex: abs(sum(map(lambda weight: weight.weight, vertex.groups)) - 1.0), obj.data.vertices))}
+    editable.free()
+    return report
+
+
+def triangle_area(points: tuple[Vec3, ...], indices: tuple[int, int, int]) -> float:
+    a, b, c = tuple(map(lambda index: Vector(points[index]), indices))
+    return (b - a).cross(c - a).length * 0.5
+
+
+def pose_report(rig: bpy.types.Object, blade: bpy.types.Object, rigid: tuple[bpy.types.Object, ...],
+                rest: tuple[Vec3, ...], rigid_rest: tuple[tuple[Vec3, ...], ...], pose: Pose) -> dict[str, object]:
+    apply_pose(rig, pose)
+    posed = coordinates(blade)
+    triangles = tuple(map(lambda triangle: tuple(triangle.vertices), blade.data.loop_triangles))
+    ratios = tuple(map(lambda triangle: triangle_area(posed, triangle) / triangle_area(rest, triangle), triangles))
+    drift = max(chain.from_iterable(map(lambda pair: map(
+        lambda points: (Vector(points[0]) - Vector(points[1])).length, zip(coordinates(pair[0]), pair[1])), zip(rigid, rigid_rest))))
+    return {"pose": pose.name, "finite": all(map(isfinite, chain.from_iterable(posed))),
+            "min_face_area_ratio": min(ratios), "max_face_area_ratio": max(ratios), "rigid_drift_m": drift,
+            "tip": posed[max(range(len(rest)), key=lambda index: rest[index][2])],
+            "max_vertex_displacement_m": max(map(lambda pair: (Vector(pair[0]) - Vector(pair[1])).length, zip(rest, posed)))}
+
+
+def validate_rig(rig: bpy.types.Object, blade: bpy.types.Object, rigid: tuple[bpy.types.Object, ...]) -> dict[str, object]:
+    apply_pose(rig, Pose("neutral"))
+    blade.data.calc_loop_triangles()
+    rest, rigid_rest = coordinates(blade), tuple(map(coordinates, rigid))
+    topology = tuple(map(topology_report, (blade,) + rigid))
+    poses = tuple(map(lambda pose: pose_report(rig, blade, rigid, rest, rigid_rest, pose), POSE_TESTS))
+    apply_pose(rig, Pose("neutral"))
+    passed = all(map(lambda entry: entry["nonmanifold_edges"] == 0 and entry["zero_area_faces"] == 0 and entry["max_weight_error"] < 1e-6, topology)) and all(map(
+        lambda entry: entry["finite"] and entry["min_face_area_ratio"] > 0.15 and entry["max_face_area_ratio"] < 5.0 and entry["rigid_drift_m"] < 1e-7, poses))
+    clamped = (Vector(poses[-1]["tip"]) - Vector(poses[-3]["tip"])).length < 1e-7
+    return {"passed": passed and clamped, "bounds_enforced": clamped, "topology": topology, "poses": poses,
+            "scope": "Sampled bounded poses; not collision certification or physical metal simulation."}
+
+
+def key_pose(rig: bpy.types.Object, frame: int, pose: Pose) -> None:
+    apply_pose(rig, pose)
+    tuple(map(lambda drive: rig.pose.bones[ROOT].keyframe_insert(data_path=f'["{drive.control.value}"]', frame=frame), DRIVES))
+
+
+def animate_rig(rig: bpy.types.Object) -> None:
+    tuple(map(lambda sample: key_pose(rig, sample[0], sample[1]), (
+        (1, Pose("neutral")), (25, Pose("side", side=25)), (49, Pose("flat", flat=15)),
+        (73, Pose("twist", twist=25)), (97, Pose("combined", -25, -15, -25)), (121, Pose("neutral")),
+    )))
+    rig.animation_data.action.name = "Vesper | bounded blade demonstration"
+    bpy.context.scene.frame_start, bpy.context.scene.frame_end = 1, 120
+    bpy.context.scene.render.fps = 24
+    bpy.context.scene.frame_set(1)
+
+
+def aim(obj: bpy.types.Object, target: Vec3) -> None:
+    obj.rotation_euler = (Vector(target) - obj.location).to_track_quat("-Z", "Y").to_euler()
+
+
+def area_light(name: str, location: Vec3, color: Vec3, energy: float, size: float) -> None:
+    data = bpy.data.lights.new(name, "AREA")
+    data.energy, data.color, data.shape, data.size = energy, color, "DISK", size
+    obj = bpy.data.objects.new(name, data)
+    bpy.context.scene.collection.objects.link(obj)
+    obj.location = location
+    aim(obj, (0.0, 0.0, 0.4))
+
+
+def stage() -> None:
+    scene = bpy.context.scene
+    scene.render.engine = "CYCLES"
+    scene.cycles.samples = 24
+    scene.cycles.use_denoising = True
+    scene.render.resolution_x, scene.render.resolution_y, scene.render.resolution_percentage = 840, 1080, 100
+    scene.render.image_settings.file_format = "PNG"
+    scene.world = bpy.data.worlds.new("Midnight studio")
+    scene.world.use_nodes = True
+    scene.world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.065, 0.1, 0.18, 1.0)
+    scene.world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.32
+    camera_data = bpy.data.cameras.new("Vesper portrait")
+    camera = bpy.data.objects.new("Vesper portrait", camera_data)
+    scene.collection.objects.link(camera)
+    camera.location = (0.95, -4.8, 1.55)
+    camera_data.type, camera_data.ortho_scale = "ORTHO", 1.93
+    aim(camera, (0.035, 0.0, 0.43))
+    scene.camera = camera
+    tuple(map(lambda spec: area_light(*spec), (
+        ("Silver softbox", (-2.0, -3.0, 2.5), (0.64, 0.84, 1.0), 420.0, 3.0),
+        ("Gold strip", (2.0, -1.5, 1.1), (1.0, 0.75, 0.43), 260.0, 1.4),
+        ("Moon rim", (0.5, 2.0, 1.8), (0.23, 0.63, 1.0), 560.0, 2.0),
+    )))
+    floor_spec = MeshSpec("Studio floor", ((-100, -100, -0.39), (100, -100, -0.39), (100, 100, -0.39), (-100, 100, -0.39)), ((0, 1, 2, 3),), Finish.LEATHER)
+    floor_obj = create_mesh(floor_spec)
+    floor_obj.data.materials[0].node_tree.nodes.get("Principled BSDF").inputs["Roughness"].default_value = 0.42
+
+
+GUIDE = """VESPER - RIGGED CRESCENT MOONBLADE
+
+Select VESPER | Rig. In Pose Mode select ROOT_Sword.
+Move/rotate ROOT_Sword to place the complete rigid sword.
+N sidebar > Item > Properties (Custom Properties):
+  bend_side_deg: -25 to +25 degrees
+  bend_flat_deg: -15 to +15 degrees
+  twist_deg:     -25 to +25 degrees
+Zero all three for a rigid sword. Blender drivers clamp even scripted values.
+The first blade segment and the grip, guard, gemstone and pommel stay rigid.
+Six connected blade bones use analytic smoothstep weights, at most two per vertex.
+No automatic weights, cloth solver, simulation cache or corrective shape keys.
+
+Frames 1-120: a five-second sampled bend/twist demonstration. Frame 1 is neutral.
+Clear the 'Vesper | bounded blade demonstration' action to pose without animation.
+Native .blend keeps the controls. GLB retains skin/bones and baked animation;
+Blender custom-property drivers are not portable glTF controls.
+
+Solid native CDT blade and guard; fuller is an actual open tunnel. Material
+facets are authored; projection UVs are provided, not a unique bake-ready atlas.
+This is a stylized rigged prop, not a collision-certified or physical metal model.
+"""
+
+
+def save_asset(rig: bpy.types.Object, objects: tuple[bpy.types.Object, ...], output: Path) -> None:
+    bpy.ops.object.select_all(action="DESELECT")
+    tuple(map(lambda obj: obj.select_set(True), (rig,) + objects))
+    bpy.context.view_layer.objects.active = rig
+    bpy.ops.export_scene.gltf(filepath=str(output / "vesper-rig.glb"), export_format="GLB", use_selection=True,
+        export_animations=True, export_animation_mode="SCENE", export_force_sampling=True,
+        export_bake_animation=True, export_frame_range=True, export_def_bones=True,
+        export_skins=True, export_materials="EXPORT", export_extras=True)
+    bpy.ops.object.select_all(action="DESELECT")
+    rig.select_set(True)
+    bpy.context.view_layer.objects.active = rig
+    bpy.ops.object.mode_set(mode="POSE")
+    rig.data.bones.active = rig.data.bones[ROOT]
+    rig.pose.bones[ROOT].select = True
+    tuple(map(configure_viewport, chain.from_iterable(map(
+        lambda screen: chain.from_iterable(map(lambda area: filter(lambda space: space.type == "VIEW_3D", area.spaces), screen.areas)),
+        bpy.data.screens))))
+    bpy.data.texts.new("START HERE - Vesper controls").write(GUIDE)
+    bpy.ops.wm.save_as_mainfile(filepath=str(output / "vesper-rig.blend"))
+    (output / "README.txt").write_text(GUIDE)
+
+
+def configure_viewport(space: bpy.types.SpaceView3D) -> None:
+    space.region_3d.view_perspective = "CAMERA"
+    space.show_region_ui = True
+    space.shading.type = "SOLID"
+    space.shading.color_type = "MATERIAL"
+    space.overlay.show_floor = False
+
+
+def main() -> None:
+    args = tuple(sys.argv[sys.argv.index("--") + 1:]) if "--" in sys.argv else ()
+    if len(args) != 2:
+        raise SystemExit("usage: blender -b --factory-startup --python rig.py -- INPUT.obj OUTPUT_DIRECTORY")
+    source, output = Path(args[0]), Path(args[1])
+    output.mkdir(parents=True, exist_ok=True)
+    bpy.ops.wm.read_factory_settings(use_empty=True)
+    blade, guard = import_native(source)
+    ornaments = tuple(map(create_mesh, fittings()))
+    rig = create_rig()
+    bind_mesh(blade, rig, deform=True)
+    tuple(map(lambda obj: bind_mesh(obj, rig, deform=False), (guard,) + ornaments))
+    report = validate_rig(rig, blade, (guard,) + ornaments)
+    (output / "deformation-checks.json").write_text(json.dumps(report, indent=2))
+    print(json.dumps({"passed": report["passed"], "poses": report["poses"]}), flush=True)
+    if not report["passed"]:
+        raise SystemExit("Rig checks failed; see deformation-checks.json")
+    animate_rig(rig)
+    stage()
+    save_asset(rig, (blade, guard) + ornaments, output)
+    bpy.context.scene.render.filepath = str(output / "vesper-rig.png")
+    bpy.ops.render.render(write_still=True)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/docs/noctilucent-garden/Main.hs b/docs/noctilucent-garden/Main.hs
new file mode 100644
--- /dev/null
+++ b/docs/noctilucent-garden/Main.hs
@@ -0,0 +1,211 @@
+module Main (main) where
+
+import Data.Maybe (mapMaybe)
+import qualified Data.Vector as Vector
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Dcel (innerFaceVertexTriples, numInnerFaces, vertexPoint)
+import Moonlight.Planar.Exhibit.Svg (polygon, circle, scalar)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Types (BuildError)
+import Moonlight.Planar.Voronoi
+  ( voronoiFaceAdjacentEdges, voronoiFaceSite, voronoiFaces
+  , voronoiFrom, voronoiVertexPosition
+  )
+import System.Environment (getArgs)
+import System.Exit (die)
+import Text.Printf (printf)
+
+data Bloom = Bloom
+  { bloomCenter :: !Point
+  , bloomRadius :: !Double
+  , bloomPetals :: !Int
+  , bloomPhase :: !Double
+  , bloomHue :: !Double
+  }
+
+data Blossom = Blossom
+  { blossomPicture :: !String
+  , blossomTriangleCount :: !Int
+  , blossomCellCount :: !Int
+  }
+
+main :: IO ()
+main = do
+  arguments <- getArgs
+  case arguments of
+    [outputPath] -> either (die . show) (writeFile outputPath) garden
+    _ -> die "usage: moonlight-planar-noctilucent-garden OUTPUT.svg"
+
+garden :: Either BuildError String
+garden = do
+  blossoms <- traverse buildBlossom blooms
+  pure $
+    "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1600\" height=\"1200\" viewBox=\"0 0 1600 1200\">"
+      <> "<title>Noctilucent garden</title><desc>A midnight botanical study. Five phyllotactic point clouds are triangulated by Moonlight Planar; their finite Voronoi cells become luminous glass petals. "
+      <> show (sum (blossomTriangleCount <$> blossoms)) <> " native Delaunay triangles and "
+      <> show (sum (blossomCellCount <$> blossoms)) <> " finite dual cells. Typography, stems, color and lighting are authored SVG, not additional geometry algorithms.</desc>"
+      <> definitions
+      <> "<rect width=\"1600\" height=\"1200\" fill=\"url(#night)\"/>"
+      <> stars
+      <> "<circle cx=\"1020\" cy=\"443\" r=\"407\" fill=\"url(#aureole)\"/>"
+      <> orbits
+      <> foldMap renderStem blooms
+      <> foldMap renderLeaf leaves
+      <> foldMap blossomPicture blossoms
+      <> typography
+      <> "</svg>"
+
+blooms :: [Bloom]
+blooms =
+  [ Bloom (Point 1040 438) 300 7 0.35 209
+  , Bloom (Point 638 668) 164 6 1.1 257
+  , Bloom (Point 1295 819) 177 8 0.2 185
+  , Bloom (Point 930 971) 103 5 0.9 28
+  , Bloom (Point 400 873) 92 6 0.4 220
+  ]
+
+-- The surrounding ring supplies bounded dual cells; it is not painted.
+bloomSites :: Bloom -> Vector.Vector Point
+bloomSites bloom = Vector.fromList (fmap site [0 .. 239] <> fmap guardSite [0 .. 39])
+ where
+  site :: Int -> Point
+  site index =
+    let theta = fromIntegral index * pi * (3 - sqrt 5) + bloomPhase bloom
+        fraction = sqrt ((fromIntegral index + 0.5) / 240)
+        radial = bloomRadius bloom * fraction
+          * (0.83 + 0.17 * cos (fromIntegral (bloomPetals bloom) * theta))
+     in polar radial theta
+  guardSite :: Int -> Point
+  guardSite index = polar (bloomRadius bloom * 1.35) (2 * pi * fromIntegral index / 40)
+
+buildBlossom :: Bloom -> Either BuildError Blossom
+buildBlossom bloom = do
+  mesh <- delaunayGeometry (bloomSites bloom)
+  let visibleSite point@(Point x y) =
+        let theta = atan2 y x
+            limit = bloomRadius bloom * (0.84 + 0.17 * cos (fromIntegral (bloomPetals bloom) * theta))
+         in magnitude point < limit
+      cells = mapMaybe
+        (\face ->
+          let site = vertexPoint mesh (voronoiFaceSite face)
+           in if visibleSite site
+                then (\corners -> (site, corners)) <$>
+                  traverse (voronoiVertexPosition mesh . voronoiFrom mesh)
+                    (voronoiFaceAdjacentEdges mesh face)
+                else Nothing)
+        (voronoiFaces mesh)
+      triangles = Vector.toList $ fmap
+        (\(a, b, c) -> fmap (vertexPoint mesh) [a, b, c])
+        (innerFaceVertexTriples mesh)
+      visibleTriangles = filter (all visibleSite) triangles
+      Point centerX centerY = bloomCenter bloom
+      picture = "<g transform=\"translate(" <> scalar centerX <> " " <> scalar centerY <> ")\">"
+        <> "<circle r=\"" <> scalar (bloomRadius bloom * 1.18) <> "\" fill=\"url(#bloom-light)\"/>"
+        <> foldMap (renderCell bloom) cells
+        <> "<g fill=\"none\" stroke=\"#d1e7ed\" stroke-width=\"0.55\" opacity=\"0.22\">"
+        <> foldMap (polygon "") visibleTriangles <> "</g>"
+        <> foldMap (renderPollen bloom . fst) cells
+        <> "<circle r=\"9\" fill=\"#061019\" stroke=\"#dabb83\" stroke-width=\"0.8\"/>"
+        <> "<circle r=\"2\" fill=\"#ffe5a2\"/></g>"
+  pure (Blossom picture (numInnerFaces mesh) (length cells))
+
+renderCell :: Bloom -> (Point, [Point]) -> String
+renderCell bloom (site@(Point x y), corners) =
+  let radial = magnitude site / bloomRadius bloom
+      wave = sin (atan2 y x * fromIntegral (bloomPetals bloom) + radial * 11)
+      hue = bloomHue bloom + 23 * wave + 24 * radial
+      lightness = 18 + 30 * radial + 9 * wave
+      -- Insetting is a presentation transform, not a second Voronoi solver.
+      inset (Point px py) = Point (x + 0.87 * (px - x)) (y + 0.87 * (py - y))
+      attributes = "fill=\"" <> hslColor hue 0.55 (lightness / 100)
+        <> "\" stroke=\"" <> hslColor hue 0.65 0.75 <> "\" stroke-opacity=\"0.53\" stroke-width=\"0.65\""
+   in polygon attributes (inset <$> corners)
+
+renderPollen :: Bloom -> Point -> String
+renderPollen bloom point =
+  circle point (if magnitude point < bloomRadius bloom * 0.36 then 1.6 else 0.85)
+    "fill=\"#f8d9a1\" opacity=\"0.85\""
+
+renderStem :: Bloom -> String
+renderStem bloom =
+  let Point x y = bloomCenter bloom
+   in "<path d=\"M " <> scalar x <> " " <> scalar y
+      <> " C " <> scalar (x - 110) <> " " <> scalar (y + 260)
+      <> ", 720 1040, 790 1198\" fill=\"none\" stroke=\"url(#stem)\" stroke-width=\"1.5\"/>"
+
+leaves :: [(Point, Double, Double)]
+leaves = [(Point 771 1057, -148, 210), (Point 821 1110, -36, 215), (Point 668 962, -163, 180)]
+
+renderLeaf :: (Point, Double, Double) -> String
+renderLeaf (Point x y, angle, size) =
+  "<g transform=\"translate(" <> scalar x <> " " <> scalar y <> ") rotate(" <> scalar angle <> ")\">"
+    <> "<path d=\"M 0 0 Q " <> scalar (size * 0.46) <> " -62 " <> scalar size <> " 0 Q "
+    <> scalar (size * 0.46) <> " 53 0 0\" fill=\"#102d34\" fill-opacity=\"0.7\" stroke=\"#648b83\" stroke-width=\"0.6\"/>"
+    <> foldMap vein [1 .. 17 :: Int]
+    <> "<path d=\"M 0 0 L " <> scalar size <> " 0\" stroke=\"#b3ad86\" stroke-width=\"0.7\"/></g>"
+ where
+  vein :: Int -> String
+  vein index =
+    let fraction = fromIntegral index / 18
+        position = fraction * size
+        spanY = 26 * sin (pi * fraction)
+     in "<path d=\"M " <> scalar (position - 16) <> " 0 L " <> scalar position <> " " <> scalar (-spanY)
+        <> " M " <> scalar (position - 16) <> " 0 L " <> scalar position <> " " <> scalar spanY
+        <> "\" stroke=\"#648b83\" stroke-width=\"0.5\" opacity=\"0.65\"/>"
+
+orbits :: String
+orbits = "<g fill=\"none\" stroke=\"#c9b88c\" stroke-width=\"0.65\">"
+  <> "<circle cx=\"1040\" cy=\"438\" r=\"372\" opacity=\"0.34\"/>"
+  <> "<circle cx=\"1040\" cy=\"438\" r=\"379\" opacity=\"0.16\" stroke-dasharray=\"1 8\"/>"
+  <> "<path d=\"M 768 113 A 424 424 0 0 1 1430 608\" opacity=\"0.22\"/></g>"
+  <> circle (Point 790 160) 4 "fill=\"#d8c08d\""
+
+stars :: String
+stars = foldMap star [0 .. 239 :: Int]
+ where
+  star :: Int -> String
+  star index =
+    let x = 45 + fromIntegral ((index * 947 + 113) `mod` 1510)
+        y = 40 + fromIntegral ((index * 419 + 71) `mod` 1100)
+     in circle (Point x y) (if index `mod` 13 == 0 then 1.1 else 0.55)
+          "fill=\"#b1c8ca\" opacity=\"0.32\""
+
+definitions :: String
+definitions = "<defs>"
+  <> "<radialGradient id=\"night\" cx=\"67%\" cy=\"40%\" r=\"75%\"><stop stop-color=\"#153442\"/><stop offset=\"0.6\" stop-color=\"#091922\"/><stop offset=\"1\" stop-color=\"#050d15\"/></radialGradient>"
+  <> "<radialGradient id=\"aureole\"><stop offset=\"0.4\" stop-color=\"#38798a\" stop-opacity=\"0\"/><stop offset=\"0.77\" stop-color=\"#38798a\" stop-opacity=\"0.13\"/><stop offset=\"1\" stop-color=\"#38798a\" stop-opacity=\"0\"/></radialGradient>"
+  <> "<radialGradient id=\"bloom-light\"><stop stop-color=\"#91c5e9\" stop-opacity=\"0.09\"/><stop offset=\"1\" stop-color=\"#91c5e9\" stop-opacity=\"0\"/></radialGradient>"
+  <> "<linearGradient id=\"stem\" x2=\"0.4\" y2=\"1\"><stop stop-color=\"#9eb9a7\"/><stop offset=\"1\" stop-color=\"#466661\"/></linearGradient></defs>"
+
+typography :: String
+typography =
+  "<g fill=\"#ddcdaa\"><text x=\"83\" y=\"93\" font-family=\"Helvetica, sans-serif\" font-size=\"13\" letter-spacing=\"4\">MOONLIGHT / BOTANICAL STUDIES</text>"
+  <> "<path d=\"M 83 117 H 418\" stroke=\"#9c987d\" stroke-width=\"0.65\"/>"
+  <> "<text x=\"78\" y=\"239\" font-family=\"Georgia, serif\" font-size=\"71\" letter-spacing=\"-3\">Noctilucent</text>"
+  <> "<text x=\"81\" y=\"315\" font-family=\"Georgia, serif\" font-size=\"76\" font-style=\"italic\">garden.</text></g>"
+  <> "<g fill=\"#9cb4b7\" font-family=\"Georgia, serif\" font-size=\"20\"><text x=\"85\" y=\"371\">Flowers for the hours</text><text x=\"85\" y=\"401\">when no one is looking.</text></g>"
+  <> "<g fill=\"#a7b0a2\" font-family=\"Helvetica, sans-serif\" font-size=\"11\" letter-spacing=\"2\"><text x=\"85\" y=\"1111\">I / PHYLLOTAXIS</text><text x=\"85\" y=\"1135\">II / DELAUNAY</text><text x=\"85\" y=\"1159\">III / VORONOI</text><text x=\"1515\" y=\"1159\" text-anchor=\"end\">NATIVE GEOMETRY / STUDY 01</text></g>"
+
+-- Emit RGB so native SVG previewers and browsers agree on the palette.
+hslColor :: Double -> Double -> Double -> String
+hslColor hue saturation lightness =
+  let chroma = (1 - abs (2 * lightness - 1)) * saturation
+      sector = hue / 60
+      alternate = chroma * (1 - abs (sector - 2 * fromIntegral (floor (sector / 2) :: Int) - 1))
+      offset = lightness - chroma / 2
+      (red, green, blue) = case (floor sector :: Int) `mod` 6 of
+        0 -> (chroma, alternate, 0)
+        1 -> (alternate, chroma, 0)
+        2 -> (0, chroma, alternate)
+        3 -> (0, alternate, chroma)
+        4 -> (alternate, 0, chroma)
+        _ -> (chroma, 0, alternate)
+      channel :: Double -> Int
+      channel value = round (255 * (value + offset))
+   in printf "#%02x%02x%02x" (channel red) (channel green) (channel blue)
+
+polar :: Double -> Double -> Point
+polar radius angle = Point (radius * cos angle) (radius * sin angle)
+
+magnitude :: Point -> Double
+magnitude (Point x y) = sqrt (x * x + y * y)
diff --git a/docs/noctilucent-garden/README.md b/docs/noctilucent-garden/README.md
new file mode 100644
--- /dev/null
+++ b/docs/noctilucent-garden/README.md
@@ -0,0 +1,27 @@
+# Noctilucent garden
+
+A generative botanical print made with the public `build`, `dcel`, and `dual`
+components of Moonlight Planar. No private modules or replacement geometry
+algorithms are used.
+
+Five deterministic phyllotactic point clouds become Delaunay meshes through
+`delaunayGeometry`. The finite cells of their Voronoi dual become inset glass
+petals; the primal triangles remain visible as a fine vein network. A surrounding
+unpainted site ring bounds the flower cells. The artwork does not claim these
+stylized shapes are a biological simulation.
+
+Geometry construction and rendering are pure; `main` only reads the output path
+and writes the result. Change `blooms` for position, size, petal count, phase and
+color. Typography, lighting, leaf silhouettes and stems are authored SVG.
+
+From the repository root, after preparing the public DCEL dependency if needed:
+
+```sh
+scripts/safe-cabal.sh build moonlight-planar:lib:dcel \
+  --project-file=cabal.project.planar-dev --builddir=dist-newstyle-planar-dev --offline -j6
+scripts/safe-cabal.sh run moonlight-planar:exe:moonlight-planar-noctilucent-garden \
+  --project-file=cabal.project.planar-dev --builddir=dist-newstyle-planar-dev --offline -j6 -- \
+  /Users/bluerose/Developer/pale-meridian/compiler/foundation/moonlight-planar/docs/noctilucent-garden/noctilucent-garden.svg
+```
+
+The SVG is generated by this source; edit the source and rerun, not the output.
diff --git a/docs/noctilucent-garden/noctilucent-garden.svg b/docs/noctilucent-garden/noctilucent-garden.svg
new file mode 100644
--- /dev/null
+++ b/docs/noctilucent-garden/noctilucent-garden.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200"><title>Noctilucent garden</title><desc>A midnight botanical study. Five phyllotactic point clouds are triangulated by Moonlight Planar; their finite Voronoi cells become luminous glass petals. 2590 native Delaunay triangles and 1200 finite dual cells. Typography, stems, color and lighting are authored SVG, not additional geometry algorithms.</desc><defs><radialGradient id="night" cx="67%" cy="40%" r="75%"><stop stop-color="#153442"/><stop offset="0.6" stop-color="#091922"/><stop offset="1" stop-color="#050d15"/></radialGradient><radialGradient id="aureole"><stop offset="0.4" stop-color="#38798a" stop-opacity="0"/><stop offset="0.77" stop-color="#38798a" stop-opacity="0.13"/><stop offset="1" stop-color="#38798a" stop-opacity="0"/></radialGradient><radialGradient id="bloom-light"><stop stop-color="#91c5e9" stop-opacity="0.09"/><stop offset="1" stop-color="#91c5e9" stop-opacity="0"/></radialGradient><linearGradient id="stem" x2="0.4" y2="1"><stop stop-color="#9eb9a7"/><stop offset="1" stop-color="#466661"/></linearGradient></defs><rect width="1600" height="1200" fill="url(#night)"/><circle cx="158.000" cy="111.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1105.000" cy="530.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="542.000" cy="949.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1489.000" cy="268.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="926.000" cy="687.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="363.000" cy="1106.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1310.000" cy="425.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="747.000" cy="844.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="184.000" cy="163.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1131.000" cy="582.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="568.000" cy="1001.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1515.000" cy="320.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="952.000" cy="739.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="389.000" cy="58.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1336.000" cy="477.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="773.000" cy="896.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="210.000" cy="215.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1157.000" cy="634.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="594.000" cy="1053.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1541.000" cy="372.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="978.000" cy="791.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="415.000" cy="110.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1362.000" cy="529.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="799.000" cy="948.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="236.000" cy="267.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1183.000" cy="686.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="620.000" cy="1105.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="57.000" cy="424.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1004.000" cy="843.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="441.000" cy="162.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1388.000" cy="581.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="825.000" cy="1000.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="262.000" cy="319.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1209.000" cy="738.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="646.000" cy="57.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="83.000" cy="476.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1030.000" cy="895.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="467.000" cy="214.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1414.000" cy="633.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="851.000" cy="1052.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="288.000" cy="371.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1235.000" cy="790.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="672.000" cy="109.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="109.000" cy="528.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1056.000" cy="947.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="493.000" cy="266.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1440.000" cy="685.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="877.000" cy="1104.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="314.000" cy="423.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1261.000" cy="842.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="698.000" cy="161.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="135.000" cy="580.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1082.000" cy="999.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="519.000" cy="318.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1466.000" cy="737.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="903.000" cy="56.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="340.000" cy="475.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1287.000" cy="894.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="724.000" cy="213.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="161.000" cy="632.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1108.000" cy="1051.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="545.000" cy="370.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1492.000" cy="789.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="929.000" cy="108.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="366.000" cy="527.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1313.000" cy="946.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="750.000" cy="265.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="187.000" cy="684.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1134.000" cy="1103.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="571.000" cy="422.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1518.000" cy="841.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="955.000" cy="160.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="392.000" cy="579.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1339.000" cy="998.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="776.000" cy="317.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="213.000" cy="736.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1160.000" cy="55.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="597.000" cy="474.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1544.000" cy="893.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="981.000" cy="212.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="418.000" cy="631.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1365.000" cy="1050.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="802.000" cy="369.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="239.000" cy="788.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1186.000" cy="107.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="623.000" cy="526.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="60.000" cy="945.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1007.000" cy="264.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="444.000" cy="683.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1391.000" cy="1102.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="828.000" cy="421.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="265.000" cy="840.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1212.000" cy="159.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="649.000" cy="578.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="86.000" cy="997.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1033.000" cy="316.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="470.000" cy="735.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1417.000" cy="54.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="854.000" cy="473.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="291.000" cy="892.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1238.000" cy="211.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="675.000" cy="630.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="112.000" cy="1049.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1059.000" cy="368.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="496.000" cy="787.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1443.000" cy="106.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="880.000" cy="525.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="317.000" cy="944.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1264.000" cy="263.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="701.000" cy="682.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="138.000" cy="1101.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1085.000" cy="420.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="522.000" cy="839.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1469.000" cy="158.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="906.000" cy="577.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="343.000" cy="996.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1290.000" cy="315.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="727.000" cy="734.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="164.000" cy="53.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1111.000" cy="472.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="548.000" cy="891.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1495.000" cy="210.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="932.000" cy="629.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="369.000" cy="1048.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1316.000" cy="367.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="753.000" cy="786.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="190.000" cy="105.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1137.000" cy="524.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="574.000" cy="943.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1521.000" cy="262.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="958.000" cy="681.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="395.000" cy="1100.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1342.000" cy="419.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="779.000" cy="838.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="216.000" cy="157.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1163.000" cy="576.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="600.000" cy="995.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1547.000" cy="314.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="984.000" cy="733.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="421.000" cy="52.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1368.000" cy="471.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="805.000" cy="890.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="242.000" cy="209.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1189.000" cy="628.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="626.000" cy="1047.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="63.000" cy="366.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1010.000" cy="785.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="447.000" cy="104.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1394.000" cy="523.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="831.000" cy="942.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="268.000" cy="261.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1215.000" cy="680.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="652.000" cy="1099.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="89.000" cy="418.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1036.000" cy="837.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="473.000" cy="156.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1420.000" cy="575.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="857.000" cy="994.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="294.000" cy="313.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1241.000" cy="732.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="678.000" cy="51.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="115.000" cy="470.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1062.000" cy="889.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="499.000" cy="208.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1446.000" cy="627.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="883.000" cy="1046.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="320.000" cy="365.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1267.000" cy="784.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="704.000" cy="103.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="141.000" cy="522.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1088.000" cy="941.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="525.000" cy="260.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1472.000" cy="679.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="909.000" cy="1098.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="346.000" cy="417.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1293.000" cy="836.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="730.000" cy="155.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="167.000" cy="574.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1114.000" cy="993.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="551.000" cy="312.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1498.000" cy="731.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="935.000" cy="50.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="372.000" cy="469.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1319.000" cy="888.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="756.000" cy="207.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="193.000" cy="626.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1140.000" cy="1045.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="577.000" cy="364.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1524.000" cy="783.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="961.000" cy="102.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="398.000" cy="521.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1345.000" cy="940.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="782.000" cy="259.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="219.000" cy="678.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1166.000" cy="1097.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="603.000" cy="416.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="1550.000" cy="835.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="987.000" cy="154.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="424.000" cy="573.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1371.000" cy="992.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="808.000" cy="311.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="245.000" cy="730.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1192.000" cy="49.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="629.000" cy="468.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="66.000" cy="887.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1013.000" cy="206.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="450.000" cy="625.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1397.000" cy="1044.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="834.000" cy="363.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="271.000" cy="782.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1218.000" cy="101.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="655.000" cy="520.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="92.000" cy="939.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1039.000" cy="258.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="476.000" cy="677.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1423.000" cy="1096.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="860.000" cy="415.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="297.000" cy="834.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1244.000" cy="153.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="681.000" cy="572.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="118.000" cy="991.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1065.000" cy="310.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="502.000" cy="729.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1449.000" cy="48.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="886.000" cy="467.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="323.000" cy="886.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1270.000" cy="205.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="707.000" cy="624.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="144.000" cy="1043.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1091.000" cy="362.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="528.000" cy="781.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1475.000" cy="100.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="912.000" cy="519.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="349.000" cy="938.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1296.000" cy="257.000" r="1.100" fill="#b1c8ca" opacity="0.32"/><circle cx="733.000" cy="676.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="170.000" cy="1095.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1117.000" cy="414.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="554.000" cy="833.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1501.000" cy="152.000" r="0.550" fill="#b1c8ca" opacity="0.32"/><circle cx="1020" cy="443" r="407" fill="url(#aureole)"/><g fill="none" stroke="#c9b88c" stroke-width="0.65"><circle cx="1040" cy="438" r="372" opacity="0.34"/><circle cx="1040" cy="438" r="379" opacity="0.16" stroke-dasharray="1 8"/><path d="M 768 113 A 424 424 0 0 1 1430 608" opacity="0.22"/></g><circle cx="790.000" cy="160.000" r="4.000" fill="#d8c08d"/><path d="M 1040.000 438.000 C 930.000 698.000, 720 1040, 790 1198" fill="none" stroke="url(#stem)" stroke-width="1.5"/><path d="M 638.000 668.000 C 528.000 928.000, 720 1040, 790 1198" fill="none" stroke="url(#stem)" stroke-width="1.5"/><path d="M 1295.000 819.000 C 1185.000 1079.000, 720 1040, 790 1198" fill="none" stroke="url(#stem)" stroke-width="1.5"/><path d="M 930.000 971.000 C 820.000 1231.000, 720 1040, 790 1198" fill="none" stroke="url(#stem)" stroke-width="1.5"/><path d="M 400.000 873.000 C 290.000 1133.000, 720 1040, 790 1198" fill="none" stroke="url(#stem)" stroke-width="1.5"/><g transform="translate(771.000 1057.000) rotate(-148.000)"><path d="M 0 0 Q 96.600 -62 210.000 0 Q 96.600 53 0 0" fill="#102d34" fill-opacity="0.7" stroke="#648b83" stroke-width="0.6"/><path d="M -4.333 0 L 11.667 -4.515 M -4.333 0 L 11.667 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 7.333 0 L 23.333 -8.893 M 7.333 0 L 23.333 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 19.000 0 L 35.000 -13.000 M 19.000 0 L 35.000 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 30.667 0 L 46.667 -16.712 M 30.667 0 L 46.667 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 42.333 0 L 58.333 -19.917 M 42.333 0 L 58.333 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 54.000 0 L 70.000 -22.517 M 54.000 0 L 70.000 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 65.667 0 L 81.667 -24.432 M 65.667 0 L 81.667 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 77.333 0 L 93.333 -25.605 M 77.333 0 L 93.333 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 89.000 0 L 105.000 -26.000 M 89.000 0 L 105.000 26.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 100.667 0 L 116.667 -25.605 M 100.667 0 L 116.667 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 112.333 0 L 128.333 -24.432 M 112.333 0 L 128.333 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 124.000 0 L 140.000 -22.517 M 124.000 0 L 140.000 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 135.667 0 L 151.667 -19.917 M 135.667 0 L 151.667 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 147.333 0 L 163.333 -16.712 M 147.333 0 L 163.333 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 159.000 0 L 175.000 -13.000 M 159.000 0 L 175.000 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 170.667 0 L 186.667 -8.893 M 170.667 0 L 186.667 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 182.333 0 L 198.333 -4.515 M 182.333 0 L 198.333 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 0 0 L 210.000 0" stroke="#b3ad86" stroke-width="0.7"/></g><g transform="translate(821.000 1110.000) rotate(-36.000)"><path d="M 0 0 Q 98.900 -62 215.000 0 Q 98.900 53 0 0" fill="#102d34" fill-opacity="0.7" stroke="#648b83" stroke-width="0.6"/><path d="M -4.056 0 L 11.944 -4.515 M -4.056 0 L 11.944 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 7.889 0 L 23.889 -8.893 M 7.889 0 L 23.889 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 19.833 0 L 35.833 -13.000 M 19.833 0 L 35.833 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 31.778 0 L 47.778 -16.712 M 31.778 0 L 47.778 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 43.722 0 L 59.722 -19.917 M 43.722 0 L 59.722 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 55.667 0 L 71.667 -22.517 M 55.667 0 L 71.667 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 67.611 0 L 83.611 -24.432 M 67.611 0 L 83.611 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 79.556 0 L 95.556 -25.605 M 79.556 0 L 95.556 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 91.500 0 L 107.500 -26.000 M 91.500 0 L 107.500 26.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 103.444 0 L 119.444 -25.605 M 103.444 0 L 119.444 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 115.389 0 L 131.389 -24.432 M 115.389 0 L 131.389 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 127.333 0 L 143.333 -22.517 M 127.333 0 L 143.333 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 139.278 0 L 155.278 -19.917 M 139.278 0 L 155.278 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 151.222 0 L 167.222 -16.712 M 151.222 0 L 167.222 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 163.167 0 L 179.167 -13.000 M 163.167 0 L 179.167 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 175.111 0 L 191.111 -8.893 M 175.111 0 L 191.111 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 187.056 0 L 203.056 -4.515 M 187.056 0 L 203.056 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 0 0 L 215.000 0" stroke="#b3ad86" stroke-width="0.7"/></g><g transform="translate(668.000 962.000) rotate(-163.000)"><path d="M 0 0 Q 82.800 -62 180.000 0 Q 82.800 53 0 0" fill="#102d34" fill-opacity="0.7" stroke="#648b83" stroke-width="0.6"/><path d="M -6.000 0 L 10.000 -4.515 M -6.000 0 L 10.000 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 4.000 0 L 20.000 -8.893 M 4.000 0 L 20.000 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 14.000 0 L 30.000 -13.000 M 14.000 0 L 30.000 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 24.000 0 L 40.000 -16.712 M 24.000 0 L 40.000 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 34.000 0 L 50.000 -19.917 M 34.000 0 L 50.000 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 44.000 0 L 60.000 -22.517 M 44.000 0 L 60.000 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 54.000 0 L 70.000 -24.432 M 54.000 0 L 70.000 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 64.000 0 L 80.000 -25.605 M 64.000 0 L 80.000 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 74.000 0 L 90.000 -26.000 M 74.000 0 L 90.000 26.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 84.000 0 L 100.000 -25.605 M 84.000 0 L 100.000 25.605" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 94.000 0 L 110.000 -24.432 M 94.000 0 L 110.000 24.432" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 104.000 0 L 120.000 -22.517 M 104.000 0 L 120.000 22.517" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 114.000 0 L 130.000 -19.917 M 114.000 0 L 130.000 19.917" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 124.000 0 L 140.000 -16.712 M 124.000 0 L 140.000 16.712" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 134.000 0 L 150.000 -13.000 M 134.000 0 L 150.000 13.000" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 144.000 0 L 160.000 -8.893 M 144.000 0 L 160.000 8.893" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 154.000 0 L 170.000 -4.515 M 154.000 0 L 170.000 4.515" stroke="#648b83" stroke-width="0.5" opacity="0.65"/><path d="M 0 0 L 180.000 0" stroke="#b3ad86" stroke-width="0.7"/></g><g transform="translate(1040.000 438.000)"><circle r="354.000" fill="url(#bloom-light)"/><polygon points="26.235,10.500 23.434,13.315 -3.305,11.282 -7.052,-9.032 18.952,-7.425" fill="#193057" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.758,18.815 -25.381,-4.571 -12.893,-10.195 -11.033,-8.298 -7.285,12.017 -8.981,15.353" fill="#212b72" stroke="#96a0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="10.649,40.859 -2.565,36.961 -5.202,17.273 -3.507,13.937 23.232,15.970 16.981,40.305" fill="#143245" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.419,-26.011 19.160,-10.796 -6.844,-12.403 -8.704,-14.301 -9.033,-20.160 -6.305,-29.061 2.241,-34.296 22.556,-33.956" fill="#112f39" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.916,34.400 -37.249,23.462 -34.438,21.199 -8.661,17.737 -6.024,37.425 -22.479,46.260 -24.233,45.843" fill="#11303b" stroke="#96d4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.874,7.087 40.446,9.012 29.905,9.008 22.622,-8.917 30.882,-24.131 31.891,-23.984 52.780,-6.119" fill="#183652" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.739,14.458 -57.079,7.776 -52.630,-10.114 -51.836,-11.142 -27.742,-5.517 -37.118,17.869 -39.930,20.132" fill="#102f36" stroke="#96d9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-48.688,-32.826 -14.922,-19.829 -14.593,-13.970 -27.081,-8.345 -51.175,-13.971 -49.901,-31.606" fill="#232c77" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-47.953,-34.736 -43.270,-45.464 -26.715,-52.242 -11.458,-30.641 -14.187,-21.740" fill="#102f36" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="47.574,45.334 24.361,44.172 20.851,41.300 27.103,16.965 29.903,14.149 40.444,14.153" fill="#1e3568" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.676,-64.301 12.237,-66.526 27.204,-57.475 22.613,-37.394 2.298,-37.735" fill="#103139" stroke="#96dae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.386,-64.505 0.009,-37.939 -8.537,-32.704 -23.794,-54.306 -22.402,-57.307 -10.102,-67.027" fill="#20336f" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.963,-14.205 54.438,-8.058 33.549,-25.922 58.206,-36.120 65.253,-31.493" fill="#12343f" stroke="#96d5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-3.902,78.023 -7.312,78.513 -20.335,50.253 -3.880,41.417 9.334,45.315" fill="#23307a" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="55.617,47.868 49.609,44.869 42.479,13.688 46.907,11.762 66.999,25.313 68.110,33.664 67.110,37.294" fill="#153848" stroke="#96cfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.191,-38.574 32.534,-28.376 31.524,-28.524 26.661,-36.469 31.251,-56.550 36.434,-58.291 50.498,-55.586" fill="#1d3864" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.135,78.738 11.101,46.031 17.433,45.476 20.943,48.349 22.083,70.760 16.272,77.507 8.805,80.733" fill="#183a53" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-66.425,52.818 -75.487,44.349 -75.508,44.280 -55.687,19.198 -41.878,24.872 -38.544,35.811" fill="#262c84" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.627,-6.731 81.334,5.593 68.660,22.849 48.569,9.299 56.475,-3.908 69.000,-10.055" fill="#1a3b59" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.417,54.471 -37.536,37.464 -27.853,48.906 -43.178,62.758 -53.355,63.127 -63.822,57.638" fill="#1c3b5f" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="54.366,59.107 48.665,72.529 25.300,70.597 24.161,48.186 47.373,49.347 53.381,52.346" fill="#173b51" stroke="#96cae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-88.901,-16.363 -57.069,-11.218 -61.519,6.672 -77.668,4.716 -86.280,-3.480" fill="#1f396c" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-85.148,-38.339 -54.666,-31.950 -55.940,-14.315 -56.735,-13.287 -88.567,-18.432 -89.646,-19.985 -88.585,-33.880" fill="#272b88" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-14.336,83.745 -29.736,87.791 -40.890,65.290 -25.565,51.437 -23.811,51.855 -10.787,80.115" fill="#163b4a" stroke="#96d0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-78.428,41.973 -92.576,30.921 -78.096,8.254 -61.947,10.210 -58.607,16.891" fill="#153a47" stroke="#96d4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-44.504,-83.328 -27.052,-59.463 -28.443,-56.462 -44.998,-49.684 -51.966,-59.434 -52.612,-73.346" fill="#233579" stroke="#96a7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-84.386,-41.974 -84.121,-57.741 -54.977,-57.283 -48.008,-47.532 -52.691,-36.805 -53.904,-35.584" fill="#143a45" stroke="#96d7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.748,-96.652 -12.885,-70.549 -25.185,-60.828 -42.638,-84.693 -41.708,-89.670 -30.797,-97.100" fill="#272f85" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="40.491,-90.723 34.790,-63.189 29.607,-61.448 14.640,-70.499 16.024,-77.740 26.447,-90.582" fill="#263084" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.837,-105.761 12.239,-78.464 10.855,-71.223 3.293,-68.997 -9.195,-71.519 -16.058,-97.622" fill="#143b45" stroke="#96d7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="83.593,-69.602 69.260,-36.324 67.778,-35.340 60.731,-39.967 54.038,-56.979 72.230,-73.389 83.405,-69.943" fill="#153c47" stroke="#96d6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="106.050,25.133 72.357,33.098 71.246,24.748 83.919,7.491 105.267,14.997 107.639,21.576" fill="#153d4a" stroke="#96d4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.335,99.855 19.262,80.083 25.074,73.337 48.438,75.269 52.275,95.634 50.444,98.442" fill="#163e4d" stroke="#96d1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.979,-90.208 48.475,-93.864 60.755,-90.056 69.532,-76.379 51.341,-59.969 37.277,-62.674" fill="#24367d" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-45.275,100.973 -47.616,100.267 -52.348,96.318 -53.224,66.728 -43.047,66.359 -31.894,88.860" fill="#183e51" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="102.005,-24.455 77.356,-11.353 71.729,-14.676 70.019,-31.965 71.501,-32.948 91.508,-36.758" fill="#292c8d" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-108.740,17.530 -88.536,-1.110 -79.924,7.086 -94.403,29.754 -107.392,26.256 -108.958,22.215" fill="#193f56" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-80.916,-75.773 -55.591,-73.208 -54.945,-59.296 -84.090,-59.755 -86.875,-67.927 -83.094,-74.271" fill="#1b405c" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-3.203,-106.784 -0.892,-109.927 10.353,-111.374 10.619,-111.287 24.296,-92.328 13.873,-79.486" fill="#1c3f62" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="95.470,-58.920 91.008,-39.384 71.001,-35.574 85.334,-68.853" fill="#1e3f69" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="86.160,70.055 59.971,58.291 58.986,51.530 70.479,40.956 89.263,51.383" fill="#2a2b91" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="106.489,26.989 104.977,41.351 90.579,49.011 71.795,38.585 72.796,34.954" fill="#203d70" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="103.298,-22.023 110.221,-20.409 114.823,-8.522 106.704,10.909 85.356,3.404 78.649,-8.920" fill="#213c73" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.122,101.393 29.660,110.004 18.076,105.412 10.583,84.848 18.049,81.621" fill="#233b77" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-54.399,96.379 -71.917,90.508 -74.631,82.265 -65.742,61.299 -55.275,66.788" fill="#25387f" stroke="#96a7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-3.644,117.720 -10.023,87.960 -6.475,84.329 -3.066,83.839 7.874,85.835 15.368,106.399" fill="#2a2c91" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-110.225,15.921 -117.836,-4.262 -115.074,-8.612 -93.720,-17.155 -92.641,-15.602 -90.020,-2.719" fill="#273486" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.954,72.740 88.238,78.721 81.738,89.724 56.901,94.763 53.064,74.398 58.765,60.976" fill="#1e4069" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-80.676,-78.137 -68.318,-99.761 -66.267,-99.995 -46.314,-90.530 -47.244,-85.553 -55.351,-75.571" fill="#29308d" stroke="#969ce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-101.838,72.656 -79.303,48.432 -70.241,56.901 -68.646,60.068 -77.535,81.034" fill="#29328d" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.678,118.370 -8.741,123.787 -21.575,125.485 -41.501,105.548 -41.840,104.768 -28.458,92.655 -13.058,88.610" fill="#1c435f" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.737,-112.815 40.231,-117.829 45.955,-97.653 40.458,-93.996 26.414,-93.856" fill="#2b2c93" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="111.250,-24.822 104.327,-26.436 93.830,-38.739 98.292,-58.276 100.401,-57.877 121.901,-40.479" fill="#2d2c97" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-104.210,70.449 -107.978,71.675 -117.466,62.356 -111.571,35.293 -108.833,31.607 -95.844,35.105 -81.696,46.157 -81.675,46.225" fill="#1a4459" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-127.045,-39.608 -93.955,-34.290 -95.016,-20.395 -116.370,-11.852" fill="#273985" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="92.272,76.507 88.988,70.525 92.091,51.853 106.489,44.192 108.890,47.647 110.042,75.057" fill="#2e2c98" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-126.511,-42.926 -127.484,-43.594 -125.869,-57.453 -99.220,-66.879 -92.506,-66.007 -89.721,-57.835 -89.985,-42.068 -93.422,-37.609" fill="#194455" stroke="#96d1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.748,138.557 -4.316,125.472 -2.253,120.056 16.759,108.735 28.343,113.327 27.557,119.046" fill="#2c2d97" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-46.820,-131.282 -33.753,-101.440 -44.663,-94.010 -64.616,-103.475 -48.996,-131.416" fill="#243f7b" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-43.632,-132.678 -31.632,-139.908 -8.822,-120.861 -5.983,-113.671 -8.295,-110.527 -19.515,-102.387 -30.565,-102.836" fill="#194555" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-109.780,94.994 -106.456,76.349 -102.689,75.123 -78.386,83.501 -75.672,91.744 -80.857,96.261" fill="#2a3392" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="82.627,-118.282 95.730,-113.168 89.967,-81.653 85.220,-75.829 74.045,-79.275 65.268,-92.951" fill="#194657" stroke="#96d1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="79.646,-120.325 62.287,-94.994 50.007,-98.802 44.283,-118.979 44.454,-119.369 76.635,-125.110" fill="#214570" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-142.736,6.132 -129.092,-2.830 -121.473,-2.891 -113.862,17.292 -114.079,21.977 -144.639,14.941" fill="#2e2d9b" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-82.494,116.421 -78.444,99.030 -73.260,94.513 -55.741,100.384 -51.009,104.333 -76.302,123.811" fill="#2d2e9a" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-100.608,-105.813 -92.234,-109.847 -77.488,-106.796 -71.404,-101.524 -83.762,-79.900 -85.940,-78.398" fill="#312e9d" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.126,136.819 31.331,119.564 32.117,113.846 45.579,105.235 51.689,103.822 52.203,105.069 52.050,136.399" fill="#2c3098" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-149.471,-29.902 -130.388,-39.364 -129.415,-38.696 -118.741,-10.940 -121.502,-6.590 -129.122,-6.529" fill="#283a8a" stroke="#96a5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="21.252,-143.388 24.959,-144.234 31.686,-140.904 39.004,-129.124 39.822,-121.398 39.651,-121.008 12.158,-115.993 11.891,-116.080" fill="#322e9e" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="148.325,-5.041 152.454,9.168 118.744,20.211 113.662,19.404 111.291,12.826 119.409,-6.606" fill="#1b475c" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="125.208,-71.284 131.625,-66.317 132.873,-60.236 129.417,-48.141 125.684,-43.526 123.742,-42.754 102.241,-60.151" fill="#332e9f" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="133.576,52.147 112.242,45.318 109.841,41.863 111.353,27.501 112.941,23.943 118.023,24.750 140.929,41.809" fill="#2b3296" stroke="#969be9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="132.866,54.365 133.345,66.841 129.661,71.156 119.647,75.859 115.378,76.171 112.684,74.946 111.532,47.536" fill="#322e9f" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="119.848,-87.499 124.194,-73.377 101.227,-62.244 99.118,-62.643 88.982,-72.575 88.793,-72.916 93.541,-78.739" fill="#2b3494" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="85.737,131.458 56.493,103.298 55.979,102.051 57.810,99.243 82.647,94.204 95.415,118.306" fill="#1c4862" stroke="#96cae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="148.529,-8.811 119.613,-10.377 115.011,-22.263 125.663,-37.921 127.605,-38.693 152.064,-17.365" fill="#1e4867" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.908,-150.084 19.010,-144.156 9.649,-116.849 -1.596,-115.402 -4.435,-122.593" fill="#2a3592" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="10.369,143.188 9.429,140.264 29.238,120.753 39.032,138.007 26.802,149.347 20.875,149.076 12.455,145.215" fill="#2f2e9d" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-54.469,148.558 -55.327,146.798 -44.453,108.498 -24.527,128.435 -37.703,150.302 -50.878,150.544" fill="#1e4868" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-119.567,-94.069 -102.982,-104.686 -102.757,-104.663 -88.089,-77.248 -91.870,-70.904 -98.584,-71.776" fill="#2a3790" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-149.217,39.882 -147.665,19.741 -145.201,17.379 -114.640,24.416 -113.075,28.457 -115.813,32.143" fill="#29398e" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-151.369,46.808 -148.674,42.226 -115.269,34.488 -121.164,61.550 -144.693,61.644 -150.290,54.584" fill="#20476f" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.437,-142.827 -51.184,-132.640 -66.805,-104.699 -68.856,-104.464 -74.941,-109.736 -71.985,-142.813" fill="#254181" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-57.647,146.140 -74.328,131.470 -74.746,125.832 -49.452,106.354 -47.111,107.060 -46.773,107.839" fill="#283b8b" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-111.118,99.218 -109.875,97.179 -80.952,98.446 -85.002,115.837 -105.385,113.825 -109.397,108.135 -111.247,101.168" fill="#2d309a" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.020,133.241 83.608,134.819 82.603,135.830 60.559,140.446 54.623,136.411 54.777,105.081" fill="#273d88" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-128.616,-91.983 -121.344,-92.397 -100.360,-70.104 -127.009,-60.678 -138.160,-78.509 -136.794,-83.888" fill="#224675" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="150.149,37.168 142.462,39.750 119.556,22.691 153.266,11.648 158.658,17.826 158.771,18.653" fill="#264084" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.522,-157.277 0.503,-150.727 -6.839,-123.235 -29.650,-142.282 -21.969,-158.322 -19.250,-159.481" fill="#24447b" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="121.596,-97.997 119.046,-89.906 92.740,-81.146 98.502,-112.662 111.950,-114.420 113.128,-113.751" fill="#254280" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="98.934,116.442 86.166,92.340 92.667,81.338 110.436,79.888 113.130,81.113 111.508,113.650" fill="#1c4961" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-156.036,-23.517 -153.076,-27.560 -151.826,-28.455 -150.998,-28.573 -130.649,-5.200 -144.293,3.762 -158.585,-15.213" fill="#2b3694" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.502,-151.339 70.880,-151.229 71.663,-150.697 73.642,-148.094 76.194,-127.583 44.013,-121.842 43.194,-129.568" fill="#224776" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-77.106,-142.982 -74.278,-143.094 -73.889,-142.983 -76.845,-109.906 -91.590,-112.957 -87.401,-139.112" fill="#293c8d" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.071,152.490 -20.895,130.623 -8.061,128.925 4.002,142.010 4.943,144.934 -22.042,163.710" fill="#1b4a5f" stroke="#96d0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="168.696,-39.918 169.819,-37.062 168.375,-32.124 153.740,-19.288 129.281,-40.615 133.015,-45.231" fill="#204a6d" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.014,-159.809 69.351,-152.783 42.043,-131.012 34.726,-142.792" fill="#264384" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-144.674,66.196 -121.146,66.102 -111.658,75.422 -114.981,94.067 -116.224,96.106 -147.834,85.847" fill="#1c4c61" stroke="#96cfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="168.965,-41.724 133.284,-47.036 136.740,-59.131 166.076,-55.286 167.960,-51.708" fill="#24487b" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="140.741,112.389 138.494,116.146 131.636,118.625 114.127,113.780 115.749,81.244 120.019,80.931" fill="#1e4c67" stroke="#96cbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-106.075,129.709 -105.674,116.753 -85.291,118.765 -79.100,126.155 -78.682,131.793 -97.144,142.278 -98.007,142.143" fill="#2e339e" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="38.963,162.627 29.034,151.754 41.264,140.414 52.188,139.994 58.124,144.028 53.681,164.091" fill="#2f30a1" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-170.262,10.728 -172.722,6.614 -168.267,-6.745 -160.684,-13.633 -146.391,5.342 -148.294,14.151 -150.759,16.513" fill="#2d369a" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-142.327,-75.904 -131.177,-58.072 -132.792,-44.213 -151.875,-34.751 -152.703,-34.634 -163.537,-66.460" fill="#1e4d67" stroke="#96cbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="152.831,68.145 136.978,66.701 136.499,54.226 143.852,43.888 151.539,41.306 163.516,57.223" fill="#3130a4" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-89.722,-139.483 -93.911,-113.329 -102.285,-109.295 -102.511,-109.318 -112.326,-126.760 -109.482,-135.188 -96.081,-140.072" fill="#2b3a96" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="147.830,-79.469 134.069,-69.474 127.652,-74.441 123.306,-88.564 125.856,-96.655 145.827,-95.851" fill="#3430a6" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="55.916,-162.690 56.760,-161.526 33.471,-144.509 26.744,-147.839 30.840,-165.740 42.813,-170.764" fill="#2a3f90" stroke="#96a7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="142.277,111.377 121.554,79.920 131.568,75.217 150.819,98.648 147.090,106.854" fill="#214c72" stroke="#96c2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.089,-166.370 23.993,-148.469 20.286,-147.622 4.184,-153.550 -0.841,-160.100 12.615,-175.873" fill="#3631a8" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.398,187.111 -7.249,187.684 -15.622,181.979 -20.486,165.946 6.498,147.170 8.584,149.197" fill="#1d4e65" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="166.331,-57.237 136.996,-61.083 135.747,-67.163 149.508,-77.158 164.850,-70.125" fill="#284489" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-114.852,-125.338 -105.037,-107.896 -121.622,-97.279 -128.895,-96.865 -133.630,-118.457" fill="#3831a9" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.765,-160.619 -34.446,-144.579 -46.446,-137.349 -48.622,-137.483 -67.874,-147.670 -48.724,-175.287" fill="#214d71" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-171.129,13.651 -151.626,19.436 -153.177,39.577 -155.873,44.158 -177.849,34.452" fill="#3a32ab" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="152.272,97.454 133.020,74.024 136.704,69.708 152.557,71.152 159.377,90.444" fill="#254981" stroke="#96b6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-0.593,187.634 10.389,149.720 18.809,153.581 16.531,185.265 3.607,188.327" fill="#1f4e6c" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-95.547,145.090 -77.085,134.605 -60.404,149.274 -59.546,151.034 -82.182,164.607" fill="#3b32ac" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="112.492,-149.648 111.204,-120.120 97.757,-118.362 84.654,-123.476 81.643,-128.261 79.091,-148.772" fill="#244b7c" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-155.276,116.817 -158.553,113.464 -158.841,99.915 -148.715,88.562 -117.105,98.821 -117.234,100.771" fill="#1e4f68" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.385,185.398 20.662,153.714 26.590,153.985 36.520,164.859 26.561,186.467" fill="#234c79" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.407,166.884 57.021,164.831 61.464,144.768 83.508,140.152 81.929,168.089" fill="#3d32ae" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="192.407,-1.495 163.324,13.754 157.932,7.576 153.803,-6.632 157.337,-15.186 171.973,-28.022" fill="#274788" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-154.522,118.604 -116.481,102.557 -114.631,109.525 -140.093,131.772 -153.113,121.257" fill="#1f506a" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-138.849,133.196 -113.387,110.949 -109.375,116.638 -109.776,129.595 -134.835,140.048" fill="#214f73" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="186.360,37.766 171.233,55.836 166.382,55.066 154.405,39.150 163.027,20.634" fill="#3e33af" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="120.610,156.495 90.026,136.495 90.438,134.917 100.116,121.765 112.690,118.973 130.199,123.818" fill="#2b4293" stroke="#96a8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-194.215,-53.932 -193.700,-57.568 -193.498,-57.942 -179.633,-68.116 -166.388,-65.490 -155.553,-33.663 -156.804,-32.768" fill="#215071" stroke="#96c7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="145.567,-127.381 151.160,-103.810 145.976,-99.563 126.005,-100.367 117.538,-116.121" fill="#3f33b0" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.848,193.423 -50.768,156.509 -37.593,156.267 -25.564,167.486 -20.700,183.520 -51.614,194.574" fill="#2d3b9d" stroke="#96a0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-200.385,-19.919 -196.982,-28.367 -160.936,-25.022 -163.486,-16.718 -171.070,-9.830" fill="#20516e" stroke="#96cae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.636,-196.369 9.834,-186.335 9.726,-178.338 -3.730,-162.565 -18.459,-164.769" fill="#4134b2" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="179.465,70.914 181.537,85.584 161.836,89.574 155.016,70.283 165.701,59.361 170.552,60.130" fill="#284989" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.685,187.446 38.644,165.838 53.361,167.302 54.747,169.355 53.322,183.642 41.922,196.677" fill="#264b84" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="179.398,-92.491 166.072,-72.791 150.730,-79.823 148.727,-96.205 153.911,-100.452 167.892,-99.659" fill="#29468d" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-195.207,-52.179 -157.796,-31.015 -160.755,-26.972 -196.801,-30.317 -196.392,-49.609" fill="#1f516c" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-134.009,142.028 -108.950,131.575 -100.882,144.008 -111.852,153.587 -132.511,153.838" fill="#254e7e" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="37.508,-201.033 41.539,-173.799 29.566,-168.775 14.093,-178.279 14.201,-186.276 24.286,-196.546" fill="#2a4491" stroke="#96abe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-168.005,-109.247 -144.238,-123.246 -137.371,-117.637 -132.636,-96.044 -140.814,-87.950" fill="#4234b3" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-188.041,72.641 -155.060,58.367 -149.463,65.426 -152.623,85.077 -162.749,96.430 -186.813,80.038" fill="#3035a5" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-137.270,-157.241 -112.959,-136.361 -115.803,-127.933 -134.581,-121.052 -141.448,-126.662 -142.925,-141.781" fill="#2b4394" stroke="#96a8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-189.315,69.697 -191.211,67.486 -188.449,40.179 -179.390,37.940 -157.414,47.647 -156.334,55.423" fill="#4334b4" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-74.385,-194.789 -55.036,-188.054 -51.348,-177.107 -70.498,-149.490 -72.046,-149.476 -72.436,-149.586 -77.762,-193.768 -76.538,-194.460" fill="#244f7d" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-218.835,1.861 -201.049,-17.990 -171.734,-7.900 -176.188,5.458 -214.416,5.316" fill="#235079" stroke="#96c1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-171.337,-106.769 -169.991,-106.711 -142.800,-85.414 -144.166,-80.035 -165.376,-70.592 -178.622,-73.218 -179.440,-98.371" fill="#3432ab" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-57.104,193.152 -64.712,193.670 -83.689,176.566 -80.253,167.825 -57.616,154.252 -54.025,156.237" fill="#4334b4" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-214.428,8.551 -176.200,8.693 -173.740,12.808 -180.461,33.608 -189.520,35.847 -203.241,26.832" fill="#2c4197" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="118.745,159.347 119.456,161.592 113.393,171.031 90.704,175.731 85.577,168.295 87.156,140.358 88.161,139.347" fill="#4334b4" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.107,-200.908 -16.894,-196.866 -21.716,-165.267 -24.435,-164.108 -46.393,-178.776 -50.082,-189.724 -33.416,-202.777" fill="#3a33af" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-113.125,-175.937 -101.917,-179.078 -78.997,-148.010 -89.291,-144.139 -95.650,-144.728" fill="#20526e" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="194.087,1.711 200.197,3.052 205.146,16.099 196.646,34.654 188.451,34.918 165.118,17.786 165.005,16.960" fill="#4234b4" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="147.691,-136.422 144.297,-130.542 116.268,-119.282 115.089,-119.951 116.377,-149.479 122.530,-156.336 139.240,-151.662" fill="#3f34b2" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-136.586,-175.663 -114.892,-174.948 -97.417,-143.738 -110.818,-138.854 -135.129,-159.734" fill="#225274" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-87.900,174.911 -99.936,178.990 -109.662,156.096 -98.692,146.517 -97.828,146.652 -84.463,166.169" fill="#2c4199" stroke="#96a5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-79.850,-193.516 -74.524,-149.334 -77.352,-149.223 -100.273,-180.291 -83.453,-192.720" fill="#225275" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="112.562,-182.711 118.556,-159.902 112.403,-153.045 79.002,-152.168 77.023,-154.772 110.304,-184.917" fill="#284a8b" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="86.912,178.346 82.516,191.532 56.837,183.993 58.262,169.705 81.784,170.911" fill="#2d409b" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.091,-220.877 67.989,-206.598 58.102,-166.237 44.999,-174.312 40.968,-201.546" fill="#215372" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.013,-206.102 82.243,-196.936 72.684,-157.442 72.307,-157.552 60.970,-164.578 60.127,-165.742" fill="#215372" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="205.853,52.221 182.389,68.497 173.476,57.713 188.603,39.644 196.798,39.380" fill="#2d409d" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="108.822,-186.553 75.540,-156.409 74.757,-156.940 84.316,-196.434 105.066,-190.522 108.082,-187.957" fill="#265081" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="216.307,-94.450 212.284,-78.309 172.628,-54.166 170.744,-57.744 169.263,-70.632 182.588,-90.333" fill="#215471" stroke="#96cbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="219.063,-21.929 201.386,-2.364 195.276,-3.705 174.842,-30.233 176.286,-35.171 176.555,-35.195 219.461,-24.978" fill="#2d429b" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="169.978,-133.845 168.122,-103.724 154.141,-104.517 148.548,-128.088 151.941,-133.968" fill="#2e409e" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="213.424,-76.436 213.613,-56.764 176.165,-39.477 175.897,-39.452 174.774,-42.309 173.769,-52.293" fill="#24537b" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="237.092,127.631 226.389,129.808 151.858,109.020 155.587,100.814 162.692,93.804 182.394,89.814" fill="#225575" stroke="#96c8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="153.400,164.726 125.028,159.828 124.317,157.582 133.906,124.906 140.764,122.427 141.598,123.411 156.360,163.057" fill="#3138a8" stroke="#969be9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-152.902,197.597 -132.469,157.260 -111.811,157.009 -102.085,179.903 -105.926,183.766" fill="#255281" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.626,-220.374 21.192,-199.584 11.107,-189.313 -12.363,-199.348 -14.576,-203.389" fill="#2e419f" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="220.001,-27.246 177.095,-37.464 214.543,-54.751 222.413,-34.968 222.221,-31.640" fill="#2a4a92" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.155,254.835 44.800,199.194 56.200,186.160 81.880,193.699 82.590,199.811" fill="#265283" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-179.667,-134.519 -147.466,-141.338 -145.989,-126.218 -169.755,-112.220 -171.102,-112.277" fill="#2f41a0" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.399,225.915 -50.228,198.451 -19.314,187.397 -10.941,193.103 -11.346,194.544 -35.505,227.633" fill="#3a34b4" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="225.764,132.049 189.658,144.542 145.005,120.527 144.172,119.542 146.420,115.785 151.233,111.262" fill="#285089" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="51.952,280.812 42.940,272.816 4.812,193.415 17.737,190.352 25.913,191.422 39.150,200.652 53.504,256.293" fill="#24557e" stroke="#96c2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="248.495,123.547 240.426,122.810 185.727,84.993 183.655,70.322 207.119,54.046 212.743,57.359" fill="#275285" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-219.956,61.914 -205.843,30.793 -192.122,39.807 -194.884,67.114" fill="#2f41a2" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="188.572,146.562 166.898,161.023 158.681,162.193 143.919,122.547" fill="#2f40a3" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-207.107,106.305 -189.279,83.659 -165.215,100.051 -164.927,113.599 -165.781,113.870 -205.832,111.884" fill="#4636bc" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="240.674,-126.030 215.564,-100.537 181.845,-96.419 170.339,-103.588 172.195,-133.708 178.212,-136.355" fill="#275387" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-85.839,216.165 -102.219,187.451 -98.378,183.588 -86.342,179.509 -67.364,196.613" fill="#2f41a3" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-195.629,224.315 -198.200,221.922 -197.538,214.676 -156.412,125.341 -143.391,135.857 -139.378,142.709 -137.880,154.519 -158.312,194.856 -192.781,225.595" fill="#28528b" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="40.718,273.882 -0.460,245.364 -6.865,195.803 -6.460,194.362 -1.609,193.789 2.591,194.482" fill="#2d489a" stroke="#96abe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-211.941,-102.588 -184.084,-98.220 -183.266,-73.067 -197.131,-62.892 -217.016,-96.650" fill="#4d38c0" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="52.023,-225.104 35.900,-205.772 22.677,-201.285 -1.141,-222.075 0.023,-228.237 51.794,-277.493" fill="#275388" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.801,245.666 -25.633,237.649 -33.366,229.195 -9.207,196.106" fill="#3434b2" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="122.220,202.746 87.882,199.196 87.172,193.084 91.568,179.898 114.256,175.198" fill="#3041a5" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-45.813,-232.836 -36.444,-206.643 -53.110,-193.589 -72.459,-200.325 -56.262,-233.570" fill="#4e38c2" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="236.659,24.475 237.716,27.446 215.480,52.712 209.856,49.399 200.801,36.557 209.301,18.002" fill="#3040a7" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-143.751,-175.007 -142.294,-159.078 -147.949,-143.619 -180.150,-136.800 -184.166,-140.661 -195.106,-226.864 -194.473,-227.119" fill="#285489" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-301.361,-25.753 -201.692,-49.721 -202.101,-30.429 -205.504,-21.981 -223.290,-2.130 -297.299,-4.909" fill="#2d4a9b" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-199.891,213.593 -201.104,151.424 -164.306,118.522 -163.452,118.252 -160.174,121.605 -158.765,124.258" fill="#323cac" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="159.216,-177.301 140.632,-156.638 123.922,-161.313 117.928,-184.121 151.929,-188.797" fill="#4b38c1" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="162.107,-174.700 168.628,-172.759 176.029,-141.320 170.011,-138.674 151.975,-138.797 143.524,-154.037" fill="#313fa9" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-202.720,149.616 -209.378,128.510 -205.973,114.729 -165.922,116.715" fill="#4637be" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="237.563,20.652 210.206,14.179 205.256,1.133 222.934,-18.432 243.785,4.755" fill="#4637be" stroke="#9f96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-297.681,5.270 -223.672,8.049 -219.253,11.504 -208.066,29.785 -222.178,60.906 -226.438,61.980 -301.424,21.442" fill="#285589" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-42.106,-234.162 -33.512,-240.755 -5.314,-229.246 -6.479,-223.084 -18.429,-206.099 -32.737,-207.969" fill="#323dac" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="126.102,201.628 126.099,201.625 118.135,174.077 124.198,164.639 152.570,169.537 146.099,196.362" fill="#3f36bb" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-211.310,-106.611 -211.785,-121.694 -187.931,-136.743 -183.916,-132.883 -175.350,-110.641 -183.453,-102.243" fill="#333aaf" stroke="#969be9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-83.544,221.543 -82.790,219.046 -64.315,199.494 -56.708,198.975 -54.474,200.126 -43.645,227.590 -65.671,236.155" fill="#3835b7" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-210.476,103.653 -226.642,92.904 -225.103,67.271 -220.844,66.197 -195.772,71.398 -193.876,73.609 -192.648,81.006" fill="#3437b3" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-162.678,-259.673 -86.649,-197.045 -103.469,-184.617 -114.677,-181.476 -136.371,-182.191 -187.092,-234.303" fill="#323ead" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-185.909,233.302 -151.440,202.563 -104.464,188.732 -88.084,217.446 -88.838,219.942 -163.843,257.728" fill="#28568a" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-302.047,-30.670 -247.569,-70.990 -200.622,-58.549 -201.137,-54.913 -202.322,-52.343 -301.992,-28.375 -302.123,-28.795" fill="#3f37bc" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-246.931,-73.398 -235.171,-89.752 -219.667,-95.088 -199.782,-61.331 -199.984,-60.957" fill="#513ac6" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="76.919,292.408 66.050,290.119 62.611,281.487 64.163,256.968 87.598,201.944 121.936,205.494 121.939,205.497 111.400,287.930" fill="#28578b" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-91.758,-246.080 -73.257,-246.388 -59.258,-235.030 -75.455,-201.784 -77.607,-201.455" fill="#553cc6" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="112.538,-289.865 106.567,-195.794 85.818,-201.705 73.588,-210.872 62.690,-225.150 62.461,-277.540 67.039,-293.622 76.966,-295.526" fill="#3e37bd" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-156.981,-266.439 -94.207,-245.303 -80.056,-200.678 -81.280,-199.986 -84.882,-199.190 -160.911,-261.817 -157.816,-265.914" fill="#513bc6" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="285.633,111.957 269.635,126.748 253.463,120.863 217.711,54.675 239.947,29.409 301.216,81.490" fill="#29588c" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="143.522,-225.158 154.140,-211.330 151.437,-192.375 117.436,-187.699 115.178,-189.905 114.438,-191.308" fill="#4e3ac6" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="262.477,-166.913 265.508,-144.167 241.616,-131.732 179.155,-142.056 171.754,-173.495 246.006,-197.050" fill="#29598d" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="304.551,-83.462 227.622,-37.040 219.752,-56.823 219.562,-76.495 223.586,-92.635 248.696,-118.129 272.588,-130.564 289.784,-114.362" fill="#543dc7" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="123.936,-289.416 141.496,-226.898 112.413,-193.048 109.397,-195.614 115.368,-289.686 123.892,-289.436" fill="#5b40c8" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="269.876,-21.666 265.157,-5.247 246.691,2.141 225.840,-21.046 226.238,-24.095 228.458,-28.489" fill="#4338c2" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-14.747,-317.669 20.296,-314.386 52.349,-297.804 47.771,-281.722 -4.000,-232.465 -32.198,-243.975" fill="#295a8e" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="182.475,204.178 166.042,210.246 150.200,197.352 156.671,170.526 159.631,168.857 167.847,167.687" fill="#3638bb" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-227.082,-213.477 -204.193,-225.627 -200.881,-226.131 -189.941,-139.928 -213.794,-124.879 -229.890,-139.977" fill="#2a5a8f" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="311.669,-72.474 270.332,-24.431 228.914,-31.254 229.106,-34.582 306.035,-81.004" fill="#563fc7" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-67.885,260.943 -64.027,240.380 -42.002,231.815 -38.109,233.533 -30.376,241.987 -52.295,271.295" fill="#3441b3" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-309.481,53.970 -307.022,32.203 -304.152,26.489 -229.167,67.027 -230.705,92.660 -251.141,99.245" fill="#2a5b91" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-161.326,262.724 -86.321,224.939 -68.447,239.551 -72.305,260.114 -143.828,281.839 -157.195,270.396" fill="#2b5a93" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-249.762,103.526 -229.326,96.941 -213.160,107.689 -211.884,113.268 -215.290,127.050 -248.904,124.085" fill="#3249ac" stroke="#96a5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="116.762,288.616 127.301,206.183 147.298,200.917 163.140,213.811 139.581,286.263 127.007,289.772" fill="#2b5a95" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-233.029,-136.631 -216.933,-121.532 -216.458,-106.449 -221.534,-100.511 -237.038,-95.175 -251.077,-122.448" fill="#304fa6" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="246.455,211.593 185.234,203.072 170.607,166.581 192.280,152.120 228.386,139.627 239.089,137.451 247.159,138.187 263.331,144.072 263.074,169.949" fill="#463ac6" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="304.576,77.538 243.306,25.457 242.249,22.486 248.471,6.589 266.936,-0.800 314.469,61.116 311.874,69.103" fill="#2c5998" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-28.796,-321.975 -19.371,-318.764 -36.822,-245.070 -45.416,-238.477 -55.866,-239.211 -69.865,-250.569" fill="#2e54a0" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="244.506,-201.779 170.254,-178.224 163.734,-180.164 156.447,-191.661 159.150,-210.616 239.240,-215.560 241.605,-212.597" fill="#2d579c" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-25.298,328.294 -49.745,273.202 -27.826,243.894 -4.994,251.911 36.183,280.430 45.195,288.426 48.635,297.059 18.288,318.499" fill="#373bbd" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-283.160,175.581 -249.206,127.506 -215.592,130.471 -208.933,151.577 -207.720,213.746 -208.383,220.992 -250.697,206.218" fill="#3447b3" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-311.846,-126.176 -254.384,-120.746 -240.345,-93.472 -252.105,-77.118 -306.583,-36.798 -319.478,-82.046" fill="#3152aa" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.739,-324.247 -32.291,-323.985 -73.360,-252.580 -91.861,-252.271 -154.635,-273.407 -124.015,-310.740 -80.572,-329.411" fill="#2f59a2" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="148.263,-228.799 130.703,-291.317 179.867,-286.933 219.155,-260.337 238.971,-219.915 158.881,-214.971" fill="#2e5e9d" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="341.053,-24.330 339.576,23.291 318.199,58.252 270.667,-3.663 275.386,-20.082 316.723,-68.125 317.405,-67.594" fill="#2d619a" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="222.398,262.770 181.054,286.988 144.375,287.822 167.934,215.370 184.367,209.302 245.588,217.823 245.497,219.498" fill="#2c6298" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-255.776,-208.791 -233.030,-213.704 -235.838,-140.205 -253.885,-126.022 -311.348,-131.453 -311.730,-134.078 -294.725,-178.964" fill="#2c6298" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.708,330.694 -32.672,331.882 -81.213,335.858 -124.021,313.567 -142.268,286.975 -70.745,265.251 -55.155,275.602" fill="#2c6398" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-323.320,84.198 -312.960,58.453 -254.620,103.728 -253.762,124.288 -287.717,172.362 -290.102,171.248 -319.245,132.690" fill="#2c6398" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><g fill="none" stroke="#d1e7ed" stroke-width="0.55" opacity="0.22"><polygon points="-21.627,8.931 8.992,3.282 7.439,23.705" /><polygon points="8.992,3.282 -21.627,8.931 10.594,-22.648" /><polygon points="-21.627,8.931 7.439,23.705 -19.164,27.268" /><polygon points="8.992,3.282 37.213,31.353 7.439,23.705" /><polygon points="37.228,-8.190 8.992,3.282 10.594,-22.648" /><polygon points="-21.627,8.931 -34.704,-20.105 10.594,-22.648" /><polygon points="-34.704,-20.105 -21.627,8.931 -39.784,1.651" /><polygon points="10.594,-22.648 -34.704,-20.105 -29.048,-34.799" /><polygon points="37.213,31.353 8.992,3.282 37.228,-8.190" /><polygon points="10.594,-22.648 -6.574,-50.672 11.037,-49.096" /><polygon points="-6.574,-50.672 10.594,-22.648 -29.048,-34.799" /><polygon points="37.228,-8.190 42.171,-41.979 49.979,-23.101" /><polygon points="42.171,-41.979 37.228,-8.190 10.594,-22.648" /><polygon points="-19.164,27.268 7.439,23.705 -2.672,57.981" /><polygon points="37.213,31.353 37.228,-8.190 52.865,27.774" /><polygon points="42.171,-41.979 10.594,-22.648 11.037,-49.096" /><polygon points="-2.672,57.981 7.439,23.705 10.920,63.482" /><polygon points="10.920,63.482 7.439,23.705 37.213,31.353" /><polygon points="-19.164,27.268 -39.784,1.651 -21.627,8.931" /><polygon points="-39.784,1.651 -19.164,27.268 -54.767,38.117" /><polygon points="52.865,27.774 37.228,-8.190 65.646,8.823" /><polygon points="65.646,8.823 37.228,-8.190 49.979,-23.101" /><polygon points="-54.767,38.117 -19.164,27.268 -47.011,50.832" /><polygon points="-19.164,27.268 -29.410,70.304 -47.011,50.832" /><polygon points="10.920,63.482 37.213,31.353 35.668,62.224" /><polygon points="-39.784,1.651 -77.226,20.369 -73.931,-6.841" /><polygon points="-39.784,1.651 -71.359,-22.754 -34.704,-20.105" /><polygon points="-71.359,-22.754 -39.784,1.651 -73.931,-6.841" /><polygon points="-29.410,70.304 -19.164,27.268 -2.672,57.981" /><polygon points="-77.226,20.369 -39.784,1.651 -54.767,38.117" /><polygon points="-6.574,-50.672 -29.048,-34.799 -42.340,-67.260" /><polygon points="-29.048,-34.799 -34.704,-20.105 -65.498,-50.710" /><polygon points="-42.340,-67.260 -29.048,-34.799 -65.498,-50.710" /><polygon points="-65.498,-50.710 -34.704,-20.105 -71.359,-22.754" /><polygon points="-6.574,-50.672 -42.340,-67.260 -27.982,-77.761" /><polygon points="42.171,-41.979 11.037,-49.096 29.518,-79.657" /><polygon points="11.037,-49.096 -6.574,-50.672 0.403,-85.223" /><polygon points="29.518,-79.657 11.037,-49.096 0.403,-85.223" /><polygon points="0.403,-85.223 -6.574,-50.672 -27.982,-77.761" /><polygon points="49.979,-23.101 42.171,-41.979 69.405,-52.692" /><polygon points="42.171,-41.979 48.655,-75.695 69.405,-52.692" /><polygon points="52.865,27.774 65.646,8.823 85.531,23.427" /><polygon points="10.920,63.482 35.668,62.224 33.925,83.299" /><polygon points="48.655,-75.695 42.171,-41.979 29.518,-79.657" /><polygon points="-47.011,50.832 -29.410,70.304 -46.008,78.531" /><polygon points="65.646,8.823 49.979,-23.101 86.641,-26.727" /><polygon points="86.641,-26.727 49.979,-23.101 69.405,-52.692" /><polygon points="-73.931,-6.841 -77.226,20.369 -91.282,11.390" /><polygon points="-42.340,-67.260 -65.498,-50.710 -65.255,-66.197" /><polygon points="29.518,-79.657 0.403,-85.223 12.973,-93.086" /><polygon points="86.641,-26.727 69.405,-52.692 82.795,-46.925" /><polygon points="35.668,62.224 52.865,27.774 78.781,55.944" /><polygon points="52.865,27.774 35.668,62.224 37.213,31.353" /><polygon points="52.865,27.774 88.906,37.703 78.781,55.944" /><polygon points="88.906,37.703 52.865,27.774 85.531,23.427" /><polygon points="65.646,8.823 86.641,-26.727 96.587,-8.017" /><polygon points="85.531,23.427 65.646,8.823 96.587,-8.017" /><polygon points="10.920,63.482 33.925,83.299 24.595,95.130" /><polygon points="-47.011,50.832 -46.008,78.531 -61.780,78.998" /><polygon points="-2.672,57.981 10.920,63.482 3.761,102.721" /><polygon points="-29.410,70.304 -2.672,57.981 3.761,102.721" /><polygon points="3.761,102.721 10.920,63.482 24.595,95.130" /><polygon points="-73.931,-6.841 -91.282,11.390 -102.700,-0.987" /><polygon points="35.668,62.224 78.781,55.944 69.505,76.596" /><polygon points="33.925,83.299 35.668,62.224 69.505,76.596" /><polygon points="-42.340,-67.260 -65.255,-66.197 -63.413,-84.378" /><polygon points="-54.767,38.117 -47.011,50.832 -84.121,69.526" /><polygon points="-54.767,38.117 -102.367,52.552 -77.226,20.369" /><polygon points="-84.121,69.526 -47.011,50.832 -61.780,78.998" /><polygon points="-29.410,70.304 3.761,102.721 -19.581,107.725" /><polygon points="-46.008,78.531 -29.410,70.304 -19.581,107.725" /><polygon points="29.518,-79.657 12.973,-93.086 29.266,-104.839" /><polygon points="86.641,-26.727 82.795,-46.925 104.502,-41.967" /><polygon points="-102.367,52.552 -54.767,38.117 -84.121,69.526" /><polygon points="-91.282,11.390 -77.226,20.369 -102.367,52.552" /><polygon points="-71.359,-22.754 -102.700,-0.987 -112.671,-25.908" /><polygon points="-102.700,-0.987 -71.359,-22.754 -73.931,-6.841" /><polygon points="78.781,55.944 88.906,37.703 100.534,59.559" /><polygon points="-65.498,-50.710 -71.359,-22.754 -108.569,-51.433" /><polygon points="-65.255,-66.197 -65.498,-50.710 -108.569,-51.433" /><polygon points="-108.569,-51.433 -71.359,-22.754 -112.671,-25.908" /><polygon points="3.761,102.721 24.595,95.130 14.462,120.691" /><polygon points="-27.982,-77.761 -63.413,-84.378 -50.715,-111.145" /><polygon points="-63.413,-84.378 -27.982,-77.761 -42.340,-67.260" /><polygon points="0.403,-85.223 -27.982,-77.761 -26.192,-121.883" /><polygon points="-26.192,-121.883 -27.982,-77.761 -50.715,-111.145" /><polygon points="-84.121,69.526 -61.780,78.998 -90.664,88.506" /><polygon points="69.405,-52.692 48.655,-75.695 83.364,-97.969" /><polygon points="48.655,-75.695 29.266,-104.839 60.436,-113.682" /><polygon points="83.364,-97.969 48.655,-75.695 60.436,-113.682" /><polygon points="29.266,-104.839 48.655,-75.695 29.518,-79.657" /><polygon points="-102.700,-0.987 -91.282,11.390 -130.676,9.563" /><polygon points="-102.700,-0.987 -130.905,-18.895 -112.671,-25.908" /><polygon points="-91.282,11.390 -134.995,28.322 -130.676,9.563" /><polygon points="-61.780,78.998 -46.008,78.531 -72.105,109.807" /><polygon points="-90.664,88.506 -61.780,78.998 -72.105,109.807" /><polygon points="-46.008,78.531 -60.134,125.352 -72.105,109.807" /><polygon points="-63.413,-84.378 -65.255,-66.197 -87.147,-97.942" /><polygon points="-63.413,-84.378 -67.552,-120.558 -50.715,-111.145" /><polygon points="-65.255,-66.197 -103.678,-89.098 -87.147,-97.942" /><polygon points="24.595,95.130 33.925,83.299 43.497,124.680" /><polygon points="14.462,120.691 24.595,95.130 43.497,124.680" /><polygon points="33.925,83.299 76.497,111.062 43.497,124.680" /><polygon points="-130.905,-18.895 -102.700,-0.987 -130.676,9.563" /><polygon points="29.266,-104.839 12.973,-93.086 24.806,-129.290" /><polygon points="60.436,-113.682 29.266,-104.839 24.806,-129.290" /><polygon points="85.531,23.427 96.587,-8.017 131.865,6.723" /><polygon points="104.502,-41.967 110.853,-75.559 118.657,-59.460" /><polygon points="88.906,37.703 85.531,23.427 126.320,41.641" /><polygon points="100.534,59.559 88.906,37.703 126.320,41.641" /><polygon points="126.320,41.641 85.531,23.427 131.865,6.723" /><polygon points="100.534,59.559 126.320,41.641 120.857,58.705" /><polygon points="82.795,-46.925 69.405,-52.692 110.853,-75.559" /><polygon points="110.853,-75.559 104.502,-41.967 82.795,-46.925" /><polygon points="110.853,-75.559 69.405,-52.692 83.364,-97.969" /><polygon points="76.497,111.062 33.925,83.299 69.505,76.596" /><polygon points="96.587,-8.017 104.502,-41.967 133.436,-22.283" /><polygon points="131.865,6.723 96.587,-8.017 133.436,-22.283" /><polygon points="104.502,-41.967 96.587,-8.017 86.641,-26.727" /><polygon points="133.436,-22.283 104.502,-41.967 118.657,-59.460" /><polygon points="12.973,-93.086 -26.192,-121.883 7.554,-135.204" /><polygon points="24.806,-129.290 12.973,-93.086 7.554,-135.204" /><polygon points="-26.192,-121.883 12.973,-93.086 0.403,-85.223" /><polygon points="14.462,120.691 43.497,124.680 27.394,133.821" /><polygon points="-19.581,107.725 -60.134,125.352 -46.008,78.531" /><polygon points="-103.678,-89.098 -65.255,-66.197 -108.569,-51.433" /><polygon points="-134.995,28.322 -91.282,11.390 -102.367,52.552" /><polygon points="-134.995,28.322 -102.367,52.552 -130.818,46.355" /><polygon points="-67.552,-120.558 -63.413,-84.378 -87.147,-97.942" /><polygon points="-60.134,125.352 -19.581,107.725 -42.288,130.419" /><polygon points="-90.664,88.506 -72.105,109.807 -91.400,105.314" /><polygon points="43.497,124.680 76.497,111.062 63.290,124.777" /><polygon points="-103.678,-89.098 -108.569,-51.433 -117.342,-76.237" /><polygon points="126.320,41.641 131.865,6.723 138.115,25.801" /><polygon points="7.554,-135.204 -26.192,-121.883 -10.944,-140.144" /><polygon points="110.853,-75.559 83.364,-97.969 104.689,-94.070" /><polygon points="69.505,76.596 100.534,59.559 103.567,96.722" /><polygon points="76.497,111.062 69.505,76.596 103.567,96.722" /><polygon points="100.534,59.559 69.505,76.596 78.781,55.944" /><polygon points="103.567,96.722 100.534,59.559 120.857,58.705" /><polygon points="-130.905,-18.895 -130.676,9.563 -142.651,-8.669" /><polygon points="60.436,-113.682 24.806,-129.290 57.042,-132.705" /><polygon points="-67.552,-120.558 -87.147,-97.942 -82.198,-121.867" /><polygon points="-19.581,107.725 14.462,120.691 -14.350,147.253" /><polygon points="-42.288,130.419 -19.581,107.725 -14.350,147.253" /><polygon points="14.462,120.691 -19.581,107.725 3.761,102.721" /><polygon points="133.436,-22.283 118.657,-59.460 146.332,-37.073" /><polygon points="57.042,-132.705 24.806,-129.290 48.186,-143.813" /><polygon points="-102.367,52.552 -90.664,88.506 -130.678,81.374" /><polygon points="-130.818,46.355 -102.367,52.552 -130.678,81.374" /><polygon points="-90.664,88.506 -102.367,52.552 -84.121,69.526" /><polygon points="146.332,-37.073 118.657,-59.460 148.400,-50.961" /><polygon points="103.567,96.722 120.857,58.705 123.713,97.726" /><polygon points="-91.400,105.314 -72.105,109.807 -93.623,127.834" /><polygon points="-93.623,127.834 -72.105,109.807 -60.134,125.352" /><polygon points="27.394,133.821 43.497,124.680 44.561,152.335" /><polygon points="44.561,152.335 43.497,124.680 63.290,124.777" /><polygon points="-142.651,-8.669 -130.676,9.563 -158.791,3.489" /><polygon points="-158.791,3.489 -130.676,9.563 -134.995,28.322" /><polygon points="-108.569,-51.433 -130.905,-18.895 -149.397,-56.192" /><polygon points="-117.342,-76.237 -108.569,-51.433 -149.397,-56.192" /><polygon points="-130.905,-18.895 -108.569,-51.433 -112.671,-25.908" /><polygon points="120.857,58.705 126.320,41.641 148.805,57.633" /><polygon points="120.857,58.705 135.527,89.944 123.713,97.726" /><polygon points="148.805,57.633 126.320,41.641 138.115,25.801" /><polygon points="-82.198,-121.867 -87.147,-97.942 -100.051,-124.727" /><polygon points="118.657,-59.460 110.853,-75.559 137.458,-83.746" /><polygon points="118.657,-59.460 150.367,-65.971 148.400,-50.961" /><polygon points="48.186,-143.813 24.806,-129.290 38.534,-157.023" /><polygon points="120.857,58.705 146.699,80.764 135.527,89.944" /><polygon points="24.806,-129.290 7.554,-135.204 17.370,-161.866" /><polygon points="38.534,-157.023 24.806,-129.290 17.370,-161.866" /><polygon points="-14.350,147.253 27.394,133.821 -2.382,164.453" /><polygon points="27.394,133.821 -14.350,147.253 14.462,120.691" /><polygon points="27.394,133.821 11.504,168.475 -2.382,164.453" /><polygon points="150.367,-65.971 118.657,-59.460 137.458,-83.746" /><polygon points="-103.678,-89.098 -100.051,-124.727 -87.147,-97.942" /><polygon points="-100.051,-124.727 -103.678,-89.098 -119.485,-113.791" /><polygon points="-26.192,-121.883 -50.715,-111.145 -47.839,-157.812" /><polygon points="-10.944,-140.144 -26.192,-121.883 -47.839,-157.812" /><polygon points="-47.839,-157.812 -50.715,-111.145 -67.552,-120.558" /><polygon points="-158.791,3.489 -134.995,28.322 -165.460,25.974" /><polygon points="146.699,80.764 120.857,58.705 148.805,57.633" /><polygon points="27.394,133.821 25.762,169.500 11.504,168.475" /><polygon points="-93.623,127.834 -60.134,125.352 -81.339,149.465" /><polygon points="83.364,-97.969 60.436,-113.682 98.956,-137.919" /><polygon points="104.689,-94.070 83.364,-97.969 98.956,-137.919" /><polygon points="98.956,-137.919 60.436,-113.682 57.042,-132.705" /><polygon points="-130.678,81.374 -91.400,105.314 -137.455,102.258" /><polygon points="-91.400,105.314 -130.678,81.374 -90.664,88.506" /><polygon points="-91.400,105.314 -131.658,116.002 -137.455,102.258" /><polygon points="25.762,169.500 27.394,133.821 44.561,152.335" /><polygon points="44.561,152.335 63.290,124.777 70.252,158.024" /><polygon points="131.865,6.723 133.436,-22.283 174.004,-5.522" /><polygon points="138.115,25.801 131.865,6.723 174.004,-5.522" /><polygon points="174.004,-5.522 133.436,-22.283 146.332,-37.073" /><polygon points="-91.400,105.314 -122.089,126.953 -131.658,116.002" /><polygon points="-122.089,126.953 -91.400,105.314 -93.623,127.834" /><polygon points="148.805,57.633 138.115,25.801 170.850,41.044" /><polygon points="138.115,25.801 186.934,19.138 170.850,41.044" /><polygon points="76.497,111.062 103.567,96.722 112.660,137.671" /><polygon points="112.660,137.671 103.567,96.722 123.713,97.726" /><polygon points="-149.397,-56.192 -142.651,-8.669 -171.326,-48.727" /><polygon points="-142.651,-8.669 -149.397,-56.192 -130.905,-18.895" /><polygon points="104.689,-94.070 137.458,-83.746 110.853,-75.559" /><polygon points="137.458,-83.746 104.689,-94.070 138.608,-112.302" /><polygon points="104.689,-94.070 128.840,-136.616 138.608,-112.302" /><polygon points="-42.288,130.419 -14.350,147.253 -41.446,176.304" /><polygon points="-41.446,176.304 -14.350,147.253 -2.382,164.453" /><polygon points="-142.651,-8.669 -178.955,-35.241 -171.326,-48.727" /><polygon points="-180.347,-20.243 -142.651,-8.669 -158.791,3.489" /><polygon points="-10.944,-140.144 17.370,-161.866 7.554,-135.204" /><polygon points="17.370,-161.866 -10.944,-140.144 -4.855,-180.827" /><polygon points="-10.944,-140.144 -29.911,-184.651 -4.855,-180.827" /><polygon points="146.699,80.764 148.805,57.633 165.613,74.077" /><polygon points="165.613,74.077 148.805,57.633 170.850,41.044" /><polygon points="25.762,169.500 44.561,152.335 42.104,177.032" /><polygon points="42.104,177.032 44.561,152.335 70.252,158.024" /><polygon points="150.367,-65.971 137.458,-83.746 159.766,-86.474" /><polygon points="159.766,-86.474 137.458,-83.746 138.608,-112.302" /><polygon points="-178.955,-35.241 -142.651,-8.669 -180.347,-20.243" /><polygon points="-122.089,126.953 -93.623,127.834 -115.736,142.184" /><polygon points="-93.623,127.834 -98.886,161.481 -115.736,142.184" /><polygon points="38.534,-157.023 17.370,-161.866 28.736,-180.372" /><polygon points="28.736,-180.372 17.370,-161.866 -4.855,-180.827" /><polygon points="-117.342,-76.237 -119.485,-113.791 -103.678,-89.098" /><polygon points="-119.485,-113.791 -117.342,-76.237 -148.265,-107.479" /><polygon points="-117.342,-76.237 -163.546,-87.970 -148.265,-107.479" /><polygon points="-130.818,46.355 -130.678,81.374 -167.512,75.451" /><polygon points="-167.512,75.451 -130.678,81.374 -137.455,102.258" /><polygon points="-100.051,-124.727 -119.485,-113.791 -126.800,-133.753" /><polygon points="-126.800,-133.753 -119.485,-113.791 -148.265,-107.479" /><polygon points="-130.818,46.355 -165.460,25.974 -134.995,28.322" /><polygon points="-165.460,25.974 -130.818,46.355 -177.312,52.807" /><polygon points="-177.312,52.807 -130.818,46.355 -167.512,75.451" /><polygon points="-47.839,-157.812 -67.552,-120.558 -68.025,-171.809" /><polygon points="-68.025,-171.809 -67.552,-120.558 -82.198,-121.867" /><polygon points="-180.347,-20.243 -158.791,3.489 -185.455,-5.402" /><polygon points="-163.546,-87.970 -117.342,-76.237 -149.397,-56.192" /><polygon points="-163.546,-87.970 -149.397,-56.192 -171.326,-48.727" /><polygon points="-42.288,130.419 -81.339,149.465 -60.134,125.352" /><polygon points="-81.339,149.465 -42.288,130.419 -66.498,174.214" /><polygon points="-66.498,174.214 -42.288,130.419 -41.446,176.304" /><polygon points="-158.791,3.489 -165.460,25.974 -185.547,19.484" /><polygon points="-185.455,-5.402 -158.791,3.489 -185.547,19.484" /><polygon points="-185.547,19.484 -165.460,25.974 -177.312,52.807" /><polygon points="63.290,124.777 112.660,137.671 98.313,159.611" /><polygon points="70.252,158.024 63.290,124.777 98.313,159.611" /><polygon points="112.660,137.671 63.290,124.777 76.497,111.062" /><polygon points="-29.911,-184.651 -10.944,-140.144 -47.839,-157.812" /><polygon points="-29.911,-184.651 -47.839,-157.812 -68.025,-171.809" /><polygon points="-82.198,-121.867 -84.090,-169.872 -68.025,-171.809" /><polygon points="-96.738,-160.541 -82.198,-121.867 -100.051,-124.727" /><polygon points="-100.051,-124.727 -110.330,-152.930 -96.738,-160.541" /><polygon points="186.934,19.138 138.115,25.801 174.004,-5.522" /><polygon points="128.840,-136.616 104.689,-94.070 98.956,-137.919" /><polygon points="-110.330,-152.930 -100.051,-124.727 -126.800,-133.753" /><polygon points="-98.886,161.481 -93.623,127.834 -81.339,149.465" /><polygon points="-98.886,161.481 -81.339,149.465 -66.498,174.214" /><polygon points="-84.090,-169.872 -82.198,-121.867 -96.738,-160.541" /><polygon points="98.956,-137.919 57.042,-132.705 98.270,-164.048" /><polygon points="42.104,177.032 70.252,158.024 69.140,179.729" /><polygon points="69.140,179.729 70.252,158.024 98.313,159.611" /><polygon points="38.534,-157.023 70.922,-180.497 48.186,-143.813" /><polygon points="55.349,-184.312 38.534,-157.023 28.736,-180.372" /><polygon points="57.042,-132.705 48.186,-143.813 70.922,-180.497" /><polygon points="57.042,-132.705 86.867,-176.638 98.270,-164.048" /><polygon points="70.922,-180.497 38.534,-157.023 55.349,-184.312" /><polygon points="165.613,74.077 170.850,41.044 188.104,55.489" /><polygon points="86.867,-176.638 57.042,-132.705 70.922,-180.497" /><polygon points="148.400,-50.961 150.367,-65.971 184.310,-69.872" /><polygon points="184.310,-69.872 150.367,-65.971 159.766,-86.474" /><polygon points="174.004,-5.522 146.332,-37.073 196.079,-22.526" /><polygon points="159.766,-86.474 138.608,-112.302 161.540,-117.744" /><polygon points="146.332,-37.073 148.400,-50.961 193.085,-55.460" /><polygon points="196.079,-22.526 146.332,-37.073 193.085,-55.460" /><polygon points="193.085,-55.460 148.400,-50.961 184.310,-69.872" /><polygon points="135.527,89.944 146.699,80.764 172.202,106.610" /><polygon points="172.202,106.610 146.699,80.764 165.613,74.077" /><polygon points="112.660,137.671 123.713,97.726 141.179,146.040" /><polygon points="-115.736,142.184 -98.886,161.481 -115.416,168.503" /><polygon points="28.736,-180.372 -4.855,-180.827 4.940,-203.738" /><polygon points="196.079,-22.526 193.085,-55.460 200.234,-39.973" /><polygon points="42.104,177.032 69.140,179.729 64.246,196.398" /><polygon points="-126.800,-133.753 -148.265,-107.479 -161.732,-130.341" /><polygon points="-41.446,176.304 -2.382,164.453 -30.781,206.131" /><polygon points="123.713,97.726 135.527,89.944 167.392,123.853" /><polygon points="141.179,146.040 123.713,97.726 167.392,123.853" /><polygon points="167.392,123.853 135.527,89.944 172.202,106.610" /><polygon points="11.504,168.475 25.762,169.500 20.778,207.612" /><polygon points="20.778,207.612 25.762,169.500 42.104,177.032" /><polygon points="20.778,207.612 42.104,177.032 64.246,196.398" /><polygon points="165.613,74.077 188.104,55.489 197.841,69.526" /><polygon points="172.202,106.610 165.613,74.077 197.841,69.526" /><polygon points="-185.547,19.484 -177.312,52.807 -205.562,49.950" /><polygon points="141.179,146.040 167.392,123.853 159.036,139.391" /><polygon points="-167.512,75.451 -137.455,102.258 -186.482,103.299" /><polygon points="159.766,-86.474 161.540,-117.744 178.593,-116.693" /><polygon points="-98.886,161.481 -66.498,174.214 -86.900,196.851" /><polygon points="-115.416,168.503 -98.886,161.481 -86.900,196.851" /><polygon points="-131.658,116.002 -122.089,126.953 -157.034,147.422" /><polygon points="-157.034,147.422 -122.089,126.953 -115.736,142.184" /><polygon points="-157.034,147.422 -115.736,142.184 -115.416,168.503" /><polygon points="-2.382,164.453 11.504,168.475 3.688,215.818" /><polygon points="-30.781,206.131 -2.382,164.453 3.688,215.818" /><polygon points="3.688,215.818 11.504,168.475 20.778,207.612" /><polygon points="-163.546,-87.970 -171.326,-48.727 -199.269,-86.808" /><polygon points="28.736,-180.372 4.940,-203.738 16.364,-216.826" /><polygon points="-30.781,206.131 3.688,215.818 -14.325,218.146" /><polygon points="69.140,179.729 98.313,159.611 104.954,191.669" /><polygon points="64.246,196.398 69.140,179.729 104.954,191.669" /><polygon points="-29.911,-184.651 -68.025,-171.809 -53.203,-214.388" /><polygon points="186.934,19.138 188.104,55.489 170.850,41.044" /><polygon points="188.104,55.489 186.934,19.138 218.894,33.779" /><polygon points="-126.800,-133.753 -161.732,-130.341 -165.448,-147.889" /><polygon points="-178.955,-35.241 -180.347,-20.243 -219.722,-36.105" /><polygon points="-137.455,102.258 -131.658,116.002 -175.136,139.089" /><polygon points="-186.482,103.299 -137.455,102.258 -175.136,139.089" /><polygon points="-175.136,139.089 -131.658,116.002 -157.034,147.422" /><polygon points="128.840,-136.616 98.270,-164.048 139.549,-174.896" /><polygon points="98.270,-164.048 128.840,-136.616 98.956,-137.919" /><polygon points="128.840,-136.616 161.540,-117.744 138.608,-112.302" /><polygon points="161.540,-117.744 128.840,-136.616 161.794,-154.889" /><polygon points="161.794,-154.889 128.840,-136.616 139.549,-174.896" /><polygon points="-186.482,103.299 -175.136,139.089 -187.567,125.185" /><polygon points="186.934,19.138 196.079,-22.526 225.851,4.374" /><polygon points="218.894,33.779 186.934,19.138 225.851,4.374" /><polygon points="196.079,-22.526 186.934,19.138 174.004,-5.522" /><polygon points="-185.547,19.484 -205.562,49.950 -222.661,42.196" /><polygon points="-29.911,-184.651 4.940,-203.738 -4.855,-180.827" /><polygon points="4.940,-203.738 -29.911,-184.651 -24.692,-224.587" /><polygon points="-24.692,-224.587 -29.911,-184.651 -53.203,-214.388" /><polygon points="98.313,159.611 141.179,146.040 134.790,183.044" /><polygon points="104.954,191.669 98.313,159.611 134.790,183.044" /><polygon points="141.179,146.040 98.313,159.611 112.660,137.671" /><polygon points="-163.546,-87.970 -161.732,-130.341 -148.265,-107.479" /><polygon points="-161.732,-130.341 -163.546,-87.970 -194.416,-117.755" /><polygon points="-194.416,-117.755 -163.546,-87.970 -199.269,-86.808" /><polygon points="-66.498,174.214 -41.446,176.304 -63.447,219.012" /><polygon points="-86.900,196.851 -66.498,174.214 -63.447,219.012" /><polygon points="-63.447,219.012 -41.446,176.304 -30.781,206.131" /><polygon points="-177.312,52.807 -167.512,75.451 -212.397,82.898" /><polygon points="-205.562,49.950 -177.312,52.807 -212.397,82.898" /><polygon points="-212.397,82.898 -167.512,75.451 -186.482,103.299" /><polygon points="-84.090,-169.872 -96.738,-160.541 -108.676,-203.146" /><polygon points="-115.416,168.503 -86.900,196.851 -104.169,206.702" /><polygon points="-171.326,-48.727 -178.955,-35.241 -224.572,-56.273" /><polygon points="-171.326,-48.727 -219.663,-74.795 -199.269,-86.808" /><polygon points="-224.572,-56.273 -178.955,-35.241 -219.722,-36.105" /><polygon points="-219.663,-74.795 -171.326,-48.727 -224.572,-56.273" /><polygon points="64.246,196.398 104.954,191.669 102.769,212.804" /><polygon points="-53.203,-214.388 -68.025,-171.809 -76.249,-225.615" /><polygon points="-68.025,-171.809 -95.089,-219.641 -76.249,-225.615" /><polygon points="-95.089,-219.641 -68.025,-171.809 -84.090,-169.872" /><polygon points="86.867,-176.638 70.922,-180.497 98.419,-217.185" /><polygon points="-95.089,-219.641 -84.090,-169.872 -108.676,-203.146" /><polygon points="218.894,33.779 197.841,69.526 188.104,55.489" /><polygon points="197.841,69.526 218.894,33.779 236.057,48.883" /><polygon points="139.549,-174.896 98.270,-164.048 135.763,-202.418" /><polygon points="135.763,-202.418 98.270,-164.048 86.867,-176.638" /><polygon points="86.867,-176.638 120.184,-215.803 135.763,-202.418" /><polygon points="161.794,-154.889 178.593,-116.693 161.540,-117.744" /><polygon points="178.593,-116.693 161.794,-154.889 185.842,-160.549" /><polygon points="200.234,-39.973 193.085,-55.460 240.302,-55.914" /><polygon points="120.184,-215.803 86.867,-176.638 98.419,-217.185" /><polygon points="225.851,4.374 196.079,-22.526 248.210,-15.732" /><polygon points="248.210,-15.732 196.079,-22.526 200.234,-39.973" /><polygon points="200.234,-39.973 251.714,-37.002 248.210,-15.732" /><polygon points="-24.692,-224.587 16.364,-216.826 4.940,-203.738" /><polygon points="16.364,-216.826 -24.692,-224.587 -14.583,-249.353" /><polygon points="134.790,183.044 141.179,146.040 166.337,190.654" /><polygon points="166.337,190.654 141.179,146.040 159.036,139.391" /><polygon points="-194.416,-117.755 -165.448,-147.889 -161.732,-130.341" /><polygon points="-165.448,-147.889 -194.416,-117.755 -209.871,-142.252" /><polygon points="251.714,-37.002 200.234,-39.973 240.302,-55.914" /><polygon points="-63.447,219.012 -30.781,206.131 -50.806,251.517" /><polygon points="-50.806,251.517 -30.781,206.131 -14.325,218.146" /><polygon points="-212.397,82.898 -222.661,42.196 -205.562,49.950" /><polygon points="-222.661,42.196 -212.397,82.898 -243.651,81.022" /><polygon points="-63.447,219.012 -104.169,206.702 -86.900,196.851" /><polygon points="-104.169,206.702 -63.447,219.012 -84.806,245.139" /><polygon points="-84.806,245.139 -63.447,219.012 -50.806,251.517" /><polygon points="-212.397,82.898 -186.482,103.299 -233.042,113.948" /><polygon points="-243.651,81.022 -212.397,82.898 -233.042,113.948" /><polygon points="-233.042,113.948 -186.482,103.299 -187.567,125.185" /><polygon points="134.790,183.044 102.769,212.804 104.954,191.669" /><polygon points="102.769,212.804 134.790,183.044 144.016,218.078" /><polygon points="144.016,218.078 134.790,183.044 166.337,190.654" /><polygon points="-194.416,-117.755 -199.269,-86.808 -234.020,-116.508" /><polygon points="-209.871,-142.252 -194.416,-117.755 -234.020,-116.508" /><polygon points="-234.020,-116.508 -199.269,-86.808 -219.663,-74.795" /><polygon points="166.337,190.654 159.036,139.391 187.562,182.146" /><polygon points="218.894,33.779 225.851,4.374 261.899,18.481" /><polygon points="236.057,48.883 218.894,33.779 261.899,18.481" /><polygon points="261.899,18.481 225.851,4.374 248.210,-15.732" /><polygon points="-24.692,-224.587 -53.203,-214.388 -50.154,-257.777" /><polygon points="-14.583,-249.353 -24.692,-224.587 -50.154,-257.777" /><polygon points="-50.154,-257.777 -53.203,-214.388 -76.249,-225.615" /><polygon points="161.794,-154.889 139.549,-174.896 174.303,-196.923" /><polygon points="185.842,-160.549 161.794,-154.889 174.303,-196.923" /><polygon points="174.303,-196.923 139.549,-174.896 135.763,-202.418" /><polygon points="-50.806,251.517 -14.325,218.146 -31.194,266.184" /><polygon points="-233.042,113.948 -187.567,125.185 -235.362,140.264" /><polygon points="-234.020,-116.508 -219.663,-74.795 -259.460,-103.412" /><polygon points="-50.154,-257.777 -76.249,-225.615 -77.042,-273.241" /><polygon points="174.303,-196.923 135.763,-202.418 172.235,-230.423" /><polygon points="261.899,18.481 248.210,-15.732 290.594,-3.548" /><polygon points="144.016,218.078 166.337,190.654 180.892,230.069" /><polygon points="-209.871,-142.252 -234.020,-116.508 -255.625,-143.999" /><polygon points="-255.625,-143.999 -234.020,-116.508 -259.460,-103.412" /><polygon points="-84.806,245.139 -50.806,251.517 -72.805,284.649" /><polygon points="-72.805,284.649 -50.806,251.517 -31.194,266.184" /><polygon points="-243.651,81.022 -233.042,113.948 -270.414,115.507" /><polygon points="-270.414,115.507 -233.042,113.948 -235.362,140.264" /><polygon points="-104.169,206.702 -157.034,147.422 -115.416,168.503" /><polygon points="-157.034,147.422 -235.362,140.264 -175.136,139.089" /><polygon points="-175.136,139.089 -235.362,140.264 -187.567,125.185" /><polygon points="3.688,215.818 -31.194,266.184 -14.325,218.146" /><polygon points="-222.661,42.196 -185.455,-5.402 -185.547,19.484" /><polygon points="102.769,212.804 -31.194,266.184 20.778,207.612" /><polygon points="20.778,207.612 -31.194,266.184 3.688,215.818" /><polygon points="102.769,212.804 20.778,207.612 64.246,196.398" /><polygon points="-222.661,42.196 -219.722,-36.105 -185.455,-5.402" /><polygon points="-185.455,-5.402 -219.722,-36.105 -180.347,-20.243" /><polygon points="-224.572,-56.273 -259.460,-103.412 -219.663,-74.795" /><polygon points="187.562,182.146 180.892,230.069 166.337,190.654" /><polygon points="167.392,123.853 187.562,182.146 159.036,139.391" /><polygon points="-165.448,-147.889 -110.330,-152.930 -126.800,-133.753" /><polygon points="-110.330,-152.930 -108.676,-203.146 -96.738,-160.541" /><polygon points="172.202,106.610 187.562,182.146 167.392,123.853" /><polygon points="187.562,182.146 197.841,69.526 236.057,48.883" /><polygon points="197.841,69.526 187.562,182.146 172.202,106.610" /><polygon points="251.714,-37.002 290.594,-3.548 248.210,-15.732" /><polygon points="-14.583,-249.353 98.419,-217.185 16.364,-216.826" /><polygon points="184.310,-69.872 240.302,-55.914 193.085,-55.460" /><polygon points="16.364,-216.826 55.349,-184.312 28.736,-180.372" /><polygon points="55.349,-184.312 98.419,-217.185 70.922,-180.497" /><polygon points="178.593,-116.693 240.302,-55.914 184.310,-69.872" /><polygon points="178.593,-116.693 184.310,-69.872 159.766,-86.474" /><polygon points="185.842,-160.549 240.302,-55.914 178.593,-116.693" /><polygon points="98.419,-217.185 55.349,-184.312 16.364,-216.826" /><polygon points="120.184,-215.803 172.235,-230.423 135.763,-202.418" /><polygon points="-95.089,-219.641 -77.042,-273.241 -76.249,-225.615" /><polygon points="-108.676,-203.146 -110.330,-152.930 -165.448,-147.889" /></g><circle cx="8.992" cy="3.282" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.627" cy="8.931" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="7.439" cy="23.705" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.594" cy="-22.648" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-19.164" cy="27.268" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="37.228" cy="-8.190" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.784" cy="1.651" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.704" cy="-20.105" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.048" cy="-34.799" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="37.213" cy="31.353" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="11.037" cy="-49.096" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.574" cy="-50.672" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="49.979" cy="-23.101" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.672" cy="57.981" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="52.865" cy="27.774" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="42.171" cy="-41.979" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.920" cy="63.482" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-54.767" cy="38.117" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="65.646" cy="8.823" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.011" cy="50.832" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="35.668" cy="62.224" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-73.931" cy="-6.841" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.359" cy="-22.754" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.410" cy="70.304" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-77.226" cy="20.369" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.340" cy="-67.260" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-65.498" cy="-50.710" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.982" cy="-77.761" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="29.518" cy="-79.657" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="0.403" cy="-85.223" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="69.405" cy="-52.692" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="85.531" cy="23.427" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="33.925" cy="83.299" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="48.655" cy="-75.695" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.008" cy="78.531" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="86.641" cy="-26.727" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-91.282" cy="11.390" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-65.255" cy="-66.197" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="12.973" cy="-93.086" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="82.795" cy="-46.925" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="78.781" cy="55.944" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="88.906" cy="37.703" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="96.587" cy="-8.017" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="24.595" cy="95.130" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-61.780" cy="78.998" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="3.761" cy="102.721" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-102.700" cy="-0.987" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="69.505" cy="76.596" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.413" cy="-84.378" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.121" cy="69.526" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-19.581" cy="107.725" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="29.266" cy="-104.839" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="104.502" cy="-41.967" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-102.367" cy="52.552" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-112.671" cy="-25.908" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="100.534" cy="59.559" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-108.569" cy="-51.433" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.462" cy="120.691" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.715" cy="-111.145" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.192" cy="-121.883" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-90.664" cy="88.506" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="83.364" cy="-97.969" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="60.436" cy="-113.682" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-130.676" cy="9.563" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-72.105" cy="109.807" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.147" cy="-97.942" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="43.497" cy="124.680" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-130.905" cy="-18.895" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="24.806" cy="-129.290" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="131.865" cy="6.723" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="118.657" cy="-59.460" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="126.320" cy="41.641" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="120.857" cy="58.705" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="110.853" cy="-75.559" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="76.497" cy="111.062" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="133.436" cy="-22.283" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="7.554" cy="-135.204" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="27.394" cy="133.821" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.288" cy="130.419" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-103.678" cy="-89.098" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-134.995" cy="28.322" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-130.818" cy="46.355" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.552" cy="-120.558" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-60.134" cy="125.352" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-91.400" cy="105.314" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="63.290" cy="124.777" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-117.342" cy="-76.237" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="138.115" cy="25.801" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-10.944" cy="-140.144" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="104.689" cy="-94.070" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="103.567" cy="96.722" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-142.651" cy="-8.669" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.042" cy="-132.705" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.198" cy="-121.867" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.350" cy="147.253" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="146.332" cy="-37.073" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.186" cy="-143.813" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-130.678" cy="81.374" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="148.400" cy="-50.961" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="123.713" cy="97.726" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-93.623" cy="127.834" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.561" cy="152.335" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-158.791" cy="3.489" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-149.397" cy="-56.192" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="148.805" cy="57.633" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-100.051" cy="-124.727" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="137.458" cy="-83.746" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.534" cy="-157.023" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="135.527" cy="89.944" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="17.370" cy="-161.866" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.382" cy="164.453" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="150.367" cy="-65.971" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-119.485" cy="-113.791" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.839" cy="-157.812" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-165.460" cy="25.974" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="146.699" cy="80.764" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.504" cy="168.475" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-81.339" cy="149.465" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="98.956" cy="-137.919" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-137.455" cy="102.258" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="25.762" cy="169.500" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="70.252" cy="158.024" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="174.004" cy="-5.522" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-131.658" cy="116.002" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-122.089" cy="126.953" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="170.850" cy="41.044" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="112.660" cy="137.671" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-171.326" cy="-48.727" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="138.608" cy="-112.302" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.446" cy="176.304" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-180.347" cy="-20.243" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.855" cy="-180.827" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="165.613" cy="74.077" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.104" cy="177.032" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="159.766" cy="-86.474" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-178.955" cy="-35.241" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-115.736" cy="142.184" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="28.736" cy="-180.372" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-148.265" cy="-107.479" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-167.512" cy="75.451" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-126.800" cy="-133.753" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-177.312" cy="52.807" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-68.025" cy="-171.809" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-185.455" cy="-5.402" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-163.546" cy="-87.970" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.498" cy="174.214" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-185.547" cy="19.484" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="98.313" cy="159.611" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.911" cy="-184.651" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-96.738" cy="-160.541" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="186.934" cy="19.138" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="128.840" cy="-136.616" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-110.330" cy="-152.930" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-98.886" cy="161.481" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.090" cy="-169.872" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="98.270" cy="-164.048" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="69.140" cy="179.729" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.349" cy="-184.312" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="70.922" cy="-180.497" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="188.104" cy="55.489" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.867" cy="-176.638" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="184.310" cy="-69.872" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="196.079" cy="-22.526" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="161.540" cy="-117.744" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="193.085" cy="-55.460" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="172.202" cy="106.610" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="141.179" cy="146.040" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-115.416" cy="168.503" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="4.940" cy="-203.738" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="200.234" cy="-39.973" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.246" cy="196.398" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-161.732" cy="-130.341" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-30.781" cy="206.131" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="167.392" cy="123.853" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="20.778" cy="207.612" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="197.841" cy="69.526" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-205.562" cy="49.950" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="159.036" cy="139.391" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-186.482" cy="103.299" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="178.593" cy="-116.693" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-86.900" cy="196.851" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-157.034" cy="147.422" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="3.688" cy="215.818" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-199.269" cy="-86.808" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="16.364" cy="-216.826" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.325" cy="218.146" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="104.954" cy="191.669" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.203" cy="-214.388" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="218.894" cy="33.779" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-165.448" cy="-147.889" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-219.722" cy="-36.105" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-175.136" cy="139.089" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="139.549" cy="-174.896" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="161.794" cy="-154.889" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-187.567" cy="125.185" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="225.851" cy="4.374" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-222.661" cy="42.196" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.692" cy="-224.587" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="134.790" cy="183.044" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-194.416" cy="-117.755" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.447" cy="219.012" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-212.397" cy="82.898" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-108.676" cy="-203.146" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-104.169" cy="206.702" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-224.572" cy="-56.273" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-219.663" cy="-74.795" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="102.769" cy="212.804" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.249" cy="-225.615" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="98.419" cy="-217.185" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-95.089" cy="-219.641" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="236.057" cy="48.883" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="135.763" cy="-202.418" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="185.842" cy="-160.549" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="240.302" cy="-55.914" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="120.184" cy="-215.803" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="248.210" cy="-15.732" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.583" cy="-249.353" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="166.337" cy="190.654" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-209.871" cy="-142.252" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="251.714" cy="-37.002" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.806" cy="251.517" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-243.651" cy="81.022" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.806" cy="245.139" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-233.042" cy="113.948" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="144.016" cy="218.078" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-234.020" cy="-116.508" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="187.562" cy="182.146" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="261.899" cy="18.481" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.154" cy="-257.777" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="174.303" cy="-196.923" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-31.194" cy="266.184" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-235.362" cy="140.264" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-259.460" cy="-103.412" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-77.042" cy="-273.241" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="172.235" cy="-230.423" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="290.594" cy="-3.548" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="180.892" cy="230.069" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-255.625" cy="-143.999" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-72.805" cy="284.649" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-270.414" cy="115.507" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle r="9" fill="#061019" stroke="#dabb83" stroke-width="0.8"/><circle r="2" fill="#ffe5a2"/></g><g transform="translate(638.000 668.000)"><circle r="193.520" fill="url(#bloom-light)"/><polygon points="-0.292,12.933 -5.810,6.999 1.421,-1.935 8.715,3.164 7.091,13.308" fill="#481e66" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.815,-10.684 -0.475,-8.448 -0.180,-3.230 -7.410,5.704 -11.998,5.827 -18.221,-4.987" fill="#361a5a" stroke="#ba96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.836,-0.868 9.724,1.721 2.430,-3.378 2.135,-8.596 5.705,-12.852 8.140,-14.010 13.628,-12.442 21.329,-4.249" fill="#0d0f2c" stroke="#969ce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="14.155,18.474 8.488,13.531 10.112,3.388 21.225,0.799 23.179,4.938 21.829,14.779 21.388,15.242" fill="#0f1032" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-5.435,24.335 -12.505,24.597 -15.373,12.629 -11.925,8.543 -7.338,8.420 -1.819,14.354" fill="#512070" stroke="#c996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.165,-26.850 -12.841,-27.110 -8.015,-27.271 3.523,-14.682 -0.047,-10.426 -10.387,-12.662" fill="#421d65" stroke="#c096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="0.109,28.466 -4.006,24.853 -0.390,14.872 6.992,15.246 12.659,20.189 9.865,29.195 8.440,30.039" fill="#16123f" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.162,4.080 -26.368,-3.936 -23.701,-4.711 -20.092,-3.910 -13.869,6.903 -17.316,10.989 -21.685,11.638" fill="#3f1d65" stroke="#bd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-14.859,-26.518 -12.081,-12.330 -19.488,-6.633 -23.097,-7.434 -23.577,-25.358" fill="#21164c" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="9.016,-32.127 6.870,-16.677 4.436,-15.518 -7.103,-28.107 3.806,-34.218 8.630,-32.502" fill="#111139" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="25.544,-25.962 14.312,-14.838 8.824,-16.406 10.970,-31.856 21.891,-30.900" fill="#502172" stroke="#c796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.442,-25.055 28.338,-24.080 30.977,-12.172 30.793,-11.521 22.912,-5.737 15.211,-13.930" fill="#101138" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-20.634,31.681 -23.654,30.701 -27.379,20.522 -21.383,13.672 -17.014,13.023 -14.146,24.990" fill="#20164c" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.660,8.131 -32.944,5.164 -22.467,12.723 -28.463,19.573 -37.719,16.083 -39.051,14.316" fill="#241851" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-38.151,-15.382 -38.331,-17.488 -27.278,-26.096 -24.962,-25.321 -24.482,-7.397 -27.148,-6.622" fill="#141240" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.924,42.222 -14.617,42.181 -18.916,33.346 -12.428,26.656 -5.359,26.394 -1.244,30.007" fill="#4f2172" stroke="#c496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.417,4.658 25.786,3.707 23.832,-0.432 24.324,-3.813 32.205,-9.596 44.662,0.490" fill="#64257f" stroke="#d096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.332,6.235 43.890,8.786 33.697,17.045 24.351,15.125 25.701,5.284" fill="#141341" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="30.652,36.103 29.270,37.789 27.302,38.560 12.491,30.010 15.285,21.004 22.518,17.772" fill="#6d2684" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.184,5.400 -46.466,-4.034 -39.224,-14.036 -28.221,-5.275 -34.015,2.741 -40.730,5.707" fill="#2c1a5a" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-7.974,42.818 -0.295,30.604 8.036,32.178 5.096,43.586 2.044,46.007 -0.140,46.525" fill="#221851" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.690,35.642 23.556,17.311 23.997,16.849 33.343,18.768 37.375,27.150 34.640,33.853" fill="#492070" stroke="#c096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.092,-47.182 -12.815,-47.428 1.072,-39.803 2.806,-36.003 -8.103,-29.892 -12.928,-29.731" fill="#131340" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-38.210,35.186 -38.968,34.919 -38.981,34.907 -38.308,17.644 -29.051,21.134 -25.327,31.313" fill="#181548" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="46.768,-18.431 32.924,-12.604 30.286,-24.512 37.103,-26.594 42.588,-25.144 46.687,-20.871" fill="#3e1e69" stroke="#b896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="47.238,-17.316 52.800,-7.791 52.756,-7.398 45.667,-0.751 33.209,-10.837 33.394,-11.489" fill="#732889" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-15.090,-47.163 -14.925,-29.712 -15.250,-29.452 -23.967,-28.293 -26.283,-29.068 -27.334,-30.840 -27.211,-46.450" fill="#401f6b" stroke="#ba96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.245,40.392 20.811,49.350 7.069,44.094 10.008,32.686 11.433,31.841" fill="#1a164a" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="50.573,16.959 50.586,17.030 49.844,20.775 38.635,26.544 34.603,18.162 44.795,9.904" fill="#241955" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="17.221,-52.744 17.514,-52.939 22.751,-52.074 27.993,-46.646 22.111,-33.411 11.190,-34.367 10.804,-34.742" fill="#622681" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.999,-53.180 9.581,-35.178 4.757,-36.893 3.023,-40.693 5.403,-51.458" fill="#6f2888" stroke="#d396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.341,-52.711 -8.977,-53.923 -2.652,-56.029 4.099,-51.746 1.719,-40.981 -12.167,-48.607" fill="#381e66" stroke="#b496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-43.516,-35.613 -29.597,-29.497 -28.546,-27.726 -39.600,-19.117 -41.718,-20.665 -45.515,-28.006" fill="#612581" stroke="#cc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="38.481,-45.174 36.459,-28.700 29.643,-26.618 27.747,-27.592 24.094,-32.530 29.976,-45.764" fill="#161547" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.391,34.852 -45.565,33.499 -52.209,27.699 -52.053,23.956 -41.049,15.822 -39.718,17.589" fill="#53237a" stroke="#c496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.157,-15.557 -43.072,-18.812 -40.954,-17.264 -40.774,-15.158 -48.017,-5.157 -58.959,-8.139 -61.621,-14.541" fill="#19164b" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-31.767,52.528 -37.501,37.544 -24.618,33.671 -21.597,34.651 -17.298,43.486 -18.014,44.910" fill="#241956" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.338,-49.894 -28.879,-46.463 -29.002,-30.853 -42.921,-36.968 -43.374,-38.175 -37.387,-48.925" fill="#6c2888" stroke="#d196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.145,62.032 3.507,47.850 6.559,45.428 20.301,50.684 20.064,59.263 18.368,61.228 16.979,61.905" fill="#211854" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.742,12.659 -43.014,8.109 -42.560,8.416 -41.952,14.601 -52.955,22.735" fill="#7a2a90" stroke="#d796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-12.671,60.814 -15.351,46.248 -14.636,44.824 -8.943,44.865 -1.108,48.572 -3.486,57.842" fill="#7c2a91" stroke="#d796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-62.102,11.176 -62.666,10.974 -64.711,5.412 -59.599,-5.790 -48.657,-2.808 -43.375,6.626" fill="#3d1f6c" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.643,-45.031 43.822,-46.301 47.610,-43.594 49.305,-40.474 43.107,-27.107 37.622,-28.557" fill="#2d1c5f" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.347,-16.611 -60.532,-23.461 -59.513,-24.937 -47.059,-27.208 -43.262,-19.867" fill="#3c1f6c" stroke="#b596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="50.519,37.823 36.629,34.665 39.364,27.962 50.573,22.193 52.712,27.180 50.991,37.585" fill="#70298c" stroke="#d296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.261,62.632 5.562,65.404 -1.939,58.239 0.439,48.968 2.623,48.451" fill="#502379" stroke="#c196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="67.976,4.485 52.764,15.164 46.986,8.109 46.429,5.558 47.674,1.390 54.763,-5.257" fill="#1a174e" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="50.335,-39.997 57.206,-34.739 56.075,-27.809 48.235,-22.356 44.136,-26.630" fill="#672787" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.409,58.490 -30.848,54.187 -17.095,46.569 -14.415,61.134 -15.625,63.413 -27.959,63.639" fill="#482275" stroke="#bc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.201,-67.633 15.988,-55.240 15.695,-55.046 5.099,-53.323 -1.651,-57.606 0.592,-62.899" fill="#822c96" stroke="#d996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.864,-18.629 54.854,-8.990 49.292,-18.515 49.210,-20.955 57.049,-26.408" fill="#792a92" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-54.805,-39.190 -45.743,-37.286 -45.290,-36.079 -47.289,-28.473 -59.743,-26.202 -58.538,-37.605" fill="#7c2b94" stroke="#d696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.049,-67.901 -28.298,-68.464 -13.129,-53.952 -14.955,-49.847 -15.233,-49.601 -27.355,-48.888 -32.813,-52.319" fill="#1c1852" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.251,55.102 31.888,39.936 33.271,38.249 36.221,36.460 50.111,39.618 45.437,54.741" fill="#602684" stroke="#c896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.904,56.200 38.983,60.748 22.904,59.341 23.140,50.763 28.574,41.805 30.542,41.033" fill="#231a5a" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-51.101,55.724 -40.037,37.958 -39.280,38.225 -33.546,53.209 -35.107,57.511 -47.305,59.470 -48.529,58.899" fill="#251b5b" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-9.949,72.107 -13.395,64.598 -12.184,62.320 -2.999,59.348 4.502,66.514 4.245,66.930 -8.244,73.374" fill="#872d9a" stroke="#da96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-71.609,23.285 -63.600,13.586 -63.036,13.788 -54.250,23.864 -54.406,27.607 -59.858,30.230" fill="#7a2b94" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-52.089,55.109 -53.249,54.410 -57.882,45.689 -46.213,35.978 -41.039,37.331 -41.026,37.343" fill="#4e247b" stroke="#be96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.190,21.406 72.132,21.443 53.644,16.470 53.630,16.398 68.842,5.719 71.336,6.083 75.619,12.809 75.673,13.855" fill="#321e67" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-12.414,-73.481 -11.754,-73.804 -6.240,-73.023 -1.185,-63.652 -3.428,-58.359 -9.753,-56.253" fill="#6b298c" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.621,44.800 -61.278,42.978 -62.256,40.215 -59.048,31.912 -53.597,29.289 -46.953,35.089" fill="#7a2b94" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="71.834,22.551 67.180,29.138 54.742,26.309 52.603,21.322 53.346,17.577" fill="#5c2684" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-25.892,-71.721 -23.690,-73.037 -13.552,-73.306 -10.890,-56.077 -12.255,-54.865 -27.423,-69.378" fill="#402171" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="71.775,3.078 69.282,2.713 56.069,-7.028 56.113,-7.422 68.123,-17.061 70.392,-17.390 76.754,-6.584" fill="#2a1c61" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.134,-57.079 -39.341,-50.013 -45.328,-39.264 -54.390,-41.167" fill="#592683" stroke="#c496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.916,30.301 66.474,37.005 65.805,37.729 58.791,39.753 52.756,37.877 54.478,27.472" fill="#872d9b" stroke="#da96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="33.634,-69.242 39.282,-68.017 42.978,-49.993 42.868,-49.442 38.689,-48.172 30.185,-48.762 24.942,-54.190" fill="#1d1955" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.952,-70.213 23.261,-55.161 18.024,-56.026 13.237,-68.420 13.473,-69.476 29.257,-73.334" fill="#3a206e" stroke="#b296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.355,-26.564 70.005,-20.058 67.736,-19.729 57.921,-27.508 59.052,-34.438 61.212,-34.945 74.577,-28.198" fill="#7c2c96" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.739,-74.883 10.822,-70.069 10.585,-69.013 -0.024,-64.278 -5.079,-73.649 4.056,-78.649 4.370,-78.565" fill="#8b2e9d" stroke="#db96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-82.136,1.170 -83.880,-4.983 -65.771,-13.272 -64.650,-13.282 -61.988,-6.880 -67.099,4.322" fill="#241b5d" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.471,58.254 46.994,55.222 51.668,40.100 52.139,39.862 58.175,41.737 59.899,58.108" fill="#592684" stroke="#c396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-75.490,28.475 -73.236,24.848 -72.267,24.399 -60.516,31.345 -63.724,39.648 -75.877,33.679 -76.133,32.552" fill="#7f2c99" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-52.141,-59.016 -52.477,-59.434 -45.935,-70.355 -31.535,-68.501 -35.299,-52.919 -38.349,-51.950" fill="#201a5a" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-75.254,-33.243 -75.200,-34.955 -71.721,-38.623 -63.055,-39.213 -60.429,-37.805 -61.634,-26.401 -62.653,-24.925" fill="#902fa1" stroke="#dc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="65.201,-51.328 67.302,-45.021 60.724,-37.022 58.564,-36.514 51.694,-41.772 49.998,-44.892" fill="#8c2e9f" stroke="#db96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.541,79.888 -27.918,65.829 -15.585,65.604 -12.140,73.112 -27.965,81.615" fill="#36206d" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.646,82.286 7.735,81.002 6.286,68.193 6.544,67.777 13.243,65.005 17.078,64.878" fill="#862d9d" stroke="#d896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="6.421,81.150 1.672,83.837 -1.577,82.953 -6.679,77.469 -7.515,74.786 4.973,68.342" fill="#8e2fa0" stroke="#db96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-62.891,-58.410 -54.767,-57.600 -54.432,-57.181 -55.688,-41.270 -59.421,-39.685 -62.047,-41.093 -65.413,-57.299 -64.988,-57.648" fill="#5d2787" stroke="#c496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="33.685,81.492 21.015,63.512 22.711,61.546 38.790,62.954 40.076,76.046 34.558,81.290" fill="#341f6c" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-81.929,-24.035 -75.996,-32.119 -63.395,-23.801 -64.210,-16.952 -64.674,-15.937 -65.795,-15.926" fill="#7f2d9a" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-82.583,3.302 -67.546,6.454 -65.501,12.017 -73.509,21.715 -74.478,22.163 -85.926,10.004" fill="#251c5f" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.587,-64.221 64.974,-53.748 64.658,-52.612 49.455,-46.176 45.667,-48.882 45.777,-49.434" fill="#772b96" stroke="#d196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.566,-66.183 58.592,-65.150 44.782,-50.363 41.087,-68.387 50.338,-72.487 54.643,-71.008" fill="#49247b" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="76.042,-30.688 75.175,-29.382 61.809,-36.129 68.388,-44.129 76.665,-39.932 77.383,-36.828" fill="#812d9b" stroke="#d596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-84.998,-21.783 -82.552,-22.796 -66.418,-14.687 -84.528,-6.398 -88.373,-10.403 -88.835,-13.516" fill="#5c2787" stroke="#c396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="32.542,82.298 30.236,83.965 18.919,84.179 17.051,82.402 18.483,64.993 19.872,64.317" fill="#6b2a90" stroke="#cb96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.750,58.069 61.053,57.986 59.329,41.616 66.342,39.592 69.755,55.030 67.291,57.129" fill="#62288b" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-31.813,79.626 -43.987,78.967 -46.839,62.377 -34.640,60.418 -30.190,65.567" fill="#281d63" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.050,-86.222 28.873,-74.908 13.088,-71.050 11.005,-75.863 24.935,-88.373" fill="#36206f" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-66.533,-57.066 -63.167,-40.860 -71.833,-40.271 -74.052,-52.605 -70.544,-55.661" fill="#67298f" stroke="#c996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.918,-89.250 -7.754,-89.935 -1.445,-88.080 3.204,-80.205 -5.930,-75.206 -11.445,-75.987" fill="#8f2fa3" stroke="#da96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-20.582,92.542 -23.108,91.446 -24.865,89.784 -27.359,82.743 -11.534,74.240 -9.828,75.507 -8.992,78.190" fill="#3a2173" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-73.335,49.461 -77.834,47.114 -78.966,41.990 -76.523,34.994 -64.370,40.963 -63.392,43.726" fill="#812d9c" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="78.382,39.669 68.580,37.144 69.023,30.440 73.677,23.853 73.734,23.816 83.942,34.133" fill="#9530a6" stroke="#dd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-72.604,50.728 -62.661,44.994 -60.004,46.816 -55.371,55.537 -69.960,59.595" fill="#9831a8" stroke="#de96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="90.652,-10.005 78.670,-7.712 72.309,-18.518 76.659,-25.024 92.942,-15.141" fill="#251c62" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.546,75.803 41.260,62.711 45.181,58.162 46.368,57.802 58.844,60.833 53.426,76.877" fill="#251c62" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.239,-94.575 16.646,-94.572 21.836,-92.454 23.185,-91.245 24.192,-89.200 10.263,-76.690 5.894,-80.373" fill="#402377" stroke="#b296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="96.054,3.389 78.424,11.022 74.141,4.297 79.119,-5.365 91.102,-7.659" fill="#3e2276" stroke="#b196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.948,54.767 67.536,39.328 68.204,38.605 78.006,41.129 79.132,49.930 74.895,53.627" fill="#6b2a91" stroke="#ca96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.254,-91.619 -10.435,-89.539 -12.962,-76.276 -13.622,-75.953 -23.761,-75.685" fill="#9a31a9" stroke="#de96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.411,93.511 -19.759,93.207 -8.169,78.855 -3.067,84.339 -12.507,96.187" fill="#47247d" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-97.962,21.571 -95.801,15.924 -87.150,11.156 -75.702,23.316 -77.956,26.943" fill="#261d64" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="93.814,23.587 86.644,32.927 85.112,32.976 74.904,22.658 78.387,15.106" fill="#9b31aa" stroke="#de96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.012,-91.264 -28.954,-73.722 -30.486,-71.380 -31.237,-70.816 -45.637,-72.670 -50.110,-81.775 -42.778,-90.838" fill="#5c288a" stroke="#c196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.366,-95.211 5.021,-81.009 4.707,-81.092 0.058,-88.968 8.488,-97.795" fill="#4e2682" stroke="#ba96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="78.391,-55.985 79.773,-55.266 81.237,-47.613 77.430,-41.441 69.153,-45.638 67.052,-51.944 67.368,-53.080" fill="#7e2d9c" stroke="#d296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.148,75.540 -70.954,63.893 -69.504,61.234 -54.915,57.176 -53.756,57.875 -51.183,61.050" fill="#9931aa" stroke="#dd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-85.019,-41.999 -85.485,-47.129 -80.237,-51.575 -75.418,-52.359 -73.199,-40.025 -76.678,-36.357" fill="#6b2b93" stroke="#c996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.503,93.993 2.673,85.608 7.423,82.921 15.334,84.206 17.202,85.983 13.187,96.928" fill="#8a2fa3" stroke="#d796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-64.043,77.223 -64.009,76.637 -50.045,62.147 -48.822,62.718 -45.970,79.308 -51.699,84.318 -59.617,82.596" fill="#772d99" stroke="#ce96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.762,-86.659 46.044,-89.002 49.181,-75.099 39.929,-70.999 34.281,-72.224 31.585,-75.345" fill="#392274" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-52.397,-80.651 -47.924,-71.546 -54.466,-60.626 -62.589,-61.436 -61.450,-80.966" fill="#2b1f6a" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="98.522,-28.405 99.398,-26.456 99.545,-24.055 98.425,-20.583 93.631,-16.276 77.348,-26.159 77.569,-27.792 78.437,-29.098" fill="#281e67" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.689,-92.173 -37.013,-94.517 -24.975,-94.690 -22.924,-91.882 -25.431,-75.948 -27.632,-74.631" fill="#9531a8" stroke="#db96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-85.916,-40.673 -77.575,-35.031 -77.629,-33.318 -83.562,-25.234 -86.008,-24.221 -93.799,-33.509" fill="#9331a7" stroke="#da96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-11.592,96.916 -2.152,85.068 1.097,85.952 2.926,94.337 -3.744,100.188 -11.092,97.416" fill="#532786" stroke="#bc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="96.750,4.997 99.990,7.055 101.414,12.592 97.341,21.408 94.601,22.156 79.174,13.675 79.120,12.630" fill="#8a2fa3" stroke="#d796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-99.067,23.672 -98.256,22.667 -78.250,28.040 -78.894,32.116 -97.170,33.689 -99.151,29.878 -99.540,25.409" fill="#2b1f6a" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="98.559,-29.464 78.473,-30.157 79.814,-36.297 95.954,-37.733 98.033,-34.217" fill="#2f206e" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-97.081,34.733 -78.804,33.160 -78.547,34.287 -80.990,41.284 -96.240,41.248" fill="#352172" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.917,-69.662 77.951,-57.657 66.928,-54.752 61.541,-65.226 61.514,-66.259" fill="#9932ab" stroke="#dc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-106.275,0.076 -90.735,-8.134 -86.891,-4.130 -85.146,2.023 -88.490,8.725 -97.141,13.493" fill="#5d298d" stroke="#c196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="93.122,42.471 93.281,44.557 86.323,50.027 80.755,49.723 79.629,40.921 85.189,35.386 86.721,35.337" fill="#672b93" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.065,82.765 60.915,83.129 55.134,77.453 60.552,61.410 60.980,61.264 64.676,61.346" fill="#2f206e" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-84.279,53.547 -78.752,48.874 -74.253,51.220 -71.609,60.087 -73.058,62.745 -82.854,63.904" fill="#752d9a" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.518,-98.721 -0.912,-89.894 -7.221,-91.748 -6.200,-100.469 1.470,-106.406" fill="#552888" stroke="#bd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="95.858,-38.804 79.719,-37.368 79.001,-40.472 82.808,-46.644 95.003,-44.964" fill="#3b2377" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.262,101.648 15.414,98.753 14.951,97.575 18.965,86.630 30.282,86.416" fill="#9e33ae" stroke="#de96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-46.001,98.350 -49.849,86.433 -44.120,81.423 -31.946,82.082 -30.370,83.810 -27.875,90.851" fill="#4a2682" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-98.077,48.846 -96.243,42.403 -80.993,42.438 -79.861,47.562 -85.388,52.236" fill="#40247c" stroke="#b096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-77.110,-79.548 -75.364,-81.782 -73.352,-82.956 -67.579,-83.611 -62.881,-81.049 -64.020,-61.520 -66.117,-60.758" fill="#302070" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.031,-105.198 -7.919,-100.670 -8.939,-91.949 -10.103,-91.265 -20.922,-93.344 -22.974,-96.153" fill="#7f2ea0" stroke="#d096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-106.738,-21.913 -98.077,-32.775 -95.310,-32.241 -87.520,-22.953 -91.357,-14.686" fill="#a333b1" stroke="#e096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="49.593,98.909 36.774,83.622 42.292,78.378 53.172,79.451 58.954,85.127 51.215,98.491" fill="#832fa2" stroke="#d296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="71.962,82.685 71.242,82.469 65.853,61.050 68.394,60.111 81.558,74.931 77.876,80.258 74.802,82.174" fill="#2e206f" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-86.932,-73.116 -78.074,-78.984 -67.082,-60.194 -67.507,-59.845 -71.517,-58.439" fill="#2c206d" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.997,119.232 -2.690,101.390 3.980,95.538 12.665,98.474 13.128,99.652" fill="#522888" stroke="#ba96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="88.943,69.618 82.376,74.204 69.211,59.384 71.676,57.286 75.622,56.146" fill="#2b206d" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-93.483,-70.588 -87.687,-72.323 -72.272,-57.646 -75.781,-54.590 -80.600,-53.806" fill="#2d206f" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="105.229,32.556 94.534,41.204 88.133,34.070 95.303,24.730 98.044,23.982" fill="#8530a4" stroke="#d396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-103.377,-53.102 -87.427,-46.953 -86.961,-41.823 -94.844,-34.659 -97.611,-35.193 -102.479,-42.245" fill="#5e2a90" stroke="#c096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="102.496,-63.924 95.182,-46.267 82.988,-47.948 81.524,-55.601" fill="#43257f" stroke="#b196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="68.788,-87.576 70.215,-87.151 76.522,-74.297 74.428,-71.586 61.026,-68.183 57.102,-73.008" fill="#a834b4" stroke="#e196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="67.398,-88.690 55.713,-74.123 51.408,-75.602 48.271,-89.504 51.256,-94.666 63.130,-93.736" fill="#9d33af" stroke="#dd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="94.355,67.491 89.732,68.837 76.411,55.366 80.649,51.669 86.217,51.973" fill="#312172" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-107.215,-1.703 -110.041,-2.515 -113.253,-10.780 -109.147,-19.885 -107.518,-20.253 -92.137,-13.026 -91.675,-9.913" fill="#a935b5" stroke="#e196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="48.189,100.087 44.212,104.378 31.769,104.593 30.171,101.901 32.190,86.669 34.497,85.002 35.370,84.800" fill="#ab35b6" stroke="#e296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.823,-107.334 48.747,-96.116 45.763,-90.955 29.481,-88.612 27.366,-90.764 26.359,-92.809" fill="#53288a" stroke="#bb96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-94.408,-69.878 -81.525,-53.096 -86.773,-48.650 -102.723,-54.799 -111.171,-69.361" fill="#352275" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="114.892,-4.838 101.695,4.372 98.455,2.313 93.502,-8.735 95.792,-13.871 100.586,-18.178" fill="#782e9e" stroke="#cc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-80.029,81.025 -83.054,66.399 -82.609,65.979 -72.813,64.820 -67.007,76.467 -67.040,77.053" fill="#8a31a7" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="102.756,-66.697 101.814,-65.642 80.842,-57.319 79.460,-58.039 76.426,-70.043 78.519,-72.754 88.179,-73.430" fill="#662b95" stroke="#c496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-37.632,113.191 -39.367,112.876 -42.027,111.138 -46.223,103.662 -45.393,99.821 -27.266,92.321 -25.510,93.984" fill="#55298c" stroke="#bb96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-117.010,69.451 -98.483,50.366 -85.794,53.756 -84.370,64.113 -84.815,64.533" fill="#422580" stroke="#b096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="121.540,68.249 95.514,66.883 87.375,51.365 94.333,45.895" fill="#372378" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-16.728,127.797 -23.842,120.572 -20.450,96.190 -13.545,98.866 -13.045,99.367" fill="#332276" stroke="#a796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.814,147.223 -15.567,127.948 -11.884,99.517 -4.536,102.288 4.150,120.131 4.625,129.177" fill="#2f2172" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.396,128.979 7.922,119.933 15.053,100.353 27.902,103.248 29.500,105.940 25.019,116.504" fill="#6b2c99" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.211,-141.495 0.754,-135.229 0.291,-107.930 -7.380,-101.993 -18.492,-106.520 -18.858,-108.684" fill="#4b2786" stroke="#b596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="3.803,-135.177 18.547,-120.876 16.674,-97.606 16.266,-97.609 9.389,-100.193 3.340,-107.878" fill="#2f2173" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-36.458,-112.026 -21.433,-108.248 -21.067,-106.085 -25.009,-97.040 -37.047,-96.866" fill="#9032ab" stroke="#d696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.460,-104.461 -44.964,-92.607 -52.297,-83.544 -61.350,-83.859 -66.047,-86.420 -59.983,-104.031" fill="#a034b2" stroke="#dd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="30.491,-115.338 37.303,-114.517 40.130,-112.796 42.956,-108.658 42.970,-108.361 25.506,-93.835 24.157,-95.044" fill="#4f2889" stroke="#b896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-25.007,120.410 -36.610,113.836 -24.489,94.628 -21.962,95.724 -21.615,96.028" fill="#462683" stroke="#b296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.719,-120.782 29.370,-115.688 23.035,-95.394 17.846,-97.511" fill="#3b247c" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-121.829,10.569 -110.906,0.491 -108.079,1.304 -98.945,14.721 -101.105,20.368 -101.916,21.373 -121.920,11.167" fill="#8831a8" stroke="#d396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="130.423,68.784 122.783,66.736 95.576,44.382 95.418,42.296 106.112,33.648 108.857,34.562" fill="#51288b" stroke="#b896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="119.671,18.705 119.876,19.458 109.704,32.020 106.959,31.105 99.774,22.532 103.846,13.715" fill="#9733af" stroke="#d896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-68.600,100.166 -60.251,85.514 -52.333,87.236 -48.486,99.153 -49.316,102.994 -64.899,104.361" fill="#b336bc" stroke="#e396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-117.858,-31.252 -104.355,-40.951 -99.486,-33.898 -108.147,-23.037 -109.776,-22.669" fill="#6e2d9c" stroke="#c796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.318,99.187 -74.689,98.371 -81.219,85.233 -79.385,83.133 -66.396,79.162 -61.970,84.535" fill="#a034b4" stroke="#dc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="120.271,16.802 104.447,11.812 103.023,6.275 116.220,-2.934 118.003,-2.992 124.109,7.866" fill="#b437bc" stroke="#e396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-135.485,85.689 -130.891,80.230 -116.679,71.613 -84.485,66.694 -81.460,81.321 -83.294,83.420 -134.247,87.746 -135.478,85.722" fill="#54298e" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-57.080,-106.031 -51.899,-113.200 -39.420,-113.580 -38.553,-112.108 -39.142,-96.948 -41.818,-94.604 -43.584,-94.177" fill="#ab36b9" stroke="#e096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="125.254,-15.022 117.899,-6.208 116.116,-6.151 101.810,-19.491 102.930,-22.963 125.782,-19.430" fill="#8e32ac" stroke="#d496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="141.482,-75.710 141.979,-74.904 131.702,-55.413 100.430,-35.634 98.350,-39.151 97.494,-45.310 104.808,-62.966 105.750,-64.021" fill="#3b247e" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-133.605,75.754 -124.387,44.220 -101.854,31.282 -99.873,35.094 -99.033,41.609 -100.867,48.052 -119.394,67.137" fill="#472786" stroke="#b296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.849,107.957 53.303,99.700 61.043,86.337 70.193,85.973 70.913,86.189 69.083,107.258" fill="#b136bc" stroke="#e296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-124.994,27.760 -126.973,22.549 -126.644,18.682 -122.602,12.504 -102.598,22.710 -103.071,24.447" fill="#8b32ab" stroke="#d396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.937,-93.508 88.008,-75.870 78.349,-75.193 72.042,-88.048" fill="#712e9e" stroke="#c796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="132.383,-54.336 126.599,-37.540 102.512,-27.856 101.636,-29.804 101.111,-34.557" fill="#592a91" stroke="#bb96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-125.026,43.106 -124.805,29.012 -102.882,25.700 -102.493,30.169" fill="#6d2d9c" stroke="#c596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="127.073,-36.361 126.324,-21.394 125.985,-20.742 103.133,-24.275 102.986,-26.676" fill="#7f30a6" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.397,-142.604 -21.044,-109.793 -36.069,-113.571 -36.937,-115.043 -33.091,-125.935 -5.725,-149.170" fill="#552a90" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="45.910,121.548 45.331,122.161 27.328,117.483 31.809,106.919 44.252,106.704" fill="#762fa2" stroke="#c996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-140.352,-80.784 -114.473,-67.445 -106.025,-52.883 -105.127,-42.026 -118.631,-32.327 -120.465,-33.005 -146.276,-72.475" fill="#442786" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="68.102,-110.687 63.337,-96.378 51.463,-97.308 46.539,-108.526 46.525,-108.823 64.672,-114.362" fill="#bc38c2" stroke="#e596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-74.117,-109.536 -61.908,-104.694 -67.972,-87.083 -73.745,-86.429 -78.391,-108.045" fill="#b938c1" stroke="#e496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-130.361,-13.620 -131.786,-17.283 -121.488,-30.239 -119.654,-29.561 -111.571,-20.978 -115.677,-11.874" fill="#7d30a7" stroke="#cc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-130.615,-11.486 -115.931,-9.740 -112.719,-1.475 -123.643,8.603 -133.724,-3.672" fill="#b037be" stroke="#e096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="128.922,105.681 116.725,104.099 80.438,82.028 84.120,76.701 90.687,72.115 95.310,70.768 121.336,72.134 128.976,74.182 138.250,79.205 139.496,80.342 138.228,88.249" fill="#8031a8" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="132.665,67.371 111.099,33.149 121.271,20.587 131.417,29.277 141.940,72.394" fill="#562a92" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="87.579,106.416 79.779,109.461 70.790,107.407 72.620,86.337 75.460,85.825" fill="#bc39c3" stroke="#e596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="48.114,121.302 46.457,106.458 50.434,102.167 52.055,101.748 65.601,110.004 60.078,120.644" fill="#9c35b5" stroke="#d896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.195,-109.990 77.996,-109.518 84.750,-96.557 84.020,-95.672 71.126,-90.211 69.698,-90.636 65.430,-95.681" fill="#8a32ad" stroke="#d196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-132.568,-97.907 -96.989,-101.665 -78.139,-83.952 -79.885,-81.718 -88.743,-75.850 -94.539,-74.115 -111.302,-73.598 -137.181,-86.937 -137.025,-88.332" fill="#9a35b5" stroke="#d796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="139.728,-81.071 103.996,-69.382 89.419,-76.115 86.348,-93.754 87.077,-94.639 132.120,-101.325 138.636,-88.694" fill="#452788" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-96.058,-102.655 -83.579,-108.083 -79.842,-107.733 -75.197,-86.117 -77.208,-84.942" fill="#b938c2" stroke="#e396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="116.005,105.283 88.763,105.719 76.644,85.129 79.718,83.213" fill="#ae37be" stroke="#df96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-66.189,119.655 -64.698,106.653 -49.115,105.285 -44.919,112.761 -62.251,123.596" fill="#bf39c5" stroke="#e596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-134.015,90.475 -83.062,86.149 -76.532,99.287 -87.897,106.344 -131.621,96.925" fill="#582b94" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.313,166.662 3.257,161.904 2.862,149.563 10.301,131.516 26.924,119.041 44.926,123.719 34.776,166.645" fill="#45288a" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="139.257,5.275 126.349,6.606 120.244,-4.252 127.599,-13.065 141.851,-1.968" fill="#a837bd" stroke="#dc96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.562,-131.864 63.255,-126.589 64.099,-116.242 45.951,-110.704 43.125,-114.842" fill="#c13bc6" stroke="#e596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-55.975,-128.051 -53.568,-131.215 -35.646,-126.837 -39.492,-115.945 -51.971,-115.566" fill="#5d2c98" stroke="#bb96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.501,-167.675 43.134,-161.258 37.689,-117.724 30.878,-118.544 21.226,-123.638 6.483,-137.939 3.518,-144.205 2.190,-150.771 3.358,-163.319 16.352,-168.123 40.745,-167.993" fill="#bf3bc6" stroke="#e496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.192,-127.340 -54.187,-114.855 -59.369,-107.686 -60.892,-107.257 -73.101,-112.098 -68.757,-126.390" fill="#8b33b1" stroke="#d096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-50.778,138.857 -59.765,131.194 -61.356,125.029 -44.024,114.194 -41.364,115.932" fill="#c43ec7" stroke="#e796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="139.497,7.600 143.028,14.105 133.102,27.309 122.956,18.619 122.752,17.867 126.589,8.932" fill="#652e9e" stroke="#bf96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-86.611,108.415 -75.246,101.358 -70.876,102.174 -67.175,106.369 -68.666,119.371 -79.860,120.397" fill="#7430a6" stroke="#c696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.420,167.744 -49.438,139.407 -40.023,116.482 -38.288,116.798 -26.685,123.371 -19.571,130.597 -6.817,149.872 -6.423,162.214 -16.994,166.779" fill="#c640c7" stroke="#e896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.590,-161.076 56.408,-132.843 41.972,-115.820 39.145,-117.542" fill="#c740c8" stroke="#e896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-169.400,-40.758 -158.427,-62.193 -148.747,-70.859 -122.935,-31.390 -133.234,-18.434" fill="#46298c" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-145.795,1.327 -135.461,-2.245 -125.380,10.030 -125.471,10.627 -129.513,16.806 -148.251,9.179" fill="#a437bd" stroke="#d996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="129.163,-114.423 131.689,-104.231 86.646,-97.545 79.892,-110.506 107.236,-136.427 109.256,-137.622 114.573,-134.576" fill="#49298f" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="68.161,131.438 62.331,121.813 67.854,111.174 70.089,110.476 79.078,112.530 77.967,130.134" fill="#8032ad" stroke="#ca96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="165.196,-17.022 143.044,-3.502 128.793,-14.598 129.320,-19.006 129.660,-19.659" fill="#a737bf" stroke="#da96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-173.800,34.649 -172.153,33.577 -130.133,23.749 -128.154,28.960 -128.375,43.054 -137.594,74.588 -142.188,80.047 -161.881,63.604 -173.510,41.362" fill="#c643c8" stroke="#e796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="37.521,167.294 47.670,124.368 48.249,123.755 60.213,123.097 66.043,132.721 45.479,168.478 43.648,168.774" fill="#4d2b93" stroke="#b196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-7.881,-151.709 -35.246,-128.473 -53.168,-132.852 -50.231,-169.926 -42.778,-172.443 -17.599,-169.793 -6.712,-164.256" fill="#41288b" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-172.503,32.078 -148.893,10.756 -130.155,18.383 -130.484,22.250" fill="#b039c4" stroke="#dd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-172.243,-34.567 -171.152,-38.081 -170.792,-38.502 -134.626,-16.179 -133.201,-12.515 -136.310,-4.701 -146.644,-1.128" fill="#552c98" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="105.510,-138.248 78.166,-112.326 70.365,-112.799 66.935,-116.473 66.092,-126.820" fill="#652fa1" stroke="#bd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="146.586,-72.475 160.452,-62.166 173.811,-40.638 175.961,-22.286 165.312,-18.584 129.776,-21.221 130.525,-36.188 136.309,-52.984" fill="#bc3ec7" stroke="#e296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-91.220,-147.181 -70.894,-127.040 -75.239,-112.748 -79.512,-111.257 -83.249,-111.607" fill="#7c32ad" stroke="#c796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="177.511,27.305 175.746,40.924 161.526,62.346 146.434,72.738 145.188,71.601 134.666,28.484 144.591,15.280" fill="#44298e" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-98.427,152.157 -79.628,122.924 -68.435,121.898 -64.497,125.840 -62.906,132.004 -98.255,152.355" fill="#5e2e9f" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="88.825,109.608 116.067,109.172 128.264,110.754 127.878,115.511 116.426,139.030 106.539,146.594 79.914,130.256 81.025,112.653" fill="#8033b0" stroke="#c896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-100.630,150.740 -117.701,139.519 -130.390,116.833 -132.307,100.106 -88.583,109.526 -81.831,121.508" fill="#462a91" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-97.153,-153.425 -92.917,-146.800 -84.947,-111.227 -97.425,-105.799 -133.005,-102.041 -131.244,-117.408 -118.333,-140.038" fill="#8b35b6" stroke="#cd96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.078,-170.151 -56.016,-133.077 -58.423,-129.913 -68.988,-128.963 -89.314,-149.104 -93.549,-155.729 -93.228,-156.236 -71.196,-169.353" fill="#492b93" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="166.728,-14.512 177.376,-18.214 181.190,-11.957 183.311,13.610 178.433,24.780 145.514,12.755 141.982,6.251 144.576,-0.992" fill="#4f2c97" stroke="#b196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="106.905,-141.598 104.885,-140.404 65.467,-128.976 59.774,-134.251 47.955,-162.485 46.322,-168.902 72.187,-169.916 95.303,-158.408" fill="#5d2ea0" stroke="#b896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-72.547,172.924 -94.588,158.927 -97.268,154.070 -61.919,133.719 -52.932,141.383 -36.915,169.720 -45.856,173.578" fill="#612fa3" stroke="#b996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-186.190,-12.944 -174.274,-33.012 -148.675,0.426 -151.131,8.278 -174.741,29.600 -176.389,30.672 -186.446,13.123" fill="#4e2c99" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="97.083,161.103 73.393,172.265 47.957,169.904 68.522,134.147 78.328,132.842 104.952,149.180" fill="#4c2c98" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><g fill="none" stroke="#d1e7ed" stroke-width="0.55" opacity="0.22"><polygon points="3.367,6.615 -8.947,-3.351 11.127,-4.486" /><polygon points="3.367,6.615 11.127,-4.486 14.115,8.336" /><polygon points="-8.947,-3.351 3.367,6.615 -8.386,17.544" /><polygon points="11.127,-4.486 -8.947,-3.351 -5.657,-18.568" /><polygon points="-8.386,17.544 3.367,6.615 2.610,21.528" /><polygon points="2.610,21.528 3.367,6.615 14.115,8.336" /><polygon points="-8.947,-3.351 -8.386,17.544 -23.337,4.930" /><polygon points="-5.657,-18.568 -8.947,-3.351 -18.689,-16.016" /><polygon points="-18.689,-16.016 -8.947,-3.351 -23.337,4.930" /><polygon points="11.127,-4.486 -5.657,-18.568 1.362,-25.001" /><polygon points="11.127,-4.486 1.362,-25.001 16.392,-22.914" /><polygon points="11.127,-4.486 16.392,-22.914 23.307,-15.933" /><polygon points="-23.337,4.930 -8.386,17.544 -21.014,20.571" /><polygon points="-23.337,4.930 -21.014,20.571 -29.354,13.271" /><polygon points="-18.689,-16.016 -23.337,4.930 -29.343,-15.731" /><polygon points="-8.386,17.544 2.610,21.528 -7.799,33.384" /><polygon points="-21.014,20.571 -8.386,17.544 -7.799,33.384" /><polygon points="14.115,8.336 11.127,-4.486 34.170,-1.132" /><polygon points="34.170,-1.132 11.127,-4.486 23.307,-15.933" /><polygon points="14.115,8.336 34.170,-1.132 33.515,10.997" /><polygon points="2.610,21.528 14.115,8.336 22.811,27.796" /><polygon points="14.115,8.336 30.792,24.255 22.811,27.796" /><polygon points="-29.343,-15.731 -23.337,4.930 -37.590,-5.373" /><polygon points="-7.799,33.384 2.610,21.528 -0.497,37.975" /><polygon points="2.610,21.528 14.677,41.885 -0.497,37.975" /><polygon points="30.792,24.255 14.115,8.336 33.515,10.997" /><polygon points="1.362,-25.001 -5.657,-18.568 -6.329,-38.733" /><polygon points="-29.354,13.271 -21.014,20.571 -33.882,25.279" /><polygon points="23.307,-15.933 41.898,-10.677 34.170,-1.132" /><polygon points="41.898,-10.677 23.307,-15.933 38.288,-19.253" /><polygon points="-5.657,-18.568 -18.689,-16.016 -21.692,-38.588" /><polygon points="-6.329,-38.733 -5.657,-18.568 -21.692,-38.588" /><polygon points="-21.692,-38.588 -18.689,-16.016 -29.343,-15.731" /><polygon points="14.677,41.885 2.610,21.528 22.811,27.796" /><polygon points="30.792,24.255 33.515,10.997 40.481,19.594" /><polygon points="16.392,-22.914 1.362,-25.001 18.083,-42.231" /><polygon points="1.362,-25.001 8.680,-45.583 18.083,-42.231" /><polygon points="8.680,-45.583 1.362,-25.001 -6.329,-38.733" /><polygon points="8.680,-45.583 -6.329,-38.733 -1.350,-47.801" /><polygon points="-21.692,-38.588 -29.343,-15.731 -39.102,-28.263" /><polygon points="23.307,-15.933 16.392,-22.914 33.340,-35.450" /><polygon points="38.288,-19.253 23.307,-15.933 33.340,-35.450" /><polygon points="33.340,-35.450 16.392,-22.914 18.083,-42.231" /><polygon points="-29.354,13.271 -33.882,25.279 -44.725,24.856" /><polygon points="-29.343,-15.731 -37.590,-5.373 -49.514,-14.008" /><polygon points="-39.102,-28.263 -29.343,-15.731 -49.514,-14.008" /><polygon points="-21.014,20.571 -7.799,33.384 -28.427,43.422" /><polygon points="-33.882,25.279 -21.014,20.571 -28.427,43.422" /><polygon points="-21.692,-38.588 -39.102,-28.263 -34.521,-38.689" /><polygon points="-0.497,37.975 14.677,41.885 10.751,52.151" /><polygon points="-29.354,13.271 -37.590,-5.373 -23.337,4.930" /><polygon points="-37.590,-5.373 -29.354,13.271 -51.666,15.465" /><polygon points="-51.666,15.465 -29.354,13.271 -44.725,24.856" /><polygon points="-7.799,33.384 -0.497,37.975 -7.945,53.718" /><polygon points="-28.427,43.422 -7.799,33.384 -7.945,53.718" /><polygon points="-0.497,37.975 3.955,56.769 -7.945,53.718" /><polygon points="-37.590,-5.373 -51.666,15.465 -54.437,4.060" /><polygon points="-49.514,-14.008 -37.590,-5.373 -54.437,4.060" /><polygon points="38.288,-19.253 33.340,-35.450 42.281,-34.353" /><polygon points="-39.102,-28.263 -49.514,-14.008 -50.975,-22.122" /><polygon points="30.792,24.255 40.481,19.594 46.094,30.499" /><polygon points="3.955,56.769 -0.497,37.975 10.751,52.151" /><polygon points="33.515,10.997 34.170,-1.132 57.337,5.790" /><polygon points="40.481,19.594 33.515,10.997 57.337,5.790" /><polygon points="57.337,5.790 34.170,-1.132 41.898,-10.677" /><polygon points="38.288,-19.253 42.281,-34.353 50.201,-30.680" /><polygon points="-28.427,43.422 -7.945,53.718 -21.357,56.186" /><polygon points="8.680,-45.583 -1.350,-47.801 6.347,-59.933" /><polygon points="41.898,-10.677 38.288,-19.253 57.698,-19.903" /><polygon points="41.898,-10.677 67.383,-7.836 57.337,5.790" /><polygon points="57.698,-19.903 38.288,-19.253 50.201,-30.680" /><polygon points="-39.102,-28.263 -50.975,-22.122 -52.748,-31.849" /><polygon points="-6.329,-38.733 -21.692,-38.588 -22.796,-57.343" /><polygon points="-1.350,-47.801 -6.329,-38.733 -22.796,-57.343" /><polygon points="-22.796,-57.343 -21.692,-38.588 -34.521,-38.689" /><polygon points="22.811,27.796 30.792,24.255 42.953,44.309" /><polygon points="22.811,27.796 32.595,52.753 14.677,41.885" /><polygon points="42.953,44.309 30.792,24.255 46.094,30.499" /><polygon points="32.595,52.753 22.811,27.796 42.953,44.309" /><polygon points="10.751,52.151 14.677,41.885 32.595,52.753" /><polygon points="-33.882,25.279 -28.427,43.422 -42.112,48.658" /><polygon points="-33.882,25.279 -49.713,43.924 -44.725,24.856" /><polygon points="-7.945,53.718 3.955,56.769 -4.197,65.303" /><polygon points="-51.666,15.465 -44.725,24.856 -61.627,24.152" /><polygon points="-49.713,43.924 -33.882,25.279 -42.112,48.658" /><polygon points="-44.725,24.856 -55.403,37.088 -61.627,24.152" /><polygon points="40.481,19.594 57.337,5.790 64.001,15.281" /><polygon points="40.481,19.594 61.709,23.802 46.094,30.499" /><polygon points="-1.350,-47.801 -16.070,-64.373 -7.318,-65.725" /><polygon points="6.347,-59.933 -1.350,-47.801 -7.318,-65.725" /><polygon points="-55.403,37.088 -44.725,24.856 -49.713,43.924" /><polygon points="61.709,23.802 40.481,19.594 64.001,15.281" /><polygon points="-16.070,-64.373 -1.350,-47.801 -22.796,-57.343" /><polygon points="67.383,-7.836 41.898,-10.677 57.698,-19.903" /><polygon points="-34.521,-38.689 -52.748,-31.849 -49.553,-47.060" /><polygon points="-52.748,-31.849 -34.521,-38.689 -39.102,-28.263" /><polygon points="46.094,30.499 61.709,23.802 59.675,32.746" /><polygon points="33.340,-35.450 18.083,-42.231 34.941,-58.513" /><polygon points="18.083,-42.231 6.347,-59.933 22.006,-65.982" /><polygon points="34.941,-58.513 18.083,-42.231 22.006,-65.982" /><polygon points="6.347,-59.933 18.083,-42.231 8.680,-45.583" /><polygon points="57.698,-19.903 50.201,-30.680 64.402,-28.362" /><polygon points="6.347,-59.933 -7.318,-65.725 1.613,-70.542" /><polygon points="-49.514,-14.008 -54.437,4.060 -72.811,-4.323" /><polygon points="42.953,44.309 46.094,30.499 54.931,48.011" /><polygon points="54.931,48.011 46.094,30.499 59.675,32.746" /><polygon points="-61.627,24.152 -55.403,37.088 -66.695,32.726" /><polygon points="-22.796,-57.343 -34.521,-38.689 -41.918,-61.963" /><polygon points="-41.918,-61.963 -34.521,-38.689 -49.553,-47.060" /><polygon points="-52.748,-31.849 -50.975,-22.122 -67.294,-33.386" /><polygon points="50.201,-30.680 42.281,-34.353 60.651,-44.337" /><polygon points="64.402,-28.362 50.201,-30.680 60.651,-44.337" /><polygon points="-21.357,56.186 -4.197,65.303 -21.048,73.036" /><polygon points="-4.197,65.303 -21.357,56.186 -7.945,53.718" /><polygon points="3.955,56.769 10.751,52.151 11.509,75.022" /><polygon points="-4.197,65.303 3.955,56.769 11.509,75.022" /><polygon points="10.751,52.151 22.320,75.912 11.509,75.022" /><polygon points="-4.197,65.303 11.509,75.022 1.407,76.165" /><polygon points="-49.553,-47.060 -52.748,-31.849 -59.541,-47.848" /><polygon points="-59.541,-47.848 -52.748,-31.849 -67.294,-33.386" /><polygon points="31.110,69.718 10.751,52.151 32.595,52.753" /><polygon points="-50.975,-22.122 -49.514,-14.008 -72.999,-24.743" /><polygon points="-67.294,-33.386 -50.975,-22.122 -72.999,-24.743" /><polygon points="-72.999,-24.743 -49.514,-14.008 -72.811,-4.323" /><polygon points="-54.437,4.060 -61.627,24.152 -76.249,12.078" /><polygon points="-72.811,-4.323 -54.437,4.060 -76.249,12.078" /><polygon points="-61.627,24.152 -54.437,4.060 -51.666,15.465" /><polygon points="42.281,-34.353 34.941,-58.513 56.471,-54.211" /><polygon points="60.651,-44.337 42.281,-34.353 56.471,-54.211" /><polygon points="34.941,-58.513 42.281,-34.353 33.340,-35.450" /><polygon points="56.471,-54.211 34.941,-58.513 48.818,-61.358" /><polygon points="64.402,-28.362 60.651,-44.337 69.001,-37.471" /><polygon points="-72.999,-24.743 -72.811,-4.323 -77.792,-15.207" /><polygon points="22.320,75.912 10.751,52.151 31.110,69.718" /><polygon points="54.931,48.011 59.675,32.746 63.809,47.075" /><polygon points="-21.357,56.186 -42.112,48.658 -28.427,43.422" /><polygon points="-42.112,48.658 -21.357,56.186 -38.521,71.019" /><polygon points="-38.521,71.019 -21.357,56.186 -21.048,73.036" /><polygon points="22.006,-65.982 1.613,-70.542 19.048,-78.086" /><polygon points="1.613,-70.542 22.006,-65.982 6.347,-59.933" /><polygon points="-59.541,-47.848 -67.294,-33.386 -68.157,-46.059" /><polygon points="1.613,-70.542 -7.318,-65.725 -4.941,-82.517" /><polygon points="-21.048,73.036 -4.197,65.303 -16.388,81.710" /><polygon points="-16.388,81.710 -4.197,65.303 1.407,76.165" /><polygon points="-66.695,32.726 -55.403,37.088 -71.664,42.843" /><polygon points="59.675,32.746 61.709,23.802 75.880,33.814" /><polygon points="59.675,32.746 72.987,45.047 63.809,47.075" /><polygon points="-55.403,37.088 -49.713,43.924 -66.039,52.596" /><polygon points="-71.664,42.843 -55.403,37.088 -66.039,52.596" /><polygon points="67.383,-7.836 64.402,-28.362 82.123,-16.513" /><polygon points="64.402,-28.362 67.383,-7.836 57.698,-19.903" /><polygon points="32.595,52.753 42.953,44.309 50.109,67.851" /><polygon points="31.110,69.718 32.595,52.753 50.109,67.851" /><polygon points="50.109,67.851 42.953,44.309 54.931,48.011" /><polygon points="19.048,-78.086 1.613,-70.542 13.334,-84.449" /><polygon points="1.613,-70.542 6.618,-89.340 13.334,-84.449" /><polygon points="67.383,-7.836 64.001,15.281 57.337,5.790" /><polygon points="64.001,15.281 67.383,-7.836 85.577,1.539" /><polygon points="85.577,1.539 67.383,-7.836 82.123,-16.513" /><polygon points="72.987,45.047 59.675,32.746 75.880,33.814" /><polygon points="-7.318,-65.725 -16.070,-64.373 -16.609,-84.740" /><polygon points="-4.941,-82.517 -7.318,-65.725 -16.609,-84.740" /><polygon points="-16.388,81.710 1.407,76.165 -10.053,86.826" /><polygon points="-76.249,12.078 -66.695,32.726 -85.662,20.941" /><polygon points="-66.695,32.726 -76.249,12.078 -61.627,24.152" /><polygon points="64.001,15.281 75.880,33.814 61.709,23.802" /><polygon points="75.880,33.814 64.001,15.281 84.880,24.911" /><polygon points="64.001,15.281 90.931,13.903 84.880,24.911" /><polygon points="-22.796,-57.343 -41.918,-61.963 -39.626,-79.771" /><polygon points="-16.070,-64.373 -22.796,-57.343 -39.626,-79.771" /><polygon points="-16.070,-64.373 -29.456,-86.761 -16.609,-84.740" /><polygon points="6.618,-89.340 1.613,-70.542 -4.941,-82.517" /><polygon points="69.001,-37.471 60.651,-44.337 74.886,-49.079" /><polygon points="-49.713,43.924 -42.112,48.658 -62.532,65.204" /><polygon points="-66.039,52.596 -49.713,43.924 -62.532,65.204" /><polygon points="-42.112,48.658 -53.776,73.641 -62.532,65.204" /><polygon points="-68.157,-46.059 -67.294,-33.386 -78.663,-44.169" /><polygon points="1.407,76.165 11.509,75.022 9.112,89.788" /><polygon points="1.407,76.165 -3.016,92.434 -10.053,86.826" /><polygon points="-53.776,73.641 -42.112,48.658 -38.521,71.019" /><polygon points="34.941,-58.513 22.006,-65.982 39.915,-81.448" /><polygon points="48.818,-61.358 34.941,-58.513 39.915,-81.448" /><polygon points="39.915,-81.448 22.006,-65.982 19.048,-78.086" /><polygon points="-41.918,-61.963 -59.541,-47.848 -57.218,-71.128" /><polygon points="-39.626,-79.771 -41.918,-61.963 -57.218,-71.128" /><polygon points="-59.541,-47.848 -41.918,-61.963 -49.553,-47.060" /><polygon points="82.123,-16.513 64.402,-28.362 87.420,-25.241" /><polygon points="87.420,-25.241 64.402,-28.362 69.001,-37.471" /><polygon points="-29.456,-86.761 -16.070,-64.373 -39.626,-79.771" /><polygon points="-67.294,-33.386 -72.999,-24.743 -85.564,-33.966" /><polygon points="-78.663,-44.169 -67.294,-33.386 -85.564,-33.966" /><polygon points="-3.016,92.434 1.407,76.165 9.112,89.788" /><polygon points="90.931,13.903 64.001,15.281 85.577,1.539" /><polygon points="-85.662,20.941 -66.695,32.726 -87.927,29.374" /><polygon points="-66.695,32.726 -87.235,37.405 -87.927,29.374" /><polygon points="87.420,-25.241 69.001,-37.471 87.701,-33.387" /><polygon points="69.001,-37.471 86.968,-41.625 87.701,-33.387" /><polygon points="-87.235,37.405 -66.695,32.726 -71.664,42.843" /><polygon points="56.471,-54.211 74.886,-49.079 60.651,-44.337" /><polygon points="74.886,-49.079 56.471,-54.211 71.496,-61.939" /><polygon points="-72.811,-4.323 -76.249,12.078 -95.967,2.241" /><polygon points="-77.792,-15.207 -72.811,-4.323 -95.967,2.241" /><polygon points="-95.967,2.241 -76.249,12.078 -85.662,20.941" /><polygon points="72.987,45.047 75.880,33.814 85.472,43.449" /><polygon points="85.472,43.449 75.880,33.814 84.880,24.911" /><polygon points="50.109,67.851 54.931,48.011 63.244,72.287" /><polygon points="63.244,72.287 54.931,48.011 63.809,47.075" /><polygon points="-71.664,42.843 -66.039,52.596 -78.724,56.377" /><polygon points="-71.664,42.843 -87.256,46.288 -87.235,37.405" /><polygon points="-78.724,56.377 -66.039,52.596 -62.532,65.204" /><polygon points="6.618,-89.340 -4.941,-82.517 -0.841,-96.464" /><polygon points="86.968,-41.625 69.001,-37.471 74.886,-49.079" /><polygon points="22.320,75.912 9.112,89.788 11.509,75.022" /><polygon points="9.112,89.788 22.320,75.912 22.676,94.764" /><polygon points="-38.521,71.019 -21.048,73.036 -39.544,89.915" /><polygon points="-53.776,73.641 -38.521,71.019 -39.544,89.915" /><polygon points="-39.544,89.915 -21.048,73.036 -16.388,81.710" /><polygon points="-87.256,46.288 -71.664,42.843 -78.724,56.377" /><polygon points="-57.218,-71.128 -59.541,-47.848 -68.228,-71.770" /><polygon points="-59.541,-47.848 -75.646,-67.430 -68.228,-71.770" /><polygon points="-4.941,-82.517 -16.609,-84.740 -14.058,-98.011" /><polygon points="-0.841,-96.464 -4.941,-82.517 -14.058,-98.011" /><polygon points="-77.792,-15.207 -85.564,-33.966 -72.999,-24.743" /><polygon points="-85.564,-33.966 -77.792,-15.207 -97.193,-24.212" /><polygon points="-77.792,-15.207 -103.195,-11.438 -97.193,-24.212" /><polygon points="31.110,69.718 50.109,67.851 48.156,87.655" /><polygon points="48.156,87.655 50.109,67.851 63.244,72.287" /><polygon points="63.244,72.287 63.809,47.075 72.292,70.010" /><polygon points="63.809,47.075 78.582,64.423 72.292,70.010" /><polygon points="-75.646,-67.430 -59.541,-47.848 -68.157,-46.059" /><polygon points="-68.157,-46.059 -81.455,-61.330 -75.646,-67.430" /><polygon points="-3.016,92.434 9.112,89.788 5.093,101.677" /><polygon points="5.093,101.677 9.112,89.788 22.676,94.764" /><polygon points="78.582,64.423 63.809,47.075 72.987,45.047" /><polygon points="72.987,45.047 84.654,58.419 78.582,64.423" /><polygon points="-81.455,-61.330 -68.157,-46.059 -78.663,-44.169" /><polygon points="85.472,43.449 84.880,24.911 96.333,33.703" /><polygon points="-78.663,-44.169 -85.564,-33.966 -93.605,-42.812" /><polygon points="-78.663,-44.169 -88.572,-55.866 -81.455,-61.330" /><polygon points="-93.605,-42.812 -85.564,-33.966 -97.193,-24.212" /><polygon points="86.968,-41.625 74.886,-49.079 88.350,-51.655" /><polygon points="48.818,-61.358 71.496,-61.939 56.471,-54.211" /><polygon points="71.496,-61.939 48.818,-61.358 67.737,-76.742" /><polygon points="48.818,-61.358 57.050,-85.315 67.737,-76.742" /><polygon points="57.050,-85.315 48.818,-61.358 39.915,-81.448" /><polygon points="84.654,58.419 72.987,45.047 85.472,43.449" /><polygon points="-103.195,-11.438 -77.792,-15.207 -95.967,2.241" /><polygon points="22.320,75.912 31.110,69.718 37.357,96.710" /><polygon points="22.676,94.764 22.320,75.912 37.357,96.710" /><polygon points="37.357,96.710 31.110,69.718 48.156,87.655" /><polygon points="39.915,-81.448 19.048,-78.086 37.753,-96.474" /><polygon points="57.050,-85.315 39.915,-81.448 37.753,-96.474" /><polygon points="37.753,-96.474 19.048,-78.086 13.334,-84.449" /><polygon points="-88.572,-55.866 -78.663,-44.169 -93.605,-42.812" /><polygon points="85.577,1.539 82.123,-16.513 104.044,-6.739" /><polygon points="104.044,-6.739 82.123,-16.513 87.420,-25.241" /><polygon points="-78.724,56.377 -62.532,65.204 -76.836,72.334" /><polygon points="74.886,-49.079 71.496,-61.939 83.104,-64.873" /><polygon points="88.350,-51.655 74.886,-49.079 83.104,-64.873" /><polygon points="-39.544,89.915 -16.388,81.710 -34.863,101.228" /><polygon points="-87.256,46.288 -78.724,56.377 -90.379,57.980" /><polygon points="-90.379,57.980 -78.724,56.377 -76.836,72.334" /><polygon points="84.654,58.419 85.472,43.449 93.566,53.745" /><polygon points="-10.053,86.826 -27.003,106.189 -16.388,81.710" /><polygon points="-16.388,81.710 -27.003,106.189 -34.863,101.228" /><polygon points="-18.039,107.436 -10.053,86.826 -3.016,92.434" /><polygon points="-3.016,92.434 -9.110,108.592 -18.039,107.436" /><polygon points="-9.110,108.592 -3.016,92.434 5.093,101.677" /><polygon points="5.093,101.677 22.676,94.764 19.903,107.071" /><polygon points="-0.841,-96.464 -14.058,-98.011 -9.913,-108.185" /><polygon points="13.334,-84.449 6.618,-89.340 13.547,-107.788" /><polygon points="13.334,-84.449 31.185,-104.370 37.753,-96.474" /><polygon points="13.547,-107.788 6.618,-89.340 -0.841,-96.464" /><polygon points="13.547,-107.788 -0.841,-96.464 -9.913,-108.185" /><polygon points="-29.456,-86.761 -14.058,-98.011 -16.609,-84.740" /><polygon points="-14.058,-98.011 -29.456,-86.761 -29.716,-104.836" /><polygon points="-9.913,-108.185 -14.058,-98.011 -29.716,-104.836" /><polygon points="-39.626,-79.771 -57.218,-71.128 -56.445,-93.379" /><polygon points="-56.445,-93.379 -57.218,-71.128 -68.228,-71.770" /><polygon points="13.334,-84.449 22.561,-107.062 31.185,-104.370" /><polygon points="-27.003,106.189 -10.053,86.826 -18.039,107.436" /><polygon points="22.561,-107.062 13.334,-84.449 13.547,-107.788" /><polygon points="-95.967,2.241 -85.662,20.941 -109.843,11.688" /><polygon points="85.472,43.449 96.333,33.703 103.128,42.107" /><polygon points="93.566,53.745 85.472,43.449 103.128,42.107" /><polygon points="90.931,13.903 96.333,33.703 84.880,24.911" /><polygon points="96.333,33.703 90.931,13.903 109.644,22.547" /><polygon points="-53.776,73.641 -39.544,89.915 -58.657,96.085" /><polygon points="-93.605,-42.812 -97.193,-24.212 -108.031,-32.854" /><polygon points="-53.776,73.641 -76.836,72.334 -62.532,65.204" /><polygon points="-76.836,72.334 -53.776,73.641 -71.877,88.553" /><polygon points="-71.877,88.553 -53.776,73.641 -58.657,96.085" /><polygon points="90.931,13.903 104.044,-6.739 114.262,7.904" /><polygon points="109.644,22.547 90.931,13.903 114.262,7.904" /><polygon points="104.044,-6.739 90.931,13.903 85.577,1.539" /><polygon points="-90.379,57.980 -76.836,72.334 -87.839,74.609" /><polygon points="-29.456,-86.761 -39.626,-79.771 -45.831,-105.462" /><polygon points="-29.716,-104.836 -29.456,-86.761 -45.831,-105.462" /><polygon points="-45.831,-105.462 -39.626,-79.771 -56.445,-93.379" /><polygon points="104.044,-6.739 87.420,-25.241 113.462,-16.840" /><polygon points="87.701,-33.387 86.968,-41.625 106.135,-44.289" /><polygon points="106.135,-44.289 86.968,-41.625 88.350,-51.655" /><polygon points="-87.927,29.374 -87.235,37.405 -108.718,40.177" /><polygon points="-108.718,40.177 -87.235,37.405 -87.256,46.288" /><polygon points="48.156,87.655 63.244,72.287 64.224,96.960" /><polygon points="-109.843,11.688 -87.927,29.374 -115.092,21.976" /><polygon points="-87.927,29.374 -109.843,11.688 -85.662,20.941" /><polygon points="-87.927,29.374 -113.636,31.612 -115.092,21.976" /><polygon points="67.737,-76.742 83.104,-64.873 71.496,-61.939" /><polygon points="83.104,-64.873 67.737,-76.742 81.791,-83.637" /><polygon points="87.420,-25.241 87.701,-33.387 111.375,-36.003" /><polygon points="87.420,-25.241 115.023,-26.932 113.462,-16.840" /><polygon points="111.375,-36.003 87.701,-33.387 106.135,-44.289" /><polygon points="-113.636,31.612 -87.927,29.374 -108.718,40.177" /><polygon points="115.023,-26.932 87.420,-25.241 111.375,-36.003" /><polygon points="-9.913,-108.185 -29.716,-104.836 -26.729,-116.717" /><polygon points="37.357,96.710 19.903,107.071 22.676,94.764" /><polygon points="19.903,107.071 37.357,96.710 37.666,114.604" /><polygon points="-93.605,-42.812 -108.031,-32.854 -113.975,-41.128" /><polygon points="57.050,-85.315 37.753,-96.474 58.642,-105.643" /><polygon points="-56.445,-93.379 -68.228,-71.770 -71.254,-98.478" /><polygon points="-103.195,-11.438 -108.031,-32.854 -97.193,-24.212" /><polygon points="-108.031,-32.854 -103.195,-11.438 -121.842,-19.848" /><polygon points="-103.195,-11.438 -109.843,11.688 -123.795,-3.434" /><polygon points="-121.842,-19.848 -103.195,-11.438 -123.795,-3.434" /><polygon points="-109.843,11.688 -103.195,-11.438 -95.967,2.241" /><polygon points="72.292,70.010 78.582,64.423 91.998,83.630" /><polygon points="109.644,22.547 103.128,42.107 96.333,33.703" /><polygon points="103.128,42.107 109.644,22.547 120.377,31.237" /><polygon points="64.224,96.960 72.292,70.010 77.352,98.100" /><polygon points="72.292,70.010 64.224,96.960 63.244,72.287" /><polygon points="72.292,70.010 86.457,92.741 77.352,98.100" /><polygon points="37.357,96.710 48.156,87.655 54.624,112.711" /><polygon points="37.666,114.604 37.357,96.710 54.624,112.711" /><polygon points="54.624,112.711 48.156,87.655 64.224,96.960" /><polygon points="67.737,-76.742 57.050,-85.315 74.742,-100.282" /><polygon points="81.791,-83.637 67.737,-76.742 74.742,-100.282" /><polygon points="74.742,-100.282 57.050,-85.315 58.642,-105.643" /><polygon points="-68.228,-71.770 -75.646,-67.430 -89.578,-88.460" /><polygon points="-68.228,-71.770 -82.419,-96.079 -71.254,-98.478" /><polygon points="83.104,-64.873 81.791,-83.637 92.645,-85.527" /><polygon points="-82.419,-96.079 -68.228,-71.770 -89.578,-88.460" /><polygon points="86.457,92.741 72.292,70.010 91.998,83.630" /><polygon points="-58.657,96.085 -34.863,101.228 -57.110,113.715" /><polygon points="-34.863,101.228 -58.657,96.085 -39.544,89.915" /><polygon points="-71.877,88.553 -87.839,74.609 -76.836,72.334" /><polygon points="-87.839,74.609 -71.877,88.553 -86.057,95.600" /><polygon points="19.903,107.071 37.666,114.604 34.551,126.588" /><polygon points="114.262,7.904 113.462,-16.840 131.497,-1.788" /><polygon points="113.462,-16.840 114.262,7.904 104.044,-6.739" /><polygon points="58.642,-105.643 31.185,-104.370 54.228,-120.108" /><polygon points="31.185,-104.370 58.642,-105.643 37.753,-96.474" /><polygon points="-45.831,-105.462 -26.729,-116.717 -29.716,-104.836" /><polygon points="-26.729,-116.717 -45.831,-105.462 -46.385,-123.658" /><polygon points="31.185,-104.370 22.561,-107.062 34.156,-129.034" /><polygon points="31.185,-104.370 45.354,-127.633 54.228,-120.108" /><polygon points="-45.831,-105.462 -56.445,-93.379 -63.438,-118.189" /><polygon points="-46.385,-123.658 -45.831,-105.462 -63.438,-118.189" /><polygon points="-63.438,-118.189 -56.445,-93.379 -71.254,-98.478" /><polygon points="-57.110,113.715 -34.863,101.228 -50.221,124.735" /><polygon points="109.644,22.547 114.262,7.904 133.342,16.099" /><polygon points="120.377,31.237 109.644,22.547 133.342,16.099" /><polygon points="133.342,16.099 114.262,7.904 131.497,-1.788" /><polygon points="-71.877,88.553 -58.657,96.085 -76.165,111.530" /><polygon points="-86.057,95.600 -71.877,88.553 -76.165,111.530" /><polygon points="-76.165,111.530 -58.657,96.085 -57.110,113.715" /><polygon points="-34.863,101.228 -27.003,106.189 -39.910,128.970" /><polygon points="-50.221,124.735 -34.863,101.228 -39.910,128.970" /><polygon points="45.354,-127.633 31.185,-104.370 34.156,-129.034" /><polygon points="-121.842,-19.848 -113.975,-41.128 -108.031,-32.854" /><polygon points="-113.975,-41.128 -121.842,-19.848 -132.979,-28.700" /><polygon points="-123.795,-3.434 -109.843,11.688 -137.159,7.541" /><polygon points="-137.159,7.541 -109.843,11.688 -115.092,21.976" /><polygon points="74.742,-100.282 92.645,-85.527 81.791,-83.637" /><polygon points="92.645,-85.527 74.742,-100.282 89.326,-107.882" /><polygon points="54.624,112.711 64.224,96.960 71.957,121.709" /><polygon points="71.957,121.709 64.224,96.960 77.352,98.100" /><polygon points="131.497,-1.788 113.462,-16.840 140.681,-13.583" /><polygon points="140.681,-13.583 113.462,-16.840 115.023,-26.932" /><polygon points="-115.092,21.976 -113.636,31.612 -139.400,31.208" /><polygon points="-115.092,21.976 -142.097,19.675 -137.159,7.541" /><polygon points="37.666,114.604 54.624,112.711 55.662,131.580" /><polygon points="34.551,126.588 37.666,114.604 55.662,131.580" /><polygon points="55.662,131.580 54.624,112.711 71.957,121.709" /><polygon points="-26.729,-116.717 -46.385,-123.658 -43.310,-136.246" /><polygon points="-142.097,19.675 -115.092,21.976 -139.400,31.208" /><polygon points="-121.842,-19.848 -123.795,-3.434 -143.689,-11.349" /><polygon points="-132.979,-28.700 -121.842,-19.848 -143.689,-11.349" /><polygon points="-143.689,-11.349 -123.795,-3.434 -137.159,7.541" /><polygon points="74.742,-100.282 58.642,-105.643 76.050,-121.887" /><polygon points="89.326,-107.882 74.742,-100.282 76.050,-121.887" /><polygon points="76.050,-121.887 58.642,-105.643 54.228,-120.108" /><polygon points="140.681,-13.583 115.023,-26.932 141.573,-25.603" /><polygon points="-63.438,-118.189 -71.254,-98.478 -79.878,-123.186" /><polygon points="-79.878,-123.186 -71.254,-98.478 -82.419,-96.079" /><polygon points="120.377,31.237 133.342,16.099 145.368,25.138" /><polygon points="-76.165,111.530 -57.110,113.715 -74.383,130.972" /><polygon points="-74.383,130.972 -57.110,113.715 -50.221,124.735" /><polygon points="71.957,121.709 77.352,98.100 86.936,122.654" /><polygon points="-86.057,95.600 -76.165,111.530 -91.329,120.074" /><polygon points="-91.329,120.074 -76.165,111.530 -74.383,130.972" /><polygon points="-79.878,-123.186 -82.419,-96.079 -92.937,-120.260" /><polygon points="-46.385,-123.658 -63.438,-118.189 -65.217,-137.982" /><polygon points="-43.310,-136.246 -46.385,-123.658 -65.217,-137.982" /><polygon points="-65.217,-137.982 -63.438,-118.189 -79.878,-123.186" /><polygon points="133.342,16.099 131.497,-1.788 152.462,5.719" /><polygon points="145.368,25.138 133.342,16.099 152.462,5.719" /><polygon points="152.462,5.719 131.497,-1.788 140.681,-13.583" /><polygon points="76.050,-121.887 54.228,-120.108 71.242,-138.470" /><polygon points="-74.383,130.972 -50.221,124.735 -66.789,144.163" /><polygon points="-143.689,-11.349 -137.159,7.541 -159.312,0.612" /><polygon points="-159.312,0.612 -137.159,7.541 -142.097,19.675" /><polygon points="55.662,131.580 71.957,121.709 74.729,142.546" /><polygon points="74.729,142.546 71.957,121.709 86.936,122.654" /><polygon points="-87.839,74.609 -139.400,31.208 -108.718,40.177" /><polygon points="-90.379,57.980 -108.718,40.177 -87.256,46.288" /><polygon points="-87.839,74.609 -108.718,40.177 -90.379,57.980" /><polygon points="-108.718,40.177 -139.400,31.208 -113.636,31.612" /><polygon points="-139.400,31.208 -159.312,0.612 -142.097,19.675" /><polygon points="-39.910,128.970 -66.789,144.163 -50.221,124.735" /><polygon points="-9.110,108.592 -39.910,128.970 -18.039,107.436" /><polygon points="-18.039,107.436 -39.910,128.970 -27.003,106.189" /><polygon points="-113.975,-41.128 -89.578,-88.460 -88.572,-55.866" /><polygon points="-113.975,-41.128 -88.572,-55.866 -93.605,-42.812" /><polygon points="34.551,126.588 -39.910,128.970 -9.110,108.592" /><polygon points="-9.110,108.592 19.903,107.071 34.551,126.588" /><polygon points="19.903,107.071 -9.110,108.592 5.093,101.677" /><polygon points="-81.455,-61.330 -89.578,-88.460 -75.646,-67.430" /><polygon points="-89.578,-88.460 -92.937,-120.260 -82.419,-96.079" /><polygon points="-92.937,-120.260 -65.217,-137.982 -79.878,-123.186" /><polygon points="86.457,92.741 86.936,122.654 77.352,98.100" /><polygon points="91.998,83.630 86.936,122.654 86.457,92.741" /><polygon points="34.156,-129.034 -9.913,-108.185 -26.729,-116.717" /><polygon points="-9.913,-108.185 34.156,-129.034 13.547,-107.788" /><polygon points="91.998,83.630 84.654,58.419 93.566,53.745" /><polygon points="84.654,58.419 91.998,83.630 78.582,64.423" /><polygon points="34.156,-129.034 -26.729,-116.717 -43.310,-136.246" /><polygon points="91.998,83.630 103.128,42.107 120.377,31.237" /><polygon points="103.128,42.107 91.998,83.630 93.566,53.745" /><polygon points="145.368,25.138 91.998,83.630 120.377,31.237" /><polygon points="13.547,-107.788 34.156,-129.034 22.561,-107.062" /><polygon points="34.156,-129.034 71.242,-138.470 45.354,-127.633" /><polygon points="45.354,-127.633 71.242,-138.470 54.228,-120.108" /><polygon points="71.242,-138.470 89.326,-107.882 76.050,-121.887" /><polygon points="141.573,-25.603 152.462,5.719 140.681,-13.583" /><polygon points="106.135,-44.289 141.573,-25.603 111.375,-36.003" /><polygon points="111.375,-36.003 141.573,-25.603 115.023,-26.932" /><polygon points="106.135,-44.289 83.104,-64.873 92.645,-85.527" /><polygon points="83.104,-64.873 106.135,-44.289 88.350,-51.655" /><polygon points="-89.578,-88.460 -81.455,-61.330 -88.572,-55.866" /></g><circle cx="3.367" cy="6.615" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.947" cy="-3.351" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="11.127" cy="-4.486" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="14.115" cy="8.336" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.386" cy="17.544" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.657" cy="-18.568" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="2.610" cy="21.528" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.337" cy="4.930" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.689" cy="-16.016" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="1.362" cy="-25.001" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="16.392" cy="-22.914" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="23.307" cy="-15.933" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.014" cy="20.571" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.354" cy="13.271" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.343" cy="-15.731" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.799" cy="33.384" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="34.170" cy="-1.132" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="33.515" cy="10.997" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="22.811" cy="27.796" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-37.590" cy="-5.373" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.497" cy="37.975" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="30.792" cy="24.255" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.329" cy="-38.733" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.882" cy="25.279" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="38.288" cy="-19.253" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="41.898" cy="-10.677" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.692" cy="-38.588" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="14.677" cy="41.885" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="40.481" cy="19.594" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="18.083" cy="-42.231" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="8.680" cy="-45.583" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-1.350" cy="-47.801" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.102" cy="-28.263" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="33.340" cy="-35.450" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.725" cy="24.856" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-49.514" cy="-14.008" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.427" cy="43.422" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.521" cy="-38.689" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.751" cy="52.151" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.666" cy="15.465" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.945" cy="53.718" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-54.437" cy="4.060" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="42.281" cy="-34.353" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.975" cy="-22.122" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="46.094" cy="30.499" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="3.955" cy="56.769" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="57.337" cy="5.790" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="50.201" cy="-30.680" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.357" cy="56.186" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="6.347" cy="-59.933" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.698" cy="-19.903" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.748" cy="-31.849" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-22.796" cy="-57.343" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.953" cy="44.309" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="32.595" cy="52.753" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.112" cy="48.658" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.197" cy="65.303" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-61.627" cy="24.152" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-49.713" cy="43.924" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.001" cy="15.281" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.318" cy="-65.725" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-55.403" cy="37.088" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="61.709" cy="23.802" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.070" cy="-64.373" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.383" cy="-7.836" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-49.553" cy="-47.060" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="59.675" cy="32.746" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.941" cy="-58.513" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.006" cy="-65.982" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.402" cy="-28.362" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="1.613" cy="-70.542" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-72.811" cy="-4.323" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="54.931" cy="48.011" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.695" cy="32.726" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.918" cy="-61.963" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.294" cy="-33.386" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="60.651" cy="-44.337" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.048" cy="73.036" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.509" cy="75.022" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="1.407" cy="76.165" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-59.541" cy="-47.848" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.110" cy="69.718" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-72.999" cy="-24.743" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.249" cy="12.078" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="56.471" cy="-54.211" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.818" cy="-61.358" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="69.001" cy="-37.471" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-77.792" cy="-15.207" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.320" cy="75.912" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="63.809" cy="47.075" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-38.521" cy="71.019" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="19.048" cy="-78.086" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-68.157" cy="-46.059" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.941" cy="-82.517" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.388" cy="81.710" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.664" cy="42.843" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="75.880" cy="33.814" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.039" cy="52.596" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="82.123" cy="-16.513" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.109" cy="67.851" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.334" cy="-84.449" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="85.577" cy="1.539" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="72.987" cy="45.047" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.609" cy="-84.740" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-10.053" cy="86.826" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-85.662" cy="20.941" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="84.880" cy="24.911" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.626" cy="-79.771" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="6.618" cy="-89.340" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="74.886" cy="-49.079" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-62.532" cy="65.204" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-78.663" cy="-44.169" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="9.112" cy="89.788" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.776" cy="73.641" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="39.915" cy="-81.448" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-57.218" cy="-71.128" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="87.420" cy="-25.241" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.456" cy="-86.761" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-85.564" cy="-33.966" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-3.016" cy="92.434" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="90.931" cy="13.903" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.927" cy="29.374" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="87.701" cy="-33.387" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.235" cy="37.405" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="71.496" cy="-61.939" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-95.967" cy="2.241" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="85.472" cy="43.449" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="63.244" cy="72.287" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-78.724" cy="56.377" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.841" cy="-96.464" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.968" cy="-41.625" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.676" cy="94.764" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.544" cy="89.915" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.256" cy="46.288" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-68.228" cy="-71.770" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.058" cy="-98.011" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-97.193" cy="-24.212" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.156" cy="87.655" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="72.292" cy="70.010" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-75.646" cy="-67.430" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.093" cy="101.677" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="78.582" cy="64.423" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-81.455" cy="-61.330" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="96.333" cy="33.703" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-93.605" cy="-42.812" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="88.350" cy="-51.655" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.737" cy="-76.742" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.050" cy="-85.315" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="84.654" cy="58.419" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-103.195" cy="-11.438" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="37.357" cy="96.710" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="37.753" cy="-96.474" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-88.572" cy="-55.866" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="104.044" cy="-6.739" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.836" cy="72.334" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="83.104" cy="-64.873" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.863" cy="101.228" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-90.379" cy="57.980" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="93.566" cy="53.745" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.039" cy="107.436" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.110" cy="108.592" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="19.903" cy="107.071" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.913" cy="-108.185" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.547" cy="-107.788" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.716" cy="-104.836" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-56.445" cy="-93.379" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.185" cy="-104.370" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.003" cy="106.189" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.561" cy="-107.062" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-109.843" cy="11.688" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="103.128" cy="42.107" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="109.644" cy="22.547" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-58.657" cy="96.085" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-108.031" cy="-32.854" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.877" cy="88.553" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="114.262" cy="7.904" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.839" cy="74.609" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-45.831" cy="-105.462" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="113.462" cy="-16.840" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="106.135" cy="-44.289" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-108.718" cy="40.177" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.224" cy="96.960" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-115.092" cy="21.976" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="81.791" cy="-83.637" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="111.375" cy="-36.003" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-113.636" cy="31.612" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="115.023" cy="-26.932" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.729" cy="-116.717" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="37.666" cy="114.604" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-113.975" cy="-41.128" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="58.642" cy="-105.643" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.254" cy="-98.478" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-121.842" cy="-19.848" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-123.795" cy="-3.434" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="91.998" cy="83.630" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="120.377" cy="31.237" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="77.352" cy="98.100" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="54.624" cy="112.711" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="74.742" cy="-100.282" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-89.578" cy="-88.460" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="92.645" cy="-85.527" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.419" cy="-96.079" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.457" cy="92.741" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-57.110" cy="113.715" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-86.057" cy="95.600" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.551" cy="126.588" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="131.497" cy="-1.788" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="54.228" cy="-120.108" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.385" cy="-123.658" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.156" cy="-129.034" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.438" cy="-118.189" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.221" cy="124.735" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="133.342" cy="16.099" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.165" cy="111.530" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.910" cy="128.970" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.354" cy="-127.633" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-132.979" cy="-28.700" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-137.159" cy="7.541" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="89.326" cy="-107.882" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="71.957" cy="121.709" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="140.681" cy="-13.583" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-139.400" cy="31.208" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.662" cy="131.580" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-43.310" cy="-136.246" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-142.097" cy="19.675" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-143.689" cy="-11.349" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="76.050" cy="-121.887" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="141.573" cy="-25.603" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-79.878" cy="-123.186" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="145.368" cy="25.138" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-74.383" cy="130.972" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.936" cy="122.654" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-91.329" cy="120.074" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-92.937" cy="-120.260" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-65.217" cy="-137.982" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="152.462" cy="5.719" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="71.242" cy="-138.470" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.789" cy="144.163" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-159.312" cy="0.612" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="74.729" cy="142.546" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle r="9" fill="#061019" stroke="#dabb83" stroke-width="0.8"/><circle r="2" fill="#ffe5a2"/></g><g transform="translate(1295.000 819.000)"><circle r="208.860" fill="url(#bloom-light)"/><polygon points="13.393,6.523 0.546,6.352 -1.993,-3.136 8.407,-5.302 15.448,3.908" fill="#1f4a6c" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.757,11.101 -17.785,3.631 -4.595,-2.988 -4.038,-2.589 -1.499,6.899 -3.373,10.774" fill="#1c4c60" stroke="#96d0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.374,-22.293 12.456,-16.549 8.035,-7.088 -2.365,-4.923 -2.922,-5.322 -5.176,-11.730 5.695,-23.196" fill="#184a51" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.417,23.564 2.153,22.872 -1.348,11.753 0.525,7.878 13.373,8.049 12.929,22.453" fill="#134341" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.469,-7.533 -26.420,-9.197 -23.984,-12.832 -18.486,-16.800 -7.594,-10.880 -5.339,-4.472 -18.529,2.148" fill="#103730" stroke="#96e9db" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="22.999,-14.400 28.611,-8.344 27.007,-1.207 25.112,0.606 16.757,2.907 9.716,-6.303 14.137,-15.763" fill="#0e3128" stroke="#96e9d4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-20.011,20.626 -19.159,15.051 -17.721,12.706 -3.336,12.379 0.165,23.498 -4.186,26.484 -17.084,26.168" fill="#0e3229" stroke="#96e9d4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="1.452,-29.769 4.405,-24.419 -6.467,-12.953 -17.359,-18.874 -17.109,-20.185 -5.319,-32.672" fill="#123c36" stroke="#96e9dd" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.128,23.846 14.972,22.516 15.416,8.112 17.470,5.497 25.825,3.196 28.540,21.385" fill="#174d4f" stroke="#96e5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.335,5.343 -28.289,-6.041 -20.350,3.640 -20.322,11.111 -21.760,13.456" fill="#1d5265" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.179,-35.994 29.051,-35.083 23.403,-17.030 14.541,-18.394 9.460,-24.138" fill="#235279" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.076,21.799 -41.416,21.309 -41.899,7.312 -39.852,6.463 -22.277,14.575 -23.129,20.149" fill="#134139" stroke="#96e9db" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.645,10.961 33.770,22.329 30.084,21.154 27.369,2.965 29.264,1.152" fill="#123c33" stroke="#96e9d7" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="6.748,48.537 4.139,49.322 -1.088,48.267 -2.533,28.892 1.817,25.905 8.082,26.598" fill="#265083" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.573,-36.901 8.853,-25.045 7.174,-25.947 4.221,-31.297 14.236,-43.154 15.303,-43.329 21.072,-42.156" fill="#164b48" stroke="#96e9e4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-27.290,-38.464 -8.988,-38.132 -6.790,-34.061 -18.580,-21.573" fill="#123c33" stroke="#96e9d7" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.803,48.395 -5.639,49.167 -18.821,33.831 -18.856,33.675 -17.146,28.704 -4.248,29.020" fill="#124037" stroke="#96e9d9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.847,48.603 9.180,26.664 13.692,25.554 16.849,26.884 19.438,39.612 16.940,43.843 9.507,48.615" fill="#1b545c" stroke="#96dfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.373,-33.046 -30.326,-38.579 -28.860,-37.654 -20.150,-20.763 -20.400,-19.452 -25.898,-15.484" fill="#265282" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="33.716,-34.734 39.253,-32.176 37.487,-16.220 31.671,-10.724 30.835,-10.406 25.223,-16.461 30.870,-34.514" fill="#14453d" stroke="#96e9dd" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="52.685,-2.309 48.216,8.921 45.678,9.340 30.297,-0.468 31.901,-7.605 32.737,-7.923" fill="#235679" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-48.411,1.735 -51.388,-4.181 -40.895,-13.172 -29.882,-9.298 -29.931,-7.634 -40.977,3.750 -43.025,4.600" fill="#164b47" stroke="#96e9e2" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.694,-32.215 -41.289,-32.291 -26.814,-14.729 -29.250,-11.094 -40.263,-14.969 -43.958,-22.226" fill="#215771" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.849,30.471 34.407,38.876 21.344,39.225 18.755,26.496 29.166,24.034 32.853,25.209" fill="#185454" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.889,23.823 -22.943,22.174 -20.015,27.717 -21.725,32.688 -37.380,36.918 -40.950,25.067" fill="#1f586c" stroke="#96d2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.684,-44.465 2.669,-32.608 -4.102,-35.511 -6.300,-39.583 -5.855,-43.858 2.025,-52.138" fill="#1c575f" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="55.687,-6.149 53.047,-3.597 33.099,-9.212 38.915,-14.708 49.823,-15.648" fill="#265484" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-24.261,49.955 -19.917,34.773 -6.735,50.109 -12.864,54.938 -20.727,54.009" fill="#1b585e" stroke="#96e1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="49.611,-31.716 51.452,-29.478 49.705,-17.015 38.797,-16.075 40.564,-32.031" fill="#195454" stroke="#96e9e7" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-27.959,-47.746 -18.393,-55.224 -8.511,-44.135 -8.956,-39.860 -27.259,-40.191 -28.724,-41.116" fill="#205a6d" stroke="#96d4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.472,45.984 -36.963,38.458 -21.309,34.228 -21.274,34.384 -25.618,49.566" fill="#29538c" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-60.444,-13.555 -45.469,-21.456 -41.774,-14.199 -52.268,-5.207 -61.134,-11.836 -61.112,-12.173" fill="#174f4b" stroke="#96e9e2" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="52.948,19.725 51.924,29.360 38.312,29.069 35.315,23.806 46.190,12.439 48.728,12.019 50.369,13.695" fill="#25587f" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.527,51.205 38.872,52.599 34.091,54.477 18.887,44.993 21.385,40.761 34.448,40.413" fill="#164d47" stroke="#96e9df" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-0.042,-64.250 0.590,-53.503 -7.289,-45.223 -17.171,-56.313 -16.681,-57.939 -3.333,-65.131" fill="#275788" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.589,13.665 -49.901,4.538 -44.516,7.403 -44.032,21.399 -55.408,20.802" fill="#29548e" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.084,-66.012 14.841,-46.153 13.774,-45.979 3.115,-53.651 2.483,-64.398 9.816,-68.040" fill="#164d45" stroke="#96e9dd" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="33.495,55.432 25.922,62.079 21.823,61.507 10.858,50.720 18.291,45.948" fill="#245c7a" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.909,50.084 36.830,39.292 38.273,30.887 51.885,31.178 55.081,35.216 51.227,45.945" fill="#235d79" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.118,-65.999 20.095,-65.776 20.806,-65.362 25.907,-56.623 21.643,-44.968 15.874,-46.141" fill="#215d71" stroke="#96d5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.110,-14.818 -60.347,-31.125 -45.374,-33.417 -43.872,-32.709 -46.136,-22.720" fill="#275987" stroke="#96c1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-59.384,40.674 -59.365,39.547 -43.159,25.733 -39.589,37.583 -42.097,45.109 -43.522,46.140 -57.784,43.860" fill="#164d45" stroke="#96e9dc" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.787,-55.591 33.429,-38.449 30.583,-38.228 28.711,-39.140 23.211,-44.395 27.474,-56.050" fill="#2a5591" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-60.052,38.741 -61.669,33.254 -55.502,22.595 -44.126,23.192 -43.786,23.682 -43.846,24.926" fill="#1f5e6b" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="60.769,-7.699 57.470,-7.249 51.605,-16.749 53.352,-29.211 57.394,-28.046 68.024,-17.630" fill="#2b5593" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-62.740,12.191 -68.754,11.892 -71.926,3.899 -65.738,-7.298 -62.896,-9.480 -54.029,-2.851 -51.052,3.065" fill="#1e5e68" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="75.761,9.137 74.522,10.429 52.470,11.636 50.829,9.961 55.297,-1.269 57.938,-3.821 61.236,-4.271 76.026,8.179" fill="#174e45" stroke="#96e9dc" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.498,-54.957 45.150,-57.759 50.712,-54.911 52.106,-40.337 49.724,-34.942 40.676,-35.256 35.139,-37.814" fill="#1a5a59" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-52.885,-55.995 -51.430,-58.121 -31.232,-48.195 -31.161,-48.116 -31.927,-41.486 -41.974,-35.953 -42.379,-35.877 -43.881,-36.585" fill="#174f46" stroke="#96e9dc" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.584,11.562 69.824,21.032 55.111,18.800 52.532,12.769" fill="#1e5f69" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.894,75.495 0.123,76.700 -10.301,62.436 -10.798,57.560 -4.669,52.730 -1.833,51.958 3.394,53.013" fill="#18524c" stroke="#96e9df" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="6.713,75.374 5.213,52.891 7.822,52.107 9.482,52.118 20.448,62.905 7.019,75.454" fill="#275c87" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.684,62.783 -41.422,49.044 -39.997,48.013 -26.143,51.596 -22.609,55.650 -27.103,61.308" fill="#2b5695" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.652,22.169 69.521,29.601 69.495,29.639 65.166,32.584 57.111,33.610 53.915,29.572 54.939,19.937" fill="#2a598f" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-68.883,14.473 -62.868,14.772 -56.687,21.910 -62.854,32.569 -73.689,22.289" fill="#2c5696" stroke="#96b6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-35.370,-67.709 -32.131,-68.426 -24.160,-65.597 -19.392,-58.754 -19.881,-57.128 -29.447,-49.650 -29.518,-49.729" fill="#285c88" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.887,-62.783 -37.348,-67.510 -36.513,-67.337 -30.661,-49.357 -50.859,-59.283" fill="#1e6169" stroke="#96e0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-24.475,73.329 -25.559,62.534 -21.066,56.876 -13.203,57.805 -12.706,62.681 -20.841,75.551" fill="#275f85" stroke="#96c8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-54.733,-55.138 -45.728,-35.728 -60.701,-33.437 -62.238,-34.688 -60.819,-52.370" fill="#226273" stroke="#96d7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.496,-59.915 39.844,-57.112 27.530,-57.571 22.430,-66.310 41.467,-68.874" fill="#2c5698" stroke="#96b6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.877,-80.122 -4.393,-67.097 -17.741,-59.905 -22.508,-66.747" fill="#285e89" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.211,80.813 -19.732,76.252 -11.596,63.383 -1.172,77.647 -8.772,84.147" fill="#1f6369" stroke="#96e1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.341,-35.107 76.307,-31.340 76.840,-28.174 75.637,-22.874 73.951,-20.378 69.216,-18.847 58.587,-29.263" fill="#2d569b" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.702,-44.303 71.840,-36.287 58.086,-30.442 54.043,-31.608 52.203,-33.847 54.585,-39.242" fill="#266185" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-42.788,69.169 -40.536,64.149 -26.955,62.674 -25.870,73.469 -39.315,74.791 -41.243,73.355 -41.831,72.529" fill="#2d579c" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.394,-29.605 -68.503,-32.806 -64.037,-32.477 -62.499,-31.226 -63.263,-14.919 -63.932,-13.537 -78.837,-26.084" fill="#2c5998" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.205,84.489 8.046,76.552 21.475,64.004 25.573,64.577 26.445,68.283" fill="#295e8e" stroke="#96c1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="77.635,6.268 62.846,-6.182 70.101,-16.113 74.835,-17.644 83.997,-4.820 78.169,5.850" fill="#1c6262" stroke="#96e9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="68.838,55.109 53.568,46.786 57.422,36.057 65.477,35.032 70.404,54.304" fill="#226575" stroke="#96d9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.929,-51.669 69.270,-45.593 54.153,-40.533 52.760,-55.106 67.353,-58.291 67.766,-58.004" fill="#1e6468" stroke="#96e4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.204,75.587 28.551,67.788 27.679,64.081 35.252,57.434 40.033,55.557 41.680,58.828 41.674,74.562" fill="#2c5b98" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-43.271,62.922 -45.523,67.942 -59.531,64.136 -60.610,51.651 -58.270,46.903 -44.009,49.183" fill="#1a5953" stroke="#96e9e0" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.124,-86.549 -20.352,-86.752 -15.752,-84.070 -14.888,-80.775 -23.520,-67.400 -31.491,-70.229" fill="#2b5d94" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-87.479,-13.733 -81.974,-24.083 -79.778,-24.966 -64.872,-12.420 -64.894,-12.083 -67.736,-9.901" fill="#276387" stroke="#96cae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-91.494,-9.636 -87.741,-12.380 -67.999,-8.548 -74.187,2.649 -89.902,0.126 -91.960,-7.925" fill="#1d6465" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.930,-73.172 41.266,-70.367 22.229,-67.803 21.518,-68.217 29.344,-81.815 34.029,-82.126 38.697,-78.601" fill="#2e579f" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-11.908,-81.556 -12.771,-84.851 -0.278,-90.450 8.426,-77.268 8.202,-71.291 0.868,-67.650 -2.423,-68.531" fill="#1a5a53" stroke="#96e9df" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="61.801,69.284 44.158,57.580 42.511,54.308 43.166,52.914 52.484,48.775 67.754,57.098" fill="#1c5f5b" stroke="#96e9e4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-80.818,34.103 -76.575,25.111 -75.259,23.804 -75.052,23.725 -64.217,34.005 -62.600,39.492 -62.620,40.618 -79.986,39.974" fill="#2f57a1" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-77.781,-53.673 -62.445,-52.501 -63.864,-34.818 -68.330,-35.147 -78.346,-53.343" fill="#23667a" stroke="#96d6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.728,87.267 19.260,85.026 27.500,68.819 35.153,76.619 27.998,86.720 21.845,87.478" fill="#2d5c99" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.098,-82.532 20.272,-68.934 16.295,-69.157 11.027,-71.185 11.252,-77.162 20.318,-87.727" fill="#2d5c99" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-90.270,2.419 -74.556,4.942 -71.384,12.935 -76.190,20.751 -76.397,20.830 -93.397,9.167" fill="#1d6361" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="81.651,49.740 80.743,51.955 80.137,52.660 71.557,54.009 66.630,34.737 70.959,31.792" fill="#266682" stroke="#96cfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="60.881,73.183 48.386,77.691 43.323,74.562 43.330,58.828 60.972,70.533" fill="#286489" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.328,30.661 72.044,29.646 72.175,22.214 76.934,12.743 78.173,11.452 88.764,24.857" fill="#2f58a3" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-83.946,-47.571 -79.689,-52.717 -79.264,-52.838 -69.248,-34.642 -77.139,-31.441" fill="#28648b" stroke="#96c8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-23.315,-87.232 -32.681,-70.912 -35.920,-70.195 -36.756,-70.368 -39.229,-84.107 -36.364,-87.643 -28.194,-88.565" fill="#2e5c9d" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-36.149,85.746 -39.134,76.637 -25.689,75.314 -22.055,77.536 -19.534,82.097 -30.156,90.602" fill="#2f59a3" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="82.581,49.186 71.890,31.238 71.915,31.200 84.200,32.215 87.019,43.739" fill="#2b6392" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="100.241,-16.428 98.233,-11.022 85.534,-5.918 76.372,-18.742 78.058,-21.238" fill="#1e6767" stroke="#96e9e8" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-78.983,55.055 -81.535,43.919 -80.049,41.687 -62.683,42.331 -61.082,45.517 -63.422,50.266" fill="#2e5d9d" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.771,-60.957 52.178,-57.773 46.616,-60.621 44.586,-69.581 45.250,-72.385 46.076,-72.710 64.519,-73.546" fill="#1b5f58" stroke="#96e9e0" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="17.525,-95.736 19.020,-88.841 9.953,-78.276 1.249,-91.458 5.694,-99.557 7.143,-99.883" fill="#28668a" stroke="#96cbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.793,-83.825 -38.320,-70.087 -50.858,-65.360 -52.426,-68.305 -51.656,-82.430" fill="#3058a6" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-99.525,23.397 -101.780,17.335 -94.138,10.248 -77.138,21.911 -78.455,23.218" fill="#206a6d" stroke="#96e5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="86.516,-39.663 77.978,-33.100 74.012,-36.866 71.874,-44.882 73.534,-50.958 85.800,-48.642" fill="#3058a7" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="101.212,-18.529 100.466,-17.464 78.283,-22.273 79.486,-27.574 98.333,-27.024 101.088,-20.719" fill="#226b76" stroke="#96dde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.241,87.003 17.709,89.244 17.181,90.048 3.130,101.250 -6.326,90.132 -6.596,86.692 1.004,80.191 5.776,78.987 6.082,79.066" fill="#1e6764" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-99.517,24.407 -78.446,24.228 -82.689,33.220 -98.256,29.251 -99.309,27.139" fill="#256a80" stroke="#96d5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-86.165,-46.499 -85.129,-47.072 -78.321,-30.942 -80.764,-27.421 -82.961,-26.538 -92.220,-33.741 -92.893,-38.468" fill="#2c6298" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-81.151,62.303 -78.495,56.641 -62.934,51.852 -61.855,64.337 -70.661,72.072 -78.182,68.170" fill="#276987" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.932,91.178 29.433,87.737 36.588,77.635 42.058,76.610 47.121,79.739 45.411,90.517" fill="#2e5ea0" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="103.177,11.460 96.318,23.728 90.310,23.636 79.719,10.231 79.984,9.273 80.518,8.855" fill="#2f5ea2" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="98.364,-28.075 79.516,-28.625 78.984,-31.791 87.522,-38.354 97.957,-32.991" fill="#286989" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.907,84.168 -68.928,74.045 -60.122,66.310 -46.114,70.116 -45.157,73.476" fill="#1d635d" stroke="#96e9e1" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="105.402,8.539 103.363,9.840 80.704,7.235 86.532,-3.435 99.231,-8.539 105.855,6.793" fill="#256b80" stroke="#96d5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-72.669,-75.278 -55.274,-66.789 -53.706,-63.844 -54.678,-60.344 -56.133,-58.218 -62.220,-55.451 -77.555,-56.623" fill="#226c74" stroke="#96e0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-28.842,94.618 -28.940,92.121 -18.318,83.616 -9.880,86.950 -9.610,90.390 -18.646,102.205" fill="#3159aa" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-101.648,-23.051 -93.282,-32.376 -84.023,-25.173 -89.527,-14.823 -93.280,-12.079" fill="#3159aa" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.913,-96.254 27.697,-101.054 35.372,-93.774 33.874,-84.475 29.188,-84.164 21.408,-89.359" fill="#305da5" stroke="#96b6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-98.559,30.442 -82.993,34.411 -82.160,40.282 -83.646,42.514 -93.611,43.413 -97.905,40.422" fill="#2a698f" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="60.348,-91.312 62.064,-91.196 65.912,-89.230 67.910,-84.405 64.449,-75.102 46.006,-74.266" fill="#1d655f" stroke="#96e9e2" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.450,89.695 -65.371,85.208 -44.621,74.516 -44.033,75.343 -54.861,93.897 -55.817,93.914" fill="#1e6864" stroke="#96e9e4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.949,94.430 -43.120,75.875 -41.193,77.311 -38.207,86.420 -49.929,95.707" fill="#226e77" stroke="#96e0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="93.691,41.852 88.656,43.339 85.837,31.814 90.274,26.011 96.281,26.103 101.365,33.593" fill="#2d659a" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.537,-91.995 45.195,-74.949 44.369,-74.624 41.136,-80.053 51.676,-94.926" fill="#206d6e" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-71.875,-76.906 -73.215,-80.949 -71.942,-83.134 -58.460,-87.926 -53.709,-82.542 -54.479,-68.417" fill="#305ea5" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="50.801,-95.546 40.262,-80.673 35.594,-84.198 37.093,-93.496 48.103,-97.914" fill="#256e80" stroke="#96d8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.264,-110.542 3.070,-100.997 -1.376,-92.899 -13.869,-87.300 -18.469,-89.982 -17.999,-93.324" fill="#206e6f" stroke="#96e7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.181,96.650 -37.460,87.364 -31.467,92.220 -31.369,94.717 -37.074,101.262 -42.791,102.492" fill="#276e87" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-56.629,-89.542 -54.844,-96.110 -42.824,-95.797 -38.150,-89.090 -41.015,-85.553 -51.878,-84.158" fill="#2e63a0" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-93.772,59.232 -93.444,45.263 -83.479,44.365 -80.927,55.500 -83.583,61.162" fill="#315ea9" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="88.286,-68.275 91.121,-53.997 86.219,-50.862 73.953,-53.178 70.790,-59.514" fill="#3359af" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="48.821,92.397 47.537,90.855 49.247,80.076 61.742,75.568 62.820,76.895 63.249,91.571" fill="#335aae" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="1.289,110.709 -6.948,113.766 -16.579,110.011 -17.077,103.405 -8.040,91.591 1.416,102.708" fill="#3061a5" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="100.236,-37.221 98.701,-34.439 88.266,-39.802 87.550,-48.781 92.452,-51.916 100.160,-50.264" fill="#2a6b92" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="83.057,78.589 65.600,74.634 64.522,73.308 64.613,70.658 70.566,58.472 72.132,57.667 80.712,56.318 82.237,59.741" fill="#266f84" stroke="#96d6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="17.444,116.569 16.465,117.352 15.469,117.625 4.203,110.755 4.329,102.755 18.380,91.552" fill="#217071" stroke="#96e7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="32.100,110.989 22.218,90.502 28.370,89.745 34.869,93.185 35.103,110.858" fill="#217070" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-35.949,-112.386 -28.475,-91.060 -36.646,-90.138 -41.320,-96.845" fill="#237078" stroke="#96e1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.814,-114.100 -27.182,-114.273 -20.289,-93.646 -20.758,-90.303 -22.531,-90.100 -27.410,-91.433 -34.884,-112.759" fill="#1f6b67" stroke="#96e9e5" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-117.446,-4.122 -95.386,-7.049 -93.328,1.002 -96.455,7.750 -104.096,14.838 -104.430,14.740 -117.643,-3.289" fill="#2c6a96" stroke="#96c7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="89.080,-73.316 87.458,-69.929 69.962,-61.167 69.549,-61.454 67.297,-74.043 70.758,-83.345 86.420,-78.358" fill="#2f65a1" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.599,116.612 19.535,91.595 20.063,90.792 21.181,91.002 31.064,111.489" fill="#1f6b67" stroke="#96e9e4" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.100,-114.635 -15.878,-118.128 -11.971,-116.361 -9.472,-111.225 -19.207,-94.007" fill="#206e6d" stroke="#96e9e7" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.305,110.842 36.071,93.170 45.550,92.509 46.834,94.052 47.266,101.746 40.963,112.288" fill="#25717f" stroke="#96dce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-109.623,-26.071 -112.144,-30.731 -101.988,-39.432 -95.123,-38.151 -94.451,-33.424 -102.816,-24.099" fill="#2c6a98" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.188,110.207 -25.950,114.443 -35.586,102.559 -29.881,96.014 -19.685,103.602" fill="#2f65a3" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.257,-110.691 43.957,-111.784 47.514,-99.383 36.503,-94.966 28.828,-102.246 30.780,-107.266" fill="#28708a" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-117.696,-5.999 -118.587,-9.011 -110.344,-23.583 -103.537,-21.611 -95.169,-10.638 -95.635,-8.927" fill="#3459b3" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="116.528,21.440 104.524,32.522 103.300,32.280 98.216,24.790 105.075,12.522 107.114,11.220" fill="#3260ab" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="114.045,59.152 83.985,53.285 84.894,51.070 89.331,45.623 94.365,44.136" fill="#206f6b" stroke="#96e9e5" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-109.518,-54.965 -88.042,-48.072 -94.770,-40.041 -101.635,-41.322" fill="#217273" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-94.661,-79.808 -76.309,-79.923 -74.969,-75.881 -79.854,-57.225 -80.420,-56.895 -80.846,-56.774 -83.728,-59.188" fill="#28718b" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.959,-118.197 28.325,-108.220 26.373,-103.200 18.589,-98.400 8.207,-102.548" fill="#345bb2" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-111.877,-58.524 -106.710,-63.768 -85.262,-57.356 -82.379,-54.942 -86.636,-49.797 -87.673,-49.223 -109.149,-56.117" fill="#206f6c" stroke="#96e9e5" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-115.477,53.403 -99.170,42.239 -94.877,45.230 -95.205,59.198 -101.525,61.424 -107.830,59.419" fill="#2a7090" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="126.238,55.254 116.136,58.781 114.785,58.182 95.105,43.166 102.780,34.908 104.004,35.150" fill="#237379" stroke="#96e3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-51.026,-123.405 -37.343,-114.233 -37.413,-112.892 -42.783,-97.351 -54.803,-97.664 -56.173,-102.122" fill="#267383" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="82.560,80.782 85.473,83.645 81.971,92.197 69.433,96.838 65.533,91.504 65.103,76.828" fill="#3559b6" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="113.801,60.400 115.152,60.999 103.799,71.825 84.660,58.661 83.136,55.238 83.742,54.533" fill="#227476" stroke="#96e7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="67.237,98.443 65.607,104.697 49.341,101.629 48.909,93.935 63.337,93.109" fill="#2d6c9b" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="103.108,72.829 99.691,79.044 87.703,81.377 84.790,78.513 83.970,59.665" fill="#287389" stroke="#96d6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="126.867,-3.024 108.484,5.657 101.859,-9.675 103.868,-15.081 104.614,-16.146 109.310,-15.311 126.949,-3.431" fill="#2f69a3" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-96.513,-79.414 -95.833,-79.187 -84.900,-58.566 -106.349,-64.978 -100.158,-78.155" fill="#257480" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-131.734,48.215 -102.014,28.488 -100.961,30.600 -100.306,40.580 -116.613,51.743" fill="#217270" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="128.401,-42.486 104.049,-22.012 101.294,-28.317 100.887,-33.234 102.421,-36.016" fill="#21716e" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="103.521,-70.942 100.875,-52.819 100.685,-52.714 92.977,-54.366 90.142,-68.644 91.764,-72.031" fill="#3069a4" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-93.952,84.730 -79.711,71.116 -72.190,75.019 -69.169,85.141 -69.248,89.628 -69.577,89.884 -70.542,90.173 -91.560,88.960" fill="#3360b1" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.570,-123.206 14.084,-119.126 6.332,-103.477 4.883,-103.150 -6.451,-112.695 -8.950,-117.831 5.159,-126.258" fill="#355bb6" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-46.690,128.524 -42.332,104.624 -36.615,103.394 -26.979,115.278 -30.176,120.276" fill="#287489" stroke="#96d7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-95.352,83.265 -97.031,82.406 -100.590,64.081 -94.269,61.855 -84.080,63.785 -81.111,69.652" fill="#355cb6" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="133.788,-51.948 130.489,-44.962 128.098,-43.703 102.118,-37.232 102.042,-50.275 102.233,-50.381" fill="#227576" stroke="#96e8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-127.959,-53.486 -113.566,-56.610 -110.837,-54.203 -102.954,-40.560 -113.110,-31.859 -116.152,-33.833" fill="#24767d" stroke="#96e2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.404,-101.177 -58.876,-101.292 -57.507,-96.833 -59.292,-90.266 -72.774,-85.474" fill="#3263ad" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-137.075,46.869 -134.572,38.506 -107.046,18.918 -105.532,18.509 -105.198,18.607 -102.944,24.668 -102.736,27.400 -132.456,47.127" fill="#237678" stroke="#96e6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="130.689,53.494 127.696,53.642 105.461,33.538 117.466,22.456 123.381,24.507 127.680,32.242" fill="#2b7293" stroke="#96cfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-60.543,140.580 -55.762,97.145 -54.806,97.129 -50.786,98.405 -44.395,104.247 -48.753,128.148" fill="#227472" stroke="#96e9e6" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-133.284,13.632 -132.633,9.638 -119.412,-1.992 -106.199,16.037 -107.713,16.445" fill="#3069a7" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="129.305,-41.411 131.696,-42.670 131.019,-25.617 109.773,-17.912 105.076,-18.747 104.953,-20.937" fill="#277687" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.247,132.906 42.808,113.391 49.111,102.849 65.377,105.917 65.966,110.626" fill="#267783" stroke="#96dee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="62.537,-134.607 62.300,-94.688 60.585,-94.804 52.723,-97.736 50.025,-100.104 46.469,-112.504" fill="#237779" stroke="#96e7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="24.877,-129.997 34.849,-112.942 29.372,-109.517 17.006,-119.495 16.492,-123.574" fill="#2d719b" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="127.845,-0.953 131.870,7.789 124.338,21.745 118.423,19.695 109.009,9.475 109.462,7.729" fill="#365cb9" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-135.385,37.363 -133.656,15.397 -133.431,14.962 -107.859,17.775" fill="#2c7397" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="131.470,-24.373 134.654,-14.019 127.863,-4.788 110.224,-16.668" fill="#2f6da2" stroke="#96c3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="141.363,-62.862 133.676,-54.205 102.120,-52.638 104.767,-70.760 109.997,-72.461" fill="#28778b" stroke="#96d8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.946,140.426 -63.806,143.040 -70.074,131.104 -67.126,93.027 -66.798,92.771 -57.165,96.990" fill="#277886" stroke="#96dde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-132.818,-19.105 -117.803,-31.289 -114.761,-29.315 -112.240,-24.655 -120.483,-10.084" fill="#306da4" stroke="#96c2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="94.852,-96.738 87.241,-80.937 71.579,-85.924 69.581,-90.749 72.352,-93.415 92.644,-100.312" fill="#365db9" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.559,134.222 -11.863,134.673 -27.586,121.933 -24.389,116.935 -17.627,112.698 -7.996,116.453" fill="#3366af" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-81.307,-103.606 -78.685,-100.650 -75.054,-84.947 -76.327,-82.762 -94.678,-82.647 -95.359,-82.874 -92.158,-99.186" fill="#375abd" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.399,134.534 -5.835,116.765 2.402,113.708 13.669,120.578 12.970,122.345 -0.854,136.957" fill="#3759bf" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-62.619,-139.380 -53.739,-124.061 -58.886,-102.779 -76.414,-102.663 -79.036,-105.620 -76.977,-118.107" fill="#2a7792" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.080,-134.598 69.985,-146.966 79.810,-123.891 70.462,-95.379 67.691,-92.713 63.843,-94.679" fill="#2d749a" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="65.612,-153.286 65.527,-149.094 59.623,-136.726 43.554,-114.623 35.854,-113.530 25.882,-130.584 28.205,-134.159" fill="#277a85" stroke="#96dfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-88.109,111.272 -90.780,109.681 -93.546,105.617 -91.680,91.043 -70.662,92.256" fill="#365ebc" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-71.618,130.984 -87.083,112.213 -69.636,93.197 -68.671,92.908" fill="#326bac" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="60.394,162.774 18.804,120.276 19.783,119.493 32.248,114.370 35.250,114.240 39.908,115.685 55.347,135.200" fill="#2c7796" stroke="#96d0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-113.732,84.210 -108.792,62.444 -102.487,64.449 -98.928,82.774" fill="#2d769a" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="96.978,-95.714 105.080,-92.987 109.015,-75.483 103.784,-73.782 92.027,-74.871 89.367,-79.913" fill="#3467b3" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="81.186,-123.441 91.186,-110.117 92.130,-101.825 71.838,-94.928" fill="#3563b8" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="91.458,108.761 90.863,110.037 69.284,110.211 68.694,105.502 70.325,99.248 82.863,94.607" fill="#3071a5" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-163.663,-60.270 -130.457,-51.985 -118.650,-32.333 -133.664,-20.149 -136.946,-20.860" fill="#287b8b" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="0.065,-143.503 3.818,-128.503 -10.291,-120.077 -14.198,-121.843 -14.467,-124.404 -7.004,-145.417" fill="#3959c4" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-59.034,-162.350 -34.798,-152.170 -17.315,-124.105 -17.046,-121.544 -27.268,-118.051 -34.899,-117.878 -48.582,-127.050 -57.462,-142.369" fill="#326dad" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-137.625,-17.732 -134.343,-17.020 -122.009,-7.998 -121.117,-4.987 -121.314,-4.154 -134.535,7.477 -143.790,-5.027" fill="#3664b9" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="93.468,107.540 84.873,93.386 88.376,84.834 100.364,82.502 100.874,83.418 102.899,103.172" fill="#385ec0" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-60.091,157.668 -58.840,146.573 -56.980,143.959 -45.191,131.527 -28.677,123.279 -12.954,136.019 -23.414,147.588" fill="#2a7c91" stroke="#96d8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="53.841,170.135 20.356,150.777 15.897,123.503 16.596,121.736 17.591,121.463 59.182,163.961 60.041,166.707" fill="#3761be" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-137.382,52.368 -132.763,52.626 -117.642,56.154 -109.995,62.171 -114.935,83.936 -117.012,84.931 -163.288,74.980 -164.680,71.301 -164.696,70.358" fill="#277e86" stroke="#96e2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.903,151.015 7.604,148.565 0.621,138.352 14.444,123.740" fill="#3a5ac6" stroke="#96a8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.436,-144.097 9.482,-147.810 25.308,-136.042 22.985,-132.467 14.600,-126.045 6.189,-129.097" fill="#3173a8" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.458,-153.005 -12.809,-149.626 -8.512,-145.952 -15.975,-124.939" fill="#3a5bc6" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="148.447,13.682 149.106,15.417 130.672,30.579 126.373,22.844 133.906,8.887" fill="#2d7a9a" stroke="#96d1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-159.125,-89.270 -104.246,-80.277 -103.197,-79.582 -109.387,-66.405 -114.553,-61.161 -128.947,-58.037 -162.153,-66.321 -164.785,-67.784 -164.888,-71.952" fill="#3764bd" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.301,164.649 67.483,164.381 66.624,161.634 61.576,134.060 69.296,111.779 90.876,111.605 91.429,158.970" fill="#28808b" stroke="#96dfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-114.728,-103.016 -105.904,-107.710 -94.678,-99.681 -97.879,-83.368 -101.524,-82.110 -102.573,-82.804" fill="#3960c3" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-115.536,88.011 -113.460,87.017 -98.656,85.582 -96.977,86.441 -94.585,90.671 -96.451,105.245 -111.085,103.632" fill="#3372ae" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="149.238,11.282 134.697,6.487 130.671,-2.254 130.754,-2.661 137.545,-11.892 137.720,-11.857 154.018,-0.229" fill="#356bb8" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="160.103,88.683 128.429,96.438 103.666,81.865 103.156,80.948 106.572,74.734 117.926,63.908 128.028,60.381 131.021,60.233 163.373,76.088" fill="#3e5cc7" stroke="#96a8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="127.636,97.787 115.042,105.450 104.897,102.967 102.872,83.213" fill="#3b5fc6" stroke="#96abe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="162.330,-76.637 142.428,-66.344 111.063,-75.943 107.128,-93.448 120.110,-98.950 161.391,-80.938" fill="#2a8091" stroke="#96dbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-104.427,-109.775 -102.283,-117.222 -80.290,-118.653 -82.350,-106.166 -93.201,-101.746" fill="#2d7d9c" stroke="#96d2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-156.349,-100.382 -116.125,-102.176 -103.969,-81.964 -158.848,-90.958 -157.316,-98.892" fill="#3f5cc7" stroke="#96a7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="169.998,66.213 166.946,68.796 134.594,52.941 131.586,31.690 150.019,16.527 180.183,48.005" fill="#288388" stroke="#96e4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.152,149.634 -10.692,138.064 -9.388,137.614 -1.843,140.037 5.140,150.249 -5.663,159.492" fill="#2f7da2" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-162.165,9.321 -158.178,-2.364 -145.840,-3.509 -136.585,8.994 -137.236,12.988 -137.462,13.423" fill="#366eba" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="135.678,-42.512 138.977,-49.498 146.664,-58.155 166.566,-68.448 170.192,-65.665 184.530,-42.828 186.278,-31.137 138.359,-15.070 138.185,-15.105 135.000,-25.459" fill="#435dc8" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.395,-184.467 64.858,-169.312 67.066,-166.281 63.861,-156.710 26.455,-137.583 10.628,-149.352 28.419,-187.060" fill="#29848d" stroke="#96e2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="119.054,-101.440 106.073,-95.938 97.971,-98.664 95.763,-102.238 94.819,-110.530 114.181,-114.838" fill="#3279ac" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-181.137,-44.561 -169.268,-59.717 -166.636,-58.254 -139.919,-18.845 -146.084,-6.140 -158.422,-4.995" fill="#2b8494" stroke="#96dce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-186.402,35.540 -162.439,10.974 -137.736,15.075 -139.466,37.041 -141.968,45.405 -169.282,63.395 -184.936,42.060" fill="#3d63c7" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="189.572,-20.393 155.186,-1.866 138.888,-13.494 186.806,-29.561" fill="#386ac1" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.266,-166.879 -67.619,-161.674 -66.047,-141.694 -80.405,-120.421 -102.398,-118.989 -113.476,-155.374 -102.172,-163.171 -74.920,-167.787" fill="#29878d" stroke="#96e4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="94.701,158.932 94.148,111.567 94.743,110.291 104.174,105.923 114.318,108.406 111.976,153.927 101.625,158.693" fill="#2c8598" stroke="#96dae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-137.915,134.223 -111.365,106.172 -96.731,107.784 -93.965,111.848 -131.040,142.117" fill="#337bae" stroke="#96c7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-162.025,102.609 -164.090,78.709 -117.814,88.660 -113.362,104.281 -139.913,132.332 -148.631,126.416" fill="#2a8890" stroke="#96e2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="25.813,-188.289 8.021,-150.582 0.975,-146.868 -6.094,-148.781 -10.391,-152.455 13.488,-193.908 13.801,-193.912" fill="#2e83a0" stroke="#96d4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-129.945,143.458 -92.871,113.189 -90.199,114.781 -74.735,133.552 -68.467,145.488 -69.718,156.583 -74.316,166.432 -75.108,166.626 -103.066,163.541 -126.368,149.234" fill="#386ec1" stroke="#96b6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.552,-148.911 74.637,-153.103 77.841,-162.674 102.825,-161.504 125.489,-150.040 113.740,-116.820 94.378,-112.512 84.377,-125.836" fill="#3578b6" stroke="#96c1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="191.045,-17.659 194.365,-12.468 197.610,15.032 187.365,41.028 182.702,45.591 152.538,14.113 151.879,12.378 156.658,0.867" fill="#2b8a94" stroke="#96e1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="14.599,197.836 -5.102,196.379 -3.989,161.449 6.814,152.206 18.114,154.656 51.598,174.014 40.311,186.342" fill="#2f85a3" stroke="#96d3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-42.728,190.348 -63.614,171.428 -59.016,161.579 -22.340,151.500 -6.851,161.358 -7.964,196.288 -16.058,198.071" fill="#2a8a92" stroke="#96e3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-116.403,-154.483 -105.325,-118.098 -107.469,-110.651 -116.293,-105.958 -156.518,-104.163 -149.220,-129.968 -130.079,-150.040" fill="#2c8a99" stroke="#96dde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.700,-194.937 -12.178,-153.484 -32.827,-156.863 -57.063,-167.042 -59.710,-172.248 -42.068,-190.151 -15.629,-199.663" fill="#3183aa" stroke="#96cee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="128.476,-148.984 130.837,-148.591 153.356,-131.617 164.110,-105.194 162.881,-84.354 121.600,-102.366 116.727,-115.764" fill="#2b8d96" stroke="#96e2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="153.251,132.287 131.818,150.583 114.580,154.061 116.922,108.540 129.516,100.877 161.189,93.122 162.420,104.856" fill="#2d8c9c" stroke="#96dce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-187.839,-39.989 -183.920,-42.963 -161.205,-3.397 -165.192,8.288 -189.155,32.855 -201.134,13.771 -202.084,-14.424" fill="#2c8e98" stroke="#96e1e9" stroke-opacity="0.53" stroke-width="0.65"/><g fill="none" stroke="#d1e7ed" stroke-width="0.55" opacity="0.22"><polygon points="6.533,1.324 -9.197,5.533 3.672,-12.416" /><polygon points="-9.197,5.533 6.533,1.324 6.377,13.064" /><polygon points="3.672,-12.416 -9.197,5.533 -14.922,-5.875" /><polygon points="6.533,1.324 3.672,-12.416 16.602,-6.374" /><polygon points="-9.197,5.533 6.377,13.064 -8.917,17.880" /><polygon points="3.672,-12.416 -14.922,-5.875 -6.253,-21.825" /><polygon points="6.377,13.064 6.533,1.324 22.090,13.548" /><polygon points="22.090,13.548 6.533,1.324 16.602,-6.374" /><polygon points="-14.922,-5.875 -9.197,5.533 -28.923,5.607" /><polygon points="16.602,-6.374 3.672,-12.416 19.715,-26.608" /><polygon points="-28.923,5.607 -8.917,17.880 -32.897,14.215" /><polygon points="-8.917,17.880 -28.923,5.607 -9.197,5.533" /><polygon points="22.090,13.548 16.602,-6.374 33.964,11.776" /><polygon points="-8.917,17.880 6.377,13.064 3.797,36.399" /><polygon points="19.715,-26.608 3.672,-12.416 15.049,-33.582" /><polygon points="15.049,-33.582 3.672,-12.416 -6.253,-21.825" /><polygon points="-6.253,-21.825 -29.645,-26.276 -17.565,-32.506" /><polygon points="-8.917,17.880 3.797,36.399 -9.394,37.382" /><polygon points="3.797,36.399 6.377,13.064 12.246,36.912" /><polygon points="12.246,36.912 6.377,13.064 22.090,13.548" /><polygon points="-29.645,-26.276 -6.253,-21.825 -14.922,-5.875" /><polygon points="16.602,-6.374 19.715,-26.608 33.712,-22.230" /><polygon points="16.602,-6.374 41.912,-0.688 33.964,11.776" /><polygon points="41.912,-0.688 16.602,-6.374 33.712,-22.230" /><polygon points="-14.922,-5.875 -28.923,5.607 -41.554,-6.649" /><polygon points="-14.922,-5.875 -36.693,-20.468 -29.645,-26.276" /><polygon points="-36.693,-20.468 -14.922,-5.875 -41.554,-6.649" /><polygon points="12.246,36.912 22.090,13.548 26.909,33.930" /><polygon points="26.909,33.930 22.090,13.548 33.964,11.776" /><polygon points="-32.897,14.215 -8.917,17.880 -31.466,29.789" /><polygon points="-31.466,29.789 -8.917,17.880 -9.394,37.382" /><polygon points="15.049,-33.582 -6.253,-21.825 3.111,-43.666" /><polygon points="3.111,-43.666 -6.253,-21.825 -17.565,-32.506" /><polygon points="41.912,-0.688 33.712,-22.230 44.702,-10.599" /><polygon points="-9.394,37.382 -28.264,41.638 -31.466,29.789" /><polygon points="44.702,-10.599 33.712,-22.230 43.796,-21.113" /><polygon points="3.111,-43.666 -17.565,-32.506 -17.324,-45.793" /><polygon points="-28.264,41.638 -9.394,37.382 -17.822,44.626" /><polygon points="-36.693,-20.468 -41.554,-6.649 -48.321,-14.547" /><polygon points="26.909,33.930 33.964,11.776 45.848,23.145" /><polygon points="12.246,36.912 26.909,33.930 27.224,45.752" /><polygon points="3.111,-43.666 -17.324,-45.793 -7.925,-54.169" /><polygon points="-32.897,14.215 -41.554,-6.649 -28.923,5.607" /><polygon points="-41.554,-6.649 -32.897,14.215 -53.021,14.910" /><polygon points="15.049,-33.582 3.111,-43.666 11.493,-55.310" /><polygon points="11.493,-55.310 3.111,-43.666 -7.925,-54.169" /><polygon points="12.246,36.912 27.224,45.752 22.639,53.101" /><polygon points="26.909,33.930 45.848,23.145 45.549,37.128" /><polygon points="27.224,45.752 26.909,33.930 45.549,37.128" /><polygon points="15.049,-33.582 11.493,-55.310 19.445,-55.213" /><polygon points="-36.693,-20.468 -48.321,-14.547 -53.450,-24.265" /><polygon points="-32.897,14.215 -53.745,28.704 -53.021,14.910" /><polygon points="-53.745,28.704 -32.897,14.215 -31.466,29.789" /><polygon points="-48.456,34.908 -31.466,29.789 -28.264,41.638" /><polygon points="19.715,-26.608 15.049,-33.582 31.500,-50.803" /><polygon points="33.712,-22.230 19.715,-26.608 31.500,-50.803" /><polygon points="31.500,-50.803 15.049,-33.582 19.445,-55.213" /><polygon points="-53.745,28.704 -31.466,29.789 -48.456,34.908" /><polygon points="44.702,-10.599 43.796,-21.113 58.414,-19.064" /><polygon points="-41.554,-6.649 -53.021,14.910 -61.871,3.576" /><polygon points="-48.321,-14.547 -41.554,-6.649 -61.871,3.576" /><polygon points="41.912,-0.688 45.848,23.145 33.964,11.776" /><polygon points="45.848,23.145 41.912,-0.688 62.011,7.310" /><polygon points="33.712,-22.230 31.500,-50.803 44.658,-45.923" /><polygon points="43.796,-21.113 33.712,-22.230 44.658,-45.923" /><polygon points="-29.645,-26.276 -17.324,-45.793 -17.565,-32.506" /><polygon points="-17.324,-45.793 -29.645,-26.276 -41.961,-48.638" /><polygon points="45.848,23.145 62.011,7.310 62.488,16.028" /><polygon points="-9.394,37.382 3.797,36.399 -1.932,64.792" /><polygon points="-17.822,44.626 -9.394,37.382 -1.932,64.792" /><polygon points="3.797,36.399 12.246,36.912 12.056,63.859" /><polygon points="-1.932,64.792 3.797,36.399 12.056,63.859" /><polygon points="12.056,63.859 12.246,36.912 22.639,53.101" /><polygon points="-28.264,41.638 -17.822,44.626 -32.301,57.248" /><polygon points="-17.822,44.626 -20.428,66.678 -32.301,57.248" /><polygon points="45.848,23.145 62.488,16.028 61.162,24.772" /><polygon points="45.549,37.128 45.848,23.145 61.162,24.772" /><polygon points="-53.021,14.910 -53.745,28.704 -62.859,23.431" /><polygon points="-17.324,-45.793 -41.961,-48.638 -28.775,-60.441" /><polygon points="-7.925,-54.169 -17.324,-45.793 -28.775,-60.441" /><polygon points="-28.775,-60.441 -41.961,-48.638 -37.567,-57.580" /><polygon points="-20.428,66.678 -17.822,44.626 -1.932,64.792" /><polygon points="-41.961,-48.638 -36.693,-20.468 -53.450,-24.265" /><polygon points="-41.961,-48.638 -53.450,-24.265 -56.171,-42.046" /><polygon points="-36.693,-20.468 -41.961,-48.638 -29.645,-26.276" /><polygon points="31.500,-50.803 19.445,-55.213 31.936,-62.503" /><polygon points="-7.925,-54.169 -28.775,-60.441 -16.073,-69.292" /><polygon points="-20.428,66.678 -1.932,64.792 -11.894,72.073" /><polygon points="58.414,-19.064 63.731,-37.502 67.587,-28.426" /><polygon points="43.796,-21.113 63.731,-37.502 58.414,-19.064" /><polygon points="63.731,-37.502 43.796,-21.113 44.658,-45.923" /><polygon points="-32.301,57.248 -20.428,66.678 -31.159,67.755" /><polygon points="-53.450,-24.265 -48.321,-14.547 -70.008,-25.040" /><polygon points="-56.171,-42.046 -53.450,-24.265 -70.008,-25.040" /><polygon points="-48.321,-14.547 -77.243,-16.444 -70.008,-25.040" /><polygon points="12.056,63.859 22.639,53.101 19.955,72.312" /><polygon points="44.702,-10.599 62.011,7.310 41.912,-0.688" /><polygon points="62.011,7.310 58.414,-19.064 74.389,-7.393" /><polygon points="58.414,-19.064 62.011,7.310 44.702,-10.599" /><polygon points="74.389,-7.393 58.414,-19.064 67.587,-28.426" /><polygon points="45.549,37.128 61.162,24.772 63.558,43.598" /><polygon points="63.731,-37.502 44.658,-45.923 60.408,-47.429" /><polygon points="22.639,53.101 27.224,45.752 36.157,68.500" /><polygon points="19.955,72.312 22.639,53.101 36.157,68.500" /><polygon points="-48.456,34.908 -32.301,57.248 -52.199,58.316" /><polygon points="-32.301,57.248 -48.456,34.908 -28.264,41.638" /><polygon points="-16.073,-69.292 -28.775,-60.441 -23.852,-74.312" /><polygon points="-77.243,-16.444 -48.321,-14.547 -61.871,3.576" /><polygon points="-77.243,-16.444 -61.871,3.576 -79.264,-6.037" /><polygon points="31.936,-62.503 19.445,-55.213 30.389,-73.990" /><polygon points="11.493,-55.310 -7.925,-54.169 -0.926,-80.321" /><polygon points="-0.926,-80.321 -7.925,-54.169 -16.073,-69.292" /><polygon points="27.224,45.752 45.549,37.128 55.219,58.899" /><polygon points="36.157,68.500 27.224,45.752 55.219,58.899" /><polygon points="55.219,58.899 45.549,37.128 63.558,43.598" /><polygon points="-62.859,23.431 -53.745,28.704 -73.343,34.480" /><polygon points="-56.171,-42.046 -70.008,-25.040 -68.680,-43.050" /><polygon points="19.955,72.312 36.157,68.500 28.068,76.437" /><polygon points="19.445,-55.213 11.493,-55.310 20.807,-79.504" /><polygon points="30.389,-73.990 19.445,-55.213 20.807,-79.504" /><polygon points="20.807,-79.504 11.493,-55.310 -0.926,-80.321" /><polygon points="-61.871,3.576 -62.859,23.431 -82.096,11.602" /><polygon points="-79.264,-6.037 -61.871,3.576 -82.096,11.602" /><polygon points="-62.859,23.431 -61.871,3.576 -53.021,14.910" /><polygon points="-82.096,11.602 -62.859,23.431 -73.343,34.480" /><polygon points="63.558,43.598 61.162,24.772 72.425,41.331" /><polygon points="36.157,68.500 55.219,58.899 48.846,68.505" /><polygon points="61.162,24.772 62.488,16.028 80.568,25.114" /><polygon points="61.162,24.772 79.580,37.069 72.425,41.331" /><polygon points="-68.680,-43.050 -70.008,-25.040 -75.737,-39.165" /><polygon points="-23.852,-74.312 -28.775,-60.441 -33.009,-79.567" /><polygon points="-31.159,67.755 -20.428,66.678 -29.763,81.949" /><polygon points="-29.763,81.949 -20.428,66.678 -11.894,72.073" /><polygon points="79.580,37.069 61.162,24.772 80.568,25.114" /><polygon points="74.389,-7.393 67.587,-28.426 86.214,-15.842" /><polygon points="-48.456,34.908 -73.343,34.480 -53.745,28.704" /><polygon points="-73.343,34.480 -48.456,34.908 -73.832,47.658" /><polygon points="-73.832,47.658 -48.456,34.908 -52.199,58.316" /><polygon points="44.658,-45.923 31.936,-62.503 55.932,-67.938" /><polygon points="60.408,-47.429 44.658,-45.923 55.932,-67.938" /><polygon points="31.936,-62.503 44.658,-45.923 31.500,-50.803" /><polygon points="55.932,-67.938 31.936,-62.503 30.389,-73.990" /><polygon points="20.807,-79.504 -0.926,-80.321 10.818,-88.076" /><polygon points="-37.567,-57.580 -33.009,-79.567 -28.775,-60.441" /><polygon points="-33.009,-79.567 -37.567,-57.580 -45.039,-77.402" /><polygon points="-82.096,11.602 -73.343,34.480 -87.799,19.915" /><polygon points="67.587,-28.426 63.731,-37.502 80.439,-41.958" /><polygon points="67.587,-28.426 87.941,-23.807 86.214,-15.842" /><polygon points="67.587,-28.426 88.177,-31.892 87.941,-23.807" /><polygon points="-1.932,64.792 12.056,63.859 4.849,91.647" /><polygon points="-11.894,72.073 -1.932,64.792 4.849,91.647" /><polygon points="4.849,91.647 12.056,63.859 19.955,72.312" /><polygon points="-87.799,19.915 -73.343,34.480 -87.733,27.689" /><polygon points="-75.737,-39.165 -70.008,-25.040 -84.832,-35.327" /><polygon points="-84.832,-35.327 -70.008,-25.040 -77.243,-16.444" /><polygon points="-73.832,47.658 -52.199,58.316 -70.076,59.862" /><polygon points="28.068,76.437 36.157,68.500 39.111,84.258" /><polygon points="39.111,84.258 36.157,68.500 48.846,68.505" /><polygon points="62.011,7.310 80.568,25.114 62.488,16.028" /><polygon points="80.568,25.114 62.011,7.310 92.457,15.721" /><polygon points="92.457,15.721 62.011,7.310 74.389,-7.393" /><polygon points="74.389,-7.393 93.890,3.258 92.457,15.721" /><polygon points="88.177,-31.892 67.587,-28.426 80.439,-41.958" /><polygon points="-52.199,58.316 -31.159,67.755 -56.744,75.040" /><polygon points="-70.076,59.862 -52.199,58.316 -56.744,75.040" /><polygon points="-31.159,67.755 -52.199,58.316 -32.301,57.248" /><polygon points="-31.159,67.755 -52.619,83.044 -56.744,75.040" /><polygon points="93.890,3.258 74.389,-7.393 86.214,-15.842" /><polygon points="-41.961,-48.638 -56.171,-42.046 -66.945,-65.740" /><polygon points="-37.567,-57.580 -41.961,-48.638 -66.945,-65.740" /><polygon points="-45.039,-77.402 -37.567,-57.580 -66.945,-65.740" /><polygon points="-66.945,-65.740 -56.171,-42.046 -68.680,-43.050" /><polygon points="-29.763,81.949 -11.894,72.073 -20.411,93.630" /><polygon points="-20.411,93.630 -11.894,72.073 4.849,91.647" /><polygon points="-84.832,-35.327 -77.243,-16.444 -93.002,-24.824" /><polygon points="30.389,-73.990 20.807,-79.504 29.190,-92.060" /><polygon points="29.190,-92.060 20.807,-79.504 10.818,-88.076" /><polygon points="-87.733,27.689 -73.343,34.480 -90.069,36.852" /><polygon points="-90.069,36.852 -73.343,34.480 -73.832,47.658" /><polygon points="55.932,-67.938 49.151,-85.159 55.390,-79.910" /><polygon points="49.151,-85.159 55.932,-67.938 30.389,-73.990" /><polygon points="-31.159,67.755 -45.601,87.139 -52.619,83.044" /><polygon points="-45.601,87.139 -31.159,67.755 -29.763,81.949" /><polygon points="79.580,37.069 80.568,25.114 92.176,33.988" /><polygon points="92.176,33.988 80.568,25.114 92.457,15.721" /><polygon points="30.389,-73.990 42.423,-89.927 49.151,-85.159" /><polygon points="-45.039,-77.402 -66.945,-65.740 -60.833,-78.263" /><polygon points="42.423,-89.927 30.389,-73.990 29.190,-92.060" /><polygon points="-0.926,-80.321 -23.852,-74.312 -9.367,-99.157" /><polygon points="10.818,-88.076 -0.926,-80.321 -9.367,-99.157" /><polygon points="-23.852,-74.312 -0.926,-80.321 -16.073,-69.292" /><polygon points="-45.601,87.139 -29.763,81.949 -39.851,94.397" /><polygon points="-39.851,94.397 -29.763,81.949 -20.411,93.630" /><polygon points="-33.009,-79.567 -45.039,-77.402 -46.746,-90.695" /><polygon points="-46.746,-90.695 -45.039,-77.402 -60.833,-78.263" /><polygon points="-90.069,36.852 -73.832,47.658 -88.785,51.085" /><polygon points="60.408,-47.429 80.439,-41.958 63.731,-37.502" /><polygon points="80.439,-41.958 60.408,-47.429 83.664,-59.041" /><polygon points="60.408,-47.429 77.295,-71.760 83.664,-59.041" /><polygon points="39.111,84.258 48.846,68.505 55.465,86.853" /><polygon points="-20.411,93.630 4.849,91.647 -8.339,102.863" /><polygon points="88.177,-31.892 80.439,-41.958 93.902,-43.031" /><polygon points="93.902,-43.031 80.439,-41.958 83.664,-59.041" /><polygon points="55.219,58.899 63.558,43.598 76.850,69.467" /><polygon points="76.850,69.467 63.558,43.598 72.425,41.331" /><polygon points="4.849,91.647 22.958,103.551 14.074,103.218" /><polygon points="-8.339,102.863 4.849,91.647 14.074,103.218" /><polygon points="28.068,76.437 4.849,91.647 19.955,72.312" /><polygon points="22.958,103.551 4.849,91.647 28.068,76.437" /><polygon points="30.933,99.704 28.068,76.437 39.111,84.258" /><polygon points="-23.852,-74.312 -26.982,-101.630 -9.367,-99.157" /><polygon points="-26.982,-101.630 -23.852,-74.312 -33.009,-79.567" /><polygon points="-35.175,-98.759 -33.009,-79.567 -46.746,-90.695" /><polygon points="-26.982,-101.630 -33.009,-79.567 -35.175,-98.759" /><polygon points="-79.264,-6.037 -82.096,11.602 -105.618,0.702" /><polygon points="-105.618,0.702 -82.096,11.602 -87.799,19.915" /><polygon points="77.295,-71.760 60.408,-47.429 55.932,-67.938" /><polygon points="77.295,-71.760 55.932,-67.938 55.390,-79.910" /><polygon points="22.958,103.551 28.068,76.437 30.933,99.704" /><polygon points="-9.367,-99.157 -26.982,-101.630 -18.659,-104.411" /><polygon points="30.933,99.704 39.111,84.258 40.178,99.581" /><polygon points="40.178,99.581 39.111,84.258 55.465,86.853" /><polygon points="-84.832,-35.327 -93.002,-24.824 -101.988,-32.887" /><polygon points="-39.851,94.397 -20.411,93.630 -28.406,104.374" /><polygon points="-28.406,104.374 -20.411,93.630 -8.339,102.863" /><polygon points="42.423,-89.927 29.190,-92.060 37.888,-101.230" /><polygon points="-79.264,-6.037 -93.002,-24.824 -77.243,-16.444" /><polygon points="-93.002,-24.824 -79.264,-6.037 -107.534,-13.741" /><polygon points="-101.988,-32.887 -93.002,-24.824 -107.534,-13.741" /><polygon points="-107.534,-13.741 -79.264,-6.037 -105.618,0.702" /><polygon points="92.176,33.988 92.457,15.721 107.059,23.886" /><polygon points="72.425,41.331 79.580,37.069 97.368,51.561" /><polygon points="72.425,41.331 95.493,61.163 76.850,69.467" /><polygon points="97.368,51.561 79.580,37.069 92.176,33.988" /><polygon points="-84.832,-35.327 -96.432,-56.285 -75.737,-39.165" /><polygon points="-99.275,-47.427 -84.832,-35.327 -101.988,-32.887" /><polygon points="-66.945,-65.740 -68.680,-43.050 -84.633,-70.372" /><polygon points="-84.633,-70.372 -68.680,-43.050 -75.737,-39.165" /><polygon points="-75.737,-39.165 -96.432,-56.285 -84.633,-70.372" /><polygon points="29.190,-92.060 10.818,-88.076 19.007,-108.573" /><polygon points="37.888,-101.230 29.190,-92.060 19.007,-108.573" /><polygon points="10.818,-88.076 4.580,-115.719 19.007,-108.573" /><polygon points="-96.432,-56.285 -84.832,-35.327 -99.275,-47.427" /><polygon points="-90.069,36.852 -88.785,51.085 -99.804,50.826" /><polygon points="97.368,51.561 92.176,33.988 103.059,44.101" /><polygon points="103.059,44.101 92.176,33.988 107.059,23.886" /><polygon points="-35.175,-98.759 -46.746,-90.695 -46.436,-102.650" /><polygon points="76.850,69.467 55.465,86.853 48.846,68.505" /><polygon points="55.465,86.853 76.850,69.467 73.028,86.339" /><polygon points="76.850,69.467 48.846,68.505 55.219,58.899" /><polygon points="95.493,61.163 72.425,41.331 97.368,51.561" /><polygon points="40.178,99.581 55.465,86.853 56.142,98.686" /><polygon points="56.142,98.686 55.465,86.853 73.028,86.339" /><polygon points="76.850,69.467 95.493,61.163 90.181,68.887" /><polygon points="93.890,3.258 86.214,-15.842 114.110,-5.479" /><polygon points="-84.633,-70.372 -96.432,-56.285 -93.649,-65.592" /><polygon points="-87.733,27.689 -90.069,36.852 -108.541,38.064" /><polygon points="-108.541,38.064 -90.069,36.852 -99.804,50.826" /><polygon points="87.941,-23.807 88.177,-31.892 110.714,-33.758" /><polygon points="110.714,-33.758 88.177,-31.892 93.902,-43.031" /><polygon points="93.902,-43.031 83.664,-59.041 97.941,-61.875" /><polygon points="-70.076,59.862 -56.744,75.040 -81.836,82.528" /><polygon points="4.580,-115.719 10.818,-88.076 -9.367,-99.157" /><polygon points="-39.851,94.397 -28.406,104.374 -36.324,110.793" /><polygon points="-70.076,59.862 -88.785,51.085 -73.832,47.658" /><polygon points="-88.785,51.085 -70.076,59.862 -92.607,71.261" /><polygon points="-99.804,50.826 -88.785,51.085 -92.607,71.261" /><polygon points="-92.607,71.261 -70.076,59.862 -81.836,82.528" /><polygon points="110.714,-33.758 93.902,-43.031 108.384,-43.116" /><polygon points="108.384,-43.116 93.902,-43.031 97.941,-61.875" /><polygon points="-99.275,-47.427 -101.988,-32.887 -109.422,-41.564" /><polygon points="-46.746,-90.695 -60.833,-78.263 -67.231,-96.263" /><polygon points="-46.436,-102.650 -46.746,-90.695 -67.231,-96.263" /><polygon points="-87.799,19.915 -87.733,27.689 -114.095,29.696" /><polygon points="-105.618,0.702 -87.799,19.915 -114.095,29.696" /><polygon points="-114.095,29.696 -87.733,27.689 -108.541,38.064" /><polygon points="103.059,44.101 107.059,23.886 114.273,31.700" /><polygon points="-45.601,87.139 -39.851,94.397 -52.195,107.899" /><polygon points="-52.195,107.899 -39.851,94.397 -36.324,110.793" /><polygon points="-105.618,0.702 -114.095,29.696 -119.227,10.676" /><polygon points="87.941,-23.807 114.110,-5.479 86.214,-15.842" /><polygon points="114.110,-5.479 87.941,-23.807 117.669,-25.486" /><polygon points="117.669,-25.486 87.941,-23.807 110.714,-33.758" /><polygon points="40.178,99.581 56.142,98.686 54.373,108.068" /><polygon points="49.151,-85.159 42.423,-89.927 57.209,-106.771" /><polygon points="57.209,-106.771 42.423,-89.927 37.888,-101.230" /><polygon points="37.888,-101.230 19.007,-108.573 27.057,-118.551" /><polygon points="93.890,3.258 107.059,23.886 92.457,15.721" /><polygon points="107.059,23.886 93.890,3.258 121.636,10.458" /><polygon points="114.273,31.700 107.059,23.886 121.636,10.458" /><polygon points="121.636,10.458 93.890,3.258 114.110,-5.479" /><polygon points="-119.227,10.676 -114.095,29.696 -120.352,20.903" /><polygon points="114.110,-5.479 117.669,-25.486 121.139,-15.916" /><polygon points="108.384,-43.116 97.941,-61.875 107.521,-60.476" /><polygon points="-52.619,83.044 -52.195,107.899 -62.985,106.712" /><polygon points="-52.195,107.899 -52.619,83.044 -45.601,87.139" /><polygon points="-101.988,-32.887 -107.534,-13.741 -122.122,-21.993" /><polygon points="-109.422,-41.564 -101.988,-32.887 -122.122,-21.993" /><polygon points="77.295,-71.760 55.390,-79.910 83.612,-91.596" /><polygon points="-28.406,104.374 -8.339,102.863 -16.399,123.538" /><polygon points="-36.324,110.793 -28.406,104.374 -16.399,123.538" /><polygon points="-60.833,-78.263 -84.633,-70.372 -84.770,-92.208" /><polygon points="-67.231,-96.263 -60.833,-78.263 -84.770,-92.208" /><polygon points="-84.633,-70.372 -60.833,-78.263 -66.945,-65.740" /><polygon points="-8.339,102.863 14.074,103.218 0.223,125.935" /><polygon points="-16.399,123.538 -8.339,102.863 0.223,125.935" /><polygon points="-46.436,-102.650 -67.231,-96.263 -67.306,-107.697" /><polygon points="55.390,-79.910 57.209,-106.771 69.077,-106.701" /><polygon points="83.612,-91.596 55.390,-79.910 69.077,-106.701" /><polygon points="57.209,-106.771 55.390,-79.910 49.151,-85.159" /><polygon points="37.888,-101.230 27.057,-118.551 34.788,-123.071" /><polygon points="57.209,-106.771 37.888,-101.230 34.788,-123.071" /><polygon points="-81.836,82.528 -74.868,105.792 -82.761,98.551" /><polygon points="-52.619,83.044 -81.836,82.528 -56.744,75.040" /><polygon points="-62.985,106.712 -81.836,82.528 -52.619,83.044" /><polygon points="-74.868,105.792 -81.836,82.528 -62.985,106.712" /><polygon points="22.958,103.551 30.933,99.704 32.066,125.715" /><polygon points="32.066,125.715 30.933,99.704 40.178,99.581" /><polygon points="32.066,125.715 40.178,99.581 54.373,108.068" /><polygon points="-99.804,50.826 -92.607,71.261 -107.202,74.095" /><polygon points="77.295,-71.760 97.941,-61.875 83.664,-59.041" /><polygon points="97.941,-61.875 77.295,-71.760 99.964,-83.719" /><polygon points="99.964,-83.719 77.295,-71.760 83.612,-91.596" /><polygon points="83.612,-91.596 69.077,-106.701 79.657,-103.232" /><polygon points="56.142,98.686 73.028,86.339 79.890,104.876" /><polygon points="54.373,108.068 56.142,98.686 79.890,104.876" /><polygon points="-109.422,-41.564 -122.122,-21.993 -128.636,-30.020" /><polygon points="4.580,-115.719 -18.659,-104.411 -5.735,-132.991" /><polygon points="-18.659,-104.411 4.580,-115.719 -9.367,-99.157" /><polygon points="-18.659,-104.411 -26.982,-101.630 -27.640,-130.690" /><polygon points="-5.735,-132.991 -18.659,-104.411 -27.640,-130.690" /><polygon points="-107.534,-13.741 -105.618,0.702 -133.855,-5.952" /><polygon points="-122.122,-21.993 -107.534,-13.741 -133.855,-5.952" /><polygon points="-133.855,-5.952 -105.618,0.702 -119.227,10.676" /><polygon points="73.028,86.339 90.181,68.887 95.356,95.484" /><polygon points="79.890,104.876 73.028,86.339 95.356,95.484" /><polygon points="90.181,68.887 73.028,86.339 76.850,69.467" /><polygon points="-36.324,110.793 -16.399,123.538 -24.788,133.890" /><polygon points="14.074,103.218 32.066,125.715 22.742,134.840" /><polygon points="0.223,125.935 14.074,103.218 22.742,134.840" /><polygon points="32.066,125.715 14.074,103.218 22.958,103.551" /><polygon points="-99.804,50.826 -107.202,74.095 -116.459,71.994" /><polygon points="0.223,125.935 22.742,134.840 11.566,136.667" /><polygon points="4.580,-115.719 27.057,-118.551 19.007,-108.573" /><polygon points="27.057,-118.551 4.580,-115.719 12.503,-137.555" /><polygon points="12.503,-137.555 4.580,-115.719 -5.735,-132.991" /><polygon points="-5.735,-132.991 -27.640,-130.690 -17.334,-137.110" /><polygon points="114.273,31.700 121.636,10.458 137.291,18.907" /><polygon points="-93.649,-65.592 -96.432,-56.285 -117.021,-76.572" /><polygon points="54.373,108.068 79.890,104.876 79.987,116.943" /><polygon points="-84.770,-92.208 -93.649,-65.592 -104.155,-96.012" /><polygon points="-93.649,-65.592 -84.770,-92.208 -84.633,-70.372" /><polygon points="-104.155,-96.012 -93.649,-65.592 -117.021,-76.572" /><polygon points="-92.607,71.261 -81.836,82.528 -105.109,95.689" /><polygon points="-107.202,74.095 -92.607,71.261 -105.109,95.689" /><polygon points="-105.109,95.689 -81.836,82.528 -82.761,98.551" /><polygon points="121.636,10.458 114.110,-5.479 143.378,0.446" /><polygon points="137.291,18.907 121.636,10.458 143.378,0.446" /><polygon points="143.378,0.446 114.110,-5.479 121.139,-15.916" /><polygon points="90.181,68.887 95.493,61.163 116.827,83.536" /><polygon points="95.356,95.484 90.181,68.887 116.827,83.536" /><polygon points="95.356,95.484 116.827,83.536 110.724,93.908" /><polygon points="99.964,-83.719 107.521,-60.476 97.941,-61.875" /><polygon points="107.521,-60.476 99.964,-83.719 115.719,-87.261" /><polygon points="-84.770,-92.208 -67.306,-107.697 -67.231,-96.263" /><polygon points="-67.306,-107.697 -84.770,-92.208 -92.792,-111.901" /><polygon points="-92.792,-111.901 -84.770,-92.208 -104.155,-96.012" /><polygon points="-104.155,-96.012 -117.021,-76.572 -114.894,-89.553" /><polygon points="114.273,31.700 137.291,18.907 144.315,27.447" /><polygon points="-16.399,123.538 0.223,125.935 -7.385,149.624" /><polygon points="-24.788,133.890 -16.399,123.538 -7.385,149.624" /><polygon points="-7.385,149.624 0.223,125.935 11.566,136.667" /><polygon points="-133.855,-5.952 -119.227,10.676 -149.627,5.722" /><polygon points="121.139,-15.916 117.669,-25.486 148.298,-24.269" /><polygon points="143.378,0.446 121.139,-15.916 148.298,-24.269" /><polygon points="12.503,-137.555 34.788,-123.071 27.057,-118.551" /><polygon points="34.788,-123.071 12.503,-137.555 21.319,-149.411" /><polygon points="99.964,-83.719 83.612,-91.596 107.601,-106.413" /><polygon points="115.719,-87.261 99.964,-83.719 107.601,-106.413" /><polygon points="107.601,-106.413 83.612,-91.596 79.657,-103.232" /><polygon points="-133.855,-5.952 -128.636,-30.020 -122.122,-21.993" /><polygon points="-128.636,-30.020 -133.855,-5.952 -151.505,-14.517" /><polygon points="-151.505,-14.517 -133.855,-5.952 -149.627,5.722" /><polygon points="-120.352,20.903 -149.627,5.722 -119.227,10.676" /><polygon points="-149.627,5.722 -120.352,20.903 -151.737,18.432" /><polygon points="143.378,0.446 148.298,-24.269 152.362,-12.146" /><polygon points="-67.306,-107.697 -92.792,-111.901 -93.677,-125.496" /><polygon points="79.890,104.876 95.356,95.484 105.159,116.649" /><polygon points="79.987,116.943 79.890,104.876 105.159,116.649" /><polygon points="105.159,116.649 95.356,95.484 110.724,93.908" /><polygon points="-105.109,95.689 -82.761,98.551 -107.261,115.226" /><polygon points="-105.109,95.689 -116.459,71.994 -107.202,74.095" /><polygon points="-116.459,71.994 -105.109,95.689 -122.628,100.681" /><polygon points="-122.628,100.681 -105.109,95.689 -107.261,115.226" /><polygon points="12.503,-137.555 -5.735,-132.991 1.269,-158.871" /><polygon points="21.319,-149.411 12.503,-137.555 1.269,-158.871" /><polygon points="1.269,-158.871 -5.735,-132.991 -17.334,-137.110" /><polygon points="-82.761,98.551 -74.868,105.792 -98.840,125.541" /><polygon points="-107.261,115.226 -82.761,98.551 -98.840,125.541" /><polygon points="107.601,-106.413 79.657,-103.232 104.209,-121.660" /><polygon points="137.291,18.907 143.378,0.446 163.691,8.880" /><polygon points="144.315,27.447 137.291,18.907 163.691,8.880" /><polygon points="163.691,8.880 143.378,0.446 152.362,-12.146" /><polygon points="-7.385,149.624 11.566,136.667 5.493,164.677" /><polygon points="-24.788,133.890 -7.385,149.624 -16.519,163.976" /><polygon points="-16.519,163.976 -7.385,149.624 5.493,164.677" /><polygon points="-92.792,-111.901 -104.155,-96.012 -116.192,-118.640" /><polygon points="-93.677,-125.496 -92.792,-111.901 -116.192,-118.640" /><polygon points="1.269,-158.871 -17.334,-137.110 -12.478,-166.789" /><polygon points="115.719,-87.261 107.601,-106.413 127.182,-113.535" /><polygon points="127.182,-113.535 107.601,-106.413 104.209,-121.660" /><polygon points="105.159,116.649 110.724,93.908 125.187,117.679" /><polygon points="-151.505,-14.517 -149.627,5.722 -172.915,-2.225" /><polygon points="-16.519,163.976 -98.840,125.541 -24.788,133.890" /><polygon points="22.742,134.840 5.493,164.677 11.566,136.667" /><polygon points="-24.788,133.890 -52.195,107.899 -36.324,110.793" /><polygon points="-24.788,133.890 -62.985,106.712 -52.195,107.899" /><polygon points="-62.985,106.712 -24.788,133.890 -98.840,125.541" /><polygon points="-62.985,106.712 -98.840,125.541 -74.868,105.792" /><polygon points="79.987,116.943 22.742,134.840 32.066,125.715" /><polygon points="79.987,116.943 32.066,125.715 54.373,108.068" /><polygon points="116.827,83.536 125.187,117.679 110.724,93.908" /><polygon points="-116.459,71.994 -114.095,29.696 -108.541,38.064" /><polygon points="-116.459,71.994 -108.541,38.064 -99.804,50.826" /><polygon points="95.493,61.163 103.059,44.101 116.827,83.536" /><polygon points="116.827,83.536 114.273,31.700 144.315,27.447" /><polygon points="103.059,44.101 95.493,61.163 97.368,51.561" /><polygon points="114.273,31.700 116.827,83.536 103.059,44.101" /><polygon points="-114.095,29.696 -151.737,18.432 -120.352,20.903" /><polygon points="-151.737,18.432 -114.095,29.696 -116.459,71.994" /><polygon points="-151.737,18.432 -172.915,-2.225 -149.627,5.722" /><polygon points="-151.505,-14.517 -117.021,-76.572 -128.636,-30.020" /><polygon points="-128.636,-30.020 -117.021,-76.572 -109.422,-41.564" /><polygon points="108.384,-43.116 117.669,-25.486 110.714,-33.758" /><polygon points="117.669,-25.486 108.384,-43.116 148.298,-24.269" /><polygon points="107.521,-60.476 148.298,-24.269 108.384,-43.116" /><polygon points="115.719,-87.261 148.298,-24.269 107.521,-60.476" /><polygon points="-109.422,-41.564 -117.021,-76.572 -96.432,-56.285" /><polygon points="-109.422,-41.564 -96.432,-56.285 -99.275,-47.427" /><polygon points="-114.894,-89.553 -116.192,-118.640 -104.155,-96.012" /><polygon points="-67.306,-107.697 -27.640,-130.690 -46.436,-102.650" /><polygon points="69.077,-106.701 104.209,-121.660 79.657,-103.232" /><polygon points="-93.677,-125.496 -27.640,-130.690 -67.306,-107.697" /><polygon points="34.788,-123.071 69.077,-106.701 57.209,-106.771" /><polygon points="69.077,-106.701 34.788,-123.071 104.209,-121.660" /><polygon points="21.319,-149.411 104.209,-121.660 34.788,-123.071" /><polygon points="-46.436,-102.650 -27.640,-130.690 -26.982,-101.630" /><polygon points="-93.677,-125.496 -12.478,-166.789 -27.640,-130.690" /><polygon points="-27.640,-130.690 -12.478,-166.789 -17.334,-137.110" /><polygon points="-46.436,-102.650 -26.982,-101.630 -35.175,-98.759" /></g><circle cx="6.533" cy="1.324" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.197" cy="5.533" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="3.672" cy="-12.416" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="6.377" cy="13.064" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.922" cy="-5.875" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="16.602" cy="-6.374" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.917" cy="17.880" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.253" cy="-21.825" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="22.090" cy="13.548" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.923" cy="5.607" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="19.715" cy="-26.608" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.897" cy="14.215" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="33.964" cy="11.776" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="3.797" cy="36.399" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="15.049" cy="-33.582" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.565" cy="-32.506" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.394" cy="37.382" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="12.246" cy="36.912" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.645" cy="-26.276" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="33.712" cy="-22.230" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="41.912" cy="-0.688" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.554" cy="-6.649" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.693" cy="-20.468" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="26.909" cy="33.930" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-31.466" cy="29.789" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="3.111" cy="-43.666" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="44.702" cy="-10.599" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.822" cy="44.626" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="43.796" cy="-21.113" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.324" cy="-45.793" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.264" cy="41.638" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-48.321" cy="-14.547" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="45.848" cy="23.145" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="27.224" cy="45.752" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.925" cy="-54.169" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.021" cy="14.910" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="11.493" cy="-55.310" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="22.639" cy="53.101" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="45.549" cy="37.128" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="19.445" cy="-55.213" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.450" cy="-24.265" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-48.456" cy="34.908" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="31.500" cy="-50.803" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.745" cy="28.704" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="58.414" cy="-19.064" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-61.871" cy="3.576" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="62.011" cy="7.310" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="44.658" cy="-45.923" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.961" cy="-48.638" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="62.488" cy="16.028" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-1.932" cy="64.792" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="12.056" cy="63.859" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.301" cy="57.248" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="61.162" cy="24.772" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-62.859" cy="23.431" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.775" cy="-60.441" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-37.567" cy="-57.580" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-20.428" cy="66.678" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-56.171" cy="-42.046" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.936" cy="-62.503" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.073" cy="-69.292" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-11.894" cy="72.073" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.587" cy="-28.426" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="63.731" cy="-37.502" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-31.159" cy="67.755" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-70.008" cy="-25.040" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="19.955" cy="72.312" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="74.389" cy="-7.393" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="63.558" cy="43.598" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="60.408" cy="-47.429" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="36.157" cy="68.500" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.199" cy="58.316" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.852" cy="-74.312" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-77.243" cy="-16.444" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-79.264" cy="-6.037" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="30.389" cy="-73.990" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.926" cy="-80.321" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.219" cy="58.899" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-73.343" cy="34.480" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-68.680" cy="-43.050" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="28.068" cy="76.437" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="20.807" cy="-79.504" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.096" cy="11.602" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="72.425" cy="41.331" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.846" cy="68.505" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="80.568" cy="25.114" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-75.737" cy="-39.165" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.009" cy="-79.567" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.763" cy="81.949" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="79.580" cy="37.069" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.214" cy="-15.842" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-73.832" cy="47.658" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.932" cy="-67.938" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="10.818" cy="-88.076" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-45.039" cy="-77.402" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.799" cy="19.915" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="80.439" cy="-41.958" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="87.941" cy="-23.807" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="4.849" cy="91.647" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-87.733" cy="27.689" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.832" cy="-35.327" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-70.076" cy="59.862" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="39.111" cy="84.258" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="92.457" cy="15.721" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="88.177" cy="-31.892" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-56.744" cy="75.040" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="93.890" cy="3.258" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.945" cy="-65.740" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-20.411" cy="93.630" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-93.002" cy="-24.824" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="29.190" cy="-92.060" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-90.069" cy="36.852" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.390" cy="-79.910" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.619" cy="83.044" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-45.601" cy="87.139" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="92.176" cy="33.988" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.151" cy="-85.159" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-60.833" cy="-78.263" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.423" cy="-89.927" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.367" cy="-99.157" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.851" cy="94.397" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.746" cy="-90.695" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-88.785" cy="51.085" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="83.664" cy="-59.041" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.465" cy="86.853" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.339" cy="102.863" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="93.902" cy="-43.031" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="76.850" cy="69.467" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.074" cy="103.218" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="30.933" cy="99.704" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-35.175" cy="-98.759" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.982" cy="-101.630" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-105.618" cy="0.702" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="77.295" cy="-71.760" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.958" cy="103.551" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.659" cy="-104.411" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="40.178" cy="99.581" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-101.988" cy="-32.887" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.406" cy="104.374" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="37.888" cy="-101.230" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-107.534" cy="-13.741" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="107.059" cy="23.886" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="97.368" cy="51.561" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-99.275" cy="-47.427" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.633" cy="-70.372" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="19.007" cy="-108.573" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-96.432" cy="-56.285" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-99.804" cy="50.826" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="103.059" cy="44.101" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.436" cy="-102.650" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="73.028" cy="86.339" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="95.493" cy="61.163" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="56.142" cy="98.686" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="90.181" cy="68.887" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="114.110" cy="-5.479" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-93.649" cy="-65.592" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-108.541" cy="38.064" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="110.714" cy="-33.758" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="97.941" cy="-61.875" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-81.836" cy="82.528" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="4.580" cy="-115.719" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.324" cy="110.793" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-92.607" cy="71.261" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="108.384" cy="-43.116" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-109.422" cy="-41.564" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.231" cy="-96.263" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-114.095" cy="29.696" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="114.273" cy="31.700" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.195" cy="107.899" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-119.227" cy="10.676" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="117.669" cy="-25.486" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="54.373" cy="108.068" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.209" cy="-106.771" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="27.057" cy="-118.551" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="121.636" cy="10.458" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-120.352" cy="20.903" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="121.139" cy="-15.916" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="107.521" cy="-60.476" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-62.985" cy="106.712" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-122.122" cy="-21.993" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="83.612" cy="-91.596" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.399" cy="123.538" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-84.770" cy="-92.208" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="0.223" cy="125.935" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.306" cy="-107.697" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="69.077" cy="-106.701" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.788" cy="-123.071" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.761" cy="98.551" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-74.868" cy="105.792" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="32.066" cy="125.715" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-107.202" cy="74.095" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="99.964" cy="-83.719" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="79.657" cy="-103.232" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="79.890" cy="104.876" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-128.636" cy="-30.020" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.735" cy="-132.991" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.640" cy="-130.690" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-133.855" cy="-5.952" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="95.356" cy="95.484" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.788" cy="133.890" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.742" cy="134.840" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-116.459" cy="71.994" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.566" cy="136.667" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="12.503" cy="-137.555" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.334" cy="-137.110" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="137.291" cy="18.907" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-117.021" cy="-76.572" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="79.987" cy="116.943" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-104.155" cy="-96.012" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-105.109" cy="95.689" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="143.378" cy="0.446" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="116.827" cy="83.536" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="110.724" cy="93.908" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="115.719" cy="-87.261" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-92.792" cy="-111.901" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-114.894" cy="-89.553" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="144.315" cy="27.447" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.385" cy="149.624" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-149.627" cy="5.722" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="148.298" cy="-24.269" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="21.319" cy="-149.411" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="107.601" cy="-106.413" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-151.505" cy="-14.517" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-151.737" cy="18.432" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="152.362" cy="-12.146" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-93.677" cy="-125.496" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="105.159" cy="116.649" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-107.261" cy="115.226" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-122.628" cy="100.681" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="1.269" cy="-158.871" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-98.840" cy="125.541" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="104.209" cy="-121.660" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="163.691" cy="8.880" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.493" cy="164.677" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.519" cy="163.976" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-116.192" cy="-118.640" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-12.478" cy="-166.789" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="127.182" cy="-113.535" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="125.187" cy="117.679" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-172.915" cy="-2.225" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle r="9" fill="#061019" stroke="#dabb83" stroke-width="0.8"/><circle r="2" fill="#ffe5a2"/></g><g transform="translate(930.000 971.000)"><circle r="121.540" fill="url(#bloom-light)"/><polygon points="4.833,8.882 -2.046,3.080 0.310,-1.853 6.775,1.497 6.054,7.172" fill="#280f0c" stroke="#e99e96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.594,-4.980 -1.151,-3.991 -0.734,-2.352 -3.090,2.581 -7.491,4.385 -10.393,0.911 -11.246,-4.114" fill="#2b100c" stroke="#e99f96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.540,-2.701 7.231,0.616 0.766,-2.733 0.350,-4.372 2.890,-9.644 7.597,-9.098 11.371,-6.248" fill="#32150e" stroke="#e9a596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.268,9.552 4.124,10.506 -0.166,13.891 -2.943,14.173 -7.536,8.757 -7.012,5.554 -2.611,3.750" fill="#3e1f12" stroke="#e9af96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.074,-13.682 1.245,-10.741 1.500,-10.314 -1.041,-5.042 -10.484,-6.031 -10.101,-13.039 -9.962,-13.199" fill="#4c2e16" stroke="#e9ba96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.779,6.351 7.311,7.332 8.032,1.656 12.340,-1.662 18.758,3.414 18.764,3.509" fill="#5a3e1a" stroke="#e9c496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.053,12.038 -19.215,10.143 -19.658,6.209 -11.485,1.823 -8.582,5.297 -9.107,8.500" fill="#674f1e" stroke="#e9ce96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.879,16.292 6.029,10.792 6.172,9.839 7.394,8.129 16.861,7.148 16.308,14.409 13.560,16.210" fill="#3a1811" stroke="#e9a596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-20.016,5.543 -21.974,2.591 -20.420,-2.436 -19.195,-3.557 -12.695,-3.868 -11.842,1.157" fill="#371610" stroke="#e9a296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.005,-21.371 1.560,-11.739 -7.760,-14.680 -1.275,-23.108 -0.291,-22.971" fill="#4a2815" stroke="#e9b396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-15.507,19.630 -16.540,13.192 -8.593,9.654 -4.000,15.069 -4.340,15.548" fill="#63481d" stroke="#e9c996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.189,-20.125 7.783,-10.707 3.077,-11.253 2.822,-11.681 3.267,-21.313 10.042,-22.017" fill="#736121" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.949,-19.770 14.816,-19.275 17.080,-13.290 15.461,-9.485 12.317,-7.502 8.543,-10.352" fill="#3c1a12" stroke="#e9a696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.880,25.323 0.891,15.232 5.182,11.846 12.032,17.347 6.595,25.306" fill="#7d7324" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="25.706,-4.622 19.644,2.294 13.226,-2.782 13.057,-6.329 16.201,-8.311" fill="#796c23" stroke="#e9dc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.298,-14.303 -11.726,-13.128 -12.109,-6.120 -12.761,-5.254 -19.262,-4.943" fill="#827b26" stroke="#e9e396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.003,25.757 2.147,27.377 -4.302,23.460 -3.102,16.426 -2.763,15.948 0.014,15.665" fill="#442014" stroke="#e9ab96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-15.298,20.588 -15.247,20.342 -4.080,16.259 -5.279,23.293 -9.993,25.369 -10.415,25.309" fill="#512d17" stroke="#e9b596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-12.350,-23.396 -11.205,-25.176 -2.852,-24.266 -1.985,-23.654 -8.469,-15.226 -10.357,-14.743" fill="#431e13" stroke="#e9a996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.094,-5.902 25.992,-5.359 16.487,-9.048 18.106,-12.854 25.012,-12.921 27.081,-10.491 27.430,-6.696" fill="#6b501f" stroke="#e9cc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.859,10.297 27.969,11.139 20.338,15.150 17.497,14.500 18.050,7.239 20.035,4.396" fill="#411b13" stroke="#e9a596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.164,-15.505 -22.994,-16.090 -21.675,-21.725 -13.447,-23.143 -11.454,-14.491 -11.593,-14.330" fill="#4e2817" stroke="#e9b196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.712,-6.688 -29.553,-11.774 -28.560,-13.081 -24.055,-14.587 -23.225,-14.002 -20.189,-4.643 -21.414,-3.521" fill="#837a26" stroke="#e9e096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.978,-5.992 -21.679,-2.825 -23.233,2.201 -27.456,2.120 -30.853,-0.760 -31.458,-3.440" fill="#694b1e" stroke="#e9c896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.900,-21.739 23.327,-21.276 24.267,-20.575 25.004,-13.743 18.098,-13.675 15.834,-19.660" fill="#553019" stroke="#e9b696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.053,13.107 -32.022,11.562 -27.480,3.360 -23.257,3.442 -21.299,6.394 -20.855,10.328" fill="#441d14" stroke="#e9a696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.423,27.346 16.341,30.435 14.002,30.812 7.657,26.031 13.093,18.071 13.774,17.989" fill="#898428" stroke="#e9e496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.618,9.292 20.793,3.391 20.787,3.296 26.850,-3.620 27.951,-4.163 33.366,2.750" fill="#59341a" stroke="#e9b996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.104,26.934 14.456,17.578 17.204,15.777 20.046,16.428 23.642,21.816 21.147,26.567" fill="#837626" stroke="#e9de96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-1.328,33.789 -2.686,34.512 -3.578,34.587 -9.540,26.399 -4.826,24.323 1.623,28.240" fill="#4e2717" stroke="#e9ae96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.674,14.361 -20.476,11.581 -18.315,13.477 -17.282,19.915 -17.334,20.162 -17.715,20.409 -28.511,21.354" fill="#69491e" stroke="#e9c696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.836,-35.893 6.557,-35.912 11.403,-32.475 9.873,-23.644 3.098,-22.940 0.802,-24.540" fill="#7c6924" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.780,13.809 30.274,16.527 29.367,19.537 24.356,21.340 20.760,15.951 28.390,11.940" fill="#69491e" stroke="#e9c596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-5.113,-34.523 -2.725,-25.430 -11.078,-26.340 -11.432,-27.807 -10.118,-32.515 -6.530,-34.913" fill="#7d6a24" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.012,-36.258 -0.022,-24.905 -1.006,-25.042 -1.873,-25.653 -4.261,-34.747 2.590,-37.308" fill="#8f8b29" stroke="#e9e696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.794,-32.234 18.355,-33.250 20.343,-23.095 15.278,-21.017 13.410,-21.511 11.263,-23.403" fill="#482015" stroke="#e9a796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-36.269,9.690 -37.707,6.488 -31.576,0.093 -28.179,2.973 -32.721,11.174 -35.392,10.486" fill="#5f3a1c" stroke="#e9bb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.736,-32.346 -12.841,-27.468 -12.488,-26.001 -13.633,-24.221 -21.860,-22.802 -24.438,-28.522 -23.470,-31.550" fill="#4f2617" stroke="#e9ac96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.201,31.964 -16.731,21.923 -16.350,21.676 -11.467,26.396 -14.659,32.587" fill="#908d2a" stroke="#e9e696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.249,35.075 -6.609,36.488 -11.267,36.745 -13.825,33.017 -10.633,26.826 -10.210,26.887" fill="#7a6324" stroke="#e9d396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.966,25.486 -28.413,22.473 -17.617,21.529 -22.087,31.570 -22.271,31.668 -28.550,29.002" fill="#806d25" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="34.633,-17.297 28.187,-11.433 26.118,-13.863 25.381,-20.696 30.120,-21.391 34.633,-18.229" fill="#8c8529" stroke="#e9e296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="9.170,40.135 0.122,34.560 3.073,29.011 5.930,27.391 6.645,27.374 12.990,32.156" fill="#4e2417" stroke="#e9aa96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.419,-32.990 -22.277,-33.323 -18.415,-36.975 -18.069,-37.062 -11.210,-32.820 -12.524,-28.112" fill="#715121" stroke="#e9c896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.060,5.312 -41.382,-0.132 -38.743,-3.271 -32.764,-3.145 -32.159,-0.465 -38.289,5.930" fill="#897c28" stroke="#e9dd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.410,-32.594 23.616,-22.799 21.190,-23.261 19.201,-33.416 21.643,-35.213 23.765,-35.274 25.966,-34.316" fill="#60381c" stroke="#e9b996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-36.122,-24.558 -36.096,-24.625 -25.948,-27.842 -23.371,-22.122 -24.690,-16.488 -29.195,-14.981" fill="#572d19" stroke="#e9b096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.213,-8.020 42.777,-4.286 39.877,2.120 34.506,1.857 29.091,-5.056 29.428,-5.850" fill="#68441e" stroke="#e9c096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.027,-9.032 29.241,-6.862 28.892,-10.658 35.339,-16.521 41.222,-10.256" fill="#8b7f28" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.259,9.450 43.266,10.194 31.176,12.771 29.786,10.902 29.676,10.060 34.425,3.518 39.795,3.782" fill="#522818" stroke="#e9ac96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.948,-11.465 -31.102,-11.823 -31.261,-6.737 -32.742,-4.185 -38.721,-4.310" fill="#94912b" stroke="#e9e796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.319,30.304 22.238,27.140 24.733,22.389 29.745,20.586 31.698,23.738 30.180,30.059" fill="#96952b" stroke="#e9e896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.066,-32.340 31.543,-30.533 32.191,-29.422 29.951,-22.539 25.213,-21.844 24.272,-22.545" fill="#806a25" stroke="#e9d496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.642,-37.753 -12.807,-42.282 -9.210,-40.685 -7.195,-35.908 -10.783,-33.510" fill="#948f2b" stroke="#e9e596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.746,11.808 43.473,15.236 41.291,18.448 31.831,16.244 31.337,13.526 43.427,10.949" fill="#725021" stroke="#e9c696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-37.078,-23.867 -30.151,-14.290 -31.144,-12.983 -40.990,-12.625 -41.312,-12.820 -41.182,-20.965" fill="#704d21" stroke="#e9c596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.255,24.140 -41.781,23.602 -35.882,12.388 -33.211,13.076 -31.242,14.622 -30.078,21.615 -31.631,24.627" fill="#542818" stroke="#e9ab96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.782,39.786 -21.487,33.135 -21.303,33.037 -14.761,33.659 -12.203,37.387 -13.852,39.730 -21.152,40.794" fill="#99992c" stroke="#e9e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.127,19.151 40.698,22.068 39.375,23.454 32.714,23.109 30.760,19.956 31.667,16.947" fill="#8d8129" stroke="#e9de96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="1.369,48.034 -1.793,36.189 -0.435,35.466 8.613,41.041 8.955,43.454 5.562,47.650 3.645,48.339" fill="#643b1d" stroke="#e9b996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-1.567,-46.728 2.135,-38.527 -4.717,-35.966 -6.134,-36.356 -8.148,-41.132 -1.580,-46.734" fill="#90862a" stroke="#e9e096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="23.902,40.207 17.617,31.708 20.699,28.619 21.741,28.251 28.822,31.415 25.011,40.095" fill="#7f6625" stroke="#e9d196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="32.983,-29.164 38.855,-26.206 38.347,-22.270 35.257,-19.120 30.743,-22.281" fill="#9a9b2d" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="22.945,40.914 18.704,44.993 10.844,43.186 10.502,40.772 14.321,32.793 16.660,32.416" fill="#5b2f1a" stroke="#e9b096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="0.635,48.230 -1.477,48.768 -6.168,47.208 -5.780,37.873 -3.420,36.460 -2.528,36.385" fill="#826a26" stroke="#e9d396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.944,-46.965 15.591,-46.912 20.476,-36.798 18.035,-35.001 12.474,-33.984 7.628,-37.421" fill="#552819" stroke="#e9aa96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.923,47.176 -9.325,47.630 -11.418,46.542 -12.841,40.441 -11.193,38.098 -6.534,37.842" fill="#97932c" stroke="#e9e596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-47.373,-9.711 -42.188,-11.374 -41.866,-11.179 -39.639,-4.025 -42.279,-0.885 -44.373,-1.344 -48.524,-8.412" fill="#97922c" stroke="#e9e596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="34.459,31.681 31.150,30.293 32.669,23.972 39.331,24.317 38.575,30.986" fill="#9b9e2e" stroke="#e7e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="47.617,-16.517 42.048,-11.032 36.165,-17.297 36.164,-18.230 39.254,-21.380 47.731,-17.253" fill="#877127" stroke="#e9d696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-48.595,15.355 -37.596,11.151 -36.719,11.947 -42.618,23.162 -46.335,21.792 -47.963,19.768" fill="#775623" stroke="#e9c896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-48.877,14.616 -49.350,13.889 -49.092,8.250 -40.087,6.592 -39.316,7.211 -37.879,10.412" fill="#90822a" stroke="#e9dd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.137,-34.240 -37.596,-37.391 -25.391,-32.164 -26.358,-29.136 -36.506,-25.919" fill="#653b1d" stroke="#e9b796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.235,7.469 -50.342,5.533 -49.820,2.901 -44.647,-0.091 -42.553,0.368 -40.231,5.811" fill="#9d9e2e" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.818,-47.592 6.502,-38.049 5.780,-38.030 3.359,-39.080 -0.343,-47.281 8.303,-50.650" fill="#63371d" stroke="#e9b596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.924,38.207 -29.147,30.408 -22.868,33.074 -23.163,39.725 -33.368,39.668" fill="#724d21" stroke="#e9c296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.103,44.375 -21.018,41.709 -13.718,40.645 -12.295,46.746 -18.772,48.358 -19.267,48.015" fill="#9d9c2e" stroke="#e9e896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.876,-50.978 -2.148,-47.400 -8.716,-41.798 -12.313,-43.395 -12.573,-44.847 -7.137,-52.030" fill="#91832a" stroke="#e9de96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-35.072,37.503 -41.523,34.484 -41.334,25.701 -31.710,26.188 -30.295,29.704" fill="#5b2c1a" stroke="#e9ac96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.530,-46.591 -21.077,-48.252 -18.155,-48.513 -13.889,-44.611 -13.629,-43.159 -18.464,-38.630 -18.810,-38.543" fill="#9ea22f" stroke="#e6e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-29.288,-44.783 -29.234,-44.853 -23.287,-46.241 -19.567,-38.193 -23.429,-34.540" fill="#9b972d" stroke="#e9e596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="53.156,-2.430 54.625,1.839 44.856,8.475 41.392,2.806 44.292,-3.600" fill="#61331c" stroke="#e9b296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.845,43.416 25.939,40.503 29.750,31.823 30.611,31.578 33.920,32.967 33.548,43.200" fill="#7c5b24" stroke="#e9ca96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.019,-44.365 -24.160,-34.122 -24.301,-33.790 -25.035,-32.994 -37.241,-38.222 -37.205,-39.212 -35.467,-41.956" fill="#8a7428" stroke="#e9d696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-50.809,-7.459 -49.269,-7.974 -45.119,-0.906 -50.292,2.086 -54.096,-3.425 -53.693,-4.661" fill="#99922d" stroke="#e9e396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="49.852,-20.632 48.085,-17.980 39.608,-22.107 40.116,-26.043 40.800,-26.476 50.033,-24.115 50.238,-23.308" fill="#897128" stroke="#e9d496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-50.255,-22.569 -42.529,-20.986 -42.659,-12.842 -47.844,-11.179 -52.139,-21.194" fill="#683b1e" stroke="#e9b696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.108,-36.613 41.250,-35.744 42.403,-32.820 40.051,-27.657 39.368,-27.224 33.496,-30.182 32.848,-31.293" fill="#9fa32f" stroke="#e6e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.493,-49.709 23.704,-37.393 21.581,-37.331 16.696,-47.445 21.860,-51.282 26.179,-50.772" fill="#7b5824" stroke="#e9c796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="53.341,-3.827 44.477,-4.998 42.913,-8.732 43.108,-9.956 48.677,-15.441 55.562,-8.181" fill="#5f2f1b" stroke="#e9ae96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.595,-42.614 38.543,-37.277 32.284,-31.958 28.806,-33.765 27.362,-35.487" fill="#a0a22f" stroke="#e7e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.090,45.846 -33.373,40.597 -23.168,40.654 -22.538,41.661 -22.623,44.326 -32.947,48.486" fill="#724821" stroke="#e9be96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.063,-52.869 -7.788,-52.522 -13.224,-45.339 -17.490,-49.241 -13.637,-54.842 -11.630,-54.797" fill="#94842b" stroke="#e9dc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.361,-49.442 30.714,-48.870 35.024,-43.641 35.006,-43.295 26.772,-36.167 24.572,-37.126" fill="#998f2c" stroke="#e9e196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="48.110,27.226 47.983,29.728 45.355,31.781 39.738,31.170 39.651,31.108 40.407,24.439 41.730,23.052" fill="#a2a12f" stroke="#e9e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="52.968,22.147 48.607,26.466 42.227,22.293 42.656,19.376 44.839,16.164" fill="#a1a730" stroke="#e4e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="38.606,43.290 36.006,43.685 34.309,43.228 34.681,32.994 38.796,32.300 38.883,32.362 40.389,42.219" fill="#805e25" stroke="#e9ca96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.902,53.727 15.168,57.544 7.104,48.896 10.496,44.700 18.355,46.508" fill="#754d22" stroke="#e9c096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.955,-24.029 -49.357,-32.103 -40.731,-33.736 -38.099,-25.415 -38.126,-25.348 -42.230,-22.446" fill="#66371e" stroke="#e9b396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.430,54.369 -18.523,49.361 -12.045,47.749 -9.952,48.837 -11.906,55.634 -14.556,56.412" fill="#9e982e" stroke="#e9e496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.361,13.051 45.831,11.032 45.511,10.173 45.504,9.430 55.273,2.794 58.877,5.336 59.819,9.638" fill="#907a2a" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="50.225,-24.867 40.992,-27.228 43.344,-32.391 50.115,-30.777 50.849,-28.132" fill="#8d7429" stroke="#e9d596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.033,-58.416 7.859,-51.789 -0.787,-48.420 -0.801,-48.427 -3.529,-52.005 4.446,-60.470" fill="#64331d" stroke="#e9b096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-5.560,58.964 -11.001,55.894 -9.048,49.097 -6.646,48.643 -1.954,50.204" fill="#a2a931" stroke="#e4e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="58.211,13.981 58.316,15.206 54.065,21.291 53.538,21.373 45.408,15.390 45.681,11.962" fill="#a3a430" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="21.398,53.407 19.852,46.187 24.093,42.108 25.202,41.997 31.108,44.910 26.694,54.617" fill="#64331d" stroke="#e9b096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-59.069,-18.294 -56.835,-20.147 -52.938,-20.851 -48.643,-10.836 -49.793,-9.537 -51.333,-9.021 -59.691,-17.116" fill="#6b3c1f" stroke="#e9b596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-31.530,51.754 -32.662,49.383 -32.675,49.163 -22.350,45.004 -20.514,48.644 -29.081,53.974" fill="#774e23" stroke="#e9c096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.764,-0.550 -54.908,-2.865 -51.104,2.646 -51.626,5.278 -58.175,5.472 -59.963,2.984" fill="#9a8d2d" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-59.640,12.654 -58.147,6.444 -51.597,6.250 -50.490,8.186 -50.748,13.825" fill="#a4a831" stroke="#e6e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.258,41.853 41.127,42.106 39.621,32.249 45.238,32.860 46.230,39.967" fill="#856427" stroke="#e9cc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="3.494,62.366 3.109,62.531 -4.415,59.876 -4.618,59.352 -1.012,50.592 1.099,50.054 3.375,50.358" fill="#a4ab32" stroke="#e4e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-54.609,33.554 -47.061,23.762 -43.344,25.132 -42.818,25.669 -43.007,34.452 -48.176,37.493" fill="#8c7029" stroke="#e9d196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.486,62.356 4.367,50.348 6.285,49.659 14.349,58.307 13.601,60.664 9.701,63.031" fill="#a19a2f" stroke="#e9e496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.349,-57.862 -14.415,-55.377 -18.268,-49.776 -21.190,-49.515 -23.253,-55.210 -21.311,-57.999" fill="#947f2b" stroke="#e9d996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.643,-58.617 17.242,-61.142 20.795,-52.716 15.631,-48.879 12.984,-48.933 9.469,-51.990" fill="#7a5023" stroke="#e9c196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.730,-54.588 -24.195,-54.869 -22.131,-49.174 -23.584,-47.513 -29.531,-46.125" fill="#a6a630" stroke="#e9e896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.208,-15.670 -60.187,-16.604 -51.828,-8.510 -54.713,-5.712 -63.048,-11.514 -63.045,-11.798" fill="#704221" stroke="#e9b896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="63.079,-15.348 61.359,-11.282 56.388,-8.964 49.502,-16.224 49.617,-16.960 51.384,-19.611" fill="#66341e" stroke="#e9b096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-28.307,55.450 -28.697,54.591 -20.130,49.260 -19.635,49.603 -18.543,54.611 -25.015,58.283" fill="#7e5624" stroke="#e9c496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="0.294,-64.897 2.635,-63.319 3.841,-61.040 -4.134,-52.575 -6.395,-53.626 -6.671,-53.974 -2.422,-65.432" fill="#68361e" stroke="#e9b196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-47.341,38.912 -42.172,35.871 -35.721,38.890 -35.165,40.352 -35.882,45.602 -47.190,45.081" fill="#724321" stroke="#e9b996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-60.760,22.318 -60.177,13.989 -59.780,13.718 -50.888,14.888 -50.416,15.615 -49.784,20.029" fill="#a5af33" stroke="#e2e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="51.480,-34.595 50.325,-31.659 43.554,-33.274 42.402,-36.198 46.767,-39.676 50.393,-38.688" fill="#8e7229" stroke="#e9d296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.994,-46.833 46.126,-40.481 41.761,-37.003 39.618,-37.872 36.670,-43.208 36.688,-43.554" fill="#a39c2f" stroke="#e9e496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-55.490,32.876 -56.009,32.872 -59.698,29.772 -60.621,23.446 -60.545,23.348 -49.569,21.059 -47.941,23.083" fill="#a7ad32" stroke="#e5e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-63.457,-10.928 -55.121,-5.126 -55.523,-3.890 -59.380,-1.576 -65.444,-6.413" fill="#774a23" stroke="#e9bd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="53.582,40.147 47.096,39.846 46.104,32.739 48.731,30.686 53.052,33.437" fill="#876527" stroke="#e9cb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-3.088,-65.679 -3.085,-65.677 -7.333,-54.220 -10.900,-56.147 -8.760,-66.530" fill="#6d3b20" stroke="#e9b396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.143,28.499 59.146,28.712 53.619,32.546 49.298,29.795 49.425,27.293 53.787,22.973 54.314,22.891" fill="#9e912e" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-50.387,-45.022 -39.422,-39.292 -39.457,-38.302 -40.999,-35.150 -49.625,-33.517 -53.105,-36.645 -51.482,-44.220" fill="#a0952e" stroke="#e9e196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.400,-52.964 -32.075,-54.641 -31.841,-54.430 -30.642,-45.967 -30.697,-45.898 -36.145,-43.488" fill="#a7af33" stroke="#e3e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.886,58.922 30.570,58.238 27.677,55.064 32.091,45.356 33.794,45.141 35.491,45.598 36.148,58.830" fill="#703f21" stroke="#e9b696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="67.953,-1.840 59.974,3.781 56.371,1.238 54.903,-3.030 57.124,-7.384 62.096,-9.702" fill="#90732a" stroke="#e9d296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.132,-23.677 64.734,-22.587 65.025,-19.616 64.173,-16.934 63.356,-16.108 51.661,-20.371 52.046,-23.047" fill="#6b381f" stroke="#e9b196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-9.549,66.214 -10.060,66.394 -15.266,62.189 -14.197,57.638 -11.546,56.860 -6.105,59.930 -5.901,60.454" fill="#99852c" stroke="#e9da96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.889,64.928 -24.598,59.018 -18.126,55.347 -15.252,57.390 -16.321,61.941" fill="#815926" stroke="#e9c496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.506,-69.041 -9.457,-66.674 -11.596,-56.291 -13.604,-56.336 -16.537,-58.821" fill="#734221" stroke="#e9b796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.058,-33.425 -54.280,-35.338 -50.800,-32.210 -51.398,-24.136 -53.282,-22.761 -57.180,-22.057" fill="#845e26" stroke="#e9c796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.619,-55.777 43.553,-47.968 43.516,-47.898 36.210,-44.619 31.900,-49.848" fill="#aaad32" stroke="#e6e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.867,-46.017 -48.372,-49.809 -41.827,-52.710 -41.419,-52.506 -37.164,-43.031 -38.902,-40.287" fill="#a7b334" stroke="#e1e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-71.932,4.916 -60.969,3.706 -59.182,6.195 -60.675,12.405 -61.073,12.676 -67.174,11.031" fill="#94792b" stroke="#e9d496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="63.271,-30.010 64.095,-24.387 52.009,-23.758 51.804,-24.565 52.429,-27.830" fill="#6d3920" stroke="#e9b196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.383,1.495 -65.957,-5.771 -59.892,-0.933 -61.091,2.601 -72.054,3.810" fill="#7c4f24" stroke="#e9be96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.785,-63.895 26.413,-52.754 22.094,-53.264 18.542,-61.690 19.963,-64.749 27.491,-65.680" fill="#abaf33" stroke="#e6e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.100,55.948 42.531,57.626 39.561,58.846 36.948,58.790 36.290,45.558 38.891,45.163" fill="#734121" stroke="#e9b696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.688,21.728 60.057,27.712 55.229,22.104 59.480,16.019" fill="#aaa731" stroke="#e9e796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-48.056,54.019 -48.819,48.785 -47.238,46.122 -35.930,46.643 -34.787,49.282 -34.775,49.502 -47.120,55.312" fill="#7f5425" stroke="#e9c196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.154,-35.254 63.130,-30.711 52.288,-28.532 51.553,-31.176 52.707,-34.112" fill="#703d21" stroke="#e9b396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.950,-68.220 -22.297,-58.685 -24.239,-55.896 -30.774,-55.615 -31.008,-55.825 -31.261,-61.718" fill="#8e6e29" stroke="#e9ce96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="23.337,68.405 15.529,61.276 16.276,58.919 21.010,55.102 26.307,56.312 29.200,59.486" fill="#a49a30" stroke="#e9e196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-24.076,-81.230 -14.277,-69.270 -17.308,-59.050 -21.270,-59.186 -25.923,-68.721" fill="#784723" stroke="#e9ba96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="49.981,53.170 44.753,55.633 39.543,44.848 41.325,43.777 43.456,43.524" fill="#713d21" stroke="#e9b396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="30.885,-63.562 34.069,-63.388 39.058,-57.316 38.898,-56.715 31.180,-50.786 28.827,-51.358 27.512,-52.421" fill="#a9b635" stroke="#e1e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-0.843,71.776 -8.501,66.878 -4.853,61.118 2.671,63.774" fill="#a59b30" stroke="#e9e196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="65.088,-54.654 50.699,-39.810 47.073,-40.799 44.941,-47.151 44.978,-47.221" fill="#896328" stroke="#e9c996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="14.245,-72.468 18.272,-65.536 16.850,-62.476 8.251,-59.951 5.664,-62.005 4.458,-64.284" fill="#99812c" stroke="#e9d696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="65.827,51.461 50.583,52.762 44.059,43.116 47.032,41.230 53.518,41.531" fill="#713c21" stroke="#e9b296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="75.255,52.348 67.015,49.988 54.706,40.059 54.176,33.348 59.703,29.514 73.193,46.559" fill="#845a26" stroke="#e9c396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-42.268,62.558 -46.386,57.487 -46.778,56.039 -34.433,50.229 -33.301,52.599" fill="#7f5225" stroke="#e9bf96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="76.464,-56.461 64.076,-36.042 52.629,-34.900 51.542,-38.993 65.932,-53.836 66.241,-54.032" fill="#754222" stroke="#e9b696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.250,21.099 -67.504,12.258 -61.403,13.903 -61.986,22.232 -62.062,22.330" fill="#a08e2e" stroke="#e9dc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.482,10.239 61.720,9.222 60.777,4.920 68.755,-0.700 70.778,-0.406 74.058,6.336" fill="#aab836" stroke="#e0e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.370,11.420 73.024,13.839 68.129,20.783 67.462,20.749 60.255,15.041 60.150,13.815 61.608,10.402" fill="#adb434" stroke="#e4e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-68.702,-31.405 -63.855,-33.554 -62.024,-33.095 -58.146,-21.727 -60.380,-19.874 -70.386,-29.210" fill="#92712a" stroke="#e9cf96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.126,70.050 -41.715,63.056 -32.748,53.097 -30.299,55.318 -29.909,56.177" fill="#794723" stroke="#e9b896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-31.351,93.868 -22.437,65.922 -15.869,62.936 -10.663,67.141 -28.898,94.883" fill="#805225" stroke="#e9bf96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-61.469,44.510 -56.024,35.012 -55.504,35.016 -49.071,38.955 -48.919,45.124 -50.500,47.787 -61.191,45.192" fill="#aeb434" stroke="#e5e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-35.421,92.871 -38.505,70.462 -29.288,56.590 -25.995,59.422 -24.286,65.332 -33.200,93.278" fill="#744022" stroke="#e9b496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.977,-61.967 -32.556,-61.663 -32.303,-55.770 -40.628,-54.093 -41.035,-54.296" fill="#9b822d" stroke="#e9d696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="10.657,74.909 8.997,75.998 0.462,72.915 0.358,72.304 3.871,64.301 4.256,64.135 9.471,64.810" fill="#b0b033" stroke="#e9e896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="77.145,-8.344 71.061,-2.354 69.039,-2.649 63.182,-10.511 64.902,-14.578 65.719,-15.403 77.220,-8.900" fill="#aca632" stroke="#e9e596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.942,-56.085 64.632,-55.888 44.522,-48.456 40.588,-56.265 40.748,-56.866 47.505,-59.717" fill="#95762b" stroke="#e9d096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.880,74.765 10.693,64.666 14.594,62.299 22.402,69.429 22.495,70.084 17.172,75.338" fill="#acbc36" stroke="#dfe996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-75.667,-22.029 -71.379,-28.346 -70.941,-28.615 -60.935,-19.279 -61.557,-18.101 -62.578,-17.167" fill="#8f6a2a" stroke="#e9cb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-82.549,-16.278 -75.934,-21.309 -62.846,-16.447 -64.682,-12.575" fill="#845726" stroke="#e9c196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-100.670,-6.864 -82.705,-15.525 -64.838,-11.822 -64.842,-11.538 -66.830,-7.023 -77.256,0.243 -99.818,-1.302" fill="#7b4824" stroke="#e9b896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.238,45.731 60.748,28.687 60.745,28.474 67.375,22.490 68.042,22.523 72.279,27.749" fill="#906c2a" stroke="#e9cb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="10.145,-78.051 13.569,-73.902 13.596,-73.244 3.809,-65.060 1.468,-66.638 6.530,-79.272 6.882,-79.298" fill="#a49330" stroke="#e9dd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.436,33.216 -71.685,32.214 -70.909,22.647 -70.453,22.456 -62.266,23.686 -61.342,30.012" fill="#aba132" stroke="#e9e296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-62.895,-47.579 -53.126,-44.572 -54.749,-36.998 -61.527,-35.084 -63.358,-35.542 -64.439,-46.362" fill="#adbe37" stroke="#dee996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-19.384,99.511 -22.409,98.835 -27.772,95.623 -9.538,67.880 -9.027,67.701 -1.369,72.599 -1.264,73.210" fill="#8c6329" stroke="#e9c696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-70.010,34.425 -60.916,31.222 -57.228,34.322 -62.672,43.820 -68.964,40.627" fill="#b0ba36" stroke="#e2e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.865,74.201 24.545,69.793 24.452,69.138 30.315,60.219 35.631,60.903 33.488,73.952" fill="#b0ba36" stroke="#e2e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-101.449,11.349 -101.672,9.210 -100.045,2.007 -77.483,3.551 -73.154,5.866 -68.395,11.982 -71.140,20.823 -71.595,21.014" fill="#885b27" stroke="#e9c296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.189,-82.073 5.760,-79.581 0.697,-66.947 -2.019,-67.482 -2.021,-67.483" fill="#a0882e" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-24.531,-97.618 -2.986,-82.064 -2.818,-67.474 -8.491,-68.325 -12.540,-70.693 -22.340,-82.653 -26.097,-96.612" fill="#926d2a" stroke="#e9cb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.951,-58.952 -53.478,-60.200 -43.572,-62.033 -43.395,-61.793 -42.453,-54.122 -48.998,-51.222" fill="#a49130" stroke="#e9db96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.126,-95.527 -26.368,-81.569 -28.215,-69.059 -32.527,-62.558 -41.948,-62.862 -42.126,-63.102 -41.523,-94.014 -40.374,-94.539" fill="#855527" stroke="#e9be96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="80.973,-18.667 80.615,-13.936 77.719,-9.782 66.217,-16.285 67.069,-18.967" fill="#b4b534" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="27.985,-76.728 27.287,-67.330 19.759,-66.400 15.732,-73.332 15.705,-73.990 25.504,-78.507" fill="#afc138" stroke="#dde996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-55.078,-58.230 -50.125,-50.500 -51.620,-46.707 -52.715,-45.906 -62.485,-48.913 -61.535,-55.188" fill="#b5b535" stroke="#e9e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.224,-70.639 42.393,-70.580 46.929,-61.082 40.172,-58.231 35.182,-64.303" fill="#9e822e" stroke="#e9d496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="94.707,-42.909 85.888,-31.064 66.498,-23.561 65.896,-24.651 65.072,-30.274 66.096,-34.816 78.485,-55.235 82.010,-57.268 88.525,-52.254" fill="#a1882f" stroke="#e9d796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-63.841,54.278 -61.504,46.481 -50.813,49.076 -50.051,54.310 -62.738,56.634" fill="#afc339" stroke="#dde996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="86.187,-30.291 80.991,-19.518 67.088,-19.818 66.797,-22.789" fill="#b0a933" stroke="#e9e496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="81.385,18.721 81.668,19.785 73.468,26.785 69.231,21.559 74.125,14.615" fill="#98742c" stroke="#e9cd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.060,74.373 38.604,75.558 34.495,74.118 36.639,61.069 36.901,60.977 39.514,61.033" fill="#b2c138" stroke="#dfe996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.215,4.274 75.547,5.612 72.267,-1.130 78.350,-7.120 85.564,0.481" fill="#b3bf38" stroke="#e1e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.391,-76.624 35.058,-77.575 40.213,-71.603 34.172,-65.266 30.988,-65.441 28.693,-67.225" fill="#aea233" stroke="#e9e196" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.890,100.488 -18.087,100.405 0.033,74.103 8.567,77.186 6.519,84.838" fill="#92692b" stroke="#e9c796" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.676,79.665 58.583,75.693 43.962,58.964 45.531,57.285 50.759,54.822 66.003,53.521 74.243,55.882 80.343,61.095 77.835,68.930" fill="#b0a633" stroke="#e9e296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.934,76.260 44.889,74.091 40.343,60.751 43.313,59.531" fill="#b5be37" stroke="#e3e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-73.185,-46.534 -65.760,-46.230 -64.678,-35.410 -69.525,-33.261 -75.781,-44.243" fill="#b1c63a" stroke="#dce996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-102.051,14.142 -101.946,12.885 -72.093,22.551 -72.870,32.118 -80.355,32.098" fill="#8e6029" stroke="#e9c396" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="84.433,5.688 86.853,10.335 82.102,17.454 74.843,13.347 74.189,10.928 75.765,7.026" fill="#a68e30" stroke="#e9d896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-59.746,66.946 -62.801,61.393 -62.552,57.653 -49.864,55.328 -48.928,56.621 -48.536,58.070" fill="#b0c73d" stroke="#dbe996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="21.411,84.141 18.336,76.518 23.659,71.264 30.980,75.672 26.311,84.447" fill="#baba36" stroke="#e9e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-56.784,-90.787 -43.173,-94.047 -43.776,-63.134 -53.682,-61.301 -60.817,-87.973" fill="#8b5928" stroke="#e9bf96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-56.407,90.855 -59.185,67.654 -47.975,58.778 -43.857,63.849 -41.268,70.843 -38.184,93.252 -41.993,93.734" fill="#b2c63c" stroke="#dce996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.060,84.686 16.678,88.112 7.985,85.230 10.033,77.578 11.694,76.490 16.985,77.062" fill="#9f7c2e" stroke="#e9d096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.394,-82.611 77.484,-69.551 79.070,-62.368 75.544,-60.334 65.321,-57.906 47.884,-61.538 43.349,-71.036 66.604,-84.658" fill="#885428" stroke="#e9bc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="23.531,-86.399 24.944,-79.720 15.146,-75.203 11.722,-79.353 19.941,-88.664" fill="#b6c439" stroke="#e1e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="90.774,52.706 84.242,56.534 78.141,51.320 76.079,45.531 74.119,27.549 82.319,20.548 101.283,38.878 100.667,40.849" fill="#875127" stroke="#e9ba96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.037,46.593 -69.718,42.113 -63.426,45.306 -63.149,45.988 -65.486,53.785 -74.812,52.180" fill="#b1a333" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-89.507,-41.081 -76.697,-43.721 -70.441,-32.739 -72.125,-30.545 -72.564,-30.276" fill="#b1c73f" stroke="#dbe996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.897,45.380 -80.879,43.362 -80.359,33.677 -72.874,33.696 -71.624,34.698 -70.579,40.900" fill="#986e2c" stroke="#e9c896" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-101.925,-40.640 -90.008,-40.295 -73.065,-29.490 -77.352,-23.173 -83.967,-18.142 -101.932,-9.481 -102.854,-11.245 -105.261,-26.731" fill="#afc842" stroke="#d9e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="92.201,-3.235 86.544,-0.449 79.330,-8.050 79.405,-8.606 82.301,-12.761 93.566,-8.005" fill="#bdbe37" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-68.715,-60.253 -63.196,-55.439 -64.146,-49.165 -65.690,-47.947 -73.114,-48.252 -74.413,-57.192" fill="#a88d31" stroke="#e9d696" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-63.550,-87.081 -62.400,-87.550 -55.264,-60.878 -55.737,-59.630 -62.194,-56.588 -67.713,-61.401" fill="#93632b" stroke="#e9c296" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="12.726,-101.932 19.133,-89.377 10.914,-80.065 7.652,-81.312" fill="#b8c63c" stroke="#e0e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="9.520,-111.008 11.793,-102.162 6.718,-81.542 6.366,-81.516 -1.582,-84.008 -23.128,-99.562 -21.011,-101.920 -7.044,-109.715 8.533,-111.344" fill="#b2c842" stroke="#dbe996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="65.746,-86.122 42.491,-72.500 41.322,-72.560 36.166,-78.531 44.602,-94.858 49.217,-99.490 57.748,-95.532" fill="#905b2a" stroke="#e9be96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.447,95.563 27.521,85.091 32.190,76.316 33.812,76.067 37.921,77.507" fill="#b5a634" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.213,-95.576 34.777,-79.249 29.110,-78.298 26.629,-80.077 25.216,-86.756" fill="#a0792f" stroke="#e9cc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="109.725,-23.362 93.994,-9.020 82.730,-13.776 83.088,-18.507 88.284,-29.280 97.103,-41.125 100.533,-38.717 109.426,-25.381" fill="#c0c138" stroke="#e8e996" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="107.872,15.820 110.562,17.317 109.725,25.758 102.502,37.617 83.539,19.287 83.255,18.224 88.007,11.105" fill="#8e5729" stroke="#e9bc96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-83.656,65.688 -75.048,53.554 -65.722,55.159 -64.619,57.515 -64.868,61.255" fill="#ab8e32" stroke="#e9d596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.586,111.568 9.375,113.321 -6.292,109.722 -16.838,102.128 7.571,86.478 16.264,89.361" fill="#8f5629" stroke="#e9bb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="108.243,14.256 88.378,9.541 85.958,4.894 87.307,1.100 92.964,-1.685" fill="#9a6b2d" stroke="#e9c596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.503,76.414 57.548,78.583 68.642,82.555 68.190,84.325 58.612,97.594 44.461,104.984 41.531,105.023 37.573,95.655 39.047,77.599" fill="#b8a936" stroke="#e9df96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-103.302,15.654 -81.606,33.610 -82.126,43.295 -102.704,48.891 -105.776,44.179 -107.350,28.086" fill="#90582a" stroke="#e9bb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.126,96.688 39.084,106.057 27.044,111.882 18.240,111.544 17.919,89.337 21.301,85.910 26.201,86.217" fill="#96612c" stroke="#e9c096" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-94.785,-64.329 -75.819,-56.987 -74.520,-48.047 -77.116,-45.756 -89.927,-43.116 -101.844,-43.461 -102.077,-44.773 -98.172,-60.587" fill="#a37a2f" stroke="#e9cb96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-89.065,71.301 -83.390,66.817 -64.602,62.384 -61.547,67.937 -58.769,91.138 -59.574,91.231 -75.743,86.870 -87.564,75.840" fill="#af9133" stroke="#e9d596" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-102.257,50.536 -81.679,44.940 -77.697,46.957 -76.471,52.544 -85.080,64.678 -90.755,69.163 -99.244,60.879" fill="#955d2b" stroke="#e9bd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.721,-107.050 47.190,-101.510 42.575,-96.878 24.577,-88.058 20.987,-90.323 14.580,-102.878 12.307,-111.724 27.768,-113.502" fill="#9d6c2e" stroke="#e9c496" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-75.905,-85.884 -65.323,-87.368 -69.486,-61.688 -75.184,-58.627 -94.151,-65.969 -89.423,-76.476" fill="#965d2b" stroke="#e9bd96" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="111.129,-21.822 117.923,-8.627 116.683,7.776 112.003,14.727 109.313,13.230 94.034,-2.710 95.398,-7.480" fill="#9a642d" stroke="#e9bf96" stroke-opacity="0.53" stroke-width="0.65"/><g fill="none" stroke="#d1e7ed" stroke-width="0.55" opacity="0.22"><polygon points="2.321,2.925 -5.714,-0.913 5.829,-3.846" /><polygon points="-5.714,-0.913 2.321,2.925 -2.028,8.081" /><polygon points="5.829,-3.846 -5.714,-0.913 -4.866,-8.999" /><polygon points="2.321,2.925 5.829,-3.846 11.988,4.152" /><polygon points="-5.714,-0.913 -2.028,8.081 -14.111,6.103" /><polygon points="2.321,2.925 11.988,4.152 12.622,10.283" /><polygon points="-5.714,-0.913 -14.111,6.103 -16.861,0.980" /><polygon points="-4.866,-8.999 7.265,-16.225 5.829,-3.846" /><polygon points="-14.111,6.103 -2.028,8.081 -10.160,14.977" /><polygon points="7.265,-16.225 -4.866,-8.999 -2.444,-16.674" /><polygon points="5.829,-3.846 7.265,-16.225 13.111,-13.490" /><polygon points="-2.028,8.081 12.622,10.283 6.109,18.394" /><polygon points="-2.028,8.081 -0.641,21.731 -10.160,14.977" /><polygon points="12.622,10.283 -2.028,8.081 2.321,2.925" /><polygon points="11.988,4.152 5.829,-3.846 18.803,-4.464" /><polygon points="18.803,-4.464 5.829,-3.846 13.111,-13.490" /><polygon points="-4.866,-8.999 -5.714,-0.913 -17.370,-9.682" /><polygon points="-17.370,-9.682 -5.714,-0.913 -16.861,0.980" /><polygon points="-0.641,21.731 -2.028,8.081 6.109,18.394" /><polygon points="-10.160,14.977 -0.641,21.731 -8.160,20.449" /><polygon points="-2.444,-16.674 -4.866,-8.999 -7.904,-20.875" /><polygon points="-4.866,-8.999 -16.342,-18.931 -7.904,-20.875" /><polygon points="18.803,-4.464 13.111,-13.490 21.003,-10.133" /><polygon points="12.622,10.283 11.988,4.152 21.765,10.979" /><polygon points="11.988,4.152 27.598,3.245 21.765,10.979" /><polygon points="-16.342,-18.931 -4.866,-8.999 -17.370,-9.682" /><polygon points="-17.370,-9.682 -16.861,0.980 -24.502,-7.368" /><polygon points="-24.502,-7.368 -16.861,0.980 -26.546,-2.014" /><polygon points="21.003,-10.133 13.111,-13.490 20.941,-16.452" /><polygon points="-16.861,0.980 -14.111,6.103 -26.730,7.526" /><polygon points="-26.546,-2.014 -16.861,0.980 -26.730,7.526" /><polygon points="6.109,18.394 12.622,10.283 14.272,23.970" /><polygon points="12.622,10.283 19.516,20.804 14.272,23.970" /><polygon points="27.598,3.245 11.988,4.152 18.803,-4.464" /><polygon points="19.516,20.804 12.622,10.283 21.765,10.979" /><polygon points="-8.160,20.449 -0.641,21.731 -4.673,28.368" /><polygon points="-14.111,6.103 -10.160,14.977 -23.816,17.168" /><polygon points="-26.730,7.526 -14.111,6.103 -23.816,17.168" /><polygon points="7.265,-16.225 -2.444,-16.674 5.963,-28.742" /><polygon points="-2.444,-16.674 -0.373,-31.551 5.963,-28.742" /><polygon points="19.516,20.804 21.765,10.979 25.005,17.141" /><polygon points="-0.373,-31.551 -2.444,-16.674 -7.904,-20.875" /><polygon points="-0.373,-31.551 -7.904,-20.875 -6.929,-29.829" /><polygon points="13.111,-13.490 7.265,-16.225 16.660,-26.887" /><polygon points="20.941,-16.452 13.111,-13.490 16.660,-26.887" /><polygon points="16.660,-26.887 7.265,-16.225 5.963,-28.742" /><polygon points="-26.546,-2.014 -26.730,7.526 -32.108,4.547" /><polygon points="-7.904,-20.875 -16.342,-18.931 -17.771,-27.220" /><polygon points="-6.929,-29.829 -7.904,-20.875 -17.771,-27.220" /><polygon points="-8.160,20.449 -23.816,17.168 -10.160,14.977" /><polygon points="-23.816,17.168 -8.160,20.449 -16.247,28.815" /><polygon points="-8.160,20.449 -9.831,32.124 -16.247,28.815" /><polygon points="-9.831,32.124 -8.160,20.449 -4.673,28.368" /><polygon points="-23.816,17.168 -16.247,28.815 -23.062,25.781" /><polygon points="21.003,-10.133 20.941,-16.452 29.510,-17.377" /><polygon points="-0.641,21.731 6.109,18.394 6.487,34.302" /><polygon points="-4.673,28.368 -0.641,21.731 6.487,34.302" /><polygon points="6.487,34.302 6.109,18.394 14.272,23.970" /><polygon points="-6.929,-29.829 -17.771,-27.220 -15.329,-32.173" /><polygon points="-26.546,-2.014 -32.108,4.547 -36.587,0.254" /><polygon points="20.941,-16.452 16.660,-26.887 23.171,-28.162" /><polygon points="20.941,-16.452 28.214,-26.209 29.510,-17.377" /><polygon points="-16.342,-18.931 -24.502,-7.368 -29.390,-21.986" /><polygon points="-17.771,-27.220 -16.342,-18.931 -29.390,-21.986" /><polygon points="-24.502,-7.368 -16.342,-18.931 -17.370,-9.682" /><polygon points="27.598,3.245 21.003,-10.133 36.368,-3.626" /><polygon points="21.003,-10.133 27.598,3.245 18.803,-4.464" /><polygon points="21.003,-10.133 34.934,-11.413 36.368,-3.626" /><polygon points="34.934,-11.413 21.003,-10.133 29.510,-17.377" /><polygon points="21.765,10.979 27.598,3.245 35.742,9.156" /><polygon points="25.005,17.141 21.765,10.979 35.742,9.156" /><polygon points="35.742,9.156 27.598,3.245 36.368,-3.626" /><polygon points="-24.502,-7.368 -26.546,-2.014 -36.419,-7.742" /><polygon points="-24.502,-7.368 -36.743,-16.667 -29.390,-21.986" /><polygon points="-36.419,-7.742 -26.546,-2.014 -36.587,0.254" /><polygon points="19.516,20.804 25.005,17.141 27.908,25.212" /><polygon points="28.214,-26.209 20.941,-16.452 23.171,-28.162" /><polygon points="-6.929,-29.829 -15.329,-32.173 -12.044,-37.485" /><polygon points="25.005,17.141 35.742,9.156 36.980,14.965" /><polygon points="25.005,17.141 35.720,20.370 27.908,25.212" /><polygon points="-36.743,-16.667 -24.502,-7.368 -36.419,-7.742" /><polygon points="-26.730,7.526 -23.816,17.168 -35.876,19.174" /><polygon points="-32.108,4.547 -26.730,7.526 -35.876,19.174" /><polygon points="-35.876,19.174 -23.816,17.168 -23.062,25.781" /><polygon points="-16.247,28.815 -9.831,32.124 -17.031,37.064" /><polygon points="35.720,20.370 25.005,17.141 36.980,14.965" /><polygon points="-4.673,28.368 6.487,34.302 2.196,41.267" /><polygon points="-4.673,28.368 -3.455,42.775 -9.831,32.124" /><polygon points="-0.373,-31.551 -6.929,-29.829 -3.879,-40.929" /><polygon points="-3.879,-40.929 -6.929,-29.829 -12.044,-37.485" /><polygon points="14.272,23.970 19.516,20.804 24.088,33.761" /><polygon points="14.272,23.970 16.728,39.204 6.487,34.302" /><polygon points="24.088,33.761 19.516,20.804 27.908,25.212" /><polygon points="29.510,-17.377 28.214,-26.209 34.308,-24.226" /><polygon points="16.728,39.204 14.272,23.970 24.088,33.761" /><polygon points="-3.455,42.775 -4.673,28.368 2.196,41.267" /><polygon points="-9.831,32.124 -9.259,42.533 -17.031,37.064" /><polygon points="16.660,-26.887 5.963,-28.742 14.198,-40.353" /><polygon points="23.171,-28.162 16.660,-26.887 14.198,-40.353" /><polygon points="-9.259,42.533 -9.831,32.124 -3.455,42.775" /><polygon points="-36.419,-7.742 -36.587,0.254 -43.482,-5.543" /><polygon points="27.908,25.212 35.720,20.370 35.376,27.006" /><polygon points="34.934,-11.413 29.510,-17.377 41.288,-17.380" /><polygon points="41.288,-17.380 29.510,-17.377 34.308,-24.226" /><polygon points="-32.108,4.547 -35.876,19.174 -42.315,15.788" /><polygon points="-32.108,4.547 -42.315,15.788 -44.487,10.105" /><polygon points="-36.587,0.254 -32.108,4.547 -44.487,10.105" /><polygon points="-17.771,-27.220 -29.390,-21.986 -32.545,-31.940" /><polygon points="-36.587,0.254 -44.487,10.105 -45.593,4.096" /><polygon points="-43.482,-5.543 -36.587,0.254 -45.593,4.096" /><polygon points="5.963,-28.742 -0.373,-31.551 5.536,-45.178" /><polygon points="14.198,-40.353 5.963,-28.742 5.536,-45.178" /><polygon points="5.536,-45.178 -0.373,-31.551 -3.879,-40.929" /><polygon points="-23.062,25.781 -17.031,37.064 -27.654,36.593" /><polygon points="-17.031,37.064 -23.062,25.781 -16.247,28.815" /><polygon points="-17.031,37.064 -9.259,42.533 -16.005,44.108" /><polygon points="-3.879,-40.929 -12.044,-37.485 -8.243,-46.046" /><polygon points="-35.876,19.174 -23.062,25.781 -36.484,31.184" /><polygon points="-36.484,31.184 -23.062,25.781 -27.654,36.593" /><polygon points="-12.044,-37.485 -15.329,-32.173 -18.366,-44.234" /><polygon points="-8.243,-46.046 -12.044,-37.485 -18.366,-44.234" /><polygon points="-15.329,-32.173 -29.810,-38.327 -24.189,-41.542" /><polygon points="-18.366,-44.234 -15.329,-32.173 -24.189,-41.542" /><polygon points="-29.810,-38.327 -15.329,-32.173 -17.771,-27.220" /><polygon points="35.742,9.156 36.368,-3.626 48.025,1.652" /><polygon points="24.088,33.761 27.908,25.212 31.227,36.896" /><polygon points="31.227,36.896 27.908,25.212 35.376,27.006" /><polygon points="-29.810,-38.327 -17.771,-27.220 -32.545,-31.940" /><polygon points="-43.482,-5.543 -45.593,4.096 -49.220,-2.174" /><polygon points="41.288,-17.380 34.308,-24.226 44.012,-22.974" /><polygon points="-36.743,-16.667 -43.482,-5.543 -47.103,-16.832" /><polygon points="-43.482,-5.543 -36.743,-16.667 -36.419,-7.742" /><polygon points="34.308,-24.226 28.214,-26.209 38.252,-32.056" /><polygon points="44.012,-22.974 34.308,-24.226 38.252,-32.056" /><polygon points="23.171,-28.162 14.198,-40.353 22.699,-44.459" /><polygon points="36.368,-3.626 34.934,-11.413 49.444,-9.101" /><polygon points="48.025,1.652 36.368,-3.626 49.444,-9.101" /><polygon points="49.444,-9.101 34.934,-11.413 41.288,-17.380" /><polygon points="28.214,-26.209 23.171,-28.162 33.908,-37.168" /><polygon points="38.252,-32.056 28.214,-26.209 33.908,-37.168" /><polygon points="23.171,-28.162 29.374,-42.405 33.908,-37.168" /><polygon points="-27.654,36.593 -17.031,37.064 -27.694,43.735" /><polygon points="-27.694,43.735 -17.031,37.064 -16.005,44.108" /><polygon points="-8.243,-46.046 -18.366,-44.234 -13.246,-49.832" /><polygon points="29.374,-42.405 23.171,-28.162 22.699,-44.459" /><polygon points="35.376,27.006 35.720,20.370 43.659,27.945" /><polygon points="35.720,20.370 36.980,14.965 47.483,22.101" /><polygon points="43.659,27.945 35.720,20.370 47.483,22.101" /><polygon points="31.227,36.896 35.376,27.006 37.081,37.109" /><polygon points="37.081,37.109 35.376,27.006 43.659,27.945" /><polygon points="16.728,39.204 2.196,41.267 6.487,34.302" /><polygon points="2.196,41.267 16.728,39.204 14.050,50.853" /><polygon points="-29.390,-21.986 -36.743,-16.667 -44.802,-28.064" /><polygon points="-32.545,-31.940 -29.390,-21.986 -44.802,-28.064" /><polygon points="-44.802,-28.064 -36.743,-16.667 -47.103,-16.832" /><polygon points="-16.005,44.108 -9.259,42.533 -14.086,51.818" /><polygon points="35.742,9.156 48.025,1.652 53.015,8.998" /><polygon points="36.980,14.965 35.742,9.156 53.015,8.998" /><polygon points="36.980,14.965 51.863,16.150 47.483,22.101" /><polygon points="44.012,-22.974 38.252,-32.056 45.491,-28.759" /><polygon points="5.536,-45.178 -3.879,-40.929 2.119,-53.946" /><polygon points="2.119,-53.946 -3.879,-40.929 -8.243,-46.046" /><polygon points="-9.259,42.533 -3.455,42.775 -7.128,53.818" /><polygon points="-14.086,51.818 -9.259,42.533 -7.128,53.818" /><polygon points="51.863,16.150 36.980,14.965 53.015,8.998" /><polygon points="16.728,39.204 24.088,33.761 25.559,48.387" /><polygon points="14.050,50.853 16.728,39.204 25.559,48.387" /><polygon points="25.559,48.387 24.088,33.761 31.227,36.896" /><polygon points="-47.103,-16.832 -43.482,-5.543 -53.249,-14.196" /><polygon points="-53.249,-14.196 -43.482,-5.543 -49.220,-2.174" /><polygon points="-27.694,43.735 -16.005,44.108 -25.595,48.945" /><polygon points="-16.005,44.108 -22.645,53.686 -25.595,48.945" /><polygon points="-49.220,-2.174 -45.593,4.096 -55.467,2.138" /><polygon points="-45.593,4.096 -44.487,10.105 -55.245,9.612" /><polygon points="-55.467,2.138 -45.593,4.096 -55.245,9.612" /><polygon points="37.081,37.109 43.659,27.945 42.757,36.242" /><polygon points="-3.455,42.775 2.196,41.267 0.118,56.801" /><polygon points="-7.128,53.818 -3.455,42.775 0.118,56.801" /><polygon points="2.196,41.267 7.752,56.725 0.118,56.801" /><polygon points="-35.876,19.174 -36.484,31.184 -47.899,30.939" /><polygon points="-42.315,15.788 -35.876,19.174 -47.899,30.939" /><polygon points="7.752,56.725 2.196,41.267 14.050,50.853" /><polygon points="-13.246,-49.832 -18.366,-44.234 -19.234,-53.951" /><polygon points="14.198,-40.353 5.536,-45.178 14.504,-55.489" /><polygon points="22.699,-44.459 14.198,-40.353 14.504,-55.489" /><polygon points="14.504,-55.489 5.536,-45.178 2.119,-53.946" /><polygon points="-18.366,-44.234 -24.189,-41.542 -26.475,-51.327" /><polygon points="-19.234,-53.951 -18.366,-44.234 -26.475,-51.327" /><polygon points="-53.249,-14.196 -49.220,-2.174 -57.062,-10.259" /><polygon points="-49.220,-2.174 -60.201,-5.750 -57.062,-10.259" /><polygon points="49.444,-9.101 41.288,-17.380 55.793,-15.122" /><polygon points="55.793,-15.122 41.288,-17.380 44.012,-22.974" /><polygon points="-22.645,53.686 -16.005,44.108 -14.086,51.818" /><polygon points="2.119,-53.946 -8.243,-46.046 -2.533,-58.329" /><polygon points="-2.533,-58.329 -8.243,-46.046 -13.246,-49.832" /><polygon points="-36.484,31.184 -27.654,36.593 -41.478,41.854" /><polygon points="-47.899,30.939 -36.484,31.184 -41.478,41.854" /><polygon points="-41.478,41.854 -27.654,36.593 -27.694,43.735" /><polygon points="-44.487,10.105 -42.315,15.788 -56.322,17.794" /><polygon points="-55.245,9.612 -44.487,10.105 -56.322,17.794" /><polygon points="-42.315,15.788 -54.669,25.721 -56.322,17.794" /><polygon points="45.491,-28.759 38.252,-32.056 47.110,-35.547" /><polygon points="38.252,-32.056 33.908,-37.168 42.178,-41.738" /><polygon points="47.110,-35.547 38.252,-32.056 42.178,-41.738" /><polygon points="-54.669,25.721 -42.315,15.788 -47.899,30.939" /><polygon points="-60.201,-5.750 -49.220,-2.174 -55.467,2.138" /><polygon points="42.757,36.242 43.659,27.945 49.416,35.312" /><polygon points="-2.533,-58.329 -13.246,-49.832 -7.632,-60.219" /><polygon points="-13.246,-49.832 -12.988,-61.323 -7.632,-60.219" /><polygon points="43.659,27.945 47.483,22.101 53.780,28.458" /><polygon points="49.416,35.312 43.659,27.945 53.780,28.458" /><polygon points="-32.545,-31.940 -44.802,-28.064 -46.861,-38.941" /><polygon points="-29.810,-38.327 -32.545,-31.940 -46.861,-38.941" /><polygon points="-24.189,-41.542 -29.810,-38.327 -35.023,-50.116" /><polygon points="-26.475,-51.327 -24.189,-41.542 -35.023,-50.116" /><polygon points="-29.810,-38.327 -42.862,-46.595 -35.023,-50.116" /><polygon points="25.559,48.387 31.227,36.896 33.117,51.824" /><polygon points="33.117,51.824 31.227,36.896 37.081,37.109" /><polygon points="48.025,1.652 49.444,-9.101 61.459,-2.969" /><polygon points="53.015,8.998 48.025,1.652 61.459,-2.969" /><polygon points="61.459,-2.969 49.444,-9.101 55.793,-15.122" /><polygon points="55.793,-15.122 44.012,-22.974 57.925,-20.970" /><polygon points="44.012,-22.974 57.640,-26.435 57.925,-20.970" /><polygon points="-14.086,51.818 -7.128,53.818 -11.320,61.250" /><polygon points="-22.645,53.686 -14.086,51.818 -19.436,59.343" /><polygon points="-19.436,59.343 -14.086,51.818 -11.320,61.250" /><polygon points="-12.988,-61.323 -13.246,-49.832 -19.234,-53.951" /><polygon points="-44.802,-28.064 -47.103,-16.832 -55.901,-28.886" /><polygon points="-46.861,-38.941 -44.802,-28.064 -55.901,-28.886" /><polygon points="-55.901,-28.886 -47.103,-16.832 -53.249,-14.196" /><polygon points="29.374,-42.405 42.178,-41.738 33.908,-37.168" /><polygon points="42.178,-41.738 29.374,-42.405 38.502,-49.928" /><polygon points="-42.862,-46.595 -29.810,-38.327 -46.861,-38.941" /><polygon points="-55.467,2.138 -55.245,9.612 -63.207,7.697" /><polygon points="57.640,-26.435 44.012,-22.974 45.491,-28.759" /><polygon points="45.491,-28.759 56.555,-31.832 57.640,-26.435" /><polygon points="-60.201,-5.750 -55.467,2.138 -64.145,-0.806" /><polygon points="-64.145,-0.806 -55.467,2.138 -63.207,7.697" /><polygon points="22.699,-44.459 14.504,-55.489 24.500,-59.704" /><polygon points="33.117,51.824 37.081,37.109 39.269,51.519" /><polygon points="51.863,16.150 53.780,28.458 47.483,22.101" /><polygon points="53.780,28.458 51.863,16.150 60.814,22.403" /><polygon points="-41.478,41.854 -27.694,43.735 -41.847,49.861" /><polygon points="-41.847,49.861 -27.694,43.735 -25.595,48.945" /><polygon points="56.555,-31.832 45.491,-28.759 47.110,-35.547" /><polygon points="-19.234,-53.951 -26.475,-51.327 -26.815,-59.229" /><polygon points="14.050,50.853 25.559,48.387 22.579,61.430" /><polygon points="7.752,56.725 14.050,50.853 22.579,61.430" /><polygon points="22.579,61.430 25.559,48.387 33.117,51.824" /><polygon points="-12.988,-61.323 -19.234,-53.951 -18.919,-63.082" /><polygon points="-18.919,-63.082 -19.234,-53.951 -26.815,-59.229" /><polygon points="39.269,51.519 37.081,37.109 44.286,49.095" /><polygon points="44.286,49.095 37.081,37.109 42.757,36.242" /><polygon points="29.374,-42.405 22.699,-44.459 32.958,-57.144" /><polygon points="38.502,-49.928 29.374,-42.405 32.958,-57.144" /><polygon points="32.958,-57.144 22.699,-44.459 24.500,-59.704" /><polygon points="0.118,56.801 -11.320,61.250 -7.128,53.818" /><polygon points="-11.320,61.250 0.118,56.801 -3.255,66.357" /><polygon points="47.110,-35.547 42.178,-41.738 49.464,-44.183" /><polygon points="14.504,-55.489 2.119,-53.946 11.490,-65.751" /><polygon points="24.500,-59.704 14.504,-55.489 11.490,-65.751" /><polygon points="11.490,-65.751 2.119,-53.946 -2.533,-58.329" /><polygon points="44.286,49.095 42.757,36.242 48.921,45.960" /><polygon points="48.921,45.960 42.757,36.242 49.416,35.312" /><polygon points="49.416,35.312 53.780,28.458 58.061,34.629" /><polygon points="-41.847,49.861 -25.595,48.945 -39.216,55.450" /><polygon points="56.555,-31.832 47.110,-35.547 55.950,-37.894" /><polygon points="55.950,-37.894 47.110,-35.547 49.464,-44.183" /><polygon points="-56.322,17.794 -63.207,7.697 -55.245,9.612" /><polygon points="-63.207,7.697 -56.322,17.794 -65.751,17.134" /><polygon points="53.015,8.998 61.459,-2.969 67.633,5.795" /><polygon points="51.863,16.150 53.015,8.998 66.774,14.877" /><polygon points="60.814,22.403 51.863,16.150 66.774,14.877" /><polygon points="66.774,14.877 53.015,8.998 67.633,5.795" /><polygon points="-55.901,-28.886 -53.249,-14.196 -63.332,-26.351" /><polygon points="-39.216,55.450 -25.595,48.945 -34.964,59.278" /><polygon points="-34.964,59.278 -25.595,48.945 -22.645,53.686" /><polygon points="-19.436,59.343 -11.320,61.250 -15.958,66.991" /><polygon points="-47.899,30.939 -41.478,41.854 -54.783,42.181" /><polygon points="-54.783,42.181 -41.478,41.854 -41.847,49.861" /><polygon points="-34.964,59.278 -22.645,53.686 -30.187,62.452" /><polygon points="-30.187,62.452 -22.645,53.686 -19.436,59.343" /><polygon points="-35.023,-50.116 -26.815,-59.229 -26.475,-51.327" /><polygon points="-26.815,-59.229 -35.023,-50.116 -36.773,-58.801" /><polygon points="0.118,56.801 7.752,56.725 5.980,70.412" /><polygon points="-3.255,66.357 0.118,56.801 5.980,70.412" /><polygon points="7.752,56.725 15.387,69.306 5.980,70.412" /><polygon points="61.459,-2.969 55.793,-15.122 69.815,-9.194" /><polygon points="38.502,-49.928 49.464,-44.183 42.178,-41.738" /><polygon points="49.464,-44.183 38.502,-49.928 45.953,-53.682" /><polygon points="15.387,69.306 7.752,56.725 22.579,61.430" /><polygon points="-63.332,-26.351 -53.249,-14.196 -67.601,-21.776" /><polygon points="-67.601,-21.776 -53.249,-14.196 -57.062,-10.259" /><polygon points="-67.601,-21.776 -57.062,-10.259 -69.659,-16.235" /><polygon points="-57.062,-10.259 -70.860,-10.442 -69.659,-16.235" /><polygon points="-70.860,-10.442 -57.062,-10.259 -60.201,-5.750" /><polygon points="-70.860,-10.442 -60.201,-5.750 -64.145,-0.806" /><polygon points="53.780,28.458 60.814,22.403 66.103,28.264" /><polygon points="58.061,34.629 53.780,28.458 66.103,28.264" /><polygon points="11.490,-65.751 -2.533,-58.329 6.497,-71.722" /><polygon points="-54.669,25.721 -65.751,17.134 -56.322,17.794" /><polygon points="-65.751,17.134 -54.669,25.721 -67.320,27.568" /><polygon points="-46.861,-38.941 -55.901,-28.886 -59.504,-41.650" /><polygon points="-11.320,61.250 -3.255,66.357 -7.300,72.681" /><polygon points="-15.958,66.991 -11.320,61.250 -7.300,72.681" /><polygon points="-54.669,25.721 -54.783,42.181 -64.042,36.874" /><polygon points="-67.320,27.568 -54.669,25.721 -64.042,36.874" /><polygon points="-54.783,42.181 -54.669,25.721 -47.899,30.939" /><polygon points="22.579,61.430 33.117,51.824 31.156,67.068" /><polygon points="-63.207,7.697 -65.751,17.134 -72.602,15.007" /><polygon points="6.497,-71.722 -2.533,-58.329 0.570,-74.097" /><polygon points="0.570,-74.097 -2.533,-58.329 -7.632,-60.219" /><polygon points="0.570,-74.097 -7.632,-60.219 -5.560,-74.027" /><polygon points="-5.560,-74.027 -7.632,-60.219 -12.988,-61.323" /><polygon points="-42.862,-46.595 -36.773,-58.801 -35.023,-50.116" /><polygon points="-36.773,-58.801 -42.862,-46.595 -47.679,-57.462" /><polygon points="-26.815,-59.229 -36.773,-58.801 -36.550,-65.685" /><polygon points="69.815,-9.194 57.925,-20.970 73.650,-15.975" /><polygon points="57.925,-20.970 69.815,-9.194 55.793,-15.122" /><polygon points="24.500,-59.704 11.490,-65.751 22.932,-72.398" /><polygon points="-42.862,-46.595 -46.861,-38.941 -56.346,-51.909" /><polygon points="-47.679,-57.462 -42.862,-46.595 -56.346,-51.909" /><polygon points="-56.346,-51.909 -46.861,-38.941 -59.504,-41.650" /><polygon points="32.958,-57.144 45.953,-53.682 38.502,-49.928" /><polygon points="45.953,-53.682 32.958,-57.144 41.524,-64.182" /><polygon points="57.925,-20.970 57.640,-26.435 71.492,-28.465" /><polygon points="57.925,-20.970 73.791,-22.524 73.650,-15.975" /><polygon points="-54.783,42.181 -41.847,49.861 -57.189,52.095" /><polygon points="73.791,-22.524 57.925,-20.970 71.492,-28.465" /><polygon points="66.774,14.877 66.103,28.264 60.814,22.403" /><polygon points="66.103,28.264 66.774,14.877 75.248,20.850" /><polygon points="31.156,67.068 33.117,51.824 38.907,68.342" /><polygon points="38.907,68.342 33.117,51.824 39.269,51.519" /><polygon points="67.633,5.795 69.815,-9.194 79.086,0.222" /><polygon points="69.815,-9.194 67.633,5.795 61.459,-2.969" /><polygon points="32.958,-57.144 24.500,-59.704 33.749,-71.595" /><polygon points="41.524,-64.182 32.958,-57.144 33.749,-71.595" /><polygon points="33.749,-71.595 24.500,-59.704 22.932,-72.398" /><polygon points="5.980,70.412 -7.300,72.681 -3.255,66.357" /><polygon points="-7.300,72.681 5.980,70.412 2.677,79.555" /><polygon points="39.269,51.519 44.286,49.095 50.274,61.808" /><polygon points="39.269,51.519 45.287,66.167 38.907,68.342" /><polygon points="45.287,66.167 39.269,51.519 50.274,61.808" /><polygon points="-59.504,-41.650 -63.332,-26.351 -69.665,-40.634" /><polygon points="-63.332,-26.351 -59.504,-41.650 -55.901,-28.886" /><polygon points="-67.320,27.568 -72.602,15.007 -65.751,17.134" /><polygon points="-72.602,15.007 -67.320,27.568 -76.429,26.828" /><polygon points="66.774,14.877 67.633,5.795 80.765,11.098" /><polygon points="75.248,20.850 66.774,14.877 80.765,11.098" /><polygon points="80.765,11.098 67.633,5.795 79.086,0.222" /><polygon points="-57.189,52.095 -41.847,49.861 -55.754,59.929" /><polygon points="-55.754,59.929 -41.847,49.861 -39.216,55.450" /><polygon points="15.387,69.306 31.156,67.068 24.344,78.382" /><polygon points="31.156,67.068 15.387,69.306 22.579,61.430" /><polygon points="-47.679,-57.462 -36.550,-65.685 -36.773,-58.801" /><polygon points="-36.550,-65.685 -47.679,-57.462 -49.246,-65.933" /><polygon points="-55.754,59.929 -39.216,55.450 -51.440,65.377" /><polygon points="5.980,70.412 15.387,69.306 13.952,82.573" /><polygon points="2.677,79.555 5.980,70.412 13.952,82.573" /><polygon points="13.952,82.573 15.387,69.306 24.344,78.382" /><polygon points="45.953,-53.682 41.524,-64.182 48.871,-67.690" /><polygon points="22.932,-72.398 6.497,-71.722 18.628,-81.732" /><polygon points="6.497,-71.722 22.932,-72.398 11.490,-65.751" /><polygon points="66.103,28.264 75.248,20.850 80.261,26.721" /><polygon points="-64.042,36.874 -54.783,42.181 -69.840,48.303" /><polygon points="-69.840,48.303 -54.783,42.181 -57.189,52.095" /><polygon points="-69.665,-40.634 -63.332,-26.351 -76.712,-36.619" /><polygon points="-76.712,-36.619 -63.332,-26.351 -67.601,-21.776" /><polygon points="-67.320,27.568 -64.042,36.874 -76.460,38.967" /><polygon points="-76.429,26.828 -67.320,27.568 -76.460,38.967" /><polygon points="-76.460,38.967 -64.042,36.874 -69.840,48.303" /><polygon points="-76.712,-36.619 -67.601,-21.776 -80.566,-30.575" /><polygon points="79.086,0.222 69.815,-9.194 86.624,-6.932" /><polygon points="86.624,-6.932 69.815,-9.194 73.650,-15.975" /><polygon points="-56.346,-51.909 -59.504,-41.650 -69.122,-53.844" /><polygon points="-69.122,-53.844 -59.504,-41.650 -69.665,-40.634" /><polygon points="-47.679,-57.462 -56.346,-51.909 -61.419,-62.676" /><polygon points="-49.246,-65.933 -47.679,-57.462 -61.419,-62.676" /><polygon points="-61.419,-62.676 -56.346,-51.909 -69.122,-53.844" /><polygon points="18.628,-81.732 6.497,-71.722 12.418,-87.214" /><polygon points="6.497,-71.722 5.236,-88.982 12.418,-87.214" /><polygon points="5.236,-88.982 6.497,-71.722 0.570,-74.097" /><polygon points="41.524,-64.182 33.749,-71.595 42.274,-78.954" /><polygon points="48.871,-67.690 41.524,-64.182 42.274,-78.954" /><polygon points="24.344,78.382 31.156,67.068 33.651,83.333" /><polygon points="33.651,83.333 31.156,67.068 38.907,68.342" /><polygon points="33.749,-71.595 22.932,-72.398 31.588,-84.475" /><polygon points="42.274,-78.954 33.749,-71.595 31.588,-84.475" /><polygon points="31.588,-84.475 22.932,-72.398 18.628,-81.732" /><polygon points="86.624,-6.932 73.650,-15.975 89.922,-14.743" /><polygon points="75.248,20.850 80.765,11.098 89.638,17.020" /><polygon points="80.261,26.721 75.248,20.850 89.638,17.020" /><polygon points="-69.840,48.303 -57.189,52.095 -71.659,58.871" /><polygon points="-71.659,58.871 -57.189,52.095 -55.754,59.929" /><polygon points="2.677,79.555 13.952,82.573 10.769,92.174" /><polygon points="80.765,11.098 79.086,0.222 92.493,4.990" /><polygon points="89.638,17.020 80.765,11.098 92.493,4.990" /><polygon points="92.493,4.990 79.086,0.222 86.624,-6.932" /><polygon points="33.651,83.333 38.907,68.342 42.315,84.040" /><polygon points="-76.429,26.828 -76.460,38.967 -86.049,38.452" /><polygon points="13.952,82.573 24.344,78.382 23.494,91.990" /><polygon points="10.769,92.174 13.952,82.573 23.494,91.990" /><polygon points="23.494,91.990 24.344,78.382 33.651,83.333" /><polygon points="-69.122,-53.844 -69.665,-40.634 -79.938,-52.272" /><polygon points="-79.938,-52.272 -69.665,-40.634 -76.712,-36.619" /><polygon points="-71.659,58.871 -55.754,59.929 -69.611,67.552" /><polygon points="-76.460,38.967 -69.840,48.303 -82.609,51.103" /><polygon points="-86.049,38.452 -76.460,38.967 -82.609,51.103" /><polygon points="-82.609,51.103 -69.840,48.303 -71.659,58.871" /><polygon points="31.588,-84.475 18.628,-81.732 26.679,-94.492" /><polygon points="26.679,-94.492 18.628,-81.732 12.418,-87.214" /><polygon points="-61.419,-62.676 -69.122,-53.844 -75.055,-64.887" /><polygon points="-75.055,-64.887 -69.122,-53.844 -79.938,-52.272" /><polygon points="92.493,4.990 86.624,-6.932 100.722,-2.898" /><polygon points="100.722,-2.898 86.624,-6.932 89.922,-14.743" /><polygon points="-51.440,65.377 -69.611,67.552 -55.754,59.929" /><polygon points="-34.964,59.278 -51.440,65.377 -39.216,55.450" /><polygon points="-69.611,67.552 -82.609,51.103 -71.659,58.871" /><polygon points="-30.187,62.452 -51.440,65.377 -34.964,59.278" /><polygon points="-15.958,66.991 -30.187,62.452 -19.436,59.343" /><polygon points="-72.602,15.007 -64.145,-0.806 -63.207,7.697" /><polygon points="-72.602,15.007 -70.860,-10.442 -64.145,-0.806" /><polygon points="-70.860,-10.442 -80.566,-30.575 -69.659,-16.235" /><polygon points="42.315,84.040 23.494,91.990 33.651,83.333" /><polygon points="-69.659,-16.235 -80.566,-30.575 -67.601,-21.776" /><polygon points="45.287,66.167 42.315,84.040 38.907,68.342" /><polygon points="-80.566,-30.575 -79.938,-52.272 -76.712,-36.619" /><polygon points="50.274,61.808 42.315,84.040 45.287,66.167" /><polygon points="48.921,45.960 50.274,61.808 44.286,49.095" /><polygon points="58.061,34.629 48.921,45.960 49.416,35.312" /><polygon points="50.274,61.808 58.061,34.629 80.261,26.721" /><polygon points="58.061,34.629 50.274,61.808 48.921,45.960" /><polygon points="80.261,26.721 58.061,34.629 66.103,28.264" /><polygon points="-36.550,-65.685 -18.919,-63.082 -26.815,-59.229" /><polygon points="-36.550,-65.685 -5.560,-74.027 -18.919,-63.082" /><polygon points="100.722,-2.898 89.638,17.020 92.493,4.990" /><polygon points="-18.919,-63.082 -5.560,-74.027 -12.988,-61.323" /><polygon points="-5.560,-74.027 5.236,-88.982 0.570,-74.097" /><polygon points="5.236,-88.982 26.679,-94.492 12.418,-87.214" /><polygon points="71.492,-28.465 89.922,-14.743 73.791,-22.524" /><polygon points="26.679,-94.492 42.274,-78.954 31.588,-84.475" /><polygon points="56.555,-31.832 71.492,-28.465 57.640,-26.435" /><polygon points="55.950,-37.894 71.492,-28.465 56.555,-31.832" /><polygon points="48.871,-67.690 71.492,-28.465 55.950,-37.894" /><polygon points="55.950,-37.894 45.953,-53.682 48.871,-67.690" /><polygon points="45.953,-53.682 55.950,-37.894 49.464,-44.183" /><polygon points="73.791,-22.524 89.922,-14.743 73.650,-15.975" /></g><circle cx="2.321" cy="2.925" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.714" cy="-0.913" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="5.829" cy="-3.846" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.028" cy="8.081" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.866" cy="-8.999" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="11.988" cy="4.152" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.111" cy="6.103" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="12.622" cy="10.283" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.861" cy="0.980" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.444" cy="-16.674" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-10.160" cy="14.977" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="7.265" cy="-16.225" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="13.111" cy="-13.490" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="6.109" cy="18.394" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="18.803" cy="-4.464" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.370" cy="-9.682" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.641" cy="21.731" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.160" cy="20.449" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.904" cy="-20.875" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="21.003" cy="-10.133" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="21.765" cy="10.979" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.342" cy="-18.931" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.502" cy="-7.368" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.546" cy="-2.014" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="20.941" cy="-16.452" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.730" cy="7.526" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="14.272" cy="23.970" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="27.598" cy="3.245" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="19.516" cy="20.804" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.673" cy="28.368" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.816" cy="17.168" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="5.963" cy="-28.742" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="25.005" cy="17.141" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.929" cy="-29.829" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.373" cy="-31.551" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="16.660" cy="-26.887" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.108" cy="4.547" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.771" cy="-27.220" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.247" cy="28.815" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.831" cy="32.124" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.062" cy="25.781" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="29.510" cy="-17.377" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="6.487" cy="34.302" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-15.329" cy="-32.173" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.587" cy="0.254" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="23.171" cy="-28.162" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.390" cy="-21.986" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="36.368" cy="-3.626" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="34.934" cy="-11.413" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="35.742" cy="9.156" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.419" cy="-7.742" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="27.908" cy="25.212" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="28.214" cy="-26.209" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-12.044" cy="-37.485" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="36.980" cy="14.965" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.743" cy="-16.667" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-35.876" cy="19.174" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.031" cy="37.064" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="35.720" cy="20.370" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="2.196" cy="41.267" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-3.879" cy="-40.929" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="24.088" cy="33.761" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.308" cy="-24.226" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="16.728" cy="39.204" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-3.455" cy="42.775" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.198" cy="-40.353" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.259" cy="42.533" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-43.482" cy="-5.543" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="35.376" cy="27.006" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="41.288" cy="-17.380" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.315" cy="15.788" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.487" cy="10.105" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.545" cy="-31.940" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-45.593" cy="4.096" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.536" cy="-45.178" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.654" cy="36.593" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.005" cy="44.108" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.243" cy="-46.046" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.484" cy="31.184" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.366" cy="-44.234" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.189" cy="-41.542" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.025" cy="1.652" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.227" cy="36.896" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.810" cy="-38.327" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-49.220" cy="-2.174" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.012" cy="-22.974" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.103" cy="-16.832" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.252" cy="-32.056" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.699" cy="-44.459" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.444" cy="-9.101" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="33.908" cy="-37.168" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.694" cy="43.735" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-13.246" cy="-49.832" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="29.374" cy="-42.405" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="43.659" cy="27.945" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="47.483" cy="22.101" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="37.081" cy="37.109" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.050" cy="50.853" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.802" cy="-28.064" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.086" cy="51.818" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="53.015" cy="8.998" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.491" cy="-28.759" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="2.119" cy="-53.946" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.128" cy="53.818" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="51.863" cy="16.150" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="25.559" cy="48.387" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.249" cy="-14.196" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-25.595" cy="48.945" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-55.467" cy="2.138" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-55.245" cy="9.612" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.757" cy="36.242" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="0.118" cy="56.801" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.899" cy="30.939" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="7.752" cy="56.725" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-19.234" cy="-53.951" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.504" cy="-55.489" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.475" cy="-51.327" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-57.062" cy="-10.259" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.793" cy="-15.122" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-22.645" cy="53.686" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.533" cy="-58.329" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.478" cy="41.854" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-56.322" cy="17.794" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="47.110" cy="-35.547" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.178" cy="-41.738" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-54.669" cy="25.721" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-60.201" cy="-5.750" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.416" cy="35.312" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.632" cy="-60.219" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="53.780" cy="28.458" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.861" cy="-38.941" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-35.023" cy="-50.116" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="33.117" cy="51.824" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="61.459" cy="-2.969" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.925" cy="-20.970" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-11.320" cy="61.250" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-19.436" cy="59.343" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-12.988" cy="-61.323" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-55.901" cy="-28.886" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.502" cy="-49.928" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.862" cy="-46.595" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.207" cy="7.697" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="57.640" cy="-26.435" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-64.145" cy="-0.806" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="24.500" cy="-59.704" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="39.269" cy="51.519" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="60.814" cy="22.403" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-41.847" cy="49.861" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="56.555" cy="-31.832" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.815" cy="-59.229" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.579" cy="61.430" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.919" cy="-63.082" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.286" cy="49.095" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="32.958" cy="-57.144" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-3.255" cy="66.357" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.464" cy="-44.183" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.490" cy="-65.751" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.921" cy="45.960" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="58.061" cy="34.629" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.216" cy="55.450" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.950" cy="-37.894" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-65.751" cy="17.134" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.633" cy="5.795" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="66.774" cy="14.877" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.332" cy="-26.351" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.964" cy="59.278" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-15.958" cy="66.991" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-54.783" cy="42.181" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-30.187" cy="62.452" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.773" cy="-58.801" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.980" cy="70.412" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="69.815" cy="-9.194" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.953" cy="-53.682" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="15.387" cy="69.306" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.601" cy="-21.776" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-69.659" cy="-16.235" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-70.860" cy="-10.442" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="66.103" cy="28.264" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="6.497" cy="-71.722" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.320" cy="27.568" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-59.504" cy="-41.650" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.300" cy="72.681" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-64.042" cy="36.874" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.156" cy="67.068" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-72.602" cy="15.007" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="0.570" cy="-74.097" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.560" cy="-74.027" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.679" cy="-57.462" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.550" cy="-65.685" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="73.650" cy="-15.975" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.932" cy="-72.398" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-56.346" cy="-51.909" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="41.524" cy="-64.182" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="71.492" cy="-28.465" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-57.189" cy="52.095" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="73.791" cy="-22.524" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="75.248" cy="20.850" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.907" cy="68.342" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="79.086" cy="0.222" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="33.749" cy="-71.595" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="2.677" cy="79.555" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.274" cy="61.808" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.287" cy="66.167" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-69.665" cy="-40.634" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.429" cy="26.828" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="80.765" cy="11.098" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-55.754" cy="59.929" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="24.344" cy="78.382" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-49.246" cy="-65.933" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.440" cy="65.377" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.952" cy="82.573" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.871" cy="-67.690" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="18.628" cy="-81.732" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="80.261" cy="26.721" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-69.840" cy="48.303" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.712" cy="-36.619" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-76.460" cy="38.967" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-80.566" cy="-30.575" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="86.624" cy="-6.932" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-69.122" cy="-53.844" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-61.419" cy="-62.676" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="12.418" cy="-87.214" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.236" cy="-88.982" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.274" cy="-78.954" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="33.651" cy="83.333" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.588" cy="-84.475" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="89.922" cy="-14.743" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="89.638" cy="17.020" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.659" cy="58.871" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="10.769" cy="92.174" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="92.493" cy="4.990" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.315" cy="84.040" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-86.049" cy="38.452" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="23.494" cy="91.990" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-79.938" cy="-52.272" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-69.611" cy="67.552" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.609" cy="51.103" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="26.679" cy="-94.492" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-75.055" cy="-64.887" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="100.722" cy="-2.898" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle r="9" fill="#061019" stroke="#dabb83" stroke-width="0.8"/><circle r="2" fill="#ffe5a2"/></g><g transform="translate(400.000 873.000)"><circle r="108.560" fill="url(#bloom-light)"/><polygon points="5.963,4.688 -0.452,4.330 -1.017,-2.220 -0.217,-3.587 6.335,-2.435 8.067,2.811" fill="#1a2558" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.937,1.406 -10.797,0.479 -2.041,-2.132 -1.476,4.418 -3.927,7.022 -7.528,5.632" fill="#0d222b" stroke="#96cfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-7.457,-10.347 -1.613,-4.719 -1.489,-4.332 -2.290,-2.965 -11.045,-0.354 -11.946,-2.644 -10.802,-8.882" fill="#0e2430" stroke="#96cde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.654,13.876 -2.865,8.360 -2.959,7.933 -0.508,5.329 5.907,5.688 5.344,13.865" fill="#1c2662" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="3.927,-12.963 6.340,-12.765 8.146,-5.813 6.550,-3.659 -0.001,-4.811 -0.126,-5.198" fill="#21206f" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.880,-15.127 -3.113,-16.055 0.697,-15.294 3.245,-13.320 -0.809,-5.554 -6.652,-11.183" fill="#11293b" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.425,2.963 12.727,3.146 9.359,2.385 7.627,-2.862 9.222,-5.016 12.857,-5.728 17.314,-1.639" fill="#1f256a" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.558,14.756 6.353,13.935 6.917,5.757 9.021,3.880 12.389,4.642 12.608,14.426" fill="#182c54" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.134,17.323 -3.336,9.002 4.183,14.518 -3.838,19.064 -4.816,18.687" fill="#122b3d" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-7.104,16.996 -11.492,16.146 -13.459,13.319 -8.001,6.859 -4.401,8.249 -4.307,8.676" fill="#20246e" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.970,12.887 -16.252,11.618 -17.486,6.025 -11.922,2.201 -8.513,6.427" fill="#102937" stroke="#96cce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="11.963,-16.119 12.719,-15.813 14.657,-10.459 12.656,-6.753 9.022,-6.041 7.216,-12.992" fill="#152d48" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="15.230,15.165 13.335,14.410 13.116,4.626 13.813,4.442 19.984,8.603 20.204,11.292" fill="#132e43" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-23.000,-3.849 -13.407,-2.069 -12.506,0.221 -12.646,1.148 -18.210,4.972 -18.607,4.819 -23.897,-1.242" fill="#28247b" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.849,-4.664 -22.948,-5.311 -17.432,-10.036 -13.885,-10.054 -12.112,-9.122 -13.256,-2.884" fill="#122d3e" stroke="#96c8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="22.421,-3.324 17.876,-2.252 13.419,-6.341 15.420,-10.047 21.638,-8.304 22.911,-6.472 23.030,-4.726" fill="#1a2d5b" stroke="#96aee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="22.742,-1.965 24.625,2.525 20.478,7.870 14.308,3.709 18.197,-0.892" fill="#192f55" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.854,-24.193 8.853,-24.027 11.201,-17.276 6.454,-14.149 4.041,-14.348 1.494,-16.322" fill="#27247b" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.812,-22.207 -6.338,-15.782 -8.110,-11.838 -11.455,-10.372 -13.228,-11.304 -15.852,-21.511" fill="#332684" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.691,25.203 -2.386,21.364 -3.344,19.937 4.678,15.391 5.367,15.381 6.573,16.202" fill="#122e3f" stroke="#96c9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.322,-24.488 0.961,-16.617 -2.849,-17.378 -2.058,-23.371 0.408,-25.516 2.357,-25.915" fill="#1a3059" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.989,-17.883 21.446,-17.704 21.598,-17.565 21.852,-9.070 15.634,-10.812 13.697,-16.166" fill="#153148" stroke="#96c2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-16.515,-21.341 -13.891,-11.134 -17.438,-11.116 -21.032,-16.027 -18.955,-20.629 -17.630,-21.367" fill="#24247b" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.120,-23.012 -10.432,-26.513 -3.088,-23.507 -3.879,-17.514 -5.647,-16.586" fill="#153147" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="6.887,25.548 6.778,25.310 7.660,16.309 12.710,15.979 14.605,16.734 15.174,18.278 13.606,25.535" fill="#1d2f65" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-27.462,9.788 -19.117,6.145 -18.720,6.298 -17.487,11.890 -20.593,13.485 -24.684,12.927 -27.460,9.899" fill="#222973" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.565,-9.822 -26.709,-10.649 -26.186,-12.909 -21.597,-15.613 -18.003,-10.702 -23.518,-5.977" fill="#1a3259" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-27.740,9.151 -30.440,3.920 -30.309,3.113 -24.686,-0.554 -19.395,5.508" fill="#37288a" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-17.961,23.908 -21.003,23.393 -20.074,14.498 -16.967,12.903 -14.685,14.172 -12.719,16.999" fill="#143144" stroke="#96c8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-12.064,29.604 -12.904,29.417 -17.036,24.610 -11.794,17.701 -7.406,18.551 -6.088,19.916" fill="#23267a" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="4.611,28.233 2.941,29.810 -0.788,30.959 -2.970,29.678 -2.754,22.139 5.322,25.978 5.432,26.217" fill="#202d6d" stroke="#96a5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-11.419,30.002 -5.443,20.314 -4.465,20.690 -3.508,22.117 -3.724,29.657 -10.702,30.518" fill="#3a298d" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="32.317,8.779 22.247,11.503 21.506,11.185 21.285,8.496 25.432,3.151 31.610,6.290 32.365,8.649 32.362,8.689" fill="#153349" stroke="#96c5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="24.054,21.539 16.609,17.748 16.039,16.204 21.014,12.331 21.755,12.649 25.338,16.933" fill="#34288a" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.212,-27.564 20.682,-19.185 13.390,-17.468 12.634,-17.774 10.287,-24.526 10.926,-25.319" fill="#183452" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.666,-33.219 -0.330,-26.365 -2.796,-24.220 -10.140,-27.226 -9.225,-32.243 -9.013,-32.458 -7.253,-33.221" fill="#183453" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-26.845,21.543 -24.813,13.872 -20.722,14.430 -21.651,23.325 -23.969,23.773" fill="#1c335f" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="30.494,-13.122 24.053,-7.265 22.779,-9.098 22.525,-17.593 26.656,-16.914 30.506,-13.491" fill="#282682" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.034,29.178 14.655,25.762 16.224,18.505 23.669,22.296 24.115,23.485 20.957,28.445" fill="#252580" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="32.479,9.377 31.720,14.391 25.993,16.386 22.409,12.102" fill="#20306e" stroke="#96a7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.044,-3.914 36.452,-2.562 32.232,5.065 26.055,1.925 24.172,-2.564 24.780,-3.966" fill="#1d3364" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-36.500,-3.961 -27.741,-8.890 -24.695,-5.045 -24.596,-4.398 -25.493,-1.791 -31.116,1.876" fill="#16354c" stroke="#96c6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.192,-33.658 -1.115,-35.636 2.087,-33.183 2.093,-27.203 0.144,-26.804" fill="#24287d" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.048,-4.793 24.785,-4.845 24.666,-6.591 31.108,-12.448 36.271,-5.937" fill="#32288b" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-28.034,-22.810 -20.057,-21.126 -22.133,-16.524 -26.722,-13.820 -28.931,-17.127" fill="#3c2a92" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.989,-35.051 9.581,-26.404 8.941,-25.611 5.942,-25.777 2.977,-27.204 2.971,-33.184" fill="#3d2b93" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.703,15.075 -28.289,10.659 -25.513,13.687 -27.546,21.357 -27.580,21.350 -32.920,17.224" fill="#33298d" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.939,37.378 13.641,37.576 6.069,28.827 6.890,26.810 13.608,26.797 16.987,30.214" fill="#173750" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.304,-28.383 22.061,-19.278 21.604,-19.456 19.134,-27.836 21.174,-30.792 26.375,-30.900 26.468,-30.850" fill="#1a3759" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.040,-33.396 -20.946,-34.516 -10.825,-32.534 -11.740,-27.517 -14.429,-24.016 -16.469,-23.320 -17.585,-23.346" fill="#193855" stroke="#96c0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.898,-31.673 17.857,-28.717 10.571,-26.472 9.979,-35.119 10.710,-36.327 16.687,-36.037" fill="#1f346a" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.071,23.709 25.436,22.989 24.990,21.800 26.274,17.194 32.002,15.199 32.467,16.138 31.834,23.363" fill="#412c97" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.941,-7.723 -39.941,-8.509 -37.856,-12.596 -28.258,-10.379 -28.114,-9.552 -36.873,-4.623 -38.810,-5.404" fill="#1d3664" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-8.143,38.881 -10.569,31.597 -3.591,30.736 -1.408,32.016 -1.944,35.940" fill="#3e2b96" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.081,38.088 11.049,39.175 6.280,39.126 3.905,30.831 5.575,29.254 13.148,38.004" fill="#20336e" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.813,-28.035 29.667,-27.469 31.424,-23.887 26.853,-18.110 22.721,-18.790 22.570,-18.929" fill="#232f78" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-37.718,-13.193 -37.627,-14.287 -36.037,-16.411 -29.805,-16.543 -27.597,-13.236 -28.120,-10.976" fill="#262982" stroke="#9698e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.683,39.297 3.625,40.240 2.445,40.044 -0.957,36.075 -0.421,32.151 3.308,31.002" fill="#30298c" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.763,9.114 -31.725,4.583 -29.025,9.814 -29.023,9.925 -33.437,14.341" fill="#2c288a" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-22.961,-32.987 -18.505,-22.938 -19.830,-22.200 -27.807,-23.884 -28.333,-24.512 -25.339,-32.438" fill="#233078" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="31.933,-23.484 35.291,-21.137 35.309,-21.081 34.366,-16.938 31.212,-14.284 27.362,-17.708" fill="#3d2b96" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.332,37.493 -23.216,36.956 -23.845,26.213 -23.617,25.593 -21.300,25.145 -18.258,25.660 -14.126,30.467" fill="#183954" stroke="#96c4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="7.209,-41.915 9.465,-37.080 8.735,-35.871 2.716,-34.004 -0.485,-36.457 0.117,-38.180 6.941,-42.116" fill="#402c98" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.967,-24.092 -29.449,-23.579 -28.924,-22.951 -29.821,-17.267 -36.052,-17.135 -35.751,-23.042" fill="#452d9c" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.305,8.154 -40.993,8.171 -43.693,2.692 -39.457,-3.802 -37.520,-3.020 -32.135,2.816 -32.267,3.623" fill="#1a3a5a" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.500,-11.980 37.067,-6.569 31.904,-13.079 31.915,-13.448 35.070,-16.101" fill="#2a288a" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="45.467,2.042 46.098,4.980 34.163,8.073 33.407,5.714 37.627,-1.912" fill="#1b3a5e" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.644,43.280 -8.014,40.607 -7.812,39.579 -1.613,36.638 1.789,40.607 -4.865,44.685" fill="#412d9b" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.936,34.741 22.120,29.186 25.278,24.225 30.913,24.945 29.730,34.544" fill="#223475" stroke="#96a8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-20.528,38.318 -13.322,31.291 -12.482,31.479 -11.764,31.995 -9.339,39.279 -9.540,40.308 -18.083,41.692" fill="#1d3a64" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-35.946,26.609 -36.835,26.040 -37.607,23.600 -35.531,19.185 -33.596,18.098 -28.256,22.225" fill="#462e9e" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.718,-44.624 -3.181,-43.978 -0.982,-38.565 -1.584,-36.842 -6.661,-34.863 -7.248,-34.865" fill="#402d9a" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="43.068,14.676 40.367,18.157 33.651,15.551 33.186,14.612 33.944,9.599 33.989,9.510" fill="#392c96" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-44.153,14.063 -40.955,9.740 -40.267,9.723 -33.940,14.950 -34.157,17.099 -36.092,18.186 -44.269,14.780" fill="#2b298d" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="40.108,18.824 40.021,21.946 38.952,23.039 34.936,24.007 32.759,23.444 33.392,16.219" fill="#482ea0" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-15.542,-44.679 -10.401,-33.830 -10.613,-33.615 -20.734,-35.597 -19.803,-42.678 -17.940,-44.236" fill="#1f396c" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.167,32.366 -35.589,27.235 -27.900,22.850 -27.865,22.858 -24.988,25.088 -25.216,25.709" fill="#312a91" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.731,33.982 -32.700,32.924 -24.749,26.266 -24.120,37.009 -28.757,38.083 -30.103,37.525" fill="#233479" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.189,35.658 24.725,40.523 15.401,38.000 18.450,30.836 21.373,30.103" fill="#1a3c5a" stroke="#96c2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-14.884,-44.991 -14.649,-45.142 -7.838,-45.000 -7.453,-44.664 -7.984,-34.905 -9.743,-34.142" fill="#28288b" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="47.098,6.878 47.235,8.419 44.562,13.582 43.433,14.034 34.355,8.868 34.358,8.827 46.293,5.734" fill="#262e83" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="5.795,-44.393 6.553,-42.789 -0.271,-38.853 -2.470,-44.267 2.018,-47.021 3.263,-46.658" fill="#422e9d" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.760,-15.169 43.256,-12.752 42.833,-12.580 35.403,-16.702 36.346,-20.845 44.822,-17.610 45.003,-16.687" fill="#2e2a90" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.043,-35.517 -26.268,-32.789 -29.262,-24.863 -32.779,-25.376 -33.884,-35.281" fill="#223675" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="46.064,0.857 38.224,-3.097 37.816,-4.449 38.039,-5.593 43.472,-11.004 43.895,-11.175 49.319,-4.199" fill="#1b3d5d" stroke="#96c1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.518,-44.576 16.753,-37.401 10.776,-37.691 8.520,-42.527 17.905,-46.665" fill="#1d3d63" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-44.936,16.092 -44.525,15.395 -36.348,18.801 -38.424,23.216 -44.992,20.539 -45.390,18.616" fill="#312b93" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="35.625,35.637 30.490,34.638 31.673,25.038 32.437,24.693 34.613,25.255 37.385,34.960 37.103,35.174" fill="#233778" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-44.789,-20.050 -44.786,-20.745 -42.300,-23.196 -37.156,-23.281 -36.772,-23.094 -37.074,-17.187 -38.664,-15.063" fill="#4a2fa3" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="20.692,-44.124 26.440,-43.343 26.338,-32.693 21.137,-32.585 17.926,-36.949" fill="#1e3c67" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="38.983,-30.506 40.145,-26.477 35.984,-22.130 32.627,-24.477 30.870,-28.059" fill="#4d30a5" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="3.547,48.285 1.169,49.737 -1.361,48.976 -4.393,45.494 -4.422,45.408 2.232,41.330 3.412,41.526" fill="#422e9f" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="9.221,49.301 4.343,48.269 4.208,41.510 6.266,40.567 11.034,40.615" fill="#4d30a6" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.850,-33.585 -38.006,-35.048 -37.008,-35.388 -34.529,-35.209 -33.424,-25.304 -36.208,-24.254 -36.592,-24.441" fill="#24367d" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="45.074,-18.269 36.598,-21.505 36.579,-21.561 40.740,-25.907 45.236,-23.879 45.789,-21.228" fill="#322b95" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.112,-14.753 -45.315,-19.404 -39.190,-14.417 -39.281,-13.323 -41.366,-9.237" fill="#4c30a6" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-18.136,49.044 -17.921,42.691 -9.379,41.307 -8.009,43.979 -16.490,50.985" fill="#1d3e65" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.474,-4.585 -41.717,-6.857 -40.586,-4.539 -44.822,1.956 -51.359,1.369" fill="#23387a" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="37.968,34.793 35.197,25.089 39.213,24.121 41.855,32.207 39.258,34.341" fill="#263482" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.441,-38.078 39.100,-32.165 38.729,-31.349 30.616,-28.902 29.761,-29.467 27.925,-31.935" fill="#4a30a5" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-32.504,-36.855 -28.893,-44.740 -21.326,-42.878 -22.257,-35.797 -23.351,-34.677 -25.729,-34.127" fill="#1e3f68" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="19.251,50.646 14.736,39.397 14.802,39.314 15.100,39.115 24.423,41.638 25.368,46.789 22.264,50.071" fill="#282f8b" stroke="#969be9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-51.480,2.721 -44.943,3.309 -42.243,8.787 -45.441,13.110 -53.777,6.697" fill="#1d4063" stroke="#96bfe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="18.466,50.962 18.310,51.081 11.103,50.854 10.105,49.486 11.918,40.800 13.950,39.713" fill="#462fa4" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.081,-33.162 -40.430,-33.379 -37.172,-24.234 -42.316,-24.149 -43.953,-30.752" fill="#273287" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.974,-54.013 14.112,-53.999 15.147,-53.498 16.665,-51.871 17.590,-47.381 8.205,-43.243 7.936,-43.444 7.178,-45.047" fill="#1e3f68" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-5.210,-52.675 -4.589,-53.061 -0.592,-51.742 1.511,-47.847 -2.977,-45.094 -6.514,-45.739 -6.899,-46.075" fill="#3f2e9f" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-43.269,29.035 -45.757,27.831 -46.440,24.316 -45.293,21.277 -38.725,23.954 -37.954,26.393" fill="#322c97" stroke="#9a96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.641,-5.451 -53.896,-5.696 -54.349,-8.137 -51.468,-13.652 -49.630,-14.027 -41.884,-8.510 -41.884,-7.723" fill="#3f2ea0" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="46.049,23.516 41.182,21.979 41.269,18.857 43.970,15.376 45.099,14.924 49.685,20.185" fill="#4830a5" stroke="#a796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="36.155,-40.132 35.936,-38.777 27.420,-32.634 27.328,-32.684 27.429,-43.333 30.378,-44.847 33.283,-43.989" fill="#332c98" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.958,55.045 -12.541,54.967 -14.631,54.018 -15.392,53.270 -16.074,51.488 -7.594,44.482 -5.815,45.887 -5.785,45.974" fill="#1f3f6c" stroke="#96b8e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-42.879,29.819 -37.564,27.177 -36.674,27.747 -34.252,32.878 -34.283,33.937 -41.510,35.420" fill="#4d31a9" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="42.490,31.999 39.847,23.914 40.916,22.821 45.783,24.359 46.602,29.161 43.558,31.711" fill="#28318a" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-28.024,49.302 -28.358,39.803 -23.721,38.730 -21.838,39.267 -19.393,42.641 -19.607,48.994" fill="#24397e" stroke="#96a9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="13.499,-54.374 6.703,-45.407 4.171,-47.673 8.634,-55.759 10.232,-55.768" fill="#213f70" stroke="#96b5e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.221,-10.116 55.300,-6.231 50.162,-4.854 44.737,-11.830 46.241,-14.248" fill="#1e4167" stroke="#96bee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-10.450,55.335 -5.276,46.264 -2.245,49.745 -6.881,56.783" fill="#223d76" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-12.656,-54.548 -6.121,-52.909 -7.811,-46.308 -14.622,-46.451" fill="#5132ac" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.853,46.516 25.908,41.366 29.372,36.500 30.166,36.304 35.301,37.303 33.316,47.623" fill="#223e75" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="45.889,-32.822 46.773,-32.385 47.527,-27.628 45.621,-24.731 41.124,-26.759 39.963,-30.789 40.333,-31.605" fill="#2d2c96" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="8.106,-56.050 3.643,-47.964 2.399,-48.327 0.296,-52.222 4.680,-57.403" fill="#243c7b" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-49.716,-24.610 -50.059,-27.512 -46.610,-30.294 -44.696,-30.568 -43.058,-23.965 -45.544,-21.514" fill="#29328c" stroke="#969ee9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.512,54.963 1.758,50.701 4.136,49.249 9.014,50.281 10.012,51.650 7.798,56.998" fill="#382e9e" stroke="#9d96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="55.929,14.221 51.282,19.599 50.451,19.517 45.864,14.257 48.538,9.093" fill="#5433ae" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.643,14.264 -58.886,13.654 -58.905,12.526 -57.599,9.371 -54.286,7.359 -45.950,13.771 -46.067,14.488 -46.477,15.185" fill="#1f426a" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="59.798,2.845 48.730,5.732 47.926,4.587 47.294,1.649 50.549,-3.407 55.688,-4.784" fill="#2b2c94" stroke="#9697e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-6.337,57.142 -1.701,50.104 0.830,50.866 1.583,55.127 -3.559,59.999" fill="#253b7f" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.301,44.769 -42.307,37.788 -41.296,36.458 -34.070,34.975 -31.442,38.518" fill="#5733b1" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-50.283,-23.847 -46.111,-20.751 -46.114,-20.056 -49.911,-15.406 -51.749,-15.031 -55.340,-19.708" fill="#3f2fa3" stroke="#a196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.790,-17.851 59.026,-15.208 59.108,-13.387 58.560,-11.797 57.465,-10.762 46.484,-14.894 46.727,-16.412" fill="#1f436a" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.690,14.885 -46.524,15.806 -46.978,18.330 -56.778,20.522 -58.518,17.296" fill="#1f436b" stroke="#96bde9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-23.926,-55.601 -19.155,-45.689 -21.018,-44.131 -28.585,-45.993 -30.636,-49.588 -26.728,-55.031" fill="#4230a5" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.287,-22.891 57.713,-18.448 46.649,-17.010 46.468,-17.932 47.183,-20.891" fill="#1f436c" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-23.058,-56.019 -22.180,-56.836 -14.571,-56.555 -13.688,-54.798 -15.653,-46.701 -15.888,-46.550 -18.286,-46.107" fill="#5934b2" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-56.646,21.109 -46.847,18.917 -46.449,20.841 -47.596,23.880 -56.252,24.556" fill="#20436f" stroke="#96bae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-38.113,-49.309 -31.991,-48.815 -29.941,-45.220 -33.551,-37.334 -34.393,-37.099 -36.872,-37.277" fill="#243e7d" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-38.855,46.287 -38.690,45.537 -30.832,39.286 -29.485,39.843 -29.151,49.342 -30.997,51.119 -35.816,50.299" fill="#5133ae" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="54.733,29.568 47.500,29.008 46.681,24.206 50.317,20.874 51.148,20.956 54.268,24.651" fill="#29358c" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.264,-41.216 45.666,-33.837 40.111,-32.620 37.452,-38.532 37.671,-39.887" fill="#4430a7" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="60.053,3.824 60.150,3.881 61.214,7.469 58.235,13.211 56.513,13.378 49.122,8.250 48.985,6.710" fill="#5834b2" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.434,-54.817 29.594,-46.374 26.646,-44.860 20.898,-45.640 19.285,-47.730 18.360,-52.220" fill="#2a3390" stroke="#969de9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.410,-66.436 4.110,-57.886 -0.274,-52.704 -4.271,-54.023" fill="#263c82" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="57.837,-26.269 57.168,-23.494 47.064,-21.494 46.512,-24.144 48.418,-27.041" fill="#214273" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-53.826,34.859 -46.240,28.828 -43.752,30.033 -42.382,35.633 -43.393,36.963 -48.501,37.419" fill="#2b3093" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-58.814,34.022 -56.202,25.207 -47.545,24.531 -46.862,28.045 -54.449,34.076" fill="#234177" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="16.723,60.210 8.992,57.947 8.850,57.434 11.064,52.085 18.272,52.313" fill="#4832ab" stroke="#a596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.385,49.774 36.453,49.820 34.217,47.797 36.202,37.476 37.680,37.013 42.931,48.902 42.831,49.019" fill="#253e80" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-3.697,-67.448 -3.418,-66.587 -5.280,-54.174 -5.901,-53.789 -12.436,-55.428 -13.319,-57.184 -10.829,-62.468" fill="#2c2d99" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.119,-0.277 -55.253,-4.288 -54.999,-4.043 -52.884,1.911 -55.181,5.886 -58.493,7.899 -65.125,-0.249" fill="#4231a7" stroke="#a296e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="0.934,75.486 -2.944,60.648 2.198,55.777 7.485,57.812 7.626,58.325" fill="#263e82" stroke="#96abe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-46.132,-46.098 -44.186,-48.667 -42.727,-49.486 -39.875,-49.749 -38.866,-49.232 -37.625,-37.200 -38.622,-36.860" fill="#25407f" stroke="#96afe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="55.472,42.948 50.306,44.330 40.287,35.593 42.884,33.458 43.952,33.169" fill="#214572" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-27.785,60.070 -29.823,52.337 -27.978,50.560 -19.561,50.252 -17.916,52.194 -17.233,53.975" fill="#2d2d9b" stroke="#9696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="66.114,-39.313 57.897,-27.006 48.478,-27.778 47.724,-32.536" fill="#24427a" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="49.885,44.813 43.545,48.631 38.294,36.742 38.576,36.528 39.866,36.076" fill="#24427a" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-55.176,-41.057 -52.020,-41.973 -42.008,-34.267 -44.880,-31.857 -46.794,-31.583" fill="#214570" stroke="#96bce9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-63.256,-13.158 -57.622,-19.397 -56.296,-18.974 -52.705,-14.298 -55.585,-8.783" fill="#4c33ae" stroke="#a796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="64.793,41.938 55.888,42.457 44.368,32.679 47.413,30.130 54.646,30.690" fill="#214572" stroke="#96bbe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-51.629,-42.481 -46.632,-45.692 -39.121,-36.455 -40.965,-34.991 -41.617,-34.775" fill="#224475" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="61.871,19.142 55.130,23.923 52.010,20.228 56.657,14.850 58.378,14.683" fill="#2f2d9d" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.450,-33.291 -51.145,-27.384 -50.802,-24.482 -55.860,-20.343 -57.187,-20.765" fill="#283b89" stroke="#96a6e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="29.051,59.500 23.502,51.241 26.606,47.959 33.068,49.066 35.305,51.089 31.407,58.974" fill="#5935b6" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-71.483,-40.034 -55.675,-40.616 -47.293,-31.141 -50.742,-28.359 -65.048,-34.267 -71.431,-39.948" fill="#224574" stroke="#96b9e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.297,-51.840 41.865,-51.686 45.264,-43.648 44.024,-42.411 37.430,-41.082 34.558,-44.938" fill="#5134b2" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.219,-43.721 65.739,-40.329 47.349,-33.552 46.465,-33.989 45.064,-41.368 46.304,-42.605" fill="#29398e" stroke="#96a3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-47.153,47.700 -48.389,38.665 -43.282,38.208 -40.275,45.189 -40.440,45.939" fill="#322ea0" stroke="#9896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.152,60.104 26.324,62.203 18.522,61.951 17.887,60.438 19.435,52.541 19.591,52.421 22.603,51.845" fill="#5835b6" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="40.525,-52.594 33.786,-45.692 30.881,-46.550 29.721,-54.994 30.578,-56.612 37.527,-56.419" fill="#6136bb" stroke="#b096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.627,-63.493 29.035,-57.429 28.178,-55.812 18.104,-53.215 16.585,-54.841 25.253,-64.493" fill="#322fa0" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-65.556,-1.352 -67.694,-6.276 -64.760,-12.100 -63.814,-12.179 -56.143,-7.804 -55.690,-5.363" fill="#5e36ba" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.228,-2.662 61.110,2.248 61.013,2.191 56.903,-5.438 58.824,-9.323 59.919,-10.358 69.345,-3.225" fill="#5234b3" stroke="#a996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.958,-88.477 10.225,-76.954 10.223,-57.372 8.625,-57.363 5.199,-58.716 -1.321,-67.266 -1.600,-68.127" fill="#234679" stroke="#96b7e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-2.439,82.132 -13.242,70.980 -12.628,56.723 -11.045,56.801 -7.477,58.250 -4.699,61.107 -0.821,75.945 -0.810,77.521" fill="#25437f" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="2.071,77.501 2.060,75.925 8.753,58.764 16.484,61.027 17.120,62.540 14.777,67.721" fill="#2a3991" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.176,66.800 -23.711,66.377 -25.381,65.244 -27.459,61.324 -27.360,60.805 -16.809,54.710 -16.048,55.459" fill="#302ea0" stroke="#9796e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="10.919,-76.954 16.981,-70.102 14.321,-55.963 14.183,-55.978 10.917,-57.372" fill="#283f88" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-13.939,70.950 -20.542,67.086 -15.414,55.745 -13.324,56.693" fill="#2b3993" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-21.409,-66.071 -12.037,-63.037 -14.527,-57.754 -22.135,-58.034" fill="#342fa3" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="17.661,-69.974 24.089,-65.920 24.703,-64.987 16.036,-55.335 15.001,-55.835" fill="#2d329b" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="78.137,44.492 66.172,40.694 56.025,29.446 55.559,24.529 62.300,19.748 63.478,20.272 78.171,44.439" fill="#274185" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-34.209,-62.286 -27.950,-55.908 -31.858,-50.465 -37.980,-50.960 -38.989,-51.477 -36.323,-61.756" fill="#6537bf" stroke="#b196e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-72.097,5.621 -66.023,0.481 -59.390,8.629 -60.697,11.784 -72.418,7.460" fill="#5a36ba" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-72.961,-38.229 -66.578,-32.547 -58.314,-20.021 -63.948,-13.782 -64.893,-13.704 -69.476,-18.853" fill="#2b3a94" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.505,11.232 70.644,11.704 64.121,18.825 62.944,18.301 59.451,13.842 62.430,8.100" fill="#3830a7" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.050,50.493 -77.068,48.796 -77.384,47.349 -58.840,36.154 -54.475,36.208 -49.149,38.769 -47.913,47.804 -48.842,48.663" fill="#274288" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.910,59.066 -36.088,51.898 -31.269,52.718 -29.230,60.451 -29.328,60.970 -38.258,62.282" fill="#6338bf" stroke="#b096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-83.726,37.896 -79.784,31.414 -59.935,18.061 -58.195,21.286 -57.801,24.733 -60.413,33.548 -78.957,44.743" fill="#2a3c92" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-41.884,58.411 -44.202,58.040 -47.743,49.851 -46.814,48.991 -40.101,47.231 -37.062,51.243" fill="#3f32ac" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="73.780,-12.305 74.131,-11.233 73.720,-8.284 69.928,-3.995 60.502,-11.128 61.050,-12.719" fill="#5c37bc" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="80.733,-41.667 74.046,-22.757 60.652,-15.969 59.416,-18.612 58.991,-23.054 59.660,-25.829 67.876,-38.136 71.356,-41.528 75.920,-44.292 78.163,-44.985" fill="#2e329f" stroke="#9699e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="70.939,10.111 62.865,6.979 61.801,3.391 69.920,-1.519 73.632,4.455" fill="#5736ba" stroke="#aa96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.339,-63.140 -30.755,-66.978 -22.976,-66.862 -22.674,-66.185 -23.400,-58.149 -24.279,-57.332 -27.080,-56.762" fill="#4934b2" stroke="#a496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="69.149,-45.173 46.233,-44.058 42.834,-52.096 50.132,-54.885 73.713,-47.937" fill="#2b3a96" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-80.209,30.782 -74.719,18.067 -60.776,14.407 -60.533,15.017 -60.360,17.428" fill="#3e32ac" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-74.908,17.349 -74.203,10.432 -72.705,8.237 -60.984,12.561 -60.965,13.689" fill="#5836bb" stroke="#ab96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="39.018,63.945 32.572,59.551 36.471,51.666 41.403,51.620 41.465,63.248" fill="#6a39c4" stroke="#b396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="74.387,-22.085 73.804,-13.062 61.074,-13.475 60.993,-15.296" fill="#4e35b6" stroke="#a696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-4.648,-68.810 -11.780,-63.830 -21.153,-66.863 -21.455,-67.540 -12.195,-92.128 -8.596,-92.384 -0.231,-90.070 -0.090,-89.159" fill="#28438a" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="26.799,71.170 26.313,71.703 16.141,68.337 18.483,63.156 26.285,63.408" fill="#2c3b98" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="40.380,-64.959 37.567,-57.865 30.618,-58.058 28.210,-64.122 37.865,-67.815" fill="#5f38c0" stroke="#ae96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-43.135,-64.543 -37.394,-62.033 -40.059,-51.754 -42.911,-51.492 -46.751,-63.367" fill="#6d3bc6" stroke="#b496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="87.712,34.923 79.507,43.627 64.813,19.459 71.336,12.338 90.091,29.262" fill="#29458c" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.455,-8.207 -77.325,-10.306 -70.578,-17.872 -65.995,-12.723 -68.930,-6.899" fill="#2d399d" stroke="#969fe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-76.673,-6.953 -69.148,-5.645 -67.010,-0.721 -67.016,-0.693 -73.090,4.447 -78.878,-1.909" fill="#4a35b6" stroke="#a396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-52.610,-44.008 -55.767,-43.093 -71.575,-42.511 -78.132,-47.251 -77.882,-50.270 -72.356,-62.706 -69.286,-65.724 -63.764,-64.217 -45.666,-49.789 -47.613,-47.219" fill="#5e38c1" stroke="#ad96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="51.981,62.124 46.292,64.227 42.408,63.242 42.346,51.615 43.792,50.860" fill="#703dc7" stroke="#b496e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="41.578,-64.484 45.866,-64.347 49.630,-56.200 42.332,-53.411 41.763,-53.564 38.765,-57.390" fill="#2f35a3" stroke="#969ae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="28.121,71.082 27.607,63.321 29.435,61.222 31.791,60.696 38.237,65.091 35.462,70.971" fill="#3732ab" stroke="#9996e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="72.535,61.574 52.625,61.657 44.436,50.392 44.536,50.275 50.875,46.457 56.041,45.075 64.946,44.555 76.911,48.354 76.847,50.115" fill="#6c3cc6" stroke="#b396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-63.265,-64.844 -47.548,-63.110 -43.708,-51.234 -45.167,-50.416" fill="#713ec7" stroke="#b596e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-74.155,57.325 -75.941,52.109 -48.733,50.279 -45.192,58.468 -51.174,61.914" fill="#29468e" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-39.076,69.940 -38.075,63.529 -29.145,62.217 -27.067,66.137 -36.226,72.936" fill="#5c38c1" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="9.312,94.772 1.822,91.809 1.538,83.537 3.167,78.925 15.873,69.145 26.046,72.511 20.282,94.909" fill="#274a88" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-18.840,-93.479 -13.698,-92.694 -22.958,-68.106 -30.736,-68.223 -32.542,-74.741" fill="#2a4691" stroke="#96ace9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="81.429,3.313 74.839,3.705 71.127,-2.269 71.243,-2.831 75.036,-7.120 83.291,-1.457" fill="#4034b3" stroke="#9e96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="33.293,-77.527 37.218,-73.738 37.477,-68.829 27.822,-65.136 26.448,-66.136 25.834,-67.069" fill="#5e39c3" stroke="#ac96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-30.839,93.404 -31.061,89.980 -24.030,68.291 -21.494,68.714 -14.891,72.577 -4.088,83.730 -3.805,92.002 -9.713,94.792 -23.705,95.910" fill="#7643c8" stroke="#b696e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-31.865,89.719 -35.662,73.695 -26.503,66.897 -24.834,68.030" fill="#653bc6" stroke="#af96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="91.121,28.120 72.366,11.196 72.227,10.724 74.920,5.068 81.510,4.676 93.277,25.138" fill="#2b4595" stroke="#96aae9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-33.865,-74.374 -32.059,-67.856 -34.643,-64.018 -36.758,-63.488 -42.499,-65.998 -40.448,-74.006" fill="#3038a6" stroke="#969be9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-81.404,-42.725 -74.846,-37.985 -74.794,-37.899 -71.308,-18.523 -78.056,-10.957 -96.674,-23.104 -90.167,-35.358" fill="#284c8b" stroke="#96b4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-40.502,69.718 -46.710,70.492 -50.452,63.166 -44.471,59.720 -42.153,60.091 -39.501,63.307" fill="#2d419c" stroke="#96a4e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="25.742,-95.055 32.589,-78.029 25.130,-67.571 18.703,-71.625 12.640,-78.477 5.373,-90.000 5.233,-90.911 10.037,-93.467 24.050,-95.515" fill="#6f40c8" stroke="#b396e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-94.020,3.323 -79.800,-1.070 -74.012,5.286 -74.333,7.125 -75.831,9.320" fill="#3c34b3" stroke="#9b96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="62.296,-78.544 65.399,-76.816 73.530,-65.187 76.538,-50.921 76.483,-50.421 74.240,-49.728 50.660,-56.676 46.896,-64.823" fill="#294c8e" stroke="#96b3e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="98.603,-10.842 83.905,-2.352 75.650,-8.015 76.060,-10.964" fill="#3e35b6" stroke="#9c96e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="45.613,87.425 36.742,71.575 39.517,65.695 41.964,64.997 45.847,65.981" fill="#2f3ea4" stroke="#96a0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="44.352,88.130 44.909,90.023 39.078,94.139 24.911,96.177 21.891,95.323 27.655,72.925 28.141,72.392 35.482,72.281" fill="#2a4d90" stroke="#96b2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-100.284,4.598 -94.321,4.238 -76.133,10.235 -76.838,17.152 -82.328,29.867 -86.270,36.349 -89.736,34.199 -98.598,22.574 -101.394,8.620" fill="#4837bc" stroke="#a096e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="99.119,-23.160 100.343,-12.361 98.607,-11.744 76.065,-11.866 75.714,-12.938 76.297,-21.962 82.984,-40.871 91.176,-35.268" fill="#5739c4" stroke="#a896e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="61.316,-79.643 45.917,-65.923 41.629,-66.060 39.114,-68.915 38.855,-73.824 60.061,-80.868" fill="#2c4899" stroke="#96abe9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-97.630,-21.686 -97.528,-21.796 -78.909,-9.649 -78.039,-7.550 -80.244,-2.507 -94.463,1.887 -100.426,2.247 -102.123,-8.044" fill="#2b4b94" stroke="#96b0e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-64.797,-78.123 -54.437,-87.697 -41.658,-74.316 -43.708,-66.307 -47.325,-65.131 -63.042,-66.865 -68.563,-68.372" fill="#3041a4" stroke="#96a2e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="52.634,63.891 72.544,63.808 72.486,65.575 66.411,79.341 54.931,88.193 47.267,89.330 46.710,87.437 46.944,65.993" fill="#3140a8" stroke="#96a1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-74.010,66.382 -74.516,59.130 -51.534,63.719 -47.792,71.045 -57.771,85.652 -67.129,79.585" fill="#2b4e95" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-56.513,86.512 -46.534,71.904 -40.326,71.130 -37.476,74.125 -33.678,90.149 -33.457,93.573 -40.643,94.430 -54.263,89.269" fill="#2d4b9a" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="99.449,-9.378 101.184,-9.995 103.336,-6.789 104.713,7.769 98.319,21.271 94.656,24.345 82.889,3.883 84.752,-0.887" fill="#2c4e96" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="-25.677,-96.866 -20.240,-94.503 -33.943,-75.765 -40.526,-75.397 -53.305,-88.778 -53.127,-89.087 -40.647,-96.670" fill="#2c4f96" stroke="#96b1e9" stroke-opacity="0.53" stroke-width="0.65"/><polygon points="34.532,-78.810 27.684,-95.836 41.202,-96.928 54.375,-90.404 59.664,-82.065 38.457,-75.021" fill="#2d4d9c" stroke="#96ade9" stroke-opacity="0.53" stroke-width="0.65"/><g fill="none" stroke="#d1e7ed" stroke-width="0.55" opacity="0.22"><polygon points="2.725,1.152 -5.151,1.832 -7.062,-4.577" /><polygon points="-5.151,1.832 2.725,1.152 2.296,8.841" /><polygon points="2.725,1.152 -7.062,-4.577 4.381,-8.263" /><polygon points="4.381,-8.263 -7.062,-4.577 -0.871,-11.004" /><polygon points="2.725,1.152 4.381,-8.263 12.662,-2.129" /><polygon points="2.296,8.841 2.725,1.152 10.061,9.376" /><polygon points="10.061,9.376 2.725,1.152 12.662,-2.129" /><polygon points="2.296,8.841 -8.795,11.271 -5.151,1.832" /><polygon points="-8.795,11.271 2.296,8.841 -1.328,13.781" /><polygon points="-5.151,1.832 -18.299,-0.156 -7.062,-4.577" /><polygon points="-12.730,7.946 -5.151,1.832 -8.795,11.271" /><polygon points="12.662,-2.129 4.381,-8.263 11.118,-10.012" /><polygon points="10.061,9.376 12.662,-2.129 15.653,9.251" /><polygon points="-18.299,-0.156 -5.151,1.832 -12.730,7.946" /><polygon points="-7.062,-4.577 -18.299,-0.156 -17.136,-6.424" /><polygon points="12.662,-2.129 11.118,-10.012 16.988,-6.844" /><polygon points="15.653,9.251 12.662,-2.129 19.455,3.613" /><polygon points="19.455,3.613 12.662,-2.129 16.988,-6.844" /><polygon points="4.381,-8.263 -0.871,-11.004 5.257,-18.911" /><polygon points="11.118,-10.012 4.381,-8.263 5.257,-18.911" /><polygon points="-0.871,-11.004 -7.062,-4.577 -12.082,-16.041" /><polygon points="-12.082,-16.041 -7.062,-4.577 -17.136,-6.424" /><polygon points="-1.328,13.781 2.296,8.841 2.479,20.498" /><polygon points="2.479,20.498 2.296,8.841 10.061,9.376" /><polygon points="-0.871,-11.004 -6.764,-22.226 1.160,-21.180" /><polygon points="5.257,-18.911 -0.871,-11.004 1.160,-21.180" /><polygon points="16.988,-6.844 11.118,-10.012 18.639,-12.734" /><polygon points="-12.082,-16.041 -17.136,-6.424 -17.179,-14.730" /><polygon points="-6.764,-22.226 -0.871,-11.004 -12.082,-16.041" /><polygon points="2.479,20.498 10.061,9.376 10.842,21.318" /><polygon points="10.842,21.318 10.061,9.376 15.653,9.251" /><polygon points="-18.299,-0.156 -12.730,7.946 -22.227,10.040" /><polygon points="-17.179,-14.730 -17.136,-6.424 -21.527,-11.549" /><polygon points="-18.299,-0.156 -22.227,10.040 -24.366,5.140" /><polygon points="-12.730,7.946 -8.795,11.271 -18.228,17.833" /><polygon points="-22.227,10.040 -12.730,7.946 -18.228,17.833" /><polygon points="-8.795,11.271 -1.328,13.781 -11.113,23.231" /><polygon points="-18.228,17.833 -8.795,11.271 -11.113,23.231" /><polygon points="-1.328,13.781 -6.151,26.292 -11.113,23.231" /><polygon points="-6.151,26.292 -1.328,13.781 2.479,20.498" /><polygon points="-6.151,26.292 2.479,20.498 -0.355,26.458" /><polygon points="15.653,9.251 19.455,3.613 25.663,8.429" /><polygon points="10.842,21.318 15.653,9.251 21.877,17.243" /><polygon points="21.877,17.243 15.653,9.251 25.663,8.429" /><polygon points="11.118,-10.012 5.257,-18.911 16.282,-22.745" /><polygon points="18.639,-12.734 11.118,-10.012 16.282,-22.745" /><polygon points="1.160,-21.180 -6.764,-22.226 -4.520,-27.711" /><polygon points="-22.227,10.040 -18.228,17.833 -23.217,17.311" /><polygon points="16.988,-6.844 18.639,-12.734 25.767,-12.947" /><polygon points="10.842,21.318 21.877,17.243 18.914,23.062" /><polygon points="21.877,17.243 25.663,8.429 26.909,13.035" /><polygon points="19.455,3.613 16.988,-6.844 30.454,-1.000" /><polygon points="25.663,8.429 19.455,3.613 30.454,-1.000" /><polygon points="16.988,-6.844 30.485,-7.759 30.454,-1.000" /><polygon points="-17.136,-6.424 -18.299,-0.156 -30.574,-4.379" /><polygon points="-21.527,-11.549 -17.136,-6.424 -30.574,-4.379" /><polygon points="-30.574,-4.379 -18.299,-0.156 -24.366,5.140" /><polygon points="1.160,-21.180 -4.520,-27.711 -0.868,-31.086" /><polygon points="30.485,-7.759 16.988,-6.844 25.767,-12.947" /><polygon points="-17.179,-14.730 -21.527,-11.549 -25.655,-18.555" /><polygon points="5.257,-18.911 1.160,-21.180 5.931,-31.093" /><polygon points="16.282,-22.745 5.257,-18.911 5.931,-31.093" /><polygon points="5.931,-31.093 1.160,-21.180 -0.868,-31.086" /><polygon points="-22.227,10.040 -23.217,17.311 -28.602,15.885" /><polygon points="10.842,21.318 -0.355,26.458 2.479,20.498" /><polygon points="-0.355,26.458 10.842,21.318 10.861,31.025" /><polygon points="10.861,31.025 10.842,21.318 18.914,23.062" /><polygon points="18.639,-12.734 16.282,-22.745 23.370,-24.834" /><polygon points="18.639,-12.734 27.281,-22.153 25.767,-12.947" /><polygon points="-6.764,-22.226 -12.082,-16.041 -16.827,-29.955" /><polygon points="-4.520,-27.711 -6.764,-22.226 -16.827,-29.955" /><polygon points="-16.827,-29.955 -12.082,-16.041 -17.179,-14.730" /><polygon points="-17.179,-14.730 -23.911,-26.814 -16.827,-29.955" /><polygon points="16.282,-22.745 5.931,-31.093 13.548,-31.614" /><polygon points="21.877,17.243 26.909,13.035 29.075,19.251" /><polygon points="-21.527,-11.549 -30.574,-4.379 -33.440,-9.472" /><polygon points="-21.527,-11.549 -32.379,-14.064 -25.655,-18.555" /><polygon points="-6.151,26.292 -0.355,26.458 -5.127,34.590" /><polygon points="-0.355,26.458 10.861,31.025 7.063,34.313" /><polygon points="-0.355,26.458 2.472,35.627 -5.127,34.590" /><polygon points="27.281,-22.153 18.639,-12.734 23.370,-24.834" /><polygon points="-32.379,-14.064 -21.527,-11.549 -33.440,-9.472" /><polygon points="2.472,35.627 -0.355,26.458 7.063,34.313" /><polygon points="-24.366,5.140 -22.227,10.040 -34.249,10.241" /><polygon points="-24.366,5.140 -38.414,2.853 -30.574,-4.379" /><polygon points="-34.249,10.241 -22.227,10.040 -28.602,15.885" /><polygon points="-23.911,-26.814 -17.179,-14.730 -25.655,-18.555" /><polygon points="25.767,-12.947 27.281,-22.153 31.198,-19.054" /><polygon points="-18.228,17.833 -11.113,23.231 -20.511,31.309" /><polygon points="-23.217,17.311 -18.228,17.833 -20.511,31.309" /><polygon points="5.931,-31.093 -0.868,-31.086 3.973,-37.403" /><polygon points="-25.655,-18.555 -32.379,-14.064 -32.498,-19.635" /><polygon points="-38.414,2.853 -24.366,5.140 -34.249,10.241" /><polygon points="30.485,-7.759 25.767,-12.947 36.612,-12.617" /><polygon points="36.612,-12.617 25.767,-12.947 31.198,-19.054" /><polygon points="25.663,8.429 30.454,-1.000 39.489,3.999" /><polygon points="-5.127,34.590 2.472,35.627 -2.580,39.957" /><polygon points="18.914,23.062 29.075,19.251 27.861,28.758" /><polygon points="29.075,19.251 18.914,23.062 21.877,17.243" /><polygon points="-11.113,23.231 -6.151,26.292 -14.325,37.653" /><polygon points="-20.511,31.309 -11.113,23.231 -14.325,37.653" /><polygon points="-14.325,37.653 -6.151,26.292 -5.127,34.590" /><polygon points="-28.602,15.885 -31.059,27.426 -33.803,22.614" /><polygon points="-31.059,27.426 -28.602,15.885 -23.217,17.311" /><polygon points="-0.868,-31.086 -4.520,-27.711 -4.482,-40.359" /><polygon points="3.973,-37.403 -0.868,-31.086 -4.482,-40.359" /><polygon points="-4.520,-27.711 -10.139,-40.666 -4.482,-40.359" /><polygon points="26.909,13.035 25.663,8.429 38.182,14.740" /><polygon points="29.075,19.251 26.909,13.035 38.182,14.740" /><polygon points="25.663,8.429 40.993,9.801 38.182,14.740" /><polygon points="-34.249,10.241 -28.602,15.885 -38.120,14.926" /><polygon points="-38.120,14.926 -28.602,15.885 -33.803,22.614" /><polygon points="29.075,19.251 38.182,14.740 36.191,19.873" /><polygon points="-15.200,-38.268 -4.520,-27.711 -16.827,-29.955" /><polygon points="-31.059,27.426 -23.217,17.311 -20.511,31.309" /><polygon points="-31.059,27.426 -20.511,31.309 -27.466,31.717" /><polygon points="10.861,31.025 18.914,23.062 22.112,35.812" /><polygon points="22.112,35.812 18.914,23.062 27.861,28.758" /><polygon points="-10.139,-40.666 -4.520,-27.711 -15.200,-38.268" /><polygon points="40.993,9.801 25.663,8.429 39.489,3.999" /><polygon points="3.973,-37.403 -4.482,-40.359 0.986,-42.580" /><polygon points="36.612,-12.617 31.198,-19.054 39.175,-17.238" /><polygon points="-23.911,-26.814 -32.498,-19.635 -31.058,-29.513" /><polygon points="-32.498,-19.635 -23.911,-26.814 -25.655,-18.555" /><polygon points="30.454,-1.000 30.485,-7.759 44.085,-5.113" /><polygon points="39.489,3.999 30.454,-1.000 44.085,-5.113" /><polygon points="44.085,-5.113 30.485,-7.759 36.612,-12.617" /><polygon points="13.548,-31.614 3.973,-37.403 14.058,-42.108" /><polygon points="3.973,-37.403 13.548,-31.614 5.931,-31.093" /><polygon points="-38.120,14.926 -33.803,22.614 -40.091,19.657" /><polygon points="27.861,28.758 29.075,19.251 33.708,29.479" /><polygon points="33.708,29.479 29.075,19.251 36.191,19.873" /><polygon points="-32.498,-19.635 -32.379,-14.064 -40.355,-20.036" /><polygon points="13.548,-31.614 23.370,-24.834 16.282,-22.745" /><polygon points="23.370,-24.834 13.548,-31.614 23.083,-38.629" /><polygon points="23.083,-38.629 13.548,-31.614 14.058,-42.108" /><polygon points="31.198,-19.054 27.281,-22.153 36.534,-26.690" /><polygon points="31.198,-19.054 41.111,-22.309 39.175,-17.238" /><polygon points="-2.580,39.957 2.472,35.627 0.827,45.518" /><polygon points="2.472,35.627 7.063,34.313 6.950,45.395" /><polygon points="0.827,45.518 2.472,35.627 6.950,45.395" /><polygon points="-31.058,-29.513 -32.498,-19.635 -36.015,-28.960" /><polygon points="-36.015,-28.960 -32.498,-19.635 -40.355,-20.036" /><polygon points="41.111,-22.309 31.198,-19.054 36.534,-26.690" /><polygon points="-32.379,-14.064 -33.440,-9.472 -44.403,-15.065" /><polygon points="-40.355,-20.036 -32.379,-14.064 -44.403,-15.065" /><polygon points="-14.325,37.653 -2.580,39.957 -13.080,45.337" /><polygon points="-2.580,39.957 -14.325,37.653 -5.127,34.590" /><polygon points="-38.414,2.853 -33.440,-9.472 -30.574,-4.379" /><polygon points="-33.440,-9.472 -38.414,2.853 -47.097,-2.811" /><polygon points="-33.440,-9.472 -48.385,-9.474 -44.403,-15.065" /><polygon points="33.708,29.479 36.191,19.873 38.196,28.197" /><polygon points="27.281,-22.153 23.370,-24.834 34.578,-33.174" /><polygon points="36.534,-26.690 27.281,-22.153 34.578,-33.174" /><polygon points="23.370,-24.834 30.695,-38.556 34.578,-33.174" /><polygon points="-16.827,-29.955 -23.911,-26.814 -26.913,-39.808" /><polygon points="-15.200,-38.268 -16.827,-29.955 -26.913,-39.808" /><polygon points="-26.913,-39.808 -23.911,-26.814 -31.058,-29.513" /><polygon points="10.861,31.025 22.112,35.812 19.791,44.389" /><polygon points="7.063,34.313 10.861,31.025 19.791,44.389" /><polygon points="7.063,34.313 13.749,46.814 6.950,45.395" /><polygon points="-38.414,2.853 -38.120,14.926 -48.032,7.594" /><polygon points="-47.097,-2.811 -38.414,2.853 -48.032,7.594" /><polygon points="-38.120,14.926 -38.414,2.853 -34.249,10.241" /><polygon points="13.749,46.814 7.063,34.313 19.791,44.389" /><polygon points="-36.015,-28.960 -40.355,-20.036 -40.476,-27.371" /><polygon points="14.058,-42.108 3.973,-37.403 11.630,-47.614" /><polygon points="11.630,-47.614 3.973,-37.403 0.986,-42.580" /><polygon points="0.986,-42.580 -4.482,-40.359 -2.916,-48.940" /><polygon points="-40.091,19.657 -33.803,22.614 -42.405,25.333" /><polygon points="-48.385,-9.474 -33.440,-9.472 -47.097,-2.811" /><polygon points="36.191,19.873 38.182,14.740 45.122,20.124" /><polygon points="36.191,19.873 43.075,26.603 38.196,28.197" /><polygon points="30.695,-38.556 23.370,-24.834 23.083,-38.629" /><polygon points="-13.080,45.337 -2.580,39.957 -9.884,49.206" /><polygon points="-9.884,49.206 -2.580,39.957 0.827,45.518" /><polygon points="-33.803,22.614 -31.059,27.426 -39.406,31.365" /><polygon points="-42.405,25.333 -33.803,22.614 -39.406,31.365" /><polygon points="43.075,26.603 36.191,19.873 45.122,20.124" /><polygon points="-20.511,31.309 -14.325,37.653 -24.401,44.954" /><polygon points="-27.466,31.717 -20.511,31.309 -24.401,44.954" /><polygon points="-24.401,44.954 -14.325,37.653 -13.080,45.337" /><polygon points="11.630,-47.614 0.986,-42.580 7.971,-50.387" /><polygon points="0.986,-42.580 3.914,-52.626 7.971,-50.387" /><polygon points="44.085,-5.113 39.175,-17.238 50.565,-10.152" /><polygon points="39.175,-17.238 44.085,-5.113 36.612,-12.617" /><polygon points="-9.884,49.206 0.827,45.518 -5.971,51.437" /><polygon points="-10.139,-40.666 -2.916,-48.940 -4.482,-40.359" /><polygon points="-2.916,-48.940 -10.139,-40.666 -9.928,-50.734" /><polygon points="22.112,35.812 27.861,28.758 31.215,42.293" /><polygon points="19.791,44.389 22.112,35.812 31.215,42.293" /><polygon points="31.215,42.293 27.861,28.758 33.708,29.479" /><polygon points="41.111,-22.309 36.534,-26.690 44.068,-28.862" /><polygon points="3.914,-52.626 0.986,-42.580 -2.916,-48.940" /><polygon points="-40.476,-27.371 -40.355,-20.036 -46.190,-25.954" /><polygon points="0.827,45.518 6.950,45.395 5.356,52.933" /><polygon points="0.827,45.518 -1.784,54.196 -5.971,51.437" /><polygon points="40.993,9.801 45.122,20.124 38.182,14.740" /><polygon points="45.122,20.124 40.993,9.801 51.012,14.988" /><polygon points="-48.032,7.594 -38.120,14.926 -51.946,12.682" /><polygon points="-51.946,12.682 -38.120,14.926 -40.091,19.657" /><polygon points="39.489,3.999 44.085,-5.113 53.548,0.979" /><polygon points="40.993,9.801 39.489,3.999 53.548,0.979" /><polygon points="40.993,9.801 55.511,8.504 51.012,14.988" /><polygon points="53.548,0.979 44.085,-5.113 50.565,-10.152" /><polygon points="-1.784,54.196 0.827,45.518 5.356,52.933" /><polygon points="-27.466,31.717 -39.406,31.365 -31.059,27.426" /><polygon points="-39.406,31.365 -27.466,31.717 -37.766,39.356" /><polygon points="-27.466,31.717 -33.071,45.259 -37.766,39.356" /><polygon points="-40.355,-20.036 -44.403,-15.065 -50.547,-20.083" /><polygon points="-46.190,-25.954 -40.355,-20.036 -50.547,-20.083" /><polygon points="50.565,-10.152 39.175,-17.238 52.436,-15.123" /><polygon points="39.175,-17.238 51.839,-19.716 52.436,-15.123" /><polygon points="-51.946,12.682 -40.091,19.657 -52.308,17.457" /><polygon points="-40.091,19.657 -51.297,21.977 -52.308,17.457" /><polygon points="-15.200,-38.268 -26.913,-39.808 -24.542,-49.443" /><polygon points="51.839,-19.716 39.175,-17.238 41.111,-22.309" /><polygon points="41.111,-22.309 50.921,-24.353 51.839,-19.716" /><polygon points="-10.139,-40.666 -15.200,-38.268 -17.861,-52.660" /><polygon points="-9.928,-50.734 -10.139,-40.666 -17.861,-52.660" /><polygon points="-17.861,-52.660 -15.200,-38.268 -24.542,-49.443" /><polygon points="-51.297,21.977 -40.091,19.657 -42.405,25.333" /><polygon points="-26.913,-39.808 -31.058,-29.513 -34.969,-43.496" /><polygon points="-24.542,-49.443 -26.913,-39.808 -34.969,-43.496" /><polygon points="-34.969,-43.496 -31.058,-29.513 -36.015,-28.960" /><polygon points="-33.071,45.259 -27.466,31.717 -24.401,44.954" /><polygon points="43.075,26.603 45.122,20.124 49.980,25.426" /><polygon points="49.980,25.426 45.122,20.124 51.012,14.988" /><polygon points="34.578,-33.174 44.068,-28.862 36.534,-26.690" /><polygon points="44.068,-28.862 34.578,-33.174 42.357,-36.672" /><polygon points="55.511,8.504 40.993,9.801 53.548,0.979" /><polygon points="23.083,-38.629 14.058,-42.108 24.668,-50.298" /><polygon points="30.695,-38.556 23.083,-38.629 24.668,-50.298" /><polygon points="24.668,-50.298 14.058,-42.108 11.630,-47.614" /><polygon points="3.914,-52.626 -2.916,-48.940 -0.474,-56.340" /><polygon points="50.921,-24.353 41.111,-22.309 44.068,-28.862" /><polygon points="-42.405,25.333 -39.406,31.365 -46.119,33.007" /><polygon points="-42.405,25.333 -50.906,26.985 -51.297,21.977" /><polygon points="-46.119,33.007 -39.406,31.365 -37.766,39.356" /><polygon points="-50.906,26.985 -42.405,25.333 -46.119,33.007" /><polygon points="13.749,46.814 5.356,52.933 6.950,45.395" /><polygon points="5.356,52.933 13.749,46.814 13.451,56.284" /><polygon points="31.215,42.293 33.708,29.479 38.146,43.626" /><polygon points="33.708,29.479 42.869,41.540 38.146,43.626" /><polygon points="-2.916,-48.940 -9.928,-50.734 -8.230,-57.502" /><polygon points="-0.474,-56.340 -2.916,-48.940 -8.230,-57.502" /><polygon points="-47.097,-2.811 -48.032,7.594 -58.827,1.356" /><polygon points="-58.827,1.356 -48.032,7.594 -51.946,12.682" /><polygon points="-1.784,54.196 5.356,52.933 2.947,59.190" /><polygon points="2.947,59.190 5.356,52.933 13.451,56.284" /><polygon points="-34.969,-43.496 -36.015,-28.960 -40.758,-42.899" /><polygon points="42.869,41.540 33.708,29.479 38.196,28.197" /><polygon points="46.109,37.824 38.196,28.197 43.075,26.603" /><polygon points="43.075,26.603 49.312,34.051 46.109,37.824" /><polygon points="-24.401,44.954 -13.080,45.337 -24.046,54.632" /><polygon points="-24.046,54.632 -13.080,45.337 -9.884,49.206" /><polygon points="50.921,-24.353 44.068,-28.862 51.386,-30.023" /><polygon points="42.869,41.540 38.196,28.197 46.109,37.824" /><polygon points="-40.476,-27.371 -44.597,-39.777 -36.015,-28.960" /><polygon points="-36.015,-28.960 -44.597,-39.777 -40.758,-42.899" /><polygon points="-47.607,-35.867 -40.476,-27.371 -46.190,-25.954" /><polygon points="-48.385,-9.474 -50.547,-20.083 -44.403,-15.065" /><polygon points="-50.547,-20.083 -48.385,-9.474 -57.895,-14.441" /><polygon points="49.312,34.051 43.075,26.603 49.980,25.426" /><polygon points="-44.597,-39.777 -40.476,-27.371 -47.607,-35.867" /><polygon points="49.980,25.426 51.012,14.988 56.612,19.825" /><polygon points="-46.190,-25.954 -50.547,-20.083 -54.544,-24.967" /><polygon points="-46.190,-25.954 -51.446,-32.471 -47.607,-35.867" /><polygon points="-54.544,-24.967 -50.547,-20.083 -57.895,-14.441" /><polygon points="19.791,44.389 31.215,42.293 29.313,53.394" /><polygon points="29.313,53.394 31.215,42.293 38.146,43.626" /><polygon points="-51.446,-32.471 -46.190,-25.954 -54.544,-24.967" /><polygon points="30.695,-38.556 42.357,-36.672 34.578,-33.174" /><polygon points="42.357,-36.672 30.695,-38.556 40.505,-45.861" /><polygon points="30.695,-38.556 34.567,-51.659 40.505,-45.861" /><polygon points="44.068,-28.862 42.357,-36.672 48.505,-37.840" /><polygon points="51.386,-30.023 44.068,-28.862 48.505,-37.840" /><polygon points="-46.119,33.007 -37.766,39.356 -45.262,42.584" /><polygon points="13.749,46.814 19.791,44.389 22.399,58.039" /><polygon points="13.451,56.284 13.749,46.814 22.399,58.039" /><polygon points="22.399,58.039 19.791,44.389 29.313,53.394" /><polygon points="34.567,-51.659 30.695,-38.556 24.668,-50.298" /><polygon points="24.668,-50.298 11.630,-47.614 22.696,-57.947" /><polygon points="-48.385,-9.474 -58.827,1.356 -62.189,-6.912" /><polygon points="-57.895,-14.441 -48.385,-9.474 -62.189,-6.912" /><polygon points="-58.827,1.356 -48.385,-9.474 -47.097,-2.811" /><polygon points="53.548,0.979 50.565,-10.152 62.892,-4.055" /><polygon points="7.971,-50.387 3.914,-52.626 7.903,-62.728" /><polygon points="7.903,-62.728 3.914,-52.626 -0.474,-56.340" /><polygon points="-9.884,49.206 -5.971,51.437 -10.550,62.717" /><polygon points="-9.884,49.206 -20.782,60.284 -24.046,54.632" /><polygon points="-10.550,62.717 -5.971,51.437 -1.784,54.196" /><polygon points="-10.550,62.717 -1.784,54.196 2.947,59.190" /><polygon points="2.947,59.190 13.451,56.284 11.611,62.569" /><polygon points="-9.884,49.206 -15.910,62.486 -20.782,60.284" /><polygon points="11.630,-47.614 7.971,-50.387 13.238,-62.727" /><polygon points="11.630,-47.614 18.468,-61.743 22.696,-57.947" /><polygon points="13.238,-62.727 7.971,-50.387 7.903,-62.728" /><polygon points="-15.910,62.486 -9.884,49.206 -10.550,62.717" /><polygon points="-17.861,-52.660 -8.230,-57.502 -9.928,-50.734" /><polygon points="-8.230,-57.502 -17.861,-52.660 -17.520,-61.881" /><polygon points="18.468,-61.743 11.630,-47.614 13.238,-62.727" /><polygon points="49.980,25.426 56.612,19.825 59.917,24.485" /><polygon points="-24.542,-49.443 -34.969,-43.496 -33.944,-56.193" /><polygon points="-58.827,1.356 -51.946,12.682 -65.729,6.974" /><polygon points="-54.544,-24.967 -57.895,-14.441 -63.217,-19.246" /><polygon points="55.511,8.504 56.612,19.825 51.012,14.988" /><polygon points="56.612,19.825 55.511,8.504 64.867,13.359" /><polygon points="-46.119,33.007 -45.262,42.584 -51.108,43.384" /><polygon points="-33.071,45.259 -24.046,54.632 -35.164,57.562" /><polygon points="-24.046,54.632 -33.071,45.259 -24.401,44.954" /><polygon points="-52.308,17.457 -51.297,21.977 -63.211,23.338" /><polygon points="-63.211,23.338 -51.297,21.977 -50.906,26.985" /><polygon points="-33.071,45.259 -45.262,42.584 -37.766,39.356" /><polygon points="-45.262,42.584 -33.071,45.259 -42.656,52.521" /><polygon points="-42.656,52.521 -33.071,45.259 -35.164,57.562" /><polygon points="62.892,-4.055 52.436,-15.123 67.376,-9.979" /><polygon points="52.436,-15.123 62.892,-4.055 50.565,-10.152" /><polygon points="52.436,-15.123 51.839,-19.716 64.943,-20.972" /><polygon points="52.436,-15.123 67.565,-15.798 67.376,-9.979" /><polygon points="55.511,8.504 62.892,-4.055 68.211,4.739" /><polygon points="64.867,13.359 55.511,8.504 68.211,4.739" /><polygon points="62.892,-4.055 55.511,8.504 53.548,0.979" /><polygon points="-17.861,-52.660 -24.542,-49.443 -27.252,-62.760" /><polygon points="-17.520,-61.881 -17.861,-52.660 -27.252,-62.760" /><polygon points="-27.252,-62.760 -24.542,-49.443 -33.944,-56.193" /><polygon points="40.505,-45.861 48.505,-37.840 42.357,-36.672" /><polygon points="48.505,-37.840 40.505,-45.861 47.961,-49.014" /><polygon points="-51.946,12.682 -52.308,17.457 -66.484,18.474" /><polygon points="-51.946,12.682 -67.934,12.950 -65.729,6.974" /><polygon points="-66.484,18.474 -52.308,17.457 -63.211,23.338" /><polygon points="-67.934,12.950 -51.946,12.682 -66.484,18.474" /><polygon points="29.313,53.394 38.146,43.626 38.278,57.826" /><polygon points="67.565,-15.798 52.436,-15.123 64.943,-20.972" /><polygon points="-8.230,-57.502 -17.520,-61.881 -15.546,-67.980" /><polygon points="22.399,58.039 11.611,62.569 13.451,56.284" /><polygon points="11.611,62.569 22.399,58.039 22.100,67.311" /><polygon points="34.567,-51.659 22.696,-57.947 34.875,-62.782" /><polygon points="22.696,-57.947 34.567,-51.659 24.668,-50.298" /><polygon points="-33.944,-56.193 -40.758,-42.899 -42.177,-58.328" /><polygon points="-40.758,-42.899 -33.944,-56.193 -34.969,-43.496" /><polygon points="64.867,13.359 59.917,24.485 56.612,19.825" /><polygon points="59.917,24.485 64.867,13.359 70.193,18.237" /><polygon points="-62.189,-6.912 -63.217,-19.246 -57.895,-14.441" /><polygon points="-63.217,-19.246 -62.189,-6.912 -71.694,-11.701" /><polygon points="-62.189,-6.912 -58.827,1.356 -73.371,-2.057" /><polygon points="-71.694,-11.701 -62.189,-6.912 -73.371,-2.057" /><polygon points="-73.371,-2.057 -58.827,1.356 -65.729,6.974" /><polygon points="-40.758,-42.899 -44.597,-39.777 -52.147,-51.528" /><polygon points="-40.758,-42.899 -48.306,-56.346 -42.177,-58.328" /><polygon points="38.278,57.826 38.146,43.626 45.537,57.787" /><polygon points="38.146,43.626 50.487,54.189 45.537,57.787" /><polygon points="40.505,-45.861 34.567,-51.659 44.095,-59.126" /><polygon points="47.961,-49.014 40.505,-45.861 44.095,-59.126" /><polygon points="44.095,-59.126 34.567,-51.659 34.875,-62.782" /><polygon points="22.399,58.039 29.313,53.394 32.270,66.637" /><polygon points="22.100,67.311 22.399,58.039 32.270,66.637" /><polygon points="32.270,66.637 29.313,53.394 38.278,57.826" /><polygon points="50.487,54.189 38.146,43.626 42.869,41.540" /><polygon points="-48.306,-56.346 -40.758,-42.899 -52.147,-51.528" /><polygon points="-42.656,52.521 -51.108,43.384 -45.262,42.584" /><polygon points="-51.108,43.384 -42.656,52.521 -50.273,55.814" /><polygon points="-35.164,57.562 -20.782,60.284 -33.754,67.159" /><polygon points="-20.782,60.284 -35.164,57.562 -24.046,54.632" /><polygon points="11.611,62.569 22.100,67.311 20.044,73.523" /><polygon points="-27.252,-62.760 -15.546,-67.980 -17.520,-61.881" /><polygon points="-15.546,-67.980 -27.252,-62.760 -27.109,-72.334" /><polygon points="68.211,4.739 62.892,-4.055 77.495,-1.030" /><polygon points="77.495,-1.030 62.892,-4.055 67.376,-9.979" /><polygon points="34.875,-62.782 22.696,-57.947 31.892,-70.581" /><polygon points="31.892,-70.581 22.696,-57.947 18.468,-61.743" /><polygon points="-20.782,60.284 -15.910,62.486 -23.235,75.005" /><polygon points="-20.782,60.284 -29.417,73.001 -33.754,67.159" /><polygon points="-29.417,73.001 -20.782,60.284 -23.235,75.005" /><polygon points="64.867,13.359 68.211,4.739 78.118,9.455" /><polygon points="70.193,18.237 64.867,13.359 78.118,9.455" /><polygon points="78.118,9.455 68.211,4.739 77.495,-1.030" /><polygon points="-27.252,-62.760 -33.944,-56.193 -37.285,-69.515" /><polygon points="-27.109,-72.334 -27.252,-62.760 -37.285,-69.515" /><polygon points="-37.285,-69.515 -33.944,-56.193 -42.177,-58.328" /><polygon points="-63.217,-19.246 -71.694,-11.701 -77.312,-16.710" /><polygon points="-42.656,52.521 -35.164,57.562 -44.723,65.446" /><polygon points="-50.273,55.814 -42.656,52.521 -44.723,65.446" /><polygon points="-44.723,65.446 -35.164,57.562 -33.754,67.159" /><polygon points="31.892,-70.581 18.468,-61.743 26.478,-74.443" /><polygon points="-73.371,-2.057 -65.729,6.974 -80.461,4.400" /><polygon points="-80.461,4.400 -65.729,6.974 -67.934,12.950" /><polygon points="47.961,-49.014 44.095,-59.126 52.019,-62.787" /><polygon points="77.495,-1.030 67.376,-9.979 82.216,-7.913" /><polygon points="32.270,66.637 38.278,57.826 42.116,71.284" /><polygon points="42.116,71.284 38.278,57.826 45.537,57.787" /><polygon points="22.100,67.311 32.270,66.637 32.423,76.709" /><polygon points="20.044,73.523 22.100,67.311 32.423,76.709" /><polygon points="32.423,76.709 32.270,66.637 42.116,71.284" /><polygon points="-80.461,4.400 -67.934,12.950 -82.781,11.437" /><polygon points="67.376,-9.979 67.565,-15.798 82.254,-14.850" /><polygon points="82.216,-7.913 67.376,-9.979 82.254,-14.850" /><polygon points="44.095,-59.126 34.875,-62.782 44.483,-71.246" /><polygon points="52.019,-62.787 44.095,-59.126 44.483,-71.246" /><polygon points="44.483,-71.246 34.875,-62.782 31.892,-70.581" /><polygon points="-71.694,-11.701 -73.371,-2.057 -83.875,-6.650" /><polygon points="-77.312,-16.710 -71.694,-11.701 -83.875,-6.650" /><polygon points="-83.875,-6.650 -73.371,-2.057 -80.461,4.400" /><polygon points="-37.285,-69.515 -42.177,-58.328 -46.590,-71.897" /><polygon points="-46.590,-71.897 -42.177,-58.328 -48.306,-56.346" /><polygon points="42.116,71.284 45.537,57.787 50.559,71.376" /><polygon points="-50.273,55.814 -44.723,65.446 -53.045,69.697" /><polygon points="-44.723,65.446 -33.754,67.159 -43.368,76.307" /><polygon points="-53.045,69.697 -44.723,65.446 -43.368,76.307" /><polygon points="-43.368,76.307 -33.754,67.159 -29.417,73.001" /><polygon points="78.118,9.455 77.495,-1.030 88.726,3.355" /><polygon points="88.726,3.355 77.495,-1.030 82.216,-7.913" /><polygon points="-27.109,-72.334 -37.285,-69.515 -37.883,-80.213" /><polygon points="-37.883,-80.213 -37.285,-69.515 -46.590,-71.897" /><polygon points="44.483,-71.246 31.892,-70.581 41.424,-80.454" /><polygon points="20.044,73.523 -23.235,75.005 -10.550,62.717" /><polygon points="-10.550,62.717 11.611,62.569 20.044,73.523" /><polygon points="11.611,62.569 -10.550,62.717 2.947,59.190" /><polygon points="-10.550,62.717 -23.235,75.005 -15.910,62.486" /><polygon points="-23.235,75.005 -43.368,76.307 -29.417,73.001" /><polygon points="50.559,71.376 32.423,76.709 42.116,71.284" /><polygon points="-51.108,43.384 -63.211,23.338 -50.906,26.985" /><polygon points="-51.108,43.384 -50.906,26.985 -46.119,33.007" /><polygon points="49.312,34.051 50.487,54.189 46.109,37.824" /><polygon points="59.917,24.485 50.487,54.189 49.312,34.051" /><polygon points="59.917,24.485 49.312,34.051 49.980,25.426" /><polygon points="-63.211,23.338 -82.781,11.437 -66.484,18.474" /><polygon points="-82.781,11.437 -83.875,-6.650 -80.461,4.400" /><polygon points="82.254,-14.850 88.726,3.355 82.216,-7.913" /><polygon points="-51.446,-32.471 -63.217,-19.246 -77.312,-16.710" /><polygon points="-63.217,-19.246 -51.446,-32.471 -54.544,-24.967" /><polygon points="64.943,-20.972 82.254,-14.850 67.565,-15.798" /><polygon points="64.943,-20.972 50.921,-24.353 51.386,-30.023" /><polygon points="50.921,-24.353 64.943,-20.972 51.839,-19.716" /><polygon points="-77.312,-16.710 -52.147,-51.528 -51.446,-32.471" /><polygon points="64.943,-20.972 48.505,-37.840 47.961,-49.014" /><polygon points="48.505,-37.840 64.943,-20.972 51.386,-30.023" /><polygon points="52.019,-62.787 64.943,-20.972 47.961,-49.014" /><polygon points="-51.446,-32.471 -52.147,-51.528 -47.607,-35.867" /><polygon points="-52.147,-51.528 -46.590,-71.897 -48.306,-56.346" /><polygon points="26.478,-74.443 41.424,-80.454 31.892,-70.581" /><polygon points="13.238,-62.727 26.478,-74.443 18.468,-61.743" /><polygon points="-8.230,-57.502 7.903,-62.728 -0.474,-56.340" /><polygon points="7.903,-62.728 26.478,-74.443 13.238,-62.727" /><polygon points="7.903,-62.728 -8.230,-57.502 -15.546,-67.980" /><polygon points="-15.546,-67.980 26.478,-74.443 7.903,-62.728" /><polygon points="-47.607,-35.867 -52.147,-51.528 -44.597,-39.777" /><polygon points="-66.484,18.474 -82.781,11.437 -67.934,12.950" /><polygon points="50.487,54.189 50.559,71.376 45.537,57.787" /><polygon points="46.109,37.824 50.487,54.189 42.869,41.540" /></g><circle cx="2.725" cy="1.152" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.151" cy="1.832" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-7.062" cy="-4.577" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="2.296" cy="8.841" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="4.381" cy="-8.263" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.871" cy="-11.004" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="12.662" cy="-2.129" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.061" cy="9.376" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-1.328" cy="13.781" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.795" cy="11.271" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-12.730" cy="7.946" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="11.118" cy="-10.012" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="15.653" cy="9.251" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.299" cy="-0.156" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.136" cy="-6.424" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="16.988" cy="-6.844" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="19.455" cy="3.613" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="5.257" cy="-18.911" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-12.082" cy="-16.041" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="2.479" cy="20.498" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="1.160" cy="-21.180" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="18.639" cy="-12.734" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.179" cy="-14.730" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.764" cy="-22.226" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.842" cy="21.318" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-22.227" cy="10.040" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-21.527" cy="-11.549" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.366" cy="5.140" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-18.228" cy="17.833" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-11.113" cy="23.231" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.355" cy="26.458" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-6.151" cy="26.292" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="25.663" cy="8.429" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="21.877" cy="17.243" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="16.282" cy="-22.745" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.520" cy="-27.711" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.217" cy="17.311" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="25.767" cy="-12.947" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="18.914" cy="23.062" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="26.909" cy="13.035" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="30.454" cy="-1.000" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-30.574" cy="-4.379" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.868" cy="-31.086" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="30.485" cy="-7.759" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-25.655" cy="-18.555" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="5.931" cy="-31.093" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="-28.602" cy="15.885" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="10.861" cy="31.025" r="1.600" fill="#f8d9a1" opacity="0.85"/><circle cx="23.370" cy="-24.834" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-16.827" cy="-29.955" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.548" cy="-31.614" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="29.075" cy="19.251" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.440" cy="-9.472" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.127" cy="34.590" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="7.063" cy="34.313" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="27.281" cy="-22.153" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.379" cy="-14.064" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="2.472" cy="35.627" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.249" cy="10.241" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.911" cy="-26.814" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.198" cy="-19.054" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-20.511" cy="31.309" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="3.973" cy="-37.403" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-32.498" cy="-19.635" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-38.414" cy="2.853" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="36.612" cy="-12.617" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="39.489" cy="3.999" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.580" cy="39.957" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="27.861" cy="28.758" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-14.325" cy="37.653" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.803" cy="22.614" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-4.482" cy="-40.359" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.182" cy="14.740" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-38.120" cy="14.926" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="36.191" cy="19.873" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-15.200" cy="-38.268" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-31.059" cy="27.426" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.466" cy="31.717" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.112" cy="35.812" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-10.139" cy="-40.666" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="40.993" cy="9.801" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="0.986" cy="-42.580" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="39.175" cy="-17.238" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-31.058" cy="-29.513" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.085" cy="-5.113" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="14.058" cy="-42.108" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-40.091" cy="19.657" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="33.708" cy="29.479" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-40.355" cy="-20.036" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="23.083" cy="-38.629" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="36.534" cy="-26.690" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="0.827" cy="45.518" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="6.950" cy="45.395" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-36.015" cy="-28.960" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="41.111" cy="-22.309" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.403" cy="-15.065" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-13.080" cy="45.337" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.097" cy="-2.811" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.196" cy="28.197" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.578" cy="-33.174" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-26.913" cy="-39.808" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="19.791" cy="44.389" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-48.032" cy="7.594" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.749" cy="46.814" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-40.476" cy="-27.371" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.630" cy="-47.614" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-2.916" cy="-48.940" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.405" cy="25.333" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-48.385" cy="-9.474" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.122" cy="20.124" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="30.695" cy="-38.556" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.884" cy="49.206" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-39.406" cy="31.365" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="43.075" cy="26.603" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.401" cy="44.954" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="7.971" cy="-50.387" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.565" cy="-10.152" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-5.971" cy="51.437" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-9.928" cy="-50.734" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.215" cy="42.293" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.068" cy="-28.862" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="3.914" cy="-52.626" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.190" cy="-25.954" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="5.356" cy="52.933" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="51.012" cy="14.988" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.946" cy="12.682" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="53.548" cy="0.979" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-1.784" cy="54.196" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-37.766" cy="39.356" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.547" cy="-20.083" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="52.436" cy="-15.123" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.308" cy="17.457" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.542" cy="-49.443" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="51.839" cy="-19.716" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.861" cy="-52.660" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.297" cy="21.977" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-34.969" cy="-43.496" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.071" cy="45.259" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.980" cy="25.426" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.357" cy="-36.672" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="55.511" cy="8.504" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="24.668" cy="-50.298" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-0.474" cy="-56.340" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.921" cy="-24.353" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.119" cy="33.007" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.906" cy="26.985" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.451" cy="56.284" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.146" cy="43.626" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-8.230" cy="-57.502" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-58.827" cy="1.356" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="2.947" cy="59.190" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-40.758" cy="-42.899" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="46.109" cy="37.824" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-24.046" cy="54.632" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="51.386" cy="-30.023" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.869" cy="41.540" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-47.607" cy="-35.867" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-57.895" cy="-14.441" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="49.312" cy="34.051" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.597" cy="-39.777" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="56.612" cy="19.825" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-54.544" cy="-24.967" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="29.313" cy="53.394" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.446" cy="-32.471" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="40.505" cy="-45.861" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="48.505" cy="-37.840" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-45.262" cy="42.584" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.399" cy="58.039" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.567" cy="-51.659" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.696" cy="-57.947" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-62.189" cy="-6.912" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="62.892" cy="-4.055" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="7.903" cy="-62.728" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-10.550" cy="62.717" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="11.611" cy="62.569" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-20.782" cy="60.284" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="13.238" cy="-62.727" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-15.910" cy="62.486" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-17.520" cy="-61.881" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="18.468" cy="-61.743" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="59.917" cy="24.485" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.944" cy="-56.193" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-65.729" cy="6.974" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.217" cy="-19.246" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.867" cy="13.359" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-51.108" cy="43.384" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-35.164" cy="57.562" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-63.211" cy="23.338" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.656" cy="52.521" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.376" cy="-9.979" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="64.943" cy="-20.972" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="68.211" cy="4.739" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.252" cy="-62.760" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="47.961" cy="-49.014" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-66.484" cy="18.474" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-67.934" cy="12.950" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="38.278" cy="57.826" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="67.565" cy="-15.798" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-15.546" cy="-67.980" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="22.100" cy="67.311" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="34.875" cy="-62.782" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-42.177" cy="-58.328" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="70.193" cy="18.237" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-71.694" cy="-11.701" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-73.371" cy="-2.057" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-52.147" cy="-51.528" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="45.537" cy="57.787" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.095" cy="-59.126" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="32.270" cy="66.637" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.487" cy="54.189" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-48.306" cy="-56.346" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-50.273" cy="55.814" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-33.754" cy="67.159" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="20.044" cy="73.523" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-27.109" cy="-72.334" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="77.495" cy="-1.030" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="31.892" cy="-70.581" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-23.235" cy="75.005" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-29.417" cy="73.001" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="78.118" cy="9.455" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-37.285" cy="-69.515" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-77.312" cy="-16.710" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-44.723" cy="65.446" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="26.478" cy="-74.443" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-80.461" cy="4.400" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="52.019" cy="-62.787" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="82.216" cy="-7.913" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="42.116" cy="71.284" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="32.423" cy="76.709" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-82.781" cy="11.437" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="82.254" cy="-14.850" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="44.483" cy="-71.246" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-83.875" cy="-6.650" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-46.590" cy="-71.897" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="50.559" cy="71.376" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-53.045" cy="69.697" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-43.368" cy="76.307" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="88.726" cy="3.355" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="-37.883" cy="-80.213" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle cx="41.424" cy="-80.454" r="0.850" fill="#f8d9a1" opacity="0.85"/><circle r="9" fill="#061019" stroke="#dabb83" stroke-width="0.8"/><circle r="2" fill="#ffe5a2"/></g><g fill="#ddcdaa"><text x="83" y="93" font-family="Helvetica, sans-serif" font-size="13" letter-spacing="4">MOONLIGHT / BOTANICAL STUDIES</text><path d="M 83 117 H 418" stroke="#9c987d" stroke-width="0.65"/><text x="78" y="239" font-family="Georgia, serif" font-size="71" letter-spacing="-3">Noctilucent</text><text x="81" y="315" font-family="Georgia, serif" font-size="76" font-style="italic">garden.</text></g><g fill="#9cb4b7" font-family="Georgia, serif" font-size="20"><text x="85" y="371">Flowers for the hours</text><text x="85" y="401">when no one is looking.</text></g><g fill="#a7b0a2" font-family="Helvetica, sans-serif" font-size="11" letter-spacing="2"><text x="85" y="1111">I / PHYLLOTAXIS</text><text x="85" y="1135">II / DELAUNAY</text><text x="85" y="1159">III / VORONOI</text><text x="1515" y="1159" text-anchor="end">NATIVE GEOMETRY / STUDY 01</text></g></svg>
diff --git a/docs/persistence-rose/Main.hs b/docs/persistence-rose/Main.hs
--- a/docs/persistence-rose/Main.hs
+++ b/docs/persistence-rose/Main.hs
@@ -11,7 +11,7 @@
 import Data.ByteString (ByteString)
 import Data.ByteString qualified as ByteString
 import Data.Foldable (traverse_)
-import Moonlight.Triangulation.Exhibit.PersistenceRose
+import Moonlight.Planar.Exhibit.PersistenceRose
   ( PersistenceRoseError,
     PersistenceRoseScene,
     alphaEclipseReceipt,
diff --git a/docs/persistence-rose/Moonlight/Planar/Exhibit/PersistenceRose.hs b/docs/persistence-rose/Moonlight/Planar/Exhibit/PersistenceRose.hs
new file mode 100644
--- /dev/null
+++ b/docs/persistence-rose/Moonlight/Planar/Exhibit/PersistenceRose.hs
@@ -0,0 +1,1591 @@
+{-# LANGUAGE NamedFieldPuns #-}
+
+-- | A deterministic, pure observatory assembled from Moonlight's existing
+-- geometry, exact filtration, interpolation, and persistence owners.
+module Moonlight.Planar.Exhibit.PersistenceRose
+  ( PersistenceRoseError (..),
+    PersistenceRoseReceipt (..),
+    PersistenceRoseScene,
+    PersistenceRoseFrame,
+    PersistenceRoseAnimationReceipt (..),
+    AlphaEclipseReceipt (..),
+    AlphaEclipseScene,
+    buildPersistenceRoseScene,
+    buildAlphaEclipseScene,
+    persistenceRoseReceipt,
+    alphaEclipseReceipt,
+    renderPersistenceRoseReceipt,
+    renderAlphaEclipseReceipt,
+    renderPersistenceRosePoster,
+    renderAlphaEclipse,
+    persistenceRoseFrameSpecs,
+    renderPersistenceRoseFrame,
+    persistenceRoseAnimationReceipt,
+    renderPersistenceRoseAnimationReceipt,
+    stampPersistenceRoseGif,
+    verifyPersistenceRoseGif,
+  )
+where
+
+import Control.Monad (foldM)
+import Control.Monad.ST (runST)
+import Data.Bifunctor (first)
+import Data.Bits ((.&.), xor)
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as ByteString
+import Data.ByteString.Char8 qualified as ByteString.Char8
+import Data.List (find, intercalate, sortOn)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Maybe (mapMaybe)
+import Data.Ord (Down (..))
+import Data.Set qualified as Set
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified as Text.Encoding
+import Data.Vector qualified as Vector
+import Data.Word (Word64, Word8)
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..),
+    HomologyFailure,
+    PersistencePair (..),
+  )
+import Moonlight.Homology.Persistence
+  ( criticalBettiVectors,
+    mod2PersistentPairsWithCriticalBettiTable,
+  )
+import Moonlight.Planar.Alpha (AlphaBirth, AlphaFiltration, AlphaFiltrationError, alphaBirthDenominator, alphaBirthNumerator, alphaBirthToDouble, alphaEdgeBirth, alphaFaceBirth, alphaFiltration, alphaFiltrationCriticalBirths)
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Convex (ConvexError, convexPolygon)
+import Moonlight.Planar.Dcel (innerFaceVertexTriples, undirectedEndpoints, vertexPoint)
+import Moonlight.Planar.Exact (exactPointFromPoint, exactPointToEmbeddingCandidate)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces, undirectedEdges, vertices)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Minkowski (MinkowskiError, polygonOffset, structuringElement)
+import Moonlight.Planar.Overlay (OverlayError, overlayLayers, overlaySelectedRegion)
+import Moonlight.Planar.Region (ExactLoop, PlanarRegion, PolygonComponent, RegionPublicationError, RegionValidationError, exactLoop, exactLoopPoints, planarLayer, planarRegion, planarRegionComponents, polygonComponent, polygonHoleLoops, polygonOuterLoop)
+import Moonlight.Planar.Types (BuildError, DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..), PointValidationError, QueryPoint, queryPointValue)
+import Moonlight.Planar.CellComplex
+  ( DCELError,
+    filteredAlphaComplex,
+  )
+import Moonlight.Planar.FloodFillIterator
+  ( BoundaryObstruction,
+    FaceComponent,
+    boundaryLoopVertices,
+    componentBoundaryLoops,
+    faceComponentFaces,
+    faceComponentsBy,
+    outerFaceFloodFill,
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..),
+    UndirectedEdgeId,
+    VertexId,
+    normalizedDirected,
+  )
+import Moonlight.Planar.Interpolation
+  ( InterpolationStats (..),
+    NaturalNeighborResult (..),
+    naturalNeighborWeights,
+    newNaturalNeighborWorkspace,
+  )
+import Moonlight.Planar.Voronoi
+  ( VoronoiEdgeGeometry (..),
+    asDirectedVoronoiEdge,
+    voronoiEdgeGeometry,
+  )
+import Numeric (showFFloat, showHex)
+
+-- | Every boundary at which the exhibit may be refused. The renderer never
+-- invents a fallback geometry or topology state.
+data PersistenceRoseError
+  = RoseDelaunayBuildFailed !BuildError
+  | RoseAlphaFiltrationFailed !AlphaFiltrationError
+  | RoseCellComplexLoweringFailed !DCELError
+  | RosePersistenceFailed !HomologyFailure
+  | RoseCriticalProfileCountMismatch !Int !Int
+  | RoseCriticalProfileDegreeCountMismatch !Int !Int
+  | RoseBirthRankMissing !AlphaBirth
+  | RoseEdgeBirthMissing !UndirectedEdgeId
+  | RoseFaceBirthMissing !FaceId
+  | RoseBoundaryDescentFailed !Int !BoundaryObstruction
+  | RoseNoCriticalStates
+  | RoseNoMaskRegions
+  | RoseNoPersistentHole
+  | RoseNoCoherentHero !Int ![(Int, Int)]
+  | RoseVoronoiGeometryMissing !UndirectedEdgeId
+  | RoseNaturalNeighborFallbacks !Int
+  | RosePointInvalid !PointValidationError
+  | RoseRegionInvalid !RegionValidationError
+  | RoseOverlayFailed !OverlayError
+  | RoseOverlayPublicationFailed !RegionPublicationError
+  | RoseMinkowskiFailed !MinkowskiError
+  | RoseConvexFailed !ConvexError
+  | RoseFrameCountTooSmall !Int
+  | RoseEclipseFiniteHoleCountMismatch !Int
+  | RoseEclipseFiniteHoleMissingDeath
+  | RoseEclipseBettiProfileMismatch !Int !Int !Int
+  | RoseGifInvalidSignature
+  | RoseGifTruncated !Int
+  | RoseGifUnexpectedBlock !Int !Word8
+  | RoseGifDimensionMismatch !Int !Int !Int !Int
+  | RoseGifFrameCountMismatch !Int !Int
+  | RoseGifFrameDelayCountMismatch !Int !Int
+  | RoseGifDurationMismatch !Int !Int
+  | RoseGifProvenanceMismatch
+  | RoseGifMarkerTooLong !Int
+  deriving stock (Show)
+
+-- | A compact, test-readable projection of everything the scene actually
+-- computed. It is intentionally not another geometry representation.
+data PersistenceRoseReceipt = PersistenceRoseReceipt
+  { roseSiteCount :: !Int,
+    roseEdgeCount :: !Int,
+    roseFaceCount :: !Int,
+    roseCriticalBirthCount :: !Int,
+    roseFinitePersistentHoleCount :: !Int,
+    roseEssentialPersistentHoleCount :: !Int,
+    roseHeroCriticalRank :: !Int,
+    roseHeroBettiOne :: !Int,
+    roseHeroGeometricHoleCount :: !Int,
+    roseVoronoiSegmentCount :: !Int,
+    roseNaturalNeighborSampleCount :: !Int,
+    roseNaturalNeighborFallbackCount :: !Int,
+    roseMaskComponentCount :: !Int
+  }
+  deriving stock (Eq, Show)
+
+data PersistenceRoseScene = PersistenceRoseScene
+  { sceneSites :: ![Point],
+    sceneEdges :: ![RankedSegment],
+    sceneFaces :: ![RankedTriangle],
+    sceneCriticalStates :: !(NonEmpty CriticalState),
+    sceneHeroState :: !CriticalState,
+    sceneVoronoiSegments :: ![Segment],
+    sceneFieldSamples :: ![FieldSample],
+    sceneMaskComponents :: ![MaskComponent],
+    scenePersistenceArcs :: ![PersistenceArc],
+    sceneReceipt :: !PersistenceRoseReceipt
+  }
+
+-- | The canonical source projection carried inside the retained GIF. The GIF
+-- codec remains an effect-boundary view; this receipt binds that binary view
+-- to the exact deterministic SVG frame section used to produce it.
+data PersistenceRoseAnimationReceipt = PersistenceRoseAnimationReceipt
+  { roseAnimationFrameCount :: !Int,
+    roseAnimationFramesPerSecond :: !Int,
+    roseAnimationWidth :: !Int,
+    roseAnimationHeight :: !Int,
+    roseAnimationSourceFingerprint :: !Word64
+  }
+  deriving stock (Eq, Show)
+
+-- | The exact, test-readable facts shown by the alpha eclipse. The numerator
+-- and denominator fields are projections of opaque 'AlphaBirth' values; the
+-- renderer never chooses thresholds independently of the filtration.
+data AlphaEclipseReceipt = AlphaEclipseReceipt
+  { eclipseSiteCount :: !Int,
+    eclipseEdgeCount :: !Int,
+    eclipseFaceCount :: !Int,
+    eclipseHoleBirthNumerator :: !Integer,
+    eclipseHoleBirthDenominator :: !Integer,
+    eclipseHoleDeathNumerator :: !Integer,
+    eclipseHoleDeathDenominator :: !Integer,
+    eclipseBettiBeforeBirth :: !Int,
+    eclipseBettiAtBirth :: !Int,
+    eclipseBettiAtDeath :: !Int
+  }
+  deriving stock (Eq, Show)
+
+-- | A three-section observation of one fixed resident Delaunay DCEL. Every
+-- displayed edge and face is projected from that DCEL with its exact birth
+-- rank; only the selected sublevel differs between sections.
+data AlphaEclipseScene = AlphaEclipseScene
+  { eclipseSites :: ![Point],
+    eclipseEdges :: ![RankedSegment],
+    eclipseFaces :: ![RankedTriangle],
+    eclipseBeforeState :: !CriticalState,
+    eclipseBirthState :: !CriticalState,
+    eclipseDeathState :: !CriticalState,
+    eclipseReceipt :: !AlphaEclipseReceipt
+  }
+
+-- | The shared local descent used by both exhibits. This is not a second
+-- filtration carrier: it retains the canonical mesh and filtration beside
+-- their checked Homology projection and only the ranked rendering views.
+data AlphaProjection = AlphaProjection
+  { projectionTriangulation :: !(DelaunayTriangulation ()),
+    projectionPersistencePairs :: ![PersistencePair AlphaBirth],
+    projectionCriticalStates :: !(NonEmpty CriticalState),
+    projectionBirthRanks :: !(Map AlphaBirth Int),
+    projectionEdges :: ![RankedSegment],
+    projectionFaces :: ![RankedTriangle]
+  }
+
+newtype PersistenceRoseFrame = PersistenceRoseFrame
+  { frameProgress :: Double
+  }
+  deriving stock (Eq, Show)
+
+data CriticalState = CriticalState
+  { criticalRank :: !Int,
+    criticalBirth :: !AlphaBirth,
+    criticalBettiOne :: !Int,
+    criticalHoles :: ![HoleComponent]
+  }
+
+data RankedSegment = RankedSegment
+  { rankedSegmentGeometry :: !Segment,
+    rankedSegmentBirthRank :: !Int
+  }
+
+data RankedTriangle = RankedTriangle
+  { rankedTrianglePoints :: !(Point, Point, Point),
+    rankedTriangleBirthRank :: !Int
+  }
+
+data Segment = Segment !Point !Point
+
+newtype HoleComponent = HoleComponent [[Point]]
+
+data FieldSample = FieldSample
+  { fieldSamplePoint :: !Point,
+    fieldSampleValue :: !Double,
+    fieldSampleNeighborCount :: !Int
+  }
+
+data MaskComponent = MaskComponent
+  { maskOuterLoop :: ![Point],
+    maskHoleLoops :: ![[Point]]
+  }
+
+data PersistenceArc = PersistenceArc
+  { persistenceArcBirthRank :: !Int,
+    persistenceArcDeathRank :: !Int
+  }
+
+buildPersistenceRoseScene :: Either PersistenceRoseError PersistenceRoseScene
+buildPersistenceRoseScene = do
+  projection <- buildAlphaProjection roseSites
+  let triangulation = projectionTriangulation projection
+      criticalStates = projectionCriticalStates projection
+      persistencePairs = projectionPersistencePairs projection
+      criticalBirthCount = NonEmpty.length criticalStates
+  heroState <- chooseHeroState criticalStates
+  finiteVoronoiSegments <- projectFiniteVoronoiSegments triangulation
+  (fieldSamples, fallbackCount) <- naturalNeighborField triangulation
+  if fallbackCount == 0
+    then pure ()
+    else Left (RoseNaturalNeighborFallbacks fallbackCount)
+  maskComponents <- buildRoseMask
+  persistenceArcs <-
+    projectPersistenceArcs
+      (projectionBirthRanks projection)
+      criticalBirthCount
+      persistencePairs
+  let finiteHoles = filter isFinitePositiveHole persistencePairs
+      essentialHoles = filter isEssentialHole persistencePairs
+      heroHoleCount = length (criticalHoles heroState)
+      receipt =
+        PersistenceRoseReceipt
+          { roseSiteCount = length (vertices triangulation),
+            roseEdgeCount = length (projectionEdges projection),
+            roseFaceCount = length (projectionFaces projection),
+            roseCriticalBirthCount = criticalBirthCount,
+            roseFinitePersistentHoleCount = length finiteHoles,
+            roseEssentialPersistentHoleCount = length essentialHoles,
+            roseHeroCriticalRank = criticalRank heroState,
+            roseHeroBettiOne = criticalBettiOne heroState,
+            roseHeroGeometricHoleCount = heroHoleCount,
+            roseVoronoiSegmentCount = length finiteVoronoiSegments,
+            roseNaturalNeighborSampleCount = length fieldSamples,
+            roseNaturalNeighborFallbackCount = fallbackCount,
+            roseMaskComponentCount = length maskComponents
+          }
+  pure
+    PersistenceRoseScene
+      { sceneSites = fmap (vertexPoint triangulation) (vertices triangulation),
+        sceneEdges = projectionEdges projection,
+        sceneFaces = projectionFaces projection,
+        sceneCriticalStates = criticalStates,
+        sceneHeroState = heroState,
+        sceneVoronoiSegments = finiteVoronoiSegments,
+        sceneFieldSamples = fieldSamples,
+        sceneMaskComponents = maskComponents,
+        scenePersistenceArcs = persistenceArcs,
+        sceneReceipt = receipt
+      }
+
+buildAlphaProjection :: [Point] -> Either PersistenceRoseError AlphaProjection
+buildAlphaProjection sites = do
+  triangulation <- first RoseDelaunayBuildFailed (delaunayGeometry (Vector.fromList sites))
+  filtration <- first RoseAlphaFiltrationFailed (alphaFiltration triangulation)
+  filteredComplex <- first RoseCellComplexLoweringFailed (filteredAlphaComplex filtration)
+  (persistencePairs, criticalBettiTable) <-
+    first RosePersistenceFailed
+      (mod2PersistentPairsWithCriticalBettiTable filteredComplex)
+  let criticalBirths = alphaFiltrationCriticalBirths filtration
+      criticalProfiles = criticalBettiVectors criticalBettiTable
+      birthRanks = Map.fromDistinctAscList (zip criticalBirths [0 ..])
+  if length criticalBirths == length criticalProfiles
+    then pure ()
+    else Left (RoseCriticalProfileCountMismatch (length criticalBirths) (length criticalProfiles))
+  criticalStates <-
+    traverse
+      (buildCriticalState triangulation filtration)
+      (zip3 [0 ..] criticalBirths criticalProfiles)
+      >>= maybe (Left RoseNoCriticalStates) Right . NonEmpty.nonEmpty
+  rankedEdges <-
+    traverse
+      (projectRankedEdge triangulation filtration birthRanks)
+      (undirectedEdges triangulation)
+  rankedFaces <-
+    traverse
+      (projectRankedFace triangulation filtration birthRanks)
+      (zip [1 ..] (Vector.toList (innerFaceVertexTriples triangulation)))
+  pure
+    AlphaProjection
+      { projectionTriangulation = triangulation,
+        projectionPersistencePairs = persistencePairs,
+        projectionCriticalStates = criticalStates,
+        projectionBirthRanks = birthRanks,
+        projectionEdges = rankedEdges,
+        projectionFaces = rankedFaces
+      }
+
+buildAlphaEclipseScene :: Either PersistenceRoseError AlphaEclipseScene
+buildAlphaEclipseScene = do
+  projection <- buildAlphaProjection alphaEclipseSites
+  finiteHoles <-
+    case filter isFinitePositiveHole (projectionPersistencePairs projection) of
+      [finiteHole] -> Right finiteHole
+      holes -> Left (RoseEclipseFiniteHoleCountMismatch (length holes))
+  deathBirth <-
+    maybe
+      (Left RoseEclipseFiniteHoleMissingDeath)
+      Right
+      (persistenceDeath finiteHoles)
+  let birth = persistenceBirth finiteHoles
+      birthRanks = projectionBirthRanks projection
+      states = projectionCriticalStates projection
+  birthRank <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  deathRank <- maybe (Left (RoseBirthRankMissing deathBirth)) Right (Map.lookup deathBirth birthRanks)
+  let beforeState = criticalStateAt (max 0 (birthRank - 1)) states
+      birthState = criticalStateAt birthRank states
+      deathState = criticalStateAt deathRank states
+      bettiProfile =
+        ( criticalBettiOne beforeState,
+          criticalBettiOne birthState,
+          criticalBettiOne deathState
+        )
+  case bettiProfile of
+    (0, 1, 0) -> pure ()
+    (beforeBetti, birthBetti, deathBetti) ->
+      Left (RoseEclipseBettiProfileMismatch beforeBetti birthBetti deathBetti)
+  let triangulation = projectionTriangulation projection
+      receipt =
+        AlphaEclipseReceipt
+          { eclipseSiteCount = length (vertices triangulation),
+            eclipseEdgeCount = length (projectionEdges projection),
+            eclipseFaceCount = length (projectionFaces projection),
+            eclipseHoleBirthNumerator = alphaBirthNumerator birth,
+            eclipseHoleBirthDenominator = alphaBirthDenominator birth,
+            eclipseHoleDeathNumerator = alphaBirthNumerator deathBirth,
+            eclipseHoleDeathDenominator = alphaBirthDenominator deathBirth,
+            eclipseBettiBeforeBirth = criticalBettiOne beforeState,
+            eclipseBettiAtBirth = criticalBettiOne birthState,
+            eclipseBettiAtDeath = criticalBettiOne deathState
+          }
+  pure
+    AlphaEclipseScene
+      { eclipseSites = fmap (vertexPoint triangulation) (vertices triangulation),
+        eclipseEdges = projectionEdges projection,
+        eclipseFaces = projectionFaces projection,
+        eclipseBeforeState = beforeState,
+        eclipseBirthState = birthState,
+        eclipseDeathState = deathState,
+        eclipseReceipt = receipt
+      }
+
+persistenceRoseReceipt :: PersistenceRoseScene -> PersistenceRoseReceipt
+persistenceRoseReceipt = sceneReceipt
+
+alphaEclipseReceipt :: AlphaEclipseScene -> AlphaEclipseReceipt
+alphaEclipseReceipt = eclipseReceipt
+
+renderPersistenceRoseReceipt :: PersistenceRoseReceipt -> String
+renderPersistenceRoseReceipt receipt =
+  intercalate
+    " · "
+    [ "sites " <> show (roseSiteCount receipt),
+      "edges " <> show (roseEdgeCount receipt),
+      "faces " <> show (roseFaceCount receipt),
+      "critical births " <> show (roseCriticalBirthCount receipt),
+      "finite H1 intervals " <> show (roseFinitePersistentHoleCount receipt),
+      "essential H1 intervals " <> show (roseEssentialPersistentHoleCount receipt),
+      "hero rank " <> show (roseHeroCriticalRank receipt),
+      "beta1 " <> show (roseHeroBettiOne receipt),
+      "geometric holes " <> show (roseHeroGeometricHoleCount receipt),
+      "Voronoi segments " <> show (roseVoronoiSegmentCount receipt),
+      "Sibson samples " <> show (roseNaturalNeighborSampleCount receipt),
+      "fallbacks " <> show (roseNaturalNeighborFallbackCount receipt),
+      "mask components " <> show (roseMaskComponentCount receipt)
+    ]
+
+renderAlphaEclipseReceipt :: AlphaEclipseReceipt -> String
+renderAlphaEclipseReceipt receipt =
+  intercalate
+    " · "
+    [ "sites " <> show (eclipseSiteCount receipt),
+      "edges " <> show (eclipseEdgeCount receipt),
+      "faces " <> show (eclipseFaceCount receipt),
+      "H1 ["
+        <> renderExactRatio (eclipseHoleBirthNumerator receipt) (eclipseHoleBirthDenominator receipt)
+        <> ", "
+        <> renderExactRatio (eclipseHoleDeathNumerator receipt) (eclipseHoleDeathDenominator receipt)
+        <> ")",
+      "beta1 "
+        <> show
+          ( eclipseBettiBeforeBirth receipt,
+            eclipseBettiAtBirth receipt,
+            eclipseBettiAtDeath receipt
+          )
+    ]
+
+buildCriticalState
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> (Int, AlphaBirth, [Int])
+  -> Either PersistenceRoseError CriticalState
+buildCriticalState triangulation filtration (rankValue, birthValue, profile) = do
+  bettiOne <-
+    case profile of
+      _ : secondDegree : _ : _ -> Right secondDegree
+      _ -> Left (RoseCriticalProfileDegreeCountMismatch rankValue (length profile))
+  holes <- holeComponentsAt rankValue triangulation filtration birthValue
+  pure
+    CriticalState
+      { criticalRank = rankValue,
+        criticalBirth = birthValue,
+        criticalBettiOne = bettiOne,
+        criticalHoles = holes
+      }
+
+chooseHeroState :: NonEmpty CriticalState -> Either PersistenceRoseError CriticalState
+chooseHeroState states =
+  let stateList = NonEmpty.toList states
+      maximumBettiOne = foldl' (\best state -> max best (criticalBettiOne state)) 0 stateList
+      maximalStates = filter ((== maximumBettiOne) . criticalBettiOne) stateList
+      agreements = fmap (\state -> (criticalBettiOne state, length (criticalHoles state))) maximalStates
+      coherentStates =
+        filter
+          (\state -> criticalBettiOne state > 0 && criticalBettiOne state == length (criticalHoles state))
+          stateList
+      sevenPetalStates = filter ((== rosePetalCount) . criticalBettiOne) coherentStates
+   in if maximumBettiOne <= 0
+        then Left RoseNoPersistentHole
+        else
+          case NonEmpty.nonEmpty sevenPetalStates of
+            Just preferredStates -> Right (latestCriticalState preferredStates)
+            Nothing ->
+              maybe
+                (Left (RoseNoCoherentHero maximumBettiOne agreements))
+                Right
+                (find (\state -> criticalBettiOne state == length (criticalHoles state)) maximalStates)
+
+latestCriticalState :: NonEmpty CriticalState -> CriticalState
+latestCriticalState (firstState :| remainingStates) =
+  foldl'
+    (\latest candidate -> if criticalRank candidate > criticalRank latest then candidate else latest)
+    firstState
+    remainingStates
+
+holeComponentsAt
+  :: Int
+  -> DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> AlphaBirth
+  -> Either PersistenceRoseError [HoleComponent]
+holeComponentsAt rankValue triangulation filtration threshold = do
+  inactiveEdges <-
+    inactiveHandlesAt
+      RoseEdgeBirthMissing
+      (alphaEdgeBirth filtration)
+      (undirectedEdges triangulation)
+      threshold
+  inactiveFaces <-
+    inactiveHandlesAt
+      RoseFaceBirthMissing
+      (alphaFaceBirth filtration)
+      (innerFaces triangulation)
+      threshold
+  let edgeInactive edge = Set.member edge inactiveEdges
+      faceInactive face = Set.member face inactiveFaces
+      exteriorFaces = Set.fromList (outerFaceFloodFill triangulation edgeInactive)
+      inactiveComponents =
+        fmap snd
+          ( faceComponentsBy
+              triangulation
+              (\face -> if faceInactive face then Just () else Nothing)
+              edgeInactive
+          )
+      boundedComponents =
+        filter
+          (all (`Set.notMember` exteriorFaces) . faceComponentFaces)
+          inactiveComponents
+  traverse
+    (projectHoleComponent rankValue triangulation)
+    boundedComponents
+
+inactiveHandlesAt
+  :: Ord handle
+  => (handle -> PersistenceRoseError)
+  -> (handle -> Maybe AlphaBirth)
+  -> [handle]
+  -> AlphaBirth
+  -> Either PersistenceRoseError (Set.Set handle)
+inactiveHandlesAt missingBirth birthAt handles threshold =
+  Set.fromList . mapMaybe id
+    <$> traverse
+      ( \handle ->
+          maybe
+            (Left (missingBirth handle))
+            (\birth -> Right (if birth > threshold then Just handle else Nothing))
+            (birthAt handle)
+      )
+      handles
+
+projectHoleComponent
+  :: Int
+  -> DelaunayTriangulation ()
+  -> FaceComponent
+  -> Either PersistenceRoseError HoleComponent
+projectHoleComponent rankValue triangulation component = do
+  loops <-
+    first (RoseBoundaryDescentFailed rankValue)
+      (componentBoundaryLoops triangulation component)
+  pure
+    ( HoleComponent
+        ( fmap
+            (fmap (vertexPoint triangulation) . NonEmpty.toList . boundaryLoopVertices)
+            (NonEmpty.toList loops)
+        )
+    )
+
+projectRankedEdge
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> Map AlphaBirth Int
+  -> UndirectedEdgeId
+  -> Either PersistenceRoseError RankedSegment
+projectRankedEdge triangulation filtration birthRanks edge = do
+  birth <- maybe (Left (RoseEdgeBirthMissing edge)) Right (alphaEdgeBirth filtration edge)
+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
+  pure
+    RankedSegment
+      { rankedSegmentGeometry = Segment (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex),
+        rankedSegmentBirthRank = rankValue
+      }
+
+projectRankedFace
+  :: DelaunayTriangulation ()
+  -> AlphaFiltration
+  -> Map AlphaBirth Int
+  -> (Int, (VertexId, VertexId, VertexId))
+  -> Either PersistenceRoseError RankedTriangle
+projectRankedFace triangulation filtration birthRanks (rawFace, (firstVertex, secondVertex, thirdVertex)) = do
+  let face = toFaceId rawFace
+  birth <- maybe (Left (RoseFaceBirthMissing face)) Right (alphaFaceBirth filtration face)
+  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+  pure
+    RankedTriangle
+      { rankedTrianglePoints =
+          ( vertexPoint triangulation firstVertex,
+            vertexPoint triangulation secondVertex,
+            vertexPoint triangulation thirdVertex
+          ),
+        rankedTriangleBirthRank = rankValue
+      }
+
+toFaceId :: Int -> FaceId
+toFaceId = FaceId . fromIntegral
+
+projectFiniteVoronoiSegments
+  :: DelaunayTriangulation ()
+  -> Either PersistenceRoseError [Segment]
+projectFiniteVoronoiSegments triangulation =
+  fmap (mapMaybe id)
+    . traverse projectSegment
+    $ undirectedEdges triangulation
+ where
+  projectSegment :: UndirectedEdgeId -> Either PersistenceRoseError (Maybe Segment)
+  projectSegment edge =
+    case voronoiEdgeGeometry triangulation (asDirectedVoronoiEdge (normalizedDirected edge)) of
+      Nothing -> Left (RoseVoronoiGeometryMissing edge)
+      Just (VoronoiSegment fromPoint toPoint) -> Right (Just (Segment fromPoint toPoint))
+      Just (VoronoiRay _ _) -> Right Nothing
+      Just (VoronoiLine _ _) -> Right Nothing
+
+naturalNeighborField
+  :: DelaunayTriangulation ()
+  -> Either PersistenceRoseError ([FieldSample], Int)
+naturalNeighborField triangulation = do
+  admittedQueries <- traverse (first RosePointInvalid . mkQueryPoint) fieldQueryPoints
+  let results = runST $ do
+        workspace <- newNaturalNeighborWorkspace triangulation
+        traverse (naturalNeighborWeights workspace Nothing) admittedQueries
+      pairedResults = zip admittedQueries results
+      fallbackCount = length (filter (interpolationUsedFallback . naturalNeighborStats . snd) pairedResults)
+      samples = mapMaybe (fieldSampleFromResult triangulation) pairedResults
+  pure (samples, fallbackCount)
+
+fieldSampleFromResult
+  :: DelaunayTriangulation ()
+  -> (QueryPoint, NaturalNeighborResult)
+  -> Maybe FieldSample
+fieldSampleFromResult triangulation (query, result)
+  | Vector.null weights = Nothing
+  | interpolationUsedFallback (naturalNeighborStats result) = Nothing
+  | otherwise =
+      let weightedValue =
+            Vector.foldl'
+              (\total (vertex, weight) -> total + weight * siteSignal (vertexPoint triangulation vertex))
+              0
+              weights
+       in Just
+            FieldSample
+              { fieldSamplePoint = queryPointValue query,
+                fieldSampleValue = clamp 0 1 weightedValue,
+                fieldSampleNeighborCount = Vector.length weights
+              }
+ where
+  weights = naturalNeighborValues result
+
+siteSignal :: Point -> Double
+siteSignal (Point x y) =
+  clamp 0 1 (0.5 + 0.24 * sin (1.31 * x - 0.73 * y) + 0.18 * cos (0.61 * x + 1.17 * y))
+
+fieldQueryPoints :: [Point]
+fieldQueryPoints =
+  [ Point (fromIntegral column * 0.34 - 5.1) (fromIntegral row * 0.34 - 4.42)
+  | row <- [0 .. 26 :: Int],
+    column <- [0 .. 30 :: Int]
+  ]
+
+projectPersistenceArcs
+  :: Map AlphaBirth Int
+  -> Int
+  -> [PersistencePair AlphaBirth]
+  -> Either PersistenceRoseError [PersistenceArc]
+projectPersistenceArcs birthRanks criticalCount pairs = do
+  arcs <- traverse pairArc (filter isPositiveHole pairs)
+  pure (take 36 (sortOn (Down . persistenceArcLifetime) arcs))
+ where
+  finalRank = max 0 (criticalCount - 1)
+  pairArc pair = do
+    birthRank <- rankOf (persistenceBirth pair)
+    deathRank <- maybe (Right finalRank) rankOf (persistenceDeath pair)
+    pure
+      PersistenceArc
+        { persistenceArcBirthRank = birthRank,
+          persistenceArcDeathRank = deathRank
+        }
+  rankOf birth = maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
+
+persistenceArcLifetime :: PersistenceArc -> Int
+persistenceArcLifetime PersistenceArc{persistenceArcBirthRank, persistenceArcDeathRank} =
+  max 0 (persistenceArcDeathRank - persistenceArcBirthRank)
+
+isPositiveHole :: PersistencePair AlphaBirth -> Bool
+isPositiveHole pair =
+  persistenceDegree pair == HomologicalDegree 1
+    && maybe True (> persistenceBirth pair) (persistenceDeath pair)
+
+isFinitePositiveHole :: PersistencePair AlphaBirth -> Bool
+isFinitePositiveHole pair = isPositiveHole pair && maybe False (> persistenceBirth pair) (persistenceDeath pair)
+
+isEssentialHole :: PersistencePair AlphaBirth -> Bool
+isEssentialHole pair = persistenceDegree pair == HomologicalDegree 1 && persistenceDeath pair == Nothing
+
+buildRoseMask :: Either PersistenceRoseError [MaskComponent]
+buildRoseMask = do
+  kernelPoints <- traverse (first RosePointInvalid . exactPointFromPoint) minkowskiKernel
+  kernelPolygon <- first RoseConvexFailed (convexPolygon kernelPoints)
+  kernel <- first RoseMinkowskiFailed (structuringElement kernelPolygon)
+  authoredRegions <- traverse authoredRegion roseMaskLoops
+  offsetRegions <-
+    traverse
+      (fmap fst . first RoseMinkowskiFailed . polygonOffset kernel)
+      authoredRegions
+  unioned <-
+    case NonEmpty.nonEmpty offsetRegions of
+      Nothing -> Left RoseNoMaskRegions
+      Just (firstRegion :| remainingRegions) -> foldM exactRegionUnion firstRegion remainingRegions
+  traverse projectMaskComponent (planarRegionComponents unioned)
+
+authoredRegion :: NonEmpty Point -> Either PersistenceRoseError PlanarRegion
+authoredRegion points = do
+  exactPoints <- traverse (first RosePointInvalid . exactPointFromPoint) points
+  loop <- first RoseRegionInvalid (exactLoop exactPoints)
+  component <- first RoseRegionInvalid (polygonComponent loop [])
+  first RoseRegionInvalid (planarRegion [component])
+
+exactRegionUnion :: PlanarRegion -> PlanarRegion -> Either PersistenceRoseError PlanarRegion
+exactRegionUnion leftRegion rightRegion = do
+  leftLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True leftRegion))
+  rightLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True rightRegion))
+  overlaid <- first RoseOverlayFailed (overlayLayers leftLayer rightLayer)
+  first RoseOverlayPublicationFailed (overlaySelectedRegion (uncurry (||)) overlaid)
+
+projectMaskComponent :: PolygonComponent -> Either PersistenceRoseError MaskComponent
+projectMaskComponent component =
+  MaskComponent
+    <$> projectExactLoop (polygonOuterLoop component)
+    <*> traverse projectExactLoop (polygonHoleLoops component)
+
+projectExactLoop :: ExactLoop -> Either PersistenceRoseError [Point]
+projectExactLoop =
+  traverse
+    (fmap queryPointValue . first RosePointInvalid . exactPointToEmbeddingCandidate)
+    . NonEmpty.toList
+    . exactLoopPoints
+
+roseSites :: [Point]
+roseSites =
+  concatMap petalSites [0 .. rosePetalCount - 1]
+    <> centerSites
+
+petalSites :: Int -> [Point]
+petalSites petalIndex =
+  fmap
+    (petalPoint petalIndex 2.52 1.82 0.69)
+    (sampleAngles 22 (0.021 * fromIntegral petalIndex))
+
+centerSites :: [Point]
+centerSites =
+  Point 0 0
+    : fmap
+      (\angle -> Point (0.56 * cos angle) (0.56 * sin angle))
+      (sampleAngles 14 0.037)
+
+roseMaskLoops :: [NonEmpty Point]
+roseMaskLoops =
+  fmap
+    (\petalIndex -> fmap (petalPoint petalIndex 2.42 2.12 0.9) (sampleAnglesNonEmpty 34 0))
+    [0 .. rosePetalCount - 1]
+    <> [fmap (\angle -> Point (1.08 * cos angle) (1.08 * sin angle)) (sampleAnglesNonEmpty 30 0)]
+
+petalPoint :: Int -> Double -> Double -> Double -> Double -> Point
+petalPoint petalIndex centerRadius radialRadius tangentialRadius localAngle =
+  let orientation = twoPi * fromIntegral petalIndex / fromIntegral rosePetalCount - pi / 2
+      radial = centerRadius + radialRadius * cos localAngle
+      tangential = tangentialRadius * sin localAngle * (0.91 + 0.09 * cos (2 * localAngle))
+      radialX = cos orientation
+      radialY = sin orientation
+      tangentX = -radialY
+      tangentY = radialX
+   in Point
+        (radial * radialX + tangential * tangentX)
+        (radial * radialY + tangential * tangentY)
+
+sampleAngles :: Int -> Double -> [Double]
+sampleAngles sampleCount phase =
+  fmap
+    (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
+    [0 .. sampleCount - 1]
+
+sampleAnglesNonEmpty :: Int -> Double -> NonEmpty Double
+sampleAnglesNonEmpty sampleCount phase =
+  phase
+    :| fmap
+      (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
+      [1 .. max 1 sampleCount - 1]
+
+minkowskiKernel :: NonEmpty Point
+minkowskiKernel =
+  Point 0.085 0
+    :| [ Point 0.043 0.074,
+         Point (-0.043) 0.074,
+         Point (-0.085) 0,
+         Point (-0.043) (-0.074),
+         Point 0.043 (-0.074)
+       ]
+
+rosePetalCount :: Int
+rosePetalCount = 7
+
+-- | The package's canonical exact persistence witness. Its four hull edges
+-- enter at squared radius one; the resident diagonal and two resident faces
+-- enter at squared radius two. Homology therefore observes exactly one finite
+-- degree-one interval @[1,2)@ over this fixed DCEL.
+alphaEclipseSites :: [Point]
+alphaEclipseSites =
+  [ Point (-1) (-1),
+    Point 1 (-1),
+    Point 1 1,
+    Point (-1) 1
+  ]
+
+renderPersistenceRosePoster :: PersistenceRoseScene -> String
+renderPersistenceRosePoster scene =
+  renderScene scene (criticalRank (sceneHeroState scene)) 1 True
+
+renderAlphaEclipse :: AlphaEclipseScene -> String
+renderAlphaEclipse scene =
+  let receipt = eclipseReceipt scene
+      birthLabel =
+        renderExactRatio
+          (eclipseHoleBirthNumerator receipt)
+          (eclipseHoleBirthDenominator receipt)
+      deathLabel =
+        renderExactRatio
+          (eclipseHoleDeathNumerator receipt)
+          (eclipseHoleDeathDenominator receipt)
+   in concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 620\" width=\"1440\" height=\"620\" role=\"img\" aria-labelledby=\"title description\">\n",
+          "<title id=\"title\">Moonlight Triangulation alpha eclipse</title>\n",
+          "<desc id=\"description\">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius ",
+          birthLabel,
+          ", and its resident diagonal and faces kill that hole at squared radius ",
+          deathLabel,
+          ".</desc>\n",
+          renderAlphaEclipseDefinitions,
+          "<rect width=\"1440\" height=\"620\" fill=\"url(#eclipse-background)\"/>\n",
+          "<circle cx=\"720\" cy=\"300\" r=\"246\" fill=\"#ff68bb\" opacity=\"0.035\" filter=\"url(#eclipse-soft-glow)\"/>\n",
+          "<text x=\"58\" y=\"58\" fill=\"#84f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"17\" font-weight=\"600\" letter-spacing=\"6\">MOONLIGHT TRIANGULATION</text>\n",
+          "<text x=\"58\" y=\"101\" fill=\"#f7f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"38\" font-weight=\"420\">ALPHA ECLIPSE</text>\n",
+          "<text x=\"1382\" y=\"61\" text-anchor=\"end\" fill=\"#9baedb\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.5\">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>\n",
+          renderAlphaEclipseStage
+            scene
+            245
+            "BEFORE BIRTH"
+            "four isolated sites"
+            (eclipseBeforeState scene),
+          renderAlphaEclipseStage
+            scene
+            720
+            "LOOP BORN"
+            "resident boundary closes"
+            (eclipseBirthState scene),
+          renderAlphaEclipseStage
+            scene
+            1195
+            "CYCLE KILLED"
+            "resident faces fill it"
+            (eclipseDeathState scene),
+          "<g font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\">\n",
+          "<text x=\"58\" y=\"516\" fill=\"#9badd8\" font-size=\"13\" letter-spacing=\"2.5\">ONE EXACT H₁ INTERVAL</text>\n",
+          "<line x1=\"58\" y1=\"559\" x2=\"1382\" y2=\"559\" stroke=\"#344070\" stroke-width=\"3\"/>\n",
+          "<line x1=\"535\" y1=\"559\" x2=\"1042\" y2=\"559\" stroke=\"#ff72c6\" stroke-width=\"9\" stroke-linecap=\"round\"/>\n",
+          "<circle cx=\"535\" cy=\"559\" r=\"8\" fill=\"#fff6d6\" stroke=\"#ff72c6\" stroke-width=\"3\"/>\n",
+          "<rect x=\"1034\" y=\"551\" width=\"16\" height=\"16\" fill=\"#fff6d6\" stroke=\"#66efff\" stroke-width=\"3\"/>\n",
+          "<text x=\"58\" y=\"589\" fill=\"#7588ba\" font-size=\"12\">α² = 0</text>\n",
+          "<text x=\"535\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">birth α² = ",
+          birthLabel,
+          "</text>\n",
+          "<text x=\"788\" y=\"548\" text-anchor=\"middle\" fill=\"#fff6d6\" font-size=\"14\">the eclipse hole persists</text>\n",
+          "<text x=\"1042\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">death α² = ",
+          deathLabel,
+          "</text>\n",
+          "<text x=\"1382\" y=\"589\" text-anchor=\"end\" fill=\"#7588ba\" font-size=\"12\">exact α² increases →</text>\n",
+          "</g>\n",
+          "</svg>\n"
+        ]
+
+renderAlphaEclipseDefinitions :: String
+renderAlphaEclipseDefinitions =
+  concat
+    [ "<defs>\n",
+      "<radialGradient id=\"eclipse-background\" cx=\"50%\" cy=\"38%\" r=\"78%\"><stop offset=\"0\" stop-color=\"#111744\"/><stop offset=\"0.55\" stop-color=\"#070b25\"/><stop offset=\"1\" stop-color=\"#02040f\"/></radialGradient>\n",
+      "<linearGradient id=\"eclipse-corona\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#66efff\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#ffd86b\"/></linearGradient>\n",
+      "<linearGradient id=\"eclipse-face\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7c4dff\"/><stop offset=\"0.5\" stop-color=\"#ff4fa3\"/><stop offset=\"1\" stop-color=\"#ffbf5b\"/></linearGradient>\n",
+      "<filter id=\"eclipse-soft-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<filter id=\"eclipse-dot-glow\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "</defs>\n"
+    ]
+
+renderAlphaEclipseStage
+  :: AlphaEclipseScene
+  -> Double
+  -> String
+  -> String
+  -> CriticalState
+  -> String
+renderAlphaEclipseStage scene centerX stageLabel explanation state =
+  let rankValue = criticalRank state
+      activeFaces = filter ((<= rankValue) . rankedTriangleBirthRank) (eclipseFaces scene)
+      activeEdges = filter ((<= rankValue) . rankedSegmentBirthRank) (eclipseEdges scene)
+      highlighted = criticalBettiOne state > 0
+      frameStroke = if highlighted then "#ff6fbd" else "#344070"
+      frameOpacity = if highlighted then "0.72" else "0.55"
+   in concat
+        [ "<g transform=\"translate(",
+          formatDouble centerX,
+          " 292)\">\n",
+          "<rect x=\"-194\" y=\"-150\" width=\"388\" height=\"292\" rx=\"28\" fill=\"#090d26\" fill-opacity=\"0.72\" stroke=\"",
+          frameStroke,
+          "\" stroke-opacity=\"",
+          frameOpacity,
+          "\"/>\n",
+          "<text x=\"-162\" y=\"-114\" fill=\"",
+          if highlighted then "#ff89cb" else "#8fa4d6",
+          "\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"13\" letter-spacing=\"2.4\">",
+          stageLabel,
+          "</text>\n",
+          "<circle r=\"104\" fill=\"#01020a\" stroke=\"#7fefff\" stroke-opacity=\"0.18\" stroke-width=\"2\"/>\n",
+          "<g fill=\"url(#eclipse-face)\" fill-opacity=\"0.58\" stroke=\"#ffd56d\" stroke-opacity=\"0.48\" stroke-width=\"1.2\">",
+          concatMap renderAlphaEclipseTriangle activeFaces,
+          "</g>\n",
+          "<g fill=\"none\" stroke=\"url(#eclipse-corona)\" stroke-width=\"5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" filter=\"url(#eclipse-soft-glow)\">",
+          concatMap renderAlphaEclipseSegment activeEdges,
+          "</g>\n",
+          "<g fill=\"none\" stroke=\"#fff7da\" stroke-width=\"3.2\" stroke-linejoin=\"round\" opacity=\"0.86\">",
+          concatMap renderAlphaEclipseHole (criticalHoles state),
+          "</g>\n",
+          "<g fill=\"#fff8df\" stroke=\"#ff75c5\" stroke-width=\"2\" filter=\"url(#eclipse-dot-glow)\">",
+          concatMap renderAlphaEclipseSite (eclipseSites scene),
+          "</g>\n",
+          "<rect x=\"-126\" y=\"-18\" width=\"252\" height=\"36\" rx=\"18\" fill=\"#05071a\" fill-opacity=\"0.82\"/>\n",
+          "<text y=\"5\" text-anchor=\"middle\" fill=\"#fff7da\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.2\">",
+          explanation,
+          "</text>\n",
+          "<text x=\"-162\" y=\"121\" fill=\"#fff4cc\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"25\">β₁ = ",
+          show (criticalBettiOne state),
+          "</text>\n",
+          "<text x=\"162\" y=\"121\" text-anchor=\"end\" fill=\"#8495c4\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"12\">α² = ",
+          renderAlphaBirth (criticalBirth state),
+          "</text>\n",
+          "</g>\n"
+        ]
+
+renderAlphaEclipseTriangle :: RankedTriangle -> String
+renderAlphaEclipseTriangle RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint)} =
+  "<polygon points=\""
+    <> renderScreenPoints (fmap toAlphaEclipseLocal [firstPoint, secondPoint, thirdPoint])
+    <> "\"/>\n"
+
+renderAlphaEclipseSegment :: RankedSegment -> String
+renderAlphaEclipseSegment RankedSegment{rankedSegmentGeometry = Segment firstPoint secondPoint} =
+  let Point firstX firstY = toAlphaEclipseLocal firstPoint
+      Point secondX secondY = toAlphaEclipseLocal secondPoint
+   in "<line x1=\""
+        <> formatDouble firstX
+        <> "\" y1=\""
+        <> formatDouble firstY
+        <> "\" x2=\""
+        <> formatDouble secondX
+        <> "\" y2=\""
+        <> formatDouble secondY
+        <> "\"/>\n"
+
+renderAlphaEclipseHole :: HoleComponent -> String
+renderAlphaEclipseHole (HoleComponent loops) =
+  concatMap
+    (\points -> "<path d=\"" <> renderClosedPath (fmap toAlphaEclipseLocal points) <> "\"/>\n")
+    loops
+
+renderAlphaEclipseSite :: Point -> String
+renderAlphaEclipseSite point =
+  let Point x y = toAlphaEclipseLocal point
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\"6\"/>\n"
+
+toAlphaEclipseLocal :: Point -> Point
+toAlphaEclipseLocal (Point x y) =
+  let scale = 72
+      inverseSquareRootTwo = sqrt 0.5
+   in Point
+        (scale * inverseSquareRootTwo * (x - y))
+        (scale * inverseSquareRootTwo * (x + y))
+
+renderAlphaBirth :: AlphaBirth -> String
+renderAlphaBirth birth =
+  renderExactRatio (alphaBirthNumerator birth) (alphaBirthDenominator birth)
+
+renderExactRatio :: Integer -> Integer -> String
+renderExactRatio numerator denominator
+  | denominator == 1 = show numerator
+  | otherwise = show numerator <> "/" <> show denominator
+
+persistenceRoseFrameSpecs :: Int -> Either PersistenceRoseError [PersistenceRoseFrame]
+persistenceRoseFrameSpecs frameCount
+  | frameCount < 2 = Left (RoseFrameCountTooSmall frameCount)
+  | otherwise =
+      Right
+        ( fmap
+            (\frameIndex ->
+               PersistenceRoseFrame
+                 { frameProgress = frameProgressAt frameCount frameIndex
+                 })
+            [0 .. frameCount - 1]
+        )
+
+renderPersistenceRoseFrame :: PersistenceRoseScene -> PersistenceRoseFrame -> String
+renderPersistenceRoseFrame scene frame =
+  let finalRank = criticalRank (NonEmpty.last (sceneCriticalStates scene))
+      selectedRank = clampInt 0 finalRank (floor (frameProgress frame * fromIntegral finalRank))
+   in renderScene scene selectedRank (frameProgress frame) False
+
+persistenceRoseAnimationReceipt
+  :: PersistenceRoseScene
+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
+persistenceRoseAnimationReceipt scene = do
+  frames <- persistenceRoseFrameSpecs canonicalAnimationFrameCount
+  let renderedFrames = fmap (renderPersistenceRoseFrame scene) frames
+  pure
+    PersistenceRoseAnimationReceipt
+      { roseAnimationFrameCount = canonicalAnimationFrameCount,
+        roseAnimationFramesPerSecond = canonicalAnimationFramesPerSecond,
+        roseAnimationWidth = canonicalAnimationWidth,
+        roseAnimationHeight = canonicalAnimationHeight,
+        roseAnimationSourceFingerprint = fingerprintSections renderedFrames
+      }
+
+renderPersistenceRoseAnimationReceipt :: PersistenceRoseAnimationReceipt -> String
+renderPersistenceRoseAnimationReceipt receipt =
+  intercalate
+    " "
+    [ "MOONLIGHT_PERSISTENCE_ROSE",
+      "frame-count=" <> show (roseAnimationFrameCount receipt),
+      "fps=" <> show (roseAnimationFramesPerSecond receipt),
+      "size=" <> show (roseAnimationWidth receipt) <> "x" <> show (roseAnimationHeight receipt),
+      "source-fnv1a64=" <> renderWord64Hex (roseAnimationSourceFingerprint receipt)
+    ]
+
+stampPersistenceRoseGif
+  :: PersistenceRoseScene
+  -> ByteString
+  -> Either PersistenceRoseError (PersistenceRoseAnimationReceipt, ByteString)
+stampPersistenceRoseGif scene gifBytes = do
+  receipt <- persistenceRoseAnimationReceipt scene
+  summary <- inspectGif gifBytes
+  validateGifSummary receipt summary
+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
+      markerLength = ByteString.length marker
+  if markerLength > 255
+    then Left (RoseGifMarkerTooLong markerLength)
+    else
+      if marker `ByteString.isInfixOf` gifBytes
+        then Right (receipt, gifBytes)
+        else
+          if gifMarkerPrefix `ByteString.isInfixOf` gifBytes
+            then Left RoseGifProvenanceMismatch
+            else do
+              insertionOffset <- gifDataStart gifBytes
+              let (prefix, suffix) = ByteString.splitAt insertionOffset gifBytes
+                  commentExtension =
+                    ByteString.pack [0x21, 0xfe, fromIntegral markerLength]
+                      <> marker
+                      <> ByteString.singleton 0
+              pure (receipt, prefix <> commentExtension <> suffix)
+
+verifyPersistenceRoseGif
+  :: PersistenceRoseScene
+  -> ByteString
+  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
+verifyPersistenceRoseGif scene gifBytes = do
+  receipt <- persistenceRoseAnimationReceipt scene
+  summary <- inspectGif gifBytes
+  validateGifSummary receipt summary
+  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
+  if marker `ByteString.isInfixOf` gifBytes
+    then Right receipt
+    else Left RoseGifProvenanceMismatch
+
+data GifSummary = GifSummary
+  { gifSummaryWidth :: !Int,
+    gifSummaryHeight :: !Int,
+    gifSummaryFrameCount :: !Int,
+    gifSummaryFrameDelayCount :: !Int,
+    gifSummaryDurationCentiseconds :: !Int
+  }
+
+inspectGif :: ByteString -> Either PersistenceRoseError GifSummary
+inspectGif gifBytes = do
+  dataStart <- gifDataStart gifBytes
+  width <- gifLittleEndianWord16 gifBytes 6
+  height <- gifLittleEndianWord16 gifBytes 8
+  (frameCount, frameDelayCount, durationCentiseconds) <-
+    countGifFrames gifBytes dataStart 0 0 0
+  pure
+    GifSummary
+      { gifSummaryWidth = width,
+        gifSummaryHeight = height,
+        gifSummaryFrameCount = frameCount,
+        gifSummaryFrameDelayCount = frameDelayCount,
+        gifSummaryDurationCentiseconds = durationCentiseconds
+      }
+
+gifDataStart :: ByteString -> Either PersistenceRoseError Int
+gifDataStart gifBytes = do
+  signature <- maybe (Left (RoseGifTruncated 0)) Right (gifSlice gifBytes 0 6)
+  if signature == ByteString.Char8.pack "GIF87a" || signature == ByteString.Char8.pack "GIF89a"
+    then pure ()
+    else Left RoseGifInvalidSignature
+  packed <- maybe (Left (RoseGifTruncated 10)) Right (gifByteAt gifBytes 10)
+  let globalColourTableBytes =
+        if packed .&. 0x80 == 0
+          then 0
+          else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
+      dataStart = 13 + globalColourTableBytes
+  maybe (Left (RoseGifTruncated dataStart)) (const (Right dataStart)) (gifSlice gifBytes 0 dataStart)
+
+countGifFrames
+  :: ByteString
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Either PersistenceRoseError (Int, Int, Int)
+countGifFrames gifBytes offset frameCount frameDelayCount durationCentiseconds = do
+  block <- maybe (Left (RoseGifTruncated offset)) Right (gifByteAt gifBytes offset)
+  case block of
+    0x3b -> Right (frameCount, frameDelayCount, durationCentiseconds)
+    0x21 -> do
+      extensionLabel <- maybe (Left (RoseGifTruncated (offset + 1))) Right (gifByteAt gifBytes (offset + 1))
+      nextOffset <- skipGifSubBlocks gifBytes (offset + 2)
+      if extensionLabel == 0xf9
+        then do
+          delay <- gifLittleEndianWord16 gifBytes (offset + 4)
+          countGifFrames
+            gifBytes
+            nextOffset
+            frameCount
+            (frameDelayCount + 1)
+            (durationCentiseconds + delay)
+        else
+          countGifFrames
+            gifBytes
+            nextOffset
+            frameCount
+            frameDelayCount
+            durationCentiseconds
+    0x2c -> do
+      packed <- maybe (Left (RoseGifTruncated (offset + 9))) Right (gifByteAt gifBytes (offset + 9))
+      let localColourTableBytes =
+            if packed .&. 0x80 == 0
+              then 0
+              else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
+          imageDataStart = offset + 11 + localColourTableBytes
+      maybe (Left (RoseGifTruncated imageDataStart)) (const (Right ())) (gifByteAt gifBytes (imageDataStart - 1))
+      skipGifSubBlocks gifBytes imageDataStart >>= \next ->
+        countGifFrames
+          gifBytes
+          next
+          (frameCount + 1)
+          frameDelayCount
+          durationCentiseconds
+    0x00 ->
+      countGifFrames
+        gifBytes
+        (offset + 1)
+        frameCount
+        frameDelayCount
+        durationCentiseconds
+    unexpected -> Left (RoseGifUnexpectedBlock offset unexpected)
+
+skipGifSubBlocks :: ByteString -> Int -> Either PersistenceRoseError Int
+skipGifSubBlocks gifBytes offset = do
+  blockLength <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
+  if blockLength == 0
+    then Right (offset + 1)
+    else do
+      let nextOffset = offset + 1 + blockLength
+      maybe (Left (RoseGifTruncated nextOffset)) (const (Right ())) (gifSlice gifBytes offset (1 + blockLength))
+      skipGifSubBlocks gifBytes nextOffset
+
+validateGifSummary
+  :: PersistenceRoseAnimationReceipt
+  -> GifSummary
+  -> Either PersistenceRoseError ()
+validateGifSummary receipt summary
+  | actualWidth /= expectedWidth || actualHeight /= expectedHeight =
+      Left (RoseGifDimensionMismatch expectedWidth expectedHeight actualWidth actualHeight)
+  | actualFrameCount /= expectedFrameCount =
+      Left (RoseGifFrameCountMismatch expectedFrameCount actualFrameCount)
+  | actualFrameDelayCount /= expectedFrameCount =
+      Left (RoseGifFrameDelayCountMismatch expectedFrameCount actualFrameDelayCount)
+  | actualDurationCentiseconds /= canonicalAnimationDurationCentiseconds =
+      Left
+        ( RoseGifDurationMismatch
+            canonicalAnimationDurationCentiseconds
+            actualDurationCentiseconds
+        )
+  | otherwise = Right ()
+ where
+  expectedWidth = roseAnimationWidth receipt
+  expectedHeight = roseAnimationHeight receipt
+  actualWidth = gifSummaryWidth summary
+  actualHeight = gifSummaryHeight summary
+  expectedFrameCount = roseAnimationFrameCount receipt
+  actualFrameCount = gifSummaryFrameCount summary
+  actualFrameDelayCount = gifSummaryFrameDelayCount summary
+  actualDurationCentiseconds = gifSummaryDurationCentiseconds summary
+
+gifLittleEndianWord16 :: ByteString -> Int -> Either PersistenceRoseError Int
+gifLittleEndianWord16 gifBytes offset = do
+  low <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
+  high <- maybe (Left (RoseGifTruncated (offset + 1))) (Right . fromIntegral) (gifByteAt gifBytes (offset + 1))
+  pure (low + 256 * high)
+
+gifByteAt :: ByteString -> Int -> Maybe Word8
+gifByteAt gifBytes offset = fst <$> ByteString.uncons (ByteString.drop offset gifBytes)
+
+gifSlice :: ByteString -> Int -> Int -> Maybe ByteString
+gifSlice gifBytes offset width =
+  let selected = ByteString.take width (ByteString.drop offset gifBytes)
+   in if ByteString.length selected == width then Just selected else Nothing
+
+fingerprintSections :: [String] -> Word64
+fingerprintSections =
+  foldl'
+    (\fingerprint section -> fingerprintByte (fingerprintText fingerprint section) 0)
+    fnvOffsetBasis
+
+fingerprintText :: Word64 -> String -> Word64
+fingerprintText fingerprint =
+  ByteString.foldl' fingerprintByte fingerprint
+    . Text.Encoding.encodeUtf8
+    . Text.pack
+
+fingerprintByte :: Word64 -> Word8 -> Word64
+fingerprintByte fingerprint byte =
+  (fingerprint `xor` fromIntegral byte) * fnvPrime
+
+renderWord64Hex :: Word64 -> String
+renderWord64Hex value =
+  let rendered = showHex value ""
+   in replicate (16 - length rendered) '0' <> rendered
+
+gifMarkerPrefix :: ByteString
+gifMarkerPrefix = ByteString.Char8.pack "MOONLIGHT_PERSISTENCE_ROSE "
+
+canonicalAnimationFrameCount :: Int
+canonicalAnimationFrameCount = 65
+
+canonicalAnimationFramesPerSecond :: Int
+canonicalAnimationFramesPerSecond = 8
+
+canonicalAnimationWidth :: Int
+canonicalAnimationWidth = 960
+
+canonicalAnimationHeight :: Int
+canonicalAnimationHeight = 540
+
+canonicalAnimationDurationCentiseconds :: Int
+canonicalAnimationDurationCentiseconds = 813
+
+fnvOffsetBasis :: Word64
+fnvOffsetBasis = 14695981039346656037
+
+fnvPrime :: Word64
+fnvPrime = 1099511628211
+
+frameProgressAt :: Int -> Int -> Double
+frameProgressAt frameCount frameIndex
+  | frameIndex <= 0 || frameIndex >= frameCount - 1 = 0
+  | otherwise =
+      let phase = fromIntegral frameIndex / fromIntegral (frameCount - 1)
+       in 0.5 - 0.5 * cos (twoPi * phase)
+
+renderScene :: PersistenceRoseScene -> Int -> Double -> Bool -> String
+renderScene scene selectedRank progress poster =
+  let selectedState = criticalStateAt selectedRank (sceneCriticalStates scene)
+      finalRank = max 1 (criticalRank (NonEmpty.last (sceneCriticalStates scene)))
+      activation = if poster then fromIntegral selectedRank / fromIntegral finalRank else progress
+   in concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\" role=\"img\" aria-labelledby=\"title description\">\n",
+          "<title id=\"title\">Moonlight Triangulation Persistence Rose</title>\n",
+          "<desc id=\"description\">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>\n",
+          renderDefinitions (sceneMaskComponents scene),
+          "<rect width=\"1920\" height=\"1080\" fill=\"url(#background)\"/>\n",
+          renderAmbientOrbits activation,
+          renderPersistenceHalo finalRank selectedRank (scenePersistenceArcs scene),
+          "<g clip-path=\"url(#rose-clip)\">\n",
+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"url(#rose-wash)\" opacity=\"0.28\"/>\n",
+          "<g filter=\"url(#field-soften)\">", concatMap renderFieldSample (sceneFieldSamples scene), "</g>\n",
+          "<g opacity=\"0.58\">", concatMap renderVoronoiSegment (sceneVoronoiSegments scene), "</g>\n",
+          concatMap (renderRankedTriangle finalRank activation) (sceneFaces scene),
+          concatMap (renderRankedSegment finalRank activation) (sceneEdges scene),
+          "<g filter=\"url(#hole-glow)\">", concatMap renderHoleComponent (criticalHoles selectedState), "</g>\n",
+          concatMap (renderSite activation) (sceneSites scene),
+          "</g>\n",
+          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"none\" stroke=\"url(#mask-stroke)\" stroke-width=\"2.2\" opacity=\"0.92\" filter=\"url(#edge-glow)\"/>\n",
+          renderTitle selectedState,
+          "</svg>\n"
+        ]
+
+criticalStateAt :: Int -> NonEmpty CriticalState -> CriticalState
+criticalStateAt target (firstState :| remainingStates) =
+  foldl'
+    (\chosen candidate -> if criticalRank candidate <= target then candidate else chosen)
+    firstState
+    remainingStates
+
+renderDefinitions :: [MaskComponent] -> String
+renderDefinitions maskComponents =
+  concat
+    [ "<defs>\n",
+      "<radialGradient id=\"background\" cx=\"52%\" cy=\"48%\" r=\"72%\"><stop offset=\"0\" stop-color=\"#10143d\"/><stop offset=\"0.48\" stop-color=\"#070b24\"/><stop offset=\"1\" stop-color=\"#02040e\"/></radialGradient>\n",
+      "<radialGradient id=\"rose-wash\" cx=\"48%\" cy=\"42%\" r=\"63%\"><stop offset=\"0\" stop-color=\"#ffe7a3\"/><stop offset=\"0.38\" stop-color=\"#ff5fb7\"/><stop offset=\"0.72\" stop-color=\"#7c4dff\"/><stop offset=\"1\" stop-color=\"#00d9ff\"/></radialGradient>\n",
+      "<linearGradient id=\"mask-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffd86b\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#66efff\"/></linearGradient>\n",
+      "<linearGradient id=\"hole-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff9db\"/><stop offset=\"0.52\" stop-color=\"#ff9ad7\"/><stop offset=\"1\" stop-color=\"#9ff7ff\"/></linearGradient>\n",
+      "<filter id=\"field-soften\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"24\"/></filter>\n",
+      "<filter id=\"hole-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<filter id=\"edge-glow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
+      "<clipPath id=\"rose-clip\"><path d=\"",
+      renderMaskPath maskComponents,
+      "\" fill-rule=\"evenodd\"/></clipPath>\n",
+      "</defs>\n"
+    ]
+
+renderAmbientOrbits :: Double -> String
+renderAmbientOrbits activation =
+  concatMap
+    ( \orbit ->
+        let radius = 322 + fromIntegral orbit * 34
+            opacity = 0.055 + activation * 0.025
+         in "<circle cx=\"960\" cy=\"552\" r=\""
+              <> formatDouble radius
+              <> "\" fill=\"none\" stroke=\"#8ba5ff\" stroke-width=\"0.8\" stroke-dasharray=\"2 12\" opacity=\""
+              <> formatDouble opacity
+              <> "\"/>\n"
+    )
+    [0 .. 5 :: Int]
+
+renderPersistenceHalo :: Int -> Int -> [PersistenceArc] -> String
+renderPersistenceHalo finalRank selectedRank arcs =
+  "<g fill=\"none\" stroke-linecap=\"round\">\n"
+    <> concatMap renderArc (zip ([0 :: Int ..]) arcs)
+    <> "</g>\n"
+ where
+  renderArc :: (Int, PersistenceArc) -> String
+  renderArc (index, arc) =
+    let active = persistenceArcBirthRank arc <= selectedRank && selectedRank < persistenceArcDeathRank arc
+        radius = 414 + fromIntegral (index `mod` 7) * 7.5
+        start = rankAngle finalRank (persistenceArcBirthRank arc)
+        finish = rankAngle finalRank (persistenceArcDeathRank arc)
+        points = arcPolyline radius start finish 42
+        hue = 188 + (index * 17 `mod` 122)
+        opacity = if active then 0.72 else 0.16
+        width = if active then 2.6 else 1.1
+     in "<polyline points=\""
+          <> renderScreenPoints points
+          <> "\" stroke=\"hsl("
+          <> show hue
+          <> " 92% 72%)\" stroke-width=\""
+          <> formatDouble width
+          <> "\" opacity=\""
+          <> formatDouble opacity
+          <> "\"/>\n"
+
+rankAngle :: Int -> Int -> Double
+rankAngle finalRank rankValue =
+  -pi / 2 + twoPi * fromIntegral rankValue / fromIntegral (max 1 finalRank)
+
+arcPolyline :: Double -> Double -> Double -> Int -> [Point]
+arcPolyline radius start finish sampleCount =
+  fmap
+    ( \sampleIndex ->
+        let fraction = fromIntegral sampleIndex / fromIntegral sampleCount
+            angle = start + fraction * (finish - start)
+         in screenPointFromPolar radius angle
+    )
+    [0 .. sampleCount]
+
+screenPointFromPolar :: Double -> Double -> Point
+screenPointFromPolar radius angle =
+  Point (960 + radius * cos angle) (552 + radius * sin angle)
+
+renderFieldSample :: FieldSample -> String
+renderFieldSample FieldSample{fieldSamplePoint, fieldSampleValue, fieldSampleNeighborCount} =
+  let Point x y = toScreen fieldSamplePoint
+      hue = round (186 + 126 * fieldSampleValue) :: Int
+      radius = 30 + min 5 (fromIntegral fieldSampleNeighborCount)
+      opacity = 0.16 + 0.16 * fieldSampleValue
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\""
+        <> formatDouble radius
+        <> "\" fill=\"hsl("
+        <> show hue
+        <> " 94% 61%)\" opacity=\""
+        <> formatDouble opacity
+        <> "\"/>\n"
+
+renderVoronoiSegment :: Segment -> String
+renderVoronoiSegment segment =
+  renderLine segment "#61efff" 1.15 0.5 ""
+
+renderRankedTriangle :: Int -> Double -> RankedTriangle -> String
+renderRankedTriangle finalRank activation RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint), rankedTriangleBirthRank} =
+  let opacity = activationOpacity finalRank activation rankedTriangleBirthRank
+      hue :: Int
+      hue = 282 - round (94 * fromIntegral rankedTriangleBirthRank / fromIntegral (max 1 finalRank) :: Double)
+   in if opacity <= 0.001
+        then ""
+        else
+          "<polygon points=\""
+            <> renderWorldPoints [firstPoint, secondPoint, thirdPoint]
+            <> "\" fill=\"hsl("
+            <> show hue
+            <> " 88% 60%)\" opacity=\""
+            <> formatDouble (0.06 + 0.17 * opacity)
+            <> "\"/>\n"
+
+renderRankedSegment :: Int -> Double -> RankedSegment -> String
+renderRankedSegment finalRank activation RankedSegment{rankedSegmentGeometry, rankedSegmentBirthRank} =
+  let opacity = activationOpacity finalRank activation rankedSegmentBirthRank
+      width = 0.75 + 1.25 * opacity
+      color = if opacity > 0.72 then "#ffd56a" else "#dd78ff"
+   in renderLine rankedSegmentGeometry color width (0.06 + 0.82 * opacity) "filter=\"url(#edge-glow)\""
+
+activationOpacity :: Int -> Double -> Int -> Double
+activationOpacity finalRank activation birthRank =
+  let level = activation * fromIntegral finalRank
+      distance = level - fromIntegral birthRank
+   in smoothstep (distance + 0.6)
+
+smoothstep :: Double -> Double
+smoothstep value =
+  let bounded = clamp 0 1 value
+   in bounded * bounded * (3 - 2 * bounded)
+
+renderHoleComponent :: HoleComponent -> String
+renderHoleComponent (HoleComponent loops) =
+  concatMap
+    ( \points ->
+        "<path d=\""
+          <> renderClosedWorldPath points
+          <> "\" fill=\"none\" stroke=\"url(#hole-stroke)\" stroke-width=\"5.4\" stroke-linejoin=\"round\" opacity=\"0.98\"/>\n"
+    )
+    loops
+
+renderSite :: Double -> Point -> String
+renderSite activation point =
+  let Point x y = toScreen point
+      radius = 2.5 + 1.5 * activation
+   in "<circle cx=\""
+        <> formatDouble x
+        <> "\" cy=\""
+        <> formatDouble y
+        <> "\" r=\""
+        <> formatDouble radius
+        <> "\" fill=\"#fff8d9\" stroke=\"#ff9ad7\" stroke-width=\"1.2\" opacity=\"0.95\"/>\n"
+
+renderLine :: Segment -> String -> Double -> Double -> String -> String
+renderLine (Segment fromPoint toPoint) color width opacity extraAttributes =
+  let Point fromX fromY = toScreen fromPoint
+      Point toX toY = toScreen toPoint
+   in "<line x1=\""
+        <> formatDouble fromX
+        <> "\" y1=\""
+        <> formatDouble fromY
+        <> "\" x2=\""
+        <> formatDouble toX
+        <> "\" y2=\""
+        <> formatDouble toY
+        <> "\" stroke=\""
+        <> color
+        <> "\" stroke-width=\""
+        <> formatDouble width
+        <> "\" stroke-linecap=\"round\" opacity=\""
+        <> formatDouble opacity
+        <> "\" "
+        <> extraAttributes
+        <> "/>\n"
+
+renderTitle :: CriticalState -> String
+renderTitle state =
+  let alphaFraction = exactFractionSummary (criticalBirth state)
+      alphaDecimal = formatDouble (alphaBirthToDouble (criticalBirth state))
+      bettiMeaning = bettiOneMeaning (criticalBettiOne state)
+   in concat
+        [ "<g font-family=\"Inter, Avenir Next, Helvetica, Arial, sans-serif\" fill=\"#f7f3ff\">\n",
+          "<text x=\"72\" y=\"82\" font-size=\"24\" font-weight=\"600\" letter-spacing=\"8\" fill=\"#9ff7ff\">MOONLIGHT TRIANGULATION</text>\n",
+          "<text x=\"70\" y=\"145\" font-size=\"58\" font-weight=\"780\" letter-spacing=\"-1.5\">PERSISTENCE ROSE</text>\n",
+          "<g transform=\"translate(1500 790)\">\n",
+          "<text x=\"0\" y=\"0\" font-size=\"18\" letter-spacing=\"2.4\" fill=\"#91a8d8\">TOPOLOGICAL HOLES AT THIS SCALE</text>\n",
+          "<text x=\"0\" y=\"74\" font-size=\"78\" font-weight=\"720\" fill=\"#fff5cd\">β₁ = ",
+          show (criticalBettiOne state),
+          "</text>\n",
+          "<text x=\"2\" y=\"116\" font-size=\"17\" fill=\"#c9d3ff\">",
+          bettiMeaning,
+          "</text>\n",
+          "<text x=\"2\" y=\"144\" font-size=\"15\" fill=\"#9ba8d4\">in the exact alpha complex</text>\n",
+          "<text x=\"2\" y=\"176\" font-size=\"14\" fill=\"#7682aa\">critical rank ",
+          show (criticalRank state),
+          " · α² = ",
+          alphaDecimal,
+          "</text>\n",
+          "<text x=\"2\" y=\"200\" font-size=\"12\" fill=\"#596486\">",
+          alphaFraction,
+          "</text>\n",
+          "</g>\n",
+          "</g>\n"
+        ]
+
+bettiOneMeaning :: Int -> String
+bettiOneMeaning holeCount =
+  case holeCount of
+    0 -> "no independent one-dimensional holes"
+    1 -> "1 independent one-dimensional hole"
+    _ -> show holeCount <> " independent one-dimensional holes"
+
+exactFractionSummary :: AlphaBirth -> String
+exactFractionSummary birth =
+  let numeratorDigits = length (show (abs (alphaBirthNumerator birth)))
+      denominatorDigits = length (show (alphaBirthDenominator birth))
+   in "exact " <> show numeratorDigits <> "-digit / " <> show denominatorDigits <> "-digit rational"
+
+renderMaskPath :: [MaskComponent] -> String
+renderMaskPath =
+  intercalate " "
+    . concatMap
+      (\MaskComponent{maskOuterLoop, maskHoleLoops} -> renderClosedScreenPath maskOuterLoop : fmap renderClosedScreenPath maskHoleLoops)
+
+renderClosedWorldPath :: [Point] -> String
+renderClosedWorldPath = renderClosedPath . fmap toScreen
+
+renderClosedScreenPath :: [Point] -> String
+renderClosedScreenPath = renderClosedPath . fmap toScreen
+
+renderClosedPath :: [Point] -> String
+renderClosedPath points =
+  case points of
+    [] -> ""
+    firstPoint : remainingPoints ->
+      "M " <> renderPoint firstPoint <> concatMap (" L " <>) (fmap renderPoint remainingPoints) <> " Z"
+
+renderWorldPoints :: [Point] -> String
+renderWorldPoints = renderScreenPoints . fmap toScreen
+
+renderScreenPoints :: [Point] -> String
+renderScreenPoints = unwords . fmap renderPoint
+
+renderPoint :: Point -> String
+renderPoint (Point x y) = formatDouble x <> "," <> formatDouble y
+
+toScreen :: Point -> Point
+toScreen (Point x y) = Point (960 + x * 101) (552 + y * 101)
+
+formatDouble :: Double -> String
+formatDouble value = showFFloat (Just 3) value ""
+
+clamp :: Double -> Double -> Double -> Double
+clamp lower upper = min upper . max lower
+
+clampInt :: Int -> Int -> Int -> Int
+clampInt lower upper = min upper . max lower
+
+twoPi :: Double
+twoPi = 2 * pi
diff --git a/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs b/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
deleted file mode 100644
--- a/docs/persistence-rose/Moonlight/Triangulation/Exhibit/PersistenceRose.hs
+++ /dev/null
@@ -1,1625 +0,0 @@
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | A deterministic, pure observatory assembled from Moonlight's existing
--- geometry, exact filtration, interpolation, and persistence owners.
-module Moonlight.Triangulation.Exhibit.PersistenceRose
-  ( PersistenceRoseError (..),
-    PersistenceRoseReceipt (..),
-    PersistenceRoseScene,
-    PersistenceRoseFrame,
-    PersistenceRoseAnimationReceipt (..),
-    AlphaEclipseReceipt (..),
-    AlphaEclipseScene,
-    buildPersistenceRoseScene,
-    buildAlphaEclipseScene,
-    persistenceRoseReceipt,
-    alphaEclipseReceipt,
-    renderPersistenceRoseReceipt,
-    renderAlphaEclipseReceipt,
-    renderPersistenceRosePoster,
-    renderAlphaEclipse,
-    persistenceRoseFrameSpecs,
-    renderPersistenceRoseFrame,
-    persistenceRoseAnimationReceipt,
-    renderPersistenceRoseAnimationReceipt,
-    stampPersistenceRoseGif,
-    verifyPersistenceRoseGif,
-  )
-where
-
-import Control.Monad (foldM)
-import Control.Monad.ST (runST)
-import Data.Bifunctor (first)
-import Data.Bits ((.&.), xor)
-import Data.ByteString (ByteString)
-import Data.ByteString qualified as ByteString
-import Data.ByteString.Char8 qualified as ByteString.Char8
-import Data.List (find, intercalate, sortOn)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Maybe (mapMaybe)
-import Data.Ord (Down (..))
-import Data.Set qualified as Set
-import Data.Text qualified as Text
-import Data.Text.Encoding qualified as Text.Encoding
-import Data.Vector qualified as Vector
-import Data.Word (Word64, Word8)
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..),
-    HomologyFailure,
-    PersistencePair (..),
-  )
-import Moonlight.Homology.Persistence
-  ( criticalBettiVectors,
-    mod2PersistentPairsWithCriticalBettiTable,
-  )
-import Moonlight.Triangulation
-  ( AlphaBirth,
-    AlphaFiltration,
-    AlphaFiltrationError,
-    BuildError,
-    DelaunayTriangulation,
-    ExactLoop,
-    MinkowskiError,
-    OverlayError,
-    PlanarRegion,
-    Point (..),
-    PointValidationError,
-    PolygonComponent,
-    QueryPoint,
-    RegionPublicationError,
-    RegionValidationError,
-    alphaBirthDenominator,
-    alphaBirthNumerator,
-    alphaBirthToDouble,
-    alphaEdgeBirth,
-    alphaFaceBirth,
-    alphaFiltration,
-    alphaFiltrationCriticalBirths,
-    convexPolygon,
-    delaunayGeometry,
-    exactLoop,
-    exactLoopPoints,
-    exactPointFromPoint,
-    exactPointToEmbeddingCandidate,
-    innerFaces,
-    innerFaceVertexTriples,
-    mkQueryPoint,
-    overlayLayers,
-    overlaySelectedRegion,
-    planarLayer,
-    planarRegion,
-    planarRegionComponents,
-    polygonComponent,
-    polygonHoleLoops,
-    polygonOffset,
-    polygonOuterLoop,
-    queryPointValue,
-    structuringElement,
-    undirectedEdges,
-    undirectedEndpoints,
-    vertexPoint,
-    vertices,
-  )
-import Moonlight.Triangulation.CellComplex
-  ( DCELError,
-    filteredAlphaComplex,
-  )
-import Moonlight.Triangulation.FloodFillIterator
-  ( BoundaryObstruction,
-    FaceComponent,
-    boundaryLoopVertices,
-    componentBoundaryLoops,
-    faceComponentFaces,
-    faceComponentsBy,
-    outerFaceFloodFill,
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..),
-    UndirectedEdgeId,
-    VertexId,
-    normalizedDirected,
-  )
-import Moonlight.Triangulation.Interpolation
-  ( InterpolationStats (..),
-    NaturalNeighborResult (..),
-    naturalNeighborWeights,
-    newNaturalNeighborWorkspace,
-  )
-import Moonlight.Triangulation.Voronoi
-  ( VoronoiEdgeGeometry (..),
-    asDirectedVoronoiEdge,
-    voronoiEdgeGeometry,
-  )
-import Numeric (showFFloat, showHex)
-
--- | Every boundary at which the exhibit may be refused. The renderer never
--- invents a fallback geometry or topology state.
-data PersistenceRoseError
-  = RoseDelaunayBuildFailed !BuildError
-  | RoseAlphaFiltrationFailed !AlphaFiltrationError
-  | RoseCellComplexLoweringFailed !DCELError
-  | RosePersistenceFailed !HomologyFailure
-  | RoseCriticalProfileCountMismatch !Int !Int
-  | RoseCriticalProfileDegreeCountMismatch !Int !Int
-  | RoseBirthRankMissing !AlphaBirth
-  | RoseEdgeBirthMissing !UndirectedEdgeId
-  | RoseFaceBirthMissing !FaceId
-  | RoseBoundaryDescentFailed !Int !BoundaryObstruction
-  | RoseNoCriticalStates
-  | RoseNoMaskRegions
-  | RoseNoPersistentHole
-  | RoseNoCoherentHero !Int ![(Int, Int)]
-  | RoseVoronoiGeometryMissing !UndirectedEdgeId
-  | RoseNaturalNeighborFallbacks !Int
-  | RosePointInvalid !PointValidationError
-  | RoseRegionInvalid !RegionValidationError
-  | RoseOverlayFailed !(OverlayError Bool Bool)
-  | RoseOverlayPublicationFailed !RegionPublicationError
-  | RoseMinkowskiFailed !MinkowskiError
-  | RoseFrameCountTooSmall !Int
-  | RoseEclipseFiniteHoleCountMismatch !Int
-  | RoseEclipseFiniteHoleMissingDeath
-  | RoseEclipseBettiProfileMismatch !Int !Int !Int
-  | RoseGifInvalidSignature
-  | RoseGifTruncated !Int
-  | RoseGifUnexpectedBlock !Int !Word8
-  | RoseGifDimensionMismatch !Int !Int !Int !Int
-  | RoseGifFrameCountMismatch !Int !Int
-  | RoseGifFrameDelayCountMismatch !Int !Int
-  | RoseGifDurationMismatch !Int !Int
-  | RoseGifProvenanceMismatch
-  | RoseGifMarkerTooLong !Int
-  deriving stock (Show)
-
--- | A compact, test-readable projection of everything the scene actually
--- computed. It is intentionally not another geometry representation.
-data PersistenceRoseReceipt = PersistenceRoseReceipt
-  { roseSiteCount :: !Int,
-    roseEdgeCount :: !Int,
-    roseFaceCount :: !Int,
-    roseCriticalBirthCount :: !Int,
-    roseFinitePersistentHoleCount :: !Int,
-    roseEssentialPersistentHoleCount :: !Int,
-    roseHeroCriticalRank :: !Int,
-    roseHeroBettiOne :: !Int,
-    roseHeroGeometricHoleCount :: !Int,
-    roseVoronoiSegmentCount :: !Int,
-    roseNaturalNeighborSampleCount :: !Int,
-    roseNaturalNeighborFallbackCount :: !Int,
-    roseMaskComponentCount :: !Int
-  }
-  deriving stock (Eq, Show)
-
-data PersistenceRoseScene = PersistenceRoseScene
-  { sceneSites :: ![Point],
-    sceneEdges :: ![RankedSegment],
-    sceneFaces :: ![RankedTriangle],
-    sceneCriticalStates :: !(NonEmpty CriticalState),
-    sceneHeroState :: !CriticalState,
-    sceneVoronoiSegments :: ![Segment],
-    sceneFieldSamples :: ![FieldSample],
-    sceneMaskComponents :: ![MaskComponent],
-    scenePersistenceArcs :: ![PersistenceArc],
-    sceneReceipt :: !PersistenceRoseReceipt
-  }
-
--- | The canonical source projection carried inside the retained GIF. The GIF
--- codec remains an effect-boundary view; this receipt binds that binary view
--- to the exact deterministic SVG frame section used to produce it.
-data PersistenceRoseAnimationReceipt = PersistenceRoseAnimationReceipt
-  { roseAnimationFrameCount :: !Int,
-    roseAnimationFramesPerSecond :: !Int,
-    roseAnimationWidth :: !Int,
-    roseAnimationHeight :: !Int,
-    roseAnimationSourceFingerprint :: !Word64
-  }
-  deriving stock (Eq, Show)
-
--- | The exact, test-readable facts shown by the alpha eclipse. The numerator
--- and denominator fields are projections of opaque 'AlphaBirth' values; the
--- renderer never chooses thresholds independently of the filtration.
-data AlphaEclipseReceipt = AlphaEclipseReceipt
-  { eclipseSiteCount :: !Int,
-    eclipseEdgeCount :: !Int,
-    eclipseFaceCount :: !Int,
-    eclipseHoleBirthNumerator :: !Integer,
-    eclipseHoleBirthDenominator :: !Integer,
-    eclipseHoleDeathNumerator :: !Integer,
-    eclipseHoleDeathDenominator :: !Integer,
-    eclipseBettiBeforeBirth :: !Int,
-    eclipseBettiAtBirth :: !Int,
-    eclipseBettiAtDeath :: !Int
-  }
-  deriving stock (Eq, Show)
-
--- | A three-section observation of one fixed resident Delaunay DCEL. Every
--- displayed edge and face is projected from that DCEL with its exact birth
--- rank; only the selected sublevel differs between sections.
-data AlphaEclipseScene = AlphaEclipseScene
-  { eclipseSites :: ![Point],
-    eclipseEdges :: ![RankedSegment],
-    eclipseFaces :: ![RankedTriangle],
-    eclipseBeforeState :: !CriticalState,
-    eclipseBirthState :: !CriticalState,
-    eclipseDeathState :: !CriticalState,
-    eclipseReceipt :: !AlphaEclipseReceipt
-  }
-
--- | The shared local descent used by both exhibits. This is not a second
--- filtration carrier: it retains the canonical mesh and filtration beside
--- their checked Homology projection and only the ranked rendering views.
-data AlphaProjection = AlphaProjection
-  { projectionTriangulation :: !(DelaunayTriangulation ()),
-    projectionPersistencePairs :: ![PersistencePair AlphaBirth],
-    projectionCriticalStates :: !(NonEmpty CriticalState),
-    projectionBirthRanks :: !(Map AlphaBirth Int),
-    projectionEdges :: ![RankedSegment],
-    projectionFaces :: ![RankedTriangle]
-  }
-
-newtype PersistenceRoseFrame = PersistenceRoseFrame
-  { frameProgress :: Double
-  }
-  deriving stock (Eq, Show)
-
-data CriticalState = CriticalState
-  { criticalRank :: !Int,
-    criticalBirth :: !AlphaBirth,
-    criticalBettiOne :: !Int,
-    criticalHoles :: ![HoleComponent]
-  }
-
-data RankedSegment = RankedSegment
-  { rankedSegmentGeometry :: !Segment,
-    rankedSegmentBirthRank :: !Int
-  }
-
-data RankedTriangle = RankedTriangle
-  { rankedTrianglePoints :: !(Point, Point, Point),
-    rankedTriangleBirthRank :: !Int
-  }
-
-data Segment = Segment !Point !Point
-
-newtype HoleComponent = HoleComponent [[Point]]
-
-data FieldSample = FieldSample
-  { fieldSamplePoint :: !Point,
-    fieldSampleValue :: !Double,
-    fieldSampleNeighborCount :: !Int
-  }
-
-data MaskComponent = MaskComponent
-  { maskOuterLoop :: ![Point],
-    maskHoleLoops :: ![[Point]]
-  }
-
-data PersistenceArc = PersistenceArc
-  { persistenceArcBirthRank :: !Int,
-    persistenceArcDeathRank :: !Int
-  }
-
-buildPersistenceRoseScene :: Either PersistenceRoseError PersistenceRoseScene
-buildPersistenceRoseScene = do
-  projection <- buildAlphaProjection roseSites
-  let triangulation = projectionTriangulation projection
-      criticalStates = projectionCriticalStates projection
-      persistencePairs = projectionPersistencePairs projection
-      criticalBirthCount = NonEmpty.length criticalStates
-  heroState <- chooseHeroState criticalStates
-  finiteVoronoiSegments <- projectFiniteVoronoiSegments triangulation
-  (fieldSamples, fallbackCount) <- naturalNeighborField triangulation
-  if fallbackCount == 0
-    then pure ()
-    else Left (RoseNaturalNeighborFallbacks fallbackCount)
-  maskComponents <- buildRoseMask
-  persistenceArcs <-
-    projectPersistenceArcs
-      (projectionBirthRanks projection)
-      criticalBirthCount
-      persistencePairs
-  let finiteHoles = filter isFinitePositiveHole persistencePairs
-      essentialHoles = filter isEssentialHole persistencePairs
-      heroHoleCount = length (criticalHoles heroState)
-      receipt =
-        PersistenceRoseReceipt
-          { roseSiteCount = length (vertices triangulation),
-            roseEdgeCount = length (projectionEdges projection),
-            roseFaceCount = length (projectionFaces projection),
-            roseCriticalBirthCount = criticalBirthCount,
-            roseFinitePersistentHoleCount = length finiteHoles,
-            roseEssentialPersistentHoleCount = length essentialHoles,
-            roseHeroCriticalRank = criticalRank heroState,
-            roseHeroBettiOne = criticalBettiOne heroState,
-            roseHeroGeometricHoleCount = heroHoleCount,
-            roseVoronoiSegmentCount = length finiteVoronoiSegments,
-            roseNaturalNeighborSampleCount = length fieldSamples,
-            roseNaturalNeighborFallbackCount = fallbackCount,
-            roseMaskComponentCount = length maskComponents
-          }
-  pure
-    PersistenceRoseScene
-      { sceneSites = fmap (vertexPoint triangulation) (vertices triangulation),
-        sceneEdges = projectionEdges projection,
-        sceneFaces = projectionFaces projection,
-        sceneCriticalStates = criticalStates,
-        sceneHeroState = heroState,
-        sceneVoronoiSegments = finiteVoronoiSegments,
-        sceneFieldSamples = fieldSamples,
-        sceneMaskComponents = maskComponents,
-        scenePersistenceArcs = persistenceArcs,
-        sceneReceipt = receipt
-      }
-
-buildAlphaProjection :: [Point] -> Either PersistenceRoseError AlphaProjection
-buildAlphaProjection sites = do
-  triangulation <- first RoseDelaunayBuildFailed (delaunayGeometry (Vector.fromList sites))
-  filtration <- first RoseAlphaFiltrationFailed (alphaFiltration triangulation)
-  filteredComplex <- first RoseCellComplexLoweringFailed (filteredAlphaComplex filtration)
-  (persistencePairs, criticalBettiTable) <-
-    first RosePersistenceFailed
-      (mod2PersistentPairsWithCriticalBettiTable filteredComplex)
-  let criticalBirths = alphaFiltrationCriticalBirths filtration
-      criticalProfiles = criticalBettiVectors criticalBettiTable
-      birthRanks = Map.fromDistinctAscList (zip criticalBirths [0 ..])
-  if length criticalBirths == length criticalProfiles
-    then pure ()
-    else Left (RoseCriticalProfileCountMismatch (length criticalBirths) (length criticalProfiles))
-  criticalStates <-
-    traverse
-      (buildCriticalState triangulation filtration)
-      (zip3 [0 ..] criticalBirths criticalProfiles)
-      >>= maybe (Left RoseNoCriticalStates) Right . NonEmpty.nonEmpty
-  rankedEdges <-
-    traverse
-      (projectRankedEdge triangulation filtration birthRanks)
-      (undirectedEdges triangulation)
-  rankedFaces <-
-    traverse
-      (projectRankedFace triangulation filtration birthRanks)
-      (zip [1 ..] (Vector.toList (innerFaceVertexTriples triangulation)))
-  pure
-    AlphaProjection
-      { projectionTriangulation = triangulation,
-        projectionPersistencePairs = persistencePairs,
-        projectionCriticalStates = criticalStates,
-        projectionBirthRanks = birthRanks,
-        projectionEdges = rankedEdges,
-        projectionFaces = rankedFaces
-      }
-
-buildAlphaEclipseScene :: Either PersistenceRoseError AlphaEclipseScene
-buildAlphaEclipseScene = do
-  projection <- buildAlphaProjection alphaEclipseSites
-  finiteHoles <-
-    case filter isFinitePositiveHole (projectionPersistencePairs projection) of
-      [finiteHole] -> Right finiteHole
-      holes -> Left (RoseEclipseFiniteHoleCountMismatch (length holes))
-  deathBirth <-
-    maybe
-      (Left RoseEclipseFiniteHoleMissingDeath)
-      Right
-      (persistenceDeath finiteHoles)
-  let birth = persistenceBirth finiteHoles
-      birthRanks = projectionBirthRanks projection
-      states = projectionCriticalStates projection
-  birthRank <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  deathRank <- maybe (Left (RoseBirthRankMissing deathBirth)) Right (Map.lookup deathBirth birthRanks)
-  let beforeState = criticalStateAt (max 0 (birthRank - 1)) states
-      birthState = criticalStateAt birthRank states
-      deathState = criticalStateAt deathRank states
-      bettiProfile =
-        ( criticalBettiOne beforeState,
-          criticalBettiOne birthState,
-          criticalBettiOne deathState
-        )
-  case bettiProfile of
-    (0, 1, 0) -> pure ()
-    (beforeBetti, birthBetti, deathBetti) ->
-      Left (RoseEclipseBettiProfileMismatch beforeBetti birthBetti deathBetti)
-  let triangulation = projectionTriangulation projection
-      receipt =
-        AlphaEclipseReceipt
-          { eclipseSiteCount = length (vertices triangulation),
-            eclipseEdgeCount = length (projectionEdges projection),
-            eclipseFaceCount = length (projectionFaces projection),
-            eclipseHoleBirthNumerator = alphaBirthNumerator birth,
-            eclipseHoleBirthDenominator = alphaBirthDenominator birth,
-            eclipseHoleDeathNumerator = alphaBirthNumerator deathBirth,
-            eclipseHoleDeathDenominator = alphaBirthDenominator deathBirth,
-            eclipseBettiBeforeBirth = criticalBettiOne beforeState,
-            eclipseBettiAtBirth = criticalBettiOne birthState,
-            eclipseBettiAtDeath = criticalBettiOne deathState
-          }
-  pure
-    AlphaEclipseScene
-      { eclipseSites = fmap (vertexPoint triangulation) (vertices triangulation),
-        eclipseEdges = projectionEdges projection,
-        eclipseFaces = projectionFaces projection,
-        eclipseBeforeState = beforeState,
-        eclipseBirthState = birthState,
-        eclipseDeathState = deathState,
-        eclipseReceipt = receipt
-      }
-
-persistenceRoseReceipt :: PersistenceRoseScene -> PersistenceRoseReceipt
-persistenceRoseReceipt = sceneReceipt
-
-alphaEclipseReceipt :: AlphaEclipseScene -> AlphaEclipseReceipt
-alphaEclipseReceipt = eclipseReceipt
-
-renderPersistenceRoseReceipt :: PersistenceRoseReceipt -> String
-renderPersistenceRoseReceipt receipt =
-  intercalate
-    " · "
-    [ "sites " <> show (roseSiteCount receipt),
-      "edges " <> show (roseEdgeCount receipt),
-      "faces " <> show (roseFaceCount receipt),
-      "critical births " <> show (roseCriticalBirthCount receipt),
-      "finite H1 intervals " <> show (roseFinitePersistentHoleCount receipt),
-      "essential H1 intervals " <> show (roseEssentialPersistentHoleCount receipt),
-      "hero rank " <> show (roseHeroCriticalRank receipt),
-      "beta1 " <> show (roseHeroBettiOne receipt),
-      "geometric holes " <> show (roseHeroGeometricHoleCount receipt),
-      "Voronoi segments " <> show (roseVoronoiSegmentCount receipt),
-      "Sibson samples " <> show (roseNaturalNeighborSampleCount receipt),
-      "fallbacks " <> show (roseNaturalNeighborFallbackCount receipt),
-      "mask components " <> show (roseMaskComponentCount receipt)
-    ]
-
-renderAlphaEclipseReceipt :: AlphaEclipseReceipt -> String
-renderAlphaEclipseReceipt receipt =
-  intercalate
-    " · "
-    [ "sites " <> show (eclipseSiteCount receipt),
-      "edges " <> show (eclipseEdgeCount receipt),
-      "faces " <> show (eclipseFaceCount receipt),
-      "H1 ["
-        <> renderExactRatio (eclipseHoleBirthNumerator receipt) (eclipseHoleBirthDenominator receipt)
-        <> ", "
-        <> renderExactRatio (eclipseHoleDeathNumerator receipt) (eclipseHoleDeathDenominator receipt)
-        <> ")",
-      "beta1 "
-        <> show
-          ( eclipseBettiBeforeBirth receipt,
-            eclipseBettiAtBirth receipt,
-            eclipseBettiAtDeath receipt
-          )
-    ]
-
-buildCriticalState
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> (Int, AlphaBirth, [Int])
-  -> Either PersistenceRoseError CriticalState
-buildCriticalState triangulation filtration (rankValue, birthValue, profile) = do
-  bettiOne <-
-    case profile of
-      _ : secondDegree : _ : _ -> Right secondDegree
-      _ -> Left (RoseCriticalProfileDegreeCountMismatch rankValue (length profile))
-  holes <- holeComponentsAt rankValue triangulation filtration birthValue
-  pure
-    CriticalState
-      { criticalRank = rankValue,
-        criticalBirth = birthValue,
-        criticalBettiOne = bettiOne,
-        criticalHoles = holes
-      }
-
-chooseHeroState :: NonEmpty CriticalState -> Either PersistenceRoseError CriticalState
-chooseHeroState states =
-  let stateList = NonEmpty.toList states
-      maximumBettiOne = foldl' (\best state -> max best (criticalBettiOne state)) 0 stateList
-      maximalStates = filter ((== maximumBettiOne) . criticalBettiOne) stateList
-      agreements = fmap (\state -> (criticalBettiOne state, length (criticalHoles state))) maximalStates
-      coherentStates =
-        filter
-          (\state -> criticalBettiOne state > 0 && criticalBettiOne state == length (criticalHoles state))
-          stateList
-      sevenPetalStates = filter ((== rosePetalCount) . criticalBettiOne) coherentStates
-   in if maximumBettiOne <= 0
-        then Left RoseNoPersistentHole
-        else
-          case NonEmpty.nonEmpty sevenPetalStates of
-            Just preferredStates -> Right (latestCriticalState preferredStates)
-            Nothing ->
-              maybe
-                (Left (RoseNoCoherentHero maximumBettiOne agreements))
-                Right
-                (find (\state -> criticalBettiOne state == length (criticalHoles state)) maximalStates)
-
-latestCriticalState :: NonEmpty CriticalState -> CriticalState
-latestCriticalState (firstState :| remainingStates) =
-  foldl'
-    (\latest candidate -> if criticalRank candidate > criticalRank latest then candidate else latest)
-    firstState
-    remainingStates
-
-holeComponentsAt
-  :: Int
-  -> DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> AlphaBirth
-  -> Either PersistenceRoseError [HoleComponent]
-holeComponentsAt rankValue triangulation filtration threshold = do
-  inactiveEdges <-
-    inactiveHandlesAt
-      RoseEdgeBirthMissing
-      (alphaEdgeBirth filtration)
-      (undirectedEdges triangulation)
-      threshold
-  inactiveFaces <-
-    inactiveHandlesAt
-      RoseFaceBirthMissing
-      (alphaFaceBirth filtration)
-      (innerFaces triangulation)
-      threshold
-  let edgeInactive edge = Set.member edge inactiveEdges
-      faceInactive face = Set.member face inactiveFaces
-      exteriorFaces = Set.fromList (outerFaceFloodFill triangulation edgeInactive)
-      inactiveComponents =
-        fmap snd
-          ( faceComponentsBy
-              triangulation
-              (\face -> if faceInactive face then Just () else Nothing)
-              edgeInactive
-          )
-      boundedComponents =
-        filter
-          (all (`Set.notMember` exteriorFaces) . faceComponentFaces)
-          inactiveComponents
-  traverse
-    (projectHoleComponent rankValue triangulation)
-    boundedComponents
-
-inactiveHandlesAt
-  :: Ord handle
-  => (handle -> PersistenceRoseError)
-  -> (handle -> Maybe AlphaBirth)
-  -> [handle]
-  -> AlphaBirth
-  -> Either PersistenceRoseError (Set.Set handle)
-inactiveHandlesAt missingBirth birthAt handles threshold =
-  Set.fromList . mapMaybe id
-    <$> traverse
-      ( \handle ->
-          maybe
-            (Left (missingBirth handle))
-            (\birth -> Right (if birth > threshold then Just handle else Nothing))
-            (birthAt handle)
-      )
-      handles
-
-projectHoleComponent
-  :: Int
-  -> DelaunayTriangulation ()
-  -> FaceComponent
-  -> Either PersistenceRoseError HoleComponent
-projectHoleComponent rankValue triangulation component = do
-  loops <-
-    first (RoseBoundaryDescentFailed rankValue)
-      (componentBoundaryLoops triangulation component)
-  pure
-    ( HoleComponent
-        ( fmap
-            (fmap (vertexPoint triangulation) . NonEmpty.toList . boundaryLoopVertices)
-            (NonEmpty.toList loops)
-        )
-    )
-
-projectRankedEdge
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> Map AlphaBirth Int
-  -> UndirectedEdgeId
-  -> Either PersistenceRoseError RankedSegment
-projectRankedEdge triangulation filtration birthRanks edge = do
-  birth <- maybe (Left (RoseEdgeBirthMissing edge)) Right (alphaEdgeBirth filtration edge)
-  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-  pure
-    RankedSegment
-      { rankedSegmentGeometry = Segment (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex),
-        rankedSegmentBirthRank = rankValue
-      }
-
-projectRankedFace
-  :: DelaunayTriangulation ()
-  -> AlphaFiltration
-  -> Map AlphaBirth Int
-  -> (Int, (VertexId, VertexId, VertexId))
-  -> Either PersistenceRoseError RankedTriangle
-projectRankedFace triangulation filtration birthRanks (rawFace, (firstVertex, secondVertex, thirdVertex)) = do
-  let face = toFaceId rawFace
-  birth <- maybe (Left (RoseFaceBirthMissing face)) Right (alphaFaceBirth filtration face)
-  rankValue <- maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-  pure
-    RankedTriangle
-      { rankedTrianglePoints =
-          ( vertexPoint triangulation firstVertex,
-            vertexPoint triangulation secondVertex,
-            vertexPoint triangulation thirdVertex
-          ),
-        rankedTriangleBirthRank = rankValue
-      }
-
-toFaceId :: Int -> FaceId
-toFaceId = FaceId . fromIntegral
-
-projectFiniteVoronoiSegments
-  :: DelaunayTriangulation ()
-  -> Either PersistenceRoseError [Segment]
-projectFiniteVoronoiSegments triangulation =
-  fmap (mapMaybe id)
-    . traverse projectSegment
-    $ undirectedEdges triangulation
- where
-  projectSegment :: UndirectedEdgeId -> Either PersistenceRoseError (Maybe Segment)
-  projectSegment edge =
-    case voronoiEdgeGeometry triangulation (asDirectedVoronoiEdge (normalizedDirected edge)) of
-      Nothing -> Left (RoseVoronoiGeometryMissing edge)
-      Just (VoronoiSegment fromPoint toPoint) -> Right (Just (Segment fromPoint toPoint))
-      Just (VoronoiRay _ _) -> Right Nothing
-      Just (VoronoiLine _ _) -> Right Nothing
-
-naturalNeighborField
-  :: DelaunayTriangulation ()
-  -> Either PersistenceRoseError ([FieldSample], Int)
-naturalNeighborField triangulation = do
-  admittedQueries <- traverse (first RosePointInvalid . mkQueryPoint) fieldQueryPoints
-  let results = runST $ do
-        workspace <- newNaturalNeighborWorkspace triangulation
-        traverse (naturalNeighborWeights workspace Nothing) admittedQueries
-      pairedResults = zip admittedQueries results
-      fallbackCount = length (filter (interpolationUsedFallback . naturalNeighborStats . snd) pairedResults)
-      samples = mapMaybe (fieldSampleFromResult triangulation) pairedResults
-  pure (samples, fallbackCount)
-
-fieldSampleFromResult
-  :: DelaunayTriangulation ()
-  -> (QueryPoint, NaturalNeighborResult)
-  -> Maybe FieldSample
-fieldSampleFromResult triangulation (query, result)
-  | Vector.null weights = Nothing
-  | interpolationUsedFallback (naturalNeighborStats result) = Nothing
-  | otherwise =
-      let weightedValue =
-            Vector.foldl'
-              (\total (vertex, weight) -> total + weight * siteSignal (vertexPoint triangulation vertex))
-              0
-              weights
-       in Just
-            FieldSample
-              { fieldSamplePoint = queryPointValue query,
-                fieldSampleValue = clamp 0 1 weightedValue,
-                fieldSampleNeighborCount = Vector.length weights
-              }
- where
-  weights = naturalNeighborValues result
-
-siteSignal :: Point -> Double
-siteSignal (Point x y) =
-  clamp 0 1 (0.5 + 0.24 * sin (1.31 * x - 0.73 * y) + 0.18 * cos (0.61 * x + 1.17 * y))
-
-fieldQueryPoints :: [Point]
-fieldQueryPoints =
-  [ Point (fromIntegral column * 0.34 - 5.1) (fromIntegral row * 0.34 - 4.42)
-  | row <- [0 .. 26 :: Int],
-    column <- [0 .. 30 :: Int]
-  ]
-
-projectPersistenceArcs
-  :: Map AlphaBirth Int
-  -> Int
-  -> [PersistencePair AlphaBirth]
-  -> Either PersistenceRoseError [PersistenceArc]
-projectPersistenceArcs birthRanks criticalCount pairs = do
-  arcs <- traverse pairArc (filter isPositiveHole pairs)
-  pure (take 36 (sortOn (Down . persistenceArcLifetime) arcs))
- where
-  finalRank = max 0 (criticalCount - 1)
-  pairArc pair = do
-    birthRank <- rankOf (persistenceBirth pair)
-    deathRank <- maybe (Right finalRank) rankOf (persistenceDeath pair)
-    pure
-      PersistenceArc
-        { persistenceArcBirthRank = birthRank,
-          persistenceArcDeathRank = deathRank
-        }
-  rankOf birth = maybe (Left (RoseBirthRankMissing birth)) Right (Map.lookup birth birthRanks)
-
-persistenceArcLifetime :: PersistenceArc -> Int
-persistenceArcLifetime PersistenceArc{persistenceArcBirthRank, persistenceArcDeathRank} =
-  max 0 (persistenceArcDeathRank - persistenceArcBirthRank)
-
-isPositiveHole :: PersistencePair AlphaBirth -> Bool
-isPositiveHole pair =
-  persistenceDegree pair == HomologicalDegree 1
-    && maybe True (> persistenceBirth pair) (persistenceDeath pair)
-
-isFinitePositiveHole :: PersistencePair AlphaBirth -> Bool
-isFinitePositiveHole pair = isPositiveHole pair && maybe False (> persistenceBirth pair) (persistenceDeath pair)
-
-isEssentialHole :: PersistencePair AlphaBirth -> Bool
-isEssentialHole pair = persistenceDegree pair == HomologicalDegree 1 && persistenceDeath pair == Nothing
-
-buildRoseMask :: Either PersistenceRoseError [MaskComponent]
-buildRoseMask = do
-  kernelPoints <- traverse (first RosePointInvalid . exactPointFromPoint) minkowskiKernel
-  kernel <- first RoseMinkowskiFailed (convexPolygon kernelPoints >>= structuringElement)
-  authoredRegions <- traverse authoredRegion roseMaskLoops
-  offsetRegions <-
-    traverse
-      (fmap fst . first RoseMinkowskiFailed . polygonOffset kernel)
-      authoredRegions
-  unioned <-
-    case NonEmpty.nonEmpty offsetRegions of
-      Nothing -> Left RoseNoMaskRegions
-      Just (firstRegion :| remainingRegions) -> foldM exactRegionUnion firstRegion remainingRegions
-  traverse projectMaskComponent (planarRegionComponents unioned)
-
-authoredRegion :: NonEmpty Point -> Either PersistenceRoseError PlanarRegion
-authoredRegion points = do
-  exactPoints <- traverse (first RosePointInvalid . exactPointFromPoint) points
-  loop <- first RoseRegionInvalid (exactLoop exactPoints)
-  component <- first RoseRegionInvalid (polygonComponent loop [])
-  first RoseRegionInvalid (planarRegion [component])
-
-exactRegionUnion :: PlanarRegion -> PlanarRegion -> Either PersistenceRoseError PlanarRegion
-exactRegionUnion leftRegion rightRegion = do
-  leftLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True leftRegion))
-  rightLayer <- first RoseRegionInvalid (planarLayer False (Map.singleton True rightRegion))
-  overlaid <- first RoseOverlayFailed (overlayLayers leftLayer rightLayer)
-  first RoseOverlayPublicationFailed (overlaySelectedRegion (uncurry (||)) overlaid)
-
-projectMaskComponent :: PolygonComponent -> Either PersistenceRoseError MaskComponent
-projectMaskComponent component =
-  MaskComponent
-    <$> projectExactLoop (polygonOuterLoop component)
-    <*> traverse projectExactLoop (polygonHoleLoops component)
-
-projectExactLoop :: ExactLoop -> Either PersistenceRoseError [Point]
-projectExactLoop =
-  traverse
-    (fmap queryPointValue . first RosePointInvalid . exactPointToEmbeddingCandidate)
-    . NonEmpty.toList
-    . exactLoopPoints
-
-roseSites :: [Point]
-roseSites =
-  concatMap petalSites [0 .. rosePetalCount - 1]
-    <> centerSites
-
-petalSites :: Int -> [Point]
-petalSites petalIndex =
-  fmap
-    (petalPoint petalIndex 2.52 1.82 0.69)
-    (sampleAngles 22 (0.021 * fromIntegral petalIndex))
-
-centerSites :: [Point]
-centerSites =
-  Point 0 0
-    : fmap
-      (\angle -> Point (0.56 * cos angle) (0.56 * sin angle))
-      (sampleAngles 14 0.037)
-
-roseMaskLoops :: [NonEmpty Point]
-roseMaskLoops =
-  fmap
-    (\petalIndex -> fmap (petalPoint petalIndex 2.42 2.12 0.9) (sampleAnglesNonEmpty 34 0))
-    [0 .. rosePetalCount - 1]
-    <> [fmap (\angle -> Point (1.08 * cos angle) (1.08 * sin angle)) (sampleAnglesNonEmpty 30 0)]
-
-petalPoint :: Int -> Double -> Double -> Double -> Double -> Point
-petalPoint petalIndex centerRadius radialRadius tangentialRadius localAngle =
-  let orientation = twoPi * fromIntegral petalIndex / fromIntegral rosePetalCount - pi / 2
-      radial = centerRadius + radialRadius * cos localAngle
-      tangential = tangentialRadius * sin localAngle * (0.91 + 0.09 * cos (2 * localAngle))
-      radialX = cos orientation
-      radialY = sin orientation
-      tangentX = -radialY
-      tangentY = radialX
-   in Point
-        (radial * radialX + tangential * tangentX)
-        (radial * radialY + tangential * tangentY)
-
-sampleAngles :: Int -> Double -> [Double]
-sampleAngles sampleCount phase =
-  fmap
-    (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
-    [0 .. sampleCount - 1]
-
-sampleAnglesNonEmpty :: Int -> Double -> NonEmpty Double
-sampleAnglesNonEmpty sampleCount phase =
-  phase
-    :| fmap
-      (\sampleIndex -> twoPi * fromIntegral sampleIndex / fromIntegral sampleCount + phase)
-      [1 .. max 1 sampleCount - 1]
-
-minkowskiKernel :: NonEmpty Point
-minkowskiKernel =
-  Point 0.085 0
-    :| [ Point 0.043 0.074,
-         Point (-0.043) 0.074,
-         Point (-0.085) 0,
-         Point (-0.043) (-0.074),
-         Point 0.043 (-0.074)
-       ]
-
-rosePetalCount :: Int
-rosePetalCount = 7
-
--- | The package's canonical exact persistence witness. Its four hull edges
--- enter at squared radius one; the resident diagonal and two resident faces
--- enter at squared radius two. Homology therefore observes exactly one finite
--- degree-one interval @[1,2)@ over this fixed DCEL.
-alphaEclipseSites :: [Point]
-alphaEclipseSites =
-  [ Point (-1) (-1),
-    Point 1 (-1),
-    Point 1 1,
-    Point (-1) 1
-  ]
-
-renderPersistenceRosePoster :: PersistenceRoseScene -> String
-renderPersistenceRosePoster scene =
-  renderScene scene (criticalRank (sceneHeroState scene)) 1 True
-
-renderAlphaEclipse :: AlphaEclipseScene -> String
-renderAlphaEclipse scene =
-  let receipt = eclipseReceipt scene
-      birthLabel =
-        renderExactRatio
-          (eclipseHoleBirthNumerator receipt)
-          (eclipseHoleBirthDenominator receipt)
-      deathLabel =
-        renderExactRatio
-          (eclipseHoleDeathNumerator receipt)
-          (eclipseHoleDeathDenominator receipt)
-   in concat
-        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1440 620\" width=\"1440\" height=\"620\" role=\"img\" aria-labelledby=\"title description\">\n",
-          "<title id=\"title\">Moonlight Triangulation alpha eclipse</title>\n",
-          "<desc id=\"description\">One fixed four-site Delaunay DCEL at three exact alpha sublevels. Its resident boundary creates one hole at squared radius ",
-          birthLabel,
-          ", and its resident diagonal and faces kill that hole at squared radius ",
-          deathLabel,
-          ".</desc>\n",
-          renderAlphaEclipseDefinitions,
-          "<rect width=\"1440\" height=\"620\" fill=\"url(#eclipse-background)\"/>\n",
-          "<circle cx=\"720\" cy=\"300\" r=\"246\" fill=\"#ff68bb\" opacity=\"0.035\" filter=\"url(#eclipse-soft-glow)\"/>\n",
-          "<text x=\"58\" y=\"58\" fill=\"#84f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"17\" font-weight=\"600\" letter-spacing=\"6\">MOONLIGHT TRIANGULATION</text>\n",
-          "<text x=\"58\" y=\"101\" fill=\"#f7f3ff\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"38\" font-weight=\"420\">ALPHA ECLIPSE</text>\n",
-          "<text x=\"1382\" y=\"61\" text-anchor=\"end\" fill=\"#9baedb\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.5\">ONE FIXED DELAUNAY DCEL · EXACT CRITICAL BIRTHS</text>\n",
-          renderAlphaEclipseStage
-            scene
-            245
-            "BEFORE BIRTH"
-            "four isolated sites"
-            (eclipseBeforeState scene),
-          renderAlphaEclipseStage
-            scene
-            720
-            "LOOP BORN"
-            "resident boundary closes"
-            (eclipseBirthState scene),
-          renderAlphaEclipseStage
-            scene
-            1195
-            "CYCLE KILLED"
-            "resident faces fill it"
-            (eclipseDeathState scene),
-          "<g font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\">\n",
-          "<text x=\"58\" y=\"516\" fill=\"#9badd8\" font-size=\"13\" letter-spacing=\"2.5\">ONE EXACT H₁ INTERVAL</text>\n",
-          "<line x1=\"58\" y1=\"559\" x2=\"1382\" y2=\"559\" stroke=\"#344070\" stroke-width=\"3\"/>\n",
-          "<line x1=\"535\" y1=\"559\" x2=\"1042\" y2=\"559\" stroke=\"#ff72c6\" stroke-width=\"9\" stroke-linecap=\"round\"/>\n",
-          "<circle cx=\"535\" cy=\"559\" r=\"8\" fill=\"#fff6d6\" stroke=\"#ff72c6\" stroke-width=\"3\"/>\n",
-          "<rect x=\"1034\" y=\"551\" width=\"16\" height=\"16\" fill=\"#fff6d6\" stroke=\"#66efff\" stroke-width=\"3\"/>\n",
-          "<text x=\"58\" y=\"589\" fill=\"#7588ba\" font-size=\"12\">α² = 0</text>\n",
-          "<text x=\"535\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">birth α² = ",
-          birthLabel,
-          "</text>\n",
-          "<text x=\"788\" y=\"548\" text-anchor=\"middle\" fill=\"#fff6d6\" font-size=\"14\">the eclipse hole persists</text>\n",
-          "<text x=\"1042\" y=\"589\" text-anchor=\"middle\" fill=\"#f7f3ff\" font-size=\"12\">death α² = ",
-          deathLabel,
-          "</text>\n",
-          "<text x=\"1382\" y=\"589\" text-anchor=\"end\" fill=\"#7588ba\" font-size=\"12\">exact α² increases →</text>\n",
-          "</g>\n",
-          "</svg>\n"
-        ]
-
-renderAlphaEclipseDefinitions :: String
-renderAlphaEclipseDefinitions =
-  concat
-    [ "<defs>\n",
-      "<radialGradient id=\"eclipse-background\" cx=\"50%\" cy=\"38%\" r=\"78%\"><stop offset=\"0\" stop-color=\"#111744\"/><stop offset=\"0.55\" stop-color=\"#070b25\"/><stop offset=\"1\" stop-color=\"#02040f\"/></radialGradient>\n",
-      "<linearGradient id=\"eclipse-corona\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#66efff\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#ffd86b\"/></linearGradient>\n",
-      "<linearGradient id=\"eclipse-face\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#7c4dff\"/><stop offset=\"0.5\" stop-color=\"#ff4fa3\"/><stop offset=\"1\" stop-color=\"#ffbf5b\"/></linearGradient>\n",
-      "<filter id=\"eclipse-soft-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<filter id=\"eclipse-dot-glow\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"/><feMerge><feMergeNode in=\"blur\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "</defs>\n"
-    ]
-
-renderAlphaEclipseStage
-  :: AlphaEclipseScene
-  -> Double
-  -> String
-  -> String
-  -> CriticalState
-  -> String
-renderAlphaEclipseStage scene centerX stageLabel explanation state =
-  let rankValue = criticalRank state
-      activeFaces = filter ((<= rankValue) . rankedTriangleBirthRank) (eclipseFaces scene)
-      activeEdges = filter ((<= rankValue) . rankedSegmentBirthRank) (eclipseEdges scene)
-      highlighted = criticalBettiOne state > 0
-      frameStroke = if highlighted then "#ff6fbd" else "#344070"
-      frameOpacity = if highlighted then "0.72" else "0.55"
-   in concat
-        [ "<g transform=\"translate(",
-          formatDouble centerX,
-          " 292)\">\n",
-          "<rect x=\"-194\" y=\"-150\" width=\"388\" height=\"292\" rx=\"28\" fill=\"#090d26\" fill-opacity=\"0.72\" stroke=\"",
-          frameStroke,
-          "\" stroke-opacity=\"",
-          frameOpacity,
-          "\"/>\n",
-          "<text x=\"-162\" y=\"-114\" fill=\"",
-          if highlighted then "#ff89cb" else "#8fa4d6",
-          "\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"13\" letter-spacing=\"2.4\">",
-          stageLabel,
-          "</text>\n",
-          "<circle r=\"104\" fill=\"#01020a\" stroke=\"#7fefff\" stroke-opacity=\"0.18\" stroke-width=\"2\"/>\n",
-          "<g fill=\"url(#eclipse-face)\" fill-opacity=\"0.58\" stroke=\"#ffd56d\" stroke-opacity=\"0.48\" stroke-width=\"1.2\">",
-          concatMap renderAlphaEclipseTriangle activeFaces,
-          "</g>\n",
-          "<g fill=\"none\" stroke=\"url(#eclipse-corona)\" stroke-width=\"5\" stroke-linejoin=\"round\" stroke-linecap=\"round\" filter=\"url(#eclipse-soft-glow)\">",
-          concatMap renderAlphaEclipseSegment activeEdges,
-          "</g>\n",
-          "<g fill=\"none\" stroke=\"#fff7da\" stroke-width=\"3.2\" stroke-linejoin=\"round\" opacity=\"0.86\">",
-          concatMap renderAlphaEclipseHole (criticalHoles state),
-          "</g>\n",
-          "<g fill=\"#fff8df\" stroke=\"#ff75c5\" stroke-width=\"2\" filter=\"url(#eclipse-dot-glow)\">",
-          concatMap renderAlphaEclipseSite (eclipseSites scene),
-          "</g>\n",
-          "<rect x=\"-126\" y=\"-18\" width=\"252\" height=\"36\" rx=\"18\" fill=\"#05071a\" fill-opacity=\"0.82\"/>\n",
-          "<text y=\"5\" text-anchor=\"middle\" fill=\"#fff7da\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"14\" letter-spacing=\"1.2\">",
-          explanation,
-          "</text>\n",
-          "<text x=\"-162\" y=\"121\" fill=\"#fff4cc\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"25\">β₁ = ",
-          show (criticalBettiOne state),
-          "</text>\n",
-          "<text x=\"162\" y=\"121\" text-anchor=\"end\" fill=\"#8495c4\" font-family=\"Inter,ui-sans-serif,system-ui,sans-serif\" font-size=\"12\">α² = ",
-          renderAlphaBirth (criticalBirth state),
-          "</text>\n",
-          "</g>\n"
-        ]
-
-renderAlphaEclipseTriangle :: RankedTriangle -> String
-renderAlphaEclipseTriangle RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint)} =
-  "<polygon points=\""
-    <> renderScreenPoints (fmap toAlphaEclipseLocal [firstPoint, secondPoint, thirdPoint])
-    <> "\"/>\n"
-
-renderAlphaEclipseSegment :: RankedSegment -> String
-renderAlphaEclipseSegment RankedSegment{rankedSegmentGeometry = Segment firstPoint secondPoint} =
-  let Point firstX firstY = toAlphaEclipseLocal firstPoint
-      Point secondX secondY = toAlphaEclipseLocal secondPoint
-   in "<line x1=\""
-        <> formatDouble firstX
-        <> "\" y1=\""
-        <> formatDouble firstY
-        <> "\" x2=\""
-        <> formatDouble secondX
-        <> "\" y2=\""
-        <> formatDouble secondY
-        <> "\"/>\n"
-
-renderAlphaEclipseHole :: HoleComponent -> String
-renderAlphaEclipseHole (HoleComponent loops) =
-  concatMap
-    (\points -> "<path d=\"" <> renderClosedPath (fmap toAlphaEclipseLocal points) <> "\"/>\n")
-    loops
-
-renderAlphaEclipseSite :: Point -> String
-renderAlphaEclipseSite point =
-  let Point x y = toAlphaEclipseLocal point
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\"6\"/>\n"
-
-toAlphaEclipseLocal :: Point -> Point
-toAlphaEclipseLocal (Point x y) =
-  let scale = 72
-      inverseSquareRootTwo = sqrt 0.5
-   in Point
-        (scale * inverseSquareRootTwo * (x - y))
-        (scale * inverseSquareRootTwo * (x + y))
-
-renderAlphaBirth :: AlphaBirth -> String
-renderAlphaBirth birth =
-  renderExactRatio (alphaBirthNumerator birth) (alphaBirthDenominator birth)
-
-renderExactRatio :: Integer -> Integer -> String
-renderExactRatio numerator denominator
-  | denominator == 1 = show numerator
-  | otherwise = show numerator <> "/" <> show denominator
-
-persistenceRoseFrameSpecs :: Int -> Either PersistenceRoseError [PersistenceRoseFrame]
-persistenceRoseFrameSpecs frameCount
-  | frameCount < 2 = Left (RoseFrameCountTooSmall frameCount)
-  | otherwise =
-      Right
-        ( fmap
-            (\frameIndex ->
-               PersistenceRoseFrame
-                 { frameProgress = frameProgressAt frameCount frameIndex
-                 })
-            [0 .. frameCount - 1]
-        )
-
-renderPersistenceRoseFrame :: PersistenceRoseScene -> PersistenceRoseFrame -> String
-renderPersistenceRoseFrame scene frame =
-  let finalRank = criticalRank (NonEmpty.last (sceneCriticalStates scene))
-      selectedRank = clampInt 0 finalRank (floor (frameProgress frame * fromIntegral finalRank))
-   in renderScene scene selectedRank (frameProgress frame) False
-
-persistenceRoseAnimationReceipt
-  :: PersistenceRoseScene
-  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
-persistenceRoseAnimationReceipt scene = do
-  frames <- persistenceRoseFrameSpecs canonicalAnimationFrameCount
-  let renderedFrames = fmap (renderPersistenceRoseFrame scene) frames
-  pure
-    PersistenceRoseAnimationReceipt
-      { roseAnimationFrameCount = canonicalAnimationFrameCount,
-        roseAnimationFramesPerSecond = canonicalAnimationFramesPerSecond,
-        roseAnimationWidth = canonicalAnimationWidth,
-        roseAnimationHeight = canonicalAnimationHeight,
-        roseAnimationSourceFingerprint = fingerprintSections renderedFrames
-      }
-
-renderPersistenceRoseAnimationReceipt :: PersistenceRoseAnimationReceipt -> String
-renderPersistenceRoseAnimationReceipt receipt =
-  intercalate
-    " "
-    [ "MOONLIGHT_PERSISTENCE_ROSE",
-      "frame-count=" <> show (roseAnimationFrameCount receipt),
-      "fps=" <> show (roseAnimationFramesPerSecond receipt),
-      "size=" <> show (roseAnimationWidth receipt) <> "x" <> show (roseAnimationHeight receipt),
-      "source-fnv1a64=" <> renderWord64Hex (roseAnimationSourceFingerprint receipt)
-    ]
-
-stampPersistenceRoseGif
-  :: PersistenceRoseScene
-  -> ByteString
-  -> Either PersistenceRoseError (PersistenceRoseAnimationReceipt, ByteString)
-stampPersistenceRoseGif scene gifBytes = do
-  receipt <- persistenceRoseAnimationReceipt scene
-  summary <- inspectGif gifBytes
-  validateGifSummary receipt summary
-  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
-      markerLength = ByteString.length marker
-  if markerLength > 255
-    then Left (RoseGifMarkerTooLong markerLength)
-    else
-      if marker `ByteString.isInfixOf` gifBytes
-        then Right (receipt, gifBytes)
-        else
-          if gifMarkerPrefix `ByteString.isInfixOf` gifBytes
-            then Left RoseGifProvenanceMismatch
-            else do
-              insertionOffset <- gifDataStart gifBytes
-              let (prefix, suffix) = ByteString.splitAt insertionOffset gifBytes
-                  commentExtension =
-                    ByteString.pack [0x21, 0xfe, fromIntegral markerLength]
-                      <> marker
-                      <> ByteString.singleton 0
-              pure (receipt, prefix <> commentExtension <> suffix)
-
-verifyPersistenceRoseGif
-  :: PersistenceRoseScene
-  -> ByteString
-  -> Either PersistenceRoseError PersistenceRoseAnimationReceipt
-verifyPersistenceRoseGif scene gifBytes = do
-  receipt <- persistenceRoseAnimationReceipt scene
-  summary <- inspectGif gifBytes
-  validateGifSummary receipt summary
-  let marker = ByteString.Char8.pack (renderPersistenceRoseAnimationReceipt receipt)
-  if marker `ByteString.isInfixOf` gifBytes
-    then Right receipt
-    else Left RoseGifProvenanceMismatch
-
-data GifSummary = GifSummary
-  { gifSummaryWidth :: !Int,
-    gifSummaryHeight :: !Int,
-    gifSummaryFrameCount :: !Int,
-    gifSummaryFrameDelayCount :: !Int,
-    gifSummaryDurationCentiseconds :: !Int
-  }
-
-inspectGif :: ByteString -> Either PersistenceRoseError GifSummary
-inspectGif gifBytes = do
-  dataStart <- gifDataStart gifBytes
-  width <- gifLittleEndianWord16 gifBytes 6
-  height <- gifLittleEndianWord16 gifBytes 8
-  (frameCount, frameDelayCount, durationCentiseconds) <-
-    countGifFrames gifBytes dataStart 0 0 0
-  pure
-    GifSummary
-      { gifSummaryWidth = width,
-        gifSummaryHeight = height,
-        gifSummaryFrameCount = frameCount,
-        gifSummaryFrameDelayCount = frameDelayCount,
-        gifSummaryDurationCentiseconds = durationCentiseconds
-      }
-
-gifDataStart :: ByteString -> Either PersistenceRoseError Int
-gifDataStart gifBytes = do
-  signature <- maybe (Left (RoseGifTruncated 0)) Right (gifSlice gifBytes 0 6)
-  if signature == ByteString.Char8.pack "GIF87a" || signature == ByteString.Char8.pack "GIF89a"
-    then pure ()
-    else Left RoseGifInvalidSignature
-  packed <- maybe (Left (RoseGifTruncated 10)) Right (gifByteAt gifBytes 10)
-  let globalColourTableBytes =
-        if packed .&. 0x80 == 0
-          then 0
-          else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
-      dataStart = 13 + globalColourTableBytes
-  maybe (Left (RoseGifTruncated dataStart)) (const (Right dataStart)) (gifSlice gifBytes 0 dataStart)
-
-countGifFrames
-  :: ByteString
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Either PersistenceRoseError (Int, Int, Int)
-countGifFrames gifBytes offset frameCount frameDelayCount durationCentiseconds = do
-  block <- maybe (Left (RoseGifTruncated offset)) Right (gifByteAt gifBytes offset)
-  case block of
-    0x3b -> Right (frameCount, frameDelayCount, durationCentiseconds)
-    0x21 -> do
-      extensionLabel <- maybe (Left (RoseGifTruncated (offset + 1))) Right (gifByteAt gifBytes (offset + 1))
-      nextOffset <- skipGifSubBlocks gifBytes (offset + 2)
-      if extensionLabel == 0xf9
-        then do
-          delay <- gifLittleEndianWord16 gifBytes (offset + 4)
-          countGifFrames
-            gifBytes
-            nextOffset
-            frameCount
-            (frameDelayCount + 1)
-            (durationCentiseconds + delay)
-        else
-          countGifFrames
-            gifBytes
-            nextOffset
-            frameCount
-            frameDelayCount
-            durationCentiseconds
-    0x2c -> do
-      packed <- maybe (Left (RoseGifTruncated (offset + 9))) Right (gifByteAt gifBytes (offset + 9))
-      let localColourTableBytes =
-            if packed .&. 0x80 == 0
-              then 0
-              else 3 * (2 ^ (fromIntegral (packed .&. 0x07) + 1 :: Int))
-          imageDataStart = offset + 11 + localColourTableBytes
-      maybe (Left (RoseGifTruncated imageDataStart)) (const (Right ())) (gifByteAt gifBytes (imageDataStart - 1))
-      skipGifSubBlocks gifBytes imageDataStart >>= \next ->
-        countGifFrames
-          gifBytes
-          next
-          (frameCount + 1)
-          frameDelayCount
-          durationCentiseconds
-    0x00 ->
-      countGifFrames
-        gifBytes
-        (offset + 1)
-        frameCount
-        frameDelayCount
-        durationCentiseconds
-    unexpected -> Left (RoseGifUnexpectedBlock offset unexpected)
-
-skipGifSubBlocks :: ByteString -> Int -> Either PersistenceRoseError Int
-skipGifSubBlocks gifBytes offset = do
-  blockLength <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
-  if blockLength == 0
-    then Right (offset + 1)
-    else do
-      let nextOffset = offset + 1 + blockLength
-      maybe (Left (RoseGifTruncated nextOffset)) (const (Right ())) (gifSlice gifBytes offset (1 + blockLength))
-      skipGifSubBlocks gifBytes nextOffset
-
-validateGifSummary
-  :: PersistenceRoseAnimationReceipt
-  -> GifSummary
-  -> Either PersistenceRoseError ()
-validateGifSummary receipt summary
-  | actualWidth /= expectedWidth || actualHeight /= expectedHeight =
-      Left (RoseGifDimensionMismatch expectedWidth expectedHeight actualWidth actualHeight)
-  | actualFrameCount /= expectedFrameCount =
-      Left (RoseGifFrameCountMismatch expectedFrameCount actualFrameCount)
-  | actualFrameDelayCount /= expectedFrameCount =
-      Left (RoseGifFrameDelayCountMismatch expectedFrameCount actualFrameDelayCount)
-  | actualDurationCentiseconds /= canonicalAnimationDurationCentiseconds =
-      Left
-        ( RoseGifDurationMismatch
-            canonicalAnimationDurationCentiseconds
-            actualDurationCentiseconds
-        )
-  | otherwise = Right ()
- where
-  expectedWidth = roseAnimationWidth receipt
-  expectedHeight = roseAnimationHeight receipt
-  actualWidth = gifSummaryWidth summary
-  actualHeight = gifSummaryHeight summary
-  expectedFrameCount = roseAnimationFrameCount receipt
-  actualFrameCount = gifSummaryFrameCount summary
-  actualFrameDelayCount = gifSummaryFrameDelayCount summary
-  actualDurationCentiseconds = gifSummaryDurationCentiseconds summary
-
-gifLittleEndianWord16 :: ByteString -> Int -> Either PersistenceRoseError Int
-gifLittleEndianWord16 gifBytes offset = do
-  low <- maybe (Left (RoseGifTruncated offset)) (Right . fromIntegral) (gifByteAt gifBytes offset)
-  high <- maybe (Left (RoseGifTruncated (offset + 1))) (Right . fromIntegral) (gifByteAt gifBytes (offset + 1))
-  pure (low + 256 * high)
-
-gifByteAt :: ByteString -> Int -> Maybe Word8
-gifByteAt gifBytes offset = fst <$> ByteString.uncons (ByteString.drop offset gifBytes)
-
-gifSlice :: ByteString -> Int -> Int -> Maybe ByteString
-gifSlice gifBytes offset width =
-  let selected = ByteString.take width (ByteString.drop offset gifBytes)
-   in if ByteString.length selected == width then Just selected else Nothing
-
-fingerprintSections :: [String] -> Word64
-fingerprintSections =
-  foldl'
-    (\fingerprint section -> fingerprintByte (fingerprintText fingerprint section) 0)
-    fnvOffsetBasis
-
-fingerprintText :: Word64 -> String -> Word64
-fingerprintText fingerprint =
-  ByteString.foldl' fingerprintByte fingerprint
-    . Text.Encoding.encodeUtf8
-    . Text.pack
-
-fingerprintByte :: Word64 -> Word8 -> Word64
-fingerprintByte fingerprint byte =
-  (fingerprint `xor` fromIntegral byte) * fnvPrime
-
-renderWord64Hex :: Word64 -> String
-renderWord64Hex value =
-  let rendered = showHex value ""
-   in replicate (16 - length rendered) '0' <> rendered
-
-gifMarkerPrefix :: ByteString
-gifMarkerPrefix = ByteString.Char8.pack "MOONLIGHT_PERSISTENCE_ROSE "
-
-canonicalAnimationFrameCount :: Int
-canonicalAnimationFrameCount = 65
-
-canonicalAnimationFramesPerSecond :: Int
-canonicalAnimationFramesPerSecond = 8
-
-canonicalAnimationWidth :: Int
-canonicalAnimationWidth = 960
-
-canonicalAnimationHeight :: Int
-canonicalAnimationHeight = 540
-
-canonicalAnimationDurationCentiseconds :: Int
-canonicalAnimationDurationCentiseconds = 813
-
-fnvOffsetBasis :: Word64
-fnvOffsetBasis = 14695981039346656037
-
-fnvPrime :: Word64
-fnvPrime = 1099511628211
-
-frameProgressAt :: Int -> Int -> Double
-frameProgressAt frameCount frameIndex
-  | frameIndex <= 0 || frameIndex >= frameCount - 1 = 0
-  | otherwise =
-      let phase = fromIntegral frameIndex / fromIntegral (frameCount - 1)
-       in 0.5 - 0.5 * cos (twoPi * phase)
-
-renderScene :: PersistenceRoseScene -> Int -> Double -> Bool -> String
-renderScene scene selectedRank progress poster =
-  let selectedState = criticalStateAt selectedRank (sceneCriticalStates scene)
-      finalRank = max 1 (criticalRank (NonEmpty.last (sceneCriticalStates scene)))
-      activation = if poster then fromIntegral selectedRank / fromIntegral finalRank else progress
-   in concat
-        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1920 1080\" width=\"1920\" height=\"1080\" role=\"img\" aria-labelledby=\"title description\">\n",
-          "<title id=\"title\">Moonlight Triangulation Persistence Rose</title>\n",
-          "<desc id=\"description\">Exact Delaunay alpha filtration, persistent homology, Voronoi dual, Sibson interpolation, exact overlay, and Minkowski morphology composed into a seven-petal rose.</desc>\n",
-          renderDefinitions (sceneMaskComponents scene),
-          "<rect width=\"1920\" height=\"1080\" fill=\"url(#background)\"/>\n",
-          renderAmbientOrbits activation,
-          renderPersistenceHalo finalRank selectedRank (scenePersistenceArcs scene),
-          "<g clip-path=\"url(#rose-clip)\">\n",
-          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"url(#rose-wash)\" opacity=\"0.28\"/>\n",
-          "<g filter=\"url(#field-soften)\">", concatMap renderFieldSample (sceneFieldSamples scene), "</g>\n",
-          "<g opacity=\"0.58\">", concatMap renderVoronoiSegment (sceneVoronoiSegments scene), "</g>\n",
-          concatMap (renderRankedTriangle finalRank activation) (sceneFaces scene),
-          concatMap (renderRankedSegment finalRank activation) (sceneEdges scene),
-          "<g filter=\"url(#hole-glow)\">", concatMap renderHoleComponent (criticalHoles selectedState), "</g>\n",
-          concatMap (renderSite activation) (sceneSites scene),
-          "</g>\n",
-          "<path d=\"", renderMaskPath (sceneMaskComponents scene), "\" fill=\"none\" stroke=\"url(#mask-stroke)\" stroke-width=\"2.2\" opacity=\"0.92\" filter=\"url(#edge-glow)\"/>\n",
-          renderTitle selectedState,
-          "</svg>\n"
-        ]
-
-criticalStateAt :: Int -> NonEmpty CriticalState -> CriticalState
-criticalStateAt target (firstState :| remainingStates) =
-  foldl'
-    (\chosen candidate -> if criticalRank candidate <= target then candidate else chosen)
-    firstState
-    remainingStates
-
-renderDefinitions :: [MaskComponent] -> String
-renderDefinitions maskComponents =
-  concat
-    [ "<defs>\n",
-      "<radialGradient id=\"background\" cx=\"52%\" cy=\"48%\" r=\"72%\"><stop offset=\"0\" stop-color=\"#10143d\"/><stop offset=\"0.48\" stop-color=\"#070b24\"/><stop offset=\"1\" stop-color=\"#02040e\"/></radialGradient>\n",
-      "<radialGradient id=\"rose-wash\" cx=\"48%\" cy=\"42%\" r=\"63%\"><stop offset=\"0\" stop-color=\"#ffe7a3\"/><stop offset=\"0.38\" stop-color=\"#ff5fb7\"/><stop offset=\"0.72\" stop-color=\"#7c4dff\"/><stop offset=\"1\" stop-color=\"#00d9ff\"/></radialGradient>\n",
-      "<linearGradient id=\"mask-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#ffd86b\"/><stop offset=\"0.48\" stop-color=\"#ff72c6\"/><stop offset=\"1\" stop-color=\"#66efff\"/></linearGradient>\n",
-      "<linearGradient id=\"hole-stroke\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#fff9db\"/><stop offset=\"0.52\" stop-color=\"#ff9ad7\"/><stop offset=\"1\" stop-color=\"#9ff7ff\"/></linearGradient>\n",
-      "<filter id=\"field-soften\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"24\"/></filter>\n",
-      "<filter id=\"hole-glow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><feGaussianBlur stdDeviation=\"7\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<filter id=\"edge-glow\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"b\"/><feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge></filter>\n",
-      "<clipPath id=\"rose-clip\"><path d=\"",
-      renderMaskPath maskComponents,
-      "\" fill-rule=\"evenodd\"/></clipPath>\n",
-      "</defs>\n"
-    ]
-
-renderAmbientOrbits :: Double -> String
-renderAmbientOrbits activation =
-  concatMap
-    ( \orbit ->
-        let radius = 322 + fromIntegral orbit * 34
-            opacity = 0.055 + activation * 0.025
-         in "<circle cx=\"960\" cy=\"552\" r=\""
-              <> formatDouble radius
-              <> "\" fill=\"none\" stroke=\"#8ba5ff\" stroke-width=\"0.8\" stroke-dasharray=\"2 12\" opacity=\""
-              <> formatDouble opacity
-              <> "\"/>\n"
-    )
-    [0 .. 5 :: Int]
-
-renderPersistenceHalo :: Int -> Int -> [PersistenceArc] -> String
-renderPersistenceHalo finalRank selectedRank arcs =
-  "<g fill=\"none\" stroke-linecap=\"round\">\n"
-    <> concatMap renderArc (zip ([0 :: Int ..]) arcs)
-    <> "</g>\n"
- where
-  renderArc :: (Int, PersistenceArc) -> String
-  renderArc (index, arc) =
-    let active = persistenceArcBirthRank arc <= selectedRank && selectedRank < persistenceArcDeathRank arc
-        radius = 414 + fromIntegral (index `mod` 7) * 7.5
-        start = rankAngle finalRank (persistenceArcBirthRank arc)
-        finish = rankAngle finalRank (persistenceArcDeathRank arc)
-        points = arcPolyline radius start finish 42
-        hue = 188 + (index * 17 `mod` 122)
-        opacity = if active then 0.72 else 0.16
-        width = if active then 2.6 else 1.1
-     in "<polyline points=\""
-          <> renderScreenPoints points
-          <> "\" stroke=\"hsl("
-          <> show hue
-          <> " 92% 72%)\" stroke-width=\""
-          <> formatDouble width
-          <> "\" opacity=\""
-          <> formatDouble opacity
-          <> "\"/>\n"
-
-rankAngle :: Int -> Int -> Double
-rankAngle finalRank rankValue =
-  -pi / 2 + twoPi * fromIntegral rankValue / fromIntegral (max 1 finalRank)
-
-arcPolyline :: Double -> Double -> Double -> Int -> [Point]
-arcPolyline radius start finish sampleCount =
-  fmap
-    ( \sampleIndex ->
-        let fraction = fromIntegral sampleIndex / fromIntegral sampleCount
-            angle = start + fraction * (finish - start)
-         in screenPointFromPolar radius angle
-    )
-    [0 .. sampleCount]
-
-screenPointFromPolar :: Double -> Double -> Point
-screenPointFromPolar radius angle =
-  Point (960 + radius * cos angle) (552 + radius * sin angle)
-
-renderFieldSample :: FieldSample -> String
-renderFieldSample FieldSample{fieldSamplePoint, fieldSampleValue, fieldSampleNeighborCount} =
-  let Point x y = toScreen fieldSamplePoint
-      hue = round (186 + 126 * fieldSampleValue) :: Int
-      radius = 30 + min 5 (fromIntegral fieldSampleNeighborCount)
-      opacity = 0.16 + 0.16 * fieldSampleValue
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\""
-        <> formatDouble radius
-        <> "\" fill=\"hsl("
-        <> show hue
-        <> " 94% 61%)\" opacity=\""
-        <> formatDouble opacity
-        <> "\"/>\n"
-
-renderVoronoiSegment :: Segment -> String
-renderVoronoiSegment segment =
-  renderLine segment "#61efff" 1.15 0.5 ""
-
-renderRankedTriangle :: Int -> Double -> RankedTriangle -> String
-renderRankedTriangle finalRank activation RankedTriangle{rankedTrianglePoints = (firstPoint, secondPoint, thirdPoint), rankedTriangleBirthRank} =
-  let opacity = activationOpacity finalRank activation rankedTriangleBirthRank
-      hue :: Int
-      hue = 282 - round (94 * fromIntegral rankedTriangleBirthRank / fromIntegral (max 1 finalRank) :: Double)
-   in if opacity <= 0.001
-        then ""
-        else
-          "<polygon points=\""
-            <> renderWorldPoints [firstPoint, secondPoint, thirdPoint]
-            <> "\" fill=\"hsl("
-            <> show hue
-            <> " 88% 60%)\" opacity=\""
-            <> formatDouble (0.06 + 0.17 * opacity)
-            <> "\"/>\n"
-
-renderRankedSegment :: Int -> Double -> RankedSegment -> String
-renderRankedSegment finalRank activation RankedSegment{rankedSegmentGeometry, rankedSegmentBirthRank} =
-  let opacity = activationOpacity finalRank activation rankedSegmentBirthRank
-      width = 0.75 + 1.25 * opacity
-      color = if opacity > 0.72 then "#ffd56a" else "#dd78ff"
-   in renderLine rankedSegmentGeometry color width (0.06 + 0.82 * opacity) "filter=\"url(#edge-glow)\""
-
-activationOpacity :: Int -> Double -> Int -> Double
-activationOpacity finalRank activation birthRank =
-  let level = activation * fromIntegral finalRank
-      distance = level - fromIntegral birthRank
-   in smoothstep (distance + 0.6)
-
-smoothstep :: Double -> Double
-smoothstep value =
-  let bounded = clamp 0 1 value
-   in bounded * bounded * (3 - 2 * bounded)
-
-renderHoleComponent :: HoleComponent -> String
-renderHoleComponent (HoleComponent loops) =
-  concatMap
-    ( \points ->
-        "<path d=\""
-          <> renderClosedWorldPath points
-          <> "\" fill=\"none\" stroke=\"url(#hole-stroke)\" stroke-width=\"5.4\" stroke-linejoin=\"round\" opacity=\"0.98\"/>\n"
-    )
-    loops
-
-renderSite :: Double -> Point -> String
-renderSite activation point =
-  let Point x y = toScreen point
-      radius = 2.5 + 1.5 * activation
-   in "<circle cx=\""
-        <> formatDouble x
-        <> "\" cy=\""
-        <> formatDouble y
-        <> "\" r=\""
-        <> formatDouble radius
-        <> "\" fill=\"#fff8d9\" stroke=\"#ff9ad7\" stroke-width=\"1.2\" opacity=\"0.95\"/>\n"
-
-renderLine :: Segment -> String -> Double -> Double -> String -> String
-renderLine (Segment fromPoint toPoint) color width opacity extraAttributes =
-  let Point fromX fromY = toScreen fromPoint
-      Point toX toY = toScreen toPoint
-   in "<line x1=\""
-        <> formatDouble fromX
-        <> "\" y1=\""
-        <> formatDouble fromY
-        <> "\" x2=\""
-        <> formatDouble toX
-        <> "\" y2=\""
-        <> formatDouble toY
-        <> "\" stroke=\""
-        <> color
-        <> "\" stroke-width=\""
-        <> formatDouble width
-        <> "\" stroke-linecap=\"round\" opacity=\""
-        <> formatDouble opacity
-        <> "\" "
-        <> extraAttributes
-        <> "/>\n"
-
-renderTitle :: CriticalState -> String
-renderTitle state =
-  let alphaFraction = exactFractionSummary (criticalBirth state)
-      alphaDecimal = formatDouble (alphaBirthToDouble (criticalBirth state))
-      bettiMeaning = bettiOneMeaning (criticalBettiOne state)
-   in concat
-        [ "<g font-family=\"Inter, Avenir Next, Helvetica, Arial, sans-serif\" fill=\"#f7f3ff\">\n",
-          "<text x=\"72\" y=\"82\" font-size=\"24\" font-weight=\"600\" letter-spacing=\"8\" fill=\"#9ff7ff\">MOONLIGHT TRIANGULATION</text>\n",
-          "<text x=\"70\" y=\"145\" font-size=\"58\" font-weight=\"780\" letter-spacing=\"-1.5\">PERSISTENCE ROSE</text>\n",
-          "<g transform=\"translate(1500 790)\">\n",
-          "<text x=\"0\" y=\"0\" font-size=\"18\" letter-spacing=\"2.4\" fill=\"#91a8d8\">TOPOLOGICAL HOLES AT THIS SCALE</text>\n",
-          "<text x=\"0\" y=\"74\" font-size=\"78\" font-weight=\"720\" fill=\"#fff5cd\">β₁ = ",
-          show (criticalBettiOne state),
-          "</text>\n",
-          "<text x=\"2\" y=\"116\" font-size=\"17\" fill=\"#c9d3ff\">",
-          bettiMeaning,
-          "</text>\n",
-          "<text x=\"2\" y=\"144\" font-size=\"15\" fill=\"#9ba8d4\">in the exact alpha complex</text>\n",
-          "<text x=\"2\" y=\"176\" font-size=\"14\" fill=\"#7682aa\">critical rank ",
-          show (criticalRank state),
-          " · α² = ",
-          alphaDecimal,
-          "</text>\n",
-          "<text x=\"2\" y=\"200\" font-size=\"12\" fill=\"#596486\">",
-          alphaFraction,
-          "</text>\n",
-          "</g>\n",
-          "</g>\n"
-        ]
-
-bettiOneMeaning :: Int -> String
-bettiOneMeaning holeCount =
-  case holeCount of
-    0 -> "no independent one-dimensional holes"
-    1 -> "1 independent one-dimensional hole"
-    _ -> show holeCount <> " independent one-dimensional holes"
-
-exactFractionSummary :: AlphaBirth -> String
-exactFractionSummary birth =
-  let numeratorDigits = length (show (abs (alphaBirthNumerator birth)))
-      denominatorDigits = length (show (alphaBirthDenominator birth))
-   in "exact " <> show numeratorDigits <> "-digit / " <> show denominatorDigits <> "-digit rational"
-
-renderMaskPath :: [MaskComponent] -> String
-renderMaskPath =
-  intercalate " "
-    . concatMap
-      (\MaskComponent{maskOuterLoop, maskHoleLoops} -> renderClosedScreenPath maskOuterLoop : fmap renderClosedScreenPath maskHoleLoops)
-
-renderClosedWorldPath :: [Point] -> String
-renderClosedWorldPath = renderClosedPath . fmap toScreen
-
-renderClosedScreenPath :: [Point] -> String
-renderClosedScreenPath = renderClosedPath . fmap toScreen
-
-renderClosedPath :: [Point] -> String
-renderClosedPath points =
-  case points of
-    [] -> ""
-    firstPoint : remainingPoints ->
-      "M " <> renderPoint firstPoint <> concatMap (" L " <>) (fmap renderPoint remainingPoints) <> " Z"
-
-renderWorldPoints :: [Point] -> String
-renderWorldPoints = renderScreenPoints . fmap toScreen
-
-renderScreenPoints :: [Point] -> String
-renderScreenPoints = unwords . fmap renderPoint
-
-renderPoint :: Point -> String
-renderPoint (Point x y) = formatDouble x <> "," <> formatDouble y
-
-toScreen :: Point -> Point
-toScreen (Point x y) = Point (960 + x * 101) (552 + y * 101)
-
-formatDouble :: Double -> String
-formatDouble value = showFFloat (Just 3) value ""
-
-clamp :: Double -> Double -> Double -> Double
-clamp lower upper = min upper . max lower
-
-clampInt :: Int -> Int -> Int -> Int
-clampInt lower upper = min upper . max lower
-
-twoPi :: Double
-twoPi = 2 * pi
diff --git a/docs/persistence-rose/README.md b/docs/persistence-rose/README.md
--- a/docs/persistence-rose/README.md
+++ b/docs/persistence-rose/README.md
@@ -26,14 +26,14 @@
 geometry, reducing another boundary matrix, or rescanning the barcode per
 radius.
 
-The external component path uses the main facade for Delaunay construction and
-exact alpha births, then adds only the opt-in cell-complex lowering and
-Homology.
+The external component path names `build` for Delaunay construction and `dcel`
+for exact alpha births, then adds the opt-in cell-complex lowering and Homology.
 
 ```cabal
 build-depends:
   base >= 4.22 && < 5,
-  moonlight-triangulation >= 1.4 && < 1.5,
+  moonlight-planar:dcel >= 1.0 && < 1.1,
+  moonlight-planar:build >= 1.0 && < 1.1,
   moonlight-planar:cell-complex >= 1.0 && < 1.1,
   moonlight-homology >= 0.1 && < 0.2,
   vector >= 0.13 && < 0.14
@@ -47,12 +47,10 @@
   , mod2PersistentPairs
   , persistentBettiAtCriticalValues
   )
-import Moonlight.Triangulation
-  ( Point (..)
-  , alphaFiltration
-  , delaunayGeometry
-  )
-import Moonlight.Triangulation.CellComplex (filteredAlphaComplex)
+import Moonlight.Planar.Alpha (alphaFiltration)
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.CellComplex (filteredAlphaComplex)
 
 main :: IO ()
 main = do
@@ -67,7 +65,7 @@
 requireRight = either (fail . show) pure
 ```
 
-The focused `moonlight-triangulation-alpha-bench` measures exact preparation,
+The focused `moonlight-planar-alpha-bench` measures exact preparation,
 one persistence reduction plus all critical-radius queries, and repeated
 threshold homology as separate lanes over the same prebuilt Delaunay mesh.
 
@@ -77,8 +75,8 @@
 65-frame seamless SVG sweep:
 
 ```console
-cabal run moonlight-triangulation-persistence-rose -- poster persistence-rose.svg
-cabal run moonlight-triangulation-persistence-rose -- frames 65 persistence-rose-frames
+cabal run moonlight-planar-persistence-rose -- poster persistence-rose.svg
+cabal run moonlight-planar-persistence-rose -- frames 65 persistence-rose-frames
 ```
 
 ## Retained GIF reproduction
@@ -98,9 +96,9 @@
   -i persistence-rose-palette.png \
   -lavfi "[0:v]scale=960:-2:flags=lanczos[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=4:diff_mode=rectangle" \
   -loop 0 -y persistence-rose-raw.gif
-cabal run moonlight-triangulation-persistence-rose -- \
+cabal run moonlight-planar-persistence-rose -- \
   stamp-gif persistence-rose-raw.gif docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
-cabal run moonlight-triangulation-persistence-rose -- verify-artifacts .
+cabal run moonlight-planar-persistence-rose -- verify-artifacts .
 ```
 
 `verify-artifacts` compares both retained SVGs byte-for-byte with their pure
diff --git a/docs/persistence-rose/alpha-eclipse.md b/docs/persistence-rose/alpha-eclipse.md
--- a/docs/persistence-rose/alpha-eclipse.md
+++ b/docs/persistence-rose/alpha-eclipse.md
@@ -19,5 +19,5 @@
 On GHC 9.14, the package-owned generator reproduces the retained vector:
 
 ```console
-cabal run moonlight-triangulation-persistence-rose -- eclipse alpha-eclipse.svg
+cabal run moonlight-planar-persistence-rose -- eclipse alpha-eclipse.svg
 ```
diff --git a/docs/regular-site-algebra.md b/docs/regular-site-algebra.md
--- a/docs/regular-site-algebra.md
+++ b/docs/regular-site-algebra.md
@@ -1,6 +1,6 @@
 # Persistent regular-site algebra
 
-`Moonlight.Triangulation.PowerDiagram` owns the complete weighted-site value.
+`Moonlight.Planar.PowerDiagram` owns the complete weighted-site value.
 Use this surface when site identity must survive visibility changes or repeated
 weight updates.
 
diff --git a/docs/weighted-alpha.md b/docs/weighted-alpha.md
--- a/docs/weighted-alpha.md
+++ b/docs/weighted-alpha.md
@@ -1,6 +1,6 @@
 # Exact weighted alpha filtration
 
-`Moonlight.Triangulation.RegularAlpha` derives a signed exact alpha filtration
+`Moonlight.Planar.RegularAlpha` derives a signed exact alpha filtration
 from the canonical `RegularTriangulation`. It reuses resident regular faces,
 edges, weighted-dual points, rays, and lines; no second weighted geometry is
 constructed.
@@ -25,7 +25,7 @@
 edits are semantic inputs.
 
 ```console
-cabal bench moonlight-triangulation-alpha-bench
+cabal bench moonlight-planar-alpha-bench
 ```
 
 [Return to the operation index.](../README.md#operations)
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Boundary.hs b/ffi/abi/Moonlight/Planar/Foreign/Boundary.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Boundary.hs
@@ -0,0 +1,139 @@
+{-# LANGUAGE TypeFamilies #-}
+
+module Moonlight.Planar.Foreign.Boundary
+  ( GeometryMesh
+  , HandleValue
+  , checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , prepareHandleOutput
+  , produceHandle
+  , publishHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  ) where
+
+import Control.Exception (SomeException, try)
+import Foreign.C.Types (CDouble (..), CSize, CUInt)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Foreign.StablePtr
+  ( StablePtr
+  , castPtrToStablePtr
+  , castStablePtrToPtr
+  , deRefStablePtr
+  , freeStablePtr
+  , newStablePtr
+  )
+import Foreign.Storable (peekElemOff, poke)
+import qualified Data.Vector as V
+import Moonlight.Planar.Minkowski qualified as T (StructuringElement)
+import Moonlight.Planar.Region qualified as T (PlanarRegion)
+import qualified Moonlight.Planar.Types as T (DelaunayTriangulation)
+import qualified Moonlight.Planar.Point as T (Point (..))
+import Moonlight.Planar.Foreign.Contract
+  ( CMesh
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  )
+import Moonlight.Planar.Foreign.Obstruction
+  ( AbiFailure (..)
+  , bufferTooSmallFailure
+  , countOverflowFailure
+  , emptyObstruction
+  , nullPointerFailure
+  , runtimeFailure
+  , statusOk
+  )
+
+type GeometryMesh = T.DelaunayTriangulation ()
+
+type family HandleValue carrier where
+  HandleValue CMesh = GeometryMesh
+  HandleValue CRegion = T.PlanarRegion
+  HandleValue CStructuringElement = T.StructuringElement
+
+runBoundary :: Ptr CObstruction -> IO (Either AbiFailure ()) -> IO CUInt
+runBoundary obstructionPointer action = do
+  writeObstruction obstructionPointer emptyObstruction
+  outcome <- try action :: IO (Either SomeException (Either AbiFailure ()))
+  case outcome of
+    Left exception -> finishFailure obstructionPointer (runtimeFailure exception)
+    Right (Left failure) -> finishFailure obstructionPointer failure
+    Right (Right ()) -> pure statusOk
+
+finishFailure :: Ptr CObstruction -> AbiFailure -> IO CUInt
+finishFailure obstructionPointer (AbiFailure status obstruction) = do
+  writeObstruction obstructionPointer obstruction
+  pure status
+
+writeObstruction :: Ptr CObstruction -> CObstruction -> IO ()
+writeObstruction pointer obstruction
+  | pointer == nullPtr = pure ()
+  | otherwise = poke pointer obstruction
+
+requirePointer :: String -> Ptr value -> Either AbiFailure ()
+requirePointer label pointer
+  | pointer == nullPtr = Left (nullPointerFailure label)
+  | otherwise = Right ()
+
+checkedCount :: Int -> CSize -> Either AbiFailure Int
+checkedCount elementsPerItem rawCount
+  | toInteger rawCount * toInteger elementsPerItem > toInteger (maxBound :: Int) =
+      Left (countOverflowFailure (fromIntegral rawCount))
+  | otherwise = Right (fromIntegral rawCount)
+
+readPoints :: Ptr CDouble -> Int -> IO (Either AbiFailure (V.Vector T.Point))
+readPoints pointer count
+  | count == 0 = pure (Right V.empty)
+  | pointer == nullPtr = pure (Left (nullPointerFailure "coordinates"))
+  | otherwise =
+      Right
+        <$> V.generateM
+          count
+          ( \index -> do
+              CDouble x <- peekElemOff pointer (index * 2)
+              CDouble y <- peekElemOff pointer (index * 2 + 1)
+              pure (T.Point x y)
+          )
+
+prepareHandleOutput :: Ptr (Ptr carrier) -> IO (Either AbiFailure ())
+prepareHandleOutput pointer =
+  case requirePointer "result" pointer of
+    Left failure -> pure (Left failure)
+    Right () -> poke pointer nullPtr >> pure (Right ())
+
+publishHandle :: Ptr (Ptr carrier) -> HandleValue carrier -> IO ()
+publishHandle output value = do
+  stable <- newStablePtr value
+  poke output (castPtr (castStablePtrToPtr stable))
+
+produceHandle
+  :: Ptr (Ptr carrier)
+  -> IO (Either AbiFailure (HandleValue carrier))
+  -> IO (Either AbiFailure ())
+produceHandle output obtain = do
+  prepared <- prepareHandleOutput output
+  case prepared of
+    Left failure -> pure (Left failure)
+    Right () -> do
+      outcome <- obtain
+      case outcome of
+        Left failure -> pure (Left failure)
+        Right value -> publishHandle output value >> pure (Right ())
+
+dereferenceHandle :: Ptr carrier -> IO (HandleValue carrier)
+dereferenceHandle pointer = deRefStablePtr (castPtrToStablePtr (castPtr pointer))
+
+freeHandle :: Ptr carrier -> IO ()
+freeHandle pointer
+  | pointer == nullPtr = pure ()
+  | otherwise = freeStablePtr (castPtrToStablePtr (castPtr pointer) :: StablePtr ())
+
+requireOutputCapacity :: String -> Ptr value -> Int -> Int -> Either AbiFailure ()
+requireOutputCapacity label output required capacity
+  | capacity < required = Left (bufferTooSmallFailure required capacity)
+  | required > 0 = requirePointer label output
+  | otherwise = Right ()
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Contract.hs b/ffi/abi/Moonlight/Planar/Foreign/Contract.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Contract.hs
@@ -0,0 +1,652 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Moonlight.Planar.Foreign.Contract
+  ( AbiField (..)
+  , AbiFieldType (..)
+  , AbiFunction
+  , AbiFunctionFamily (..)
+  , AbiFunctionImplementation (..)
+  , AbiFunctionName (..)
+  , AbiHandle (..)
+  , AbiParameter (..)
+  , AbiParameterKind (..)
+  , AbiResult (..)
+  , AbiStatus (..)
+  , AbiStruct (..)
+  , CMesh
+  , CMinkowskiReceipt (..)
+  , CObstruction (..)
+  , CRegion
+  , CStructuringElement
+  , CoordinateErrorCode (..)
+  , MinkowskiOperationCode (..)
+  , ObstructionCode (..)
+  , RegionLocationCode (..)
+  , abiFunction
+  , abiFunctionImplementation
+  , abiFunctionParameters
+  , abiFunctionResult
+  , abiFunctionSymbol
+  , abiHandleSymbol
+  , abiStatusId
+  , abiStatusSymbol
+  , abiStructFields
+  , abiStructSymbol
+  , abiVersion
+  , allAbiFunctionNames
+  , allAbiFunctions
+  , allAbiHandles
+  , allAbiStatuses
+  , allAbiStructs
+  , allCoordinateErrorCodes
+  , allMinkowskiOperationCodes
+  , allObstructionCodes
+  , allRegionLocationCodes
+  , coordinateErrorCodeId
+  , coordinateErrorCodeSymbol
+  , minkowskiOperationCodeId
+  , minkowskiOperationCodeSymbol
+  , obstructionCodeId
+  , obstructionCodeSymbol
+  , regionLocationCodeId
+  , regionLocationCodeSymbol
+  ) where
+
+import Data.Word (Word32, Word64)
+import Foreign.C.String (peekCString, withCStringLen)
+import Foreign.C.Types (CChar)
+import Foreign.Marshal.Utils (copyBytes, fillBytes)
+import Foreign.Ptr (Ptr, castPtr, plusPtr)
+import Foreign.Storable (Storable (..))
+import GHC.Generics (Generic)
+
+abiVersion :: Word32
+abiVersion = 2
+
+data AbiStatus
+  = AbiStatusOk
+  | AbiStatusNullPointer
+  | AbiStatusCountOverflow
+  | AbiStatusBufferTooSmall
+  | AbiStatusGeometryObstruction
+  | AbiStatusRuntimeFailure
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiStatuses :: [AbiStatus]
+allAbiStatuses = [minBound .. maxBound]
+
+abiStatusId :: AbiStatus -> Word32
+abiStatusId status =
+  case status of
+    AbiStatusOk -> 0
+    AbiStatusNullPointer -> 1
+    AbiStatusCountOverflow -> 2
+    AbiStatusBufferTooSmall -> 3
+    AbiStatusGeometryObstruction -> 4
+    AbiStatusRuntimeFailure -> 5
+
+abiStatusSymbol :: AbiStatus -> String
+abiStatusSymbol status =
+  case status of
+    AbiStatusOk -> "ML_STATUS_OK"
+    AbiStatusNullPointer -> "ML_STATUS_NULL_POINTER"
+    AbiStatusCountOverflow -> "ML_STATUS_COUNT_OVERFLOW"
+    AbiStatusBufferTooSmall -> "ML_STATUS_BUFFER_TOO_SMALL"
+    AbiStatusGeometryObstruction -> "ML_STATUS_GEOMETRY_OBSTRUCTION"
+    AbiStatusRuntimeFailure -> "ML_STATUS_RUNTIME_FAILURE"
+
+data ObstructionCode
+  = ObstructionNone
+  | ObstructionInvalidCoordinate
+  | ObstructionPointLocationFailed
+  | ObstructionLocationWalkExhausted
+  | ObstructionRefinementInputTopologyInvalid
+  | ObstructionFreshInsertionMatchedExistingVertex
+  | ObstructionDegenerateLineEndpointMissingOutgoing
+  | ObstructionDegenerateLineEndpointTurnMissing
+  | ObstructionDegenerateLineConnectedVertexMissing
+  | ObstructionHullStartNotVisible
+  | ObstructionOuterRangeDidNotTerminate
+  | ObstructionOuterRangeContainsInnerEdge
+  | ObstructionConstrainedEdgeFlipRefused
+  | ObstructionRemovalVertexOutOfRange
+  | ObstructionRemovalEdgeOutOfRange
+  | ObstructionRemovalFaceOutOfRange
+  | ObstructionRemovalFaceCycleDidNotTerminate
+  | ObstructionRemovalEmptyTriangulation
+  | ObstructionRemovalTwoPointDegreeMismatch
+  | ObstructionRemovalCollinearDegreeMismatch
+  | ObstructionRemovalBorderTooShort
+  | ObstructionRemovalBorderArityMismatch
+  | ObstructionRemovalOutgoingCycleDidNotTerminate
+  | ObstructionCircleSweepHullEmpty
+  | ObstructionOuterCycleDidNotTerminate
+  | ObstructionHierarchyLevelPopulationMismatch
+  | ObstructionHierarchyInsertionHandleMismatch
+  | ObstructionPointIndexCapacityExhausted
+  | ObstructionRefinementMinimumAngleNotFinite
+  | ObstructionRefinementMinimumAngleOutOfRange
+  | ObstructionRefinementMinimumAngleDerivedRatioNotFinite
+  | ObstructionRefinementMaximumAdditionalVerticesNegative
+  | ObstructionRefinementMinimumAreaNotFinite
+  | ObstructionRefinementMinimumAreaNegative
+  | ObstructionRefinementMaximumAreaNotFinite
+  | ObstructionRefinementMaximumAreaNotPositive
+  | ObstructionRefinementMaximumRadiusEdgeRatioNotFinite
+  | ObstructionRefinementMaximumRadiusEdgeRatioNotPositive
+  | ObstructionRefinementMinimumAreaExceedsMaximum
+  | ObstructionRefinementSeedFaceNotActive
+  | ObstructionRefinementDomainInterfaceEdgeNotActive
+  | ObstructionRefinementDomainInterfaceMissing
+  | ObstructionRefinementDomainInterfaceExtraneous
+  | ObstructionRefinementDomainTopologyChanged
+  | ObstructionRefinementDomainRequiresConvexHullPreservation
+  | ObstructionRefinementDomainRequiresConstraintPreservation
+  | ObstructionRefinementDomainForbidsOuterFaceExclusion
+  | ObstructionRefinementDomainWouldCrossInterface
+  | ObstructionRefinementDomainWouldRewriteProtectedFace
+  | ObstructionRefinementDomainProtectedFaceChanged
+  | ObstructionCapacityExceeded
+  | ObstructionHalfEdgeCapacityExceeded
+  | ObstructionFaceCapacityExceeded
+  | ObstructionPayloadStorageFailure
+  | ObstructionCoordinatePayloadCountMismatch
+  | ObstructionCircleSweepRequiresDenseStorage
+  | ObstructionSeamFrontierUnavailable
+  | ObstructionRefinementDomainRequiresFiniteVertexBudget
+  | ObstructionSeamSourceEdgeRequiresFlip
+  | ObstructionSeamProtectedSourceFaceChanged
+  | ObstructionRefinementMaximumEdgeLengthNotFinite
+  | ObstructionRefinementMaximumEdgeLengthNotPositive
+  | ObstructionRefinementOversizedEdge
+  | ObstructionRefinementSeamBridgeBudgetExceeded
+  | ObstructionRefinementSeamBridgeMidpointCollapsed
+  | ObstructionBoundarySplitRequiresBoundaryEdge
+  | ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted
+  | ObstructionNullPointer
+  | ObstructionCountOverflow
+  | ObstructionBufferTooSmall
+  | ObstructionRuntimeFailure
+  | ObstructionRegionLayoutInvalid
+  | ObstructionRegionValidationFailed
+  | ObstructionOverlayFailed
+  | ObstructionRegionPublicationFailed
+  | ObstructionValuationFailed
+  | ObstructionMinkowskiFailed
+  | ObstructionRegionProjectionFailed
+  | ObstructionConvexFailed
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allObstructionCodes :: [ObstructionCode]
+allObstructionCodes = [minBound .. maxBound]
+
+obstructionCodeId :: ObstructionCode -> Word32
+obstructionCodeId code = fst (obstructionCodeMetadata code)
+
+obstructionCodeSymbol :: ObstructionCode -> String
+obstructionCodeSymbol code = snd (obstructionCodeMetadata code)
+
+obstructionCodeMetadata :: ObstructionCode -> (Word32, String)
+obstructionCodeMetadata code =
+  case code of
+    ObstructionNone -> (0, "ML_OBSTRUCTION_NONE")
+    ObstructionInvalidCoordinate -> (1, "ML_OBSTRUCTION_INVALID_COORDINATE")
+    ObstructionPointLocationFailed -> (2, "ML_OBSTRUCTION_POINT_LOCATION_FAILED")
+    ObstructionLocationWalkExhausted -> (3, "ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED")
+    ObstructionRefinementInputTopologyInvalid -> (4, "ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID")
+    ObstructionFreshInsertionMatchedExistingVertex -> (5, "ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX")
+    ObstructionDegenerateLineEndpointMissingOutgoing -> (6, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING")
+    ObstructionDegenerateLineEndpointTurnMissing -> (7, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING")
+    ObstructionDegenerateLineConnectedVertexMissing -> (8, "ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING")
+    ObstructionHullStartNotVisible -> (9, "ML_OBSTRUCTION_HULL_START_NOT_VISIBLE")
+    ObstructionOuterRangeDidNotTerminate -> (10, "ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE")
+    ObstructionOuterRangeContainsInnerEdge -> (11, "ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE")
+    ObstructionConstrainedEdgeFlipRefused -> (12, "ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED")
+    ObstructionRemovalVertexOutOfRange -> (13, "ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE")
+    ObstructionRemovalEdgeOutOfRange -> (14, "ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE")
+    ObstructionRemovalFaceOutOfRange -> (15, "ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE")
+    ObstructionRemovalFaceCycleDidNotTerminate -> (16, "ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE")
+    ObstructionRemovalEmptyTriangulation -> (17, "ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION")
+    ObstructionRemovalTwoPointDegreeMismatch -> (18, "ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH")
+    ObstructionRemovalCollinearDegreeMismatch -> (19, "ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH")
+    ObstructionRemovalBorderTooShort -> (20, "ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT")
+    ObstructionRemovalBorderArityMismatch -> (21, "ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH")
+    ObstructionRemovalOutgoingCycleDidNotTerminate -> (22, "ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE")
+    ObstructionCircleSweepHullEmpty -> (23, "ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY")
+    ObstructionOuterCycleDidNotTerminate -> (24, "ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE")
+    ObstructionHierarchyLevelPopulationMismatch -> (25, "ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH")
+    ObstructionHierarchyInsertionHandleMismatch -> (26, "ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH")
+    ObstructionPointIndexCapacityExhausted -> (27, "ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED")
+    ObstructionRefinementMinimumAngleNotFinite -> (28, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE")
+    ObstructionRefinementMinimumAngleOutOfRange -> (29, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE")
+    ObstructionRefinementMinimumAngleDerivedRatioNotFinite -> (30, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE")
+    ObstructionRefinementMaximumAdditionalVerticesNegative -> (31, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE")
+    ObstructionRefinementMinimumAreaNotFinite -> (32, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE")
+    ObstructionRefinementMinimumAreaNegative -> (33, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE")
+    ObstructionRefinementMaximumAreaNotFinite -> (34, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE")
+    ObstructionRefinementMaximumAreaNotPositive -> (35, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE")
+    ObstructionRefinementMaximumRadiusEdgeRatioNotFinite -> (36, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE")
+    ObstructionRefinementMaximumRadiusEdgeRatioNotPositive -> (37, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE")
+    ObstructionRefinementMinimumAreaExceedsMaximum -> (38, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM")
+    ObstructionRefinementSeedFaceNotActive -> (39, "ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE")
+    ObstructionRefinementDomainInterfaceEdgeNotActive -> (40, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE")
+    ObstructionRefinementDomainInterfaceMissing -> (41, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING")
+    ObstructionRefinementDomainInterfaceExtraneous -> (42, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS")
+    ObstructionRefinementDomainTopologyChanged -> (43, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED")
+    ObstructionRefinementDomainRequiresConvexHullPreservation -> (44, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION")
+    ObstructionRefinementDomainRequiresConstraintPreservation -> (45, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION")
+    ObstructionRefinementDomainForbidsOuterFaceExclusion -> (46, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION")
+    ObstructionRefinementDomainWouldCrossInterface -> (47, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE")
+    ObstructionRefinementDomainWouldRewriteProtectedFace -> (48, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE")
+    ObstructionRefinementDomainProtectedFaceChanged -> (49, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED")
+    ObstructionCapacityExceeded -> (50, "ML_OBSTRUCTION_CAPACITY_EXCEEDED")
+    ObstructionHalfEdgeCapacityExceeded -> (51, "ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED")
+    ObstructionFaceCapacityExceeded -> (52, "ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED")
+    ObstructionPayloadStorageFailure -> (53, "ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE")
+    ObstructionCoordinatePayloadCountMismatch -> (54, "ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH")
+    ObstructionCircleSweepRequiresDenseStorage -> (55, "ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE")
+    ObstructionSeamFrontierUnavailable -> (56, "ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE")
+    ObstructionRefinementDomainRequiresFiniteVertexBudget -> (57, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET")
+    ObstructionSeamSourceEdgeRequiresFlip -> (58, "ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP")
+    ObstructionSeamProtectedSourceFaceChanged -> (59, "ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED")
+    ObstructionRefinementMaximumEdgeLengthNotFinite -> (60, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE")
+    ObstructionRefinementMaximumEdgeLengthNotPositive -> (61, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE")
+    ObstructionRefinementOversizedEdge -> (62, "ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE")
+    ObstructionRefinementSeamBridgeBudgetExceeded -> (63, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED")
+    ObstructionRefinementSeamBridgeMidpointCollapsed -> (64, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED")
+    ObstructionBoundarySplitRequiresBoundaryEdge -> (65, "ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE")
+    ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted -> (66, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED")
+    ObstructionNullPointer -> (100, "ML_OBSTRUCTION_NULL_POINTER")
+    ObstructionCountOverflow -> (101, "ML_OBSTRUCTION_COUNT_OVERFLOW")
+    ObstructionBufferTooSmall -> (102, "ML_OBSTRUCTION_BUFFER_TOO_SMALL")
+    ObstructionRuntimeFailure -> (103, "ML_OBSTRUCTION_RUNTIME_FAILURE")
+    ObstructionRegionLayoutInvalid -> (200, "ML_OBSTRUCTION_REGION_LAYOUT_INVALID")
+    ObstructionRegionValidationFailed -> (201, "ML_OBSTRUCTION_REGION_VALIDATION_FAILED")
+    ObstructionOverlayFailed -> (202, "ML_OBSTRUCTION_OVERLAY_FAILED")
+    ObstructionRegionPublicationFailed -> (203, "ML_OBSTRUCTION_REGION_PUBLICATION_FAILED")
+    ObstructionValuationFailed -> (204, "ML_OBSTRUCTION_VALUATION_FAILED")
+    ObstructionMinkowskiFailed -> (205, "ML_OBSTRUCTION_MINKOWSKI_FAILED")
+    ObstructionRegionProjectionFailed -> (206, "ML_OBSTRUCTION_REGION_PROJECTION_FAILED")
+    ObstructionConvexFailed -> (207, "ML_OBSTRUCTION_CONVEX_FAILED")
+
+data CoordinateErrorCode
+  = CoordinateErrorNone
+  | CoordinateErrorNaN
+  | CoordinateErrorInfinite
+  | CoordinateErrorTooSmall
+  | CoordinateErrorTooLarge
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allCoordinateErrorCodes :: [CoordinateErrorCode]
+allCoordinateErrorCodes = [minBound .. maxBound]
+
+coordinateErrorCodeId :: CoordinateErrorCode -> Word32
+coordinateErrorCodeId code =
+  case code of
+    CoordinateErrorNone -> 0
+    CoordinateErrorNaN -> 1
+    CoordinateErrorInfinite -> 2
+    CoordinateErrorTooSmall -> 3
+    CoordinateErrorTooLarge -> 4
+
+coordinateErrorCodeSymbol :: CoordinateErrorCode -> String
+coordinateErrorCodeSymbol code =
+  case code of
+    CoordinateErrorNone -> "ML_COORDINATE_ERROR_NONE"
+    CoordinateErrorNaN -> "ML_COORDINATE_ERROR_NAN"
+    CoordinateErrorInfinite -> "ML_COORDINATE_ERROR_INFINITE"
+    CoordinateErrorTooSmall -> "ML_COORDINATE_ERROR_TOO_SMALL"
+    CoordinateErrorTooLarge -> "ML_COORDINATE_ERROR_TOO_LARGE"
+
+data RegionLocationCode
+  = RegionLocationExterior
+  | RegionLocationBoundary
+  | RegionLocationInterior
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allRegionLocationCodes :: [RegionLocationCode]
+allRegionLocationCodes = [minBound .. maxBound]
+
+regionLocationCodeId :: RegionLocationCode -> Word32
+regionLocationCodeId code =
+  case code of
+    RegionLocationExterior -> 0
+    RegionLocationBoundary -> 1
+    RegionLocationInterior -> 2
+
+regionLocationCodeSymbol :: RegionLocationCode -> String
+regionLocationCodeSymbol code =
+  case code of
+    RegionLocationExterior -> "ML_REGION_EXTERIOR"
+    RegionLocationBoundary -> "ML_REGION_BOUNDARY"
+    RegionLocationInterior -> "ML_REGION_INTERIOR"
+
+data MinkowskiOperationCode
+  = MinkowskiOperationAddition
+  | MinkowskiOperationErosion
+  | MinkowskiOperationOpening
+  | MinkowskiOperationClosing
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allMinkowskiOperationCodes :: [MinkowskiOperationCode]
+allMinkowskiOperationCodes = [minBound .. maxBound]
+
+minkowskiOperationCodeId :: MinkowskiOperationCode -> Word32
+minkowskiOperationCodeId code =
+  case code of
+    MinkowskiOperationAddition -> 0
+    MinkowskiOperationErosion -> 1
+    MinkowskiOperationOpening -> 2
+    MinkowskiOperationClosing -> 3
+
+minkowskiOperationCodeSymbol :: MinkowskiOperationCode -> String
+minkowskiOperationCodeSymbol code =
+  case code of
+    MinkowskiOperationAddition -> "ML_MINKOWSKI_ADDITION"
+    MinkowskiOperationErosion -> "ML_MINKOWSKI_EROSION"
+    MinkowskiOperationOpening -> "ML_MINKOWSKI_OPENING"
+    MinkowskiOperationClosing -> "ML_MINKOWSKI_CLOSING"
+
+data CMesh
+data CRegion
+data CStructuringElement
+
+data CObstruction = CObstruction
+  { obstructionCode :: !Word32
+  , obstructionCoordinateError :: !Word32
+  , obstructionInputIndex :: !Word64
+  , obstructionFirstIndex :: !Word64
+  , obstructionSecondIndex :: !Word64
+  , obstructionFirstValue :: !Double
+  , obstructionSecondValue :: !Double
+  , obstructionPointX :: !Double
+  , obstructionPointY :: !Double
+  , obstructionMessage :: !String
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Storable CObstruction where
+  sizeOf _ = 320
+  alignment _ = alignment (0 :: Word64)
+  peek pointer = do
+    obstructionCode <- peekByteOff pointer 0
+    obstructionCoordinateError <- peekByteOff pointer 4
+    obstructionInputIndex <- peekByteOff pointer 8
+    obstructionFirstIndex <- peekByteOff pointer 16
+    obstructionSecondIndex <- peekByteOff pointer 24
+    obstructionFirstValue <- peekByteOff pointer 32
+    obstructionSecondValue <- peekByteOff pointer 40
+    obstructionPointX <- peekByteOff pointer 48
+    obstructionPointY <- peekByteOff pointer 56
+    obstructionMessage <- peekCString (castPtr pointer `plusPtr` 64)
+    pure CObstruction {..}
+  poke pointer CObstruction {..} = do
+    pokeByteOff pointer 0 obstructionCode
+    pokeByteOff pointer 4 obstructionCoordinateError
+    pokeByteOff pointer 8 obstructionInputIndex
+    pokeByteOff pointer 16 obstructionFirstIndex
+    pokeByteOff pointer 24 obstructionSecondIndex
+    pokeByteOff pointer 32 obstructionFirstValue
+    pokeByteOff pointer 40 obstructionSecondValue
+    pokeByteOff pointer 48 obstructionPointX
+    pokeByteOff pointer 56 obstructionPointY
+    let messagePointer = castPtr pointer `plusPtr` 64 :: Ptr CChar
+    fillBytes messagePointer 0 256
+    withCStringLen obstructionMessage $ \(source, lengthInBytes) ->
+      copyBytes messagePointer source (min 255 lengthInBytes)
+
+data CMinkowskiReceipt = CMinkowskiReceipt
+  { receiptOperation :: !Word32
+  , receiptInputComponents :: !Word64
+  , receiptConvexPieces :: !Word64
+  , receiptGeneratedPieces :: !Word64
+  , receiptGeneratedConvolutionEdges :: !Word64
+  , receiptOverlayPasses :: !Word64
+  , receiptExactCrossings :: !Word64
+  , receiptOutputCells :: !Word64
+  , receiptExactCoordinateBitGrowth :: !Word64
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance Storable CMinkowskiReceipt where
+  sizeOf _ = 72
+  alignment _ = alignment (0 :: Word64)
+  peek pointer = do
+    receiptOperation <- peekByteOff pointer 0
+    receiptInputComponents <- peekByteOff pointer 8
+    receiptConvexPieces <- peekByteOff pointer 16
+    receiptGeneratedPieces <- peekByteOff pointer 24
+    receiptGeneratedConvolutionEdges <- peekByteOff pointer 32
+    receiptOverlayPasses <- peekByteOff pointer 40
+    receiptExactCrossings <- peekByteOff pointer 48
+    receiptOutputCells <- peekByteOff pointer 56
+    receiptExactCoordinateBitGrowth <- peekByteOff pointer 64
+    pure CMinkowskiReceipt {..}
+  poke pointer CMinkowskiReceipt {..} = do
+    pokeByteOff pointer 0 receiptOperation
+    pokeByteOff pointer 4 (0 :: Word32)
+    pokeByteOff pointer 8 receiptInputComponents
+    pokeByteOff pointer 16 receiptConvexPieces
+    pokeByteOff pointer 24 receiptGeneratedPieces
+    pokeByteOff pointer 32 receiptGeneratedConvolutionEdges
+    pokeByteOff pointer 40 receiptOverlayPasses
+    pokeByteOff pointer 48 receiptExactCrossings
+    pokeByteOff pointer 56 receiptOutputCells
+    pokeByteOff pointer 64 receiptExactCoordinateBitGrowth
+
+data AbiFieldType
+  = AbiFieldUInt32
+  | AbiFieldUInt64
+  | AbiFieldDouble
+  | AbiFieldCharArray !Int
+  deriving stock (Eq, Show, Generic)
+
+data AbiField = AbiField
+  { abiFieldName :: !String
+  , abiFieldType :: !AbiFieldType
+  }
+  deriving stock (Eq, Show, Generic)
+
+data AbiStruct
+  = AbiStructObstruction
+  | AbiStructMinkowskiReceipt
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiStructs :: [AbiStruct]
+allAbiStructs = [minBound .. maxBound]
+
+abiStructSymbol :: AbiStruct -> String
+abiStructSymbol structure =
+  case structure of
+    AbiStructObstruction -> "ml_obstruction"
+    AbiStructMinkowskiReceipt -> "ml_minkowski_receipt"
+
+abiStructFields :: AbiStruct -> [AbiField]
+abiStructFields structure =
+  case structure of
+    AbiStructObstruction ->
+      [ AbiField "code" AbiFieldUInt32
+      , AbiField "coordinate_error" AbiFieldUInt32
+      , AbiField "input_index" AbiFieldUInt64
+      , AbiField "first_index" AbiFieldUInt64
+      , AbiField "second_index" AbiFieldUInt64
+      , AbiField "first_value" AbiFieldDouble
+      , AbiField "second_value" AbiFieldDouble
+      , AbiField "point_x" AbiFieldDouble
+      , AbiField "point_y" AbiFieldDouble
+      , AbiField "message" (AbiFieldCharArray 256)
+      ]
+    AbiStructMinkowskiReceipt ->
+      [ AbiField "operation" AbiFieldUInt32
+      , AbiField "reserved" AbiFieldUInt32
+      , AbiField "input_components" AbiFieldUInt64
+      , AbiField "convex_pieces" AbiFieldUInt64
+      , AbiField "generated_pieces" AbiFieldUInt64
+      , AbiField "generated_convolution_edges" AbiFieldUInt64
+      , AbiField "overlay_passes" AbiFieldUInt64
+      , AbiField "exact_crossings" AbiFieldUInt64
+      , AbiField "output_cells" AbiFieldUInt64
+      , AbiField "exact_coordinate_bit_growth" AbiFieldUInt64
+      ]
+
+data AbiHandle
+  = AbiMesh
+  | AbiRegion
+  | AbiStructuringElement
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+allAbiHandles :: [AbiHandle]
+allAbiHandles = [minBound .. maxBound]
+
+abiHandleSymbol :: AbiHandle -> String
+abiHandleSymbol handle =
+  case handle of
+    AbiMesh -> "ml_mesh"
+    AbiRegion -> "ml_region"
+    AbiStructuringElement -> "ml_structuring_element"
+
+data AbiParameterKind
+  = AbiValueSize
+  | AbiValueDouble
+  | AbiHandleInput !AbiHandle
+  | AbiHandleRelease !AbiHandle
+  | AbiHandleOutput !AbiHandle
+  | AbiBufferInputDouble
+  | AbiBufferInputSize
+  | AbiBufferOutputChar
+  | AbiBufferOutputDouble
+  | AbiBufferOutputUInt32
+  | AbiBufferOutputSize
+  | AbiScalarOutputRegionLocation
+  | AbiScalarOutputInt64
+  | AbiScalarOutputSize
+  | AbiScalarOutputDouble
+  | AbiStructOutput !AbiStruct
+  deriving stock (Eq, Show, Generic)
+
+data AbiParameter = AbiParameter
+  { abiParameterName :: !String
+  , abiParameterKind :: !AbiParameterKind
+  }
+  deriving stock (Eq, Show, Generic)
+
+data AbiResult
+  = AbiResultVoid
+  | AbiResultUInt32
+  | AbiResultStatus
+  deriving stock (Eq, Show, Generic)
+
+data AbiFunctionFamily
+  = AbiFunctionMesh
+  | AbiFunctionRegion
+  | AbiFunctionMorphology
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+data AbiFunctionImplementation
+  = AbiFunctionRuntime
+  | AbiFunctionHaskell !AbiFunctionFamily !String
+  deriving stock (Eq, Show, Generic)
+
+data AbiFunctionName
+  = AbiVersion
+  | RuntimeInitialize
+  | DelaunayF64
+  | MeshInsertManyF64
+  | MeshSiteUnion
+  | MeshSiteIntersection
+  | MeshSiteDifference
+  | MeshSiteSymmetricDifference
+  | MeshVertexCount
+  | MeshTriangleCount
+  | MeshCopyVerticesF64
+  | MeshCopyTrianglesU32
+  | MeshFree
+  | RegionCreateF64
+  | RegionCounts
+  | RegionCopyF64
+  | RegionUnion
+  | RegionIntersection
+  | RegionDifference
+  | RegionSymmetricDifference
+  | RegionLocatePointF64
+  | RegionMeasure
+  | RegionFree
+  | StructuringElementCreateF64
+  | StructuringElementFree
+  | RegionMinkowskiSum
+  | RegionOffset
+  | RegionInset
+  | RegionOpen
+  | RegionClose
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+
+data AbiFunction = AbiFunction
+  { abiFunctionImplementation :: !AbiFunctionImplementation
+  , abiFunctionSymbol :: !String
+  , abiFunctionResult :: !AbiResult
+  , abiFunctionParameters :: ![AbiParameter]
+  }
+  deriving stock (Eq, Show, Generic)
+
+allAbiFunctionNames :: [AbiFunctionName]
+allAbiFunctionNames = [minBound .. maxBound]
+
+allAbiFunctions :: [AbiFunction]
+allAbiFunctions = fmap abiFunction allAbiFunctionNames
+
+abiFunction :: AbiFunctionName -> AbiFunction
+abiFunction name =
+  case name of
+    AbiVersion -> runtime "ml_abi_version" AbiResultUInt32 []
+    RuntimeInitialize -> runtime "ml_runtime_initialize" AbiResultStatus []
+    DelaunayF64 -> mesh "ml_delaunay_f64" "delaunayF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
+    MeshInsertManyF64 -> mesh "ml_mesh_insert_many_f64" "meshInsertManyF64" AbiResultStatus [inputHandle "mesh" AbiMesh, inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
+    MeshSiteUnion -> binaryMesh "ml_mesh_site_union" "meshSiteUnion"
+    MeshSiteIntersection -> binaryMesh "ml_mesh_site_intersection" "meshSiteIntersection"
+    MeshSiteDifference -> binaryMesh "ml_mesh_site_difference" "meshSiteDifference"
+    MeshSiteSymmetricDifference -> binaryMesh "ml_mesh_site_symmetric_difference" "meshSiteSymmetricDifference"
+    MeshVertexCount -> meshCount "ml_mesh_vertex_count" "meshVertexCount"
+    MeshTriangleCount -> meshCount "ml_mesh_triangle_count" "meshTriangleCount"
+    MeshCopyVerticesF64 -> mesh "ml_mesh_copy_vertices_f64" "meshCopyVerticesF64" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "points_written" AbiScalarOutputSize, obstruction]
+    MeshCopyTrianglesU32 -> mesh "ml_mesh_copy_triangles_u32" "meshCopyTrianglesU32" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "triangles" AbiBufferOutputUInt32, valueSize "triangle_capacity", AbiParameter "triangles_written" AbiScalarOutputSize, obstruction]
+    MeshFree -> mesh "ml_mesh_free" "meshFree" AbiResultVoid [releaseHandle "mesh" AbiMesh]
+    RegionCreateF64 -> region "ml_region_create_f64" "regionCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", AbiParameter "loop_point_counts" AbiBufferInputSize, valueSize "loop_count", AbiParameter "component_loop_counts" AbiBufferInputSize, valueSize "component_count", outputHandle "result" AbiRegion, obstruction]
+    RegionCounts -> region "ml_region_counts" "regionCounts" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "component_count" AbiScalarOutputSize, AbiParameter "loop_count" AbiScalarOutputSize, AbiParameter "point_count" AbiScalarOutputSize, obstruction]
+    RegionCopyF64 -> region "ml_region_copy_f64" "regionCopyF64" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "loop_point_offsets" AbiBufferOutputSize, valueSize "loop_offset_capacity", AbiParameter "component_loop_offsets" AbiBufferOutputSize, valueSize "component_offset_capacity", obstruction]
+    RegionUnion -> binaryRegion "ml_region_union" "regionUnion"
+    RegionIntersection -> binaryRegion "ml_region_intersection" "regionIntersection"
+    RegionDifference -> binaryRegion "ml_region_difference" "regionDifference"
+    RegionSymmetricDifference -> binaryRegion "ml_region_symmetric_difference" "regionSymmetricDifference"
+    RegionLocatePointF64 -> region "ml_region_locate_point_f64" "regionLocatePointF64" AbiResultStatus [inputHandle "region" AbiRegion, valueDouble "x", valueDouble "y", AbiParameter "location" AbiScalarOutputRegionLocation, obstruction]
+    RegionMeasure -> region "ml_region_measure" "regionMeasure" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "euler_characteristic" AbiScalarOutputInt64, AbiParameter "area_ratio_utf8" AbiBufferOutputChar, valueSize "area_capacity", AbiParameter "area_bytes_written" AbiScalarOutputSize, AbiParameter "perimeter_lower" AbiScalarOutputDouble, AbiParameter "perimeter_upper" AbiScalarOutputDouble, obstruction]
+    RegionFree -> region "ml_region_free" "regionFree" AbiResultVoid [releaseHandle "region" AbiRegion]
+    StructuringElementCreateF64 -> morphology "ml_structuring_element_create_f64" "structuringElementCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiStructuringElement, obstruction]
+    StructuringElementFree -> morphology "ml_structuring_element_free" "structuringElementFree" AbiResultVoid [releaseHandle "element" AbiStructuringElement]
+    RegionMinkowskiSum -> morphologyOperation "ml_region_minkowski_sum" "regionMinkowskiSum" (inputHandle "left" AbiRegion) (inputHandle "right" AbiRegion)
+    RegionOffset -> morphologyOperation "ml_region_offset" "regionOffset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionInset -> morphologyOperation "ml_region_inset" "regionInset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionOpen -> morphologyOperation "ml_region_open" "regionOpen" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+    RegionClose -> morphologyOperation "ml_region_close" "regionClose" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
+ where
+  runtime = AbiFunction AbiFunctionRuntime
+  implemented family symbol haskellName =
+    AbiFunction (AbiFunctionHaskell family haskellName) symbol
+  mesh = implemented AbiFunctionMesh
+  region = implemented AbiFunctionRegion
+  morphology = implemented AbiFunctionMorphology
+  binaryMesh symbol haskellName =
+    mesh symbol haskellName AbiResultStatus [inputHandle "left" AbiMesh, inputHandle "right" AbiMesh, outputHandle "result" AbiMesh, obstruction]
+  meshCount symbol haskellName =
+    mesh symbol haskellName AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "count" AbiScalarOutputSize, obstruction]
+  binaryRegion symbol haskellName =
+    region symbol haskellName AbiResultStatus [inputHandle "left" AbiRegion, inputHandle "right" AbiRegion, outputHandle "result" AbiRegion, obstruction]
+  morphologyOperation symbol haskellName first second =
+    morphology symbol haskellName AbiResultStatus [first, second, outputHandle "result" AbiRegion, AbiParameter "receipt" (AbiStructOutput AbiStructMinkowskiReceipt), obstruction]
+  valueSize parameterName = AbiParameter parameterName AbiValueSize
+  valueDouble parameterName = AbiParameter parameterName AbiValueDouble
+  inputHandle parameterName handle = AbiParameter parameterName (AbiHandleInput handle)
+  releaseHandle parameterName handle = AbiParameter parameterName (AbiHandleRelease handle)
+  outputHandle parameterName handle = AbiParameter parameterName (AbiHandleOutput handle)
+  inputDoubles parameterName = AbiParameter parameterName AbiBufferInputDouble
+  obstruction = AbiParameter "obstruction" (AbiStructOutput AbiStructObstruction)
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Contract/Render.hs b/ffi/abi/Moonlight/Planar/Foreign/Contract/Render.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Contract/Render.hs
@@ -0,0 +1,763 @@
+module Moonlight.Planar.Foreign.Contract.Render
+  ( GeneratedFile (..)
+  , generatedFiles
+  , renderCHeader
+  , renderDefinitionFile
+  , renderHaskellExports
+  , renderPythonRaw
+  , renderRustRaw
+  , renderTypeScriptWire
+  , renderTypeScriptRaw
+  ) where
+
+import Data.Char (toLower, toUpper)
+import Data.List (intercalate)
+import Data.Maybe (mapMaybe)
+import Data.Word (Word32)
+import Moonlight.Planar.Foreign.Contract
+
+data GeneratedFile = GeneratedFile
+  { generatedFilePath :: !FilePath
+  , generatedFileContents :: !String
+  }
+  deriving stock (Eq, Show)
+
+generatedFiles :: [GeneratedFile]
+generatedFiles =
+  [ GeneratedFile "ffi/include/moonlight_triangulation.h" renderCHeader
+  , GeneratedFile "ffi/cbits/moonlight-triangulation.def" renderDefinitionFile
+  , GeneratedFile "ffi/exports/Moonlight/Planar/Foreign/Exports.hs" renderHaskellExports
+  , GeneratedFile "ffi/bindings/typescript/src/wire.generated.ts" renderTypeScriptWire
+  , GeneratedFile "ffi/bindings/typescript/src/internal/native.generated.ts" renderTypeScriptRaw
+  , GeneratedFile "ffi/bindings/python/src/moonlight_triangulation/_native_generated.py" renderPythonRaw
+  , GeneratedFile "ffi/bindings/rust/src/raw_generated.rs" renderRustRaw
+  ]
+
+generatedNotice :: String -> String
+generatedNotice comment =
+  comment
+    <> " Generated from Moonlight.Planar.Foreign.Contract.\n"
+    <> comment
+    <> " Run moonlight-planar-ffi-contract write; do not edit this file.\n\n"
+
+renderCHeader :: String
+renderCHeader =
+  generatedNotice "//"
+    <> unlines
+      [ "#ifndef MOONLIGHT_TRIANGULATION_H"
+      , "#define MOONLIGHT_TRIANGULATION_H"
+      , ""
+      , "#include <stddef.h>"
+      , "#include <stdint.h>"
+      , ""
+      , "#if defined(_WIN32)"
+      , "#define ML_API __declspec(dllexport)"
+      , "#else"
+      , "#define ML_API __attribute__((visibility(\"default\")))"
+      , "#endif"
+      , ""
+      , "#ifdef __cplusplus"
+      , "extern \"C\" {"
+      , "#endif"
+      , ""
+      , "#define ML_ABI_VERSION " <> show abiVersion <> "u"
+      , ""
+      , concatMap renderCOpaqueHandle allAbiHandles
+      , "typedef uint32_t ml_status;"
+      , "typedef uint32_t ml_obstruction_code;"
+      , "typedef uint32_t ml_coordinate_error;"
+      , "typedef uint32_t ml_region_location;"
+      , "typedef uint32_t ml_minkowski_operation;"
+      , ""
+      ]
+    <> renderCEnum (fmap (wireEntry abiStatusSymbol abiStatusId) allAbiStatuses)
+    <> renderCEnum (fmap (wireEntry obstructionCodeSymbol obstructionCodeId) allObstructionCodes)
+    <> renderCEnum (fmap (wireEntry coordinateErrorCodeSymbol coordinateErrorCodeId) allCoordinateErrorCodes)
+    <> renderCEnum (fmap (wireEntry regionLocationCodeSymbol regionLocationCodeId) allRegionLocationCodes)
+    <> renderCEnum (fmap (wireEntry minkowskiOperationCodeSymbol minkowskiOperationCodeId) allMinkowskiOperationCodes)
+    <> concatMap renderCStruct allAbiStructs
+    <> concatMap renderCFunction allAbiFunctions
+    <> unlines
+      [ ""
+      , "#ifdef __cplusplus"
+      , "}"
+      , "#endif"
+      , ""
+      , "#endif"
+      ]
+
+wireEntry :: (value -> String) -> (value -> Word32) -> value -> (String, Word32)
+wireEntry symbol identifier value = (symbol value, identifier value)
+
+renderCEnum :: [(String, Word32)] -> String
+renderCEnum entries =
+  "enum {\n"
+    <> intercalate ",\n" (fmap (\(symbol, identifier) -> "  " <> symbol <> " = " <> show identifier) entries)
+    <> "\n};\n\n"
+
+renderCOpaqueHandle :: AbiHandle -> String
+renderCOpaqueHandle handle =
+  "typedef struct " <> abiHandleSymbol handle <> " " <> abiHandleSymbol handle <> ";\n"
+
+renderCStruct :: AbiStruct -> String
+renderCStruct structure =
+  "typedef struct "
+    <> abiStructSymbol structure
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "} "
+    <> abiStructSymbol structure
+    <> ";\n\n"
+ where
+  renderField (AbiField name fieldType) =
+    "  " <> cFieldType fieldType <> " " <> name <> cFieldSuffix fieldType <> ";\n"
+
+renderCFunction :: AbiFunction -> String
+renderCFunction function =
+  "ML_API "
+    <> cResultType (abiFunctionResult function)
+    <> " "
+    <> abiFunctionSymbol function
+    <> "("
+    <> renderParameters
+    <> ");\n"
+ where
+  parameters = abiFunctionParameters function
+  renderParameters =
+    if null parameters
+      then "void"
+      else intercalate ", " (fmap renderParameter parameters)
+  renderParameter (AbiParameter name kind) = cParameterType kind <> " " <> name
+
+cFieldType :: AbiFieldType -> String
+cFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "uint32_t"
+    AbiFieldUInt64 -> "uint64_t"
+    AbiFieldDouble -> "double"
+    AbiFieldCharArray _ -> "char"
+
+cFieldSuffix :: AbiFieldType -> String
+cFieldSuffix fieldType =
+  case fieldType of
+    AbiFieldCharArray lengthInBytes -> "[" <> show lengthInBytes <> "]"
+    _ -> ""
+
+cParameterType :: AbiParameterKind -> String
+cParameterType kind =
+  case kind of
+    AbiValueSize -> "size_t"
+    AbiValueDouble -> "double"
+    AbiHandleInput handle -> "const " <> abiHandleSymbol handle <> " *"
+    AbiHandleRelease handle -> abiHandleSymbol handle <> " *"
+    AbiHandleOutput handle -> abiHandleSymbol handle <> " **"
+    AbiBufferInputDouble -> "const double *"
+    AbiBufferInputSize -> "const size_t *"
+    AbiBufferOutputChar -> "char *"
+    AbiBufferOutputDouble -> "double *"
+    AbiBufferOutputUInt32 -> "uint32_t *"
+    AbiBufferOutputSize -> "size_t *"
+    AbiScalarOutputRegionLocation -> "ml_region_location *"
+    AbiScalarOutputInt64 -> "int64_t *"
+    AbiScalarOutputSize -> "size_t *"
+    AbiScalarOutputDouble -> "double *"
+    AbiStructOutput structure -> abiStructSymbol structure <> " *"
+
+cResultType :: AbiResult -> String
+cResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "uint32_t"
+    AbiResultStatus -> "ml_status"
+
+renderDefinitionFile :: String
+renderDefinitionFile =
+  generatedNotice ";"
+    <> "LIBRARY moonlight-triangulation-c\nEXPORTS\n"
+    <> concatMap (\function -> "  " <> abiFunctionSymbol function <> "\n") allAbiFunctions
+
+renderHaskellExports :: String
+renderHaskellExports =
+  generatedNotice "--"
+    <> unlines
+      [ "{-# LANGUAGE ForeignFunctionInterface #-}"
+      , "{-# OPTIONS_GHC -Wno-missing-signatures #-}"
+      , ""
+      , "module Moonlight.Planar.Foreign.Exports where"
+      , ""
+      , "import Data.Int (Int64)"
+      , "import Data.Word (Word32)"
+      , "import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))"
+      , "import Foreign.Ptr (Ptr)"
+      , "import Moonlight.Planar.Foreign.Contract"
+      , "  ( CMesh"
+      , "  , CMinkowskiReceipt"
+      , "  , CObstruction"
+      , "  , CRegion"
+      , "  , CStructuringElement"
+      , "  )"
+      , "import qualified Moonlight.Planar.Foreign.Mesh as Mesh"
+      , "import qualified Moonlight.Planar.Foreign.Morphology as Morphology"
+      , "import qualified Moonlight.Planar.Foreign.Region as Region"
+      , ""
+      ]
+    <> concatMap renderHaskellAlias implementedFunctions
+    <> "\n"
+    <> concatMap renderHaskellExport implementedFunctions
+ where
+  implementedFunctions :: [(AbiFunction, String, AbiFunctionFamily)]
+  implementedFunctions = mapMaybe implemented allAbiFunctions
+
+  implemented :: AbiFunction -> Maybe (AbiFunction, String, AbiFunctionFamily)
+  implemented function =
+    case abiFunctionImplementation function of
+      AbiFunctionRuntime -> Nothing
+      AbiFunctionHaskell family haskellName -> Just (function, haskellName, family)
+
+  renderHaskellAlias :: (AbiFunction, String, AbiFunctionFamily) -> String
+  renderHaskellAlias (_, haskellName, family) =
+    haskellName <> " = " <> familyQualifier family <> "." <> haskellName <> "\n"
+
+  renderHaskellExport :: (AbiFunction, String, AbiFunctionFamily) -> String
+  renderHaskellExport (function, haskellName, _) =
+    "foreign export ccall \""
+      <> abiFunctionSymbol function
+      <> "\" "
+      <> haskellName
+      <> " :: "
+      <> haskellFunctionType function
+      <> "\n"
+
+familyQualifier :: AbiFunctionFamily -> String
+familyQualifier family =
+  case family of
+    AbiFunctionMesh -> "Mesh"
+    AbiFunctionRegion -> "Region"
+    AbiFunctionMorphology -> "Morphology"
+
+renderTypeScriptWire :: String
+renderTypeScriptWire =
+  generatedNotice "//"
+    <> intercalate
+      "\n"
+      [ renderTypeScriptVocabulary "AbiStatus" "ML_STATUS_" abiStatusSymbol abiStatusId allAbiStatuses
+      , renderTypeScriptVocabulary "ObstructionCode" "ML_OBSTRUCTION_" obstructionCodeSymbol obstructionCodeId allObstructionCodes
+      , renderTypeScriptVocabulary "CoordinateError" "ML_COORDINATE_ERROR_" coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+      , renderTypeScriptVocabulary "RegionLocation" "ML_REGION_" regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+      , renderTypeScriptVocabulary "MinkowskiOperation" "ML_MINKOWSKI_" minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+      ]
+
+renderTypeScriptVocabulary
+  :: String
+  -> String
+  -> (value -> String)
+  -> (value -> Word32)
+  -> [value]
+  -> String
+renderTypeScriptVocabulary typeName prefix symbol identifier values =
+  "export const "
+    <> typeName
+    <> " = {\n"
+    <> concatMap renderValue values
+    <> "} as const;\n"
+    <> "export type "
+    <> typeName
+    <> " = (typeof "
+    <> typeName
+    <> ")[keyof typeof "
+    <> typeName
+    <> "];\n"
+    <> "const "
+    <> lowerInitial typeName
+    <> "ByWire: Readonly<Record<number, "
+    <> typeName
+    <> ">> = {\n"
+    <> concatMap renderWireEntry values
+    <> "};\n"
+    <> "export function decode"
+    <> typeName
+    <> "(value: number): "
+    <> typeName
+    <> " | undefined {\n"
+    <> "  return "
+    <> lowerInitial typeName
+    <> "ByWire[value];\n"
+    <> "}\n"
+ where
+  renderValue value =
+    "  " <> wireKey prefix (symbol value) <> ": \"" <> wireValue prefix (symbol value) <> "\",\n"
+  renderWireEntry value =
+    "  " <> show (identifier value) <> ": " <> typeName <> "." <> wireKey prefix (symbol value) <> ",\n"
+
+wireKey :: String -> String -> String
+wireKey prefix = concatMap capitalize . underscoreWords . drop (length prefix)
+
+wireValue :: String -> String -> String
+wireValue prefix = intercalate "-" . fmap (fmap toLower) . underscoreWords . drop (length prefix)
+
+underscoreWords :: String -> [String]
+underscoreWords value =
+  case break (== '_') value of
+    (word, []) -> [word]
+    (word, _ : rest) -> word : underscoreWords rest
+
+capitalize :: String -> String
+capitalize value =
+  case value of
+    [] -> []
+    firstCharacter : rest -> toUpper firstCharacter : fmap toLower rest
+
+lowerInitial :: String -> String
+lowerInitial value =
+  case value of
+    [] -> []
+    firstCharacter : rest -> toLower firstCharacter : rest
+
+haskellFunctionType :: AbiFunction -> String
+haskellFunctionType function =
+  intercalate " -> " (fmap (haskellParameterType . abiParameterKind) (abiFunctionParameters function) <> [haskellResultType (abiFunctionResult function)])
+
+haskellParameterType :: AbiParameterKind -> String
+haskellParameterType kind =
+  case kind of
+    AbiValueSize -> "CSize"
+    AbiValueDouble -> "CDouble"
+    AbiHandleInput handle -> "Ptr " <> haskellHandleName handle
+    AbiHandleRelease handle -> "Ptr " <> haskellHandleName handle
+    AbiHandleOutput handle -> "Ptr (Ptr " <> haskellHandleName handle <> ")"
+    AbiBufferInputDouble -> "Ptr CDouble"
+    AbiBufferInputSize -> "Ptr CSize"
+    AbiBufferOutputChar -> "Ptr CChar"
+    AbiBufferOutputDouble -> "Ptr CDouble"
+    AbiBufferOutputUInt32 -> "Ptr Word32"
+    AbiBufferOutputSize -> "Ptr CSize"
+    AbiScalarOutputRegionLocation -> "Ptr CUInt"
+    AbiScalarOutputInt64 -> "Ptr Int64"
+    AbiScalarOutputSize -> "Ptr CSize"
+    AbiScalarOutputDouble -> "Ptr CDouble"
+    AbiStructOutput structure -> "Ptr " <> haskellStructName structure
+
+haskellResultType :: AbiResult -> String
+haskellResultType result =
+  case result of
+    AbiResultVoid -> "IO ()"
+    AbiResultUInt32 -> "IO CUInt"
+    AbiResultStatus -> "IO CUInt"
+
+haskellHandleName :: AbiHandle -> String
+haskellHandleName handle =
+  case handle of
+    AbiMesh -> "CMesh"
+    AbiRegion -> "CRegion"
+    AbiStructuringElement -> "CStructuringElement"
+
+haskellStructName :: AbiStruct -> String
+haskellStructName structure =
+  case structure of
+    AbiStructObstruction -> "CObstruction"
+    AbiStructMinkowskiReceipt -> "CMinkowskiReceipt"
+
+renderTypeScriptRaw :: String
+renderTypeScriptRaw =
+  generatedNotice "//"
+    <> "import koffi from \"koffi\";\n\n"
+    <> "export const ABI_VERSION = " <> show abiVersion <> ";\n"
+    <> renderTypeScriptConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderTypeScriptConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderTypeScriptConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderTypeScriptConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderTypeScriptConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\nexport type NativeHandle = object;\n"
+    <> "export type NativeInteger = number | bigint;\n"
+    <> "export type HandleOutput = Array<NativeHandle | null>;\n\n"
+    <> renderTypeScriptStruct "NativeObstruction" AbiStructObstruction
+    <> renderTypeScriptStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> "export interface NativeApi {\n"
+    <> concatMap renderTypeScriptFunctionType allAbiFunctions
+    <> "}\n\n"
+    <> unlines
+      [ "export function loadNativeApi(libraryPath: string): NativeApi {"
+      , "  const library = koffi.load(libraryPath);"
+      , concatMap renderTypeScriptKoffiHandleDeclarations allAbiHandles
+      , renderTypeScriptKoffiStruct "obstruction" AbiStructObstruction
+      , renderTypeScriptKoffiStruct "receipt" AbiStructMinkowskiReceipt
+      , "  const obstructionOutput = koffi.out(koffi.pointer(obstruction));"
+      , "  const receiptOutput = koffi.out(koffi.pointer(receipt));"
+      , "  const sizeOutput = koffi.out(koffi.pointer(\"size_t\"));"
+      , "  const uint32Output = koffi.out(koffi.pointer(\"uint32_t\"));"
+      , "  const int64Output = koffi.out(koffi.pointer(\"int64_t\"));"
+      , "  const doubleOutput = koffi.out(koffi.pointer(\"double\"));"
+      , "  const sizeArray = koffi.pointer(\"size_t\");"
+      , "  return {"
+      ]
+    <> concatMap renderTypeScriptFunctionBinding allAbiFunctions
+    <> "  };\n}\n"
+
+renderTypeScriptConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderTypeScriptConstants symbol identifier =
+  concatMap (\value -> "export const " <> symbol value <> " = " <> show (identifier value) <> ";\n")
+
+renderTypeScriptStruct :: String -> AbiStruct -> String
+renderTypeScriptStruct name structure =
+  "export interface "
+    <> name
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "}\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "  " <> fieldName <> "?: " <> typeScriptFieldType fieldType <> ";\n"
+
+typeScriptFieldType :: AbiFieldType -> String
+typeScriptFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "number"
+    AbiFieldUInt64 -> "NativeInteger"
+    AbiFieldDouble -> "number"
+    AbiFieldCharArray _ -> "string | readonly number[]"
+
+renderTypeScriptFunctionType :: AbiFunction -> String
+renderTypeScriptFunctionType function =
+  "  readonly "
+    <> abiFunctionSymbol function
+    <> ": ("
+    <> intercalate ", " (fmap renderParameter (abiFunctionParameters function))
+    <> ") => "
+    <> typeScriptResultType (abiFunctionResult function)
+    <> ";\n"
+ where
+  renderParameter (AbiParameter name kind) = name <> ": " <> typeScriptParameterType kind
+
+typeScriptParameterType :: AbiParameterKind -> String
+typeScriptParameterType kind =
+  case kind of
+    AbiValueSize -> "number"
+    AbiValueDouble -> "number"
+    AbiHandleInput _ -> "NativeHandle"
+    AbiHandleRelease _ -> "NativeHandle"
+    AbiHandleOutput _ -> "HandleOutput"
+    AbiBufferInputDouble -> "Float64Array"
+    AbiBufferInputSize -> "BigUint64Array"
+    AbiBufferOutputChar -> "Buffer"
+    AbiBufferOutputDouble -> "Float64Array"
+    AbiBufferOutputUInt32 -> "Uint32Array"
+    AbiBufferOutputSize -> "BigUint64Array"
+    AbiScalarOutputRegionLocation -> "number[]"
+    AbiScalarOutputInt64 -> "NativeInteger[]"
+    AbiScalarOutputSize -> "NativeInteger[]"
+    AbiScalarOutputDouble -> "number[]"
+    AbiStructOutput AbiStructObstruction -> "NativeObstruction"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "NativeMinkowskiReceipt"
+
+typeScriptResultType :: AbiResult -> String
+typeScriptResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "number"
+    AbiResultStatus -> "number"
+
+renderTypeScriptKoffiStruct :: String -> AbiStruct -> String
+renderTypeScriptKoffiStruct variableName structure =
+  "  const "
+    <> variableName
+    <> " = koffi.struct({\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "  });"
+ where
+  renderField (AbiField name fieldType) =
+    "    " <> name <> ": " <> typeScriptKoffiFieldType fieldType <> ",\n"
+
+typeScriptKoffiFieldType :: AbiFieldType -> String
+typeScriptKoffiFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "\"uint32_t\""
+    AbiFieldUInt64 -> "\"uint64_t\""
+    AbiFieldDouble -> "\"double\""
+    AbiFieldCharArray lengthInBytes -> "koffi.array(\"char\", " <> show lengthInBytes <> ")"
+
+renderTypeScriptFunctionBinding :: AbiFunction -> String
+renderTypeScriptFunctionBinding function =
+  "    "
+    <> abiFunctionSymbol function
+    <> ": library.func(\""
+    <> abiFunctionSymbol function
+    <> "\", \""
+    <> typeScriptKoffiResultType (abiFunctionResult function)
+    <> "\", ["
+    <> intercalate ", " (fmap (typeScriptKoffiParameterType . abiParameterKind) (abiFunctionParameters function))
+    <> "]),\n"
+
+typeScriptKoffiResultType :: AbiResult -> String
+typeScriptKoffiResultType result =
+  case result of
+    AbiResultVoid -> "void"
+    AbiResultUInt32 -> "uint32_t"
+    AbiResultStatus -> "uint32_t"
+
+typeScriptKoffiParameterType :: AbiParameterKind -> String
+typeScriptKoffiParameterType kind =
+  case kind of
+    AbiValueSize -> "\"size_t\""
+    AbiValueDouble -> "\"double\""
+    AbiHandleInput handle -> typeScriptHandleVariable handle <> "Pointer"
+    AbiHandleRelease handle -> typeScriptHandleVariable handle <> "Pointer"
+    AbiHandleOutput handle -> typeScriptHandleVariable handle <> "Output"
+    AbiBufferInputDouble -> "koffi.pointer(\"double\")"
+    AbiBufferInputSize -> "sizeArray"
+    AbiBufferOutputChar -> "koffi.out(koffi.pointer(\"char\"))"
+    AbiBufferOutputDouble -> "koffi.out(koffi.pointer(\"double\"))"
+    AbiBufferOutputUInt32 -> "koffi.out(koffi.pointer(\"uint32_t\"))"
+    AbiBufferOutputSize -> "koffi.out(sizeArray)"
+    AbiScalarOutputRegionLocation -> "uint32Output"
+    AbiScalarOutputInt64 -> "int64Output"
+    AbiScalarOutputSize -> "sizeOutput"
+    AbiScalarOutputDouble -> "doubleOutput"
+    AbiStructOutput AbiStructObstruction -> "obstructionOutput"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "receiptOutput"
+
+typeScriptHandleVariable :: AbiHandle -> String
+typeScriptHandleVariable handle =
+  case handle of
+    AbiMesh -> "mesh"
+    AbiRegion -> "region"
+    AbiStructuringElement -> "structuringElement"
+
+renderTypeScriptKoffiHandleDeclarations :: AbiHandle -> String
+renderTypeScriptKoffiHandleDeclarations handle =
+  let variableName = typeScriptHandleVariable handle
+   in unlines
+        [ "  const " <> variableName <> " = koffi.opaque();"
+        , "  const " <> variableName <> "Pointer = koffi.pointer(" <> variableName <> ");"
+        , "  const " <> variableName <> "Output = koffi.out(koffi.pointer(" <> variableName <> ", 2));"
+        ]
+
+renderPythonRaw :: String
+renderPythonRaw =
+  generatedNotice "#"
+    <> unlines
+      [ "from __future__ import annotations"
+      , ""
+      , "import ctypes"
+      , "from collections.abc import Sequence"
+      , "from pathlib import Path"
+      , "from typing import Final"
+      , ""
+      , "ABI_VERSION: Final = " <> show abiVersion
+      ]
+    <> renderPythonConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderPythonConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderPythonConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderPythonConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderPythonConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\n"
+    <> renderPythonStruct "_Obstruction" AbiStructObstruction
+    <> renderPythonStruct "_NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> unlines
+      [ "class _NativeApi:"
+      , "    def __init__(self, library_path: Path) -> None:"
+      , "        library = ctypes.CDLL(str(library_path))"
+      ]
+    <> concatMap renderPythonConfiguration allAbiFunctions
+    <> unlines
+      [ "        self.library = library"
+      , ""
+      , ""
+      , "def _configure("
+      , "    library: ctypes.CDLL,"
+      , "    name: str,"
+      , "    parameters: Sequence[object],"
+      , "    result: object = ctypes.c_uint32,"
+      , ") -> None:"
+      , "    function = getattr(library, name)"
+      , "    setattr(function, \"argtypes\", list(parameters))"
+      , "    setattr(function, \"restype\", result)"
+      ]
+
+renderPythonConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderPythonConstants symbol identifier =
+  concatMap (\value -> symbol value <> ": Final = " <> show (identifier value) <> "\n")
+
+renderPythonStruct :: String -> AbiStruct -> String
+renderPythonStruct name structure =
+  "class "
+    <> name
+    <> "(ctypes.Structure):\n"
+    <> "    _fields_ = [\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "    ]\n\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "        (\"" <> fieldName <> "\", " <> pythonFieldType fieldType <> "),\n"
+
+pythonFieldType :: AbiFieldType -> String
+pythonFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "ctypes.c_uint32"
+    AbiFieldUInt64 -> "ctypes.c_uint64"
+    AbiFieldDouble -> "ctypes.c_double"
+    AbiFieldCharArray lengthInBytes -> "ctypes.c_char * " <> show lengthInBytes
+
+renderPythonConfiguration :: AbiFunction -> String
+renderPythonConfiguration function =
+  "        _configure(library, \""
+    <> abiFunctionSymbol function
+    <> "\", "
+    <> pythonTuple (fmap (pythonParameterType . abiParameterKind) (abiFunctionParameters function))
+    <> ", "
+    <> pythonResultType (abiFunctionResult function)
+    <> ")\n"
+
+pythonTuple :: [String] -> String
+pythonTuple values =
+  case values of
+    [] -> "()"
+    [value] -> "(" <> value <> ",)"
+    _ -> "(" <> intercalate ", " values <> ")"
+
+pythonParameterType :: AbiParameterKind -> String
+pythonParameterType kind =
+  case kind of
+    AbiValueSize -> "ctypes.c_size_t"
+    AbiValueDouble -> "ctypes.c_double"
+    AbiHandleInput _ -> "ctypes.c_void_p"
+    AbiHandleRelease _ -> "ctypes.c_void_p"
+    AbiHandleOutput _ -> "ctypes.POINTER(ctypes.c_void_p)"
+    AbiBufferInputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiBufferInputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiBufferOutputChar -> "ctypes.POINTER(ctypes.c_char)"
+    AbiBufferOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiBufferOutputUInt32 -> "ctypes.POINTER(ctypes.c_uint32)"
+    AbiBufferOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiScalarOutputRegionLocation -> "ctypes.POINTER(ctypes.c_uint32)"
+    AbiScalarOutputInt64 -> "ctypes.POINTER(ctypes.c_int64)"
+    AbiScalarOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
+    AbiScalarOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
+    AbiStructOutput AbiStructObstruction -> "ctypes.POINTER(_Obstruction)"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "ctypes.POINTER(_NativeMinkowskiReceipt)"
+
+pythonResultType :: AbiResult -> String
+pythonResultType result =
+  case result of
+    AbiResultVoid -> "None"
+    AbiResultUInt32 -> "ctypes.c_uint32"
+    AbiResultStatus -> "ctypes.c_uint32"
+
+renderRustRaw :: String
+renderRustRaw =
+  generatedNotice "//"
+    <> "// This private module is the complete wire projection; wrappers consume a subset.\n"
+    <> "#![allow(dead_code)]\n\n"
+    <> "use std::ffi::{c_char, c_double, c_uint};\n\n"
+    <> "pub(crate) const ABI_VERSION: u32 = " <> show abiVersion <> ";\n"
+    <> renderRustConstants abiStatusSymbol abiStatusId allAbiStatuses
+    <> renderRustConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
+    <> renderRustConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
+    <> renderRustConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
+    <> renderRustConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
+    <> "\n"
+    <> concatMap renderRustHandle allAbiHandles
+    <> renderRustStruct "NativeObstruction" AbiStructObstruction
+    <> renderRustStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
+    <> "#[link(name = \"moonlight-triangulation-c\")]\nunsafe extern \"C\" {\n"
+    <> concatMap renderRustFunction allAbiFunctions
+    <> "}\n"
+
+renderRustConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
+renderRustConstants symbol identifier =
+  concatMap (\value -> "pub(crate) const " <> symbol value <> ": u32 = " <> show (identifier value) <> ";\n")
+
+renderRustHandle :: AbiHandle -> String
+renderRustHandle handle =
+  "#[repr(C)]\npub(crate) struct "
+    <> rustHandleName handle
+    <> " {\n    _private: [u8; 0],\n}\n\n"
+
+renderRustStruct :: String -> AbiStruct -> String
+renderRustStruct name structure =
+  "#[repr(C)]\npub(crate) struct "
+    <> name
+    <> " {\n"
+    <> concatMap renderField (abiStructFields structure)
+    <> "}\n\n"
+    <> "impl Default for "
+    <> name
+    <> " {\n"
+    <> "    fn default() -> Self {\n"
+    <> "        Self {\n"
+    <> concatMap renderDefaultField (abiStructFields structure)
+    <> "        }\n"
+    <> "    }\n"
+    <> "}\n\n"
+ where
+  renderField (AbiField fieldName fieldType) =
+    "    pub(crate) " <> fieldName <> ": " <> rustFieldType fieldType <> ",\n"
+  renderDefaultField (AbiField fieldName fieldType) =
+    "            " <> fieldName <> ": " <> rustFieldDefault fieldType <> ",\n"
+
+rustFieldType :: AbiFieldType -> String
+rustFieldType fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "u32"
+    AbiFieldUInt64 -> "u64"
+    AbiFieldDouble -> "f64"
+    AbiFieldCharArray lengthInBytes -> "[c_char; " <> show lengthInBytes <> "]"
+
+rustFieldDefault :: AbiFieldType -> String
+rustFieldDefault fieldType =
+  case fieldType of
+    AbiFieldUInt32 -> "0"
+    AbiFieldUInt64 -> "0"
+    AbiFieldDouble -> "0.0"
+    AbiFieldCharArray lengthInBytes -> "[0; " <> show lengthInBytes <> "]"
+
+renderRustFunction :: AbiFunction -> String
+renderRustFunction function =
+  case abiFunctionParameters function of
+    [] -> declarationPrefix <> "()" <> resultSuffix <> ";\n"
+    [parameter] -> declarationPrefix <> "(" <> renderParameter parameter <> ")" <> resultSuffix <> ";\n"
+    parameters ->
+      declarationPrefix
+        <> "(\n"
+        <> concatMap (\parameter -> "        " <> renderParameter parameter <> ",\n") parameters
+        <> "    )"
+        <> resultSuffix
+        <> ";\n"
+ where
+  declarationPrefix = "    pub(crate) fn " <> abiFunctionSymbol function
+  resultSuffix = rustResultSuffix (abiFunctionResult function)
+  renderParameter (AbiParameter name kind) = name <> ": " <> rustParameterType kind
+
+rustParameterType :: AbiParameterKind -> String
+rustParameterType kind =
+  case kind of
+    AbiValueSize -> "usize"
+    AbiValueDouble -> "c_double"
+    AbiHandleInput handle -> "*const " <> rustHandleName handle
+    AbiHandleRelease handle -> "*mut " <> rustHandleName handle
+    AbiHandleOutput handle -> "*mut *mut " <> rustHandleName handle
+    AbiBufferInputDouble -> "*const c_double"
+    AbiBufferInputSize -> "*const usize"
+    AbiBufferOutputChar -> "*mut c_char"
+    AbiBufferOutputDouble -> "*mut c_double"
+    AbiBufferOutputUInt32 -> "*mut u32"
+    AbiBufferOutputSize -> "*mut usize"
+    AbiScalarOutputRegionLocation -> "*mut u32"
+    AbiScalarOutputInt64 -> "*mut i64"
+    AbiScalarOutputSize -> "*mut usize"
+    AbiScalarOutputDouble -> "*mut c_double"
+    AbiStructOutput AbiStructObstruction -> "*mut NativeObstruction"
+    AbiStructOutput AbiStructMinkowskiReceipt -> "*mut NativeMinkowskiReceipt"
+
+rustResultSuffix :: AbiResult -> String
+rustResultSuffix result =
+  case result of
+    AbiResultVoid -> ""
+    AbiResultUInt32 -> " -> c_uint"
+    AbiResultStatus -> " -> c_uint"
+
+rustHandleName :: AbiHandle -> String
+rustHandleName handle =
+  case handle of
+    AbiMesh -> "NativeMesh"
+    AbiRegion -> "NativeRegion"
+    AbiStructuringElement -> "NativeStructuringElement"
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Mesh.hs b/ffi/abi/Moonlight/Planar/Foreign/Mesh.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Mesh.hs
@@ -0,0 +1,186 @@
+module Moonlight.Planar.Foreign.Mesh
+  ( delaunayF64
+  , insertGeometryBatch
+  , meshCopyTrianglesU32
+  , meshCopyVerticesF64
+  , meshFree
+  , meshInsertManyF64
+  , meshSiteDifference
+  , meshSiteIntersection
+  , meshSiteSymmetricDifference
+  , meshSiteUnion
+  , meshTriangleCount
+  , meshVertexCount
+  ) where
+
+import Control.Monad (void)
+import Data.Bifunctor (first)
+import Data.Word (Word32)
+import Foreign.C.Types (CDouble (..), CSize, CUInt)
+import Foreign.Ptr (Ptr)
+import Foreign.Storable (poke, pokeElemOff)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad qualified as T (delaunayGeometry)
+import Moonlight.Planar.Dcel qualified as T (innerFaceVertexTriples, numVertices, vertexPoints)
+import Moonlight.Planar.Handles.HandleDefs qualified as T (unVertexId)
+import Moonlight.Planar.SetAlgebra qualified as T (difference, intersection, symmetricDifference, union)
+import qualified Moonlight.Planar.Types as T (BuildError (..))
+import qualified Moonlight.Planar.Point as T (Point (..))
+import Moonlight.Planar.Foreign.Boundary
+  ( GeometryMesh
+  , HandleValue
+  , checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , produceHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Planar.Foreign.Contract (CMesh, CObstruction)
+import Moonlight.Planar.Foreign.Obstruction (buildFailure)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import qualified Moonlight.Planar.Internal.Session as Session
+
+delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+delaunayF64 coordinates rawCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case checkedCount 2 rawCount of
+      Left failure -> pure (Left failure)
+      Right count -> do
+        points <- readPoints coordinates count
+        pure (points >>= first buildFailure . T.delaunayGeometry)
+
+meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+meshInsertManyF64 meshPointer coordinates rawCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "mesh" meshPointer >> checkedCount 2 rawCount of
+      Left failure -> pure (Left failure)
+      Right count -> do
+        points <- readPoints coordinates count
+        case points of
+          Left failure -> pure (Left failure)
+          Right admitted -> do
+            mesh <- dereferenceHandle meshPointer
+            pure (first buildFailure (insertGeometryBatch mesh admitted))
+
+-- | Admit the complete batch before opening the immutable insertion session.
+-- The empty batch is the identity and needs no transaction.
+insertGeometryBatch :: GeometryMesh -> V.Vector T.Point -> Either T.BuildError GeometryMesh
+insertGeometryBatch mesh points
+  | V.null points = Right mesh
+  | otherwise = do
+      admittedPoints <- V.imapM (\index point -> validatePoint (Just index) point) points
+      (_, revised, _) <-
+        Session.withSession
+          mesh
+          (V.length points)
+          ( V.mapM_
+              ( \point ->
+                  void
+                    ( Session.insertAdmittedVertexAtCombining
+                        (\_ replacement -> replacement)
+                        point
+                        ()
+                    )
+              )
+              admittedPoints
+          )
+      pure revised
+
+meshSiteUnion, meshSiteIntersection, meshSiteDifference, meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+meshSiteUnion = binaryMeshOperation T.union
+meshSiteIntersection = binaryMeshOperation T.intersection
+meshSiteDifference = binaryMeshOperation T.difference
+meshSiteSymmetricDifference = binaryMeshOperation T.symmetricDifference
+
+binaryMeshOperation
+  :: (GeometryMesh -> GeometryMesh -> Either T.BuildError GeometryMesh)
+  -> Ptr CMesh
+  -> Ptr CMesh
+  -> Ptr (Ptr CMesh)
+  -> Ptr CObstruction
+  -> IO CUInt
+binaryMeshOperation operation leftPointer rightPointer output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "left mesh" leftPointer >> requirePointer "right mesh" rightPointer of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        left <- dereferenceHandle leftPointer
+        right <- dereferenceHandle rightPointer
+        pure (first buildFailure (operation left right))
+
+meshVertexCount, meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshVertexCount = handleCount "mesh" T.numVertices
+meshTriangleCount = handleCount "mesh" (V.length . T.innerFaceVertexTriples)
+
+handleCount
+  :: String
+  -> (HandleValue carrier -> Int)
+  -> Ptr carrier
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+handleCount handleLabel observe handlePointer output obstructionPointer =
+  runBoundary obstructionPointer $
+    case requirePointer handleLabel handlePointer >> requirePointer "count" output of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        value <- dereferenceHandle handlePointer
+        poke output (fromIntegral (observe value))
+        pure (Right ())
+
+meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshCopyVerticesF64 =
+  copyHandleProjection
+    "mesh"
+    "points_written"
+    "coordinates"
+    T.vertexPoints
+    ( \output index (T.Point x y) -> do
+        pokeElemOff output (index * 2) (CDouble x)
+        pokeElemOff output (index * 2 + 1) (CDouble y)
+    )
+
+meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+meshCopyTrianglesU32 =
+  copyHandleProjection
+    "mesh"
+    "triangles_written"
+    "triangles"
+    T.innerFaceVertexTriples
+    ( \output index (firstVertex, secondVertex, thirdVertex) -> do
+        pokeElemOff output (index * 3) (T.unVertexId firstVertex)
+        pokeElemOff output (index * 3 + 1) (T.unVertexId secondVertex)
+        pokeElemOff output (index * 3 + 2) (T.unVertexId thirdVertex)
+    )
+
+copyHandleProjection
+  :: String
+  -> String
+  -> String
+  -> (HandleValue carrier -> V.Vector item)
+  -> (Ptr element -> Int -> item -> IO ())
+  -> Ptr carrier
+  -> Ptr element
+  -> CSize
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+{-# INLINE copyHandleProjection #-}
+copyHandleProjection handleLabel writtenLabel outputLabel project writeItem handlePointer output rawCapacity written obstructionPointer =
+  runBoundary obstructionPointer $
+    case requirePointer handleLabel handlePointer >> requirePointer writtenLabel written >> checkedCount 1 rawCapacity of
+      Left failure -> pure (Left failure)
+      Right capacity -> do
+        value <- dereferenceHandle handlePointer
+        let items = project value
+            required = V.length items
+        poke written (fromIntegral required)
+        case requireOutputCapacity outputLabel output required capacity of
+          Left failure -> pure (Left failure)
+          Right () -> V.imapM_ (writeItem output) items >> pure (Right ())
+
+meshFree :: Ptr CMesh -> IO ()
+meshFree = freeHandle
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Morphology.hs b/ffi/abi/Moonlight/Planar/Foreign/Morphology.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Morphology.hs
@@ -0,0 +1,165 @@
+module Moonlight.Planar.Foreign.Morphology
+  ( regionClose
+  , regionInset
+  , regionMinkowskiSum
+  , regionOffset
+  , regionOpen
+  , structuringElementCreateF64
+  , structuringElementFree
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Word (Word32)
+import Foreign.C.Types (CDouble, CSize, CUInt)
+import Foreign.Ptr (Ptr)
+import Foreign.Storable (poke)
+import qualified Data.Vector as V
+import Moonlight.Planar.Convex qualified as T (convexPolygon)
+import Moonlight.Planar.Exact qualified as T (exactPointFromPoint)
+import Moonlight.Planar.Minkowski qualified as T (MinkowskiError (..), MinkowskiOperation (..), MinkowskiReceipt (..), StructuringElement, closeWith, erodeBy, minkowskiSum, openWith, polygonOffset, structuringElement)
+import Moonlight.Planar.Region qualified as T (PlanarRegion)
+import qualified Moonlight.Planar.Point as T (Point (..))
+import Moonlight.Planar.Foreign.Boundary
+  ( checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , prepareHandleOutput
+  , produceHandle
+  , publishHandle
+  , readPoints
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Planar.Foreign.Contract
+  ( CMinkowskiReceipt (..)
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  , MinkowskiOperationCode (..)
+  , minkowskiOperationCodeId
+  )
+import Moonlight.Planar.Foreign.Obstruction
+  ( AbiFailure
+  , convexFailure
+  , minkowskiFailure
+  , pointInputFailure
+  , structuringElementEmptyFailure
+  )
+
+structuringElementCreateF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr (Ptr CStructuringElement)
+  -> Ptr CObstruction
+  -> IO CUInt
+structuringElementCreateF64 coordinates rawPointCount output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case checkedCount 2 rawPointCount of
+      Left failure -> pure (Left failure)
+      Right pointCount -> do
+        points <- readPoints coordinates pointCount
+        pure (points >>= buildStructuringElement)
+
+buildStructuringElement
+  :: V.Vector T.Point
+  -> Either AbiFailure T.StructuringElement
+buildStructuringElement points = do
+  exactPoints <-
+    V.imapM
+      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
+      points
+  submitted <-
+    maybe
+      (Left structuringElementEmptyFailure)
+      Right
+      (NonEmpty.nonEmpty (V.toList exactPoints))
+  polygon <- first convexFailure (T.convexPolygon submitted)
+  first minkowskiFailure (T.structuringElement polygon)
+
+structuringElementFree :: Ptr CStructuringElement -> IO ()
+structuringElementFree = freeHandle
+
+regionMinkowskiSum
+  :: Ptr CRegion
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> Ptr CObstruction
+  -> IO CUInt
+regionMinkowskiSum leftPointer rightPointer output receiptOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    produceRegionWithReceipt output receiptOutput $ do
+      case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          left <- dereferenceHandle leftPointer
+          right <- dereferenceHandle rightPointer
+          pure (first minkowskiFailure (T.minkowskiSum left right))
+
+regionOffset, regionInset, regionOpen, regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+regionOffset = structuringElementOperation T.polygonOffset
+regionInset = structuringElementOperation T.erodeBy
+regionOpen = structuringElementOperation T.openWith
+regionClose = structuringElementOperation T.closeWith
+
+structuringElementOperation
+  :: (T.StructuringElement -> T.PlanarRegion -> Either T.MinkowskiError (T.PlanarRegion, T.MinkowskiReceipt))
+  -> Ptr CStructuringElement
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> Ptr CObstruction
+  -> IO CUInt
+structuringElementOperation operation elementPointer regionPointer output receiptOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    produceRegionWithReceipt output receiptOutput $ do
+      case requirePointer "structuring element" elementPointer >> requirePointer "region" regionPointer of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          element <- dereferenceHandle elementPointer
+          region <- dereferenceHandle regionPointer
+          pure (first minkowskiFailure (operation element region))
+
+produceRegionWithReceipt
+  :: Ptr (Ptr CRegion)
+  -> Ptr CMinkowskiReceipt
+  -> IO (Either AbiFailure (T.PlanarRegion, T.MinkowskiReceipt))
+  -> IO (Either AbiFailure ())
+produceRegionWithReceipt output receiptOutput obtain = do
+  prepared <- prepareHandleOutput output
+  case (prepared, requirePointer "receipt" receiptOutput) of
+    (Left failure, _) -> pure (Left failure)
+    (_, Left failure) -> pure (Left failure)
+    (Right (), Right ()) -> do
+      outcome <- obtain
+      case outcome of
+        Left failure -> pure (Left failure)
+        Right (region, receipt) -> do
+          poke receiptOutput (minkowskiReceiptProjection receipt)
+          publishHandle output region
+          pure (Right ())
+
+minkowskiReceiptProjection :: T.MinkowskiReceipt -> CMinkowskiReceipt
+minkowskiReceiptProjection receipt =
+  CMinkowskiReceipt
+    { receiptOperation = minkowskiOperationCode (T.minkowskiOperation receipt)
+    , receiptInputComponents = fromIntegral (T.minkowskiInputComponents receipt)
+    , receiptConvexPieces = fromIntegral (T.minkowskiConvexPieces receipt)
+    , receiptGeneratedPieces = fromIntegral (T.minkowskiGeneratedPieces receipt)
+    , receiptGeneratedConvolutionEdges = fromIntegral (T.minkowskiGeneratedConvolutionEdges receipt)
+    , receiptOverlayPasses = fromIntegral (T.minkowskiOverlayPasses receipt)
+    , receiptExactCrossings = fromIntegral (T.minkowskiExactCrossings receipt)
+    , receiptOutputCells = fromIntegral (T.minkowskiOutputCells receipt)
+    , receiptExactCoordinateBitGrowth = fromIntegral (T.minkowskiExactCoordinateBitGrowth receipt)
+    }
+
+minkowskiOperationCode :: T.MinkowskiOperation -> Word32
+minkowskiOperationCode operation =
+  minkowskiOperationCodeId
+    ( case operation of
+        T.MinkowskiAddition -> MinkowskiOperationAddition
+        T.MinkowskiErosion -> MinkowskiOperationErosion
+        T.MinkowskiOpening -> MinkowskiOperationOpening
+        T.MinkowskiClosing -> MinkowskiOperationClosing
+    )
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Obstruction.hs b/ffi/abi/Moonlight/Planar/Foreign/Obstruction.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Obstruction.hs
@@ -0,0 +1,325 @@
+module Moonlight.Planar.Foreign.Obstruction
+  ( AbiFailure (..)
+  , RegionCountKind (..)
+  , RegionLayoutError (..)
+  , bufferTooSmallFailure
+  , buildFailure
+  , countOverflowFailure
+  , emptyObstruction
+  , minkowskiFailure
+  , convexFailure
+  , nullPointerFailure
+  , overlayFailure
+  , pointInputFailure
+  , projectionFailure
+  , regionLayoutFailure
+  , regionPublicationFailure
+  , regionValidationFailure
+  , runtimeFailure
+  , statusOk
+  , structuringElementEmptyFailure
+  , valuationFailure
+  ) where
+
+import Control.Exception (SomeException, displayException)
+import Data.Word (Word32, Word64)
+import Foreign.C.Types (CUInt)
+import Moonlight.Planar.Foreign.Contract
+import Moonlight.Planar.Convex qualified as T (ConvexError (..))
+import Moonlight.Planar.Handles.HandleDefs qualified as T (unDirectedEdgeId, unFaceId, unUndirectedEdgeId, unVertexId)
+import Moonlight.Planar.Minkowski qualified as T (MinkowskiError (..))
+import Moonlight.Planar.Overlay qualified as T (OverlayError (..))
+import Moonlight.Planar.Region qualified as T (RegionPublicationError (..), RegionValidationError (..))
+import qualified Moonlight.Planar.Types as T (BuildError (..))
+import qualified Moonlight.Planar.Scalar as T (CoordinateError (..), NonFiniteValue (..))
+import qualified Moonlight.Planar.Point as T (Point (..), PointValidationError (..))
+import Moonlight.Planar.Valuation qualified as T (ValuationError (..))
+
+data AbiFailure = AbiFailure !CUInt !CObstruction
+
+statusCode :: AbiStatus -> CUInt
+statusCode = fromIntegral . abiStatusId
+
+statusOk :: CUInt
+statusOk = statusCode AbiStatusOk
+
+emptyObstruction :: CObstruction
+emptyObstruction =
+  CObstruction
+    { obstructionCode = obstructionCodeId ObstructionNone
+    , obstructionCoordinateError = coordinateErrorCodeId CoordinateErrorNone
+    , obstructionInputIndex = maxBound
+    , obstructionFirstIndex = 0
+    , obstructionSecondIndex = 0
+    , obstructionFirstValue = 0
+    , obstructionSecondValue = 0
+    , obstructionPointX = 0
+    , obstructionPointY = 0
+    , obstructionMessage = ""
+    }
+
+apiFailure :: AbiStatus -> ObstructionCode -> String -> AbiFailure
+apiFailure status code message =
+  AbiFailure
+    (statusCode status)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId code
+      , obstructionMessage = message
+      }
+
+geometryFailure :: Show obstruction => ObstructionCode -> obstruction -> AbiFailure
+geometryFailure code obstruction =
+  apiFailure AbiStatusGeometryObstruction code (show obstruction)
+
+nullPointerFailure :: String -> AbiFailure
+nullPointerFailure label =
+  apiFailure AbiStatusNullPointer ObstructionNullPointer (label <> " must not be null")
+
+countOverflowFailure :: Word64 -> AbiFailure
+countOverflowFailure count =
+  AbiFailure
+    (statusCode AbiStatusCountOverflow)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionCountOverflow
+      , obstructionFirstIndex = count
+      , obstructionMessage = "count exceeds the host Int range"
+      }
+
+bufferTooSmallFailure :: Int -> Int -> AbiFailure
+bufferTooSmallFailure required capacity =
+  AbiFailure
+    (statusCode AbiStatusBufferTooSmall)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionBufferTooSmall
+      , obstructionFirstIndex = fromIntegral required
+      , obstructionSecondIndex = fromIntegral capacity
+      , obstructionMessage = "output buffer is smaller than the required element count"
+      }
+
+runtimeFailure :: SomeException -> AbiFailure
+runtimeFailure = apiFailure AbiStatusRuntimeFailure ObstructionRuntimeFailure . displayException
+
+buildFailure :: T.BuildError -> AbiFailure
+buildFailure obstruction =
+  AbiFailure (statusCode AbiStatusGeometryObstruction) (buildErrorObstruction obstruction)
+
+data RegionCountKind
+  = LoopPointCounts
+  | ComponentLoopCounts
+  deriving stock (Eq, Show)
+
+data RegionLayoutError
+  = RegionGroupEmpty !RegionCountKind !Int
+  | RegionCountTotalMismatch !RegionCountKind !Integer !Integer
+  deriving stock (Eq, Show)
+
+regionLayoutFailure :: RegionLayoutError -> AbiFailure
+regionLayoutFailure layoutError =
+  AbiFailure (statusCode AbiStatusGeometryObstruction) (layoutObstruction layoutError)
+
+layoutObstruction :: RegionLayoutError -> CObstruction
+layoutObstruction layoutError =
+  ( case layoutError of
+      RegionCountTotalMismatch _ actual expected ->
+        base
+          { obstructionFirstIndex = fromIntegral actual
+          , obstructionSecondIndex = fromIntegral expected
+          }
+      RegionGroupEmpty _ index -> base {obstructionInputIndex = fromIntegral index}
+  )
+    {obstructionMessage = show layoutError}
+ where
+  base = emptyObstruction {obstructionCode = obstructionCodeId ObstructionRegionLayoutInvalid}
+
+regionValidationFailure :: T.RegionValidationError -> AbiFailure
+regionValidationFailure = geometryFailure ObstructionRegionValidationFailed
+
+overlayFailure :: T.OverlayError -> AbiFailure
+overlayFailure = geometryFailure ObstructionOverlayFailed
+
+regionPublicationFailure :: T.RegionPublicationError -> AbiFailure
+regionPublicationFailure = geometryFailure ObstructionRegionPublicationFailed
+
+valuationFailure :: T.ValuationError -> AbiFailure
+valuationFailure = geometryFailure ObstructionValuationFailed
+
+minkowskiFailure :: T.MinkowskiError -> AbiFailure
+minkowskiFailure = geometryFailure ObstructionMinkowskiFailed
+
+convexFailure :: T.ConvexError -> AbiFailure
+convexFailure = geometryFailure ObstructionConvexFailed
+
+structuringElementEmptyFailure :: AbiFailure
+structuringElementEmptyFailure =
+  apiFailure
+    AbiStatusGeometryObstruction
+    ObstructionRegionLayoutInvalid
+    "structuring element requires at least one point"
+
+pointInputFailure :: Int -> T.Point -> T.PointValidationError -> AbiFailure
+pointInputFailure index (T.Point x y) pointError =
+  AbiFailure
+    (statusCode AbiStatusGeometryObstruction)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
+      , obstructionCoordinateError = coordinateErrorCode reason
+      , obstructionInputIndex = fromIntegral index
+      , obstructionFirstValue = invalidValue
+      , obstructionPointX = x
+      , obstructionPointY = y
+      , obstructionMessage = show pointError
+      }
+ where
+  (invalidValue, reason) =
+    case pointError of
+      T.InvalidPointX coordinateError -> (x, coordinateError)
+      T.InvalidPointY coordinateError -> (y, coordinateError)
+
+projectionFailure :: Int -> T.PointValidationError -> AbiFailure
+projectionFailure index pointError =
+  AbiFailure
+    (statusCode AbiStatusGeometryObstruction)
+    emptyObstruction
+      { obstructionCode = obstructionCodeId ObstructionRegionProjectionFailed
+      , obstructionCoordinateError =
+          coordinateErrorCode
+            ( case pointError of
+                T.InvalidPointX coordinateError -> coordinateError
+                T.InvalidPointY coordinateError -> coordinateError
+            )
+      , obstructionInputIndex = fromIntegral index
+      , obstructionMessage = show pointError
+      }
+
+buildErrorObstruction :: T.BuildError -> CObstruction
+buildErrorObstruction failure =
+  ( case failure of
+      T.InvalidCoordinate inputIndex value reason ->
+        emptyObstruction
+          { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
+          , obstructionCoordinateError = coordinateErrorCode reason
+          , obstructionInputIndex = maybe maxBound fromIntegral inputIndex
+          , obstructionFirstValue = value
+          }
+      T.PointLocationFailed (T.Point x y) -> pointObstruction ObstructionPointLocationFailed x y
+      T.LocationWalkExhausted (T.Point x y) steps ->
+        (pointObstruction ObstructionLocationWalkExhausted x y) {obstructionFirstIndex = fromIntegral steps}
+      T.RefinementInputTopologyInvalid _ -> codeOnly ObstructionRefinementInputTopologyInvalid
+      T.FreshInsertionMatchedExistingVertex firstVertex secondVertex -> indices ObstructionFreshInsertionMatchedExistingVertex (T.unVertexId firstVertex) (T.unVertexId secondVertex)
+      T.DegenerateLineEndpointMissingOutgoing vertex -> firstIndex ObstructionDegenerateLineEndpointMissingOutgoing (T.unVertexId vertex)
+      T.DegenerateLineEndpointTurnMissing index -> firstIndex ObstructionDegenerateLineEndpointTurnMissing index
+      T.DegenerateLineConnectedVertexMissing index -> firstIndex ObstructionDegenerateLineConnectedVertexMissing index
+      T.HullStartNotVisible edge -> firstIndex ObstructionHullStartNotVisible (T.unDirectedEdgeId edge)
+      T.OuterRangeDidNotTerminate firstEdge secondEdge steps ->
+        (indices ObstructionOuterRangeDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.OuterRangeContainsInnerEdge edge face -> indices ObstructionOuterRangeContainsInnerEdge (T.unDirectedEdgeId edge) (T.unFaceId face)
+      T.ConstrainedEdgeFlipRefused edge -> firstIndex ObstructionConstrainedEdgeFlipRefused (T.unUndirectedEdgeId edge)
+      T.RemovalVertexOutOfRange vertex count -> indices ObstructionRemovalVertexOutOfRange (T.unVertexId vertex) count
+      T.RemovalEdgeOutOfRange edge count -> indices ObstructionRemovalEdgeOutOfRange (T.unUndirectedEdgeId edge) count
+      T.RemovalFaceOutOfRange face count -> indices ObstructionRemovalFaceOutOfRange (T.unFaceId face) count
+      T.RemovalFaceCycleDidNotTerminate face edge steps ->
+        (indices ObstructionRemovalFaceCycleDidNotTerminate (T.unFaceId face) (T.unDirectedEdgeId edge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.RemovalEmptyTriangulation vertex -> firstIndex ObstructionRemovalEmptyTriangulation (T.unVertexId vertex)
+      T.RemovalTwoPointDegreeMismatch vertex degree -> indices ObstructionRemovalTwoPointDegreeMismatch (T.unVertexId vertex) degree
+      T.RemovalCollinearDegreeMismatch vertex degree -> indices ObstructionRemovalCollinearDegreeMismatch (T.unVertexId vertex) degree
+      T.RemovalBorderTooShort count -> firstIndex ObstructionRemovalBorderTooShort count
+      T.RemovalBorderArityMismatch count -> firstIndex ObstructionRemovalBorderArityMismatch count
+      T.RemovalOutgoingCycleDidNotTerminate vertex edge steps ->
+        (indices ObstructionRemovalOutgoingCycleDidNotTerminate (T.unVertexId vertex) (T.unDirectedEdgeId edge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.CircleSweepHullEmpty -> codeOnly ObstructionCircleSweepHullEmpty
+      T.OuterCycleDidNotTerminate firstEdge secondEdge steps ->
+        (indices ObstructionOuterCycleDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
+          {obstructionFirstValue = fromIntegral steps}
+      T.HierarchyLevelPopulationMismatch level expected observed ->
+        (indices ObstructionHierarchyLevelPopulationMismatch expected observed) {obstructionFirstValue = fromIntegral level}
+      T.HierarchyInsertionHandleMismatch expected observed -> indices ObstructionHierarchyInsertionHandleMismatch (T.unVertexId expected) (T.unVertexId observed)
+      T.PointIndexCapacityExhausted count -> firstIndex ObstructionPointIndexCapacityExhausted count
+      T.RefinementMinimumAngleNotFinite value -> nonFinite ObstructionRefinementMinimumAngleNotFinite value
+      T.RefinementMinimumAngleOutOfRange value -> firstValue ObstructionRefinementMinimumAngleOutOfRange value
+      T.RefinementMinimumAngleDerivedRatioNotFinite value -> nonFinite ObstructionRefinementMinimumAngleDerivedRatioNotFinite value
+      T.RefinementMaximumAdditionalVerticesNegative value -> firstValue ObstructionRefinementMaximumAdditionalVerticesNegative (fromIntegral value)
+      T.RefinementMinimumAreaNotFinite value -> nonFinite ObstructionRefinementMinimumAreaNotFinite value
+      T.RefinementMinimumAreaNegative value -> firstValue ObstructionRefinementMinimumAreaNegative value
+      T.RefinementMaximumAreaNotFinite value -> nonFinite ObstructionRefinementMaximumAreaNotFinite value
+      T.RefinementMaximumAreaNotPositive value -> firstValue ObstructionRefinementMaximumAreaNotPositive value
+      T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite ObstructionRefinementMaximumRadiusEdgeRatioNotFinite value
+      T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue ObstructionRefinementMaximumRadiusEdgeRatioNotPositive value
+      T.RefinementMaximumEdgeLengthNotFinite value -> nonFinite ObstructionRefinementMaximumEdgeLengthNotFinite value
+      T.RefinementMaximumEdgeLengthNotPositive value -> firstValue ObstructionRefinementMaximumEdgeLengthNotPositive value
+      T.RefinementMinimumAreaExceedsMaximum minimumArea maximumArea -> values ObstructionRefinementMinimumAreaExceedsMaximum minimumArea maximumArea
+      T.RefinementSeedFaceNotActive face count -> indices ObstructionRefinementSeedFaceNotActive (T.unFaceId face) count
+      T.RefinementDomainTopologyChanged -> codeOnly ObstructionRefinementDomainTopologyChanged
+      T.RefinementDomainRequiresConvexHullPreservation -> codeOnly ObstructionRefinementDomainRequiresConvexHullPreservation
+      T.RefinementDomainRequiresConstraintPreservation -> codeOnly ObstructionRefinementDomainRequiresConstraintPreservation
+      T.RefinementDomainForbidsOuterFaceExclusion -> codeOnly ObstructionRefinementDomainForbidsOuterFaceExclusion
+      T.RefinementDomainWouldCrossInterface edge face -> indices ObstructionRefinementDomainWouldCrossInterface (T.unUndirectedEdgeId edge) (T.unFaceId face)
+      T.RefinementDomainWouldRewriteProtectedFace face -> firstIndex ObstructionRefinementDomainWouldRewriteProtectedFace (T.unFaceId face)
+      T.RefinementDomainProtectedFaceChanged face -> firstIndex ObstructionRefinementDomainProtectedFaceChanged (T.unFaceId face)
+      T.RefinementDomainInterfaceOppositeFaceNotPermitted edge face ->
+        indices ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted (T.unUndirectedEdgeId edge) (T.unFaceId face)
+      T.RefinementOversizedEdge face edge actual bound ->
+        (indices ObstructionRefinementOversizedEdge (T.unFaceId face) (T.unUndirectedEdgeId edge))
+          { obstructionFirstValue = actual
+          , obstructionSecondValue = bound
+          }
+      T.CapacityExceeded count -> firstIndex ObstructionCapacityExceeded count
+      T.HalfEdgeCapacityExceeded requested capacity -> indices ObstructionHalfEdgeCapacityExceeded requested capacity
+      T.FaceCapacityExceeded requested capacity -> indices ObstructionFaceCapacityExceeded requested capacity
+      T.PayloadStorageFailure _ -> codeOnly ObstructionPayloadStorageFailure
+      T.CoordinatePayloadCountMismatch coordinates payloads -> indices ObstructionCoordinatePayloadCountMismatch coordinates payloads
+      T.CircleSweepRequiresDenseStorage -> codeOnly ObstructionCircleSweepRequiresDenseStorage
+      T.SeamFrontierUnavailable -> codeOnly ObstructionSeamFrontierUnavailable
+      T.RefinementDomainRequiresFiniteVertexBudget -> codeOnly ObstructionRefinementDomainRequiresFiniteVertexBudget
+      T.SeamSourceEdgeRequiresFlip edge -> firstIndex ObstructionSeamSourceEdgeRequiresFlip (T.unUndirectedEdgeId edge)
+      T.RefinementSeamBridgeBudgetExceeded required available -> indices ObstructionRefinementSeamBridgeBudgetExceeded required available
+      T.RefinementSeamBridgeMidpointCollapsed edge -> firstIndex ObstructionRefinementSeamBridgeMidpointCollapsed (T.unUndirectedEdgeId edge)
+      T.BoundarySplitRequiresBoundaryEdge edge -> firstIndex ObstructionBoundarySplitRequiresBoundaryEdge (T.unUndirectedEdgeId edge)
+  )
+    {obstructionMessage = show failure}
+ where
+  codeOnly :: ObstructionCode -> CObstruction
+  codeOnly code = emptyObstruction {obstructionCode = obstructionCodeId code}
+  firstIndex :: Integral index => ObstructionCode -> index -> CObstruction
+  firstIndex code index = (codeOnly code) {obstructionFirstIndex = fromIntegral index}
+  indices :: (Integral first, Integral second) => ObstructionCode -> first -> second -> CObstruction
+  indices code firstIndexValue secondIndexValue =
+    (codeOnly code)
+      { obstructionFirstIndex = fromIntegral firstIndexValue
+      , obstructionSecondIndex = fromIntegral secondIndexValue
+      }
+  firstValue :: ObstructionCode -> Double -> CObstruction
+  firstValue code value = (codeOnly code) {obstructionFirstValue = value}
+  values :: ObstructionCode -> Double -> Double -> CObstruction
+  values code firstCoordinateValue secondCoordinateValue =
+    (codeOnly code)
+      { obstructionFirstValue = firstCoordinateValue
+      , obstructionSecondValue = secondCoordinateValue
+      }
+  pointObstruction :: ObstructionCode -> Double -> Double -> CObstruction
+  pointObstruction code x y =
+    (codeOnly code)
+      { obstructionPointX = x
+      , obstructionPointY = y
+      }
+  nonFinite :: ObstructionCode -> T.NonFiniteValue -> CObstruction
+  nonFinite code value = firstIndex code (nonFiniteCode value)
+
+coordinateErrorCode :: T.CoordinateError -> Word32
+coordinateErrorCode reason =
+  coordinateErrorCodeId
+    ( case reason of
+        T.CoordinateNaN -> CoordinateErrorNaN
+        T.CoordinateInfinite -> CoordinateErrorInfinite
+        T.CoordinateTooSmall -> CoordinateErrorTooSmall
+        T.CoordinateTooLarge -> CoordinateErrorTooLarge
+    )
+
+nonFiniteCode :: T.NonFiniteValue -> Word32
+nonFiniteCode value =
+  case value of
+    T.ValueNaN -> 1
+    T.ValuePositiveInfinity -> 2
+    T.ValueNegativeInfinity -> 3
diff --git a/ffi/abi/Moonlight/Planar/Foreign/Region.hs b/ffi/abi/Moonlight/Planar/Foreign/Region.hs
new file mode 100644
--- /dev/null
+++ b/ffi/abi/Moonlight/Planar/Foreign/Region.hs
@@ -0,0 +1,413 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Moonlight.Planar.Foreign.Region
+  ( regionCopyF64
+  , regionCounts
+  , regionCreateF64
+  , regionDifference
+  , regionFree
+  , regionIntersection
+  , regionLocatePointF64
+  , regionMeasure
+  , regionSymmetricDifference
+  , regionUnion
+  ) where
+
+import Data.Bifunctor (first)
+import Data.Int (Int64)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Foreign.C.String (withCStringLen)
+import Foreign.C.Types (CChar, CDouble (..), CSize, CUInt)
+import Foreign.Marshal.Utils (copyBytes)
+import Foreign.Ptr (Ptr, nullPtr)
+import Foreign.Storable (peekElemOff, poke, pokeElemOff)
+import qualified Data.Vector as V
+import Moonlight.Planar.Exact qualified as T (ExactPoint, exactPointFromPoint, exactPointToEmbeddingCandidate)
+import Moonlight.Planar.Internal.ExactRational qualified as T (exactRationalDenominator, exactRationalNumerator)
+import Moonlight.Planar.Overlay qualified as T (overlayLayers, overlaySelectedRegion)
+import Moonlight.Planar.Region qualified as T (ExactLoop, PlanarRegion, PolygonComponent, RegionPointLocation (..), exactLoop, exactLoopPoints, planarLayer, planarRegion, planarRegionComponents, polygonComponent, polygonHoleLoops, polygonOuterLoop, regionPointLocation)
+import qualified Moonlight.Planar.Point as T (Point (..), queryPointValue)
+import Moonlight.Planar.Valuation qualified as T (CertifiedInterval (..), ExactLengthMeasurement, PlanarValuations, eulerCharacteristicValue, exactAreaValue, exactLengthBounds, planarValuationsPerimeter, regionValuations, valuationArea, valuationEuler)
+import Moonlight.Planar.Foreign.Boundary
+  ( checkedCount
+  , dereferenceHandle
+  , freeHandle
+  , produceHandle
+  , readPoints
+  , requireOutputCapacity
+  , requirePointer
+  , runBoundary
+  )
+import Moonlight.Planar.Foreign.Contract
+  ( CObstruction
+  , CRegion
+  , RegionLocationCode (..)
+  , regionLocationCodeId
+  )
+import Moonlight.Planar.Foreign.Obstruction
+  ( AbiFailure
+  , RegionCountKind (..)
+  , RegionLayoutError (..)
+  , nullPointerFailure
+  , overlayFailure
+  , pointInputFailure
+  , projectionFailure
+  , regionLayoutFailure
+  , regionPublicationFailure
+  , regionValidationFailure
+  , valuationFailure
+  )
+
+readCounts
+  :: String
+  -> Ptr CSize
+  -> Int
+  -> IO (Either AbiFailure (V.Vector Int))
+readCounts _ _ 0 = pure (Right V.empty)
+readCounts label pointer count
+  | pointer == nullPtr = pure (Left (nullPointerFailure label))
+  | otherwise = do
+      rawCounts <- V.generateM count (peekElemOff pointer)
+      pure (V.mapM (checkedCount 1) rawCounts)
+
+validateCounts
+  :: RegionCountKind
+  -> Int
+  -> V.Vector Int
+  -> Either RegionLayoutError ()
+validateCounts kind expectedTotal counts =
+  case V.findIndex (== 0) counts of
+    Just index -> Left (RegionGroupEmpty kind index)
+    Nothing
+      | observedTotal /= toInteger expectedTotal ->
+          Left (RegionCountTotalMismatch kind observedTotal (toInteger expectedTotal))
+      | otherwise -> Right ()
+ where
+  observedTotal = V.foldl' (\total count -> total + toInteger count) 0 counts
+
+regionCreateF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr (Ptr CRegion)
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCreateF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount output obstructionPointer =
+  runBoundary obstructionPointer $
+    produceHandle output $
+      readRegionF64
+        coordinates
+        rawPointCount
+        loopPointCounts
+        rawLoopCount
+        componentLoopCounts
+        rawComponentCount
+
+readRegionF64
+  :: Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> IO (Either AbiFailure T.PlanarRegion)
+readRegionF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount =
+  case checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount of
+    Left failure -> pure (Left failure)
+    Right (pointCount, loopCount, componentCount) -> do
+      points <- readPoints coordinates pointCount
+      loopCounts <- readCounts "loop_point_counts" loopPointCounts loopCount
+      componentCounts <- readCounts "component_loop_counts" componentLoopCounts componentCount
+      pure $ do
+        admittedPoints <- points
+        admittedLoopCounts <- loopCounts
+        admittedComponentCounts <- componentCounts
+        first regionLayoutFailure (validateCounts LoopPointCounts pointCount admittedLoopCounts)
+        first regionLayoutFailure (validateCounts ComponentLoopCounts loopCount admittedComponentCounts)
+        buildRegion admittedPoints admittedLoopCounts admittedComponentCounts
+
+checkedRegionInputCounts
+  :: CSize
+  -> CSize
+  -> CSize
+  -> Either AbiFailure (Int, Int, Int)
+checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount = do
+  pointCount <- checkedCount 2 rawPointCount
+  loopCount <- checkedCount 1 rawLoopCount
+  componentCount <- checkedCount 1 rawComponentCount
+  pure (pointCount, loopCount, componentCount)
+
+buildRegion
+  :: V.Vector T.Point
+  -> V.Vector Int
+  -> V.Vector Int
+  -> Either AbiFailure T.PlanarRegion
+buildRegion points loopCounts componentCounts = do
+  exactPoints <-
+    V.imapM
+      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
+      points
+  loops <- V.imapM (buildLoop exactPoints) (adjacentOffsets loopCounts)
+  components <- V.imapM (buildComponent loops) (adjacentOffsets componentCounts)
+  first regionValidationFailure (T.planarRegion (V.toList components))
+
+adjacentOffsets :: V.Vector Int -> V.Vector (Int, Int)
+adjacentOffsets counts =
+  let offsets = V.scanl' (+) 0 counts
+   in V.zip offsets (V.drop 1 offsets)
+
+buildLoop
+  :: V.Vector T.ExactPoint
+  -> Int
+  -> (Int, Int)
+  -> Either AbiFailure T.ExactLoop
+buildLoop points loopIndex (start, end) =
+  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) points)) of
+    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty LoopPointCounts loopIndex))
+    Just submitted -> first regionValidationFailure (T.exactLoop submitted)
+
+buildComponent
+  :: V.Vector T.ExactLoop
+  -> Int
+  -> (Int, Int)
+  -> Either AbiFailure T.PolygonComponent
+buildComponent loops componentIndex (start, end) =
+  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) loops)) of
+    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty ComponentLoopCounts componentIndex))
+    Just (outer :| holes) -> first regionValidationFailure (T.polygonComponent outer holes)
+
+regionShape
+  :: T.PlanarRegion
+  -> ([T.PolygonComponent], [[T.ExactLoop]], [T.ExactLoop])
+regionShape region =
+  let components = T.planarRegionComponents region
+      componentLoops component = T.polygonOuterLoop component : T.polygonHoleLoops component
+      loopsByComponent = map componentLoops components
+   in (components, loopsByComponent, concat loopsByComponent)
+
+regionCounts
+  :: Ptr CRegion
+  -> Ptr CSize
+  -> Ptr CSize
+  -> Ptr CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCounts regionPointer componentCountOutput loopCountOutput pointCountOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "component_count" componentCountOutput
+        >> requirePointer "loop_count" loopCountOutput
+        >> requirePointer "point_count" pointCountOutput
+    of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        region <- dereferenceHandle regionPointer
+        let (components, _, loops) = regionShape region
+            pointCount = sum (map (NonEmpty.length . T.exactLoopPoints) loops)
+        poke componentCountOutput (fromIntegral (length components))
+        poke loopCountOutput (fromIntegral (length loops))
+        poke pointCountOutput (fromIntegral pointCount)
+        pure (Right ())
+
+data RegionProjection = RegionProjection
+  { projectionPoints :: !(V.Vector T.Point)
+  , projectionLoopPointOffsets :: !(V.Vector CSize)
+  , projectionComponentLoopOffsets :: !(V.Vector CSize)
+  }
+
+regionProjection :: T.PlanarRegion -> Either AbiFailure RegionProjection
+regionProjection region = do
+  let (_, loopsByComponent, loops) = regionShape region
+      exactPoints = concatMap (NonEmpty.toList . T.exactLoopPoints) loops
+      loopPointOffsets = scanl (+) 0 (map (NonEmpty.length . T.exactLoopPoints) loops)
+      componentLoopOffsets = scanl (+) 0 (map length loopsByComponent)
+  projectedPoints <-
+    V.fromList
+      <$> traverse
+        (uncurry projectPoint)
+        (zip [0 ..] exactPoints)
+  pure
+    RegionProjection
+      { projectionPoints = projectedPoints
+      , projectionLoopPointOffsets = V.fromList (map fromIntegral loopPointOffsets)
+      , projectionComponentLoopOffsets = V.fromList (map fromIntegral componentLoopOffsets)
+      }
+ where
+  projectPoint index point =
+    T.queryPointValue
+      <$> first (projectionFailure index) (T.exactPointToEmbeddingCandidate point)
+
+regionCopyF64
+  :: Ptr CRegion
+  -> Ptr CDouble
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CSize
+  -> CSize
+  -> Ptr CObstruction
+  -> IO CUInt
+regionCopyF64 regionPointer coordinates rawPointCapacity loopPointOffsets rawLoopOffsetCapacity componentLoopOffsets rawComponentOffsetCapacity obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      (,,)
+        <$> (requirePointer "region" regionPointer >> checkedCount 2 rawPointCapacity)
+        <*> checkedCount 1 rawLoopOffsetCapacity
+        <*> checkedCount 1 rawComponentOffsetCapacity
+    of
+      Left failure -> pure (Left failure)
+      Right (pointCapacity, loopOffsetCapacity, componentOffsetCapacity) -> do
+        region <- dereferenceHandle regionPointer
+        case regionProjection region of
+          Left failure -> pure (Left failure)
+          Right RegionProjection {..} ->
+            case
+              requireOutputCapacity "coordinates" coordinates (V.length projectionPoints) pointCapacity
+                >> requireOutputCapacity "loop_point_offsets" loopPointOffsets (V.length projectionLoopPointOffsets) loopOffsetCapacity
+                >> requireOutputCapacity "component_loop_offsets" componentLoopOffsets (V.length projectionComponentLoopOffsets) componentOffsetCapacity
+            of
+              Left failure -> pure (Left failure)
+              Right () -> do
+                V.imapM_
+                  ( \index (T.Point x y) -> do
+                      pokeElemOff coordinates (index * 2) (CDouble x)
+                      pokeElemOff coordinates (index * 2 + 1) (CDouble y)
+                  )
+                  projectionPoints
+                V.imapM_ (pokeElemOff loopPointOffsets) projectionLoopPointOffsets
+                V.imapM_ (pokeElemOff componentLoopOffsets) projectionComponentLoopOffsets
+                pure (Right ())
+
+regionUnion, regionIntersection, regionDifference, regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+regionUnion = binaryRegionOperation (\(left, right) -> left || right)
+regionIntersection = binaryRegionOperation (\(left, right) -> left && right)
+regionDifference = binaryRegionOperation (\(left, right) -> left && not right)
+regionSymmetricDifference = binaryRegionOperation (uncurry (/=))
+
+binaryRegionOperation
+  :: ((Bool, Bool) -> Bool)
+  -> Ptr CRegion
+  -> Ptr CRegion
+  -> Ptr (Ptr CRegion)
+  -> Ptr CObstruction
+  -> IO CUInt
+binaryRegionOperation selected leftPointer rightPointer output obstructionPointer =
+  runBoundary obstructionPointer $ produceHandle output $ do
+    case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        left <- dereferenceHandle leftPointer
+        right <- dereferenceHandle rightPointer
+        pure (exactRegionBoolean selected left right)
+
+exactRegionBoolean
+  :: ((Bool, Bool) -> Bool)
+  -> T.PlanarRegion
+  -> T.PlanarRegion
+  -> Either AbiFailure T.PlanarRegion
+exactRegionBoolean selected left right = do
+  leftLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True left))
+  rightLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True right))
+  overlay <- first overlayFailure (T.overlayLayers leftLayer rightLayer)
+  first regionPublicationFailure (T.overlaySelectedRegion selected overlay)
+
+regionLocatePointF64
+  :: Ptr CRegion
+  -> CDouble
+  -> CDouble
+  -> Ptr CUInt
+  -> Ptr CObstruction
+  -> IO CUInt
+regionLocatePointF64 regionPointer (CDouble x) (CDouble y) output obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "location" output
+        >> first (pointInputFailure 0 (T.Point x y)) (T.exactPointFromPoint (T.Point x y))
+    of
+      Left failure -> pure (Left failure)
+      Right query -> do
+        region <- dereferenceHandle regionPointer
+        poke output (regionLocationCode (T.regionPointLocation region query))
+        pure (Right ())
+
+regionLocationCode :: T.RegionPointLocation -> CUInt
+regionLocationCode location =
+  fromIntegral
+    ( regionLocationCodeId
+        ( case location of
+            T.RegionExterior -> RegionLocationExterior
+            T.RegionOnBoundary -> RegionLocationBoundary
+            T.RegionInterior -> RegionLocationInterior
+        )
+    )
+
+regionMeasure
+  :: Ptr CRegion
+  -> Ptr Int64
+  -> Ptr CChar
+  -> CSize
+  -> Ptr CSize
+  -> Ptr CDouble
+  -> Ptr CDouble
+  -> Ptr CObstruction
+  -> IO CUInt
+regionMeasure regionPointer eulerOutput areaRatioOutput rawAreaCapacity areaBytesWritten perimeterLowerOutput perimeterUpperOutput obstructionPointer =
+  runBoundary obstructionPointer $
+    case
+      requirePointer "region" regionPointer
+        >> requirePointer "euler_characteristic" eulerOutput
+        >> requirePointer "area_bytes_written" areaBytesWritten
+        >> requirePointer "perimeter_lower" perimeterLowerOutput
+        >> requirePointer "perimeter_upper" perimeterUpperOutput
+        >> checkedCount 1 rawAreaCapacity
+    of
+      Left failure -> pure (Left failure)
+      Right areaCapacity -> do
+        region <- dereferenceHandle regionPointer
+        case regionMeasurements region of
+          Left failure -> pure (Left failure)
+          Right (valuations, perimeter) -> do
+            let area = T.exactAreaValue (T.valuationArea valuations)
+                areaText =
+                  show (T.exactRationalNumerator area)
+                    <> "/"
+                    <> show (T.exactRationalDenominator area)
+                bounds = T.exactLengthBounds perimeter
+            poke eulerOutput (fromIntegral (T.eulerCharacteristicValue (T.valuationEuler valuations)))
+            poke perimeterLowerOutput (CDouble (T.intervalLower bounds))
+            poke perimeterUpperOutput (CDouble (T.intervalUpper bounds))
+            copyCStringOutput areaText areaRatioOutput areaCapacity areaBytesWritten
+
+regionMeasurements
+  :: T.PlanarRegion
+  -> Either AbiFailure (T.PlanarValuations, T.ExactLengthMeasurement)
+regionMeasurements region = do
+  valuations <- first valuationFailure (T.regionValuations region)
+  perimeter <- first valuationFailure (T.planarValuationsPerimeter valuations)
+  pure (valuations, perimeter)
+
+copyCStringOutput
+  :: String
+  -> Ptr CChar
+  -> Int
+  -> Ptr CSize
+  -> IO (Either AbiFailure ())
+copyCStringOutput value output capacity bytesWritten =
+  withCStringLen value $ \(source, byteCount) -> do
+    poke bytesWritten (fromIntegral byteCount)
+    case requireOutputCapacity "area_ratio_utf8" output (byteCount + 1) capacity of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        copyBytes output source byteCount
+        pokeElemOff output byteCount 0
+        pure (Right ())
+
+regionFree :: Ptr CRegion -> IO ()
+regionFree = freeHandle
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Boundary.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE TypeFamilies #-}
-
-module Moonlight.Triangulation.Foreign.Boundary
-  ( GeometryMesh
-  , HandleValue
-  , checkedCount
-  , dereferenceHandle
-  , freeHandle
-  , prepareHandleOutput
-  , produceHandle
-  , publishHandle
-  , readPoints
-  , requireOutputCapacity
-  , requirePointer
-  , runBoundary
-  ) where
-
-import Control.Exception (SomeException, try)
-import Foreign.C.Types (CDouble (..), CSize, CUInt)
-import Foreign.Ptr (Ptr, castPtr, nullPtr)
-import Foreign.StablePtr
-  ( StablePtr
-  , castPtrToStablePtr
-  , castStablePtrToPtr
-  , deRefStablePtr
-  , freeStablePtr
-  , newStablePtr
-  )
-import Foreign.Storable (peekElemOff, poke)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation as T
-import Moonlight.Triangulation.Foreign.Contract
-  ( CMesh
-  , CObstruction
-  , CRegion
-  , CStructuringElement
-  )
-import Moonlight.Triangulation.Foreign.Obstruction
-  ( AbiFailure (..)
-  , bufferTooSmallFailure
-  , countOverflowFailure
-  , emptyObstruction
-  , nullPointerFailure
-  , runtimeFailure
-  , statusOk
-  )
-
-type GeometryMesh = T.DelaunayTriangulation ()
-
-type family HandleValue carrier where
-  HandleValue CMesh = GeometryMesh
-  HandleValue CRegion = T.PlanarRegion
-  HandleValue CStructuringElement = T.StructuringElement
-
-runBoundary :: Ptr CObstruction -> IO (Either AbiFailure ()) -> IO CUInt
-runBoundary obstructionPointer action = do
-  writeObstruction obstructionPointer emptyObstruction
-  outcome <- try action :: IO (Either SomeException (Either AbiFailure ()))
-  case outcome of
-    Left exception -> finishFailure obstructionPointer (runtimeFailure exception)
-    Right (Left failure) -> finishFailure obstructionPointer failure
-    Right (Right ()) -> pure statusOk
-
-finishFailure :: Ptr CObstruction -> AbiFailure -> IO CUInt
-finishFailure obstructionPointer (AbiFailure status obstruction) = do
-  writeObstruction obstructionPointer obstruction
-  pure status
-
-writeObstruction :: Ptr CObstruction -> CObstruction -> IO ()
-writeObstruction pointer obstruction
-  | pointer == nullPtr = pure ()
-  | otherwise = poke pointer obstruction
-
-requirePointer :: String -> Ptr value -> Either AbiFailure ()
-requirePointer label pointer
-  | pointer == nullPtr = Left (nullPointerFailure label)
-  | otherwise = Right ()
-
-checkedCount :: Int -> CSize -> Either AbiFailure Int
-checkedCount elementsPerItem rawCount
-  | toInteger rawCount * toInteger elementsPerItem > toInteger (maxBound :: Int) =
-      Left (countOverflowFailure (fromIntegral rawCount))
-  | otherwise = Right (fromIntegral rawCount)
-
-readPoints :: Ptr CDouble -> Int -> IO (Either AbiFailure (V.Vector T.Point))
-readPoints pointer count
-  | count == 0 = pure (Right V.empty)
-  | pointer == nullPtr = pure (Left (nullPointerFailure "coordinates"))
-  | otherwise =
-      Right
-        <$> V.generateM
-          count
-          ( \index -> do
-              CDouble x <- peekElemOff pointer (index * 2)
-              CDouble y <- peekElemOff pointer (index * 2 + 1)
-              pure (T.Point x y)
-          )
-
-prepareHandleOutput :: Ptr (Ptr carrier) -> IO (Either AbiFailure ())
-prepareHandleOutput pointer =
-  case requirePointer "result" pointer of
-    Left failure -> pure (Left failure)
-    Right () -> poke pointer nullPtr >> pure (Right ())
-
-publishHandle :: Ptr (Ptr carrier) -> HandleValue carrier -> IO ()
-publishHandle output value = do
-  stable <- newStablePtr value
-  poke output (castPtr (castStablePtrToPtr stable))
-
-produceHandle
-  :: Ptr (Ptr carrier)
-  -> IO (Either AbiFailure (HandleValue carrier))
-  -> IO (Either AbiFailure ())
-produceHandle output obtain = do
-  prepared <- prepareHandleOutput output
-  case prepared of
-    Left failure -> pure (Left failure)
-    Right () -> do
-      outcome <- obtain
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right value -> publishHandle output value >> pure (Right ())
-
-dereferenceHandle :: Ptr carrier -> IO (HandleValue carrier)
-dereferenceHandle pointer = deRefStablePtr (castPtrToStablePtr (castPtr pointer))
-
-freeHandle :: Ptr carrier -> IO ()
-freeHandle pointer
-  | pointer == nullPtr = pure ()
-  | otherwise = freeStablePtr (castPtrToStablePtr (castPtr pointer) :: StablePtr ())
-
-requireOutputCapacity :: String -> Ptr value -> Int -> Int -> Either AbiFailure ()
-requireOutputCapacity label output required capacity
-  | capacity < required = Left (bufferTooSmallFailure required capacity)
-  | required > 0 = requirePointer label output
-  | otherwise = Right ()
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Contract.hs
+++ /dev/null
@@ -1,650 +0,0 @@
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE RecordWildCards #-}
-
-module Moonlight.Triangulation.Foreign.Contract
-  ( AbiField (..)
-  , AbiFieldType (..)
-  , AbiFunction
-  , AbiFunctionFamily (..)
-  , AbiFunctionImplementation (..)
-  , AbiFunctionName (..)
-  , AbiHandle (..)
-  , AbiParameter (..)
-  , AbiParameterKind (..)
-  , AbiResult (..)
-  , AbiStatus (..)
-  , AbiStruct (..)
-  , CMesh
-  , CMinkowskiReceipt (..)
-  , CObstruction (..)
-  , CRegion
-  , CStructuringElement
-  , CoordinateErrorCode (..)
-  , MinkowskiOperationCode (..)
-  , ObstructionCode (..)
-  , RegionLocationCode (..)
-  , abiFunction
-  , abiFunctionImplementation
-  , abiFunctionParameters
-  , abiFunctionResult
-  , abiFunctionSymbol
-  , abiHandleSymbol
-  , abiStatusId
-  , abiStatusSymbol
-  , abiStructFields
-  , abiStructSymbol
-  , abiVersion
-  , allAbiFunctionNames
-  , allAbiFunctions
-  , allAbiHandles
-  , allAbiStatuses
-  , allAbiStructs
-  , allCoordinateErrorCodes
-  , allMinkowskiOperationCodes
-  , allObstructionCodes
-  , allRegionLocationCodes
-  , coordinateErrorCodeId
-  , coordinateErrorCodeSymbol
-  , minkowskiOperationCodeId
-  , minkowskiOperationCodeSymbol
-  , obstructionCodeId
-  , obstructionCodeSymbol
-  , regionLocationCodeId
-  , regionLocationCodeSymbol
-  ) where
-
-import Data.Word (Word32, Word64)
-import Foreign.C.String (peekCString, withCStringLen)
-import Foreign.C.Types (CChar)
-import Foreign.Marshal.Utils (copyBytes, fillBytes)
-import Foreign.Ptr (Ptr, castPtr, plusPtr)
-import Foreign.Storable (Storable (..))
-import GHC.Generics (Generic)
-
-abiVersion :: Word32
-abiVersion = 2
-
-data AbiStatus
-  = AbiStatusOk
-  | AbiStatusNullPointer
-  | AbiStatusCountOverflow
-  | AbiStatusBufferTooSmall
-  | AbiStatusGeometryObstruction
-  | AbiStatusRuntimeFailure
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allAbiStatuses :: [AbiStatus]
-allAbiStatuses = [minBound .. maxBound]
-
-abiStatusId :: AbiStatus -> Word32
-abiStatusId status =
-  case status of
-    AbiStatusOk -> 0
-    AbiStatusNullPointer -> 1
-    AbiStatusCountOverflow -> 2
-    AbiStatusBufferTooSmall -> 3
-    AbiStatusGeometryObstruction -> 4
-    AbiStatusRuntimeFailure -> 5
-
-abiStatusSymbol :: AbiStatus -> String
-abiStatusSymbol status =
-  case status of
-    AbiStatusOk -> "ML_STATUS_OK"
-    AbiStatusNullPointer -> "ML_STATUS_NULL_POINTER"
-    AbiStatusCountOverflow -> "ML_STATUS_COUNT_OVERFLOW"
-    AbiStatusBufferTooSmall -> "ML_STATUS_BUFFER_TOO_SMALL"
-    AbiStatusGeometryObstruction -> "ML_STATUS_GEOMETRY_OBSTRUCTION"
-    AbiStatusRuntimeFailure -> "ML_STATUS_RUNTIME_FAILURE"
-
-data ObstructionCode
-  = ObstructionNone
-  | ObstructionInvalidCoordinate
-  | ObstructionPointLocationFailed
-  | ObstructionLocationWalkExhausted
-  | ObstructionRefinementInputTopologyInvalid
-  | ObstructionFreshInsertionMatchedExistingVertex
-  | ObstructionDegenerateLineEndpointMissingOutgoing
-  | ObstructionDegenerateLineEndpointTurnMissing
-  | ObstructionDegenerateLineConnectedVertexMissing
-  | ObstructionHullStartNotVisible
-  | ObstructionOuterRangeDidNotTerminate
-  | ObstructionOuterRangeContainsInnerEdge
-  | ObstructionConstrainedEdgeFlipRefused
-  | ObstructionRemovalVertexOutOfRange
-  | ObstructionRemovalEdgeOutOfRange
-  | ObstructionRemovalFaceOutOfRange
-  | ObstructionRemovalFaceCycleDidNotTerminate
-  | ObstructionRemovalEmptyTriangulation
-  | ObstructionRemovalTwoPointDegreeMismatch
-  | ObstructionRemovalCollinearDegreeMismatch
-  | ObstructionRemovalBorderTooShort
-  | ObstructionRemovalBorderArityMismatch
-  | ObstructionRemovalOutgoingCycleDidNotTerminate
-  | ObstructionCircleSweepHullEmpty
-  | ObstructionOuterCycleDidNotTerminate
-  | ObstructionHierarchyLevelPopulationMismatch
-  | ObstructionHierarchyInsertionHandleMismatch
-  | ObstructionPointIndexCapacityExhausted
-  | ObstructionRefinementMinimumAngleNotFinite
-  | ObstructionRefinementMinimumAngleOutOfRange
-  | ObstructionRefinementMinimumAngleDerivedRatioNotFinite
-  | ObstructionRefinementMaximumAdditionalVerticesNegative
-  | ObstructionRefinementMinimumAreaNotFinite
-  | ObstructionRefinementMinimumAreaNegative
-  | ObstructionRefinementMaximumAreaNotFinite
-  | ObstructionRefinementMaximumAreaNotPositive
-  | ObstructionRefinementMaximumRadiusEdgeRatioNotFinite
-  | ObstructionRefinementMaximumRadiusEdgeRatioNotPositive
-  | ObstructionRefinementMinimumAreaExceedsMaximum
-  | ObstructionRefinementSeedFaceNotActive
-  | ObstructionRefinementDomainInterfaceEdgeNotActive
-  | ObstructionRefinementDomainInterfaceMissing
-  | ObstructionRefinementDomainInterfaceExtraneous
-  | ObstructionRefinementDomainTopologyChanged
-  | ObstructionRefinementDomainRequiresConvexHullPreservation
-  | ObstructionRefinementDomainRequiresConstraintPreservation
-  | ObstructionRefinementDomainForbidsOuterFaceExclusion
-  | ObstructionRefinementDomainWouldCrossInterface
-  | ObstructionRefinementDomainWouldRewriteProtectedFace
-  | ObstructionRefinementDomainProtectedFaceChanged
-  | ObstructionCapacityExceeded
-  | ObstructionHalfEdgeCapacityExceeded
-  | ObstructionFaceCapacityExceeded
-  | ObstructionPayloadStorageFailure
-  | ObstructionCoordinatePayloadCountMismatch
-  | ObstructionCircleSweepRequiresDenseStorage
-  | ObstructionSeamFrontierUnavailable
-  | ObstructionRefinementDomainRequiresFiniteVertexBudget
-  | ObstructionSeamSourceEdgeRequiresFlip
-  | ObstructionSeamProtectedSourceFaceChanged
-  | ObstructionRefinementMaximumEdgeLengthNotFinite
-  | ObstructionRefinementMaximumEdgeLengthNotPositive
-  | ObstructionRefinementOversizedEdge
-  | ObstructionRefinementSeamBridgeBudgetExceeded
-  | ObstructionRefinementSeamBridgeMidpointCollapsed
-  | ObstructionBoundarySplitRequiresBoundaryEdge
-  | ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted
-  | ObstructionNullPointer
-  | ObstructionCountOverflow
-  | ObstructionBufferTooSmall
-  | ObstructionRuntimeFailure
-  | ObstructionRegionLayoutInvalid
-  | ObstructionRegionValidationFailed
-  | ObstructionOverlayFailed
-  | ObstructionRegionPublicationFailed
-  | ObstructionValuationFailed
-  | ObstructionMinkowskiFailed
-  | ObstructionRegionProjectionFailed
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allObstructionCodes :: [ObstructionCode]
-allObstructionCodes = [minBound .. maxBound]
-
-obstructionCodeId :: ObstructionCode -> Word32
-obstructionCodeId code = fst (obstructionCodeMetadata code)
-
-obstructionCodeSymbol :: ObstructionCode -> String
-obstructionCodeSymbol code = snd (obstructionCodeMetadata code)
-
-obstructionCodeMetadata :: ObstructionCode -> (Word32, String)
-obstructionCodeMetadata code =
-  case code of
-    ObstructionNone -> (0, "ML_OBSTRUCTION_NONE")
-    ObstructionInvalidCoordinate -> (1, "ML_OBSTRUCTION_INVALID_COORDINATE")
-    ObstructionPointLocationFailed -> (2, "ML_OBSTRUCTION_POINT_LOCATION_FAILED")
-    ObstructionLocationWalkExhausted -> (3, "ML_OBSTRUCTION_LOCATION_WALK_EXHAUSTED")
-    ObstructionRefinementInputTopologyInvalid -> (4, "ML_OBSTRUCTION_REFINEMENT_INPUT_TOPOLOGY_INVALID")
-    ObstructionFreshInsertionMatchedExistingVertex -> (5, "ML_OBSTRUCTION_FRESH_INSERTION_MATCHED_EXISTING_VERTEX")
-    ObstructionDegenerateLineEndpointMissingOutgoing -> (6, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_MISSING_OUTGOING")
-    ObstructionDegenerateLineEndpointTurnMissing -> (7, "ML_OBSTRUCTION_DEGENERATE_LINE_ENDPOINT_TURN_MISSING")
-    ObstructionDegenerateLineConnectedVertexMissing -> (8, "ML_OBSTRUCTION_DEGENERATE_LINE_CONNECTED_VERTEX_MISSING")
-    ObstructionHullStartNotVisible -> (9, "ML_OBSTRUCTION_HULL_START_NOT_VISIBLE")
-    ObstructionOuterRangeDidNotTerminate -> (10, "ML_OBSTRUCTION_OUTER_RANGE_DID_NOT_TERMINATE")
-    ObstructionOuterRangeContainsInnerEdge -> (11, "ML_OBSTRUCTION_OUTER_RANGE_CONTAINS_INNER_EDGE")
-    ObstructionConstrainedEdgeFlipRefused -> (12, "ML_OBSTRUCTION_CONSTRAINED_EDGE_FLIP_REFUSED")
-    ObstructionRemovalVertexOutOfRange -> (13, "ML_OBSTRUCTION_REMOVAL_VERTEX_OUT_OF_RANGE")
-    ObstructionRemovalEdgeOutOfRange -> (14, "ML_OBSTRUCTION_REMOVAL_EDGE_OUT_OF_RANGE")
-    ObstructionRemovalFaceOutOfRange -> (15, "ML_OBSTRUCTION_REMOVAL_FACE_OUT_OF_RANGE")
-    ObstructionRemovalFaceCycleDidNotTerminate -> (16, "ML_OBSTRUCTION_REMOVAL_FACE_CYCLE_DID_NOT_TERMINATE")
-    ObstructionRemovalEmptyTriangulation -> (17, "ML_OBSTRUCTION_REMOVAL_EMPTY_TRIANGULATION")
-    ObstructionRemovalTwoPointDegreeMismatch -> (18, "ML_OBSTRUCTION_REMOVAL_TWO_POINT_DEGREE_MISMATCH")
-    ObstructionRemovalCollinearDegreeMismatch -> (19, "ML_OBSTRUCTION_REMOVAL_COLLINEAR_DEGREE_MISMATCH")
-    ObstructionRemovalBorderTooShort -> (20, "ML_OBSTRUCTION_REMOVAL_BORDER_TOO_SHORT")
-    ObstructionRemovalBorderArityMismatch -> (21, "ML_OBSTRUCTION_REMOVAL_BORDER_ARITY_MISMATCH")
-    ObstructionRemovalOutgoingCycleDidNotTerminate -> (22, "ML_OBSTRUCTION_REMOVAL_OUTGOING_CYCLE_DID_NOT_TERMINATE")
-    ObstructionCircleSweepHullEmpty -> (23, "ML_OBSTRUCTION_CIRCLE_SWEEP_HULL_EMPTY")
-    ObstructionOuterCycleDidNotTerminate -> (24, "ML_OBSTRUCTION_OUTER_CYCLE_DID_NOT_TERMINATE")
-    ObstructionHierarchyLevelPopulationMismatch -> (25, "ML_OBSTRUCTION_HIERARCHY_LEVEL_POPULATION_MISMATCH")
-    ObstructionHierarchyInsertionHandleMismatch -> (26, "ML_OBSTRUCTION_HIERARCHY_INSERTION_HANDLE_MISMATCH")
-    ObstructionPointIndexCapacityExhausted -> (27, "ML_OBSTRUCTION_POINT_INDEX_CAPACITY_EXHAUSTED")
-    ObstructionRefinementMinimumAngleNotFinite -> (28, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_NOT_FINITE")
-    ObstructionRefinementMinimumAngleOutOfRange -> (29, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_OUT_OF_RANGE")
-    ObstructionRefinementMinimumAngleDerivedRatioNotFinite -> (30, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_ANGLE_DERIVED_RATIO_NOT_FINITE")
-    ObstructionRefinementMaximumAdditionalVerticesNegative -> (31, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_ADDITIONAL_VERTICES_NEGATIVE")
-    ObstructionRefinementMinimumAreaNotFinite -> (32, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NOT_FINITE")
-    ObstructionRefinementMinimumAreaNegative -> (33, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_NEGATIVE")
-    ObstructionRefinementMaximumAreaNotFinite -> (34, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_FINITE")
-    ObstructionRefinementMaximumAreaNotPositive -> (35, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_AREA_NOT_POSITIVE")
-    ObstructionRefinementMaximumRadiusEdgeRatioNotFinite -> (36, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_FINITE")
-    ObstructionRefinementMaximumRadiusEdgeRatioNotPositive -> (37, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_RADIUS_EDGE_RATIO_NOT_POSITIVE")
-    ObstructionRefinementMinimumAreaExceedsMaximum -> (38, "ML_OBSTRUCTION_REFINEMENT_MINIMUM_AREA_EXCEEDS_MAXIMUM")
-    ObstructionRefinementSeedFaceNotActive -> (39, "ML_OBSTRUCTION_REFINEMENT_SEED_FACE_NOT_ACTIVE")
-    ObstructionRefinementDomainInterfaceEdgeNotActive -> (40, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EDGE_NOT_ACTIVE")
-    ObstructionRefinementDomainInterfaceMissing -> (41, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_MISSING")
-    ObstructionRefinementDomainInterfaceExtraneous -> (42, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_EXTRANEOUS")
-    ObstructionRefinementDomainTopologyChanged -> (43, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_TOPOLOGY_CHANGED")
-    ObstructionRefinementDomainRequiresConvexHullPreservation -> (44, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONVEX_HULL_PRESERVATION")
-    ObstructionRefinementDomainRequiresConstraintPreservation -> (45, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_CONSTRAINT_PRESERVATION")
-    ObstructionRefinementDomainForbidsOuterFaceExclusion -> (46, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_FORBIDS_OUTER_FACE_EXCLUSION")
-    ObstructionRefinementDomainWouldCrossInterface -> (47, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_CROSS_INTERFACE")
-    ObstructionRefinementDomainWouldRewriteProtectedFace -> (48, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_WOULD_REWRITE_PROTECTED_FACE")
-    ObstructionRefinementDomainProtectedFaceChanged -> (49, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_PROTECTED_FACE_CHANGED")
-    ObstructionCapacityExceeded -> (50, "ML_OBSTRUCTION_CAPACITY_EXCEEDED")
-    ObstructionHalfEdgeCapacityExceeded -> (51, "ML_OBSTRUCTION_HALF_EDGE_CAPACITY_EXCEEDED")
-    ObstructionFaceCapacityExceeded -> (52, "ML_OBSTRUCTION_FACE_CAPACITY_EXCEEDED")
-    ObstructionPayloadStorageFailure -> (53, "ML_OBSTRUCTION_PAYLOAD_STORAGE_FAILURE")
-    ObstructionCoordinatePayloadCountMismatch -> (54, "ML_OBSTRUCTION_COORDINATE_PAYLOAD_COUNT_MISMATCH")
-    ObstructionCircleSweepRequiresDenseStorage -> (55, "ML_OBSTRUCTION_CIRCLE_SWEEP_REQUIRES_DENSE_STORAGE")
-    ObstructionSeamFrontierUnavailable -> (56, "ML_OBSTRUCTION_SEAM_FRONTIER_UNAVAILABLE")
-    ObstructionRefinementDomainRequiresFiniteVertexBudget -> (57, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_REQUIRES_FINITE_VERTEX_BUDGET")
-    ObstructionSeamSourceEdgeRequiresFlip -> (58, "ML_OBSTRUCTION_SEAM_SOURCE_EDGE_REQUIRES_FLIP")
-    ObstructionSeamProtectedSourceFaceChanged -> (59, "ML_OBSTRUCTION_SEAM_PROTECTED_SOURCE_FACE_CHANGED")
-    ObstructionRefinementMaximumEdgeLengthNotFinite -> (60, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_FINITE")
-    ObstructionRefinementMaximumEdgeLengthNotPositive -> (61, "ML_OBSTRUCTION_REFINEMENT_MAXIMUM_EDGE_LENGTH_NOT_POSITIVE")
-    ObstructionRefinementOversizedEdge -> (62, "ML_OBSTRUCTION_REFINEMENT_OVERSIZED_EDGE")
-    ObstructionRefinementSeamBridgeBudgetExceeded -> (63, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_BUDGET_EXCEEDED")
-    ObstructionRefinementSeamBridgeMidpointCollapsed -> (64, "ML_OBSTRUCTION_REFINEMENT_SEAM_BRIDGE_MIDPOINT_COLLAPSED")
-    ObstructionBoundarySplitRequiresBoundaryEdge -> (65, "ML_OBSTRUCTION_BOUNDARY_SPLIT_REQUIRES_BOUNDARY_EDGE")
-    ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted -> (66, "ML_OBSTRUCTION_REFINEMENT_DOMAIN_INTERFACE_OPPOSITE_FACE_NOT_PERMITTED")
-    ObstructionNullPointer -> (100, "ML_OBSTRUCTION_NULL_POINTER")
-    ObstructionCountOverflow -> (101, "ML_OBSTRUCTION_COUNT_OVERFLOW")
-    ObstructionBufferTooSmall -> (102, "ML_OBSTRUCTION_BUFFER_TOO_SMALL")
-    ObstructionRuntimeFailure -> (103, "ML_OBSTRUCTION_RUNTIME_FAILURE")
-    ObstructionRegionLayoutInvalid -> (200, "ML_OBSTRUCTION_REGION_LAYOUT_INVALID")
-    ObstructionRegionValidationFailed -> (201, "ML_OBSTRUCTION_REGION_VALIDATION_FAILED")
-    ObstructionOverlayFailed -> (202, "ML_OBSTRUCTION_OVERLAY_FAILED")
-    ObstructionRegionPublicationFailed -> (203, "ML_OBSTRUCTION_REGION_PUBLICATION_FAILED")
-    ObstructionValuationFailed -> (204, "ML_OBSTRUCTION_VALUATION_FAILED")
-    ObstructionMinkowskiFailed -> (205, "ML_OBSTRUCTION_MINKOWSKI_FAILED")
-    ObstructionRegionProjectionFailed -> (206, "ML_OBSTRUCTION_REGION_PROJECTION_FAILED")
-
-data CoordinateErrorCode
-  = CoordinateErrorNone
-  | CoordinateErrorNaN
-  | CoordinateErrorInfinite
-  | CoordinateErrorTooSmall
-  | CoordinateErrorTooLarge
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allCoordinateErrorCodes :: [CoordinateErrorCode]
-allCoordinateErrorCodes = [minBound .. maxBound]
-
-coordinateErrorCodeId :: CoordinateErrorCode -> Word32
-coordinateErrorCodeId code =
-  case code of
-    CoordinateErrorNone -> 0
-    CoordinateErrorNaN -> 1
-    CoordinateErrorInfinite -> 2
-    CoordinateErrorTooSmall -> 3
-    CoordinateErrorTooLarge -> 4
-
-coordinateErrorCodeSymbol :: CoordinateErrorCode -> String
-coordinateErrorCodeSymbol code =
-  case code of
-    CoordinateErrorNone -> "ML_COORDINATE_ERROR_NONE"
-    CoordinateErrorNaN -> "ML_COORDINATE_ERROR_NAN"
-    CoordinateErrorInfinite -> "ML_COORDINATE_ERROR_INFINITE"
-    CoordinateErrorTooSmall -> "ML_COORDINATE_ERROR_TOO_SMALL"
-    CoordinateErrorTooLarge -> "ML_COORDINATE_ERROR_TOO_LARGE"
-
-data RegionLocationCode
-  = RegionLocationExterior
-  | RegionLocationBoundary
-  | RegionLocationInterior
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allRegionLocationCodes :: [RegionLocationCode]
-allRegionLocationCodes = [minBound .. maxBound]
-
-regionLocationCodeId :: RegionLocationCode -> Word32
-regionLocationCodeId code =
-  case code of
-    RegionLocationExterior -> 0
-    RegionLocationBoundary -> 1
-    RegionLocationInterior -> 2
-
-regionLocationCodeSymbol :: RegionLocationCode -> String
-regionLocationCodeSymbol code =
-  case code of
-    RegionLocationExterior -> "ML_REGION_EXTERIOR"
-    RegionLocationBoundary -> "ML_REGION_BOUNDARY"
-    RegionLocationInterior -> "ML_REGION_INTERIOR"
-
-data MinkowskiOperationCode
-  = MinkowskiOperationAddition
-  | MinkowskiOperationErosion
-  | MinkowskiOperationOpening
-  | MinkowskiOperationClosing
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allMinkowskiOperationCodes :: [MinkowskiOperationCode]
-allMinkowskiOperationCodes = [minBound .. maxBound]
-
-minkowskiOperationCodeId :: MinkowskiOperationCode -> Word32
-minkowskiOperationCodeId code =
-  case code of
-    MinkowskiOperationAddition -> 0
-    MinkowskiOperationErosion -> 1
-    MinkowskiOperationOpening -> 2
-    MinkowskiOperationClosing -> 3
-
-minkowskiOperationCodeSymbol :: MinkowskiOperationCode -> String
-minkowskiOperationCodeSymbol code =
-  case code of
-    MinkowskiOperationAddition -> "ML_MINKOWSKI_ADDITION"
-    MinkowskiOperationErosion -> "ML_MINKOWSKI_EROSION"
-    MinkowskiOperationOpening -> "ML_MINKOWSKI_OPENING"
-    MinkowskiOperationClosing -> "ML_MINKOWSKI_CLOSING"
-
-data CMesh
-data CRegion
-data CStructuringElement
-
-data CObstruction = CObstruction
-  { obstructionCode :: !Word32
-  , obstructionCoordinateError :: !Word32
-  , obstructionInputIndex :: !Word64
-  , obstructionFirstIndex :: !Word64
-  , obstructionSecondIndex :: !Word64
-  , obstructionFirstValue :: !Double
-  , obstructionSecondValue :: !Double
-  , obstructionPointX :: !Double
-  , obstructionPointY :: !Double
-  , obstructionMessage :: !String
-  }
-  deriving stock (Eq, Show, Generic)
-
-instance Storable CObstruction where
-  sizeOf _ = 320
-  alignment _ = alignment (0 :: Word64)
-  peek pointer = do
-    obstructionCode <- peekByteOff pointer 0
-    obstructionCoordinateError <- peekByteOff pointer 4
-    obstructionInputIndex <- peekByteOff pointer 8
-    obstructionFirstIndex <- peekByteOff pointer 16
-    obstructionSecondIndex <- peekByteOff pointer 24
-    obstructionFirstValue <- peekByteOff pointer 32
-    obstructionSecondValue <- peekByteOff pointer 40
-    obstructionPointX <- peekByteOff pointer 48
-    obstructionPointY <- peekByteOff pointer 56
-    obstructionMessage <- peekCString (castPtr pointer `plusPtr` 64)
-    pure CObstruction {..}
-  poke pointer CObstruction {..} = do
-    pokeByteOff pointer 0 obstructionCode
-    pokeByteOff pointer 4 obstructionCoordinateError
-    pokeByteOff pointer 8 obstructionInputIndex
-    pokeByteOff pointer 16 obstructionFirstIndex
-    pokeByteOff pointer 24 obstructionSecondIndex
-    pokeByteOff pointer 32 obstructionFirstValue
-    pokeByteOff pointer 40 obstructionSecondValue
-    pokeByteOff pointer 48 obstructionPointX
-    pokeByteOff pointer 56 obstructionPointY
-    let messagePointer = castPtr pointer `plusPtr` 64 :: Ptr CChar
-    fillBytes messagePointer 0 256
-    withCStringLen obstructionMessage $ \(source, lengthInBytes) ->
-      copyBytes messagePointer source (min 255 lengthInBytes)
-
-data CMinkowskiReceipt = CMinkowskiReceipt
-  { receiptOperation :: !Word32
-  , receiptInputComponents :: !Word64
-  , receiptConvexPieces :: !Word64
-  , receiptGeneratedPieces :: !Word64
-  , receiptGeneratedConvolutionEdges :: !Word64
-  , receiptOverlayPasses :: !Word64
-  , receiptExactCrossings :: !Word64
-  , receiptOutputCells :: !Word64
-  , receiptExactCoordinateBitGrowth :: !Word64
-  }
-  deriving stock (Eq, Show, Generic)
-
-instance Storable CMinkowskiReceipt where
-  sizeOf _ = 72
-  alignment _ = alignment (0 :: Word64)
-  peek pointer = do
-    receiptOperation <- peekByteOff pointer 0
-    receiptInputComponents <- peekByteOff pointer 8
-    receiptConvexPieces <- peekByteOff pointer 16
-    receiptGeneratedPieces <- peekByteOff pointer 24
-    receiptGeneratedConvolutionEdges <- peekByteOff pointer 32
-    receiptOverlayPasses <- peekByteOff pointer 40
-    receiptExactCrossings <- peekByteOff pointer 48
-    receiptOutputCells <- peekByteOff pointer 56
-    receiptExactCoordinateBitGrowth <- peekByteOff pointer 64
-    pure CMinkowskiReceipt {..}
-  poke pointer CMinkowskiReceipt {..} = do
-    pokeByteOff pointer 0 receiptOperation
-    pokeByteOff pointer 4 (0 :: Word32)
-    pokeByteOff pointer 8 receiptInputComponents
-    pokeByteOff pointer 16 receiptConvexPieces
-    pokeByteOff pointer 24 receiptGeneratedPieces
-    pokeByteOff pointer 32 receiptGeneratedConvolutionEdges
-    pokeByteOff pointer 40 receiptOverlayPasses
-    pokeByteOff pointer 48 receiptExactCrossings
-    pokeByteOff pointer 56 receiptOutputCells
-    pokeByteOff pointer 64 receiptExactCoordinateBitGrowth
-
-data AbiFieldType
-  = AbiFieldUInt32
-  | AbiFieldUInt64
-  | AbiFieldDouble
-  | AbiFieldCharArray !Int
-  deriving stock (Eq, Show, Generic)
-
-data AbiField = AbiField
-  { abiFieldName :: !String
-  , abiFieldType :: !AbiFieldType
-  }
-  deriving stock (Eq, Show, Generic)
-
-data AbiStruct
-  = AbiStructObstruction
-  | AbiStructMinkowskiReceipt
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allAbiStructs :: [AbiStruct]
-allAbiStructs = [minBound .. maxBound]
-
-abiStructSymbol :: AbiStruct -> String
-abiStructSymbol structure =
-  case structure of
-    AbiStructObstruction -> "ml_obstruction"
-    AbiStructMinkowskiReceipt -> "ml_minkowski_receipt"
-
-abiStructFields :: AbiStruct -> [AbiField]
-abiStructFields structure =
-  case structure of
-    AbiStructObstruction ->
-      [ AbiField "code" AbiFieldUInt32
-      , AbiField "coordinate_error" AbiFieldUInt32
-      , AbiField "input_index" AbiFieldUInt64
-      , AbiField "first_index" AbiFieldUInt64
-      , AbiField "second_index" AbiFieldUInt64
-      , AbiField "first_value" AbiFieldDouble
-      , AbiField "second_value" AbiFieldDouble
-      , AbiField "point_x" AbiFieldDouble
-      , AbiField "point_y" AbiFieldDouble
-      , AbiField "message" (AbiFieldCharArray 256)
-      ]
-    AbiStructMinkowskiReceipt ->
-      [ AbiField "operation" AbiFieldUInt32
-      , AbiField "reserved" AbiFieldUInt32
-      , AbiField "input_components" AbiFieldUInt64
-      , AbiField "convex_pieces" AbiFieldUInt64
-      , AbiField "generated_pieces" AbiFieldUInt64
-      , AbiField "generated_convolution_edges" AbiFieldUInt64
-      , AbiField "overlay_passes" AbiFieldUInt64
-      , AbiField "exact_crossings" AbiFieldUInt64
-      , AbiField "output_cells" AbiFieldUInt64
-      , AbiField "exact_coordinate_bit_growth" AbiFieldUInt64
-      ]
-
-data AbiHandle
-  = AbiMesh
-  | AbiRegion
-  | AbiStructuringElement
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-allAbiHandles :: [AbiHandle]
-allAbiHandles = [minBound .. maxBound]
-
-abiHandleSymbol :: AbiHandle -> String
-abiHandleSymbol handle =
-  case handle of
-    AbiMesh -> "ml_mesh"
-    AbiRegion -> "ml_region"
-    AbiStructuringElement -> "ml_structuring_element"
-
-data AbiParameterKind
-  = AbiValueSize
-  | AbiValueDouble
-  | AbiHandleInput !AbiHandle
-  | AbiHandleRelease !AbiHandle
-  | AbiHandleOutput !AbiHandle
-  | AbiBufferInputDouble
-  | AbiBufferInputSize
-  | AbiBufferOutputChar
-  | AbiBufferOutputDouble
-  | AbiBufferOutputUInt32
-  | AbiBufferOutputSize
-  | AbiScalarOutputRegionLocation
-  | AbiScalarOutputInt64
-  | AbiScalarOutputSize
-  | AbiScalarOutputDouble
-  | AbiStructOutput !AbiStruct
-  deriving stock (Eq, Show, Generic)
-
-data AbiParameter = AbiParameter
-  { abiParameterName :: !String
-  , abiParameterKind :: !AbiParameterKind
-  }
-  deriving stock (Eq, Show, Generic)
-
-data AbiResult
-  = AbiResultVoid
-  | AbiResultUInt32
-  | AbiResultStatus
-  deriving stock (Eq, Show, Generic)
-
-data AbiFunctionFamily
-  = AbiFunctionMesh
-  | AbiFunctionRegion
-  | AbiFunctionMorphology
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-data AbiFunctionImplementation
-  = AbiFunctionRuntime
-  | AbiFunctionHaskell !AbiFunctionFamily !String
-  deriving stock (Eq, Show, Generic)
-
-data AbiFunctionName
-  = AbiVersion
-  | RuntimeInitialize
-  | DelaunayF64
-  | MeshInsertManyF64
-  | MeshSiteUnion
-  | MeshSiteIntersection
-  | MeshSiteDifference
-  | MeshSiteSymmetricDifference
-  | MeshVertexCount
-  | MeshTriangleCount
-  | MeshCopyVerticesF64
-  | MeshCopyTrianglesU32
-  | MeshFree
-  | RegionCreateF64
-  | RegionCounts
-  | RegionCopyF64
-  | RegionUnion
-  | RegionIntersection
-  | RegionDifference
-  | RegionSymmetricDifference
-  | RegionLocatePointF64
-  | RegionMeasure
-  | RegionFree
-  | StructuringElementCreateF64
-  | StructuringElementFree
-  | RegionMinkowskiSum
-  | RegionOffset
-  | RegionInset
-  | RegionOpen
-  | RegionClose
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-
-data AbiFunction = AbiFunction
-  { abiFunctionImplementation :: !AbiFunctionImplementation
-  , abiFunctionSymbol :: !String
-  , abiFunctionResult :: !AbiResult
-  , abiFunctionParameters :: ![AbiParameter]
-  }
-  deriving stock (Eq, Show, Generic)
-
-allAbiFunctionNames :: [AbiFunctionName]
-allAbiFunctionNames = [minBound .. maxBound]
-
-allAbiFunctions :: [AbiFunction]
-allAbiFunctions = fmap abiFunction allAbiFunctionNames
-
-abiFunction :: AbiFunctionName -> AbiFunction
-abiFunction name =
-  case name of
-    AbiVersion -> runtime "ml_abi_version" AbiResultUInt32 []
-    RuntimeInitialize -> runtime "ml_runtime_initialize" AbiResultStatus []
-    DelaunayF64 -> mesh "ml_delaunay_f64" "delaunayF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
-    MeshInsertManyF64 -> mesh "ml_mesh_insert_many_f64" "meshInsertManyF64" AbiResultStatus [inputHandle "mesh" AbiMesh, inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiMesh, obstruction]
-    MeshSiteUnion -> binaryMesh "ml_mesh_site_union" "meshSiteUnion"
-    MeshSiteIntersection -> binaryMesh "ml_mesh_site_intersection" "meshSiteIntersection"
-    MeshSiteDifference -> binaryMesh "ml_mesh_site_difference" "meshSiteDifference"
-    MeshSiteSymmetricDifference -> binaryMesh "ml_mesh_site_symmetric_difference" "meshSiteSymmetricDifference"
-    MeshVertexCount -> meshCount "ml_mesh_vertex_count" "meshVertexCount"
-    MeshTriangleCount -> meshCount "ml_mesh_triangle_count" "meshTriangleCount"
-    MeshCopyVerticesF64 -> mesh "ml_mesh_copy_vertices_f64" "meshCopyVerticesF64" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "points_written" AbiScalarOutputSize, obstruction]
-    MeshCopyTrianglesU32 -> mesh "ml_mesh_copy_triangles_u32" "meshCopyTrianglesU32" AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "triangles" AbiBufferOutputUInt32, valueSize "triangle_capacity", AbiParameter "triangles_written" AbiScalarOutputSize, obstruction]
-    MeshFree -> mesh "ml_mesh_free" "meshFree" AbiResultVoid [releaseHandle "mesh" AbiMesh]
-    RegionCreateF64 -> region "ml_region_create_f64" "regionCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", AbiParameter "loop_point_counts" AbiBufferInputSize, valueSize "loop_count", AbiParameter "component_loop_counts" AbiBufferInputSize, valueSize "component_count", outputHandle "result" AbiRegion, obstruction]
-    RegionCounts -> region "ml_region_counts" "regionCounts" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "component_count" AbiScalarOutputSize, AbiParameter "loop_count" AbiScalarOutputSize, AbiParameter "point_count" AbiScalarOutputSize, obstruction]
-    RegionCopyF64 -> region "ml_region_copy_f64" "regionCopyF64" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "coordinates" AbiBufferOutputDouble, valueSize "point_capacity", AbiParameter "loop_point_offsets" AbiBufferOutputSize, valueSize "loop_offset_capacity", AbiParameter "component_loop_offsets" AbiBufferOutputSize, valueSize "component_offset_capacity", obstruction]
-    RegionUnion -> binaryRegion "ml_region_union" "regionUnion"
-    RegionIntersection -> binaryRegion "ml_region_intersection" "regionIntersection"
-    RegionDifference -> binaryRegion "ml_region_difference" "regionDifference"
-    RegionSymmetricDifference -> binaryRegion "ml_region_symmetric_difference" "regionSymmetricDifference"
-    RegionLocatePointF64 -> region "ml_region_locate_point_f64" "regionLocatePointF64" AbiResultStatus [inputHandle "region" AbiRegion, valueDouble "x", valueDouble "y", AbiParameter "location" AbiScalarOutputRegionLocation, obstruction]
-    RegionMeasure -> region "ml_region_measure" "regionMeasure" AbiResultStatus [inputHandle "region" AbiRegion, AbiParameter "euler_characteristic" AbiScalarOutputInt64, AbiParameter "area_ratio_utf8" AbiBufferOutputChar, valueSize "area_capacity", AbiParameter "area_bytes_written" AbiScalarOutputSize, AbiParameter "perimeter_lower" AbiScalarOutputDouble, AbiParameter "perimeter_upper" AbiScalarOutputDouble, obstruction]
-    RegionFree -> region "ml_region_free" "regionFree" AbiResultVoid [releaseHandle "region" AbiRegion]
-    StructuringElementCreateF64 -> morphology "ml_structuring_element_create_f64" "structuringElementCreateF64" AbiResultStatus [inputDoubles "coordinates", valueSize "point_count", outputHandle "result" AbiStructuringElement, obstruction]
-    StructuringElementFree -> morphology "ml_structuring_element_free" "structuringElementFree" AbiResultVoid [releaseHandle "element" AbiStructuringElement]
-    RegionMinkowskiSum -> morphologyOperation "ml_region_minkowski_sum" "regionMinkowskiSum" (inputHandle "left" AbiRegion) (inputHandle "right" AbiRegion)
-    RegionOffset -> morphologyOperation "ml_region_offset" "regionOffset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
-    RegionInset -> morphologyOperation "ml_region_inset" "regionInset" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
-    RegionOpen -> morphologyOperation "ml_region_open" "regionOpen" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
-    RegionClose -> morphologyOperation "ml_region_close" "regionClose" (inputHandle "element" AbiStructuringElement) (inputHandle "region" AbiRegion)
- where
-  runtime = AbiFunction AbiFunctionRuntime
-  implemented family symbol haskellName =
-    AbiFunction (AbiFunctionHaskell family haskellName) symbol
-  mesh = implemented AbiFunctionMesh
-  region = implemented AbiFunctionRegion
-  morphology = implemented AbiFunctionMorphology
-  binaryMesh symbol haskellName =
-    mesh symbol haskellName AbiResultStatus [inputHandle "left" AbiMesh, inputHandle "right" AbiMesh, outputHandle "result" AbiMesh, obstruction]
-  meshCount symbol haskellName =
-    mesh symbol haskellName AbiResultStatus [inputHandle "mesh" AbiMesh, AbiParameter "count" AbiScalarOutputSize, obstruction]
-  binaryRegion symbol haskellName =
-    region symbol haskellName AbiResultStatus [inputHandle "left" AbiRegion, inputHandle "right" AbiRegion, outputHandle "result" AbiRegion, obstruction]
-  morphologyOperation symbol haskellName first second =
-    morphology symbol haskellName AbiResultStatus [first, second, outputHandle "result" AbiRegion, AbiParameter "receipt" (AbiStructOutput AbiStructMinkowskiReceipt), obstruction]
-  valueSize parameterName = AbiParameter parameterName AbiValueSize
-  valueDouble parameterName = AbiParameter parameterName AbiValueDouble
-  inputHandle parameterName handle = AbiParameter parameterName (AbiHandleInput handle)
-  releaseHandle parameterName handle = AbiParameter parameterName (AbiHandleRelease handle)
-  outputHandle parameterName handle = AbiParameter parameterName (AbiHandleOutput handle)
-  inputDoubles parameterName = AbiParameter parameterName AbiBufferInputDouble
-  obstruction = AbiParameter "obstruction" (AbiStructOutput AbiStructObstruction)
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Contract/Render.hs
+++ /dev/null
@@ -1,763 +0,0 @@
-module Moonlight.Triangulation.Foreign.Contract.Render
-  ( GeneratedFile (..)
-  , generatedFiles
-  , renderCHeader
-  , renderDefinitionFile
-  , renderHaskellExports
-  , renderPythonRaw
-  , renderRustRaw
-  , renderTypeScriptWire
-  , renderTypeScriptRaw
-  ) where
-
-import Data.Char (toLower, toUpper)
-import Data.List (intercalate)
-import Data.Maybe (mapMaybe)
-import Data.Word (Word32)
-import Moonlight.Triangulation.Foreign.Contract
-
-data GeneratedFile = GeneratedFile
-  { generatedFilePath :: !FilePath
-  , generatedFileContents :: !String
-  }
-  deriving stock (Eq, Show)
-
-generatedFiles :: [GeneratedFile]
-generatedFiles =
-  [ GeneratedFile "ffi/include/moonlight_triangulation.h" renderCHeader
-  , GeneratedFile "ffi/cbits/moonlight-triangulation.def" renderDefinitionFile
-  , GeneratedFile "ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs" renderHaskellExports
-  , GeneratedFile "ffi/bindings/typescript/src/wire.generated.ts" renderTypeScriptWire
-  , GeneratedFile "ffi/bindings/typescript/src/internal/native.generated.ts" renderTypeScriptRaw
-  , GeneratedFile "ffi/bindings/python/src/moonlight_triangulation/_native_generated.py" renderPythonRaw
-  , GeneratedFile "ffi/bindings/rust/src/raw_generated.rs" renderRustRaw
-  ]
-
-generatedNotice :: String -> String
-generatedNotice comment =
-  comment
-    <> " Generated from Moonlight.Triangulation.Foreign.Contract.\n"
-    <> comment
-    <> " Run moonlight-triangulation-ffi-contract write; do not edit this file.\n\n"
-
-renderCHeader :: String
-renderCHeader =
-  generatedNotice "//"
-    <> unlines
-      [ "#ifndef MOONLIGHT_TRIANGULATION_H"
-      , "#define MOONLIGHT_TRIANGULATION_H"
-      , ""
-      , "#include <stddef.h>"
-      , "#include <stdint.h>"
-      , ""
-      , "#if defined(_WIN32)"
-      , "#define ML_API __declspec(dllexport)"
-      , "#else"
-      , "#define ML_API __attribute__((visibility(\"default\")))"
-      , "#endif"
-      , ""
-      , "#ifdef __cplusplus"
-      , "extern \"C\" {"
-      , "#endif"
-      , ""
-      , "#define ML_ABI_VERSION " <> show abiVersion <> "u"
-      , ""
-      , concatMap renderCOpaqueHandle allAbiHandles
-      , "typedef uint32_t ml_status;"
-      , "typedef uint32_t ml_obstruction_code;"
-      , "typedef uint32_t ml_coordinate_error;"
-      , "typedef uint32_t ml_region_location;"
-      , "typedef uint32_t ml_minkowski_operation;"
-      , ""
-      ]
-    <> renderCEnum (fmap (wireEntry abiStatusSymbol abiStatusId) allAbiStatuses)
-    <> renderCEnum (fmap (wireEntry obstructionCodeSymbol obstructionCodeId) allObstructionCodes)
-    <> renderCEnum (fmap (wireEntry coordinateErrorCodeSymbol coordinateErrorCodeId) allCoordinateErrorCodes)
-    <> renderCEnum (fmap (wireEntry regionLocationCodeSymbol regionLocationCodeId) allRegionLocationCodes)
-    <> renderCEnum (fmap (wireEntry minkowskiOperationCodeSymbol minkowskiOperationCodeId) allMinkowskiOperationCodes)
-    <> concatMap renderCStruct allAbiStructs
-    <> concatMap renderCFunction allAbiFunctions
-    <> unlines
-      [ ""
-      , "#ifdef __cplusplus"
-      , "}"
-      , "#endif"
-      , ""
-      , "#endif"
-      ]
-
-wireEntry :: (value -> String) -> (value -> Word32) -> value -> (String, Word32)
-wireEntry symbol identifier value = (symbol value, identifier value)
-
-renderCEnum :: [(String, Word32)] -> String
-renderCEnum entries =
-  "enum {\n"
-    <> intercalate ",\n" (fmap (\(symbol, identifier) -> "  " <> symbol <> " = " <> show identifier) entries)
-    <> "\n};\n\n"
-
-renderCOpaqueHandle :: AbiHandle -> String
-renderCOpaqueHandle handle =
-  "typedef struct " <> abiHandleSymbol handle <> " " <> abiHandleSymbol handle <> ";\n"
-
-renderCStruct :: AbiStruct -> String
-renderCStruct structure =
-  "typedef struct "
-    <> abiStructSymbol structure
-    <> " {\n"
-    <> concatMap renderField (abiStructFields structure)
-    <> "} "
-    <> abiStructSymbol structure
-    <> ";\n\n"
- where
-  renderField (AbiField name fieldType) =
-    "  " <> cFieldType fieldType <> " " <> name <> cFieldSuffix fieldType <> ";\n"
-
-renderCFunction :: AbiFunction -> String
-renderCFunction function =
-  "ML_API "
-    <> cResultType (abiFunctionResult function)
-    <> " "
-    <> abiFunctionSymbol function
-    <> "("
-    <> renderParameters
-    <> ");\n"
- where
-  parameters = abiFunctionParameters function
-  renderParameters =
-    if null parameters
-      then "void"
-      else intercalate ", " (fmap renderParameter parameters)
-  renderParameter (AbiParameter name kind) = cParameterType kind <> " " <> name
-
-cFieldType :: AbiFieldType -> String
-cFieldType fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "uint32_t"
-    AbiFieldUInt64 -> "uint64_t"
-    AbiFieldDouble -> "double"
-    AbiFieldCharArray _ -> "char"
-
-cFieldSuffix :: AbiFieldType -> String
-cFieldSuffix fieldType =
-  case fieldType of
-    AbiFieldCharArray lengthInBytes -> "[" <> show lengthInBytes <> "]"
-    _ -> ""
-
-cParameterType :: AbiParameterKind -> String
-cParameterType kind =
-  case kind of
-    AbiValueSize -> "size_t"
-    AbiValueDouble -> "double"
-    AbiHandleInput handle -> "const " <> abiHandleSymbol handle <> " *"
-    AbiHandleRelease handle -> abiHandleSymbol handle <> " *"
-    AbiHandleOutput handle -> abiHandleSymbol handle <> " **"
-    AbiBufferInputDouble -> "const double *"
-    AbiBufferInputSize -> "const size_t *"
-    AbiBufferOutputChar -> "char *"
-    AbiBufferOutputDouble -> "double *"
-    AbiBufferOutputUInt32 -> "uint32_t *"
-    AbiBufferOutputSize -> "size_t *"
-    AbiScalarOutputRegionLocation -> "ml_region_location *"
-    AbiScalarOutputInt64 -> "int64_t *"
-    AbiScalarOutputSize -> "size_t *"
-    AbiScalarOutputDouble -> "double *"
-    AbiStructOutput structure -> abiStructSymbol structure <> " *"
-
-cResultType :: AbiResult -> String
-cResultType result =
-  case result of
-    AbiResultVoid -> "void"
-    AbiResultUInt32 -> "uint32_t"
-    AbiResultStatus -> "ml_status"
-
-renderDefinitionFile :: String
-renderDefinitionFile =
-  generatedNotice ";"
-    <> "LIBRARY moonlight-triangulation-c\nEXPORTS\n"
-    <> concatMap (\function -> "  " <> abiFunctionSymbol function <> "\n") allAbiFunctions
-
-renderHaskellExports :: String
-renderHaskellExports =
-  generatedNotice "--"
-    <> unlines
-      [ "{-# LANGUAGE ForeignFunctionInterface #-}"
-      , "{-# OPTIONS_GHC -Wno-missing-signatures #-}"
-      , ""
-      , "module Moonlight.Triangulation.Foreign.Exports where"
-      , ""
-      , "import Data.Int (Int64)"
-      , "import Data.Word (Word32)"
-      , "import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))"
-      , "import Foreign.Ptr (Ptr)"
-      , "import Moonlight.Triangulation.Foreign.Contract"
-      , "  ( CMesh"
-      , "  , CMinkowskiReceipt"
-      , "  , CObstruction"
-      , "  , CRegion"
-      , "  , CStructuringElement"
-      , "  )"
-      , "import qualified Moonlight.Triangulation.Foreign.Mesh as Mesh"
-      , "import qualified Moonlight.Triangulation.Foreign.Morphology as Morphology"
-      , "import qualified Moonlight.Triangulation.Foreign.Region as Region"
-      , ""
-      ]
-    <> concatMap renderHaskellAlias implementedFunctions
-    <> "\n"
-    <> concatMap renderHaskellExport implementedFunctions
- where
-  implementedFunctions :: [(AbiFunction, String, AbiFunctionFamily)]
-  implementedFunctions = mapMaybe implemented allAbiFunctions
-
-  implemented :: AbiFunction -> Maybe (AbiFunction, String, AbiFunctionFamily)
-  implemented function =
-    case abiFunctionImplementation function of
-      AbiFunctionRuntime -> Nothing
-      AbiFunctionHaskell family haskellName -> Just (function, haskellName, family)
-
-  renderHaskellAlias :: (AbiFunction, String, AbiFunctionFamily) -> String
-  renderHaskellAlias (_, haskellName, family) =
-    haskellName <> " = " <> familyQualifier family <> "." <> haskellName <> "\n"
-
-  renderHaskellExport :: (AbiFunction, String, AbiFunctionFamily) -> String
-  renderHaskellExport (function, haskellName, _) =
-    "foreign export ccall \""
-      <> abiFunctionSymbol function
-      <> "\" "
-      <> haskellName
-      <> " :: "
-      <> haskellFunctionType function
-      <> "\n"
-
-familyQualifier :: AbiFunctionFamily -> String
-familyQualifier family =
-  case family of
-    AbiFunctionMesh -> "Mesh"
-    AbiFunctionRegion -> "Region"
-    AbiFunctionMorphology -> "Morphology"
-
-renderTypeScriptWire :: String
-renderTypeScriptWire =
-  generatedNotice "//"
-    <> intercalate
-      "\n"
-      [ renderTypeScriptVocabulary "AbiStatus" "ML_STATUS_" abiStatusSymbol abiStatusId allAbiStatuses
-      , renderTypeScriptVocabulary "ObstructionCode" "ML_OBSTRUCTION_" obstructionCodeSymbol obstructionCodeId allObstructionCodes
-      , renderTypeScriptVocabulary "CoordinateError" "ML_COORDINATE_ERROR_" coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
-      , renderTypeScriptVocabulary "RegionLocation" "ML_REGION_" regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
-      , renderTypeScriptVocabulary "MinkowskiOperation" "ML_MINKOWSKI_" minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
-      ]
-
-renderTypeScriptVocabulary
-  :: String
-  -> String
-  -> (value -> String)
-  -> (value -> Word32)
-  -> [value]
-  -> String
-renderTypeScriptVocabulary typeName prefix symbol identifier values =
-  "export const "
-    <> typeName
-    <> " = {\n"
-    <> concatMap renderValue values
-    <> "} as const;\n"
-    <> "export type "
-    <> typeName
-    <> " = (typeof "
-    <> typeName
-    <> ")[keyof typeof "
-    <> typeName
-    <> "];\n"
-    <> "const "
-    <> lowerInitial typeName
-    <> "ByWire: Readonly<Record<number, "
-    <> typeName
-    <> ">> = {\n"
-    <> concatMap renderWireEntry values
-    <> "};\n"
-    <> "export function decode"
-    <> typeName
-    <> "(value: number): "
-    <> typeName
-    <> " | undefined {\n"
-    <> "  return "
-    <> lowerInitial typeName
-    <> "ByWire[value];\n"
-    <> "}\n"
- where
-  renderValue value =
-    "  " <> wireKey prefix (symbol value) <> ": \"" <> wireValue prefix (symbol value) <> "\",\n"
-  renderWireEntry value =
-    "  " <> show (identifier value) <> ": " <> typeName <> "." <> wireKey prefix (symbol value) <> ",\n"
-
-wireKey :: String -> String -> String
-wireKey prefix = concatMap capitalize . underscoreWords . drop (length prefix)
-
-wireValue :: String -> String -> String
-wireValue prefix = intercalate "-" . fmap (fmap toLower) . underscoreWords . drop (length prefix)
-
-underscoreWords :: String -> [String]
-underscoreWords value =
-  case break (== '_') value of
-    (word, []) -> [word]
-    (word, _ : rest) -> word : underscoreWords rest
-
-capitalize :: String -> String
-capitalize value =
-  case value of
-    [] -> []
-    firstCharacter : rest -> toUpper firstCharacter : fmap toLower rest
-
-lowerInitial :: String -> String
-lowerInitial value =
-  case value of
-    [] -> []
-    firstCharacter : rest -> toLower firstCharacter : rest
-
-haskellFunctionType :: AbiFunction -> String
-haskellFunctionType function =
-  intercalate " -> " (fmap (haskellParameterType . abiParameterKind) (abiFunctionParameters function) <> [haskellResultType (abiFunctionResult function)])
-
-haskellParameterType :: AbiParameterKind -> String
-haskellParameterType kind =
-  case kind of
-    AbiValueSize -> "CSize"
-    AbiValueDouble -> "CDouble"
-    AbiHandleInput handle -> "Ptr " <> haskellHandleName handle
-    AbiHandleRelease handle -> "Ptr " <> haskellHandleName handle
-    AbiHandleOutput handle -> "Ptr (Ptr " <> haskellHandleName handle <> ")"
-    AbiBufferInputDouble -> "Ptr CDouble"
-    AbiBufferInputSize -> "Ptr CSize"
-    AbiBufferOutputChar -> "Ptr CChar"
-    AbiBufferOutputDouble -> "Ptr CDouble"
-    AbiBufferOutputUInt32 -> "Ptr Word32"
-    AbiBufferOutputSize -> "Ptr CSize"
-    AbiScalarOutputRegionLocation -> "Ptr CUInt"
-    AbiScalarOutputInt64 -> "Ptr Int64"
-    AbiScalarOutputSize -> "Ptr CSize"
-    AbiScalarOutputDouble -> "Ptr CDouble"
-    AbiStructOutput structure -> "Ptr " <> haskellStructName structure
-
-haskellResultType :: AbiResult -> String
-haskellResultType result =
-  case result of
-    AbiResultVoid -> "IO ()"
-    AbiResultUInt32 -> "IO CUInt"
-    AbiResultStatus -> "IO CUInt"
-
-haskellHandleName :: AbiHandle -> String
-haskellHandleName handle =
-  case handle of
-    AbiMesh -> "CMesh"
-    AbiRegion -> "CRegion"
-    AbiStructuringElement -> "CStructuringElement"
-
-haskellStructName :: AbiStruct -> String
-haskellStructName structure =
-  case structure of
-    AbiStructObstruction -> "CObstruction"
-    AbiStructMinkowskiReceipt -> "CMinkowskiReceipt"
-
-renderTypeScriptRaw :: String
-renderTypeScriptRaw =
-  generatedNotice "//"
-    <> "import koffi from \"koffi\";\n\n"
-    <> "export const ABI_VERSION = " <> show abiVersion <> ";\n"
-    <> renderTypeScriptConstants abiStatusSymbol abiStatusId allAbiStatuses
-    <> renderTypeScriptConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
-    <> renderTypeScriptConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
-    <> renderTypeScriptConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
-    <> renderTypeScriptConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
-    <> "\nexport type NativeHandle = object;\n"
-    <> "export type NativeInteger = number | bigint;\n"
-    <> "export type HandleOutput = Array<NativeHandle | null>;\n\n"
-    <> renderTypeScriptStruct "NativeObstruction" AbiStructObstruction
-    <> renderTypeScriptStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
-    <> "export interface NativeApi {\n"
-    <> concatMap renderTypeScriptFunctionType allAbiFunctions
-    <> "}\n\n"
-    <> unlines
-      [ "export function loadNativeApi(libraryPath: string): NativeApi {"
-      , "  const library = koffi.load(libraryPath);"
-      , concatMap renderTypeScriptKoffiHandleDeclarations allAbiHandles
-      , renderTypeScriptKoffiStruct "obstruction" AbiStructObstruction
-      , renderTypeScriptKoffiStruct "receipt" AbiStructMinkowskiReceipt
-      , "  const obstructionOutput = koffi.out(koffi.pointer(obstruction));"
-      , "  const receiptOutput = koffi.out(koffi.pointer(receipt));"
-      , "  const sizeOutput = koffi.out(koffi.pointer(\"size_t\"));"
-      , "  const uint32Output = koffi.out(koffi.pointer(\"uint32_t\"));"
-      , "  const int64Output = koffi.out(koffi.pointer(\"int64_t\"));"
-      , "  const doubleOutput = koffi.out(koffi.pointer(\"double\"));"
-      , "  const sizeArray = koffi.pointer(\"size_t\");"
-      , "  return {"
-      ]
-    <> concatMap renderTypeScriptFunctionBinding allAbiFunctions
-    <> "  };\n}\n"
-
-renderTypeScriptConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
-renderTypeScriptConstants symbol identifier =
-  concatMap (\value -> "export const " <> symbol value <> " = " <> show (identifier value) <> ";\n")
-
-renderTypeScriptStruct :: String -> AbiStruct -> String
-renderTypeScriptStruct name structure =
-  "export interface "
-    <> name
-    <> " {\n"
-    <> concatMap renderField (abiStructFields structure)
-    <> "}\n\n"
- where
-  renderField (AbiField fieldName fieldType) =
-    "  " <> fieldName <> "?: " <> typeScriptFieldType fieldType <> ";\n"
-
-typeScriptFieldType :: AbiFieldType -> String
-typeScriptFieldType fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "number"
-    AbiFieldUInt64 -> "NativeInteger"
-    AbiFieldDouble -> "number"
-    AbiFieldCharArray _ -> "string | readonly number[]"
-
-renderTypeScriptFunctionType :: AbiFunction -> String
-renderTypeScriptFunctionType function =
-  "  readonly "
-    <> abiFunctionSymbol function
-    <> ": ("
-    <> intercalate ", " (fmap renderParameter (abiFunctionParameters function))
-    <> ") => "
-    <> typeScriptResultType (abiFunctionResult function)
-    <> ";\n"
- where
-  renderParameter (AbiParameter name kind) = name <> ": " <> typeScriptParameterType kind
-
-typeScriptParameterType :: AbiParameterKind -> String
-typeScriptParameterType kind =
-  case kind of
-    AbiValueSize -> "number"
-    AbiValueDouble -> "number"
-    AbiHandleInput _ -> "NativeHandle"
-    AbiHandleRelease _ -> "NativeHandle"
-    AbiHandleOutput _ -> "HandleOutput"
-    AbiBufferInputDouble -> "Float64Array"
-    AbiBufferInputSize -> "BigUint64Array"
-    AbiBufferOutputChar -> "Buffer"
-    AbiBufferOutputDouble -> "Float64Array"
-    AbiBufferOutputUInt32 -> "Uint32Array"
-    AbiBufferOutputSize -> "BigUint64Array"
-    AbiScalarOutputRegionLocation -> "number[]"
-    AbiScalarOutputInt64 -> "NativeInteger[]"
-    AbiScalarOutputSize -> "NativeInteger[]"
-    AbiScalarOutputDouble -> "number[]"
-    AbiStructOutput AbiStructObstruction -> "NativeObstruction"
-    AbiStructOutput AbiStructMinkowskiReceipt -> "NativeMinkowskiReceipt"
-
-typeScriptResultType :: AbiResult -> String
-typeScriptResultType result =
-  case result of
-    AbiResultVoid -> "void"
-    AbiResultUInt32 -> "number"
-    AbiResultStatus -> "number"
-
-renderTypeScriptKoffiStruct :: String -> AbiStruct -> String
-renderTypeScriptKoffiStruct variableName structure =
-  "  const "
-    <> variableName
-    <> " = koffi.struct({\n"
-    <> concatMap renderField (abiStructFields structure)
-    <> "  });"
- where
-  renderField (AbiField name fieldType) =
-    "    " <> name <> ": " <> typeScriptKoffiFieldType fieldType <> ",\n"
-
-typeScriptKoffiFieldType :: AbiFieldType -> String
-typeScriptKoffiFieldType fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "\"uint32_t\""
-    AbiFieldUInt64 -> "\"uint64_t\""
-    AbiFieldDouble -> "\"double\""
-    AbiFieldCharArray lengthInBytes -> "koffi.array(\"char\", " <> show lengthInBytes <> ")"
-
-renderTypeScriptFunctionBinding :: AbiFunction -> String
-renderTypeScriptFunctionBinding function =
-  "    "
-    <> abiFunctionSymbol function
-    <> ": library.func(\""
-    <> abiFunctionSymbol function
-    <> "\", \""
-    <> typeScriptKoffiResultType (abiFunctionResult function)
-    <> "\", ["
-    <> intercalate ", " (fmap (typeScriptKoffiParameterType . abiParameterKind) (abiFunctionParameters function))
-    <> "]),\n"
-
-typeScriptKoffiResultType :: AbiResult -> String
-typeScriptKoffiResultType result =
-  case result of
-    AbiResultVoid -> "void"
-    AbiResultUInt32 -> "uint32_t"
-    AbiResultStatus -> "uint32_t"
-
-typeScriptKoffiParameterType :: AbiParameterKind -> String
-typeScriptKoffiParameterType kind =
-  case kind of
-    AbiValueSize -> "\"size_t\""
-    AbiValueDouble -> "\"double\""
-    AbiHandleInput handle -> typeScriptHandleVariable handle <> "Pointer"
-    AbiHandleRelease handle -> typeScriptHandleVariable handle <> "Pointer"
-    AbiHandleOutput handle -> typeScriptHandleVariable handle <> "Output"
-    AbiBufferInputDouble -> "koffi.pointer(\"double\")"
-    AbiBufferInputSize -> "sizeArray"
-    AbiBufferOutputChar -> "koffi.out(koffi.pointer(\"char\"))"
-    AbiBufferOutputDouble -> "koffi.out(koffi.pointer(\"double\"))"
-    AbiBufferOutputUInt32 -> "koffi.out(koffi.pointer(\"uint32_t\"))"
-    AbiBufferOutputSize -> "koffi.out(sizeArray)"
-    AbiScalarOutputRegionLocation -> "uint32Output"
-    AbiScalarOutputInt64 -> "int64Output"
-    AbiScalarOutputSize -> "sizeOutput"
-    AbiScalarOutputDouble -> "doubleOutput"
-    AbiStructOutput AbiStructObstruction -> "obstructionOutput"
-    AbiStructOutput AbiStructMinkowskiReceipt -> "receiptOutput"
-
-typeScriptHandleVariable :: AbiHandle -> String
-typeScriptHandleVariable handle =
-  case handle of
-    AbiMesh -> "mesh"
-    AbiRegion -> "region"
-    AbiStructuringElement -> "structuringElement"
-
-renderTypeScriptKoffiHandleDeclarations :: AbiHandle -> String
-renderTypeScriptKoffiHandleDeclarations handle =
-  let variableName = typeScriptHandleVariable handle
-   in unlines
-        [ "  const " <> variableName <> " = koffi.opaque();"
-        , "  const " <> variableName <> "Pointer = koffi.pointer(" <> variableName <> ");"
-        , "  const " <> variableName <> "Output = koffi.out(koffi.pointer(" <> variableName <> ", 2));"
-        ]
-
-renderPythonRaw :: String
-renderPythonRaw =
-  generatedNotice "#"
-    <> unlines
-      [ "from __future__ import annotations"
-      , ""
-      , "import ctypes"
-      , "from collections.abc import Sequence"
-      , "from pathlib import Path"
-      , "from typing import Final"
-      , ""
-      , "ABI_VERSION: Final = " <> show abiVersion
-      ]
-    <> renderPythonConstants abiStatusSymbol abiStatusId allAbiStatuses
-    <> renderPythonConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
-    <> renderPythonConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
-    <> renderPythonConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
-    <> renderPythonConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
-    <> "\n"
-    <> renderPythonStruct "_Obstruction" AbiStructObstruction
-    <> renderPythonStruct "_NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
-    <> unlines
-      [ "class _NativeApi:"
-      , "    def __init__(self, library_path: Path) -> None:"
-      , "        library = ctypes.CDLL(str(library_path))"
-      ]
-    <> concatMap renderPythonConfiguration allAbiFunctions
-    <> unlines
-      [ "        self.library = library"
-      , ""
-      , ""
-      , "def _configure("
-      , "    library: ctypes.CDLL,"
-      , "    name: str,"
-      , "    parameters: Sequence[object],"
-      , "    result: object = ctypes.c_uint32,"
-      , ") -> None:"
-      , "    function = getattr(library, name)"
-      , "    setattr(function, \"argtypes\", list(parameters))"
-      , "    setattr(function, \"restype\", result)"
-      ]
-
-renderPythonConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
-renderPythonConstants symbol identifier =
-  concatMap (\value -> symbol value <> ": Final = " <> show (identifier value) <> "\n")
-
-renderPythonStruct :: String -> AbiStruct -> String
-renderPythonStruct name structure =
-  "class "
-    <> name
-    <> "(ctypes.Structure):\n"
-    <> "    _fields_ = [\n"
-    <> concatMap renderField (abiStructFields structure)
-    <> "    ]\n\n\n"
- where
-  renderField (AbiField fieldName fieldType) =
-    "        (\"" <> fieldName <> "\", " <> pythonFieldType fieldType <> "),\n"
-
-pythonFieldType :: AbiFieldType -> String
-pythonFieldType fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "ctypes.c_uint32"
-    AbiFieldUInt64 -> "ctypes.c_uint64"
-    AbiFieldDouble -> "ctypes.c_double"
-    AbiFieldCharArray lengthInBytes -> "ctypes.c_char * " <> show lengthInBytes
-
-renderPythonConfiguration :: AbiFunction -> String
-renderPythonConfiguration function =
-  "        _configure(library, \""
-    <> abiFunctionSymbol function
-    <> "\", "
-    <> pythonTuple (fmap (pythonParameterType . abiParameterKind) (abiFunctionParameters function))
-    <> ", "
-    <> pythonResultType (abiFunctionResult function)
-    <> ")\n"
-
-pythonTuple :: [String] -> String
-pythonTuple values =
-  case values of
-    [] -> "()"
-    [value] -> "(" <> value <> ",)"
-    _ -> "(" <> intercalate ", " values <> ")"
-
-pythonParameterType :: AbiParameterKind -> String
-pythonParameterType kind =
-  case kind of
-    AbiValueSize -> "ctypes.c_size_t"
-    AbiValueDouble -> "ctypes.c_double"
-    AbiHandleInput _ -> "ctypes.c_void_p"
-    AbiHandleRelease _ -> "ctypes.c_void_p"
-    AbiHandleOutput _ -> "ctypes.POINTER(ctypes.c_void_p)"
-    AbiBufferInputDouble -> "ctypes.POINTER(ctypes.c_double)"
-    AbiBufferInputSize -> "ctypes.POINTER(ctypes.c_size_t)"
-    AbiBufferOutputChar -> "ctypes.POINTER(ctypes.c_char)"
-    AbiBufferOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
-    AbiBufferOutputUInt32 -> "ctypes.POINTER(ctypes.c_uint32)"
-    AbiBufferOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
-    AbiScalarOutputRegionLocation -> "ctypes.POINTER(ctypes.c_uint32)"
-    AbiScalarOutputInt64 -> "ctypes.POINTER(ctypes.c_int64)"
-    AbiScalarOutputSize -> "ctypes.POINTER(ctypes.c_size_t)"
-    AbiScalarOutputDouble -> "ctypes.POINTER(ctypes.c_double)"
-    AbiStructOutput AbiStructObstruction -> "ctypes.POINTER(_Obstruction)"
-    AbiStructOutput AbiStructMinkowskiReceipt -> "ctypes.POINTER(_NativeMinkowskiReceipt)"
-
-pythonResultType :: AbiResult -> String
-pythonResultType result =
-  case result of
-    AbiResultVoid -> "None"
-    AbiResultUInt32 -> "ctypes.c_uint32"
-    AbiResultStatus -> "ctypes.c_uint32"
-
-renderRustRaw :: String
-renderRustRaw =
-  generatedNotice "//"
-    <> "// This private module is the complete wire projection; wrappers consume a subset.\n"
-    <> "#![allow(dead_code)]\n\n"
-    <> "use std::ffi::{c_char, c_double, c_uint};\n\n"
-    <> "pub(crate) const ABI_VERSION: u32 = " <> show abiVersion <> ";\n"
-    <> renderRustConstants abiStatusSymbol abiStatusId allAbiStatuses
-    <> renderRustConstants obstructionCodeSymbol obstructionCodeId allObstructionCodes
-    <> renderRustConstants coordinateErrorCodeSymbol coordinateErrorCodeId allCoordinateErrorCodes
-    <> renderRustConstants regionLocationCodeSymbol regionLocationCodeId allRegionLocationCodes
-    <> renderRustConstants minkowskiOperationCodeSymbol minkowskiOperationCodeId allMinkowskiOperationCodes
-    <> "\n"
-    <> concatMap renderRustHandle allAbiHandles
-    <> renderRustStruct "NativeObstruction" AbiStructObstruction
-    <> renderRustStruct "NativeMinkowskiReceipt" AbiStructMinkowskiReceipt
-    <> "#[link(name = \"moonlight-triangulation-c\")]\nunsafe extern \"C\" {\n"
-    <> concatMap renderRustFunction allAbiFunctions
-    <> "}\n"
-
-renderRustConstants :: (value -> String) -> (value -> Word32) -> [value] -> String
-renderRustConstants symbol identifier =
-  concatMap (\value -> "pub(crate) const " <> symbol value <> ": u32 = " <> show (identifier value) <> ";\n")
-
-renderRustHandle :: AbiHandle -> String
-renderRustHandle handle =
-  "#[repr(C)]\npub(crate) struct "
-    <> rustHandleName handle
-    <> " {\n    _private: [u8; 0],\n}\n\n"
-
-renderRustStruct :: String -> AbiStruct -> String
-renderRustStruct name structure =
-  "#[repr(C)]\npub(crate) struct "
-    <> name
-    <> " {\n"
-    <> concatMap renderField (abiStructFields structure)
-    <> "}\n\n"
-    <> "impl Default for "
-    <> name
-    <> " {\n"
-    <> "    fn default() -> Self {\n"
-    <> "        Self {\n"
-    <> concatMap renderDefaultField (abiStructFields structure)
-    <> "        }\n"
-    <> "    }\n"
-    <> "}\n\n"
- where
-  renderField (AbiField fieldName fieldType) =
-    "    pub(crate) " <> fieldName <> ": " <> rustFieldType fieldType <> ",\n"
-  renderDefaultField (AbiField fieldName fieldType) =
-    "            " <> fieldName <> ": " <> rustFieldDefault fieldType <> ",\n"
-
-rustFieldType :: AbiFieldType -> String
-rustFieldType fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "u32"
-    AbiFieldUInt64 -> "u64"
-    AbiFieldDouble -> "f64"
-    AbiFieldCharArray lengthInBytes -> "[c_char; " <> show lengthInBytes <> "]"
-
-rustFieldDefault :: AbiFieldType -> String
-rustFieldDefault fieldType =
-  case fieldType of
-    AbiFieldUInt32 -> "0"
-    AbiFieldUInt64 -> "0"
-    AbiFieldDouble -> "0.0"
-    AbiFieldCharArray lengthInBytes -> "[0; " <> show lengthInBytes <> "]"
-
-renderRustFunction :: AbiFunction -> String
-renderRustFunction function =
-  case abiFunctionParameters function of
-    [] -> declarationPrefix <> "()" <> resultSuffix <> ";\n"
-    [parameter] -> declarationPrefix <> "(" <> renderParameter parameter <> ")" <> resultSuffix <> ";\n"
-    parameters ->
-      declarationPrefix
-        <> "(\n"
-        <> concatMap (\parameter -> "        " <> renderParameter parameter <> ",\n") parameters
-        <> "    )"
-        <> resultSuffix
-        <> ";\n"
- where
-  declarationPrefix = "    pub(crate) fn " <> abiFunctionSymbol function
-  resultSuffix = rustResultSuffix (abiFunctionResult function)
-  renderParameter (AbiParameter name kind) = name <> ": " <> rustParameterType kind
-
-rustParameterType :: AbiParameterKind -> String
-rustParameterType kind =
-  case kind of
-    AbiValueSize -> "usize"
-    AbiValueDouble -> "c_double"
-    AbiHandleInput handle -> "*const " <> rustHandleName handle
-    AbiHandleRelease handle -> "*mut " <> rustHandleName handle
-    AbiHandleOutput handle -> "*mut *mut " <> rustHandleName handle
-    AbiBufferInputDouble -> "*const c_double"
-    AbiBufferInputSize -> "*const usize"
-    AbiBufferOutputChar -> "*mut c_char"
-    AbiBufferOutputDouble -> "*mut c_double"
-    AbiBufferOutputUInt32 -> "*mut u32"
-    AbiBufferOutputSize -> "*mut usize"
-    AbiScalarOutputRegionLocation -> "*mut u32"
-    AbiScalarOutputInt64 -> "*mut i64"
-    AbiScalarOutputSize -> "*mut usize"
-    AbiScalarOutputDouble -> "*mut c_double"
-    AbiStructOutput AbiStructObstruction -> "*mut NativeObstruction"
-    AbiStructOutput AbiStructMinkowskiReceipt -> "*mut NativeMinkowskiReceipt"
-
-rustResultSuffix :: AbiResult -> String
-rustResultSuffix result =
-  case result of
-    AbiResultVoid -> ""
-    AbiResultUInt32 -> " -> c_uint"
-    AbiResultStatus -> " -> c_uint"
-
-rustHandleName :: AbiHandle -> String
-rustHandleName handle =
-  case handle of
-    AbiMesh -> "NativeMesh"
-    AbiRegion -> "NativeRegion"
-    AbiStructuringElement -> "NativeStructuringElement"
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Mesh.hs
+++ /dev/null
@@ -1,178 +0,0 @@
-module Moonlight.Triangulation.Foreign.Mesh
-  ( delaunayF64
-  , insertGeometryBatch
-  , meshCopyTrianglesU32
-  , meshCopyVerticesF64
-  , meshFree
-  , meshInsertManyF64
-  , meshSiteDifference
-  , meshSiteIntersection
-  , meshSiteSymmetricDifference
-  , meshSiteUnion
-  , meshTriangleCount
-  , meshVertexCount
-  ) where
-
-import Control.Monad (void)
-import Data.Bifunctor (first)
-import Data.Word (Word32)
-import Foreign.C.Types (CDouble (..), CSize, CUInt)
-import Foreign.Ptr (Ptr)
-import Foreign.Storable (poke, pokeElemOff)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation as T
-import Moonlight.Triangulation.Foreign.Boundary
-  ( GeometryMesh
-  , HandleValue
-  , checkedCount
-  , dereferenceHandle
-  , freeHandle
-  , produceHandle
-  , readPoints
-  , requireOutputCapacity
-  , requirePointer
-  , runBoundary
-  )
-import Moonlight.Triangulation.Foreign.Contract (CMesh, CObstruction)
-import Moonlight.Triangulation.Foreign.Obstruction (buildFailure)
-import Moonlight.Triangulation.Math (validatePoint)
-import qualified Moonlight.Triangulation.Internal.Session as Session
-
-delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-delaunayF64 coordinates rawCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case checkedCount 2 rawCount of
-      Left failure -> pure (Left failure)
-      Right count -> do
-        points <- readPoints coordinates count
-        pure (points >>= first buildFailure . T.delaunayGeometry)
-
-meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-meshInsertManyF64 meshPointer coordinates rawCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "mesh" meshPointer >> checkedCount 2 rawCount of
-      Left failure -> pure (Left failure)
-      Right count -> do
-        points <- readPoints coordinates count
-        case points of
-          Left failure -> pure (Left failure)
-          Right admitted -> do
-            mesh <- dereferenceHandle meshPointer
-            pure (first buildFailure (insertGeometryBatch mesh admitted))
-
--- | Admit the complete batch before opening the immutable insertion session.
-insertGeometryBatch :: GeometryMesh -> V.Vector T.Point -> Either T.BuildError GeometryMesh
-insertGeometryBatch mesh points = do
-  admittedPoints <- V.imapM (\index point -> validatePoint (Just index) point) points
-  (_, revised, _) <-
-    Session.withSession
-      mesh
-      (V.length points)
-      ( V.mapM_
-          ( \point ->
-              void
-                ( Session.insertAdmittedVertexAtCombining
-                    (\_ replacement -> replacement)
-                    point
-                    ()
-                )
-          )
-          admittedPoints
-      )
-  pure revised
-
-meshSiteUnion, meshSiteIntersection, meshSiteDifference, meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-meshSiteUnion = binaryMeshOperation T.union
-meshSiteIntersection = binaryMeshOperation T.intersection
-meshSiteDifference = binaryMeshOperation T.difference
-meshSiteSymmetricDifference = binaryMeshOperation T.symmetricDifference
-
-binaryMeshOperation
-  :: (GeometryMesh -> GeometryMesh -> Either T.BuildError GeometryMesh)
-  -> Ptr CMesh
-  -> Ptr CMesh
-  -> Ptr (Ptr CMesh)
-  -> Ptr CObstruction
-  -> IO CUInt
-binaryMeshOperation operation leftPointer rightPointer output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "left mesh" leftPointer >> requirePointer "right mesh" rightPointer of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        left <- dereferenceHandle leftPointer
-        right <- dereferenceHandle rightPointer
-        pure (first buildFailure (operation left right))
-
-meshVertexCount, meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshVertexCount = handleCount "mesh" T.numVertices
-meshTriangleCount = handleCount "mesh" (V.length . T.innerFaceVertexTriples)
-
-handleCount
-  :: String
-  -> (HandleValue carrier -> Int)
-  -> Ptr carrier
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-handleCount handleLabel observe handlePointer output obstructionPointer =
-  runBoundary obstructionPointer $
-    case requirePointer handleLabel handlePointer >> requirePointer "count" output of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        value <- dereferenceHandle handlePointer
-        poke output (fromIntegral (observe value))
-        pure (Right ())
-
-meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshCopyVerticesF64 =
-  copyHandleProjection
-    "mesh"
-    "points_written"
-    "coordinates"
-    T.vertexPoints
-    ( \output index (T.Point x y) -> do
-        pokeElemOff output (index * 2) (CDouble x)
-        pokeElemOff output (index * 2 + 1) (CDouble y)
-    )
-
-meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-meshCopyTrianglesU32 =
-  copyHandleProjection
-    "mesh"
-    "triangles_written"
-    "triangles"
-    T.innerFaceVertexTriples
-    ( \output index (firstVertex, secondVertex, thirdVertex) -> do
-        pokeElemOff output (index * 3) (T.unVertexId firstVertex)
-        pokeElemOff output (index * 3 + 1) (T.unVertexId secondVertex)
-        pokeElemOff output (index * 3 + 2) (T.unVertexId thirdVertex)
-    )
-
-copyHandleProjection
-  :: String
-  -> String
-  -> String
-  -> (HandleValue carrier -> V.Vector item)
-  -> (Ptr element -> Int -> item -> IO ())
-  -> Ptr carrier
-  -> Ptr element
-  -> CSize
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-{-# INLINE copyHandleProjection #-}
-copyHandleProjection handleLabel writtenLabel outputLabel project writeItem handlePointer output rawCapacity written obstructionPointer =
-  runBoundary obstructionPointer $
-    case requirePointer handleLabel handlePointer >> requirePointer writtenLabel written >> checkedCount 1 rawCapacity of
-      Left failure -> pure (Left failure)
-      Right capacity -> do
-        value <- dereferenceHandle handlePointer
-        let items = project value
-            required = V.length items
-        poke written (fromIntegral required)
-        case requireOutputCapacity outputLabel output required capacity of
-          Left failure -> pure (Left failure)
-          Right () -> V.imapM_ (writeItem output) items >> pure (Right ())
-
-meshFree :: Ptr CMesh -> IO ()
-meshFree = freeHandle
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Morphology.hs
+++ /dev/null
@@ -1,160 +0,0 @@
-module Moonlight.Triangulation.Foreign.Morphology
-  ( regionClose
-  , regionInset
-  , regionMinkowskiSum
-  , regionOffset
-  , regionOpen
-  , structuringElementCreateF64
-  , structuringElementFree
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Word (Word32)
-import Foreign.C.Types (CDouble, CSize, CUInt)
-import Foreign.Ptr (Ptr)
-import Foreign.Storable (poke)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation as T
-import Moonlight.Triangulation.Foreign.Boundary
-  ( checkedCount
-  , dereferenceHandle
-  , freeHandle
-  , prepareHandleOutput
-  , produceHandle
-  , publishHandle
-  , readPoints
-  , requirePointer
-  , runBoundary
-  )
-import Moonlight.Triangulation.Foreign.Contract
-  ( CMinkowskiReceipt (..)
-  , CObstruction
-  , CRegion
-  , CStructuringElement
-  , MinkowskiOperationCode (..)
-  , minkowskiOperationCodeId
-  )
-import Moonlight.Triangulation.Foreign.Obstruction
-  ( AbiFailure
-  , minkowskiFailure
-  , pointInputFailure
-  , structuringElementEmptyFailure
-  )
-
-structuringElementCreateF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr (Ptr CStructuringElement)
-  -> Ptr CObstruction
-  -> IO CUInt
-structuringElementCreateF64 coordinates rawPointCount output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case checkedCount 2 rawPointCount of
-      Left failure -> pure (Left failure)
-      Right pointCount -> do
-        points <- readPoints coordinates pointCount
-        pure (points >>= buildStructuringElement)
-
-buildStructuringElement
-  :: V.Vector T.Point
-  -> Either AbiFailure T.StructuringElement
-buildStructuringElement points = do
-  exactPoints <-
-    V.imapM
-      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
-      points
-  submitted <-
-    maybe
-      (Left structuringElementEmptyFailure)
-      Right
-      (NonEmpty.nonEmpty (V.toList exactPoints))
-  polygon <- first minkowskiFailure (T.convexPolygon submitted)
-  first minkowskiFailure (T.structuringElement polygon)
-
-structuringElementFree :: Ptr CStructuringElement -> IO ()
-structuringElementFree = freeHandle
-
-regionMinkowskiSum
-  :: Ptr CRegion
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> Ptr CObstruction
-  -> IO CUInt
-regionMinkowskiSum leftPointer rightPointer output receiptOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    produceRegionWithReceipt output receiptOutput $ do
-      case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          left <- dereferenceHandle leftPointer
-          right <- dereferenceHandle rightPointer
-          pure (first minkowskiFailure (T.minkowskiSum left right))
-
-regionOffset, regionInset, regionOpen, regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-regionOffset = structuringElementOperation T.polygonOffset
-regionInset = structuringElementOperation T.polygonInset
-regionOpen = structuringElementOperation T.openWith
-regionClose = structuringElementOperation T.closeWith
-
-structuringElementOperation
-  :: (T.StructuringElement -> T.PlanarRegion -> Either T.MinkowskiError (T.PlanarRegion, T.MinkowskiReceipt))
-  -> Ptr CStructuringElement
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> Ptr CObstruction
-  -> IO CUInt
-structuringElementOperation operation elementPointer regionPointer output receiptOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    produceRegionWithReceipt output receiptOutput $ do
-      case requirePointer "structuring element" elementPointer >> requirePointer "region" regionPointer of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          element <- dereferenceHandle elementPointer
-          region <- dereferenceHandle regionPointer
-          pure (first minkowskiFailure (operation element region))
-
-produceRegionWithReceipt
-  :: Ptr (Ptr CRegion)
-  -> Ptr CMinkowskiReceipt
-  -> IO (Either AbiFailure (T.PlanarRegion, T.MinkowskiReceipt))
-  -> IO (Either AbiFailure ())
-produceRegionWithReceipt output receiptOutput obtain = do
-  prepared <- prepareHandleOutput output
-  case (prepared, requirePointer "receipt" receiptOutput) of
-    (Left failure, _) -> pure (Left failure)
-    (_, Left failure) -> pure (Left failure)
-    (Right (), Right ()) -> do
-      outcome <- obtain
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right (region, receipt) -> do
-          poke receiptOutput (minkowskiReceiptProjection receipt)
-          publishHandle output region
-          pure (Right ())
-
-minkowskiReceiptProjection :: T.MinkowskiReceipt -> CMinkowskiReceipt
-minkowskiReceiptProjection receipt =
-  CMinkowskiReceipt
-    { receiptOperation = minkowskiOperationCode (T.minkowskiOperation receipt)
-    , receiptInputComponents = fromIntegral (T.minkowskiInputComponents receipt)
-    , receiptConvexPieces = fromIntegral (T.minkowskiConvexPieces receipt)
-    , receiptGeneratedPieces = fromIntegral (T.minkowskiGeneratedPieces receipt)
-    , receiptGeneratedConvolutionEdges = fromIntegral (T.minkowskiGeneratedConvolutionEdges receipt)
-    , receiptOverlayPasses = fromIntegral (T.minkowskiOverlayPasses receipt)
-    , receiptExactCrossings = fromIntegral (T.minkowskiExactCrossings receipt)
-    , receiptOutputCells = fromIntegral (T.minkowskiOutputCells receipt)
-    , receiptExactCoordinateBitGrowth = fromIntegral (T.minkowskiExactCoordinateBitGrowth receipt)
-    }
-
-minkowskiOperationCode :: T.MinkowskiOperation -> Word32
-minkowskiOperationCode operation =
-  minkowskiOperationCodeId
-    ( case operation of
-        T.MinkowskiAddition -> MinkowskiOperationAddition
-        T.MinkowskiErosion -> MinkowskiOperationErosion
-        T.MinkowskiOpening -> MinkowskiOperationOpening
-        T.MinkowskiClosing -> MinkowskiOperationClosing
-    )
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Obstruction.hs
+++ /dev/null
@@ -1,313 +0,0 @@
-module Moonlight.Triangulation.Foreign.Obstruction
-  ( AbiFailure (..)
-  , RegionCountKind (..)
-  , RegionLayoutError (..)
-  , bufferTooSmallFailure
-  , buildFailure
-  , countOverflowFailure
-  , emptyObstruction
-  , minkowskiFailure
-  , nullPointerFailure
-  , overlayFailure
-  , pointInputFailure
-  , projectionFailure
-  , regionLayoutFailure
-  , regionPublicationFailure
-  , regionValidationFailure
-  , runtimeFailure
-  , statusOk
-  , structuringElementEmptyFailure
-  , valuationFailure
-  ) where
-
-import Control.Exception (SomeException, displayException)
-import Data.Word (Word32, Word64)
-import Foreign.C.Types (CUInt)
-import Moonlight.Triangulation.Foreign.Contract
-import qualified Moonlight.Triangulation as T
-
-data AbiFailure = AbiFailure !CUInt !CObstruction
-
-statusCode :: AbiStatus -> CUInt
-statusCode = fromIntegral . abiStatusId
-
-statusOk :: CUInt
-statusOk = statusCode AbiStatusOk
-
-emptyObstruction :: CObstruction
-emptyObstruction =
-  CObstruction
-    { obstructionCode = obstructionCodeId ObstructionNone
-    , obstructionCoordinateError = coordinateErrorCodeId CoordinateErrorNone
-    , obstructionInputIndex = maxBound
-    , obstructionFirstIndex = 0
-    , obstructionSecondIndex = 0
-    , obstructionFirstValue = 0
-    , obstructionSecondValue = 0
-    , obstructionPointX = 0
-    , obstructionPointY = 0
-    , obstructionMessage = ""
-    }
-
-apiFailure :: AbiStatus -> ObstructionCode -> String -> AbiFailure
-apiFailure status code message =
-  AbiFailure
-    (statusCode status)
-    emptyObstruction
-      { obstructionCode = obstructionCodeId code
-      , obstructionMessage = message
-      }
-
-geometryFailure :: Show obstruction => ObstructionCode -> obstruction -> AbiFailure
-geometryFailure code obstruction =
-  apiFailure AbiStatusGeometryObstruction code (show obstruction)
-
-nullPointerFailure :: String -> AbiFailure
-nullPointerFailure label =
-  apiFailure AbiStatusNullPointer ObstructionNullPointer (label <> " must not be null")
-
-countOverflowFailure :: Word64 -> AbiFailure
-countOverflowFailure count =
-  AbiFailure
-    (statusCode AbiStatusCountOverflow)
-    emptyObstruction
-      { obstructionCode = obstructionCodeId ObstructionCountOverflow
-      , obstructionFirstIndex = count
-      , obstructionMessage = "count exceeds the host Int range"
-      }
-
-bufferTooSmallFailure :: Int -> Int -> AbiFailure
-bufferTooSmallFailure required capacity =
-  AbiFailure
-    (statusCode AbiStatusBufferTooSmall)
-    emptyObstruction
-      { obstructionCode = obstructionCodeId ObstructionBufferTooSmall
-      , obstructionFirstIndex = fromIntegral required
-      , obstructionSecondIndex = fromIntegral capacity
-      , obstructionMessage = "output buffer is smaller than the required element count"
-      }
-
-runtimeFailure :: SomeException -> AbiFailure
-runtimeFailure = apiFailure AbiStatusRuntimeFailure ObstructionRuntimeFailure . displayException
-
-buildFailure :: T.BuildError -> AbiFailure
-buildFailure obstruction =
-  AbiFailure (statusCode AbiStatusGeometryObstruction) (buildErrorObstruction obstruction)
-
-data RegionCountKind
-  = LoopPointCounts
-  | ComponentLoopCounts
-  deriving stock (Eq, Show)
-
-data RegionLayoutError
-  = RegionGroupEmpty !RegionCountKind !Int
-  | RegionCountTotalMismatch !RegionCountKind !Integer !Integer
-  deriving stock (Eq, Show)
-
-regionLayoutFailure :: RegionLayoutError -> AbiFailure
-regionLayoutFailure layoutError =
-  AbiFailure (statusCode AbiStatusGeometryObstruction) (layoutObstruction layoutError)
-
-layoutObstruction :: RegionLayoutError -> CObstruction
-layoutObstruction layoutError =
-  ( case layoutError of
-      RegionCountTotalMismatch _ actual expected ->
-        base
-          { obstructionFirstIndex = fromIntegral actual
-          , obstructionSecondIndex = fromIntegral expected
-          }
-      RegionGroupEmpty _ index -> base {obstructionInputIndex = fromIntegral index}
-  )
-    {obstructionMessage = show layoutError}
- where
-  base = emptyObstruction {obstructionCode = obstructionCodeId ObstructionRegionLayoutInvalid}
-
-regionValidationFailure :: T.RegionValidationError -> AbiFailure
-regionValidationFailure = geometryFailure ObstructionRegionValidationFailed
-
-overlayFailure :: T.OverlayError Bool Bool -> AbiFailure
-overlayFailure = geometryFailure ObstructionOverlayFailed
-
-regionPublicationFailure :: T.RegionPublicationError -> AbiFailure
-regionPublicationFailure = geometryFailure ObstructionRegionPublicationFailed
-
-valuationFailure :: T.ValuationError -> AbiFailure
-valuationFailure = geometryFailure ObstructionValuationFailed
-
-minkowskiFailure :: T.MinkowskiError -> AbiFailure
-minkowskiFailure = geometryFailure ObstructionMinkowskiFailed
-
-structuringElementEmptyFailure :: AbiFailure
-structuringElementEmptyFailure =
-  apiFailure
-    AbiStatusGeometryObstruction
-    ObstructionRegionLayoutInvalid
-    "structuring element requires at least one point"
-
-pointInputFailure :: Int -> T.Point -> T.PointValidationError -> AbiFailure
-pointInputFailure index (T.Point x y) pointError =
-  AbiFailure
-    (statusCode AbiStatusGeometryObstruction)
-    emptyObstruction
-      { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
-      , obstructionCoordinateError = coordinateErrorCode reason
-      , obstructionInputIndex = fromIntegral index
-      , obstructionFirstValue = invalidValue
-      , obstructionPointX = x
-      , obstructionPointY = y
-      , obstructionMessage = show pointError
-      }
- where
-  (invalidValue, reason) =
-    case pointError of
-      T.InvalidPointX coordinateError -> (x, coordinateError)
-      T.InvalidPointY coordinateError -> (y, coordinateError)
-
-projectionFailure :: Int -> T.PointValidationError -> AbiFailure
-projectionFailure index pointError =
-  AbiFailure
-    (statusCode AbiStatusGeometryObstruction)
-    emptyObstruction
-      { obstructionCode = obstructionCodeId ObstructionRegionProjectionFailed
-      , obstructionCoordinateError =
-          coordinateErrorCode
-            ( case pointError of
-                T.InvalidPointX coordinateError -> coordinateError
-                T.InvalidPointY coordinateError -> coordinateError
-            )
-      , obstructionInputIndex = fromIntegral index
-      , obstructionMessage = show pointError
-      }
-
-buildErrorObstruction :: T.BuildError -> CObstruction
-buildErrorObstruction failure =
-  ( case failure of
-      T.InvalidCoordinate inputIndex value reason ->
-        emptyObstruction
-          { obstructionCode = obstructionCodeId ObstructionInvalidCoordinate
-          , obstructionCoordinateError = coordinateErrorCode reason
-          , obstructionInputIndex = maybe maxBound fromIntegral inputIndex
-          , obstructionFirstValue = value
-          }
-      T.PointLocationFailed (T.Point x y) -> pointObstruction ObstructionPointLocationFailed x y
-      T.LocationWalkExhausted (T.Point x y) steps ->
-        (pointObstruction ObstructionLocationWalkExhausted x y) {obstructionFirstIndex = fromIntegral steps}
-      T.RefinementInputTopologyInvalid _ -> codeOnly ObstructionRefinementInputTopologyInvalid
-      T.FreshInsertionMatchedExistingVertex firstVertex secondVertex -> indices ObstructionFreshInsertionMatchedExistingVertex (T.unVertexId firstVertex) (T.unVertexId secondVertex)
-      T.DegenerateLineEndpointMissingOutgoing vertex -> firstIndex ObstructionDegenerateLineEndpointMissingOutgoing (T.unVertexId vertex)
-      T.DegenerateLineEndpointTurnMissing index -> firstIndex ObstructionDegenerateLineEndpointTurnMissing index
-      T.DegenerateLineConnectedVertexMissing index -> firstIndex ObstructionDegenerateLineConnectedVertexMissing index
-      T.HullStartNotVisible edge -> firstIndex ObstructionHullStartNotVisible (T.unDirectedEdgeId edge)
-      T.OuterRangeDidNotTerminate firstEdge secondEdge steps ->
-        (indices ObstructionOuterRangeDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
-          {obstructionFirstValue = fromIntegral steps}
-      T.OuterRangeContainsInnerEdge edge face -> indices ObstructionOuterRangeContainsInnerEdge (T.unDirectedEdgeId edge) (T.unFaceId face)
-      T.ConstrainedEdgeFlipRefused edge -> firstIndex ObstructionConstrainedEdgeFlipRefused (T.unUndirectedEdgeId edge)
-      T.RemovalVertexOutOfRange vertex count -> indices ObstructionRemovalVertexOutOfRange (T.unVertexId vertex) count
-      T.RemovalEdgeOutOfRange edge count -> indices ObstructionRemovalEdgeOutOfRange (T.unUndirectedEdgeId edge) count
-      T.RemovalFaceOutOfRange face count -> indices ObstructionRemovalFaceOutOfRange (T.unFaceId face) count
-      T.RemovalFaceCycleDidNotTerminate face edge steps ->
-        (indices ObstructionRemovalFaceCycleDidNotTerminate (T.unFaceId face) (T.unDirectedEdgeId edge))
-          {obstructionFirstValue = fromIntegral steps}
-      T.RemovalEmptyTriangulation vertex -> firstIndex ObstructionRemovalEmptyTriangulation (T.unVertexId vertex)
-      T.RemovalTwoPointDegreeMismatch vertex degree -> indices ObstructionRemovalTwoPointDegreeMismatch (T.unVertexId vertex) degree
-      T.RemovalCollinearDegreeMismatch vertex degree -> indices ObstructionRemovalCollinearDegreeMismatch (T.unVertexId vertex) degree
-      T.RemovalBorderTooShort count -> firstIndex ObstructionRemovalBorderTooShort count
-      T.RemovalBorderArityMismatch count -> firstIndex ObstructionRemovalBorderArityMismatch count
-      T.RemovalOutgoingCycleDidNotTerminate vertex edge steps ->
-        (indices ObstructionRemovalOutgoingCycleDidNotTerminate (T.unVertexId vertex) (T.unDirectedEdgeId edge))
-          {obstructionFirstValue = fromIntegral steps}
-      T.CircleSweepHullEmpty -> codeOnly ObstructionCircleSweepHullEmpty
-      T.OuterCycleDidNotTerminate firstEdge secondEdge steps ->
-        (indices ObstructionOuterCycleDidNotTerminate (T.unDirectedEdgeId firstEdge) (T.unDirectedEdgeId secondEdge))
-          {obstructionFirstValue = fromIntegral steps}
-      T.HierarchyLevelPopulationMismatch level expected observed ->
-        (indices ObstructionHierarchyLevelPopulationMismatch expected observed) {obstructionFirstValue = fromIntegral level}
-      T.HierarchyInsertionHandleMismatch expected observed -> indices ObstructionHierarchyInsertionHandleMismatch (T.unVertexId expected) (T.unVertexId observed)
-      T.PointIndexCapacityExhausted count -> firstIndex ObstructionPointIndexCapacityExhausted count
-      T.RefinementMinimumAngleNotFinite value -> nonFinite ObstructionRefinementMinimumAngleNotFinite value
-      T.RefinementMinimumAngleOutOfRange value -> firstValue ObstructionRefinementMinimumAngleOutOfRange value
-      T.RefinementMinimumAngleDerivedRatioNotFinite value -> nonFinite ObstructionRefinementMinimumAngleDerivedRatioNotFinite value
-      T.RefinementMaximumAdditionalVerticesNegative value -> firstValue ObstructionRefinementMaximumAdditionalVerticesNegative (fromIntegral value)
-      T.RefinementMinimumAreaNotFinite value -> nonFinite ObstructionRefinementMinimumAreaNotFinite value
-      T.RefinementMinimumAreaNegative value -> firstValue ObstructionRefinementMinimumAreaNegative value
-      T.RefinementMaximumAreaNotFinite value -> nonFinite ObstructionRefinementMaximumAreaNotFinite value
-      T.RefinementMaximumAreaNotPositive value -> firstValue ObstructionRefinementMaximumAreaNotPositive value
-      T.RefinementMaximumRadiusEdgeRatioNotFinite value -> nonFinite ObstructionRefinementMaximumRadiusEdgeRatioNotFinite value
-      T.RefinementMaximumRadiusEdgeRatioNotPositive value -> firstValue ObstructionRefinementMaximumRadiusEdgeRatioNotPositive value
-      T.RefinementMaximumEdgeLengthNotFinite value -> nonFinite ObstructionRefinementMaximumEdgeLengthNotFinite value
-      T.RefinementMaximumEdgeLengthNotPositive value -> firstValue ObstructionRefinementMaximumEdgeLengthNotPositive value
-      T.RefinementMinimumAreaExceedsMaximum minimumArea maximumArea -> values ObstructionRefinementMinimumAreaExceedsMaximum minimumArea maximumArea
-      T.RefinementSeedFaceNotActive face count -> indices ObstructionRefinementSeedFaceNotActive (T.unFaceId face) count
-      T.RefinementDomainTopologyChanged -> codeOnly ObstructionRefinementDomainTopologyChanged
-      T.RefinementDomainRequiresConvexHullPreservation -> codeOnly ObstructionRefinementDomainRequiresConvexHullPreservation
-      T.RefinementDomainRequiresConstraintPreservation -> codeOnly ObstructionRefinementDomainRequiresConstraintPreservation
-      T.RefinementDomainForbidsOuterFaceExclusion -> codeOnly ObstructionRefinementDomainForbidsOuterFaceExclusion
-      T.RefinementDomainWouldCrossInterface edge face -> indices ObstructionRefinementDomainWouldCrossInterface (T.unUndirectedEdgeId edge) (T.unFaceId face)
-      T.RefinementDomainWouldRewriteProtectedFace face -> firstIndex ObstructionRefinementDomainWouldRewriteProtectedFace (T.unFaceId face)
-      T.RefinementDomainProtectedFaceChanged face -> firstIndex ObstructionRefinementDomainProtectedFaceChanged (T.unFaceId face)
-      T.RefinementDomainInterfaceOppositeFaceNotPermitted edge face ->
-        indices ObstructionRefinementDomainInterfaceOppositeFaceNotPermitted (T.unUndirectedEdgeId edge) (T.unFaceId face)
-      T.RefinementOversizedEdge face edge actual bound ->
-        (indices ObstructionRefinementOversizedEdge (T.unFaceId face) (T.unUndirectedEdgeId edge))
-          { obstructionFirstValue = actual
-          , obstructionSecondValue = bound
-          }
-      T.CapacityExceeded count -> firstIndex ObstructionCapacityExceeded count
-      T.HalfEdgeCapacityExceeded requested capacity -> indices ObstructionHalfEdgeCapacityExceeded requested capacity
-      T.FaceCapacityExceeded requested capacity -> indices ObstructionFaceCapacityExceeded requested capacity
-      T.PayloadStorageFailure _ -> codeOnly ObstructionPayloadStorageFailure
-      T.CoordinatePayloadCountMismatch coordinates payloads -> indices ObstructionCoordinatePayloadCountMismatch coordinates payloads
-      T.CircleSweepRequiresDenseStorage -> codeOnly ObstructionCircleSweepRequiresDenseStorage
-      T.SeamFrontierUnavailable -> codeOnly ObstructionSeamFrontierUnavailable
-      T.RefinementDomainRequiresFiniteVertexBudget -> codeOnly ObstructionRefinementDomainRequiresFiniteVertexBudget
-      T.SeamSourceEdgeRequiresFlip edge -> firstIndex ObstructionSeamSourceEdgeRequiresFlip (T.unUndirectedEdgeId edge)
-      T.RefinementSeamBridgeBudgetExceeded required available -> indices ObstructionRefinementSeamBridgeBudgetExceeded required available
-      T.RefinementSeamBridgeMidpointCollapsed edge -> firstIndex ObstructionRefinementSeamBridgeMidpointCollapsed (T.unUndirectedEdgeId edge)
-      T.BoundarySplitRequiresBoundaryEdge edge -> firstIndex ObstructionBoundarySplitRequiresBoundaryEdge (T.unUndirectedEdgeId edge)
-  )
-    {obstructionMessage = show failure}
- where
-  codeOnly :: ObstructionCode -> CObstruction
-  codeOnly code = emptyObstruction {obstructionCode = obstructionCodeId code}
-  firstIndex :: Integral index => ObstructionCode -> index -> CObstruction
-  firstIndex code index = (codeOnly code) {obstructionFirstIndex = fromIntegral index}
-  indices :: (Integral first, Integral second) => ObstructionCode -> first -> second -> CObstruction
-  indices code firstIndexValue secondIndexValue =
-    (codeOnly code)
-      { obstructionFirstIndex = fromIntegral firstIndexValue
-      , obstructionSecondIndex = fromIntegral secondIndexValue
-      }
-  firstValue :: ObstructionCode -> Double -> CObstruction
-  firstValue code value = (codeOnly code) {obstructionFirstValue = value}
-  values :: ObstructionCode -> Double -> Double -> CObstruction
-  values code firstCoordinateValue secondCoordinateValue =
-    (codeOnly code)
-      { obstructionFirstValue = firstCoordinateValue
-      , obstructionSecondValue = secondCoordinateValue
-      }
-  pointObstruction :: ObstructionCode -> Double -> Double -> CObstruction
-  pointObstruction code x y =
-    (codeOnly code)
-      { obstructionPointX = x
-      , obstructionPointY = y
-      }
-  nonFinite :: ObstructionCode -> T.NonFiniteValue -> CObstruction
-  nonFinite code value = firstIndex code (nonFiniteCode value)
-
-coordinateErrorCode :: T.CoordinateError -> Word32
-coordinateErrorCode reason =
-  coordinateErrorCodeId
-    ( case reason of
-        T.CoordinateNaN -> CoordinateErrorNaN
-        T.CoordinateInfinite -> CoordinateErrorInfinite
-        T.CoordinateTooSmall -> CoordinateErrorTooSmall
-        T.CoordinateTooLarge -> CoordinateErrorTooLarge
-    )
-
-nonFiniteCode :: T.NonFiniteValue -> Word32
-nonFiniteCode value =
-  case value of
-    T.ValueNaN -> 1
-    T.ValuePositiveInfinity -> 2
-    T.ValueNegativeInfinity -> 3
diff --git a/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs b/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs
deleted file mode 100644
--- a/ffi/abi/Moonlight/Triangulation/Foreign/Region.hs
+++ /dev/null
@@ -1,408 +0,0 @@
-{-# LANGUAGE RecordWildCards #-}
-
-module Moonlight.Triangulation.Foreign.Region
-  ( regionCopyF64
-  , regionCounts
-  , regionCreateF64
-  , regionDifference
-  , regionFree
-  , regionIntersection
-  , regionLocatePointF64
-  , regionMeasure
-  , regionSymmetricDifference
-  , regionUnion
-  ) where
-
-import Data.Bifunctor (first)
-import Data.Int (Int64)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Foreign.C.String (withCStringLen)
-import Foreign.C.Types (CChar, CDouble (..), CSize, CUInt)
-import Foreign.Marshal.Utils (copyBytes)
-import Foreign.Ptr (Ptr, nullPtr)
-import Foreign.Storable (peekElemOff, poke, pokeElemOff)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation as T
-import Moonlight.Triangulation.Foreign.Boundary
-  ( checkedCount
-  , dereferenceHandle
-  , freeHandle
-  , produceHandle
-  , readPoints
-  , requireOutputCapacity
-  , requirePointer
-  , runBoundary
-  )
-import Moonlight.Triangulation.Foreign.Contract
-  ( CObstruction
-  , CRegion
-  , RegionLocationCode (..)
-  , regionLocationCodeId
-  )
-import Moonlight.Triangulation.Foreign.Obstruction
-  ( AbiFailure
-  , RegionCountKind (..)
-  , RegionLayoutError (..)
-  , nullPointerFailure
-  , overlayFailure
-  , pointInputFailure
-  , projectionFailure
-  , regionLayoutFailure
-  , regionPublicationFailure
-  , regionValidationFailure
-  , valuationFailure
-  )
-
-readCounts
-  :: String
-  -> Ptr CSize
-  -> Int
-  -> IO (Either AbiFailure (V.Vector Int))
-readCounts _ _ 0 = pure (Right V.empty)
-readCounts label pointer count
-  | pointer == nullPtr = pure (Left (nullPointerFailure label))
-  | otherwise = do
-      rawCounts <- V.generateM count (peekElemOff pointer)
-      pure (V.mapM (checkedCount 1) rawCounts)
-
-validateCounts
-  :: RegionCountKind
-  -> Int
-  -> V.Vector Int
-  -> Either RegionLayoutError ()
-validateCounts kind expectedTotal counts =
-  case V.findIndex (== 0) counts of
-    Just index -> Left (RegionGroupEmpty kind index)
-    Nothing
-      | observedTotal /= toInteger expectedTotal ->
-          Left (RegionCountTotalMismatch kind observedTotal (toInteger expectedTotal))
-      | otherwise -> Right ()
- where
-  observedTotal = V.foldl' (\total count -> total + toInteger count) 0 counts
-
-regionCreateF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr (Ptr CRegion)
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCreateF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount output obstructionPointer =
-  runBoundary obstructionPointer $
-    produceHandle output $
-      readRegionF64
-        coordinates
-        rawPointCount
-        loopPointCounts
-        rawLoopCount
-        componentLoopCounts
-        rawComponentCount
-
-readRegionF64
-  :: Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> IO (Either AbiFailure T.PlanarRegion)
-readRegionF64 coordinates rawPointCount loopPointCounts rawLoopCount componentLoopCounts rawComponentCount =
-  case checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount of
-    Left failure -> pure (Left failure)
-    Right (pointCount, loopCount, componentCount) -> do
-      points <- readPoints coordinates pointCount
-      loopCounts <- readCounts "loop_point_counts" loopPointCounts loopCount
-      componentCounts <- readCounts "component_loop_counts" componentLoopCounts componentCount
-      pure $ do
-        admittedPoints <- points
-        admittedLoopCounts <- loopCounts
-        admittedComponentCounts <- componentCounts
-        first regionLayoutFailure (validateCounts LoopPointCounts pointCount admittedLoopCounts)
-        first regionLayoutFailure (validateCounts ComponentLoopCounts loopCount admittedComponentCounts)
-        buildRegion admittedPoints admittedLoopCounts admittedComponentCounts
-
-checkedRegionInputCounts
-  :: CSize
-  -> CSize
-  -> CSize
-  -> Either AbiFailure (Int, Int, Int)
-checkedRegionInputCounts rawPointCount rawLoopCount rawComponentCount = do
-  pointCount <- checkedCount 2 rawPointCount
-  loopCount <- checkedCount 1 rawLoopCount
-  componentCount <- checkedCount 1 rawComponentCount
-  pure (pointCount, loopCount, componentCount)
-
-buildRegion
-  :: V.Vector T.Point
-  -> V.Vector Int
-  -> V.Vector Int
-  -> Either AbiFailure T.PlanarRegion
-buildRegion points loopCounts componentCounts = do
-  exactPoints <-
-    V.imapM
-      (\index point -> first (pointInputFailure index point) (T.exactPointFromPoint point))
-      points
-  loops <- V.imapM (buildLoop exactPoints) (adjacentOffsets loopCounts)
-  components <- V.imapM (buildComponent loops) (adjacentOffsets componentCounts)
-  first regionValidationFailure (T.planarRegion (V.toList components))
-
-adjacentOffsets :: V.Vector Int -> V.Vector (Int, Int)
-adjacentOffsets counts =
-  let offsets = V.scanl' (+) 0 counts
-   in V.zip offsets (V.drop 1 offsets)
-
-buildLoop
-  :: V.Vector T.ExactPoint
-  -> Int
-  -> (Int, Int)
-  -> Either AbiFailure T.ExactLoop
-buildLoop points loopIndex (start, end) =
-  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) points)) of
-    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty LoopPointCounts loopIndex))
-    Just submitted -> first regionValidationFailure (T.exactLoop submitted)
-
-buildComponent
-  :: V.Vector T.ExactLoop
-  -> Int
-  -> (Int, Int)
-  -> Either AbiFailure T.PolygonComponent
-buildComponent loops componentIndex (start, end) =
-  case NonEmpty.nonEmpty (V.toList (V.slice start (end - start) loops)) of
-    Nothing -> Left (regionLayoutFailure (RegionGroupEmpty ComponentLoopCounts componentIndex))
-    Just (outer :| holes) -> first regionValidationFailure (T.polygonComponent outer holes)
-
-regionShape
-  :: T.PlanarRegion
-  -> ([T.PolygonComponent], [[T.ExactLoop]], [T.ExactLoop])
-regionShape region =
-  let components = T.planarRegionComponents region
-      componentLoops component = T.polygonOuterLoop component : T.polygonHoleLoops component
-      loopsByComponent = map componentLoops components
-   in (components, loopsByComponent, concat loopsByComponent)
-
-regionCounts
-  :: Ptr CRegion
-  -> Ptr CSize
-  -> Ptr CSize
-  -> Ptr CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCounts regionPointer componentCountOutput loopCountOutput pointCountOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "component_count" componentCountOutput
-        >> requirePointer "loop_count" loopCountOutput
-        >> requirePointer "point_count" pointCountOutput
-    of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        region <- dereferenceHandle regionPointer
-        let (components, _, loops) = regionShape region
-            pointCount = sum (map (NonEmpty.length . T.exactLoopPoints) loops)
-        poke componentCountOutput (fromIntegral (length components))
-        poke loopCountOutput (fromIntegral (length loops))
-        poke pointCountOutput (fromIntegral pointCount)
-        pure (Right ())
-
-data RegionProjection = RegionProjection
-  { projectionPoints :: !(V.Vector T.Point)
-  , projectionLoopPointOffsets :: !(V.Vector CSize)
-  , projectionComponentLoopOffsets :: !(V.Vector CSize)
-  }
-
-regionProjection :: T.PlanarRegion -> Either AbiFailure RegionProjection
-regionProjection region = do
-  let (_, loopsByComponent, loops) = regionShape region
-      exactPoints = concatMap (NonEmpty.toList . T.exactLoopPoints) loops
-      loopPointOffsets = scanl (+) 0 (map (NonEmpty.length . T.exactLoopPoints) loops)
-      componentLoopOffsets = scanl (+) 0 (map length loopsByComponent)
-  projectedPoints <-
-    V.fromList
-      <$> traverse
-        (uncurry projectPoint)
-        (zip [0 ..] exactPoints)
-  pure
-    RegionProjection
-      { projectionPoints = projectedPoints
-      , projectionLoopPointOffsets = V.fromList (map fromIntegral loopPointOffsets)
-      , projectionComponentLoopOffsets = V.fromList (map fromIntegral componentLoopOffsets)
-      }
- where
-  projectPoint index point =
-    T.queryPointValue
-      <$> first (projectionFailure index) (T.exactPointToEmbeddingCandidate point)
-
-regionCopyF64
-  :: Ptr CRegion
-  -> Ptr CDouble
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CSize
-  -> CSize
-  -> Ptr CObstruction
-  -> IO CUInt
-regionCopyF64 regionPointer coordinates rawPointCapacity loopPointOffsets rawLoopOffsetCapacity componentLoopOffsets rawComponentOffsetCapacity obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      (,,)
-        <$> (requirePointer "region" regionPointer >> checkedCount 2 rawPointCapacity)
-        <*> checkedCount 1 rawLoopOffsetCapacity
-        <*> checkedCount 1 rawComponentOffsetCapacity
-    of
-      Left failure -> pure (Left failure)
-      Right (pointCapacity, loopOffsetCapacity, componentOffsetCapacity) -> do
-        region <- dereferenceHandle regionPointer
-        case regionProjection region of
-          Left failure -> pure (Left failure)
-          Right RegionProjection {..} ->
-            case
-              requireOutputCapacity "coordinates" coordinates (V.length projectionPoints) pointCapacity
-                >> requireOutputCapacity "loop_point_offsets" loopPointOffsets (V.length projectionLoopPointOffsets) loopOffsetCapacity
-                >> requireOutputCapacity "component_loop_offsets" componentLoopOffsets (V.length projectionComponentLoopOffsets) componentOffsetCapacity
-            of
-              Left failure -> pure (Left failure)
-              Right () -> do
-                V.imapM_
-                  ( \index (T.Point x y) -> do
-                      pokeElemOff coordinates (index * 2) (CDouble x)
-                      pokeElemOff coordinates (index * 2 + 1) (CDouble y)
-                  )
-                  projectionPoints
-                V.imapM_ (pokeElemOff loopPointOffsets) projectionLoopPointOffsets
-                V.imapM_ (pokeElemOff componentLoopOffsets) projectionComponentLoopOffsets
-                pure (Right ())
-
-regionUnion, regionIntersection, regionDifference, regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-regionUnion = binaryRegionOperation (\(left, right) -> left || right)
-regionIntersection = binaryRegionOperation (\(left, right) -> left && right)
-regionDifference = binaryRegionOperation (\(left, right) -> left && not right)
-regionSymmetricDifference = binaryRegionOperation (uncurry (/=))
-
-binaryRegionOperation
-  :: ((Bool, Bool) -> Bool)
-  -> Ptr CRegion
-  -> Ptr CRegion
-  -> Ptr (Ptr CRegion)
-  -> Ptr CObstruction
-  -> IO CUInt
-binaryRegionOperation selected leftPointer rightPointer output obstructionPointer =
-  runBoundary obstructionPointer $ produceHandle output $ do
-    case requirePointer "left region" leftPointer >> requirePointer "right region" rightPointer of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        left <- dereferenceHandle leftPointer
-        right <- dereferenceHandle rightPointer
-        pure (exactRegionBoolean selected left right)
-
-exactRegionBoolean
-  :: ((Bool, Bool) -> Bool)
-  -> T.PlanarRegion
-  -> T.PlanarRegion
-  -> Either AbiFailure T.PlanarRegion
-exactRegionBoolean selected left right = do
-  leftLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True left))
-  rightLayer <- first regionValidationFailure (T.planarLayer False (Map.singleton True right))
-  overlay <- first overlayFailure (T.overlayLayers leftLayer rightLayer)
-  first regionPublicationFailure (T.overlaySelectedRegion selected overlay)
-
-regionLocatePointF64
-  :: Ptr CRegion
-  -> CDouble
-  -> CDouble
-  -> Ptr CUInt
-  -> Ptr CObstruction
-  -> IO CUInt
-regionLocatePointF64 regionPointer (CDouble x) (CDouble y) output obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "location" output
-        >> first (pointInputFailure 0 (T.Point x y)) (T.exactPointFromPoint (T.Point x y))
-    of
-      Left failure -> pure (Left failure)
-      Right query -> do
-        region <- dereferenceHandle regionPointer
-        poke output (regionLocationCode (T.regionPointLocation region query))
-        pure (Right ())
-
-regionLocationCode :: T.RegionPointLocation -> CUInt
-regionLocationCode location =
-  fromIntegral
-    ( regionLocationCodeId
-        ( case location of
-            T.RegionExterior -> RegionLocationExterior
-            T.RegionOnBoundary -> RegionLocationBoundary
-            T.RegionInterior -> RegionLocationInterior
-        )
-    )
-
-regionMeasure
-  :: Ptr CRegion
-  -> Ptr Int64
-  -> Ptr CChar
-  -> CSize
-  -> Ptr CSize
-  -> Ptr CDouble
-  -> Ptr CDouble
-  -> Ptr CObstruction
-  -> IO CUInt
-regionMeasure regionPointer eulerOutput areaRatioOutput rawAreaCapacity areaBytesWritten perimeterLowerOutput perimeterUpperOutput obstructionPointer =
-  runBoundary obstructionPointer $
-    case
-      requirePointer "region" regionPointer
-        >> requirePointer "euler_characteristic" eulerOutput
-        >> requirePointer "area_bytes_written" areaBytesWritten
-        >> requirePointer "perimeter_lower" perimeterLowerOutput
-        >> requirePointer "perimeter_upper" perimeterUpperOutput
-        >> checkedCount 1 rawAreaCapacity
-    of
-      Left failure -> pure (Left failure)
-      Right areaCapacity -> do
-        region <- dereferenceHandle regionPointer
-        case regionMeasurements region of
-          Left failure -> pure (Left failure)
-          Right (valuations, perimeter) -> do
-            let area = T.exactAreaValue (T.valuationArea valuations)
-                areaText =
-                  show (T.exactRationalNumerator area)
-                    <> "/"
-                    <> show (T.exactRationalDenominator area)
-                bounds = T.exactLengthBounds perimeter
-            poke eulerOutput (fromIntegral (T.eulerCharacteristicValue (T.valuationEuler valuations)))
-            poke perimeterLowerOutput (CDouble (T.intervalLower bounds))
-            poke perimeterUpperOutput (CDouble (T.intervalUpper bounds))
-            copyCStringOutput areaText areaRatioOutput areaCapacity areaBytesWritten
-
-regionMeasurements
-  :: T.PlanarRegion
-  -> Either AbiFailure (T.PlanarValuations, T.ExactLengthMeasurement)
-regionMeasurements region = do
-  valuations <- first valuationFailure (T.regionValuations region)
-  perimeter <- first valuationFailure (T.planarValuationsPerimeter valuations)
-  pure (valuations, perimeter)
-
-copyCStringOutput
-  :: String
-  -> Ptr CChar
-  -> Int
-  -> Ptr CSize
-  -> IO (Either AbiFailure ())
-copyCStringOutput value output capacity bytesWritten =
-  withCStringLen value $ \(source, byteCount) -> do
-    poke bytesWritten (fromIntegral byteCount)
-    case requireOutputCapacity "area_ratio_utf8" output (byteCount + 1) capacity of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        copyBytes output source byteCount
-        pokeElemOff output byteCount 0
-        pure (Right ())
-
-regionFree :: Ptr CRegion -> IO ()
-regionFree = freeHandle
diff --git a/ffi/bindings/README.md b/ffi/bindings/README.md
--- a/ffi/bindings/README.md
+++ b/ffi/bindings/README.md
@@ -5,7 +5,7 @@
 thin lifecycle and bulk-layout leaves over that sole ABI; none implements
 triangulation, overlay, publication, valuation, or morphology.
 
-`Moonlight.Triangulation.Foreign.Contract` is the closed wire authority. Its
+`Moonlight.Planar.Foreign.Contract` is the closed wire authority. Its
 pure renderer owns the installed header, Windows export list, Haskell export
 module, TypeScript wire vocabulary and raw declarations, Python ctypes
 declarations, and Rust extern block. Check or rewrite those derived sections
@@ -13,7 +13,7 @@
 
 ```bash
 ../scripts/safe-cabal.sh run \
-  moonlight-planar:exe:moonlight-triangulation-ffi-contract \
+  moonlight-planar:exe:moonlight-planar-ffi-contract \
   --project-file=cabal.project.planar-dev \
   -- check foundation/moonlight-planar
 ```
diff --git a/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py b/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
--- a/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
+++ b/ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
@@ -1,5 +1,5 @@
-# Generated from Moonlight.Triangulation.Foreign.Contract.
-# Run moonlight-triangulation-ffi-contract write; do not edit this file.
+# Generated from Moonlight.Planar.Foreign.Contract.
+# Run moonlight-planar-ffi-contract write; do not edit this file.
 
 from __future__ import annotations
 
@@ -93,6 +93,7 @@
 ML_OBSTRUCTION_VALUATION_FAILED: Final = 204
 ML_OBSTRUCTION_MINKOWSKI_FAILED: Final = 205
 ML_OBSTRUCTION_REGION_PROJECTION_FAILED: Final = 206
+ML_OBSTRUCTION_CONVEX_FAILED: Final = 207
 ML_COORDINATE_ERROR_NONE: Final = 0
 ML_COORDINATE_ERROR_NAN: Final = 1
 ML_COORDINATE_ERROR_INFINITE: Final = 2
diff --git a/ffi/bindings/rust/src/raw_generated.rs b/ffi/bindings/rust/src/raw_generated.rs
--- a/ffi/bindings/rust/src/raw_generated.rs
+++ b/ffi/bindings/rust/src/raw_generated.rs
@@ -1,5 +1,5 @@
-// Generated from Moonlight.Triangulation.Foreign.Contract.
-// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+// Generated from Moonlight.Planar.Foreign.Contract.
+// Run moonlight-planar-ffi-contract write; do not edit this file.
 
 // This private module is the complete wire projection; wrappers consume a subset.
 #![allow(dead_code)]
@@ -91,6 +91,7 @@
 pub(crate) const ML_OBSTRUCTION_VALUATION_FAILED: u32 = 204;
 pub(crate) const ML_OBSTRUCTION_MINKOWSKI_FAILED: u32 = 205;
 pub(crate) const ML_OBSTRUCTION_REGION_PROJECTION_FAILED: u32 = 206;
+pub(crate) const ML_OBSTRUCTION_CONVEX_FAILED: u32 = 207;
 pub(crate) const ML_COORDINATE_ERROR_NONE: u32 = 0;
 pub(crate) const ML_COORDINATE_ERROR_NAN: u32 = 1;
 pub(crate) const ML_COORDINATE_ERROR_INFINITE: u32 = 2;
diff --git a/ffi/bindings/typescript/src/internal/native.generated.ts b/ffi/bindings/typescript/src/internal/native.generated.ts
--- a/ffi/bindings/typescript/src/internal/native.generated.ts
+++ b/ffi/bindings/typescript/src/internal/native.generated.ts
@@ -1,5 +1,5 @@
-// Generated from Moonlight.Triangulation.Foreign.Contract.
-// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+// Generated from Moonlight.Planar.Foreign.Contract.
+// Run moonlight-planar-ffi-contract write; do not edit this file.
 
 import koffi from "koffi";
 
@@ -88,6 +88,7 @@
 export const ML_OBSTRUCTION_VALUATION_FAILED = 204;
 export const ML_OBSTRUCTION_MINKOWSKI_FAILED = 205;
 export const ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206;
+export const ML_OBSTRUCTION_CONVEX_FAILED = 207;
 export const ML_COORDINATE_ERROR_NONE = 0;
 export const ML_COORDINATE_ERROR_NAN = 1;
 export const ML_COORDINATE_ERROR_INFINITE = 2;
diff --git a/ffi/bindings/typescript/src/wire.generated.ts b/ffi/bindings/typescript/src/wire.generated.ts
--- a/ffi/bindings/typescript/src/wire.generated.ts
+++ b/ffi/bindings/typescript/src/wire.generated.ts
@@ -1,5 +1,5 @@
-// Generated from Moonlight.Triangulation.Foreign.Contract.
-// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+// Generated from Moonlight.Planar.Foreign.Contract.
+// Run moonlight-planar-ffi-contract write; do not edit this file.
 
 export const AbiStatus = {
   Ok: "ok",
@@ -101,6 +101,7 @@
   ValuationFailed: "valuation-failed",
   MinkowskiFailed: "minkowski-failed",
   RegionProjectionFailed: "region-projection-failed",
+  ConvexFailed: "convex-failed",
 } as const;
 export type ObstructionCode = (typeof ObstructionCode)[keyof typeof ObstructionCode];
 const obstructionCodeByWire: Readonly<Record<number, ObstructionCode>> = {
@@ -182,6 +183,7 @@
   204: ObstructionCode.ValuationFailed,
   205: ObstructionCode.MinkowskiFailed,
   206: ObstructionCode.RegionProjectionFailed,
+  207: ObstructionCode.ConvexFailed,
 };
 export function decodeObstructionCode(value: number): ObstructionCode | undefined {
   return obstructionCodeByWire[value];
diff --git a/ffi/cbits/moonlight-triangulation.def b/ffi/cbits/moonlight-triangulation.def
--- a/ffi/cbits/moonlight-triangulation.def
+++ b/ffi/cbits/moonlight-triangulation.def
@@ -1,5 +1,5 @@
-; Generated from Moonlight.Triangulation.Foreign.Contract.
-; Run moonlight-triangulation-ffi-contract write; do not edit this file.
+; Generated from Moonlight.Planar.Foreign.Contract.
+; Run moonlight-planar-ffi-contract write; do not edit this file.
 
 LIBRARY moonlight-triangulation-c
 EXPORTS
diff --git a/ffi/exports/Moonlight/Planar/Foreign/Exports.hs b/ffi/exports/Moonlight/Planar/Foreign/Exports.hs
new file mode 100644
--- /dev/null
+++ b/ffi/exports/Moonlight/Planar/Foreign/Exports.hs
@@ -0,0 +1,80 @@
+-- Generated from Moonlight.Planar.Foreign.Contract.
+-- Run moonlight-planar-ffi-contract write; do not edit this file.
+
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# OPTIONS_GHC -Wno-missing-signatures #-}
+
+module Moonlight.Planar.Foreign.Exports where
+
+import Data.Int (Int64)
+import Data.Word (Word32)
+import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))
+import Foreign.Ptr (Ptr)
+import Moonlight.Planar.Foreign.Contract
+  ( CMesh
+  , CMinkowskiReceipt
+  , CObstruction
+  , CRegion
+  , CStructuringElement
+  )
+import qualified Moonlight.Planar.Foreign.Mesh as Mesh
+import qualified Moonlight.Planar.Foreign.Morphology as Morphology
+import qualified Moonlight.Planar.Foreign.Region as Region
+
+delaunayF64 = Mesh.delaunayF64
+meshInsertManyF64 = Mesh.meshInsertManyF64
+meshSiteUnion = Mesh.meshSiteUnion
+meshSiteIntersection = Mesh.meshSiteIntersection
+meshSiteDifference = Mesh.meshSiteDifference
+meshSiteSymmetricDifference = Mesh.meshSiteSymmetricDifference
+meshVertexCount = Mesh.meshVertexCount
+meshTriangleCount = Mesh.meshTriangleCount
+meshCopyVerticesF64 = Mesh.meshCopyVerticesF64
+meshCopyTrianglesU32 = Mesh.meshCopyTrianglesU32
+meshFree = Mesh.meshFree
+regionCreateF64 = Region.regionCreateF64
+regionCounts = Region.regionCounts
+regionCopyF64 = Region.regionCopyF64
+regionUnion = Region.regionUnion
+regionIntersection = Region.regionIntersection
+regionDifference = Region.regionDifference
+regionSymmetricDifference = Region.regionSymmetricDifference
+regionLocatePointF64 = Region.regionLocatePointF64
+regionMeasure = Region.regionMeasure
+regionFree = Region.regionFree
+structuringElementCreateF64 = Morphology.structuringElementCreateF64
+structuringElementFree = Morphology.structuringElementFree
+regionMinkowskiSum = Morphology.regionMinkowskiSum
+regionOffset = Morphology.regionOffset
+regionInset = Morphology.regionInset
+regionOpen = Morphology.regionOpen
+regionClose = Morphology.regionClose
+
+foreign export ccall "ml_delaunay_f64" delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_insert_many_f64" meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_union" meshSiteUnion :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_intersection" meshSiteIntersection :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_difference" meshSiteDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_site_symmetric_difference" meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_vertex_count" meshVertexCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_triangle_count" meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_copy_vertices_f64" meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_copy_triangles_u32" meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_mesh_free" meshFree :: Ptr CMesh -> IO ()
+foreign export ccall "ml_region_create_f64" regionCreateF64 :: Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_counts" regionCounts :: Ptr CRegion -> Ptr CSize -> Ptr CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_copy_f64" regionCopyF64 :: Ptr CRegion -> Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_union" regionUnion :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_intersection" regionIntersection :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_difference" regionDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_symmetric_difference" regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_locate_point_f64" regionLocatePointF64 :: Ptr CRegion -> CDouble -> CDouble -> Ptr CUInt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_measure" regionMeasure :: Ptr CRegion -> Ptr Int64 -> Ptr CChar -> CSize -> Ptr CSize -> Ptr CDouble -> Ptr CDouble -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_free" regionFree :: Ptr CRegion -> IO ()
+foreign export ccall "ml_structuring_element_create_f64" structuringElementCreateF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CStructuringElement) -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_structuring_element_free" structuringElementFree :: Ptr CStructuringElement -> IO ()
+foreign export ccall "ml_region_minkowski_sum" regionMinkowskiSum :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_offset" regionOffset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_inset" regionInset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_open" regionOpen :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
+foreign export ccall "ml_region_close" regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
diff --git a/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs b/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs
deleted file mode 100644
--- a/ffi/exports/Moonlight/Triangulation/Foreign/Exports.hs
+++ /dev/null
@@ -1,80 +0,0 @@
--- Generated from Moonlight.Triangulation.Foreign.Contract.
--- Run moonlight-triangulation-ffi-contract write; do not edit this file.
-
-{-# LANGUAGE ForeignFunctionInterface #-}
-{-# OPTIONS_GHC -Wno-missing-signatures #-}
-
-module Moonlight.Triangulation.Foreign.Exports where
-
-import Data.Int (Int64)
-import Data.Word (Word32)
-import Foreign.C.Types (CChar, CDouble (..), CSize (..), CUInt (..))
-import Foreign.Ptr (Ptr)
-import Moonlight.Triangulation.Foreign.Contract
-  ( CMesh
-  , CMinkowskiReceipt
-  , CObstruction
-  , CRegion
-  , CStructuringElement
-  )
-import qualified Moonlight.Triangulation.Foreign.Mesh as Mesh
-import qualified Moonlight.Triangulation.Foreign.Morphology as Morphology
-import qualified Moonlight.Triangulation.Foreign.Region as Region
-
-delaunayF64 = Mesh.delaunayF64
-meshInsertManyF64 = Mesh.meshInsertManyF64
-meshSiteUnion = Mesh.meshSiteUnion
-meshSiteIntersection = Mesh.meshSiteIntersection
-meshSiteDifference = Mesh.meshSiteDifference
-meshSiteSymmetricDifference = Mesh.meshSiteSymmetricDifference
-meshVertexCount = Mesh.meshVertexCount
-meshTriangleCount = Mesh.meshTriangleCount
-meshCopyVerticesF64 = Mesh.meshCopyVerticesF64
-meshCopyTrianglesU32 = Mesh.meshCopyTrianglesU32
-meshFree = Mesh.meshFree
-regionCreateF64 = Region.regionCreateF64
-regionCounts = Region.regionCounts
-regionCopyF64 = Region.regionCopyF64
-regionUnion = Region.regionUnion
-regionIntersection = Region.regionIntersection
-regionDifference = Region.regionDifference
-regionSymmetricDifference = Region.regionSymmetricDifference
-regionLocatePointF64 = Region.regionLocatePointF64
-regionMeasure = Region.regionMeasure
-regionFree = Region.regionFree
-structuringElementCreateF64 = Morphology.structuringElementCreateF64
-structuringElementFree = Morphology.structuringElementFree
-regionMinkowskiSum = Morphology.regionMinkowskiSum
-regionOffset = Morphology.regionOffset
-regionInset = Morphology.regionInset
-regionOpen = Morphology.regionOpen
-regionClose = Morphology.regionClose
-
-foreign export ccall "ml_delaunay_f64" delaunayF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_insert_many_f64" meshInsertManyF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_union" meshSiteUnion :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_intersection" meshSiteIntersection :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_difference" meshSiteDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_site_symmetric_difference" meshSiteSymmetricDifference :: Ptr CMesh -> Ptr CMesh -> Ptr (Ptr CMesh) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_vertex_count" meshVertexCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_triangle_count" meshTriangleCount :: Ptr CMesh -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_copy_vertices_f64" meshCopyVerticesF64 :: Ptr CMesh -> Ptr CDouble -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_copy_triangles_u32" meshCopyTrianglesU32 :: Ptr CMesh -> Ptr Word32 -> CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_mesh_free" meshFree :: Ptr CMesh -> IO ()
-foreign export ccall "ml_region_create_f64" regionCreateF64 :: Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_counts" regionCounts :: Ptr CRegion -> Ptr CSize -> Ptr CSize -> Ptr CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_copy_f64" regionCopyF64 :: Ptr CRegion -> Ptr CDouble -> CSize -> Ptr CSize -> CSize -> Ptr CSize -> CSize -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_union" regionUnion :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_intersection" regionIntersection :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_difference" regionDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_symmetric_difference" regionSymmetricDifference :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_locate_point_f64" regionLocatePointF64 :: Ptr CRegion -> CDouble -> CDouble -> Ptr CUInt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_measure" regionMeasure :: Ptr CRegion -> Ptr Int64 -> Ptr CChar -> CSize -> Ptr CSize -> Ptr CDouble -> Ptr CDouble -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_free" regionFree :: Ptr CRegion -> IO ()
-foreign export ccall "ml_structuring_element_create_f64" structuringElementCreateF64 :: Ptr CDouble -> CSize -> Ptr (Ptr CStructuringElement) -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_structuring_element_free" structuringElementFree :: Ptr CStructuringElement -> IO ()
-foreign export ccall "ml_region_minkowski_sum" regionMinkowskiSum :: Ptr CRegion -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_offset" regionOffset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_inset" regionInset :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_open" regionOpen :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
-foreign export ccall "ml_region_close" regionClose :: Ptr CStructuringElement -> Ptr CRegion -> Ptr (Ptr CRegion) -> Ptr CMinkowskiReceipt -> Ptr CObstruction -> IO CUInt
diff --git a/ffi/generator/Main.hs b/ffi/generator/Main.hs
--- a/ffi/generator/Main.hs
+++ b/ffi/generator/Main.hs
@@ -3,7 +3,7 @@
 import Data.Either (partitionEithers)
 import Data.Foldable (traverse_)
 import Data.Bifunctor (first)
-import Moonlight.Triangulation.Foreign.Contract.Render
+import Moonlight.Planar.Foreign.Contract.Render
   ( GeneratedFile (..)
   , generatedFiles
   )
@@ -109,7 +109,7 @@
 renderObstruction obstruction =
   case obstruction of
     InvalidArguments ->
-      "usage: moonlight-triangulation-ffi-contract (write|check) PACKAGE_ROOT"
+      "usage: moonlight-planar-ffi-contract (write|check) PACKAGE_ROOT"
     GeneratedFileMissing path -> "missing generated file: " <> path
     GeneratedFileStale path -> "stale generated file: " <> path
     GeneratedFileReadFailed path message -> "cannot read generated file " <> path <> ": " <> message
diff --git a/ffi/include/moonlight_triangulation.h b/ffi/include/moonlight_triangulation.h
--- a/ffi/include/moonlight_triangulation.h
+++ b/ffi/include/moonlight_triangulation.h
@@ -1,5 +1,5 @@
-// Generated from Moonlight.Triangulation.Foreign.Contract.
-// Run moonlight-triangulation-ffi-contract write; do not edit this file.
+// Generated from Moonlight.Planar.Foreign.Contract.
+// Run moonlight-planar-ffi-contract write; do not edit this file.
 
 #ifndef MOONLIGHT_TRIANGULATION_H
 #define MOONLIGHT_TRIANGULATION_H
@@ -116,7 +116,8 @@
   ML_OBSTRUCTION_REGION_PUBLICATION_FAILED = 203,
   ML_OBSTRUCTION_VALUATION_FAILED = 204,
   ML_OBSTRUCTION_MINKOWSKI_FAILED = 205,
-  ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206
+  ML_OBSTRUCTION_REGION_PROJECTION_FAILED = 206,
+  ML_OBSTRUCTION_CONVEX_FAILED = 207
 };
 
 enum {
diff --git a/moonlight-planar.cabal b/moonlight-planar.cabal
--- a/moonlight-planar.cabal
+++ b/moonlight-planar.cabal
@@ -1,1396 +1,1763 @@
 cabal-version:       3.4
 name:                moonlight-planar
-version:             1.0.0.0
-synopsis:            Native hex regions, Delaunay meshes, and exact planar algebra.
-description:         Native packed hexagonal cell regions and Delaunay and
-                     constrained Delaunay triangulation as lawful
-                     finite-set algebras, together with exact rational planar
-                     regions, labelled common refinement, intrinsic valuations,
-                     and polygonal Minkowski morphology. A mesh is a value of
-                     its site set, so
-                     union, intersection and difference return triangulations
-                     and refinement composes after them rather than replacing
-                     them. One structure-of-arrays half-edge mesh carries the
-                     constrained and unconstrained layers. Private finite-DCEL
-                     and construction kernels own trusted representations and
-                     admitted fast paths. Public dcel and build sublibraries
-                     expose their lawful immutable observations, typed queries,
-                     construction, incremental edits, constraint recovery, and
-                     refinement without duplicating those owners. Further opt-in
-                     sublibraries expose the exact scalar core, Voronoi and
-                     natural-neighbour dual, bounded concurrent join interpreter,
-                     and versioned serialization surface beside the broad main
-                     facade. The dependency-light hex component provides axial
-                     elements, arithmetic neighbours, wordwise Boolean algebra,
-                     restriction, and overlap-compatible gluing without DCEL or
-                     polygon construction.
-                     On GHC 9.14, a public cell-complex component interprets
-                     admitted exact cell selections for Homology and Category
-                     and lowers exact Delaunay alpha filtrations into persistent
-                     homology; an opt-in zigzag component tracks exact topology
-                     across independently sampled, non-nested labelled depths.
-                     Failure is values: every refusal names its witness.
-license:             MIT
-license-file:        LICENSE
-author:              Blue Rose
-maintainer:          rosaliafialkova@gmail.com
-copyright:           (c) 2026 Blue Rose
-category:            Geometry, Math
-homepage:            https://github.com/PaleRoses/moonlight
-bug-reports:         https://github.com/PaleRoses/moonlight/issues
-build-type:          Simple
-tested-with:         GHC == 9.8.4
-                     GHC == 9.10.3
-                     GHC == 9.12.4
-                     GHC == 9.14.1
-extra-doc-files:
-  README.md
-  CHANGELOG.md
-  docs/README.md
-  docs/development.md
-  docs/activation-zigzag.md
-  docs/regular-site-algebra.md
-  docs/weighted-alpha.md
-  docs/examples/README.md
-  docs/mesh-publication.md
-  docs/category-observatory/README.md
-  docs/category-observatory/moonlight-triangulation-category-observatory.svg
-  docs/delaunay-compare/README.md
-  docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg
-  docs/persistence-rose/alpha-eclipse.md
-  docs/persistence-rose/README.md
-  docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg
-  docs/persistence-rose/moonlight-triangulation-persistence-rose.svg
-  docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
-extra-source-files:
-  weeder.toml
-  -- The retained CSV, source receipt, and big-point SVG remain beside the
-  -- benchmark but are not package inputs. The small documentation projection
-  -- is the sole published chart and is declared above.
-  docs/delaunay-compare/rust/Cargo.lock
-  docs/delaunay-compare/rust/Cargo.toml
-  docs/delaunay-compare/rust/src/lib.rs
-  ffi/include/moonlight_triangulation.h
-  ffi/bindings/README.md
-  ffi/bindings/python/pyproject.toml
-  ffi/bindings/python/src/moonlight_triangulation/__init__.py
-  ffi/bindings/python/src/moonlight_triangulation/_binding.py
-  ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
-  ffi/bindings/python/src/moonlight_triangulation/errors.py
-  ffi/bindings/python/src/moonlight_triangulation/values.py
-  ffi/bindings/python/tests/test_binding.py
-  ffi/bindings/rust/Cargo.toml
-  ffi/bindings/rust/build.rs
-  ffi/bindings/rust/src/binding.rs
-  ffi/bindings/rust/src/error.rs
-  ffi/bindings/rust/src/lib.rs
-  ffi/bindings/rust/src/raw_generated.rs
-  ffi/bindings/rust/src/values.rs
-  ffi/bindings/rust/tests/binding.rs
-  ffi/bindings/typescript/README.md
-  ffi/bindings/typescript/package.json
-  ffi/bindings/typescript/pnpm-workspace.yaml
-  ffi/bindings/typescript/tsconfig.json
-  ffi/bindings/typescript/src/failure.ts
-  ffi/bindings/typescript/src/index.ts
-  ffi/bindings/typescript/src/internal/call.ts
-  ffi/bindings/typescript/src/internal/decode.ts
-  ffi/bindings/typescript/src/internal/native.generated.ts
-  ffi/bindings/typescript/src/internal/resource.ts
-  ffi/bindings/typescript/src/mesh.ts
-  ffi/bindings/typescript/src/moonlight.ts
-  ffi/bindings/typescript/src/morphology.ts
-  ffi/bindings/typescript/src/region.ts
-  ffi/bindings/typescript/src/resource.ts
-  ffi/bindings/typescript/src/values.ts
-  ffi/bindings/typescript/src/wire.generated.ts
-  ffi/bindings/typescript/test/binding.test.ts
-
-source-repository head
-  type:     git
-  location: https://github.com/PaleRoses/moonlight.git
-  subdir:   moonlight-planar
-
-source-repository this
-  type:     git
-  location: https://github.com/PaleRoses/moonlight.git
-  tag:      moonlight-planar-1.0.0.0
-  subdir:   moonlight-planar
-
-flag warnings-as-errors
-  description: Treat compiler warnings as errors in package validation.
-  default: False
-  manual: True
-
-common shared-properties
-  default-language: GHC2021
-  default-extensions:
-    DerivingStrategies
-  ghc-options:
-    -Wall
-    -Wcompat
-    -Widentities
-    -Wincomplete-record-updates
-    -Wincomplete-uni-patterns
-    -Wredundant-constraints
-    -Wpartial-fields
-    -Wno-missing-import-lists
-  if flag(warnings-as-errors)
-    ghc-options: -Werror
-  -- The hot modules that request LLVM O3 replace GHC's default optimiser
-  -- recipe. GHC 9.8 therefore needs its supported legacy pass manager stated
-  -- explicitly; LLVM 15 otherwise combines the new manager with old TBAA
-  -- pass flags and refuses before code generation.
-  if impl(ghc < 9.10)
-    ghc-options: -optlo-enable-new-pm=0
-
--- ── the tower ────────────────────────────────────────────────────────────────
--- The implementation tower separates the public exact scalar core from the
--- package-private paged-storage kernel. Both feed dcel-internal, followed by
--- build-internal. Public dcel and build components are restrictions of those
--- owners: they reexport lawful modules without copying implementations or
--- publishing trusted constructors. Dual branches directly from the DCEL owner;
--- the apex owns only the genuine overlaps. The graph is acyclic and uses no
--- @.hs-boot@ files.
-
-library core
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-core
-  exposed-modules:
-    Moonlight.Triangulation.Scalar
-    Moonlight.Triangulation.LineSideInfo
-    Moonlight.Triangulation.Internal.Dyadic
-    Moonlight.Triangulation.Internal.ExactRational
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-  ghc-options: -fexpose-all-unfoldings
-
--- Native hexagonal cell algebra is a bottom component: consumers pay for one
--- packed bit per admitted cell and do not acquire DCEL, overlay, Homology, or
--- serialization dependencies merely to manipulate a hexagonal world.
-library hex
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-hex
-  exposed-modules:
-    Moonlight.Hex.Coordinate
-    Moonlight.Hex.Element
-    Moonlight.Hex.Region
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-    , transformers >= 0.6 && < 0.7
-    , vector >= 0.13 && < 0.14
-  ghc-options: -O2 -fexpose-all-unfoldings
-
--- Trusted paged storage is shared by the immutable DCEL and construction
--- owners. It is private because raw page indices and unsafe vector operations
--- are valid only after those owners have admitted array bounds and page order.
-library storage-internal
-  import: shared-properties
-  visibility: private
-  hs-source-dirs: src-core
-  exposed-modules:
-    Moonlight.Triangulation.Internal.PageDirectory
-    Moonlight.Triangulation.Internal.Paged
-    Moonlight.Triangulation.Internal.BoxedPaged
-    Moonlight.Triangulation.Internal.Growable
-    Moonlight.Triangulation.Internal.PackedIndex
-    Moonlight.Triangulation.Internal.FaceQueue
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , vector >= 0.13 && < 0.14
-  ghc-options: -fexpose-all-unfoldings
-
-library dcel-internal
-  import: shared-properties
-  visibility: private
-  hs-source-dirs: src-dcel
-  exposed-modules:
-    Moonlight.Triangulation.Alpha
-    Moonlight.Triangulation.Types
-    Moonlight.Triangulation.Math
-    Moonlight.Triangulation.Exact
-    Moonlight.Triangulation.Simplex
-    Moonlight.Triangulation.CellSet
-    Moonlight.Triangulation.Region
-    Moonlight.Triangulation.Valuation
-    Moonlight.Triangulation.Interop
-    Moonlight.Triangulation.Dcel
-    Moonlight.Triangulation.Canonical
-    Moonlight.Triangulation.Payload
-    Moonlight.Triangulation.JoinSemilattice
-    Moonlight.Triangulation.Handles.HandleDefs
-    Moonlight.Triangulation.Internal.HandleDefs
-    Moonlight.Triangulation.Handles.Dynamic
-    Moonlight.Triangulation.Handles.Scoped
-    Moonlight.Triangulation.Handles.Iterators.CircularIterator
-    Moonlight.Triangulation.Handles.Iterators.DynamicIterators
-    Moonlight.Triangulation.Handles.Iterators.FixedIterators
-    Moonlight.Triangulation.Handles.Iterators.HullIterator
-    Moonlight.Triangulation.PointLocation
-    Moonlight.Triangulation.Validation
-    Moonlight.Triangulation.Internal.Validation
-    Moonlight.Triangulation.FloodFillIterator
-    Moonlight.Triangulation.Internal.FloodFillIterator
-    Moonlight.Triangulation.IntersectionIterator
-    Moonlight.Triangulation.Internal.FaceProbe
-    Moonlight.Triangulation.Internal.CellSet
-    Moonlight.Triangulation.Internal.BoundaryCycle
-    Moonlight.Triangulation.Internal.ExactSegmentEvents
-    Moonlight.Triangulation.Internal.Region.Publication
-    Moonlight.Triangulation.Internal.Region.Types
-    Moonlight.Triangulation.Internal.Predicates
-    Moonlight.Triangulation.Internal.SegmentRelation
-    Moonlight.Triangulation.Internal.Types
-    Moonlight.Triangulation.Internal.Representation
-    Moonlight.Triangulation.Internal.PointIndex
-    Moonlight.Triangulation.Internal.Mutable
-    Moonlight.Triangulation.Internal.OperationState
-    Moonlight.Triangulation.Internal.Tournament
-    Moonlight.Triangulation.Internal.Probe
-    Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-    Moonlight.Triangulation.Internal.DcelOperations.Chain
-    Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite
-    Moonlight.Triangulation.Internal.DcelOperations.FlipRule
-    Moonlight.Triangulation.Internal.DcelOperations.Hull
-    Moonlight.Triangulation.Internal.DcelOperations.Legalize
-    Moonlight.Triangulation.Internal.DcelOperations.Normalize
-    Moonlight.Triangulation.Internal.DcelOperations.Subdivide
-    Moonlight.Triangulation.Internal.DcelOperations.Twin
-    Moonlight.Triangulation.Internal.Canonical
-  other-modules:
-    Moonlight.Triangulation.Internal.Region.Bounds
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , primitive >= 0.9 && < 0.10
-    , vector >= 0.13 && < 0.14
-    , vector-algorithms >= 0.9 && < 0.10
-    , moonlight-planar:core
-    , moonlight-planar:storage-internal
-  ghc-options: -fexpose-all-unfoldings
-
--- Public immutable-DCEL and exact-geometry surface. Every module is the module
--- compiled by dcel-internal; reexporting preserves one type and implementation
--- identity while leaving representation modules hidden.
-library dcel
-  import: shared-properties
-  visibility: public
-  reexported-modules:
-    Moonlight.Triangulation.Alpha
-    , Moonlight.Triangulation.Types
-    , Moonlight.Triangulation.Math
-    , Moonlight.Triangulation.Exact
-    , Moonlight.Triangulation.Simplex
-    , Moonlight.Triangulation.CellSet
-    , Moonlight.Triangulation.Region
-    , Moonlight.Triangulation.Valuation
-    , Moonlight.Triangulation.Interop
-    , Moonlight.Triangulation.Dcel
-    , Moonlight.Triangulation.Canonical
-    , Moonlight.Triangulation.Payload
-    , Moonlight.Triangulation.JoinSemilattice
-    , Moonlight.Triangulation.Handles.HandleDefs
-    , Moonlight.Triangulation.Handles.Dynamic
-    , Moonlight.Triangulation.Handles.Scoped
-    , Moonlight.Triangulation.Handles.Iterators.DynamicIterators
-    , Moonlight.Triangulation.Handles.Iterators.FixedIterators
-    , Moonlight.Triangulation.Handles.Iterators.HullIterator
-    , Moonlight.Triangulation.PointLocation
-    , Moonlight.Triangulation.Validation
-    , Moonlight.Triangulation.FloodFillIterator
-    , Moonlight.Triangulation.IntersectionIterator
-  build-depends:
-    moonlight-planar:dcel-internal
-
--- An ExactCellSet already carries every handle and closure witness needed to
--- interpret it as a generic finite cell complex. This component is the
--- canonical owner of that interpretation and of the alpha-birth lowering into
--- Homology. It deliberately remains outside the portable geometry tower
--- because its Homology dependency requires GHC 9.14.
-library cell-complex
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-cell-complex
-  default-extensions:
-    TypeFamilies
-  exposed-modules:
-    Moonlight.Hex.CellComplex
-    Moonlight.Triangulation.CellComplex
-    Moonlight.Triangulation.LabelledComplex
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-algebra:abstract >= 0.1 && < 0.2
-    , moonlight-homology >= 0.1.0.2 && < 0.2
-    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
-    , moonlight-planar:hex
-    , moonlight-planar:dcel-internal
-    , vector >= 0.13 && < 0.14
-
--- Stable activation labels induce canonical simplices across independently
--- built depth meshes.  This opt-in layer owns the exact alpha ingestion and
--- adjacent-union correspondence witnesses; Homology owns the zigzag reducer.
-library zigzag
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-zigzag
-  exposed-modules:
-    Moonlight.Triangulation.Zigzag
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , moonlight-core >= 0.1 && < 0.2
-    , moonlight-homology >= 0.1.0.3 && < 0.2
-    , moonlight-planar:build
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel
-    , vector >= 0.13 && < 0.14
-
-library build-internal
-  import: shared-properties
-  visibility: private
-  hs-source-dirs: src-build
-  exposed-modules:
-    Moonlight.Triangulation.BulkLoad
-    Moonlight.Triangulation.Internal.BulkLoad
-    Moonlight.Triangulation.Removal
-    Moonlight.Triangulation.Session
-    Moonlight.Triangulation.Internal.Session
-    Moonlight.Triangulation.Cdt
-    Moonlight.Triangulation.Refinement
-    Moonlight.Triangulation.SetAlgebra
-    Moonlight.Triangulation.Internal.Cdt.Build
-    Moonlight.Triangulation.Internal.Cdt.Query
-    Moonlight.Triangulation.Internal.Cdt.Types
-    Moonlight.Triangulation.Internal.Cdt.Union
-    Moonlight.Triangulation.Internal.Join
-    Moonlight.Triangulation.Internal.Join.Seam
-  other-modules:
-    Moonlight.Triangulation.Insertion
-    Moonlight.Triangulation.Internal.Capacity
-    Moonlight.Triangulation.Internal.Cdt.Admission
-    Moonlight.Triangulation.Internal.Cdt.Batch
-    Moonlight.Triangulation.Internal.Cdt.Combinators
-    Moonlight.Triangulation.Internal.Cdt.Corridor
-    Moonlight.Triangulation.Internal.Cdt.Corridor.Trace
-    Moonlight.Triangulation.Internal.Cdt.Recovery
-    Moonlight.Triangulation.Internal.Cdt.Region
-    Moonlight.Triangulation.Internal.Cdt.Segment
-    Moonlight.Triangulation.Internal.Cdt.Site
-    Moonlight.Triangulation.Internal.Cdt.Split
-    Moonlight.Triangulation.Internal.Excision
-    Moonlight.Triangulation.Internal.Location
-    Moonlight.Triangulation.Internal.CircleSweep
-    Moonlight.Triangulation.Internal.Refinement
-    Moonlight.Triangulation.Internal.Transaction
-    Moonlight.Triangulation.Internal.Join.Plan
-    Moonlight.Triangulation.Internal.Join.SiteSet
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , primitive >= 0.9 && < 0.10
-    , vector >= 0.13 && < 0.14
-    , vector-algorithms >= 0.9 && < 0.10
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:storage-internal
-  ghc-options: -fexpose-all-unfoldings
-
--- Public construction surface over the private kernel. Trusted admitted-input
--- and local-transaction entries live only in build-internal; the modules below
--- are their typed, immutable restrictions.
-library build
-  import: shared-properties
-  visibility: public
-  reexported-modules:
-    Moonlight.Triangulation.BulkLoad
-    , Moonlight.Triangulation.Removal
-    , Moonlight.Triangulation.Session
-    , Moonlight.Triangulation.Cdt
-    , Moonlight.Triangulation.Refinement
-    , Moonlight.Triangulation.SetAlgebra
-  build-depends:
-    moonlight-planar:build-internal
-
--- Concurrency is an effect boundary over the pure build planner. Keeping it in
--- its own sublibrary prevents @async@ from infecting the geometry core.
-library parallel
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-parallel
-  exposed-modules:
-    Moonlight.Triangulation.Parallel
-  build-depends:
-    base >= 4.19 && < 5
-    , async >= 2.2 && < 2.3
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-planar:build-internal
-    , moonlight-planar:dcel-internal
-  ghc-options: -fexpose-all-unfoldings
-
--- Its own sublibrary so that @binary@ and @bytestring@ stay out of the minimal
--- geometry core.  This is a dependency boundary, not a size split; it replaces
--- the former @serialization@ flag, which could not be checked by a single build.
-library serialize
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-serialize
-  exposed-modules:
-    Moonlight.Hex.Serialization
-    Moonlight.Triangulation.Serialization
-  build-depends:
-    base >= 4.19 && < 5
-    , binary >= 0.8 && < 0.9
-    , bytestring >= 0.12 && < 0.13
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , transformers >= 0.6 && < 0.7
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar:hex
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:storage-internal
-  -- The @Binary@ instances for the identifier and point types are orphans by
-  -- construction: the types belong to @dcel@ and the class to @binary@, and the
-  -- whole purpose of this component is that neither one has to know about the
-  -- other.  Rehoming them would pull @binary@ into the core and defeat the split.
-  ghc-options: -fexpose-all-unfoldings -Wno-orphans
-
-library dual
-  import: shared-properties
-  visibility: public
-  hs-source-dirs: src-dual
-  exposed-modules:
-    Moonlight.Triangulation.Voronoi
-    Moonlight.Triangulation.Voronoi.Handles
-    Moonlight.Triangulation.Interpolation
-  other-modules:
-    Moonlight.Triangulation.Internal.InterpolationWorkspace
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-    , primitive >= 0.9 && < 0.10
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-  ghc-options: -fexpose-all-unfoldings
-
--- Package-private overlay machine room. The main library and package-owned
--- oracle tests share these exact modules; external consumers receive only the
--- public embedding certifier and Overlay equations reexported across this wall.
-library overlay-internal
-  import: shared-properties
-  visibility: private
-  hs-source-dirs: src-overlay
-  exposed-modules:
-    Moonlight.Triangulation.Embedding
-    Moonlight.Triangulation.Internal.Overlay.Arrangement
-    Moonlight.Triangulation.Internal.Overlay.Resident
-    Moonlight.Triangulation.Internal.Overlay.Types
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:storage-internal
-
-library
-  import: shared-properties
-  hs-source-dirs:
-    src-public
-  exposed-modules:
-    Moonlight.Hex.Planar
-    Moonlight.Triangulation
-    Moonlight.Triangulation.HintGenerator
-    Moonlight.Triangulation.Minkowski
-    Moonlight.Triangulation.Overlay
-    Moonlight.Triangulation.PowerDiagram
-    Moonlight.Triangulation.RegularAlpha
-  -- The default library is the broad surface. Scoped/owning handles are the safe
-  -- path; Dcel is the explicitly unchecked fixed-index kernel. These are the
-  -- exact focused-component modules, not facade copies.
-  reexported-modules:
-    Moonlight.Triangulation.Alpha
-    , Moonlight.Triangulation.Types
-    , Moonlight.Triangulation.Math
-    , Moonlight.Triangulation.Exact
-    , Moonlight.Triangulation.CellSet
-    , Moonlight.Triangulation.Region
-    , Moonlight.Triangulation.Valuation
-    , Moonlight.Triangulation.Interop
-    , Moonlight.Triangulation.Dcel
-    , Moonlight.Triangulation.Canonical
-    , Moonlight.Triangulation.Payload
-    , Moonlight.Triangulation.JoinSemilattice
-    , Moonlight.Triangulation.Handles.HandleDefs
-    , Moonlight.Triangulation.Handles.Dynamic
-    , Moonlight.Triangulation.Handles.Scoped
-    , Moonlight.Triangulation.Handles.Iterators.DynamicIterators
-    , Moonlight.Triangulation.Handles.Iterators.FixedIterators
-    , Moonlight.Triangulation.Handles.Iterators.HullIterator
-    , Moonlight.Triangulation.PointLocation
-    , Moonlight.Triangulation.Validation
-    , Moonlight.Triangulation.FloodFillIterator
-    , Moonlight.Triangulation.IntersectionIterator
-    , Moonlight.Triangulation.BulkLoad
-    , Moonlight.Triangulation.Removal
-    , Moonlight.Triangulation.Session
-    , Moonlight.Triangulation.Cdt
-    , Moonlight.Triangulation.Refinement
-    , Moonlight.Triangulation.SetAlgebra
-    , Moonlight.Triangulation.Voronoi
-    , Moonlight.Triangulation.Voronoi.Handles
-    , Moonlight.Triangulation.Interpolation
-    , Moonlight.Triangulation.Embedding
-  other-modules:
-    Moonlight.Triangulation.Internal.Minkowski.Convex
-    Moonlight.Triangulation.Internal.Minkowski.Types
-    Moonlight.Triangulation.Internal.PowerDiagram.Edit
-    Moonlight.Triangulation.Internal.PowerDiagram.Generator
-    Moonlight.Triangulation.Internal.PowerDiagram.Hull
-    Moonlight.Triangulation.Internal.PowerDiagram.Locality
-    Moonlight.Triangulation.Internal.PowerDiagram.Model
-    Moonlight.Triangulation.Internal.PowerDiagram.Projection
-    Moonlight.Triangulation.Internal.PowerDiagram.Section
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar:hex
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:dual
-    , moonlight-planar:overlay-internal
-    , moonlight-planar:storage-internal
-  ghc-options: -fexpose-all-unfoldings
-
-library ffi
-  import: shared-properties
-  visibility: private
-  hs-source-dirs: ffi/abi
-  exposed-modules:
-    Moonlight.Triangulation.Foreign.Boundary
-    Moonlight.Triangulation.Foreign.Contract
-    Moonlight.Triangulation.Foreign.Contract.Render
-    Moonlight.Triangulation.Foreign.Mesh
-    Moonlight.Triangulation.Foreign.Morphology
-    Moonlight.Triangulation.Foreign.Obstruction
-    Moonlight.Triangulation.Foreign.Region
-  build-depends:
-    base >= 4.19 && < 5
-    , containers >= 0.8 && < 0.9
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar
-    , moonlight-planar:build-internal
-    , moonlight-planar:dcel-internal
-
-foreign-library moonlight-triangulation-c
-  import: shared-properties
-  type: native-shared
-  hs-source-dirs: ffi/exports
-  other-modules:
-    Moonlight.Triangulation.Foreign.Exports
-  c-sources:
-    ffi/cbits/moonlight_runtime.c
-  include-dirs:
-    ffi/include
-  install-includes:
-    moonlight_triangulation.h
-  build-depends:
-    base >= 4.19 && < 5
-    , moonlight-planar:ffi >= 1.0 && < 1.1
-  ghc-options: -threaded
-  if os(windows)
-    options: standalone
-    mod-def-file: ffi/cbits/moonlight-triangulation.def
-  else
-    extra-libraries: pthread
-  if os(linux)
-    lib-version-info: 2:0:0
-
-executable moonlight-triangulation-ffi-contract
-  import: shared-properties
-  hs-source-dirs: ffi/generator
-  main-is: Main.hs
-  build-depends:
-    base >= 4.19 && < 5
-    , directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , moonlight-planar:ffi >= 1.0 && < 1.1
-
--- ── test slices ──────────────────────────────────────────────────────────────
-
--- Each slice is an ATOM: a @common@ stanza binding a spec module to the
--- dependencies and RTS options that module requires.  A suite is then a choice
--- of atoms and nothing more.  The coherence component below imports every atom
--- and therefore checks their combined module, instance, and dependency surface
--- without running every behavioral suite a second time.
-
-common triangulation-test-properties
-  import: shared-properties
-  ghc-options: -O0
-  build-depends:
-    base >= 4.19 && < 5
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar
-
-common triangulation-test-support-slice
-  other-modules: Support
-
-common triangulation-native-test-slice
-  other-modules:
-    Moonlight.Triangulation.NativeSpec
-    Moonlight.Triangulation.FilteredPredicateOptimizationSpec
-    Moonlight.Triangulation.ExactEmbeddingSpec
-    Moonlight.Triangulation.OverlaySpec
-    Moonlight.Triangulation.RegionSpec
-  -- The RTS options this module needs are named on the SUITES rather than
-  -- here, which is the one place the slice-owns-its-requirements rule cannot
-  -- hold.  @-with-rtsopts@ is CONCATENATED by GHC across repetitions, so a
-  -- suite importing two slices that both name it links a binary whose RTS
-  -- string is the two spliced together — @-N4 -T-N4 -T@ — which the RTS
-  -- discards whole, silently disarming both.  It must appear exactly once per
-  -- suite, so @triangulation-suite-rtsopts@ below carries it.
-  ghc-options: -threaded -rtsopts
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , primitive >= 0.9 && < 0.10
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:dual
-    , moonlight-planar:overlay-internal
-    , moonlight-planar:storage-internal
-
-common triangulation-serialization-test-slice
-  other-modules: Moonlight.Triangulation.SerializationSpec
-  build-depends:
-    binary >= 0.8 && < 0.9
-    , bytestring >= 0.12 && < 0.13
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:serialize
-
--- The operand meshes both algebra slices are stated over.  It sits at the
--- surface tier with them, so the fixtures cannot quietly acquire a
--- machine-room dependency that the law slice would then inherit.
-common triangulation-algebra-fixture-slice
-  other-modules: Moonlight.Triangulation.AlgebraFixtures
-  build-depends: containers >= 0.8 && < 0.9
-
--- The finite-set laws, stated against the facade a caller has.
-common triangulation-algebra-law-slice
-  other-modules: Moonlight.Triangulation.AlgebraSpec
-  build-depends: containers >= 0.8 && < 0.9
-
--- Exact planar Boolean laws, stated exclusively through the public facade.
-common triangulation-region-algebra-law-slice
-  other-modules: Moonlight.Triangulation.RegionAlgebraSpec
-  build-depends: containers >= 0.8 && < 0.9
-
--- Exact intrinsic-volume laws over the resident cell carrier and published
--- region view.
-common triangulation-valuation-law-slice
-  other-modules: Moonlight.Triangulation.ValuationSpec
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
-common triangulation-minkowski-law-slice
-  other-modules: Moonlight.Triangulation.MinkowskiSpec
-  build-depends:
-    moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
--- A test-only endpoint-reconstruction oracle demonstrating why the production
--- clipping owner retains original affine source lines.
-common triangulation-exact-clip-retention-law-slice
-  other-modules: Moonlight.Triangulation.ExactClipRetentionSpec
-  build-depends:
-    moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
-common triangulation-power-diagram-law-slice
-  other-modules: Moonlight.Triangulation.PowerDiagramSpec
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
-common triangulation-layer-operations-law-slice
-  other-modules: Moonlight.Triangulation.LayerOperationsSpec
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
--- The agreement between the seam schedule and the reference rebuild.  It names
--- an internal schedule, so it reaches below the wall and travels with the
--- schedule it names: a replacement kernel carries its own copy of this.
-common triangulation-algebra-schedule-slice
-  other-modules: Moonlight.Triangulation.ScheduleAgreementSpec
-  build-depends: moonlight-planar:build-internal
-
-common triangulation-parallel-test-slice
-  other-modules: Moonlight.Triangulation.ParallelSpec
-  ghc-options: -threaded -rtsopts
-  build-depends: moonlight-planar:parallel
-
-test-suite moonlight-triangulation-cell-complex-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs: test/cell-complex
-  default-extensions:
-    TypeFamilies
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , moonlight-homology >= 0.1.0.2 && < 0.2
-    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
-    , moonlight-planar:hex
-    , moonlight-planar:core
-    , moonlight-planar:build-internal
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-    , tasty >= 1.4 && < 1.6
-    , tasty-hunit >= 0.10 && < 0.11
-    , vector >= 0.13 && < 0.14
-
--- Public activation ingestion, labelled adjacent-union correspondences, and
--- exact non-monotone persistence, including its compiled documentation path.
-test-suite moonlight-triangulation-zigzag-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/zigzag
-    docs/examples
-  other-modules:
-    Moonlight.Triangulation.Example.ActivationZigzag
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , moonlight-homology >= 0.1.0.3 && < 0.2
-    , moonlight-planar:dcel
-    , moonlight-planar:zigzag
-    , tasty >= 1.4 && < 1.6
-    , tasty-hunit >= 0.10 && < 0.11
-    , vector >= 0.13 && < 0.14
-
--- Canonical documentation examples compile exclusively against the public
--- facade and retain their expected semantic summaries as executable checks.
-test-suite moonlight-triangulation-examples-test
-  import: triangulation-test-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/examples
-    docs/examples
-  other-modules:
-    Moonlight.Triangulation.Example.AlphaBoundary
-    Moonlight.Triangulation.Example.ConstrainedRegion
-    Moonlight.Triangulation.Example.PlanarRegion
-    Moonlight.Triangulation.Example.SetAlgebra
-  build-depends: containers >= 0.8 && < 0.9
-
--- One behavioral owner for exact clipping, power cells, morphology, affine
--- envelopes, coverage, n-ary overlay, and mass. The law slices remain
--- independently reusable by the compile-only coherence component.
-test-suite moonlight-triangulation-exact-planar-test
-  import:
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-algebra-fixture-slice,
-    triangulation-exact-clip-retention-law-slice,
-    triangulation-minkowski-law-slice,
-    triangulation-power-diagram-law-slice,
-    triangulation-layer-operations-law-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/exact-planar
-    test/algebra
-    test/support
-
--- A real external shape over only the two focused public components. It proves
--- that immutable observation and construction/editing compose without either
--- the broad facade or a private implementation token.
-test-suite moonlight-triangulation-public-components-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs: test/public-components
-  build-depends:
-    base >= 4.19 && < 5
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar:dcel
-    , moonlight-planar:build
-
--- Native coordinate, element, finite-set, descent/gluing, exact-publication,
--- and serialization laws through public component boundaries only.
-test-suite moonlight-planar-hex-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs: test/hex
-  ghc-options: -O0
-  build-depends:
-    base >= 4.19 && < 5
-    , bytestring >= 0.12 && < 0.13
-    , containers >= 0.8 && < 0.9
-    , moonlight-planar
-    , moonlight-planar:hex
-    , moonlight-planar:serialize
-    , vector >= 0.13 && < 0.14
-
--- One pure, source-grounded projection of the alpha, dual, interpolation,
--- planar-algebra, and persistence owners. The test shares the internal exhibit
--- module; neither component creates a public visualization API.
-test-suite moonlight-triangulation-persistence-rose-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/persistence-rose
-    docs/persistence-rose
-  other-modules:
-    Moonlight.Triangulation.Exhibit.PersistenceRose
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , bytestring >= 0.12 && < 0.13
-    , containers >= 0.6 && < 0.9
-    , moonlight-homology >= 0.1.0.2 && < 0.2
-    , moonlight-planar
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:dual
-    , tasty >= 1.4 && < 1.6
-    , tasty-hunit >= 0.10 && < 0.11
-    , text >= 2.0 && < 2.2
-    , vector >= 0.13 && < 0.14
-
--- The single statement of the RTS environment every suite runs under: -N so
--- that the concurrent tournament has more than one capability to be scheduled
--- onto, -T so that the filtered-predicate test can read allocation counters.
--- It is one stanza rather than a line on each slice because @-with-rtsopts@
--- concatenates rather than replaces, so naming it twice in one suite produces
--- a spliced string the RTS rejects entirely.  Every behavioral suite that
--- needs those capabilities imports this exactly once; the compile-only
--- coherence suite deliberately installs no runtime defaults.
-common triangulation-suite-rtsopts
-  ghc-options: -threaded -rtsopts "-with-rtsopts=-N4 -T"
-
-test-suite moonlight-triangulation-native-test
-  import:
-    triangulation-suite-rtsopts,
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-native-test-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/native
-    test/support
-
-test-suite moonlight-triangulation-serialization-test
-  import:
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-serialization-test-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/serialization
-    test/support
-
-test-suite moonlight-triangulation-algebra-test
-  import:
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-algebra-fixture-slice,
-    triangulation-algebra-law-slice,
-    triangulation-region-algebra-law-slice,
-    triangulation-valuation-law-slice,
-    triangulation-algebra-schedule-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/algebra
-    test/support
-
-test-suite moonlight-triangulation-parallel-test
-  import:
-    triangulation-suite-rtsopts,
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-algebra-fixture-slice,
-    triangulation-parallel-test-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/parallel
-    test/algebra
-    test/support
-
-test-suite moonlight-triangulation-ffi-test
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs: test/ffi
-  build-depends:
-    base >= 4.19 && < 5
-    , moonlight-planar:ffi
-
--- This component owns only cross-slice compile coherence.  Behavioral ownership
--- remains in focused suites.  The shared test-properties stanza keeps
--- every test body at @-O0@, so the union retains type/module/instance collision
--- detection without paying to optimize a second copy of every test body.
-test-suite moonlight-triangulation-coherence-test
-  import:
-    triangulation-test-properties,
-    triangulation-test-support-slice,
-    triangulation-native-test-slice,
-    triangulation-serialization-test-slice,
-    triangulation-algebra-fixture-slice,
-    triangulation-algebra-law-slice,
-    triangulation-region-algebra-law-slice,
-    triangulation-valuation-law-slice,
-    triangulation-minkowski-law-slice,
-    triangulation-exact-clip-retention-law-slice,
-    triangulation-power-diagram-law-slice,
-    triangulation-layer-operations-law-slice,
-    triangulation-algebra-schedule-slice,
-    triangulation-parallel-test-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    test/coherence
-    test/native
-    test/serialization
-    test/algebra
-    test/parallel
-    test/support
-
--- ── benchmark slices ─────────────────────────────────────────────────────────
--- Deliberately not @tasty-bench@: these report allocated bytes and the work
--- counters the library keeps about itself — hierarchy walk steps, refinement
--- queue pops, bytes per interpolation query — and a wall-clock harness cannot
--- express any of them.  @core@ and @serialize@ carry no benchmark today; that is
--- stated rather than filled with an invented one.
-
-common triangulation-benchmark-properties
-  import: shared-properties
-  -- The allocation arms read GHC.Stats, which is dark without -T.  As with the
-  -- test suites, @-with-rtsopts@ is CONCATENATED by GHC across repetitions, so
-  -- it must appear exactly once per benchmark: this stanza is that one place,
-  -- and no benchmark slice may name it again.
-  ghc-options: -threaded -rtsopts "-with-rtsopts=-T"
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-    , vector >= 0.13 && < 0.14
-    , moonlight-planar
-
-common triangulation-benchmark-support-slice
-  other-modules:
-    BenchMeasure
-    BenchSupport
-  build-depends: moonlight-planar:dcel-internal
-
-common triangulation-build-benchmark-slice
-  other-modules: Moonlight.Triangulation.BuildBench
-  build-depends:
-    primitive >= 0.9 && < 0.10
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:ffi
-
-common triangulation-dcel-benchmark-slice
-  other-modules: Moonlight.Triangulation.DcelBench
-  build-depends: moonlight-planar:dcel-internal
-
-common triangulation-dual-benchmark-slice
-  other-modules: Moonlight.Triangulation.DualBench
-  build-depends: moonlight-planar:dual
-
-common triangulation-join-benchmark-slice
-  other-modules: Moonlight.Triangulation.JoinBench
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:storage-internal
-
-common triangulation-region-benchmark-slice
-  other-modules: Moonlight.Triangulation.RegionBench
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , moonlight-planar:core
-    , moonlight-planar:dcel-internal
-
--- The package-owned external construction board deliberately uses tasty-bench:
--- unlike the native work-counter slices above, its only lawful common metric
--- across Haskell and four Rust referents is elapsed or CPU time.  The Rust
--- crate is a foreign adapter; Moonlight owns the case algebra, agreement
--- descent, timing, receipt parser, and derived pictures here.
-common triangulation-delaunay-compare-properties
-  import: shared-properties
-  ghc-options: -O2
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-planar:build-internal
-    , moonlight-planar:dcel-internal
-
-executable moonlight-triangulation-delaunay-compare
-  import: triangulation-delaunay-compare-properties
-  ghc-options: -threaded -rtsopts
-  main-is: Main.hs
-  hs-source-dirs: docs/delaunay-compare
-  other-modules:
-    Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-    Moonlight.Triangulation.Bench.DelaunayCompare.Native
-    Moonlight.Triangulation.Bench.DelaunayCompare.Suite
-  build-depends:
-    directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , process >= 1.6 && < 1.7
-    , tasty-bench >= 0.3 && < 0.6
-    , transformers >= 0.5 && < 0.7
-    , unix >= 2.8 && < 2.9
-    , vector >= 0.13 && < 0.14
-
-executable moonlight-triangulation-delaunay-pictures
-  import: triangulation-delaunay-compare-properties
-  main-is: PictureMain.hs
-  hs-source-dirs: docs/delaunay-compare
-  other-modules:
-    Moonlight.Triangulation.Bench.DelaunayCompare.Domain
-    Moonlight.Triangulation.Bench.DelaunayCompare.Picture
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , transformers >= 0.5 && < 0.7
-
-executable moonlight-triangulation-category-observatory-export
-  import: shared-properties
-  main-is: Main.hs
-  hs-source-dirs: docs/category-observatory
-  other-modules:
-    Moonlight.Triangulation.Exhibit.CategoryObservatory
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Category
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Geometry
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Identifier
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Model
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Nerve
-    Moonlight.Triangulation.Exhibit.CategoryObservatory.Picture
-  ghc-options: -Wall -Wcompat
-  default-extensions:
-    TypeFamilies
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    aeson >= 2.2 && < 2.4
-    , base >= 4.22 && < 5
-    , bytestring >= 0.12 && < 0.13
-    , containers >= 0.6 && < 0.9
-    , directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , moonlight-category >= 1.1.0.0 && < 1.2
-    , moonlight-category:simplicial >= 1.1.0.0 && < 1.2
-    , moonlight-homology:cell-category >= 0.1.0.2 && < 0.2
-    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
-    , moonlight-planar:build-internal
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-    , transformers >= 0.5 && < 0.7
-    , vector >= 0.13 && < 0.14
-
-executable moonlight-triangulation-persistence-rose
-  import: shared-properties
-  main-is: Main.hs
-  hs-source-dirs: docs/persistence-rose
-  other-modules:
-    Moonlight.Triangulation.Exhibit.PersistenceRose
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , bytestring >= 0.12 && < 0.13
-    , containers >= 0.6 && < 0.9
-    , directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , moonlight-homology >= 0.1.0.2 && < 0.2
-    , moonlight-planar
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:dual
-    , text >= 2.0 && < 2.2
-    , transformers >= 0.5 && < 0.7
-    , vector >= 0.13 && < 0.14
-
--- These referent instruments inspect package-internal handles and thaw/freeze
--- lanes. They belong to the triangulation package rather than reopening its
--- private construction boundary through a second package.
-executable moonlight-triangulation-spade-referent
-  default-language: GHC2024
-  ghc-options:
-    -Wall
-    -Wcompat
-    -Wincomplete-record-updates
-    -Wincomplete-uni-patterns
-    -Wredundant-constraints
-    -Wpartial-fields
-    -O2
-    -rtsopts
-  default-extensions:
-    BangPatterns
-    LambdaCase
-    NumericUnderscores
-  hs-source-dirs: bench/spade-compare/hs
-  main-is: Main.hs
-  -- Main is the entry and the lane dispatch; every workload, gate and
-  -- diagnostic sits in the domain module that owns it. The domains depend on
-  -- SpadeCompare.Support and on nothing else of each other, so they compile in
-  -- parallel and a lane edit rebuilds one slice.
-  other-modules:
-    Moonlight.Triangulation.Bench.SpadeCompare.Lane
-    Moonlight.Triangulation.Bench.SpadeCompare.Input
-    Moonlight.Triangulation.Bench.SpadeCompare.Timing
-    Moonlight.Triangulation.Bench.SpadeCompare.Support
-    Moonlight.Triangulation.Bench.SpadeCompare.Delaunay
-    Moonlight.Triangulation.Bench.SpadeCompare.Removal
-    Moonlight.Triangulation.Bench.SpadeCompare.Hierarchy
-    Moonlight.Triangulation.Bench.SpadeCompare.Constraint
-    Moonlight.Triangulation.Bench.SpadeCompare.Interpolation
-    Moonlight.Triangulation.Bench.SpadeCompare.Voronoi
-    Moonlight.Triangulation.Bench.SpadeCompare.Dcel
-    Moonlight.Triangulation.Bench.SpadeCompare.Intersection
-    Moonlight.Triangulation.Bench.SpadeCompare.Gates
-  build-depends:
-    base >= 4.22 && < 5
-    , deepseq >= 1.4 && < 1.6
-    , directory >= 1.3 && < 1.4
-    , filepath >= 1.4 && < 1.6
-    , vector >= 0.13 && < 0.14
-    , primitive >= 0.9 && < 0.10
-    , moonlight-planar
-    , moonlight-planar:dcel-internal
-    , moonlight-planar:build-internal
-    , moonlight-planar:dual
-
--- The alpha comparison is deliberately opt-in: ordinary scorecard listing and
--- replay do not inherit Homology merely because this diagnostic exists beside
--- the canonical board.
-executable moonlight-triangulation-alpha-spade-referent
-  default-language: GHC2024
-  ghc-options:
-    -Wall
-    -Wcompat
-    -Wincomplete-record-updates
-    -Wincomplete-uni-patterns
-    -Wredundant-constraints
-    -Wpartial-fields
-    -O2
-    -rtsopts
-  default-extensions:
-    BangPatterns
-    LambdaCase
-    NumericUnderscores
-  hs-source-dirs:
-    bench/spade-compare/hs-alpha
-    bench/spade-compare/hs
-  main-is: Main.hs
-  other-modules:
-    Moonlight.Triangulation.Bench.SpadeCompare.Alpha
-    Moonlight.Triangulation.Bench.SpadeCompare.Input
-    Moonlight.Triangulation.Bench.SpadeCompare.Lane
-    Moonlight.Triangulation.Bench.SpadeCompare.Timing
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , deepseq >= 1.4 && < 1.6
-    , vector >= 0.13 && < 0.14
-    , moonlight-homology >= 0.1 && < 0.2
-    , moonlight-planar:build-internal
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-
--- One exact Delaunay alpha filtration versus independently restricting and
--- reducing the same chain complex at every critical radius. Construction of
--- the shared Delaunay geometry is outside all timed lanes.
-benchmark moonlight-triangulation-alpha-bench
-  import:
-    shared-properties,
-    triangulation-benchmark-support-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/alpha
-    bench/support
-  ghc-options: -threaded -rtsopts "-with-rtsopts=-T"
-  if impl(ghc < 9.14)
-    buildable: False
-  build-depends:
-    base >= 4.22 && < 5
-    , containers >= 0.6 && < 0.9
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-homology >= 0.1.0.2 && < 0.2
-    , moonlight-planar
-    , moonlight-planar:build-internal
-    , moonlight-planar:cell-complex
-    , moonlight-planar:dcel-internal
-    , vector >= 0.13 && < 0.14
-
--- Exact bounded power-cell construction.  Tasty-bench owns the timing
--- distribution; the benchmark's one-shot report derives boundary/intersection and
--- rational-width receipts from the public power-diagram result.
-benchmark moonlight-triangulation-power-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/power
-    bench/support
-  other-modules:
-    Moonlight.Triangulation.PowerBench
-  build-depends:
-    containers >= 0.6 && < 0.9
-    , tasty-bench >= 0.3 && < 0.6
-
--- Exact affine-envelope construction, balanced n-ary refinement, and direct
--- cell analytics against the public compositions callers would otherwise
--- materialize by hand.
-benchmark moonlight-triangulation-layer-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/layer
-    bench/support
-  other-modules:
-    Moonlight.Triangulation.LayerBench
-  build-depends:
-    containers >= 0.8 && < 0.9
-    , tasty-bench >= 0.3 && < 0.6
-
-benchmark moonlight-triangulation-build-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-build-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/build
-    bench/support
-
-benchmark moonlight-triangulation-dcel-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-dcel-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/dcel
-    bench/support
-
-benchmark moonlight-triangulation-dual-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-dual-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/dual
-    bench/support
-
-benchmark moonlight-triangulation-join-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-join-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/join
-    bench/support
-
-benchmark moonlight-triangulation-publication-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-join-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: PublicationMain.hs
-  hs-source-dirs:
-    bench/publication
-    bench/join
-    bench/support
-
-benchmark moonlight-triangulation-region-bench
-  import:
-    triangulation-benchmark-properties,
-    triangulation-benchmark-support-slice,
-    triangulation-region-benchmark-slice
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/region
-    bench/support
-
--- Packed native finite-set kernels. The measurement module is shared, but the
--- component depends on no triangulation, DCEL, overlay, or Homology library.
-benchmark moonlight-planar-hex-bench
-  import: shared-properties
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
-  hs-source-dirs:
-    bench/hex
-    bench/support
-  other-modules: BenchMeasure
-  ghc-options: -O2 -threaded -rtsopts "-with-rtsopts=-T"
-  build-depends:
-    base >= 4.19 && < 5
-    , deepseq >= 1.5 && < 1.6
-    , moonlight-planar:hex
-
-benchmark moonlight-triangulation-bench
+version:             1.1.0.0
+synopsis:            Native hex regions, Delaunay meshes, and exact planar algebra.
+description:         Native packed hexagonal cell regions and Delaunay and
+                     constrained Delaunay triangulation as lawful
+                     finite-set algebras, together with exact rational planar
+                     regions, labelled common refinement, intrinsic valuations,
+                     and polygonal Minkowski morphology. A mesh is a value of
+                     its site set, so
+                     union, intersection and difference return triangulations
+                     and refinement composes after them rather than replacing
+                     them. One structure-of-arrays half-edge mesh carries the
+                     constrained and unconstrained layers. Private finite-DCEL
+                     and construction kernels own trusted representations and
+                     admitted fast paths. Public dcel and build sublibraries
+                     expose their lawful immutable observations, typed queries,
+                     construction, incremental edits, constraint recovery, and
+                     refinement without duplicating those owners. Further opt-in
+                     sublibraries expose the exact scalar core, Voronoi and
+                     natural-neighbour dual, bounded concurrent join interpreter,
+                     and versioned serialization surface beside the dependency-light
+                     exact planar library; construction statistics and session
+                     verbs live in the opt-in build component. The
+                     dependency-light hex component provides axial
+                     elements, arithmetic neighbours, wordwise Boolean algebra
+                     and morphology, components, shortest distances,
+                     restriction, and overlap-compatible gluing without DCEL or
+                     polygon construction.
+                     On GHC 9.14, a public cell-complex component interprets
+                     admitted exact cell selections for Homology and Category
+                     and lowers exact Delaunay alpha filtrations into persistent
+                     homology; an opt-in zigzag component tracks exact topology
+                     across independently sampled, non-nested labelled depths.
+                     Failure is values: every refusal names its witness.
+license:             MIT
+license-file:        LICENSE
+author:              Blue Rose
+maintainer:          rosaliafialkova@gmail.com
+copyright:           (c) 2026 Blue Rose
+category:            Geometry, Math
+homepage:            https://github.com/PaleRoses/moonlight
+bug-reports:         https://github.com/PaleRoses/moonlight/issues
+build-type:          Simple
+tested-with:         GHC == 9.14.1
+extra-doc-files:
+  README.md
+  CHANGELOG.md
+  docs/README.md
+  docs/development.md
+  docs/activation-zigzag.md
+  docs/regular-site-algebra.md
+  docs/weighted-alpha.md
+  docs/examples/README.md
+  docs/mesh-publication.md
+  docs/category-observatory/README.md
+  docs/category-observatory/moonlight-triangulation-category-observatory.svg
+  docs/delaunay-compare/README.md
+  docs/delaunay-compare/results/moonlight-delaunay-compare-small.svg
+  docs/persistence-rose/alpha-eclipse.md
+  docs/persistence-rose/README.md
+  docs/persistence-rose/moonlight-triangulation-alpha-eclipse.svg
+  docs/persistence-rose/moonlight-triangulation-persistence-rose.svg
+  docs/persistence-rose/moonlight-triangulation-persistence-rose.gif
+  docs/noctilucent-garden/README.md
+  docs/noctilucent-garden/noctilucent-garden.svg
+  docs/moonblade/README.md
+  docs/moonblade/moonblade.svg
+  docs/knight-rig/README.md
+extra-source-files:
+  weeder.toml
+  docs/moonblade/rig.py
+  docs/knight-rig/rig.py
+  docs/knight-rig/package.py
+  test/public-components/compile-fail/*.hs
+  -- The retained CSV, source receipt, and big-point SVG remain beside the
+  -- benchmark but are not package inputs. The small documentation projection
+  -- is the sole published chart and is declared above.
+  docs/delaunay-compare/rust/Cargo.lock
+  docs/delaunay-compare/rust/Cargo.toml
+  docs/delaunay-compare/rust/src/lib.rs
+  ffi/include/moonlight_triangulation.h
+  ffi/bindings/README.md
+  ffi/bindings/python/pyproject.toml
+  ffi/bindings/python/src/moonlight_triangulation/__init__.py
+  ffi/bindings/python/src/moonlight_triangulation/_binding.py
+  ffi/bindings/python/src/moonlight_triangulation/_native_generated.py
+  ffi/bindings/python/src/moonlight_triangulation/errors.py
+  ffi/bindings/python/src/moonlight_triangulation/values.py
+  ffi/bindings/python/tests/test_binding.py
+  ffi/bindings/rust/Cargo.toml
+  ffi/bindings/rust/build.rs
+  ffi/bindings/rust/src/binding.rs
+  ffi/bindings/rust/src/error.rs
+  ffi/bindings/rust/src/lib.rs
+  ffi/bindings/rust/src/raw_generated.rs
+  ffi/bindings/rust/src/values.rs
+  ffi/bindings/rust/tests/binding.rs
+  ffi/bindings/typescript/README.md
+  ffi/bindings/typescript/package.json
+  ffi/bindings/typescript/pnpm-workspace.yaml
+  ffi/bindings/typescript/tsconfig.json
+  ffi/bindings/typescript/src/failure.ts
+  ffi/bindings/typescript/src/index.ts
+  ffi/bindings/typescript/src/internal/call.ts
+  ffi/bindings/typescript/src/internal/decode.ts
+  ffi/bindings/typescript/src/internal/native.generated.ts
+  ffi/bindings/typescript/src/internal/resource.ts
+  ffi/bindings/typescript/src/mesh.ts
+  ffi/bindings/typescript/src/moonlight.ts
+  ffi/bindings/typescript/src/morphology.ts
+  ffi/bindings/typescript/src/region.ts
+  ffi/bindings/typescript/src/resource.ts
+  ffi/bindings/typescript/src/values.ts
+  ffi/bindings/typescript/src/wire.generated.ts
+  ffi/bindings/typescript/test/binding.test.ts
+
+source-repository head
+  type:     git
+  location: https://github.com/PaleRoses/moonlight.git
+  subdir:   moonlight-planar
+
+source-repository this
+  type:     git
+  location: https://github.com/PaleRoses/moonlight.git
+  tag:      moonlight-planar-1.1.0.0
+  subdir:   moonlight-planar
+
+flag warnings-as-errors
+  description: Treat compiler warnings as errors in package validation.
+  default: False
+  manual: True
+
+common shared-properties
+  default-language: GHC2021
+  default-extensions:
+    DerivingStrategies
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Widentities
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+    -Wpartial-fields
+    -Wno-missing-import-lists
+  if flag(warnings-as-errors)
+    ghc-options: -Werror
+  -- The hot modules that request LLVM O3 replace GHC's default optimiser
+  -- recipe. GHC 9.8 therefore needs its supported legacy pass manager stated
+  -- explicitly; LLVM 15 otherwise combines the new manager with old TBAA
+  -- pass flags and refuses before code generation.
+  if impl(ghc < 9.10)
+    ghc-options: -optlo-enable-new-pm=0
+
+-- ── the tower ────────────────────────────────────────────────────────────────
+-- The implementation tower separates the trusted exact scalar core from the
+-- package-private paged-storage kernel. Both feed dcel-internal, followed by
+-- build-internal. Public dcel and build components are restrictions of those
+-- owners: they expose lawful modules without copying implementations or
+-- publishing trusted constructors. Dual and construction branch independently
+-- from the DCEL owner; public build composes them for hints and telemetry;
+-- exact planar algebra depends only on read owners. The graph is acyclic and uses no
+-- @.hs-boot@ files.
+
+-- Exact arithmetic admits trusted finite/dyadic/normalized inputs inside the
+-- package. Public exact geometry exposes only its checked restrictions.
+library core
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: src-core
+  exposed-modules:
+    Moonlight.Planar.Scalar
+    Moonlight.Planar.LineSideInfo
+    Moonlight.Planar.Point
+    Moonlight.Planar.Internal.Point
+    Moonlight.Planar.Internal.Dyadic
+    Moonlight.Planar.Internal.ExactRational
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+  ghc-options: -fexpose-all-unfoldings
+
+-- Native hexagonal cell algebra is a bottom component: consumers pay for one
+-- packed bit per admitted cell and do not acquire DCEL, overlay, Homology, or
+-- serialization dependencies merely to manipulate a hexagonal world.
+library hex
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-hex
+  exposed-modules:
+    Moonlight.Hex.Coordinate
+    Moonlight.Hex.Element
+    Moonlight.Hex.Region
+    Moonlight.Hex.Topology
+  other-modules:
+    Moonlight.Hex.Region.Internal
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , transformers >= 0.6 && < 0.7
+    , vector >= 0.13 && < 0.14
+  ghc-options: -O2 -fexpose-all-unfoldings
+
+-- Trusted paged storage is shared by the immutable DCEL and construction
+-- owners. It is private because raw page indices and unsafe vector operations
+-- are valid only after those owners have admitted array bounds and page order.
+library storage-internal
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: src-core
+  exposed-modules:
+    Moonlight.Planar.BuildStats
+    Moonlight.Planar.Internal.BuildStats
+    Moonlight.Planar.Internal.PageDirectory
+    Moonlight.Planar.Internal.Paged
+    Moonlight.Planar.Internal.BoxedPaged
+    Moonlight.Planar.Internal.Growable
+    Moonlight.Planar.Internal.PackedIndex
+    Moonlight.Planar.Internal.FaceQueue
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+  ghc-options: -fexpose-all-unfoldings
+
+library dcel-internal
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: src-dcel
+  exposed-modules:
+    Moonlight.Planar.Alpha
+    Moonlight.Planar.Types
+    Moonlight.Planar.Math
+    Moonlight.Planar.Exact
+    Moonlight.Planar.Curve
+    Moonlight.Planar.Curve.Authoring
+    Moonlight.Planar.Curve.Lowering
+    Moonlight.Planar.Curve.Region
+    Moonlight.Planar.Affine
+    Moonlight.Planar.Convex
+    Moonlight.Planar.Simplex
+    Moonlight.Planar.CellSet
+    Moonlight.Planar.Region
+    Moonlight.Planar.Valuation
+    Moonlight.Planar.Interop
+    Moonlight.Planar.Dcel
+    Moonlight.Planar.Payload
+    Moonlight.Planar.Handles.HandleDefs
+    Moonlight.Planar.Internal.HandleDefs
+    Moonlight.Planar.Handles.Dynamic
+    Moonlight.Planar.Handles.Scoped
+    Moonlight.Planar.Handles.Iterators.CircularIterator
+    Moonlight.Planar.Handles.Iterators.DynamicIterators
+    Moonlight.Planar.Handles.Iterators.FixedIterators
+    Moonlight.Planar.Handles.Iterators.HullIterator
+    Moonlight.Planar.PointLocation
+    Moonlight.Planar.Validation
+    Moonlight.Planar.Internal.Validation
+    Moonlight.Planar.FloodFillIterator
+    Moonlight.Planar.Internal.FloodFillIterator
+    Moonlight.Planar.IntersectionIterator
+    Moonlight.Planar.Internal.FaceProbe
+    Moonlight.Planar.Internal.CellSet
+    Moonlight.Planar.Internal.Incidence
+    Moonlight.Planar.Internal.BoundaryCycle
+    Moonlight.Planar.Internal.ExactSegmentEvents
+    Moonlight.Planar.Internal.Region.Publication
+    Moonlight.Planar.Internal.Region.Types
+    Moonlight.Planar.Internal.Region.Bounds
+    Moonlight.Planar.Internal.Predicates
+    Moonlight.Planar.Internal.SegmentRelation
+    Moonlight.Planar.Internal.Types
+    Moonlight.Planar.Internal.Representation
+    Moonlight.Planar.Internal.PointIndex
+    Moonlight.Planar.Internal.Tournament
+  other-modules:
+    Moonlight.Planar.Internal.Exact
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , primitive >= 0.9 && < 0.10
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:core
+    , moonlight-planar:storage-internal
+  ghc-options: -fexpose-all-unfoldings
+
+-- Public immutable-DCEL and exact-geometry surface. Every module is the module
+-- compiled by dcel-internal; reexporting preserves one type and implementation
+-- identity while leaving representation modules hidden.
+library dcel
+  import: shared-properties
+  visibility: public
+  reexported-modules:
+    Moonlight.Planar.Scalar
+    , Moonlight.Planar.LineSideInfo
+    , Moonlight.Planar.Point
+    , Moonlight.Planar.BuildStats
+    , Moonlight.Planar.Alpha
+    , Moonlight.Planar.Types
+    , Moonlight.Planar.Math
+    , Moonlight.Planar.Exact
+    , Moonlight.Planar.Curve
+    , Moonlight.Planar.Curve.Authoring
+    , Moonlight.Planar.Curve.Lowering
+    , Moonlight.Planar.Curve.Region
+    , Moonlight.Planar.Affine
+    , Moonlight.Planar.Convex
+    , Moonlight.Planar.Simplex
+    , Moonlight.Planar.CellSet
+    , Moonlight.Planar.Region
+    , Moonlight.Planar.Valuation
+    , Moonlight.Planar.Interop
+    , Moonlight.Planar.Dcel
+    , Moonlight.Planar.Payload
+    , Moonlight.Planar.Handles.HandleDefs
+    , Moonlight.Planar.Handles.Dynamic
+    , Moonlight.Planar.Handles.Scoped
+    , Moonlight.Planar.Handles.Iterators.DynamicIterators
+    , Moonlight.Planar.Handles.Iterators.FixedIterators
+    , Moonlight.Planar.Handles.Iterators.HullIterator
+    , Moonlight.Planar.PointLocation
+    , Moonlight.Planar.Validation
+    , Moonlight.Planar.FloodFillIterator
+    , Moonlight.Planar.IntersectionIterator
+  build-depends:
+    moonlight-planar:dcel-internal
+    , moonlight-planar:core
+    , moonlight-planar:storage-internal
+
+-- Optional ordered painting and pure SVG publication. Geometry-only consumers
+-- do not inherit a renderer or an additional geometric representation.
+library illustration
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-illustration
+  exposed-modules:
+    Moonlight.Planar.Illustration
+    Moonlight.Planar.Illustration.Svg
+    Moonlight.Planar.Illustration.Motif
+    Moonlight.Planar.Illustration.Layout
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , moonlight-planar:dcel
+
+-- An ExactCellSet already carries the incidence required for generic cellular
+-- lowering. This separate Homology interpretation requires GHC 9.14.
+library cell-complex
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-cell-complex
+  default-extensions:
+    TypeFamilies
+  exposed-modules:
+    Moonlight.Hex.CellComplex
+    Moonlight.Planar.CellComplex
+    Moonlight.Planar.LabelledComplex
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-algebra:abstract >= 0.1 && < 0.2
+    , moonlight-homology >= 0.1.0.2 && < 0.2
+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
+    , moonlight-planar:hex
+    , moonlight-planar:dcel-internal
+    , vector >= 0.13 && < 0.14
+
+-- Stable activation labels induce canonical simplices across independently
+-- built depth meshes.  This opt-in layer owns the exact alpha ingestion and
+-- adjacent-union correspondence witnesses; Homology owns the zigzag reducer.
+library zigzag
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-zigzag
+  exposed-modules:
+    Moonlight.Planar.Zigzag
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , moonlight-core >= 0.1 && < 0.2
+    , moonlight-homology >= 0.1.0.3 && < 0.2
+    , moonlight-planar:build
+    , moonlight-planar:cell-complex
+    , moonlight-planar:dcel
+    , vector >= 0.13 && < 0.14
+
+library build-internal
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: src-build
+  exposed-modules:
+    Moonlight.Planar.Canonical
+    Moonlight.Planar.Internal.Canonical
+    Moonlight.Planar.Internal.Mutable
+    Moonlight.Planar.Internal.OperationState
+    Moonlight.Planar.Internal.Probe
+    Moonlight.Planar.Internal.DcelOperations.FlipRewrite
+    Moonlight.Planar.Internal.DcelOperations.Subdivide
+    Moonlight.Planar.Internal.DcelOperations.Twin
+    Moonlight.Planar.Internal.DcelOperations.FlipRule
+    Moonlight.Planar.Internal.DcelOperations.CandidateArena
+    Moonlight.Planar.Internal.DcelOperations.Legalize
+    Moonlight.Planar.Internal.DcelOperations.Hull
+    Moonlight.Planar.Internal.DcelOperations.Normalize
+    Moonlight.Planar.Internal.DcelOperations.Chain
+    Moonlight.Planar.BulkLoad
+    Moonlight.Planar.Internal.BulkLoad
+    Moonlight.Planar.Removal
+    Moonlight.Planar.Session
+    Moonlight.Planar.Internal.Session
+    Moonlight.Planar.Cdt
+    Moonlight.Planar.Refinement
+    Moonlight.Planar.SetAlgebra
+    Moonlight.Planar.Internal.Cdt.Build
+    Moonlight.Planar.Internal.Cdt.Query
+    Moonlight.Planar.Internal.Cdt.Types
+    Moonlight.Planar.Internal.Cdt.Union
+    Moonlight.Planar.Internal.Join
+    Moonlight.Planar.Internal.Join.Seam
+  other-modules:
+    Moonlight.Planar.Insertion
+    Moonlight.Planar.Internal.Capacity
+    Moonlight.Planar.Internal.Cdt.Admission
+    Moonlight.Planar.Internal.Cdt.Batch
+    Moonlight.Planar.Internal.Cdt.Combinators
+    Moonlight.Planar.Internal.Cdt.Corridor
+    Moonlight.Planar.Internal.Cdt.Corridor.Trace
+    Moonlight.Planar.Internal.Cdt.Recovery
+    Moonlight.Planar.Internal.Cdt.Region
+    Moonlight.Planar.Internal.Cdt.Segment
+    Moonlight.Planar.Internal.Cdt.Site
+    Moonlight.Planar.Internal.Cdt.Split
+    Moonlight.Planar.Internal.Excision
+    Moonlight.Planar.Internal.Location
+    Moonlight.Planar.Internal.CircleSweep
+    Moonlight.Planar.Internal.Refinement
+    Moonlight.Planar.Internal.Transaction
+    Moonlight.Planar.Internal.Join.Plan
+    Moonlight.Planar.Internal.Join.SiteSet
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-algebra:join >= 0.1.1 && < 0.2
+    , primitive >= 0.9 && < 0.10
+    , vector >= 0.13 && < 0.14
+    , vector-algorithms >= 0.9 && < 0.10
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+  ghc-options: -fexpose-all-unfoldings
+
+-- Public construction surface over the private kernel. Trusted admitted-input
+-- and local-transaction entries live only in build-internal; the modules below
+-- are their typed, immutable restrictions. Hints and telemetry compose the
+-- independent construction and dual-query owners here, not inside either one.
+library build
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-build-public
+  exposed-modules:
+    Moonlight.Planar.HintGenerator
+    Moonlight.Planar.Telemetry
+  reexported-modules:
+    Moonlight.Planar.Canonical
+    , Moonlight.Planar.BulkLoad
+    , Moonlight.Planar.Removal
+    , Moonlight.Planar.Session
+    , Moonlight.Planar.Cdt
+    , Moonlight.Planar.Refinement
+    , Moonlight.Planar.SetAlgebra
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:core
+    , moonlight-planar:build-internal
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:dual
+    , moonlight-planar:storage-internal
+  ghc-options: -fexpose-all-unfoldings
+
+-- Concurrency is an effect boundary over the pure build planner. Keeping it in
+-- its own sublibrary prevents @async@ from infecting the geometry core.
+library parallel
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-parallel
+  exposed-modules:
+    Moonlight.Planar.Parallel
+  build-depends:
+    base >= 4.19 && < 5
+    , async >= 2.2 && < 2.3
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-algebra:join >= 0.1.1 && < 0.2
+    , moonlight-planar:build-internal
+    , moonlight-planar:dcel-internal
+  ghc-options: -fexpose-all-unfoldings
+
+-- Canonical hex bytes retain the bottom component's small dependency cone.
+library hex-serialize
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-serialize
+  exposed-modules:
+    Moonlight.Hex.Serialization
+  build-depends:
+    base >= 4.19 && < 5
+    , binary >= 0.8 && < 0.9
+    , bytestring >= 0.12 && < 0.13
+    , deepseq >= 1.5 && < 1.6
+    , transformers >= 0.6 && < 0.7
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:hex
+  ghc-options: -fexpose-all-unfoldings
+
+-- Planar bytes remain separate so a native-hex consumer does not build
+-- the DCEL merely to decode one packed region.
+library serialize
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-serialize
+  exposed-modules:
+    Moonlight.Planar.Serialization
+  build-depends:
+    base >= 4.19 && < 5
+    , binary >= 0.8 && < 0.9
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , transformers >= 0.6 && < 0.7
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+  -- The @Binary@ instances for the identifier and point types are orphans by
+  -- construction: the types belong to @dcel@ and the class to @binary@, and the
+  -- whole purpose of this component is that neither one has to know about the
+  -- other.  Rehoming them would pull @binary@ into the core and defeat the split.
+  ghc-options: -fexpose-all-unfoldings -Wno-orphans
+
+library dual
+  import: shared-properties
+  visibility: public
+  hs-source-dirs: src-dual
+  exposed-modules:
+    Moonlight.Planar.Voronoi
+    Moonlight.Planar.Voronoi.Handles
+    Moonlight.Planar.Interpolation
+  other-modules:
+    Moonlight.Planar.Internal.InterpolationWorkspace
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , primitive >= 0.9 && < 0.10
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+  ghc-options: -fexpose-all-unfoldings
+
+-- Package-private overlay machine room. The main library and package-owned
+-- oracle tests share these exact modules; external consumers receive only the
+-- public embedding certifier and Overlay equations reexported across this wall.
+library overlay-internal
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: src-overlay
+  exposed-modules:
+    Moonlight.Planar.Embedding
+    Moonlight.Planar.Internal.Overlay.Arrangement
+    Moonlight.Planar.Internal.Overlay.Types
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+
+library
+  import: shared-properties
+  hs-source-dirs:
+    src-public
+  exposed-modules:
+    Moonlight.Hex.Planar
+    Moonlight.Planar.Minkowski
+    Moonlight.Planar.Overlay
+    Moonlight.Planar.PowerDiagram
+    Moonlight.Planar.RegularAlpha
+  -- The default library is exact planar algebra. Scoped/owning handles are the safe
+  -- path; Dcel is the explicitly unchecked fixed-index kernel. These are the
+  -- exact focused-component modules, not facade copies.
+  reexported-modules:
+    Moonlight.Planar.Scalar
+    , Moonlight.Planar.LineSideInfo
+    , Moonlight.Planar.Point
+    , Moonlight.Planar.BuildStats
+    , Moonlight.Planar.Alpha
+    , Moonlight.Planar.Types
+    , Moonlight.Planar.Math
+    , Moonlight.Planar.Exact
+    , Moonlight.Planar.Convex
+    , Moonlight.Planar.CellSet
+    , Moonlight.Planar.Region
+    , Moonlight.Planar.Valuation
+    , Moonlight.Planar.Interop
+    , Moonlight.Planar.Dcel
+    , Moonlight.Planar.Payload
+    , Moonlight.Planar.Handles.HandleDefs
+    , Moonlight.Planar.Handles.Dynamic
+    , Moonlight.Planar.Handles.Scoped
+    , Moonlight.Planar.Handles.Iterators.DynamicIterators
+    , Moonlight.Planar.Handles.Iterators.FixedIterators
+    , Moonlight.Planar.Handles.Iterators.HullIterator
+    , Moonlight.Planar.PointLocation
+    , Moonlight.Planar.Validation
+    , Moonlight.Planar.FloodFillIterator
+    , Moonlight.Planar.IntersectionIterator
+    , Moonlight.Planar.Embedding
+  other-modules:
+    Moonlight.Planar.Internal.Minkowski.Operations
+    Moonlight.Planar.Internal.Minkowski.Types
+    Moonlight.Planar.Internal.PowerDiagram.Edit
+    Moonlight.Planar.Internal.PowerDiagram.Generator
+    Moonlight.Planar.Internal.PowerDiagram.Hull
+    Moonlight.Planar.Internal.PowerDiagram.Locality
+    Moonlight.Planar.Internal.PowerDiagram.Mass
+    Moonlight.Planar.Internal.PowerDiagram.Model
+    Moonlight.Planar.Internal.PowerDiagram.Projection
+    Moonlight.Planar.Internal.PowerDiagram.Section
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:hex
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:overlay-internal
+    , moonlight-planar:storage-internal
+  ghc-options: -fexpose-all-unfoldings
+
+library ffi
+  import: shared-properties
+  visibility: private
+  hs-source-dirs: ffi/abi
+  exposed-modules:
+    Moonlight.Planar.Foreign.Boundary
+    Moonlight.Planar.Foreign.Contract
+    Moonlight.Planar.Foreign.Contract.Render
+    Moonlight.Planar.Foreign.Mesh
+    Moonlight.Planar.Foreign.Morphology
+    Moonlight.Planar.Foreign.Obstruction
+    Moonlight.Planar.Foreign.Region
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar
+    , moonlight-planar:core
+    , moonlight-planar:build-internal
+    , moonlight-planar:dcel-internal
+
+foreign-library moonlight-triangulation-c
+  import: shared-properties
+  type: native-shared
+  hs-source-dirs: ffi/exports
+  other-modules:
+    Moonlight.Planar.Foreign.Exports
+  c-sources:
+    ffi/cbits/moonlight_runtime.c
+  include-dirs:
+    ffi/include
+  install-includes:
+    moonlight_triangulation.h
+  build-depends:
+    base >= 4.19 && < 5
+    , moonlight-planar:ffi
+  ghc-options: -threaded
+  if os(windows)
+    options: standalone
+    mod-def-file: ffi/cbits/moonlight-triangulation.def
+  else
+    extra-libraries: pthread
+  if os(linux)
+    lib-version-info: 2:0:0
+
+executable moonlight-planar-ffi-contract
+  import: shared-properties
+  hs-source-dirs: ffi/generator
+  main-is: Main.hs
+  build-depends:
+    base >= 4.19 && < 5
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-planar:ffi
+
+-- ── test slices ──────────────────────────────────────────────────────────────
+
+-- Each slice is an ATOM: a @common@ stanza binding a spec module to the
+-- dependencies and RTS options that module requires.  A suite is then a choice
+-- of atoms and nothing more.  The coherence component below imports every atom
+-- and therefore checks their combined module, instance, and dependency surface
+-- without running every behavioral suite a second time.
+
+common triangulation-test-properties
+  import: shared-properties
+  ghc-options: -O0
+  build-depends:
+    base >= 4.19 && < 5
+    , vector >= 0.13 && < 0.14
+
+common triangulation-test-support-slice
+  other-modules: Support
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:dcel
+
+
+common triangulation-mesh-fixture-slice
+  other-modules: Moonlight.Planar.MeshFixtures
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:build
+
+common triangulation-overlay-fixture-slice
+  other-modules: Moonlight.Planar.OverlayFixtures
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar
+
+common triangulation-native-test-slice
+  import: triangulation-mesh-fixture-slice, triangulation-overlay-fixture-slice
+  other-modules:
+    Moonlight.Planar.ConstructionSpec
+    Moonlight.Planar.ValidationSpec
+    Moonlight.Planar.PredicateSpec
+    Moonlight.Planar.NearestSpec
+    Moonlight.Planar.LocationSpec
+    Moonlight.Planar.EditSpec
+    Moonlight.Planar.DcelSpec
+    Moonlight.Planar.PayloadSpec
+    Moonlight.Planar.PayloadTypeErrors
+    Moonlight.Planar.ConstraintSpec
+    Moonlight.Planar.ConstrainedJoinSpec
+    Moonlight.Planar.ConstrainedSeamSpec
+    Moonlight.Planar.RefinementAssertions
+    Moonlight.Planar.RefinementSpec
+    Moonlight.Planar.AlphaSpec
+    Moonlight.Planar.BoundaryFixtures
+    Moonlight.Planar.PayloadFixtures
+    Moonlight.Planar.IncidenceSpec
+    Moonlight.Planar.FilteredPredicateOptimizationSpec
+    Moonlight.Planar.ExactEmbeddingSpec
+    Moonlight.Planar.ExactSegmentEventsSpec
+    Moonlight.Planar.OverlaySpec
+    Moonlight.Planar.OverlaySelectionSpec
+    Moonlight.Planar.OverlayPublicationSpec
+    Moonlight.Planar.OverlayAssertions
+    Moonlight.Planar.RegionSpec
+  -- The RTS options this module needs are named on the SUITES rather than
+  -- here, which is the one place the slice-owns-its-requirements rule cannot
+  -- hold.  @-with-rtsopts@ is CONCATENATED by GHC across repetitions, so a
+  -- suite importing two slices that both name it links a binary whose RTS
+  -- string is the two spliced together — @-N4 -T-N4 -T@ — which the RTS
+  -- discards whole, silently disarming both.  It must appear exactly once per
+  -- suite, so @triangulation-suite-rtsopts@ below carries it.
+  ghc-options: -threaded -rtsopts
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , primitive >= 0.9 && < 0.10
+    , moonlight-planar:core
+    , transformers >= 0.6 && < 0.7
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build-internal
+    , moonlight-planar:dual
+    , moonlight-planar:overlay-internal
+    , moonlight-planar:storage-internal
+    , moonlight-planar
+
+common triangulation-serialization-test-slice
+  other-modules:
+    Moonlight.Planar.SerializationSpec
+    Moonlight.Planar.SerializationV6Oracle
+    Moonlight.Planar.SerializationFixtures
+  build-depends:
+    binary >= 0.8 && < 0.9
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build-internal
+    , moonlight-planar:serialize
+    , moonlight-planar:storage-internal
+
+-- The operand meshes both algebra slices are stated over.  It sits at the
+-- surface tier with them, so the fixtures cannot quietly acquire a
+-- machine-room dependency that the law slice would then inherit.
+common triangulation-algebra-fixture-slice
+  import: triangulation-mesh-fixture-slice
+  other-modules: Moonlight.Planar.AlgebraFixtures
+  build-depends: containers >= 0.8 && < 0.9
+    , moonlight-planar
+    , moonlight-planar:build
+
+-- The finite-set laws, stated against the public construction owner.
+common triangulation-algebra-law-slice
+  other-modules: Moonlight.Planar.AlgebraSpec
+  build-depends: containers >= 0.8 && < 0.9
+    , moonlight-algebra:join
+    , moonlight-planar
+    , moonlight-planar:build
+
+-- Exact planar Boolean laws, stated through the public exact owners.
+common triangulation-region-algebra-law-slice
+  import: triangulation-overlay-fixture-slice
+  other-modules: Moonlight.Planar.RegionAlgebraSpec
+  build-depends: containers >= 0.8 && < 0.9
+    , moonlight-planar
+    , moonlight-planar:build
+
+-- Exact intrinsic-volume laws over the resident cell carrier and published
+-- region view.
+common triangulation-valuation-law-slice
+  import: triangulation-overlay-fixture-slice
+  other-modules: Moonlight.Planar.ValuationSpec
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar
+
+common triangulation-minkowski-law-slice
+  other-modules:
+    Moonlight.Planar.MinkowskiSpec
+    Moonlight.Planar.ConvexSpec
+  build-depends:
+    moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar
+
+-- A test-only endpoint-reconstruction oracle demonstrating why the production
+-- clipping owner retains original affine source lines.
+common triangulation-exact-clip-retention-law-slice
+  other-modules: Moonlight.Planar.ExactClipRetentionSpec
+  build-depends:
+    moonlight-planar:core
+    , moonlight-planar:dcel-internal
+
+-- The fixed-width in-circle worker cross-checked against the arbitrary-
+-- precision determinant on random, subnormal, wide-exponent, exactly
+-- cocircular and one-ulp-off quadrilaterals.
+common triangulation-in-circle-fixed-width-law-slice
+  other-modules: Moonlight.Planar.InCircleFixedWidthSpec
+  build-depends:
+    moonlight-planar:core
+
+common triangulation-power-diagram-law-slice
+  other-modules:
+    Moonlight.Planar.RegularTopologySpec
+    Moonlight.Planar.RegularEditSpec
+    Moonlight.Planar.RegularAlphaSpec
+    Moonlight.Planar.PowerDiagramSpec
+    Moonlight.Planar.PowerFixtures
+    Moonlight.Planar.PowerMassSpec
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar
+
+common triangulation-layer-operations-law-slice
+  import: triangulation-overlay-fixture-slice
+  other-modules:
+    Moonlight.Planar.LayerOperationsSpec
+    Moonlight.Planar.ExactIncidencePublicationSpec
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+    , moonlight-planar
+
+-- The agreement between the seam schedule and the reference rebuild.  It names
+-- an internal schedule, so it reaches below the wall and travels with the
+-- schedule it names: a replacement kernel carries its own copy of this.
+common triangulation-algebra-schedule-slice
+  other-modules: Moonlight.Planar.ScheduleAgreementSpec
+  build-depends: moonlight-planar:build-internal
+    , moonlight-planar
+
+common triangulation-parallel-test-slice
+  other-modules: Moonlight.Planar.ParallelSpec
+  ghc-options: -threaded -rtsopts
+  build-depends: moonlight-planar:parallel
+    , moonlight-planar:build
+
+test-suite moonlight-planar-cell-complex-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: test/cell-complex
+  default-extensions:
+    TypeFamilies
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , moonlight-homology >= 0.1.0.2 && < 0.2
+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
+    , moonlight-planar:hex
+    , moonlight-planar:core
+    , moonlight-planar:build-internal
+    , moonlight-planar:cell-complex
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+    , tasty >= 1.4 && < 1.6
+    , tasty-hunit >= 0.10 && < 0.11
+    , vector >= 0.13 && < 0.14
+
+-- Public activation ingestion, labelled adjacent-union correspondences, and
+-- exact non-monotone persistence, including its compiled documentation path.
+test-suite moonlight-planar-zigzag-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/zigzag
+    docs/examples
+  other-modules:
+    Moonlight.Planar.Example.ActivationZigzag
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , moonlight-homology >= 0.1.0.3 && < 0.2
+    , moonlight-planar:dcel
+    , moonlight-planar:zigzag
+    , tasty >= 1.4 && < 1.6
+    , tasty-hunit >= 0.10 && < 0.11
+    , vector >= 0.13 && < 0.14
+
+-- Canonical documentation examples compile exclusively against the public
+-- components and retain their expected semantic summaries as executable checks.
+test-suite moonlight-planar-examples-test
+  import: triangulation-test-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/examples
+    docs/examples
+  other-modules:
+    Moonlight.Planar.Example.AlphaBoundary
+    Moonlight.Planar.Example.ConstrainedRegion
+    Moonlight.Planar.Example.PlanarRegion
+    Moonlight.Planar.Example.SetAlgebra
+  build-depends: containers >= 0.8 && < 0.9
+    , moonlight-planar
+    , moonlight-planar:build
+    , moonlight-planar:core
+    , moonlight-planar:dcel
+    , moonlight-planar:dual
+
+-- One behavioral owner for exact clipping, power cells, morphology, affine
+-- envelopes, coverage, n-ary overlay, and mass. The law slices remain
+-- independently reusable by the compile-only coherence component.
+test-suite moonlight-planar-exact-planar-test
+  import:
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-exact-clip-retention-law-slice,
+    triangulation-in-circle-fixed-width-law-slice,
+    triangulation-minkowski-law-slice,
+    triangulation-power-diagram-law-slice,
+    triangulation-layer-operations-law-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/exact-planar
+    test/algebra
+    test/support
+
+-- A real external shape over only the two focused public components. It proves
+-- that immutable observation and construction/editing compose without either
+-- an umbrella facade or a private implementation token.
+test-suite moonlight-planar-public-components-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: test/public-components
+  other-modules:
+    Moonlight.Planar.AdmissionSpec
+    Moonlight.Planar.AdmissionTypeErrors
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.6 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar
+    , moonlight-planar:dcel
+    , moonlight-planar:build
+
+test-suite moonlight-planar-curve-test
+  import: triangulation-test-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/curve
+    test/support
+  other-modules:
+    Moonlight.Planar.CurveSpec
+    Moonlight.Planar.Curve.AuthoringSpec
+    Moonlight.Planar.AffineSpec
+    Moonlight.Planar.CurveLoweringSpec
+    Moonlight.Planar.CurveLoweringTypeErrors
+    Moonlight.Planar.RegionAdmissionSpec
+    Support
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:dcel
+
+test-suite moonlight-planar-illustration-test
+  import: triangulation-test-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/illustration
+    docs/art-common
+  other-modules:
+    Moonlight.Planar.IllustrationSpec
+    Moonlight.Planar.IllustrationStudySpec
+    Moonlight.Planar.EquationalGardenSpec
+    Moonlight.Planar.MotifSpec
+    Moonlight.Planar.LayoutSpec
+    Moonlight.Planar.SpaceSwordSpec
+    Moonlight.Planar.Exhibit.IllustrationStudy
+    Moonlight.Planar.Exhibit.EquationalGarden
+    Moonlight.Planar.Exhibit.SpaceSword
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:dcel
+    , moonlight-planar:illustration
+
+executable moonlight-planar-illustration-study
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs:
+    docs/illustration-study
+    docs/art-common
+  other-modules:
+    Moonlight.Planar.Exhibit.IllustrationStudy
+    Moonlight.Planar.Exhibit.EquationalGarden
+    Moonlight.Planar.Exhibit.SpaceSword
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-planar:dcel
+    , moonlight-planar:illustration
+
+-- Native coordinate, element, finite-set, descent/gluing, exact-publication,
+-- and serialization laws through public component boundaries only.
+test-suite moonlight-planar-hex-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: test/hex
+  ghc-options: -O0
+  build-depends:
+    base >= 4.19 && < 5
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.8 && < 0.9
+    , moonlight-planar
+    , moonlight-planar:hex
+    , moonlight-planar:hex-serialize
+    , vector >= 0.13 && < 0.14
+
+-- One pure, source-grounded projection of the alpha, dual, interpolation,
+-- planar-algebra, and persistence owners. The test shares the internal exhibit
+-- module; neither component creates a public visualization API.
+test-suite moonlight-planar-persistence-rose-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/persistence-rose
+    docs/persistence-rose
+  other-modules:
+    Moonlight.Planar.Exhibit.PersistenceRose
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.6 && < 0.9
+    , moonlight-homology >= 0.1.0.2 && < 0.2
+    , moonlight-planar
+    , moonlight-planar:cell-complex
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:dual
+    , tasty >= 1.4 && < 1.6
+    , tasty-hunit >= 0.10 && < 0.11
+    , text >= 2.0 && < 2.2
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:build
+
+-- The single statement of the RTS environment every suite runs under: -N so
+-- that the concurrent tournament has more than one capability to be scheduled
+-- onto, -T so that the filtered-predicate test can read allocation counters.
+-- It is one stanza rather than a line on each slice because @-with-rtsopts@
+-- concatenates rather than replaces, so naming it twice in one suite produces
+-- a spliced string the RTS rejects entirely.  Every behavioral suite that
+-- needs those capabilities imports this exactly once; the compile-only
+-- coherence suite deliberately installs no runtime defaults.
+common triangulation-suite-rtsopts
+  ghc-options: -threaded -rtsopts "-with-rtsopts=-N4 -T"
+
+test-suite moonlight-planar-native-test
+  import:
+    triangulation-suite-rtsopts,
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-native-test-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/native
+    test/support
+
+test-suite moonlight-planar-serialization-test
+  import:
+    triangulation-suite-rtsopts,
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-serialization-test-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/serialization
+    test/support
+
+test-suite moonlight-planar-algebra-test
+  import:
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-algebra-fixture-slice,
+    triangulation-algebra-law-slice,
+    triangulation-region-algebra-law-slice,
+    triangulation-valuation-law-slice,
+    triangulation-algebra-schedule-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/algebra
+    test/support
+
+test-suite moonlight-planar-parallel-test
+  import:
+    triangulation-suite-rtsopts,
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-algebra-fixture-slice,
+    triangulation-parallel-test-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/parallel
+    test/algebra
+    test/support
+
+test-suite moonlight-planar-ffi-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: test/ffi
+  build-depends:
+    base >= 4.19 && < 5
+    , moonlight-planar:ffi
+
+-- This component owns only cross-slice compile coherence.  Behavioral ownership
+-- remains in focused suites.  The shared test-properties stanza keeps
+-- every test body at @-O0@, so the union retains type/module/instance collision
+-- detection without paying to optimize a second copy of every test body.
+test-suite moonlight-planar-coherence-test
+  import:
+    triangulation-test-properties,
+    triangulation-test-support-slice,
+    triangulation-native-test-slice,
+    triangulation-serialization-test-slice,
+    triangulation-algebra-fixture-slice,
+    triangulation-algebra-law-slice,
+    triangulation-region-algebra-law-slice,
+    triangulation-valuation-law-slice,
+    triangulation-minkowski-law-slice,
+    triangulation-exact-clip-retention-law-slice,
+    triangulation-power-diagram-law-slice,
+    triangulation-layer-operations-law-slice,
+    triangulation-algebra-schedule-slice,
+    triangulation-parallel-test-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/coherence
+    test/native
+    test/serialization
+    test/algebra
+    test/parallel
+    test/support
+
+-- ── benchmark slices ─────────────────────────────────────────────────────────
+-- Deliberately not @tasty-bench@: these report allocated bytes and the work
+-- counters the library keeps about itself — hierarchy walk steps, refinement
+-- queue pops, bytes per interpolation query — and a wall-clock harness cannot
+-- express any of them. The serialization benchmark below additionally compares
+-- complete V6 bytes with the retained test-only gather oracle.
+
+common triangulation-benchmark-properties
+  import: shared-properties
+  -- The allocation arms read GHC.Stats, which is dark without -T.  As with the
+  -- test suites, @-with-rtsopts@ is CONCATENATED by GHC across repetitions, so
+  -- it must appear exactly once per benchmark: this stanza is that one place,
+  -- and no benchmark slice may name it again.
+  ghc-options: -threaded -rtsopts "-with-rtsopts=-T"
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+
+common triangulation-benchmark-support-slice
+  other-modules:
+    BenchMeasure
+    BenchSupport
+  build-depends: moonlight-planar:dcel-internal
+    , moonlight-planar:core
+
+common triangulation-build-benchmark-slice
+  other-modules: Moonlight.Planar.BuildBench
+  build-depends:
+    primitive >= 0.9 && < 0.10
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build-internal
+    , moonlight-planar:storage-internal
+    , moonlight-planar:ffi
+
+common triangulation-dcel-benchmark-slice
+  other-modules: Moonlight.Planar.DcelBench
+  build-depends: moonlight-planar:dcel-internal
+    , moonlight-planar:build
+
+common triangulation-dual-benchmark-slice
+  other-modules: Moonlight.Planar.DualBench
+  build-depends: moonlight-planar:dual
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build
+
+common triangulation-join-benchmark-slice
+  other-modules: Moonlight.Planar.JoinBench
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build-internal
+    , moonlight-planar:storage-internal
+    , moonlight-algebra:join
+
+common triangulation-region-benchmark-slice
+  other-modules: Moonlight.Planar.RegionBench
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar
+    , moonlight-planar:build
+
+-- The package-owned external construction board deliberately uses tasty-bench:
+-- unlike the native work-counter slices above, its only lawful common metric
+-- across Haskell and four Rust referents is elapsed or CPU time.  The Rust
+-- crate is a foreign adapter; Moonlight owns the case algebra, agreement
+-- descent, timing, receipt parser, and derived pictures here.
+common triangulation-delaunay-compare-properties
+  import: shared-properties
+  ghc-options: -O2
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-planar:build-internal
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:core
+
+executable moonlight-planar-delaunay-compare
+  import: triangulation-delaunay-compare-properties
+  ghc-options: -threaded -rtsopts
+  main-is: Main.hs
+  hs-source-dirs: docs/delaunay-compare
+  other-modules:
+    Moonlight.Planar.Bench.DelaunayCompare.Domain
+    Moonlight.Planar.Bench.DelaunayCompare.Native
+    Moonlight.Planar.Bench.DelaunayCompare.Suite
+  build-depends:
+    directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , process >= 1.6 && < 1.7
+    , tasty-bench >= 0.3 && < 0.6
+    , transformers >= 0.5 && < 0.7
+    , unix >= 2.8 && < 2.9
+    , vector >= 0.13 && < 0.14
+
+executable moonlight-planar-delaunay-pictures
+  import: triangulation-delaunay-compare-properties
+  main-is: PictureMain.hs
+  hs-source-dirs: docs/delaunay-compare
+  other-modules:
+    Moonlight.Planar.Bench.DelaunayCompare.Domain
+    Moonlight.Planar.Bench.DelaunayCompare.Picture
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , transformers >= 0.5 && < 0.7
+
+executable moonlight-planar-category-observatory-export
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs: docs/category-observatory
+  other-modules:
+    Moonlight.Planar.Exhibit.CategoryObservatory
+    Moonlight.Planar.Exhibit.CategoryObservatory.Category
+    Moonlight.Planar.Exhibit.CategoryObservatory.Geometry
+    Moonlight.Planar.Exhibit.CategoryObservatory.Identifier
+    Moonlight.Planar.Exhibit.CategoryObservatory.Model
+    Moonlight.Planar.Exhibit.CategoryObservatory.Nerve
+    Moonlight.Planar.Exhibit.CategoryObservatory.Picture
+  ghc-options: -Wall -Wcompat
+  default-extensions:
+    TypeFamilies
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    aeson >= 2.2 && < 2.4
+    , base >= 4.22 && < 5
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.6 && < 0.9
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-category >= 1.1.0.0 && < 1.2
+    , moonlight-category:simplicial >= 1.1.0.0 && < 1.2
+    , moonlight-homology:cell-category >= 0.1.0.2 && < 0.2
+    , moonlight-homology:cell-complex >= 0.1.0.2 && < 0.2
+    , moonlight-planar:build-internal
+    , moonlight-planar:cell-complex
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , transformers >= 0.5 && < 0.7
+    , vector >= 0.13 && < 0.14
+
+executable moonlight-planar-noctilucent-garden
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs: docs/noctilucent-garden docs/art-common
+  other-modules: Moonlight.Planar.Exhibit.Svg
+  build-depends:
+    base >= 4.19 && < 5
+    , moonlight-planar:build
+    , moonlight-planar:dcel
+    , moonlight-planar:dual
+    , vector >= 0.13 && < 0.14
+
+executable moonlight-planar-moonblade
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs: docs/moonblade docs/art-common
+  other-modules:
+    Moonlight.Planar.Exhibit.Svg
+    Moonlight.Planar.Exhibit.Moonblade.Crescent
+    Moonlight.Planar.Exhibit.Geometry
+  build-depends:
+    base >= 4.19 && < 5
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-planar:build
+    , moonlight-planar:dcel
+    , vector >= 0.13 && < 0.14
+
+executable moonlight-planar-knight-rig
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs: docs/knight-rig docs/art-common
+  other-modules: Moonlight.Planar.Exhibit.Geometry
+  build-depends:
+    base >= 4.19 && < 5
+    , moonlight-planar:build
+    , moonlight-planar:dcel
+    , vector >= 0.13 && < 0.14
+
+executable moonlight-planar-persistence-rose
+  import: shared-properties
+  main-is: Main.hs
+  hs-source-dirs: docs/persistence-rose
+  other-modules:
+    Moonlight.Planar.Exhibit.PersistenceRose
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.6 && < 0.9
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , moonlight-homology >= 0.1.0.2 && < 0.2
+    , moonlight-planar
+    , moonlight-planar:cell-complex
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:dual
+    , text >= 2.0 && < 2.2
+    , transformers >= 0.5 && < 0.7
+    , vector >= 0.13 && < 0.14
+    , moonlight-planar:build
+
+-- These referent instruments inspect package-internal handles and thaw/freeze
+-- lanes. They belong to the triangulation package rather than reopening its
+-- private construction boundary through a second package.
+executable moonlight-planar-spade-referent
+  default-language: GHC2024
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+    -Wpartial-fields
+    -O2
+    -rtsopts
+  default-extensions:
+    BangPatterns
+    LambdaCase
+    NumericUnderscores
+  hs-source-dirs: bench/spade-compare/hs
+  main-is: Main.hs
+  -- Main is the entry and the lane dispatch; every workload, gate and
+  -- diagnostic sits in the domain module that owns it. The domains depend on
+  -- SpadeCompare.Support and on nothing else of each other, so they compile in
+  -- parallel and a lane edit rebuilds one slice.
+  other-modules:
+    Moonlight.Planar.Bench.SpadeCompare.Lane
+    Moonlight.Planar.Bench.SpadeCompare.Input
+    Moonlight.Planar.Bench.SpadeCompare.Timing
+    Moonlight.Planar.Bench.SpadeCompare.Support
+    Moonlight.Planar.Bench.SpadeCompare.Delaunay
+    Moonlight.Planar.Bench.SpadeCompare.Removal
+    Moonlight.Planar.Bench.SpadeCompare.Hierarchy
+    Moonlight.Planar.Bench.SpadeCompare.Constraint
+    Moonlight.Planar.Bench.SpadeCompare.Interpolation
+    Moonlight.Planar.Bench.SpadeCompare.Voronoi
+    Moonlight.Planar.Bench.SpadeCompare.Dcel
+    Moonlight.Planar.Bench.SpadeCompare.Intersection
+    Moonlight.Planar.Bench.SpadeCompare.Gates
+  build-depends:
+    base >= 4.22 && < 5
+    , deepseq >= 1.4 && < 1.6
+    , directory >= 1.3 && < 1.4
+    , filepath >= 1.4 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , primitive >= 0.9 && < 0.10
+    , moonlight-planar
+    , moonlight-planar:build
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:build-internal
+    , moonlight-planar:dual
+
+-- The alpha comparison is deliberately opt-in: ordinary scorecard listing and
+-- replay do not inherit Homology merely because this diagnostic exists beside
+-- the canonical board.
+executable moonlight-planar-alpha-spade-referent
+  default-language: GHC2024
+  ghc-options:
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+    -Wpartial-fields
+    -O2
+    -rtsopts
+  default-extensions:
+    BangPatterns
+    LambdaCase
+    NumericUnderscores
+  hs-source-dirs:
+    bench/spade-compare/hs-alpha
+    bench/spade-compare/hs
+  main-is: Main.hs
+  other-modules:
+    Moonlight.Planar.Bench.SpadeCompare.Alpha
+    Moonlight.Planar.Bench.SpadeCompare.Input
+    Moonlight.Planar.Bench.SpadeCompare.Lane
+    Moonlight.Planar.Bench.SpadeCompare.Timing
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , deepseq >= 1.4 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , moonlight-homology >= 0.1 && < 0.2
+    , moonlight-planar:build-internal
+    , moonlight-planar:cell-complex
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:dual
+
+-- Exact sign-only generator observations versus retained magnitude arithmetic.
+test-suite moonlight-planar-lifted-sign-test
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    test/lifted-sign
+    src-public
+  other-modules:
+    Moonlight.Planar.LiftedSignCases
+    Moonlight.Planar.Internal.PowerDiagram.Generator
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+
+benchmark moonlight-planar-lifted-sign-bench
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/lifted-sign
+    test/lifted-sign
+    src-public
+    bench/support
+  other-modules:
+    Moonlight.Planar.LiftedSignCases
+    Moonlight.Planar.Internal.PowerDiagram.Generator
+    BenchMeasure
+  ghc-options: -fexpose-all-unfoldings -threaded -rtsopts "-with-rtsopts=-T"
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , tasty-bench >= 0.3 && < 0.6
+    , moonlight-planar:core
+    , moonlight-planar:dcel-internal
+
+benchmark moonlight-planar-serialization-bench
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/serialization
+    bench/support
+    test/serialization
+  other-modules:
+    BenchMeasure
+    Moonlight.Planar.SerializationV6Oracle
+    Moonlight.Planar.SerializationFixtures
+  ghc-options: -threaded -rtsopts "-with-rtsopts=-T"
+  build-depends:
+    base >= 4.19 && < 5
+    , deepseq >= 1.5 && < 1.6
+    , vector >= 0.13 && < 0.14
+    , binary >= 0.8 && < 0.9
+    , bytestring >= 0.12 && < 0.13
+    , tasty-bench >= 0.3 && < 0.6
+    , moonlight-planar:core
+    , moonlight-planar:serialize
+    , moonlight-planar:build-internal
+    , moonlight-planar:dcel-internal
+    , moonlight-planar:storage-internal
+
+-- One exact Delaunay alpha filtration versus independently restricting and
+-- reducing the same chain complex at every critical radius. Construction of
+-- the shared Delaunay geometry is outside all timed lanes.
+benchmark moonlight-planar-alpha-bench
+  import:
+    shared-properties,
+    triangulation-benchmark-support-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/alpha
+    bench/support
+  ghc-options: -threaded -rtsopts "-with-rtsopts=-T"
+  if impl(ghc < 9.14)
+    buildable: False
+  build-depends:
+    base >= 4.22 && < 5
+    , containers >= 0.6 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-homology >= 0.1.0.2 && < 0.2
+    , moonlight-planar
+    , moonlight-planar:build-internal
+    , moonlight-planar:cell-complex
+    , moonlight-planar:dcel-internal
+    , vector >= 0.13 && < 0.14
+
+-- Exact bounded power-cell construction.  Tasty-bench owns the timing
+-- distribution; the benchmark's one-shot report derives boundary/intersection and
+-- rational-width receipts from the public power-diagram result.
+benchmark moonlight-planar-power-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/power
+    bench/support
+  other-modules:
+    Moonlight.Planar.PowerBench
+    Moonlight.Planar.PowerMassBench
+  build-depends:
+    containers >= 0.6 && < 0.9
+    , tasty-bench >= 0.3 && < 0.6
+    , moonlight-planar
+
+-- Exact affine-envelope construction, balanced n-ary refinement, and direct
+-- cell analytics against the public compositions callers would otherwise
+-- materialize by hand.
+benchmark moonlight-planar-layer-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/layer
+    bench/support
+  other-modules:
+    Moonlight.Planar.LayerBench
+  build-depends:
+    containers >= 0.8 && < 0.9
+    , tasty-bench >= 0.3 && < 0.6
+    , moonlight-planar:core
+    , moonlight-planar
+
+benchmark moonlight-planar-build-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-build-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/build
+    bench/support
+
+benchmark moonlight-planar-dcel-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-dcel-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/dcel
+    bench/support
+
+benchmark moonlight-planar-dual-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-dual-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/dual
+    bench/support
+
+benchmark moonlight-planar-join-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-join-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/join
+    bench/support
+
+benchmark moonlight-planar-publication-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-join-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: PublicationMain.hs
+  hs-source-dirs:
+    bench/publication
+    bench/join
+    bench/support
+
+benchmark moonlight-planar-region-bench
+  import:
+    triangulation-benchmark-properties,
+    triangulation-benchmark-support-slice,
+    triangulation-region-benchmark-slice
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/region
+    bench/support
+
+benchmark moonlight-planar-curve-bench
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/curve
+    bench/support
+  other-modules:
+    BenchMeasure
+    Moonlight.Planar.CurveBench
+    Moonlight.Planar.AuthoringBench
+  ghc-options: -O2 -rtsopts "-with-rtsopts=-T"
+  build-depends:
+    base >= 4.19 && < 5
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , moonlight-planar:dcel
+    , moonlight-planar:illustration
+
+-- Packed native finite-set kernels plus the opt-in canonical byte interpreter.
+-- The measured core itself remains the dependency-light @hex@ component.
+benchmark moonlight-planar-hex-bench
+  import: shared-properties
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs:
+    bench/hex
+    bench/support
+  other-modules:
+    BenchMeasure
+    PlanarSelectionBaseline
+    PlanarSelectionBench
+  ghc-options: -O2 -threaded -rtsopts "-with-rtsopts=-T"
+  build-depends:
+    base >= 4.19 && < 5
+    , bytestring >= 0.12 && < 0.13
+    , containers >= 0.8 && < 0.9
+    , deepseq >= 1.5 && < 1.6
+    , tasty-bench >= 0.4 && < 0.5
+    , moonlight-planar
+    , moonlight-planar:hex
+    , moonlight-planar:hex-serialize
+
+benchmark moonlight-planar-bench
   import:
     triangulation-benchmark-properties,
     triangulation-benchmark-support-slice,
diff --git a/src-build-public/Moonlight/Planar/HintGenerator.hs b/src-build-public/Moonlight/Planar/HintGenerator.hs
new file mode 100644
--- /dev/null
+++ b/src-build-public/Moonlight/Planar/HintGenerator.hs
@@ -0,0 +1,485 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+-- | Reusable point-location hints and their topology-preserving maintenance.
+module Moonlight.Planar.HintGenerator
+  ( LastUsedHint
+  , emptyLastUsedHint
+  , lastUsedHint
+  , rememberVertex
+  , HierarchyHint
+  , defaultHierarchyBranchFactor
+  , buildHierarchyHint
+  , hierarchyHint
+  , hierarchyBranchFactor
+  , hierarchyBaseCount
+  , hierarchyLevelCount
+  , hierarchyVertexCount
+  , updateHierarchyAfterInsertion
+  , updateHierarchyAfterRemoval
+  , rebuildHierarchyHint
+  , removeManyWithHierarchy
+  ) where
+
+import qualified Data.Vector as V
+import Control.DeepSeq (NFData)
+import Data.List (sort)
+import Data.Word (Word32)
+import Moonlight.Planar.BulkLoad (delaunay, insert)
+import Moonlight.Planar.Dcel (numUndirectedEdges, numVertices, undirectedEndpoints, vertexPoint)
+import Moonlight.Planar.Internal.HandleDefs
+  ( UndirectedEdgeId (..)
+  , VertexId (..)
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Interpolation (nearestNeighbor)
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Removal (RemovalOutcome, removalTriangulation, removeVertex)
+import Moonlight.Planar.Session (removeManyAtNear, withSession)
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint)
+import GHC.Generics (Generic)
+
+-- | Most recently admitted vertex, suitable as the next descent seed.
+newtype LastUsedHint = LastUsedHint (Maybe VertexId)
+  deriving stock (Show)
+  deriving newtype (Eq, Ord)
+
+-- | A last-used hint with no remembered vertex.
+emptyLastUsedHint :: LastUsedHint
+emptyLastUsedHint = LastUsedHint Nothing
+
+-- | Project a remembered vertex into a location hint.
+lastUsedHint :: LastUsedHint -> Maybe LocationHint
+lastUsedHint (LastUsedHint vertex) = VertexHint <$> vertex
+
+-- | Replace the remembered vertex.
+rememberVertex :: VertexId -> LastUsedHint -> LastUsedHint
+rememberVertex vertex _ = LastUsedHint (Just vertex)
+
+-- | A Delaunay hierarchy for logarithmic expected random point location.
+-- The first vector element is the finest sparse level; the final element is
+-- the coarsest. The base triangulation is not duplicated.
+--
+-- The levels are nested by a single arithmetic law rather than by a stored
+-- correspondence. Level @i@ holds every @branch^(i+1)@-th base vertex in base
+-- order, so a level-local handle @j@ names handle @j * branch@ one level finer
+-- — and at level 0 the finer level is the base mesh itself, under the very
+-- same multiplication. Descent therefore never rediscovers a handle it has
+-- already computed, and no level carries an index vector.
+--
+-- 'hierarchyBaseCount' is the base cardinality the levels were sampled from.
+-- It is the hierarchy's claim about which triangulation it answers for, and
+-- every maintenance entry is stated against it: a base whose cardinality is not
+-- the one recorded here plus the movement the operation performs is not the
+-- base this hierarchy describes, and is rebuilt for rather than patched.
+data HierarchyHint = HierarchyHint
+  { hierarchyBranchFactor :: {-# UNPACK #-} !Int
+    -- ^ Sampling stride between adjacent hierarchy levels.
+  , hierarchyBaseCount :: {-# UNPACK #-} !Int
+    -- ^ Cardinality of the base mesh described by the hierarchy.
+  , hierarchyLevels :: !(V.Vector (Triangulation 'Unconstrained (Point) () () ()))
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+instance Show (HierarchyHint) where
+  showsPrec precedence hierarchy =
+    showParen (precedence > 10) $
+      showString "HierarchyHint "
+        . shows (hierarchyBranchFactor hierarchy)
+        . showString " "
+        . shows (hierarchyBaseCount hierarchy)
+        . showString " "
+        . shows (hierarchyLevelCount hierarchy)
+        . showString " "
+        . shows (hierarchyVertexCount hierarchy)
+
+instance Eq (HierarchyHint) where
+  left == right =
+    hierarchyBranchFactor left == hierarchyBranchFactor right
+      && hierarchyBaseCount left == hierarchyBaseCount right
+      && hierarchyLevelCount left == hierarchyLevelCount right
+      && V.and (V.zipWith sameSparseMesh (hierarchyLevels left) (hierarchyLevels right))
+
+-- Two sparse levels are the same hint when they carry the same points and the
+-- same undirected edges. Half-edge index labelling records the order
+-- construction happened to visit, so a level rebuilt from scratch and a level
+-- extended in place are structurally equal while their arrays are not.
+sameSparseMesh
+  :: Triangulation 'Unconstrained (Point) () () ()
+  -> Triangulation 'Unconstrained (Point) () () ()
+  -> Bool
+sameSparseMesh left right =
+  numVertices left == numVertices right
+    && numUndirectedEdges left == numUndirectedEdges right
+    && meshPoints left == meshPoints right
+    && meshEdges left == meshEdges right
+ where
+  meshPoints
+    :: Triangulation mode vertex directed undirected face
+    -> [Point]
+  meshPoints triangulation =
+    sort [vertexPoint triangulation (VertexId (fromIntegral index)) | index <- [0 .. numVertices triangulation - 1]]
+
+  meshEdges
+    :: Triangulation mode vertex directed undirected face
+    -> [(Point, Point)]
+  meshEdges triangulation =
+    sort
+      [ if from <= to then (from, to) else (to, from)
+      | index <- [0 .. numUndirectedEdges triangulation - 1]
+      , let (fromVertex, toVertex) = undirectedEndpoints triangulation (UndirectedEdgeId (fromIntegral index))
+            from = vertexPoint triangulation fromVertex
+            to = vertexPoint triangulation toVertex
+      ]
+
+-- | Default sampling stride between hierarchy levels.
+defaultHierarchyBranchFactor :: Int
+defaultHierarchyBranchFactor = 16
+
+-- | Number of stored sparse levels.
+hierarchyLevelCount :: HierarchyHint -> Int
+hierarchyLevelCount = V.length . hierarchyLevels
+
+-- | Total vertices retained across every sparse level.
+hierarchyVertexCount :: HierarchyHint -> Int
+hierarchyVertexCount =
+  V.foldl' (\total level -> total + numVertices level) 0 . hierarchyLevels
+
+-- | Build nested sparse Delaunay levels. A branch factor of 16 mirrors Spade's
+-- default and gives O(log n) expected descent on uniformly distributed input.
+--
+-- Each level is bulk loaded from the sampled points in base order. Base
+-- vertices carry pairwise distinct positions, so the load deduplicates nothing
+-- and assigns local handle @j@ to sample @j@ — which is what makes the nesting
+-- law on 'hierarchyLevels' an identity rather than a lookup.
+buildHierarchyHint
+  :: Int
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError (HierarchyHint)
+buildHierarchyHint requestedBranch triangulation =
+  HierarchyHint branch count <$> V.mapM buildLevel levelDivisors
+ where
+  !branch = max 2 requestedBranch
+  !count = numVertices triangulation
+  levelDivisors =
+    V.unfoldr
+      (\candidate ->
+         case candidate of
+           Nothing -> Nothing
+           Just divisor ->
+             let !population = samplePopulation count divisor
+                 next =
+                   if population <= 1
+                     then Nothing
+                     else Just (safeMultiply divisor branch)
+              in Just (divisor, next)
+      )
+      (if count <= 0 then Nothing else Just branch)
+
+  buildLevel divisor =
+    buildTriangulation
+      <$> delaunay
+        unitElementDefaults
+        ( V.generate
+            (samplePopulation count divisor)
+            (\index ->
+               vertexPoint
+                 triangulation
+                 (VertexId (fromIntegral (index * divisor)))
+            )
+        )
+
+-- | Descend from the coarsest sparse triangulation. The handle a level returns
+-- is carried to the next finer level by one multiplication, and the same
+-- multiplication at level 0 names the base vertex. Nothing is relocated: the
+-- coarse answer is not searched for again, it is computed.
+hierarchyHint :: HierarchyHint -> QueryPoint -> Maybe LocationHint
+hierarchyHint HierarchyHint{hierarchyBranchFactor, hierarchyLevels} query =
+  VertexHint <$> descend (V.length hierarchyLevels - 1) Nothing
+ where
+  !branch = fromIntegral hierarchyBranchFactor :: Word32
+
+  descend !levelIndex !coarse
+    | levelIndex < 0 = Nothing
+    | otherwise =
+        case nearestNeighbor (hierarchyLevels V.! levelIndex) coarse query of
+          Nothing -> Nothing
+          Just (VertexId local, _) ->
+            let !finer = VertexId (local * branch)
+             in if levelIndex == 0 then Just finer else descend (levelIndex - 1) (Just finer)
+
+
+-- | Update the nested hierarchy from an insertion's own report: the point the
+-- insertion was asked for, the handle it answered, and whether it created a
+-- site. Only the levels selected by the branch divisibility rule are changed.
+-- Unaffected levels are structurally shared.
+--
+-- No triangulation is named. The hierarchy walks its own levels and nothing
+-- else, so a base was only ever a lookup table for three facts — its
+-- cardinality, the stored position of the new vertex, and the position of
+-- vertex zero — and every one of them is in the report or already in the
+-- levels, because level-local handle zero is base handle zero at every level.
+-- A caller maintaining the hierarchy across a run of insertions therefore
+-- never has to publish a mesh to be allowed to speak to it, which is the whole
+-- cost of the arrangement this replaces: one full arena copy per step, paid
+-- only to name the thing that was just edited.
+--
+-- An insertion that found its point already present created no site, and a
+-- hierarchy valid for a triangulation is valid for that same triangulation, so
+-- the answer is the argument, unexamined.
+updateHierarchyAfterInsertion
+  :: HierarchyHint
+  -> Point
+  -> VertexId
+  -> InsertionDisposition
+  -> Either BuildError (HierarchyHint)
+updateHierarchyAfterInsertion hierarchy requested vertex disposition =
+  case disposition of
+    AlreadyPresent -> Right hierarchy
+    Inserted
+      | vertexIdIndex vertex /= baseIndex ->
+          Left
+            ( HierarchyInsertionHandleMismatch
+                (VertexId (fromIntegral baseIndex))
+                vertex
+            )
+      | otherwise -> do
+          updated <- updateLevels branch (V.toList (hierarchyLevels hierarchy))
+          topped <- ensureSingletonTop updated
+          pure
+            hierarchy
+              { hierarchyBaseCount = baseIndex + 1
+              , hierarchyLevels = V.fromList topped
+              }
+ where
+  !branch = hierarchyBranchFactor hierarchy
+  !baseIndex = hierarchyBaseCount hierarchy
+  -- A level holds what the mesh stores, not what the caller wrote: storage
+  -- rounds a signed zero, and a level built from the unrounded point would
+  -- settle a distance tie against a different handle.
+  !point = canonicalPoint requested
+
+  -- An update is the only operation that can break the nesting law, so the law
+  -- is stated here as an equation rather than trusted: a level admitted to the
+  -- new base vertex must already hold exactly @baseIndex / divisor@ samples,
+  -- and must receive the vertex at its end.
+  updateLevels
+    :: Int
+    -> [Triangulation 'Unconstrained Point () () ()]
+    -> Either BuildError [Triangulation 'Unconstrained Point () () ()]
+  updateLevels !_ [] = Right []
+  updateLevels !divisor levels@(level : rest)
+    | baseIndex `rem` divisor /= 0 = Right levels
+    | safeMultiply (numVertices level) divisor /= baseIndex =
+        Left
+          ( HierarchyLevelPopulationMismatch
+              divisor
+              (numVertices level)
+              baseIndex
+          )
+    | otherwise = do
+        inserted <- insert level point
+        if vertexIdIndex (insertionVertex inserted) /= numVertices level
+          then
+            Left
+              ( HierarchyInsertionHandleMismatch
+                  (VertexId (fromIntegral (numVertices level)))
+                  (insertionVertex inserted)
+              )
+          else (insertionTriangulation inserted :) <$> updateLevels (safeMultiply divisor branch) rest
+
+  -- With no levels at all the base was empty, so the vertex just appended is
+  -- vertex zero. Otherwise vertex zero is the finest level's local vertex
+  -- zero, under the same law that makes the descent a multiplication.
+  ensureSingletonTop [] = pure <$> singletonLevel point
+  ensureSingletonTop levels@(finest : _) =
+    case reverse levels of
+      top : _
+        | numVertices top <= 1 -> Right levels
+        | otherwise ->
+            (\first -> levels ++ [first])
+              <$> singletonLevel (vertexPoint finest (VertexId 0))
+      [] -> Right levels
+
+-- | Repair the nested hierarchy from a removal's swap report — the slot
+-- compaction freed and the position of the vertex it moved into that slot, or
+-- 'Nothing' when the removal took the last vertex and compaction moved
+-- nothing — rather than rebuilding because removal renumbers.
+--
+-- No triangulation is named, for the reason 'updateHierarchyAfterInsertion'
+-- gives: the only base position this repair cannot find in its own levels is
+-- the relocated vertex's, and that is what the report carries.
+--
+-- Swap compaction moves exactly one vertex — the former last one, into the slot
+-- the removed vertex vacated — so a level's sample sequence changes in at most
+-- one place, and which place is decided by the two divisibilities the level's
+-- divisor gives the freed slot and the vacated last index:
+--
+-- * neither is sampled: the level, and every coarser level above it, is
+--   untouched, because a divisor that divides neither index is divided by no
+--   multiple of itself either;
+-- * the vacated index is sampled and the freed slot is not: the level loses its
+--   last sample and nothing else;
+-- * both are sampled: the level loses its last sample and that sample's point
+--   lands in the freed slot's local position — which is the level's own swap
+--   removal, mirroring the base's;
+-- * the freed slot is sampled and the vacated index is not: the level keeps its
+--   population and substitutes the relocated position at an interior local
+--   slot. No removal expresses a substitution, so that level is rebuilt — from
+--   its own points and the reported one, never from a mesh.
+--
+-- The last case is the only one that pays a build, and it is the rarest: it
+-- needs the freed slot to be sampled and the vacated index not to be.
+updateHierarchyAfterRemoval
+  :: HierarchyHint
+  -> Maybe (VertexId, Point)
+  -> Either BuildError (HierarchyHint)
+updateHierarchyAfterRemoval hierarchy swap
+  | baseCount <= 0 = Left (RemovalEmptyTriangulation (maybe (VertexId 0) fst swap))
+  | otherwise = do
+      repaired <- repairLevels branch (V.toList (hierarchyLevels hierarchy))
+      pure
+        hierarchy
+          { hierarchyBaseCount = surviving
+          , hierarchyLevels = V.fromList (levelsThroughSingleton repaired)
+          }
+ where
+  !branch = hierarchyBranchFactor hierarchy
+  !baseCount = hierarchyBaseCount hierarchy
+  !surviving = baseCount - 1
+  -- The freed slot is the removed vertex's own index, and it is where the
+  -- former last vertex now stands. A removal that took the last vertex frees
+  -- no slot and reports none, and its removed index is that last index.
+  !freedSlot = maybe surviving (vertexIdIndex . fst) swap
+  !vacatedIndex = surviving
+
+  -- The nesting law is stated here as an equation for the same reason the
+  -- insertion path states it: a repair is the other operation that can break
+  -- it. A level the removal reaches must hold exactly the samples the
+  -- pre-removal base owed it.
+  repairLevels !_ [] = Right []
+  repairLevels !divisor levels@(level : rest)
+    | not freedSampled && not vacatedSampled = Right levels
+    | numVertices level /= population =
+        Left
+          ( HierarchyLevelPopulationMismatch
+              divisor
+              (numVertices level)
+              baseCount
+          )
+    | otherwise =
+        case swap of
+          -- The two divisibilities differ only when the two indices do, so a
+          -- level reaching the substitution has a relocation to substitute:
+          -- the guard cannot hold while compaction moved nothing.
+          Just (_, relocated)
+            | freedSampled && not vacatedSampled -> do
+                substituted <- substituteSample level (freedSlot `quot` divisor) relocated
+                (substituted :) <$> repairLevels (safeMultiply divisor branch) rest
+          _ -> do
+            shrunk <- removeVertex level (VertexId (fromIntegral localSample))
+            (removalTriangulation shrunk :) <$> repairLevels (safeMultiply divisor branch) rest
+   where
+    !freedSampled = freedSlot `rem` divisor == 0
+    !vacatedSampled = vacatedIndex `rem` divisor == 0
+    !population = samplePopulation baseCount divisor
+    !localSample
+      | freedSampled = freedSlot `quot` divisor
+      | otherwise = population - 1
+
+  -- A build stops at the first level holding one sample, and removal only
+  -- shrinks populations, so the shape a rebuild would answer with is this list
+  -- cut after its first singleton.
+  levelsThroughSingleton
+    :: [Triangulation mode vertex directed undirected face]
+    -> [Triangulation mode vertex directed undirected face]
+  levelsThroughSingleton [] = []
+  levelsThroughSingleton (level : rest)
+    | numVertices level <= 0 = []
+    | numVertices level <= 1 = [level]
+    | otherwise = level : levelsThroughSingleton rest
+
+-- | The coarsest level a growing hierarchy needs: one sample, the base's
+-- vertex zero.
+singletonLevel
+  :: Point
+  -> Either BuildError (Triangulation 'Unconstrained (Point) () () ())
+singletonLevel origin =
+  buildTriangulation <$> delaunay unitElementDefaults (V.singleton origin)
+
+-- | The level a substitution asks for: the same samples in the same local
+-- order, one slot carrying the relocated position instead of the one that
+-- left. Stated over the level's own points, so no mesh is consulted.
+substituteSample
+  :: Triangulation 'Unconstrained (Point) () () ()
+  -> Int
+  -> Point
+  -> Either BuildError (Triangulation 'Unconstrained (Point) () () ())
+substituteSample level localSlot relocated =
+  buildTriangulation
+    <$> delaunay
+      unitElementDefaults
+      ( V.generate
+          (numVertices level)
+          (\index ->
+            if index == localSlot
+              then relocated
+              else vertexPoint level (VertexId (fromIntegral index))
+          )
+      )
+
+-- | Rebuild a hierarchy after an operation that may renumber vertices, such as
+-- removal. The branch factor remains canonical.
+rebuildHierarchyHint
+  :: HierarchyHint
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError (HierarchyHint)
+rebuildHierarchyHint hierarchy = buildHierarchyHint (hierarchyBranchFactor hierarchy)
+
+-- | Remove many points, each locate starting from the hierarchy's nearest
+-- sample instead of the mesh boundary. One session publishes once; the
+-- hierarchy is rebuilt against the surviving mesh and returned alongside it.
+removeManyWithHierarchy
+  :: HierarchyHint
+  -> Triangulation mode vertex directed undirected face
+  -> V.Vector (Point)
+  -> Either
+      BuildError
+      ( V.Vector (Maybe (RemovalOutcome vertex))
+      , Triangulation mode vertex directed undirected face
+      , HierarchyHint
+      )
+removeManyWithHierarchy hierarchy triangulation points = do
+  queryPoints <- traverse (validatePoint Nothing) points
+  let guesses = fmap hierarchyGuess queryPoints
+  (outcomes, surviving, _) <-
+    withSession triangulation 0 (removeManyAtNear guesses points)
+  repaired <- rebuildHierarchyHint hierarchy surviving
+  pure (outcomes, surviving, repaired)
+ where
+  hierarchyGuess queryPoint =
+    case hierarchyHint hierarchy queryPoint of
+      Just (VertexHint vertex) -> Just vertex
+      _ -> Nothing
+
+safeMultiply :: Int -> Int -> Int
+safeMultiply left right
+  | left > maxBound `quot` right = maxBound
+  | otherwise = left * right
+
+-- | How many samples a divisor takes from a base of this size. Sampling takes
+-- index zero and every @divisor@-th index after it; the direct
+-- vector generator above states the same ceiling without constructing an
+-- intermediate handle list.
+samplePopulation :: Int -> Int -> Int
+samplePopulation count divisor
+  | count <= 0 = 0
+  | otherwise = 1 + (count - 1) `quot` divisor
diff --git a/src-build-public/Moonlight/Planar/Telemetry.hs b/src-build-public/Moonlight/Planar/Telemetry.hs
new file mode 100644
--- /dev/null
+++ b/src-build-public/Moonlight/Planar/Telemetry.hs
@@ -0,0 +1,105 @@
+-- | The operational layer beside the equational surface: the statistics the
+-- engine reports about its own work, and the verbs that name a session, a
+-- hint, or a query workspace. Nothing here is a function of the value alone,
+-- which is why none of it sits on the equational surface; every name is a
+-- re-export of its sublibrary owner, never a wrapper.
+module Moonlight.Planar.Telemetry
+  ( -- * Construction statistics — a triangulation is not a history of its
+    -- construction; the history is read here, beside the value, and two
+    -- results that differ only here are the same value
+    buildStats
+  , insertionStats
+  , PublicationStats (..)
+  , ClosureStats (..)
+
+    -- * Constraint statistics — what recovery and seam joins cost; the
+    -- constrained value they produced is observed on the surface
+  , ConstraintBatchStats (..)
+  , constraintBatchStats
+  , constrainedExtensionBuildStats
+  , constrainedSeamConstraintStats
+  , constrainedSeamBuildStats
+  , constrainedSeamPublicationStats
+
+    -- * Refinement statistics — the receipt's work counters; every mesh
+    -- projection of the receipt stays on the surface
+  , refinementStats
+  , refinementPublicationStats
+  , refinementClosureStats
+
+    -- * Location and query work — a hint preserves geometric support, not
+    -- necessarily the orientation of an edge witness. The same hint retains
+    -- the same oriented-witness semantics as the uninstrumented descent.
+  , LocationHint (..)
+  , LocationStats (..)
+  , emptyLocationStats
+  , locatePointWithHint
+  , NearestStats (..)
+  , nearestNeighbor
+
+    -- * Sessions — many edits behind one sealed thaw/freeze boundary; the
+    -- published value equals the fold of the persistent surface verbs
+  , Session
+  , withSession
+  , insertVertex
+  , insertVertexAt
+  , insertVertexAtNear
+  , insertVertexAtNearVertex
+  , removeAt
+  , removeAtNear
+  , removeManyAt
+  , removeManyAtNear
+  , refuse
+
+    -- * Incremental bulk insertion — the incremental route to the value
+    -- 'Moonlight.Planar.delaunay' publishes; exposed for its cost,
+    -- not for a different answer
+  , insertMany
+
+    -- * Hint-seeded interpolation — the same samples the surface's own
+    -- weights would select, reached from a caller-supplied seed
+  , interpolateNearest
+  , interpolateBarycentric
+  ) where
+
+import Moonlight.Planar.BulkLoad (insertMany)
+import Moonlight.Planar.Cdt
+  ( ConstraintBatchStats (..)
+  , constrainedExtensionBuildStats
+  , constrainedSeamBuildStats
+  , constrainedSeamConstraintStats
+  , constrainedSeamPublicationStats
+  , constraintBatchStats
+  )
+import Moonlight.Planar.Internal.Paged (PublicationStats (..))
+import Moonlight.Planar.Interpolation
+  ( interpolateBarycentric
+  , interpolateNearest
+  , nearestNeighbor
+  )
+import Moonlight.Planar.PointLocation (locatePointWithHint)
+import Moonlight.Planar.Session
+  ( Session
+  , insertVertex
+  , insertVertexAt
+  , insertVertexAtNear
+  , insertVertexAtNearVertex
+  , refuse
+  , removeAt
+  , removeAtNear
+  , removeManyAt
+  , removeManyAtNear
+  , withSession
+  )
+import Moonlight.Planar.Types
+  ( LocationHint (..)
+  , LocationStats (..)
+  , NearestStats (..)
+  , ClosureStats (..)
+  , buildStats
+  , emptyLocationStats
+  , insertionStats
+  , refinementPublicationStats
+  , refinementStats
+  , refinementClosureStats
+  )
diff --git a/src-build/Moonlight/Planar/BulkLoad.hs b/src-build/Moonlight/Planar/BulkLoad.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/BulkLoad.hs
@@ -0,0 +1,14 @@
+-- | Whole-support construction and persistent incremental insertion.
+module Moonlight.Planar.BulkLoad
+  ( empty
+  , clear
+  , delaunay
+  , delaunayGeometry
+  , DuplicatePayloadPolicy (..)
+  , delaunayFromCoordinates
+  , insert
+  , insertAt
+  , insertMany
+  ) where
+
+import Moonlight.Planar.Internal.BulkLoad
diff --git a/src-build/Moonlight/Planar/Canonical.hs b/src-build/Moonlight/Planar/Canonical.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Canonical.hs
@@ -0,0 +1,6 @@
+-- | Construction-independent resident numbering for explicit global comparison.
+module Moonlight.Planar.Canonical
+  ( canonicalize
+  ) where
+
+import Moonlight.Planar.Internal.Canonical (canonicalize)
diff --git a/src-build/Moonlight/Planar/Cdt.hs b/src-build/Moonlight/Planar/Cdt.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Cdt.hs
@@ -0,0 +1,119 @@
+-- | The constraint layer: the partial map from sites and segments, defined
+-- exactly on realizable segment sets and naming its witness where it is not.
+module Moonlight.Planar.Cdt
+  ( ConstrainedDelaunayTriangulation
+  , CdtError (..)
+  , CorridorObstruction (..)
+  , ConstraintRecoveryResult (..)
+  , ConstraintResult
+  , ConstraintOutcome (..)
+  , ConstraintBatchStats (..)
+  , ConstraintBatchResult (..)
+  , ConstrainedExtensionResult (..)
+  , ConstrainedSeamSide (..)
+  , ConstrainedSeamFaceEvidence
+  , constrainedSeamSourceFace
+  , constrainedSeamTargetFace
+  , constrainedSeamFaceFirstPoint
+  , constrainedSeamFaceSecondPoint
+  , constrainedSeamFaceThirdPoint
+  , ConstrainedSeamResult
+  , constrainedSeamResultTriangulation
+  , constrainedSeamLeftFaceCount
+  , constrainedSeamRightFaceEvidence
+  , constrainedSeamJoinFaces
+  , constrainedSeamLeftConstraintCount
+  , constrainedSeamConstraintStats
+  , constrainedSeamBuildStats
+  , constrainedSeamPublicationStats
+  , constrainedSeamCachedFrontierPointReads
+  , ConstraintSplitBatchResult
+  , CdtBuildResult (..)
+  , constrainedDelaunay
+  , constrainedDelaunayMaximal
+  , fromDelaunay
+  , constraintEdges
+  , CanonicalSegment
+  , segmentStart
+  , segmentEnd
+  , ConstraintConflict
+  , conflictFirstSegment
+  , conflictSecondSegment
+  , ConstrainedUnionError (..)
+  , constraintSegments
+  , unionConstrainedWith
+  , unionConstrained
+  , joinSeparatedConstrained
+  , extendConstrainedWith
+  , constraintStorageBytes
+  , existsConstraint
+  , canAddConstraint
+  , intersectsConstraint
+  , getConflictingEdgesBetweenPoints
+  , getConflictingEdgesBetweenVertices
+  , recoverConstraints
+  , addConstraintEdge
+  , addConstraintEdges
+  , addConstraintAndSplit
+  , addConstraintsAndSplit
+  , removeConstraintEdge
+  , outerRegionFaces
+  , boundedRegionFaces
+  ) where
+
+import Moonlight.Planar.Internal.Cdt.Batch (recoverConstraints)
+import Moonlight.Planar.Internal.Cdt.Build
+  ( constrainedDelaunay
+  , constrainedDelaunayMaximal
+  , fromDelaunay
+  )
+import Moonlight.Planar.Internal.Cdt.Query
+  ( canAddConstraint
+  , constraintEdges
+  , constraintStorageBytes
+  , existsConstraint
+  , getConflictingEdgesBetweenPoints
+  , getConflictingEdgesBetweenVertices
+  , intersectsConstraint
+  )
+import Moonlight.Planar.Internal.Cdt.Region
+  ( boundedRegionFaces
+  , outerRegionFaces
+  )
+import Moonlight.Planar.Internal.Cdt.Segment
+  ( addConstraintEdge
+  , addConstraintEdges
+  , removeConstraintEdge
+  )
+import Moonlight.Planar.Internal.Cdt.Split
+  ( addConstraintAndSplit
+  , addConstraintsAndSplit
+  )
+import Moonlight.Planar.Internal.Cdt.Types
+  ( CanonicalSegment (..)
+  , CdtBuildResult (..)
+  , CdtError (..)
+  , ConstrainedUnionError (..)
+  , ConstraintBatchResult (..)
+  , ConstraintBatchStats (..)
+  , ConstrainedExtensionResult (..)
+  , ConstrainedSeamSide (..)
+  , ConstrainedSeamFaceEvidence (..)
+  , ConstrainedSeamResult (..)
+  , ConstraintConflict (..)
+  , ConstraintOutcome (..)
+  , ConstraintRecoveryResult (..)
+  , ConstraintResult
+  , ConstraintSplitBatchResult
+  , CorridorObstruction (..)
+  )
+import Moonlight.Planar.Internal.Cdt.Union
+  ( constraintSegments
+  , extendConstrainedWith
+  , joinSeparatedConstrained
+  , unionConstrained
+  , unionConstrainedWith
+  )
+import Moonlight.Planar.Internal.Representation
+  ( ConstrainedDelaunayTriangulation
+  )
diff --git a/src-build/Moonlight/Planar/Insertion.hs b/src-build/Moonlight/Planar/Insertion.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Insertion.hs
@@ -0,0 +1,214 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Moonlight.Planar.Insertion
+  ( insertExistingVertex
+  , insertExistingVertexWithHint
+  , insertPointCombining
+  , insertAdmittedPointCombining
+  , insertVertexAtPoint
+  , insertExistingVertexAtLocation
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Internal.DcelOperations.Chain
+  ( extendLine
+  , lineToArea
+  , setupFirstVertex
+  , setupSecondVertex
+  , splitLineEdge
+  )
+import Moonlight.Planar.Internal.DcelOperations.Hull (insertOutsideHull)
+import Moonlight.Planar.Internal.DcelOperations.Subdivide
+  ( insertIntoFace
+  , insertOnEdge
+  )
+import Moonlight.Planar.Internal.Location
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  )
+import Moonlight.Planar.Internal.Probe (KnownProbe, Probe (..))
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Types
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, ExistingPoints, DuplicatePoints))
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+
+-- | Admit a raw site, then delegate to the shared admitted-site interpreter.
+-- Geometry decides whether a site is new; callers choose only the annotation
+-- law for an occupied site. Keeping raw admission and the common location,
+-- placement, and counters in this one owner prevents sessions and constrained
+-- extension from drifting into two insertion semantics merely because they own
+-- different enclosing transactions.
+insertPointCombining
+  :: (vertex -> vertex -> vertex)
+  -> Maybe Int
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Point
+  -> vertex
+  -> ST s (Either BuildError (Int, InsertionDisposition))
+insertPointCombining combine seed mutable operation point payload =
+  case validatePoint Nothing point of
+    Left failure -> pure (Left failure)
+    Right admitted ->
+      insertAdmittedPointCombining combine seed mutable operation admitted payload
+{-# INLINE insertPointCombining #-}
+
+-- | Insert an already admitted site. The caller has retained the canonical
+-- coordinate section, so this interpreter only locates and glues the payload;
+-- it must not reopen coordinate admission.
+insertAdmittedPointCombining
+  :: (vertex -> vertex -> vertex)
+  -> Maybe Int
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> QueryPoint
+  -> vertex
+  -> ST s (Either BuildError (Int, InsertionDisposition))
+insertAdmittedPointCombining combine seed mutable operation queryPoint payload = do
+  addCounter operation (Reported InputPoints) 1
+  outcome <-
+    insertVertexAtPoint
+      @'ProbeOff
+      mutable
+      operation
+      seed
+      (queryPointValue queryPoint)
+      payload
+  case outcome of
+    Left failure -> pure (Left failure)
+    Right resolved@(vertex, disposition) -> do
+      case disposition of
+        AlreadyPresent -> do
+          resident <- vertexDataAt mutable vertex
+          writeVertexData mutable vertex (combine resident payload)
+          addCounter operation (Reported ExistingPoints) 1
+          addCounter operation (Reported DuplicatePoints) 1
+        Inserted -> addCounter operation (Reported UniquePoints) 1
+      pure (Right resolved)
+{-# INLINE insertAdmittedPointCombining #-}
+
+insertExistingVertex
+  :: forall p s vertex directed undirected face
+   . KnownProbe p
+  => MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError ())
+insertExistingVertex mutable operation = insertExistingVertexWithHint @p mutable operation Nothing
+
+-- | Insert a materialized vertex while beginning point location from a face
+-- already known to be geometrically adjacent to the request. The hint changes
+-- only the amount of walking; the located site remains authoritative.
+insertExistingVertexWithHint
+  :: forall p s vertex directed undirected face
+   . KnownProbe p
+  => MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Int
+  -> ST s (Either BuildError ())
+insertExistingVertexWithHint mutable operation hint vertex = do
+  query <- pointAt mutable vertex
+  located <- locateMutable mutable operation hint query
+  case located of
+    Left obstruction -> pure (Left obstruction)
+    Right site -> insertExistingVertexAtLocation @p mutable operation vertex site
+
+-- | Locate before materializing a point, so duplicate detection remains a
+-- topological fact rather than a resident coordinate index.
+insertVertexAtPoint
+  :: forall p s vertex directed undirected face
+   . KnownProbe p
+  => MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> vertex
+  -> ST s (Either BuildError (Int, InsertionDisposition))
+insertVertexAtPoint mutable operation hint point vertexData = do
+  located <- locateMutable mutable operation hint point
+  case located of
+    Left obstruction -> pure (Left obstruction)
+    Right (MutableOnVertex existing) -> pure (Right (existing, AlreadyPresent))
+    Right site -> do
+      capacity <- ensurePointCapacity mutable 1
+      case capacity of
+        Left obstruction -> pure (Left obstruction)
+        Right () -> do
+          vertex <- appendVertex mutable point vertexData
+          inserted <- insertExistingVertexAtLocation @p mutable operation vertex site
+          pure ((vertex, Inserted) <$ inserted)
+
+-- | Interpret a point-location result without locating the same point again.
+-- Callers may hold this witness only while no topology mutation intervenes.
+--
+-- The site decides which counts are consulted, and the vertex's own point is
+-- read only by the two strata that compare against it — the degenerate line and
+-- the failure report. The area strata already stand on a located site and would
+-- otherwise rebuild a point the locate stage was handed.
+insertExistingVertexAtLocation
+  :: forall p s vertex directed undirected face
+   . KnownProbe p
+  => MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> MutableLocation
+  -> ST s (Either BuildError ())
+insertExistingVertexAtLocation mutable operation vertex located =
+  case located of
+    MutableOnVertex existing ->
+      pure
+        ( Left
+            ( FreshInsertionMatchedExistingVertex
+                (VertexId (fromIntegral vertex))
+                (VertexId (fromIntegral existing))
+            )
+        )
+    MutableEmpty -> do
+      connected <- connectedCount mutable
+      if connected == 0
+        then setupFirstVertex mutable vertex >> pure (Right ())
+        else locationFailed
+    MutableOnEdge edge -> do
+      faces <- faceCount mutable
+      if faces <= 1
+        then splitLineEdge mutable operation edge vertex
+        else insertOnEdge @p mutable operation edge vertex
+    MutableInFace face -> do
+      faces <- faceCount mutable
+      if faces <= 1
+        then locationFailed
+        else insertIntoFace @p mutable operation face vertex
+    MutableOutsideHull edge -> do
+      connected <- connectedCount mutable
+      if connected == 1
+        then setupSecondVertex mutable vertex
+        else do
+          faces <- faceCount mutable
+          if faces <= 1
+            then extendDegenerateLine edge
+            else insertOutsideHull @p mutable operation edge vertex
+ where
+  extendDegenerateLine edge = do
+    from <- edgeOriginPoint mutable edge
+    to <- edgeOriginPoint mutable (edge `xor` 1)
+    query <- pointAt mutable vertex
+    if orient2d from to query == EQ
+      then do
+        endpoint <- readOrigin mutable edge
+        extendLine mutable operation endpoint vertex
+      else lineToArea @p mutable operation vertex
+
+  locationFailed :: ST s (Either BuildError ())
+  locationFailed = do
+    query <- pointAt mutable vertex
+    pure (Left (PointLocationFailed query))
diff --git a/src-build/Moonlight/Planar/Internal/BulkLoad.hs b/src-build/Moonlight/Planar/Internal/BulkLoad.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/BulkLoad.hs
@@ -0,0 +1,949 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | Generation. @delaunay@ builds a mesh from a whole site set by circle sweep;
+-- the insertion verbs extend an existing mesh one site at a time.
+module Moonlight.Planar.Internal.BulkLoad
+  ( empty
+  , clear
+  , delaunay
+  , delaunayGeometry
+  , DuplicatePayloadPolicy (..)
+  , delaunayFromCoordinates
+  , delaunayFromAdmittedDistinctAscending
+  , insert
+  , insertAt
+  , insertMany
+  ) where
+
+import Control.Monad (forM_)
+import Control.Monad.ST (ST, runST)
+import qualified Data.IntSet as IntSet
+import Data.Primitive.PrimArray
+  ( MutablePrimArray
+  , newPrimArray
+  , readPrimArray
+  , unsafeFreezePrimArray
+  , writePrimArray
+  )
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Dcel (numInnerFaces, numVertices)
+import Moonlight.Planar.Internal.HandleDefs
+  ( VertexId (..)
+  , directedEdgeIdIndex
+  , faceIdIndex
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.BoxedPaged (BoxedFill (..), boxedFromVector, boxedUpdate, emptyBoxedPaged)
+import Moonlight.Planar.Internal.Capacity (ensureCapacity)
+import Moonlight.Planar.Insertion (insertExistingVertexAtLocation, insertVertexAtPoint)
+import Moonlight.Planar.Internal.Location (MutableLocation (..))
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , finalizeBuildStats
+  , newOperationState
+  , setCounter
+  )
+import Moonlight.Planar.Internal.CircleSweep
+  ( RadiallyOrderedArena
+  , circleSweepInsert
+  , radiallyOrderArena
+  , radiallyOrderGeometry
+  )
+import Moonlight.Planar.Internal.PointIndex
+  ( MutablePointIndex
+  , emptyPointIndex
+  , newMutablePointIndex
+  , resolveMutablePoint
+  , seedMutablePointIndex
+  )
+import Moonlight.Planar.Scalar (canonicalScalarZero)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.PointLocation (locatePointWithHint)
+import Moonlight.Planar.Internal.Probe (Probe (..))
+import Moonlight.Planar.Internal.Representation (CanonicalAdmission (..), Triangulation (..), authoringElementDefaults)
+import Moonlight.Planar.Internal.PackedIndex (noIndex, unpackIndex)
+import Moonlight.Planar.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction), emptyPaged, fromVector)
+import Moonlight.Planar.Internal.Transaction (runTransaction)
+import Moonlight.Planar.Types
+import Moonlight.Planar.BuildStats (BuildStats, tabulateBuildStats, updateBuildStats, buildStat, BuildMetric (InputPoints, UniquePoints, ExistingPoints, DuplicatePoints, SpatialSeedPoints, LocationWalkSteps, LocationFallbacks, LocationMaxWalk))
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue, HasPosition (..))
+
+-- | The vertexless triangulation: the outer face and nothing else. This is the
+-- canonical origin of the type — bulk loading, incremental insertion and
+-- refinement all agree with growing this value.
+empty
+  :: ElementDefaults directed undirected face
+  -> Triangulation mode vertex directed undirected face
+empty ElementDefaults{defaultDirectedEdgeData, defaultUndirectedEdgeData, defaultFaceData} =
+  Triangulation
+    { triPointX = emptyPaged
+    , triPointY = emptyPaged
+    , triPointIndex = emptyPointIndex
+    , triVertexOut = emptyPaged
+    , triVertexData = emptyBoxedPaged NoFill
+    , triHalfTopology = emptyPaged
+    , triDirectedData = emptyBoxedPaged (Fill defaultDirectedEdgeData)
+    , triUndirectedData = emptyBoxedPaged (Fill defaultUndirectedEdgeData)
+    , triFaceEdge = fromVector noIndex (U.singleton noIndex)
+    , triFaceData = boxedFromVector (Fill defaultFaceData) (V.singleton defaultFaceData)
+    , triConstraint = emptyPaged
+    , triConstraintCount = 0
+    , triConstraintEdges = IntSet.empty
+    , triSeamFrontier = Nothing
+    , triCanonical = CanonicalUnknown
+    }
+
+-- | Discard every vertex while retaining the element defaults the
+-- triangulation was built with.
+clear
+  :: Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face
+clear = empty . authoringElementDefaults
+
+-- | How a canonical bulk source combines payloads whose exact coordinates
+-- coincide. Geometry identity is settled independently by the point index.
+data DuplicatePayloadPolicy vertex
+  = KeepFirstPayload
+  | CombineDuplicatePayload !(vertex -> vertex -> vertex)
+
+-- | The two lawful ingress sections for the one bulk construction engine.
+-- Raw callers need coordinate admission and duplicate classification; an exact
+-- SiteSet has already established canonical, distinct, lexicographically
+-- ascending 'QueryPoint's. The latter remains package-internal through the
+-- private build component, rather than becoming a second constructor.
+data BulkInput vertex
+  = RawPossiblyDuplicate
+      !(Int -> Point)
+      !(DuplicatePayloadPolicy vertex)
+  | AdmittedDistinctAscending !(Int -> QueryPoint)
+
+-- | The local identity verdict for one input position. A fresh verdict carries
+-- the canonical coordinates already admitted to the mutable DCEL, so ingress
+-- never rereads its own writes merely to accumulate the sweep centre.
+data PositionClaim
+  = ResidentPosition !Int
+  | FreshPosition !Int !Double !Double
+
+-- | Build a finite Delaunay DCEL while preserving the first input payload at
+-- every duplicate position. The returned mapping relates every input slot to
+-- the canonical stored vertex.
+delaunay
+  :: forall vertex directed undirected face
+   . HasPosition vertex
+  => ElementDefaults directed undirected face
+  -> V.Vector vertex
+  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
+delaunay defaults input =
+  buildDelaunayFromSource
+    defaults
+    (V.length input)
+    (RawPossiblyDuplicate (position . (input V.!)) KeepFirstPayload)
+    (input V.!)
+
+-- | Build the geometry-only Delaunay triangulation of a coordinate vector.
+-- Exact duplicate positions collapse to one site. Use 'delaunay' or
+-- 'delaunayFromCoordinates' when vertex annotations or the input-to-vertex
+-- mapping are part of the result.
+delaunayGeometry
+  :: V.Vector Point
+  -> Either BuildError (DelaunayTriangulation ())
+delaunayGeometry coordinates
+  | inputCount == 0 = Right (empty unitElementDefaults)
+  | otherwise = do
+    V.iforM_ coordinates (\index point -> () <$ validatePoint (Just index) point)
+    ensureCapacity inputCount
+    let !canonicalArena =
+          U.generate inputCount $ \index ->
+            case coordinates V.! index of
+              Point rawX rawY ->
+                ( 0
+                , canonicalScalarZero rawX
+                , canonicalScalarZero rawY
+                , fromIntegral index
+                )
+        (!inputSumX, !inputSumY) =
+          U.foldl'
+            (\(!sumX, !sumY) (_, x, y, _) -> (sumX + x, sumY + y))
+            (0, 0)
+            canonicalArena
+    runST $ do
+      inputArena <- U.unsafeThaw canonicalArena
+      let !inputScale = recip (fromIntegral inputCount)
+          !inputCenterX = inputSumX * inputScale
+          !inputCenterY = inputSumY * inputScale
+          (distanceArena, pointXMutable, pointYMutable, inputOrder) =
+            MUV.unzip4 inputArena
+      assignGeometryRadialDistances
+        distanceArena
+        pointXMutable
+        pointYMutable
+        inputCenterX
+        inputCenterY
+      distances <- U.unsafeFreeze distanceArena
+      pointXs <- U.unsafeFreeze pointXMutable
+      pointYs <- U.unsafeFreeze pointYMutable
+      orderedInputArena <-
+        radiallyOrderGeometry distances pointXs pointYs inputOrder
+      defaultedOutcome <-
+        newMutableDcelWithVertexDefault
+          ()
+          unitElementDefaults
+          (planarDcelCapacity inputCount)
+      case defaultedOutcome of
+        Left failure -> pure (Left failure)
+        Right defaultedMutable -> do
+          let !mutable = defaultedVertexDcel defaultedMutable
+              !dense = defaultedDenseVertexDcel defaultedMutable
+          operation <- newOperationState (halfEdgeCapacity mutable)
+          unique <- appendSortedGeometry defaultedMutable pointXs pointYs inputOrder
+          orderedArena <-
+            if unique == inputCount
+              then pure orderedInputArena
+              else do
+                let !scale = recip (fromIntegral unique)
+                    !uniqueOrder = MUV.unsafeSlice 0 unique inputOrder
+                (sumX, sumY) <- sumGeometryVertices dense uniqueOrder
+                uniqueArena <-
+                  fillRadialArena
+                    mutable
+                    (sumX * scale)
+                    (sumY * scale)
+                    (MUV.unsafeRead uniqueOrder)
+                    unique
+                radiallyOrderArena uniqueArena
+          inserted <- circleSweepInsert mutable operation orderedArena
+          case inserted of
+            Left failure -> pure (Left failure)
+            Right _ -> freezeTriangulation mutable
+ where
+  !inputCount = V.length coordinates
+
+  assignGeometryRadialDistances
+    :: forall s
+     . MUV.MVector s Double
+    -> MUV.MVector s Double
+    -> MUV.MVector s Double
+    -> Double
+    -> Double
+    -> ST s ()
+  assignGeometryRadialDistances distances pointXs pointYs centerX centerY =
+    MUV.imapM_
+      (\index _ -> do
+        x <- MUV.unsafeRead pointXs index
+        y <- MUV.unsafeRead pointYs index
+        let !deltaX = centerX - x
+            !deltaY = centerY - y
+        MUV.unsafeWrite distances index (deltaX * deltaX + deltaY * deltaY)
+      )
+      distances
+
+  appendSortedGeometry
+    :: forall s
+     . DefaultedVertexDcel s () () () ()
+    -> U.Vector Double
+    -> U.Vector Double
+    -> MUV.MVector s Word32
+    -> ST s Int
+  appendSortedGeometry defaultedMutable pointXs pointYs order
+    | inputCount == 0 = pure 0
+    | otherwise = do
+        firstInput <- MUV.unsafeRead order 0
+        let !x = U.unsafeIndex pointXs (fromIntegral firstInput)
+            !y = U.unsafeIndex pointYs (fromIntegral firstInput)
+        denseWriteFreshDefaultPoint defaultedMutable 0 x y
+        MUV.unsafeWrite order 0 0
+        (_, _, unique) <-
+          MUV.foldM'
+            (\(!previousX, !previousY, !uniqueCount) nextInput ->
+               let !nextX = U.unsafeIndex pointXs (fromIntegral nextInput)
+                   !nextY = U.unsafeIndex pointYs (fromIntegral nextInput)
+                in if nextX == previousX && nextY == previousY
+                     then pure (previousX, previousY, uniqueCount)
+                     else do
+                       denseWriteFreshDefaultPoint defaultedMutable uniqueCount nextX nextY
+                       MUV.unsafeWrite order uniqueCount (fromIntegral uniqueCount)
+                       pure (nextX, nextY, uniqueCount + 1)
+            )
+            (x, y, 1)
+            (MUV.unsafeSlice 1 (inputCount - 1) order)
+        unique <$ denseCommitFreshDefaultPoints defaultedMutable unique
+
+  sumGeometryVertices
+    :: forall s
+     . DenseMutableDcel s () () () ()
+    -> MUV.MVector s Word32
+    -> ST s (Double, Double)
+  sumGeometryVertices dense vertices =
+    MUV.foldM'
+      (\(!sumX, !sumY) raw -> do
+         x <- denseReadPointX dense (unpackIndex raw)
+         y <- denseReadPointY dense (unpackIndex raw)
+         pure (sumX + x, sumY + y)
+      )
+      (0, 0)
+      vertices
+
+-- | Canonical construction from separate geometry and annotation sources.
+-- The coordinate vector remains the only geometry in ingress; payloads never
+-- acquire a fabricated 'HasPosition' instance merely to reach the loader.
+delaunayFromCoordinates
+  :: forall vertex directed undirected face
+   . ElementDefaults directed undirected face
+  -> V.Vector (Point)
+  -> V.Vector vertex
+  -> DuplicatePayloadPolicy vertex
+  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
+delaunayFromCoordinates defaults coordinates payloads duplicatePolicy
+  | coordinateCount /= payloadCount =
+      Left (CoordinatePayloadCountMismatch coordinateCount payloadCount)
+  | otherwise =
+      buildDelaunayFromSource
+        defaults
+        coordinateCount
+        (RawPossiblyDuplicate (coordinates V.!) duplicatePolicy)
+        (payloads V.!)
+ where
+  !coordinateCount = V.length coordinates
+  !payloadCount = V.length payloads
+
+-- | Construction from an already-admitted exact site section. The caller
+-- supplies Map-ascending, distinct 'QueryPoint's; this skips only raw
+-- admission, duplicate classification, and unique-site recentering. It still
+-- derives the radial execution order and runs the one circle sweep.
+delaunayFromAdmittedDistinctAscending
+  :: forall vertex directed undirected face
+   . ElementDefaults directed undirected face
+  -> V.Vector (QueryPoint, vertex)
+  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
+delaunayFromAdmittedDistinctAscending defaults input =
+  buildDelaunayFromSource
+    defaults
+    (V.length input)
+    (AdmittedDistinctAscending (\index -> fst (input V.! index)))
+    (\index -> snd (input V.! index))
+{-# INLINE delaunayFromAdmittedDistinctAscending #-}
+
+buildDelaunayFromSource
+  :: forall vertex directed undirected face
+   . ElementDefaults directed undirected face
+  -> Int
+  -> BulkInput vertex
+  -> (Int -> vertex)
+  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
+buildDelaunayFromSource defaults inputCount input payloadAtInput = do
+  ensureCapacity inputCount
+  runST $ do
+    inputArena <- MUV.new inputCount
+    admitted <- initializeInputArena inputArena
+    case admitted of
+      Left failure -> pure (Left failure)
+      Right (inputSumX, inputSumY) -> do
+        mutable <- newMutableDcel defaults (planarDcelCapacity inputCount)
+        operation <- newOperationState (halfEdgeCapacity mutable)
+        (mapping, unique, orderedArena) <-
+          case input of
+            RawPossiblyDuplicate pointAtInput duplicatePolicy ->
+              prepareRawInput
+                inputArena
+                mutable
+                pointAtInput
+                duplicatePolicy
+                inputSumX
+                inputSumY
+            AdmittedDistinctAscending _ ->
+              prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY
+        setCounter operation (Reported InputPoints) inputCount
+        setCounter operation (Reported UniquePoints) unique
+        setCounter operation (Reported DuplicatePoints) (inputCount - unique)
+        inserted <-
+          if unique == 0
+            then pure (Right 0)
+            else circleSweepInsert mutable operation orderedArena
+        case inserted of
+          Left failure -> pure (Left failure)
+          Right seedCount -> do
+            setCounter operation (Reported SpatialSeedPoints) seedCount
+            frozenOutcome <- freezeTriangulation mutable
+            case frozenOutcome of
+              Left failure -> pure (Left failure)
+              Right frozen -> do
+                mapped <- unsafeFreezePrimArray mapping
+                stats <- finalizeBuildStats operation
+                pure
+                  ( Right
+                      BuildResult
+                        { buildTriangulation = frozen
+                        , buildInputVertices = mapped
+                        , buildStats = stats
+                        }
+                  )
+ where
+  -- Raw sources validate and canonicalize at this one ingress. Admitted
+  -- sources merely project their retained QueryPoint evidence into the sweep
+  -- arena; neither route reconstructs coordinate identity downstream.
+  initializeInputArena
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> ST s (Either BuildError (Double, Double))
+  initializeInputArena arena =
+    case input of
+      RawPossiblyDuplicate pointAtInput _ ->
+        initializeRawInputArena arena pointAtInput 0 0 0
+      AdmittedDistinctAscending pointAtInput ->
+        initializeAdmittedInputArena arena pointAtInput
+
+  initializeRawInputArena
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> (Int -> Point)
+    -> Int
+    -> Double
+    -> Double
+    -> ST s (Either BuildError (Double, Double))
+  initializeRawInputArena arena pointAtInput !index !sumX !sumY
+    | index >= inputCount = pure (Right (sumX, sumY))
+    | otherwise =
+        case validatePoint (Just index) (pointAtInput index) of
+          Left failure -> pure (Left failure)
+          Right admitted ->
+            case queryPointValue admitted of
+              Point x y -> do
+                MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
+                initializeRawInputArena
+                  arena
+                  pointAtInput
+                  (index + 1)
+                  (sumX + x)
+                  (sumY + y)
+
+  initializeAdmittedInputArena
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> (Int -> QueryPoint)
+    -> ST s (Either BuildError (Double, Double))
+  initializeAdmittedInputArena arena pointAtInput =
+    Right
+      <$> MUV.ifoldM'
+        (\(!sumX, !sumY) index _ ->
+           case queryPointValue (pointAtInput index) of
+             Point x y -> do
+               MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
+               pure (sumX + x, sumY + y)
+        )
+        (0, 0)
+        arena
+
+  prepareRawInput
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> MutableDcel s vertex directed undirected face
+    -> (Int -> Point)
+    -> DuplicatePayloadPolicy vertex
+    -> Double
+    -> Double
+    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
+  prepareRawInput inputArena mutable pointAtInput duplicatePolicy inputSumX inputSumY = do
+    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
+        !inputCenterX = inputSumX * inputScale
+        !inputCenterY = inputSumY * inputScale
+    assignRadialDistances inputArena inputCenterX inputCenterY 0
+    orderedInputArena <- radiallyOrderArena inputArena
+    mapping <- newPrimArray inputCount
+    unique <- classifySortedInputs mapping inputArena
+    (sumX, sumY) <-
+      appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy 0 0 0
+    orderedArena <-
+      if unique == inputCount
+        then pure orderedInputArena
+        else do
+          let !scale = recip (fromIntegral unique)
+              !uniqueArena = MUV.unsafeSlice 0 unique inputArena
+          rewriteCompactedArena
+            mapping
+            (sumX * scale)
+            (sumY * scale)
+            uniqueArena
+            0
+          radiallyOrderArena uniqueArena
+    pure (mapping, unique, orderedArena)
+
+  prepareAdmittedDistinctInput
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> MutableDcel s vertex directed undirected face
+    -> Double
+    -> Double
+    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
+  prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY = do
+    mapping <- newPrimArray inputCount
+    appendAdmittedDistinctInputs mutable mapping inputArena
+    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
+        !inputCenterX = inputSumX * inputScale
+        !inputCenterY = inputSumY * inputScale
+    assignRadialDistances inputArena inputCenterX inputCenterY 0
+    orderedArena <- radiallyOrderArena inputArena
+    pure (mapping, inputCount, orderedArena)
+
+  -- An admitted SiteSet is already distinct. Sequential append therefore
+  -- transports the identity class map directly into authoritative vertex ids,
+  -- without reopening duplicate policy or recentering a compacted arena.
+  appendAdmittedDistinctInputs
+    :: forall s
+     . MutableDcel s vertex directed undirected face
+    -> MutablePrimArray s Word32
+    -> MUV.MVector s (Double, Double, Double, Word32)
+    -> ST s ()
+  appendAdmittedDistinctInputs mutable mapping arena =
+    MUV.imapM_
+      (\index (_, x, y, _) -> do
+         vertex <- appendVertexCoordinates mutable x y (payloadAtInput index)
+         writePrimArray mapping index (fromIntegral vertex)
+         MUV.unsafeWrite arena index (0, x, y, fromIntegral vertex)
+      )
+      arena
+
+  -- The all-input centre seeds the first radial ordering. Duplicate
+  -- equivalence classes become adjacent in that total order; after descent,
+  -- the compacted section is recentered over unique sites and ordered once
+  -- more only when required.
+  assignRadialDistances
+    :: forall s
+     . MUV.MVector s (Double, Double, Double, Word32)
+    -> Double
+    -> Double
+    -> Int
+    -> ST s ()
+  assignRadialDistances arena centerX centerY !index
+    | index >= inputCount = pure ()
+    | otherwise = do
+        (_, x, y, inputOrdinal) <- MUV.unsafeRead arena index
+        let !deltaX = centerX - x
+            !deltaY = centerY - y
+        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, inputOrdinal)
+        assignRadialDistances arena centerX centerY (index + 1)
+
+  -- Classify the sorted local sections by exact canonical position. The
+  -- mapping first names each class by its earliest input slot. Compacting the
+  -- unique radial representatives in place cannot overwrite an unread slot.
+  classifySortedInputs
+    :: forall s
+     . MutablePrimArray s Word32
+    -> MUV.MVector s (Double, Double, Double, Word32)
+    -> ST s Int
+  classifySortedInputs mapping arena
+    | inputCount == 0 = pure 0
+    | otherwise = do
+        first@(_, firstX, firstY, firstInput) <- MUV.unsafeRead arena 0
+        writePrimArray mapping (fromIntegral firstInput) firstInput
+        classifyFrom first firstX firstY firstInput 1 1
+   where
+    classifyFrom
+      :: (Double, Double, Double, Word32)
+      -> Double
+      -> Double
+      -> Word32
+      -> Int
+      -> Int
+      -> ST s Int
+    classifyFrom !_ !previousX !previousY !classInput !readIndex !uniqueCount
+      | readIndex >= inputCount = pure uniqueCount
+      | otherwise = do
+          record@(_, x, y, inputOrdinal) <- MUV.unsafeRead arena readIndex
+          if x == previousX && y == previousY
+            then do
+              writePrimArray mapping (fromIntegral inputOrdinal) classInput
+              classifyFrom record previousX previousY classInput (readIndex + 1) uniqueCount
+            else do
+              writePrimArray mapping (fromIntegral inputOrdinal) inputOrdinal
+              if uniqueCount == readIndex
+                then pure ()
+                else MUV.unsafeWrite arena uniqueCount record
+              classifyFrom record x y inputOrdinal (readIndex + 1) (uniqueCount + 1)
+
+  -- Materialize in original order, preserving the established handle
+  -- assignment and duplicate-payload law. The equivalence mapping for a
+  -- duplicate always points backward to an already materialized class owner.
+  appendClassifiedInputs
+    :: forall s
+     . MutableDcel s vertex directed undirected face
+    -> MutablePrimArray s Word32
+    -> (Int -> Point)
+    -> DuplicatePayloadPolicy vertex
+    -> Int
+    -> Double
+    -> Double
+    -> ST s (Double, Double)
+  appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy !index !sumX !sumY
+    | index >= inputCount = pure (sumX, sumY)
+    | otherwise = do
+        classInput <- readPrimArray mapping index
+        let !vertexData = payloadAtInput index
+        if fromIntegral classInput == index
+          then case pointAtInput index of
+            Point x y -> do
+              let !canonicalX = canonicalScalarZero x
+                  !canonicalY = canonicalScalarZero y
+              vertex <- appendVertexCoordinates mutable canonicalX canonicalY vertexData
+              writePrimArray mapping index (fromIntegral vertex)
+              appendClassifiedInputs
+                mutable
+                mapping
+                pointAtInput
+                duplicatePolicy
+                (index + 1)
+                (sumX + canonicalX)
+                (sumY + canonicalY)
+          else do
+            resident <- fromIntegral <$> readPrimArray mapping (fromIntegral classInput)
+            writePrimArray mapping index (fromIntegral resident)
+            case duplicatePolicy of
+              KeepFirstPayload -> pure ()
+              CombineDuplicatePayload combine -> do
+                residentData <- vertexDataAt mutable resident
+                writeVertexData mutable resident (combine residentData vertexData)
+            appendClassifiedInputs
+              mutable
+              mapping
+              pointAtInput
+              duplicatePolicy
+              (index + 1)
+              sumX
+              sumY
+
+  -- Once duplicates have shortened the vertex arena, translate each compacted
+  -- representative from its input-class name to its authoritative vertex and
+  -- restate its radial key around the exact unique-site centre.
+  rewriteCompactedArena
+    :: forall s
+     . MutablePrimArray s Word32
+    -> Double
+    -> Double
+    -> MUV.MVector s (Double, Double, Double, Word32)
+    -> Int
+    -> ST s ()
+  rewriteCompactedArena mapping centerX centerY arena !index
+    | index >= MUV.length arena = pure ()
+    | otherwise = do
+        (_, x, y, classInput) <- MUV.unsafeRead arena index
+        vertex <- readPrimArray mapping (fromIntegral classInput)
+        let !deltaX = centerX - x
+            !deltaY = centerY - y
+        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, vertex)
+        rewriteCompactedArena mapping centerX centerY arena (index + 1)
+
+-- | Insert or replace a vertex payload. A payload at an existing position is
+-- overwritten without changing topology.
+--
+-- The published mesh is independent of the one passed in. A singleton below
+-- ten thousand resident sites copies densely; larger bases publish through
+-- copy-on-write pages. A caller inserting a sequence wants one
+-- 'Moonlight.Planar.Session.withSession' over
+-- 'Moonlight.Planar.Session.insertVertex' instead — see 'insertAt'.
+insert
+  :: HasPosition vertex
+  => Triangulation mode vertex directed undirected face
+  -> vertex
+  -> Either BuildError (InsertionResult mode vertex directed undirected face)
+insert triangulation vertexData = insertAt triangulation (position vertexData) vertexData
+
+-- | Insert at a stated point. 'insert' is this with the point read out of the
+-- payload, which is what a caller holding only a payload wants; a caller that
+-- computed the point — a constraint split, a Steiner refinement — wants to say
+-- so rather than build a payload and hope the round trip through 'HasPosition'
+-- returns what it started with.
+--
+-- This is one shaped transaction over a single insertion. Replacing a fold of
+-- it with one session is sound because the two agree on every mesh and differ
+-- only in how many intermediate meshes they publish. A fold publishes @k@
+-- meshes and pays a thaw for each, so it runs in Θ(n·k); the session pays one
+-- thaw and runs in O(k·log n) expected.
+insertAt
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> vertex
+  -> Either BuildError (InsertionResult mode vertex directed undirected face)
+insertAt triangulation rawPoint vertexData = do
+  queryPoint <- validatePoint Nothing rawPoint
+  case locatePointWithHint triangulation Nothing queryPoint of
+    (OnVertex resident, walked) ->
+      Right (replaceResidentPayload triangulation resident walked vertexData)
+    (located, walked) -> do
+      let transactionShape =
+            if numVertices triangulation < 10_000
+              then DenseTransaction
+              else LocalTransaction
+      ((vertex, disposition), frozen, stats) <-
+        runTransaction
+          id
+          transactionShape
+          triangulation
+          1
+          (\mutable operation -> do
+             addCounter operation (Reported InputPoints) 1
+             inserted <-
+               case located of
+                 -- A frozen degenerate-line location points at an arbitrary visible
+                 -- segment, while the line extension interpreter requires a terminal
+                 -- edge. The frozen section carries no terminal witness, so retain
+                 -- the existing mutable line locator for this one non-lawful case.
+                 OutsideConvexHull (Just _)
+                   | numInnerFaces triangulation == 0 ->
+                       insertVertexAtPoint @'ProbeOff mutable operation Nothing (queryPointValue queryPoint) vertexData
+                 _ -> do
+                   capacityOutcome <- ensurePointCapacity mutable 1
+                   case capacityOutcome of
+                     Left failure -> pure (Left failure)
+                     Right () -> do
+                       vertex <- appendVertex mutable (queryPointValue queryPoint) vertexData
+                       let thawedLocation =
+                             case located of
+                               EmptyTriangulation -> MutableEmpty
+                               OnEdge edge -> MutableOnEdge (directedEdgeIdIndex edge)
+                               InFace face -> MutableInFace (faceIdIndex face)
+                               -- The frozen locator emits no edge only for a
+                               -- singleton mesh. Its mutable interpreter ignores
+                               -- this sentinel while constructing the second vertex.
+                               OutsideConvexHull Nothing -> MutableOutsideHull 0
+                               OutsideConvexHull (Just edge) -> MutableOutsideHull (directedEdgeIdIndex edge)
+                       ((vertex, Inserted) <$) <$> insertExistingVertexAtLocation @'ProbeOff mutable operation vertex thawedLocation
+             case inserted of
+               Left failure -> pure (Left failure)
+               Right (vertex, disposition) -> do
+                 case disposition of
+                   Inserted -> addCounter operation (Reported UniquePoints) 1
+                   AlreadyPresent -> do
+                     writeVertexData mutable vertex vertexData
+                     addCounter operation (Reported ExistingPoints) 1
+                     addCounter operation (Reported DuplicatePoints) 1
+                 pure (Right (vertex, disposition))
+          )
+      pure
+        InsertionResult
+          { insertionTriangulation = frozen
+          , insertionVertex = VertexId (fromIntegral vertex)
+          , insertionDisposition = disposition
+          , insertionStats = withFrozenLocationStats walked stats
+          }
+
+-- | Publish a payload replacement without opening a transaction.
+--
+-- A position already resident changes exactly one thing: the payload slot the
+-- vertex already occupies. No coordinate, no half-edge, no constraint flag and
+-- no face record differs, so the five unboxed planes are the ones the argument
+-- already holds rather than copies taken out of it — which is what a thaw costs
+-- and what this exists to refuse. They are immutable values; nothing reached
+-- from here is a mutable buffer, and 'boxedUpdate' materializes a fresh page
+-- for the one it rewrites, leaving the argument's own directory intact.
+--
+-- The location counters are the frozen walk's, not a thawed walk's. They
+-- describe the walk that actually ran.
+replaceResidentPayload
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> LocationStats
+  -> vertex
+  -> InsertionResult mode vertex directed undirected face
+replaceResidentPayload triangulation resident walked vertexData =
+  InsertionResult
+    { insertionTriangulation =
+        triangulation
+          { triVertexData =
+              boxedUpdate (vertexIdIndex resident) vertexData (triVertexData triangulation)
+          }
+    , insertionVertex = resident
+    , insertionDisposition = AlreadyPresent
+    , insertionStats =
+        withFrozenLocationStats
+          walked
+          (tabulateBuildStats (\metric -> case metric of
+            InputPoints -> 1
+            ExistingPoints -> 1
+            DuplicatePoints -> 1
+            _ -> 0))
+    }
+
+-- | Add the frozen locator's observation to the local topology interpreter's
+-- operation-owned counters. Direct frozen-site insertion contributes no mutable
+-- walk; the degenerate fallback contributes its real mutable walk rather than
+-- having it erased from the published result.
+withFrozenLocationStats :: LocationStats -> BuildStats -> BuildStats
+withFrozenLocationStats walked stats = updateBuildStats
+  [ (LocationWalkSteps, locationWalkSteps walked + buildStat LocationWalkSteps stats)
+  , (LocationMaxWalk, max (locationWalkSteps walked) (buildStat LocationMaxWalk stats))
+  , (LocationFallbacks, (if locationUsedFallback walked then 1 else 0) + buildStat LocationFallbacks stats)
+  ] stats
+
+-- | Apply a batch in one page transaction. Duplicate positions are processed
+-- in input order, so their last payload wins exactly as repeated 'insert'
+-- calls would, while topology is inserted only once per new position.
+insertMany
+  :: forall mode vertex directed undirected face
+   . HasPosition vertex
+  => Triangulation mode vertex directed undirected face
+  -> V.Vector vertex
+  -> Either BuildError (BuildResult mode vertex directed undirected face)
+insertMany triangulation input = do
+  validateVertices input
+  (mapped, frozen, stats) <-
+    runTransaction id DenseTransaction triangulation (V.length input) $ \mutable operation -> do
+      table <- newMutablePointIndex (pointCapacity mutable)
+      seeded <- seedPointTable mutable table
+      case seeded of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          mapping <- newPrimArray (V.length input)
+          freshBuffer <- MUV.new (V.length input)
+          filled <- fill mutable operation table mapping freshBuffer 0 0 0 0
+          case filled of
+            Left failure -> pure (Left failure)
+            Right (sumX, sumY, freshCount) -> do
+              let !existingCount = V.length input - freshCount
+              setCounter operation (Reported InputPoints) (V.length input)
+              setCounter operation (Reported UniquePoints) freshCount
+              setCounter operation (Reported ExistingPoints) existingCount
+              setCounter operation (Reported DuplicatePoints) existingCount
+              inserted <-
+                if freshCount == 0
+                  then pure (Right 0)
+                  else do
+                    let !scale = recip (fromIntegral freshCount)
+                    arena <-
+                      fillRadialArena
+                        mutable
+                        (sumX * scale)
+                        (sumY * scale)
+                        (MUV.unsafeRead freshBuffer)
+                        freshCount
+                    orderedArena <- radiallyOrderArena arena
+                    circleSweepInsert mutable operation orderedArena
+              case inserted of
+                Left failure -> pure (Left failure)
+                Right seedCount -> do
+                  setCounter operation (Reported SpatialSeedPoints) seedCount
+                  Right <$> unsafeFreezePrimArray mapping
+  pure
+    BuildResult
+      { buildTriangulation = frozen
+      , buildInputVertices = mapped
+      , buildStats = stats
+      }
+ where
+  fill
+    :: forall s
+     . MutableDcel s vertex directed undirected face
+    -> OperationState s
+    -> MutablePointIndex s
+    -> MutablePrimArray s Word32
+    -> MUV.MVector s Word32
+    -> Int
+    -> Double
+    -> Double
+    -> Int
+    -> ST s (Either BuildError (Double, Double, Int))
+  fill mutable operation table mapping freshBuffer !index !sumX !sumY !freshCount
+    | index >= V.length input = pure (Right (sumX, sumY, freshCount))
+    | otherwise = do
+        let !vertexData = input V.! index
+        claimed <- claimPosition mutable table (position vertexData) vertexData
+        case claimed of
+          Left failure -> pure (Left failure)
+          Right (FreshPosition vertex canonicalX canonicalY) -> do
+            writePrimArray mapping index (fromIntegral vertex)
+            MUV.unsafeWrite freshBuffer freshCount (fromIntegral vertex)
+            fill
+              mutable
+              operation
+              table
+              mapping
+              freshBuffer
+              (index + 1)
+              (sumX + canonicalX)
+              (sumY + canonicalY)
+              (freshCount + 1)
+          Right (ResidentPosition vertex) -> do
+            writePrimArray mapping index (fromIntegral vertex)
+            writeVertexData mutable vertex vertexData
+            fill mutable operation table mapping freshBuffer (index + 1) sumX sumY freshCount
+
+-- | One packed radial record per swept vertex — the derived sort fields and
+-- the vertex handle, nothing else — filled straight from the coordinate
+-- arenas and consumed in place by the sweep. The squared distance is stated
+-- against the ingress-accumulated centre, in the widened comparison format.
+fillRadialArena
+  :: MutableDcel s vertex directed undirected face
+  -> Double
+  -> Double
+  -> (Int -> ST s Word32)
+  -> Int
+  -> ST s (MUV.MVector s (Double, Double, Double, Word32))
+fillRadialArena mutable centerX centerY lookupId count = do
+  arena <- MUV.new count
+  forM_ [0 .. count - 1] $ \index -> do
+    raw <- lookupId index
+    x <- readPointX mutable (unpackIndex raw)
+    y <- readPointY mutable (unpackIndex raw)
+    let !wideX = x
+        !wideY = y
+        !deltaX = centerX - wideX
+        !deltaY = centerY - wideY
+    MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, wideX, wideY, raw)
+  pure arena
+
+-- | Claim a position for the vertex the arena would append next, or answer the
+-- vertex already holding it. The claim is written before the append, so the two
+-- must stay adjacent: nothing may consume a vertex slot in between.
+claimPosition
+  :: MutableDcel s vertex directed undirected face
+  -> MutablePointIndex s
+  -> Point
+  -> vertex
+  -> ST s (Either BuildError PositionClaim)
+claimPosition mutable table rawPoint vertexData =
+  case rawPoint of
+    Point x y -> do
+      let !canonicalX = canonicalScalarZero x
+          !canonicalY = canonicalScalarZero y
+      slot <- nextVertexSlot mutable
+      let !candidate = nextVertexSlotIndex slot
+      owner <-
+        resolveMutablePoint
+          table
+          (readPointX mutable)
+          (readPointY mutable)
+          canonicalX
+          canonicalY
+          candidate
+      case owner of
+        Left failure -> pure (Left failure)
+        Right (Just existing) -> pure (Right (ResidentPosition existing))
+        Right Nothing -> do
+          vertex <- appendVertexCoordinatesAtSlot mutable slot canonicalX canonicalY vertexData
+          pure (Right (FreshPosition vertex canonicalX canonicalY))
+-- | Enter the positions a batch inherits from the triangulation it extends, so
+-- that an input repeating one of them maps to the vertex already there.
+seedPointTable
+  :: MutableDcel s vertex directed undirected face
+  -> MutablePointIndex s
+  -> ST s (Either BuildError ())
+seedPointTable mutable table = do
+  existing <- pointCount mutable
+  seedMutablePointIndex
+    table
+    existing
+    (readPointX mutable)
+    (readPointY mutable)
+
+validateVertices :: HasPosition vertex => V.Vector vertex -> Either BuildError ()
+validateVertices vertices =
+  V.iforM_ vertices (\index vertexData -> validatePoint (Just index) (position vertexData))
diff --git a/src-build/Moonlight/Planar/Internal/Canonical.hs b/src-build/Moonlight/Planar/Internal/Canonical.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Canonical.hs
@@ -0,0 +1,333 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Canonical publication: the same triangulation, renumbered so that its
+-- representation is a function of its geometry alone.
+module Moonlight.Planar.Internal.Canonical
+  ( canonicalize
+  , canonicalizeKnownAscending
+  ) where
+
+import Control.Monad (when)
+import Control.Monad.ST (ST, runST)
+import qualified Data.Vector.Algorithms.Intro as Intro
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Dcel (numFaces, numUndirectedEdges, numVertices, vertexData)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Internal.Validation (coordinatesAscend)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.Paged (pagedUnsafeIndex)
+import Moonlight.Planar.Internal.Representation (CanonicalAdmission (..), Triangulation (..), authoringElementDefaults)
+import Moonlight.Planar.Internal.Types (BuildError)
+
+-- | Whether the caller has already established the authoritative vertex-order
+-- section. The canonicalizer still owns every edge, directed-edge, face,
+-- anchor, and constraint normalization; this witness affects only its vertex
+-- rank entrance.
+data VertexOrderAdmission
+  = VertexOrderUnknown
+  | VertexOrderKnownAscending
+
+-- | Renumber a triangulation into its canonical representation.
+--
+-- Two triangulations of the same sites are the same triangulation — Delaunay
+-- uniqueness says so, and the tie-break on an exactly cocircular quadrilateral
+-- is keyed on coordinates rather than on identifiers so that it stays true.
+-- What differs between two builds of one site set is only /numbering/: which
+-- vertex got index 0, which half-edge got the even slot, where a face's cycle
+-- was anchored. All of that records the schedule the value was constructed by,
+-- and none of it is geometry.
+--
+-- This is what removes it. Every identifier is assigned from the geometry:
+--
+--     * vertices in lexicographic coordinate rank;
+--     * undirected edges in lexicographic rank of their endpoint pair, each
+--       taken low first, so the pair is an unordered pair by construction;
+--     * of a pair's two half-edges, the even slot is the one leaving the
+--       lower-ranked endpoint, which keeps twinning an @xor@ with one;
+--     * inner faces in order of the least half-edge on their boundary, which
+--       is a single index rather than a vertex tuple and so needs no special
+--       case for a cycle that is not a triangle;
+--     * every anchor — each vertex's outgoing edge, each face's edge — set to
+--       the least admissible half-edge.
+--
+-- Construction therefore need not run in canonical order. Local insertion and
+-- seam fusion may preserve their cheaper schedule-specific numbering; callers
+-- invoke this operation only when they require the construction-independent
+-- physical representative used to observe the finite-set laws.
+--
+-- Element payloads are @()@ because a renumbering is a bijection and could
+-- carry them, but nothing that wants that exists; the writers a general form
+-- would need were retired when their last caller went. Vertex payloads travel
+-- with their vertices, and constraint flags with their edges, so a constrained
+-- triangulation canonicalizes as readily as an unconstrained one.
+canonicalize
+  :: Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+canonicalize = canonicalizeWithVertexOrder VertexOrderUnknown
+
+-- | Canonicalize a triangulation whose vertices were appended in strict
+-- lexicographic coordinate order by the admitted SiteSet bulk entrance.
+-- This is an execution witness, not a second numbering implementation.
+canonicalizeKnownAscending
+  :: Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+canonicalizeKnownAscending = canonicalizeWithVertexOrder VertexOrderKnownAscending
+
+-- A value already published canonically is its own canonical
+-- representative: every thaw forgets the witness, and nothing else
+-- renumbers, so honouring it is the identity and not a cache.
+canonicalizeWithVertexOrder
+  :: VertexOrderAdmission
+  -> Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+canonicalizeWithVertexOrder vertexOrderAdmission source =
+  case triCanonical source of
+    CanonicalKnown -> Right source
+    CanonicalUnknown ->
+      (\published -> published{triCanonical = CanonicalKnown})
+        <$> renumber vertexOrderAdmission source
+
+renumber
+  :: VertexOrderAdmission
+  -> Triangulation mode vertex () () ()
+  -> Either BuildError (Triangulation mode vertex () () ())
+renumber vertexOrderAdmission source = runST $ do
+  mutable <-
+    newMutableDcel
+      (authoringElementDefaults source)
+      (exactDcelCapacity vertexTotal directedTotal faceTotal)
+  forRange 0 vertexTotal $ \canonical -> do
+    let !old = vertexOrder `U.unsafeIndex` canonical
+    _ <-
+      appendVertexCoordinates
+        mutable
+        (coordinateX `pagedUnsafeIndex` old)
+        (coordinateY `pagedUnsafeIndex` old)
+        (vertexData source (VertexId (fromIntegral old)))
+    pure ()
+  _ <- addEdgeBlock mutable edgeTotal
+  _ <- addFaceBlock mutable (faceTotal - 1)
+  forRange 0 directedTotal $ \canonical -> do
+    let !old = directedFromCanonical `U.unsafeIndex` canonical
+    writeOrigin mutable canonical (vertexRank `U.unsafeIndex` originOf old)
+    writeNext mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 1))
+    writePrevious mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 2))
+    writeFace mutable canonical (faceRank `U.unsafeIndex` topology (4 * old + 3))
+  forRange 0 vertexTotal $ \canonical -> do
+    let !least = leastOutgoing `U.unsafeIndex` canonical
+    if least == absent
+      then writeVertexOut mutable canonical (-1)
+      else markConnected mutable canonical least
+  forRange 0 faceTotal $ \canonical -> do
+    let !least = leastOnFace `U.unsafeIndex` oldFaceOf canonical
+    writeFaceEdge mutable canonical (if least == absent then -1 else least)
+  forRange 0 edgeTotal $ \canonicalUndirected ->
+    when (constraintFlag `pagedUnsafeIndex` (edgeOrder `U.unsafeIndex` canonicalUndirected) /= 0) $
+      () <$ setConstraint mutable (2 * canonicalUndirected)
+  freezeTriangulation mutable
+ where
+  !vertexTotal = numVertices source
+  !edgeTotal = numUndirectedEdges source
+  !faceTotal = numFaces source
+  !directedTotal = 2 * edgeTotal
+
+  -- Read the arenas rather than the handle accessors. Every one of these is
+  -- indexed a few times per element, and the accessors would box a t'Point' or
+  -- an identifier newtype at each of them.
+  !coordinateX = triPointX source
+  !coordinateY = triPointY source
+  !topologyArena = triHalfTopology source
+  !constraintFlag = triConstraint source
+  topology slot = fromIntegral (topologyArena `pagedUnsafeIndex` slot) :: Int
+  originOf directed = topology (4 * directed)
+
+  -- Vertices, in lexicographic coordinate rank. Sorting a vector of keys with
+  -- the type's own ordering rather than an index vector under a closure: the
+  -- comparison then specializes instead of being an unknown call per step.
+  --
+  -- Sortedness is checked first, because a great many of the meshes handed to
+  -- this function already have it and the check costs one linear scan against a
+  -- sort's @n log n@. A seam merge is the reason: it copies two canonically
+  -- numbered operands into one arena, lower abscissa first, and the sites of
+  -- the result are then already in rank order by construction. Nothing about
+  -- the schedule is assumed here — the coordinates are simply read and
+  -- believed, so a mesh that arrives sorted for any other reason is served just
+  -- as well.
+  !alreadyRanked =
+    case vertexOrderAdmission of
+      VertexOrderUnknown -> coordinatesAscend vertexTotal coordinateX coordinateY
+      VertexOrderKnownAscending -> True
+  !vertexOrder
+    | alreadyRanked = U.enumFromN 0 vertexTotal
+    | otherwise =
+        thirdColumn $
+          sortedVector
+            ( U.generate
+                vertexTotal
+                ( \index ->
+                    ( coordinateX `pagedUnsafeIndex` index
+                    , coordinateY `pagedUnsafeIndex` index
+                    , index
+                    )
+                )
+            )
+  !vertexRank
+    | alreadyRanked = vertexOrder
+    | otherwise = invertPermutation vertexTotal vertexOrder
+
+  -- Undirected edges, in lexicographic rank of their endpoint pair.
+  --
+  -- Both components of the key are vertex ranks, so they are already dense
+  -- indices into a range this function knows: comparing them is a counting
+  -- sort's job, not a comparison sort's. Two stable passes — the high endpoint
+  -- first, then the low one — leave the pairs in lexicographic order, in time
+  -- linear in the edges and the vertices rather than @E log E@.
+  !edgeLow =
+    U.generate edgeTotal $ \index ->
+      min
+        (vertexRank `U.unsafeIndex` originOf (2 * index))
+        (vertexRank `U.unsafeIndex` originOf (2 * index + 1))
+  !edgeHigh =
+    U.generate edgeTotal $ \index ->
+      max
+        (vertexRank `U.unsafeIndex` originOf (2 * index))
+        (vertexRank `U.unsafeIndex` originOf (2 * index + 1))
+  !edgeOrder =
+    countingSortOn vertexTotal edgeLow $
+      countingSortOn vertexTotal edgeHigh (U.enumFromN 0 edgeTotal)
+
+  -- The even half of each canonical pair leaves the lower-ranked endpoint.
+  !directedFromCanonical =
+    U.generate directedTotal $ \canonical ->
+      let !oldEdge = edgeOrder `U.unsafeIndex` (canonical `quot` 2)
+          !evenHalf = 2 * oldEdge
+          !leavesLower =
+            vertexRank `U.unsafeIndex` originOf evenHalf
+              <= vertexRank `U.unsafeIndex` originOf (evenHalf + 1)
+       in if even canonical == leavesLower then evenHalf else evenHalf + 1
+  !directedToCanonical = invertPermutation directedTotal directedFromCanonical
+
+  -- The least canonical half-edge on each old face and leaving each canonical
+  -- vertex, in one pass. Anchors have to be a function of the geometry too, or
+  -- two builds of one site set would publish the same cycles anchored in
+  -- different places.
+  (!leastOnFace, !leastOutgoing) = runST $ do
+    faces <- MUV.replicate (max 1 faceTotal) absent
+    vertices <- MUV.replicate (max 1 vertexTotal) absent
+    forRange 0 directedTotal $ \canonical -> do
+      let !old = directedFromCanonical `U.unsafeIndex` canonical
+          !face = topology (4 * old + 3)
+          !rank = vertexRank `U.unsafeIndex` originOf old
+      onFace <- MUV.unsafeRead faces face
+      when (canonical < onFace) (MUV.unsafeWrite faces face canonical)
+      leaving <- MUV.unsafeRead vertices rank
+      when (canonical < leaving) (MUV.unsafeWrite vertices rank canonical)
+    (,) <$> U.unsafeFreeze faces <*> U.unsafeFreeze vertices
+
+  -- Inner faces, in order of the least canonical half-edge on their boundary.
+  -- The outer face keeps index zero, which the arena reserves for it anyway.
+  --
+  -- No two faces share a least half-edge, so this key is injective and the
+  -- ordering can be read off by inverting it: mark each face at its own least
+  -- half-edge, then scan the half-edges in order. That is one linear pass and
+  -- no comparisons at all. A face the scan never reaches has no boundary — only
+  -- reachable in a mesh with no edges — and follows in old index order so that
+  -- the result stays a permutation whatever it is handed.
+  !innerFaceOrder = U.create $ do
+    (owner :: MUV.MVector state Int) <- MUV.replicate (max 1 directedTotal) absent
+    forRange 1 faceTotal $ \face -> do
+      let !least = leastOnFace `U.unsafeIndex` face
+      when (least /= absent) (MUV.unsafeWrite owner least face)
+    emitted <- MUV.replicate (max 1 faceTotal) False
+    out <- MUV.new (max 0 (faceTotal - 1))
+    let scan :: Int -> Int -> ST state Int
+        scan !slot !filled
+          | slot >= directedTotal = pure filled
+          | otherwise = do
+              !face <- MUV.unsafeRead owner slot
+              if face == absent
+                then scan (slot + 1) filled
+                else do
+                  MUV.unsafeWrite out filled face
+                  MUV.unsafeWrite emitted face True
+                  scan (slot + 1) (filled + 1)
+        sweep :: Int -> Int -> ST state ()
+        sweep !face !filled
+          | face >= faceTotal = pure ()
+          | otherwise = do
+              !done <- MUV.unsafeRead emitted face
+              if done
+                then sweep (face + 1) filled
+                else do
+                  MUV.unsafeWrite out filled face
+                  sweep (face + 1) (filled + 1)
+    scan 0 0 >>= sweep 1
+    pure out
+  !faceRank = U.create $ do
+    ranks <- MUV.replicate (max 1 faceTotal) 0
+    U.iforM_ innerFaceOrder $ \rank old -> MUV.unsafeWrite ranks old (rank + 1)
+    pure ranks
+  oldFaceOf canonical
+    | canonical == 0 = 0
+    | otherwise = innerFaceOrder `U.unsafeIndex` (canonical - 1)
+
+
+-- | No half-edge reaches this face or vertex. A one-site mesh has such a
+-- vertex and an edgeless outer face; the arena spells the same absence as a
+-- packed sentinel, which is not a value an index may take.
+absent :: Int
+absent = maxBound
+
+-- | @[from, to)@, without materializing the range as a list.
+forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
+forRange from to action = go from
+ where
+  go !index
+    | index >= to = pure ()
+    | otherwise = action index >> go (index + 1)
+{-# INLINE forRange #-}
+
+sortedVector :: (U.Unbox key, Ord key) => U.Vector key -> U.Vector key
+sortedVector = U.modify Intro.sort
+{-# INLINE sortedVector #-}
+
+-- | Stably reorder @items@ by a key that is already a dense index below
+-- @range@, in time linear in both.
+--
+-- Applied least-significant key first, repeated application leaves the items in
+-- lexicographic order of the whole key — which is what makes a two-component
+-- ordering over vertex ranks cost @O(V + E)@ instead of @O(E log E)@.
+countingSortOn :: Int -> U.Vector Int -> U.Vector Int -> U.Vector Int
+countingSortOn range keys items = U.create $ do
+  (counts :: MUV.MVector state Int) <- MUV.replicate (range + 1) 0
+  U.forM_ items $ \item ->
+    MUV.unsafeModify counts (+ 1) (keys `U.unsafeIndex` item)
+  let prefix :: Int -> Int -> ST state ()
+      prefix !key !running
+        | key > range = pure ()
+        | otherwise = do
+            !count <- MUV.unsafeRead counts key
+            MUV.unsafeWrite counts key running
+            prefix (key + 1) (running + count)
+  prefix 0 0
+  out <- MUV.new (max 1 (U.length items))
+  U.forM_ items $ \item -> do
+    let !key = keys `U.unsafeIndex` item
+    !slot <- MUV.unsafeRead counts key
+    MUV.unsafeWrite counts key (slot + 1)
+    MUV.unsafeWrite out slot item
+  pure (MUV.slice 0 (U.length items) out)
+
+thirdColumn :: (U.Unbox a, U.Unbox b) => U.Vector (a, b, Int) -> U.Vector Int
+thirdColumn = U.map (\(_, _, index) -> index)
+{-# INLINE thirdColumn #-}
+
+-- | @inverse ! (order ! i) == i@: the rank each element was given.
+invertPermutation :: Int -> U.Vector Int -> U.Vector Int
+invertPermutation count order = U.create $ do
+  inverse <- MUV.replicate (max 1 count) 0
+  U.iforM_ order $ \rank element -> MUV.unsafeWrite inverse element rank
+  pure inverse
diff --git a/src-build/Moonlight/Planar/Internal/Capacity.hs b/src-build/Moonlight/Planar/Internal/Capacity.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Capacity.hs
@@ -0,0 +1,18 @@
+-- | Admissibility of a requested vertex count against the packed index limit.
+module Moonlight.Planar.Internal.Capacity
+  ( ensureCapacity
+  ) where
+
+import Moonlight.Planar.Internal.PackedIndex (indexLimit)
+import Moonlight.Planar.Internal.Types (BuildError (..))
+
+ensureCapacity :: Int -> Either BuildError ()
+ensureCapacity count
+  | count < 0 || count > maximumVertexCapacity = Left (CapacityExceeded count)
+  | otherwise = Right ()
+
+-- Mutable allocation reserves @8n + 16@ directed-edge slots. Every topology
+-- handle is packed below 'indexLimit', so admissibility is stated against that
+-- actual representation bound rather than the machine 'Int' bound.
+maximumVertexCapacity :: Int
+maximumVertexCapacity = (indexLimit - 16) `quot` 8
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Admission.hs b/src-build/Moonlight/Planar/Internal/Cdt/Admission.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Admission.hs
@@ -0,0 +1,77 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Immutable admission of one requested segment against the sparse
+-- constrained-edge section, decided without thawing topology.
+module Moonlight.Planar.Internal.Cdt.Admission
+  ( ConstraintAdmission (..)
+  , constraintAdmission
+  , segmentBoxesAreDisjoint
+  ) where
+
+import qualified Data.IntSet as IntSet
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Predicates (segmentsProperlyCross)
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (Point (..))
+
+-- | Immutable admission descends through the sparse constrained-edge section,
+-- not through every ordinary edge in the requested corridor. Constraint
+-- cardinality is the lawful index here: rejection depends only on a proper
+-- crossing with an existing protected segment, while shared endpoints,
+-- duplicate segments, and collinear overlap remain recoverable by the mutable
+-- corridor algebra. This is the read-only answer behind 'canAddConstraint';
+-- the singleton verbs walk the corridor directly and refuse on the first
+-- resident constraint it meets, which is the witness they publish.
+data ConstraintAdmission
+  = ConstraintBlocked !UndirectedEdgeId
+  | ConstraintAdmitted
+
+constraintAdmission
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> ConstraintAdmission
+constraintAdmission triangulation from to =
+  IntSet.foldr firstBlocking ConstraintAdmitted (triConstraintEdges triangulation)
+ where
+  !requestFrom = Dcel.vertexPoint triangulation from
+  !requestTo = Dcel.vertexPoint triangulation to
+
+  firstBlocking
+    :: Int
+    -> ConstraintAdmission
+    -> ConstraintAdmission
+  firstBlocking raw later =
+    let !edge = UndirectedEdgeId (fromIntegral raw)
+        (!edgeFromId, !edgeToId) = Dcel.undirectedEndpoints triangulation edge
+     in if
+          from == edgeFromId
+            || from == edgeToId
+            || to == edgeFromId
+            || to == edgeToId
+          then later
+          else
+            let !edgeFrom = Dcel.vertexPoint triangulation edgeFromId
+                !edgeTo = Dcel.vertexPoint triangulation edgeToId
+             in if segmentBoxesAreDisjoint requestFrom requestTo edgeFrom edgeTo
+                  then later
+                  else
+                    if segmentsProperlyCross requestFrom requestTo edgeFrom edgeTo
+                      then ConstraintBlocked edge
+                      else later
+
+segmentBoxesAreDisjoint
+  :: Point
+  -> Point
+  -> Point
+  -> Point
+  -> Bool
+segmentBoxesAreDisjoint (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
+  max ax bx < min cx dx
+    || max cx dx < min ax bx
+    || max ay by < min cy dy
+    || max cy dy < min ay by
+{-# INLINE segmentBoxesAreDisjoint #-}
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Batch.hs b/src-build/Moonlight/Planar/Internal/Cdt/Batch.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Batch.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | The batch interpreter: many constraint requests in order inside one sealed
+-- dense transaction, with rejection carried as a value.
+module Moonlight.Planar.Internal.Cdt.Batch
+  ( recoverConstraints
+  , recoverConstraintBatch
+  , finalizeConstraintBatch
+  , initialConstraintBatchStats
+  , interpretConstraintRequest
+  , interpretConstraintRequests
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Either (isRight)
+import qualified Data.Vector as V
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Cdt.Combinators (foldWhileM)
+import Moonlight.Planar.Internal.Cdt.Query (validateEndpoints)
+import Moonlight.Planar.Internal.Cdt.Recovery (applyMutableConstraint)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  )
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( OperationState )
+import Moonlight.Planar.Internal.Paged (TransactionShape (DenseTransaction))
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Transaction (runTransaction)
+import Moonlight.Planar.Internal.Types (ConstraintMode (..))
+
+-- | Interpret constraint requests in order inside one sealed mutable DCEL
+-- transaction. Rejections are values because an earlier accepted request can
+-- lawfully obstruct a later request; structural recovery failures remain typed
+-- errors and prevent a partially rewritten mesh from escaping. The public batch
+-- interpreter amortizes dense materialization; known singleton repair sites
+-- reuse this algebra under the sparse persistent-page interpretation.
+recoverConstraints
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector (VertexId, VertexId)
+  -> Either (CdtError) (ConstraintBatchResult vertex directed undirected face)
+recoverConstraints triangulation requests
+  | V.null requests =
+      Right
+        ConstraintBatchResult
+          { constraintBatchTriangulation = triangulation
+          , constraintBatchOutcomes = V.empty
+          , constraintBatchStats = initialConstraintBatchStats 0
+          }
+  | otherwise =
+      recoverConstraintBatch
+        triangulation
+        requests
+
+recoverConstraintBatch
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector (VertexId, VertexId)
+  -> Either (CdtError) (ConstraintBatchResult vertex directed undirected face)
+recoverConstraintBatch triangulation requests = do
+  V.mapM_ (uncurry (validateEndpoints triangulation)) requests
+  (completed, frozen, _) <-
+    runTransaction
+      CdtBuildError
+      DenseTransaction
+      triangulation
+      0
+      (interpretConstraintRequests requests)
+  pure (finalizeConstraintBatch frozen completed)
+{-# INLINE recoverConstraintBatch #-}
+
+-- | Interpret a complete constraint request section against an already-open
+-- transaction. Both ordinary recovery and asymmetric constrained extension
+-- use this one interpreter; callers decide only which request section is
+-- resident before it begins.
+interpretConstraintRequests
+  :: V.Vector (VertexId, VertexId)
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> ST s (Either (CdtError) ConstraintBatchAccumulator)
+interpretConstraintRequests requests mutable operation = do
+  programWords <- newGrowableWord32 256
+  let initialAccumulator =
+        ConstraintBatchAccumulator
+          { accumulatedConstraintOutcomes = []
+          , accumulatedConstraintStats =
+              initialConstraintBatchStats (V.length requests)
+          }
+  foldWhileM
+    isRight
+    (interpretConstraintRequest programWords mutable operation)
+    (Right initialAccumulator)
+    requests
+{-# INLINE interpretConstraintRequests #-}
+
+-- | Materialize the ordinary recovery receipt only after the enclosing
+-- transaction has frozen. The mutable accumulator cannot escape as a partial
+-- constrained mesh.
+finalizeConstraintBatch
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> ConstraintBatchAccumulator
+  -> ConstraintBatchResult vertex directed undirected face
+finalizeConstraintBatch frozen completed =
+  ConstraintBatchResult
+    { constraintBatchTriangulation = frozen
+    , constraintBatchOutcomes = V.fromList (reverse (accumulatedConstraintOutcomes completed))
+    , constraintBatchStats = accumulatedConstraintStats completed
+    }
+{-# INLINE finalizeConstraintBatch #-}
+
+initialConstraintBatchStats :: Int -> ConstraintBatchStats
+initialConstraintBatchStats requestCount =
+  ConstraintBatchStats
+    { constraintBatchRequests = requestCount
+    , constraintBatchAccepted = 0
+    , constraintBatchRejected = 0
+    , constraintBatchCorridors = 0
+    , constraintBatchReusedFaces = 0
+    , constraintBatchCrossedEdges = 0
+    }
+
+interpretConstraintRequest
+  :: GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Either (CdtError) ConstraintBatchAccumulator
+  -> (VertexId, VertexId)
+  -> ST s (Either (CdtError) ConstraintBatchAccumulator)
+interpretConstraintRequest _ _ _ rejected@(Left _) _ = pure rejected
+interpretConstraintRequest programWords mutable operation (Right accumulator) (from, to) = do
+  applied <- applyMutableConstraint programWords mutable operation from to
+  pure $ case applied of
+    Left obstruction -> Left obstruction
+    Right (MutableConstraintRejected blocking) ->
+      let previousStats = accumulatedConstraintStats accumulator
+       in Right
+            accumulator
+              { accumulatedConstraintOutcomes =
+                  ConstraintRejected blocking : accumulatedConstraintOutcomes accumulator
+              , accumulatedConstraintStats =
+                  previousStats
+                    { constraintBatchRejected = constraintBatchRejected previousStats + 1
+                    }
+              }
+    Right (MutableConstraintAccepted request) ->
+      let previousStats = accumulatedConstraintStats accumulator
+       in Right
+            accumulator
+              { accumulatedConstraintOutcomes =
+                  ConstraintAccepted
+                    (V.fromList (reverse (accumulatedRequestPath request)))
+                    (accumulatedRequestAddedEdges request)
+                    : accumulatedConstraintOutcomes accumulator
+              , accumulatedConstraintStats =
+                  previousStats
+                    { constraintBatchAccepted = constraintBatchAccepted previousStats + 1
+                    , constraintBatchCorridors =
+                        constraintBatchCorridors previousStats + accumulatedRequestCorridors request
+                    , constraintBatchReusedFaces =
+                        constraintBatchReusedFaces previousStats + accumulatedRequestReusedFaces request
+                    , constraintBatchCrossedEdges =
+                        constraintBatchCrossedEdges previousStats + accumulatedRequestCrossedEdges request
+                    }
+              }
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Build.hs b/src-build/Moonlight/Planar/Internal/Cdt/Build.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Build.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Constrained bulk loading: an unconstrained load promoted to the
+-- constrained layer, then driven through the batch corridor interpreter.
+module Moonlight.Planar.Internal.Cdt.Build
+  ( fromDelaunay
+  , constrainedDelaunay
+  , constrainedDelaunayMaximal
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.Vector as V
+import Data.Primitive.PrimArray (PrimArray, indexPrimArray, sizeofPrimArray)
+import Data.Word (Word32)
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Cdt.Batch (recoverConstraints)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (HasPosition (..))
+
+-- | Promote an unconstrained mesh into the constrained layer with no marked edges.
+fromDelaunay
+  :: Triangulation 'Unconstrained vertex directed undirected face
+  -> Triangulation 'Constrained vertex directed undirected face
+fromDelaunay = promoteConstrained
+
+-- | Build a constrained triangulation, refusing the complete request when any
+-- input constraint cannot be admitted.
+constrainedDelaunay
+  :: HasPosition vertex
+  => ElementDefaults directed undirected face
+  -> V.Vector vertex
+  -> V.Vector (Int, Int)
+  -> Either (CdtError) (BuildResult 'Constrained vertex directed undirected face)
+constrainedDelaunay defaults inputVertices constraints = do
+  result <- constrainedDelaunayMaximal defaults inputVertices constraints
+  if V.null (cdtRejectedConstraints result)
+    then Right (cdtAcceptedBuild result)
+    else Left (ConstraintInputConflicts (cdtRejectedConstraints result))
+
+-- | Stable constrained bulk loading. Duplicate input vertices are rerouted to
+-- the first surviving handle. Every proper constraint conflict is returned in
+-- input order; accepted constraints remain in the result.
+constrainedDelaunayMaximal
+  :: HasPosition vertex
+  => ElementDefaults directed undirected face
+  -> V.Vector vertex
+  -> V.Vector (Int, Int)
+  -> Either (CdtError) (CdtBuildResult vertex directed undirected face)
+constrainedDelaunayMaximal defaults inputVertices constraints = do
+  built <- first CdtBuildError (delaunay defaults inputVertices)
+  let !mapping = buildInputVertices built
+      !initial = fromDelaunay (buildTriangulation built)
+  requests <- V.mapM (mapConstraintRequest mapping) constraints
+  batch <- recoverConstraints initial requests
+  let rejected =
+        V.mapMaybe
+          (\(request, outcome) ->
+            case outcome of
+              ConstraintAccepted _ _ -> Nothing
+              ConstraintRejected _ -> Just request
+          )
+          (V.zip constraints (constraintBatchOutcomes batch))
+  pure
+    CdtBuildResult
+      { cdtAcceptedBuild =
+          BuildResult
+            { buildTriangulation = constraintBatchTriangulation batch
+            , buildInputVertices = mapping
+            , buildStats = buildStats built
+            }
+      , cdtRejectedConstraints = rejected
+      }
+ where
+  mapConstraintRequest
+    :: PrimArray Word32
+    -> (Int, Int)
+    -> Either (CdtError) (VertexId, VertexId)
+  mapConstraintRequest mapping (fromIndex, toIndex) =
+    (,)
+      <$> mapConstraintEndpoint mapping fromIndex
+      <*> mapConstraintEndpoint mapping toIndex
+
+  mapConstraintEndpoint
+    :: PrimArray Word32
+    -> Int
+    -> Either (CdtError) VertexId
+  mapConstraintEndpoint mapping endpointIndex
+    | endpointIndex >= 0 && endpointIndex < sizeofPrimArray mapping =
+        Right (VertexId (indexPrimArray mapping endpointIndex))
+    | otherwise =
+        Left (ConstraintEndpointIndexOutOfRange endpointIndex (sizeofPrimArray mapping))
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Combinators.hs b/src-build/Moonlight/Planar/Internal/Cdt/Combinators.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Combinators.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Bounded monadic folding, refusable transaction sequencing, and the handle
+-- and error coercions the constrained layer shares.
+module Moonlight.Planar.Internal.Cdt.Combinators
+  ( foldWhileM
+  , bindMutable
+  , asConstraintStep
+  ) where
+
+import Moonlight.Planar.Internal.Cdt.Types (CdtError (..))
+import Moonlight.Planar.Internal.Types (BuildError)
+
+-- | Monadic left fold whose continuation is supplied by a lazy right fold.
+-- The state predicate decides descent before the next effect is constructed,
+-- so graph walks stop at their authoritative local answer without mutable loop
+-- control or traversing the unused safety suffix.
+foldWhileM
+  :: (Foldable container, Monad monad)
+  => (state -> Bool)
+  -> (state -> item -> monad state)
+  -> state
+  -> container item
+  -> monad state
+foldWhileM shouldContinue step initial items =
+  foldr
+    (\item continuation state ->
+      if shouldContinue state
+        then step state item >>= continuation
+        else pure state
+    )
+    pure
+    items
+    initial
+{-# INLINE foldWhileM #-}
+
+-- | Sequence two refusable transaction steps. Refusal short-circuits, so a
+-- transaction that abandons never reaches its publication; writing the bind
+-- once is what keeps the constraint verbs from nesting their case analysis
+-- five deep.
+bindMutable :: Monad monad => monad (Either failure a) -> (a -> monad (Either failure b)) -> monad (Either failure b)
+bindMutable step continue = do
+  outcome <- step
+  case outcome of
+    Left failure -> pure (Left failure)
+    Right value -> continue value
+{-# INLINE bindMutable #-}
+
+-- | Relabel a step whose refusal is a build failure, so it composes with the
+-- constraint layer's own.
+asConstraintStep :: Functor f => f (Either BuildError a) -> f (Either (CdtError) a)
+asConstraintStep = fmap (either (Left . CdtBuildError) Right)
+{-# INLINE asConstraintStep #-}
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Corridor.hs b/src-build/Moonlight/Planar/Internal/Cdt/Corridor.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Corridor.hs
@@ -0,0 +1,281 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | The corridor walk: one traversal of the requested segment that yields
+-- either the first oriented blocking witness or a complete recovery program.
+module Moonlight.Planar.Internal.Cdt.Corridor
+  ( constraintWorkspaceFor
+  , scanMutableConstraint
+  , advanceMutablePlan
+  , continueMutablePlan
+  , beginMutableRecover
+  , finishMutableRecover
+  , settleMutableProgram
+  , mutablePlanIsActive
+  , writeConstraintProgram
+  , readConstraintProgram
+  , existingProgramTag
+  , recoverProgramTag
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.IntersectionIterator (Intersection (..))
+import Moonlight.Planar.Internal.Cdt.Combinators (foldWhileM)
+import Moonlight.Planar.Internal.Cdt.Corridor.Trace (nextMutableIntersection)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , clearGrowable
+  , readGrowable
+  , writeGrowable
+  )
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Point (Point)
+
+-- | The walk budget bounds a corridor through the mesh as it stands, so it is
+-- taken per request rather than per transaction: a verb that inserts before it
+-- recovers has changed the mesh the budget describes. Recovery itself adds
+-- neither vertex nor edge, so a batch reads the same number every time.
+constraintWorkspaceFor
+  :: GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> ST s (ConstraintWorkspace s)
+constraintWorkspaceFor constraintProgramWords mutable = do
+  halfEdges <- directedEdgeCount mutable
+  vertices <- pointCount mutable
+  pure
+    ConstraintWorkspace
+      { constraintProgramWords
+      , constraintWalkBudget = 2 * halfEdges + vertices + 8
+      }
+
+writeConstraintProgram :: ConstraintWorkspace s -> Int -> Int -> ST s ()
+writeConstraintProgram ConstraintWorkspace{constraintProgramWords} index value =
+  writeGrowable constraintProgramWords index (fromIntegral value)
+{-# INLINE writeConstraintProgram #-}
+
+readConstraintProgram :: ConstraintWorkspace s -> Int -> ST s Int
+readConstraintProgram ConstraintWorkspace{constraintProgramWords} index =
+  fromIntegral <$> readGrowable constraintProgramWords index
+{-# INLINE readConstraintProgram #-}
+
+-- | Walk one corridor once, gluing the local observations into either the
+-- first oriented blocking witness or a complete recovery program. The former
+-- admission/planning pair traversed every admitted corridor twice and could
+-- disagree if either walk evolved independently; this scanner is their single
+-- semantic owner.
+scanMutableConstraint
+  :: ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> ST s (Either (CdtError) MutableConstraintScan)
+scanMutableConstraint workspace@ConstraintWorkspace{constraintProgramWords, constraintWalkBudget} mutable from to = do
+  clearGrowable constraintProgramWords
+  lineFrom <- pointAt mutable (vertexIdIndex from)
+  lineTo <- pointAt mutable (vertexIdIndex to)
+  initialCursor <- beginMutableRecover workspace 0 from 0 False
+  walked <-
+    foldWhileM
+      mutablePlanIsActive
+      (advanceMutablePlan workspace mutable lineFrom lineTo to)
+      (MutablePlanActive (VertexIntersection from) initialCursor)
+      [1 .. constraintWalkBudget]
+  pure $ case walked of
+    MutablePlanComplete plan -> Right (MutableConstraintScanAdmitted plan)
+    MutablePlanBlocked blocking -> Right (MutableConstraintScanBlocked blocking)
+    MutablePlanFailed obstruction -> Left (ConstraintCorridorObstructed obstruction)
+    MutablePlanActive _ _ ->
+      Left
+        ( ConstraintCorridorObstructed
+            (CorridorWalkDidNotTerminate constraintWalkBudget)
+        )
+
+advanceMutablePlan
+  :: ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> VertexId
+  -> MutablePlanWalk
+  -> Int
+  -> ST s MutablePlanWalk
+advanceMutablePlan _ _ _ _ _ complete@(MutablePlanComplete _) _ = pure complete
+advanceMutablePlan _ _ _ _ _ blocked@(MutablePlanBlocked _) _ = pure blocked
+advanceMutablePlan _ _ _ _ _ failed@(MutablePlanFailed _) _ = pure failed
+advanceMutablePlan workspace mutable lineFrom lineTo target (MutablePlanActive event cursor) _ =
+  case event of
+    EdgeIntersection directed -> do
+      constrained <- readConstraint mutable (directedEdgeIdIndex directed)
+      if constrained
+        then pure (MutablePlanBlocked directed)
+        else do
+          writeConstraintProgram workspace (mutableCursorWriteAt cursor) (directedEdgeIdIndex directed)
+          continueMutablePlan
+            workspace
+            mutable
+            lineFrom
+            lineTo
+            target
+            event
+            cursor
+              { mutableCursorWriteAt = mutableCursorWriteAt cursor + 1
+              , mutableCursorConflictCount = mutableCursorConflictCount cursor + 1
+              , mutableCursorAfterOverlap = False
+              }
+    VertexIntersection vertex
+      | mutableCursorAfterOverlap cursor ->
+          continueMutablePlan
+            workspace
+            mutable
+            lineFrom
+            lineTo
+            target
+            event
+            cursor{mutableCursorAfterOverlap = False}
+      | vertex == mutableCursorAt cursor ->
+          continueMutablePlan
+            workspace
+            mutable
+            lineFrom
+            lineTo
+            target
+            event
+            cursor{mutableCursorAfterOverlap = False}
+      | otherwise -> do
+          finished <- finishMutableRecover workspace vertex cursor
+          nextCursor <-
+            beginMutableRecover
+              workspace
+              (mutableCursorWriteAt finished)
+              vertex
+              (mutableCursorPieceCount finished)
+              False
+          continueMutablePlan workspace mutable lineFrom lineTo target event nextCursor
+    EdgeOverlap rawDirected -> do
+      let rawEdge = directedEdgeIdIndex rawDirected
+      rawOrigin <- readOrigin mutable rawEdge
+      rawDestination <- readOrigin mutable (rawEdge `xor` 1)
+      let current = vertexIdIndex (mutableCursorAt cursor)
+          oriented
+            | rawOrigin == current = Just rawEdge
+            | rawDestination == current = Just (rawEdge `xor` 1)
+            | otherwise = Nothing
+      case oriented of
+        Nothing ->
+          pure
+            ( MutablePlanFailed
+                ( CorridorBoundaryMissing
+                    (mutableCursorAt cursor)
+                    (VertexId (fromIntegral rawOrigin))
+                )
+            )
+        Just edge -> do
+          prefix <-
+            if mutableCursorConflictCount cursor == 0
+              then pure cursor
+              else finishMutableRecover workspace (mutableCursorAt cursor) cursor
+          let !existingAt =
+                if mutableCursorConflictCount cursor == 0
+                  then mutableCursorHeader cursor
+                  else mutableCursorWriteAt prefix
+              !edgeDestination =
+                VertexId
+                  ( fromIntegral
+                      (if edge == rawEdge then rawDestination else rawOrigin)
+                  )
+              !pieceCount = mutableCursorPieceCount prefix + 1
+              !nextHeader = existingAt + 2
+          writeConstraintProgram workspace existingAt existingProgramTag
+          writeConstraintProgram workspace (existingAt + 1) edge
+          nextCursor <-
+            beginMutableRecover
+              workspace
+              nextHeader
+              edgeDestination
+              pieceCount
+              True
+          continueMutablePlan workspace mutable lineFrom lineTo target event nextCursor
+
+continueMutablePlan
+  :: ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> VertexId
+  -> Intersection
+  -> MutableProgramCursor
+  -> ST s MutablePlanWalk
+continueMutablePlan workspace mutable lineFrom lineTo target event cursor = do
+  following <- nextMutableIntersection mutable lineFrom lineTo event
+  case following of
+    Just nextEvent -> pure (MutablePlanActive nextEvent cursor)
+    Nothing -> MutablePlanComplete <$> settleMutableProgram workspace target cursor
+
+beginMutableRecover
+  :: ConstraintWorkspace s
+  -> Int
+  -> VertexId
+  -> Int
+  -> Bool
+  -> ST s MutableProgramCursor
+beginMutableRecover workspace header from pieceCount afterOverlap = do
+  writeConstraintProgram workspace header recoverProgramTag
+  writeConstraintProgram workspace (header + 1) (vertexIdIndex from)
+  writeConstraintProgram workspace (header + 2) 0
+  writeConstraintProgram workspace (header + 3) 0
+  pure
+    MutableProgramCursor
+      { mutableCursorAt = from
+      , mutableCursorHeader = header
+      , mutableCursorWriteAt = header + 4
+      , mutableCursorConflictCount = 0
+      , mutableCursorPieceCount = pieceCount
+      , mutableCursorAfterOverlap = afterOverlap
+      }
+
+finishMutableRecover
+  :: ConstraintWorkspace s
+  -> VertexId
+  -> MutableProgramCursor
+  -> ST s MutableProgramCursor
+finishMutableRecover workspace to cursor = do
+  writeConstraintProgram workspace (mutableCursorHeader cursor + 2) (vertexIdIndex to)
+  writeConstraintProgram workspace (mutableCursorHeader cursor + 3) (mutableCursorConflictCount cursor)
+  pure
+    cursor
+      { mutableCursorAt = to
+      , mutableCursorPieceCount = mutableCursorPieceCount cursor + 1
+      }
+
+settleMutableProgram
+  :: ConstraintWorkspace s
+  -> VertexId
+  -> MutableProgramCursor
+  -> ST s MutableConstraintProgram
+settleMutableProgram workspace target cursor
+  | mutableCursorAt cursor == target =
+      pure
+        MutableConstraintProgram
+          { mutableProgramWordCount = mutableCursorHeader cursor
+          , mutableProgramPieceCount = mutableCursorPieceCount cursor
+          }
+  | otherwise = do
+      finished <- finishMutableRecover workspace target cursor
+      pure
+        MutableConstraintProgram
+          { mutableProgramWordCount = mutableCursorWriteAt finished
+          , mutableProgramPieceCount = mutableCursorPieceCount finished
+          }
+
+existingProgramTag :: Int
+existingProgramTag = 0
+
+recoverProgramTag :: Int
+recoverProgramTag = 1
+
+mutablePlanIsActive :: MutablePlanWalk -> Bool
+mutablePlanIsActive (MutablePlanActive _ _) = True
+mutablePlanIsActive _ = False
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Corridor/Trace.hs b/src-build/Moonlight/Planar/Internal/Cdt/Corridor/Trace.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Corridor/Trace.hs
@@ -0,0 +1,240 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Local direction tracing inside the thawed mesh: where a directed line
+-- leaves the vertex or edge it currently stands on.
+module Moonlight.Planar.Internal.Cdt.Corridor.Trace
+  ( MutableVertexOut (..)
+  , MutableVertexTrace (..)
+  , MutableEdgeOut (..)
+  , nextMutableIntersection
+  , traceMutableDirectionOutOfVertex
+  , advanceMutableVertexTrace
+  , traceMutableDirectionOutOfEdge
+  , mutableEdgeIntersectsNonCollinear
+  , mutableVertexTraceIsSearching
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.IntersectionIterator
+import Moonlight.Planar.Internal.Cdt.Combinators (foldWhileM)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.Predicates
+  ( onClosedSegment
+  , orient2d
+  , orient2dCoordinates
+  )
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Math (projectionFactor)
+
+data MutableVertexOut
+  = MutableVertexOutHull
+  | MutableVertexOutOverlap {-# UNPACK #-} !Int
+  | MutableVertexOutEdge {-# UNPACK #-} !Int
+
+data MutableVertexTrace
+  = MutableVertexTraceSearching {-# UNPACK #-} !Int !Ordering
+  | MutableVertexTraceComplete !MutableVertexOut
+
+data MutableEdgeOut
+  = MutableEdgeOutHull
+  | MutableEdgeOutVertex {-# UNPACK #-} !Int
+  | MutableEdgeOutEdge {-# UNPACK #-} !Int
+  | MutableEdgeOutNone
+
+nextMutableIntersection
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> Intersection
+  -> ST s (Maybe Intersection)
+nextMutableIntersection mutable lineFrom lineTo current =
+  case current of
+    EdgeIntersection directed -> do
+      edgeOut <- traceMutableDirectionOutOfEdge mutable (directedEdgeIdIndex directed) lineFrom lineTo
+      pure $ case edgeOut of
+        MutableEdgeOutHull -> Nothing
+        MutableEdgeOutVertex vertex ->
+          Just (VertexIntersection (VertexId (fromIntegral vertex)))
+        MutableEdgeOutEdge edge ->
+          Just (EdgeIntersection (DirectedEdgeId (fromIntegral edge)))
+        MutableEdgeOutNone -> Nothing
+    VertexIntersection vertex -> do
+      currentPoint <- pointAt mutable (vertexIdIndex vertex)
+      if currentPoint == lineTo
+        then pure Nothing
+        else do
+          vertexOut <- traceMutableDirectionOutOfVertex mutable (vertexIdIndex vertex) lineTo
+          case vertexOut of
+            MutableVertexOutHull -> pure Nothing
+            MutableVertexOutOverlap edge ->
+              pure (Just (EdgeOverlap (DirectedEdgeId (fromIntegral edge))))
+            MutableVertexOutEdge edge -> do
+              edgeFrom <- edgeOriginPoint mutable edge
+              edgeTo <- edgeOriginPoint mutable (edge `xor` 1)
+              pure
+                ( if orient2d edgeFrom edgeTo lineTo == LT
+                    then Nothing
+                    else Just (EdgeIntersection (DirectedEdgeId (fromIntegral edge)))
+                )
+    EdgeOverlap directed
+      | lineFrom == lineTo -> pure Nothing
+      | otherwise -> do
+          destination <- readOrigin mutable (directedEdgeIdIndex directed `xor` 1)
+          destinationPoint <- pointAt mutable destination
+          pure
+            ( if onClosedSegment lineFrom lineTo destinationPoint
+                then Just (VertexIntersection (VertexId (fromIntegral destination)))
+                else Nothing
+            )
+
+traceMutableDirectionOutOfVertex
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Point
+  -> ST s MutableVertexOut
+traceMutableDirectionOutOfVertex mutable vertex target = do
+  start <- readVertexOut mutable vertex
+  if start < 0
+    then pure MutableVertexOutHull
+    else do
+      currentPoint <- pointAt mutable vertex
+      startTarget <- edgeOriginPoint mutable (start `xor` 1)
+      halfEdges <- directedEdgeCount mutable
+      let startSide = orient2d currentPoint startTarget target
+          rotateCounterClockwise = startSide == GT
+      traced <-
+        foldWhileM
+          mutableVertexTraceIsSearching
+          (advanceMutableVertexTrace mutable currentPoint target rotateCounterClockwise)
+          (MutableVertexTraceSearching start startSide)
+          [0 .. halfEdges]
+      pure $ case traced of
+        MutableVertexTraceComplete result -> result
+        MutableVertexTraceSearching _ _ -> MutableVertexOutHull
+
+advanceMutableVertexTrace
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> Bool
+  -> MutableVertexTrace
+  -> Int
+  -> ST s MutableVertexTrace
+advanceMutableVertexTrace _ _ _ _ complete@(MutableVertexTraceComplete _) _ = pure complete
+advanceMutableVertexTrace mutable currentPoint target rotateCounterClockwise (MutableVertexTraceSearching current currentSide) _ = do
+  currentTarget <- edgeOriginPoint mutable (current `xor` 1)
+  if currentSide == EQ && projectionFactor currentPoint currentTarget target >= 0
+    then pure (MutableVertexTraceComplete (MutableVertexOutOverlap current))
+    else do
+      following <-
+        if rotateCounterClockwise
+          then (`xor` 1) <$> readPrevious mutable current
+          else readNext mutable (current `xor` 1)
+      followingTarget <- edgeOriginPoint mutable (following `xor` 1)
+      let followingSide = orient2d currentPoint followingTarget target
+      if followingSide == EQ && projectionFactor currentPoint followingTarget target >= 0
+        then pure (MutableVertexTraceComplete (MutableVertexOutOverlap following))
+        else do
+          faceBetween <-
+            readFace mutable (if rotateCounterClockwise then current else following)
+          if faceBetween == 0
+            then pure (MutableVertexTraceComplete MutableVertexOutHull)
+            else
+              if rotateCounterClockwise == (followingSide == LT)
+                then do
+                  segment <-
+                    if rotateCounterClockwise
+                      then readNext mutable current
+                      else readPrevious mutable (current `xor` 1)
+                  pure
+                    ( MutableVertexTraceComplete
+                        (MutableVertexOutEdge (segment `xor` 1))
+                    )
+                else pure (MutableVertexTraceSearching following followingSide)
+
+traceMutableDirectionOutOfEdge
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Point
+  -> Point
+  -> ST s MutableEdgeOut
+traceMutableDirectionOutOfEdge mutable edge lineFrom lineTo = do
+  incident <- readFace mutable edge
+  if incident == 0
+    then pure MutableEdgeOutHull
+    else do
+      edgePrevious <- readPrevious mutable edge
+      edgeNext <- readNext mutable edge
+      edgeOrigin <- edgeOriginPoint mutable edge
+      oppositeVertex <- edgeOriginPoint mutable edgePrevious
+      let !originSide = orient2d lineFrom lineTo edgeOrigin
+          !oppositeSide = orient2d lineFrom lineTo oppositeVertex
+      if originSide == EQ || oppositeSide == EQ
+        then classifyDegenerate edgePrevious edgeNext
+        else
+          if originSide == oppositeSide
+            then do
+              nextOrigin <- edgeOriginPoint mutable edgeNext
+              let !outgoing = edgeNext `xor` 1
+                  !targetSide = orient2d oppositeVertex nextOrigin lineTo
+              pure
+                ( if targetSide == LT
+                    then MutableEdgeOutNone
+                    else MutableEdgeOutEdge outgoing
+                )
+            else do
+              let !outgoing = edgePrevious `xor` 1
+                  !targetSide = orient2d edgeOrigin oppositeVertex lineTo
+              pure
+                ( if targetSide == LT
+                    then MutableEdgeOutNone
+                    else MutableEdgeOutEdge outgoing
+                )
+ where
+  classifyDegenerate edgePrevious edgeNext = do
+    previousIntersects <- mutableEdgeIntersectsNonCollinear mutable lineFrom lineTo edgePrevious
+    nextIntersects <- mutableEdgeIntersectsNonCollinear mutable lineFrom lineTo edgeNext
+    case (previousIntersects, nextIntersects) of
+      (True, False) -> pure (MutableEdgeOutEdge (edgePrevious `xor` 1))
+      (False, True) -> pure (MutableEdgeOutEdge (edgeNext `xor` 1))
+      (True, True) -> MutableEdgeOutVertex <$> readOrigin mutable edgePrevious
+      (False, False) -> pure MutableEdgeOutNone
+
+mutableEdgeIntersectsNonCollinear
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> Int
+  -> ST s Bool
+mutableEdgeIntersectsNonCollinear
+  mutable
+  (Point lineFromX lineFromY)
+  (Point lineToX lineToY)
+  edge = do
+    edgeFromVertex <- readOrigin mutable edge
+    edgeToVertex <- readOrigin mutable (edge `xor` 1)
+    Point edgeFromX edgeFromY <- pointAt mutable edgeFromVertex
+    Point edgeToX edgeToY <- pointAt mutable edgeToVertex
+    let !lineFromSide =
+          orient2dCoordinates
+            edgeFromX edgeFromY edgeToX edgeToY lineFromX lineFromY
+        !lineToSide =
+          orient2dCoordinates
+            edgeFromX edgeFromY edgeToX edgeToY lineToX lineToY
+        !edgeFromSide =
+          orient2dCoordinates
+            lineFromX lineFromY lineToX lineToY edgeFromX edgeFromY
+        !edgeToSide =
+          orient2dCoordinates
+            lineFromX lineFromY lineToX lineToY edgeToX edgeToY
+    pure
+      ( lineFromSide /= lineToSide
+          && edgeFromSide /= edgeToSide
+      )
+
+mutableVertexTraceIsSearching :: MutableVertexTrace -> Bool
+mutableVertexTraceIsSearching (MutableVertexTraceSearching _ _) = True
+mutableVertexTraceIsSearching _ = False
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Query.hs b/src-build/Moonlight/Planar/Internal/Cdt/Query.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Query.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Read-only interrogation of the constrained layer, over both the published
+-- mesh and the thawed one, plus the endpoint validity the verbs share.
+module Moonlight.Planar.Internal.Cdt.Query
+  ( constraintEdges
+  , constraintStorageBytes
+  , existsConstraint
+  , canAddConstraint
+  , intersectsConstraint
+  , getConflictingEdgesBetweenPoints
+  , getConflictingEdgesBetweenVertices
+  , findDirectedEdge
+  , findMutableEdge
+  , validateEndpoints
+  , validVertex
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import qualified Data.IntSet as IntSet
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.IntersectionIterator
+import Moonlight.Planar.Internal.Cdt.Admission
+  ( ConstraintAdmission (..)
+  , constraintAdmission
+  )
+import Moonlight.Planar.Internal.Cdt.Types (CdtError (..))
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.Paged (pagedLength)
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (QueryPoint)
+
+-- | Marked undirected edges in ascending identifier order.
+constraintEdges
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> [UndirectedEdgeId]
+constraintEdges =
+  fmap (UndirectedEdgeId . fromIntegral)
+    . IntSet.toAscList
+    . triConstraintEdges
+{-# INLINE constraintEdges #-}
+
+-- | Bytes occupied by the dense constraint marker plane.
+constraintStorageBytes
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> Integer
+constraintStorageBytes = toInteger . pagedLength . triConstraint
+
+-- | Whether a marked edge directly joins two admitted vertices.
+existsConstraint
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> Bool
+existsConstraint triangulation from to =
+  case findDirectedEdge triangulation from to of
+    Just edge -> Dcel.isConstraintEdge triangulation (asUndirected edge)
+    Nothing -> False
+
+-- | Whether a segment can be admitted without crossing a resident constraint.
+canAddConstraint
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> Bool
+canAddConstraint triangulation from to =
+  from /= to
+    && validVertex triangulation from
+    && validVertex triangulation to
+    && case constraintAdmission triangulation from to of
+      ConstraintBlocked _ -> False
+      ConstraintAdmitted -> True
+
+-- | Whether a query segment properly crosses a resident constraint.
+intersectsConstraint
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> QueryPoint
+  -> QueryPoint
+  -> Bool
+intersectsConstraint triangulation from to =
+  case foldCorridorBetweenPoints triangulation from to firstBlocking () of
+    Just (Left ()) -> True
+    Just (Right ()) -> False
+    Nothing -> not (null (getConflictingEdgesBetweenPoints triangulation from to))
+ where
+  firstBlocking :: () -> Intersection -> Either () ()
+  firstBlocking _ (EdgeIntersection edge)
+    | Dcel.isConstraintEdge triangulation (asUndirected edge) = Left ()
+  firstBlocking _ _ = Right ()
+
+-- | Resident constrained edges properly crossed by a query segment.
+getConflictingEdgesBetweenPoints
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> QueryPoint
+  -> QueryPoint
+  -> [DirectedEdgeId]
+getConflictingEdgesBetweenPoints triangulation from to =
+  [ edge
+  | EdgeIntersection edge <- lineIntersections triangulation from to
+  , Dcel.isConstraintEdge triangulation (asUndirected edge)
+  ]
+
+-- | Resident constrained edges crossed between two admitted vertices.
+getConflictingEdgesBetweenVertices
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> [DirectedEdgeId]
+getConflictingEdgesBetweenVertices triangulation from to
+  | not (validVertex triangulation from && validVertex triangulation to) = []
+  | otherwise =
+      [ edge
+      | EdgeIntersection edge <- lineIntersectionsBetweenVertices triangulation from to
+      , Dcel.isConstraintEdge triangulation (asUndirected edge)
+      ]
+
+-- | Find an oriented edge joining two admitted vertices.
+findDirectedEdge :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> VertexId -> Maybe DirectedEdgeId
+findDirectedEdge triangulation from to =
+  case filter ((== to) . Dcel.destination triangulation) (Dcel.vertexOutgoingEdges triangulation from) of
+    edge : _ -> Just edge
+    [] -> Nothing
+
+-- | Find an oriented edge in a thawed triangulation.
+findMutableEdge
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> ST s (Maybe Int)
+findMutableEdge mutable from to = do
+  start <- readVertexOut mutable from
+  halfEdges <- directedEdgeCount mutable
+  if start < 0
+    then pure Nothing
+    else go (halfEdges + 1) start start False
+ where
+  go
+    :: Int
+    -> Int
+    -> Int
+    -> Bool
+    -> ST s (Maybe Int)
+  go !remaining !start !edge !visited
+    | remaining <= 0 = pure Nothing
+    | visited && edge == start = pure Nothing
+    | otherwise = do
+        destination <- readOrigin mutable (edge `xor` 1)
+        if destination == to
+          then pure (Just edge)
+          else do
+            previousEdge <- readPrevious mutable edge
+            go (remaining - 1) start (previousEdge `xor` 1) True
+
+-- | Validate that both constraint endpoints belong to the mesh.
+validateEndpoints :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> VertexId -> Either (CdtError) ()
+validateEndpoints triangulation from to
+  | not (validVertex triangulation from) = Left (InvalidConstraintVertex from)
+  | not (validVertex triangulation to) = Left (InvalidConstraintVertex to)
+  | otherwise = Right ()
+
+-- | Whether a vertex handle is admitted by the triangulation.
+validVertex :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> Bool
+validVertex triangulation (VertexId vertex) = fromIntegral vertex < Dcel.numVertices triangulation
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Recovery.hs b/src-build/Moonlight/Planar/Internal/Cdt/Recovery.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Recovery.hs
@@ -0,0 +1,412 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Interpretation of a recovery program against the thawed mesh: conflict
+-- strips are flipped away segment by segment and the constraints are set.
+module Moonlight.Planar.Internal.Cdt.Recovery
+  ( ConflictRecovery (..)
+  , applyMutableConstraint
+  , emptyConstraintRequest
+  , recoverMutableRequest
+  , recoverMutableProgramPiece
+  , recordRecoveredSegment
+  , resolveConflictStrip
+  , mutableEdgeCrosses
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (EdgeFlips))
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import Data.Either (isRight)
+import Data.Foldable (traverse_)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Cdt.Combinators
+  ( foldWhileM
+  )
+import Moonlight.Planar.Internal.Cdt.Corridor
+  ( constraintWorkspaceFor
+  , existingProgramTag
+  , readConstraintProgram
+  , recoverProgramTag
+  , scanMutableConstraint
+  )
+import Moonlight.Planar.Internal.Cdt.Query (findMutableEdge)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.DcelOperations.FlipRewrite (flipEdge)
+import Moonlight.Planar.Internal.DcelOperations.FlipRule (isFlippableEdge)
+import Moonlight.Planar.Internal.DcelOperations.Legalize (legalizeEdges)
+import Moonlight.Planar.Internal.Growable (GrowableWord32)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , readScratch
+  , writeScratch
+  )
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Internal.Predicates (orient2dCoordinates)
+
+data ConflictRecovery = ConflictRecovery
+  { recoveredConstraintEdge :: {-# UNPACK #-} !Int
+  , recoveredConstraintFresh :: !Bool
+  }
+
+-- | Admit one request against the thawed mesh and recover it. Every constraint
+-- verb in this module reaches the topology through here; the callers differ
+-- only in what they do with a rejection and in when they publish.
+applyMutableConstraint
+  :: GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> VertexId
+  -> VertexId
+  -> ST s (Either (CdtError) MutableConstraintOutcome)
+applyMutableConstraint programWords mutable operation from to
+  | from == to = pure (Right (MutableConstraintAccepted emptyConstraintRequest))
+  | otherwise = do
+      workspace <- constraintWorkspaceFor programWords mutable
+      scanned <- scanMutableConstraint workspace mutable from to
+      case scanned of
+        Left failure -> pure (Left failure)
+        Right (MutableConstraintScanBlocked blocking) ->
+          pure (Right (MutableConstraintRejected (asUndirected blocking)))
+        Right (MutableConstraintScanAdmitted program) ->
+          fmap MutableConstraintAccepted
+            <$> recoverMutableRequest workspace mutable operation program
+
+emptyConstraintRequest :: ConstraintRequestAccumulator
+emptyConstraintRequest =
+  ConstraintRequestAccumulator
+    { accumulatedRequestPath = []
+    , accumulatedRequestAddedEdges = 0
+    , accumulatedRequestCorridors = 0
+    , accumulatedRequestReusedFaces = 0
+    , accumulatedRequestCrossedEdges = 0
+    }
+
+recoverMutableRequest
+  :: ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> MutableConstraintProgram
+  -> ST s (Either (CdtError) ConstraintRequestAccumulator)
+recoverMutableRequest workspace mutable operation program = do
+  interpreted <-
+    foldWhileM
+      isRight
+      (recoverMutableProgramPiece workspace mutable operation (mutableProgramWordCount program))
+      ( Right
+          ConstraintProgramAccumulator
+            { accumulatedProgramCursor = 0
+            , accumulatedProgramRequest = emptyConstraintRequest
+            }
+      )
+      [1 .. mutableProgramPieceCount program]
+  pure $ case interpreted of
+    Left obstruction -> Left obstruction
+    Right completed
+      | accumulatedProgramCursor completed == mutableProgramWordCount program ->
+          Right (accumulatedProgramRequest completed)
+      | otherwise ->
+          Left
+            ( ConstraintCorridorObstructed
+                (CorridorProgramMalformed (accumulatedProgramCursor completed))
+            )
+
+recoverMutableProgramPiece
+  :: ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Either (CdtError) ConstraintProgramAccumulator
+  -> Int
+  -> ST s (Either (CdtError) ConstraintProgramAccumulator)
+recoverMutableProgramPiece _ _ _ _ failed@(Left _) _ = pure failed
+recoverMutableProgramPiece workspace mutable operation wordCount (Right accumulator) _ = do
+  let !cursor = accumulatedProgramCursor accumulator
+      malformed :: Either CdtError ConstraintProgramAccumulator
+      malformed =
+        Left
+          ( ConstraintCorridorObstructed
+              (CorridorProgramMalformed cursor)
+          )
+  if cursor < 0 || cursor >= wordCount
+    then pure malformed
+    else do
+      tag <- readConstraintProgram workspace cursor
+      case tag of
+        _
+          | tag == existingProgramTag && cursor + 1 < wordCount -> do
+              edge <- readConstraintProgram workspace (cursor + 1)
+              fresh <- setConstraint mutable edge
+              pure
+                ( Right
+                    accumulator
+                      { accumulatedProgramCursor = cursor + 2
+                      , accumulatedProgramRequest =
+                          recordRecoveredSegment
+                            (accumulatedProgramRequest accumulator)
+                            edge
+                            fresh
+                            0
+                            0
+                      }
+                )
+          | tag == recoverProgramTag && cursor + 3 < wordCount -> do
+              rawFrom <- readConstraintProgram workspace (cursor + 1)
+              rawTo <- readConstraintProgram workspace (cursor + 2)
+              conflictCount <- readConstraintProgram workspace (cursor + 3)
+              let !conflictStart = cursor + 4
+                  !nextCursor = conflictStart + conflictCount
+                  !from = VertexId (fromIntegral rawFrom)
+                  !to = VertexId (fromIntegral rawTo)
+              if conflictCount < 0 || nextCursor > wordCount
+                then pure malformed
+                else do
+                  direct <- findMutableEdge mutable rawFrom rawTo
+                  recovered <-
+                    case direct of
+                      Just edge -> do
+                        fresh <- setConstraint mutable edge
+                        pure
+                          ( Right
+                              ConflictRecovery
+                                { recoveredConstraintEdge = edge
+                                , recoveredConstraintFresh = fresh
+                                }
+                          )
+                      Nothing
+                        | conflictCount == 0 ->
+                            pure
+                              ( Left
+                                  ( ConstraintCorridorObstructed
+                                      (CorridorTargetMissing from to)
+                                  )
+                              )
+                        | otherwise ->
+                            resolveConflictStrip
+                              workspace
+                              mutable
+                              operation
+                              from
+                              to
+                              conflictStart
+                              conflictCount
+                  pure $ case recovered of
+                    Left obstruction -> Left obstruction
+                    Right recovery ->
+                      Right
+                        accumulator
+                          { accumulatedProgramCursor = nextCursor
+                          , accumulatedProgramRequest =
+                              recordRecoveredSegment
+                                (accumulatedProgramRequest accumulator)
+                                (recoveredConstraintEdge recovery)
+                                (recoveredConstraintFresh recovery)
+                                conflictCount
+                                (if conflictCount == 0 then 0 else conflictCount + 1)
+                          }
+          | otherwise -> pure malformed
+
+recordRecoveredSegment
+  :: ConstraintRequestAccumulator
+  -> Int
+  -> Bool
+  -> Int
+  -> Int
+  -> ConstraintRequestAccumulator
+recordRecoveredSegment accumulator edge fresh crossed reusedFaces =
+  accumulator
+    { accumulatedRequestPath = DirectedEdgeId (fromIntegral edge) : accumulatedRequestPath accumulator
+    , accumulatedRequestAddedEdges =
+        accumulatedRequestAddedEdges accumulator + if fresh then 1 else 0
+    , accumulatedRequestCorridors =
+        accumulatedRequestCorridors accumulator + if crossed == 0 then 0 else 1
+    , accumulatedRequestReusedFaces =
+        accumulatedRequestReusedFaces accumulator + reusedFaces
+    , accumulatedRequestCrossedEdges =
+        accumulatedRequestCrossedEdges accumulator + crossed
+    }
+
+resolveConflictStrip
+  :: forall s vertex directed undirected face.
+     ConstraintWorkspace s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> VertexId
+  -> VertexId
+  -> Int
+  -> Int
+  -> ST s (Either (CdtError) ConflictRecovery)
+resolveConflictStrip workspace mutable operation from to conflictStart stripLength = do
+  fromPoint <- pointAt mutable (vertexIdIndex from)
+  toPoint <- pointAt mutable (vertexIdIndex to)
+  traverse_
+    (\index -> do
+      edge <- readConstraintProgram workspace (conflictStart + index)
+      writeScratch operation index edge
+    )
+    [0 .. stripLength - 1]
+  recovered <-
+    recover
+      stripLength
+      safetyBudget
+      0
+      0
+      stripLength
+      stripLength
+      []
+      fromPoint
+      toPoint
+  case recovered of
+    Left failure -> pure (Left failure)
+    Right (edge, flipped) -> do
+      fresh <- setConstraint mutable edge
+      legalizeEdges mutable operation flipped
+      pure
+        ( Right
+            ConflictRecovery
+              { recoveredConstraintEdge = edge
+              , recoveredConstraintFresh = fresh
+              }
+        )
+ where
+  safetyBudget = max 64 (32 * (stripLength + 1) * (stripLength + 1))
+
+  -- The corridor is an ordered section through the current triangulation.
+  -- Until an original section is dequeued, no flip can have changed that
+  -- edge's endpoints: flips only repurpose the identity being flipped, and
+  -- requeued identities remain behind every unseen original in this FIFO.
+  -- Consequently the first visit inherits the crossing proof established by
+  -- the corridor walk. Only requeued edges require the predicate again.
+  recover
+    :: Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> [Int]
+    -> Point
+    -> Point
+    -> ST s (Either CdtError (Int, [Int]))
+  recover !capacity !remaining !stalled !headIndex !pendingCount !unseenOriginalCount !flipped !fromPoint !toPoint
+    | remaining <= 0 =
+        pure
+          ( Left
+              ( ConstraintRecoverySafetyBudgetExhausted
+                  from
+                  to
+                  safetyBudget
+                  (safetyBudget - remaining)
+              )
+          )
+    | pendingCount <= 0 = do
+        direct <- findMutableEdge mutable (vertexIdIndex from) (vertexIdIndex to)
+        case direct of
+          Just edge -> pure (Right (edge, flipped))
+          Nothing ->
+            pure
+              (Left (ConstraintRecoveryStripExhausted from to stripLength))
+    | otherwise = do
+        edge <- readScratch operation headIndex
+        let !nextHead = advance capacity headIndex
+            !restCount = pendingCount - 1
+            !nextUnseenOriginalCount = max 0 (unseenOriginalCount - 1)
+        stillCrosses <-
+          if unseenOriginalCount > 0
+            then pure True
+            else mutableEdgeCrosses mutable fromPoint toPoint edge
+        if not stillCrosses
+          then recover capacity (remaining - 1) 0 nextHead restCount nextUnseenOriginalCount flipped fromPoint toPoint
+          else do
+            flippable <- isFlippableEdge mutable edge
+            if flippable
+              then do
+                rewritten <- flipEdge mutable edge
+                case rewritten of
+                  Left obstruction -> pure (Left (CdtBuildError obstruction))
+                  Right () -> do
+                    addCounter operation (Reported EdgeFlips) 1
+                    crossesAfterFlip <- mutableEdgeCrosses mutable fromPoint toPoint edge
+                    if crossesAfterFlip
+                      then do
+                        enqueue capacity nextHead restCount edge
+                        recover
+                          capacity
+                          (remaining - 1)
+                          0
+                          nextHead
+                          (restCount + 1)
+                          nextUnseenOriginalCount
+                          (edge : flipped)
+                          fromPoint
+                          toPoint
+                      else recover capacity (remaining - 1) 0 nextHead restCount nextUnseenOriginalCount (edge : flipped) fromPoint toPoint
+              else do
+                enqueue capacity nextHead restCount edge
+                let !nextCount = restCount + 1
+                    !nextStalled = stalled + 1
+                if nextStalled >= nextCount
+                  then
+                    pure
+                      ( Left
+                          ( ConstraintRecoveryStripUnflippable
+                              from
+                              to
+                              (DirectedEdgeId (fromIntegral edge))
+                              nextCount
+                          )
+                      )
+                  else recover capacity (remaining - 1) nextStalled nextHead nextCount nextUnseenOriginalCount flipped fromPoint toPoint
+
+  enqueue !capacity !headIndex !count !edge =
+    writeScratch operation (wrap capacity (headIndex + count)) edge
+
+  advance :: Int -> Int -> Int
+  advance !capacity !index
+    | index + 1 == capacity = 0
+    | otherwise = index + 1
+
+  wrap :: Int -> Int -> Int
+  wrap !capacity !index
+    | index >= capacity = index - capacity
+    | otherwise = index
+
+mutableEdgeCrosses
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> Point
+  -> Int
+  -> ST s Bool
+mutableEdgeCrosses
+  mutable
+  (Point lineFromX lineFromY)
+  (Point lineToX lineToY)
+  edge = do
+    edgeFromVertex <- readOrigin mutable edge
+    edgeToVertex <- readOrigin mutable (edge `xor` 1)
+    Point edgeFromX edgeFromY <- pointAt mutable edgeFromVertex
+    Point edgeToX edgeToY <- pointAt mutable edgeToVertex
+    let !edgeFromSide =
+          orient2dCoordinates
+            lineFromX lineFromY lineToX lineToY edgeFromX edgeFromY
+        !edgeToSide =
+          orient2dCoordinates
+            lineFromX lineFromY lineToX lineToY edgeToX edgeToY
+        !lineFromSide =
+          orient2dCoordinates
+            edgeFromX edgeFromY edgeToX edgeToY lineFromX lineFromY
+        !lineToSide =
+          orient2dCoordinates
+            edgeFromX edgeFromY edgeToX edgeToY lineToX lineToY
+    pure
+      ( opposite edgeFromSide edgeToSide
+          && opposite lineFromSide lineToSide
+      )
+ where
+  opposite LT GT = True
+  opposite GT LT = True
+  opposite _ _ = False
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Region.hs b/src-build/Moonlight/Planar/Internal/Cdt/Region.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Region.hs
@@ -0,0 +1,40 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | The two-sided reading of the constrained domain: which faces lie outside
+-- the protected boundary and which lie within it.
+module Moonlight.Planar.Internal.Cdt.Region
+  ( outerRegionFaces
+  , boundedRegionFaces
+  ) where
+
+import qualified Data.IntSet as IntSet
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.FloodFillIterator (facesAtEvenBarrierDepth)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Handles.Iterators.FixedIterators (innerFaces)
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+
+-- | Inner faces at even minimum constraint-crossing depth from the outer face.
+outerRegionFaces
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> [FaceId]
+-- Crossing depth, not bare reachability. A face lies outside the constrained
+-- domain when the fewest constraints separating it from the outer face is even,
+-- so the hole of an annulus (two crossings) is outside exactly as its exterior
+-- (none) is. Reachability alone is the depth-zero layer and calls that hole
+-- domain, which would mesh it.
+outerRegionFaces triangulation =
+  facesAtEvenBarrierDepth triangulation (Dcel.isConstraintEdge triangulation)
+
+-- | Inner faces enclosed at odd constraint-crossing depth.
+boundedRegionFaces
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> [FaceId]
+boundedRegionFaces triangulation =
+  [ face
+  | face <- innerFaces triangulation
+  , not (IntSet.member (faceIdIndex face) outside)
+  ]
+ where
+  outside = IntSet.fromList (fmap faceIdIndex (outerRegionFaces triangulation))
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Segment.hs b/src-build/Moonlight/Planar/Internal/Cdt/Segment.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Segment.hs
@@ -0,0 +1,237 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The singleton segment verbs: glue one segment or a polyline, and retire a
+-- constraint, each inside one sealed transaction.
+module Moonlight.Planar.Internal.Cdt.Segment
+  ( addConstraintEdge
+  , addConstraintEdges
+  , removeConstraintEdge
+  , retireConstraintEdge
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bifunctor (first)
+import qualified Data.Vector as V
+import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
+import Moonlight.Planar.BulkLoad (insertMany)
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Cdt.Batch (recoverConstraints)
+import Moonlight.Planar.Internal.Cdt.Combinators
+  ( asConstraintStep
+  , bindMutable
+  )
+import Moonlight.Planar.Internal.Cdt.Recovery (applyMutableConstraint)
+import Moonlight.Planar.Internal.Cdt.Site
+  ( lookupExistingConstraintEndpoint
+  , placeConstraintEndpoint
+  )
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.DcelOperations.Legalize (legalizeEdges)
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  )
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState (OperationState)
+import Moonlight.Planar.Internal.Paged (TransactionShape (LocalTransaction))
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Transaction (runUnmeasuredTransaction)
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (Point (..), HasPosition (..))
+
+-- | Site both endpoints and glue the segment between them, in one transaction.
+-- A refused request publishes nothing, so the caller's triangulation still
+-- stands and the endpoints it would have sited are not among its vertices.
+addConstraintEdge
+  :: forall vertex directed undirected face
+   . HasPosition vertex
+  => Triangulation 'Constrained vertex directed undirected face
+  -> vertex
+  -> vertex
+  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
+addConstraintEdge triangulation fromVertex toVertex = do
+  result <-
+    case (fromExisting, toExisting) of
+      (Just from, Just to) ->
+        applyConstraintToExistingEndpoints
+          triangulation
+          from
+          fromVertex
+          to
+          toVertex
+      _ -> addConstraintWithEndpointPlacement fromExisting toExisting
+  pure result
+ where
+  !fromPoint = position fromVertex
+  !toPoint = position toVertex
+  !fromExisting = lookupExistingConstraintEndpoint triangulation fromPoint
+  !toExisting = lookupExistingConstraintEndpoint triangulation toPoint
+
+  addConstraintWithEndpointPlacement existingFrom existingTo = do
+    (request, frozen) <-
+      runUnmeasuredTransaction
+        CdtBuildError
+        LocalTransaction
+        triangulation
+        (missingEndpointCount existingFrom + missingEndpointCount existingTo)
+        $ \mutable operation -> do
+            programWords <- newGrowableWord32 256
+            asConstraintStep
+              (placeOrReuseConstraintEndpoint mutable operation existingFrom fromPoint fromVertex)
+              `bindMutable` \from ->
+                asConstraintStep
+                  (placeOrReuseConstraintEndpoint mutable operation existingTo toPoint toVertex)
+                  `bindMutable` \to ->
+                    recoverConstraintRequest programWords mutable operation from to
+    pure (publishConstraintResult frozen request)
+
+  missingEndpointCount :: Maybe VertexId -> Int
+  missingEndpointCount = maybe 1 (const 0)
+
+  placeOrReuseConstraintEndpoint
+    :: MutableDcel s vertex directed undirected face
+    -> OperationState s
+    -> Maybe VertexId
+    -> Point
+    -> vertex
+    -> ST s (Either BuildError VertexId)
+  placeOrReuseConstraintEndpoint mutable operation existing point payload =
+    case existing of
+      Just vertex -> do
+        writeVertexData mutable (vertexIdIndex vertex) payload
+        pure (Right vertex)
+      Nothing -> placeConstraintEndpoint mutable operation Nothing point payload
+
+-- | The common singleton case already owns both sites. Resolve them before
+-- opening topology, then retain the original payload semantics in the
+-- transaction. The corridor worker is unchanged; only two redundant point
+-- locations and two unused vertex-capacity reservations disappear.
+applyConstraintToExistingEndpoints
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> vertex
+  -> VertexId
+  -> vertex
+  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
+applyConstraintToExistingEndpoints triangulation from fromPayload to toPayload =
+  do
+    (request, frozen) <-
+      runUnmeasuredTransaction CdtBuildError LocalTransaction triangulation 0 $ \mutable operation -> do
+        programWords <- newGrowableWord32 256
+        writeVertexData mutable (vertexIdIndex from) fromPayload
+        writeVertexData mutable (vertexIdIndex to) toPayload
+        recoverConstraintRequest programWords mutable operation from to
+    pure (publishConstraintResult frozen request)
+
+recoverConstraintRequest
+  :: GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> VertexId
+  -> VertexId
+  -> ST s (Either CdtError ConstraintRequestAccumulator)
+recoverConstraintRequest programWords mutable operation from to =
+  applyMutableConstraint programWords mutable operation from to
+    `bindMutable` \applied ->
+      case applied of
+        MutableConstraintRejected blocking ->
+          pure (Left (ConstraintIntersection blocking))
+        MutableConstraintAccepted request -> pure (Right request)
+{-# INLINE recoverConstraintRequest #-}
+
+-- | Attach one accepted local result to the mesh published by the transaction.
+publishConstraintResult
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> ConstraintRequestAccumulator
+  -> ConstraintResult vertex directed undirected face
+publishConstraintResult frozen request =
+  ConstraintRecoveryResult
+    { constraintRecoveryTriangulation = frozen
+    , constraintRecoveryPathReceipt = V.fromList (reverse (accumulatedRequestPath request))
+    , constraintRecoveryAddedEdges = accumulatedRequestAddedEdges request
+    }
+
+-- | Insert a polyline's vertices and recover each adjacent segment as a
+-- constraint, optionally closing the final segment back to the first.
+addConstraintEdges
+  :: HasPosition vertex
+  => Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector vertex
+  -> Bool
+  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face)
+addConstraintEdges triangulation polylineVertices closed
+  | V.null polylineVertices = Right triangulation
+  | otherwise = do
+      (withVertices, handles) <- insertPolylineVertices triangulation polylineVertices
+      let adjacent = V.zip handles (V.drop 1 handles)
+          closing =
+            if closed && V.length handles > 1
+              then
+                case (handles V.!? (V.length handles - 1), handles V.!? 0) of
+                  (Just finalVertex, Just firstVertex) ->
+                    V.singleton (finalVertex, firstVertex)
+                  _ -> V.empty
+              else V.empty
+      batch <- recoverConstraints withVertices (adjacent V.++ closing)
+      case V.foldl' firstBlocking Nothing (constraintBatchOutcomes batch) of
+        Nothing -> Right (constraintBatchTriangulation batch)
+        Just blocking -> Left (ConstraintIntersection blocking)
+ where
+  firstBlocking found@(Just _) _ = found
+  firstBlocking Nothing outcome =
+    case outcome of
+      ConstraintAccepted _ _ -> Nothing
+      ConstraintRejected blocking -> Just blocking
+
+insertPolylineVertices
+  :: HasPosition vertex
+  => Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector vertex
+  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face, V.Vector VertexId)
+insertPolylineVertices triangulation points = do
+  result <- first CdtBuildError (insertMany triangulation points)
+  let !mapping = buildInputVertices result
+  pure (buildTriangulation result, V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping))
+
+-- | Retire one constraint edge and restore local Delaunay legality.
+removeConstraintEdge
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> UndirectedEdgeId
+  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face)
+removeConstraintEdge triangulation edge
+  | undirectedEdgeIdIndex edge >= edgeCount =
+      Left (ConstraintEdgeIndexOutOfRange edge edgeCount)
+  -- An edge carrying no constraint has nothing to retire, and answering that
+  -- without thawing is the difference between O(1) and a republished mesh.
+  | not (Dcel.isConstraintEdge triangulation edge) = Right triangulation
+  | otherwise = do
+      (_, frozen) <-
+        runUnmeasuredTransaction CdtBuildError LocalTransaction triangulation 0 $ \mutable operation ->
+          retireConstraintEdge mutable operation edge
+      pure frozen
+ where
+  edgeCount = Dcel.numUndirectedEdges triangulation
+
+-- | Retire one constraint inside the open transaction and restore the Delaunay
+-- property across the edge it protected.
+retireConstraintEdge
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> UndirectedEdgeId
+  -> ST s (Either (CdtError) ())
+retireConstraintEdge mutable operation edge = do
+  halfEdges <- directedEdgeCount mutable
+  let !directed = directedEdgeIdIndex (normalizedDirected edge)
+  if directed >= halfEdges
+    then pure (Left (ConstraintEdgeIndexOutOfRange edge (halfEdges `quot` 2)))
+    else do
+      constrained <- readConstraint mutable directed
+      if not constrained
+        then pure (Right ())
+        else do
+          _ <- clearConstraint mutable directed
+          legalizeEdges mutable operation [directed]
+          pure (Right ())
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Site.hs b/src-build/Moonlight/Planar/Internal/Cdt/Site.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Site.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Siting of constraint endpoints: resolving a point to an existing handle,
+-- and materializing one inside an open transaction when it has none.
+module Moonlight.Planar.Internal.Cdt.Site
+  ( lookupExistingConstraintEndpoint
+  , placeConstraintEndpoint
+  ) where
+
+import Control.Monad.ST (ST)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Insertion (insertVertexAtPoint)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  )
+import Moonlight.Planar.Internal.PointIndex (lookupPointIndex)
+import Moonlight.Planar.Internal.Probe (Probe (..))
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, ExistingPoints, DuplicatePoints))
+import Moonlight.Planar.Point (Point (..))
+
+lookupExistingConstraintEndpoint
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Maybe VertexId
+lookupExistingConstraintEndpoint triangulation point =
+  VertexId . fromIntegral
+    <$> lookupPointIndex
+      (triPointX triangulation)
+      (triPointY triangulation)
+      (triPointIndex triangulation)
+      point
+
+-- | Materialize one point in the open transaction. A payload standing at an
+-- occupied position keeps that handle and overwrites the payload, which is what
+-- the persistent insertion verb settled on.
+--
+-- The coordinates are checked here rather than by the callers, because a split
+-- point is computed rather than supplied: @lineIntersection@ refuses only a
+-- zero denominator, and a denominator merely close to zero answers a coordinate
+-- no arena should hold.
+placeConstraintEndpoint
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> vertex
+  -> ST s (Either BuildError VertexId)
+placeConstraintEndpoint mutable operation hint point payload =
+  case validatePoint Nothing point of
+    Left failure -> pure (Left failure)
+    Right _ -> do
+      addCounter operation (Reported InputPoints) 1
+      outcome <- insertVertexAtPoint @'ProbeOff mutable operation hint point payload
+      case outcome of
+        Left failure -> pure (Left failure)
+        Right (vertex, disposition) -> do
+          case disposition of
+            AlreadyPresent -> do
+              writeVertexData mutable vertex payload
+              addCounter operation (Reported ExistingPoints) 1
+              addCounter operation (Reported DuplicatePoints) 1
+            Inserted -> addCounter operation (Reported UniquePoints) 1
+          pure (Right (VertexId (fromIntegral vertex)))
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Split.hs b/src-build/Moonlight/Planar/Internal/Cdt/Split.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Split.hs
@@ -0,0 +1,473 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The splitting program: corridors that divide every constraint they cross,
+-- driven across as few sealed transactions as the vertex reservation allows.
+module Moonlight.Planar.Internal.Cdt.Split
+  ( addConstraintAndSplit
+  , addConstraintsAndSplit
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import qualified Data.List as List
+import qualified Data.Vector as V
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Cdt.Combinators
+  ( asConstraintStep
+  , bindMutable
+  )
+import Moonlight.Planar.Internal.Cdt.Corridor
+  ( constraintWorkspaceFor
+  , scanMutableConstraint
+  )
+import Moonlight.Planar.Internal.Cdt.Query
+  ( findMutableEdge
+  , getConflictingEdgesBetweenVertices
+  , validateEndpoints
+  )
+import Moonlight.Planar.Internal.Cdt.Recovery
+  ( applyMutableConstraint
+  , recoverMutableRequest
+  )
+import Moonlight.Planar.Internal.Cdt.Segment (retireConstraintEdge)
+import Moonlight.Planar.Internal.Cdt.Site (placeConstraintEndpoint)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  )
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState (OperationState)
+import Moonlight.Planar.Internal.Paged
+  ( TransactionShape (DenseTransaction, LocalTransaction)
+  )
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Transaction (runUnmeasuredTransaction)
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Point (canonicalPoint)
+
+-- | The part of a splitting request that does not move: the payload maker, the
+-- endpoints the failure report must name, the iteration budget, and the
+-- transaction's vertex reservation, past which the corridor suspends rather
+-- than writes.
+data SplitRequest vertex = SplitRequest
+  { splitRequestMakeVertex :: !(Point -> vertex)
+  , splitRequestFrom :: !VertexId
+  , splitRequestTo :: !VertexId
+  , splitRequestBudget :: {-# UNPACK #-} !Int
+  , splitRequestVertexBound :: {-# UNPACK #-} !Int
+  }
+
+-- | The part that does: how much budget is left, which vertex the unrecovered
+-- remainder starts at, and the path and edge count accumulated behind it. The
+-- path runs newest-first and is reversed once, at publication.
+data SplitCursor = SplitCursor
+  { splitCursorRemaining :: {-# UNPACK #-} !Int
+  , splitCursorAt :: !VertexId
+  , splitCursorPath :: ![DirectedEdgeId]
+  , splitCursorAdded :: {-# UNPACK #-} !Int
+  }
+
+-- | How a corridor run ends inside its transaction: settled at the target, or
+-- suspended mid-corridor because the next division would outgrow the vertex
+-- reservation. A suspension is not a refusal; the driver publishes,
+-- re-reserves against the published mesh, and resumes at the cursor.
+data SplitStep
+  = SplitSettled !SplitCursor
+  | SplitSuspended !SplitCursor
+
+-- | What one settled request leaves behind.
+data SplitReceipt = SplitReceipt
+  { splitReceiptPath :: !(V.Vector DirectedEdgeId)
+  , splitReceiptAdded :: {-# UNPACK #-} !Int
+  }
+
+-- | Glue the requested segment, dividing every constraint it crosses at the
+-- crossing point rather than refusing it. One transaction carries the whole
+-- corridor: a singleton is a batch of one under the shared driver.
+addConstraintAndSplit
+  :: (Point -> vertex)
+  -> Triangulation 'Constrained vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
+addConstraintAndSplit makeVertex triangulation from to = do
+  (published, receipts) <-
+    driveConstraintSplits LocalTransaction makeVertex triangulation (V.singleton (from, to))
+  pure
+    ( case receipts V.!? 0 of
+        Just receipt ->
+          ConstraintRecoveryResult
+            { constraintRecoveryTriangulation = published
+            , constraintRecoveryPathReceipt = splitReceiptPath receipt
+            , constraintRecoveryAddedEdges = splitReceiptAdded receipt
+            }
+        Nothing -> ConstraintRecoveryResult published V.empty 0
+    )
+
+-- | Divide every requested segment inside as few transactions as the vertex
+-- reservation allows: usually one, which is the referent semantics for a
+-- splitting batch. Nothing is republished between corridors unless a corridor
+-- crosses constraints created by an earlier request in the same batch --
+-- growth no census against the base can see. In that one case the corridor
+-- suspends, the chunk publishes, and the driver re-reserves against the
+-- published mesh and resumes at the suspended cursor.
+addConstraintsAndSplit
+  :: (Point -> vertex)
+  -> Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector (VertexId, VertexId)
+  -> Either (CdtError) (ConstraintSplitBatchResult vertex directed undirected face)
+addConstraintsAndSplit makeVertex triangulation requests = do
+  (published, receipts) <- driveConstraintSplits DenseTransaction makeVertex triangulation requests
+  pure
+    ConstraintRecoveryResult
+      { constraintRecoveryTriangulation = published
+      , constraintRecoveryPathReceipt = V.map splitReceiptPath receipts
+      , constraintRecoveryAddedEdges = V.sum (V.map splitReceiptAdded receipts)
+      }
+
+-- | The obstruction census for one request against a published mesh: how many
+-- constrained crossings its corridor holds, and the first of them. The first
+-- crossing is a valid local section of the next transaction before any rewrite
+-- occurs, so a chunk's first request carries it across the thaw boundary
+-- instead of immediately rediscovering it; every later request rescans,
+-- because earlier corridors may have rewritten the topology the witness names.
+splitCensus
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> (VertexId, VertexId)
+  -> (Int, Maybe DirectedEdgeId)
+splitCensus triangulation (from, to)
+  | from == to = (0, Nothing)
+  | otherwise =
+      List.foldl'
+        countCrossing
+        (0, Nothing)
+        (getConflictingEdgesBetweenVertices triangulation from to)
+ where
+  countCrossing :: (Int, Maybe DirectedEdgeId) -> DirectedEdgeId -> (Int, Maybe DirectedEdgeId)
+  countCrossing (!count, firstCrossing) crossing =
+    ( count + 1
+    , case firstCrossing of
+        Just first -> Just first
+        Nothing -> Just crossing
+    )
+
+-- | Interpret splitting requests in order across as few sealed transactions as
+-- possible. Each chunk reserves against the exact obstruction census of every
+-- remaining request, so a chunk always settles at least its first request:
+-- that census ran against the very mesh the chunk thawed and is exact before
+-- any in-session rewrite. Termination follows.
+driveConstraintSplits
+  :: forall vertex directed undirected face.
+     TransactionShape
+  -> (Point -> vertex)
+  -> Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector (VertexId, VertexId)
+  -> Either
+      (CdtError)
+      ( Triangulation 'Constrained vertex directed undirected face
+      , V.Vector SplitReceipt
+      )
+driveConstraintSplits shape makeVertex base requests = do
+  V.mapM_ (uncurry (validateEndpoints base)) requests
+  if V.all (uncurry (==)) requests
+    then Right (base, V.replicate (V.length requests) emptySplitReceipt)
+    else advance base 0 [] Nothing
+ where
+  emptySplitReceipt = SplitReceipt V.empty 0
+
+  advance
+    :: Triangulation 'Constrained vertex directed undirected face
+    -> Int
+    -> [SplitReceipt]
+    -> Maybe SplitCursor
+    -> Either
+        CdtError
+        ( Triangulation 'Constrained vertex directed undirected face
+        , V.Vector SplitReceipt
+        )
+  advance triangulation start settled resumed
+    | start >= V.length requests =
+        Right (triangulation, V.fromList (reverse settled))
+    | otherwise = do
+        let remaining = V.drop start requests
+            firstRound = start == 0 && case resumed of
+              Nothing -> True
+              Just _ -> False
+            -- The opening chunk reserves optimistically -- one crossing per
+            -- request -- rather than walking every corridor for an exact
+            -- census before any work begins. A request that outgrows the
+            -- reservation suspends mid-corridor with its partial work kept,
+            -- and the next round censuses exactly against the published mesh,
+            -- so heavy batches pay at most one speculative chunk.
+            censuses
+              | firstRound = V.replicate (V.length remaining) (1, Nothing)
+              | otherwise = V.map (splitCensus triangulation) remaining
+            !reservedSites = V.sum (V.map fst censuses)
+            !additionalCapacity = 2 * reservedSites + 8
+            -- The walk budget bounds divisions per request; constraints born
+            -- inside the chunk are covered by the reservation term.
+            !budget =
+              2 * (Dcel.numConstraints triangulation + 2 * reservedSites + V.length remaining)
+                + Dcel.numUndirectedEdges triangulation
+                + 8
+        ((receipts, suspended), published) <-
+          runUnmeasuredTransaction
+            CdtBuildError
+            shape
+            triangulation
+            additionalCapacity
+            $ \mutable operation -> do
+            programWords <- newGrowableWord32 32
+            runSplitChunk
+              makeVertex
+              mutable
+              operation
+              programWords
+              remaining
+              censuses
+              (pointCapacity mutable)
+              budget
+              resumed
+        advance published (start + length receipts) (receipts ++ settled) suspended
+
+-- | One sealed transaction over a prefix of the remaining requests. Receipts
+-- run newest-first; a suspension carries no receipt, so the settled count is
+-- exactly the receipt count.
+runSplitChunk
+  :: (Point -> vertex)
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> GrowableWord32 s
+  -> V.Vector (VertexId, VertexId)
+  -> V.Vector (Int, Maybe DirectedEdgeId)
+  -> Int
+  -> Int
+  -> Maybe SplitCursor
+  -> ST s (Either CdtError ([SplitReceipt], Maybe SplitCursor))
+runSplitChunk makeVertex mutable operation programWords requests censuses capacity budget resumed =
+  go 0 [] resumed
+ where
+  go offset receipts pending
+    | offset >= V.length requests = pure (Right (receipts, Nothing))
+    | otherwise =
+        let (from, to) = requests V.! offset
+        in case pending of
+             Nothing
+               | from == to ->
+                   go (offset + 1) (SplitReceipt V.empty 0 : receipts) Nothing
+             _ ->
+               let witness = if offset == 0 then snd (censuses V.! 0) else Nothing
+                   cursor = case pending of
+                     Just resumedCursor -> resumedCursor
+                     Nothing -> SplitCursor budget from [] 0
+                   request =
+                     SplitRequest
+                       { splitRequestMakeVertex = makeVertex
+                       , splitRequestFrom = from
+                       , splitRequestTo = to
+                       , splitRequestBudget = budget
+                       , splitRequestVertexBound = capacity
+                       }
+               in splitConstraintCorridor request programWords mutable operation witness cursor
+                    `bindMutable` \step ->
+                      case step of
+                        SplitSettled settledCursor ->
+                          go (offset + 1) (receiptOf settledCursor : receipts) Nothing
+                        SplitSuspended suspendedCursor ->
+                          pure (Right (receipts, Just suspendedCursor))
+
+  receiptOf cursor =
+    SplitReceipt
+      { splitReceiptPath = V.fromList (reverse (splitCursorPath cursor))
+      , splitReceiptAdded = splitCursorAdded cursor
+      }
+
+splitConstraintCorridor
+  :: SplitRequest vertex
+  -> GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe DirectedEdgeId
+  -> SplitCursor
+  -> ST s (Either (CdtError) SplitStep)
+splitConstraintCorridor request programWords mutable operation initialCrossing initialCursor =
+  case initialCrossing of
+    Just crossing -> divide initialCursor crossing
+    Nothing -> descend initialCursor
+ where
+  !target = splitRequestTo request
+
+  descend cursor
+    | splitCursorRemaining cursor <= 0 =
+        pure
+          ( Left
+              ( ConstraintSplitBudgetExhausted
+                  (splitRequestFrom request)
+                  target
+                  (splitRequestBudget request)
+              )
+          )
+    | otherwise = do
+        workspace <- constraintWorkspaceFor programWords mutable
+        scanMutableConstraint workspace mutable (splitCursorAt cursor) target
+          `bindMutable` \scanned ->
+            case scanned of
+              MutableConstraintScanAdmitted program ->
+                recoverMutableRequest workspace mutable operation program
+                  `bindMutable` \final ->
+                    pure
+                      ( Right
+                          ( SplitSettled
+                              (advanceCursor cursor (splitCursorRemaining cursor) target final)
+                          )
+                      )
+              MutableConstraintScanBlocked crossing -> divide cursor crossing
+
+  divide cursor crossing = do
+    occupied <- pointCount mutable
+    if occupied + 2 > splitRequestVertexBound request
+      then pure (Right (SplitSuspended cursor))
+      else divideWithin cursor crossing
+
+  divideWithin cursor crossing = do
+    let !crossingEdge = directedEdgeIdIndex crossing
+        !oldConstraint = asUndirected crossing
+    segmentFrom <- pointAt mutable (vertexIdIndex (splitCursorAt cursor))
+    segmentTo <- pointAt mutable (vertexIdIndex target)
+    oldFrom <- readOrigin mutable crossingEdge
+    oldTo <- readOrigin mutable (crossingEdge `xor` 1)
+    edgeFrom <- pointAt mutable oldFrom
+    edgeTo <- pointAt mutable oldTo
+    -- The split point lies on the crossing edge, so both its incident faces
+    -- already contain it and the exact walk settles on its first probe. Without
+    -- this the locate is unhinted, and an unhinted locate descends vertex by
+    -- vertex from the arena's first face -- a distance that grows with the mesh
+    -- while the corridor it is splitting stays a fixed few faces wide. A hull
+    -- edge carries the outer face on one side, which the locator would refuse.
+    incidentFace <- readFace mutable crossingEdge
+    twinFace <- readFace mutable (crossingEdge `xor` 1)
+    let !splitHint = Just (if incidentFace > 0 then incidentFace else twinFace)
+    let !oldEndpoints =
+          ( VertexId (fromIntegral oldFrom)
+          , VertexId (fromIntegral oldTo)
+          )
+    case lineIntersection crossing segmentFrom segmentTo edgeFrom edgeTo of
+      Left indeterminate -> pure (Left indeterminate)
+      Right splitPoint ->
+        -- The split lands where the intersection says, not where a round trip
+        -- through 'makeVertex' happens to put it.
+        asConstraintStep
+          ( placeConstraintEndpoint
+              mutable
+              operation
+              splitHint
+              splitPoint
+              (splitRequestMakeVertex request splitPoint)
+          )
+          `bindMutable` \splitVertex ->
+            repairIfRounded oldConstraint oldEndpoints splitVertex `bindMutable` \() ->
+              applyMutableConstraint programWords mutable operation (splitCursorAt cursor) splitVertex
+                `bindMutable` \prefix ->
+                  case prefix of
+                    MutableConstraintRejected blocking ->
+                      pure (Left (ConstraintIntersection blocking))
+                    MutableConstraintAccepted segment ->
+                      descend
+                        (advanceCursor cursor (splitCursorRemaining cursor - 1) splitVertex segment)
+
+  repairIfRounded oldConstraint oldEndpoints splitVertex = do
+    divided <- mutableEdgeWasSplit mutable oldConstraint oldEndpoints splitVertex
+    if divided
+      then pure (Right ())
+      else repairRoundedSplit programWords mutable operation oldConstraint oldEndpoints splitVertex
+
+  advanceCursor cursor remaining reached segment =
+    cursor
+      { splitCursorRemaining = remaining
+      , splitCursorAt = reached
+      , splitCursorPath = accumulatedRequestPath segment ++ splitCursorPath cursor
+      , splitCursorAdded = splitCursorAdded cursor + accumulatedRequestAddedEdges segment
+      }
+
+-- | Whether the inserted vertex actually divided the constraint it was placed
+-- on. It did if that constraint is gone — the split retired the identity — or
+-- if both halves now carry one. Rounding can place the vertex somewhere that
+-- leaves the old constraint standing and neither half glued.
+mutableEdgeWasSplit
+  :: MutableDcel s vertex directed undirected face
+  -> UndirectedEdgeId
+  -> (VertexId, VertexId)
+  -> VertexId
+  -> ST s Bool
+mutableEdgeWasSplit mutable edge (from, to) splitVertex = do
+  halfEdges <- directedEdgeCount mutable
+  let !directed = directedEdgeIdIndex (normalizedDirected edge)
+  stillConstrained <-
+    if directed >= halfEdges
+      then pure False
+      else readConstraint mutable directed
+  if not stillConstrained
+    then pure True
+    else do
+      leading <- mutableConstraintBetween mutable from splitVertex
+      if leading
+        then mutableConstraintBetween mutable splitVertex to
+        else pure False
+
+mutableConstraintBetween
+  :: MutableDcel s vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> ST s Bool
+mutableConstraintBetween mutable from to = do
+  found <- findMutableEdge mutable (vertexIdIndex from) (vertexIdIndex to)
+  case found of
+    Nothing -> pure False
+    Just edge -> readConstraint mutable edge
+
+-- | The split point rounded onto neither half of the constraint it was meant to
+-- divide. Retire the stale identity and glue both halves against the vertex
+-- that was actually sited. What this recovers is repair rather than the
+-- requested segment, so it contributes nothing to the request's path.
+repairRoundedSplit
+  :: GrowableWord32 s
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> UndirectedEdgeId
+  -> (VertexId, VertexId)
+  -> VertexId
+  -> ST s (Either (CdtError) ())
+repairRoundedSplit programWords mutable operation oldEdge (from, to) splitVertex =
+  retireConstraintEdge mutable operation oldEdge `bindMutable` \() ->
+    glue from splitVertex `bindMutable` \() -> glue splitVertex to
+ where
+  glue start end =
+    applyMutableConstraint programWords mutable operation start end `bindMutable` \recovered ->
+      pure $ case recovered of
+        MutableConstraintRejected blocking -> Left (ConstraintIntersection blocking)
+        MutableConstraintAccepted _ -> Right ()
+
+lineIntersection
+  :: DirectedEdgeId
+  -> Point
+  -> Point
+  -> Point
+  -> Point
+  -> Either (CdtError) (Point)
+lineIntersection crossing (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy)
+  | denominator == 0 =
+      Left (ConstraintSplitIntersectionIndeterminate crossing)
+  | otherwise =
+      Right (canonicalPoint (Point (ax + t * rx) (ay + t * ry)))
+ where
+  rx = bx - ax
+  ry = by - ay
+  sx = dx - cx
+  sy = dy - cy
+  denominator = rx * sy - ry * sx
+  t = ((cx - ax) * sy - (cy - ay) * sx) / denominator
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Types.hs b/src-build/Moonlight/Planar/Internal/Cdt/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Types.hs
@@ -0,0 +1,364 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | The shared vocabulary of the constrained layer: its refusals, its published
+-- results, and the transaction-local state its interpreters carry.
+module Moonlight.Planar.Internal.Cdt.Types
+  ( CdtError (..)
+  , CanonicalSegment (..)
+  , ConstraintConflict (..)
+  , ConstrainedUnionError (..)
+  , ConstrainedSeamSide (..)
+  , ConstrainedSeamFaceEvidence (..)
+  , ConstrainedSeamResult (..)
+  , CorridorObstruction (..)
+  , ConstraintRecoveryResult (..)
+  , ConstraintResult
+  , ConstraintOutcome (..)
+  , ConstraintBatchStats (..)
+  , ConstraintBatchResult (..)
+  , ConstrainedExtensionResult (..)
+  , ConstraintSplitBatchResult
+  , ConstraintBatchAccumulator (..)
+  , ConstraintRequestAccumulator (..)
+  , ConstraintWorkspace (..)
+  , MutableConstraintProgram (..)
+  , MutableConstraintOutcome (..)
+  , MutableProgramCursor (..)
+  , MutablePlanWalk (..)
+  , MutableConstraintScan (..)
+  , ConstraintProgramAccumulator (..)
+  , CdtBuildResult (..)
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.Vector as V
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId
+  , FaceId
+  , UndirectedEdgeId
+  , VertexId
+  )
+import Moonlight.Planar.Internal.Growable (GrowableWord32)
+import Moonlight.Planar.Internal.Paged (PublicationStats)
+import Moonlight.Planar.Internal.Representation (BuildResult, Triangulation)
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.IntersectionIterator (Intersection)
+import GHC.Generics (Generic)
+
+-- | Typed refusal surface for constrained construction and corridor recovery.
+data CdtError
+  = CdtBuildError !BuildError
+  | InvalidConstraintVertex !VertexId
+  | ConstraintIntersection !UndirectedEdgeId
+  -- ^ The first resident constraint the corridor from the source vertex
+  -- meets; a request crossing several is refused on the nearest one.
+  | ConstraintInputConflicts !(V.Vector (Int, Int))
+  | ConstraintCorridorObstructed !CorridorObstruction
+  | ConstraintBatchCardinalityMismatch
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  | ConstraintEndpointIndexOutOfRange
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  | ConstraintSplitBudgetExhausted
+      !VertexId
+      !VertexId
+      {-# UNPACK #-} !Int
+  | ConstraintSplitIntersectionIndeterminate
+      !DirectedEdgeId
+  | ConstraintEdgeIndexOutOfRange
+      !UndirectedEdgeId
+      {-# UNPACK #-} !Int
+  | ConstraintRecoverySafetyBudgetExhausted
+      !VertexId
+      !VertexId
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  | ConstraintRecoveryStripExhausted
+      !VertexId
+      !VertexId
+      {-# UNPACK #-} !Int
+  | ConstraintRecoveryStripUnflippable
+      !VertexId
+      !VertexId
+      !DirectedEdgeId
+      {-# UNPACK #-} !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Geometric identity of one constraint segment. The constructor is private:
+-- endpoints are always canonical points in ascending order, so direction and
+-- edge numbering cannot leak into union witnesses.
+data CanonicalSegment = CanonicalSegment
+  { -- | Lesser endpoint under the point ordering.
+    segmentStart :: !(Point)
+  , -- | Greater endpoint under the point ordering.
+    segmentEnd :: !(Point)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A pair of properly crossing constraints, ordered independently of operand
+-- and traversal order.
+data ConstraintConflict = ConstraintConflict
+  { -- | Lesser segment under the canonical segment ordering.
+    conflictFirstSegment :: !(CanonicalSegment)
+  , -- | Greater segment under the canonical segment ordering.
+    conflictSecondSegment :: !(CanonicalSegment)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Complete obstruction surface for atomic constrained union.
+data ConstrainedUnionError
+  = ConstraintUnionConflicts !(NonEmpty (ConstraintConflict))
+  | ConstraintUnionConstructionFailed !(CdtError)
+  | ConstraintUnionSiteMissing !(Point)
+  | ConstraintUnionNotSeparated
+  -- | Local publication would otherwise enumerate a dense resident payload
+  -- plane. Compact geometry-only payloads before requesting a seam.
+  | ConstraintUnionRequiresGeometryOnlyPublication
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Source-local side presented to a separated seam's face-preservation
+-- selector. The resident side keeps its handles; incoming face handles are
+-- translated only inside the join interpreter.
+data ConstrainedSeamSide
+  = SeamResident
+  | SeamIncoming
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact proof that one active source face survives as one target face. The
+-- three points are stored in ascending order, so the witness is independent
+-- of local face rotation and handle numbering.
+data ConstrainedSeamFaceEvidence = ConstrainedSeamFaceEvidence
+  { constrainedSeamSourceFace :: !FaceId
+    -- ^ Face handle in the source operand.
+  , constrainedSeamTargetFace :: !FaceId
+    -- ^ Corresponding face handle in the published result.
+  , constrainedSeamFaceFirstPoint :: !(Point)
+    -- ^ Least vertex position under canonical point order.
+  , constrainedSeamFaceSecondPoint :: !(Point)
+    -- ^ Middle vertex position under canonical point order.
+  , constrainedSeamFaceThirdPoint :: !(Point)
+    -- ^ Greatest vertex position under canonical point order.
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Atomic publication of a source-preserving constrained seam and the proof
+-- needed to transport solved face-indexed data. The receipt is derived during
+-- the ordered append/seam interpretation; canonical numbering remains an
+-- explicit observation rather than hidden work in this local operation.
+data ConstrainedSeamResult vertex = ConstrainedSeamResult
+  { constrainedSeamResultTriangulation
+      :: !(Triangulation 'Constrained vertex () () ())
+    -- ^ Published constrained union.
+  , constrainedSeamLeftFaceCount :: {-# UNPACK #-} !Int
+    -- ^ Stable size of the resident source face block. In selective mode this
+    -- is a handle-partition witness, not proof that every resident face was
+    -- selected.
+  , constrainedSeamRightFaceEvidence
+      :: !(V.Vector (ConstrainedSeamFaceEvidence))
+    -- ^ Exact transport evidence only for selected incoming source faces.
+  , constrainedSeamJoinFaces :: !(V.Vector FaceId)
+    -- ^ Exact final J component, including rewritten exterior source filler.
+  , constrainedSeamLeftConstraintCount :: {-# UNPACK #-} !Int
+    -- ^ Stable number of left-source constraints.
+  , constrainedSeamConstraintStats :: !ConstraintBatchStats
+    -- ^ Constraint recovery work performed by the seam.
+  , constrainedSeamBuildStats :: !BuildStats
+    -- ^ Topology work performed by the seam.
+  , constrainedSeamPublicationStats :: !PublicationStats
+    -- ^ Page publication work measured by the transaction owner.
+  , constrainedSeamCachedFrontierPointReads :: {-# UNPACK #-} !Int
+    -- ^ Frontier point reads performed while interpreting the cached seam.
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance Eq vertex => Eq (ConstrainedSeamResult vertex)
+deriving stock instance Show vertex => Show (ConstrainedSeamResult vertex)
+
+-- | Structural witness that corridor recovery could not complete.
+data CorridorObstruction
+  = CorridorWalkDidNotTerminate {-# UNPACK #-} !Int
+  | CorridorBoundaryMissing !VertexId !VertexId
+  | CorridorProgramMalformed {-# UNPACK #-} !Int
+  | CorridorTargetMissing !VertexId !VertexId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One constrained publication and its path receipt. The path carrier names
+-- whether the caller requested one recovered path or an ordered family; the
+-- mesh and edge count have one representation in either case.
+data ConstraintRecoveryResult pathReceipt vertex directed undirected face = ConstraintRecoveryResult
+  { constraintRecoveryTriangulation :: !(Triangulation 'Constrained vertex directed undirected face)
+    -- ^ Published constrained triangulation.
+  , constraintRecoveryPathReceipt :: !pathReceipt
+    -- ^ Recovered path evidence at the operation's requested multiplicity.
+  , constraintRecoveryAddedEdges :: {-# UNPACK #-} !Int
+    -- ^ Edges created while recovering the path evidence.
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance
+  (Eq pathReceipt, Eq vertex, Eq directed, Eq undirected, Eq face)
+  => Eq (ConstraintRecoveryResult pathReceipt vertex directed undirected face)
+deriving stock instance
+  (Show pathReceipt, Show vertex, Show directed, Show undirected, Show face)
+  => Show (ConstraintRecoveryResult pathReceipt vertex directed undirected face)
+
+-- | Atomic result of admitting one constraint segment.
+type ConstraintResult vertex directed undirected face =
+  ConstraintRecoveryResult (V.Vector DirectedEdgeId) vertex directed undirected face
+
+-- | Per-request outcome in a constraint batch.
+data ConstraintOutcome
+  = ConstraintAccepted
+      !(V.Vector DirectedEdgeId)
+      {-# UNPACK #-} !Int
+  | ConstraintRejected
+      !UndirectedEdgeId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Aggregate work and disposition counts for a constraint batch.
+data ConstraintBatchStats = ConstraintBatchStats
+  { constraintBatchRequests :: {-# UNPACK #-} !Int
+    -- ^ Requests interpreted.
+  , constraintBatchAccepted :: {-# UNPACK #-} !Int
+    -- ^ Requests admitted.
+  , constraintBatchRejected :: {-# UNPACK #-} !Int
+    -- ^ Requests refused by an existing constraint.
+  , constraintBatchCorridors :: {-# UNPACK #-} !Int
+    -- ^ Recovery corridors opened.
+  , constraintBatchReusedFaces :: {-# UNPACK #-} !Int
+    -- ^ Existing faces retained while recovering corridors.
+  , constraintBatchCrossedEdges :: {-# UNPACK #-} !Int
+    -- ^ Edges crossed while tracing corridors.
+  }
+  deriving stock (Eq, Show, Read, Generic)
+  deriving anyclass (NFData)
+
+-- | Atomic publication of one constraint batch and its receipts.
+data ConstraintBatchResult vertex directed undirected face = ConstraintBatchResult
+  { constraintBatchTriangulation :: !(Triangulation 'Constrained vertex directed undirected face)
+    -- ^ Published constrained triangulation.
+  , constraintBatchOutcomes :: !(V.Vector ConstraintOutcome)
+    -- ^ Outcomes in request order.
+  , constraintBatchStats :: !ConstraintBatchStats
+    -- ^ Aggregate work performed by the batch.
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance
+  (Eq vertex, Eq directed, Eq undirected, Eq face)
+  => Eq (ConstraintBatchResult vertex directed undirected face)
+deriving stock instance
+  (Show vertex, Show directed, Show undirected, Show face)
+  => Show (ConstraintBatchResult vertex directed undirected face)
+
+-- | Receipt of one asymmetric constrained extension. The triangulation and
+-- all telemetry arise from the same sealed transaction: base sites were
+-- resident, incoming sites were inserted, and only incoming constraints were
+-- interpreted. A later refinement is deliberately a separate operation over
+-- this immutable result, not a hidden continuation of this transaction.
+data ConstrainedExtensionResult vertex directed undirected face = ConstrainedExtensionResult
+  { constrainedExtensionConstraintBatch :: {-# UNPACK #-} !(ConstraintBatchResult vertex directed undirected face)
+    -- ^ Published extension and incoming-constraint receipts.
+  , constrainedExtensionBuildStats :: !BuildStats
+    -- ^ Topology work performed by the extension.
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance
+  (Eq vertex, Eq directed, Eq undirected, Eq face)
+  => Eq (ConstrainedExtensionResult vertex directed undirected face)
+deriving stock instance
+  (Show vertex, Show directed, Show undirected, Show face)
+  => Show (ConstrainedExtensionResult vertex directed undirected face)
+
+-- | One published mesh carrying every requested division. The paths are
+-- per-request receipts in traversal order; a later request may have rewritten
+-- topology an earlier path names, which is the same as-traversed reading the
+-- singleton path already carries.
+type ConstraintSplitBatchResult vertex directed undirected face =
+  ConstraintRecoveryResult (V.Vector (V.Vector DirectedEdgeId)) vertex directed undirected face
+
+data ConstraintBatchAccumulator = ConstraintBatchAccumulator
+  { accumulatedConstraintOutcomes :: ![ConstraintOutcome]
+  , accumulatedConstraintStats :: !ConstraintBatchStats
+  }
+
+data ConstraintRequestAccumulator = ConstraintRequestAccumulator
+  { accumulatedRequestPath :: ![DirectedEdgeId]
+  , accumulatedRequestAddedEdges :: {-# UNPACK #-} !Int
+  , accumulatedRequestCorridors :: {-# UNPACK #-} !Int
+  , accumulatedRequestReusedFaces :: {-# UNPACK #-} !Int
+  , accumulatedRequestCrossedEdges :: {-# UNPACK #-} !Int
+  }
+
+data ConstraintWorkspace s = ConstraintWorkspace
+  { constraintProgramWords :: !(GrowableWord32 s)
+  , constraintWalkBudget :: {-# UNPACK #-} !Int
+  }
+
+data MutableConstraintProgram = MutableConstraintProgram
+  { mutableProgramWordCount :: {-# UNPACK #-} !Int
+  , mutableProgramPieceCount :: {-# UNPACK #-} !Int
+  }
+
+-- | What one request did to the thawed mesh. A rejection is a value: the batch
+-- interpreter records it and carries on, the singleton verbs abandon the
+-- transaction on it, and both readings are lawful because an accepted request
+-- may lawfully obstruct a later one.
+data MutableConstraintOutcome
+  = MutableConstraintRejected !UndirectedEdgeId
+  | MutableConstraintAccepted !ConstraintRequestAccumulator
+
+data MutableProgramCursor = MutableProgramCursor
+  { mutableCursorAt :: !VertexId
+  , mutableCursorHeader :: {-# UNPACK #-} !Int
+  , mutableCursorWriteAt :: {-# UNPACK #-} !Int
+  , mutableCursorConflictCount :: {-# UNPACK #-} !Int
+  , mutableCursorPieceCount :: {-# UNPACK #-} !Int
+  , mutableCursorAfterOverlap :: !Bool
+  }
+
+data MutablePlanWalk
+  = MutablePlanActive !Intersection !MutableProgramCursor
+  | MutablePlanComplete !MutableConstraintProgram
+  | MutablePlanBlocked !DirectedEdgeId
+  | MutablePlanFailed !CorridorObstruction
+
+data MutableConstraintScan
+  = MutableConstraintScanBlocked !DirectedEdgeId
+  | MutableConstraintScanAdmitted !MutableConstraintProgram
+
+data ConstraintProgramAccumulator = ConstraintProgramAccumulator
+  { accumulatedProgramCursor :: {-# UNPACK #-} !Int
+  , accumulatedProgramRequest :: !ConstraintRequestAccumulator
+  }
+
+-- | Result of maximal constrained construction, including rejected requests.
+data CdtBuildResult vertex directed undirected face = CdtBuildResult
+  { cdtAcceptedBuild :: {-# UNPACK #-} !(BuildResult 'Constrained vertex directed undirected face)
+    -- ^ Canonical build result containing every admitted constraint.
+  , cdtRejectedConstraints :: !(V.Vector (Int, Int))
+    -- ^ Input-index pairs that could not be admitted.
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
diff --git a/src-build/Moonlight/Planar/Internal/Cdt/Union.hs b/src-build/Moonlight/Planar/Internal/Cdt/Union.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Cdt/Union.hs
@@ -0,0 +1,424 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Canonical constraint segments and the atomic partial union of two
+-- constrained meshes, retaining or combining their site annotations.
+module Moonlight.Planar.Internal.Cdt.Union
+  ( canonicalSegment
+  , constraintSegments
+  , unionConstrainedWith
+  , unionConstrained
+  , joinSeparatedConstrained
+  , extendConstrainedWith
+  , firstRejected
+  , completeConstraintConflicts
+  , orderedConflict
+  , crossingIsRepresented
+  ) where
+
+import Control.Monad.ST (ST)
+import qualified Data.Bifunctor as Bifunctor
+import Data.Either (isRight)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Canonical (canonicalize)
+import Moonlight.Planar.Insertion (insertAdmittedPointCombining)
+import Moonlight.Planar.Refinement (validateRefinementParameters)
+import Moonlight.Planar.Internal.Cdt.Batch
+  ( finalizeConstraintBatch
+  , interpretConstraintRequests
+  , recoverConstraints
+  )
+import Moonlight.Planar.Internal.Cdt.Build (fromDelaunay)
+import Moonlight.Planar.Internal.Cdt.Combinators (foldWhileM)
+import Moonlight.Planar.Internal.Cdt.Query (constraintEdges)
+import Moonlight.Planar.IntersectionIterator (foldCorridorBetweenPoints)
+import Moonlight.Planar.Internal.Cdt.Types
+import Moonlight.Planar.Internal.Join.Seam
+  ( executeConstrainedSeam
+  , planSeam
+  , seamExecutionBuildStats
+  , seamExecutionPublicationStats
+  , seamExecutionCachedFrontierPointReads
+  , seamExecutionLeftFaceCount
+  , seamExecutionJoinFaces
+  , seamExecutionRightFaceEvidence
+  , seamExecutionTriangulation
+  )
+import Moonlight.Planar.Internal.Join.SiteSet
+  ( SiteSet
+  , delaunayFromSiteSet
+  , siteSetAssocs
+  , siteSetFromTriangulation
+  , siteSetPoints
+  , siteSetSize
+  , siteSetUnionWith
+  )
+import Moonlight.Planar.Internal.BoxedPaged (BoxedFill (..), boxedFill)
+import Moonlight.Planar.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction))
+import Moonlight.Planar.Internal.Predicates
+  ( onClosedSegment
+  , segmentsProperlyCross
+  )
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Mutable (MutableDcel)
+import Moonlight.Planar.Internal.OperationState (OperationState)
+import Moonlight.Planar.Internal.Transaction (runTransaction)
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (Point (..), queryPointValue)
+import Moonlight.Planar.Internal.Point (QueryPoint (..))
+
+canonicalSegment :: Point -> Point -> CanonicalSegment
+canonicalSegment from to
+  | from <= to = CanonicalSegment from to
+  | otherwise = CanonicalSegment to from
+{-# INLINE canonicalSegment #-}
+
+-- | Geometry of the constraint section, deduplicated and canonically ordered.
+constraintSegments
+  :: Triangulation 'Constrained vertex directed undirected face
+  -> V.Vector (CanonicalSegment)
+constraintSegments triangulation =
+  V.fromList
+    ( Set.toAscList
+        ( Set.fromList
+            [ canonicalSegment
+                (Dcel.vertexPoint triangulation from)
+                (Dcel.vertexPoint triangulation to)
+            | edge <- constraintEdges triangulation
+            , let (from, to) = Dcel.undirectedEndpoints triangulation edge
+            ]
+        )
+    )
+
+-- | Atomic partial union of constrained meshes. Coincident sites combine
+-- their annotations before construction. Complete canonical conflict
+-- witnesses descend first; a successful branch then reaches the existing
+-- batch corridor interpreter exactly once.
+unionConstrainedWith
+  :: (annotation -> annotation -> annotation)
+  -> Triangulation 'Constrained annotation () () ()
+  -> Triangulation 'Constrained annotation () () ()
+  -> Either
+      (ConstrainedUnionError)
+      (Triangulation 'Constrained annotation () () ())
+unionConstrainedWith combine left right =
+  case NonEmpty.nonEmpty (Set.toAscList conflicts) of
+    Just witnesses -> Left (ConstraintUnionConflicts witnesses)
+    Nothing -> do
+      unconstrained <-
+        Bifunctor.first
+          (ConstraintUnionConstructionFailed . CdtBuildError)
+          (delaunayFromSiteSet unionSites)
+      requests <- traverse (segmentRequestFromHandles (vertexHandles unconstrained)) (V.toList segments)
+      recovered <-
+        Bifunctor.first ConstraintUnionConstructionFailed
+          (recoverConstraints (fromDelaunay unconstrained) (V.fromList requests))
+      case firstRejected (constraintBatchOutcomes recovered) of
+        Just blocking ->
+          Left
+            ( ConstraintUnionConstructionFailed
+                (ConstraintIntersection blocking)
+            )
+        Nothing ->
+          Bifunctor.first
+            (ConstraintUnionConstructionFailed . CdtBuildError)
+            (canonicalize (constraintBatchTriangulation recovered))
+ where
+  leftSites = siteSetFromTriangulation left
+  rightSites = siteSetFromTriangulation right
+  unionSites = siteSetUnionWith combine leftSites rightSites
+  segments =
+    V.fromList
+      ( Set.toAscList
+          ( Set.union
+              (Set.fromList (V.toList (constraintSegments left)))
+              (Set.fromList (V.toList (constraintSegments right)))
+          )
+      )
+  conflicts = completeConstraintConflicts unionSites segments
+{-# INLINE unionConstrainedWith #-}
+
+-- | Atomic partial union specialized to geometry-only constrained meshes.
+unionConstrained
+  :: Triangulation 'Constrained () () () ()
+  -> Triangulation 'Constrained () () () ()
+  -> Either
+      (ConstrainedUnionError)
+      (Triangulation 'Constrained () () () ())
+unionConstrained = unionConstrainedWith (\_ _ -> ())
+{-# INLINE unionConstrained #-}
+
+-- | Join two strictly separated constrained triangulations with the caller's
+-- left mesh resident. The right source is appended and only their common
+-- tangent corridor and its selected-face-free exterior cavity are legalized.
+-- The selector sees source-local handles on an explicit resident/incoming
+-- side; selected face handles and constraint contours are immutable barriers.
+-- Passing a selector that is always true retains the preserve-all law and its
+-- typed incompatibility when the seam requires a source-face rewrite. The
+-- result carries the exact final J component plus transport evidence only for
+-- selected incoming faces.
+-- The resident operand must have gone through 'geometryOnlyPublication'; a
+-- dense boxed payload section would make local freeze enumerate the resident
+-- world and is refused as a typed obstruction.
+--
+-- The published constraint section is exactly the source constraints union
+-- the synthetic lower and upper seam bridge paths. Those paths are certified
+-- perimeter constraints for the joined world and are subdivided locally when
+-- an edge-length law requests it; the operation does not perform a global
+-- constraint scan or canonicalization.
+--
+-- Strict separation in one admitted chart (x, or the orientation-preserving
+-- y chart used by the seam planner) proves that the two site sets have no
+-- coincident point, so annotations are copied from their source mesh and
+-- never combined.
+joinSeparatedConstrained
+  :: (ConstrainedSeamSide -> FaceId -> Bool)
+  -> RefinementParameters
+  -> Triangulation 'Constrained annotation () () ()
+  -> Triangulation 'Constrained annotation () () ()
+  -> Either
+      (ConstrainedUnionError)
+      (ConstrainedSeamResult annotation)
+joinSeparatedConstrained sourceFacePreserved parameters left right = do
+  Bifunctor.first
+    (ConstraintUnionConstructionFailed . CdtBuildError)
+    (validateRefinementParameters parameters)
+  seamPlan <- maybe (Left ConstraintUnionNotSeparated) Right (planSeam left right)
+  vertexDefault <- ensureDefaultedPayloads left right
+  seamExecution <-
+    Bifunctor.first
+      (ConstraintUnionConstructionFailed . CdtBuildError)
+      ( executeConstrainedSeam
+          sourceFacePreserved
+          parameters
+          vertexDefault
+          seamPlan
+          left
+          right
+      )
+  let published = seamExecutionTriangulation seamExecution
+  pure
+    ConstrainedSeamResult
+      { constrainedSeamResultTriangulation = published
+      , constrainedSeamLeftFaceCount = seamExecutionLeftFaceCount seamExecution
+      , constrainedSeamRightFaceEvidence = seamExecutionRightFaceEvidence seamExecution
+      , constrainedSeamJoinFaces = seamExecutionJoinFaces seamExecution
+      , constrainedSeamLeftConstraintCount = Dcel.numConstraints left
+      , constrainedSeamConstraintStats = ConstraintBatchStats 0 0 0 0 0 0
+      , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution
+      , constrainedSeamPublicationStats = seamExecutionPublicationStats seamExecution
+      , constrainedSeamCachedFrontierPointReads = seamExecutionCachedFrontierPointReads seamExecution
+      }
+
+ensureDefaultedPayloads
+  :: Triangulation 'Constrained annotation () () ()
+  -> Triangulation 'Constrained annotation () () ()
+  -> Either ConstrainedUnionError annotation
+ensureDefaultedPayloads left right =
+  case (boxedFill (triVertexData left), boxedFill (triVertexData right)) of
+    (Fill vertexDefault, Fill _) -> Right vertexDefault
+    _ -> Left ConstraintUnionRequiresGeometryOnlyPublication
+
+-- | Extend one already-resident constrained triangulation with one new
+-- constrained section. This is intentionally asymmetric: the base mesh is
+-- thawed once, extension sites are inserted into it, and only the extension's
+-- constraint section is replayed. Unlike 'unionConstrainedWith', it neither
+-- rebuilds a canonical site set nor replays base constraints, because both
+-- would erase the physical distinction between solved base and new work.
+--
+-- Incoming constraint recovery is itself the spatial conflict authority. It
+-- walks only the incoming corridors against the resident base and returns a
+-- typed intersection obstruction. Re-running the canonical all-pairs union
+-- preflight here would make a tiny extension quadratic in the base. Any
+-- structural or recovery obstruction abandons the transaction before a
+-- partially extended mesh can be published.
+extendConstrainedWith
+  :: (annotation -> annotation -> annotation)
+  -> Triangulation 'Constrained annotation () () ()
+  -> Triangulation 'Constrained annotation () () ()
+  -> Either
+      (ConstrainedUnionError)
+      (ConstrainedExtensionResult annotation () () ())
+extendConstrainedWith combine base extension = do
+    (completed, extended, buildStats) <-
+      runTransaction
+        (ConstraintUnionConstructionFailed . CdtBuildError)
+        transactionShape
+        base
+        (siteSetSize extensionSites)
+        (insertAndRecoverExtension combine extensionSites extensionSegments)
+    pure
+      ConstrainedExtensionResult
+        { constrainedExtensionConstraintBatch = finalizeConstraintBatch extended completed
+        , constrainedExtensionBuildStats = buildStats
+        }
+ where
+  extensionSites = siteSetFromTriangulation extension
+  extensionSegments = constraintSegments extension
+  transactionShape =
+    case V.uncons extensionSegments of
+      Just (segment, remaining)
+        | Dcel.numVertices base >= 200000
+        , siteSetSize extensionSites <= 128
+        , V.null remaining
+        , residentCorridorIsEmpty segment -> LocalTransaction
+      _ -> DenseTransaction
+  residentCorridorIsEmpty segment =
+    foldCorridorBetweenPoints
+      base
+      (QueryPoint (segmentStart segment))
+      (QueryPoint (segmentEnd segment))
+      (\_ _ -> Left ())
+      ()
+      == Just (Right ())
+{-# INLINE extendConstrainedWith #-}
+
+insertAndRecoverExtension
+  :: (annotation -> annotation -> annotation)
+  -> SiteSet annotation
+  -> V.Vector (CanonicalSegment)
+  -> MutableDcel s annotation () () ()
+  -> OperationState s
+  -> ST s (Either (ConstrainedUnionError) ConstraintBatchAccumulator)
+insertAndRecoverExtension combine extensionSites extensionSegments mutable operation = do
+  placed <- insertExtensionSites combine extensionSites mutable operation
+  case placed of
+    Left obstruction -> pure (Left obstruction)
+    Right handles ->
+      case traverse (segmentRequestFromHandles handles) (V.toList extensionSegments) of
+        Left obstruction -> pure (Left obstruction)
+        Right requests -> do
+          interpreted <-
+            fmap
+              (Bifunctor.first ConstraintUnionConstructionFailed)
+              (interpretConstraintRequests (V.fromList requests) mutable operation)
+          case interpreted of
+            Left obstruction -> pure (Left obstruction)
+            Right completed ->
+              case firstRejected (accumulatorOutcomes completed) of
+                Just blocking ->
+                  pure
+                    ( Left
+                        ( ConstraintUnionConstructionFailed
+                            (ConstraintIntersection blocking)
+                        )
+                    )
+                Nothing -> pure (Right completed)
+
+insertExtensionSites
+  :: forall s annotation
+   . (annotation -> annotation -> annotation)
+  -> SiteSet annotation
+  -> MutableDcel s annotation () () ()
+  -> OperationState s
+  -> ST s (Either (ConstrainedUnionError) (Map.Map (Point) VertexId))
+insertExtensionSites combine extensionSites mutable operation =
+  fmap
+    (fmap (Map.fromDistinctAscList . reverse))
+    ( foldWhileM
+        isRight
+        insertOne
+        (Right [])
+        (siteSetAssocs extensionSites)
+    )
+ where
+  insertOne
+    :: Either (ConstrainedUnionError) [(Point, VertexId)]
+    -> (QueryPoint, annotation)
+    -> ST s (Either (ConstrainedUnionError) [(Point, VertexId)])
+  insertOne rejected@(Left _) _ = pure rejected
+  insertOne (Right accumulated) (queryPoint, annotation) =
+    fmap
+      ( Bifunctor.first (ConstraintUnionConstructionFailed . CdtBuildError)
+          . fmap
+            ( \(vertex, _) ->
+                ( queryPointValue queryPoint
+                , VertexId (fromIntegral vertex)
+                ) : accumulated
+            )
+      )
+      (insertAdmittedPointCombining combine Nothing mutable operation queryPoint annotation)
+
+accumulatorOutcomes :: ConstraintBatchAccumulator -> V.Vector ConstraintOutcome
+accumulatorOutcomes = V.fromList . reverse . accumulatedConstraintOutcomes
+{-# INLINE accumulatorOutcomes #-}
+
+vertexHandles :: Triangulation mode annotation () () () -> Map.Map Point VertexId
+vertexHandles triangulation =
+  Map.fromList
+    [ ( Dcel.vertexPoint triangulation vertex
+      , vertex
+      )
+    | raw <- [0 .. Dcel.numVertices triangulation - 1]
+    , let vertex = VertexId (fromIntegral raw)
+    ]
+
+segmentRequestFromHandles
+  :: Map.Map (Point) VertexId
+  -> CanonicalSegment
+  -> Either (ConstrainedUnionError) (VertexId, VertexId)
+segmentRequestFromHandles handles segment =
+  case
+      ( Map.lookup (segmentStart segment) handles
+      , Map.lookup (segmentEnd segment) handles
+      ) of
+    (Just from, Just to) -> Right (from, to)
+    (Nothing, _) -> Left (ConstraintUnionSiteMissing (segmentStart segment))
+    (_, Nothing) -> Left (ConstraintUnionSiteMissing (segmentEnd segment))
+
+firstRejected :: V.Vector ConstraintOutcome -> Maybe UndirectedEdgeId
+firstRejected =
+  V.foldr
+    (\outcome later ->
+       case outcome of
+         ConstraintAccepted _ _ -> later
+         ConstraintRejected blocking -> Just blocking
+    )
+    Nothing
+
+completeConstraintConflicts
+  :: SiteSet annotation
+  -> V.Vector (CanonicalSegment)
+  -> Set.Set (ConstraintConflict)
+completeConstraintConflicts unionSites segments =
+  Set.fromList
+    [ orderedConflict leftSegment rightSegment
+    | leftIndex <- [0 .. V.length segments - 1]
+    , rightIndex <- [leftIndex + 1 .. V.length segments - 1]
+    , let leftSegment = segments V.! leftIndex
+    , let rightSegment = segments V.! rightIndex
+    , segmentsProperlyCross
+        (segmentStart leftSegment)
+        (segmentEnd leftSegment)
+        (segmentStart rightSegment)
+        (segmentEnd rightSegment)
+    , not (crossingIsRepresented unionSites leftSegment rightSegment)
+    ]
+
+orderedConflict
+  :: CanonicalSegment
+  -> CanonicalSegment
+  -> ConstraintConflict
+orderedConflict left right
+  | left <= right = ConstraintConflict left right
+  | otherwise = ConstraintConflict right left
+
+crossingIsRepresented
+  :: SiteSet annotation
+  -> CanonicalSegment
+  -> CanonicalSegment
+  -> Bool
+crossingIsRepresented sites first second =
+  V.any
+    (\point ->
+       onClosedSegment (segmentStart first) (segmentEnd first) point
+         && onClosedSegment (segmentStart second) (segmentEnd second) point
+    )
+    (siteSetPoints sites)
diff --git a/src-build/Moonlight/Planar/Internal/CircleSweep.hs b/src-build/Moonlight/Planar/Internal/CircleSweep.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/CircleSweep.hs
@@ -0,0 +1,970 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+module Moonlight.Planar.Internal.CircleSweep
+  ( RadiallyOrderedArena
+  , radiallyOrderArena
+  , radiallyOrderGeometry
+  , circleSweepInsert
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (HullInsertions, EdgeFlips, LegalizationMaxStack, SweepFastPoints, SweepSkippedPoints))
+
+import Control.Monad (forM_, when)
+import Control.Monad.ST (ST)
+import Data.Bits ((.&.), shiftL, shiftR, xor)
+import qualified Data.Vector.Algorithms.Intro as Intro
+import qualified Data.Vector.Algorithms.Radix as Radix
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32, Word64)
+import GHC.Float (castDoubleToWord64)
+import Moonlight.Planar.Internal.HandleDefs (DirectedEdgeId (..))
+import Moonlight.Planar.Insertion (insertExistingVertex)
+import Moonlight.Planar.Internal.DcelOperations.Hull
+  ( ReservedSweepCells
+  , SweepCellCursor
+  , SweepInsertion (..)
+  , closeOuterTurnReserved
+  , commitReservedSweepConnections
+  , fixHullConvexity
+  , initialSweepCellCursor
+  , insertOutsideHullAtEdge
+  , reserveSweepCells
+  )
+import Moonlight.Planar.Internal.DcelOperations.CandidateArena
+  ( seedGenericPairInArena
+  )
+import Moonlight.Planar.Internal.DcelOperations.Chain
+  ( connectAscendingLineArena
+  , prepareAscendingLineArena
+  )
+import Moonlight.Planar.Internal.DcelOperations.Normalize
+  ( LegalizationDrain (..)
+  , drainDenseUnconstrainedGenericLegalization
+  )
+import Moonlight.Planar.Internal.Mutable
+  ( DenseMutableDcel
+  , MutableDcel
+  , connectedCount
+  , denseMutableDcel
+  , denseMutableOwner
+  , denseFaceEdges
+  , denseReadFaceEdge
+  , denseReadNext
+  , denseReadOrigin
+  , denseReadPointX
+  , denseReadPointY
+  , denseReadPrevious
+  , directedEdgeCount
+  , faceCount
+  , halfEdgeCapacity
+  , pointCapacity
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , LegalizationArena
+  , OperationState
+  , addCounter
+  , legalizationArena
+  , maxCounter
+  , readScratch
+  , storeLegalizationArena
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.PackedIndex (noIndex, unpackIndex)
+import Moonlight.Planar.Internal.Probe (Probe (..))
+import Moonlight.Planar.Internal.Predicates (orient2dCoordinates)
+import Moonlight.Planar.Types (BuildError (..))
+
+-- | The hull is an angular index over the DCEL outer-face cycle. The cycle
+-- itself already owns hull adjacency, so left and right walking is
+-- 'readPrevious'/'readNext' on the live mesh and this record only caches what
+-- topology cannot state: the pseudo-angle of each outer edge's origin and the
+-- bucket anchors accelerating the predecessor search. An outer edge's key is
+-- its origin's @(angle, x, y)@ with the edge id as the final tie-break; the
+-- angle lives in 'hullAngleByEdge' and the coordinates are re-read from the
+-- immutable origin only when two cached angles compare exactly equal. Slots
+-- of edges that have left the outer cycle are never read again, so the cache
+-- needs no invalidation, and there is no second ring beside the authoritative
+-- one.
+data Hull s = Hull
+  { hullCenterX :: {-# UNPACK #-} !Double
+  , hullCenterY :: {-# UNPACK #-} !Double
+  , hullBucketCapacity :: {-# UNPACK #-} !Int
+  , hullAngleByEdge :: !(MUV.MVector s Double)
+  }
+
+-- | The strict state of the derived angular section. The bucket vector is
+-- mutated only to transport the section across one local hull rewrite; its
+-- extent and active cardinality are threaded as values, so the sweep does not
+-- bounce through singleton mutable cells for facts already known at descent.
+data HullIndex s = HullIndex
+  !(MUV.MVector s Word32)
+  {-# UNPACK #-} !Int
+
+data DeferredInsertion s
+  = DeferredInsertionFailure !BuildError
+  | DeferredInsertionSuccess
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !SweepCellCursor
+      !(LegalizationArena s)
+      {-# UNPACK #-} !(HullIndex s)
+
+data ClosedHullSection s = ClosedHullSection
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+  !(LegalizationArena s)
+  {-# UNPACK #-} !SweepCellCursor
+
+-- | Vertex ids after their radial keys have descended to one total order. The
+-- constructor is private: the circle sweep consumes the proof and therefore
+-- never pays to establish the same ordering twice. Coordinates remain owned
+-- by the DCEL rather than being duplicated in the sweep input.
+newtype RadiallyOrderedArena s = RadiallyOrderedArena
+  (MUV.MVector s Word32)
+
+radiallyOrderArena
+  :: MUV.MVector s (Double, Double, Double, Word32)
+  -> ST s (RadiallyOrderedArena s)
+radiallyOrderArena arena = do
+  Intro.sort arena
+  let (_, _, _, vertices) = MUV.unzip4 arena
+  pure (RadiallyOrderedArena vertices)
+{-# INLINE radiallyOrderArena #-}
+
+-- | Order the geometry-only ingress by moving only its existing vertex-id
+-- plane. Stable radix descent first orders the high 36 bits of each binary64
+-- distance. Every equal-prefix fibre is then normalized by the complete
+-- historical @(distance, x, y, vertex)@ order, recovering the discarded low
+-- 28 bits before the fibres glue into the authoritative radial order. The key
+-- coordinates remain in their immutable planes, so the sole linear scratch
+-- plane is four bytes per site rather than a second copy of every radial
+-- record.
+--
+-- Coordinate admission bounds every squared distance to a finite,
+-- non-negative binary64 value, whose unsigned word order is numeric order.
+radiallyOrderGeometry
+  :: U.Vector Double
+  -> U.Vector Double
+  -> U.Vector Double
+  -> MUV.MVector s Word32
+  -> ST s (RadiallyOrderedArena s)
+radiallyOrderGeometry distances pointXs pointYs vertices = do
+  Radix.sortBy
+    radialPassCount
+    radialBucketCount
+    (radialDistanceBucket distances)
+    vertices
+  when (MUV.length vertices > 1) $ do
+    first <- MUV.unsafeRead vertices 0
+    let !firstPrefix = geometryDistancePrefix distances first
+    (!finalFibreStart, _) <-
+      MUV.ifoldM'
+        (\(!fibreStart, !distancePrefix) relativeIndex vertex ->
+           let !nextPrefix = geometryDistancePrefix distances vertex
+               !index = relativeIndex + 1
+            in if nextPrefix == distancePrefix
+                 then pure (fibreStart, distancePrefix)
+                 else do
+                   sortGeometryPrefixFibre
+                     distances
+                     pointXs
+                     pointYs
+                     vertices
+                     fibreStart
+                     index
+                   pure (index, nextPrefix)
+        )
+        (0, firstPrefix)
+        (MUV.unsafeSlice 1 (MUV.length vertices - 1) vertices)
+    sortGeometryPrefixFibre
+      distances
+      pointXs
+      pointYs
+      vertices
+      finalFibreStart
+      (MUV.length vertices)
+  pure (RadiallyOrderedArena vertices)
+{-# INLINE radiallyOrderGeometry #-}
+
+radialPassCount :: Int
+radialPassCount = 3
+
+radialBucketCount :: Int
+radialBucketCount = 1 `shiftL` radialPassWidth
+
+radialPassWidth :: Int
+radialPassWidth = 12
+
+radialPrefixDiscardedBits :: Int
+radialPrefixDiscardedBits = 64 - radialPassCount * radialPassWidth
+
+radialBucketMask :: Word64
+radialBucketMask = fromIntegral (radialBucketCount - 1)
+
+radialDistanceBucket :: U.Vector Double -> Int -> Word32 -> Int
+radialDistanceBucket distances pass vertex =
+  fromIntegral
+    ( (castDoubleToWord64 (geometryDistance distances vertex)
+         `shiftR` (radialPrefixDiscardedBits + radialPassWidth * pass)
+      )
+        .&. radialBucketMask
+    )
+{-# INLINE radialDistanceBucket #-}
+
+geometryDistancePrefix :: U.Vector Double -> Word32 -> Word64
+geometryDistancePrefix distances vertex =
+  castDoubleToWord64 (geometryDistance distances vertex)
+    `shiftR` radialPrefixDiscardedBits
+{-# INLINE geometryDistancePrefix #-}
+
+geometryDistance :: U.Vector Double -> Word32 -> Double
+geometryDistance distances vertex = U.unsafeIndex distances (fromIntegral vertex)
+{-# INLINE geometryDistance #-}
+
+compareGeometryVertex
+  :: U.Vector Double
+  -> U.Vector Double
+  -> U.Vector Double
+  -> Word32
+  -> Word32
+  -> Ordering
+compareGeometryVertex distances pointXs pointYs left right =
+  case compare (geometryDistance distances left) (geometryDistance distances right) of
+    EQ ->
+      case compare (coordinateAt pointXs left) (coordinateAt pointXs right) of
+        EQ ->
+          case compare (coordinateAt pointYs left) (coordinateAt pointYs right) of
+            EQ -> compare left right
+            yOrder -> yOrder
+        xOrder -> xOrder
+    distanceOrder -> distanceOrder
+{-# INLINE compareGeometryVertex #-}
+
+coordinateAt :: U.Vector Double -> Word32 -> Double
+coordinateAt coordinates vertex = U.unsafeIndex coordinates (fromIntegral vertex)
+{-# INLINE coordinateAt #-}
+
+sortGeometryPrefixFibre
+  :: U.Vector Double
+  -> U.Vector Double
+  -> U.Vector Double
+  -> MUV.MVector s Word32
+  -> Int
+  -> Int
+  -> ST s ()
+sortGeometryPrefixFibre distances pointXs pointYs vertices start end =
+  when (end - start > 1) $
+    Intro.sortBy
+      (compareGeometryVertex distances pointXs pointYs)
+      (MUV.unsafeSlice start (end - start) vertices)
+{-# INLINE sortGeometryPrefixFibre #-}
+
+-- | Circle sweep over one mutable DCEL, consuming one radial vertex order.
+-- Coordinates are read from the authoritative dense DCEL. Insertions
+-- initially close only acute hull turns. One terminal Graham pass restores
+-- full convexity, so construction does not repeatedly pay for global
+-- convexity that no intermediate observer can see.
+circleSweepInsert
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> RadiallyOrderedArena s
+  -> ST s (Either BuildError Int)
+circleSweepInsert mutable operation (RadiallyOrderedArena arena) =
+  case denseMutableDcel mutable of
+    Nothing -> pure (Left CircleSweepRequiresDenseStorage)
+    Just dense -> circleSweepInsertDense dense operation arena
+{-# INLINE circleSweepInsert #-}
+
+circleSweepInsertDense
+  :: forall s vertex directed undirected face
+   . DenseMutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> MUV.MVector s Word32
+  -> ST s (Either BuildError Int)
+circleSweepInsertDense dense operation arena
+  | MUV.length arena == 0 = pure (Right 0)
+  | otherwise = do
+      residentCount <- connectedCount mutable
+      ascendingLine <-
+        if residentCount == 0
+          then prepareAscendingLineArena dense arena
+          else pure Nothing
+      case ascendingLine of
+        Just line -> connectAscendingLineArena dense operation line
+        Nothing -> circleSweepInsertArea dense operation arena
+ where
+  !mutable = denseMutableOwner dense
+{-# INLINE circleSweepInsertDense #-}
+
+circleSweepInsertArea
+  :: forall s vertex directed undirected face
+   . DenseMutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> MUV.MVector s Word32
+  -> ST s (Either BuildError Int)
+circleSweepInsertArea dense operation arena = do
+      let !ordered = MUV.length arena
+      seed <- insertSeed 0
+      case seed of
+        Left failure -> pure (Left failure)
+        Right seedCount -> do
+          faces <- faceCount mutable
+          if faces <= 1 || seedCount >= ordered
+            then pure (Right seedCount)
+            else do
+              (centerX, centerY) <- seedCentre dense
+              reservedOutcome <- reserveSweepCells dense (ordered - seedCount)
+              case reservedOutcome of
+                Left failure -> pure (Left failure)
+                Right reserved -> do
+                  builtHull <- buildHull dense operation centerX centerY
+                  case builtHull of
+                    Left failure -> pure (Left failure)
+                    Right (!hull, !initialHullIndex) -> do
+                      initialLegalizationArena <- legalizationArena operation
+                      let !initialCursor = initialSweepCellCursor reserved
+                      inserted <-
+                        insertRemaining
+                          reserved
+                          initialCursor
+                          initialLegalizationArena
+                          hull
+                          initialHullIndex
+                          ordered
+                          seedCount
+                          (0 :: Int)
+                          (0 :: Int)
+                          (0 :: Int)
+                          (0 :: Int)
+                      case inserted of
+                        Left failure -> pure (Left failure)
+                        Right (!skippedCount, !fastCount, !flips, !maxDepth, !sweepCursor, !sweepArena) -> do
+                          repaired <- fixHullConvexity reserved sweepCursor operation sweepArena
+                          case repaired of
+                            Left failure -> pure (Left failure)
+                            Right (!_closures, !terminalFlips, !terminalMaxDepth, !finalArena, !finalCursor) -> do
+                              commitReservedSweepConnections reserved finalCursor fastCount
+                              storeLegalizationArena operation finalArena
+                              -- The sweep counts its own hull insertions; its drains
+                              -- hand their flip and depth tallies up once.
+                              addCounter operation (Reported HullInsertions) fastCount
+                          -- The angular candidate leaves a point to the fallback
+                          -- whenever it lands right of, or on, the hull edge its
+                          -- own angle selected — spade's
+                          -- `is_on_right_side_or_on_line` branch, which spade's own
+                          -- source calls "very slow". Both counts ride out in
+                          -- BuildStats so the split is comparable across the two
+                          -- implementations and satisfies
+                          -- seed + fast + skipped = unique.
+                          --
+                          -- (Reported SweepFastPoints) is NOT (Reported HullInsertions)
+                          -- renamed: the latter is also charged by
+                          -- insertOutsideHull, so it counts hull-adjacent
+                          -- insertions from either path and dominates this one
+                          -- whenever a skipped point lands outside the hull. The
+                          -- two coincide only while skipped is zero, and charging
+                          -- them independently is what makes the identity a check
+                          -- rather than a restatement.
+                              addCounter operation (Reported SweepFastPoints) fastCount
+                              addCounter operation (Reported SweepSkippedPoints) skippedCount
+                              addCounter operation (Reported EdgeFlips) flips
+                              maxCounter operation (Reported LegalizationMaxStack) maxDepth
+                              addCounter operation (Reported EdgeFlips) terminalFlips
+                              maxCounter operation (Reported LegalizationMaxStack) terminalMaxDepth
+                              insertedSkipped <- insertSkipped skippedCount 0
+                              pure (seedCount <$ insertedSkipped)
+ where
+  !mutable = denseMutableOwner dense
+
+  insertSeed !index
+    | index >= MUV.length arena = pure (Right index)
+    | otherwise = do
+        raw <- MUV.unsafeRead arena index
+        result <- insertExistingVertex @'ProbeOff mutable operation (unpackIndex raw)
+        case result of
+          Left failure -> pure (Left failure)
+          Right () -> do
+            faces <- faceCount mutable
+            if faces > 1
+              then pure (Right (index + 1))
+              else insertSeed (index + 1)
+
+  insertRemaining
+    :: ReservedSweepCells s vertex directed undirected face
+    -> SweepCellCursor
+    -> LegalizationArena s
+    -> Hull s
+    -> HullIndex s
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> ST s (Either BuildError (Int, Int, Int, Int, SweepCellCursor, LegalizationArena s))
+  insertRemaining !reserved !cursor !candidateArena !hull !hullIndex !ordered !index !skippedCount !fastCount !flips !maxDepth
+    | index >= ordered = pure (Right (skippedCount, fastCount, flips, maxDepth, cursor, candidateArena))
+    | otherwise = do
+        raw <- MUV.unsafeRead arena index
+        let !vertex = unpackIndex raw
+        queryXWide <- denseReadPointX dense vertex
+        queryYWide <- denseReadPointY dense vertex
+        let !queryAngle =
+              pseudoAngle (hullCenterX hull) (hullCenterY hull) queryXWide queryYWide
+        edge <- hullCandidate dense hull hullIndex queryAngle queryXWide queryYWide
+        fromVertex <- denseReadOrigin dense edge
+        toVertex <- denseReadOrigin dense (edge `xor` 1)
+        fromX <- denseReadPointX dense fromVertex
+        fromY <- denseReadPointY dense fromVertex
+        toX <- denseReadPointX dense toVertex
+        toY <- denseReadPointY dense toVertex
+        if orient2dCoordinates fromX fromY toX toY queryXWide queryYWide == GT
+          then do
+            deferred <-
+              insertDeferred
+                reserved
+                cursor
+                dense
+                hull
+                hullIndex
+                candidateArena
+                edge
+                fromVertex
+                toVertex
+                vertex
+                queryXWide
+                queryYWide
+                queryAngle
+            case deferred of
+              DeferredInsertionFailure failure -> pure (Left failure)
+              DeferredInsertionSuccess !newFlips !newMaxDepth !nextCursor !nextArena !nextHullIndex ->
+                insertRemaining
+                  reserved
+                  nextCursor
+                  nextArena
+                  hull
+                  nextHullIndex
+                  ordered
+                  (index + 1)
+                  skippedCount
+                  (fastCount + 1)
+                  (flips + newFlips)
+                  (max maxDepth newMaxDepth)
+          else do
+            -- The radial order is consumed monotonically, so its processed
+            -- prefix is dead. Compact deferred vertices into that prefix
+            -- instead of allocating a second linear arena.
+            MUV.unsafeWrite arena skippedCount raw
+            insertRemaining reserved cursor candidateArena hull hullIndex ordered (index + 1) (skippedCount + 1) fastCount flips maxDepth
+
+  insertSkipped !count = go
+   where
+    go !index
+      | index >= count = pure (Right ())
+      | otherwise = do
+          vertex <- fromIntegral <$> MUV.unsafeRead arena index
+          inserted <- insertExistingVertex @'ProbeOff mutable operation vertex
+          case inserted of
+            Left failure -> pure (Left failure)
+            Right () -> go (index + 1)
+
+-- | The hull centre: the centroid of the first inner face, in the widened
+-- comparison format, computed exactly as @centroid@ states it.
+seedCentre
+  :: DenseMutableDcel s vertex directed undirected face
+  -> ST s (Double, Double)
+seedCentre dense = do
+  (e0, e1, e2) <- denseFaceEdges dense 1
+  o0 <- denseReadOrigin dense e0
+  o1 <- denseReadOrigin dense e1
+  o2 <- denseReadOrigin dense e2
+  x0 <- denseReadPointX dense o0
+  y0 <- denseReadPointY dense o0
+  x1 <- denseReadPointX dense o1
+  y1 <- denseReadPointY dense o1
+  x2 <- denseReadPointX dense o2
+  y2 <- denseReadPointY dense o2
+  pure (x0 + (x1 - x0) / 3 + (x2 - x0) / 3, y0 + (y1 - y0) / 3 + (y2 - y0) / 3)
+
+-- | Index the authoritative outer cycle. The seed fan is star-shaped around
+-- the first face's centroid — a collinear chain closed by its apex — so the
+-- cycle is already the angular order the predecessor search assumes; what
+-- remains is caching each edge's angle and anchoring the buckets.
+buildHull
+  :: DenseMutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Double
+  -> Double
+  -> ST s (Either BuildError (Hull s, HullIndex s))
+buildHull dense operation centerX centerY = do
+  let !mutable = denseMutableOwner dense
+  countResult <- collectOuterEdges dense operation
+  case countResult of
+    Left failure -> pure (Left failure)
+    Right count
+      | count <= 0 -> pure (Left CircleSweepHullEmpty)
+      | otherwise -> do
+          let !capacity = max (count + 4) (pointCapacity mutable + 8)
+          hullAngleByEdge <- MUV.new (halfEdgeCapacity mutable)
+          let hull =
+                Hull
+                  { hullCenterX = centerX
+                  , hullCenterY = centerY
+                  , hullBucketCapacity = capacity
+                  , hullAngleByEdge
+                  }
+          forM_ [0 .. count - 1] $ \index -> do
+            edge <- readScratch operation index
+            origin <- denseReadOrigin dense edge
+            x <- denseReadPointX dense origin
+            y <- denseReadPointY dense origin
+            MUV.unsafeWrite hullAngleByEdge edge (pseudoAngle centerX centerY x y)
+          hullIndex <- rebuildBuckets dense hull count (initialBucketCount count capacity)
+          pure (Right (hull, hullIndex))
+
+collectOuterEdges
+  :: forall s vertex directed undirected face.
+     DenseMutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> ST s (Either BuildError Int)
+collectOuterEdges dense operation = do
+  let !mutable = denseMutableOwner dense
+  start <- denseReadFaceEdge dense 0
+  if start < 0
+    then pure (Right 0)
+    else do
+      bound <- directedEdgeCount mutable
+      go (bound + 1) start start False 0
+ where
+  go
+    :: Int
+    -> Int
+    -> Int
+    -> Bool
+    -> Int
+    -> ST s (Either BuildError Int)
+  go !remaining !start !edge !seen !count
+    | remaining <= 0 =
+        pure
+          ( Left
+              ( OuterCycleDidNotTerminate
+                  (DirectedEdgeId (fromIntegral start))
+                  (DirectedEdgeId (fromIntegral edge))
+                  count
+              )
+          )
+    | seen && edge == start = pure (Right count)
+    | otherwise = do
+        writeScratch operation count edge
+        following <- denseReadNext dense edge
+        go (remaining - 1) start following True (count + 1)
+
+initialBucketCount :: Int -> Int -> Int
+initialBucketCount active capacity =
+  min capacity (nextPowerOfTwo (max 8 ((active + 1) `quot` 2)))
+
+nextPowerOfTwo :: Int -> Int
+nextPowerOfTwo requested = go 1
+ where
+  target = max 1 requested
+  go !value
+    | value >= target = value
+    | value > maxBound `quot` 2 = maxBound
+    | otherwise = go (value * 2)
+
+readAngle :: Hull s -> Int -> ST s Double
+readAngle hull edge = MUV.unsafeRead (hullAngleByEdge hull) edge
+{-# INLINE readAngle #-}
+
+rebuildBuckets
+  :: forall s vertex directed undirected face.
+     DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> Int
+  -> Int
+  -> ST s (HullIndex s)
+rebuildBuckets dense hull active requested = do
+  let !count = max 1 (min (hullBucketCapacity hull) requested)
+  buckets <- MUV.replicate count noIndex
+  start <- denseReadFaceEdge dense 0
+  let go
+        :: Int
+        -> Int
+        -> ST s ()
+      go !remaining !edge
+        | remaining <= 0 = pure ()
+        | otherwise = do
+            following <- denseReadNext dense edge
+            angle <- readAngle hull edge
+            followingAngle <- readAngle hull following
+            writeBucketSegment buckets angle followingAngle edge
+            go (remaining - 1) following
+  when (active > 0 && start >= 0) (go active start)
+  pure (HullIndex buckets active)
+
+maybeGrowBuckets :: DenseMutableDcel s vertex directed undirected face -> Hull s -> HullIndex s -> ST s (HullIndex s)
+maybeGrowBuckets dense hull hullIndex@(HullIndex buckets active) = do
+  let !current = MUV.length buckets
+  if active > 2 * current && current < hullBucketCapacity hull
+    then rebuildBuckets dense hull active (min (hullBucketCapacity hull) (2 * current))
+    else pure hullIndex
+
+bucketFor :: Int -> Double -> Int
+bucketFor count angle =
+  min (count - 1) (max 0 (floor (angle * fromIntegral count * 0.25)))
+{-# INLINE bucketFor #-}
+
+ceilingBucketFor :: Int -> Double -> Int
+ceilingBucketFor count angle =
+  ceiling (angle * fromIntegral count * 0.25) `rem` count
+{-# INLINE ceilingBucketFor #-}
+
+-- | Install the authoritative outer edge whose angular segment contains each
+-- bucket boundary in the half-open clockwise arc from @fromAngle@ to
+-- @toAngle@. Adjacency remains solely in the DCEL; this is the derived section
+-- needed to land a lookup near that ring.
+writeBucketSegment
+  :: MUV.MVector s Word32
+  -> Double
+  -> Double
+  -> Int
+  -> ST s ()
+writeBucketSegment buckets fromAngle toAngle edge = do
+  let !count = MUV.length buckets
+      !fromBucket = ceilingBucketFor count fromAngle
+      !toBucket = ceilingBucketFor count toAngle
+      !packed = fromIntegral edge
+  case compare fromBucket toBucket of
+    LT -> MUV.set (MUV.unsafeSlice fromBucket (toBucket - fromBucket) buckets) packed
+    GT -> do
+      MUV.set (MUV.unsafeSlice fromBucket (count - fromBucket) buckets) packed
+      MUV.set (MUV.unsafeSlice 0 toBucket buckets) packed
+    EQ -> pure ()
+{-# INLINE writeBucketSegment #-}
+
+-- | Whether an outer edge's key orders at or before the stated query key,
+-- settled field by field without materializing either key.
+edgeAtMostAtAngle
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> Double
+  -> Double
+  -> Double
+  -> Double
+  -> Int
+  -> ST s Bool
+edgeAtMostAtAngle dense edge angle queryAngle queryX queryY tie =
+  case compare angle queryAngle of
+    LT -> pure True
+    GT -> pure False
+    EQ -> do
+      origin <- denseReadOrigin dense edge
+      x <- denseReadPointX dense origin
+      case compare x queryX of
+        LT -> pure True
+        GT -> pure False
+        EQ -> do
+          y <- denseReadPointY dense origin
+          case compare y queryY of
+            LT -> pure True
+            GT -> pure False
+            EQ -> pure (edge <= tie)
+{-# INLINE edgeAtMostAtAngle #-}
+
+-- | Reconcile the derived angular index once after the local topology section
+-- has glued. Replacing one outer edge by two adds one active edge; every closed
+-- turn removes one. The final two edges cover the entire rewritten angular
+-- arc, so their segments descend to the bucket view in one gluing step.
+finishHullRewrite
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> HullIndex s
+  -> Int
+  -> Int
+  -> Int
+  -> ST s (HullIndex s)
+finishHullRewrite dense hull (HullIndex buckets active) leftEdge rightEdge activeDelta = do
+  leftAngle <- readAngle hull leftEdge
+  middleAngle <- readAngle hull rightEdge
+  afterRight <- denseReadNext dense rightEdge
+  rightAngle <- readAngle hull afterRight
+  writeBucketSegment buckets leftAngle middleAngle leftEdge
+  writeBucketSegment buckets middleAngle rightAngle rightEdge
+  maybeGrowBuckets dense hull (HullIndex buckets (active + activeDelta))
+{-# INLINE finishHullRewrite #-}
+
+-- | The outer edge whose key is the greatest key at or below the query: the
+-- visible candidate the sweep inserts against. Bucket anchors land the walk
+-- near the answer and the live outer cycle carries it the rest of the way.
+hullCandidate
+  :: forall s vertex directed undirected face.
+     DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> HullIndex s
+  -> Double
+  -> Double
+  -> Double
+  -> ST s Int
+hullCandidate dense hull (HullIndex buckets active) queryAngle queryX queryY = do
+  let !count = MUV.length buckets
+      !bucket = bucketFor count queryAngle
+  raw <- MUV.unsafeRead buckets bucket
+  if raw == noIndex
+    then denseReadFaceEdge dense 0
+    else adjustFromBoundary bucket (unpackIndex raw)
+ where
+  adjustFromBoundary
+    :: Int
+    -> Int
+    -> ST s Int
+  adjustFromBoundary !bucket !initial = do
+    initialAngle <- readAngle hull initial
+    initialAtMost <- cyclicAtMost bucket initial initialAngle
+    if initialAtMost
+      then advance active initial initialAngle
+      else retreat active initial
+   where
+    cyclicAtMost
+      :: Int
+      -> Int
+      -> Double
+      -> ST s Bool
+    cyclicAtMost boundaryBucket edge angle =
+      if boundaryBucket == 0 && angle > queryAngle
+        then pure True
+        else edgeAtMostAtAngle dense edge angle queryAngle queryX queryY maxBound
+
+    advance
+      :: Int
+      -> Int
+      -> Double
+      -> ST s Int
+    advance !remaining !edge !edgeAngle
+      | remaining <= 0 = pure initial
+      | otherwise = do
+          following <- denseReadNext dense edge
+          followingAngle <- readAngle hull following
+          -- Only the bucket-zero anchor may precede the query by crossing the
+          -- angular seam. Once the walk leaves that anchor, ordinary key order
+          -- is authoritative; treating every high-angle edge as below a
+          -- bucket-zero query walks straight past the answer and around the
+          -- entire ring.
+          let crossesSeam = followingAngle < edgeAngle
+              seamPermitted = bucket == 0 && edgeAngle > queryAngle
+          followingAtMost <- edgeAtMostAtAngle dense following followingAngle queryAngle queryX queryY maxBound
+          if (not crossesSeam || seamPermitted) && followingAtMost
+            then advance (remaining - 1) following followingAngle
+            else pure edge
+
+    retreat
+      :: Int
+      -> Int
+      -> ST s Int
+    retreat !remaining !edge
+      | remaining <= 0 = pure initial
+      | otherwise = do
+          previous <- denseReadPrevious dense edge
+          previousAngle <- readAngle hull previous
+          previousAtMost <- cyclicAtMost bucket previous previousAngle
+          if previousAtMost
+            then advance remaining previous previousAngle
+            else retreat (remaining - 1) previous
+
+insertDeferred
+  :: ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+  -> DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> HullIndex s
+  -> LegalizationArena s
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Double
+  -> Double
+  -> Double
+  -> ST s (DeferredInsertion s)
+insertDeferred reserved cursor dense hull hullIndex arena replacedEdge fromVertex toVertex vertex insertedX insertedY insertedAngle = do
+  inserted <-
+    insertOutsideHullAtEdge
+      reserved
+      cursor
+      arena
+      replacedEdge
+      fromVertex
+      toVertex
+      vertex
+  case inserted of
+    SweepInsertionFailure failure -> pure (DeferredInsertionFailure failure)
+    SweepInsertion firstOuterEdge lastOuterEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion ->
+      insertDeferredBetween
+        firstOuterEdge
+        lastOuterEdge
+        initialFlips
+        initialMaxDepth
+        arenaAfterInsertion
+        cursorAfterInsertion
+ where
+  insertDeferredBetween firstEdge lastEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion = do
+    -- The patch proves the new keys algebraically, so no key is rediscovered
+    -- from the mesh: replacing outer edge a->b with a->v, v->b keeps the
+    -- replaced edge's origin for the first spoke (its cached angle carries
+    -- over) and starts the last spoke at the inserted vertex, whose angle is the
+    -- one the candidate search was already given. The tie-break is each fresh
+    -- edge id itself.
+    replacedAngle <- readAngle hull replacedEdge
+    -- Both spokes join the outer cycle before either is indexed, and a bucket
+    -- rebuild during activation walks the live cycle: both angle slots must be
+    -- initialized before the first activation can read them.
+    MUV.unsafeWrite (hullAngleByEdge hull) firstEdge replacedAngle
+    MUV.unsafeWrite (hullAngleByEdge hull) lastEdge insertedAngle
+    -- Every turn this insertion closes legalizes together in one epoch. Closure
+    -- never deletes an edge and never touches the outer cycle, so which turns
+    -- close does not depend on when the interior is repaired; the fan above
+    -- keeps its own drain because its candidates are oriented against the
+    -- inserted vertex, and that star must still be intact when they are tested.
+    ClosedHullSection left closuresLeft topLeft arenaAfterLeft cursorAfterLeft <- closeLeft firstEdge 0 0 arenaAfterInsertion cursorAfterInsertion
+    ClosedHullSection right closuresRight topAll arenaAfterRight cursorAfterRight <- closeRight lastEdge 0 topLeft arenaAfterLeft cursorAfterLeft
+    let !closures = closuresLeft + closuresRight
+    nextHullIndex <- finishHullRewrite dense hull hullIndex left right (1 - closures)
+    (!flips, !maxDepth, !finalArena) <-
+      if topAll == 0
+        then pure (0, 0, arenaAfterRight)
+        else do
+          LegalizationDrain drainedFlips drainedMaxDepth () drainedArena <-
+            drainDenseUnconstrainedGenericLegalization dense arenaAfterRight topAll
+          pure (drainedFlips, drainedMaxDepth, drainedArena)
+    pure
+      ( DeferredInsertionSuccess
+          (initialFlips + flips)
+          (max initialMaxDepth maxDepth)
+          cursorAfterRight
+          finalArena
+          nextHullIndex
+      )
+
+  closeLeft !current !closures !top !sectionArena !sectionCursor = do
+    left <- denseReadPrevious dense current
+    close <- shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY left current
+    if not close
+      then pure (ClosedHullSection current closures top sectionArena sectionCursor)
+      else do
+        leftAngle <- readAngle hull left
+        (replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor left
+        -- Closing consecutive a->b, b->c into a->c keeps the first edge's
+        -- origin, so the replacement inherits its angle; the tie is the fresh
+        -- edge id. Both retired edges answer to the same left neighbour, the
+        -- edge now preceding the replacement on the outer cycle. Seeding
+        -- happens here, after the replacement's links exist.
+        MUV.unsafeWrite (hullAngleByEdge hull) replacement leftAngle
+        (nextArena, nextTop) <- seedGenericPairInArena sectionArena top left current
+        closeLeft replacement (closures + 1) nextTop nextArena nextCursor
+
+  closeRight !current !closures !top !sectionArena !sectionCursor = do
+    right <- denseReadNext dense current
+    close <- shouldCloseRightTurn dense hull insertedAngle insertedX insertedY current right
+    if not close
+      then pure (ClosedHullSection current closures top sectionArena sectionCursor)
+      else do
+        currentAngle <- readAngle hull current
+        (replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor current
+        MUV.unsafeWrite (hullAngleByEdge hull) replacement currentAngle
+        (nextArena, nextTop) <- seedGenericPairInArena sectionArena top current right
+        closeRight replacement (closures + 1) nextTop nextArena nextCursor
+{-# INLINE insertDeferred #-}
+
+-- | Test the left-hand turn where the inserted point is the target of the
+-- second edge. Adjacency is not rediscovered: 'closeLeft' obtained @first@
+-- from @previous second@ in the same local section.
+shouldCloseLeftTurn
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> Double
+  -> Double
+  -> Double
+  -> Int
+  -> Int
+  -> ST s Bool
+shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY first second = do
+  fromVertex <- denseReadOrigin dense first
+  middleVertex <- denseReadOrigin dense (first `xor` 1)
+  fromX <- denseReadPointX dense fromVertex
+  fromY <- denseReadPointY dense fromVertex
+  middleX <- denseReadPointX dense middleVertex
+  middleY <- denseReadPointY dense middleVertex
+  middleAngle <- readAngle hull second
+  -- Same-ray/acute compatibility rejects most local sections.  Settle that
+  -- cheap obstruction before paying for the exact orientation predicate; the
+  -- conjunction is unchanged, only its evaluation order is less profligate.
+  if
+    middleAngle /= insertedAngle
+      && not (acuteAtMiddle fromX fromY middleX middleY insertedX insertedY)
+    then pure False
+    else pure (orient2dCoordinates fromX fromY middleX middleY insertedX insertedY == GT)
+{-# INLINE shouldCloseLeftTurn #-}
+
+-- | The symmetric right-hand test, where the inserted point is the first
+-- edge's source. 'closeRight' obtained @second@ from @next first@, so this
+-- section likewise consumes that adjacency proof instead of reading it again.
+shouldCloseRightTurn
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Hull s
+  -> Double
+  -> Double
+  -> Double
+  -> Int
+  -> Int
+  -> ST s Bool
+shouldCloseRightTurn dense hull insertedAngle insertedX insertedY first second = do
+  middleVertex <- denseReadOrigin dense (first `xor` 1)
+  targetVertex <- denseReadOrigin dense (second `xor` 1)
+  middleX <- denseReadPointX dense middleVertex
+  middleY <- denseReadPointY dense middleVertex
+  targetX <- denseReadPointX dense targetVertex
+  targetY <- denseReadPointY dense targetVertex
+  middleAngle <- readAngle hull second
+  -- Symmetric to the left-hand descent above: compatibility first, exact
+  -- orientation only for sections that can actually glue.
+  if
+    middleAngle /= insertedAngle
+      && not (acuteAtMiddle insertedX insertedY middleX middleY targetX targetY)
+    then pure False
+    else pure (orient2dCoordinates insertedX insertedY middleX middleY targetX targetY == GT)
+{-# INLINE shouldCloseRightTurn #-}
+
+-- Spade's deferred-convexity rule is local: close the turn when the angle at
+-- the shared hull vertex is strictly below 90 degrees. Requiring the entire
+-- triangle to be acute leaves avoidable star-hull work for the terminal pass.
+acuteAtMiddle :: Double -> Double -> Double -> Double -> Double -> Double -> Bool
+acuteAtMiddle ax ay bx by cx cy =
+  let !ux = ax - bx
+      !uy = ay - by
+      !vx = cx - bx
+      !vy = cy - by
+      !dot = ux * vx + uy * vy
+      !scale = max 1 (ux * ux + uy * uy + vx * vx + vy * vy)
+   in dot > 64 * encodeFloat 1 (-52) * scale
+{-# INLINE acuteAtMiddle #-}
+
+-- Clockwise pseudo-angle in [0,4), matching the orientation of the outer-face
+-- cycle.
+pseudoAngle :: Double -> Double -> Double -> Double -> Double
+pseudoAngle centerX centerY x y
+  | norm == 0 = 0
+  | raw >= 4 = 0
+  | otherwise = raw
+ where
+  !dx = x - centerX
+  !dy = y - centerY
+  !norm = abs dx + abs dy
+  !projection = dx / norm
+  !raw = if dy > 0 then 1 + projection else 3 - projection
+{-# INLINE pseudoAngle #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/CandidateArena.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/CandidateArena.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/CandidateArena.hs
@@ -0,0 +1,99 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | The legalization candidate stack: discipline, growth, and seeding.
+module Moonlight.Planar.Internal.DcelOperations.CandidateArena
+  ( CandidateDiscipline (..)
+  , growLegalizationArena
+  , seedStarScratch
+  , seedGenericPairInArena
+  , seedGenericEdges
+  ) where
+
+import Control.Monad (forM_, when)
+import Control.Monad.ST (ST)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.OperationState
+  ( LegalizationArena (..)
+  , OperationState
+  , legalizationArena
+  , legalizationArenaLength
+  , readScratch
+  , storeLegalizationArena
+  )
+import Moonlight.Planar.Internal.PackedIndex (packIndex)
+
+-- | Every normalization epoch is homogeneous. A star epoch turns each edge so
+-- the inserted vertex is the opposite apex; a generic epoch consumes the
+-- directed edge exactly as seeded. Keeping that fact at the epoch boundary
+-- prevents every candidate from carrying and decoding a tag for a distinction
+-- that cannot vary inside the stack.
+data CandidateDiscipline
+  = StarCandidates !Int
+  | GenericCandidates
+
+-- | Seed candidates from the scratch arena, returning the new stack top.
+-- Transaction-sized preallocation covers the normal path; rare adversarial
+-- overflow grows the operation-owned vector without changing LIFO order.
+seedStarScratch :: OperationState s -> Int -> Int -> ST s Int
+seedStarScratch operation top candidateCount = do
+  initialArena <- legalizationArena operation
+  arena <- growLegalizationArena initialArena (top + candidateCount)
+  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
+  let LegalizationArena values = arena
+  forM_ [0 .. candidateCount - 1] $ \index -> do
+    edge <- readScratch operation index
+    MUV.unsafeWrite values (top + index) (packIndex edge)
+  pure (top + candidateCount)
+
+-- | Append the fixed two-edge section produced by one closed hull turn.
+-- Materializing @[left, right]@ only to count, zip and traverse it made the
+-- dominant sweep rewrite pay list traffic for an arity known by construction.
+-- The arena is explicit because a circle sweep borrows it once and glues it
+-- back to the operation once, rather than performing three reference lookups
+-- around every inserted point.
+seedGenericPairInArena
+  :: LegalizationArena s
+  -> Int
+  -> Int
+  -> Int
+  -> ST s (LegalizationArena s, Int)
+seedGenericPairInArena initialArena top left right = do
+  let !nextTop = top + 2
+  arena <- growLegalizationArena initialArena nextTop
+  let LegalizationArena values = arena
+  MUV.unsafeWrite values top (packIndex left)
+  MUV.unsafeWrite values (top + 1) (packIndex right)
+  pure (arena, nextTop)
+{-# INLINE seedGenericPairInArena #-}
+
+-- | Seed generic candidates from a list, returning the new stack top.
+seedGenericEdges :: OperationState s -> Int -> [Int] -> ST s Int
+seedGenericEdges operation top edges = do
+  initialArena <- legalizationArena operation
+  let !count = length edges
+  arena <- growLegalizationArena initialArena (top + count)
+  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
+  let LegalizationArena values = arena
+  forM_ (zip [0 ..] edges) $ \(!index, !edge) ->
+    MUV.unsafeWrite values (top + index) (packIndex edge)
+  pure (top + count)
+
+growLegalizationArena :: LegalizationArena s -> Int -> ST s (LegalizationArena s)
+growLegalizationArena arena@(LegalizationArena values) required
+  | required <= current = pure arena
+  | otherwise = growLegalizationArenaCold arena (max (required - current) (max 1 current))
+ where
+  !current = MUV.length values
+{-# INLINE growLegalizationArena #-}
+
+-- Keep allocation out of every specialized drain while retaining its inline
+-- capacity guard. Growth preserves the candidate prefix and its LIFO order.
+growLegalizationArenaCold :: LegalizationArena s -> Int -> ST s (LegalizationArena s)
+growLegalizationArenaCold (LegalizationArena values) extra =
+  LegalizationArena <$> MUV.grow values extra
+{-# NOINLINE growLegalizationArenaCold #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Chain.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Chain.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Chain.hs
@@ -0,0 +1,426 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | The degenerate dimensions: a point, a segment chain, and its promotion.
+module Moonlight.Planar.Internal.DcelOperations.Chain
+  ( setupFirstVertex
+  , setupSecondVertex
+  , splitLineEdge
+  , extendLine
+  , prepareAscendingLineArena
+  , connectAscendingLineArena
+  , collectLineChain
+  , lineToArea
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (LineSplits, LineExtensions, LineToAreaTransitions))
+
+import Control.Monad (forM_, when)
+import Control.Monad.ST (ST)
+import Data.STRef (writeSTRef)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Internal.DcelOperations.Legalize (legalizeScratch)
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.PackedIndex (unpackIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel (..)
+  , DenseMutableDcel
+  , addEdge
+  , addEdgeBlock
+  , addFaceBlock
+  , denseAddEdgeBlock
+  , denseCommitFreshConnections
+  , denseMarkFreshConnected
+  , denseMutableOwner
+  , denseReadPointX
+  , denseReadPointY
+  , denseWriteFace
+  , denseWriteFaceEdge
+  , denseLinkEdges
+  , denseWriteOrigin
+  , directedEdgeCount
+  , edgeOriginPoint
+  , ensureCellCapacity
+  , isConnected
+  , linkEdges
+  , markConnected
+  , pointAt
+  , pointCount
+  , readConstraint
+  , readNext
+  , readOrigin
+  , readPrevious
+  , readVertexOut
+  , resetEdgeData
+  , setConstraint
+  , setCycle3
+  , writeFace
+  , writeFaceEdge
+  , writeOrigin
+  , writeVertexOut
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , readScratch
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.Probe (KnownProbe)
+import Moonlight.Planar.Internal.Types (BuildError (..))
+import Moonlight.Planar.Internal.Predicates (orient2d, orient2dCoordinates)
+
+setupFirstVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+setupFirstVertex mutable vertex = markConnected mutable vertex (-1)
+
+setupSecondVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
+setupSecondVertex mutable vertex = do
+  vertices <- pointCount mutable
+  connected <- findConnected mutable vertices 0
+  case connected of
+    Left obstruction -> pure (Left obstruction)
+    Right first -> do
+      capacity <- ensureCellCapacity mutable 1 0
+      case capacity of
+        Left obstruction -> pure (Left obstruction)
+        Right () -> do
+          (edge, reverseEdgeEdge) <- addEdge mutable first vertex
+          linkEdges mutable edge reverseEdgeEdge
+          linkEdges mutable reverseEdgeEdge edge
+          writeFace mutable edge 0
+          writeFace mutable reverseEdgeEdge 0
+          writeFaceEdge mutable 0 edge
+          writeVertexOut mutable first edge
+          markConnected mutable vertex reverseEdgeEdge
+          pure (Right ())
+
+splitLineEdge :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+splitLineEdge mutable operation edge vertex = do
+  capacity <- ensureCellCapacity mutable 1 0
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> do
+      protected <- readConstraint mutable edge
+      let !reverseEdgeEdge = reverseIndex edge
+      destinationVertex <- readOrigin mutable reverseEdgeEdge
+      oldNextEdge <- readNext mutable edge
+      oldPreviousTwin <- readPrevious mutable reverseEdgeEdge
+      oldNextTwin <- readNext mutable reverseEdgeEdge
+      writeOrigin mutable reverseEdgeEdge vertex
+      (newEdge, newTwin) <- addEdge mutable vertex destinationVertex
+      writeFace mutable newEdge 0
+      writeFace mutable newTwin 0
+      if oldNextEdge == reverseEdgeEdge
+        then do
+          -- The split segment reaches a line endpoint. Its forward edge and twin
+          -- are adjacent, so the replacement is one contiguous four-edge run.
+          -- This also covers the initial two-vertex topology.
+          linkEdges mutable edge newEdge
+          linkEdges mutable newEdge newTwin
+          linkEdges mutable newTwin reverseEdgeEdge
+          linkEdges mutable reverseEdgeEdge oldNextTwin
+        else do
+          -- Replace the two occurrences independently: [edge] becomes
+          -- [edge,newEdge], while [twin] becomes [newTwin,twin].
+          linkEdges mutable edge newEdge
+          linkEdges mutable newEdge oldNextEdge
+          linkEdges mutable oldPreviousTwin newTwin
+          linkEdges mutable newTwin reverseEdgeEdge
+      writeVertexOut mutable destinationVertex newTwin
+      markConnected mutable vertex newEdge
+      -- AB became AV. The new half is a fresh slot and already carries the
+      -- default; the truncated half is a different edge in an old slot.
+      resetEdgeData mutable (edge `quot` 2)
+      when protected $ do
+        _ <- setConstraint mutable newEdge
+        pure ()
+      addCounter operation (Reported LineSplits) 1
+      pure (Right ())
+
+extendLine :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+extendLine mutable operation endpoint vertex = do
+  outgoing <- readVertexOut mutable endpoint
+  if outgoing < 0
+    then pure (Left (DegenerateLineEndpointMissingOutgoing (VertexId (fromIntegral endpoint))))
+    else do
+      capacity <- ensureCellCapacity mutable 1 0
+      case capacity of
+        Left obstruction -> pure (Left obstruction)
+        Right () -> do
+          let !incoming = reverseIndex outgoing
+          (newEdge, newTwin) <- addEdge mutable endpoint vertex
+          writeFace mutable newEdge 0
+          writeFace mutable newTwin 0
+          linkEdges mutable incoming newEdge
+          linkEdges mutable newEdge newTwin
+          linkEdges mutable newTwin outgoing
+          writeVertexOut mutable endpoint newEdge
+          markConnected mutable vertex newTwin
+          addCounter operation (Reported LineExtensions) 1
+          pure (Right ())
+
+-- | A consumed radial arena whose sites have been proved collinear and placed
+-- in ascending coordinate order. The constructor stays here with the chain
+-- topology it licenses; the sweep can only obtain the witness through
+-- 'prepareAscendingLineArena'.
+newtype AscendingLineArena s = AscendingLineArena (MUV.MVector s Word32)
+
+-- | Recognize a one-dimensional fresh section and derive its chain order from
+-- the radial order already paid for by bulk admission. The nearest radial site
+-- is a lawful pivot: distances increase monotonically on each side of it, so
+-- reversing the lower fibre and retaining the upper fibre yields total line
+-- order without another sort.
+--
+-- The third point is inspected before the remaining fold. General-position
+-- input therefore pays one exact orientation, not a speculative linear scan.
+prepareAscendingLineArena
+  :: forall s vertex directed undirected face
+   . DenseMutableDcel s vertex directed undirected face
+  -> MUV.MVector s Word32
+  -> ST s (Maybe (AscendingLineArena s))
+prepareAscendingLineArena dense radial
+  | MUV.length radial < 3 = pure Nothing
+  | otherwise = do
+      first <- MUV.unsafeRead radial 0
+      second <- MUV.unsafeRead radial 1
+      third <- MUV.unsafeRead radial 2
+      firstX <- coordinateX first
+      firstY <- coordinateY first
+      secondX <- coordinateX second
+      secondY <- coordinateY second
+      thirdX <- coordinateX third
+      thirdY <- coordinateY third
+      let !useX = firstX /= secondX
+          pointCoordinate :: Double -> Double -> Double
+          pointCoordinate x y = if useX then x else y
+          coordinateAt :: Word32 -> ST s Double
+          coordinateAt = if useX then coordinateX else coordinateY
+          !pivotCoordinate = pointCoordinate firstX firstY
+          !initialLowerCount =
+            fromEnum (pointCoordinate secondX secondY < pivotCoordinate)
+              + fromEnum (pointCoordinate thirdX thirdY < pivotCoordinate)
+      if orient2dCoordinates firstX firstY secondX secondY thirdX thirdY /= EQ
+        then pure Nothing
+        else do
+          (!collinear, !lowerCount) <-
+            MUV.foldM'
+              (\(!admitted, !count) raw ->
+                 if admitted
+                   then do
+                     x <- coordinateX raw
+                     y <- coordinateY raw
+                     pure
+                       ( orient2dCoordinates firstX firstY secondX secondY x y == EQ
+                       , count + fromEnum (pointCoordinate x y < pivotCoordinate)
+                       )
+                   else pure (False, count)
+              )
+              (True, initialLowerCount)
+              (MUV.unsafeSlice 3 (MUV.length radial - 3) radial)
+          if collinear
+            then Just <$> orderAlongLine coordinateAt pivotCoordinate lowerCount
+            else pure Nothing
+ where
+  coordinateX :: Word32 -> ST s Double
+  coordinateX = denseReadPointX dense . fromIntegral
+  coordinateY :: Word32 -> ST s Double
+  coordinateY = denseReadPointY dense . fromIntegral
+
+  orderAlongLine
+    :: (Word32 -> ST s Double)
+    -> Double
+    -> Int
+    -> ST s (AscendingLineArena s)
+  orderAlongLine coordinateAt pivotCoordinate lowerCount = do
+    ascending <- MUV.new (MUV.length radial)
+    _ <-
+      MUV.foldM'
+        (\(!lowerSeen, !upperSeen) raw -> do
+           coordinate <- coordinateAt raw
+           if coordinate < pivotCoordinate
+             then do
+               MUV.unsafeWrite ascending (lowerCount - lowerSeen - 1) raw
+               pure (lowerSeen + 1, upperSeen)
+             else do
+               MUV.unsafeWrite ascending (lowerCount + upperSeen) raw
+               pure (lowerSeen, upperSeen + 1)
+        )
+        (0, 0)
+        radial
+    pure (AscendingLineArena ascending)
+
+-- | Glue a proved ascending line as one outer-face cycle. Every site and cell
+-- is fresh, so the dense section initializes each topology slot exactly once
+-- and commits connectivity once after the local pieces agree on their shared
+-- endpoints.
+connectAscendingLineArena
+  :: DenseMutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> AscendingLineArena s
+  -> ST s (Either BuildError Int)
+connectAscendingLineArena dense operation (AscendingLineArena vertices) = do
+  let !vertexCount = MUV.length vertices
+      !segmentCount = vertexCount - 1
+      mutable = denseMutableOwner dense
+  capacity <- ensureCellCapacity mutable segmentCount 0
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> do
+      edgeBase <- denseAddEdgeBlock dense segmentCount
+      MUV.imapM_
+        (\index fromRaw -> do
+           toRaw <- MUV.unsafeRead vertices (index + 1)
+           let !forward = edgeBase + 2 * index
+               !backward = forward + 1
+               !nextForward = if index == segmentCount - 1 then backward else forward + 2
+               !nextBackward = if index == 0 then edgeBase else backward - 2
+           denseWriteOrigin dense forward (unpackIndex fromRaw)
+           denseWriteOrigin dense backward (unpackIndex toRaw)
+           denseLinkEdges dense forward nextForward
+           denseLinkEdges dense backward nextBackward
+           denseWriteFace dense forward 0
+           denseWriteFace dense backward 0
+        )
+        (MUV.unsafeSlice 0 segmentCount vertices)
+      MUV.imapM_
+        (\index raw ->
+           denseMarkFreshConnected
+             dense
+             (unpackIndex raw)
+             ( if index < segmentCount
+                 then edgeBase + 2 * index
+                 else edgeBase + 2 * segmentCount - 1
+             )
+        )
+        vertices
+      denseCommitFreshConnections dense vertexCount
+      denseWriteFaceEdge dense 0 edgeBase
+      addCounter operation (Reported LineExtensions) (max 0 (vertexCount - 2))
+      pure (Right vertexCount)
+
+collectLineChain :: forall s vertex directed undirected face. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either BuildError Int)
+collectLineChain mutable operation = do
+  halfEdges <- directedEdgeCount mutable
+  turn <- findTurn halfEdges 0
+  case turn of
+    Left obstruction -> pure (Left obstruction)
+    Right incoming -> do
+      let !first = reverseIndex incoming
+          go :: Int -> Int -> ST s (Either BuildError Int)
+          go !count !edge = do
+            writeScratch operation count edge
+            edgeNext <- readNext mutable edge
+            if edgeNext == reverseIndex edge
+              then pure (Right (count + 1))
+              else go (count + 1) edgeNext
+      go 0 first
+ where
+  findTurn halfEdges !edge
+    | edge >= halfEdges = pure (Left (DegenerateLineEndpointTurnMissing halfEdges))
+    | otherwise = do
+        edgeNext <- readNext mutable edge
+        if edgeNext == reverseIndex edge then pure (Right edge) else findTurn halfEdges (edge + 1)
+
+lineToArea :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> ST s (Either BuildError ())
+lineToArea mutable operation vertex = do
+  collected <- collectLineChain mutable operation
+  case collected of
+    Left obstruction -> pure (Left obstruction)
+    Right segmentCount -> lineToAreaCollected @p mutable operation vertex segmentCount
+
+lineToAreaCollected :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+lineToAreaCollected mutable@MutableDcel{mdLastFace} operation vertex segmentCount = do
+  capacity <- ensureCellCapacity mutable (segmentCount + 1) segmentCount
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> lineToAreaWithCapacity
+ where
+  lineToAreaWithCapacity :: ST s (Either BuildError ())
+  lineToAreaWithCapacity = do
+   firstSegment <- readScratch operation 0
+   lastSegment <- readScratch operation (segmentCount - 1)
+   firstPoint <- edgeOriginPoint mutable firstSegment
+   lastPoint <- edgeOriginPoint mutable (reverseIndex lastSegment)
+   insertedPoint <- pointAt mutable vertex
+   when (orient2d firstPoint lastPoint insertedPoint == LT) $ reverseScratchDirections operation segmentCount
+   spokeBase <- addEdgeBlock mutable (segmentCount + 1)
+   faceBase <- addFaceBlock mutable segmentCount
+   forM_ [0 .. segmentCount] $ \index -> do
+    chainVertex <-
+      if index < segmentCount
+        then readScratch operation index >>= readOrigin mutable
+        else readScratch operation (segmentCount - 1) >>= readOrigin mutable . reverseIndex
+    let !forward = spokeBase + 2 * index
+        !backward = forward + 1
+    writeOrigin mutable forward chainVertex
+    writeOrigin mutable backward vertex
+   forM_ [0 .. segmentCount - 1] $ \index -> do
+    segment <- readScratch operation index
+    let !face = faceBase + index
+        !nextSpoke = spokeBase + 2 * (index + 1)
+        !previousSpoke = spokeBase + 2 * index + 1
+    setCycle3 mutable face segment nextSpoke previousSpoke
+   lastInnerSegment <- readScratch operation (segmentCount - 1)
+   let !outerStart = reverseIndex lastInnerSegment
+       !firstOuterSpoke = spokeBase
+       !lastOuterSpoke = spokeBase + 2 * segmentCount + 1
+   linkOuterTwins mutable operation segmentCount
+   firstInnerSegment <- readScratch operation 0
+   linkEdges mutable (reverseIndex firstInnerSegment) firstOuterSpoke
+   linkEdges mutable firstOuterSpoke lastOuterSpoke
+   linkEdges mutable lastOuterSpoke outerStart
+   writeFace mutable firstOuterSpoke 0
+   writeFace mutable lastOuterSpoke 0
+   writeFaceEdge mutable 0 outerStart
+   forM_ [0 .. segmentCount - 1] $ \index -> do
+    segment <- readScratch operation index
+    chainVertex <- readOrigin mutable segment
+    writeVertexOut mutable chainVertex segment
+   finalVertex <- readOrigin mutable (reverseIndex lastInnerSegment)
+   writeVertexOut mutable finalVertex (spokeBase + 2 * segmentCount)
+   markConnected mutable vertex (spokeBase + 1)
+   writeSTRef mdLastFace faceBase
+   addCounter operation (Reported LineToAreaTransitions) 1
+   legalizeScratch @p mutable operation vertex segmentCount
+   pure (Right ())
+
+  linkOuterTwins :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> ST s ()
+  linkOuterTwins target ops count =
+    let go !index
+          | index <= 0 = pure ()
+          | otherwise = do
+              right <- readScratch ops index
+              left <- readScratch ops (index - 1)
+              linkEdges target (reverseIndex right) (reverseIndex left)
+              writeFace target (reverseIndex right) 0
+              go (index - 1)
+     in do
+          go (count - 1)
+          first <- readScratch ops 0
+          writeFace target (reverseIndex first) 0
+
+reverseScratchDirections :: OperationState s -> Int -> ST s ()
+reverseScratchDirections operation count = do
+  forM_ [0 .. count `quot` 2 - 1] $ \left -> do
+    let !right = count - 1 - left
+    leftEdge <- readScratch operation left
+    rightEdge <- readScratch operation right
+    writeScratch operation left (reverseIndex rightEdge)
+    writeScratch operation right (reverseIndex leftEdge)
+  when (odd count) $ do
+    let !middle = count `quot` 2
+    edge <- readScratch operation middle
+    writeScratch operation middle (reverseIndex edge)
+
+findConnected :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Either BuildError Int)
+findConnected mutable limit !vertex
+  | vertex >= limit = pure (Left (DegenerateLineConnectedVertexMissing limit))
+  | otherwise = do
+      connected <- isConnected mutable vertex
+      if connected then pure (Right vertex) else findConnected mutable limit (vertex + 1)
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRewrite.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRewrite.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRewrite.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | The flip rewrite itself, on a quadrilateral.
+module Moonlight.Planar.Internal.DcelOperations.FlipRewrite
+  ( flipEdge
+  , applyFlip
+  ) where
+
+import Control.Monad (unless)
+import Control.Monad.ST (ST)
+import Moonlight.Planar.Internal.HandleDefs (UndirectedEdgeId (..))
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel
+  , MutableTopology (..)
+  , payloadsPristine
+  , readConstraint
+  , readFace
+  , readNext
+  , readOrigin
+  , readPrevious
+  , resetEdgeData
+  , resetFaceData
+  )
+import Moonlight.Planar.Internal.Types (BuildError (ConstrainedEdgeFlipRefused))
+
+flipEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
+flipEdge mutable edge = do
+  protected <- readConstraint mutable edge
+  if protected
+    then pure (Left (ConstrainedEdgeFlipRefused (UndirectedEdgeId (fromIntegral (edge `quot` 2)))))
+    else do
+      let !twin = reverseIndex edge
+      edgeNext <- readNext mutable edge
+      edgePrevious <- readPrevious mutable edge
+      twinNext <- readNext mutable twin
+      twinPrevious <- readPrevious mutable twin
+      leftFace <- readFace mutable edge
+      rightFace <- readFace mutable twin
+      a <- readOrigin mutable edge
+      b <- readOrigin mutable twin
+      c <- readOrigin mutable edgePrevious
+      d <- readOrigin mutable twinPrevious
+      applyFlip mutable edge twin edgeNext edgePrevious twinNext twinPrevious leftFace rightFace a b c d
+      pure (Right ())
+
+-- | The rewrite itself, from a quadrilateral the caller already holds. The
+-- decision that licenses a flip reads the same two half-edge records the
+-- rewrite consumes, so the drain hands its neighbourhood straight here rather
+-- than making 'flipEdge' fetch it a second time; 'flipEdge' is that fetch, for
+-- callers arriving with nothing but an index.
+applyFlip
+  :: MutableTopology mutable
+  => mutable s vertex directed undirected face
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s ()
+applyFlip topology edge twin edgeNext edgePrevious twinNext twinPrevious leftFace rightFace a b c d = do
+  let !mutable = topologyOwner topology
+  -- Rewrite only fields whose denotation changes. Re-stating both complete
+  -- triangles writes the two unchanged face labels and face anchors, then
+  -- needlessly redirects the two new diagonal endpoints even though each
+  -- already owns another live outgoing edge. The local quadrilateral proof
+  -- above names every changed adjacency, so the minimal section is exact.
+  topologyWriteOrigin topology edge c
+  topologyWriteOrigin topology twin d
+  topologyWriteNext topology edgeNext edge
+  topologyWritePrevious topology edgeNext twinPrevious
+  topologyWriteNext topology edge twinPrevious
+  topologyWritePrevious topology edge edgeNext
+  topologyWriteNext topology twinPrevious edgeNext
+  topologyWritePrevious topology twinPrevious edge
+  topologyWriteFace topology twinPrevious leftFace
+  topologyWriteFaceEdge topology leftFace edge
+  topologyWriteNext topology twinNext twin
+  topologyWritePrevious topology twinNext edgePrevious
+  topologyWriteNext topology twin edgePrevious
+  topologyWritePrevious topology twin twinNext
+  topologyWriteNext topology edgePrevious twinNext
+  topologyWritePrevious topology edgePrevious twin
+  topologyWriteFace topology edgePrevious rightFace
+  topologyWriteFaceEdge topology rightFace twin
+  -- The diagonal AB is gone and CD stands in its slot; both triangles have
+  -- swapped a corner. Three elements changed what they are, so three labels go.
+  -- Each reset carries the same test; a site doing several states it once.
+  unless (payloadsPristine mutable) $ do
+    resetEdgeData mutable (edge `quot` 2)
+    resetFaceData mutable leftFace
+    resetFaceData mutable rightFace
+  topologyWriteVertexOut topology a twinNext
+  topologyWriteVertexOut topology b edgeNext
+{-# INLINE applyFlip #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRule.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRule.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/FlipRule.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | The firing condition of the flip rule, and the laws restricting it.
+module Moonlight.Planar.Internal.DcelOperations.FlipRule
+  ( LegalizationLaw (..)
+  , diagonalFires
+  , illegalDiagonal
+  , isFlippableEdge
+  ) where
+
+import Control.Monad.ST (ST)
+import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel
+  , readConstraint
+  , readFace
+  , readOrigin
+  , readPointX
+  , readPointY
+  , readPrevious
+  )
+import Moonlight.Planar.Internal.Predicates
+  ( inCircleCoordinates
+  , orient2dCoordinates
+  )
+
+-- | Whether the flip rule may fire on a quadrilateral @a b c d@ under a law,
+-- where @ab@ is the diagonal and @c@, @d@ the apexes opposite it. The
+-- coordinates arrive raw, in the order the mutable accessors carry.
+--
+-- Both interpreters reach the same incircle rule with different eligibility
+-- proofs. 'ValidMesh' arrives from two consistently oriented incident faces;
+-- 'CavityRepair' restricts firing to the new fan through its floor witness.
+-- The drain reaches this with the quadrilateral already in hand; the firing
+-- condition itself has one owner.
+diagonalFires
+  :: LegalizationLaw
+  -> Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Bool
+diagonalFires law ax ay bx by cx cy dx dy =
+  case law of
+    -- A valid triangulation already proves the two incident triangles are
+    -- consistently oriented. If their union is concave, the opposite apex is
+    -- outside the first triangle's circumcircle and the in-circle rule refuses
+    -- the flip; re-running two exact orientation predicates merely re-proves
+    -- that premise for every candidate. This is the same lawful section used
+    -- by Spade's removal legalizer: the diagonal rule alone decides.
+    ValidMesh -> illegalDiagonal ax ay bx by cx cy dx dy
+    CavityRepair _ -> illegalDiagonal ax ay bx by cx cy dx dy
+    SeamPreserving _ -> illegalDiagonal ax ay bx by cx cy dx dy
+{-# INLINE diagonalFires #-}
+
+-- | Whether a diagonal is locally illegal — the firing condition of the flip
+-- rule @drainLegalization@ normalizes under, and the step at which its
+-- potential strictly decreases.
+--
+-- Lift the quadrilateral to @z = x² + y²@ and the in-circle sign is the
+-- orientation of the lifted tetrahedron: 'GT' says the fourth point is below
+-- the plane of the other three, so the current diagonal spans a fold the flip
+-- pushes downward. 'EQ' says the four lifted points are coplanar and the flip
+-- changes nothing about the surface — so the ordering on diagonal keys stands
+-- in as the potential, and the rule fires only downward in it. That tie-break
+-- is not a convention for picking among equals; it is what stops a cocircular
+-- quadrilateral from flipping forever.
+illegalDiagonal
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double -> Bool
+illegalDiagonal ax ay bx by cx cy dx dy =
+  case inCircleCoordinates ax ay bx by cx cy dx dy of
+    GT -> True
+    LT -> False
+    EQ -> orderedPair (cx, cy) (dx, dy) < orderedPair (ax, ay) (bx, by)
+{-# INLINE illegalDiagonal #-}
+
+isFlippableEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
+isFlippableEdge mutable edge = do
+  protected <- readConstraint mutable edge
+  if protected
+    then pure False
+    else do
+      let !reverseEdgeEdge = reverseIndex edge
+      leftFace <- readFace mutable edge
+      rightFace <- readFace mutable reverseEdgeEdge
+      if leftFace == 0 || rightFace == 0
+        then pure False
+        else do
+          edgePrevious <- readPrevious mutable edge
+          reverseEdgePrevious <- readPrevious mutable reverseEdgeEdge
+          a <- readOrigin mutable edge
+          b <- readOrigin mutable reverseEdgeEdge
+          c <- readOrigin mutable edgePrevious
+          d <- readOrigin mutable reverseEdgePrevious
+          ax <- readPointX mutable a
+          ay <- readPointY mutable a
+          bx <- readPointX mutable b
+          by <- readPointY mutable b
+          cx <- readPointX mutable c
+          cy <- readPointY mutable c
+          dx <- readPointX mutable d
+          dy <- readPointY mutable d
+          pure
+            ( orient2dCoordinates cx cy dx dy bx by == GT
+                && orient2dCoordinates dx dy cx cy ax ay == GT
+            )
+{-# INLINE isFlippableEdge #-}
+
+-- | The rewrite strategy: which candidates the flip rule is allowed to fire
+-- on. All laws leave the normal form alone — they restrict where the rule may
+-- be applied, never what a legal diagonal is.
+--
+-- 'ValidMesh' legalizes a mesh that is already a triangulation. There a
+-- non-convex quadrilateral's diagonal is necessarily locally Delaunay, so the
+-- incircle rule itself declines the flip; separately re-running two exact
+-- orientation predicates would only re-prove the incident-face premise.
+--
+-- 'CavityRepair' legalizes the fan that fills a removed vertex's hole, which is
+-- not yet a triangulation. That implication fails on an inverted quadrilateral,
+-- and the inverted one is exactly the one that must flip, so the short-circuit
+-- becomes a refusal to perform the repair. The carried index is the undirected
+-- edge count taken before the fan was built: every edge at or above it was
+-- appended by the fan, and only those may flip, so a transiently inverted
+-- neighbourhood cannot flip the cavity's border away.
+--
+-- 'SeamPreserving' admits a candidate exactly when neither incident target
+-- face handle is selected by the caller. Face handles survive a flip, so this
+-- protection is inductive while legalization propagates through the complete
+-- unselected seam cavity.
+data LegalizationLaw
+  = ValidMesh
+  | CavityRepair {-# UNPACK #-} !Int
+  | SeamPreserving !(Int -> Bool)
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Hull.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Hull.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Hull.hs
@@ -0,0 +1,590 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | Growth outside the hull: visible ranges, turn closure, and convexity repair.
+module Moonlight.Planar.Internal.DcelOperations.Hull
+  ( ReservedSweepCells
+  , SweepCellCursor
+  , SweepInsertion (..)
+  , reserveSweepCells
+  , initialSweepCellCursor
+  , commitReservedSweepConnections
+  , insertOutsideHull
+  , insertOutsideHullAtEdge
+  , closeOuterTurn
+  , closeOuterTurnReserved
+  , fixHullConvexity
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (HullInsertions))
+
+import Control.Monad (forM_, when)
+import Control.Monad.ST (ST)
+import Data.STRef (writeSTRef)
+import Moonlight.Planar.Internal.HandleDefs (DirectedEdgeId (..), FaceId (..))
+import Moonlight.Planar.Internal.DcelOperations.CandidateArena
+  ( seedGenericPairInArena
+  )
+import Moonlight.Planar.Internal.DcelOperations.Legalize
+  ( legalizeScratch
+  , legalizeDenseStarEdgeInArena
+  )
+import Moonlight.Planar.Internal.DcelOperations.Normalize
+  ( LegalizationDrain (..)
+  , drainDenseUnconstrainedGenericLegalization
+  )
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( DenseMutableDcel
+  , MutableDcel (..)
+  , addEdge
+  , addEdgeBlock
+  , addFaceBlock
+  , directedEdgeCount
+  , denseCommitFreshConnections
+  , denseInitializeUnconstrainedEdgeBlock
+  , denseLinkEdges
+  , denseMarkFreshConnected
+  , denseMutableOwner
+  , denseReadFace
+  , denseReadFaceEdge
+  , denseReadNext
+  , denseReadOrigin
+  , denseReadPointX
+  , denseReadPointY
+  , denseReadPrevious
+  , denseSetCycle3
+  , denseWriteFace
+  , denseWriteFaceEdge
+  , denseWriteNext
+  , denseWriteOrigin
+  , denseWritePrevious
+  , denseWriteVertexOut
+  , ensureCellCapacity
+  , faceCount
+  , linkEdges
+  , markConnected
+  , pointAt
+  , readFace
+  , readNext
+  , readOrigin
+  , readPointX
+  , readPointY
+  , readPrevious
+  , setCycle3
+  , writeFace
+  , writeFaceEdge
+  , writeNext
+  , writeOrigin
+  , writePrevious
+  , writeVertexOut
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , LegalizationArena
+  , OperationState
+  , addCounter
+  , readScratch
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.Probe (KnownProbe)
+import Moonlight.Planar.Types (BuildError (..))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Internal.Predicates (orient2dCoordinates)
+
+-- | Proof that the remaining circle-sweep program fits the particular mutable
+-- arena carried here. The constructor is private: only 'reserveSweepCells' can
+-- pair a DCEL with the one-time capacity check. Carrying the DCEL inside the
+-- witness prevents a proof for one mutable mesh from being applied to another
+-- mesh that happens to share the same @ST@ region.
+data ReservedSweepCells s vertex directed undirected face = ReservedSweepCells
+  !(DenseMutableDcel s vertex directed undirected face)
+  {-# UNPACK #-} !SweepCellCursor
+
+-- | The uncommitted directed-edge and face cardinalities of one reserved
+-- sweep section. Its constructor is private: only the reservation can mint the
+-- initial cursor, and only reserved topology rewrites can advance it.
+data SweepCellCursor = SweepCellCursor
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+
+-- | The outer edges glued by one direct sweep insertion and the normalization
+-- work discharged while its star still existed. The sweep accumulates these
+-- strict metrics and charges the operation counters once, instead of mutating
+-- diagnostic cells once per point.
+data SweepInsertion s
+  = SweepInsertionFailure !BuildError
+  | SweepInsertion
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      !(LegalizationArena s)
+      {-# UNPACK #-} !SweepCellCursor
+
+-- | Discharge the monotone circle-sweep allocation budget once. Every
+-- remaining point can contribute at most three undirected edges and two faces
+-- to a planar triangulation. The seed already occupies its own cells, so this
+-- bound is stated only over the points the sweep has not connected yet.
+reserveSweepCells
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError (ReservedSweepCells s vertex directed undirected face))
+reserveSweepCells dense remainingPoints = do
+  let !mutable = denseMutableOwner dense
+  halfEdges <- directedEdgeCount mutable
+  faces <- faceCount mutable
+  capacity <- ensureCellCapacity mutable (3 * remainingPoints) (2 * remainingPoints)
+  pure (ReservedSweepCells dense (SweepCellCursor halfEdges faces) <$ capacity)
+
+initialSweepCellCursor
+  :: ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+initialSweepCellCursor (ReservedSweepCells _ cursor) = cursor
+{-# INLINE initialSweepCellCursor #-}
+
+-- | Commit the cardinality of the fresh connectivity sections materialized by
+-- direct sweep insertions. Their per-vertex bits and outgoing edges are already
+-- present; this is the one global descent step before any skipped point enters
+-- the ordinary insertion interpreter.
+commitReservedSweepConnections
+  :: ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+  -> Int
+  -> ST s ()
+commitReservedSweepConnections (ReservedSweepCells dense _) (SweepCellCursor halfEdges faces) inserted = do
+  denseCommitFreshConnections dense inserted
+  let !MutableDcel{mdHalfCount, mdFaceCount, mdLastFace} = denseMutableOwner dense
+  writeSTRef mdHalfCount halfEdges
+  writeSTRef mdFaceCount faces
+  when (inserted > 0 && faces > 1) (writeSTRef mdLastFace (faces - 1))
+{-# INLINE commitReservedSweepConnections #-}
+
+insertOutsideHull :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+insertOutsideHull mutable operation start vertex = do
+  query <- pointAt mutable vertex
+  visibleStart <- visibleOuter mutable start query
+  if not visibleStart
+    then pure (Left (HullStartNotVisible (DirectedEdgeId (fromIntegral start))))
+    else do
+      halfEdges <- directedEdgeCount mutable
+      left <- expandPrevious halfEdges start start query
+      right <- expandNext halfEdges start left query
+      collected <- collectOuterChain mutable operation left right
+      case collected of
+        Left obstruction -> pure (Left obstruction)
+        Right chainCount -> do
+          inserted <- insertOutsideHullCollected @p mutable operation vertex chainCount
+          case inserted of
+            Left obstruction -> pure (Left obstruction)
+            Right _ -> do
+              -- The regular insertion path owns its own hull-insertion count; the sweep
+              -- path through 'insertOutsideHullBetween' counts its own instead.
+              addCounter operation (Reported HullInsertions) 1
+              pure (Right ())
+ where
+  expandPrevious
+    :: Int
+    -> Int
+    -> Int
+    -> Point
+    -> ST s Int
+  expandPrevious !bound !stopAt !current !query
+    | bound <= 0 = pure current
+    | otherwise = do
+        candidate <- readPrevious mutable current
+        if candidate == stopAt
+          then pure current
+          else do
+            visible <- visibleOuter mutable candidate query
+            if visible then expandPrevious (bound - 1) stopAt candidate query else pure current
+
+  expandNext
+    :: Int
+    -> Int
+    -> Int
+    -> Point
+    -> ST s Int
+  expandNext !bound !current !left !query
+    | bound <= 0 = pure current
+    | otherwise = do
+        candidate <- readNext mutable current
+        if candidate == left
+          then pure current
+          else do
+            visible <- visibleOuter mutable candidate query
+            if visible then expandNext (bound - 1) candidate left query else pure current
+
+-- | Replace one selected outer edge @a->b@ by @a->v, v->b@ and materialize
+-- the covered triangle. This is the circle sweep's actual local section: the
+-- ordinary outside-hull path continues to own arbitrary visible ranges, while
+-- the sweep no longer collects a singleton range into shared scratch merely to
+-- rediscover its first and last edge.
+insertOutsideHullAtEdge
+  :: ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+  -> LegalizationArena s
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s (SweepInsertion s)
+insertOutsideHullAtEdge (ReservedSweepCells dense _) (SweepCellCursor nextHalf nextFace) arena outerEdge from to vertex = do
+  incident <- denseReadFace dense outerEdge
+  if incident /= 0
+    then
+      pure
+        ( SweepInsertionFailure
+            ( OuterRangeContainsInnerEdge
+                (DirectedEdgeId (fromIntegral outerEdge))
+                (FaceId (fromIntegral incident))
+            )
+        )
+    else do
+      oldPrevious <- denseReadPrevious dense outerEdge
+      oldNext <- denseReadNext dense outerEdge
+      denseInitializeUnconstrainedEdgeBlock dense nextHalf 2
+      let !edgeBase = nextHalf
+          !face = nextFace
+          !firstOuterSpoke = edgeBase
+          !firstInnerSpoke = edgeBase + 1
+          !secondInnerSpoke = edgeBase + 2
+          !lastOuterSpoke = edgeBase + 3
+      denseWriteOrigin dense firstOuterSpoke from
+      denseWriteOrigin dense firstInnerSpoke vertex
+      denseWriteOrigin dense secondInnerSpoke to
+      denseWriteOrigin dense lastOuterSpoke vertex
+      denseSetCycle3 dense face outerEdge secondInnerSpoke firstInnerSpoke
+      denseWriteFace dense firstOuterSpoke 0
+      denseWriteFace dense lastOuterSpoke 0
+      denseLinkEdges dense oldPrevious firstOuterSpoke
+      denseLinkEdges dense firstOuterSpoke lastOuterSpoke
+      denseLinkEdges dense lastOuterSpoke oldNext
+      denseWriteFaceEdge dense 0 firstOuterSpoke
+      denseWriteVertexOut dense from outerEdge
+      denseWriteVertexOut dense to secondInnerSpoke
+      denseMarkFreshConnected dense vertex lastOuterSpoke
+      LegalizationDrain flips maxDepth () finalArena <-
+        legalizeDenseStarEdgeInArena dense arena vertex outerEdge
+      pure
+        ( SweepInsertion
+            firstOuterSpoke
+            lastOuterSpoke
+            flips
+            maxDepth
+            finalArena
+            (SweepCellCursor (nextHalf + 4) (nextFace + 1))
+        )
+{-# INLINE insertOutsideHullAtEdge #-}
+
+collectOuterChain
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Either BuildError Int)
+collectOuterChain mutable operation left right = do
+  halfEdges <- directedEdgeCount mutable
+  go (halfEdges + 1) left 0
+ where
+  go
+    :: Int
+    -> Int
+    -> Int
+    -> ST s (Either BuildError Int)
+  go !remaining !current !count
+    | remaining <= 0 =
+        pure
+          ( Left
+              ( OuterRangeDidNotTerminate
+                  (DirectedEdgeId (fromIntegral left))
+                  (DirectedEdgeId (fromIntegral right))
+                  count
+              )
+          )
+    | otherwise = do
+        incident <- readFace mutable current
+        if incident /= 0
+          then
+            pure
+              ( Left
+                  ( OuterRangeContainsInnerEdge
+                      (DirectedEdgeId (fromIntegral current))
+                      (FaceId (fromIntegral incident))
+                  )
+              )
+          else do
+            writeScratch operation count current
+            if current == right
+              then pure (Right (count + 1))
+              else readNext mutable current >>= \following -> go (remaining - 1) following (count + 1)
+
+insertOutsideHullCollected
+  :: forall p s vertex directed undirected face
+   . KnownProbe p
+  => MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Either BuildError (Int, Int))
+insertOutsideHullCollected mutable@MutableDcel{mdLastFace} operation vertex chainCount = do
+  capacity <- ensureCellCapacity mutable (chainCount + 1) chainCount
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> Right <$> insertOutsideHullWithCapacity
+ where
+  insertOutsideHullWithCapacity = do
+    left <- readScratch operation 0
+    right <- readScratch operation (chainCount - 1)
+    oldPrevious <- readPrevious mutable left
+    oldNext <- readNext mutable right
+    edgeBase <- addEdgeBlock mutable (chainCount + 1)
+    faceBase <- addFaceBlock mutable chainCount
+    forM_ [0 .. chainCount] $ \index -> do
+      chainVertex <-
+        if index == 0
+          then readScratch operation 0 >>= readOrigin mutable
+          else readScratch operation (index - 1) >>= readOrigin mutable . reverseIndex
+      let !forward = edgeBase + 2 * index
+          !backward = forward + 1
+      writeOrigin mutable forward chainVertex
+      writeOrigin mutable backward vertex
+    forM_ [0 .. chainCount - 1] $ \index -> do
+      outerEdge <- readScratch operation index
+      let !face = faceBase + index
+          !nextSpoke = edgeBase + 2 * (index + 1)
+          !previousSpoke = edgeBase + 2 * index + 1
+      setCycle3 mutable face outerEdge nextSpoke previousSpoke
+    let !firstOuterSpoke = edgeBase
+        !lastOuterSpoke = edgeBase + 2 * chainCount + 1
+    writeFace mutable firstOuterSpoke 0
+    writeFace mutable lastOuterSpoke 0
+    linkEdges mutable oldPrevious firstOuterSpoke
+    linkEdges mutable firstOuterSpoke lastOuterSpoke
+    linkEdges mutable lastOuterSpoke oldNext
+    writeFaceEdge mutable 0 firstOuterSpoke
+    forM_ [0 .. chainCount - 1] $ \index -> do
+      chainEdge <- readScratch operation index
+      chainVertex <- readOrigin mutable chainEdge
+      writeVertexOut mutable chainVertex chainEdge
+    lastChain <- readScratch operation (chainCount - 1)
+    lastVertex <- readOrigin mutable (reverseIndex lastChain)
+    writeVertexOut mutable lastVertex (edgeBase + 2 * chainCount)
+    markConnected mutable vertex lastOuterSpoke
+    writeSTRef mdLastFace faceBase
+    legalizeScratch @p mutable operation vertex chainCount
+    pure (firstOuterSpoke, lastOuterSpoke)
+
+-- | Replace two consecutive outer edges @a->b, b->c@ by @a->c@ and
+-- materialize the triangle they bound. The returned edge is the new outer
+-- diagonal. This is the sole turn-closing primitive used both by deferred
+-- circle sweep and the final Graham repair. Topology mutation only: the
+-- caller owns the legalization epoch, and seeds the two closed edges itself
+-- once the replacement's links are in place.
+closeOuterTurn
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError Int)
+closeOuterTurn mutable first = do
+  capacity <- ensureCellCapacity mutable 1 1
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> Right <$> closeOuterTurnWithCapacity mutable first
+{-# INLINE closeOuterTurn #-}
+
+-- | The circle-sweep form of 'closeOuterTurn'. Its capacity obstruction was
+-- discharged by 'reserveSweepCells'; topology mutation is otherwise identical
+-- to the checked public-internal operation used by joins.
+closeOuterTurnReserved
+  :: ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+  -> Int
+  -> ST s (Int, SweepCellCursor)
+closeOuterTurnReserved
+  (ReservedSweepCells dense _)
+  (SweepCellCursor nextHalf nextFace)
+  first = do
+    denseInitializeUnconstrainedEdgeBlock dense nextHalf 1
+    replacement <- closeOuterTurnDenseAt dense nextHalf nextFace first
+    pure (replacement, SweepCellCursor (nextHalf + 2) (nextFace + 1))
+{-# INLINE closeOuterTurnReserved #-}
+
+-- | Materialize one turn closure at cells already owned by the caller's
+-- allocation section. Ordinary edits obtain those cells from the checked
+-- allocator; the reserved sweep obtains them from its immutable cursor.
+closeOuterTurnDenseAt
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Int
+closeOuterTurnDenseAt dense edgeBase newFace first = do
+  second <- denseReadNext dense first
+  oldPrevious <- denseReadPrevious dense first
+  oldNext <- denseReadNext dense second
+  from <- denseReadOrigin dense first
+  to <- denseReadOrigin dense (reverseIndex second)
+  let !outer = edgeBase
+      !inner = edgeBase + 1
+  denseWriteOrigin dense outer from
+  denseWriteOrigin dense inner to
+  denseWriteFace dense outer 0
+  -- @first -> second@ is the authoritative outer adjacency that licensed this
+  -- closure. Preserve those two already-correct cells and write only the six
+  -- changed links plus the new face section.
+  denseWriteNext dense second inner
+  denseWritePrevious dense inner second
+  denseWriteNext dense inner first
+  denseWritePrevious dense first inner
+  denseWriteFace dense first newFace
+  denseWriteFace dense second newFace
+  denseWriteFace dense inner newFace
+  denseWriteFaceEdge dense newFace first
+  denseLinkEdges dense oldPrevious outer
+  denseLinkEdges dense outer oldNext
+  denseWriteFaceEdge dense 0 outer
+  denseWriteVertexOut dense from outer
+  denseWriteVertexOut dense to inner
+  pure outer
+{-# INLINE closeOuterTurnDenseAt #-}
+
+closeOuterTurnWithCapacity
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s Int
+closeOuterTurnWithCapacity mutable first = do
+  second <- readNext mutable first
+  oldPrevious <- readPrevious mutable first
+  oldNext <- readNext mutable second
+  from <- readOrigin mutable first
+  to <- readOrigin mutable (reverseIndex second)
+  (outer, inner) <- addEdge mutable from to
+  newFace <- addFaceBlock mutable 1
+  writeFace mutable outer 0
+  writeNext mutable second inner
+  writePrevious mutable inner second
+  writeNext mutable inner first
+  writePrevious mutable first inner
+  writeFace mutable first newFace
+  writeFace mutable second newFace
+  writeFace mutable inner newFace
+  writeFaceEdge mutable newFace first
+  linkEdges mutable oldPrevious outer
+  linkEdges mutable outer oldNext
+  writeFaceEdge mutable 0 outer
+  writeVertexOut mutable from outer
+  writeVertexOut mutable to inner
+  pure outer
+{-# INLINE closeOuterTurnWithCapacity #-}
+
+-- | Close every remaining left turn of the star-shaped sweep hull in one
+-- Graham-style pass. Each closure strictly decreases the outer-edge count, so
+-- the pass is linear in the visited hull plus the local Delaunay legalization
+-- work it causes. Turn closures are seeded into one shared epoch as they
+-- happen and drained once at the end: closure never deletes an edge and never
+-- touches the outer cycle's legality, so which turns close does not depend on
+-- when the interior is repaired. The pass counts its own closures and returns
+-- the drain's tallies; nothing is reported behind its back.
+fixHullConvexity
+  :: forall s vertex directed undirected face.
+     ReservedSweepCells s vertex directed undirected face
+  -> SweepCellCursor
+  -> OperationState s
+  -> LegalizationArena s
+  -> ST s (Either BuildError (Int, Int, Int, LegalizationArena s, SweepCellCursor))
+fixHullConvexity reserved@(ReservedSweepCells dense _) initialCursor operation initialArena = do
+  start <- denseReadFaceEdge dense 0
+  if start < 0
+    then pure (Right (0, 0, 0, initialArena, initialCursor))
+    else do
+      walked <- walk start start 0 0 0 0 initialArena initialCursor
+      case walked of
+        Left obstruction -> pure (Left obstruction)
+        Right (top, closures, seededArena, finalCursor) -> do
+          LegalizationDrain flips maxDepth () finalArena <-
+            drainDenseUnconstrainedGenericLegalization dense seededArena top
+          pure (Right (closures, flips, maxDepth, finalArena, finalCursor))
+ where
+  walk
+    :: Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> Int
+    -> LegalizationArena s
+    -> SweepCellCursor
+    -> ST s (Either BuildError (Int, Int, LegalizationArena s, SweepCellCursor))
+  walk !start !current !stackSize !steps !top !closures !arena cursor@(SweepCellCursor halfEdges _) = do
+    if steps > halfEdges + 2
+      then
+        pure
+          ( Left
+              ( OuterCycleDidNotTerminate
+                  (DirectedEdgeId (fromIntegral start))
+                  (DirectedEdgeId (fromIntegral current))
+                  steps
+              )
+          )
+      else do
+        following <- denseReadNext dense current
+        writeScratch operation stackSize current
+        reduction <- reduce (stackSize + 1) top closures arena cursor
+        case reduction of
+          Left obstruction -> pure (Left obstruction)
+          Right (reduced, nextTop, nextClosures, nextArena, nextCursor) -> do
+            finished <-
+              if reduced < 2
+                then pure False
+                else (== following) <$> readScratch operation 1
+            if finished
+              then pure (Right (nextTop, nextClosures, nextArena, nextCursor))
+              else walk start following reduced (steps + 1) nextTop nextClosures nextArena nextCursor
+
+  reduce
+    :: Int
+    -> Int
+    -> Int
+    -> LegalizationArena s
+    -> SweepCellCursor
+    -> ST s (Either BuildError (Int, Int, Int, LegalizationArena s, SweepCellCursor))
+  reduce !count !top !closures !arena !cursor
+    | count < 2 = pure (Right (count, top, closures, arena, cursor))
+    | otherwise = do
+        first <- readScratch operation (count - 2)
+        second <- readScratch operation (count - 1)
+        fromVertex <- denseReadOrigin dense first
+        middleVertex <- denseReadOrigin dense (reverseIndex first)
+        targetVertex <- denseReadOrigin dense (reverseIndex second)
+        fromX <- denseReadPointX dense fromVertex
+        fromY <- denseReadPointY dense fromVertex
+        middleX <- denseReadPointX dense middleVertex
+        middleY <- denseReadPointY dense middleVertex
+        targetX <- denseReadPointX dense targetVertex
+        targetY <- denseReadPointY dense targetVertex
+        if orient2dCoordinates fromX fromY middleX middleY targetX targetY == GT
+          then do
+            (replacement, nextCursor) <- closeOuterTurnReserved reserved cursor first
+            writeScratch operation (count - 2) replacement
+            (nextArena, nextTop) <- seedGenericPairInArena arena top first second
+            reduce (count - 1) nextTop (closures + 1) nextArena nextCursor
+          else pure (Right (count, top, closures, arena, cursor))
+
+visibleOuter :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s Bool
+visibleOuter mutable edge query = do
+  fromVertex <- readOrigin mutable edge
+  toVertex <- readOrigin mutable (reverseIndex edge)
+  case query of
+    Point queryX queryY -> do
+      fromX <- readPointX mutable fromVertex
+      fromY <- readPointY mutable fromVertex
+      toX <- readPointX mutable toVertex
+      toY <- readPointY mutable toVertex
+      pure (orient2dCoordinates fromX fromY toX toY queryX queryY == GT)
+{-# INLINE visibleOuter #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Legalize.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Legalize.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Legalize.hs
@@ -0,0 +1,166 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | The seeding entry points that drive one legalization epoch.
+module Moonlight.Planar.Internal.DcelOperations.Legalize
+  ( legalizeScratch
+  , legalizeStarEdge
+  , legalizeDenseStarEdgeInArena
+  , legalizeEdges
+  , legalizeEdgesPinned
+  , legalizeCavityFanScratch
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (EdgeFlips, LegalizationMaxStack))
+
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.Foldable (traverse_)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.DcelOperations.CandidateArena
+  ( CandidateDiscipline (..)
+  , growLegalizationArena
+  , seedGenericEdges
+  , seedStarScratch
+  )
+import Moonlight.Planar.Internal.DcelOperations.FlipRule
+  ( LegalizationLaw (..)
+  )
+import Moonlight.Planar.Internal.DcelOperations.Normalize
+  ( LegalizationDrain
+  , drainDenseUnconstrainedStarLegalization
+  , drainLegalization
+  )
+import Moonlight.Planar.Internal.Mutable
+  ( DenseMutableDcel
+  , MutableDcel
+  , MutableTopology
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , LegalizationArena (..)
+  , OperationState
+  , addCounter
+  , legalizationArena
+  , legalizationArenaLength
+  , maxCounter
+  , readScratch
+  , storeLegalizationArena
+  )
+import Moonlight.Planar.Internal.PackedIndex (packIndex)
+import Moonlight.Planar.Internal.Probe (KnownProbe, Probe (..))
+
+legalizeEdges :: MutableDcel s vertex directed undirected face -> OperationState s -> [Int] -> ST s ()
+legalizeEdges mutable operation initial = do
+  top <- seedGenericEdges operation 0 initial
+  (flips, maxDepth) <- drainLegalization @'ProbeOff mutable operation top GenericCandidates ValidMesh
+  addCounter operation (Reported EdgeFlips) flips
+  maxCounter operation (Reported LegalizationMaxStack) maxDepth
+
+-- | Legalize the new seam and the source-prefix cavity whose current incident
+-- face handles are both unprotected. The selector is evaluated dynamically as
+-- flips propagate; it therefore admits the complete local exterior cavity
+-- rather than only the zipper's first seeds. The seam owner certifies the
+-- resulting cavity boundary after immutable publication.
+legalizeEdgesPinned
+  :: forall s vertex directed undirected face.
+     (Int -> Bool)
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> [Int]
+  -> ST s ()
+legalizeEdgesPinned targetFaceProtected mutable operation initial = do
+  top <- seedGenericEdges operation 0 initial
+  (flips, maxDepth) <-
+    drainLegalization
+      @'ProbeOff
+      mutable
+      operation
+      top
+      GenericCandidates
+      (SeamPreserving targetFaceProtected)
+  addCounter operation (Reported EdgeFlips) flips
+  maxCounter operation (Reported LegalizationMaxStack) maxDepth
+
+-- | Repair the fan that fills a removed vertex's hole, draining cavity
+-- candidates already written into the operation-owned scratch section. The
+-- fan is a valid combinatorial filling but not yet a triangulation — a link
+-- polygon that is non-convex at the fan origin yields an inverted triangle —
+-- so this drain carries 'CavityRepair' rather than the insertion law: it
+-- flips on the incircle determinant alone, and only the fan's own edges may
+-- flip. Removal discovers and constructs the cavity inside that same
+-- transaction; materializing a list merely to seed the legalization arena
+-- would duplicate the local program.
+legalizeCavityFanScratch
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> Int
+  -> ST s ()
+legalizeCavityFanScratch mutable operation cavityFloor scratchOffset candidateCount = do
+  initialArena <- legalizationArena operation
+  arena <- growLegalizationArena initialArena candidateCount
+  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
+  let LegalizationArena values = arena
+  traverse_
+    (\index -> do
+       edge <- readScratch operation (scratchOffset + index)
+       MUV.unsafeWrite values index (packIndex edge)
+    )
+    [0 .. candidateCount - 1]
+  (flips, maxDepth) <-
+    drainLegalization
+      @'ProbeOff
+      mutable
+      operation
+      candidateCount
+      GenericCandidates
+      (CavityRepair cavityFloor)
+  addCounter operation (Reported EdgeFlips) flips
+  maxCounter operation (Reported LegalizationMaxStack) maxDepth
+
+legalizeScratch :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
+legalizeScratch mutable operation vertex candidateCount = do
+  top <- seedStarScratch operation 0 candidateCount
+  (flips, maxDepth) <- drainLegalization @p mutable operation top (StarCandidates vertex) ValidMesh
+  addCounter operation (Reported EdgeFlips) flips
+  maxCounter operation (Reported LegalizationMaxStack) maxDepth
+
+-- | Normalize one newly covered outer edge against the inserted star vertex.
+-- The circle sweep always covers exactly one edge; routing that singleton
+-- through the shared scratch section merely materializes a one-element range
+-- before immediately copying it into this arena. This is the same star-law
+-- section written at its actual arity.
+legalizeStarEdge
+  :: forall p mutable s vertex directed undirected face
+   . (KnownProbe p, MutableTopology mutable)
+  => mutable s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Int, Int)
+legalizeStarEdge mutable operation vertex edge = do
+  arena <- legalizationArena operation
+  let LegalizationArena values = arena
+  MUV.unsafeWrite values 0 (packIndex edge)
+  drainLegalization @p mutable operation 1 (StarCandidates vertex) ValidMesh
+{-# INLINE legalizeStarEdge #-}
+
+-- | The monomorphic fresh-build interpreter for the singleton star epoch,
+-- consuming and returning the sweep's borrowed candidate section.
+legalizeDenseStarEdgeInArena
+  :: DenseMutableDcel s vertex directed undirected face
+  -> LegalizationArena s
+  -> Int
+  -> Int
+  -> ST s (LegalizationDrain s ())
+legalizeDenseStarEdgeInArena dense arena vertex edge = do
+  let LegalizationArena values = arena
+  MUV.unsafeWrite values 0 (packIndex edge)
+  drainDenseUnconstrainedStarLegalization dense arena 1 vertex
+{-# NOINLINE legalizeDenseStarEdgeInArena #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Normalize.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Normalize.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Normalize.hs
@@ -0,0 +1,379 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | The normalization procedure of the flip rewrite system.
+module Moonlight.Planar.Internal.DcelOperations.Normalize
+  ( drainLegalization
+  , LegalizationDrain (..)
+  , drainDenseUnconstrainedStarLegalization
+  , drainDenseUnconstrainedGenericLegalization
+  ) where
+
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.Bits (shiftR)
+import Data.STRef (readSTRef)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Internal.DcelOperations.CandidateArena
+  ( CandidateDiscipline (..)
+  , growLegalizationArena
+  )
+import Moonlight.Planar.Internal.DcelOperations.FlipRewrite (applyFlip)
+import Moonlight.Planar.Internal.DcelOperations.FlipRule
+  ( LegalizationLaw (..)
+  , diagonalFires
+  , illegalDiagonal
+  )
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( DenseMutableDcel
+  , MutableDcel (..)
+  , MutableTopology (..)
+  )
+import Moonlight.Planar.Internal.OperationState
+  (DiagnosticMetric (..),  Counter (..)
+  , LegalizationArena (..)
+  , OperationState
+  , legalizationArena
+  , legalizationArenaLength
+  , storeLegalizationArena
+  )
+import Moonlight.Planar.Internal.PackedIndex (packIndex)
+import Moonlight.Planar.Internal.Probe
+  ( KnownProbe (..)
+  , Probe (..)
+  , ProbeCounter
+  )
+
+-- | The authoritative result of one normalization section. The arena is part
+-- of the result because an adversarial frontier may grow it; callers that
+-- borrow the section for several local rewrites can therefore compose those
+-- rewrites without bouncing through the operation's reference between them.
+data LegalizationDrain s counter = LegalizationDrain
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+  !counter
+  !(LegalizationArena s)
+
+-- | The normalization procedure of a confluent terminating rewrite system, and
+-- one canonical legalization engine because a normalization procedure is what
+-- it is.
+--
+-- The objects are the triangulations of a fixed point set. The single rule is
+-- the Lawson flip: a locally illegal diagonal is replaced by the other
+-- diagonal of its quadrilateral. A normal form is a mesh with no illegal
+-- diagonal left to fire on.
+--
+-- /Termination/ is by the lifted-paraboloid potential. Send each point to
+-- @(x, y, x² + y²)@ and read a triangulation as a piecewise-linear surface
+-- over the point set; @illegalDiagonal@ is exactly the test that the flip
+-- lowers that surface, so every rewrite strictly decreases it, and a finite
+-- point set has finitely many triangulations. Exact cocircularity is the one
+-- case where the surface does not move — the four lifted points are coplanar
+-- and both diagonals give the same surface — so there the potential is the
+-- diagonal's own key order, and the rule fires only downward in it. Without
+-- that tie-break a cocircular quadrilateral flips forever.
+--
+-- /Confluence/ is Delaunay's theorem, in its strong form: a triangulation with
+-- no locally illegal diagonal is globally Delaunay. Local normality is thus
+-- global normality, the normal form is unique, and every rewrite order reaches
+-- it. That is what licenses the arena below to be a LIFO stack rather than a
+-- priority queue, and it is why callers may seed it in whatever order is
+-- cheapest to produce — the fan first, or the hull turns first, or both
+-- interleaved — without any of them changing the mesh that comes out.
+--
+-- All callers differ only in how they seed the arena; topology mutation and
+-- propagation have exactly one owner.
+-- The stack top, the maximum top, and the flip count are strict loop
+-- variables, returned once when the drain finishes — the mesh reports nothing
+-- per candidate, and 'applyFlip' reports nothing at all. The phantom
+-- 'KnownProbe' parameter counts popped candidates for the instrumented lane
+-- and is erased everywhere else.
+--
+-- A popped candidate is read once. Turning it against the star vertex, judging
+-- it, and rewriting it are three questions about the same two half-edge
+-- records, and the apex the turn looks for is the apex the judgement needs, so
+-- one pass over the quadrilateral answers all three.
+drainLegalization
+  :: forall p mutable s vertex directed undirected face
+   . (KnownProbe p, MutableTopology mutable)
+  => mutable s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> CandidateDiscipline
+  -> LegalizationLaw
+  -> ST s (Int, Int)
+drainLegalization topology operation seededTop discipline law = do
+  let !mutable = topologyOwner topology
+  -- No constraint can appear during a drain, so a mesh holding none at entry
+  -- never needs the per-candidate protection read.
+  constrained <- readSTRef (mdConstraintCount mutable)
+  initialArena <- legalizationArena operation
+  LegalizationDrain flips maxTop candidates finalArena <-
+    drainLegalizationInArena
+      @p
+      topology
+      initialArena
+      (constrained /= 0)
+      seededTop
+      discipline
+      law
+  when (legalizationArenaLength finalArena /= legalizationArenaLength initialArena) $
+    storeLegalizationArena operation finalArena
+  probeCharge @p operation (Diagnostic LegalizationCandidates) candidates
+  pure (flips, maxTop)
+{-# INLINE drainLegalization #-}
+
+-- | Interpret the single normalization law in an explicitly borrowed arena.
+-- The boolean is the already-established constraint obstruction: a fresh
+-- unconstrained build passes 'False', while the general entry derives it once
+-- from the mutable owner above.
+drainLegalizationInArena
+  :: forall p mutable s vertex directed undirected face
+   . (KnownProbe p, MutableTopology mutable)
+  => mutable s vertex directed undirected face
+  -> LegalizationArena s
+  -> Bool
+  -> Int
+  -> CandidateDiscipline
+  -> LegalizationLaw
+  -> ST s (LegalizationDrain s (ProbeCounter p))
+drainLegalizationInArena topology (LegalizationArena initialArena) guarded seededTop discipline law = do
+  let -- The cavity-repair floor pins the fan border. Ordinary legalization
+      -- pins nothing; seam legalization instead uses its incident-face
+      -- selector below, so both copied and appended pairs obey the same
+      -- protection law.
+      !floorPair = case law of
+        ValidMesh -> 0
+        CavityRepair floorEdge -> floorEdge
+        SeamPreserving _ -> 0
+
+      eligible rawEdge = do
+        unconstrained <-
+          if guarded
+            then not <$> topologyReadConstraint topology rawEdge
+            else pure True
+        if not unconstrained
+          then pure False
+          else
+            case law of
+              SeamPreserving targetFaceProtected -> do
+                leftFace <- topologyReadFace topology rawEdge
+                rightFace <- topologyReadFace topology (reverseIndex rawEdge)
+                pure
+                  ( not (targetFaceProtected leftFace)
+                      && not (targetFaceProtected rightFace)
+                  )
+              _ -> pure (rawEdge `shiftR` 1 >= floorPair)
+
+      loopStar
+        :: Int
+        -> Double
+        -> Double
+        -> MUV.MVector s Word32
+        -> Int
+        -> Int
+        -> Int
+        -> ProbeCounter p
+        -> ST s (Int, Int, ProbeCounter p, MUV.MVector s Word32)
+      loopStar !starVertex !starX !starY !arena !top !maxTop !flips !candidates
+        | top <= 0 = pure (flips, maxTop, candidates, arena)
+        | otherwise = do
+            let !nextTop = top - 1
+            packedWord <- MUV.unsafeRead arena nextTop
+            let !rawEdge = fromIntegral packedWord :: Int
+            mayFire <- eligible rawEdge
+            if not mayFire
+              then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
+              else do
+                let !rawTwin = reverseIndex rawEdge
+                rawFace <- topologyReadFace topology rawEdge
+                rawTwinFace <- topologyReadFace topology rawTwin
+                if rawFace == 0 || rawTwinFace == 0
+                  then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
+                  else do
+                    rawBefore <- topologyReadPrevious topology rawEdge
+                    rawTwinBefore <- topologyReadPrevious topology rawTwin
+                    rawTwinApex <- topologyReadOrigin topology rawTwinBefore
+                    a <- topologyReadOrigin topology rawEdge
+                    b <- topologyReadOrigin topology rawTwin
+                    ax <- topologyReadPointX topology a
+                    ay <- topologyReadPointY topology a
+                    bx <- topologyReadPointX topology b
+                    by <- topologyReadPointY topology b
+                    dx <- topologyReadPointX topology rawTwinApex
+                    dy <- topologyReadPointY topology rawTwinApex
+                    -- A star candidate is oriented against the inserted apex,
+                    -- exactly the premise used by the ordinary insertion
+                    -- legalizer. The two convexity predicates in the generic
+                    -- valid-mesh law merely re-prove that premise for every
+                    -- pop; the in-circle rule alone owns this section.
+                    if not (illegalDiagonal ax ay bx by starX starY dx dy)
+                      then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
+                      else do
+                        -- Star seeding is directional: the inserted vertex is
+                        -- the previous-origin apex of every candidate. A flip
+                        -- preserves that proof for precisely these two
+                        -- directed neighbours, so neither an undirected tag nor
+                        -- a rediscovery read belongs in this epoch.
+                        rawNext <- topologyReadNext topology rawEdge
+                        rawTwinNext <- topologyReadNext topology rawTwin
+                        applyFlip
+                          topology
+                          rawEdge
+                          rawTwin
+                          rawNext
+                          rawBefore
+                          rawTwinNext
+                          rawTwinBefore
+                          rawFace
+                          rawTwinFace
+                          a
+                          b
+                          starVertex
+                          rawTwinApex
+                        let !addedTop = nextTop + 2
+                        grown <- growValues arena addedTop
+                        MUV.unsafeWrite grown nextTop (packIndex rawTwinBefore)
+                        MUV.unsafeWrite grown (nextTop + 1) (packIndex rawTwinNext)
+                        loopStar
+                          starVertex
+                          starX
+                          starY
+                          grown
+                          addedTop
+                          (max maxTop addedTop)
+                          (flips + 1)
+                          (probeBump @p candidates)
+
+      loopGeneric
+        :: MUV.MVector s Word32
+        -> Int
+        -> Int
+        -> Int
+        -> ProbeCounter p
+        -> ST s (Int, Int, ProbeCounter p, MUV.MVector s Word32)
+      loopGeneric !arena !top !maxTop !flips !candidates
+        | top <= 0 = pure (flips, maxTop, candidates, arena)
+        | otherwise = do
+            let !nextTop = top - 1
+            packedWord <- MUV.unsafeRead arena nextTop
+            let !edge = fromIntegral packedWord :: Int
+            mayFire <- eligible edge
+            if not mayFire
+              then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
+              else do
+                let !twin = reverseIndex edge
+                leftFace <- topologyReadFace topology edge
+                rightFace <- topologyReadFace topology twin
+                if leftFace == 0 || rightFace == 0
+                  then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
+                  else do
+                    edgePrevious <- topologyReadPrevious topology edge
+                    twinPrevious <- topologyReadPrevious topology twin
+                    c <- topologyReadOrigin topology edgePrevious
+                    d <- topologyReadOrigin topology twinPrevious
+                    a <- topologyReadOrigin topology edge
+                    b <- topologyReadOrigin topology twin
+                    ax <- topologyReadPointX topology a
+                    ay <- topologyReadPointY topology a
+                    bx <- topologyReadPointX topology b
+                    by <- topologyReadPointY topology b
+                    cx <- topologyReadPointX topology c
+                    cy <- topologyReadPointY topology c
+                    dx <- topologyReadPointX topology d
+                    dy <- topologyReadPointY topology d
+                    if not (diagonalFires law ax ay bx by cx cy dx dy)
+                      then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
+                      else do
+                        edgeNext <- topologyReadNext topology edge
+                        twinNext <- topologyReadNext topology twin
+                        applyFlip
+                          topology
+                          edge
+                          twin
+                          edgeNext
+                          edgePrevious
+                          twinNext
+                          twinPrevious
+                          leftFace
+                          rightFace
+                          a
+                          b
+                          c
+                          d
+                        let !addedTop = nextTop + 4
+                        grown <- growValues arena addedTop
+                        MUV.unsafeWrite grown nextTop (packIndex edgeNext)
+                        MUV.unsafeWrite grown (nextTop + 1) (packIndex edgePrevious)
+                        MUV.unsafeWrite grown (nextTop + 2) (packIndex twinNext)
+                        MUV.unsafeWrite grown (nextTop + 3) (packIndex twinPrevious)
+                        loopGeneric
+                          grown
+                          addedTop
+                          (max maxTop addedTop)
+                          (flips + 1)
+                          (probeBump @p candidates)
+  drained <-
+    case discipline of
+      StarCandidates starVertex -> do
+        starX <- topologyReadPointX topology starVertex
+        starY <- topologyReadPointY topology starVertex
+        loopStar starVertex starX starY initialArena seededTop seededTop 0 (probeZero @p)
+      GenericCandidates ->
+        loopGeneric initialArena seededTop seededTop 0 (probeZero @p)
+  let (!flips, !maxTop, !candidates, !finalArena) = drained
+  pure (LegalizationDrain flips maxTop candidates (LegalizationArena finalArena))
+ where
+  growValues :: MUV.MVector s Word32 -> Int -> ST s (MUV.MVector s Word32)
+  growValues values required = do
+    LegalizationArena grown <-
+      growLegalizationArena (LegalizationArena values) required
+    pure grown
+{-# INLINE drainLegalizationInArena #-}
+
+-- | The dense, fresh-build interpreter for a star epoch. Fresh construction
+-- proves the absence of constrained edges; the borrowed arena is returned so
+-- the surrounding sweep can glue several epochs before restoring operation
+-- ownership.
+drainDenseUnconstrainedStarLegalization
+  :: DenseMutableDcel s vertex directed undirected face
+  -> LegalizationArena s
+  -> Int
+  -> Int
+  -> ST s (LegalizationDrain s ())
+drainDenseUnconstrainedStarLegalization dense arena top starVertex =
+  drainLegalizationInArena
+    @'ProbeOff
+    dense
+    arena
+    False
+    top
+    (StarCandidates starVertex)
+    ValidMesh
+{-# INLINE drainDenseUnconstrainedStarLegalization #-}
+
+-- | The matching dense interpreter for a generic legalization epoch. Keeping
+-- both monomorphic boundaries out of the circle-sweep worker prevents the
+-- normalizer and flip rewrite from being copied into every insertion
+-- continuation; both still descend through 'drainLegalizationInArena'.
+drainDenseUnconstrainedGenericLegalization
+  :: DenseMutableDcel s vertex directed undirected face
+  -> LegalizationArena s
+  -> Int
+  -> ST s (LegalizationDrain s ())
+drainDenseUnconstrainedGenericLegalization dense arena top =
+  drainLegalizationInArena
+    @'ProbeOff
+    dense
+    arena
+    False
+    top
+    GenericCandidates
+    ValidMesh
+{-# INLINE drainDenseUnconstrainedGenericLegalization #-}
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Subdivide.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Subdivide.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Subdivide.hs
@@ -0,0 +1,286 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Insertion into an existing element: face split and edge split.
+module Moonlight.Planar.Internal.DcelOperations.Subdivide
+  ( insertIntoFace
+  , insertOnEdge
+  , splitBoundaryEdgeWithLegalization
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (FaceSplits, InteriorEdgeSplits, BoundaryEdgeSplits))
+
+import Control.Monad (unless, void, when)
+import Control.Monad.ST (ST)
+import Moonlight.Planar.Internal.HandleDefs (UndirectedEdgeId (..))
+import Moonlight.Planar.Internal.DcelOperations.Legalize (legalizeScratch)
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel
+  , addEdgeBlock
+  , addFaceBlock
+  , ensureCellCapacity
+  , faceEdges
+  , linkEdges
+  , markConnected
+  , payloadsPristine
+  , readConstraint
+  , readFace
+  , readNext
+  , readOrigin
+  , readPrevious
+  , resetEdgeData
+  , resetFaceData
+  , setConstraint
+  , setCycle3
+  , writeFace
+  , writeFaceEdge
+  , writeOrigin
+  , writeVertexOut
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.Probe (KnownProbe)
+import Moonlight.Planar.Internal.Types (BuildError (..))
+
+insertIntoFace :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+insertIntoFace mutable operation face vertex = do
+  capacity <- ensureCellCapacity mutable 3 2
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> do
+      insertIntoFaceWithCapacity @p mutable operation face vertex
+      pure (Right ())
+
+insertIntoFaceWithCapacity :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
+insertIntoFaceWithCapacity mutable operation face vertex = do
+  (eAB, eBC, eCA) <- faceEdges mutable face
+  a <- readOrigin mutable eAB
+  b <- readOrigin mutable eBC
+  c <- readOrigin mutable eCA
+  edgeBase <- addEdgeBlock mutable 3
+  let !eAV = edgeBase
+      !eVA = edgeBase + 1
+      !eBV = edgeBase + 2
+      !eVB = edgeBase + 3
+      !eCV = edgeBase + 4
+      !eVC = edgeBase + 5
+  writeOrigin mutable eAV a
+  writeOrigin mutable eVA vertex
+  writeOrigin mutable eBV b
+  writeOrigin mutable eVB vertex
+  writeOrigin mutable eCV c
+  writeOrigin mutable eVC vertex
+  faceBase <- addFaceBlock mutable 2
+  setCycle3 mutable face eAB eBV eVA
+  setCycle3 mutable faceBase eBC eCV eVB
+  setCycle3 mutable (faceBase + 1) eCA eAV eVC
+  writeVertexOut mutable a eAB
+  writeVertexOut mutable b eBC
+  writeVertexOut mutable c eCA
+  markConnected mutable vertex eVA
+  -- ABC has become ABV; the other two thirds of it are fresh faces. The three
+  -- boundary edges keep their endpoints and so keep their labels.
+  resetFaceData mutable face
+  writeScratch operation 0 eAB
+  writeScratch operation 1 eBC
+  writeScratch operation 2 eCA
+  addCounter operation (Reported FaceSplits) 1
+  legalizeScratch @p mutable operation vertex 3
+
+insertOnEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+insertOnEdge mutable operation suppliedEdge vertex = do
+  suppliedFace <- readFace mutable suppliedEdge
+  reverseFace <- readFace mutable (reverseIndex suppliedEdge)
+  if suppliedFace == 0 || reverseFace == 0
+    then splitBoundaryEdge @p mutable operation suppliedEdge vertex
+    else splitInteriorEdge @p mutable operation suppliedEdge vertex
+
+splitInteriorEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+splitInteriorEdge mutable operation suppliedEdge vertex = do
+  capacity <- ensureCellCapacity mutable 3 2
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> do
+      splitInteriorEdgeWithCapacity @p mutable operation suppliedEdge vertex
+      pure (Right ())
+
+splitInteriorEdgeWithCapacity :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
+splitInteriorEdgeWithCapacity mutable operation suppliedEdge vertex = do
+  protected <- readConstraint mutable suppliedEdge
+  suppliedFace <- readFace mutable suppliedEdge
+  let !edge = if suppliedFace == 0 then reverseIndex suppliedEdge else suppliedEdge
+      !reverseEdgeEdge = reverseIndex edge
+  leftFace <- readFace mutable edge
+  rightFace <- readFace mutable reverseEdgeEdge
+  eBC <- readNext mutable edge
+  eCA <- readPrevious mutable edge
+  eAD <- readNext mutable reverseEdgeEdge
+  eDB <- readPrevious mutable reverseEdgeEdge
+  a <- readOrigin mutable edge
+  b <- readOrigin mutable reverseEdgeEdge
+  c <- readOrigin mutable eCA
+  d <- readOrigin mutable eDB
+  writeOrigin mutable reverseEdgeEdge vertex
+  edgeBase <- addEdgeBlock mutable 3
+  let !eVB = edgeBase
+      !eBV = edgeBase + 1
+      !eVC = edgeBase + 2
+      !eCV = edgeBase + 3
+      !eVD = edgeBase + 4
+      !eDV = edgeBase + 5
+  writeOrigin mutable eVB vertex
+  writeOrigin mutable eBV b
+  writeOrigin mutable eVC vertex
+  writeOrigin mutable eCV c
+  writeOrigin mutable eVD vertex
+  writeOrigin mutable eDV d
+  faceBase <- addFaceBlock mutable 2
+  setCycle3 mutable leftFace edge eVC eCA
+  setCycle3 mutable faceBase eVB eBC eCV
+  setCycle3 mutable rightFace eBV eVD eDB
+  setCycle3 mutable (faceBase + 1) reverseEdgeEdge eAD eDV
+  writeVertexOut mutable a edge
+  writeVertexOut mutable b eBC
+  writeVertexOut mutable c eCA
+  writeVertexOut mutable d eDB
+  markConnected mutable vertex reverseEdgeEdge
+  -- AB became AV, and both incident triangles lost a corner to the new vertex.
+  unless (payloadsPristine mutable) $ do
+    resetEdgeData mutable (edge `quot` 2)
+    resetFaceData mutable leftFace
+    resetFaceData mutable rightFace
+  when protected $ do
+    _ <- setConstraint mutable eVB
+    pure ()
+  writeScratch operation 0 eCA
+  writeScratch operation 1 eBC
+  writeScratch operation 2 eDB
+  writeScratch operation 3 eAD
+  addCounter operation (Reported InteriorEdgeSplits) 1
+  legalizeScratch @p mutable operation vertex 4
+
+splitBoundaryEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
+splitBoundaryEdge mutable operation suppliedEdge vertex = do
+  outcome <-
+    splitBoundaryEdgeWithLegalization
+      (\target targetOperation insertedVertex _ _ ->
+         legalizeScratch @p target targetOperation insertedVertex 2
+      )
+      mutable
+      operation
+      suppliedEdge
+      vertex
+  pure (void outcome)
+
+-- | Split one hull edge and return its two replacement outer edges in the
+-- original outer-cycle order.  Topology and constraint inheritance have one
+-- owner; callers choose only the lawful legalization boundary.  Ordinary
+-- insertion drains the new vertex's scratch star, while a source-preserving
+-- seam can pin its immutable face section.
+splitBoundaryEdgeWithLegalization
+  :: ( MutableDcel s vertex directed undirected face
+       -> OperationState s
+       -> Int
+       -> Int
+       -> Int
+       -> ST s ()
+     )
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Either BuildError (Int, Int))
+splitBoundaryEdgeWithLegalization legalize mutable operation suppliedEdge vertex = do
+  suppliedFace <- readFace mutable suppliedEdge
+  reverseFace <- readFace mutable (reverseIndex suppliedEdge)
+  if suppliedFace /= 0 && reverseFace /= 0
+    then
+      pure
+        ( Left
+            ( BoundarySplitRequiresBoundaryEdge
+                (UndirectedEdgeId (fromIntegral (suppliedEdge `quot` 2)))
+            )
+        )
+    else do
+      capacity <- ensureCellCapacity mutable 2 1
+      case capacity of
+        Left obstruction -> pure (Left obstruction)
+        Right () ->
+          Right
+            <$> splitBoundaryEdgeWithCapacity
+              legalize
+              mutable
+              operation
+              suppliedEdge
+              vertex
+
+splitBoundaryEdgeWithCapacity
+  :: ( MutableDcel s vertex directed undirected face
+       -> OperationState s
+       -> Int
+       -> Int
+       -> Int
+       -> ST s ()
+     )
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Int, Int)
+splitBoundaryEdgeWithCapacity legalize mutable operation suppliedEdge vertex = do
+  protected <- readConstraint mutable suppliedEdge
+  suppliedFace <- readFace mutable suppliedEdge
+  let !edge = if suppliedFace == 0 then reverseIndex suppliedEdge else suppliedEdge
+      !outerEdge = reverseIndex edge
+  innerFace <- readFace mutable edge
+  eBC <- readNext mutable edge
+  eCA <- readPrevious mutable edge
+  a <- readOrigin mutable edge
+  b <- readOrigin mutable outerEdge
+  c <- readOrigin mutable eCA
+  oldOuterPrevious <- readPrevious mutable outerEdge
+  oldOuterNext <- readNext mutable outerEdge
+  writeOrigin mutable outerEdge vertex
+  edgeBase <- addEdgeBlock mutable 2
+  let !eVB = edgeBase
+      !eBV = edgeBase + 1
+      !eVC = edgeBase + 2
+      !eCV = edgeBase + 3
+  writeOrigin mutable eVB vertex
+  writeOrigin mutable eBV b
+  writeOrigin mutable eVC vertex
+  writeOrigin mutable eCV c
+  newFace <- addFaceBlock mutable 1
+  setCycle3 mutable innerFace edge eVC eCA
+  setCycle3 mutable newFace eVB eBC eCV
+  writeFace mutable outerEdge 0
+  writeFace mutable eBV 0
+  linkEdges mutable oldOuterPrevious eBV
+  linkEdges mutable eBV outerEdge
+  linkEdges mutable outerEdge oldOuterNext
+  writeFaceEdge mutable 0 outerEdge
+  writeVertexOut mutable a edge
+  writeVertexOut mutable b eBC
+  writeVertexOut mutable c eCA
+  markConnected mutable vertex eVB
+  -- AB became AV and the one interior triangle lost a corner. The outer face
+  -- is not an element and keeps nothing to lose.
+  unless (payloadsPristine mutable) $ do
+    resetEdgeData mutable (edge `quot` 2)
+    resetFaceData mutable innerFace
+  when protected $ do
+    _ <- setConstraint mutable eVB
+    pure ()
+  writeScratch operation 0 eCA
+  writeScratch operation 1 eBC
+  addCounter operation (Reported BoundaryEdgeSplits) 1
+  legalize mutable operation vertex eCA eBC
+  pure (eBV, outerEdge)
diff --git a/src-build/Moonlight/Planar/Internal/DcelOperations/Twin.hs b/src-build/Moonlight/Planar/Internal/DcelOperations/Twin.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/DcelOperations/Twin.hs
@@ -0,0 +1,17 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | The pairing involution on half-edge indices.
+module Moonlight.Planar.Internal.DcelOperations.Twin
+  ( reverseIndex
+  ) where
+
+import Data.Bits (xor)
+
+reverseIndex :: Int -> Int
+reverseIndex edge = edge `xor` 1
+{-# INLINE reverseIndex #-}
diff --git a/src-build/Moonlight/Planar/Internal/Excision.hs b/src-build/Moonlight/Planar/Internal/Excision.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Excision.hs
@@ -0,0 +1,478 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Excision of a vertex from a thawed mesh: the removal kernel, stated over
+-- the mutable arena and publishing nothing.
+module Moonlight.Planar.Internal.Excision
+  ( removeMutable
+  ) where
+
+import Control.Monad (forM, when)
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import Data.Foldable (traverse_)
+import qualified Data.IntSet as IntSet
+import Moonlight.Planar.Internal.DcelOperations.FlipRewrite (flipEdge)
+import Moonlight.Planar.Internal.DcelOperations.Legalize
+  ( legalizeCavityFanScratch
+  , legalizeEdges
+  )
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , readScratch
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.BuildStats (BuildMetric (EdgeFlips))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Internal.Predicates (orient2d)
+
+-- | The proved outgoing section of one ordinary removal. Its edges live in the
+-- operation scratch arena in counter-clockwise order; the record carries only
+-- the section's extent and the first outer-face incidence, if any.
+data RemovalStar = RemovalStar {-# UNPACK #-} !Int !(Maybe Int)
+
+removeMutable
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+removeMutable mutable operation vertex = do
+  faces <- faceCount mutable
+  if faces <= 1
+    then removeDegenerate mutable vertex
+    else do
+      collected <- collectRemovalStar mutable operation vertex
+      case collected of
+        Left obstruction -> pure (Left obstruction)
+        Right (RemovalStar degree outerOutgoing) -> do
+          case outerOutgoing of
+            Nothing -> removeInterior mutable operation vertex degree
+            Just hullEdge -> removeHull mutable operation vertex hullEdge
+
+removeDegenerate
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+removeDegenerate mutable vertex = do
+  vertexCount <- pointCount mutable
+  case vertexCount of
+    0 ->
+      pure
+        (Left (RemovalEmptyTriangulation (VertexId (fromIntegral vertex))))
+    1 -> do
+      writeFaceEdge mutable 0 (-1)
+      swapRemoveVertex mutable vertex
+    2 -> do
+      collected <- collectOutgoing mutable vertex
+      case collected of
+        Left obstruction -> pure (Left obstruction)
+        Right outgoing ->
+          case outgoing of
+            [edge] -> do
+              _ <- clearConstraint mutable edge
+              removedEdge <- swapRemoveUndirectedEdge mutable (edge `quot` 2)
+              case removedEdge of
+                Left obstruction -> pure (Left obstruction)
+                Right () -> do
+                  let !other = if vertex == 0 then 1 else 0
+                  writeVertexOut mutable other (-1)
+                  writeFaceEdge mutable 0 (-1)
+                  swapRemoveVertex mutable vertex
+            _ ->
+              pure
+                ( Left
+                    ( RemovalTwoPointDegreeMismatch
+                        (VertexId (fromIntegral vertex))
+                        (length outgoing)
+                    )
+                )
+    _ -> removeCollinear mutable vertex
+
+removeCollinear
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+removeCollinear mutable vertex = do
+  collected <- collectOutgoing mutable vertex
+  case collected of
+    Left obstruction -> pure (Left obstruction)
+    Right outgoing ->
+      case outgoing of
+        [edge] -> do
+          let !reversedEdge = edge `xor` 1
+          target <- readOrigin mutable reversedEdge
+          edgeNext <- readNext mutable edge
+          writePrevious mutable edgeNext (edgeNext `xor` 1)
+          writeNext mutable (edgeNext `xor` 1) edgeNext
+          writeVertexOut mutable target edgeNext
+          writeFaceEdge mutable 0 edgeNext
+          _ <- clearConstraint mutable edge
+          removedEdge <- swapRemoveUndirectedEdge mutable (edge `quot` 2)
+          case removedEdge of
+            Left obstruction -> pure (Left obstruction)
+            Right () -> swapRemoveVertex mutable vertex
+        [edge1, edge2] -> do
+          let !t1 = edge1 `xor` 1
+              !t1Reverse = edge1
+              !t2 = edge2 `xor` 1
+          constrained1 <- readConstraint mutable edge1
+          constrained2 <- readConstraint mutable edge2
+          edge2Next <- readNext mutable edge2
+          edge2To <- readOrigin mutable t2
+          t2Previous <- readPrevious mutable t2
+          if edge2Next == t2
+            then do
+              writeNext mutable t1 t1Reverse
+              writePrevious mutable t1Reverse t1
+            else do
+              writePrevious mutable edge2Next t1
+              writeNext mutable t1 edge2Next
+              writeNext mutable t2Previous t1Reverse
+              writePrevious mutable t1Reverse t2Previous
+          writeVertexOut mutable edge2To t1Reverse
+          writeOrigin mutable t1Reverse edge2To
+          -- The two segments meeting at the removed vertex are welded into
+          -- one, and edge1's slot now spans both. It is neither of them.
+          resetEdgeData mutable (edge1 `quot` 2)
+          writeFaceEdge mutable 0 t1
+          _ <- clearConstraint mutable edge1
+          _ <- clearConstraint mutable edge2
+          when (constrained1 || constrained2) $ do
+            _ <- setConstraint mutable edge1
+            pure ()
+          removedVertex <- swapRemoveVertex mutable vertex
+          case removedVertex of
+            Left obstruction -> pure (Left obstruction)
+            Right result -> do
+              removedEdge <- swapRemoveUndirectedEdge mutable (edge2 `quot` 2)
+              pure (result <$ removedEdge)
+        _ ->
+          pure
+            ( Left
+                ( RemovalCollinearDegreeMismatch
+                    (VertexId (fromIntegral vertex))
+                    (length outgoing)
+                )
+            )
+
+removeInterior
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+removeInterior mutable operation vertex degree = do
+  traverse_ recordRing [0 .. degree - 1]
+  capacity <- ensureCellCapacity mutable (max 0 (degree - 3)) (max 0 (degree - 2))
+  case capacity of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> do
+      -- Taken before the fan appends anything, so it separates the fan's own edges
+      -- from the cavity border exactly, the way spade's is_new_edge does.
+      !cavityFloor <- (`quot` 2) <$> directedEdgeCount mutable
+      remeshed <- remeshRingScratch mutable operation degree
+      case remeshed of
+        Left obstruction -> pure (Left obstruction)
+        Right newEdgeCount -> do
+          legalizeCavityFanScratch
+            mutable
+            operation
+            cavityFloor
+            (3 * degree)
+            newEdgeCount
+          edgesToRemove <-
+            traverse
+              (readScratch operation . (degree +))
+              [0 .. degree - 1]
+          facesToRemove <-
+            traverse
+              (readScratch operation . (2 * degree +))
+              [0 .. degree - 1]
+          cleaned <- cleanupEdgesAndFaces mutable edgesToRemove facesToRemove
+          case cleaned of
+            Left obstruction -> pure (Left obstruction)
+            Right () -> swapRemoveVertex mutable vertex
+ where
+  recordRing index = do
+    edge <- readScratch operation index
+    following <- readNext mutable edge
+    face <- readFace mutable edge
+    writeScratch operation index following
+    writeScratch operation (degree + index) (edge `quot` 2)
+    writeScratch operation (2 * degree + index) face
+
+-- | Fan the cavity a removal leaves. The border arrives in ring order, which is
+-- the order the fan consumes it in.
+remeshRingScratch
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError Int)
+remeshRingScratch mutable operation degree
+  | degree < 3 = pure (Left (RemovalBorderTooShort degree))
+  | otherwise = do
+      inner0 <- readScratch operation 0
+      fanOrigin <- readOrigin mutable inner0
+      build fanOrigin 1 inner0 0
+ where
+  build !fanOrigin !index !innerEdge !newEdgeCount
+    | index == degree - 2 = do
+      innerNext <- readScratch operation index
+      innerPrevious <- readScratch operation (index + 1)
+      newFace <- addFace mutable innerEdge
+      writeFace mutable innerEdge newFace
+      writeFace mutable innerPrevious newFace
+      writeFace mutable innerNext newFace
+      writeNext mutable innerEdge innerNext
+      writePrevious mutable innerNext innerEdge
+      writePrevious mutable innerEdge innerPrevious
+      writeNext mutable innerPrevious innerEdge
+      writePrevious mutable innerPrevious innerNext
+      writeNext mutable innerNext innerPrevious
+      previousOrigin <- readOrigin mutable innerPrevious
+      nextOrigin <- readOrigin mutable innerNext
+      writeVertexOut mutable previousOrigin innerPrevious
+      writeVertexOut mutable nextOrigin innerNext
+      writeVertexOut mutable fanOrigin innerEdge
+      pure (Right newEdgeCount)
+    | index < degree - 2 = do
+      outerEdge <- readScratch operation index
+      outerFrom <- readOrigin mutable outerEdge
+      outerTo <- readOrigin mutable (outerEdge `xor` 1)
+      (newEdge, newTwin) <- addEdge mutable outerTo fanOrigin
+      newFace <- addFace mutable newEdge
+      writeNext mutable newEdge innerEdge
+      writePrevious mutable newEdge outerEdge
+      writeFace mutable newEdge newFace
+      writeNext mutable newTwin 0
+      writePrevious mutable newTwin 0
+      writeFace mutable newTwin 0
+      writeFace mutable outerEdge newFace
+      writeNext mutable outerEdge newEdge
+      writePrevious mutable outerEdge innerEdge
+      writePrevious mutable innerEdge newEdge
+      writeNext mutable innerEdge outerEdge
+      writeFace mutable innerEdge newFace
+      writeFaceEdge mutable newFace newEdge
+      writeVertexOut mutable outerFrom outerEdge
+      writeScratch operation (3 * degree + newEdgeCount) newEdge
+      build
+        fanOrigin
+        (index + 1)
+        newTwin
+        (newEdgeCount + 1)
+    | otherwise =
+      pure (Left (RemovalBorderArityMismatch (degree - index)))
+
+removeHull :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+removeHull mutable operation vertex loopEnd = do
+  loopStart <- counterClockwiseMutable mutable loopEnd
+  loopEndNext <- readNext mutable loopEnd
+  collected <- collectConvexStrip loopEnd loopStart [] []
+  case collected of
+    Left obstruction -> pure (Left obstruction)
+    Right (!convexEdges, !edgesToValidate) -> do
+      let !strip = convexEdges ++ [loopEndNext]
+      (!edgesToRemove, !facesToRemove) <- disconnectStrip strip
+      legalizeEdges mutable operation edgesToValidate
+      cleaned <- cleanupEdgesAndFaces mutable edgesToRemove facesToRemove
+      case cleaned of
+        Left obstruction -> pure (Left obstruction)
+        Right () -> swapRemoveVertex mutable vertex
+ where
+  collectConvexStrip !end !current !convexReversed !validate = do
+    nextCurrent <- counterClockwiseMutable mutable current
+    edge <- readNext mutable current
+    repaired <- repairConvexity (edge : convexReversed) validate
+    case repaired of
+      Left obstruction -> pure (Left obstruction)
+      Right (!repairedReversed, !validate') ->
+        if nextCurrent == end
+          then pure (Right (reverse repairedReversed, validate'))
+          else collectConvexStrip end nextCurrent repairedReversed validate'
+
+  repairConvexity !edgesReversed !validate =
+    case edgesReversed of
+      edge2 : edge1 : restReversed -> do
+        target <- readOrigin mutable (edge2 `xor` 1)
+        from <- edgeOriginPoint mutable edge1
+        to <- edgeOriginPoint mutable (edge1 `xor` 1)
+        targetPoint <- pointAt mutable target
+        if orient2d from to targetPoint == GT
+          then do
+            previousEdge <- readPrevious mutable edge2
+            let !toFlip = previousEdge `xor` 1
+            rewritten <- flipEdge mutable toFlip
+            case rewritten of
+              Left obstruction -> pure (Left obstruction)
+              Right () -> do
+                addCounter operation (Reported EdgeFlips) 1
+                repairConvexity (toFlip : restReversed) (toFlip : validate)
+          else pure (Right (edgesReversed, validate))
+      _ -> pure (Right (edgesReversed, validate))
+
+  disconnectStrip strip = do
+    removed <- forM strip $ \edge -> do
+      previousSpoke <- readPrevious mutable edge
+      face <- readFace mutable edge
+      from <- readOrigin mutable edge
+      ccw <- counterClockwiseMutable mutable edge
+      predecessor <- readPrevious mutable ccw
+      writeNext mutable predecessor edge
+      writePrevious mutable edge predecessor
+      writeFace mutable edge 0
+      writeFaceEdge mutable 0 edge
+      writeVertexOut mutable from edge
+      pure (previousSpoke `quot` 2, face)
+    pure (map fst removed, map snd removed)
+
+cleanupEdgesAndFaces :: MutableDcel s vertex directed undirected face -> [Int] -> [Int] -> ST s (Either BuildError ())
+cleanupEdgesAndFaces mutable rawEdges rawFaces = do
+  let !edges = sortUniqueDesc rawEdges
+      !faces = sortUniqueDesc (filter (> 0) rawFaces)
+  removedEdges <- traverseUntilFailure (swapRemoveUndirectedEdge mutable) edges
+  case removedEdges of
+    Left obstruction -> pure (Left obstruction)
+    Right () -> traverseUntilFailure (swapRemoveFace mutable) faces
+ where
+  traverseUntilFailure
+    :: (Int -> ST s (Either BuildError ()))
+    -> [Int]
+    -> ST s (Either BuildError ())
+  traverseUntilFailure action =
+    foldr
+      ( \item continuation -> do
+          outcome <- action item
+          case outcome of
+            Left obstruction -> pure (Left obstruction)
+            Right () -> continuation
+      )
+      (pure (Right ()))
+
+-- | Descending, deduplicated. Both properties are load-bearing: swap-remove
+-- must retire the high index first (a lower index shifts under it), and a
+-- duplicated index would be retired twice.
+--
+-- A removal hands over its vertex's degree, which is small on ordinary meshes
+-- and unbounded in the worst case, so the shape is chosen by size. Insertion
+-- sort wins outright while the ring is short — measured 6.75 against
+-- 8.24 KiB/removal for @IntSet@ on the n=10000 lane — and is quadratic, so a
+-- high-degree ring goes to the ordered set that carries the asymptotics.
+--
+-- The lazy 'foldl' is deliberate: its accumulator is the output structure.
+-- 'foldl'' forced each intermediate spine and measured 7.31 versus
+-- 6.75 KiB/removal on the same lane.
+sortUniqueDesc :: [Int] -> [Int]
+sortUniqueDesc values
+  | exceedsInsertionRing values = IntSet.toDescList (IntSet.fromList values)
+  | otherwise = foldl insertUnique [] values
+ where
+  insertUnique :: [Int] -> Int -> [Int]
+  insertUnique sorted value = go sorted
+   where
+    go [] = [value]
+    go (first : rest) = case compare value first of
+      GT -> value : first : rest
+      EQ -> first : rest
+      LT -> first : go rest
+
+-- | Whether a ring is long enough to owe the ordered set its logarithm,
+-- decided without measuring the whole list: the insertion path is chosen by
+-- the prefix, never by a full traversal.
+exceedsInsertionRing :: [Int] -> Bool
+exceedsInsertionRing = not . null . drop insertionRingLimit
+
+insertionRingLimit :: Int
+insertionRingLimit = 32
+
+collectRemovalStar
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError RemovalStar)
+collectRemovalStar mutable operation vertex = do
+  start <- readVertexOut mutable vertex
+  if start < 0
+    then pure (Right (RemovalStar 0 Nothing))
+    else do
+      halfEdges <- directedEdgeCount mutable
+      let !budget = halfEdges + 1
+          go
+            :: Int
+            -> Int
+            -> Bool
+            -> Int
+            -> Maybe Int
+            -> ST s (Either BuildError RemovalStar)
+          go !remaining !current !seen !degree !outerEdge
+            | remaining <= 0 =
+                pure
+                  ( Left
+                      ( RemovalOutgoingCycleDidNotTerminate
+                          (VertexId (fromIntegral vertex))
+                          (DirectedEdgeId (fromIntegral current))
+                          budget
+                      )
+                  )
+            | seen && current == start =
+                pure (Right (RemovalStar degree outerEdge))
+            | otherwise = do
+                writeScratch operation degree current
+                face <- readFace mutable current
+                _ <- clearConstraint mutable current
+                previousEdge <- readPrevious mutable current
+                let !nextOuter =
+                      case outerEdge of
+                        Just edge -> Just edge
+                        Nothing
+                          | face == 0 -> Just current
+                          | otherwise -> Nothing
+                go
+                  (remaining - 1)
+                  (previousEdge `xor` 1)
+                  True
+                  (degree + 1)
+                  nextOuter
+      go budget start False 0 Nothing
+
+collectOutgoing
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError [Int])
+collectOutgoing mutable vertex = do
+  start <- readVertexOut mutable vertex
+  if start < 0
+    then pure (Right [])
+    else do
+      halfEdges <- directedEdgeCount mutable
+      let !budget = halfEdges + 1
+          go
+            :: Int
+            -> Int
+            -> Bool
+            -> [Int]
+            -> ST s (Either BuildError [Int])
+          go !remaining !current !seen !result
+            | remaining <= 0 =
+                pure
+                  ( Left
+                      ( RemovalOutgoingCycleDidNotTerminate
+                          (VertexId (fromIntegral vertex))
+                          (DirectedEdgeId (fromIntegral current))
+                          budget
+                      )
+                  )
+            | seen && current == start = pure (Right (reverse result))
+            | otherwise = do
+                previousEdge <- readPrevious mutable current
+                go (remaining - 1) (previousEdge `xor` 1) True (current : result)
+      go budget start False []
+
+counterClockwiseMutable :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+counterClockwiseMutable mutable edge = (`xor` 1) <$> readPrevious mutable edge
diff --git a/src-build/Moonlight/Planar/Internal/Join.hs b/src-build/Moonlight/Planar/Internal/Join.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Join.hs
@@ -0,0 +1,115 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The join of two triangulations: a Delaunay representative of the union of
+-- their sites.
+module Moonlight.Planar.Internal.Join
+  ( joinNormalForm
+  , joinBalanced
+  ) where
+
+import Control.Monad (void)
+import Data.Foldable (traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import Moonlight.Algebra.Pure.Join (JoinSemilattice (join))
+import Moonlight.Planar.BulkLoad (empty)
+import Moonlight.Planar.Internal.Join.Plan
+  ( PairPlan (..)
+  , planPair
+  )
+import Moonlight.Planar.Internal.Join.Seam (executeSeam)
+import Moonlight.Planar.Internal.Join.SiteSet
+  ( SiteSet
+  , rebuildCanonicalSiteSet
+  , siteSetAssocs
+  , siteSetSize
+  )
+import Moonlight.Planar.Internal.Session
+  ( insertAdmittedVertexAtCombining
+  , withLocalSession
+  )
+import Moonlight.Planar.Internal.Tournament
+  ( interpretTournament
+  , planTournament
+  )
+import Moonlight.Planar.Types
+
+-- | A Delaunay representative of the union of two site sets.
+--
+-- Skewed operands descend through the existing local copy-on-write session, so
+-- the larger operand's vertex handles and untouched pages survive. Comparable
+-- operands may merge along a separating seam or rebuild from their combined
+-- site set. Every schedule returns valid topology;
+-- 'Moonlight.Planar.Canonical.canonicalize' is the
+-- separate physical observation when construction-independent numbering is
+-- required.
+--
+-- Empty and structurally identical operands return an existing value verbatim.
+-- Algebraic agreement between all schedules is stated by equal canonical
+-- observations; structural equality continues to describe exact resident
+-- representation.
+joinNormalForm
+  :: (Eq annotation, JoinSemilattice annotation)
+  => Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+joinNormalForm left right = executePairPlan (planPair left right) left right
+
+executePairPlan
+  :: JoinSemilattice annotation
+  => PairPlan annotation
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+executePairPlan pairPlan left right =
+  case pairPlan of
+    ReturnLeftOperand -> Right left
+    ReturnRightOperand -> Right right
+    InsertLeftIntoRight sites -> insertSites sites right
+    InsertRightIntoLeft sites -> insertSites sites left
+    MergeSeparated seamPlan -> executeSeam seamPlan left right
+    RebuildCanonicalUnion sites -> rebuildCanonicalSiteSet sites
+
+insertSites
+  :: JoinSemilattice annotation
+  => SiteSet annotation
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+insertSites sites base = do
+  ((), inserted, _) <-
+    withLocalSession
+      base
+      (siteSetSize sites)
+      ( traverse_
+          (\(point, annotation) ->
+             void (insertAdmittedVertexAtCombining join point annotation)
+          )
+          (siteSetAssocs sites)
+      )
+  pure inserted
+
+-- | Combine by a balanced tournament rather than by a fold.
+--
+-- Associativity and commutativity make every bracketing canonically equivalent,
+-- so this is a cost choice and not a semantic one. A left fold republishes an
+-- accumulator that grows by one shard per step and so rebuilds @Θ(nk)@ sites
+-- over @k@ shards; halving the list rebuilds @Θ(n log k)@.
+--
+-- The specialization below is load-bearing rather than decorative. Without it
+-- this function is the only one on the path that stays polymorphic, and every
+-- join in the tournament pays for a dictionary while a caller's own fold at a
+-- known element type does not. That alone cost a factor of two and hid the
+-- advantage this function exists for.
+--
+-- The tournament retains the binary operation's shortcut and annotation-gluing
+-- semantics rather than inventing a second n-ary implementation.
+joinBalanced
+  :: (Eq annotation, JoinSemilattice annotation)
+  => [Triangulation 'Unconstrained annotation () () ()]
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+joinBalanced [] = Right (empty unitElementDefaults)
+joinBalanced (first : rest) =
+  interpretTournament joinNormalForm (planTournament (first :| rest))
+{-# SPECIALIZE joinBalanced
+  :: [Triangulation 'Unconstrained () () () ()]
+  -> Either BuildError (Triangulation 'Unconstrained () () () ()) #-}
diff --git a/src-build/Moonlight/Planar/Internal/Join/Plan.hs b/src-build/Moonlight/Planar/Internal/Join/Plan.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Join/Plan.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | The sole physical planner for binary and n-ary joins. It derives exact
+-- local compatibility facts, selects one schedule, and leaves execution to a
+-- consumer; sequential and concurrent interpreters share the same tournament
+-- tree rather than inventing pairing policies of their own.
+module Moonlight.Planar.Internal.Join.Plan
+  ( PairPlan (..)
+  , planPair
+  ) where
+
+import Moonlight.Planar.Dcel (numVertices)
+import Moonlight.Algebra.Pure.Join (JoinSemilattice (join))
+import Moonlight.Planar.Internal.BulkLoad (empty)
+import Moonlight.Planar.Internal.Join.Seam (SeamPlan, planSeam)
+import Moonlight.Planar.Internal.Join.SiteSet
+  ( SiteSet
+  , siteSetFromTriangulation
+  , siteSetRelation
+  , siteSetUnionWith
+  )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Internal.Types
+  ( ConstraintMode (Unconstrained)
+  , SiteRelation (..)
+  , unitElementDefaults
+  )
+
+data PairPlan annotation
+  = ReturnLeftOperand
+  | ReturnRightOperand
+  | InsertLeftIntoRight !(SiteSet annotation)
+  | InsertRightIntoLeft !(SiteSet annotation)
+  | MergeSeparated !SeamPlan
+  | RebuildCanonicalUnion !(SiteSet annotation)
+
+-- | Stage cheap facts before exact set classification. Empty and structurally
+-- identical operands return verbatim; skewed pairs preserve the larger value
+-- through local insertion before seam planning is considered.
+planPair
+  :: (Eq annotation, JoinSemilattice annotation)
+  => Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> PairPlan annotation
+planPair left right
+  -- Two zero-site values may differ structurally; the package-owned empty is
+  -- the identity, so preserve the other representative when only one is it.
+  | leftCount == 0 && rightCount == 0 =
+      if left == empty unitElementDefaults
+        then ReturnRightOperand
+        else ReturnLeftOperand
+  | leftCount == 0 = ReturnRightOperand
+  | rightCount == 0 = ReturnLeftOperand
+  | left == right = ReturnLeftOperand
+  | insertionIsCheaper leftCount rightCount = InsertLeftIntoRight leftSites
+  | insertionIsCheaper rightCount leftCount = InsertRightIntoLeft rightSites
+  | Just seamPlan <- planSeam left right = MergeSeparated seamPlan
+  | otherwise =
+      case siteSetRelation leftSites rightSites of
+        EqualSites -> InsertLeftIntoRight leftSites
+        LeftProperSubset -> InsertLeftIntoRight leftSites
+        RightProperSubset -> InsertRightIntoLeft rightSites
+        DisjointSites -> rebuildUnion
+        PartialOverlap _ -> rebuildUnion
+ where
+  !leftCount = numVertices left
+  !rightCount = numVertices right
+  leftSites = siteSetFromTriangulation left
+  rightSites = siteSetFromTriangulation right
+  rebuildUnion =
+    RebuildCanonicalUnion
+      (siteSetUnionWith join leftSites rightSites)
+
+-- A transaction reuses an existing topology only when the added side is small
+-- enough that its expected local cavities beat one bulk sweep. This is an
+-- internal cost estimate, deliberately not a caller-controlled threshold.
+insertionIsCheaper :: Int -> Int -> Bool
+insertionIsCheaper addition base = addition <= 64 || addition <= base `quot` 8
+{-# INLINE insertionIsCheaper #-}
diff --git a/src-build/Moonlight/Planar/Internal/Join/Seam.hs b/src-build/Moonlight/Planar/Internal/Join/Seam.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Join/Seam.hs
@@ -0,0 +1,1363 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Linear seam construction for separated Delaunay triangulations. Admission
+-- returns an opaque proof carrying the exact source order and tangents; the
+-- executor therefore has no untyped precondition and owns no fallback.
+module Moonlight.Planar.Internal.Join.Seam
+  ( SeamPlan
+  , planSeam
+  , executeSeam
+  , ConstrainedSeamExecution
+  , seamExecutionTriangulation
+  , seamExecutionBuildStats
+  , seamExecutionPublicationStats
+  , seamExecutionCachedFrontierPointReads
+  , seamExecutionLeftFaceCount
+  , seamExecutionRightFaceEvidence
+  , seamExecutionJoinFaces
+  , executeConstrainedSeam
+  ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad.ST (ST, runST)
+import Data.Bits (finiteBitSize, xor)
+import Data.Foldable (traverse_)
+import qualified Data.IntSet as IntSet
+import qualified Data.List as List
+import qualified Data.Sequence as Seq
+import qualified Data.Vector as V
+import Moonlight.Planar.Dcel
+  ( adjacentEdge
+  , faceDirectedEdges
+  , faceVertices
+  , incidentFace
+  , isConstraintEdge
+  , numDirectedEdges
+  , numFaces
+  , numInnerFaces
+  , numVertices
+  , origin
+  , previous
+  , vertexOutEdge
+  , vertexData
+  )
+import Moonlight.Planar.Internal.FloodFillIterator (floodFillFacesWithRejectedEdges)
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  , directedPair
+  )
+import Moonlight.Planar.Internal.DcelOperations.Hull (closeOuterTurn)
+import Moonlight.Planar.Internal.DcelOperations.Legalize
+  ( legalizeEdges
+  , legalizeEdgesPinned
+  )
+import Moonlight.Planar.Internal.DcelOperations.Subdivide
+  ( splitBoundaryEdgeWithLegalization
+  )
+import Moonlight.Planar.Internal.DcelOperations.FlipRule (illegalDiagonal)
+import Moonlight.Planar.Internal.Cdt.Combinators (bindMutable)
+import Moonlight.Planar.Internal.Cdt.Query (constraintEdges)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (Reported)
+  , OperationState
+  , addCounter
+  , finalizeBuildStats
+  , newOperationState
+  )
+import Moonlight.Planar.Internal.Paged
+  ( PublicationStats
+  , TransactionShape (LocalTransaction)
+  , pagedUnsafeIndex
+  )
+import Moonlight.Planar.Internal.Representation
+  ( SeamFrontierIndex (..)
+  , Triangulation (..)
+  , seamQuarterTurn
+  , summarizeSeamChart
+  )
+import Moonlight.Planar.Internal.Cdt.Types
+  ( ConstrainedSeamFaceEvidence (..)
+  , ConstrainedSeamSide (..)
+  )
+import Moonlight.Planar.Internal.Types (BuildError (..), ConstraintMode (Constrained, Unconstrained), InvariantViolation (..), RefinementParameters (..), unitElementDefaults)
+import Moonlight.Planar.BuildStats (BuildStats, BuildMetric (SteinerPoints))
+import Moonlight.Planar.Point (Point (..), queryPointValue)
+import Moonlight.Planar.Internal.Predicates
+  ( inCircleCoordinates
+  , orient2dCoordinates
+  )
+import Moonlight.Planar.Math (distance, midpoint)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Internal.Transaction (runTransactionWithPublication)
+
+-- | Proof that a particular pair can be copied and stitched by the seam
+-- kernel. Constructors stay private so an unseparated pair cannot be handed to
+-- the executor by accident.
+data SeamPlan
+  = -- | The operands are already in geometric left/right order.  The two
+    -- retained frontiers are therefore the exact ones consumed by the zipper.
+    SeamLeftBeforeRight !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
+  | -- | The input operands are reversed, but the retained frontiers remain in
+    -- geometric left/right order rather than input-operand order.
+    SeamRightBeforeLeft !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
+
+-- | Coordinate chart used by the separated seam.  The second chart is the
+-- orientation-preserving quarter turn @(u,v) = (y,-x)@; exact predicates and
+-- the zipper therefore remain unchanged while north/south admission reuses
+-- the same tangent interpreter.
+data SeamAxis
+  = SeamAxisX
+  | SeamAxisY
+  deriving stock (Eq, Show)
+
+planSeam
+  :: Triangulation mode vertex () () ()
+  -> Triangulation mode' vertex () () ()
+  -> Maybe SeamPlan
+planSeam left right = do
+  leftFrontier <- triSeamFrontier left
+  rightFrontier <- triSeamFrontier right
+  planAxis SeamAxisX leftFrontier rightFrontier
+    <|> planAxis SeamAxisY leftFrontier rightFrontier
+ where
+  planAxis axis leftFrontier rightFrontier =
+    case separatedOrder axis left right leftFrontier rightFrontier of
+      Just LeftBeforeRight ->
+        (\tangents -> SeamLeftBeforeRight tangents leftFrontier rightFrontier)
+          <$> seamTangents axis left leftFrontier right rightFrontier
+      Just RightBeforeLeft ->
+        (\tangents -> SeamRightBeforeLeft tangents rightFrontier leftFrontier)
+          <$> seamTangents axis right rightFrontier left leftFrontier
+      Nothing -> Nothing
+
+-- | Execute a proved seam schedule. Numbering follows the schedule;
+-- 'Moonlight.Planar.Dcel.canonicalize' remains the explicit
+-- construction-independent observation.
+executeSeam
+  :: forall vertex
+   . SeamPlan
+  -> Triangulation 'Unconstrained vertex () () ()
+  -> Triangulation 'Unconstrained vertex () () ()
+  -> Either BuildError (Triangulation 'Unconstrained vertex () () ())
+executeSeam plan left right =
+  case plan of
+    SeamLeftBeforeRight tangents _ _ ->
+      fmap fst (mergeSeparated left right tangents)
+    SeamRightBeforeLeft tangents _ _ ->
+      fmap fst (mergeSeparated right left tangents)
+
+-- | Result of the constrained seam kernel. Constraint flags are copied before
+-- legalization, so source contour edges are immutable barriers while the
+-- zipper constructs only the missing corridor.
+data ConstrainedSeamExecution vertex = ConstrainedSeamExecution
+  { seamExecutionTriangulation
+      :: !(Triangulation 'Constrained vertex () () ())
+  , seamExecutionBuildStats :: !BuildStats
+  , seamExecutionPublicationStats :: !PublicationStats
+  , seamExecutionCachedFrontierPointReads :: {-# UNPACK #-} !Int
+  , seamExecutionLeftFaceCount :: {-# UNPACK #-} !Int
+  , seamExecutionRightFaceEvidence :: !(V.Vector ConstrainedSeamFaceEvidence)
+  , seamExecutionJoinFaces :: !(V.Vector FaceId)
+  }
+
+-- | Execute a proved seam while transporting both source constraint planes.
+-- This is distinct from promoting the unconstrained result afterward: source
+-- hull constraints must already be visible to seam legalization or the
+-- legalization schedule could erase a solved source face before recovery had
+-- a chance to mark it.
+executeConstrainedSeam
+  :: forall vertex
+   . (ConstrainedSeamSide -> FaceId -> Bool)
+  -> RefinementParameters
+  -> vertex
+  -> SeamPlan
+  -> Triangulation 'Constrained vertex () () ()
+  -> Triangulation 'Constrained vertex () () ()
+  -> Either BuildError (ConstrainedSeamExecution vertex)
+executeConstrainedSeam sourceFacePreserved parameters vertexDefault plan left right = do
+  let (order, tangents, leftFrontier, rightFrontier) =
+        case plan of
+          SeamLeftBeforeRight selectedTangents selectedLeftFrontier selectedRightFrontier ->
+            (GeometricBaseLeft, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
+          SeamRightBeforeLeft selectedTangents selectedLeftFrontier selectedRightFrontier ->
+            (GeometricBaseRight, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
+  (triangulation, statistics, publicationStats, joinFaces) <-
+    executeResidentConstrained
+      targetFaceProtected
+      parameters
+      vertexDefault
+      order
+      left
+      right
+      leftFrontier
+      rightFrontier
+      tangents
+  rightEvidence <-
+    seamFaceEvidenceForSource
+      right
+      incomingFaceOffset
+      rightEvidenceFaces
+  pure
+    ConstrainedSeamExecution
+      { seamExecutionTriangulation = triangulation
+      , seamExecutionBuildStats = statistics
+      , seamExecutionPublicationStats = publicationStats
+      , seamExecutionCachedFrontierPointReads =
+          seamFrontierPointReads tangents
+            + case refineMaxEdgeLength parameters of
+              Nothing -> 0
+              Just _ -> 4
+      , seamExecutionLeftFaceCount = numInnerFaces left
+      , seamExecutionRightFaceEvidence = rightEvidence
+      , seamExecutionJoinFaces = joinFaces
+      }
+ where
+  !residentFaceLimit = numFaces left
+  !incomingFaceOffset = residentFaceLimit - 1
+  !incomingFaceLimit = incomingFaceOffset + numFaces right
+
+  targetFaceProtected rawFace
+    | rawFace <= 0 = False
+    | rawFace < residentFaceLimit =
+        sourceFacePreserved SeamResident (FaceId (fromIntegral rawFace))
+    | rawFace < incomingFaceLimit =
+        sourceFacePreserved
+          SeamIncoming
+          (FaceId (fromIntegral (rawFace - incomingFaceOffset)))
+    | otherwise = False
+
+  rightEvidenceFaces =
+    filter
+      (sourceFacePreserved SeamIncoming)
+      (fmap (FaceId . fromIntegral) [1 .. numFaces right - 1])
+
+-- | Descend exactly the unprotected face component touched by the zipper.
+-- This is the authoritative J section: it contains both newly allocated seam
+-- faces and any source exterior filler lawfully retriangulated into them.
+exactJoinFaces
+  :: Triangulation 'Constrained vertex () () ()
+  -> (Int -> Bool)
+  -> V.Vector FaceId
+  -> (V.Vector FaceId, [UndirectedEdgeId])
+exactJoinFaces triangulation targetFaceProtected seeds =
+  let (faces, rejectedPairs) =
+        floodFillFacesWithRejectedEdges
+          triangulation
+          (V.toList seeds)
+          canCross
+   in (V.fromList faces, rejectedPairs)
+ where
+  canCross edge =
+    not (isConstraintEdge triangulation edge)
+      && not
+        ( any
+            targetFaceIsProtected
+            (incidentInnerFaces triangulation edge)
+        )
+
+  targetFaceIsProtected (FaceId raw) =
+    targetFaceProtected (fromIntegral raw)
+
+-- | Certify only the J/selected-face overlap after local descent. A selected
+-- source face is immutable; therefore an unconstrained overlap edge that still
+-- requires a Delaunay flip is a genuine incompatibility, never permission to
+-- rewrite the solved section.
+certifyJoinBoundary
+  :: Triangulation 'Constrained vertex () () ()
+  -> [UndirectedEdgeId]
+  -> Either BuildError ()
+certifyJoinBoundary triangulation = traverse_ certifyPair
+ where
+  certifyPair edge =
+    if isConstraintEdge triangulation edge
+          || not (immutableEdgeRequiresFlip triangulation edge)
+          then Right ()
+          else Left (SeamSourceEdgeRequiresFlip edge)
+
+incidentInnerFaces
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> [FaceId]
+incidentInnerFaces triangulation edge =
+  filter (/= FaceId 0)
+    [ incidentFace triangulation forward
+    , incidentFace triangulation backward
+    ]
+ where
+  (forward, backward) = directedPair edge
+
+immutableEdgeRequiresFlip
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> Bool
+immutableEdgeRequiresFlip triangulation edge =
+  leftFace /= FaceId 0
+    && rightFace /= FaceId 0
+    && orient2dCoordinates cx cy dx dy bx by == GT
+    && orient2dCoordinates dx dy cx cy ax ay == GT
+    && illegalDiagonal ax ay bx by cx cy dx dy
+ where
+  (forward, backward) = directedPair edge
+  leftFace = incidentFace triangulation forward
+  rightFace = incidentFace triangulation backward
+  forwardPrevious = previous triangulation forward
+  backwardPrevious = previous triangulation backward
+  Point ax ay = vertexPointAt triangulation (origin triangulation forward)
+  Point bx by = vertexPointAt triangulation (origin triangulation backward)
+  Point cx cy = vertexPointAt triangulation (origin triangulation forwardPrevious)
+  Point dx dy = vertexPointAt triangulation (origin triangulation backwardPrevious)
+
+vertexPointAt
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> Point
+vertexPointAt triangulation (VertexId raw) =
+  Point
+    (triPointX triangulation `pagedUnsafeIndex` fromIntegral raw)
+    (triPointY triangulation `pagedUnsafeIndex` fromIntegral raw)
+
+data GeometricBaseOrder
+  = GeometricBaseLeft
+  | GeometricBaseRight
+  deriving stock (Eq, Show)
+
+-- | Finite local work required to turn the two synthetic perimeter bridges
+-- into edge-bounded constrained paths.  The witness is derived while the
+-- tangent endpoints still identify J exactly; after publication those edges
+-- are ordinary constraints and must not be rediscovered by a global scan.
+data SeamBridgeSubdivision = SeamBridgeSubdivision
+  { seamLowerBridgeDepth :: {-# UNPACK #-} !Int
+  , seamUpperBridgeDepth :: {-# UNPACK #-} !Int
+  , seamBridgeAddedVertices :: {-# UNPACK #-} !Int
+  }
+
+planSeamBridgeSubdivision
+  :: RefinementParameters
+  -> Point
+  -> Point
+  -> Point
+  -> Point
+  -> Either BuildError SeamBridgeSubdivision
+planSeamBridgeSubdivision parameters lowerLeft lowerRight upperLeft upperRight =
+  case refineMaxEdgeLength parameters of
+    Nothing -> Right (SeamBridgeSubdivision 0 0 0)
+    Just maximumLength -> do
+      let !lowerDepth = requiredBridgeSubdivisionDepth maximumLength lowerLeft lowerRight
+          !upperDepth = requiredBridgeSubdivisionDepth maximumLength upperLeft upperRight
+          !required =
+            subdivisionVertexCount lowerDepth
+              + subdivisionVertexCount upperDepth
+      if required > toInteger (maxBound :: Int)
+        then Left (CapacityExceeded maxBound)
+        else do
+          let !requiredVertices = fromInteger required
+          available <-
+            case (requiredVertices, refineMaxAdditionalVertices parameters) of
+              (0, Nothing) -> Right 0
+              (_, Nothing) -> Left RefinementDomainRequiresFiniteVertexBudget
+              (_, Just budget) -> Right budget
+          if requiredVertices <= available
+            then
+              Right
+                SeamBridgeSubdivision
+                  { seamLowerBridgeDepth = lowerDepth
+                  , seamUpperBridgeDepth = upperDepth
+                  , seamBridgeAddedVertices = requiredVertices
+                  }
+            else
+              Left
+                ( RefinementSeamBridgeBudgetExceeded
+                    requiredVertices
+                    available
+                )
+ where
+  subdivisionVertexCount :: Int -> Integer
+  subdivisionVertexCount depth = (2 :: Integer) ^ depth - 1
+
+requiredBridgeSubdivisionDepth :: Double -> Point -> Point -> Int
+requiredBridgeSubdivisionDepth maximumLength from to =
+  descend 0 (distance from to)
+ where
+  descend !depth !currentLength
+    | currentLength <= maximumLength = depth
+    | depth >= finiteBitSize (0 :: Int) = depth
+    | otherwise = descend (depth + 1) (0.5 * currentLength)
+
+vertexPointForOuterEdge
+  :: Triangulation mode vertex directed undirected face
+  -> Int
+  -> Point
+vertexPointForOuterEdge triangulation edge =
+  vertexPointAt
+    triangulation
+    (VertexId (fromIntegral (topologyAt triangulation (4 * edge))))
+
+subdivideSeamBridge
+  :: forall s vertex directed undirected face
+   . (Int -> Bool)
+  -> vertex
+  -> Int
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError (Seq.Seq Int))
+subdivideSeamBridge targetFaceProtected vertexDefault = descend
+ where
+  descend
+    :: Int
+    -> MutableDcel s vertex directed undirected face
+    -> OperationState s
+    -> Int
+    -> ST s (Either BuildError (Seq.Seq Int))
+  descend depth mutable operation outerEdge
+    | depth <= 0 = pure (Right (Seq.singleton outerEdge))
+    | otherwise = do
+        fromVertex <- readOrigin mutable outerEdge
+        toVertex <- readOrigin mutable (outerEdge `xor` 1)
+        fromPoint <- pointAt mutable fromVertex
+        toPoint <- pointAt mutable toVertex
+        case validatePoint Nothing (midpoint fromPoint toPoint) of
+          Left obstruction -> pure (Left obstruction)
+          Right admitted ->
+            let !splitPoint = queryPointValue admitted
+             in if splitPoint == fromPoint || splitPoint == toPoint
+                  then
+                    pure
+                      ( Left
+                          ( RefinementSeamBridgeMidpointCollapsed
+                              (UndirectedEdgeId (fromIntegral (outerEdge `quot` 2)))
+                          )
+                      )
+                  else do
+                    vertex <- appendVertex mutable splitPoint vertexDefault
+                    splitBoundaryEdgeWithLegalization
+                      (\target targetOperation _ firstCandidate secondCandidate ->
+                         legalizeEdgesPinned
+                           targetFaceProtected
+                           target
+                           targetOperation
+                           [firstCandidate, secondCandidate]
+                      )
+                      mutable
+                      operation
+                      outerEdge
+                      vertex
+                      `bindMutable` \(firstOuter, secondOuter) -> do
+                        addCounter operation (Reported SteinerPoints) 1
+                        descend (depth - 1) mutable operation firstOuter
+                          `bindMutable` \firstChain ->
+                            fmap (fmap (firstChain Seq.><))
+                              (descend (depth - 1) mutable operation secondOuter)
+
+executeResidentConstrained
+  :: forall vertex
+   . (Int -> Bool)
+  -> RefinementParameters
+  -> vertex
+  -> GeometricBaseOrder
+  -> Triangulation 'Constrained vertex () () ()
+  -> Triangulation 'Constrained vertex () () ()
+  -> SeamFrontierIndex
+  -> SeamFrontierIndex
+  -> SeamTangents
+  -> Either BuildError
+      ( Triangulation 'Constrained vertex () () ()
+      , BuildStats
+      , PublicationStats
+      , V.Vector FaceId
+      )
+executeResidentConstrained targetFaceProtected parameters vertexDefault order base extension leftFrontier rightFrontier tangents = do
+  let !baseDirected = numDirectedEdges base
+      !extensionDirected = numDirectedEdges extension
+      !baseVertices = numVertices base
+      (leftSource, rightSource, leftVertexOffset, rightVertexOffset, leftEdgeOffset, rightEdgeOffset) =
+        case order of
+          GeometricBaseLeft ->
+            (base, extension, 0, baseVertices, 0, baseDirected)
+          GeometricBaseRight ->
+            (extension, base, baseVertices, 0, baseDirected, 0)
+      SeamTangents
+        { seamLowerLeft = SeamTangent _ lowerLeft
+        , seamLowerRight = SeamTangent _ lowerRight
+        , seamUpperLeft = SeamTangent _ upperLeft
+        , seamUpperRight = SeamTangent _ upperRight
+        } = tangents
+  bridgePlan <-
+    planSeamBridgeSubdivision
+      parameters
+      (vertexPointForOuterEdge leftSource lowerLeft)
+      (vertexPointForOuterEdge rightSource lowerRight)
+      (vertexPointForOuterEdge leftSource upperLeft)
+      (vertexPointForOuterEdge rightSource upperRight)
+  let !extensionVertices = numVertices extension
+      !bridgeVertices = seamBridgeAddedVertices bridgePlan
+  transactionAdditional <-
+    if bridgeVertices > maxBound - extensionVertices
+      then Left (CapacityExceeded maxBound)
+      else Right (extensionVertices + bridgeVertices)
+  ((newFaces, progress, upperBridgeChain, lowerBridgeChain), triangulation, statistics, publicationStats) <-
+    runTransactionWithPublication
+      id
+      LocalTransaction
+      base
+      transactionAdditional
+      (\mutable operation -> do
+         appendSourceVertices mutable extension
+         _ <- addEdgeBlock mutable (extensionDirected `quot` 2)
+         _ <- addFaceBlock mutable (numFaces extension - 1)
+         copySource
+           mutable
+           extension
+           baseVertices
+           baseDirected
+           (numFaces base - 1)
+         copySourceConstraints mutable baseDirected extension
+         seamBase <-
+           spliceLowerTangent
+             mutable
+             rightEdgeOffset
+             (leftEdgeOffset + lowerLeft)
+             lowerRight
+         -- The two synthetic hull bridges are the certified perimeter of the
+         -- published world.  Once a later extension consumes either bridge it
+         -- becomes Γ, so fixing it now preserves the prior solve without a
+         -- retrospective source rewrite.
+         _ <- setConstraint mutable seamBase
+         stitchSeamPinned
+           targetFaceProtected
+           mutable
+           operation
+           (Seq.length (seamFrontierEdges leftFrontier))
+           (tangentIndex (seamLowerLeft tangents))
+           (Seq.length (seamFrontierEdges rightFrontier))
+           (tangentIndex (seamLowerRight tangents))
+           seamBase
+           (leftVertexOffset + topologyAt leftSource (4 * upperLeft))
+           (rightVertexOffset + topologyAt rightSource (4 * upperRight))
+           []
+           []
+           `bindMutable` \(upperBridge, newFaces, progress) -> do
+             _ <- setConstraint mutable upperBridge
+             subdivideSeamBridge
+               targetFaceProtected
+               vertexDefault
+               (seamLowerBridgeDepth bridgePlan)
+               mutable
+               operation
+               (seamBase `xor` 1)
+               `bindMutable` \lowerBridgeChain ->
+                 subdivideSeamBridge
+                   targetFaceProtected
+                   vertexDefault
+                   (seamUpperBridgeDepth bridgePlan)
+                   mutable
+                   operation
+                   upperBridge
+                   `bindMutable` \upperBridgeChain ->
+                     case Seq.lookup 0 upperBridgeChain of
+                       Nothing -> pure (Left SeamFrontierUnavailable)
+                       Just frontierStart -> do
+                         writeFaceEdge mutable 0 frontierStart
+                         pure
+                           ( Right
+                               (newFaces, progress, upperBridgeChain, lowerBridgeChain)
+                           )
+      )
+  let newFaceSeeds = V.fromList (fmap (FaceId . fromIntegral) newFaces)
+      (joinFaces, rejectedBoundaryPairs) =
+        exactJoinFaces triangulation targetFaceProtected newFaceSeeds
+  certifyJoinBoundary triangulation rejectedBoundaryPairs
+  frontier <-
+    combineSeamFrontier
+      triangulation
+      leftFrontier
+      rightFrontier
+      leftEdgeOffset
+      rightEdgeOffset
+      upperBridgeChain
+      lowerBridgeChain
+      progress
+  let published :: Triangulation 'Constrained vertex () () ()
+      published = triangulation{triSeamFrontier = Just frontier}
+  pure
+    ( published
+    , statistics
+    , publicationStats
+    , joinFaces
+    )
+
+seamFaceEvidenceForSource
+  :: Triangulation 'Constrained vertex () () ()
+  -> Int
+  -> [FaceId]
+  -> Either BuildError (V.Vector ConstrainedSeamFaceEvidence)
+seamFaceEvidenceForSource source targetOffset sourceFaces =
+  V.fromList <$> traverse evidenceFor sourceFaces
+ where
+  evidenceFor sourceFace = do
+    (first, second, third) <- facePoints source sourceFace
+    let targetFace = FaceId (unFaceId sourceFace + fromIntegral targetOffset)
+    Right
+      ConstrainedSeamFaceEvidence
+        { constrainedSeamSourceFace = sourceFace
+        , constrainedSeamTargetFace = targetFace
+        , constrainedSeamFaceFirstPoint = first
+        , constrainedSeamFaceSecondPoint = second
+        , constrainedSeamFaceThirdPoint = third
+        }
+
+  facePoints
+    :: Triangulation mode sourceVertex directed undirected face
+    -> FaceId
+    -> Either BuildError (Point, Point, Point)
+  facePoints triangulation faceHandle =
+    case List.sort (fmap (vertexPointAt triangulation) (faceVertices triangulation faceHandle)) of
+      [first, second, third] -> Right (first, second, third)
+      _ -> Left (faceCardinalityFailure triangulation faceHandle)
+
+  faceCardinalityFailure
+    :: Triangulation mode sourceVertex directed undirected face
+    -> FaceId
+    -> BuildError
+  faceCardinalityFailure triangulation faceHandle =
+    case faceDirectedEdges triangulation faceHandle of
+      edge : _ -> RefinementInputTopologyInvalid (InnerFaceNotTriangularAtEdge edge)
+      [] -> RefinementInputTopologyInvalid (FaceMissingAdjacentEdge faceHandle)
+
+data SeparatedOrder
+  = LeftBeforeRight
+  | RightBeforeLeft
+
+separatedOrder
+  :: SeamAxis
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode' vertex' directed' undirected' face'
+  -> SeamFrontierIndex
+  -> SeamFrontierIndex
+  -> Maybe SeparatedOrder
+separatedOrder axis left right leftFrontier rightFrontier
+  | numInnerFaces left <= 0 || numInnerFaces right <= 0 = Nothing
+  | chartMaximum axis leftFrontier < chartMinimum axis rightFrontier =
+      Just LeftBeforeRight
+  | chartMaximum axis rightFrontier < chartMinimum axis leftFrontier =
+      Just RightBeforeLeft
+  | otherwise = Nothing
+
+chartMinimum :: SeamAxis -> SeamFrontierIndex -> Double
+chartMinimum axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierXMinimum frontier
+    SeamAxisY -> seamFrontierYMinimum frontier
+
+chartMaximum :: SeamAxis -> SeamFrontierIndex -> Double
+chartMaximum axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierXMaximum frontier
+    SeamAxisY -> seamFrontierYMaximum frontier
+
+-- The opposite-sign branch cannot overflow in its sum. The same-sign branch
+-- cannot overflow in its difference.
+data SeamTangent = SeamTangent
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+
+tangentIndex :: SeamTangent -> Int
+tangentIndex (SeamTangent index _) = index
+{-# INLINE tangentIndex #-}
+
+data SeamTangents = SeamTangents
+  { seamLowerLeft :: !SeamTangent
+  , seamLowerRight :: !SeamTangent
+  , seamUpperLeft :: !SeamTangent
+  , seamUpperRight :: !SeamTangent
+  , seamFrontierPointReads :: {-# UNPACK #-} !Int
+  }
+
+seamTangents
+  :: SeamAxis
+  -> Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> Triangulation mode' vertex' directed' undirected' face'
+  -> SeamFrontierIndex
+  -> Maybe SeamTangents
+seamTangents axis left leftFrontier right rightFrontier = do
+  (lowerLeft, lowerRight, lowerReads) <- lowerTangent axis left leftFrontier right rightFrontier
+  (upperLeft, upperRight, upperReads) <- upperTangent axis left leftFrontier right rightFrontier
+  pure
+    SeamTangents
+      { seamLowerLeft = lowerLeft
+      , seamLowerRight = lowerRight
+      , seamUpperLeft = upperLeft
+      , seamUpperRight = upperRight
+      , seamFrontierPointReads = lowerReads + upperReads
+      }
+
+lowerTangent
+  :: SeamAxis
+  -> Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> Triangulation mode' vertex' directed' undirected' face'
+  -> SeamFrontierIndex
+  -> Maybe (SeamTangent, SeamTangent, Int)
+lowerTangent axis left leftFrontier right rightFrontier =
+  tangentWalk
+    axis
+    LT
+    (nextIndex (Seq.length (seamFrontierEdges leftFrontier)))
+    (previousIndex (Seq.length (seamFrontierEdges rightFrontier)))
+    left
+    leftFrontier
+    (chartLowerRightmost axis leftFrontier)
+    right
+    rightFrontier
+    (chartLowerLeftmost axis rightFrontier)
+
+upperTangent
+  :: SeamAxis
+  -> Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> Triangulation mode' vertex' directed' undirected' face'
+  -> SeamFrontierIndex
+  -> Maybe (SeamTangent, SeamTangent, Int)
+upperTangent axis left leftFrontier right rightFrontier =
+  tangentWalk
+    axis
+    GT
+    (previousIndex (Seq.length (seamFrontierEdges leftFrontier)))
+    (nextIndex (Seq.length (seamFrontierEdges rightFrontier)))
+    left
+    leftFrontier
+    (chartUpperRightmost axis leftFrontier)
+    right
+    rightFrontier
+    (chartUpperLeftmost axis rightFrontier)
+
+-- The walk carries both endpoints' coordinates: a step replaces exactly one
+-- endpoint, and the replacement is the neighbour whose coordinates the step's
+-- own test already read. A neighbour advances its side when it falls on the
+-- given side of the current chord; the left side is asked first. The read
+-- count is the two start reads plus two per step.
+tangentWalk
+  :: SeamAxis
+  -> Ordering
+  -> (Int -> Int)
+  -> (Int -> Int)
+  -> Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> Int
+  -> Triangulation mode' vertex' directed' undirected' face'
+  -> SeamFrontierIndex
+  -> Int
+  -> Maybe (SeamTangent, SeamTangent, Int)
+tangentWalk axis advancing stepLeft stepRight left leftFrontier leftStartIndex right rightFrontier rightStartIndex = do
+  leftStart <- frontierPointAt axis left leftFrontier leftStartIndex
+  rightStart <- frontierPointAt axis right rightFrontier rightStartIndex
+  walk leftStartIndex leftStart rightStartIndex rightStart 2
+ where
+  walk
+    :: Int
+    -> (Double, Double)
+    -> Int
+    -> (Double, Double)
+    -> Int
+    -> Maybe (SeamTangent, SeamTangent, Int)
+  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !frontierReads = do
+    let !candidateLeftIndex = stepLeft leftIndex
+        !candidateRightIndex = stepRight rightIndex
+    (candidateLeftX, candidateLeftY) <- frontierPointAt axis left leftFrontier candidateLeftIndex
+    (candidateRightX, candidateRightY) <- frontierPointAt axis right rightFrontier candidateRightIndex
+    let !nextReads = frontierReads + 2
+        !leftAdvances =
+          orient2dCoordinates leftX leftY rightX rightY candidateLeftX candidateLeftY == advancing
+        !rightAdvances =
+          orient2dCoordinates leftX leftY rightX rightY candidateRightX candidateRightY == advancing
+    if leftAdvances
+      then walk candidateLeftIndex (candidateLeftX, candidateLeftY) rightIndex (rightX, rightY) nextReads
+      else
+        if rightAdvances
+          then walk leftIndex (leftX, leftY) candidateRightIndex (candidateRightX, candidateRightY) nextReads
+          else do
+            leftEdge <- frontierEdgeAt leftFrontier leftIndex
+            rightEdge <- frontierEdgeAt rightFrontier rightIndex
+            pure
+              ( SeamTangent leftIndex leftEdge
+              , SeamTangent rightIndex rightEdge
+              , nextReads
+              )
+{-# INLINE tangentWalk #-}
+
+frontierEdgeAt :: SeamFrontierIndex -> Int -> Maybe Int
+frontierEdgeAt frontier index = Seq.lookup index (seamFrontierEdges frontier)
+
+chartLowerRightmost :: SeamAxis -> SeamFrontierIndex -> Int
+chartLowerRightmost axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierLowerRightmost frontier
+    SeamAxisY -> seamFrontierYLowerRightmost frontier
+
+chartLowerLeftmost :: SeamAxis -> SeamFrontierIndex -> Int
+chartLowerLeftmost axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierLowerLeftmost frontier
+    SeamAxisY -> seamFrontierYLowerLeftmost frontier
+
+chartUpperRightmost :: SeamAxis -> SeamFrontierIndex -> Int
+chartUpperRightmost axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierUpperRightmost frontier
+    SeamAxisY -> seamFrontierYUpperRightmost frontier
+
+chartUpperLeftmost :: SeamAxis -> SeamFrontierIndex -> Int
+chartUpperLeftmost axis frontier =
+  case axis of
+    SeamAxisX -> seamFrontierUpperLeftmost frontier
+    SeamAxisY -> seamFrontierYUpperLeftmost frontier
+
+frontierPointAt
+  :: SeamAxis
+  -> Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> Int
+  -> Maybe (Double, Double)
+frontierPointAt axis triangulation frontier index = do
+  edge <- frontierEdgeAt frontier index
+  let !vertex = topologyAt triangulation (4 * edge)
+  pure (chartPoint axis (physicalPoint triangulation vertex))
+{-# INLINE frontierPointAt #-}
+
+chartPoint :: SeamAxis -> (Double, Double) -> (Double, Double)
+chartPoint axis (x, y) =
+  case axis of
+    SeamAxisX -> (x, y)
+    SeamAxisY -> seamQuarterTurn (x, y)
+
+physicalPoint
+  :: Triangulation mode vertex directed undirected face
+  -> Int
+  -> (Double, Double)
+physicalPoint triangulation vertex =
+  ( triPointX triangulation `pagedUnsafeIndex` vertex
+  , triPointY triangulation `pagedUnsafeIndex` vertex
+  )
+
+nextIndex :: Int -> Int -> Int
+nextIndex size index
+  | index + 1 == size = 0
+  | otherwise = index + 1
+{-# INLINE nextIndex #-}
+
+previousIndex :: Int -> Int -> Int
+previousIndex size index
+  | index == 0 = size - 1
+  | otherwise = index - 1
+{-# INLINE previousIndex #-}
+
+
+mergeSeparated
+  :: forall outputMode leftMode rightMode vertex
+   . Triangulation leftMode vertex () () ()
+  -> Triangulation rightMode vertex () () ()
+  -> SeamTangents
+   -> Either
+      BuildError
+      (Triangulation outputMode vertex () () (), BuildStats)
+mergeSeparated
+  left
+  right
+  SeamTangents
+    { seamLowerLeft = SeamTangent _ lowerLeft
+    , seamLowerRight = SeamTangent _ lowerRight
+    , seamUpperLeft = SeamTangent _ upperLeft
+    , seamUpperRight = SeamTangent _ upperRight
+    } = runST $ do
+  mutable <- newMutableDcel unitElementDefaults (planarDcelCapacity totalVertices)
+  pointCapacityOutcome <- ensurePointCapacity mutable totalVertices
+  cellCapacityOutcome <-
+    ensureCellCapacity
+      mutable
+      ((leftDirected + rightDirected) `quot` 2 + 1)
+      (leftFaces + rightFaces - 2)
+  case (pointCapacityOutcome, cellCapacityOutcome) of
+    (Left obstruction, _) -> pure (Left obstruction)
+    (_, Left obstruction) -> pure (Left obstruction)
+    (Right (), Right ()) -> do
+      appendSourceVertices mutable left
+      appendSourceVertices mutable right
+      _ <- addEdgeBlock mutable ((leftDirected + rightDirected) `quot` 2)
+      _ <- addFaceBlock mutable (leftFaces + rightFaces - 2)
+      copySource mutable left 0 0 0
+      copySource mutable right leftVertices leftDirected (leftFaces - 1)
+      base <- spliceLowerTangent mutable leftDirected lowerLeft lowerRight
+      operation <- newOperationState (halfEdgeCapacity mutable)
+      stitched <-
+        stitchSeam
+          mutable
+          operation
+          base
+          (topologyAt left (4 * upperLeft))
+          (leftVertices + topologyAt right (4 * upperRight))
+          []
+          []
+      case stitched of
+        Left obstruction -> pure (Left obstruction)
+        Right _ -> do
+          statistics <- finalizeBuildStats operation
+          fmap (\triangulation -> (triangulation, statistics))
+            <$> freezeTriangulation mutable
+ where
+  !leftVertices = numVertices left
+  !rightVertices = numVertices right
+  !totalVertices = leftVertices + rightVertices
+  !leftDirected = numDirectedEdges left
+  !rightDirected = numDirectedEdges right
+  !leftFaces = numFaces left
+  !rightFaces = numFaces right
+
+stitchSeam
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> Int
+  -> [Int]
+  -> [Int]
+  -> ST s (Either BuildError (Int, [Int]))
+stitchSeam mutable operation base upperLeft upperRight seeds newFaces =
+  fmap (fmap (\(finalBase, faces, _) -> (finalBase, faces)))
+    ( stitchSeamWith
+        (\target ops edges -> legalizeEdges target ops edges >> pure (Right ()))
+        (SeamProgress 0 0 0 0 0 0)
+        mutable
+        operation
+        base
+        upperLeft
+        upperRight
+        seeds
+        newFaces
+    )
+
+stitchSeamPinned
+  :: (Int -> Bool)
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> [Int]
+  -> [Int]
+  -> ST s (Either BuildError (Int, [Int], SeamProgress))
+stitchSeamPinned targetFaceProtected mutable operation leftSize leftStart rightSize rightStart base upperLeft upperRight seeds newFaces =
+  stitchSeamWith
+    (\target ops edges -> legalizeEdgesPinned targetFaceProtected target ops edges >> pure (Right ()))
+    (SeamProgress leftSize leftStart (previousIndex leftSize leftStart) rightSize rightStart rightStart)
+    mutable
+    operation
+    base
+    upperLeft
+    upperRight
+    seeds
+    newFaces
+
+data SeamProgress = SeamProgress
+  { seamProgressLeftSize :: {-# UNPACK #-} !Int
+  , seamProgressLeftStart :: {-# UNPACK #-} !Int
+  , seamProgressLeftCursor :: {-# UNPACK #-} !Int
+  , seamProgressRightSize :: {-# UNPACK #-} !Int
+  , seamProgressRightStart :: {-# UNPACK #-} !Int
+  , seamProgressRightCursor :: {-# UNPACK #-} !Int
+  }
+
+stitchSeamWith
+  :: (MutableDcel s vertex directed undirected face -> OperationState s -> [Int] -> ST s (Either BuildError ()))
+  -> SeamProgress
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> Int
+  -> Int
+  -> [Int]
+  -> [Int]
+  -> ST s (Either BuildError (Int, [Int], SeamProgress))
+stitchSeamWith legalize progress mutable operation base upperLeft upperRight seeds newFaces = do
+  leftVertex <- readOrigin mutable base
+  rightVertex <- readOrigin mutable (base `xor` 1)
+  if leftVertex == upperLeft && rightVertex == upperRight
+    then do
+      legalized <- legalize mutable operation seeds
+      pure (fmap (const (base, newFaces, progress)) legalized)
+    else do
+      leftEdge <- readPrevious mutable base
+      rightEdge <- readNext mutable base
+      nextLeft <- readOrigin mutable leftEdge
+      nextRight <- readOrigin mutable (rightEdge `xor` 1)
+      leftTurn <- vertexOrientation mutable nextLeft leftVertex rightVertex
+      rightTurn <- vertexOrientation mutable leftVertex rightVertex nextRight
+      chooseRight <-
+        if rightVertex == upperRight
+          then pure False
+          else
+            if leftVertex == upperLeft
+              then pure True
+              else
+                case (leftTurn == GT, rightTurn == GT) of
+                  (True, True) ->
+                    (== GT) <$> vertexInCircle mutable nextLeft leftVertex rightVertex nextRight
+                  (False, True) -> pure True
+                  _ -> pure False
+      if chooseRight
+        then do
+          closed <- closeOuterTurn mutable base
+          case closed of
+            Left obstruction -> pure (Left obstruction)
+            Right nextBase ->
+              do
+                face <- readFace mutable (nextBase `xor` 1)
+                stitchSeamWith
+                  legalize
+                  (advanceRight progress)
+                  mutable
+                  operation
+                  nextBase
+                  upperLeft
+                  upperRight
+                  (base : rightEdge : seeds)
+                  (face : newFaces)
+        else do
+          closed <- closeOuterTurn mutable leftEdge
+          case closed of
+            Left obstruction -> pure (Left obstruction)
+            Right nextBase ->
+              do
+                face <- readFace mutable (nextBase `xor` 1)
+                stitchSeamWith
+                  legalize
+                  (advanceLeft progress)
+                  mutable
+                  operation
+                  nextBase
+                  upperLeft
+                  upperRight
+                  (leftEdge : base : seeds)
+                  (face : newFaces)
+
+advanceLeft :: SeamProgress -> SeamProgress
+advanceLeft progress@SeamProgress{seamProgressLeftSize, seamProgressLeftCursor} =
+  progress
+    { seamProgressLeftCursor =
+        previousIndex seamProgressLeftSize seamProgressLeftCursor
+    }
+
+advanceRight :: SeamProgress -> SeamProgress
+advanceRight progress@SeamProgress{seamProgressRightSize, seamProgressRightCursor} =
+  progress
+    { seamProgressRightCursor =
+        nextIndex seamProgressRightSize seamProgressRightCursor
+    }
+
+combineSeamFrontier
+  :: Triangulation mode vertex directed undirected face
+  -> SeamFrontierIndex
+  -> SeamFrontierIndex
+  -> Int
+  -> Int
+  -> Seq.Seq Int
+  -> Seq.Seq Int
+  -> SeamProgress
+  -> Either BuildError SeamFrontierIndex
+combineSeamFrontier triangulation leftFrontier rightFrontier leftEdgeOffset rightEdgeOffset upperBridge lowerBridge progress
+  | seamProgressLeftSize progress /= Seq.length (seamFrontierEdges leftFrontier) = Left SeamFrontierUnavailable
+  | seamProgressRightSize progress /= Seq.length (seamFrontierEdges rightFrontier) = Left SeamFrontierUnavailable
+  | otherwise = do
+  let leftEdges = translatedEdges leftFrontier leftEdgeOffset
+      rightEdges = translatedEdges rightFrontier rightEdgeOffset
+  rightResidual <-
+    cycleSlice
+      rightEdges
+      (seamProgressRightSize progress)
+      (seamProgressRightCursor progress)
+      (previousIndex
+         (seamProgressRightSize progress)
+         (seamProgressRightStart progress))
+  leftResidual <-
+    cycleSlice
+      leftEdges
+      (seamProgressLeftSize progress)
+      (seamProgressLeftStart progress)
+      (seamProgressLeftCursor progress)
+  let !outputEdges =
+        upperBridge
+          Seq.>< rightResidual
+          Seq.>< lowerBridge
+          Seq.>< leftResidual
+      !rightOffset = Seq.length upperBridge
+      !lowerOffset = rightOffset + Seq.length rightResidual
+      !leftOffset = lowerOffset + Seq.length lowerBridge
+      !rightEnd = previousIndex (seamProgressRightSize progress) progressRightStart
+      !leftEnd = seamProgressLeftCursor progress
+      !candidatePositions =
+        IntSet.toAscList
+          ( IntSet.fromList
+              ( residualBoundaryPositions 0 upperBridge
+                  ++ residualBoundaryPositions rightOffset rightResidual
+                  ++ residualBoundaryPositions lowerOffset lowerBridge
+                  ++ residualBoundaryPositions leftOffset leftResidual
+                  ++ sourceAnchorPositions
+                    rightOffset
+                    (seamProgressRightCursor progress)
+                    rightEnd
+                    (seamProgressRightSize progress)
+                    rightFrontier
+                  ++ sourceAnchorPositions
+                    leftOffset
+                    progressLeftStart
+                    leftEnd
+                    (seamProgressLeftSize progress)
+                    leftFrontier
+              )
+          )
+  (minimumX, maximumX, lowerRight, lowerLeft, upperRight, upperLeft) <-
+    summarizeOutputChart id candidatePositions outputEdges
+  (minimumY, maximumY, yLowerRight, yLowerLeft, yUpperRight, yUpperLeft) <-
+    summarizeOutputChart seamQuarterTurn candidatePositions outputEdges
+  pure
+    SeamFrontierIndex
+      { seamFrontierEdges = outputEdges
+      , seamFrontierXMinimum = minimumX
+      , seamFrontierXMaximum = maximumX
+      , seamFrontierLowerRightmost = lowerRight
+      , seamFrontierLowerLeftmost = lowerLeft
+      , seamFrontierUpperRightmost = upperRight
+      , seamFrontierUpperLeftmost = upperLeft
+      , seamFrontierYMinimum = minimumY
+      , seamFrontierYMaximum = maximumY
+      , seamFrontierYLowerRightmost = yLowerRight
+      , seamFrontierYLowerLeftmost = yLowerLeft
+      , seamFrontierYUpperRightmost = yUpperRight
+      , seamFrontierYUpperLeftmost = yUpperLeft
+      }
+ where
+  progressRightStart = seamProgressRightStart progress
+  progressLeftStart = seamProgressLeftStart progress
+
+  translatedEdges :: SeamFrontierIndex -> Int -> Seq.Seq Int
+  translatedEdges frontier offset
+    | offset == 0 = seamFrontierEdges frontier
+    | otherwise = fmap (+ offset) (seamFrontierEdges frontier)
+
+  cycleSlice
+    :: Seq.Seq Int
+    -> Int
+    -> Int
+    -> Int
+    -> Either BuildError (Seq.Seq Int)
+  cycleSlice edges size start end
+    | size <= 0 || start < 0 || end < 0 || start >= size || end >= size = Left SeamFrontierUnavailable
+    | start <= end = Right (Seq.take (end - start + 1) (Seq.drop start edges))
+    | otherwise =
+        Right
+          ( Seq.drop start edges
+              Seq.>< Seq.take (end + 1) edges
+          )
+
+  residualBoundaryPositions :: Int -> Seq.Seq Int -> [Int]
+  residualBoundaryPositions offset residual =
+    case Seq.length residual of
+      0 -> []
+      lengthOfResidual -> [offset, offset + lengthOfResidual - 1]
+
+  sourceAnchorPositions
+    :: Int
+    -> Int
+    -> Int
+    -> Int
+    -> SeamFrontierIndex
+    -> [Int]
+  sourceAnchorPositions offset start end size frontier =
+    [ offset + position
+    | anchor <- sourceAnchors frontier
+    , Right position <- [intervalPosition size anchor start end]
+    ]
+
+  sourceAnchors :: SeamFrontierIndex -> [Int]
+  sourceAnchors frontier =
+    [ seamFrontierLowerRightmost frontier
+    , seamFrontierLowerLeftmost frontier
+    , seamFrontierUpperRightmost frontier
+    , seamFrontierUpperLeftmost frontier
+    , seamFrontierYLowerRightmost frontier
+    , seamFrontierYLowerLeftmost frontier
+    , seamFrontierYUpperRightmost frontier
+    , seamFrontierYUpperLeftmost frontier
+    ]
+
+  summarizeOutputChart
+    :: ((Double, Double) -> (Double, Double))
+    -> [Int]
+    -> Seq.Seq Int
+    -> Either BuildError (Double, Double, Int, Int, Int, Int)
+  summarizeOutputChart transform candidatePositions outputEdges = do
+    candidates <-
+      traverse
+        (\index -> do
+           edge <- maybe (Left SeamFrontierUnavailable) Right (Seq.lookup index outputEdges)
+           let !vertex = topologyAt triangulation (4 * edge)
+           pure (index, physicalPoint triangulation vertex))
+        candidatePositions
+    maybe (Left SeamFrontierUnavailable) Right
+      (summarizeSeamChart transform candidates)
+
+  intervalPosition :: Int -> Int -> Int -> Int -> Either BuildError Int
+  intervalPosition size target start end
+    | target < 0 || target >= size = Left SeamFrontierUnavailable
+    | start <= end =
+        if target >= start && target <= end
+          then Right (target - start)
+          else Left SeamFrontierUnavailable
+    | target >= start = Right (target - start)
+    | target <= end = Right (size - start + target)
+    | otherwise = Left SeamFrontierUnavailable
+
+vertexOrientation
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Ordering
+vertexOrientation mutable a b c = do
+  ax <- readPointX mutable a
+  ay <- readPointY mutable a
+  bx <- readPointX mutable b
+  by <- readPointY mutable b
+  cx <- readPointX mutable c
+  cy <- readPointY mutable c
+  pure $! orient2dCoordinates ax ay bx by cx cy
+{-# INLINE vertexOrientation #-}
+
+vertexInCircle
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Ordering
+vertexInCircle mutable a b c d = do
+  ax <- readPointX mutable a
+  ay <- readPointY mutable a
+  bx <- readPointX mutable b
+  by <- readPointY mutable b
+  cx <- readPointX mutable c
+  cy <- readPointY mutable c
+  dx <- readPointX mutable d
+  dy <- readPointY mutable d
+  pure $! inCircleCoordinates ax ay bx by cx cy dx dy
+{-# INLINE vertexInCircle #-}
+
+appendSourceVertices
+  :: MutableDcel s vertex () () ()
+  -> Triangulation mode vertex () () ()
+  -> ST s ()
+appendSourceVertices mutable source =
+  forRange 0 (numVertices source) $ \vertex -> do
+    _ <-
+      appendVertexCoordinates
+        mutable
+        (triPointX source `pagedUnsafeIndex` vertex)
+        (triPointY source `pagedUnsafeIndex` vertex)
+        (vertexData source (VertexId (fromIntegral vertex)))
+    pure ()
+
+copySource
+  :: MutableDcel s vertex () () ()
+  -> Triangulation mode vertex () () ()
+  -> Int
+  -> Int
+  -> Int
+  -> ST s ()
+copySource mutable source vertexOffset edgeOffset faceOffset = do
+  forRange 0 (numDirectedEdges source) $ \edge -> do
+    let !target = edgeOffset + edge
+        !sourceBase = 4 * edge
+        !sourceFace = topologyAt source (sourceBase + 3)
+        !targetFace = if sourceFace == 0 then 0 else faceOffset + sourceFace
+    writeOrigin mutable target (vertexOffset + topologyAt source sourceBase)
+    writeNext mutable target (edgeOffset + topologyAt source (sourceBase + 1))
+    writePrevious mutable target (edgeOffset + topologyAt source (sourceBase + 2))
+    writeFace mutable target targetFace
+  forRange 0 (numVertices source) $ \vertex ->
+    case vertexOutEdge source (VertexId (fromIntegral vertex)) of
+      Nothing -> markConnected mutable (vertexOffset + vertex) (-1)
+      Just (DirectedEdgeId edge) ->
+        markConnected mutable (vertexOffset + vertex) (edgeOffset + fromIntegral edge)
+  forRange 1 (numFaces source) $ \face ->
+    case adjacentEdge source (FaceId (fromIntegral face)) of
+      Nothing -> writeFaceEdge mutable (faceOffset + face) (-1)
+      Just (DirectedEdgeId edge) ->
+        writeFaceEdge mutable (faceOffset + face) (edgeOffset + fromIntegral edge)
+
+copySourceConstraints
+  :: MutableDcel s vertex () () ()
+  -> Int
+  -> Triangulation 'Constrained vertex () () ()
+  -> ST s ()
+copySourceConstraints mutable directedEdgeOffset source =
+  traverse_
+    (\(UndirectedEdgeId edge) ->
+       ()
+         <$ setConstraint
+           mutable
+           (directedEdgeOffset + 2 * fromIntegral edge)
+    )
+    (constraintEdges source)
+
+spliceLowerTangent
+  :: MutableDcel s vertex () () ()
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Int
+spliceLowerTangent mutable rightEdgeOffset leftOuter rightOuterSource = do
+  let !rightOuter = rightEdgeOffset + rightOuterSource
+  leftVertex <- readOrigin mutable leftOuter
+  rightVertex <- readOrigin mutable rightOuter
+  leftPrevious <- readPrevious mutable leftOuter
+  rightPrevious <- readPrevious mutable rightOuter
+  (forward, backward) <- addEdge mutable leftVertex rightVertex
+  writeFace mutable forward 0
+  writeFace mutable backward 0
+  linkEdges mutable leftPrevious forward
+  linkEdges mutable forward rightOuter
+  linkEdges mutable rightPrevious backward
+  linkEdges mutable backward leftOuter
+  writeFaceEdge mutable 0 forward
+  writeVertexOut mutable leftVertex forward
+  writeVertexOut mutable rightVertex backward
+  pure forward
+{-# INLINE spliceLowerTangent #-}
+
+topologyAt
+  :: Triangulation mode vertex directed undirected face
+  -> Int
+  -> Int
+topologyAt triangulation slot =
+  fromIntegral (triHalfTopology triangulation `pagedUnsafeIndex` slot)
+{-# INLINE topologyAt #-}
+
+
+forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
+forRange from to action = go from
+ where
+  go !index
+    | index >= to = pure ()
+    | otherwise = action index >> go (index + 1)
+{-# INLINE forRange #-}
diff --git a/src-build/Moonlight/Planar/Internal/Join/SiteSet.hs b/src-build/Moonlight/Planar/Internal/Join/SiteSet.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Join/SiteSet.hs
@@ -0,0 +1,395 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Exact transient site sections derived from the authoritative coordinate
+-- planes. This is the sole owner of coordinate-set classification for joins,
+-- set algebra, and constrained union. Payloads travel as annotations; they do
+-- not participate in site identity.
+module Moonlight.Planar.Internal.Join.SiteSet
+  ( SiteSet
+  , siteSetFromTriangulation
+  , siteSupportFromTriangulation
+  , siteSetSize
+  , siteSetRelation
+  , siteRelationFromTriangulations
+  , siteSetUnionWith
+  , siteSetIntersectionWith
+  , siteSetDifference
+  , siteSetSymmetricDifferenceFromTriangulations
+  , siteSetAssocs
+  , siteSetPoints
+  , delaunayFromSiteSet
+  , rebuildCanonicalSiteSet
+  ) where
+
+import Control.Monad.ST (ST, runST)
+import qualified Data.Map.Strict as Map
+import qualified Data.Map.Merge.Strict as MapMerge
+import Data.Functor.Const (Const (..))
+import Data.Maybe (isJust)
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.BulkLoad (delaunayFromAdmittedDistinctAscending)
+import Moonlight.Planar.Dcel (numVertices, vertexData, vertexPoint)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Internal.Canonical (canonicalizeKnownAscending)
+import Moonlight.Planar.Handles.Iterators.FixedIterators
+  ( foldVertices'
+  )
+import Moonlight.Planar.Internal.Paged
+  ( pagedUnsafeIndex
+  , toVector
+  )
+import Moonlight.Planar.Internal.PointIndex
+  ( MutablePointIndex
+  , lookupMutablePoint
+  , lookupPointIndex
+  , newMutablePointIndex
+  , seedMutablePointIndex
+  )
+import Moonlight.Planar.Internal.Representation
+  ( BuildResult (buildTriangulation)
+  , Triangulation (..)
+  )
+import Moonlight.Planar.Internal.Types (BuildError, ConstraintMode (Unconstrained), SiteRelation (..), unitElementDefaults)
+import Moonlight.Planar.Point (Point (..), queryPointValue)
+import Moonlight.Planar.Internal.Point (QueryPoint (..))
+
+newtype SiteSet annotation = SiteSet (Map.Map QueryPoint annotation)
+
+-- Strict sufficient statistics for the one-pass ordered-map descent used by
+-- 'siteSetRelation'. Keeping the census strict prevents a relation query from
+-- replacing an intermediate map allocation with a chain of monoidal thunks.
+data SiteRelationCensus = SiteRelationCensus !Int !Int !Int
+
+instance Semigroup SiteRelationCensus where
+  SiteRelationCensus leftA rightA overlapA <> SiteRelationCensus leftB rightB overlapB =
+    SiteRelationCensus
+      (leftA + leftB)
+      (rightA + rightB)
+      (overlapA + overlapB)
+
+instance Monoid SiteRelationCensus where
+  mempty = SiteRelationCensus 0 0 0
+
+siteSetFromTriangulation
+  :: Triangulation mode annotation directed undirected face
+  -> SiteSet annotation
+siteSetFromTriangulation triangulation =
+  SiteSet
+    ( Map.fromList
+        [ ( QueryPoint
+              ( Point
+                  (triPointX triangulation `pagedUnsafeIndex` vertex)
+                  (triPointY triangulation `pagedUnsafeIndex` vertex)
+              )
+          , vertexData triangulation (VertexId (fromIntegral vertex))
+          )
+        | vertex <- [0 .. numVertices triangulation - 1]
+        ]
+    )
+
+-- | Coordinate support without touching the boxed annotation plane. Order and
+-- set identity are geometric observations; callers that discard annotations
+-- should not pay a boxed-page read per vertex merely to manufacture ignored
+-- map values.
+siteSupportFromTriangulation
+  :: Triangulation mode vertex directed undirected face
+  -> SiteSet ()
+siteSupportFromTriangulation triangulation =
+  SiteSet
+    ( Map.fromList
+        [ ( QueryPoint
+              ( Point
+                  (triPointX triangulation `pagedUnsafeIndex` vertex)
+                  (triPointY triangulation `pagedUnsafeIndex` vertex)
+              )
+          , ()
+          )
+        | vertex <- [0 .. numVertices triangulation - 1]
+        ]
+    )
+{-# INLINE siteSupportFromTriangulation #-}
+
+siteSetSize :: SiteSet annotation -> Int
+siteSetSize (SiteSet sites) = Map.size sites
+{-# INLINE siteSetSize #-}
+
+siteSetRelation
+  :: SiteSet leftAnnotation
+  -> SiteSet rightAnnotation
+  -> SiteRelation
+siteSetRelation (SiteSet left) (SiteSet right) =
+  siteRelationFromCardinalities
+    (leftOnly + overlap)
+    (rightOnly + overlap)
+    overlap
+ where
+  SiteRelationCensus leftOnly rightOnly overlap =
+    getConst
+      ( MapMerge.mergeA
+          (MapMerge.traverseMissing (\_ _ -> Const (SiteRelationCensus 1 0 0)))
+          (MapMerge.traverseMissing (\_ _ -> Const (SiteRelationCensus 0 1 0)))
+          (MapMerge.zipWithAMatched (\_ _ _ -> Const (SiteRelationCensus 0 0 1)))
+          left
+          right
+      )
+{-# INLINE siteSetRelation #-}
+
+siteRelationFromTriangulations
+  :: Triangulation leftMode leftAnnotation leftDirected leftUndirected leftFace
+  -> Triangulation rightMode rightAnnotation rightDirected rightUndirected rightFace
+  -> SiteRelation
+siteRelationFromTriangulations left right =
+  siteRelationFromCardinalities leftCount rightCount overlap
+ where
+  !leftCount = numVertices left
+  !rightCount = numVertices right
+  !overlap
+    | leftCount <= rightCount = exactOverlapCount right left
+    | otherwise = exactOverlapCount left right
+{-# INLINE siteRelationFromTriangulations #-}
+
+siteRelationFromCardinalities :: Int -> Int -> Int -> SiteRelation
+siteRelationFromCardinalities leftCount rightCount overlap
+  | leftCount == rightCount && overlap == leftCount = EqualSites
+  | overlap == leftCount = LeftProperSubset
+  | overlap == rightCount = RightProperSubset
+  | overlap == 0 = DisjointSites
+  | otherwise = PartialOverlap overlap
+{-# INLINE siteRelationFromCardinalities #-}
+
+exactOverlapCount
+  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
+  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
+  -> Int
+exactOverlapCount source indexed =
+  either
+    (const (persistentExactOverlapCount source indexed))
+    id
+    (transientExactOverlapCount source indexed)
+{-# INLINE exactOverlapCount #-}
+
+transientExactOverlapCount
+  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
+  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
+  -> Either BuildError Int
+transientExactOverlapCount source indexed = runST $ do
+  pointIndex <- newMutablePointIndex (numVertices indexed)
+  seeded <-
+    seedMutablePointIndex
+      pointIndex
+      (numVertices indexed)
+      (readCoordinateX indexed)
+      (readCoordinateY indexed)
+  case seeded of
+    Left failure -> pure (Left failure)
+    Right () ->
+      fmap Right
+        ( U.ifoldM'
+            (\count vertex x -> do
+               let y = triPointY source `pagedUnsafeIndex` vertex
+               match <-
+                 lookupMutablePoint
+                   pointIndex
+                   (readCoordinateX indexed)
+                   (readCoordinateY indexed)
+                   x
+                   y
+               pure (if isJust match then count + 1 else count)
+            )
+            0
+            (toVector (triPointX source))
+        )
+{-# INLINE transientExactOverlapCount #-}
+
+persistentExactOverlapCount
+  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
+  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
+  -> Int
+persistentExactOverlapCount source indexed =
+  foldVertices'
+    source
+    (\count vertex ->
+       if pointOccursIn indexed (vertexPoint source vertex)
+         then count + 1
+         else count
+    )
+    0
+{-# INLINE persistentExactOverlapCount #-}
+
+siteSetUnionWith
+  :: (annotation -> annotation -> annotation)
+  -> SiteSet annotation
+  -> SiteSet annotation
+  -> SiteSet annotation
+siteSetUnionWith combine (SiteSet left) (SiteSet right) =
+  SiteSet (Map.unionWith combine left right)
+{-# INLINE siteSetUnionWith #-}
+
+siteSetIntersectionWith
+  :: (leftAnnotation -> rightAnnotation -> annotation)
+  -> SiteSet leftAnnotation
+  -> SiteSet rightAnnotation
+  -> SiteSet annotation
+siteSetIntersectionWith combine (SiteSet left) (SiteSet right) =
+  SiteSet (Map.intersectionWith combine left right)
+{-# INLINE siteSetIntersectionWith #-}
+
+siteSetDifference
+  :: SiteSet annotation
+  -> SiteSet other
+  -> SiteSet annotation
+siteSetDifference (SiteSet left) (SiteSet right) = SiteSet (Map.difference left right)
+{-# INLINE siteSetDifference #-}
+
+siteSetSymmetricDifferenceFromTriangulations
+  :: Triangulation leftMode annotation leftDirected leftUndirected leftFace
+  -> Triangulation rightMode annotation rightDirected rightUndirected rightFace
+  -> Either BuildError (SiteSet annotation)
+siteSetSymmetricDifferenceFromTriangulations left right
+  | numVertices left >= numVertices right = indexedSymmetricDifference left right
+  | otherwise = indexedSymmetricDifference right left
+{-# INLINE siteSetSymmetricDifferenceFromTriangulations #-}
+
+indexedSymmetricDifference
+  :: forall sourceMode annotation sourceDirected sourceUndirected sourceFace
+            indexedMode indexedDirected indexedUndirected indexedFace
+  . Triangulation sourceMode annotation sourceDirected sourceUndirected sourceFace
+  -> Triangulation indexedMode annotation indexedDirected indexedUndirected indexedFace
+  -> Either BuildError (SiteSet annotation)
+indexedSymmetricDifference source indexed =
+  fmap (SiteSet . Map.fromList) (runST collectExclusiveAssociations)
+ where
+  collectExclusiveAssociations
+    :: forall state. ST state (Either BuildError [(QueryPoint, annotation)])
+  collectExclusiveAssociations = do
+    pointIndex <- newMutablePointIndex (numVertices indexed)
+    seeded <-
+      seedMutablePointIndex
+        pointIndex
+        (numVertices indexed)
+        (readCoordinateX indexed)
+        (readCoordinateY indexed)
+    case seeded of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        matchedIndexedVertices <- MUV.replicate (numVertices indexed) False
+        sourceExclusive <-
+          U.ifoldM'
+            (collectSourceExclusive pointIndex matchedIndexedVertices)
+            []
+            (toVector (triPointX source))
+        indexedExclusive <-
+          U.ifoldM'
+            (collectIndexedExclusive matchedIndexedVertices)
+            []
+            (toVector (triPointX indexed))
+        pure (Right (sourceExclusive <> indexedExclusive))
+   where
+    collectSourceExclusive
+      :: MutablePointIndex state
+      -> MUV.MVector state Bool
+      -> [(QueryPoint, annotation)]
+      -> Int
+      -> Double
+      -> ST state [(QueryPoint, annotation)]
+    collectSourceExclusive pointIndex matchedIndexedVertices associations rawVertex x = do
+      let vertex = VertexId (fromIntegral rawVertex)
+          y = triPointY source `pagedUnsafeIndex` rawVertex
+          point = QueryPoint (Point x y)
+      match <-
+        lookupMutablePoint
+          pointIndex
+          (readCoordinateX indexed)
+          (readCoordinateY indexed)
+          x
+          y
+      case match of
+        Nothing -> pure ((point, vertexData source vertex) : associations)
+        Just indexedVertex -> do
+          MUV.unsafeWrite matchedIndexedVertices indexedVertex True
+          pure associations
+
+    collectIndexedExclusive
+      :: MUV.MVector state Bool
+      -> [(QueryPoint, annotation)]
+      -> Int
+      -> Double
+      -> ST state [(QueryPoint, annotation)]
+    collectIndexedExclusive matchedIndexedVertices associations rawVertex x = do
+      let vertex = VertexId (fromIntegral rawVertex)
+          point = QueryPoint (Point x (triPointY indexed `pagedUnsafeIndex` rawVertex))
+      matched <- MUV.unsafeRead matchedIndexedVertices rawVertex
+      pure
+        ( if matched
+            then associations
+            else (point, vertexData indexed vertex) : associations
+        )
+{-# INLINE indexedSymmetricDifference #-}
+
+readCoordinateX
+  :: Triangulation mode annotation directed undirected face
+  -> Int
+  -> ST state Double
+readCoordinateX triangulation vertex =
+  pure (triPointX triangulation `pagedUnsafeIndex` vertex)
+{-# INLINE readCoordinateX #-}
+
+readCoordinateY
+  :: Triangulation mode annotation directed undirected face
+  -> Int
+  -> ST state Double
+readCoordinateY triangulation vertex =
+  pure (triPointY triangulation `pagedUnsafeIndex` vertex)
+{-# INLINE readCoordinateY #-}
+
+pointOccursIn
+  :: Triangulation mode annotation directed undirected face
+  -> Point
+  -> Bool
+pointOccursIn triangulation = isJust . lookupPointIn triangulation
+{-# INLINE pointOccursIn #-}
+
+lookupPointIn
+  :: Triangulation mode annotation directed undirected face
+  -> Point
+  -> Maybe Int
+lookupPointIn triangulation =
+  lookupPointIndex
+    (triPointX triangulation)
+    (triPointY triangulation)
+    (triPointIndex triangulation)
+{-# INLINE lookupPointIn #-}
+
+siteSetAssocs :: SiteSet annotation -> [(QueryPoint, annotation)]
+siteSetAssocs (SiteSet sites) = Map.toAscList sites
+{-# INLINE siteSetAssocs #-}
+
+siteSetPoints :: SiteSet annotation -> V.Vector (Point)
+siteSetPoints (SiteSet sites) = V.fromList (fmap queryPointValue (Map.keys sites))
+{-# INLINE siteSetPoints #-}
+
+-- | Publish an admitted exact site section through the one bulk-construction
+-- engine. Its Map order is the trusted strict lexicographic vertex order; the
+-- circle sweep still establishes the algorithm's radial execution order.
+delaunayFromSiteSet
+  :: SiteSet annotation
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+delaunayFromSiteSet sites =
+  buildTriangulation
+    <$> delaunayFromAdmittedDistinctAscending
+      unitElementDefaults
+      (V.fromList (siteSetAssocs sites))
+{-# INLINE delaunayFromSiteSet #-}
+
+-- | Canonical finite-set publication. The preceding SiteSet build preserves
+-- lexicographic vertex order, so Canonical need not rediscover that fact.
+rebuildCanonicalSiteSet
+  :: SiteSet annotation
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+rebuildCanonicalSiteSet sites =
+  delaunayFromSiteSet sites >>= canonicalizeKnownAscending
+{-# INLINE rebuildCanonicalSiteSet #-}
diff --git a/src-build/Moonlight/Planar/Internal/Location.hs b/src-build/Moonlight/Planar/Internal/Location.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Location.hs
@@ -0,0 +1,388 @@
+{-# LANGUAGE BangPatterns #-}
+
+module Moonlight.Planar.Internal.Location
+  ( MutableLocation (..)
+  , locateMutable
+  , locateMutableWithin
+  , locateLineMutable
+  , visibleOuterEdge
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Bits ((.&.))
+import qualified Data.IntSet as IntSet
+import Data.STRef (readSTRef, writeSTRef)
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.FaceProbe
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , maxCounter
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , UndirectedEdgeId (..)
+  )
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Math (squaredDistanceWide)
+import Moonlight.Planar.Types
+import Moonlight.Planar.BuildStats (BuildMetric (LocationWalkSteps, LocationFallbacks, LocationMaxWalk))
+import Moonlight.Planar.Point (Point (..))
+
+data MutableLocation
+  = MutableEmpty
+  | MutableOnVertex {-# UNPACK #-} !Int
+  | MutableOnEdge {-# UNPACK #-} !Int
+  | MutableInFace {-# UNPACK #-} !Int
+  | MutableOutsideHull {-# UNPACK #-} !Int
+  deriving stock (Eq, Ord, Show)
+
+locateMutable
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> ST s (Either BuildError MutableLocation)
+locateMutable mutable operation hint rawQuery = do
+  locateMutableWithBarrier Nothing mutable operation hint rawQuery
+
+-- | Locate from a checked local face while refusing to walk across the exact
+-- interface section. The ordinary locator remains unchanged for unrestricted
+-- refinement; the local interpreter supplies only its compact barrier pairs.
+locateMutableWithin
+  :: IntSet.IntSet
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> ST s (Either BuildError MutableLocation)
+locateMutableWithin interfacePairs =
+  locateMutableWithBarrier (Just interfacePairs)
+
+locateMutableWithBarrier
+  :: Maybe IntSet.IntSet
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> ST s (Either BuildError MutableLocation)
+locateMutableWithBarrier barrier mutable operation hint rawQuery = do
+  connected <- connectedCount mutable
+  faces <- faceCount mutable
+  let !query = canonicalPoint rawQuery
+  case connected of
+    0 -> pure (Right MutableEmpty)
+    _ | faces <= 1 -> Right <$> locateLineMutable mutable query
+    -- A hint is the caller vouching for adjacency, so the exact walk starts
+    -- there directly; only an unhinted locate buys the vertex descent.
+    _ | Just face <- hint, face > 0 && face < faces -> walkWithBarrier barrier mutable operation query face
+    _ -> do
+      start <- chooseStartFace mutable hint
+      anchor <- readFaceEdge mutable start
+      if anchor < 0
+        then walkWithBarrier barrier mutable operation query start
+        else do
+          origin <- readOrigin mutable anchor
+          nearest <- descendToNearest mutable operation query origin
+          face <- incidentInnerFace mutable nearest start
+          walkWithBarrier barrier mutable operation query face
+
+-- | Greedy first-improvement descent through vertex neighbours: hop to the
+-- first neighbour strictly closer to the query until none improves. Distance
+-- is a heuristic only — exact containment belongs to 'walkWithBarrier' — so the widened
+-- comparison can lengthen the path but never move the located answer.
+--
+-- Strictly decreasing distance bounds the hops and nothing else: no vertex is
+-- entered twice, but the star rotation terminates only if @reverse . previous@
+-- closes an orbit, which is a property of the links rather than of the
+-- geometry. Disjoint stars over distinct entered vertices spend at most one
+-- step per outgoing half-edge, so a budget past 'directedEdgeCount' cannot be
+-- exhausted while the links are well formed. Exhausting it means they are not,
+-- and yields the vertex in hand rather than an error — a truncated descent can
+-- only lengthen 'walkWithBarrier', never move what it finds.
+descendToNearest
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Point
+  -> Int
+  -> ST s Int
+descendToNearest mutable operation query start = do
+  halfEdges <- directedEdgeCount mutable
+  point <- pointAt mutable start
+  settle (halfEdges + 1) start (squaredDistanceWide query point)
+ where
+  settle
+    :: Int
+    -> Int
+    -> Double
+    -> ST s Int
+  settle !budget !vertex !best = do
+    first <- readVertexOut mutable vertex
+    if first < 0
+      then pure vertex
+      else rotate budget vertex best first first
+  rotate
+    :: Int
+    -> Int
+    -> Double
+    -> Int
+    -> Int
+    -> ST s Int
+  rotate !budget !vertex !best !first !edge
+    | budget <= 0 = pure vertex
+    | otherwise = do
+        neighbour <- readOrigin mutable (reverseIndex edge)
+        point <- pointAt mutable neighbour
+        let !candidate = squaredDistanceWide query point
+        if candidate < best
+          then do
+            addCounter operation (Reported LocationWalkSteps) 1
+            settle (budget - 1) neighbour candidate
+          else do
+            previousEdge <- readPrevious mutable edge
+            let !outgoing = reverseIndex previousEdge
+            if outgoing == first then pure vertex else rotate (budget - 1) vertex best first outgoing
+
+-- | An inner face incident to the vertex, so the exact walk starts adjacent
+-- to where the descent settled; the caller's face stands in when the star
+-- offers none.
+incidentInnerFace
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> ST s Int
+incidentInnerFace mutable vertex fallback = do
+  first <- readVertexOut mutable vertex
+  if first < 0 then pure fallback else rotate first first
+ where
+  rotate !first !edge = do
+    face <- readFace mutable edge
+    if face > 0
+      then pure face
+      else do
+        previousEdge <- readPrevious mutable edge
+        let !outgoing = reverseIndex previousEdge
+        if outgoing == first then pure fallback else rotate first outgoing
+
+locateLineMutable :: MutableDcel s vertex directed undirected face -> Point -> ST s MutableLocation
+locateLineMutable mutable rawQuery = do
+  let !query = canonicalPoint rawQuery
+  vertices <- pointCount mutable
+  exactVertex <- findVertex query vertices 0
+  case exactVertex of
+    Just vertex -> pure (MutableOnVertex vertex)
+    Nothing -> do
+      halfEdges <- directedEdgeCount mutable
+      if halfEdges == 0
+        then pure (MutableOutsideHull 0)
+        else do
+          onEdge <- findSegment query halfEdges 0
+          case onEdge of
+            Just edge -> pure (MutableOnEdge edge)
+            Nothing -> do
+              firstConnected <- findConnected vertices 0
+              firstOut <- readVertexOut mutable firstConnected
+              let edge = if firstOut < 0 then 0 else firstOut
+              from <- edgeOriginPoint mutable edge
+              to <- edgeOriginPoint mutable (reverseIndex edge)
+              case orient2d from to query of
+                GT -> pure (MutableOutsideHull (orientOuter edge))
+                LT -> pure (MutableOutsideHull (orientOuter (reverseIndex edge)))
+                EQ -> MutableOutsideHull <$> nearestTerminalEdge query halfEdges edge
+ where
+  findVertex !query !limit !index
+    | index >= limit = pure Nothing
+    | otherwise = do
+        connected <- isConnected mutable index
+        if not connected
+          then findVertex query limit (index + 1)
+          else do
+            point <- pointAt mutable index
+            if point == query then pure (Just index) else findVertex query limit (index + 1)
+
+  findSegment !query !limit !edge
+    | edge >= limit = pure Nothing
+    | otherwise = do
+        let normalized = edge .&. complementOne
+        from <- edgeOriginPoint mutable normalized
+        to <- edgeOriginPoint mutable (reverseIndex normalized)
+        if onClosedSegment from to query
+          then pure (Just normalized)
+          else findSegment query limit (normalized + 2)
+
+  findConnected !limit !index
+    | index >= limit = pure 0
+    | otherwise = do
+        connected <- isConnected mutable index
+        if connected then pure index else findConnected limit (index + 1)
+
+  orientOuter :: Int -> Int
+  orientOuter edge = edge
+
+  -- A degenerate chain's outer cycle doubles back exactly at its two terminal
+  -- vertices, so @next e == reverse e@ characterises the edge entering a
+  -- terminal. Collinear extension must attach to the terminal nearest the
+  -- query, which for three or more vertices is not an endpoint of any single
+  -- arbitrary edge.
+  nearestTerminalEdge !query !limit !fallback = go 0 Nothing
+   where
+    go !edge !best
+      | edge >= limit = pure (maybe fallback fst best)
+      | otherwise = do
+          edgeNext <- readNext mutable edge
+          if edgeNext /= reverseIndex edge
+            then go (edge + 1) best
+            else do
+              let !terminal = reverseIndex edge
+              point <- edgeOriginPoint mutable terminal
+              let !candidate = squaredDistanceWide query point
+              case best of
+                Just (_, closest) | closest <= candidate -> go (edge + 1) best
+                _ -> go (edge + 1) (Just (terminal, candidate))
+
+  complementOne = -2
+
+walkWithBarrier
+  :: forall s vertex directed undirected face.
+     Maybe IntSet.IntSet
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Point
+  -> Int
+  -> ST s (Either BuildError MutableLocation)
+walkWithBarrier barrier mutable operation query initialFace = do
+  faces <- faceCount mutable
+  halfEdges <- directedEdgeCount mutable
+  let !budget = max 8 (faces + halfEdges `quot` 2 + 4)
+  go budget 0 initialFace
+ where
+  go
+    :: Int
+    -> Int
+    -> Int
+    -> ST s (Either BuildError MutableLocation)
+  go !remaining !steps !face
+    | remaining <= 0 = do
+        addCounter operation (Reported LocationFallbacks) 1
+        maxCounter operation (Reported LocationMaxWalk) steps
+        pure (Left (LocationWalkExhausted query steps))
+    | face <= 0 = Right . MutableOutsideHull <$> visibleOuterEdge mutable query
+    | otherwise = do
+        addCounter operation (Reported LocationWalkSteps) 1
+        let !nextSteps = steps + 1
+        probe <- probeFace mutable face query
+        case probe of
+          MutableInFace found -> do
+            writeSTRef (mdLastFace mutable) found
+            maxCounter operation (Reported LocationMaxWalk) nextSteps
+            pure (Right probe)
+          MutableOnVertex _ -> do
+            writeSTRef (mdLastFace mutable) face
+            maxCounter operation (Reported LocationMaxWalk) nextSteps
+            pure (Right probe)
+          MutableOnEdge _ -> do
+            writeSTRef (mdLastFace mutable) face
+            maxCounter operation (Reported LocationMaxWalk) nextSteps
+            pure (Right probe)
+          MutableOutsideHull edge -> do
+            adjacent <- readFace mutable edge
+            if adjacent > 0
+                && maybe False (IntSet.member (edge `quot` 2)) barrier
+              then
+                pure
+                  ( Left
+                      ( RefinementDomainWouldCrossInterface
+                          (UndirectedEdgeId (fromIntegral (edge `quot` 2)))
+                          (FaceId (fromIntegral face))
+                      )
+                  )
+              else
+                if adjacent == 0
+                  then do
+                    maxCounter operation (Reported LocationMaxWalk) nextSteps
+                    pure (Right (MutableOutsideHull edge))
+                  else go (remaining - 1) nextSteps adjacent
+          MutableEmpty -> pure (Left (PointLocationFailed query))
+
+probeFace :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s MutableLocation
+-- 'faceEdges' walks the @next@ chain, so the cycle already states each
+-- boundary's destination: @destination e0 = origin e1@. Reading the twin's
+-- origin instead asks the store for what the face has already said, at twice
+-- the endpoint loads and twice the coordinate loads per probe. Three origins
+-- and three points settle all three boundaries. Vertex and edge hits still
+-- return on the first boundary that reports one, and a later crossing still
+-- displaces an earlier one.
+probeFace mutable face query = do
+  (e0, e1, e2) <- faceEdges mutable face
+  a <- readOrigin mutable e0
+  b <- readOrigin mutable e1
+  c <- readOrigin mutable e2
+  pa <- pointAt mutable a
+  pb <- pointAt mutable b
+  pc <- pointAt mutable c
+  let !first = probeBoundary reverseIndex query e0 a pa b pb
+  case first of
+    BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
+    BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
+    _ -> do
+      let !second = probeBoundary reverseIndex query e1 b pb c pc
+      case second of
+        BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
+        BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
+        _ -> do
+          let !third = probeBoundary reverseIndex query e2 c pc a pa
+          case third of
+            BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
+            BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
+            _ -> pure $ case keep third (keep second (keep first Nothing)) of
+              Nothing -> MutableInFace face
+              Just boundary -> MutableOutsideHull boundary
+ where
+  keep :: BoundaryProbe boundary probeVertex -> Maybe boundary -> Maybe boundary
+  keep (BoundaryCrossing boundary) _ = Just boundary
+  keep _ held = held
+
+chooseStartFace :: MutableDcel s vertex directed undirected face -> Maybe Int -> ST s Int
+chooseStartFace mutable hint = do
+  faces <- faceCount mutable
+  cached <- readSTRef (mdLastFace mutable)
+  let candidate = case hint of
+        Just face | face > 0 && face < faces -> face
+        _ | cached > 0 && cached < faces -> cached
+        _ -> 1
+  pure candidate
+
+visibleOuterEdge
+  :: forall s vertex directed undirected face.
+     MutableDcel s vertex directed undirected face
+  -> Point
+  -> ST s Int
+visibleOuterEdge mutable query = do
+  start <- readFaceEdge mutable 0
+  halfEdges <- directedEdgeCount mutable
+  if start < 0 || halfEdges == 0
+    then pure 0
+    else go (halfEdges + 1) start start Nothing
+ where
+  go
+    :: Int
+    -> Int
+    -> Int
+    -> Maybe Int
+    -> ST s Int
+  go !remaining !start !edge !fallback
+    | remaining <= 0 = pure (maybe start id fallback)
+    | otherwise = do
+        from <- edgeOriginPoint mutable edge
+        to <- edgeOriginPoint mutable (reverseIndex edge)
+        let !side = orient2d from to query
+            !fallback' = if side /= LT then Just edge else fallback
+        if side == GT
+          then pure edge
+          else do
+            edgeNext <- readNext mutable edge
+            if edgeNext == start then pure (maybe edge id fallback') else go (remaining - 1) start edgeNext fallback'
diff --git a/src-build/Moonlight/Planar/Internal/Mutable.hs b/src-build/Moonlight/Planar/Internal/Mutable.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Mutable.hs
@@ -0,0 +1,1573 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module Moonlight.Planar.Internal.Mutable
+  ( MutableDcel (..)
+  , DenseMutableDcel
+  , MutableTopology (..)
+  , denseMutableDcel
+  , denseMutableOwner
+  , DcelCapacity
+  , generalDcelCapacity
+  , planarDcelCapacity
+  , exactDcelCapacity
+  , newMutableDcel
+  , DefaultedVertexDcel
+  , newMutableDcelWithVertexDefault
+  , defaultedVertexDcel
+  , defaultedDenseVertexDcel
+  , thawTriangulation
+  , thawTriangulationDense
+  , freezeTriangulation
+  , freezeTriangulationWithStats
+  , pointCapacity
+  , halfEdgeCapacity
+  , pointCount
+  , connectedCount
+  , directedEdgeCount
+  , faceCount
+  , pointAt
+  , lookupPointVertex
+  , activatePointIndex
+  , activateBatchPointIndex
+  , discardBatchPointIndex
+  , identityIndexActive
+  , readPointX
+  , readPointY
+  , writePoint
+  , vertexDataAt
+  , writeVertexData
+  , payloadsPristine
+  , resetEdgeData
+  , resetFaceData
+  , edgeOriginPoint
+  , appendVertex
+  , appendVertexCoordinates
+  , appendDefaultVertexCoordinates
+  , NextVertexSlot
+  , nextVertexSlot
+  , nextVertexSlotIndex
+  , appendVertexCoordinatesAtSlot
+  , ensurePointCapacity
+  , markConnected
+  , isConnected
+  , addEdge
+  , addEdgeBlock
+  , addFace
+  , addFaceBlock
+  , denseAddEdgeBlock
+  , denseAddFaceBlock
+  , denseInitializeUnconstrainedEdgeBlock
+  , ensureCellCapacity
+  , truncatePoints
+  , truncateDirectedEdges
+  , truncateFaces
+  , swapRemoveUndirectedEdge
+  , swapRemoveFace
+  , swapRemoveVertex
+  , linkEdges
+  , setCycle3
+  , faceEdges
+  , readOrigin
+  , writeOrigin
+  , readNext
+  , writeNext
+  , readPrevious
+  , writePrevious
+  , readFace
+  , writeFace
+  , readVertexOut
+  , writeVertexOut
+  , readFaceEdge
+  , writeFaceEdge
+  , readConstraint
+  , denseReadPointX
+  , denseReadPointY
+  , denseWriteFreshDefaultPoint
+  , denseCommitFreshDefaultPoints
+  , denseReadOrigin
+  , denseWriteOrigin
+  , denseReadNext
+  , denseWriteNext
+  , denseReadPrevious
+  , denseWritePrevious
+  , denseReadFace
+  , denseWriteFace
+  , denseWriteVertexOut
+  , denseMarkFreshConnected
+  , denseCommitFreshConnections
+  , denseReadFaceEdge
+  , denseWriteFaceEdge
+  , denseReadConstraint
+  , denseLinkEdges
+  , denseSetCycle3
+  , denseFaceEdges
+  , setConstraint
+  , clearConstraint
+  ) where
+
+import Control.Monad (foldM, forM_, unless, when)
+import Data.Bits (xor)
+import Control.Monad.ST (ST)
+import qualified Data.IntSet as IntSet
+import Data.STRef
+  ( STRef
+  , modifySTRef'
+  , newSTRef
+  , readSTRef
+  , writeSTRef
+  )
+import Data.Word (Word8, Word32)
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  )
+import Moonlight.Planar.Internal.BoxedPaged
+import Moonlight.Planar.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
+import Moonlight.Planar.Internal.Paged
+import Moonlight.Planar.Internal.PointIndex
+  ( MutablePointIndex
+  , MutablePointIndexUpdate (..)
+  , PointIndex
+  , buildPointIndex
+  , emptyPointIndex
+  , insertPointIndex
+  , lookupMutablePoint
+  , newMutablePointIndex
+  , pointIndexCandidates
+  , relocateMutablePoint
+  , relocatePointIndex
+  , removeMutablePoint
+  , removePointIndex
+  , seedMutablePointIndex
+  )
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Internal.Representation (CanonicalAdmission (..), Triangulation (..), authoringElementDefaults)
+import Moonlight.Planar.Internal.Types (BuildError (..), ElementDefaults (..))
+import Moonlight.Planar.Point (Point (..))
+-- | Dormant carries the inherited index through mutations as unforced pure
+-- updates; persistent Active is the same index forced and kept strict because
+-- a singleton lookup already proved someone is asking. Batch Active is the
+-- existing open-addressed owner scoped to one dense identity program. Missing
+-- means publication owes a lazy whole-mesh rebuild to any eventual asker.
+data MutablePointIndexState s
+  = DormantPointIndex PointIndex
+  | ActivePersistentPointIndex !PointIndex
+  | ActiveBatchPointIndex !(MutablePointIndex s)
+  | MissingPointIndex
+
+data MutableDcel s vertex directed undirected face = MutableDcel
+  { mdPointX :: !(MutablePaged s Double)
+  , mdPointY :: !(MutablePaged s Double)
+  , mdPointIndex :: !(STRef s (MutablePointIndexState s))
+  , mdVertexOut :: !(MutablePaged s Word32)
+  , mdVertexData :: !(MutableBoxedPaged 'OptionalFill s vertex)
+  , mdNewConnected :: !(MutablePaged s Word8)
+  , mdRecycledNew :: !(STRef s IntSet.IntSet)
+  , mdHalfTopology :: !(MutablePaged s Word32)
+  , mdDirectedData :: !(MutableBoxedPaged 'RequiredFill s directed)
+  , mdUndirectedData :: !(MutableBoxedPaged 'RequiredFill s undirected)
+  , mdConstraint :: !(MutablePaged s Word8)
+  , mdFaceEdge :: !(MutablePaged s Word32)
+  , mdFaceData :: !(MutableBoxedPaged 'RequiredFill s face)
+  , mdPointCount :: !(STRef s Int)
+  , mdConnectedCount :: !(STRef s Int)
+  , mdHalfCount :: !(STRef s Int)
+  , mdFaceCount :: !(STRef s Int)
+  , mdConstraintCount :: !(STRef s Int)
+  , mdConstraintEdges :: !(STRef s IntSet.IntSet)
+  , mdLastFace :: !(STRef s Int)
+  , mdInitialPointCount :: {-# UNPACK #-} !Int
+  , mdPointCapacity :: {-# UNPACK #-} !Int
+  , mdHalfCapacity :: {-# UNPACK #-} !Int
+  , mdFaceCapacity :: {-# UNPACK #-} !Int
+  , -- | No element payload plane held a materialized page at thaw. Nothing
+    -- inside a transaction can write one: the three planes are written only
+    -- through the persistent setters, which run outside one, and both the
+    -- rewrite reset and the swap-compaction relocation below are no-ops while
+    -- this holds. So it is constant for the transaction's whole life, and a
+    -- rewrite decides in a predictable branch that it has no label to move.
+    mdPayloadsPristine :: !Bool
+  }
+
+-- | Physical bounds for one fresh mutable DCEL. Vertex, directed-edge and
+-- face sections are stated independently because a known planar construction
+-- and an arbitrary append program obey different allocation laws. Keeping the
+-- law in the constructor input prevents every fresh caller from silently
+-- inheriting the loosest reservation and then copying its dead tail at freeze.
+data DcelCapacity = DcelCapacity
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+  {-# UNPACK #-} !Int
+
+-- | Conservative append-program capacity, preserving the historical slack
+-- for operations whose intermediate cells may outlive their final topology.
+generalDcelCapacity :: Int -> DcelCapacity
+generalDcelCapacity maximumVertices =
+  let !vertices = max 1 maximumVertices
+   in DcelCapacity vertices (max 2 (8 * vertices + 16)) (max 1 (3 * vertices + 8))
+
+-- | Tight fresh planar capacity. Circle sweep allocates monotonically and no
+-- rewrite abandons a cell, so Euler's bounds plus seed slack are authoritative:
+-- at most @6n@ directed edges and @2n@ faces.
+--
+-- The separated seam merge is the second lawful caller. It copies two planar
+-- sources and then only adds: stitching creates seam cells, legalization
+-- rewires without allocating, and nothing is abandoned. Its peak is therefore
+-- its published result, a planar triangulation on the summed vertex count.
+planarDcelCapacity :: Int -> DcelCapacity
+planarDcelCapacity maximumVertices =
+  let !vertices = max 1 maximumVertices
+   in DcelCapacity vertices (max 2 (6 * vertices + 16)) (max 1 (2 * vertices + 8))
+
+-- | Exact section bounds for reconstruction programs that already know the
+-- published cardinalities they will materialize.
+exactDcelCapacity :: Int -> Int -> Int -> DcelCapacity
+exactDcelCapacity vertices directedEdges faces =
+  DcelCapacity (max 1 vertices) (max 2 directedEdges) (max 1 faces)
+
+-- | The contiguous physical section of one mutable DCEL. It is a refinement
+-- of the canonical owner, not a second mesh: every plane below is the exact
+-- flat vector already held by 'denseMutableOwner'. Circle sweep and dense
+-- sessions discharge this section once, then interpret their hot local
+-- rewrites without re-testing each 'MutablePaged' sum at every cell.
+data DenseMutableDcel s vertex directed undirected face = DenseMutableDcel
+  { dmdOwner :: !(MutableDcel s vertex directed undirected face)
+  , dmdPointX :: !(FlatMutablePaged s Double)
+  , dmdPointY :: !(FlatMutablePaged s Double)
+  , dmdVertexOut :: !(FlatMutablePaged s Word32)
+  , dmdNewConnected :: !(FlatMutablePaged s Word8)
+  , dmdHalfTopology :: !(FlatMutablePaged s Word32)
+  , dmdConstraint :: !(FlatMutablePaged s Word8)
+  , dmdFaceEdge :: !(FlatMutablePaged s Word32)
+  }
+
+denseMutableDcel
+  :: MutableDcel s vertex directed undirected face
+  -> Maybe (DenseMutableDcel s vertex directed undirected face)
+denseMutableDcel owner@MutableDcel{mdPointX, mdPointY, mdVertexOut, mdNewConnected, mdHalfTopology, mdConstraint, mdFaceEdge} =
+  DenseMutableDcel owner
+    <$> flatMutableSection mdPointX
+    <*> flatMutableSection mdPointY
+    <*> flatMutableSection mdVertexOut
+    <*> flatMutableSection mdNewConnected
+    <*> flatMutableSection mdHalfTopology
+    <*> flatMutableSection mdConstraint
+    <*> flatMutableSection mdFaceEdge
+{-# INLINE denseMutableDcel #-}
+
+denseMutableOwner
+  :: DenseMutableDcel s vertex directed undirected face
+  -> MutableDcel s vertex directed undirected face
+denseMutableOwner = dmdOwner
+{-# INLINE denseMutableOwner #-}
+
+-- | The physical interpretation required by local topology rewrites. The
+-- semantic owner remains 'MutableDcel'; this algebra merely preserves the
+-- storage refinement a caller has already proved, so one normalization law
+-- specializes to either paged or flat cells instead of growing a sibling
+-- rewrite engine.
+class MutableTopology mutable where
+  topologyOwner
+    :: mutable s vertex directed undirected face
+    -> MutableDcel s vertex directed undirected face
+  topologyReadPointX
+    :: mutable s vertex directed undirected face -> Int -> ST s Double
+  topologyReadPointY
+    :: mutable s vertex directed undirected face -> Int -> ST s Double
+  topologyReadOrigin
+    :: mutable s vertex directed undirected face -> Int -> ST s Int
+  topologyReadNext
+    :: mutable s vertex directed undirected face -> Int -> ST s Int
+  topologyReadPrevious
+    :: mutable s vertex directed undirected face -> Int -> ST s Int
+  topologyReadFace
+    :: mutable s vertex directed undirected face -> Int -> ST s Int
+  topologyReadConstraint
+    :: mutable s vertex directed undirected face -> Int -> ST s Bool
+  topologyWriteOrigin
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+  topologyWriteNext
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+  topologyWritePrevious
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+  topologyWriteFace
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+  topologyWriteFaceEdge
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+  topologyWriteVertexOut
+    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
+
+instance MutableTopology MutableDcel where
+  topologyOwner = id
+  topologyReadPointX = readPointX
+  topologyReadPointY = readPointY
+  topologyReadOrigin = readOrigin
+  topologyReadNext = readNext
+  topologyReadPrevious = readPrevious
+  topologyReadFace = readFace
+  topologyReadConstraint = readConstraint
+  topologyWriteOrigin = writeOrigin
+  topologyWriteNext = writeNext
+  topologyWritePrevious = writePrevious
+  topologyWriteFace = writeFace
+  topologyWriteFaceEdge = writeFaceEdge
+  topologyWriteVertexOut = writeVertexOut
+  {-# INLINE topologyOwner #-}
+  {-# INLINE topologyReadPointX #-}
+  {-# INLINE topologyReadPointY #-}
+  {-# INLINE topologyReadOrigin #-}
+  {-# INLINE topologyReadNext #-}
+  {-# INLINE topologyReadPrevious #-}
+  {-# INLINE topologyReadFace #-}
+  {-# INLINE topologyReadConstraint #-}
+  {-# INLINE topologyWriteOrigin #-}
+  {-# INLINE topologyWriteNext #-}
+  {-# INLINE topologyWritePrevious #-}
+  {-# INLINE topologyWriteFace #-}
+  {-# INLINE topologyWriteFaceEdge #-}
+  {-# INLINE topologyWriteVertexOut #-}
+
+instance MutableTopology DenseMutableDcel where
+  topologyOwner = denseMutableOwner
+  topologyReadPointX = denseReadPointX
+  topologyReadPointY = denseReadPointY
+  topologyReadOrigin = denseReadOrigin
+  topologyReadNext = denseReadNext
+  topologyReadPrevious = denseReadPrevious
+  topologyReadFace = denseReadFace
+  topologyReadConstraint = denseReadConstraint
+  topologyWriteOrigin = denseWriteOrigin
+  topologyWriteNext = denseWriteNext
+  topologyWritePrevious = denseWritePrevious
+  topologyWriteFace = denseWriteFace
+  topologyWriteFaceEdge = denseWriteFaceEdge
+  topologyWriteVertexOut = denseWriteVertexOut
+  {-# INLINE topologyOwner #-}
+  {-# INLINE topologyReadPointX #-}
+  {-# INLINE topologyReadPointY #-}
+  {-# INLINE topologyReadOrigin #-}
+  {-# INLINE topologyReadNext #-}
+  {-# INLINE topologyReadPrevious #-}
+  {-# INLINE topologyReadFace #-}
+  {-# INLINE topologyReadConstraint #-}
+  {-# INLINE topologyWriteOrigin #-}
+  {-# INLINE topologyWriteNext #-}
+  {-# INLINE topologyWritePrevious #-}
+  {-# INLINE topologyWriteFace #-}
+  {-# INLINE topologyWriteFaceEdge #-}
+  {-# INLINE topologyWriteVertexOut #-}
+
+newMutableDcel :: ElementDefaults directed undirected face -> DcelCapacity -> ST s (MutableDcel s vertex directed undirected face)
+newMutableDcel defaults capacity = newMutableDcelFrom DenseTransaction Nothing defaults capacity Nothing
+
+-- | A fresh dense mutable DCEL whose vertex payload plane is uniformly filled.
+-- The witness keeps the flat section tied to its canonical owner and permits
+-- geometry-only ingress to extend the payload plane without materializing one
+-- boxed value per site.
+data DefaultedVertexDcel s vertex directed undirected face = DefaultedVertexDcel
+  { defaultedVertexDcel :: !(MutableDcel s vertex directed undirected face)
+  , defaultedDenseVertexDcel :: !(DenseMutableDcel s vertex directed undirected face)
+  }
+
+newMutableDcelWithVertexDefault
+  :: vertex
+  -> ElementDefaults directed undirected face
+  -> DcelCapacity
+  -> ST s (Either BuildError (DefaultedVertexDcel s vertex directed undirected face))
+newMutableDcelWithVertexDefault vertexDefault defaults capacity = do
+  mutable <-
+    newMutableDcelFrom
+      DenseTransaction
+      (Just vertexDefault)
+      defaults
+      capacity
+      Nothing
+  pure $
+    case denseMutableDcel mutable of
+      Nothing -> Left CircleSweepRequiresDenseStorage
+      Just dense -> Right (DefaultedVertexDcel mutable dense)
+
+-- | Open a local-edit transaction: copy-on-write pages, publication
+-- proportional to dirtied pages. The section for singleton persistent verbs.
+thawTriangulation
+  :: Int
+  -> Triangulation mode vertex directed undirected face
+  -> ST s (MutableDcel s vertex directed undirected face)
+thawTriangulation maximumVertices triangulation =
+  newMutableDcelFrom
+    LocalTransaction
+    Nothing
+    (authoringElementDefaults triangulation)
+    (generalDcelCapacity maximumVertices)
+    (Just triangulation)
+
+-- | Open a batch transaction: one dense copy up front, flat reads and writes
+-- thereafter. The section for sessions and every other many-edit operation.
+thawTriangulationDense
+  :: Int
+  -> Triangulation mode vertex directed undirected face
+  -> ST s (MutableDcel s vertex directed undirected face)
+thawTriangulationDense maximumVertices triangulation =
+  newMutableDcelFrom
+    DenseTransaction
+    Nothing
+    (authoringElementDefaults triangulation)
+    (generalDcelCapacity maximumVertices)
+    (Just triangulation)
+
+newMutableDcelFrom
+  :: TransactionShape
+  -> Maybe vertex
+  -> ElementDefaults directed undirected face
+  -> DcelCapacity
+  -> Maybe (Triangulation mode vertex directed undirected face)
+  -> ST s (MutableDcel s vertex directed undirected face)
+newMutableDcelFrom shape vertexDefault defaults (DcelCapacity requestedVertices requestedHalfEdges requestedFaces) source = do
+  let !existingVertices = maybe 0 (pagedLength . triPointX) source
+      !existingHalfEdges = maybe 0 ((`quot` 4) . pagedLength . triHalfTopology) source
+      !existingFaces = maybe 1 (pagedLength . triFaceEdge) source
+      !vertexCapacity = max existingVertices requestedVertices
+      !halfCapacity = max existingHalfEdges requestedHalfEdges
+      !faceCapacity = max existingFaces requestedFaces
+      vertexDataBase = maybe (emptyBoxedPaged (maybe NoFill Fill vertexDefault)) triVertexData source
+      directedDataBase = maybe (emptyBoxedPaged (Fill (defaultDirectedEdgeData defaults))) triDirectedData source
+      undirectedDataBase = maybe (emptyBoxedPaged (Fill (defaultUndirectedEdgeData defaults))) triUndirectedData source
+      faceDataBase = maybe (emptyBoxedPaged (Fill (defaultFaceData defaults))) triFaceData source
+      constraintBaseCount = maybe 0 triConstraintCount source
+      constraintBaseEdges = maybe IntSet.empty triConstraintEdges source
+      pointIndexBase = maybe MissingPointIndex (DormantPointIndex . triPointIndex) source
+  mdPointX <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointX) source
+  mdPointY <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointY) source
+  mdVertexOut <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triVertexOut) source
+  mdVertexData <- thawBoxedPaged shape vertexDataBase
+  mdNewConnected <- newLocalMutablePaged (vertexCapacity - existingVertices)
+  mdRecycledNew <- newSTRef IntSet.empty
+  mdHalfTopology <- maybe (newMutablePaged (4 * halfCapacity)) (thawPagedShaped shape (4 * halfCapacity) . triHalfTopology) source
+  mdDirectedData <- thawBoxedPaged shape directedDataBase
+  mdUndirectedData <- thawBoxedPaged shape undirectedDataBase
+  mdConstraint <- maybe (newMutablePaged (halfCapacity `quot` 2)) (thawPagedShaped shape (halfCapacity `quot` 2) . triConstraint) source
+  mdFaceEdge <-
+    case source of
+      Just triangulation -> thawPagedShaped shape faceCapacity (triFaceEdge triangulation)
+      Nothing -> do
+        freshFaceEdges <- newLocalMutablePaged faceCapacity
+        writePaged freshFaceEdges 0 noIndex
+        pure freshFaceEdges
+  mdFaceData <- thawBoxedPaged shape faceDataBase
+  mdPointCount <- newSTRef existingVertices
+  mdPointIndex <- newSTRef pointIndexBase
+  mdConnectedCount <- newSTRef existingVertices
+  mdHalfCount <- newSTRef existingHalfEdges
+  mdFaceCount <- newSTRef existingFaces
+  mdConstraintCount <- newSTRef constraintBaseCount
+  mdConstraintEdges <- newSTRef constraintBaseEdges
+  mdLastFace <- newSTRef (if existingFaces > 1 then 1 else 0)
+  let !mdPayloadsPristine =
+        boxedThawPristine mdDirectedData
+          && boxedThawPristine mdUndirectedData
+          && boxedThawPristine mdFaceData
+  pure
+    MutableDcel
+      { mdInitialPointCount = existingVertices
+      , mdPointCapacity = vertexCapacity
+      , mdHalfCapacity = halfCapacity
+      , mdFaceCapacity = faceCapacity
+      , ..
+      }
+
+freezeTriangulation :: MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face))
+freezeTriangulation mutable =
+  fmap (fmap fst) (freezeTriangulationCore (const (pure emptyPublicationStats)) mutable)
+
+freezeTriangulationWithStats
+  :: MutableDcel s vertex directed undirected face
+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
+freezeTriangulationWithStats = freezeTriangulationCore readMutableDcelPublicationStats
+
+freezeTriangulationCore
+  :: (MutableDcel s vertex directed undirected face -> ST s PublicationStats)
+  -> MutableDcel s vertex directed undirected face
+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
+freezeTriangulationCore readPublicationStats mutable@MutableDcel
+  { mdPointX
+  , mdPointY
+  , mdPointIndex
+  , mdVertexOut
+  , mdVertexData
+  , mdHalfTopology
+  , mdDirectedData
+  , mdUndirectedData
+  , mdConstraint
+  , mdFaceEdge
+  , mdFaceData
+  , mdPointCount
+  , mdHalfCount
+  , mdFaceCount
+  , mdConstraintCount
+  , mdConstraintEdges
+  } = do
+    vertices <- readSTRef mdPointCount
+    halfEdges <- readSTRef mdHalfCount
+    faces <- readSTRef mdFaceCount
+    triConstraintCount <- readSTRef mdConstraintCount
+    triConstraintEdges <- readSTRef mdConstraintEdges
+    vertexDataOutcome <- freezeBoxedPaged vertices mdVertexData
+    directedDataOutcome <- freezeBoxedPaged halfEdges mdDirectedData
+    undirectedDataOutcome <- freezeBoxedPaged (halfEdges `quot` 2) mdUndirectedData
+    faceDataOutcome <- freezeBoxedPaged faces mdFaceData
+    case
+        (,,,)
+          <$> vertexDataOutcome
+          <*> directedDataOutcome
+          <*> undirectedDataOutcome
+          <*> faceDataOutcome
+      of
+        Left obstruction -> pure (Left (PayloadStorageFailure obstruction))
+        Right (triVertexData, triDirectedData, triUndirectedData, triFaceData) -> do
+          triPointX <- freezePaged vertices mdPointX
+          triPointY <- freezePaged vertices mdPointY
+          pointIndexState <- readSTRef mdPointIndex
+          let triPointIndex =
+                case pointIndexState of
+                  DormantPointIndex residentIndex -> residentIndex
+                  ActivePersistentPointIndex residentIndex -> residentIndex
+                  ActiveBatchPointIndex _ -> buildPointIndex triPointX triPointY
+                  MissingPointIndex -> buildPointIndex triPointX triPointY
+          triVertexOut <- freezePaged vertices mdVertexOut
+          triHalfTopology <- freezePaged (4 * halfEdges) mdHalfTopology
+          triConstraint <- freezePaged (halfEdges `quot` 2) mdConstraint
+          triFaceEdge <- freezePaged faces mdFaceEdge
+          publicationStats <- readPublicationStats mutable
+          pure (Right (Triangulation{triSeamFrontier = Nothing, triCanonical = CanonicalUnknown, ..}, publicationStats))
+
+readMutableDcelPublicationStats
+  :: MutableDcel s vertex directed undirected face
+  -> ST s PublicationStats
+readMutableDcelPublicationStats MutableDcel
+  { mdPointX
+  , mdPointY
+  , mdVertexOut
+  , mdHalfTopology
+  , mdConstraint
+  , mdFaceEdge
+  , mdVertexData
+  , mdDirectedData
+  , mdUndirectedData
+  , mdFaceData
+  } =
+  foldl'
+    addPublicationStats
+    emptyPublicationStats
+    <$> sequence
+      [ readMutablePagedPublicationStats mdPointX
+      , readMutablePagedPublicationStats mdPointY
+      , readMutablePagedPublicationStats mdVertexOut
+      , readMutablePagedPublicationStats mdHalfTopology
+      , readMutablePagedPublicationStats mdConstraint
+      , readMutablePagedPublicationStats mdFaceEdge
+      , readMutableBoxedPublicationStats mdVertexData
+      , readMutableBoxedPublicationStats mdDirectedData
+      , readMutableBoxedPublicationStats mdUndirectedData
+      , readMutableBoxedPublicationStats mdFaceData
+      ]
+
+pointCapacity :: MutableDcel s vertex directed undirected face -> Int
+pointCapacity = mdPointCapacity
+{-# INLINE pointCapacity #-}
+
+halfEdgeCapacity :: MutableDcel s vertex directed undirected face -> Int
+halfEdgeCapacity = mdHalfCapacity
+{-# INLINE halfEdgeCapacity #-}
+
+pointCount :: MutableDcel s vertex directed undirected face -> ST s Int
+pointCount = readSTRef . mdPointCount
+{-# INLINE pointCount #-}
+
+connectedCount :: MutableDcel s vertex directed undirected face -> ST s Int
+connectedCount = readSTRef . mdConnectedCount
+{-# INLINE connectedCount #-}
+
+directedEdgeCount :: MutableDcel s vertex directed undirected face -> ST s Int
+directedEdgeCount = readSTRef . mdHalfCount
+{-# INLINE directedEdgeCount #-}
+
+faceCount :: MutableDcel s vertex directed undirected face -> ST s Int
+faceCount = readSTRef . mdFaceCount
+{-# INLINE faceCount #-}
+
+pointAt :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
+pointAt MutableDcel{mdPointX, mdPointY} index =
+  Point <$> readPaged mdPointX index <*> readPaged mdPointY index
+{-# INLINE pointAt #-}
+
+-- | Resolve a canonical site through the derived handle index, confirming
+-- every hash candidate against the authoritative coordinate planes. A mesh
+-- created from scratch derives the index only if a caller actually asks; a
+-- thawed published mesh inherits its structurally shared index.
+lookupPointVertex
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> ST s (Maybe Int)
+lookupPointVertex mutable@MutableDcel{mdPointIndex} rawPoint = do
+  indexState <- readSTRef mdPointIndex
+  case canonicalPoint rawPoint of
+    Point x y ->
+      case indexState of
+        ActiveBatchPointIndex table ->
+          lookupMutablePoint table (readPointX mutable) (readPointY mutable) x y
+        ActivePersistentPointIndex residentIndex ->
+          resolvePersistent x y residentIndex
+        DormantPointIndex residentIndex -> do
+          writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
+          resolvePersistent x y residentIndex
+        MissingPointIndex -> do
+          derived <- deriveMutablePointIndex mutable
+          writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
+          resolvePersistent x y derived
+ where
+  resolvePersistent x y pointIndex =
+    foldM
+      (confirmCandidate mutable x y)
+      Nothing
+      (pointIndexCandidates x y pointIndex)
+
+-- | Whether the transaction has already committed to incremental identity
+-- transport. Answering does not force a dormant index's lazy rebuild, which
+-- is the point: a per-question caller must not buy a whole-mesh build.
+identityIndexActive :: MutableDcel s vertex directed undirected face -> ST s Bool
+identityIndexActive MutableDcel{mdPointIndex} = do
+  indexState <- readSTRef mdPointIndex
+  pure $ case indexState of
+    ActivePersistentPointIndex _ -> True
+    ActiveBatchPointIndex _ -> True
+    _ -> False
+{-# INLINE identityIndexActive #-}
+
+-- | Declare that a singleton handle-keyed rewrite must transport the resident
+-- immutable identity section strictly. Dense point-keyed removal uses
+-- 'activateBatchPointIndex' instead, so it neither forces nor incrementally
+-- allocates the published 'PointIndex'.
+activatePointIndex
+  :: MutableDcel s vertex directed undirected face
+  -> ST s ()
+activatePointIndex mutable@MutableDcel{mdPointIndex} = do
+  indexState <- readSTRef mdPointIndex
+  case indexState of
+    ActivePersistentPointIndex _ -> pure ()
+    ActiveBatchPointIndex _ -> pure ()
+    DormantPointIndex residentIndex ->
+      writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
+    MissingPointIndex -> do
+      derived <- deriveMutablePointIndex mutable
+      writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
+
+-- | Open an identity section for one dense removal program. The table derives
+-- only from coordinate authority, and its extent is deliberately narrower
+-- than the surrounding session: publication returns to the lazy immutable
+-- derivation instead of retaining a second mutable identity owner.
+activateBatchPointIndex
+  :: MutableDcel s vertex directed undirected face
+  -> ST s (Either BuildError ())
+activateBatchPointIndex mutable@MutableDcel{mdPointIndex} = do
+  vertices <- pointCount mutable
+  table <- newMutablePointIndex vertices
+  seeded <-
+    seedMutablePointIndex
+      table
+      vertices
+      (readPointX mutable)
+      (readPointY mutable)
+  case seeded of
+    Left failure -> pure (Left failure)
+    Right () -> do
+      writeSTRef mdPointIndex (ActiveBatchPointIndex table)
+      pure (Right ())
+-- | Close the batch-local identity section after its removal program. Its
+-- contents cannot escape @ST@; marking the cache missing makes freeze glue a
+-- lazy immutable derivation from the final coordinate arenas.
+discardBatchPointIndex
+  :: MutableDcel s vertex directed undirected face
+  -> ST s ()
+discardBatchPointIndex MutableDcel{mdPointIndex} =
+  modifySTRef'
+    mdPointIndex
+    (\indexState ->
+       case indexState of
+         ActiveBatchPointIndex _ -> MissingPointIndex
+         retained -> retained
+    )
+
+deriveMutablePointIndex
+  :: MutableDcel s vertex directed undirected face
+  -> ST s PointIndex
+deriveMutablePointIndex mutable = do
+  vertices <- pointCount mutable
+  foldM insertResident emptyPointIndex [0 .. vertices - 1]
+ where
+  insertResident pointIndex vertex = do
+    x <- readPointX mutable vertex
+    y <- readPointY mutable vertex
+    pure (insertPointIndex x y vertex pointIndex)
+
+confirmCandidate
+  :: MutableDcel s vertex directed undirected face
+  -> Double
+  -> Double
+  -> Maybe Int
+  -> Int
+  -> ST s (Maybe Int)
+confirmCandidate _ _ _ resident@(Just _) _ = pure resident
+confirmCandidate mutable x y Nothing candidate = do
+  heldX <- readPointX mutable candidate
+  heldY <- readPointY mutable candidate
+  pure (if heldX == x && heldY == y then Just candidate else Nothing)
+{-# INLINE confirmCandidate #-}
+
+-- | Read one stored coordinate without building a t'Point'. Coordinates are
+-- the authoritative state owned by 'mdPointX'/'mdPointY'; the t'Point'
+-- constructor is the cold accessor's packaging, and the construction kernel
+-- reads these arenas directly so a specialized sweep never boxes one.
+readPointX :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
+readPointX MutableDcel{mdPointX} index = readPaged mdPointX index
+{-# INLINE readPointX #-}
+
+readPointY :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
+readPointY MutableDcel{mdPointY} index = readPaged mdPointY index
+{-# INLINE readPointY #-}
+
+writePoint :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s ()
+writePoint MutableDcel{mdPointX, mdPointY} index rawPoint =
+  case canonicalPoint rawPoint of
+    Point x y -> do
+      writePaged mdPointX index x
+      writePaged mdPointY index y
+{-# INLINE writePoint #-}
+
+vertexDataAt :: MutableDcel s vertex directed undirected face -> Int -> ST s vertex
+vertexDataAt MutableDcel{mdVertexData} = readBoxedPaged mdVertexData
+{-# INLINE vertexDataAt #-}
+
+writeVertexData :: MutableDcel s vertex directed undirected face -> Int -> vertex -> ST s ()
+writeVertexData MutableDcel{mdVertexData} = writeBoxedPaged mdVertexData
+{-# INLINE writeVertexData #-}
+
+-- | Whether no element payload plane can be holding anything. Constant for the
+-- transaction: see 'mdPayloadsPristine'. A rewrite site that performs several
+-- resets together tests this once rather than paying the test inside each.
+payloadsPristine :: MutableDcel s vertex directed undirected face -> Bool
+payloadsPristine = mdPayloadsPristine
+{-# INLINE payloadsPristine #-}
+
+-- | Return one undirected edge and both its half-edges to the element
+-- defaults. A payload labels the element occupying a slot, and an element is
+-- its geometry: a rewrite that gives a slot new endpoints has put a different
+-- edge there, and the label the old one carried does not describe it. Leaving
+-- it would also make the payload plane depend on the flip order that reached
+-- the normal form, while the topology does not.
+--
+-- The constraint flag is deliberately not reset with it. A flag states that a
+-- segment of the input is present, and a segment that gets split is still
+-- present as its two halves; a payload states what an element is.
+resetEdgeData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+resetEdgeData MutableDcel{mdDirectedData, mdUndirectedData, mdPayloadsPristine} pair =
+  unless mdPayloadsPristine $ do
+    resetBoxedRange mdDirectedData (2 * pair) 2
+    resetBoxedRange mdUndirectedData pair 1
+{-# INLINE resetEdgeData #-}
+
+-- | Return one face to the element default. See 'resetEdgeData'.
+resetFaceData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+resetFaceData MutableDcel{mdFaceData, mdPayloadsPristine} face =
+  unless mdPayloadsPristine (resetBoxedRange mdFaceData face 1)
+{-# INLINE resetFaceData #-}
+
+edgeOriginPoint :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
+edgeOriginPoint mutable edge = readOrigin mutable edge >>= pointAt mutable
+{-# INLINE edgeOriginPoint #-}
+
+appendVertex
+  :: MutableDcel s vertex directed undirected face
+  -> Point
+  -> vertex
+  -> ST s Int
+appendVertex mutable rawPoint vertexData =
+  case canonicalPoint rawPoint of
+    Point x y -> appendVertexCoordinates mutable x y vertexData
+{-# INLINE appendVertex #-}
+
+-- | The next unmaterialized vertex record in one mutable DCEL. The constructor
+-- stays private: a caller may transport the candidate to a derived identity
+-- index, but cannot fabricate a different arena position for the append that
+-- follows. The slot remains lawful only while no intervening append occurs.
+newtype NextVertexSlot s = NextVertexSlot Int
+
+nextVertexSlot
+  :: MutableDcel s vertex directed undirected face
+  -> ST s (NextVertexSlot s)
+nextVertexSlot MutableDcel{mdPointCount} = NextVertexSlot <$> readSTRef mdPointCount
+{-# INLINE nextVertexSlot #-}
+
+nextVertexSlotIndex :: NextVertexSlot s -> Int
+nextVertexSlotIndex (NextVertexSlot vertex) = vertex
+{-# INLINE nextVertexSlotIndex #-}
+
+-- | Append a vertex whose coordinates are already canonical, by components.
+-- This is the one owner of the append record; 'appendVertex' is its
+-- t'Point'-carrying form for callers holding a point. Raw capacity means the
+-- appender initializes every field of the record it exposes.
+appendVertexCoordinates
+  :: MutableDcel s vertex directed undirected face
+  -> Double
+  -> Double
+  -> vertex
+  -> ST s Int
+appendVertexCoordinates mutable x y vertexData = do
+  slot <- nextVertexSlot mutable
+  appendVertexCoordinatesAtSlot mutable slot x y vertexData
+{-# INLINE appendVertexCoordinates #-}
+
+-- | Append the uniform vertex payload carried by a defaulted-vertex witness.
+-- No boxed page is written: extending the authoritative point count extends
+-- the defaulted payload plane at freeze.
+appendDefaultVertexCoordinates
+  :: DefaultedVertexDcel s vertex directed undirected face
+  -> Double
+  -> Double
+  -> ST s Int
+appendDefaultVertexCoordinates DefaultedVertexDcel{defaultedVertexDcel = mutable} x y = do
+  slot <- nextVertexSlot mutable
+  initializeVertexCoordinatesAtSlot mutable slot x y
+{-# INLINE appendDefaultVertexCoordinates #-}
+
+-- | Materialize the exact fresh record named by a previously acquired slot.
+-- Keeping the slot typed and adjacent to identity resolution lets bulk ingress
+-- share one point-count read between the identity candidate and the append.
+appendVertexCoordinatesAtSlot
+  :: MutableDcel s vertex directed undirected face
+  -> NextVertexSlot s
+  -> Double
+  -> Double
+  -> vertex
+  -> ST s Int
+appendVertexCoordinatesAtSlot mutable slot@(NextVertexSlot vertex) x y vertexData = do
+  writeVertexData mutable vertex vertexData
+  initializeVertexCoordinatesAtSlot mutable slot x y
+{-# INLINE appendVertexCoordinatesAtSlot #-}
+
+initializeVertexCoordinatesAtSlot
+  :: MutableDcel s vertex directed undirected face
+  -> NextVertexSlot s
+  -> Double
+  -> Double
+  -> ST s Int
+initializeVertexCoordinatesAtSlot mutable@MutableDcel{mdPointCount, mdPointX, mdPointY, mdPointIndex, mdNewConnected, mdRecycledNew} (NextVertexSlot vertex) x y = do
+  writePaged mdPointX vertex x
+  writePaged mdPointY vertex y
+  writeVertexOut mutable vertex (-1)
+  if vertex >= mdInitialPointCount mutable
+    then writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 0
+    else modifySTRef' mdRecycledNew (IntSet.insert vertex)
+  pointIndexState <- readSTRef mdPointIndex
+  case pointIndexState of
+    ActivePersistentPointIndex pointIndex ->
+      writeSTRef mdPointIndex (ActivePersistentPointIndex (insertPointIndex x y vertex pointIndex))
+    -- The batch table is only lawful over the removal subprogram that opened
+    -- it. An insertion before that scope is closed invalidates the derived
+    -- cache rather than pretending an unregistered handle exists.
+    ActiveBatchPointIndex _ -> writeSTRef mdPointIndex MissingPointIndex
+    -- Transported lazily: the field holds a pure update thunk, so a batch that
+    -- never asks an identity question pays one allocation per append, while a
+    -- persistent chain that asks every publication forces a depth-one thunk
+    -- instead of rebuilding the index over the whole mesh.
+    DormantPointIndex pointIndex ->
+      writeSTRef mdPointIndex (DormantPointIndex (insertPointIndex x y vertex pointIndex))
+    -- A missing derived view stays missing without writing its cell once per
+    -- bulk vertex. Freeze already descends from the coordinate authority when
+    -- a future identity query demands the view.
+    MissingPointIndex -> pure ()
+  writeSTRef mdPointCount (vertex + 1)
+  pure vertex
+{-# INLINE initializeVertexCoordinatesAtSlot #-}
+
+-- | Check the point arena before a local rewrite materializes vertices. The
+-- caller performs this before the first write, so refusal needs no rollback.
+ensurePointCapacity
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Either BuildError ())
+ensurePointCapacity MutableDcel{mdPointCount, mdPointCapacity} additional
+  | additional < 0 = pure (Left (CapacityExceeded additional))
+  | otherwise = do
+      current <- readSTRef mdPointCount
+      let !required = current + additional
+      pure $
+        if required > mdPointCapacity
+          then Left (CapacityExceeded required)
+          else Right ()
+{-# INLINE ensurePointCapacity #-}
+
+markConnected :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+markConnected mutable@MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew, mdConnectedCount} vertex outgoing = do
+  if vertex >= mdInitialPointCount
+    then do
+      let !newVertex = vertex - mdInitialPointCount
+      connected <- readPaged mdNewConnected newVertex
+      unless (connected /= 0) $ do
+        writePaged mdNewConnected newVertex 1
+        modifySTRef' mdConnectedCount (+ 1)
+    else do
+      recycled <- readSTRef mdRecycledNew
+      when (IntSet.member vertex recycled) $ do
+        writeSTRef mdRecycledNew (IntSet.delete vertex recycled)
+        modifySTRef' mdConnectedCount (+ 1)
+  writeVertexOut mutable vertex outgoing
+{-# INLINE markConnected #-}
+
+isConnected :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
+isConnected MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew} vertex
+  | vertex < mdInitialPointCount = IntSet.notMember vertex <$> readSTRef mdRecycledNew
+  | otherwise = (/= 0) <$> readPaged mdNewConnected (vertex - mdInitialPointCount)
+{-# INLINE isConnected #-}
+
+addEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Int, Int)
+addEdge mutable from to = do
+  base <- addEdgeBlock mutable 1
+  writeOrigin mutable base from
+  writeOrigin mutable (base + 1) to
+  pure (base, base + 1)
+{-# INLINE addEdge #-}
+
+-- Each appended pair owns the initialization of its exposed topology and
+-- constraint records; reserved capacity remains untouched.
+addEdgeBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+addEdgeBlock mutable@MutableDcel{mdHalfCount, mdConstraint} pairs = do
+  base <- readSTRef mdHalfCount
+  let !required = base + 2 * pairs
+      !firstUndirected = base `quot` 2
+      !lastUndirected = firstUndirected + pairs - 1
+  forM_ [4 * base .. 4 * required - 1] $ \slot ->
+    writePaged (mdHalfTopology mutable) slot noIndex
+  forM_ [firstUndirected .. lastUndirected] $ \edge -> writePaged mdConstraint edge 0
+  writeSTRef mdHalfCount required
+  pure base
+{-# INLINE addEdgeBlock #-}
+
+-- | Append initialized cells through the already-proved contiguous section.
+-- The semantic allocator and its tail invariant are identical to
+-- 'addEdgeBlock'; only the physical interpreter is selected once rather than
+-- once per topology slot.
+denseAddEdgeBlock :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseAddEdgeBlock dense@DenseMutableDcel{dmdOwner = MutableDcel{mdHalfCount}} pairs = do
+  base <- readSTRef mdHalfCount
+  let !required = base + 2 * pairs
+  denseInitializeUnconstrainedEdgeBlock dense base pairs
+  writeSTRef mdHalfCount required
+  pure base
+{-# INLINE denseAddEdgeBlock #-}
+
+-- | Initialize the constraint section of a proved fresh directed-edge block
+-- without advancing the global arena count. A reserved bulk program threads
+-- its allocation cursor immutably and commits the count once after gluing;
+-- the ordinary allocator above shares this exact record initializer.
+denseInitializeUnconstrainedEdgeBlock
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> ST s ()
+denseInitializeUnconstrainedEdgeBlock DenseMutableDcel{dmdConstraint} directedBase pairs =
+  forM_ [directedBase `quot` 2 .. directedBase `quot` 2 + pairs - 1] $ \edge ->
+    writeFlatMutable dmdConstraint edge 0
+{-# INLINE denseInitializeUnconstrainedEdgeBlock #-}
+
+addFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+addFace mutable anchor = do
+  base <- addFaceBlock mutable 1
+  writeFaceEdge mutable base anchor
+  pure base
+{-# INLINE addFace #-}
+
+addFaceBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+addFaceBlock mutable@MutableDcel{mdFaceCount} count = do
+  base <- readSTRef mdFaceCount
+  let !required = base + count
+  mapM_ (\face -> writeFaceEdge mutable face (-1)) [base .. required - 1]
+  writeSTRef mdFaceCount required
+  pure base
+{-# INLINE addFaceBlock #-}
+
+denseAddFaceBlock :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseAddFaceBlock DenseMutableDcel{dmdOwner = MutableDcel{mdFaceCount}} count = do
+  base <- readSTRef mdFaceCount
+  let !required = base + count
+  writeSTRef mdFaceCount required
+  pure base
+{-# INLINE denseAddFaceBlock #-}
+
+-- | Check the local allocation section before any topology rewrite begins.
+-- A refusal leaves every mutable plane untouched, so the enclosing transaction
+-- can abandon publication without rollback machinery.
+ensureCellCapacity
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> ST s (Either BuildError ())
+ensureCellCapacity MutableDcel{mdHalfCount, mdHalfCapacity, mdFaceCount, mdFaceCapacity} additionalPairs additionalFaces
+  | additionalPairs < 0 = pure (Left (HalfEdgeCapacityExceeded additionalPairs mdHalfCapacity))
+  | additionalFaces < 0 = pure (Left (FaceCapacityExceeded additionalFaces mdFaceCapacity))
+  | otherwise = do
+      currentHalfEdges <- readSTRef mdHalfCount
+      currentFaces <- readSTRef mdFaceCount
+      let !requiredHalfEdges = currentHalfEdges + 2 * additionalPairs
+          !requiredFaces = currentFaces + additionalFaces
+      if requiredHalfEdges > mdHalfCapacity
+        then pure (Left (HalfEdgeCapacityExceeded requiredHalfEdges mdHalfCapacity))
+        else
+          if requiredFaces > mdFaceCapacity
+            then pure (Left (FaceCapacityExceeded requiredFaces mdFaceCapacity))
+            else pure (Right ())
+{-# INLINE ensureCellCapacity #-}
+
+truncatePoints :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+truncatePoints MutableDcel{mdPointCount, mdConnectedCount} count = do
+  writeSTRef mdPointCount count
+  connected <- readSTRef mdConnectedCount
+  when (connected > count) (writeSTRef mdConnectedCount count)
+
+truncateDirectedEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+truncateDirectedEdges MutableDcel{mdHalfCount} = writeSTRef mdHalfCount
+
+truncateFaces :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
+truncateFaces MutableDcel{mdFaceCount, mdLastFace} count = do
+  writeSTRef mdFaceCount count
+  lastFace <- readSTRef mdLastFace
+  when (lastFace >= count) (writeSTRef mdLastFace (if count > 1 then 1 else 0))
+
+-- | Remove one undirected edge by moving the last pair into its slot. Only the
+-- two neighboring links, one vertex representative, and one face
+-- representative per moved half-edge can reference the old handles.
+--
+-- The slot the tail vacates is returned to the defaults here rather than when
+-- the allocator hands it back out. That keeps one invariant — every slot at or
+-- above the live count holds the fill — which the thaw establishes, this
+-- preserves, and 'addEdgeBlock' may therefore assume without testing anything.
+-- The cost lands on removal, which is where the element was retired, instead of
+-- on every allocation a pure insertion makes.
+swapRemoveUndirectedEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
+swapRemoveUndirectedEdge mutable@MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} pair = do
+  halfEdges <- directedEdgeCount mutable
+  let !pairs = halfEdges `quot` 2
+      !lastPair = pairs - 1
+  if pair < 0 || pair > lastPair
+    then
+      pure
+        ( Left
+            ( RemovalEdgeOutOfRange
+                (UndirectedEdgeId (fromIntegral pair))
+                pairs
+            )
+        )
+    else do
+      swapRemoveUndirectedEdgeInRange lastPair
+      pure (Right ())
+ where
+  swapRemoveUndirectedEdgeInRange lastPair = do
+    removedFlag <- readPaged mdConstraint pair
+    lastFlag <- readPaged mdConstraint lastPair
+    when (removedFlag /= 0) (modifySTRef' mdConstraintCount (subtract 1))
+    when (pair /= lastPair) $ do
+      let !oldBase = 2 * lastPair
+          !newBase = 2 * pair
+          remap !handle
+            | handle == oldBase = newBase
+            | handle == oldBase + 1 = newBase + 1
+            | otherwise = handle
+      -- Both records are taken before either is republished: the pair's two
+      -- half-edges can name each other, so a read after the first write would
+      -- see the new handle where the old one belongs.
+      !forwardOrigin <- readOrigin mutable oldBase
+      !forwardNext <- remap <$> readNext mutable oldBase
+      !forwardPrevious <- remap <$> readPrevious mutable oldBase
+      !forwardFace <- readFace mutable oldBase
+      !backwardOrigin <- readOrigin mutable (oldBase + 1)
+      !backwardNext <- remap <$> readNext mutable (oldBase + 1)
+      !backwardPrevious <- remap <$> readPrevious mutable (oldBase + 1)
+      !backwardFace <- readFace mutable (oldBase + 1)
+      writePaged mdConstraint pair lastFlag
+      unless (mdPayloadsPristine mutable) $ do
+        readBoxedPaged (mdUndirectedData mutable) lastPair >>= writeBoxedPaged (mdUndirectedData mutable) pair
+        readBoxedPaged (mdDirectedData mutable) oldBase >>= writeBoxedPaged (mdDirectedData mutable) newBase
+        readBoxedPaged (mdDirectedData mutable) (oldBase + 1) >>= writeBoxedPaged (mdDirectedData mutable) (newBase + 1)
+      writeOrigin mutable newBase forwardOrigin
+      writeNext mutable newBase forwardNext
+      writePrevious mutable newBase forwardPrevious
+      writeFace mutable newBase forwardFace
+      writeOrigin mutable (newBase + 1) backwardOrigin
+      writeNext mutable (newBase + 1) backwardNext
+      writePrevious mutable (newBase + 1) backwardPrevious
+      writeFace mutable (newBase + 1) backwardFace
+      writeNext mutable forwardPrevious newBase
+      writePrevious mutable forwardNext newBase
+      writeVertexOut mutable forwardOrigin newBase
+      writeFaceEdge mutable forwardFace newBase
+      writeNext mutable backwardPrevious (newBase + 1)
+      writePrevious mutable backwardNext (newBase + 1)
+      writeVertexOut mutable backwardOrigin (newBase + 1)
+      writeFaceEdge mutable backwardFace (newBase + 1)
+    modifySTRef'
+      mdConstraintEdges
+      (\edges ->
+         let withoutRetired = IntSet.delete pair (IntSet.delete lastPair edges)
+          in if pair /= lastPair && lastFlag /= 0
+               then IntSet.insert pair withoutRetired
+               else withoutRetired
+      )
+    resetEdgeData mutable lastPair
+    truncateDirectedEdges mutable (2 * lastPair)
+
+swapRemoveFace :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
+swapRemoveFace mutable face = do
+  faces <- faceCount mutable
+  let !lastFace = faces - 1
+  if face <= 0 || face > lastFace
+    then
+      pure
+        ( Left
+            (RemovalFaceOutOfRange (FaceId (fromIntegral face)) faces)
+        )
+    else do
+      relocated <-
+        if face == lastFace
+          then pure (Right ())
+          else relocateLastFace lastFace
+      case relocated of
+        Left obstruction -> pure (Left obstruction)
+        Right () -> do
+          resetFaceData mutable lastFace
+          truncateFaces mutable lastFace
+          pure (Right ())
+ where
+  relocateLastFace lastFace = do
+    start <- readFaceEdge mutable lastFace
+    writeFaceEdge mutable face start
+    -- Swap-compaction moves the last face's index, not the face. When the
+    -- locator's cached start is that face, following it here is the difference
+    -- between a batch of removals resuming where the previous one settled and
+    -- 'truncateFaces' finding the cached index out of range and resetting it to
+    -- the first inner face. The cache is a start, never an answer.
+    cachedFace <- readSTRef (mdLastFace mutable)
+    when (cachedFace == lastFace) (writeSTRef (mdLastFace mutable) face)
+    unless (mdPayloadsPristine mutable) $
+      readBoxedPaged (mdFaceData mutable) lastFace >>= writeBoxedPaged (mdFaceData mutable) face
+    halfEdges <- directedEdgeCount mutable
+    let go !remaining !current !seen
+          | remaining <= 0 =
+              pure
+                ( Left
+                    ( RemovalFaceCycleDidNotTerminate
+                        (FaceId (fromIntegral lastFace))
+                        (DirectedEdgeId (fromIntegral current))
+                        (halfEdges + 1)
+                    )
+                )
+          | seen && current == start = pure (Right ())
+          | otherwise = do
+              writeFace mutable current face
+              nextEdge <- readNext mutable current
+              go (remaining - 1) nextEdge True
+    go (halfEdges + 1) start False
+
+-- | Retire a vertex by moving the arena's last into its slot. The relocation is
+-- reported as the slot together with the position now standing in it: the two
+-- are one fact, and a caller told only the slot has to consult the mesh to
+-- learn what landed there.
+swapRemoveVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
+swapRemoveVertex mutable@MutableDcel{mdConnectedCount, mdPointIndex, mdRecycledNew, mdNewConnected} vertex = do
+  vertices <- pointCount mutable
+  let !lastVertex = vertices - 1
+  if vertex < 0 || vertex > lastVertex
+    then
+      pure
+        ( Left
+            (RemovalVertexOutOfRange (VertexId (fromIntegral vertex)) vertices)
+        )
+    else swapRemoveVertexInRange lastVertex
+ where
+  swapRemoveVertexInRange lastVertex = do
+    removedPoint <- pointAt mutable vertex
+    removedPayload <- vertexDataAt mutable vertex
+    movedOutcome <-
+      if vertex == lastVertex
+        then pure (Right Nothing)
+        else moveTailVertex lastVertex
+    case movedOutcome of
+      Left obstruction -> pure (Left obstruction)
+      Right moved -> do
+        indexState <- readSTRef mdPointIndex
+        updatedIndexState <-
+          updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved
+        writeSTRef mdPointIndex updatedIndexState
+        -- The connectivity companions must agree with the aggregate assertion two
+        -- lines down: after a swap removal every surviving vertex is connected. The
+        -- retired slot's entry and the relocated occupant's old entry are both
+        -- stale, and a relocated occupant landing in the appended region must read
+        -- connected through the offset store, not through its predecessor's bit.
+        modifySTRef' mdRecycledNew (IntSet.delete vertex . IntSet.delete lastVertex)
+        case moved of
+          Just _
+            | vertex >= mdInitialPointCount mutable ->
+                writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 1
+          _ -> pure ()
+        truncatePoints mutable lastVertex
+        writeSTRef mdConnectedCount lastVertex
+        pure (Right (removedPoint, removedPayload, moved))
+
+  moveTailVertex lastVertex = do
+    movedPoint <- pointAt mutable lastVertex
+    movedPayload <- vertexDataAt mutable lastVertex
+    movedOut <- readVertexOut mutable lastVertex
+    writePoint mutable vertex movedPoint
+    writeVertexData mutable vertex movedPayload
+    writeVertexOut mutable vertex movedOut
+    relocated <-
+      if movedOut < 0
+        then pure (Right ())
+        else relocateOutgoingCycle lastVertex movedOut
+    pure (Just (vertex, movedPoint) <$ relocated)
+
+  relocateOutgoingCycle lastVertex movedOut = do
+    halfEdges <- directedEdgeCount mutable
+    let go !remaining !current !seen
+          | remaining <= 0 =
+              pure
+                ( Left
+                    ( RemovalOutgoingCycleDidNotTerminate
+                        (VertexId (fromIntegral lastVertex))
+                        (DirectedEdgeId (fromIntegral current))
+                        (halfEdges + 1)
+                    )
+                )
+          | seen && current == movedOut = pure (Right ())
+          | otherwise = do
+              writeOrigin mutable current vertex
+              previousEdge <- readPrevious mutable current
+              go (remaining - 1) (previousEdge `xor` 1) True
+    go (halfEdges + 1) movedOut False
+
+-- | Transport the identity view across one vertex swap. Persistent sections
+-- retain their existing pure update law. A batch table mutates in place, then
+-- deliberately falls back to @MissingPointIndex@ if either local proof cannot
+-- be completed; geometry remains authoritative and subsequent operations walk
+-- rather than observe a stale cache.
+updatePointIndexAfterSwap
+  :: MutableDcel s vertex directed undirected face
+  -> MutablePointIndexState s
+  -> Point
+  -> Int
+  -> Int
+  -> Maybe (Int, Point)
+  -> ST s (MutablePointIndexState s)
+updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved =
+  case indexState of
+    DormantPointIndex pointIndex ->
+      pure (DormantPointIndex (updatePersistentPointIndex pointIndex))
+    ActivePersistentPointIndex pointIndex ->
+      pure (ActivePersistentPointIndex (updatePersistentPointIndex pointIndex))
+    ActiveBatchPointIndex table ->
+      transportBatchPointIndex table
+    MissingPointIndex -> pure MissingPointIndex
+ where
+  updatePersistentPointIndex pointIndex =
+    case removedPoint of
+      Point removedX removedY ->
+        let withoutRemoved = removePointIndex removedX removedY vertex pointIndex
+         in case moved of
+              Nothing -> withoutRemoved
+              Just (_, Point movedX movedY) ->
+                relocatePointIndex movedX movedY lastVertex vertex withoutRemoved
+
+  transportBatchPointIndex table =
+    case removedPoint of
+      Point removedX removedY -> do
+        removed <-
+          removeMutablePoint
+            table
+            (readPointX mutable)
+            (readPointY mutable)
+            removedX
+            removedY
+            vertex
+        case (removed, moved) of
+          (MutablePointIndexUpdated, Nothing) ->
+            pure (ActiveBatchPointIndex table)
+          (MutablePointIndexUpdated, Just (_, Point movedX movedY)) -> do
+            relocated <- relocateMutablePoint table movedX movedY lastVertex vertex
+            pure $
+              case relocated of
+                MutablePointIndexUpdated -> ActiveBatchPointIndex table
+                MutablePointIndexInvalidated -> MissingPointIndex
+          (MutablePointIndexInvalidated, _) -> pure MissingPointIndex
+
+  -- 'swapRemoveVertex' calls us before truncation. The tail's coordinate cells
+  -- still carry the moved point, so backward-shift repair can derive every
+  -- occupant home from the same canonical storage that the table indexes.
+linkEdges :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+linkEdges mutable left right = do
+  writeNext mutable left right
+  writePrevious mutable right left
+{-# INLINE linkEdges #-}
+
+setCycle3 :: MutableDcel s vertex directed undirected face -> Int -> Int -> Int -> Int -> ST s ()
+setCycle3 mutable face e0 e1 e2 = do
+  writeNext mutable e0 e1
+  writeNext mutable e1 e2
+  writeNext mutable e2 e0
+  writePrevious mutable e0 e2
+  writePrevious mutable e1 e0
+  writePrevious mutable e2 e1
+  writeFace mutable e0 face
+  writeFace mutable e1 face
+  writeFace mutable e2 face
+  writeFaceEdge mutable face e0
+{-# INLINE setCycle3 #-}
+
+faceEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s (Int, Int, Int)
+faceEdges mutable face = do
+  e0 <- readFaceEdge mutable face
+  e1 <- readNext mutable e0
+  e2 <- readNext mutable e1
+  pure (e0, e1, e2)
+{-# INLINE faceEdges #-}
+
+denseLinkEdges :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseLinkEdges dense left right = do
+  denseWriteNext dense left right
+  denseWritePrevious dense right left
+{-# INLINE denseLinkEdges #-}
+
+denseSetCycle3 :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> Int -> Int -> ST s ()
+denseSetCycle3 dense face e0 e1 e2 = do
+  denseWriteNext dense e0 e1
+  denseWriteNext dense e1 e2
+  denseWriteNext dense e2 e0
+  denseWritePrevious dense e0 e2
+  denseWritePrevious dense e1 e0
+  denseWritePrevious dense e2 e1
+  denseWriteFace dense e0 face
+  denseWriteFace dense e1 face
+  denseWriteFace dense e2 face
+  denseWriteFaceEdge dense face e0
+{-# INLINE denseSetCycle3 #-}
+
+denseFaceEdges :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s (Int, Int, Int)
+denseFaceEdges dense face = do
+  e0 <- denseReadFaceEdge dense face
+  e1 <- denseReadNext dense e0
+  e2 <- denseReadNext dense e1
+  pure (e0, e1, e2)
+{-# INLINE denseFaceEdges #-}
+
+denseReadPointX :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Double
+denseReadPointX DenseMutableDcel{dmdPointX} = readFlatMutable dmdPointX
+denseReadPointY :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Double
+denseReadPointY DenseMutableDcel{dmdPointY} = readFlatMutable dmdPointY
+{-# INLINE denseReadPointX #-}
+{-# INLINE denseReadPointY #-}
+
+-- | Materialize one coordinate slot in a fresh dense point section. The
+-- caller owns the capacity witness and supplies the final cardinality once;
+-- the point index remains the fresh owner's missing derived view until
+-- publication rebuilds it from these authoritative coordinate planes.
+denseWriteFreshDefaultPoint
+  :: DefaultedVertexDcel s vertex directed undirected face
+  -> Int
+  -> Double
+  -> Double
+  -> ST s ()
+denseWriteFreshDefaultPoint
+  DefaultedVertexDcel
+    { defaultedDenseVertexDcel =
+        dense@DenseMutableDcel
+          { dmdOwner = MutableDcel{mdInitialPointCount}
+          , dmdPointX
+          , dmdPointY
+          , dmdNewConnected
+          }
+    }
+  vertex
+  x
+  y = do
+  writeFlatMutable dmdPointX vertex x
+  writeFlatMutable dmdPointY vertex y
+  denseWriteVertexOut dense vertex (-1)
+  writeFlatMutable dmdNewConnected (vertex - mdInitialPointCount) 0
+{-# INLINE denseWriteFreshDefaultPoint #-}
+
+-- | Glue a completed fresh point section into the mutable owner's logical
+-- extent. Per-site writes remain local; the aggregate cardinality descends
+-- once after the section is complete.
+denseCommitFreshDefaultPoints
+  :: DefaultedVertexDcel s vertex directed undirected face
+  -> Int
+  -> ST s ()
+denseCommitFreshDefaultPoints
+  DefaultedVertexDcel
+    { defaultedDenseVertexDcel = DenseMutableDcel
+      { dmdOwner = MutableDcel{mdPointCount}
+      }
+    }
+  = writeSTRef mdPointCount
+{-# INLINE denseCommitFreshDefaultPoints #-}
+
+denseReadOrigin :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseReadOrigin DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index)
+denseReadNext :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseReadNext DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 1)
+denseReadPrevious :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseReadPrevious DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 2)
+denseReadFace :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseReadFace DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 3)
+{-# INLINE denseReadOrigin #-}
+{-# INLINE denseReadNext #-}
+{-# INLINE denseReadPrevious #-}
+{-# INLINE denseReadFace #-}
+
+denseWriteOrigin :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWriteOrigin DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index) (packIndex value)
+denseWriteNext :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWriteNext DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 1) (packIndex value)
+denseWritePrevious :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWritePrevious DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 2) (packIndex value)
+denseWriteFace :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWriteFace DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 3) (packIndex value)
+{-# INLINE denseWriteOrigin #-}
+{-# INLINE denseWriteNext #-}
+{-# INLINE denseWritePrevious #-}
+{-# INLINE denseWriteFace #-}
+
+denseWriteVertexOut :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWriteVertexOut DenseMutableDcel{dmdVertexOut} index value =
+  writeFlatMutable dmdVertexOut index (if value < 0 then noIndex else packIndex value)
+{-# INLINE denseWriteVertexOut #-}
+
+-- | Materialize the connectivity section for a vertex proven fresh by the
+-- circle-sweep reservation. Arbitrary insertion retains 'markConnected'; this
+-- refined write has no resident case to rediscover, and its aggregate count is
+-- committed once after the sweep's local sections glue.
+denseMarkFreshConnected
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> Int
+  -> ST s ()
+denseMarkFreshConnected dense@DenseMutableDcel{dmdOwner = MutableDcel{mdInitialPointCount}, dmdNewConnected} vertex outgoing = do
+  writeFlatMutable dmdNewConnected (vertex - mdInitialPointCount) 1
+  denseWriteVertexOut dense vertex outgoing
+{-# INLINE denseMarkFreshConnected #-}
+
+denseCommitFreshConnections
+  :: DenseMutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s ()
+denseCommitFreshConnections DenseMutableDcel{dmdOwner = MutableDcel{mdConnectedCount}} count =
+  modifySTRef' mdConnectedCount (+ count)
+{-# INLINE denseCommitFreshConnections #-}
+
+denseReadFaceEdge :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
+denseReadFaceEdge DenseMutableDcel{dmdFaceEdge} index = do
+  value <- readFlatMutable dmdFaceEdge index
+  pure (if value == noIndex then -1 else unpackIndex value)
+{-# INLINE denseReadFaceEdge #-}
+
+denseWriteFaceEdge :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+denseWriteFaceEdge DenseMutableDcel{dmdFaceEdge} index value =
+  writeFlatMutable dmdFaceEdge index (if value < 0 then noIndex else packIndex value)
+{-# INLINE denseWriteFaceEdge #-}
+
+denseReadConstraint :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Bool
+denseReadConstraint DenseMutableDcel{dmdConstraint} directed =
+  (/= 0) <$> readFlatMutable dmdConstraint (directed `quot` 2)
+{-# INLINE denseReadConstraint #-}
+
+readOrigin :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readOrigin MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index)
+readNext :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readNext MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 1)
+readPrevious :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readPrevious MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 2)
+readFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readFace MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 3)
+{-# INLINE readOrigin #-}
+{-# INLINE readNext #-}
+{-# INLINE readPrevious #-}
+{-# INLINE readFace #-}
+
+writeOrigin :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writeOrigin MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index) (packIndex value)
+writeNext :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writeNext MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 1) (packIndex value)
+writePrevious :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writePrevious MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 2) (packIndex value)
+writeFace :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writeFace MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 3) (packIndex value)
+{-# INLINE writeOrigin #-}
+{-# INLINE writeNext #-}
+{-# INLINE writePrevious #-}
+{-# INLINE writeFace #-}
+
+readVertexOut :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readVertexOut MutableDcel{mdVertexOut} index = do
+  value <- readPaged mdVertexOut index
+  pure (if value == noIndex then -1 else unpackIndex value)
+{-# INLINE readVertexOut #-}
+
+writeVertexOut :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writeVertexOut MutableDcel{mdVertexOut} index value =
+  writePaged mdVertexOut index (if value < 0 then noIndex else packIndex value)
+{-# INLINE writeVertexOut #-}
+
+readFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
+readFaceEdge MutableDcel{mdFaceEdge} index = do
+  value <- readPaged mdFaceEdge index
+  pure (if value == noIndex then -1 else unpackIndex value)
+{-# INLINE readFaceEdge #-}
+
+writeFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
+writeFaceEdge MutableDcel{mdFaceEdge} index value =
+  writePaged mdFaceEdge index (if value < 0 then noIndex else packIndex value)
+{-# INLINE writeFaceEdge #-}
+
+readConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
+readConstraint MutableDcel{mdConstraint} directed = (/= 0) <$> readPaged mdConstraint (directed `quot` 2)
+{-# INLINE readConstraint #-}
+
+setConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
+setConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
+  let !index = directed `quot` 2
+  current <- readPaged mdConstraint index
+  if current /= 0
+    then pure False
+    else do
+      writePaged mdConstraint index 1
+      modifySTRef' mdConstraintCount (+ 1)
+      modifySTRef' mdConstraintEdges (IntSet.insert index)
+      pure True
+{-# INLINE setConstraint #-}
+
+clearConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
+clearConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
+  let !index = directed `quot` 2
+  current <- readPaged mdConstraint index
+  if current == 0
+    then pure False
+    else do
+      writePaged mdConstraint index 0
+      modifySTRef' mdConstraintCount (subtract 1)
+      modifySTRef' mdConstraintEdges (IntSet.delete index)
+      pure True
+{-# INLINE clearConstraint #-}
diff --git a/src-build/Moonlight/Planar/Internal/OperationState.hs b/src-build/Moonlight/Planar/Internal/OperationState.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/OperationState.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+-- | The state one operation owns while it works: the legalization arena, the
+-- shared scratch arena, and the instrumentation cells. None of it is
+-- topology, so none of it lives on @MutableDcel@ — a transaction allocates
+-- this record when it thaws, hands it down to the operations it runs, and
+-- reads the counters back once when it freezes. The hot loops thread their
+-- stack top, maximum depth and flip count as strict loop variables and charge
+-- these cells once per drain; cold events (one per insertion, one per walk
+-- probe) charge them where they happen.
+module Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , DiagnosticMetric (..)
+  , LegalizationArena (..)
+  , OperationState
+  , newOperationState
+  , legalizationArena
+  , legalizationArenaLength
+  , storeLegalizationArena
+  , writeScratch
+  , readScratch
+  , addCounter
+  , setCounter
+  , maxCounter
+  , readCounter
+  , finalizeBuildStats
+  ) where
+
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word32, Word64)
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  , readGrowable
+  , writeGrowable
+  )
+import Moonlight.Planar.Internal.PackedIndex (packIndex, unpackIndex)
+import Moonlight.Planar.Internal.BuildStats (BuildMetric, BuildStats (..), buildMetricCount)
+
+-- | The typed candidate section owned by one operation. The newtype prevents
+-- unrelated scratch vectors from being handed to the normalizer while
+-- erasing to the same contiguous Word32 arena in the hot path.
+newtype LegalizationArena s = LegalizationArena
+  (MUV.MVector s Word32)
+
+-- | Reported metrics use their canonical schema; diagnostics occupy a
+-- separate finite suffix. Cardinality is not an inhabitable counter.
+data Counter
+  = Reported !BuildMetric
+  | Diagnostic !DiagnosticMetric
+  deriving stock (Eq, Ord, Show)
+
+data DiagnosticMetric
+  = LegalizationCandidates
+  | HullBucketProbeSteps
+  | HullKeyRebuilds
+  deriving stock (Eq, Ord, Enum, Bounded, Show)
+
+counterIndex :: Counter -> Int
+counterIndex (Reported metric) = fromEnum metric
+counterIndex (Diagnostic metric) = buildMetricCount + fromEnum metric
+{-# INLINE counterIndex #-}
+
+-- | One operation's working state. Scratch is fixed and raw: writes always
+-- precede reads within an epoch. The legalization vector is held behind one
+-- reference solely so an adversarial generic drain can grow it without
+-- reintroducing mesh-global work state. A drain reads that reference once,
+-- carries the vector and all stack metrics strictly, and stores it once when
+-- finished; there is no per-candidate reference traffic.
+data OperationState s = OperationState
+  { osLegalizationArena :: !(STRef s (LegalizationArena s))
+  , osScratchArena :: !(GrowableWord32 s)
+  , osCounters :: !(MUV.MVector s Word64)
+  }
+
+-- | Allocate transaction-sized working state. The legalization reservation is a
+-- starting size, not a semantic limit: a generic flip pops one candidate and may
+-- push four, no linear worst-case depth follows from the input size, and
+-- overflow grows the operation-owned vector by doubling. So the reservation is
+-- capped. A transaction-sized one charges every singleton verb a fresh block
+-- group whose tail no drain reaches, and the doublings that reach a real peak
+-- copy less in total than reserving that tail costs.
+--
+-- Scratch retains four disjoint half-edge sections as its semantic limit: the
+-- removal kernel locally glues border, retired-edge, retired-face, and new-fan
+-- sections there. Its physical storage still grows only with the star or strip
+-- actually observed. Reserving the full mesh bound made every singleton
+-- persistent edit allocate an arena whose untouched tail was orders of
+-- magnitude larger than the edit.
+newOperationState :: Int -> ST s (OperationState s)
+newOperationState halfEdgeCapacity = do
+  initialArena <- LegalizationArena <$> MUV.new (min (2 * halfEdgeCapacity + 64) initialLegalizationReservation)
+  arena <- newSTRef initialArena
+  scratch <- newGrowableWord32 (min 64 (halfEdgeCapacity + 8))
+  counters <- MUV.replicate (buildMetricCount + fromEnum (maxBound :: DiagnosticMetric) + 1) 0
+  pure
+    OperationState
+      { osLegalizationArena = arena
+      , osScratchArena = scratch
+      , osCounters = counters
+      }
+
+-- | Ordinary cavities are tiny; exceptional stars and recovered strips grow
+-- geometrically behind the sealed arena rather than taxing every singleton
+-- edit for a pathological frontier it never visits.
+initialLegalizationReservation :: Int
+initialLegalizationReservation = 64
+
+legalizationArena :: OperationState s -> ST s (LegalizationArena s)
+legalizationArena = readSTRef . osLegalizationArena
+{-# INLINE legalizationArena #-}
+
+legalizationArenaLength :: LegalizationArena s -> Int
+legalizationArenaLength (LegalizationArena values) = MUV.length values
+{-# INLINE legalizationArenaLength #-}
+
+storeLegalizationArena :: OperationState s -> LegalizationArena s -> ST s ()
+storeLegalizationArena = writeSTRef . osLegalizationArena
+{-# INLINE storeLegalizationArena #-}
+
+-- | Write a scratch cell. Collection walks carry their topology-derived
+-- termination budgets; the growable arena is physical storage, not a second
+-- semantic bound capable of disagreeing with those typed obstructions.
+writeScratch :: OperationState s -> Int -> Int -> ST s ()
+writeScratch OperationState{osScratchArena} index value =
+  writeGrowable osScratchArena index (packIndex value)
+{-# INLINE writeScratch #-}
+
+readScratch :: OperationState s -> Int -> ST s Int
+readScratch OperationState{osScratchArena} index = unpackIndex <$> readGrowable osScratchArena index
+{-# INLINE readScratch #-}
+
+addCounter :: OperationState s -> Counter -> Int -> ST s ()
+addCounter OperationState{osCounters} counter amount = do
+  let !index = counterIndex counter
+  current <- MUV.unsafeRead osCounters index
+  MUV.unsafeWrite osCounters index (current + fromIntegral amount)
+{-# INLINE addCounter #-}
+
+setCounter :: OperationState s -> Counter -> Int -> ST s ()
+setCounter OperationState{osCounters} counter value =
+  MUV.unsafeWrite osCounters (counterIndex counter) (fromIntegral value)
+{-# INLINE setCounter #-}
+
+maxCounter :: OperationState s -> Counter -> Int -> ST s ()
+maxCounter OperationState{osCounters} counter value = do
+  let !index = counterIndex counter
+  current <- MUV.unsafeRead osCounters index
+  when (fromIntegral value > current) (MUV.unsafeWrite osCounters index (fromIntegral value))
+{-# INLINE maxCounter #-}
+
+readCounter :: OperationState s -> Counter -> ST s Int
+readCounter OperationState{osCounters} counter =
+  fromIntegral <$> MUV.unsafeRead osCounters (counterIndex counter)
+{-# INLINE readCounter #-}
+
+-- | Transfer the reported prefix at the completed operation's publication
+-- boundary. Every caller has finished charging counters and never uses the
+-- operation again. The immutable result escapes ST; its mutable owner cannot.
+finalizeBuildStats :: OperationState s -> ST s BuildStats
+finalizeBuildStats OperationState{osCounters} =
+  BuildStats <$> U.unsafeFreeze (MUV.slice 0 buildMetricCount osCounters)
+{-# INLINE finalizeBuildStats #-}
diff --git a/src-build/Moonlight/Planar/Internal/Probe.hs b/src-build/Moonlight/Planar/Internal/Probe.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Probe.hs
@@ -0,0 +1,60 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeFamilyDependencies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- | Compile-time instrumentation switch. The bulk board is the measuring
+-- instrument, so per-candidate probe counters must not exist on its path at
+-- all: gating behind a flag the compiler can eliminate is the only honest way
+-- to have both one sweep implementation and an undistorted measurement. The
+-- flag is a type, not a value: at 'ProbeOff the counter type is @()@, so no
+-- arithmetic on it can survive in the worker — erasure is a property of the
+-- kind, not of the optimizer's mood. The instrumented entry instantiates the
+-- same code at 'ProbeOn and pays the cold counter writes itself.
+module Moonlight.Planar.Internal.Probe
+  ( Probe (..)
+  , ProbeCounter
+  , KnownProbe (..)
+  ) where
+
+import Control.Monad.ST (ST)
+import Data.Kind (Type)
+import Moonlight.Planar.Internal.OperationState
+  ( Counter
+  , OperationState
+  , addCounter
+  )
+
+-- | The instrumentation switch, promoted to a kind by @DataKinds@.
+data Probe = ProbeOff | ProbeOn
+
+-- | The counter a probe site threads. At 'ProbeOff there is nothing to
+-- thread; at 'ProbeOn it is a strict @Int@.
+type family ProbeCounter (probe :: Probe) = (counter :: Type) | counter -> probe where
+  ProbeCounter 'ProbeOff = ()
+  ProbeCounter 'ProbeOn = Int
+
+class KnownProbe probe where
+  probeZero :: ProbeCounter probe
+  probeBump :: ProbeCounter probe -> ProbeCounter probe
+  -- | Charge one finished counter to the operation's diagnostic cell. At
+  -- 'ProbeOff this is @pure ()@ and disappears with the dictionary; at
+  -- 'ProbeOn it is one cold vector write per drain or probe site, on the
+  -- instrumented entry that asked for it.
+  probeCharge :: OperationState s -> Counter -> ProbeCounter probe -> ST s ()
+
+instance KnownProbe 'ProbeOff where
+  probeZero = ()
+  probeBump = id
+  probeCharge _ _ _ = pure ()
+  {-# INLINE probeZero #-}
+  {-# INLINE probeBump #-}
+  {-# INLINE probeCharge #-}
+
+instance KnownProbe 'ProbeOn where
+  probeZero = 0
+  probeBump counter = counter + 1
+  probeCharge operation counter value = addCounter operation counter value
+  {-# INLINE probeZero #-}
+  {-# INLINE probeBump #-}
+  {-# INLINE probeCharge #-}
diff --git a/src-build/Moonlight/Planar/Internal/Refinement.hs b/src-build/Moonlight/Planar/Internal/Refinement.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Refinement.hs
@@ -0,0 +1,1723 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Ruppert refinement over three local worklists: forced segment splits,
+-- fixed-edge encroachment candidates, and skinny faces. A face candidate is
+-- transactional: preflight locates the circumcenter and follows the
+-- prospective legalization cavity without mutating topology; a fixed edge the
+-- cavity meets is tested against the point's diametral disk and aborts the
+-- plan into a segment split; otherwise the located site commits directly.
+module Moonlight.Planar.Internal.Refinement
+  ( RefinementInitialSeed (..)
+  , RefinementDomain (..)
+  , refineMutable
+  ) where
+
+import Moonlight.Planar.BuildStats (BuildMetric (LegalizationMaxStack, SteinerPoints, RefinementFaceChecks, RefinementQueuePops))
+
+import Control.Monad (filterM, forM_, unless, when)
+import Control.Monad.ST (ST)
+import Data.Foldable (traverse_)
+import qualified Data.IntSet as IntSet
+import Data.List (find)
+import qualified Data.List as List
+import qualified Data.IntMap.Strict as IntMap
+import Data.Maybe (catMaybes, fromMaybe, isJust)
+import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Internal.HandleDefs (FaceId (..), UndirectedEdgeId (..))
+import Moonlight.Planar.Internal.DcelOperations.Legalize (legalizeEdges)
+import Moonlight.Planar.Internal.DcelOperations.Subdivide (insertOnEdge)
+import Moonlight.Planar.Internal.DcelOperations.Twin (reverseIndex)
+import Moonlight.Planar.Internal.Dyadic (exactDiametralDot)
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , clearGrowable
+  , growableLength
+  , newGrowableWord32
+  , popGrowableOr
+  , pushGrowable
+  , readGrowable
+  )
+import Moonlight.Planar.Internal.Location
+  ( MutableLocation (..)
+  , locateMutable
+  , locateMutableWithin
+  )
+import Moonlight.Planar.Internal.FaceQueue
+  ( FaceQueue
+  , newFaceQueue
+  , popFace
+  , pushFace
+  )
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel (..)
+  , addEdgeBlock
+  , addFaceBlock
+  , appendVertex
+  , directedEdgeCount
+  , edgeOriginPoint
+  , ensureCellCapacity
+  , faceCount
+  , faceEdges
+  , markConnected
+  , pointAt
+  , pointCapacity
+  , readConstraint
+  , readFace
+  , readNext
+  , readOrigin
+  , readPrevious
+  , readVertexOut
+  , resetEdgeData
+  , resetFaceData
+  , setCycle3
+  , writeOrigin
+  , writeVertexOut
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( Counter (..)
+  , OperationState
+  , addCounter
+  , maxCounter
+  , readScratch
+  , writeScratch
+  )
+import Moonlight.Planar.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
+import Moonlight.Planar.Internal.Predicates
+  ( inCircle
+  , inDiametralCircle
+  , orient2d
+  )
+import Moonlight.Planar.Internal.Probe (Probe (..))
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Math (circumcenter, midpoint, squaredDistance, squaredDistanceWide, triangleArea, triangleRadiusEdgeRatioSquaredWithArea, triangleRadiusEdgeRatioSquaredWithAreaAndSides)
+import Moonlight.Planar.Scalar (isFinite, validateCoordinate)
+import Moonlight.Planar.Insertion (insertExistingVertexAtLocation)
+import Moonlight.Planar.Internal.Types (BuildError (..), RefinementParameters (..))
+import Moonlight.Planar.Point (Point (..))
+
+-- | The worklists refinement owns for the duration of one transaction. Cavity
+-- and segment queues are local to the current rewrite; the face-capacity mark
+-- planes remain a bounded publication workspace until the compact arena is
+-- replaced by a domain-indexed one.
+data BoolMarks s
+  = DenseBoolMarks !(MUV.MVector s Bool)
+  | SparseBoolMarks !(STRef s IntSet.IntSet)
+
+data EpochMarks s
+  = DenseEpochMarks !(MUV.MVector s Word32)
+  | SparseEpochMarks !(STRef s (IntMap.IntMap Word32))
+
+data PairSlots s
+  = DensePairSlots !(MUV.MVector s Word32)
+  | SparsePairSlots !(STRef s (IntMap.IntMap Word32))
+
+data SegmentOrigins s
+  = DenseSegmentOrigins !(MUV.MVector s Word32)
+  | SparseSegmentOrigins !(STRef s (IntMap.IntMap (Word32, Word32)))
+
+newBoolMarks :: Maybe Int -> ST s (BoolMarks s)
+newBoolMarks capacity =
+  case capacity of
+    Just size -> DenseBoolMarks <$> MUV.replicate (max 1 size) False
+    Nothing -> SparseBoolMarks <$> newSTRef IntSet.empty
+
+readBoolMark :: BoolMarks s -> Int -> ST s Bool
+readBoolMark marks index =
+  case marks of
+    DenseBoolMarks values -> MUV.unsafeRead values index
+    SparseBoolMarks values -> IntSet.member index <$> readSTRef values
+
+writeBoolMark :: BoolMarks s -> Int -> Bool -> ST s ()
+writeBoolMark marks index enabled =
+  case marks of
+    DenseBoolMarks values -> MUV.unsafeWrite values index enabled
+    SparseBoolMarks values ->
+      modifySTRef' values
+        (if enabled then IntSet.insert index else IntSet.delete index)
+
+newEpochMarks :: Maybe Int -> ST s (EpochMarks s)
+newEpochMarks capacity =
+  case capacity of
+    Just size -> DenseEpochMarks <$> MUV.replicate (max 1 size) 0
+    Nothing -> SparseEpochMarks <$> newSTRef IntMap.empty
+
+readEpochMark :: EpochMarks s -> Int -> ST s Word32
+readEpochMark marks index =
+  case marks of
+    DenseEpochMarks values -> MUV.unsafeRead values index
+    SparseEpochMarks values -> fromMaybe 0 . IntMap.lookup index <$> readSTRef values
+
+writeEpochMark :: EpochMarks s -> Int -> Word32 -> ST s ()
+writeEpochMark marks index epoch =
+  case marks of
+    DenseEpochMarks values -> MUV.unsafeWrite values index epoch
+    SparseEpochMarks values -> modifySTRef' values (IntMap.insert index epoch)
+
+clearEpochMarks :: EpochMarks s -> ST s ()
+clearEpochMarks marks =
+  case marks of
+    DenseEpochMarks values -> MUV.set values 0
+    SparseEpochMarks values -> writeSTRef values IntMap.empty
+
+newPairSlots :: Maybe Int -> ST s (PairSlots s)
+newPairSlots capacity =
+  case capacity of
+    Just size -> DensePairSlots <$> MUV.replicate (max 1 size) noIndex
+    Nothing -> SparsePairSlots <$> newSTRef IntMap.empty
+
+readPairSlot :: PairSlots s -> Int -> ST s Word32
+readPairSlot slots pair =
+  case slots of
+    DensePairSlots values -> MUV.unsafeRead values pair
+    SparsePairSlots values -> fromMaybe noIndex . IntMap.lookup pair <$> readSTRef values
+
+writePairSlot :: PairSlots s -> Int -> Word32 -> ST s ()
+writePairSlot slots pair value =
+  case slots of
+    DensePairSlots values -> MUV.unsafeWrite values pair value
+    SparsePairSlots values ->
+      modifySTRef' values
+        (if value == noIndex then IntMap.delete pair else IntMap.insert pair value)
+
+newSegmentOrigins :: Maybe Int -> ST s (SegmentOrigins s)
+newSegmentOrigins capacity =
+  case capacity of
+    Just size -> DenseSegmentOrigins <$> MUV.replicate (2 * max 1 size) noIndex
+    Nothing -> SparseSegmentOrigins <$> newSTRef IntMap.empty
+
+readSegmentOrigin :: SegmentOrigins s -> Int -> ST s (Word32, Word32)
+readSegmentOrigin origins vertex = do
+  case origins of
+    DenseSegmentOrigins values ->
+      (,)
+        <$> MUV.unsafeRead values (2 * vertex)
+        <*> MUV.unsafeRead values (2 * vertex + 1)
+    SparseSegmentOrigins values -> do
+      stored <- readSTRef values
+      pure (fromMaybe (noIndex, noIndex) (IntMap.lookup vertex stored))
+
+writeSegmentOrigin :: SegmentOrigins s -> Int -> Word32 -> Word32 -> ST s ()
+writeSegmentOrigin origins vertex first second =
+  case origins of
+    DenseSegmentOrigins values -> do
+      MUV.unsafeWrite values (2 * vertex) first
+      MUV.unsafeWrite values (2 * vertex + 1) second
+    SparseSegmentOrigins values ->
+      modifySTRef' values (IntMap.insert vertex (first, second))
+
+data Workspace s = Workspace
+  { wsQueue :: !(FaceQueue s)
+  , wsExcluded :: !(BoolMarks s)
+  , wsPermitted :: !(Maybe (BoolMarks s))
+  , wsVisited :: !(Maybe (BoolMarks s))
+  , wsInterfaceBoundaryReads :: !(STRef s Int)
+  , wsBoundaryCrossingAttempts :: !(STRef s Int)
+  , wsForcedSplits :: !(GrowableWord32 s)
+  , wsEncroachment :: !(GrowableWord32 s)
+  -- | Where each pair's live entry sits in 'wsEncroachment', 'noIndex' when
+  -- the pair is not queued. A re-push relocates the pair to the top and leaves
+  -- the older entry behind as a slot that no longer names it, so a pair is
+  -- tested once at its most recent offer instead of once per offer.
+  , wsEncroachmentSlot :: !(PairSlots s)
+  -- | Steiner vertices on constraints carry the original segment's endpoint
+  -- pair, two words per vertex, 'noIndex' when the vertex is not on one.
+  , wsSegmentOrigin :: !(SegmentOrigins s)
+  -- | Cavity discovery state: an epoch stamp per face, edge pair and boundary
+  -- vertex, plus the three records a successful commit consumes. A cavity is
+  -- explored without mutating topology; the commit either fans the recorded
+  -- boundary or abandons the records entirely.
+  , wsCavityEpoch :: !(STRef s Word32)
+  , wsCavityFaceMarks :: !(EpochMarks s)
+  , wsCavityPairMarks :: !(EpochMarks s)
+  , wsCavityVertexMarks :: !(EpochMarks s)
+  , wsCavityFaces :: !(GrowableWord32 s)
+  , wsCavityInternal :: !(GrowableWord32 s)
+  , wsCavityBoundary :: !(GrowableWord32 s)
+  , wsCavityCocircular :: !(GrowableWord32 s)
+  -- | Signatures of the faces a pending commit may rewrite, four words each:
+  -- the face and its sorted vertex triple. Empty without a domain.
+  , wsCavitySignatures :: !(GrowableWord32 s)
+  -- | The recorded boundary in cycle order, written by the chaining walk and
+  -- read by the fan.
+  , wsCavityChain :: !(GrowableWord32 s)
+  -- | The flood and hull-simulation stack. The old mesh-global work stack
+  -- served both; the transaction owns it now, cleared before either use.
+  , wsCavityWork :: !(GrowableWord32 s)
+  }
+
+data FaceHint
+  = FaceAcceptable
+  | FaceMustSplitLongestEdge
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Double
+  | FaceMustRefine
+  | FaceShouldRefine
+
+data EdgeSplitLaw
+  = SplitFixedSegment
+  | SplitQualityEdge
+
+-- | The initial support a refinement run is allowed to inspect. Subsequent
+-- cavity, star, and fixed-edge propagation remains exactly the ordinary
+-- refinement law; only the first offers differ. The constructor is internal
+-- so public callers cannot smuggle a mutable worklist across the transaction
+-- boundary.
+data RefinementInitialSeed
+  = RefineEveryFace
+  | RefineSeededFaces !IntSet.IntSet
+
+-- | A checked local section. Face and edge integers are admitted by the
+-- immutable constructor in "Moonlight.Planar.Refinement"; the mutable
+-- interpreter sees only the already-descended dense membership witnesses.
+data RefinementDomain = RefinementDomain
+  { refinementDomainPermittedFaces :: !IntSet.IntSet
+  , refinementDomainInterfacePairs :: !IntSet.IntSet
+  , refinementDomainInputFaceCount :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Eq, Show)
+
+-- | Refine the existing finite DCEL. Fixed edges (constraints and the convex
+-- hull) are legal barriers: a candidate whose prospective cavity meets one
+-- inside its diametral disk is abandoned and the segment is split instead.
+-- The result is @(worklists-drained, Steiner-count, excluded-outer-faces)@.
+refineMutable
+  :: forall s vertex directed undirected face. (Point -> vertex)
+  -> MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> RefinementParameters
+  -> Int
+  -> IntSet.IntSet
+  -> RefinementInitialSeed
+  -> Maybe RefinementDomain
+  -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
+refineMutable makeVertex mutable operation parameters budget initialExcludedFaces initialSeed domain = do
+  let !faceBound =
+        case domain of
+          Nothing -> Just (3 * pointCapacity mutable + 8)
+          Just _ -> Nothing
+      !pairBound =
+        case domain of
+          Nothing -> Just (mdHalfCapacity mutable `quot` 2)
+          Just _ -> Nothing
+      !vertexBound =
+        case domain of
+          Nothing -> Just (pointCapacity mutable)
+          Just _ -> Nothing
+  wsQueue <- newFaceQueue faceBound
+  wsExcluded <- newBoolMarks faceBound
+  wsPermitted <- traverse (const (newBoolMarks Nothing)) domain
+  wsVisited <- traverse (const (newBoolMarks Nothing)) domain
+  wsInterfaceBoundaryReads <- newSTRef 0
+  wsBoundaryCrossingAttempts <- newSTRef 0
+  traverse_
+    (\membership ->
+       traverse_
+         (\face -> writeBoolMark membership face True)
+         (maybe [] (IntSet.toAscList . refinementDomainPermittedFaces) domain)
+    )
+    wsPermitted
+  when (refineExcludeOuterFaces parameters) $
+    traverse_ (\face -> writeBoolMark wsExcluded face True)
+      (IntSet.toAscList initialExcludedFaces)
+  wsForcedSplits <- newGrowableWord32 16
+  wsEncroachment <- newGrowableWord32 64
+  wsEncroachmentSlot <- newPairSlots pairBound
+  wsSegmentOrigin <- newSegmentOrigins vertexBound
+  wsCavityEpoch <- newSTRef 0
+  wsCavityFaceMarks <- newEpochMarks faceBound
+  wsCavityPairMarks <- newEpochMarks pairBound
+  wsCavityVertexMarks <- newEpochMarks vertexBound
+  wsCavityFaces <- newGrowableWord32 16
+  wsCavityInternal <- newGrowableWord32 16
+  wsCavityBoundary <- newGrowableWord32 16
+  wsCavityCocircular <- newGrowableWord32 4
+  wsCavitySignatures <- newGrowableWord32 64
+  wsCavityChain <- newGrowableWord32 16
+  wsCavityWork <- newGrowableWord32 16
+  let workspace = Workspace{wsQueue, wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts, wsForcedSplits, wsEncroachment, wsEncroachmentSlot, wsSegmentOrigin, wsCavityEpoch, wsCavityFaceMarks, wsCavityPairMarks, wsCavityVertexMarks, wsCavityFaces, wsCavityInternal, wsCavityBoundary, wsCavityCocircular, wsCavitySignatures, wsCavityChain, wsCavityWork}
+  seedInitialWork workspace initialSeed
+  loop workspace IntMap.empty 0
+ where
+  !limit = max 0 budget
+
+  -- The three bounds every queued face is measured against. They are fixed for
+  -- the whole run, and the ratio bound is squared once here rather than once
+  -- per face the ratio test reaches.
+  !maximumArea = refineMaxArea parameters
+  !minimumArea = refineMinArea parameters
+  !maximumSquaredEdgeLength = fmap (\bound -> bound * bound) (refineMaxEdgeLength parameters)
+  !squaredRatioBound = case refineMaxRadiusEdgeRatio parameters of
+    Nothing -> Nothing
+    Just bound -> Just $! squareBound bound
+
+  -- One charged stack push: the old mesh-global 'pushWork' wrapper fed the
+  -- legalization depth counter on every push, and the hull-simulation sites
+  -- used that wrapper deliberately. The cavity flood's bypass stays raw.
+  pushChargedWork :: GrowableWord32 s -> Int -> ST s ()
+  pushChargedWork work value = do
+    pushGrowable work (packIndex value)
+    size <- growableLength work
+    maxCounter operation (Reported LegalizationMaxStack) size
+
+  -- Offer a fixed edge pair for the encroachment question. An offer always
+  -- wins the pair's slot, so the pair is answered at its most recent offer and
+  -- the entries it left behind are recognised as superseded when reached.
+  pushEncroachment :: Workspace s -> Int -> ST s ()
+  pushEncroachment Workspace{wsEncroachment, wsEncroachmentSlot} pair = do
+    index <- growableLength wsEncroachment
+    pushGrowable wsEncroachment (packIndex pair)
+    writePairSlot wsEncroachmentSlot pair (packIndex index)
+
+  -- 'noIndex' when the offers are drained.
+  popEncroachment :: Workspace s -> ST s Word32
+  popEncroachment Workspace{wsEncroachment, wsEncroachmentSlot} = go
+   where
+    go = do
+      size <- growableLength wsEncroachment
+      if size <= 0
+        then pure noIndex
+        else do
+          let !index = size - 1
+          packed <- popGrowableOr noIndex wsEncroachment
+          let !pair = fromIntegral packed
+          slot <- readPairSlot wsEncroachmentSlot pair
+          if slot == packIndex index
+            then do
+              writePairSlot wsEncroachmentSlot pair noIndex
+              pure packed
+            else go
+
+  -- The cavity epoch stamps three planes and is compared against them, so the
+  -- wrap has to retire every stamp a reused value would answer for.
+  nextCavityEpoch :: Workspace s -> ST s Word32
+  nextCavityEpoch Workspace{wsCavityEpoch, wsCavityFaceMarks, wsCavityPairMarks, wsCavityVertexMarks} = do
+    current <- readSTRef wsCavityEpoch
+    let !next = current + 1
+    if next == 0
+      then do
+        clearEpochMarks wsCavityFaceMarks
+        clearEpochMarks wsCavityPairMarks
+        clearEpochMarks wsCavityVertexMarks
+        writeSTRef wsCavityEpoch 1
+        pure 1
+      else do
+        writeSTRef wsCavityEpoch next
+        pure next
+
+  loop
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> Int
+    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
+  loop workspace@Workspace{wsQueue, wsForcedSplits} pendingOversizedEdges !added
+    | added >= limit = finish workspace pendingOversizedEdges False added
+    | otherwise = do
+        forced <- popGrowableOr noIndex wsForcedSplits
+        if forced /= noIndex
+          then do
+            splitOutcome <- resolveSplit SplitFixedSegment workspace (fromIntegral forced)
+            case splitOutcome of
+              Left obstruction -> pure (Left obstruction)
+              Right split ->
+                if split
+                  then loop workspace pendingOversizedEdges (added + 1)
+                  else do
+                    -- The split refused (degenerate position or a kept
+                    -- constraint). Retrying the face that forced it would loop,
+                    -- so one queued face is sacrificed, exactly the face whose
+                    -- requeue sits on top.
+                    _ <- popFace wsQueue
+                    loop workspace pendingOversizedEdges added
+          else do
+            candidate <- popEncroachment workspace
+            if candidate /= noIndex
+              then do
+                splitOutcome <- checkEncroachment workspace (fromIntegral candidate)
+                case splitOutcome of
+                  Left obstruction -> pure (Left obstruction)
+                  Right split ->
+                    loop
+                      workspace
+                      pendingOversizedEdges
+                      (if split then added + 1 else added)
+              else do
+                next <- popFace wsQueue
+                case next of
+                  Nothing -> finish workspace pendingOversizedEdges True added
+                  Just face -> do
+                    outcome <- handleFace workspace face
+                    case outcome of
+                      Left failure -> pure (Left failure)
+                      Right (gained, pendingEdge) ->
+                        loop
+                          workspace
+                          ( maybe
+                              pendingOversizedEdges
+                              (\(pair, actualSquaredLength) ->
+                                 IntMap.insertWith
+                                   max
+                                   pair
+                                   actualSquaredLength
+                                   pendingOversizedEdges
+                              )
+                              pendingEdge
+                          )
+                          (added + gained)
+
+  finish
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> Bool
+    -> Int
+    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
+  finish workspace@Workspace{wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} pendingOversizedEdges complete added = do
+    pendingObstruction <-
+      if complete
+        then completedMaximumEdgeObstruction workspace pendingOversizedEdges
+        else pure (Right Nothing)
+    case pendingObstruction of
+      Left failure -> pure (Left failure)
+      Right (Just failure) -> pure (Left failure)
+      Right Nothing -> do
+        excludedFaces <- markedIndices wsExcluded
+        visitedFaces <- maybe (pure []) markedIndices wsVisited
+        permittedFaces <- maybe (pure []) markedIndices wsPermitted
+        interfaceBoundaryReads <- readSTRef wsInterfaceBoundaryReads
+        boundaryCrossingAttempts <- readSTRef wsBoundaryCrossingAttempts
+        pure (Right (complete, added, excludedFaces, visitedFaces, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts))
+
+  -- A quality edge that cannot be split is remembered at the point where the
+  -- worklist discovers it.  Only a drained run discharges the pending section;
+  -- a budget-limited run remains an ordinary resumable publication.  The
+  -- final face is read from the constant-degree live edge neighbourhood so the
+  -- reported obstruction retains the former face/edge ordering without an
+  -- all-face audit.
+  completedMaximumEdgeObstruction
+    :: Workspace s
+    -> IntMap.IntMap Double
+    -> ST s (Either BuildError (Maybe BuildError))
+  completedMaximumEdgeObstruction workspace pendingOversizedEdges =
+    case refineMaxEdgeLength parameters of
+      Nothing -> pure (Right Nothing)
+      Just maximumLength -> do
+        candidateResults <-
+          traverse
+            (liveOversizedEdgeCandidate workspace)
+            (IntMap.toAscList pendingOversizedEdges)
+        pure $ do
+          candidates <- catMaybes <$> sequence candidateResults
+          Right
+            ( case List.sortOn (\(face, pair, _) -> (face, pair)) candidates of
+                [] -> Nothing
+                (face, pair, actualSquaredLength) : _ ->
+                  Just
+                    ( RefinementOversizedEdge
+                        (FaceId (fromIntegral face))
+                        (UndirectedEdgeId (fromIntegral pair))
+                        (sqrt actualSquaredLength)
+                        maximumLength
+                    )
+            )
+
+  liveOversizedEdgeCandidate
+    :: Workspace s
+    -> (Int, Double)
+    -> ST s (Either BuildError (Maybe (Int, Int, Double)))
+  liveOversizedEdgeCandidate workspace (pair, actualSquaredLength) = do
+    directedTotal <- directedEdgeCount mutable
+    let directed = 2 * pair
+    if directed < 0 || directed + 1 >= directedTotal
+      then pure (Left RefinementDomainTopologyChanged)
+      else do
+        forwardFace <- readFace mutable directed
+        backwardFace <- readFace mutable (reverseIndex directed)
+        eligibleFaces <-
+          filterM
+            (maximumEdgeFaceEligible workspace)
+            [forwardFace, backwardFace]
+        pure
+          ( Right
+              ( case eligibleFaces of
+                  [] -> Nothing
+                  _ -> Just (minimum eligibleFaces, pair, actualSquaredLength)
+              )
+          )
+
+  maximumEdgeFaceEligible :: Workspace s -> Int -> ST s Bool
+  maximumEdgeFaceEligible Workspace{wsExcluded, wsPermitted} face
+    | face <= 0 = pure False
+    | otherwise = do
+        excluded <- readBoolMark wsExcluded face
+        permitted <-
+          maybe
+            (pure True)
+            (\membership -> readBoolMark membership face)
+            wsPermitted
+        pure (not excluded && permitted)
+
+  markedIndices :: BoolMarks s -> ST s [Int]
+  markedIndices marks =
+    case marks of
+      DenseBoolMarks values -> do
+        let indices = [0 .. MUV.length values - 1]
+        filterM (MUV.unsafeRead values) indices
+      SparseBoolMarks values -> IntSet.toAscList <$> readSTRef values
+
+
+  offerAll :: FaceQueue s -> ST s ()
+  offerAll queue = do
+    faces <- faceCount mutable
+    forM_ [1 .. faces - 1] (pushFace queue)
+
+  -- A seeded run is not a weaker refinement interpreter. It begins at the
+  -- supplied faces and asks only the fixed edges those faces can immediately
+  -- encroach; every later cavity and star contributes its own local closure
+  -- through the same queue and encroachment machinery as the global entry.
+  seedInitialWork :: Workspace s -> RefinementInitialSeed -> ST s ()
+  seedInitialWork workspace seed =
+    case seed of
+      RefineEveryFace -> seedFixedEdges workspace >> offerAll (wsQueue workspace)
+      RefineSeededFaces faces ->
+        traverse_ (seedFace workspace) (IntSet.toAscList faces)
+
+  seedFace :: Workspace s -> Int -> ST s ()
+  seedFace workspace face = do
+    pushFace (wsQueue workspace) face
+    (e0, e1, e2) <- faceEdges mutable face
+    traverse_ (offerFixedEdge workspace) [e0, e1, e2]
+
+  -- Every fixed edge present at entry, queued once for the existing-vertex
+  -- encroachment question. Later candidates arrive from the star walks of the
+  -- insertions that could have created a new encroachment.
+  seedFixedEdges :: Workspace s -> ST s ()
+  seedFixedEdges workspace = do
+    halfEdges <- directedEdgeCount mutable
+    traverse_
+      (offerFixedEdge workspace . (2 *))
+      [0 .. halfEdges `quot` 2 - 1]
+
+  offerFixedEdge :: Workspace s -> Int -> ST s ()
+  offerFixedEdge workspace directed = do
+    fixed <- isFixedEdge directed
+    let !pair = directed `quot` 2
+    -- An interface is a fixed boundary condition, not a quality obligation
+    -- owned by this section. It is still encountered by candidate cavities,
+    -- where any demand to split or cross it is a typed obstruction.
+    when (fixed && not (isInterfacePair pair)) $
+      pushEncroachment workspace pair
+
+  isFixedEdge :: Int -> ST s Bool
+  isFixedEdge directed = do
+    if isInterfacePair (directed `quot` 2)
+      then pure True
+      else do
+        protected <- readConstraint mutable directed
+        if protected
+          then pure True
+          else do
+            own <- readFace mutable directed
+            if own == 0
+              then pure True
+              else (== 0) <$> readFace mutable (reverseIndex directed)
+
+  isInterfacePair :: Int -> Bool
+  isInterfacePair pair =
+    maybe False (IntSet.member pair . refinementDomainInterfacePairs) domain
+  {-# INLINE isInterfacePair #-}
+
+  isHullPair :: Int -> ST s Bool
+  isHullPair pair = do
+    forwardFace <- readFace mutable (2 * pair)
+    backwardFace <- readFace mutable (2 * pair + 1)
+    pure (forwardFace == 0 || backwardFace == 0)
+
+  interfaceCrossing :: Workspace s -> Int -> ST s BuildError
+  interfaceCrossing Workspace{wsPermitted, wsBoundaryCrossingAttempts} pair = do
+    modifySTRef' wsBoundaryCrossingAttempts (+ 1)
+    forwardFace <- readFace mutable (2 * pair)
+    backwardFace <- readFace mutable (2 * pair + 1)
+    joinFace <-
+      case wsPermitted of
+        Nothing -> pure Nothing
+        Just permitted -> do
+          forwardPermitted <- dynamicPermitted permitted forwardFace
+          backwardPermitted <- dynamicPermitted permitted backwardFace
+          pure
+            ( if forwardPermitted
+                then Just forwardFace
+                else if backwardPermitted then Just backwardFace else Nothing
+            )
+    let result =
+          case joinFace of
+            Just face ->
+              RefinementDomainWouldCrossInterface
+                (UndirectedEdgeId (fromIntegral pair))
+                (FaceId (fromIntegral face))
+            Nothing -> RefinementDomainTopologyChanged
+    pure result
+   where
+    dynamicPermitted :: BoolMarks s -> Int -> ST s Bool
+    dynamicPermitted membership face =
+      if face > 0 then readBoolMark membership face else pure False
+
+  -- The edge bound is a hard geometric law and therefore precedes the minimum
+  -- area short-circuit: a long, thin face must get a direct longest-edge split
+  -- even when its area is below the configured minimum. The area bound then
+  -- condemns outright ('FaceMustRefine'); the ratio bound only invites
+  -- refinement, which the input-angle guard may still decline.
+  faceHint :: Int -> Int -> Int -> Point -> Point -> Point -> FaceHint
+  faceHint e0 e1 e2 p0 p1 p2 =
+    case maximumSquaredEdgeLength of
+      Just maximumSquaredLength ->
+        let (!longestEdge, !longestSquaredLength, !side01, !side12, !side20) =
+              faceEdgeMetrics e0 e1 e2 p0 p1 p2
+         in if exceeds longestSquaredLength maximumSquaredLength
+              then
+                FaceMustSplitLongestEdge
+                  (longestEdge `quot` 2)
+                  longestSquaredLength
+              else
+                nonEdgeHint
+                  ( ratioBad
+                      ( triangleRadiusEdgeRatioSquaredWithAreaAndSides
+                          area
+                          side01
+                          side12
+                          side20
+                      )
+                  )
+      Nothing ->
+        nonEdgeHint
+          (ratioBad (triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2))
+   where
+    !area = triangleArea p0 p1 p2
+    areaBad = maybe False (exceeds area) maximumArea
+    belowMinimum = maybe False (area <) minimumArea
+    ratioBad :: Double -> Bool
+    ratioBad ratio = case squaredRatioBound of
+      Nothing -> False
+      Just bound -> exceeds ratio bound
+    nonEdgeHint angleBad
+      | areaBad = FaceMustRefine
+      | belowMinimum = FaceAcceptable
+      | angleBad = FaceShouldRefine
+      | otherwise = FaceAcceptable
+
+  handleFace
+    :: Workspace s
+    -> Int
+    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
+  handleFace workspace@Workspace{wsExcluded, wsPermitted, wsVisited} face = do
+    addCounter operation (Reported RefinementQueuePops) 1
+    permitted <-
+      maybe
+        (pure True)
+        (\membership -> readBoolMark membership face)
+        wsPermitted
+    if not permitted
+      then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral face))))
+      else do
+        traverse_ (\visited -> writeBoolMark visited face True) wsVisited
+        skip <- readBoolMark wsExcluded face
+        if skip
+          then pure (Right (0, Nothing))
+          else do
+            addCounter operation (Reported RefinementFaceChecks) 1
+            (e0, e1, e2) <- faceEdges mutable face
+            p0 <- edgeOriginPoint mutable e0
+            p1 <- edgeOriginPoint mutable e1
+            p2 <- edgeOriginPoint mutable e2
+            case faceHint e0 e1 e2 p0 p1 p2 of
+              FaceAcceptable -> pure (Right (0, Nothing))
+              FaceMustSplitLongestEdge pair actualSquaredLength ->
+                qualitySplit workspace pair actualSquaredLength
+              FaceMustRefine ->
+                fmap
+                  (fmap (\gained -> (gained, Nothing)))
+                  (attemptCandidate workspace face p0 p1 p2)
+              FaceShouldRefine -> do
+                blocked <- inputAngleBlocks workspace e0 e1 e2 p0 p1 p2
+                if blocked
+                  then pure (Right (0, Nothing))
+                  else
+                    fmap
+                      (fmap (\gained -> (gained, Nothing)))
+                      (attemptCandidate workspace face p0 p1 p2)
+
+  faceEdgeMetrics
+    :: Int
+    -> Int
+    -> Int
+    -> Point
+    -> Point
+    -> Point
+    -> (Int, Double, Double, Double, Double)
+  faceEdgeMetrics e0 e1 e2 p0 p1 p2
+    | side01 >= side12 && side01 >= side20 = result e0 side01
+    | side12 >= side20 = result e1 side12
+    | otherwise = result e2 side20
+   where
+    !side01 = squaredDistanceWide p0 p1
+    !side12 = squaredDistanceWide p1 p2
+    !side20 = squaredDistanceWide p2 p0
+    result :: Int -> Double -> (Int, Double, Double, Double, Double)
+    result edge longest = (edge, longest, side01, side12, side20)
+
+  -- An unsplittable quality edge is a pending law obstruction, not permission
+  -- to cross a frozen interface and not immediate failure of a resumable run.
+  qualitySplit
+    :: Workspace s
+    -> Int
+    -> Double
+    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
+  qualitySplit workspace pair actualSquaredLength = do
+    outcome <- resolveSplit SplitQualityEdge workspace pair
+    case outcome of
+      Left (RefinementDomainWouldCrossInterface _ _) ->
+        pure (Right (0, Just (pair, actualSquaredLength)))
+      Left failure -> pure (Left failure)
+      Right True -> pure (Right (1, Nothing))
+      Right False -> pure (Right (0, Just (pair, actualSquaredLength)))
+
+  -- Two subsegments of one original segment meeting at a small input angle
+  -- cannot be refined apart: the angle is fixed by the input geometry. When
+  -- the shortest edge of a ratio-bound face joins two Steiner vertices whose
+  -- lineages share an original endpoint, the face is as good as it gets.
+  inputAngleBlocks :: Workspace s -> Int -> Int -> Int -> Point -> Point -> Point -> ST s Bool
+  inputAngleBlocks Workspace{wsSegmentOrigin} e0 e1 e2 p0 p1 p2 = do
+    let !shortest = shortestFaceEdge e0 e1 e2 p0 p1 p2
+    a <- readOrigin mutable shortest
+    b <- readOrigin mutable (reverseIndex shortest)
+    shared <- sharedSegmentOrigin wsSegmentOrigin a b
+    if not shared
+      then pure False
+      else not <$> isFixedEdge shortest
+
+  shortestFaceEdge :: Int -> Int -> Int -> Point -> Point -> Point -> Int
+  shortestFaceEdge e0 e1 e2 p0 p1 p2
+    | side01 <= side12 && side01 <= side20 = e0
+    | side12 <= side20 = e1
+    | otherwise = e2
+   where
+    !side01 = squaredDistanceWide p0 p1
+    !side12 = squaredDistanceWide p1 p2
+    !side20 = squaredDistanceWide p2 p0
+
+  sharedSegmentOrigin :: SegmentOrigins s -> Int -> Int -> ST s Bool
+  sharedSegmentOrigin lineage a b = do
+    (a0, a1) <- readSegmentOrigin lineage a
+    (b0, b1) <- readSegmentOrigin lineage b
+    pure (a0 /= noIndex && b0 /= noIndex && (a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1))
+
+  validCandidate :: Point -> Maybe (Point)
+  validCandidate point@(Point x y)
+    | validateCoordinate x == Nothing && validateCoordinate y == Nothing = Just point
+    | otherwise = Nothing
+
+  attemptCandidate :: Workspace s -> Int -> Point -> Point -> Point -> ST s (Either BuildError Int)
+  attemptCandidate workspace@Workspace{wsExcluded, wsForcedSplits} face p0 p1 p2 =
+    case circumcenter p0 p1 p2 >>= validCandidate of
+      Nothing -> pure (Right 0)
+      Just point -> do
+        located <-
+          case domain of
+            Nothing -> locateMutable mutable operation (Just face) point
+            Just refinementDomain ->
+              locateMutableWithin
+                (refinementDomainInterfacePairs refinementDomain)
+                mutable
+                operation
+                (Just face)
+                point
+        case located of
+          -- The local locator has proved that the circumcenter lies beyond
+          -- Γ without crossing it.  This is the same boundary-limited fixed
+          -- point as a circumcenter outside a preserved hull: no admissible
+          -- rewrite exists inside this section, so the face is discharged.
+          -- Actual cavity demands involving Γ remain typed obstructions in
+          -- 'exploreThenCommit'.
+          Left (RefinementDomainWouldCrossInterface _ _) -> do
+            modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)
+            pure (Right 0)
+          Left failure -> pure (Left failure)
+          Right site -> case site of
+            MutableOnVertex _ -> pure (Right 0)
+            MutableEmpty -> pure (Right 0)
+            MutableOutsideHull edge
+              | refinePreserveConvexHull parameters -> pure (Right 0)
+              | otherwise -> attemptOutside workspace face point edge
+            MutableOnEdge edge -> do
+              hull <- case domain of
+                Nothing -> pure False
+                Just _ -> isHullPair (edge `quot` 2)
+              if hull
+                then pure (Right 0)
+                else if isInterfacePair (edge `quot` 2)
+                then Left <$> interfaceCrossing workspace (edge `quot` 2)
+                else do
+                  protected <- readConstraint mutable edge
+                  if protected
+                    then do
+                      -- A circumcenter on a constraint is a request to split that
+                      -- constraint, unless constraints are kept whole.
+                      unless (refineKeepConstraintEdges parameters) $
+                        pushGrowable wsForcedSplits (packIndex (edge `quot` 2))
+                      pure (Right 0)
+                    else exploreThenCommit workspace face point site
+            MutableInFace under -> do
+              permittedSite <-
+                maybe
+                  (pure True)
+                  (\membership -> readBoolMark membership under)
+                  (wsPermitted workspace)
+              if not permittedSite
+                then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral under))))
+                else do
+                  excludedSite <- readBoolMark wsExcluded under
+                  if excludedSite
+                    then pure (Right 0)
+                    else exploreThenCommit workspace face point site
+
+  -- Follow the prospective legalization cavity of the candidate without
+  -- mutating topology, recording what the commit will consume: the cavity
+  -- faces, the internal edge pairs, and the ordered boundary. Fixed edges are
+  -- never crossed: the ones the cavity meets become boundary and are tested
+  -- against the candidate's diametral disk. Every encroached fixed edge met
+  -- is queued for splitting and the source face is revisited afterwards; a
+  -- clean cavity is committed directly.
+  exploreThenCommit :: Workspace s -> Int -> Point -> MutableLocation -> ST s (Either BuildError Int)
+  exploreThenCommit workspace@Workspace{wsQueue, wsForcedSplits} face point site = do
+    (visible, encroached) <- exploreCavity workspace point site
+    case find isInterfacePair encroached of
+      Just pair
+        | all isInterfacePair encroached -> pure (Right 0)
+        | otherwise -> Left <$> interfaceCrossing workspace pair
+      Nothing
+        | null encroached -> commitCavity workspace point site visible
+        | otherwise ->
+            do
+              pushable <- filterM splittablePair encroached
+              traverse_ (\pair -> pushGrowable wsForcedSplits (packIndex pair)) pushable
+              unless (null pushable) $ pushFace wsQueue face
+              pure (Right 0)
+
+  splittablePair :: Int -> ST s Bool
+  splittablePair pair = do
+    hull <- case domain of
+      Nothing -> pure False
+      Just _ -> isHullPair pair
+    if hull || isInterfacePair pair
+      then pure False
+      else if refineKeepConstraintEdges parameters
+        then not <$> readConstraint mutable (2 * pair)
+        else pure True
+
+  admitStar :: Workspace s -> Int -> ST s ()
+  admitStar Workspace{wsPermitted} vertex =
+    case wsPermitted of
+      Just membership ->
+        forEachStarFace vertex $ \face -> rememberPermittedFace membership face
+      Nothing -> pure ()
+
+  rememberPermittedFace :: BoolMarks s -> Int -> ST s ()
+  rememberPermittedFace membership face =
+    if face <= 0
+      then pure ()
+      else do
+        already <- readBoolMark membership face
+        unless already $ do
+          writeBoolMark membership face True
+
+  retirePermittedFace :: Workspace s -> Int -> ST s ()
+  retirePermittedFace Workspace{wsPermitted} face = do
+    when (face >= 0) $ do
+      traverse_
+        (\membership -> writeBoolMark membership face False)
+        wsPermitted
+
+  retirePermittedFaces :: Workspace s -> GrowableWord32 s -> ST s ()
+  retirePermittedFaces workspace faces = do
+    count <- growableLength faces
+    traverse_
+      (\index -> do
+         face <- unpackIndex <$> readGrowable faces index
+         retirePermittedFace workspace face
+      )
+      [0 .. count - 1]
+
+  -- Permitted membership is retired for every face a commit rewrites. The
+  -- faces a commit can rewrite are read before it as one signature each, the
+  -- face and its sorted vertex triple, and compared after it. Without a domain
+  -- there is no membership to retire and nothing is read.
+  captureCavitySignatures :: Workspace s -> ST s ()
+  captureCavitySignatures Workspace{wsPermitted, wsCavityFaces, wsCavitySignatures} = do
+    clearGrowable wsCavitySignatures
+    when (isJust wsPermitted) $ do
+      count <- growableLength wsCavityFaces
+      let record !index
+            | index >= count = pure ()
+            | otherwise = do
+                face <- unpackIndex <$> readGrowable wsCavityFaces index
+                recordFaceSignature wsCavitySignatures face
+                record (index + 1)
+      record 0
+
+  captureStarSignatures :: Workspace s -> Int -> Int -> ST s ()
+  captureStarSignatures Workspace{wsPermitted, wsCavitySignatures} first second = do
+    clearGrowable wsCavitySignatures
+    when (isJust wsPermitted) $ do
+      forEachStarFace first (recordFaceSignature wsCavitySignatures)
+      forEachStarFace second (recordFaceSignature wsCavitySignatures)
+
+  recordFaceSignature :: GrowableWord32 s -> Int -> ST s ()
+  recordFaceSignature signatures face =
+    when (face > 0) $ do
+      (v0, v1, v2) <- faceVertexTriple face
+      pushGrowable signatures (packIndex face)
+      pushGrowable signatures (packIndex v0)
+      pushGrowable signatures (packIndex v1)
+      pushGrowable signatures (packIndex v2)
+
+  retireRewrittenFaces :: Workspace s -> ST s ()
+  retireRewrittenFaces workspace@Workspace{wsCavitySignatures} = do
+    count <- growableLength wsCavitySignatures
+    let compareEntry !index
+          | index + 4 > count = pure ()
+          | otherwise = do
+              face <- unpackIndex <$> readGrowable wsCavitySignatures index
+              v0 <- unpackIndex <$> readGrowable wsCavitySignatures (index + 1)
+              v1 <- unpackIndex <$> readGrowable wsCavitySignatures (index + 2)
+              v2 <- unpackIndex <$> readGrowable wsCavitySignatures (index + 3)
+              (after0, after1, after2) <- faceVertexTriple face
+              when (v0 /= after0 || v1 /= after1 || v2 /= after2) $
+                retirePermittedFace workspace face
+              compareEntry (index + 4)
+    compareEntry 0
+
+  faceVertexTriple :: Int -> ST s (Int, Int, Int)
+  faceVertexTriple face = do
+    (e0, e1, e2) <- faceEdges mutable face
+    v0 <- readOrigin mutable e0
+    v1 <- readOrigin mutable e1
+    v2 <- readOrigin mutable e2
+    pure (sortedTriple v0 v1 v2)
+
+  -- The flood never mutates, so a popped edge can be retested idempotently:
+  -- an edge whose far face conflicts crosses, marks the face, and pushes that
+  -- face's other two edges; each directed edge has at most its two
+  -- face-neighbours as pushers, and a revisited crossing is deduplicated by
+  -- the face and pair stamps. Boundary visibility for the fan is accumulated
+  -- in place: every boundary edge is asked here, where its endpoints are
+  -- already loaded, whether the point sees it from the cavity side.
+  exploreCavity :: Workspace s -> Point -> MutableLocation -> ST s (Bool, [Int])
+  exploreCavity workspace@Workspace{wsVisited, wsCavityFaceMarks, wsCavityPairMarks, wsCavityFaces, wsCavityInternal, wsCavityBoundary, wsCavityCocircular, wsCavityWork} point site = do
+    next <- nextCavityEpoch workspace
+    clearGrowable wsCavityFaces
+    clearGrowable wsCavityInternal
+    clearGrowable wsCavityBoundary
+    clearGrowable wsCavityCocircular
+    clearGrowable wsCavityWork
+    seedVisible <- case site of
+      MutableInFace under -> do
+        markFace next under
+        (e0, e1, e2) <- faceEdges mutable under
+        pushCavityWork e0
+        pushCavityWork e1
+        pushCavityWork e2
+        pure True
+      MutableOnEdge edge -> do
+        seedAcross next edge
+        seedAcross next (reverseIndex edge)
+        -- The located edge is never encroachment-tested: a point on it lies
+        -- trivially inside its own diametral circle, and the question would
+        -- force the same split forever. A fixed edge is boundary; any other
+        -- is the cavity's first internal pair. Its visibility still counts.
+        fixed <- isFixedEdge edge
+        if fixed
+          then do
+            pushGrowable wsCavityBoundary (packIndex edge)
+            from <- edgeOriginPoint mutable edge
+            to <- edgeOriginPoint mutable (reverseIndex edge)
+            pure (orient2d from to point == GT)
+          else do
+            recordInternal next (edge `quot` 2)
+            pure True
+      _ -> pure True
+    drain next [] seedVisible
+   where
+    markFace :: Word32 -> Int -> ST s ()
+    markFace epoch face = do
+      writeEpochMark wsCavityFaceMarks face epoch
+      traverse_ (\visited -> writeBoolMark visited face True) wsVisited
+      pushGrowable wsCavityFaces (packIndex face)
+
+    isMarked :: Word32 -> Int -> ST s Bool
+    isMarked epoch face = (== epoch) <$> readEpochMark wsCavityFaceMarks face
+
+    recordInternal :: Word32 -> Int -> ST s ()
+    recordInternal epoch pair = do
+      seen <- readEpochMark wsCavityPairMarks pair
+      when (seen /= epoch) $ do
+        writeEpochMark wsCavityPairMarks pair epoch
+        pushGrowable wsCavityInternal (packIndex pair)
+
+    -- The flood's stack traffic is transaction-local and owns no diagnostics;
+    -- the charged wrapper would report every push as legalization depth.
+    pushCavityWork :: Int -> ST s ()
+    pushCavityWork value = pushGrowable wsCavityWork (packIndex value)
+
+    seedAcross :: Word32 -> Int -> ST s ()
+    seedAcross epoch directed = do
+      adjacent <- readFace mutable directed
+      when (adjacent /= 0) $ do
+        markFace epoch adjacent
+        next <- readNext mutable directed
+        previous <- readPrevious mutable directed
+        pushCavityWork next
+        pushCavityWork previous
+
+    -- Visibility is read by the caller only when nothing was encroached, so
+    -- the first encroached edge retires the question: every later boundary
+    -- edge skips the orientation that would only be conjoined into a value
+    -- about to be discarded.
+    drain :: Word32 -> [Int] -> Bool -> ST s (Bool, [Int])
+    drain epoch = go
+     where
+      go !acc !visible = do
+        packed <- popGrowableOr noIndex wsCavityWork
+        if packed == noIndex
+          then pure (visible, acc)
+          else do
+            let !directed = fromIntegral packed
+            fixed <- isFixedEdge directed
+            if fixed
+              then do
+                pushGrowable wsCavityBoundary (packIndex directed)
+                from <- edgeOriginPoint mutable directed
+                to <- edgeOriginPoint mutable (reverseIndex directed)
+                let pair = directed `quot` 2
+                hull <- case domain of
+                  Nothing -> pure False
+                  Just _ -> isHullPair pair
+                let encroached =
+                      if isInterfacePair pair || hull
+                        then strictlyInDiametralCircle from to point
+                        else inDiametralCircle from to point
+                when (isInterfacePair pair) $
+                  modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)
+                if encroached
+                  then go (pair : acc) False
+                  else go acc (visible && orient2d from to point == GT)
+              else do
+                across <- readFace mutable (reverseIndex directed)
+                marked <- isMarked epoch across
+                if marked
+                  then do
+                    recordInternal epoch (directed `quot` 2)
+                    go acc visible
+                  else do
+                    let !twin = reverseIndex directed
+                    acrossPrevious <- readPrevious mutable twin
+                    opposite <- readOrigin mutable acrossPrevious
+                    from <- edgeOriginPoint mutable directed
+                    to <- pointAt mutable =<< readOrigin mutable twin
+                    acrossPoint <- pointAt mutable opposite
+                    let !verdict = inCircle to from acrossPoint point
+                    if verdict == GT
+                      then do
+                        markFace epoch across
+                        recordInternal epoch (directed `quot` 2)
+                        acrossNext <- readNext mutable twin
+                        pushCavityWork acrossNext
+                        pushCavityWork acrossPrevious
+                        go acc visible
+                      else do
+                        pushGrowable wsCavityBoundary (packIndex directed)
+                        when (verdict == EQ) $
+                          pushGrowable wsCavityCocircular (packIndex directed)
+                        go acc (visible && orient2d from to point == GT)
+
+  strictlyInDiametralCircle :: Point -> Point -> Point -> Bool
+  strictlyInDiametralCircle (Point ax ay) (Point bx by) (Point px py) =
+    exactDiametralDot ax ay bx by px py < 0
+
+  -- The flood never mutates, so a popped edge can be retested idempotently:
+  -- an edge whose far face conflicts pushes that face's other two edges, and
+  -- each directed edge has at most its two face-neighbours as pushers.
+  drainSimulation :: GrowableWord32 s -> Point -> [Int] -> ST s [Int]
+  drainSimulation work point = go
+   where
+    go :: [Int] -> ST s [Int]
+    go !acc = do
+      packed <- popGrowableOr noIndex work
+      if packed == noIndex
+        then pure acc
+        else do
+          let !directed = fromIntegral packed
+          fixed <- isFixedEdge directed
+          if fixed
+            then do
+              from <- edgeOriginPoint mutable directed
+              to <- edgeOriginPoint mutable (reverseIndex directed)
+              if inDiametralCircle from to point
+                then go (directed `quot` 2 : acc)
+                else go acc
+            else do
+              let !twin = reverseIndex directed
+              acrossPrevious <- readPrevious mutable twin
+              opposite <- readOrigin mutable acrossPrevious
+              from <- edgeOriginPoint mutable directed
+              to <- pointAt mutable =<< readOrigin mutable twin
+              across <- pointAt mutable opposite
+              if inCircle to from across point == GT
+                then do
+                  acrossNext <- readNext mutable twin
+                  pushChargedWork work acrossNext
+                  pushChargedWork work acrossPrevious
+                  go acc
+                else go acc
+
+  -- A cavity the fan commit cannot take: a degenerate or barrier-bent
+  -- boundary, a pinched boundary walk, or a duplicate site. The located site
+  -- commits through the ordinary split-and-legalize path instead; the
+  -- recorded cavity is simply abandoned.
+  commitFlipCandidate :: Workspace s -> Point -> MutableLocation -> ST s (Either BuildError Int)
+  commitFlipCandidate workspace@Workspace{wsQueue, wsExcluded} point site = do
+    let !canonical = canonicalPoint point
+    captureCavitySignatures workspace
+    vertex <- appendVertex mutable canonical (makeVertex canonical)
+    splitSides <- case site of
+      MutableOnEdge edge
+        | refineExcludeOuterFaces parameters -> do
+            own <- readFace mutable edge
+            let !inner = if own == 0 then reverseIndex edge else edge
+            leftFace <- readFace mutable inner
+            rightFace <- readFace mutable (reverseIndex inner)
+            leftExcluded <- readBoolMark wsExcluded leftFace
+            rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)
+            faceBase <- faceCount mutable
+            pure (Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase))
+      _ -> pure Nothing
+    inserted <- insertExistingVertexAtLocation @'ProbeOff mutable operation vertex site
+    case inserted of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        addCounter operation (Reported SteinerPoints) 1
+        retireRewrittenFaces workspace
+        admitStar workspace vertex
+        case splitSides of
+          Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase) ->
+            inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase
+          Nothing -> pure ()
+        forEachStarFace vertex $ \starFace -> do
+          skip <- readBoolMark wsExcluded starFace
+          unless skip $ pushFace wsQueue starFace
+        pure (Right 1)
+
+  -- Commit a clean cavity by fanning its boundary to the new vertex. The
+  -- recorded cavity faces and internal pairs are recycled into fan faces and
+  -- spokes; exactly two faces and three edge pairs are appended, whatever the
+  -- cavity's size. Any boundary that is not a simple, strictly-visible cycle
+  -- falls back to the split-and-legalize commit: that is the degenerate and
+  -- the barrier-bent case, never an error.
+  commitCavity :: Workspace s -> Point -> MutableLocation -> Bool -> ST s (Either BuildError Int)
+  commitCavity workspace@Workspace{wsCavityFaces, wsCavityInternal, wsCavityBoundary} point site visible = do
+    boundaryCount <- growableLength wsCavityBoundary
+    cavityFaceCount <- growableLength wsCavityFaces
+    internalCount <- growableLength wsCavityInternal
+    chained <-
+      if visible && boundaryCount == cavityFaceCount + 2 && boundaryCount == internalCount + 3
+        then chainBoundary workspace boundaryCount
+        else pure False
+    if chained
+      then fanCommit workspace point boundaryCount cavityFaceCount internalCount
+      else commitFlipCandidate workspace point site
+
+  -- Order the recorded boundary edges into the cycle they form, starting each
+  -- successor by rotating around the current edge's destination. The records
+  -- agree with a simple cycle only when the walk closes after exactly the
+  -- recorded count and meets no boundary vertex twice; anything else is the
+  -- pinched case the fan cannot take.
+  chainBoundary :: Workspace s -> Int -> ST s Bool
+  chainBoundary Workspace{wsCavityEpoch, wsCavityFaceMarks, wsCavityVertexMarks, wsCavityBoundary, wsCavityChain} count = do
+    epoch <- readSTRef wsCavityEpoch
+    walkBudget <- directedEdgeCount mutable
+    start <- unpackIndex <$> readGrowable wsCavityBoundary 0
+    clearGrowable wsCavityChain
+    pushGrowable wsCavityChain (packIndex start)
+    walk epoch (walkBudget + 2) start start (count - 1)
+   where
+    walk :: Word32 -> Int -> Int -> Int -> Int -> ST s Bool
+    walk epoch walkBudget start current remaining = do
+      destination <- readOrigin mutable (reverseIndex current)
+      seen <- readEpochMark wsCavityVertexMarks destination
+      if seen == epoch
+        then pure False
+        else do
+          writeEpochMark wsCavityVertexMarks destination epoch
+          successor <- nextBoundaryEdge epoch walkBudget current
+          if remaining == 0
+            then pure (successor == start)
+            else
+              if successor == start
+                then pure False
+                else do
+                  pushGrowable wsCavityChain (packIndex successor)
+                  walk epoch walkBudget start successor (remaining - 1)
+
+    nextBoundaryEdge :: Word32 -> Int -> Int -> ST s Int
+    nextBoundaryEdge epoch walkBudget edge = do
+      first <- readNext mutable edge
+      rotate first walkBudget
+     where
+      rotate :: Int -> Int -> ST s Int
+      rotate candidate !remaining
+        | remaining <= 0 = pure candidate
+        | otherwise = do
+            adjacent <- readFace mutable candidate
+            markedHere <- (== epoch) <$> readEpochMark wsCavityFaceMarks adjacent
+            across <- readFace mutable (reverseIndex candidate)
+            markedAcross <- (== epoch) <$> readEpochMark wsCavityFaceMarks across
+            if markedHere && not markedAcross
+              then pure candidate
+              else do
+                following <- readNext mutable (reverseIndex candidate)
+                rotate following (remaining - 1)
+
+  fanCommit :: Workspace s -> Point -> Int -> Int -> Int -> ST s (Either BuildError Int)
+  fanCommit workspace@Workspace{wsQueue, wsExcluded, wsCavityFaces, wsCavityInternal, wsCavityCocircular, wsCavityChain} point boundaryCount cavityFaceCount internalCount = do
+    capacity <- ensureCellCapacity mutable (boundaryCount - internalCount) (boundaryCount - cavityFaceCount)
+    case capacity of
+      Left obstruction -> pure (Left obstruction)
+      Right () -> fanCommitWithCapacity
+   where
+    fanCommitWithCapacity :: ST s (Either BuildError Int)
+    fanCommitWithCapacity = do
+      let !canonical = canonicalPoint point
+      retirePermittedFaces workspace wsCavityFaces
+      vertex <- appendVertex mutable canonical (makeVertex canonical)
+      faceBase <- addFaceBlock mutable (boundaryCount - cavityFaceCount)
+      edgeBase <- addEdgeBlock mutable (boundaryCount - internalCount)
+      firstPair <- fanPair edgeBase 0
+      let spoke :: Int -> Int -> ST s ()
+          spoke !index !pair
+            | index >= boundaryCount = pure ()
+            | otherwise = do
+                boundaryEdge <- unpackIndex <$> readGrowable wsCavityChain index
+                face <- fanFace faceBase index
+                nextPair <-
+                  if index + 1 >= boundaryCount
+                    then pure firstPair
+                    else fanPair edgeBase (index + 1)
+                origin <- readOrigin mutable boundaryEdge
+                writeOrigin mutable (2 * pair) origin
+                writeOrigin mutable (2 * pair + 1) vertex
+                setCycle3 mutable face boundaryEdge (2 * nextPair) (2 * pair + 1)
+                writeVertexOut mutable origin boundaryEdge
+                -- 'fanFace' and 'fanPair' hand back the cavity's own faces and
+                -- interior edges before they hand out new ones. A recycled slot
+                -- is given a spoke to the Steiner vertex and a triangle that did
+                -- not exist, so whatever the cavity element it displaced was
+                -- labelled with does not survive.
+                resetEdgeData mutable pair
+                resetFaceData mutable face
+                spoke (index + 1) nextPair
+      spoke 0 firstPair
+      markConnected mutable vertex (2 * firstPair + 1)
+      addCounter operation (Reported SteinerPoints) 1
+      -- Exactly-cocircular boundary quads are legal ties for the flip
+      -- commit but not for a static boundary. Only those edges are drained:
+      -- the flood already decided every other boundary edge's quad, so a
+      -- full shouldFlip pass would ask again what it already answered.
+      cocircular <- growableLength wsCavityCocircular
+      when (cocircular > 0) $ do
+        ties <- traverse (\index -> unpackIndex <$> readGrowable wsCavityCocircular index) [0 .. cocircular - 1]
+        legalizeEdges mutable operation ties
+      -- A tie flip replaces two faces by two faces of the star, so the star
+      -- only grows under the drain and one admission after it covers the fan.
+      admitStar workspace vertex
+      let offer :: Int -> ST s ()
+          offer !index
+            | index >= boundaryCount = pure ()
+            | otherwise = do
+                face <- fanFace faceBase index
+                skip <- readBoolMark wsExcluded face
+                unless skip $ pushFace wsQueue face
+                offer (index + 1)
+      offer 0
+      pure (Right 1)
+
+    fanFace :: Int -> Int -> ST s Int
+    fanFace faceBase index
+      | index < cavityFaceCount = unpackIndex <$> readGrowable wsCavityFaces index
+      | otherwise = pure (faceBase + (index - cavityFaceCount))
+
+    fanPair :: Int -> Int -> ST s Int
+    fanPair edgeBase index
+      | index < internalCount = unpackIndex <$> readGrowable wsCavityInternal index
+      | otherwise = pure ((edgeBase + 2 * (index - internalCount)) `quot` 2)
+
+  -- A candidate outside the hull (only reachable when the hull is not
+  -- preserved) preflights against the whole visible hull chain: the
+  -- prospective cavity reaches the hull, so the chain edges are exactly the
+  -- fixed edges the insertion would meet.
+  attemptOutside :: Workspace s -> Int -> Point -> Int -> ST s (Either BuildError Int)
+  attemptOutside workspace@Workspace{wsQueue, wsExcluded, wsForcedSplits, wsCavityWork} face point edge = do
+    chain <- visibleChain edge point
+    clearGrowable wsCavityWork
+    forM_ chain $ \hull -> do
+      pushChargedWork wsCavityWork hull
+      let !inner = reverseIndex hull
+      innerFace <- readFace mutable inner
+      when (innerFace /= 0) $ do
+        next <- readNext mutable inner
+        previous <- readPrevious mutable inner
+        pushChargedWork wsCavityWork next
+        pushChargedWork wsCavityWork previous
+    encroached <- drainSimulation wsCavityWork point []
+    if not (null encroached)
+      then do
+        pushable <- filterM splittablePair encroached
+        forM_ pushable $ \pair -> pushGrowable wsForcedSplits (packIndex pair)
+        unless (null pushable) $ pushFace wsQueue face
+        pure (Right 0)
+      else do
+        let !canonical = canonicalPoint point
+        vertex <- appendVertex mutable canonical (makeVertex canonical)
+        inserted <- insertExistingVertexAtLocation @'ProbeOff mutable operation vertex (MutableOutsideHull edge)
+        case inserted of
+          Left failure -> pure (Left failure)
+          Right () -> do
+            addCounter operation (Reported SteinerPoints) 1
+            admitStar workspace vertex
+            when (refineExcludeOuterFaces parameters) $ do
+              seedCount <- newSTRef 0
+              forEachStarFace vertex $ \starFace -> do
+                writeBoolMark wsExcluded starFace True
+                count <- readSTRef seedCount
+                writeScratch operation count starFace
+                writeSTRef seedCount (count + 1)
+              count <- readSTRef seedCount
+              propagateExcluded wsExcluded count
+            forEachStarFace vertex $ \starFace -> do
+              skip <- readBoolMark wsExcluded starFace
+              unless skip $ pushFace wsQueue starFace
+            forEachStarFace vertex $ \starFace -> do
+              (e0, e1, e2) <- faceEdges mutable starFace
+              traverse_ (offerFixedEdge workspace) [e0, e1, e2]
+            pure (Right 1)
+
+  -- The maximal run of outer-cycle edges visible from the candidate, centred
+  -- on the edge location reported. Bounded by the hull length.
+  visibleChain :: Int -> Point -> ST s [Int]
+  visibleChain edge point = do
+    halfEdges <- directedEdgeCount mutable
+    left <- expand (readPrevious mutable) (halfEdges + 1) edge
+    right <- expand (readNext mutable) (halfEdges + 1) edge
+    walk left right (halfEdges + 1) []
+   where
+    expand :: (Int -> ST s Int) -> Int -> Int -> ST s Int
+    expand step !walkBudget !current
+      | walkBudget <= 0 = pure current
+      | otherwise = do
+          candidate <- step current
+          if candidate == edge
+            then pure current
+            else do
+              from <- edgeOriginPoint mutable candidate
+              to <- edgeOriginPoint mutable (reverseIndex candidate)
+              if orient2d from to point == GT
+                then expand step (walkBudget - 1) candidate
+                else pure current
+
+    walk :: Int -> Int -> Int -> [Int] -> ST s [Int]
+    walk !current !end !walkBudget !acc
+      | walkBudget <= 0 = pure acc
+      | current == end = pure (current : acc)
+      | otherwise = do
+          following <- readNext mutable current
+          walk following end (walkBudget - 1) (current : acc)
+
+  -- New faces outside the hull sit at barrier depth zero; whatever they now
+  -- reach without crossing a constraint joined the outer region with them.
+  propagateExcluded :: BoolMarks s -> Int -> ST s ()
+  propagateExcluded excluded = drain
+   where
+    drain :: Int -> ST s ()
+    drain !count
+      | count <= 0 = pure ()
+      | otherwise = do
+          face <- readScratch operation (count - 1)
+          (e0, e1, e2) <- faceEdges mutable face
+          next <- spread (count - 1) e0 >>= (`spread` e1) >>= (`spread` e2)
+          drain next
+
+    spread :: Int -> Int -> ST s Int
+    spread !count !directed = do
+      protected <- readConstraint mutable directed
+      if protected
+        then pure count
+        else do
+          adjacent <- readFace mutable (reverseIndex directed)
+          if adjacent == 0
+            then pure count
+            else do
+              already <- readBoolMark excluded adjacent
+              if already
+                then pure count
+                else do
+                  writeBoolMark excluded adjacent True
+                  writeScratch operation count adjacent
+                  pure (count + 1)
+
+  -- A queued fixed edge against the vertices currently opposite it, from each
+  -- non-excluded side. A stale candidate simply answers for the subsegment
+  -- its handle now names, which is the edge the queue cares about.
+  checkEncroachment :: Workspace s -> Int -> ST s (Either BuildError Bool)
+  checkEncroachment workspace@Workspace{wsExcluded} pair = do
+    let !directed = 2 * pair
+    first <- sideEncroaches wsExcluded directed
+    encroached <-
+      if first
+        then pure True
+        else sideEncroaches wsExcluded (reverseIndex directed)
+    if encroached
+      then
+        if isInterfacePair pair
+          then Left <$> interfaceCrossing workspace pair
+          else resolveSplit SplitFixedSegment workspace pair
+      else pure (Right False)
+   where
+    sideEncroaches :: BoolMarks s -> Int -> ST s Bool
+    sideEncroaches excluded side = do
+      adjacent <- readFace mutable side
+      if adjacent == 0
+        then pure False
+        else do
+          skip <- readBoolMark excluded adjacent
+          if skip
+            then pure False
+            else do
+              opposite <- readOrigin mutable =<< readPrevious mutable side
+              query <- pointAt mutable opposite
+              from <- edgeOriginPoint mutable side
+              to <- edgeOriginPoint mutable (reverseIndex side)
+              hull <- case domain of
+                Nothing -> pure False
+                Just _ -> isHullPair pair
+              pure
+                ( if isInterfacePair pair || hull
+                    then strictlyInDiametralCircle from to query
+                    else inDiametralCircle from to query
+                )
+
+  -- Split a fixed edge. A first split lands at the midpoint; a subsegment
+  -- split rounds its offset to the nearest power of two toward the original
+  -- endpoint, so segments meeting at a small input angle stop encroaching
+  -- each other instead of subdividing forever.
+  resolveSplit :: EdgeSplitLaw -> Workspace s -> Int -> ST s (Either BuildError Bool)
+  resolveSplit splitLaw workspace@Workspace{wsExcluded, wsSegmentOrigin} pair = do
+    let !directed = 2 * pair
+    protected <- readConstraint mutable directed
+    hull <- case domain of
+      Nothing -> pure False
+      Just _ -> isHullPair pair
+    if hull
+      then pure (Right False)
+      else if isInterfacePair pair
+      then Left <$> interfaceCrossing workspace pair
+      else if refineKeepConstraintEdges parameters && protected
+      then pure (Right False)
+      else do
+        v0 <- readOrigin mutable directed
+        v1 <- readOrigin mutable (reverseIndex directed)
+        from <- pointAt mutable v0
+        to <- pointAt mutable v1
+        segmentLineage <-
+          case splitLaw of
+            SplitQualityEdge -> pure Nothing
+            SplitFixedSegment -> do
+              (lineage0, second0) <- readSegmentOrigin wsSegmentOrigin v0
+              (lineage1, second1) <- readSegmentOrigin wsSegmentOrigin v1
+              pure (Just (lineage0, second0, lineage1, second1))
+        let (!onSegment0, !onSegment1) =
+              case segmentLineage of
+                Nothing -> (False, False)
+                Just (lineage0, _, lineage1, _) ->
+                  (lineage0 /= noIndex, lineage1 /= noIndex)
+            !splitPoint = splitPosition onSegment0 onSegment1 from to
+        captureStarSignatures workspace v0 v1
+        valid <- validateSplitPosition directed splitPoint
+        if not valid
+          then pure (Right False)
+          else do
+            -- Inheritance is read as structural face identities, never as
+            -- post-split handle membership: the split's own legalization can
+            -- flip seeded edges and relocate handles before any read, but it
+            -- cannot rename a face. The kept side faces and the append base
+            -- name the sides exactly.
+            own <- readFace mutable directed
+            let !inner = if own == 0 then reverseIndex directed else directed
+            leftFace <- readFace mutable inner
+            rightFace <- readFace mutable (reverseIndex inner)
+            leftExcluded <- readBoolMark wsExcluded leftFace
+            rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)
+            faceBase <- faceCount mutable
+            let !canonical = canonicalPoint splitPoint
+            retirePermittedFace workspace leftFace
+            retirePermittedFace workspace rightFace
+            vertex <- appendVertex mutable canonical (makeVertex canonical)
+            inserted <- insertOnEdge @'ProbeOff mutable operation directed vertex
+            case inserted of
+              Left obstruction -> pure (Left obstruction)
+              Right () -> do
+                retireRewrittenFaces workspace
+                admitStar workspace vertex
+                case segmentLineage of
+                  Nothing -> pure ()
+                  Just (lineage0, second0, lineage1, second1) ->
+                    if onSegment0
+                      then writeSegmentOrigin wsSegmentOrigin vertex lineage0 second0
+                      else
+                        if onSegment1
+                          then writeSegmentOrigin wsSegmentOrigin vertex lineage1 second1
+                          else writeSegmentOrigin wsSegmentOrigin vertex (packIndex v0) (packIndex v1)
+                when (refineExcludeOuterFaces parameters) $
+                  inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase
+                addCounter operation (Reported SteinerPoints) 1
+                pushStarAfterSplit workspace vertex
+                pure (Right True)
+
+  splitPosition :: Bool -> Bool -> Point -> Point -> Point
+  splitPosition onSegment0 onSegment1 from to
+    | not onSegment0 && not onSegment1 = midpoint from to
+    | otherwise =
+        let !halfLength = sqrt (squaredDistance from to) * 0.5
+         in if not (isFinite halfLength) || halfLength <= 0
+              then midpoint from to
+              else
+                let !nearest = 2 ** fromIntegral (round (logBase 2 halfLength) :: Int)
+                    !otherWeight = 0.5 * nearest / halfLength
+                    !originalWeight = 1 - otherWeight
+                    (!weight0, !weight1) =
+                      if onSegment0
+                        then (otherWeight, originalWeight)
+                        else (originalWeight, otherWeight)
+                    Point fromX fromY = from
+                    Point toX toY = to
+                 in Point (fromX * weight0 + toX * weight1) (fromY * weight0 + toY * weight1)
+
+  -- The split is refused when the constructed vertex would leave one of the
+  -- four new faces degenerate or clockwise — for an in-segment point that is
+  -- exactly the coincidence-with-an-endpoint-or-opposite case.
+  validateSplitPosition :: Int -> Point -> ST s Bool
+  validateSplitPosition directed splitPoint@(Point splitX splitY)
+    | validateCoordinate splitX /= Nothing = pure False
+    | validateCoordinate splitY /= Nothing = pure False
+    | otherwise = do
+        from <- edgeOriginPoint mutable directed
+        to <- edgeOriginPoint mutable (reverseIndex directed)
+        first <- sideKeepsOrientation directed from to
+        if not first
+          then pure False
+          else sideKeepsOrientation (reverseIndex directed) to from
+   where
+    sideKeepsOrientation :: Int -> Point -> Point -> ST s Bool
+    sideKeepsOrientation side sideFrom sideTo = do
+      adjacent <- readFace mutable side
+      if adjacent == 0
+        then pure True
+        else do
+          opposite <- readOrigin mutable =<< readPrevious mutable side
+          oppositePoint <- pointAt mutable opposite
+          pure
+            ( orient2d sideFrom oppositePoint splitPoint == LT
+                && orient2d oppositePoint sideTo splitPoint == LT
+            )
+
+  sideExcluded :: BoolMarks s -> Int -> ST s Bool
+  sideExcluded excluded side = do
+    adjacent <- readFace mutable side
+    if adjacent == 0 then pure True else readBoolMark excluded adjacent
+
+  -- A split keeps each side's classification. The faces a side holds
+  -- afterwards are the side face it kept and the face appended over it:
+  -- interior splits append two, boundary splits one, and both ids are
+  -- immune to the legalization that runs between the geometry and here.
+  inheritSplitExclusion :: BoolMarks s -> Int -> Bool -> Int -> Bool -> Int -> ST s ()
+  inheritSplitExclusion excluded leftFace leftExcluded rightFace rightExcluded faceBase = do
+    writeSide leftExcluded leftFace
+    writeSide leftExcluded faceBase
+    when (rightFace /= 0) $ do
+      writeSide rightExcluded rightFace
+      writeSide rightExcluded (faceBase + 1)
+   where
+    writeSide :: Bool -> Int -> ST s ()
+    writeSide status side = do
+      when (side /= 0) $ writeBoolMark excluded side status
+
+  pushStarAfterSplit :: Workspace s -> Int -> ST s ()
+  pushStarAfterSplit workspace@Workspace{wsQueue} vertex =
+    forEachStarFace vertex $ \starFace -> do
+      pushFace wsQueue starFace
+      (e0, e1, e2) <- faceEdges mutable starFace
+      traverse_ (offerFixedEdge workspace) [e0, e1, e2]
+
+  -- Every interior face incident to a vertex, once. One rotation direction
+  -- covers a full interior star; a hull star additionally needs the other
+  -- direction from the starting edge.
+  forEachStarFace :: Int -> (Int -> ST s ()) -> ST s ()
+  forEachStarFace vertex visit = do
+    start <- readVertexOut mutable vertex
+    when (start >= 0) $ do
+      wrapped <- rotate start start
+      unless wrapped (rotateBack start)
+   where
+    rotate :: Int -> Int -> ST s Bool
+    rotate !start !edge = do
+      adjacent <- readFace mutable edge
+      if adjacent == 0
+        then pure False
+        else do
+          visit adjacent
+          next <- reverseIndex <$> readPrevious mutable edge
+          if next == start then pure True else rotate start next
+
+    rotateBack :: Int -> ST s ()
+    rotateBack edge = do
+      let !twin = reverseIndex edge
+      adjacent <- readFace mutable twin
+      when (adjacent /= 0) $ do
+        visit adjacent
+        next <- readNext mutable twin
+        rotateBack next
+
+-- | Whether a measurement is over its bound. A non-positive bound admits
+-- nothing, which is what dividing by it used to say by returning an infinity
+-- that then compared greater than one.
+sortedTriple :: Int -> Int -> Int -> (Int, Int, Int)
+sortedTriple a b c =
+  let (!low1, !high1) = if a <= b then (a, b) else (b, a)
+      (!low, !middle1) = if low1 <= c then (low1, c) else (c, low1)
+      (!middle, !high) = if middle1 <= high1 then (middle1, high1) else (high1, middle1)
+   in (low, middle, high)
+{-# INLINE sortedTriple #-}
+
+exceeds :: Double -> Double -> Bool
+exceeds value bound = bound <= 0 || value > bound
+{-# INLINE exceeds #-}
+
+-- | The bound the squared radius-edge ratio is compared against. Squaring is
+-- monotone on the non-negative reals, so this asks the same question of the
+-- squares that the unsquared bound asked of the lengths.
+squareBound :: Double -> Double
+squareBound bound
+  | bound <= 0 = bound
+  | otherwise = bound * bound
+{-# INLINE squareBound #-}
diff --git a/src-build/Moonlight/Planar/Internal/Session.hs b/src-build/Moonlight/Planar/Internal/Session.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Session.hs
@@ -0,0 +1,418 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | One owned editing transaction over a triangulation: thaw once, edit, publish once.
+module Moonlight.Planar.Internal.Session
+  ( RemovalOutcome (..)
+  , Session
+  , withSession
+  , withLocalSession
+  , insertVertex
+  , insertVertexAt
+  , insertVertexAtNear
+  , insertVertexAtNearVertex
+  , insertAdmittedVertexAtCombining
+  , removeAt
+  , removeAtNear
+  , removeManyAt
+  , removeManyAtNear
+  , excise
+  , refuse
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad.ST (ST)
+import Data.Bits (xor)
+import qualified Data.Vector as V
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Insertion
+  ( insertAdmittedPointCombining
+  , insertPointCombining
+  )
+import Moonlight.Planar.Internal.Excision (removeMutable)
+import Moonlight.Planar.Internal.Location (MutableLocation (..), locateMutable)
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.OperationState (OperationState)
+import Moonlight.Planar.Internal.Paged (TransactionShape (..))
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Transaction (runTransaction)
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Point (Point (..), QueryPoint, HasPosition (..))
+import GHC.Generics (Generic)
+
+-- | What a single removal produced: the removed position and payload, plus the
+-- slot and position changed by swap compaction when another vertex moved.
+data RemovalOutcome vertex = RemovalOutcome
+  { removalOutcomePoint :: !(Point)
+  , removalOutcomeData :: !vertex
+  , removalOutcomeSwap :: !(Maybe (VertexId, Point))
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | An edit sequence against one thawed mesh.
+--
+-- The two published entry points before this one — an insertion session and a
+-- removal session — each handed the caller a function and could not compose,
+-- so a caller who wanted to remove a hundred vertices and insert fifty thawed
+-- twice and paid the O(n) publication a session exists to delete. They are the
+-- same transaction; only the verb differed. This is that transaction, and the
+-- verbs are its primitives.
+--
+-- The public session algebra and the private exact-site single-insertion
+-- interpreter cross the same hidden transaction boundary. Sessions provide
+-- composition; the private interpreter reaches that boundary only with
+-- evidence from the immediately preceding frozen read. A fold of persistent
+-- verbs and a session over the same verbs therefore differ only in which
+-- intermediate meshes they publish.
+--
+-- Refusal short-circuits: the first 'BuildError' abandons the rest of the
+-- sequence, and 'withSession' freezes nothing. A partly edited arena cannot
+-- reach a caller as a published triangulation.
+newtype Session s vertex directed undirected face a = Session
+  { stepSession
+      :: MutableDcel s vertex directed undirected face
+      -> OperationState s
+      -> ST s (Either BuildError a)
+  }
+
+instance Functor (Session s vertex directed undirected face) where
+  fmap f (Session step) = Session $ \mesh operation -> fmap (fmap f) (step mesh operation)
+  {-# INLINE fmap #-}
+
+instance Applicative (Session s vertex directed undirected face) where
+  pure a = Session $ \_ _ -> pure (Right a)
+  {-# INLINE pure #-}
+  Session left <*> Session right = Session $ \mesh operation -> do
+    outcome <- left mesh operation
+    case outcome of
+      Left refusal -> pure (Left refusal)
+      Right f -> fmap (fmap f) (right mesh operation)
+  {-# INLINE (<*>) #-}
+
+instance Monad (Session s vertex directed undirected face) where
+  Session step >>= f = Session $ \mesh operation -> do
+    outcome <- step mesh operation
+    case outcome of
+      Left refusal -> pure (Left refusal)
+      Right a -> stepSession (f a) mesh operation
+  {-# INLINE (>>=) #-}
+
+-- | Abandon the transaction. Nothing is published.
+refuse :: BuildError -> Session s vertex directed undirected face a
+refuse failure = Session $ \_ _ -> pure (Left failure)
+{-# INLINE refuse #-}
+
+-- | Insert a vertex, answering the handle it was given. A point already
+-- present keeps its handle and takes the new payload.
+insertVertex
+  :: HasPosition vertex
+  => vertex
+  -> Session s vertex directed undirected face VertexId
+insertVertex payload = Session $ \mesh operation ->
+  fmap
+    (fmap (VertexId . fromIntegral . fst))
+    (insertPointCombining (\_ replacement -> replacement) Nothing mesh operation (position payload) payload)
+
+-- | Insert at a stated point, answering the handle and whether a site was
+-- created. 'insertVertex' is this with the point read out of the payload; a
+-- caller that computed the point — a constraint split, a Steiner refinement —
+-- states it rather than round-tripping through 'HasPosition'.
+insertVertexAt
+  :: Point
+  -> vertex
+  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
+insertVertexAt = insertVertexAtFrom (const (pure Nothing))
+
+-- | 'insertVertexAt' with the walk seeded at a face the caller vouches for --
+-- typically the face a locate on the just-published value settled on, which
+-- is exact on the mesh this transaction thawed. The seed is a hint, not an
+-- authority: an out-of-range face degrades to the unhinted descent, and the
+-- walk corrects.
+insertVertexAtNear
+  :: FaceId
+  -> Point
+  -> vertex
+  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
+insertVertexAtNear seed =
+  insertVertexAtFrom (const (pure (Just (faceIdIndex seed))))
+
+-- | 'insertVertexAt' with the exact walk restricted first to an inner face in
+-- the supplied vertex's star. This is the native boundary for hierarchy and
+-- nearest-neighbour hints, which answer vertices rather than faces. The seed
+-- remains non-authoritative: an invalid handle or a vertex with no incident
+-- inner face degrades to the ordinary unhinted descent.
+insertVertexAtNearVertex
+  :: VertexId
+  -> Point
+  -> vertex
+  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
+insertVertexAtNearVertex seed = insertVertexAtFrom (`interiorFaceNearVertex` seed)
+
+insertVertexAtFrom
+  :: (MutableDcel s vertex directed undirected face -> ST s (Maybe Int))
+  -> Point
+  -> vertex
+  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
+insertVertexAtFrom seedFor point payload = Session $ \mesh operation -> do
+  seed <- seedFor mesh
+  fmap
+    (fmap (\(vertex, disposition) -> (VertexId (fromIntegral vertex), disposition)))
+    (insertPointCombining
+       (\_ replacement -> replacement)
+       seed
+       mesh
+       operation
+       point
+       payload
+    )
+{-# INLINE insertVertexAtFrom #-}
+
+-- | Insert an admitted site while combining an occupied annotation. This is
+-- package-internal descent from a boundary that already owns coordinate
+-- admission; only location and annotation gluing remain here.
+insertAdmittedVertexAtCombining
+  :: (vertex -> vertex -> vertex)
+  -> QueryPoint
+  -> vertex
+  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
+insertAdmittedVertexAtCombining combine queryPoint payload = Session $ \mesh operation ->
+  fmap
+    (fmap (\(vertex, disposition) -> (VertexId (fromIntegral vertex), disposition)))
+    (insertAdmittedPointCombining combine Nothing mesh operation queryPoint payload)
+
+-- | Remove the vertex standing at a point, answering 'Nothing' when no vertex
+-- stands there.
+--
+-- Keyed by position rather than by handle because removal swap-compacts the
+-- arenas: every outstanding t'VertexId' may relocate, and over a sequence of
+-- removals a handle-keyed verb would force the caller to thread every
+-- relocation by hand. A position is invariant under compaction. The relocation
+-- is still reported, in the 'RemovalOutcome', for callers holding handles.
+-- The question is an identity question -- which vertex stands at this exact
+-- position. A session that has committed to identity work ('removeManyAt',
+-- 'excise') answers it through the identity index in O(1); one that has not
+-- answers it with a single walk, because a published index is a lazy rebuild
+-- and forcing a whole-mesh build to answer one question is the wrong trade.
+removeAt
+  :: Point
+  -> Session s vertex directed undirected face (Maybe (RemovalOutcome vertex))
+removeAt point = Session $ \mesh operation -> do
+  indexed <- identityIndexActive mesh
+  if indexed
+    then removeIndexed mesh operation point
+    else walkAndRemove mesh operation Nothing point
+
+removeIndexed
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Point
+  -> ST s (Either BuildError (Maybe (RemovalOutcome vertex)))
+removeIndexed mesh operation point = do
+  located <- lookupPointVertex mesh point
+  case located of
+    Just vertex -> fmap (fmap Just) (removeMutableOutcome mesh operation vertex)
+    Nothing -> pure (Right Nothing)
+
+-- | Remove the vertex standing at a point, starting from a caller-supplied
+-- near vertex -- a locate hint from an external structure such as the
+-- Delaunay hierarchy. The guess is a hint, not an authority: a slot renamed
+-- by swap-compaction or out of range degrades to a walk hinted by the
+-- guess's incident face, and the walk corrects.
+removeAtNear
+  :: VertexId
+  -> Point
+  -> Session s vertex directed undirected face (Maybe (RemovalOutcome vertex))
+removeAtNear guessVertex point = Session $ \mesh operation -> do
+  indexed <- identityIndexActive mesh
+  let !guess = vertexIdIndex guessVertex
+      !query = canonicalPoint point
+  if indexed
+    then removeIndexed mesh operation query
+    else do
+      vertices <- pointCount mesh
+      if guess < 0 || guess >= vertices
+        then walkAndRemove mesh operation Nothing query
+        else do
+          stored <- pointAt mesh guess
+          if stored == query
+            then fmap (fmap Just) (removeMutableOutcome mesh operation guess)
+            else do
+              hint <- interiorFaceAtVertex mesh guess
+              walkAndRemove mesh operation hint query
+
+interiorFaceNearVertex
+  :: MutableDcel s vertex directed undirected face
+  -> VertexId
+  -> ST s (Maybe Int)
+interiorFaceNearVertex mesh vertex@(VertexId rawVertex) = do
+  vertices <- pointCount mesh
+  if toInteger rawVertex >= toInteger vertices
+    then pure Nothing
+    else interiorFaceAtVertex mesh (vertexIdIndex vertex)
+
+interiorFaceAtVertex
+  :: MutableDcel s vertex directed undirected face
+  -> Int
+  -> ST s (Maybe Int)
+interiorFaceAtVertex mesh vertex = do
+  outgoing <- readVertexOut mesh vertex
+  if outgoing < 0
+    then pure Nothing
+    else do
+      primary <- innerFaceOf outgoing
+      maybe (innerFaceOf (outgoing `xor` 1)) (pure . Just) primary
+ where
+  innerFaceOf edge = do
+    face <- readFace mesh edge
+    pure (if face > 0 then Just face else Nothing)
+
+walkAndRemove
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Maybe Int
+  -> Point
+  -> ST s (Either BuildError (Maybe (RemovalOutcome vertex)))
+walkAndRemove mesh operation hint query = do
+  located <- locateMutable mesh operation hint query
+  case located of
+    Left obstruction -> pure (Left obstruction)
+    Right (MutableOnVertex vertex) ->
+      fmap (fmap Just) (removeMutableOutcome mesh operation vertex)
+    Right _ -> pure (Right Nothing)
+
+-- | Remove the vertex standing at each point, answering per point in order.
+-- One session-level decision buys the locate strategy: few removals walk the
+-- mesh, each a ~O(sqrt n) descent, while many seed the existing mutable
+-- open-addressed identity section once from coordinate authority and answer
+-- each question by hash. The guard squares the crossover to avoid a root.
+removeManyAt
+  :: V.Vector (Point)
+  -> Session s vertex directed undirected face (V.Vector (Maybe (RemovalOutcome vertex)))
+removeManyAt points =
+  withBatchIdentityForLoad (V.length points) (V.mapM removeAt points)
+
+-- | Install the mutable identity section only around a dense removal program.
+-- It is a local section over the coordinate arenas, not a new published cache:
+-- on every result path it is discarded and freeze glues a lazy @PointIndex@
+-- back from those arenas. Sparse loads retain the existing locate walk.
+withBatchIdentityForLoad
+  :: Int
+  -> Session s vertex directed undirected face result
+  -> Session s vertex directed undirected face result
+withBatchIdentityForLoad count (Session action) = Session $ \mesh operation -> do
+  vertices <- pointCount mesh
+  if count * count > 10 * vertices
+    then do
+      activated <- activateBatchPointIndex mesh
+      case activated of
+        Left failure -> pure (Left failure)
+        Right () -> do
+          outcome <- action mesh operation
+          discardBatchPointIndex mesh
+          pure outcome
+    else action mesh operation
+
+identityCommitted :: Session s vertex directed undirected face Bool
+identityCommitted = Session $ \mesh _ -> fmap Right (identityIndexActive mesh)
+{-# INLINE identityCommitted #-}
+
+-- | 'removeManyAt' with a near vertex per point where the caller has one — a
+-- hierarchy sample, a previous answer. The batch commits to its load first,
+-- so the identity-index crossover decides the regime once: a dense batch buys
+-- its local mutable table and never examines a guess, a sparse one walks from
+-- its guesses.
+removeManyAtNear
+  :: V.Vector (Maybe VertexId)
+  -> V.Vector (Point)
+  -> Session s vertex directed undirected face (V.Vector (Maybe (RemovalOutcome vertex)))
+removeManyAtNear guesses points =
+  withBatchIdentityForLoad (V.length points) $ do
+    indexed <- identityCommitted
+    if indexed
+      then V.mapM removeAt points
+      else
+        V.zipWithM
+          (\guess point -> maybe (removeAt point) (\vertex -> removeAtNear vertex point) guess)
+          guesses
+          points
+
+-- | Remove a stated vertex. Sound only while the handle still denotes what the
+-- caller means: the first removal in a sequence compacts the arenas, so a
+-- handle taken before it may name a different vertex after. @removeAt@ is the
+-- verb for a sequence; this is the verb for a handle the caller has just been
+-- given and has not yet let a removal run underneath.
+excise
+  :: VertexId
+  -> Session s vertex directed undirected face (RemovalOutcome vertex)
+excise requested@(VertexId raw) = Session $ \mesh operation -> do
+  vertices <- pointCount mesh
+  if toInteger raw >= toInteger vertices
+    then pure (Left (RemovalVertexOutOfRange requested vertices))
+    else do
+      activatePointIndex mesh
+      removeMutableOutcome mesh operation (vertexIdIndex requested)
+
+removeMutableOutcome
+  :: MutableDcel s vertex directed undirected face
+  -> OperationState s
+  -> Int
+  -> ST s (Either BuildError (RemovalOutcome vertex))
+removeMutableOutcome mesh operation vertex =
+  fmap (fmap removalOutcomeOf) (removeMutable mesh operation vertex)
+
+removalOutcomeOf
+  :: (Point, vertex, Maybe (Int, Point))
+  -> RemovalOutcome vertex
+removalOutcomeOf (point, payload, swapped) =
+  RemovalOutcome
+    { removalOutcomePoint = point
+    , removalOutcomeData = payload
+    , removalOutcomeSwap =
+        (\(slot, standing) -> (VertexId (fromIntegral slot), standing)) <$> swapped
+    }
+
+-- | Run an edit sequence: thaw once, edit, freeze once, and publish the
+-- counters the whole transaction charged.
+--
+-- The reservation is taken for the peak vertex count, so it is the insertion
+-- count that sizes it; removals only shrink the mesh and a mixed sequence
+-- cannot exceed the peak an insert-only sequence of the same count reaches.
+--
+-- The session cannot escape its callback: the state token is universally
+-- quantified, so the mesh it addresses is dead by the time the frozen
+-- triangulation is returned.
+withSession
+  :: forall mode vertex directed undirected face result
+   . Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. Session s vertex directed undirected face result)
+  -> Either BuildError (result, Triangulation mode vertex directed undirected face, BuildStats)
+withSession triangulation additional session =
+  runTransaction
+    id
+    DenseTransaction
+    triangulation
+    additional
+    (\mesh operation -> stepSession session mesh operation)
+
+-- | The local-edit transaction: copy-on-write pages, publication proportional
+-- to what the edit dirtied. This is the section for singleton persistent
+-- verbs, whose one edit cannot amortize a dense copy of the whole mesh.
+withLocalSession
+  :: forall mode vertex directed undirected face result
+   . Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. Session s vertex directed undirected face result)
+  -> Either BuildError (result, Triangulation mode vertex directed undirected face, BuildStats)
+withLocalSession triangulation additional session =
+  runTransaction
+    id
+    LocalTransaction
+    triangulation
+    additional
+    (\mesh operation -> stepSession session mesh operation)
diff --git a/src-build/Moonlight/Planar/Internal/Transaction.hs b/src-build/Moonlight/Planar/Internal/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Internal/Transaction.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- | The hidden publication boundary shared by persistent build-side edits.
+--
+-- The rank-two action can observe one mutable section, but neither the section
+-- nor any site witness can escape it.  This is deliberately below the public
+-- Session surface: a caller may compose public verbs, while an owner that has
+-- just derived private evidence can interpret it without making that evidence
+-- forgeable.
+module Moonlight.Planar.Internal.Transaction
+  ( runTransaction
+  , runTransactionWithPublication
+  , runUnmeasuredTransaction
+  ) where
+
+import Control.Monad.ST (ST, runST)
+import Moonlight.Planar.Dcel (numVertices)
+import Moonlight.Planar.Internal.Capacity (ensureCapacity)
+import Moonlight.Planar.Internal.Mutable
+  ( MutableDcel
+  , freezeTriangulation
+  , freezeTriangulationWithStats
+  , halfEdgeCapacity
+  , thawTriangulation
+  , thawTriangulationDense
+  )
+import Moonlight.Planar.Internal.OperationState
+  ( OperationState
+  , finalizeBuildStats
+  , newOperationState
+  )
+import Moonlight.Planar.Internal.Paged
+  ( PublicationStats
+  , TransactionShape (..)
+  , emptyPublicationStats
+  )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Internal.Types (BuildError)
+import Moonlight.Planar.BuildStats (BuildStats)
+
+-- | Reserve, thaw, interpret, and publish one transaction.
+--
+-- The physical section is selected by the operation that knows its edit
+-- volume.  Refusal short-circuits before freezing, so a partially rewritten
+-- mutable mesh cannot escape as a published triangulation.
+runTransaction
+  :: (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face, BuildStats)
+runTransaction = runTransactionWithReceipt finalizeBuildStats
+{-# INLINE runTransaction #-}
+
+-- | The measured publication boundary.  The storage owner returns its page
+-- counters alongside the immutable mesh; callers must not infer locality from
+-- a final page count or from a benchmark timer.
+runTransactionWithPublication
+  :: (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face, BuildStats, PublicationStats)
+runTransactionWithPublication = runTransactionWithPublicationReceipt finalizeBuildStats
+{-# INLINE runTransactionWithPublication #-}
+
+-- | Publish a transaction whose caller observes no instrumentation. Avoiding
+-- the statistics fold matters for singleton constraint verbs: their public
+-- result has no statistics field, so reading every counter would be dead work.
+runUnmeasuredTransaction
+  :: (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face)
+runUnmeasuredTransaction mapBuildFailure shape triangulation additional action =
+  fmap
+    (\(result, frozen, ()) -> (result, frozen))
+    ( runTransactionWithReceipt
+        (const (pure ()))
+        mapBuildFailure
+        shape
+        triangulation
+        additional
+        action
+    )
+{-# INLINE runUnmeasuredTransaction #-}
+
+runTransactionWithReceipt
+  :: (forall s. OperationState s -> ST s receipt)
+  -> (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt)
+runTransactionWithReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do
+  fmap
+    (\(value, frozen, receipt, _publicationStats) -> (value, frozen, receipt))
+    (runTransactionCore freezeWithoutPublicationStats freezeReceipt mapBuildFailure shape triangulation additional action)
+{-# INLINE runTransactionWithReceipt #-}
+
+runTransactionWithPublicationReceipt
+  :: (forall s. OperationState s -> ST s receipt)
+  -> (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)
+runTransactionWithPublicationReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do
+  runTransactionCore freezeTriangulationWithStats freezeReceipt mapBuildFailure shape triangulation additional action
+{-# INLINE runTransactionWithPublicationReceipt #-}
+
+runTransactionCore
+  :: (forall s. MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats)))
+  -> (forall s. OperationState s -> ST s receipt)
+  -> (BuildError -> failure)
+  -> TransactionShape
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
+  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)
+runTransactionCore freezePublication freezeReceipt mapBuildFailure shape triangulation additional action = do
+  let !currentCapacity = numVertices triangulation
+      !requestedAdditional = max 0 additional
+      !capacity
+        | requestedAdditional > maxBound - currentCapacity = maxBound
+        | otherwise = currentCapacity + requestedAdditional
+  case ensureCapacity capacity of
+    Left failure -> Left (mapBuildFailure failure)
+    Right () -> pure ()
+  runST $ do
+    mutable <-
+      case shape of
+        DenseTransaction -> thawTriangulationDense capacity triangulation
+        LocalTransaction -> thawTriangulation capacity triangulation
+    operation <- newOperationState (halfEdgeCapacity mutable)
+    outcome <- action mutable operation
+    case outcome of
+      Left refusal -> pure (Left refusal)
+      Right value -> do
+        frozenOutcome <- freezePublication mutable
+        case frozenOutcome of
+          Left obstruction -> pure (Left (mapBuildFailure obstruction))
+          Right (frozen, publicationStats) -> do
+            receipt <- freezeReceipt operation
+            pure (Right (value, frozen, receipt, publicationStats))
+{-# INLINE runTransactionCore #-}
+
+freezeWithoutPublicationStats
+  :: MutableDcel s vertex directed undirected face
+  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
+freezeWithoutPublicationStats mutable =
+  fmap (fmap (\frozen -> (frozen, emptyPublicationStats))) (freezeTriangulation mutable)
diff --git a/src-build/Moonlight/Planar/Refinement.hs b/src-build/Moonlight/Planar/Refinement.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Refinement.hs
@@ -0,0 +1,722 @@
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Ruppert refinement: Steiner insertion composed after a built mesh, never a
+-- second kind of mesh. Parameters are reached through checked verbs rather than
+-- a raw record, so an unrealizable quality bar is a refusal.
+module Moonlight.Planar.Refinement
+  ( refine
+  , refineWithinDomain
+  , validateRefinementParameters
+  , withMinimumAngle
+  , radiusEdgeRatioForAngle
+  , withAdditionalVertexBudget
+  , withoutAdditionalVertexBudget
+  , withMinimumArea
+  , withoutMinimumArea
+  , withMaximumArea
+  , withoutMaximumArea
+  , withMaximumRadiusEdgeRatio
+  , withoutMaximumRadiusEdgeRatio
+  , withMaximumEdgeLength
+  , withoutMaximumEdgeLength
+  , withConvexHullPreservation
+  , withConstraintPreservation
+  , withOuterFaceExclusion
+  , refinementAdditionalVertexBudget
+  , refinementMinimumArea
+  , refinementMaximumArea
+  , refinementMaximumRadiusEdgeRatio
+  , refinementMaximumEdgeLength
+  , refinementPreservesConvexHull
+  , refinementPreservesConstraints
+  , refinementExcludesOuterFaces
+  ) where
+
+import Control.Monad (void)
+import Control.Monad.ST (ST)
+import Data.Foldable (traverse_)
+import qualified Data.IntSet as IntSet
+import Data.List (sort)
+import Data.Maybe (fromMaybe)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Moonlight.Planar.Dcel
+  ( faceDirectedEdges
+  , faceVertices
+  , incidentFace
+  , innerFaceDirectedEdges
+  , isConstraintEdge
+  , numFaces
+  , numUndirectedEdges
+  , numVertices
+  , origin
+  , undirectedEndpoints
+  , vertexPoint
+  )
+import Moonlight.Planar.FloodFillIterator (facesAtEvenBarrierDepth)
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , FaceId (..)
+  , UndirectedEdgeId (..)
+  , asUndirected
+  , directedPair
+  , faceIdIndex
+  , isNormalized
+  , reverseEdge
+  , undirectedEdgeIdIndex
+  )
+import Moonlight.Planar.Internal.Mutable
+import Moonlight.Planar.Internal.Paged
+  ( PublicationStats
+  , TransactionShape (DenseTransaction, LocalTransaction)
+  )
+import Moonlight.Planar.Internal.Refinement
+import Moonlight.Planar.Internal.Types (RefinementParameters (..))
+import Moonlight.Planar.Internal.Representation (Triangulation (..))
+import Moonlight.Planar.Internal.Transaction (runTransactionWithPublication)
+import Moonlight.Planar.Internal.Validation
+  ( topologyClosureStats
+  )
+import Moonlight.Planar.Types hiding (RefinementParameters)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (NonFiniteValue (..), classifyNonFinite)
+
+data RefinementExecution mode vertex directed undirected face = RefinementExecution
+  { refinementExecutionResult :: !(RefinementResult mode vertex directed undirected face)
+  , refinementExecutionVisitedFaces :: ![Int]
+  , refinementExecutionPermittedFaces :: ![Int]
+  , refinementExecutionInterfaceBoundaryReads :: {-# UNPACK #-} !Int
+  , refinementExecutionBoundaryCrossingAttempts :: {-# UNPACK #-} !Int
+  , refinementExecutionPublicationStats :: !PublicationStats
+  }
+
+-- | The radius-edge ratio admitting a minimum angle, in degrees.
+radiusEdgeRatioForAngle :: Double -> Either BuildError (Maybe Double)
+radiusEdgeRatioForAngle degrees =
+  case classifyNonFinite degrees of
+    Just nonFinite ->
+      Left (RefinementMinimumAngleNotFinite nonFinite)
+    Nothing
+      | degrees < 0 || degrees > 60 ->
+          Left (RefinementMinimumAngleOutOfRange degrees)
+      | degrees == 0 -> Right Nothing
+      | otherwise ->
+          let ratio = 0.5 / sin (degrees * pi / 180)
+           in case classifyNonFinite ratio of
+                Just nonFinite ->
+                  Left (RefinementMinimumAngleDerivedRatioNotFinite nonFinite)
+                Nothing -> Right (Just ratio)
+
+-- | Set the minimum angle, in degrees.
+withMinimumAngle :: Double -> RefinementParameters -> Either BuildError (RefinementParameters)
+withMinimumAngle degrees parameters = do
+  ratio <- radiusEdgeRatioForAngle degrees
+  checkedRefinementParameters parameters{refineMaxRadiusEdgeRatio = ratio}
+
+withAdditionalVertexBudget :: Int -> RefinementParameters -> Either BuildError RefinementParameters
+withAdditionalVertexBudget budget parameters =
+  checkedRefinementParameters parameters{refineMaxAdditionalVertices = Just budget}
+
+withoutAdditionalVertexBudget :: RefinementParameters -> RefinementParameters
+withoutAdditionalVertexBudget parameters =
+  parameters{refineMaxAdditionalVertices = Nothing}
+
+withMinimumArea :: Double -> RefinementParameters -> Either BuildError RefinementParameters
+withMinimumArea area parameters =
+  checkedRefinementParameters parameters{refineMinArea = Just area}
+
+withoutMinimumArea :: RefinementParameters -> RefinementParameters
+withoutMinimumArea parameters =
+  parameters{refineMinArea = Nothing}
+
+withMaximumArea :: Double -> RefinementParameters -> Either BuildError RefinementParameters
+withMaximumArea area parameters =
+  checkedRefinementParameters parameters{refineMaxArea = Just area}
+
+withoutMaximumArea :: RefinementParameters -> RefinementParameters
+withoutMaximumArea parameters =
+  parameters{refineMaxArea = Nothing}
+
+withMaximumRadiusEdgeRatio :: Double -> RefinementParameters -> Either BuildError RefinementParameters
+withMaximumRadiusEdgeRatio ratio parameters =
+  checkedRefinementParameters parameters{refineMaxRadiusEdgeRatio = Just ratio}
+
+withoutMaximumRadiusEdgeRatio :: RefinementParameters -> RefinementParameters
+withoutMaximumRadiusEdgeRatio parameters =
+  parameters{refineMaxRadiusEdgeRatio = Nothing}
+
+withMaximumEdgeLength :: Double -> RefinementParameters -> Either BuildError RefinementParameters
+withMaximumEdgeLength lengthValue parameters =
+  checkedRefinementParameters parameters{refineMaxEdgeLength = Just lengthValue}
+
+withoutMaximumEdgeLength :: RefinementParameters -> RefinementParameters
+withoutMaximumEdgeLength parameters =
+  parameters{refineMaxEdgeLength = Nothing}
+
+withConvexHullPreservation :: Bool -> RefinementParameters -> RefinementParameters
+withConvexHullPreservation preserve parameters =
+  parameters{refinePreserveConvexHull = preserve}
+
+withConstraintPreservation :: Bool -> RefinementParameters -> RefinementParameters
+withConstraintPreservation preserve parameters =
+  parameters{refineKeepConstraintEdges = preserve}
+
+withOuterFaceExclusion :: Bool -> RefinementParameters -> RefinementParameters
+withOuterFaceExclusion exclude parameters =
+  parameters{refineExcludeOuterFaces = exclude}
+
+refinementAdditionalVertexBudget :: RefinementParameters -> Maybe Int
+refinementAdditionalVertexBudget = refineMaxAdditionalVertices
+
+refinementMinimumArea :: RefinementParameters -> Maybe Double
+refinementMinimumArea = refineMinArea
+
+refinementMaximumArea :: RefinementParameters -> Maybe Double
+refinementMaximumArea = refineMaxArea
+
+refinementMaximumRadiusEdgeRatio :: RefinementParameters -> Maybe Double
+refinementMaximumRadiusEdgeRatio = refineMaxRadiusEdgeRatio
+
+refinementMaximumEdgeLength :: RefinementParameters -> Maybe Double
+refinementMaximumEdgeLength = refineMaxEdgeLength
+
+refinementPreservesConvexHull :: RefinementParameters -> Bool
+refinementPreservesConvexHull = refinePreserveConvexHull
+
+refinementPreservesConstraints :: RefinementParameters -> Bool
+refinementPreservesConstraints = refineKeepConstraintEdges
+
+refinementExcludesOuterFaces :: RefinementParameters -> Bool
+refinementExcludesOuterFaces = refineExcludeOuterFaces
+
+checkedRefinementParameters
+  :: RefinementParameters
+  -> Either BuildError RefinementParameters
+checkedRefinementParameters parameters = do
+  validateRefinementParameters parameters
+  pure parameters
+
+-- | Refine an unconstrained or constrained triangulation in the same finite
+-- DCEL. The constructor supplies application payloads for Steiner vertices;
+-- payloads annotate the requested geometric point and cannot reauthor it.
+refine
+  :: (Point -> vertex)
+  -> RefinementParameters
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError (RefinementResult mode vertex directed undirected face)
+refine makeVertex parameters =
+  fmap refinementExecutionResult
+    . refineWithInitialSeed RefineEveryFace Nothing makeVertex parameters
+
+-- | Derive the exact immutable interface separating a checked active face
+-- section from every protected inner face.  A permitted face set determines
+-- this cut uniquely, so callers cannot supply a second claim about Gamma.
+mkRefinementDomain
+  :: Set.Set FaceId
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError RefinementDomain
+mkRefinementDomain permittedFaces triangulation = do
+  permitted <- validateSeedFaces triangulation permittedFaces
+  Right
+    RefinementDomain
+      { refinementDomainPermittedFaces = permitted
+      , refinementDomainInterfacePairs = expectedInterface permitted
+      , refinementDomainInputFaceCount = numFaces triangulation
+      }
+ where
+  expectedInterface permitted =
+    IntSet.fromList
+      [ pair
+      | rawFace <- IntSet.toAscList permitted
+      , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))
+      , let pair = undirectedEdgeIdIndex (asUndirected edge)
+      , let adjacent = incidentFace triangulation (reverseEdge edge)
+      , adjacent /= FaceId 0
+      , IntSet.notMember (faceIdIndex adjacent) permitted
+      ]
+
+-- | Refine exactly one checked local section. Interface edges are installed as
+-- transaction-local legalization barriers and removed before publication.
+-- The collar proof checks the seam (protected faces and interface edges);
+-- the receipt reads only the closure the domain spans, and counts it rather
+-- than re-validating the interpreter's output.
+refineWithinDomain
+  :: forall mode vertex directed undirected face.
+     (Point -> vertex)
+  -> RefinementParameters
+  -> Set.Set FaceId
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError (RefinementDomainResult mode vertex directed undirected face)
+refineWithinDomain makeVertex parameters permittedFaces triangulation = do
+  validateDomainParameters parameters
+  domain <- mkRefinementDomain permittedFaces triangulation
+  execution <-
+    refineWithInitialSeed
+      (RefineSeededFaces (refinementDomainPermittedFaces domain))
+      (Just domain)
+      makeVertex
+      parameters
+      triangulation
+  let result = refinementExecutionResult execution
+      finalPermittedList = refinementExecutionPermittedFaces execution
+      -- The interpreter publishes its permitted faces as marked indices in
+      -- ascending order, so the set is built once here without sorting and
+      -- shared by the seam check and the receipt.
+      finalPermitted = IntSet.fromDistinctAscList finalPermittedList
+  (closureStats, finalInterfaceIncidence) <- validateDomainClosure
+    domain
+    triangulation
+    (refinedTriangulation result)
+    finalPermitted
+  let receipt =
+        buildRefinementReceipt
+          domain
+          triangulation
+          (refinedTriangulation result)
+          (refinementExecutionVisitedFaces execution)
+          finalPermittedList
+          finalPermitted
+          finalInterfaceIncidence
+          (refinementExecutionInterfaceBoundaryReads execution)
+          (refinementExecutionBoundaryCrossingAttempts execution)
+          (refinementExecutionPublicationStats execution)
+          closureStats
+  case V.toList (refinementVisitedProtectedFaces receipt) of
+    protected : _ -> Left (RefinementDomainWouldRewriteProtectedFace protected)
+    [] ->
+       let restoredResult :: RefinementResult mode vertex directed undirected face
+           restoredResult =
+            result
+              { refinedTriangulation =
+                  (refinedTriangulation result)
+                    -- The checked local interpreter rejects every hull-pair
+                    -- split, so the cached outer cycle remains an exact
+                    -- handle witness and can be restored without a scan.
+                    { triSeamFrontier = triSeamFrontier triangulation
+                    }
+              }
+       in Right
+        RefinementDomainResult
+          { refinementDomainResult = restoredResult
+          , refinementDomainReceipt = receipt
+          }
+
+validateDomainParameters :: RefinementParameters -> Either BuildError ()
+validateDomainParameters parameters
+  | not (refinePreserveConvexHull parameters) =
+      Left RefinementDomainRequiresConvexHullPreservation
+  | not (refineKeepConstraintEdges parameters) =
+      Left RefinementDomainRequiresConstraintPreservation
+  | refineExcludeOuterFaces parameters =
+      Left RefinementDomainForbidsOuterFaceExclusion
+  | refineMaxAdditionalVertices parameters == Nothing =
+      Left RefinementDomainRequiresFiniteVertexBudget
+  | otherwise = Right ()
+
+refineWithInitialSeed
+  :: RefinementInitialSeed
+  -> Maybe RefinementDomain
+  -> (Point -> vertex)
+  -> RefinementParameters
+  -> Triangulation mode vertex directed undirected face
+  -> Either BuildError (RefinementExecution mode vertex directed undirected face)
+refineWithInitialSeed initialSeed domain makeVertex parameters triangulation = do
+  validateRefinementParameters parameters
+  let originalCount = numVertices triangulation
+      budget =
+        case domain of
+          Just _ -> max 0 (fromMaybe 0 (refineMaxAdditionalVertices parameters))
+          Nothing -> max 0 (fromMaybe (10 * max 1 originalCount) (refineMaxAdditionalVertices parameters))
+      initialExcludedFaces =
+        if domain == Nothing && refineExcludeOuterFaces parameters
+          then
+            IntSet.fromList
+              [ faceIdIndex face
+              | face <-
+                  facesAtEvenBarrierDepth
+                    triangulation
+                    (isConstraintEdge triangulation)
+              ]
+          else IntSet.empty
+  ( outcome
+    , frozen
+    , stats
+    , publicationStats
+    ) <-
+    runTransactionWithPublication id (transactionShape domain) triangulation budget $ \mutable operation -> do
+      installInterfaceBarriers mutable domain
+      refinement <-
+        refineMutable
+          makeVertex
+          mutable
+          operation
+          parameters
+          budget
+          initialExcludedFaces
+          initialSeed
+          domain
+      removeInterfaceBarriers mutable triangulation domain
+      pure refinement
+  let (complete, added, excluded, visited, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts) = outcome
+  pure
+    RefinementExecution
+      { refinementExecutionResult =
+          RefinementResult
+            { refinedTriangulation = frozen
+            , refinementStats = stats
+            , refinementAddedVertices = added
+            , refinementComplete = complete
+            , refinementExcludedFaces = V.fromList (map (FaceId . fromIntegral) excluded)
+            }
+      , refinementExecutionVisitedFaces = visited
+      , refinementExecutionPermittedFaces = permittedFaces
+      , refinementExecutionInterfaceBoundaryReads = interfaceBoundaryReads
+      , refinementExecutionBoundaryCrossingAttempts = boundaryCrossingAttempts
+      , refinementExecutionPublicationStats = publicationStats
+      }
+
+transactionShape :: Maybe RefinementDomain -> TransactionShape
+transactionShape = maybe DenseTransaction (const LocalTransaction)
+
+installInterfaceBarriers
+  :: MutableDcel s vertex directed undirected face
+  -> Maybe RefinementDomain
+  -> ST s ()
+installInterfaceBarriers mutable =
+  traverse_
+    (\pair -> void (setConstraint mutable (2 * pair)))
+    . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs)
+
+removeInterfaceBarriers
+  :: MutableDcel s vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face
+  -> Maybe RefinementDomain
+  -> ST s ()
+removeInterfaceBarriers mutable input =
+  traverse_
+    (\pair ->
+       let edge = UndirectedEdgeId (fromIntegral pair)
+        in if isConstraintEdge input edge
+             then pure ()
+             else void (clearConstraint mutable (2 * pair))
+    )
+    . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs)
+
+buildRefinementReceipt
+  :: RefinementDomain
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face
+  -> [Int]
+  -> [Int]
+  -> IntSet.IntSet
+  -> V.Vector (UndirectedEdgeId, FaceId, FaceId)
+  -> Int
+  -> Int
+  -> PublicationStats
+  -> ClosureStats
+  -> RefinementReceipt
+buildRefinementReceipt domain before after visited permittedFaces finalFaces finalInterfaceIncidence interfaceBoundaryReads boundaryCrossingAttempts publicationStats closureStats =
+  RefinementReceipt
+    { refinementVisitedJoinFaces = V.fromList (fmap toFace visitedJoin)
+    , refinementVisitedProtectedFaces = V.fromList (fmap toFace visitedProtected)
+    , refinementCreatedFaces = V.fromList (fmap toFace createdFaces)
+    , refinementFinalPermittedFaces = V.fromList (fmap toFace permittedFaces)
+    , refinementFinalInterfaceIncidence = finalInterfaceIncidence
+    , refinementTouchedEdges = V.fromList (fmap toEdge touchedEdges)
+    , refinementRemovedEdges = V.fromList (fmap toEdge removedEdges)
+    , refinementInterfaceBoundaryReads = interfaceBoundaryReads
+    , refinementAttemptedBoundaryCrossings = boundaryCrossingAttempts
+    , refinementPublicationStats = publicationStats
+    , refinementClosureStats = closureStats
+    }
+ where
+  permitted = refinementDomainPermittedFaces domain
+  (visitedProtected, visitedJoin) =
+    foldr
+      (\face (protected, join) ->
+         if IntSet.member face permitted
+           then (protected, face : join)
+           else if face >= refinementDomainInputFaceCount domain
+             then (protected, face : join)
+           else (face : protected, join))
+      ([], []) visited
+  initialFaces = refinementDomainPermittedFaces domain
+  -- The face closure is the initial faces merged with the final faces new to
+  -- them, both ascending, so the union is never materialized.
+  faceClosure =
+    mergeAscending
+      (IntSet.toAscList initialFaces)
+      (filter (`IntSet.notMember` initialFaces) permittedFaces)
+  createdFaces =
+    [ face
+    | face <- faceClosure
+    , localFaceSignature before face /= localFaceSignature after face
+    ]
+  -- The edge closure is emitted once per pair from the cycles of its faces:
+  -- an initial face at `before` yields the pairs of its cycle whose other
+  -- side is not a lower initial face, a final face at `after` likewise over
+  -- the final faces, minus every pair an initial cycle already carried.
+  touchedEdges =
+    sort
+      [ pair
+      | pair <-
+          sectionPairs before initialFaces (IntSet.toAscList initialFaces)
+            ++ filter (not . inInitialSection)
+                 (sectionPairs after finalFaces permittedFaces)
+      , localEdgeSignature before pair /= localEdgeSignature after pair
+      ]
+  -- A removed edge is a touched edge of the initial section: the touched list
+  -- restricted to the initial edges, not a second pass over their signatures.
+  removedEdges = filter inInitialSection touchedEdges
+  initialPairCount = numUndirectedEdges before
+  inInitialSection pair =
+    pair < initialPairCount
+      && any (carriedByInitialFace . fromIntegral) [2 * pair, 2 * pair + 1]
+  -- A recycled slot may still point at an initial face from before, so the
+  -- face's cycle is consulted rather than the slot's face pointer alone.
+  carriedByInitialFace edge =
+    let face = incidentFace before (DirectedEdgeId edge)
+     in IntSet.member (faceIdIndex face) initialFaces
+          && DirectedEdgeId edge `elem` faceDirectedEdges before face
+  toFace = FaceId . fromIntegral
+  toEdge = UndirectedEdgeId . fromIntegral
+
+-- | A face's geometric identity: its cycle's points in ascending order. A
+-- triangle is held in three fields rather than a sorted list; any other cycle
+-- keeps the list so the identity stays total over what the arena may hold.
+data FaceSignature
+  = FaceAbsent
+  | FaceTriangle !Point !Point !Point
+  | FaceCycle [Point]
+  deriving stock (Eq)
+
+localFaceSignature
+  :: Triangulation mode vertex directed undirected face
+  -> Int
+  -> FaceSignature
+localFaceSignature triangulation raw
+  | raw <= 0 || raw >= numFaces triangulation = FaceAbsent
+  | otherwise =
+      let face = FaceId (fromIntegral raw)
+       in case innerFaceDirectedEdges triangulation face of
+            Just (e0, e1, e2)
+              | e0 /= e1 ->
+                  sortedTriangle
+                    (vertexPoint triangulation (origin triangulation e0))
+                    (vertexPoint triangulation (origin triangulation e1))
+                    (vertexPoint triangulation (origin triangulation e2))
+            _ -> FaceCycle (sort (fmap (vertexPoint triangulation) (faceVertices triangulation face)))
+
+sortedTriangle :: Point -> Point -> Point -> FaceSignature
+sortedTriangle a b c
+  | a <= b =
+      if b <= c
+        then FaceTriangle a b c
+        else if a <= c then FaceTriangle a c b else FaceTriangle c a b
+  | otherwise =
+      if a <= c
+        then FaceTriangle b a c
+        else if b <= c then FaceTriangle b c a else FaceTriangle c b a
+
+-- | The undirected edges carried by a section's faces, each once: a cycle
+-- edge belongs to the lowest section face on either side of it, and to the
+-- normalized direction when one face carries both directions.
+sectionPairs
+  :: Triangulation mode vertex directed undirected face
+  -> IntSet.IntSet
+  -> [Int]
+  -> [Int]
+sectionPairs triangulation faces faceList =
+  [ undirectedEdgeIdIndex (asUndirected edge)
+  | rawFace <- faceList
+  , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))
+  , let across = faceIdIndex (incidentFace triangulation (reverseEdge edge))
+  , IntSet.notMember across faces
+      || rawFace < across
+      || (rawFace == across && isNormalized edge)
+  ]
+
+mergeAscending :: [Int] -> [Int] -> [Int]
+mergeAscending [] ys = ys
+mergeAscending xs [] = xs
+mergeAscending (x : xs) (y : ys)
+  | x < y = x : mergeAscending xs (y : ys)
+  | otherwise = y : mergeAscending (x : xs) ys
+
+-- | An edge's identity: its endpoints and its two incident faces, each pair
+-- in ascending order so the directed orientation does not count.
+data EdgeSignature
+  = EdgeAbsent
+  | EdgePresent !Point !Point !FaceId !FaceId
+  deriving stock (Eq)
+
+localEdgeSignature
+  :: Triangulation mode vertex directed undirected face
+  -> Int
+  -> EdgeSignature
+localEdgeSignature triangulation raw
+  | raw < 0 || raw >= numUndirectedEdges triangulation = EdgeAbsent
+  | otherwise =
+      let edge = UndirectedEdgeId (fromIntegral raw)
+          (fromVertex, toVertex) = undirectedEndpoints triangulation edge
+          !fromPoint = vertexPoint triangulation fromVertex
+          !toPoint = vertexPoint triangulation toVertex
+          (forward, backward) = directedPair edge
+          !forwardFace = incidentFace triangulation forward
+          !backwardFace = incidentFace triangulation backward
+       in EdgePresent
+            (min fromPoint toPoint)
+            (max fromPoint toPoint)
+            (min forwardFace backwardFace)
+            (max forwardFace backwardFace)
+
+validateDomainClosure
+  :: RefinementDomain
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face
+  -> IntSet.IntSet
+  -> Either BuildError (ClosureStats, V.Vector (UndirectedEdgeId, FaceId, FaceId))
+validateDomainClosure domain before after finalPermitted = do
+  -- The published value is the interpreter's own output, admitted by its
+  -- type as every other publication is; the closure it spans is counted,
+  -- not walked. What is checked here is the seam: the protected collar and
+  -- the interface edges the domain promised to leave alone.
+  let closureStats =
+        topologyClosureStats
+          (refinementDomainPermittedFaces domain)
+          finalPermitted
+          (refinementDomainInterfacePairs domain)
+          after
+  traverse_ validateProtectedFace (IntSet.toAscList protectedFaces)
+  finalInterfaceIncidence <-
+    V.fromList
+      <$> traverse
+        validateInterfaceEdge
+        (IntSet.toAscList (refinementDomainInterfacePairs domain))
+  pure (closureStats, finalInterfaceIncidence)
+ where
+  permitted = refinementDomainPermittedFaces domain
+  protectedFaces =
+    IntSet.fromList
+      [ rawFace
+      | rawPair <- IntSet.toAscList (refinementDomainInterfacePairs domain)
+      , let edge = UndirectedEdgeId (fromIntegral rawPair)
+      , let (forward, backward) = directedPair edge
+      , rawFace <-
+          [ faceIdIndex (incidentFace before forward)
+          , faceIdIndex (incidentFace before backward)
+          ]
+      , rawFace > 0
+      , IntSet.notMember rawFace permitted
+      ]
+
+  validateProtectedFace rawFace =
+    if localFaceSignature before rawFace == localFaceSignature after rawFace
+      then Right ()
+      else Left (RefinementDomainProtectedFaceChanged (FaceId (fromIntegral rawFace)))
+
+  validateInterfaceEdge rawPair =
+    let edge = UndirectedEdgeId (fromIntegral rawPair)
+     in case (localEdgeSignature before rawPair, localEdgeSignature after rawPair) of
+      ( EdgePresent beforeFrom beforeTo beforeLeft beforeRight
+        , EdgePresent afterFrom afterTo afterLeft afterRight
+        )
+          | beforeFrom == afterFrom && beforeTo == afterTo ->
+              validateInterfaceFaces edge [beforeLeft, beforeRight] [afterLeft, afterRight]
+      _ -> Left RefinementDomainTopologyChanged
+
+  validateInterfaceFaces edge beforeFaces afterFaces =
+    case filter protected beforeFaces of
+      [protectedFace]
+        | protectedFace `elem` afterFaces ->
+            case filter (/= protectedFace) afterFaces of
+              [oppositeFace]
+                | IntSet.member (faceIdIndex oppositeFace) finalPermitted ->
+                    Right (edge, protectedFace, oppositeFace)
+                | otherwise ->
+                    Left
+                      ( RefinementDomainInterfaceOppositeFaceNotPermitted
+                          edge
+                          oppositeFace
+                      )
+              _ -> Left RefinementDomainTopologyChanged
+      _ -> Left RefinementDomainTopologyChanged
+
+  protected face =
+    let raw = faceIdIndex face
+     in raw > 0 && IntSet.notMember raw permitted
+
+-- | Refuse outer or absent faces rather than silently treating an invalid
+-- topology witness as an empty repair. Face zero is the outer face and has no
+-- active refinement equation.
+validateSeedFaces
+  :: Triangulation mode vertex directed undirected face
+  -> Set.Set FaceId
+  -> Either BuildError IntSet.IntSet
+validateSeedFaces triangulation requested =
+  IntSet.fromList <$> traverse validateFace (Set.toAscList requested)
+ where
+  totalFaces = numFaces triangulation
+
+  validateFace :: FaceId -> Either BuildError Int
+  validateFace face
+    | face == FaceId 0 || toInteger (unFaceId face) >= toInteger totalFaces =
+        Left (RefinementSeedFaceNotActive face totalFaces)
+    | otherwise = Right (faceIdIndex face)
+
+-- | Refuse parameters no mesh can satisfy.
+validateRefinementParameters :: RefinementParameters -> Either BuildError ()
+validateRefinementParameters RefinementParameters{refineMaxAdditionalVertices, refineMinArea, refineMaxArea, refineMaxRadiusEdgeRatio, refineMaxEdgeLength} = do
+  case refineMaxAdditionalVertices of
+    Just value
+      | value < 0 ->
+          Left (RefinementMaximumAdditionalVerticesNegative value)
+    _ -> Right ()
+  validateOptionalRefinementParameter
+    RefinementMinimumAreaNotFinite
+    RefinementMinimumAreaNegative
+    (>= 0)
+    refineMinArea
+  validateOptionalRefinementParameter
+    RefinementMaximumAreaNotFinite
+    RefinementMaximumAreaNotPositive
+    (> 0)
+    refineMaxArea
+  validateOptionalRefinementParameter
+    RefinementMaximumRadiusEdgeRatioNotFinite
+    RefinementMaximumRadiusEdgeRatioNotPositive
+    (> 0)
+    refineMaxRadiusEdgeRatio
+  validateOptionalRefinementParameter
+    RefinementMaximumEdgeLengthNotFinite
+    RefinementMaximumEdgeLengthNotPositive
+    (> 0)
+    refineMaxEdgeLength
+  case (refineMinArea, refineMaxArea) of
+    (Just minimumArea, Just maximumArea)
+      | minimumArea > maximumArea ->
+          Left
+            ( RefinementMinimumAreaExceedsMaximum
+                minimumArea
+                maximumArea
+            )
+    _ -> Right ()
+
+validateOptionalRefinementParameter
+  :: (NonFiniteValue -> BuildError)
+  -> (Double -> BuildError)
+  -> (Double -> Bool)
+  -> Maybe Double
+  -> Either BuildError ()
+validateOptionalRefinementParameter nonFinite outsideRange predicate value =
+  case value of
+    Nothing -> Right ()
+    Just number ->
+      case classifyNonFinite number of
+        Just nonFiniteValue ->
+          Left (nonFinite nonFiniteValue)
+        Nothing
+          | predicate number -> Right ()
+          | otherwise ->
+              Left (outsideRange number)
diff --git a/src-build/Moonlight/Planar/Removal.hs b/src-build/Moonlight/Planar/Removal.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Removal.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Persistent removal: one excision, published as its own triangulation.
+module Moonlight.Planar.Removal
+  ( RemovalOutcome (..)
+  , RemovalResult (..)
+  , removeVertex
+  , locateAndRemove
+  ) where
+
+import Control.DeepSeq (NFData)
+import Moonlight.Planar.Internal.HandleDefs (VertexId (..))
+import Moonlight.Planar.Internal.PointIndex (lookupPointIndex)
+import Moonlight.Planar.Internal.Representation (Triangulation (..))
+import Moonlight.Planar.Internal.Types (BuildError)
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Point (Point, queryPointValue)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Internal.Session (RemovalOutcome (..), excise, withLocalSession)
+import GHC.Generics (Generic)
+
+-- | One removal and its publication: the frozen triangulation, the outcome
+-- record, and the operation's counters. The persistent entries publish all
+-- three; a session publishes the outcome per call and the counters once.
+data RemovalResult mode vertex directed undirected face = RemovalResult
+  { removalTriangulation :: !(Triangulation mode vertex directed undirected face)
+  , removalOutcome :: !(RemovalOutcome vertex)
+  , removalStats :: !BuildStats
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance
+  (Eq vertex, Eq directed, Eq undirected, Eq face)
+  => Eq (RemovalResult mode vertex directed undirected face)
+deriving stock instance
+  (Show vertex, Show directed, Show undirected, Show face)
+  => Show (RemovalResult mode vertex directed undirected face)
+
+-- | Remove the vertex a handle names. The triangulation the caller passed in
+-- still denotes the mesh it always did; the removal is in the returned one.
+--
+-- This is a session over a single 'excise', which is what makes replacing a
+-- fold of it with one session sound: the two agree on every mesh, and differ
+-- only in how many of the intermediate ones are published. A fold publishes
+-- @k@ meshes and pays a thaw for each, so it runs in Θ(n·k); the session pays
+-- one thaw and runs in O(k·deg).
+removeVertex
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> Either BuildError (RemovalResult mode vertex directed undirected face)
+removeVertex triangulation requested = do
+  (outcome, frozen, stats) <- withLocalSession triangulation 0 (excise requested)
+  pure
+    RemovalResult
+      { removalTriangulation = frozen
+      , removalOutcome = outcome
+      , removalStats = stats
+      }
+
+-- | Resolve a point through the mesh's exact derived identity section, then
+-- remove the handle it proves. A hash is only a rejection filter; the lookup
+-- confirms both authoritative coordinate planes before the topology is
+-- opened. A point that sites no vertex answers 'Nothing' without publishing a
+-- cosmetically different mesh.
+--
+-- See 'removeVertex' on the cost of folding this rather than opening one
+-- session over @removeAt@.
+locateAndRemove
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Either BuildError (Maybe (RemovalResult mode vertex directed undirected face))
+locateAndRemove triangulation point = do
+  queryPoint <- validatePoint Nothing point
+  case
+      lookupPointIndex
+        (triPointX triangulation)
+        (triPointY triangulation)
+        (triPointIndex triangulation)
+        -- Stored coordinates are canonical; the query must be too, or any
+        -- coordinate whose canonicalization exceeds signed zero misses a vertex
+        -- the session verbs would find.
+        (queryPointValue queryPoint) of
+    Nothing -> Right Nothing
+    Just rawVertex ->
+      Just <$> removeVertex triangulation (VertexId (fromIntegral rawVertex))
+
+-- Polymorphic entries consumed from other packages; without exposed
+-- unfoldings they run boxed through an imported boundary.
diff --git a/src-build/Moonlight/Planar/Session.hs b/src-build/Moonlight/Planar/Session.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/Session.hs
@@ -0,0 +1,17 @@
+-- | One sealed editing transaction: thaw once, compose edits, publish once.
+module Moonlight.Planar.Session
+  ( RemovalOutcome (..)
+  , Session
+  , withSession
+  , insertVertex
+  , insertVertexAt
+  , insertVertexAtNear
+  , insertVertexAtNearVertex
+  , removeAt
+  , removeAtNear
+  , removeManyAt
+  , removeManyAtNear
+  , refuse
+  ) where
+
+import Moonlight.Planar.Internal.Session
diff --git a/src-build/Moonlight/Planar/SetAlgebra.hs b/src-build/Moonlight/Planar/SetAlgebra.hs
new file mode 100644
--- /dev/null
+++ b/src-build/Moonlight/Planar/SetAlgebra.hs
@@ -0,0 +1,231 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Finite-set operations on unconstrained meshes. Every
+-- constructing operation returns the finite-arena obstruction instead of
+-- laundering it through a partial class instance.
+module Moonlight.Planar.SetAlgebra
+  ( siteRelation
+  , union
+  , unions
+  , intersection
+  , intersectionWith
+  , difference
+  , symmetricDifference
+  ) where
+
+import Data.Foldable (traverse_)
+import Data.Maybe (isJust)
+import qualified Data.Vector as V
+import Moonlight.Planar.BulkLoad (empty)
+import Moonlight.Planar.Dcel (numVertices, vertexData, vertexPoint, vertexPoints)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Internal.Join (joinBalanced, joinNormalForm)
+import Moonlight.Planar.Internal.Join.SiteSet
+  ( rebuildCanonicalSiteSet
+  , siteSetDifference
+  , siteSetFromTriangulation
+  , siteSetIntersectionWith
+  , siteSetPoints
+  , siteRelationFromTriangulations
+  , siteSetSymmetricDifferenceFromTriangulations
+  , siteSupportFromTriangulation
+  )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Internal.Types (BuildError (PointLocationFailed), ConstraintMode (Unconstrained), InsertionDisposition (..), SiteRelation (..), unitElementDefaults)
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Point (Point)
+import Moonlight.Algebra.Pure.Join (JoinSemilattice)
+import Moonlight.Planar.Internal.Session
+  ( Session
+  , insertVertexAt
+  , refuse
+  , removeAtNear
+  , removeManyAt
+  , withLocalSession
+  )
+
+-- | Exact relation between two triangulations' coordinate supports. Vertex
+-- annotations and topology-element payloads are observations over the support;
+-- none participates in this classification.
+siteRelation
+  :: Triangulation leftMode leftAnnotation leftDirected leftUndirected leftFace
+  -> Triangulation rightMode rightAnnotation rightDirected rightUndirected rightFace
+  -> SiteRelation
+siteRelation left right =
+  siteRelationFromTriangulations left right
+{-# INLINE siteRelation #-}
+
+-- | A valid Delaunay representative of both site sets. Use
+-- 'Moonlight.Planar.Canonical.canonicalize'
+-- when construction-independent dense numbering is required.
+union
+  :: (Eq annotation, JoinSemilattice annotation)
+  => Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+union = joinNormalForm
+
+-- | @union@ over a list, folded as a balanced tournament.
+unions
+  :: (Eq annotation, JoinSemilattice annotation)
+  => [Triangulation 'Unconstrained annotation () () ()]
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+unions = joinBalanced
+
+-- | The sites both meshes hold.
+intersection
+  :: Triangulation 'Unconstrained () () () ()
+  -> Triangulation 'Unconstrained () () () ()
+  -> Either BuildError (Triangulation 'Unconstrained () () () ())
+intersection left right
+  | numVertices left == 0 = Right left
+  | numVertices right == 0 = Right right
+  | otherwise =
+      case siteRelationFromTriangulations left right of
+        EqualSites -> Right left
+        LeftProperSubset -> Right left
+        RightProperSubset -> Right right
+        DisjointSites -> Right (empty unitElementDefaults)
+        PartialOverlap overlap -> intersectPartialOverlap overlap
+ where
+  leftSites = siteSupportFromTriangulation left
+  rightSites = siteSupportFromTriangulation right
+  rebuildIntersection = intersectionWith (\_ _ -> ()) left right
+  intersectPartialOverlap overlap
+    | leftRemoved <= rightRemoved
+    , removalDeltaIsEligible leftRemoved overlap =
+        removeExpectedFrom left (siteSetPoints (siteSetDifference leftSites rightSites))
+    | rightRemoved < leftRemoved
+    , removalDeltaIsEligible rightRemoved overlap =
+        removeExpectedFrom right (siteSetPoints (siteSetDifference rightSites leftSites))
+    | otherwise = rebuildIntersection
+   where
+    leftRemoved = numVertices left - overlap
+    rightRemoved = numVertices right - overlap
+{-# INLINE intersection #-}
+
+-- | The sites both meshes hold, with the result annotation computed from the
+-- left and right annotations at that exact coordinate. The combiner is called
+-- only for shared sites, in left-then-right order; geometry remains the sole
+-- authority for membership. @intersectionWith const left mask@ is the
+-- annotation-preserving restriction of @left@ to @mask@'s support.
+intersectionWith
+  :: (leftAnnotation -> rightAnnotation -> annotation)
+  -> Triangulation 'Unconstrained leftAnnotation () () ()
+  -> Triangulation 'Unconstrained rightAnnotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+intersectionWith combine left right =
+  rebuildCanonicalSiteSet
+    ( siteSetIntersectionWith
+        combine
+        (siteSetFromTriangulation left)
+        (siteSetFromTriangulation right)
+    )
+{-# INLINE intersectionWith #-}
+
+-- | The left's sites, less the right's.
+difference
+  :: Triangulation 'Unconstrained leftAnnotation () () ()
+  -> Triangulation 'Unconstrained rightAnnotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained leftAnnotation () () ())
+difference left right
+  | numVertices left == 0 || numVertices right == 0 = Right left
+  | numVertices right < numVertices left
+  , removalDeltaIsEligible (numVertices right) (numVertices left - numVertices right) =
+      removeAvailableFrom left (vertexPoints right)
+  | otherwise =
+      rebuildCanonicalSiteSet
+        ( siteSetDifference
+            (siteSetFromTriangulation left)
+            (siteSupportFromTriangulation right)
+        )
+{-# INLINE difference #-}
+
+-- | The sites exactly one mesh holds.
+symmetricDifference
+  :: Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+symmetricDifference left right
+  | numVertices left == 0 = Right right
+  | numVertices right == 0 = Right left
+  | numVertices right < numVertices left
+  , toggleDeltaIsEligible (numVertices right) (numVertices left - numVertices right) =
+      toggleIncoming left right
+  | numVertices left < numVertices right
+  , toggleDeltaIsEligible (numVertices left) (numVertices right - numVertices left) =
+      toggleIncoming right left
+  | otherwise =
+      siteSetSymmetricDifferenceFromTriangulations left right
+        >>= rebuildCanonicalSiteSet
+{-# INLINE symmetricDifference #-}
+
+removalDeltaIsEligible :: Int -> Int -> Bool
+removalDeltaIsEligible removed survivors = removed <= survivors `quot` 128
+{-# INLINE removalDeltaIsEligible #-}
+
+toggleDeltaIsEligible :: Int -> Int -> Bool
+toggleDeltaIsEligible incoming residentRemainder = incoming <= residentRemainder `quot` 128
+{-# INLINE toggleDeltaIsEligible #-}
+
+removeAvailableFrom
+  :: forall annotation. Triangulation 'Unconstrained annotation () () ()
+  -> V.Vector (Point)
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+removeAvailableFrom triangulation points =
+  fmap published
+    (withLocalSession triangulation 0 (V.any isJust <$> removeManyAt points))
+ where
+  published
+    :: (Bool, Triangulation 'Unconstrained annotation () () (), BuildStats)
+    -> Triangulation 'Unconstrained annotation () () ()
+  published (removed, revised, _) = if removed then revised else triangulation
+{-# INLINE removeAvailableFrom #-}
+
+removeExpectedFrom
+  :: Triangulation 'Unconstrained annotation () () ()
+  -> V.Vector (Point)
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+removeExpectedFrom triangulation points =
+  fmap (\(_, revised, _) -> revised)
+    (withLocalSession triangulation 0 (removeExpectedPoints points))
+{-# INLINE removeExpectedFrom #-}
+
+removeExpectedPoints
+  :: V.Vector (Point)
+  -> Session state annotation () () () ()
+removeExpectedPoints points = do
+  outcomes <- removeManyAt points
+  V.zipWithM_
+    (\point outcome -> maybe (refuse (PointLocationFailed point)) (const (pure ())) outcome)
+    points
+    outcomes
+{-# INLINE removeExpectedPoints #-}
+
+toggleIncoming
+  :: Triangulation 'Unconstrained annotation () () ()
+  -> Triangulation 'Unconstrained annotation () () ()
+  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
+toggleIncoming base incoming =
+  fmap (\(_, revised, _) -> revised)
+    (withLocalSession base (numVertices incoming) (toggleIncomingVertices incoming))
+{-# INLINE toggleIncoming #-}
+
+toggleIncomingVertices
+  :: Triangulation 'Unconstrained annotation () () ()
+  -> Session state annotation () () () ()
+toggleIncomingVertices incoming =
+  traverse_
+    (\vertex -> do
+      let point = vertexPoint incoming vertex
+          annotation = vertexData incoming vertex
+      (fresh, disposition) <- insertVertexAt point annotation
+      case disposition of
+        Inserted -> pure ()
+        AlreadyPresent -> do
+          outcome <- removeAtNear fresh point
+          maybe (refuse (PointLocationFailed point)) (const (pure ())) outcome
+    )
+    (vertices incoming)
+{-# INLINE toggleIncomingVertices #-}
diff --git a/src-build/Moonlight/Triangulation/BulkLoad.hs b/src-build/Moonlight/Triangulation/BulkLoad.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/BulkLoad.hs
+++ /dev/null
@@ -1,14 +0,0 @@
--- | Whole-support construction and persistent incremental insertion.
-module Moonlight.Triangulation.BulkLoad
-  ( empty
-  , clear
-  , delaunay
-  , delaunayGeometry
-  , DuplicatePayloadPolicy (..)
-  , delaunayFromCoordinates
-  , insert
-  , insertAt
-  , insertMany
-  ) where
-
-import Moonlight.Triangulation.Internal.BulkLoad
diff --git a/src-build/Moonlight/Triangulation/Cdt.hs b/src-build/Moonlight/Triangulation/Cdt.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Cdt.hs
+++ /dev/null
@@ -1,119 +0,0 @@
--- | The constraint layer: the partial map from sites and segments, defined
--- exactly on realizable segment sets and naming its witness where it is not.
-module Moonlight.Triangulation.Cdt
-  ( ConstrainedDelaunayTriangulation
-  , CdtError (..)
-  , CorridorObstruction (..)
-  , ConstraintRecoveryResult (..)
-  , ConstraintResult
-  , ConstraintOutcome (..)
-  , ConstraintBatchStats (..)
-  , ConstraintBatchResult (..)
-  , ConstrainedExtensionResult (..)
-  , ConstrainedSeamSide (..)
-  , ConstrainedSeamFaceEvidence
-  , constrainedSeamSourceFace
-  , constrainedSeamTargetFace
-  , constrainedSeamFaceFirstPoint
-  , constrainedSeamFaceSecondPoint
-  , constrainedSeamFaceThirdPoint
-  , ConstrainedSeamResult
-  , constrainedSeamResultTriangulation
-  , constrainedSeamLeftFaceCount
-  , constrainedSeamRightFaceEvidence
-  , constrainedSeamJoinFaces
-  , constrainedSeamLeftConstraintCount
-  , constrainedSeamConstraintStats
-  , constrainedSeamBuildStats
-  , constrainedSeamPublicationStats
-  , constrainedSeamCachedFrontierPointReads
-  , ConstraintSplitBatchResult
-  , CdtBuildResult (..)
-  , constrainedDelaunay
-  , constrainedDelaunayMaximal
-  , fromDelaunay
-  , constraintEdges
-  , CanonicalSegment
-  , segmentStart
-  , segmentEnd
-  , ConstraintConflict
-  , conflictFirstSegment
-  , conflictSecondSegment
-  , ConstrainedUnionError (..)
-  , constraintSegments
-  , unionConstrainedWith
-  , unionConstrained
-  , joinSeparatedConstrained
-  , extendConstrainedWith
-  , constraintStorageBytes
-  , existsConstraint
-  , canAddConstraint
-  , intersectsConstraint
-  , getConflictingEdgesBetweenPoints
-  , getConflictingEdgesBetweenVertices
-  , recoverConstraints
-  , addConstraintEdge
-  , addConstraintEdges
-  , addConstraintAndSplit
-  , addConstraintsAndSplit
-  , removeConstraintEdge
-  , outerRegionFaces
-  , boundedRegionFaces
-  ) where
-
-import Moonlight.Triangulation.Internal.Cdt.Batch (recoverConstraints)
-import Moonlight.Triangulation.Internal.Cdt.Build
-  ( constrainedDelaunay
-  , constrainedDelaunayMaximal
-  , fromDelaunay
-  )
-import Moonlight.Triangulation.Internal.Cdt.Query
-  ( canAddConstraint
-  , constraintEdges
-  , constraintStorageBytes
-  , existsConstraint
-  , getConflictingEdgesBetweenPoints
-  , getConflictingEdgesBetweenVertices
-  , intersectsConstraint
-  )
-import Moonlight.Triangulation.Internal.Cdt.Region
-  ( boundedRegionFaces
-  , outerRegionFaces
-  )
-import Moonlight.Triangulation.Internal.Cdt.Segment
-  ( addConstraintEdge
-  , addConstraintEdges
-  , removeConstraintEdge
-  )
-import Moonlight.Triangulation.Internal.Cdt.Split
-  ( addConstraintAndSplit
-  , addConstraintsAndSplit
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types
-  ( CanonicalSegment (..)
-  , CdtBuildResult (..)
-  , CdtError (..)
-  , ConstrainedUnionError (..)
-  , ConstraintBatchResult (..)
-  , ConstraintBatchStats (..)
-  , ConstrainedExtensionResult (..)
-  , ConstrainedSeamSide (..)
-  , ConstrainedSeamFaceEvidence (..)
-  , ConstrainedSeamResult (..)
-  , ConstraintConflict (..)
-  , ConstraintOutcome (..)
-  , ConstraintRecoveryResult (..)
-  , ConstraintResult
-  , ConstraintSplitBatchResult
-  , CorridorObstruction (..)
-  )
-import Moonlight.Triangulation.Internal.Cdt.Union
-  ( constraintSegments
-  , extendConstrainedWith
-  , joinSeparatedConstrained
-  , unionConstrained
-  , unionConstrainedWith
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( ConstrainedDelaunayTriangulation
-  )
diff --git a/src-build/Moonlight/Triangulation/Insertion.hs b/src-build/Moonlight/Triangulation/Insertion.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Insertion.hs
+++ /dev/null
@@ -1,215 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
-module Moonlight.Triangulation.Insertion
-  ( insertExistingVertex
-  , insertExistingVertexWithHint
-  , insertPointCombining
-  , insertAdmittedPointCombining
-  , insertVertexAtPoint
-  , insertExistingVertexAtLocation
-  ) where
-
-import Control.Monad.ST (ST)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.Chain
-  ( extendLine
-  , lineToArea
-  , setupFirstVertex
-  , setupSecondVertex
-  , splitLineEdge
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Hull (insertOutsideHull)
-import Moonlight.Triangulation.Internal.DcelOperations.Subdivide
-  ( insertIntoFace
-  , insertOnEdge
-  )
-import Moonlight.Triangulation.Internal.Location
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  )
-import Moonlight.Triangulation.Internal.Probe (KnownProbe, Probe (..))
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Math (validatePoint)
-import Moonlight.Triangulation.Types
-
--- | Admit a raw site, then delegate to the shared admitted-site interpreter.
--- Geometry decides whether a site is new; callers choose only the annotation
--- law for an occupied site. Keeping raw admission and the common location,
--- placement, and counters in this one owner prevents sessions and constrained
--- extension from drifting into two insertion semantics merely because they own
--- different enclosing transactions.
-insertPointCombining
-  :: (vertex -> vertex -> vertex)
-  -> Maybe Int
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Point
-  -> vertex
-  -> ST s (Either BuildError (Int, InsertionDisposition))
-insertPointCombining combine seed mutable operation point payload =
-  case validatePoint Nothing point of
-    Left failure -> pure (Left failure)
-    Right admitted ->
-      insertAdmittedPointCombining combine seed mutable operation admitted payload
-{-# INLINE insertPointCombining #-}
-
--- | Insert an already admitted site. The caller has retained the canonical
--- coordinate section, so this interpreter only locates and glues the payload;
--- it must not reopen coordinate admission.
-insertAdmittedPointCombining
-  :: (vertex -> vertex -> vertex)
-  -> Maybe Int
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> QueryPoint
-  -> vertex
-  -> ST s (Either BuildError (Int, InsertionDisposition))
-insertAdmittedPointCombining combine seed mutable operation queryPoint payload = do
-  addCounter operation CounterInputPoints 1
-  outcome <-
-    insertVertexAtPoint
-      @'ProbeOff
-      mutable
-      operation
-      seed
-      (queryPointValue queryPoint)
-      payload
-  case outcome of
-    Left failure -> pure (Left failure)
-    Right resolved@(vertex, disposition) -> do
-      case disposition of
-        AlreadyPresent -> do
-          resident <- vertexDataAt mutable vertex
-          writeVertexData mutable vertex (combine resident payload)
-          addCounter operation CounterExistingPoints 1
-          addCounter operation CounterDuplicatePoints 1
-        Inserted -> addCounter operation CounterUniquePoints 1
-      pure (Right resolved)
-{-# INLINE insertAdmittedPointCombining #-}
-
-insertExistingVertex
-  :: forall p s vertex directed undirected face
-   . KnownProbe p
-  => MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError ())
-insertExistingVertex mutable operation = insertExistingVertexWithHint @p mutable operation Nothing
-
--- | Insert a materialized vertex while beginning point location from a face
--- already known to be geometrically adjacent to the request. The hint changes
--- only the amount of walking; the located site remains authoritative.
-insertExistingVertexWithHint
-  :: forall p s vertex directed undirected face
-   . KnownProbe p
-  => MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Int
-  -> ST s (Either BuildError ())
-insertExistingVertexWithHint mutable operation hint vertex = do
-  query <- pointAt mutable vertex
-  located <- locateMutable mutable operation hint query
-  case located of
-    Left obstruction -> pure (Left obstruction)
-    Right site -> insertExistingVertexAtLocation @p mutable operation vertex site
-
--- | Locate before materializing a point, so duplicate detection remains a
--- topological fact rather than a resident coordinate index.
-insertVertexAtPoint
-  :: forall p s vertex directed undirected face
-   . KnownProbe p
-  => MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> vertex
-  -> ST s (Either BuildError (Int, InsertionDisposition))
-insertVertexAtPoint mutable operation hint point vertexData = do
-  located <- locateMutable mutable operation hint point
-  case located of
-    Left obstruction -> pure (Left obstruction)
-    Right (MutableOnVertex existing) -> pure (Right (existing, AlreadyPresent))
-    Right site -> do
-      capacity <- ensurePointCapacity mutable 1
-      case capacity of
-        Left obstruction -> pure (Left obstruction)
-        Right () -> do
-          vertex <- appendVertex mutable point vertexData
-          inserted <- insertExistingVertexAtLocation @p mutable operation vertex site
-          pure ((vertex, Inserted) <$ inserted)
-
--- | Interpret a point-location result without locating the same point again.
--- Callers may hold this witness only while no topology mutation intervenes.
---
--- The site decides which counts are consulted, and the vertex's own point is
--- read only by the two strata that compare against it — the degenerate line and
--- the failure report. The area strata already stand on a located site and would
--- otherwise rebuild a point the locate stage was handed.
-insertExistingVertexAtLocation
-  :: forall p s vertex directed undirected face
-   . KnownProbe p
-  => MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> MutableLocation
-  -> ST s (Either BuildError ())
-insertExistingVertexAtLocation mutable operation vertex located =
-  case located of
-    MutableOnVertex existing ->
-      pure
-        ( Left
-            ( FreshInsertionMatchedExistingVertex
-                (VertexId (fromIntegral vertex))
-                (VertexId (fromIntegral existing))
-            )
-        )
-    MutableEmpty -> do
-      connected <- connectedCount mutable
-      if connected == 0
-        then setupFirstVertex mutable vertex >> pure (Right ())
-        else locationFailed
-    MutableOnEdge edge -> do
-      faces <- faceCount mutable
-      if faces <= 1
-        then splitLineEdge mutable operation edge vertex
-        else insertOnEdge @p mutable operation edge vertex
-    MutableInFace face -> do
-      faces <- faceCount mutable
-      if faces <= 1
-        then locationFailed
-        else insertIntoFace @p mutable operation face vertex
-    MutableOutsideHull edge -> do
-      connected <- connectedCount mutable
-      if connected == 1
-        then setupSecondVertex mutable vertex
-        else do
-          faces <- faceCount mutable
-          if faces <= 1
-            then extendDegenerateLine edge
-            else insertOutsideHull @p mutable operation edge vertex
- where
-  extendDegenerateLine edge = do
-    from <- edgeOriginPoint mutable edge
-    to <- edgeOriginPoint mutable (edge `xorInt` 1)
-    query <- pointAt mutable vertex
-    if orient2d from to query == EQ
-      then do
-        endpoint <- readOrigin mutable edge
-        extendLine mutable operation endpoint vertex
-      else lineToArea @p mutable operation vertex
-
-  locationFailed = do
-    query <- pointAt mutable vertex
-    pure (Left (PointLocationFailed query))
-
-xorInt :: Int -> Int -> Int
-xorInt value 1 = if even value then value + 1 else value - 1
-xorInt value _ = value
-{-# INLINE xorInt #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/BulkLoad.hs b/src-build/Moonlight/Triangulation/Internal/BulkLoad.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/BulkLoad.hs
+++ /dev/null
@@ -1,947 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | Generation. @delaunay@ builds a mesh from a whole site set by circle sweep;
--- the insertion verbs extend an existing mesh one site at a time.
-module Moonlight.Triangulation.Internal.BulkLoad
-  ( empty
-  , clear
-  , delaunay
-  , delaunayGeometry
-  , DuplicatePayloadPolicy (..)
-  , delaunayFromCoordinates
-  , delaunayFromAdmittedDistinctAscending
-  , insert
-  , insertAt
-  , insertMany
-  ) where
-
-import Control.Monad (forM_)
-import Control.Monad.ST (ST, runST)
-import qualified Data.IntSet as IntSet
-import Data.Primitive.PrimArray
-  ( MutablePrimArray
-  , newPrimArray
-  , readPrimArray
-  , unsafeFreezePrimArray
-  , writePrimArray
-  )
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Dcel (numInnerFaces, numVertices)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( VertexId (..)
-  , directedEdgeIdIndex
-  , faceIdIndex
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedFromVector, boxedUpdate, emptyBoxedPaged)
-import Moonlight.Triangulation.Internal.Capacity (ensureCapacity)
-import Moonlight.Triangulation.Insertion (insertExistingVertexAtLocation, insertVertexAtPoint)
-import Moonlight.Triangulation.Internal.Location (MutableLocation (..))
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , freezeBuildStats
-  , newOperationState
-  , setCounter
-  )
-import Moonlight.Triangulation.Internal.CircleSweep
-  ( RadiallyOrderedArena
-  , circleSweepInsert
-  , radiallyOrderArena
-  , radiallyOrderGeometry
-  )
-import Moonlight.Triangulation.Internal.PointIndex
-  ( MutablePointIndex
-  , emptyPointIndex
-  , newMutablePointIndex
-  , resolveMutablePoint
-  , seedMutablePointIndex
-  )
-import Moonlight.Triangulation.Math (canonicalCoordinate, validatePoint)
-import Moonlight.Triangulation.PointLocation (locatePointWithHint)
-import Moonlight.Triangulation.Internal.Probe (Probe (..))
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction), emptyPaged, fromVector)
-import Moonlight.Triangulation.Internal.Transaction (runTransaction)
-import Moonlight.Triangulation.Types
-
--- | The vertexless triangulation: the outer face and nothing else. This is the
--- canonical origin of the type — bulk loading, incremental insertion and
--- refinement all agree with growing this value.
-empty
-  :: ElementDefaults directed undirected face
-  -> Triangulation mode vertex directed undirected face
-empty defaults@ElementDefaults{defaultDirectedEdgeData, defaultUndirectedEdgeData, defaultFaceData} =
-  Triangulation
-    { triPointX = emptyPaged
-    , triPointY = emptyPaged
-    , triPointIndex = emptyPointIndex
-    , triVertexOut = emptyPaged
-    , triVertexData = emptyBoxedPaged Nothing
-    , triHalfTopology = emptyPaged
-    , triDirectedData = emptyBoxedPaged (Just defaultDirectedEdgeData)
-    , triUndirectedData = emptyBoxedPaged (Just defaultUndirectedEdgeData)
-    , triFaceEdge = fromVector noIndex (U.singleton noIndex)
-    , triFaceData = boxedFromVector (Just defaultFaceData) (V.singleton defaultFaceData)
-    , triConstraint = emptyPaged
-    , triConstraintCount = 0
-    , triConstraintEdges = IntSet.empty
-    , triSeamFrontier = Nothing
-    , triElementDefaults = defaults
-    }
-
--- | Discard every vertex while retaining the element defaults the
--- triangulation was built with.
-clear
-  :: Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face
-clear = empty . triElementDefaults
-
--- | How a canonical bulk source combines payloads whose exact coordinates
--- coincide. Geometry identity is settled independently by the point index.
-data DuplicatePayloadPolicy vertex
-  = KeepFirstPayload
-  | CombineDuplicatePayload !(vertex -> vertex -> vertex)
-
--- | The two lawful ingress sections for the one bulk construction engine.
--- Raw callers need coordinate admission and duplicate classification; an exact
--- SiteSet has already established canonical, distinct, lexicographically
--- ascending 'QueryPoint's. The latter remains package-internal through the
--- private build component, rather than becoming a second constructor.
-data BulkInput vertex
-  = RawPossiblyDuplicate
-      !(Int -> Point)
-      !(DuplicatePayloadPolicy vertex)
-  | AdmittedDistinctAscending !(Int -> QueryPoint)
-
--- | The local identity verdict for one input position. A fresh verdict carries
--- the canonical coordinates already admitted to the mutable DCEL, so ingress
--- never rereads its own writes merely to accumulate the sweep centre.
-data PositionClaim
-  = ResidentPosition !Int
-  | FreshPosition !Int !Double !Double
-
--- | Build a finite Delaunay DCEL while preserving the first input payload at
--- every duplicate position. The returned mapping relates every input slot to
--- the canonical stored vertex.
-delaunay
-  :: forall vertex directed undirected face
-   . HasPosition vertex
-  => ElementDefaults directed undirected face
-  -> V.Vector vertex
-  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
-delaunay defaults input =
-  buildDelaunayFromSource
-    defaults
-    (V.length input)
-    (RawPossiblyDuplicate (position . (input V.!)) KeepFirstPayload)
-    (input V.!)
-
--- | Build the geometry-only Delaunay triangulation of a coordinate vector.
--- Exact duplicate positions collapse to one site. Use 'delaunay' or
--- 'delaunayFromCoordinates' when vertex annotations or the input-to-vertex
--- mapping are part of the result.
-delaunayGeometry
-  :: V.Vector Point
-  -> Either BuildError (DelaunayTriangulation ())
-delaunayGeometry coordinates
-  | inputCount == 0 = Right (empty unitElementDefaults)
-  | otherwise = do
-    V.iforM_ coordinates (\index point -> () <$ validatePoint (Just index) point)
-    ensureCapacity inputCount
-    let !canonicalArena =
-          U.generate inputCount $ \index ->
-            case coordinates V.! index of
-              Point rawX rawY ->
-                ( 0
-                , canonicalCoordinate rawX
-                , canonicalCoordinate rawY
-                , fromIntegral index
-                )
-        (!inputSumX, !inputSumY) =
-          U.foldl'
-            (\(!sumX, !sumY) (_, x, y, _) -> (sumX + x, sumY + y))
-            (0, 0)
-            canonicalArena
-    runST $ do
-      inputArena <- U.unsafeThaw canonicalArena
-      let !inputScale = recip (fromIntegral inputCount)
-          !inputCenterX = inputSumX * inputScale
-          !inputCenterY = inputSumY * inputScale
-          (distanceArena, pointXMutable, pointYMutable, inputOrder) =
-            MUV.unzip4 inputArena
-      assignGeometryRadialDistances
-        distanceArena
-        pointXMutable
-        pointYMutable
-        inputCenterX
-        inputCenterY
-      distances <- U.unsafeFreeze distanceArena
-      pointXs <- U.unsafeFreeze pointXMutable
-      pointYs <- U.unsafeFreeze pointYMutable
-      orderedInputArena <-
-        radiallyOrderGeometry distances pointXs pointYs inputOrder
-      defaultedOutcome <-
-        newMutableDcelWithVertexDefault
-          ()
-          unitElementDefaults
-          (planarDcelCapacity inputCount)
-      case defaultedOutcome of
-        Left failure -> pure (Left failure)
-        Right defaultedMutable -> do
-          let !mutable = defaultedVertexDcel defaultedMutable
-              !dense = defaultedDenseVertexDcel defaultedMutable
-          operation <- newOperationState (halfEdgeCapacity mutable)
-          unique <- appendSortedGeometry defaultedMutable pointXs pointYs inputOrder
-          orderedArena <-
-            if unique == inputCount
-              then pure orderedInputArena
-              else do
-                let !scale = recip (fromIntegral unique)
-                    !uniqueOrder = MUV.unsafeSlice 0 unique inputOrder
-                (sumX, sumY) <- sumGeometryVertices dense uniqueOrder
-                uniqueArena <-
-                  fillRadialArena
-                    mutable
-                    (sumX * scale)
-                    (sumY * scale)
-                    (MUV.unsafeRead uniqueOrder)
-                    unique
-                radiallyOrderArena uniqueArena
-          inserted <- circleSweepInsert mutable operation orderedArena
-          case inserted of
-            Left failure -> pure (Left failure)
-            Right _ -> freezeTriangulation mutable
- where
-  !inputCount = V.length coordinates
-
-  assignGeometryRadialDistances
-    :: forall s
-     . MUV.MVector s Double
-    -> MUV.MVector s Double
-    -> MUV.MVector s Double
-    -> Double
-    -> Double
-    -> ST s ()
-  assignGeometryRadialDistances distances pointXs pointYs centerX centerY =
-    MUV.imapM_
-      (\index _ -> do
-        x <- MUV.unsafeRead pointXs index
-        y <- MUV.unsafeRead pointYs index
-        let !deltaX = centerX - x
-            !deltaY = centerY - y
-        MUV.unsafeWrite distances index (deltaX * deltaX + deltaY * deltaY)
-      )
-      distances
-
-  appendSortedGeometry
-    :: forall s
-     . DefaultedVertexDcel s () () () ()
-    -> U.Vector Double
-    -> U.Vector Double
-    -> MUV.MVector s Word32
-    -> ST s Int
-  appendSortedGeometry defaultedMutable pointXs pointYs order
-    | inputCount == 0 = pure 0
-    | otherwise = do
-        firstInput <- MUV.unsafeRead order 0
-        let !x = U.unsafeIndex pointXs (fromIntegral firstInput)
-            !y = U.unsafeIndex pointYs (fromIntegral firstInput)
-        denseWriteFreshDefaultPoint defaultedMutable 0 x y
-        MUV.unsafeWrite order 0 0
-        (_, _, unique) <-
-          MUV.foldM'
-            (\(!previousX, !previousY, !uniqueCount) nextInput ->
-               let !nextX = U.unsafeIndex pointXs (fromIntegral nextInput)
-                   !nextY = U.unsafeIndex pointYs (fromIntegral nextInput)
-                in if nextX == previousX && nextY == previousY
-                     then pure (previousX, previousY, uniqueCount)
-                     else do
-                       denseWriteFreshDefaultPoint defaultedMutable uniqueCount nextX nextY
-                       MUV.unsafeWrite order uniqueCount (fromIntegral uniqueCount)
-                       pure (nextX, nextY, uniqueCount + 1)
-            )
-            (x, y, 1)
-            (MUV.unsafeSlice 1 (inputCount - 1) order)
-        unique <$ denseCommitFreshDefaultPoints defaultedMutable unique
-
-  sumGeometryVertices
-    :: forall s
-     . DenseMutableDcel s () () () ()
-    -> MUV.MVector s Word32
-    -> ST s (Double, Double)
-  sumGeometryVertices dense vertices =
-    MUV.foldM'
-      (\(!sumX, !sumY) raw -> do
-         x <- denseReadPointX dense (unpackIndex raw)
-         y <- denseReadPointY dense (unpackIndex raw)
-         pure (sumX + x, sumY + y)
-      )
-      (0, 0)
-      vertices
-
--- | Canonical construction from separate geometry and annotation sources.
--- The coordinate vector remains the only geometry in ingress; payloads never
--- acquire a fabricated 'HasPosition' instance merely to reach the loader.
-delaunayFromCoordinates
-  :: forall vertex directed undirected face
-   . ElementDefaults directed undirected face
-  -> V.Vector (Point)
-  -> V.Vector vertex
-  -> DuplicatePayloadPolicy vertex
-  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
-delaunayFromCoordinates defaults coordinates payloads duplicatePolicy
-  | coordinateCount /= payloadCount =
-      Left (CoordinatePayloadCountMismatch coordinateCount payloadCount)
-  | otherwise =
-      buildDelaunayFromSource
-        defaults
-        coordinateCount
-        (RawPossiblyDuplicate (coordinates V.!) duplicatePolicy)
-        (payloads V.!)
- where
-  !coordinateCount = V.length coordinates
-  !payloadCount = V.length payloads
-
--- | Construction from an already-admitted exact site section. The caller
--- supplies Map-ascending, distinct 'QueryPoint's; this skips only raw
--- admission, duplicate classification, and unique-site recentering. It still
--- derives the radial execution order and runs the one circle sweep.
-delaunayFromAdmittedDistinctAscending
-  :: forall vertex directed undirected face
-   . ElementDefaults directed undirected face
-  -> V.Vector (QueryPoint, vertex)
-  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
-delaunayFromAdmittedDistinctAscending defaults input =
-  buildDelaunayFromSource
-    defaults
-    (V.length input)
-    (AdmittedDistinctAscending (\index -> fst (input V.! index)))
-    (\index -> snd (input V.! index))
-{-# INLINE delaunayFromAdmittedDistinctAscending #-}
-
-buildDelaunayFromSource
-  :: forall vertex directed undirected face
-   . ElementDefaults directed undirected face
-  -> Int
-  -> BulkInput vertex
-  -> (Int -> vertex)
-  -> Either BuildError (BuildResult 'Unconstrained vertex directed undirected face)
-buildDelaunayFromSource defaults inputCount input payloadAtInput = do
-  ensureCapacity inputCount
-  runST $ do
-    inputArena <- MUV.new inputCount
-    admitted <- initializeInputArena inputArena
-    case admitted of
-      Left failure -> pure (Left failure)
-      Right (inputSumX, inputSumY) -> do
-        mutable <- newMutableDcel defaults (planarDcelCapacity inputCount)
-        operation <- newOperationState (halfEdgeCapacity mutable)
-        (mapping, unique, orderedArena) <-
-          case input of
-            RawPossiblyDuplicate pointAtInput duplicatePolicy ->
-              prepareRawInput
-                inputArena
-                mutable
-                pointAtInput
-                duplicatePolicy
-                inputSumX
-                inputSumY
-            AdmittedDistinctAscending _ ->
-              prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY
-        setCounter operation CounterInputPoints inputCount
-        setCounter operation CounterUniquePoints unique
-        setCounter operation CounterDuplicatePoints (inputCount - unique)
-        inserted <-
-          if unique == 0
-            then pure (Right 0)
-            else circleSweepInsert mutable operation orderedArena
-        case inserted of
-          Left failure -> pure (Left failure)
-          Right seedCount -> do
-            setCounter operation CounterSpatialSeedPoints seedCount
-            frozenOutcome <- freezeTriangulation mutable
-            case frozenOutcome of
-              Left failure -> pure (Left failure)
-              Right frozen -> do
-                mapped <- unsafeFreezePrimArray mapping
-                stats <- freezeBuildStats operation
-                pure
-                  ( Right
-                      BuildResult
-                        { buildTriangulation = frozen
-                        , buildInputVertices = mapped
-                        , buildStats = stats
-                        }
-                  )
- where
-  -- Raw sources validate and canonicalize at this one ingress. Admitted
-  -- sources merely project their retained QueryPoint evidence into the sweep
-  -- arena; neither route reconstructs coordinate identity downstream.
-  initializeInputArena
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> ST s (Either BuildError (Double, Double))
-  initializeInputArena arena =
-    case input of
-      RawPossiblyDuplicate pointAtInput _ ->
-        initializeRawInputArena arena pointAtInput 0 0 0
-      AdmittedDistinctAscending pointAtInput ->
-        initializeAdmittedInputArena arena pointAtInput
-
-  initializeRawInputArena
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> (Int -> Point)
-    -> Int
-    -> Double
-    -> Double
-    -> ST s (Either BuildError (Double, Double))
-  initializeRawInputArena arena pointAtInput !index !sumX !sumY
-    | index >= inputCount = pure (Right (sumX, sumY))
-    | otherwise =
-        case validatePoint (Just index) (pointAtInput index) of
-          Left failure -> pure (Left failure)
-          Right admitted ->
-            case queryPointValue admitted of
-              Point x y -> do
-                MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
-                initializeRawInputArena
-                  arena
-                  pointAtInput
-                  (index + 1)
-                  (sumX + x)
-                  (sumY + y)
-
-  initializeAdmittedInputArena
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> (Int -> QueryPoint)
-    -> ST s (Either BuildError (Double, Double))
-  initializeAdmittedInputArena arena pointAtInput =
-    Right
-      <$> MUV.ifoldM'
-        (\(!sumX, !sumY) index _ ->
-           case queryPointValue (pointAtInput index) of
-             Point x y -> do
-               MUV.unsafeWrite arena index (0, x, y, fromIntegral index)
-               pure (sumX + x, sumY + y)
-        )
-        (0, 0)
-        arena
-
-  prepareRawInput
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> MutableDcel s vertex directed undirected face
-    -> (Int -> Point)
-    -> DuplicatePayloadPolicy vertex
-    -> Double
-    -> Double
-    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
-  prepareRawInput inputArena mutable pointAtInput duplicatePolicy inputSumX inputSumY = do
-    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
-        !inputCenterX = inputSumX * inputScale
-        !inputCenterY = inputSumY * inputScale
-    assignRadialDistances inputArena inputCenterX inputCenterY 0
-    orderedInputArena <- radiallyOrderArena inputArena
-    mapping <- newPrimArray inputCount
-    unique <- classifySortedInputs mapping inputArena
-    (sumX, sumY) <-
-      appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy 0 0 0
-    orderedArena <-
-      if unique == inputCount
-        then pure orderedInputArena
-        else do
-          let !scale = recip (fromIntegral unique)
-              !uniqueArena = MUV.unsafeSlice 0 unique inputArena
-          rewriteCompactedArena
-            mapping
-            (sumX * scale)
-            (sumY * scale)
-            uniqueArena
-            0
-          radiallyOrderArena uniqueArena
-    pure (mapping, unique, orderedArena)
-
-  prepareAdmittedDistinctInput
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> MutableDcel s vertex directed undirected face
-    -> Double
-    -> Double
-    -> ST s (MutablePrimArray s Word32, Int, RadiallyOrderedArena s)
-  prepareAdmittedDistinctInput inputArena mutable inputSumX inputSumY = do
-    mapping <- newPrimArray inputCount
-    appendAdmittedDistinctInputs mutable mapping inputArena
-    let !inputScale = if inputCount == 0 then 0 else recip (fromIntegral inputCount)
-        !inputCenterX = inputSumX * inputScale
-        !inputCenterY = inputSumY * inputScale
-    assignRadialDistances inputArena inputCenterX inputCenterY 0
-    orderedArena <- radiallyOrderArena inputArena
-    pure (mapping, inputCount, orderedArena)
-
-  -- An admitted SiteSet is already distinct. Sequential append therefore
-  -- transports the identity class map directly into authoritative vertex ids,
-  -- without reopening duplicate policy or recentering a compacted arena.
-  appendAdmittedDistinctInputs
-    :: forall s
-     . MutableDcel s vertex directed undirected face
-    -> MutablePrimArray s Word32
-    -> MUV.MVector s (Double, Double, Double, Word32)
-    -> ST s ()
-  appendAdmittedDistinctInputs mutable mapping arena =
-    MUV.imapM_
-      (\index (_, x, y, _) -> do
-         vertex <- appendVertexCoordinates mutable x y (payloadAtInput index)
-         writePrimArray mapping index (fromIntegral vertex)
-         MUV.unsafeWrite arena index (0, x, y, fromIntegral vertex)
-      )
-      arena
-
-  -- The all-input centre seeds the first radial ordering. Duplicate
-  -- equivalence classes become adjacent in that total order; after descent,
-  -- the compacted section is recentered over unique sites and ordered once
-  -- more only when required.
-  assignRadialDistances
-    :: forall s
-     . MUV.MVector s (Double, Double, Double, Word32)
-    -> Double
-    -> Double
-    -> Int
-    -> ST s ()
-  assignRadialDistances arena centerX centerY !index
-    | index >= inputCount = pure ()
-    | otherwise = do
-        (_, x, y, inputOrdinal) <- MUV.unsafeRead arena index
-        let !deltaX = centerX - x
-            !deltaY = centerY - y
-        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, inputOrdinal)
-        assignRadialDistances arena centerX centerY (index + 1)
-
-  -- Classify the sorted local sections by exact canonical position. The
-  -- mapping first names each class by its earliest input slot. Compacting the
-  -- unique radial representatives in place cannot overwrite an unread slot.
-  classifySortedInputs
-    :: forall s
-     . MutablePrimArray s Word32
-    -> MUV.MVector s (Double, Double, Double, Word32)
-    -> ST s Int
-  classifySortedInputs mapping arena
-    | inputCount == 0 = pure 0
-    | otherwise = do
-        first@(_, firstX, firstY, firstInput) <- MUV.unsafeRead arena 0
-        writePrimArray mapping (fromIntegral firstInput) firstInput
-        classifyFrom first firstX firstY firstInput 1 1
-   where
-    classifyFrom
-      :: (Double, Double, Double, Word32)
-      -> Double
-      -> Double
-      -> Word32
-      -> Int
-      -> Int
-      -> ST s Int
-    classifyFrom !_ !previousX !previousY !classInput !readIndex !uniqueCount
-      | readIndex >= inputCount = pure uniqueCount
-      | otherwise = do
-          record@(_, x, y, inputOrdinal) <- MUV.unsafeRead arena readIndex
-          if x == previousX && y == previousY
-            then do
-              writePrimArray mapping (fromIntegral inputOrdinal) classInput
-              classifyFrom record previousX previousY classInput (readIndex + 1) uniqueCount
-            else do
-              writePrimArray mapping (fromIntegral inputOrdinal) inputOrdinal
-              if uniqueCount == readIndex
-                then pure ()
-                else MUV.unsafeWrite arena uniqueCount record
-              classifyFrom record x y inputOrdinal (readIndex + 1) (uniqueCount + 1)
-
-  -- Materialize in original order, preserving the established handle
-  -- assignment and duplicate-payload law. The equivalence mapping for a
-  -- duplicate always points backward to an already materialized class owner.
-  appendClassifiedInputs
-    :: forall s
-     . MutableDcel s vertex directed undirected face
-    -> MutablePrimArray s Word32
-    -> (Int -> Point)
-    -> DuplicatePayloadPolicy vertex
-    -> Int
-    -> Double
-    -> Double
-    -> ST s (Double, Double)
-  appendClassifiedInputs mutable mapping pointAtInput duplicatePolicy !index !sumX !sumY
-    | index >= inputCount = pure (sumX, sumY)
-    | otherwise = do
-        classInput <- readPrimArray mapping index
-        let !vertexData = payloadAtInput index
-        if fromIntegral classInput == index
-          then case pointAtInput index of
-            Point x y -> do
-              let !canonicalX = canonicalCoordinate x
-                  !canonicalY = canonicalCoordinate y
-              vertex <- appendVertexCoordinates mutable canonicalX canonicalY vertexData
-              writePrimArray mapping index (fromIntegral vertex)
-              appendClassifiedInputs
-                mutable
-                mapping
-                pointAtInput
-                duplicatePolicy
-                (index + 1)
-                (sumX + canonicalX)
-                (sumY + canonicalY)
-          else do
-            resident <- fromIntegral <$> readPrimArray mapping (fromIntegral classInput)
-            writePrimArray mapping index (fromIntegral resident)
-            case duplicatePolicy of
-              KeepFirstPayload -> pure ()
-              CombineDuplicatePayload combine -> do
-                residentData <- vertexDataAt mutable resident
-                writeVertexData mutable resident (combine residentData vertexData)
-            appendClassifiedInputs
-              mutable
-              mapping
-              pointAtInput
-              duplicatePolicy
-              (index + 1)
-              sumX
-              sumY
-
-  -- Once duplicates have shortened the vertex arena, translate each compacted
-  -- representative from its input-class name to its authoritative vertex and
-  -- restate its radial key around the exact unique-site centre.
-  rewriteCompactedArena
-    :: forall s
-     . MutablePrimArray s Word32
-    -> Double
-    -> Double
-    -> MUV.MVector s (Double, Double, Double, Word32)
-    -> Int
-    -> ST s ()
-  rewriteCompactedArena mapping centerX centerY arena !index
-    | index >= MUV.length arena = pure ()
-    | otherwise = do
-        (_, x, y, classInput) <- MUV.unsafeRead arena index
-        vertex <- readPrimArray mapping (fromIntegral classInput)
-        let !deltaX = centerX - x
-            !deltaY = centerY - y
-        MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, x, y, vertex)
-        rewriteCompactedArena mapping centerX centerY arena (index + 1)
-
--- | Insert or replace a vertex payload. A payload at an existing position is
--- overwritten without changing topology.
---
--- The published mesh is independent of the one passed in. A singleton below
--- ten thousand resident sites copies densely; larger bases publish through
--- copy-on-write pages. A caller inserting a sequence wants one
--- 'Moonlight.Triangulation.Session.withSession' over
--- 'Moonlight.Triangulation.Session.insertVertex' instead — see 'insertAt'.
-insert
-  :: HasPosition vertex
-  => Triangulation mode vertex directed undirected face
-  -> vertex
-  -> Either BuildError (InsertionResult mode vertex directed undirected face)
-insert triangulation vertexData = insertAt triangulation (position vertexData) vertexData
-
--- | Insert at a stated point. 'insert' is this with the point read out of the
--- payload, which is what a caller holding only a payload wants; a caller that
--- computed the point — a constraint split, a Steiner refinement — wants to say
--- so rather than build a payload and hope the round trip through 'HasPosition'
--- returns what it started with.
---
--- This is one shaped transaction over a single insertion. Replacing a fold of
--- it with one session is sound because the two agree on every mesh and differ
--- only in how many intermediate meshes they publish. A fold publishes @k@
--- meshes and pays a thaw for each, so it runs in Θ(n·k); the session pays one
--- thaw and runs in O(k·log n) expected.
-insertAt
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> vertex
-  -> Either BuildError (InsertionResult mode vertex directed undirected face)
-insertAt triangulation rawPoint vertexData = do
-  queryPoint <- validatePoint Nothing rawPoint
-  case locatePointWithHint triangulation Nothing queryPoint of
-    (OnVertex resident, walked) ->
-      Right (replaceResidentPayload triangulation resident walked vertexData)
-    (located, walked) -> do
-      let transactionShape =
-            if numVertices triangulation < 10_000
-              then DenseTransaction
-              else LocalTransaction
-      ((vertex, disposition), frozen, stats) <-
-        runTransaction
-          id
-          transactionShape
-          triangulation
-          1
-          (\mutable operation -> do
-             addCounter operation CounterInputPoints 1
-             inserted <-
-               case located of
-                 -- A frozen degenerate-line location points at an arbitrary visible
-                 -- segment, while the line extension interpreter requires a terminal
-                 -- edge. The frozen section carries no terminal witness, so retain
-                 -- the existing mutable line locator for this one non-lawful case.
-                 OutsideConvexHull (Just _)
-                   | numInnerFaces triangulation == 0 ->
-                       insertVertexAtPoint @'ProbeOff mutable operation Nothing (queryPointValue queryPoint) vertexData
-                 _ -> do
-                   capacityOutcome <- ensurePointCapacity mutable 1
-                   case capacityOutcome of
-                     Left failure -> pure (Left failure)
-                     Right () -> do
-                       vertex <- appendVertex mutable (queryPointValue queryPoint) vertexData
-                       let thawedLocation =
-                             case located of
-                               EmptyTriangulation -> MutableEmpty
-                               OnEdge edge -> MutableOnEdge (directedEdgeIdIndex edge)
-                               InFace face -> MutableInFace (faceIdIndex face)
-                               -- The frozen locator emits no edge only for a
-                               -- singleton mesh. Its mutable interpreter ignores
-                               -- this sentinel while constructing the second vertex.
-                               OutsideConvexHull Nothing -> MutableOutsideHull 0
-                               OutsideConvexHull (Just edge) -> MutableOutsideHull (directedEdgeIdIndex edge)
-                       ((vertex, Inserted) <$) <$> insertExistingVertexAtLocation @'ProbeOff mutable operation vertex thawedLocation
-             case inserted of
-               Left failure -> pure (Left failure)
-               Right (vertex, disposition) -> do
-                 case disposition of
-                   Inserted -> addCounter operation CounterUniquePoints 1
-                   AlreadyPresent -> do
-                     writeVertexData mutable vertex vertexData
-                     addCounter operation CounterExistingPoints 1
-                     addCounter operation CounterDuplicatePoints 1
-                 pure (Right (vertex, disposition))
-          )
-      pure
-        InsertionResult
-          { insertionTriangulation = frozen
-          , insertionVertex = VertexId (fromIntegral vertex)
-          , insertionDisposition = disposition
-          , insertionStats = withFrozenLocationStats walked stats
-          }
-
--- | Publish a payload replacement without opening a transaction.
---
--- A position already resident changes exactly one thing: the payload slot the
--- vertex already occupies. No coordinate, no half-edge, no constraint flag and
--- no face record differs, so the five unboxed planes are the ones the argument
--- already holds rather than copies taken out of it — which is what a thaw costs
--- and what this exists to refuse. They are immutable values; nothing reached
--- from here is a mutable buffer, and 'boxedUpdate' materializes a fresh page
--- for the one it rewrites, leaving the argument's own directory intact.
---
--- The location counters are the frozen walk's, not a thawed walk's. They
--- describe the walk that actually ran.
-replaceResidentPayload
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> LocationStats
-  -> vertex
-  -> InsertionResult mode vertex directed undirected face
-replaceResidentPayload triangulation resident walked vertexData =
-  InsertionResult
-    { insertionTriangulation =
-        triangulation
-          { triVertexData =
-              boxedUpdate (vertexIdIndex resident) vertexData (triVertexData triangulation)
-          }
-    , insertionVertex = resident
-    , insertionDisposition = AlreadyPresent
-    , insertionStats =
-        withFrozenLocationStats
-          walked
-          emptyBuildStats
-            { statInputPoints = 1
-            , statExistingPoints = 1
-            , statDuplicatePoints = 1
-            }
-    }
-
--- | Add the frozen locator's observation to the local topology interpreter's
--- operation-owned counters. Direct frozen-site insertion contributes no mutable
--- walk; the degenerate fallback contributes its real mutable walk rather than
--- having it erased from the published result.
-withFrozenLocationStats :: LocationStats -> BuildStats -> BuildStats
-withFrozenLocationStats walked stats =
-  stats
-    { statLocationWalkSteps = locationWalkSteps walked + statLocationWalkSteps stats
-    , statLocationMaxWalk = max (locationWalkSteps walked) (statLocationMaxWalk stats)
-    , statLocationFallbacks = (if locationUsedFallback walked then 1 else 0) + statLocationFallbacks stats
-    }
-
--- | Apply a batch in one page transaction. Duplicate positions are processed
--- in input order, so their last payload wins exactly as repeated 'insert'
--- calls would, while topology is inserted only once per new position.
-insertMany
-  :: forall mode vertex directed undirected face
-   . HasPosition vertex
-  => Triangulation mode vertex directed undirected face
-  -> V.Vector vertex
-  -> Either BuildError (BuildResult mode vertex directed undirected face)
-insertMany triangulation input = do
-  validateVertices input
-  (mapped, frozen, stats) <-
-    runTransaction id DenseTransaction triangulation (V.length input) $ \mutable operation -> do
-      table <- newMutablePointIndex (pointCapacity mutable)
-      seeded <- seedPointTable mutable table
-      case seeded of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          mapping <- newPrimArray (V.length input)
-          freshBuffer <- MUV.new (V.length input)
-          filled <- fill mutable operation table mapping freshBuffer 0 0 0 0
-          case filled of
-            Left failure -> pure (Left failure)
-            Right (sumX, sumY, freshCount) -> do
-              let !existingCount = V.length input - freshCount
-              setCounter operation CounterInputPoints (V.length input)
-              setCounter operation CounterUniquePoints freshCount
-              setCounter operation CounterExistingPoints existingCount
-              setCounter operation CounterDuplicatePoints existingCount
-              inserted <-
-                if freshCount == 0
-                  then pure (Right 0)
-                  else do
-                    let !scale = recip (fromIntegral freshCount)
-                    arena <-
-                      fillRadialArena
-                        mutable
-                        (sumX * scale)
-                        (sumY * scale)
-                        (MUV.unsafeRead freshBuffer)
-                        freshCount
-                    orderedArena <- radiallyOrderArena arena
-                    circleSweepInsert mutable operation orderedArena
-              case inserted of
-                Left failure -> pure (Left failure)
-                Right seedCount -> do
-                  setCounter operation CounterSpatialSeedPoints seedCount
-                  Right <$> unsafeFreezePrimArray mapping
-  pure
-    BuildResult
-      { buildTriangulation = frozen
-      , buildInputVertices = mapped
-      , buildStats = stats
-      }
- where
-  fill
-    :: forall s
-     . MutableDcel s vertex directed undirected face
-    -> OperationState s
-    -> MutablePointIndex s
-    -> MutablePrimArray s Word32
-    -> MUV.MVector s Word32
-    -> Int
-    -> Double
-    -> Double
-    -> Int
-    -> ST s (Either BuildError (Double, Double, Int))
-  fill mutable operation table mapping freshBuffer !index !sumX !sumY !freshCount
-    | index >= V.length input = pure (Right (sumX, sumY, freshCount))
-    | otherwise = do
-        let !vertexData = input V.! index
-        claimed <- claimPosition mutable table (position vertexData) vertexData
-        case claimed of
-          Left failure -> pure (Left failure)
-          Right (FreshPosition vertex canonicalX canonicalY) -> do
-            writePrimArray mapping index (fromIntegral vertex)
-            MUV.unsafeWrite freshBuffer freshCount (fromIntegral vertex)
-            fill
-              mutable
-              operation
-              table
-              mapping
-              freshBuffer
-              (index + 1)
-              (sumX + canonicalX)
-              (sumY + canonicalY)
-              (freshCount + 1)
-          Right (ResidentPosition vertex) -> do
-            writePrimArray mapping index (fromIntegral vertex)
-            writeVertexData mutable vertex vertexData
-            fill mutable operation table mapping freshBuffer (index + 1) sumX sumY freshCount
-
--- | One packed radial record per swept vertex — the derived sort fields and
--- the vertex handle, nothing else — filled straight from the coordinate
--- arenas and consumed in place by the sweep. The squared distance is stated
--- against the ingress-accumulated centre, in the widened comparison format.
-fillRadialArena
-  :: MutableDcel s vertex directed undirected face
-  -> Double
-  -> Double
-  -> (Int -> ST s Word32)
-  -> Int
-  -> ST s (MUV.MVector s (Double, Double, Double, Word32))
-fillRadialArena mutable centerX centerY lookupId count = do
-  arena <- MUV.new count
-  forM_ [0 .. count - 1] $ \index -> do
-    raw <- lookupId index
-    x <- readPointX mutable (unpackIndex raw)
-    y <- readPointY mutable (unpackIndex raw)
-    let !wideX = x
-        !wideY = y
-        !deltaX = centerX - wideX
-        !deltaY = centerY - wideY
-    MUV.unsafeWrite arena index (deltaX * deltaX + deltaY * deltaY, wideX, wideY, raw)
-  pure arena
-
--- | Claim a position for the vertex the arena would append next, or answer the
--- vertex already holding it. The claim is written before the append, so the two
--- must stay adjacent: nothing may consume a vertex slot in between.
-claimPosition
-  :: MutableDcel s vertex directed undirected face
-  -> MutablePointIndex s
-  -> Point
-  -> vertex
-  -> ST s (Either BuildError PositionClaim)
-claimPosition mutable table rawPoint vertexData =
-  case rawPoint of
-    Point x y -> do
-      let !canonicalX = canonicalCoordinate x
-          !canonicalY = canonicalCoordinate y
-      slot <- nextVertexSlot mutable
-      let !candidate = nextVertexSlotIndex slot
-      owner <-
-        resolveMutablePoint
-          table
-          (readPointX mutable)
-          (readPointY mutable)
-          canonicalX
-          canonicalY
-          candidate
-      case owner of
-        Left failure -> pure (Left failure)
-        Right (Just existing) -> pure (Right (ResidentPosition existing))
-        Right Nothing -> do
-          vertex <- appendVertexCoordinatesAtSlot mutable slot canonicalX canonicalY vertexData
-          pure (Right (FreshPosition vertex canonicalX canonicalY))
--- | Enter the positions a batch inherits from the triangulation it extends, so
--- that an input repeating one of them maps to the vertex already there.
-seedPointTable
-  :: MutableDcel s vertex directed undirected face
-  -> MutablePointIndex s
-  -> ST s (Either BuildError ())
-seedPointTable mutable table = do
-  existing <- pointCount mutable
-  seedMutablePointIndex
-    table
-    existing
-    (readPointX mutable)
-    (readPointY mutable)
-
-validateVertices :: HasPosition vertex => V.Vector vertex -> Either BuildError ()
-validateVertices vertices =
-  V.iforM_ vertices (\index vertexData -> validatePoint (Just index) (position vertexData))
diff --git a/src-build/Moonlight/Triangulation/Internal/Capacity.hs b/src-build/Moonlight/Triangulation/Internal/Capacity.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Capacity.hs
+++ /dev/null
@@ -1,18 +0,0 @@
--- | Admissibility of a requested vertex count against the packed index limit.
-module Moonlight.Triangulation.Internal.Capacity
-  ( ensureCapacity
-  ) where
-
-import Moonlight.Triangulation.Internal.PackedIndex (indexLimit)
-import Moonlight.Triangulation.Internal.Types (BuildError (..))
-
-ensureCapacity :: Int -> Either BuildError ()
-ensureCapacity count
-  | count < 0 || count > maximumVertexCapacity = Left (CapacityExceeded count)
-  | otherwise = Right ()
-
--- Mutable allocation reserves @8n + 16@ directed-edge slots. Every topology
--- handle is packed below 'indexLimit', so admissibility is stated against that
--- actual representation bound rather than the machine 'Int' bound.
-maximumVertexCapacity :: Int
-maximumVertexCapacity = (indexLimit - 16) `quot` 8
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Admission.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Admission.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Admission.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Immutable admission of one requested segment against the sparse
--- constrained-edge section, decided without thawing topology.
-module Moonlight.Triangulation.Internal.Cdt.Admission
-  ( ConstraintAdmission (..)
-  , constraintAdmission
-  , segmentBoxesAreDisjoint
-  ) where
-
-import qualified Data.IntSet as IntSet
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Predicates (segmentsProperlyCross)
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-
--- | Immutable admission descends through the sparse constrained-edge section,
--- not through every ordinary edge in the requested corridor. Constraint
--- cardinality is the lawful index here: rejection depends only on a proper
--- crossing with an existing protected segment, while shared endpoints,
--- duplicate segments, and collinear overlap remain recoverable by the mutable
--- corridor algebra. Accepted singleton requests descend once more inside their
--- sealed transaction; the overwhelmingly common rejected request stops here
--- without thawing topology.
-data ConstraintAdmission
-  = ConstraintBlocked !UndirectedEdgeId
-  | ConstraintAdmitted
-
-constraintAdmission
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> ConstraintAdmission
-constraintAdmission triangulation from to =
-  IntSet.foldr firstBlocking ConstraintAdmitted (triConstraintEdges triangulation)
- where
-  !requestFrom = Dcel.vertexPoint triangulation from
-  !requestTo = Dcel.vertexPoint triangulation to
-
-  firstBlocking raw later =
-    let !edge = UndirectedEdgeId (fromIntegral raw)
-        (!edgeFromId, !edgeToId) = Dcel.undirectedEndpoints triangulation edge
-     in if
-          from == edgeFromId
-            || from == edgeToId
-            || to == edgeFromId
-            || to == edgeToId
-          then later
-          else
-            let !edgeFrom = Dcel.vertexPoint triangulation edgeFromId
-                !edgeTo = Dcel.vertexPoint triangulation edgeToId
-             in if segmentBoxesAreDisjoint requestFrom requestTo edgeFrom edgeTo
-                  then later
-                  else
-                    if segmentsProperlyCross requestFrom requestTo edgeFrom edgeTo
-                      then ConstraintBlocked edge
-                      else later
-
-segmentBoxesAreDisjoint
-  :: Point
-  -> Point
-  -> Point
-  -> Point
-  -> Bool
-segmentBoxesAreDisjoint (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
-  max ax bx < min cx dx
-    || max cx dx < min ax bx
-    || max ay by < min cy dy
-    || max cy dy < min ay by
-{-# INLINE segmentBoxesAreDisjoint #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Batch.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Batch.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Batch.hs
+++ /dev/null
@@ -1,166 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | The batch interpreter: many constraint requests in order inside one sealed
--- dense transaction, with rejection carried as a value.
-module Moonlight.Triangulation.Internal.Cdt.Batch
-  ( recoverConstraints
-  , recoverConstraintBatch
-  , finalizeConstraintBatch
-  , initialConstraintBatchStats
-  , interpretConstraintRequest
-  , interpretConstraintRequests
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Either (isRight)
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM)
-import Moonlight.Triangulation.Internal.Cdt.Query (validateEndpoints)
-import Moonlight.Triangulation.Internal.Cdt.Recovery (applyMutableConstraint)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  )
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( OperationState )
-import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction))
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Transaction (runTransaction)
-import Moonlight.Triangulation.Internal.Types (ConstraintMode (..))
-
--- | Interpret constraint requests in order inside one sealed mutable DCEL
--- transaction. Rejections are values because an earlier accepted request can
--- lawfully obstruct a later request; structural recovery failures remain typed
--- errors and prevent a partially rewritten mesh from escaping. The public batch
--- interpreter amortizes dense materialization; known singleton repair sites
--- reuse this algebra under the sparse persistent-page interpretation.
-recoverConstraints
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector (VertexId, VertexId)
-  -> Either (CdtError) (ConstraintBatchResult vertex directed undirected face)
-recoverConstraints triangulation requests
-  | V.null requests =
-      Right
-        ConstraintBatchResult
-          { constraintBatchTriangulation = triangulation
-          , constraintBatchOutcomes = V.empty
-          , constraintBatchStats = initialConstraintBatchStats 0
-          }
-  | otherwise =
-      recoverConstraintBatch
-        triangulation
-        requests
-
-recoverConstraintBatch
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector (VertexId, VertexId)
-  -> Either (CdtError) (ConstraintBatchResult vertex directed undirected face)
-recoverConstraintBatch triangulation requests = do
-  V.mapM_ (uncurry (validateEndpoints triangulation)) requests
-  (completed, frozen, _) <-
-    runTransaction
-      CdtBuildError
-      DenseTransaction
-      triangulation
-      0
-      (interpretConstraintRequests requests)
-  pure (finalizeConstraintBatch frozen completed)
-{-# INLINE recoverConstraintBatch #-}
-
--- | Interpret a complete constraint request section against an already-open
--- transaction. Both ordinary recovery and asymmetric constrained extension
--- use this one interpreter; callers decide only which request section is
--- resident before it begins.
-interpretConstraintRequests
-  :: V.Vector (VertexId, VertexId)
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> ST s (Either (CdtError) ConstraintBatchAccumulator)
-interpretConstraintRequests requests mutable operation = do
-  programWords <- newGrowableWord32 256
-  let initialAccumulator =
-        ConstraintBatchAccumulator
-          { accumulatedConstraintOutcomes = []
-          , accumulatedConstraintStats =
-              initialConstraintBatchStats (V.length requests)
-          }
-  foldWhileM
-    isRight
-    (interpretConstraintRequest programWords mutable operation)
-    (Right initialAccumulator)
-    requests
-{-# INLINE interpretConstraintRequests #-}
-
--- | Materialize the ordinary recovery receipt only after the enclosing
--- transaction has frozen. The mutable accumulator cannot escape as a partial
--- constrained mesh.
-finalizeConstraintBatch
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> ConstraintBatchAccumulator
-  -> ConstraintBatchResult vertex directed undirected face
-finalizeConstraintBatch frozen completed =
-  ConstraintBatchResult
-    { constraintBatchTriangulation = frozen
-    , constraintBatchOutcomes = V.fromList (reverse (accumulatedConstraintOutcomes completed))
-    , constraintBatchStats = accumulatedConstraintStats completed
-    }
-{-# INLINE finalizeConstraintBatch #-}
-
-initialConstraintBatchStats :: Int -> ConstraintBatchStats
-initialConstraintBatchStats requestCount =
-  ConstraintBatchStats
-    { constraintBatchRequests = requestCount
-    , constraintBatchAccepted = 0
-    , constraintBatchRejected = 0
-    , constraintBatchCorridors = 0
-    , constraintBatchReusedFaces = 0
-    , constraintBatchCrossedEdges = 0
-    }
-
-interpretConstraintRequest
-  :: GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Either (CdtError) ConstraintBatchAccumulator
-  -> (VertexId, VertexId)
-  -> ST s (Either (CdtError) ConstraintBatchAccumulator)
-interpretConstraintRequest _ _ _ rejected@(Left _) _ = pure rejected
-interpretConstraintRequest programWords mutable operation (Right accumulator) (from, to) = do
-  applied <- applyMutableConstraint programWords mutable operation from to
-  pure $ case applied of
-    Left obstruction -> Left obstruction
-    Right (MutableConstraintRejected blocking) ->
-      let previousStats = accumulatedConstraintStats accumulator
-       in Right
-            accumulator
-              { accumulatedConstraintOutcomes =
-                  ConstraintRejected blocking : accumulatedConstraintOutcomes accumulator
-              , accumulatedConstraintStats =
-                  previousStats
-                    { constraintBatchRejected = constraintBatchRejected previousStats + 1
-                    }
-              }
-    Right (MutableConstraintAccepted request) ->
-      let previousStats = accumulatedConstraintStats accumulator
-       in Right
-            accumulator
-              { accumulatedConstraintOutcomes =
-                  ConstraintAccepted
-                    (V.fromList (reverse (accumulatedRequestPath request)))
-                    (accumulatedRequestAddedEdges request)
-                    : accumulatedConstraintOutcomes accumulator
-              , accumulatedConstraintStats =
-                  previousStats
-                    { constraintBatchAccepted = constraintBatchAccepted previousStats + 1
-                    , constraintBatchCorridors =
-                        constraintBatchCorridors previousStats + accumulatedRequestCorridors request
-                    , constraintBatchReusedFaces =
-                        constraintBatchReusedFaces previousStats + accumulatedRequestReusedFaces request
-                    , constraintBatchCrossedEdges =
-                        constraintBatchCrossedEdges previousStats + accumulatedRequestCrossedEdges request
-                    }
-              }
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Build.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Build.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Build.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Constrained bulk loading: an unconstrained load promoted to the
--- constrained layer, then driven through the batch corridor interpreter.
-module Moonlight.Triangulation.Internal.Cdt.Build
-  ( fromDelaunay
-  , constrainedDelaunay
-  , constrainedDelaunayMaximal
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.Vector as V
-import Data.Primitive.PrimArray (PrimArray, indexPrimArray, sizeofPrimArray)
-import Data.Word (Word32)
-import Moonlight.Triangulation.BulkLoad (delaunay)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Cdt.Batch (recoverConstraints)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-
--- | Promote an unconstrained mesh into the constrained layer with no marked edges.
-fromDelaunay
-  :: Triangulation 'Unconstrained vertex directed undirected face
-  -> Triangulation 'Constrained vertex directed undirected face
-fromDelaunay = promoteConstrained
-
--- | Build a constrained triangulation, refusing the complete request when any
--- input constraint cannot be admitted.
-constrainedDelaunay
-  :: HasPosition vertex
-  => ElementDefaults directed undirected face
-  -> V.Vector vertex
-  -> V.Vector (Int, Int)
-  -> Either (CdtError) (BuildResult 'Constrained vertex directed undirected face)
-constrainedDelaunay defaults inputVertices constraints = do
-  result <- constrainedDelaunayMaximal defaults inputVertices constraints
-  if V.null (cdtRejectedConstraints result)
-    then Right (cdtAcceptedBuild result)
-    else Left (ConstraintInputConflicts (cdtRejectedConstraints result))
-
--- | Stable constrained bulk loading. Duplicate input vertices are rerouted to
--- the first surviving handle. Every proper constraint conflict is returned in
--- input order; accepted constraints remain in the result.
-constrainedDelaunayMaximal
-  :: HasPosition vertex
-  => ElementDefaults directed undirected face
-  -> V.Vector vertex
-  -> V.Vector (Int, Int)
-  -> Either (CdtError) (CdtBuildResult vertex directed undirected face)
-constrainedDelaunayMaximal defaults inputVertices constraints = do
-  built <- first CdtBuildError (delaunay defaults inputVertices)
-  let !mapping = buildInputVertices built
-      !initial = fromDelaunay (buildTriangulation built)
-  requests <- V.mapM (mapConstraintRequest mapping) constraints
-  batch <- recoverConstraints initial requests
-  let rejected =
-        V.mapMaybe
-          (\(request, outcome) ->
-            case outcome of
-              ConstraintAccepted _ _ -> Nothing
-              ConstraintRejected _ -> Just request
-          )
-          (V.zip constraints (constraintBatchOutcomes batch))
-  pure
-    CdtBuildResult
-      { cdtAcceptedBuild =
-          BuildResult
-            { buildTriangulation = constraintBatchTriangulation batch
-            , buildInputVertices = mapping
-            , buildStats = buildStats built
-            }
-      , cdtRejectedConstraints = rejected
-      }
- where
-  mapConstraintRequest
-    :: PrimArray Word32
-    -> (Int, Int)
-    -> Either (CdtError) (VertexId, VertexId)
-  mapConstraintRequest mapping (fromIndex, toIndex) =
-    (,)
-      <$> mapConstraintEndpoint mapping fromIndex
-      <*> mapConstraintEndpoint mapping toIndex
-
-  mapConstraintEndpoint
-    :: PrimArray Word32
-    -> Int
-    -> Either (CdtError) VertexId
-  mapConstraintEndpoint mapping endpointIndex
-    | endpointIndex >= 0 && endpointIndex < sizeofPrimArray mapping =
-        Right (VertexId (indexPrimArray mapping endpointIndex))
-    | otherwise =
-        Left (ConstraintEndpointIndexOutOfRange endpointIndex (sizeofPrimArray mapping))
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Combinators.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Combinators.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Combinators.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Bounded monadic folding, refusable transaction sequencing, and the handle
--- and error coercions the constrained layer shares.
-module Moonlight.Triangulation.Internal.Cdt.Combinators
-  ( foldWhileM
-  , bindMutable
-  , asConstraintStep
-  ) where
-
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError (..))
-import Moonlight.Triangulation.Internal.Types (BuildError)
-
--- | Monadic left fold whose continuation is supplied by a lazy right fold.
--- The state predicate decides descent before the next effect is constructed,
--- so graph walks stop at their authoritative local answer without mutable loop
--- control or traversing the unused safety suffix.
-foldWhileM
-  :: (Foldable container, Monad monad)
-  => (state -> Bool)
-  -> (state -> item -> monad state)
-  -> state
-  -> container item
-  -> monad state
-foldWhileM shouldContinue step initial items =
-  foldr
-    (\item continuation state ->
-      if shouldContinue state
-        then step state item >>= continuation
-        else pure state
-    )
-    pure
-    items
-    initial
-{-# INLINE foldWhileM #-}
-
--- | Sequence two refusable transaction steps. Refusal short-circuits, so a
--- transaction that abandons never reaches its publication; writing the bind
--- once is what keeps the constraint verbs from nesting their case analysis
--- five deep.
-bindMutable :: Monad monad => monad (Either failure a) -> (a -> monad (Either failure b)) -> monad (Either failure b)
-bindMutable step continue = do
-  outcome <- step
-  case outcome of
-    Left failure -> pure (Left failure)
-    Right value -> continue value
-{-# INLINE bindMutable #-}
-
--- | Relabel a step whose refusal is a build failure, so it composes with the
--- constraint layer's own.
-asConstraintStep :: Functor f => f (Either BuildError a) -> f (Either (CdtError) a)
-asConstraintStep = fmap (either (Left . CdtBuildError) Right)
-{-# INLINE asConstraintStep #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor.hs
+++ /dev/null
@@ -1,281 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | The corridor walk: one traversal of the requested segment that yields
--- either the first oriented blocking witness or a complete recovery program.
-module Moonlight.Triangulation.Internal.Cdt.Corridor
-  ( constraintWorkspaceFor
-  , scanMutableConstraint
-  , advanceMutablePlan
-  , continueMutablePlan
-  , beginMutableRecover
-  , finishMutableRecover
-  , settleMutableProgram
-  , mutablePlanIsActive
-  , writeConstraintProgram
-  , readConstraintProgram
-  , existingProgramTag
-  , recoverProgramTag
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.IntersectionIterator (Intersection (..))
-import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM)
-import Moonlight.Triangulation.Internal.Cdt.Corridor.Trace (nextMutableIntersection)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , clearGrowable
-  , readGrowable
-  , writeGrowable
-  )
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.Types (Point)
-
--- | The walk budget bounds a corridor through the mesh as it stands, so it is
--- taken per request rather than per transaction: a verb that inserts before it
--- recovers has changed the mesh the budget describes. Recovery itself adds
--- neither vertex nor edge, so a batch reads the same number every time.
-constraintWorkspaceFor
-  :: GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> ST s (ConstraintWorkspace s)
-constraintWorkspaceFor constraintProgramWords mutable = do
-  halfEdges <- directedEdgeCount mutable
-  vertices <- pointCount mutable
-  pure
-    ConstraintWorkspace
-      { constraintProgramWords
-      , constraintWalkBudget = 2 * halfEdges + vertices + 8
-      }
-
-writeConstraintProgram :: ConstraintWorkspace s -> Int -> Int -> ST s ()
-writeConstraintProgram ConstraintWorkspace{constraintProgramWords} index value =
-  writeGrowable constraintProgramWords index (fromIntegral value)
-{-# INLINE writeConstraintProgram #-}
-
-readConstraintProgram :: ConstraintWorkspace s -> Int -> ST s Int
-readConstraintProgram ConstraintWorkspace{constraintProgramWords} index =
-  fromIntegral <$> readGrowable constraintProgramWords index
-{-# INLINE readConstraintProgram #-}
-
--- | Walk one corridor once, gluing the local observations into either the
--- first oriented blocking witness or a complete recovery program. The former
--- admission/planning pair traversed every admitted corridor twice and could
--- disagree if either walk evolved independently; this scanner is their single
--- semantic owner.
-scanMutableConstraint
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> ST s (Either (CdtError) MutableConstraintScan)
-scanMutableConstraint workspace@ConstraintWorkspace{constraintProgramWords, constraintWalkBudget} mutable from to = do
-  clearGrowable constraintProgramWords
-  lineFrom <- pointAt mutable (vertexIdIndex from)
-  lineTo <- pointAt mutable (vertexIdIndex to)
-  initialCursor <- beginMutableRecover workspace 0 from 0 False
-  walked <-
-    foldWhileM
-      mutablePlanIsActive
-      (advanceMutablePlan workspace mutable lineFrom lineTo to)
-      (MutablePlanActive (VertexIntersection from) initialCursor)
-      [1 .. constraintWalkBudget]
-  pure $ case walked of
-    MutablePlanComplete plan -> Right (MutableConstraintScanAdmitted plan)
-    MutablePlanBlocked blocking -> Right (MutableConstraintScanBlocked blocking)
-    MutablePlanFailed obstruction -> Left (ConstraintCorridorObstructed obstruction)
-    MutablePlanActive _ _ ->
-      Left
-        ( ConstraintCorridorObstructed
-            (CorridorWalkDidNotTerminate constraintWalkBudget)
-        )
-
-advanceMutablePlan
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> VertexId
-  -> MutablePlanWalk
-  -> Int
-  -> ST s MutablePlanWalk
-advanceMutablePlan _ _ _ _ _ complete@(MutablePlanComplete _) _ = pure complete
-advanceMutablePlan _ _ _ _ _ blocked@(MutablePlanBlocked _) _ = pure blocked
-advanceMutablePlan _ _ _ _ _ failed@(MutablePlanFailed _) _ = pure failed
-advanceMutablePlan workspace mutable lineFrom lineTo target (MutablePlanActive event cursor) _ =
-  case event of
-    EdgeIntersection directed -> do
-      constrained <- readConstraint mutable (directedEdgeIdIndex directed)
-      if constrained
-        then pure (MutablePlanBlocked directed)
-        else do
-          writeConstraintProgram workspace (mutableCursorWriteAt cursor) (directedEdgeIdIndex directed)
-          continueMutablePlan
-            workspace
-            mutable
-            lineFrom
-            lineTo
-            target
-            event
-            cursor
-              { mutableCursorWriteAt = mutableCursorWriteAt cursor + 1
-              , mutableCursorConflictCount = mutableCursorConflictCount cursor + 1
-              , mutableCursorAfterOverlap = False
-              }
-    VertexIntersection vertex
-      | mutableCursorAfterOverlap cursor ->
-          continueMutablePlan
-            workspace
-            mutable
-            lineFrom
-            lineTo
-            target
-            event
-            cursor{mutableCursorAfterOverlap = False}
-      | vertex == mutableCursorAt cursor ->
-          continueMutablePlan
-            workspace
-            mutable
-            lineFrom
-            lineTo
-            target
-            event
-            cursor{mutableCursorAfterOverlap = False}
-      | otherwise -> do
-          finished <- finishMutableRecover workspace vertex cursor
-          nextCursor <-
-            beginMutableRecover
-              workspace
-              (mutableCursorWriteAt finished)
-              vertex
-              (mutableCursorPieceCount finished)
-              False
-          continueMutablePlan workspace mutable lineFrom lineTo target event nextCursor
-    EdgeOverlap rawDirected -> do
-      let rawEdge = directedEdgeIdIndex rawDirected
-      rawOrigin <- readOrigin mutable rawEdge
-      rawDestination <- readOrigin mutable (rawEdge `xor` 1)
-      let current = vertexIdIndex (mutableCursorAt cursor)
-          oriented
-            | rawOrigin == current = Just rawEdge
-            | rawDestination == current = Just (rawEdge `xor` 1)
-            | otherwise = Nothing
-      case oriented of
-        Nothing ->
-          pure
-            ( MutablePlanFailed
-                ( CorridorBoundaryMissing
-                    (mutableCursorAt cursor)
-                    (VertexId (fromIntegral rawOrigin))
-                )
-            )
-        Just edge -> do
-          prefix <-
-            if mutableCursorConflictCount cursor == 0
-              then pure cursor
-              else finishMutableRecover workspace (mutableCursorAt cursor) cursor
-          let !existingAt =
-                if mutableCursorConflictCount cursor == 0
-                  then mutableCursorHeader cursor
-                  else mutableCursorWriteAt prefix
-              !edgeDestination =
-                VertexId
-                  ( fromIntegral
-                      (if edge == rawEdge then rawDestination else rawOrigin)
-                  )
-              !pieceCount = mutableCursorPieceCount prefix + 1
-              !nextHeader = existingAt + 2
-          writeConstraintProgram workspace existingAt existingProgramTag
-          writeConstraintProgram workspace (existingAt + 1) edge
-          nextCursor <-
-            beginMutableRecover
-              workspace
-              nextHeader
-              edgeDestination
-              pieceCount
-              True
-          continueMutablePlan workspace mutable lineFrom lineTo target event nextCursor
-
-continueMutablePlan
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> VertexId
-  -> Intersection
-  -> MutableProgramCursor
-  -> ST s MutablePlanWalk
-continueMutablePlan workspace mutable lineFrom lineTo target event cursor = do
-  following <- nextMutableIntersection mutable lineFrom lineTo event
-  case following of
-    Just nextEvent -> pure (MutablePlanActive nextEvent cursor)
-    Nothing -> MutablePlanComplete <$> settleMutableProgram workspace target cursor
-
-beginMutableRecover
-  :: ConstraintWorkspace s
-  -> Int
-  -> VertexId
-  -> Int
-  -> Bool
-  -> ST s MutableProgramCursor
-beginMutableRecover workspace header from pieceCount afterOverlap = do
-  writeConstraintProgram workspace header recoverProgramTag
-  writeConstraintProgram workspace (header + 1) (vertexIdIndex from)
-  writeConstraintProgram workspace (header + 2) 0
-  writeConstraintProgram workspace (header + 3) 0
-  pure
-    MutableProgramCursor
-      { mutableCursorAt = from
-      , mutableCursorHeader = header
-      , mutableCursorWriteAt = header + 4
-      , mutableCursorConflictCount = 0
-      , mutableCursorPieceCount = pieceCount
-      , mutableCursorAfterOverlap = afterOverlap
-      }
-
-finishMutableRecover
-  :: ConstraintWorkspace s
-  -> VertexId
-  -> MutableProgramCursor
-  -> ST s MutableProgramCursor
-finishMutableRecover workspace to cursor = do
-  writeConstraintProgram workspace (mutableCursorHeader cursor + 2) (vertexIdIndex to)
-  writeConstraintProgram workspace (mutableCursorHeader cursor + 3) (mutableCursorConflictCount cursor)
-  pure
-    cursor
-      { mutableCursorAt = to
-      , mutableCursorPieceCount = mutableCursorPieceCount cursor + 1
-      }
-
-settleMutableProgram
-  :: ConstraintWorkspace s
-  -> VertexId
-  -> MutableProgramCursor
-  -> ST s MutableConstraintProgram
-settleMutableProgram workspace target cursor
-  | mutableCursorAt cursor == target =
-      pure
-        MutableConstraintProgram
-          { mutableProgramWordCount = mutableCursorHeader cursor
-          , mutableProgramPieceCount = mutableCursorPieceCount cursor
-          }
-  | otherwise = do
-      finished <- finishMutableRecover workspace target cursor
-      pure
-        MutableConstraintProgram
-          { mutableProgramWordCount = mutableCursorWriteAt finished
-          , mutableProgramPieceCount = mutableCursorPieceCount finished
-          }
-
-existingProgramTag :: Int
-existingProgramTag = 0
-
-recoverProgramTag :: Int
-recoverProgramTag = 1
-
-mutablePlanIsActive :: MutablePlanWalk -> Bool
-mutablePlanIsActive (MutablePlanActive _ _) = True
-mutablePlanIsActive _ = False
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor/Trace.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor/Trace.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Corridor/Trace.hs
+++ /dev/null
@@ -1,240 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Local direction tracing inside the thawed mesh: where a directed line
--- leaves the vertex or edge it currently stands on.
-module Moonlight.Triangulation.Internal.Cdt.Corridor.Trace
-  ( MutableVertexOut (..)
-  , MutableVertexTrace (..)
-  , MutableEdgeOut (..)
-  , nextMutableIntersection
-  , traceMutableDirectionOutOfVertex
-  , advanceMutableVertexTrace
-  , traceMutableDirectionOutOfEdge
-  , mutableEdgeIntersectsNonCollinear
-  , mutableVertexTraceIsSearching
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.IntersectionIterator
-import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM)
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.Predicates
-  ( onClosedSegment
-  , orient2d
-  , orient2dCoordinates
-  )
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Math (projectionFactor)
-
-data MutableVertexOut
-  = MutableVertexOutHull
-  | MutableVertexOutOverlap {-# UNPACK #-} !Int
-  | MutableVertexOutEdge {-# UNPACK #-} !Int
-
-data MutableVertexTrace
-  = MutableVertexTraceSearching {-# UNPACK #-} !Int !Ordering
-  | MutableVertexTraceComplete !MutableVertexOut
-
-data MutableEdgeOut
-  = MutableEdgeOutHull
-  | MutableEdgeOutVertex {-# UNPACK #-} !Int
-  | MutableEdgeOutEdge {-# UNPACK #-} !Int
-  | MutableEdgeOutNone
-
-nextMutableIntersection
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> Intersection
-  -> ST s (Maybe Intersection)
-nextMutableIntersection mutable lineFrom lineTo current =
-  case current of
-    EdgeIntersection directed -> do
-      edgeOut <- traceMutableDirectionOutOfEdge mutable (directedEdgeIdIndex directed) lineFrom lineTo
-      pure $ case edgeOut of
-        MutableEdgeOutHull -> Nothing
-        MutableEdgeOutVertex vertex ->
-          Just (VertexIntersection (VertexId (fromIntegral vertex)))
-        MutableEdgeOutEdge edge ->
-          Just (EdgeIntersection (DirectedEdgeId (fromIntegral edge)))
-        MutableEdgeOutNone -> Nothing
-    VertexIntersection vertex -> do
-      currentPoint <- pointAt mutable (vertexIdIndex vertex)
-      if currentPoint == lineTo
-        then pure Nothing
-        else do
-          vertexOut <- traceMutableDirectionOutOfVertex mutable (vertexIdIndex vertex) lineTo
-          case vertexOut of
-            MutableVertexOutHull -> pure Nothing
-            MutableVertexOutOverlap edge ->
-              pure (Just (EdgeOverlap (DirectedEdgeId (fromIntegral edge))))
-            MutableVertexOutEdge edge -> do
-              edgeFrom <- edgeOriginPoint mutable edge
-              edgeTo <- edgeOriginPoint mutable (edge `xor` 1)
-              pure
-                ( if orient2d edgeFrom edgeTo lineTo == LT
-                    then Nothing
-                    else Just (EdgeIntersection (DirectedEdgeId (fromIntegral edge)))
-                )
-    EdgeOverlap directed
-      | lineFrom == lineTo -> pure Nothing
-      | otherwise -> do
-          destination <- readOrigin mutable (directedEdgeIdIndex directed `xor` 1)
-          destinationPoint <- pointAt mutable destination
-          pure
-            ( if onClosedSegment lineFrom lineTo destinationPoint
-                then Just (VertexIntersection (VertexId (fromIntegral destination)))
-                else Nothing
-            )
-
-traceMutableDirectionOutOfVertex
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Point
-  -> ST s MutableVertexOut
-traceMutableDirectionOutOfVertex mutable vertex target = do
-  start <- readVertexOut mutable vertex
-  if start < 0
-    then pure MutableVertexOutHull
-    else do
-      currentPoint <- pointAt mutable vertex
-      startTarget <- edgeOriginPoint mutable (start `xor` 1)
-      halfEdges <- directedEdgeCount mutable
-      let startSide = orient2d currentPoint startTarget target
-          rotateCounterClockwise = startSide == GT
-      traced <-
-        foldWhileM
-          mutableVertexTraceIsSearching
-          (advanceMutableVertexTrace mutable currentPoint target rotateCounterClockwise)
-          (MutableVertexTraceSearching start startSide)
-          [0 .. halfEdges]
-      pure $ case traced of
-        MutableVertexTraceComplete result -> result
-        MutableVertexTraceSearching _ _ -> MutableVertexOutHull
-
-advanceMutableVertexTrace
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> Bool
-  -> MutableVertexTrace
-  -> Int
-  -> ST s MutableVertexTrace
-advanceMutableVertexTrace _ _ _ _ complete@(MutableVertexTraceComplete _) _ = pure complete
-advanceMutableVertexTrace mutable currentPoint target rotateCounterClockwise (MutableVertexTraceSearching current currentSide) _ = do
-  currentTarget <- edgeOriginPoint mutable (current `xor` 1)
-  if currentSide == EQ && projectionFactor currentPoint currentTarget target >= 0
-    then pure (MutableVertexTraceComplete (MutableVertexOutOverlap current))
-    else do
-      following <-
-        if rotateCounterClockwise
-          then (`xor` 1) <$> readPrevious mutable current
-          else readNext mutable (current `xor` 1)
-      followingTarget <- edgeOriginPoint mutable (following `xor` 1)
-      let followingSide = orient2d currentPoint followingTarget target
-      if followingSide == EQ && projectionFactor currentPoint followingTarget target >= 0
-        then pure (MutableVertexTraceComplete (MutableVertexOutOverlap following))
-        else do
-          faceBetween <-
-            readFace mutable (if rotateCounterClockwise then current else following)
-          if faceBetween == 0
-            then pure (MutableVertexTraceComplete MutableVertexOutHull)
-            else
-              if rotateCounterClockwise == (followingSide == LT)
-                then do
-                  segment <-
-                    if rotateCounterClockwise
-                      then readNext mutable current
-                      else readPrevious mutable (current `xor` 1)
-                  pure
-                    ( MutableVertexTraceComplete
-                        (MutableVertexOutEdge (segment `xor` 1))
-                    )
-                else pure (MutableVertexTraceSearching following followingSide)
-
-traceMutableDirectionOutOfEdge
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Point
-  -> Point
-  -> ST s MutableEdgeOut
-traceMutableDirectionOutOfEdge mutable edge lineFrom lineTo = do
-  incident <- readFace mutable edge
-  if incident == 0
-    then pure MutableEdgeOutHull
-    else do
-      edgePrevious <- readPrevious mutable edge
-      edgeNext <- readNext mutable edge
-      edgeOrigin <- edgeOriginPoint mutable edge
-      oppositeVertex <- edgeOriginPoint mutable edgePrevious
-      let !originSide = orient2d lineFrom lineTo edgeOrigin
-          !oppositeSide = orient2d lineFrom lineTo oppositeVertex
-      if originSide == EQ || oppositeSide == EQ
-        then classifyDegenerate edgePrevious edgeNext
-        else
-          if originSide == oppositeSide
-            then do
-              nextOrigin <- edgeOriginPoint mutable edgeNext
-              let !outgoing = edgeNext `xor` 1
-                  !targetSide = orient2d oppositeVertex nextOrigin lineTo
-              pure
-                ( if targetSide == LT
-                    then MutableEdgeOutNone
-                    else MutableEdgeOutEdge outgoing
-                )
-            else do
-              let !outgoing = edgePrevious `xor` 1
-                  !targetSide = orient2d edgeOrigin oppositeVertex lineTo
-              pure
-                ( if targetSide == LT
-                    then MutableEdgeOutNone
-                    else MutableEdgeOutEdge outgoing
-                )
- where
-  classifyDegenerate edgePrevious edgeNext = do
-    previousIntersects <- mutableEdgeIntersectsNonCollinear mutable lineFrom lineTo edgePrevious
-    nextIntersects <- mutableEdgeIntersectsNonCollinear mutable lineFrom lineTo edgeNext
-    case (previousIntersects, nextIntersects) of
-      (True, False) -> pure (MutableEdgeOutEdge (edgePrevious `xor` 1))
-      (False, True) -> pure (MutableEdgeOutEdge (edgeNext `xor` 1))
-      (True, True) -> MutableEdgeOutVertex <$> readOrigin mutable edgePrevious
-      (False, False) -> pure MutableEdgeOutNone
-
-mutableEdgeIntersectsNonCollinear
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> Int
-  -> ST s Bool
-mutableEdgeIntersectsNonCollinear
-  mutable
-  (Point lineFromX lineFromY)
-  (Point lineToX lineToY)
-  edge = do
-    edgeFromVertex <- readOrigin mutable edge
-    edgeToVertex <- readOrigin mutable (edge `xor` 1)
-    Point edgeFromX edgeFromY <- pointAt mutable edgeFromVertex
-    Point edgeToX edgeToY <- pointAt mutable edgeToVertex
-    let !lineFromSide =
-          orient2dCoordinates
-            edgeFromX edgeFromY edgeToX edgeToY lineFromX lineFromY
-        !lineToSide =
-          orient2dCoordinates
-            edgeFromX edgeFromY edgeToX edgeToY lineToX lineToY
-        !edgeFromSide =
-          orient2dCoordinates
-            lineFromX lineFromY lineToX lineToY edgeFromX edgeFromY
-        !edgeToSide =
-          orient2dCoordinates
-            lineFromX lineFromY lineToX lineToY edgeToX edgeToY
-    pure
-      ( lineFromSide /= lineToSide
-          && edgeFromSide /= edgeToSide
-      )
-
-mutableVertexTraceIsSearching :: MutableVertexTrace -> Bool
-mutableVertexTraceIsSearching (MutableVertexTraceSearching _ _) = True
-mutableVertexTraceIsSearching _ = False
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Query.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Query.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Query.hs
+++ /dev/null
@@ -1,156 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Read-only interrogation of the constrained layer, over both the published
--- mesh and the thawed one, plus the endpoint validity the verbs share.
-module Moonlight.Triangulation.Internal.Cdt.Query
-  ( constraintEdges
-  , constraintStorageBytes
-  , existsConstraint
-  , canAddConstraint
-  , intersectsConstraint
-  , getConflictingEdgesBetweenPoints
-  , getConflictingEdgesBetweenVertices
-  , findDirectedEdge
-  , findMutableEdge
-  , validateEndpoints
-  , validVertex
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import qualified Data.IntSet as IntSet
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.IntersectionIterator
-import Moonlight.Triangulation.Internal.Cdt.Admission
-  ( ConstraintAdmission (..)
-  , constraintAdmission
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError (..))
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.Paged (pagedLength)
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-
--- | Marked undirected edges in ascending identifier order.
-constraintEdges
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> [UndirectedEdgeId]
-constraintEdges =
-  fmap (UndirectedEdgeId . fromIntegral)
-    . IntSet.toAscList
-    . triConstraintEdges
-{-# INLINE constraintEdges #-}
-
--- | Bytes occupied by the dense constraint marker plane.
-constraintStorageBytes
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> Integer
-constraintStorageBytes = toInteger . pagedLength . triConstraint
-
--- | Whether a marked edge directly joins two admitted vertices.
-existsConstraint
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> Bool
-existsConstraint triangulation from to =
-  case findDirectedEdge triangulation from to of
-    Just edge -> Dcel.isConstraintEdge triangulation (asUndirected edge)
-    Nothing -> False
-
--- | Whether a segment can be admitted without crossing a resident constraint.
-canAddConstraint
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> Bool
-canAddConstraint triangulation from to =
-  from /= to
-    && validVertex triangulation from
-    && validVertex triangulation to
-    && case constraintAdmission triangulation from to of
-      ConstraintBlocked _ -> False
-      ConstraintAdmitted -> True
-
--- | Whether a query segment properly crosses a resident constraint.
-intersectsConstraint
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> QueryPoint
-  -> QueryPoint
-  -> Bool
-intersectsConstraint triangulation from to =
-  case foldCorridorBetweenPoints triangulation from to firstBlocking () of
-    Just (Left ()) -> True
-    Just (Right ()) -> False
-    Nothing -> not (null (getConflictingEdgesBetweenPoints triangulation from to))
- where
-  firstBlocking :: () -> Intersection -> Either () ()
-  firstBlocking _ (EdgeIntersection edge)
-    | Dcel.isConstraintEdge triangulation (asUndirected edge) = Left ()
-  firstBlocking _ _ = Right ()
-
--- | Resident constrained edges properly crossed by a query segment.
-getConflictingEdgesBetweenPoints
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> QueryPoint
-  -> QueryPoint
-  -> [DirectedEdgeId]
-getConflictingEdgesBetweenPoints triangulation from to =
-  [ edge
-  | EdgeIntersection edge <- lineIntersections triangulation from to
-  , Dcel.isConstraintEdge triangulation (asUndirected edge)
-  ]
-
--- | Resident constrained edges crossed between two admitted vertices.
-getConflictingEdgesBetweenVertices
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> [DirectedEdgeId]
-getConflictingEdgesBetweenVertices triangulation from to
-  | not (validVertex triangulation from && validVertex triangulation to) = []
-  | otherwise =
-      [ edge
-      | EdgeIntersection edge <- lineIntersectionsBetweenVertices triangulation from to
-      , Dcel.isConstraintEdge triangulation (asUndirected edge)
-      ]
-
--- | Find an oriented edge joining two admitted vertices.
-findDirectedEdge :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> VertexId -> Maybe DirectedEdgeId
-findDirectedEdge triangulation from to =
-  case filter ((== to) . Dcel.destination triangulation) (Dcel.vertexOutgoingEdges triangulation from) of
-    edge : _ -> Just edge
-    [] -> Nothing
-
--- | Find an oriented edge in a thawed triangulation.
-findMutableEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Maybe Int)
-findMutableEdge mutable from to = do
-  start <- readVertexOut mutable from
-  halfEdges <- directedEdgeCount mutable
-  if start < 0
-    then pure Nothing
-    else go (halfEdges + 1) start start False
- where
-  go !remaining !start !edge !visited
-    | remaining <= 0 = pure Nothing
-    | visited && edge == start = pure Nothing
-    | otherwise = do
-        destination <- readOrigin mutable (edge `xor` 1)
-        if destination == to
-          then pure (Just edge)
-          else do
-            previousEdge <- readPrevious mutable edge
-            go (remaining - 1) start (previousEdge `xor` 1) True
-
--- | Validate that both constraint endpoints belong to the mesh.
-validateEndpoints :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> VertexId -> Either (CdtError) ()
-validateEndpoints triangulation from to
-  | not (validVertex triangulation from) = Left (InvalidConstraintVertex from)
-  | not (validVertex triangulation to) = Left (InvalidConstraintVertex to)
-  | otherwise = Right ()
-
--- | Whether a vertex handle is admitted by the triangulation.
-validVertex :: Triangulation 'Constrained vertex directed undirected face -> VertexId -> Bool
-validVertex triangulation (VertexId vertex) = fromIntegral vertex < Dcel.numVertices triangulation
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Recovery.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Recovery.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Recovery.hs
+++ /dev/null
@@ -1,397 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Interpretation of a recovery program against the thawed mesh: conflict
--- strips are flipped away segment by segment and the constraints are set.
-module Moonlight.Triangulation.Internal.Cdt.Recovery
-  ( ConflictRecovery (..)
-  , applyMutableConstraint
-  , emptyConstraintRequest
-  , recoverMutableRequest
-  , recoverMutableProgramPiece
-  , recordRecoveredSegment
-  , resolveConflictStrip
-  , mutableEdgeCrosses
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import Data.Either (isRight)
-import Data.Foldable (traverse_)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Cdt.Combinators
-  ( foldWhileM
-  )
-import Moonlight.Triangulation.Internal.Cdt.Corridor
-  ( constraintWorkspaceFor
-  , existingProgramTag
-  , readConstraintProgram
-  , recoverProgramTag
-  , scanMutableConstraint
-  )
-import Moonlight.Triangulation.Internal.Cdt.Query (findMutableEdge)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite (flipEdge)
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRule (isFlippableEdge)
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeEdges)
-import Moonlight.Triangulation.Internal.Growable (GrowableWord32)
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , readScratch
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Internal.Predicates (orient2dCoordinates)
-
-data ConflictRecovery = ConflictRecovery
-  { recoveredConstraintEdge :: {-# UNPACK #-} !Int
-  , recoveredConstraintFresh :: !Bool
-  }
-
--- | Admit one request against the thawed mesh and recover it. Every constraint
--- verb in this module reaches the topology through here; the callers differ
--- only in what they do with a rejection and in when they publish.
-applyMutableConstraint
-  :: GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> VertexId
-  -> VertexId
-  -> ST s (Either (CdtError) MutableConstraintOutcome)
-applyMutableConstraint programWords mutable operation from to
-  | from == to = pure (Right (MutableConstraintAccepted emptyConstraintRequest))
-  | otherwise = do
-      workspace <- constraintWorkspaceFor programWords mutable
-      scanned <- scanMutableConstraint workspace mutable from to
-      case scanned of
-        Left failure -> pure (Left failure)
-        Right (MutableConstraintScanBlocked blocking) ->
-          pure (Right (MutableConstraintRejected (asUndirected blocking)))
-        Right (MutableConstraintScanAdmitted program) ->
-          fmap MutableConstraintAccepted
-            <$> recoverMutableRequest workspace mutable operation program
-
-emptyConstraintRequest :: ConstraintRequestAccumulator
-emptyConstraintRequest =
-  ConstraintRequestAccumulator
-    { accumulatedRequestPath = []
-    , accumulatedRequestAddedEdges = 0
-    , accumulatedRequestCorridors = 0
-    , accumulatedRequestReusedFaces = 0
-    , accumulatedRequestCrossedEdges = 0
-    }
-
-recoverMutableRequest
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> MutableConstraintProgram
-  -> ST s (Either (CdtError) ConstraintRequestAccumulator)
-recoverMutableRequest workspace mutable operation program = do
-  interpreted <-
-    foldWhileM
-      isRight
-      (recoverMutableProgramPiece workspace mutable operation (mutableProgramWordCount program))
-      ( Right
-          ConstraintProgramAccumulator
-            { accumulatedProgramCursor = 0
-            , accumulatedProgramRequest = emptyConstraintRequest
-            }
-      )
-      [1 .. mutableProgramPieceCount program]
-  pure $ case interpreted of
-    Left obstruction -> Left obstruction
-    Right completed
-      | accumulatedProgramCursor completed == mutableProgramWordCount program ->
-          Right (accumulatedProgramRequest completed)
-      | otherwise ->
-          Left
-            ( ConstraintCorridorObstructed
-                (CorridorProgramMalformed (accumulatedProgramCursor completed))
-            )
-
-recoverMutableProgramPiece
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Either (CdtError) ConstraintProgramAccumulator
-  -> Int
-  -> ST s (Either (CdtError) ConstraintProgramAccumulator)
-recoverMutableProgramPiece _ _ _ _ failed@(Left _) _ = pure failed
-recoverMutableProgramPiece workspace mutable operation wordCount (Right accumulator) _ = do
-  let !cursor = accumulatedProgramCursor accumulator
-      malformed =
-        Left
-          ( ConstraintCorridorObstructed
-              (CorridorProgramMalformed cursor)
-          )
-  if cursor < 0 || cursor >= wordCount
-    then pure malformed
-    else do
-      tag <- readConstraintProgram workspace cursor
-      case tag of
-        _
-          | tag == existingProgramTag && cursor + 1 < wordCount -> do
-              edge <- readConstraintProgram workspace (cursor + 1)
-              fresh <- setConstraint mutable edge
-              pure
-                ( Right
-                    accumulator
-                      { accumulatedProgramCursor = cursor + 2
-                      , accumulatedProgramRequest =
-                          recordRecoveredSegment
-                            (accumulatedProgramRequest accumulator)
-                            edge
-                            fresh
-                            0
-                            0
-                      }
-                )
-          | tag == recoverProgramTag && cursor + 3 < wordCount -> do
-              rawFrom <- readConstraintProgram workspace (cursor + 1)
-              rawTo <- readConstraintProgram workspace (cursor + 2)
-              conflictCount <- readConstraintProgram workspace (cursor + 3)
-              let !conflictStart = cursor + 4
-                  !nextCursor = conflictStart + conflictCount
-                  !from = VertexId (fromIntegral rawFrom)
-                  !to = VertexId (fromIntegral rawTo)
-              if conflictCount < 0 || nextCursor > wordCount
-                then pure malformed
-                else do
-                  direct <- findMutableEdge mutable rawFrom rawTo
-                  recovered <-
-                    case direct of
-                      Just edge -> do
-                        fresh <- setConstraint mutable edge
-                        pure
-                          ( Right
-                              ConflictRecovery
-                                { recoveredConstraintEdge = edge
-                                , recoveredConstraintFresh = fresh
-                                }
-                          )
-                      Nothing
-                        | conflictCount == 0 ->
-                            pure
-                              ( Left
-                                  ( ConstraintCorridorObstructed
-                                      (CorridorTargetMissing from to)
-                                  )
-                              )
-                        | otherwise ->
-                            resolveConflictStrip
-                              workspace
-                              mutable
-                              operation
-                              from
-                              to
-                              conflictStart
-                              conflictCount
-                  pure $ case recovered of
-                    Left obstruction -> Left obstruction
-                    Right recovery ->
-                      Right
-                        accumulator
-                          { accumulatedProgramCursor = nextCursor
-                          , accumulatedProgramRequest =
-                              recordRecoveredSegment
-                                (accumulatedProgramRequest accumulator)
-                                (recoveredConstraintEdge recovery)
-                                (recoveredConstraintFresh recovery)
-                                conflictCount
-                                (if conflictCount == 0 then 0 else conflictCount + 1)
-                          }
-          | otherwise -> pure malformed
-
-recordRecoveredSegment
-  :: ConstraintRequestAccumulator
-  -> Int
-  -> Bool
-  -> Int
-  -> Int
-  -> ConstraintRequestAccumulator
-recordRecoveredSegment accumulator edge fresh crossed reusedFaces =
-  accumulator
-    { accumulatedRequestPath = DirectedEdgeId (fromIntegral edge) : accumulatedRequestPath accumulator
-    , accumulatedRequestAddedEdges =
-        accumulatedRequestAddedEdges accumulator + if fresh then 1 else 0
-    , accumulatedRequestCorridors =
-        accumulatedRequestCorridors accumulator + if crossed == 0 then 0 else 1
-    , accumulatedRequestReusedFaces =
-        accumulatedRequestReusedFaces accumulator + reusedFaces
-    , accumulatedRequestCrossedEdges =
-        accumulatedRequestCrossedEdges accumulator + crossed
-    }
-
-resolveConflictStrip
-  :: ConstraintWorkspace s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> VertexId
-  -> VertexId
-  -> Int
-  -> Int
-  -> ST s (Either (CdtError) ConflictRecovery)
-resolveConflictStrip workspace mutable operation from to conflictStart stripLength = do
-  fromPoint <- pointAt mutable (vertexIdIndex from)
-  toPoint <- pointAt mutable (vertexIdIndex to)
-  traverse_
-    (\index -> do
-      edge <- readConstraintProgram workspace (conflictStart + index)
-      writeScratch operation index edge
-    )
-    [0 .. stripLength - 1]
-  recovered <-
-    recover
-      stripLength
-      safetyBudget
-      0
-      0
-      stripLength
-      stripLength
-      []
-      fromPoint
-      toPoint
-  case recovered of
-    Left failure -> pure (Left failure)
-    Right (edge, flipped) -> do
-      fresh <- setConstraint mutable edge
-      legalizeEdges mutable operation flipped
-      pure
-        ( Right
-            ConflictRecovery
-              { recoveredConstraintEdge = edge
-              , recoveredConstraintFresh = fresh
-              }
-        )
- where
-  safetyBudget = max 64 (32 * (stripLength + 1) * (stripLength + 1))
-
-  -- The corridor is an ordered section through the current triangulation.
-  -- Until an original section is dequeued, no flip can have changed that
-  -- edge's endpoints: flips only repurpose the identity being flipped, and
-  -- requeued identities remain behind every unseen original in this FIFO.
-  -- Consequently the first visit inherits the crossing proof established by
-  -- the corridor walk. Only requeued edges require the predicate again.
-  recover !capacity !remaining !stalled !headIndex !pendingCount !unseenOriginalCount !flipped !fromPoint !toPoint
-    | remaining <= 0 =
-        pure
-          ( Left
-              ( ConstraintRecoverySafetyBudgetExhausted
-                  from
-                  to
-                  safetyBudget
-                  (safetyBudget - remaining)
-              )
-          )
-    | pendingCount <= 0 = do
-        direct <- findMutableEdge mutable (vertexIdIndex from) (vertexIdIndex to)
-        case direct of
-          Just edge -> pure (Right (edge, flipped))
-          Nothing ->
-            pure
-              (Left (ConstraintRecoveryStripExhausted from to stripLength))
-    | otherwise = do
-        edge <- readScratch operation headIndex
-        let !nextHead = advance capacity headIndex
-            !restCount = pendingCount - 1
-            !nextUnseenOriginalCount = max 0 (unseenOriginalCount - 1)
-        stillCrosses <-
-          if unseenOriginalCount > 0
-            then pure True
-            else mutableEdgeCrosses mutable fromPoint toPoint edge
-        if not stillCrosses
-          then recover capacity (remaining - 1) 0 nextHead restCount nextUnseenOriginalCount flipped fromPoint toPoint
-          else do
-            flippable <- isFlippableEdge mutable edge
-            if flippable
-              then do
-                rewritten <- flipEdge mutable edge
-                case rewritten of
-                  Left obstruction -> pure (Left (CdtBuildError obstruction))
-                  Right () -> do
-                    addCounter operation CounterEdgeFlips 1
-                    crossesAfterFlip <- mutableEdgeCrosses mutable fromPoint toPoint edge
-                    if crossesAfterFlip
-                      then do
-                        enqueue capacity nextHead restCount edge
-                        recover
-                          capacity
-                          (remaining - 1)
-                          0
-                          nextHead
-                          (restCount + 1)
-                          nextUnseenOriginalCount
-                          (edge : flipped)
-                          fromPoint
-                          toPoint
-                      else recover capacity (remaining - 1) 0 nextHead restCount nextUnseenOriginalCount (edge : flipped) fromPoint toPoint
-              else do
-                enqueue capacity nextHead restCount edge
-                let !nextCount = restCount + 1
-                    !nextStalled = stalled + 1
-                if nextStalled >= nextCount
-                  then
-                    pure
-                      ( Left
-                          ( ConstraintRecoveryStripUnflippable
-                              from
-                              to
-                              (DirectedEdgeId (fromIntegral edge))
-                              nextCount
-                          )
-                      )
-                  else recover capacity (remaining - 1) nextStalled nextHead nextCount nextUnseenOriginalCount flipped fromPoint toPoint
-
-  enqueue !capacity !headIndex !count !edge =
-    writeScratch operation (wrap capacity (headIndex + count)) edge
-
-  advance :: Int -> Int -> Int
-  advance !capacity !index
-    | index + 1 == capacity = 0
-    | otherwise = index + 1
-
-  wrap :: Int -> Int -> Int
-  wrap !capacity !index
-    | index >= capacity = index - capacity
-    | otherwise = index
-
-mutableEdgeCrosses
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> Point
-  -> Int
-  -> ST s Bool
-mutableEdgeCrosses
-  mutable
-  (Point lineFromX lineFromY)
-  (Point lineToX lineToY)
-  edge = do
-    edgeFromVertex <- readOrigin mutable edge
-    edgeToVertex <- readOrigin mutable (edge `xor` 1)
-    Point edgeFromX edgeFromY <- pointAt mutable edgeFromVertex
-    Point edgeToX edgeToY <- pointAt mutable edgeToVertex
-    let !edgeFromSide =
-          orient2dCoordinates
-            lineFromX lineFromY lineToX lineToY edgeFromX edgeFromY
-        !edgeToSide =
-          orient2dCoordinates
-            lineFromX lineFromY lineToX lineToY edgeToX edgeToY
-        !lineFromSide =
-          orient2dCoordinates
-            edgeFromX edgeFromY edgeToX edgeToY lineFromX lineFromY
-        !lineToSide =
-          orient2dCoordinates
-            edgeFromX edgeFromY edgeToX edgeToY lineToX lineToY
-    pure
-      ( opposite edgeFromSide edgeToSide
-          && opposite lineFromSide lineToSide
-      )
- where
-  opposite LT GT = True
-  opposite GT LT = True
-  opposite _ _ = False
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Region.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Region.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Region.hs
+++ /dev/null
@@ -1,40 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | The two-sided reading of the constrained domain: which faces lie outside
--- the protected boundary and which lie within it.
-module Moonlight.Triangulation.Internal.Cdt.Region
-  ( outerRegionFaces
-  , boundedRegionFaces
-  ) where
-
-import qualified Data.IntSet as IntSet
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.FloodFillIterator (facesAtEvenBarrierDepth)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-
--- | Inner faces at even minimum constraint-crossing depth from the outer face.
-outerRegionFaces
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> [FaceId]
--- Crossing depth, not bare reachability. A face lies outside the constrained
--- domain when the fewest constraints separating it from the outer face is even,
--- so the hole of an annulus (two crossings) is outside exactly as its exterior
--- (none) is. Reachability alone is the depth-zero layer and calls that hole
--- domain, which would mesh it.
-outerRegionFaces triangulation =
-  facesAtEvenBarrierDepth triangulation (Dcel.isConstraintEdge triangulation)
-
--- | Inner faces enclosed at odd constraint-crossing depth.
-boundedRegionFaces
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> [FaceId]
-boundedRegionFaces triangulation =
-  [ face
-  | face <- innerFaces triangulation
-  , not (IntSet.member (faceIdIndex face) outside)
-  ]
- where
-  outside = IntSet.fromList (fmap faceIdIndex (outerRegionFaces triangulation))
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Segment.hs
+++ /dev/null
@@ -1,243 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The singleton segment verbs: glue one segment or a polyline, and retire a
--- constraint, each inside one sealed transaction.
-module Moonlight.Triangulation.Internal.Cdt.Segment
-  ( addConstraintEdge
-  , addConstraintEdges
-  , removeConstraintEdge
-  , retireConstraintEdge
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bifunctor (first)
-import qualified Data.Vector as V
-import Data.Primitive.PrimArray (indexPrimArray, sizeofPrimArray)
-import Moonlight.Triangulation.BulkLoad (insertMany)
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Cdt.Admission
-  ( ConstraintAdmission (..)
-  , constraintAdmission
-  )
-import Moonlight.Triangulation.Internal.Cdt.Batch (recoverConstraints)
-import Moonlight.Triangulation.Internal.Cdt.Combinators
-  ( asConstraintStep
-  , bindMutable
-  )
-import Moonlight.Triangulation.Internal.Cdt.Recovery (applyMutableConstraint)
-import Moonlight.Triangulation.Internal.Cdt.Site
-  ( lookupExistingConstraintEndpoint
-  , placeConstraintEndpoint
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeEdges)
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  )
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState (OperationState)
-import Moonlight.Triangulation.Internal.Paged (TransactionShape (LocalTransaction))
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Transaction (runUnmeasuredTransaction)
-import Moonlight.Triangulation.Internal.Types
-
--- | Site both endpoints and glue the segment between them, in one transaction.
--- A refused request publishes nothing, so the caller's triangulation still
--- stands and the endpoints it would have sited are not among its vertices.
-addConstraintEdge
-  :: forall vertex directed undirected face
-   . HasPosition vertex
-  => Triangulation 'Constrained vertex directed undirected face
-  -> vertex
-  -> vertex
-  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
-addConstraintEdge triangulation fromVertex toVertex = do
-  result <-
-    case (fromExisting, toExisting) of
-      (Just from, Just to) ->
-        case constraintAdmission triangulation from to of
-          ConstraintBlocked blocking -> Left (ConstraintIntersection blocking)
-          ConstraintAdmitted ->
-            applyConstraintToExistingEndpoints
-              triangulation
-              from
-              fromVertex
-              to
-              toVertex
-      _ -> addConstraintWithEndpointPlacement fromExisting toExisting
-  pure result
- where
-  !fromPoint = position fromVertex
-  !toPoint = position toVertex
-  !fromExisting = lookupExistingConstraintEndpoint triangulation fromPoint
-  !toExisting = lookupExistingConstraintEndpoint triangulation toPoint
-
-  addConstraintWithEndpointPlacement existingFrom existingTo = do
-    (request, frozen) <-
-      runUnmeasuredTransaction
-        CdtBuildError
-        LocalTransaction
-        triangulation
-        (missingEndpointCount existingFrom + missingEndpointCount existingTo)
-        $ \mutable operation -> do
-            programWords <- newGrowableWord32 256
-            asConstraintStep
-              (placeOrReuseConstraintEndpoint mutable operation existingFrom fromPoint fromVertex)
-              `bindMutable` \from ->
-                asConstraintStep
-                  (placeOrReuseConstraintEndpoint mutable operation existingTo toPoint toVertex)
-                  `bindMutable` \to ->
-                    recoverConstraintRequest programWords mutable operation from to
-    pure (publishConstraintResult frozen request)
-
-  missingEndpointCount :: Maybe VertexId -> Int
-  missingEndpointCount = maybe 1 (const 0)
-
-  placeOrReuseConstraintEndpoint
-    :: MutableDcel s vertex directed undirected face
-    -> OperationState s
-    -> Maybe VertexId
-    -> Point
-    -> vertex
-    -> ST s (Either BuildError VertexId)
-  placeOrReuseConstraintEndpoint mutable operation existing point payload =
-    case existing of
-      Just vertex -> do
-        writeVertexData mutable (vertexIdIndex vertex) payload
-        pure (Right vertex)
-      Nothing -> placeConstraintEndpoint mutable operation Nothing point payload
-
--- | The common singleton case already owns both sites. Resolve them before
--- opening topology, then retain the original payload semantics in the
--- transaction. The corridor worker is unchanged; only two redundant point
--- locations and two unused vertex-capacity reservations disappear.
-applyConstraintToExistingEndpoints
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> vertex
-  -> VertexId
-  -> vertex
-  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
-applyConstraintToExistingEndpoints triangulation from fromPayload to toPayload =
-  do
-    (request, frozen) <-
-      runUnmeasuredTransaction CdtBuildError LocalTransaction triangulation 0 $ \mutable operation -> do
-        programWords <- newGrowableWord32 256
-        writeVertexData mutable (vertexIdIndex from) fromPayload
-        writeVertexData mutable (vertexIdIndex to) toPayload
-        recoverConstraintRequest programWords mutable operation from to
-    pure (publishConstraintResult frozen request)
-
-recoverConstraintRequest
-  :: GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> VertexId
-  -> VertexId
-  -> ST s (Either CdtError ConstraintRequestAccumulator)
-recoverConstraintRequest programWords mutable operation from to =
-  applyMutableConstraint programWords mutable operation from to
-    `bindMutable` \applied ->
-      case applied of
-        MutableConstraintRejected blocking ->
-          pure (Left (ConstraintIntersection blocking))
-        MutableConstraintAccepted request -> pure (Right request)
-{-# INLINE recoverConstraintRequest #-}
-
--- | Attach one accepted local result to the mesh published by the transaction.
-publishConstraintResult
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> ConstraintRequestAccumulator
-  -> ConstraintResult vertex directed undirected face
-publishConstraintResult frozen request =
-  ConstraintRecoveryResult
-    { constraintRecoveryTriangulation = frozen
-    , constraintRecoveryPathReceipt = V.fromList (reverse (accumulatedRequestPath request))
-    , constraintRecoveryAddedEdges = accumulatedRequestAddedEdges request
-    }
-
--- | Insert a polyline's vertices and recover each adjacent segment as a
--- constraint, optionally closing the final segment back to the first.
-addConstraintEdges
-  :: HasPosition vertex
-  => Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector vertex
-  -> Bool
-  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face)
-addConstraintEdges triangulation polylineVertices closed
-  | V.null polylineVertices = Right triangulation
-  | otherwise = do
-      (withVertices, handles) <- insertPolylineVertices triangulation polylineVertices
-      let adjacent = V.zip handles (V.drop 1 handles)
-          closing =
-            if closed && V.length handles > 1
-              then
-                case (handles V.!? (V.length handles - 1), handles V.!? 0) of
-                  (Just finalVertex, Just firstVertex) ->
-                    V.singleton (finalVertex, firstVertex)
-                  _ -> V.empty
-              else V.empty
-      batch <- recoverConstraints withVertices (adjacent V.++ closing)
-      case V.foldl' firstBlocking Nothing (constraintBatchOutcomes batch) of
-        Nothing -> Right (constraintBatchTriangulation batch)
-        Just blocking -> Left (ConstraintIntersection blocking)
- where
-  firstBlocking found@(Just _) _ = found
-  firstBlocking Nothing outcome =
-    case outcome of
-      ConstraintAccepted _ _ -> Nothing
-      ConstraintRejected blocking -> Just blocking
-
-insertPolylineVertices
-  :: HasPosition vertex
-  => Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector vertex
-  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face, V.Vector VertexId)
-insertPolylineVertices triangulation points = do
-  result <- first CdtBuildError (insertMany triangulation points)
-  let !mapping = buildInputVertices result
-  pure (buildTriangulation result, V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping))
-
--- | Retire one constraint edge and restore local Delaunay legality.
-removeConstraintEdge
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> UndirectedEdgeId
-  -> Either (CdtError) (Triangulation 'Constrained vertex directed undirected face)
-removeConstraintEdge triangulation edge
-  | undirectedEdgeIdIndex edge >= edgeCount =
-      Left (ConstraintEdgeIndexOutOfRange edge edgeCount)
-  -- An edge carrying no constraint has nothing to retire, and answering that
-  -- without thawing is the difference between O(1) and a republished mesh.
-  | not (Dcel.isConstraintEdge triangulation edge) = Right triangulation
-  | otherwise = do
-      (_, frozen) <-
-        runUnmeasuredTransaction CdtBuildError LocalTransaction triangulation 0 $ \mutable operation ->
-          retireConstraintEdge mutable operation edge
-      pure frozen
- where
-  edgeCount = Dcel.numUndirectedEdges triangulation
-
--- | Retire one constraint inside the open transaction and restore the Delaunay
--- property across the edge it protected.
-retireConstraintEdge
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> UndirectedEdgeId
-  -> ST s (Either (CdtError) ())
-retireConstraintEdge mutable operation edge = do
-  halfEdges <- directedEdgeCount mutable
-  let !directed = directedEdgeIdIndex (normalizedDirected edge)
-  if directed >= halfEdges
-    then pure (Left (ConstraintEdgeIndexOutOfRange edge (halfEdges `quot` 2)))
-    else do
-      constrained <- readConstraint mutable directed
-      if not constrained
-        then pure (Right ())
-        else do
-          _ <- clearConstraint mutable directed
-          legalizeEdges mutable operation [directed]
-          pure (Right ())
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Site.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Site.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Site.hs
+++ /dev/null
@@ -1,68 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Siting of constraint endpoints: resolving a point to an existing handle,
--- and materializing one inside an open transaction when it has none.
-module Moonlight.Triangulation.Internal.Cdt.Site
-  ( lookupExistingConstraintEndpoint
-  , placeConstraintEndpoint
-  ) where
-
-import Control.Monad.ST (ST)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Insertion (insertVertexAtPoint)
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  )
-import Moonlight.Triangulation.Internal.PointIndex (lookupPointIndex)
-import Moonlight.Triangulation.Internal.Probe (Probe (..))
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Math
-
-lookupExistingConstraintEndpoint
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Maybe VertexId
-lookupExistingConstraintEndpoint triangulation point =
-  VertexId . fromIntegral
-    <$> lookupPointIndex
-      (triPointX triangulation)
-      (triPointY triangulation)
-      (triPointIndex triangulation)
-      point
-
--- | Materialize one point in the open transaction. A payload standing at an
--- occupied position keeps that handle and overwrites the payload, which is what
--- the persistent insertion verb settled on.
---
--- The coordinates are checked here rather than by the callers, because a split
--- point is computed rather than supplied: @lineIntersection@ refuses only a
--- zero denominator, and a denominator merely close to zero answers a coordinate
--- no arena should hold.
-placeConstraintEndpoint
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> vertex
-  -> ST s (Either BuildError VertexId)
-placeConstraintEndpoint mutable operation hint point payload =
-  case validatePoint Nothing point of
-    Left failure -> pure (Left failure)
-    Right _ -> do
-      addCounter operation CounterInputPoints 1
-      outcome <- insertVertexAtPoint @'ProbeOff mutable operation hint point payload
-      case outcome of
-        Left failure -> pure (Left failure)
-        Right (vertex, disposition) -> do
-          case disposition of
-            AlreadyPresent -> do
-              writeVertexData mutable vertex payload
-              addCounter operation CounterExistingPoints 1
-              addCounter operation CounterDuplicatePoints 1
-            Inserted -> addCounter operation CounterUniquePoints 1
-          pure (Right (VertexId (fromIntegral vertex)))
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Split.hs
+++ /dev/null
@@ -1,472 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The splitting program: corridors that divide every constraint they cross,
--- driven across as few sealed transactions as the vertex reservation allows.
-module Moonlight.Triangulation.Internal.Cdt.Split
-  ( addConstraintAndSplit
-  , addConstraintsAndSplit
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import qualified Data.List as List
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Cdt.Combinators
-  ( asConstraintStep
-  , bindMutable
-  )
-import Moonlight.Triangulation.Internal.Cdt.Corridor
-  ( constraintWorkspaceFor
-  , scanMutableConstraint
-  )
-import Moonlight.Triangulation.Internal.Cdt.Query
-  ( findMutableEdge
-  , getConflictingEdgesBetweenVertices
-  , validateEndpoints
-  )
-import Moonlight.Triangulation.Internal.Cdt.Recovery
-  ( applyMutableConstraint
-  , recoverMutableRequest
-  )
-import Moonlight.Triangulation.Internal.Cdt.Segment (retireConstraintEdge)
-import Moonlight.Triangulation.Internal.Cdt.Site (placeConstraintEndpoint)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  )
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState (OperationState)
-import Moonlight.Triangulation.Internal.Paged
-  ( TransactionShape (DenseTransaction, LocalTransaction)
-  )
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Transaction (runUnmeasuredTransaction)
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Math
-
--- | The part of a splitting request that does not move: the payload maker, the
--- endpoints the failure report must name, the iteration budget, and the
--- transaction's vertex reservation, past which the corridor suspends rather
--- than writes.
-data SplitRequest vertex = SplitRequest
-  { splitRequestMakeVertex :: !(Point -> vertex)
-  , splitRequestFrom :: !VertexId
-  , splitRequestTo :: !VertexId
-  , splitRequestBudget :: {-# UNPACK #-} !Int
-  , splitRequestVertexBound :: {-# UNPACK #-} !Int
-  }
-
--- | The part that does: how much budget is left, which vertex the unrecovered
--- remainder starts at, and the path and edge count accumulated behind it. The
--- path runs newest-first and is reversed once, at publication.
-data SplitCursor = SplitCursor
-  { splitCursorRemaining :: {-# UNPACK #-} !Int
-  , splitCursorAt :: !VertexId
-  , splitCursorPath :: ![DirectedEdgeId]
-  , splitCursorAdded :: {-# UNPACK #-} !Int
-  }
-
--- | How a corridor run ends inside its transaction: settled at the target, or
--- suspended mid-corridor because the next division would outgrow the vertex
--- reservation. A suspension is not a refusal; the driver publishes,
--- re-reserves against the published mesh, and resumes at the cursor.
-data SplitStep
-  = SplitSettled !SplitCursor
-  | SplitSuspended !SplitCursor
-
--- | What one settled request leaves behind.
-data SplitReceipt = SplitReceipt
-  { splitReceiptPath :: !(V.Vector DirectedEdgeId)
-  , splitReceiptAdded :: {-# UNPACK #-} !Int
-  }
-
--- | Glue the requested segment, dividing every constraint it crosses at the
--- crossing point rather than refusing it. One transaction carries the whole
--- corridor: a singleton is a batch of one under the shared driver.
-addConstraintAndSplit
-  :: (Point -> vertex)
-  -> Triangulation 'Constrained vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> Either (CdtError) (ConstraintResult vertex directed undirected face)
-addConstraintAndSplit makeVertex triangulation from to = do
-  (published, receipts) <-
-    driveConstraintSplits LocalTransaction makeVertex triangulation (V.singleton (from, to))
-  pure
-    ( case receipts V.!? 0 of
-        Just receipt ->
-          ConstraintRecoveryResult
-            { constraintRecoveryTriangulation = published
-            , constraintRecoveryPathReceipt = splitReceiptPath receipt
-            , constraintRecoveryAddedEdges = splitReceiptAdded receipt
-            }
-        Nothing -> ConstraintRecoveryResult published V.empty 0
-    )
-
--- | Divide every requested segment inside as few transactions as the vertex
--- reservation allows: usually one, which is the referent semantics for a
--- splitting batch. Nothing is republished between corridors unless a corridor
--- crosses constraints created by an earlier request in the same batch --
--- growth no census against the base can see. In that one case the corridor
--- suspends, the chunk publishes, and the driver re-reserves against the
--- published mesh and resumes at the suspended cursor.
-addConstraintsAndSplit
-  :: (Point -> vertex)
-  -> Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector (VertexId, VertexId)
-  -> Either (CdtError) (ConstraintSplitBatchResult vertex directed undirected face)
-addConstraintsAndSplit makeVertex triangulation requests = do
-  (published, receipts) <- driveConstraintSplits DenseTransaction makeVertex triangulation requests
-  pure
-    ConstraintRecoveryResult
-      { constraintRecoveryTriangulation = published
-      , constraintRecoveryPathReceipt = V.map splitReceiptPath receipts
-      , constraintRecoveryAddedEdges = V.sum (V.map splitReceiptAdded receipts)
-      }
-
--- | The obstruction census for one request against a published mesh: how many
--- constrained crossings its corridor holds, and the first of them. The first
--- crossing is a valid local section of the next transaction before any rewrite
--- occurs, so a chunk's first request carries it across the thaw boundary
--- instead of immediately rediscovering it; every later request rescans,
--- because earlier corridors may have rewritten the topology the witness names.
-splitCensus
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> (VertexId, VertexId)
-  -> (Int, Maybe DirectedEdgeId)
-splitCensus triangulation (from, to)
-  | from == to = (0, Nothing)
-  | otherwise =
-      List.foldl'
-        countCrossing
-        (0, Nothing)
-        (getConflictingEdgesBetweenVertices triangulation from to)
- where
-  countCrossing :: (Int, Maybe DirectedEdgeId) -> DirectedEdgeId -> (Int, Maybe DirectedEdgeId)
-  countCrossing (!count, firstCrossing) crossing =
-    ( count + 1
-    , case firstCrossing of
-        Just first -> Just first
-        Nothing -> Just crossing
-    )
-
--- | Interpret splitting requests in order across as few sealed transactions as
--- possible. Each chunk reserves against the exact obstruction census of every
--- remaining request, so a chunk always settles at least its first request:
--- that census ran against the very mesh the chunk thawed and is exact before
--- any in-session rewrite. Termination follows.
-driveConstraintSplits
-  :: forall vertex directed undirected face.
-     TransactionShape
-  -> (Point -> vertex)
-  -> Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector (VertexId, VertexId)
-  -> Either
-      (CdtError)
-      ( Triangulation 'Constrained vertex directed undirected face
-      , V.Vector SplitReceipt
-      )
-driveConstraintSplits shape makeVertex base requests = do
-  V.mapM_ (uncurry (validateEndpoints base)) requests
-  if V.all (uncurry (==)) requests
-    then Right (base, V.replicate (V.length requests) emptySplitReceipt)
-    else advance base 0 [] Nothing
- where
-  emptySplitReceipt = SplitReceipt V.empty 0
-
-  advance
-    :: Triangulation 'Constrained vertex directed undirected face
-    -> Int
-    -> [SplitReceipt]
-    -> Maybe SplitCursor
-    -> Either
-        CdtError
-        ( Triangulation 'Constrained vertex directed undirected face
-        , V.Vector SplitReceipt
-        )
-  advance triangulation start settled resumed
-    | start >= V.length requests =
-        Right (triangulation, V.fromList (reverse settled))
-    | otherwise = do
-        let remaining = V.drop start requests
-            firstRound = start == 0 && case resumed of
-              Nothing -> True
-              Just _ -> False
-            -- The opening chunk reserves optimistically -- one crossing per
-            -- request -- rather than walking every corridor for an exact
-            -- census before any work begins. A request that outgrows the
-            -- reservation suspends mid-corridor with its partial work kept,
-            -- and the next round censuses exactly against the published mesh,
-            -- so heavy batches pay at most one speculative chunk.
-            censuses
-              | firstRound = V.replicate (V.length remaining) (1, Nothing)
-              | otherwise = V.map (splitCensus triangulation) remaining
-            !reservedSites = V.sum (V.map fst censuses)
-            !additionalCapacity = 2 * reservedSites + 8
-            -- The walk budget bounds divisions per request; constraints born
-            -- inside the chunk are covered by the reservation term.
-            !budget =
-              2 * (Dcel.numConstraints triangulation + 2 * reservedSites + V.length remaining)
-                + Dcel.numUndirectedEdges triangulation
-                + 8
-        ((receipts, suspended), published) <-
-          runUnmeasuredTransaction
-            CdtBuildError
-            shape
-            triangulation
-            additionalCapacity
-            $ \mutable operation -> do
-            programWords <- newGrowableWord32 32
-            runSplitChunk
-              makeVertex
-              mutable
-              operation
-              programWords
-              remaining
-              censuses
-              (pointCapacity mutable)
-              budget
-              resumed
-        advance published (start + length receipts) (receipts ++ settled) suspended
-
--- | One sealed transaction over a prefix of the remaining requests. Receipts
--- run newest-first; a suspension carries no receipt, so the settled count is
--- exactly the receipt count.
-runSplitChunk
-  :: (Point -> vertex)
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> GrowableWord32 s
-  -> V.Vector (VertexId, VertexId)
-  -> V.Vector (Int, Maybe DirectedEdgeId)
-  -> Int
-  -> Int
-  -> Maybe SplitCursor
-  -> ST s (Either CdtError ([SplitReceipt], Maybe SplitCursor))
-runSplitChunk makeVertex mutable operation programWords requests censuses capacity budget resumed =
-  go 0 [] resumed
- where
-  go offset receipts pending
-    | offset >= V.length requests = pure (Right (receipts, Nothing))
-    | otherwise =
-        let (from, to) = requests V.! offset
-        in case pending of
-             Nothing
-               | from == to ->
-                   go (offset + 1) (SplitReceipt V.empty 0 : receipts) Nothing
-             _ ->
-               let witness = if offset == 0 then snd (censuses V.! 0) else Nothing
-                   cursor = case pending of
-                     Just resumedCursor -> resumedCursor
-                     Nothing -> SplitCursor budget from [] 0
-                   request =
-                     SplitRequest
-                       { splitRequestMakeVertex = makeVertex
-                       , splitRequestFrom = from
-                       , splitRequestTo = to
-                       , splitRequestBudget = budget
-                       , splitRequestVertexBound = capacity
-                       }
-               in splitConstraintCorridor request programWords mutable operation witness cursor
-                    `bindMutable` \step ->
-                      case step of
-                        SplitSettled settledCursor ->
-                          go (offset + 1) (receiptOf settledCursor : receipts) Nothing
-                        SplitSuspended suspendedCursor ->
-                          pure (Right (receipts, Just suspendedCursor))
-
-  receiptOf cursor =
-    SplitReceipt
-      { splitReceiptPath = V.fromList (reverse (splitCursorPath cursor))
-      , splitReceiptAdded = splitCursorAdded cursor
-      }
-
-splitConstraintCorridor
-  :: SplitRequest vertex
-  -> GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe DirectedEdgeId
-  -> SplitCursor
-  -> ST s (Either (CdtError) SplitStep)
-splitConstraintCorridor request programWords mutable operation initialCrossing initialCursor =
-  case initialCrossing of
-    Just crossing -> divide initialCursor crossing
-    Nothing -> descend initialCursor
- where
-  !target = splitRequestTo request
-
-  descend cursor
-    | splitCursorRemaining cursor <= 0 =
-        pure
-          ( Left
-              ( ConstraintSplitBudgetExhausted
-                  (splitRequestFrom request)
-                  target
-                  (splitRequestBudget request)
-              )
-          )
-    | otherwise = do
-        workspace <- constraintWorkspaceFor programWords mutable
-        scanMutableConstraint workspace mutable (splitCursorAt cursor) target
-          `bindMutable` \scanned ->
-            case scanned of
-              MutableConstraintScanAdmitted program ->
-                recoverMutableRequest workspace mutable operation program
-                  `bindMutable` \final ->
-                    pure
-                      ( Right
-                          ( SplitSettled
-                              (advanceCursor cursor (splitCursorRemaining cursor) target final)
-                          )
-                      )
-              MutableConstraintScanBlocked crossing -> divide cursor crossing
-
-  divide cursor crossing = do
-    occupied <- pointCount mutable
-    if occupied + 2 > splitRequestVertexBound request
-      then pure (Right (SplitSuspended cursor))
-      else divideWithin cursor crossing
-
-  divideWithin cursor crossing = do
-    let !crossingEdge = directedEdgeIdIndex crossing
-        !oldConstraint = asUndirected crossing
-    segmentFrom <- pointAt mutable (vertexIdIndex (splitCursorAt cursor))
-    segmentTo <- pointAt mutable (vertexIdIndex target)
-    oldFrom <- readOrigin mutable crossingEdge
-    oldTo <- readOrigin mutable (crossingEdge `xor` 1)
-    edgeFrom <- pointAt mutable oldFrom
-    edgeTo <- pointAt mutable oldTo
-    -- The split point lies on the crossing edge, so both its incident faces
-    -- already contain it and the exact walk settles on its first probe. Without
-    -- this the locate is unhinted, and an unhinted locate descends vertex by
-    -- vertex from the arena's first face -- a distance that grows with the mesh
-    -- while the corridor it is splitting stays a fixed few faces wide. A hull
-    -- edge carries the outer face on one side, which the locator would refuse.
-    incidentFace <- readFace mutable crossingEdge
-    twinFace <- readFace mutable (crossingEdge `xor` 1)
-    let !splitHint = Just (if incidentFace > 0 then incidentFace else twinFace)
-    let !oldEndpoints =
-          ( VertexId (fromIntegral oldFrom)
-          , VertexId (fromIntegral oldTo)
-          )
-    case lineIntersection crossing segmentFrom segmentTo edgeFrom edgeTo of
-      Left indeterminate -> pure (Left indeterminate)
-      Right splitPoint ->
-        -- The split lands where the intersection says, not where a round trip
-        -- through 'makeVertex' happens to put it.
-        asConstraintStep
-          ( placeConstraintEndpoint
-              mutable
-              operation
-              splitHint
-              splitPoint
-              (splitRequestMakeVertex request splitPoint)
-          )
-          `bindMutable` \splitVertex ->
-            repairIfRounded oldConstraint oldEndpoints splitVertex `bindMutable` \() ->
-              applyMutableConstraint programWords mutable operation (splitCursorAt cursor) splitVertex
-                `bindMutable` \prefix ->
-                  case prefix of
-                    MutableConstraintRejected blocking ->
-                      pure (Left (ConstraintIntersection blocking))
-                    MutableConstraintAccepted segment ->
-                      descend
-                        (advanceCursor cursor (splitCursorRemaining cursor - 1) splitVertex segment)
-
-  repairIfRounded oldConstraint oldEndpoints splitVertex = do
-    divided <- mutableEdgeWasSplit mutable oldConstraint oldEndpoints splitVertex
-    if divided
-      then pure (Right ())
-      else repairRoundedSplit programWords mutable operation oldConstraint oldEndpoints splitVertex
-
-  advanceCursor cursor remaining reached segment =
-    cursor
-      { splitCursorRemaining = remaining
-      , splitCursorAt = reached
-      , splitCursorPath = accumulatedRequestPath segment ++ splitCursorPath cursor
-      , splitCursorAdded = splitCursorAdded cursor + accumulatedRequestAddedEdges segment
-      }
-
--- | Whether the inserted vertex actually divided the constraint it was placed
--- on. It did if that constraint is gone — the split retired the identity — or
--- if both halves now carry one. Rounding can place the vertex somewhere that
--- leaves the old constraint standing and neither half glued.
-mutableEdgeWasSplit
-  :: MutableDcel s vertex directed undirected face
-  -> UndirectedEdgeId
-  -> (VertexId, VertexId)
-  -> VertexId
-  -> ST s Bool
-mutableEdgeWasSplit mutable edge (from, to) splitVertex = do
-  halfEdges <- directedEdgeCount mutable
-  let !directed = directedEdgeIdIndex (normalizedDirected edge)
-  stillConstrained <-
-    if directed >= halfEdges
-      then pure False
-      else readConstraint mutable directed
-  if not stillConstrained
-    then pure True
-    else do
-      leading <- mutableConstraintBetween mutable from splitVertex
-      if leading
-        then mutableConstraintBetween mutable splitVertex to
-        else pure False
-
-mutableConstraintBetween
-  :: MutableDcel s vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> ST s Bool
-mutableConstraintBetween mutable from to = do
-  found <- findMutableEdge mutable (vertexIdIndex from) (vertexIdIndex to)
-  case found of
-    Nothing -> pure False
-    Just edge -> readConstraint mutable edge
-
--- | The split point rounded onto neither half of the constraint it was meant to
--- divide. Retire the stale identity and glue both halves against the vertex
--- that was actually sited. What this recovers is repair rather than the
--- requested segment, so it contributes nothing to the request's path.
-repairRoundedSplit
-  :: GrowableWord32 s
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> UndirectedEdgeId
-  -> (VertexId, VertexId)
-  -> VertexId
-  -> ST s (Either (CdtError) ())
-repairRoundedSplit programWords mutable operation oldEdge (from, to) splitVertex =
-  retireConstraintEdge mutable operation oldEdge `bindMutable` \() ->
-    glue from splitVertex `bindMutable` \() -> glue splitVertex to
- where
-  glue start end =
-    applyMutableConstraint programWords mutable operation start end `bindMutable` \recovered ->
-      pure $ case recovered of
-        MutableConstraintRejected blocking -> Left (ConstraintIntersection blocking)
-        MutableConstraintAccepted _ -> Right ()
-
-lineIntersection
-  :: DirectedEdgeId
-  -> Point
-  -> Point
-  -> Point
-  -> Point
-  -> Either (CdtError) (Point)
-lineIntersection crossing (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy)
-  | denominator == 0 =
-      Left (ConstraintSplitIntersectionIndeterminate crossing)
-  | otherwise =
-      Right (canonicalPoint (Point (ax + t * rx) (ay + t * ry)))
- where
-  rx = bx - ax
-  ry = by - ay
-  sx = dx - cx
-  sy = dy - cy
-  denominator = rx * sy - ry * sx
-  t = ((cx - ax) * sy - (cy - ay) * sx) / denominator
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Types.hs
+++ /dev/null
@@ -1,360 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | The shared vocabulary of the constrained layer: its refusals, its published
--- results, and the transaction-local state its interpreters carry.
-module Moonlight.Triangulation.Internal.Cdt.Types
-  ( CdtError (..)
-  , CanonicalSegment (..)
-  , ConstraintConflict (..)
-  , ConstrainedUnionError (..)
-  , ConstrainedSeamSide (..)
-  , ConstrainedSeamFaceEvidence (..)
-  , ConstrainedSeamResult (..)
-  , CorridorObstruction (..)
-  , ConstraintRecoveryResult (..)
-  , ConstraintResult
-  , ConstraintOutcome (..)
-  , ConstraintBatchStats (..)
-  , ConstraintBatchResult (..)
-  , ConstrainedExtensionResult (..)
-  , ConstraintSplitBatchResult
-  , ConstraintBatchAccumulator (..)
-  , ConstraintRequestAccumulator (..)
-  , ConstraintWorkspace (..)
-  , MutableConstraintProgram (..)
-  , MutableConstraintOutcome (..)
-  , MutableProgramCursor (..)
-  , MutablePlanWalk (..)
-  , MutableConstraintScan (..)
-  , ConstraintProgramAccumulator (..)
-  , CdtBuildResult (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  )
-import Moonlight.Triangulation.Internal.Growable (GrowableWord32)
-import Moonlight.Triangulation.Internal.Paged (PublicationStats)
-import Moonlight.Triangulation.Internal.Representation (BuildResult, Triangulation)
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.IntersectionIterator (Intersection)
-import GHC.Generics (Generic)
-
--- | Typed refusal surface for constrained construction and corridor recovery.
-data CdtError
-  = CdtBuildError !BuildError
-  | InvalidConstraintVertex !VertexId
-  | ConstraintIntersection !UndirectedEdgeId
-  | ConstraintInputConflicts !(V.Vector (Int, Int))
-  | ConstraintCorridorObstructed !CorridorObstruction
-  | ConstraintBatchCardinalityMismatch
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  | ConstraintEndpointIndexOutOfRange
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  | ConstraintSplitBudgetExhausted
-      !VertexId
-      !VertexId
-      {-# UNPACK #-} !Int
-  | ConstraintSplitIntersectionIndeterminate
-      !DirectedEdgeId
-  | ConstraintEdgeIndexOutOfRange
-      !UndirectedEdgeId
-      {-# UNPACK #-} !Int
-  | ConstraintRecoverySafetyBudgetExhausted
-      !VertexId
-      !VertexId
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  | ConstraintRecoveryStripExhausted
-      !VertexId
-      !VertexId
-      {-# UNPACK #-} !Int
-  | ConstraintRecoveryStripUnflippable
-      !VertexId
-      !VertexId
-      !DirectedEdgeId
-      {-# UNPACK #-} !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Geometric identity of one constraint segment. The constructor is private:
--- endpoints are always canonical points in ascending order, so direction and
--- edge numbering cannot leak into union witnesses.
-data CanonicalSegment = CanonicalSegment
-  { -- | Lesser endpoint under the point ordering.
-    segmentStart :: !(Point)
-  , -- | Greater endpoint under the point ordering.
-    segmentEnd :: !(Point)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A pair of properly crossing constraints, ordered independently of operand
--- and traversal order.
-data ConstraintConflict = ConstraintConflict
-  { -- | Lesser segment under the canonical segment ordering.
-    conflictFirstSegment :: !(CanonicalSegment)
-  , -- | Greater segment under the canonical segment ordering.
-    conflictSecondSegment :: !(CanonicalSegment)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Complete obstruction surface for atomic constrained union.
-data ConstrainedUnionError
-  = ConstraintUnionConflicts !(NonEmpty (ConstraintConflict))
-  | ConstraintUnionConstructionFailed !(CdtError)
-  | ConstraintUnionSiteMissing !(Point)
-  | ConstraintUnionNotSeparated
-  -- | Local publication would otherwise enumerate a dense resident payload
-  -- plane. Compact geometry-only payloads before requesting a seam.
-  | ConstraintUnionRequiresGeometryOnlyPublication
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Source-local side presented to a separated seam's face-preservation
--- selector. The resident side keeps its handles; incoming face handles are
--- translated only inside the join interpreter.
-data ConstrainedSeamSide
-  = SeamResident
-  | SeamIncoming
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact proof that one active source face survives as one target face. The
--- three points are stored in ascending order, so the witness is independent
--- of local face rotation and handle numbering.
-data ConstrainedSeamFaceEvidence = ConstrainedSeamFaceEvidence
-  { constrainedSeamSourceFace :: !FaceId
-    -- ^ Face handle in the source operand.
-  , constrainedSeamTargetFace :: !FaceId
-    -- ^ Corresponding face handle in the published result.
-  , constrainedSeamFaceFirstPoint :: !(Point)
-    -- ^ Least vertex position under canonical point order.
-  , constrainedSeamFaceSecondPoint :: !(Point)
-    -- ^ Middle vertex position under canonical point order.
-  , constrainedSeamFaceThirdPoint :: !(Point)
-    -- ^ Greatest vertex position under canonical point order.
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Atomic publication of a source-preserving constrained seam and the proof
--- needed to transport solved face-indexed data. The receipt is derived during
--- the ordered append/seam interpretation; canonical numbering remains an
--- explicit observation rather than hidden work in this local operation.
-data ConstrainedSeamResult vertex = ConstrainedSeamResult
-  { constrainedSeamResultTriangulation
-      :: !(Triangulation 'Constrained vertex () () ())
-    -- ^ Published constrained union.
-  , constrainedSeamLeftFaceCount :: {-# UNPACK #-} !Int
-    -- ^ Stable size of the resident source face block. In selective mode this
-    -- is a handle-partition witness, not proof that every resident face was
-    -- selected.
-  , constrainedSeamRightFaceEvidence
-      :: !(V.Vector (ConstrainedSeamFaceEvidence))
-    -- ^ Exact transport evidence only for selected incoming source faces.
-  , constrainedSeamJoinFaces :: !(V.Vector FaceId)
-    -- ^ Exact final J component, including rewritten exterior source filler.
-  , constrainedSeamLeftConstraintCount :: {-# UNPACK #-} !Int
-    -- ^ Stable number of left-source constraints.
-  , constrainedSeamConstraintStats :: !ConstraintBatchStats
-    -- ^ Constraint recovery work performed by the seam.
-  , constrainedSeamBuildStats :: !BuildStats
-    -- ^ Topology work performed by the seam.
-  , constrainedSeamPublicationStats :: !PublicationStats
-    -- ^ Page publication work measured by the transaction owner.
-  , constrainedSeamCachedFrontierPointReads :: {-# UNPACK #-} !Int
-    -- ^ Frontier point reads performed while interpreting the cached seam.
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance Eq vertex => Eq (ConstrainedSeamResult vertex)
-deriving stock instance Show vertex => Show (ConstrainedSeamResult vertex)
-
--- | Structural witness that corridor recovery could not complete.
-data CorridorObstruction
-  = CorridorWalkDidNotTerminate {-# UNPACK #-} !Int
-  | CorridorBoundaryMissing !VertexId !VertexId
-  | CorridorProgramMalformed {-# UNPACK #-} !Int
-  | CorridorTargetMissing !VertexId !VertexId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One constrained publication and its path receipt. The path carrier names
--- whether the caller requested one recovered path or an ordered family; the
--- mesh and edge count have one representation in either case.
-data ConstraintRecoveryResult pathReceipt vertex directed undirected face = ConstraintRecoveryResult
-  { constraintRecoveryTriangulation :: !(Triangulation 'Constrained vertex directed undirected face)
-    -- ^ Published constrained triangulation.
-  , constraintRecoveryPathReceipt :: !pathReceipt
-    -- ^ Recovered path evidence at the operation's requested multiplicity.
-  , constraintRecoveryAddedEdges :: {-# UNPACK #-} !Int
-    -- ^ Edges created while recovering the path evidence.
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance
-  (Eq pathReceipt, Eq vertex, Eq directed, Eq undirected, Eq face)
-  => Eq (ConstraintRecoveryResult pathReceipt vertex directed undirected face)
-deriving stock instance
-  (Show pathReceipt, Show vertex, Show directed, Show undirected, Show face)
-  => Show (ConstraintRecoveryResult pathReceipt vertex directed undirected face)
-
--- | Atomic result of admitting one constraint segment.
-type ConstraintResult vertex directed undirected face =
-  ConstraintRecoveryResult (V.Vector DirectedEdgeId) vertex directed undirected face
-
--- | Per-request outcome in a constraint batch.
-data ConstraintOutcome
-  = ConstraintAccepted
-      !(V.Vector DirectedEdgeId)
-      {-# UNPACK #-} !Int
-  | ConstraintRejected
-      !UndirectedEdgeId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Aggregate work and disposition counts for a constraint batch.
-data ConstraintBatchStats = ConstraintBatchStats
-  { constraintBatchRequests :: {-# UNPACK #-} !Int
-    -- ^ Requests interpreted.
-  , constraintBatchAccepted :: {-# UNPACK #-} !Int
-    -- ^ Requests admitted.
-  , constraintBatchRejected :: {-# UNPACK #-} !Int
-    -- ^ Requests refused by an existing constraint.
-  , constraintBatchCorridors :: {-# UNPACK #-} !Int
-    -- ^ Recovery corridors opened.
-  , constraintBatchReusedFaces :: {-# UNPACK #-} !Int
-    -- ^ Existing faces retained while recovering corridors.
-  , constraintBatchCrossedEdges :: {-# UNPACK #-} !Int
-    -- ^ Edges crossed while tracing corridors.
-  }
-  deriving stock (Eq, Show, Read, Generic)
-  deriving anyclass (NFData)
-
--- | Atomic publication of one constraint batch and its receipts.
-data ConstraintBatchResult vertex directed undirected face = ConstraintBatchResult
-  { constraintBatchTriangulation :: !(Triangulation 'Constrained vertex directed undirected face)
-    -- ^ Published constrained triangulation.
-  , constraintBatchOutcomes :: !(V.Vector ConstraintOutcome)
-    -- ^ Outcomes in request order.
-  , constraintBatchStats :: !ConstraintBatchStats
-    -- ^ Aggregate work performed by the batch.
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance
-  (Eq vertex, Eq directed, Eq undirected, Eq face)
-  => Eq (ConstraintBatchResult vertex directed undirected face)
-deriving stock instance
-  (Show vertex, Show directed, Show undirected, Show face)
-  => Show (ConstraintBatchResult vertex directed undirected face)
-
--- | Receipt of one asymmetric constrained extension. The triangulation and
--- all telemetry arise from the same sealed transaction: base sites were
--- resident, incoming sites were inserted, and only incoming constraints were
--- interpreted. A later refinement is deliberately a separate operation over
--- this immutable result, not a hidden continuation of this transaction.
-data ConstrainedExtensionResult vertex directed undirected face = ConstrainedExtensionResult
-  { constrainedExtensionConstraintBatch :: {-# UNPACK #-} !(ConstraintBatchResult vertex directed undirected face)
-    -- ^ Published extension and incoming-constraint receipts.
-  , constrainedExtensionBuildStats :: !BuildStats
-    -- ^ Topology work performed by the extension.
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance
-  (Eq vertex, Eq directed, Eq undirected, Eq face)
-  => Eq (ConstrainedExtensionResult vertex directed undirected face)
-deriving stock instance
-  (Show vertex, Show directed, Show undirected, Show face)
-  => Show (ConstrainedExtensionResult vertex directed undirected face)
-
--- | One published mesh carrying every requested division. The paths are
--- per-request receipts in traversal order; a later request may have rewritten
--- topology an earlier path names, which is the same as-traversed reading the
--- singleton path already carries.
-type ConstraintSplitBatchResult vertex directed undirected face =
-  ConstraintRecoveryResult (V.Vector (V.Vector DirectedEdgeId)) vertex directed undirected face
-
-data ConstraintBatchAccumulator = ConstraintBatchAccumulator
-  { accumulatedConstraintOutcomes :: ![ConstraintOutcome]
-  , accumulatedConstraintStats :: !ConstraintBatchStats
-  }
-
-data ConstraintRequestAccumulator = ConstraintRequestAccumulator
-  { accumulatedRequestPath :: ![DirectedEdgeId]
-  , accumulatedRequestAddedEdges :: {-# UNPACK #-} !Int
-  , accumulatedRequestCorridors :: {-# UNPACK #-} !Int
-  , accumulatedRequestReusedFaces :: {-# UNPACK #-} !Int
-  , accumulatedRequestCrossedEdges :: {-# UNPACK #-} !Int
-  }
-
-data ConstraintWorkspace s = ConstraintWorkspace
-  { constraintProgramWords :: !(GrowableWord32 s)
-  , constraintWalkBudget :: {-# UNPACK #-} !Int
-  }
-
-data MutableConstraintProgram = MutableConstraintProgram
-  { mutableProgramWordCount :: {-# UNPACK #-} !Int
-  , mutableProgramPieceCount :: {-# UNPACK #-} !Int
-  }
-
--- | What one request did to the thawed mesh. A rejection is a value: the batch
--- interpreter records it and carries on, the singleton verbs abandon the
--- transaction on it, and both readings are lawful because an accepted request
--- may lawfully obstruct a later one.
-data MutableConstraintOutcome
-  = MutableConstraintRejected !UndirectedEdgeId
-  | MutableConstraintAccepted !ConstraintRequestAccumulator
-
-data MutableProgramCursor = MutableProgramCursor
-  { mutableCursorAt :: !VertexId
-  , mutableCursorHeader :: {-# UNPACK #-} !Int
-  , mutableCursorWriteAt :: {-# UNPACK #-} !Int
-  , mutableCursorConflictCount :: {-# UNPACK #-} !Int
-  , mutableCursorPieceCount :: {-# UNPACK #-} !Int
-  , mutableCursorAfterOverlap :: !Bool
-  }
-
-data MutablePlanWalk
-  = MutablePlanActive !Intersection !MutableProgramCursor
-  | MutablePlanComplete !MutableConstraintProgram
-  | MutablePlanBlocked !DirectedEdgeId
-  | MutablePlanFailed !CorridorObstruction
-
-data MutableConstraintScan
-  = MutableConstraintScanBlocked !DirectedEdgeId
-  | MutableConstraintScanAdmitted !MutableConstraintProgram
-
-data ConstraintProgramAccumulator = ConstraintProgramAccumulator
-  { accumulatedProgramCursor :: {-# UNPACK #-} !Int
-  , accumulatedProgramRequest :: !ConstraintRequestAccumulator
-  }
-
--- | Result of maximal constrained construction, including rejected requests.
-data CdtBuildResult vertex directed undirected face = CdtBuildResult
-  { cdtAcceptedBuild :: {-# UNPACK #-} !(BuildResult 'Constrained vertex directed undirected face)
-    -- ^ Canonical build result containing every admitted constraint.
-  , cdtRejectedConstraints :: !(V.Vector (Int, Int))
-    -- ^ Input-index pairs that could not be admitted.
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
diff --git a/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs b/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Cdt/Union.hs
+++ /dev/null
@@ -1,445 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Canonical constraint segments and the atomic partial union of two
--- constrained meshes, retaining or combining their site annotations.
-module Moonlight.Triangulation.Internal.Cdt.Union
-  ( canonicalSegment
-  , constraintSegments
-  , unionConstrainedWith
-  , unionConstrained
-  , joinSeparatedConstrained
-  , extendConstrainedWith
-  , segmentRequest
-  , firstRejected
-  , completeConstraintConflicts
-  , orderedConflict
-  , crossingIsRepresented
-  ) where
-
-import Control.Monad.ST (ST)
-import qualified Data.Bifunctor as Bifunctor
-import Data.Either (isRight)
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Insertion (insertAdmittedPointCombining)
-import Moonlight.Triangulation.Refinement (validateRefinementParameters)
-import Moonlight.Triangulation.Internal.Cdt.Batch
-  ( finalizeConstraintBatch
-  , interpretConstraintRequests
-  , recoverConstraints
-  )
-import Moonlight.Triangulation.Internal.Cdt.Build (fromDelaunay)
-import Moonlight.Triangulation.Internal.Cdt.Combinators (foldWhileM)
-import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)
-import Moonlight.Triangulation.IntersectionIterator (foldCorridorBetweenPoints)
-import Moonlight.Triangulation.Internal.Cdt.Types
-import Moonlight.Triangulation.Internal.Join.Seam
-  ( executeConstrainedSeam
-  , planSeam
-  , seamExecutionBuildStats
-  , seamExecutionPublicationStats
-  , seamExecutionCachedFrontierPointReads
-  , seamExecutionLeftFaceCount
-  , seamExecutionJoinFaces
-  , seamExecutionRightFaceEvidence
-  , seamExecutionTriangulation
-  )
-import Moonlight.Triangulation.Internal.Join.SiteSet
-  ( SiteSet
-  , delaunayFromSiteSet
-  , siteSetAssocs
-  , siteSetFromTriangulation
-  , siteSetPoints
-  , siteSetSize
-  , siteSetUnionWith
-  )
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedFill)
-import Moonlight.Triangulation.Internal.Paged (TransactionShape (DenseTransaction, LocalTransaction))
-import Moonlight.Triangulation.Internal.Predicates
-  ( onClosedSegment
-  , segmentsProperlyCross
-  )
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Mutable (MutableDcel)
-import Moonlight.Triangulation.Internal.OperationState (OperationState)
-import Moonlight.Triangulation.Internal.Transaction (runTransaction)
-import Moonlight.Triangulation.Internal.Types
-
-canonicalSegment :: Point -> Point -> CanonicalSegment
-canonicalSegment from to
-  | from <= to = CanonicalSegment from to
-  | otherwise = CanonicalSegment to from
-{-# INLINE canonicalSegment #-}
-
--- | Geometry of the constraint section, deduplicated and canonically ordered.
-constraintSegments
-  :: Triangulation 'Constrained vertex directed undirected face
-  -> V.Vector (CanonicalSegment)
-constraintSegments triangulation =
-  V.fromList
-    ( Set.toAscList
-        ( Set.fromList
-            [ canonicalSegment
-                (Dcel.vertexPoint triangulation from)
-                (Dcel.vertexPoint triangulation to)
-            | edge <- constraintEdges triangulation
-            , let (from, to) = Dcel.undirectedEndpoints triangulation edge
-            ]
-        )
-    )
-
--- | Atomic partial union of constrained meshes. Coincident sites combine
--- their annotations before construction. Complete canonical conflict
--- witnesses descend first; a successful branch then reaches the existing
--- batch corridor interpreter exactly once.
-unionConstrainedWith
-  :: (annotation -> annotation -> annotation)
-  -> Triangulation 'Constrained annotation () () ()
-  -> Triangulation 'Constrained annotation () () ()
-  -> Either
-      (ConstrainedUnionError)
-      (Triangulation 'Constrained annotation () () ())
-unionConstrainedWith combine left right =
-  case NonEmpty.nonEmpty (Set.toAscList conflicts) of
-    Just witnesses -> Left (ConstraintUnionConflicts witnesses)
-    Nothing -> do
-      unconstrained <-
-        Bifunctor.first
-          (ConstraintUnionConstructionFailed . CdtBuildError)
-          (delaunayFromSiteSet unionSites)
-      requests <- traverse (segmentRequest unconstrained) (V.toList segments)
-      recovered <-
-        Bifunctor.first ConstraintUnionConstructionFailed
-          (recoverConstraints (fromDelaunay unconstrained) (V.fromList requests))
-      case firstRejected (constraintBatchOutcomes recovered) of
-        Just blocking ->
-          Left
-            ( ConstraintUnionConstructionFailed
-                (ConstraintIntersection blocking)
-            )
-        Nothing ->
-          Bifunctor.first
-            (ConstraintUnionConstructionFailed . CdtBuildError)
-            (canonicalize (constraintBatchTriangulation recovered))
- where
-  leftSites = siteSetFromTriangulation left
-  rightSites = siteSetFromTriangulation right
-  unionSites = siteSetUnionWith combine leftSites rightSites
-  segments =
-    V.fromList
-      ( Set.toAscList
-          ( Set.union
-              (Set.fromList (V.toList (constraintSegments left)))
-              (Set.fromList (V.toList (constraintSegments right)))
-          )
-      )
-  conflicts = completeConstraintConflicts unionSites segments
-{-# INLINE unionConstrainedWith #-}
-
--- | Atomic partial union specialized to geometry-only constrained meshes.
-unionConstrained
-  :: Triangulation 'Constrained () () () ()
-  -> Triangulation 'Constrained () () () ()
-  -> Either
-      (ConstrainedUnionError)
-      (Triangulation 'Constrained () () () ())
-unionConstrained = unionConstrainedWith (\_ _ -> ())
-{-# INLINE unionConstrained #-}
-
--- | Join two strictly separated constrained triangulations with the caller's
--- left mesh resident. The right source is appended and only their common
--- tangent corridor and its selected-face-free exterior cavity are legalized.
--- The selector sees source-local handles on an explicit resident/incoming
--- side; selected face handles and constraint contours are immutable barriers.
--- Passing a selector that is always true retains the preserve-all law and its
--- typed incompatibility when the seam requires a source-face rewrite. The
--- result carries the exact final J component plus transport evidence only for
--- selected incoming faces.
--- The resident operand must have gone through 'geometryOnlyPublication'; a
--- dense boxed payload section would make local freeze enumerate the resident
--- world and is refused as a typed obstruction.
---
--- The published constraint section is exactly the source constraints union
--- the synthetic lower and upper seam bridge paths. Those paths are certified
--- perimeter constraints for the joined world and are subdivided locally when
--- an edge-length law requests it; the operation does not perform a global
--- constraint scan or canonicalization.
---
--- Strict separation in one admitted chart (x, or the orientation-preserving
--- y chart used by the seam planner) proves that the two site sets have no
--- coincident point, so annotations are copied from their source mesh and
--- never combined.
-joinSeparatedConstrained
-  :: (ConstrainedSeamSide -> FaceId -> Bool)
-  -> RefinementParameters
-  -> Triangulation 'Constrained annotation () () ()
-  -> Triangulation 'Constrained annotation () () ()
-  -> Either
-      (ConstrainedUnionError)
-      (ConstrainedSeamResult annotation)
-joinSeparatedConstrained sourceFacePreserved parameters left right = do
-  Bifunctor.first
-    (ConstraintUnionConstructionFailed . CdtBuildError)
-    (validateRefinementParameters parameters)
-  seamPlan <- maybe (Left ConstraintUnionNotSeparated) Right (planSeam left right)
-  vertexDefault <- ensureDefaultedPayloads left right
-  seamExecution <-
-    Bifunctor.first
-      (ConstraintUnionConstructionFailed . CdtBuildError)
-      ( executeConstrainedSeam
-          sourceFacePreserved
-          parameters
-          vertexDefault
-          seamPlan
-          left
-          right
-      )
-  let published = seamExecutionTriangulation seamExecution
-  pure
-    ConstrainedSeamResult
-      { constrainedSeamResultTriangulation = published
-      , constrainedSeamLeftFaceCount = seamExecutionLeftFaceCount seamExecution
-      , constrainedSeamRightFaceEvidence = seamExecutionRightFaceEvidence seamExecution
-      , constrainedSeamJoinFaces = seamExecutionJoinFaces seamExecution
-      , constrainedSeamLeftConstraintCount = Dcel.numConstraints left
-      , constrainedSeamConstraintStats = ConstraintBatchStats 0 0 0 0 0 0
-      , constrainedSeamBuildStats = seamExecutionBuildStats seamExecution
-      , constrainedSeamPublicationStats = seamExecutionPublicationStats seamExecution
-      , constrainedSeamCachedFrontierPointReads = seamExecutionCachedFrontierPointReads seamExecution
-      }
-
-ensureDefaultedPayloads
-  :: Triangulation 'Constrained annotation () () ()
-  -> Triangulation 'Constrained annotation () () ()
-  -> Either ConstrainedUnionError annotation
-ensureDefaultedPayloads left right =
-  case
-      ( boxedFill (triVertexData left)
-      , boxedFill (triDirectedData left)
-      , boxedFill (triUndirectedData left)
-      , boxedFill (triFaceData left)
-      , boxedFill (triVertexData right)
-      , boxedFill (triDirectedData right)
-      , boxedFill (triUndirectedData right)
-      , boxedFill (triFaceData right)
-      ) of
-    ( Just vertexDefault
-      , Just _
-      , Just _
-      , Just _
-      , Just _
-      , Just _
-      , Just _
-      , Just _
-      ) -> Right vertexDefault
-    _ -> Left ConstraintUnionRequiresGeometryOnlyPublication
-
--- | Extend one already-resident constrained triangulation with one new
--- constrained section. This is intentionally asymmetric: the base mesh is
--- thawed once, extension sites are inserted into it, and only the extension's
--- constraint section is replayed. Unlike 'unionConstrainedWith', it neither
--- rebuilds a canonical site set nor replays base constraints, because both
--- would erase the physical distinction between solved base and new work.
---
--- Incoming constraint recovery is itself the spatial conflict authority. It
--- walks only the incoming corridors against the resident base and returns a
--- typed intersection obstruction. Re-running the canonical all-pairs union
--- preflight here would make a tiny extension quadratic in the base. Any
--- structural or recovery obstruction abandons the transaction before a
--- partially extended mesh can be published.
-extendConstrainedWith
-  :: (annotation -> annotation -> annotation)
-  -> Triangulation 'Constrained annotation () () ()
-  -> Triangulation 'Constrained annotation () () ()
-  -> Either
-      (ConstrainedUnionError)
-      (ConstrainedExtensionResult annotation () () ())
-extendConstrainedWith combine base extension = do
-    (completed, extended, buildStats) <-
-      runTransaction
-        (ConstraintUnionConstructionFailed . CdtBuildError)
-        transactionShape
-        base
-        (siteSetSize extensionSites)
-        (insertAndRecoverExtension combine extensionSites extensionSegments)
-    pure
-      ConstrainedExtensionResult
-        { constrainedExtensionConstraintBatch = finalizeConstraintBatch extended completed
-        , constrainedExtensionBuildStats = buildStats
-        }
- where
-  extensionSites = siteSetFromTriangulation extension
-  extensionSegments = constraintSegments extension
-  transactionShape =
-    case V.uncons extensionSegments of
-      Just (segment, remaining)
-        | Dcel.numVertices base >= 200000
-        , siteSetSize extensionSites <= 128
-        , V.null remaining
-        , residentCorridorIsEmpty segment -> LocalTransaction
-      _ -> DenseTransaction
-  residentCorridorIsEmpty segment =
-    foldCorridorBetweenPoints
-      base
-      (QueryPoint (segmentStart segment))
-      (QueryPoint (segmentEnd segment))
-      (\_ _ -> Left ())
-      ()
-      == Just (Right ())
-{-# INLINE extendConstrainedWith #-}
-
-insertAndRecoverExtension
-  :: (annotation -> annotation -> annotation)
-  -> SiteSet annotation
-  -> V.Vector (CanonicalSegment)
-  -> MutableDcel s annotation () () ()
-  -> OperationState s
-  -> ST s (Either (ConstrainedUnionError) ConstraintBatchAccumulator)
-insertAndRecoverExtension combine extensionSites extensionSegments mutable operation = do
-  placed <- insertExtensionSites combine extensionSites mutable operation
-  case placed of
-    Left obstruction -> pure (Left obstruction)
-    Right handles ->
-      case traverse (segmentRequestFromHandles handles) (V.toList extensionSegments) of
-        Left obstruction -> pure (Left obstruction)
-        Right requests -> do
-          interpreted <-
-            fmap
-              (Bifunctor.first ConstraintUnionConstructionFailed)
-              (interpretConstraintRequests (V.fromList requests) mutable operation)
-          case interpreted of
-            Left obstruction -> pure (Left obstruction)
-            Right completed ->
-              case firstRejected (accumulatorOutcomes completed) of
-                Just blocking ->
-                  pure
-                    ( Left
-                        ( ConstraintUnionConstructionFailed
-                            (ConstraintIntersection blocking)
-                        )
-                    )
-                Nothing -> pure (Right completed)
-
-insertExtensionSites
-  :: forall s annotation
-   . (annotation -> annotation -> annotation)
-  -> SiteSet annotation
-  -> MutableDcel s annotation () () ()
-  -> OperationState s
-  -> ST s (Either (ConstrainedUnionError) (Map.Map (Point) VertexId))
-insertExtensionSites combine extensionSites mutable operation =
-  fmap
-    (fmap (Map.fromDistinctAscList . reverse))
-    ( foldWhileM
-        isRight
-        insertOne
-        (Right [])
-        (siteSetAssocs extensionSites)
-    )
- where
-  insertOne
-    :: Either (ConstrainedUnionError) [(Point, VertexId)]
-    -> (QueryPoint, annotation)
-    -> ST s (Either (ConstrainedUnionError) [(Point, VertexId)])
-  insertOne rejected@(Left _) _ = pure rejected
-  insertOne (Right accumulated) (queryPoint, annotation) =
-    fmap
-      ( Bifunctor.first (ConstraintUnionConstructionFailed . CdtBuildError)
-          . fmap
-            ( \(vertex, _) ->
-                ( queryPointValue queryPoint
-                , VertexId (fromIntegral vertex)
-                ) : accumulated
-            )
-      )
-      (insertAdmittedPointCombining combine Nothing mutable operation queryPoint annotation)
-
-accumulatorOutcomes :: ConstraintBatchAccumulator -> V.Vector ConstraintOutcome
-accumulatorOutcomes = V.fromList . reverse . accumulatedConstraintOutcomes
-{-# INLINE accumulatorOutcomes #-}
-
-segmentRequest
-  :: Triangulation mode annotation () () ()
-  -> CanonicalSegment
-  -> Either (ConstrainedUnionError) (VertexId, VertexId)
-segmentRequest triangulation segment =
-  segmentRequestFromHandles handles segment
- where
-  handles =
-    Map.fromList
-      [ ( Dcel.vertexPoint triangulation vertex
-        , vertex
-        )
-      | raw <- [0 .. Dcel.numVertices triangulation - 1]
-      , let vertex = VertexId (fromIntegral raw)
-      ]
-
-segmentRequestFromHandles
-  :: Map.Map (Point) VertexId
-  -> CanonicalSegment
-  -> Either (ConstrainedUnionError) (VertexId, VertexId)
-segmentRequestFromHandles handles segment =
-  case
-      ( Map.lookup (segmentStart segment) handles
-      , Map.lookup (segmentEnd segment) handles
-      ) of
-    (Just from, Just to) -> Right (from, to)
-    (Nothing, _) -> Left (ConstraintUnionSiteMissing (segmentStart segment))
-    (_, Nothing) -> Left (ConstraintUnionSiteMissing (segmentEnd segment))
-
-firstRejected :: V.Vector ConstraintOutcome -> Maybe UndirectedEdgeId
-firstRejected =
-  V.foldr
-    (\outcome later ->
-       case outcome of
-         ConstraintAccepted _ _ -> later
-         ConstraintRejected blocking -> Just blocking
-    )
-    Nothing
-
-completeConstraintConflicts
-  :: SiteSet annotation
-  -> V.Vector (CanonicalSegment)
-  -> Set.Set (ConstraintConflict)
-completeConstraintConflicts unionSites segments =
-  Set.fromList
-    [ orderedConflict leftSegment rightSegment
-    | leftIndex <- [0 .. V.length segments - 1]
-    , rightIndex <- [leftIndex + 1 .. V.length segments - 1]
-    , let leftSegment = segments V.! leftIndex
-    , let rightSegment = segments V.! rightIndex
-    , segmentsProperlyCross
-        (segmentStart leftSegment)
-        (segmentEnd leftSegment)
-        (segmentStart rightSegment)
-        (segmentEnd rightSegment)
-    , not (crossingIsRepresented unionSites leftSegment rightSegment)
-    ]
-
-orderedConflict
-  :: CanonicalSegment
-  -> CanonicalSegment
-  -> ConstraintConflict
-orderedConflict left right
-  | left <= right = ConstraintConflict left right
-  | otherwise = ConstraintConflict right left
-
-crossingIsRepresented
-  :: SiteSet annotation
-  -> CanonicalSegment
-  -> CanonicalSegment
-  -> Bool
-crossingIsRepresented sites first second =
-  V.any
-    (\point ->
-       onClosedSegment (segmentStart first) (segmentEnd first) point
-         && onClosedSegment (segmentStart second) (segmentEnd second) point
-    )
-    (siteSetPoints sites)
diff --git a/src-build/Moonlight/Triangulation/Internal/CircleSweep.hs b/src-build/Moonlight/Triangulation/Internal/CircleSweep.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/CircleSweep.hs
+++ /dev/null
@@ -1,931 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE TypeApplications #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
-module Moonlight.Triangulation.Internal.CircleSweep
-  ( RadiallyOrderedArena
-  , radiallyOrderArena
-  , radiallyOrderGeometry
-  , circleSweepInsert
-  ) where
-
-import Control.Monad (forM_, when)
-import Control.Monad.ST (ST)
-import Data.Bits ((.&.), shiftL, shiftR, xor)
-import qualified Data.Vector.Algorithms.Intro as Intro
-import qualified Data.Vector.Algorithms.Radix as Radix
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32, Word64)
-import GHC.Float (castDoubleToWord64)
-import Moonlight.Triangulation.Internal.HandleDefs (DirectedEdgeId (..))
-import Moonlight.Triangulation.Insertion (insertExistingVertex)
-import Moonlight.Triangulation.Internal.DcelOperations.Hull
-  ( ReservedSweepCells
-  , SweepCellCursor
-  , SweepInsertion (..)
-  , closeOuterTurnReserved
-  , commitReservedSweepConnections
-  , fixHullConvexity
-  , initialSweepCellCursor
-  , insertOutsideHullAtEdge
-  , reserveSweepCells
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-  ( seedGenericPairInArena
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Chain
-  ( connectAscendingLineArena
-  , prepareAscendingLineArena
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Normalize
-  ( LegalizationDrain (..)
-  , drainDenseUnconstrainedGenericLegalization
-  )
-import Moonlight.Triangulation.Internal.Mutable
-  ( DenseMutableDcel
-  , MutableDcel
-  , connectedCount
-  , denseMutableDcel
-  , denseMutableOwner
-  , denseFaceEdges
-  , denseReadFaceEdge
-  , denseReadNext
-  , denseReadOrigin
-  , denseReadPointX
-  , denseReadPointY
-  , denseReadPrevious
-  , directedEdgeCount
-  , faceCount
-  , halfEdgeCapacity
-  , pointCapacity
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , LegalizationArena
-  , OperationState
-  , addCounter
-  , legalizationArena
-  , maxCounter
-  , readScratch
-  , storeLegalizationArena
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Probe (Probe (..))
-import Moonlight.Triangulation.Internal.Predicates (orient2dCoordinates)
-import Moonlight.Triangulation.Types (BuildError (..))
-
--- | The hull is an angular index over the DCEL outer-face cycle. The cycle
--- itself already owns hull adjacency, so left and right walking is
--- 'readPrevious'/'readNext' on the live mesh and this record only caches what
--- topology cannot state: the pseudo-angle of each outer edge's origin and the
--- bucket anchors accelerating the predecessor search. An outer edge's key is
--- its origin's @(angle, x, y)@ with the edge id as the final tie-break; the
--- angle lives in 'hullAngleByEdge' and the coordinates are re-read from the
--- immutable origin only when two cached angles compare exactly equal. Slots
--- of edges that have left the outer cycle are never read again, so the cache
--- needs no invalidation, and there is no second ring beside the authoritative
--- one.
-data Hull s = Hull
-  { hullCenterX :: {-# UNPACK #-} !Double
-  , hullCenterY :: {-# UNPACK #-} !Double
-  , hullBucketCapacity :: {-# UNPACK #-} !Int
-  , hullAngleByEdge :: !(MUV.MVector s Double)
-  }
-
--- | The strict state of the derived angular section. The bucket vector is
--- mutated only to transport the section across one local hull rewrite; its
--- extent and active cardinality are threaded as values, so the sweep does not
--- bounce through singleton mutable cells for facts already known at descent.
-data HullIndex s = HullIndex
-  !(MUV.MVector s Word32)
-  {-# UNPACK #-} !Int
-
-data DeferredInsertion s
-  = DeferredInsertionFailure !BuildError
-  | DeferredInsertionSuccess
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !SweepCellCursor
-      !(LegalizationArena s)
-      {-# UNPACK #-} !(HullIndex s)
-
-data ClosedHullSection s = ClosedHullSection
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-  !(LegalizationArena s)
-  {-# UNPACK #-} !SweepCellCursor
-
--- | Vertex ids after their radial keys have descended to one total order. The
--- constructor is private: the circle sweep consumes the proof and therefore
--- never pays to establish the same ordering twice. Coordinates remain owned
--- by the DCEL rather than being duplicated in the sweep input.
-newtype RadiallyOrderedArena s = RadiallyOrderedArena
-  (MUV.MVector s Word32)
-
-radiallyOrderArena
-  :: MUV.MVector s (Double, Double, Double, Word32)
-  -> ST s (RadiallyOrderedArena s)
-radiallyOrderArena arena = do
-  Intro.sort arena
-  let (_, _, _, vertices) = MUV.unzip4 arena
-  pure (RadiallyOrderedArena vertices)
-{-# INLINE radiallyOrderArena #-}
-
--- | Order the geometry-only ingress by moving only its existing vertex-id
--- plane. Stable radix descent first orders the high 36 bits of each binary64
--- distance. Every equal-prefix fibre is then normalized by the complete
--- historical @(distance, x, y, vertex)@ order, recovering the discarded low
--- 28 bits before the fibres glue into the authoritative radial order. The key
--- coordinates remain in their immutable planes, so the sole linear scratch
--- plane is four bytes per site rather than a second copy of every radial
--- record.
---
--- Coordinate admission bounds every squared distance to a finite,
--- non-negative binary64 value, whose unsigned word order is numeric order.
-radiallyOrderGeometry
-  :: U.Vector Double
-  -> U.Vector Double
-  -> U.Vector Double
-  -> MUV.MVector s Word32
-  -> ST s (RadiallyOrderedArena s)
-radiallyOrderGeometry distances pointXs pointYs vertices = do
-  Radix.sortBy
-    radialPassCount
-    radialBucketCount
-    (radialDistanceBucket distances)
-    vertices
-  when (MUV.length vertices > 1) $ do
-    first <- MUV.unsafeRead vertices 0
-    let !firstPrefix = geometryDistancePrefix distances first
-    (!finalFibreStart, _) <-
-      MUV.ifoldM'
-        (\(!fibreStart, !distancePrefix) relativeIndex vertex ->
-           let !nextPrefix = geometryDistancePrefix distances vertex
-               !index = relativeIndex + 1
-            in if nextPrefix == distancePrefix
-                 then pure (fibreStart, distancePrefix)
-                 else do
-                   sortGeometryPrefixFibre
-                     distances
-                     pointXs
-                     pointYs
-                     vertices
-                     fibreStart
-                     index
-                   pure (index, nextPrefix)
-        )
-        (0, firstPrefix)
-        (MUV.unsafeSlice 1 (MUV.length vertices - 1) vertices)
-    sortGeometryPrefixFibre
-      distances
-      pointXs
-      pointYs
-      vertices
-      finalFibreStart
-      (MUV.length vertices)
-  pure (RadiallyOrderedArena vertices)
-{-# INLINE radiallyOrderGeometry #-}
-
-radialPassCount :: Int
-radialPassCount = 3
-
-radialBucketCount :: Int
-radialBucketCount = 1 `shiftL` radialPassWidth
-
-radialPassWidth :: Int
-radialPassWidth = 12
-
-radialPrefixDiscardedBits :: Int
-radialPrefixDiscardedBits = 64 - radialPassCount * radialPassWidth
-
-radialBucketMask :: Word64
-radialBucketMask = fromIntegral (radialBucketCount - 1)
-
-radialDistanceBucket :: U.Vector Double -> Int -> Word32 -> Int
-radialDistanceBucket distances pass vertex =
-  fromIntegral
-    ( (castDoubleToWord64 (geometryDistance distances vertex)
-         `shiftR` (radialPrefixDiscardedBits + radialPassWidth * pass)
-      )
-        .&. radialBucketMask
-    )
-{-# INLINE radialDistanceBucket #-}
-
-geometryDistancePrefix :: U.Vector Double -> Word32 -> Word64
-geometryDistancePrefix distances vertex =
-  castDoubleToWord64 (geometryDistance distances vertex)
-    `shiftR` radialPrefixDiscardedBits
-{-# INLINE geometryDistancePrefix #-}
-
-geometryDistance :: U.Vector Double -> Word32 -> Double
-geometryDistance distances vertex = U.unsafeIndex distances (fromIntegral vertex)
-{-# INLINE geometryDistance #-}
-
-compareGeometryVertex
-  :: U.Vector Double
-  -> U.Vector Double
-  -> U.Vector Double
-  -> Word32
-  -> Word32
-  -> Ordering
-compareGeometryVertex distances pointXs pointYs left right =
-  case compare (geometryDistance distances left) (geometryDistance distances right) of
-    EQ ->
-      case compare (coordinateAt pointXs left) (coordinateAt pointXs right) of
-        EQ ->
-          case compare (coordinateAt pointYs left) (coordinateAt pointYs right) of
-            EQ -> compare left right
-            yOrder -> yOrder
-        xOrder -> xOrder
-    distanceOrder -> distanceOrder
-{-# INLINE compareGeometryVertex #-}
-
-coordinateAt :: U.Vector Double -> Word32 -> Double
-coordinateAt coordinates vertex = U.unsafeIndex coordinates (fromIntegral vertex)
-{-# INLINE coordinateAt #-}
-
-sortGeometryPrefixFibre
-  :: U.Vector Double
-  -> U.Vector Double
-  -> U.Vector Double
-  -> MUV.MVector s Word32
-  -> Int
-  -> Int
-  -> ST s ()
-sortGeometryPrefixFibre distances pointXs pointYs vertices start end =
-  when (end - start > 1) $
-    Intro.sortBy
-      (compareGeometryVertex distances pointXs pointYs)
-      (MUV.unsafeSlice start (end - start) vertices)
-{-# INLINE sortGeometryPrefixFibre #-}
-
--- | Circle sweep over one mutable DCEL, consuming one radial vertex order.
--- Coordinates are read from the authoritative dense DCEL. Insertions
--- initially close only acute hull turns. One terminal Graham pass restores
--- full convexity, so construction does not repeatedly pay for global
--- convexity that no intermediate observer can see.
-circleSweepInsert
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> RadiallyOrderedArena s
-  -> ST s (Either BuildError Int)
-circleSweepInsert mutable operation (RadiallyOrderedArena arena) =
-  case denseMutableDcel mutable of
-    Nothing -> pure (Left CircleSweepRequiresDenseStorage)
-    Just dense -> circleSweepInsertDense dense operation arena
-{-# INLINE circleSweepInsert #-}
-
-circleSweepInsertDense
-  :: forall s vertex directed undirected face
-   . DenseMutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> MUV.MVector s Word32
-  -> ST s (Either BuildError Int)
-circleSweepInsertDense dense operation arena
-  | MUV.length arena == 0 = pure (Right 0)
-  | otherwise = do
-      residentCount <- connectedCount mutable
-      ascendingLine <-
-        if residentCount == 0
-          then prepareAscendingLineArena dense arena
-          else pure Nothing
-      case ascendingLine of
-        Just line -> connectAscendingLineArena dense operation line
-        Nothing -> circleSweepInsertArea dense operation arena
- where
-  !mutable = denseMutableOwner dense
-{-# INLINE circleSweepInsertDense #-}
-
-circleSweepInsertArea
-  :: forall s vertex directed undirected face
-   . DenseMutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> MUV.MVector s Word32
-  -> ST s (Either BuildError Int)
-circleSweepInsertArea dense operation arena = do
-      let !ordered = MUV.length arena
-      seed <- insertSeed 0
-      case seed of
-        Left failure -> pure (Left failure)
-        Right seedCount -> do
-          faces <- faceCount mutable
-          if faces <= 1 || seedCount >= ordered
-            then pure (Right seedCount)
-            else do
-              (centerX, centerY) <- seedCentre dense
-              reservedOutcome <- reserveSweepCells dense (ordered - seedCount)
-              case reservedOutcome of
-                Left failure -> pure (Left failure)
-                Right reserved -> do
-                  builtHull <- buildHull dense operation centerX centerY
-                  case builtHull of
-                    Left failure -> pure (Left failure)
-                    Right (!hull, !initialHullIndex) -> do
-                      initialLegalizationArena <- legalizationArena operation
-                      let !initialCursor = initialSweepCellCursor reserved
-                      inserted <-
-                        insertRemaining
-                          reserved
-                          initialCursor
-                          initialLegalizationArena
-                          hull
-                          initialHullIndex
-                          ordered
-                          seedCount
-                          (0 :: Int)
-                          (0 :: Int)
-                          (0 :: Int)
-                          (0 :: Int)
-                      case inserted of
-                        Left failure -> pure (Left failure)
-                        Right (!skippedCount, !fastCount, !flips, !maxDepth, !sweepCursor, !sweepArena) -> do
-                          repaired <- fixHullConvexity reserved sweepCursor operation sweepArena
-                          case repaired of
-                            Left failure -> pure (Left failure)
-                            Right (!_closures, !terminalFlips, !terminalMaxDepth, !finalArena, !finalCursor) -> do
-                              commitReservedSweepConnections reserved finalCursor fastCount
-                              storeLegalizationArena operation finalArena
-                              -- The sweep counts its own hull insertions; its drains
-                              -- hand their flip and depth tallies up once.
-                              addCounter operation CounterHullInsertions fastCount
-                          -- The angular candidate leaves a point to the fallback
-                          -- whenever it lands right of, or on, the hull edge its
-                          -- own angle selected — spade's
-                          -- `is_on_right_side_or_on_line` branch, which spade's own
-                          -- source calls "very slow". Both counts ride out in
-                          -- BuildStats so the split is comparable across the two
-                          -- implementations and satisfies
-                          -- seed + fast + skipped = unique.
-                          --
-                          -- CounterSweepFastPoints is NOT CounterHullInsertions
-                          -- renamed: the latter is also charged by
-                          -- insertOutsideHull, so it counts hull-adjacent
-                          -- insertions from either path and dominates this one
-                          -- whenever a skipped point lands outside the hull. The
-                          -- two coincide only while skipped is zero, and charging
-                          -- them independently is what makes the identity a check
-                          -- rather than a restatement.
-                              addCounter operation CounterSweepFastPoints fastCount
-                              addCounter operation CounterSweepSkippedPoints skippedCount
-                              addCounter operation CounterEdgeFlips flips
-                              maxCounter operation CounterLegalizationMaxStack maxDepth
-                              addCounter operation CounterEdgeFlips terminalFlips
-                              maxCounter operation CounterLegalizationMaxStack terminalMaxDepth
-                              insertedSkipped <- insertSkipped skippedCount 0
-                              pure (seedCount <$ insertedSkipped)
- where
-  !mutable = denseMutableOwner dense
-
-  insertSeed !index
-    | index >= MUV.length arena = pure (Right index)
-    | otherwise = do
-        raw <- MUV.unsafeRead arena index
-        result <- insertExistingVertex @'ProbeOff mutable operation (unpackIndex raw)
-        case result of
-          Left failure -> pure (Left failure)
-          Right () -> do
-            faces <- faceCount mutable
-            if faces > 1
-              then pure (Right (index + 1))
-              else insertSeed (index + 1)
-
-  insertRemaining
-    :: ReservedSweepCells s vertex directed undirected face
-    -> SweepCellCursor
-    -> LegalizationArena s
-    -> Hull s
-    -> HullIndex s
-    -> Int
-    -> Int
-    -> Int
-    -> Int
-    -> Int
-    -> Int
-    -> ST s (Either BuildError (Int, Int, Int, Int, SweepCellCursor, LegalizationArena s))
-  insertRemaining !reserved !cursor !candidateArena !hull !hullIndex !ordered !index !skippedCount !fastCount !flips !maxDepth
-    | index >= ordered = pure (Right (skippedCount, fastCount, flips, maxDepth, cursor, candidateArena))
-    | otherwise = do
-        raw <- MUV.unsafeRead arena index
-        let !vertex = unpackIndex raw
-        queryXWide <- denseReadPointX dense vertex
-        queryYWide <- denseReadPointY dense vertex
-        let !queryAngle =
-              pseudoAngle (hullCenterX hull) (hullCenterY hull) queryXWide queryYWide
-        edge <- hullCandidate dense hull hullIndex queryAngle queryXWide queryYWide
-        fromVertex <- denseReadOrigin dense edge
-        toVertex <- denseReadOrigin dense (edge `xor` 1)
-        fromX <- denseReadPointX dense fromVertex
-        fromY <- denseReadPointY dense fromVertex
-        toX <- denseReadPointX dense toVertex
-        toY <- denseReadPointY dense toVertex
-        if orient2dCoordinates fromX fromY toX toY queryXWide queryYWide == GT
-          then do
-            deferred <-
-              insertDeferred
-                reserved
-                cursor
-                dense
-                hull
-                hullIndex
-                candidateArena
-                edge
-                fromVertex
-                toVertex
-                vertex
-                queryXWide
-                queryYWide
-                queryAngle
-            case deferred of
-              DeferredInsertionFailure failure -> pure (Left failure)
-              DeferredInsertionSuccess !newFlips !newMaxDepth !nextCursor !nextArena !nextHullIndex ->
-                insertRemaining
-                  reserved
-                  nextCursor
-                  nextArena
-                  hull
-                  nextHullIndex
-                  ordered
-                  (index + 1)
-                  skippedCount
-                  (fastCount + 1)
-                  (flips + newFlips)
-                  (max maxDepth newMaxDepth)
-          else do
-            -- The radial order is consumed monotonically, so its processed
-            -- prefix is dead. Compact deferred vertices into that prefix
-            -- instead of allocating a second linear arena.
-            MUV.unsafeWrite arena skippedCount raw
-            insertRemaining reserved cursor candidateArena hull hullIndex ordered (index + 1) (skippedCount + 1) fastCount flips maxDepth
-
-  insertSkipped !count = go
-   where
-    go !index
-      | index >= count = pure (Right ())
-      | otherwise = do
-          vertex <- fromIntegral <$> MUV.unsafeRead arena index
-          inserted <- insertExistingVertex @'ProbeOff mutable operation vertex
-          case inserted of
-            Left failure -> pure (Left failure)
-            Right () -> go (index + 1)
-
--- | The hull centre: the centroid of the first inner face, in the widened
--- comparison format, computed exactly as @centroid@ states it.
-seedCentre
-  :: DenseMutableDcel s vertex directed undirected face
-  -> ST s (Double, Double)
-seedCentre dense = do
-  (e0, e1, e2) <- denseFaceEdges dense 1
-  o0 <- denseReadOrigin dense e0
-  o1 <- denseReadOrigin dense e1
-  o2 <- denseReadOrigin dense e2
-  x0 <- denseReadPointX dense o0
-  y0 <- denseReadPointY dense o0
-  x1 <- denseReadPointX dense o1
-  y1 <- denseReadPointY dense o1
-  x2 <- denseReadPointX dense o2
-  y2 <- denseReadPointY dense o2
-  pure (x0 + (x1 - x0) / 3 + (x2 - x0) / 3, y0 + (y1 - y0) / 3 + (y2 - y0) / 3)
-
--- | Index the authoritative outer cycle. The seed fan is star-shaped around
--- the first face's centroid — a collinear chain closed by its apex — so the
--- cycle is already the angular order the predecessor search assumes; what
--- remains is caching each edge's angle and anchoring the buckets.
-buildHull
-  :: DenseMutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Double
-  -> Double
-  -> ST s (Either BuildError (Hull s, HullIndex s))
-buildHull dense operation centerX centerY = do
-  let !mutable = denseMutableOwner dense
-  countResult <- collectOuterEdges dense operation
-  case countResult of
-    Left failure -> pure (Left failure)
-    Right count
-      | count <= 0 -> pure (Left CircleSweepHullEmpty)
-      | otherwise -> do
-          let !capacity = max (count + 4) (pointCapacity mutable + 8)
-          hullAngleByEdge <- MUV.new (halfEdgeCapacity mutable)
-          let hull =
-                Hull
-                  { hullCenterX = centerX
-                  , hullCenterY = centerY
-                  , hullBucketCapacity = capacity
-                  , hullAngleByEdge
-                  }
-          forM_ [0 .. count - 1] $ \index -> do
-            edge <- readScratch operation index
-            origin <- denseReadOrigin dense edge
-            x <- denseReadPointX dense origin
-            y <- denseReadPointY dense origin
-            MUV.unsafeWrite hullAngleByEdge edge (pseudoAngle centerX centerY x y)
-          hullIndex <- rebuildBuckets dense hull count (initialBucketCount count capacity)
-          pure (Right (hull, hullIndex))
-
-collectOuterEdges
-  :: DenseMutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> ST s (Either BuildError Int)
-collectOuterEdges dense operation = do
-  let !mutable = denseMutableOwner dense
-  start <- denseReadFaceEdge dense 0
-  if start < 0
-    then pure (Right 0)
-    else do
-      bound <- directedEdgeCount mutable
-      go (bound + 1) start start False 0
- where
-  go !remaining !start !edge !seen !count
-    | remaining <= 0 =
-        pure
-          ( Left
-              ( OuterCycleDidNotTerminate
-                  (DirectedEdgeId (fromIntegral start))
-                  (DirectedEdgeId (fromIntegral edge))
-                  count
-              )
-          )
-    | seen && edge == start = pure (Right count)
-    | otherwise = do
-        writeScratch operation count edge
-        following <- denseReadNext dense edge
-        go (remaining - 1) start following True (count + 1)
-
-initialBucketCount :: Int -> Int -> Int
-initialBucketCount active capacity =
-  min capacity (nextPowerOfTwo (max 8 ((active + 1) `quot` 2)))
-
-nextPowerOfTwo :: Int -> Int
-nextPowerOfTwo requested = go 1
- where
-  target = max 1 requested
-  go !value
-    | value >= target = value
-    | value > maxBound `quot` 2 = maxBound
-    | otherwise = go (value * 2)
-
-readAngle :: Hull s -> Int -> ST s Double
-readAngle hull edge = MUV.unsafeRead (hullAngleByEdge hull) edge
-{-# INLINE readAngle #-}
-
-rebuildBuckets :: DenseMutableDcel s vertex directed undirected face -> Hull s -> Int -> Int -> ST s (HullIndex s)
-rebuildBuckets dense hull active requested = do
-  let !count = max 1 (min (hullBucketCapacity hull) requested)
-  buckets <- MUV.replicate count noIndex
-  start <- denseReadFaceEdge dense 0
-  let go !remaining !edge
-        | remaining <= 0 = pure ()
-        | otherwise = do
-            following <- denseReadNext dense edge
-            angle <- readAngle hull edge
-            followingAngle <- readAngle hull following
-            writeBucketSegment buckets angle followingAngle edge
-            go (remaining - 1) following
-  when (active > 0 && start >= 0) (go active start)
-  pure (HullIndex buckets active)
-
-maybeGrowBuckets :: DenseMutableDcel s vertex directed undirected face -> Hull s -> HullIndex s -> ST s (HullIndex s)
-maybeGrowBuckets dense hull hullIndex@(HullIndex buckets active) = do
-  let !current = MUV.length buckets
-  if active > 2 * current && current < hullBucketCapacity hull
-    then rebuildBuckets dense hull active (min (hullBucketCapacity hull) (2 * current))
-    else pure hullIndex
-
-bucketFor :: Int -> Double -> Int
-bucketFor count angle =
-  min (count - 1) (max 0 (floor (angle * fromIntegral count * 0.25)))
-{-# INLINE bucketFor #-}
-
-ceilingBucketFor :: Int -> Double -> Int
-ceilingBucketFor count angle =
-  ceiling (angle * fromIntegral count * 0.25) `rem` count
-{-# INLINE ceilingBucketFor #-}
-
--- | Install the authoritative outer edge whose angular segment contains each
--- bucket boundary in the half-open clockwise arc from @fromAngle@ to
--- @toAngle@. Adjacency remains solely in the DCEL; this is the derived section
--- needed to land a lookup near that ring.
-writeBucketSegment
-  :: MUV.MVector s Word32
-  -> Double
-  -> Double
-  -> Int
-  -> ST s ()
-writeBucketSegment buckets fromAngle toAngle edge = do
-  let !count = MUV.length buckets
-      !fromBucket = ceilingBucketFor count fromAngle
-      !toBucket = ceilingBucketFor count toAngle
-      !packed = fromIntegral edge
-  case compare fromBucket toBucket of
-    LT -> MUV.set (MUV.unsafeSlice fromBucket (toBucket - fromBucket) buckets) packed
-    GT -> do
-      MUV.set (MUV.unsafeSlice fromBucket (count - fromBucket) buckets) packed
-      MUV.set (MUV.unsafeSlice 0 toBucket buckets) packed
-    EQ -> pure ()
-{-# INLINE writeBucketSegment #-}
-
--- | Whether an outer edge's key orders at or before the stated query key,
--- settled field by field without materializing either key.
-edgeAtMostAtAngle
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> Double
-  -> Double
-  -> Double
-  -> Double
-  -> Int
-  -> ST s Bool
-edgeAtMostAtAngle dense edge angle queryAngle queryX queryY tie =
-  case compare angle queryAngle of
-    LT -> pure True
-    GT -> pure False
-    EQ -> do
-      origin <- denseReadOrigin dense edge
-      x <- denseReadPointX dense origin
-      case compare x queryX of
-        LT -> pure True
-        GT -> pure False
-        EQ -> do
-          y <- denseReadPointY dense origin
-          case compare y queryY of
-            LT -> pure True
-            GT -> pure False
-            EQ -> pure (edge <= tie)
-{-# INLINE edgeAtMostAtAngle #-}
-
--- | Reconcile the derived angular index once after the local topology section
--- has glued. Replacing one outer edge by two adds one active edge; every closed
--- turn removes one. The final two edges cover the entire rewritten angular
--- arc, so their segments descend to the bucket view in one gluing step.
-finishHullRewrite
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Hull s
-  -> HullIndex s
-  -> Int
-  -> Int
-  -> Int
-  -> ST s (HullIndex s)
-finishHullRewrite dense hull (HullIndex buckets active) leftEdge rightEdge activeDelta = do
-  leftAngle <- readAngle hull leftEdge
-  middleAngle <- readAngle hull rightEdge
-  afterRight <- denseReadNext dense rightEdge
-  rightAngle <- readAngle hull afterRight
-  writeBucketSegment buckets leftAngle middleAngle leftEdge
-  writeBucketSegment buckets middleAngle rightAngle rightEdge
-  maybeGrowBuckets dense hull (HullIndex buckets (active + activeDelta))
-{-# INLINE finishHullRewrite #-}
-
--- | The outer edge whose key is the greatest key at or below the query: the
--- visible candidate the sweep inserts against. Bucket anchors land the walk
--- near the answer and the live outer cycle carries it the rest of the way.
-hullCandidate
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Hull s
-  -> HullIndex s
-  -> Double
-  -> Double
-  -> Double
-  -> ST s Int
-hullCandidate dense hull (HullIndex buckets active) queryAngle queryX queryY = do
-  let !count = MUV.length buckets
-      !bucket = bucketFor count queryAngle
-  raw <- MUV.unsafeRead buckets bucket
-  if raw == noIndex
-    then denseReadFaceEdge dense 0
-    else adjustFromBoundary bucket (unpackIndex raw)
- where
-  adjustFromBoundary !bucket !initial = do
-    initialAngle <- readAngle hull initial
-    initialAtMost <- cyclicAtMost bucket initial initialAngle
-    if initialAtMost
-      then advance active initial initialAngle
-      else retreat active initial
-   where
-    cyclicAtMost boundaryBucket edge angle =
-      if boundaryBucket == 0 && angle > queryAngle
-        then pure True
-        else edgeAtMostAtAngle dense edge angle queryAngle queryX queryY maxBound
-
-    advance !remaining !edge !edgeAngle
-      | remaining <= 0 = pure initial
-      | otherwise = do
-          following <- denseReadNext dense edge
-          followingAngle <- readAngle hull following
-          -- Only the bucket-zero anchor may precede the query by crossing the
-          -- angular seam. Once the walk leaves that anchor, ordinary key order
-          -- is authoritative; treating every high-angle edge as below a
-          -- bucket-zero query walks straight past the answer and around the
-          -- entire ring.
-          let crossesSeam = followingAngle < edgeAngle
-              seamPermitted = bucket == 0 && edgeAngle > queryAngle
-          followingAtMost <- edgeAtMostAtAngle dense following followingAngle queryAngle queryX queryY maxBound
-          if (not crossesSeam || seamPermitted) && followingAtMost
-            then advance (remaining - 1) following followingAngle
-            else pure edge
-
-    retreat !remaining !edge
-      | remaining <= 0 = pure initial
-      | otherwise = do
-          previous <- denseReadPrevious dense edge
-          previousAngle <- readAngle hull previous
-          previousAtMost <- cyclicAtMost bucket previous previousAngle
-          if previousAtMost
-            then advance remaining previous previousAngle
-            else retreat (remaining - 1) previous
-
-insertDeferred
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-  -> DenseMutableDcel s vertex directed undirected face
-  -> Hull s
-  -> HullIndex s
-  -> LegalizationArena s
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Double
-  -> Double
-  -> Double
-  -> ST s (DeferredInsertion s)
-insertDeferred reserved cursor dense hull hullIndex arena replacedEdge fromVertex toVertex vertex insertedX insertedY insertedAngle = do
-  inserted <-
-    insertOutsideHullAtEdge
-      reserved
-      cursor
-      arena
-      replacedEdge
-      fromVertex
-      toVertex
-      vertex
-  case inserted of
-    SweepInsertionFailure failure -> pure (DeferredInsertionFailure failure)
-    SweepInsertion firstOuterEdge lastOuterEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion ->
-      insertDeferredBetween
-        firstOuterEdge
-        lastOuterEdge
-        initialFlips
-        initialMaxDepth
-        arenaAfterInsertion
-        cursorAfterInsertion
- where
-  insertDeferredBetween firstEdge lastEdge initialFlips initialMaxDepth arenaAfterInsertion cursorAfterInsertion = do
-    -- The patch proves the new keys algebraically, so no key is rediscovered
-    -- from the mesh: replacing outer edge a->b with a->v, v->b keeps the
-    -- replaced edge's origin for the first spoke (its cached angle carries
-    -- over) and starts the last spoke at the inserted vertex, whose angle is the
-    -- one the candidate search was already given. The tie-break is each fresh
-    -- edge id itself.
-    replacedAngle <- readAngle hull replacedEdge
-    -- Both spokes join the outer cycle before either is indexed, and a bucket
-    -- rebuild during activation walks the live cycle: both angle slots must be
-    -- initialized before the first activation can read them.
-    MUV.unsafeWrite (hullAngleByEdge hull) firstEdge replacedAngle
-    MUV.unsafeWrite (hullAngleByEdge hull) lastEdge insertedAngle
-    -- Every turn this insertion closes legalizes together in one epoch. Closure
-    -- never deletes an edge and never touches the outer cycle, so which turns
-    -- close does not depend on when the interior is repaired; the fan above
-    -- keeps its own drain because its candidates are oriented against the
-    -- inserted vertex, and that star must still be intact when they are tested.
-    ClosedHullSection left closuresLeft topLeft arenaAfterLeft cursorAfterLeft <- closeLeft firstEdge 0 0 arenaAfterInsertion cursorAfterInsertion
-    ClosedHullSection right closuresRight topAll arenaAfterRight cursorAfterRight <- closeRight lastEdge 0 topLeft arenaAfterLeft cursorAfterLeft
-    let !closures = closuresLeft + closuresRight
-    nextHullIndex <- finishHullRewrite dense hull hullIndex left right (1 - closures)
-    (!flips, !maxDepth, !finalArena) <-
-      if topAll == 0
-        then pure (0, 0, arenaAfterRight)
-        else do
-          LegalizationDrain drainedFlips drainedMaxDepth () drainedArena <-
-            drainDenseUnconstrainedGenericLegalization dense arenaAfterRight topAll
-          pure (drainedFlips, drainedMaxDepth, drainedArena)
-    pure
-      ( DeferredInsertionSuccess
-          (initialFlips + flips)
-          (max initialMaxDepth maxDepth)
-          cursorAfterRight
-          finalArena
-          nextHullIndex
-      )
-
-  closeLeft !current !closures !top !sectionArena !sectionCursor = do
-    left <- denseReadPrevious dense current
-    close <- shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY left current
-    if not close
-      then pure (ClosedHullSection current closures top sectionArena sectionCursor)
-      else do
-        leftAngle <- readAngle hull left
-        (replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor left
-        -- Closing consecutive a->b, b->c into a->c keeps the first edge's
-        -- origin, so the replacement inherits its angle; the tie is the fresh
-        -- edge id. Both retired edges answer to the same left neighbour, the
-        -- edge now preceding the replacement on the outer cycle. Seeding
-        -- happens here, after the replacement's links exist.
-        MUV.unsafeWrite (hullAngleByEdge hull) replacement leftAngle
-        (nextArena, nextTop) <- seedGenericPairInArena sectionArena top left current
-        closeLeft replacement (closures + 1) nextTop nextArena nextCursor
-
-  closeRight !current !closures !top !sectionArena !sectionCursor = do
-    right <- denseReadNext dense current
-    close <- shouldCloseRightTurn dense hull insertedAngle insertedX insertedY current right
-    if not close
-      then pure (ClosedHullSection current closures top sectionArena sectionCursor)
-      else do
-        currentAngle <- readAngle hull current
-        (replacement, nextCursor) <- closeOuterTurnReserved reserved sectionCursor current
-        MUV.unsafeWrite (hullAngleByEdge hull) replacement currentAngle
-        (nextArena, nextTop) <- seedGenericPairInArena sectionArena top current right
-        closeRight replacement (closures + 1) nextTop nextArena nextCursor
-{-# INLINE insertDeferred #-}
-
--- | Test the left-hand turn where the inserted point is the target of the
--- second edge. Adjacency is not rediscovered: 'closeLeft' obtained @first@
--- from @previous second@ in the same local section.
-shouldCloseLeftTurn
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Hull s
-  -> Double
-  -> Double
-  -> Double
-  -> Int
-  -> Int
-  -> ST s Bool
-shouldCloseLeftTurn dense hull insertedAngle insertedX insertedY first second = do
-  fromVertex <- denseReadOrigin dense first
-  middleVertex <- denseReadOrigin dense (first `xor` 1)
-  fromX <- denseReadPointX dense fromVertex
-  fromY <- denseReadPointY dense fromVertex
-  middleX <- denseReadPointX dense middleVertex
-  middleY <- denseReadPointY dense middleVertex
-  middleAngle <- readAngle hull second
-  -- Same-ray/acute compatibility rejects most local sections.  Settle that
-  -- cheap obstruction before paying for the exact orientation predicate; the
-  -- conjunction is unchanged, only its evaluation order is less profligate.
-  if
-    middleAngle /= insertedAngle
-      && not (acuteAtMiddle fromX fromY middleX middleY insertedX insertedY)
-    then pure False
-    else pure (orient2dCoordinates fromX fromY middleX middleY insertedX insertedY == GT)
-{-# INLINE shouldCloseLeftTurn #-}
-
--- | The symmetric right-hand test, where the inserted point is the first
--- edge's source. 'closeRight' obtained @second@ from @next first@, so this
--- section likewise consumes that adjacency proof instead of reading it again.
-shouldCloseRightTurn
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Hull s
-  -> Double
-  -> Double
-  -> Double
-  -> Int
-  -> Int
-  -> ST s Bool
-shouldCloseRightTurn dense hull insertedAngle insertedX insertedY first second = do
-  middleVertex <- denseReadOrigin dense (first `xor` 1)
-  targetVertex <- denseReadOrigin dense (second `xor` 1)
-  middleX <- denseReadPointX dense middleVertex
-  middleY <- denseReadPointY dense middleVertex
-  targetX <- denseReadPointX dense targetVertex
-  targetY <- denseReadPointY dense targetVertex
-  middleAngle <- readAngle hull second
-  -- Symmetric to the left-hand descent above: compatibility first, exact
-  -- orientation only for sections that can actually glue.
-  if
-    middleAngle /= insertedAngle
-      && not (acuteAtMiddle insertedX insertedY middleX middleY targetX targetY)
-    then pure False
-    else pure (orient2dCoordinates insertedX insertedY middleX middleY targetX targetY == GT)
-{-# INLINE shouldCloseRightTurn #-}
-
--- Spade's deferred-convexity rule is local: close the turn when the angle at
--- the shared hull vertex is strictly below 90 degrees. Requiring the entire
--- triangle to be acute leaves avoidable star-hull work for the terminal pass.
-acuteAtMiddle :: Double -> Double -> Double -> Double -> Double -> Double -> Bool
-acuteAtMiddle ax ay bx by cx cy =
-  let !ux = ax - bx
-      !uy = ay - by
-      !vx = cx - bx
-      !vy = cy - by
-      !dot = ux * vx + uy * vy
-      !scale = max 1 (ux * ux + uy * uy + vx * vx + vy * vy)
-   in dot > 64 * encodeFloat 1 (-52) * scale
-{-# INLINE acuteAtMiddle #-}
-
--- Clockwise pseudo-angle in [0,4), matching the orientation of the outer-face
--- cycle.
-pseudoAngle :: Double -> Double -> Double -> Double -> Double
-pseudoAngle centerX centerY x y
-  | norm == 0 = 0
-  | raw >= 4 = 0
-  | otherwise = raw
- where
-  !dx = x - centerX
-  !dy = y - centerY
-  !norm = abs dx + abs dy
-  !projection = dx / norm
-  !raw = if dy > 0 then 1 + projection else 3 - projection
-{-# INLINE pseudoAngle #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Excision.hs b/src-build/Moonlight/Triangulation/Internal/Excision.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Excision.hs
+++ /dev/null
@@ -1,461 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Excision of a vertex from a thawed mesh: the removal kernel, stated over
--- the mutable arena and publishing nothing.
-module Moonlight.Triangulation.Internal.Excision
-  ( removeMutable
-  ) where
-
-import Control.Monad (forM, when)
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import Data.Foldable (traverse_)
-import qualified Data.IntSet as IntSet
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite (flipEdge)
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize
-  ( legalizeCavityFanScratch
-  , legalizeEdges
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , readScratch
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Internal.Predicates (orient2d)
-
--- | The proved outgoing section of one ordinary removal. Its edges live in the
--- operation scratch arena in counter-clockwise order; the record carries only
--- the section's extent and the first outer-face incidence, if any.
-data RemovalStar = RemovalStar {-# UNPACK #-} !Int !(Maybe Int)
-
-removeMutable
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-removeMutable mutable operation vertex = do
-  faces <- faceCount mutable
-  if faces <= 1
-    then removeDegenerate mutable vertex
-    else do
-      collected <- collectRemovalStar mutable operation vertex
-      case collected of
-        Left obstruction -> pure (Left obstruction)
-        Right (RemovalStar degree outerOutgoing) -> do
-          case outerOutgoing of
-            Nothing -> removeInterior mutable operation vertex degree
-            Just hullEdge -> removeHull mutable operation vertex hullEdge
-
-removeDegenerate
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-removeDegenerate mutable vertex = do
-  vertexCount <- pointCount mutable
-  case vertexCount of
-    0 ->
-      pure
-        (Left (RemovalEmptyTriangulation (VertexId (fromIntegral vertex))))
-    1 -> do
-      writeFaceEdge mutable 0 (-1)
-      swapRemoveVertex mutable vertex
-    2 -> do
-      collected <- collectOutgoing mutable vertex
-      case collected of
-        Left obstruction -> pure (Left obstruction)
-        Right outgoing ->
-          case outgoing of
-            [edge] -> do
-              _ <- clearConstraint mutable edge
-              removedEdge <- swapRemoveUndirectedEdge mutable (edge `quot` 2)
-              case removedEdge of
-                Left obstruction -> pure (Left obstruction)
-                Right () -> do
-                  let !other = if vertex == 0 then 1 else 0
-                  writeVertexOut mutable other (-1)
-                  writeFaceEdge mutable 0 (-1)
-                  swapRemoveVertex mutable vertex
-            _ ->
-              pure
-                ( Left
-                    ( RemovalTwoPointDegreeMismatch
-                        (VertexId (fromIntegral vertex))
-                        (length outgoing)
-                    )
-                )
-    _ -> removeCollinear mutable vertex
-
-removeCollinear
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-removeCollinear mutable vertex = do
-  collected <- collectOutgoing mutable vertex
-  case collected of
-    Left obstruction -> pure (Left obstruction)
-    Right outgoing ->
-      case outgoing of
-        [edge] -> do
-          let !reversedEdge = edge `xor` 1
-          target <- readOrigin mutable reversedEdge
-          edgeNext <- readNext mutable edge
-          writePrevious mutable edgeNext (edgeNext `xor` 1)
-          writeNext mutable (edgeNext `xor` 1) edgeNext
-          writeVertexOut mutable target edgeNext
-          writeFaceEdge mutable 0 edgeNext
-          _ <- clearConstraint mutable edge
-          removedEdge <- swapRemoveUndirectedEdge mutable (edge `quot` 2)
-          case removedEdge of
-            Left obstruction -> pure (Left obstruction)
-            Right () -> swapRemoveVertex mutable vertex
-        [edge1, edge2] -> do
-          let !t1 = edge1 `xor` 1
-              !t1Reverse = edge1
-              !t2 = edge2 `xor` 1
-          constrained1 <- readConstraint mutable edge1
-          constrained2 <- readConstraint mutable edge2
-          edge2Next <- readNext mutable edge2
-          edge2To <- readOrigin mutable t2
-          t2Previous <- readPrevious mutable t2
-          if edge2Next == t2
-            then do
-              writeNext mutable t1 t1Reverse
-              writePrevious mutable t1Reverse t1
-            else do
-              writePrevious mutable edge2Next t1
-              writeNext mutable t1 edge2Next
-              writeNext mutable t2Previous t1Reverse
-              writePrevious mutable t1Reverse t2Previous
-          writeVertexOut mutable edge2To t1Reverse
-          writeOrigin mutable t1Reverse edge2To
-          -- The two segments meeting at the removed vertex are welded into
-          -- one, and edge1's slot now spans both. It is neither of them.
-          resetEdgeData mutable (edge1 `quot` 2)
-          writeFaceEdge mutable 0 t1
-          _ <- clearConstraint mutable edge1
-          _ <- clearConstraint mutable edge2
-          when (constrained1 || constrained2) $ do
-            _ <- setConstraint mutable edge1
-            pure ()
-          removedVertex <- swapRemoveVertex mutable vertex
-          case removedVertex of
-            Left obstruction -> pure (Left obstruction)
-            Right result -> do
-              removedEdge <- swapRemoveUndirectedEdge mutable (edge2 `quot` 2)
-              pure (result <$ removedEdge)
-        _ ->
-          pure
-            ( Left
-                ( RemovalCollinearDegreeMismatch
-                    (VertexId (fromIntegral vertex))
-                    (length outgoing)
-                )
-            )
-
-removeInterior
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-removeInterior mutable operation vertex degree = do
-  traverse_ recordRing [0 .. degree - 1]
-  capacity <- ensureCellCapacity mutable (max 0 (degree - 3)) (max 0 (degree - 2))
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> do
-      -- Taken before the fan appends anything, so it separates the fan's own edges
-      -- from the cavity border exactly, the way spade's is_new_edge does.
-      !cavityFloor <- (`quot` 2) <$> directedEdgeCount mutable
-      remeshed <- remeshRingScratch mutable operation degree
-      case remeshed of
-        Left obstruction -> pure (Left obstruction)
-        Right newEdgeCount -> do
-          legalizeCavityFanScratch
-            mutable
-            operation
-            cavityFloor
-            (3 * degree)
-            newEdgeCount
-          edgesToRemove <-
-            traverse
-              (readScratch operation . (degree +))
-              [0 .. degree - 1]
-          facesToRemove <-
-            traverse
-              (readScratch operation . (2 * degree +))
-              [0 .. degree - 1]
-          cleaned <- cleanupEdgesAndFaces mutable edgesToRemove facesToRemove
-          case cleaned of
-            Left obstruction -> pure (Left obstruction)
-            Right () -> swapRemoveVertex mutable vertex
- where
-  recordRing index = do
-    edge <- readScratch operation index
-    following <- readNext mutable edge
-    face <- readFace mutable edge
-    writeScratch operation index following
-    writeScratch operation (degree + index) (edge `quot` 2)
-    writeScratch operation (2 * degree + index) face
-
--- | Fan the cavity a removal leaves. The border arrives in ring order, which is
--- the order the fan consumes it in.
-remeshRingScratch
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError Int)
-remeshRingScratch mutable operation degree
-  | degree < 3 = pure (Left (RemovalBorderTooShort degree))
-  | otherwise = do
-      inner0 <- readScratch operation 0
-      fanOrigin <- readOrigin mutable inner0
-      build fanOrigin 1 inner0 0
- where
-  build !fanOrigin !index !innerEdge !newEdgeCount
-    | index == degree - 2 = do
-      innerNext <- readScratch operation index
-      innerPrevious <- readScratch operation (index + 1)
-      newFace <- addFace mutable innerEdge
-      writeFace mutable innerEdge newFace
-      writeFace mutable innerPrevious newFace
-      writeFace mutable innerNext newFace
-      writeNext mutable innerEdge innerNext
-      writePrevious mutable innerNext innerEdge
-      writePrevious mutable innerEdge innerPrevious
-      writeNext mutable innerPrevious innerEdge
-      writePrevious mutable innerPrevious innerNext
-      writeNext mutable innerNext innerPrevious
-      previousOrigin <- readOrigin mutable innerPrevious
-      nextOrigin <- readOrigin mutable innerNext
-      writeVertexOut mutable previousOrigin innerPrevious
-      writeVertexOut mutable nextOrigin innerNext
-      writeVertexOut mutable fanOrigin innerEdge
-      pure (Right newEdgeCount)
-    | index < degree - 2 = do
-      outerEdge <- readScratch operation index
-      outerFrom <- readOrigin mutable outerEdge
-      outerTo <- readOrigin mutable (outerEdge `xor` 1)
-      (newEdge, newTwin) <- addEdge mutable outerTo fanOrigin
-      newFace <- addFace mutable newEdge
-      writeNext mutable newEdge innerEdge
-      writePrevious mutable newEdge outerEdge
-      writeFace mutable newEdge newFace
-      writeNext mutable newTwin 0
-      writePrevious mutable newTwin 0
-      writeFace mutable newTwin 0
-      writeFace mutable outerEdge newFace
-      writeNext mutable outerEdge newEdge
-      writePrevious mutable outerEdge innerEdge
-      writePrevious mutable innerEdge newEdge
-      writeNext mutable innerEdge outerEdge
-      writeFace mutable innerEdge newFace
-      writeFaceEdge mutable newFace newEdge
-      writeVertexOut mutable outerFrom outerEdge
-      writeScratch operation (3 * degree + newEdgeCount) newEdge
-      build
-        fanOrigin
-        (index + 1)
-        newTwin
-        (newEdgeCount + 1)
-    | otherwise =
-      pure (Left (RemovalBorderArityMismatch (degree - index)))
-
-removeHull :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-removeHull mutable operation vertex loopEnd = do
-  loopStart <- counterClockwiseMutable mutable loopEnd
-  loopEndNext <- readNext mutable loopEnd
-  collected <- collectConvexStrip loopEnd loopStart [] []
-  case collected of
-    Left obstruction -> pure (Left obstruction)
-    Right (!convexEdges, !edgesToValidate) -> do
-      let !strip = convexEdges ++ [loopEndNext]
-      (!edgesToRemove, !facesToRemove) <- disconnectStrip strip
-      legalizeEdges mutable operation edgesToValidate
-      cleaned <- cleanupEdgesAndFaces mutable edgesToRemove facesToRemove
-      case cleaned of
-        Left obstruction -> pure (Left obstruction)
-        Right () -> swapRemoveVertex mutable vertex
- where
-  collectConvexStrip !end !current !convexReversed !validate = do
-    nextCurrent <- counterClockwiseMutable mutable current
-    edge <- readNext mutable current
-    repaired <- repairConvexity (edge : convexReversed) validate
-    case repaired of
-      Left obstruction -> pure (Left obstruction)
-      Right (!repairedReversed, !validate') ->
-        if nextCurrent == end
-          then pure (Right (reverse repairedReversed, validate'))
-          else collectConvexStrip end nextCurrent repairedReversed validate'
-
-  repairConvexity !edgesReversed !validate =
-    case edgesReversed of
-      edge2 : edge1 : restReversed -> do
-        target <- readOrigin mutable (edge2 `xor` 1)
-        from <- edgeOriginPoint mutable edge1
-        to <- edgeOriginPoint mutable (edge1 `xor` 1)
-        targetPoint <- pointAt mutable target
-        if orient2d from to targetPoint == GT
-          then do
-            previousEdge <- readPrevious mutable edge2
-            let !toFlip = previousEdge `xor` 1
-            rewritten <- flipEdge mutable toFlip
-            case rewritten of
-              Left obstruction -> pure (Left obstruction)
-              Right () -> do
-                addCounter operation CounterEdgeFlips 1
-                repairConvexity (toFlip : restReversed) (toFlip : validate)
-          else pure (Right (edgesReversed, validate))
-      _ -> pure (Right (edgesReversed, validate))
-
-  disconnectStrip strip = do
-    removed <- forM strip $ \edge -> do
-      previousSpoke <- readPrevious mutable edge
-      face <- readFace mutable edge
-      from <- readOrigin mutable edge
-      ccw <- counterClockwiseMutable mutable edge
-      predecessor <- readPrevious mutable ccw
-      writeNext mutable predecessor edge
-      writePrevious mutable edge predecessor
-      writeFace mutable edge 0
-      writeFaceEdge mutable 0 edge
-      writeVertexOut mutable from edge
-      pure (previousSpoke `quot` 2, face)
-    pure (map fst removed, map snd removed)
-
-cleanupEdgesAndFaces :: MutableDcel s vertex directed undirected face -> [Int] -> [Int] -> ST s (Either BuildError ())
-cleanupEdgesAndFaces mutable rawEdges rawFaces = do
-  let !edges = sortUniqueDesc rawEdges
-      !faces = sortUniqueDesc (filter (> 0) rawFaces)
-  removedEdges <- traverseUntilFailure (swapRemoveUndirectedEdge mutable) edges
-  case removedEdges of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> traverseUntilFailure (swapRemoveFace mutable) faces
- where
-  traverseUntilFailure
-    :: (Int -> ST s (Either BuildError ()))
-    -> [Int]
-    -> ST s (Either BuildError ())
-  traverseUntilFailure action =
-    foldr
-      ( \item continuation -> do
-          outcome <- action item
-          case outcome of
-            Left obstruction -> pure (Left obstruction)
-            Right () -> continuation
-      )
-      (pure (Right ()))
-
--- | Descending, deduplicated. Both properties are load-bearing: swap-remove
--- must retire the high index first (a lower index shifts under it), and a
--- duplicated index would be retired twice.
---
--- A removal hands over its vertex's degree, which is small on ordinary meshes
--- and unbounded in the worst case, so the shape is chosen by size. Insertion
--- sort wins outright while the ring is short — measured 6.75 against
--- 8.24 KiB/removal for @IntSet@ on the n=10000 lane — and is quadratic, so a
--- high-degree ring goes to the ordered set that carries the asymptotics.
---
--- The lazy 'foldl' is deliberate: its accumulator is the output structure.
--- 'foldl'' forced each intermediate spine and measured 7.31 versus
--- 6.75 KiB/removal on the same lane.
-sortUniqueDesc :: [Int] -> [Int]
-sortUniqueDesc values
-  | exceedsInsertionRing values = IntSet.toDescList (IntSet.fromList values)
-  | otherwise = foldl insertUnique [] values
- where
-  insertUnique :: [Int] -> Int -> [Int]
-  insertUnique sorted value = go sorted
-   where
-    go [] = [value]
-    go (first : rest) = case compare value first of
-      GT -> value : first : rest
-      EQ -> first : rest
-      LT -> first : go rest
-
--- | Whether a ring is long enough to owe the ordered set its logarithm,
--- decided without measuring the whole list: the insertion path is chosen by
--- the prefix, never by a full traversal.
-exceedsInsertionRing :: [Int] -> Bool
-exceedsInsertionRing = not . null . drop insertionRingLimit
-
-insertionRingLimit :: Int
-insertionRingLimit = 32
-
-collectRemovalStar
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError RemovalStar)
-collectRemovalStar mutable operation vertex = do
-  start <- readVertexOut mutable vertex
-  if start < 0
-    then pure (Right (RemovalStar 0 Nothing))
-    else do
-      halfEdges <- directedEdgeCount mutable
-      let !budget = halfEdges + 1
-          go !remaining !current !seen !degree !outerEdge
-            | remaining <= 0 =
-                pure
-                  ( Left
-                      ( RemovalOutgoingCycleDidNotTerminate
-                          (VertexId (fromIntegral vertex))
-                          (DirectedEdgeId (fromIntegral current))
-                          budget
-                      )
-                  )
-            | seen && current == start =
-                pure (Right (RemovalStar degree outerEdge))
-            | otherwise = do
-                writeScratch operation degree current
-                face <- readFace mutable current
-                _ <- clearConstraint mutable current
-                previousEdge <- readPrevious mutable current
-                let !nextOuter =
-                      case outerEdge of
-                        Just edge -> Just edge
-                        Nothing
-                          | face == 0 -> Just current
-                          | otherwise -> Nothing
-                go
-                  (remaining - 1)
-                  (previousEdge `xor` 1)
-                  True
-                  (degree + 1)
-                  nextOuter
-      go budget start False 0 Nothing
-
-collectOutgoing
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError [Int])
-collectOutgoing mutable vertex = do
-  start <- readVertexOut mutable vertex
-  if start < 0
-    then pure (Right [])
-    else do
-      halfEdges <- directedEdgeCount mutable
-      let !budget = halfEdges + 1
-          go !remaining !current !seen !result
-            | remaining <= 0 =
-                pure
-                  ( Left
-                      ( RemovalOutgoingCycleDidNotTerminate
-                          (VertexId (fromIntegral vertex))
-                          (DirectedEdgeId (fromIntegral current))
-                          budget
-                      )
-                  )
-            | seen && current == start = pure (Right (reverse result))
-            | otherwise = do
-                previousEdge <- readPrevious mutable current
-                go (remaining - 1) (previousEdge `xor` 1) True (current : result)
-      go budget start False []
-
-counterClockwiseMutable :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-counterClockwiseMutable mutable edge = (`xor` 1) <$> readPrevious mutable edge
diff --git a/src-build/Moonlight/Triangulation/Internal/Join.hs b/src-build/Moonlight/Triangulation/Internal/Join.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Join.hs
+++ /dev/null
@@ -1,117 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The join of two triangulations: a Delaunay representative of the union of
--- their sites.
-module Moonlight.Triangulation.Internal.Join
-  ( joinNormalForm
-  , joinBalanced
-  ) where
-
-import Control.Monad (void)
-import Data.Foldable (traverse_)
-import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Triangulation.BulkLoad (empty)
-import Moonlight.Triangulation.Internal.Join.Plan
-  ( PairPlan (..)
-  , planPair
-  )
-import Moonlight.Triangulation.Internal.Join.Seam (executeSeam)
-import Moonlight.Triangulation.Internal.Join.SiteSet
-  ( SiteSet
-  , rebuildCanonicalSiteSet
-  , siteSetAssocs
-  , siteSetSize
-  )
-import Moonlight.Triangulation.JoinSemilattice
-  ( JoinSemilattice (joinAnnotations)
-  )
-import Moonlight.Triangulation.Internal.Session
-  ( insertAdmittedVertexAtCombining
-  , withLocalSession
-  )
-import Moonlight.Triangulation.Internal.Tournament
-  ( interpretTournament
-  , planTournament
-  )
-import Moonlight.Triangulation.Types
-
--- | A Delaunay representative of the union of two site sets.
---
--- Skewed operands descend through the existing local copy-on-write session, so
--- the larger operand's vertex handles and untouched pages survive. Comparable
--- operands may merge along a separating seam or rebuild from their combined
--- site set. Every schedule returns valid topology;
--- 'Moonlight.Triangulation.Canonical.canonicalize' is the
--- separate physical observation when construction-independent numbering is
--- required.
---
--- Empty and structurally identical operands return an existing value verbatim.
--- Algebraic agreement between all schedules is stated by equal canonical
--- observations; structural equality continues to describe exact resident
--- representation.
-joinNormalForm
-  :: JoinSemilattice annotation
-  => Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-joinNormalForm left right = executePairPlan (planPair left right) left right
-
-executePairPlan
-  :: JoinSemilattice annotation
-  => PairPlan annotation
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-executePairPlan pairPlan left right =
-  case pairPlan of
-    ReturnLeftOperand -> Right left
-    ReturnRightOperand -> Right right
-    InsertLeftIntoRight sites -> insertSites sites right
-    InsertRightIntoLeft sites -> insertSites sites left
-    MergeSeparated seamPlan -> executeSeam seamPlan left right
-    RebuildCanonicalUnion sites -> rebuildCanonicalSiteSet sites
-
-insertSites
-  :: JoinSemilattice annotation
-  => SiteSet annotation
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-insertSites sites base = do
-  ((), inserted, _) <-
-    withLocalSession
-      base
-      (siteSetSize sites)
-      ( traverse_
-          (\(point, annotation) ->
-             void (insertAdmittedVertexAtCombining joinAnnotations point annotation)
-          )
-          (siteSetAssocs sites)
-      )
-  pure inserted
-
--- | Combine by a balanced tournament rather than by a fold.
---
--- Associativity and commutativity make every bracketing canonically equivalent,
--- so this is a cost choice and not a semantic one. A left fold republishes an
--- accumulator that grows by one shard per step and so rebuilds @Θ(nk)@ sites
--- over @k@ shards; halving the list rebuilds @Θ(n log k)@.
---
--- The specialization below is load-bearing rather than decorative. Without it
--- this function is the only one on the path that stays polymorphic, and every
--- join in the tournament pays for a dictionary while a caller's own fold at a
--- known element type does not. That alone cost a factor of two and hid the
--- advantage this function exists for.
---
--- The tournament retains the binary operation's shortcut and annotation-gluing
--- semantics rather than inventing a second n-ary implementation.
-joinBalanced
-  :: JoinSemilattice annotation
-  => [Triangulation 'Unconstrained annotation () () ()]
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-joinBalanced [] = Right (empty unitElementDefaults)
-joinBalanced (first : rest) =
-  interpretTournament joinNormalForm (planTournament (first :| rest))
-{-# SPECIALIZE joinBalanced
-  :: [Triangulation 'Unconstrained () () () ()]
-  -> Either BuildError (Triangulation 'Unconstrained () () () ()) #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/Plan.hs b/src-build/Moonlight/Triangulation/Internal/Join/Plan.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Join/Plan.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | The sole physical planner for binary and n-ary joins. It derives exact
--- local compatibility facts, selects one schedule, and leaves execution to a
--- consumer; sequential and concurrent interpreters share the same tournament
--- tree rather than inventing pairing policies of their own.
-module Moonlight.Triangulation.Internal.Join.Plan
-  ( PairPlan (..)
-  , planPair
-  ) where
-
-import Moonlight.Triangulation.Dcel (numVertices)
-import Moonlight.Triangulation.Internal.BulkLoad (empty)
-import Moonlight.Triangulation.Internal.Join.Seam (SeamPlan, planSeam)
-import Moonlight.Triangulation.Internal.Join.SiteSet
-  ( SiteSet
-  , siteSetFromTriangulation
-  , siteSetRelation
-  , siteSetUnionWith
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.JoinSemilattice (JoinSemilattice (joinAnnotations))
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Unconstrained)
-  , SiteRelation (..)
-  , unitElementDefaults
-  )
-
-data PairPlan annotation
-  = ReturnLeftOperand
-  | ReturnRightOperand
-  | InsertLeftIntoRight !(SiteSet annotation)
-  | InsertRightIntoLeft !(SiteSet annotation)
-  | MergeSeparated !SeamPlan
-  | RebuildCanonicalUnion !(SiteSet annotation)
-
--- | Stage cheap facts before exact set classification. Empty and structurally
--- identical operands return verbatim; skewed pairs preserve the larger value
--- through local insertion before seam planning is considered.
-planPair
-  :: JoinSemilattice annotation
-  => Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> PairPlan annotation
-planPair left right
-  -- Two zero-site values may differ structurally; the package-owned empty is
-  -- the identity, so preserve the other representative when only one is it.
-  | leftCount == 0 && rightCount == 0 =
-      if left == empty unitElementDefaults
-        then ReturnRightOperand
-        else ReturnLeftOperand
-  | leftCount == 0 = ReturnRightOperand
-  | rightCount == 0 = ReturnLeftOperand
-  | left == right = ReturnLeftOperand
-  | insertionIsCheaper leftCount rightCount = InsertLeftIntoRight leftSites
-  | insertionIsCheaper rightCount leftCount = InsertRightIntoLeft rightSites
-  | Just seamPlan <- planSeam left right = MergeSeparated seamPlan
-  | otherwise =
-      case siteSetRelation leftSites rightSites of
-        EqualSites -> InsertLeftIntoRight leftSites
-        LeftProperSubset -> InsertLeftIntoRight leftSites
-        RightProperSubset -> InsertRightIntoLeft rightSites
-        DisjointSites -> rebuildUnion
-        PartialOverlap _ -> rebuildUnion
- where
-  !leftCount = numVertices left
-  !rightCount = numVertices right
-  leftSites = siteSetFromTriangulation left
-  rightSites = siteSetFromTriangulation right
-  rebuildUnion =
-    RebuildCanonicalUnion
-      (siteSetUnionWith joinAnnotations leftSites rightSites)
-
--- A transaction reuses an existing topology only when the added side is small
--- enough that its expected local cavities beat one bulk sweep. This is an
--- internal cost estimate, deliberately not a caller-controlled threshold.
-insertionIsCheaper :: Int -> Int -> Bool
-insertionIsCheaper addition base = addition <= 64 || addition <= base `quot` 8
-{-# INLINE insertionIsCheaper #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs b/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Join/Seam.hs
+++ /dev/null
@@ -1,1395 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Linear seam construction for separated Delaunay triangulations. Admission
--- returns an opaque proof carrying the exact source order and tangents; the
--- executor therefore has no untyped precondition and owns no fallback.
-module Moonlight.Triangulation.Internal.Join.Seam
-  ( SeamPlan
-  , planSeam
-  , executeSeam
-  , ConstrainedSeamExecution
-  , seamExecutionTriangulation
-  , seamExecutionBuildStats
-  , seamExecutionPublicationStats
-  , seamExecutionCachedFrontierPointReads
-  , seamExecutionLeftFaceCount
-  , seamExecutionRightFaceEvidence
-  , seamExecutionJoinFaces
-  , executeConstrainedSeam
-  ) where
-
-import Control.Applicative ((<|>))
-import Control.Monad.ST (ST, runST)
-import Data.Bits (finiteBitSize, xor)
-import Data.Foldable (traverse_)
-import qualified Data.IntSet as IntSet
-import qualified Data.List as List
-import qualified Data.Sequence as Seq
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Dcel
-  ( adjacentEdge
-  , faceDirectedEdges
-  , faceVertices
-  , incidentFace
-  , isConstraintEdge
-  , numDirectedEdges
-  , numFaces
-  , numInnerFaces
-  , numVertices
-  , origin
-  , previous
-  , vertexOutEdge
-  , vertexData
-  )
-import Moonlight.Triangulation.Internal.FloodFillIterator (floodFillFacesWithRejectedEdges)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId (..)
-  , FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  , directedPair
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Hull (closeOuterTurn)
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize
-  ( legalizeEdges
-  , legalizeEdgesPinned
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Subdivide
-  ( splitBoundaryEdgeWithLegalization
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRule (illegalDiagonal)
-import Moonlight.Triangulation.Internal.Cdt.Combinators (bindMutable)
-import Moonlight.Triangulation.Internal.Cdt.Query (constraintEdges)
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (CounterSteinerPoints)
-  , OperationState
-  , addCounter
-  , freezeBuildStats
-  , newOperationState
-  )
-import Moonlight.Triangulation.Internal.Paged
-  ( PublicationStats
-  , TransactionShape (LocalTransaction)
-  , pagedUnsafeIndex
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( SeamFrontierIndex (..)
-  , Triangulation (..)
-  , seamQuarterTurn
-  , summarizeSeamChart
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types
-  ( ConstrainedSeamFaceEvidence (..)
-  , ConstrainedSeamSide (..)
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( BuildError (..)
-  , BuildStats
-  , ConstraintMode (Constrained, Unconstrained)
-  , InvariantViolation (..)
-  , Point (..)
-  , QueryPoint (queryPointValue)
-  , RefinementParameters (..)
-  , unitElementDefaults
-  )
-import Moonlight.Triangulation.Internal.Predicates
-  ( inCircleCoordinates
-  , orient2dCoordinates
-  )
-import Moonlight.Triangulation.Math (distance, midpoint, validatePoint)
-import Moonlight.Triangulation.Internal.Transaction (runTransactionWithPublication)
-
--- | Proof that a particular pair can be copied and stitched by the seam
--- kernel. Constructors stay private so an unseparated pair cannot be handed to
--- the executor by accident.
-data SeamPlan
-  = -- | The operands are already in geometric left/right order.  The two
-    -- retained frontiers are therefore the exact ones consumed by the zipper.
-    SeamLeftBeforeRight !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
-  | -- | The input operands are reversed, but the retained frontiers remain in
-    -- geometric left/right order rather than input-operand order.
-    SeamRightBeforeLeft !SeamTangents !SeamFrontierIndex !SeamFrontierIndex
-
--- | Coordinate chart used by the separated seam.  The second chart is the
--- orientation-preserving quarter turn @(u,v) = (y,-x)@; exact predicates and
--- the zipper therefore remain unchanged while north/south admission reuses
--- the same tangent interpreter.
-data SeamAxis
-  = SeamAxisX
-  | SeamAxisY
-  deriving stock (Eq, Show)
-
-planSeam
-  :: Triangulation mode vertex () () ()
-  -> Triangulation mode' vertex () () ()
-  -> Maybe SeamPlan
-planSeam left right = do
-  leftFrontier <- triSeamFrontier left
-  rightFrontier <- triSeamFrontier right
-  planAxis SeamAxisX leftFrontier rightFrontier
-    <|> planAxis SeamAxisY leftFrontier rightFrontier
- where
-  planAxis axis leftFrontier rightFrontier =
-    case separatedOrder axis left right leftFrontier rightFrontier of
-      Just LeftBeforeRight ->
-        (\tangents -> SeamLeftBeforeRight tangents leftFrontier rightFrontier)
-          <$> seamTangents axis left leftFrontier right rightFrontier
-      Just RightBeforeLeft ->
-        (\tangents -> SeamRightBeforeLeft tangents rightFrontier leftFrontier)
-          <$> seamTangents axis right rightFrontier left leftFrontier
-      Nothing -> Nothing
-
--- | Execute a proved seam schedule. Numbering follows the schedule;
--- 'Moonlight.Triangulation.Dcel.canonicalize' remains the explicit
--- construction-independent observation.
-executeSeam
-  :: forall vertex
-   . SeamPlan
-  -> Triangulation 'Unconstrained vertex () () ()
-  -> Triangulation 'Unconstrained vertex () () ()
-  -> Either BuildError (Triangulation 'Unconstrained vertex () () ())
-executeSeam plan left right =
-  case plan of
-    SeamLeftBeforeRight tangents _ _ ->
-      fmap fst (mergeSeparated left right tangents)
-    SeamRightBeforeLeft tangents _ _ ->
-      fmap fst (mergeSeparated right left tangents)
-
--- | Result of the constrained seam kernel. Constraint flags are copied before
--- legalization, so source contour edges are immutable barriers while the
--- zipper constructs only the missing corridor.
-data ConstrainedSeamExecution vertex = ConstrainedSeamExecution
-  { seamExecutionTriangulation
-      :: !(Triangulation 'Constrained vertex () () ())
-  , seamExecutionBuildStats :: !BuildStats
-  , seamExecutionPublicationStats :: !PublicationStats
-  , seamExecutionCachedFrontierPointReads :: {-# UNPACK #-} !Int
-  , seamExecutionLeftFaceCount :: {-# UNPACK #-} !Int
-  , seamExecutionRightFaceEvidence :: !(V.Vector ConstrainedSeamFaceEvidence)
-  , seamExecutionJoinFaces :: !(V.Vector FaceId)
-  }
-
--- | Execute a proved seam while transporting both source constraint planes.
--- This is distinct from promoting the unconstrained result afterward: source
--- hull constraints must already be visible to seam legalization or the
--- legalization schedule could erase a solved source face before recovery had
--- a chance to mark it.
-executeConstrainedSeam
-  :: forall vertex
-   . (ConstrainedSeamSide -> FaceId -> Bool)
-  -> RefinementParameters
-  -> vertex
-  -> SeamPlan
-  -> Triangulation 'Constrained vertex () () ()
-  -> Triangulation 'Constrained vertex () () ()
-  -> Either BuildError (ConstrainedSeamExecution vertex)
-executeConstrainedSeam sourceFacePreserved parameters vertexDefault plan left right = do
-  let (order, tangents, leftFrontier, rightFrontier) =
-        case plan of
-          SeamLeftBeforeRight selectedTangents selectedLeftFrontier selectedRightFrontier ->
-            (GeometricBaseLeft, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
-          SeamRightBeforeLeft selectedTangents selectedLeftFrontier selectedRightFrontier ->
-            (GeometricBaseRight, selectedTangents, selectedLeftFrontier, selectedRightFrontier)
-  (triangulation, statistics, publicationStats, joinFaces) <-
-    executeResidentConstrained
-      targetFaceProtected
-      parameters
-      vertexDefault
-      order
-      left
-      right
-      leftFrontier
-      rightFrontier
-      tangents
-  rightEvidence <-
-    seamFaceEvidenceForSource
-      right
-      incomingFaceOffset
-      rightEvidenceFaces
-  pure
-    ConstrainedSeamExecution
-      { seamExecutionTriangulation = triangulation
-      , seamExecutionBuildStats = statistics
-      , seamExecutionPublicationStats = publicationStats
-      , seamExecutionCachedFrontierPointReads =
-          seamFrontierPointReads tangents
-            + case refineMaxEdgeLength parameters of
-              Nothing -> 0
-              Just _ -> 4
-      , seamExecutionLeftFaceCount = numInnerFaces left
-      , seamExecutionRightFaceEvidence = rightEvidence
-      , seamExecutionJoinFaces = joinFaces
-      }
- where
-  !residentFaceLimit = numFaces left
-  !incomingFaceOffset = residentFaceLimit - 1
-  !incomingFaceLimit = incomingFaceOffset + numFaces right
-
-  targetFaceProtected rawFace
-    | rawFace <= 0 = False
-    | rawFace < residentFaceLimit =
-        sourceFacePreserved SeamResident (FaceId (fromIntegral rawFace))
-    | rawFace < incomingFaceLimit =
-        sourceFacePreserved
-          SeamIncoming
-          (FaceId (fromIntegral (rawFace - incomingFaceOffset)))
-    | otherwise = False
-
-  rightEvidenceFaces =
-    filter
-      (sourceFacePreserved SeamIncoming)
-      (fmap (FaceId . fromIntegral) [1 .. numFaces right - 1])
-
--- | Descend exactly the unprotected face component touched by the zipper.
--- This is the authoritative J section: it contains both newly allocated seam
--- faces and any source exterior filler lawfully retriangulated into them.
-exactJoinFaces
-  :: Triangulation 'Constrained vertex () () ()
-  -> (Int -> Bool)
-  -> V.Vector FaceId
-  -> (V.Vector FaceId, [UndirectedEdgeId])
-exactJoinFaces triangulation targetFaceProtected seeds =
-  let (faces, rejectedPairs) =
-        floodFillFacesWithRejectedEdges
-          triangulation
-          (V.toList seeds)
-          canCross
-   in (V.fromList faces, rejectedPairs)
- where
-  canCross edge =
-    not (isConstraintEdge triangulation edge)
-      && not
-        ( any
-            targetFaceIsProtected
-            (incidentInnerFaces triangulation edge)
-        )
-
-  targetFaceIsProtected (FaceId raw) =
-    targetFaceProtected (fromIntegral raw)
-
--- | Certify only the J/selected-face overlap after local descent. A selected
--- source face is immutable; therefore an unconstrained overlap edge that still
--- requires a Delaunay flip is a genuine incompatibility, never permission to
--- rewrite the solved section.
-certifyJoinBoundary
-  :: Triangulation 'Constrained vertex () () ()
-  -> [UndirectedEdgeId]
-  -> Either BuildError ()
-certifyJoinBoundary triangulation = traverse_ certifyPair
- where
-  certifyPair edge =
-    if isConstraintEdge triangulation edge
-          || not (immutableEdgeRequiresFlip triangulation edge)
-          then Right ()
-          else Left (SeamSourceEdgeRequiresFlip edge)
-
-incidentInnerFaces
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> [FaceId]
-incidentInnerFaces triangulation edge =
-  filter (/= FaceId 0)
-    [ incidentFace triangulation forward
-    , incidentFace triangulation backward
-    ]
- where
-  (forward, backward) = directedPair edge
-
-immutableEdgeRequiresFlip
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> Bool
-immutableEdgeRequiresFlip triangulation edge =
-  leftFace /= FaceId 0
-    && rightFace /= FaceId 0
-    && orient2dCoordinates cx cy dx dy bx by == GT
-    && orient2dCoordinates dx dy cx cy ax ay == GT
-    && illegalDiagonal ax ay bx by cx cy dx dy
- where
-  (forward, backward) = directedPair edge
-  leftFace = incidentFace triangulation forward
-  rightFace = incidentFace triangulation backward
-  forwardPrevious = previous triangulation forward
-  backwardPrevious = previous triangulation backward
-  Point ax ay = vertexPointAt triangulation (origin triangulation forward)
-  Point bx by = vertexPointAt triangulation (origin triangulation backward)
-  Point cx cy = vertexPointAt triangulation (origin triangulation forwardPrevious)
-  Point dx dy = vertexPointAt triangulation (origin triangulation backwardPrevious)
-
-vertexPointAt
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> Point
-vertexPointAt triangulation (VertexId raw) =
-  Point
-    (triPointX triangulation `pagedUnsafeIndex` fromIntegral raw)
-    (triPointY triangulation `pagedUnsafeIndex` fromIntegral raw)
-
-data GeometricBaseOrder
-  = GeometricBaseLeft
-  | GeometricBaseRight
-  deriving stock (Eq, Show)
-
--- | Finite local work required to turn the two synthetic perimeter bridges
--- into edge-bounded constrained paths.  The witness is derived while the
--- tangent endpoints still identify J exactly; after publication those edges
--- are ordinary constraints and must not be rediscovered by a global scan.
-data SeamBridgeSubdivision = SeamBridgeSubdivision
-  { seamLowerBridgeDepth :: {-# UNPACK #-} !Int
-  , seamUpperBridgeDepth :: {-# UNPACK #-} !Int
-  , seamBridgeAddedVertices :: {-# UNPACK #-} !Int
-  }
-
-planSeamBridgeSubdivision
-  :: RefinementParameters
-  -> Point
-  -> Point
-  -> Point
-  -> Point
-  -> Either BuildError SeamBridgeSubdivision
-planSeamBridgeSubdivision parameters lowerLeft lowerRight upperLeft upperRight =
-  case refineMaxEdgeLength parameters of
-    Nothing -> Right (SeamBridgeSubdivision 0 0 0)
-    Just maximumLength -> do
-      let !lowerDepth = requiredBridgeSubdivisionDepth maximumLength lowerLeft lowerRight
-          !upperDepth = requiredBridgeSubdivisionDepth maximumLength upperLeft upperRight
-          !required =
-            subdivisionVertexCount lowerDepth
-              + subdivisionVertexCount upperDepth
-      if required > toInteger (maxBound :: Int)
-        then Left (CapacityExceeded maxBound)
-        else do
-          let !requiredVertices = fromInteger required
-          available <-
-            case (requiredVertices, refineMaxAdditionalVertices parameters) of
-              (0, Nothing) -> Right 0
-              (_, Nothing) -> Left RefinementDomainRequiresFiniteVertexBudget
-              (_, Just budget) -> Right budget
-          if requiredVertices <= available
-            then
-              Right
-                SeamBridgeSubdivision
-                  { seamLowerBridgeDepth = lowerDepth
-                  , seamUpperBridgeDepth = upperDepth
-                  , seamBridgeAddedVertices = requiredVertices
-                  }
-            else
-              Left
-                ( RefinementSeamBridgeBudgetExceeded
-                    requiredVertices
-                    available
-                )
- where
-  subdivisionVertexCount :: Int -> Integer
-  subdivisionVertexCount depth = (2 :: Integer) ^ depth - 1
-
-requiredBridgeSubdivisionDepth :: Double -> Point -> Point -> Int
-requiredBridgeSubdivisionDepth maximumLength from to =
-  descend 0 (distance from to)
- where
-  descend !depth !currentLength
-    | currentLength <= maximumLength = depth
-    | depth >= finiteBitSize (0 :: Int) = depth
-    | otherwise = descend (depth + 1) (0.5 * currentLength)
-
-vertexPointForOuterEdge
-  :: Triangulation mode vertex directed undirected face
-  -> Int
-  -> Point
-vertexPointForOuterEdge triangulation edge =
-  vertexPointAt
-    triangulation
-    (VertexId (fromIntegral (topologyAt triangulation (4 * edge))))
-
-subdivideSeamBridge
-  :: forall s vertex directed undirected face
-   . (Int -> Bool)
-  -> vertex
-  -> Int
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError (Seq.Seq Int))
-subdivideSeamBridge targetFaceProtected vertexDefault = descend
- where
-  descend
-    :: Int
-    -> MutableDcel s vertex directed undirected face
-    -> OperationState s
-    -> Int
-    -> ST s (Either BuildError (Seq.Seq Int))
-  descend depth mutable operation outerEdge
-    | depth <= 0 = pure (Right (Seq.singleton outerEdge))
-    | otherwise = do
-        fromVertex <- readOrigin mutable outerEdge
-        toVertex <- readOrigin mutable (outerEdge `xor` 1)
-        fromPoint <- pointAt mutable fromVertex
-        toPoint <- pointAt mutable toVertex
-        case validatePoint Nothing (midpoint fromPoint toPoint) of
-          Left obstruction -> pure (Left obstruction)
-          Right admitted ->
-            let !splitPoint = queryPointValue admitted
-             in if splitPoint == fromPoint || splitPoint == toPoint
-                  then
-                    pure
-                      ( Left
-                          ( RefinementSeamBridgeMidpointCollapsed
-                              (UndirectedEdgeId (fromIntegral (outerEdge `quot` 2)))
-                          )
-                      )
-                  else do
-                    vertex <- appendVertex mutable splitPoint vertexDefault
-                    splitBoundaryEdgeWithLegalization
-                      (\target targetOperation _ firstCandidate secondCandidate ->
-                         legalizeEdgesPinned
-                           targetFaceProtected
-                           target
-                           targetOperation
-                           [firstCandidate, secondCandidate]
-                      )
-                      mutable
-                      operation
-                      outerEdge
-                      vertex
-                      `bindMutable` \(firstOuter, secondOuter) -> do
-                        addCounter operation CounterSteinerPoints 1
-                        descend (depth - 1) mutable operation firstOuter
-                          `bindMutable` \firstChain ->
-                            fmap (fmap (firstChain Seq.><))
-                              (descend (depth - 1) mutable operation secondOuter)
-
-executeResidentConstrained
-  :: forall vertex
-   . (Int -> Bool)
-  -> RefinementParameters
-  -> vertex
-  -> GeometricBaseOrder
-  -> Triangulation 'Constrained vertex () () ()
-  -> Triangulation 'Constrained vertex () () ()
-  -> SeamFrontierIndex
-  -> SeamFrontierIndex
-  -> SeamTangents
-  -> Either BuildError
-      ( Triangulation 'Constrained vertex () () ()
-      , BuildStats
-      , PublicationStats
-      , V.Vector FaceId
-      )
-executeResidentConstrained targetFaceProtected parameters vertexDefault order base extension leftFrontier rightFrontier tangents = do
-  let !baseDirected = numDirectedEdges base
-      !extensionDirected = numDirectedEdges extension
-      !baseVertices = numVertices base
-      (leftSource, rightSource, leftVertexOffset, rightVertexOffset, leftEdgeOffset, rightEdgeOffset) =
-        case order of
-          GeometricBaseLeft ->
-            (base, extension, 0, baseVertices, 0, baseDirected)
-          GeometricBaseRight ->
-            (extension, base, baseVertices, 0, baseDirected, 0)
-      SeamTangents
-        { seamLowerLeft = SeamTangent _ lowerLeft
-        , seamLowerRight = SeamTangent _ lowerRight
-        , seamUpperLeft = SeamTangent _ upperLeft
-        , seamUpperRight = SeamTangent _ upperRight
-        } = tangents
-  bridgePlan <-
-    planSeamBridgeSubdivision
-      parameters
-      (vertexPointForOuterEdge leftSource lowerLeft)
-      (vertexPointForOuterEdge rightSource lowerRight)
-      (vertexPointForOuterEdge leftSource upperLeft)
-      (vertexPointForOuterEdge rightSource upperRight)
-  let !extensionVertices = numVertices extension
-      !bridgeVertices = seamBridgeAddedVertices bridgePlan
-  transactionAdditional <-
-    if bridgeVertices > maxBound - extensionVertices
-      then Left (CapacityExceeded maxBound)
-      else Right (extensionVertices + bridgeVertices)
-  ((newFaces, progress, upperBridgeChain, lowerBridgeChain), triangulation, statistics, publicationStats) <-
-    runTransactionWithPublication
-      id
-      LocalTransaction
-      base
-      transactionAdditional
-      (\mutable operation -> do
-         appendSourceVertices mutable extension
-         _ <- addEdgeBlock mutable (extensionDirected `quot` 2)
-         _ <- addFaceBlock mutable (numFaces extension - 1)
-         copySource
-           mutable
-           extension
-           baseVertices
-           baseDirected
-           (numFaces base - 1)
-         copySourceConstraints mutable baseDirected extension
-         seamBase <-
-           spliceLowerTangent
-             mutable
-             rightEdgeOffset
-             (leftEdgeOffset + lowerLeft)
-             lowerRight
-         -- The two synthetic hull bridges are the certified perimeter of the
-         -- published world.  Once a later extension consumes either bridge it
-         -- becomes Γ, so fixing it now preserves the prior solve without a
-         -- retrospective source rewrite.
-         _ <- setConstraint mutable seamBase
-         stitchSeamPinned
-           targetFaceProtected
-           mutable
-           operation
-           (Seq.length (seamFrontierEdges leftFrontier))
-           (tangentIndex (seamLowerLeft tangents))
-           (Seq.length (seamFrontierEdges rightFrontier))
-           (tangentIndex (seamLowerRight tangents))
-           seamBase
-           (leftVertexOffset + topologyAt leftSource (4 * upperLeft))
-           (rightVertexOffset + topologyAt rightSource (4 * upperRight))
-           []
-           []
-           `bindMutable` \(upperBridge, newFaces, progress) -> do
-             _ <- setConstraint mutable upperBridge
-             subdivideSeamBridge
-               targetFaceProtected
-               vertexDefault
-               (seamLowerBridgeDepth bridgePlan)
-               mutable
-               operation
-               (seamBase `xor` 1)
-               `bindMutable` \lowerBridgeChain ->
-                 subdivideSeamBridge
-                   targetFaceProtected
-                   vertexDefault
-                   (seamUpperBridgeDepth bridgePlan)
-                   mutable
-                   operation
-                   upperBridge
-                   `bindMutable` \upperBridgeChain ->
-                     case Seq.lookup 0 upperBridgeChain of
-                       Nothing -> pure (Left SeamFrontierUnavailable)
-                       Just frontierStart -> do
-                         writeFaceEdge mutable 0 frontierStart
-                         pure
-                           ( Right
-                               (newFaces, progress, upperBridgeChain, lowerBridgeChain)
-                           )
-      )
-  let newFaceSeeds = V.fromList (fmap (FaceId . fromIntegral) newFaces)
-      (joinFaces, rejectedBoundaryPairs) =
-        exactJoinFaces triangulation targetFaceProtected newFaceSeeds
-  certifyJoinBoundary triangulation rejectedBoundaryPairs
-  frontier <-
-    combineSeamFrontier
-      triangulation
-      leftFrontier
-      rightFrontier
-      leftEdgeOffset
-      rightEdgeOffset
-      upperBridgeChain
-      lowerBridgeChain
-      progress
-  let published :: Triangulation 'Constrained vertex () () ()
-      published = triangulation{triSeamFrontier = Just frontier}
-  pure
-    ( published
-    , statistics
-    , publicationStats
-    , joinFaces
-    )
-
-seamFaceEvidenceForSource
-  :: Triangulation 'Constrained vertex () () ()
-  -> Int
-  -> [FaceId]
-  -> Either BuildError (V.Vector ConstrainedSeamFaceEvidence)
-seamFaceEvidenceForSource source targetOffset sourceFaces =
-  V.fromList <$> traverse evidenceFor sourceFaces
- where
-  evidenceFor sourceFace = do
-    (first, second, third) <- facePoints source sourceFace
-    let targetFace = FaceId (unFaceId sourceFace + fromIntegral targetOffset)
-    Right
-      ConstrainedSeamFaceEvidence
-        { constrainedSeamSourceFace = sourceFace
-        , constrainedSeamTargetFace = targetFace
-        , constrainedSeamFaceFirstPoint = first
-        , constrainedSeamFaceSecondPoint = second
-        , constrainedSeamFaceThirdPoint = third
-        }
-
-  facePoints
-    :: Triangulation mode sourceVertex directed undirected face
-    -> FaceId
-    -> Either BuildError (Point, Point, Point)
-  facePoints triangulation faceHandle =
-    case List.sort (fmap (vertexPointAt triangulation) (faceVertices triangulation faceHandle)) of
-      [first, second, third] -> Right (first, second, third)
-      _ -> Left (faceCardinalityFailure triangulation faceHandle)
-
-  faceCardinalityFailure
-    :: Triangulation mode sourceVertex directed undirected face
-    -> FaceId
-    -> BuildError
-  faceCardinalityFailure triangulation faceHandle =
-    case faceDirectedEdges triangulation faceHandle of
-      edge : _ -> RefinementInputTopologyInvalid (InnerFaceNotTriangularAtEdge edge)
-      [] -> RefinementInputTopologyInvalid (FaceMissingAdjacentEdge faceHandle)
-
-data SeparatedOrder
-  = LeftBeforeRight
-  | RightBeforeLeft
-
-separatedOrder
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode' vertex' directed' undirected' face'
-  -> SeamFrontierIndex
-  -> SeamFrontierIndex
-  -> Maybe SeparatedOrder
-separatedOrder axis left right leftFrontier rightFrontier
-  | numInnerFaces left <= 0 || numInnerFaces right <= 0 = Nothing
-  | chartMaximum axis leftFrontier < chartMinimum axis rightFrontier =
-      Just LeftBeforeRight
-  | chartMaximum axis rightFrontier < chartMinimum axis leftFrontier =
-      Just RightBeforeLeft
-  | otherwise = Nothing
-
-chartMinimum :: SeamAxis -> SeamFrontierIndex -> Double
-chartMinimum axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierXMinimum frontier
-    SeamAxisY -> seamFrontierYMinimum frontier
-
-chartMaximum :: SeamAxis -> SeamFrontierIndex -> Double
-chartMaximum axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierXMaximum frontier
-    SeamAxisY -> seamFrontierYMaximum frontier
-
--- The opposite-sign branch cannot overflow in its sum. The same-sign branch
--- cannot overflow in its difference.
-data SeamTangent = SeamTangent
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-
-tangentIndex :: SeamTangent -> Int
-tangentIndex (SeamTangent index _) = index
-{-# INLINE tangentIndex #-}
-
-data SeamTangents = SeamTangents
-  { seamLowerLeft :: !SeamTangent
-  , seamLowerRight :: !SeamTangent
-  , seamUpperLeft :: !SeamTangent
-  , seamUpperRight :: !SeamTangent
-  , seamFrontierPointReads :: {-# UNPACK #-} !Int
-  }
-
-seamTangents
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> Triangulation mode' vertex' directed' undirected' face'
-  -> SeamFrontierIndex
-  -> Maybe SeamTangents
-seamTangents axis left leftFrontier right rightFrontier = do
-  (lowerLeft, lowerRight, lowerReads) <- lowerTangent axis left leftFrontier right rightFrontier
-  (upperLeft, upperRight, upperReads) <- upperTangent axis left leftFrontier right rightFrontier
-  pure
-    SeamTangents
-      { seamLowerLeft = lowerLeft
-      , seamLowerRight = lowerRight
-      , seamUpperLeft = upperLeft
-      , seamUpperRight = upperRight
-      , seamFrontierPointReads = lowerReads + upperReads
-      }
-
-lowerTangent
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> Triangulation mode' vertex' directed' undirected' face'
-  -> SeamFrontierIndex
-  -> Maybe (SeamTangent, SeamTangent, Int)
--- The walk carries both endpoints' coordinates: a step replaces exactly one
--- endpoint, and the replacement is the neighbour whose coordinates the step's
--- own test already read.
-lowerTangent axis left leftFrontier right rightFrontier = do
-  (leftStart, leftReads) <- frontierPointAtCount axis left leftFrontier leftStartIndex
-  (rightStart, rightReads) <- frontierPointAtCount axis right rightFrontier rightStartIndex
-  walk leftStartIndex leftStart rightStartIndex rightStart (leftReads + rightReads)
- where
-  leftStartIndex :: Int
-  rightStartIndex :: Int
-  leftSize :: Int
-  rightSize :: Int
-  walk
-    :: Int
-    -> (Double, Double)
-    -> Int
-    -> (Double, Double)
-    -> Int
-    -> Maybe (SeamTangent, SeamTangent, Int)
-  leftStartIndex = chartLowerRightmost axis leftFrontier
-  rightStartIndex = chartLowerLeftmost axis rightFrontier
-  leftSize = Seq.length (seamFrontierEdges leftFrontier)
-  rightSize = Seq.length (seamFrontierEdges rightFrontier)
-
-  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !frontierReads = do
-    nextLeftIndex <- pure (nextIndex leftSize leftIndex)
-    previousRightIndex <- pure (previousIndex rightSize rightIndex)
-    (nextLeftX, nextLeftY) <- frontierPointAt axis left leftFrontier nextLeftIndex
-    (previousRightX, previousRightY) <- frontierPointAt axis right rightFrontier previousRightIndex
-    let !nextReads = frontierReads + 2
-        !leftBelow =
-          orient2dCoordinates leftX leftY rightX rightY nextLeftX nextLeftY == LT
-        !rightBelow =
-          orient2dCoordinates leftX leftY rightX rightY previousRightX previousRightY == LT
-    if leftBelow
-      then walk nextLeftIndex (nextLeftX, nextLeftY) rightIndex (rightX, rightY) nextReads
-      else
-        if rightBelow
-          then walk leftIndex (leftX, leftY) previousRightIndex (previousRightX, previousRightY) nextReads
-          else do
-            leftEdge <- frontierEdgeAt leftFrontier leftIndex
-            rightEdge <- frontierEdgeAt rightFrontier rightIndex
-            pure
-              ( SeamTangent leftIndex leftEdge
-              , SeamTangent rightIndex rightEdge
-              , nextReads
-              )
-
-upperTangent
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> Triangulation mode' vertex' directed' undirected' face'
-  -> SeamFrontierIndex
-  -> Maybe (SeamTangent, SeamTangent, Int)
-upperTangent axis left leftFrontier right rightFrontier = do
-  (leftStart, leftReads) <- frontierPointAtCount axis left leftFrontier leftStartIndex
-  (rightStart, rightReads) <- frontierPointAtCount axis right rightFrontier rightStartIndex
-  walk leftStartIndex leftStart rightStartIndex rightStart (leftReads + rightReads)
- where
-  leftStartIndex :: Int
-  rightStartIndex :: Int
-  leftSize :: Int
-  rightSize :: Int
-  walk
-    :: Int
-    -> (Double, Double)
-    -> Int
-    -> (Double, Double)
-    -> Int
-    -> Maybe (SeamTangent, SeamTangent, Int)
-  leftStartIndex = chartUpperRightmost axis leftFrontier
-  rightStartIndex = chartUpperLeftmost axis rightFrontier
-  leftSize = Seq.length (seamFrontierEdges leftFrontier)
-  rightSize = Seq.length (seamFrontierEdges rightFrontier)
-
-  walk !leftIndex (!leftX, !leftY) !rightIndex (!rightX, !rightY) !frontierReads = do
-    previousLeftIndex <- pure (previousIndex leftSize leftIndex)
-    nextRightIndex <- pure (nextIndex rightSize rightIndex)
-    (previousLeftX, previousLeftY) <- frontierPointAt axis left leftFrontier previousLeftIndex
-    (nextRightX, nextRightY) <- frontierPointAt axis right rightFrontier nextRightIndex
-    let !nextReads = frontierReads + 2
-        !leftAbove =
-          orient2dCoordinates leftX leftY rightX rightY previousLeftX previousLeftY == GT
-        !rightAbove =
-          orient2dCoordinates leftX leftY rightX rightY nextRightX nextRightY == GT
-    if leftAbove
-      then walk previousLeftIndex (previousLeftX, previousLeftY) rightIndex (rightX, rightY) nextReads
-      else
-        if rightAbove
-          then walk leftIndex (leftX, leftY) nextRightIndex (nextRightX, nextRightY) nextReads
-          else do
-            leftEdge <- frontierEdgeAt leftFrontier leftIndex
-            rightEdge <- frontierEdgeAt rightFrontier rightIndex
-            pure
-              ( SeamTangent leftIndex leftEdge
-              , SeamTangent rightIndex rightEdge
-              , nextReads
-              )
-
-frontierEdgeAt :: SeamFrontierIndex -> Int -> Maybe Int
-frontierEdgeAt frontier index = Seq.lookup index (seamFrontierEdges frontier)
-
-chartLowerRightmost :: SeamAxis -> SeamFrontierIndex -> Int
-chartLowerRightmost axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierLowerRightmost frontier
-    SeamAxisY -> seamFrontierYLowerRightmost frontier
-
-chartLowerLeftmost :: SeamAxis -> SeamFrontierIndex -> Int
-chartLowerLeftmost axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierLowerLeftmost frontier
-    SeamAxisY -> seamFrontierYLowerLeftmost frontier
-
-chartUpperRightmost :: SeamAxis -> SeamFrontierIndex -> Int
-chartUpperRightmost axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierUpperRightmost frontier
-    SeamAxisY -> seamFrontierYUpperRightmost frontier
-
-chartUpperLeftmost :: SeamAxis -> SeamFrontierIndex -> Int
-chartUpperLeftmost axis frontier =
-  case axis of
-    SeamAxisX -> seamFrontierUpperLeftmost frontier
-    SeamAxisY -> seamFrontierYUpperLeftmost frontier
-
-frontierPointAt
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> Int
-  -> Maybe (Double, Double)
-frontierPointAt axis triangulation frontier index = do
-  edge <- frontierEdgeAt frontier index
-  let !vertex = topologyAt triangulation (4 * edge)
-  pure (chartPoint axis (physicalPoint triangulation vertex))
-{-# INLINE frontierPointAt #-}
-
-chartPoint :: SeamAxis -> (Double, Double) -> (Double, Double)
-chartPoint axis (x, y) =
-  case axis of
-    SeamAxisX -> (x, y)
-    SeamAxisY -> seamQuarterTurn (x, y)
-
-physicalPoint
-  :: Triangulation mode vertex directed undirected face
-  -> Int
-  -> (Double, Double)
-physicalPoint triangulation vertex =
-  ( triPointX triangulation `pagedUnsafeIndex` vertex
-  , triPointY triangulation `pagedUnsafeIndex` vertex
-  )
-
-frontierPointAtCount
-  :: SeamAxis
-  -> Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> Int
-  -> Maybe ((Double, Double), Int)
-frontierPointAtCount axis triangulation frontier index = do
-  point <- frontierPointAt axis triangulation frontier index
-  pure (point, 1)
-
-nextIndex :: Int -> Int -> Int
-nextIndex size index
-  | index + 1 == size = 0
-  | otherwise = index + 1
-{-# INLINE nextIndex #-}
-
-previousIndex :: Int -> Int -> Int
-previousIndex size index
-  | index == 0 = size - 1
-  | otherwise = index - 1
-{-# INLINE previousIndex #-}
-
-
-mergeSeparated
-  :: forall outputMode leftMode rightMode vertex
-   . Triangulation leftMode vertex () () ()
-  -> Triangulation rightMode vertex () () ()
-  -> SeamTangents
-   -> Either
-      BuildError
-      (Triangulation outputMode vertex () () (), BuildStats)
-mergeSeparated
-  left
-  right
-  SeamTangents
-    { seamLowerLeft = SeamTangent _ lowerLeft
-    , seamLowerRight = SeamTangent _ lowerRight
-    , seamUpperLeft = SeamTangent _ upperLeft
-    , seamUpperRight = SeamTangent _ upperRight
-    } = runST $ do
-  mutable <- newMutableDcel unitElementDefaults (planarDcelCapacity totalVertices)
-  pointCapacityOutcome <- ensurePointCapacity mutable totalVertices
-  cellCapacityOutcome <-
-    ensureCellCapacity
-      mutable
-      ((leftDirected + rightDirected) `quot` 2 + 1)
-      (leftFaces + rightFaces - 2)
-  case (pointCapacityOutcome, cellCapacityOutcome) of
-    (Left obstruction, _) -> pure (Left obstruction)
-    (_, Left obstruction) -> pure (Left obstruction)
-    (Right (), Right ()) -> do
-      appendSourceVertices mutable left
-      appendSourceVertices mutable right
-      _ <- addEdgeBlock mutable ((leftDirected + rightDirected) `quot` 2)
-      _ <- addFaceBlock mutable (leftFaces + rightFaces - 2)
-      copySource mutable left 0 0 0
-      copySource mutable right leftVertices leftDirected (leftFaces - 1)
-      base <- spliceLowerTangent mutable leftDirected lowerLeft lowerRight
-      operation <- newOperationState (halfEdgeCapacity mutable)
-      stitched <-
-        stitchSeam
-          mutable
-          operation
-          base
-          (topologyAt left (4 * upperLeft))
-          (leftVertices + topologyAt right (4 * upperRight))
-          []
-          []
-      case stitched of
-        Left obstruction -> pure (Left obstruction)
-        Right _ -> do
-          statistics <- freezeBuildStats operation
-          fmap (\triangulation -> (triangulation, statistics))
-            <$> freezeTriangulation mutable
- where
-  !leftVertices = numVertices left
-  !rightVertices = numVertices right
-  !totalVertices = leftVertices + rightVertices
-  !leftDirected = numDirectedEdges left
-  !rightDirected = numDirectedEdges right
-  !leftFaces = numFaces left
-  !rightFaces = numFaces right
-
-stitchSeam
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> Int
-  -> [Int]
-  -> [Int]
-  -> ST s (Either BuildError (Int, [Int]))
-stitchSeam mutable operation base upperLeft upperRight seeds newFaces =
-  fmap (fmap (\(finalBase, faces, _) -> (finalBase, faces)))
-    ( stitchSeamWith
-        (\target ops edges -> legalizeEdges target ops edges >> pure (Right ()))
-        (SeamProgress 0 0 0 0 0 0)
-        mutable
-        operation
-        base
-        upperLeft
-        upperRight
-        seeds
-        newFaces
-    )
-
-stitchSeamPinned
-  :: (Int -> Bool)
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> [Int]
-  -> [Int]
-  -> ST s (Either BuildError (Int, [Int], SeamProgress))
-stitchSeamPinned targetFaceProtected mutable operation leftSize leftStart rightSize rightStart base upperLeft upperRight seeds newFaces =
-  stitchSeamWith
-    (\target ops edges -> legalizeEdgesPinned targetFaceProtected target ops edges >> pure (Right ()))
-    (SeamProgress leftSize leftStart (previousIndex leftSize leftStart) rightSize rightStart rightStart)
-    mutable
-    operation
-    base
-    upperLeft
-    upperRight
-    seeds
-    newFaces
-
-data SeamProgress = SeamProgress
-  { seamProgressLeftSize :: {-# UNPACK #-} !Int
-  , seamProgressLeftStart :: {-# UNPACK #-} !Int
-  , seamProgressLeftCursor :: {-# UNPACK #-} !Int
-  , seamProgressRightSize :: {-# UNPACK #-} !Int
-  , seamProgressRightStart :: {-# UNPACK #-} !Int
-  , seamProgressRightCursor :: {-# UNPACK #-} !Int
-  }
-
-stitchSeamWith
-  :: (MutableDcel s vertex directed undirected face -> OperationState s -> [Int] -> ST s (Either BuildError ()))
-  -> SeamProgress
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> Int
-  -> [Int]
-  -> [Int]
-  -> ST s (Either BuildError (Int, [Int], SeamProgress))
-stitchSeamWith legalize progress mutable operation base upperLeft upperRight seeds newFaces = do
-  leftVertex <- readOrigin mutable base
-  rightVertex <- readOrigin mutable (base `xor` 1)
-  if leftVertex == upperLeft && rightVertex == upperRight
-    then do
-      legalized <- legalize mutable operation seeds
-      pure (fmap (const (base, newFaces, progress)) legalized)
-    else do
-      leftEdge <- readPrevious mutable base
-      rightEdge <- readNext mutable base
-      nextLeft <- readOrigin mutable leftEdge
-      nextRight <- readOrigin mutable (rightEdge `xor` 1)
-      leftTurn <- vertexOrientation mutable nextLeft leftVertex rightVertex
-      rightTurn <- vertexOrientation mutable leftVertex rightVertex nextRight
-      chooseRight <-
-        if rightVertex == upperRight
-          then pure False
-          else
-            if leftVertex == upperLeft
-              then pure True
-              else
-                case (leftTurn == GT, rightTurn == GT) of
-                  (True, True) ->
-                    (== GT) <$> vertexInCircle mutable nextLeft leftVertex rightVertex nextRight
-                  (False, True) -> pure True
-                  _ -> pure False
-      if chooseRight
-        then do
-          closed <- closeOuterTurn mutable base
-          case closed of
-            Left obstruction -> pure (Left obstruction)
-            Right nextBase ->
-              do
-                face <- readFace mutable (nextBase `xor` 1)
-                stitchSeamWith
-                  legalize
-                  (advanceRight progress)
-                  mutable
-                  operation
-                  nextBase
-                  upperLeft
-                  upperRight
-                  (base : rightEdge : seeds)
-                  (face : newFaces)
-        else do
-          closed <- closeOuterTurn mutable leftEdge
-          case closed of
-            Left obstruction -> pure (Left obstruction)
-            Right nextBase ->
-              do
-                face <- readFace mutable (nextBase `xor` 1)
-                stitchSeamWith
-                  legalize
-                  (advanceLeft progress)
-                  mutable
-                  operation
-                  nextBase
-                  upperLeft
-                  upperRight
-                  (leftEdge : base : seeds)
-                  (face : newFaces)
-
-advanceLeft :: SeamProgress -> SeamProgress
-advanceLeft progress@SeamProgress{seamProgressLeftSize, seamProgressLeftCursor} =
-  progress
-    { seamProgressLeftCursor =
-        previousIndex seamProgressLeftSize seamProgressLeftCursor
-    }
-
-advanceRight :: SeamProgress -> SeamProgress
-advanceRight progress@SeamProgress{seamProgressRightSize, seamProgressRightCursor} =
-  progress
-    { seamProgressRightCursor =
-        nextIndex seamProgressRightSize seamProgressRightCursor
-    }
-
-combineSeamFrontier
-  :: Triangulation mode vertex directed undirected face
-  -> SeamFrontierIndex
-  -> SeamFrontierIndex
-  -> Int
-  -> Int
-  -> Seq.Seq Int
-  -> Seq.Seq Int
-  -> SeamProgress
-  -> Either BuildError SeamFrontierIndex
-combineSeamFrontier triangulation leftFrontier rightFrontier leftEdgeOffset rightEdgeOffset upperBridge lowerBridge progress
-  | seamProgressLeftSize progress /= Seq.length (seamFrontierEdges leftFrontier) = Left SeamFrontierUnavailable
-  | seamProgressRightSize progress /= Seq.length (seamFrontierEdges rightFrontier) = Left SeamFrontierUnavailable
-  | otherwise = do
-  leftEdges <- translatedEdges leftFrontier leftEdgeOffset
-  rightEdges <- translatedEdges rightFrontier rightEdgeOffset
-  rightResidual <-
-    cycleSlice
-      rightEdges
-      (seamProgressRightSize progress)
-      (seamProgressRightCursor progress)
-      (previousIndex
-         (seamProgressRightSize progress)
-         (seamProgressRightStart progress))
-  leftResidual <-
-    cycleSlice
-      leftEdges
-      (seamProgressLeftSize progress)
-      (seamProgressLeftStart progress)
-      (seamProgressLeftCursor progress)
-  let !outputEdges =
-        upperBridge
-          Seq.>< rightResidual
-          Seq.>< lowerBridge
-          Seq.>< leftResidual
-      !rightOffset = Seq.length upperBridge
-      !lowerOffset = rightOffset + Seq.length rightResidual
-      !leftOffset = lowerOffset + Seq.length lowerBridge
-      !rightEnd = previousIndex (seamProgressRightSize progress) progressRightStart
-      !leftEnd = seamProgressLeftCursor progress
-      !candidatePositions =
-        IntSet.toAscList
-          ( IntSet.fromList
-              ( residualBoundaryPositions 0 upperBridge
-                  ++ residualBoundaryPositions rightOffset rightResidual
-                  ++ residualBoundaryPositions lowerOffset lowerBridge
-                  ++ residualBoundaryPositions leftOffset leftResidual
-                  ++ sourceAnchorPositions
-                    rightOffset
-                    (seamProgressRightCursor progress)
-                    rightEnd
-                    (seamProgressRightSize progress)
-                    rightFrontier
-                  ++ sourceAnchorPositions
-                    leftOffset
-                    progressLeftStart
-                    leftEnd
-                    (seamProgressLeftSize progress)
-                    leftFrontier
-              )
-          )
-  (minimumX, maximumX, lowerRight, lowerLeft, upperRight, upperLeft) <-
-    summarizeOutputChart id candidatePositions outputEdges
-  (minimumY, maximumY, yLowerRight, yLowerLeft, yUpperRight, yUpperLeft) <-
-    summarizeOutputChart seamQuarterTurn candidatePositions outputEdges
-  pure
-    SeamFrontierIndex
-      { seamFrontierEdges = outputEdges
-      , seamFrontierXMinimum = minimumX
-      , seamFrontierXMaximum = maximumX
-      , seamFrontierLowerRightmost = lowerRight
-      , seamFrontierLowerLeftmost = lowerLeft
-      , seamFrontierUpperRightmost = upperRight
-      , seamFrontierUpperLeftmost = upperLeft
-      , seamFrontierYMinimum = minimumY
-      , seamFrontierYMaximum = maximumY
-      , seamFrontierYLowerRightmost = yLowerRight
-      , seamFrontierYLowerLeftmost = yLowerLeft
-      , seamFrontierYUpperRightmost = yUpperRight
-      , seamFrontierYUpperLeftmost = yUpperLeft
-      }
- where
-  progressRightStart = seamProgressRightStart progress
-  progressLeftStart = seamProgressLeftStart progress
-
-  translatedEdges
-    :: SeamFrontierIndex
-    -> Int
-    -> Either BuildError (Seq.Seq Int)
-  translatedEdges frontier offset
-    | offset == 0 = Right (seamFrontierEdges frontier)
-    | otherwise = Right (fmap (+ offset) (seamFrontierEdges frontier))
-
-  cycleSlice
-    :: Seq.Seq Int
-    -> Int
-    -> Int
-    -> Int
-    -> Either BuildError (Seq.Seq Int)
-  cycleSlice edges size start end
-    | size <= 0 || start < 0 || end < 0 || start >= size || end >= size = Left SeamFrontierUnavailable
-    | start <= end = Right (Seq.take (end - start + 1) (Seq.drop start edges))
-    | otherwise =
-        Right
-          ( Seq.drop start edges
-              Seq.>< Seq.take (end + 1) edges
-          )
-
-  residualBoundaryPositions :: Int -> Seq.Seq Int -> [Int]
-  residualBoundaryPositions offset residual =
-    case Seq.length residual of
-      0 -> []
-      lengthOfResidual -> [offset, offset + lengthOfResidual - 1]
-
-  sourceAnchorPositions
-    :: Int
-    -> Int
-    -> Int
-    -> Int
-    -> SeamFrontierIndex
-    -> [Int]
-  sourceAnchorPositions offset start end size frontier =
-    [ offset + position
-    | anchor <- sourceAnchors frontier
-    , Right position <- [intervalPosition size anchor start end]
-    ]
-
-  sourceAnchors :: SeamFrontierIndex -> [Int]
-  sourceAnchors frontier =
-    [ seamFrontierLowerRightmost frontier
-    , seamFrontierLowerLeftmost frontier
-    , seamFrontierUpperRightmost frontier
-    , seamFrontierUpperLeftmost frontier
-    , seamFrontierYLowerRightmost frontier
-    , seamFrontierYLowerLeftmost frontier
-    , seamFrontierYUpperRightmost frontier
-    , seamFrontierYUpperLeftmost frontier
-    ]
-
-  summarizeOutputChart
-    :: ((Double, Double) -> (Double, Double))
-    -> [Int]
-    -> Seq.Seq Int
-    -> Either BuildError (Double, Double, Int, Int, Int, Int)
-  summarizeOutputChart transform candidatePositions outputEdges = do
-    candidates <-
-      traverse
-        (\index -> do
-           edge <- maybe (Left SeamFrontierUnavailable) Right (Seq.lookup index outputEdges)
-           let !vertex = topologyAt triangulation (4 * edge)
-           pure (index, physicalPoint triangulation vertex))
-        candidatePositions
-    maybe (Left SeamFrontierUnavailable) Right
-      (summarizeSeamChart transform candidates)
-
-  intervalPosition :: Int -> Int -> Int -> Int -> Either BuildError Int
-  intervalPosition size target start end
-    | target < 0 || target >= size = Left SeamFrontierUnavailable
-    | start <= end =
-        if target >= start && target <= end
-          then Right (target - start)
-          else Left SeamFrontierUnavailable
-    | target >= start = Right (target - start)
-    | target <= end = Right (size - start + target)
-    | otherwise = Left SeamFrontierUnavailable
-
-vertexOrientation
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> Int
-  -> ST s Ordering
-vertexOrientation mutable a b c = do
-  ax <- readPointX mutable a
-  ay <- readPointY mutable a
-  bx <- readPointX mutable b
-  by <- readPointY mutable b
-  cx <- readPointX mutable c
-  cy <- readPointY mutable c
-  pure $! orient2dCoordinates ax ay bx by cx cy
-{-# INLINE vertexOrientation #-}
-
-vertexInCircle
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> ST s Ordering
-vertexInCircle mutable a b c d = do
-  ax <- readPointX mutable a
-  ay <- readPointY mutable a
-  bx <- readPointX mutable b
-  by <- readPointY mutable b
-  cx <- readPointX mutable c
-  cy <- readPointY mutable c
-  dx <- readPointX mutable d
-  dy <- readPointY mutable d
-  pure $! inCircleCoordinates ax ay bx by cx cy dx dy
-{-# INLINE vertexInCircle #-}
-
-appendSourceVertices
-  :: MutableDcel s vertex () () ()
-  -> Triangulation mode vertex () () ()
-  -> ST s ()
-appendSourceVertices mutable source =
-  forRange 0 (numVertices source) $ \vertex -> do
-    _ <-
-      appendVertexCoordinates
-        mutable
-        (triPointX source `pagedUnsafeIndex` vertex)
-        (triPointY source `pagedUnsafeIndex` vertex)
-        (vertexData source (VertexId (fromIntegral vertex)))
-    pure ()
-
-copySource
-  :: MutableDcel s vertex () () ()
-  -> Triangulation mode vertex () () ()
-  -> Int
-  -> Int
-  -> Int
-  -> ST s ()
-copySource mutable source vertexOffset edgeOffset faceOffset = do
-  forRange 0 (numDirectedEdges source) $ \edge -> do
-    let !target = edgeOffset + edge
-        !sourceBase = 4 * edge
-        !sourceFace = topologyAt source (sourceBase + 3)
-        !targetFace = if sourceFace == 0 then 0 else faceOffset + sourceFace
-    writeOrigin mutable target (vertexOffset + topologyAt source sourceBase)
-    writeNext mutable target (edgeOffset + topologyAt source (sourceBase + 1))
-    writePrevious mutable target (edgeOffset + topologyAt source (sourceBase + 2))
-    writeFace mutable target targetFace
-  forRange 0 (numVertices source) $ \vertex ->
-    case vertexOutEdge source (VertexId (fromIntegral vertex)) of
-      Nothing -> markConnected mutable (vertexOffset + vertex) (-1)
-      Just (DirectedEdgeId edge) ->
-        markConnected mutable (vertexOffset + vertex) (edgeOffset + fromIntegral edge)
-  forRange 1 (numFaces source) $ \face ->
-    case adjacentEdge source (FaceId (fromIntegral face)) of
-      Nothing -> writeFaceEdge mutable (faceOffset + face) (-1)
-      Just (DirectedEdgeId edge) ->
-        writeFaceEdge mutable (faceOffset + face) (edgeOffset + fromIntegral edge)
-
-copySourceConstraints
-  :: MutableDcel s vertex () () ()
-  -> Int
-  -> Triangulation 'Constrained vertex () () ()
-  -> ST s ()
-copySourceConstraints mutable directedEdgeOffset source =
-  traverse_
-    (\(UndirectedEdgeId edge) ->
-       ()
-         <$ setConstraint
-           mutable
-           (directedEdgeOffset + 2 * fromIntegral edge)
-    )
-    (constraintEdges source)
-
-spliceLowerTangent
-  :: MutableDcel s vertex () () ()
-  -> Int
-  -> Int
-  -> Int
-  -> ST s Int
-spliceLowerTangent mutable rightEdgeOffset leftOuter rightOuterSource = do
-  let !rightOuter = rightEdgeOffset + rightOuterSource
-  leftVertex <- readOrigin mutable leftOuter
-  rightVertex <- readOrigin mutable rightOuter
-  leftPrevious <- readPrevious mutable leftOuter
-  rightPrevious <- readPrevious mutable rightOuter
-  (forward, backward) <- addEdge mutable leftVertex rightVertex
-  writeFace mutable forward 0
-  writeFace mutable backward 0
-  linkEdges mutable leftPrevious forward
-  linkEdges mutable forward rightOuter
-  linkEdges mutable rightPrevious backward
-  linkEdges mutable backward leftOuter
-  writeFaceEdge mutable 0 forward
-  writeVertexOut mutable leftVertex forward
-  writeVertexOut mutable rightVertex backward
-  pure forward
-{-# INLINE spliceLowerTangent #-}
-
-topologyAt
-  :: Triangulation mode vertex directed undirected face
-  -> Int
-  -> Int
-topologyAt triangulation slot =
-  fromIntegral (triHalfTopology triangulation `pagedUnsafeIndex` slot)
-{-# INLINE topologyAt #-}
-
-
-forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
-forRange from to action = go from
- where
-  go !index
-    | index >= to = pure ()
-    | otherwise = action index >> go (index + 1)
-{-# INLINE forRange #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs b/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Join/SiteSet.hs
+++ /dev/null
@@ -1,400 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Exact transient site sections derived from the authoritative coordinate
--- planes. This is the sole owner of coordinate-set classification for joins,
--- set algebra, and constrained union. Payloads travel as annotations; they do
--- not participate in site identity.
-module Moonlight.Triangulation.Internal.Join.SiteSet
-  ( SiteSet
-  , siteSetFromTriangulation
-  , siteSupportFromTriangulation
-  , siteSetSize
-  , siteSetRelation
-  , siteRelationFromTriangulations
-  , siteSetUnionWith
-  , siteSetIntersectionWith
-  , siteSetDifference
-  , siteSetSymmetricDifferenceFromTriangulations
-  , siteSetAssocs
-  , siteSetPoints
-  , delaunayFromSiteSet
-  , rebuildCanonicalSiteSet
-  ) where
-
-import Control.Monad.ST (ST, runST)
-import qualified Data.Map.Strict as Map
-import qualified Data.Map.Merge.Strict as MapMerge
-import Data.Functor.Const (Const (..))
-import Data.Maybe (isJust)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Internal.BulkLoad (delaunayFromAdmittedDistinctAscending)
-import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Internal.Canonical (canonicalizeKnownAscending)
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( foldVertices'
-  )
-import Moonlight.Triangulation.Internal.Paged
-  ( pagedUnsafeIndex
-  , toVector
-  )
-import Moonlight.Triangulation.Internal.PointIndex
-  ( MutablePointIndex
-  , lookupMutablePoint
-  , lookupPointIndex
-  , newMutablePointIndex
-  , seedMutablePointIndex
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( BuildResult (buildTriangulation)
-  , Triangulation (..)
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( BuildError
-  , ConstraintMode (Unconstrained)
-  , Point (..)
-  , QueryPoint (..)
-  , SiteRelation (..)
-  , unitElementDefaults
-  )
-
-newtype SiteSet annotation = SiteSet (Map.Map QueryPoint annotation)
-
--- Strict sufficient statistics for the one-pass ordered-map descent used by
--- 'siteSetRelation'. Keeping the census strict prevents a relation query from
--- replacing an intermediate map allocation with a chain of monoidal thunks.
-data SiteRelationCensus = SiteRelationCensus !Int !Int !Int
-
-instance Semigroup SiteRelationCensus where
-  SiteRelationCensus leftA rightA overlapA <> SiteRelationCensus leftB rightB overlapB =
-    SiteRelationCensus
-      (leftA + leftB)
-      (rightA + rightB)
-      (overlapA + overlapB)
-
-instance Monoid SiteRelationCensus where
-  mempty = SiteRelationCensus 0 0 0
-
-siteSetFromTriangulation
-  :: Triangulation mode annotation directed undirected face
-  -> SiteSet annotation
-siteSetFromTriangulation triangulation =
-  SiteSet
-    ( Map.fromList
-        [ ( QueryPoint
-              ( Point
-                  (triPointX triangulation `pagedUnsafeIndex` vertex)
-                  (triPointY triangulation `pagedUnsafeIndex` vertex)
-              )
-          , vertexData triangulation (VertexId (fromIntegral vertex))
-          )
-        | vertex <- [0 .. numVertices triangulation - 1]
-        ]
-    )
-
--- | Coordinate support without touching the boxed annotation plane. Order and
--- set identity are geometric observations; callers that discard annotations
--- should not pay a boxed-page read per vertex merely to manufacture ignored
--- map values.
-siteSupportFromTriangulation
-  :: Triangulation mode vertex directed undirected face
-  -> SiteSet ()
-siteSupportFromTriangulation triangulation =
-  SiteSet
-    ( Map.fromList
-        [ ( QueryPoint
-              ( Point
-                  (triPointX triangulation `pagedUnsafeIndex` vertex)
-                  (triPointY triangulation `pagedUnsafeIndex` vertex)
-              )
-          , ()
-          )
-        | vertex <- [0 .. numVertices triangulation - 1]
-        ]
-    )
-{-# INLINE siteSupportFromTriangulation #-}
-
-siteSetSize :: SiteSet annotation -> Int
-siteSetSize (SiteSet sites) = Map.size sites
-{-# INLINE siteSetSize #-}
-
-siteSetRelation
-  :: SiteSet leftAnnotation
-  -> SiteSet rightAnnotation
-  -> SiteRelation
-siteSetRelation (SiteSet left) (SiteSet right) =
-  siteRelationFromCardinalities
-    (leftOnly + overlap)
-    (rightOnly + overlap)
-    overlap
- where
-  SiteRelationCensus leftOnly rightOnly overlap =
-    getConst
-      ( MapMerge.mergeA
-          (MapMerge.traverseMissing (\_ _ -> Const (SiteRelationCensus 1 0 0)))
-          (MapMerge.traverseMissing (\_ _ -> Const (SiteRelationCensus 0 1 0)))
-          (MapMerge.zipWithAMatched (\_ _ _ -> Const (SiteRelationCensus 0 0 1)))
-          left
-          right
-      )
-{-# INLINE siteSetRelation #-}
-
-siteRelationFromTriangulations
-  :: Triangulation leftMode leftAnnotation leftDirected leftUndirected leftFace
-  -> Triangulation rightMode rightAnnotation rightDirected rightUndirected rightFace
-  -> SiteRelation
-siteRelationFromTriangulations left right =
-  siteRelationFromCardinalities leftCount rightCount overlap
- where
-  !leftCount = numVertices left
-  !rightCount = numVertices right
-  !overlap
-    | leftCount <= rightCount = exactOverlapCount right left
-    | otherwise = exactOverlapCount left right
-{-# INLINE siteRelationFromTriangulations #-}
-
-siteRelationFromCardinalities :: Int -> Int -> Int -> SiteRelation
-siteRelationFromCardinalities leftCount rightCount overlap
-  | leftCount == rightCount && overlap == leftCount = EqualSites
-  | overlap == leftCount = LeftProperSubset
-  | overlap == rightCount = RightProperSubset
-  | overlap == 0 = DisjointSites
-  | otherwise = PartialOverlap overlap
-{-# INLINE siteRelationFromCardinalities #-}
-
-exactOverlapCount
-  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
-  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
-  -> Int
-exactOverlapCount source indexed =
-  either
-    (const (persistentExactOverlapCount source indexed))
-    id
-    (transientExactOverlapCount source indexed)
-{-# INLINE exactOverlapCount #-}
-
-transientExactOverlapCount
-  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
-  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
-  -> Either BuildError Int
-transientExactOverlapCount source indexed = runST $ do
-  pointIndex <- newMutablePointIndex (numVertices indexed)
-  seeded <-
-    seedMutablePointIndex
-      pointIndex
-      (numVertices indexed)
-      (readCoordinateX indexed)
-      (readCoordinateY indexed)
-  case seeded of
-    Left failure -> pure (Left failure)
-    Right () ->
-      fmap Right
-        ( U.ifoldM'
-            (\count vertex x -> do
-               let y = triPointY source `pagedUnsafeIndex` vertex
-               match <-
-                 lookupMutablePoint
-                   pointIndex
-                   (readCoordinateX indexed)
-                   (readCoordinateY indexed)
-                   x
-                   y
-               pure (if isJust match then count + 1 else count)
-            )
-            0
-            (toVector (triPointX source))
-        )
-{-# INLINE transientExactOverlapCount #-}
-
-persistentExactOverlapCount
-  :: Triangulation sourceMode sourceAnnotation sourceDirected sourceUndirected sourceFace
-  -> Triangulation indexedMode indexedAnnotation indexedDirected indexedUndirected indexedFace
-  -> Int
-persistentExactOverlapCount source indexed =
-  foldVertices'
-    source
-    (\count vertex ->
-       if pointOccursIn indexed (vertexPoint source vertex)
-         then count + 1
-         else count
-    )
-    0
-{-# INLINE persistentExactOverlapCount #-}
-
-siteSetUnionWith
-  :: (annotation -> annotation -> annotation)
-  -> SiteSet annotation
-  -> SiteSet annotation
-  -> SiteSet annotation
-siteSetUnionWith combine (SiteSet left) (SiteSet right) =
-  SiteSet (Map.unionWith combine left right)
-{-# INLINE siteSetUnionWith #-}
-
-siteSetIntersectionWith
-  :: (leftAnnotation -> rightAnnotation -> annotation)
-  -> SiteSet leftAnnotation
-  -> SiteSet rightAnnotation
-  -> SiteSet annotation
-siteSetIntersectionWith combine (SiteSet left) (SiteSet right) =
-  SiteSet (Map.intersectionWith combine left right)
-{-# INLINE siteSetIntersectionWith #-}
-
-siteSetDifference
-  :: SiteSet annotation
-  -> SiteSet other
-  -> SiteSet annotation
-siteSetDifference (SiteSet left) (SiteSet right) = SiteSet (Map.difference left right)
-{-# INLINE siteSetDifference #-}
-
-siteSetSymmetricDifferenceFromTriangulations
-  :: Triangulation leftMode annotation leftDirected leftUndirected leftFace
-  -> Triangulation rightMode annotation rightDirected rightUndirected rightFace
-  -> Either BuildError (SiteSet annotation)
-siteSetSymmetricDifferenceFromTriangulations left right
-  | numVertices left >= numVertices right = indexedSymmetricDifference left right
-  | otherwise = indexedSymmetricDifference right left
-{-# INLINE siteSetSymmetricDifferenceFromTriangulations #-}
-
-indexedSymmetricDifference
-  :: forall sourceMode annotation sourceDirected sourceUndirected sourceFace
-            indexedMode indexedDirected indexedUndirected indexedFace
-  . Triangulation sourceMode annotation sourceDirected sourceUndirected sourceFace
-  -> Triangulation indexedMode annotation indexedDirected indexedUndirected indexedFace
-  -> Either BuildError (SiteSet annotation)
-indexedSymmetricDifference source indexed =
-  fmap (SiteSet . Map.fromList) (runST collectExclusiveAssociations)
- where
-  collectExclusiveAssociations
-    :: forall state. ST state (Either BuildError [(QueryPoint, annotation)])
-  collectExclusiveAssociations = do
-    pointIndex <- newMutablePointIndex (numVertices indexed)
-    seeded <-
-      seedMutablePointIndex
-        pointIndex
-        (numVertices indexed)
-        (readCoordinateX indexed)
-        (readCoordinateY indexed)
-    case seeded of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        matchedIndexedVertices <- MUV.replicate (numVertices indexed) False
-        sourceExclusive <-
-          U.ifoldM'
-            (collectSourceExclusive pointIndex matchedIndexedVertices)
-            []
-            (toVector (triPointX source))
-        indexedExclusive <-
-          U.ifoldM'
-            (collectIndexedExclusive matchedIndexedVertices)
-            []
-            (toVector (triPointX indexed))
-        pure (Right (sourceExclusive <> indexedExclusive))
-   where
-    collectSourceExclusive
-      :: MutablePointIndex state
-      -> MUV.MVector state Bool
-      -> [(QueryPoint, annotation)]
-      -> Int
-      -> Double
-      -> ST state [(QueryPoint, annotation)]
-    collectSourceExclusive pointIndex matchedIndexedVertices associations rawVertex x = do
-      let vertex = VertexId (fromIntegral rawVertex)
-          y = triPointY source `pagedUnsafeIndex` rawVertex
-          point = QueryPoint (Point x y)
-      match <-
-        lookupMutablePoint
-          pointIndex
-          (readCoordinateX indexed)
-          (readCoordinateY indexed)
-          x
-          y
-      case match of
-        Nothing -> pure ((point, vertexData source vertex) : associations)
-        Just indexedVertex -> do
-          MUV.unsafeWrite matchedIndexedVertices indexedVertex True
-          pure associations
-
-    collectIndexedExclusive
-      :: MUV.MVector state Bool
-      -> [(QueryPoint, annotation)]
-      -> Int
-      -> Double
-      -> ST state [(QueryPoint, annotation)]
-    collectIndexedExclusive matchedIndexedVertices associations rawVertex x = do
-      let vertex = VertexId (fromIntegral rawVertex)
-          point = QueryPoint (Point x (triPointY indexed `pagedUnsafeIndex` rawVertex))
-      matched <- MUV.unsafeRead matchedIndexedVertices rawVertex
-      pure
-        ( if matched
-            then associations
-            else (point, vertexData indexed vertex) : associations
-        )
-{-# INLINE indexedSymmetricDifference #-}
-
-readCoordinateX
-  :: Triangulation mode annotation directed undirected face
-  -> Int
-  -> ST state Double
-readCoordinateX triangulation vertex =
-  pure (triPointX triangulation `pagedUnsafeIndex` vertex)
-{-# INLINE readCoordinateX #-}
-
-readCoordinateY
-  :: Triangulation mode annotation directed undirected face
-  -> Int
-  -> ST state Double
-readCoordinateY triangulation vertex =
-  pure (triPointY triangulation `pagedUnsafeIndex` vertex)
-{-# INLINE readCoordinateY #-}
-
-pointOccursIn
-  :: Triangulation mode annotation directed undirected face
-  -> Point
-  -> Bool
-pointOccursIn triangulation = isJust . lookupPointIn triangulation
-{-# INLINE pointOccursIn #-}
-
-lookupPointIn
-  :: Triangulation mode annotation directed undirected face
-  -> Point
-  -> Maybe Int
-lookupPointIn triangulation =
-  lookupPointIndex
-    (triPointX triangulation)
-    (triPointY triangulation)
-    (triPointIndex triangulation)
-{-# INLINE lookupPointIn #-}
-
-siteSetAssocs :: SiteSet annotation -> [(QueryPoint, annotation)]
-siteSetAssocs (SiteSet sites) = Map.toAscList sites
-{-# INLINE siteSetAssocs #-}
-
-siteSetPoints :: SiteSet annotation -> V.Vector (Point)
-siteSetPoints (SiteSet sites) = V.fromList (fmap queryPointValue (Map.keys sites))
-{-# INLINE siteSetPoints #-}
-
--- | Publish an admitted exact site section through the one bulk-construction
--- engine. Its Map order is the trusted strict lexicographic vertex order; the
--- circle sweep still establishes the algorithm's radial execution order.
-delaunayFromSiteSet
-  :: SiteSet annotation
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-delaunayFromSiteSet sites =
-  buildTriangulation
-    <$> delaunayFromAdmittedDistinctAscending
-      unitElementDefaults
-      (V.fromList (siteSetAssocs sites))
-{-# INLINE delaunayFromSiteSet #-}
-
--- | Canonical finite-set publication. The preceding SiteSet build preserves
--- lexicographic vertex order, so Canonical need not rediscover that fact.
-rebuildCanonicalSiteSet
-  :: SiteSet annotation
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-rebuildCanonicalSiteSet sites =
-  delaunayFromSiteSet sites >>= canonicalizeKnownAscending
-{-# INLINE rebuildCanonicalSiteSet #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Location.hs b/src-build/Moonlight/Triangulation/Internal/Location.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Location.hs
+++ /dev/null
@@ -1,359 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
-module Moonlight.Triangulation.Internal.Location
-  ( MutableLocation (..)
-  , locateMutable
-  , locateMutableWithin
-  , locateLineMutable
-  , visibleOuterEdge
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Bits ((.&.))
-import qualified Data.IntSet as IntSet
-import Data.STRef (readSTRef, writeSTRef)
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.FaceProbe
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , maxCounter
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  )
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Math
-  ( canonicalPoint
-  , squaredDistanceWide
-  )
-import Moonlight.Triangulation.Types
-
-data MutableLocation
-  = MutableEmpty
-  | MutableOnVertex {-# UNPACK #-} !Int
-  | MutableOnEdge {-# UNPACK #-} !Int
-  | MutableInFace {-# UNPACK #-} !Int
-  | MutableOutsideHull {-# UNPACK #-} !Int
-  deriving stock (Eq, Ord, Show)
-
-locateMutable
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> ST s (Either BuildError MutableLocation)
-locateMutable mutable operation hint rawQuery = do
-  locateMutableWithBarrier Nothing mutable operation hint rawQuery
-
--- | Locate from a checked local face while refusing to walk across the exact
--- interface section. The ordinary locator remains unchanged for unrestricted
--- refinement; the local interpreter supplies only its compact barrier pairs.
-locateMutableWithin
-  :: IntSet.IntSet
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> ST s (Either BuildError MutableLocation)
-locateMutableWithin interfacePairs =
-  locateMutableWithBarrier (Just interfacePairs)
-
-locateMutableWithBarrier
-  :: Maybe IntSet.IntSet
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> ST s (Either BuildError MutableLocation)
-locateMutableWithBarrier barrier mutable operation hint rawQuery = do
-  connected <- connectedCount mutable
-  faces <- faceCount mutable
-  let !query = canonicalPoint rawQuery
-  case connected of
-    0 -> pure (Right MutableEmpty)
-    _ | faces <= 1 -> Right <$> locateLineMutable mutable query
-    -- A hint is the caller vouching for adjacency, so the exact walk starts
-    -- there directly; only an unhinted locate buys the vertex descent.
-    _ | Just face <- hint, face > 0 && face < faces -> walkWithBarrier barrier mutable operation query face
-    _ -> do
-      start <- chooseStartFace mutable hint
-      anchor <- readFaceEdge mutable start
-      if anchor < 0
-        then walkWithBarrier barrier mutable operation query start
-        else do
-          origin <- readOrigin mutable anchor
-          nearest <- descendToNearest mutable operation query origin
-          face <- incidentInnerFace mutable nearest start
-          walkWithBarrier barrier mutable operation query face
-
--- | Greedy first-improvement descent through vertex neighbours: hop to the
--- first neighbour strictly closer to the query until none improves. Distance
--- is a heuristic only — exact containment belongs to 'walkWithBarrier' — so the widened
--- comparison can lengthen the path but never move the located answer.
---
--- Strictly decreasing distance bounds the hops and nothing else: no vertex is
--- entered twice, but the star rotation terminates only if @reverse . previous@
--- closes an orbit, which is a property of the links rather than of the
--- geometry. Disjoint stars over distinct entered vertices spend at most one
--- step per outgoing half-edge, so a budget past 'directedEdgeCount' cannot be
--- exhausted while the links are well formed. Exhausting it means they are not,
--- and yields the vertex in hand rather than an error — a truncated descent can
--- only lengthen 'walkWithBarrier', never move what it finds.
-descendToNearest
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Point
-  -> Int
-  -> ST s Int
-descendToNearest mutable operation query start = do
-  halfEdges <- directedEdgeCount mutable
-  point <- pointAt mutable start
-  settle (halfEdges + 1) start (squaredDistanceWide query point)
- where
-  settle !budget !vertex !best = do
-    first <- readVertexOut mutable vertex
-    if first < 0
-      then pure vertex
-      else rotate budget vertex best first first
-  rotate !budget !vertex !best !first !edge
-    | budget <= 0 = pure vertex
-    | otherwise = do
-        neighbour <- readOrigin mutable (reverseIndex edge)
-        point <- pointAt mutable neighbour
-        let !candidate = squaredDistanceWide query point
-        if candidate < best
-          then do
-            addCounter operation CounterLocationWalkSteps 1
-            settle (budget - 1) neighbour candidate
-          else do
-            previousEdge <- readPrevious mutable edge
-            let !outgoing = reverseIndex previousEdge
-            if outgoing == first then pure vertex else rotate (budget - 1) vertex best first outgoing
-
--- | An inner face incident to the vertex, so the exact walk starts adjacent
--- to where the descent settled; the caller's face stands in when the star
--- offers none.
-incidentInnerFace
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> ST s Int
-incidentInnerFace mutable vertex fallback = do
-  first <- readVertexOut mutable vertex
-  if first < 0 then pure fallback else rotate first first
- where
-  rotate !first !edge = do
-    face <- readFace mutable edge
-    if face > 0
-      then pure face
-      else do
-        previousEdge <- readPrevious mutable edge
-        let !outgoing = reverseIndex previousEdge
-        if outgoing == first then pure fallback else rotate first outgoing
-
-locateLineMutable :: MutableDcel s vertex directed undirected face -> Point -> ST s MutableLocation
-locateLineMutable mutable rawQuery = do
-  let !query = canonicalPoint rawQuery
-  vertices <- pointCount mutable
-  exactVertex <- findVertex query vertices 0
-  case exactVertex of
-    Just vertex -> pure (MutableOnVertex vertex)
-    Nothing -> do
-      halfEdges <- directedEdgeCount mutable
-      if halfEdges == 0
-        then pure (MutableOutsideHull 0)
-        else do
-          onEdge <- findSegment query halfEdges 0
-          case onEdge of
-            Just edge -> pure (MutableOnEdge edge)
-            Nothing -> do
-              firstConnected <- findConnected vertices 0
-              firstOut <- readVertexOut mutable firstConnected
-              let edge = if firstOut < 0 then 0 else firstOut
-              from <- edgeOriginPoint mutable edge
-              to <- edgeOriginPoint mutable (reverseIndex edge)
-              case orient2d from to query of
-                GT -> pure (MutableOutsideHull (orientOuter edge))
-                LT -> pure (MutableOutsideHull (orientOuter (reverseIndex edge)))
-                EQ -> MutableOutsideHull <$> nearestTerminalEdge query halfEdges edge
- where
-  findVertex !query !limit !index
-    | index >= limit = pure Nothing
-    | otherwise = do
-        connected <- isConnected mutable index
-        if not connected
-          then findVertex query limit (index + 1)
-          else do
-            point <- pointAt mutable index
-            if point == query then pure (Just index) else findVertex query limit (index + 1)
-
-  findSegment !query !limit !edge
-    | edge >= limit = pure Nothing
-    | otherwise = do
-        let normalized = edge .&. complementOne
-        from <- edgeOriginPoint mutable normalized
-        to <- edgeOriginPoint mutable (reverseIndex normalized)
-        if onClosedSegment from to query
-          then pure (Just normalized)
-          else findSegment query limit (normalized + 2)
-
-  findConnected !limit !index
-    | index >= limit = pure 0
-    | otherwise = do
-        connected <- isConnected mutable index
-        if connected then pure index else findConnected limit (index + 1)
-
-  orientOuter :: Int -> Int
-  orientOuter edge = edge
-
-  -- A degenerate chain's outer cycle doubles back exactly at its two terminal
-  -- vertices, so @next e == reverse e@ characterises the edge entering a
-  -- terminal. Collinear extension must attach to the terminal nearest the
-  -- query, which for three or more vertices is not an endpoint of any single
-  -- arbitrary edge.
-  nearestTerminalEdge !query !limit !fallback = go 0 Nothing
-   where
-    go !edge !best
-      | edge >= limit = pure (maybe fallback fst best)
-      | otherwise = do
-          edgeNext <- readNext mutable edge
-          if edgeNext /= reverseIndex edge
-            then go (edge + 1) best
-            else do
-              let !terminal = reverseIndex edge
-              point <- edgeOriginPoint mutable terminal
-              let !candidate = squaredDistanceWide query point
-              case best of
-                Just (_, closest) | closest <= candidate -> go (edge + 1) best
-                _ -> go (edge + 1) (Just (terminal, candidate))
-
-  complementOne = -2
-
-walkWithBarrier
-  :: Maybe IntSet.IntSet
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Point
-  -> Int
-  -> ST s (Either BuildError MutableLocation)
-walkWithBarrier barrier mutable operation query initialFace = do
-  faces <- faceCount mutable
-  halfEdges <- directedEdgeCount mutable
-  let !budget = max 8 (faces + halfEdges `quot` 2 + 4)
-  go budget 0 initialFace
- where
-  go !remaining !steps !face
-    | remaining <= 0 = do
-        addCounter operation CounterLocationFallbacks 1
-        maxCounter operation CounterLocationMaxWalk steps
-        pure (Left (LocationWalkExhausted query steps))
-    | face <= 0 = Right . MutableOutsideHull <$> visibleOuterEdge mutable query
-    | otherwise = do
-        addCounter operation CounterLocationWalkSteps 1
-        let !nextSteps = steps + 1
-        probe <- probeFace mutable face query
-        case probe of
-          MutableInFace found -> do
-            writeSTRef (mdLastFace mutable) found
-            maxCounter operation CounterLocationMaxWalk nextSteps
-            pure (Right probe)
-          MutableOnVertex _ -> do
-            writeSTRef (mdLastFace mutable) face
-            maxCounter operation CounterLocationMaxWalk nextSteps
-            pure (Right probe)
-          MutableOnEdge _ -> do
-            writeSTRef (mdLastFace mutable) face
-            maxCounter operation CounterLocationMaxWalk nextSteps
-            pure (Right probe)
-          MutableOutsideHull edge -> do
-            adjacent <- readFace mutable edge
-            if adjacent > 0
-                && maybe False (IntSet.member (edge `quot` 2)) barrier
-              then
-                pure
-                  ( Left
-                      ( RefinementDomainWouldCrossInterface
-                          (UndirectedEdgeId (fromIntegral (edge `quot` 2)))
-                          (FaceId (fromIntegral face))
-                      )
-                  )
-              else
-                if adjacent == 0
-                  then do
-                    maxCounter operation CounterLocationMaxWalk nextSteps
-                    pure (Right (MutableOutsideHull edge))
-                  else go (remaining - 1) nextSteps adjacent
-          MutableEmpty -> pure (Left (PointLocationFailed query))
-
-probeFace :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s MutableLocation
--- 'faceEdges' walks the @next@ chain, so the cycle already states each
--- boundary's destination: @destination e0 = origin e1@. Reading the twin's
--- origin instead asks the store for what the face has already said, at twice
--- the endpoint loads and twice the coordinate loads per probe. Three origins
--- and three points settle all three boundaries. Vertex and edge hits still
--- return on the first boundary that reports one, and a later crossing still
--- displaces an earlier one.
-probeFace mutable face query = do
-  (e0, e1, e2) <- faceEdges mutable face
-  a <- readOrigin mutable e0
-  b <- readOrigin mutable e1
-  c <- readOrigin mutable e2
-  pa <- pointAt mutable a
-  pb <- pointAt mutable b
-  pc <- pointAt mutable c
-  let !first = probeBoundary reverseIndex query e0 a pa b pb
-  case first of
-    BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
-    BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
-    _ -> do
-      let !second = probeBoundary reverseIndex query e1 b pb c pc
-      case second of
-        BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
-        BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
-        _ -> do
-          let !third = probeBoundary reverseIndex query e2 c pc a pa
-          case third of
-            BoundaryOnVertex vertex -> pure (MutableOnVertex vertex)
-            BoundaryOnEdge boundary -> pure (MutableOnEdge boundary)
-            _ -> pure $ case keep third (keep second (keep first Nothing)) of
-              Nothing -> MutableInFace face
-              Just boundary -> MutableOutsideHull boundary
- where
-  keep :: BoundaryProbe boundary probeVertex -> Maybe boundary -> Maybe boundary
-  keep (BoundaryCrossing boundary) _ = Just boundary
-  keep _ held = held
-
-chooseStartFace :: MutableDcel s vertex directed undirected face -> Maybe Int -> ST s Int
-chooseStartFace mutable hint = do
-  faces <- faceCount mutable
-  cached <- readSTRef (mdLastFace mutable)
-  let candidate = case hint of
-        Just face | face > 0 && face < faces -> face
-        _ | cached > 0 && cached < faces -> cached
-        _ -> 1
-  pure candidate
-
-visibleOuterEdge :: MutableDcel s vertex directed undirected face -> Point -> ST s Int
-visibleOuterEdge mutable query = do
-  start <- readFaceEdge mutable 0
-  halfEdges <- directedEdgeCount mutable
-  if start < 0 || halfEdges == 0
-    then pure 0
-    else go (halfEdges + 1) start start Nothing
- where
-  go !remaining !start !edge !fallback
-    | remaining <= 0 = pure (maybe start id fallback)
-    | otherwise = do
-        from <- edgeOriginPoint mutable edge
-        to <- edgeOriginPoint mutable (reverseIndex edge)
-        let !side = orient2d from to query
-            !fallback' = if side /= LT then Just edge else fallback
-        if side == GT
-          then pure edge
-          else do
-            edgeNext <- readNext mutable edge
-            if edgeNext == start then pure (maybe edge id fallback') else go (remaining - 1) start edgeNext fallback'
diff --git a/src-build/Moonlight/Triangulation/Internal/Refinement.hs b/src-build/Moonlight/Triangulation/Internal/Refinement.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Refinement.hs
+++ /dev/null
@@ -1,1707 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Ruppert refinement over three local worklists: forced segment splits,
--- fixed-edge encroachment candidates, and skinny faces. A face candidate is
--- transactional: preflight locates the circumcenter and follows the
--- prospective legalization cavity without mutating topology; a fixed edge the
--- cavity meets is tested against the point's diametral disk and aborts the
--- plan into a segment split; otherwise the located site commits directly.
-module Moonlight.Triangulation.Internal.Refinement
-  ( RefinementInitialSeed (..)
-  , RefinementDomain (..)
-  , refineMutable
-  ) where
-
-import Control.Monad (filterM, forM_, unless, when)
-import Control.Monad.ST (ST)
-import Data.Foldable (traverse_)
-import qualified Data.IntSet as IntSet
-import Data.List (find)
-import qualified Data.List as List
-import qualified Data.IntMap.Strict as IntMap
-import Data.Maybe (catMaybes, fromMaybe)
-import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId (..), UndirectedEdgeId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeEdges)
-import Moonlight.Triangulation.Internal.DcelOperations.Subdivide (insertOnEdge)
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Dyadic (exactDiametralDot)
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , clearGrowable
-  , growableLength
-  , newGrowableWord32
-  , popGrowableOr
-  , pushGrowable
-  , readGrowable
-  )
-import Moonlight.Triangulation.Internal.Location
-  ( MutableLocation (..)
-  , locateMutable
-  , locateMutableWithin
-  )
-import Moonlight.Triangulation.Internal.FaceQueue
-  ( FaceQueue
-  , newFaceQueue
-  , popFace
-  , pushFace
-  )
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel (..)
-  , addEdgeBlock
-  , addFaceBlock
-  , appendVertex
-  , directedEdgeCount
-  , edgeOriginPoint
-  , ensureCellCapacity
-  , faceCount
-  , faceEdges
-  , markConnected
-  , pointAt
-  , pointCapacity
-  , readConstraint
-  , readFace
-  , readNext
-  , readOrigin
-  , readPrevious
-  , readVertexOut
-  , resetEdgeData
-  , resetFaceData
-  , setCycle3
-  , writeOrigin
-  , writeVertexOut
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , maxCounter
-  , readScratch
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Predicates
-  ( inCircle
-  , inDiametralCircle
-  , orient2d
-  )
-import Moonlight.Triangulation.Internal.Probe (Probe (..))
-import Moonlight.Triangulation.Math
-  ( canonicalPoint
-  , circumcenter
-  , isFinite
-  , midpoint
-  , squaredDistance
-  , squaredDistanceWide
-  , triangleArea
-  , triangleRadiusEdgeRatioSquaredWithArea
-  , triangleRadiusEdgeRatioSquaredWithAreaAndSides
-  , validateCoordinate
-  )
-import Moonlight.Triangulation.Insertion (insertExistingVertexAtLocation)
-import Moonlight.Triangulation.Types
-  ( BuildError (..)
-  , Point (..)
-  , RefinementParameters (..)
-  )
-
--- | The worklists refinement owns for the duration of one transaction. Cavity
--- and segment queues are local to the current rewrite; the face-capacity mark
--- planes remain a bounded publication workspace until the compact arena is
--- replaced by a domain-indexed one.
-data BoolMarks s
-  = DenseBoolMarks !(MUV.MVector s Bool)
-  | SparseBoolMarks !(STRef s IntSet.IntSet)
-
-data EpochMarks s
-  = DenseEpochMarks !(MUV.MVector s Word32)
-  | SparseEpochMarks !(STRef s (IntMap.IntMap Word32))
-
-data PairSlots s
-  = DensePairSlots !(MUV.MVector s Word32)
-  | SparsePairSlots !(STRef s (IntMap.IntMap Word32))
-
-data SegmentOrigins s
-  = DenseSegmentOrigins !(MUV.MVector s Word32)
-  | SparseSegmentOrigins !(STRef s (IntMap.IntMap (Word32, Word32)))
-
-newBoolMarks :: Maybe Int -> ST s (BoolMarks s)
-newBoolMarks capacity =
-  case capacity of
-    Just size -> DenseBoolMarks <$> MUV.replicate (max 1 size) False
-    Nothing -> SparseBoolMarks <$> newSTRef IntSet.empty
-
-readBoolMark :: BoolMarks s -> Int -> ST s Bool
-readBoolMark marks index =
-  case marks of
-    DenseBoolMarks values -> MUV.unsafeRead values index
-    SparseBoolMarks values -> IntSet.member index <$> readSTRef values
-
-writeBoolMark :: BoolMarks s -> Int -> Bool -> ST s ()
-writeBoolMark marks index enabled =
-  case marks of
-    DenseBoolMarks values -> MUV.unsafeWrite values index enabled
-    SparseBoolMarks values ->
-      modifySTRef' values
-        (if enabled then IntSet.insert index else IntSet.delete index)
-
-newEpochMarks :: Maybe Int -> ST s (EpochMarks s)
-newEpochMarks capacity =
-  case capacity of
-    Just size -> DenseEpochMarks <$> MUV.replicate (max 1 size) 0
-    Nothing -> SparseEpochMarks <$> newSTRef IntMap.empty
-
-readEpochMark :: EpochMarks s -> Int -> ST s Word32
-readEpochMark marks index =
-  case marks of
-    DenseEpochMarks values -> MUV.unsafeRead values index
-    SparseEpochMarks values -> fromMaybe 0 . IntMap.lookup index <$> readSTRef values
-
-writeEpochMark :: EpochMarks s -> Int -> Word32 -> ST s ()
-writeEpochMark marks index epoch =
-  case marks of
-    DenseEpochMarks values -> MUV.unsafeWrite values index epoch
-    SparseEpochMarks values -> modifySTRef' values (IntMap.insert index epoch)
-
-clearEpochMarks :: EpochMarks s -> ST s ()
-clearEpochMarks marks =
-  case marks of
-    DenseEpochMarks values -> MUV.set values 0
-    SparseEpochMarks values -> writeSTRef values IntMap.empty
-
-newPairSlots :: Maybe Int -> ST s (PairSlots s)
-newPairSlots capacity =
-  case capacity of
-    Just size -> DensePairSlots <$> MUV.replicate (max 1 size) noIndex
-    Nothing -> SparsePairSlots <$> newSTRef IntMap.empty
-
-readPairSlot :: PairSlots s -> Int -> ST s Word32
-readPairSlot slots pair =
-  case slots of
-    DensePairSlots values -> MUV.unsafeRead values pair
-    SparsePairSlots values -> fromMaybe noIndex . IntMap.lookup pair <$> readSTRef values
-
-writePairSlot :: PairSlots s -> Int -> Word32 -> ST s ()
-writePairSlot slots pair value =
-  case slots of
-    DensePairSlots values -> MUV.unsafeWrite values pair value
-    SparsePairSlots values ->
-      modifySTRef' values
-        (if value == noIndex then IntMap.delete pair else IntMap.insert pair value)
-
-newSegmentOrigins :: Maybe Int -> ST s (SegmentOrigins s)
-newSegmentOrigins capacity =
-  case capacity of
-    Just size -> DenseSegmentOrigins <$> MUV.replicate (2 * max 1 size) noIndex
-    Nothing -> SparseSegmentOrigins <$> newSTRef IntMap.empty
-
-readSegmentOrigin :: SegmentOrigins s -> Int -> ST s (Word32, Word32)
-readSegmentOrigin origins vertex = do
-  case origins of
-    DenseSegmentOrigins values ->
-      (,)
-        <$> MUV.unsafeRead values (2 * vertex)
-        <*> MUV.unsafeRead values (2 * vertex + 1)
-    SparseSegmentOrigins values -> do
-      stored <- readSTRef values
-      pure (fromMaybe (noIndex, noIndex) (IntMap.lookup vertex stored))
-
-writeSegmentOrigin :: SegmentOrigins s -> Int -> Word32 -> Word32 -> ST s ()
-writeSegmentOrigin origins vertex first second =
-  case origins of
-    DenseSegmentOrigins values -> do
-      MUV.unsafeWrite values (2 * vertex) first
-      MUV.unsafeWrite values (2 * vertex + 1) second
-    SparseSegmentOrigins values ->
-      modifySTRef' values (IntMap.insert vertex (first, second))
-
-data Workspace s = Workspace
-  { wsQueue :: !(FaceQueue s)
-  , wsExcluded :: !(BoolMarks s)
-  , wsPermitted :: !(Maybe (BoolMarks s))
-  , wsVisited :: !(Maybe (BoolMarks s))
-  , wsInterfaceBoundaryReads :: !(STRef s Int)
-  , wsBoundaryCrossingAttempts :: !(STRef s Int)
-  , wsForcedSplits :: !(GrowableWord32 s)
-  , wsEncroachment :: !(GrowableWord32 s)
-  -- | Where each pair's live entry sits in 'wsEncroachment', 'noIndex' when
-  -- the pair is not queued. A re-push relocates the pair to the top and leaves
-  -- the older entry behind as a slot that no longer names it, so a pair is
-  -- tested once at its most recent offer instead of once per offer.
-  , wsEncroachmentSlot :: !(PairSlots s)
-  -- | Steiner vertices on constraints carry the original segment's endpoint
-  -- pair, two words per vertex, 'noIndex' when the vertex is not on one.
-  , wsSegmentOrigin :: !(SegmentOrigins s)
-  -- | Cavity discovery state: an epoch stamp per face, edge pair and boundary
-  -- vertex, plus the three records a successful commit consumes. A cavity is
-  -- explored without mutating topology; the commit either fans the recorded
-  -- boundary or abandons the records entirely.
-  , wsCavityEpoch :: !(STRef s Word32)
-  , wsCavityFaceMarks :: !(EpochMarks s)
-  , wsCavityPairMarks :: !(EpochMarks s)
-  , wsCavityVertexMarks :: !(EpochMarks s)
-  , wsCavityFaces :: !(GrowableWord32 s)
-  , wsCavityInternal :: !(GrowableWord32 s)
-  , wsCavityBoundary :: !(GrowableWord32 s)
-  , wsCavityCocircular :: !(GrowableWord32 s)
-  -- | The recorded boundary in cycle order, written by the chaining walk and
-  -- read by the fan.
-  , wsCavityChain :: !(GrowableWord32 s)
-  -- | The flood and hull-simulation stack. The old mesh-global work stack
-  -- served both; the transaction owns it now, cleared before either use.
-  , wsCavityWork :: !(GrowableWord32 s)
-  }
-
-data FaceHint
-  = FaceAcceptable
-  | FaceMustSplitLongestEdge
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Double
-  | FaceMustRefine
-  | FaceShouldRefine
-
-data EdgeSplitLaw
-  = SplitFixedSegment
-  | SplitQualityEdge
-
--- | The initial support a refinement run is allowed to inspect. Subsequent
--- cavity, star, and fixed-edge propagation remains exactly the ordinary
--- refinement law; only the first offers differ. The constructor is internal
--- so public callers cannot smuggle a mutable worklist across the transaction
--- boundary.
-data RefinementInitialSeed
-  = RefineEveryFace
-  | RefineSeededFaces !IntSet.IntSet
-
--- | A checked local section. Face and edge integers are admitted by the
--- immutable constructor in "Moonlight.Triangulation.Refinement"; the mutable
--- interpreter sees only the already-descended dense membership witnesses.
-data RefinementDomain = RefinementDomain
-  { refinementDomainPermittedFaces :: !IntSet.IntSet
-  , refinementDomainInterfacePairs :: !IntSet.IntSet
-  , refinementDomainInputFaceCount :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Show)
-
--- | Refine the existing finite DCEL. Fixed edges (constraints and the convex
--- hull) are legal barriers: a candidate whose prospective cavity meets one
--- inside its diametral disk is abandoned and the segment is split instead.
--- The result is @(worklists-drained, Steiner-count, excluded-outer-faces)@.
-refineMutable
-  :: forall s vertex directed undirected face. (Point -> vertex)
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> RefinementParameters
-  -> Int
-  -> IntSet.IntSet
-  -> RefinementInitialSeed
-  -> Maybe RefinementDomain
-  -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
-refineMutable makeVertex mutable operation parameters budget initialExcludedFaces initialSeed domain = do
-  let !faceBound =
-        case domain of
-          Nothing -> Just (3 * pointCapacity mutable + 8)
-          Just _ -> Nothing
-      !pairBound =
-        case domain of
-          Nothing -> Just (mdHalfCapacity mutable `quot` 2)
-          Just _ -> Nothing
-      !vertexBound =
-        case domain of
-          Nothing -> Just (pointCapacity mutable)
-          Just _ -> Nothing
-  wsQueue <- newFaceQueue faceBound
-  wsExcluded <- newBoolMarks faceBound
-  wsPermitted <- traverse (const (newBoolMarks Nothing)) domain
-  wsVisited <- traverse (const (newBoolMarks Nothing)) domain
-  wsInterfaceBoundaryReads <- newSTRef 0
-  wsBoundaryCrossingAttempts <- newSTRef 0
-  traverse_
-    (\membership ->
-       traverse_
-         (\face -> writeBoolMark membership face True)
-         (maybe [] (IntSet.toAscList . refinementDomainPermittedFaces) domain)
-    )
-    wsPermitted
-  when (refineExcludeOuterFaces parameters) $
-    traverse_ (\face -> writeBoolMark wsExcluded face True)
-      (IntSet.toAscList initialExcludedFaces)
-  wsForcedSplits <- newGrowableWord32 16
-  wsEncroachment <- newGrowableWord32 64
-  wsEncroachmentSlot <- newPairSlots pairBound
-  wsSegmentOrigin <- newSegmentOrigins vertexBound
-  wsCavityEpoch <- newSTRef 0
-  wsCavityFaceMarks <- newEpochMarks faceBound
-  wsCavityPairMarks <- newEpochMarks pairBound
-  wsCavityVertexMarks <- newEpochMarks vertexBound
-  wsCavityFaces <- newGrowableWord32 16
-  wsCavityInternal <- newGrowableWord32 16
-  wsCavityBoundary <- newGrowableWord32 16
-  wsCavityCocircular <- newGrowableWord32 4
-  wsCavityChain <- newGrowableWord32 16
-  wsCavityWork <- newGrowableWord32 16
-  let workspace = Workspace{wsQueue, wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts, wsForcedSplits, wsEncroachment, wsEncroachmentSlot, wsSegmentOrigin, wsCavityEpoch, wsCavityFaceMarks, wsCavityPairMarks, wsCavityVertexMarks, wsCavityFaces, wsCavityInternal, wsCavityBoundary, wsCavityCocircular, wsCavityChain, wsCavityWork}
-  seedInitialWork workspace initialSeed
-  loop workspace IntMap.empty 0
- where
-  !limit = max 0 budget
-
-  -- The three bounds every queued face is measured against. They are fixed for
-  -- the whole run, and the ratio bound is squared once here rather than once
-  -- per face the ratio test reaches.
-  !maximumArea = refineMaxArea parameters
-  !minimumArea = refineMinArea parameters
-  !maximumSquaredEdgeLength = fmap (\bound -> bound * bound) (refineMaxEdgeLength parameters)
-  !squaredRatioBound = case refineMaxRadiusEdgeRatio parameters of
-    Nothing -> Nothing
-    Just bound -> Just $! squareBound bound
-
-  -- One charged stack push: the old mesh-global 'pushWork' wrapper fed the
-  -- legalization depth counter on every push, and the hull-simulation sites
-  -- used that wrapper deliberately. The cavity flood's bypass stays raw.
-  pushChargedWork :: GrowableWord32 s -> Int -> ST s ()
-  pushChargedWork work value = do
-    pushGrowable work (packIndex value)
-    size <- growableLength work
-    maxCounter operation CounterLegalizationMaxStack size
-
-  -- Offer a fixed edge pair for the encroachment question. An offer always
-  -- wins the pair's slot, so the pair is answered at its most recent offer and
-  -- the entries it left behind are recognised as superseded when reached.
-  pushEncroachment :: Workspace s -> Int -> ST s ()
-  pushEncroachment Workspace{wsEncroachment, wsEncroachmentSlot} pair = do
-    index <- growableLength wsEncroachment
-    pushGrowable wsEncroachment (packIndex pair)
-    writePairSlot wsEncroachmentSlot pair (packIndex index)
-
-  -- 'noIndex' when the offers are drained.
-  popEncroachment :: Workspace s -> ST s Word32
-  popEncroachment Workspace{wsEncroachment, wsEncroachmentSlot} = go
-   where
-    go = do
-      size <- growableLength wsEncroachment
-      if size <= 0
-        then pure noIndex
-        else do
-          let !index = size - 1
-          packed <- popGrowableOr noIndex wsEncroachment
-          let !pair = fromIntegral packed
-          slot <- readPairSlot wsEncroachmentSlot pair
-          if slot == packIndex index
-            then do
-              writePairSlot wsEncroachmentSlot pair noIndex
-              pure packed
-            else go
-
-  -- The cavity epoch stamps three planes and is compared against them, so the
-  -- wrap has to retire every stamp a reused value would answer for.
-  nextCavityEpoch :: Workspace s -> ST s Word32
-  nextCavityEpoch Workspace{wsCavityEpoch, wsCavityFaceMarks, wsCavityPairMarks, wsCavityVertexMarks} = do
-    current <- readSTRef wsCavityEpoch
-    let !next = current + 1
-    if next == 0
-      then do
-        clearEpochMarks wsCavityFaceMarks
-        clearEpochMarks wsCavityPairMarks
-        clearEpochMarks wsCavityVertexMarks
-        writeSTRef wsCavityEpoch 1
-        pure 1
-      else do
-        writeSTRef wsCavityEpoch next
-        pure next
-
-  loop
-    :: Workspace s
-    -> IntMap.IntMap Double
-    -> Int
-    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
-  loop workspace@Workspace{wsQueue, wsForcedSplits} pendingOversizedEdges !added
-    | added >= limit = finish workspace pendingOversizedEdges False added
-    | otherwise = do
-        forced <- popGrowableOr noIndex wsForcedSplits
-        if forced /= noIndex
-          then do
-            splitOutcome <- resolveSplit SplitFixedSegment workspace (fromIntegral forced)
-            case splitOutcome of
-              Left obstruction -> pure (Left obstruction)
-              Right split ->
-                if split
-                  then loop workspace pendingOversizedEdges (added + 1)
-                  else do
-                    -- The split refused (degenerate position or a kept
-                    -- constraint). Retrying the face that forced it would loop,
-                    -- so one queued face is sacrificed, exactly the face whose
-                    -- requeue sits on top.
-                    _ <- popFace wsQueue
-                    loop workspace pendingOversizedEdges added
-          else do
-            candidate <- popEncroachment workspace
-            if candidate /= noIndex
-              then do
-                splitOutcome <- checkEncroachment workspace (fromIntegral candidate)
-                case splitOutcome of
-                  Left obstruction -> pure (Left obstruction)
-                  Right split ->
-                    loop
-                      workspace
-                      pendingOversizedEdges
-                      (if split then added + 1 else added)
-              else do
-                next <- popFace wsQueue
-                case next of
-                  Nothing -> finish workspace pendingOversizedEdges True added
-                  Just face -> do
-                    outcome <- handleFace workspace face
-                    case outcome of
-                      Left failure -> pure (Left failure)
-                      Right (gained, pendingEdge) ->
-                        loop
-                          workspace
-                          ( maybe
-                              pendingOversizedEdges
-                              (\(pair, actualSquaredLength) ->
-                                 IntMap.insertWith
-                                   max
-                                   pair
-                                   actualSquaredLength
-                                   pendingOversizedEdges
-                              )
-                              pendingEdge
-                          )
-                          (added + gained)
-
-  finish
-    :: Workspace s
-    -> IntMap.IntMap Double
-    -> Bool
-    -> Int
-    -> ST s (Either BuildError (Bool, Int, [Int], [Int], [Int], Int, Int))
-  finish workspace@Workspace{wsExcluded, wsPermitted, wsVisited, wsInterfaceBoundaryReads, wsBoundaryCrossingAttempts} pendingOversizedEdges complete added = do
-    pendingObstruction <-
-      if complete
-        then completedMaximumEdgeObstruction workspace pendingOversizedEdges
-        else pure (Right Nothing)
-    case pendingObstruction of
-      Left failure -> pure (Left failure)
-      Right (Just failure) -> pure (Left failure)
-      Right Nothing -> do
-        excludedFaces <- markedIndices wsExcluded
-        visitedFaces <- maybe (pure []) markedIndices wsVisited
-        permittedFaces <- maybe (pure []) markedIndices wsPermitted
-        interfaceBoundaryReads <- readSTRef wsInterfaceBoundaryReads
-        boundaryCrossingAttempts <- readSTRef wsBoundaryCrossingAttempts
-        pure (Right (complete, added, excludedFaces, visitedFaces, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts))
-
-  -- A quality edge that cannot be split is remembered at the point where the
-  -- worklist discovers it.  Only a drained run discharges the pending section;
-  -- a budget-limited run remains an ordinary resumable publication.  The
-  -- final face is read from the constant-degree live edge neighbourhood so the
-  -- reported obstruction retains the former face/edge ordering without an
-  -- all-face audit.
-  completedMaximumEdgeObstruction
-    :: Workspace s
-    -> IntMap.IntMap Double
-    -> ST s (Either BuildError (Maybe BuildError))
-  completedMaximumEdgeObstruction workspace pendingOversizedEdges =
-    case refineMaxEdgeLength parameters of
-      Nothing -> pure (Right Nothing)
-      Just maximumLength -> do
-        candidateResults <-
-          traverse
-            (liveOversizedEdgeCandidate workspace)
-            (IntMap.toAscList pendingOversizedEdges)
-        pure $ do
-          candidates <- catMaybes <$> sequence candidateResults
-          Right
-            ( case List.sortOn (\(face, pair, _) -> (face, pair)) candidates of
-                [] -> Nothing
-                (face, pair, actualSquaredLength) : _ ->
-                  Just
-                    ( RefinementOversizedEdge
-                        (FaceId (fromIntegral face))
-                        (UndirectedEdgeId (fromIntegral pair))
-                        (sqrt actualSquaredLength)
-                        maximumLength
-                    )
-            )
-
-  liveOversizedEdgeCandidate
-    :: Workspace s
-    -> (Int, Double)
-    -> ST s (Either BuildError (Maybe (Int, Int, Double)))
-  liveOversizedEdgeCandidate workspace (pair, actualSquaredLength) = do
-    directedTotal <- directedEdgeCount mutable
-    let directed = 2 * pair
-    if directed < 0 || directed + 1 >= directedTotal
-      then pure (Left RefinementDomainTopologyChanged)
-      else do
-        forwardFace <- readFace mutable directed
-        backwardFace <- readFace mutable (reverseIndex directed)
-        eligibleFaces <-
-          filterM
-            (maximumEdgeFaceEligible workspace)
-            [forwardFace, backwardFace]
-        pure
-          ( Right
-              ( case eligibleFaces of
-                  [] -> Nothing
-                  _ -> Just (minimum eligibleFaces, pair, actualSquaredLength)
-              )
-          )
-
-  maximumEdgeFaceEligible :: Workspace s -> Int -> ST s Bool
-  maximumEdgeFaceEligible Workspace{wsExcluded, wsPermitted} face
-    | face <= 0 = pure False
-    | otherwise = do
-        excluded <- readBoolMark wsExcluded face
-        permitted <-
-          maybe
-            (pure True)
-            (\membership -> readBoolMark membership face)
-            wsPermitted
-        pure (not excluded && permitted)
-
-  markedIndices :: BoolMarks s -> ST s [Int]
-  markedIndices marks =
-    case marks of
-      DenseBoolMarks values -> do
-        let indices = [0 .. MUV.length values - 1]
-        filterM (MUV.unsafeRead values) indices
-      SparseBoolMarks values -> IntSet.toAscList <$> readSTRef values
-
-
-  offerAll :: FaceQueue s -> ST s ()
-  offerAll queue = do
-    faces <- faceCount mutable
-    forM_ [1 .. faces - 1] (pushFace queue)
-
-  -- A seeded run is not a weaker refinement interpreter. It begins at the
-  -- supplied faces and asks only the fixed edges those faces can immediately
-  -- encroach; every later cavity and star contributes its own local closure
-  -- through the same queue and encroachment machinery as the global entry.
-  seedInitialWork :: Workspace s -> RefinementInitialSeed -> ST s ()
-  seedInitialWork workspace seed =
-    case seed of
-      RefineEveryFace -> seedFixedEdges workspace >> offerAll (wsQueue workspace)
-      RefineSeededFaces faces ->
-        traverse_ (seedFace workspace) (IntSet.toAscList faces)
-
-  seedFace :: Workspace s -> Int -> ST s ()
-  seedFace workspace face = do
-    pushFace (wsQueue workspace) face
-    (e0, e1, e2) <- faceEdges mutable face
-    traverse_ (offerFixedEdge workspace) [e0, e1, e2]
-
-  -- Every fixed edge present at entry, queued once for the existing-vertex
-  -- encroachment question. Later candidates arrive from the star walks of the
-  -- insertions that could have created a new encroachment.
-  seedFixedEdges :: Workspace s -> ST s ()
-  seedFixedEdges workspace = do
-    halfEdges <- directedEdgeCount mutable
-    traverse_
-      (offerFixedEdge workspace . (2 *))
-      [0 .. halfEdges `quot` 2 - 1]
-
-  offerFixedEdge :: Workspace s -> Int -> ST s ()
-  offerFixedEdge workspace directed = do
-    fixed <- isFixedEdge directed
-    let !pair = directed `quot` 2
-    -- An interface is a fixed boundary condition, not a quality obligation
-    -- owned by this section. It is still encountered by candidate cavities,
-    -- where any demand to split or cross it is a typed obstruction.
-    when (fixed && not (isInterfacePair pair)) $
-      pushEncroachment workspace pair
-
-  isFixedEdge :: Int -> ST s Bool
-  isFixedEdge directed = do
-    if isInterfacePair (directed `quot` 2)
-      then pure True
-      else do
-        protected <- readConstraint mutable directed
-        if protected
-          then pure True
-          else do
-            own <- readFace mutable directed
-            if own == 0
-              then pure True
-              else (== 0) <$> readFace mutable (reverseIndex directed)
-
-  isInterfacePair :: Int -> Bool
-  isInterfacePair pair =
-    maybe False (IntSet.member pair . refinementDomainInterfacePairs) domain
-  {-# INLINE isInterfacePair #-}
-
-  isHullPair :: Int -> ST s Bool
-  isHullPair pair = do
-    forwardFace <- readFace mutable (2 * pair)
-    backwardFace <- readFace mutable (2 * pair + 1)
-    pure (forwardFace == 0 || backwardFace == 0)
-
-  interfaceCrossing :: Workspace s -> Int -> ST s BuildError
-  interfaceCrossing Workspace{wsPermitted, wsBoundaryCrossingAttempts} pair = do
-    modifySTRef' wsBoundaryCrossingAttempts (+ 1)
-    forwardFace <- readFace mutable (2 * pair)
-    backwardFace <- readFace mutable (2 * pair + 1)
-    joinFace <-
-      case wsPermitted of
-        Nothing -> pure Nothing
-        Just permitted -> do
-          forwardPermitted <- dynamicPermitted permitted forwardFace
-          backwardPermitted <- dynamicPermitted permitted backwardFace
-          pure
-            ( if forwardPermitted
-                then Just forwardFace
-                else if backwardPermitted then Just backwardFace else Nothing
-            )
-    let result =
-          case joinFace of
-            Just face ->
-              RefinementDomainWouldCrossInterface
-                (UndirectedEdgeId (fromIntegral pair))
-                (FaceId (fromIntegral face))
-            Nothing -> RefinementDomainTopologyChanged
-    pure result
-   where
-    dynamicPermitted :: BoolMarks s -> Int -> ST s Bool
-    dynamicPermitted membership face =
-      if face > 0 then readBoolMark membership face else pure False
-
-  -- The edge bound is a hard geometric law and therefore precedes the minimum
-  -- area short-circuit: a long, thin face must get a direct longest-edge split
-  -- even when its area is below the configured minimum. The area bound then
-  -- condemns outright ('FaceMustRefine'); the ratio bound only invites
-  -- refinement, which the input-angle guard may still decline.
-  faceHint :: Int -> Int -> Int -> Point -> Point -> Point -> FaceHint
-  faceHint e0 e1 e2 p0 p1 p2 =
-    case maximumSquaredEdgeLength of
-      Just maximumSquaredLength ->
-        let (!longestEdge, !longestSquaredLength, !side01, !side12, !side20) =
-              faceEdgeMetrics e0 e1 e2 p0 p1 p2
-         in if exceeds longestSquaredLength maximumSquaredLength
-              then
-                FaceMustSplitLongestEdge
-                  (longestEdge `quot` 2)
-                  longestSquaredLength
-              else
-                nonEdgeHint
-                  ( ratioBad
-                      ( triangleRadiusEdgeRatioSquaredWithAreaAndSides
-                          area
-                          side01
-                          side12
-                          side20
-                      )
-                  )
-      Nothing ->
-        nonEdgeHint
-          (ratioBad (triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2))
-   where
-    !area = triangleArea p0 p1 p2
-    areaBad = maybe False (exceeds area) maximumArea
-    belowMinimum = maybe False (area <) minimumArea
-    ratioBad :: Double -> Bool
-    ratioBad ratio = case squaredRatioBound of
-      Nothing -> False
-      Just bound -> exceeds ratio bound
-    nonEdgeHint angleBad
-      | areaBad = FaceMustRefine
-      | belowMinimum = FaceAcceptable
-      | angleBad = FaceShouldRefine
-      | otherwise = FaceAcceptable
-
-  handleFace
-    :: Workspace s
-    -> Int
-    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
-  handleFace workspace@Workspace{wsExcluded, wsPermitted, wsVisited} face = do
-    addCounter operation CounterRefinementQueuePops 1
-    permitted <-
-      maybe
-        (pure True)
-        (\membership -> readBoolMark membership face)
-        wsPermitted
-    if not permitted
-      then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral face))))
-      else do
-        traverse_ (\visited -> writeBoolMark visited face True) wsVisited
-        skip <- readBoolMark wsExcluded face
-        if skip
-          then pure (Right (0, Nothing))
-          else do
-            addCounter operation CounterRefinementFaceChecks 1
-            (e0, e1, e2) <- faceEdges mutable face
-            p0 <- edgeOriginPoint mutable e0
-            p1 <- edgeOriginPoint mutable e1
-            p2 <- edgeOriginPoint mutable e2
-            case faceHint e0 e1 e2 p0 p1 p2 of
-              FaceAcceptable -> pure (Right (0, Nothing))
-              FaceMustSplitLongestEdge pair actualSquaredLength ->
-                qualitySplit workspace pair actualSquaredLength
-              FaceMustRefine ->
-                fmap
-                  (fmap (\gained -> (gained, Nothing)))
-                  (attemptCandidate workspace face p0 p1 p2)
-              FaceShouldRefine -> do
-                blocked <- inputAngleBlocks workspace e0 e1 e2 p0 p1 p2
-                if blocked
-                  then pure (Right (0, Nothing))
-                  else
-                    fmap
-                      (fmap (\gained -> (gained, Nothing)))
-                      (attemptCandidate workspace face p0 p1 p2)
-
-  faceEdgeMetrics
-    :: Int
-    -> Int
-    -> Int
-    -> Point
-    -> Point
-    -> Point
-    -> (Int, Double, Double, Double, Double)
-  faceEdgeMetrics e0 e1 e2 p0 p1 p2
-    | side01 >= side12 && side01 >= side20 = result e0 side01
-    | side12 >= side20 = result e1 side12
-    | otherwise = result e2 side20
-   where
-    !side01 = squaredDistanceWide p0 p1
-    !side12 = squaredDistanceWide p1 p2
-    !side20 = squaredDistanceWide p2 p0
-    result :: Int -> Double -> (Int, Double, Double, Double, Double)
-    result edge longest = (edge, longest, side01, side12, side20)
-
-  -- An unsplittable quality edge is a pending law obstruction, not permission
-  -- to cross a frozen interface and not immediate failure of a resumable run.
-  qualitySplit
-    :: Workspace s
-    -> Int
-    -> Double
-    -> ST s (Either BuildError (Int, Maybe (Int, Double)))
-  qualitySplit workspace pair actualSquaredLength = do
-    outcome <- resolveSplit SplitQualityEdge workspace pair
-    case outcome of
-      Left (RefinementDomainWouldCrossInterface _ _) ->
-        pure (Right (0, Just (pair, actualSquaredLength)))
-      Left failure -> pure (Left failure)
-      Right True -> pure (Right (1, Nothing))
-      Right False -> pure (Right (0, Just (pair, actualSquaredLength)))
-
-  -- Two subsegments of one original segment meeting at a small input angle
-  -- cannot be refined apart: the angle is fixed by the input geometry. When
-  -- the shortest edge of a ratio-bound face joins two Steiner vertices whose
-  -- lineages share an original endpoint, the face is as good as it gets.
-  inputAngleBlocks :: Workspace s -> Int -> Int -> Int -> Point -> Point -> Point -> ST s Bool
-  inputAngleBlocks Workspace{wsSegmentOrigin} e0 e1 e2 p0 p1 p2 = do
-    let !shortest = shortestFaceEdge e0 e1 e2 p0 p1 p2
-    a <- readOrigin mutable shortest
-    b <- readOrigin mutable (reverseIndex shortest)
-    shared <- sharedSegmentOrigin wsSegmentOrigin a b
-    if not shared
-      then pure False
-      else not <$> isFixedEdge shortest
-
-  shortestFaceEdge :: Int -> Int -> Int -> Point -> Point -> Point -> Int
-  shortestFaceEdge e0 e1 e2 p0 p1 p2
-    | side01 <= side12 && side01 <= side20 = e0
-    | side12 <= side20 = e1
-    | otherwise = e2
-   where
-    !side01 = squaredDistanceWide p0 p1
-    !side12 = squaredDistanceWide p1 p2
-    !side20 = squaredDistanceWide p2 p0
-
-  sharedSegmentOrigin :: SegmentOrigins s -> Int -> Int -> ST s Bool
-  sharedSegmentOrigin lineage a b = do
-    (a0, a1) <- readSegmentOrigin lineage a
-    (b0, b1) <- readSegmentOrigin lineage b
-    pure (a0 /= noIndex && b0 /= noIndex && (a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1))
-
-  validCandidate :: Point -> Maybe (Point)
-  validCandidate point@(Point x y)
-    | validateCoordinate x == Nothing && validateCoordinate y == Nothing = Just point
-    | otherwise = Nothing
-
-  attemptCandidate :: Workspace s -> Int -> Point -> Point -> Point -> ST s (Either BuildError Int)
-  attemptCandidate workspace@Workspace{wsExcluded, wsForcedSplits} face p0 p1 p2 =
-    case circumcenter p0 p1 p2 >>= validCandidate of
-      Nothing -> pure (Right 0)
-      Just point -> do
-        located <-
-          case domain of
-            Nothing -> locateMutable mutable operation (Just face) point
-            Just refinementDomain ->
-              locateMutableWithin
-                (refinementDomainInterfacePairs refinementDomain)
-                mutable
-                operation
-                (Just face)
-                point
-        case located of
-          -- The local locator has proved that the circumcenter lies beyond
-          -- Γ without crossing it.  This is the same boundary-limited fixed
-          -- point as a circumcenter outside a preserved hull: no admissible
-          -- rewrite exists inside this section, so the face is discharged.
-          -- Actual cavity demands involving Γ remain typed obstructions in
-          -- 'exploreThenCommit'.
-          Left (RefinementDomainWouldCrossInterface _ _) -> do
-            modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)
-            pure (Right 0)
-          Left failure -> pure (Left failure)
-          Right site -> case site of
-            MutableOnVertex _ -> pure (Right 0)
-            MutableEmpty -> pure (Right 0)
-            MutableOutsideHull edge
-              | refinePreserveConvexHull parameters -> pure (Right 0)
-              | otherwise -> attemptOutside workspace face point edge
-            MutableOnEdge edge -> do
-              hull <- case domain of
-                Nothing -> pure False
-                Just _ -> isHullPair (edge `quot` 2)
-              if hull
-                then pure (Right 0)
-                else if isInterfacePair (edge `quot` 2)
-                then Left <$> interfaceCrossing workspace (edge `quot` 2)
-                else do
-                  protected <- readConstraint mutable edge
-                  if protected
-                    then do
-                      -- A circumcenter on a constraint is a request to split that
-                      -- constraint, unless constraints are kept whole.
-                      unless (refineKeepConstraintEdges parameters) $
-                        pushGrowable wsForcedSplits (packIndex (edge `quot` 2))
-                      pure (Right 0)
-                    else exploreThenCommit workspace face point site
-            MutableInFace under -> do
-              permittedSite <-
-                maybe
-                  (pure True)
-                  (\membership -> readBoolMark membership under)
-                  (wsPermitted workspace)
-              if not permittedSite
-                then pure (Left (RefinementDomainWouldRewriteProtectedFace (FaceId (fromIntegral under))))
-                else do
-                  excludedSite <- readBoolMark wsExcluded under
-                  if excludedSite
-                    then pure (Right 0)
-                    else exploreThenCommit workspace face point site
-
-  -- Follow the prospective legalization cavity of the candidate without
-  -- mutating topology, recording what the commit will consume: the cavity
-  -- faces, the internal edge pairs, and the ordered boundary. Fixed edges are
-  -- never crossed: the ones the cavity meets become boundary and are tested
-  -- against the candidate's diametral disk. Every encroached fixed edge met
-  -- is queued for splitting and the source face is revisited afterwards; a
-  -- clean cavity is committed directly.
-  exploreThenCommit :: Workspace s -> Int -> Point -> MutableLocation -> ST s (Either BuildError Int)
-  exploreThenCommit workspace@Workspace{wsQueue, wsForcedSplits} face point site = do
-    (visible, encroached) <- exploreCavity workspace point site
-    case find isInterfacePair encroached of
-      Just pair
-        | all isInterfacePair encroached -> pure (Right 0)
-        | otherwise -> Left <$> interfaceCrossing workspace pair
-      Nothing
-        | null encroached -> commitCavity workspace point site visible
-        | otherwise ->
-            do
-              pushable <- filterM splittablePair encroached
-              traverse_ (\pair -> pushGrowable wsForcedSplits (packIndex pair)) pushable
-              unless (null pushable) $ pushFace wsQueue face
-              pure (Right 0)
-
-  splittablePair :: Int -> ST s Bool
-  splittablePair pair = do
-    hull <- case domain of
-      Nothing -> pure False
-      Just _ -> isHullPair pair
-    if hull || isInterfacePair pair
-      then pure False
-      else if refineKeepConstraintEdges parameters
-        then not <$> readConstraint mutable (2 * pair)
-        else pure True
-
-  admitStar :: Workspace s -> Int -> ST s ()
-  admitStar Workspace{wsPermitted} vertex =
-    case wsPermitted of
-      Just membership ->
-        forEachStarFace vertex $ \face -> rememberPermittedFace membership face
-      Nothing -> pure ()
-
-  rememberPermittedFace :: BoolMarks s -> Int -> ST s ()
-  rememberPermittedFace membership face =
-    if face <= 0
-      then pure ()
-      else do
-        already <- readBoolMark membership face
-        unless already $ do
-          writeBoolMark membership face True
-
-  retirePermittedFace :: Workspace s -> Int -> ST s ()
-  retirePermittedFace Workspace{wsPermitted} face = do
-    when (face >= 0) $ do
-      traverse_
-        (\membership -> writeBoolMark membership face False)
-        wsPermitted
-
-  retirePermittedFaces :: Workspace s -> GrowableWord32 s -> ST s ()
-  retirePermittedFaces workspace faces = do
-    count <- growableLength faces
-    traverse_
-      (\index -> do
-         face <- unpackIndex <$> readGrowable faces index
-         retirePermittedFace workspace face
-      )
-      [0 .. count - 1]
-
-  captureCavityFaces :: GrowableWord32 s -> ST s [(Int, Maybe [Point])]
-  captureCavityFaces faces = do
-    count <- growableLength faces
-    traverse
-      (\index -> do
-         face <- unpackIndex <$> readGrowable faces index
-         signature <- mutableFaceSignature face
-         pure (face, signature)
-      )
-      [0 .. count - 1]
-
-  reconcileCavityFaces :: Workspace s -> [(Int, Maybe [Point])] -> ST s ()
-  reconcileCavityFaces workspace signatures =
-    traverse_
-      (\(face, before) -> do
-         after <- mutableFaceSignature face
-         when (after /= before) (retirePermittedFace workspace face)
-      )
-      signatures
-
-  mutableFaceSignature :: Int -> ST s (Maybe [Point])
-  mutableFaceSignature face = do
-    faces <- faceCount mutable
-    if face <= 0 || face >= faces
-      then pure Nothing
-      else do
-        (e0, e1, e2) <- faceEdges mutable face
-        points <- traverse (edgeOriginPoint mutable) [e0, e1, e2]
-        pure (Just (List.sort points))
-
-  captureIncidentFaceSignatures :: [Int] -> ST s [(Int, Maybe [Point])]
-  captureIncidentFaceSignatures vertices = do
-    faces <- newSTRef IntSet.empty
-    traverse_
-      (\vertex -> forEachStarFace vertex (modifySTRef' faces . IntSet.insert))
-      vertices
-    captured <- IntSet.toAscList <$> readSTRef faces
-    traverse
-      (\face -> do
-         signature <- mutableFaceSignature face
-         pure (face, signature)
-      )
-      captured
-
-  -- The flood never mutates, so a popped edge can be retested idempotently:
-  -- an edge whose far face conflicts crosses, marks the face, and pushes that
-  -- face's other two edges; each directed edge has at most its two
-  -- face-neighbours as pushers, and a revisited crossing is deduplicated by
-  -- the face and pair stamps. Boundary visibility for the fan is accumulated
-  -- in place: every boundary edge is asked here, where its endpoints are
-  -- already loaded, whether the point sees it from the cavity side.
-  exploreCavity :: Workspace s -> Point -> MutableLocation -> ST s (Bool, [Int])
-  exploreCavity workspace@Workspace{wsVisited, wsCavityFaceMarks, wsCavityPairMarks, wsCavityFaces, wsCavityInternal, wsCavityBoundary, wsCavityCocircular, wsCavityWork} point site = do
-    next <- nextCavityEpoch workspace
-    clearGrowable wsCavityFaces
-    clearGrowable wsCavityInternal
-    clearGrowable wsCavityBoundary
-    clearGrowable wsCavityCocircular
-    clearGrowable wsCavityWork
-    seedVisible <- case site of
-      MutableInFace under -> do
-        markFace next under
-        (e0, e1, e2) <- faceEdges mutable under
-        pushCavityWork e0
-        pushCavityWork e1
-        pushCavityWork e2
-        pure True
-      MutableOnEdge edge -> do
-        seedAcross next edge
-        seedAcross next (reverseIndex edge)
-        -- The located edge is never encroachment-tested: a point on it lies
-        -- trivially inside its own diametral circle, and the question would
-        -- force the same split forever. A fixed edge is boundary; any other
-        -- is the cavity's first internal pair. Its visibility still counts.
-        fixed <- isFixedEdge edge
-        if fixed
-          then do
-            pushGrowable wsCavityBoundary (packIndex edge)
-            from <- edgeOriginPoint mutable edge
-            to <- edgeOriginPoint mutable (reverseIndex edge)
-            pure (orient2d from to point == GT)
-          else do
-            recordInternal next (edge `quot` 2)
-            pure True
-      _ -> pure True
-    drain next [] seedVisible
-   where
-    markFace :: Word32 -> Int -> ST s ()
-    markFace epoch face = do
-      writeEpochMark wsCavityFaceMarks face epoch
-      traverse_ (\visited -> writeBoolMark visited face True) wsVisited
-      pushGrowable wsCavityFaces (packIndex face)
-
-    isMarked :: Word32 -> Int -> ST s Bool
-    isMarked epoch face = (== epoch) <$> readEpochMark wsCavityFaceMarks face
-
-    recordInternal :: Word32 -> Int -> ST s ()
-    recordInternal epoch pair = do
-      seen <- readEpochMark wsCavityPairMarks pair
-      when (seen /= epoch) $ do
-        writeEpochMark wsCavityPairMarks pair epoch
-        pushGrowable wsCavityInternal (packIndex pair)
-
-    -- The flood's stack traffic is transaction-local and owns no diagnostics;
-    -- the charged wrapper would report every push as legalization depth.
-    pushCavityWork :: Int -> ST s ()
-    pushCavityWork value = pushGrowable wsCavityWork (packIndex value)
-
-    seedAcross :: Word32 -> Int -> ST s ()
-    seedAcross epoch directed = do
-      adjacent <- readFace mutable directed
-      when (adjacent /= 0) $ do
-        markFace epoch adjacent
-        next <- readNext mutable directed
-        previous <- readPrevious mutable directed
-        pushCavityWork next
-        pushCavityWork previous
-
-    -- Visibility is read by the caller only when nothing was encroached, so
-    -- the first encroached edge retires the question: every later boundary
-    -- edge skips the orientation that would only be conjoined into a value
-    -- about to be discarded.
-    drain :: Word32 -> [Int] -> Bool -> ST s (Bool, [Int])
-    drain epoch = go
-     where
-      go !acc !visible = do
-        packed <- popGrowableOr noIndex wsCavityWork
-        if packed == noIndex
-          then pure (visible, acc)
-          else do
-            let !directed = fromIntegral packed
-            fixed <- isFixedEdge directed
-            if fixed
-              then do
-                pushGrowable wsCavityBoundary (packIndex directed)
-                from <- edgeOriginPoint mutable directed
-                to <- edgeOriginPoint mutable (reverseIndex directed)
-                let pair = directed `quot` 2
-                hull <- case domain of
-                  Nothing -> pure False
-                  Just _ -> isHullPair pair
-                let encroached =
-                      if isInterfacePair pair || hull
-                        then strictlyInDiametralCircle from to point
-                        else inDiametralCircle from to point
-                when (isInterfacePair pair) $
-                  modifySTRef' (wsInterfaceBoundaryReads workspace) (+ 1)
-                if encroached
-                  then go (pair : acc) False
-                  else go acc (visible && orient2d from to point == GT)
-              else do
-                across <- readFace mutable (reverseIndex directed)
-                marked <- isMarked epoch across
-                if marked
-                  then do
-                    recordInternal epoch (directed `quot` 2)
-                    go acc visible
-                  else do
-                    let !twin = reverseIndex directed
-                    acrossPrevious <- readPrevious mutable twin
-                    opposite <- readOrigin mutable acrossPrevious
-                    from <- edgeOriginPoint mutable directed
-                    to <- pointAt mutable =<< readOrigin mutable twin
-                    acrossPoint <- pointAt mutable opposite
-                    let !verdict = inCircle to from acrossPoint point
-                    if verdict == GT
-                      then do
-                        markFace epoch across
-                        recordInternal epoch (directed `quot` 2)
-                        acrossNext <- readNext mutable twin
-                        pushCavityWork acrossNext
-                        pushCavityWork acrossPrevious
-                        go acc visible
-                      else do
-                        pushGrowable wsCavityBoundary (packIndex directed)
-                        when (verdict == EQ) $
-                          pushGrowable wsCavityCocircular (packIndex directed)
-                        go acc (visible && orient2d from to point == GT)
-
-  strictlyInDiametralCircle :: Point -> Point -> Point -> Bool
-  strictlyInDiametralCircle (Point ax ay) (Point bx by) (Point px py) =
-    exactDiametralDot ax ay bx by px py < 0
-
-  -- The flood never mutates, so a popped edge can be retested idempotently:
-  -- an edge whose far face conflicts pushes that face's other two edges, and
-  -- each directed edge has at most its two face-neighbours as pushers.
-  drainSimulation :: GrowableWord32 s -> Point -> [Int] -> ST s [Int]
-  drainSimulation work point = go
-   where
-    go :: [Int] -> ST s [Int]
-    go !acc = do
-      packed <- popGrowableOr noIndex work
-      if packed == noIndex
-        then pure acc
-        else do
-          let !directed = fromIntegral packed
-          fixed <- isFixedEdge directed
-          if fixed
-            then do
-              from <- edgeOriginPoint mutable directed
-              to <- edgeOriginPoint mutable (reverseIndex directed)
-              if inDiametralCircle from to point
-                then go (directed `quot` 2 : acc)
-                else go acc
-            else do
-              let !twin = reverseIndex directed
-              acrossPrevious <- readPrevious mutable twin
-              opposite <- readOrigin mutable acrossPrevious
-              from <- edgeOriginPoint mutable directed
-              to <- pointAt mutable =<< readOrigin mutable twin
-              across <- pointAt mutable opposite
-              if inCircle to from across point == GT
-                then do
-                  acrossNext <- readNext mutable twin
-                  pushChargedWork work acrossNext
-                  pushChargedWork work acrossPrevious
-                  go acc
-                else go acc
-
-  -- A cavity the fan commit cannot take: a degenerate or barrier-bent
-  -- boundary, a pinched boundary walk, or a duplicate site. The located site
-  -- commits through the ordinary split-and-legalize path instead; the
-  -- recorded cavity is simply abandoned.
-  commitFlipCandidate :: Workspace s -> Point -> MutableLocation -> ST s (Either BuildError Int)
-  commitFlipCandidate workspace@Workspace{wsQueue, wsExcluded, wsCavityFaces} point site = do
-    let !canonical = canonicalPoint point
-    cavitySignatures <- captureCavityFaces wsCavityFaces
-    vertex <- appendVertex mutable canonical (makeVertex canonical)
-    splitSides <- case site of
-      MutableOnEdge edge
-        | refineExcludeOuterFaces parameters -> do
-            own <- readFace mutable edge
-            let !inner = if own == 0 then reverseIndex edge else edge
-            leftFace <- readFace mutable inner
-            rightFace <- readFace mutable (reverseIndex inner)
-            leftExcluded <- readBoolMark wsExcluded leftFace
-            rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)
-            faceBase <- faceCount mutable
-            pure (Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase))
-      _ -> pure Nothing
-    inserted <- insertExistingVertexAtLocation @'ProbeOff mutable operation vertex site
-    case inserted of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        addCounter operation CounterSteinerPoints 1
-        reconcileCavityFaces workspace cavitySignatures
-        admitStar workspace vertex
-        case splitSides of
-          Just (leftFace, leftExcluded, rightFace, rightExcluded, faceBase) ->
-            inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase
-          Nothing -> pure ()
-        forEachStarFace vertex $ \starFace -> do
-          skip <- readBoolMark wsExcluded starFace
-          unless skip $ pushFace wsQueue starFace
-        pure (Right 1)
-
-  -- Commit a clean cavity by fanning its boundary to the new vertex. The
-  -- recorded cavity faces and internal pairs are recycled into fan faces and
-  -- spokes; exactly two faces and three edge pairs are appended, whatever the
-  -- cavity's size. Any boundary that is not a simple, strictly-visible cycle
-  -- falls back to the split-and-legalize commit: that is the degenerate and
-  -- the barrier-bent case, never an error.
-  commitCavity :: Workspace s -> Point -> MutableLocation -> Bool -> ST s (Either BuildError Int)
-  commitCavity workspace@Workspace{wsCavityFaces, wsCavityInternal, wsCavityBoundary} point site visible = do
-    boundaryCount <- growableLength wsCavityBoundary
-    cavityFaceCount <- growableLength wsCavityFaces
-    internalCount <- growableLength wsCavityInternal
-    chained <-
-      if visible && boundaryCount == cavityFaceCount + 2 && boundaryCount == internalCount + 3
-        then chainBoundary workspace boundaryCount
-        else pure False
-    if chained
-      then fanCommit workspace point boundaryCount cavityFaceCount internalCount
-      else commitFlipCandidate workspace point site
-
-  -- Order the recorded boundary edges into the cycle they form, starting each
-  -- successor by rotating around the current edge's destination. The records
-  -- agree with a simple cycle only when the walk closes after exactly the
-  -- recorded count and meets no boundary vertex twice; anything else is the
-  -- pinched case the fan cannot take.
-  chainBoundary :: Workspace s -> Int -> ST s Bool
-  chainBoundary Workspace{wsCavityEpoch, wsCavityFaceMarks, wsCavityVertexMarks, wsCavityBoundary, wsCavityChain} count = do
-    epoch <- readSTRef wsCavityEpoch
-    walkBudget <- directedEdgeCount mutable
-    start <- unpackIndex <$> readGrowable wsCavityBoundary 0
-    clearGrowable wsCavityChain
-    pushGrowable wsCavityChain (packIndex start)
-    walk epoch (walkBudget + 2) start start (count - 1)
-   where
-    walk :: Word32 -> Int -> Int -> Int -> Int -> ST s Bool
-    walk epoch walkBudget start current remaining = do
-      destination <- readOrigin mutable (reverseIndex current)
-      seen <- readEpochMark wsCavityVertexMarks destination
-      if seen == epoch
-        then pure False
-        else do
-          writeEpochMark wsCavityVertexMarks destination epoch
-          successor <- nextBoundaryEdge epoch walkBudget current
-          if remaining == 0
-            then pure (successor == start)
-            else
-              if successor == start
-                then pure False
-                else do
-                  pushGrowable wsCavityChain (packIndex successor)
-                  walk epoch walkBudget start successor (remaining - 1)
-
-    nextBoundaryEdge :: Word32 -> Int -> Int -> ST s Int
-    nextBoundaryEdge epoch walkBudget edge = do
-      first <- readNext mutable edge
-      rotate first walkBudget
-     where
-      rotate :: Int -> Int -> ST s Int
-      rotate candidate !remaining
-        | remaining <= 0 = pure candidate
-        | otherwise = do
-            adjacent <- readFace mutable candidate
-            markedHere <- (== epoch) <$> readEpochMark wsCavityFaceMarks adjacent
-            across <- readFace mutable (reverseIndex candidate)
-            markedAcross <- (== epoch) <$> readEpochMark wsCavityFaceMarks across
-            if markedHere && not markedAcross
-              then pure candidate
-              else do
-                following <- readNext mutable (reverseIndex candidate)
-                rotate following (remaining - 1)
-
-  fanCommit :: Workspace s -> Point -> Int -> Int -> Int -> ST s (Either BuildError Int)
-  fanCommit workspace@Workspace{wsQueue, wsExcluded, wsCavityFaces, wsCavityInternal, wsCavityCocircular, wsCavityChain} point boundaryCount cavityFaceCount internalCount = do
-    capacity <- ensureCellCapacity mutable (boundaryCount - internalCount) (boundaryCount - cavityFaceCount)
-    case capacity of
-      Left obstruction -> pure (Left obstruction)
-      Right () -> fanCommitWithCapacity
-   where
-    fanCommitWithCapacity :: ST s (Either BuildError Int)
-    fanCommitWithCapacity = do
-      let !canonical = canonicalPoint point
-      retirePermittedFaces workspace wsCavityFaces
-      vertex <- appendVertex mutable canonical (makeVertex canonical)
-      faceBase <- addFaceBlock mutable (boundaryCount - cavityFaceCount)
-      edgeBase <- addEdgeBlock mutable (boundaryCount - internalCount)
-      firstPair <- fanPair edgeBase 0
-      let spoke :: Int -> Int -> ST s ()
-          spoke !index !pair
-            | index >= boundaryCount = pure ()
-            | otherwise = do
-                boundaryEdge <- unpackIndex <$> readGrowable wsCavityChain index
-                face <- fanFace faceBase index
-                nextPair <-
-                  if index + 1 >= boundaryCount
-                    then pure firstPair
-                    else fanPair edgeBase (index + 1)
-                origin <- readOrigin mutable boundaryEdge
-                writeOrigin mutable (2 * pair) origin
-                writeOrigin mutable (2 * pair + 1) vertex
-                setCycle3 mutable face boundaryEdge (2 * nextPair) (2 * pair + 1)
-                writeVertexOut mutable origin boundaryEdge
-                -- 'fanFace' and 'fanPair' hand back the cavity's own faces and
-                -- interior edges before they hand out new ones. A recycled slot
-                -- is given a spoke to the Steiner vertex and a triangle that did
-                -- not exist, so whatever the cavity element it displaced was
-                -- labelled with does not survive.
-                resetEdgeData mutable pair
-                resetFaceData mutable face
-                spoke (index + 1) nextPair
-      spoke 0 firstPair
-      markConnected mutable vertex (2 * firstPair + 1)
-      addCounter operation CounterSteinerPoints 1
-      admitStar workspace vertex
-      -- Exactly-cocircular boundary quads are legal ties for the flip
-      -- commit but not for a static boundary. Only those edges are drained:
-      -- the flood already decided every other boundary edge's quad, so a
-      -- full shouldFlip pass would ask again what it already answered.
-      cocircular <- growableLength wsCavityCocircular
-      when (cocircular > 0) $ do
-        ties <- traverse (\index -> unpackIndex <$> readGrowable wsCavityCocircular index) [0 .. cocircular - 1]
-        legalizeEdges mutable operation ties
-      admitStar workspace vertex
-      let offer :: Int -> ST s ()
-          offer !index
-            | index >= boundaryCount = pure ()
-            | otherwise = do
-                face <- fanFace faceBase index
-                skip <- readBoolMark wsExcluded face
-                unless skip $ pushFace wsQueue face
-                offer (index + 1)
-      offer 0
-      pure (Right 1)
-
-    fanFace :: Int -> Int -> ST s Int
-    fanFace faceBase index
-      | index < cavityFaceCount = unpackIndex <$> readGrowable wsCavityFaces index
-      | otherwise = pure (faceBase + (index - cavityFaceCount))
-
-    fanPair :: Int -> Int -> ST s Int
-    fanPair edgeBase index
-      | index < internalCount = unpackIndex <$> readGrowable wsCavityInternal index
-      | otherwise = pure ((edgeBase + 2 * (index - internalCount)) `quot` 2)
-
-  -- A candidate outside the hull (only reachable when the hull is not
-  -- preserved) preflights against the whole visible hull chain: the
-  -- prospective cavity reaches the hull, so the chain edges are exactly the
-  -- fixed edges the insertion would meet.
-  attemptOutside :: Workspace s -> Int -> Point -> Int -> ST s (Either BuildError Int)
-  attemptOutside workspace@Workspace{wsQueue, wsExcluded, wsForcedSplits, wsCavityWork} face point edge = do
-    chain <- visibleChain edge point
-    clearGrowable wsCavityWork
-    forM_ chain $ \hull -> do
-      pushChargedWork wsCavityWork hull
-      let !inner = reverseIndex hull
-      innerFace <- readFace mutable inner
-      when (innerFace /= 0) $ do
-        next <- readNext mutable inner
-        previous <- readPrevious mutable inner
-        pushChargedWork wsCavityWork next
-        pushChargedWork wsCavityWork previous
-    encroached <- drainSimulation wsCavityWork point []
-    if not (null encroached)
-      then do
-        pushable <- filterM splittablePair encroached
-        forM_ pushable $ \pair -> pushGrowable wsForcedSplits (packIndex pair)
-        unless (null pushable) $ pushFace wsQueue face
-        pure (Right 0)
-      else do
-        let !canonical = canonicalPoint point
-        vertex <- appendVertex mutable canonical (makeVertex canonical)
-        inserted <- insertExistingVertexAtLocation @'ProbeOff mutable operation vertex (MutableOutsideHull edge)
-        case inserted of
-          Left failure -> pure (Left failure)
-          Right () -> do
-            addCounter operation CounterSteinerPoints 1
-            admitStar workspace vertex
-            when (refineExcludeOuterFaces parameters) $ do
-              seedCount <- newSTRef 0
-              forEachStarFace vertex $ \starFace -> do
-                writeBoolMark wsExcluded starFace True
-                count <- readSTRef seedCount
-                writeScratch operation count starFace
-                writeSTRef seedCount (count + 1)
-              count <- readSTRef seedCount
-              propagateExcluded wsExcluded count
-            forEachStarFace vertex $ \starFace -> do
-              skip <- readBoolMark wsExcluded starFace
-              unless skip $ pushFace wsQueue starFace
-            forEachStarFace vertex $ \starFace -> do
-              (e0, e1, e2) <- faceEdges mutable starFace
-              traverse_ (offerFixedEdge workspace) [e0, e1, e2]
-            pure (Right 1)
-
-  -- The maximal run of outer-cycle edges visible from the candidate, centred
-  -- on the edge location reported. Bounded by the hull length.
-  visibleChain :: Int -> Point -> ST s [Int]
-  visibleChain edge point = do
-    halfEdges <- directedEdgeCount mutable
-    left <- expand (readPrevious mutable) (halfEdges + 1) edge
-    right <- expand (readNext mutable) (halfEdges + 1) edge
-    walk left right (halfEdges + 1) []
-   where
-    expand :: (Int -> ST s Int) -> Int -> Int -> ST s Int
-    expand step !walkBudget !current
-      | walkBudget <= 0 = pure current
-      | otherwise = do
-          candidate <- step current
-          if candidate == edge
-            then pure current
-            else do
-              from <- edgeOriginPoint mutable candidate
-              to <- edgeOriginPoint mutable (reverseIndex candidate)
-              if orient2d from to point == GT
-                then expand step (walkBudget - 1) candidate
-                else pure current
-
-    walk :: Int -> Int -> Int -> [Int] -> ST s [Int]
-    walk !current !end !walkBudget !acc
-      | walkBudget <= 0 = pure acc
-      | current == end = pure (current : acc)
-      | otherwise = do
-          following <- readNext mutable current
-          walk following end (walkBudget - 1) (current : acc)
-
-  -- New faces outside the hull sit at barrier depth zero; whatever they now
-  -- reach without crossing a constraint joined the outer region with them.
-  propagateExcluded :: BoolMarks s -> Int -> ST s ()
-  propagateExcluded excluded = drain
-   where
-    drain :: Int -> ST s ()
-    drain !count
-      | count <= 0 = pure ()
-      | otherwise = do
-          face <- readScratch operation (count - 1)
-          (e0, e1, e2) <- faceEdges mutable face
-          next <- spread (count - 1) e0 >>= (`spread` e1) >>= (`spread` e2)
-          drain next
-
-    spread :: Int -> Int -> ST s Int
-    spread !count !directed = do
-      protected <- readConstraint mutable directed
-      if protected
-        then pure count
-        else do
-          adjacent <- readFace mutable (reverseIndex directed)
-          if adjacent == 0
-            then pure count
-            else do
-              already <- readBoolMark excluded adjacent
-              if already
-                then pure count
-                else do
-                  writeBoolMark excluded adjacent True
-                  writeScratch operation count adjacent
-                  pure (count + 1)
-
-  -- A queued fixed edge against the vertices currently opposite it, from each
-  -- non-excluded side. A stale candidate simply answers for the subsegment
-  -- its handle now names, which is the edge the queue cares about.
-  checkEncroachment :: Workspace s -> Int -> ST s (Either BuildError Bool)
-  checkEncroachment workspace@Workspace{wsExcluded} pair = do
-    let !directed = 2 * pair
-    first <- sideEncroaches wsExcluded directed
-    encroached <-
-      if first
-        then pure True
-        else sideEncroaches wsExcluded (reverseIndex directed)
-    if encroached
-      then
-        if isInterfacePair pair
-          then Left <$> interfaceCrossing workspace pair
-          else resolveSplit SplitFixedSegment workspace pair
-      else pure (Right False)
-   where
-    sideEncroaches :: BoolMarks s -> Int -> ST s Bool
-    sideEncroaches excluded side = do
-      adjacent <- readFace mutable side
-      if adjacent == 0
-        then pure False
-        else do
-          skip <- readBoolMark excluded adjacent
-          if skip
-            then pure False
-            else do
-              opposite <- readOrigin mutable =<< readPrevious mutable side
-              query <- pointAt mutable opposite
-              from <- edgeOriginPoint mutable side
-              to <- edgeOriginPoint mutable (reverseIndex side)
-              hull <- case domain of
-                Nothing -> pure False
-                Just _ -> isHullPair pair
-              pure
-                ( if isInterfacePair pair || hull
-                    then strictlyInDiametralCircle from to query
-                    else inDiametralCircle from to query
-                )
-
-  -- Split a fixed edge. A first split lands at the midpoint; a subsegment
-  -- split rounds its offset to the nearest power of two toward the original
-  -- endpoint, so segments meeting at a small input angle stop encroaching
-  -- each other instead of subdividing forever.
-  resolveSplit :: EdgeSplitLaw -> Workspace s -> Int -> ST s (Either BuildError Bool)
-  resolveSplit splitLaw workspace@Workspace{wsExcluded, wsSegmentOrigin} pair = do
-    let !directed = 2 * pair
-    protected <- readConstraint mutable directed
-    hull <- case domain of
-      Nothing -> pure False
-      Just _ -> isHullPair pair
-    if hull
-      then pure (Right False)
-      else if isInterfacePair pair
-      then Left <$> interfaceCrossing workspace pair
-      else if refineKeepConstraintEdges parameters && protected
-      then pure (Right False)
-      else do
-        v0 <- readOrigin mutable directed
-        v1 <- readOrigin mutable (reverseIndex directed)
-        from <- pointAt mutable v0
-        to <- pointAt mutable v1
-        segmentLineage <-
-          case splitLaw of
-            SplitQualityEdge -> pure Nothing
-            SplitFixedSegment -> do
-              (lineage0, second0) <- readSegmentOrigin wsSegmentOrigin v0
-              (lineage1, second1) <- readSegmentOrigin wsSegmentOrigin v1
-              pure (Just (lineage0, second0, lineage1, second1))
-        let (!onSegment0, !onSegment1) =
-              case segmentLineage of
-                Nothing -> (False, False)
-                Just (lineage0, _, lineage1, _) ->
-                  (lineage0 /= noIndex, lineage1 /= noIndex)
-            !splitPoint = splitPosition onSegment0 onSegment1 from to
-        incidentSignatures <- captureIncidentFaceSignatures [v0, v1]
-        valid <- validateSplitPosition directed splitPoint
-        if not valid
-          then pure (Right False)
-          else do
-            -- Inheritance is read as structural face identities, never as
-            -- post-split handle membership: the split's own legalization can
-            -- flip seeded edges and relocate handles before any read, but it
-            -- cannot rename a face. The kept side faces and the append base
-            -- name the sides exactly.
-            own <- readFace mutable directed
-            let !inner = if own == 0 then reverseIndex directed else directed
-            leftFace <- readFace mutable inner
-            rightFace <- readFace mutable (reverseIndex inner)
-            leftExcluded <- readBoolMark wsExcluded leftFace
-            rightExcluded <- sideExcluded wsExcluded (reverseIndex inner)
-            faceBase <- faceCount mutable
-            let !canonical = canonicalPoint splitPoint
-            retirePermittedFace workspace leftFace
-            retirePermittedFace workspace rightFace
-            vertex <- appendVertex mutable canonical (makeVertex canonical)
-            inserted <- insertOnEdge @'ProbeOff mutable operation directed vertex
-            case inserted of
-              Left obstruction -> pure (Left obstruction)
-              Right () -> do
-                reconcileCavityFaces workspace incidentSignatures
-                admitStar workspace vertex
-                case segmentLineage of
-                  Nothing -> pure ()
-                  Just (lineage0, second0, lineage1, second1) ->
-                    if onSegment0
-                      then writeSegmentOrigin wsSegmentOrigin vertex lineage0 second0
-                      else
-                        if onSegment1
-                          then writeSegmentOrigin wsSegmentOrigin vertex lineage1 second1
-                          else writeSegmentOrigin wsSegmentOrigin vertex (packIndex v0) (packIndex v1)
-                when (refineExcludeOuterFaces parameters) $
-                  inheritSplitExclusion wsExcluded leftFace leftExcluded rightFace rightExcluded faceBase
-                addCounter operation CounterSteinerPoints 1
-                pushStarAfterSplit workspace vertex
-                pure (Right True)
-
-  splitPosition :: Bool -> Bool -> Point -> Point -> Point
-  splitPosition onSegment0 onSegment1 from to
-    | not onSegment0 && not onSegment1 = midpoint from to
-    | otherwise =
-        let !halfLength = sqrt (squaredDistance from to) * 0.5
-         in if not (isFinite halfLength) || halfLength <= 0
-              then midpoint from to
-              else
-                let !nearest = 2 ** fromIntegral (round (logBase 2 halfLength) :: Int)
-                    !otherWeight = 0.5 * nearest / halfLength
-                    !originalWeight = 1 - otherWeight
-                    (!weight0, !weight1) =
-                      if onSegment0
-                        then (otherWeight, originalWeight)
-                        else (originalWeight, otherWeight)
-                    Point fromX fromY = from
-                    Point toX toY = to
-                 in Point (fromX * weight0 + toX * weight1) (fromY * weight0 + toY * weight1)
-
-  -- The split is refused when the constructed vertex would leave one of the
-  -- four new faces degenerate or clockwise — for an in-segment point that is
-  -- exactly the coincidence-with-an-endpoint-or-opposite case.
-  validateSplitPosition :: Int -> Point -> ST s Bool
-  validateSplitPosition directed splitPoint@(Point splitX splitY)
-    | validateCoordinate splitX /= Nothing = pure False
-    | validateCoordinate splitY /= Nothing = pure False
-    | otherwise = do
-        from <- edgeOriginPoint mutable directed
-        to <- edgeOriginPoint mutable (reverseIndex directed)
-        first <- sideKeepsOrientation directed from to
-        if not first
-          then pure False
-          else sideKeepsOrientation (reverseIndex directed) to from
-   where
-    sideKeepsOrientation :: Int -> Point -> Point -> ST s Bool
-    sideKeepsOrientation side sideFrom sideTo = do
-      adjacent <- readFace mutable side
-      if adjacent == 0
-        then pure True
-        else do
-          opposite <- readOrigin mutable =<< readPrevious mutable side
-          oppositePoint <- pointAt mutable opposite
-          pure
-            ( orient2d sideFrom oppositePoint splitPoint == LT
-                && orient2d oppositePoint sideTo splitPoint == LT
-            )
-
-  sideExcluded :: BoolMarks s -> Int -> ST s Bool
-  sideExcluded excluded side = do
-    adjacent <- readFace mutable side
-    if adjacent == 0 then pure True else readBoolMark excluded adjacent
-
-  -- A split keeps each side's classification. The faces a side holds
-  -- afterwards are the side face it kept and the face appended over it:
-  -- interior splits append two, boundary splits one, and both ids are
-  -- immune to the legalization that runs between the geometry and here.
-  inheritSplitExclusion :: BoolMarks s -> Int -> Bool -> Int -> Bool -> Int -> ST s ()
-  inheritSplitExclusion excluded leftFace leftExcluded rightFace rightExcluded faceBase = do
-    writeSide leftExcluded leftFace
-    writeSide leftExcluded faceBase
-    when (rightFace /= 0) $ do
-      writeSide rightExcluded rightFace
-      writeSide rightExcluded (faceBase + 1)
-   where
-    writeSide :: Bool -> Int -> ST s ()
-    writeSide status side = do
-      when (side /= 0) $ writeBoolMark excluded side status
-
-  pushStarAfterSplit :: Workspace s -> Int -> ST s ()
-  pushStarAfterSplit workspace@Workspace{wsQueue} vertex =
-    forEachStarFace vertex $ \starFace -> do
-      pushFace wsQueue starFace
-      (e0, e1, e2) <- faceEdges mutable starFace
-      traverse_ (offerFixedEdge workspace) [e0, e1, e2]
-
-  -- Every interior face incident to a vertex, once. One rotation direction
-  -- covers a full interior star; a hull star additionally needs the other
-  -- direction from the starting edge.
-  forEachStarFace :: Int -> (Int -> ST s ()) -> ST s ()
-  forEachStarFace vertex visit = do
-    start <- readVertexOut mutable vertex
-    when (start >= 0) $ do
-      wrapped <- rotate start start
-      unless wrapped (rotateBack start)
-   where
-    rotate :: Int -> Int -> ST s Bool
-    rotate !start !edge = do
-      adjacent <- readFace mutable edge
-      if adjacent == 0
-        then pure False
-        else do
-          visit adjacent
-          next <- reverseIndex <$> readPrevious mutable edge
-          if next == start then pure True else rotate start next
-
-    rotateBack :: Int -> ST s ()
-    rotateBack edge = do
-      let !twin = reverseIndex edge
-      adjacent <- readFace mutable twin
-      when (adjacent /= 0) $ do
-        visit adjacent
-        next <- readNext mutable twin
-        rotateBack next
-
--- | Whether a measurement is over its bound. A non-positive bound admits
--- nothing, which is what dividing by it used to say by returning an infinity
--- that then compared greater than one.
-exceeds :: Double -> Double -> Bool
-exceeds value bound = bound <= 0 || value > bound
-{-# INLINE exceeds #-}
-
--- | The bound the squared radius-edge ratio is compared against. Squaring is
--- monotone on the non-negative reals, so this asks the same question of the
--- squares that the unsquared bound asked of the lengths.
-squareBound :: Double -> Double
-squareBound bound
-  | bound <= 0 = bound
-  | otherwise = bound * bound
-{-# INLINE squareBound #-}
diff --git a/src-build/Moonlight/Triangulation/Internal/Session.hs b/src-build/Moonlight/Triangulation/Internal/Session.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Session.hs
+++ /dev/null
@@ -1,416 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | One owned editing transaction over a triangulation: thaw once, edit, publish once.
-module Moonlight.Triangulation.Internal.Session
-  ( RemovalOutcome (..)
-  , Session
-  , withSession
-  , withLocalSession
-  , insertVertex
-  , insertVertexAt
-  , insertVertexAtNear
-  , insertVertexAtNearVertex
-  , insertAdmittedVertexAtCombining
-  , removeAt
-  , removeAtNear
-  , removeManyAt
-  , removeManyAtNear
-  , excise
-  , refuse
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad.ST (ST)
-import Data.Bits (xor)
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Insertion
-  ( insertAdmittedPointCombining
-  , insertPointCombining
-  )
-import Moonlight.Triangulation.Internal.Excision (removeMutable)
-import Moonlight.Triangulation.Internal.Location (MutableLocation (..), locateMutable)
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.OperationState (OperationState)
-import Moonlight.Triangulation.Internal.Paged (TransactionShape (..))
-import Moonlight.Triangulation.Math (canonicalPoint)
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Transaction (runTransaction)
-import Moonlight.Triangulation.Internal.Types
-import GHC.Generics (Generic)
-
--- | What a single removal produced: the removed position and payload, plus the
--- slot and position changed by swap compaction when another vertex moved.
-data RemovalOutcome vertex = RemovalOutcome
-  { removalOutcomePoint :: !(Point)
-  , removalOutcomeData :: !vertex
-  , removalOutcomeSwap :: !(Maybe (VertexId, Point))
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An edit sequence against one thawed mesh.
---
--- The two published entry points before this one — an insertion session and a
--- removal session — each handed the caller a function and could not compose,
--- so a caller who wanted to remove a hundred vertices and insert fifty thawed
--- twice and paid the O(n) publication a session exists to delete. They are the
--- same transaction; only the verb differed. This is that transaction, and the
--- verbs are its primitives.
---
--- The public session algebra and the private exact-site single-insertion
--- interpreter cross the same hidden transaction boundary. Sessions provide
--- composition; the private interpreter reaches that boundary only with
--- evidence from the immediately preceding frozen read. A fold of persistent
--- verbs and a session over the same verbs therefore differ only in which
--- intermediate meshes they publish.
---
--- Refusal short-circuits: the first 'BuildError' abandons the rest of the
--- sequence, and 'withSession' freezes nothing. A partly edited arena cannot
--- reach a caller as a published triangulation.
-newtype Session s vertex directed undirected face a = Session
-  { stepSession
-      :: MutableDcel s vertex directed undirected face
-      -> OperationState s
-      -> ST s (Either BuildError a)
-  }
-
-instance Functor (Session s vertex directed undirected face) where
-  fmap f (Session step) = Session $ \mesh operation -> fmap (fmap f) (step mesh operation)
-  {-# INLINE fmap #-}
-
-instance Applicative (Session s vertex directed undirected face) where
-  pure a = Session $ \_ _ -> pure (Right a)
-  {-# INLINE pure #-}
-  Session left <*> Session right = Session $ \mesh operation -> do
-    outcome <- left mesh operation
-    case outcome of
-      Left refusal -> pure (Left refusal)
-      Right f -> fmap (fmap f) (right mesh operation)
-  {-# INLINE (<*>) #-}
-
-instance Monad (Session s vertex directed undirected face) where
-  Session step >>= f = Session $ \mesh operation -> do
-    outcome <- step mesh operation
-    case outcome of
-      Left refusal -> pure (Left refusal)
-      Right a -> stepSession (f a) mesh operation
-  {-# INLINE (>>=) #-}
-
--- | Abandon the transaction. Nothing is published.
-refuse :: BuildError -> Session s vertex directed undirected face a
-refuse failure = Session $ \_ _ -> pure (Left failure)
-{-# INLINE refuse #-}
-
--- | Insert a vertex, answering the handle it was given. A point already
--- present keeps its handle and takes the new payload.
-insertVertex
-  :: HasPosition vertex
-  => vertex
-  -> Session s vertex directed undirected face VertexId
-insertVertex payload = Session $ \mesh operation ->
-  fmap
-    (fmap (VertexId . fromIntegral . fst))
-    (insertPointCombining (\_ replacement -> replacement) Nothing mesh operation (position payload) payload)
-
--- | Insert at a stated point, answering the handle and whether a site was
--- created. 'insertVertex' is this with the point read out of the payload; a
--- caller that computed the point — a constraint split, a Steiner refinement —
--- states it rather than round-tripping through 'HasPosition'.
-insertVertexAt
-  :: Point
-  -> vertex
-  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertVertexAt = insertVertexAtFrom (const (pure Nothing))
-
--- | 'insertVertexAt' with the walk seeded at a face the caller vouches for --
--- typically the face a locate on the just-published value settled on, which
--- is exact on the mesh this transaction thawed. The seed is a hint, not an
--- authority: an out-of-range face degrades to the unhinted descent, and the
--- walk corrects.
-insertVertexAtNear
-  :: FaceId
-  -> Point
-  -> vertex
-  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertVertexAtNear seed =
-  insertVertexAtFrom (const (pure (Just (faceIdIndex seed))))
-
--- | 'insertVertexAt' with the exact walk restricted first to an inner face in
--- the supplied vertex's star. This is the native boundary for hierarchy and
--- nearest-neighbour hints, which answer vertices rather than faces. The seed
--- remains non-authoritative: an invalid handle or a vertex with no incident
--- inner face degrades to the ordinary unhinted descent.
-insertVertexAtNearVertex
-  :: VertexId
-  -> Point
-  -> vertex
-  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertVertexAtNearVertex seed = insertVertexAtFrom (`interiorFaceNearVertex` seed)
-
-insertVertexAtFrom
-  :: (MutableDcel s vertex directed undirected face -> ST s (Maybe Int))
-  -> Point
-  -> vertex
-  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertVertexAtFrom seedFor point payload = Session $ \mesh operation -> do
-  seed <- seedFor mesh
-  fmap
-    (fmap (\(vertex, disposition) -> (VertexId (fromIntegral vertex), disposition)))
-    (insertPointCombining
-       (\_ replacement -> replacement)
-       seed
-       mesh
-       operation
-       point
-       payload
-    )
-{-# INLINE insertVertexAtFrom #-}
-
--- | Insert an admitted site while combining an occupied annotation. This is
--- package-internal descent from a boundary that already owns coordinate
--- admission; only location and annotation gluing remain here.
-insertAdmittedVertexAtCombining
-  :: (vertex -> vertex -> vertex)
-  -> QueryPoint
-  -> vertex
-  -> Session s vertex directed undirected face (VertexId, InsertionDisposition)
-insertAdmittedVertexAtCombining combine queryPoint payload = Session $ \mesh operation ->
-  fmap
-    (fmap (\(vertex, disposition) -> (VertexId (fromIntegral vertex), disposition)))
-    (insertAdmittedPointCombining combine Nothing mesh operation queryPoint payload)
-
--- | Remove the vertex standing at a point, answering 'Nothing' when no vertex
--- stands there.
---
--- Keyed by position rather than by handle because removal swap-compacts the
--- arenas: every outstanding t'VertexId' may relocate, and over a sequence of
--- removals a handle-keyed verb would force the caller to thread every
--- relocation by hand. A position is invariant under compaction. The relocation
--- is still reported, in the 'RemovalOutcome', for callers holding handles.
--- The question is an identity question -- which vertex stands at this exact
--- position. A session that has committed to identity work ('removeManyAt',
--- 'excise') answers it through the identity index in O(1); one that has not
--- answers it with a single walk, because a published index is a lazy rebuild
--- and forcing a whole-mesh build to answer one question is the wrong trade.
-removeAt
-  :: Point
-  -> Session s vertex directed undirected face (Maybe (RemovalOutcome vertex))
-removeAt point = Session $ \mesh operation -> do
-  indexed <- identityIndexActive mesh
-  if indexed
-    then removeIndexed mesh operation point
-    else walkAndRemove mesh operation Nothing point
-
-removeIndexed
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Point
-  -> ST s (Either BuildError (Maybe (RemovalOutcome vertex)))
-removeIndexed mesh operation point = do
-  located <- lookupPointVertex mesh point
-  case located of
-    Just vertex -> fmap (fmap Just) (removeMutableOutcome mesh operation vertex)
-    Nothing -> pure (Right Nothing)
-
--- | Remove the vertex standing at a point, starting from a caller-supplied
--- near vertex -- a locate hint from an external structure such as the
--- Delaunay hierarchy. The guess is a hint, not an authority: a slot renamed
--- by swap-compaction or out of range degrades to a walk hinted by the
--- guess's incident face, and the walk corrects.
-removeAtNear
-  :: VertexId
-  -> Point
-  -> Session s vertex directed undirected face (Maybe (RemovalOutcome vertex))
-removeAtNear guessVertex point = Session $ \mesh operation -> do
-  indexed <- identityIndexActive mesh
-  let !guess = vertexIdIndex guessVertex
-      !query = canonicalPoint point
-  if indexed
-    then removeIndexed mesh operation query
-    else do
-      vertices <- pointCount mesh
-      if guess < 0 || guess >= vertices
-        then walkAndRemove mesh operation Nothing query
-        else do
-          stored <- pointAt mesh guess
-          if stored == query
-            then fmap (fmap Just) (removeMutableOutcome mesh operation guess)
-            else do
-              hint <- interiorFaceAtVertex mesh guess
-              walkAndRemove mesh operation hint query
-
-interiorFaceNearVertex
-  :: MutableDcel s vertex directed undirected face
-  -> VertexId
-  -> ST s (Maybe Int)
-interiorFaceNearVertex mesh vertex@(VertexId rawVertex) = do
-  vertices <- pointCount mesh
-  if toInteger rawVertex >= toInteger vertices
-    then pure Nothing
-    else interiorFaceAtVertex mesh (vertexIdIndex vertex)
-
-interiorFaceAtVertex
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Maybe Int)
-interiorFaceAtVertex mesh vertex = do
-  outgoing <- readVertexOut mesh vertex
-  if outgoing < 0
-    then pure Nothing
-    else do
-      primary <- innerFaceOf outgoing
-      maybe (innerFaceOf (outgoing `xor` 1)) (pure . Just) primary
- where
-  innerFaceOf edge = do
-    face <- readFace mesh edge
-    pure (if face > 0 then Just face else Nothing)
-
-walkAndRemove
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Maybe Int
-  -> Point
-  -> ST s (Either BuildError (Maybe (RemovalOutcome vertex)))
-walkAndRemove mesh operation hint query = do
-  located <- locateMutable mesh operation hint query
-  case located of
-    Left obstruction -> pure (Left obstruction)
-    Right (MutableOnVertex vertex) ->
-      fmap (fmap Just) (removeMutableOutcome mesh operation vertex)
-    Right _ -> pure (Right Nothing)
-
--- | Remove the vertex standing at each point, answering per point in order.
--- One session-level decision buys the locate strategy: few removals walk the
--- mesh, each a ~O(sqrt n) descent, while many seed the existing mutable
--- open-addressed identity section once from coordinate authority and answer
--- each question by hash. The guard squares the crossover to avoid a root.
-removeManyAt
-  :: V.Vector (Point)
-  -> Session s vertex directed undirected face (V.Vector (Maybe (RemovalOutcome vertex)))
-removeManyAt points =
-  withBatchIdentityForLoad (V.length points) (V.mapM removeAt points)
-
--- | Install the mutable identity section only around a dense removal program.
--- It is a local section over the coordinate arenas, not a new published cache:
--- on every result path it is discarded and freeze glues a lazy @PointIndex@
--- back from those arenas. Sparse loads retain the existing locate walk.
-withBatchIdentityForLoad
-  :: Int
-  -> Session s vertex directed undirected face result
-  -> Session s vertex directed undirected face result
-withBatchIdentityForLoad count (Session action) = Session $ \mesh operation -> do
-  vertices <- pointCount mesh
-  if count * count > 10 * vertices
-    then do
-      activated <- activateBatchPointIndex mesh
-      case activated of
-        Left failure -> pure (Left failure)
-        Right () -> do
-          outcome <- action mesh operation
-          discardBatchPointIndex mesh
-          pure outcome
-    else action mesh operation
-
-identityCommitted :: Session s vertex directed undirected face Bool
-identityCommitted = Session $ \mesh _ -> fmap Right (identityIndexActive mesh)
-{-# INLINE identityCommitted #-}
-
--- | 'removeManyAt' with a near vertex per point where the caller has one — a
--- hierarchy sample, a previous answer. The batch commits to its load first,
--- so the identity-index crossover decides the regime once: a dense batch buys
--- its local mutable table and never examines a guess, a sparse one walks from
--- its guesses.
-removeManyAtNear
-  :: V.Vector (Maybe VertexId)
-  -> V.Vector (Point)
-  -> Session s vertex directed undirected face (V.Vector (Maybe (RemovalOutcome vertex)))
-removeManyAtNear guesses points =
-  withBatchIdentityForLoad (V.length points) $ do
-    indexed <- identityCommitted
-    if indexed
-      then V.mapM removeAt points
-      else
-        V.zipWithM
-          (\guess point -> maybe (removeAt point) (\vertex -> removeAtNear vertex point) guess)
-          guesses
-          points
-
--- | Remove a stated vertex. Sound only while the handle still denotes what the
--- caller means: the first removal in a sequence compacts the arenas, so a
--- handle taken before it may name a different vertex after. @removeAt@ is the
--- verb for a sequence; this is the verb for a handle the caller has just been
--- given and has not yet let a removal run underneath.
-excise
-  :: VertexId
-  -> Session s vertex directed undirected face (RemovalOutcome vertex)
-excise requested@(VertexId raw) = Session $ \mesh operation -> do
-  vertices <- pointCount mesh
-  if toInteger raw >= toInteger vertices
-    then pure (Left (RemovalVertexOutOfRange requested vertices))
-    else do
-      activatePointIndex mesh
-      removeMutableOutcome mesh operation (vertexIdIndex requested)
-
-removeMutableOutcome
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> ST s (Either BuildError (RemovalOutcome vertex))
-removeMutableOutcome mesh operation vertex =
-  fmap (fmap removalOutcomeOf) (removeMutable mesh operation vertex)
-
-removalOutcomeOf
-  :: (Point, vertex, Maybe (Int, Point))
-  -> RemovalOutcome vertex
-removalOutcomeOf (point, payload, swapped) =
-  RemovalOutcome
-    { removalOutcomePoint = point
-    , removalOutcomeData = payload
-    , removalOutcomeSwap =
-        (\(slot, standing) -> (VertexId (fromIntegral slot), standing)) <$> swapped
-    }
-
--- | Run an edit sequence: thaw once, edit, freeze once, and publish the
--- counters the whole transaction charged.
---
--- The reservation is taken for the peak vertex count, so it is the insertion
--- count that sizes it; removals only shrink the mesh and a mixed sequence
--- cannot exceed the peak an insert-only sequence of the same count reaches.
---
--- The session cannot escape its callback: the state token is universally
--- quantified, so the mesh it addresses is dead by the time the frozen
--- triangulation is returned.
-withSession
-  :: forall mode vertex directed undirected face result
-   . Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. Session s vertex directed undirected face result)
-  -> Either BuildError (result, Triangulation mode vertex directed undirected face, BuildStats)
-withSession triangulation additional session =
-  runTransaction
-    id
-    DenseTransaction
-    triangulation
-    additional
-    (\mesh operation -> stepSession session mesh operation)
-
--- | The local-edit transaction: copy-on-write pages, publication proportional
--- to what the edit dirtied. This is the section for singleton persistent
--- verbs, whose one edit cannot amortize a dense copy of the whole mesh.
-withLocalSession
-  :: forall mode vertex directed undirected face result
-   . Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. Session s vertex directed undirected face result)
-  -> Either BuildError (result, Triangulation mode vertex directed undirected face, BuildStats)
-withLocalSession triangulation additional session =
-  runTransaction
-    id
-    LocalTransaction
-    triangulation
-    additional
-    (\mesh operation -> stepSession session mesh operation)
diff --git a/src-build/Moonlight/Triangulation/Internal/Transaction.hs b/src-build/Moonlight/Triangulation/Internal/Transaction.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Internal/Transaction.hs
+++ /dev/null
@@ -1,158 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE RankNTypes #-}
-
--- | The hidden publication boundary shared by persistent build-side edits.
---
--- The rank-two action can observe one mutable section, but neither the section
--- nor any site witness can escape it.  This is deliberately below the public
--- Session surface: a caller may compose public verbs, while an owner that has
--- just derived private evidence can interpret it without making that evidence
--- forgeable.
-module Moonlight.Triangulation.Internal.Transaction
-  ( runTransaction
-  , runTransactionWithPublication
-  , runUnmeasuredTransaction
-  ) where
-
-import Control.Monad.ST (ST, runST)
-import Moonlight.Triangulation.Dcel (numVertices)
-import Moonlight.Triangulation.Internal.Capacity (ensureCapacity)
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel
-  , freezeTriangulation
-  , freezeTriangulationWithStats
-  , halfEdgeCapacity
-  , thawTriangulation
-  , thawTriangulationDense
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( OperationState
-  , freezeBuildStats
-  , newOperationState
-  )
-import Moonlight.Triangulation.Internal.Paged
-  ( PublicationStats
-  , TransactionShape (..)
-  , emptyPublicationStats
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types (BuildError, BuildStats)
-
--- | Reserve, thaw, interpret, and publish one transaction.
---
--- The physical section is selected by the operation that knows its edit
--- volume.  Refusal short-circuits before freezing, so a partially rewritten
--- mutable mesh cannot escape as a published triangulation.
-runTransaction
-  :: (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face, BuildStats)
-runTransaction = runTransactionWithReceipt freezeBuildStats
-{-# INLINE runTransaction #-}
-
--- | The measured publication boundary.  The storage owner returns its page
--- counters alongside the immutable mesh; callers must not infer locality from
--- a final page count or from a benchmark timer.
-runTransactionWithPublication
-  :: (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face, BuildStats, PublicationStats)
-runTransactionWithPublication = runTransactionWithPublicationReceipt freezeBuildStats
-{-# INLINE runTransactionWithPublication #-}
-
--- | Publish a transaction whose caller observes no instrumentation. Avoiding
--- the statistics fold matters for singleton constraint verbs: their public
--- result has no statistics field, so reading every counter would be dead work.
-runUnmeasuredTransaction
-  :: (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face)
-runUnmeasuredTransaction mapBuildFailure shape triangulation additional action =
-  fmap
-    (\(result, frozen, ()) -> (result, frozen))
-    ( runTransactionWithReceipt
-        (const (pure ()))
-        mapBuildFailure
-        shape
-        triangulation
-        additional
-        action
-    )
-{-# INLINE runUnmeasuredTransaction #-}
-
-runTransactionWithReceipt
-  :: (forall s. OperationState s -> ST s receipt)
-  -> (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt)
-runTransactionWithReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do
-  fmap
-    (\(value, frozen, receipt, _publicationStats) -> (value, frozen, receipt))
-    (runTransactionCore freezeWithoutPublicationStats freezeReceipt mapBuildFailure shape triangulation additional action)
-{-# INLINE runTransactionWithReceipt #-}
-
-runTransactionWithPublicationReceipt
-  :: (forall s. OperationState s -> ST s receipt)
-  -> (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)
-runTransactionWithPublicationReceipt freezeReceipt mapBuildFailure shape triangulation additional action = do
-  runTransactionCore freezeTriangulationWithStats freezeReceipt mapBuildFailure shape triangulation additional action
-{-# INLINE runTransactionWithPublicationReceipt #-}
-
-runTransactionCore
-  :: (forall s. MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats)))
-  -> (forall s. OperationState s -> ST s receipt)
-  -> (BuildError -> failure)
-  -> TransactionShape
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> (forall s. MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either failure result))
-  -> Either failure (result, Triangulation mode vertex directed undirected face, receipt, PublicationStats)
-runTransactionCore freezePublication freezeReceipt mapBuildFailure shape triangulation additional action = do
-  let !currentCapacity = numVertices triangulation
-      !requestedAdditional = max 0 additional
-      !capacity
-        | requestedAdditional > maxBound - currentCapacity = maxBound
-        | otherwise = currentCapacity + requestedAdditional
-  case ensureCapacity capacity of
-    Left failure -> Left (mapBuildFailure failure)
-    Right () -> pure ()
-  runST $ do
-    mutable <-
-      case shape of
-        DenseTransaction -> thawTriangulationDense capacity triangulation
-        LocalTransaction -> thawTriangulation capacity triangulation
-    operation <- newOperationState (halfEdgeCapacity mutable)
-    outcome <- action mutable operation
-    case outcome of
-      Left refusal -> pure (Left refusal)
-      Right value -> do
-        frozenOutcome <- freezePublication mutable
-        case frozenOutcome of
-          Left obstruction -> pure (Left (mapBuildFailure obstruction))
-          Right (frozen, publicationStats) -> do
-            receipt <- freezeReceipt operation
-            pure (Right (value, frozen, receipt, publicationStats))
-{-# INLINE runTransactionCore #-}
-
-freezeWithoutPublicationStats
-  :: MutableDcel s vertex directed undirected face
-  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
-freezeWithoutPublicationStats mutable =
-  fmap (fmap (\frozen -> (frozen, emptyPublicationStats))) (freezeTriangulation mutable)
diff --git a/src-build/Moonlight/Triangulation/Refinement.hs b/src-build/Moonlight/Triangulation/Refinement.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Refinement.hs
+++ /dev/null
@@ -1,551 +0,0 @@
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Ruppert refinement: Steiner insertion composed after a built mesh, never a
--- second kind of mesh. Parameters are reached through checked verbs rather than
--- a raw record, so an unrealizable quality bar is a refusal.
-module Moonlight.Triangulation.Refinement
-  ( refine
-  , refineWithinDomain
-  , validateRefinementParameters
-  , withMinimumAngle
-  , radiusEdgeRatioForAngle
-  ) where
-
-import Control.Monad (void)
-import Control.Monad.ST (ST)
-import Data.Foldable (traverse_)
-import qualified Data.IntSet as IntSet
-import Data.List (sort)
-import Data.Maybe (fromMaybe)
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Dcel
-  ( faceDirectedEdges
-  , faceVertices
-  , incidentFace
-  , isConstraintEdge
-  , numFaces
-  , numUndirectedEdges
-  , numVertices
-  , undirectedEndpoints
-  , vertexPoint
-  )
-import Moonlight.Triangulation.FloodFillIterator (facesAtEvenBarrierDepth)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  , asUndirected
-  , directedPair
-  , faceIdIndex
-  , reverseEdge
-  , undirectedEdgeIdIndex
-  )
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.Paged
-  ( PublicationStats
-  , TransactionShape (DenseTransaction, LocalTransaction)
-  )
-import Moonlight.Triangulation.Internal.Refinement
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
-import Moonlight.Triangulation.Internal.Transaction (runTransactionWithPublication)
-import Moonlight.Triangulation.Internal.Validation
-  ( validateTopologyClosureWithStats
-  )
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Validation
-  ( validateTopology
-  )
-
-data RefinementExecution mode vertex directed undirected face = RefinementExecution
-  { refinementExecutionResult :: !(RefinementResult mode vertex directed undirected face)
-  , refinementExecutionVisitedFaces :: ![Int]
-  , refinementExecutionPermittedFaces :: ![Int]
-  , refinementExecutionInterfaceBoundaryReads :: {-# UNPACK #-} !Int
-  , refinementExecutionBoundaryCrossingAttempts :: {-# UNPACK #-} !Int
-  , refinementExecutionPublicationStats :: !PublicationStats
-  }
-
--- | The radius-edge ratio admitting a minimum angle, in degrees.
-radiusEdgeRatioForAngle :: Double -> Either BuildError (Maybe Double)
-radiusEdgeRatioForAngle degrees =
-  case classifyNonFinite degrees of
-    Just nonFinite ->
-      Left (RefinementMinimumAngleNotFinite nonFinite)
-    Nothing
-      | degrees < 0 || degrees > 60 ->
-          Left (RefinementMinimumAngleOutOfRange degrees)
-      | degrees == 0 -> Right Nothing
-      | otherwise ->
-          let ratio = 0.5 / sin (degrees * pi / 180)
-           in case classifyNonFinite ratio of
-                Just nonFinite ->
-                  Left (RefinementMinimumAngleDerivedRatioNotFinite nonFinite)
-                Nothing -> Right (Just ratio)
-
--- | Set the minimum angle, in degrees.
-withMinimumAngle :: Double -> RefinementParameters -> Either BuildError (RefinementParameters)
-withMinimumAngle degrees parameters = do
-  ratio <- radiusEdgeRatioForAngle degrees
-  pure parameters{refineMaxRadiusEdgeRatio = ratio}
-
--- | Refine an unconstrained or constrained triangulation in the same finite
--- DCEL. The constructor supplies application payloads for Steiner vertices;
--- payloads annotate the requested geometric point and cannot reauthor it.
-refine
-  :: (Point -> vertex)
-  -> RefinementParameters
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError (RefinementResult mode vertex directed undirected face)
-refine makeVertex parameters =
-  fmap refinementExecutionResult
-    . refineWithInitialSeed RefineEveryFace Nothing makeVertex parameters
-
--- | Derive the exact immutable interface separating a checked active face
--- section from every protected inner face.  A permitted face set determines
--- this cut uniquely, so callers cannot supply a second claim about Gamma.
-mkRefinementDomain
-  :: Set.Set FaceId
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError RefinementDomain
-mkRefinementDomain permittedFaces triangulation = do
-  permitted <- validateSeedFaces triangulation permittedFaces
-  Right
-    RefinementDomain
-      { refinementDomainPermittedFaces = permitted
-      , refinementDomainInterfacePairs = expectedInterface permitted
-      , refinementDomainInputFaceCount = numFaces triangulation
-      }
- where
-  expectedInterface permitted =
-    IntSet.fromList
-      [ pair
-      | rawFace <- IntSet.toAscList permitted
-      , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))
-      , let pair = undirectedEdgeIdIndex (asUndirected edge)
-      , let adjacent = incidentFace triangulation (reverseEdge edge)
-      , adjacent /= FaceId 0
-      , IntSet.notMember (faceIdIndex adjacent) permitted
-      ]
-
--- | Refine exactly one checked local section. Interface edges are installed as
--- transaction-local legalization barriers and removed before publication;
--- the receipt and collar proof inspect only the admitted local closure.
-refineWithinDomain
-  :: forall mode vertex directed undirected face.
-     (Point -> vertex)
-  -> RefinementParameters
-  -> Set.Set FaceId
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError (RefinementDomainResult mode vertex directed undirected face)
-refineWithinDomain makeVertex parameters permittedFaces triangulation = do
-  validateDomainParameters parameters
-  domain <- mkRefinementDomain permittedFaces triangulation
-  execution <-
-    refineWithInitialSeed
-      (RefineSeededFaces (refinementDomainPermittedFaces domain))
-      (Just domain)
-      makeVertex
-      parameters
-      triangulation
-  let result = refinementExecutionResult execution
-  (validationStats, finalInterfaceIncidence) <- validateDomainClosure
-    domain
-    triangulation
-    (refinedTriangulation result)
-    (refinementExecutionPermittedFaces execution)
-  let receipt =
-        buildRefinementReceipt
-          domain
-          triangulation
-          (refinedTriangulation result)
-          (refinementExecutionVisitedFaces execution)
-          (refinementExecutionPermittedFaces execution)
-          finalInterfaceIncidence
-          (refinementExecutionInterfaceBoundaryReads execution)
-          (refinementExecutionBoundaryCrossingAttempts execution)
-          (refinementExecutionPublicationStats execution)
-          validationStats
-  case V.toList (refinementVisitedProtectedFaces receipt) of
-    protected : _ -> Left (RefinementDomainWouldRewriteProtectedFace protected)
-    [] ->
-       let restoredResult :: RefinementResult mode vertex directed undirected face
-           restoredResult =
-            result
-              { refinedTriangulation =
-                  (refinedTriangulation result)
-                    -- The checked local interpreter rejects every hull-pair
-                    -- split, so the cached outer cycle remains an exact
-                    -- handle witness and can be restored without a scan.
-                    { triSeamFrontier = triSeamFrontier triangulation
-                    }
-              }
-       in Right
-        RefinementDomainResult
-          { refinementDomainResult = restoredResult
-          , refinementDomainReceipt = receipt
-          }
-
-validateDomainParameters :: RefinementParameters -> Either BuildError ()
-validateDomainParameters parameters
-  | not (refinePreserveConvexHull parameters) =
-      Left RefinementDomainRequiresConvexHullPreservation
-  | not (refineKeepConstraintEdges parameters) =
-      Left RefinementDomainRequiresConstraintPreservation
-  | refineExcludeOuterFaces parameters =
-      Left RefinementDomainForbidsOuterFaceExclusion
-  | refineMaxAdditionalVertices parameters == Nothing =
-      Left RefinementDomainRequiresFiniteVertexBudget
-  | otherwise = Right ()
-
-refineWithInitialSeed
-  :: RefinementInitialSeed
-  -> Maybe RefinementDomain
-  -> (Point -> vertex)
-  -> RefinementParameters
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError (RefinementExecution mode vertex directed undirected face)
-refineWithInitialSeed initialSeed domain makeVertex parameters triangulation = do
-  validateRefinementParameters parameters
-  case domain of
-    Just _ -> pure ()
-    Nothing ->
-      case validateTopology triangulation of
-        violation : _ -> Left (RefinementInputTopologyInvalid violation)
-        [] -> pure ()
-  let originalCount = numVertices triangulation
-      budget =
-        case domain of
-          Just _ -> max 0 (fromMaybe 0 (refineMaxAdditionalVertices parameters))
-          Nothing -> max 0 (fromMaybe (10 * max 1 originalCount) (refineMaxAdditionalVertices parameters))
-      initialExcludedFaces =
-        if domain == Nothing && refineExcludeOuterFaces parameters
-          then
-            IntSet.fromList
-              [ faceIdIndex face
-              | face <-
-                  facesAtEvenBarrierDepth
-                    triangulation
-                    (isConstraintEdge triangulation)
-              ]
-          else IntSet.empty
-  ( outcome
-    , frozen
-    , stats
-    , publicationStats
-    ) <-
-    runTransactionWithPublication id (transactionShape domain) triangulation budget $ \mutable operation -> do
-      installInterfaceBarriers mutable domain
-      refinement <-
-        refineMutable
-          makeVertex
-          mutable
-          operation
-          parameters
-          budget
-          initialExcludedFaces
-          initialSeed
-          domain
-      removeInterfaceBarriers mutable triangulation domain
-      pure refinement
-  let (complete, added, excluded, visited, permittedFaces, interfaceBoundaryReads, boundaryCrossingAttempts) = outcome
-  pure
-    RefinementExecution
-      { refinementExecutionResult =
-          RefinementResult
-            { refinedTriangulation = frozen
-            , refinementStats = stats
-            , refinementAddedVertices = added
-            , refinementComplete = complete
-            , refinementExcludedFaces = V.fromList (map (FaceId . fromIntegral) excluded)
-            }
-      , refinementExecutionVisitedFaces = visited
-      , refinementExecutionPermittedFaces = permittedFaces
-      , refinementExecutionInterfaceBoundaryReads = interfaceBoundaryReads
-      , refinementExecutionBoundaryCrossingAttempts = boundaryCrossingAttempts
-      , refinementExecutionPublicationStats = publicationStats
-      }
-
-transactionShape :: Maybe RefinementDomain -> TransactionShape
-transactionShape = maybe DenseTransaction (const LocalTransaction)
-
-installInterfaceBarriers
-  :: MutableDcel s vertex directed undirected face
-  -> Maybe RefinementDomain
-  -> ST s ()
-installInterfaceBarriers mutable =
-  traverse_
-    (\pair -> void (setConstraint mutable (2 * pair)))
-    . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs)
-
-removeInterfaceBarriers
-  :: MutableDcel s vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face
-  -> Maybe RefinementDomain
-  -> ST s ()
-removeInterfaceBarriers mutable input =
-  traverse_
-    (\pair ->
-       let edge = UndirectedEdgeId (fromIntegral pair)
-        in if isConstraintEdge input edge
-             then pure ()
-             else void (clearConstraint mutable (2 * pair))
-    )
-    . maybe [] (IntSet.toAscList . refinementDomainInterfacePairs)
-
-buildRefinementReceipt
-  :: RefinementDomain
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face
-  -> [Int]
-  -> [Int]
-  -> V.Vector (UndirectedEdgeId, FaceId, FaceId)
-  -> Int
-  -> Int
-  -> PublicationStats
-  -> ValidationClosureStats
-  -> RefinementReceipt
-buildRefinementReceipt domain before after visited permittedFaces finalInterfaceIncidence interfaceBoundaryReads boundaryCrossingAttempts publicationStats validationStats =
-  RefinementReceipt
-    { refinementVisitedJoinFaces = V.fromList (fmap toFace visitedJoin)
-    , refinementVisitedProtectedFaces = V.fromList (fmap toFace visitedProtected)
-    , refinementCreatedFaces = V.fromList (fmap toFace createdFaces)
-    , refinementFinalPermittedFaces = V.fromList (fmap toFace permittedFaces)
-    , refinementFinalInterfaceIncidence = finalInterfaceIncidence
-    , refinementTouchedEdges = V.fromList (fmap toEdge touchedEdges)
-    , refinementRemovedEdges = V.fromList (fmap toEdge removedEdges)
-    , refinementInterfaceBoundaryReads = interfaceBoundaryReads
-    , refinementAttemptedBoundaryCrossings = boundaryCrossingAttempts
-    , refinementPublicationStats = publicationStats
-    , refinementValidationClosureStats = validationStats
-    }
- where
-  permitted = refinementDomainPermittedFaces domain
-  (visitedProtected, visitedJoin) =
-    foldr
-      (\face (protected, join) ->
-         if IntSet.member face permitted
-           then (protected, face : join)
-           else if face >= refinementDomainInputFaceCount domain
-             then (protected, face : join)
-           else (face : protected, join))
-      ([], []) visited
-  initialFaces = refinementDomainPermittedFaces domain
-  finalFaces = IntSet.fromList permittedFaces
-  faceClosure = IntSet.toAscList (IntSet.union initialFaces finalFaces)
-  createdFaces =
-    [ face
-    | face <- faceClosure
-    , localFaceSignature before face /= localFaceSignature after face
-    ]
-  initialEdges = localDomainEdges before initialFaces
-  finalEdges = localDomainEdges after finalFaces
-  edgeClosure = IntSet.toAscList (IntSet.union initialEdges finalEdges)
-  touchedEdges =
-    [ edge
-    | edge <- edgeClosure
-    , localEdgeSignature before edge /= localEdgeSignature after edge
-    ]
-  removedEdges =
-    [ edge
-    | edge <- IntSet.toAscList initialEdges
-    , localEdgeSignature before edge /= localEdgeSignature after edge
-    ]
-  toFace = FaceId . fromIntegral
-  toEdge = UndirectedEdgeId . fromIntegral
-
-localFaceSignature
-  :: Triangulation mode vertex directed undirected face
-  -> Int
-  -> Maybe [Point]
-localFaceSignature triangulation raw
-  | raw <= 0 || raw >= numFaces triangulation = Nothing
-  | otherwise =
-      Just
-        ( sort
-            (fmap (vertexPoint triangulation) (faceVertices triangulation (FaceId (fromIntegral raw))))
-        )
-
-localDomainEdges
-  :: Triangulation mode vertex directed undirected face
-  -> IntSet.IntSet
-  -> IntSet.IntSet
-localDomainEdges triangulation faces =
-  IntSet.fromList
-    [ undirectedEdgeIdIndex (asUndirected edge)
-    | rawFace <- IntSet.toAscList faces
-    , edge <- faceDirectedEdges triangulation (FaceId (fromIntegral rawFace))
-    ]
-
-localEdgeSignature
-  :: Triangulation mode vertex directed undirected face
-  -> Int
-  -> Maybe (Point, Point, [FaceId])
-localEdgeSignature triangulation raw
-  | raw < 0 || raw >= numUndirectedEdges triangulation = Nothing
-  | otherwise =
-      let edge = UndirectedEdgeId (fromIntegral raw)
-          (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-          fromPoint = vertexPoint triangulation fromVertex
-          toPoint = vertexPoint triangulation toVertex
-          (forward, backward) = directedPair edge
-       in Just
-            ( min fromPoint toPoint
-            , max fromPoint toPoint
-            , sort
-                [ incidentFace triangulation forward
-                , incidentFace triangulation backward
-                ]
-            )
-
-validateDomainClosure
-  :: RefinementDomain
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face
-  -> [Int]
-  -> Either BuildError (ValidationClosureStats, V.Vector (UndirectedEdgeId, FaceId, FaceId))
-validateDomainClosure domain before after finalPermittedFaces = do
-  let finalPermitted = IntSet.fromList finalPermittedFaces
-      (closureStats, closureViolations) =
-        validateTopologyClosureWithStats
-          ( IntSet.union
-              (refinementDomainPermittedFaces domain)
-              finalPermitted
-          )
-          (refinementDomainInterfacePairs domain)
-          after
-  case closureViolations of
-    violation : _ -> Left (RefinementInputTopologyInvalid violation)
-    [] -> pure ()
-  traverse_ validateProtectedFace (IntSet.toAscList protectedFaces)
-  finalInterfaceIncidence <-
-    V.fromList
-      <$> traverse
-        (validateInterfaceEdge finalPermitted)
-        (IntSet.toAscList (refinementDomainInterfacePairs domain))
-  pure (closureStats, finalInterfaceIncidence)
- where
-  permitted = refinementDomainPermittedFaces domain
-  protectedFaces =
-    IntSet.fromList
-      [ rawFace
-      | rawPair <- IntSet.toAscList (refinementDomainInterfacePairs domain)
-      , let edge = UndirectedEdgeId (fromIntegral rawPair)
-      , let (forward, backward) = directedPair edge
-      , rawFace <-
-          [ faceIdIndex (incidentFace before forward)
-          , faceIdIndex (incidentFace before backward)
-          ]
-      , rawFace > 0
-      , IntSet.notMember rawFace permitted
-      ]
-
-  validateProtectedFace rawFace =
-    if localFaceSignature before rawFace == localFaceSignature after rawFace
-      then Right ()
-      else Left (RefinementDomainProtectedFaceChanged (FaceId (fromIntegral rawFace)))
-
-  validateInterfaceEdge finalPermitted rawPair =
-    let edge = UndirectedEdgeId (fromIntegral rawPair)
-     in case (localEdgeSignature before rawPair, localEdgeSignature after rawPair) of
-      ( Just (beforeFrom, beforeTo, beforeFaces)
-        , Just (afterFrom, afterTo, afterFaces)
-        )
-          | beforeFrom == afterFrom && beforeTo == afterTo ->
-              validateInterfaceFaces finalPermitted edge beforeFaces afterFaces
-      _ -> Left RefinementDomainTopologyChanged
-
-  validateInterfaceFaces finalPermitted edge beforeFaces afterFaces =
-    case filter protected beforeFaces of
-      [protectedFace]
-        | protectedFace `elem` afterFaces ->
-            case filter (/= protectedFace) afterFaces of
-              [oppositeFace]
-                | IntSet.member (faceIdIndex oppositeFace) finalPermitted ->
-                    Right (edge, protectedFace, oppositeFace)
-                | otherwise ->
-                    Left
-                      ( RefinementDomainInterfaceOppositeFaceNotPermitted
-                          edge
-                          oppositeFace
-                      )
-              _ -> Left RefinementDomainTopologyChanged
-      _ -> Left RefinementDomainTopologyChanged
-
-  protected face =
-    let raw = faceIdIndex face
-     in raw > 0 && IntSet.notMember raw permitted
-
--- | Refuse outer or absent faces rather than silently treating an invalid
--- topology witness as an empty repair. Face zero is the outer face and has no
--- active refinement equation.
-validateSeedFaces
-  :: Triangulation mode vertex directed undirected face
-  -> Set.Set FaceId
-  -> Either BuildError IntSet.IntSet
-validateSeedFaces triangulation requested =
-  IntSet.fromList <$> traverse validateFace (Set.toAscList requested)
- where
-  totalFaces = numFaces triangulation
-
-  validateFace :: FaceId -> Either BuildError Int
-  validateFace face
-    | face == FaceId 0 || toInteger (unFaceId face) >= toInteger totalFaces =
-        Left (RefinementSeedFaceNotActive face totalFaces)
-    | otherwise = Right (faceIdIndex face)
-
--- | Refuse parameters no mesh can satisfy.
-validateRefinementParameters :: RefinementParameters -> Either BuildError ()
-validateRefinementParameters RefinementParameters{refineMaxAdditionalVertices, refineMinArea, refineMaxArea, refineMaxRadiusEdgeRatio, refineMaxEdgeLength} = do
-  case refineMaxAdditionalVertices of
-    Just value
-      | value < 0 ->
-          Left (RefinementMaximumAdditionalVerticesNegative value)
-    _ -> Right ()
-  validateOptionalRefinementParameter
-    RefinementMinimumAreaNotFinite
-    RefinementMinimumAreaNegative
-    (>= 0)
-    refineMinArea
-  validateOptionalRefinementParameter
-    RefinementMaximumAreaNotFinite
-    RefinementMaximumAreaNotPositive
-    (> 0)
-    refineMaxArea
-  validateOptionalRefinementParameter
-    RefinementMaximumRadiusEdgeRatioNotFinite
-    RefinementMaximumRadiusEdgeRatioNotPositive
-    (> 0)
-    refineMaxRadiusEdgeRatio
-  validateOptionalRefinementParameter
-    RefinementMaximumEdgeLengthNotFinite
-    RefinementMaximumEdgeLengthNotPositive
-    (> 0)
-    refineMaxEdgeLength
-  case (refineMinArea, refineMaxArea) of
-    (Just minimumArea, Just maximumArea)
-      | minimumArea > maximumArea ->
-          Left
-            ( RefinementMinimumAreaExceedsMaximum
-                minimumArea
-                maximumArea
-            )
-    _ -> Right ()
-
-validateOptionalRefinementParameter
-  :: (NonFiniteValue -> BuildError)
-  -> (Double -> BuildError)
-  -> (Double -> Bool)
-  -> Maybe Double
-  -> Either BuildError ()
-validateOptionalRefinementParameter nonFinite outsideRange predicate value =
-  case value of
-    Nothing -> Right ()
-    Just number ->
-      case classifyNonFinite number of
-        Just nonFiniteValue ->
-          Left (nonFinite nonFiniteValue)
-        Nothing
-          | predicate number -> Right ()
-          | otherwise ->
-              Left (outsideRange number)
diff --git a/src-build/Moonlight/Triangulation/Removal.hs b/src-build/Moonlight/Triangulation/Removal.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Removal.hs
+++ /dev/null
@@ -1,89 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Persistent removal: one excision, published as its own triangulation.
-module Moonlight.Triangulation.Removal
-  ( RemovalOutcome (..)
-  , RemovalResult (..)
-  , removeVertex
-  , locateAndRemove
-  ) where
-
-import Control.DeepSeq (NFData)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Internal.PointIndex (lookupPointIndex)
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
-import Moonlight.Triangulation.Internal.Types (BuildError, BuildStats, Point, queryPointValue)
-import Moonlight.Triangulation.Math (validatePoint)
-import Moonlight.Triangulation.Internal.Session (RemovalOutcome (..), excise, withLocalSession)
-import GHC.Generics (Generic)
-
--- | One removal and its publication: the frozen triangulation, the outcome
--- record, and the operation's counters. The persistent entries publish all
--- three; a session publishes the outcome per call and the counters once.
-data RemovalResult mode vertex directed undirected face = RemovalResult
-  { removalTriangulation :: !(Triangulation mode vertex directed undirected face)
-  , removalOutcome :: !(RemovalOutcome vertex)
-  , removalStats :: !BuildStats
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance
-  (Eq vertex, Eq directed, Eq undirected, Eq face)
-  => Eq (RemovalResult mode vertex directed undirected face)
-deriving stock instance
-  (Show vertex, Show directed, Show undirected, Show face)
-  => Show (RemovalResult mode vertex directed undirected face)
-
--- | Remove the vertex a handle names. The triangulation the caller passed in
--- still denotes the mesh it always did; the removal is in the returned one.
---
--- This is a session over a single 'excise', which is what makes replacing a
--- fold of it with one session sound: the two agree on every mesh, and differ
--- only in how many of the intermediate ones are published. A fold publishes
--- @k@ meshes and pays a thaw for each, so it runs in Θ(n·k); the session pays
--- one thaw and runs in O(k·deg).
-removeVertex
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> Either BuildError (RemovalResult mode vertex directed undirected face)
-removeVertex triangulation requested = do
-  (outcome, frozen, stats) <- withLocalSession triangulation 0 (excise requested)
-  pure
-    RemovalResult
-      { removalTriangulation = frozen
-      , removalOutcome = outcome
-      , removalStats = stats
-      }
-
--- | Resolve a point through the mesh's exact derived identity section, then
--- remove the handle it proves. A hash is only a rejection filter; the lookup
--- confirms both authoritative coordinate planes before the topology is
--- opened. A point that sites no vertex answers 'Nothing' without publishing a
--- cosmetically different mesh.
---
--- See 'removeVertex' on the cost of folding this rather than opening one
--- session over @removeAt@.
-locateAndRemove
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Either BuildError (Maybe (RemovalResult mode vertex directed undirected face))
-locateAndRemove triangulation point = do
-  queryPoint <- validatePoint Nothing point
-  case
-      lookupPointIndex
-        (triPointX triangulation)
-        (triPointY triangulation)
-        (triPointIndex triangulation)
-        -- Stored coordinates are canonical; the query must be too, or any
-        -- coordinate whose canonicalization exceeds signed zero misses a vertex
-        -- the session verbs would find.
-        (queryPointValue queryPoint) of
-    Nothing -> Right Nothing
-    Just rawVertex ->
-      Just <$> removeVertex triangulation (VertexId (fromIntegral rawVertex))
-
--- Polymorphic entries consumed from other packages; without exposed
--- unfoldings they run boxed through an imported boundary.
diff --git a/src-build/Moonlight/Triangulation/Session.hs b/src-build/Moonlight/Triangulation/Session.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/Session.hs
+++ /dev/null
@@ -1,17 +0,0 @@
--- | One sealed editing transaction: thaw once, compose edits, publish once.
-module Moonlight.Triangulation.Session
-  ( RemovalOutcome (..)
-  , Session
-  , withSession
-  , insertVertex
-  , insertVertexAt
-  , insertVertexAtNear
-  , insertVertexAtNearVertex
-  , removeAt
-  , removeAtNear
-  , removeManyAt
-  , removeManyAtNear
-  , refuse
-  ) where
-
-import Moonlight.Triangulation.Internal.Session
diff --git a/src-build/Moonlight/Triangulation/SetAlgebra.hs b/src-build/Moonlight/Triangulation/SetAlgebra.hs
deleted file mode 100644
--- a/src-build/Moonlight/Triangulation/SetAlgebra.hs
+++ /dev/null
@@ -1,232 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Finite-set operations on unconstrained meshes. Every
--- constructing operation returns the finite-arena obstruction instead of
--- laundering it through a partial class instance.
-module Moonlight.Triangulation.SetAlgebra
-  ( siteRelation
-  , union
-  , unions
-  , intersection
-  , intersectionWith
-  , difference
-  , symmetricDifference
-  ) where
-
-import Data.Foldable (traverse_)
-import Data.Maybe (isJust)
-import qualified Data.Vector as V
-import Moonlight.Triangulation.BulkLoad (empty)
-import Moonlight.Triangulation.Dcel (numVertices, vertexData, vertexPoint, vertexPoints)
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (vertices)
-import Moonlight.Triangulation.Internal.Join (joinBalanced, joinNormalForm)
-import Moonlight.Triangulation.Internal.Join.SiteSet
-  ( rebuildCanonicalSiteSet
-  , siteSetDifference
-  , siteSetFromTriangulation
-  , siteSetIntersectionWith
-  , siteSetPoints
-  , siteRelationFromTriangulations
-  , siteSetSymmetricDifferenceFromTriangulations
-  , siteSupportFromTriangulation
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types
-  ( BuildError (PointLocationFailed)
-  , ConstraintMode (Unconstrained)
-  , InsertionDisposition (..)
-  , Point
-  , SiteRelation (..)
-  , unitElementDefaults
-  )
-import Moonlight.Triangulation.JoinSemilattice (JoinSemilattice)
-import Moonlight.Triangulation.Internal.Session
-  ( Session
-  , insertVertexAt
-  , refuse
-  , removeAtNear
-  , removeManyAt
-  , withLocalSession
-  )
-
--- | Exact relation between two triangulations' coordinate supports. Vertex
--- annotations and topology-element payloads are observations over the support;
--- none participates in this classification.
-siteRelation
-  :: Triangulation leftMode leftAnnotation leftDirected leftUndirected leftFace
-  -> Triangulation rightMode rightAnnotation rightDirected rightUndirected rightFace
-  -> SiteRelation
-siteRelation left right =
-  siteRelationFromTriangulations left right
-{-# INLINE siteRelation #-}
-
--- | A valid Delaunay representative of both site sets. Use
--- 'Moonlight.Triangulation.Canonical.canonicalize'
--- when construction-independent dense numbering is required.
-union
-  :: JoinSemilattice annotation
-  => Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-union = joinNormalForm
-
--- | @union@ over a list, folded as a balanced tournament.
-unions
-  :: JoinSemilattice annotation
-  => [Triangulation 'Unconstrained annotation () () ()]
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-unions = joinBalanced
-
--- | The sites both meshes hold.
-intersection
-  :: Triangulation 'Unconstrained () () () ()
-  -> Triangulation 'Unconstrained () () () ()
-  -> Either BuildError (Triangulation 'Unconstrained () () () ())
-intersection left right
-  | numVertices left == 0 = Right left
-  | numVertices right == 0 = Right right
-  | otherwise =
-      case siteRelationFromTriangulations left right of
-        EqualSites -> Right left
-        LeftProperSubset -> Right left
-        RightProperSubset -> Right right
-        DisjointSites -> Right (empty unitElementDefaults)
-        PartialOverlap overlap -> intersectPartialOverlap overlap
- where
-  leftSites = siteSupportFromTriangulation left
-  rightSites = siteSupportFromTriangulation right
-  rebuildIntersection = intersectionWith (\_ _ -> ()) left right
-  intersectPartialOverlap overlap
-    | leftRemoved <= rightRemoved
-    , removalDeltaIsEligible leftRemoved overlap =
-        removeExpectedFrom left (siteSetPoints (siteSetDifference leftSites rightSites))
-    | rightRemoved < leftRemoved
-    , removalDeltaIsEligible rightRemoved overlap =
-        removeExpectedFrom right (siteSetPoints (siteSetDifference rightSites leftSites))
-    | otherwise = rebuildIntersection
-   where
-    leftRemoved = numVertices left - overlap
-    rightRemoved = numVertices right - overlap
-{-# INLINE intersection #-}
-
--- | The sites both meshes hold, with the result annotation computed from the
--- left and right annotations at that exact coordinate. The combiner is called
--- only for shared sites, in left-then-right order; geometry remains the sole
--- authority for membership. @intersectionWith const left mask@ is the
--- annotation-preserving restriction of @left@ to @mask@'s support.
-intersectionWith
-  :: (leftAnnotation -> rightAnnotation -> annotation)
-  -> Triangulation 'Unconstrained leftAnnotation () () ()
-  -> Triangulation 'Unconstrained rightAnnotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-intersectionWith combine left right =
-  rebuildCanonicalSiteSet
-    ( siteSetIntersectionWith
-        combine
-        (siteSetFromTriangulation left)
-        (siteSetFromTriangulation right)
-    )
-{-# INLINE intersectionWith #-}
-
--- | The left's sites, less the right's.
-difference
-  :: Triangulation 'Unconstrained leftAnnotation () () ()
-  -> Triangulation 'Unconstrained rightAnnotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained leftAnnotation () () ())
-difference left right
-  | numVertices left == 0 || numVertices right == 0 = Right left
-  | numVertices right < numVertices left
-  , removalDeltaIsEligible (numVertices right) (numVertices left - numVertices right) =
-      removeAvailableFrom left (vertexPoints right)
-  | otherwise =
-      rebuildCanonicalSiteSet
-        ( siteSetDifference
-            (siteSetFromTriangulation left)
-            (siteSupportFromTriangulation right)
-        )
-{-# INLINE difference #-}
-
--- | The sites exactly one mesh holds.
-symmetricDifference
-  :: Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-symmetricDifference left right
-  | numVertices left == 0 = Right right
-  | numVertices right == 0 = Right left
-  | numVertices right < numVertices left
-  , toggleDeltaIsEligible (numVertices right) (numVertices left - numVertices right) =
-      toggleIncoming left right
-  | numVertices left < numVertices right
-  , toggleDeltaIsEligible (numVertices left) (numVertices right - numVertices left) =
-      toggleIncoming right left
-  | otherwise =
-      siteSetSymmetricDifferenceFromTriangulations left right
-        >>= rebuildCanonicalSiteSet
-{-# INLINE symmetricDifference #-}
-
-removalDeltaIsEligible :: Int -> Int -> Bool
-removalDeltaIsEligible removed survivors = removed <= survivors `quot` 128
-{-# INLINE removalDeltaIsEligible #-}
-
-toggleDeltaIsEligible :: Int -> Int -> Bool
-toggleDeltaIsEligible incoming residentRemainder = incoming <= residentRemainder `quot` 128
-{-# INLINE toggleDeltaIsEligible #-}
-
-removeAvailableFrom
-  :: Triangulation 'Unconstrained annotation () () ()
-  -> V.Vector (Point)
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-removeAvailableFrom triangulation points =
-  fmap published
-    (withLocalSession triangulation 0 (V.any isJust <$> removeManyAt points))
- where
-  published (removed, revised, _) = if removed then revised else triangulation
-{-# INLINE removeAvailableFrom #-}
-
-removeExpectedFrom
-  :: Triangulation 'Unconstrained annotation () () ()
-  -> V.Vector (Point)
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-removeExpectedFrom triangulation points =
-  fmap (\(_, revised, _) -> revised)
-    (withLocalSession triangulation 0 (removeExpectedPoints points))
-{-# INLINE removeExpectedFrom #-}
-
-removeExpectedPoints
-  :: V.Vector (Point)
-  -> Session state annotation () () () ()
-removeExpectedPoints points = do
-  outcomes <- removeManyAt points
-  V.zipWithM_
-    (\point outcome -> maybe (refuse (PointLocationFailed point)) (const (pure ())) outcome)
-    points
-    outcomes
-{-# INLINE removeExpectedPoints #-}
-
-toggleIncoming
-  :: Triangulation 'Unconstrained annotation () () ()
-  -> Triangulation 'Unconstrained annotation () () ()
-  -> Either BuildError (Triangulation 'Unconstrained annotation () () ())
-toggleIncoming base incoming =
-  fmap (\(_, revised, _) -> revised)
-    (withLocalSession base (numVertices incoming) (toggleIncomingVertices incoming))
-{-# INLINE toggleIncoming #-}
-
-toggleIncomingVertices
-  :: Triangulation 'Unconstrained annotation () () ()
-  -> Session state annotation () () () ()
-toggleIncomingVertices incoming =
-  traverse_
-    (\vertex -> do
-      let point = vertexPoint incoming vertex
-          annotation = vertexData incoming vertex
-      (fresh, disposition) <- insertVertexAt point annotation
-      case disposition of
-        Inserted -> pure ()
-        AlreadyPresent -> do
-          outcome <- removeAtNear fresh point
-          maybe (refuse (PointLocationFailed point)) (const (pure ())) outcome
-    )
-    (vertices incoming)
-{-# INLINE toggleIncomingVertices #-}
diff --git a/src-cell-complex/Moonlight/Hex/CellComplex.hs b/src-cell-complex/Moonlight/Hex/CellComplex.hs
--- a/src-cell-complex/Moonlight/Hex/CellComplex.hs
+++ b/src-cell-complex/Moonlight/Hex/CellComplex.hs
@@ -7,7 +7,6 @@
 import Data.Bifunctor (first)
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Set qualified as Set
-import Data.Vector qualified as Vector
 import Moonlight.Hex.Coordinate (HexCoord)
 import Moonlight.Hex.Element
   ( HexSide
@@ -20,7 +19,7 @@
   , hexCellVertices
   , hexSideEndpoints
   )
-import Moonlight.Hex.Region (HexRegion, hexRegionCoords)
+import Moonlight.Hex.Region (HexRegion, foldHexRegionCoords)
 import Moonlight.Homology.Boundary
   ( BoundaryIncidence
   , FiniteChainComplex
@@ -54,7 +53,7 @@
   first HexCellComplexInvalid
     (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
  where
-  faceBasis = Vector.toList (hexRegionCoords region)
+  faceBasis = reverse (foldHexRegionCoords (flip (:)) [] region)
   edgeBasis =
     Set.toAscList
       (Set.fromList (faceBasis >>= NonEmpty.toList . hexCellSides))
diff --git a/src-cell-complex/Moonlight/Planar/CellComplex.hs b/src-cell-complex/Moonlight/Planar/CellComplex.hs
new file mode 100644
--- /dev/null
+++ b/src-cell-complex/Moonlight/Planar/CellComplex.hs
@@ -0,0 +1,642 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | An admitted exact geometric cell selection as a generic
+-- 'CellComplex2D', an integral cellular chain complex, and an exact filtered
+-- alpha complex. The 'ExactCellSet' remains the semantic owner: this module
+-- supplies only the incidence and Homology interpretations required by
+-- downstream topology.
+module Moonlight.Planar.CellComplex
+  ( DCELComplex,
+    DCELCellEdge (..),
+    FaceCut,
+    faceCutFace,
+    faceCutComponent,
+    DCELError (..),
+    fromExactCellSet,
+    finiteChainComplex,
+    filteredAlphaComplex,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.IntMap.Strict qualified as IntMap
+import Data.IntSet qualified as IntSet
+import Data.List qualified as List
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Vector qualified as Vector
+import GHC.Exts (build)
+import Moonlight.Algebra.Pure.Orientation (Orientation (..))
+import Moonlight.Homology.Boundary
+  ( BoundaryIncidence
+  , BoundaryIncidenceShapeError
+  , BoundaryEntry
+  , FiniteChainComplex
+  , emptyBoundaryIncidence
+  , emptyBoundaryIncidenceOf
+  , mkBoundaryEntryFromInts
+  , mkBoundaryIncidenceFromOrderedColumns
+  , mkBoundaryIncidenceFromOrderedEntries
+  , mkFiniteChainComplexChecked
+  , targetIndex
+  )
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..)
+  , HomologyFailure
+  )
+import Moonlight.Homology.Persistence
+  ( FilteredFiniteChainComplex
+  , mkFilteredFiniteChainComplex
+  )
+import Moonlight.Homology.Pure.Topology.CellComplex
+  ( CellComplex2D (..)
+  , CellRef (..)
+  , CellTypes (..)
+  , OrientedEdge (..)
+  , ValidateComplex2D (..)
+  )
+import Moonlight.Homology.Topology (BasisCellRef (..))
+import Moonlight.Planar.Alpha
+  ( AlphaBirth
+  , AlphaFiltration
+  , alphaEdgeBirth
+  , alphaFaceBirth
+  , alphaVertexBirth
+  , withAlphaResidentTriangulation
+  )
+import Moonlight.Planar.Dcel qualified as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId,
+    FaceId (..),
+    UndirectedEdgeId (..),
+    VertexId (..),
+    asUndirected,
+    directedPair,
+    isNormalized,
+  )
+import Moonlight.Planar.Internal.CellSet (ExactCellSet (..))
+import Moonlight.Planar.Internal.Incidence
+  ( FaceBoundaryComponent (..),
+    PlanarIncidence,
+    circularEdgeWalk,
+    faceInnerBoundaryComponents,
+    incidenceAdjacentEdge,
+    incidenceDirectedEdgeCount,
+    incidenceIncidentFace,
+    incidenceNext,
+    incidenceNonCellularFaces,
+    incidenceOrigin,
+    incidenceUndirectedEndpoints,
+  )
+import Moonlight.Planar.Types (Triangulation)
+
+-- | The cellular refinement of one closed geometric selection. The two cut
+-- indices are derived views of its face components, never a second selection.
+-- Native triangulations have empty cut indices and retain every native handle.
+data DCELComplex = DCELComplex
+  !ExactCellSet
+  !(IntMap.IntMap [FaceCut])
+  !(IntMap.IntMap [FaceCut])
+
+-- | Geometric atoms retain their original identities. A cut is an abstract
+-- arc in one face, not a fabricated resident DCEL edge or a new exact segment.
+data DCELCellEdge
+  = GeometricCellEdge !UndirectedEdgeId
+  | FaceBoundaryCut !FaceCut
+  deriving stock (Eq, Ord, Show)
+
+-- Construction fixes endpoints from the sealed face-component incidence.
+-- The owning face and component root give restriction-stable cut identities.
+data FaceCut = FaceCut !FaceId !FaceBoundaryComponent !VertexId !VertexId
+  deriving stock (Eq, Ord, Show)
+
+faceCutFace :: FaceCut -> FaceId
+faceCutFace (FaceCut face _ _ _) = face
+
+faceCutComponent :: FaceCut -> FaceBoundaryComponent
+faceCutComponent (FaceCut _ component _ _) = component
+
+-- | Incidence materialization, Homology's independent chain-law seal, and
+-- the exact-birth join retain typed obstructions at their respective boundaries.
+type DCELCellRef = CellRef VertexId DCELCellEdge FaceId
+
+data DCELError
+  = DCELBoundaryCellMissing !DCELCellRef !DCELCellRef
+  | DCELBoundaryIncidenceInvalid !BoundaryIncidenceShapeError
+  | DCELChainComplexInvalid !HomologyFailure
+  | DCELAlphaBirthMissing !DCELCellRef
+  | DCELFilteredComplexInvalid !HomologyFailure
+  | DCELBoundedFaceAnchorMissing !FaceId
+  deriving stock (Eq, Show)
+
+-- | Cut each disconnected inner boundary component to its bounded face's
+-- outer component. Planar arrangement admission supplies the genus-zero face
+-- and its oriented boundary-component decomposition; the standard cut-system
+-- construction then gives one disk characteristic map. A slit or cut appears
+-- twice oppositely in its attaching walk. Chain nilpotence checks the resulting
+-- coefficients, but is not the proof that a holed stratum was a disk.
+--
+-- Only the sparse noncellular-face support is visited. Native triangulations
+-- have empty support, so their construction remains constant-time.
+fromExactCellSet :: ExactCellSet -> Either DCELError DCELComplex
+fromExactCellSet cells@(ExactCellSet incidence _ _ selectedFaces) =
+  case filter (\(FaceId rawFace) -> IntSet.member (fromIntegral rawFace) selectedFaces) (incidenceNonCellularFaces incidence) of
+    [] -> Right (DCELComplex cells IntMap.empty IntMap.empty)
+    nonCellularFaces -> do
+      cutsByFace <-
+        IntMap.fromDistinctAscList <$> traverse
+          (\face@(FaceId rawFace) -> do
+            root <- maybe (Left (DCELBoundedFaceAnchorMissing face)) Right (incidenceAdjacentEdge incidence face)
+            let sourceVertex = incidenceOrigin incidence root
+                cuts = List.sort
+                  (fmap
+                    (\component -> FaceCut face component sourceVertex (componentBaseVertex incidence component))
+                    (faceInnerBoundaryComponents incidence face))
+            pure (fromIntegral rawFace, cuts))
+          nonCellularFaces
+      let cutsByVertex =
+            fmap List.sort
+              ( IntMap.fromListWith (<>)
+                  [ (basisIndexOfVertex vertex, [cut])
+                  | cuts <- IntMap.elems cutsByFace,
+                    cut@(FaceCut _ _ sourceVertex targetVertex) <- cuts,
+                    vertex <- [sourceVertex, targetVertex]
+                  ]
+              )
+      pure (DCELComplex cells cutsByFace cutsByVertex)
+
+componentBaseVertex :: PlanarIncidence -> FaceBoundaryComponent -> VertexId
+componentBaseVertex incidence component =
+  case component of
+    BoundaryCycleRoot root -> incidenceOrigin incidence root
+    IsolatedBoundaryVertex vertex -> vertex
+
+-- | Canonical integral cellular chains in ascending resident-handle order.
+-- Degree one uses target minus source; degree two uses the DCEL's oriented
+-- face boundary. The Homology boundary seals the result only after checking
+-- shape and @d . d = 0@; no unchecked chain constructor crosses the public
+-- package boundary.
+finiteChainComplex :: DCELComplex -> Either DCELError (FiniteChainComplex Int)
+finiteChainComplex complexValue =
+  let basis = dcelBasis complexValue
+   in finiteChainComplexWithBasis complexValue basis
+
+-- | Lower the exact alpha section into Homology without converting its birth
+-- order through binary64. Persistence remains wholly owned by Homology.
+filteredAlphaComplex
+  :: AlphaFiltration
+  -> Either DCELError (FilteredFiniteChainComplex AlphaBirth Int)
+filteredAlphaComplex filtration =
+  withAlphaResidentTriangulation
+    (\triangulation -> do
+      finite <- residentAlphaFiniteChainComplex triangulation
+      vertexBirthAssignments <-
+        traverse
+          ( residentAlphaBirthAssignment
+              CellVertexRef
+              residentVertexBasisRef
+              (alphaVertexBirth filtration)
+          )
+          (vertexHandlesOf triangulation)
+      edgeBirthAssignments <-
+        traverse
+          ( residentAlphaBirthAssignment
+              (CellEdgeRef . GeometricCellEdge)
+              residentEdgeBasisRef
+              (alphaEdgeBirth filtration)
+          )
+          (undirectedEdgesOf triangulation)
+      faceBirthAssignments <-
+        traverse
+          ( residentAlphaBirthAssignment
+              CellFaceRef
+              residentFaceBasisRef
+              (alphaFaceBirth filtration)
+          )
+          (innerFacesOf triangulation)
+      first DCELFilteredComplexInvalid
+        ( mkFilteredFiniteChainComplex
+            finite
+            (vertexBirthAssignments <> edgeBirthAssignments <> faceBirthAssignments)
+        )
+    )
+    filtration
+
+-- | The opaque 'AlphaFiltration' constructor admits the entire resident DCEL,
+-- whose handle ranges are dense. This local section therefore lowers those
+-- handles directly to basis indices while retaining Homology's independent
+-- shape and nilpotence seal. Sparse 'ExactCellSet' values continue through the
+-- generic map-indexed 'finiteChainComplex' path.
+residentAlphaFiniteChainComplex
+  :: Triangulation mode vertex directed undirected face
+  -> Either DCELError (FiniteChainComplex Int)
+residentAlphaFiniteChainComplex triangulation = do
+  let degreeOneColumns =
+        Vector.generate
+          (Dcel.numUndirectedEdges triangulation)
+          (residentEdgeBoundaryEntries triangulation . UndirectedEdgeId . fromIntegral)
+      degreeTwoColumns =
+        Vector.imap
+          residentFaceBoundaryEntries
+          (Dcel.innerFaceDirectedEdgeTriples triangulation)
+  degreeOneBoundary <-
+    first DCELBoundaryIncidenceInvalid
+      ( mkBoundaryIncidenceFromOrderedColumns
+          (fromIntegral (Dcel.numUndirectedEdges triangulation))
+          (fromIntegral (Dcel.numVertices triangulation))
+          degreeOneColumns
+      )
+  degreeTwoBoundary <-
+    first DCELBoundaryIncidenceInvalid
+      ( mkBoundaryIncidenceFromOrderedColumns
+          (fromIntegral (Dcel.numInnerFaces triangulation))
+          (fromIntegral (Dcel.numUndirectedEdges triangulation))
+          degreeTwoColumns
+      )
+  let degreeZeroBoundary =
+        emptyBoundaryIncidenceOf
+          (fromIntegral (Dcel.numVertices triangulation))
+          0
+      boundaryAt (HomologicalDegree degreeValue) =
+        case degreeValue of
+          0 -> degreeZeroBoundary
+          1 -> degreeOneBoundary
+          2 -> degreeTwoBoundary
+          _ -> emptyBoundaryIncidence
+  first DCELChainComplexInvalid
+    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
+
+residentEdgeBoundaryEntries
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> [BoundaryEntry Int]
+residentEdgeBoundaryEntries triangulation edgeValue =
+  let (sourceVertex, targetVertex) = Dcel.undirectedEndpoints triangulation edgeValue
+      sourceEntry =
+        mkBoundaryEntryFromInts
+          (basisIndexOfEdge edgeValue)
+          (basisIndexOfVertex sourceVertex)
+          (-1)
+      targetEntry =
+        mkBoundaryEntryFromInts
+          (basisIndexOfEdge edgeValue)
+          (basisIndexOfVertex targetVertex)
+          1
+   in if targetIndex sourceEntry <= targetIndex targetEntry
+        then [sourceEntry, targetEntry]
+        else [targetEntry, sourceEntry]
+
+residentFaceBoundaryEntries
+  :: Int
+  -> (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
+  -> [BoundaryEntry Int]
+residentFaceBoundaryEntries faceIndexValue (firstEdge, secondEdge, thirdEdge) =
+  sortThreeBoundaryEntries
+    (residentFaceBoundaryEntry faceIndexValue firstEdge)
+    (residentFaceBoundaryEntry faceIndexValue secondEdge)
+    (residentFaceBoundaryEntry faceIndexValue thirdEdge)
+
+sortThreeBoundaryEntries
+  :: BoundaryEntry Int
+  -> BoundaryEntry Int
+  -> BoundaryEntry Int
+  -> [BoundaryEntry Int]
+sortThreeBoundaryEntries firstEntry secondEntry thirdEntry =
+  let (firstLow, firstHigh) = orderedBoundaryPair firstEntry secondEntry
+      (secondLow, finalHigh) = orderedBoundaryPair firstHigh thirdEntry
+      (finalLow, finalMiddle) = orderedBoundaryPair firstLow secondLow
+   in [finalLow, finalMiddle, finalHigh]
+
+orderedBoundaryPair
+  :: BoundaryEntry Int
+  -> BoundaryEntry Int
+  -> (BoundaryEntry Int, BoundaryEntry Int)
+orderedBoundaryPair firstEntry secondEntry =
+  if targetIndex firstEntry <= targetIndex secondEntry
+    then (firstEntry, secondEntry)
+    else (secondEntry, firstEntry)
+
+residentFaceBoundaryEntry :: Int -> DirectedEdgeId -> BoundaryEntry Int
+residentFaceBoundaryEntry faceIndexValue directedEdge =
+  mkBoundaryEntryFromInts
+    faceIndexValue
+    (basisIndexOfEdge (asUndirected directedEdge))
+    (if isNormalized directedEdge then 1 else -1)
+
+residentAlphaBirthAssignment
+  :: (cell -> DCELCellRef)
+  -> (cell -> BasisCellRef)
+  -> (cell -> Maybe AlphaBirth)
+  -> cell
+  -> Either DCELError (BasisCellRef, AlphaBirth)
+residentAlphaBirthAssignment cellReference basisReference birthAt cell =
+  maybe
+    (Left (DCELAlphaBirthMissing (cellReference cell)))
+    (Right . (,) (basisReference cell))
+    (birthAt cell)
+
+residentVertexBasisRef :: VertexId -> BasisCellRef
+residentVertexBasisRef vertexValue =
+  BasisCellRef (HomologicalDegree 0) (basisIndexOfVertex vertexValue)
+
+residentEdgeBasisRef :: UndirectedEdgeId -> BasisCellRef
+residentEdgeBasisRef edgeValue =
+  BasisCellRef (HomologicalDegree 1) (basisIndexOfEdge edgeValue)
+
+residentFaceBasisRef :: FaceId -> BasisCellRef
+residentFaceBasisRef faceValue =
+  BasisCellRef (HomologicalDegree 2) (basisIndexOfFace faceValue)
+
+basisIndexOfVertex :: VertexId -> Int
+basisIndexOfVertex (VertexId rawVertex) = fromIntegral rawVertex
+
+basisIndexOfEdge :: UndirectedEdgeId -> Int
+basisIndexOfEdge (UndirectedEdgeId rawEdge) = fromIntegral rawEdge
+
+basisIndexOfFace :: FaceId -> Int
+basisIndexOfFace (FaceId rawFace) = fromIntegral rawFace - 1
+
+undirectedEdgesOf
+  :: Triangulation mode vertex directed undirected face
+  -> [UndirectedEdgeId]
+undirectedEdgesOf triangulation =
+  fmap (UndirectedEdgeId . fromIntegral) [0 .. Dcel.numUndirectedEdges triangulation - 1]
+
+vertexHandlesOf
+  :: Triangulation mode vertex directed undirected face
+  -> [VertexId]
+vertexHandlesOf triangulation =
+  fmap (VertexId . fromIntegral) [0 .. Dcel.numVertices triangulation - 1]
+
+innerFacesOf
+  :: Triangulation mode vertex directed undirected face
+  -> [FaceId]
+innerFacesOf triangulation =
+  fmap (FaceId . fromIntegral) [1 .. Dcel.numFaces triangulation - 1]
+
+data DCELBasis = DCELBasis
+  { dcelVertexBasis :: !(Map VertexId BasisCellRef)
+  , dcelEdgeBasis :: !(Map DCELCellEdge BasisCellRef)
+  , dcelFaceBasis :: !(Map FaceId BasisCellRef)
+  }
+
+dcelBasis :: DCELComplex -> DCELBasis
+dcelBasis complexValue =
+  DCELBasis
+    { dcelVertexBasis = basisMap 0 (vertices complexValue)
+    , dcelEdgeBasis = basisMap 1 (edges complexValue)
+    , dcelFaceBasis = basisMap 2 (faces complexValue)
+    }
+
+basisMap :: Ord cell => Int -> [cell] -> Map cell BasisCellRef
+basisMap degreeValue cells =
+  Map.fromAscList
+    ( zipWith
+        (\indexValue cell -> (cell, BasisCellRef (HomologicalDegree degreeValue) indexValue))
+        [0 ..]
+        cells
+    )
+
+finiteChainComplexWithBasis
+  :: DCELComplex
+  -> DCELBasis
+  -> Either DCELError (FiniteChainComplex Int)
+finiteChainComplexWithBasis complexValue basis = do
+  degreeOneEntries <-
+    concat
+      <$> traverse
+        (edgeBoundaryEntries complexValue basis)
+        (Map.toAscList (dcelEdgeBasis basis))
+  degreeTwoEntries <-
+    concat
+      <$> traverse
+        (faceBoundaryEntries complexValue basis)
+        (Map.toAscList (dcelFaceBasis basis))
+  degreeOneBoundary <-
+    first DCELBoundaryIncidenceInvalid
+      ( mkBoundaryIncidenceFromOrderedEntries
+          (fromIntegral (Map.size (dcelEdgeBasis basis)))
+          (fromIntegral (Map.size (dcelVertexBasis basis)))
+          degreeOneEntries
+      )
+  degreeTwoBoundary <-
+    first DCELBoundaryIncidenceInvalid
+      ( mkBoundaryIncidenceFromOrderedEntries
+          (fromIntegral (Map.size (dcelFaceBasis basis)))
+          (fromIntegral (Map.size (dcelEdgeBasis basis)))
+          degreeTwoEntries
+      )
+  let degreeZeroBoundary =
+        emptyBoundaryIncidenceOf
+          (fromIntegral (Map.size (dcelVertexBasis basis)))
+          0
+      boundaryAt :: HomologicalDegree -> BoundaryIncidence Int
+      boundaryAt (HomologicalDegree degreeValue) =
+        case degreeValue of
+          0 -> degreeZeroBoundary
+          1 -> degreeOneBoundary
+          2 -> degreeTwoBoundary
+          _ -> emptyBoundaryIncidence
+  first DCELChainComplexInvalid
+    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
+
+edgeBoundaryEntries
+  :: DCELComplex
+  -> DCELBasis
+  -> (DCELCellEdge, BasisCellRef)
+  -> Either DCELError [BoundaryEntry Int]
+edgeBoundaryEntries complexValue basis (edgeValue, edgeBasisRef) = do
+  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
+      sourceCell = CellEdgeRef edgeValue
+  sourceBasisRef <-
+    requireBoundaryCell
+      sourceCell
+      (CellVertexRef sourceVertex)
+      sourceVertex
+      (dcelVertexBasis basis)
+  targetBasisRef <-
+    requireBoundaryCell
+      sourceCell
+      (CellVertexRef targetVertex)
+      targetVertex
+      (dcelVertexBasis basis)
+  pure
+    ( case orderedBoundaryPair
+        ( mkBoundaryEntryFromInts
+            (cellIndex edgeBasisRef)
+            (cellIndex sourceBasisRef)
+            (-1)
+        )
+        ( mkBoundaryEntryFromInts
+            (cellIndex edgeBasisRef)
+            (cellIndex targetBasisRef)
+            1
+        ) of
+        (lowerEntry, higherEntry) -> [lowerEntry, higherEntry]
+    )
+
+faceBoundaryEntries
+  :: DCELComplex
+  -> DCELBasis
+  -> (FaceId, BasisCellRef)
+  -> Either DCELError [BoundaryEntry Int]
+faceBoundaryEntries complexValue basis (faceValue, faceBasisRef) =
+  List.sortOn targetIndex
+    <$> traverse
+      boundaryEntry
+      (faceBoundary complexValue faceValue)
+ where
+  boundaryEntry orientedBoundary = do
+    let edgeValue = orientedEdge orientedBoundary
+    edgeBasisRef <-
+      requireBoundaryCell
+        (CellFaceRef faceValue)
+        (CellEdgeRef edgeValue)
+        edgeValue
+        (dcelEdgeBasis basis)
+    pure
+      ( mkBoundaryEntryFromInts
+          (cellIndex faceBasisRef)
+          (cellIndex edgeBasisRef)
+          (orientationCoefficient (edgeOrientation orientedBoundary))
+      )
+
+orientationCoefficient :: Orientation -> Int
+orientationCoefficient orientation =
+  case orientation of
+    Positive -> 1
+    Negative -> -1
+
+requireBoundaryCell
+  :: Ord cell
+  => DCELCellRef
+  -> DCELCellRef
+  -> cell
+  -> Map cell BasisCellRef
+  -> Either DCELError BasisCellRef
+requireBoundaryCell sourceCell targetCell cell basis =
+  maybe
+    (Left (DCELBoundaryCellMissing sourceCell targetCell))
+    Right
+    (Map.lookup cell basis)
+
+instance CellTypes DCELComplex where
+  type Vertex DCELComplex = VertexId
+  type Edge DCELComplex = DCELCellEdge
+  type Face DCELComplex = FaceId
+
+instance CellComplex2D DCELComplex where
+  vertices (DCELComplex (ExactCellSet _ selectedVertices _ _) _ _) =
+    build
+      (\emit finish ->
+        IntMap.foldrWithKey
+          (\rawVertex _ remaining -> emit (VertexId (fromIntegral rawVertex)) remaining)
+          finish
+          selectedVertices)
+  {-# INLINE vertices #-}
+
+  edges (DCELComplex (ExactCellSet _ _ selectedEdges _) cutsByFace _) =
+    if IntMap.null cutsByFace
+      then build foldGeometricEdges
+      else build
+        (\emit finish ->
+          foldGeometricEdges emit
+            (IntMap.foldr (\cuts remaining -> foldr (emit . FaceBoundaryCut) remaining cuts) finish cutsByFace))
+    where
+      foldGeometricEdges :: (DCELCellEdge -> result -> result) -> result -> result
+      foldGeometricEdges emit finish =
+        IntSet.foldr
+          (\rawEdge remaining -> emit (GeometricCellEdge (UndirectedEdgeId (fromIntegral rawEdge))) remaining)
+          finish
+          selectedEdges
+      {-# INLINE foldGeometricEdges #-}
+  {-# INLINE edges #-}
+
+  faces (DCELComplex (ExactCellSet _ _ _ selectedFaces) _ _) =
+    build
+      (\emit finish ->
+        IntSet.foldr
+          (\rawFace remaining -> emit (FaceId (fromIntegral rawFace)) remaining)
+          finish
+          selectedFaces)
+  {-# INLINE faces #-}
+
+  edgeBoundary (DCELComplex (ExactCellSet incidence _ _ _) _ _) edgeValue =
+    case edgeValue of
+      GeometricCellEdge edge -> incidenceUndirectedEndpoints incidence edge
+      FaceBoundaryCut (FaceCut _ _ sourceVertex targetVertex) -> (sourceVertex, targetVertex)
+  {-# INLINE edgeBoundary #-}
+
+  faceBoundary (DCELComplex (ExactCellSet incidence _ _ _) cutsByFace _) face@(FaceId rawFace) =
+    build
+      (\(emit :: OrientedEdge DCELCellEdge -> result -> result) (finish :: result) ->
+        let geometricBoundary :: DirectedEdgeId -> result -> result
+            geometricBoundary root remaining =
+              foldr
+                (emit . orientedBoundaryEdge)
+                remaining
+                (circularEdgeWalk (incidenceDirectedEdgeCount incidence) root (incidenceNext incidence))
+            cutBoundary :: FaceCut -> result -> result
+            cutBoundary cut remaining =
+              let innerTail = emit (OrientedEdge (FaceBoundaryCut cut) Negative) remaining
+               in emit (OrientedEdge (FaceBoundaryCut cut) Positive)
+                    (case faceCutComponent cut of
+                       BoundaryCycleRoot root -> geometricBoundary root innerTail
+                       IsolatedBoundaryVertex _ -> innerTail)
+            cutTail :: result
+            cutTail = foldr cutBoundary finish (IntMap.findWithDefault [] (fromIntegral rawFace) cutsByFace)
+         in maybe cutTail (\root -> geometricBoundary root cutTail) (incidenceAdjacentEdge incidence face))
+    where
+      orientedBoundaryEdge :: DirectedEdgeId -> OrientedEdge DCELCellEdge
+      orientedBoundaryEdge directedEdge =
+        OrientedEdge
+          { orientedEdge = GeometricCellEdge (asUndirected directedEdge),
+            edgeOrientation =
+              if isNormalized directedEdge
+                then Positive
+                else Negative
+          }
+  {-# INLINE faceBoundary #-}
+
+  edgesAtVertex (DCELComplex (ExactCellSet incidence _ selectedEdges _) _ cutsByVertex) vertex =
+    case IntMap.lookup (basisIndexOfVertex vertex) cutsByVertex of
+      Nothing -> build foldIncidentGeometricEdges
+      Just cuts -> build
+        (\emit finish -> foldIncidentGeometricEdges emit (foldr (emit . FaceBoundaryCut) finish cuts))
+    where
+      foldIncidentGeometricEdges :: (DCELCellEdge -> result -> result) -> result -> result
+      foldIncidentGeometricEdges emit finish =
+        IntSet.foldr
+          (\rawEdge remaining ->
+            let edge = UndirectedEdgeId (fromIntegral rawEdge)
+             in if edgeContainsVertex incidence vertex edge
+                  then emit (GeometricCellEdge edge) remaining
+                  else remaining)
+          finish
+          selectedEdges
+      {-# INLINE foldIncidentGeometricEdges #-}
+  {-# INLINE edgesAtVertex #-}
+
+  facesAtEdge (DCELComplex (ExactCellSet incidence _ _ selectedFaces) _ _) edgeValue =
+    let selectedFace face@(FaceId rawFace) =
+          if IntSet.member (fromIntegral rawFace) selectedFaces then Just face else Nothing
+     in case edgeValue of
+          GeometricCellEdge edge ->
+            let (forward, backward) = directedPair edge
+             in (selectedFace (incidenceIncidentFace incidence forward), selectedFace (incidenceIncidentFace incidence backward))
+          FaceBoundaryCut cut ->
+            let owner = selectedFace (faceCutFace cut)
+             in (owner, owner)
+
+instance ValidateComplex2D DCELComplex where
+  type ValidationIssue DCELComplex = DCELError
+  validateComplex _ = []
+
+edgeContainsVertex ::
+  PlanarIncidence ->
+  VertexId ->
+  UndirectedEdgeId ->
+  Bool
+edgeContainsVertex incidence vertex edge =
+  let (sourceVertex, targetVertex) = incidenceUndirectedEndpoints incidence edge
+   in vertex == sourceVertex || vertex == targetVertex
diff --git a/src-cell-complex/Moonlight/Planar/LabelledComplex.hs b/src-cell-complex/Moonlight/Planar/LabelledComplex.hs
new file mode 100644
--- /dev/null
+++ b/src-cell-complex/Moonlight/Planar/LabelledComplex.hs
@@ -0,0 +1,131 @@
+-- | Homology interpretations of canonical labelled planar complexes.
+--
+-- 'PlanarComplex' owns simplex identity and downward closure. This module
+-- materializes that admitted section once; Homology remains the sole owner of
+-- chain-law and filtration-law validation.
+module Moonlight.Planar.LabelledComplex
+  ( PlanarComplexInterpretationError (..)
+  , finitePlanarComplex
+  , filteredPlanarComplex
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Vector qualified as Vector
+import Moonlight.Homology.Boundary
+  ( BoundaryIncidence
+  , FiniteChainComplex
+  , emptyBoundaryIncidence
+  , emptyBoundaryIncidenceOf
+  , materializeIncidenceBoundary
+  , mkFiniteChainComplexChecked
+  )
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..)
+  , HomologyFailure
+  )
+import Moonlight.Homology.Persistence
+  ( FilteredFiniteChainComplex
+  , mkFilteredFiniteChainComplex
+  )
+import Moonlight.Homology.Topology (BasisCellRef (..))
+import Moonlight.Planar.Simplex
+  ( PlanarComplex
+  , PlanarSimplex
+  , SimplexDimension (..)
+  , planarComplexBasisAt
+  , planarSimplexBoundary
+  )
+
+-- | Typed obstructions at the labelled-complex/Homology boundary.
+data PlanarComplexInterpretationError label
+  = PlanarComplexChainInvalid !HomologyFailure
+  | PlanarComplexBirthUnexpected !(PlanarSimplex label)
+  | PlanarComplexFiltrationInvalid !HomologyFailure
+  deriving stock (Eq, Show)
+
+-- | Canonical integral chains in ascending labelled-simplex order.
+finitePlanarComplex
+  :: Ord label
+  => PlanarComplex label
+  -> Either (PlanarComplexInterpretationError label) (FiniteChainComplex Int)
+finitePlanarComplex complexValue = do
+  degreeOneBoundary <-
+    first PlanarComplexChainInvalid
+      ( materializeIncidenceBoundary
+          planarSimplexBoundary
+          (basisList SimplexDimension1 complexValue)
+          (basisList SimplexDimension0 complexValue)
+      )
+  degreeTwoBoundary <-
+    first PlanarComplexChainInvalid
+      ( materializeIncidenceBoundary
+          planarSimplexBoundary
+          (basisList SimplexDimension2 complexValue)
+          (basisList SimplexDimension1 complexValue)
+      )
+  let degreeZeroBoundary :: BoundaryIncidence Int
+      degreeZeroBoundary =
+        emptyBoundaryIncidenceOf
+          (fromIntegral (Vector.length (planarComplexBasisAt SimplexDimension0 complexValue)))
+          0
+      boundaryAt (HomologicalDegree degreeIndex) =
+        case degreeIndex of
+          0 -> degreeZeroBoundary
+          1 -> degreeOneBoundary
+          2 -> degreeTwoBoundary
+          _ -> emptyBoundaryIncidence
+  first PlanarComplexChainInvalid
+    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
+
+-- | Add exact births to the admitted complex. Homology validates uniqueness,
+-- total coverage, and every boundary inequality; this interpreter deliberately
+-- does not duplicate those laws.
+filteredPlanarComplex
+  :: (Ord label, Ord filtration)
+  => PlanarComplex label
+  -> Map (PlanarSimplex label) filtration
+  -> Either
+      (PlanarComplexInterpretationError label)
+      (FilteredFiniteChainComplex filtration Int)
+filteredPlanarComplex complexValue births = do
+  finite <- finitePlanarComplex complexValue
+  assignments <- traverse birthAssignment (Map.toAscList births)
+  first PlanarComplexFiltrationInvalid
+    (mkFilteredFiniteChainComplex finite assignments)
+ where
+  basisReferences = planarBasisReferences complexValue
+  birthAssignment (simplex, birthValue) =
+    maybe
+      (Left (PlanarComplexBirthUnexpected simplex))
+      (Right . (,birthValue))
+      (Map.lookup simplex basisReferences)
+
+planarBasisReferences
+  :: Ord label
+  => PlanarComplex label
+  -> Map (PlanarSimplex label) BasisCellRef
+planarBasisReferences complexValue =
+  Map.unions
+    [ referencesAt 0 SimplexDimension0
+    , referencesAt 1 SimplexDimension1
+    , referencesAt 2 SimplexDimension2
+    ]
+ where
+  referencesAt degreeValue dimension =
+    Map.fromDistinctAscList
+      ( zipWith
+          (\basisIndex simplex ->
+              (simplex, BasisCellRef (HomologicalDegree degreeValue) basisIndex)
+          )
+          [0 ..]
+          (basisList dimension complexValue)
+      )
+
+basisList
+  :: SimplexDimension
+  -> PlanarComplex label
+  -> [PlanarSimplex label]
+basisList dimension = Vector.toList . planarComplexBasisAt dimension
diff --git a/src-cell-complex/Moonlight/Triangulation/CellComplex.hs b/src-cell-complex/Moonlight/Triangulation/CellComplex.hs
deleted file mode 100644
--- a/src-cell-complex/Moonlight/Triangulation/CellComplex.hs
+++ /dev/null
@@ -1,495 +0,0 @@
-{-# LANGUAGE EmptyDataDeriving #-}
-
--- | The admitted Moonlight triangulation cell section as a generic
--- 'CellComplex2D', an integral cellular chain complex, and an exact filtered
--- alpha complex. The 'ExactCellSet' remains the semantic owner: this module
--- supplies only the incidence and Homology interpretations required by
--- downstream topology.
-module Moonlight.Triangulation.CellComplex
-  ( DCELComplex,
-    DCELError (..),
-    fromExactCellSet,
-    finiteChainComplex,
-    filteredAlphaComplex,
-  )
-where
-
-import Data.Bifunctor (first)
-import Data.IntMap.Strict qualified as IntMap
-import Data.IntSet qualified as IntSet
-import Data.List qualified as List
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Vector qualified as Vector
-import Moonlight.Algebra.Pure.Orientation (Orientation (..))
-import Moonlight.Homology.Boundary
-  ( BoundaryIncidence
-  , BoundaryIncidenceShapeError
-  , BoundaryEntry
-  , FiniteChainComplex
-  , emptyBoundaryIncidence
-  , emptyBoundaryIncidenceOf
-  , mkBoundaryEntryFromInts
-  , mkBoundaryIncidenceFromOrderedColumns
-  , mkBoundaryIncidenceFromOrderedEntries
-  , mkFiniteChainComplexChecked
-  , targetIndex
-  )
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..)
-  , HomologyFailure
-  )
-import Moonlight.Homology.Persistence
-  ( FilteredFiniteChainComplex
-  , mkFilteredFiniteChainComplex
-  )
-import Moonlight.Homology.Pure.Topology.CellComplex
-  ( CellComplex2D (..)
-  , CellRef (..)
-  , CellTypes (..)
-  , OrientedEdge (..)
-  , ValidateComplex2D (..)
-  )
-import Moonlight.Homology.Topology (BasisCellRef (..))
-import Moonlight.Triangulation.Alpha
-  ( AlphaBirth
-  , AlphaFiltration
-  , alphaEdgeBirth
-  , alphaFaceBirth
-  , alphaFiltrationCellSet
-  , alphaVertexBirth
-  )
-import Moonlight.Triangulation.Dcel qualified as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId,
-    FaceId (..),
-    UndirectedEdgeId (..),
-    VertexId (..),
-    asUndirected,
-    directedPair,
-    isNormalized,
-  )
-import Moonlight.Triangulation.Internal.CellSet (ExactCellSet (..))
-import Moonlight.Triangulation.Types (Triangulation)
-
--- | An incidence view of one already-validated, downward-closed exact cell
--- selection. It deliberately has no independent cell inventory.
-newtype DCELComplex = DCELComplex ExactCellSet
-
--- | 'ExactCellSet' construction discharges every closure and handle
--- obligation before this view exists. Incidence materialization, Homology's
--- independent chain-law seal, and the exact-birth join retain typed
--- obstructions at their respective boundaries.
-type DCELCellRef = CellRef VertexId UndirectedEdgeId FaceId
-
-data DCELError
-  = DCELBoundaryCellMissing !DCELCellRef !DCELCellRef
-  | DCELBoundaryIncidenceInvalid !BoundaryIncidenceShapeError
-  | DCELChainComplexInvalid !HomologyFailure
-  | DCELAlphaBirthMissing !DCELCellRef
-  | DCELFilteredComplexInvalid !HomologyFailure
-  deriving stock (Eq, Show)
-
-fromExactCellSet :: ExactCellSet -> DCELComplex
-fromExactCellSet = DCELComplex
-
--- | Canonical integral cellular chains in ascending resident-handle order.
--- Degree one uses target minus source; degree two uses the DCEL's oriented
--- face boundary. The Homology boundary seals the result only after checking
--- shape and @d . d = 0@; no unchecked chain constructor crosses the public
--- package boundary.
-finiteChainComplex :: DCELComplex -> Either DCELError (FiniteChainComplex Int)
-finiteChainComplex complexValue =
-  let basis = dcelBasis complexValue
-   in finiteChainComplexWithBasis complexValue basis
-
--- | Lower the exact alpha section into Homology without converting its birth
--- order through binary64. Persistence remains wholly owned by Homology.
-filteredAlphaComplex
-  :: AlphaFiltration
-  -> Either DCELError (FilteredFiniteChainComplex AlphaBirth Int)
-filteredAlphaComplex filtration =
-  case alphaFiltrationCellSet filtration of
-    ExactCellSet triangulation _ _ _ -> do
-      finite <- residentAlphaFiniteChainComplex triangulation
-      vertexBirthAssignments <-
-        traverse
-          ( residentAlphaBirthAssignment
-              CellVertexRef
-              residentVertexBasisRef
-              (alphaVertexBirth filtration)
-          )
-          (vertexHandlesOf triangulation)
-      edgeBirthAssignments <-
-        traverse
-          ( residentAlphaBirthAssignment
-              CellEdgeRef
-              residentEdgeBasisRef
-              (alphaEdgeBirth filtration)
-          )
-          (undirectedEdgesOf triangulation)
-      faceBirthAssignments <-
-        traverse
-          ( residentAlphaBirthAssignment
-              CellFaceRef
-              residentFaceBasisRef
-              (alphaFaceBirth filtration)
-          )
-          (innerFacesOf triangulation)
-      first DCELFilteredComplexInvalid
-        ( mkFilteredFiniteChainComplex
-            finite
-            (vertexBirthAssignments <> edgeBirthAssignments <> faceBirthAssignments)
-        )
-
--- | The opaque 'AlphaFiltration' constructor admits the entire resident DCEL,
--- whose handle ranges are dense. This local section therefore lowers those
--- handles directly to basis indices while retaining Homology's independent
--- shape and nilpotence seal. Sparse 'ExactCellSet' values continue through the
--- generic map-indexed 'finiteChainComplex' path.
-residentAlphaFiniteChainComplex
-  :: Triangulation mode vertex directed undirected face
-  -> Either DCELError (FiniteChainComplex Int)
-residentAlphaFiniteChainComplex triangulation = do
-  let degreeOneColumns =
-        Vector.generate
-          (Dcel.numUndirectedEdges triangulation)
-          (residentEdgeBoundaryEntries triangulation . UndirectedEdgeId . fromIntegral)
-      degreeTwoColumns =
-        Vector.imap
-          residentFaceBoundaryEntries
-          (Dcel.innerFaceDirectedEdgeTriples triangulation)
-  degreeOneBoundary <-
-    first DCELBoundaryIncidenceInvalid
-      ( mkBoundaryIncidenceFromOrderedColumns
-          (fromIntegral (Dcel.numUndirectedEdges triangulation))
-          (fromIntegral (Dcel.numVertices triangulation))
-          degreeOneColumns
-      )
-  degreeTwoBoundary <-
-    first DCELBoundaryIncidenceInvalid
-      ( mkBoundaryIncidenceFromOrderedColumns
-          (fromIntegral (Dcel.numInnerFaces triangulation))
-          (fromIntegral (Dcel.numUndirectedEdges triangulation))
-          degreeTwoColumns
-      )
-  let degreeZeroBoundary =
-        emptyBoundaryIncidenceOf
-          (fromIntegral (Dcel.numVertices triangulation))
-          0
-      boundaryAt (HomologicalDegree degreeValue) =
-        case degreeValue of
-          0 -> degreeZeroBoundary
-          1 -> degreeOneBoundary
-          2 -> degreeTwoBoundary
-          _ -> emptyBoundaryIncidence
-  first DCELChainComplexInvalid
-    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
-
-residentEdgeBoundaryEntries
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> [BoundaryEntry Int]
-residentEdgeBoundaryEntries triangulation edgeValue =
-  let (sourceVertex, targetVertex) = Dcel.undirectedEndpoints triangulation edgeValue
-      sourceEntry =
-        mkBoundaryEntryFromInts
-          (basisIndexOfEdge edgeValue)
-          (basisIndexOfVertex sourceVertex)
-          (-1)
-      targetEntry =
-        mkBoundaryEntryFromInts
-          (basisIndexOfEdge edgeValue)
-          (basisIndexOfVertex targetVertex)
-          1
-   in if targetIndex sourceEntry <= targetIndex targetEntry
-        then [sourceEntry, targetEntry]
-        else [targetEntry, sourceEntry]
-
-residentFaceBoundaryEntries
-  :: Int
-  -> (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
-  -> [BoundaryEntry Int]
-residentFaceBoundaryEntries faceIndexValue (firstEdge, secondEdge, thirdEdge) =
-  sortThreeBoundaryEntries
-    (residentFaceBoundaryEntry faceIndexValue firstEdge)
-    (residentFaceBoundaryEntry faceIndexValue secondEdge)
-    (residentFaceBoundaryEntry faceIndexValue thirdEdge)
-
-sortThreeBoundaryEntries
-  :: BoundaryEntry Int
-  -> BoundaryEntry Int
-  -> BoundaryEntry Int
-  -> [BoundaryEntry Int]
-sortThreeBoundaryEntries firstEntry secondEntry thirdEntry =
-  let (firstLow, firstHigh) = orderedBoundaryPair firstEntry secondEntry
-      (secondLow, finalHigh) = orderedBoundaryPair firstHigh thirdEntry
-      (finalLow, finalMiddle) = orderedBoundaryPair firstLow secondLow
-   in [finalLow, finalMiddle, finalHigh]
-
-orderedBoundaryPair
-  :: BoundaryEntry Int
-  -> BoundaryEntry Int
-  -> (BoundaryEntry Int, BoundaryEntry Int)
-orderedBoundaryPair firstEntry secondEntry =
-  if targetIndex firstEntry <= targetIndex secondEntry
-    then (firstEntry, secondEntry)
-    else (secondEntry, firstEntry)
-
-residentFaceBoundaryEntry :: Int -> DirectedEdgeId -> BoundaryEntry Int
-residentFaceBoundaryEntry faceIndexValue directedEdge =
-  mkBoundaryEntryFromInts
-    faceIndexValue
-    (basisIndexOfEdge (asUndirected directedEdge))
-    (if isNormalized directedEdge then 1 else -1)
-
-residentAlphaBirthAssignment
-  :: (cell -> DCELCellRef)
-  -> (cell -> BasisCellRef)
-  -> (cell -> Maybe AlphaBirth)
-  -> cell
-  -> Either DCELError (BasisCellRef, AlphaBirth)
-residentAlphaBirthAssignment cellReference basisReference birthAt cell =
-  maybe
-    (Left (DCELAlphaBirthMissing (cellReference cell)))
-    (Right . (,) (basisReference cell))
-    (birthAt cell)
-
-residentVertexBasisRef :: VertexId -> BasisCellRef
-residentVertexBasisRef vertexValue =
-  BasisCellRef (HomologicalDegree 0) (basisIndexOfVertex vertexValue)
-
-residentEdgeBasisRef :: UndirectedEdgeId -> BasisCellRef
-residentEdgeBasisRef edgeValue =
-  BasisCellRef (HomologicalDegree 1) (basisIndexOfEdge edgeValue)
-
-residentFaceBasisRef :: FaceId -> BasisCellRef
-residentFaceBasisRef faceValue =
-  BasisCellRef (HomologicalDegree 2) (basisIndexOfFace faceValue)
-
-basisIndexOfVertex :: VertexId -> Int
-basisIndexOfVertex (VertexId rawVertex) = fromIntegral rawVertex
-
-basisIndexOfEdge :: UndirectedEdgeId -> Int
-basisIndexOfEdge (UndirectedEdgeId rawEdge) = fromIntegral rawEdge
-
-basisIndexOfFace :: FaceId -> Int
-basisIndexOfFace (FaceId rawFace) = fromIntegral rawFace - 1
-
-undirectedEdgesOf
-  :: Triangulation mode vertex directed undirected face
-  -> [UndirectedEdgeId]
-undirectedEdgesOf triangulation =
-  fmap (UndirectedEdgeId . fromIntegral) [0 .. Dcel.numUndirectedEdges triangulation - 1]
-
-vertexHandlesOf
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexId]
-vertexHandlesOf triangulation =
-  fmap (VertexId . fromIntegral) [0 .. Dcel.numVertices triangulation - 1]
-
-innerFacesOf
-  :: Triangulation mode vertex directed undirected face
-  -> [FaceId]
-innerFacesOf triangulation =
-  fmap (FaceId . fromIntegral) [1 .. Dcel.numFaces triangulation - 1]
-
-data DCELBasis = DCELBasis
-  { dcelVertexBasis :: !(Map VertexId BasisCellRef)
-  , dcelEdgeBasis :: !(Map UndirectedEdgeId BasisCellRef)
-  , dcelFaceBasis :: !(Map FaceId BasisCellRef)
-  }
-
-dcelBasis :: DCELComplex -> DCELBasis
-dcelBasis complexValue =
-  DCELBasis
-    { dcelVertexBasis = basisMap 0 (vertices complexValue)
-    , dcelEdgeBasis = basisMap 1 (edges complexValue)
-    , dcelFaceBasis = basisMap 2 (faces complexValue)
-    }
-
-basisMap :: Ord cell => Int -> [cell] -> Map cell BasisCellRef
-basisMap degreeValue cells =
-  Map.fromAscList
-    ( zipWith
-        (\indexValue cell -> (cell, BasisCellRef (HomologicalDegree degreeValue) indexValue))
-        [0 ..]
-        cells
-    )
-
-finiteChainComplexWithBasis
-  :: DCELComplex
-  -> DCELBasis
-  -> Either DCELError (FiniteChainComplex Int)
-finiteChainComplexWithBasis complexValue basis = do
-  degreeOneEntries <-
-    concat
-      <$> traverse
-        (edgeBoundaryEntries complexValue basis)
-        (Map.toAscList (dcelEdgeBasis basis))
-  degreeTwoEntries <-
-    concat
-      <$> traverse
-        (faceBoundaryEntries complexValue basis)
-        (Map.toAscList (dcelFaceBasis basis))
-  degreeOneBoundary <-
-    first DCELBoundaryIncidenceInvalid
-      ( mkBoundaryIncidenceFromOrderedEntries
-          (fromIntegral (Map.size (dcelEdgeBasis basis)))
-          (fromIntegral (Map.size (dcelVertexBasis basis)))
-          degreeOneEntries
-      )
-  degreeTwoBoundary <-
-    first DCELBoundaryIncidenceInvalid
-      ( mkBoundaryIncidenceFromOrderedEntries
-          (fromIntegral (Map.size (dcelFaceBasis basis)))
-          (fromIntegral (Map.size (dcelEdgeBasis basis)))
-          degreeTwoEntries
-      )
-  let degreeZeroBoundary =
-        emptyBoundaryIncidenceOf
-          (fromIntegral (Map.size (dcelVertexBasis basis)))
-          0
-      boundaryAt :: HomologicalDegree -> BoundaryIncidence Int
-      boundaryAt (HomologicalDegree degreeValue) =
-        case degreeValue of
-          0 -> degreeZeroBoundary
-          1 -> degreeOneBoundary
-          2 -> degreeTwoBoundary
-          _ -> emptyBoundaryIncidence
-  first DCELChainComplexInvalid
-    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
-
-edgeBoundaryEntries
-  :: DCELComplex
-  -> DCELBasis
-  -> (UndirectedEdgeId, BasisCellRef)
-  -> Either DCELError [BoundaryEntry Int]
-edgeBoundaryEntries complexValue basis (edgeValue, edgeBasisRef) = do
-  let (sourceVertex, targetVertex) = edgeBoundary complexValue edgeValue
-      sourceCell = CellEdgeRef edgeValue
-  sourceBasisRef <-
-    requireBoundaryCell
-      sourceCell
-      (CellVertexRef sourceVertex)
-      sourceVertex
-      (dcelVertexBasis basis)
-  targetBasisRef <-
-    requireBoundaryCell
-      sourceCell
-      (CellVertexRef targetVertex)
-      targetVertex
-      (dcelVertexBasis basis)
-  pure
-    ( List.sortOn
-        targetIndex
-        [ mkBoundaryEntryFromInts
-            (cellIndex edgeBasisRef)
-            (cellIndex sourceBasisRef)
-            (-1)
-        , mkBoundaryEntryFromInts
-            (cellIndex edgeBasisRef)
-            (cellIndex targetBasisRef)
-            1
-        ]
-    )
-
-faceBoundaryEntries
-  :: DCELComplex
-  -> DCELBasis
-  -> (FaceId, BasisCellRef)
-  -> Either DCELError [BoundaryEntry Int]
-faceBoundaryEntries complexValue basis (faceValue, faceBasisRef) =
-  List.sortOn targetIndex
-    <$> traverse
-      boundaryEntry
-      (faceBoundary complexValue faceValue)
- where
-  boundaryEntry orientedBoundary = do
-    let edgeValue = orientedEdge orientedBoundary
-    edgeBasisRef <-
-      requireBoundaryCell
-        (CellFaceRef faceValue)
-        (CellEdgeRef edgeValue)
-        edgeValue
-        (dcelEdgeBasis basis)
-    pure
-      ( mkBoundaryEntryFromInts
-          (cellIndex faceBasisRef)
-          (cellIndex edgeBasisRef)
-          (orientationCoefficient (edgeOrientation orientedBoundary))
-      )
-
-orientationCoefficient :: Orientation -> Int
-orientationCoefficient orientation =
-  case orientation of
-    Positive -> 1
-    Negative -> -1
-
-requireBoundaryCell
-  :: Ord cell
-  => DCELCellRef
-  -> DCELCellRef
-  -> cell
-  -> Map cell BasisCellRef
-  -> Either DCELError BasisCellRef
-requireBoundaryCell sourceCell targetCell cell basis =
-  maybe
-    (Left (DCELBoundaryCellMissing sourceCell targetCell))
-    Right
-    (Map.lookup cell basis)
-
-instance CellTypes DCELComplex where
-  type Vertex DCELComplex = VertexId
-  type Edge DCELComplex = UndirectedEdgeId
-  type Face DCELComplex = FaceId
-
-instance CellComplex2D DCELComplex where
-  vertices (DCELComplex (ExactCellSet _ selectedVertices _ _)) =
-    fmap (VertexId . fromIntegral) (IntMap.keys selectedVertices)
-
-  edges (DCELComplex (ExactCellSet _ _ selectedEdges _)) =
-    fmap (UndirectedEdgeId . fromIntegral) (IntSet.toAscList selectedEdges)
-
-  faces (DCELComplex (ExactCellSet _ _ _ selectedFaces)) =
-    fmap (FaceId . fromIntegral) (IntSet.toAscList selectedFaces)
-
-  edgeBoundary (DCELComplex (ExactCellSet triangulation _ _ _)) =
-    Dcel.undirectedEndpoints triangulation
-
-  faceBoundary (DCELComplex (ExactCellSet triangulation _ _ _)) face =
-    fmap orientedBoundaryEdge (Dcel.faceDirectedEdges triangulation face)
-    where
-      orientedBoundaryEdge directedEdge =
-        OrientedEdge
-          { orientedEdge = asUndirected directedEdge,
-            edgeOrientation =
-              if isNormalized directedEdge
-                then Positive
-                else Negative
-          }
-
-  edgesAtVertex complexValue@(DCELComplex (ExactCellSet triangulation _ _ _)) vertex =
-    filter (edgeContainsVertex triangulation vertex) (edges complexValue)
-
-  facesAtEdge (DCELComplex (ExactCellSet triangulation _ _ selectedFaces)) edge =
-    let (forward, backward) = directedPair edge
-        selectedIncidentFace directedEdge =
-          let face@(FaceId rawFace) = Dcel.incidentFace triangulation directedEdge
-           in if IntSet.member (fromIntegral rawFace) selectedFaces
-                then Just face
-                else Nothing
-     in (selectedIncidentFace forward, selectedIncidentFace backward)
-
-instance ValidateComplex2D DCELComplex where
-  type ValidationIssue DCELComplex = DCELError
-  validateComplex _ = []
-
-edgeContainsVertex ::
-  Triangulation mode vertex directed undirected face ->
-  VertexId ->
-  UndirectedEdgeId ->
-  Bool
-edgeContainsVertex triangulation vertex edge =
-  let (sourceVertex, targetVertex) = Dcel.undirectedEndpoints triangulation edge
-   in vertex == sourceVertex || vertex == targetVertex
diff --git a/src-cell-complex/Moonlight/Triangulation/LabelledComplex.hs b/src-cell-complex/Moonlight/Triangulation/LabelledComplex.hs
deleted file mode 100644
--- a/src-cell-complex/Moonlight/Triangulation/LabelledComplex.hs
+++ /dev/null
@@ -1,131 +0,0 @@
--- | Homology interpretations of canonical labelled planar complexes.
---
--- 'PlanarComplex' owns simplex identity and downward closure. This module
--- materializes that admitted section once; Homology remains the sole owner of
--- chain-law and filtration-law validation.
-module Moonlight.Triangulation.LabelledComplex
-  ( PlanarComplexInterpretationError (..)
-  , finitePlanarComplex
-  , filteredPlanarComplex
-  )
-where
-
-import Data.Bifunctor (first)
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Vector qualified as Vector
-import Moonlight.Homology.Boundary
-  ( BoundaryIncidence
-  , FiniteChainComplex
-  , emptyBoundaryIncidence
-  , emptyBoundaryIncidenceOf
-  , materializeIncidenceBoundary
-  , mkFiniteChainComplexChecked
-  )
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..)
-  , HomologyFailure
-  )
-import Moonlight.Homology.Persistence
-  ( FilteredFiniteChainComplex
-  , mkFilteredFiniteChainComplex
-  )
-import Moonlight.Homology.Topology (BasisCellRef (..))
-import Moonlight.Triangulation.Simplex
-  ( PlanarComplex
-  , PlanarSimplex
-  , SimplexDimension (..)
-  , planarComplexBasisAt
-  , planarSimplexBoundary
-  )
-
--- | Typed obstructions at the labelled-complex/Homology boundary.
-data PlanarComplexInterpretationError label
-  = PlanarComplexChainInvalid !HomologyFailure
-  | PlanarComplexBirthUnexpected !(PlanarSimplex label)
-  | PlanarComplexFiltrationInvalid !HomologyFailure
-  deriving stock (Eq, Show)
-
--- | Canonical integral chains in ascending labelled-simplex order.
-finitePlanarComplex
-  :: Ord label
-  => PlanarComplex label
-  -> Either (PlanarComplexInterpretationError label) (FiniteChainComplex Int)
-finitePlanarComplex complexValue = do
-  degreeOneBoundary <-
-    first PlanarComplexChainInvalid
-      ( materializeIncidenceBoundary
-          planarSimplexBoundary
-          (basisList SimplexDimension1 complexValue)
-          (basisList SimplexDimension0 complexValue)
-      )
-  degreeTwoBoundary <-
-    first PlanarComplexChainInvalid
-      ( materializeIncidenceBoundary
-          planarSimplexBoundary
-          (basisList SimplexDimension2 complexValue)
-          (basisList SimplexDimension1 complexValue)
-      )
-  let degreeZeroBoundary :: BoundaryIncidence Int
-      degreeZeroBoundary =
-        emptyBoundaryIncidenceOf
-          (fromIntegral (Vector.length (planarComplexBasisAt SimplexDimension0 complexValue)))
-          0
-      boundaryAt (HomologicalDegree degreeIndex) =
-        case degreeIndex of
-          0 -> degreeZeroBoundary
-          1 -> degreeOneBoundary
-          2 -> degreeTwoBoundary
-          _ -> emptyBoundaryIncidence
-  first PlanarComplexChainInvalid
-    (mkFiniteChainComplexChecked (HomologicalDegree 2) boundaryAt)
-
--- | Add exact births to the admitted complex. Homology validates uniqueness,
--- total coverage, and every boundary inequality; this interpreter deliberately
--- does not duplicate those laws.
-filteredPlanarComplex
-  :: (Ord label, Ord filtration)
-  => PlanarComplex label
-  -> Map (PlanarSimplex label) filtration
-  -> Either
-      (PlanarComplexInterpretationError label)
-      (FilteredFiniteChainComplex filtration Int)
-filteredPlanarComplex complexValue births = do
-  finite <- finitePlanarComplex complexValue
-  assignments <- traverse birthAssignment (Map.toAscList births)
-  first PlanarComplexFiltrationInvalid
-    (mkFilteredFiniteChainComplex finite assignments)
- where
-  basisReferences = planarBasisReferences complexValue
-  birthAssignment (simplex, birthValue) =
-    maybe
-      (Left (PlanarComplexBirthUnexpected simplex))
-      (Right . (,birthValue))
-      (Map.lookup simplex basisReferences)
-
-planarBasisReferences
-  :: Ord label
-  => PlanarComplex label
-  -> Map (PlanarSimplex label) BasisCellRef
-planarBasisReferences complexValue =
-  Map.unions
-    [ referencesAt 0 SimplexDimension0
-    , referencesAt 1 SimplexDimension1
-    , referencesAt 2 SimplexDimension2
-    ]
- where
-  referencesAt degreeValue dimension =
-    Map.fromDistinctAscList
-      ( zipWith
-          (\basisIndex simplex ->
-              (simplex, BasisCellRef (HomologicalDegree degreeValue) basisIndex)
-          )
-          [0 ..]
-          (basisList dimension complexValue)
-      )
-
-basisList
-  :: SimplexDimension
-  -> PlanarComplex label
-  -> [PlanarSimplex label]
-basisList dimension = Vector.toList . planarComplexBasisAt dimension
diff --git a/src-core/Moonlight/Planar/BuildStats.hs b/src-core/Moonlight/Planar/BuildStats.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/BuildStats.hs
@@ -0,0 +1,11 @@
+-- | Typed observations of completed construction work.
+module Moonlight.Planar.BuildStats
+  ( BuildMetric (..)
+  , BuildStats
+  , buildStat
+  , tabulateBuildStats
+  , emptyBuildStats
+  , updateBuildStats
+  ) where
+
+import Moonlight.Planar.Internal.BuildStats
diff --git a/src-core/Moonlight/Planar/Internal/BoxedPaged.hs b/src-core/Moonlight/Planar/Internal/BoxedPaged.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/BoxedPaged.hs
@@ -0,0 +1,531 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE RoleAnnotations #-}
+{-# LANGUAGE StandaloneDeriving #-}
+
+module Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedPaged
+  , MutableBoxedPaged
+  , FillRequirement (..)
+  , BoxedFill (..)
+  , BoxedStorageError (..)
+  , emptyBoxedPaged
+  , boxedFill
+  , requiredBoxedFill
+  , boxedDefaulted
+  , boxedMaterializedPageCount
+  , boxedPagedLength
+  , boxedUnsafeIndex
+  , boxedFromVector
+  , boxedToVector
+  , boxedFoldr
+  , thawBoxedPaged
+  , readMutableBoxedPublicationStats
+  , readBoxedPaged
+  , writeBoxedPaged
+  , resetBoxedRange
+  , boxedThawPristine
+  , freezeBoxedPaged
+  , boxedUpdate
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.Bits (shiftL, shiftR, (.&.))
+import Data.Foldable (traverse_)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.List as List
+import Data.STRef
+import qualified Data.Vector as V
+import qualified Data.Vector.Mutable as MV
+import Moonlight.Planar.Internal.PageDirectory
+import Moonlight.Planar.Internal.Paged
+  ( PublicationStats (..)
+  , TransactionShape (..)
+  , addPublicationStats
+  , emptyPublicationStats
+  )
+
+import GHC.Generics (Generic)
+
+-- | A paged payload store in which an absent page means every slot in it holds
+-- the fill. The fill belongs to the store rather than to a thaw of it: the
+-- three element planes require a default for future and recycled slots. Vertex
+-- fills are optional: authored dense vertices arrive with their payloads,
+-- while geometry-only publication supplies a fill without materialized pages.
+--
+-- The near-universal instantiation is @() () ()@, and under an unconditional
+-- representation that costs one pointer per directed edge, undirected edge and
+-- face, all aimed at the same closure. Here it costs nothing: an element
+-- created at its default writes no slot, so no page is ever materialized.
+data FillRequirement = OptionalFill | RequiredFill
+
+-- | Construction/observation of a fill, indexed by its obligation. Absence
+-- cannot be submitted to a required plane. The store remains the sole owner.
+type role BoxedFill nominal representational
+data BoxedFill (requirement :: FillRequirement) a where
+  NoFill :: BoxedFill 'OptionalFill a
+  Fill :: a -> BoxedFill requirement a
+
+deriving stock instance Eq a => Eq (BoxedFill requirement a)
+deriving stock instance Show a => Show (BoxedFill requirement a)
+deriving stock instance Functor (BoxedFill requirement)
+deriving stock instance Foldable (BoxedFill requirement)
+deriving stock instance Traversable (BoxedFill requirement)
+
+type role BoxedStorage nominal representational
+data BoxedStorage (requirement :: FillRequirement) a where
+  DenseBoxedPages :: !(V.Vector (V.Vector a)) -> BoxedStorage 'OptionalFill a
+  DefaultedBoxedPages :: !a -> !(PageDirectory (V.Vector a)) -> BoxedStorage requirement a
+
+instance Functor (BoxedStorage requirement) where
+  fmap f storage = case storage of
+    DenseBoxedPages pages -> DenseBoxedPages (fmap (fmap f) pages)
+    DefaultedBoxedPages fill pages -> DefaultedBoxedPages (f fill) (fmap (fmap f) pages)
+
+instance NFData a => NFData (BoxedStorage requirement a) where
+  rnf storage = case storage of
+    DenseBoxedPages pages -> rnf pages
+    DefaultedBoxedPages fill pages -> rnf fill `seq` rnf pages
+
+type role BoxedPaged nominal representational
+data BoxedPaged (requirement :: FillRequirement) a = BoxedPaged
+  { boxedLength :: {-# UNPACK #-} !Int
+  , boxedStorage :: !(BoxedStorage requirement a)
+  }
+  deriving stock (Functor)
+
+instance NFData a => NFData (BoxedPaged requirement a) where
+  rnf (BoxedPaged count storage) = rnf count `seq` rnf storage
+
+-- Logical residents in index order, then the fill once. Sparse fmap stays
+-- specialized; traversal must distinguish effects at every logical slot.
+instance Foldable (BoxedPaged requirement) where
+  foldr step initial store = boxedFoldr step (foldr step initial (boxedFill store)) store
+
+instance Traversable (BoxedPaged requirement) where
+  traverse visit store =
+    flip boxedFromVector <$> traverse visit (boxedToVector store) <*> traverse visit (boxedFill store)
+
+data BoxedStorageError
+  = BoxedFreezeLengthNegative {-# UNPACK #-} !Int
+  | BoxedFreezeDensePageMissing {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+boxedFill :: BoxedPaged requirement a -> BoxedFill requirement a
+boxedFill BoxedPaged{boxedStorage = DenseBoxedPages _} = NoFill
+boxedFill BoxedPaged{boxedStorage = DefaultedBoxedPages fill _} = Fill fill
+{-# INLINE boxedFill #-}
+
+-- | Total: a required-fill store has no dense/no-fill constructor.
+requiredBoxedFill :: BoxedPaged 'RequiredFill a -> a
+requiredBoxedFill BoxedPaged{boxedStorage = DefaultedBoxedPages fill _} = fill
+{-# INLINE requiredBoxedFill #-}
+
+-- | Number of materialized payload pages.  Defaulted stores report only their
+-- sparse directory; an absent page is the fill and was never copied.
+boxedMaterializedPageCount :: BoxedPaged requirement a -> Int
+boxedMaterializedPageCount BoxedPaged{boxedStorage} = case boxedStorage of
+  DenseBoxedPages pages -> V.length pages
+  DefaultedBoxedPages _ pages -> directorySize pages
+{-# INLINE boxedMaterializedPageCount #-}
+
+-- | A length-bearing payload section whose every slot is its default.  The
+-- section deliberately owns no materialized pages; a geometry-only
+-- publication can therefore carry an arbitrarily large unit payload plane
+-- without forcing a dense boxed vector at its next freeze.
+boxedDefaulted :: a -> Int -> BoxedPaged requirement a
+boxedDefaulted fill length' =
+  BoxedPaged
+    { boxedLength = max 0 length'
+    , boxedStorage = DefaultedBoxedPages fill emptyDirectory
+    }
+
+-- Structural equality would distinguish a store that materialized a page of
+-- defaults from one that did not, and the two are the same store. Equality is
+-- therefore what can be observed: the length, the fill every unwritten slot
+-- will report, and the sequence itself.
+instance Eq a => Eq (BoxedPaged requirement a) where
+  left == right =
+    boxedLength left == boxedLength right
+      && boxedFill left == boxedFill right
+      && boxedToVector left == boxedToVector right
+
+instance Show a => Show (BoxedPaged requirement a) where
+  showsPrec precedence paged =
+    showParen (precedence > 10) $
+      showString "BoxedPaged "
+        . showsPrec 11 (boxedFill paged)
+        . showString " "
+        . showsPrec 11 (boxedToVector paged)
+
+type role MutableBoxedPaged nominal nominal representational
+data MutableBoxedPaged (requirement :: FillRequirement) s a = MutableBoxedPaged
+  { mutableBoxedBase :: !(BoxedPaged requirement a)
+  , mutableBoxedDirty :: !(STRef s (IntMap.IntMap (MV.MVector s a)))
+  , mutableBoxedStats :: !(Maybe (STRef s PublicationStats))
+  , -- | The thaw found no materialized page. Together with an empty dirty set
+    -- this says every slot in the store reports the fill, which is the state a
+    -- payload component nobody writes stays in for the whole transaction —
+    -- the near-universal @() () ()@ case. Read through 'boxedThawPristine'.
+    mutableBoxedPristineBase :: !Bool
+  }
+
+pageBits :: Int
+pageBits = 8
+{-# INLINE pageBits #-}
+
+pageSize :: Int
+pageSize = 1 `shiftL` pageBits
+{-# INLINE pageSize #-}
+
+-- Every index this store decomposes is an element identifier or a walk over
+-- one, so the signed division a 'quotRem' would emit corrects for a sign that
+-- cannot occur.
+pageOf :: Int -> Int
+pageOf index = index `shiftR` pageBits
+{-# INLINE pageOf #-}
+
+offsetOf :: Int -> Int
+offsetOf index = index .&. (pageSize - 1)
+{-# INLINE offsetOf #-}
+
+emptyBoxedPaged :: BoxedFill requirement a -> BoxedPaged requirement a
+emptyBoxedPaged fill =
+  BoxedPaged
+    0
+    (case fill of
+       NoFill -> DenseBoxedPages V.empty
+       Fill value -> DefaultedBoxedPages value emptyDirectory)
+
+boxedPagedLength :: BoxedPaged requirement a -> Int
+boxedPagedLength = boxedLength
+{-# INLINE boxedPagedLength #-}
+
+-- | Index with a handle already admitted by the owning triangulation. Dense
+-- stores carry every live page explicitly; defaulted stores can answer an
+-- absent or short page from their stored default. The handle domain is owned
+-- by the DCEL, so this raw kernel performs no second, disagreeing bounds check.
+boxedUnsafeIndex :: BoxedPaged requirement a -> Int -> a
+boxedUnsafeIndex BoxedPaged{boxedStorage} index =
+  let !page = pageOf index
+      !offset = offsetOf index
+   in case boxedStorage of
+        DenseBoxedPages pages ->
+          V.unsafeIndex (V.unsafeIndex pages page) offset
+        DefaultedBoxedPages fill pages ->
+          case lookupDirectory page pages of
+            Just values | offset < V.length values -> V.unsafeIndex values offset
+            _ -> fill
+{-# INLINE boxedUnsafeIndex #-}
+
+boxedFromVector :: BoxedFill requirement a -> V.Vector a -> BoxedPaged requirement a
+boxedFromVector fill values =
+  let !chunks =
+        List.unfoldr
+          (\offset ->
+             if offset >= V.length values
+               then Nothing
+               else
+                 let !page = pageOf offset
+                     !count = min pageSize (V.length values - offset)
+                     !chunk = V.slice offset count values
+                  in Just ((page, chunk), offset + count))
+          0
+   in
+  BoxedPaged
+    { boxedLength = V.length values
+    , boxedStorage =
+        case fill of
+          NoFill -> DenseBoxedPages (V.fromList (map snd chunks))
+          Fill value -> DefaultedBoxedPages value (directoryFromAscList chunks)
+    }
+
+-- Resolved a page at a time. Asking the directory per element paid a descent
+-- for every slot of a run the descent had already found.
+boxedToVector :: forall requirement a. BoxedPaged requirement a -> V.Vector a
+boxedToVector BoxedPaged{boxedLength, boxedStorage}
+  | boxedLength <= 0 = V.empty
+  | otherwise = case boxedStorage of
+      DenseBoxedPages pages -> V.take boxedLength (V.concat (V.toList pages))
+      DefaultedBoxedPages fill pages ->
+        V.concat (map (pageRun fill pages) [0 .. pageOf (boxedLength - 1)])
+ where
+  pageRun :: a -> PageDirectory (V.Vector a) -> Int -> V.Vector a
+  pageRun fill pages page =
+    let !base = page `shiftL` pageBits
+        !width = min pageSize (boxedLength - base)
+     in case lookupDirectory page pages of
+          Just values
+            | V.length values >= width -> V.slice 0 width values
+            | otherwise -> values V.++ V.replicate (width - V.length values) fill
+          Nothing -> V.replicate width fill
+
+-- | Consume every logical payload, including absent defaulted runs and the
+-- unwritten suffix of a short page. Neither a dense gather nor a vector of
+-- repeated defaults is needed. Physical pages remain private to storage.
+boxedFoldr :: (a -> b -> b) -> b -> BoxedPaged requirement a -> b
+boxedFoldr step initial BoxedPaged{boxedLength, boxedStorage}
+  | boxedLength <= 0 = initial
+  | otherwise = case boxedStorage of
+      DenseBoxedPages pages ->
+        V.ifoldr
+          (\page values rest -> V.foldr step rest (V.take (pageWidth page) values))
+          initial
+          (V.take pageCount pages)
+      DefaultedBoxedPages fill pages ->
+        foldr
+          (\page rest ->
+             let values = maybe V.empty id (lookupDirectory page pages)
+                 width = pageWidth page
+                 missing = width - V.length values
+                 suffix = foldr (const (step fill)) rest [1 .. missing]
+              in V.foldr step suffix (V.take width values))
+          initial
+          [0 .. pageCount - 1]
+ where
+  pageCount :: Int
+  pageCount = pageOf (boxedLength - 1) + 1
+  pageWidth :: Int -> Int
+  pageWidth page = min pageSize (boxedLength - page * pageSize)
+{-# INLINE boxedFoldr #-}
+
+-- The fill comes from the store, not from the caller. A thaw that took it as an
+-- argument obliged every call site to name the right one, and 'boxedUpdate' —
+-- which passed the value being written — named the wrong one. That was harmless
+-- only while pages were never absent.
+thawBoxedPaged :: TransactionShape -> BoxedPaged requirement a -> ST s (MutableBoxedPaged requirement s a)
+thawBoxedPaged shape base = do
+  mutableBoxedDirty <- newSTRef IntMap.empty
+  mutableBoxedStats <-
+    case shape of
+      DenseTransaction -> pure Nothing
+      LocalTransaction -> Just <$> newSTRef emptyPublicationStats
+  pure
+    MutableBoxedPaged
+      { mutableBoxedBase = base
+      , mutableBoxedDirty
+      , mutableBoxedStats
+      , mutableBoxedPristineBase = case boxedStorage base of
+          DenseBoxedPages pages -> V.null pages
+          DefaultedBoxedPages _ pages -> directorySize pages == 0
+      }
+
+readBoxedPaged :: MutableBoxedPaged requirement s a -> Int -> ST s a
+readBoxedPaged MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty} index = do
+  dirty <- readSTRef mutableBoxedDirty
+  let !page = pageOf index
+      !offset = offsetOf index
+  case IntMap.lookup page dirty of
+    Just values -> MV.unsafeRead values offset
+    Nothing -> case boxedStorage mutableBoxedBase of
+      DenseBoxedPages pages ->
+        pure (V.unsafeIndex (V.unsafeIndex pages page) offset)
+      DefaultedBoxedPages fill pages ->
+        case lookupDirectory page pages of
+          Just values | offset < V.length values -> pure (V.unsafeIndex values offset)
+          _ -> pure fill
+{-# INLINE readBoxedPaged #-}
+
+writeBoxedPaged :: MutableBoxedPaged requirement s a -> Int -> a -> ST s ()
+writeBoxedPaged paged index value = do
+  let !page = pageOf index
+      !offset = offsetOf index
+  values <- ensureMutableBoxedPage paged page
+  MV.unsafeWrite values offset value
+{-# INLINE writeBoxedPaged #-}
+
+-- | Whether the thaw found no materialized page. With no writer into the
+-- element payload planes inside a transaction — there is none; they are
+-- written only through the persistent setters, outside one — this answers for
+-- the whole transaction that every slot reports the fill, so a rewrite has
+-- nothing to return and a relocation has nothing to move. It is why the
+-- near-universal @() () ()@ mesh pays a predictable branch and no page.
+boxedThawPristine :: MutableBoxedPaged requirement s a -> Bool
+boxedThawPristine = mutableBoxedPristineBase
+{-# INLINE boxedThawPristine #-}
+
+-- | Return a contiguous run of slots to the store's fill. A slot no write has
+-- reached already reports the fill, so a page absent from both the base and the
+-- dirty set is left absent: recycling an element at its default keeps the store
+-- sparse. Only a page holding written values is materialized and overwritten.
+resetBoxedRange :: MutableBoxedPaged 'RequiredFill s a -> Int -> Int -> ST s ()
+resetBoxedRange paged@MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty} start count
+  | count <= 0 = pure ()
+  | otherwise = traverse_ resetChunk chunkStarts
+ where
+  fill = requiredBoxedFill mutableBoxedBase
+  !end = start + count
+  chunkStarts =
+    List.unfoldr
+      (\index ->
+         if index >= end
+           then Nothing
+           else
+             let !width = min (pageSize - offsetOf index) (end - index)
+              in Just ((index, width), index + width))
+      start
+
+  resetChunk (index, width) = do
+    dirty <- readSTRef mutableBoxedDirty
+    let !page = pageOf index
+        !offset = offsetOf index
+        !written =
+          IntMap.member page dirty
+            || case boxedStorage mutableBoxedBase of
+              DefaultedBoxedPages _ pages ->
+                case lookupDirectory page pages of
+                  Just values -> offset < V.length values
+                  Nothing -> False
+    when written $ do
+      values <- ensureMutableBoxedPage paged page
+      MV.set (MV.slice offset width values) fill
+
+ensureMutableBoxedPage :: forall requirement s a. MutableBoxedPaged requirement s a -> Int -> ST s (MV.MVector s a)
+ensureMutableBoxedPage MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats} page = do
+  dirty <- readSTRef mutableBoxedDirty
+  case IntMap.lookup page dirty of
+    Just values -> pure values
+    Nothing -> do
+      -- Copying a page slot by slot writes every element twice — once to
+      -- establish the fill and once to overwrite it — and pays a write barrier
+      -- per store. A full page is one thaw; a short tail page fills only the
+      -- slots the base does not reach. This is the same shape the unboxed
+      -- store already uses.
+      (values, copiedCells) <- case boxedStorage mutableBoxedBase of
+        DenseBoxedPages pages
+          | page < V.length pages -> copyExisting (V.unsafeIndex pages page)
+          | otherwise -> do
+              vector <- MV.new pageSize
+              pure (vector, 0)
+        DefaultedBoxedPages fill pages ->
+          case lookupDirectory page pages of
+            Just old -> copyExisting old
+            Nothing -> do
+              vector <- MV.replicate pageSize fill
+              pure (vector, 0)
+      writeSTRef mutableBoxedDirty $! IntMap.insert page values dirty
+      let !basePageCount = boxedPageCount mutableBoxedBase
+          !basePage = page < basePageCount
+      recordPublicationStats
+        mutableBoxedStats
+        (emptyPublicationStats
+             { publicationBoxedBasePageOpens = if basePage then 1 else 0
+             , publicationBoxedDirtyBasePages = if basePage then 1 else 0
+             , publicationBoxedDirtyAppendedPages = if basePage then 0 else 1
+             , publicationBoxedCopiedCells = copiedCells
+             })
+      pure values
+ where
+  boxedPageCount :: BoxedPaged requirement a -> Int
+  boxedPageCount base
+    | boxedLength base <= 0 = 0
+    | otherwise = pageOf (boxedLength base - 1) + 1
+
+  copyExisting :: V.Vector a -> ST s (MV.MVector s a, Int)
+  copyExisting old
+    | V.length old == pageSize = (, V.length old) <$> V.thaw old
+    | otherwise = do
+        vector <- case boxedStorage mutableBoxedBase of
+          DenseBoxedPages _ -> MV.new pageSize
+          DefaultedBoxedPages fill _ -> MV.replicate pageSize fill
+        V.copy (MV.slice 0 (V.length old) vector) old
+        pure (vector, V.length old)
+
+recordPublicationStats :: Maybe (STRef s PublicationStats) -> PublicationStats -> ST s ()
+recordPublicationStats Nothing _ = pure ()
+recordPublicationStats (Just statsRef) delta = modifySTRef' statsRef (addPublicationStats delta)
+{-# INLINE recordPublicationStats #-}
+
+freezeBoxedPaged :: forall requirement s a. Int -> MutableBoxedPaged requirement s a -> ST s (Either BoxedStorageError (BoxedPaged requirement a))
+freezeBoxedPaged length' MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats}
+  | length' < 0 = pure (Left (BoxedFreezeLengthNegative length'))
+  | otherwise = do
+      dirty <- readSTRef mutableBoxedDirty
+      frozenDirty <- traverse V.unsafeFreeze dirty
+      case boxedStorage mutableBoxedBase of
+        DenseBoxedPages _ ->
+          recordPublicationStats
+            mutableBoxedStats
+            (emptyPublicationStats
+                 { publicationBoxedBasePageEnumerations = pageCount
+                 , publicationBoxedBasePageFreezes = pageCount
+                 })
+        DefaultedBoxedPages _ _ -> pure ()
+      pure $ case boxedStorage mutableBoxedBase of
+        DenseBoxedPages basePages -> do
+          pages <-
+            traverse
+              (densePage frozenDirty basePages)
+              [0 .. pageCount - 1]
+          pure
+            BoxedPaged
+              { boxedLength = length'
+              , boxedStorage = DenseBoxedPages (V.fromList pages)
+              }
+        DefaultedBoxedPages fill basePages ->
+          let !merged =
+                List.foldl'
+                  (\pages (page, values) -> insertDirectory page values pages)
+                  basePages
+                  (IntMap.toAscList frozenDirty)
+              !kept = directoryRestrict pageCount merged
+              !trimmed = case directoryLookupMax kept of
+                Nothing -> kept
+                Just (lastPage, values) ->
+                  let !lastLength = length' - lastPage * pageSize
+                   in if lastLength < V.length values
+                        then insertDirectory lastPage (V.take lastLength values) kept
+                        else kept
+           in Right
+                BoxedPaged
+                  { boxedLength = length'
+                  , boxedStorage = DefaultedBoxedPages fill trimmed
+                }
+ where
+  !pageCount = if length' <= 0 then 0 else pageOf (length' - 1) + 1
+
+  densePage
+    :: IntMap.IntMap (V.Vector a)
+    -> V.Vector (V.Vector a)
+    -> Int
+    -> Either BoxedStorageError (V.Vector a)
+  densePage frozenDirty basePages page =
+    let !width = min pageSize (length' - page * pageSize)
+     in case IntMap.lookup page frozenDirty of
+          Just values -> Right (V.take width values)
+          Nothing -> case basePages V.!? page of
+            Just values -> Right (V.take width values)
+            Nothing -> Left (BoxedFreezeDensePageMissing page pageCount)
+
+readMutableBoxedPublicationStats :: MutableBoxedPaged requirement s a -> ST s PublicationStats
+readMutableBoxedPublicationStats MutableBoxedPaged{mutableBoxedStats} =
+  maybe (pure emptyPublicationStats) readSTRef mutableBoxedStats
+
+boxedUpdate :: forall requirement a. Int -> a -> BoxedPaged requirement a -> BoxedPaged requirement a
+boxedUpdate index value source@BoxedPaged{boxedStorage} =
+  source{boxedStorage = updateStorage boxedStorage}
+ where
+  !page = pageOf index
+  !offset = offsetOf index
+
+  updateStorage :: BoxedStorage requirement a -> BoxedStorage requirement a
+  updateStorage (DenseBoxedPages pages) =
+    let !values = V.unsafeIndex pages page
+        !updated = V.modify (\mutable -> MV.unsafeWrite mutable offset value) values
+     in DenseBoxedPages (V.modify (\mutable -> MV.unsafeWrite mutable page updated) pages)
+  updateStorage (DefaultedBoxedPages fill pages) =
+    let !values = case lookupDirectory page pages of
+          Just existing
+            | V.length existing == pageSize -> existing
+            | otherwise -> existing V.++ V.replicate (pageSize - V.length existing) fill
+          Nothing -> V.replicate pageSize fill
+        !updated = V.modify (\mutable -> MV.unsafeWrite mutable offset value) values
+     in DefaultedBoxedPages fill (insertDirectory page updated pages)
diff --git a/src-core/Moonlight/Planar/Internal/BuildStats.hs b/src-core/Moonlight/Planar/Internal/BuildStats.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/BuildStats.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | The closed reported-metric schema and its dense immutable section.
+-- Only the operation's final publication boundary may adopt a counter vector.
+module Moonlight.Planar.Internal.BuildStats where
+
+import Control.DeepSeq (NFData (..))
+import Data.Word (Word64)
+import qualified Data.Vector.Unboxed as U
+
+data BuildMetric
+  = InputPoints
+  | UniquePoints
+  | ExistingPoints
+  | DuplicatePoints
+  | SpatialSeedPoints
+  | FaceSplits
+  | InteriorEdgeSplits
+  | BoundaryEdgeSplits
+  | HullInsertions
+  | LineSplits
+  | LineExtensions
+  | LineToAreaTransitions
+  | EdgeFlips
+  | LocationWalkSteps
+  | LocationFallbacks
+  | LocationMaxWalk
+  | LegalizationMaxStack
+  | SteinerPoints
+  | RefinementFaceChecks
+  | RefinementQueuePops
+  | SweepFastPoints
+  | SweepSkippedPoints
+  deriving stock (Eq, Ord, Enum, Bounded, Show, Read)
+
+buildMetricCount :: Int
+buildMetricCount = fromEnum (maxBound :: BuildMetric) + 1
+{-# INLINE buildMetricCount #-}
+
+-- The vector has exactly buildMetricCount slots. No public constructor,
+-- Generic or Read route can fabricate a shorter section.
+newtype BuildStats = BuildStats (U.Vector Word64)
+  deriving stock (Eq)
+
+instance NFData BuildStats where
+  rnf (BuildStats values) = rnf values
+
+instance Show BuildStats where
+  showsPrec precedence stats = showParen (precedence > 10) $
+    showString "BuildStats " . showsPrec 11 [(metric, buildStat metric stats) | metric <- [minBound .. maxBound]]
+
+buildStat :: BuildMetric -> BuildStats -> Int
+buildStat metric (BuildStats values) = fromIntegral (U.unsafeIndex values (fromEnum metric))
+{-# INLINE buildStat #-}
+
+tabulateBuildStats :: (BuildMetric -> Int) -> BuildStats
+tabulateBuildStats value = BuildStats (U.fromList [fromIntegral (value metric) | metric <- [minBound .. maxBound]])
+
+emptyBuildStats :: BuildStats
+emptyBuildStats = BuildStats (U.replicate buildMetricCount 0)
+
+updateBuildStats :: [(BuildMetric, Int)] -> BuildStats -> BuildStats
+updateBuildStats changes (BuildStats values) =
+  BuildStats (values U.// [(fromEnum metric, fromIntegral value) | (metric, value) <- changes])
diff --git a/src-core/Moonlight/Planar/Internal/Dyadic.hs b/src-core/Moonlight/Planar/Internal/Dyadic.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/Dyadic.hs
@@ -0,0 +1,1065 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE MagicHash #-}
+{-# LANGUAGE UnboxedTuples #-}
+
+#include "MachDeps.h"
+
+module Moonlight.Planar.Internal.Dyadic
+  ( exactOrientDet
+  , exactOrientSignDouble
+  , exactInCircleDet
+  , exactInCircleSignDouble
+  , exactInCircleSignFixedWidth
+  , exactSquaredDistanceOrdering
+  , exactQuarterSquaredDistanceRational
+  , exactCircumradiusSquaredRational
+  , exactCircumradiusSquaredWithin
+  , exactBarycentricDeterminants
+  , exactDiametralDot
+  , integerRatioToDouble
+  , integerBitLength
+  ) where
+
+import Data.Bits
+  ( countLeadingZeros
+  , countTrailingZeros
+  , finiteBitSize
+  , shiftL
+  , shiftR
+  )
+import Data.Word (Word64)
+import GHC.Exts (Int (I#), (+#))
+import GHC.Integer.Logarithms (integerLog2#)
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactArithmeticError
+  , ExactRational
+  , exactRationalFromDyadic
+  , exactRationalFromDyadicRatio
+  )
+#if WORD_SIZE_IN_BITS == 64
+import GHC.Exts
+  ( Double (D#)
+  , Double#
+  , Int#
+  , Word#
+  , and#
+  , castDoubleToWord64#
+  , eqWord#
+  , gtWord#
+  , int2Word#
+  , isTrue#
+  , or#
+  , plusWord#
+  , plusWord2#
+  , subWordC#
+  , timesWord2#
+  , uncheckedShiftL#
+  , uncheckedShiftRL#
+  , word2Int#
+  , word64ToWord#
+  , (*#)
+  , (-#)
+  , (<=#)
+  , (==#)
+  , (>#)
+  , (>=#)
+  )
+#endif
+
+-- Every finite binary64 value is a dyadic rational. Aligning all mantissas to
+-- one exponent gives exact integer predicates without constructing Rational
+-- expression trees.
+type Decoded = (Integer, Int)
+
+alignDecoded :: Int -> Decoded -> Integer
+alignDecoded !power (!mantissa, !sourcePower)
+  | mantissa == 0 = 0
+  | otherwise = mantissa `shiftL` (sourcePower - power)
+{-# INLINE alignDecoded #-}
+
+decodedExponentFloor :: Decoded -> Int
+decodedExponentFloor (mantissa, power)
+  | mantissa == 0 = 0
+  | otherwise = min 0 power
+{-# INLINE decodedExponentFloor #-}
+
+commonExponent4 :: Decoded -> Decoded -> Decoded -> Decoded -> Int
+commonExponent4 firstValue secondValue thirdValue fourthValue =
+  min
+    (min (decodedExponentFloor firstValue) (decodedExponentFloor secondValue))
+    (min (decodedExponentFloor thirdValue) (decodedExponentFloor fourthValue))
+{-# INLINE commonExponent4 #-}
+
+commonExponent6
+  :: Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Int
+commonExponent6 firstValue secondValue thirdValue fourthValue fifthValue sixthValue =
+  min
+    ( commonExponent4
+        firstValue
+        secondValue
+        thirdValue
+        fourthValue
+    )
+    (min (decodedExponentFloor fifthValue) (decodedExponentFloor sixthValue))
+{-# INLINE commonExponent6 #-}
+
+commonExponent8
+  :: Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Decoded
+  -> Int
+commonExponent8 firstValue secondValue thirdValue fourthValue fifthValue sixthValue seventhValue eighthValue =
+  min
+    (commonExponent4 firstValue secondValue thirdValue fourthValue)
+    (commonExponent4 fifthValue sixthValue seventhValue eighthValue)
+{-# INLINE commonExponent8 #-}
+
+aligned4
+  :: Double -> Double -> Double -> Double
+  -> (Int, Integer, Integer, Integer, Integer)
+aligned4 a b c d =
+  let !da = decodeFloat a
+      !db = decodeFloat b
+      !dc = decodeFloat c
+      !dd = decodeFloat d
+      !power = commonExponent4 da db dc dd
+   in ( power
+      , alignDecoded power da
+      , alignDecoded power db
+      , alignDecoded power dc
+      , alignDecoded power dd
+      )
+{-# INLINE aligned4 #-}
+
+aligned6
+  :: Double -> Double -> Double -> Double -> Double -> Double
+  -> (Int, Integer, Integer, Integer, Integer, Integer, Integer)
+aligned6 a b c d e f =
+  let !da = decodeFloat a
+      !db = decodeFloat b
+      !dc = decodeFloat c
+      !dd = decodeFloat d
+      !de = decodeFloat e
+      !df = decodeFloat f
+      !power = commonExponent6 da db dc dd de df
+   in ( power
+      , alignDecoded power da
+      , alignDecoded power db
+      , alignDecoded power dc
+      , alignDecoded power dd
+      , alignDecoded power de
+      , alignDecoded power df
+      )
+
+aligned8
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> (Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)
+aligned8 a b c d e f g h =
+  let !da = decodeFloat a
+      !db = decodeFloat b
+      !dc = decodeFloat c
+      !dd = decodeFloat d
+      !de = decodeFloat e
+      !df = decodeFloat f
+      !dg = decodeFloat g
+      !dh = decodeFloat h
+      !power = commonExponent8 da db dc dd de df dg dh
+   in ( alignDecoded power da
+      , alignDecoded power db
+      , alignDecoded power dc
+      , alignDecoded power dd
+      , alignDecoded power de
+      , alignDecoded power df
+      , alignDecoded power dg
+      , alignDecoded power dh
+      )
+
+exactOrientDet
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Integer
+exactOrientDet ax ay bx by cx cy =
+  let (!_, !iax, !iay, !ibx, !iby, !icx, !icy) = aligned6 ax ay bx by cx cy
+      !acx = iax - icx
+      !acy = iay - icy
+      !bcx = ibx - icx
+      !bcy = iby - icy
+   in acx * bcy - acy * bcx
+
+-- | Compare exact squared distances from one finite binary64 query. All six
+-- coordinates share a dyadic exponent, whose positive square cancels from the
+-- ordering. No rounded distance or rational normalization participates.
+exactSquaredDistanceOrdering
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Ordering
+exactSquaredDistanceOrdering qx qy ax ay bx by =
+  let (!_, !iqx, !iqy, !iax, !iay, !ibx, !iby) = aligned6 qx qy ax ay bx by
+      !difference =
+        (iax - ibx) * (iax + ibx - 2 * iqx)
+          + (iay - iby) * (iay + iby - 2 * iqy)
+   in compare difference 0
+{-# INLINE exactSquaredDistanceOrdering #-}
+
+-- | One quarter of the exact squared distance between two finite binary64
+-- points. Alpha's Gabriel-edge birth needs this value directly, so the dyadic
+-- exponent absorbs the quarter before the single rational normalization.
+exactQuarterSquaredDistanceRational
+  :: Double -> Double -> Double -> Double -> ExactRational
+exactQuarterSquaredDistanceRational ax ay bx by =
+  let (!coordinatePower, !iax, !iay, !ibx, !iby) = aligned4 ax ay bx by
+      !deltaX = ibx - iax
+      !deltaY = iby - iay
+      !squaredDistance = deltaX * deltaX + deltaY * deltaY
+   in exactRationalFromDyadic squaredDistance (2 * coordinatePower - 2)
+{-# INLINE exactQuarterSquaredDistanceRational #-}
+
+-- | Exact squared circumradius of three finite binary64 points. The local
+-- dyadic section computes the integer numerator and denominator without
+-- constructing intermediate ratios; descent normalizes the authoritative
+-- result exactly once. A degenerate triangle is the typed zero-divisor
+-- obstruction from 'ExactRational'.
+exactCircumradiusSquaredRational
+  :: Double -> Double -> Double -> Double -> Double -> Double
+  -> Either ExactArithmeticError ExactRational
+exactCircumradiusSquaredRational ax ay bx by cx cy =
+  let (!coordinatePower, !iax, !iay, !ibx, !iby, !icx, !icy) =
+        aligned6 ax ay bx by cx cy
+      !abx = ibx - iax
+      !aby = iby - iay
+      !acx = icx - iax
+      !acy = icy - iay
+      !bcx = icx - ibx
+      !bcy = icy - iby
+      !abSquared = abx * abx + aby * aby
+      !acSquared = acx * acx + acy * acy
+      !bcSquared = bcx * bcx + bcy * bcy
+      !determinant = abx * acy - aby * acx
+      !radiusNumerator = abSquared * acSquared * bcSquared
+      !radiusDenominator = 4 * determinant * determinant
+   in exactRationalFromDyadicRatio
+        radiusNumerator
+        radiusDenominator
+        (2 * coordinatePower)
+{-# INLINE exactCircumradiusSquaredRational #-}
+
+-- | Exact closed comparison of squared circumradius with a finite,
+-- non-negative binary64 threshold. No constructed circumcenter participates.
+exactCircumradiusSquaredWithin
+  :: Double
+  -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Bool
+exactCircumradiusSquaredWithin
+  threshold
+  ax ay bx by cx cy =
+    let (!thresholdMantissa, !thresholdPower) = decodeFloat threshold
+     in smallIntegralCircumradiusSquaredWithin
+          (fromInteger thresholdMantissa)
+          thresholdPower
+          ax ay bx by cx cy
+{-# INLINE exactCircumradiusSquaredWithin #-}
+
+arbitraryCircumradiusSquaredWithin
+  :: Integer
+  -> Int
+  -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Bool
+arbitraryCircumradiusSquaredWithin
+  thresholdMantissa
+  thresholdPower
+  ax ay bx by cx cy =
+  let (!coordinatePower, !iax, !iay, !ibx, !iby, !icx, !icy) =
+        aligned6 ax ay bx by cx cy
+      !abx = ibx - iax
+      !aby = iby - iay
+      !acx = icx - iax
+      !acy = icy - iay
+      !bcx = icx - ibx
+      !bcy = icy - iby
+      !abSquared = abx * abx + aby * aby
+      !acSquared = acx * acx + acy * acy
+      !bcSquared = bcx * bcx + bcy * bcy
+      !determinant = abx * acy - aby * acx
+      !radiusNumerator = abSquared * acSquared * bcSquared
+      !thresholdDenominator =
+        4 * determinant * determinant * thresholdMantissa
+   in determinant /= 0
+        && compareDyadic
+          radiusNumerator
+          (6 * coordinatePower)
+          thresholdDenominator
+          (4 * coordinatePower + thresholdPower)
+          /= GT
+
+-- Integer-sized edges cover grid, pixel, and indexed-world faces without
+-- allocating arbitrary-precision mantissas. Larger or fractional edges
+-- descend to the general dyadic comparison above.
+smallIntegralCircumradiusSquaredWithin
+  :: Word64
+  -> Int
+  -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Bool
+smallIntegralCircumradiusSquaredWithin
+  rawThresholdMantissa
+  rawThresholdPower
+  ax ay bx by cx cy =
+    if not admittedDifferences
+      then
+        arbitraryCircumradiusSquaredWithin
+          (toInteger rawThresholdMantissa)
+          rawThresholdPower
+          ax ay bx by cx cy
+      else
+        let !abSquared = squaredIntegralLength abx aby
+            !acSquared = squaredIntegralLength acx acy
+            !bcSquared = squaredIntegralLength bcx bcy
+            !radiusNumerator = abSquared * acSquared * bcSquared
+            !determinant = abx * acy - aby * acx
+            !determinantMagnitude = fromIntegral (abs determinant)
+         in if determinant == 0 || rawThresholdMantissa == 0
+              then False
+              else
+                let !trailingZeros = countTrailingZeros rawThresholdMantissa
+                    !thresholdMantissa = rawThresholdMantissa `shiftR` trailingZeros
+                    !thresholdPower = rawThresholdPower + trailingZeros
+                    !determinantSquared = determinantMagnitude * determinantMagnitude
+                    !scaledDeterminant = 4 * determinantSquared
+                    !productsFit =
+                      determinantMagnitude <= maxBound `quot` determinantMagnitude
+                        && determinantSquared <= maxBound `quot` 4
+                        && thresholdMantissa <= maxBound `quot` scaledDeterminant
+                 in if productsFit
+                      then
+                        compareWordDyadic
+                          radiusNumerator
+                          0
+                          (scaledDeterminant * thresholdMantissa)
+                          thresholdPower
+                          /= GT
+                      else
+                        arbitraryCircumradiusSquaredWithin
+                          (toInteger rawThresholdMantissa)
+                          rawThresholdPower
+                          ax ay bx by cx cy
+ where
+  !abxValue = bx - ax
+  !abyValue = by - ay
+  !acxValue = cx - ax
+  !acyValue = cy - ay
+  !abx = truncate abxValue
+  !aby = truncate abyValue
+  !acx = truncate acxValue
+  !acy = truncate acyValue
+  !bcx = acx - abx
+  !bcy = acy - aby
+  admittedDifferences =
+    integralDifference abxValue abx
+      && integralDifference abyValue aby
+      && integralDifference acxValue acx
+      && integralDifference acyValue acy
+{-# INLINE smallIntegralCircumradiusSquaredWithin #-}
+
+integralDifference :: Double -> Int -> Bool
+integralDifference value integral =
+  abs value <= 512 && fromIntegral integral == value
+{-# INLINE integralDifference #-}
+
+squaredIntegralLength :: Int -> Int -> Word64
+squaredIntegralLength x y =
+  let !xMagnitude = fromIntegral (abs x)
+      !yMagnitude = fromIntegral (abs y)
+   in xMagnitude * xMagnitude + yMagnitude * yMagnitude
+{-# INLINE squaredIntegralLength #-}
+
+compareWordDyadic :: Word64 -> Int -> Word64 -> Int -> Ordering
+compareWordDyadic left leftPower right rightPower
+  | left == 0 = compare left right
+  | right == 0 = GT
+  | leftMagnitude /= rightMagnitude = compare leftMagnitude rightMagnitude
+  | leftPower < rightPower = compare left (right `shiftL` (rightPower - leftPower))
+  | otherwise = compare (left `shiftL` (leftPower - rightPower)) right
+ where
+  !leftMagnitude = wordBitLength left + leftPower
+  !rightMagnitude = wordBitLength right + rightPower
+
+  wordBitLength :: Word64 -> Int
+  wordBitLength value = finiteBitSize value - countLeadingZeros value
+{-# INLINE compareWordDyadic #-}
+
+compareDyadic :: Integer -> Int -> Integer -> Int -> Ordering
+compareDyadic left leftPower right rightPower =
+  case compare leftPower rightPower of
+    LT -> compare left (right `shiftL` (rightPower - leftPower))
+    EQ -> compare left right
+    GT -> compare (left `shiftL` (leftPower - rightPower)) right
+{-# INLINE compareDyadic #-}
+
+exactInCircleDet
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Integer
+exactInCircleDet ax ay bx by cx cy dx dy =
+  let (!iax, !iay, !ibx, !iby, !icx, !icy, !idx, !idy) =
+        aligned8 ax ay bx by cx cy dx dy
+      !adx = iax - idx
+      !ady = iay - idy
+      !bdx = ibx - idx
+      !bdy = iby - idy
+      !cdx = icx - idx
+      !cdy = icy - idy
+      !abdet = adx * bdy - bdx * ady
+      !bcdet = bdx * cdy - cdx * bdy
+      !cadet = cdx * ady - adx * cdy
+      !alift = adx * adx + ady * ady
+      !blift = bdx * bdx + bdy * bdy
+      !clift = cdx * cdx + cdy * cdy
+   in alift * bcdet + blift * cadet + clift * abdet
+
+exactDiametralDot
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Integer
+exactDiametralDot ax ay bx by px py =
+  let (!_, !iax, !iay, !ibx, !iby, !ipx, !ipy) = aligned6 ax ay bx by px py
+      !pax = ipx - iax
+      !pay = ipy - iay
+      !pbx = ipx - ibx
+      !pby = ipy - iby
+   in pax * pbx + pay * pby
+
+exactBarycentricDeterminants
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> (Integer, Integer, Integer, Integer)
+exactBarycentricDeterminants ax ay bx by cx cy qx qy =
+  let (!iax, !iay, !ibx, !iby, !icx, !icy, !iqx, !iqy) =
+        aligned8 ax ay bx by cx cy qx qy
+      determinant :: Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Integer
+      determinant px py rx ry sx sy =
+        let !psx = px - sx
+            !psy = py - sy
+            !rsx = rx - sx
+            !rsy = ry - sy
+         in psx * rsy - psy * rsx
+      !denominator = determinant iax iay ibx iby icx icy
+      !weightA = determinant iqx iqy ibx iby icx icy
+      !weightB = determinant iax iay iqx iqy icx icy
+      !weightC = determinant iax iay ibx iby iqx iqy
+   in (denominator, weightA, weightB, weightC)
+
+integerRatioToDouble :: Integer -> Integer -> Double
+integerRatioToDouble numerator denominator
+  | denominator == 0 = 0 / 0
+  | numerator == 0 = 0
+  | otherwise =
+      let !precision = floatDigits (0 :: Double)
+          !numeratorMagnitude = abs numerator
+          !denominatorMagnitude = abs denominator
+          !numeratorBits = integerBitLength numeratorMagnitude
+          !denominatorBits = integerBitLength denominatorMagnitude
+          !numeratorShift = max 0 (numeratorBits - precision)
+          !denominatorShift = max 0 (denominatorBits - precision)
+          !scaledNumerator = fromInteger (numeratorMagnitude `shiftR` numeratorShift)
+          !scaledDenominator = fromInteger (denominatorMagnitude `shiftR` denominatorShift)
+          !magnitude = scaleFloat (numeratorShift - denominatorShift) (scaledNumerator / scaledDenominator)
+          !sameSign = (numerator < 0) == (denominator < 0)
+       in if sameSign then magnitude else negate magnitude
+
+integerBitLength :: Integer -> Int
+integerBitLength value
+  | value <= 0 = 0
+  | otherwise = I# (integerLog2# value +# 1#)
+{-# INLINE integerBitLength #-}
+
+-- ---------------------------------------------------------------------------
+-- Fixed-precision exact orient sign for Double.
+--
+-- The generic dyadic path answers every exact query with arbitrary-precision
+-- Integers: six decodes, one alignment, and two multiplies, each allocating.
+-- Practical inputs have an exponent spread small enough that the determinant's
+-- exact sign is decided by 128-bit differences and 256-bit products in machine
+-- words, without a single heap object. 'exactOrientSignDouble' takes that path
+-- and falls back to 'exactOrientDet' the moment an operand is non-finite or an
+-- alignment shift would outgrow the fixed width. The two agree by
+-- construction: both compute the sign of the same integer determinant.
+--
+-- The fixed-width worker reads a Double as one machine word and aligns
+-- mantissas across a 128-bit pair, so it is only meaningful where a machine
+-- word is 64 bits wide. On a narrower target the same sign is taken from the
+-- arbitrary-precision determinant directly, which is the branch this path
+-- already falls back to whenever an alignment shift would outgrow the width.
+
+#if WORD_SIZE_IN_BITS == 64
+
+exactOrientSignDouble
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Ordering
+exactOrientSignDouble ax ay bx by cx cy =
+  case ax of
+    D# axw ->
+      case ay of
+        D# ayw ->
+          case bx of
+            D# bxw ->
+              case by of
+                D# byw ->
+                  case cx of
+                    D# cxw ->
+                      case cy of
+                        D# cyw ->
+                          case orientSignWorker axw ayw bxw byw cxw cyw of
+                            2# -> compare (exactOrientDet ax ay bx by cx cy) 0
+                            0# -> EQ
+                            sign ->
+                              case sign ># 0# of
+                                1# -> GT
+                                _ -> LT
+{-# NOINLINE exactOrientSignDouble #-}
+
+-- Decode a Double into sign bit (0/1), mantissa, and power-of-two exponent
+-- with value = (-1)^sign * mantissa * 2^exponent. Zero decodes to a zero
+-- mantissa; subnormals decode without a hidden bit. The fourth component is 1
+-- when the value is finite and 0 when it is not.
+decodeExact :: Double# -> (# Int#, Word#, Int#, Int# #)
+decodeExact d =
+  case word64ToWord# (castDoubleToWord64# d) of
+    bits ->
+      let neg = word2Int# (uncheckedShiftRL# bits 63#)
+          exponentField = word2Int# (and# (uncheckedShiftRL# bits 52#) 2047##)
+          mantissaField = and# bits 4503599627370495##
+       in case exponentField of
+            0# -> (# neg, mantissaField, -1074#, 1# #)
+            2047# -> (# neg, mantissaField, 0#, 0# #)
+            raw -> (# neg, or# mantissaField 4503599627370496##, raw -# 1075#, 1# #)
+
+-- A mantissa of at most 53 bits shifted left by at most 73 bits: the pair
+-- (high, low) of a value below 2^126.
+shiftMantissa :: Word# -> Int# -> (# Word#, Word# #)
+shiftMantissa mantissa k =
+  case k >=# 64# of
+    1# -> (# uncheckedShiftL# mantissa (k -# 64#), 0## #)
+    _ ->
+      case k ==# 0# of
+        1# -> (# 0##, mantissa #)
+        _ ->
+          (#
+            uncheckedShiftRL# mantissa (64# -# k),
+            uncheckedShiftL# mantissa k
+          #)
+
+-- The exact sign and 128-bit magnitude of sa*ma*2^ea - sc*mc*2^ec, aligned
+-- to the caller-supplied floor exponent, as (sign, high, low, status) with
+-- sign in {-1, 0, 1} and status 1 when an alignment shift outgrows the fixed
+-- width. The floor never exceeds the exponent of a nonzero operand, so every
+-- shift is non-negative; one shared floor is what makes the four differences
+-- of one determinant comparable after multiplication.
+differenceExact
+  :: Int# -> Word# -> Int# -> Int# -> Word# -> Int# -> Int# -> (# Int#, Word#, Word#, Int# #)
+differenceExact nega ma ea negc mc ec emin =
+  case ma of
+    0## ->
+      case mc of
+        0## -> (# 0#, 0##, 0##, 0# #)
+        _ -> aligned (negateSign (positiveSign negc)) mc (ec -# emin)
+    _ ->
+      case mc of
+        0## -> aligned (positiveSign nega) ma (ea -# emin)
+        _ ->
+          case ea -# emin of
+            da ->
+              case da ># 73# of
+                1# -> (# 0#, 0##, 0##, 1# #)
+                _ ->
+                  case ec -# emin of
+                    dc ->
+                      case dc ># 73# of
+                        1# -> (# 0#, 0##, 0##, 1# #)
+                        _ ->
+                          case shiftMantissa ma da of
+                            (# ahi, alo #) ->
+                              case shiftMantissa mc dc of
+                                (# chi, clo #) ->
+                                  case nega ==# negc of
+                                    1# ->
+                                      -- Same operand signs: subtract magnitudes.
+                                      case compareWord2 ahi alo chi clo of
+                                        0# -> (# 0#, 0##, 0##, 0# #)
+                                        1# ->
+                                          case subtractWord2 ahi alo chi clo of
+                                            (# hi, lo #) -> (# positiveSign nega, hi, lo, 0# #)
+                                        _ ->
+                                          case subtractWord2 chi clo ahi alo of
+                                            (# hi, lo #) -> (# negateSign (positiveSign nega), hi, lo, 0# #)
+                                    _ ->
+                                      -- Opposite operand signs: add magnitudes.
+                                      case addWord2 ahi alo chi clo of
+                                        (# hi, lo #) -> (# positiveSign nega, hi, lo, 0# #)
+  where
+    aligned sign mantissa k =
+      case k ># 73# of
+        1# -> (# 0#, 0##, 0##, 1# #)
+        _ ->
+          case shiftMantissa mantissa k of
+            (# hi, lo #) -> (# sign, hi, lo, 0# #)
+    positiveSign neg = case neg of
+      1# -> -1#
+      _ -> 1#
+    negateSign sign = case sign of
+      1# -> -1#
+      _ -> 1#
+
+-- Lexicographic comparison of 128-bit magnitudes: 1, 0, or -1.
+compareWord2 :: Word# -> Word# -> Word# -> Word# -> Int#
+compareWord2 ahi alo chi clo =
+  case eqWord# ahi chi of
+    1# ->
+      case eqWord# alo clo of
+        1# -> 0#
+        _ ->
+          case gtWord# alo clo of
+            1# -> 1#
+            _ -> -1#
+    _ ->
+      case gtWord# ahi chi of
+        1# -> 1#
+        _ -> -1#
+
+-- 128-bit difference of magnitudes, first operand at least the second.
+subtractWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word# #)
+subtractWord2 ahi alo chi clo =
+  case subWordC# alo clo of
+    (# low, borrow #) ->
+      case subWordC# ahi chi of
+        (# high0, _ #) ->
+          case subWordC# high0 (int2Word# borrow) of
+            (# high, _ #) -> (# high, low #)
+
+-- 128-bit sum of magnitudes each below 2^126: the total stays below 2^127 and
+-- the final carry is empty by the shift bound.
+addWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word# #)
+addWord2 ahi alo chi clo =
+  case plusWord2# alo clo of
+    (# carry0, low #) ->
+      case plusWord2# ahi chi of
+        (# _, high0 #) ->
+          case plusWord2# high0 carry0 of
+            (# _, high #) -> (# high, low #)
+
+-- 128-bit by 128-bit exact product, (r3, r2, r1, r0), most significant first.
+-- Each factor stays below 2^127, so the product stays below 2^254 and the top
+-- accumulation cannot overflow.
+multiplyWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word#, Word#, Word# #)
+multiplyWord2 ahi alo bhi blo =
+  case timesWord2# alo blo of
+    (# h00, l00 #) ->
+      case timesWord2# alo bhi of
+        (# h01, l01 #) ->
+          case timesWord2# ahi blo of
+            (# h10, l10 #) ->
+              case timesWord2# ahi bhi of
+                (# h11, l11 #) ->
+                  case plusWord2# h00 l01 of
+                    (# carryA, sumA #) ->
+                      case plusWord2# sumA l10 of
+                        (# carryB, r1 #) ->
+                          case plusWord# carryA carryB of
+                            carry2 ->
+                              case plusWord2# h01 h10 of
+                                (# carryC, sumC #) ->
+                                  case plusWord2# sumC l11 of
+                                    (# carryD, sumD #) ->
+                                      case plusWord2# sumD carry2 of
+                                        (# carryE, r2 #) ->
+                                          case plusWord# (plusWord# carryC carryD) carryE of
+                                            carry3 ->
+                                              case plusWord# h11 carry3 of
+                                                r3 -> (# r3, r2, r1, l00 #)
+
+-- Lexicographic comparison of 256-bit magnitudes: 1, 0, or -1.
+compareWord4
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Int#
+compareWord4 a3 a2 a1 a0 b3 b2 b1 b0 =
+  case compareWord2 a3 a2 b3 b2 of
+    0# -> compareWord2 a1 a0 b1 b0
+    answer -> answer
+
+-- The exponent a mantissa contributes to the alignment floor: a zero
+-- mantissa is exact at any floor and votes for the impossibly high sentinel.
+floorExp :: Word# -> Int# -> Int#
+floorExp mantissa power =
+  case mantissa of
+    0## -> 2000000#
+    _ -> power
+
+minExp :: Int# -> Int# -> Int#
+minExp a b = if isTrue# (a <=# b) then a else b
+
+orientSignWorker :: Double# -> Double# -> Double# -> Double# -> Double# -> Double# -> Int#
+orientSignWorker ax ay bx by cx cy =
+  case decodeExact ax of
+    (# negax, max_, eax, okax #) ->
+      case decodeExact ay of
+        (# negay, may, eay, okay #) ->
+          case decodeExact bx of
+            (# negbx, mbx, ebx, okbx #) ->
+              case decodeExact by of
+                (# negby, mby, eby, okby #) ->
+                  case decodeExact cx of
+                    (# negcx, mcx, ecx, okcx #) ->
+                      case decodeExact cy of
+                        (# negcy, mcy, ecy, okcy #) ->
+                          case okax +# okay +# okbx +# okby +# okcx +# okcy of
+                            6# ->
+                              -- The alignment floor is the least exponent
+                              -- among nonzero mantissas; a zero mantissa is
+                              -- exact at any floor and must not drag it down.
+                              case floorExp max_ eax `minExp` floorExp may eay `minExp` floorExp mbx ebx `minExp` floorExp mby eby `minExp` floorExp mcx ecx `minExp` floorExp mcy ecy of
+                                emin ->
+                                  case differenceExact negax max_ eax negcx mcx ecx emin of
+                                    (# s1, d1hi, d1lo, f1 #) ->
+                                      case differenceExact negby mby eby negcy mcy ecy emin of
+                                        (# s2, d2hi, d2lo, f2 #) ->
+                                          case differenceExact negay may eay negcy mcy ecy emin of
+                                            (# s3, d3hi, d3lo, f3 #) ->
+                                              case differenceExact negbx mbx ebx negcx mcx ecx emin of
+                                                (# s4, d4hi, d4lo, f4 #) ->
+                                                  case f1 +# f2 +# f3 +# f4 of
+                                                    0# ->
+                                                      combineSigns
+                                                        (s1 *# s2) d1hi d1lo d2hi d2lo
+                                                        (s3 *# s4) d3hi d3lo d4hi d4lo
+                                                    _ -> 2#
+                            _ -> 2#
+  where
+    -- det = leftSign * leftProduct - rightSign * rightProduct
+    combineSigns leftSign d1hi d1lo d2hi d2lo rightSign d3hi d3lo d4hi d4lo =
+      case leftSign of
+        0# ->
+          case rightSign of
+            0# -> 0#
+            _ -> 0# -# rightSign
+        _ ->
+          case rightSign of
+            0# -> leftSign
+            _ ->
+              case leftSign ==# rightSign of
+                1# ->
+                  case multiplyWord2 d1hi d1lo d2hi d2lo of
+                    (# p3, p2, p1, p0 #) ->
+                      case multiplyWord2 d3hi d3lo d4hi d4lo of
+                        (# q3, q2, q1, q0 #) ->
+                          case compareWord4 p3 p2 p1 p0 q3 q2 q1 q0 of
+                            0# -> 0#
+                            1# -> leftSign
+                            _ -> 0# -# leftSign
+                _ -> leftSign
+
+-- | The sign of the in-circle determinant of four finite binary64 points,
+-- resolved in fixed width where the eight coordinates align within it and
+-- read from the arbitrary-precision determinant otherwise. The two agree by
+-- construction: the worker evaluates the same integer determinant as
+-- 'exactInCircleDet', over the same alignment floor, and only declines.
+exactInCircleSignDouble
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Ordering
+exactInCircleSignDouble ax ay bx by cx cy dx dy =
+  case inCircleSignDouble ax ay bx by cx cy dx dy of
+    2# -> compare (exactInCircleDet ax ay bx by cx cy dx dy) 0
+    0# -> EQ
+    sign ->
+      case sign ># 0# of
+        1# -> GT
+        _ -> LT
+{-# NOINLINE exactInCircleSignDouble #-}
+
+-- | The fixed-width verdict alone: 'Nothing' where the worker declined and
+-- the sign would come from the determinant. A test that cross-checks the
+-- worker against 'exactInCircleDet' counts how many cases it resolved
+-- through this, rather than crediting the fallback with the worker's work.
+exactInCircleSignFixedWidth
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Maybe Ordering
+exactInCircleSignFixedWidth ax ay bx by cx cy dx dy =
+  case inCircleSignDouble ax ay bx by cx cy dx dy of
+    2# -> Nothing
+    0# -> Just EQ
+    sign ->
+      case sign ># 0# of
+        1# -> Just GT
+        _ -> Just LT
+
+inCircleSignDouble
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Int#
+inCircleSignDouble (D# ax) (D# ay) (D# bx) (D# by) (D# cx) (D# cy) (D# dx) (D# dy) =
+  inCircleSignWorker ax ay bx by cx cy dx dy
+{-# INLINE inCircleSignDouble #-}
+
+-- The degree-four analogue of 'orientSignWorker': the six differences
+-- against the fourth point are the same 128-bit aligned magnitudes, each
+-- minor and lift is a signed sum of two 256-bit products of them, each term
+-- is a 512-bit product of a lift and a minor, and the determinant is the
+-- signed sum of the three terms. Every product of two magnitudes below 2^127
+-- stays below 2^254, so a minor or lift stays below 2^255, a term below
+-- 2^510, and the sum of three terms below 2^512: no accumulation overflows
+-- its width, and the only way out is an alignment shift beyond 73 bits.
+inCircleSignWorker
+  :: Double# -> Double# -> Double# -> Double# -> Double# -> Double# -> Double# -> Double#
+  -> Int#
+inCircleSignWorker ax ay bx by cx cy dx dy =
+  case decodeExact ax of { (# negax, max_, eax, okax #) ->
+  case decodeExact ay of { (# negay, may, eay, okay #) ->
+  case decodeExact bx of { (# negbx, mbx, ebx, okbx #) ->
+  case decodeExact by of { (# negby, mby, eby, okby #) ->
+  case decodeExact cx of { (# negcx, mcx, ecx, okcx #) ->
+  case decodeExact cy of { (# negcy, mcy, ecy, okcy #) ->
+  case decodeExact dx of { (# negdx, mdx, edx, okdx #) ->
+  case decodeExact dy of { (# negdy, mdy, edy, okdy #) ->
+  case okax +# okay +# okbx +# okby +# okcx +# okcy +# okdx +# okdy of
+    8# ->
+      case floorExp max_ eax `minExp` floorExp may eay `minExp` floorExp mbx ebx `minExp` floorExp mby eby `minExp` floorExp mcx ecx `minExp` floorExp mcy ecy `minExp` floorExp mdx edx `minExp` floorExp mdy edy of
+        emin ->
+          case differenceExact negax max_ eax negdx mdx edx emin of { (# sadx, adxh, adxl, f1 #) ->
+          case differenceExact negay may eay negdy mdy edy emin of { (# sady, adyh, adyl, f2 #) ->
+          case differenceExact negbx mbx ebx negdx mdx edx emin of { (# sbdx, bdxh, bdxl, f3 #) ->
+          case differenceExact negby mby eby negdy mdy edy emin of { (# sbdy, bdyh, bdyl, f4 #) ->
+          case differenceExact negcx mcx ecx negdx mdx edx emin of { (# scdx, cdxh, cdxl, f5 #) ->
+          case differenceExact negcy mcy ecy negdy mdy edy emin of { (# scdy, cdyh, cdyl, f6 #) ->
+          case f1 +# f2 +# f3 +# f4 +# f5 +# f6 of
+            0# ->
+              inCircleFromDifferences
+                sadx adxh adxl sady adyh adyl
+                sbdx bdxh bdxl sbdy bdyh bdyl
+                scdx cdxh cdxl scdy cdyh cdyl
+            _ -> 2# }}}}}}
+    _ -> 2# }}}}}}}}
+
+inCircleFromDifferences
+  :: Int# -> Word# -> Word# -> Int# -> Word# -> Word#
+  -> Int# -> Word# -> Word# -> Int# -> Word# -> Word#
+  -> Int# -> Word# -> Word# -> Int# -> Word# -> Word#
+  -> Int#
+inCircleFromDifferences
+  sadx adxh adxl sady adyh adyl
+  sbdx bdxh bdxl sbdy bdyh bdyl
+  scdx cdxh cdxl scdy cdyh cdyl =
+    case minor sadx adxh adxl sbdy bdyh bdyl sbdx bdxh bdxl sady adyh adyl of { (# sab, ab3, ab2, ab1, ab0 #) ->
+    case minor sbdx bdxh bdxl scdy cdyh cdyl scdx cdxh cdxl sbdy bdyh bdyl of { (# sbc, bc3, bc2, bc1, bc0 #) ->
+    case minor scdx cdxh cdxl sady adyh adyl sadx adxh adxl scdy cdyh cdyl of { (# sca, ca3, ca2, ca1, ca0 #) ->
+    case lift sadx adxh adxl sady adyh adyl of { (# sal, al3, al2, al1, al0 #) ->
+    case lift sbdx bdxh bdxl sbdy bdyh bdyl of { (# sbl, bl3, bl2, bl1, bl0 #) ->
+    case lift scdx cdxh cdxl scdy cdyh cdyl of { (# scl, cl3, cl2, cl1, cl0 #) ->
+    case product8 sal al3 al2 al1 al0 sbc bc3 bc2 bc1 bc0 of { (# s1, a7, a6, a5, a4, a3, a2, a1, a0 #) ->
+    case product8 sbl bl3 bl2 bl1 bl0 sca ca3 ca2 ca1 ca0 of { (# s2, b7, b6, b5, b4, b3, b2, b1, b0 #) ->
+    case product8 scl cl3 cl2 cl1 cl0 sab ab3 ab2 ab1 ab0 of { (# s3, c7, c6, c5, c4, c3, c2, c1, c0 #) ->
+    case sum8 s1 a7 a6 a5 a4 a3 a2 a1 a0 s2 b7 b6 b5 b4 b3 b2 b1 b0 of { (# s12, d7, d6, d5, d4, d3, d2, d1, d0 #) ->
+    case sum8 s12 d7 d6 d5 d4 d3 d2 d1 d0 s3 c7 c6 c5 c4 c3 c2 c1 c0 of { (# sign, _, _, _, _, _, _, _, _ #) ->
+    sign }}}}}}}}}}}
+  where
+    -- p*q - r*s over signed 128-bit magnitudes.
+    minor sp ph pl sq qh ql sr rh rl ss sh sl =
+      case product4 sp ph pl sq qh ql of
+        (# s1, p3, p2, p1, p0 #) ->
+          case product4 sr rh rl ss sh sl of
+            (# s2, q3, q2, q1, q0 #) ->
+              sum4 s1 p3 p2 p1 p0 (0# -# s2) q3 q2 q1 q0
+    -- x*x + y*y over signed 128-bit magnitudes.
+    lift sx xh xl sy yh yl =
+      case product4 sx xh xl sx xh xl of
+        (# s1, p3, p2, p1, p0 #) ->
+          case product4 sy yh yl sy yh yl of
+            (# s2, q3, q2, q1, q0 #) ->
+              sum4 s1 p3 p2 p1 p0 s2 q3 q2 q1 q0
+
+-- Signed 256-bit product of two signed 128-bit magnitudes.
+product4
+  :: Int# -> Word# -> Word# -> Int# -> Word# -> Word#
+  -> (# Int#, Word#, Word#, Word#, Word# #)
+product4 sa ahi alo sb bhi blo =
+  case multiplyWord2 ahi alo bhi blo of
+    (# p3, p2, p1, p0 #) -> (# sa *# sb, p3, p2, p1, p0 #)
+
+-- Signed 512-bit product of two signed 256-bit magnitudes.
+product8
+  :: Int# -> Word# -> Word# -> Word# -> Word# -> Int# -> Word# -> Word# -> Word# -> Word#
+  -> (# Int#, Word#, Word#, Word#, Word#, Word#, Word#, Word#, Word# #)
+product8 sa a3 a2 a1 a0 sb b3 b2 b1 b0 =
+  case multiplyWord4 a3 a2 a1 a0 b3 b2 b1 b0 of
+    (# r7, r6, r5, r4, r3, r2, r1, r0 #) -> (# sa *# sb, r7, r6, r5, r4, r3, r2, r1, r0 #)
+
+-- Signed sum of two signed 256-bit values whose magnitudes' sum fits.
+sum4
+  :: Int# -> Word# -> Word# -> Word# -> Word# -> Int# -> Word# -> Word# -> Word# -> Word#
+  -> (# Int#, Word#, Word#, Word#, Word# #)
+sum4 sa a3 a2 a1 a0 sb b3 b2 b1 b0 =
+  case sa of
+    0# -> (# sb, b3, b2, b1, b0 #)
+    _ ->
+      case sb of
+        0# -> (# sa, a3, a2, a1, a0 #)
+        _ ->
+          case sa ==# sb of
+            1# ->
+              case addWord4 a3 a2 a1 a0 b3 b2 b1 b0 of
+                (# _, r3, r2, r1, r0 #) -> (# sa, r3, r2, r1, r0 #)
+            _ ->
+              case compareWord4 a3 a2 a1 a0 b3 b2 b1 b0 of
+                0# -> (# 0#, 0##, 0##, 0##, 0## #)
+                1# ->
+                  case subtractWord4 a3 a2 a1 a0 b3 b2 b1 b0 of
+                    (# r3, r2, r1, r0 #) -> (# sa, r3, r2, r1, r0 #)
+                _ ->
+                  case subtractWord4 b3 b2 b1 b0 a3 a2 a1 a0 of
+                    (# r3, r2, r1, r0 #) -> (# sb, r3, r2, r1, r0 #)
+
+-- Signed sum of two signed 512-bit values whose magnitudes' sum fits.
+sum8
+  :: Int# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> Int# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Int#, Word#, Word#, Word#, Word#, Word#, Word#, Word#, Word# #)
+sum8 sa a7 a6 a5 a4 a3 a2 a1 a0 sb b7 b6 b5 b4 b3 b2 b1 b0 =
+  case sa of
+    0# -> (# sb, b7, b6, b5, b4, b3, b2, b1, b0 #)
+    _ ->
+      case sb of
+        0# -> (# sa, a7, a6, a5, a4, a3, a2, a1, a0 #)
+        _ ->
+          case sa ==# sb of
+            1# ->
+              case addWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 of
+                (# r7, r6, r5, r4, r3, r2, r1, r0 #) -> (# sa, r7, r6, r5, r4, r3, r2, r1, r0 #)
+            _ ->
+              case compareWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 of
+                0# -> (# 0#, 0##, 0##, 0##, 0##, 0##, 0##, 0##, 0## #)
+                1# ->
+                  case subtractWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 of
+                    (# r7, r6, r5, r4, r3, r2, r1, r0 #) -> (# sa, r7, r6, r5, r4, r3, r2, r1, r0 #)
+                _ ->
+                  case subtractWord8 b7 b6 b5 b4 b3 b2 b1 b0 a7 a6 a5 a4 a3 a2 a1 a0 of
+                    (# r7, r6, r5, r4, r3, r2, r1, r0 #) -> (# sb, r7, r6, r5, r4, r3, r2, r1, r0 #)
+
+-- a + b + carry with carry in {0, 1}, as (carry out, sum).
+addCarry :: Word# -> Word# -> Word# -> (# Word#, Word# #)
+addCarry a b carry =
+  case plusWord2# a b of
+    (# carry1, partial #) ->
+      case plusWord2# partial carry of
+        (# carry2, total #) -> (# plusWord# carry1 carry2, total #)
+
+-- a - b - borrow with borrow in {0, 1}, as (borrow out, difference).
+subtractBorrow :: Word# -> Word# -> Word# -> (# Word#, Word# #)
+subtractBorrow a b borrow =
+  case subWordC# a b of
+    (# partial, borrow1 #) ->
+      case subWordC# partial borrow of
+        (# total, borrow2 #) -> (# int2Word# (borrow1 +# borrow2), total #)
+
+-- 256-bit sum with its carry out.
+addWord4
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Word#, Word#, Word#, Word#, Word# #)
+addWord4 a3 a2 a1 a0 b3 b2 b1 b0 =
+  case addCarry a0 b0 0## of { (# c0, r0 #) ->
+  case addCarry a1 b1 c0 of { (# c1, r1 #) ->
+  case addCarry a2 b2 c1 of { (# c2, r2 #) ->
+  case addCarry a3 b3 c2 of { (# c3, r3 #) ->
+  (# c3, r3, r2, r1, r0 #) }}}}
+
+-- 256-bit difference of magnitudes, first operand at least the second.
+subtractWord4
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Word#, Word#, Word#, Word# #)
+subtractWord4 a3 a2 a1 a0 b3 b2 b1 b0 =
+  case subtractBorrow a0 b0 0## of { (# c0, r0 #) ->
+  case subtractBorrow a1 b1 c0 of { (# c1, r1 #) ->
+  case subtractBorrow a2 b2 c1 of { (# c2, r2 #) ->
+  case subtractBorrow a3 b3 c2 of { (# _, r3 #) ->
+  (# r3, r2, r1, r0 #) }}}}
+
+-- 512-bit sum of magnitudes whose total fits, so the carry out is empty.
+addWord8
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Word#, Word#, Word#, Word#, Word#, Word#, Word#, Word# #)
+addWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 =
+  case addWord4 a3 a2 a1 a0 b3 b2 b1 b0 of { (# c3, r3, r2, r1, r0 #) ->
+  case addCarry a4 b4 c3 of { (# c4, r4 #) ->
+  case addCarry a5 b5 c4 of { (# c5, r5 #) ->
+  case addCarry a6 b6 c5 of { (# c6, r6 #) ->
+  case addCarry a7 b7 c6 of { (# _, r7 #) ->
+  (# r7, r6, r5, r4, r3, r2, r1, r0 #) }}}}}
+
+-- 512-bit difference of magnitudes, first operand at least the second.
+subtractWord8
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Word#, Word#, Word#, Word#, Word#, Word#, Word#, Word# #)
+subtractWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 =
+  case subtractBorrow a0 b0 0## of { (# c0, r0 #) ->
+  case subtractBorrow a1 b1 c0 of { (# c1, r1 #) ->
+  case subtractBorrow a2 b2 c1 of { (# c2, r2 #) ->
+  case subtractBorrow a3 b3 c2 of { (# c3, r3 #) ->
+  case subtractBorrow a4 b4 c3 of { (# c4, r4 #) ->
+  case subtractBorrow a5 b5 c4 of { (# c5, r5 #) ->
+  case subtractBorrow a6 b6 c5 of { (# c6, r6 #) ->
+  case subtractBorrow a7 b7 c6 of { (# _, r7 #) ->
+  (# r7, r6, r5, r4, r3, r2, r1, r0 #) }}}}}}}}
+
+-- Lexicographic comparison of 512-bit magnitudes: 1, 0, or -1.
+compareWord8
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> Int#
+compareWord8 a7 a6 a5 a4 a3 a2 a1 a0 b7 b6 b5 b4 b3 b2 b1 b0 =
+  case compareWord4 a7 a6 a5 a4 b7 b6 b5 b4 of
+    0# -> compareWord4 a3 a2 a1 a0 b3 b2 b1 b0
+    answer -> answer
+
+-- 256-bit by 256-bit exact product as eight words, most significant first:
+-- schoolbook over 128-bit halves, each half product taken exactly by
+-- 'multiplyWord2', the two middle products summed to 257 bits before the
+-- three bands are added at their offsets.
+multiplyWord4
+  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word#
+  -> (# Word#, Word#, Word#, Word#, Word#, Word#, Word#, Word# #)
+multiplyWord4 a3 a2 a1 a0 b3 b2 b1 b0 =
+  case multiplyWord2 a1 a0 b1 b0 of { (# p3, p2, p1, p0 #) ->
+  case multiplyWord2 a1 a0 b3 b2 of { (# q3, q2, q1, q0 #) ->
+  case multiplyWord2 a3 a2 b1 b0 of { (# r3, r2, r1, r0 #) ->
+  case multiplyWord2 a3 a2 b3 b2 of { (# t3, t2, t1, t0 #) ->
+  case addWord4 q3 q2 q1 q0 r3 r2 r1 r0 of { (# m4, m3, m2, m1, m0 #) ->
+  case addCarry p2 m0 0## of { (# c2, w2 #) ->
+  case addCarry p3 m1 c2 of { (# c3, w3 #) ->
+  case addCarry t0 m2 c3 of { (# c4, w4 #) ->
+  case addCarry t1 m3 c4 of { (# c5, w5 #) ->
+  case addCarry t2 m4 c5 of { (# c6, w6 #) ->
+  case plusWord# t3 c6 of { w7 ->
+  (# w7, w6, w5, w4, w3, w2, p1, p0 #) }}}}}}}}}}}
+
+#else
+
+-- The narrow-word answer to the same question. 'exactOrientDet' is the
+-- determinant the fixed-width worker exists to avoid allocating, not a
+-- different quantity, so the two branches agree by construction.
+exactOrientSignDouble
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Ordering
+exactOrientSignDouble ax ay bx by cx cy =
+  compare (exactOrientDet ax ay bx by cx cy) 0
+{-# NOINLINE exactOrientSignDouble #-}
+
+exactInCircleSignDouble
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Ordering
+exactInCircleSignDouble ax ay bx by cx cy dx dy =
+  compare (exactInCircleDet ax ay bx by cx cy dx dy) 0
+{-# NOINLINE exactInCircleSignDouble #-}
+
+exactInCircleSignFixedWidth
+  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Maybe Ordering
+exactInCircleSignFixedWidth _ _ _ _ _ _ _ _ = Nothing
+
+#endif
diff --git a/src-core/Moonlight/Planar/Internal/ExactRational.hs b/src-core/Moonlight/Planar/Internal/ExactRational.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/ExactRational.hs
@@ -0,0 +1,267 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MagicHash #-}
+
+-- | Normalized exact rational arithmetic without geometric dependencies.
+module Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , ExactArithmeticError (..)
+  , exactRational
+  , exactRationalFromDouble
+  , exactRationalFromFiniteDouble
+  , exactRationalFromDyadic
+  , exactRationalFromDyadicRatio
+  , exactRationalFromNormalizedRatio
+  , exactRationalNumerator
+  , exactRationalDenominator
+  , exactRationalBitWidth
+  , exactRationalDenominatorBitWidth
+  , exactRationalIsZero
+  , exactDivide
+  , exactSignum
+  , PositiveExact
+  , UnitInterval
+  , ScalarRefinementError (..)
+  , positiveExact
+  , positiveExactValue
+  , unitInterval
+  , unitIntervalValue
+  , unitZero
+  , unitHalf
+  , unitOne
+  , positiveOne
+  , positiveTwo
+  , exactHalf
+  , exactThird
+  , blendPositive
+  , divideByPositive
+  , ratioPositive
+  , multiplyPositive
+  , positiveSumSquares
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.Bits ((.&.), shiftL, shiftR)
+import Data.Ratio (Ratio, (%))
+import qualified Data.Ratio as Ratio
+import GHC.Generics (Generic)
+import GHC.Exts (Int (I#))
+import GHC.Integer.Logarithms (integerLog2#)
+import GHC.Real (Ratio ((:%)))
+
+-- | A checked wrapper around a reduced ratio with a strictly positive
+-- denominator. 'Ratio' owns normalization, including the unique zero
+-- representation @0 / 1@.
+newtype ExactRational = ExactRational (Ratio Integer)
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (Num)
+
+instance NFData ExactRational where
+  rnf (ExactRational value) = rnf value
+  {-# INLINE rnf #-}
+
+-- | Typed refusals from exact rational construction and division.
+data ExactArithmeticError
+  = ExactZeroDenominator
+  | ExactZeroDivisor
+  | ExactNaNInput
+  | ExactInfiniteInput
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Construct a reduced rational, moving any sign onto the numerator and
+-- refusing a zero denominator.
+exactRational :: Integer -> Integer -> Either ExactArithmeticError ExactRational
+exactRational _ 0 = Left ExactZeroDenominator
+exactRational numerator denominator = Right (ExactRational (numerator % denominator))
+
+-- | Convert a binary64 value without loss, refusing NaN and infinities.
+exactRationalFromDouble :: Double -> Either ExactArithmeticError ExactRational
+exactRationalFromDouble value
+  | isNaN value = Left ExactNaNInput
+  | isInfinite value = Left ExactInfiniteInput
+  | otherwise = Right (exactRationalFromFiniteDouble value)
+
+-- | Convert an already-admitted finite coordinate without loss. The caller
+-- supplies a finite value, such as a coordinate inside a validated
+-- @QueryPoint@, so this worker does not repeat the admission refusal.
+exactRationalFromFiniteDouble :: Double -> ExactRational
+exactRationalFromFiniteDouble value =
+  uncurry exactRationalFromDyadic (decodeFloat value)
+{-# INLINE exactRationalFromFiniteDouble #-}
+
+-- | Construct @numerator * 2^power@ without a partial denominator path.
+exactRationalFromDyadic :: Integer -> Int -> ExactRational
+exactRationalFromDyadic numerator power
+  | numerator == 0 = ExactRational (0 :% 1)
+  | power >= 0 = ExactRational ((numerator `shiftL` power) :% 1)
+  | otherwise =
+      let denominatorPower = negate power
+          removableFactor =
+            min denominatorPower (integerTrailingZeroBits numerator)
+          reducedNumerator = numerator `shiftR` removableFactor
+          reducedDenominator =
+            1 `shiftL` (denominatorPower - removableFactor)
+       in ExactRational (reducedNumerator :% reducedDenominator)
+{-# INLINE exactRationalFromDyadic #-}
+
+-- | The denominator of a dyadic rational is a power of two, so its complete
+-- normalization requires only the numerator's least set bit. Calling generic
+-- rational GCD here merely rediscovers that closed arithmetic fact.
+integerTrailingZeroBits :: Integer -> Int
+integerTrailingZeroBits value =
+  let magnitude = abs value
+   in I# (integerLog2# (magnitude .&. negate magnitude))
+{-# INLINE integerTrailingZeroBits #-}
+
+-- | Construct @(numerator / denominator) * 2^power@ with one final rational
+-- normalization. This is the publication boundary for exact dyadic kernels:
+-- their integer arithmetic must not pay a greatest-common-divisor reduction
+-- after every intermediate operation.
+exactRationalFromDyadicRatio
+  :: Integer
+  -> Integer
+  -> Int
+  -> Either ExactArithmeticError ExactRational
+exactRationalFromDyadicRatio _ 0 _ = Left ExactZeroDivisor
+exactRationalFromDyadicRatio 0 _ _ = Right (ExactRational (0 :% 1))
+exactRationalFromDyadicRatio numerator denominator power =
+  let numeratorFactor = integerTrailingZeroBits numerator
+      denominatorFactor = integerTrailingZeroBits denominator
+      oddNumerator = numerator `shiftR` numeratorFactor
+      oddDenominator = denominator `shiftR` denominatorFactor
+      residualPower = power + numeratorFactor - denominatorFactor
+   in if residualPower >= 0
+        then exactRational (oddNumerator `shiftL` residualPower) oddDenominator
+        else exactRational oddNumerator (oddDenominator `shiftL` negate residualPower)
+{-# INLINE exactRationalFromDyadicRatio #-}
+
+-- | Internal bridge from the normalized carrier owned by @Data.Ratio@. This
+-- exists for statically nonzero rational constants in exact kernels; public
+-- callers continue through 'exactRational'.
+exactRationalFromNormalizedRatio :: Ratio Integer -> ExactRational
+exactRationalFromNormalizedRatio = ExactRational
+{-# INLINE exactRationalFromNormalizedRatio #-}
+
+-- | Read the reduced numerator.
+exactRationalNumerator :: ExactRational -> Integer
+exactRationalNumerator (ExactRational value) = Ratio.numerator value
+{-# INLINE exactRationalNumerator #-}
+
+-- | Read the strictly positive reduced denominator.
+exactRationalDenominator :: ExactRational -> Integer
+exactRationalDenominator (ExactRational value) = Ratio.denominator value
+{-# INLINE exactRationalDenominator #-}
+
+-- | Maximum bit width of the reduced numerator magnitude and strictly
+-- positive denominator. This is an observation, not an arithmetic bound.
+exactRationalBitWidth :: ExactRational -> Int
+exactRationalBitWidth value =
+  max
+    (integerBitWidth (abs (exactRationalNumerator value)))
+    (exactRationalDenominatorBitWidth value)
+{-# INLINE exactRationalBitWidth #-}
+
+-- | Bit width of the reduced, strictly positive denominator.
+exactRationalDenominatorBitWidth :: ExactRational -> Int
+exactRationalDenominatorBitWidth =
+  integerBitWidth . exactRationalDenominator
+{-# INLINE exactRationalDenominatorBitWidth #-}
+
+integerBitWidth :: Integer -> Int
+integerBitWidth value
+  | value <= 0 = 0
+  | otherwise = I# (integerLog2# value) + 1
+{-# INLINE integerBitWidth #-}
+
+-- | Test whether the exact value is zero.
+exactRationalIsZero :: ExactRational -> Bool
+exactRationalIsZero (ExactRational value) = Ratio.numerator value == 0
+{-# INLINE exactRationalIsZero #-}
+
+-- | Divide by a nonzero exact rational, refusing a zero divisor explicitly.
+exactDivide
+  :: ExactRational
+  -> ExactRational
+  -> Either ExactArithmeticError ExactRational
+exactDivide (ExactRational left) (ExactRational right)
+  | Ratio.numerator right == 0 = Left ExactZeroDivisor
+  | otherwise = Right (ExactRational (left / right))
+
+-- | Compare an exact rational with zero through its canonical numerator.
+exactSignum :: ExactRational -> Ordering
+exactSignum (ExactRational value) = compare (Ratio.numerator value) 0
+{-# INLINE exactSignum #-}
+
+-- | A strictly positive scalar. Only admission and positivity-preserving
+-- arithmetic in this scalar owner can construct the witness.
+newtype PositiveExact = PositiveExact ExactRational
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (NFData)
+
+-- | A rational parameter in the closed unit interval.
+newtype UnitInterval = UnitInterval ExactRational
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (NFData)
+
+data ScalarRefinementError
+  = ExactNotPositive !ExactRational
+  | ExactOutsideUnitInterval !ExactRational
+  deriving stock (Eq, Ord, Show)
+
+positiveExact :: ExactRational -> Either ScalarRefinementError PositiveExact
+positiveExact value
+  | value > 0 = Right (PositiveExact value)
+  | otherwise = Left (ExactNotPositive value)
+
+positiveExactValue :: PositiveExact -> ExactRational
+positiveExactValue (PositiveExact value) = value
+
+unitInterval :: ExactRational -> Either ScalarRefinementError UnitInterval
+unitInterval value
+  | value >= 0 && value <= 1 = Right (UnitInterval value)
+  | otherwise = Left (ExactOutsideUnitInterval value)
+
+unitIntervalValue :: UnitInterval -> ExactRational
+unitIntervalValue (UnitInterval value) = value
+
+unitZero, unitHalf, unitOne :: UnitInterval
+unitZero = UnitInterval 0
+unitHalf = UnitInterval exactHalf
+unitOne = UnitInterval 1
+
+positiveOne, positiveTwo :: PositiveExact
+positiveOne = PositiveExact 1
+positiveTwo = PositiveExact 2
+
+exactHalf, exactThird :: ExactRational
+exactHalf = ExactRational (1 :% 2)
+exactThird = ExactRational (1 :% 3)
+
+-- | Closed-interval convex blending preserves strict positivity, including
+-- either endpoint. No repeated admission is required by its consumers.
+blendPositive :: UnitInterval -> PositiveExact -> PositiveExact -> PositiveExact
+blendPositive (UnitInterval t) (PositiveExact a) (PositiveExact b) =
+  PositiveExact ((1 - t) * a + t * b)
+
+-- | The same Ratio division as 'exactDivide', with its nonzero precondition
+-- already discharged by the opaque positive divisor.
+divideByPositive :: ExactRational -> PositiveExact -> ExactRational
+divideByPositive (ExactRational a) (PositiveExact (ExactRational b)) =
+  ExactRational (a / b)
+
+ratioPositive :: PositiveExact -> PositiveExact -> PositiveExact
+ratioPositive (PositiveExact a) b = PositiveExact (divideByPositive a b)
+
+-- | Products of admitted positive scalars remain positive without readmission.
+multiplyPositive :: PositiveExact -> PositiveExact -> PositiveExact
+multiplyPositive (PositiveExact a) (PositiveExact b) = PositiveExact (a * b)
+
+-- | A squared Euclidean norm is zero exactly when both coordinates vanish.
+-- The nonzero branch constructs its positivity witness without re-admission.
+positiveSumSquares :: ExactRational -> ExactRational -> Maybe PositiveExact
+positiveSumSquares x y
+  | x == 0 && y == 0 = Nothing
+  | otherwise = Just (PositiveExact (x * x + y * y))
diff --git a/src-core/Moonlight/Planar/Internal/FaceQueue.hs b/src-core/Moonlight/Planar/Internal/FaceQueue.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/FaceQueue.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RecordWildCards #-}
+
+-- | The set of faces still owed a refinement decision.
+module Moonlight.Planar.Internal.FaceQueue
+  ( FaceQueue
+  , newFaceQueue
+  , pushFace
+  , popFace
+  ) where
+
+import Control.Monad.ST (ST)
+import qualified Data.IntSet as IntSet
+import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef)
+import Data.Word (Word32)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  , popGrowableOr
+  , pushGrowable
+  )
+import Moonlight.Planar.Internal.PackedIndex (noIndex, packIndex)
+
+-- A worst-first heap has to be told a score for every face it is offered, so
+-- the caller derives one — circumradius, area, the encroachment verdict — for
+-- faces that are then found acceptable and dropped. Ruppert's termination does
+-- not rest on that order; it rests on every bad face being reached before the
+-- run ends. So this only has to be a set with a discipline for draining it.
+--
+-- A stack is that, and it makes membership O(1) with nothing to compute. The
+-- pending flags are what keep it a set: a face touched by several insertions
+-- before it is drained is decided once, not once per touch.
+--
+-- The size lives in an unboxed cell for the reason the growable stack's does:
+-- it is written on every push and every pop, and a boxed counter allocates a
+-- box per write once the size leaves the shared small-'Int' range.
+data FaceQueue s
+  = DenseFaceQueue
+      !(MUV.MVector s Word32)
+      !(MUV.MVector s Bool)
+      !(MUV.MVector s Int)
+  | SparseFaceQueue
+      !(GrowableWord32 s)
+      !(STRef s IntSet.IntSet)
+
+newFaceQueue :: Maybe Int -> ST s (FaceQueue s)
+newFaceQueue capacity =
+  case capacity of
+    Just size -> do
+      faces <- MUV.new (max 1 size)
+      pending <- MUV.replicate (max 1 size) False
+      stackSize <- MUV.replicate 1 0
+      pure (DenseFaceQueue faces pending stackSize)
+    Nothing -> do
+      faces <- newGrowableWord32 16
+      pending <- newSTRef IntSet.empty
+      pure (SparseFaceQueue faces pending)
+
+-- The checked read of the pending flag is the one bound this module does not
+-- establish itself: the face arrives from mesh topology. Once it succeeds,
+-- uniqueness proves the stack write: at most one slot exists for each pending
+-- flag, and the two vectors have the same length.
+pushFace :: FaceQueue s -> Int -> ST s ()
+pushFace queue face =
+  case queue of
+    DenseFaceQueue faces pending stackSize -> do
+      isPending <- MUV.unsafeRead pending face
+      if isPending
+        then pure ()
+        else do
+          size <- MUV.unsafeRead stackSize 0
+          MUV.unsafeWrite faces size (packIndex face)
+          MUV.unsafeWrite pending face True
+          MUV.unsafeWrite stackSize 0 (size + 1)
+    SparseFaceQueue faces pending -> do
+      isPending <- IntSet.member face <$> readSTRef pending
+      if isPending
+        then pure ()
+        else do
+          pushGrowable faces (packIndex face)
+          modifySTRef' pending (IntSet.insert face)
+
+popFace :: FaceQueue s -> ST s (Maybe Int)
+popFace queue =
+  case queue of
+    DenseFaceQueue faces pending stackSize -> do
+      size <- MUV.unsafeRead stackSize 0
+      if size == 0
+        then pure Nothing
+        else do
+          let !index = size - 1
+          face <- fromIntegral <$> MUV.unsafeRead faces index
+          MUV.unsafeWrite pending face False
+          MUV.unsafeWrite stackSize 0 index
+          pure (Just face)
+    SparseFaceQueue faces pending -> do
+      packed <- popGrowableOr noIndex faces
+      if packed == noIndex
+        then pure Nothing
+        else do
+          let !face = fromIntegral packed
+          modifySTRef' pending (IntSet.delete face)
+          pure (Just face)
diff --git a/src-core/Moonlight/Planar/Internal/Growable.hs b/src-core/Moonlight/Planar/Internal/Growable.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/Growable.hs
@@ -0,0 +1,94 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+module Moonlight.Planar.Internal.Growable
+  ( GrowableWord32
+  , newGrowableWord32
+  , clearGrowable
+  , growableLength
+  , pushGrowable
+  , popGrowableOr
+  , readGrowable
+  , writeGrowable
+  ) where
+
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+
+-- | Transaction-local vector. It starts small and grows only with the local
+-- frontier; no operation allocates scratch memory proportional to mesh size.
+-- The size lives in an unboxed cell: the stack is popped and pushed inside
+-- every legalization drain, and a boxed counter would allocate on each step.
+data GrowableWord32 s = GrowableWord32
+  { growableVector :: !(STRef s (MUV.MVector s Word32))
+  , growableSize :: !(MUV.MVector s Int)
+  }
+
+newGrowableWord32 :: Int -> ST s (GrowableWord32 s)
+newGrowableWord32 requested = do
+  vector <- MUV.new (max 8 requested)
+  growableVector <- newSTRef vector
+  growableSize <- MUV.replicate 1 0
+  pure GrowableWord32{growableVector, growableSize}
+
+clearGrowable :: GrowableWord32 s -> ST s ()
+clearGrowable GrowableWord32{growableSize} = MUV.unsafeWrite growableSize 0 0
+{-# INLINE clearGrowable #-}
+
+growableLength :: GrowableWord32 s -> ST s Int
+growableLength GrowableWord32{growableSize} = MUV.unsafeRead growableSize 0
+{-# INLINE growableLength #-}
+
+pushGrowable :: GrowableWord32 s -> Word32 -> ST s ()
+pushGrowable growable@GrowableWord32{growableSize} value = do
+  index <- MUV.unsafeRead growableSize 0
+  vector <- ensureCapacity growable (index + 1)
+  MUV.unsafeWrite vector index value
+  MUV.unsafeWrite growableSize 0 (index + 1)
+{-# INLINE pushGrowable #-}
+
+-- | Pop, answering the caller's sentinel on emptiness instead of allocating a
+-- 'Maybe' per step. Sound only against a sentinel no push can store.
+popGrowableOr :: Word32 -> GrowableWord32 s -> ST s Word32
+popGrowableOr sentinel GrowableWord32{growableVector, growableSize} = do
+  size <- MUV.unsafeRead growableSize 0
+  if size <= 0
+    then pure sentinel
+    else do
+      let !index = size - 1
+      vector <- readSTRef growableVector
+      value <- MUV.unsafeRead vector index
+      MUV.unsafeWrite growableSize 0 index
+      pure value
+{-# INLINE popGrowableOr #-}
+
+readGrowable :: GrowableWord32 s -> Int -> ST s Word32
+readGrowable GrowableWord32{growableVector} index = do
+  vector <- readSTRef growableVector
+  MUV.unsafeRead vector index
+{-# INLINE readGrowable #-}
+
+writeGrowable :: GrowableWord32 s -> Int -> Word32 -> ST s ()
+writeGrowable growable@GrowableWord32{growableSize} index value = do
+  vector <- ensureCapacity growable (index + 1)
+  MUV.unsafeWrite vector index value
+  size <- MUV.unsafeRead growableSize 0
+  when (index >= size) (MUV.unsafeWrite growableSize 0 (index + 1))
+{-# INLINE writeGrowable #-}
+
+-- Answering with the vector is what keeps a push to one cell read: the caller
+-- would otherwise re-read the reference this just proved current.
+ensureCapacity :: GrowableWord32 s -> Int -> ST s (MUV.MVector s Word32)
+ensureCapacity GrowableWord32{growableVector} required = do
+  vector <- readSTRef growableVector
+  let !current = MUV.length vector
+  if required <= current
+    then pure vector
+    else do
+      let !next = until (>= required) (* 2) current
+      grown <- MUV.grow vector (next - current)
+      writeSTRef growableVector grown
+      pure grown
diff --git a/src-core/Moonlight/Planar/Internal/PackedIndex.hs b/src-core/Moonlight/Planar/Internal/PackedIndex.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/PackedIndex.hs
@@ -0,0 +1,34 @@
+module Moonlight.Planar.Internal.PackedIndex
+  ( noIndex
+  , indexLimit
+  , packIndex
+  , unpackIndex
+  , unpackOptionalIndex
+  ) where
+
+import Data.Word (Word32)
+
+noIndex :: Word32
+noIndex = maxBound
+
+indexLimit :: Int
+indexLimit = fromIntegral (maxBound - 1 :: Word32)
+
+-- | Pack an admitted arena handle. The build boundary limits vertex capacity
+-- so the mutable topology reservation, @8n + 16@, stays below 'indexLimit';
+-- every caller supplies a non-negative handle or worklist position derived
+-- from those arenas. Optional @-1@ handles are represented separately by
+-- 'noIndex' before this function is reached.
+packIndex :: Int -> Word32
+packIndex = fromIntegral
+{-# INLINE packIndex #-}
+
+unpackIndex :: Word32 -> Int
+unpackIndex = fromIntegral
+{-# INLINE unpackIndex #-}
+
+unpackOptionalIndex :: Word32 -> Maybe Int
+unpackOptionalIndex value
+  | value == noIndex = Nothing
+  | otherwise = Just (unpackIndex value)
+{-# INLINE unpackOptionalIndex #-}
diff --git a/src-core/Moonlight/Planar/Internal/PageDirectory.hs b/src-core/Moonlight/Planar/Internal/PageDirectory.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/PageDirectory.hs
@@ -0,0 +1,206 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE NamedFieldPuns #-}
+
+-- | The map from page number to page, for the paged stores.
+module Moonlight.Planar.Internal.PageDirectory
+  ( PageDirectory
+  , emptyDirectory
+  , lookupDirectory
+  , insertDirectory
+  , directoryFromAscList
+  , directoryToAscList
+  , directorySize
+  , directoryLookupMax
+  , directoryRestrict
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bits (shiftL, shiftR, (.&.))
+import qualified Data.List as List
+import qualified Data.Vector as V
+import qualified Data.Vector.Mutable as MV
+import GHC.Generics (Generic)
+
+-- Page numbers are small, dense and non-negative, and the read path resolves
+-- one on every field access that misses its owner's current page. An ordered
+-- map answers that with a chain of prefix comparisons whose length grows with
+-- the mesh; a 32-way radix answers it with two indexed loads at every size the
+-- meshes reach, and the arithmetic is shifts and masks rather than branches.
+--
+-- Absence is representable because the boxed payload store leaves a page out
+-- entirely when every slot in it still holds the fill.
+data Slot a
+  = Absent
+  | Page !a
+  | Fanout !(V.Vector (Slot a))
+  deriving stock (Generic, Functor)
+  deriving anyclass (NFData)
+
+-- | @directoryHeight@ counts the fanout levels below the root: at height 0 the
+-- root's children are pages, at height 1 they are fanouts of pages, and so on.
+data PageDirectory a = PageDirectory
+  { directoryHeight :: {-# UNPACK #-} !Int
+  , directoryRoot :: !(Slot a)
+  , directorySize :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Generic, Functor)
+  deriving anyclass (NFData)
+
+fanout :: Int
+fanout = 32
+{-# INLINE fanout #-}
+
+slotBits :: Int
+slotBits = 5
+{-# INLINE slotBits #-}
+
+-- Path copying replaces one slot per level, and the bulk-update operator would
+-- route that through a list of one pair. This is the copy the level needs and
+-- nothing besides.
+updateSlot :: V.Vector (Slot a) -> Int -> Slot a -> V.Vector (Slot a)
+updateSlot children index child =
+  V.modify (\node -> MV.unsafeWrite node index child) children
+{-# INLINE updateSlot #-}
+
+-- | The first page number a directory of this height cannot address.
+capacity :: Int -> Int
+capacity height = 1 `shiftL` (slotBits * (height + 1))
+{-# INLINE capacity #-}
+
+emptyDirectory :: PageDirectory a
+emptyDirectory = PageDirectory 0 Absent 0
+
+lookupDirectory :: Int -> PageDirectory a -> Maybe a
+lookupDirectory key PageDirectory{directoryHeight, directoryRoot}
+  | key < 0 || key >= capacity directoryHeight = Nothing
+  | otherwise = go directoryHeight directoryRoot
+ where
+  go :: forall value. Int -> Slot value -> Maybe value
+  go !height slot = case slot of
+    Absent -> Nothing
+    Page value -> Just value
+    Fanout children ->
+      go (height - 1) (V.unsafeIndex children ((key `shiftR` (slotBits * height)) .&. (fanout - 1)))
+{-# INLINE lookupDirectory #-}
+
+insertDirectory :: Int -> a -> PageDirectory a -> PageDirectory a
+insertDirectory key value directory
+  | key >= capacity (directoryHeight directory) = insertDirectory key value (grow directory)
+  | otherwise =
+      let (!root, !added) = write (directoryHeight directory) (directoryRoot directory)
+       in directory
+            { directoryRoot = root
+            , directorySize = directorySize directory + if added then 1 else 0
+            }
+ where
+  write !height slot
+    | height < 0 = (Page value, case slot of Page _ -> False; _ -> True)
+    | otherwise =
+        let !index = (key `shiftR` (slotBits * height)) .&. (fanout - 1)
+            !children = case slot of
+              Fanout existing -> existing
+              _ -> V.replicate fanout Absent
+            (!child, !added) = write (height - 1) (V.unsafeIndex children index)
+         in (Fanout (updateSlot children index child), added)
+
+-- A directory that has run out of addressable pages gains a level, and its
+-- whole former extent becomes child zero of the new root.
+grow :: PageDirectory a -> PageDirectory a
+grow directory@PageDirectory{directoryHeight, directoryRoot} =
+  directory
+    { directoryHeight = directoryHeight + 1
+    , directoryRoot = case directoryRoot of
+        Absent -> Absent
+        occupied -> Fanout (updateSlot (V.replicate fanout Absent) 0 occupied)
+    }
+
+-- | Build from ascending, distinct page numbers in one descent.
+--
+-- Every dense transaction ends by publishing a whole directory, so this is on
+-- the freeze path of bulk load, constraint recovery and refinement alike.
+-- Folding 'insertDirectory' over the list would copy each root path once per
+-- page; laying the levels down directly touches each node once.
+directoryFromAscList :: [(Int, a)] -> PageDirectory a
+directoryFromAscList [] = emptyDirectory
+directoryFromAscList entries@((firstKey, _) : remainingEntries) =
+  PageDirectory
+    { directoryHeight = height
+    , directoryRoot = fst (build height 0 entries)
+    , directorySize = length entries
+    }
+ where
+  !largest = List.foldl' (\largestKey (key, _) -> max largestKey key) firstKey remainingEntries
+  !height = heightFor 0
+  heightFor !candidate
+    | largest < capacity candidate = candidate
+    | otherwise = heightFor (candidate + 1)
+
+  build :: Int -> Int -> [(Int, b)] -> (Slot b, [(Int, b)])
+  build !level !base remaining = case remaining of
+    [] -> (Absent, [])
+    ((key, value) : rest)
+      | key >= base + capacity level -> (Absent, remaining)
+      | level < 0 -> (Page value, rest)
+      | otherwise ->
+          let !childSpan = capacity (level - 1)
+              step
+                :: forall value.
+                   ([Slot value], [(Int, value)])
+                -> Int
+                -> ([Slot value], [(Int, value)])
+              step (children, unconsumed) index =
+                let (!child, !beyond) = build (level - 1) (base + index * childSpan) unconsumed
+                 in (child : children, beyond)
+              (!reversed, !left) = List.foldl' step ([], remaining) [0 .. fanout - 1]
+           in (Fanout (V.fromListN fanout (reverse reversed)), left)
+
+directoryToAscList :: PageDirectory a -> [(Int, a)]
+directoryToAscList PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot []
+ where
+  go :: Int -> Int -> Slot b -> [(Int, b)] -> [(Int, b)]
+  go !height !prefix slot rest = case slot of
+    Absent -> rest
+    Page value -> (prefix, value) : rest
+    Fanout children ->
+      foldr
+        (\index accumulated ->
+          go
+            (height - 1)
+            (prefix + (index `shiftL` (slotBits * height)))
+            (V.unsafeIndex children index)
+            accumulated
+        )
+        rest
+        [0 .. fanout - 1]
+
+directoryLookupMax :: PageDirectory a -> Maybe (Int, a)
+directoryLookupMax PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot
+ where
+  go :: Int -> Int -> Slot b -> Maybe (Int, b)
+  go !height !prefix slot = case slot of
+    Absent -> Nothing
+    Page value -> Just (prefix, value)
+    Fanout children -> descend (fanout - 1)
+     where
+      descend !index
+        | index < 0 = Nothing
+        | otherwise =
+            case go (height - 1) (prefix + (index `shiftL` (slotBits * height))) (V.unsafeIndex children index) of
+              Nothing -> descend (index - 1)
+              found -> found
+
+-- | Keep only the pages a directory of the given page count can hold. The
+-- common case is that every page is already within the count, and that is
+-- settled by one descent rather than a rebuild.
+directoryRestrict :: Int -> PageDirectory a -> PageDirectory a
+directoryRestrict pageCount directory
+  | pageCount <= 0 = emptyDirectory
+  | otherwise = case directoryLookupMax directory of
+      Nothing -> directory
+      Just (largest, _)
+        | largest < pageCount -> directory
+        | otherwise ->
+            directoryFromAscList (filter ((< pageCount) . fst) (directoryToAscList directory))
diff --git a/src-core/Moonlight/Planar/Internal/Paged.hs b/src-core/Moonlight/Planar/Internal/Paged.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/Paged.hs
@@ -0,0 +1,453 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Moonlight.Planar.Internal.Paged
+  ( Paged
+  , MutablePaged
+  , FlatMutablePaged
+  , emptyPaged
+  , fromVector
+  , fromLocalVector
+  , toVector
+  , pagedLength
+  , pagedPageCount
+  , pagedOverlayPageCount
+  , PublicationStats (..)
+  , emptyPublicationStats
+  , addPublicationStats
+  , pagedUnsafeIndex
+  , pagedFoldl'
+  , pagedFoldr
+  , pagedIFoldr
+  , newMutablePaged
+  , newLocalMutablePaged
+  , TransactionShape (..)
+  , thawPaged
+  , thawPagedDense
+  , thawPagedShaped
+  , flatMutableSection
+  , readFlatMutable
+  , writeFlatMutable
+  , readPaged
+  , writePaged
+  , freezePaged
+  , readMutablePagedPublicationStats
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Control.Monad (when)
+import Control.Monad.ST (ST)
+import Data.Bits (shiftL, shiftR, (.&.))
+import qualified Data.IntMap.Strict as IntMap
+import Data.List (unfoldr)
+import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
+import qualified Data.Vector.Unboxed as U
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.PageDirectory
+  ( PageDirectory
+  , directoryRestrict
+  , directorySize
+  , emptyDirectory
+  , insertDirectory
+  , lookupDirectory
+  )
+
+-- | One flat root plus one cumulative page overlay. An extension never wraps
+-- a published value in another 'Paged' node: dirty pages are merged into the
+-- existing directory, so an untouched read remains one overlay lookup plus
+-- one root lookup regardless of extension count.
+data Paged a
+  = FlatPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(U.Vector a)
+  | SharedPaged
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      !(U.Vector a)
+      !(PageDirectory (U.Vector a))
+
+-- | Exact storage work observed by one persistent publication.  The dense
+-- page counters describe a whole-base traversal; dirty-page and copied-cell
+-- counters describe only pages opened by the local write set.  A zero dense
+-- counter is therefore a storage fact from the transaction owner, never a
+-- benchmark inference; page work is independent of the resident prefix except
+-- for directory-path depth.
+data PublicationStats = PublicationStats
+  { publicationUnboxedBasePageEnumerations :: {-# UNPACK #-} !Int
+  , publicationUnboxedBasePageOpens :: {-# UNPACK #-} !Int
+  , publicationUnboxedBasePageFreezes :: {-# UNPACK #-} !Int
+  , publicationUnboxedDirtyBasePages :: {-# UNPACK #-} !Int
+  , publicationUnboxedDirtyAppendedPages :: {-# UNPACK #-} !Int
+  , publicationUnboxedCopiedCells :: {-# UNPACK #-} !Int
+  , publicationBoxedBasePageEnumerations :: {-# UNPACK #-} !Int
+  , publicationBoxedBasePageOpens :: {-# UNPACK #-} !Int
+  , publicationBoxedBasePageFreezes :: {-# UNPACK #-} !Int
+  , publicationBoxedDirtyBasePages :: {-# UNPACK #-} !Int
+  , publicationBoxedDirtyAppendedPages :: {-# UNPACK #-} !Int
+  , publicationBoxedCopiedCells :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Eq, Show)
+
+instance NFData PublicationStats where
+  rnf stats = stats `seq` ()
+
+emptyPublicationStats :: PublicationStats
+emptyPublicationStats =
+  PublicationStats
+    { publicationUnboxedBasePageEnumerations = 0
+    , publicationUnboxedBasePageOpens = 0
+    , publicationUnboxedBasePageFreezes = 0
+    , publicationUnboxedDirtyBasePages = 0
+    , publicationUnboxedDirtyAppendedPages = 0
+    , publicationUnboxedCopiedCells = 0
+    , publicationBoxedBasePageEnumerations = 0
+    , publicationBoxedBasePageOpens = 0
+    , publicationBoxedBasePageFreezes = 0
+    , publicationBoxedDirtyBasePages = 0
+    , publicationBoxedDirtyAppendedPages = 0
+    , publicationBoxedCopiedCells = 0
+    }
+
+addPublicationStats :: PublicationStats -> PublicationStats -> PublicationStats
+addPublicationStats left right =
+  PublicationStats
+    { publicationUnboxedBasePageEnumerations = publicationUnboxedBasePageEnumerations left + publicationUnboxedBasePageEnumerations right
+    , publicationUnboxedBasePageOpens = publicationUnboxedBasePageOpens left + publicationUnboxedBasePageOpens right
+    , publicationUnboxedBasePageFreezes = publicationUnboxedBasePageFreezes left + publicationUnboxedBasePageFreezes right
+    , publicationUnboxedDirtyBasePages = publicationUnboxedDirtyBasePages left + publicationUnboxedDirtyBasePages right
+    , publicationUnboxedDirtyAppendedPages = publicationUnboxedDirtyAppendedPages left + publicationUnboxedDirtyAppendedPages right
+    , publicationUnboxedCopiedCells = publicationUnboxedCopiedCells left + publicationUnboxedCopiedCells right
+    , publicationBoxedBasePageEnumerations = publicationBoxedBasePageEnumerations left + publicationBoxedBasePageEnumerations right
+    , publicationBoxedBasePageOpens = publicationBoxedBasePageOpens left + publicationBoxedBasePageOpens right
+    , publicationBoxedBasePageFreezes = publicationBoxedBasePageFreezes left + publicationBoxedBasePageFreezes right
+    , publicationBoxedDirtyBasePages = publicationBoxedDirtyBasePages left + publicationBoxedDirtyBasePages right
+    , publicationBoxedDirtyAppendedPages = publicationBoxedDirtyAppendedPages left + publicationBoxedDirtyAppendedPages right
+    , publicationBoxedCopiedCells = publicationBoxedCopiedCells left + publicationBoxedCopiedCells right
+    }
+
+-- | A fresh build owns a contiguous arena. A local transaction owns only its
+-- dirty pages; clean values remain reachable through the immutable base.
+data MutablePaged s a
+  = MutableFlatPaged {-# UNPACK #-} !Int !(MUV.MVector s a)
+  | MutableSharedPaged
+      {-# UNPACK #-} !Int
+      !(Paged a)
+      !(STRef s (IntMap.IntMap (MUV.MVector s a)))
+      !(STRef s PublicationStats)
+
+-- | A proof that one mutable sequence is in its contiguous physical section.
+data FlatMutablePaged s a = FlatMutablePaged !(MUV.MVector s a)
+
+localPageBits :: Int
+localPageBits = 8
+{-# INLINE localPageBits #-}
+
+traversalPageBits :: Int
+traversalPageBits = 10
+{-# INLINE traversalPageBits #-}
+
+pageSize :: Int -> Int
+pageSize bits = 1 `shiftL` bits
+{-# INLINE pageSize #-}
+
+pageOf :: Int -> Int -> Int
+pageOf bits index = index `shiftR` bits
+{-# INLINE pageOf #-}
+
+offsetOf :: Int -> Int -> Int
+offsetOf bits index = index .&. (pageSize bits - 1)
+{-# INLINE offsetOf #-}
+
+pagesFor :: Int -> Int -> Int
+pagesFor bits count
+  | count <= 0 = 0
+  | otherwise = pageOf bits (count - 1) + 1
+{-# INLINE pagesFor #-}
+
+pagedLength :: Paged a -> Int
+pagedLength paged = case paged of
+  FlatPaged count _ _ -> count
+  SharedPaged count _ _ _ -> count
+{-# INLINE pagedLength #-}
+
+pagedPageCount :: Paged a -> Int
+pagedPageCount paged = pagesFor (pagedBits paged) (pagedLength paged)
+{-# INLINE pagedPageCount #-}
+
+-- | Number of pages in the persistent overlay.  A flat root has no copied
+-- pages; a shared value records only pages dirtied by its transaction.  This
+-- is an observation for publication receipts, not a read-path traversal.
+pagedOverlayPageCount :: Paged a -> Int
+pagedOverlayPageCount paged = case paged of
+  FlatPaged{} -> 0
+  SharedPaged _ _ _ directory -> directorySize directory
+{-# INLINE pagedOverlayPageCount #-}
+
+pagedBits :: Paged a -> Int
+pagedBits paged = case paged of
+  FlatPaged _ bits _ -> bits
+  SharedPaged _ bits _ _ -> bits
+{-# INLINE pagedBits #-}
+
+instance (U.Unbox a, Eq a) => Eq (Paged a) where
+  left == right =
+    pagedLength left == pagedLength right
+      && case (left, right) of
+        (FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) -> leftValues == rightValues
+        _ -> pagedChunksEqual left right
+
+pagedChunksEqual :: forall a. (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool
+pagedChunksEqual left right =
+  foldl'
+    (\equal (index, width) ->
+       equal
+         && U.unsafeSlice 0 width (pageSlice left index)
+              == U.unsafeSlice 0 width (pageSlice right index))
+    True
+    chunks
+ where
+  !count = pagedLength left
+  !leftBits = pagedBits left
+  !rightBits = pagedBits right
+  chunks = unfoldr nextChunk 0
+
+  nextChunk !index
+    | index >= count = Nothing
+    | otherwise = Just ((index, width), index + width)
+    where
+      !leftPageEnd = (pageOf leftBits index + 1) * pageSize leftBits
+      !rightPageEnd = (pageOf rightBits index + 1) * pageSize rightBits
+      !width = min (count - index) (min (leftPageEnd - index) (rightPageEnd - index))
+  pageSlice :: Paged a -> Int -> U.Vector a
+  pageSlice paged index =
+    let !bits = pagedBits paged
+        !page = pageOf bits index
+        !offset = offsetOf bits index
+     in case paged of
+          FlatPaged _ _ values -> U.unsafeSlice index (U.length values - index) values
+          SharedPaged _ _ root directory ->
+            case lookupDirectory page directory of
+              Just values -> U.unsafeSlice offset (U.length values - offset) values
+              Nothing -> U.unsafeSlice index (U.length root - index) root
+
+instance (U.Unbox a, Show a) => Show (Paged a) where
+  showsPrec precedence = showsPrec precedence . U.toList . toVector
+
+instance NFData (Paged a) where
+  rnf paged = case paged of
+    FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()
+    SharedPaged count bits root directory ->
+      count `seq` bits `seq` root `seq` directory `seq` ()
+
+emptyPaged :: U.Unbox a => Paged a
+emptyPaged = FlatPaged 0 traversalPageBits U.empty
+
+fromVector :: U.Unbox a => a -> U.Vector a -> Paged a
+fromVector _padding values = FlatPaged (U.length values) traversalPageBits values
+
+fromLocalVector :: U.Unbox a => a -> U.Vector a -> Paged a
+fromLocalVector _padding values = FlatPaged (U.length values) localPageBits values
+
+toVector :: U.Unbox a => Paged a -> U.Vector a
+toVector paged = case paged of
+  FlatPaged count _ values -> U.unsafeSlice 0 count values
+  SharedPaged count bits _ _ ->
+    U.concat (map (pagedPageSlice paged) [0 .. pagesFor bits count - 1])
+
+pagedUnsafeIndex :: U.Unbox a => Paged a -> Int -> a
+pagedUnsafeIndex paged index =
+  case paged of
+    FlatPaged _ _ values -> U.unsafeIndex values index
+    SharedPaged _ bits root directory ->
+      case lookupDirectory (pageOf bits index) directory of
+        Just values -> U.unsafeIndex values (offsetOf bits index)
+        Nothing -> U.unsafeIndex root index
+{-# INLINE pagedUnsafeIndex #-}
+
+-- | The resident cells of one page, resolved by one directory descent. Every
+-- page below the length is either an overlay page or a run of the root.
+pagedPageSlice :: U.Unbox a => Paged a -> Int -> U.Vector a
+pagedPageSlice paged page =
+  let !bits = pagedBits paged
+      !start = page * pageSize bits
+      !width = min (pageSize bits) (pagedLength paged - start)
+   in case paged of
+        FlatPaged _ _ values -> U.unsafeSlice start width values
+        SharedPaged _ _ root directory ->
+          case lookupDirectory page directory of
+            Just values -> U.unsafeSlice 0 width values
+            Nothing -> U.unsafeSlice start width root
+{-# INLINE pagedPageSlice #-}
+
+-- | Fold directly over the physical sections. Unlike the old implementation,
+-- this does not allocate a complete vector merely to consume it.
+pagedFoldl' :: U.Unbox a => (b -> a -> b) -> b -> Paged a -> b
+pagedFoldl' step initial paged =
+  case paged of
+    FlatPaged count _ values -> U.foldl' step initial (U.take count values)
+    SharedPaged count bits _ _ ->
+      foldl'
+        (\accumulated page -> U.foldl' step accumulated (pagedPageSlice paged page))
+        initial
+        [0 .. pagesFor bits count - 1]
+{-# INLINE pagedFoldl' #-}
+
+-- | Consume the logical sequence in ascending order without gathering shared
+-- pages. A right fold lets producers compose their continuations rather than
+-- accumulating one left-associated action for the complete store.
+pagedFoldr :: U.Unbox a => (a -> b -> b) -> b -> Paged a -> b
+pagedFoldr step = pagedIFoldr (const step)
+{-# INLINE pagedFoldr #-}
+
+-- | The indexed producer has the same logical domain as 'pagedFoldr'. Indices
+-- are absolute positions, not page-relative offsets: field-major observers of
+-- interleaved storage therefore retain their order across page boundaries.
+pagedIFoldr :: U.Unbox a => (Int -> a -> b -> b) -> b -> Paged a -> b
+pagedIFoldr step initial paged =
+  case paged of
+    FlatPaged count _ values -> U.ifoldr step initial (U.take count values)
+    SharedPaged count bits _ _ ->
+      foldr
+        (\page rest ->
+           U.ifoldr
+             (\offset -> step (page * pageSize bits + offset))
+             rest
+             (pagedPageSlice paged page))
+        initial
+        [0 .. pagesFor bits count - 1]
+{-# INLINE pagedIFoldr #-}
+
+newMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
+newMutablePaged capacity = do
+  values <- MUV.new (max 0 capacity)
+  pure (MutableFlatPaged traversalPageBits values)
+
+newLocalMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
+newLocalMutablePaged capacity = do
+  values <- MUV.new (max 0 capacity)
+  pure (MutableFlatPaged localPageBits values)
+
+-- | Open a persistent transaction without constructing a page table over the
+-- resident mesh. The mutable dictionary starts empty and acquires storage only
+-- when a write first reaches a page.
+thawPaged :: Int -> Paged a -> ST s (MutablePaged s a)
+thawPaged _requestedCapacity paged = do
+  dirty <- newSTRef IntMap.empty
+  stats <- newSTRef emptyPublicationStats
+  pure (MutableSharedPaged (pagedBits paged) paged dirty stats)
+
+data TransactionShape = DenseTransaction | LocalTransaction
+
+thawPagedShaped :: U.Unbox a => TransactionShape -> Int -> Paged a -> ST s (MutablePaged s a)
+thawPagedShaped shape = case shape of
+  DenseTransaction -> thawPagedDense
+  LocalTransaction -> thawPaged
+{-# INLINE thawPagedShaped #-}
+
+thawPagedDense :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)
+thawPagedDense requestedCapacity paged = do
+  let !count = pagedLength paged
+      !capacity = max requestedCapacity count
+  values <- MUV.new capacity
+  U.copy (MUV.unsafeSlice 0 count values) (toVector paged)
+  pure (MutableFlatPaged (pagedBits paged) values)
+
+flatMutableSection :: MutablePaged s a -> Maybe (FlatMutablePaged s a)
+flatMutableSection mutable = case mutable of
+  MutableFlatPaged _ values -> Just (FlatMutablePaged values)
+  MutableSharedPaged{} -> Nothing
+{-# INLINE flatMutableSection #-}
+
+readFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> ST s a
+readFlatMutable (FlatMutablePaged values) = MUV.unsafeRead values
+{-# INLINE readFlatMutable #-}
+
+writeFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> a -> ST s ()
+writeFlatMutable (FlatMutablePaged values) = MUV.unsafeWrite values
+{-# INLINE writeFlatMutable #-}
+
+readPaged :: U.Unbox a => MutablePaged s a -> Int -> ST s a
+readPaged mutable index = case mutable of
+  MutableFlatPaged _ values -> MUV.unsafeRead values index
+  MutableSharedPaged bits base dirtyRef _ -> do
+    dirty <- readSTRef dirtyRef
+    case IntMap.lookup (pageOf bits index) dirty of
+      Just values -> MUV.unsafeRead values (offsetOf bits index)
+      Nothing -> pure (pagedUnsafeIndex base index)
+{-# INLINE readPaged #-}
+
+writePaged :: U.Unbox a => MutablePaged s a -> Int -> a -> ST s ()
+writePaged mutable index value = case mutable of
+  MutableFlatPaged _ values -> MUV.unsafeWrite values index value
+  MutableSharedPaged bits base dirtyRef statsRef -> do
+    let !page = pageOf bits index
+        !start = page * pageSize bits
+        !existingWidth = min (pageSize bits) (max 0 (pagedLength base - start))
+    dirty <- readSTRef dirtyRef
+    values <- case IntMap.lookup page dirty of
+      Just existing -> pure existing
+      Nothing -> do
+        pageValues <- MUV.new (pageSize bits)
+        when (existingWidth > 0) $
+            U.copy
+              (MUV.unsafeSlice 0 existingWidth pageValues)
+              (pagedPageSlice base page)
+        writeSTRef dirtyRef $! IntMap.insert page pageValues dirty
+        modifySTRef'
+          statsRef
+          (addPublicationStats
+             emptyPublicationStats
+               { publicationUnboxedBasePageOpens = if existingWidth > 0 then 1 else 0
+               , publicationUnboxedDirtyBasePages = if existingWidth > 0 then 1 else 0
+               , publicationUnboxedDirtyAppendedPages = if existingWidth > 0 then 0 else 1
+               , publicationUnboxedCopiedCells = existingWidth
+               })
+        pure pageValues
+    MUV.unsafeWrite values (offsetOf bits index) value
+{-# INLINE writePaged #-}
+
+-- | Publish only dirty pages. A local publication retains one immutable root
+-- and merges the changed page paths into its cumulative directory.
+freezePaged :: forall s a. U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a)
+freezePaged count mutable
+  | count <= 0 = pure emptyPaged
+  | otherwise = case mutable of
+      MutableFlatPaged bits values
+        | MUV.length values <= count + (count `quot` 8) ->
+            FlatPaged count bits . U.take count <$> U.unsafeFreeze values
+        | otherwise ->
+            FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)
+      MutableSharedPaged bits base dirtyRef _statsRef -> do
+        dirty <- readSTRef dirtyRef
+        if IntMap.null dirty && count == pagedLength base
+          then pure base
+          else do
+            frozen <- traverse freezePage (IntMap.toAscList dirty)
+            let !priorDirectory = sharedDirectory base
+                !directory =
+                  foldl'
+                    (\accumulated (page, values) -> insertDirectory page values accumulated)
+                    priorDirectory
+                    frozen
+                !root = sharedRoot base
+            pure (SharedPaged count bits root (directoryRestrict (pagesFor bits count) directory))
+ where
+  freezePage :: (Int, MUV.MVector s a) -> ST s (Int, U.Vector a)
+  freezePage (page, values) = do
+    frozen <- U.unsafeFreeze values
+    pure (page, frozen)
+
+sharedRoot :: Paged a -> U.Vector a
+sharedRoot paged = case paged of
+  FlatPaged _ _ values -> values
+  SharedPaged _ _ root _ -> root
+{-# INLINE sharedRoot #-}
+
+sharedDirectory :: Paged a -> PageDirectory (U.Vector a)
+sharedDirectory paged = case paged of
+  FlatPaged _ _ _ -> emptyDirectory
+  SharedPaged _ _ _ directory -> directory
+{-# INLINE sharedDirectory #-}
+
+readMutablePagedPublicationStats :: MutablePaged s a -> ST s PublicationStats
+readMutablePagedPublicationStats mutable = case mutable of
+  MutableFlatPaged _ _ -> pure emptyPublicationStats
+  MutableSharedPaged _ _ _ statsRef -> readSTRef statsRef
diff --git a/src-core/Moonlight/Planar/Internal/Point.hs b/src-core/Moonlight/Planar/Internal/Point.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Internal/Point.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+-- | Coordinate values and finite-query admission, independent of mesh storage.
+-- Trusted mesh owners may construct QueryPoint from already-admitted planes.
+module Moonlight.Planar.Internal.Point where
+
+import Control.DeepSeq (NFData (..))
+import Foreign.Ptr (castPtr)
+import Foreign.Storable (Storable (..), peekElemOff, pokeElemOff)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Scalar
+  ( CoordinateError, canonicalScalarZero, minimumAllowedCoordinate, validateCoordinate )
+
+-- | Cartesian binary64 point.
+data Point = Point
+  { pointX :: !Double
+  , pointY :: !Double
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A coordinate pair admitted to the exact-predicate domain and normalized
+-- at its construction boundary. Query algorithms consume this phase rather
+-- than each inventing a fallback for invalid floating-point input.
+newtype QueryPoint = QueryPoint Point
+  deriving stock (Eq, Ord, Show)
+
+instance NFData QueryPoint where
+  rnf (QueryPoint point) = rnf point
+  {-# INLINE rnf #-}
+
+-- | Read the admitted coordinate without exposing a record-update route around
+-- finite-coordinate admission and canonical normalization.
+queryPointValue :: QueryPoint -> Point
+queryPointValue (QueryPoint point) = point
+{-# INLINE queryPointValue #-}
+
+-- | Coordinate axis and reason that kept a point outside the query domain.
+data PointValidationError
+  = InvalidPointX !CoordinateError
+  | InvalidPointY !CoordinateError
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+instance Storable (Point) where
+  sizeOf _ = 2 * sizeOf (0 :: Double)
+  alignment _ = alignment (0 :: Double)
+  peek pointer = do
+    x <- peekElemOff (castPtr pointer) 0
+    y <- peekElemOff (castPtr pointer) 1
+    pure (Point x y)
+  poke pointer (Point x y) = do
+    pokeElemOff (castPtr pointer) 0 x
+    pokeElemOff (castPtr pointer) 1 y
+
+-- | Extract a vertex's position once, at the construction boundary.
+class HasPosition vertex where
+  position :: vertex -> Point
+
+instance HasPosition (Point) where
+  position = id
+
+-- | Admit and normalize a finite point for read-only geometric queries.
+mkQueryPoint :: Point -> Either PointValidationError (QueryPoint)
+mkQueryPoint point@(Point x y) = do
+  maybe (Right ()) (Left . InvalidPointX) (validateCoordinate x)
+  maybe (Right ()) (Left . InvalidPointY) (validateCoordinate y)
+  Right (QueryPoint (canonicalPoint point))
+
+-- | Round coordinates below the robust-predicate input floor toward zero.
+-- The operation never changes a coordinate already accepted by
+-- 'validateCoordinate'.
+mitigateUnderflow :: Point -> Point
+mitigateUnderflow (Point x y) = Point (mitigate x) (mitigate y)
+ where
+  mitigate :: Double -> Double
+  mitigate value
+    | value /= 0 && abs value < minimumAllowedCoordinate = 0
+    | otherwise = value
+
+-- | Canonicalize both coordinate components for point identity.
+canonicalPoint :: Point -> Point
+canonicalPoint (Point x y) = Point (canonicalScalarZero x) (canonicalScalarZero y)
+{-# INLINE canonicalPoint #-}
diff --git a/src-core/Moonlight/Planar/LineSideInfo.hs b/src-core/Moonlight/Planar/LineSideInfo.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/LineSideInfo.hs
@@ -0,0 +1,55 @@
+-- | Which side of a directed line a point falls on, as a total three-valued
+-- answer that carries the collinear case rather than rounding it away.
+module Moonlight.Planar.LineSideInfo
+  ( LineSideInfo
+  , fromDeterminant
+  , fromOrdering
+  , sideOrdering
+  , isOnLeftSide
+  , isOnRightSide
+  , isOnLine
+  , isOnLeftSideOrLine
+  , isOnRightSideOrLine
+  , reverseSide
+  ) where
+
+-- | A three-valued side: the collinear case is a value, not a rounding.
+newtype LineSideInfo = LineSideInfo Ordering
+  deriving stock (Show)
+  deriving newtype (Eq, Ord)
+
+-- | The side an orientation determinant's sign names.
+fromDeterminant :: Double -> LineSideInfo
+fromDeterminant value = LineSideInfo (compare value 0)
+{-# INLINE fromDeterminant #-}
+
+-- | The side an 'Ordering' names.
+fromOrdering :: Ordering -> LineSideInfo
+fromOrdering = LineSideInfo
+{-# INLINE fromOrdering #-}
+
+-- | The underlying 'Ordering'.
+sideOrdering :: LineSideInfo -> Ordering
+sideOrdering (LineSideInfo ordering) = ordering
+{-# INLINE sideOrdering #-}
+
+-- | The five side tests; the @OrLine@ pair admit the collinear case.
+isOnLeftSide, isOnRightSide, isOnLine, isOnLeftSideOrLine, isOnRightSideOrLine :: LineSideInfo -> Bool
+isOnLeftSide (LineSideInfo ordering) = ordering == GT
+isOnRightSide (LineSideInfo ordering) = ordering == LT
+isOnLine (LineSideInfo ordering) = ordering == EQ
+isOnLeftSideOrLine side = not (isOnRightSide side)
+isOnRightSideOrLine side = not (isOnLeftSide side)
+{-# INLINE isOnLeftSide #-}
+{-# INLINE isOnRightSide #-}
+{-# INLINE isOnLine #-}
+{-# INLINE isOnLeftSideOrLine #-}
+{-# INLINE isOnRightSideOrLine #-}
+
+-- | The same point, seen along the reversed line.
+reverseSide :: LineSideInfo -> LineSideInfo
+reverseSide (LineSideInfo ordering) = LineSideInfo $ case ordering of
+  LT -> GT
+  EQ -> EQ
+  GT -> LT
+{-# INLINE reverseSide #-}
diff --git a/src-core/Moonlight/Planar/Point.hs b/src-core/Moonlight/Planar/Point.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Point.hs
@@ -0,0 +1,13 @@
+-- | Binary64 points and sealed, normalized query coordinates.
+module Moonlight.Planar.Point
+  ( Point (..)
+  , QueryPoint
+  , queryPointValue
+  , PointValidationError (..)
+  , HasPosition (..)
+  , mkQueryPoint
+  , canonicalPoint
+  , mitigateUnderflow
+  ) where
+
+import Moonlight.Planar.Internal.Point
diff --git a/src-core/Moonlight/Planar/Scalar.hs b/src-core/Moonlight/Planar/Scalar.hs
new file mode 100644
--- /dev/null
+++ b/src-core/Moonlight/Planar/Scalar.hs
@@ -0,0 +1,198 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | The binary64 coordinate kernel and exact predicate boundary.
+module Moonlight.Planar.Scalar
+  ( scalarName
+  , scalarByteSize
+  , scalarBinaryFormat
+  , scalarEpsilon
+  , scalarUnitRoundoff
+  , scalarCcwErrorBound
+  , scalarInCircleErrorBound
+  , circumradiusSquaredWithinCoordinates
+  , BinaryFormat
+  , formatRadix
+  , formatMantissaDigits
+  , formatExponentRange
+  , minimumAllowedCoordinate
+  , maximumAllowedCoordinate
+  , isFinite
+  , canonicalScalarZero
+  , CoordinateError (..)
+  , validateCoordinate
+  , NonFiniteValue (..)
+  , classifyNonFinite
+  , RadiusSquared
+  , RadiusSquaredError (..)
+  , mkRadiusSquared
+  , radiusSquaredValue
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import GHC.Generics (Generic)
+import Moonlight.Planar.Internal.Dyadic
+  ( exactCircumradiusSquaredWithin
+  )
+
+-- | The coordinate component of canonical point identity. IEEE signed zeros
+-- compare equal but hash differently by bits; every coordinate-keyed owner
+-- therefore normalizes them before storage or hashing.
+canonicalScalarZero :: Double -> Double
+canonicalScalarZero value
+  | value == 0 = 0
+  | otherwise = value
+{-# INLINE canonicalScalarZero #-}
+
+-- | Machine format of the coordinate scalar.
+data BinaryFormat = BinaryFormat
+  { formatRadix :: !Integer
+    -- ^ Numeric base of the significand.
+  , formatMantissaDigits :: !Int
+    -- ^ Number of base-'formatRadix' digits in the significand.
+  , formatExponentRange :: !(Int, Int)
+    -- ^ Inclusive minimum and exclusive maximum exponent bounds.
+  }
+  deriving stock (Eq, Show)
+
+-- | Stable name of the coordinate scalar.
+scalarName :: String
+scalarName = "binary64"
+
+-- | Bytes occupied by one coordinate component.
+scalarByteSize :: Int
+scalarByteSize = 8
+
+-- | Runtime-confirmed binary format of 'Double'.
+scalarBinaryFormat :: BinaryFormat
+scalarBinaryFormat =
+  BinaryFormat
+    { formatRadix = floatRadix (0 :: Double)
+    , formatMantissaDigits = floatDigits (0 :: Double)
+    , formatExponentRange = floatRange (0 :: Double)
+    }
+
+-- | Difference between one and the next representable value above one.
+scalarEpsilon :: Double
+scalarEpsilon = 2.220446049250313e-16
+
+-- | Maximum relative rounding error of one binary64 operation.
+scalarUnitRoundoff :: Double
+scalarUnitRoundoff = 1.1102230246251565e-16
+
+-- | Error coefficient for the filtered orientation predicate.
+scalarCcwErrorBound :: Double
+scalarCcwErrorBound = 3.3306690738754716e-16
+
+-- | Error coefficient for the filtered in-circle predicate.
+scalarInCircleErrorBound :: Double
+scalarInCircleErrorBound = 1.1102230246251577e-15
+
+-- | Closed exact circumradius membership at a finite, non-negative binary64
+-- threshold. Invalid thresholds and collinear triples are outside.
+circumradiusSquaredWithinCoordinates
+  :: Double
+  -> Double -> Double -> Double -> Double -> Double -> Double
+  -> Bool
+circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy
+    | threshold < 0 || not (isFinite threshold) = False
+    | not (isFinite ax && isFinite ay && isFinite bx
+        && isFinite by && isFinite cx && isFinite cy) = False
+    | isFinite difference && isFinite tolerance
+        && tolerance > 0 && abs difference > tolerance = difference <= 0
+    | otherwise = exactCircumradiusSquaredWithin threshold ax ay bx by cx cy
+   where
+    !abx = bx - ax
+    !aby = by - ay
+    !acx = cx - ax
+    !acy = cy - ay
+    !bcx = cx - bx
+    !bcy = cy - by
+    !abSquared = abx * abx + aby * aby
+    !acSquared = acx * acx + acy * acy
+    !bcSquared = bcx * bcx + bcy * bcy
+    !determinant = abx * acy - aby * acx
+    !radiusNumerator = abSquared * acSquared * bcSquared
+    !thresholdDenominator = 4 * determinant * determinant * threshold
+    !difference = radiusNumerator - thresholdDenominator
+    !permanent = abs radiusNumerator + abs thresholdDenominator
+    !tolerance = 128 * scalarUnitRoundoff * permanent
+
+{-# INLINE circumradiusSquaredWithinCoordinates #-}
+
+-- | Whether a scalar is neither infinite nor NaN. Pure subtraction avoids the
+-- FFI calls used by base's predicates in the supported GHC.
+isFinite :: Double -> Bool
+isFinite value = value - value == 0
+{-# INLINE isFinite #-}
+
+-- | The smallest coordinate magnitude the exact predicates accept.
+minimumAllowedCoordinate :: Double
+minimumAllowedCoordinate = 1.793662034335766e-43
+
+-- | The largest coordinate magnitude the exact predicates accept.
+maximumAllowedCoordinate :: Double
+maximumAllowedCoordinate = 3.2138760885179806e60
+
+-- | Reason a floating-point coordinate cannot enter the exact-predicate domain.
+data CoordinateError
+  = CoordinateNaN
+  | CoordinateInfinite
+  | CoordinateTooSmall
+  | CoordinateTooLarge
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Classification retained when a numeric parameter is not finite.
+data NonFiniteValue
+  = ValueNaN
+  | ValuePositiveInfinity
+  | ValueNegativeInfinity
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Classify NaN and signed infinity, leaving finite values unclassified.
+classifyNonFinite :: Double -> Maybe NonFiniteValue
+classifyNonFinite value
+  | isNaN value = Just ValueNaN
+  | isInfinite value && value < 0 = Just ValueNegativeInfinity
+  | isInfinite value = Just ValuePositiveInfinity
+  | otherwise = Nothing
+
+-- | An admitted finite, non-negative squared radius. This belongs to the
+-- shared geometric vocabulary because both circle queries and alpha
+-- filtration consume it.
+newtype RadiusSquared = RadiusSquared Double
+  deriving stock (Eq, Ord, Show)
+
+instance NFData RadiusSquared where
+  rnf (RadiusSquared value) = rnf value
+
+-- | Typed refusal shared by every squared-radius consumer.
+data RadiusSquaredError
+  = NonFiniteRadiusSquared !NonFiniteValue
+  | NegativeRadiusSquared !Double
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Admit a finite, non-negative squared radius.
+mkRadiusSquared :: Double -> Either RadiusSquaredError RadiusSquared
+mkRadiusSquared value =
+  case classifyNonFinite value of
+    Just nonFinite -> Left (NonFiniteRadiusSquared nonFinite)
+    Nothing
+      | value < 0 -> Left (NegativeRadiusSquared value)
+      | otherwise -> Right (RadiusSquared value)
+
+-- | The admitted binary64 threshold.
+radiusSquaredValue :: RadiusSquared -> Double
+radiusSquaredValue (RadiusSquared value) = value
+
+validateCoordinate :: Double -> Maybe CoordinateError
+validateCoordinate value
+  | isNaN value = Just CoordinateNaN
+  | isInfinite value = Just CoordinateInfinite
+  | value /= 0 && abs value < minimumAllowedCoordinate = Just CoordinateTooSmall
+  | abs value > maximumAllowedCoordinate = Just CoordinateTooLarge
+  | otherwise = Nothing
diff --git a/src-core/Moonlight/Triangulation/Internal/BoxedPaged.hs b/src-core/Moonlight/Triangulation/Internal/BoxedPaged.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/BoxedPaged.hs
+++ /dev/null
@@ -1,452 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
-module Moonlight.Triangulation.Internal.BoxedPaged
-  ( BoxedPaged
-  , MutableBoxedPaged
-  , BoxedStorageError (..)
-  , emptyBoxedPaged
-  , boxedFill
-  , boxedDefaulted
-  , boxedMaterializedPageCount
-  , boxedPagedLength
-  , boxedUnsafeIndex
-  , boxedFromVector
-  , boxedToVector
-  , thawBoxedPaged
-  , readMutableBoxedPublicationStats
-  , readBoxedPaged
-  , writeBoxedPaged
-  , resetBoxedRange
-  , boxedThawPristine
-  , freezeBoxedPaged
-  , boxedUpdate
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.Bits (shiftL, shiftR, (.&.))
-import Data.Foldable (traverse_)
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.List as List
-import Data.STRef
-import qualified Data.Vector as V
-import qualified Data.Vector.Mutable as MV
-import Moonlight.Triangulation.Internal.PageDirectory
-import Moonlight.Triangulation.Internal.Paged
-  ( PublicationStats (..)
-  , TransactionShape (..)
-  , addPublicationStats
-  , emptyPublicationStats
-  )
-
-import GHC.Generics (Generic)
-
--- | A paged payload store in which an absent page means every slot in it holds
--- the fill. The fill belongs to the store rather than to a thaw of it: a
--- payload component either has a default that new elements inherit — the three
--- element payloads do — or it has none and every slot must be written before it
--- is read, which is the vertex component, whose payloads arrive with it.
---
--- The near-universal instantiation is @() () ()@, and under an unconditional
--- representation that costs one pointer per directed edge, undirected edge and
--- face, all aimed at the same closure. Here it costs nothing: an element
--- created at its default writes no slot, so no page is ever materialized.
-data BoxedStorage a
-  = DenseBoxedPages !(V.Vector (V.Vector a))
-  | DefaultedBoxedPages !a !(PageDirectory (V.Vector a))
-  deriving stock (Generic, Functor)
-  deriving anyclass (NFData)
-
-data BoxedPaged a = BoxedPaged
-  { boxedLength :: {-# UNPACK #-} !Int
-  , boxedStorage :: !(BoxedStorage a)
-  }
-  deriving stock (Generic, Functor)
-  deriving anyclass (NFData)
-
-data BoxedStorageError
-  = BoxedFreezeLengthNegative {-# UNPACK #-} !Int
-  | BoxedFreezeDensePageMissing {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-boxedFill :: BoxedPaged a -> Maybe a
-boxedFill BoxedPaged{boxedStorage = DenseBoxedPages _} = Nothing
-boxedFill BoxedPaged{boxedStorage = DefaultedBoxedPages fill _} = Just fill
-
--- | Number of materialized payload pages.  Defaulted stores report only their
--- sparse directory; an absent page is the fill and was never copied.
-boxedMaterializedPageCount :: BoxedPaged a -> Int
-boxedMaterializedPageCount BoxedPaged{boxedStorage} = case boxedStorage of
-  DenseBoxedPages pages -> V.length pages
-  DefaultedBoxedPages _ pages -> directorySize pages
-{-# INLINE boxedMaterializedPageCount #-}
-
--- | A length-bearing payload section whose every slot is its default.  The
--- section deliberately owns no materialized pages; a geometry-only
--- publication can therefore carry an arbitrarily large unit payload plane
--- without forcing a dense boxed vector at its next freeze.
-boxedDefaulted :: a -> Int -> BoxedPaged a
-boxedDefaulted fill length' =
-  BoxedPaged
-    { boxedLength = max 0 length'
-    , boxedStorage = DefaultedBoxedPages fill emptyDirectory
-    }
-
--- Structural equality would distinguish a store that materialized a page of
--- defaults from one that did not, and the two are the same store. Equality is
--- therefore what can be observed: the length, the fill every unwritten slot
--- will report, and the sequence itself.
-instance Eq a => Eq (BoxedPaged a) where
-  left == right =
-    boxedLength left == boxedLength right
-      && boxedFill left == boxedFill right
-      && boxedToVector left == boxedToVector right
-
-instance Show a => Show (BoxedPaged a) where
-  showsPrec precedence paged =
-    showParen (precedence > 10) $
-      showString "BoxedPaged "
-        . showsPrec 11 (boxedFill paged)
-        . showString " "
-        . showsPrec 11 (boxedToVector paged)
-
-data MutableBoxedPaged s a = MutableBoxedPaged
-  { mutableBoxedBase :: !(BoxedPaged a)
-  , mutableBoxedDirty :: !(STRef s (IntMap.IntMap (MV.MVector s a)))
-  , mutableBoxedStats :: !(Maybe (STRef s PublicationStats))
-  , -- | The thaw found no materialized page. Together with an empty dirty set
-    -- this says every slot in the store reports the fill, which is the state a
-    -- payload component nobody writes stays in for the whole transaction —
-    -- the near-universal @() () ()@ case. Read through 'boxedThawPristine'.
-    mutableBoxedPristineBase :: !Bool
-  }
-
-pageBits :: Int
-pageBits = 8
-{-# INLINE pageBits #-}
-
-pageSize :: Int
-pageSize = 1 `shiftL` pageBits
-{-# INLINE pageSize #-}
-
--- Every index this store decomposes is an element identifier or a walk over
--- one, so the signed division a 'quotRem' would emit corrects for a sign that
--- cannot occur.
-pageOf :: Int -> Int
-pageOf index = index `shiftR` pageBits
-{-# INLINE pageOf #-}
-
-offsetOf :: Int -> Int
-offsetOf index = index .&. (pageSize - 1)
-{-# INLINE offsetOf #-}
-
-emptyBoxedPaged :: Maybe a -> BoxedPaged a
-emptyBoxedPaged fill =
-  BoxedPaged
-    0
-    (case fill of
-       Nothing -> DenseBoxedPages V.empty
-       Just value -> DefaultedBoxedPages value emptyDirectory)
-
-boxedPagedLength :: BoxedPaged a -> Int
-boxedPagedLength = boxedLength
-{-# INLINE boxedPagedLength #-}
-
--- | Index with a handle already admitted by the owning triangulation. Dense
--- stores carry every live page explicitly; defaulted stores can answer an
--- absent or short page from their stored default. The handle domain is owned
--- by the DCEL, so this raw kernel performs no second, disagreeing bounds check.
-boxedUnsafeIndex :: BoxedPaged a -> Int -> a
-boxedUnsafeIndex BoxedPaged{boxedStorage} index =
-  let !page = pageOf index
-      !offset = offsetOf index
-   in case boxedStorage of
-        DenseBoxedPages pages ->
-          V.unsafeIndex (V.unsafeIndex pages page) offset
-        DefaultedBoxedPages fill pages ->
-          case lookupDirectory page pages of
-            Just values | offset < V.length values -> V.unsafeIndex values offset
-            _ -> fill
-{-# INLINE boxedUnsafeIndex #-}
-
-boxedFromVector :: Maybe a -> V.Vector a -> BoxedPaged a
-boxedFromVector fill values =
-  let !chunks =
-        List.unfoldr
-          (\offset ->
-             if offset >= V.length values
-               then Nothing
-               else
-                 let !page = pageOf offset
-                     !count = min pageSize (V.length values - offset)
-                     !chunk = V.slice offset count values
-                  in Just ((page, chunk), offset + count))
-          0
-   in
-  BoxedPaged
-    { boxedLength = V.length values
-    , boxedStorage =
-        case fill of
-          Nothing -> DenseBoxedPages (V.fromList (map snd chunks))
-          Just value -> DefaultedBoxedPages value (directoryFromAscList chunks)
-    }
-
--- Resolved a page at a time. Asking the directory per element paid a descent
--- for every slot of a run the descent had already found.
-boxedToVector :: forall a. BoxedPaged a -> V.Vector a
-boxedToVector BoxedPaged{boxedLength, boxedStorage}
-  | boxedLength <= 0 = V.empty
-  | otherwise = case boxedStorage of
-      DenseBoxedPages pages -> V.take boxedLength (V.concat (V.toList pages))
-      DefaultedBoxedPages fill pages ->
-        V.concat (map (pageRun fill pages) [0 .. pageOf (boxedLength - 1)])
- where
-  pageRun :: a -> PageDirectory (V.Vector a) -> Int -> V.Vector a
-  pageRun fill pages page =
-    let !base = page `shiftL` pageBits
-        !width = min pageSize (boxedLength - base)
-     in case lookupDirectory page pages of
-          Just values
-            | V.length values >= width -> V.slice 0 width values
-            | otherwise -> values V.++ V.replicate (width - V.length values) fill
-          Nothing -> V.replicate width fill
-
--- The fill comes from the store, not from the caller. A thaw that took it as an
--- argument obliged every call site to name the right one, and 'boxedUpdate' —
--- which passed the value being written — named the wrong one. That was harmless
--- only while pages were never absent.
-thawBoxedPaged :: TransactionShape -> BoxedPaged a -> ST s (MutableBoxedPaged s a)
-thawBoxedPaged shape base = do
-  mutableBoxedDirty <- newSTRef IntMap.empty
-  mutableBoxedStats <-
-    case shape of
-      DenseTransaction -> pure Nothing
-      LocalTransaction -> Just <$> newSTRef emptyPublicationStats
-  pure
-    MutableBoxedPaged
-      { mutableBoxedBase = base
-      , mutableBoxedDirty
-      , mutableBoxedStats
-      , mutableBoxedPristineBase = case boxedStorage base of
-          DenseBoxedPages pages -> V.null pages
-          DefaultedBoxedPages _ pages -> directorySize pages == 0
-      }
-
-readBoxedPaged :: MutableBoxedPaged s a -> Int -> ST s a
-readBoxedPaged MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty} index = do
-  dirty <- readSTRef mutableBoxedDirty
-  let !page = pageOf index
-      !offset = offsetOf index
-  case IntMap.lookup page dirty of
-    Just values -> MV.unsafeRead values offset
-    Nothing -> case boxedStorage mutableBoxedBase of
-      DenseBoxedPages pages ->
-        pure (V.unsafeIndex (V.unsafeIndex pages page) offset)
-      DefaultedBoxedPages fill pages ->
-        case lookupDirectory page pages of
-          Just values | offset < V.length values -> pure (V.unsafeIndex values offset)
-          _ -> pure fill
-{-# INLINE readBoxedPaged #-}
-
-writeBoxedPaged :: MutableBoxedPaged s a -> Int -> a -> ST s ()
-writeBoxedPaged paged index value = do
-  let !page = pageOf index
-      !offset = offsetOf index
-  values <- ensureMutableBoxedPage paged page
-  MV.unsafeWrite values offset value
-{-# INLINE writeBoxedPaged #-}
-
--- | Whether the thaw found no materialized page. With no writer into the
--- element payload planes inside a transaction — there is none; they are
--- written only through the persistent setters, outside one — this answers for
--- the whole transaction that every slot reports the fill, so a rewrite has
--- nothing to return and a relocation has nothing to move. It is why the
--- near-universal @() () ()@ mesh pays a predictable branch and no page.
-boxedThawPristine :: MutableBoxedPaged s a -> Bool
-boxedThawPristine = mutableBoxedPristineBase
-{-# INLINE boxedThawPristine #-}
-
--- | Return a contiguous run of slots to the store's fill. A slot no write has
--- reached already reports the fill, so a page absent from both the base and the
--- dirty set is left absent: recycling an element at its default keeps the store
--- sparse. Only a page holding written values is materialized and overwritten.
-resetBoxedRange :: MutableBoxedPaged s a -> a -> Int -> Int -> ST s ()
-resetBoxedRange paged@MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty} fill start count
-  | count <= 0 = pure ()
-  | otherwise = traverse_ resetChunk chunkStarts
- where
-  !end = start + count
-  chunkStarts =
-    List.unfoldr
-      (\index ->
-         if index >= end
-           then Nothing
-           else
-             let !width = min (pageSize - offsetOf index) (end - index)
-              in Just ((index, width), index + width))
-      start
-
-  resetChunk (index, width) = do
-    dirty <- readSTRef mutableBoxedDirty
-    let !page = pageOf index
-        !offset = offsetOf index
-        !written =
-          IntMap.member page dirty
-            || case boxedStorage mutableBoxedBase of
-              DenseBoxedPages pages ->
-                page < V.length pages
-                  && offset < V.length (V.unsafeIndex pages page)
-              DefaultedBoxedPages _ pages ->
-                case lookupDirectory page pages of
-                  Just values -> offset < V.length values
-                  Nothing -> False
-    when written $ do
-      values <- ensureMutableBoxedPage paged page
-      MV.set (MV.slice offset width values) fill
-
-ensureMutableBoxedPage :: forall s a. MutableBoxedPaged s a -> Int -> ST s (MV.MVector s a)
-ensureMutableBoxedPage MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats} page = do
-  dirty <- readSTRef mutableBoxedDirty
-  case IntMap.lookup page dirty of
-    Just values -> pure values
-    Nothing -> do
-      -- Copying a page slot by slot writes every element twice — once to
-      -- establish the fill and once to overwrite it — and pays a write barrier
-      -- per store. A full page is one thaw; a short tail page fills only the
-      -- slots the base does not reach. This is the same shape the unboxed
-      -- store already uses.
-      (values, copiedCells) <- case boxedStorage mutableBoxedBase of
-        DenseBoxedPages pages
-          | page < V.length pages -> copyExisting (V.unsafeIndex pages page)
-          | otherwise -> do
-              vector <- MV.new pageSize
-              pure (vector, 0)
-        DefaultedBoxedPages fill pages ->
-          case lookupDirectory page pages of
-            Just old -> copyExisting old
-            Nothing -> do
-              vector <- MV.replicate pageSize fill
-              pure (vector, 0)
-      writeSTRef mutableBoxedDirty $! IntMap.insert page values dirty
-      let !basePageCount = boxedPageCount mutableBoxedBase
-          !basePage = page < basePageCount
-      recordPublicationStats
-        mutableBoxedStats
-        (emptyPublicationStats
-             { publicationBoxedBasePageOpens = if basePage then 1 else 0
-             , publicationBoxedDirtyBasePages = if basePage then 1 else 0
-             , publicationBoxedDirtyAppendedPages = if basePage then 0 else 1
-             , publicationBoxedCopiedCells = copiedCells
-             })
-      pure values
- where
-  boxedPageCount :: BoxedPaged a -> Int
-  boxedPageCount base
-    | boxedLength base <= 0 = 0
-    | otherwise = pageOf (boxedLength base - 1) + 1
-
-  copyExisting :: V.Vector a -> ST s (MV.MVector s a, Int)
-  copyExisting old
-    | V.length old == pageSize = (, V.length old) <$> V.thaw old
-    | otherwise = do
-        vector <- case boxedStorage mutableBoxedBase of
-          DenseBoxedPages _ -> MV.new pageSize
-          DefaultedBoxedPages fill _ -> MV.replicate pageSize fill
-        V.copy (MV.slice 0 (V.length old) vector) old
-        pure (vector, V.length old)
-
-recordPublicationStats :: Maybe (STRef s PublicationStats) -> PublicationStats -> ST s ()
-recordPublicationStats Nothing _ = pure ()
-recordPublicationStats (Just statsRef) delta = modifySTRef' statsRef (addPublicationStats delta)
-{-# INLINE recordPublicationStats #-}
-
-freezeBoxedPaged :: forall s a. Int -> MutableBoxedPaged s a -> ST s (Either BoxedStorageError (BoxedPaged a))
-freezeBoxedPaged length' MutableBoxedPaged{mutableBoxedBase, mutableBoxedDirty, mutableBoxedStats}
-  | length' < 0 = pure (Left (BoxedFreezeLengthNegative length'))
-  | otherwise = do
-      dirty <- readSTRef mutableBoxedDirty
-      frozenDirty <- traverse V.unsafeFreeze dirty
-      case boxedStorage mutableBoxedBase of
-        DenseBoxedPages _ ->
-          recordPublicationStats
-            mutableBoxedStats
-            (emptyPublicationStats
-                 { publicationBoxedBasePageEnumerations = pageCount
-                 , publicationBoxedBasePageFreezes = pageCount
-                 })
-        DefaultedBoxedPages _ _ -> pure ()
-      pure $ case boxedStorage mutableBoxedBase of
-        DenseBoxedPages basePages -> do
-          pages <-
-            traverse
-              (densePage frozenDirty basePages)
-              [0 .. pageCount - 1]
-          pure
-            BoxedPaged
-              { boxedLength = length'
-              , boxedStorage = DenseBoxedPages (V.fromList pages)
-              }
-        DefaultedBoxedPages fill basePages ->
-          let !merged =
-                List.foldl'
-                  (\pages (page, values) -> insertDirectory page values pages)
-                  basePages
-                  (IntMap.toAscList frozenDirty)
-              !kept = directoryRestrict pageCount merged
-              !trimmed = case directoryLookupMax kept of
-                Nothing -> kept
-                Just (lastPage, values) ->
-                  let !lastLength = length' - lastPage * pageSize
-                   in if lastLength < V.length values
-                        then insertDirectory lastPage (V.take lastLength values) kept
-                        else kept
-           in Right
-                BoxedPaged
-                  { boxedLength = length'
-                  , boxedStorage = DefaultedBoxedPages fill trimmed
-                }
- where
-  !pageCount = if length' <= 0 then 0 else pageOf (length' - 1) + 1
-
-  densePage
-    :: IntMap.IntMap (V.Vector a)
-    -> V.Vector (V.Vector a)
-    -> Int
-    -> Either BoxedStorageError (V.Vector a)
-  densePage frozenDirty basePages page =
-    let !width = min pageSize (length' - page * pageSize)
-     in case IntMap.lookup page frozenDirty of
-          Just values -> Right (V.take width values)
-          Nothing -> case basePages V.!? page of
-            Just values -> Right (V.take width values)
-            Nothing -> Left (BoxedFreezeDensePageMissing page pageCount)
-
-readMutableBoxedPublicationStats :: MutableBoxedPaged s a -> ST s PublicationStats
-readMutableBoxedPublicationStats MutableBoxedPaged{mutableBoxedStats} =
-  maybe (pure emptyPublicationStats) readSTRef mutableBoxedStats
-
-boxedUpdate :: Int -> a -> BoxedPaged a -> BoxedPaged a
-boxedUpdate index value source@BoxedPaged{boxedStorage} =
-  source{boxedStorage = updateStorage boxedStorage}
- where
-  !page = pageOf index
-  !offset = offsetOf index
-
-  updateStorage (DenseBoxedPages pages) =
-    let !values = V.unsafeIndex pages page
-        !updated = V.modify (\mutable -> MV.unsafeWrite mutable offset value) values
-     in DenseBoxedPages (V.modify (\mutable -> MV.unsafeWrite mutable page updated) pages)
-  updateStorage (DefaultedBoxedPages fill pages) =
-    let !values = case lookupDirectory page pages of
-          Just existing
-            | V.length existing == pageSize -> existing
-            | otherwise -> existing V.++ V.replicate (pageSize - V.length existing) fill
-          Nothing -> V.replicate pageSize fill
-        !updated = V.modify (\mutable -> MV.unsafeWrite mutable offset value) values
-     in DefaultedBoxedPages fill (insertDirectory page updated pages)
diff --git a/src-core/Moonlight/Triangulation/Internal/Dyadic.hs b/src-core/Moonlight/Triangulation/Internal/Dyadic.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/Dyadic.hs
+++ /dev/null
@@ -1,747 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE CPP #-}
-{-# LANGUAGE MagicHash #-}
-{-# LANGUAGE UnboxedTuples #-}
-
-module Moonlight.Triangulation.Internal.Dyadic
-  ( exactOrientDet
-  , exactOrientSignDouble
-  , exactInCircleDet
-  , exactQuarterSquaredDistanceRational
-  , exactCircumradiusSquaredRational
-  , exactCircumradiusSquaredWithin
-  , exactBarycentricDeterminants
-  , exactDiametralDot
-  , integerRatioToDouble
-  , integerBitLength
-  ) where
-
-import Data.Bits
-  ( countLeadingZeros
-  , countTrailingZeros
-  , finiteBitSize
-  , shiftL
-  , shiftR
-  )
-import qualified Data.List as List
-import Data.Word (Word64)
-import GHC.Exts (Int (I#), (+#))
-import GHC.Integer.Logarithms (integerLog2#)
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactArithmeticError
-  , ExactRational
-  , exactRationalFromDyadic
-  , exactRationalFromDyadicRatio
-  )
-#if WORD_SIZE_IN_BITS == 64
-import GHC.Exts
-  ( Double (D#)
-  , Double#
-  , Int#
-  , Word#
-  , and#
-  , castDoubleToWord64#
-  , eqWord#
-  , gtWord#
-  , int2Word#
-  , isTrue#
-  , or#
-  , plusWord#
-  , plusWord2#
-  , subWordC#
-  , timesWord2#
-  , uncheckedShiftL#
-  , uncheckedShiftRL#
-  , word2Int#
-  , word64ToWord#
-  , (*#)
-  , (-#)
-  , (<=#)
-  , (==#)
-  , (>#)
-  , (>=#)
-  )
-#endif
-
--- Every finite binary64 value is a dyadic rational. Aligning all mantissas to
--- one exponent gives exact integer predicates without constructing Rational
--- expression trees.
-type Decoded = (Integer, Int)
-
-commonExponent :: [Decoded] -> Int
-commonExponent = List.foldl' step 0
- where
-  step :: Int -> Decoded -> Int
-  step !current (!mantissa, !power)
-    | mantissa == 0 = current
-    | otherwise = min current power
-{-# INLINE commonExponent #-}
-
-alignDecoded :: Int -> Decoded -> Integer
-alignDecoded !power (!mantissa, !sourcePower)
-  | mantissa == 0 = 0
-  | otherwise = mantissa `shiftL` (sourcePower - power)
-{-# INLINE alignDecoded #-}
-
-decodedExponentFloor :: Decoded -> Int
-decodedExponentFloor (mantissa, power)
-  | mantissa == 0 = 0
-  | otherwise = min 0 power
-{-# INLINE decodedExponentFloor #-}
-
-commonExponent4 :: Decoded -> Decoded -> Decoded -> Decoded -> Int
-commonExponent4 firstValue secondValue thirdValue fourthValue =
-  min
-    (min (decodedExponentFloor firstValue) (decodedExponentFloor secondValue))
-    (min (decodedExponentFloor thirdValue) (decodedExponentFloor fourthValue))
-{-# INLINE commonExponent4 #-}
-
-commonExponent6
-  :: Decoded
-  -> Decoded
-  -> Decoded
-  -> Decoded
-  -> Decoded
-  -> Decoded
-  -> Int
-commonExponent6 firstValue secondValue thirdValue fourthValue fifthValue sixthValue =
-  min
-    ( commonExponent4
-        firstValue
-        secondValue
-        thirdValue
-        fourthValue
-    )
-    (min (decodedExponentFloor fifthValue) (decodedExponentFloor sixthValue))
-{-# INLINE commonExponent6 #-}
-
-aligned4
-  :: Double -> Double -> Double -> Double
-  -> (Int, Integer, Integer, Integer, Integer)
-aligned4 a b c d =
-  let !da = decodeFloat a
-      !db = decodeFloat b
-      !dc = decodeFloat c
-      !dd = decodeFloat d
-      !power = commonExponent4 da db dc dd
-   in ( power
-      , alignDecoded power da
-      , alignDecoded power db
-      , alignDecoded power dc
-      , alignDecoded power dd
-      )
-{-# INLINE aligned4 #-}
-
-aligned6
-  :: Double -> Double -> Double -> Double -> Double -> Double
-  -> (Int, Integer, Integer, Integer, Integer, Integer, Integer)
-aligned6 a b c d e f =
-  let !da = decodeFloat a
-      !db = decodeFloat b
-      !dc = decodeFloat c
-      !dd = decodeFloat d
-      !de = decodeFloat e
-      !df = decodeFloat f
-      !power = commonExponent6 da db dc dd de df
-   in ( power
-      , alignDecoded power da
-      , alignDecoded power db
-      , alignDecoded power dc
-      , alignDecoded power dd
-      , alignDecoded power de
-      , alignDecoded power df
-      )
-
-aligned8
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
-  -> (Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer)
-aligned8 a b c d e f g h =
-  let !da = decodeFloat a
-      !db = decodeFloat b
-      !dc = decodeFloat c
-      !dd = decodeFloat d
-      !de = decodeFloat e
-      !df = decodeFloat f
-      !dg = decodeFloat g
-      !dh = decodeFloat h
-      !power = commonExponent [da, db, dc, dd, de, df, dg, dh]
-   in ( alignDecoded power da
-      , alignDecoded power db
-      , alignDecoded power dc
-      , alignDecoded power dd
-      , alignDecoded power de
-      , alignDecoded power df
-      , alignDecoded power dg
-      , alignDecoded power dh
-      )
-
-exactOrientDet
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Integer
-exactOrientDet ax ay bx by cx cy =
-  let (!_, !iax, !iay, !ibx, !iby, !icx, !icy) = aligned6 ax ay bx by cx cy
-      !acx = iax - icx
-      !acy = iay - icy
-      !bcx = ibx - icx
-      !bcy = iby - icy
-   in acx * bcy - acy * bcx
-
--- | One quarter of the exact squared distance between two finite binary64
--- points. Alpha's Gabriel-edge birth needs this value directly, so the dyadic
--- exponent absorbs the quarter before the single rational normalization.
-exactQuarterSquaredDistanceRational
-  :: Double -> Double -> Double -> Double -> ExactRational
-exactQuarterSquaredDistanceRational ax ay bx by =
-  let (!coordinatePower, !iax, !iay, !ibx, !iby) = aligned4 ax ay bx by
-      !deltaX = ibx - iax
-      !deltaY = iby - iay
-      !squaredDistance = deltaX * deltaX + deltaY * deltaY
-   in exactRationalFromDyadic squaredDistance (2 * coordinatePower - 2)
-{-# INLINE exactQuarterSquaredDistanceRational #-}
-
--- | Exact squared circumradius of three finite binary64 points. The local
--- dyadic section computes the integer numerator and denominator without
--- constructing intermediate ratios; descent normalizes the authoritative
--- result exactly once. A degenerate triangle is the typed zero-divisor
--- obstruction from 'ExactRational'.
-exactCircumradiusSquaredRational
-  :: Double -> Double -> Double -> Double -> Double -> Double
-  -> Either ExactArithmeticError ExactRational
-exactCircumradiusSquaredRational ax ay bx by cx cy =
-  let (!coordinatePower, !iax, !iay, !ibx, !iby, !icx, !icy) =
-        aligned6 ax ay bx by cx cy
-      !abx = ibx - iax
-      !aby = iby - iay
-      !acx = icx - iax
-      !acy = icy - iay
-      !bcx = icx - ibx
-      !bcy = icy - iby
-      !abSquared = abx * abx + aby * aby
-      !acSquared = acx * acx + acy * acy
-      !bcSquared = bcx * bcx + bcy * bcy
-      !determinant = abx * acy - aby * acx
-      !radiusNumerator = abSquared * acSquared * bcSquared
-      !radiusDenominator = 4 * determinant * determinant
-   in exactRationalFromDyadicRatio
-        radiusNumerator
-        radiusDenominator
-        (2 * coordinatePower)
-{-# INLINE exactCircumradiusSquaredRational #-}
-
--- | Exact closed comparison of squared circumradius with a finite,
--- non-negative binary64 threshold. No constructed circumcenter participates.
-exactCircumradiusSquaredWithin
-  :: Double
-  -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Bool
-exactCircumradiusSquaredWithin
-  threshold
-  ax ay bx by cx cy =
-    let (!thresholdMantissa, !thresholdPower) = decodeFloat threshold
-     in smallIntegralCircumradiusSquaredWithin
-          (fromInteger thresholdMantissa)
-          thresholdPower
-          ax ay bx by cx cy
-{-# INLINE exactCircumradiusSquaredWithin #-}
-
-arbitraryCircumradiusSquaredWithin
-  :: Integer
-  -> Int
-  -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Bool
-arbitraryCircumradiusSquaredWithin
-  thresholdMantissa
-  thresholdPower
-  ax ay bx by cx cy =
-  let (!coordinatePower, !iax, !iay, !ibx, !iby, !icx, !icy) =
-        aligned6 ax ay bx by cx cy
-      !abx = ibx - iax
-      !aby = iby - iay
-      !acx = icx - iax
-      !acy = icy - iay
-      !bcx = icx - ibx
-      !bcy = icy - iby
-      !abSquared = abx * abx + aby * aby
-      !acSquared = acx * acx + acy * acy
-      !bcSquared = bcx * bcx + bcy * bcy
-      !determinant = abx * acy - aby * acx
-      !radiusNumerator = abSquared * acSquared * bcSquared
-      !thresholdDenominator =
-        4 * determinant * determinant * thresholdMantissa
-   in determinant /= 0
-        && compareDyadic
-          radiusNumerator
-          (6 * coordinatePower)
-          thresholdDenominator
-          (4 * coordinatePower + thresholdPower)
-          /= GT
-
--- Integer-sized edges cover grid, pixel, and indexed-world faces without
--- allocating arbitrary-precision mantissas. Larger or fractional edges
--- descend to the general dyadic comparison above.
-smallIntegralCircumradiusSquaredWithin
-  :: Word64
-  -> Int
-  -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Bool
-smallIntegralCircumradiusSquaredWithin
-  rawThresholdMantissa
-  rawThresholdPower
-  ax ay bx by cx cy =
-    if not admittedDifferences
-      then
-        arbitraryCircumradiusSquaredWithin
-          (toInteger rawThresholdMantissa)
-          rawThresholdPower
-          ax ay bx by cx cy
-      else
-        let !abSquared = squaredIntegralLength abx aby
-            !acSquared = squaredIntegralLength acx acy
-            !bcSquared = squaredIntegralLength bcx bcy
-            !radiusNumerator = abSquared * acSquared * bcSquared
-            !determinant = abx * acy - aby * acx
-            !determinantMagnitude = fromIntegral (abs determinant)
-         in if determinant == 0 || rawThresholdMantissa == 0
-              then False
-              else
-                let !trailingZeros = countTrailingZeros rawThresholdMantissa
-                    !thresholdMantissa = rawThresholdMantissa `shiftR` trailingZeros
-                    !thresholdPower = rawThresholdPower + trailingZeros
-                    !determinantSquared = determinantMagnitude * determinantMagnitude
-                    !scaledDeterminant = 4 * determinantSquared
-                    !productsFit =
-                      determinantMagnitude <= maxBound `quot` determinantMagnitude
-                        && determinantSquared <= maxBound `quot` 4
-                        && thresholdMantissa <= maxBound `quot` scaledDeterminant
-                 in if productsFit
-                      then
-                        compareWordDyadic
-                          radiusNumerator
-                          0
-                          (scaledDeterminant * thresholdMantissa)
-                          thresholdPower
-                          /= GT
-                      else
-                        arbitraryCircumradiusSquaredWithin
-                          (toInteger rawThresholdMantissa)
-                          rawThresholdPower
-                          ax ay bx by cx cy
- where
-  !abxValue = bx - ax
-  !abyValue = by - ay
-  !acxValue = cx - ax
-  !acyValue = cy - ay
-  !abx = truncate abxValue
-  !aby = truncate abyValue
-  !acx = truncate acxValue
-  !acy = truncate acyValue
-  !bcx = acx - abx
-  !bcy = acy - aby
-  admittedDifferences =
-    integralDifference abxValue abx
-      && integralDifference abyValue aby
-      && integralDifference acxValue acx
-      && integralDifference acyValue acy
-{-# INLINE smallIntegralCircumradiusSquaredWithin #-}
-
-integralDifference :: Double -> Int -> Bool
-integralDifference value integral =
-  abs value <= 512 && fromIntegral integral == value
-{-# INLINE integralDifference #-}
-
-squaredIntegralLength :: Int -> Int -> Word64
-squaredIntegralLength x y =
-  let !xMagnitude = fromIntegral (abs x)
-      !yMagnitude = fromIntegral (abs y)
-   in xMagnitude * xMagnitude + yMagnitude * yMagnitude
-{-# INLINE squaredIntegralLength #-}
-
-compareWordDyadic :: Word64 -> Int -> Word64 -> Int -> Ordering
-compareWordDyadic left leftPower right rightPower
-  | left == 0 = compare left right
-  | right == 0 = GT
-  | leftMagnitude /= rightMagnitude = compare leftMagnitude rightMagnitude
-  | leftPower < rightPower = compare left (right `shiftL` (rightPower - leftPower))
-  | otherwise = compare (left `shiftL` (leftPower - rightPower)) right
- where
-  !leftMagnitude = wordBitLength left + leftPower
-  !rightMagnitude = wordBitLength right + rightPower
-
-  wordBitLength :: Word64 -> Int
-  wordBitLength value = finiteBitSize value - countLeadingZeros value
-{-# INLINE compareWordDyadic #-}
-
-compareDyadic :: Integer -> Int -> Integer -> Int -> Ordering
-compareDyadic left leftPower right rightPower =
-  case compare leftPower rightPower of
-    LT -> compare left (right `shiftL` (rightPower - leftPower))
-    EQ -> compare left right
-    GT -> compare (left `shiftL` (leftPower - rightPower)) right
-{-# INLINE compareDyadic #-}
-
-exactInCircleDet
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Integer
-exactInCircleDet ax ay bx by cx cy dx dy =
-  let (!iax, !iay, !ibx, !iby, !icx, !icy, !idx, !idy) =
-        aligned8 ax ay bx by cx cy dx dy
-      !adx = iax - idx
-      !ady = iay - idy
-      !bdx = ibx - idx
-      !bdy = iby - idy
-      !cdx = icx - idx
-      !cdy = icy - idy
-      !abdet = adx * bdy - bdx * ady
-      !bcdet = bdx * cdy - cdx * bdy
-      !cadet = cdx * ady - adx * cdy
-      !alift = adx * adx + ady * ady
-      !blift = bdx * bdx + bdy * bdy
-      !clift = cdx * cdx + cdy * cdy
-   in alift * bcdet + blift * cadet + clift * abdet
-
-exactDiametralDot
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Integer
-exactDiametralDot ax ay bx by px py =
-  let (!_, !iax, !iay, !ibx, !iby, !ipx, !ipy) = aligned6 ax ay bx by px py
-      !pax = ipx - iax
-      !pay = ipy - iay
-      !pbx = ipx - ibx
-      !pby = ipy - iby
-   in pax * pbx + pay * pby
-
-exactBarycentricDeterminants
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
-  -> (Integer, Integer, Integer, Integer)
-exactBarycentricDeterminants ax ay bx by cx cy qx qy =
-  let (!iax, !iay, !ibx, !iby, !icx, !icy, !iqx, !iqy) =
-        aligned8 ax ay bx by cx cy qx qy
-      determinant :: Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Integer
-      determinant px py rx ry sx sy =
-        let !psx = px - sx
-            !psy = py - sy
-            !rsx = rx - sx
-            !rsy = ry - sy
-         in psx * rsy - psy * rsx
-      !denominator = determinant iax iay ibx iby icx icy
-      !weightA = determinant iqx iqy ibx iby icx icy
-      !weightB = determinant iax iay iqx iqy icx icy
-      !weightC = determinant iax iay ibx iby iqx iqy
-   in (denominator, weightA, weightB, weightC)
-
-integerRatioToDouble :: Integer -> Integer -> Double
-integerRatioToDouble numerator denominator
-  | denominator == 0 = 0 / 0
-  | numerator == 0 = 0
-  | otherwise =
-      let !precision = floatDigits (0 :: Double)
-          !numeratorMagnitude = abs numerator
-          !denominatorMagnitude = abs denominator
-          !numeratorBits = integerBitLength numeratorMagnitude
-          !denominatorBits = integerBitLength denominatorMagnitude
-          !numeratorShift = max 0 (numeratorBits - precision)
-          !denominatorShift = max 0 (denominatorBits - precision)
-          !scaledNumerator = fromInteger (numeratorMagnitude `shiftR` numeratorShift)
-          !scaledDenominator = fromInteger (denominatorMagnitude `shiftR` denominatorShift)
-          !magnitude = scaleFloat (numeratorShift - denominatorShift) (scaledNumerator / scaledDenominator)
-          !sameSign = (numerator < 0) == (denominator < 0)
-       in if sameSign then magnitude else negate magnitude
-
-integerBitLength :: Integer -> Int
-integerBitLength value
-  | value <= 0 = 0
-  | otherwise = I# (integerLog2# value +# 1#)
-{-# INLINE integerBitLength #-}
-
--- ---------------------------------------------------------------------------
--- Fixed-precision exact orient sign for Double.
---
--- The generic dyadic path answers every exact query with arbitrary-precision
--- Integers: six decodes, one alignment, and two multiplies, each allocating.
--- Practical inputs have an exponent spread small enough that the determinant's
--- exact sign is decided by 128-bit differences and 256-bit products in machine
--- words, without a single heap object. 'exactOrientSignDouble' takes that path
--- and falls back to 'exactOrientDet' the moment an operand is non-finite or an
--- alignment shift would outgrow the fixed width. The two agree by
--- construction: both compute the sign of the same integer determinant.
---
--- The fixed-width worker reads a Double as one machine word and aligns
--- mantissas across a 128-bit pair, so it is only meaningful where a machine
--- word is 64 bits wide. On a narrower target the same sign is taken from the
--- arbitrary-precision determinant directly, which is the branch this path
--- already falls back to whenever an alignment shift would outgrow the width.
-
-#if WORD_SIZE_IN_BITS == 64
-
-exactOrientSignDouble
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Ordering
-exactOrientSignDouble ax ay bx by cx cy =
-  case ax of
-    D# axw ->
-      case ay of
-        D# ayw ->
-          case bx of
-            D# bxw ->
-              case by of
-                D# byw ->
-                  case cx of
-                    D# cxw ->
-                      case cy of
-                        D# cyw ->
-                          case orientSignWorker axw ayw bxw byw cxw cyw of
-                            2# -> compare (exactOrientDet ax ay bx by cx cy) 0
-                            0# -> EQ
-                            sign ->
-                              case sign ># 0# of
-                                1# -> GT
-                                _ -> LT
-{-# NOINLINE exactOrientSignDouble #-}
-
--- Decode a Double into sign bit (0/1), mantissa, and power-of-two exponent
--- with value = (-1)^sign * mantissa * 2^exponent. Zero decodes to a zero
--- mantissa; subnormals decode without a hidden bit. The fourth component is 1
--- when the value is finite and 0 when it is not.
-decodeExact :: Double# -> (# Int#, Word#, Int#, Int# #)
-decodeExact d =
-  case word64ToWord# (castDoubleToWord64# d) of
-    bits ->
-      let neg = word2Int# (uncheckedShiftRL# bits 63#)
-          exponentField = word2Int# (and# (uncheckedShiftRL# bits 52#) 2047##)
-          mantissaField = and# bits 4503599627370495##
-       in case exponentField of
-            0# -> (# neg, mantissaField, -1074#, 1# #)
-            2047# -> (# neg, mantissaField, 0#, 0# #)
-            raw -> (# neg, or# mantissaField 4503599627370496##, raw -# 1075#, 1# #)
-
--- A mantissa of at most 53 bits shifted left by at most 73 bits: the pair
--- (high, low) of a value below 2^126.
-shiftMantissa :: Word# -> Int# -> (# Word#, Word# #)
-shiftMantissa mantissa k =
-  case k >=# 64# of
-    1# -> (# uncheckedShiftL# mantissa (k -# 64#), 0## #)
-    _ ->
-      case k ==# 0# of
-        1# -> (# 0##, mantissa #)
-        _ ->
-          (#
-            uncheckedShiftRL# mantissa (64# -# k),
-            uncheckedShiftL# mantissa k
-          #)
-
--- The exact sign and 128-bit magnitude of sa*ma*2^ea - sc*mc*2^ec, aligned
--- to the caller-supplied floor exponent, as (sign, high, low, status) with
--- sign in {-1, 0, 1} and status 1 when an alignment shift outgrows the fixed
--- width. The floor never exceeds the exponent of a nonzero operand, so every
--- shift is non-negative; one shared floor is what makes the four differences
--- of one determinant comparable after multiplication.
-differenceExact
-  :: Int# -> Word# -> Int# -> Int# -> Word# -> Int# -> Int# -> (# Int#, Word#, Word#, Int# #)
-differenceExact nega ma ea negc mc ec emin =
-  case ma of
-    0## ->
-      case mc of
-        0## -> (# 0#, 0##, 0##, 0# #)
-        _ -> aligned (negateSign (positiveSign negc)) mc (ec -# emin)
-    _ ->
-      case mc of
-        0## -> aligned (positiveSign nega) ma (ea -# emin)
-        _ ->
-          case ea -# emin of
-            da ->
-              case da ># 73# of
-                1# -> (# 0#, 0##, 0##, 1# #)
-                _ ->
-                  case ec -# emin of
-                    dc ->
-                      case dc ># 73# of
-                        1# -> (# 0#, 0##, 0##, 1# #)
-                        _ ->
-                          case shiftMantissa ma da of
-                            (# ahi, alo #) ->
-                              case shiftMantissa mc dc of
-                                (# chi, clo #) ->
-                                  case nega ==# negc of
-                                    1# ->
-                                      -- Same operand signs: subtract magnitudes.
-                                      case compareWord2 ahi alo chi clo of
-                                        0# -> (# 0#, 0##, 0##, 0# #)
-                                        1# ->
-                                          case subtractWord2 ahi alo chi clo of
-                                            (# hi, lo #) -> (# positiveSign nega, hi, lo, 0# #)
-                                        _ ->
-                                          case subtractWord2 chi clo ahi alo of
-                                            (# hi, lo #) -> (# negateSign (positiveSign nega), hi, lo, 0# #)
-                                    _ ->
-                                      -- Opposite operand signs: add magnitudes.
-                                      case addWord2 ahi alo chi clo of
-                                        (# hi, lo #) -> (# positiveSign nega, hi, lo, 0# #)
-  where
-    aligned sign mantissa k =
-      case k ># 73# of
-        1# -> (# 0#, 0##, 0##, 1# #)
-        _ ->
-          case shiftMantissa mantissa k of
-            (# hi, lo #) -> (# sign, hi, lo, 0# #)
-    positiveSign neg = case neg of
-      1# -> -1#
-      _ -> 1#
-    negateSign sign = case sign of
-      1# -> -1#
-      _ -> 1#
-
--- Lexicographic comparison of 128-bit magnitudes: 1, 0, or -1.
-compareWord2 :: Word# -> Word# -> Word# -> Word# -> Int#
-compareWord2 ahi alo chi clo =
-  case eqWord# ahi chi of
-    1# ->
-      case eqWord# alo clo of
-        1# -> 0#
-        _ ->
-          case gtWord# alo clo of
-            1# -> 1#
-            _ -> -1#
-    _ ->
-      case gtWord# ahi chi of
-        1# -> 1#
-        _ -> -1#
-
--- 128-bit difference of magnitudes, first operand at least the second.
-subtractWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word# #)
-subtractWord2 ahi alo chi clo =
-  case subWordC# alo clo of
-    (# low, borrow #) ->
-      case subWordC# ahi chi of
-        (# high0, _ #) ->
-          case subWordC# high0 (int2Word# borrow) of
-            (# high, _ #) -> (# high, low #)
-
--- 128-bit sum of magnitudes each below 2^126: the total stays below 2^127 and
--- the final carry is empty by the shift bound.
-addWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word# #)
-addWord2 ahi alo chi clo =
-  case plusWord2# alo clo of
-    (# carry0, low #) ->
-      case plusWord2# ahi chi of
-        (# _, high0 #) ->
-          case plusWord2# high0 carry0 of
-            (# _, high #) -> (# high, low #)
-
--- 128-bit by 128-bit exact product, (r3, r2, r1, r0), most significant first.
--- Each factor stays below 2^127, so the product stays below 2^254 and the top
--- accumulation cannot overflow.
-multiplyWord2 :: Word# -> Word# -> Word# -> Word# -> (# Word#, Word#, Word#, Word# #)
-multiplyWord2 ahi alo bhi blo =
-  case timesWord2# alo blo of
-    (# h00, l00 #) ->
-      case timesWord2# alo bhi of
-        (# h01, l01 #) ->
-          case timesWord2# ahi blo of
-            (# h10, l10 #) ->
-              case timesWord2# ahi bhi of
-                (# h11, l11 #) ->
-                  case plusWord2# h00 l01 of
-                    (# carryA, sumA #) ->
-                      case plusWord2# sumA l10 of
-                        (# carryB, r1 #) ->
-                          case plusWord# carryA carryB of
-                            carry2 ->
-                              case plusWord2# h01 h10 of
-                                (# carryC, sumC #) ->
-                                  case plusWord2# sumC l11 of
-                                    (# carryD, sumD #) ->
-                                      case plusWord2# sumD carry2 of
-                                        (# carryE, r2 #) ->
-                                          case plusWord# (plusWord# carryC carryD) carryE of
-                                            carry3 ->
-                                              case plusWord# h11 carry3 of
-                                                r3 -> (# r3, r2, r1, l00 #)
-
--- Lexicographic comparison of 256-bit magnitudes: 1, 0, or -1.
-compareWord4
-  :: Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Word# -> Int#
-compareWord4 a3 a2 a1 a0 b3 b2 b1 b0 =
-  case compareWord2 a3 a2 b3 b2 of
-    0# -> compareWord2 a1 a0 b1 b0
-    answer -> answer
-
--- The exponent a mantissa contributes to the alignment floor: a zero
--- mantissa is exact at any floor and votes for the impossibly high sentinel.
-floorExp :: Word# -> Int# -> Int#
-floorExp mantissa power =
-  case mantissa of
-    0## -> 2000000#
-    _ -> power
-
-minExp :: Int# -> Int# -> Int#
-minExp a b = if isTrue# (a <=# b) then a else b
-
-orientSignWorker :: Double# -> Double# -> Double# -> Double# -> Double# -> Double# -> Int#
-orientSignWorker ax ay bx by cx cy =
-  case decodeExact ax of
-    (# negax, max_, eax, okax #) ->
-      case decodeExact ay of
-        (# negay, may, eay, okay #) ->
-          case decodeExact bx of
-            (# negbx, mbx, ebx, okbx #) ->
-              case decodeExact by of
-                (# negby, mby, eby, okby #) ->
-                  case decodeExact cx of
-                    (# negcx, mcx, ecx, okcx #) ->
-                      case decodeExact cy of
-                        (# negcy, mcy, ecy, okcy #) ->
-                          case okax +# okay +# okbx +# okby +# okcx +# okcy of
-                            6# ->
-                              -- The alignment floor is the least exponent
-                              -- among nonzero mantissas; a zero mantissa is
-                              -- exact at any floor and must not drag it down.
-                              case floorExp max_ eax `minExp` floorExp may eay `minExp` floorExp mbx ebx `minExp` floorExp mby eby `minExp` floorExp mcx ecx `minExp` floorExp mcy ecy of
-                                emin ->
-                                  case differenceExact negax max_ eax negcx mcx ecx emin of
-                                    (# s1, d1hi, d1lo, f1 #) ->
-                                      case differenceExact negby mby eby negcy mcy ecy emin of
-                                        (# s2, d2hi, d2lo, f2 #) ->
-                                          case differenceExact negay may eay negcy mcy ecy emin of
-                                            (# s3, d3hi, d3lo, f3 #) ->
-                                              case differenceExact negbx mbx ebx negcx mcx ecx emin of
-                                                (# s4, d4hi, d4lo, f4 #) ->
-                                                  case f1 +# f2 +# f3 +# f4 of
-                                                    0# ->
-                                                      combineSigns
-                                                        (s1 *# s2) d1hi d1lo d2hi d2lo
-                                                        (s3 *# s4) d3hi d3lo d4hi d4lo
-                                                    _ -> 2#
-                            _ -> 2#
-  where
-    -- det = leftSign * leftProduct - rightSign * rightProduct
-    combineSigns leftSign d1hi d1lo d2hi d2lo rightSign d3hi d3lo d4hi d4lo =
-      case leftSign of
-        0# ->
-          case rightSign of
-            0# -> 0#
-            _ -> 0# -# rightSign
-        _ ->
-          case rightSign of
-            0# -> leftSign
-            _ ->
-              case leftSign ==# rightSign of
-                1# ->
-                  case multiplyWord2 d1hi d1lo d2hi d2lo of
-                    (# p3, p2, p1, p0 #) ->
-                      case multiplyWord2 d3hi d3lo d4hi d4lo of
-                        (# q3, q2, q1, q0 #) ->
-                          case compareWord4 p3 p2 p1 p0 q3 q2 q1 q0 of
-                            0# -> 0#
-                            1# -> leftSign
-                            _ -> 0# -# leftSign
-                _ -> leftSign
-
-#else
-
--- The narrow-word answer to the same question. 'exactOrientDet' is the
--- determinant the fixed-width worker exists to avoid allocating, not a
--- different quantity, so the two branches agree by construction.
-exactOrientSignDouble
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Ordering
-exactOrientSignDouble ax ay bx by cx cy =
-  compare (exactOrientDet ax ay bx by cx cy) 0
-{-# NOINLINE exactOrientSignDouble #-}
-
-#endif
diff --git a/src-core/Moonlight/Triangulation/Internal/ExactRational.hs b/src-core/Moonlight/Triangulation/Internal/ExactRational.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/ExactRational.hs
+++ /dev/null
@@ -1,174 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE MagicHash #-}
-
--- | Normalized exact rational arithmetic without geometric dependencies.
-module Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , ExactArithmeticError (..)
-  , exactRational
-  , exactRationalFromDouble
-  , exactRationalFromFiniteDouble
-  , exactRationalFromDyadic
-  , exactRationalFromDyadicRatio
-  , exactRationalFromNormalizedRatio
-  , exactRationalNumerator
-  , exactRationalDenominator
-  , exactRationalBitWidth
-  , exactRationalDenominatorBitWidth
-  , exactRationalIsZero
-  , exactDivide
-  , exactSignum
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bits ((.&.), shiftL, shiftR)
-import Data.Ratio (Ratio, (%))
-import qualified Data.Ratio as Ratio
-import GHC.Generics (Generic)
-import GHC.Exts (Int (I#))
-import GHC.Integer.Logarithms (integerLog2#)
-import GHC.Real (Ratio ((:%)))
-
--- | A checked wrapper around a reduced ratio with a strictly positive
--- denominator. 'Ratio' owns normalization, including the unique zero
--- representation @0 / 1@.
-newtype ExactRational = ExactRational (Ratio Integer)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving newtype (Num)
-  deriving anyclass (NFData)
-
--- | Typed refusals from exact rational construction and division.
-data ExactArithmeticError
-  = ExactZeroDenominator
-  | ExactZeroDivisor
-  | ExactNaNInput
-  | ExactInfiniteInput
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Construct a reduced rational, moving any sign onto the numerator and
--- refusing a zero denominator.
-exactRational :: Integer -> Integer -> Either ExactArithmeticError ExactRational
-exactRational _ 0 = Left ExactZeroDenominator
-exactRational numerator denominator = Right (ExactRational (numerator % denominator))
-
--- | Convert a binary64 value without loss, refusing NaN and infinities.
-exactRationalFromDouble :: Double -> Either ExactArithmeticError ExactRational
-exactRationalFromDouble value
-  | isNaN value = Left ExactNaNInput
-  | isInfinite value = Left ExactInfiniteInput
-  | otherwise = Right (exactRationalFromFiniteDouble value)
-
--- | Convert an already-admitted finite coordinate without loss. The caller
--- supplies a finite value, such as a coordinate inside a validated
--- @QueryPoint@, so this worker does not repeat the admission refusal.
-exactRationalFromFiniteDouble :: Double -> ExactRational
-exactRationalFromFiniteDouble value =
-  uncurry exactRationalFromDyadic (decodeFloat value)
-{-# INLINE exactRationalFromFiniteDouble #-}
-
--- | Construct @numerator * 2^power@ without a partial denominator path.
-exactRationalFromDyadic :: Integer -> Int -> ExactRational
-exactRationalFromDyadic numerator power
-  | numerator == 0 = ExactRational (0 :% 1)
-  | power >= 0 = ExactRational ((numerator `shiftL` power) :% 1)
-  | otherwise =
-      let denominatorPower = negate power
-          removableFactor =
-            min denominatorPower (integerTrailingZeroBits numerator)
-          reducedNumerator = numerator `shiftR` removableFactor
-          reducedDenominator =
-            1 `shiftL` (denominatorPower - removableFactor)
-       in ExactRational (reducedNumerator :% reducedDenominator)
-{-# INLINE exactRationalFromDyadic #-}
-
--- | The denominator of a dyadic rational is a power of two, so its complete
--- normalization requires only the numerator's least set bit. Calling generic
--- rational GCD here merely rediscovers that closed arithmetic fact.
-integerTrailingZeroBits :: Integer -> Int
-integerTrailingZeroBits value =
-  let magnitude = abs value
-   in I# (integerLog2# (magnitude .&. negate magnitude))
-{-# INLINE integerTrailingZeroBits #-}
-
--- | Construct @(numerator / denominator) * 2^power@ with one final rational
--- normalization. This is the publication boundary for exact dyadic kernels:
--- their integer arithmetic must not pay a greatest-common-divisor reduction
--- after every intermediate operation.
-exactRationalFromDyadicRatio
-  :: Integer
-  -> Integer
-  -> Int
-  -> Either ExactArithmeticError ExactRational
-exactRationalFromDyadicRatio _ 0 _ = Left ExactZeroDivisor
-exactRationalFromDyadicRatio 0 _ _ = Right (ExactRational (0 :% 1))
-exactRationalFromDyadicRatio numerator denominator power =
-  let numeratorFactor = integerTrailingZeroBits numerator
-      denominatorFactor = integerTrailingZeroBits denominator
-      oddNumerator = numerator `shiftR` numeratorFactor
-      oddDenominator = denominator `shiftR` denominatorFactor
-      residualPower = power + numeratorFactor - denominatorFactor
-   in if residualPower >= 0
-        then exactRational (oddNumerator `shiftL` residualPower) oddDenominator
-        else exactRational oddNumerator (oddDenominator `shiftL` negate residualPower)
-{-# INLINE exactRationalFromDyadicRatio #-}
-
--- | Internal bridge from the normalized carrier owned by @Data.Ratio@. This
--- exists for statically nonzero rational constants in exact kernels; public
--- callers continue through 'exactRational'.
-exactRationalFromNormalizedRatio :: Ratio Integer -> ExactRational
-exactRationalFromNormalizedRatio = ExactRational
-{-# INLINE exactRationalFromNormalizedRatio #-}
-
--- | Read the reduced numerator.
-exactRationalNumerator :: ExactRational -> Integer
-exactRationalNumerator (ExactRational value) = Ratio.numerator value
-{-# INLINE exactRationalNumerator #-}
-
--- | Read the strictly positive reduced denominator.
-exactRationalDenominator :: ExactRational -> Integer
-exactRationalDenominator (ExactRational value) = Ratio.denominator value
-{-# INLINE exactRationalDenominator #-}
-
--- | Maximum bit width of the reduced numerator magnitude and strictly
--- positive denominator. This is an observation, not an arithmetic bound.
-exactRationalBitWidth :: ExactRational -> Int
-exactRationalBitWidth value =
-  max
-    (integerBitWidth (abs (exactRationalNumerator value)))
-    (exactRationalDenominatorBitWidth value)
-{-# INLINE exactRationalBitWidth #-}
-
--- | Bit width of the reduced, strictly positive denominator.
-exactRationalDenominatorBitWidth :: ExactRational -> Int
-exactRationalDenominatorBitWidth =
-  integerBitWidth . exactRationalDenominator
-{-# INLINE exactRationalDenominatorBitWidth #-}
-
-integerBitWidth :: Integer -> Int
-integerBitWidth value
-  | value <= 0 = 0
-  | otherwise = I# (integerLog2# value) + 1
-{-# INLINE integerBitWidth #-}
-
--- | Test whether the exact value is zero.
-exactRationalIsZero :: ExactRational -> Bool
-exactRationalIsZero (ExactRational value) = Ratio.numerator value == 0
-{-# INLINE exactRationalIsZero #-}
-
--- | Divide by a nonzero exact rational, refusing a zero divisor explicitly.
-exactDivide
-  :: ExactRational
-  -> ExactRational
-  -> Either ExactArithmeticError ExactRational
-exactDivide (ExactRational left) (ExactRational right)
-  | Ratio.numerator right == 0 = Left ExactZeroDivisor
-  | otherwise = Right (ExactRational (left / right))
-
--- | Compare an exact rational with zero through its canonical numerator.
-exactSignum :: ExactRational -> Ordering
-exactSignum (ExactRational value) = compare (Ratio.numerator value) 0
-{-# INLINE exactSignum #-}
diff --git a/src-core/Moonlight/Triangulation/Internal/FaceQueue.hs b/src-core/Moonlight/Triangulation/Internal/FaceQueue.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/FaceQueue.hs
+++ /dev/null
@@ -1,105 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE RecordWildCards #-}
-
--- | The set of faces still owed a refinement decision.
-module Moonlight.Triangulation.Internal.FaceQueue
-  ( FaceQueue
-  , newFaceQueue
-  , pushFace
-  , popFace
-  ) where
-
-import Control.Monad.ST (ST)
-import qualified Data.IntSet as IntSet
-import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef)
-import Data.Word (Word32)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  , popGrowableOr
-  , pushGrowable
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, packIndex)
-
--- A worst-first heap has to be told a score for every face it is offered, so
--- the caller derives one — circumradius, area, the encroachment verdict — for
--- faces that are then found acceptable and dropped. Ruppert's termination does
--- not rest on that order; it rests on every bad face being reached before the
--- run ends. So this only has to be a set with a discipline for draining it.
---
--- A stack is that, and it makes membership O(1) with nothing to compute. The
--- pending flags are what keep it a set: a face touched by several insertions
--- before it is drained is decided once, not once per touch.
---
--- The size lives in an unboxed cell for the reason the growable stack's does:
--- it is written on every push and every pop, and a boxed counter allocates a
--- box per write once the size leaves the shared small-'Int' range.
-data FaceQueue s
-  = DenseFaceQueue
-      !(MUV.MVector s Word32)
-      !(MUV.MVector s Bool)
-      !(MUV.MVector s Int)
-  | SparseFaceQueue
-      !(GrowableWord32 s)
-      !(STRef s IntSet.IntSet)
-
-newFaceQueue :: Maybe Int -> ST s (FaceQueue s)
-newFaceQueue capacity =
-  case capacity of
-    Just size -> do
-      faces <- MUV.new (max 1 size)
-      pending <- MUV.replicate (max 1 size) False
-      stackSize <- MUV.replicate 1 0
-      pure (DenseFaceQueue faces pending stackSize)
-    Nothing -> do
-      faces <- newGrowableWord32 16
-      pending <- newSTRef IntSet.empty
-      pure (SparseFaceQueue faces pending)
-
--- The checked read of the pending flag is the one bound this module does not
--- establish itself: the face arrives from mesh topology. Once it succeeds,
--- uniqueness proves the stack write: at most one slot exists for each pending
--- flag, and the two vectors have the same length.
-pushFace :: FaceQueue s -> Int -> ST s ()
-pushFace queue face =
-  case queue of
-    DenseFaceQueue faces pending stackSize -> do
-      isPending <- MUV.unsafeRead pending face
-      if isPending
-        then pure ()
-        else do
-          size <- MUV.unsafeRead stackSize 0
-          MUV.unsafeWrite faces size (packIndex face)
-          MUV.unsafeWrite pending face True
-          MUV.unsafeWrite stackSize 0 (size + 1)
-    SparseFaceQueue faces pending -> do
-      isPending <- IntSet.member face <$> readSTRef pending
-      if isPending
-        then pure ()
-        else do
-          pushGrowable faces (packIndex face)
-          modifySTRef' pending (IntSet.insert face)
-
-popFace :: FaceQueue s -> ST s (Maybe Int)
-popFace queue =
-  case queue of
-    DenseFaceQueue faces pending stackSize -> do
-      size <- MUV.unsafeRead stackSize 0
-      if size == 0
-        then pure Nothing
-        else do
-          let !index = size - 1
-          face <- fromIntegral <$> MUV.unsafeRead faces index
-          MUV.unsafeWrite pending face False
-          MUV.unsafeWrite stackSize 0 index
-          pure (Just face)
-    SparseFaceQueue faces pending -> do
-      packed <- popGrowableOr noIndex faces
-      if packed == noIndex
-        then pure Nothing
-        else do
-          let !face = fromIntegral packed
-          modifySTRef' pending (IntSet.delete face)
-          pure (Just face)
diff --git a/src-core/Moonlight/Triangulation/Internal/Growable.hs b/src-core/Moonlight/Triangulation/Internal/Growable.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/Growable.hs
+++ /dev/null
@@ -1,94 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
-module Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  , clearGrowable
-  , growableLength
-  , pushGrowable
-  , popGrowableOr
-  , readGrowable
-  , writeGrowable
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-
--- | Transaction-local vector. It starts small and grows only with the local
--- frontier; no operation allocates scratch memory proportional to mesh size.
--- The size lives in an unboxed cell: the stack is popped and pushed inside
--- every legalization drain, and a boxed counter would allocate on each step.
-data GrowableWord32 s = GrowableWord32
-  { growableVector :: !(STRef s (MUV.MVector s Word32))
-  , growableSize :: !(MUV.MVector s Int)
-  }
-
-newGrowableWord32 :: Int -> ST s (GrowableWord32 s)
-newGrowableWord32 requested = do
-  vector <- MUV.new (max 8 requested)
-  growableVector <- newSTRef vector
-  growableSize <- MUV.replicate 1 0
-  pure GrowableWord32{growableVector, growableSize}
-
-clearGrowable :: GrowableWord32 s -> ST s ()
-clearGrowable GrowableWord32{growableSize} = MUV.unsafeWrite growableSize 0 0
-{-# INLINE clearGrowable #-}
-
-growableLength :: GrowableWord32 s -> ST s Int
-growableLength GrowableWord32{growableSize} = MUV.unsafeRead growableSize 0
-{-# INLINE growableLength #-}
-
-pushGrowable :: GrowableWord32 s -> Word32 -> ST s ()
-pushGrowable growable@GrowableWord32{growableSize} value = do
-  index <- MUV.unsafeRead growableSize 0
-  vector <- ensureCapacity growable (index + 1)
-  MUV.unsafeWrite vector index value
-  MUV.unsafeWrite growableSize 0 (index + 1)
-{-# INLINE pushGrowable #-}
-
--- | Pop, answering the caller's sentinel on emptiness instead of allocating a
--- 'Maybe' per step. Sound only against a sentinel no push can store.
-popGrowableOr :: Word32 -> GrowableWord32 s -> ST s Word32
-popGrowableOr sentinel GrowableWord32{growableVector, growableSize} = do
-  size <- MUV.unsafeRead growableSize 0
-  if size <= 0
-    then pure sentinel
-    else do
-      let !index = size - 1
-      vector <- readSTRef growableVector
-      value <- MUV.unsafeRead vector index
-      MUV.unsafeWrite growableSize 0 index
-      pure value
-{-# INLINE popGrowableOr #-}
-
-readGrowable :: GrowableWord32 s -> Int -> ST s Word32
-readGrowable GrowableWord32{growableVector} index = do
-  vector <- readSTRef growableVector
-  MUV.unsafeRead vector index
-{-# INLINE readGrowable #-}
-
-writeGrowable :: GrowableWord32 s -> Int -> Word32 -> ST s ()
-writeGrowable growable@GrowableWord32{growableSize} index value = do
-  vector <- ensureCapacity growable (index + 1)
-  MUV.unsafeWrite vector index value
-  size <- MUV.unsafeRead growableSize 0
-  when (index >= size) (MUV.unsafeWrite growableSize 0 (index + 1))
-{-# INLINE writeGrowable #-}
-
--- Answering with the vector is what keeps a push to one cell read: the caller
--- would otherwise re-read the reference this just proved current.
-ensureCapacity :: GrowableWord32 s -> Int -> ST s (MUV.MVector s Word32)
-ensureCapacity GrowableWord32{growableVector} required = do
-  vector <- readSTRef growableVector
-  let !current = MUV.length vector
-  if required <= current
-    then pure vector
-    else do
-      let !next = until (>= required) (* 2) current
-      grown <- MUV.grow vector (next - current)
-      writeSTRef growableVector grown
-      pure grown
diff --git a/src-core/Moonlight/Triangulation/Internal/PackedIndex.hs b/src-core/Moonlight/Triangulation/Internal/PackedIndex.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/PackedIndex.hs
+++ /dev/null
@@ -1,34 +0,0 @@
-module Moonlight.Triangulation.Internal.PackedIndex
-  ( noIndex
-  , indexLimit
-  , packIndex
-  , unpackIndex
-  , unpackOptionalIndex
-  ) where
-
-import Data.Word (Word32)
-
-noIndex :: Word32
-noIndex = maxBound
-
-indexLimit :: Int
-indexLimit = fromIntegral (maxBound - 1 :: Word32)
-
--- | Pack an admitted arena handle. The build boundary limits vertex capacity
--- so the mutable topology reservation, @8n + 16@, stays below 'indexLimit';
--- every caller supplies a non-negative handle or worklist position derived
--- from those arenas. Optional @-1@ handles are represented separately by
--- 'noIndex' before this function is reached.
-packIndex :: Int -> Word32
-packIndex = fromIntegral
-{-# INLINE packIndex #-}
-
-unpackIndex :: Word32 -> Int
-unpackIndex = fromIntegral
-{-# INLINE unpackIndex #-}
-
-unpackOptionalIndex :: Word32 -> Maybe Int
-unpackOptionalIndex value
-  | value == noIndex = Nothing
-  | otherwise = Just (unpackIndex value)
-{-# INLINE unpackOptionalIndex #-}
diff --git a/src-core/Moonlight/Triangulation/Internal/PageDirectory.hs b/src-core/Moonlight/Triangulation/Internal/PageDirectory.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/PageDirectory.hs
+++ /dev/null
@@ -1,200 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | The map from page number to page, for the paged stores.
-module Moonlight.Triangulation.Internal.PageDirectory
-  ( PageDirectory
-  , emptyDirectory
-  , lookupDirectory
-  , insertDirectory
-  , directoryFromAscList
-  , directoryToAscList
-  , directorySize
-  , directoryLookupMax
-  , directoryRestrict
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bits (shiftL, shiftR, (.&.))
-import qualified Data.List as List
-import qualified Data.Vector as V
-import qualified Data.Vector.Mutable as MV
-import GHC.Generics (Generic)
-
--- Page numbers are small, dense and non-negative, and the read path resolves
--- one on every field access that misses its owner's current page. An ordered
--- map answers that with a chain of prefix comparisons whose length grows with
--- the mesh; a 32-way radix answers it with two indexed loads at every size the
--- meshes reach, and the arithmetic is shifts and masks rather than branches.
---
--- Absence is representable because the boxed payload store leaves a page out
--- entirely when every slot in it still holds the fill.
-data Slot a
-  = Absent
-  | Page !a
-  | Fanout !(V.Vector (Slot a))
-  deriving stock (Generic, Functor)
-  deriving anyclass (NFData)
-
--- | @directoryHeight@ counts the fanout levels below the root: at height 0 the
--- root's children are pages, at height 1 they are fanouts of pages, and so on.
-data PageDirectory a = PageDirectory
-  { directoryHeight :: {-# UNPACK #-} !Int
-  , directoryRoot :: !(Slot a)
-  , directorySize :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Generic, Functor)
-  deriving anyclass (NFData)
-
-fanout :: Int
-fanout = 32
-{-# INLINE fanout #-}
-
-slotBits :: Int
-slotBits = 5
-{-# INLINE slotBits #-}
-
--- Path copying replaces one slot per level, and the bulk-update operator would
--- route that through a list of one pair. This is the copy the level needs and
--- nothing besides.
-updateSlot :: V.Vector (Slot a) -> Int -> Slot a -> V.Vector (Slot a)
-updateSlot children index child =
-  V.modify (\node -> MV.unsafeWrite node index child) children
-{-# INLINE updateSlot #-}
-
--- | The first page number a directory of this height cannot address.
-capacity :: Int -> Int
-capacity height = 1 `shiftL` (slotBits * (height + 1))
-{-# INLINE capacity #-}
-
-emptyDirectory :: PageDirectory a
-emptyDirectory = PageDirectory 0 Absent 0
-
-lookupDirectory :: Int -> PageDirectory a -> Maybe a
-lookupDirectory key PageDirectory{directoryHeight, directoryRoot}
-  | key < 0 || key >= capacity directoryHeight = Nothing
-  | otherwise = go directoryHeight directoryRoot
- where
-  go !height slot = case slot of
-    Absent -> Nothing
-    Page value -> Just value
-    Fanout children ->
-      go (height - 1) (V.unsafeIndex children ((key `shiftR` (slotBits * height)) .&. (fanout - 1)))
-{-# INLINE lookupDirectory #-}
-
-insertDirectory :: Int -> a -> PageDirectory a -> PageDirectory a
-insertDirectory key value directory
-  | key >= capacity (directoryHeight directory) = insertDirectory key value (grow directory)
-  | otherwise =
-      let (!root, !added) = write (directoryHeight directory) (directoryRoot directory)
-       in directory
-            { directoryRoot = root
-            , directorySize = directorySize directory + if added then 1 else 0
-            }
- where
-  write !height slot
-    | height < 0 = (Page value, case slot of Page _ -> False; _ -> True)
-    | otherwise =
-        let !index = (key `shiftR` (slotBits * height)) .&. (fanout - 1)
-            !children = case slot of
-              Fanout existing -> existing
-              _ -> V.replicate fanout Absent
-            (!child, !added) = write (height - 1) (V.unsafeIndex children index)
-         in (Fanout (updateSlot children index child), added)
-
--- A directory that has run out of addressable pages gains a level, and its
--- whole former extent becomes child zero of the new root.
-grow :: PageDirectory a -> PageDirectory a
-grow directory@PageDirectory{directoryHeight, directoryRoot} =
-  directory
-    { directoryHeight = directoryHeight + 1
-    , directoryRoot = case directoryRoot of
-        Absent -> Absent
-        occupied -> Fanout (updateSlot (V.replicate fanout Absent) 0 occupied)
-    }
-
--- | Build from ascending, distinct page numbers in one descent.
---
--- Every dense transaction ends by publishing a whole directory, so this is on
--- the freeze path of bulk load, constraint recovery and refinement alike.
--- Folding 'insertDirectory' over the list would copy each root path once per
--- page; laying the levels down directly touches each node once.
-directoryFromAscList :: [(Int, a)] -> PageDirectory a
-directoryFromAscList [] = emptyDirectory
-directoryFromAscList entries@((firstKey, _) : remainingEntries) =
-  PageDirectory
-    { directoryHeight = height
-    , directoryRoot = fst (build height 0 entries)
-    , directorySize = length entries
-    }
- where
-  !largest = List.foldl' (\largestKey (key, _) -> max largestKey key) firstKey remainingEntries
-  !height = heightFor 0
-  heightFor !candidate
-    | largest < capacity candidate = candidate
-    | otherwise = heightFor (candidate + 1)
-
-  build :: Int -> Int -> [(Int, b)] -> (Slot b, [(Int, b)])
-  build !level !base remaining = case remaining of
-    [] -> (Absent, [])
-    ((key, value) : rest)
-      | key >= base + capacity level -> (Absent, remaining)
-      | level < 0 -> (Page value, rest)
-      | otherwise ->
-          let !childSpan = capacity (level - 1)
-              step (children, unconsumed) index =
-                let (!child, !beyond) = build (level - 1) (base + index * childSpan) unconsumed
-                 in (child : children, beyond)
-              (!reversed, !left) = List.foldl' step ([], remaining) [0 .. fanout - 1]
-           in (Fanout (V.fromListN fanout (reverse reversed)), left)
-
-directoryToAscList :: PageDirectory a -> [(Int, a)]
-directoryToAscList PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot []
- where
-  go :: Int -> Int -> Slot b -> [(Int, b)] -> [(Int, b)]
-  go !height !prefix slot rest = case slot of
-    Absent -> rest
-    Page value -> (prefix, value) : rest
-    Fanout children ->
-      foldr
-        (\index accumulated ->
-          go
-            (height - 1)
-            (prefix + (index `shiftL` (slotBits * height)))
-            (V.unsafeIndex children index)
-            accumulated
-        )
-        rest
-        [0 .. fanout - 1]
-
-directoryLookupMax :: PageDirectory a -> Maybe (Int, a)
-directoryLookupMax PageDirectory{directoryHeight, directoryRoot} = go directoryHeight 0 directoryRoot
- where
-  go :: Int -> Int -> Slot b -> Maybe (Int, b)
-  go !height !prefix slot = case slot of
-    Absent -> Nothing
-    Page value -> Just (prefix, value)
-    Fanout children -> descend (fanout - 1)
-     where
-      descend !index
-        | index < 0 = Nothing
-        | otherwise =
-            case go (height - 1) (prefix + (index `shiftL` (slotBits * height))) (V.unsafeIndex children index) of
-              Nothing -> descend (index - 1)
-              found -> found
-
--- | Keep only the pages a directory of the given page count can hold. The
--- common case is that every page is already within the count, and that is
--- settled by one descent rather than a rebuild.
-directoryRestrict :: Int -> PageDirectory a -> PageDirectory a
-directoryRestrict pageCount directory
-  | pageCount <= 0 = emptyDirectory
-  | otherwise = case directoryLookupMax directory of
-      Nothing -> directory
-      Just (largest, _)
-        | largest < pageCount -> directory
-        | otherwise ->
-            directoryFromAscList (filter ((< pageCount) . fst) (directoryToAscList directory))
diff --git a/src-core/Moonlight/Triangulation/Internal/Paged.hs b/src-core/Moonlight/Triangulation/Internal/Paged.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Internal/Paged.hs
+++ /dev/null
@@ -1,416 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module Moonlight.Triangulation.Internal.Paged
-  ( Paged
-  , MutablePaged
-  , FlatMutablePaged
-  , emptyPaged
-  , fromVector
-  , fromLocalVector
-  , toVector
-  , pagedLength
-  , pagedPageCount
-  , pagedOverlayPageCount
-  , PublicationStats (..)
-  , emptyPublicationStats
-  , addPublicationStats
-  , pagedUnsafeIndex
-  , pagedFoldl'
-  , newMutablePaged
-  , newLocalMutablePaged
-  , TransactionShape (..)
-  , thawPaged
-  , thawPagedDense
-  , thawPagedShaped
-  , flatMutableSection
-  , readFlatMutable
-  , writeFlatMutable
-  , readPaged
-  , writePaged
-  , freezePaged
-  , readMutablePagedPublicationStats
-  ) where
-
-import Control.DeepSeq (NFData (..))
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.Bits (shiftL, shiftR, (.&.))
-import qualified Data.IntMap.Strict as IntMap
-import Data.List (unfoldr)
-import Data.STRef (STRef, modifySTRef', newSTRef, readSTRef, writeSTRef)
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Internal.PageDirectory
-  ( PageDirectory
-  , directoryRestrict
-  , directorySize
-  , emptyDirectory
-  , insertDirectory
-  , lookupDirectory
-  )
-
--- | One flat root plus one cumulative page overlay. An extension never wraps
--- a published value in another 'Paged' node: dirty pages are merged into the
--- existing directory, so an untouched read remains one overlay lookup plus
--- one root lookup regardless of extension count.
-data Paged a
-  = FlatPaged {-# UNPACK #-} !Int {-# UNPACK #-} !Int !(U.Vector a)
-  | SharedPaged
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      !(U.Vector a)
-      !(PageDirectory (U.Vector a))
-
--- | Exact storage work observed by one persistent publication.  The dense
--- page counters describe a whole-base traversal; dirty-page and copied-cell
--- counters describe only pages opened by the local write set.  A zero dense
--- counter is therefore a storage fact from the transaction owner, never a
--- benchmark inference; page work is independent of the resident prefix except
--- for directory-path depth.
-data PublicationStats = PublicationStats
-  { publicationUnboxedBasePageEnumerations :: {-# UNPACK #-} !Int
-  , publicationUnboxedBasePageOpens :: {-# UNPACK #-} !Int
-  , publicationUnboxedBasePageFreezes :: {-# UNPACK #-} !Int
-  , publicationUnboxedDirtyBasePages :: {-# UNPACK #-} !Int
-  , publicationUnboxedDirtyAppendedPages :: {-# UNPACK #-} !Int
-  , publicationUnboxedCopiedCells :: {-# UNPACK #-} !Int
-  , publicationBoxedBasePageEnumerations :: {-# UNPACK #-} !Int
-  , publicationBoxedBasePageOpens :: {-# UNPACK #-} !Int
-  , publicationBoxedBasePageFreezes :: {-# UNPACK #-} !Int
-  , publicationBoxedDirtyBasePages :: {-# UNPACK #-} !Int
-  , publicationBoxedDirtyAppendedPages :: {-# UNPACK #-} !Int
-  , publicationBoxedCopiedCells :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Show)
-
-instance NFData PublicationStats where
-  rnf stats = stats `seq` ()
-
-emptyPublicationStats :: PublicationStats
-emptyPublicationStats =
-  PublicationStats
-    { publicationUnboxedBasePageEnumerations = 0
-    , publicationUnboxedBasePageOpens = 0
-    , publicationUnboxedBasePageFreezes = 0
-    , publicationUnboxedDirtyBasePages = 0
-    , publicationUnboxedDirtyAppendedPages = 0
-    , publicationUnboxedCopiedCells = 0
-    , publicationBoxedBasePageEnumerations = 0
-    , publicationBoxedBasePageOpens = 0
-    , publicationBoxedBasePageFreezes = 0
-    , publicationBoxedDirtyBasePages = 0
-    , publicationBoxedDirtyAppendedPages = 0
-    , publicationBoxedCopiedCells = 0
-    }
-
-addPublicationStats :: PublicationStats -> PublicationStats -> PublicationStats
-addPublicationStats left right =
-  PublicationStats
-    { publicationUnboxedBasePageEnumerations = publicationUnboxedBasePageEnumerations left + publicationUnboxedBasePageEnumerations right
-    , publicationUnboxedBasePageOpens = publicationUnboxedBasePageOpens left + publicationUnboxedBasePageOpens right
-    , publicationUnboxedBasePageFreezes = publicationUnboxedBasePageFreezes left + publicationUnboxedBasePageFreezes right
-    , publicationUnboxedDirtyBasePages = publicationUnboxedDirtyBasePages left + publicationUnboxedDirtyBasePages right
-    , publicationUnboxedDirtyAppendedPages = publicationUnboxedDirtyAppendedPages left + publicationUnboxedDirtyAppendedPages right
-    , publicationUnboxedCopiedCells = publicationUnboxedCopiedCells left + publicationUnboxedCopiedCells right
-    , publicationBoxedBasePageEnumerations = publicationBoxedBasePageEnumerations left + publicationBoxedBasePageEnumerations right
-    , publicationBoxedBasePageOpens = publicationBoxedBasePageOpens left + publicationBoxedBasePageOpens right
-    , publicationBoxedBasePageFreezes = publicationBoxedBasePageFreezes left + publicationBoxedBasePageFreezes right
-    , publicationBoxedDirtyBasePages = publicationBoxedDirtyBasePages left + publicationBoxedDirtyBasePages right
-    , publicationBoxedDirtyAppendedPages = publicationBoxedDirtyAppendedPages left + publicationBoxedDirtyAppendedPages right
-    , publicationBoxedCopiedCells = publicationBoxedCopiedCells left + publicationBoxedCopiedCells right
-    }
-
--- | A fresh build owns a contiguous arena. A local transaction owns only its
--- dirty pages; clean values remain reachable through the immutable base.
-data MutablePaged s a
-  = MutableFlatPaged {-# UNPACK #-} !Int !(MUV.MVector s a)
-  | MutableSharedPaged
-      {-# UNPACK #-} !Int
-      !(Paged a)
-      !(STRef s (IntMap.IntMap (MUV.MVector s a)))
-      !(STRef s PublicationStats)
-
--- | A proof that one mutable sequence is in its contiguous physical section.
-data FlatMutablePaged s a = FlatMutablePaged !(MUV.MVector s a)
-
-localPageBits :: Int
-localPageBits = 8
-{-# INLINE localPageBits #-}
-
-traversalPageBits :: Int
-traversalPageBits = 10
-{-# INLINE traversalPageBits #-}
-
-pageSize :: Int -> Int
-pageSize bits = 1 `shiftL` bits
-{-# INLINE pageSize #-}
-
-pageOf :: Int -> Int -> Int
-pageOf bits index = index `shiftR` bits
-{-# INLINE pageOf #-}
-
-offsetOf :: Int -> Int -> Int
-offsetOf bits index = index .&. (pageSize bits - 1)
-{-# INLINE offsetOf #-}
-
-pagesFor :: Int -> Int -> Int
-pagesFor bits count
-  | count <= 0 = 0
-  | otherwise = pageOf bits (count - 1) + 1
-{-# INLINE pagesFor #-}
-
-pagedLength :: Paged a -> Int
-pagedLength paged = case paged of
-  FlatPaged count _ _ -> count
-  SharedPaged count _ _ _ -> count
-{-# INLINE pagedLength #-}
-
-pagedPageCount :: Paged a -> Int
-pagedPageCount paged = pagesFor (pagedBits paged) (pagedLength paged)
-{-# INLINE pagedPageCount #-}
-
--- | Number of pages in the persistent overlay.  A flat root has no copied
--- pages; a shared value records only pages dirtied by its transaction.  This
--- is an observation for publication receipts, not a read-path traversal.
-pagedOverlayPageCount :: Paged a -> Int
-pagedOverlayPageCount paged = case paged of
-  FlatPaged{} -> 0
-  SharedPaged _ _ _ directory -> directorySize directory
-{-# INLINE pagedOverlayPageCount #-}
-
-pagedBits :: Paged a -> Int
-pagedBits paged = case paged of
-  FlatPaged _ bits _ -> bits
-  SharedPaged _ bits _ _ -> bits
-{-# INLINE pagedBits #-}
-
-instance (U.Unbox a, Eq a) => Eq (Paged a) where
-  left == right =
-    pagedLength left == pagedLength right
-      && case (left, right) of
-        (FlatPaged _ _ leftValues, FlatPaged _ _ rightValues) -> leftValues == rightValues
-        _ -> pagedChunksEqual left right
-
-pagedChunksEqual :: forall a. (U.Unbox a, Eq a) => Paged a -> Paged a -> Bool
-pagedChunksEqual left right =
-  foldl'
-    (\equal (index, width) ->
-       equal
-         && U.unsafeSlice 0 width (pageSlice left index)
-              == U.unsafeSlice 0 width (pageSlice right index))
-    True
-    chunks
- where
-  !count = pagedLength left
-  !leftBits = pagedBits left
-  !rightBits = pagedBits right
-  chunks = unfoldr nextChunk 0
-
-  nextChunk !index
-    | index >= count = Nothing
-    | otherwise = Just ((index, width), index + width)
-    where
-      !leftPageEnd = (pageOf leftBits index + 1) * pageSize leftBits
-      !rightPageEnd = (pageOf rightBits index + 1) * pageSize rightBits
-      !width = min (count - index) (min (leftPageEnd - index) (rightPageEnd - index))
-  pageSlice :: Paged a -> Int -> U.Vector a
-  pageSlice paged index =
-    let !bits = pagedBits paged
-        !page = pageOf bits index
-        !offset = offsetOf bits index
-     in case paged of
-          FlatPaged _ _ values -> U.unsafeSlice index (U.length values - index) values
-          SharedPaged _ _ root directory ->
-            case lookupDirectory page directory of
-              Just values -> U.unsafeSlice offset (U.length values - offset) values
-              Nothing -> U.unsafeSlice index (U.length root - index) root
-
-instance (U.Unbox a, Show a) => Show (Paged a) where
-  showsPrec precedence = showsPrec precedence . U.toList . toVector
-
-instance NFData (Paged a) where
-  rnf paged = case paged of
-    FlatPaged count bits values -> count `seq` bits `seq` values `seq` ()
-    SharedPaged count bits root directory ->
-      count `seq` bits `seq` root `seq` directory `seq` ()
-
-emptyPaged :: U.Unbox a => Paged a
-emptyPaged = FlatPaged 0 traversalPageBits U.empty
-
-fromVector :: U.Unbox a => a -> U.Vector a -> Paged a
-fromVector _padding values = FlatPaged (U.length values) traversalPageBits values
-
-fromLocalVector :: U.Unbox a => a -> U.Vector a -> Paged a
-fromLocalVector _padding values = FlatPaged (U.length values) localPageBits values
-
-toVector :: U.Unbox a => Paged a -> U.Vector a
-toVector paged = case paged of
-  FlatPaged count _ values -> U.unsafeSlice 0 count values
-  SharedPaged count _ _ _ -> U.generate count (pagedUnsafeIndex paged)
-
-pagedUnsafeIndex :: U.Unbox a => Paged a -> Int -> a
-pagedUnsafeIndex paged index =
-  case paged of
-    FlatPaged _ _ values -> U.unsafeIndex values index
-    SharedPaged _ bits root directory ->
-      case lookupDirectory (pageOf bits index) directory of
-        Just values -> U.unsafeIndex values (offsetOf bits index)
-        Nothing -> U.unsafeIndex root index
-{-# INLINE pagedUnsafeIndex #-}
-
--- | Fold directly over the physical sections. Unlike the old implementation,
--- this does not allocate a complete vector merely to consume it.
-pagedFoldl' :: U.Unbox a => (b -> a -> b) -> b -> Paged a -> b
-pagedFoldl' step initial paged =
-  case paged of
-    FlatPaged count _ values -> U.foldl' step initial (U.take count values)
-    SharedPaged count bits root directory ->
-      foldl' foldPage initial [0 .. pagesFor bits count - 1]
-     where
-      foldPage !accumulated page =
-        let !start = page * pageSize bits
-            !width = min (pageSize bits) (count - start)
-            !values =
-              case lookupDirectory page directory of
-                Just pageValues -> U.unsafeSlice 0 width pageValues
-                Nothing -> U.unsafeSlice start width root
-         in U.foldl' step accumulated values
-{-# INLINE pagedFoldl' #-}
-
-newMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
-newMutablePaged capacity = do
-  values <- MUV.new (max 0 capacity)
-  pure (MutableFlatPaged traversalPageBits values)
-
-newLocalMutablePaged :: U.Unbox a => Int -> ST s (MutablePaged s a)
-newLocalMutablePaged capacity = do
-  values <- MUV.new (max 0 capacity)
-  pure (MutableFlatPaged localPageBits values)
-
--- | Open a persistent transaction without constructing a page table over the
--- resident mesh. The mutable dictionary starts empty and acquires storage only
--- when a write first reaches a page.
-thawPaged :: Int -> Paged a -> ST s (MutablePaged s a)
-thawPaged _requestedCapacity paged = do
-  dirty <- newSTRef IntMap.empty
-  stats <- newSTRef emptyPublicationStats
-  pure (MutableSharedPaged (pagedBits paged) paged dirty stats)
-
-data TransactionShape = DenseTransaction | LocalTransaction
-
-thawPagedShaped :: U.Unbox a => TransactionShape -> Int -> Paged a -> ST s (MutablePaged s a)
-thawPagedShaped shape = case shape of
-  DenseTransaction -> thawPagedDense
-  LocalTransaction -> thawPaged
-{-# INLINE thawPagedShaped #-}
-
-thawPagedDense :: U.Unbox a => Int -> Paged a -> ST s (MutablePaged s a)
-thawPagedDense requestedCapacity paged = do
-  let !count = pagedLength paged
-      !capacity = max requestedCapacity count
-  values <- MUV.new capacity
-  U.copy (MUV.unsafeSlice 0 count values) (toVector paged)
-  pure (MutableFlatPaged (pagedBits paged) values)
-
-flatMutableSection :: MutablePaged s a -> Maybe (FlatMutablePaged s a)
-flatMutableSection mutable = case mutable of
-  MutableFlatPaged _ values -> Just (FlatMutablePaged values)
-  MutableSharedPaged{} -> Nothing
-{-# INLINE flatMutableSection #-}
-
-readFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> ST s a
-readFlatMutable (FlatMutablePaged values) = MUV.unsafeRead values
-{-# INLINE readFlatMutable #-}
-
-writeFlatMutable :: U.Unbox a => FlatMutablePaged s a -> Int -> a -> ST s ()
-writeFlatMutable (FlatMutablePaged values) = MUV.unsafeWrite values
-{-# INLINE writeFlatMutable #-}
-
-readPaged :: U.Unbox a => MutablePaged s a -> Int -> ST s a
-readPaged mutable index = case mutable of
-  MutableFlatPaged _ values -> MUV.unsafeRead values index
-  MutableSharedPaged bits base dirtyRef _ -> do
-    dirty <- readSTRef dirtyRef
-    case IntMap.lookup (pageOf bits index) dirty of
-      Just values -> MUV.unsafeRead values (offsetOf bits index)
-      Nothing -> pure (pagedUnsafeIndex base index)
-{-# INLINE readPaged #-}
-
-writePaged :: U.Unbox a => MutablePaged s a -> Int -> a -> ST s ()
-writePaged mutable index value = case mutable of
-  MutableFlatPaged _ values -> MUV.unsafeWrite values index value
-  MutableSharedPaged bits base dirtyRef statsRef -> do
-    let !page = pageOf bits index
-        !start = page * pageSize bits
-        !existingWidth = min (pageSize bits) (max 0 (pagedLength base - start))
-    dirty <- readSTRef dirtyRef
-    values <- case IntMap.lookup page dirty of
-      Just existing -> pure existing
-      Nothing -> do
-        pageValues <- MUV.new (pageSize bits)
-        when (existingWidth > 0) $
-            U.copy
-              (MUV.unsafeSlice 0 existingWidth pageValues)
-              (U.generate existingWidth (\offset -> pagedUnsafeIndex base (start + offset)))
-        writeSTRef dirtyRef $! IntMap.insert page pageValues dirty
-        modifySTRef'
-          statsRef
-          (addPublicationStats
-             emptyPublicationStats
-               { publicationUnboxedBasePageOpens = if existingWidth > 0 then 1 else 0
-               , publicationUnboxedDirtyBasePages = if existingWidth > 0 then 1 else 0
-               , publicationUnboxedDirtyAppendedPages = if existingWidth > 0 then 0 else 1
-               , publicationUnboxedCopiedCells = existingWidth
-               })
-        pure pageValues
-    MUV.unsafeWrite values (offsetOf bits index) value
-{-# INLINE writePaged #-}
-
--- | Publish only dirty pages. A local publication retains one immutable root
--- and merges the changed page paths into its cumulative directory.
-freezePaged :: forall s a. U.Unbox a => Int -> MutablePaged s a -> ST s (Paged a)
-freezePaged count mutable
-  | count <= 0 = pure emptyPaged
-  | otherwise = case mutable of
-      MutableFlatPaged bits values
-        | MUV.length values <= count + (count `quot` 8) ->
-            FlatPaged count bits . U.take count <$> U.unsafeFreeze values
-        | otherwise ->
-            FlatPaged count bits <$> U.freeze (MUV.unsafeSlice 0 count values)
-      MutableSharedPaged bits base dirtyRef _statsRef -> do
-        dirty <- readSTRef dirtyRef
-        if IntMap.null dirty && count == pagedLength base
-          then pure base
-          else do
-            frozen <- traverse freezePage (IntMap.toAscList dirty)
-            let !priorDirectory = sharedDirectory base
-                !directory =
-                  foldl'
-                    (\accumulated (page, values) -> insertDirectory page values accumulated)
-                    priorDirectory
-                    frozen
-                !root = sharedRoot base
-            pure (SharedPaged count bits root (directoryRestrict (pagesFor bits count) directory))
- where
-  freezePage :: (Int, MUV.MVector s a) -> ST s (Int, U.Vector a)
-  freezePage (page, values) = do
-    frozen <- U.unsafeFreeze values
-    pure (page, frozen)
-
-sharedRoot :: Paged a -> U.Vector a
-sharedRoot paged = case paged of
-  FlatPaged _ _ values -> values
-  SharedPaged _ _ root _ -> root
-{-# INLINE sharedRoot #-}
-
-sharedDirectory :: Paged a -> PageDirectory (U.Vector a)
-sharedDirectory paged = case paged of
-  FlatPaged _ _ _ -> emptyDirectory
-  SharedPaged _ _ _ directory -> directory
-{-# INLINE sharedDirectory #-}
-
-readMutablePagedPublicationStats :: MutablePaged s a -> ST s PublicationStats
-readMutablePagedPublicationStats mutable = case mutable of
-  MutableFlatPaged _ _ -> pure emptyPublicationStats
-  MutableSharedPaged _ _ _ statsRef -> readSTRef statsRef
diff --git a/src-core/Moonlight/Triangulation/LineSideInfo.hs b/src-core/Moonlight/Triangulation/LineSideInfo.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/LineSideInfo.hs
+++ /dev/null
@@ -1,55 +0,0 @@
--- | Which side of a directed line a point falls on, as a total three-valued
--- answer that carries the collinear case rather than rounding it away.
-module Moonlight.Triangulation.LineSideInfo
-  ( LineSideInfo
-  , fromDeterminant
-  , fromOrdering
-  , sideOrdering
-  , isOnLeftSide
-  , isOnRightSide
-  , isOnLine
-  , isOnLeftSideOrLine
-  , isOnRightSideOrLine
-  , reverseSide
-  ) where
-
--- | A three-valued side: the collinear case is a value, not a rounding.
-newtype LineSideInfo = LineSideInfo Ordering
-  deriving stock (Show)
-  deriving newtype (Eq, Ord)
-
--- | The side an orientation determinant's sign names.
-fromDeterminant :: Double -> LineSideInfo
-fromDeterminant value = LineSideInfo (compare value 0)
-{-# INLINE fromDeterminant #-}
-
--- | The side an 'Ordering' names.
-fromOrdering :: Ordering -> LineSideInfo
-fromOrdering = LineSideInfo
-{-# INLINE fromOrdering #-}
-
--- | The underlying 'Ordering'.
-sideOrdering :: LineSideInfo -> Ordering
-sideOrdering (LineSideInfo ordering) = ordering
-{-# INLINE sideOrdering #-}
-
--- | The five side tests; the @OrLine@ pair admit the collinear case.
-isOnLeftSide, isOnRightSide, isOnLine, isOnLeftSideOrLine, isOnRightSideOrLine :: LineSideInfo -> Bool
-isOnLeftSide (LineSideInfo ordering) = ordering == GT
-isOnRightSide (LineSideInfo ordering) = ordering == LT
-isOnLine (LineSideInfo ordering) = ordering == EQ
-isOnLeftSideOrLine side = not (isOnRightSide side)
-isOnRightSideOrLine side = not (isOnLeftSide side)
-{-# INLINE isOnLeftSide #-}
-{-# INLINE isOnRightSide #-}
-{-# INLINE isOnLine #-}
-{-# INLINE isOnLeftSideOrLine #-}
-{-# INLINE isOnRightSideOrLine #-}
-
--- | The same point, seen along the reversed line.
-reverseSide :: LineSideInfo -> LineSideInfo
-reverseSide (LineSideInfo ordering) = LineSideInfo $ case ordering of
-  LT -> GT
-  EQ -> EQ
-  GT -> LT
-{-# INLINE reverseSide #-}
diff --git a/src-core/Moonlight/Triangulation/Scalar.hs b/src-core/Moonlight/Triangulation/Scalar.hs
deleted file mode 100644
--- a/src-core/Moonlight/Triangulation/Scalar.hs
+++ /dev/null
@@ -1,124 +0,0 @@
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | The binary64 coordinate kernel and exact predicate boundary.
-module Moonlight.Triangulation.Scalar
-  ( scalarName
-  , scalarByteSize
-  , scalarBinaryFormat
-  , scalarEpsilon
-  , scalarUnitRoundoff
-  , scalarCcwErrorBound
-  , scalarInCircleErrorBound
-  , circumradiusSquaredWithinCoordinates
-  , BinaryFormat
-  , formatRadix
-  , formatMantissaDigits
-  , formatExponentRange
-  , minimumAllowedCoordinate
-  , maximumAllowedCoordinate
-  , isFinite
-  , canonicalScalarZero
-  ) where
-
-import Moonlight.Triangulation.Internal.Dyadic
-  ( exactCircumradiusSquaredWithin
-  )
-
--- | The coordinate component of canonical point identity. IEEE signed zeros
--- compare equal but hash differently by bits; every coordinate-keyed owner
--- therefore normalizes them before storage or hashing.
-canonicalScalarZero :: Double -> Double
-canonicalScalarZero value
-  | value == 0 = 0
-  | otherwise = value
-{-# INLINE canonicalScalarZero #-}
-
--- | Machine format of the coordinate scalar.
-data BinaryFormat = BinaryFormat
-  { formatRadix :: !Integer
-    -- ^ Numeric base of the significand.
-  , formatMantissaDigits :: !Int
-    -- ^ Number of base-'formatRadix' digits in the significand.
-  , formatExponentRange :: !(Int, Int)
-    -- ^ Inclusive minimum and exclusive maximum exponent bounds.
-  }
-  deriving stock (Eq, Show)
-
--- | Stable name of the coordinate scalar.
-scalarName :: String
-scalarName = "binary64"
-
--- | Bytes occupied by one coordinate component.
-scalarByteSize :: Int
-scalarByteSize = 8
-
--- | Runtime-confirmed binary format of 'Double'.
-scalarBinaryFormat :: BinaryFormat
-scalarBinaryFormat =
-  BinaryFormat
-    { formatRadix = floatRadix (0 :: Double)
-    , formatMantissaDigits = floatDigits (0 :: Double)
-    , formatExponentRange = floatRange (0 :: Double)
-    }
-
--- | Difference between one and the next representable value above one.
-scalarEpsilon :: Double
-scalarEpsilon = 2.220446049250313e-16
-
--- | Maximum relative rounding error of one binary64 operation.
-scalarUnitRoundoff :: Double
-scalarUnitRoundoff = 1.1102230246251565e-16
-
--- | Error coefficient for the filtered orientation predicate.
-scalarCcwErrorBound :: Double
-scalarCcwErrorBound = 3.3306690738754716e-16
-
--- | Error coefficient for the filtered in-circle predicate.
-scalarInCircleErrorBound :: Double
-scalarInCircleErrorBound = 1.1102230246251577e-15
-
--- | Closed exact circumradius membership at a finite, non-negative binary64
--- threshold. Invalid thresholds and collinear triples are outside.
-circumradiusSquaredWithinCoordinates
-  :: Double
-  -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Bool
-circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy
-    | threshold < 0 || not (isFinite threshold) = False
-    | not (isFinite ax && isFinite ay && isFinite bx
-        && isFinite by && isFinite cx && isFinite cy) = False
-    | isFinite difference && isFinite tolerance
-        && tolerance > 0 && abs difference > tolerance = difference <= 0
-    | otherwise = exactCircumradiusSquaredWithin threshold ax ay bx by cx cy
-   where
-    !abx = bx - ax
-    !aby = by - ay
-    !acx = cx - ax
-    !acy = cy - ay
-    !bcx = cx - bx
-    !bcy = cy - by
-    !abSquared = abx * abx + aby * aby
-    !acSquared = acx * acx + acy * acy
-    !bcSquared = bcx * bcx + bcy * bcy
-    !determinant = abx * acy - aby * acx
-    !radiusNumerator = abSquared * acSquared * bcSquared
-    !thresholdDenominator = 4 * determinant * determinant * threshold
-    !difference = radiusNumerator - thresholdDenominator
-    !permanent = abs radiusNumerator + abs thresholdDenominator
-    !tolerance = 128 * scalarUnitRoundoff * permanent
-
-{-# INLINE circumradiusSquaredWithinCoordinates #-}
-
--- | Whether a scalar is neither infinite nor NaN. Pure subtraction avoids the
--- FFI calls used by base's predicates in the supported GHC.
-isFinite :: Double -> Bool
-isFinite value = value - value == 0
-{-# INLINE isFinite #-}
-
--- | The smallest coordinate magnitude the exact predicates accept.
-minimumAllowedCoordinate :: Double
-minimumAllowedCoordinate = 1.793662034335766e-43
-
--- | The largest coordinate magnitude the exact predicates accept.
-maximumAllowedCoordinate :: Double
-maximumAllowedCoordinate = 3.2138760885179806e60
diff --git a/src-dcel/Moonlight/Planar/Affine.hs b/src-dcel/Moonlight/Planar/Affine.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Affine.hs
@@ -0,0 +1,181 @@
+-- | Exact planar affine actions. General maps may collapse authored geometry;
+-- only the non-singular refinement transports admitted polygon topology.
+module Moonlight.Planar.Affine
+  ( Affine2
+  , affine2
+  , affineColumns
+  , identityAffine2
+  , composeAffine2
+  , transformPoint
+  , transformVector
+  , affineDeterminant
+  , AffineIso2
+  , affineIso2
+  , affineIsoMap
+  , identityAffineIso2
+  , translationAffineIso2
+  , composeAffineIso2
+  , inverseAffineIso2
+  , transformExactLoop
+  , transformPolygonComponent
+  , transformPlanarRegion
+  , transformConvexPolygon
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.List (sort)
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , ExactVector (..)
+  , PositiveExact
+  , addExactVectors
+  , exactCross
+  , exactPoint
+  , exactPointCoordinates
+  , divideByPositive
+  , multiplyPositive
+  , positiveExact
+  , positiveOne
+  , ratioPositive
+  , translateExactPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (rotateCycleLeast)
+import Moonlight.Planar.Internal.Region.Types
+  ( ConvexPolygon (..)
+  , ExactLoop (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  )
+
+-- | Two linear columns followed by translation. No invertibility is assumed.
+data Affine2 = Affine2 !ExactVector !ExactVector !ExactVector
+  deriving stock (Eq, Ord, Show)
+
+instance NFData Affine2 where
+  rnf (Affine2 x y offset) = rnf x `seq` rnf y `seq` rnf offset
+
+affine2 :: ExactVector -> ExactVector -> ExactVector -> Affine2
+affine2 = Affine2
+
+affineColumns :: Affine2 -> (ExactVector, ExactVector, ExactVector)
+affineColumns (Affine2 x y offset) = (x, y, offset)
+
+identityAffine2 :: Affine2
+identityAffine2 = Affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector 0 0)
+
+-- | @composeAffine2 outer inner@ acts as @outer . inner@.
+composeAffine2 :: Affine2 -> Affine2 -> Affine2
+composeAffine2 outer@(Affine2 _ _ offset) (Affine2 x y innerOffset) =
+  Affine2
+    (transformVector outer x)
+    (transformVector outer y)
+    (addExactVectors (transformVector outer innerOffset) offset)
+
+-- | Translation does not act on vectors.
+transformVector :: Affine2 -> ExactVector -> ExactVector
+transformVector (Affine2 (ExactVector a b) (ExactVector c d) _) (ExactVector x y) =
+  ExactVector (a * x + c * y) (b * x + d * y)
+
+transformPoint :: Affine2 -> ExactPoint -> ExactPoint
+transformPoint transform@(Affine2 _ _ offset) point =
+  let (x, y) = exactPointCoordinates point
+      ExactVector mappedX mappedY = transformVector transform (ExactVector x y)
+   in translateExactPoint (exactPoint mappedX mappedY) offset
+
+affineDeterminant :: Affine2 -> ExactRational
+affineDeterminant (Affine2 x y _) = exactCross x y
+
+data Orientation = Preserving | Reversing
+  deriving stock (Eq, Ord, Show)
+
+-- | A nonzero determinant, including its sign and positive magnitude, admitted
+-- once. Inversion consumes the same divisor witness without readmission.
+data AffineIso2 = AffineIso2 !Affine2 !Orientation !PositiveExact
+  deriving stock (Eq, Ord, Show)
+
+instance NFData AffineIso2 where
+  rnf (AffineIso2 transform orientation magnitude) =
+    rnf transform `seq` orientation `seq` rnf magnitude
+
+affineIso2 :: Affine2 -> Maybe AffineIso2
+affineIso2 transform =
+  let determinant = affineDeterminant transform
+      orientation = if determinant < 0 then Reversing else Preserving
+   in case positiveExact (abs determinant) of
+        Left _ -> Nothing
+        Right magnitude -> Just (AffineIso2 transform orientation magnitude)
+
+affineIsoMap :: AffineIso2 -> Affine2
+affineIsoMap (AffineIso2 transform _ _) = transform
+
+identityAffineIso2 :: AffineIso2
+identityAffineIso2 = AffineIso2 identityAffine2 Preserving positiveOne
+
+-- | Translation preserves the identity linear frame. Its determinant is one
+-- by construction, so no dynamic admission is required for layout or ports.
+translationAffineIso2 :: ExactVector -> AffineIso2
+translationAffineIso2 offset = AffineIso2
+  (Affine2 (ExactVector 1 0) (ExactVector 0 1) offset) Preserving positiveOne
+
+-- | Composition preserves invertibility; orientation signs multiply.
+composeAffineIso2 :: AffineIso2 -> AffineIso2 -> AffineIso2
+composeAffineIso2 (AffineIso2 outer outerOrientation outerMagnitude) (AffineIso2 inner innerOrientation innerMagnitude) =
+  AffineIso2
+    (composeAffine2 outer inner)
+    (composeOrientation outerOrientation innerOrientation)
+    (multiplyPositive outerMagnitude innerMagnitude)
+ where
+  composeOrientation :: Orientation -> Orientation -> Orientation
+  composeOrientation Preserving orientation = orientation
+  composeOrientation Reversing Preserving = Reversing
+  composeOrientation Reversing Reversing = Preserving
+
+-- | Exact inverse using the admitted determinant magnitude as divisor. The
+-- inverse has the same orientation and reciprocal determinant magnitude.
+inverseAffineIso2 :: AffineIso2 -> AffineIso2
+inverseAffineIso2
+  (AffineIso2 (Affine2 (ExactVector a b) (ExactVector c d) (ExactVector x y)) orientation magnitude) =
+    AffineIso2
+      (Affine2
+        (ExactVector (divide d) (divide (negate b)))
+        (ExactVector (divide (negate c)) (divide a))
+        (ExactVector (divide (c * y - d * x)) (divide (b * x - a * y))))
+      orientation
+      (ratioPositive positiveOne magnitude)
+ where
+  divide :: ExactRational -> ExactRational
+  divide numerator = divideByPositive (orient numerator) magnitude
+  orient :: ExactRational -> ExactRational
+  orient = case orientation of
+    Preserving -> id
+    Reversing -> negate
+
+-- | Preserve the source winding and canonical least starting vertex. This
+-- polygonal action intentionally differs from traversal-preserving curve maps.
+transformExactLoop :: AffineIso2 -> ExactLoop -> ExactLoop
+transformExactLoop (AffineIso2 transform orientation _) (ExactLoop points) =
+  ExactLoop (rotateCycleLeast (orient (fmap (transformPoint transform) points)))
+ where
+  orient :: NonEmpty.NonEmpty ExactPoint -> NonEmpty.NonEmpty ExactPoint
+  orient = case orientation of
+    Preserving -> id
+    Reversing -> NonEmpty.reverse
+
+-- | Invertibility preserves boundary separation and hole containment.
+transformPolygonComponent :: AffineIso2 -> PolygonComponent -> PolygonComponent
+transformPolygonComponent transform (PolygonComponent outer holes) =
+  PolygonComponent
+    (transformExactLoop transform outer)
+    (sort (fmap (transformExactLoop transform) holes))
+
+-- | Disjoint interiors remain disjoint; transformed canonical order may differ.
+transformPlanarRegion :: AffineIso2 -> PlanarRegion -> PlanarRegion
+transformPlanarRegion transform (PlanarRegion components) =
+  PlanarRegion (sort (fmap (transformPolygonComponent transform) components))
+
+-- | Strict convexity survives every non-singular affine map.
+transformConvexPolygon :: AffineIso2 -> ConvexPolygon -> ConvexPolygon
+transformConvexPolygon transform (ConvexPolygon loop) =
+  ConvexPolygon (transformExactLoop transform loop)
diff --git a/src-dcel/Moonlight/Planar/Alpha.hs b/src-dcel/Moonlight/Planar/Alpha.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Alpha.hs
@@ -0,0 +1,447 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE RankNTypes #-}
+
+-- | Exact alpha births over one resident Delaunay triangulation. Geometry
+-- remains owned by the DCEL and 'ExactCellSet'; this module supplies the
+-- handle-indexed filtration section used by topology consumers.
+module Moonlight.Planar.Alpha
+  ( AlphaBirth
+  , alphaBirthNumerator
+  , alphaBirthDenominator
+  , alphaBirthToDouble
+  , alphaBirthFromRadiusSquared
+  , AlphaFiltration
+  , AlphaFiltrationError (..)
+  , alphaFiltration
+  , alphaFiltrationCellSet
+  , withAlphaResidentTriangulation
+  , alphaVertexBirth
+  , alphaEdgeBirth
+  , alphaFaceBirth
+  , alphaFiltrationCriticalBirths
+  , alphaComplexAtBirth
+  , alphaComplexAtRadius
+  , alphaShapeContainsFace
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.Bifunctor (first)
+import Data.Bits (shiftL)
+import Data.IntMap.Strict qualified as IntMap
+import Data.IntSet qualified as IntSet
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Set qualified as Set
+import Data.Vector (Vector)
+import Data.Vector qualified as Vector
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+import Moonlight.Planar.CellSet (CellSelectionError)
+import Moonlight.Planar.Dcel qualified as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  , directedPair
+  , faceIdIndex
+  , undirectedEdgeIdIndex
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.CellSet
+  ( ExactCellSet (..)
+  , residentExactCellSet
+  )
+import Moonlight.Planar.Internal.Dyadic
+  ( exactCircumradiusSquaredRational
+  , exactQuarterSquaredDistanceRational
+  , integerBitLength
+  , integerRatioToDouble
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactArithmeticError (..)
+  , ExactRational
+  , exactRationalDenominator
+  , exactRationalFromFiniteDouble
+  , exactRationalNumerator
+  )
+import Moonlight.Planar.Internal.Representation
+  ( DelaunayTriangulation
+  , Triangulation
+  )
+import Moonlight.Planar.Internal.Predicates (inDiametralCircle)
+import Moonlight.Planar.Scalar (circumradiusSquaredWithinCoordinates)
+import Moonlight.Planar.Types (ConstraintMode (Unconstrained))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (RadiusSquared, radiusSquaredValue)
+
+-- | One exact squared-radius birth. The constructor is withheld so negative
+-- exact rationals cannot masquerade as geometric radii.
+data AlphaBirth = AlphaBirth !AlphaOrderKey !ExactRational
+
+instance NFData AlphaBirth where
+  rnf (AlphaBirth key value) = rnf key `seq` rnf value
+
+data AlphaOrderKey
+  = NonPositiveAlphaOrder
+  | PositiveAlphaOrder !Int !Word64
+  deriving stock (Eq, Ord, Generic)
+  deriving anyclass (NFData)
+
+instance Eq AlphaBirth where
+  AlphaBirth _ leftValue == AlphaBirth _ rightValue = leftValue == rightValue
+
+instance Ord AlphaBirth where
+  compare (AlphaBirth leftKey leftValue) (AlphaBirth rightKey rightValue) =
+    case compare leftKey rightKey of
+      EQ -> compare leftValue rightValue
+      distinctKeyOrder -> distinctKeyOrder
+
+instance Show AlphaBirth where
+  showsPrec precedence (AlphaBirth _ value) =
+    showParen
+      (precedence > 10)
+      (showString "AlphaBirth " . showsPrec 11 value)
+
+-- | Numerator of the reduced exact squared-radius birth.
+alphaBirthNumerator :: AlphaBirth -> Integer
+alphaBirthNumerator (AlphaBirth _ value) = exactRationalNumerator value
+
+-- | Positive denominator of the reduced exact squared-radius birth.
+alphaBirthDenominator :: AlphaBirth -> Integer
+alphaBirthDenominator (AlphaBirth _ value) = exactRationalDenominator value
+
+-- | Binary64 display projection. Equality and filtration order must use the
+-- 'AlphaBirth' itself, not this rounded view.
+alphaBirthToDouble :: AlphaBirth -> Double
+alphaBirthToDouble birth =
+  integerRatioToDouble
+    (alphaBirthNumerator birth)
+    (alphaBirthDenominator birth)
+
+-- | Embed an admitted binary64 threshold into the exact alpha order without
+-- loss. Every finite binary64 value is a dyadic rational.
+alphaBirthFromRadiusSquared :: RadiusSquared -> AlphaBirth
+alphaBirthFromRadiusSquared =
+  alphaBirthFromExact . exactRationalFromFiniteDouble . radiusSquaredValue
+
+-- | The exact handle-indexed filtration of one Delaunay DCEL. The cell set is
+-- the only exact geometry inventory; edge and face vectors are derived birth
+-- sections over its resident handles. Vertex birth is the constant zero section,
+-- not a stored vector of repeated values.
+data AlphaFiltration where
+  AlphaFiltration
+    :: { alphaFiltrationResident :: !(DelaunayTriangulation vertex)
+       , alphaFiltrationCells :: !ExactCellSet
+       , alphaEdgeBirths :: !(Vector AlphaBirth)
+       , alphaFaceBirths :: !(Vector AlphaBirth)
+       }
+    -> AlphaFiltration
+
+data FaceAlphaSection = FaceAlphaSection
+  { faceAlphaBirth :: !AlphaBirth
+  , faceAlphaVertices :: !(VertexId, VertexId, VertexId)
+  }
+
+-- | Typed obstruction to deriving the exact birth section from a resident
+-- Delaunay triangulation. Every incidence failure retains the affected handle.
+data AlphaFiltrationError
+  = AlphaFiltrationCellSelection !CellSelectionError
+  | AlphaFaceVerticesUnavailable !FaceId
+  | AlphaFaceDegenerate !FaceId
+  | AlphaCircumradiusDivisionFailed !FaceId !ExactArithmeticError
+  | AlphaFaceEdgeIncidenceInvalid !FaceId !UndirectedEdgeId ![VertexId]
+  | AlphaIncidentFaceBirthMissing !UndirectedEdgeId !FaceId
+  | AlphaEdgeBirthWitnessMissing !UndirectedEdgeId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Construct every exact alpha birth once. Faces descend first because a
+-- non-Gabriel edge is born at the least incident-face circumradius.
+alphaFiltration
+  :: DelaunayTriangulation vertex
+  -> Either AlphaFiltrationError AlphaFiltration
+alphaFiltration triangulation = do
+  cellSet <-
+    first AlphaFiltrationCellSelection
+      (residentExactCellSet triangulation)
+  faceSections <-
+    Vector.imapM
+      (\faceIndex -> exactFaceSection triangulation (FaceId (fromIntegral (faceIndex + 1))))
+      (Dcel.innerFaceVertexTriples triangulation)
+  edgeBirths <-
+    Vector.generateM
+      (Dcel.numUndirectedEdges triangulation)
+      (exactEdgeBirth faceSections triangulation . UndirectedEdgeId . fromIntegral)
+  let faceBirths = fmap faceAlphaBirth faceSections
+  pure
+    AlphaFiltration
+      { alphaFiltrationResident = triangulation
+      , alphaFiltrationCells = cellSet
+      , alphaEdgeBirths = edgeBirths
+      , alphaFaceBirths = faceBirths
+      }
+
+-- | The authoritative resident cells underlying this filtration. This is a
+-- read-only projection rather than an exported record field, so callers cannot
+-- splice births from one triangulation onto the cells of another.
+alphaFiltrationCellSet :: AlphaFiltration -> ExactCellSet
+alphaFiltrationCellSet = alphaFiltrationCells
+
+-- | Eliminate the resident Delaunay witness sealed together with this exact
+-- birth section. Dense native interpreters retain their original handle
+-- arithmetic; an arrangement cell selection cannot enter this branch.
+withAlphaResidentTriangulation
+  :: (forall vertex. DelaunayTriangulation vertex -> result)
+  -> AlphaFiltration
+  -> result
+withAlphaResidentTriangulation interpret (AlphaFiltration triangulation _ _ _) =
+  interpret triangulation
+
+-- | Exact birth of a resident vertex, when the handle belongs to this
+-- filtration.
+alphaVertexBirth :: AlphaFiltration -> VertexId -> Maybe AlphaBirth
+alphaVertexBirth filtration vertex =
+  withAlphaResidentTriangulation
+    (\triangulation ->
+      if vertexIdIndex vertex < Dcel.numVertices triangulation
+        then Just zeroAlphaBirth
+        else Nothing)
+    filtration
+
+-- | Exact birth of a resident undirected edge, when the handle belongs to this
+-- filtration.
+alphaEdgeBirth :: AlphaFiltration -> UndirectedEdgeId -> Maybe AlphaBirth
+alphaEdgeBirth filtration edge =
+  alphaEdgeBirths filtration Vector.!? undirectedEdgeIdIndex edge
+
+-- | Exact birth of a resident bounded face, when the handle belongs to this
+-- filtration.
+alphaFaceBirth :: AlphaFiltration -> FaceId -> Maybe AlphaBirth
+alphaFaceBirth filtration =
+  faceBirthAt (alphaFaceBirths filtration)
+
+-- | Distinct critical births in exact ascending order.
+alphaFiltrationCriticalBirths :: AlphaFiltration -> [AlphaBirth]
+alphaFiltrationCriticalBirths filtration =
+  Set.toAscList
+    ( Set.fromList
+        ( withAlphaResidentTriangulation
+            (\triangulation -> if Dcel.numVertices triangulation == 0 then [] else [zeroAlphaBirth])
+            filtration
+            <> Vector.toList (alphaEdgeBirths filtration)
+            <> Vector.toList (alphaFaceBirths filtration)
+        )
+    )
+
+-- | Select the closed alpha subcomplex at one exact birth by restricting the
+-- resident cell set. That set is the lineage witness: its handles are this
+-- DCEL's dense families and its point map already carries every exact
+-- coordinate. Births descend the face relation exactly (a vertex is born at
+-- zero, an edge no later than any incident face), so the restriction is
+-- downward closed and nothing is proved a second time.
+alphaComplexAtBirth
+  :: AlphaBirth
+  -> AlphaFiltration
+  -> ExactCellSet
+alphaComplexAtBirth threshold filtration =
+  case alphaFiltrationCells filtration of
+    ExactCellSet incidence points edges faces ->
+      ExactCellSet
+        incidence
+        (if zeroAlphaBirth <= threshold then points else IntMap.empty)
+        (IntSet.filter (bornBy (alphaEdgeBirths filtration)) edges)
+        (IntSet.filter (bornBy (alphaFaceBirths filtration) . subtract 1) faces)
+ where
+  bornBy :: Vector AlphaBirth -> Int -> Bool
+  bornBy births index = births Vector.! index <= threshold
+
+alphaComplexAtRadius
+  :: RadiusSquared
+  -> AlphaFiltration
+  -> ExactCellSet
+alphaComplexAtRadius radius =
+  alphaComplexAtBirth (alphaBirthFromRadiusSquared radius)
+
+-- | Membership of a bounded face in the closed alpha shape. Exact dyadic
+-- comparison makes equality independent of circumcenter rounding. This
+-- specialized observation does not construct the full filtration.
+alphaShapeContainsFace
+  :: RadiusSquared
+  -> Triangulation 'Unconstrained vertex directed undirected face
+  -> FaceId
+  -> Bool
+alphaShapeContainsFace radius triangulation =
+  maybe False withinRadius . Dcel.innerFaceVertices triangulation
+ where
+  threshold = radiusSquaredValue radius
+  withinRadius (firstVertex, secondVertex, thirdVertex) =
+    let Point ax ay = Dcel.vertexPoint triangulation firstVertex
+        Point bx by = Dcel.vertexPoint triangulation secondVertex
+        Point cx cy = Dcel.vertexPoint triangulation thirdVertex
+     in circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy
+
+zeroAlphaBirth :: AlphaBirth
+zeroAlphaBirth = alphaBirthFromExact 0
+
+exactFaceSection
+  :: DelaunayTriangulation vertex
+  -> FaceId
+  -> (VertexId, VertexId, VertexId)
+  -> Either AlphaFiltrationError FaceAlphaSection
+exactFaceSection triangulation face verticesValue@(firstVertex, secondVertex, thirdVertex) =
+  let Point ax ay = Dcel.vertexPoint triangulation firstVertex
+      Point bx by = Dcel.vertexPoint triangulation secondVertex
+      Point cx cy = Dcel.vertexPoint triangulation thirdVertex
+   in case exactCircumradiusSquaredRational ax ay bx by cx cy of
+        Left ExactZeroDivisor -> Left (AlphaFaceDegenerate face)
+        Left arithmeticError -> Left (AlphaCircumradiusDivisionFailed face arithmeticError)
+        Right exactBirth ->
+          Right
+            FaceAlphaSection
+              { faceAlphaBirth = alphaBirthFromExact exactBirth
+              , faceAlphaVertices = verticesValue
+              }
+
+exactEdgeBirth
+  :: Vector FaceAlphaSection
+  -> DelaunayTriangulation vertex
+  -> UndirectedEdgeId
+  -> Either AlphaFiltrationError AlphaBirth
+exactEdgeBirth faceSections triangulation edge = do
+  let (fromVertex, toVertex) = Dcel.undirectedEndpoints triangulation edge
+      fromPoint = Dcel.vertexPoint triangulation fromVertex
+      toPoint = Dcel.vertexPoint triangulation toVertex
+      incidentFaces = innerIncidentFaces triangulation edge
+  oppositeVertices <-
+    traverse
+      (oppositeVertexAcross faceSections triangulation edge fromVertex toVertex)
+      incidentFaces
+  let oppositePoints = fmap (Dcel.vertexPoint triangulation) oppositeVertices
+  if all (diametralWitnessOutside fromPoint toPoint) oppositePoints
+    then pure (alphaBirthFromExact (quarterSquaredPointDistance fromPoint toPoint))
+    else leastIncidentFaceBirth edge faceSections incidentFaces
+
+innerIncidentFaces
+  :: DelaunayTriangulation vertex
+  -> UndirectedEdgeId
+  -> [FaceId]
+innerIncidentFaces triangulation edge =
+  let (forward, backward) = directedPair edge
+      forwardFace = Dcel.incidentFace triangulation forward
+      backwardFace = Dcel.incidentFace triangulation backward
+   in case (forwardFace == Dcel.outerFace, backwardFace == Dcel.outerFace) of
+        (True, True) -> []
+        (False, True) -> [forwardFace]
+        (True, False) -> [backwardFace]
+        (False, False)
+          | forwardFace == backwardFace -> [forwardFace]
+          | forwardFace < backwardFace -> [forwardFace, backwardFace]
+          | otherwise -> [backwardFace, forwardFace]
+
+oppositeVertexAcross
+  :: Vector FaceAlphaSection
+  -> DelaunayTriangulation vertex
+  -> UndirectedEdgeId
+  -> VertexId
+  -> VertexId
+  -> FaceId
+  -> Either AlphaFiltrationError VertexId
+oppositeVertexAcross faceSections triangulation edge fromVertex toVertex face =
+  case faceSectionAt faceSections face of
+    Nothing ->
+      Left
+        (AlphaFaceEdgeIncidenceInvalid face edge (Dcel.faceVertices triangulation face))
+    Just faceSection ->
+      let verticesValue@(firstVertex, secondVertex, thirdVertex) = faceAlphaVertices faceSection
+          outsideEdge vertex = vertex /= fromVertex && vertex /= toVertex
+       in case
+            ( outsideEdge firstVertex
+            , outsideEdge secondVertex
+            , outsideEdge thirdVertex
+            )
+          of
+            (True, False, False) -> Right firstVertex
+            (False, True, False) -> Right secondVertex
+            (False, False, True) -> Right thirdVertex
+            _ ->
+              Left
+                ( AlphaFaceEdgeIncidenceInvalid
+                    face
+                    edge
+                    (filter outsideEdge (triangleVertices verticesValue))
+                )
+
+diametralWitnessOutside :: Point -> Point -> Point -> Bool
+diametralWitnessOutside firstPoint secondPoint =
+  not . inDiametralCircle firstPoint secondPoint
+
+quarterSquaredPointDistance :: Point -> Point -> ExactRational
+quarterSquaredPointDistance (Point ax ay) (Point bx by) =
+  exactQuarterSquaredDistanceRational ax ay bx by
+
+leastIncidentFaceBirth
+  :: UndirectedEdgeId
+  -> Vector FaceAlphaSection
+  -> [FaceId]
+  -> Either AlphaFiltrationError AlphaBirth
+leastIncidentFaceBirth edge faceSections incidentFaces = do
+  births <-
+    traverse
+      (\face -> maybe (Left (AlphaIncidentFaceBirthMissing edge face)) (Right . faceAlphaBirth) (faceSectionAt faceSections face))
+      incidentFaces
+  case NonEmpty.nonEmpty births of
+    Nothing -> Left (AlphaEdgeBirthWitnessMissing edge)
+    Just nonEmptyBirths -> Right (minimum nonEmptyBirths)
+
+faceSectionAt :: Vector FaceAlphaSection -> FaceId -> Maybe FaceAlphaSection
+faceSectionAt faceSections face
+  | face == Dcel.outerFace = Nothing
+  | otherwise = faceSections Vector.!? (faceIdIndex face - 1)
+
+faceBirthAt :: Vector AlphaBirth -> FaceId -> Maybe AlphaBirth
+faceBirthAt faceBirths face
+  | face == Dcel.outerFace = Nothing
+  | otherwise = faceBirths Vector.!? (faceIdIndex face - 1)
+
+triangleVertices :: (VertexId, VertexId, VertexId) -> [VertexId]
+triangleVertices (firstVertex, secondVertex, thirdVertex) =
+  [firstVertex, secondVertex, thirdVertex]
+
+alphaBirthFromExact :: ExactRational -> AlphaBirth
+alphaBirthFromExact exactValue =
+  AlphaBirth (alphaOrderKey exactValue) exactValue
+
+-- | A monotone exact fixed-point projection of the positive rational. Distinct
+-- keys prove order; collisions descend to the authoritative rational. One
+-- division at birth replaces repeated cross-products during every downstream
+-- ordered-map comparison.
+alphaOrderKey :: ExactRational -> AlphaOrderKey
+alphaOrderKey exactValue =
+  let numerator = exactRationalNumerator exactValue
+      denominator = exactRationalDenominator exactValue
+   in if numerator <= 0
+        then NonPositiveAlphaOrder
+        else
+          let candidateExponent = integerBitLength numerator - integerBitLength denominator
+              binaryExponent =
+                if candidateExponent >= 0
+                  then
+                    if numerator < denominator `shiftL` candidateExponent
+                      then candidateExponent - 1
+                      else candidateExponent
+                else
+                  if numerator `shiftL` negate candidateExponent < denominator
+                    then candidateExponent - 1
+                    else candidateExponent
+              scale = alphaOrderFractionBits - binaryExponent
+              scaledMantissa =
+                if scale >= 0
+                  then (numerator `shiftL` scale) `quot` denominator
+                  else numerator `quot` (denominator `shiftL` negate scale)
+           in PositiveAlphaOrder binaryExponent (fromIntegral scaledMantissa)
+
+alphaOrderFractionBits :: Int
+alphaOrderFractionBits = 16
diff --git a/src-dcel/Moonlight/Planar/CellSet.hs b/src-dcel/Moonlight/Planar/CellSet.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/CellSet.hs
@@ -0,0 +1,17 @@
+-- | Finite downward-closed selections of relatively open vertices, edges, and
+-- bounded faces. A selection retains shared geometry-free incidence and exact
+-- coordinates for selected vertices behind an opaque carrier.
+module Moonlight.Planar.CellSet
+  ( ExactCellSet
+  , CellSelectionError (..)
+  , exactCellSet
+  , closeFaceCellSet
+  , exactCellSetVertexCount
+  , exactCellSetEdgeCount
+  , exactCellSetFaceCount
+  , foldExactCellVertices
+  , foldExactCellEdges
+  , foldExactCellFaces
+  ) where
+
+import Moonlight.Planar.Internal.CellSet
diff --git a/src-dcel/Moonlight/Planar/Convex.hs b/src-dcel/Moonlight/Planar/Convex.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Convex.hs
@@ -0,0 +1,259 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact convex polygons: checked admission, canonical hull construction,
+-- retained-polygon publication, and geometry observations. Morphology consumes
+-- this owner; convex geometry has no dependency on morphology or overlay.
+module Moonlight.Planar.Convex
+  ( ConvexPolygon
+  , ConvexError (..)
+  , convexPolygon
+  , convexPolygonPoints
+  , convexPolygonBoundary
+  , convexPolygonComponent
+  , convexPolygonRegion
+  , convexPolygonFromRetained
+  , retainConvexPolygon
+  , retainTranslatedConvexPolygon
+  , convexPolygonFromLoop
+  , convexHullPolygon
+  , reflectConvexPolygon
+  , convexPolygonCentroid
+  , decomposeRegionIntoConvexSlabs
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import qualified Data.Map.Strict as Map
+import Data.Maybe (catMaybes)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactArithmeticError
+  , ExactPoint
+  , ExactRational
+  , ExactRetainedPolygon
+  , ExactVector
+  , exactDivide
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , exactRetainedPolygonPoints
+  , translateExactPoint
+  )
+import Moonlight.Planar.Internal.Exact (retainAdmittedConvexCycle)
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( cyclePairs
+  , consecutivePairs
+  , firstNonCounterClockwiseTurn
+  , rotateCycleLeast
+  )
+import Moonlight.Planar.Internal.ExactRational (exactRationalFromDyadic)
+import Moonlight.Planar.Internal.Region.Types
+  ( ConvexPolygon (..)
+  , ExactLoop (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , RegionValidationError
+  )
+import Moonlight.Planar.Region (exactLoop, exactLoopPoints, regionBoundaryEdges)
+
+-- | Convex admission and exact geometry failures, independent of any consumer.
+data ConvexError
+  = ConvexInvalidLoop !RegionValidationError
+  | ConvexNonConvexTurn !Int !Ordering
+  | ConvexHullDegenerate ![ExactPoint]
+  | ConvexExactArithmetic !ExactArithmeticError
+  | ConvexSlabOddCrossings !ExactRational !ExactRational !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+convexPolygon :: NonEmpty ExactPoint -> Either ConvexError ConvexPolygon
+convexPolygon submitted = do
+  loop <- first ConvexInvalidLoop (exactLoop submitted)
+  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
+    Just (index, turn) -> Left (ConvexNonConvexTurn index turn)
+    Nothing -> Right (ConvexPolygon loop)
+
+convexPolygonPoints :: ConvexPolygon -> NonEmpty ExactPoint
+convexPolygonPoints (ConvexPolygon loop) = exactLoopPoints loop
+
+convexPolygonComponent :: ConvexPolygon -> PolygonComponent
+convexPolygonComponent (ConvexPolygon loop) = PolygonComponent loop []
+
+convexPolygonRegion :: ConvexPolygon -> PlanarRegion
+convexPolygonRegion = PlanarRegion . pure . convexPolygonComponent
+
+-- | Strict convexity and simplicity descend from the retained-line carrier;
+-- publication only rotates the cycle to its canonical least point.
+convexPolygonFromRetained :: ExactRetainedPolygon -> ConvexPolygon
+convexPolygonFromRetained = admittedConvexPolygon . exactRetainedPolygonPoints
+
+-- | Retain the existing simple, strictly convex cycle without readmission.
+retainConvexPolygon :: ConvexPolygon -> ExactRetainedPolygon
+retainConvexPolygon = retainAdmittedConvexCycle . convexPolygonPoints
+
+-- | Exact translation preserves admission and the cycle's starting position.
+retainTranslatedConvexPolygon :: ExactVector -> ConvexPolygon -> ExactRetainedPolygon
+retainTranslatedConvexPolygon offset =
+  retainAdmittedConvexCycle . fmap (`translateExactPoint` offset) . convexPolygonPoints
+
+admittedConvexPolygon :: NonEmpty ExactPoint -> ConvexPolygon
+admittedConvexPolygon = ConvexPolygon . ExactLoop . rotateCycleLeast
+
+-- | A simple loop already discharges intersection checks. Only strict
+-- counter-clockwise convexity remains to admit the stronger carrier.
+convexPolygonFromLoop :: ExactLoop -> Maybe ConvexPolygon
+convexPolygonFromLoop loop =
+  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
+    Nothing -> Just (ConvexPolygon loop)
+    Just _ -> Nothing
+
+convexHullPolygon :: NonEmpty ExactPoint -> Either ConvexError ConvexPolygon
+convexHullPolygon submitted =
+  let points = NonEmpty.toList submitted
+   in case convexHullPoints points of
+        Nothing -> Left (ConvexHullDegenerate points)
+        Just hullPoints -> Right (admittedConvexPolygon hullPoints)
+
+reflectConvexPolygon :: ConvexPolygon -> ConvexPolygon
+reflectConvexPolygon =
+  admittedConvexPolygon . fmap negateExactPoint . convexPolygonPoints
+
+-- | The arithmetic mean of the vertices, a strict interior witness for every
+-- admitted polygon. This is not an area-density centroid.
+convexPolygonCentroid :: ConvexPolygon -> Either ConvexError ExactPoint
+convexPolygonCentroid polygon = do
+  let points = convexPolygonPoints polygon
+      count = fromIntegral (NonEmpty.length points)
+      (sumX, sumY) =
+        List.foldl'
+          (\(accumulatedX, accumulatedY) point ->
+             let (x, y) = exactPointCoordinates point
+              in (accumulatedX + x, accumulatedY + y))
+          (0, 0)
+          points
+  x <- first ConvexExactArithmetic (exactDivide sumX count)
+  y <- first ConvexExactArithmetic (exactDivide sumY count)
+  pure (exactPoint x y)
+
+convexHullPoints :: [ExactPoint] -> Maybe (NonEmpty ExactPoint)
+convexHullPoints submitted =
+  case Set.toAscList (Set.fromList submitted) of
+    firstPoint : secondPoint : thirdPoint : remaining ->
+      let ordered = firstPoint : secondPoint : thirdPoint : remaining
+          lower = dropFinal (reverse (List.foldl' hullStep [] ordered))
+          upper = dropFinal (reverse (List.foldl' hullStep [] (reverse ordered)))
+       in case lower <> upper of
+            firstHullPoint : secondHullPoint : thirdHullPoint : hullTail ->
+              Just (firstHullPoint :| (secondHullPoint : thirdHullPoint : hullTail))
+            _ -> Nothing
+    _ -> Nothing
+
+hullStep :: [ExactPoint] -> ExactPoint -> [ExactPoint]
+hullStep (current : previous : remaining) candidate
+  | exactOrient2d previous current candidate /= GT =
+      hullStep (previous : remaining) candidate
+hullStep hull candidate = candidate : hull
+
+dropFinal :: [value] -> [value]
+dropFinal values =
+  case reverse values of
+    _ : remaining -> reverse remaining
+    [] -> []
+
+negateExactPoint :: ExactPoint -> ExactPoint
+negateExactPoint point =
+  let (x, y) = exactPointCoordinates point
+   in exactPoint (negate x) (negate y)
+
+-- | One exact source edge restricted to a vertical slab. Its midpoint height
+-- orders crossings; the two endpoint values retain the actual affine edge,
+-- rather than a rounded segment reconstructed from a sample.
+data SlabCrossing = SlabCrossing !ExactRational !ExactPoint !ExactPoint
+
+-- | Partition an admitted polygonal region, including holes and disconnected
+-- components, at its distinct vertex abscissae. No boundary vertex lies in an
+-- open slab, so its crossing order is fixed and even-odd adjacent pairs bound
+-- exact convex trapezoids (or triangles at a collapsed endpoint). Slab closure
+-- overlaps have area zero; their union is exactly the source region. Immutable
+-- endpoint event groups restrict each slab to its active edges; dormant source
+-- boundaries are not rescanned or interpolated at unrelated abscissae.
+decomposeRegionIntoConvexSlabs
+  :: PlanarRegion
+  -> Either ConvexError [ConvexPolygon]
+decomposeRegionIntoConvexSlabs region =
+  concat <$> sequenceA
+    (snd (List.mapAccumL descendActiveSlab IntMap.empty (consecutivePairs abscissae)))
+ where
+  boundaries = regionBoundaryEdges region
+  abscissae = Set.toAscList (Set.fromList (fmap (fst . exactPointCoordinates . fst) boundaries))
+  half = exactRationalFromDyadic 1 (-1)
+  indexedNonvertical =
+    [ (index, min fromX toX, max fromX toX, edge)
+    | (index, edge@(from, to)) <- zip [0 ..] boundaries
+    , let fromX = fst (exactPointCoordinates from)
+          toX = fst (exactPointCoordinates to)
+    , fromX /= toX
+    ]
+  starts = Map.fromListWith IntMap.union
+    [(fromX, IntMap.singleton index edge) | (index, fromX, _, edge) <- indexedNonvertical]
+  ends = Map.fromListWith IntSet.union
+    [(toX, IntSet.singleton index) | (index, _, toX, _) <- indexedNonvertical]
+
+  descendActiveSlab
+    :: IntMap.IntMap (ExactPoint, ExactPoint)
+    -> (ExactRational, ExactRational)
+    -> (IntMap.IntMap (ExactPoint, ExactPoint), Either ConvexError [ConvexPolygon])
+  descendActiveSlab active bounds@(leftX, _) =
+    let entering = Map.findWithDefault IntMap.empty leftX starts
+        leaving = Map.findWithDefault IntSet.empty leftX ends
+        activeSection = IntMap.union entering (IntMap.withoutKeys active leaving)
+     in (activeSection, decomposeSlab activeSection bounds)
+
+  decomposeSlab
+    :: IntMap.IntMap (ExactPoint, ExactPoint)
+    -> (ExactRational, ExactRational)
+    -> Either ConvexError [ConvexPolygon]
+  decomposeSlab active (leftX, rightX) = do
+    let middleX = half * (leftX + rightX)
+    crossings <- traverse (restrictEdge leftX rightX middleX) (IntMap.elems active)
+    let ordered = List.sortOn crossingHeight crossings
+        count = length ordered
+        pairs = [pair | (index, pair) <- zip [0 :: Int ..] (consecutivePairs ordered), even index]
+    if odd count
+      then Left (ConvexSlabOddCrossings leftX rightX count)
+      else catMaybes <$> traverse crossingPolygon pairs
+
+  restrictEdge
+    :: ExactRational
+    -> ExactRational
+    -> ExactRational
+    -> (ExactPoint, ExactPoint)
+    -> Either ConvexError SlabCrossing
+  restrictEdge leftX rightX middleX (from, to) = do
+    let (fromX, fromY) = exactPointCoordinates from
+        (toX, toY) = exactPointCoordinates to
+    slope <- first ConvexExactArithmetic (exactDivide (toY - fromY) (toX - fromX))
+    let heightAt x = fromY + slope * (x - fromX)
+    pure (SlabCrossing (heightAt middleX) (exactPoint leftX (heightAt leftX)) (exactPoint rightX (heightAt rightX)))
+
+  crossingHeight :: SlabCrossing -> ExactRational
+  crossingHeight (SlabCrossing height _ _) = height
+
+  crossingPolygon :: (SlabCrossing, SlabCrossing) -> Either ConvexError (Maybe ConvexPolygon)
+  crossingPolygon (SlabCrossing lowerHeight lowerLeft lowerRight, SlabCrossing upperHeight upperLeft upperRight)
+    | lowerHeight == upperHeight = Right Nothing
+    | otherwise = Just <$>
+        (convexHullPolygon (lowerLeft :| [lowerRight, upperRight, upperLeft]))
+
+
+-- | Counter-clockwise boundary of an admitted convex polygon.
+convexPolygonBoundary :: ConvexPolygon -> [(ExactPoint, ExactPoint)]
+convexPolygonBoundary = cyclePairs . convexPolygonPoints
diff --git a/src-dcel/Moonlight/Planar/Curve.hs b/src-dcel/Moonlight/Planar/Curve.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Curve.hs
@@ -0,0 +1,434 @@
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact, translation-independent curve authoring. A step owns its endpoint
+-- displacement; the next step inherits that endpoint. A location is the sole
+-- absolute anchor. Closed trails derive their final displacement, and retain
+-- a distinct closing shape rather than silently inserting a straight edge.
+module Moonlight.Planar.Curve
+  ( CurveShape
+  , CurveShapeView (..)
+  , shapeView
+  , line
+  , quadratic
+  , cubic
+  , rationalQuadratic
+  , CurveStep
+  , curveStep
+  , curveStepShape
+  , curveStepEnd
+  , stepControlPoints
+  , evaluateStep
+  , splitStepHalf
+  , reverseStep
+  , startJet
+  , endJet
+  , hermiteStep
+  , JoinContinuity (..)
+  , joinContinuity
+  , transformStep
+  , OpenTrail
+  , openTrail
+  , trailSteps
+  , trailDisplacement
+  , reverseTrail
+  , transformTrail
+  , ClosedTrail
+  , closeWith
+  , closedTrailPrefix
+  , closedTrailClosingShape
+  , closedTrailSteps
+  , reverseClosedTrail
+  , transformClosedTrail
+  , Located
+  , locate
+  , location
+  , locatedValue
+  , transformLocatedTrail
+  , transformLocatedClosedTrail
+  , reverseLocatedTrail
+  , Subpath (..)
+  , Path
+  , path
+  , pathSubpaths
+  , transformPath
+  , circle
+  , ellipse
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import qualified Data.Foldable as Foldable
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Sequence (Seq, ViewL (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine (Affine2, affine2, transformPoint, transformVector)
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactVector (..), PositiveExact, UnitInterval
+  , addExactVectors, blendPositive, divideByPositive, exactCross
+  , exactHalf, exactPoint, exactThird, positiveExactValue, positiveOne
+  , positiveTwo, ratioPositive, translateExactPoint, unitHalf, unitIntervalValue
+  )
+
+data CurveShape
+  = Linear
+  | Quadratic !ExactVector
+  | Cubic !ExactVector !ExactVector
+  | RationalQuadratic !ExactVector !PositiveExact !PositiveExact
+  deriving stock (Eq, Ord, Show)
+
+-- | Exhaustive observation, not an alternate stored shape. Controls are
+-- relative to the start; rational weights are normalized to @(1,a,b)@.
+data CurveShapeView
+  = LinearView
+  | QuadraticView !ExactVector
+  | CubicView !ExactVector !ExactVector
+  | RationalQuadraticView !ExactVector !PositiveExact !PositiveExact
+  deriving stock (Eq, Ord, Show)
+
+instance NFData CurveShape where
+  rnf Linear = ()
+  rnf (Quadratic a) = rnf a
+  rnf (Cubic a b) = rnf a `seq` rnf b
+  rnf (RationalQuadratic a u v) = rnf a `seq` rnf u `seq` rnf v
+
+shapeView :: CurveShape -> CurveShapeView
+shapeView Linear = LinearView
+shapeView (Quadratic a) = QuadraticView a
+shapeView (Cubic a b) = CubicView a b
+shapeView (RationalQuadratic a u v) = RationalQuadraticView a u v
+
+line :: CurveShape
+line = Linear
+
+quadratic :: ExactVector -> CurveShape
+quadratic = Quadratic
+
+cubic :: ExactVector -> ExactVector -> CurveShape
+cubic = Cubic
+
+-- | Polynomial quadratics have one representation. Other rational weights
+-- retain their parameterization, rather than normalizing both endpoint weights.
+rationalQuadratic :: ExactVector -> PositiveExact -> PositiveExact -> CurveShape
+rationalQuadratic control a b
+  | a == positiveOne && b == positiveOne = Quadratic control
+  | otherwise = RationalQuadratic control a b
+
+data CurveStep = CurveStep !CurveShape !ExactVector
+  deriving stock (Eq, Ord, Show)
+
+instance NFData CurveStep where
+  rnf (CurveStep shape endpoint) = rnf shape `seq` rnf endpoint
+
+curveStep :: CurveShape -> ExactVector -> CurveStep
+curveStep = CurveStep
+
+curveStepShape :: CurveStep -> CurveShape
+curveStepShape (CurveStep shape _) = shape
+
+curveStepEnd :: CurveStep -> ExactVector
+curveStepEnd (CurveStep _ endpoint) = endpoint
+
+-- | Euclidean control hull, including start and end. Positive rational
+-- Bernstein weights keep the curve inside this hull.
+stepControlPoints :: CurveStep -> NonEmpty ExactVector
+stepControlPoints (CurveStep shape endpoint) = zeroVector :| case shape of
+  Linear -> [endpoint]
+  Quadratic a -> [a, endpoint]
+  Cubic a b -> [a, b, endpoint]
+  RationalQuadratic a _ _ -> [a, endpoint]
+
+evaluateStep :: UnitInterval -> CurveStep -> ExactVector
+evaluateStep parameter (CurveStep shape endpoint) = case shape of
+  Linear -> scaleVector t endpoint
+  Quadratic a -> blend t (blend t zeroVector a) (blend t a endpoint)
+  Cubic a b ->
+    blend t
+      (blend t (blend t zeroVector a) (blend t a b))
+      (blend t (blend t a b) (blend t b endpoint))
+  RationalQuadratic a u v ->
+    let start = Homogeneous zeroVector positiveOne
+        middle = weighted a u
+        finish = weighted endpoint v
+     in project (blendHomogeneous parameter
+          (blendHomogeneous parameter start middle)
+          (blendHomogeneous parameter middle finish))
+ where
+  t = unitIntervalValue parameter
+
+-- | Exact half subdivision. The right child's controls are rebased at the
+-- shared split point; parameterization is inherited on each half interval.
+splitStepHalf :: CurveStep -> (CurveStep, CurveStep)
+splitStepHalf (CurveStep shape endpoint) = case shape of
+  Linear ->
+    let midpoint = scaleVector exactHalf endpoint
+     in (CurveStep Linear midpoint, CurveStep Linear midpoint)
+  Quadratic a ->
+    let p = midpointVector zeroVector a
+        q = midpointVector a endpoint
+        m = midpointVector p q
+     in (CurveStep (Quadratic p) m,
+         CurveStep (Quadratic (subtractVectors q m)) (subtractVectors endpoint m))
+  Cubic a b ->
+    let p = midpointVector zeroVector a
+        q = midpointVector a b
+        r = midpointVector b endpoint
+        u = midpointVector p q
+        v = midpointVector q r
+        m = midpointVector u v
+     in (CurveStep (Cubic p u) m,
+         CurveStep (Cubic (subtractVectors v m) (subtractVectors r m))
+           (subtractVectors endpoint m))
+  RationalQuadratic a u v ->
+    let start = Homogeneous zeroVector positiveOne
+        middle = weighted a u
+        finish = weighted endpoint v
+        p = blendHomogeneous unitHalf start middle
+        q = blendHomogeneous unitHalf middle finish
+        m = blendHomogeneous unitHalf p q
+        midpoint = project m
+        leftShape = rationalQuadratic (project p) (weight p) (weight m)
+        rightShape = rationalQuadratic (subtractVectors (project q) midpoint)
+          (ratioPositive (weight q) (weight m))
+          (ratioPositive v (weight m))
+     in (CurveStep leftShape midpoint,
+         CurveStep rightShape (subtractVectors endpoint midpoint))
+
+reverseStep :: CurveStep -> CurveStep
+reverseStep (CurveStep shape endpoint) = CurveStep reversed (negateVector endpoint)
+ where
+  reversed = case shape of
+    Linear -> Linear
+    Quadratic a -> Quadratic (subtractVectors a endpoint)
+    Cubic a b -> Cubic (subtractVectors b endpoint) (subtractVectors a endpoint)
+    RationalQuadratic a u v -> rationalQuadratic (subtractVectors a endpoint)
+      (ratioPositive u v) (ratioPositive positiveOne v)
+
+-- | Derivatives with respect to each segment's own unit parameter. Equality
+-- of these jets is C1 for equal-duration segment parameterizations, not arc length.
+startJet :: CurveStep -> ExactVector
+startJet (CurveStep shape endpoint) = case shape of
+  Linear -> endpoint
+  Quadratic a -> scaleVector 2 a
+  Cubic a _ -> scaleVector 3 a
+  RationalQuadratic a u _ -> scaleVector (2 * positiveExactValue u) a
+
+endJet :: CurveStep -> ExactVector
+endJet (CurveStep shape endpoint) = case shape of
+  Linear -> endpoint
+  Quadratic a -> scaleVector 2 (subtractVectors endpoint a)
+  Cubic _ b -> scaleVector 3 (subtractVectors endpoint b)
+  RationalQuadratic a u v ->
+    scaleVector (2 * divideByPositive (positiveExactValue u) v)
+      (subtractVectors endpoint a)
+
+hermiteStep :: ExactVector -> ExactVector -> ExactVector -> CurveStep
+hermiteStep endpoint initial final =
+  CurveStep (Cubic (scaleVector exactThird initial)
+    (subtractVectors endpoint (scaleVector exactThird final))) endpoint
+
+data JoinContinuity
+  = StationaryJoin
+  | CornerJoin
+  | GeometricJoin
+  | ParametricJoin
+  deriving stock (Eq, Ord, Show)
+
+joinContinuity :: CurveStep -> CurveStep -> JoinContinuity
+joinContinuity before after
+  | u == zeroVector || v == zeroVector = StationaryJoin
+  | u == v = ParametricJoin
+  | exactCross u v == 0 && dotVector u v > 0 = GeometricJoin
+  | otherwise = CornerJoin
+ where
+  u = endJet before
+  v = startJet after
+
+-- | The affine map's linear part acts on relative controls and displacement.
+-- Translation belongs exclusively to the corresponding located action.
+transformStep :: Affine2 -> CurveStep -> CurveStep
+transformStep placement (CurveStep shape endpoint) =
+  CurveStep (transformShape placement shape) (transformVector placement endpoint)
+
+transformShape :: Affine2 -> CurveShape -> CurveShape
+transformShape placement shape = case shape of
+  Linear -> Linear
+  Quadratic a -> Quadratic (transformVector placement a)
+  Cubic a b -> Cubic (transformVector placement a) (transformVector placement b)
+  RationalQuadratic a u v -> RationalQuadratic (transformVector placement a) u v
+
+newtype OpenTrail = OpenTrail (Seq CurveStep)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData OpenTrail where
+  rnf (OpenTrail steps) = rnf steps
+
+instance Semigroup OpenTrail where
+  OpenTrail a <> OpenTrail b = OpenTrail (a <> b)
+
+instance Monoid OpenTrail where
+  mempty = OpenTrail Seq.empty
+
+openTrail :: Seq CurveStep -> OpenTrail
+openTrail = OpenTrail
+
+trailSteps :: OpenTrail -> Seq CurveStep
+trailSteps (OpenTrail steps) = steps
+
+trailDisplacement :: OpenTrail -> ExactVector
+trailDisplacement = Foldable.foldl' (\offset step -> addExactVectors offset (curveStepEnd step)) zeroVector . trailSteps
+
+reverseTrail :: OpenTrail -> OpenTrail
+reverseTrail = OpenTrail . fmap reverseStep . Seq.reverse . trailSteps
+
+transformTrail :: Affine2 -> OpenTrail -> OpenTrail
+transformTrail placement = OpenTrail . fmap (transformStep placement) . trailSteps
+
+data ClosedTrail = ClosedTrail !OpenTrail !CurveShape
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ClosedTrail where
+  rnf (ClosedTrail prefix closing) = rnf prefix `seq` rnf closing
+
+closeWith :: CurveShape -> OpenTrail -> ClosedTrail
+closeWith closing prefix = ClosedTrail prefix closing
+
+closedTrailPrefix :: ClosedTrail -> OpenTrail
+closedTrailPrefix (ClosedTrail prefix _) = prefix
+
+closedTrailClosingShape :: ClosedTrail -> CurveShape
+closedTrailClosingShape (ClosedTrail _ closing) = closing
+
+closedTrailSteps :: ClosedTrail -> Seq CurveStep
+closedTrailSteps (ClosedTrail prefix closing) =
+  trailSteps prefix Seq.|> CurveStep closing (negateVector (trailDisplacement prefix))
+
+reverseClosedTrail :: ClosedTrail -> ClosedTrail
+reverseClosedTrail (ClosedTrail prefix closing) =
+  let reversedClosing = reverseStep (CurveStep closing (negateVector (trailDisplacement prefix)))
+   in case Seq.viewl (trailSteps prefix) of
+        EmptyL -> ClosedTrail mempty (curveStepShape reversedClosing)
+        first :< rest -> ClosedTrail
+          (OpenTrail (Seq.singleton reversedClosing <> fmap reverseStep (Seq.reverse rest)))
+          (curveStepShape (reverseStep first))
+
+transformClosedTrail :: Affine2 -> ClosedTrail -> ClosedTrail
+transformClosedTrail placement (ClosedTrail prefix closing) =
+  ClosedTrail (transformTrail placement prefix) (transformShape placement closing)
+
+data Located a = Located !ExactPoint !a
+  deriving stock (Eq, Ord, Show)
+
+instance NFData a => NFData (Located a) where
+  rnf (Located anchor value) = rnf anchor `seq` rnf value
+
+locate :: ExactPoint -> a -> Located a
+locate = Located
+
+location :: Located a -> ExactPoint
+location (Located anchor _) = anchor
+
+locatedValue :: Located a -> a
+locatedValue (Located _ value) = value
+
+-- | One affine value owns both the point action and the relative vector action.
+transformLocatedTrail :: Affine2 -> Located OpenTrail -> Located OpenTrail
+transformLocatedTrail placement (Located anchor trail) =
+  Located (transformPoint placement anchor) (transformTrail placement trail)
+
+transformLocatedClosedTrail :: Affine2 -> Located ClosedTrail -> Located ClosedTrail
+transformLocatedClosedTrail placement (Located anchor trail) =
+  Located (transformPoint placement anchor) (transformClosedTrail placement trail)
+
+reverseLocatedTrail :: Located OpenTrail -> Located OpenTrail
+reverseLocatedTrail (Located anchor trail) =
+  Located (translateExactPoint anchor (trailDisplacement trail)) (reverseTrail trail)
+
+data Subpath
+  = OpenSubpath !(Located OpenTrail)
+  | ClosedSubpath !(Located ClosedTrail)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData Subpath where
+  rnf (OpenSubpath value) = rnf value
+  rnf (ClosedSubpath value) = rnf value
+
+newtype Path = Path (Seq Subpath)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData Path where
+  rnf (Path subpaths) = rnf subpaths
+
+instance Semigroup Path where
+  Path a <> Path b = Path (a <> b)
+
+instance Monoid Path where
+  mempty = Path Seq.empty
+
+path :: Seq Subpath -> Path
+path = Path
+
+pathSubpaths :: Path -> Seq Subpath
+pathSubpaths (Path subpaths) = subpaths
+
+transformPath :: Affine2 -> Path -> Path
+transformPath placement = Path . fmap transform . pathSubpaths
+ where
+  transform (OpenSubpath value) = OpenSubpath (transformLocatedTrail placement value)
+  transform (ClosedSubpath value) = ClosedSubpath (transformLocatedClosedTrail placement value)
+
+-- | An exact circle centered at the origin, starting on the positive x axis.
+-- The four rational quarters are G1, not uniformly parameterized by angle.
+circle :: PositiveExact -> Located ClosedTrail
+circle radius = ellipse (ExactVector r 0) (ExactVector 0 r)
+ where
+  r = positiveExactValue radius
+
+-- | The rational affine image of the unit circle, centered at the origin.
+-- The axes may be skew or singular: authored curves admit collapsed geometry.
+ellipse :: ExactVector -> ExactVector -> Located ClosedTrail
+ellipse axisX axisY =
+  transformLocatedClosedTrail (affine2 axisX axisY zeroVector)
+    (Located (exactPoint 1 0) unitCircle)
+ where
+  conic control = rationalQuadratic control positiveOne positiveTwo
+  unitCircle = closeWith (conic (ExactVector 1 0)) (openTrail (Seq.fromList
+    [ CurveStep (conic (ExactVector 0 1)) (ExactVector (-1) 1)
+    , CurveStep (conic (ExactVector (-1) 0)) (ExactVector (-1) (-1))
+    , CurveStep (conic (ExactVector 0 (-1))) (ExactVector 1 (-1))
+    ]))
+
+data Homogeneous = Homogeneous !ExactVector !PositiveExact
+
+weighted :: ExactVector -> PositiveExact -> Homogeneous
+weighted value w = Homogeneous (scaleVector (positiveExactValue w) value) w
+
+weight :: Homogeneous -> PositiveExact
+weight (Homogeneous _ w) = w
+
+project :: Homogeneous -> ExactVector
+project (Homogeneous (ExactVector x y) w) =
+  ExactVector (divideByPositive x w) (divideByPositive y w)
+
+blendHomogeneous :: UnitInterval -> Homogeneous -> Homogeneous -> Homogeneous
+blendHomogeneous t (Homogeneous a u) (Homogeneous b v) =
+  Homogeneous (blend (unitIntervalValue t) a b) (blendPositive t u v)
+
+zeroVector :: ExactVector
+zeroVector = ExactVector 0 0
+
+scaleVector :: ExactRational -> ExactVector -> ExactVector
+scaleVector scale (ExactVector x y) = ExactVector (scale * x) (scale * y)
+
+negateVector :: ExactVector -> ExactVector
+negateVector = scaleVector (-1)
+
+subtractVectors :: ExactVector -> ExactVector -> ExactVector
+subtractVectors a b = addExactVectors a (negateVector b)
+
+blend :: ExactRational -> ExactVector -> ExactVector -> ExactVector
+blend t a b = addExactVectors (scaleVector (1 - t) a) (scaleVector t b)
+
+midpointVector :: ExactVector -> ExactVector -> ExactVector
+midpointVector = blend exactHalf
+
+dotVector :: ExactVector -> ExactVector -> ExactRational
+dotVector (ExactVector ax ay) (ExactVector bx by) = ax * bx + ay * by
diff --git a/src-dcel/Moonlight/Planar/Curve/Authoring.hs b/src-dcel/Moonlight/Planar/Curve/Authoring.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Curve/Authoring.hs
@@ -0,0 +1,158 @@
+-- | Exact interpolation and transverse profiles for semantic authoring.
+-- These constructors immediately produce the canonical curve algebra; they
+-- retain neither an evaluator nor a second spline representation. Uniform
+-- knot spacing is a parameter convention, not arc length. Interpolation does
+-- not guarantee simplicity, absence of overshoot, or preservation of topology.
+module Moonlight.Planar.Curve.Authoring
+  ( Knot (..)
+  , cardinalOpen
+  , cardinalClosed
+  , ProfileStation (..)
+  , profileRails
+  , profileOutline
+  , polygonTrail
+  , bowedTrail
+  ) where
+
+import qualified Data.Foldable as Foldable
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Curve
+  ( ClosedTrail, CurveStep, Located, OpenTrail, closeWith, curveStep
+  , curveStepShape, hermiteStep, line, locate, locatedValue, location
+  , openTrail, quadratic, reverseTrail, trailDisplacement )
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactVector (..), UnitInterval, addExactVectors
+  , exactHalf, exactVectorFromPoints, translateExactPoint, unitIntervalValue )
+import Moonlight.Planar.Internal.BoundaryCycle (consecutivePairs, cyclicTriples)
+
+-- | An interpolated landmark or a landmark with independently prescribed
+-- incoming and outgoing derivatives. Explicit derivatives ignore tension;
+-- distinct directions make intentional corners without a global refit.
+data Knot
+  = Interpolating !ExactPoint
+  | ExplicitJets !ExactPoint !ExactVector !ExactVector
+  deriving stock (Eq, Show)
+
+-- | Uniform cardinal interpolation. At tension zero, interior derivatives
+-- are half the neighbor secant (Catmull-Rom). Endpoint derivatives use the
+-- one-sided secant; tension one zeros only the automatically derived jets.
+-- A singleton is a located empty trail; repeated points remain lawful input.
+cardinalOpen :: UnitInterval -> NonEmpty Knot -> Located OpenTrail
+cardinalOpen tension knots@(initial :| remaining) = assembleOpen (firstJet :| otherJets)
+ where
+  scale = 1 - unitIntervalValue tension
+  firstJet = resolve (case remaining of
+    [] -> zero
+    next : _ -> scaleVector scale (between initial next)) initial
+  otherJets = fmap derive
+    (zip3 (NonEmpty.toList knots) remaining (fmap Just (drop 1 remaining) <> [Nothing]))
+  derive :: (Knot, Knot, Maybe Knot) -> KnotJet
+  derive (previous, current, next) = resolve automatic current
+   where
+    automatic = case next of
+      Nothing -> scaleVector scale (between previous current)
+      Just following -> scaleVector (scale * exactHalf) (between previous following)
+
+-- | Periodic uniform cardinal interpolation, including the closing segment.
+-- A singleton is a constant closed curve unless explicit nonzero jets specify
+-- a returning cubic. No minimum polygon vertex count is invented here.
+cardinalClosed :: UnitInterval -> NonEmpty Knot -> Located ClosedTrail
+cardinalClosed tension knots@(initial :| _) =
+  case fmap derive (cyclicTriples (NonEmpty.toList knots)) of
+    [] -> assembleClosed (resolve zero initial :| [])
+    firstJet : remaining -> assembleClosed (firstJet :| remaining)
+ where
+  scale = (1 - unitIntervalValue tension) * exactHalf
+  derive :: (Knot, Knot, Knot) -> KnotJet
+  derive (previous, current, next) = resolve (scaleVector scale (between previous next)) current
+
+-- | A center landmark and a signed transverse half-span, not a Euclidean
+-- unit normal. Width, taper, skew, and asymmetry remain rational and affine
+-- equivariant. A zero half-span joins the two rails at that station.
+data ProfileStation = ProfileStation !ExactPoint !ExactVector
+  deriving stock (Eq, Show)
+
+-- | Interpolate center + half-span and center - half-span with one tension.
+-- The cardinal operator is affine-linear in the landmarks, so this is exactly
+-- the interpolated centerline plus/minus the interpolated transverse profile.
+profileRails
+  :: UnitInterval -> NonEmpty ProfileStation -> (Located OpenTrail, Located OpenTrail)
+profileRails tension stations =
+  (cardinalOpen tension (fmap (Interpolating . positiveSide) stations),
+   cardinalOpen tension (fmap (Interpolating . negativeSide) stations))
+
+-- | Traverse the positive rail, connect the terminal stations, return along
+-- the negative rail, and close at the root. End connectors are straight.
+-- This is a transverse ribbon, not a constant-distance offset or a guarantee
+-- that the outline is a simple region.
+profileOutline :: UnitInterval -> NonEmpty ProfileStation -> Located ClosedTrail
+profileOutline tension stations = locate (location positiveRail) (closeWith line prefix)
+ where
+  (positiveRail, negativeRail) = profileRails tension stations
+  positiveTrail = locatedValue positiveRail
+  negativeTrail = locatedValue negativeRail
+  positiveEnd = translateExactPoint (location positiveRail) (trailDisplacement positiveTrail)
+  negativeEnd = translateExactPoint (location negativeRail) (trailDisplacement negativeTrail)
+  connector = openTrail (Seq.singleton (curveStep line (exactVectorFromPoints positiveEnd negativeEnd)))
+  prefix = positiveTrail <> connector <> reverseTrail negativeTrail
+
+-- | Straight-edged authored contour. Unlike Region.exactLoop, this preserves
+-- source order and makes no simplicity or dimensionality claim.
+polygonTrail :: NonEmpty ExactPoint -> Located ClosedTrail
+polygonTrail points@(initial :| _) = locate initial (closeWith line (openTrail (Seq.fromList
+  (fmap (\(a,b) -> curveStep line (exactVectorFromPoints a b))
+    (consecutivePairs (NonEmpty.toList points))))))
+
+-- | A quadratic bow whose midpoint displacement from the chord midpoint is
+-- @bow * perpendicular(chord)@. Bow is dimensionless and signed. This is not
+-- a circular arc and performs no irrational unit-normal normalization.
+bowedTrail :: ExactRational -> ExactPoint -> ExactPoint -> Located OpenTrail
+bowedTrail bow start end = locate start (openTrail (Seq.singleton
+  (curveStep (quadratic control) chord)))
+ where
+  chord@(ExactVector x y) = exactVectorFromPoints start end
+  control = addExactVectors (scaleVector exactHalf chord)
+    (scaleVector (2 * bow) (ExactVector (negate y) x))
+
+data KnotJet = KnotJet !ExactPoint !ExactVector !ExactVector
+
+resolve :: ExactVector -> Knot -> KnotJet
+resolve automatic knot = case knot of
+  Interpolating point -> KnotJet point automatic automatic
+  ExplicitJets point incoming outgoing -> KnotJet point incoming outgoing
+
+knotPoint :: Knot -> ExactPoint
+knotPoint (Interpolating point) = point
+knotPoint (ExplicitJets point _ _) = point
+
+between :: Knot -> Knot -> ExactVector
+between a b = exactVectorFromPoints (knotPoint a) (knotPoint b)
+
+jetSpan :: KnotJet -> KnotJet -> CurveStep
+jetSpan (KnotJet a _ outgoing) (KnotJet b incoming _) =
+  hermiteStep (exactVectorFromPoints a b) outgoing incoming
+
+assembleOpen :: NonEmpty KnotJet -> Located OpenTrail
+assembleOpen jets@(KnotJet anchor _ _ :| _) = locate anchor (openTrail (Seq.fromList
+  (fmap (uncurry jetSpan) (consecutivePairs (NonEmpty.toList jets)))))
+
+assembleClosed :: NonEmpty KnotJet -> Located ClosedTrail
+assembleClosed jets@(initial :| remaining) =
+  let authored = assembleOpen jets
+      final = Foldable.foldl' (\_ current -> current) initial remaining
+   in locate (location authored)
+        (closeWith (curveStepShape (jetSpan final initial)) (locatedValue authored))
+
+positiveSide :: ProfileStation -> ExactPoint
+positiveSide (ProfileStation center halfSpan) = translateExactPoint center halfSpan
+
+negativeSide :: ProfileStation -> ExactPoint
+negativeSide (ProfileStation center halfSpan) = translateExactPoint center (scaleVector (-1) halfSpan)
+
+scaleVector :: ExactRational -> ExactVector -> ExactVector
+scaleVector factor (ExactVector x y) = ExactVector (factor*x) (factor*y)
+
+zero :: ExactVector
+zero = ExactVector 0 0
diff --git a/src-dcel/Moonlight/Planar/Curve/Lowering.hs b/src-dcel/Moonlight/Planar/Curve/Lowering.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Curve/Lowering.hs
@@ -0,0 +1,186 @@
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Bounded exact polygonal approximation. The certificate bounds Hausdorff
+-- distance in the declared affine metric, not topology or tangent error.
+module Moonlight.Planar.Curve.Lowering
+  ( LoweringPolicy
+  , loweringPolicy
+  , LoweringError (..)
+  , LoweredPath
+  , LoweredSpan
+  , loweredPoints
+  , loweredSpans
+  , loweredMetric
+  , loweredTolerance
+  , spanSourceStep
+  , spanParameterFrom
+  , spanParameterTo
+  , spanFrom
+  , spanTo
+  , spanSquaredBound
+  , lowerStep
+  , lowerOpenTrail
+  , lowerClosedTrail
+  , reverseLoweredPath
+  ) where
+
+import Control.Monad (foldM)
+import Data.Foldable (toList)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Data.Sequence (Seq)
+import Moonlight.Planar.Affine (Affine2, transformPoint)
+import Moonlight.Planar.Curve
+  ( CurveStep, OpenTrail, ClosedTrail, Located, location, locatedValue
+  , trailSteps, closedTrailSteps, curveStepEnd, stepControlPoints, splitStepHalf )
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactVector (..), ExactRational
+  , PositiveExact, positiveExactValue, exactHalf, divideByPositive, positiveSumSquares
+  , exactVectorFromPoints, translateExactPoint )
+
+data LoweringPolicy = LoweringPolicy !PositiveExact !Affine2 !Int !Int
+
+-- | Samples remain in source coordinates. The affine map is used only to
+-- measure error, allowing a renderer to retain its original transform scope.
+loweringPolicy
+  :: PositiveExact -> Affine2 -> Int -> Int
+  -> Either LoweringError LoweringPolicy
+loweringPolicy tolerance metric depth leaves
+  | depth < 0 = Left (InvalidSubdivisionDepth depth)
+  | leaves <= 0 = Left (InvalidLeafBudget leaves)
+  | otherwise = Right (LoweringPolicy tolerance metric depth leaves)
+
+data LoweringError
+  = InvalidSubdivisionDepth !Int
+  | InvalidLeafBudget !Int
+  | SubdivisionDepthExhausted !Int !ExactRational !ExactRational !ExactRational
+  | LeafBudgetExhausted !Int !ExactRational !ExactRational
+  deriving stock (Eq, Show)
+
+-- | Parameter endpoints are in the original source step. Reversal exchanges
+-- them rather than fabricating a new sampling of the reversed curve.
+data LoweredSpan = LoweredSpan
+  !Int !ExactRational !ExactRational !ExactPoint !ExactPoint !ExactRational
+  deriving stock (Eq, Show)
+
+spanSourceStep :: LoweredSpan -> Int
+spanSourceStep (LoweredSpan index _ _ _ _ _) = index
+
+spanParameterFrom :: LoweredSpan -> ExactRational
+spanParameterFrom (LoweredSpan _ from _ _ _ _) = from
+
+spanParameterTo :: LoweredSpan -> ExactRational
+spanParameterTo (LoweredSpan _ _ to _ _ _) = to
+
+spanFrom :: LoweredSpan -> ExactPoint
+spanFrom (LoweredSpan _ _ _ from _ _) = from
+
+spanTo :: LoweredSpan -> ExactPoint
+spanTo (LoweredSpan _ _ _ _ to _) = to
+
+spanSquaredBound :: LoweredSpan -> ExactRational
+spanSquaredBound (LoweredSpan _ _ _ _ _ bound) = bound
+
+data LoweredPath = LoweredPath !PositiveExact !Affine2 !ExactPoint !(Seq LoweredSpan)
+  deriving stock (Eq, Show)
+
+loweredPoints :: LoweredPath -> NonEmpty ExactPoint
+loweredPoints (LoweredPath _ _ origin spans) = origin :| map spanTo (toList spans)
+
+loweredSpans :: LoweredPath -> [LoweredSpan]
+loweredSpans (LoweredPath _ _ _ spans) = toList spans
+
+loweredMetric :: LoweredPath -> Affine2
+loweredMetric (LoweredPath _ metric _ _) = metric
+
+loweredTolerance :: LoweredPath -> PositiveExact
+loweredTolerance (LoweredPath tolerance _ _ _) = tolerance
+
+reverseLoweredPath :: LoweredPath -> LoweredPath
+reverseLoweredPath (LoweredPath tolerance metric origin spans) =
+  LoweredPath tolerance metric endpoint (fmap reverseSpan (Seq.reverse spans))
+ where
+  endpoint = case Seq.viewr spans of
+    Seq.EmptyR -> origin
+    _ Seq.:> finalSpan -> spanTo finalSpan
+  reverseSpan (LoweredSpan index t0 t1 from to bound) =
+    LoweredSpan index t1 t0 to from bound
+
+lowerStep :: LoweringPolicy -> Located CurveStep -> Either LoweringError LoweredPath
+lowerStep policy value = lowerSteps policy (location value) (Seq.singleton (locatedValue value))
+
+lowerOpenTrail :: LoweringPolicy -> Located OpenTrail -> Either LoweringError LoweredPath
+lowerOpenTrail policy value = lowerSteps policy (location value) (trailSteps (locatedValue value))
+
+lowerClosedTrail :: LoweringPolicy -> Located ClosedTrail -> Either LoweringError LoweredPath
+lowerClosedTrail policy value = lowerSteps policy (location value) (closedTrailSteps (locatedValue value))
+
+data LoweredPrefix = LoweredPrefix !ExactPoint !Int !(Seq LoweredSpan)
+
+lowerSteps :: LoweringPolicy -> ExactPoint -> Seq CurveStep -> Either LoweringError LoweredPath
+lowerSteps policy@(LoweringPolicy tolerance metric depth leaves) origin steps = do
+  LoweredPrefix _ _ spans <-
+    foldM appendStep (LoweredPrefix origin leaves Seq.empty) (Seq.mapWithIndex (,) steps)
+  pure (LoweredPath tolerance metric origin spans)
+ where
+  appendStep (LoweredPrefix from remaining prefix) (index, step) = do
+    (remainingAfter, spans) <- descend policy index depth remaining from 0 1 step
+    pure (LoweredPrefix (translateExactPoint from (curveStepEnd step)) remainingAfter (prefix <> spans))
+
+-- The binary subdivision tree is consumed directly; its depth and accepted
+-- leaf budget bound traversal before any candidate exponential tree exists.
+descend
+  :: LoweringPolicy -> Int -> Int -> Int -> ExactPoint
+  -> ExactRational -> ExactRational -> CurveStep
+  -> Either LoweringError (Int, Seq LoweredSpan)
+descend policy@(LoweringPolicy tolerance metric _ _) index depth remaining from t0 t1 step
+  | remaining <= 0 = Left (LeafBudgetExhausted index t0 t1)
+  | otherwise = do
+      let bound = chordBound metric from step
+      if bound <= epsilon * epsilon
+        then Right
+          ( remaining - 1
+          , Seq.singleton (LoweredSpan index t0 t1 from endpoint bound)
+          )
+        else if depth == 0
+          then Left (SubdivisionDepthExhausted index t0 t1 bound)
+          else do
+            let (left, right) = splitStepHalf step
+                middle = (t0 + t1) * exactHalf
+                splitPoint = translateExactPoint from (curveStepEnd left)
+            (afterLeft, leftSpans) <- descend policy index (depth - 1) remaining from t0 middle left
+            (afterRight, rightSpans) <- descend policy index (depth - 1) afterLeft splitPoint middle t1 right
+            pure (afterRight, leftSpans <> rightSpans)
+ where
+  epsilon = positiveExactValue tolerance
+  endpoint = translateExactPoint from (curveStepEnd step)
+
+-- Positive rational Bernstein weights retain the convex-hull property.
+-- A finite chord's epsilon capsule is convex; continuous endpoint-connected
+-- projection also bounds chord-to-curve distance. A zero chord is a point,
+-- not evidence that its source curve is constant.
+chordBound :: Affine2 -> ExactPoint -> CurveStep -> ExactRational
+chordBound metric from step =
+  foldl' (\bound -> max bound . squaredChordDistance start end . project) 0 (stepControlPoints step)
+ where
+  project = transformPoint metric . translateExactPoint from
+  start = transformPoint metric from
+  end = project (curveStepEnd step)
+
+squaredChordDistance :: ExactPoint -> ExactPoint -> ExactPoint -> ExactRational
+squaredChordDistance start end point =
+  case positiveSumSquares dx dy of
+    Nothing -> dot relative relative
+    Just chordSquared
+      | projection <= 0 -> dot relative relative
+      | projection >= positiveExactValue chordSquared -> dot beyond beyond
+      | otherwise -> divideByPositive (cross * cross) chordSquared
+ where
+  chord@(ExactVector dx dy) = exactVectorFromPoints start end
+  relative@(ExactVector px py) = exactVectorFromPoints start point
+  beyond = exactVectorFromPoints end point
+  projection = dot relative chord
+  cross = dx * py - dy * px
+
+dot :: ExactVector -> ExactVector -> ExactRational
+dot (ExactVector ax ay) (ExactVector bx by) = ax * bx + ay * by
diff --git a/src-dcel/Moonlight/Planar/Curve/Region.hs b/src-dcel/Moonlight/Planar/Curve/Region.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Curve/Region.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Explicit simple outer/hole assembly of approximated curves. Admission
+-- proves polygon topology only, never topology equivalence with the curves.
+module Moonlight.Planar.Curve.Region
+  ( CurveComponent (..)
+  , CurveRegionError (..)
+  , lowerSimpleRegion
+  ) where
+
+import Data.Bifunctor (first)
+import Moonlight.Planar.Curve (ClosedTrail, Located)
+import Moonlight.Planar.Curve.Lowering
+  ( LoweringPolicy, LoweringError, LoweredPath, lowerClosedTrail, loweredPoints )
+import Moonlight.Planar.Region
+  ( PlanarRegion, RegionValidationError, exactLoop, polygonComponent, planarRegion )
+
+-- | The submitted winding is significant: outer CCW, holes CW. An explicitly
+-- located closed trail does not by itself claim simplicity or containment.
+data CurveComponent = CurveComponent
+  { curveOuter :: !(Located ClosedTrail)
+  , curveHoles :: ![Located ClosedTrail]
+  }
+
+data CurveRegionError
+  = CurveLoweringRefused !LoweringError
+  | CurvePolygonRefused !RegionValidationError
+  deriving stock (Eq, Show)
+
+-- | The lowering policy's leaf budget applies per contour. Receipts remain in
+-- submitted component/outer/hole order, independently of polygon canonicalization.
+-- Coordinates remain source-local; the policy's affine map only measures error.
+lowerSimpleRegion
+  :: LoweringPolicy
+  -> [CurveComponent]
+  -> Either CurveRegionError (PlanarRegion, [LoweredPath])
+lowerSimpleRegion policy components = do
+  admitted <- traverse lowerComponent components
+  region <- first CurvePolygonRefused (planarRegion (map fst admitted))
+  pure (region, concatMap snd admitted)
+ where
+  lowerComponent (CurveComponent outer holes) = do
+    outerPath <- first CurveLoweringRefused (lowerClosedTrail policy outer)
+    holePaths <- traverse (first CurveLoweringRefused . lowerClosedTrail policy) holes
+    outerLoop <- first CurvePolygonRefused (exactLoop (loweredPoints outerPath))
+    holeLoops <- traverse (first CurvePolygonRefused . exactLoop . loweredPoints) holePaths
+    component <- first CurvePolygonRefused (polygonComponent outerLoop holeLoops)
+    pure (component, outerPath : holePaths)
diff --git a/src-dcel/Moonlight/Planar/Dcel.hs b/src-dcel/Moonlight/Planar/Dcel.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Dcel.hs
@@ -0,0 +1,367 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE BangPatterns #-}
+
+-- | Constant-time fixed-index observations and persistent payload updates over
+-- the immutable DCEL. This is the explicit unchecked resident-index kernel;
+-- ordinary consumers should use scoped or owning handles so indices cannot
+-- cross mesh boundaries.
+module Moonlight.Planar.Dcel
+  ( outerFace
+  , numVertices
+  , numDirectedEdges
+  , numUndirectedEdges
+  , numFaces
+  , numInnerFaces
+  , vertexPoint
+  , vertexPoints
+  , vertexData
+  , directedEdgeData
+  , undirectedEdgeData
+  , faceData
+  , setVertexData
+  , setDirectedEdgeData
+  , setUndirectedEdgeData
+  , setFaceData
+  , mapVertices
+  , mapDirectedEdges
+  , mapUndirectedEdges
+  , mapFaces
+  , imapUndirectedEdges
+  , imapFaces
+  , vertexOutEdge
+  , adjacentEdge
+  , origin
+  , destination
+  , next
+  , previous
+  , incidentFace
+  , isOuterDirectedEdge
+  , isBoundaryEdge
+  , isConstraintEdge
+  , numConstraints
+  , undirectedEndpoints
+  , faceDirectedEdges
+  , faceVertices
+  , innerFaceDirectedEdges
+  , innerFaceDirectedEdgeTriples
+  , innerFaceVertices
+  , innerFaceVertexTriples
+  , vertexOutgoingEdges
+  , clockwise
+  , counterClockwise
+  , foldFaceDirectedEdges'
+  , foldVertexOutgoingEdges'
+  , topologyIndexBytes
+  , geometryTopologyBytes
+  ) where
+
+import qualified Data.Vector as V
+import Data.Word (Word8)
+import Moonlight.Planar.Internal.BoxedPaged (boxedUnsafeIndex, boxedUpdate)
+import Moonlight.Planar.Internal.Paged (pagedLength, pagedUnsafeIndex)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Incidence
+  ( circularEdgeFold
+  , circularEdgeWalk
+  , packedIncidentFace
+  , packedNext
+  , packedOptionalEdge
+  , packedOrigin
+  , packedPrevious
+  )
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (scalarByteSize)
+
+-- | The unique unbounded face, always stored at index zero.
+outerFace :: FaceId
+outerFace = FaceId 0
+
+-- | Number of vertices in the mesh.
+numVertices :: Triangulation mode vertex directed undirected face -> Int
+numVertices = pagedLength . triPointX
+{-# INLINE numVertices #-}
+
+-- | Number of oriented half-edges in the mesh.
+numDirectedEdges :: Triangulation mode vertex directed undirected face -> Int
+numDirectedEdges = (`quot` 4) . pagedLength . triHalfTopology
+{-# INLINE numDirectedEdges #-}
+
+-- | Number of twin pairs in the mesh.
+numUndirectedEdges :: Triangulation mode vertex directed undirected face -> Int
+numUndirectedEdges triangulation = numDirectedEdges triangulation `quot` 2
+{-# INLINE numUndirectedEdges #-}
+
+-- | Number of faces, including 'outerFace'.
+numFaces :: Triangulation mode vertex directed undirected face -> Int
+numFaces = pagedLength . triFaceEdge
+{-# INLINE numFaces #-}
+
+-- | Number of bounded triangular faces.
+numInnerFaces :: Triangulation mode vertex directed undirected face -> Int
+numInnerFaces triangulation = max 0 (numFaces triangulation - 1)
+{-# INLINE numInnerFaces #-}
+
+-- | Authoritative geometric position of an admitted vertex handle.
+vertexPoint :: Triangulation mode vertex directed undirected face -> VertexId -> Point
+vertexPoint triangulation vertex =
+  let !index = vertexIdIndex vertex
+   in Point (pagedUnsafeIndex (triPointX triangulation) index) (pagedUnsafeIndex (triPointY triangulation) index)
+{-# INLINE vertexPoint #-}
+
+-- | Dense vertex positions in handle order.
+vertexPoints :: Triangulation mode vertex directed undirected face -> V.Vector Point
+vertexPoints triangulation =
+  V.generate (numVertices triangulation) (vertexPoint triangulation . VertexId . fromIntegral)
+{-# INLINE vertexPoints #-}
+
+-- | Annotation carried by an admitted vertex handle.
+vertexData :: Triangulation mode vertex directed undirected face -> VertexId -> vertex
+vertexData triangulation vertex =
+  boxedUnsafeIndex (triVertexData triangulation) (vertexIdIndex vertex)
+{-# INLINE vertexData #-}
+
+-- | Annotation carried by an admitted directed edge.
+directedEdgeData :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> directed
+directedEdgeData triangulation edge =
+  boxedUnsafeIndex (triDirectedData triangulation) (directedEdgeIdIndex edge)
+{-# INLINE directedEdgeData #-}
+
+-- | Annotation carried by an admitted undirected edge.
+undirectedEdgeData :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> undirected
+undirectedEdgeData triangulation edge =
+  boxedUnsafeIndex (triUndirectedData triangulation) (undirectedEdgeIdIndex edge)
+{-# INLINE undirectedEdgeData #-}
+
+-- | Annotation carried by an admitted face.
+faceData :: Triangulation mode vertex directed undirected face -> FaceId -> face
+faceData triangulation face =
+  boxedUnsafeIndex (triFaceData triangulation) (faceIdIndex face)
+{-# INLINE faceData #-}
+
+-- | Replace one vertex annotation without changing geometry or topology.
+setVertexData
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> vertex
+  -> Triangulation mode vertex directed undirected face
+setVertexData triangulation vertex payload =
+  triangulation{triVertexData = boxedUpdate (vertexIdIndex vertex) payload (triVertexData triangulation)}
+
+-- | Replace one directed-edge annotation without changing geometry or topology.
+setDirectedEdgeData
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedEdgeId
+  -> directed
+  -> Triangulation mode vertex directed undirected face
+setDirectedEdgeData triangulation edge payload =
+  triangulation{triDirectedData = boxedUpdate (directedEdgeIdIndex edge) payload (triDirectedData triangulation)}
+
+-- | Replace one undirected-edge annotation without changing geometry or topology.
+setUndirectedEdgeData
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> undirected
+  -> Triangulation mode vertex directed undirected face
+setUndirectedEdgeData triangulation edge payload =
+  triangulation{triUndirectedData = boxedUpdate (undirectedEdgeIdIndex edge) payload (triUndirectedData triangulation)}
+
+-- | Replace one face annotation without changing geometry or topology.
+setFaceData
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> face
+  -> Triangulation mode vertex directed undirected face
+setFaceData triangulation face payload =
+  triangulation{triFaceData = boxedUpdate (faceIdIndex face) payload (triFaceData triangulation)}
+
+-- | One outgoing directed edge, if the vertex is connected.
+vertexOutEdge :: Triangulation mode vertex directed undirected face -> VertexId -> Maybe DirectedEdgeId
+vertexOutEdge triangulation vertex =
+  packedOptionalEdge (triVertexOut triangulation) (vertexIdIndex vertex)
+{-# INLINE vertexOutEdge #-}
+
+-- | One boundary edge of a face, if the face has a boundary.
+adjacentEdge :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe DirectedEdgeId
+adjacentEdge triangulation face =
+  packedOptionalEdge (triFaceEdge triangulation) (faceIdIndex face)
+{-# INLINE adjacentEdge #-}
+
+-- | Origin vertex of an admitted directed edge.
+origin :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> VertexId
+origin triangulation edge =
+  packedOrigin (triHalfTopology triangulation) edge
+{-# INLINE origin #-}
+
+-- | Destination vertex of an admitted directed edge.
+destination :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> VertexId
+destination triangulation = origin triangulation . reverseEdge
+{-# INLINE destination #-}
+
+-- | Next directed edge around the incident face.
+next :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
+next triangulation edge =
+  packedNext (triHalfTopology triangulation) edge
+{-# INLINE next #-}
+
+-- | Previous directed edge around the incident face.
+previous :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
+previous triangulation edge =
+  packedPrevious (triHalfTopology triangulation) edge
+{-# INLINE previous #-}
+
+-- | Face on the left of an admitted directed edge.
+incidentFace :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> FaceId
+incidentFace triangulation edge =
+  packedIncidentFace (triHalfTopology triangulation) edge
+{-# INLINE incidentFace #-}
+
+-- | Whether the directed edge is incident to the unbounded face.
+isOuterDirectedEdge :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> Bool
+isOuterDirectedEdge triangulation edge = incidentFace triangulation edge == outerFace
+{-# INLINE isOuterDirectedEdge #-}
+
+-- | Whether either orientation is incident to the outer face.
+isBoundaryEdge :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> Bool
+isBoundaryEdge triangulation edge =
+  let (forward, backward) = directedPair edge
+   in isOuterDirectedEdge triangulation forward || isOuterDirectedEdge triangulation backward
+{-# INLINE isBoundaryEdge #-}
+
+-- | Whether the edge belongs to the constrained-edge section.
+isConstraintEdge :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> Bool
+isConstraintEdge triangulation edge =
+  pagedUnsafeIndex (triConstraint triangulation) (undirectedEdgeIdIndex edge) /= (0 :: Word8)
+{-# INLINE isConstraintEdge #-}
+
+-- | Number of constrained undirected edges.
+numConstraints :: Triangulation mode vertex directed undirected face -> Int
+numConstraints = triConstraintCount
+{-# INLINE numConstraints #-}
+
+-- | Endpoints in the normalized orientation.
+undirectedEndpoints :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> (VertexId, VertexId)
+undirectedEndpoints triangulation edge =
+  let forward = normalizedDirected edge
+   in (origin triangulation forward, destination triangulation forward)
+{-# INLINE undirectedEndpoints #-}
+
+-- | Boundary cycle of a face in traversal order.
+faceDirectedEdges :: Triangulation mode vertex directed undirected face -> FaceId -> [DirectedEdgeId]
+faceDirectedEdges triangulation face =
+  case adjacentEdge triangulation face of
+    Nothing -> []
+    Just start -> circularEdgeWalk (numDirectedEdges triangulation) start (next triangulation)
+{-# INLINE faceDirectedEdges #-}
+
+-- | Origins along a face boundary cycle, in traversal order.
+faceVertices :: Triangulation mode vertex directed undirected face -> FaceId -> [VertexId]
+faceVertices triangulation = map (origin triangulation) . faceDirectedEdges triangulation
+{-# INLINE faceVertices #-}
+
+-- | The three directed edges of a bounded triangular face.
+innerFaceDirectedEdges :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
+innerFaceDirectedEdges triangulation face
+  | face == outerFace || faceIdIndex face >= numFaces triangulation = Nothing
+  | otherwise = do
+      e0 <- adjacentEdge triangulation face
+      let !e1 = next triangulation e0
+          !e2 = next triangulation e1
+      if next triangulation e2 == e0
+        then Just (e0, e1, e2)
+        else Nothing
+{-# INLINE innerFaceDirectedEdges #-}
+
+-- | Dense bounded-face directed-edge triples in face-handle order.
+innerFaceDirectedEdgeTriples
+  :: Triangulation mode vertex directed undirected face
+  -> V.Vector (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
+innerFaceDirectedEdgeTriples triangulation =
+  V.generate (numInnerFaces triangulation) $ \innerFaceIndex ->
+    let !faceIndex = innerFaceIndex + 1
+        !firstEdge =
+          DirectedEdgeId
+            (pagedUnsafeIndex (triFaceEdge triangulation) faceIndex)
+        !secondEdge = next triangulation firstEdge
+     in (firstEdge, secondEdge, next triangulation secondEdge)
+{-# INLINE innerFaceDirectedEdgeTriples #-}
+
+-- | The three vertices of a bounded triangular face.
+innerFaceVertices :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe (VertexId, VertexId, VertexId)
+innerFaceVertices triangulation face = do
+  (e0, e1, e2) <- innerFaceDirectedEdges triangulation face
+  pure (origin triangulation e0, origin triangulation e1, origin triangulation e2)
+{-# INLINE innerFaceVertices #-}
+
+-- | Dense bounded-face vertex triples in face-handle order.
+innerFaceVertexTriples
+  :: Triangulation mode vertex directed undirected face
+  -> V.Vector (VertexId, VertexId, VertexId)
+innerFaceVertexTriples triangulation =
+  fmap
+    (\(firstEdge, secondEdge, thirdEdge) ->
+      ( origin triangulation firstEdge
+      , origin triangulation secondEdge
+      , origin triangulation thirdEdge
+      )
+    )
+    (innerFaceDirectedEdgeTriples triangulation)
+{-# INLINE innerFaceVertexTriples #-}
+
+-- | Counter-clockwise ring of directed edges originating at a vertex.
+vertexOutgoingEdges :: Triangulation mode vertex directed undirected face -> VertexId -> [DirectedEdgeId]
+vertexOutgoingEdges triangulation vertex =
+  case vertexOutEdge triangulation vertex of
+    Nothing -> []
+    Just start -> circularEdgeWalk (numDirectedEdges triangulation) start (counterClockwise triangulation)
+{-# INLINE vertexOutgoingEdges #-}
+
+-- | Previous directed edge around its origin vertex.
+clockwise :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
+clockwise triangulation edge = next triangulation (reverseEdge edge)
+{-# INLINE clockwise #-}
+
+-- | Next directed edge around its origin vertex.
+counterClockwise :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
+counterClockwise triangulation edge = reverseEdge (previous triangulation edge)
+{-# INLINE counterClockwise #-}
+
+-- | Strict fold over the directed edges around a face.
+foldFaceDirectedEdges'
+  :: Triangulation mode vertex directed undirected face -> FaceId
+  -> (a -> DirectedEdgeId -> a)
+  -> a
+  -> a
+foldFaceDirectedEdges' triangulation face step initial =
+  case adjacentEdge triangulation face of
+    Nothing -> initial
+    Just start -> circularEdgeFold (numDirectedEdges triangulation) start (next triangulation) step initial
+{-# INLINE foldFaceDirectedEdges' #-}
+
+-- | Strict fold over the directed edges originating at a vertex.
+foldVertexOutgoingEdges'
+  :: Triangulation mode vertex directed undirected face -> VertexId
+  -> (a -> DirectedEdgeId -> a)
+  -> a
+  -> a
+foldVertexOutgoingEdges' triangulation vertex step initial =
+  case vertexOutEdge triangulation vertex of
+    Nothing -> initial
+    Just start -> circularEdgeFold (numDirectedEdges triangulation) start (counterClockwise triangulation) step initial
+{-# INLINE foldVertexOutgoingEdges' #-}
+
+-- Constraint bytes are intentionally reported separately by the CDT layer.
+-- | Bytes occupied by vertex, half-edge, and face topology indices.
+topologyIndexBytes :: Triangulation mode vertex directed undirected face -> Integer
+topologyIndexBytes triangulation =
+  4 * toInteger
+    ( pagedLength (triVertexOut triangulation)
+        + pagedLength (triHalfTopology triangulation)
+        + pagedLength (triFaceEdge triangulation)
+    )
+
+-- | Bytes occupied by authoritative coordinates and topology indices.
+geometryTopologyBytes :: Triangulation mode vertex directed undirected face -> Integer
+geometryTopologyBytes triangulation =
+  2 * toInteger scalarByteSize * toInteger (numVertices triangulation)
+    + topologyIndexBytes triangulation
diff --git a/src-dcel/Moonlight/Planar/Exact.hs b/src-dcel/Moonlight/Planar/Exact.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Exact.hs
@@ -0,0 +1,100 @@
+-- | Public exact-geometry capabilities. The implementation is private so
+-- proof-preserving owners can share admitted descent without exposing raw
+-- construction of invariant-bearing carriers.
+module Moonlight.Planar.Exact
+  ( ExactRational
+  , ExactArithmeticError (..)
+  , exactRational
+  , exactRationalNumerator
+  , exactRationalDenominator
+  , exactRationalBitWidth
+  , exactDivide
+  , PositiveExact
+  , UnitInterval
+  , ScalarRefinementError (..)
+  , positiveExact
+  , positiveExactValue
+  , unitInterval
+  , unitIntervalValue
+  , unitZero
+  , unitHalf
+  , unitOne
+  , positiveOne
+  , positiveTwo
+  , exactHalf
+  , exactThird
+  , blendPositive
+  , divideByPositive
+  , ratioPositive
+  , multiplyPositive
+  , positiveSumSquares
+  , ExactBounds
+  , exactPointsBounds
+  , boundsUnion
+  , boundsOverlap
+  , pointInBounds
+  , boundsMinimumX
+  , boundsMaximumX
+  , boundsMinimumY
+  , boundsMaximumY
+  , ExactPoint
+  , exactPoint
+  , exactPointCoordinates
+  , exactPointCross
+  , exactPointBitWidth
+  , ExactAffineLine
+  , ExactHalfPlaneError (..)
+  , exactAffineLine
+  , exactAffineLineCoefficients
+  , oppositeExactAffineLine
+  , exactAffineLineIntersection
+  , ExactClosedHalfPlane
+  , exactClosedHalfPlane
+  , exactClosedHalfPlaneFromDirectedEdge
+  , exactClosedHalfPlaneLine
+  , classifyExactPoint
+  , ExactRetainedPolygon
+  , exactRetainedPolygon
+  , exactRetainedPolygonPoints
+  , exactRetainedPolygonBoundary
+  , ExactClipDisposition (..)
+  , ExactClipError (..)
+  , ExactClipReceipt (..)
+  , emptyExactClipReceipt
+  , exactClipRetainedPolygon
+  , ExactVector (..)
+  , exactVectorFromPoints
+  , addExactVectors
+  , exactCross
+  , compareExactVectorAngle
+  , translateExactPoint
+  , ExactRay
+  , exactRay
+  , exactRayOrigin
+  , exactRayDirection
+  , ExactSegment
+  , ExactGeometryError (..)
+  , exactSegment
+  , exactSegmentEndpoints
+  , exactPointFromPoint
+  , exactPointFromQueryPoint
+  , exactPointToEmbeddingCandidate
+  , exactOrient2d
+  , exactOnClosedSegment
+  , SegmentRelation (..)
+  , allSegmentRelations
+  , exactSegmentRelation
+  , ExactIntersectionError (..)
+  , exactLineIntersection
+  , exactSupportingLineIntersection
+  ) where
+
+import Moonlight.Planar.Internal.Exact
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational, ExactArithmeticError (..), exactRational, exactRationalNumerator
+  , exactRationalDenominator, exactRationalBitWidth, exactDivide
+  , PositiveExact, UnitInterval, ScalarRefinementError (..)
+  , positiveExact, positiveExactValue, unitInterval, unitIntervalValue
+  , unitZero, unitHalf, unitOne, positiveOne, positiveTwo, exactHalf, exactThird
+  , blendPositive, divideByPositive, ratioPositive, multiplyPositive, positiveSumSquares )
+import Moonlight.Planar.Internal.SegmentRelation (SegmentRelation (..), allSegmentRelations)
diff --git a/src-dcel/Moonlight/Planar/FloodFillIterator.hs b/src-dcel/Moonlight/Planar/FloodFillIterator.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/FloodFillIterator.hs
@@ -0,0 +1,38 @@
+-- | Shape queries, face descent, and boundary gluing over an immutable mesh.
+module Moonlight.Planar.FloodFillIterator
+  ( DistanceMetric (..)
+  , CircleMetric
+  , CircleMetricError (..)
+  , RectangleMetric
+  , RectangleMetricError (..)
+  , circleMetric
+  , rectangleMetric
+  , edgesInShape
+  , verticesInShape
+  , edgesInCircle
+  , verticesInCircle
+  , edgesInRectangle
+  , verticesInRectangle
+  , floodFillFaces
+  , outerFaceFloodFill
+  , facesAtEvenBarrierDepth
+  , FaceComponent
+  , faceComponentFaces
+  , BoundaryOrientation (..)
+  , BoundaryLoop
+  , boundaryLoopOrientation
+  , boundaryLoopResidentVertices
+  , boundaryLoopVertices
+  , RegionBoundary
+  , regionBoundaryOuterLoop
+  , regionBoundaryHoleLoops
+  , BoundaryObstruction (..)
+  , faceComponents
+  , faceComponentsBy
+  , labelledRegionBoundaries
+  , componentBoundaryLoops
+  , componentBoundary
+  , componentBoundaryForFaces
+  ) where
+
+import Moonlight.Planar.Internal.FloodFillIterator
diff --git a/src-dcel/Moonlight/Planar/Handles/Dynamic.hs b/src-dcel/Moonlight/Planar/Handles/Dynamic.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Dynamic.hs
@@ -0,0 +1,435 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE EmptyDataDecls #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RoleAnnotations #-}
+
+-- | Owning handles that prevent identifiers from crossing mesh boundaries.
+module Moonlight.Planar.Handles.Dynamic
+  ( InnerTag
+  , PossiblyOuterTag
+  , FixedFaceHandle
+  , asPossiblyOuter
+  , fixedFaceId
+  , VertexHandle
+  , DirectedEdgeHandle
+  , UndirectedEdgeHandle
+  , FaceHandle
+  , vertexHandle
+  , directedEdgeHandle
+  , undirectedEdgeHandle
+  , faceHandle
+  , innerFaceHandle
+  , outerFaceHandle
+  , fixVertex
+  , fixDirectedEdge
+  , fixUndirectedEdge
+  , fixFace
+  , vertexHandleData
+  , vertexHandlePosition
+  , vertexHandleOutEdge
+  , vertexHandleOutEdges
+  , directedEdgeDataH
+  , directedEdgeFrom
+  , directedEdgeTo
+  , directedEdgeVertices
+  , directedEdgePositions
+  , directedEdgeReverse
+  , directedEdgeNext
+  , directedEdgePrevious
+  , directedEdgeClockwise
+  , directedEdgeCounterClockwise
+  , directedEdgeFace
+  , directedEdgeAsUndirected
+  , directedEdgeIsOuter
+  , directedEdgeSideQuery
+  , directedEdgeOppositeVertex
+  , directedEdgeOppositePosition
+  , directedEdgeProjectionFactor
+  , directedEdgeNearestPoint
+  , undirectedEdgeDataH
+  , undirectedEdgeAsDirected
+  , undirectedEdgeVertices
+  , undirectedEdgeIsConstraint
+  , undirectedEdgeIsBoundary
+  , faceDataH
+  , faceIsOuter
+  , faceAsInner
+  , faceAdjacentEdge
+  , faceAdjacentEdges
+  , innerFaceVertices
+  , innerFaceCircumcenter
+  , innerFacePositions
+  , innerFaceBarycentric
+  ) where
+
+import Moonlight.Planar.Dcel qualified as Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Predicates qualified as Admitted
+import Moonlight.Planar.LineSideInfo (LineSideInfo)
+import Moonlight.Planar.Math qualified as Math
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+
+-- | A face handle that is statically known not to denote the outer face.
+data InnerTag
+
+-- | A face handle that may denote the unique outer face.
+data PossiblyOuterTag
+
+type role FixedFaceHandle nominal
+-- | Fixed face identifier refined by whether it may denote the outer face.
+newtype FixedFaceHandle tag = FixedFaceHandle { unFixedFaceHandle :: FaceId }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord)
+
+-- | Forget the proof that a fixed face is bounded.
+asPossiblyOuter :: FixedFaceHandle InnerTag -> FixedFaceHandle PossiblyOuterTag
+asPossiblyOuter (FixedFaceHandle face) = FixedFaceHandle face
+{-# INLINE asPossiblyOuter #-}
+
+-- | Recover the unrefined face identifier.
+fixedFaceId :: FixedFaceHandle tag -> FaceId
+fixedFaceId (FixedFaceHandle face) = face
+{-# INLINE fixedFaceId #-}
+
+-- | Admitted vertex paired with its owning triangulation.
+data VertexHandle mode vertex directed undirected face = VertexHandle
+  !(Triangulation mode vertex directed undirected face)
+  !VertexId
+
+-- | Admitted directed edge paired with its owning triangulation.
+data DirectedEdgeHandle mode vertex directed undirected face = DirectedEdgeHandle
+  !(Triangulation mode vertex directed undirected face)
+  !DirectedEdgeId
+
+-- | Admitted undirected edge paired with its owning triangulation.
+data UndirectedEdgeHandle mode vertex directed undirected face = UndirectedEdgeHandle
+  !(Triangulation mode vertex directed undirected face)
+  !UndirectedEdgeId
+
+-- | Admitted face paired with its owning triangulation and outer-face proof.
+data FaceHandle tag mode vertex directed undirected face = FaceHandle
+  !(Triangulation mode vertex directed undirected face)
+  !(FixedFaceHandle tag)
+
+instance Show (VertexHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixVertex
+
+instance Show (DirectedEdgeHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixDirectedEdge
+
+instance Show (UndirectedEdgeHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixUndirectedEdge
+
+instance Show (FaceHandle tag mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixFace
+
+-- | Admit a vertex identifier into a triangulation.
+vertexHandle
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> Maybe (VertexHandle mode vertex directed undirected face)
+vertexHandle triangulation vertex
+  | vertexIdIndex vertex < Dcel.numVertices triangulation = Just (VertexHandle triangulation vertex)
+  | otherwise = Nothing
+
+-- | Admit a directed-edge identifier into a triangulation.
+directedEdgeHandle
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedEdgeId
+  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
+directedEdgeHandle triangulation edge
+  | directedEdgeIdIndex edge < Dcel.numDirectedEdges triangulation = Just (DirectedEdgeHandle triangulation edge)
+  | otherwise = Nothing
+
+-- | Admit an undirected-edge identifier into a triangulation.
+undirectedEdgeHandle
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> Maybe (UndirectedEdgeHandle mode vertex directed undirected face)
+undirectedEdgeHandle triangulation edge
+  | undirectedEdgeIdIndex edge < Dcel.numUndirectedEdges triangulation = Just (UndirectedEdgeHandle triangulation edge)
+  | otherwise = Nothing
+
+-- | Admit a face identifier that may denote the outer face.
+faceHandle
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> Maybe (FaceHandle PossiblyOuterTag mode vertex directed undirected face)
+faceHandle triangulation face
+  | faceIdIndex face < Dcel.numFaces triangulation = Just (FaceHandle triangulation (FixedFaceHandle face))
+  | otherwise = Nothing
+
+-- | Admit a face identifier while proving that it is bounded.
+innerFaceHandle
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
+innerFaceHandle triangulation face
+  | face == Dcel.outerFace = Nothing
+  | otherwise = do
+      FaceHandle _ (FixedFaceHandle valid) <- faceHandle triangulation face
+      pure (FaceHandle triangulation (FixedFaceHandle valid))
+
+-- | Owning handle to the unique unbounded face.
+outerFaceHandle
+  :: Triangulation mode vertex directed undirected face
+  -> FaceHandle PossiblyOuterTag mode vertex directed undirected face
+outerFaceHandle triangulation = FaceHandle triangulation (FixedFaceHandle Dcel.outerFace)
+
+-- | Forget ownership and recover the vertex identifier.
+fixVertex :: VertexHandle mode vertex directed undirected face -> VertexId
+fixVertex (VertexHandle _ vertex) = vertex
+{-# INLINE fixVertex #-}
+
+-- | Forget ownership and recover the directed-edge identifier.
+fixDirectedEdge :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeId
+fixDirectedEdge (DirectedEdgeHandle _ edge) = edge
+{-# INLINE fixDirectedEdge #-}
+
+-- | Forget ownership and recover the undirected-edge identifier.
+fixUndirectedEdge :: UndirectedEdgeHandle mode vertex directed undirected face -> UndirectedEdgeId
+fixUndirectedEdge (UndirectedEdgeHandle _ edge) = edge
+{-# INLINE fixUndirectedEdge #-}
+
+-- | Forget ownership while retaining the outer-face refinement.
+fixFace :: FaceHandle tag mode vertex directed undirected face -> FixedFaceHandle tag
+fixFace (FaceHandle _ face) = face
+{-# INLINE fixFace #-}
+
+-- | Vertex annotation through an owning handle.
+vertexHandleData :: VertexHandle mode vertex directed undirected face -> vertex
+vertexHandleData (VertexHandle triangulation vertex) = Dcel.vertexData triangulation vertex
+{-# INLINE vertexHandleData #-}
+
+-- | Authoritative vertex position through an owning handle.
+vertexHandlePosition
+  :: VertexHandle mode vertex directed undirected face
+  -> Point
+vertexHandlePosition (VertexHandle triangulation vertex) = (Dcel.vertexPoint triangulation vertex)
+{-# INLINE vertexHandlePosition #-}
+
+-- | One outgoing edge of a connected vertex.
+vertexHandleOutEdge
+  :: VertexHandle mode vertex directed undirected face
+  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
+vertexHandleOutEdge (VertexHandle triangulation vertex) = DirectedEdgeHandle triangulation <$> Dcel.vertexOutEdge triangulation vertex
+
+-- | Directed edges leaving a vertex in ring order.
+vertexHandleOutEdges
+  :: VertexHandle mode vertex directed undirected face
+  -> [DirectedEdgeHandle mode vertex directed undirected face]
+vertexHandleOutEdges (VertexHandle triangulation vertex) = map (DirectedEdgeHandle triangulation) (Dcel.vertexOutgoingEdges triangulation vertex)
+
+-- | Directed-edge annotation through an owning handle.
+directedEdgeDataH :: DirectedEdgeHandle mode vertex directed undirected face -> directed
+directedEdgeDataH (DirectedEdgeHandle triangulation edge) = Dcel.directedEdgeData triangulation edge
+{-# INLINE directedEdgeDataH #-}
+
+-- | Origin vertex of an owning directed edge.
+directedEdgeFrom :: DirectedEdgeHandle mode vertex directed undirected face -> VertexHandle mode vertex directed undirected face
+directedEdgeFrom (DirectedEdgeHandle triangulation edge) = VertexHandle triangulation (Dcel.origin triangulation edge)
+{-# INLINE directedEdgeFrom #-}
+
+-- | Destination vertex of an owning directed edge.
+directedEdgeTo :: DirectedEdgeHandle mode vertex directed undirected face -> VertexHandle mode vertex directed undirected face
+directedEdgeTo (DirectedEdgeHandle triangulation edge) = VertexHandle triangulation (Dcel.destination triangulation edge)
+{-# INLINE directedEdgeTo #-}
+
+-- | Origin and destination of an owning directed edge.
+directedEdgeVertices
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> (VertexHandle mode vertex directed undirected face, VertexHandle mode vertex directed undirected face)
+directedEdgeVertices edge = (directedEdgeFrom edge, directedEdgeTo edge)
+
+-- | Origin and destination positions of an owning directed edge.
+directedEdgePositions
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> (Point, Point)
+directedEdgePositions edge = (vertexHandlePosition (directedEdgeFrom edge), vertexHandlePosition (directedEdgeTo edge))
+{-# INLINE directedEdgePositions #-}
+
+-- | Reverse an owning directed edge.
+directedEdgeReverse :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
+directedEdgeReverse (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (reverseEdge edge)
+{-# INLINE directedEdgeReverse #-}
+
+-- | Next owning edge around the incident face.
+directedEdgeNext :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
+directedEdgeNext (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.next triangulation edge)
+{-# INLINE directedEdgeNext #-}
+
+-- | Previous owning edge around the incident face.
+directedEdgePrevious :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
+directedEdgePrevious (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.previous triangulation edge)
+{-# INLINE directedEdgePrevious #-}
+
+-- | Previous owning edge around its origin vertex.
+directedEdgeClockwise :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
+directedEdgeClockwise (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.clockwise triangulation edge)
+{-# INLINE directedEdgeClockwise #-}
+
+-- | Next owning edge around its origin vertex.
+directedEdgeCounterClockwise :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
+directedEdgeCounterClockwise (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.counterClockwise triangulation edge)
+{-# INLINE directedEdgeCounterClockwise #-}
+
+-- | Owning handle to the incident face.
+directedEdgeFace
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> FaceHandle PossiblyOuterTag mode vertex directed undirected face
+directedEdgeFace (DirectedEdgeHandle triangulation edge) = FaceHandle triangulation (FixedFaceHandle (Dcel.incidentFace triangulation edge))
+
+-- | Forget the orientation of an owning edge.
+directedEdgeAsUndirected
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> UndirectedEdgeHandle mode vertex directed undirected face
+directedEdgeAsUndirected (DirectedEdgeHandle triangulation edge) = UndirectedEdgeHandle triangulation (asUndirected edge)
+
+-- | Whether the owning edge is incident to the outer face.
+directedEdgeIsOuter :: DirectedEdgeHandle mode vertex directed undirected face -> Bool
+directedEdgeIsOuter = faceIsOuter . directedEdgeFace
+
+-- | Exact side of the owning edge's oriented line.
+directedEdgeSideQuery
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> QueryPoint
+  -> LineSideInfo
+directedEdgeSideQuery edge query =
+  let (from, to) = directedEdgePositions edge
+   in Admitted.sideQuery from to (queryPointValue query)
+
+-- | Undirected-edge annotation through an owning handle.
+undirectedEdgeDataH :: UndirectedEdgeHandle mode vertex directed undirected face -> undirected
+undirectedEdgeDataH (UndirectedEdgeHandle triangulation edge) = Dcel.undirectedEdgeData triangulation edge
+{-# INLINE undirectedEdgeDataH #-}
+
+-- | Normalized directed orientation of an owning undirected edge.
+undirectedEdgeAsDirected
+  :: UndirectedEdgeHandle mode vertex directed undirected face
+  -> DirectedEdgeHandle mode vertex directed undirected face
+undirectedEdgeAsDirected (UndirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (normalizedDirected edge)
+
+-- | Endpoints of an owning undirected edge.
+undirectedEdgeVertices
+  :: UndirectedEdgeHandle mode vertex directed undirected face
+  -> (VertexHandle mode vertex directed undirected face, VertexHandle mode vertex directed undirected face)
+undirectedEdgeVertices = directedEdgeVertices . undirectedEdgeAsDirected
+
+-- | Face annotation through an owning handle.
+faceDataH :: FaceHandle tag mode vertex directed undirected face -> face
+faceDataH (FaceHandle triangulation (FixedFaceHandle face)) = Dcel.faceData triangulation face
+{-# INLINE faceDataH #-}
+
+-- | Whether the owning face is the unique unbounded face.
+faceIsOuter :: FaceHandle tag mode vertex directed undirected face -> Bool
+faceIsOuter (FaceHandle _ (FixedFaceHandle face)) = face == Dcel.outerFace
+{-# INLINE faceIsOuter #-}
+
+-- | Refine an owning face handle by excluding the outer face.
+faceAsInner
+  :: FaceHandle PossiblyOuterTag mode vertex directed undirected face
+  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
+faceAsInner handle@(FaceHandle triangulation (FixedFaceHandle face))
+  | faceIsOuter handle = Nothing
+  | otherwise = Just (FaceHandle triangulation (FixedFaceHandle face))
+
+-- | One owning edge on the face boundary.
+faceAdjacentEdge
+  :: FaceHandle tag mode vertex directed undirected face
+  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
+faceAdjacentEdge (FaceHandle triangulation (FixedFaceHandle face)) = DirectedEdgeHandle triangulation <$> Dcel.adjacentEdge triangulation face
+
+-- | Owning directed boundary of a face.
+faceAdjacentEdges
+  :: FaceHandle tag mode vertex directed undirected face
+  -> [DirectedEdgeHandle mode vertex directed undirected face]
+faceAdjacentEdges (FaceHandle triangulation (FixedFaceHandle face)) = map (DirectedEdgeHandle triangulation) (Dcel.faceDirectedEdges triangulation face)
+
+-- | Three owning vertices of a bounded face.
+innerFaceVertices
+  :: FaceHandle InnerTag mode vertex directed undirected face
+  -> Maybe
+       ( VertexHandle mode vertex directed undirected face
+       , VertexHandle mode vertex directed undirected face
+       , VertexHandle mode vertex directed undirected face
+       )
+innerFaceVertices (FaceHandle triangulation (FixedFaceHandle face)) =
+  (\(a, b, c) -> (VertexHandle triangulation a, VertexHandle triangulation b, VertexHandle triangulation c))
+    <$> Dcel.innerFaceVertices triangulation face
+
+-- | Circumcenter of an owning bounded face.
+innerFaceCircumcenter
+  :: FaceHandle InnerTag mode vertex directed undirected face
+  -> Maybe (Point)
+innerFaceCircumcenter face = do
+  (a, b, c) <- innerFaceVertices face
+  Math.circumcenter (vertexHandlePosition a) (vertexHandlePosition b) (vertexHandlePosition c)
+
+-- | Vertex opposite an owning directed edge in its bounded incident face.
+directedEdgeOppositeVertex
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> Maybe (VertexHandle mode vertex directed undirected face)
+directedEdgeOppositeVertex (DirectedEdgeHandle triangulation edge)
+  | Dcel.incidentFace triangulation edge == Dcel.outerFace = Nothing
+  | otherwise =
+      Just (VertexHandle triangulation (Dcel.destination triangulation (Dcel.next triangulation edge)))
+
+-- | Position opposite an owning directed edge in its bounded incident face.
+directedEdgeOppositePosition
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> Maybe (Point)
+directedEdgeOppositePosition = fmap vertexHandlePosition . directedEdgeOppositeVertex
+{-# INLINE directedEdgeOppositePosition #-}
+
+-- | Projection parameter of a point onto an owning directed edge's line.
+directedEdgeProjectionFactor
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> Point
+  -> Double
+directedEdgeProjectionFactor edge query =
+  let (from, to) = directedEdgePositions edge
+   in Math.projectionFactor from to query
+
+-- | Nearest point on the closed owning directed edge.
+directedEdgeNearestPoint
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> Point
+  -> Point
+directedEdgeNearestPoint edge query =
+  let (from@(Point ax ay), to@(Point bx by)) = directedEdgePositions edge
+      factor = max 0 (min 1 (Math.projectionFactor from to query))
+   in Point (ax + factor * (bx - ax)) (ay + factor * (by - ay))
+
+-- | Whether an owning undirected edge is constrained.
+undirectedEdgeIsConstraint
+  :: UndirectedEdgeHandle mode vertex directed undirected face
+  -> Bool
+undirectedEdgeIsConstraint (UndirectedEdgeHandle triangulation edge) =
+  Dcel.isConstraintEdge triangulation edge
+
+-- | Whether an owning undirected edge touches the outer face.
+undirectedEdgeIsBoundary
+  :: UndirectedEdgeHandle mode vertex directed undirected face
+  -> Bool
+undirectedEdgeIsBoundary (UndirectedEdgeHandle triangulation edge) =
+  Dcel.isBoundaryEdge triangulation edge
+
+-- | Positions of the three vertices of an owning bounded face.
+innerFacePositions
+  :: FaceHandle InnerTag mode vertex directed undirected face
+  -> Maybe (Point, Point, Point)
+innerFacePositions face = do
+  (a, b, c) <- innerFaceVertices face
+  pure (vertexHandlePosition a, vertexHandlePosition b, vertexHandlePosition c)
+{-# INLINE innerFacePositions #-}
+
+-- | Barycentric coordinates in an owning bounded face.
+innerFaceBarycentric
+  :: FaceHandle InnerTag mode vertex directed undirected face
+  -> Point
+  -> Maybe (Double, Double, Double)
+innerFaceBarycentric face query = do
+  (a, b, c) <- innerFacePositions face
+  Math.barycentricCoordinates a b c query
diff --git a/src-dcel/Moonlight/Planar/Handles/HandleDefs.hs b/src-dcel/Moonlight/Planar/Handles/HandleDefs.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/HandleDefs.hs
@@ -0,0 +1,22 @@
+-- | Opaque compact resident indices and lawful orientation operations. The
+-- indices do not encode mesh ownership: use "Moonlight.Planar.Handles.Scoped"
+-- for local traversal or "Moonlight.Planar.Handles.Dynamic" when a handle
+-- must escape its mesh scope.
+module Moonlight.Planar.Handles.HandleDefs
+  ( VertexId
+  , unVertexId
+  , FaceId
+  , unFaceId
+  , DirectedEdgeId
+  , unDirectedEdgeId
+  , UndirectedEdgeId
+  , unUndirectedEdgeId
+  , reverseEdge
+  , asUndirected
+  , normalizedDirected
+  , reversedDirected
+  , directedPair
+  , isNormalized
+  ) where
+
+import Moonlight.Planar.Internal.HandleDefs
diff --git a/src-dcel/Moonlight/Planar/Handles/Iterators/CircularIterator.hs b/src-dcel/Moonlight/Planar/Handles/Iterators/CircularIterator.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Iterators/CircularIterator.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Bounded traversal of a cyclic successor relation.
+module Moonlight.Planar.Handles.Iterators.CircularIterator
+  ( circularList
+  , foldCircular'
+  ) where
+
+import GHC.Exts (build)
+
+-- | The cycle reached from a start by repeated advance, in visit order.
+--
+-- Emitted forwards, for the reason 'Moonlight.Planar.Dcel.circularWalk'
+-- is: accumulating in reverse and reversing at the end builds the ring twice
+-- and hands back a list no consumer can fuse with.
+circularList :: forall a. Eq a => Int -> (a -> a) -> a -> [a]
+circularList limit advance start =
+  build
+    ( \(link :: a -> result -> result) (stop :: result) ->
+        let go :: Int -> a -> Bool -> result
+            go !remaining !current !visited
+              | remaining <= 0 = stop
+              | visited && current == start = stop
+              | otherwise = link current (go (remaining - 1) (advance current) True)
+         in go limit start False
+    )
+{-# INLINE circularList #-}
+
+-- | Strictly fold a bounded cycle in visit order.
+foldCircular' :: forall a b. Eq a => Int -> (a -> a) -> a -> (b -> a -> b) -> b -> b
+foldCircular' limit advance start step = go limit start False
+ where
+  go
+    :: Int
+    -> a
+    -> Bool
+    -> b
+    -> b
+  go !remaining !current !visited !accumulator
+    | remaining <= 0 = accumulator
+    | visited && current == start = accumulator
+    | otherwise = go (remaining - 1) (advance current) True (step accumulator current)
diff --git a/src-dcel/Moonlight/Planar/Handles/Iterators/DynamicIterators.hs b/src-dcel/Moonlight/Planar/Handles/Iterators/DynamicIterators.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Iterators/DynamicIterators.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Whole-mesh traversal through topology-bound dynamic handles.
+module Moonlight.Planar.Handles.Iterators.DynamicIterators
+  ( vertexHandles
+  , directedEdgeHandles
+  , undirectedEdgeHandles
+  , allFaceHandles
+  , innerFaceHandles
+  , hullEdgeHandles
+  , hullVertexHandles
+  , foldVertexHandles'
+  , foldDirectedEdgeHandles'
+  , foldUndirectedEdgeHandles'
+  , foldAllFaceHandles'
+  , foldInnerFaceHandles'
+  , foldHullEdgeHandles'
+  , foldHullVertexHandles'
+  ) where
+
+import Moonlight.Planar.Handles.Dynamic
+import Moonlight.Planar.Handles.Iterators.FixedIterators qualified as Fixed
+import Moonlight.Planar.Handles.Iterators.HullIterator qualified as Hull
+import Moonlight.Planar.Types
+
+-- | Dynamic handles in fixed-index order. The list spine is lazy; use the
+-- strict folds below for allocation-free traversal in hot paths.
+vertexHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [VertexHandle mode vertex directed undirected face]
+vertexHandles triangulation =
+  mapValid (vertexHandle triangulation) (Fixed.vertices triangulation)
+
+-- | Every directed-edge handle in fixed-index order.
+directedEdgeHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [DirectedEdgeHandle mode vertex directed undirected face]
+directedEdgeHandles triangulation =
+  mapValid (directedEdgeHandle triangulation) (Fixed.directedEdges triangulation)
+
+-- | Every undirected-edge handle in fixed-index order.
+undirectedEdgeHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [UndirectedEdgeHandle mode vertex directed undirected face]
+undirectedEdgeHandles triangulation =
+  mapValid (undirectedEdgeHandle triangulation) (Fixed.undirectedEdges triangulation)
+
+-- | Every face handle, including the outer face.
+allFaceHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [FaceHandle PossiblyOuterTag mode vertex directed undirected face]
+allFaceHandles triangulation =
+  mapValid (faceHandle triangulation) (Fixed.allFaces triangulation)
+
+-- | Every inner-face handle.
+innerFaceHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [FaceHandle InnerTag mode vertex directed undirected face]
+innerFaceHandles triangulation =
+  mapValid (innerFaceHandle triangulation) (Fixed.innerFaces triangulation)
+
+-- | Hull directed-edge handles in boundary order.
+hullEdgeHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [DirectedEdgeHandle mode vertex directed undirected face]
+hullEdgeHandles triangulation =
+  mapValid (directedEdgeHandle triangulation) (Hull.hullEdges triangulation)
+
+-- | Hull vertex handles in boundary order.
+hullVertexHandles
+  :: Triangulation mode vertex directed undirected face
+  -> [VertexHandle mode vertex directed undirected face]
+hullVertexHandles triangulation =
+  map directedEdgeFrom (hullEdgeHandles triangulation)
+
+-- | Strict fold over every vertex handle.
+foldVertexHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> VertexHandle mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldVertexHandles' triangulation step =
+  Fixed.foldVertices' triangulation (applyValid (vertexHandle triangulation) step)
+
+-- | Strict fold over every directed-edge handle.
+foldDirectedEdgeHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> DirectedEdgeHandle mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldDirectedEdgeHandles' triangulation step =
+  Fixed.foldDirectedEdges' triangulation (applyValid (directedEdgeHandle triangulation) step)
+
+-- | Strict fold over every undirected-edge handle.
+foldUndirectedEdgeHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> UndirectedEdgeHandle mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldUndirectedEdgeHandles' triangulation step =
+  Fixed.foldUndirectedEdges' triangulation (applyValid (undirectedEdgeHandle triangulation) step)
+
+-- | Strict fold over every face handle, including the outer face.
+foldAllFaceHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> FaceHandle PossiblyOuterTag mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldAllFaceHandles' triangulation step =
+  Fixed.foldAllFaces' triangulation (applyValid (faceHandle triangulation) step)
+
+-- | Strict fold over every inner-face handle.
+foldInnerFaceHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> FaceHandle InnerTag mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldInnerFaceHandles' triangulation step =
+  Fixed.foldInnerFaces' triangulation (applyValid (innerFaceHandle triangulation) step)
+
+-- | Strict fold over hull directed-edge handles.
+foldHullEdgeHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> DirectedEdgeHandle mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldHullEdgeHandles' triangulation step =
+  Hull.foldHullEdges' triangulation (applyValid (directedEdgeHandle triangulation) step)
+
+-- | Strict fold over hull vertex handles.
+foldHullVertexHandles'
+  :: Triangulation mode vertex directed undirected face
+  -> (accumulator -> VertexHandle mode vertex directed undirected face -> accumulator)
+  -> accumulator
+  -> accumulator
+foldHullVertexHandles' triangulation step =
+  foldHullEdgeHandles' triangulation (\accumulator edge -> step accumulator (directedEdgeFrom edge))
+
+mapValid :: (fixed -> Maybe dynamic) -> [fixed] -> [dynamic]
+mapValid make = foldr collect []
+ where
+  collect fixed rest = case make fixed of
+    Just dynamic -> dynamic : rest
+    Nothing -> rest
+
+applyValid
+  :: (fixed -> Maybe dynamic)
+  -> (accumulator -> dynamic -> accumulator)
+  -> accumulator
+  -> fixed
+  -> accumulator
+applyValid make step !accumulator fixed = case make fixed of
+  Just dynamic -> step accumulator dynamic
+  Nothing -> accumulator
diff --git a/src-dcel/Moonlight/Planar/Handles/Iterators/FixedIterators.hs b/src-dcel/Moonlight/Planar/Handles/Iterators/FixedIterators.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Iterators/FixedIterators.hs
@@ -0,0 +1,87 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Whole-mesh traversal by identifier range, each range paired with the
+-- strict fold that consumes it.
+module Moonlight.Planar.Handles.Iterators.FixedIterators
+  ( vertices
+  , directedEdges
+  , undirectedEdges
+  , allFaces
+  , innerFaces
+  , foldVertices'
+  , foldDirectedEdges'
+  , foldUndirectedEdges'
+  , foldAllFaces'
+  , foldInnerFaces'
+  ) where
+
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Types
+
+-- | Every vertex identifier, ascending.
+--
+-- The enumerators are producers, so their bodies have to reach the consumer's
+-- module: a fold over one of these should be a loop over an index, not a walk
+-- over cons cells built by a call it could not see into.
+vertices :: Triangulation mode vertex directed undirected face -> [VertexId]
+vertices triangulation = [VertexId (fromIntegral index) | index <- [0 .. numVertices triangulation - 1]]
+{-# INLINE vertices #-}
+
+-- | Every directed-edge identifier, ascending.
+directedEdges :: Triangulation mode vertex directed undirected face -> [DirectedEdgeId]
+directedEdges triangulation = [DirectedEdgeId (fromIntegral index) | index <- [0 .. numDirectedEdges triangulation - 1]]
+{-# INLINE directedEdges #-}
+
+-- | Every undirected-edge identifier, ascending.
+undirectedEdges :: Triangulation mode vertex directed undirected face -> [UndirectedEdgeId]
+undirectedEdges triangulation = [UndirectedEdgeId (fromIntegral index) | index <- [0 .. numUndirectedEdges triangulation - 1]]
+{-# INLINE undirectedEdges #-}
+
+-- | Every face identifier, the outer face first.
+allFaces :: Triangulation mode vertex directed undirected face -> [FaceId]
+allFaces triangulation = [FaceId (fromIntegral index) | index <- [0 .. numFaces triangulation - 1]]
+{-# INLINE allFaces #-}
+
+-- | Every face identifier except the outer face.
+innerFaces :: Triangulation mode vertex directed undirected face -> [FaceId]
+innerFaces triangulation = [FaceId (fromIntegral index) | index <- [1 .. numFaces triangulation - 1]]
+{-# INLINE innerFaces #-}
+
+-- | Strict fold over 'vertices'; the rest of the family follows.
+foldVertices' :: Triangulation mode vertex directed undirected face -> (a -> VertexId -> a) -> a -> a
+foldVertices' triangulation step = foldRange (numVertices triangulation) (VertexId . fromIntegral) step
+{-# INLINE foldVertices' #-}
+
+-- | Strict fold over every directed-edge identifier.
+foldDirectedEdges' :: Triangulation mode vertex directed undirected face -> (a -> DirectedEdgeId -> a) -> a -> a
+foldDirectedEdges' triangulation step = foldRange (numDirectedEdges triangulation) (DirectedEdgeId . fromIntegral) step
+{-# INLINE foldDirectedEdges' #-}
+
+-- | Strict fold over every undirected-edge identifier.
+foldUndirectedEdges' :: Triangulation mode vertex directed undirected face -> (a -> UndirectedEdgeId -> a) -> a -> a
+foldUndirectedEdges' triangulation step = foldRange (numUndirectedEdges triangulation) (UndirectedEdgeId . fromIntegral) step
+{-# INLINE foldUndirectedEdges' #-}
+
+-- | Strict fold over every face, including the outer face.
+foldAllFaces' :: Triangulation mode vertex directed undirected face -> (a -> FaceId -> a) -> a -> a
+foldAllFaces' triangulation step = foldRange (numFaces triangulation) (FaceId . fromIntegral) step
+{-# INLINE foldAllFaces' #-}
+
+-- | Strict fold over every inner face.
+foldInnerFaces' :: Triangulation mode vertex directed undirected face -> (a -> FaceId -> a) -> a -> a
+foldInnerFaces' triangulation step initial = go 1 initial
+ where
+  !end = numFaces triangulation
+  go !index !accumulator
+    | index >= end = accumulator
+    | otherwise = go (index + 1) (step accumulator (FaceId (fromIntegral index)))
+{-# INLINE foldInnerFaces' #-}
+
+foldRange :: Int -> (Int -> b) -> (a -> b -> a) -> a -> a
+foldRange end make step = go 0
+ where
+  go !index !accumulator
+    | index >= end = accumulator
+    | otherwise = go (index + 1) (step accumulator (make index))
+{-# INLINE foldRange #-}
diff --git a/src-dcel/Moonlight/Planar/Handles/Iterators/HullIterator.hs b/src-dcel/Moonlight/Planar/Handles/Iterators/HullIterator.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Iterators/HullIterator.hs
@@ -0,0 +1,29 @@
+-- | The convex hull, walked as its edges or as its vertices.
+module Moonlight.Planar.Handles.Iterators.HullIterator
+  ( hullEdges
+  , hullVertices
+  , foldHullEdges'
+  , foldHullVertices'
+  ) where
+
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Types
+
+-- | The convex hull as directed edges, in order.
+hullEdges :: Triangulation mode vertex directed undirected face -> [DirectedEdgeId]
+hullEdges triangulation = faceDirectedEdges triangulation outerFace
+{-# INLINE hullEdges #-}
+
+-- | The convex hull as vertices, in order.
+hullVertices :: Triangulation mode vertex directed undirected face -> [VertexId]
+hullVertices triangulation = map (origin triangulation) (hullEdges triangulation)
+{-# INLINE hullVertices #-}
+
+-- | Strict fold over 'hullEdges'.
+foldHullEdges' :: Triangulation mode vertex directed undirected face -> (a -> DirectedEdgeId -> a) -> a -> a
+foldHullEdges' triangulation = foldFaceDirectedEdges' triangulation outerFace
+
+-- | Strict fold over 'hullVertices'.
+foldHullVertices' :: Triangulation mode vertex directed undirected face -> (a -> VertexId -> a) -> a -> a
+foldHullVertices' triangulation step = foldHullEdges' triangulation (\acc edge -> step acc (origin triangulation edge))
diff --git a/src-dcel/Moonlight/Planar/Handles/Scoped.hs b/src-dcel/Moonlight/Planar/Handles/Scoped.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Handles/Scoped.hs
@@ -0,0 +1,274 @@
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RoleAnnotations #-}
+
+-- | Zero-cost mesh-scoped identifiers. A scope is introduced only by
+-- 'withScopedTriangulation', so an identifier from one mesh cannot be supplied
+-- to observations of another mesh. Use owning handles when a handle must
+-- escape the continuation.
+module Moonlight.Planar.Handles.Scoped
+  ( ScopedTriangulation
+  , ScopedVertex
+  , ScopedDirectedEdge
+  , ScopedUndirectedEdge
+  , ScopedFace
+  , withScopedTriangulation
+  , scopedVertices
+  , scopedDirectedEdges
+  , scopedUndirectedEdges
+  , scopedFaces
+  , scopedInnerFaces
+  , scopedOuterFace
+  , scopedVertexPoint
+  , scopedVertexData
+  , scopedDirectedEdgeData
+  , scopedUndirectedEdgeData
+  , scopedFaceData
+  , scopedVertexOutEdge
+  , scopedAdjacentEdge
+  , scopedOrigin
+  , scopedDestination
+  , scopedReverseEdge
+  , scopedAsUndirected
+  , scopedNext
+  , scopedPrevious
+  , scopedIncidentFace
+  , scopedIsOuterDirectedEdge
+  , scopedIsBoundaryEdge
+  , scopedIsConstraintEdge
+  , scopedUndirectedEndpoints
+  , scopedFaceDirectedEdges
+  , scopedFaceVertices
+  , scopedVertexOutgoingEdges
+  ) where
+
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Handles.HandleDefs
+  ( DirectedEdgeId
+  , FaceId
+  , UndirectedEdgeId
+  , VertexId
+  , asUndirected
+  , reverseEdge
+  )
+import qualified Moonlight.Planar.Handles.Iterators.FixedIterators as Fixed
+import Moonlight.Planar.Point (Point)
+import Moonlight.Planar.Types (Triangulation)
+
+type role ScopedTriangulation nominal nominal representational representational representational representational
+newtype ScopedTriangulation scope mode vertex directed undirected face =
+  ScopedTriangulation (Triangulation mode vertex directed undirected face)
+
+type role ScopedVertex nominal
+newtype ScopedVertex scope = ScopedVertex VertexId
+  deriving stock (Eq, Ord, Show)
+
+type role ScopedDirectedEdge nominal
+newtype ScopedDirectedEdge scope = ScopedDirectedEdge DirectedEdgeId
+  deriving stock (Eq, Ord, Show)
+
+type role ScopedUndirectedEdge nominal
+newtype ScopedUndirectedEdge scope = ScopedUndirectedEdge UndirectedEdgeId
+  deriving stock (Eq, Ord, Show)
+
+type role ScopedFace nominal
+newtype ScopedFace scope = ScopedFace FaceId
+  deriving stock (Eq, Ord, Show)
+
+-- | Introduce one nominal scope for all identifiers derived from this mesh.
+withScopedTriangulation
+  :: Triangulation mode vertex directed undirected face
+  -> (forall scope. ScopedTriangulation scope mode vertex directed undirected face -> result)
+  -> result
+withScopedTriangulation triangulation use = use (ScopedTriangulation triangulation)
+{-# INLINE withScopedTriangulation #-}
+
+scopedVertices
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> [ScopedVertex scope]
+scopedVertices (ScopedTriangulation triangulation) =
+  fmap ScopedVertex (Fixed.vertices triangulation)
+
+scopedDirectedEdges
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> [ScopedDirectedEdge scope]
+scopedDirectedEdges (ScopedTriangulation triangulation) =
+  fmap ScopedDirectedEdge (Fixed.directedEdges triangulation)
+
+scopedUndirectedEdges
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> [ScopedUndirectedEdge scope]
+scopedUndirectedEdges (ScopedTriangulation triangulation) =
+  fmap ScopedUndirectedEdge (Fixed.undirectedEdges triangulation)
+
+scopedFaces
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> [ScopedFace scope]
+scopedFaces (ScopedTriangulation triangulation) =
+  fmap ScopedFace (Fixed.allFaces triangulation)
+
+scopedInnerFaces
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> [ScopedFace scope]
+scopedInnerFaces (ScopedTriangulation triangulation) =
+  fmap ScopedFace (Fixed.innerFaces triangulation)
+
+scopedOuterFace
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedFace scope
+scopedOuterFace _ = ScopedFace Dcel.outerFace
+{-# INLINE scopedOuterFace #-}
+
+scopedVertexPoint
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedVertex scope
+  -> Point
+scopedVertexPoint (ScopedTriangulation triangulation) (ScopedVertex vertex) =
+  Dcel.vertexPoint triangulation vertex
+{-# INLINE scopedVertexPoint #-}
+
+scopedVertexData
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedVertex scope
+  -> vertex
+scopedVertexData (ScopedTriangulation triangulation) (ScopedVertex vertex) =
+  Dcel.vertexData triangulation vertex
+{-# INLINE scopedVertexData #-}
+
+scopedDirectedEdgeData
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> directed
+scopedDirectedEdgeData (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  Dcel.directedEdgeData triangulation edge
+{-# INLINE scopedDirectedEdgeData #-}
+
+scopedUndirectedEdgeData
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedUndirectedEdge scope
+  -> undirected
+scopedUndirectedEdgeData (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
+  Dcel.undirectedEdgeData triangulation edge
+{-# INLINE scopedUndirectedEdgeData #-}
+
+scopedFaceData
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedFace scope
+  -> face
+scopedFaceData (ScopedTriangulation triangulation) (ScopedFace face) =
+  Dcel.faceData triangulation face
+{-# INLINE scopedFaceData #-}
+
+scopedVertexOutEdge
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedVertex scope
+  -> Maybe (ScopedDirectedEdge scope)
+scopedVertexOutEdge (ScopedTriangulation triangulation) (ScopedVertex vertex) =
+  ScopedDirectedEdge <$> Dcel.vertexOutEdge triangulation vertex
+
+scopedAdjacentEdge
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedFace scope
+  -> Maybe (ScopedDirectedEdge scope)
+scopedAdjacentEdge (ScopedTriangulation triangulation) (ScopedFace face) =
+  ScopedDirectedEdge <$> Dcel.adjacentEdge triangulation face
+
+scopedOrigin
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> ScopedVertex scope
+scopedOrigin (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  ScopedVertex (Dcel.origin triangulation edge)
+{-# INLINE scopedOrigin #-}
+
+scopedDestination
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> ScopedVertex scope
+scopedDestination (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  ScopedVertex (Dcel.destination triangulation edge)
+{-# INLINE scopedDestination #-}
+
+scopedReverseEdge :: ScopedDirectedEdge scope -> ScopedDirectedEdge scope
+scopedReverseEdge (ScopedDirectedEdge edge) = ScopedDirectedEdge (reverseEdge edge)
+{-# INLINE scopedReverseEdge #-}
+
+scopedAsUndirected :: ScopedDirectedEdge scope -> ScopedUndirectedEdge scope
+scopedAsUndirected (ScopedDirectedEdge edge) = ScopedUndirectedEdge (asUndirected edge)
+{-# INLINE scopedAsUndirected #-}
+
+scopedNext
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> ScopedDirectedEdge scope
+scopedNext (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  ScopedDirectedEdge (Dcel.next triangulation edge)
+{-# INLINE scopedNext #-}
+
+scopedPrevious
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> ScopedDirectedEdge scope
+scopedPrevious (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  ScopedDirectedEdge (Dcel.previous triangulation edge)
+{-# INLINE scopedPrevious #-}
+
+scopedIncidentFace
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> ScopedFace scope
+scopedIncidentFace (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  ScopedFace (Dcel.incidentFace triangulation edge)
+{-# INLINE scopedIncidentFace #-}
+
+scopedIsOuterDirectedEdge
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedDirectedEdge scope
+  -> Bool
+scopedIsOuterDirectedEdge (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
+  Dcel.isOuterDirectedEdge triangulation edge
+{-# INLINE scopedIsOuterDirectedEdge #-}
+
+scopedIsBoundaryEdge
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedUndirectedEdge scope
+  -> Bool
+scopedIsBoundaryEdge (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
+  Dcel.isBoundaryEdge triangulation edge
+{-# INLINE scopedIsBoundaryEdge #-}
+
+scopedIsConstraintEdge
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedUndirectedEdge scope
+  -> Bool
+scopedIsConstraintEdge (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
+  Dcel.isConstraintEdge triangulation edge
+{-# INLINE scopedIsConstraintEdge #-}
+
+scopedUndirectedEndpoints
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedUndirectedEdge scope
+  -> (ScopedVertex scope, ScopedVertex scope)
+scopedUndirectedEndpoints (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
+  let (firstVertex, secondVertex) = Dcel.undirectedEndpoints triangulation edge
+   in (ScopedVertex firstVertex, ScopedVertex secondVertex)
+
+scopedFaceDirectedEdges
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedFace scope
+  -> [ScopedDirectedEdge scope]
+scopedFaceDirectedEdges (ScopedTriangulation triangulation) (ScopedFace face) =
+  fmap ScopedDirectedEdge (Dcel.faceDirectedEdges triangulation face)
+
+scopedFaceVertices
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedFace scope
+  -> [ScopedVertex scope]
+scopedFaceVertices (ScopedTriangulation triangulation) (ScopedFace face) =
+  fmap ScopedVertex (Dcel.faceVertices triangulation face)
+
+scopedVertexOutgoingEdges
+  :: ScopedTriangulation scope mode vertex directed undirected face
+  -> ScopedVertex scope
+  -> [ScopedDirectedEdge scope]
+scopedVertexOutgoingEdges (ScopedTriangulation triangulation) (ScopedVertex vertex) =
+  fmap ScopedDirectedEdge (Dcel.vertexOutgoingEdges triangulation vertex)
diff --git a/src-dcel/Moonlight/Planar/Internal/BoundaryCycle.hs b/src-dcel/Moonlight/Planar/Internal/BoundaryCycle.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/BoundaryCycle.hs
@@ -0,0 +1,310 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Shared pure algebra for descending oriented boundary graphs into simple
+-- cycles, then simplifying and classifying those cycles.
+module Moonlight.Planar.Internal.BoundaryCycle
+  ( traceOrientedBoundaryCircuits
+  , simplifyBoundaryCycle
+  , rotateCycleLeast
+  , rotateCycleLeastBy
+  , consecutivePairs
+  , unorderedPairs
+  , orderedPair
+  , cyclePairs
+  , cyclePairsNonEmpty
+  , cyclicTriples
+  , firstNonCounterClockwiseTurn
+  , admitsSimpleCycleEdgeRelation
+  ) where
+
+import Data.List (tails)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import Moonlight.Planar.Internal.SegmentRelation
+  ( SegmentRelation (..)
+  )
+
+-- | Consume every supplied oriented edge exactly once and split point contacts
+-- into vertex-simple cycles. The outgoing lists carry the caller's required
+-- local angular or identifier order. A malformed graph is translated directly
+-- through the caller's obstruction constructor; this shared worker owns no
+-- disposable error vocabulary.
+traceOrientedBoundaryCircuits
+  :: (Ord vertex, Ord edge)
+  => (edge -> vertex)
+  -> (edge -> vertex)
+  -> (edge -> edge -> obstruction)
+  -> Map vertex [edge]
+  -> Set edge
+  -> Either obstruction [NonEmpty vertex]
+traceOrientedBoundaryCircuits edgeOrigin edgeDestination obstruction outgoing edges =
+  descend edges []
+ where
+  descend remaining cycles =
+    case Set.lookupMin remaining of
+      Nothing -> Right (reverse cycles)
+      Just seed ->
+        let (untraced, circuitEdges) =
+              eulerCircuit remaining (edgeOrigin seed)
+         in case NonEmpty.nonEmpty circuitEdges of
+              Nothing -> Left (obstruction seed seed)
+              Just circuit ->
+                case splitCircuit circuit of
+                  Left failedEdge -> Left (obstruction seed failedEdge)
+                  Right splitCycles ->
+                    descend
+                      untraced
+                      (reverse (NonEmpty.toList splitCycles) <> cycles)
+
+  -- Hierholzer descent: delete each chosen edge, then prepend it while
+  -- backtracking. No graph-state stream or branch search is materialized.
+  eulerCircuit remaining start = walk remaining start [] []
+   where
+    walk untraced current incomingEdges circuit =
+      case
+          List.find
+            (`Set.member` untraced)
+            (Map.findWithDefault [] current outgoing) of
+        Just edge ->
+          walk
+            (Set.delete edge untraced)
+            (edgeDestination edge)
+            (edge : incomingEdges)
+            circuit
+        Nothing ->
+          case incomingEdges of
+            edge : previousEdges ->
+              walk
+                untraced
+                (edgeOrigin edge)
+                previousEdges
+                (edge : circuit)
+            [] -> (untraced, circuit)
+
+  -- The resident path stays simple. Closing at a resident vertex emits and
+  -- removes exactly that suffix, partitioning the Euler circuit into cycles.
+  splitCircuit circuit =
+    walk
+      start
+      0
+      (Map.singleton start 0)
+      []
+      []
+      (NonEmpty.toList circuit)
+   where
+    start = edgeOrigin (NonEmpty.head circuit)
+
+    walk current depth depths reversedEdges cycles remaining =
+      case remaining of
+        [] ->
+          case
+              ( depth
+              , reversedEdges
+              , NonEmpty.nonEmpty (reverse cycles)
+              ) of
+            (0, [], Just simpleCycles) -> Right simpleCycles
+            _ -> Left (NonEmpty.last circuit)
+        edge : rest
+          | edgeOrigin edge /= current -> Left edge
+          | otherwise ->
+              let target = edgeDestination edge
+               in case Map.lookup target depths of
+                    Nothing ->
+                      walk
+                        target
+                        (depth + 1)
+                        (Map.insert target (depth + 1) depths)
+                        (edge : reversedEdges)
+                        cycles
+                        rest
+                    Just repeatedDepth ->
+                      let suffixLength = depth - repeatedDepth
+                          suffixEdges = take suffixLength reversedEdges
+                          simpleCycle =
+                            fmap edgeOrigin
+                              ( NonEmpty.reverse
+                                  (edge :| suffixEdges)
+                              )
+                       in walk
+                            target
+                            repeatedDepth
+                            ( foldr
+                                (Map.delete . edgeDestination)
+                                depths
+                                suffixEdges
+                            )
+                            (drop suffixLength reversedEdges)
+                            (simpleCycle : cycles)
+                            rest
+{-# INLINABLE traceOrientedBoundaryCircuits #-}
+
+-- | Remove precisely the vertices admitted by @isRedundant@ until a fixed
+-- point is reached, then classify the winding at the least keyed retained
+-- vertex. The returned cycle preserves the tracer's start; publication layers
+-- may rotate their value-level observation independently. The caller supplies
+-- its obstruction constructor so the shared worker does not allocate a
+-- disposable intermediate error vocabulary at either specialization.
+simplifyBoundaryCycle
+  :: forall value key obstruction.
+     Ord key
+  => ([value] -> obstruction)
+  -> (value -> value -> value -> Bool)
+  -> (value -> value -> value -> Ordering)
+  -> (value -> key)
+  -> [value]
+  -> Either obstruction (Ordering, NonEmpty value)
+simplifyBoundaryCycle obstruction isRedundant orientation key = descend
+ where
+  descend values@(_ : _ : _ : _) =
+    let triples = cyclicTriples values
+        retained =
+          [ current
+          | (previousValue, current, nextValue) <- triples
+          , not (isRedundant previousValue current nextValue)
+          ]
+     in if length retained == length values
+          then classify values triples
+          else descend retained
+  descend values = Left (obstruction values)
+
+  classify values triples =
+    case triples of
+      [] -> Left (obstruction values)
+      firstTriple : remainingTriples ->
+        let (previousValue, current, nextValue) =
+              List.foldl' chooseLeast firstTriple remainingTriples
+            winding = orientation previousValue current nextValue
+         in case (winding, values) of
+              (EQ, _) -> Left (obstruction values)
+              (_, initialValue : rest) -> Right (winding, initialValue :| rest)
+              _ -> Left (obstruction values)
+
+  chooseLeast
+    :: (value, value, value)
+    -> (value, value, value)
+    -> (value, value, value)
+  chooseLeast selected@(_, selectedValue, _) candidate@(_, candidateValue, _)
+    | key candidateValue < key selectedValue = candidate
+    | otherwise = selected
+{-# INLINE simplifyBoundaryCycle #-}
+
+-- | Choose the least value as a cycle's observational origin without changing
+-- its orientation. Boundary publication and generated convex geometry share
+-- this one canonical rotation owner.
+rotateCycleLeast :: Ord value => NonEmpty value -> NonEmpty value
+rotateCycleLeast = rotateCycleLeastBy id
+{-# INLINE rotateCycleLeast #-}
+
+-- | Choose the least keyed value as a cycle's observational origin.
+rotateCycleLeastBy
+  :: Ord key
+  => (value -> key)
+  -> NonEmpty value
+  -> NonEmpty value
+rotateCycleLeastBy key values =
+  case break ((== minimumKey) . key) asList of
+    (before, selected : after) -> selected :| (after <> before)
+    _ -> values
+ where
+  asList = NonEmpty.toList values
+  minimumKey =
+    List.foldl'
+      (\selected candidate -> min selected (key candidate))
+      (key (NonEmpty.head values))
+      (NonEmpty.tail values)
+{-# INLINE rotateCycleLeastBy #-}
+
+-- | Every adjacent pair in a linear sequence.
+consecutivePairs :: [value] -> [(value, value)]
+consecutivePairs values = zip values (drop 1 values)
+{-# INLINE consecutivePairs #-}
+
+-- | Every unordered pair exactly once.
+unorderedPairs :: [value] -> [(value, value)]
+unorderedPairs values =
+  [(left, right) | left : remaining <- tails values, right <- remaining]
+{-# INLINE unorderedPairs #-}
+
+-- | Canonically orient an unordered pair.
+orderedPair :: Ord value => value -> value -> (value, value)
+orderedPair left right
+  | left <= right = (left, right)
+  | otherwise = (right, left)
+{-# INLINE orderedPair #-}
+
+-- | Every directed edge of a non-empty cycle in cycle order.
+cyclePairs :: NonEmpty value -> [(value, value)]
+cyclePairs = NonEmpty.toList . cyclePairsNonEmpty
+{-# INLINE cyclePairs #-}
+
+-- | The non-empty form of 'cyclePairs'. A singleton cycle has its sole value
+-- as both ends of its sole cyclic edge.
+cyclePairsNonEmpty :: NonEmpty value -> NonEmpty (value, value)
+cyclePairsNonEmpty values@(firstValue :| remaining) =
+  NonEmpty.zip values successors
+ where
+  successors =
+    case remaining of
+      [] -> firstValue :| []
+      nextValue : rest -> nextValue :| (rest <> [firstValue])
+{-# INLINE cyclePairsNonEmpty #-}
+
+-- | Consecutive cyclic triples, one centered at every value.
+cyclicTriples :: [value] -> [(value, value, value)]
+cyclicTriples values =
+  case values of
+    initial : second : remaining ->
+      let final = List.foldl' (\_ current -> current) initial (second : remaining)
+       in zip3
+            (final : values)
+            values
+            (second : remaining <> [initial])
+    _ -> []
+{-# INLINE cyclicTriples #-}
+
+-- | Locate the first turn that is not strictly counter-clockwise. Convex
+-- boundary admissions share this one indexed obstruction descent.
+firstNonCounterClockwiseTurn
+  :: (value -> value -> value -> Ordering)
+  -> NonEmpty value
+  -> Maybe (Int, Ordering)
+firstNonCounterClockwiseTurn orientation =
+  List.find ((/= GT) . snd)
+    . zip [0 ..]
+    . fmap (\(previousValue, current, nextValue) -> orientation previousValue current nextValue)
+    . cyclicTriples
+    . NonEmpty.toList
+{-# INLINE firstNonCounterClockwiseTurn #-}
+
+-- | Whether one exact relation between two indexed cycle edges is compatible
+-- with a simple closed boundary. Only adjacent edges may meet, and then only
+-- at their shared endpoint. Producers remain free to obtain relation sections
+-- through the sweep plan or direct pairwise descent; this is the single
+-- admissibility policy glued over both representations.
+admitsSimpleCycleEdgeRelation
+  :: Int
+  -> Int
+  -> Int
+  -> SegmentRelation
+  -> Bool
+admitsSimpleCycleEdgeRelation segmentCount leftIndex rightIndex relation =
+  relation == SegmentsDisjoint
+    || ( cycleEdgesAreAdjacent
+           segmentCount leftIndex rightIndex
+           && relation == SegmentsShareEndpoint
+       )
+{-# INLINE admitsSimpleCycleEdgeRelation #-}
+
+cycleEdgesAreAdjacent :: Int -> Int -> Int -> Bool
+cycleEdgesAreAdjacent segmentCount leftIndex rightIndex =
+  upperIndex == lowerIndex + 1
+    || (lowerIndex == 0 && upperIndex == segmentCount - 1)
+ where
+  lowerIndex = min leftIndex rightIndex
+  upperIndex = max leftIndex rightIndex
+{-# INLINE cycleEdgesAreAdjacent #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/CellSet.hs b/src-dcel/Moonlight/Planar/Internal/CellSet.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/CellSet.hs
@@ -0,0 +1,344 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Invariant-bearing finite closed cell selections. Geometry-free incidence
+-- and the exact coordinates of precisely the selected vertices remain sealed
+-- together, without retaining native coordinates, payloads or search caches.
+module Moonlight.Planar.Internal.CellSet
+  ( ExactCellSet (..)
+  , CellSelectionError (..)
+  , exactCellSet
+  , residentExactCellSet
+  , closeFaceCellSet
+  , closeExactCellSetWith
+  , exactCellSetVertexCount
+  , exactCellSetEdgeCount
+  , exactCellSetFaceCount
+  , foldExactCellVertices
+  , foldExactCellEdges
+  , foldExactCellFaces
+  , exactCellSetIsFaceClosure
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import GHC.Generics (Generic)
+import Moonlight.Planar.Dcel
+  ( numFaces
+  , numUndirectedEdges
+  , numVertices
+  , vertexPoint
+  )
+import Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , faceBoundaryComponents
+  , faceIsolatedVertices
+  , incidenceEdgeCount
+  , incidenceFaceCount
+  , incidenceUndirectedEndpoints
+  , incidenceVertexCount
+  , nativePlanarIncidence
+  )
+import Moonlight.Planar.Exact (ExactPoint, exactPointFromPoint)
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  , asUndirected
+  , faceIdIndex
+  , undirectedEdgeIdIndex
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Point (PointValidationError)
+
+-- | The keys of the exact-point map are the selected vertices; a second
+-- vertex set would merely be a disagreeable copy.
+data ExactCellSet = ExactCellSet
+  !PlanarIncidence
+  !(IntMap.IntMap ExactPoint)
+  !IntSet.IntSet
+  !IntSet.IntSet
+
+data ClosedCellIds = ClosedCellIds
+  { closedVertexIds :: !IntSet.IntSet
+  , closedEdgeIds :: !IntSet.IntSet
+  , closedFaceIds :: !IntSet.IntSet
+  }
+
+data CellSelectionError
+  = CellVertexOutOfRange !VertexId !Int
+  | CellEdgeOutOfRange !UndirectedEdgeId !Int
+  | CellFaceOutOfRange !FaceId !Int
+  | CellOuterFaceSelected
+  | CellCoordinateInvalid !VertexId !PointValidationError
+  | CellEdgeBoundaryMissing !UndirectedEdgeId !VertexId
+  | CellFaceEdgeMissing !FaceId !UndirectedEdgeId
+  | CellFaceVertexMissing !FaceId !VertexId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The strict authoring boundary validates handles and closure once, then
+-- converts only selected vertices.
+exactCellSet
+  :: Triangulation mode vertex directed undirected face
+  -> [VertexId]
+  -> [UndirectedEdgeId]
+  -> [FaceId]
+  -> Either CellSelectionError ExactCellSet
+exactCellSet triangulation selectedVertices selectedEdges selectedFaces = do
+  let incidence = nativePlanarIncidence triangulation
+      verticesSet = vertexSet selectedVertices
+      edgesSet = edgeSet selectedEdges
+      facesSet = faceSet selectedFaces
+  validateHandles incidence selectedVertices selectedEdges selectedFaces
+  traverse_ (validateEdgeClosure incidence verticesSet) selectedEdges
+  traverse_ (validateFaceClosure incidence verticesSet edgesSet) selectedFaces
+  sealCellSet
+    (ordinaryExactPoint triangulation)
+    incidence
+    (ClosedCellIds verticesSet edgesSet facesSet)
+
+-- | Seal the complete resident DCEL without rechecking ranges or closure
+-- already established by its dense handle families. Exact coordinates are
+-- still admitted at this representation boundary; only impossible handle
+-- refusals and repeated incidence walks disappear.
+residentExactCellSet
+  :: Triangulation mode vertex directed undirected face
+  -> Either CellSelectionError ExactCellSet
+residentExactCellSet triangulation = do
+  let vertexIndices = [0 .. numVertices triangulation - 1]
+      edgeIndices = [0 .. numUndirectedEdges triangulation - 1]
+      faceIndices = [1 .. numFaces triangulation - 1]
+  exactPoints <-
+    IntMap.fromDistinctAscList
+      <$> traverse
+        ( \residentIndex -> do
+            point <- ordinaryExactPoint triangulation (VertexId (fromIntegral residentIndex))
+            pure (residentIndex, point)
+        )
+        vertexIndices
+  pure
+    ( ExactCellSet
+        (nativePlanarIncidence triangulation)
+        exactPoints
+        (IntSet.fromDistinctAscList edgeIndices)
+        (IntSet.fromDistinctAscList faceIndices)
+    )
+
+-- | Close a bounded face selection over all resident boundary cells. Face
+-- handles are author input and are checked; constructed closure is not then
+-- pointlessly proved a second time.
+closeFaceCellSet
+  :: Triangulation mode vertex directed undirected face
+  -> [FaceId]
+  -> Either CellSelectionError ExactCellSet
+closeFaceCellSet triangulation selectedFaces = do
+  let incidence = nativePlanarIncidence triangulation
+  validateHandles incidence [] [] selectedFaces
+  sealCellSet
+    (ordinaryExactPoint triangulation)
+    incidence
+    (closeCellIds incidence [] [] selectedFaces)
+
+-- | Seal exact-arrangement closure derived by its trusted downstream owner.
+-- Handles come from this admitted incidence, so no coordinate projection,
+-- native mesh or repeated handle validation is introduced.
+closeExactCellSetWith
+  :: (VertexId -> Either CellSelectionError ExactPoint)
+  -> PlanarIncidence
+  -> [VertexId]
+  -> [UndirectedEdgeId]
+  -> [FaceId]
+  -> Either CellSelectionError ExactCellSet
+closeExactCellSetWith exactPointAt incidence explicitVertices explicitEdges selectedFaces =
+  sealCellSet
+    exactPointAt
+    incidence
+    (closeCellIds incidence explicitVertices explicitEdges selectedFaces)
+
+ordinaryExactPoint
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> Either CellSelectionError ExactPoint
+ordinaryExactPoint triangulation vertex =
+  first (CellCoordinateInvalid vertex)
+    (exactPointFromPoint (vertexPoint triangulation vertex))
+
+sealCellSet
+  :: (VertexId -> Either CellSelectionError ExactPoint)
+  -> PlanarIncidence
+  -> ClosedCellIds
+  -> Either CellSelectionError ExactCellSet
+sealCellSet exactPointAt incidence closed = do
+  exactPoints <- exactPointsFor exactPointAt (closedVertexIds closed)
+  pure
+    ( ExactCellSet
+        incidence
+        exactPoints
+        (closedEdgeIds closed)
+        (closedFaceIds closed)
+    )
+
+closeCellIds
+  :: PlanarIncidence
+  -> [VertexId]
+  -> [UndirectedEdgeId]
+  -> [FaceId]
+  -> ClosedCellIds
+closeCellIds incidence explicitVertices explicitEdges selectedFaces =
+  let faceEdges =
+        concatMap
+          (map asUndirected . concat . faceBoundaryComponents incidence)
+          selectedFaces
+      edges = edgeSet (explicitEdges <> faceEdges)
+      closedEdges = map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList edges)
+      edgeVertices =
+        concatMap
+          (\edge ->
+             let (from, to) = incidenceUndirectedEndpoints incidence edge
+              in [from, to])
+          closedEdges
+      vertices =
+        vertexSet
+          ( explicitVertices
+              <> edgeVertices
+              <> concatMap (faceIsolatedVertices incidence) selectedFaces
+          )
+   in ClosedCellIds
+        { closedVertexIds = vertices
+        , closedEdgeIds = edges
+        , closedFaceIds = faceSet selectedFaces
+        }
+
+exactPointsFor
+  :: (VertexId -> Either CellSelectionError ExactPoint)
+  -> IntSet.IntSet
+  -> Either CellSelectionError (IntMap.IntMap ExactPoint)
+exactPointsFor exactPointAt selected =
+  IntMap.fromAscList
+    <$> traverse
+      (\index -> do
+         point <- exactPointAt (VertexId (fromIntegral index))
+         pure (index, point))
+      (IntSet.toAscList selected)
+
+validateHandles
+  :: PlanarIncidence
+  -> [VertexId]
+  -> [UndirectedEdgeId]
+  -> [FaceId]
+  -> Either CellSelectionError ()
+validateHandles incidence selectedVertices selectedEdges selectedFaces = do
+  traverse_ validateVertex selectedVertices
+  traverse_ validateEdge selectedEdges
+  traverse_ validateFace selectedFaces
+ where
+  validateVertex vertex
+    | vertexIdIndex vertex < incidenceVertexCount incidence = Right ()
+    | otherwise = Left (CellVertexOutOfRange vertex (incidenceVertexCount incidence))
+  validateEdge edge
+    | undirectedEdgeIdIndex edge < incidenceEdgeCount incidence = Right ()
+    | otherwise = Left (CellEdgeOutOfRange edge (incidenceEdgeCount incidence))
+  validateFace face
+    | faceIdIndex face == 0 = Left CellOuterFaceSelected
+    | faceIdIndex face < incidenceFaceCount incidence = Right ()
+    | otherwise = Left (CellFaceOutOfRange face (incidenceFaceCount incidence))
+
+validateEdgeClosure
+  :: PlanarIncidence
+  -> IntSet.IntSet
+  -> UndirectedEdgeId
+  -> Either CellSelectionError ()
+validateEdgeClosure incidence selectedVertices edge =
+  traverse_ requireVertex [from, to]
+ where
+  (from, to) = incidenceUndirectedEndpoints incidence edge
+  requireVertex vertex
+    | IntSet.member (vertexIdIndex vertex) selectedVertices = Right ()
+    | otherwise = Left (CellEdgeBoundaryMissing edge vertex)
+
+validateFaceClosure
+  :: PlanarIncidence
+  -> IntSet.IntSet
+  -> IntSet.IntSet
+  -> FaceId
+  -> Either CellSelectionError ()
+validateFaceClosure incidence selectedVertices selectedEdges face = do
+  traverse_ (requireEdge . asUndirected) (concat (faceBoundaryComponents incidence face))
+  traverse_ requireVertex (faceIsolatedVertices incidence face)
+ where
+  requireEdge edge
+    | IntSet.member (undirectedEdgeIdIndex edge) selectedEdges = Right ()
+    | otherwise = Left (CellFaceEdgeMissing face edge)
+  requireVertex vertex
+    | IntSet.member (vertexIdIndex vertex) selectedVertices = Right ()
+    | otherwise = Left (CellFaceVertexMissing face vertex)
+
+vertexSet :: [VertexId] -> IntSet.IntSet
+vertexSet = IntSet.fromList . map vertexIdIndex
+
+edgeSet :: [UndirectedEdgeId] -> IntSet.IntSet
+edgeSet = IntSet.fromList . map undirectedEdgeIdIndex
+
+faceSet :: [FaceId] -> IntSet.IntSet
+faceSet = IntSet.fromList . map faceIdIndex
+
+exactCellSetVertexCount :: ExactCellSet -> Int
+exactCellSetVertexCount (ExactCellSet _ selected _ _) = IntMap.size selected
+
+exactCellSetEdgeCount :: ExactCellSet -> Int
+exactCellSetEdgeCount (ExactCellSet _ _ selected _) = IntSet.size selected
+
+exactCellSetFaceCount :: ExactCellSet -> Int
+exactCellSetFaceCount (ExactCellSet _ _ _ selected) = IntSet.size selected
+
+foldExactCellVertices
+  :: (accumulator -> VertexId -> ExactPoint -> accumulator)
+  -> accumulator
+  -> ExactCellSet
+  -> accumulator
+foldExactCellVertices step initial (ExactCellSet _ selected _ _) =
+  IntMap.foldlWithKey'
+    (\accumulator index point ->
+       step accumulator (VertexId (fromIntegral index)) point)
+    initial
+    selected
+
+foldExactCellEdges
+  :: (accumulator -> UndirectedEdgeId -> accumulator)
+  -> accumulator
+  -> ExactCellSet
+  -> accumulator
+foldExactCellEdges step initial (ExactCellSet _ _ selected _) =
+  IntSet.foldl'
+    (\accumulator index -> step accumulator (UndirectedEdgeId (fromIntegral index)))
+    initial
+    selected
+
+foldExactCellFaces
+  :: (accumulator -> FaceId -> accumulator)
+  -> accumulator
+  -> ExactCellSet
+  -> accumulator
+foldExactCellFaces step initial (ExactCellSet _ _ _ selected) =
+  IntSet.foldl'
+    (\accumulator index -> step accumulator (FaceId (fromIntegral index)))
+    initial
+    selected
+
+-- | Whether the value contains exactly the downward closure of its selected
+-- faces, with no additional isolated vertex or edge cells. This is the precise
+-- admission condition for the conventional polygonal perimeter projection.
+exactCellSetIsFaceClosure :: ExactCellSet -> Bool
+exactCellSetIsFaceClosure (ExactCellSet incidence points edges faces) =
+  let selectedFaces =
+        map (FaceId . fromIntegral) (IntSet.toAscList faces)
+      closed = closeCellIds incidence [] [] selectedFaces
+   in IntMap.keysSet points == closedVertexIds closed
+        && edges == closedEdgeIds closed
+        && faces == closedFaceIds closed
diff --git a/src-dcel/Moonlight/Planar/Internal/Exact.hs b/src-dcel/Moonlight/Planar/Internal/Exact.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Exact.hs
@@ -0,0 +1,1286 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact rational planar geometry over admitted binary64 points.
+module Moonlight.Planar.Internal.Exact where
+
+import Control.DeepSeq (NFData (..))
+import Control.Monad (foldM)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Maybe (mapMaybe)
+import qualified Data.Sequence as Sequence
+import Data.Sequence (Seq, ViewL (..), ViewR (..), (|>))
+import GHC.Generics (Generic)
+import Moonlight.Planar.Internal.Dyadic (integerRatioToDouble)
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( admitsSimpleCycleEdgeRelation
+  , cyclePairs
+  , cyclePairsNonEmpty
+  , firstNonCounterClockwiseTurn
+  , unorderedPairs
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactArithmeticError (..)
+  , ExactRational
+  , exactDivide
+  , exactRational
+  , exactRationalBitWidth
+  , exactRationalDenominator
+  , exactRationalDenominatorBitWidth
+  , exactRationalFromFiniteDouble
+  , exactRationalIsZero
+  , exactRationalNumerator
+  , exactSignum
+  )
+import Moonlight.Planar.Internal.SegmentRelation
+  ( SegmentRelation (..)
+  , segmentRelationWith
+  )
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Point (Point (..), PointValidationError, QueryPoint, queryPointValue)
+
+-- | A strict exact Cartesian point.
+data ExactPoint = ExactPoint !ExactRational !ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | An exact affine line @a*x + b*y + c = 0@ with a nonzero normal.
+data ExactAffineLine =
+  ExactAffineLine !ExactRational !ExactRational !ExactRational
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactAffineLine where
+  rnf (ExactAffineLine coefficientX coefficientY constant) =
+    rnf coefficientX `seq` rnf coefficientY `seq` rnf constant
+  {-# INLINE rnf #-}
+
+-- | The closed left half-plane of an oriented affine line. A point belongs
+-- when the line evaluation is nonnegative.
+newtype ExactClosedHalfPlane = ExactClosedHalfPlane ExactAffineLine
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One polygon vertex together with the original affine line supporting its
+-- incoming edge. Intersections therefore never reconstruct a line from
+-- already-derived endpoints.
+data ExactRetainedVertex =
+  ExactRetainedVertex !ExactPoint !ExactAffineLine
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactRetainedVertex where
+  rnf (ExactRetainedVertex point line) = rnf point `seq` rnf line
+  {-# INLINE rnf #-}
+
+-- | A strict counter-clockwise convex polygon whose incoming edges retain
+-- their original supporting equations.
+newtype ExactRetainedPolygon =
+  ExactRetainedPolygon (NonEmpty ExactRetainedVertex)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactRetainedPolygon where
+  rnf (ExactRetainedPolygon vertices) = rnf vertices
+  {-# INLINE rnf #-}
+
+-- | Typed refusals from affine-line and retained-polygon admission.
+data ExactHalfPlaneError
+  = ExactAffineLineZeroNormal
+      !ExactRational
+      !ExactRational
+      !ExactRational
+  | ExactRetainedPolygonTooFewVertices !Int
+  | ExactRetainedPolygonNonConvexTurn !Int !Ordering
+  | ExactRetainedPolygonSelfRelation !Int !Int !SegmentRelation
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Dimensional disposition after exact closed-half-plane clipping.
+data ExactClipDisposition
+  = ExactClipFullDimensional !ExactRetainedPolygon
+  | ExactClipLowerDimensional !(NonEmpty ExactPoint)
+  | ExactClipEmpty
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Typed obstructions from an affine intersection that should be unique or
+-- from an impossible orientation reversal during convex descent.
+data ExactClipError
+  = ExactClipIntersection !ExactIntersectionError
+  | ExactClipOrientationReversed !ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Structural work and exact-rational width observations for one angular
+-- half-plane descent. Submitted half-planes exclude the domain; active
+-- boundaries include domain edges admitted to the deque after
+-- equal-direction coalescence; zero records a single-plane domain rejection
+-- before deque construction.
+-- Compatibility checks count deque endpoint predicates; intersections count
+-- successful affine-line intersections actually evaluated. Widths describe
+-- observed reduced values, not bounds.
+data ExactClipReceipt = ExactClipReceipt
+  { exactClipSubmittedHalfPlanes :: !Int
+  , exactClipActiveBoundaries :: !Int
+  , exactClipBoundaryCompatibilityChecks :: !Int
+  , exactClipExactIntersections :: !Int
+  , exactClipInputCoordinateBits :: !Int
+  , exactClipMaximumAffineCoefficientBits :: !Int
+  , exactClipPeakIntermediateCoordinateBits :: !Int
+  , exactClipFinalCoordinateBits :: !Int
+  , exactClipFinalDenominatorBits :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+instance Semigroup ExactClipReceipt where
+  left <> right =
+    ExactClipReceipt
+      { exactClipSubmittedHalfPlanes =
+          exactClipSubmittedHalfPlanes left + exactClipSubmittedHalfPlanes right
+      , exactClipActiveBoundaries =
+          exactClipActiveBoundaries left + exactClipActiveBoundaries right
+      , exactClipBoundaryCompatibilityChecks =
+          exactClipBoundaryCompatibilityChecks left
+            + exactClipBoundaryCompatibilityChecks right
+      , exactClipExactIntersections =
+          exactClipExactIntersections left + exactClipExactIntersections right
+      , exactClipInputCoordinateBits =
+          max (exactClipInputCoordinateBits left) (exactClipInputCoordinateBits right)
+      , exactClipMaximumAffineCoefficientBits =
+          max
+            (exactClipMaximumAffineCoefficientBits left)
+            (exactClipMaximumAffineCoefficientBits right)
+      , exactClipPeakIntermediateCoordinateBits =
+          max
+            (exactClipPeakIntermediateCoordinateBits left)
+            (exactClipPeakIntermediateCoordinateBits right)
+      , exactClipFinalCoordinateBits =
+          max (exactClipFinalCoordinateBits left) (exactClipFinalCoordinateBits right)
+      , exactClipFinalDenominatorBits =
+          max (exactClipFinalDenominatorBits left) (exactClipFinalDenominatorBits right)
+      }
+
+-- | No clipping work. This is not a Monoid identity: public signed width
+-- fields admit negative values, for which @max 0@ is not an identity.
+emptyExactClipReceipt :: ExactClipReceipt
+emptyExactClipReceipt = ExactClipReceipt 0 0 0 0 0 0 0 0 0
+
+-- | A strict exact segment whose endpoints are distinct.
+data ExactSegment = ExactSegment !ExactPoint !ExactPoint
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactSegment where
+  rnf (ExactSegment start end) = rnf start `seq` rnf end
+  {-# INLINE rnf #-}
+
+-- | An exact half-line with an admitted nonzero direction.
+data ExactRay = ExactRay !ExactPoint !ExactVector
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactRay where
+  rnf (ExactRay origin direction) = rnf origin `seq` rnf direction
+  {-# INLINE rnf #-}
+
+-- | Witness-bearing refusals from exact segment construction.
+data ExactGeometryError
+  = ExactSegmentEndpointsCoincide !ExactPoint
+  | ExactRayZeroDirection !ExactPoint
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Admit a half-line, rejecting the only direction that cannot carry one.
+exactRay
+  :: ExactPoint
+  -> ExactVector
+  -> Either ExactGeometryError ExactRay
+exactRay originPoint direction@(ExactVector directionX directionY)
+  | exactRationalIsZero directionX && exactRationalIsZero directionY =
+      Left (ExactRayZeroDirection originPoint)
+  | otherwise = Right (ExactRay originPoint direction)
+
+-- | Finite endpoint of an exact ray.
+exactRayOrigin :: ExactRay -> ExactPoint
+exactRayOrigin (ExactRay originPoint _) = originPoint
+{-# INLINE exactRayOrigin #-}
+
+-- | Nonzero direction of an exact ray.
+exactRayDirection :: ExactRay -> ExactVector
+exactRayDirection (ExactRay _ direction) = direction
+{-# INLINE exactRayDirection #-}
+
+-- | Witness-bearing refusals from exact line intersection.
+data ExactIntersectionError
+  = ExactIntersectionAbsent !SegmentRelation
+  | ExactIntersectionNonUnique !SegmentRelation
+  | ExactIntersectionParallelOrDegenerate !ExactRational
+  | ExactIntersectionArithmetic !ExactArithmeticError
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Construct an exact point from two exact coordinates.
+exactPoint :: ExactRational -> ExactRational -> ExactPoint
+exactPoint = ExactPoint
+{-# INLINE exactPoint #-}
+
+-- | Read both exact point coordinates.
+exactPointCoordinates :: ExactPoint -> (ExactRational, ExactRational)
+exactPointCoordinates (ExactPoint x y) = (x, y)
+{-# INLINE exactPointCoordinates #-}
+
+-- | Determinant of two points regarded as vectors from the Cartesian origin.
+exactPointCross :: ExactPoint -> ExactPoint -> ExactRational
+exactPointCross (ExactPoint ax ay) (ExactPoint bx by) = ax * by - ay * bx
+{-# INLINE exactPointCross #-}
+
+-- | Admit an affine line, refusing precisely the zero normal. Coefficients
+-- are retained verbatim because their source identity, rather than a chosen
+-- scalar normalization, is the point of this carrier.
+exactAffineLine
+  :: ExactRational
+  -> ExactRational
+  -> ExactRational
+  -> Either ExactHalfPlaneError ExactAffineLine
+exactAffineLine coefficientX coefficientY constant
+  | exactRationalIsZero coefficientX
+      && exactRationalIsZero coefficientY =
+      Left
+        ( ExactAffineLineZeroNormal
+            coefficientX
+            coefficientY
+            constant
+        )
+  | otherwise =
+      Right (ExactAffineLine coefficientX coefficientY constant)
+
+-- | Read the retained coefficients @(a,b,c)@ of @a*x+b*y+c=0@.
+exactAffineLineCoefficients
+  :: ExactAffineLine
+  -> (ExactRational, ExactRational, ExactRational)
+exactAffineLineCoefficients (ExactAffineLine coefficientX coefficientY constant) =
+  (coefficientX, coefficientY, constant)
+{-# INLINE exactAffineLineCoefficients #-}
+
+-- | Reverse the oriented normal without changing the geometric line.
+-- Admission already proves the normal nonzero, so negation is total.
+oppositeExactAffineLine :: ExactAffineLine -> ExactAffineLine
+oppositeExactAffineLine (ExactAffineLine coefficientX coefficientY constant) =
+  ExactAffineLine (negate coefficientX) (negate coefficientY) (negate constant)
+{-# INLINE oppositeExactAffineLine #-}
+
+-- | Intersect two admitted affine lines directly from their retained source
+-- coefficients. A parallel pair retains the exact determinant witness.
+exactAffineLineIntersection
+  :: ExactAffineLine
+  -> ExactAffineLine
+  -> Either ExactIntersectionError ExactPoint
+exactAffineLineIntersection
+  (ExactAffineLine firstX firstY firstConstant)
+  (ExactAffineLine secondX secondY secondConstant) =
+    -- Descend in homogeneous integer coordinates and normalize each published
+    -- coordinate once. Generic Ratio arithmetic would normalize every product
+    -- in Cramer's rule even though those intermediate rationals are invisible.
+    let (firstXNumerator, firstXDenominator) = exactRationalParts firstX
+        (firstYNumerator, firstYDenominator) = exactRationalParts firstY
+        (firstConstantNumerator, firstConstantDenominator) =
+          exactRationalParts firstConstant
+        (secondXNumerator, secondXDenominator) = exactRationalParts secondX
+        (secondYNumerator, secondYDenominator) = exactRationalParts secondY
+        (secondConstantNumerator, secondConstantDenominator) =
+          exactRationalParts secondConstant
+        determinantNumerator =
+          firstXNumerator
+            * secondYNumerator
+            * secondXDenominator
+            * firstYDenominator
+            - secondXNumerator
+              * firstYNumerator
+              * firstXDenominator
+              * secondYDenominator
+        xNumerator =
+          ( firstYNumerator
+              * secondConstantNumerator
+              * secondYDenominator
+              * firstConstantDenominator
+              - secondYNumerator
+                * firstConstantNumerator
+                * firstYDenominator
+                * secondConstantDenominator
+          )
+            * firstXDenominator
+            * secondXDenominator
+        yNumerator =
+          ( firstConstantNumerator
+              * secondXNumerator
+              * secondConstantDenominator
+              * firstXDenominator
+              - secondConstantNumerator
+                * firstXNumerator
+                * firstConstantDenominator
+                * secondXDenominator
+          )
+            * firstYDenominator
+            * secondYDenominator
+        coordinateDenominator =
+          determinantNumerator
+            * firstConstantDenominator
+            * secondConstantDenominator
+     in if determinantNumerator == 0
+          then Left (ExactIntersectionParallelOrDegenerate 0)
+          else do
+            x <- admitCoordinate xNumerator coordinateDenominator
+            y <- admitCoordinate yNumerator coordinateDenominator
+            pure (ExactPoint x y)
+ where
+  admitCoordinate numerator denominator =
+    case exactRational numerator denominator of
+      Left arithmeticError ->
+        Left (ExactIntersectionArithmetic arithmeticError)
+      Right coordinate -> Right coordinate
+
+exactRationalParts :: ExactRational -> (Integer, Integer)
+exactRationalParts value =
+  (exactRationalNumerator value, exactRationalDenominator value)
+{-# INLINE exactRationalParts #-}
+
+-- | Orient an admitted affine line so its nonnegative side is retained.
+exactClosedHalfPlane :: ExactAffineLine -> ExactClosedHalfPlane
+exactClosedHalfPlane = ExactClosedHalfPlane
+{-# INLINE exactClosedHalfPlane #-}
+
+-- | Construct the closed half-plane to the left of a directed edge.
+exactClosedHalfPlaneFromDirectedEdge
+  :: ExactPoint
+  -> ExactPoint
+  -> Either ExactHalfPlaneError ExactClosedHalfPlane
+exactClosedHalfPlaneFromDirectedEdge
+  from to =
+  case directedEdgeLine from to of
+    ExactAffineLine coefficientX coefficientY constant ->
+      exactClosedHalfPlane <$> exactAffineLine coefficientX coefficientY constant
+
+-- The checked edge entrance and admitted convex descent share the exact same
+-- coefficients. Only their evidence for a nonzero normal differs.
+directedEdgeLine :: ExactPoint -> ExactPoint -> ExactAffineLine
+directedEdgeLine (ExactPoint fromX fromY) (ExactPoint toX toY) =
+  ExactAffineLine
+      (fromY - toY)
+      (toX - fromX)
+      (toY * fromX - toX * fromY)
+
+-- | Read the original affine boundary retained by a closed half-plane.
+exactClosedHalfPlaneLine :: ExactClosedHalfPlane -> ExactAffineLine
+exactClosedHalfPlaneLine (ExactClosedHalfPlane line) = line
+{-# INLINE exactClosedHalfPlaneLine #-}
+
+-- | Classify a point against a closed half-plane. 'GT' is strict interior,
+-- 'EQ' lies on the boundary, and 'LT' is exterior.
+classifyExactPoint :: ExactClosedHalfPlane -> ExactPoint -> Ordering
+classifyExactPoint
+  (ExactClosedHalfPlane (ExactAffineLine coefficientX coefficientY constant))
+  (ExactPoint x y) =
+  -- The common denominator is strictly positive, so its unnormalized integer
+  -- numerator is already the authoritative sign witness.
+  let (coefficientXNumerator, coefficientXDenominator) =
+        exactRationalParts coefficientX
+      (coefficientYNumerator, coefficientYDenominator) =
+        exactRationalParts coefficientY
+      (constantNumerator, constantDenominator) = exactRationalParts constant
+      (xNumerator, xDenominator) = exactRationalParts x
+      (yNumerator, yDenominator) = exactRationalParts y
+      evaluationNumerator =
+        coefficientXNumerator
+          * xNumerator
+          * coefficientYDenominator
+          * yDenominator
+          * constantDenominator
+          + coefficientYNumerator
+            * yNumerator
+            * coefficientXDenominator
+            * xDenominator
+            * constantDenominator
+          + constantNumerator
+            * coefficientXDenominator
+            * xDenominator
+            * coefficientYDenominator
+            * yDenominator
+   in compare evaluationNumerator 0
+{-# INLINE classifyExactPoint #-}
+
+-- | Admit strict counter-clockwise convex points and retain the original
+-- supporting line of every incoming edge.
+exactRetainedPolygon
+  :: NonEmpty ExactPoint
+  -> Either ExactHalfPlaneError ExactRetainedPolygon
+exactRetainedPolygon points
+  | NonEmpty.length points < 3 =
+      Left (ExactRetainedPolygonTooFewVertices (NonEmpty.length points))
+  | otherwise =
+      case firstNonCounterClockwiseTurn exactOrient2d points of
+        Just (index, turn) ->
+          Left (ExactRetainedPolygonNonConvexTurn index turn)
+        Nothing -> do
+          validateSimpleRetainedCycle points
+          pure (retainAdmittedConvexCycle points)
+
+-- | Private descent for a simple, strictly counter-clockwise convex cycle.
+-- Raw points reach it only after checked admission above; the convex owner
+-- supplies its already admitted cycle, possibly translated. Keeping each
+-- destination with its incoming line preserves the checked entrance's rotation.
+retainAdmittedConvexCycle :: NonEmpty ExactPoint -> ExactRetainedPolygon
+retainAdmittedConvexCycle = ExactRetainedPolygon
+  . fmap (\(from, to) -> ExactRetainedVertex to (directedEdgeLine from to))
+  . cyclePairsNonEmpty
+
+validateSimpleRetainedCycle
+  :: NonEmpty ExactPoint
+  -> Either ExactHalfPlaneError ()
+validateSimpleRetainedCycle points =
+  case
+    [ (leftIndex, rightIndex, relation)
+    | ( (leftIndex, (leftFrom, leftTo))
+        , (rightIndex, (rightFrom, rightTo))
+        ) <- unorderedPairs indexedEdges
+    , let relation =
+            exactSegmentRelation
+              leftFrom
+              leftTo
+              rightFrom
+              rightTo
+    , not
+        ( admitsSimpleCycleEdgeRelation
+            segmentCount
+            leftIndex
+            rightIndex
+            relation
+        )
+    ] of
+    (leftIndex, rightIndex, relation) : _ ->
+      Left
+        ( ExactRetainedPolygonSelfRelation
+            leftIndex
+            rightIndex
+            relation
+        )
+    [] -> Right ()
+ where
+  indexedEdges = zip [0 :: Int ..] (NonEmpty.toList (cyclePairsNonEmpty points))
+  segmentCount = NonEmpty.length points
+
+-- | Project the retained-edge carrier to its authoritative point cycle.
+exactRetainedPolygonPoints :: ExactRetainedPolygon -> NonEmpty ExactPoint
+exactRetainedPolygonPoints (ExactRetainedPolygon vertices) =
+  fmap retainedVertexPoint vertices
+
+-- | Intersect a retained convex polygon with exact closed half-planes.
+-- Boundaries descend in exact angular order through one immutable deque;
+-- every published edge retains one submitted source line, and derived
+-- endpoints never become line coefficients.
+exactClipRetainedPolygon
+  :: ExactRetainedPolygon
+  -> [ExactClosedHalfPlane]
+  -> Either ExactClipError (ExactClipDisposition, ExactClipReceipt)
+exactClipRetainedPolygon polygon halfPlanes = do
+  let inputPoints = exactRetainedPolygonPoints polygon
+      inputBits = maximumPointBitWidth inputPoints
+      domainHalfPlanes = retainedPolygonHalfPlanes polygon
+      allHalfPlanes = domainHalfPlanes <> halfPlanes
+      initialReceipt =
+        ExactClipReceipt
+          { exactClipSubmittedHalfPlanes = length halfPlanes
+          , exactClipActiveBoundaries =
+              if null halfPlanes then NonEmpty.length inputPoints else 0
+          , exactClipBoundaryCompatibilityChecks = 0
+          , exactClipExactIntersections = 0
+          , exactClipInputCoordinateBits = inputBits
+          , exactClipMaximumAffineCoefficientBits =
+              List.foldl'
+                (\bits -> max bits . affineLineBitWidth . exactClosedHalfPlaneLine)
+                0
+                allHalfPlanes
+          , exactClipPeakIntermediateCoordinateBits = inputBits
+          , exactClipFinalCoordinateBits = inputBits
+          , exactClipFinalDenominatorBits =
+              maximumPointDenominatorBitWidth inputPoints
+          }
+  if null halfPlanes
+    then pure (ExactClipFullDimensional polygon, initialReceipt)
+    else case List.find (`excludesExactDomain` inputPoints) halfPlanes of
+      Just _ ->
+        pure
+          ( ExactClipEmpty
+          , initialReceipt
+              { exactClipFinalCoordinateBits = 0
+              , exactClipFinalDenominatorBits = 0
+              }
+          )
+      Nothing -> do
+        let orderedBoundaries =
+              coalesceAngularBoundaries (fmap angularBoundary allHalfPlanes)
+            descentReceipt =
+              initialReceipt
+                { exactClipActiveBoundaries = length orderedBoundaries
+                }
+        (openDeque, descendedReceipt) <-
+          foldM insertAngularBoundary (Sequence.empty, descentReceipt) orderedBoundaries
+        (closedDeque, accumulatedReceipt) <- closeAngularBoundaryDeque openDeque descendedReceipt
+        (finalState, finalizedReceipt) <-
+          angularBoundaryDequeState orderedBoundaries closedDeque accumulatedReceipt
+        let disposition = exactClipStateDisposition finalState
+            finalPoints = exactClipDispositionPoints disposition
+            finalReceipt =
+              finalizedReceipt
+                { exactClipFinalCoordinateBits = maybe 0 maximumPointBitWidth finalPoints
+                , exactClipFinalDenominatorBits =
+                    maybe 0 maximumPointDenominatorBitWidth finalPoints
+                }
+        pure (disposition, finalReceipt)
+
+-- | A linear functional attains its maximum over a convex polygon at a
+-- vertex.  One half-plane whose closed side contains no domain vertex is
+-- therefore an exact emptiness certificate; recognizing it before angular
+-- descent avoids sorting a section that has no global point to glue.
+excludesExactDomain
+  :: ExactClosedHalfPlane
+  -> NonEmpty ExactPoint
+  -> Bool
+excludesExactDomain halfPlane =
+  all ((== LT) . classifyExactPoint halfPlane)
+
+-- | One original source boundary together with its exact counter-clockwise
+-- direction.  Equal-direction gluing retains one of these values verbatim.
+data ExactAngularBoundary = ExactAngularBoundary
+  { angularBoundaryLine :: !ExactAffineLine
+  , angularBoundaryDirection :: !ExactVector
+  }
+
+angularBoundary :: ExactClosedHalfPlane -> ExactAngularBoundary
+angularBoundary halfPlane =
+  let line@(ExactAffineLine coefficientX coefficientY _) =
+        exactClosedHalfPlaneLine halfPlane
+   in ExactAngularBoundary
+        { angularBoundaryLine = line
+        , angularBoundaryDirection = ExactVector coefficientY (negate coefficientX)
+        }
+
+angularBoundaryHalfPlane :: ExactAngularBoundary -> ExactClosedHalfPlane
+angularBoundaryHalfPlane = exactClosedHalfPlane . angularBoundaryLine
+{-# INLINE angularBoundaryHalfPlane #-}
+
+retainedPolygonHalfPlanes :: ExactRetainedPolygon -> [ExactClosedHalfPlane]
+retainedPolygonHalfPlanes (ExactRetainedPolygon vertices) =
+  fmap
+    (exactClosedHalfPlane . retainedVertexIncomingLine)
+    (NonEmpty.toList vertices)
+
+-- | Descent over one angular stalk.  A stricter parallel boundary replaces a
+-- weaker one; coincident boundaries retain the least original source line so
+-- input permutations cannot alter the glued carrier.
+coalesceAngularBoundaries :: [ExactAngularBoundary] -> [ExactAngularBoundary]
+coalesceAngularBoundaries =
+  fmap strongestAngularBoundary
+    . mapMaybe NonEmpty.nonEmpty
+    . List.groupBy sameBoundaryDirection
+    . List.sortBy compareAngularBoundaries
+ where
+  sameBoundaryDirection left right =
+    compareExactVectorAngle
+      (angularBoundaryDirection left)
+      (angularBoundaryDirection right)
+      == EQ
+
+compareAngularBoundaries :: ExactAngularBoundary -> ExactAngularBoundary -> Ordering
+compareAngularBoundaries left right =
+  case
+    compareExactVectorAngle
+      (angularBoundaryDirection left)
+      (angularBoundaryDirection right) of
+    EQ -> compare (angularBoundaryLine left) (angularBoundaryLine right)
+    ordering -> ordering
+
+strongestAngularBoundary :: NonEmpty ExactAngularBoundary -> ExactAngularBoundary
+strongestAngularBoundary (initial :| remaining) =
+  List.foldl' chooseStrongerAngularBoundary initial remaining
+
+chooseStrongerAngularBoundary
+  :: ExactAngularBoundary
+  -> ExactAngularBoundary
+  -> ExactAngularBoundary
+chooseStrongerAngularBoundary selected candidate =
+  case compareParallelBoundaryStrength selected candidate of
+    LT -> candidate
+    GT -> selected
+    EQ ->
+      if angularBoundaryLine candidate < angularBoundaryLine selected
+        then candidate
+        else selected
+
+-- | Compare same-direction boundaries.  'LT' means the right boundary is
+-- stricter, 'GT' means the left is stricter, and 'EQ' means the closed
+-- half-planes coincide.  Cross multiplication avoids inventing a normalized
+-- replacement line.
+compareParallelBoundaryStrength
+  :: ExactAngularBoundary
+  -> ExactAngularBoundary
+  -> Ordering
+compareParallelBoundaryStrength left right =
+  let ExactAffineLine leftX leftY leftConstant = angularBoundaryLine left
+      ExactAffineLine rightX rightY rightConstant = angularBoundaryLine right
+      compareScaled leftScale rightScale scaleSign =
+        let raw = compare (rightConstant * leftScale) (rightScale * leftConstant)
+         in if exactSignum scaleSign == LT then reverseOrdering raw else raw
+   in if exactRationalIsZero leftX
+        then compareScaled leftY rightY leftY
+        else compareScaled leftX rightX leftX
+
+reverseOrdering :: Ordering -> Ordering
+reverseOrdering LT = GT
+reverseOrdering EQ = EQ
+reverseOrdering GT = LT
+
+-- | One active source boundary and its cached intersection with the preceding
+-- deque boundary.  Every adjacency point is derived once and reused by both
+-- compatibility trims and final publication.
+data ExactAngularBoundaryNode = ExactAngularBoundaryNode
+  { angularNodeBoundary :: !ExactAngularBoundary
+  , angularNodePreviousIntersection :: !(Maybe ExactPoint)
+  }
+
+type ExactAngularBoundaryDeque = Seq ExactAngularBoundaryNode
+
+insertAngularBoundary
+  :: (ExactAngularBoundaryDeque, ExactClipReceipt)
+  -> ExactAngularBoundary
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+insertAngularBoundary (boundaries, receipt) incoming = do
+  (backTrimmed, backReceipt) <- trimAngularBack incoming boundaries receipt
+  (frontTrimmed, frontReceipt) <- trimAngularFront incoming backTrimmed backReceipt
+  appendAngularBoundary incoming frontTrimmed frontReceipt
+
+appendAngularBoundary
+  :: ExactAngularBoundary
+  -> ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+appendAngularBoundary incoming boundaries receipt =
+  case Sequence.viewr boundaries of
+    EmptyR ->
+      Right
+        ( Sequence.singleton (ExactAngularBoundaryNode incoming Nothing)
+        , receipt
+        )
+    _ :> finalNode -> do
+      (intersection, observedReceipt) <-
+        observeAdjacentIntersection
+          (angularNodeBoundary finalNode)
+          incoming
+          receipt
+      pure
+        ( boundaries
+            |> ExactAngularBoundaryNode incoming intersection
+        , observedReceipt
+        )
+
+trimAngularBack
+  :: ExactAngularBoundary
+  -> ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+trimAngularBack incoming boundaries receipt =
+  case Sequence.viewr boundaries of
+    EmptyR -> Right (boundaries, receipt)
+    remaining :> finalNode ->
+      case Sequence.viewr remaining of
+        EmptyR -> Right (boundaries, receipt)
+        _ :> _ ->
+          case angularNodePreviousIntersection finalNode of
+            Nothing -> Right (boundaries, receipt)
+            Just point ->
+              let checkedReceipt = observeBoundaryCompatibility receipt
+               in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
+                    then trimAngularBack incoming remaining checkedReceipt
+                    else Right (boundaries, checkedReceipt)
+
+trimAngularFront
+  :: ExactAngularBoundary
+  -> ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+trimAngularFront incoming boundaries receipt =
+  case Sequence.viewl boundaries of
+    EmptyL -> Right (boundaries, receipt)
+    _ :< remaining ->
+      case Sequence.viewl remaining of
+        EmptyL -> Right (boundaries, receipt)
+        secondNode :< suffix ->
+          case angularNodePreviousIntersection secondNode of
+            Nothing -> Right (boundaries, receipt)
+            Just point ->
+              let checkedReceipt = observeBoundaryCompatibility receipt
+               in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
+                    then
+                      trimAngularFront
+                        incoming
+                        ( secondNode
+                            { angularNodePreviousIntersection = Nothing
+                            }
+                            Sequence.<| suffix
+                        )
+                        checkedReceipt
+                    else Right (boundaries, checkedReceipt)
+
+closeAngularBoundaryDeque
+  :: ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+closeAngularBoundaryDeque boundaries receipt = do
+  (backClosed, backReceipt) <- closeAngularBack boundaries receipt
+  (frontClosed, frontReceipt) <- closeAngularFront backClosed backReceipt
+  if Sequence.length frontClosed == Sequence.length boundaries
+    then Right (frontClosed, frontReceipt)
+    else closeAngularBoundaryDeque frontClosed frontReceipt
+
+closeAngularBack
+  :: ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+closeAngularBack boundaries receipt =
+  case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
+    (firstNode :< _, remaining :> finalNode) ->
+      case Sequence.viewr remaining of
+        _ :> _ ->
+          case angularNodePreviousIntersection finalNode of
+            Nothing -> Right (boundaries, receipt)
+            Just point ->
+              let checkedReceipt = observeBoundaryCompatibility receipt
+               in if
+                    classifyExactPoint
+                      (angularBoundaryHalfPlane (angularNodeBoundary firstNode))
+                      point
+                      == LT
+                    then closeAngularBack remaining checkedReceipt
+                    else Right (boundaries, checkedReceipt)
+        EmptyR -> Right (boundaries, receipt)
+    _ -> Right (boundaries, receipt)
+
+closeAngularFront
+  :: ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
+closeAngularFront boundaries receipt =
+  case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
+    (_ :< remaining, _ :> finalNode) ->
+      case Sequence.viewl remaining of
+        secondNode :< suffix ->
+          case angularNodePreviousIntersection secondNode of
+            Nothing -> Right (boundaries, receipt)
+            Just point ->
+              let checkedReceipt = observeBoundaryCompatibility receipt
+               in if
+                    classifyExactPoint
+                      (angularBoundaryHalfPlane (angularNodeBoundary finalNode))
+                      point
+                      == LT
+                    then
+                      closeAngularFront
+                        ( secondNode
+                            { angularNodePreviousIntersection = Nothing
+                            }
+                            Sequence.<| suffix
+                        )
+                        checkedReceipt
+                    else Right (boundaries, checkedReceipt)
+        EmptyL -> Right (boundaries, receipt)
+    _ -> Right (boundaries, receipt)
+
+observeAdjacentIntersection
+  :: ExactAngularBoundary
+  -> ExactAngularBoundary
+  -> ExactClipReceipt
+  -> Either ExactClipError (Maybe ExactPoint, ExactClipReceipt)
+observeAdjacentIntersection left right receipt =
+  let leftLine = angularBoundaryLine left
+      rightLine = angularBoundaryLine right
+   in case exactAffineLineIntersection leftLine rightLine of
+        Left (ExactIntersectionParallelOrDegenerate _) ->
+          Right (Nothing, receipt)
+        Left obstruction -> Left (ExactClipIntersection obstruction)
+        Right point ->
+          pure
+            ( Just point
+            , receipt
+                { exactClipExactIntersections =
+                    exactClipExactIntersections receipt + 1
+                , exactClipPeakIntermediateCoordinateBits =
+                    max
+                      (exactClipPeakIntermediateCoordinateBits receipt)
+                      (exactPointBitWidth point)
+                }
+            )
+
+observeBoundaryCompatibility :: ExactClipReceipt -> ExactClipReceipt
+observeBoundaryCompatibility receipt =
+  receipt
+    { exactClipBoundaryCompatibilityChecks =
+        exactClipBoundaryCompatibilityChecks receipt + 1
+    }
+
+angularBoundaryDequeState
+  :: [ExactAngularBoundary]
+  -> ExactAngularBoundaryDeque
+  -> ExactClipReceipt
+  -> Either ExactClipError (ExactClipState, ExactClipReceipt)
+angularBoundaryDequeState allBoundaries boundaries receipt = do
+  let nodeList = foldr (:) [] boundaries
+      linearVertices =
+        mapMaybe
+          retainedLinearVertex
+          (zip nodeList (drop 1 nodeList))
+  (closingVertices, finalizedReceipt) <-
+    case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
+      (firstNode :< _, _ :> finalNode) -> do
+        (intersection, observedReceipt) <-
+          observeAdjacentIntersection
+            (angularNodeBoundary finalNode)
+            (angularNodeBoundary firstNode)
+            receipt
+        pure
+          ( maybe
+              []
+              (\point ->
+                 [ ExactRetainedVertex
+                     point
+                     (angularBoundaryLine (angularNodeBoundary finalNode))
+                 ])
+              intersection
+          , observedReceipt
+          )
+      _ -> Right ([], receipt)
+  state <-
+    retainedVerticesToClipState
+      (normalizeRetainedVertices (linearVertices <> closingVertices))
+  pure (validateLowerDimensionalState allBoundaries state, finalizedReceipt)
+ where
+  retainedLinearVertex (node, successor) =
+    fmap
+      (\point ->
+         ExactRetainedVertex
+           point
+           (angularBoundaryLine (angularNodeBoundary node)))
+      (angularNodePreviousIntersection successor)
+
+validateLowerDimensionalState
+  :: [ExactAngularBoundary]
+  -> ExactClipState
+  -> ExactClipState
+validateLowerDimensionalState boundaries state =
+  case state of
+    ExactClipStateSegment from to ->
+      if all (containsBoth from to) boundaries
+        then state
+        else ExactClipStateEmpty
+    ExactClipStatePoint point ->
+      if all
+          (\boundary ->
+             classifyExactPoint (angularBoundaryHalfPlane boundary) point /= LT)
+          boundaries
+        then state
+        else ExactClipStateEmpty
+    other -> other
+ where
+  containsBoth from to boundary =
+    let halfPlane = angularBoundaryHalfPlane boundary
+     in classifyExactPoint halfPlane from /= LT
+          && classifyExactPoint halfPlane to /= LT
+
+data ExactClipState
+  = ExactClipStatePolygon !ExactRetainedPolygon
+  | ExactClipStateSegment !ExactPoint !ExactPoint
+  | ExactClipStatePoint !ExactPoint
+  | ExactClipStateEmpty
+exactSegmentClipState
+  :: ExactPoint
+  -> ExactPoint
+  -> ExactClipState
+exactSegmentClipState from to
+  | from == to = ExactClipStatePoint from
+  | otherwise = ExactClipStateSegment from to
+
+retainedVerticesToClipState
+  :: [ExactRetainedVertex]
+  -> Either ExactClipError ExactClipState
+retainedVerticesToClipState [] = Right ExactClipStateEmpty
+retainedVerticesToClipState [vertex] =
+  Right (ExactClipStatePoint (retainedVertexPoint vertex))
+retainedVerticesToClipState [fromVertex, toVertex] =
+  Right
+    ( exactSegmentClipState
+        (retainedVertexPoint fromVertex)
+        (retainedVertexPoint toVertex)
+    )
+retainedVerticesToClipState (firstVertex : secondVertex : remaining) =
+  let retained = firstVertex :| (secondVertex : remaining)
+      twiceArea = retainedVerticesTwiceArea retained
+   in case exactSignum twiceArea of
+        GT -> Right (ExactClipStatePolygon (ExactRetainedPolygon retained))
+        LT -> Left (ExactClipOrientationReversed twiceArea)
+        EQ -> Right (lowerDimensionalClipState retained)
+
+lowerDimensionalClipState
+  :: NonEmpty ExactRetainedVertex
+  -> ExactClipState
+lowerDimensionalClipState vertices =
+  let points = fmap retainedVertexPoint vertices
+      initialPoint :| remainingPoints = points
+      leastPoint = List.foldl' min initialPoint remainingPoints
+      greatestPoint = List.foldl' max initialPoint remainingPoints
+   in if leastPoint == greatestPoint
+        then ExactClipStatePoint leastPoint
+        else ExactClipStateSegment leastPoint greatestPoint
+
+retainedVerticesTwiceArea
+  :: NonEmpty ExactRetainedVertex
+  -> ExactRational
+retainedVerticesTwiceArea =
+  List.foldl'
+    (\area (from, to) ->
+       area
+         + exactPointCross
+           (retainedVertexPoint from)
+           (retainedVertexPoint to))
+    0
+    . cyclePairsNonEmpty
+
+normalizeRetainedVertices
+  :: [ExactRetainedVertex]
+  -> [ExactRetainedVertex]
+normalizeRetainedVertices = stripClosingDuplicate . deduplicateAdjacent
+ where
+  deduplicateAdjacent [] = []
+  deduplicateAdjacent (firstVertex : remaining) =
+    firstVertex : deduplicateAfter firstVertex remaining
+
+  deduplicateAfter _ [] = []
+  deduplicateAfter previous (candidate : remaining)
+    | retainedVertexPoint previous == retainedVertexPoint candidate =
+        deduplicateAfter previous remaining
+    | otherwise = candidate : deduplicateAfter candidate remaining
+
+  stripClosingDuplicate vertices@(firstVertex : remainingVertices) =
+    case reverse remainingVertices of
+      finalVertex : reversedInterior
+        | retainedVertexPoint finalVertex == retainedVertexPoint firstVertex ->
+            finalVertex : reverse reversedInterior
+      _ -> vertices
+  stripClosingDuplicate [] = []
+
+exactClipStateDisposition :: ExactClipState -> ExactClipDisposition
+exactClipStateDisposition state =
+  case state of
+    ExactClipStatePolygon polygon -> ExactClipFullDimensional polygon
+    ExactClipStateSegment from to ->
+      ExactClipLowerDimensional (from :| [to])
+    ExactClipStatePoint point -> ExactClipLowerDimensional (point :| [])
+    ExactClipStateEmpty -> ExactClipEmpty
+
+exactClipDispositionPoints
+  :: ExactClipDisposition
+  -> Maybe (NonEmpty ExactPoint)
+exactClipDispositionPoints disposition =
+  case disposition of
+    ExactClipFullDimensional polygon ->
+      Just (exactRetainedPolygonPoints polygon)
+    ExactClipLowerDimensional points -> Just points
+    ExactClipEmpty -> Nothing
+
+retainedVertexPoint :: ExactRetainedVertex -> ExactPoint
+retainedVertexPoint (ExactRetainedVertex point _) = point
+{-# INLINE retainedVertexPoint #-}
+
+retainedVertexIncomingLine :: ExactRetainedVertex -> ExactAffineLine
+retainedVertexIncomingLine (ExactRetainedVertex _ line) = line
+{-# INLINE retainedVertexIncomingLine #-}
+
+affineLineBitWidth :: ExactAffineLine -> Int
+affineLineBitWidth (ExactAffineLine coefficientX coefficientY constant) =
+  max
+    (exactRationalBitWidth coefficientX)
+    (max (exactRationalBitWidth coefficientY) (exactRationalBitWidth constant))
+
+maximumPointBitWidth :: NonEmpty ExactPoint -> Int
+maximumPointBitWidth =
+  List.foldl'
+    (\maximumBits (ExactPoint x y) ->
+       max maximumBits (max (exactRationalBitWidth x) (exactRationalBitWidth y)))
+    0
+
+-- | Maximum reduced numerator-or-denominator width of either coordinate.
+exactPointBitWidth :: ExactPoint -> Int
+exactPointBitWidth (ExactPoint x y) =
+  max (exactRationalBitWidth x) (exactRationalBitWidth y)
+{-# INLINE exactPointBitWidth #-}
+
+maximumPointDenominatorBitWidth :: NonEmpty ExactPoint -> Int
+maximumPointDenominatorBitWidth =
+  List.foldl'
+    (\maximumBits (ExactPoint x y) ->
+       max
+         maximumBits
+         ( max
+             (exactRationalDenominatorBitWidth x)
+             (exactRationalDenominatorBitWidth y)
+         ))
+    0
+
+-- | Construct an exact segment, refusing coincident endpoints with their
+-- shared point as the witness.
+exactSegment
+  :: ExactPoint
+  -> ExactPoint
+  -> Either ExactGeometryError ExactSegment
+exactSegment from to
+  | from == to = Left (ExactSegmentEndpointsCoincide from)
+  | otherwise = Right (ExactSegment from to)
+
+-- | Read both distinct exact segment endpoints.
+exactSegmentEndpoints :: ExactSegment -> (ExactPoint, ExactPoint)
+exactSegmentEndpoints (ExactSegment from to) = (from, to)
+{-# INLINE exactSegmentEndpoints #-}
+
+-- | Validate and exactly embed a raw binary64 point.
+exactPointFromPoint :: Point -> Either PointValidationError ExactPoint
+exactPointFromPoint = fmap exactPointFromQueryPoint . mkQueryPoint
+
+-- | Exactly embed an already-admitted query point without repeating
+-- coordinate validation.
+exactPointFromQueryPoint :: QueryPoint -> ExactPoint
+exactPointFromQueryPoint queryPoint =
+  case queryPointValue queryPoint of
+    Point x y ->
+      ExactPoint
+        (exactRationalFromFiniteDouble x)
+        (exactRationalFromFiniteDouble y)
+{-# INLINE exactPointFromQueryPoint #-}
+
+-- | Deterministically project an exact point to a validated binary64
+-- embedding candidate. This is not a correctly-rounded nearest-double claim;
+-- callers must certify the candidate projection before relying on it.
+exactPointToEmbeddingCandidate
+  :: ExactPoint
+  -> Either PointValidationError QueryPoint
+exactPointToEmbeddingCandidate (ExactPoint x y) =
+  mkQueryPoint
+    ( Point
+        (integerRatioToDouble (exactRationalNumerator x) (exactRationalDenominator x))
+        (integerRatioToDouble (exactRationalNumerator y) (exactRationalDenominator y))
+    )
+
+-- | Exact orientation of an ordered triple. 'GT' is a positive determinant
+-- and counter-clockwise turn, 'EQ' is collinear, and 'LT' is clockwise.
+exactOrient2d :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
+exactOrient2d
+  (ExactPoint ax ay)
+  (ExactPoint bx by)
+  (ExactPoint cx cy) =
+    exactSignum
+      ((bx - ax) * (cy - ay) - (by - ay) * (cx - ax))
+{-# INLINE exactOrient2d #-}
+
+-- | Whether an exact point lies on an exact closed segment.
+exactOnClosedSegment :: ExactPoint -> ExactPoint -> ExactPoint -> Bool
+exactOnClosedSegment
+  from@(ExactPoint ax ay)
+  to@(ExactPoint bx by)
+  query@(ExactPoint qx qy) =
+    exactOrient2d from to query == EQ
+      && qx >= min ax bx
+      && qx <= max ax bx
+      && qy >= min ay by
+      && qy <= max ay by
+{-# INLINE exactOnClosedSegment #-}
+
+-- | Exact rational specialization of the one closed-segment relation policy.
+exactSegmentRelation
+  :: ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> SegmentRelation
+exactSegmentRelation =
+  segmentRelationWith (==) compare exactOrient2d exactOnClosedSegment
+{-# INLINE exactSegmentRelation #-}
+
+-- | Return the unique exact intersection of two exact segments. Disjoint and
+-- non-unique relations are refused with their relation witness; a zero line
+-- cross product and arithmetic failure retain their exact witnesses.
+exactLineIntersection
+  :: ExactSegment
+  -> ExactSegment
+  -> Either ExactIntersectionError ExactPoint
+exactLineIntersection
+  (ExactSegment a b)
+  (ExactSegment c d) =
+    case exactSegmentRelation a b c d of
+      SegmentsDisjoint -> Left (ExactIntersectionAbsent SegmentsDisjoint)
+      SegmentsDuplicate -> Left (ExactIntersectionNonUnique SegmentsDuplicate)
+      SegmentsCollinearlyOverlap ->
+        Left (ExactIntersectionNonUnique SegmentsCollinearlyOverlap)
+      SegmentsShareEndpoint -> uniqueIntersection
+      SegmentsProperlyCross -> uniqueIntersection
+      SegmentEndpointTouchesInterior -> uniqueIntersection
+ where
+  uniqueIntersection =
+    exactSupportingLineIntersection (ExactSegment a b) (ExactSegment c d)
+
+-- | Intersect the infinite supporting lines of two admitted exact segments.
+-- Unlike 'exactLineIntersection', the intersection need not lie inside either
+-- closed segment. Parallel supporting lines retain the exact zero denominator
+-- witness.
+exactSupportingLineIntersection
+  :: ExactSegment
+  -> ExactSegment
+  -> Either ExactIntersectionError ExactPoint
+exactSupportingLineIntersection
+  (ExactSegment a b)
+  (ExactSegment c d) =
+  let directionAB = exactVectorFromPoints a b
+      directionCD = exactVectorFromPoints c d
+      fromAToC = exactVectorFromPoints a c
+      denominator = exactCross directionAB directionCD
+      numerator = exactCross fromAToC directionCD
+   in case exactDivide numerator denominator of
+        Left ExactZeroDivisor ->
+          Left (ExactIntersectionParallelOrDegenerate denominator)
+        Left arithmeticError -> Left (ExactIntersectionArithmetic arithmeticError)
+        Right parameter ->
+          Right (translateExactPoint a (scaleExactVector parameter directionAB))
+
+-- | A strict exact displacement vector. Points and vectors remain distinct;
+-- all exact planar algorithms share this single vector carrier.
+data ExactVector = ExactVector !ExactRational !ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+exactVectorFromPoints :: ExactPoint -> ExactPoint -> ExactVector
+exactVectorFromPoints (ExactPoint ax ay) (ExactPoint bx by) =
+  ExactVector (bx - ax) (by - ay)
+
+addExactVectors :: ExactVector -> ExactVector -> ExactVector
+addExactVectors (ExactVector ax ay) (ExactVector bx by) =
+  ExactVector (ax + bx) (ay + by)
+
+exactCross :: ExactVector -> ExactVector -> ExactRational
+exactCross (ExactVector ax ay) (ExactVector bx by) =
+  ax * by - ay * bx
+
+-- | Counter-clockwise angular order from the positive x-axis. Collinear
+-- vectors on the same ray compare equal so convolution can merge them;
+-- callers that need a total point order may add their own radial tie-break.
+compareExactVectorAngle :: ExactVector -> ExactVector -> Ordering
+compareExactVectorAngle left right =
+  case compare (vectorHalf left) (vectorHalf right) of
+    EQ ->
+      case exactCrossSign left right of
+        GT -> LT
+        LT -> GT
+        EQ -> EQ
+    ordering -> ordering
+ where
+  vectorHalf (ExactVector x y)
+    | exactSignum y == GT = False
+    | exactSignum y == EQ && exactSignum x /= LT = False
+    | otherwise = True
+{-# INLINE compareExactVectorAngle #-}
+
+exactCrossSign :: ExactVector -> ExactVector -> Ordering
+exactCrossSign (ExactVector leftX leftY) (ExactVector rightX rightY) =
+  -- Reduced rational denominators are positive; compare the two products in a
+  -- common integer scale without constructing and normalizing their ratios.
+  let (leftXNumerator, leftXDenominator) = exactRationalParts leftX
+      (leftYNumerator, leftYDenominator) = exactRationalParts leftY
+      (rightXNumerator, rightXDenominator) = exactRationalParts rightX
+      (rightYNumerator, rightYDenominator) = exactRationalParts rightY
+   in compare
+        ( leftXNumerator
+            * rightYNumerator
+            * leftYDenominator
+            * rightXDenominator
+        )
+        ( leftYNumerator
+            * rightXNumerator
+            * leftXDenominator
+            * rightYDenominator
+        )
+{-# INLINE exactCrossSign #-}
+
+scaleExactVector :: ExactRational -> ExactVector -> ExactVector
+scaleExactVector scale (ExactVector x y) =
+  ExactVector (scale * x) (scale * y)
+
+translateExactPoint :: ExactPoint -> ExactVector -> ExactPoint
+translateExactPoint (ExactPoint x y) (ExactVector dx dy) =
+  ExactPoint (x + dx) (y + dy)
+
+-- | Exact axis-aligned bounds of a nonempty point section. Candidate filters
+-- never replace exact geometric intersection predicates.
+data ExactBounds = ExactBounds
+  !ExactRational
+  !ExactRational
+  !ExactRational
+  !ExactRational
+
+-- | Bounds need a nonempty point section, not a simple polygon proof. Exact
+-- arrangement boundary walks may repeat cut vertices and share this owner.
+exactPointsBounds :: NonEmpty ExactPoint -> ExactBounds
+exactPointsBounds (firstPoint :| remaining) =
+  List.foldl' extend (pointBounds firstPoint) remaining
+ where
+  extend bounds point = boundsUnion bounds (pointBounds point)
+
+pointBounds :: ExactPoint -> ExactBounds
+pointBounds point =
+  let (x, y) = exactPointCoordinates point
+   in ExactBounds x y x y
+
+boundsUnion :: ExactBounds -> ExactBounds -> ExactBounds
+boundsUnion
+  (ExactBounds leftMinimumX leftMinimumY leftMaximumX leftMaximumY)
+  (ExactBounds rightMinimumX rightMinimumY rightMaximumX rightMaximumY) =
+    ExactBounds
+      (min leftMinimumX rightMinimumX)
+      (min leftMinimumY rightMinimumY)
+      (max leftMaximumX rightMaximumX)
+      (max leftMaximumY rightMaximumY)
+
+boundsOverlap :: ExactBounds -> ExactBounds -> Bool
+boundsOverlap
+  (ExactBounds leftMinimumX leftMinimumY leftMaximumX leftMaximumY)
+  (ExactBounds rightMinimumX rightMinimumY rightMaximumX rightMaximumY) =
+    not
+      ( leftMaximumX < rightMinimumX
+          || rightMaximumX < leftMinimumX
+          || leftMaximumY < rightMinimumY
+          || rightMaximumY < leftMinimumY
+      )
+
+pointInBounds :: ExactPoint -> ExactBounds -> Bool
+pointInBounds point (ExactBounds minimumX minimumY maximumX maximumY) =
+  let (x, y) = exactPointCoordinates point
+   in minimumX <= x && x <= maximumX && minimumY <= y && y <= maximumY
+
+boundsMinimumX :: ExactBounds -> ExactRational
+boundsMinimumX (ExactBounds minimumX _ _ _) = minimumX
+
+boundsMaximumX :: ExactBounds -> ExactRational
+boundsMaximumX (ExactBounds _ _ maximumX _) = maximumX
+
+boundsMinimumY :: ExactBounds -> ExactRational
+boundsMinimumY (ExactBounds _ minimumY _ _) = minimumY
+
+boundsMaximumY :: ExactBounds -> ExactRational
+boundsMaximumY (ExactBounds _ _ _ maximumY) = maximumY
+
+-- | Oriented edges of an admitted retained convex polygon.
+exactRetainedPolygonBoundary :: ExactRetainedPolygon -> [(ExactPoint, ExactPoint)]
+exactRetainedPolygonBoundary = cyclePairs . exactRetainedPolygonPoints
diff --git a/src-dcel/Moonlight/Planar/Internal/ExactSegmentEvents.hs b/src-dcel/Moonlight/Planar/Internal/ExactSegmentEvents.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/ExactSegmentEvents.hs
@@ -0,0 +1,878 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | One exact owner for non-disjoint relations and split points in a finite
+-- segment family. Collinear intervals descend by supporting-line sections;
+-- non-collinear intersections descend through an immutable Bentley--Ottmann
+-- status tree. Callers attach provenance only after this geometry glues.
+module Moonlight.Planar.Internal.ExactSegmentEvents
+  ( ExactSweepSegmentId (..)
+  , ExactSegmentEvent (..)
+  , ExactSegmentEventObstruction (..)
+  , ExactSegmentEventPlan
+  , exactSegmentEventPlan
+  , exactSegmentEvents
+  , exactSegmentSplitPoints
+  , exactSegmentRelationMap
+  , exactSegmentPairChecks
+  , exactSegmentSweepMaximumHeight
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Applicative ((<|>))
+import Control.Monad (foldM)
+import Data.List (sortBy)
+import qualified Data.List as List
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as V
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactIntersectionError
+  , ExactPoint
+  , ExactSegment
+  , SegmentRelation (..)
+  , exactOnClosedSegment
+  , exactPointCoordinates
+  , exactSegmentEndpoints
+  , exactSegmentRelation
+  , exactSupportingLineIntersection
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( orderedPair
+  , unorderedPairs
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRationalDenominator
+  , exactRationalNumerator
+  )
+
+newtype ExactSweepSegmentId = ExactSweepSegmentId Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data ExactSegmentEvent
+  = ExactProperCrossing !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
+  | ExactEndpointTouch !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
+  | ExactSharedEndpoint !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
+  | ExactDuplicateSegments !ExactSweepSegmentId !ExactSweepSegmentId
+  | ExactCollinearOverlap
+      !ExactSweepSegmentId
+      !ExactSweepSegmentId
+      !ExactPoint
+      !ExactPoint
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data ExactSegmentEventObstruction
+  = ExactSweepIntersectionObstruction
+      !ExactSweepSegmentId
+      !ExactSweepSegmentId
+      !ExactIntersectionError
+  | ExactSweepRelationWitnessMissing
+      !ExactSweepSegmentId
+      !ExactSweepSegmentId
+      !SegmentRelation
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data ExactSegmentEventPlan = ExactSegmentEventPlan
+  { plannedEvents :: !(Map (ExactSweepSegmentId, ExactSweepSegmentId) ExactSegmentEvent)
+  , plannedSplitPoints :: !(V.Vector [ExactPoint])
+  , plannedPairChecks :: !Int
+  , plannedMaximumHeight :: !Int
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data SegmentMeta = SegmentMeta
+  { metaId :: !ExactSweepSegmentId
+  , metaSegment :: !ExactSegment
+  , metaLow :: !ExactPoint
+  , metaHigh :: !ExactPoint
+  , metaVertical :: !Bool
+  }
+
+data SupportingLine = SupportingLine !Integer !Integer !Integer
+  deriving stock (Eq, Ord, Show)
+
+data EventBundle = EventBundle
+  { eventStarts :: !(Set ExactSweepSegmentId)
+  , eventEnds :: !(Set ExactSweepSegmentId)
+  , eventScheduled :: !(Set (ExactSweepSegmentId, ExactSweepSegmentId))
+  }
+
+emptyEventBundle :: EventBundle
+emptyEventBundle = EventBundle Set.empty Set.empty Set.empty
+
+mergeEventBundle :: EventBundle -> EventBundle -> EventBundle
+mergeEventBundle left right =
+  EventBundle
+    { eventStarts = eventStarts left <> eventStarts right
+    , eventEnds = eventEnds left <> eventEnds right
+    , eventScheduled = eventScheduled left <> eventScheduled right
+    }
+
+data EventAccumulation = EventAccumulation
+  { accumulatedEvents :: !(Map (ExactSweepSegmentId, ExactSweepSegmentId) ExactSegmentEvent)
+  , accumulatedSplits :: !(IntMap.IntMap (Set ExactPoint))
+  , accumulatedPairChecks :: !Int
+  , accumulatedMaximumHeight :: !Int
+  }
+
+data StatusTree
+  = StatusEmpty
+  | StatusNode !Int !StatusTree !SegmentMeta !StatusTree
+
+data PointChange = PointChange
+  { changedPoint :: !ExactPoint
+  , changedContinuing :: !(Set ExactSweepSegmentId)
+  }
+
+data StatusSide = StatusBefore | StatusAfter
+  deriving stock (Eq)
+
+-- | Build the complete exact event plan. The private sweep reports its maximum
+-- AVL height and relation checks so benchmarks can distinguish output growth
+-- from residual orchestration.
+exactSegmentEventPlan
+  :: V.Vector ExactSegment
+  -> Either ExactSegmentEventObstruction ExactSegmentEventPlan
+exactSegmentEventPlan segments = do
+  let metas = V.imap segmentMeta segments
+      initialAccumulation =
+        EventAccumulation
+          { accumulatedEvents = Map.empty
+          , accumulatedSplits = IntMap.empty
+          , accumulatedPairChecks = 0
+          , accumulatedMaximumHeight = 0
+          }
+  let lineGroups = supportingLineGroups metas
+  afterCollinear <- foldM recordCollinearGroup initialAccumulation (Map.elems lineGroups)
+  let initialQueue = V.foldl' insertEndpointEvents Map.empty metas
+  completed <- sweep metas initialQueue StatusEmpty afterCollinear
+  pure
+    ExactSegmentEventPlan
+      { plannedEvents = accumulatedEvents completed
+      , plannedSplitPoints = finalizeSplitPoints segments (accumulatedSplits completed)
+      , plannedPairChecks = accumulatedPairChecks completed
+      , plannedMaximumHeight = accumulatedMaximumHeight completed
+      }
+
+finalizeSplitPoints
+  :: V.Vector ExactSegment
+  -> IntMap.IntMap (Set ExactPoint)
+  -> V.Vector [ExactPoint]
+finalizeSplitPoints segments splitPoints =
+  V.imap
+    (\index segment ->
+       let (from, to) = exactSegmentEndpoints segment
+           eventPoints = IntMap.findWithDefault Set.empty index splitPoints
+        in sortAlong segment (Set.toList (Set.insert from (Set.insert to eventPoints))))
+    segments
+
+segmentMeta :: Int -> ExactSegment -> SegmentMeta
+segmentMeta index segment =
+  let (firstPoint, secondPoint) = exactSegmentEndpoints segment
+      low = min firstPoint secondPoint
+      high = max firstPoint secondPoint
+      (lowX, _) = exactPointCoordinates low
+      (highX, _) = exactPointCoordinates high
+   in SegmentMeta
+        { metaId = ExactSweepSegmentId index
+        , metaSegment = segment
+        , metaLow = low
+        , metaHigh = high
+        , metaVertical = lowX == highX
+        }
+
+supportingLineGroups
+  :: V.Vector SegmentMeta
+  -> Map SupportingLine [SegmentMeta]
+supportingLineGroups =
+  V.foldl'
+    (\groups meta -> Map.insertWith (<>) (supportingLine meta) [meta] groups)
+    Map.empty
+
+supportingLine :: SegmentMeta -> SupportingLine
+supportingLine meta =
+  let (from, to) = exactSegmentEndpoints (metaSegment meta)
+      (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+      deltaX = toX - fromX
+      deltaY = toY - fromY
+      a = deltaY
+      b = negate deltaX
+      c = deltaX * fromY - deltaY * fromX
+      denominators = map exactRationalDenominator [a, b, c]
+      commonDenominator = List.foldl' lcm 1 denominators
+      integerCoefficient coefficient =
+        exactRationalNumerator coefficient
+          * (commonDenominator `quot` exactRationalDenominator coefficient)
+      integerA = integerCoefficient a
+      integerB = integerCoefficient b
+      integerC = integerCoefficient c
+      commonDivisor = gcd (abs integerA) (gcd (abs integerB) (abs integerC))
+      sign
+        | integerA < 0 = -1
+        | integerA == 0 && integerB < 0 = -1
+        | otherwise = 1
+      normalize coefficient = sign * (coefficient `quot` commonDivisor)
+   in SupportingLine (normalize integerA) (normalize integerB) (normalize integerC)
+
+recordCollinearGroup
+  :: EventAccumulation
+  -> [SegmentMeta]
+  -> Either ExactSegmentEventObstruction EventAccumulation
+recordCollinearGroup initial metas =
+  fst <$> foldM descend (initial, []) ordered
+ where
+  ordered = sortBy compareInterval metas
+  compareInterval left right =
+    compare (metaLow left, metaHigh left, metaId left) (metaLow right, metaHigh right, metaId right)
+  descend (accumulation, active) current = do
+    let retained = filter (\candidate -> metaHigh candidate >= metaLow current) active
+    updated <-
+      foldM
+        (\accumulated candidate -> recordRelation accumulated candidate current)
+        accumulation
+        retained
+    pure (updated, current : retained)
+
+insertEndpointEvents
+  :: Map ExactPoint EventBundle
+  -> SegmentMeta
+  -> Map ExactPoint EventBundle
+insertEndpointEvents queue meta =
+  insertBundle (metaHigh meta) (emptyEventBundle{eventEnds = Set.singleton (metaId meta)})
+    (insertBundle (metaLow meta) (emptyEventBundle{eventStarts = Set.singleton (metaId meta)}) queue)
+
+insertBundle
+  :: ExactPoint
+  -> EventBundle
+  -> Map ExactPoint EventBundle
+  -> Map ExactPoint EventBundle
+insertBundle = Map.insertWith mergeEventBundle
+
+sweep
+  :: V.Vector SegmentMeta
+  -> Map ExactPoint EventBundle
+  -> StatusTree
+  -> EventAccumulation
+  -> Either ExactSegmentEventObstruction EventAccumulation
+sweep metas queue status accumulation =
+  case Map.lookupMin queue of
+    Nothing -> Right accumulation
+    Just (firstPoint, _) -> do
+      let (currentX, _) = exactPointCoordinates firstPoint
+          (batch, laterQueue) =
+            Map.spanAntitone
+              (\point -> fst (exactPointCoordinates point) == currentX)
+              queue
+          startedIds =
+            Set.toAscList
+              (Map.foldl' (\ids bundle -> ids <> eventStarts bundle) Set.empty batch)
+      let started = map (metaAt metas) startedIds
+          startingIds = map metaId (filter (not . metaVertical) started)
+          verticalIds = map metaId (filter metaVertical started)
+      withVerticals <-
+        recordVerticalRelations
+          metas
+          currentX
+          startingIds
+          verticalIds
+          status
+          accumulation
+      (withEvents, removals, insertions, changes) <-
+        foldM
+          (processPoint metas status)
+          (withVerticals, Set.empty, Set.empty, [])
+          (Map.toAscList batch)
+      let statusWithout =
+            List.foldl'
+              (deleteStatus metas StatusBefore currentX)
+              status
+              (Set.toAscList removals)
+          statusAfter =
+            List.foldl'
+              (insertStatus metas StatusAfter currentX)
+              statusWithout
+              (Set.toAscList insertions)
+      (scheduledQueue, scheduledAccumulation) <-
+        foldM
+          (scheduleAroundChange metas currentX statusAfter)
+          (laterQueue, withEvents)
+          changes
+      let measured =
+            scheduledAccumulation
+              { accumulatedMaximumHeight =
+                  max
+                    (accumulatedMaximumHeight scheduledAccumulation)
+                    (statusHeight statusAfter)
+              }
+      sweep metas scheduledQueue statusAfter measured
+
+recordVerticalRelations
+  :: V.Vector SegmentMeta
+  -> ExactRational
+  -> [ExactSweepSegmentId]
+  -> [ExactSweepSegmentId]
+  -> StatusTree
+  -> EventAccumulation
+  -> Either ExactSegmentEventObstruction EventAccumulation
+recordVerticalRelations metas currentX startingIds verticalIds status accumulation =
+  foldM (recordVertical temporaryStatus) accumulation verticalIds
+ where
+  temporaryStatus =
+    List.foldl' (insertStatus metas StatusAfter currentX) status startingIds
+  recordVertical temporary accumulated verticalId =
+    let vertical = metaAt metas verticalId
+        (_, lowY) = exactPointCoordinates (metaLow vertical)
+        (_, highY) = exactPointCoordinates (metaHigh vertical)
+        candidates = statusRangeByY currentX lowY highY temporary
+     in foldM
+          (\current candidateId -> recordRelation current vertical (metaAt metas candidateId))
+          accumulated
+          candidates
+
+processPoint
+  :: V.Vector SegmentMeta
+  -> StatusTree
+  -> ( EventAccumulation
+     , Set ExactSweepSegmentId
+     , Set ExactSweepSegmentId
+     , [PointChange]
+     )
+  -> (ExactPoint, EventBundle)
+  -> Either
+      ExactSegmentEventObstruction
+      ( EventAccumulation
+      , Set ExactSweepSegmentId
+      , Set ExactSweepSegmentId
+      , [PointChange]
+      )
+processPoint metas status (accumulation, removals, insertions, changes) (point, bundle) = do
+  let (x, y) = exactPointCoordinates point
+      activeAtPoint = Set.fromList (statusAtY x y status)
+      scheduledIds =
+        Set.fromList
+          [ segmentId
+          | (leftId, rightId) <- Set.toList (eventScheduled bundle)
+          , segmentId <- [leftId, rightId]
+          ]
+      candidates = eventStarts bundle <> eventEnds bundle <> activeAtPoint <> scheduledIds
+      incident =
+        Set.fromList
+          (filter
+             (\segmentId ->
+                let (from, to) = exactSegmentEndpoints (metaSegment (metaAt metas segmentId))
+                 in exactOnClosedSegment from to point)
+             (Set.toAscList candidates))
+      metasAtPoint = map (metaAt metas) (Set.toAscList incident)
+  withRelations <- recordIncidentPairs metas accumulation incident
+  let removable =
+        Set.fromList
+          [ metaId meta
+          | meta <- metasAtPoint
+          , not (metaVertical meta)
+          , Set.member (metaId meta) activeAtPoint
+              || Set.member (metaId meta) (eventEnds bundle)
+          ]
+      continuing =
+        Set.fromList
+          [ metaId meta
+          | meta <- metasAtPoint
+          , not (metaVertical meta)
+          , fst (exactPointCoordinates (metaHigh meta)) > x
+          ]
+  pure
+    ( withRelations
+    , removals <> removable
+    , insertions <> continuing
+    , PointChange point continuing : changes
+    )
+
+recordIncidentPairs
+  :: V.Vector SegmentMeta
+  -> EventAccumulation
+  -> Set ExactSweepSegmentId
+  -> Either ExactSegmentEventObstruction EventAccumulation
+recordIncidentPairs metas initial incident =
+  foldM
+    (\accumulation (leftId, rightId) ->
+       recordRelation accumulation (metaAt metas leftId) (metaAt metas rightId))
+    initial
+    (unorderedPairs (Set.toAscList incident))
+
+recordRelation
+  :: EventAccumulation
+  -> SegmentMeta
+  -> SegmentMeta
+  -> Either ExactSegmentEventObstruction EventAccumulation
+recordRelation accumulation firstMeta secondMeta =
+  let (leftMeta, rightMeta) =
+        if metaId firstMeta <= metaId secondMeta
+          then (firstMeta, secondMeta)
+          else (secondMeta, firstMeta)
+      leftId = metaId leftMeta
+      rightId = metaId rightMeta
+      relation = relationOf (metaSegment leftMeta) (metaSegment rightMeta)
+      checked = accumulation{accumulatedPairChecks = accumulatedPairChecks accumulation + 1}
+   in case relation of
+        SegmentsDisjoint -> Right checked
+        _ -> do
+          (event, splitPoints) <-
+            relationEvent leftId rightId (metaSegment leftMeta) (metaSegment rightMeta) relation
+          let ExactSweepSegmentId leftIndex = leftId
+              ExactSweepSegmentId rightIndex = rightId
+              splitSet = Set.fromList splitPoints
+              withLeft =
+                IntMap.insertWith Set.union leftIndex splitSet (accumulatedSplits checked)
+              withBoth = IntMap.insertWith Set.union rightIndex splitSet withLeft
+          Right
+            checked
+              { accumulatedEvents =
+                  Map.insert
+                    (leftId, rightId)
+                    event
+                    (accumulatedEvents checked)
+              , accumulatedSplits = withBoth
+              }
+
+relationOf :: ExactSegment -> ExactSegment -> SegmentRelation
+relationOf left right =
+  let (a, b) = exactSegmentEndpoints left
+      (c, d) = exactSegmentEndpoints right
+   in exactSegmentRelation a b c d
+
+relationEvent
+  :: ExactSweepSegmentId
+  -> ExactSweepSegmentId
+  -> ExactSegment
+  -> ExactSegment
+  -> SegmentRelation
+  -> Either ExactSegmentEventObstruction (ExactSegmentEvent, [ExactPoint])
+relationEvent leftId rightId left right relation =
+  case relation of
+    SegmentsDisjoint -> missing
+    SegmentsDuplicate -> Right (ExactDuplicateSegments leftId rightId, [])
+    SegmentsProperlyCross -> do
+      crossing <-
+        either
+          (Left . ExactSweepIntersectionObstruction leftId rightId)
+          Right
+          (exactSupportingLineIntersection left right)
+      Right (ExactProperCrossing leftId rightId crossing, [crossing])
+    SegmentsShareEndpoint ->
+      uniqueWitness (ExactSharedEndpoint leftId rightId) (uniqueShared left right)
+    SegmentEndpointTouchesInterior ->
+      uniqueWitness (ExactEndpointTouch leftId rightId) (uniqueTouch left right)
+    SegmentsCollinearlyOverlap ->
+      let lower = max (min a b) (min c d)
+          upper = min (max a b) (max c d)
+       in if lower < upper
+            then Right (ExactCollinearOverlap leftId rightId lower upper, [lower, upper])
+            else missing
+ where
+  (a, b) = exactSegmentEndpoints left
+  (c, d) = exactSegmentEndpoints right
+  uniqueWitness
+    :: (ExactPoint -> ExactSegmentEvent)
+    -> Maybe ExactPoint
+    -> Either ExactSegmentEventObstruction (ExactSegmentEvent, [ExactPoint])
+  uniqueWitness make witness =
+    case witness of
+      Just point -> Right (make point, [point])
+      Nothing -> missing
+  missing :: Either ExactSegmentEventObstruction (ExactSegmentEvent, [ExactPoint])
+  missing = Left (ExactSweepRelationWitnessMissing leftId rightId relation)
+
+scheduleAroundChange
+  :: V.Vector SegmentMeta
+  -> ExactRational
+  -> StatusTree
+  -> (Map ExactPoint EventBundle, EventAccumulation)
+  -> PointChange
+  -> Either
+      ExactSegmentEventObstruction
+      (Map ExactPoint EventBundle, EventAccumulation)
+scheduleAroundChange metas currentX status state change =
+  case Set.toAscList (changedContinuing change) of
+    [] ->
+      let (_, y) = exactPointCoordinates (changedPoint change)
+          (below, above) = statusBelowAbove currentX y status
+       in scheduleMaybePair metas currentX below above state
+    continuingIds ->
+      case sortBy (statusCompare StatusAfter currentX) (map (metaAt metas) continuingIds) of
+        [] -> Right state
+        lowest : remaining ->
+          let highest = List.foldl' (\_ current -> current) lowest remaining
+              below = statusPredecessor currentX lowest status
+              above = statusSuccessor currentX highest status
+           in scheduleMaybePair metas currentX below (Just (metaId lowest)) state
+                >>= scheduleMaybePair metas currentX (Just (metaId highest)) above
+
+scheduleMaybePair
+  :: V.Vector SegmentMeta
+  -> ExactRational
+  -> Maybe ExactSweepSegmentId
+  -> Maybe ExactSweepSegmentId
+  -> (Map ExactPoint EventBundle, EventAccumulation)
+  -> Either
+      ExactSegmentEventObstruction
+      (Map ExactPoint EventBundle, EventAccumulation)
+scheduleMaybePair _ _ Nothing _ state = Right state
+scheduleMaybePair _ _ _ Nothing state = Right state
+scheduleMaybePair metas currentX (Just firstId) (Just secondId) (queue, accumulation)
+  | firstId == secondId = Right (queue, accumulation)
+  | otherwise = do
+      let firstMeta = metaAt metas firstId
+          secondMeta = metaAt metas secondId
+          relation = relationOf (metaSegment firstMeta) (metaSegment secondMeta)
+          checked = accumulation{accumulatedPairChecks = accumulatedPairChecks accumulation + 1}
+      witness <-
+        either
+          (Left . ExactSweepIntersectionObstruction firstId secondId)
+          Right
+          (relationWitnessPoint (metaSegment firstMeta) (metaSegment secondMeta) relation)
+      case witness of
+        Just point
+          | fst (exactPointCoordinates point) > currentX ->
+              let pair = orderedPair firstId secondId
+                  bundle = emptyEventBundle{eventScheduled = Set.singleton pair}
+               in Right (insertBundle point bundle queue, checked)
+        _ -> Right (queue, checked)
+
+relationWitnessPoint
+  :: ExactSegment
+  -> ExactSegment
+  -> SegmentRelation
+  -> Either ExactIntersectionError (Maybe ExactPoint)
+relationWitnessPoint left right relation =
+  case relation of
+    SegmentsProperlyCross -> Just <$> exactSupportingLineIntersection left right
+    SegmentEndpointTouchesInterior -> Right (uniqueTouch left right)
+    SegmentsShareEndpoint -> Right (uniqueShared left right)
+    _ -> Right Nothing
+
+uniqueShared :: ExactSegment -> ExactSegment -> Maybe ExactPoint
+uniqueShared left right =
+  let (a, b) = exactSegmentEndpoints left
+      (c, d) = exactSegmentEndpoints right
+   in case Set.toAscList (Set.intersection (Set.fromList [a, b]) (Set.fromList [c, d])) of
+        [point] -> Just point
+        _ -> Nothing
+
+uniqueTouch :: ExactSegment -> ExactSegment -> Maybe ExactPoint
+uniqueTouch left right =
+  let (a, b) = exactSegmentEndpoints left
+      (c, d) = exactSegmentEndpoints right
+      points =
+        Set.toAscList
+          ( Set.fromList
+              ( [point | point <- [a, b], exactOnClosedSegment c d point]
+                  <> [point | point <- [c, d], exactOnClosedSegment a b point]
+              )
+          )
+   in case points of
+        [point] -> Just point
+        _ -> Nothing
+
+statusCompare
+  :: StatusSide
+  -> ExactRational
+  -> SegmentMeta
+  -> SegmentMeta
+  -> Ordering
+statusCompare side x left right =
+  case compareOrdinateAt x left right of
+    EQ ->
+      case compareSlope left right of
+        EQ -> compare (metaId left) (metaId right)
+        slopeOrder -> if side == StatusAfter then slopeOrder else invertOrdering slopeOrder
+    order -> order
+
+compareOrdinateAt :: ExactRational -> SegmentMeta -> SegmentMeta -> Ordering
+compareOrdinateAt x left right =
+  let (leftNumerator, leftDenominator) = ordinateFraction x left
+      (rightNumerator, rightDenominator) = ordinateFraction x right
+   in compare
+        (leftNumerator * rightDenominator)
+        (rightNumerator * leftDenominator)
+
+compareSlope :: SegmentMeta -> SegmentMeta -> Ordering
+compareSlope left right =
+  let (leftRise, leftRun) = slopeFraction left
+      (rightRise, rightRun) = slopeFraction right
+   in compare
+        (leftRise * rightRun)
+        (rightRise * leftRun)
+
+ordinateFraction :: ExactRational -> SegmentMeta -> (ExactRational, ExactRational)
+ordinateFraction x meta =
+  let (fromX, fromY) = exactPointCoordinates (metaLow meta)
+      (toX, toY) = exactPointCoordinates (metaHigh meta)
+      run = toX - fromX
+      numerator = fromY * run + (x - fromX) * (toY - fromY)
+   in (numerator, run)
+
+slopeFraction :: SegmentMeta -> (ExactRational, ExactRational)
+slopeFraction meta =
+  let (fromX, fromY) = exactPointCoordinates (metaLow meta)
+      (toX, toY) = exactPointCoordinates (metaHigh meta)
+   in (toY - fromY, toX - fromX)
+
+invertOrdering :: Ordering -> Ordering
+invertOrdering LT = GT
+invertOrdering EQ = EQ
+invertOrdering GT = LT
+
+insertStatus
+  :: V.Vector SegmentMeta
+  -> StatusSide
+  -> ExactRational
+  -> StatusTree
+  -> ExactSweepSegmentId
+  -> StatusTree
+insertStatus metas side x tree segmentId =
+  statusInsert (statusCompare side x) (metaAt metas segmentId) tree
+
+deleteStatus
+  :: V.Vector SegmentMeta
+  -> StatusSide
+  -> ExactRational
+  -> StatusTree
+  -> ExactSweepSegmentId
+  -> StatusTree
+deleteStatus metas side x tree segmentId =
+  statusDelete (statusCompare side x) (metaAt metas segmentId) tree
+
+statusInsert
+  :: (SegmentMeta -> SegmentMeta -> Ordering)
+  -> SegmentMeta
+  -> StatusTree
+  -> StatusTree
+statusInsert compareIds value tree =
+  case tree of
+    StatusEmpty -> statusNode StatusEmpty value StatusEmpty
+    StatusNode _ left current right ->
+      case compareIds value current of
+        LT -> statusBalance (statusNode (statusInsert compareIds value left) current right)
+        GT -> statusBalance (statusNode left current (statusInsert compareIds value right))
+        EQ -> tree
+
+statusDelete
+  :: (SegmentMeta -> SegmentMeta -> Ordering)
+  -> SegmentMeta
+  -> StatusTree
+  -> StatusTree
+statusDelete compareIds value tree =
+  case tree of
+    StatusEmpty -> StatusEmpty
+    StatusNode _ left current right ->
+      case compareIds value current of
+        LT -> statusBalance (statusNode (statusDelete compareIds value left) current right)
+        GT -> statusBalance (statusNode left current (statusDelete compareIds value right))
+        EQ -> statusMerge left right
+
+statusMerge :: StatusTree -> StatusTree -> StatusTree
+statusMerge left StatusEmpty = left
+statusMerge left right =
+  case statusDeleteLeast right of
+    Nothing -> left
+    Just (least, remaining) -> statusBalance (statusNode left least remaining)
+
+statusDeleteLeast :: StatusTree -> Maybe (SegmentMeta, StatusTree)
+statusDeleteLeast StatusEmpty = Nothing
+statusDeleteLeast (StatusNode _ StatusEmpty value right) = Just (value, right)
+statusDeleteLeast (StatusNode _ left value right) = do
+  (least, remaining) <- statusDeleteLeast left
+  pure (least, statusBalance (statusNode remaining value right))
+
+statusHeight :: StatusTree -> Int
+statusHeight StatusEmpty = 0
+statusHeight (StatusNode height _ _ _) = height
+
+statusNode :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
+statusNode left value right =
+  StatusNode
+    (1 + max (statusHeight left) (statusHeight right))
+    left
+    value
+    right
+
+statusBalance :: StatusTree -> StatusTree
+statusBalance tree =
+  case tree of
+    StatusEmpty -> StatusEmpty
+    StatusNode _ left value right
+      | statusHeight left - statusHeight right > 1 -> balanceLeft left value right
+      | statusHeight right - statusHeight left > 1 -> balanceRight left value right
+      | otherwise -> statusNode left value right
+
+balanceLeft :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
+balanceLeft left value right =
+  case left of
+    StatusNode _ leftLeft leftValue leftRight
+      | statusHeight leftLeft >= statusHeight leftRight ->
+          statusNode leftLeft leftValue (statusNode leftRight value right)
+      | otherwise ->
+          case leftRight of
+            StatusNode _ middleLeft middleValue middleRight ->
+              statusNode
+                (statusNode leftLeft leftValue middleLeft)
+                middleValue
+                (statusNode middleRight value right)
+            StatusEmpty -> statusNode left value right
+    StatusEmpty -> statusNode left value right
+
+balanceRight :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
+balanceRight left value right =
+  case right of
+    StatusNode _ rightLeft rightValue rightRight
+      | statusHeight rightRight >= statusHeight rightLeft ->
+          statusNode (statusNode left value rightLeft) rightValue rightRight
+      | otherwise ->
+          case rightLeft of
+            StatusNode _ middleLeft middleValue middleRight ->
+              statusNode
+                (statusNode left value middleLeft)
+                middleValue
+                (statusNode middleRight rightValue rightRight)
+            StatusEmpty -> statusNode left value right
+    StatusEmpty -> statusNode left value right
+
+statusAtY
+  :: ExactRational
+  -> ExactRational
+  -> StatusTree
+  -> [ExactSweepSegmentId]
+statusAtY x y = descend
+ where
+  descend StatusEmpty = []
+  descend (StatusNode _ left meta right) =
+    case compareMetaToY x y meta of
+      LT -> descend right
+      GT -> descend left
+      EQ -> descend left <> [metaId meta] <> descend right
+
+statusRangeByY
+  :: ExactRational
+  -> ExactRational
+  -> ExactRational
+  -> StatusTree
+  -> [ExactSweepSegmentId]
+statusRangeByY x lower upper = descend
+ where
+  descend StatusEmpty = []
+  descend (StatusNode _ left meta right) =
+    let below = compareMetaToY x lower meta == LT
+        above = compareMetaToY x upper meta == GT
+     in if below
+          then descend right
+          else
+            if above
+              then descend left
+              else descend left <> [metaId meta] <> descend right
+
+compareMetaToY :: ExactRational -> ExactRational -> SegmentMeta -> Ordering
+compareMetaToY x y meta =
+  let (numerator, denominator) = ordinateFraction x meta
+   in compare numerator (y * denominator)
+
+statusBelowAbove
+  :: ExactRational
+  -> ExactRational
+  -> StatusTree
+  -> (Maybe ExactSweepSegmentId, Maybe ExactSweepSegmentId)
+statusBelowAbove x y = descend Nothing Nothing
+ where
+  descend below above StatusEmpty = (below, above)
+  descend below above (StatusNode _ left meta right) =
+    case compareMetaToY x y meta of
+      LT -> descend (Just (metaId meta)) above right
+      GT -> descend below (Just (metaId meta)) left
+      EQ -> (statusGreatest left <|> below, statusLeast right <|> above)
+
+statusPredecessor
+  :: ExactRational
+  -> SegmentMeta
+  -> StatusTree
+  -> Maybe ExactSweepSegmentId
+statusPredecessor x target = descend Nothing
+ where
+  descend candidate StatusEmpty = candidate
+  descend candidate (StatusNode _ left current right) =
+    case statusCompare StatusAfter x target current of
+      LT -> descend candidate left
+      GT -> descend (Just (metaId current)) right
+      EQ -> statusGreatest left <|> candidate
+
+statusSuccessor
+  :: ExactRational
+  -> SegmentMeta
+  -> StatusTree
+  -> Maybe ExactSweepSegmentId
+statusSuccessor x target = descend Nothing
+ where
+  descend candidate StatusEmpty = candidate
+  descend candidate (StatusNode _ left current right) =
+    case statusCompare StatusAfter x target current of
+      LT -> descend (Just (metaId current)) left
+      GT -> descend candidate right
+      EQ -> statusLeast right <|> candidate
+
+statusLeast :: StatusTree -> Maybe ExactSweepSegmentId
+statusLeast StatusEmpty = Nothing
+statusLeast (StatusNode _ StatusEmpty value _) = Just (metaId value)
+statusLeast (StatusNode _ left _ _) = statusLeast left
+
+statusGreatest :: StatusTree -> Maybe ExactSweepSegmentId
+statusGreatest StatusEmpty = Nothing
+statusGreatest (StatusNode _ _ value StatusEmpty) = Just (metaId value)
+statusGreatest (StatusNode _ _ _ right) = statusGreatest right
+
+-- Every identifier the sweep handles was minted by 'segmentMeta' over this
+-- very vector, so the map from identifier to meta is total.
+metaAt :: V.Vector SegmentMeta -> ExactSweepSegmentId -> SegmentMeta
+metaAt metas (ExactSweepSegmentId index) = metas V.! index
+
+exactSegmentEvents :: ExactSegmentEventPlan -> [ExactSegmentEvent]
+exactSegmentEvents = Map.elems . plannedEvents
+
+exactSegmentSplitPoints
+  :: ExactSegmentEventPlan
+  -> ExactSweepSegmentId
+  -> [ExactPoint]
+exactSegmentSplitPoints plan (ExactSweepSegmentId segmentIndex) =
+  maybe [] id (plannedSplitPoints plan V.!? segmentIndex)
+
+exactSegmentRelationMap
+  :: ExactSegmentEventPlan
+  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+exactSegmentRelationMap = Map.map eventRelation . plannedEvents
+
+eventRelation :: ExactSegmentEvent -> SegmentRelation
+eventRelation event =
+  case event of
+    ExactProperCrossing {} -> SegmentsProperlyCross
+    ExactEndpointTouch {} -> SegmentEndpointTouchesInterior
+    ExactSharedEndpoint {} -> SegmentsShareEndpoint
+    ExactDuplicateSegments {} -> SegmentsDuplicate
+    ExactCollinearOverlap {} -> SegmentsCollinearlyOverlap
+
+exactSegmentPairChecks :: ExactSegmentEventPlan -> Int
+exactSegmentPairChecks = plannedPairChecks
+
+exactSegmentSweepMaximumHeight :: ExactSegmentEventPlan -> Int
+exactSegmentSweepMaximumHeight = plannedMaximumHeight
+
+sortAlong :: ExactSegment -> [ExactPoint] -> [ExactPoint]
+sortAlong segment =
+  let (from, to) = exactSegmentEndpoints segment
+   in if from <= to then Set.toAscList . Set.fromList else Set.toDescList . Set.fromList
diff --git a/src-dcel/Moonlight/Planar/Internal/FaceProbe.hs b/src-dcel/Moonlight/Planar/Internal/FaceProbe.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/FaceProbe.hs
@@ -0,0 +1,38 @@
+module Moonlight.Planar.Internal.FaceProbe
+  ( BoundaryProbe (..)
+  , probeBoundary
+  ) where
+
+import Moonlight.Planar.Internal.Predicates (onClosedSegment, orient2d)
+import Moonlight.Planar.Point (Point)
+
+-- | Classification of one oriented boundary of a triangular face. A
+-- 'BoundaryCrossing' carries the half-edge whose incident face is the
+-- destination of the walk; consumers must not reverse it again.
+data BoundaryProbe edge vertex
+  = BoundaryClear
+  | BoundaryOnVertex !vertex
+  | BoundaryOnEdge !edge
+  | BoundaryCrossing !edge
+  deriving stock (Eq, Ord, Show)
+
+probeBoundary
+  :: (edge -> edge)
+  -> Point
+  -> edge
+  -> vertex
+  -> Point
+  -> vertex
+  -> Point
+  -> BoundaryProbe edge vertex
+probeBoundary reverseBoundary query edge fromVertex from toVertex to
+  | query == from = BoundaryOnVertex fromVertex
+  | query == to = BoundaryOnVertex toVertex
+  | otherwise =
+      case orient2d from to query of
+        EQ
+          | onClosedSegment from to query -> BoundaryOnEdge edge
+          | otherwise -> BoundaryClear
+        LT -> BoundaryCrossing (reverseBoundary edge)
+        GT -> BoundaryClear
+{-# INLINE probeBoundary #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/FloodFillIterator.hs b/src-dcel/Moonlight/Planar/Internal/FloodFillIterator.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/FloodFillIterator.hs
@@ -0,0 +1,742 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleInstances #-}
+
+-- | Shape queries and face flood fills over immutable triangulations.
+module Moonlight.Planar.Internal.FloodFillIterator
+  ( DistanceMetric (..)
+  , CircleMetric
+  , CircleMetricError (..)
+  , RectangleMetric
+  , RectangleMetricError (..)
+  , circleMetric
+  , rectangleMetric
+  , edgesInShape
+  , verticesInShape
+  , edgesInCircle
+  , verticesInCircle
+  , edgesInRectangle
+  , verticesInRectangle
+  , floodFillFaces
+  , floodFillFacesWithRejectedEdges
+  , outerFaceFloodFill
+  , facesAtEvenBarrierDepth
+  , FaceComponent
+  , faceComponentFaces
+  , BoundaryOrientation (..)
+  , BoundaryLoop
+  , boundaryLoopOrientation
+  , boundaryLoopResidentVertices
+  , boundaryLoopVertices
+  , RegionBoundary
+  , regionBoundaryOuterLoop
+  , regionBoundaryHoleLoops
+  , BoundaryObstruction (..)
+  , faceComponents
+  , faceComponentsBy
+  , labelledRegionBoundaries
+  , componentBoundaryLoops
+  , componentBoundary
+  , componentBoundaryForFaces
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import Data.List (partition, unfoldr)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import GHC.Generics (Generic)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Handles.Iterators.FixedIterators (undirectedEdges)
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( simplifyBoundaryCycle
+  , traceOrientedBoundaryCircuits
+  )
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Math (segmentDistanceSquaredWide, squaredDistanceWide)
+import Moonlight.Planar.PointLocation
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue, PointValidationError (..))
+import Moonlight.Planar.Scalar (RadiusSquared, RadiusSquaredError (..), mkRadiusSquared, radiusSquaredValue)
+
+-- | One non-empty connected set of equally labelled bounded face indices.
+newtype FaceComponent = FaceComponent IntSet.IntSet
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Component faces in ascending DCEL order.
+faceComponentFaces :: FaceComponent -> [FaceId]
+faceComponentFaces (FaceComponent faces) =
+  fmap (FaceId . fromIntegral) (IntSet.toAscList faces)
+
+-- | Winding carried explicitly by a simple boundary loop.
+data BoundaryOrientation
+  = BoundaryCounterClockwise
+  | BoundaryClockwise
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One non-empty simple boundary loop. Counter-clockwise loops contribute
+-- filled area under nonzero winding; clockwise loops subtract holes.
+data BoundaryLoop = BoundaryLoop
+  { boundaryLoopOrientation :: !BoundaryOrientation
+  -- | Exact resident DCEL boundary chain. Consecutive vertices, including
+  -- the closing pair, are endpoints of one boundary half-edge.
+  , boundaryLoopResidentVertices :: !(NonEmpty VertexId)
+  -- | Collinearity-reduced polygon observation of the same resident chain.
+  , boundaryLoopVertices :: !(NonEmpty VertexId)
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The authoritative boundary of one face component.
+data RegionBoundary = RegionBoundary
+  { regionBoundaryOuterLoop :: !BoundaryLoop
+  , regionBoundaryHoleLoops :: ![BoundaryLoop]
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Typed failure to descend boundary half-edges or project their oriented
+-- loops into one strict polygon component.
+data BoundaryObstruction
+  = BoundaryComponentFaceOutOfRange !FaceId {-# UNPACK #-} !Int
+  | BoundaryPinch !VertexId !DirectedEdgeId !DirectedEdgeId
+  | BoundaryCycleDidNotClose !DirectedEdgeId !DirectedEdgeId
+  | BoundaryLoopDegenerate ![VertexId]
+  | BoundaryOuterLoopCardinality !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Complete boundary edges refined by whether each vertex has one successor.
+data BoundaryGraph
+  = SimpleBoundaryGraph !IntSet.IntSet !(IntMap.IntMap DirectedEdgeId)
+  | PinchedBoundaryGraph
+      !IntSet.IntSet
+      !VertexId
+      !DirectedEdgeId
+      !DirectedEdgeId
+
+-- | A query shape that can admit points, test edges, and supply a location
+-- seed.
+class DistanceMetric metric where
+  metricContainsPoint :: metric -> Point -> Bool
+  metricIntersectsEdge :: metric -> Point -> Point -> Bool
+  metricStartPoint :: metric -> QueryPoint
+
+-- | An admitted center and squared radius.
+data CircleMetric = CircleMetric !QueryPoint !RadiusSquared
+  deriving stock (Eq, Ord, Show)
+
+-- | Typed refusal for an invalid circle query.
+data CircleMetricError
+  = InvalidCircleCenter !PointValidationError
+  | InvalidCircleRadius !RadiusSquaredError
+  deriving stock (Eq, Ord, Show)
+
+-- | Admitted lower corner, upper corner, and center of an axis-aligned box.
+data RectangleMetric = RectangleMetric !(QueryPoint) !(QueryPoint) !(QueryPoint)
+  deriving stock (Eq, Ord, Show)
+
+-- | Typed refusal for an invalid rectangle query.
+data RectangleMetricError
+  = InvalidRectangleLower !PointValidationError
+  | InvalidRectangleUpper !PointValidationError
+  | InvalidRectangleCenter !PointValidationError
+  deriving stock (Eq, Ord, Show)
+
+-- | A circle metric, or why the radius is unusable.
+circleMetric :: Point -> Double -> Either CircleMetricError CircleMetric
+circleMetric center radiusSquared =
+  CircleMetric
+    <$> first InvalidCircleCenter (mkQueryPoint center)
+    <*> first InvalidCircleRadius (mkRadiusSquared radiusSquared)
+
+-- | An axis-aligned rectangle metric, or why the corners are unusable.
+rectangleMetric :: Point -> Point -> Either RectangleMetricError RectangleMetric
+rectangleMetric lower@(Point lowerX lowerY) upper@(Point upperX upperY) = do
+  queryLower <- either (Left . InvalidRectangleLower) Right (mkQueryPoint lower)
+  queryUpper <- either (Left . InvalidRectangleUpper) Right (mkQueryPoint upper)
+  queryCenter <-
+    either
+      (Left . InvalidRectangleCenter)
+      Right
+      (mkQueryPoint (Point ((lowerX + upperX) * 0.5) ((lowerY + upperY) * 0.5)))
+  Right (RectangleMetric queryLower queryUpper queryCenter)
+
+instance DistanceMetric CircleMetric where
+  metricContainsPoint (CircleMetric center radius) point =
+    squaredDistanceWide (queryPointValue center) point <= radiusSquaredValue radius
+  metricIntersectsEdge (CircleMetric center radius) from to =
+    segmentDistanceSquaredWide from to (queryPointValue center) <= radiusSquaredValue radius
+  metricStartPoint (CircleMetric center _) = center
+
+instance DistanceMetric RectangleMetric where
+  metricContainsPoint (RectangleMetric lower upper _) (Point x y) =
+    lowerX <= upperX && lowerY <= upperY && x >= lowerX && x <= upperX && y >= lowerY && y <= upperY
+   where
+    Point lowerX lowerY = queryPointValue lower
+    Point upperX upperY = queryPointValue upper
+  metricIntersectsEdge rectangle from to =
+    metricContainsPoint rectangle from
+      || metricContainsPoint rectangle to
+      || segmentRectangleIntersection rectangle from to
+  metricStartPoint (RectangleMetric _ _ center) = center
+
+-- | Edges meeting a circle.
+edgesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [UndirectedEdgeId]
+edgesInCircle triangulation center radiusSquared =
+  edgesInShape triangulation <$> circleMetric center radiusSquared
+
+-- | Vertices inside a circle.
+verticesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [VertexId]
+verticesInCircle triangulation center radiusSquared =
+  verticesInShape triangulation <$> circleMetric center radiusSquared
+
+-- | Edges meeting an axis-aligned rectangle.
+edgesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [UndirectedEdgeId]
+edgesInRectangle triangulation lower upper = edgesInShape triangulation <$> rectangleMetric lower upper
+
+-- | Vertices inside an axis-aligned rectangle.
+verticesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [VertexId]
+verticesInRectangle triangulation lower upper = verticesInShape triangulation <$> rectangleMetric lower upper
+
+-- | Edges meeting any metric shape.
+edgesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [UndirectedEdgeId]
+edgesInShape triangulation metric
+  | numVertices triangulation <= 1 = []
+  | numInnerFaces triangulation == 0 =
+      [edge | edge <- undirectedEdges triangulation, edgeInside edge]
+  | otherwise =
+      let starts = shapeStartFaces triangulation metric
+          (_, accepted, _) = floodFillFacesWithEdges triangulation starts edgeInside
+       in map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList accepted)
+ where
+  edgeInside edge =
+    let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
+     in metricIntersectsEdge metric (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex)
+
+-- | Vertices inside any metric shape.
+verticesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [VertexId]
+verticesInShape triangulation metric =
+  [ vertex
+  | vertex <- candidateVertices
+  , metricContainsPoint metric (vertexPoint triangulation vertex)
+  ]
+ where
+  edges = edgesInShape triangulation metric
+  set = List.foldl' addEndpoints IntSet.empty edges
+  addEndpoints acc edge =
+    let (from, to) = undirectedEndpoints triangulation edge
+     in IntSet.insert (vertexIdIndex from) (IntSet.insert (vertexIdIndex to) acc)
+  candidateVertices
+    | numVertices triangulation == 1 = [VertexId 0]
+    | otherwise = map (VertexId . fromIntegral) (IntSet.toAscList set)
+
+-- | Reach inner faces from the supplied seeds by crossing only admitted edges.
+floodFillFaces
+  :: Triangulation mode vertex directed undirected face -> [FaceId]
+  -> (UndirectedEdgeId -> Bool)
+  -> [FaceId]
+floodFillFaces triangulation starts canCross =
+  let (faces, _, _) = floodFillFacesWithEdges triangulation starts canCross
+   in faces
+
+-- | The reached section and the edge pairs whose crossing was rejected while
+-- descending it. This package-internal observation retains the boundary
+-- witness instead of requiring a later face-wide rediscovery.
+floodFillFacesWithRejectedEdges
+  :: Triangulation mode vertex directed undirected face -> [FaceId]
+  -> (UndirectedEdgeId -> Bool)
+  -> ([FaceId], [UndirectedEdgeId])
+floodFillFacesWithRejectedEdges triangulation starts canCross =
+  let (faces, _, rejected) = floodFillFacesWithEdges triangulation starts canCross
+   in (faces, fmap (UndirectedEdgeId . fromIntegral) (IntSet.toAscList rejected))
+
+floodFillFacesWithEdges
+  :: Triangulation mode vertex directed undirected face -> [FaceId]
+  -> (UndirectedEdgeId -> Bool)
+  -> ([FaceId], IntSet.IntSet, IntSet.IntSet)
+floodFillFacesWithEdges triangulation starts canCross =
+  let (faces, accepted, rejected) = go initialStack initialVisited IntSet.empty IntSet.empty []
+   in (reverse faces, accepted, rejected)
+ where
+  valid face = face /= outerFace && faceIdIndex face < numFaces triangulation
+  (initialStack, initialVisited) = List.foldl' enqueueStart ([], IntSet.empty) starts
+
+  enqueueStart state face
+    | valid face = enqueue face state
+    | otherwise = state
+
+  go [] _ accepted rejected result = (result, accepted, rejected)
+  go (face : stack) visited accepted rejected result =
+    let (stack', visited', accepted', rejected') =
+          foldFaceDirectedEdges'
+            triangulation
+            face
+            expand
+            (stack, visited, accepted, rejected)
+     in go stack' visited' accepted' rejected' (face : result)
+
+  expand (stack, visited, accepted, rejected) edge =
+    let undirected = asUndirected edge
+        edgeIndex = undirectedEdgeIdIndex undirected
+        adjacent = incidentFace triangulation (reverseEdge edge)
+        edgeAdmission
+          | IntSet.member edgeIndex accepted = (True, accepted, rejected)
+          | IntSet.member edgeIndex rejected = (False, accepted, rejected)
+          | canCross undirected = (True, IntSet.insert edgeIndex accepted, rejected)
+          | otherwise = (False, accepted, IntSet.insert edgeIndex rejected)
+        (crosses, accepted', rejected') = edgeAdmission
+        (stack', visited') =
+          if crosses && valid adjacent
+            then enqueue adjacent (stack, visited)
+            else (stack, visited)
+     in (stack', visited', accepted', rejected')
+
+  enqueue face (stack, visited)
+    | IntSet.member index visited = (stack, visited)
+    | otherwise = (face : stack, IntSet.insert index visited)
+   where
+    index = faceIdIndex face
+
+-- | Connected components of equally labelled bounded faces. Labels are
+-- evaluated once; the component carrier is the same 'IntSet' used by descent.
+faceComponents
+  :: Eq label
+  => Triangulation mode vertex directed undirected face
+  -> (FaceId -> label)
+  -> [(label, FaceComponent)]
+faceComponents triangulation labelFace =
+  faceComponentsFromLabels triangulation labelAt (const True)
+ where
+  labels =
+    V.generate
+      (numInnerFaces triangulation)
+      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
+  labelAt faceIndex = labels V.!? (faceIndex - 1)
+
+-- | Connected components over a selected face section and admitted adjacency.
+-- A missing label removes a face from the section; the edge predicate states
+-- which overlaps glue. This is the common descent used by ordinary labelled
+-- regions and exact-overlay charts whose representation diagonals alone may
+-- connect collapsed resident faces.
+faceComponentsBy
+  :: Eq label
+  => Triangulation mode vertex directed undirected face
+  -> (FaceId -> Maybe label)
+  -> (UndirectedEdgeId -> Bool)
+  -> [(label, FaceComponent)]
+faceComponentsBy triangulation labelFace =
+  faceComponentsFromLabels triangulation labelAt
+ where
+  labels =
+    V.generate
+      (numInnerFaces triangulation)
+      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
+  labelAt faceIndex = labels V.!? (faceIndex - 1) >>= id
+
+faceComponentsFromLabels
+  :: Eq label
+  => Triangulation mode vertex directed undirected face
+  -> (Int -> Maybe label)
+  -> (UndirectedEdgeId -> Bool)
+  -> [(label, FaceComponent)]
+faceComponentsFromLabels triangulation labelAt canCross =
+  unfoldr descend initialUnvisited
+ where
+  initialUnvisited =
+    IntSet.fromRange (1, numFaces triangulation - 1)
+
+  descend remaining =
+    case IntSet.minView remaining of
+      Nothing -> Nothing
+      Just (seedIndex, unseeded) ->
+        case labelAt seedIndex of
+          Nothing -> descend unseeded
+          Just componentLabel ->
+            let unvisited =
+                  collectComponent componentLabel (Seq.singleton seedIndex) unseeded
+                componentFaces = IntSet.difference remaining unvisited
+             in Just ((componentLabel, FaceComponent componentFaces), unvisited)
+
+  collectComponent componentLabel queued unvisited =
+    case Seq.viewl queued of
+      Seq.EmptyL -> unvisited
+      faceIndex Seq.:< remainingQueue ->
+        let face = FaceId (fromIntegral faceIndex)
+            (expandedQueue, remainingUnvisited) =
+              foldFaceDirectedEdges'
+                triangulation
+                face
+                (admitAdjacent componentLabel)
+                (remainingQueue, unvisited)
+         in collectComponent
+              componentLabel
+              expandedQueue
+              remainingUnvisited
+
+  admitAdjacent componentLabel (queued, unvisited) edge =
+    if
+      canCross (asUndirected edge)
+        && adjacent /= outerFace
+        && IntSet.member adjacentIndex unvisited
+        && labelAt adjacentIndex == Just componentLabel
+      then
+        ( queued Seq.|> adjacentIndex
+        , IntSet.delete adjacentIndex unvisited
+        )
+      else (queued, unvisited)
+   where
+    adjacent = incidentFace triangulation (reverseEdge edge)
+    adjacentIndex = faceIdIndex adjacent
+{-# INLINE faceComponentsFromLabels #-}
+
+-- | Descend every equally labelled bounded-face component through the one
+-- authoritative boundary tracer. Components are converted independently;
+-- callers may group equal labels only after this descent has succeeded.
+labelledRegionBoundaries
+  :: Eq label
+  => Triangulation mode vertex directed undirected face
+  -> (FaceId -> label)
+  -> Either BoundaryObstruction [(label, RegionBoundary)]
+labelledRegionBoundaries triangulation labelFace =
+  traverse
+    (\(label, component) -> (label,) <$> componentBoundary triangulation component)
+    (faceComponents triangulation labelFace)
+
+-- | Extract the non-empty oriented boundary chain of one component. Boundary
+-- half-edges retain their incident component face on the left. If several
+-- boundary arms meet at one vertex, Euler descent consumes every half-edge
+-- once and repeated-vertex splitting publishes finitely many simple loops.
+componentBoundaryLoops
+  :: Triangulation mode vertex directed undirected face
+  -> FaceComponent
+  -> Either BoundaryObstruction (NonEmpty BoundaryLoop)
+componentBoundaryLoops triangulation component = do
+  graph <- componentBoundaryGraph triangulation component
+  loops <-
+    case graph of
+      SimpleBoundaryGraph edges outgoing ->
+        traceBoundaryLoops triangulation outgoing edges
+      PinchedBoundaryGraph edges _ _ _ ->
+        tracePinchedBoundaryLoops triangulation edges
+  case NonEmpty.nonEmpty loops of
+    Just nonEmptyLoops -> Right nonEmptyLoops
+    Nothing -> Left (BoundaryOuterLoopCardinality 0)
+
+-- | Project one component into a strict polygon boundary. A pinched oriented
+-- chain remains available through 'componentBoundaryLoops', but it is not one
+-- lawful 'RegionBoundary' and is refused here with its first pinch witness.
+componentBoundary
+  :: Triangulation mode vertex directed undirected face
+  -> FaceComponent
+  -> Either BoundaryObstruction RegionBoundary
+componentBoundary triangulation component = do
+  graph <- componentBoundaryGraph triangulation component
+  case graph of
+    PinchedBoundaryGraph _ vertex firstEdge secondEdge ->
+      Left (BoundaryPinch vertex firstEdge secondEdge)
+    SimpleBoundaryGraph edges outgoing ->
+      traceBoundaryLoops triangulation outgoing edges
+        >>= regionBoundaryFromLoops
+
+-- | Project exactly the supplied face section into one boundary. This is the
+-- selected-section descent used by local extension: it does not label or
+-- flood the complement, and therefore its work is proportional to the faces
+-- named by the caller plus their constant-degree boundary edges.
+componentBoundaryForFaces
+  :: Triangulation mode vertex directed undirected face
+  -> Set.Set FaceId
+  -> Either BoundaryObstruction RegionBoundary
+componentBoundaryForFaces triangulation selectedFaces = do
+  component <- selectedFaceComponent triangulation selectedFaces
+  componentBoundary triangulation component
+
+selectedFaceComponent
+  :: Triangulation mode vertex directed undirected face
+  -> Set.Set FaceId
+  -> Either BoundaryObstruction FaceComponent
+selectedFaceComponent triangulation selectedFaces =
+  case IntSet.lookupMin indices of
+    Just 0 -> Left (BoundaryComponentFaceOutOfRange (FaceId 0) (numFaces triangulation))
+    _ ->
+      case IntSet.lookupGE (numFaces triangulation) indices of
+        Just invalid ->
+          Left
+            ( BoundaryComponentFaceOutOfRange
+                (FaceId (fromIntegral invalid))
+                (numFaces triangulation)
+            )
+        Nothing -> Right (FaceComponent indices)
+ where
+  indices = IntSet.fromList (fmap faceIdIndex (Set.toAscList selectedFaces))
+
+componentBoundaryGraph
+  :: Triangulation mode vertex directed undirected face
+  -> FaceComponent
+  -> Either BoundaryObstruction BoundaryGraph
+componentBoundaryGraph triangulation (FaceComponent componentFaces) =
+  case IntSet.lookupGE (numFaces triangulation) componentFaces of
+    Just invalid ->
+      Left
+        ( BoundaryComponentFaceOutOfRange
+            (FaceId (fromIntegral invalid))
+            (numFaces triangulation)
+        )
+    Nothing ->
+      Right
+        ( IntSet.foldl'
+            collectFace
+            (SimpleBoundaryGraph IntSet.empty IntMap.empty)
+            componentFaces
+        )
+ where
+  collectFace graph (face :: Int) =
+    foldFaceDirectedEdges'
+      triangulation
+      (FaceId (fromIntegral face))
+      insertBoundaryEdge
+      graph
+
+  insertBoundaryEdge graph edge
+    | IntSet.member adjacentIndex componentFaces = graph
+    | otherwise =
+        case graph of
+          PinchedBoundaryGraph edges pinchVertex firstEdge secondEdge ->
+            PinchedBoundaryGraph
+              (insertEdge edges)
+              pinchVertex
+              firstEdge
+              secondEdge
+          SimpleBoundaryGraph edges outgoing ->
+            case IntMap.lookup residentIndex outgoing of
+              Nothing ->
+                SimpleBoundaryGraph
+                  (insertEdge edges)
+                  (IntMap.insert residentIndex edge outgoing)
+              Just previousEdge ->
+                PinchedBoundaryGraph
+                  (insertEdge edges)
+                  vertex
+                  previousEdge
+                  edge
+   where
+    adjacentIndex = faceIdIndex (incidentFace triangulation (reverseEdge edge))
+    vertex = origin triangulation edge
+    residentIndex = vertexIdIndex vertex
+    insertEdge = IntSet.insert (directedEdgeIdIndex edge)
+
+regionBoundaryFromLoops
+  :: [BoundaryLoop]
+  -> Either BoundaryObstruction RegionBoundary
+regionBoundaryFromLoops loops =
+  case outerLoops of
+    [outerLoop] ->
+      Right
+        RegionBoundary
+          { regionBoundaryOuterLoop = outerLoop
+          , regionBoundaryHoleLoops = holeLoops
+          }
+    _ -> Left (BoundaryOuterLoopCardinality (length outerLoops))
+ where
+  (outerLoops, holeLoops) =
+    partition
+      ((== BoundaryCounterClockwise) . boundaryLoopOrientation)
+      loops
+
+tracePinchedBoundaryLoops
+  :: Triangulation mode vertex directed undirected face
+  -> IntSet.IntSet
+  -> Either BoundaryObstruction [BoundaryLoop]
+tracePinchedBoundaryLoops triangulation boundaryEdges = do
+  cycles <-
+    traceOrientedBoundaryCircuits
+      (origin triangulation)
+      (destination triangulation)
+      BoundaryCycleDidNotClose
+      outgoingByVertex
+      orientedEdges
+  traverse
+    (simplifyBoundaryLoop triangulation . NonEmpty.toList)
+    cycles
+ where
+  orientedEdges =
+    Set.fromDistinctAscList
+      ( fmap
+          (DirectedEdgeId . fromIntegral)
+          (IntSet.toAscList boundaryEdges)
+      )
+  outgoingByVertex =
+    Set.foldr
+      (\edge -> Map.insertWith (<>) (origin triangulation edge) [edge])
+      Map.empty
+      orientedEdges
+
+traceBoundaryLoops
+  :: Triangulation mode vertex directed undirected face
+  -> IntMap.IntMap DirectedEdgeId
+  -> IntSet.IntSet
+  -> Either BoundaryObstruction [BoundaryLoop]
+traceBoundaryLoops triangulation outgoingByVertex = descend []
+ where
+  descend loops unvisited =
+    case IntSet.minView unvisited of
+      Nothing -> Right (reverse loops)
+      Just (rawStart, _) -> do
+        let start = DirectedEdgeId (fromIntegral rawStart)
+        (vertices, remaining) <- traceCycle start start unvisited []
+        loop <- simplifyBoundaryLoop triangulation vertices
+        descend (loop : loops) remaining
+
+  traceCycle start current unvisited reversedVertices =
+    let remaining = IntSet.delete (directedEdgeIdIndex current) unvisited
+        accumulated = origin triangulation current : reversedVertices
+        target = destination triangulation current
+     in case IntMap.lookup (vertexIdIndex target) outgoingByVertex of
+          Just successor
+            | successor == start -> Right (reverse accumulated, remaining)
+            | IntSet.member (directedEdgeIdIndex successor) remaining ->
+                traceCycle start successor remaining accumulated
+            | otherwise -> Left (BoundaryCycleDidNotClose start successor)
+          Nothing -> Left (BoundaryCycleDidNotClose start current)
+
+simplifyBoundaryLoop
+  :: Triangulation mode vertex directed undirected face
+  -> [VertexId]
+  -> Either BoundaryObstruction BoundaryLoop
+simplifyBoundaryLoop triangulation vertices = do
+  (windingOrder, simplifiedVertices) <-
+    simplifyBoundaryCycle
+      BoundaryLoopDegenerate
+      redundant
+      winding
+      key
+      vertices
+  residentVertices <-
+    maybe
+      (Left (BoundaryLoopDegenerate vertices))
+      Right
+      (NonEmpty.nonEmpty vertices)
+  case windingOrder of
+    GT -> Right (BoundaryLoop BoundaryCounterClockwise residentVertices simplifiedVertices)
+    LT -> Right (BoundaryLoop BoundaryClockwise residentVertices simplifiedVertices)
+    EQ -> Left (BoundaryLoopDegenerate (NonEmpty.toList simplifiedVertices))
+ where
+  point vertex = vertexPoint triangulation vertex
+  redundant previousVertex current nextVertex =
+    orient2d (point previousVertex) (point current) (point nextVertex) == EQ
+      && onClosedSegment (point previousVertex) (point nextVertex) (point current)
+  winding previousVertex current nextVertex =
+    orient2d (point previousVertex) (point current) (point nextVertex)
+  key vertex = (point vertex, vertex)
+
+-- | Inner faces separated from the outer face by an even minimum number of
+-- barriers. A 0–1 BFS floods freely within one depth before crossing a barrier,
+-- so a free-ended barrier can be walked around at depth zero while nested
+-- closed barriers alternate outside and inside.
+facesAtEvenBarrierDepth
+  :: Triangulation mode vertex directed undirected face
+  -> (UndirectedEdgeId -> Bool)
+  -> [FaceId]
+facesAtEvenBarrierDepth triangulation isBarrier =
+  concat (evenLayers (barrierDepthLayers triangulation isBarrier))
+ where
+  evenLayers :: [[FaceId]] -> [[FaceId]]
+  evenLayers (outsideLayer : _insideLayer : deeper) =
+    outsideLayer : evenLayers deeper
+  evenLayers shallow = shallow
+
+barrierDepthLayers
+  :: Triangulation mode vertex directed undirected face
+  -> (UndirectedEdgeId -> Bool)
+  -> [[FaceId]]
+barrierDepthLayers triangulation isBarrier =
+  map (filter (/= outerFace)) (layers IntSet.empty [outerFace])
+ where
+  known face = faceIdIndex face < numFaces triangulation
+
+  layers visited frontier = case flood visited [] frontier of
+    ([], _) -> []
+    (layer, visited') -> layer : layers visited' (concatMap (neighbours isBarrier) layer)
+
+  flood visited acc [] = (reverse acc, visited)
+  flood visited acc (face : rest)
+    | not (known face) || IntSet.member (faceIdIndex face) visited = flood visited acc rest
+    | otherwise =
+        flood
+          (IntSet.insert (faceIdIndex face) visited)
+          (face : acc)
+          (neighbours (not . isBarrier) face <> rest)
+
+  neighbours admit face =
+    [ incidentFace triangulation (reverseEdge edge)
+    | edge <- faceDirectedEdges triangulation face
+    , admit (asUndirected edge)
+    ]
+
+-- | Faces reachable from the outer face without crossing a barrier edge.
+outerFaceFloodFill :: Triangulation mode vertex directed undirected face -> (UndirectedEdgeId -> Bool) -> [FaceId]
+outerFaceFloodFill triangulation canCross = floodFillFaces triangulation starts canCross
+ where
+  starts =
+    [ face
+    | outerEdge <- faceDirectedEdges triangulation outerFace
+    , let edge = asUndirected outerEdge
+    , canCross edge
+    , let face = incidentFace triangulation (reverseEdge outerEdge)
+    , face /= outerFace
+    ]
+
+-- | The faces a shape's start point lands in.
+shapeStartFaces :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [FaceId]
+shapeStartFaces triangulation metric =
+  case locatePoint triangulation (metricStartPoint metric) of
+    InFace face -> [face]
+    OnEdge edge -> filter (/= outerFace) [incidentFace triangulation edge, incidentFace triangulation (reverseEdge edge)]
+    OnVertex vertex ->
+      intSetToFaces
+        ( List.foldl'
+            (\set edge ->
+              let face = incidentFace triangulation edge
+               in if face == outerFace then set else IntSet.insert (faceIdIndex face) set
+            )
+            IntSet.empty
+            (vertexOutgoingEdges triangulation vertex)
+        )
+    OutsideConvexHull _ ->
+      [ incidentFace triangulation (reverseEdge edge)
+      | edge <- faceDirectedEdges triangulation outerFace
+      , let from = vertexPoint triangulation (origin triangulation edge)
+      , let to = vertexPoint triangulation (destination triangulation edge)
+      , metricIntersectsEdge metric from to
+      , incidentFace triangulation (reverseEdge edge) /= outerFace
+      ]
+    EmptyTriangulation -> []
+ where
+  intSetToFaces = map (FaceId . fromIntegral) . IntSet.toAscList
+
+segmentRectangleIntersection :: RectangleMetric -> Point -> Point -> Bool
+segmentRectangleIntersection (RectangleMetric lowerQuery upperQuery _) from to
+  | lx > ux || ly > uy = False
+  | lower == upper = onClosedSegment from to lower
+  | otherwise = any (uncurry (segmentsIntersect from to)) boundaries
+ where
+  lower@(Point lx ly) = queryPointValue lowerQuery
+  upper@(Point ux uy) = queryPointValue upperQuery
+  boundaries =
+    [ (Point lx ly, Point lx uy)
+    , (Point lx uy, Point ux uy)
+    , (Point ux uy, Point ux ly)
+    , (Point ux ly, Point lx ly)
+    ]
diff --git a/src-dcel/Moonlight/Planar/Internal/HandleDefs.hs b/src-dcel/Moonlight/Planar/Internal/HandleDefs.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/HandleDefs.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | The identifier family and the arithmetic on it: a directed edge's twin is
+-- its index complement, so orientation is a bit rather than a lookup.
+module Moonlight.Planar.Internal.HandleDefs
+  ( VertexId (..)
+  , vertexIdIndex
+  , FaceId (..)
+  , faceIdIndex
+  , DirectedEdgeId (..)
+  , directedEdgeIdIndex
+  , UndirectedEdgeId (..)
+  , undirectedEdgeIdIndex
+  , reverseEdge
+  , asUndirected
+  , normalizedDirected
+  , reversedDirected
+  , directedPair
+  , isNormalized
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bits (shiftL, shiftR, xor, (.&.))
+import Data.Word (Word32)
+import Moonlight.Planar.Internal.PackedIndex (unpackIndex)
+
+-- | Index of a vertex in the immutable DCEL.
+newtype VertexId = VertexId { unVertexId :: Word32 }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | Index of a face in the immutable DCEL; zero denotes the outer face.
+newtype FaceId = FaceId { unFaceId :: Word32 }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | Oriented half-edge index. Twin orientations differ only in the low bit.
+newtype DirectedEdgeId = DirectedEdgeId { unDirectedEdgeId :: Word32 }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | Index of a twin pair, with orientation forgotten.
+newtype UndirectedEdgeId = UndirectedEdgeId { unUndirectedEdgeId :: Word32 }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | Project compact identifiers into resident vector indices.
+vertexIdIndex :: VertexId -> Int
+vertexIdIndex = unpackIndex . unVertexId
+{-# INLINE vertexIdIndex #-}
+faceIdIndex :: FaceId -> Int
+faceIdIndex = unpackIndex . unFaceId
+{-# INLINE faceIdIndex #-}
+directedEdgeIdIndex :: DirectedEdgeId -> Int
+directedEdgeIdIndex = unpackIndex . unDirectedEdgeId
+{-# INLINE directedEdgeIdIndex #-}
+undirectedEdgeIdIndex :: UndirectedEdgeId -> Int
+undirectedEdgeIdIndex = unpackIndex . unUndirectedEdgeId
+{-# INLINE undirectedEdgeIdIndex #-}
+
+-- | Select the opposite orientation of the same undirected edge.
+reverseEdge :: DirectedEdgeId -> DirectedEdgeId
+reverseEdge (DirectedEdgeId edge) = DirectedEdgeId (edge `xor` 1)
+{-# INLINE reverseEdge #-}
+
+-- | Forget a directed edge's orientation.
+asUndirected :: DirectedEdgeId -> UndirectedEdgeId
+asUndirected (DirectedEdgeId edge) = UndirectedEdgeId (edge `shiftR` 1)
+{-# INLINE asUndirected #-}
+
+-- | Select the even-indexed orientation of an undirected edge.
+normalizedDirected :: UndirectedEdgeId -> DirectedEdgeId
+normalizedDirected (UndirectedEdgeId edge) = DirectedEdgeId (edge `shiftL` 1)
+{-# INLINE normalizedDirected #-}
+
+-- | Select the odd-indexed orientation of an undirected edge.
+reversedDirected :: UndirectedEdgeId -> DirectedEdgeId
+reversedDirected edge = reverseEdge (normalizedDirected edge)
+{-# INLINE reversedDirected #-}
+
+-- | Both orientations, normalized first and reversed second.
+directedPair :: UndirectedEdgeId -> (DirectedEdgeId, DirectedEdgeId)
+directedPair edge = (normalizedDirected edge, reversedDirected edge)
+{-# INLINE directedPair #-}
+
+-- | Whether a directed edge is the normalized orientation of its pair.
+isNormalized :: DirectedEdgeId -> Bool
+isNormalized (DirectedEdgeId edge) = edge .&. 1 == 0
+{-# INLINE isNormalized #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/Incidence.hs b/src-dcel/Moonlight/Planar/Internal/Incidence.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Incidence.hs
@@ -0,0 +1,353 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Immutable geometry-free planar incidence. Native meshes share their three
+-- packed topology planes; exact arrangements admit the same paired-edge layout
+-- once, with sparse additional boundary components and isolated vertices.
+module Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , PlanarIncidenceError (..)
+  , FaceBoundaryComponent (..)
+  , nativePlanarIncidence
+  , admitPlanarIncidence
+  , validatePlanarIncidence
+  , validateIncidenceEdgeLinks
+  , incidenceVertexCount
+  , incidenceDirectedEdgeCount
+  , incidenceEdgeCount
+  , incidenceFaceCount
+  , incidenceNonCellularFaces
+  , incidenceVertexOutEdge
+  , incidenceAdjacentEdge
+  , incidenceOrigin
+  , incidenceDestination
+  , incidenceNext
+  , incidencePrevious
+  , incidenceIncidentFace
+  , incidenceUndirectedEndpoints
+  , incidenceVertexOutgoingEdges
+  , faceBoundaryComponents
+  , faceInnerBoundaryComponents
+  , faceIsolatedVertices
+  , faceEulerContribution
+  , packedOptionalEdge
+  , packedOrigin
+  , packedNext
+  , packedPrevious
+  , packedIncidentFace
+  , circularEdgeWalk
+  , circularEdgeFold
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Control.Monad (unless)
+import Data.Foldable (traverse_)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import Data.Maybe (maybeToList)
+import qualified Data.List as List
+import Data.Word (Word32)
+import GHC.Exts (build)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.PackedIndex (indexLimit, unpackOptionalIndex)
+import Moonlight.Planar.Internal.Paged (Paged, pagedLength, pagedUnsafeIndex)
+import Moonlight.Planar.Internal.Representation (Triangulation (..))
+import Moonlight.Planar.Internal.Types (PlanarIncidenceError (..))
+
+-- No Generic or exported record updates: packed admission is the only raw
+-- construction boundary. The sparse maps describe components, not geometry.
+data PlanarIncidence = PlanarIncidence
+  !(Paged Word32)
+  !(Paged Word32)
+  !(Paged Word32)
+  !(IntMap.IntMap [DirectedEdgeId])
+  !(IntMap.IntMap IntSet.IntSet)
+  deriving stock (Eq, Show)
+
+instance NFData PlanarIncidence where
+  rnf (PlanarIncidence vertices topology faces components isolated) =
+    rnf vertices `seq` rnf topology `seq` rnf faces `seq` rnf components `seq` rnf isolated
+
+data FaceBoundaryComponent
+  = BoundaryCycleRoot !DirectedEdgeId
+  | IsolatedBoundaryVertex !VertexId
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | O(1) projection retaining the exact same immutable paged arrays. The only
+-- native disconnected zero-cell is the singleton mesh, owned by face zero.
+nativePlanarIncidence :: Triangulation mode vertex directed undirected face -> PlanarIncidence
+nativePlanarIncidence mesh =
+  PlanarIncidence
+    (triVertexOut mesh)
+    (triHalfTopology mesh)
+    (triFaceEdge mesh)
+    IntMap.empty
+    ( if pagedLength (triVertexOut mesh) == 1 && pagedLength (triHalfTopology mesh) == 0
+        then IntMap.singleton 0 (IntSet.singleton 0)
+        else IntMap.empty
+    )
+{-# INLINE nativePlanarIncidence #-}
+
+-- | Admit one exact arrangement's packed incidence. Geometry, angular order,
+-- component orientation and containment are owned by arrangement construction;
+-- this boundary checks only the stored combinatorial obligations.
+admitPlanarIncidence
+  :: Paged Word32
+  -> Paged Word32
+  -> Paged Word32
+  -> IntMap.IntMap [DirectedEdgeId]
+  -> IntMap.IntMap IntSet.IntSet
+  -> Either PlanarIncidenceError PlanarIncidence
+admitPlanarIncidence vertices topology faces components isolated = do
+  let incidence = PlanarIncidence vertices topology faces components isolated
+  validatePlanarIncidence incidence
+  pure incidence
+
+-- | Discharge the same finite laws for either packed admission or the native
+-- O(1) projection. Ranges precede every linked read and orbit traversal.
+validatePlanarIncidence :: PlanarIncidence -> Either PlanarIncidenceError ()
+validatePlanarIncidence incidence@(PlanarIncidence _ topology _ components isolated) = do
+  let vertexCount = incidenceVertexCount incidence
+      directedCount = incidenceDirectedEdgeCount incidence
+      faceCount = incidenceFaceCount incidence
+      topologyWords = pagedLength topology
+      vertexIds = fmap (VertexId . fromIntegral) [0 .. vertexCount - 1]
+      edgeIds = fmap (DirectedEdgeId . fromIntegral) [0 .. directedCount - 1]
+      faceIds = fmap (FaceId . fromIntegral) [0 .. faceCount - 1]
+  unless
+    ( topologyWords `rem` 8 == 0 && faceCount > 0
+        && vertexCount <= indexLimit && directedCount <= indexLimit && faceCount <= indexLimit
+    )
+    (Left (IncidencePackedLayoutInvalid vertexCount topologyWords faceCount))
+  traverse_ (validateEdgeRanges incidence) edgeIds
+  traverse_ (validateIncidenceEdgeLinks incidence) edgeIds
+  traverse_ (validateVertexRoot incidence) vertexIds
+  traverse_ (validateSparseFace incidence) (IntMap.keys components <> IntMap.keys isolated)
+  traverse_ (validateFaceRoots incidence) faceIds
+  let edgeUniverse = IntSet.fromDistinctAscList [0 .. directedCount - 1]
+      faceEdges = concatMap (concat . faceBoundaryComponents incidence) faceIds
+      starEdges = concatMap (incidenceVertexOutgoingEdges incidence) vertexIds
+  validateCoverage IncidenceBoundaryCoverageInvalid edgeUniverse (fmap directedEdgeIdIndex faceEdges)
+  validateCoverage IncidenceVertexStarCoverageInvalid edgeUniverse (fmap directedEdgeIdIndex starEdges)
+  let isolatedUniverse = IntSet.fromDistinctAscList
+        [vertexIdIndex vertex | vertex <- vertexIds, incidenceVertexOutEdge incidence vertex == Nothing]
+  validateCoverage IncidenceIsolatedOwnershipInvalid isolatedUniverse
+    (concatMap IntSet.toAscList (IntMap.elems isolated))
+
+validateEdgeRanges :: PlanarIncidence -> DirectedEdgeId -> Either PlanarIncidenceError ()
+validateEdgeRanges incidence edge = do
+  let vertex = incidenceOrigin incidence edge
+      nextEdge = incidenceNext incidence edge
+      previousEdge = incidencePrevious incidence edge
+      face = incidenceIncidentFace incidence edge
+  unless (vertexIdIndex vertex < incidenceVertexCount incidence)
+    (Left (IncidenceVertexOriginInvalid edge vertex))
+  unless (directedEdgeIdIndex nextEdge < incidenceDirectedEdgeCount incidence
+          && directedEdgeIdIndex previousEdge < incidenceDirectedEdgeCount incidence)
+    (Left (IncidenceEdgeLinksInvalid edge nextEdge previousEdge))
+  unless (faceIdIndex face < incidenceFaceCount incidence)
+    (Left (IncidenceEdgeFaceInvalid edge face))
+
+-- | Shared local link law after edge/next/previous/twin ranges are discharged.
+-- Complete admission and certified native closures use this same predicate.
+validateIncidenceEdgeLinks :: PlanarIncidence -> DirectedEdgeId -> Either PlanarIncidenceError ()
+validateIncidenceEdgeLinks incidence edge = do
+  let nextEdge = incidenceNext incidence edge
+  unless (incidencePrevious incidence nextEdge == edge
+          && incidenceNext incidence (incidencePrevious incidence edge) == edge
+          && incidenceIncidentFace incidence nextEdge == incidenceIncidentFace incidence edge)
+    (Left (IncidenceEdgeLinkMismatch edge))
+  unless (incidenceDestination incidence edge == incidenceOrigin incidence nextEdge)
+    (Left (IncidenceEdgeEndpointMismatch edge))
+
+validateVertexRoot :: PlanarIncidence -> VertexId -> Either PlanarIncidenceError ()
+validateVertexRoot incidence vertex =
+  case incidenceVertexOutEdge incidence vertex of
+    Nothing -> Right ()
+    root@(Just edge) ->
+      unless (directedEdgeIdIndex edge < incidenceDirectedEdgeCount incidence
+              && incidenceOrigin incidence edge == vertex)
+        (Left (IncidenceVertexRootInvalid vertex root))
+
+validateSparseFace :: PlanarIncidence -> Int -> Either PlanarIncidenceError ()
+validateSparseFace incidence index =
+  unless (index >= 0 && index < incidenceFaceCount incidence)
+    (Left (IncidenceSparseFaceInvalid index))
+
+validateFaceRoots :: PlanarIncidence -> FaceId -> Either PlanarIncidenceError ()
+validateFaceRoots incidence face = do
+  unless (face == FaceId 0 || incidenceAdjacentEdge incidence face /= Nothing)
+    (Left (IncidenceBoundedFaceWithoutBoundary face))
+  traverse_
+    (\root -> unless (directedEdgeIdIndex root < incidenceDirectedEdgeCount incidence
+                      && incidenceIncidentFace incidence root == face)
+        (Left (IncidenceFaceRootInvalid face root)))
+    (faceCycleRoots incidence face)
+
+validateCoverage
+  :: (IntSet.IntSet -> IntSet.IntSet -> PlanarIncidenceError)
+  -> IntSet.IntSet
+  -> [Int]
+  -> Either PlanarIncidenceError ()
+validateCoverage obstruction expected submitted =
+  let (observed, repeated) = List.foldl' recordOccurrence (IntSet.empty, IntSet.empty) submitted
+   in unless (observed == expected && IntSet.null repeated)
+        (Left (obstruction (IntSet.difference expected observed)
+          (IntSet.union repeated (IntSet.difference observed expected))))
+ where
+  -- Coverage needs membership and duplication, not an allocated integer count
+  -- at every edge. IntSet keeps dense handle universes in packed bitmap leaves.
+  recordOccurrence (!observed, !repeated) index =
+    if IntSet.member index observed
+      then (observed, IntSet.insert index repeated)
+      else (IntSet.insert index observed, repeated)
+
+incidenceVertexCount :: PlanarIncidence -> Int
+incidenceVertexCount (PlanarIncidence vertices _ _ _ _) = pagedLength vertices
+{-# INLINE incidenceVertexCount #-}
+
+incidenceDirectedEdgeCount :: PlanarIncidence -> Int
+incidenceDirectedEdgeCount (PlanarIncidence _ topology _ _ _) = pagedLength topology `quot` 4
+{-# INLINE incidenceDirectedEdgeCount #-}
+
+incidenceEdgeCount :: PlanarIncidence -> Int
+incidenceEdgeCount incidence = incidenceDirectedEdgeCount incidence `quot` 2
+{-# INLINE incidenceEdgeCount #-}
+
+incidenceFaceCount :: PlanarIncidence -> Int
+incidenceFaceCount (PlanarIncidence _ _ faces _ _) = pagedLength faces
+{-# INLINE incidenceFaceCount #-}
+
+-- | Sparse support of bounded faces with holes or punctures. Native meshes
+-- answer immediately from their empty maps, without a face scan.
+incidenceNonCellularFaces :: PlanarIncidence -> [FaceId]
+incidenceNonCellularFaces (PlanarIncidence _ _ _ components isolated) =
+  fmap (FaceId . fromIntegral) . IntSet.toAscList . IntSet.delete 0 $
+    IntSet.union
+      (IntMap.keysSet (IntMap.filter (not . null) components))
+      (IntMap.keysSet (IntMap.filter (not . IntSet.null) isolated))
+
+incidenceVertexOutEdge :: PlanarIncidence -> VertexId -> Maybe DirectedEdgeId
+incidenceVertexOutEdge (PlanarIncidence vertices _ _ _ _) = packedOptionalEdge vertices . vertexIdIndex
+{-# INLINE incidenceVertexOutEdge #-}
+
+incidenceAdjacentEdge :: PlanarIncidence -> FaceId -> Maybe DirectedEdgeId
+incidenceAdjacentEdge (PlanarIncidence _ _ faces _ _) = packedOptionalEdge faces . faceIdIndex
+{-# INLINE incidenceAdjacentEdge #-}
+
+incidenceOrigin :: PlanarIncidence -> DirectedEdgeId -> VertexId
+incidenceOrigin (PlanarIncidence _ topology _ _ _) = packedOrigin topology
+{-# INLINE incidenceOrigin #-}
+
+incidenceDestination :: PlanarIncidence -> DirectedEdgeId -> VertexId
+incidenceDestination incidence = incidenceOrigin incidence . reverseEdge
+{-# INLINE incidenceDestination #-}
+
+incidenceNext :: PlanarIncidence -> DirectedEdgeId -> DirectedEdgeId
+incidenceNext (PlanarIncidence _ topology _ _ _) = packedNext topology
+{-# INLINE incidenceNext #-}
+
+incidencePrevious :: PlanarIncidence -> DirectedEdgeId -> DirectedEdgeId
+incidencePrevious (PlanarIncidence _ topology _ _ _) = packedPrevious topology
+{-# INLINE incidencePrevious #-}
+
+incidenceIncidentFace :: PlanarIncidence -> DirectedEdgeId -> FaceId
+incidenceIncidentFace (PlanarIncidence _ topology _ _ _) = packedIncidentFace topology
+{-# INLINE incidenceIncidentFace #-}
+
+incidenceUndirectedEndpoints :: PlanarIncidence -> UndirectedEdgeId -> (VertexId, VertexId)
+incidenceUndirectedEndpoints incidence edge =
+  let forward = normalizedDirected edge
+   in (incidenceOrigin incidence forward, incidenceDestination incidence forward)
+{-# INLINE incidenceUndirectedEndpoints #-}
+
+incidenceVertexOutgoingEdges :: PlanarIncidence -> VertexId -> [DirectedEdgeId]
+incidenceVertexOutgoingEdges incidence vertex =
+  case incidenceVertexOutEdge incidence vertex of
+    Nothing -> []
+    Just root -> circularEdgeWalk (incidenceDirectedEdgeCount incidence) root
+      (reverseEdge . incidencePrevious incidence)
+{-# INLINE incidenceVertexOutgoingEdges #-}
+
+faceCycleRoots :: PlanarIncidence -> FaceId -> [DirectedEdgeId]
+faceCycleRoots incidence@(PlanarIncidence _ _ _ components _) face =
+  maybeToList (incidenceAdjacentEdge incidence face)
+    <> IntMap.findWithDefault [] (faceIdIndex face) components
+
+faceBoundaryComponents :: PlanarIncidence -> FaceId -> [[DirectedEdgeId]]
+faceBoundaryComponents incidence =
+  fmap (\root -> circularEdgeWalk (incidenceDirectedEdgeCount incidence) root (incidenceNext incidence))
+    . faceCycleRoots incidence
+
+faceIsolatedVertices :: PlanarIncidence -> FaceId -> [VertexId]
+faceIsolatedVertices (PlanarIncidence _ _ _ _ isolated) face =
+  fmap (VertexId . fromIntegral)
+    (IntSet.toAscList (IntMap.findWithDefault IntSet.empty (faceIdIndex face) isolated))
+
+faceInnerBoundaryComponents :: PlanarIncidence -> FaceId -> [FaceBoundaryComponent]
+faceInnerBoundaryComponents incidence@(PlanarIncidence _ _ _ components _) face =
+  fmap BoundaryCycleRoot
+    ( if face == FaceId 0
+        then faceCycleRoots incidence face
+        else IntMap.findWithDefault [] (faceIdIndex face) components
+    )
+    <> fmap IsolatedBoundaryVertex (faceIsolatedVertices incidence face)
+
+-- | Compactly supported Euler contribution of the relatively open face.
+faceEulerContribution :: PlanarIncidence -> FaceId -> Int
+faceEulerContribution incidence face = 1 - length (faceInnerBoundaryComponents incidence face)
+
+-- The fixed packed readers below were extracted from Dcel. Native hot-path
+-- callers pass their resident planes directly: no incidence wrapper allocates.
+packedOptionalEdge :: Paged Word32 -> Int -> Maybe DirectedEdgeId
+packedOptionalEdge plane index = DirectedEdgeId . fromIntegral <$> unpackOptionalIndex (pagedUnsafeIndex plane index)
+{-# INLINE packedOptionalEdge #-}
+
+packedOrigin :: Paged Word32 -> DirectedEdgeId -> VertexId
+packedOrigin topology edge = VertexId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge))
+{-# INLINE packedOrigin #-}
+
+packedNext :: Paged Word32 -> DirectedEdgeId -> DirectedEdgeId
+packedNext topology edge = DirectedEdgeId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 1))
+{-# INLINE packedNext #-}
+
+packedPrevious :: Paged Word32 -> DirectedEdgeId -> DirectedEdgeId
+packedPrevious topology edge = DirectedEdgeId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 2))
+{-# INLINE packedPrevious #-}
+
+packedIncidentFace :: Paged Word32 -> DirectedEdgeId -> FaceId
+packedIncidentFace topology edge = FaceId (pagedUnsafeIndex topology (4 * directedEdgeIdIndex edge + 3))
+{-# INLINE packedIncidentFace #-}
+
+-- | Shared fusible cycle producer, extracted unchanged from the native DCEL.
+circularEdgeWalk :: Int -> DirectedEdgeId -> (DirectedEdgeId -> DirectedEdgeId) -> [DirectedEdgeId]
+circularEdgeWalk directedCount start advance =
+  build
+    ( \(link :: DirectedEdgeId -> result -> result) (stop :: result) ->
+        let go :: Int -> DirectedEdgeId -> Bool -> result
+            go !remaining !current !visited
+              | remaining <= 0 = stop
+              | visited && current == start = stop
+              | otherwise = link current (go (remaining - 1) (advance current) True)
+         in go (directedCount + 1) start False
+    )
+{-# INLINE circularEdgeWalk #-}
+
+circularEdgeFold
+  :: forall accumulator. Int -> DirectedEdgeId -> (DirectedEdgeId -> DirectedEdgeId)
+  -> (accumulator -> DirectedEdgeId -> accumulator) -> accumulator -> accumulator
+circularEdgeFold directedCount start advance step =
+  go (directedCount + 1) start False
+ where
+  go :: Int -> DirectedEdgeId -> Bool -> accumulator -> accumulator
+  go !remaining !current !visited !accumulator
+    | remaining <= 0 = accumulator
+    | visited && current == start = accumulator
+    | otherwise =
+        let !nextAccumulator = step accumulator current
+         in go (remaining - 1) (advance current) True nextAccumulator
+{-# INLINE circularEdgeFold #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/PointIndex.hs b/src-dcel/Moonlight/Planar/Internal/PointIndex.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/PointIndex.hs
@@ -0,0 +1,378 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The one derived identity index from canonical position hashes to the
+-- vertices holding them. Geometry remains solely in the coordinate arenas:
+-- both forms store handles only, and every hit is confirmed against those
+-- authoritative coordinates. The persistent form follows a published mesh;
+-- the open-addressed form is the denser ingress representation used while a
+-- bulk load is still claiming its vertices.
+module Moonlight.Planar.Internal.PointIndex
+  ( PointIndex
+  , emptyPointIndex
+  , buildPointIndex
+  , pointIndexCandidates
+  , lookupPointIndex
+  , insertPointIndex
+  , removePointIndex
+  , relocatePointIndex
+  , MutablePointIndex
+  , MutablePointIndexUpdate (..)
+  , newMutablePointIndex
+  , seedMutablePointIndex
+  , lookupMutablePoint
+  , removeMutablePoint
+  , relocateMutablePoint
+  , resolveMutablePoint
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Control.Monad.ST (ST)
+import Data.Bits (shiftR, xor, (.&.))
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32, Word64)
+import GHC.Float (castDoubleToWord64)
+import Moonlight.Planar.Internal.Types (BuildError (PointIndexCapacityExhausted))
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Internal.Paged
+  ( Paged
+  , pagedUnsafeIndex
+  , toVector
+  )
+import Moonlight.Planar.Scalar (canonicalScalarZero)
+import qualified Data.Vector.Unboxed as U
+
+-- | Hash buckets containing only vertex handles. Callers confirm candidates
+-- against the authoritative coordinate planes.
+newtype PointIndex = PointIndex (IntMap.IntMap [Int])
+  deriving stock (Eq, Show)
+
+-- A point index is a memoized derived view. Forcing a triangulation forces the
+-- geometry and topology that determine it, not a cache no read has demanded.
+-- This is the same semantic boundary as omitting the cache from serialization.
+instance NFData PointIndex where
+  rnf _ = ()
+
+emptyPointIndex :: PointIndex
+emptyPointIndex = PointIndex IntMap.empty
+
+-- | Derive an index from the authoritative structure-of-arrays geometry.
+buildPointIndex :: Paged Double -> Paged Double -> PointIndex
+buildPointIndex pointXs pointYs =
+  U.ifoldl'
+    (\index vertex x ->
+       insertPointIndex x (pagedUnsafeIndex pointYs vertex) vertex index
+    )
+    emptyPointIndex
+    (toVector pointXs)
+
+-- | Candidate vertices sharing a position hash. Exact coordinate comparison
+-- belongs to the caller that owns the coordinate reader.
+pointIndexCandidates :: Double -> Double -> PointIndex -> [Int]
+pointIndexCandidates x y (PointIndex buckets) =
+  IntMap.findWithDefault [] (pointHash x y) buckets
+{-# INLINE pointIndexCandidates #-}
+
+-- | Resolve one exact position against the authoritative coordinate planes.
+-- Hashes reject; coordinates prove. Keeping this here makes the persistent
+-- removal and constraint schedules consume the same identity law as a mutable
+-- session instead of paying a topological point-location walk for a key lookup.
+lookupPointIndex
+  :: Paged Double
+  -> Paged Double
+  -> PointIndex
+  -> Point
+  -> Maybe Int
+lookupPointIndex pointXs pointYs pointIndex (Point x y) =
+  foldr confirm Nothing (pointIndexCandidates x y pointIndex)
+ where
+  confirm candidate resolved
+    | pagedUnsafeIndex pointXs candidate == x
+        && pagedUnsafeIndex pointYs candidate == y = Just candidate
+    | otherwise = resolved
+
+insertPointIndex :: Double -> Double -> Int -> PointIndex -> PointIndex
+insertPointIndex x y vertex (PointIndex buckets) =
+  PointIndex (IntMap.insertWith (++) (pointHash x y) [vertex] buckets)
+{-# INLINE insertPointIndex #-}
+
+-- | Forget one proved handle without touching geometry. Empty collision
+-- buckets disappear, so the index remains a derived finite view rather than a
+-- history of retired sites.
+removePointIndex :: Double -> Double -> Int -> PointIndex -> PointIndex
+removePointIndex x y vertex (PointIndex buckets) =
+  PointIndex (IntMap.update retainOthers (pointHash x y) buckets)
+ where
+  retainOthers candidates = case candidates of
+    [candidate]
+      | candidate == vertex -> Nothing
+    _ ->
+      case removeCandidate candidates of
+        [] -> Nothing
+        remaining -> Just remaining
+
+  removeCandidate [] = []
+  removeCandidate (candidate : remaining)
+    | candidate == vertex = remaining
+    | otherwise = candidate : removeCandidate remaining
+{-# INLINE removePointIndex #-}
+
+-- | Transport the tail vertex into the slot vacated by swap compaction.
+-- Coordinate storage remains authoritative; this updates handles only.
+relocatePointIndex
+  :: Double
+  -> Double
+  -> Int
+  -> Int
+  -> PointIndex
+  -> PointIndex
+relocatePointIndex x y previousVertex currentVertex (PointIndex buckets) =
+  PointIndex
+    (IntMap.alter (Just . relocateCandidate . maybe [] id) (pointHash x y) buckets)
+ where
+  relocateCandidate [] = [currentVertex]
+  relocateCandidate (candidate : remaining)
+    | candidate == previousVertex = currentVertex : remaining
+    | otherwise = candidate : relocateCandidate remaining
+{-# INLINE relocatePointIndex #-}
+
+-- The arena already holds every key, so a slot is the vertex that owns the
+-- position and nothing else. Every lookup rechecks coordinates; a failed local
+-- transport invalidates the table rather than letting it outlive that proof.
+data MutablePointIndex s = MutablePointIndex
+  { tableSlots :: !(MUV.MVector s Word32)
+  , tableMask :: {-# UNPACK #-} !Int
+  }
+
+-- | A local cache transport either preserves the proof that every slot agrees
+-- with the coordinate arenas, or explicitly gives that proof up. Callers must
+-- fall back to the lazy immutable derivation after 'MutablePointIndexInvalidated';
+-- they never publish a table whose handle correspondence was not established.
+data MutablePointIndexUpdate
+  = MutablePointIndexUpdated
+  | MutablePointIndexInvalidated
+
+vacant :: Word32
+vacant = maxBound
+
+-- | Size a table for a stated number of distinct positions.
+newMutablePointIndex :: Int -> ST s (MutablePointIndex s)
+newMutablePointIndex expected = do
+  tableSlots <- MUV.replicate capacity vacant
+  pure MutablePointIndex{tableSlots, tableMask = capacity - 1}
+ where
+  -- Two slots per key, rounded up to a power of two: linear probing stays in a
+  -- short run and the mask stands in for a division.
+  !capacity = grow 16
+  !wanted = 2 * max 1 expected
+  grow !size
+    | size >= wanted = size
+    | otherwise = grow (size * 2)
+
+-- | Seed the open-addressed section from authoritative coordinates. This is
+-- the shared ingress/batch operation: no point payload or parallel identity
+-- store crosses the boundary.
+seedMutablePointIndex
+  :: MutablePointIndex s
+  -> Int
+  -> (Int -> ST s Double)
+  -> (Int -> ST s Double)
+  -> ST s (Either BuildError ())
+seedMutablePointIndex table count readX readY = seed 0
+ where
+  seed !vertex
+    | vertex >= count = pure (Right ())
+    | otherwise = do
+        x <- readX vertex
+        y <- readY vertex
+        claimed <- resolveMutablePoint table readX readY x y vertex
+        case claimed of
+          Left failure -> pure (Left failure)
+          Right _ -> seed (vertex + 1)
+
+-- | Look up a canonical position in the mutable section. A vacant slot proves
+-- absence; all occupied candidates are confirmed against coordinate authority.
+lookupMutablePoint
+  :: forall s.
+     MutablePointIndex s
+  -> (Int -> ST s Double)
+  -> (Int -> ST s Double)
+  -> Double
+  -> Double
+  -> ST s (Maybe Int)
+lookupMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y =
+  probe (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
+ where
+  probe
+    :: Int
+    -> Int
+    -> ST s (Maybe Int)
+  probe !slot !budget
+    | budget <= 0 = pure Nothing
+    | otherwise = do
+        occupant <- MUV.unsafeRead tableSlots slot
+        if occupant == vacant
+          then pure Nothing
+          else do
+            heldX <- readX (fromIntegral occupant)
+            heldY <- readY (fromIntegral occupant)
+            if heldX == x && heldY == y
+              then pure (Just (fromIntegral occupant))
+              else probe ((slot + 1) .&. tableMask) (budget - 1)
+{-# INLINE lookupMutablePoint #-}
+
+-- | Forget a retired handle and repair its linear-probe cluster by backward
+-- shifting only entries whose home run crosses the resulting hole. The reader
+-- is needed for the surviving entries' homes; the retiring entry itself is
+-- identified by handle because swap compaction may already have overwritten
+-- its coordinate slot. An exhausted or absent proof invalidates the derived
+-- cache instead of manufacturing a lookup result.
+removeMutablePoint
+  :: forall s.
+     MutablePointIndex s
+  -> (Int -> ST s Double)
+  -> (Int -> ST s Double)
+  -> Double
+  -> Double
+  -> Int
+  -> ST s MutablePointIndexUpdate
+removeMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y retiredVertex =
+  findRetired (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
+ where
+  findRetired
+    :: Int
+    -> Int
+    -> ST s MutablePointIndexUpdate
+  findRetired !slot !budget
+    | budget <= 0 = pure MutablePointIndexInvalidated
+    | otherwise = do
+        occupant <- MUV.unsafeRead tableSlots slot
+        if occupant == vacant
+          then pure MutablePointIndexInvalidated
+          else
+            if fromIntegral occupant == retiredVertex
+              then do
+                MUV.unsafeWrite tableSlots slot vacant
+                closeProbeHole slot ((slot + 1) .&. tableMask) (budget - 1)
+              else findRetired ((slot + 1) .&. tableMask) (budget - 1)
+
+  closeProbeHole
+    :: Int
+    -> Int
+    -> Int
+    -> ST s MutablePointIndexUpdate
+  closeProbeHole !hole !slot !budget
+    | budget <= 0 =
+        MutablePointIndexInvalidated <$ MUV.unsafeWrite tableSlots hole vacant
+    | otherwise = do
+        occupant <- MUV.unsafeRead tableSlots slot
+        if occupant == vacant
+          then MutablePointIndexUpdated <$ MUV.unsafeWrite tableSlots hole vacant
+          else do
+            heldX <- readX (fromIntegral occupant)
+            heldY <- readY (fromIntegral occupant)
+            let !home = fromIntegral (mixCoordinates heldX heldY) .&. tableMask
+                !distanceToHole = (hole - home) .&. tableMask
+                !distanceToSlot = (slot - home) .&. tableMask
+            if distanceToHole < distanceToSlot
+              then do
+                MUV.unsafeWrite tableSlots hole occupant
+                closeProbeHole slot ((slot + 1) .&. tableMask) (budget - 1)
+              else closeProbeHole hole ((slot + 1) .&. tableMask) (budget - 1)
+{-# INLINE removeMutablePoint #-}
+
+-- | Rename the tail handle after DCEL swap compaction. Failure remains a
+-- typed cache obstruction, so its caller drops the table and later identity
+-- questions descend from geometry rather than trusting an unproved cache.
+relocateMutablePoint
+  :: forall s.
+     MutablePointIndex s
+  -> Double
+  -> Double
+  -> Int
+  -> Int
+  -> ST s MutablePointIndexUpdate
+relocateMutablePoint MutablePointIndex{tableSlots, tableMask} x y previousVertex currentVertex =
+  findPrevious (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
+ where
+  findPrevious
+    :: Int
+    -> Int
+    -> ST s MutablePointIndexUpdate
+  findPrevious !slot !budget
+    | budget <= 0 = pure MutablePointIndexInvalidated
+    | otherwise = do
+        occupant <- MUV.unsafeRead tableSlots slot
+        if occupant == vacant
+          then pure MutablePointIndexInvalidated
+          else
+            if fromIntegral occupant == previousVertex
+              then
+                MutablePointIndexUpdated
+                  <$ MUV.unsafeWrite tableSlots slot (fromIntegral currentVertex)
+              else findPrevious ((slot + 1) .&. tableMask) (budget - 1)
+{-# INLINE relocateMutablePoint #-}
+
+-- | Answer the vertex already holding a canonical position, or claim the
+-- position for @candidate@ and answer 'Nothing'. The position travels as raw
+-- coordinates — the v'Point' constructor is the cold boundary's packaging and
+-- has no business on the ingress path. The caller supplies the arena's
+-- coordinate reader, so a claim is only sound if @candidate@ is the very next
+-- vertex the arena will append.
+resolveMutablePoint
+  :: forall s.
+     MutablePointIndex s
+  -> (Int -> ST s Double)
+  -> (Int -> ST s Double)
+  -> Double
+  -> Double
+  -> Int
+  -> ST s (Either BuildError (Maybe Int))
+resolveMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y candidate =
+  probe (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
+ where
+  probe
+    :: Int
+    -> Int
+    -> ST s (Either BuildError (Maybe Int))
+  probe !slot !budget
+    | budget <= 0 = pure (Left (PointIndexCapacityExhausted (MUV.length tableSlots)))
+    | otherwise = do
+        occupant <- MUV.unsafeRead tableSlots slot
+        if occupant == vacant
+          then Right Nothing <$ MUV.unsafeWrite tableSlots slot (fromIntegral candidate)
+          else do
+            heldX <- readX (fromIntegral occupant)
+            heldY <- readY (fromIntegral occupant)
+            if heldX == x && heldY == y
+              then pure (Right (Just (fromIntegral occupant)))
+              else probe ((slot + 1) .&. tableMask) (budget - 1)
+
+-- Inlined rather than merely specialized: the coordinate reader arrives as an
+-- argument, so until the probe loop lands at its call site every collision pays
+-- an unknown call and a boxed pair for a read the caller could have made
+-- directly.
+{-# INLINE resolveMutablePoint #-}
+
+mixCoordinates :: Double -> Double -> Word64
+mixCoordinates x y =
+  mix
+    ( castDoubleToWord64 (canonicalScalarZero x)
+        `xor` mix (castDoubleToWord64 (canonicalScalarZero y))
+    )
+{-# INLINE mixCoordinates #-}
+
+mix :: Word64 -> Word64
+mix raw =
+  let !z0 = raw + 0x9e3779b97f4a7c15
+      !z1 = (z0 `xor` (z0 `shiftR` 30)) * 0xbf58476d1ce4e5b9
+      !z2 = (z1 `xor` (z1 `shiftR` 27)) * 0x94d049bb133111eb
+   in z2 `xor` (z2 `shiftR` 31)
+{-# INLINE mix #-}
+
+pointHash :: Double -> Double -> Int
+pointHash x y = fromIntegral (mixCoordinates x y)
+{-# INLINE pointHash #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/Predicates.hs b/src-dcel/Moonlight/Planar/Internal/Predicates.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Predicates.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE BangPatterns #-}
+{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
+
+-- | Exact predicates over points already admitted by a construction or query
+-- boundary.  This module is package-private: public callers must carry
+-- 'QueryPoint' evidence through "Moonlight.Planar.Math" instead of
+-- presenting arbitrary IEEE-754 payloads to an exact classifier.
+module Moonlight.Planar.Internal.Predicates
+  ( orient2dCoordinates
+  , inCircleCoordinates
+  , orient2d
+  , sideQuery
+  , inCircle
+  , compareSquaredDistances
+  , onClosedSegment
+  , segmentRelation
+  , segmentsProperlyCross
+  , segmentsIntersect
+  , inDiametralCircle
+  ) where
+
+import Moonlight.Planar.Internal.Dyadic
+  ( exactDiametralDot
+  , exactInCircleSignDouble
+  , exactOrientSignDouble
+  , exactSquaredDistanceOrdering
+  )
+import Moonlight.Planar.Internal.SegmentRelation
+  ( SegmentRelation (..)
+  , segmentRelationWith
+  )
+import Moonlight.Planar.LineSideInfo (LineSideInfo, fromOrdering)
+import Moonlight.Planar.Scalar
+  ( isFinite
+  , scalarCcwErrorBound
+  , scalarInCircleErrorBound
+  )
+import Moonlight.Planar.Point (Point (..))
+
+-- The coordinate kernels are package-private because their raw 'Double'
+-- arguments carry no finite-range evidence. Every public exact classifier
+-- descends through 'QueryPoint'; internal hot paths call these only after their
+-- construction boundary has admitted the stored coordinates.
+orient2dCoordinates
+  :: Double -> Double -> Double -> Double -> Double -> Double
+  -> Ordering
+orient2dCoordinates ax ay bx by cx cy
+  | abs determinant > scalarCcwErrorBound * determinantSum = compare determinant 0
+  | otherwise = exactOrientSignDouble ax ay bx by cx cy
+ where
+  !left = (ax - cx) * (by - cy)
+  !right = (ay - cy) * (bx - cx)
+  !determinant = left - right
+  !determinantSum = abs left + abs right
+{-# INLINE orient2dCoordinates #-}
+
+inCircleCoordinates
+  :: Double -> Double -> Double -> Double
+  -> Double -> Double -> Double -> Double
+  -> Ordering
+inCircleCoordinates ax ay bx by cx cy dx dy
+  | abs determinant > scalarInCircleErrorBound * permanent = compare determinant 0
+  | otherwise = exactInCircleSignDouble ax ay bx by cx cy dx dy
+ where
+  !adx = ax - dx
+  !ady = ay - dy
+  !bdx = bx - dx
+  !bdy = by - dy
+  !cdx = cx - dx
+  !cdy = cy - dy
+  !abdet = adx * bdy - bdx * ady
+  !bcdet = bdx * cdy - cdx * bdy
+  !cadet = cdx * ady - adx * cdy
+  !alift = adx * adx + ady * ady
+  !blift = bdx * bdx + bdy * bdy
+  !clift = cdx * cdx + cdy * cdy
+  !determinant = alift * bcdet + blift * cadet + clift * abdet
+  !permanent =
+    (abs (bdx * cdy) + abs (cdx * bdy)) * alift
+      + (abs (cdx * ady) + abs (adx * cdy)) * blift
+      + (abs (adx * bdy) + abs (bdx * ady)) * clift
+{-# INLINE inCircleCoordinates #-}
+
+-- | Exact orientation of points whose coordinates have already been admitted.
+orient2d :: Point -> Point -> Point -> Ordering
+orient2d (Point ax ay) (Point bx by) (Point cx cy) =
+  orient2dCoordinates ax ay bx by cx cy
+{-# INLINE orient2d #-}
+
+-- | Exact side of an admitted oriented line.
+sideQuery :: Point -> Point -> Point -> LineSideInfo
+sideQuery from to query = fromOrdering (orient2d from to query)
+{-# INLINE sideQuery #-}
+
+-- | Ordering of the admitted oriented incircle determinant.
+inCircle :: Point -> Point -> Point -> Point -> Ordering
+inCircle
+  (Point ax ay)
+  (Point bx by)
+  (Point cx cy)
+  (Point dx dy) =
+    inCircleCoordinates ax ay bx by cx cy dx dy
+{-# INLINE inCircle #-}
+
+-- | Exact ordering of squared distances from an admitted query to two admitted
+-- sites. The result compares the first site's distance with the second's.
+compareSquaredDistances :: Point -> Point -> Point -> Ordering
+compareSquaredDistances (Point qx qy) (Point ax ay) (Point bx by) =
+  exactSquaredDistanceOrdering qx qy ax ay bx by
+{-# INLINE compareSquaredDistances #-}
+
+-- | Whether an admitted point lies on an admitted closed segment.
+onClosedSegment :: Point -> Point -> Point -> Bool
+onClosedSegment from@(Point fromX fromY) to@(Point toX toY) query@(Point queryX queryY) =
+  orient2d from to query == EQ
+    && queryX >= min fromX toX
+    && queryX <= max fromX toX
+    && queryY >= min fromY toY
+    && queryY <= max fromY toY
+{-# INLINE onClosedSegment #-}
+
+-- | Exact relation between admitted closed segments.
+segmentRelation :: Point -> Point -> Point -> Point -> SegmentRelation
+segmentRelation = segmentRelationWith (==) compare orient2d onClosedSegment
+
+-- | Whether two admitted closed segments share any point.
+segmentsIntersect :: Point -> Point -> Point -> Point -> Bool
+segmentsIntersect firstFrom firstTo secondFrom secondTo =
+  segmentRelation firstFrom firstTo secondFrom secondTo /= SegmentsDisjoint
+
+-- | Whether two admitted segments cross away from their endpoints.
+segmentsProperlyCross :: Point -> Point -> Point -> Point -> Bool
+segmentsProperlyCross firstFrom firstTo secondFrom secondTo =
+  opposite (orient2d firstFrom firstTo secondFrom) (orient2d firstFrom firstTo secondTo)
+    && opposite (orient2d secondFrom secondTo firstFrom) (orient2d secondFrom secondTo firstTo)
+ where
+  opposite LT GT = True
+  opposite GT LT = True
+  opposite _ _ = False
+
+-- | Whether an admitted point lies in the closed diametral disk of an admitted
+-- segment.
+inDiametralCircle :: Point -> Point -> Point -> Bool
+inDiametralCircle (Point ax ay) (Point bx by) (Point px py)
+  | isFinite dot && abs dot > scalarCcwErrorBound * dotSum = dot < 0
+  | otherwise = exactDiametralDot ax ay bx by px py <= 0
+ where
+  !left = (ax - px) * (bx - px)
+  !right = (ay - py) * (by - py)
+  !dot = left + right
+  !dotSum = abs left + abs right
+{-# INLINE inDiametralCircle #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/Region/Bounds.hs b/src-dcel/Moonlight/Planar/Internal/Region/Bounds.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Region/Bounds.hs
@@ -0,0 +1,160 @@
+-- | Exact axis-aligned candidate bounds for admitted region geometry. Bounds
+-- prune impossible overlap obligations; exact predicates remain authoritative.
+module Moonlight.Planar.Internal.Region.Bounds
+  ( exactLoopBounds
+  , componentBounds
+  , regionBounds
+  , overlappingPairs
+  , overlappingOptionalPairs
+  , overlappingPairsBetween
+  , overlappingPredecessors
+  ) where
+
+import Data.List (sortOn, tails)
+import qualified Data.List as List
+import qualified Data.IntMap.Strict as IntMap
+import Moonlight.Planar.Exact
+  ( ExactBounds
+  , exactPointsBounds
+  , boundsUnion
+  , boundsOverlap
+  , boundsMinimumX
+  , boundsMaximumX
+  )
+import Moonlight.Planar.Internal.Region.Types
+  ( ExactLoop (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , polygonOuterLoop
+  )
+
+exactLoopBounds :: ExactLoop -> ExactBounds
+exactLoopBounds (ExactLoop points) = exactPointsBounds points
+
+componentBounds :: PolygonComponent -> ExactBounds
+componentBounds = exactLoopBounds . polygonOuterLoop
+
+regionBounds :: PlanarRegion -> Maybe ExactBounds
+regionBounds (PlanarRegion components) =
+  case components of
+    [] -> Nothing
+    firstComponent : remaining ->
+      Just
+        ( List.foldl'
+            (\bounds component -> boundsUnion bounds (componentBounds component))
+            (componentBounds firstComponent)
+            remaining
+        )
+
+overlappingPairs :: (value -> ExactBounds) -> [value] -> [(value, value)]
+overlappingPairs boundsOf =
+  overlappingIndexedValuePairs
+    . zipWith (\index value -> (index, boundsOf value, value)) [0 ..]
+
+overlappingOptionalPairs
+  :: (value -> Maybe ExactBounds)
+  -> [value]
+  -> [(value, value)]
+overlappingOptionalPairs boundsOf =
+  overlappingIndexedValuePairs
+    . concatMap
+      (\(index, value) ->
+         case boundsOf value of
+           Nothing -> []
+           Just bounds -> [(index, bounds, value)])
+    . zip [0 ..]
+
+overlappingPairsBetween
+  :: (value -> ExactBounds)
+  -> [value]
+  -> [value]
+  -> [(value, value)]
+overlappingPairsBetween boundsOf leftValues rightValues =
+  [ pair
+  | (firstBounds, firstValue) : remaining <- tails ordered
+  , (secondBounds, secondValue) <-
+      takeWhile
+        (\(bounds, _) -> boundsMinimumX bounds <= boundsMaximumX firstBounds)
+        remaining
+  , pair <- crossPair firstValue secondValue
+  , boundsOverlap firstBounds secondBounds
+  ]
+ where
+  ordered =
+    sortOn
+      (boundsMinimumX . fst)
+      ( map
+          (\value -> (either boundsOf boundsOf value, value))
+          (map Left leftValues <> map Right rightValues)
+      )
+  crossPair :: Either value value -> Either value value -> [(value, value)]
+  crossPair (Left leftValue) (Right rightValue) = [(leftValue, rightValue)]
+  crossPair (Right rightValue) (Left leftValue) = [(leftValue, rightValue)]
+  crossPair _ _ = []
+
+-- | Each value together with only the earlier input values whose exact bounds
+-- overlap it. This is the local cover used when a fold glues one component at
+-- a time and needs the complete overlap section accumulated so far.
+overlappingPredecessors
+  :: (value -> ExactBounds)
+  -> [value]
+  -> [(value, [value])]
+overlappingPredecessors boundsOf values =
+  [ ( value
+    , map snd
+        ( sortOn fst
+            (IntMap.findWithDefault [] index predecessorsByIndex)
+        )
+    )
+  | (index, value) <- zip [0 ..] values
+  ]
+ where
+  indexed = zipWith (\index value -> (index, boundsOf value, value)) [0 ..] values
+  predecessorsByIndex =
+    IntMap.fromListWith (<>)
+      [ (rightIndex, [(leftIndex, leftValue)])
+      | (leftIndex, rightIndex, leftValue, _) <- overlappingIndexedEntries indexed
+      ]
+
+-- The authoring path retains its nested result pair because the flattened
+-- four-field candidate increased allocation on the registered 1,024-component
+-- workload. The predecessor path below needs the indices after selection and
+-- therefore carries the indexed specialization separately.
+overlappingIndexedValuePairs
+  :: [(Int, ExactBounds, value)]
+  -> [(value, value)]
+overlappingIndexedValuePairs indexed =
+  map (\(_, _, pair) -> pair)
+    ( sortOn (\(leftIndex, rightIndex, _) -> (leftIndex, rightIndex))
+        [ if leftIndex <= rightIndex
+            then (leftIndex, rightIndex, (leftValue, rightValue))
+            else (rightIndex, leftIndex, (rightValue, leftValue))
+        | (leftIndex, leftBounds, leftValue) : remaining <- tails ordered
+        , (rightIndex, rightBounds, rightValue) <-
+            takeWhile
+              (\(_, bounds, _) -> boundsMinimumX bounds <= boundsMaximumX leftBounds)
+              remaining
+        , boundsOverlap leftBounds rightBounds
+        ]
+    )
+ where
+  ordered = sortOn (\(_, bounds, _) -> boundsMinimumX bounds) indexed
+
+overlappingIndexedEntries
+  :: [(Int, ExactBounds, value)]
+  -> [(Int, Int, value, value)]
+overlappingIndexedEntries indexed =
+  sortOn
+    (\(leftIndex, rightIndex, _, _) -> (leftIndex, rightIndex))
+    [ if leftIndex <= rightIndex
+        then (leftIndex, rightIndex, leftValue, rightValue)
+        else (rightIndex, leftIndex, rightValue, leftValue)
+    | (leftIndex, leftBounds, leftValue) : remaining <- tails ordered
+    , (rightIndex, rightBounds, rightValue) <-
+        takeWhile
+          (\(_, bounds, _) -> boundsMinimumX bounds <= boundsMaximumX leftBounds)
+          remaining
+    , boundsOverlap leftBounds rightBounds
+    ]
+ where
+  ordered = sortOn (\(_, bounds, _) -> boundsMinimumX bounds) indexed
diff --git a/src-dcel/Moonlight/Planar/Internal/Region/Publication.hs b/src-dcel/Moonlight/Planar/Internal/Region/Publication.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Region/Publication.hs
@@ -0,0 +1,249 @@
+-- | Trusted exact-coordinate publication from admitted planar incidence.
+-- The callback-bearing entrances are internal because only a sealed
+-- downstream carrier may prove that its exact coordinate section belongs to
+-- the supplied topology.
+module Moonlight.Planar.Internal.Region.Publication
+  ( labelledPlanarLayer
+  , labelledPlanarLayerFromExactCoordinates
+  , planarRegionFromSelectedIncidence
+  , planarLayerFromAdmittedComponents
+  , polygonComponentFromBoundaryCoordinates
+  ) where
+
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import qualified Data.Graph as Graph
+import Data.List (partition, sort)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import Moonlight.Planar.Dcel (vertexPoint)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , exactOnClosedSegment
+  , exactOrient2d
+  , exactPointFromPoint
+  )
+import Moonlight.Planar.FloodFillIterator
+  ( BoundaryLoop
+  , BoundaryObstruction (..)
+  , RegionBoundary
+  , boundaryLoopVertices
+  , componentBoundary
+  , faceComponents
+  , labelledRegionBoundaries
+  , regionBoundaryHoleLoops
+  , regionBoundaryOuterLoop
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId
+  , FaceId (..)
+  , VertexId
+  , reverseEdge
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( rotateCycleLeast
+  , simplifyBoundaryCycle
+  , traceOrientedBoundaryCircuits
+  )
+import Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , faceBoundaryComponents
+  , incidenceDestination
+  , incidenceFaceCount
+  , incidenceIncidentFace
+  , incidenceOrigin
+  )
+import Moonlight.Planar.Internal.Region.Types
+  ( ExactLoop (..)
+  , PlanarLayer (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , RegionPublicationError (..)
+  , RegionValidationError (..)
+  )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+
+-- | Publish bounded resident face labels through the existing component and
+-- boundary owners, then lift only traced boundary coordinates exactly.
+labelledPlanarLayer
+  :: Ord label
+  => label
+  -> Triangulation mode vertex directed undirected face
+  -> (FaceId -> label)
+  -> Either RegionPublicationError (PlanarLayer label)
+labelledPlanarLayer outside triangulation labelFace = do
+  labelledBoundaries <-
+    first RegionBoundaryObstruction
+      (labelledRegionBoundaries triangulation labelFace)
+  labelledComponents <-
+    traverse
+      (\(label, boundary) ->
+         (label,) <$> polygonComponentFromBoundaryCoordinates exactPointAt boundary)
+      [ pair
+      | pair@(label, _) <- labelledBoundaries
+      , label /= outside
+      ]
+  pure (planarLayerFromAdmittedComponents outside labelledComponents)
+ where
+  exactPointAt vertex =
+    first (RegionCoordinateObstruction vertex)
+      (exactPointFromPoint (vertexPoint triangulation vertex))
+
+-- | Publish bounded face labels using the exact-coordinate section belonging
+-- to the resident carrier. Topology has already been proved by
+-- 'labelledRegionBoundaries'; this path performs exact simplification but does
+-- not send derived loops back through authoring event sweeps.
+labelledPlanarLayerFromExactCoordinates
+  :: Ord label
+  => label
+  -> Triangulation mode vertex directed undirected face
+  -> (VertexId -> Either RegionPublicationError ExactPoint)
+  -> (FaceId -> Either RegionPublicationError label)
+  -> Either RegionPublicationError (PlanarLayer label)
+labelledPlanarLayerFromExactCoordinates outside triangulation exactPointAt labelFace = do
+  labelledComponents <-
+    traverse
+      (\(labelResult, component) -> do
+         label <- labelResult
+         boundary <-
+           first RegionBoundaryObstruction
+             (componentBoundary triangulation component)
+         (label,) <$> polygonComponentFromBoundaryCoordinates exactPointAt boundary)
+      [ pair
+      | pair@(labelResult, _) <- faceComponents triangulation labelFace
+      , labelResult /= Right outside
+      ]
+  pure (planarLayerFromAdmittedComponents outside labelledComponents)
+
+-- | Publish exactly the selected bounded faces, cancelling each internal
+-- twin pair before tracing. Face boundary components are not polygon loops:
+-- a face can have holes, bridges, or a pinched boundary. Edge-connected face
+-- components are therefore glued first, then their frontier is projected.
+--
+-- Separate components may touch at vertices. Multiple outgoing frontier
+-- darts in one connected component instead witness a pinch and refuse only
+-- this strict polygon observation; the underlying exact selection remains
+-- valid. Isolated vertices and bridges internal to a selected face make no
+-- contribution to its closed polygon boundary.
+planarRegionFromSelectedIncidence
+  :: PlanarIncidence
+  -> (VertexId -> Either RegionPublicationError ExactPoint)
+  -> (FaceId -> Bool)
+  -> Either RegionPublicationError PlanarRegion
+planarRegionFromSelectedIncidence incidence exactPointAt selected
+  | selected (FaceId 0) = Left RegionUnboundedSelection
+  | otherwise =
+      PlanarRegion . sort
+        <$> traverse
+          (publishComponent . concat . Graph.flattenSCC)
+          ( Graph.stronglyConnComp
+              [ (edges, face, map oppositeFace edges)
+              | (face, edges) <- selectedFaceEdges
+              ]
+          )
+ where
+  selectedFaceEdges =
+    [ (face, concat (faceBoundaryComponents incidence face))
+    | index <- [1 .. incidenceFaceCount incidence - 1]
+    , let face = FaceId (fromIntegral index)
+    , selected face
+    ]
+  selectedFaces = Set.fromDistinctAscList (map fst selectedFaceEdges)
+  oppositeFace = incidenceIncidentFace incidence . reverseEdge
+
+  publishComponent
+    :: [DirectedEdgeId]
+    -> Either RegionPublicationError PolygonComponent
+  publishComponent componentEdges = do
+    let frontier =
+          Set.fromList
+            (filter ((`Set.notMember` selectedFaces) . oppositeFace) componentEdges)
+        outgoing =
+          Map.fromListWith (<>)
+            [ (incidenceOrigin incidence edge, [edge])
+            | edge <- Set.toAscList frontier
+            ]
+    traverse_ rejectPinch (Map.toAscList outgoing)
+    circuits <-
+      first RegionBoundaryObstruction
+        ( traceOrientedBoundaryCircuits
+            (incidenceOrigin incidence)
+            (incidenceDestination incidence)
+            BoundaryCycleDidNotClose
+            outgoing
+            frontier
+        )
+    classified <-
+      traverse
+        (\vertices -> traverse exactPointAt vertices >>= admittedDerivedLoop)
+        circuits
+    let (outerLoops, holeLoops) = partition ((== GT) . fst) classified
+    case map snd outerLoops of
+      [outer] -> Right (PolygonComponent outer (sort (map snd holeLoops)))
+      outer ->
+        Left
+          (RegionBoundaryObstruction (BoundaryOuterLoopCardinality (length outer)))
+
+  rejectPinch
+    :: (VertexId, [DirectedEdgeId])
+    -> Either RegionPublicationError ()
+  rejectPinch (vertex, firstEdge : secondEdge : _) =
+    Left (RegionBoundaryObstruction (BoundaryPinch vertex firstEdge secondEdge))
+  rejectPinch _ = Right ()
+
+-- | Glue already-admitted, pairwise interior-disjoint components by label.
+-- Both DCEL publication and exact overlay cells reach this point only after
+-- their topology owner has proved those obligations.
+planarLayerFromAdmittedComponents
+  :: Ord label
+  => label
+  -> [(label, PolygonComponent)]
+  -> PlanarLayer label
+planarLayerFromAdmittedComponents outside labelledComponents =
+  PlanarLayer
+    outside
+    ( Map.map
+        (PlanarRegion . sort)
+        ( Map.fromListWith (<>)
+            [(label, [component]) | (label, component) <- labelledComponents]
+        )
+    )
+
+-- | Convert one already-traced resident component boundary against the exact
+-- coordinate carrier admitted for that same resident topology.
+polygonComponentFromBoundaryCoordinates
+  :: (VertexId -> Either RegionPublicationError ExactPoint)
+  -> RegionBoundary
+  -> Either RegionPublicationError PolygonComponent
+polygonComponentFromBoundaryCoordinates exactPointAt boundary = do
+  outer <- convertLoop (regionBoundaryOuterLoop boundary)
+  holes <- traverse convertLoop (regionBoundaryHoleLoops boundary)
+  pure (PolygonComponent outer (sort holes))
+ where
+  convertLoop :: BoundaryLoop -> Either RegionPublicationError ExactLoop
+  convertLoop loop = do
+    points <- traverse exactPointAt (boundaryLoopVertices loop)
+    snd <$> admittedDerivedLoop points
+
+-- | Boundary descent already proves simplicity, winding, and component
+-- compatibility. Exact simplification remains necessary because the exact
+-- carrier may expose a collinearity that the embedded boundary retained.
+admittedDerivedLoop
+  :: NonEmpty ExactPoint
+  -> Either RegionPublicationError (Ordering, ExactLoop)
+admittedDerivedLoop points = do
+  (winding, simplified) <-
+    first RegionValidationObstruction
+      ( simplifyBoundaryCycle
+          RegionLoopDegenerate
+          (\previous current next ->
+             exactOrient2d previous current next == EQ
+               && exactOnClosedSegment previous next current)
+          exactOrient2d
+          id
+          (NonEmpty.toList points)
+      )
+  pure (winding, ExactLoop (rotateCycleLeast simplified))
+{-# INLINE admittedDerivedLoop #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/Region/Types.hs b/src-dcel/Moonlight/Planar/Internal/Region/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Region/Types.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | The invariant-bearing exact region carriers. Public construction and
+-- validation remain in "Moonlight.Planar.Region"; downstream build-tier
+-- algorithms import this owner only when their algebra proves the constructors'
+-- obligations directly.
+module Moonlight.Planar.Internal.Region.Types
+  ( ExactLoop (..)
+  , ConvexPolygon (..)
+  , PolygonComponent (..)
+  , polygonOuterLoop
+  , polygonHoleLoops
+  , PlanarRegion (..)
+  , RegionPointLocation (..)
+  , PlanarLayer (..)
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  , RegionValidationError (..)
+  , RegionPublicationError (..)
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.List.NonEmpty (NonEmpty)
+import Data.Map.Strict (Map)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , SegmentRelation
+  )
+import Moonlight.Planar.FloodFillIterator (BoundaryObstruction)
+import Moonlight.Planar.Internal.HandleDefs (FaceId, VertexId)
+import Moonlight.Planar.Internal.ExactSegmentEvents
+  ( ExactSegmentEventObstruction
+  )
+import Moonlight.Planar.Point (PointValidationError)
+
+-- | One admitted, simple exact cycle. Its first point is the least exact point
+-- on the cycle, so equality does not retain an authoring rotation.
+newtype ExactLoop = ExactLoop (NonEmpty ExactPoint)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ExactLoop where
+  rnf (ExactLoop points) = rnf points
+  {-# INLINE rnf #-}
+
+-- | One canonical, strictly convex counter-clockwise exact loop. Checked
+-- construction belongs to "Moonlight.Planar.Convex"; geometric algorithms
+-- may publish directly only when their algebra discharges these obligations.
+newtype ConvexPolygon = ConvexPolygon ExactLoop
+  deriving stock (Eq, Ord, Show)
+
+instance NFData ConvexPolygon where
+  rnf (ConvexPolygon loop) = rnf loop
+  {-# INLINE rnf #-}
+
+-- | One connected polygonal component: a counter-clockwise outer cycle and
+-- zero or more clockwise holes.
+data PolygonComponent = PolygonComponent !ExactLoop ![ExactLoop]
+  deriving stock (Eq, Ord, Show)
+
+instance NFData PolygonComponent where
+  rnf (PolygonComponent outer holes) = rnf outer `seq` rnf holes
+  {-# INLINE rnf #-}
+
+-- | Read-only outer boundary; changing it requires component admission.
+polygonOuterLoop :: PolygonComponent -> ExactLoop
+polygonOuterLoop (PolygonComponent outer _) = outer
+{-# INLINE polygonOuterLoop #-}
+
+-- | Read-only hole boundaries; changing them requires component admission.
+polygonHoleLoops :: PolygonComponent -> [ExactLoop]
+polygonHoleLoops (PolygonComponent _ holes) = holes
+{-# INLINE polygonHoleLoops #-}
+
+-- | A finite union of components with pairwise-disjoint interiors.
+newtype PlanarRegion = PlanarRegion [PolygonComponent]
+  deriving stock (Eq, Ord, Show)
+
+instance NFData PlanarRegion where
+  rnf (PlanarRegion components) = rnf components
+  {-# INLINE rnf #-}
+
+-- | Exact point position relative to a closed region.
+data RegionPointLocation
+  = RegionExterior
+  | RegionOnBoundary
+  | RegionInterior
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A finite labelled planar layer. The outside label is implicit and may not
+-- also own a bounded region.
+data PlanarLayer label = PlanarLayer !label !(Map label PlanarRegion)
+  deriving stock (Eq, Ord, Show)
+
+instance NFData label => NFData (PlanarLayer label) where
+  rnf (PlanarLayer outside regions) = rnf outside `seq` rnf regions
+  {-# INLINE rnf #-}
+
+-- | Read-only outside label; relabelling requires layer admission.
+planarLayerOutsideLabel :: PlanarLayer label -> label
+planarLayerOutsideLabel (PlanarLayer outside _) = outside
+{-# INLINE planarLayerOutsideLabel #-}
+
+-- | Read-only bounded regions; replacement requires layer admission.
+planarLayerRegions :: PlanarLayer label -> Map label PlanarRegion
+planarLayerRegions (PlanarLayer _ regions) = regions
+{-# INLINE planarLayerRegions #-}
+
+-- | Typed authoring obstructions. Indices are cycle/component positions in the
+-- submitted value and every geometric relation preserves its exact witness.
+data RegionValidationError
+  = RegionLoopDegenerate ![ExactPoint]
+  | RegionLoopSelfRelation !Int !Int !SegmentRelation
+  | RegionOuterLoopWinding !Ordering
+  | RegionHoleLoopWinding !Int !Ordering
+  | RegionHoleLocation !Int !RegionPointLocation
+  | RegionBoundaryRelation !Int !Int !SegmentRelation
+  | RegionComponentInteriorOverlap !Int !Int
+  | RegionLayerInteriorOverlap !Int !Int
+  | RegionOutsideLabelUsed
+  | RegionSegmentEventsInvalid !ExactSegmentEventObstruction
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Typed failures while publishing an existing triangulation as exact region
+-- values. The handle is retained when a resident coordinate is inadmissible.
+data RegionPublicationError
+  = RegionBoundaryObstruction !BoundaryObstruction
+  | RegionCoordinateObstruction !VertexId !PointValidationError
+  | RegionCoordinateMissing !VertexId
+  | RegionFaceLabelMissing !FaceId
+  | RegionValidationObstruction !RegionValidationError
+  | RegionUnboundedSelection
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Planar/Internal/Representation.hs b/src-dcel/Moonlight/Planar/Internal/Representation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Representation.hs
@@ -0,0 +1,632 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RoleAnnotations #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The stored representation: the structure-of-arrays mesh, the payload
+-- traversals that reach its four free parameters, and the records that carry a
+-- built mesh beside its telemetry.
+module Moonlight.Planar.Internal.Representation
+  ( Triangulation (..)
+  , SeamFrontierIndex (..)
+  , CanonicalAdmission (..)
+  , prepareSeamFrontierIndex
+  , summarizeSeamChart
+  , seamQuarterTurn
+  , geometryOnlyPublication
+  , promoteConstrained
+  , authoringElementDefaults
+  , PayloadTraversal
+  , vertexPayloads
+  , directedPayloads
+  , undirectedPayloads
+  , facePayloads
+  , mapVertices
+  , mapDirectedEdges
+  , mapUndirectedEdges
+  , mapFaces
+  , imapUndirectedEdges
+  , imapFaces
+  , DelaunayTriangulation
+  , ConstrainedDelaunayTriangulation
+  , BuildResult (..)
+  , InsertionResult (..)
+  , RefinementReceipt (..)
+  , RefinementDomainResult (..)
+  , RefinementResult (..)
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.Foldable (toList)
+import qualified Data.IntSet as IntSet
+import Data.List (unfoldr)
+import qualified Data.List as List
+import Data.Primitive.PrimArray (PrimArray)
+import qualified Data.Sequence as Seq
+import Data.Traversable (foldMapDefault)
+import qualified Data.Vector as V
+import Data.Word (Word8, Word32)
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  )
+import Moonlight.Planar.Internal.PackedIndex (unpackIndex, unpackOptionalIndex)
+import Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedPaged, BoxedFill (..), FillRequirement (..)
+  , boxedDefaulted, requiredBoxedFill, boxedFromVector, boxedToVector )
+import Moonlight.Planar.Internal.Paged
+  ( Paged
+  , PublicationStats
+  , pagedLength
+  , pagedUnsafeIndex
+  )
+import Moonlight.Planar.Internal.PointIndex (PointIndex)
+import Moonlight.Planar.Internal.Types (ConstraintMode (..), ElementDefaults (..), InsertionDisposition, ClosureStats)
+import Moonlight.Planar.BuildStats (BuildStats)
+import GHC.Generics (Generic)
+
+-- | Immutable finite DCEL. The coordinate pages own the geometry; vertex
+-- payloads are free annotations carried alongside it.
+-- 'Moonlight.Planar.Internal.Types.HasPosition' is how a point is read
+-- out of a payload at the moment of
+-- ingestion and is not consulted again, so a payload whose instance later
+-- disagrees with where its vertex sits is not a corrupt triangulation — it is a
+-- payload nobody asks about position. Half-edge topology is one interleaved
+-- arena: edge @e@ owns slots @4e..4e+3@ holding origin, next, previous and
+-- face, so a twin pair is one contiguous eight-word record.
+-- Directed edges are adjacent twin pairs, so reversal is an XOR with one.
+-- Face zero is the unique outer face. Constraint flags are stored once per
+-- undirected edge and are zero for ordinary Delaunay triangulations.
+-- All four payload components are therefore representational: coercing a
+-- newtype through any of them is a coercion, not a rebuild.
+type role Triangulation nominal representational representational representational representational
+
+data Triangulation (mode :: ConstraintMode) vertex directed undirected face = Triangulation
+  { triPointX :: !(Paged Double)
+  , triPointY :: !(Paged Double)
+  , -- | Derived position-hash buckets containing vertex handles only. This is
+    -- deliberately lazy: geometry is authoritative, so a workload that never
+    -- asks an identity question owes no cache construction.
+    triPointIndex :: PointIndex
+  , triVertexOut :: !(Paged Word32)
+  , triVertexData :: !(BoxedPaged 'OptionalFill vertex)
+  , triHalfTopology :: !(Paged Word32)
+  , triDirectedData :: !(BoxedPaged 'RequiredFill directed)
+  , triUndirectedData :: !(BoxedPaged 'RequiredFill undirected)
+  , triFaceEdge :: !(Paged Word32)
+  , triFaceData :: !(BoxedPaged 'RequiredFill face)
+  , triConstraint :: !(Paged Word8)
+  , triConstraintCount :: {-# UNPACK #-} !Int
+  , -- | Derived exact membership for the sparse constrained edge section.
+    -- The flag plane remains authoritative and serializable; this index is
+    -- transported with edge rewrites so constraint-only queries need not scan
+    -- every ordinary Delaunay edge.
+    triConstraintEdges :: !IntSet.IntSet
+  , -- | Derived outer-frontier handles for persistent separated joins. The
+    -- geometry/topology planes remain authoritative; this cache is omitted
+    -- from equality and serialization and is rebuilt only at an explicit
+    -- geometry-only preparation boundary.
+    triSeamFrontier :: !(Maybe SeamFrontierIndex)
+  , -- | Whether the numbering is already the canonical representative.
+    -- Minted only by canonical publication, forgotten by every thaw, and,
+    -- like the caches, absent from equality and serialization.
+    triCanonical :: !CanonicalAdmission
+  }
+  deriving stock (Show)
+
+-- Geometry/topology compatibility is admitted by construction. Generic
+-- reconstruction could splice coordinate planes without transporting topology.
+instance
+  (NFData vertex, NFData directed, NFData undirected, NFData face)
+  => NFData (Triangulation mode vertex directed undirected face) where
+  rnf triangulation =
+    rnf (triPointX triangulation)
+      `seq` rnf (triPointY triangulation)
+      `seq` rnf (triPointIndex triangulation)
+      `seq` rnf (triVertexOut triangulation)
+      `seq` rnf (triVertexData triangulation)
+      `seq` rnf (triHalfTopology triangulation)
+      `seq` rnf (triDirectedData triangulation)
+      `seq` rnf (triUndirectedData triangulation)
+      `seq` rnf (triFaceEdge triangulation)
+      `seq` rnf (triFaceData triangulation)
+      `seq` rnf (triConstraint triangulation)
+      `seq` rnf (triConstraintCount triangulation)
+      `seq` rnf (triConstraintEdges triangulation)
+      `seq` rnf (triSeamFrontier triangulation)
+      `seq` rnf (triCanonical triangulation)
+  {-# INLINE rnf #-}
+
+-- | The witness that a value is its own canonical representative. Every
+-- identifier of a 'CanonicalKnown' value is a function of its geometry, so
+-- canonical publication of it is the identity; an 'CanonicalUnknown' value
+-- may or may not be, and publication renumbers it to find out.
+data CanonicalAdmission
+  = CanonicalUnknown
+  | CanonicalKnown
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact immutable outer cycle plus four deterministic extreme anchors per
+-- chart used to seed separated tangents. The sequence is persistent: seam
+-- joins compose retained source sections without copying their elements.
+data SeamFrontierIndex = SeamFrontierIndex
+  { seamFrontierEdges :: !(Seq.Seq Int)
+  , seamFrontierXMinimum :: !Double
+  , seamFrontierXMaximum :: !Double
+  , seamFrontierLowerRightmost :: {-# UNPACK #-} !Int
+  , seamFrontierLowerLeftmost :: {-# UNPACK #-} !Int
+  , seamFrontierUpperRightmost :: {-# UNPACK #-} !Int
+  , seamFrontierUpperLeftmost :: {-# UNPACK #-} !Int
+  , -- | The same four extreme anchors in the orientation-preserving
+    -- quarter-turn chart @(u,v) = (y,-x)@.  Keeping this alongside the
+    -- existing x chart lets a later seam admit a north/south extension
+    -- without walking the resident frontier again.
+    seamFrontierYMinimum :: !Double
+  , seamFrontierYMaximum :: !Double
+  , seamFrontierYLowerRightmost :: {-# UNPACK #-} !Int
+  , seamFrontierYLowerLeftmost :: {-# UNPACK #-} !Int
+  , seamFrontierYUpperRightmost :: {-# UNPACK #-} !Int
+  , seamFrontierYUpperLeftmost :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Derive the exact frontier once from an admitted DCEL. This is intentionally
+-- an explicit preparation operation: ordinary mutable publication clears the
+-- cache instead of silently paying an outer-cycle traversal.
+prepareSeamFrontierIndex
+  :: Triangulation mode vertex directed undirected face
+  -> Maybe SeamFrontierIndex
+prepareSeamFrontierIndex triangulation = do
+  start <- frontierStart triangulation
+  let !directedCount = numDirectedEdges triangulation
+      !edges =
+        unfoldr
+          (frontierStep start directedCount)
+          (start, True, directedCount + 1)
+      terminal = List.foldl' (\_ edge -> Just edge) Nothing edges
+      closesAtStart = do
+        lastEdge <- terminal
+        pure
+          ( unpackIndex
+              (pagedUnsafeIndex (triHalfTopology triangulation) (4 * lastEdge + 1))
+              == start
+          )
+  if closesAtStart == Just True
+    then summarizeFrontier triangulation (Seq.fromList edges)
+    else Nothing
+ where
+  numDirectedEdges
+    :: Triangulation mode vertex directed undirected face
+    -> Int
+  numDirectedEdges mesh = pagedLength (triHalfTopology mesh) `quot` 4
+
+  frontierStart
+    :: Triangulation mode vertex directed undirected face
+    -> Maybe Int
+  frontierStart mesh =
+    unpackOptionalIndex (pagedUnsafeIndex (triFaceEdge mesh) 0)
+
+  frontierStep
+    :: Int
+    -> Int
+    -> (Int, Bool, Int)
+    -> Maybe (Int, (Int, Bool, Int))
+  frontierStep start directedCount (edge, first, remaining)
+    | remaining <= 0 = Nothing
+    | edge == start && not first = Nothing
+    | edge < 0 || edge >= directedCount = Nothing
+    | otherwise =
+        let next =
+              fromIntegral
+                (pagedUnsafeIndex (triHalfTopology triangulation) (4 * edge + 1))
+         in Just (edge, (next, False, remaining - 1))
+
+summarizeFrontier
+  :: Triangulation mode vertex directed undirected face
+  -> Seq.Seq Int
+  -> Maybe SeamFrontierIndex
+summarizeFrontier triangulation edges =
+  case fmap (frontierPoint triangulation) (toList edges) of
+    [] -> Nothing
+    points -> do
+      (minimumX, maximumX, lowerRightmost, lowerLeftmost, upperRightmost, upperLeftmost) <-
+        summarizeSeamChart id (zip [0 ..] points)
+      (minimumY, maximumY, yLowerRightmost, yLowerLeftmost, yUpperRightmost, yUpperLeftmost) <-
+        summarizeSeamChart seamQuarterTurn (zip [0 ..] points)
+      pure
+        SeamFrontierIndex
+          { seamFrontierEdges = edges
+          , seamFrontierXMinimum = minimumX
+          , seamFrontierXMaximum = maximumX
+          , seamFrontierLowerRightmost = lowerRightmost
+          , seamFrontierLowerLeftmost = lowerLeftmost
+          , seamFrontierUpperRightmost = upperRightmost
+          , seamFrontierUpperLeftmost = upperLeftmost
+          , seamFrontierYMinimum = minimumY
+          , seamFrontierYMaximum = maximumY
+          , seamFrontierYLowerRightmost = yLowerRightmost
+          , seamFrontierYLowerLeftmost = yLowerLeftmost
+          , seamFrontierYUpperRightmost = yUpperRightmost
+          , seamFrontierYUpperLeftmost = yUpperLeftmost
+          }
+ where
+  frontierPoint
+    :: Triangulation mode vertex directed undirected face
+    -> Int
+    -> (Double, Double)
+  frontierPoint mesh rawEdge =
+    let rawVertex = pagedUnsafeIndex (triHalfTopology mesh) (4 * rawEdge)
+        vertex = unpackIndex rawVertex
+     in ( pagedUnsafeIndex (triPointX mesh) vertex
+        , pagedUnsafeIndex (triPointY mesh) vertex
+        )
+
+-- | Summarize one chart from a bounded set of frontier points.  The index is
+-- retained beside the transformed point so the seam can carry the witness
+-- through a residual interval without re-reading the source frontier.
+summarizeSeamChart
+  :: ((Double, Double) -> (Double, Double))
+  -> [(Int, (Double, Double))]
+  -> Maybe (Double, Double, Int, Int, Int, Int)
+summarizeSeamChart transform points =
+  case fmap (\(index, point) -> (index, transform point)) points of
+    [] -> Nothing
+    first : remaining ->
+      let (!minimumValue, !maximumValue) =
+            List.foldl'
+              (\(!minimumSoFar, !maximumSoFar) (_, (u, _)) ->
+                 (min minimumSoFar u, max maximumSoFar u))
+              (chartU (snd first), chartU (snd first))
+              remaining
+          choose preference =
+            snd
+              ( List.foldl'
+                  (\(!bestPoint, !bestIndex) (index, candidate) ->
+                     if preference bestPoint candidate
+                       then (candidate, index)
+                       else (bestPoint, bestIndex))
+                  (snd first, fst first)
+                  remaining
+              )
+       in Just
+            ( minimumValue
+            , maximumValue
+            , choose preferRightmost
+            , choose preferLeftmost
+            , choose preferRightmostUpper
+            , choose preferLeftmostUpper
+            )
+ where
+  chartU :: (Double, Double) -> Double
+  preferRightmost :: (Double, Double) -> (Double, Double) -> Bool
+  preferLeftmost :: (Double, Double) -> (Double, Double) -> Bool
+  preferRightmostUpper :: (Double, Double) -> (Double, Double) -> Bool
+  preferLeftmostUpper :: (Double, Double) -> (Double, Double) -> Bool
+  chartU (u, _) = u
+  preferRightmost (bestU, bestV) (candidateU, candidateV) =
+    candidateU > bestU || (candidateU == bestU && candidateV < bestV)
+  preferLeftmost (bestU, bestV) (candidateU, candidateV) =
+    candidateU < bestU || (candidateU == bestU && candidateV < bestV)
+  preferRightmostUpper (bestU, bestV) (candidateU, candidateV) =
+    candidateU > bestU || (candidateU == bestU && candidateV > bestV)
+  preferLeftmostUpper (bestU, bestV) (candidateU, candidateV) =
+    candidateU < bestU || (candidateU == bestU && candidateV > bestV)
+
+-- | Orientation-preserving quarter turn used by the north/south seam chart.
+seamQuarterTurn :: (Double, Double) -> (Double, Double)
+seamQuarterTurn (x, y) = (y, -x)
+
+-- | Forget every payload while preparing a geometry-only mesh for persistent
+-- extension. Each payload plane is replaced directly with a defaulted,
+-- zero-page unit store; no source payload is traversed or densely rebuilt.
+-- Coordinates, topology, constraints, counts, and derived point identity
+-- remain authoritative, while future elements inherit the unit defaults.
+geometryOnlyPublication
+  :: Triangulation mode vertex directed undirected face
+  -> Triangulation mode () () () ()
+geometryOnlyPublication triangulation =
+  triangulation
+    { triVertexData = boxedDefaulted () (pagedLength (triPointX triangulation))
+    , triDirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 4)
+    , triUndirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 8)
+    , triFaceData = boxedDefaulted () (pagedLength (triFaceEdge triangulation))
+    , triSeamFrontier =
+        case triSeamFrontier triangulation of
+          Just frontier -> Just frontier
+          Nothing -> prepareSeamFrontierIndex triangulation
+    }
+
+-- The point index is a derived cache and therefore not an observable part of
+-- the mesh value. Structural equality compares every semantic plane and
+-- default while deliberately refusing to construct or compare that cache.
+instance
+  ( Eq vertex
+  , Eq directed
+  , Eq undirected
+  , Eq face
+  ) => Eq (Triangulation mode vertex directed undirected face) where
+  left == right =
+    triPointX left == triPointX right
+      && triPointY left == triPointY right
+      && triVertexOut left == triVertexOut right
+      && triVertexData left == triVertexData right
+      && triHalfTopology left == triHalfTopology right
+      && triDirectedData left == triDirectedData right
+      && triUndirectedData left == triUndirectedData right
+      && triFaceEdge left == triFaceEdge right
+      && triFaceData left == triFaceData right
+      && triConstraint left == triConstraint right
+      && triConstraintCount left == triConstraintCount right
+
+promoteConstrained
+  :: Triangulation 'Unconstrained vertex directed undirected face
+  -> Triangulation 'Constrained vertex directed undirected face
+promoteConstrained Triangulation{
+  triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,
+  triDirectedData, triUndirectedData, triFaceEdge, triFaceData,
+  triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier, triCanonical
+  } =
+  Triangulation{
+    triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,
+    triDirectedData, triUndirectedData, triFaceEdge, triFaceData,
+    triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier, triCanonical
+    }
+
+-- | Authoring view of the three required fills. No resident defaults record
+-- competes with the payload stores or can diverge from them during an edit.
+authoringElementDefaults
+  :: Triangulation mode vertex directed undirected face
+  -> ElementDefaults directed undirected face
+authoringElementDefaults triangulation = ElementDefaults
+  (requiredBoxedFill (triDirectedData triangulation))
+  (requiredBoxedFill (triUndirectedData triangulation))
+  (requiredBoxedFill (triFaceData triangulation))
+
+-- | A traversal of every occurrence of one payload parameter, in the van
+-- Laarhoven encoding: an effectful visit that may change the payload's type.
+-- The 'Applicative' belongs to the caller, so one traversal per parameter
+-- serves relabeling, collection and genuinely effectful annotation alike
+-- instead of a separate function for each.
+type PayloadTraversal source target payload payload' =
+  forall f. Applicative f => (payload -> f payload') -> source -> f target
+
+-- | Every stored vertex payload, in vertex order, then its optional fill.
+--
+-- Dense authored vertices have no fill. Geometry-only publication does: that
+-- payload must be visited once and preserved just like the resident values.
+vertexPayloads
+  :: PayloadTraversal
+      (Triangulation mode vertex directed undirected face)
+      (Triangulation mode vertex' directed undirected face)
+      vertex
+      vertex'
+vertexPayloads visit triangulation =
+  (\payloads -> triangulation{triVertexData = payloads}) <$> traverse visit (triVertexData triangulation)
+
+-- | Every stored directed-edge payload, then the default a later directed edge
+-- will inherit.
+--
+-- The default is visited once and written to its sole owner, the store fill.
+directedPayloads
+  :: PayloadTraversal
+      (Triangulation mode vertex directed undirected face)
+      (Triangulation mode vertex directed' undirected face)
+      directed
+      directed'
+directedPayloads visit triangulation =
+  (\payloads -> triangulation{triDirectedData = payloads}) <$> traverse visit (triDirectedData triangulation)
+
+-- | Every stored undirected-edge payload, then the default a later undirected
+-- edge will inherit.
+undirectedPayloads
+  :: PayloadTraversal
+      (Triangulation mode vertex directed undirected face)
+      (Triangulation mode vertex directed undirected' face)
+      undirected
+      undirected'
+undirectedPayloads visit triangulation =
+  (\payloads -> triangulation{triUndirectedData = payloads}) <$> traverse visit (triUndirectedData triangulation)
+
+-- | Every stored face payload, then the default a later face will inherit.
+facePayloads
+  :: PayloadTraversal
+      (Triangulation mode vertex directed undirected face)
+      (Triangulation mode vertex directed undirected face')
+      face
+      face'
+facePayloads visit triangulation =
+  (\payloads -> triangulation{triFaceData = payloads}) <$> traverse visit (triFaceData triangulation)
+
+-- | Ranges over the face payload, which is the last parameter and so the only
+-- one a class of this kind can reach. The other three payloads have exactly
+-- the same structure under 'vertexPayloads', 'directedPayloads' and
+-- 'undirectedPayloads'; they are simply not spellable as instances here.
+--
+-- 'mapFaces' rather than the traversal, because it leaves an unmaterialized
+-- page unmaterialized. The two agree on everything a 'BoxedPaged' lets anyone
+-- observe, which is what the coherence law asks and all it asks.
+instance Functor (Triangulation mode vertex directed undirected) where
+  fmap = mapFaces
+  {-# INLINE fmap #-}
+
+-- | Folds the stored face payloads and then the default, so 'length' is one
+-- greater than the number of stored faces. A fold that skipped the default
+-- would report a triangulation as holding a value it does hold.
+instance Foldable (Triangulation mode vertex directed undirected) where
+  foldMap = foldMapDefault
+  {-# INLINE foldMap #-}
+
+instance Traversable (Triangulation mode vertex directed undirected) where
+  traverse = facePayloads
+  {-# INLINE traverse #-}
+
+-- | Sparse payload mapping also maps the fill at its storage owner, so future
+-- insertion and resident observations cannot disagree about the new default.
+mapDirectedEdges
+  :: (directed -> directed')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed' undirected face
+mapDirectedEdges f triangulation =
+  triangulation
+    { triDirectedData = fmap f (triDirectedData triangulation)
+    }
+
+-- | Map every undirected-edge annotation and its future-element default.
+mapUndirectedEdges
+  :: (undirected -> undirected')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected' face
+mapUndirectedEdges f triangulation =
+  triangulation
+    { triUndirectedData = fmap f (triUndirectedData triangulation)
+    }
+
+-- | Map every face annotation and its future-element default.
+mapFaces
+  :: (face -> face')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face'
+mapFaces f triangulation =
+  triangulation
+    { triFaceData = fmap f (triFaceData triangulation)
+    }
+
+-- | The vertex component is free, like the other three. Geometry owns the
+-- points, so a payload map cannot move one — the image type need not even have
+-- a position to speak of. An optional geometry-publication fill maps with it.
+mapVertices
+  :: (vertex -> vertex')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex' directed undirected face
+mapVertices f triangulation =
+  triangulation{triVertexData = fmap f (triVertexData triangulation)}
+
+-- | Materialize every resident undirected-edge annotation in handle order
+-- while installing the declared fallback for edges created by a later edit.
+imapUndirectedEdges
+  :: undirected'
+  -> (UndirectedEdgeId -> undirected -> undirected')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected' face
+imapUndirectedEdges fallback relabel triangulation =
+  triangulation
+    { triUndirectedData =
+        boxedFromVector (Fill fallback)
+          (V.imap (\index -> relabel (UndirectedEdgeId (fromIntegral index))) payloads)
+    }
+ where
+  payloads = boxedToVector (triUndirectedData triangulation)
+
+-- | Materialize every resident face annotation in handle order while
+-- installing the declared fallback for faces created by a later edit.
+imapFaces
+  :: face'
+  -> (FaceId -> face -> face')
+  -> Triangulation mode vertex directed undirected face
+  -> Triangulation mode vertex directed undirected face'
+imapFaces fallback relabel triangulation =
+  triangulation
+    { triFaceData =
+        boxedFromVector (Fill fallback)
+          (V.imap (\index -> relabel (FaceId (fromIntegral index))) payloads)
+    }
+ where
+  payloads = boxedToVector (triFaceData triangulation)
+
+-- | Geometry-only unconstrained Delaunay triangulation.
+type DelaunayTriangulation vertex = Triangulation 'Unconstrained vertex () () ()
+
+-- | Geometry-only constrained Delaunay triangulation.
+type ConstrainedDelaunayTriangulation vertex = Triangulation 'Constrained vertex () () ()
+
+-- | A constructed triangulation and the canonical handle chosen for each input.
+--
+-- The result is a value, not a history: derived 'Eq'/'Show' would observe
+-- 'buildStats' through a facade that hides it, so neither instance exists.
+data BuildResult mode vertex directed undirected face = BuildResult
+  { -- | The immutable constructed mesh.
+    buildTriangulation :: !(Triangulation mode vertex directed undirected face)
+  , -- | Canonical vertex handle for each input position, including duplicates.
+    buildInputVertices :: !(PrimArray Word32)
+  , buildStats :: !BuildStats
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+-- | Published insertion result, selected vertex, disposition, and work receipt.
+data InsertionResult mode vertex directed undirected face = InsertionResult
+  { insertionTriangulation :: !(Triangulation mode vertex directed undirected face)
+  , insertionVertex :: !VertexId
+  , insertionDisposition :: !InsertionDisposition
+  , insertionStats :: !BuildStats
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+deriving stock instance
+  (Eq vertex, Eq directed, Eq undirected, Eq face)
+  => Eq (InsertionResult mode vertex directed undirected face)
+deriving stock instance
+  (Show vertex, Show directed, Show undirected, Show face)
+  => Show (InsertionResult mode vertex directed undirected face)
+
+-- | Exact support touched by one refinement publication. Checked local
+-- refinement uses this as the positive receipt accompanying its typed
+-- obstruction surface; unrestricted refinement deliberately avoids the
+-- additional support scan.
+data RefinementReceipt = RefinementReceipt
+  { refinementVisitedJoinFaces :: !(V.Vector FaceId)
+  , refinementVisitedProtectedFaces :: !(V.Vector FaceId)
+  , refinementCreatedFaces :: !(V.Vector FaceId)
+    -- ^ Face slots whose published triangle changed or was appended.
+  , refinementFinalPermittedFaces :: !(V.Vector FaceId)
+    -- ^ Exact final face lineage admitted by a checked local domain.
+  , refinementFinalInterfaceIncidence :: !(V.Vector (UndirectedEdgeId, FaceId, FaceId))
+  , refinementTouchedEdges :: !(V.Vector UndirectedEdgeId)
+  , refinementRemovedEdges :: !(V.Vector UndirectedEdgeId)
+  , refinementInterfaceBoundaryReads :: {-# UNPACK #-} !Int
+  , refinementAttemptedBoundaryCrossings :: {-# UNPACK #-} !Int
+  , refinementPublicationStats :: !PublicationStats
+    -- ^ Page publication work measured by the transaction owner.
+  , refinementClosureStats :: !ClosureStats
+    -- ^ Sizes of the closure the transaction's domain spans (admitted faces
+    -- with their collar, their edges, vertices and pairs), counted from the
+    -- domain's membership; no validation descent produces them.
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A locally refined section paired with the proof of what the checked
+-- interpreter observed and rewrote. Ordinary refinement does not pay to
+-- construct this proof.
+data RefinementDomainResult mode vertex directed undirected face = RefinementDomainResult
+  { refinementDomainResult :: !(RefinementResult mode vertex directed undirected face)
+  , refinementDomainReceipt :: !RefinementReceipt
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
+
+-- | Refined mesh together with the budget, exclusion, and support outcome.
+data RefinementResult mode vertex directed undirected face = RefinementResult
+  { -- | The immutable mesh after all admitted refinement steps.
+    refinedTriangulation :: !(Triangulation mode vertex directed undirected face)
+  , refinementStats :: !BuildStats
+  , refinementAddedVertices :: {-# UNPACK #-} !Int
+  -- | Whether the quality worklist drained. 'False' means the vertex budget
+  -- stopped the run with work outstanding. A drained worklist can still leave
+  -- faces the quality bounds condemn but no admissible Steiner point can fix;
+  -- auditing the result is the caller's to ask for, not a cost every run pays.
+  , refinementComplete :: !Bool
+  , -- | Faces deliberately excluded by barrier-depth policy.
+    refinementExcludedFaces :: !(V.Vector FaceId)
+  }
+  deriving stock (Generic)
+  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Planar/Internal/SegmentRelation.hs b/src-dcel/Moonlight/Planar/Internal/SegmentRelation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/SegmentRelation.hs
@@ -0,0 +1,97 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | The complete closed-segment relation vocabulary and its one policy owner.
+module Moonlight.Planar.Internal.SegmentRelation
+  ( SegmentRelation (..)
+  , allSegmentRelations
+  , segmentRelationWith
+  ) where
+
+import Control.DeepSeq (NFData)
+import GHC.Generics (Generic)
+
+-- | The complete exact-predicate relation between two closed segments. There
+-- is one vocabulary owner; traversal and constrained-union consumers derive
+-- their booleans and obstruction policy from it rather than cloning slightly
+-- different orientation formulae.
+data SegmentRelation
+  = SegmentsDisjoint
+  | SegmentsDuplicate
+  | SegmentsShareEndpoint
+  | SegmentsProperlyCross
+  | SegmentEndpointTouchesInterior
+  | SegmentsCollinearlyOverlap
+  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Every segment relation in constructor order.
+allSegmentRelations :: [SegmentRelation]
+allSegmentRelations = [minBound .. maxBound]
+
+-- | Classify two closed segments using the supplied point observations. The
+-- guards run from the most specific relation outward: collinearity is decided
+-- before shared endpoints so that collinear segments overlapping for a
+-- positive length are an overlap whether or not they also share an endpoint,
+-- while a straight continuation (zero overlap at a common endpoint) remains a
+-- shared endpoint.
+segmentRelationWith
+  :: (point -> point -> Bool)
+  -- ^ Point equality.
+  -> (point -> point -> Ordering)
+  -- ^ Lexicographic point ordering.
+  -> (point -> point -> point -> Ordering)
+  -- ^ Orientation of an ordered triple.
+  -> (point -> point -> point -> Bool)
+  -- ^ Membership of the third point in the closed segment.
+  -> point
+  -> point
+  -> point
+  -> point
+  -> SegmentRelation
+segmentRelationWith equalPoint comparePoint orientation onSegment a b c d
+  | sameUndirectedSegment = SegmentsDuplicate
+  | abC == EQ && abD == EQ && cdA == EQ && cdB == EQ = collinearRelation
+  | sharesEndpoint = SegmentsShareEndpoint
+  | opposite abC abD && opposite cdA cdB = SegmentsProperlyCross
+  | endpointTouches = SegmentEndpointTouchesInterior
+  | otherwise = SegmentsDisjoint
+ where
+  !abC = orientation a b c
+  !abD = orientation a b d
+  !cdA = orientation c d a
+  !cdB = orientation c d b
+  sameUndirectedSegment =
+    (equalPoint a c && equalPoint b d)
+      || (equalPoint a d && equalPoint b c)
+  !sharesEndpoint =
+    equalPoint a c
+      || equalPoint a d
+      || equalPoint b c
+      || equalPoint b d
+  endpointTouches =
+    (abC == EQ && onSegment a b c)
+      || (abD == EQ && onSegment a b d)
+      || (cdA == EQ && onSegment c d a)
+      || (cdB == EQ && onSegment c d b)
+  collinearRelation =
+    let !overlapLower = maximumPoint (minimumPoint a b) (minimumPoint c d)
+        !overlapUpper = minimumPoint (maximumPoint a b) (maximumPoint c d)
+     in case comparePoint overlapLower overlapUpper of
+          LT -> SegmentsCollinearlyOverlap
+          EQ | sharesEndpoint -> SegmentsShareEndpoint
+          EQ -> SegmentEndpointTouchesInterior
+          GT -> SegmentsDisjoint
+  opposite left right =
+    (left == LT && right == GT) || (left == GT && right == LT)
+  minimumPoint left right =
+    case comparePoint left right of
+      GT -> right
+      _ -> left
+  maximumPoint left right =
+    case comparePoint left right of
+      LT -> right
+      _ -> left
+{-# INLINE segmentRelationWith #-}
diff --git a/src-dcel/Moonlight/Planar/Internal/Tournament.hs b/src-dcel/Moonlight/Planar/Internal/Tournament.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Tournament.hs
@@ -0,0 +1,45 @@
+-- | One deterministic balanced binary descent shared by every pure
+-- associative schedule in the package.  Leaves retain their source values;
+-- interpreters own only their binary operation.
+module Moonlight.Planar.Internal.Tournament
+  ( TournamentPlan (..)
+  , planTournament
+  , interpretTournament
+  ) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+
+data TournamentPlan value
+  = TournamentLeaf !value
+  | TournamentNode !(TournamentPlan value) !(TournamentPlan value)
+
+planTournament :: NonEmpty value -> TournamentPlan value
+planTournament = descendTournament . fmap TournamentLeaf
+
+interpretTournament
+  :: (value -> value -> Either failure value)
+  -> TournamentPlan value
+  -> Either failure value
+interpretTournament combine tournament =
+  case tournament of
+    TournamentLeaf value -> Right value
+    TournamentNode left right -> do
+      leftValue <- interpretTournament combine left
+      rightValue <- interpretTournament combine right
+      combine leftValue rightValue
+
+descendTournament :: NonEmpty (TournamentPlan value) -> TournamentPlan value
+descendTournament (single :| []) = single
+descendTournament plans = descendTournament (pairTournamentRound plans)
+
+pairTournamentRound
+  :: NonEmpty (TournamentPlan value)
+  -> NonEmpty (TournamentPlan value)
+pairTournamentRound (left :| right : rest) =
+  TournamentNode left right :| pairTournamentTail rest
+pairTournamentRound (single :| []) = single :| []
+
+pairTournamentTail :: [TournamentPlan value] -> [TournamentPlan value]
+pairTournamentTail (left : right : rest) =
+  TournamentNode left right : pairTournamentTail rest
+pairTournamentTail rest = rest
diff --git a/src-dcel/Moonlight/Planar/Internal/Types.hs b/src-dcel/Moonlight/Planar/Internal/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Types.hs
@@ -0,0 +1,338 @@
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE UndecidableSuperClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RoleAnnotations #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The vocabulary: the types the surface names, none of which mentions the
+-- stored representation.
+module Moonlight.Planar.Internal.Types
+  ( SiteRelation (..)
+  , ElementDefaults (..)
+  , unitElementDefaults
+  , ConstraintMode (..)
+  , KnownConstraintMode (..)
+  , InsertionDisposition (..)
+  , ClosureStats (..)
+  , validatePoint
+  , BuildError (..)
+  , Location (..)
+  , LocationHint (..)
+  , LocationStats (..)
+  , emptyLocationStats
+  , NearestStats (..)
+  , RefinementParameters (..)
+  , defaultRefinementParameters
+  , InvariantViolation (..)
+  , PlanarIncidenceError (..)
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId
+  , FaceId
+  , UndirectedEdgeId
+  , VertexId
+  )
+import Data.Word (Word8)
+import qualified Data.IntSet as IntSet
+import GHC.Generics (Generic)
+import Moonlight.Planar.Internal.BoxedPaged (BoxedStorageError)
+import Moonlight.Planar.Point (Point (..), QueryPoint, PointValidationError (..), mkQueryPoint)
+import Moonlight.Planar.Scalar (CoordinateError, NonFiniteValue)
+
+-- | Type-level witness for whether constraint flags may be present.
+data ConstraintMode = Unconstrained | Constrained
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Recover a type-level constraint mode as a value.
+class KnownConstraintMode (mode :: ConstraintMode) where
+  constraintModeValue :: proxy mode -> ConstraintMode
+
+instance KnownConstraintMode 'Unconstrained where
+  constraintModeValue _ = Unconstrained
+
+instance KnownConstraintMode 'Constrained where
+  constraintModeValue _ = Constrained
+
+-- | Sizes of the closure a checked local domain spans: its admitted faces
+-- with the collar across the interface, the directed edges those faces and
+-- interface pairs carry, the vertices those edges join, the interface pairs,
+-- and the undirected pairs beneath the selected edges (the last field's name
+-- predates that meaning). The counts are taken from the closure membership
+-- the domain already knows; nothing is validated to produce them. The names
+-- keep the older validation vocabulary until the coordinated rename.
+data ClosureStats = ClosureStats
+  { closureFaces :: {-# UNPACK #-} !Int
+  , closureDirectedEdges :: {-# UNPACK #-} !Int
+  , closureVertices :: {-# UNPACK #-} !Int
+  , closureInterfacePairs :: {-# UNPACK #-} !Int
+  , closureConstraintPairs :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact geometric relation between two finite coordinate supports. The
+-- overlap count is strictly positive in 'PartialOverlap'; equality, subset and
+-- disjointness have already been excluded before that constructor is chosen.
+data SiteRelation
+  = -- | Both supports contain exactly the same coordinates.
+    EqualSites
+  | -- | Every left coordinate occurs on the right, which has at least one more.
+    LeftProperSubset
+  | -- | Every right coordinate occurs on the left, which has at least one more.
+    RightProperSubset
+  | -- | The supports share no coordinate.
+    DisjointSites
+  | -- | Neither support contains the other; the field is the positive number
+    -- of coordinates they share.
+    PartialOverlap {-# UNPACK #-} !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Payloads inherited by topology elements created after initial loading.
+data ElementDefaults directed undirected face = ElementDefaults
+  { defaultDirectedEdgeData :: !directed
+  , defaultUndirectedEdgeData :: !undirected
+  , defaultFaceData :: !face
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Payload defaults for geometry-only triangulations.
+unitElementDefaults :: ElementDefaults () () ()
+unitElementDefaults = ElementDefaults () () ()
+
+-- | Whether an insertion published a new site or selected an existing one.
+data InsertionDisposition = Inserted | AlreadyPresent
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Validate a construction point while retaining its optional input slot.
+validatePoint :: Maybe Int -> Point -> Either BuildError (QueryPoint)
+validatePoint slot point@(Point x y) =
+  case mkQueryPoint point of
+    Left (InvalidPointX reason) -> Left (InvalidCoordinate slot x reason)
+    Left (InvalidPointY reason) -> Left (InvalidCoordinate slot y reason)
+    Right queryPoint -> Right queryPoint
+
+-- | Total construction and rewrite obstruction surface.
+data BuildError
+  = InvalidCoordinate !(Maybe Int) {-# UNPACK #-} !Double !CoordinateError
+  | PointLocationFailed !(Point)
+  | LocationWalkExhausted !(Point) {-# UNPACK #-} !Int
+  | RefinementInputTopologyInvalid !InvariantViolation
+  | SeamFrontierUnavailable
+  | SeamSourceEdgeRequiresFlip !UndirectedEdgeId
+  | FreshInsertionMatchedExistingVertex !VertexId !VertexId
+  | DegenerateLineEndpointMissingOutgoing !VertexId
+  | DegenerateLineEndpointTurnMissing {-# UNPACK #-} !Int
+  | DegenerateLineConnectedVertexMissing {-# UNPACK #-} !Int
+  | HullStartNotVisible !DirectedEdgeId
+  | OuterRangeDidNotTerminate !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
+  | OuterRangeContainsInnerEdge !DirectedEdgeId !FaceId
+  | ConstrainedEdgeFlipRefused !UndirectedEdgeId
+  | RemovalVertexOutOfRange !VertexId {-# UNPACK #-} !Int
+  | RemovalEdgeOutOfRange !UndirectedEdgeId {-# UNPACK #-} !Int
+  | RemovalFaceOutOfRange !FaceId {-# UNPACK #-} !Int
+  | RemovalFaceCycleDidNotTerminate
+      !FaceId
+      !DirectedEdgeId
+      {-# UNPACK #-} !Int
+  | RemovalEmptyTriangulation !VertexId
+  | RemovalTwoPointDegreeMismatch !VertexId {-# UNPACK #-} !Int
+  | RemovalCollinearDegreeMismatch !VertexId {-# UNPACK #-} !Int
+  | RemovalBorderTooShort {-# UNPACK #-} !Int
+  | RemovalBorderArityMismatch {-# UNPACK #-} !Int
+  | RemovalOutgoingCycleDidNotTerminate
+      !VertexId
+      !DirectedEdgeId
+      {-# UNPACK #-} !Int
+  | CircleSweepRequiresDenseStorage
+  | CircleSweepHullEmpty
+  | OuterCycleDidNotTerminate
+      !DirectedEdgeId
+      !DirectedEdgeId
+      {-# UNPACK #-} !Int
+  | HierarchyLevelPopulationMismatch
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  | HierarchyInsertionHandleMismatch !VertexId !VertexId
+  | PointIndexCapacityExhausted {-# UNPACK #-} !Int
+  | RefinementMinimumAngleNotFinite !NonFiniteValue
+  | RefinementMinimumAngleOutOfRange {-# UNPACK #-} !Double
+  | RefinementMinimumAngleDerivedRatioNotFinite !NonFiniteValue
+  | RefinementMaximumAdditionalVerticesNegative {-# UNPACK #-} !Int
+  | RefinementMinimumAreaNotFinite !NonFiniteValue
+  | RefinementMinimumAreaNegative {-# UNPACK #-} !Double
+  | RefinementMaximumAreaNotFinite !NonFiniteValue
+  | RefinementMaximumAreaNotPositive {-# UNPACK #-} !Double
+  | RefinementMaximumRadiusEdgeRatioNotFinite !NonFiniteValue
+  | RefinementMaximumRadiusEdgeRatioNotPositive {-# UNPACK #-} !Double
+  | RefinementMaximumEdgeLengthNotFinite !NonFiniteValue
+  | RefinementMaximumEdgeLengthNotPositive {-# UNPACK #-} !Double
+  | RefinementMinimumAreaExceedsMaximum
+      {-# UNPACK #-} !Double
+      {-# UNPACK #-} !Double
+  | RefinementSeedFaceNotActive !FaceId {-# UNPACK #-} !Int
+  | RefinementDomainTopologyChanged
+  | RefinementDomainRequiresConvexHullPreservation
+  | RefinementDomainRequiresConstraintPreservation
+  | RefinementDomainForbidsOuterFaceExclusion
+  | RefinementDomainRequiresFiniteVertexBudget
+  | RefinementSeamBridgeBudgetExceeded
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  | RefinementSeamBridgeMidpointCollapsed !UndirectedEdgeId
+  | BoundarySplitRequiresBoundaryEdge !UndirectedEdgeId
+  | RefinementDomainWouldCrossInterface !UndirectedEdgeId !FaceId
+  | RefinementDomainWouldRewriteProtectedFace !FaceId
+  | RefinementDomainProtectedFaceChanged !FaceId
+  | RefinementDomainInterfaceOppositeFaceNotPermitted !UndirectedEdgeId !FaceId
+  | RefinementOversizedEdge
+      !FaceId
+      !UndirectedEdgeId
+      {-# UNPACK #-} !Double
+      {-# UNPACK #-} !Double
+  | CapacityExceeded {-# UNPACK #-} !Int
+  | HalfEdgeCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | FaceCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | PayloadStorageFailure !BoxedStorageError
+  | CoordinatePayloadCountMismatch
+      {-# UNPACK #-} !Int
+      {-# UNPACK #-} !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact position of a query relative to the finite triangulation.
+data Location
+  = EmptyTriangulation
+  | OnVertex !VertexId
+  | OnEdge !DirectedEdgeId
+  | InFace !FaceId
+  | OutsideConvexHull !(Maybe DirectedEdgeId)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Optional starting cell for point-location descent.
+data LocationHint
+  = VertexHint !VertexId
+  | FaceHint !FaceId
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Work performed by point-location descent.
+data LocationStats = LocationStats
+  { locationWalkSteps :: {-# UNPACK #-} !Int
+  , locationUsedFallback :: !Bool
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Location telemetry for a query that required no descent.
+emptyLocationStats :: LocationStats
+emptyLocationStats = LocationStats 0 False
+
+-- | Work performed by a nearest-neighbor query.
+data NearestStats = NearestStats
+  { nearestWalkSteps :: {-# UNPACK #-} !Int
+  , nearestDistanceTests :: {-# UNPACK #-} !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Independent quality bounds and a finite Steiner-vertex budget.
+data RefinementParameters = RefinementParameters
+  { refineMaxAdditionalVertices :: !(Maybe Int)
+  , refineMinArea :: !(Maybe Double)
+  , refineMaxArea :: !(Maybe Double)
+  , refineMaxRadiusEdgeRatio :: !(Maybe Double)
+  , refineMaxEdgeLength :: !(Maybe Double)
+  , refinePreserveConvexHull :: !Bool
+  , refineKeepConstraintEdges :: !Bool
+  , refineExcludeOuterFaces :: !Bool
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Conservative refinement defaults with no explicit area or edge bounds.
+defaultRefinementParameters :: RefinementParameters
+defaultRefinementParameters =
+  RefinementParameters
+    { refineMaxAdditionalVertices = Nothing
+    , refineMinArea = Nothing
+    , refineMaxArea = Nothing
+    , refineMaxRadiusEdgeRatio = Just 1
+    , refineMaxEdgeLength = Nothing
+    , refinePreserveConvexHull = True
+    , refineKeepConstraintEdges = False
+    , refineExcludeOuterFaces = False
+    }
+
+-- | Geometry-free finite incidence refusals, shared by native meshes and
+-- exact arrangements. The representation does not own a second vocabulary.
+data PlanarIncidenceError
+  = IncidencePackedLayoutInvalid !Int !Int !Int
+  | IncidenceVertexOriginInvalid !DirectedEdgeId !VertexId
+  | IncidenceEdgeLinksInvalid !DirectedEdgeId !DirectedEdgeId !DirectedEdgeId
+  | IncidenceEdgeFaceInvalid !DirectedEdgeId !FaceId
+  | IncidenceEdgeLinkMismatch !DirectedEdgeId
+  | IncidenceEdgeEndpointMismatch !DirectedEdgeId
+  | IncidenceVertexRootInvalid !VertexId !(Maybe DirectedEdgeId)
+  | IncidenceFaceRootInvalid !FaceId !DirectedEdgeId
+  | IncidenceSparseFaceInvalid !Int
+  | IncidenceBoundedFaceWithoutBoundary !FaceId
+  | IncidenceBoundaryCoverageInvalid !IntSet.IntSet !IntSet.IntSet
+  | IncidenceVertexStarCoverageInvalid !IntSet.IntSet !IntSet.IntSet
+  | IncidenceIsolatedOwnershipInvalid !IntSet.IntSet !IntSet.IntSet
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A concrete witness that an immutable DCEL law does not hold.
+data InvariantViolation
+  = CoordinatePlaneLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | VertexOutgoingLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | VertexPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | TopologyArenaLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | DirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | UndirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | DirectedEdgeCountOdd {-# UNPACK #-} !Int
+  | ConstraintLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | NonCanonicalConstraintFlag !UndirectedEdgeId !Word8
+  | CachedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | CachedConstraintIndexMismatch
+  | MissingOuterFace
+  | FacePayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | EdgeOriginOutOfRange !DirectedEdgeId !VertexId {-# UNPACK #-} !Int
+  | EdgeNextOutOfRange !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
+  | EdgePreviousOutOfRange !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
+  | EdgeFaceOutOfRange !DirectedEdgeId !FaceId {-# UNPACK #-} !Int
+  | VertexOutgoingOutOfRange !VertexId !DirectedEdgeId {-# UNPACK #-} !Int
+  | FaceAdjacentOutOfRange !FaceId !DirectedEdgeId {-# UNPACK #-} !Int
+  | EdgeSelfLinkedNext !DirectedEdgeId
+  | EdgeSelfLinkedPrevious !DirectedEdgeId
+  | InnerFaceNotTriangularAtEdge !DirectedEdgeId
+  | FaceMissingAdjacentEdge !FaceId
+  | FaceRepresentativeMismatch !FaceId !DirectedEdgeId !FaceId
+  | InnerFaceVertexCardinalityMismatch !FaceId {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | ConnectedVertexMissingOutgoing !VertexId
+  | VertexOutgoingOriginMismatch !VertexId !DirectedEdgeId !VertexId
+  | CollinearEdgeCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | EulerCharacteristicMismatch {-# UNPACK #-} !Int
+  | InnerFaceNotCounterClockwise !FaceId
+  | LocallyIllegalDelaunayEdge !UndirectedEdgeId
+  | DelaunayIncidentFaceNotTriangular !UndirectedEdgeId
+  | IncidenceViolation !PlanarIncidenceError
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Planar/Internal/Validation.hs b/src-dcel/Moonlight/Planar/Internal/Validation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Internal/Validation.hs
@@ -0,0 +1,759 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fno-expose-all-unfoldings #-}
+
+-- | Discharge: the invariants the constructors guarantee, checkable on a value
+-- built by any route.
+module Moonlight.Planar.Internal.Validation
+  ( canonicalAdmission
+  , coordinatesAscend
+  , validateTopology
+  , ClosureStats (..)
+  , topologyClosureStats
+  , topologyClosureSelectionStats
+  , validateTopologyClosure
+  , validateTopologyClosureWithStats
+  , validateDelaunay
+  , validateTriangulation
+  , triangulationIsValid
+  , faceArea
+  , faceMinimumAngleDegrees
+  ) where
+
+import Control.Monad (foldM)
+import Control.Monad.ST (ST, runST)
+import Data.Bits (countLeadingZeros, finiteBitSize, shiftL, shiftR, (.&.))
+import Data.List (nub)
+import qualified Data.IntSet as IntSet
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Moonlight.Planar.Internal.BoxedPaged (boxedPagedLength)
+import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
+import Moonlight.Planar.Internal.Paged (Paged, pagedFoldl', pagedLength, pagedUnsafeIndex)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Incidence (nativePlanarIncidence, validatePlanarIncidence, validateIncidenceEdgeLinks)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (allFaces, undirectedEdges)
+import Moonlight.Planar.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Math
+  ( squaredDistance
+  , triangleArea
+  )
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+
+-- | Native cardinality/triangle/geometry laws around the single finite
+-- incidence admission boundary. Unsafe geometry follows successful incidence.
+validateTopology :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
+validateTopology triangulation =
+  structuralViolations ++ orientationViolations
+ where
+  verticesCount = numVertices triangulation
+  halfCount = numDirectedEdges triangulation
+  edgeCount = numUndirectedEdges triangulation
+  facesCount = numFaces triangulation
+
+  -- Geometry descends only after the finite DCEL has glued structurally.
+  -- Reading triangle coordinates through malformed links would turn a typed
+  -- validation failure into an indexing crash.
+  structuralViolations =
+    cardinalityViolations
+      ++ incidenceViolations
+      ++ faceViolations
+      ++ eulerViolations
+
+  incidenceViolations
+    | not (null cardinalityViolations) = []
+    | otherwise = either (pure . IncidenceViolation) (const [])
+        (validatePlanarIncidence (nativePlanarIncidence triangulation))
+
+  orientationViolations
+    | not (null structuralViolations) = []
+    | otherwise =
+        [ InnerFaceNotCounterClockwise face
+        | face <- allFaces triangulation
+        , face /= outerFace
+        , Just (first, second, third) <- [innerFaceVertices triangulation face]
+        , orient2d
+            (vertexPoint triangulation first)
+            (vertexPoint triangulation second)
+            (vertexPoint triangulation third)
+            /= GT
+        ]
+
+  cardinalityViolations =
+    [ CoordinatePlaneLengthMismatch pointXCount pointYCount
+    | pointXCount /= pointYCount
+    ]
+      ++ [VertexOutgoingLengthMismatch vertexOutCount verticesCount | vertexOutCount /= verticesCount]
+      ++ [VertexPayloadLengthMismatch vertexPayloadCount verticesCount | vertexPayloadCount /= verticesCount]
+      ++ [TopologyArenaLengthMismatch topologyLength (4 * halfCount) | not halfArraysEqual]
+      ++ [DirectedPayloadLengthMismatch directedPayloadCount halfCount | directedPayloadCount /= halfCount]
+      ++ [UndirectedPayloadLengthMismatch undirectedPayloadCount edgeCount | undirectedPayloadCount /= edgeCount]
+      ++ [DirectedEdgeCountOdd halfCount | odd halfCount]
+      ++ [ConstraintLengthMismatch constraintLength edgeCount | constraintLength /= edgeCount]
+      ++ [ NonCanonicalConstraintFlag (UndirectedEdgeId (fromIntegral index)) flag
+         | index <- [0 .. pagedLength (triConstraint triangulation) - 1]
+         , let flag = pagedUnsafeIndex (triConstraint triangulation) index
+         , flag /= 0 && flag /= 1
+         ]
+      ++ [CachedConstraintCountMismatch (triConstraintCount triangulation) actualConstraintCount | triConstraintCount triangulation /= actualConstraintCount]
+      ++ [ CachedConstraintIndexMismatch
+         | constraintLength == edgeCount
+         , triConstraintEdges triangulation /= indexedConstraintEdges
+         ]
+      ++ [MissingOuterFace | facesCount == 0]
+      ++ [FacePayloadLengthMismatch facePayloadCount facesCount | facePayloadCount /= facesCount]
+
+  pointXCount = pagedLength (triPointX triangulation)
+  pointYCount = pagedLength (triPointY triangulation)
+  vertexOutCount = pagedLength (triVertexOut triangulation)
+  vertexPayloadCount = boxedPagedLength (triVertexData triangulation)
+  topologyLength = pagedLength (triHalfTopology triangulation)
+  directedPayloadCount = boxedPagedLength (triDirectedData triangulation)
+  undirectedPayloadCount = boxedPagedLength (triUndirectedData triangulation)
+  constraintLength = pagedLength (triConstraint triangulation)
+  facePayloadCount = boxedPagedLength (triFaceData triangulation)
+  actualConstraintCount = pagedFoldl' (\count flag -> if flag == 1 then count + 1 else count) 0 (triConstraint triangulation)
+  halfArraysEqual = topologyLength == 4 * halfCount
+
+  indexedConstraintEdges =
+    IntSet.fromAscList
+      [ index
+      | index <- [0 .. edgeCount - 1]
+      , pagedUnsafeIndex (triConstraint triangulation) index == 1
+      ]
+
+  faceViolations
+    | not (null cardinalityViolations) || not (null incidenceViolations) = []
+    | otherwise =
+        [ InnerFaceVertexCardinalityMismatch face (length faceVertexIds) (length (nub faceVertexIds))
+        | face <- allFaces triangulation
+        , face /= outerFace
+        , let faceVertexIds = faceVertices triangulation face
+        , length faceVertexIds /= 3 || length (nub faceVertexIds) /= 3
+        ]
+
+  eulerViolations
+    | not (null cardinalityViolations) || verticesCount < 2 = []
+    | numInnerFaces triangulation == 0 =
+        [ CollinearEdgeCountMismatch (verticesCount - 1) edgeCount
+        | edgeCount /= verticesCount - 1
+        ]
+    | otherwise =
+        [ EulerCharacteristicMismatch eulerCharacteristic
+        | eulerCharacteristic /= 2
+        ]
+   where
+    eulerCharacteristic = verticesCount - edgeCount + facesCount
+
+-- | Apply ranges, shared incidence link laws, triangle/representative laws and
+-- orientation to a certified local closure. Whole-value incidence coverage is
+-- an obligation of the admitted source, not a scan repeated for each edit. The caller
+-- supplies the admitted inner faces and the interface pairs; the collar faces
+-- on the other side of those pairs are included here so a protected source
+-- cannot be changed behind the local transaction.  Cardinality and Euler
+-- observations remain global scalar invariants and are deliberately not
+-- rebuilt from the resident mesh.
+validateTopologyClosure
+  :: IntSet.IntSet
+  -> IntSet.IntSet
+  -> Triangulation mode vertex directed undirected face
+  -> [InvariantViolation]
+validateTopologyClosure admittedFaces interfacePairs triangulation =
+  snd (validateTopologyClosureWithStats admittedFaces interfacePairs triangulation)
+
+-- | The support a local domain spans: the admitted faces with the collar
+-- faces across the interface, the directed edges those faces and interface
+-- pairs carry, the vertices those edges join, and the undirected pairs
+-- beneath them. A closure validation walks this selection; its statistics
+-- are the selection's sizes and need no walk.
+data ClosureSelection = ClosureSelection
+  { selectionFaces :: !IntSet.IntSet
+  , selectionEdges :: !IntSet.IntSet
+  , selectionVertices :: !IntSet.IntSet
+  , selectionPairs :: !IntSet.IntSet
+  }
+
+topologyClosureSelection
+  :: IntSet.IntSet
+  -> IntSet.IntSet
+  -> Triangulation mode vertex directed undirected face
+  -> ClosureSelection
+topologyClosureSelection admittedFaces interfacePairs triangulation =
+  ClosureSelection
+    { selectionFaces = selectedFaces
+    , selectionEdges = selectedEdges
+    , selectionVertices = selectedVertices
+    , selectionPairs = selectedPairs
+    }
+ where
+  verticesCount = numVertices triangulation
+  halfCount = numDirectedEdges triangulation
+
+  collarFaces =
+    IntSet.fromList
+      [ rawFace
+      | rawPair <- IntSet.toAscList interfacePairs
+      , rawPair >= 0
+      , rawPair < numUndirectedEdges triangulation
+      , let edge = UndirectedEdgeId (fromIntegral rawPair)
+      , let (forward, backward) = directedPair edge
+      , rawFace <- fmap faceIdIndex [incidentFace triangulation forward, incidentFace triangulation backward]
+      , rawFace > 0
+      ]
+  selectedFaces = IntSet.union admittedFaces collarFaces
+
+  selectedFaceEdges =
+    IntSet.fromList
+      [ rawEdge
+      | rawFace <- IntSet.toAscList selectedFaces
+      , edge <- faceEdgesBounded triangulation (FaceId (fromIntegral rawFace))
+      , rawEdge <- [directedEdgeIdIndex edge, directedEdgeIdIndex (reverseEdge edge)]
+      ]
+  selectedInterfaceEdges =
+    IntSet.fromList
+      [ rawEdge
+      | rawPair <- IntSet.toAscList interfacePairs
+      , rawPair >= 0
+      , rawPair <= (maxBound - 1) `quot` 2
+      , rawEdge <- [2 * rawPair, 2 * rawPair + 1]
+      ]
+  selectedEdges = IntSet.union selectedFaceEdges selectedInterfaceEdges
+  selectedVertices =
+    IntSet.fromList
+      [ rawVertex
+      | rawEdge <- IntSet.toAscList selectedEdges
+      , rawEdge >= 0
+      , rawEdge < halfCount
+      , directedEdgeIdIndex (reverseEdge (DirectedEdgeId (fromIntegral rawEdge))) < halfCount
+      , rawVertex <-
+          [ vertexIdIndex (origin triangulation (DirectedEdgeId (fromIntegral rawEdge)))
+          , vertexIdIndex (destination triangulation (DirectedEdgeId (fromIntegral rawEdge)))
+          ]
+      , rawVertex < verticesCount
+      ]
+  selectedPairs = IntSet.fromList [rawEdge `quot` 2 | rawEdge <- IntSet.toAscList selectedEdges]
+
+closureSelectionStats :: IntSet.IntSet -> ClosureSelection -> ClosureStats
+closureSelectionStats interfacePairs selection =
+  ClosureStats
+    { closureFaces = IntSet.size (selectionFaces selection)
+    , closureDirectedEdges = IntSet.size (selectionEdges selection)
+    , closureVertices = IntSet.size (selectionVertices selection)
+    , closureInterfacePairs = IntSet.size interfacePairs
+    , closureConstraintPairs = IntSet.size (selectionPairs selection)
+    }
+
+-- | The sizes of the closure a local domain spans, read off the materialized
+-- selection: what 'validateTopologyClosureWithStats' reports beside its
+-- violations. The oracle 'topologyClosureStats' is held to.
+topologyClosureSelectionStats
+  :: IntSet.IntSet
+  -> IntSet.IntSet
+  -> Triangulation mode vertex directed undirected face
+  -> ClosureStats
+topologyClosureSelectionStats admittedFaces interfacePairs triangulation =
+  closureSelectionStats interfacePairs (topologyClosureSelection admittedFaces interfacePairs triangulation)
+
+-- | The same sizes counted from the domain's own faces and interface pairs,
+-- for a caller whose value is already admitted: the initial and final
+-- permitted faces are taken separately so their union is never built, every
+-- directed edge is attributed to the one selected face whose cycle carries it
+-- (its reverse to that face only when the face across is unselected), the
+-- pairs are half the edges because the selection is closed under reversal,
+-- and the vertices are the distinct origins of those edges, deduplicated in
+-- a table sized by the closure rather than by the mesh. Equal to
+-- 'topologyClosureSelectionStats' on the union wherever each cycle edge's
+-- incident face is the face whose cycle it lies on, which every admitted
+-- triangulation satisfies.
+topologyClosureStats
+  :: IntSet.IntSet
+  -> IntSet.IntSet
+  -> IntSet.IntSet
+  -> Triangulation mode vertex directed undirected face
+  -> ClosureStats
+topologyClosureStats initialFaces finalFaces interfacePairs triangulation = runST $ do
+  seen <- MUV.replicate capacity (-1)
+  (!cycleEdges, !cycleVertices) <-
+    foldM (visitFace seen) (0, 0) selectedFaceList
+  (!edges, !verticesSeen) <-
+    foldM (visitInterfacePair seen) (cycleEdges, cycleVertices) (IntSet.toAscList interfacePairs)
+  pure
+    ClosureStats
+      { closureFaces = selectedFaceCount
+      , closureDirectedEdges = edges
+      , closureVertices = verticesSeen
+      , closureInterfacePairs = IntSet.size interfacePairs
+      , closureConstraintPairs = edges `quot` 2
+      }
+ where
+  verticesCount = numVertices triangulation
+  halfCount = numDirectedEdges triangulation
+  undirectedCount = numUndirectedEdges triangulation
+
+  collarFaces =
+    IntSet.fromList
+      [ rawFace
+      | rawPair <- IntSet.toAscList interfacePairs
+      , rawPair >= 0
+      , rawPair < undirectedCount
+      , let (forward, backward) = directedPair (UndirectedEdgeId (fromIntegral rawPair))
+      , rawFace <- fmap faceIdIndex [incidentFace triangulation forward, incidentFace triangulation backward]
+      , rawFace > 0
+      ]
+  admitted rawFace = IntSet.member rawFace initialFaces || IntSet.member rawFace finalFaces
+  selected rawFace = admitted rawFace || IntSet.member rawFace collarFaces
+  novelFinal = filter (\rawFace -> not (IntSet.member rawFace initialFaces)) (IntSet.toAscList finalFaces)
+  novelCollar = filter (not . admitted) (IntSet.toAscList collarFaces)
+  selectedFaceList = IntSet.toAscList initialFaces ++ novelFinal ++ novelCollar
+  selectedFaceCount = IntSet.size initialFaces + length novelFinal + length novelCollar
+
+  -- Every vertex of the closure is the origin of a selected edge and there
+  -- are at most three per selected face plus two per interface pair, so a
+  -- half-full table of that size always has a vacant slot.
+  vertexBound = 3 * (IntSet.size initialFaces + IntSet.size finalFaces + IntSet.size collarFaces) + 2 * IntSet.size interfacePairs
+  capacityBits = finiteBitSize (0 :: Int) - countLeadingZeros (max 8 (2 * vertexBound))
+  capacity = 1 `shiftL` capacityBits
+  slotOf :: Int -> Int
+  slotOf rawVertex =
+    fromIntegral ((fromIntegral rawVertex * (0x9E3779B97F4A7C15 :: Word)) `shiftR` (finiteBitSize (0 :: Word) - capacityBits))
+      .&. (capacity - 1)
+
+  visitFace :: MUV.MVector s Int -> (Int, Int) -> Int -> ST s (Int, Int)
+  visitFace seen (!edges, !seenVertices) rawFace =
+    case adjacentEdge triangulation (FaceId (fromIntegral rawFace)) of
+      Nothing -> pure (edges, seenVertices)
+      Just start
+        | directedEdgeIdIndex start >= halfCount -> pure (edges, seenVertices)
+        | otherwise -> do
+            let second = next triangulation start
+            first <- visitCycleEdge seen (edges, seenVertices) start
+            if directedEdgeIdIndex second >= halfCount
+              then pure first
+              else do
+                let third = next triangulation second
+                secondVisited <- visitCycleEdge seen first second
+                if directedEdgeIdIndex third >= halfCount
+                  then pure secondVisited
+                  else visitCycleEdge seen secondVisited third
+
+  -- A cycle edge counts for its face; its reverse counts here only when the
+  -- face across is outside the selection, otherwise that face's cycle counts it.
+  visitCycleEdge :: MUV.MVector s Int -> (Int, Int) -> DirectedEdgeId -> ST s (Int, Int)
+  visitCycleEdge seen (!edges, !seenVertices) edge = do
+    let reversed = reverseEdge edge
+    seenVertices' <- visitOrigin seen seenVertices edge
+    if selected (faceIdIndex (incidentFace triangulation reversed))
+      then pure (edges + 1, seenVertices')
+      else do
+        seenVertices'' <- visitOrigin seen seenVertices' reversed
+        pure (edges + 2, seenVertices'')
+
+  -- An interface edge is already counted through a selected face on either
+  -- side; only a pair with neither side selected adds its two edges here.
+  visitInterfacePair :: MUV.MVector s Int -> (Int, Int) -> Int -> ST s (Int, Int)
+  visitInterfacePair seen (!edges, !seenVertices) rawPair
+    | rawPair < 0 || rawPair > (maxBound - 1) `quot` 2 = pure (edges, seenVertices)
+    | otherwise =
+        foldM
+          (\(!edgesSoFar, !seenSoFar) rawEdge ->
+             if rawEdge < halfCount
+               && ( selected (faceIdIndex (incidentFace triangulation (DirectedEdgeId (fromIntegral rawEdge))))
+                      || selected (faceIdIndex (incidentFace triangulation (DirectedEdgeId (fromIntegral (rawEdge + 1 - 2 * (rawEdge `rem` 2))))))
+                  )
+               then pure (edgesSoFar, seenSoFar)
+               else
+                 if rawEdge < halfCount
+                   then do
+                     seenSoFar' <- visitOrigin seen seenSoFar (DirectedEdgeId (fromIntegral rawEdge))
+                     pure (edgesSoFar + 1, seenSoFar')
+                   else pure (edgesSoFar + 1, seenSoFar))
+          (edges, seenVertices)
+          [2 * rawPair, 2 * rawPair + 1]
+
+  visitOrigin :: forall s. MUV.MVector s Int -> Int -> DirectedEdgeId -> ST s Int
+  visitOrigin seen seenVertices edge
+    | rawVertex >= verticesCount = pure seenVertices
+    | otherwise = insertVertex (slotOf rawVertex)
+   where
+    !rawVertex = vertexIdIndex (origin triangulation edge)
+    insertVertex :: Int -> ST s Int
+    insertVertex !slot = do
+      occupant <- MUV.unsafeRead seen slot
+      if occupant < 0
+        then seenVertices + 1 <$ MUV.unsafeWrite seen slot rawVertex
+        else
+          if occupant == rawVertex
+            then pure seenVertices
+            else insertVertex ((slot + 1) .&. (capacity - 1))
+
+-- | A bounded face's cycle edges, at most three, stopping at the first
+-- handle beyond the arena.
+faceEdgesBounded
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> [DirectedEdgeId]
+faceEdgesBounded triangulation face =
+  case face of
+    FaceId raw
+      | toInteger raw <= 0 || toInteger raw >= toInteger (numFaces triangulation) -> []
+    _ -> adjacentEdges
+ where
+  halfCount = numDirectedEdges triangulation
+  adjacentEdges = case adjacentEdge triangulation face of
+    Nothing -> []
+    Just start
+      | directedEdgeIdIndex start >= halfCount -> []
+      | otherwise ->
+          let second = next triangulation start
+           in if directedEdgeIdIndex second >= halfCount
+                then [start]
+                else
+                  let third = next triangulation second
+                   in if directedEdgeIdIndex third >= halfCount
+                        then [start, second]
+                        else [start, second, third]
+
+validateTopologyClosureWithStats
+  :: IntSet.IntSet
+  -> IntSet.IntSet
+  -> Triangulation mode vertex directed undirected face
+  -> (ClosureStats, [InvariantViolation])
+validateTopologyClosureWithStats admittedFaces interfacePairs triangulation =
+  ( closureSelectionStats interfacePairs selection
+  , rangeViolations ++ edgeViolations ++ faceViolations ++ vertexViolations ++ orientationViolations ++ constraintViolations
+  )
+ where
+  verticesCount = numVertices triangulation
+  halfCount = numDirectedEdges triangulation
+  facesCount = numFaces triangulation
+
+  selection = topologyClosureSelection admittedFaces interfacePairs triangulation
+  selectedFaces = selectionFaces selection
+  selectedEdges = selectionEdges selection
+  selectedVertices = selectionVertices selection
+  selectedPairs = selectionPairs selection
+
+  rangeViolations =
+    originViolations
+      ++ nextViolations
+      ++ previousViolations
+      ++ faceRangeViolations
+      ++ [ FaceAdjacentOutOfRange face edge halfCount
+         | rawFace <- IntSet.toAscList selectedFaces
+         , rawFace > 0
+         , rawFace < facesCount
+         , let face = FaceId (fromIntegral rawFace)
+         , Just edge <- [adjacentEdge triangulation face]
+         , directedEdgeIdIndex edge >= halfCount
+         ]
+
+  -- The selected edges descend once, from the largest handle, so each family
+  -- is consed into ascending order as four separate walks would report it.
+  (originViolations, nextViolations, previousViolations, faceRangeViolations) =
+    selectedEdgeRange (IntSet.toDescList selectedEdges) [] [] [] []
+   where
+    selectedEdgeRange remaining origins nexts previouses faces =
+      case remaining of
+        [] -> (origins, nexts, previouses, faces)
+        rawEdge : rest
+          | rawEdge < 0 || rawEdge >= halfCount ->
+              selectedEdgeRange rest origins nexts previouses faces
+          | otherwise ->
+              let !edge = DirectedEdgeId (fromIntegral rawEdge)
+                  !vertex = origin triangulation edge
+                  !nextEdge = next triangulation edge
+                  !previousEdge = previous triangulation edge
+                  !face = incidentFace triangulation edge
+               in selectedEdgeRange
+                    rest
+                    ( if vertexIdIndex vertex >= verticesCount
+                        then EdgeOriginOutOfRange edge vertex verticesCount : origins
+                        else origins
+                    )
+                    ( if directedEdgeIdIndex nextEdge >= halfCount
+                        then EdgeNextOutOfRange edge nextEdge halfCount : nexts
+                        else nexts
+                    )
+                    ( if directedEdgeIdIndex previousEdge >= halfCount
+                        then EdgePreviousOutOfRange edge previousEdge halfCount : previouses
+                        else previouses
+                    )
+                    ( if faceIdIndex face >= facesCount
+                        then EdgeFaceOutOfRange edge face facesCount : faces
+                        else faces
+                    )
+
+  edgeViolations =
+    concatMap validateEdge (IntSet.toAscList selectedEdges)
+
+  validateEdge rawEdge
+    | rawEdge < 0 || rawEdge >= halfCount = []
+    | otherwise =
+        let edge = DirectedEdgeId (fromIntegral rawEdge)
+            nextEdge = next triangulation edge
+            previousEdge = previous triangulation edge
+            twinEdge = reverseEdge edge
+            nextValid = directedEdgeIdIndex nextEdge < halfCount
+            previousValid = directedEdgeIdIndex previousEdge < halfCount
+            nextNextValid = nextValid && directedEdgeIdIndex (next triangulation nextEdge) < halfCount
+            innerCycle =
+              if incidentFace triangulation edge /= outerFace && nextValid && previousValid && nextNextValid
+                then [InnerFaceNotTriangularAtEdge edge | next triangulation (next triangulation nextEdge) /= edge]
+                else []
+            linkViolations
+              | nextValid && previousValid && directedEdgeIdIndex twinEdge < halfCount =
+                  either (pure . IncidenceViolation) (const [])
+                    (validateIncidenceEdgeLinks (nativePlanarIncidence triangulation) edge)
+              | otherwise = []
+         in linkViolations
+              ++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]
+              ++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]
+              ++ innerCycle
+
+  faceViolations = concatMap validateFace (IntSet.toAscList selectedFaces)
+
+  validateFace rawFace
+    | rawFace <= 0 || rawFace >= facesCount = []
+    | otherwise =
+      let face = FaceId (fromIntegral rawFace)
+       in case adjacentEdge triangulation face of
+          Nothing -> [FaceMissingAdjacentEdge face]
+          Just edge
+            | directedEdgeIdIndex edge >= halfCount -> []
+            | otherwise ->
+                let representedFace = incidentFace triangulation edge
+                    (faceEdges, faceVertices') = triangleEdgesAndVertices face
+                 in [FaceRepresentativeMismatch face edge representedFace | representedFace /= face]
+                      ++ [ InnerFaceVertexCardinalityMismatch face (length faceVertices') (length (nub faceVertices'))
+                         | rawFace > 0
+                         , length faceVertices' /= 3 || length (nub faceVertices') /= 3
+                         ]
+                      ++ [InnerFaceNotTriangularAtEdge edge
+                         | rawFace > 0
+                         , not (triangleClosed faceEdges)
+                         ]
+
+  triangleEdgesAndVertices face =
+    let edges = faceEdgesBounded triangulation face
+        vertices' = fmap (vertexIdIndex . origin triangulation) edges
+     in (edges, vertices')
+
+  triangleClosed edges =
+    case edges of
+      [first, _, third] -> next triangulation third == first
+      _ -> False
+
+  vertexViolations = concatMap validateVertex (IntSet.toAscList selectedVertices)
+
+  validateVertex
+    :: Int
+    -> [InvariantViolation]
+  validateVertex rawVertex =
+    let vertex = VertexId (fromIntegral rawVertex)
+     in case vertexOutEdge triangulation vertex of
+          Nothing
+            | verticesCount > 1 -> [ConnectedVertexMissingOutgoing vertex]
+            | otherwise -> []
+          Just edge
+            | directedEdgeIdIndex edge >= halfCount ->
+                [VertexOutgoingOutOfRange vertex edge halfCount]
+            | otherwise ->
+                [ VertexOutgoingOriginMismatch vertex edge actualOrigin
+                | let actualOrigin = origin triangulation edge
+                , actualOrigin /= vertex
+                ]
+
+  orientationViolations =
+    [ InnerFaceNotCounterClockwise face
+    | rawFace <- IntSet.toAscList selectedFaces
+    , rawFace > 0
+    , let face = FaceId (fromIntegral rawFace)
+    , let (_, vertices') = triangleEdgesAndVertices face
+    , all (< verticesCount) vertices'
+    , [first, second, third] <- [fmap (VertexId . fromIntegral) vertices']
+    , orient2d (vertexPoint triangulation first) (vertexPoint triangulation second) (vertexPoint triangulation third) /= GT
+    ]
+
+  constraintViolations =
+    [ NonCanonicalConstraintFlag edge flag
+    | rawPair <- IntSet.toAscList selectedPairs
+    , rawPair >= 0
+    , let edge = UndirectedEdgeId (fromIntegral rawPair)
+    , rawPair < numUndirectedEdges triangulation
+    , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
+    , flag /= 0 && flag /= 1
+    ]
+      ++ [ CachedConstraintIndexMismatch
+         | rawPair <- IntSet.toAscList selectedPairs
+         , rawPair >= 0
+         , rawPair < numUndirectedEdges triangulation
+         , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
+         , (flag == 1) /= IntSet.member rawPair (triConstraintEdges triangulation)
+         ]
+
+-- | Every edge whose circumcircle is not empty.
+validateDelaunay :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
+validateDelaunay triangulation = concatMap validateEdge (undirectedEdges triangulation)
+ where
+  validateEdge edge
+    | isConstraintEdge triangulation edge = []
+    | isBoundaryEdge triangulation edge = []
+    | otherwise =
+        let directed = normalizedDirected edge
+            twin = reverseEdge directed
+         in case (innerFaceDirectedEdges triangulation (incidentFace triangulation directed), innerFaceDirectedEdges triangulation (incidentFace triangulation twin)) of
+              (Just _, Just _) ->
+                let a = vertexPoint triangulation (origin triangulation directed)
+                    b = vertexPoint triangulation (destination triangulation directed)
+                    c = vertexPoint triangulation (origin triangulation (previous triangulation directed))
+                    d = vertexPoint triangulation (origin triangulation (previous triangulation twin))
+                    convex = orient2d c d b == GT && orient2d d c a == GT
+                    circle = inCircle a b c d
+                    illegal = convex && (circle == GT || (circle == EQ && orderedPair c d < orderedPair a b))
+                 in [LocallyIllegalDelaunayEdge edge | illegal]
+              _ -> [DelaunayIncidentFaceNotTriangular edge]
+
+-- | Topology first; the Delaunay property only if the topology holds.
+validateTriangulation
+  :: Triangulation mode vertex directed undirected face
+  -> [InvariantViolation]
+validateTriangulation triangulation =
+  let topology = validateTopology triangulation
+   in if null topology
+        then validateDelaunay triangulation
+        else topology
+
+-- | Whether 'validateTriangulation' is empty.
+triangulationIsValid
+  :: Triangulation mode vertex directed undirected face
+  -> Bool
+triangulationIsValid = null . validateTriangulation
+
+-- | Signed area, or 'Nothing' where the face is not a triangle.
+faceArea :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
+faceArea triangulation face = do
+  (v0, v1, v2) <- innerFaceVertices triangulation face
+  pure (triangleArea (vertexPoint triangulation v0) (vertexPoint triangulation v1) (vertexPoint triangulation v2))
+
+-- | Smallest interior angle, in degrees.
+faceMinimumAngleDegrees :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
+faceMinimumAngleDegrees triangulation face = do
+  (v0, v1, v2) <- innerFaceVertices triangulation face
+  let p0 = vertexPoint triangulation v0
+      p1 = vertexPoint triangulation v1
+      p2 = vertexPoint triangulation v2
+      a = sqrt (squaredDistance p1 p2)
+      b = sqrt (squaredDistance p2 p0)
+      c = sqrt (squaredDistance p0 p1)
+  if min a (min b c) <= 0
+    then Nothing
+    else Just (minimum [angle b c a, angle c a b, angle a b c])
+ where
+  angle left right opposite = acos (clamp ((left * left + right * right - opposite * opposite) / (2 * left * right))) * 180 / pi
+  clamp = max (-1) . min 1
+
+-- | Decide, in one linear pass and without allocating, whether a
+-- triangulation already has the resident numbering published by the build
+-- component's canonicalizer:
+-- sites in strict lexicographic order; undirected edges in strict
+-- lexicographic order of their endpoint pair with the even half leaving the
+-- lower endpoint; every vertex and face anchored at its least half-edge;
+-- inner faces in order of those anchors, edgeless faces trailing. The arenas
+-- are the only evidence read; no carried claim is consulted. Where
+-- canonical rebuilding would be the identity this mints the same witness for the cost
+-- of the scan rather than the copy.
+canonicalAdmission :: Triangulation mode vertex directed undirected face -> CanonicalAdmission
+canonicalAdmission source
+  | coordinatesAscend vertexTotal coordinateX coordinateY
+      && edgesRanked 1
+      && halvesLeaveLower 0
+      && anchorsOwned 0
+      && anchorsLeast 0
+      && innerFacesRanked 2 =
+      CanonicalKnown
+  | otherwise = CanonicalUnknown
+ where
+  !vertexTotal = numVertices source
+  !edgeTotal = numUndirectedEdges source
+  !faceTotal = numFaces source
+  !directedTotal = 2 * edgeTotal
+  !coordinateX = triPointX source
+  !coordinateY = triPointY source
+  !topologyArena = triHalfTopology source
+  !vertexOutArena = triVertexOut source
+  !faceEdgeArena = triFaceEdge source
+  topology slot = fromIntegral (topologyArena `pagedUnsafeIndex` slot) :: Int
+  originOf directed = topology (4 * directed)
+  faceOf directed = topology (4 * directed + 3)
+  vertexOut vertex = vertexOutArena `pagedUnsafeIndex` vertex
+  faceEdge face = faceEdgeArena `pagedUnsafeIndex` face
+  edgeLow edge = min (originOf (2 * edge)) (originOf (2 * edge + 1))
+  edgeHigh edge = max (originOf (2 * edge)) (originOf (2 * edge + 1))
+
+  edgesRanked !edge
+    | edge >= edgeTotal = True
+    | otherwise =
+        let !previousLow = edgeLow (edge - 1)
+            !currentLow = edgeLow edge
+         in ( previousLow < currentLow
+                || (previousLow == currentLow && edgeHigh (edge - 1) < edgeHigh edge)
+            )
+              && edgesRanked (edge + 1)
+
+  halvesLeaveLower !edge
+    | edge >= edgeTotal = True
+    | otherwise = originOf (2 * edge) <= originOf (2 * edge + 1) && halvesLeaveLower (edge + 1)
+
+  -- Each anchor names a half-edge that really leaves its vertex or bounds its
+  -- face; together with 'anchorsLeast' that makes it the least such one.
+  anchorsOwned !vertex
+    | vertex < vertexTotal =
+        let !anchor = vertexOut vertex
+         in (anchor == noIndex || (unpackIndex anchor < directedTotal && originOf (unpackIndex anchor) == vertex))
+              && anchorsOwned (vertex + 1)
+    | vertex - vertexTotal < faceTotal =
+        let !face = vertex - vertexTotal
+            !anchor = faceEdge face
+         in (anchor == noIndex || (unpackIndex anchor < directedTotal && faceOf (unpackIndex anchor) == face))
+              && anchorsOwned (vertex + 1)
+    | otherwise = True
+
+  anchorsLeast !directed
+    | directed >= directedTotal = True
+    | otherwise =
+        let !packed = packIndex directed
+         in vertexOut (originOf directed) <= packed
+              && faceEdge (faceOf directed) <= packed
+              && anchorsLeast (directed + 1)
+
+  innerFacesRanked !face
+    | face >= faceTotal = True
+    | otherwise =
+        let !previousAnchor = faceEdge (face - 1)
+            !currentAnchor = faceEdge face
+         in (previousAnchor < currentAnchor || (previousAnchor == noIndex && currentAnchor == noIndex))
+              && innerFacesRanked (face + 1)
+
+
+-- | Whether the stored sites are already in strict lexicographic order, in
+-- which case ranking them is the identity and both permutations are free.
+--
+-- Strict rather than non-strict: a triangulation stores each site once, so
+-- equal adjacent coordinates would mean a mesh this function has no ordering
+-- for, and it is the sort's business to say so rather than this predicate's.
+coordinatesAscend :: Int -> Paged Double -> Paged Double -> Bool
+coordinatesAscend total x y = go 1
+ where
+  go !index
+    | index >= total = True
+    | otherwise =
+        let !previousX = x `pagedUnsafeIndex` (index - 1)
+            !currentX = x `pagedUnsafeIndex` index
+         in case compare previousX currentX of
+              LT -> go (index + 1)
+              GT -> False
+              EQ ->
+                y `pagedUnsafeIndex` (index - 1) < y `pagedUnsafeIndex` index
+                  && go (index + 1)
diff --git a/src-dcel/Moonlight/Planar/Interop.hs b/src-dcel/Moonlight/Planar/Interop.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Interop.hs
@@ -0,0 +1,37 @@
+{-# LANGUAGE FlexibleInstances #-}
+
+-- | Boundary conversions between Moonlight points and common coordinate pairs.
+module Moonlight.Planar.Interop
+  ( Coordinate2 (..)
+  , mapCoordinate2
+  ) where
+
+import Data.Complex (Complex ((:+)))
+import Moonlight.Planar.Point (Point (..))
+
+-- | Minimal mint-like interchange class. It owns no geometry and introduces no
+-- second point representation inside the triangulation; conversion happens only
+-- at an ecosystem boundary.
+class Coordinate2 value where
+  toPoint :: value -> Point
+  fromPoint :: Point -> value
+
+instance Coordinate2 (Point) where
+  toPoint = id
+  fromPoint = id
+
+instance Coordinate2 (Double, Double) where
+  toPoint (x, y) = Point x y
+  fromPoint (Point x y) = (x, y)
+
+instance Coordinate2 (Complex Double) where
+  toPoint (x :+ y) = Point x y
+  fromPoint (Point x y) = x :+ y
+
+-- | Transform a coordinate through the canonical 'Point' representation.
+mapCoordinate2
+  :: (Coordinate2 input, Coordinate2 output)
+  => (Point -> Point)
+  -> input
+  -> output
+mapCoordinate2 transform = fromPoint . transform . toPoint
diff --git a/src-dcel/Moonlight/Planar/IntersectionIterator.hs b/src-dcel/Moonlight/Planar/IntersectionIterator.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/IntersectionIterator.hs
@@ -0,0 +1,482 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Ordered traversal of the mesh features met by a line segment.
+module Moonlight.Planar.IntersectionIterator
+  ( Intersection (..)
+  , lineIntersections
+  , lineIntersectionsBetweenVertices
+  , foldCorridorBetweenPoints
+  , foldCorridorBetweenVertices
+  , conflictingEdges
+  , segmentIntersectsNonCollinear
+  ) where
+
+import Data.List (sortBy)
+import Data.Maybe (mapMaybe)
+import Data.Ord (comparing)
+import Data.Void (Void, absurd)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Handles.Iterators.FixedIterators (undirectedEdges, vertices)
+import Moonlight.Planar.Math
+  ( projectionFactor
+  , squaredDistanceWide
+  )
+import Moonlight.Planar.PointLocation
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+
+-- | One crossing: an edge cut, a vertex hit, or a collinear overlap.
+data Intersection
+  = EdgeIntersection !DirectedEdgeId
+  | VertexIntersection !VertexId
+  | EdgeOverlap !DirectedEdgeId
+  deriving stock (Eq, Ord, Show)
+
+-- | Every crossing between two points, ordered along the segment.
+lineIntersections
+  :: Triangulation mode vertex directed undirected face
+  -> QueryPoint
+  -> QueryPoint
+  -> [Intersection]
+lineIntersections triangulation queryFrom queryTo =
+  let !from = queryPointValue queryFrom
+      !to = queryPointValue queryTo
+   in if endpointNearerToWalkSeed triangulation to from
+        then case firstIntersection triangulation queryTo queryFrom of
+          Nothing -> []
+          Just first -> walkIntersections AgainstQuery triangulation to from first
+        else case firstIntersection triangulation queryFrom queryTo of
+          Nothing -> []
+          Just first -> walkIntersections AlongQuery triangulation from to first
+
+-- | 'lineIntersections' between two existing vertices.
+lineIntersectionsBetweenVertices :: Triangulation mode vertex directed undirected face -> VertexId -> VertexId -> [Intersection]
+lineIntersectionsBetweenVertices triangulation fromVertex toVertex =
+  let from = vertexPoint triangulation fromVertex
+      to = vertexPoint triangulation toVertex
+   in walkIntersections AlongQuery triangulation from to (VertexIntersection fromVertex)
+
+-- | The directed edges a crossing list cuts.
+conflictingEdges :: [Intersection] -> [DirectedEdgeId]
+conflictingEdges = mapMaybe asConflict
+ where
+  asConflict (EdgeIntersection edge) = Just edge
+  asConflict _ = Nothing
+
+-- | Fold the corridor one crossing at a time, stopping the instant the step
+-- function answers.
+--
+-- The list-producing walks cannot stop early: their step budget is only known
+-- to have been respected once the walk ends, so the whole corridor is
+-- materialized before the first event is visible. A caller whose answer is
+-- settled by a prefix — anything asking whether some crossing exists — should
+-- not pay for the suffix. 'Nothing' reports a walk that outran its budget and
+-- is the caller's signal to fall back to the materialized walk, which
+-- substitutes the exact scan.
+foldCorridorBetweenVertices
+  :: Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> VertexId
+  -> (state -> Intersection -> Either answer state)
+  -> state
+  -> Maybe (Either answer state)
+foldCorridorBetweenVertices triangulation fromVertex toVertex =
+  foldCorridor
+    triangulation
+    (vertexPoint triangulation fromVertex)
+    (vertexPoint triangulation toVertex)
+    (VertexIntersection fromVertex)
+
+-- | As 'foldCorridorBetweenVertices', for a corridor given by its endpoints.
+-- A segment that meets nothing at all folds to the initial state.
+foldCorridorBetweenPoints
+  :: Triangulation mode vertex directed undirected face
+  -> QueryPoint
+  -> QueryPoint
+  -> (state -> Intersection -> Either answer state)
+  -> state
+  -> Maybe (Either answer state)
+foldCorridorBetweenPoints triangulation queryFrom queryTo step state =
+  case firstIntersection triangulation queryFrom queryTo of
+    Nothing -> Just (Right state)
+    Just first ->
+      foldCorridor
+        triangulation
+        (queryPointValue queryFrom)
+        (queryPointValue queryTo)
+        first
+        step
+        state
+
+foldCorridor
+  :: forall mode vertex directed undirected face state answer.
+     Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> Intersection
+  -> (state -> Intersection -> Either answer state)
+  -> state
+  -> Maybe (Either answer state)
+foldCorridor triangulation from to first step =
+  go (2 * numDirectedEdges triangulation + numVertices triangulation + 8) first
+ where
+  go
+    :: Int
+    -> Intersection
+    -> state
+    -> Maybe (Either answer state)
+  go !remaining !current !state
+    | remaining <= 0 = Nothing
+    | otherwise = case step state current of
+        Left answer -> Just (Left answer)
+        Right advanced ->
+          case nextIntersection triangulation from to current of
+            Nothing -> Just (Right advanced)
+            Just following -> go (remaining - 1) following advanced
+-- The early-answer 'Either' exists to let a caller stop at the first event it
+-- cares about; it should not survive to runtime. Inlining the non-recursive
+-- wrapper puts the worker at each call site with 'step' statically known, so
+-- the constructor is matched where it is built — and at 'walkIntersections',
+-- where the answer is 'Void', the left branch is erased outright.
+{-# INLINE foldCorridor #-}
+
+data QueryDirection = AlongQuery | AgainstQuery
+
+walkIntersections
+  :: QueryDirection
+  -> Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> Intersection
+  -> [Intersection]
+walkIntersections direction triangulation from to first =
+  case foldCorridor triangulation from to first collect [] of
+    Nothing -> orientExact (exactIntersectionScan triangulation from to)
+    Just (Left impossible) -> absurd impossible
+    Just (Right events) -> orientCollected events
+ where
+  collect :: [Intersection] -> Intersection -> Either Void [Intersection]
+  collect accumulated event = Right (event : accumulated)
+  orientExact = case direction of
+    AlongQuery -> id
+    AgainstQuery -> map reverseIntersection . reverse
+  orientCollected = case direction of
+    AlongQuery -> reverse
+    AgainstQuery -> map reverseIntersection
+
+reverseIntersection :: Intersection -> Intersection
+reverseIntersection event = case event of
+  VertexIntersection vertex -> VertexIntersection vertex
+  EdgeIntersection edge -> EdgeIntersection (reverseEdge edge)
+  EdgeOverlap edge -> EdgeOverlap (reverseEdge edge)
+
+-- The unhinted point locator starts at face one. Choosing the segment endpoint
+-- decisively nearer one of that face's vertices shortens the same exact walk
+-- without making ordinary interior chords pay to reverse similar paths.
+endpointNearerToWalkSeed
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> Bool
+endpointNearerToWalkSeed triangulation candidate incumbent =
+  case innerFaceVertices triangulation (FaceId 1) of
+    Just (anchor, _, _) ->
+      let !seed = vertexPoint triangulation anchor
+       in 4 * squaredDistanceWide seed candidate < squaredDistanceWide seed incumbent
+    Nothing -> False
+
+nextIntersection :: Triangulation mode vertex directed undirected face -> Point -> Point -> Intersection -> Maybe Intersection
+nextIntersection triangulation lineFrom lineTo current = case current of
+  EdgeIntersection edge -> case traceDirectionOutOfEdge triangulation edge lineFrom lineTo of
+    EdgeOutHull -> Nothing
+    EdgeOutVertex vertex -> Just (VertexIntersection vertex)
+    EdgeOutEdge nextEdge -> Just (EdgeIntersection nextEdge)
+    EdgeOutNone -> Nothing
+  VertexIntersection vertex
+    | vertexPoint triangulation vertex == lineTo -> Nothing
+    | otherwise -> case traceDirectionOutOfVertex triangulation vertex lineTo of
+        VertexOutHull -> Nothing
+        VertexOutOverlap edge -> Just (EdgeOverlap edge)
+        VertexOutEdge edge ->
+          let from = vertexPoint triangulation (origin triangulation edge)
+              to = vertexPoint triangulation (destination triangulation edge)
+           in if orient2d from to lineTo == LT then Nothing else Just (EdgeIntersection edge)
+  EdgeOverlap edge
+    | lineFrom == lineTo -> Nothing
+    | onClosedSegment lineFrom lineTo (vertexPoint triangulation (destination triangulation edge)) ->
+        Just (VertexIntersection (destination triangulation edge))
+    | otherwise -> Nothing
+
+firstIntersection :: Triangulation mode vertex directed undirected face -> QueryPoint -> QueryPoint -> Maybe Intersection
+firstIntersection triangulation queryFrom queryTo =
+  case locatePoint triangulation queryFrom of
+    EmptyTriangulation -> singleVertexHit
+    OnVertex vertex -> Just (VertexIntersection vertex)
+    OnEdge edge -> Just (classifyStartingEdge edge)
+    InFace face -> firstFromFace face
+    OutsideConvexHull entry -> firstFromOutside entry
+ where
+  !lineFrom = queryPointValue queryFrom
+  !lineTo = queryPointValue queryTo
+  singleVertexHit = case vertices triangulation of
+    [vertex]
+      | onClosedSegment lineFrom lineTo (vertexPoint triangulation vertex) -> Just (VertexIntersection vertex)
+    _ -> Nothing
+
+  classifyStartingEdge edge =
+    let a = vertexPoint triangulation (origin triangulation edge)
+        b = vertexPoint triangulation (destination triangulation edge)
+     in if orient2d lineFrom lineTo a == EQ && orient2d lineFrom lineTo b == EQ
+          then EdgeOverlap (orientAlongLine edge)
+          else EdgeIntersection (orientTowardTarget edge)
+
+  firstFromFace face = firstEdgeFromRing (faceDirectedEdges triangulation face)
+
+  firstEdgeFromRing [] = Nothing
+  firstEdgeFromRing (edge : remaining) =
+    let a = vertexPoint triangulation (origin triangulation edge)
+        b = vertexPoint triangulation (destination triangulation edge)
+     in if segmentIntersectsNonCollinear lineFrom lineTo a b
+          then
+            if orient2d lineFrom lineTo a == EQ
+              then Just (VertexIntersection (origin triangulation edge))
+              else
+                if orient2d lineFrom lineTo b == EQ
+                  then Just (VertexIntersection (destination triangulation edge))
+                  else Just (EdgeIntersection (reverseEdge edge))
+          else firstEdgeFromRing remaining
+
+  -- Outside the region the segment's first contact with it lies on the ring
+  -- the locator's edge sits on, so only that ring can carry the earliest
+  -- event. A ring that outran its budget, an absent locator edge, and
+  -- endpoints that leave the parameter comparison without a total order all
+  -- keep the exact scan.
+  firstFromOutside (Just edge) =
+    case hullEntryIntersection triangulation lineFrom lineTo edge of
+      Just entry -> entry
+      Nothing -> firstFromScan
+  firstFromOutside _ = firstFromScan
+
+  firstFromScan = case exactIntersectionScan triangulation lineFrom lineTo of
+    event : _ -> Just event
+    [] -> Nothing
+
+  orientAlongLine edge =
+    let a = vertexPoint triangulation (origin triangulation edge)
+        b = vertexPoint triangulation (destination triangulation edge)
+     in if projectionFactor lineFrom lineTo a <= projectionFactor lineFrom lineTo b then edge else reverseEdge edge
+
+  orientTowardTarget edge =
+    let a = vertexPoint triangulation (origin triangulation edge)
+        b = vertexPoint triangulation (destination triangulation edge)
+     in if orient2d a b lineTo == LT then reverseEdge edge else edge
+
+data VertexOut
+  = VertexOutHull
+  | VertexOutOverlap !DirectedEdgeId
+  | VertexOutEdge !DirectedEdgeId
+
+data EdgeOut
+  = EdgeOutHull
+  | EdgeOutVertex !VertexId
+  | EdgeOutEdge !DirectedEdgeId
+  | EdgeOutNone
+
+traceDirectionOutOfVertex :: Triangulation mode vertex directed undirected face -> VertexId -> Point -> VertexOut
+traceDirectionOutOfVertex triangulation vertex target =
+  case vertexOutEdge triangulation vertex of
+    Nothing -> VertexOutHull
+    Just start ->
+      let !startSide = sideOf start
+          !rotateCounterClockwise = startSide == GT
+       in go rotateCounterClockwise (numDirectedEdges triangulation + 1) start startSide
+ where
+  go
+    :: Bool
+    -> Int
+    -> DirectedEdgeId
+    -> Ordering
+    -> VertexOut
+  go !rotateCounterClockwise !remaining !current !currentSide
+    | remaining <= 0 = VertexOutHull
+    | currentSide == EQ && projectionFactor currentPoint (edgeTarget current) target >= 0 =
+        VertexOutOverlap current
+    | otherwise =
+        let following = if rotateCounterClockwise then counterClockwise triangulation current else clockwise triangulation current
+            followingSide = sideOf following
+         in if followingSide == EQ && projectionFactor currentPoint (edgeTarget following) target >= 0
+              then VertexOutOverlap following
+              else
+                let faceBetween = if rotateCounterClockwise then incidentFace triangulation current else incidentFace triangulation following
+                 in if faceBetween == outerFace
+                      then VertexOutHull
+                      else
+                        if rotateCounterClockwise == (followingSide == LT)
+                          then
+                            let segment = if rotateCounterClockwise then next triangulation current else previous triangulation (reverseEdge current)
+                             in VertexOutEdge (reverseEdge segment)
+                          else go rotateCounterClockwise (remaining - 1) following followingSide
+
+  currentPoint = vertexPoint triangulation vertex
+  edgeTarget edge = vertexPoint triangulation (destination triangulation edge)
+  sideOf edge = orient2d currentPoint (edgeTarget edge) target
+
+traceDirectionOutOfEdge :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> Point -> Point -> EdgeOut
+traceDirectionOutOfEdge triangulation edge lineFrom lineTo
+  | incidentFace triangulation edge == outerFace = EdgeOutHull
+  | otherwise =
+      case (previousIntersects, nextIntersects) of
+        (True, False) -> EdgeOutEdge (reverseEdge edgePrevious)
+        (False, True) -> EdgeOutEdge (reverseEdge edgeNext)
+        (True, True) -> EdgeOutVertex (origin triangulation edgePrevious)
+        (False, False) -> EdgeOutNone
+ where
+  edgePrevious = previous triangulation edge
+  edgeNext = next triangulation edge
+
+  -- The face runs @edge@ A->B, @edgeNext@ B->C, @edgePrevious@ C->A, so the
+  -- two candidates share C and each vertex's side of the line is read once
+  -- rather than once per candidate. Whether the segment reaches a candidate is
+  -- then asked only of one the line already separates, and a walk that entered
+  -- across A->B leaves that true for exactly one of the two.
+  pointA = vertexPoint triangulation (origin triangulation edge)
+  pointB = vertexPoint triangulation (origin triangulation edgeNext)
+  pointC = vertexPoint triangulation (origin triangulation edgePrevious)
+
+  sideA = orient2d lineFrom lineTo pointA
+  sideB = orient2d lineFrom lineTo pointB
+  sideC = orient2d lineFrom lineTo pointC
+
+  previousIntersects = sideC /= sideA && reaches pointC pointA
+  nextIntersects = sideB /= sideC && reaches pointB pointC
+
+  reaches from to = orient2d from to lineFrom /= orient2d from to lineTo
+
+-- | Whether two segments properly cross; collinear touching does not.
+segmentIntersectsNonCollinear :: Point -> Point -> Point -> Point -> Bool
+segmentIntersectsNonCollinear p0 p1 q0 q1 =
+  -- Equality admits an endpoint on the opposite segment; four equal sides
+  -- reject the collinear case without recomputing either orientation pair.
+  p0Side /= p1Side
+    && q0Side /= q1Side
+ where
+  !p0Side = orient2d q0 q1 p0
+  !p1Side = orient2d q0 q1 p1
+  !q0Side = orient2d p0 p1 q0
+  !q1Side = orient2d p0 p1 q1
+{-# INLINE segmentIntersectsNonCollinear #-}
+
+exactIntersectionScan :: Triangulation mode vertex directed undirected face -> Point -> Point -> [Intersection]
+exactIntersectionScan triangulation lineFrom lineTo =
+  map eventValue . sortBy compareEvent $ vertexEvents ++ edgeEvents
+ where
+  vertexEvents = mapMaybe (vertexEvent triangulation lineFrom lineTo) (vertices triangulation)
+  edgeEvents = mapMaybe (edgeEvent triangulation lineFrom lineTo) (undirectedEdges triangulation)
+
+data Event = Event
+  { eventParameter :: !Double
+  , eventPriority :: {-# UNPACK #-} !Int
+  , eventValue :: !Intersection
+  }
+
+compareEvent :: Event -> Event -> Ordering
+compareEvent = comparing (\event -> (eventParameter event, eventPriority event, eventValue event))
+
+-- Ties in this order are the same 'Intersection', so a traversal that reaches
+-- the minimum by a different route still reports the value the sorted scan's
+-- head reports.
+vertexEvent
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> VertexId
+  -> Maybe (Event)
+vertexEvent triangulation lineFrom lineTo vertex
+  | onClosedSegment lineFrom lineTo point = Just (Event (projectionFactor lineFrom lineTo point) 0 (VertexIntersection vertex))
+  | otherwise = Nothing
+ where
+  point = vertexPoint triangulation vertex
+
+edgeEvent
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> UndirectedEdgeId
+  -> Maybe (Event)
+edgeEvent triangulation lineFrom lineTo edge
+  | oa == EQ && ob == EQ =
+      if low < high
+        then Just (Event low 1 (EdgeOverlap (if projectedA <= projectedB then directed else reverseEdge directed)))
+        else Nothing
+  | segmentsIntersect lineFrom lineTo a b && oa /= EQ && ob /= EQ =
+      Just (Event (segmentIntersectionParameter lineFrom lineTo a b) 2 (EdgeIntersection oriented))
+  | otherwise = Nothing
+ where
+  directed = normalizedDirected edge
+  a = vertexPoint triangulation (origin triangulation directed)
+  b = vertexPoint triangulation (destination triangulation directed)
+  oa = orient2d lineFrom lineTo a
+  ob = orient2d lineFrom lineTo b
+  projectedA = projectionFactor lineFrom lineTo a
+  projectedB = projectionFactor lineFrom lineTo b
+  low = max 0 (min projectedA projectedB)
+  high = min 1 (max projectedA projectedB)
+  oriented = if orient2d a b lineTo == LT then reverseEdge directed else directed
+
+-- | Restrict an outside segment to the hull edge supplied by point location,
+-- then follow only the side on which its entry can lie. The outer 'Maybe'
+-- witnesses a hull walk that closed within its topology budget; the inner one
+-- is the geometric answer. An exhausted or impossible local section descends
+-- to the exact scan owned by 'firstIntersection'.
+hullEntryIntersection
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> Point
+  -> DirectedEdgeId
+  -> Maybe (Maybe Intersection)
+hullEntryIntersection triangulation lineFrom lineTo entry
+  | orient2d entryFrom entryTo lineFrom == EQ =
+      Just
+        ( Just
+            ( VertexIntersection
+                ( if squaredDistanceWide entryTo lineFrom < squaredDistanceWide entryFrom lineFrom
+                    then destination triangulation entry
+                    else origin triangulation entry
+                )
+            )
+        )
+  | otherwise = go (numDirectedEdges triangulation + 1) entry
+ where
+  entryFrom = vertexPoint triangulation (origin triangulation entry)
+  entryTo = vertexPoint triangulation (destination triangulation entry)
+
+  go
+    :: Int
+    -> DirectedEdgeId
+    -> Maybe (Maybe Intersection)
+  go !remaining !edge
+    | remaining <= 0 = Nothing
+    | otherwise =
+        let !edgeFrom = vertexPoint triangulation (origin triangulation edge)
+            !edgeTo = vertexPoint triangulation (destination triangulation edge)
+            !fromSide = orient2d lineFrom lineTo edgeFrom
+            !toSide = orient2d lineFrom lineTo edgeTo
+         in if orient2d edgeFrom edgeTo lineTo == GT
+              then Just Nothing
+              else case (fromSide == GT, toSide /= LT) of
+                (True, True) -> go (remaining - 1) (previous triangulation edge)
+                (False, False) -> go (remaining - 1) (next triangulation edge)
+                (False, True)
+                  | toSide == EQ -> Just (Just (VertexIntersection (destination triangulation edge)))
+                  | fromSide == EQ -> Just (Just (VertexIntersection (origin triangulation edge)))
+                  | otherwise -> Just (Just (EdgeIntersection (reverseEdge edge)))
+                (True, False) -> Nothing
+
+segmentIntersectionParameter :: Point -> Point -> Point -> Point -> Double
+segmentIntersectionParameter (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy)
+  | denominator == 0 = 0
+  | otherwise = ((cx - ax) * (dy - cy) - (cy - ay) * (dx - cx)) / denominator
+ where
+  denominator = (bx - ax) * (dy - cy) - (by - ay) * (dx - cx)
diff --git a/src-dcel/Moonlight/Planar/Math.hs b/src-dcel/Moonlight/Planar/Math.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Math.hs
@@ -0,0 +1,383 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Robust planar predicates and derived Euclidean constructions.
+module Moonlight.Planar.Math
+  ( orient2d
+  , sideQuery
+  , inCircle
+  , orientDetApprox
+  , inCircleDetApprox
+  , onClosedSegment
+  , SegmentRelation (..)
+  , allSegmentRelations
+  , segmentRelation
+  , segmentsProperlyCross
+  , segmentsIntersect
+  , squaredDistance
+  , squaredDistanceWide
+  , segmentDistanceSquared
+  , segmentDistanceSquaredWide
+  , distance
+  , negateVector
+  , normalizeVector
+  , midpoint
+  , centroid
+  , triangleArea
+  , triangleRadiusEdgeRatio
+  , triangleRadiusEdgeRatioSquaredWithArea
+  , triangleRadiusEdgeRatioSquaredWithAreaAndSides
+  , circumcenter
+  , barycentricCoordinates
+  , inDiametralCircle
+  , projectionFactor
+  ) where
+
+import Moonlight.Planar.Internal.Dyadic
+  ( exactBarycentricDeterminants
+  , integerRatioToDouble
+  )
+import Moonlight.Planar.Internal.Predicates qualified as Admitted
+import Moonlight.Planar.Internal.SegmentRelation
+  ( SegmentRelation (..)
+  , allSegmentRelations
+  )
+import Moonlight.Planar.LineSideInfo (LineSideInfo)
+import Moonlight.Planar.Scalar
+  ( isFinite
+  , scalarCcwErrorBound
+  )
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue, canonicalPoint)
+
+-- | Exact relation between two admitted closed segments.
+segmentRelation
+  :: QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> SegmentRelation
+segmentRelation firstFrom firstTo secondFrom secondTo =
+  Admitted.segmentRelation
+    (queryPointValue firstFrom)
+    (queryPointValue firstTo)
+    (queryPointValue secondFrom)
+    (queryPointValue secondTo)
+
+-- | Whether two admitted closed segments share any point.
+segmentsIntersect
+  :: QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> Bool
+segmentsIntersect firstFrom firstTo secondFrom secondTo =
+  Admitted.segmentsIntersect
+    (queryPointValue firstFrom)
+    (queryPointValue firstTo)
+    (queryPointValue secondFrom)
+    (queryPointValue secondTo)
+
+-- | The proper-crossing section of 'segmentRelation'. Consumers which reject
+-- only that constructor need not compute the collinear and endpoint-touch
+-- distinctions required by the complete ADT after either side already proves
+-- separation.
+segmentsProperlyCross
+  :: QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> QueryPoint
+  -> Bool
+segmentsProperlyCross firstFrom firstTo secondFrom secondTo =
+  Admitted.segmentsProperlyCross
+    (queryPointValue firstFrom)
+    (queryPointValue firstTo)
+    (queryPointValue secondFrom)
+    (queryPointValue secondTo)
+
+-- | Classify a coordinate outside the exact-predicate input domain.
+-- | Fast approximate signed orientation determinant.
+orientDetApprox :: Point -> Point -> Point -> Double
+orientDetApprox (Point ax ay) (Point bx by) (Point cx cy) =
+  (ax - cx) * (by - cy) - (ay - cy) * (bx - cx)
+{-# INLINE orientDetApprox #-}
+
+-- | Exact orientation ordering of three admitted points.
+orient2d :: QueryPoint -> QueryPoint -> QueryPoint -> Ordering
+orient2d first second third =
+  Admitted.orient2d
+    (queryPointValue first)
+    (queryPointValue second)
+    (queryPointValue third)
+{-# INLINE orient2d #-}
+
+-- | Exact side of an admitted oriented line.
+sideQuery :: QueryPoint -> QueryPoint -> QueryPoint -> LineSideInfo
+sideQuery from to query =
+  Admitted.sideQuery
+    (queryPointValue from)
+    (queryPointValue to)
+    (queryPointValue query)
+{-# INLINE sideQuery #-}
+
+-- | Fast approximate oriented in-circle determinant.
+inCircleDetApprox
+  :: Point -> Point -> Point -> Point -> Double
+inCircleDetApprox
+  (Point ax ay)
+  (Point bx by)
+  (Point cx cy)
+  (Point dx dy) =
+    alift * bcdet + blift * cadet + clift * abdet
+ where
+  !adx = ax - dx
+  !ady = ay - dy
+  !bdx = bx - dx
+  !bdy = by - dy
+  !cdx = cx - dx
+  !cdy = cy - dy
+  !abdet = adx * bdy - bdx * ady
+  !bcdet = bdx * cdy - cdx * bdy
+  !cadet = cdx * ady - adx * cdy
+  !alift = adx * adx + ady * ady
+  !blift = bdx * bdx + bdy * bdy
+  !clift = cdx * cdx + cdy * cdy
+{-# INLINE inCircleDetApprox #-}
+
+-- | Ordering of the admitted oriented incircle determinant. For a
+-- counter-clockwise triangle, 'GT' means the fourth point lies strictly inside
+-- its circumcircle.
+inCircle
+  :: QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering
+inCircle first second third query =
+  Admitted.inCircle
+    (queryPointValue first)
+    (queryPointValue second)
+    (queryPointValue third)
+    (queryPointValue query)
+{-# INLINE inCircle #-}
+
+-- | Whether an admitted point lies on an admitted closed segment.
+onClosedSegment :: QueryPoint -> QueryPoint -> QueryPoint -> Bool
+onClosedSegment from to query =
+  Admitted.onClosedSegment
+    (queryPointValue from)
+    (queryPointValue to)
+    (queryPointValue query)
+{-# INLINE onClosedSegment #-}
+
+-- | Squared Euclidean distance.
+squaredDistance :: Point -> Point -> Double
+squaredDistance (Point ax ay) (Point bx by) =
+  let !dx = ax - bx
+      !dy = ay - by
+   in dx * dx + dy * dy
+{-# INLINE squaredDistance #-}
+
+-- | Squared Euclidean distance in the mesh's Binary64 coordinate domain.
+squaredDistanceWide :: Point -> Point -> Double
+squaredDistanceWide = squaredDistance
+{-# INLINE squaredDistanceWide #-}
+
+-- | Squared distance from a point to a closed segment.
+segmentDistanceSquared
+  :: Point -> Point -> Point -> Double
+segmentDistanceSquared from@(Point ax ay) to@(Point bx by) point@(Point px py)
+  | lengthSquared == 0 = squaredDistance from point
+  | factor <= 0 = squaredDistance from point
+  | factor >= 1 = squaredDistance to point
+  | otherwise = squaredDistance point (Point (ax + factor * dx) (ay + factor * dy))
+ where
+  !dx = bx - ax
+  !dy = by - ay
+  !lengthSquared = dx * dx + dy * dy
+  !factor = ((px - ax) * dx + (py - ay) * dy) / lengthSquared
+{-# INLINE segmentDistanceSquared #-}
+
+-- | Comparison form retained beside 'segmentDistanceSquared' for callers that
+-- state metric intent explicitly.
+segmentDistanceSquaredWide
+  :: Point -> Point -> Point -> Double
+segmentDistanceSquaredWide = segmentDistanceSquared
+{-# INLINE segmentDistanceSquaredWide #-}
+
+-- | Euclidean distance.
+distance :: Point -> Point -> Double
+distance left right = sqrt (squaredDistance left right)
+{-# INLINE distance #-}
+
+-- | Negate a point interpreted as a vector.
+negateVector :: Point -> Point
+negateVector (Point x y) = Point (-x) (-y)
+{-# INLINE negateVector #-}
+
+-- | Normalize a point interpreted as a vector without overflowing its length.
+normalizeVector :: Point -> Point
+normalizeVector (Point x y)
+  | scale == 0 = Point 0 0
+  | otherwise =
+      let !scaledX = x / scale
+          !scaledY = y / scale
+          !length' = sqrt (scaledX * scaledX + scaledY * scaledY)
+       in Point (scaledX / length') (scaledY / length')
+ where
+  !scale = max (abs x) (abs y)
+{-# INLINE normalizeVector #-}
+
+-- | Midpoint of two points.
+midpoint :: Point -> Point -> Point
+midpoint (Point ax ay) (Point bx by) = Point (0.5 * ax + 0.5 * bx) (0.5 * ay + 0.5 * by)
+{-# INLINE midpoint #-}
+
+-- | Centroid of three points.
+centroid :: Point -> Point -> Point -> Point
+centroid (Point ax ay) (Point bx by) (Point cx cy) =
+  Point (ax + (bx - ax) / 3 + (cx - ax) / 3) (ay + (by - ay) / 3 + (cy - ay) / 3)
+{-# INLINE centroid #-}
+
+-- | Unsigned area of a triangle.
+triangleArea :: Point -> Point -> Point -> Double
+triangleArea a b c = 0.5 * abs (orientDetApprox a b c)
+{-# INLINE triangleArea #-}
+
+-- | Circumradius divided by shortest edge length.
+triangleRadiusEdgeRatio
+  :: Point -> Point -> Point -> Maybe Double
+triangleRadiusEdgeRatio p0 p1 p2
+  | area <= 0 || shortest <= 0 = Nothing
+  | not (isFinite ratio) = Nothing
+  | otherwise = Just ratio
+ where
+  !area = triangleArea p0 p1 p2
+  !side01 = distance p0 p1
+  !side12 = distance p1 p2
+  !side20 = distance p2 p0
+  !shortest = min side01 (min side12 side20)
+  !otherProduct
+    | side01 <= side12 && side01 <= side20 = side12 * side20
+    | side12 <= side20 = side20 * side01
+    | otherwise = side01 * side12
+  !ratio = otherProduct / (4 * area)
+
+-- | The square of 'triangleRadiusEdgeRatio', for a triangle whose area the
+-- caller already has.
+--
+-- The ratio is only ever compared against a bound, and both sides are
+-- non-negative, so the comparison can be made between squares. That is the
+-- whole reason to have this: it settles the same question without the three
+-- square roots the lengths would need, on the path taken by every face
+-- refinement considers.
+--
+-- The area is a parameter and a degenerate triangle answers with an infinity
+-- rather than an absence, because the caller on that path has already computed
+-- the area to ask the area question and does nothing with the absence but
+-- compare an infinity in its place.
+triangleRadiusEdgeRatioSquaredWithArea
+  :: Double -> Point -> Point -> Point -> Double
+triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2 =
+  triangleRadiusEdgeRatioSquaredWithAreaAndSides
+    area
+    (squaredDistance p0 p1)
+    (squaredDistance p1 p2)
+    (squaredDistance p2 p0)
+
+-- | The same squared ratio from side squares already paid for by a caller.
+triangleRadiusEdgeRatioSquaredWithAreaAndSides
+  :: Double -> Double -> Double -> Double -> Double
+triangleRadiusEdgeRatioSquaredWithAreaAndSides area side01 side12 side20
+  | area <= 0 || shortest <= 0 = 1 / 0
+  | not (isFinite ratio) = 1 / 0
+  | otherwise = ratio
+ where
+  !shortest = min side01 (min side12 side20)
+  !otherProduct
+    | side01 <= side12 && side01 <= side20 = side12 * side20
+    | side12 <= side20 = side20 * side01
+    | otherwise = side01 * side12
+  !ratio = otherProduct / (16 * area * area)
+
+-- The scale the determinants are divided by cancels out of the quotient
+-- exactly, so the computation works on the unscaled differences and divides
+-- once per coordinate. Scaling would only matter against overflow, and the
+-- validated coordinate domain (|x| <= 3.3e60) keeps every intermediate below
+-- 1e183, five orders below the Double ceiling; the four divisions it cost
+-- are the circumcentre's hot-path price. Identical points answer through the
+-- denominator, which is exactly zero exactly when they are collinear.
+-- | Circumcenter of a nondegenerate triangle.
+circumcenter
+  :: Point -> Point -> Point -> Maybe (Point)
+circumcenter (Point ax ay) (Point bx by) (Point cx cy)
+  | denominator == 0 = Nothing
+  | not (isFinite resultX && isFinite resultY) = Nothing
+  | otherwise = Just (canonicalPoint (Point resultX resultY))
+ where
+  !bax = bx - ax
+  !bay = by - ay
+  !cax = cx - ax
+  !cay = cy - ay
+  !bLength = bax * bax + bay * bay
+  !cLength = cax * cax + cay * cay
+  !denominator = 2 * (bax * cay - bay * cax)
+  !offsetX = (cay * bLength - bay * cLength) / denominator
+  !offsetY = (bax * cLength - cax * bLength) / denominator
+  !resultX = ax + offsetX
+  !resultY = ay + offsetY
+
+-- | Barycentric coordinates of a point in a nondegenerate triangle.
+barycentricCoordinates
+  :: Point -> Point -> Point -> Point
+  -> Maybe (Double, Double, Double)
+barycentricCoordinates a@(Point ax ay) b@(Point bx by) c@(Point cx cy) query@(Point qx qy)
+  | all reliable [denominatorInfo, weightAInfo, weightBInfo, weightCInfo] =
+      if denominator == 0
+        then Nothing
+        else Just (weightA / denominator, weightB / denominator, weightC / denominator)
+  | exactDenominator == 0 = Nothing
+  | otherwise =
+      Just
+        ( integerRatioToDouble exactWeightA exactDenominator
+        , integerRatioToDouble exactWeightB exactDenominator
+        , integerRatioToDouble exactWeightC exactDenominator
+        )
+ where
+  !denominatorInfo@(denominator, _) = determinantInfo a b c
+  !weightAInfo@(weightA, _) = determinantInfo query b c
+  !weightBInfo@(weightB, _) = determinantInfo a query c
+  !weightCInfo@(weightC, _) = determinantInfo a b query
+  (!exactDenominator, !exactWeightA, !exactWeightB, !exactWeightC) =
+    exactBarycentricDeterminants ax ay bx by cx cy qx qy
+
+  reliable (determinant, determinantSum) =
+    isFinite determinant && abs determinant > scalarCcwErrorBound * determinantSum
+
+  determinantInfo :: Point -> Point -> Point -> (Double, Double)
+  determinantInfo (Point px py) (Point rx ry) (Point sx sy) =
+    let !left = (px - sx) * (ry - sy)
+        !right = (py - sy) * (rx - sx)
+     in (left - right, abs left + abs right)
+
+-- | Whether an admitted point lies in the closed diametral disk of an admitted
+-- segment, decided by the sign of @(a-p)·(b-p)@.
+--
+-- The approximation is two coordinate differences and one product per term
+-- combined additively, which is the arithmetic shape 'orient2d' is analysed
+-- under: the forward error of @fl(fl(a⊖b) ⊗ fl(c⊖d))@ combined by one rounded
+-- addition does not depend on the sign of that combination. The orientation
+-- coefficient therefore transfers unchanged, and the exact dot product remains
+-- the oracle for the uncertain band.
+inDiametralCircle :: QueryPoint -> QueryPoint -> QueryPoint -> Bool
+inDiametralCircle from to query =
+  Admitted.inDiametralCircle
+    (queryPointValue from)
+    (queryPointValue to)
+    (queryPointValue query)
+{-# INLINE inDiametralCircle #-}
+
+-- | Projection parameter of a point onto an oriented segment line.
+projectionFactor :: Point -> Point -> Point -> Double
+projectionFactor (Point ax ay) (Point bx by) (Point qx qy)
+  | lengthSquared == 0 = 0
+  | otherwise = ((qx - ax) * dx + (qy - ay) * dy) / lengthSquared
+ where
+  !dx = bx - ax
+  !dy = by - ay
+  !lengthSquared = dx * dx + dy * dy
+{-# INLINE projectionFactor #-}
diff --git a/src-dcel/Moonlight/Planar/Payload.hs b/src-dcel/Moonlight/Planar/Payload.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Payload.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE RankNTypes #-}
+
+-- | The payload parameters' functorial and traversable structure: relabeling a
+-- payload touches no geometry, so every law here holds for the reason that a
+-- triangulation's points and its annotations are separate things.
+module Moonlight.Planar.Payload
+  ( PayloadTraversal
+  , geometryOnlyPublication
+  , vertexPayloads
+  , directedPayloads
+  , undirectedPayloads
+  , facePayloads
+  , mapVertices
+  , mapDirectedEdges
+  , mapUndirectedEdges
+  , mapFaces
+  , overPayloads
+  , foldPayloads
+  , payloadList
+  ) where
+
+import Data.Functor.Const (Const (..))
+import Data.Functor.Identity (Identity (..))
+import Data.Monoid (Endo (..))
+import Moonlight.Planar.Internal.Representation
+  ( PayloadTraversal
+  , directedPayloads
+  , facePayloads
+  , mapDirectedEdges
+  , mapFaces
+  , mapUndirectedEdges
+  , mapVertices
+  , undirectedPayloads
+  , geometryOnlyPublication
+  , vertexPayloads
+  )
+
+-- | Relabel every payload a traversal reaches.
+--
+-- The 'mapVertices' family is this at each parameter and cheaper: a pure map
+-- leaves an unmaterialized page unmaterialized, where a traversal must visit
+-- every slot the page would have reported and so materializes it. Reach for
+-- this one when the traversal is chosen at runtime, and for the named map when
+-- the parameter is known where you stand.
+overPayloads
+  :: PayloadTraversal source target payload payload'
+  -> (payload -> payload')
+  -> source
+  -> target
+overPayloads traversal relabel = runIdentity . traversal (Identity . relabel)
+{-# INLINE overPayloads #-}
+
+-- | Summarize every payload a traversal reaches.
+foldPayloads
+  :: Monoid summary
+  => PayloadTraversal source source payload payload
+  -> (payload -> summary)
+  -> source
+  -> summary
+foldPayloads traversal measure = getConst . traversal (Const . measure)
+{-# INLINE foldPayloads #-}
+
+-- | Every payload a traversal reaches, in visit order. Accumulated through
+-- t'Endo' so the list is built by a right fold rather than by repeated append.
+payloadList :: PayloadTraversal source source payload payload -> source -> [payload]
+payloadList traversal source = appEndo (foldPayloads traversal (Endo . (:)) source) []
+{-# INLINE payloadList #-}
diff --git a/src-dcel/Moonlight/Planar/PointLocation.hs b/src-dcel/Moonlight/Planar/PointLocation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/PointLocation.hs
@@ -0,0 +1,146 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Walk location over the finite mesh; the hinted form takes a starting point
+-- and reports the steps it spent.
+module Moonlight.Planar.PointLocation
+  ( locatePoint
+  , locatePointWithHint
+  ) where
+
+import Data.List (find)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Handles.Iterators.FixedIterators
+import Moonlight.Planar.Internal.FaceProbe
+import Moonlight.Planar.Internal.Predicates
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+
+-- | Locate an admitted point without an initial topology hint.
+locatePoint :: Triangulation mode vertex directed undirected face -> QueryPoint -> Location
+locatePoint triangulation query = fst (locatePointWithHint triangulation Nothing query)
+
+-- | Locate an admitted point from an optional starting cell and report the
+-- descent work.
+locatePointWithHint :: Triangulation mode vertex directed undirected face -> Maybe LocationHint -> QueryPoint -> (Location, LocationStats)
+locatePointWithHint triangulation hint queryPoint
+  | numVertices triangulation == 0 = (EmptyTriangulation, emptyLocationStats)
+  | numInnerFaces triangulation == 0 = (locateDegenerate triangulation query, emptyLocationStats)
+  | otherwise = walk budget 0 startFace
+ where
+  !query = queryPointValue queryPoint
+  !budget = max 8 (numFaces triangulation + numUndirectedEdges triangulation + 4)
+  !startFace = chooseStart triangulation hint
+
+  walk
+    :: Int
+    -> Int
+    -> FaceId
+    -> (Location, LocationStats)
+  walk !remaining !steps !face
+    | remaining <= 0 = scanAll steps
+    | face == outerFace =
+        (outsideLocation triangulation query, LocationStats steps False)
+    | otherwise =
+        case probeFace triangulation face query of
+          FaceHit location -> (location, LocationStats (steps + 1) False)
+          FaceCross edge ->
+            let adjacent = incidentFace triangulation edge
+             in if adjacent == outerFace
+                  then (OutsideConvexHull (Just edge), LocationStats (steps + 1) False)
+                  else walk (remaining - 1) (steps + 1) adjacent
+          FaceMiss -> scanAll (steps + 1)
+
+  scanAll !steps =
+    case findHit (innerFaces triangulation) of
+      Just location -> (location, LocationStats steps True)
+      Nothing -> (outsideLocation triangulation query, LocationStats steps True)
+
+  findHit [] = Nothing
+  findHit (face : remaining) =
+    case probeFace triangulation face query of
+      FaceHit location -> Just location
+      _ -> findHit remaining
+
+data FaceProbe = FaceHit !Location | FaceCross !DirectedEdgeId | FaceMiss
+
+probeFace :: Triangulation mode vertex directed undirected face -> FaceId -> Point -> FaceProbe
+probeFace triangulation face query =
+  case innerFaceDirectedEdges triangulation face of
+    Nothing -> FaceMiss
+    Just (e0, e1, e2) ->
+      let v0 = origin triangulation e0
+          v1 = origin triangulation e1
+          v2 = origin triangulation e2
+          p0 = vertexPoint triangulation v0
+          p1 = vertexPoint triangulation v1
+          p2 = vertexPoint triangulation v2
+       in case probeBoundary reverseEdge query e0 v0 p0 v1 p1 of
+            BoundaryOnVertex vertex -> FaceHit (OnVertex vertex)
+            BoundaryOnEdge boundary -> FaceHit (OnEdge boundary)
+            firstBoundary ->
+              case probeBoundary reverseEdge query e1 v1 p1 v2 p2 of
+                BoundaryOnVertex vertex -> FaceHit (OnVertex vertex)
+                BoundaryOnEdge boundary -> FaceHit (OnEdge boundary)
+                secondBoundary ->
+                  case probeBoundary reverseEdge query e2 v2 p2 v0 p0 of
+                    BoundaryOnVertex vertex -> FaceHit (OnVertex vertex)
+                    BoundaryOnEdge boundary -> FaceHit (OnEdge boundary)
+                    BoundaryCrossing boundary -> FaceCross boundary
+                    BoundaryClear ->
+                      case secondBoundary of
+                        BoundaryCrossing boundary -> FaceCross boundary
+                        _ -> case firstBoundary of
+                          BoundaryCrossing boundary -> FaceCross boundary
+                          _ -> FaceHit (InFace face)
+
+locateDegenerate :: Triangulation mode vertex directed undirected face -> Point -> Location
+locateDegenerate triangulation query =
+  case find ((== query) . vertexPoint triangulation) (vertices triangulation) of
+    Just vertex -> OnVertex vertex
+    Nothing ->
+      case find contains (undirectedEdges triangulation) of
+        Just edge -> OnEdge (normalizedDirected edge)
+        Nothing ->
+          case undirectedEdges triangulation of
+            [] -> OutsideConvexHull Nothing
+            edge : _ ->
+              let forward = normalizedDirected edge
+                  from = vertexPoint triangulation (origin triangulation forward)
+                  to = vertexPoint triangulation (destination triangulation forward)
+                  directed = case orient2d from to query of
+                    GT -> forward
+                    LT -> reverseEdge forward
+                    EQ -> forward
+               in OutsideConvexHull (Just directed)
+ where
+  contains edge =
+    let forward = normalizedDirected edge
+     in onClosedSegment
+          (vertexPoint triangulation (origin triangulation forward))
+          (vertexPoint triangulation (destination triangulation forward))
+          query
+
+outsideLocation :: Triangulation mode vertex directed undirected face -> Point -> Location
+outsideLocation triangulation query =
+  OutsideConvexHull (find visible (faceDirectedEdges triangulation outerFace))
+ where
+  visible edge =
+    orient2d
+      (vertexPoint triangulation (origin triangulation edge))
+      (vertexPoint triangulation (destination triangulation edge))
+      query
+      /= LT
+
+chooseStart :: Triangulation mode vertex directed undirected face -> Maybe LocationHint -> FaceId
+chooseStart triangulation hint =
+  case hint of
+    Just (FaceHint face)
+      | face /= outerFace && faceIdIndex face < numFaces triangulation -> face
+    Just (VertexHint vertex@(VertexId index))
+      | toInteger index < toInteger (numVertices triangulation) ->
+          case [face | edge <- vertexOutgoingEdges triangulation vertex, let face = incidentFace triangulation edge, face /= outerFace] of
+            face : _ -> face
+            [] -> FaceId 1
+      | otherwise -> FaceId 1
+    _ -> FaceId 1
diff --git a/src-dcel/Moonlight/Planar/Region.hs b/src-dcel/Moonlight/Planar/Region.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Region.hs
@@ -0,0 +1,457 @@
+-- | Exact polygon authoring and grouped publication through the resident DCEL
+-- region traversal. Construction validates topology once; downstream overlay
+-- consumes only admitted layers.
+module Moonlight.Planar.Region
+  ( ExactLoop
+  , exactLoop
+  , exactLoopPoints
+  , PolygonComponent
+  , polygonComponent
+  , polygonOuterLoop
+  , polygonHoleLoops
+  , PlanarRegion
+  , planarRegion
+  , planarRegionComponents
+  , emptyPlanarRegion
+  , regionBoundaryEdges
+  , RegionPointLocation (..)
+  , regionPointLocation
+  , PlanarLayer
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  , planarLayer
+  , planarLayerLabelAt
+  , RegionValidationError (..)
+  , RegionPublicationError (..)
+  , labelledPlanarLayer
+  ) where
+
+import Control.Monad (unless)
+import Data.Bifunctor (first)
+import Data.Foldable (traverse_)
+import Data.List (sort)
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Vector as V
+import Moonlight.Planar.Exact
+  ( ExactBounds
+  , boundsOverlap
+  , pointInBounds
+  , ExactPoint
+  , ExactSegment
+  , SegmentRelation (..)
+  , exactOnClosedSegment
+  , exactOrient2d
+  , exactPointCross
+  , exactPointCoordinates
+  , exactSegment
+  , exactSegmentEndpoints
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( admitsSimpleCycleEdgeRelation
+  , cyclePairs
+  , rotateCycleLeast
+  , simplifyBoundaryCycle
+  )
+import Moonlight.Planar.Internal.ExactSegmentEvents
+  ( ExactSweepSegmentId (..)
+  , exactSegmentEventPlan
+  , exactSegmentRelationMap
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( exactSignum )
+import Moonlight.Planar.Internal.Region.Publication (labelledPlanarLayer)
+import Moonlight.Planar.Internal.Region.Bounds
+  ( componentBounds
+  , exactLoopBounds
+  , overlappingOptionalPairs
+  , overlappingPairs
+  , overlappingPairsBetween
+  , regionBounds
+  )
+import Moonlight.Planar.Internal.Region.Types
+  ( ExactLoop (..)
+  , PlanarLayer (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , polygonOuterLoop
+  , polygonHoleLoops
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  , RegionPointLocation (..)
+  , RegionPublicationError (..)
+  , RegionValidationError (..)
+  )
+
+data LoopRelationWitness = LoopRelationWitness
+  !Int
+  !Int
+  !ExactSegment
+  !ExactSegment
+  !SegmentRelation
+
+-- | One admitted loop with the bounds and segment vector every relation
+-- question reads, built once per loop rather than once per pair asked.
+data PreparedLoop = PreparedLoop
+  !ExactLoop
+  !ExactBounds
+  !(V.Vector ExactSegment)
+
+prepareLoop :: ExactLoop -> Either RegionValidationError PreparedLoop
+prepareLoop loop =
+  PreparedLoop loop (exactLoopBounds loop) <$> loopSegments (exactLoopPoints loop)
+
+preparedBounds :: PreparedLoop -> ExactBounds
+preparedBounds (PreparedLoop _ bounds _) = bounds
+
+-- | Admit and canonicalize one simple exact cycle.
+exactLoop :: NonEmpty ExactPoint -> Either RegionValidationError ExactLoop
+exactLoop submitted = do
+  let withoutRepeatedTerminal = removeRepeatedTerminal (NonEmpty.toList submitted)
+  (_, simplified) <-
+    simplifyBoundaryCycle
+      RegionLoopDegenerate
+      exactRedundant
+      exactOrient2d
+      id
+      withoutRepeatedTerminal
+  let canonical = rotateCycleLeast simplified
+  validateSimpleLoop canonical
+  pure (ExactLoop canonical)
+
+-- | Read the canonical cycle points.
+exactLoopPoints :: ExactLoop -> NonEmpty ExactPoint
+exactLoopPoints (ExactLoop points) = points
+
+-- | Admit one component after checking orientation, containment, boundary
+-- relations, and pairwise-disjoint hole interiors.
+polygonComponent
+  :: ExactLoop
+  -> [ExactLoop]
+  -> Either RegionValidationError PolygonComponent
+polygonComponent outer holes = do
+  let outerWinding = loopWinding outer
+  if outerWinding == GT
+    then Right ()
+    else Left (RegionOuterLoopWinding outerWinding)
+  traverse_ validateHoleWinding indexedHoles
+  unless (null holes) $ do
+    preparedOuter <- prepareLoop outer
+    preparedHoles <- traverse (traverse prepareLoop) indexedHoles
+    traverse_ (validateHole preparedOuter) preparedHoles
+    traverse_
+      validateHolePair
+      (overlappingPairs (preparedBounds . snd) preparedHoles)
+  pure (PolygonComponent outer (sort holes))
+ where
+  indexedHoles = zip [0 ..] holes
+  validateHoleWinding (holeIndex, hole) =
+    let winding = loopWinding hole
+     in if winding == LT
+          then Right ()
+          else Left (RegionHoleLoopWinding holeIndex winding)
+  validateHole preparedOuter (holeIndex, preparedHole) = do
+    relations <- crossLoopRelations preparedOuter preparedHole
+    case relations of
+      LoopRelationWitness outerEdge holeEdge _ _ relation : _ ->
+        Left (RegionBoundaryRelation outerEdge holeEdge relation)
+      [] -> Right ()
+    let location = preparedPointLocation preparedOuter (firstPreparedPoint preparedHole)
+    if location == RegionInterior
+      then Right ()
+      else Left (RegionHoleLocation holeIndex location)
+  validateHolePair ((leftIndex, left), (rightIndex, right)) =
+    do
+      relations <- crossLoopRelations left right
+      case relations of
+        LoopRelationWitness _ _ _ _ relation : _ ->
+          Left (RegionBoundaryRelation leftIndex rightIndex relation)
+        []
+          | loopContainsInteriorPoint left right || loopContainsInteriorPoint right left ->
+              Left (RegionComponentInteriorOverlap leftIndex rightIndex)
+          | otherwise -> Right ()
+
+-- | Admit a finite region after checking that component interiors do not
+-- overlap. Boundary-only contact remains lawful.
+planarRegion
+  :: [PolygonComponent]
+  -> Either RegionValidationError PlanarRegion
+planarRegion components = do
+  traverse_
+    validatePair
+    (overlappingPairs (componentBounds . snd) (zip [0 ..] components))
+  pure (PlanarRegion (sort components))
+ where
+  validatePair ((leftIndex, left), (rightIndex, right)) = do
+    overlaps <- componentInteriorsOverlap left right
+    if overlaps
+      then Left (RegionComponentInteriorOverlap leftIndex rightIndex)
+      else Right ()
+
+-- | Observe the canonically ordered components.
+planarRegionComponents :: PlanarRegion -> [PolygonComponent]
+planarRegionComponents (PlanarRegion components) = components
+
+-- | The empty finite region.
+emptyPlanarRegion :: PlanarRegion
+emptyPlanarRegion = PlanarRegion []
+
+-- | Locate an exact point against all components. Boundary membership has
+-- priority over interior membership.
+regionPointLocation :: PlanarRegion -> ExactPoint -> RegionPointLocation
+regionPointLocation (PlanarRegion components) query =
+  foldr
+    (combineLocation . (`componentPointLocation` query))
+    RegionExterior
+    components
+
+-- | Admit one labelled layer. Different labels may touch but their interiors
+-- may not overlap, and the outside label cannot also name a bounded region.
+planarLayer
+  :: Ord label
+  => label
+  -> Map label PlanarRegion
+  -> Either RegionValidationError (PlanarLayer label)
+planarLayer outside regions
+  | Map.member outside regions = Left RegionOutsideLabelUsed
+  | otherwise = do
+      let indexed = zip [0 ..] (Map.toAscList regions)
+      traverse_
+        (\((leftIndex, (_, left)), (rightIndex, (_, right))) -> do
+           overlaps <- regionsInteriorsOverlap left right
+           if overlaps
+             then Left (RegionLayerInteriorOverlap leftIndex rightIndex)
+             else Right ())
+        (overlappingOptionalPairs (regionBounds . snd . snd) indexed)
+      pure (PlanarLayer outside regions)
+
+-- | Label an exact point known to lie in a relatively open two-cell. Boundary
+-- points conservatively retain the outside label.
+planarLayerLabelAt :: PlanarLayer label -> ExactPoint -> label
+planarLayerLabelAt layer query =
+  case
+    [ label
+    | (label, region) <- Map.toAscList (planarLayerRegions layer)
+    , regionPointLocation region query == RegionInterior
+    ] of
+    label : _ -> label
+    [] -> planarLayerOutsideLabel layer
+
+removeRepeatedTerminal :: Eq value => [value] -> [value]
+removeRepeatedTerminal values =
+  case values of
+    [] -> []
+    firstValue : remaining ->
+      case reverse remaining of
+        finalValue : reversedMiddle
+          | firstValue == finalValue -> firstValue : reverse reversedMiddle
+        _ -> values
+
+exactRedundant :: ExactPoint -> ExactPoint -> ExactPoint -> Bool
+exactRedundant previous current next =
+  exactOrient2d previous current next == EQ
+    && exactOnClosedSegment previous next current
+
+validateSimpleLoop :: NonEmpty ExactPoint -> Either RegionValidationError ()
+validateSimpleLoop points = do
+  segments <- loopSegments points
+  plan <- first RegionSegmentEventsInvalid (exactSegmentEventPlan segments)
+  case
+    [ (leftIndex, rightIndex, relation)
+    | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
+        Map.toAscList (exactSegmentRelationMap plan)
+    , not
+        ( admitsSimpleCycleEdgeRelation
+            segmentCount
+            leftIndex
+            rightIndex
+            relation
+        )
+    ] of
+    (leftIndex, rightIndex, relation) : _ ->
+      Left (RegionLoopSelfRelation leftIndex rightIndex relation)
+    [] -> Right ()
+ where
+  segmentCount = NonEmpty.length points
+
+loopSegments
+  :: NonEmpty ExactPoint
+  -> Either RegionValidationError (V.Vector ExactSegment)
+loopSegments points =
+  V.fromList
+    <$> traverse
+      (\(from, to) ->
+         first (const (RegionLoopDegenerate (NonEmpty.toList points)))
+           (exactSegment from to))
+      (cyclePairs points)
+
+loopWinding :: ExactLoop -> Ordering
+loopWinding (ExactLoop points) =
+  exactSignum
+    ( List.foldl'
+        (\signedArea (from, to) ->
+           signedArea + exactPointCross from to)
+        0
+        (cyclePairs points)
+    )
+
+pointLocationInLoop :: ExactLoop -> ExactPoint -> RegionPointLocation
+pointLocationInLoop loop = pointLocationInLoopWithin (exactLoopBounds loop) loop
+
+preparedPointLocation :: PreparedLoop -> ExactPoint -> RegionPointLocation
+preparedPointLocation (PreparedLoop loop bounds _) = pointLocationInLoopWithin bounds loop
+
+pointLocationInLoopWithin :: ExactBounds -> ExactLoop -> ExactPoint -> RegionPointLocation
+pointLocationInLoopWithin bounds loop query
+  | not (pointInBounds query bounds) = RegionExterior
+  | any (\(from, to) -> exactOnClosedSegment from to query) edges = RegionOnBoundary
+  | odd (length (filter crossesRay edges)) = RegionInterior
+  | otherwise = RegionExterior
+ where
+  edges = cyclePairs (exactLoopPoints loop)
+  (_, py) = exactPointCoordinates query
+  crossesRay (from, to) =
+    let (_, ay) = exactPointCoordinates from
+        (_, by) = exactPointCoordinates to
+        orientation = exactOrient2d from to query
+     in (ay <= py && py < by && orientation == GT)
+          || (by <= py && py < ay && orientation == LT)
+
+componentPointLocation :: PolygonComponent -> ExactPoint -> RegionPointLocation
+componentPointLocation component query =
+  case pointLocationInLoop (polygonOuterLoop component) query of
+    RegionExterior -> RegionExterior
+    RegionOnBoundary -> RegionOnBoundary
+    RegionInterior -> foldr classifyHole RegionInterior (polygonHoleLoops component)
+ where
+  classifyHole hole remaining =
+    case pointLocationInLoop hole query of
+      RegionExterior -> remaining
+      RegionOnBoundary -> RegionOnBoundary
+      RegionInterior -> RegionExterior
+
+crossLoopRelations
+  :: PreparedLoop
+  -> PreparedLoop
+  -> Either RegionValidationError [LoopRelationWitness]
+crossLoopRelations (PreparedLoop _ leftBounds leftSegments) (PreparedLoop _ rightBounds rightSegments)
+  | not (boundsOverlap leftBounds rightBounds) = Right []
+  | otherwise = do
+      let leftCount = V.length leftSegments
+      plan <-
+        first RegionSegmentEventsInvalid
+          (exactSegmentEventPlan (leftSegments <> rightSegments))
+      pure
+        [ LoopRelationWitness
+            leftIndex
+            (rightIndex - leftCount)
+            (leftSegments V.! leftIndex)
+            (rightSegments V.! (rightIndex - leftCount))
+            relation
+        | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
+            Map.toAscList (exactSegmentRelationMap plan)
+        , leftIndex < leftCount
+        , rightIndex >= leftCount
+        ]
+
+loopContainsInteriorPoint :: PreparedLoop -> PreparedLoop -> Bool
+loopContainsInteriorPoint container candidate =
+  preparedPointLocation container (firstPreparedPoint candidate) == RegionInterior
+
+componentInteriorsOverlap
+  :: PolygonComponent
+  -> PolygonComponent
+  -> Either RegionValidationError Bool
+componentInteriorsOverlap left right =
+  boundariesProperlyCross left right
+    >>= \crossing ->
+      pure
+        ( crossing
+            || componentContainsInteriorPoint left right
+            || componentContainsInteriorPoint right left
+        )
+
+boundariesProperlyCross
+  :: PolygonComponent
+  -> PolygonComponent
+  -> Either RegionValidationError Bool
+boundariesProperlyCross left right =
+  anyEither
+    (uncurry loopPairInteriorsOverlap)
+    ( overlappingPairsBetween
+        exactLoopBounds
+        (componentLoops left)
+        (componentLoops right)
+    )
+ where
+  componentLoops component =
+    polygonOuterLoop component : polygonHoleLoops component
+
+loopPairInteriorsOverlap
+  :: ExactLoop
+  -> ExactLoop
+  -> Either RegionValidationError Bool
+loopPairInteriorsOverlap leftLoop rightLoop = do
+  left <- prepareLoop leftLoop
+  right <- prepareLoop rightLoop
+  relations <- crossLoopRelations left right
+  pure (any relationOverlapsInteriors relations)
+ where
+  relationOverlapsInteriors
+    (LoopRelationWitness _ _ leftSegment rightSegment relation) =
+    case relation of
+      SegmentsProperlyCross -> True
+      SegmentsDuplicate -> collinearInteriorsCoincide leftSegment rightSegment
+      SegmentsCollinearlyOverlap -> collinearInteriorsCoincide leftSegment rightSegment
+      _ -> False
+  collinearInteriorsCoincide leftSegment rightSegment =
+    canonicalDirection leftSegment == canonicalDirection rightSegment
+  canonicalDirection segment =
+    uncurry (<=) (exactSegmentEndpoints segment)
+
+componentContainsInteriorPoint :: PolygonComponent -> PolygonComponent -> Bool
+componentContainsInteriorPoint container candidate =
+  any
+    ((== RegionInterior) . componentPointLocation container)
+    (NonEmpty.toList (exactLoopPoints (polygonOuterLoop candidate)))
+
+firstPreparedPoint :: PreparedLoop -> ExactPoint
+firstPreparedPoint (PreparedLoop (ExactLoop (point :| _)) _ _) = point
+
+regionsInteriorsOverlap
+  :: PlanarRegion
+  -> PlanarRegion
+  -> Either RegionValidationError Bool
+regionsInteriorsOverlap (PlanarRegion left) (PlanarRegion right) =
+  anyEither
+    (uncurry componentInteriorsOverlap)
+    (overlappingPairsBetween componentBounds left right)
+
+combineLocation :: RegionPointLocation -> RegionPointLocation -> RegionPointLocation
+combineLocation RegionOnBoundary _ = RegionOnBoundary
+combineLocation RegionExterior accumulated = accumulated
+combineLocation RegionInterior RegionOnBoundary = RegionOnBoundary
+combineLocation RegionInterior _ = RegionInterior
+
+anyEither
+  :: (value -> Either obstruction Bool)
+  -> [value]
+  -> Either obstruction Bool
+anyEither predicate =
+  foldr
+    (\value remaining -> do
+       matches <- predicate value
+       if matches then Right True else remaining)
+    (Right False)
+
+regionBoundaryEdges :: PlanarRegion -> [(ExactPoint, ExactPoint)]
+regionBoundaryEdges region =
+  concatMap
+    (\component ->
+       concatMap
+         (cyclePairs . exactLoopPoints)
+         (polygonOuterLoop component : polygonHoleLoops component))
+    (planarRegionComponents region)
diff --git a/src-dcel/Moonlight/Planar/Simplex.hs b/src-dcel/Moonlight/Planar/Simplex.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Simplex.hs
@@ -0,0 +1,234 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Canonical labelled simplices and downward-closed planar complexes.
+-- Labels, rather than resident DCEL handles, own identity across independently
+-- constructed meshes.
+module Moonlight.Planar.Simplex
+  ( SimplexDimension (..)
+  , PlanarSimplex
+  , PlanarSimplexError (..)
+  , planarVertex
+  , planarEdge
+  , planarFace
+  , planarSimplexDimension
+  , planarSimplexVertices
+  , planarSimplexBoundary
+  , PlanarComplex
+  , PlanarComplexError (..)
+  , planarComplex
+  , planarComplexCells
+  , planarComplexBasisAt
+  , unionPlanarComplex
+  , intersectPlanarComplex
+  )
+where
+
+import Control.DeepSeq (NFData (..))
+import Data.List qualified as List
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Data.Vector (Vector)
+import Data.Vector qualified as Vector
+import GHC.Generics (Generic)
+
+-- | The closed dimension vocabulary for planar simplicial complexes.
+data SimplexDimension
+  = SimplexDimension0
+  | SimplexDimension1
+  | SimplexDimension2
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+  deriving anyclass (NFData)
+
+-- | One canonical simplex. Edge and face labels are strictly ascending.
+data PlanarSimplex label
+  = PlanarVertex !label
+  | PlanarEdge !label !label
+  | PlanarFace !label !label !label
+  deriving stock (Eq, Ord, Show)
+
+instance NFData label => NFData (PlanarSimplex label) where
+  rnf simplex = case simplex of
+    PlanarVertex vertex -> rnf vertex
+    PlanarEdge first second -> rnf first `seq` rnf second
+    PlanarFace first second third -> rnf first `seq` rnf second `seq` rnf third
+
+-- | A repeated vertex would lower the requested simplex's dimension.
+data PlanarSimplexError label
+  = PlanarSimplexRepeatedVertex !label
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+planarVertex :: label -> PlanarSimplex label
+planarVertex = PlanarVertex
+
+planarEdge
+  :: Ord label
+  => label
+  -> label
+  -> Either (PlanarSimplexError label) (PlanarSimplex label)
+planarEdge firstLabel secondLabel =
+  case compare firstLabel secondLabel of
+    LT -> Right (PlanarEdge firstLabel secondLabel)
+    EQ -> Left (PlanarSimplexRepeatedVertex firstLabel)
+    GT -> Right (PlanarEdge secondLabel firstLabel)
+
+planarFace
+  :: Ord label
+  => label
+  -> label
+  -> label
+  -> Either (PlanarSimplexError label) (PlanarSimplex label)
+planarFace firstLabel secondLabel thirdLabel =
+  let (firstLow, firstHigh) = ascendingPair firstLabel secondLabel
+      (secondLow, finalHigh) = ascendingPair firstHigh thirdLabel
+      (finalLow, finalMiddle) = ascendingPair firstLow secondLow
+   in if finalLow == finalMiddle
+        then Left (PlanarSimplexRepeatedVertex finalLow)
+        else
+          if finalMiddle == finalHigh
+            then Left (PlanarSimplexRepeatedVertex finalMiddle)
+            else Right (PlanarFace finalLow finalMiddle finalHigh)
+
+planarSimplexDimension :: PlanarSimplex label -> SimplexDimension
+planarSimplexDimension simplex =
+  case simplex of
+    PlanarVertex _ -> SimplexDimension0
+    PlanarEdge _ _ -> SimplexDimension1
+    PlanarFace _ _ _ -> SimplexDimension2
+
+planarSimplexVertices :: PlanarSimplex label -> NonEmpty label
+planarSimplexVertices simplex =
+  case simplex of
+    PlanarVertex label -> label :| []
+    PlanarEdge firstLabel secondLabel -> firstLabel :| [secondLabel]
+    PlanarFace firstLabel secondLabel thirdLabel ->
+      firstLabel :| [secondLabel, thirdLabel]
+
+-- | The canonical oriented simplicial boundary.
+planarSimplexBoundary
+  :: Num coefficient
+  => PlanarSimplex label
+  -> [(coefficient, PlanarSimplex label)]
+planarSimplexBoundary simplex =
+  case simplex of
+    PlanarVertex _ -> []
+    PlanarEdge firstLabel secondLabel ->
+      [ (-1, PlanarVertex firstLabel)
+      , (1, PlanarVertex secondLabel)
+      ]
+    PlanarFace firstLabel secondLabel thirdLabel ->
+      [ (1, PlanarEdge secondLabel thirdLabel)
+      , (-1, PlanarEdge firstLabel thirdLabel)
+      , (1, PlanarEdge firstLabel secondLabel)
+      ]
+
+-- | A requested cell whose boundary is absent from the same complex.
+data PlanarComplexError label
+  = PlanarComplexBoundaryMissing
+      !(PlanarSimplex label)
+      !(PlanarSimplex label)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | An admitted downward-closed complex and its canonical dense bases.
+data PlanarComplex label = PlanarComplex
+  { storedPlanarComplexCells :: !(Set (PlanarSimplex label))
+  , storedPlanarVertexBasis :: !(Vector (PlanarSimplex label))
+  , storedPlanarEdgeBasis :: !(Vector (PlanarSimplex label))
+  , storedPlanarFaceBasis :: !(Vector (PlanarSimplex label))
+  }
+  deriving stock (Show)
+
+instance NFData label => NFData (PlanarComplex label) where
+  rnf (PlanarComplex cells vertices edges faces) =
+    rnf cells `seq` rnf vertices `seq` rnf edges `seq` rnf faces
+
+instance Eq label => Eq (PlanarComplex label) where
+  left == right =
+    storedPlanarComplexCells left == storedPlanarComplexCells right
+
+planarComplex
+  :: Ord label
+  => Set (PlanarSimplex label)
+  -> Either (PlanarComplexError label) (PlanarComplex label)
+planarComplex cells =
+  case List.find (\(_, boundary) -> Set.notMember boundary cells) boundaryPairs of
+    Just (simplex, boundary) ->
+      Left (PlanarComplexBoundaryMissing simplex boundary)
+    Nothing -> Right (admittedPlanarComplex cells)
+ where
+  boundaryPairs =
+    [ (simplex, boundary)
+    | simplex <- Set.toAscList cells
+    , (_, boundary) <- planarSimplexBoundary @Int simplex
+    ]
+
+planarComplexCells :: PlanarComplex label -> Set (PlanarSimplex label)
+planarComplexCells = storedPlanarComplexCells
+
+planarComplexBasisAt
+  :: SimplexDimension
+  -> PlanarComplex label
+  -> Vector (PlanarSimplex label)
+planarComplexBasisAt dimension complexValue =
+  case dimension of
+    SimplexDimension0 -> storedPlanarVertexBasis complexValue
+    SimplexDimension1 -> storedPlanarEdgeBasis complexValue
+    SimplexDimension2 -> storedPlanarFaceBasis complexValue
+
+-- | Union preserves downward closure, so no second validation is needed.
+unionPlanarComplex
+  :: Ord label
+  => PlanarComplex label
+  -> PlanarComplex label
+  -> PlanarComplex label
+unionPlanarComplex left right =
+  admittedPlanarComplex
+    (Set.union (planarComplexCells left) (planarComplexCells right))
+
+-- | Intersection also preserves downward closure.
+intersectPlanarComplex
+  :: Ord label
+  => PlanarComplex label
+  -> PlanarComplex label
+  -> PlanarComplex label
+intersectPlanarComplex left right =
+  admittedPlanarComplex
+    (Set.intersection (planarComplexCells left) (planarComplexCells right))
+
+admittedPlanarComplex :: Set (PlanarSimplex label) -> PlanarComplex label
+admittedPlanarComplex cells =
+  let (vertices, edges, faces) =
+        foldr partitionSimplex ([], [], []) (Set.toAscList cells)
+   in PlanarComplex
+        { storedPlanarComplexCells = cells
+        , storedPlanarVertexBasis = Vector.fromList vertices
+        , storedPlanarEdgeBasis = Vector.fromList edges
+        , storedPlanarFaceBasis = Vector.fromList faces
+        }
+
+partitionSimplex
+  :: PlanarSimplex label
+  -> ( [PlanarSimplex label]
+     , [PlanarSimplex label]
+     , [PlanarSimplex label]
+     )
+  -> ( [PlanarSimplex label]
+     , [PlanarSimplex label]
+     , [PlanarSimplex label]
+     )
+partitionSimplex simplex (vertices, edges, faces) =
+  case simplex of
+    PlanarVertex _ -> (simplex : vertices, edges, faces)
+    PlanarEdge _ _ -> (vertices, simplex : edges, faces)
+    PlanarFace _ _ _ -> (vertices, edges, simplex : faces)
+
+ascendingPair :: Ord value => value -> value -> (value, value)
+ascendingPair firstValue secondValue =
+  if firstValue <= secondValue
+    then (firstValue, secondValue)
+    else (secondValue, firstValue)
diff --git a/src-dcel/Moonlight/Planar/Types.hs b/src-dcel/Moonlight/Planar/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Types.hs
@@ -0,0 +1,32 @@
+-- | The package vocabulary: sites and validated queries, the mode-indexed
+-- triangulation, what construction returns, and the closed failure types.
+module Moonlight.Planar.Types
+  ( SiteRelation (..)
+  , ElementDefaults (..)
+  , unitElementDefaults
+  , ConstraintMode (..)
+  , KnownConstraintMode (..)
+  , Triangulation
+  , DelaunayTriangulation
+  , ConstrainedDelaunayTriangulation
+  , BuildResult (..)
+  , InsertionDisposition (..)
+  , InsertionResult (..)
+  , BuildError (..)
+  , Location (..)
+  , LocationHint (..)
+  , LocationStats (..)
+  , emptyLocationStats
+  , NearestStats (..)
+  , RefinementParameters
+  , defaultRefinementParameters
+  , ClosureStats (..)
+  , RefinementReceipt (..)
+  , RefinementDomainResult (..)
+  , RefinementResult (..)
+  , InvariantViolation (..)
+  , PlanarIncidenceError (..)
+  ) where
+
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
diff --git a/src-dcel/Moonlight/Planar/Validation.hs b/src-dcel/Moonlight/Planar/Validation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Validation.hs
@@ -0,0 +1,11 @@
+-- | Whole-value DCEL validation and face-quality observations.
+module Moonlight.Planar.Validation
+  ( validateTopology
+  , validateDelaunay
+  , validateTriangulation
+  , triangulationIsValid
+  , faceArea
+  , faceMinimumAngleDegrees
+  ) where
+
+import Moonlight.Planar.Internal.Validation
diff --git a/src-dcel/Moonlight/Planar/Valuation.hs b/src-dcel/Moonlight/Planar/Valuation.hs
new file mode 100644
--- /dev/null
+++ b/src-dcel/Moonlight/Planar/Valuation.hs
@@ -0,0 +1,903 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GADTs #-}
+
+-- | Intrinsic valuations of exact closed cell selections and admitted planar
+-- regions. Euler characteristic and area remain exact; Euclidean length is an
+-- exact radical expression accompanied by outward-rounded binary64 bounds.
+module Moonlight.Planar.Valuation
+  ( EulerCharacteristic
+  , eulerCharacteristicValue
+  , ExactArea
+  , exactAreaValue
+  , ExactPlanarMoments
+  , exactPlanarMeasure
+  , exactPlanarFirstX
+  , exactPlanarFirstY
+  , exactPlanarSecondXX
+  , exactPlanarSecondXY
+  , exactPlanarSecondYY
+  , scaleExactPlanarMoments
+  , orientedBoundaryMoments
+  , orientedBoundaryArea
+  , polygonComponentMoments
+  , polygonComponentArea
+  , ExactLengthTerm
+  , lengthCoefficient
+  , lengthRadicand
+  , ExactLengthExpression
+  , exactLengthTerms
+  , CertifiedInterval (..)
+  , ExactLengthMeasurement
+  , exactLengthExpression
+  , exactLengthBounds
+  , PlanarValuations
+  , valuationEuler
+  , valuationArea
+  , valuationIntrinsic1
+  , ValuationError (..)
+  , cellValuations
+  , regionValuations
+  , planarValuationsPerimeter
+  , cellSetPerimeter
+  , regionPerimeter
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.Bits (shiftL)
+import Data.Foldable (foldlM)
+import qualified Data.Foldable as Foldable
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import qualified Data.Map.Strict as Map
+import Data.Maybe (catMaybes, fromMaybe)
+import qualified Data.Ratio as Ratio
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word64)
+import GHC.Float (castDoubleToWord64, castWord64ToDouble)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactBounds
+  , ExactGeometryError
+  , ExactPoint
+  , ExactSegment
+  , exactOnClosedSegment
+  , exactPointCross
+  , exactPointCoordinates
+  , exactSegment
+  , exactSegmentEndpoints
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  , directedPair
+  , faceIdIndex
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.CellSet
+  ( ExactCellSet (..)
+  , exactCellSetIsFaceClosure
+  )
+import Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , faceBoundaryComponents
+  , faceEulerContribution
+  , incidenceIncidentFace
+  , incidenceOrigin
+  , incidenceUndirectedEndpoints
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( consecutivePairs
+  , cyclePairs
+  , orderedPair
+  )
+import Moonlight.Planar.Internal.Dyadic (integerBitLength)
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRationalDenominator
+  , exactRationalFromDyadic
+  , exactRationalFromFiniteDouble
+  , exactRationalFromNormalizedRatio
+  , exactRationalIsZero
+  , exactRationalNumerator
+  , exactSignum
+  )
+import Moonlight.Planar.Internal.ExactSegmentEvents
+  ( ExactSegmentEvent (..)
+  , ExactSegmentEventObstruction
+  , ExactSegmentEventPlan
+  , ExactSweepSegmentId (..)
+  , exactSegmentEventPlan
+  , exactSegmentEvents
+  , exactSegmentSplitPoints
+  )
+import Moonlight.Planar.Internal.Region.Types
+  ( ExactLoop (..)
+  , PlanarRegion (..)
+  , PolygonComponent (..)
+  , polygonOuterLoop
+  , polygonHoleLoops
+  )
+import Moonlight.Planar.Internal.Region.Bounds
+  ( componentBounds
+  , overlappingPairs
+  )
+
+newtype EulerCharacteristic = EulerCharacteristic Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+eulerCharacteristicValue :: EulerCharacteristic -> Int
+eulerCharacteristicValue (EulerCharacteristic value) = value
+
+newtype ExactArea = ExactArea ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+instance Semigroup ExactArea where
+  ExactArea left <> ExactArea right = ExactArea (left + right)
+
+instance Monoid ExactArea where
+  mempty = ExactArea 0
+
+exactAreaValue :: ExactArea -> ExactRational
+exactAreaValue (ExactArea value) = value
+
+-- | Exact moments of a bounded planar measure through total degree two.
+data ExactPlanarMoments = ExactPlanarMoments
+  { exactPlanarMeasure :: !ExactRational
+    -- ^ Total signed measure.
+  , exactPlanarFirstX :: !ExactRational
+    -- ^ Raw first moment integral of @x@.
+  , exactPlanarFirstY :: !ExactRational
+    -- ^ Raw first moment integral of @y@.
+  , exactPlanarSecondXX :: !ExactRational
+    -- ^ Raw second moment integral of @x^2@.
+  , exactPlanarSecondXY :: !ExactRational
+    -- ^ Raw mixed moment integral of @x*y@.
+  , exactPlanarSecondYY :: !ExactRational
+    -- ^ Raw second moment integral of @y^2@.
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+instance Semigroup ExactPlanarMoments where
+  left <> right =
+    ExactPlanarMoments
+      { exactPlanarMeasure = exactPlanarMeasure left + exactPlanarMeasure right
+      , exactPlanarFirstX = exactPlanarFirstX left + exactPlanarFirstX right
+      , exactPlanarFirstY = exactPlanarFirstY left + exactPlanarFirstY right
+      , exactPlanarSecondXX = exactPlanarSecondXX left + exactPlanarSecondXX right
+      , exactPlanarSecondXY = exactPlanarSecondXY left + exactPlanarSecondXY right
+      , exactPlanarSecondYY = exactPlanarSecondYY left + exactPlanarSecondYY right
+      }
+
+instance Monoid ExactPlanarMoments where
+  mempty = ExactPlanarMoments 0 0 0 0 0 0
+
+-- | Scale every moment by one exact coefficient.
+scaleExactPlanarMoments
+  :: ExactRational
+  -> ExactPlanarMoments
+  -> ExactPlanarMoments
+scaleExactPlanarMoments scalar moments =
+  ExactPlanarMoments
+    { exactPlanarMeasure = scalar * exactPlanarMeasure moments
+    , exactPlanarFirstX = scalar * exactPlanarFirstX moments
+    , exactPlanarFirstY = scalar * exactPlanarFirstY moments
+    , exactPlanarSecondXX = scalar * exactPlanarSecondXX moments
+    , exactPlanarSecondXY = scalar * exactPlanarSecondXY moments
+    , exactPlanarSecondYY = scalar * exactPlanarSecondYY moments
+    }
+
+-- | Exact area and raw moments of one admitted component. The six unscaled
+-- boundary sums are accumulated together and normalized once.
+polygonComponentMoments :: PolygonComponent -> ExactPlanarMoments
+polygonComponentMoments component =
+  normalizeMomentSums
+    ( foldMap
+        loopMomentSums
+        (polygonOuterLoop component : polygonHoleLoops component)
+    )
+
+-- | Integrate an oriented exact boundary directly, with the represented cell
+-- on the left of each edge. Outer cycles contribute positively and holes
+-- negatively; no polygon publication or repeated geometric admission occurs.
+-- The same raw boundary algebra serves admitted polygon components.
+orientedBoundaryMoments
+  :: Foldable boundary
+  => boundary (ExactPoint, ExactPoint)
+  -> ExactPlanarMoments
+orientedBoundaryMoments = normalizeMomentSums . Foldable.foldl' accumulateMomentEdge mempty
+
+normalizeMomentSums :: RawPlanarMomentSums -> ExactPlanarMoments
+normalizeMomentSums raw =
+  ExactPlanarMoments
+    { exactPlanarMeasure = oneHalf * rawDoubleArea raw
+    , exactPlanarFirstX = oneSixth * rawFirstX raw
+    , exactPlanarFirstY = oneSixth * rawFirstY raw
+    , exactPlanarSecondXX = oneTwelfth * rawSecondXX raw
+    , exactPlanarSecondXY = oneTwentyFourth * rawSecondXY raw
+    , exactPlanarSecondYY = oneTwelfth * rawSecondYY raw
+    }
+
+-- | Exact unsigned area of one already-admitted polygon component.  Winding
+-- and hole containment were discharged by 'polygonComponent', so this
+-- observation performs no second geometric validation.
+polygonComponentArea :: PolygonComponent -> ExactArea
+polygonComponentArea = ExactArea . (oneHalf *) . componentDoubleArea
+
+-- | Exact signed area of an oriented boundary. An admitted bounded cell's
+-- left-oriented outer and hole cycles give its nonnegative area. This fold
+-- computes only area; it does not charge an area-only observation for moments.
+orientedBoundaryArea
+  :: Foldable boundary
+  => boundary (ExactPoint, ExactPoint)
+  -> ExactArea
+orientedBoundaryArea = ExactArea . (oneHalf *) . orientedBoundaryDoubleArea
+
+data RawPlanarMomentSums = RawPlanarMomentSums
+  { rawDoubleArea :: !ExactRational
+  , rawFirstX :: !ExactRational
+  , rawFirstY :: !ExactRational
+  , rawSecondXX :: !ExactRational
+  , rawSecondXY :: !ExactRational
+  , rawSecondYY :: !ExactRational
+  }
+
+instance Semigroup RawPlanarMomentSums where
+  left <> right =
+    RawPlanarMomentSums
+      { rawDoubleArea = rawDoubleArea left + rawDoubleArea right
+      , rawFirstX = rawFirstX left + rawFirstX right
+      , rawFirstY = rawFirstY left + rawFirstY right
+      , rawSecondXX = rawSecondXX left + rawSecondXX right
+      , rawSecondXY = rawSecondXY left + rawSecondXY right
+      , rawSecondYY = rawSecondYY left + rawSecondYY right
+      }
+
+instance Monoid RawPlanarMomentSums where
+  mempty = RawPlanarMomentSums 0 0 0 0 0 0
+
+loopMomentSums :: ExactLoop -> RawPlanarMomentSums
+loopMomentSums (ExactLoop points) =
+  List.foldl' accumulateMomentEdge mempty (cyclePairs points)
+
+accumulateMomentEdge
+  :: RawPlanarMomentSums
+  -> (ExactPoint, ExactPoint)
+  -> RawPlanarMomentSums
+accumulateMomentEdge accumulated (from, to) =
+  let (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+      cross = exactPointCross from to
+   in RawPlanarMomentSums
+        { rawDoubleArea = rawDoubleArea accumulated + cross
+        , rawFirstX = rawFirstX accumulated + (fromX + toX) * cross
+        , rawFirstY = rawFirstY accumulated + (fromY + toY) * cross
+        , rawSecondXX =
+            rawSecondXX accumulated
+              + (fromX * fromX + fromX * toX + toX * toX) * cross
+        , rawSecondXY =
+            rawSecondXY accumulated
+              + (2 * fromX * fromY + fromX * toY + toX * fromY + 2 * toX * toY) * cross
+        , rawSecondYY =
+            rawSecondYY accumulated
+              + (fromY * fromY + fromY * toY + toY * toY) * cross
+        }
+
+data ExactLengthTerm = ExactLengthTerm
+  { lengthCoefficient :: !ExactRational
+  , lengthRadicand :: !Integer
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A normalized sum of rational coefficients times square roots of integer
+-- radicands. No two radicands differ by a rational square, so equal lengths
+-- are one term and rational length is the radicand-1 term; each radicand is
+-- reduced by the square factors of the primes below 64 and by a perfect-square
+-- cofactor. A squared prime above that bound inside a non-square cofactor is
+-- beyond factoring-free reach, so the presentation is canonical within an
+-- expression and across expressions only up to such factors; there is no
+-- 'Eq' instance for that reason.
+newtype ExactLengthExpression = ExactLengthExpression [ExactLengthTerm]
+  deriving stock (Show, Generic)
+  deriving anyclass (NFData)
+
+exactLengthTerms :: ExactLengthExpression -> [ExactLengthTerm]
+exactLengthTerms (ExactLengthExpression terms) = terms
+
+data CertifiedInterval = CertifiedInterval
+  { intervalLower :: !Double
+  , intervalUpper :: !Double
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data ExactLengthMeasurement = ExactLengthMeasurement
+  { exactLengthExpression :: !ExactLengthExpression
+  , exactLengthBounds :: !CertifiedInterval
+  }
+  deriving stock (Show, Generic)
+  deriving anyclass (NFData)
+
+data PlanarValuations = PlanarValuations
+  { valuationEuler :: !EulerCharacteristic
+  , valuationArea :: !ExactArea
+  , valuationIntrinsic1 :: !ExactLengthMeasurement
+  }
+  deriving stock (Show, Generic)
+  deriving anyclass (NFData)
+
+data ValuationError
+  = ValuationCoordinateMissing !VertexId
+  | ValuationInvalidRegionSegment !ExactGeometryError
+  | ValuationSegmentEventsInvalid !ExactSegmentEventObstruction
+  | ValuationBoundaryMultiplicity !ExactPoint !ExactPoint !Int
+  | ValuationNegativeSquaredLength !ExactRational
+  | ValuationCellSetNotPureRegion
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+cellValuations :: ExactCellSet -> Either ValuationError PlanarValuations
+cellValuations (ExactCellSet incidence points selectedEdges selectedFaces) = do
+  let selectedFaceIds = fmap (FaceId . fromIntegral) (IntSet.toAscList selectedFaces)
+  faceDoubleAreas <-
+    traverse
+      (cellFaceDoubleArea incidence points)
+      selectedFaceIds
+  edgeContributions <-
+    traverse
+      ( cellEdgeLengthContribution incidence points selectedFaces
+          . UndirectedEdgeId
+          . fromIntegral
+      )
+      (IntSet.toAscList selectedEdges)
+  assembleValuations
+    (IntMap.size points - IntSet.size selectedEdges + sum (fmap (faceEulerContribution incidence) selectedFaceIds))
+    (List.foldl' (+) 0 faceDoubleAreas)
+    (normalizeLengthContributions id edgeContributions)
+
+regionValuations :: PlanarRegion -> Either ValuationError PlanarValuations
+regionValuations (PlanarRegion components) = do
+  componentBoundaries <- traverse componentBoundaryData components
+  let segments = V.concat (map componentBoundarySegments componentBoundaries)
+      componentEuler =
+        List.foldl'
+          (\total boundary -> total + componentBoundaryEuler boundary)
+          0
+          componentBoundaries
+  contactPlan <-
+    if null (overlappingPairs componentBoundaryBounds componentBoundaries)
+      then Right Nothing
+      else Just <$> first ValuationSegmentEventsInvalid (exactSegmentEventPlan segments)
+  boundaryAtoms <- normalizedRegionBoundaryAtoms segments contactPlan
+  let euler =
+        componentEuler
+          - maybe 0 (boundaryContactEuler componentBoundaries) contactPlan
+      doubleArea =
+        List.foldl'
+          (\area component -> area + componentDoubleArea component)
+          0
+          components
+  assembleValuations
+    euler
+    doubleArea
+    ( normalizeLengthContributions
+        (\(from, to) -> (oneHalf, segmentSquaredLength from to))
+        boundaryAtoms
+    )
+
+assembleValuations
+  :: Int
+  -> ExactRational
+  -> ExactLengthExpression
+  -> Either ValuationError PlanarValuations
+assembleValuations euler doubleArea lengthExpression =
+  PlanarValuations (EulerCharacteristic euler) (ExactArea (oneHalf * doubleArea))
+    <$> measureLength lengthExpression
+cellSetPerimeter
+  :: ExactCellSet
+  -> Either ValuationError ExactLengthMeasurement
+cellSetPerimeter cellSet
+  | exactCellSetIsFaceClosure cellSet =
+      cellValuations cellSet >>= planarValuationsPerimeter
+  | otherwise = Left ValuationCellSetNotPureRegion
+
+regionPerimeter
+  :: PlanarRegion
+  -> Either ValuationError ExactLengthMeasurement
+regionPerimeter region = regionValuations region >>= planarValuationsPerimeter
+
+-- | Derive conventional boundary length from an already-computed intrinsic
+-- valuation without traversing the source geometry again.
+planarValuationsPerimeter
+  :: PlanarValuations
+  -> Either ValuationError ExactLengthMeasurement
+planarValuationsPerimeter valuations =
+  measureLength
+    (scaleLengthExpression 2 (exactLengthExpression (valuationIntrinsic1 valuations)))
+
+cellFaceDoubleArea
+  :: PlanarIncidence
+  -> IntMap.IntMap ExactPoint
+  -> FaceId
+  -> Either ValuationError ExactRational
+cellFaceDoubleArea incidence points face =
+  sum <$> traverse boundaryDoubleArea (faceBoundaryComponents incidence face)
+ where
+  boundaryDoubleArea edges = do
+    coordinates <- traverse (cellPoint points . incidenceOrigin incidence) edges
+    pure (maybe 0 (orientedBoundaryDoubleArea . cyclePairs) (NonEmpty.nonEmpty coordinates))
+
+cellEdgeLengthContribution
+  :: PlanarIncidence
+  -> IntMap.IntMap ExactPoint
+  -> IntSet.IntSet
+  -> UndirectedEdgeId
+  -> Either ValuationError (ExactRational, ExactRational)
+cellEdgeLengthContribution incidence points selectedFaces edge = do
+  let (fromVertex, toVertex) = incidenceUndirectedEndpoints incidence edge
+      (forward, backward) = directedPair edge
+      selected face = IntSet.member (faceIdIndex face) selectedFaces
+      coefficient = case (selected (incidenceIncidentFace incidence forward), selected (incidenceIncidentFace incidence backward)) of
+        (False, False) -> 1
+        (True, True) -> 0
+        _ -> oneHalf
+  from <- cellPoint points fromVertex
+  to <- cellPoint points toVertex
+  pure (coefficient, segmentSquaredLength from to)
+
+cellPoint
+  :: IntMap.IntMap ExactPoint
+  -> VertexId
+  -> Either ValuationError ExactPoint
+cellPoint points vertex =
+  maybe
+    (Left (ValuationCoordinateMissing vertex))
+    Right
+    (IntMap.lookup (vertexIdIndex vertex) points)
+
+componentDoubleArea :: PolygonComponent -> ExactRational
+componentDoubleArea component =
+  List.foldl'
+    (\area loop -> area + loopDoubleArea loop)
+    0
+    (polygonOuterLoop component : polygonHoleLoops component)
+
+loopDoubleArea :: ExactLoop -> ExactRational
+loopDoubleArea (ExactLoop points) =
+  orientedBoundaryDoubleArea (cyclePairs points)
+
+orientedBoundaryDoubleArea
+  :: Foldable boundary
+  => boundary (ExactPoint, ExactPoint)
+  -> ExactRational
+orientedBoundaryDoubleArea =
+  Foldable.foldl' (\area (from, to) -> area + exactPointCross from to) 0
+
+segmentSquaredLength :: ExactPoint -> ExactPoint -> ExactRational
+segmentSquaredLength from to =
+  let (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+      deltaX = toX - fromX
+      deltaY = toY - fromY
+   in deltaX * deltaX + deltaY * deltaY
+
+normalizeLengthContributions
+  :: Foldable collection
+  => (value -> (ExactRational, ExactRational))
+  -> collection value
+  -> ExactLengthExpression
+normalizeLengthContributions contribution contributions =
+  ExactLengthExpression
+    [ ExactLengthTerm coefficient radicand
+    | (radicand, coefficient) <- List.sortOn fst (concat (Map.elems classes))
+    , not (exactRationalIsZero coefficient)
+    ]
+ where
+  coefficientsBySquare =
+    List.foldl' accumulateContribution Map.empty contributions
+  accumulateContribution coefficients value =
+    case contribution value of
+      (coefficient, square)
+        | exactRationalIsZero coefficient -> coefficients
+        | otherwise -> Map.insertWith (+) square coefficient coefficients
+  classes =
+    Map.foldlWithKey' accumulateRadical Map.empty coefficientsBySquare
+  accumulateRadical buckets square coefficient =
+    let reduced = reduceRadicand square
+     in if radicalRadicand reduced == 0
+          then buckets
+          else
+            Map.alter
+              ( Just
+                  . mergeRadical (coefficient * radicalScale reduced) (radicalRadicand reduced)
+                  . fromMaybe []
+              )
+              (radicalClassKey reduced)
+              buckets
+
+data ReducedRadical = ReducedRadical
+  { radicalScale :: !ExactRational
+  , radicalRadicand :: !Integer
+  , radicalClassKey :: !Word64
+  }
+
+-- | Write √(n/d) as (1/d)·√(n·d), fold the even part of each trial prime's
+-- multiplicity and a perfect-square cofactor into the scale, and key the
+-- radicand by what those primes observe of its square class: the parity of
+-- the multiplicity and the quadratic character of the prime-free part, both
+-- invariant under multiplication by rational squares.
+reduceRadicand :: ExactRational -> ReducedRadical
+reduceRadicand square
+  | radicand <= 0 = ReducedRadical inverseDenominator radicand 0
+  | otherwise =
+      let (cofactor, reduced) = List.foldl' stripPrime (radicand, initial) radicalPrimes
+          root = integerSquareRoot cofactor
+       in if root * root == cofactor
+            then reduced {radicalScale = radicalScale reduced * fromInteger root}
+            else reduced {radicalRadicand = radicalRadicand reduced * cofactor}
+ where
+  denominator = exactRationalDenominator square
+  radicand = exactRationalNumerator square * denominator
+  inverseDenominator = exactRationalFromNormalizedRatio (1 Ratio.% denominator)
+  initial = ReducedRadical inverseDenominator 1 0
+  stripPrime (remaining, reduced) (prime, residues) =
+    let (multiplicity, rest) = primeMultiplicity prime remaining
+        (halfPower, parity) = multiplicity `divMod` 2
+        character = squareClassCharacter prime residues rest
+     in ( rest
+        , ReducedRadical
+            { radicalScale = radicalScale reduced * fromInteger (prime ^ halfPower)
+            , radicalRadicand = radicalRadicand reduced * prime ^ parity
+            , radicalClassKey = radicalClassKey reduced * 8 + fromIntegral (parity * 4 + character)
+            }
+        )
+
+radicalPrimes :: [(Integer, IntSet.IntSet)]
+radicalPrimes =
+  [ (toInteger prime, IntSet.fromList [(x * x) `mod` prime | x <- [1 .. prime - 1]])
+  | prime <- [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61 :: Int]
+  ]
+
+primeMultiplicity :: Integer -> Integer -> (Int, Integer)
+primeMultiplicity prime = go 0
+ where
+  go !count value =
+    case value `quotRem` prime of
+      (quotient, 0) -> go (count + 1) quotient
+      _ -> (count, value)
+
+squareClassCharacter :: Integer -> IntSet.IntSet -> Integer -> Int
+squareClassCharacter 2 _ rest = fromInteger ((rest `mod` 8) `div` 2)
+squareClassCharacter prime residues rest =
+  if IntSet.member (fromInteger (rest `mod` prime)) residues then 1 else 0
+
+-- | Fold a term into its square class: the product of two radicands of one
+-- class is a perfect square, whose root exposes the rational square relating
+-- them, so both fold onto their common divisor with integer scales.
+mergeRadical
+  :: ExactRational
+  -> Integer
+  -> [(Integer, ExactRational)]
+  -> [(Integer, ExactRational)]
+mergeRadical coefficient radicand = go
+ where
+  go [] = [(radicand, coefficient)]
+  go ((kernel, total) : rest)
+    | pairRoot * pairRoot == pairProduct =
+        let common = gcd pairRoot kernel
+            newScale = pairRoot `div` common
+            oldScale = kernel `div` common
+         in ( kernel `div` (oldScale * oldScale)
+            , total * fromInteger oldScale + coefficient * fromInteger newScale
+            )
+              : rest
+    | otherwise = (kernel, total) : go rest
+   where
+    pairProduct = radicand * kernel
+    pairRoot = integerSquareRoot pairProduct
+
+scaleLengthExpression
+  :: Integer
+  -> ExactLengthExpression
+  -> ExactLengthExpression
+scaleLengthExpression scalar (ExactLengthExpression terms) =
+  let exactScalar = fromInteger scalar
+   in ExactLengthExpression
+        [ term
+            { lengthCoefficient =
+                exactScalar * lengthCoefficient term
+            }
+        | term <- terms
+        ]
+
+measureLength
+  :: ExactLengthExpression
+  -> Either ValuationError ExactLengthMeasurement
+measureLength expression@(ExactLengthExpression terms) = do
+  (lower, upper) <-
+    foldlM
+      addTermBounds
+      (0, 0)
+      terms
+  pure
+    ExactLengthMeasurement
+      { exactLengthExpression = expression
+      , exactLengthBounds =
+          CertifiedInterval
+            { intervalLower = directedLowerDouble lower
+            , intervalUpper = directedUpperDouble upper
+            }
+      }
+ where
+  addTermBounds (lowerTotal, upperTotal) term = do
+    (lowerRoot, upperRoot) <- exactSquareRootBounds (fromInteger (lengthRadicand term))
+    let coefficient = lengthCoefficient term
+    pure
+      ( lowerTotal + coefficient * lowerRoot
+      , upperTotal + coefficient * upperRoot
+      )
+
+exactSquareRootBounds
+  :: ExactRational
+  -> Either ValuationError (ExactRational, ExactRational)
+exactSquareRootBounds value =
+  case exactSignum value of
+    LT -> Left (ValuationNegativeSquaredLength value)
+    _ ->
+      let numerator = exactRationalNumerator value
+          denominator = exactRationalDenominator value
+          scale = 1 `shiftL` radicalPrecisionBits
+          scaledNumerator = numerator * scale * scale
+          root = integerSquareRoot (scaledNumerator `div` denominator)
+          exact = root * root * denominator == scaledNumerator
+          dyadicPower = negate radicalPrecisionBits
+       in Right
+            ( exactRationalFromDyadic root dyadicPower
+            , exactRationalFromDyadic (if exact then root else root + 1) dyadicPower
+            )
+
+radicalPrecisionBits :: Int
+radicalPrecisionBits = 128
+
+integerSquareRoot :: Integer -> Integer
+integerSquareRoot value
+  | value < 2 = value
+  | otherwise = descend initial
+ where
+  initial = 1 `shiftL` ((integerBitLength value + 1) `div` 2)
+  descend estimate =
+    let refined = (estimate + value `div` estimate) `div` 2
+     in if refined >= estimate then estimate else descend refined
+
+directedLowerDouble :: ExactRational -> Double
+directedLowerDouble value =
+  let candidate = rationalToDouble value
+   in if isInfinite candidate
+        then maximumFiniteDouble
+        else
+          if exactRationalFromFiniteDouble candidate <= value
+            then candidate
+            else previousPositiveDouble candidate
+
+directedUpperDouble :: ExactRational -> Double
+directedUpperDouble value =
+  let candidate = rationalToDouble value
+   in if isInfinite candidate
+        || exactRationalFromFiniteDouble candidate >= value
+        then candidate
+        else nextPositiveDouble candidate
+
+rationalToDouble :: ExactRational -> Double
+rationalToDouble value =
+  fromRational
+    ( exactRationalNumerator value
+        Ratio.% exactRationalDenominator value
+    )
+
+previousPositiveDouble :: Double -> Double
+previousPositiveDouble value
+  | value <= 0 = 0
+  | otherwise = castWord64ToDouble (castDoubleToWord64 value - 1)
+
+nextPositiveDouble :: Double -> Double
+nextPositiveDouble value
+  | value == 0 = castWord64ToDouble 1
+  | otherwise = castWord64ToDouble (castDoubleToWord64 value + 1)
+
+maximumFiniteDouble :: Double
+maximumFiniteDouble = castWord64ToDouble 0x7fefffffffffffff
+
+data ComponentBoundaryData = ComponentBoundaryData
+  { componentBoundaryEuler :: !Int
+  , componentBoundaryBounds :: !ExactBounds
+  , componentBoundarySegments :: !(V.Vector ExactSegment)
+  }
+
+componentBoundaryData
+  :: PolygonComponent
+  -> Either ValuationError ComponentBoundaryData
+componentBoundaryData component = do
+  segments <-
+    V.fromList
+      <$> traverse
+        admittedSegment
+        ( concatMap
+            (cyclePairs . loopPoints)
+            (polygonOuterLoop component : polygonHoleLoops component)
+        )
+  pure
+    ComponentBoundaryData
+      { componentBoundaryEuler = 1 - length (polygonHoleLoops component)
+      , componentBoundaryBounds = componentBounds component
+      , componentBoundarySegments = segments
+      }
+
+-- | The Euler characteristic of the contact each component's boundary makes
+-- with the boundaries of the components before it, summed over components.
+--
+-- Every contact is read from one plan over the whole boundary family. An
+-- event between two components is charged to the later one, so each
+-- component's contact graph is exactly what a sweep over that component and
+-- its predecessors would have reported; the vertex and edge sets stay
+-- per-component because the sum, not a global graph, is the quantity.
+boundaryContactEuler
+  :: [ComponentBoundaryData]
+  -> ExactSegmentEventPlan
+  -> Int
+boundaryContactEuler boundaries plan =
+  IntMap.foldl'
+    (\total contacts -> total + contactGraphEuler plan contacts)
+    0
+    contactsByComponent
+ where
+  componentOf =
+    U.concat
+      ( zipWith
+          (\component boundary ->
+             U.replicate (V.length (componentBoundarySegments boundary)) component)
+          [0 :: Int ..]
+          boundaries
+      )
+  contactsByComponent =
+    IntMap.fromListWith
+      (<>)
+      [ (max leftComponent rightComponent, [contactFromEvent event])
+      | event <- exactSegmentEvents plan
+      , let (ExactSweepSegmentId left, ExactSweepSegmentId right) = eventIds event
+            leftComponent = componentOf `U.unsafeIndex` left
+            rightComponent = componentOf `U.unsafeIndex` right
+      , leftComponent /= rightComponent
+      ]
+
+-- | Vertices minus edges of the graph the contacts form once every interval
+-- is subdivided at the split points of the segment carrying it.
+--
+-- A split point of any segment that lies on an interval is a contact of that
+-- segment with the interval's carrier and therefore already a split point of
+-- the carrier, so the carrier's own split points subdivide the interval
+-- exactly as the split points of the whole family would.
+contactGraphEuler :: ExactSegmentEventPlan -> [BoundaryContact] -> Int
+contactGraphEuler plan contacts = Set.size vertices - Set.size contactEdges
+ where
+  contactPoints =
+    Set.fromList
+      [ point
+      | ContactPoint point <- contacts
+      ]
+  contactEdges =
+    Set.fromList
+      [ orderedPair from to
+      | contact <- contacts
+      , (from, to) <- consecutivePairs (intervalPoints contact)
+      , from /= to
+      ]
+  intervalPoints contact =
+    case contact of
+      ContactPoint _ -> []
+      ContactSegment carrier -> exactSegmentSplitPoints plan carrier
+      ContactInterval carrier lower upper ->
+        filter (exactOnClosedSegment lower upper) (exactSegmentSplitPoints plan carrier)
+  vertices =
+    Set.union
+      contactPoints
+      ( Set.fromList
+          [ point
+          | (from, to) <- Set.toAscList contactEdges
+          , point <- [from, to]
+          ]
+      )
+
+data BoundaryContact
+  = ContactPoint !ExactPoint
+  | ContactSegment !ExactSweepSegmentId
+  | ContactInterval !ExactSweepSegmentId !ExactPoint !ExactPoint
+
+contactFromEvent :: ExactSegmentEvent -> BoundaryContact
+contactFromEvent (ExactProperCrossing _ _ point) = ContactPoint point
+contactFromEvent (ExactEndpointTouch _ _ point) = ContactPoint point
+contactFromEvent (ExactSharedEndpoint _ _ point) = ContactPoint point
+contactFromEvent (ExactDuplicateSegments leftId _) = ContactSegment leftId
+contactFromEvent (ExactCollinearOverlap leftId _ lower upper) =
+  uncurry (ContactInterval leftId) (orderedPair lower upper)
+
+eventIds
+  :: ExactSegmentEvent
+  -> (ExactSweepSegmentId, ExactSweepSegmentId)
+eventIds (ExactProperCrossing left right _) = (left, right)
+eventIds (ExactEndpointTouch left right _) = (left, right)
+eventIds (ExactSharedEndpoint left right _) = (left, right)
+eventIds (ExactDuplicateSegments left right) = (left, right)
+eventIds (ExactCollinearOverlap left right _ _) = (left, right)
+
+normalizedRegionBoundaryAtoms
+  :: V.Vector ExactSegment
+  -> Maybe ExactSegmentEventPlan
+  -> Either ValuationError (Set.Set (ExactPoint, ExactPoint))
+normalizedRegionBoundaryAtoms segments Nothing =
+  Right (Set.fromList (map canonicalSegmentEndpoints (V.toList segments)))
+normalizedRegionBoundaryAtoms segments (Just plan) =
+  traverseMultiplicity
+    (Map.toAscList (Map.fromListWith (+) orientedAtoms))
+ where
+  orientedAtoms =
+    concatMap
+      segmentAtoms
+      [ exactSegmentSplitPoints plan (ExactSweepSegmentId index)
+      | index <- [0 .. V.length segments - 1]
+      ]
+  segmentAtoms :: [ExactPoint] -> [((ExactPoint, ExactPoint), Int)]
+  segmentAtoms points =
+    [ ( orderedPair firstPoint secondPoint
+      , if firstPoint <= secondPoint then 1 else -1
+      )
+    | (firstPoint, secondPoint) <- consecutivePairs points
+    , firstPoint /= secondPoint
+    ]
+  traverseMultiplicity
+    :: [((ExactPoint, ExactPoint), Int)]
+    -> Either ValuationError (Set.Set (ExactPoint, ExactPoint))
+  traverseMultiplicity entries = do
+    retained <-
+      traverse
+        (\(edge@(from, to), multiplicity) ->
+           case abs multiplicity of
+             0 -> Right Nothing
+             1 -> Right (Just edge)
+             _ -> Left (ValuationBoundaryMultiplicity from to multiplicity))
+        entries
+    pure (Set.fromList (catMaybes retained))
+
+admittedSegment
+  :: (ExactPoint, ExactPoint)
+  -> Either ValuationError ExactSegment
+admittedSegment (from, to) = first ValuationInvalidRegionSegment (exactSegment from to)
+
+canonicalSegmentEndpoints :: ExactSegment -> (ExactPoint, ExactPoint)
+canonicalSegmentEndpoints = uncurry orderedPair . exactSegmentEndpoints
+
+loopPoints :: ExactLoop -> NonEmpty ExactPoint
+loopPoints (ExactLoop points) = points
+
+oneHalf :: ExactRational
+oneHalf = exactRationalFromDyadic 1 (-1)
+
+oneSixth :: ExactRational
+oneSixth = exactRationalFromNormalizedRatio (1 Ratio.% 6)
+
+oneTwelfth :: ExactRational
+oneTwelfth = exactRationalFromNormalizedRatio (1 Ratio.% 12)
+
+oneTwentyFourth :: ExactRational
+oneTwentyFourth = exactRationalFromNormalizedRatio (1 Ratio.% 24)
diff --git a/src-dcel/Moonlight/Triangulation/Alpha.hs b/src-dcel/Moonlight/Triangulation/Alpha.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Alpha.hs
+++ /dev/null
@@ -1,427 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact alpha births over one resident Delaunay triangulation. Geometry
--- remains owned by the DCEL and 'ExactCellSet'; this module supplies the
--- handle-indexed filtration section used by topology consumers.
-module Moonlight.Triangulation.Alpha
-  ( AlphaBirth
-  , alphaBirthNumerator
-  , alphaBirthDenominator
-  , alphaBirthToDouble
-  , alphaBirthFromRadiusSquared
-  , AlphaFiltration
-  , AlphaFiltrationError (..)
-  , alphaFiltration
-  , alphaFiltrationCellSet
-  , alphaVertexBirth
-  , alphaEdgeBirth
-  , alphaFaceBirth
-  , alphaFiltrationCriticalBirths
-  , alphaComplexAtBirth
-  , alphaComplexAtRadius
-  , alphaShapeContainsFace
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.Bits (shiftL)
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Set qualified as Set
-import Data.Vector (Vector)
-import Data.Vector qualified as Vector
-import Data.Word (Word64)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.CellSet
-  ( CellSelectionError
-  , exactCellSet
-  )
-import Moonlight.Triangulation.Dcel qualified as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  , directedPair
-  , faceIdIndex
-  , undirectedEdgeIdIndex
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Internal.CellSet
-  ( ExactCellSet (..)
-  , residentExactCellSet
-  )
-import Moonlight.Triangulation.Internal.Dyadic
-  ( exactCircumradiusSquaredRational
-  , exactQuarterSquaredDistanceRational
-  , integerBitLength
-  , integerRatioToDouble
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactArithmeticError (..)
-  , ExactRational
-  , exactRationalDenominator
-  , exactRationalFromFiniteDouble
-  , exactRationalNumerator
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( DelaunayTriangulation
-  , Triangulation
-  )
-import Moonlight.Triangulation.Internal.Predicates (inDiametralCircle)
-import Moonlight.Triangulation.Scalar (circumradiusSquaredWithinCoordinates)
-import Moonlight.Triangulation.Types
-  ( ConstraintMode (Unconstrained)
-  , Point (..)
-  , RadiusSquared
-  , radiusSquaredValue
-  )
-
--- | One exact squared-radius birth. The constructor is withheld so negative
--- exact rationals cannot masquerade as geometric radii.
-data AlphaBirth = AlphaBirth !AlphaOrderKey !ExactRational
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-data AlphaOrderKey
-  = NonPositiveAlphaOrder
-  | PositiveAlphaOrder !Int !Word64
-  deriving stock (Eq, Ord, Generic)
-  deriving anyclass (NFData)
-
-instance Eq AlphaBirth where
-  AlphaBirth _ leftValue == AlphaBirth _ rightValue = leftValue == rightValue
-
-instance Ord AlphaBirth where
-  compare (AlphaBirth leftKey leftValue) (AlphaBirth rightKey rightValue) =
-    case compare leftKey rightKey of
-      EQ -> compare leftValue rightValue
-      distinctKeyOrder -> distinctKeyOrder
-
-instance Show AlphaBirth where
-  showsPrec precedence (AlphaBirth _ value) =
-    showParen
-      (precedence > 10)
-      (showString "AlphaBirth " . showsPrec 11 value)
-
--- | Numerator of the reduced exact squared-radius birth.
-alphaBirthNumerator :: AlphaBirth -> Integer
-alphaBirthNumerator (AlphaBirth _ value) = exactRationalNumerator value
-
--- | Positive denominator of the reduced exact squared-radius birth.
-alphaBirthDenominator :: AlphaBirth -> Integer
-alphaBirthDenominator (AlphaBirth _ value) = exactRationalDenominator value
-
--- | Binary64 display projection. Equality and filtration order must use the
--- 'AlphaBirth' itself, not this rounded view.
-alphaBirthToDouble :: AlphaBirth -> Double
-alphaBirthToDouble birth =
-  integerRatioToDouble
-    (alphaBirthNumerator birth)
-    (alphaBirthDenominator birth)
-
--- | Embed an admitted binary64 threshold into the exact alpha order without
--- loss. Every finite binary64 value is a dyadic rational.
-alphaBirthFromRadiusSquared :: RadiusSquared -> AlphaBirth
-alphaBirthFromRadiusSquared =
-  alphaBirthFromExact . exactRationalFromFiniteDouble . radiusSquaredValue
-
--- | The exact handle-indexed filtration of one Delaunay DCEL. The cell set is
--- the only geometry inventory; the maps are derived birth sections over those
--- resident handles.
-data AlphaFiltration = AlphaFiltration
-  { alphaFiltrationCells :: !ExactCellSet
-  , alphaVertexBirths :: !(Vector AlphaBirth)
-  , alphaEdgeBirths :: !(Vector AlphaBirth)
-  , alphaFaceBirths :: !(Vector AlphaBirth)
-  }
-
-data FaceAlphaSection = FaceAlphaSection
-  { faceAlphaBirth :: !AlphaBirth
-  , faceAlphaVertices :: !(VertexId, VertexId, VertexId)
-  }
-
--- | Typed obstruction to deriving the exact birth section from a resident
--- Delaunay triangulation. Every incidence failure retains the affected handle.
-data AlphaFiltrationError
-  = AlphaFiltrationCellSelection !CellSelectionError
-  | AlphaFaceVerticesUnavailable !FaceId
-  | AlphaFaceDegenerate !FaceId
-  | AlphaCircumradiusDivisionFailed !FaceId !ExactArithmeticError
-  | AlphaFaceEdgeIncidenceInvalid !FaceId !UndirectedEdgeId ![VertexId]
-  | AlphaIncidentFaceBirthMissing !UndirectedEdgeId !FaceId
-  | AlphaEdgeBirthWitnessMissing !UndirectedEdgeId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Construct every exact alpha birth once. Faces descend first because a
--- non-Gabriel edge is born at the least incident-face circumradius.
-alphaFiltration
-  :: DelaunayTriangulation vertex
-  -> Either AlphaFiltrationError AlphaFiltration
-alphaFiltration triangulation = do
-  cellSet <-
-    first AlphaFiltrationCellSelection
-      (residentExactCellSet triangulation)
-  let vertexBirths = Vector.replicate (Dcel.numVertices triangulation) zeroAlphaBirth
-  faceSections <-
-    Vector.imapM
-      (\faceIndex -> exactFaceSection triangulation (FaceId (fromIntegral (faceIndex + 1))))
-      (Dcel.innerFaceVertexTriples triangulation)
-  edgeBirths <-
-    Vector.generateM
-      (Dcel.numUndirectedEdges triangulation)
-      (exactEdgeBirth faceSections triangulation . UndirectedEdgeId . fromIntegral)
-  let faceBirths = fmap faceAlphaBirth faceSections
-  pure
-    AlphaFiltration
-      { alphaFiltrationCells = cellSet
-      , alphaVertexBirths = vertexBirths
-      , alphaEdgeBirths = edgeBirths
-      , alphaFaceBirths = faceBirths
-      }
-
--- | The authoritative resident cells underlying this filtration. This is a
--- read-only projection rather than an exported record field, so callers cannot
--- splice births from one triangulation onto the cells of another.
-alphaFiltrationCellSet :: AlphaFiltration -> ExactCellSet
-alphaFiltrationCellSet = alphaFiltrationCells
-
--- | Exact birth of a resident vertex, when the handle belongs to this
--- filtration.
-alphaVertexBirth :: AlphaFiltration -> VertexId -> Maybe AlphaBirth
-alphaVertexBirth filtration vertex =
-  alphaVertexBirths filtration Vector.!? vertexIdIndex vertex
-
--- | Exact birth of a resident undirected edge, when the handle belongs to this
--- filtration.
-alphaEdgeBirth :: AlphaFiltration -> UndirectedEdgeId -> Maybe AlphaBirth
-alphaEdgeBirth filtration edge =
-  alphaEdgeBirths filtration Vector.!? undirectedEdgeIdIndex edge
-
--- | Exact birth of a resident bounded face, when the handle belongs to this
--- filtration.
-alphaFaceBirth :: AlphaFiltration -> FaceId -> Maybe AlphaBirth
-alphaFaceBirth filtration =
-  faceBirthAt (alphaFaceBirths filtration)
-
--- | Distinct critical births in exact ascending order.
-alphaFiltrationCriticalBirths :: AlphaFiltration -> [AlphaBirth]
-alphaFiltrationCriticalBirths filtration =
-  Set.toAscList
-    ( Set.fromList
-        ( Vector.toList (alphaVertexBirths filtration)
-            <> Vector.toList (alphaEdgeBirths filtration)
-            <> Vector.toList (alphaFaceBirths filtration)
-        )
-    )
-
--- | Select the closed alpha subcomplex at one exact birth. The resident DCEL
--- is retained; only its downward-closed cell section changes.
-alphaComplexAtBirth
-  :: AlphaBirth
-  -> AlphaFiltration
-  -> Either CellSelectionError ExactCellSet
-alphaComplexAtBirth threshold filtration =
-  case alphaFiltrationCells filtration of
-    ExactCellSet triangulation _ _ _ ->
-      exactCellSet
-        triangulation
-        (selectedHandles (VertexId . fromIntegral) (alphaVertexBirths filtration))
-        (selectedHandles (UndirectedEdgeId . fromIntegral) (alphaEdgeBirths filtration))
-        (selectedHandles (FaceId . fromIntegral . (+ 1)) (alphaFaceBirths filtration))
- where
-  selectedHandles :: (Int -> handle) -> Vector AlphaBirth -> [handle]
-  selectedHandles handleAt =
-    Vector.ifoldr
-      (\indexValue birthValue selected -> if birthValue <= threshold then handleAt indexValue : selected else selected)
-      []
-
-alphaComplexAtRadius
-  :: RadiusSquared
-  -> AlphaFiltration
-  -> Either CellSelectionError ExactCellSet
-alphaComplexAtRadius radius =
-  alphaComplexAtBirth (alphaBirthFromRadiusSquared radius)
-
--- | Membership of a bounded face in the closed alpha shape. Exact dyadic
--- comparison makes equality independent of circumcenter rounding. This
--- specialized observation does not construct the full filtration.
-alphaShapeContainsFace
-  :: RadiusSquared
-  -> Triangulation 'Unconstrained vertex directed undirected face
-  -> FaceId
-  -> Bool
-alphaShapeContainsFace radius triangulation =
-  maybe False withinRadius . Dcel.innerFaceVertices triangulation
- where
-  threshold = radiusSquaredValue radius
-  withinRadius (firstVertex, secondVertex, thirdVertex) =
-    let Point ax ay = Dcel.vertexPoint triangulation firstVertex
-        Point bx by = Dcel.vertexPoint triangulation secondVertex
-        Point cx cy = Dcel.vertexPoint triangulation thirdVertex
-     in circumradiusSquaredWithinCoordinates threshold ax ay bx by cx cy
-
-zeroAlphaBirth :: AlphaBirth
-zeroAlphaBirth = alphaBirthFromExact 0
-
-exactFaceSection
-  :: DelaunayTriangulation vertex
-  -> FaceId
-  -> (VertexId, VertexId, VertexId)
-  -> Either AlphaFiltrationError FaceAlphaSection
-exactFaceSection triangulation face verticesValue@(firstVertex, secondVertex, thirdVertex) =
-  let Point ax ay = Dcel.vertexPoint triangulation firstVertex
-      Point bx by = Dcel.vertexPoint triangulation secondVertex
-      Point cx cy = Dcel.vertexPoint triangulation thirdVertex
-   in case exactCircumradiusSquaredRational ax ay bx by cx cy of
-        Left ExactZeroDivisor -> Left (AlphaFaceDegenerate face)
-        Left arithmeticError -> Left (AlphaCircumradiusDivisionFailed face arithmeticError)
-        Right exactBirth ->
-          Right
-            FaceAlphaSection
-              { faceAlphaBirth = alphaBirthFromExact exactBirth
-              , faceAlphaVertices = verticesValue
-              }
-
-exactEdgeBirth
-  :: Vector FaceAlphaSection
-  -> DelaunayTriangulation vertex
-  -> UndirectedEdgeId
-  -> Either AlphaFiltrationError AlphaBirth
-exactEdgeBirth faceSections triangulation edge = do
-  let (fromVertex, toVertex) = Dcel.undirectedEndpoints triangulation edge
-      fromPoint = Dcel.vertexPoint triangulation fromVertex
-      toPoint = Dcel.vertexPoint triangulation toVertex
-      incidentFaces = innerIncidentFaces triangulation edge
-  oppositeVertices <-
-    traverse
-      (oppositeVertexAcross faceSections triangulation edge fromVertex toVertex)
-      incidentFaces
-  let oppositePoints = fmap (Dcel.vertexPoint triangulation) oppositeVertices
-  if all (diametralWitnessOutside fromPoint toPoint) oppositePoints
-    then pure (alphaBirthFromExact (quarterSquaredPointDistance fromPoint toPoint))
-    else leastIncidentFaceBirth edge faceSections incidentFaces
-
-innerIncidentFaces
-  :: DelaunayTriangulation vertex
-  -> UndirectedEdgeId
-  -> [FaceId]
-innerIncidentFaces triangulation edge =
-  let (forward, backward) = directedPair edge
-      forwardFace = Dcel.incidentFace triangulation forward
-      backwardFace = Dcel.incidentFace triangulation backward
-   in case (forwardFace == Dcel.outerFace, backwardFace == Dcel.outerFace) of
-        (True, True) -> []
-        (False, True) -> [forwardFace]
-        (True, False) -> [backwardFace]
-        (False, False)
-          | forwardFace == backwardFace -> [forwardFace]
-          | forwardFace < backwardFace -> [forwardFace, backwardFace]
-          | otherwise -> [backwardFace, forwardFace]
-
-oppositeVertexAcross
-  :: Vector FaceAlphaSection
-  -> DelaunayTriangulation vertex
-  -> UndirectedEdgeId
-  -> VertexId
-  -> VertexId
-  -> FaceId
-  -> Either AlphaFiltrationError VertexId
-oppositeVertexAcross faceSections triangulation edge fromVertex toVertex face =
-  case faceSectionAt faceSections face of
-    Nothing ->
-      Left
-        (AlphaFaceEdgeIncidenceInvalid face edge (Dcel.faceVertices triangulation face))
-    Just faceSection ->
-      let verticesValue@(firstVertex, secondVertex, thirdVertex) = faceAlphaVertices faceSection
-          outsideEdge vertex = vertex /= fromVertex && vertex /= toVertex
-       in case
-            ( outsideEdge firstVertex
-            , outsideEdge secondVertex
-            , outsideEdge thirdVertex
-            )
-          of
-            (True, False, False) -> Right firstVertex
-            (False, True, False) -> Right secondVertex
-            (False, False, True) -> Right thirdVertex
-            _ ->
-              Left
-                ( AlphaFaceEdgeIncidenceInvalid
-                    face
-                    edge
-                    (filter outsideEdge (triangleVertices verticesValue))
-                )
-
-diametralWitnessOutside :: Point -> Point -> Point -> Bool
-diametralWitnessOutside firstPoint secondPoint =
-  not . inDiametralCircle firstPoint secondPoint
-
-quarterSquaredPointDistance :: Point -> Point -> ExactRational
-quarterSquaredPointDistance (Point ax ay) (Point bx by) =
-  exactQuarterSquaredDistanceRational ax ay bx by
-
-leastIncidentFaceBirth
-  :: UndirectedEdgeId
-  -> Vector FaceAlphaSection
-  -> [FaceId]
-  -> Either AlphaFiltrationError AlphaBirth
-leastIncidentFaceBirth edge faceSections incidentFaces = do
-  births <-
-    traverse
-      (\face -> maybe (Left (AlphaIncidentFaceBirthMissing edge face)) (Right . faceAlphaBirth) (faceSectionAt faceSections face))
-      incidentFaces
-  case NonEmpty.nonEmpty births of
-    Nothing -> Left (AlphaEdgeBirthWitnessMissing edge)
-    Just nonEmptyBirths -> Right (minimum nonEmptyBirths)
-
-faceSectionAt :: Vector FaceAlphaSection -> FaceId -> Maybe FaceAlphaSection
-faceSectionAt faceSections face
-  | face == Dcel.outerFace = Nothing
-  | otherwise = faceSections Vector.!? (faceIdIndex face - 1)
-
-faceBirthAt :: Vector AlphaBirth -> FaceId -> Maybe AlphaBirth
-faceBirthAt faceBirths face
-  | face == Dcel.outerFace = Nothing
-  | otherwise = faceBirths Vector.!? (faceIdIndex face - 1)
-
-triangleVertices :: (VertexId, VertexId, VertexId) -> [VertexId]
-triangleVertices (firstVertex, secondVertex, thirdVertex) =
-  [firstVertex, secondVertex, thirdVertex]
-
-alphaBirthFromExact :: ExactRational -> AlphaBirth
-alphaBirthFromExact exactValue =
-  AlphaBirth (alphaOrderKey exactValue) exactValue
-
--- | A monotone exact fixed-point projection of the positive rational. Distinct
--- keys prove order; collisions descend to the authoritative rational. One
--- division at birth replaces repeated cross-products during every downstream
--- ordered-map comparison.
-alphaOrderKey :: ExactRational -> AlphaOrderKey
-alphaOrderKey exactValue =
-  let numerator = exactRationalNumerator exactValue
-      denominator = exactRationalDenominator exactValue
-   in if numerator <= 0
-        then NonPositiveAlphaOrder
-        else
-          let candidateExponent = integerBitLength numerator - integerBitLength denominator
-              binaryExponent =
-                if candidateExponent >= 0
-                  then
-                    if numerator < denominator `shiftL` candidateExponent
-                      then candidateExponent - 1
-                      else candidateExponent
-                else
-                  if numerator `shiftL` negate candidateExponent < denominator
-                    then candidateExponent - 1
-                    else candidateExponent
-              scale = alphaOrderFractionBits - binaryExponent
-              scaledMantissa =
-                if scale >= 0
-                  then (numerator `shiftL` scale) `quot` denominator
-                  else numerator `quot` (denominator `shiftL` negate scale)
-           in PositiveAlphaOrder binaryExponent (fromIntegral scaledMantissa)
-
-alphaOrderFractionBits :: Int
-alphaOrderFractionBits = 16
diff --git a/src-dcel/Moonlight/Triangulation/Canonical.hs b/src-dcel/Moonlight/Triangulation/Canonical.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Canonical.hs
+++ /dev/null
@@ -1,6 +0,0 @@
--- | Construction-independent resident numbering for explicit global comparison.
-module Moonlight.Triangulation.Canonical
-  ( canonicalize
-  ) where
-
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
diff --git a/src-dcel/Moonlight/Triangulation/CellSet.hs b/src-dcel/Moonlight/Triangulation/CellSet.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/CellSet.hs
+++ /dev/null
@@ -1,18 +0,0 @@
--- | Finite downward-closed selections of relatively open vertices, edges, and
--- bounded faces. A selection retains its resident DCEL and exact coordinate
--- projection behind an opaque carrier.
-module Moonlight.Triangulation.CellSet
-  ( ExactCellSet
-  , CellSelectionError (..)
-  , exactCellSet
-  , closeFaceCellSet
-  , exactCellSetVertexCount
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , foldExactCellVertices
-  , foldExactCellEdges
-  , foldExactCellFaces
-  ) where
-
-import Moonlight.Triangulation.Internal.CellSet
-
diff --git a/src-dcel/Moonlight/Triangulation/Dcel.hs b/src-dcel/Moonlight/Triangulation/Dcel.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Dcel.hs
+++ /dev/null
@@ -1,402 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE BangPatterns #-}
-
--- | Constant-time fixed-index observations and persistent payload updates over
--- the immutable DCEL. This is the explicit unchecked resident-index kernel;
--- ordinary consumers should use scoped or owning handles so indices cannot
--- cross mesh boundaries.
-module Moonlight.Triangulation.Dcel
-  ( outerFace
-  , numVertices
-  , numDirectedEdges
-  , numUndirectedEdges
-  , numFaces
-  , numInnerFaces
-  , vertexPoint
-  , vertexPoints
-  , vertexData
-  , directedEdgeData
-  , undirectedEdgeData
-  , faceData
-  , setVertexData
-  , setDirectedEdgeData
-  , setUndirectedEdgeData
-  , setFaceData
-  , mapVertices
-  , mapDirectedEdges
-  , mapUndirectedEdges
-  , mapFaces
-  , imapUndirectedEdges
-  , imapFaces
-  , vertexOutEdge
-  , adjacentEdge
-  , origin
-  , destination
-  , next
-  , previous
-  , incidentFace
-  , isOuterDirectedEdge
-  , isBoundaryEdge
-  , isConstraintEdge
-  , numConstraints
-  , undirectedEndpoints
-  , faceDirectedEdges
-  , faceVertices
-  , innerFaceDirectedEdges
-  , innerFaceDirectedEdgeTriples
-  , innerFaceVertices
-  , innerFaceVertexTriples
-  , vertexOutgoingEdges
-  , clockwise
-  , counterClockwise
-  , foldFaceDirectedEdges'
-  , foldVertexOutgoingEdges'
-  , topologyIndexBytes
-  , geometryTopologyBytes
-  ) where
-
-import qualified Data.Vector as V
-import Data.Word (Word8)
-import GHC.Exts (build)
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedUnsafeIndex, boxedUpdate)
-import Moonlight.Triangulation.Internal.Paged (pagedLength, pagedUnsafeIndex)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.PackedIndex (unpackOptionalIndex)
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Scalar (scalarByteSize)
-
--- | The unique unbounded face, always stored at index zero.
-outerFace :: FaceId
-outerFace = FaceId 0
-
--- | Number of vertices in the mesh.
-numVertices :: Triangulation mode vertex directed undirected face -> Int
-numVertices = pagedLength . triPointX
-{-# INLINE numVertices #-}
-
--- | Number of oriented half-edges in the mesh.
-numDirectedEdges :: Triangulation mode vertex directed undirected face -> Int
-numDirectedEdges = (`quot` 4) . pagedLength . triHalfTopology
-{-# INLINE numDirectedEdges #-}
-
--- | Number of twin pairs in the mesh.
-numUndirectedEdges :: Triangulation mode vertex directed undirected face -> Int
-numUndirectedEdges triangulation = numDirectedEdges triangulation `quot` 2
-{-# INLINE numUndirectedEdges #-}
-
--- | Number of faces, including 'outerFace'.
-numFaces :: Triangulation mode vertex directed undirected face -> Int
-numFaces = pagedLength . triFaceEdge
-{-# INLINE numFaces #-}
-
--- | Number of bounded triangular faces.
-numInnerFaces :: Triangulation mode vertex directed undirected face -> Int
-numInnerFaces triangulation = max 0 (numFaces triangulation - 1)
-{-# INLINE numInnerFaces #-}
-
--- | Authoritative geometric position of an admitted vertex handle.
-vertexPoint :: Triangulation mode vertex directed undirected face -> VertexId -> Point
-vertexPoint triangulation vertex =
-  let !index = vertexIdIndex vertex
-   in Point (pagedUnsafeIndex (triPointX triangulation) index) (pagedUnsafeIndex (triPointY triangulation) index)
-{-# INLINE vertexPoint #-}
-
--- | Dense vertex positions in handle order.
-vertexPoints :: Triangulation mode vertex directed undirected face -> V.Vector Point
-vertexPoints triangulation =
-  V.generate (numVertices triangulation) (vertexPoint triangulation . VertexId . fromIntegral)
-{-# INLINE vertexPoints #-}
-
--- | Annotation carried by an admitted vertex handle.
-vertexData :: Triangulation mode vertex directed undirected face -> VertexId -> vertex
-vertexData triangulation vertex =
-  boxedUnsafeIndex (triVertexData triangulation) (vertexIdIndex vertex)
-{-# INLINE vertexData #-}
-
--- | Annotation carried by an admitted directed edge.
-directedEdgeData :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> directed
-directedEdgeData triangulation edge =
-  boxedUnsafeIndex (triDirectedData triangulation) (directedEdgeIdIndex edge)
-{-# INLINE directedEdgeData #-}
-
--- | Annotation carried by an admitted undirected edge.
-undirectedEdgeData :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> undirected
-undirectedEdgeData triangulation edge =
-  boxedUnsafeIndex (triUndirectedData triangulation) (undirectedEdgeIdIndex edge)
-{-# INLINE undirectedEdgeData #-}
-
--- | Annotation carried by an admitted face.
-faceData :: Triangulation mode vertex directed undirected face -> FaceId -> face
-faceData triangulation face =
-  boxedUnsafeIndex (triFaceData triangulation) (faceIdIndex face)
-{-# INLINE faceData #-}
-
--- | Replace one vertex annotation without changing geometry or topology.
-setVertexData
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> vertex
-  -> Triangulation mode vertex directed undirected face
-setVertexData triangulation vertex payload =
-  triangulation{triVertexData = boxedUpdate (vertexIdIndex vertex) payload (triVertexData triangulation)}
-
--- | Replace one directed-edge annotation without changing geometry or topology.
-setDirectedEdgeData
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedEdgeId
-  -> directed
-  -> Triangulation mode vertex directed undirected face
-setDirectedEdgeData triangulation edge payload =
-  triangulation{triDirectedData = boxedUpdate (directedEdgeIdIndex edge) payload (triDirectedData triangulation)}
-
--- | Replace one undirected-edge annotation without changing geometry or topology.
-setUndirectedEdgeData
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> undirected
-  -> Triangulation mode vertex directed undirected face
-setUndirectedEdgeData triangulation edge payload =
-  triangulation{triUndirectedData = boxedUpdate (undirectedEdgeIdIndex edge) payload (triUndirectedData triangulation)}
-
--- | Replace one face annotation without changing geometry or topology.
-setFaceData
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> face
-  -> Triangulation mode vertex directed undirected face
-setFaceData triangulation face payload =
-  triangulation{triFaceData = boxedUpdate (faceIdIndex face) payload (triFaceData triangulation)}
-
--- | One outgoing directed edge, if the vertex is connected.
-vertexOutEdge :: Triangulation mode vertex directed undirected face -> VertexId -> Maybe DirectedEdgeId
-vertexOutEdge triangulation vertex =
-  DirectedEdgeId . fromIntegral <$> unpackOptionalIndex (pagedUnsafeIndex (triVertexOut triangulation) (vertexIdIndex vertex))
-{-# INLINE vertexOutEdge #-}
-
--- | One boundary edge of a face, if the face has a boundary.
-adjacentEdge :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe DirectedEdgeId
-adjacentEdge triangulation face =
-  DirectedEdgeId . fromIntegral <$> unpackOptionalIndex (pagedUnsafeIndex (triFaceEdge triangulation) (faceIdIndex face))
-{-# INLINE adjacentEdge #-}
-
--- | Origin vertex of an admitted directed edge.
-origin :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> VertexId
-origin triangulation edge =
-  VertexId (pagedUnsafeIndex (triHalfTopology triangulation) (4 * directedEdgeIdIndex edge))
-{-# INLINE origin #-}
-
--- | Destination vertex of an admitted directed edge.
-destination :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> VertexId
-destination triangulation = origin triangulation . reverseEdge
-{-# INLINE destination #-}
-
--- | Next directed edge around the incident face.
-next :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
-next triangulation edge =
-  DirectedEdgeId (pagedUnsafeIndex (triHalfTopology triangulation) (4 * directedEdgeIdIndex edge + 1))
-{-# INLINE next #-}
-
--- | Previous directed edge around the incident face.
-previous :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
-previous triangulation edge =
-  DirectedEdgeId (pagedUnsafeIndex (triHalfTopology triangulation) (4 * directedEdgeIdIndex edge + 2))
-{-# INLINE previous #-}
-
--- | Face on the left of an admitted directed edge.
-incidentFace :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> FaceId
-incidentFace triangulation edge =
-  FaceId (pagedUnsafeIndex (triHalfTopology triangulation) (4 * directedEdgeIdIndex edge + 3))
-{-# INLINE incidentFace #-}
-
--- | Whether the directed edge is incident to the unbounded face.
-isOuterDirectedEdge :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> Bool
-isOuterDirectedEdge triangulation edge = incidentFace triangulation edge == outerFace
-{-# INLINE isOuterDirectedEdge #-}
-
--- | Whether either orientation is incident to the outer face.
-isBoundaryEdge :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> Bool
-isBoundaryEdge triangulation edge =
-  let (forward, backward) = directedPair edge
-   in isOuterDirectedEdge triangulation forward || isOuterDirectedEdge triangulation backward
-{-# INLINE isBoundaryEdge #-}
-
--- | Whether the edge belongs to the constrained-edge section.
-isConstraintEdge :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> Bool
-isConstraintEdge triangulation edge =
-  pagedUnsafeIndex (triConstraint triangulation) (undirectedEdgeIdIndex edge) /= (0 :: Word8)
-{-# INLINE isConstraintEdge #-}
-
--- | Number of constrained undirected edges.
-numConstraints :: Triangulation mode vertex directed undirected face -> Int
-numConstraints = triConstraintCount
-{-# INLINE numConstraints #-}
-
--- | Endpoints in the normalized orientation.
-undirectedEndpoints :: Triangulation mode vertex directed undirected face -> UndirectedEdgeId -> (VertexId, VertexId)
-undirectedEndpoints triangulation edge =
-  let forward = normalizedDirected edge
-   in (origin triangulation forward, destination triangulation forward)
-{-# INLINE undirectedEndpoints #-}
-
--- | Boundary cycle of a face in traversal order.
-faceDirectedEdges :: Triangulation mode vertex directed undirected face -> FaceId -> [DirectedEdgeId]
-faceDirectedEdges triangulation face =
-  case adjacentEdge triangulation face of
-    Nothing -> []
-    Just start -> circularWalk triangulation start (next triangulation)
-{-# INLINE faceDirectedEdges #-}
-
--- | Origins along a face boundary cycle, in traversal order.
-faceVertices :: Triangulation mode vertex directed undirected face -> FaceId -> [VertexId]
-faceVertices triangulation = map (origin triangulation) . faceDirectedEdges triangulation
-{-# INLINE faceVertices #-}
-
--- | The three directed edges of a bounded triangular face.
-innerFaceDirectedEdges :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
-innerFaceDirectedEdges triangulation face
-  | face == outerFace || faceIdIndex face >= numFaces triangulation = Nothing
-  | otherwise = do
-      e0 <- adjacentEdge triangulation face
-      let !e1 = next triangulation e0
-          !e2 = next triangulation e1
-      if next triangulation e2 == e0
-        then Just (e0, e1, e2)
-        else Nothing
-{-# INLINE innerFaceDirectedEdges #-}
-
--- | Dense bounded-face directed-edge triples in face-handle order.
-innerFaceDirectedEdgeTriples
-  :: Triangulation mode vertex directed undirected face
-  -> V.Vector (DirectedEdgeId, DirectedEdgeId, DirectedEdgeId)
-innerFaceDirectedEdgeTriples triangulation =
-  V.generate (numInnerFaces triangulation) $ \innerFaceIndex ->
-    let !faceIndex = innerFaceIndex + 1
-        !firstEdge =
-          DirectedEdgeId
-            (pagedUnsafeIndex (triFaceEdge triangulation) faceIndex)
-        !secondEdge = next triangulation firstEdge
-     in (firstEdge, secondEdge, next triangulation secondEdge)
-{-# INLINE innerFaceDirectedEdgeTriples #-}
-
--- | The three vertices of a bounded triangular face.
-innerFaceVertices :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe (VertexId, VertexId, VertexId)
-innerFaceVertices triangulation face = do
-  (e0, e1, e2) <- innerFaceDirectedEdges triangulation face
-  pure (origin triangulation e0, origin triangulation e1, origin triangulation e2)
-{-# INLINE innerFaceVertices #-}
-
--- | Dense bounded-face vertex triples in face-handle order.
-innerFaceVertexTriples
-  :: Triangulation mode vertex directed undirected face
-  -> V.Vector (VertexId, VertexId, VertexId)
-innerFaceVertexTriples triangulation =
-  fmap
-    (\(firstEdge, secondEdge, thirdEdge) ->
-      ( origin triangulation firstEdge
-      , origin triangulation secondEdge
-      , origin triangulation thirdEdge
-      )
-    )
-    (innerFaceDirectedEdgeTriples triangulation)
-{-# INLINE innerFaceVertexTriples #-}
-
--- | Counter-clockwise ring of directed edges originating at a vertex.
-vertexOutgoingEdges :: Triangulation mode vertex directed undirected face -> VertexId -> [DirectedEdgeId]
-vertexOutgoingEdges triangulation vertex =
-  case vertexOutEdge triangulation vertex of
-    Nothing -> []
-    Just start -> circularWalk triangulation start (counterClockwise triangulation)
-{-# INLINE vertexOutgoingEdges #-}
-
--- | Previous directed edge around its origin vertex.
-clockwise :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
-clockwise triangulation edge = next triangulation (reverseEdge edge)
-{-# INLINE clockwise #-}
-
--- | Next directed edge around its origin vertex.
-counterClockwise :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> DirectedEdgeId
-counterClockwise triangulation edge = reverseEdge (previous triangulation edge)
-{-# INLINE counterClockwise #-}
-
--- | Strict fold over the directed edges around a face.
-foldFaceDirectedEdges'
-  :: Triangulation mode vertex directed undirected face -> FaceId
-  -> (a -> DirectedEdgeId -> a)
-  -> a
-  -> a
-foldFaceDirectedEdges' triangulation face step initial =
-  case adjacentEdge triangulation face of
-    Nothing -> initial
-    Just start -> circularFold triangulation start (next triangulation) step initial
-{-# INLINE foldFaceDirectedEdges' #-}
-
--- | Strict fold over the directed edges originating at a vertex.
-foldVertexOutgoingEdges'
-  :: Triangulation mode vertex directed undirected face -> VertexId
-  -> (a -> DirectedEdgeId -> a)
-  -> a
-  -> a
-foldVertexOutgoingEdges' triangulation vertex step initial =
-  case vertexOutEdge triangulation vertex of
-    Nothing -> initial
-    Just start -> circularFold triangulation start (counterClockwise triangulation) step initial
-{-# INLINE foldVertexOutgoingEdges' #-}
-
--- Constraint bytes are intentionally reported separately by the CDT layer.
--- | Bytes occupied by vertex, half-edge, and face topology indices.
-topologyIndexBytes :: Triangulation mode vertex directed undirected face -> Integer
-topologyIndexBytes triangulation =
-  4 * toInteger
-    ( pagedLength (triVertexOut triangulation)
-        + pagedLength (triHalfTopology triangulation)
-        + pagedLength (triFaceEdge triangulation)
-    )
-
--- | Bytes occupied by authoritative coordinates and topology indices.
-geometryTopologyBytes :: Triangulation mode vertex directed undirected face -> Integer
-geometryTopologyBytes triangulation =
-  2 * toInteger scalarByteSize * toInteger (numVertices triangulation)
-    + topologyIndexBytes triangulation
-
--- | The cycle reached from an edge by repeated advance, in visit order.
---
--- Emitted forwards. Consing in reverse and reversing at the end is the right
--- shape for a strict accumulator, and the wrong one for a producer: it built
--- the ring twice and handed back a list no consumer could fuse with. The
--- guard bound is on the steps taken, which is what it was before — the two
--- forms stop after the same edges.
-circularWalk
-  :: Triangulation mode vertex directed undirected face -> DirectedEdgeId
-  -> (DirectedEdgeId -> DirectedEdgeId)
-  -> [DirectedEdgeId]
-circularWalk triangulation start advance =
-  build
-    ( \(link :: DirectedEdgeId -> result -> result) (stop :: result) ->
-        let go :: Int -> DirectedEdgeId -> Bool -> result
-            go !remaining !current !visited
-              | remaining <= 0 = stop
-              | visited && current == start = stop
-              | otherwise = link current (go (remaining - 1) (advance current) True)
-         in go (numDirectedEdges triangulation + 1) start False
-    )
-{-# INLINE circularWalk #-}
-
-circularFold
-  :: forall mode vertex directed undirected face a.
-     Triangulation mode vertex directed undirected face -> DirectedEdgeId
-  -> (DirectedEdgeId -> DirectedEdgeId)
-  -> (a -> DirectedEdgeId -> a)
-  -> a
-  -> a
-circularFold triangulation start advance step =
-  go (numDirectedEdges triangulation + 1) start False
- where
-  go :: Int -> DirectedEdgeId -> Bool -> a -> a
-  go !remaining !current !visited !accumulator
-    | remaining <= 0 = accumulator
-    | visited && current == start = accumulator
-    | otherwise =
-        let !nextAccumulator = step accumulator current
-         in go (remaining - 1) (advance current) True nextAccumulator
-{-# INLINE circularFold #-}
diff --git a/src-dcel/Moonlight/Triangulation/Exact.hs b/src-dcel/Moonlight/Triangulation/Exact.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Exact.hs
+++ /dev/null
@@ -1,1256 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact rational planar geometry over admitted binary64 points.
-module Moonlight.Triangulation.Exact
-  ( ExactRational
-  , ExactArithmeticError (..)
-  , exactRational
-  , exactRationalNumerator
-  , exactRationalDenominator
-  , exactRationalBitWidth
-  , exactDivide
-  , ExactPoint
-  , exactPoint
-  , exactPointCoordinates
-  , exactPointCross
-  , exactPointBitWidth
-  , ExactAffineLine
-  , ExactHalfPlaneError (..)
-  , exactAffineLine
-  , exactAffineLineCoefficients
-  , oppositeExactAffineLine
-  , exactAffineLineIntersection
-  , ExactClosedHalfPlane
-  , exactClosedHalfPlane
-  , exactClosedHalfPlaneFromDirectedEdge
-  , exactClosedHalfPlaneLine
-  , classifyExactPoint
-  , ExactRetainedPolygon
-  , exactRetainedPolygon
-  , exactRetainedPolygonPoints
-  , ExactClipDisposition (..)
-  , ExactClipError (..)
-  , ExactClipReceipt (..)
-  , exactClipRetainedPolygon
-  , ExactVector (..)
-  , exactVectorFromPoints
-  , addExactVectors
-  , exactCross
-  , compareExactVectorAngle
-  , translateExactPoint
-  , ExactRay
-  , exactRay
-  , exactRayOrigin
-  , exactRayDirection
-  , ExactSegment
-  , ExactGeometryError (..)
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactPointFromPoint
-  , exactPointFromQueryPoint
-  , exactPointToEmbeddingCandidate
-  , exactOrient2d
-  , exactOnClosedSegment
-  , SegmentRelation (..)
-  , allSegmentRelations
-  , exactSegmentRelation
-  , ExactIntersectionError (..)
-  , exactLineIntersection
-  , exactSupportingLineIntersection
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad (foldM)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Maybe (mapMaybe)
-import qualified Data.Sequence as Sequence
-import Data.Sequence (Seq, ViewL (..), ViewR (..), (|>))
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Internal.Dyadic (integerRatioToDouble)
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( admitsSimpleCycleEdgeRelation
-  , cyclePairsNonEmpty
-  , firstNonCounterClockwiseTurn
-  , unorderedPairs
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactArithmeticError (..)
-  , ExactRational
-  , exactDivide
-  , exactRational
-  , exactRationalBitWidth
-  , exactRationalDenominator
-  , exactRationalDenominatorBitWidth
-  , exactRationalFromFiniteDouble
-  , exactRationalIsZero
-  , exactRationalNumerator
-  , exactSignum
-  )
-import Moonlight.Triangulation.Internal.SegmentRelation
-  ( SegmentRelation (..)
-  , allSegmentRelations
-  , segmentRelationWith
-  )
-import Moonlight.Triangulation.Math (mkQueryPoint)
-import Moonlight.Triangulation.Types
-  ( Point (..)
-  , PointValidationError
-  , QueryPoint
-  , queryPointValue
-  )
-
--- | A strict exact Cartesian point.
-data ExactPoint = ExactPoint !ExactRational !ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An exact affine line @a*x + b*y + c = 0@ with a nonzero normal.
-data ExactAffineLine =
-  ExactAffineLine !ExactRational !ExactRational !ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The closed left half-plane of an oriented affine line. A point belongs
--- when the line evaluation is nonnegative.
-newtype ExactClosedHalfPlane = ExactClosedHalfPlane ExactAffineLine
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One polygon vertex together with the original affine line supporting its
--- incoming edge. Intersections therefore never reconstruct a line from
--- already-derived endpoints.
-data ExactRetainedVertex =
-  ExactRetainedVertex !ExactPoint !ExactAffineLine
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A strict counter-clockwise convex polygon whose incoming edges retain
--- their original supporting equations.
-newtype ExactRetainedPolygon =
-  ExactRetainedPolygon (NonEmpty ExactRetainedVertex)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed refusals from affine-line and retained-polygon admission.
-data ExactHalfPlaneError
-  = ExactAffineLineZeroNormal
-      !ExactRational
-      !ExactRational
-      !ExactRational
-  | ExactRetainedPolygonTooFewVertices !Int
-  | ExactRetainedPolygonNonConvexTurn !Int !Ordering
-  | ExactRetainedPolygonSelfRelation !Int !Int !SegmentRelation
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Dimensional disposition after exact closed-half-plane clipping.
-data ExactClipDisposition
-  = ExactClipFullDimensional !ExactRetainedPolygon
-  | ExactClipLowerDimensional !(NonEmpty ExactPoint)
-  | ExactClipEmpty
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed obstructions from an affine intersection that should be unique or
--- from an impossible orientation reversal during convex descent.
-data ExactClipError
-  = ExactClipIntersection !ExactIntersectionError
-  | ExactClipOrientationReversed !ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Structural work and exact-rational width observations for one angular
--- half-plane descent. Submitted half-planes exclude the domain; active
--- boundaries include domain edges admitted to the deque after
--- equal-direction coalescence; zero records a single-plane domain rejection
--- before deque construction.
--- Compatibility checks count deque endpoint predicates; intersections count
--- successful affine-line intersections actually evaluated. Widths describe
--- observed reduced values, not bounds.
-data ExactClipReceipt = ExactClipReceipt
-  { exactClipSubmittedHalfPlanes :: !Int
-  , exactClipActiveBoundaries :: !Int
-  , exactClipBoundaryCompatibilityChecks :: !Int
-  , exactClipExactIntersections :: !Int
-  , exactClipInputCoordinateBits :: !Int
-  , exactClipMaximumAffineCoefficientBits :: !Int
-  , exactClipPeakIntermediateCoordinateBits :: !Int
-  , exactClipFinalCoordinateBits :: !Int
-  , exactClipFinalDenominatorBits :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-instance Semigroup ExactClipReceipt where
-  left <> right =
-    ExactClipReceipt
-      { exactClipSubmittedHalfPlanes =
-          exactClipSubmittedHalfPlanes left + exactClipSubmittedHalfPlanes right
-      , exactClipActiveBoundaries =
-          exactClipActiveBoundaries left + exactClipActiveBoundaries right
-      , exactClipBoundaryCompatibilityChecks =
-          exactClipBoundaryCompatibilityChecks left
-            + exactClipBoundaryCompatibilityChecks right
-      , exactClipExactIntersections =
-          exactClipExactIntersections left + exactClipExactIntersections right
-      , exactClipInputCoordinateBits =
-          max (exactClipInputCoordinateBits left) (exactClipInputCoordinateBits right)
-      , exactClipMaximumAffineCoefficientBits =
-          max
-            (exactClipMaximumAffineCoefficientBits left)
-            (exactClipMaximumAffineCoefficientBits right)
-      , exactClipPeakIntermediateCoordinateBits =
-          max
-            (exactClipPeakIntermediateCoordinateBits left)
-            (exactClipPeakIntermediateCoordinateBits right)
-      , exactClipFinalCoordinateBits =
-          max (exactClipFinalCoordinateBits left) (exactClipFinalCoordinateBits right)
-      , exactClipFinalDenominatorBits =
-          max (exactClipFinalDenominatorBits left) (exactClipFinalDenominatorBits right)
-      }
-
-instance Monoid ExactClipReceipt where
-  mempty = ExactClipReceipt 0 0 0 0 0 0 0 0 0
-
--- | A strict exact segment whose endpoints are distinct.
-data ExactSegment = ExactSegment !ExactPoint !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An exact half-line with an admitted nonzero direction.
-data ExactRay = ExactRay !ExactPoint !ExactVector
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Witness-bearing refusals from exact segment construction.
-data ExactGeometryError
-  = ExactSegmentEndpointsCoincide !ExactPoint
-  | ExactRayZeroDirection !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Admit a half-line, rejecting the only direction that cannot carry one.
-exactRay
-  :: ExactPoint
-  -> ExactVector
-  -> Either ExactGeometryError ExactRay
-exactRay originPoint direction@(ExactVector directionX directionY)
-  | exactRationalIsZero directionX && exactRationalIsZero directionY =
-      Left (ExactRayZeroDirection originPoint)
-  | otherwise = Right (ExactRay originPoint direction)
-
--- | Finite endpoint of an exact ray.
-exactRayOrigin :: ExactRay -> ExactPoint
-exactRayOrigin (ExactRay originPoint _) = originPoint
-{-# INLINE exactRayOrigin #-}
-
--- | Nonzero direction of an exact ray.
-exactRayDirection :: ExactRay -> ExactVector
-exactRayDirection (ExactRay _ direction) = direction
-{-# INLINE exactRayDirection #-}
-
--- | Witness-bearing refusals from exact line intersection.
-data ExactIntersectionError
-  = ExactIntersectionAbsent !SegmentRelation
-  | ExactIntersectionNonUnique !SegmentRelation
-  | ExactIntersectionParallelOrDegenerate !ExactRational
-  | ExactIntersectionArithmetic !ExactArithmeticError
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Construct an exact point from two exact coordinates.
-exactPoint :: ExactRational -> ExactRational -> ExactPoint
-exactPoint = ExactPoint
-{-# INLINE exactPoint #-}
-
--- | Read both exact point coordinates.
-exactPointCoordinates :: ExactPoint -> (ExactRational, ExactRational)
-exactPointCoordinates (ExactPoint x y) = (x, y)
-{-# INLINE exactPointCoordinates #-}
-
--- | Determinant of two points regarded as vectors from the Cartesian origin.
-exactPointCross :: ExactPoint -> ExactPoint -> ExactRational
-exactPointCross (ExactPoint ax ay) (ExactPoint bx by) = ax * by - ay * bx
-{-# INLINE exactPointCross #-}
-
--- | Admit an affine line, refusing precisely the zero normal. Coefficients
--- are retained verbatim because their source identity, rather than a chosen
--- scalar normalization, is the point of this carrier.
-exactAffineLine
-  :: ExactRational
-  -> ExactRational
-  -> ExactRational
-  -> Either ExactHalfPlaneError ExactAffineLine
-exactAffineLine coefficientX coefficientY constant
-  | exactRationalIsZero coefficientX
-      && exactRationalIsZero coefficientY =
-      Left
-        ( ExactAffineLineZeroNormal
-            coefficientX
-            coefficientY
-            constant
-        )
-  | otherwise =
-      Right (ExactAffineLine coefficientX coefficientY constant)
-
--- | Read the retained coefficients @(a,b,c)@ of @a*x+b*y+c=0@.
-exactAffineLineCoefficients
-  :: ExactAffineLine
-  -> (ExactRational, ExactRational, ExactRational)
-exactAffineLineCoefficients (ExactAffineLine coefficientX coefficientY constant) =
-  (coefficientX, coefficientY, constant)
-{-# INLINE exactAffineLineCoefficients #-}
-
--- | Reverse the oriented normal without changing the geometric line.
--- Admission already proves the normal nonzero, so negation is total.
-oppositeExactAffineLine :: ExactAffineLine -> ExactAffineLine
-oppositeExactAffineLine (ExactAffineLine coefficientX coefficientY constant) =
-  ExactAffineLine (negate coefficientX) (negate coefficientY) (negate constant)
-{-# INLINE oppositeExactAffineLine #-}
-
--- | Intersect two admitted affine lines directly from their retained source
--- coefficients. A parallel pair retains the exact determinant witness.
-exactAffineLineIntersection
-  :: ExactAffineLine
-  -> ExactAffineLine
-  -> Either ExactIntersectionError ExactPoint
-exactAffineLineIntersection
-  (ExactAffineLine firstX firstY firstConstant)
-  (ExactAffineLine secondX secondY secondConstant) =
-    -- Descend in homogeneous integer coordinates and normalize each published
-    -- coordinate once. Generic Ratio arithmetic would normalize every product
-    -- in Cramer's rule even though those intermediate rationals are invisible.
-    let (firstXNumerator, firstXDenominator) = exactRationalParts firstX
-        (firstYNumerator, firstYDenominator) = exactRationalParts firstY
-        (firstConstantNumerator, firstConstantDenominator) =
-          exactRationalParts firstConstant
-        (secondXNumerator, secondXDenominator) = exactRationalParts secondX
-        (secondYNumerator, secondYDenominator) = exactRationalParts secondY
-        (secondConstantNumerator, secondConstantDenominator) =
-          exactRationalParts secondConstant
-        determinantNumerator =
-          firstXNumerator
-            * secondYNumerator
-            * secondXDenominator
-            * firstYDenominator
-            - secondXNumerator
-              * firstYNumerator
-              * firstXDenominator
-              * secondYDenominator
-        xNumerator =
-          ( firstYNumerator
-              * secondConstantNumerator
-              * secondYDenominator
-              * firstConstantDenominator
-              - secondYNumerator
-                * firstConstantNumerator
-                * firstYDenominator
-                * secondConstantDenominator
-          )
-            * firstXDenominator
-            * secondXDenominator
-        yNumerator =
-          ( firstConstantNumerator
-              * secondXNumerator
-              * secondConstantDenominator
-              * firstXDenominator
-              - secondConstantNumerator
-                * firstXNumerator
-                * firstConstantDenominator
-                * secondXDenominator
-          )
-            * firstYDenominator
-            * secondYDenominator
-        coordinateDenominator =
-          determinantNumerator
-            * firstConstantDenominator
-            * secondConstantDenominator
-     in if determinantNumerator == 0
-          then Left (ExactIntersectionParallelOrDegenerate 0)
-          else do
-            x <- admitCoordinate xNumerator coordinateDenominator
-            y <- admitCoordinate yNumerator coordinateDenominator
-            pure (ExactPoint x y)
- where
-  admitCoordinate numerator denominator =
-    case exactRational numerator denominator of
-      Left arithmeticError ->
-        Left (ExactIntersectionArithmetic arithmeticError)
-      Right coordinate -> Right coordinate
-
-exactRationalParts :: ExactRational -> (Integer, Integer)
-exactRationalParts value =
-  (exactRationalNumerator value, exactRationalDenominator value)
-{-# INLINE exactRationalParts #-}
-
--- | Orient an admitted affine line so its nonnegative side is retained.
-exactClosedHalfPlane :: ExactAffineLine -> ExactClosedHalfPlane
-exactClosedHalfPlane = ExactClosedHalfPlane
-{-# INLINE exactClosedHalfPlane #-}
-
--- | Construct the closed half-plane to the left of a directed edge.
-exactClosedHalfPlaneFromDirectedEdge
-  :: ExactPoint
-  -> ExactPoint
-  -> Either ExactHalfPlaneError ExactClosedHalfPlane
-exactClosedHalfPlaneFromDirectedEdge
-  (ExactPoint fromX fromY)
-  (ExactPoint toX toY) =
-  exactClosedHalfPlane
-    <$> exactAffineLine
-      (fromY - toY)
-      (toX - fromX)
-      (toY * fromX - toX * fromY)
-
--- | Read the original affine boundary retained by a closed half-plane.
-exactClosedHalfPlaneLine :: ExactClosedHalfPlane -> ExactAffineLine
-exactClosedHalfPlaneLine (ExactClosedHalfPlane line) = line
-{-# INLINE exactClosedHalfPlaneLine #-}
-
--- | Classify a point against a closed half-plane. 'GT' is strict interior,
--- 'EQ' lies on the boundary, and 'LT' is exterior.
-classifyExactPoint :: ExactClosedHalfPlane -> ExactPoint -> Ordering
-classifyExactPoint
-  (ExactClosedHalfPlane (ExactAffineLine coefficientX coefficientY constant))
-  (ExactPoint x y) =
-  -- The common denominator is strictly positive, so its unnormalized integer
-  -- numerator is already the authoritative sign witness.
-  let (coefficientXNumerator, coefficientXDenominator) =
-        exactRationalParts coefficientX
-      (coefficientYNumerator, coefficientYDenominator) =
-        exactRationalParts coefficientY
-      (constantNumerator, constantDenominator) = exactRationalParts constant
-      (xNumerator, xDenominator) = exactRationalParts x
-      (yNumerator, yDenominator) = exactRationalParts y
-      evaluationNumerator =
-        coefficientXNumerator
-          * xNumerator
-          * coefficientYDenominator
-          * yDenominator
-          * constantDenominator
-          + coefficientYNumerator
-            * yNumerator
-            * coefficientXDenominator
-            * xDenominator
-            * constantDenominator
-          + constantNumerator
-            * coefficientXDenominator
-            * xDenominator
-            * coefficientYDenominator
-            * yDenominator
-   in compare evaluationNumerator 0
-{-# INLINE classifyExactPoint #-}
-
--- | Admit strict counter-clockwise convex points and retain the original
--- supporting line of every incoming edge.
-exactRetainedPolygon
-  :: NonEmpty ExactPoint
-  -> Either ExactHalfPlaneError ExactRetainedPolygon
-exactRetainedPolygon points
-  | NonEmpty.length points < 3 =
-      Left (ExactRetainedPolygonTooFewVertices (NonEmpty.length points))
-  | otherwise =
-      case firstNonCounterClockwiseTurn exactOrient2d points of
-        Just (index, turn) ->
-          Left (ExactRetainedPolygonNonConvexTurn index turn)
-        Nothing -> do
-          validateSimpleRetainedCycle points
-          ExactRetainedPolygon
-            <$> traverse retainIncomingLine (cyclePairsNonEmpty points)
- where
-  retainIncomingLine (from, to) = do
-    halfPlane <- exactClosedHalfPlaneFromDirectedEdge from to
-    pure (ExactRetainedVertex to (exactClosedHalfPlaneLine halfPlane))
-
-validateSimpleRetainedCycle
-  :: NonEmpty ExactPoint
-  -> Either ExactHalfPlaneError ()
-validateSimpleRetainedCycle points =
-  case
-    [ (leftIndex, rightIndex, relation)
-    | ( (leftIndex, (leftFrom, leftTo))
-        , (rightIndex, (rightFrom, rightTo))
-        ) <- unorderedPairs indexedEdges
-    , let relation =
-            exactSegmentRelation
-              leftFrom
-              leftTo
-              rightFrom
-              rightTo
-    , not
-        ( admitsSimpleCycleEdgeRelation
-            segmentCount
-            leftIndex
-            rightIndex
-            relation
-        )
-    ] of
-    (leftIndex, rightIndex, relation) : _ ->
-      Left
-        ( ExactRetainedPolygonSelfRelation
-            leftIndex
-            rightIndex
-            relation
-        )
-    [] -> Right ()
- where
-  indexedEdges = zip [0 :: Int ..] (NonEmpty.toList (cyclePairsNonEmpty points))
-  segmentCount = NonEmpty.length points
-
--- | Project the retained-edge carrier to its authoritative point cycle.
-exactRetainedPolygonPoints :: ExactRetainedPolygon -> NonEmpty ExactPoint
-exactRetainedPolygonPoints (ExactRetainedPolygon vertices) =
-  fmap retainedVertexPoint vertices
-
--- | Intersect a retained convex polygon with exact closed half-planes.
--- Boundaries descend in exact angular order through one immutable deque;
--- every published edge retains one submitted source line, and derived
--- endpoints never become line coefficients.
-exactClipRetainedPolygon
-  :: ExactRetainedPolygon
-  -> [ExactClosedHalfPlane]
-  -> Either ExactClipError (ExactClipDisposition, ExactClipReceipt)
-exactClipRetainedPolygon polygon halfPlanes = do
-  let inputPoints = exactRetainedPolygonPoints polygon
-      inputBits = maximumPointBitWidth inputPoints
-      domainHalfPlanes = retainedPolygonHalfPlanes polygon
-      allHalfPlanes = domainHalfPlanes <> halfPlanes
-      initialReceipt =
-        ExactClipReceipt
-          { exactClipSubmittedHalfPlanes = length halfPlanes
-          , exactClipActiveBoundaries =
-              if null halfPlanes then NonEmpty.length inputPoints else 0
-          , exactClipBoundaryCompatibilityChecks = 0
-          , exactClipExactIntersections = 0
-          , exactClipInputCoordinateBits = inputBits
-          , exactClipMaximumAffineCoefficientBits =
-              List.foldl'
-                (\bits -> max bits . affineLineBitWidth . exactClosedHalfPlaneLine)
-                0
-                allHalfPlanes
-          , exactClipPeakIntermediateCoordinateBits = inputBits
-          , exactClipFinalCoordinateBits = inputBits
-          , exactClipFinalDenominatorBits =
-              maximumPointDenominatorBitWidth inputPoints
-          }
-  if null halfPlanes
-    then pure (ExactClipFullDimensional polygon, initialReceipt)
-    else case List.find (`excludesExactDomain` inputPoints) halfPlanes of
-      Just _ ->
-        pure
-          ( ExactClipEmpty
-          , initialReceipt
-              { exactClipFinalCoordinateBits = 0
-              , exactClipFinalDenominatorBits = 0
-              }
-          )
-      Nothing -> do
-        let orderedBoundaries =
-              coalesceAngularBoundaries (fmap angularBoundary allHalfPlanes)
-            descentReceipt =
-              initialReceipt
-                { exactClipActiveBoundaries = length orderedBoundaries
-                }
-        (openDeque, descendedReceipt) <-
-          foldM insertAngularBoundary (Sequence.empty, descentReceipt) orderedBoundaries
-        (closedDeque, accumulatedReceipt) <- closeAngularBoundaryDeque openDeque descendedReceipt
-        (finalState, finalizedReceipt) <-
-          angularBoundaryDequeState orderedBoundaries closedDeque accumulatedReceipt
-        let disposition = exactClipStateDisposition finalState
-            finalPoints = exactClipDispositionPoints disposition
-            finalReceipt =
-              finalizedReceipt
-                { exactClipFinalCoordinateBits = maybe 0 maximumPointBitWidth finalPoints
-                , exactClipFinalDenominatorBits =
-                    maybe 0 maximumPointDenominatorBitWidth finalPoints
-                }
-        pure (disposition, finalReceipt)
-
--- | A linear functional attains its maximum over a convex polygon at a
--- vertex.  One half-plane whose closed side contains no domain vertex is
--- therefore an exact emptiness certificate; recognizing it before angular
--- descent avoids sorting a section that has no global point to glue.
-excludesExactDomain
-  :: ExactClosedHalfPlane
-  -> NonEmpty ExactPoint
-  -> Bool
-excludesExactDomain halfPlane =
-  all ((== LT) . classifyExactPoint halfPlane)
-
--- | One original source boundary together with its exact counter-clockwise
--- direction.  Equal-direction gluing retains one of these values verbatim.
-data ExactAngularBoundary = ExactAngularBoundary
-  { angularBoundaryLine :: !ExactAffineLine
-  , angularBoundaryDirection :: !ExactVector
-  }
-
-angularBoundary :: ExactClosedHalfPlane -> ExactAngularBoundary
-angularBoundary halfPlane =
-  let line@(ExactAffineLine coefficientX coefficientY _) =
-        exactClosedHalfPlaneLine halfPlane
-   in ExactAngularBoundary
-        { angularBoundaryLine = line
-        , angularBoundaryDirection = ExactVector coefficientY (negate coefficientX)
-        }
-
-angularBoundaryHalfPlane :: ExactAngularBoundary -> ExactClosedHalfPlane
-angularBoundaryHalfPlane = exactClosedHalfPlane . angularBoundaryLine
-{-# INLINE angularBoundaryHalfPlane #-}
-
-retainedPolygonHalfPlanes :: ExactRetainedPolygon -> [ExactClosedHalfPlane]
-retainedPolygonHalfPlanes (ExactRetainedPolygon vertices) =
-  fmap
-    (exactClosedHalfPlane . retainedVertexIncomingLine)
-    (NonEmpty.toList vertices)
-
--- | Descent over one angular stalk.  A stricter parallel boundary replaces a
--- weaker one; coincident boundaries retain the least original source line so
--- input permutations cannot alter the glued carrier.
-coalesceAngularBoundaries :: [ExactAngularBoundary] -> [ExactAngularBoundary]
-coalesceAngularBoundaries =
-  fmap strongestAngularBoundary
-    . mapMaybe NonEmpty.nonEmpty
-    . List.groupBy sameBoundaryDirection
-    . List.sortBy compareAngularBoundaries
- where
-  sameBoundaryDirection left right =
-    compareExactVectorAngle
-      (angularBoundaryDirection left)
-      (angularBoundaryDirection right)
-      == EQ
-
-compareAngularBoundaries :: ExactAngularBoundary -> ExactAngularBoundary -> Ordering
-compareAngularBoundaries left right =
-  case
-    compareExactVectorAngle
-      (angularBoundaryDirection left)
-      (angularBoundaryDirection right) of
-    EQ -> compare (angularBoundaryLine left) (angularBoundaryLine right)
-    ordering -> ordering
-
-strongestAngularBoundary :: NonEmpty ExactAngularBoundary -> ExactAngularBoundary
-strongestAngularBoundary (initial :| remaining) =
-  List.foldl' chooseStrongerAngularBoundary initial remaining
-
-chooseStrongerAngularBoundary
-  :: ExactAngularBoundary
-  -> ExactAngularBoundary
-  -> ExactAngularBoundary
-chooseStrongerAngularBoundary selected candidate =
-  case compareParallelBoundaryStrength selected candidate of
-    LT -> candidate
-    GT -> selected
-    EQ ->
-      if angularBoundaryLine candidate < angularBoundaryLine selected
-        then candidate
-        else selected
-
--- | Compare same-direction boundaries.  'LT' means the right boundary is
--- stricter, 'GT' means the left is stricter, and 'EQ' means the closed
--- half-planes coincide.  Cross multiplication avoids inventing a normalized
--- replacement line.
-compareParallelBoundaryStrength
-  :: ExactAngularBoundary
-  -> ExactAngularBoundary
-  -> Ordering
-compareParallelBoundaryStrength left right =
-  let ExactAffineLine leftX leftY leftConstant = angularBoundaryLine left
-      ExactAffineLine rightX rightY rightConstant = angularBoundaryLine right
-      compareScaled leftScale rightScale scaleSign =
-        let raw = compare (rightConstant * leftScale) (rightScale * leftConstant)
-         in if exactSignum scaleSign == LT then reverseOrdering raw else raw
-   in if exactRationalIsZero leftX
-        then compareScaled leftY rightY leftY
-        else compareScaled leftX rightX leftX
-
-reverseOrdering :: Ordering -> Ordering
-reverseOrdering LT = GT
-reverseOrdering EQ = EQ
-reverseOrdering GT = LT
-
--- | One active source boundary and its cached intersection with the preceding
--- deque boundary.  Every adjacency point is derived once and reused by both
--- compatibility trims and final publication.
-data ExactAngularBoundaryNode = ExactAngularBoundaryNode
-  { angularNodeBoundary :: !ExactAngularBoundary
-  , angularNodePreviousIntersection :: !(Maybe ExactPoint)
-  }
-
-type ExactAngularBoundaryDeque = Seq ExactAngularBoundaryNode
-
-insertAngularBoundary
-  :: (ExactAngularBoundaryDeque, ExactClipReceipt)
-  -> ExactAngularBoundary
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-insertAngularBoundary (boundaries, receipt) incoming = do
-  (backTrimmed, backReceipt) <- trimAngularBack incoming boundaries receipt
-  (frontTrimmed, frontReceipt) <- trimAngularFront incoming backTrimmed backReceipt
-  appendAngularBoundary incoming frontTrimmed frontReceipt
-
-appendAngularBoundary
-  :: ExactAngularBoundary
-  -> ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-appendAngularBoundary incoming boundaries receipt =
-  case Sequence.viewr boundaries of
-    EmptyR ->
-      Right
-        ( Sequence.singleton (ExactAngularBoundaryNode incoming Nothing)
-        , receipt
-        )
-    _ :> finalNode -> do
-      (intersection, observedReceipt) <-
-        observeAdjacentIntersection
-          (angularNodeBoundary finalNode)
-          incoming
-          receipt
-      pure
-        ( boundaries
-            |> ExactAngularBoundaryNode incoming intersection
-        , observedReceipt
-        )
-
-trimAngularBack
-  :: ExactAngularBoundary
-  -> ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-trimAngularBack incoming boundaries receipt =
-  case Sequence.viewr boundaries of
-    EmptyR -> Right (boundaries, receipt)
-    remaining :> finalNode ->
-      case Sequence.viewr remaining of
-        EmptyR -> Right (boundaries, receipt)
-        _ :> _ ->
-          case angularNodePreviousIntersection finalNode of
-            Nothing -> Right (boundaries, receipt)
-            Just point ->
-              let checkedReceipt = observeBoundaryCompatibility receipt
-               in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
-                    then trimAngularBack incoming remaining checkedReceipt
-                    else Right (boundaries, checkedReceipt)
-
-trimAngularFront
-  :: ExactAngularBoundary
-  -> ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-trimAngularFront incoming boundaries receipt =
-  case Sequence.viewl boundaries of
-    EmptyL -> Right (boundaries, receipt)
-    _ :< remaining ->
-      case Sequence.viewl remaining of
-        EmptyL -> Right (boundaries, receipt)
-        secondNode :< suffix ->
-          case angularNodePreviousIntersection secondNode of
-            Nothing -> Right (boundaries, receipt)
-            Just point ->
-              let checkedReceipt = observeBoundaryCompatibility receipt
-               in if classifyExactPoint (angularBoundaryHalfPlane incoming) point == LT
-                    then
-                      trimAngularFront
-                        incoming
-                        ( secondNode
-                            { angularNodePreviousIntersection = Nothing
-                            }
-                            Sequence.<| suffix
-                        )
-                        checkedReceipt
-                    else Right (boundaries, checkedReceipt)
-
-closeAngularBoundaryDeque
-  :: ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-closeAngularBoundaryDeque boundaries receipt = do
-  (backClosed, backReceipt) <- closeAngularBack boundaries receipt
-  (frontClosed, frontReceipt) <- closeAngularFront backClosed backReceipt
-  if Sequence.length frontClosed == Sequence.length boundaries
-    then Right (frontClosed, frontReceipt)
-    else closeAngularBoundaryDeque frontClosed frontReceipt
-
-closeAngularBack
-  :: ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-closeAngularBack boundaries receipt =
-  case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
-    (firstNode :< _, remaining :> finalNode) ->
-      case Sequence.viewr remaining of
-        _ :> _ ->
-          case angularNodePreviousIntersection finalNode of
-            Nothing -> Right (boundaries, receipt)
-            Just point ->
-              let checkedReceipt = observeBoundaryCompatibility receipt
-               in if
-                    classifyExactPoint
-                      (angularBoundaryHalfPlane (angularNodeBoundary firstNode))
-                      point
-                      == LT
-                    then closeAngularBack remaining checkedReceipt
-                    else Right (boundaries, checkedReceipt)
-        EmptyR -> Right (boundaries, receipt)
-    _ -> Right (boundaries, receipt)
-
-closeAngularFront
-  :: ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactAngularBoundaryDeque, ExactClipReceipt)
-closeAngularFront boundaries receipt =
-  case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
-    (_ :< remaining, _ :> finalNode) ->
-      case Sequence.viewl remaining of
-        secondNode :< suffix ->
-          case angularNodePreviousIntersection secondNode of
-            Nothing -> Right (boundaries, receipt)
-            Just point ->
-              let checkedReceipt = observeBoundaryCompatibility receipt
-               in if
-                    classifyExactPoint
-                      (angularBoundaryHalfPlane (angularNodeBoundary finalNode))
-                      point
-                      == LT
-                    then
-                      closeAngularFront
-                        ( secondNode
-                            { angularNodePreviousIntersection = Nothing
-                            }
-                            Sequence.<| suffix
-                        )
-                        checkedReceipt
-                    else Right (boundaries, checkedReceipt)
-        EmptyL -> Right (boundaries, receipt)
-    _ -> Right (boundaries, receipt)
-
-observeAdjacentIntersection
-  :: ExactAngularBoundary
-  -> ExactAngularBoundary
-  -> ExactClipReceipt
-  -> Either ExactClipError (Maybe ExactPoint, ExactClipReceipt)
-observeAdjacentIntersection left right receipt =
-  let leftLine = angularBoundaryLine left
-      rightLine = angularBoundaryLine right
-   in case exactAffineLineIntersection leftLine rightLine of
-        Left (ExactIntersectionParallelOrDegenerate _) ->
-          Right (Nothing, receipt)
-        Left obstruction -> Left (ExactClipIntersection obstruction)
-        Right point ->
-          pure
-            ( Just point
-            , receipt
-                { exactClipExactIntersections =
-                    exactClipExactIntersections receipt + 1
-                , exactClipPeakIntermediateCoordinateBits =
-                    max
-                      (exactClipPeakIntermediateCoordinateBits receipt)
-                      (exactPointBitWidth point)
-                }
-            )
-
-observeBoundaryCompatibility :: ExactClipReceipt -> ExactClipReceipt
-observeBoundaryCompatibility receipt =
-  receipt
-    { exactClipBoundaryCompatibilityChecks =
-        exactClipBoundaryCompatibilityChecks receipt + 1
-    }
-
-angularBoundaryDequeState
-  :: [ExactAngularBoundary]
-  -> ExactAngularBoundaryDeque
-  -> ExactClipReceipt
-  -> Either ExactClipError (ExactClipState, ExactClipReceipt)
-angularBoundaryDequeState allBoundaries boundaries receipt = do
-  let nodeList = foldr (:) [] boundaries
-      linearVertices =
-        mapMaybe
-          retainedLinearVertex
-          (zip nodeList (drop 1 nodeList))
-  (closingVertices, finalizedReceipt) <-
-    case (Sequence.viewl boundaries, Sequence.viewr boundaries) of
-      (firstNode :< _, _ :> finalNode) -> do
-        (intersection, observedReceipt) <-
-          observeAdjacentIntersection
-            (angularNodeBoundary finalNode)
-            (angularNodeBoundary firstNode)
-            receipt
-        pure
-          ( maybe
-              []
-              (\point ->
-                 [ ExactRetainedVertex
-                     point
-                     (angularBoundaryLine (angularNodeBoundary finalNode))
-                 ])
-              intersection
-          , observedReceipt
-          )
-      _ -> Right ([], receipt)
-  state <-
-    retainedVerticesToClipState
-      (normalizeRetainedVertices (linearVertices <> closingVertices))
-  pure (validateLowerDimensionalState allBoundaries state, finalizedReceipt)
- where
-  retainedLinearVertex (node, successor) =
-    fmap
-      (\point ->
-         ExactRetainedVertex
-           point
-           (angularBoundaryLine (angularNodeBoundary node)))
-      (angularNodePreviousIntersection successor)
-
-validateLowerDimensionalState
-  :: [ExactAngularBoundary]
-  -> ExactClipState
-  -> ExactClipState
-validateLowerDimensionalState boundaries state =
-  case state of
-    ExactClipStateSegment from to ->
-      if all (containsBoth from to) boundaries
-        then state
-        else ExactClipStateEmpty
-    ExactClipStatePoint point ->
-      if all
-          (\boundary ->
-             classifyExactPoint (angularBoundaryHalfPlane boundary) point /= LT)
-          boundaries
-        then state
-        else ExactClipStateEmpty
-    other -> other
- where
-  containsBoth from to boundary =
-    let halfPlane = angularBoundaryHalfPlane boundary
-     in classifyExactPoint halfPlane from /= LT
-          && classifyExactPoint halfPlane to /= LT
-
-data ExactClipState
-  = ExactClipStatePolygon !ExactRetainedPolygon
-  | ExactClipStateSegment !ExactPoint !ExactPoint
-  | ExactClipStatePoint !ExactPoint
-  | ExactClipStateEmpty
-exactSegmentClipState
-  :: ExactPoint
-  -> ExactPoint
-  -> ExactClipState
-exactSegmentClipState from to
-  | from == to = ExactClipStatePoint from
-  | otherwise = ExactClipStateSegment from to
-
-retainedVerticesToClipState
-  :: [ExactRetainedVertex]
-  -> Either ExactClipError ExactClipState
-retainedVerticesToClipState [] = Right ExactClipStateEmpty
-retainedVerticesToClipState [vertex] =
-  Right (ExactClipStatePoint (retainedVertexPoint vertex))
-retainedVerticesToClipState [fromVertex, toVertex] =
-  Right
-    ( exactSegmentClipState
-        (retainedVertexPoint fromVertex)
-        (retainedVertexPoint toVertex)
-    )
-retainedVerticesToClipState (firstVertex : secondVertex : remaining) =
-  let retained = firstVertex :| (secondVertex : remaining)
-      twiceArea = retainedVerticesTwiceArea retained
-   in case exactSignum twiceArea of
-        GT -> Right (ExactClipStatePolygon (ExactRetainedPolygon retained))
-        LT -> Left (ExactClipOrientationReversed twiceArea)
-        EQ -> Right (lowerDimensionalClipState retained)
-
-lowerDimensionalClipState
-  :: NonEmpty ExactRetainedVertex
-  -> ExactClipState
-lowerDimensionalClipState vertices =
-  let points = fmap retainedVertexPoint vertices
-      initialPoint :| remainingPoints = points
-      leastPoint = List.foldl' min initialPoint remainingPoints
-      greatestPoint = List.foldl' max initialPoint remainingPoints
-   in if leastPoint == greatestPoint
-        then ExactClipStatePoint leastPoint
-        else ExactClipStateSegment leastPoint greatestPoint
-
-retainedVerticesTwiceArea
-  :: NonEmpty ExactRetainedVertex
-  -> ExactRational
-retainedVerticesTwiceArea =
-  List.foldl'
-    (\area (from, to) ->
-       area
-         + exactPointCross
-           (retainedVertexPoint from)
-           (retainedVertexPoint to))
-    0
-    . cyclePairsNonEmpty
-
-normalizeRetainedVertices
-  :: [ExactRetainedVertex]
-  -> [ExactRetainedVertex]
-normalizeRetainedVertices = stripClosingDuplicate . deduplicateAdjacent
- where
-  deduplicateAdjacent [] = []
-  deduplicateAdjacent (firstVertex : remaining) =
-    firstVertex : deduplicateAfter firstVertex remaining
-
-  deduplicateAfter _ [] = []
-  deduplicateAfter previous (candidate : remaining)
-    | retainedVertexPoint previous == retainedVertexPoint candidate =
-        deduplicateAfter previous remaining
-    | otherwise = candidate : deduplicateAfter candidate remaining
-
-  stripClosingDuplicate vertices@(firstVertex : remainingVertices) =
-    case reverse remainingVertices of
-      finalVertex : reversedInterior
-        | retainedVertexPoint finalVertex == retainedVertexPoint firstVertex ->
-            finalVertex : reverse reversedInterior
-      _ -> vertices
-  stripClosingDuplicate [] = []
-
-exactClipStateDisposition :: ExactClipState -> ExactClipDisposition
-exactClipStateDisposition state =
-  case state of
-    ExactClipStatePolygon polygon -> ExactClipFullDimensional polygon
-    ExactClipStateSegment from to ->
-      ExactClipLowerDimensional (from :| [to])
-    ExactClipStatePoint point -> ExactClipLowerDimensional (point :| [])
-    ExactClipStateEmpty -> ExactClipEmpty
-
-exactClipDispositionPoints
-  :: ExactClipDisposition
-  -> Maybe (NonEmpty ExactPoint)
-exactClipDispositionPoints disposition =
-  case disposition of
-    ExactClipFullDimensional polygon ->
-      Just (exactRetainedPolygonPoints polygon)
-    ExactClipLowerDimensional points -> Just points
-    ExactClipEmpty -> Nothing
-
-retainedVertexPoint :: ExactRetainedVertex -> ExactPoint
-retainedVertexPoint (ExactRetainedVertex point _) = point
-{-# INLINE retainedVertexPoint #-}
-
-retainedVertexIncomingLine :: ExactRetainedVertex -> ExactAffineLine
-retainedVertexIncomingLine (ExactRetainedVertex _ line) = line
-{-# INLINE retainedVertexIncomingLine #-}
-
-affineLineBitWidth :: ExactAffineLine -> Int
-affineLineBitWidth (ExactAffineLine coefficientX coefficientY constant) =
-  max
-    (exactRationalBitWidth coefficientX)
-    (max (exactRationalBitWidth coefficientY) (exactRationalBitWidth constant))
-
-maximumPointBitWidth :: NonEmpty ExactPoint -> Int
-maximumPointBitWidth =
-  List.foldl'
-    (\maximumBits (ExactPoint x y) ->
-       max maximumBits (max (exactRationalBitWidth x) (exactRationalBitWidth y)))
-    0
-
--- | Maximum reduced numerator-or-denominator width of either coordinate.
-exactPointBitWidth :: ExactPoint -> Int
-exactPointBitWidth (ExactPoint x y) =
-  max (exactRationalBitWidth x) (exactRationalBitWidth y)
-{-# INLINE exactPointBitWidth #-}
-
-maximumPointDenominatorBitWidth :: NonEmpty ExactPoint -> Int
-maximumPointDenominatorBitWidth =
-  List.foldl'
-    (\maximumBits (ExactPoint x y) ->
-       max
-         maximumBits
-         ( max
-             (exactRationalDenominatorBitWidth x)
-             (exactRationalDenominatorBitWidth y)
-         ))
-    0
-
--- | Construct an exact segment, refusing coincident endpoints with their
--- shared point as the witness.
-exactSegment
-  :: ExactPoint
-  -> ExactPoint
-  -> Either ExactGeometryError ExactSegment
-exactSegment from to
-  | from == to = Left (ExactSegmentEndpointsCoincide from)
-  | otherwise = Right (ExactSegment from to)
-
--- | Read both distinct exact segment endpoints.
-exactSegmentEndpoints :: ExactSegment -> (ExactPoint, ExactPoint)
-exactSegmentEndpoints (ExactSegment from to) = (from, to)
-{-# INLINE exactSegmentEndpoints #-}
-
--- | Validate and exactly embed a raw binary64 point.
-exactPointFromPoint :: Point -> Either PointValidationError ExactPoint
-exactPointFromPoint = fmap exactPointFromQueryPoint . mkQueryPoint
-
--- | Exactly embed an already-admitted query point without repeating
--- coordinate validation.
-exactPointFromQueryPoint :: QueryPoint -> ExactPoint
-exactPointFromQueryPoint queryPoint =
-  case queryPointValue queryPoint of
-    Point x y ->
-      ExactPoint
-        (exactRationalFromFiniteDouble x)
-        (exactRationalFromFiniteDouble y)
-{-# INLINE exactPointFromQueryPoint #-}
-
--- | Deterministically project an exact point to a validated binary64
--- embedding candidate. This is not a correctly-rounded nearest-double claim;
--- callers must certify the candidate projection before relying on it.
-exactPointToEmbeddingCandidate
-  :: ExactPoint
-  -> Either PointValidationError QueryPoint
-exactPointToEmbeddingCandidate (ExactPoint x y) =
-  mkQueryPoint
-    ( Point
-        (integerRatioToDouble (exactRationalNumerator x) (exactRationalDenominator x))
-        (integerRatioToDouble (exactRationalNumerator y) (exactRationalDenominator y))
-    )
-
--- | Exact orientation of an ordered triple. 'GT' is a positive determinant
--- and counter-clockwise turn, 'EQ' is collinear, and 'LT' is clockwise.
-exactOrient2d :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
-exactOrient2d
-  (ExactPoint ax ay)
-  (ExactPoint bx by)
-  (ExactPoint cx cy) =
-    exactSignum
-      ((bx - ax) * (cy - ay) - (by - ay) * (cx - ax))
-{-# INLINE exactOrient2d #-}
-
--- | Whether an exact point lies on an exact closed segment.
-exactOnClosedSegment :: ExactPoint -> ExactPoint -> ExactPoint -> Bool
-exactOnClosedSegment
-  from@(ExactPoint ax ay)
-  to@(ExactPoint bx by)
-  query@(ExactPoint qx qy) =
-    exactOrient2d from to query == EQ
-      && qx >= min ax bx
-      && qx <= max ax bx
-      && qy >= min ay by
-      && qy <= max ay by
-{-# INLINE exactOnClosedSegment #-}
-
--- | Exact rational specialization of the one closed-segment relation policy.
-exactSegmentRelation
-  :: ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> SegmentRelation
-exactSegmentRelation =
-  segmentRelationWith (==) compare exactOrient2d exactOnClosedSegment
-{-# INLINE exactSegmentRelation #-}
-
--- | Return the unique exact intersection of two exact segments. Disjoint and
--- non-unique relations are refused with their relation witness; a zero line
--- cross product and arithmetic failure retain their exact witnesses.
-exactLineIntersection
-  :: ExactSegment
-  -> ExactSegment
-  -> Either ExactIntersectionError ExactPoint
-exactLineIntersection
-  (ExactSegment a b)
-  (ExactSegment c d) =
-    case exactSegmentRelation a b c d of
-      SegmentsDisjoint -> Left (ExactIntersectionAbsent SegmentsDisjoint)
-      SegmentsDuplicate -> Left (ExactIntersectionNonUnique SegmentsDuplicate)
-      SegmentsCollinearlyOverlap ->
-        Left (ExactIntersectionNonUnique SegmentsCollinearlyOverlap)
-      SegmentsShareEndpoint -> uniqueIntersection
-      SegmentsProperlyCross -> uniqueIntersection
-      SegmentEndpointTouchesInterior -> uniqueIntersection
- where
-  uniqueIntersection =
-    exactSupportingLineIntersection (ExactSegment a b) (ExactSegment c d)
-
--- | Intersect the infinite supporting lines of two admitted exact segments.
--- Unlike 'exactLineIntersection', the intersection need not lie inside either
--- closed segment. Parallel supporting lines retain the exact zero denominator
--- witness.
-exactSupportingLineIntersection
-  :: ExactSegment
-  -> ExactSegment
-  -> Either ExactIntersectionError ExactPoint
-exactSupportingLineIntersection
-  (ExactSegment a b)
-  (ExactSegment c d) =
-  let directionAB = exactVectorFromPoints a b
-      directionCD = exactVectorFromPoints c d
-      fromAToC = exactVectorFromPoints a c
-      denominator = exactCross directionAB directionCD
-      numerator = exactCross fromAToC directionCD
-   in case exactDivide numerator denominator of
-        Left ExactZeroDivisor ->
-          Left (ExactIntersectionParallelOrDegenerate denominator)
-        Left arithmeticError -> Left (ExactIntersectionArithmetic arithmeticError)
-        Right parameter ->
-          Right (translateExactPoint a (scaleExactVector parameter directionAB))
-
--- | A strict exact displacement vector. Points and vectors remain distinct;
--- all exact planar algorithms share this single vector carrier.
-data ExactVector = ExactVector !ExactRational !ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-exactVectorFromPoints :: ExactPoint -> ExactPoint -> ExactVector
-exactVectorFromPoints (ExactPoint ax ay) (ExactPoint bx by) =
-  ExactVector (bx - ax) (by - ay)
-
-addExactVectors :: ExactVector -> ExactVector -> ExactVector
-addExactVectors (ExactVector ax ay) (ExactVector bx by) =
-  ExactVector (ax + bx) (ay + by)
-
-exactCross :: ExactVector -> ExactVector -> ExactRational
-exactCross (ExactVector ax ay) (ExactVector bx by) =
-  ax * by - ay * bx
-
--- | Counter-clockwise angular order from the positive x-axis. Collinear
--- vectors on the same ray compare equal so convolution can merge them;
--- callers that need a total point order may add their own radial tie-break.
-compareExactVectorAngle :: ExactVector -> ExactVector -> Ordering
-compareExactVectorAngle left right =
-  case compare (vectorHalf left) (vectorHalf right) of
-    EQ ->
-      case exactCrossSign left right of
-        GT -> LT
-        LT -> GT
-        EQ -> EQ
-    ordering -> ordering
- where
-  vectorHalf (ExactVector x y)
-    | exactSignum y == GT = False
-    | exactSignum y == EQ && exactSignum x /= LT = False
-    | otherwise = True
-{-# INLINE compareExactVectorAngle #-}
-
-exactCrossSign :: ExactVector -> ExactVector -> Ordering
-exactCrossSign (ExactVector leftX leftY) (ExactVector rightX rightY) =
-  -- Reduced rational denominators are positive; compare the two products in a
-  -- common integer scale without constructing and normalizing their ratios.
-  let (leftXNumerator, leftXDenominator) = exactRationalParts leftX
-      (leftYNumerator, leftYDenominator) = exactRationalParts leftY
-      (rightXNumerator, rightXDenominator) = exactRationalParts rightX
-      (rightYNumerator, rightYDenominator) = exactRationalParts rightY
-   in compare
-        ( leftXNumerator
-            * rightYNumerator
-            * leftYDenominator
-            * rightXDenominator
-        )
-        ( leftYNumerator
-            * rightXNumerator
-            * leftXDenominator
-            * rightYDenominator
-        )
-{-# INLINE exactCrossSign #-}
-
-scaleExactVector :: ExactRational -> ExactVector -> ExactVector
-scaleExactVector scale (ExactVector x y) =
-  ExactVector (scale * x) (scale * y)
-
-translateExactPoint :: ExactPoint -> ExactVector -> ExactPoint
-translateExactPoint (ExactPoint x y) (ExactVector dx dy) =
-  ExactPoint (x + dx) (y + dy)
diff --git a/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs b/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/FloodFillIterator.hs
+++ /dev/null
@@ -1,38 +0,0 @@
--- | Shape queries, face descent, and boundary gluing over an immutable mesh.
-module Moonlight.Triangulation.FloodFillIterator
-  ( DistanceMetric (..)
-  , CircleMetric
-  , CircleMetricError (..)
-  , RectangleMetric
-  , RectangleMetricError (..)
-  , circleMetric
-  , rectangleMetric
-  , edgesInShape
-  , verticesInShape
-  , edgesInCircle
-  , verticesInCircle
-  , edgesInRectangle
-  , verticesInRectangle
-  , floodFillFaces
-  , outerFaceFloodFill
-  , facesAtEvenBarrierDepth
-  , FaceComponent
-  , faceComponentFaces
-  , BoundaryOrientation (..)
-  , BoundaryLoop
-  , boundaryLoopOrientation
-  , boundaryLoopResidentVertices
-  , boundaryLoopVertices
-  , RegionBoundary
-  , regionBoundaryOuterLoop
-  , regionBoundaryHoleLoops
-  , BoundaryObstruction (..)
-  , faceComponents
-  , faceComponentsBy
-  , labelledRegionBoundaries
-  , componentBoundaryLoops
-  , componentBoundary
-  , componentBoundaryForFaces
-  ) where
-
-import Moonlight.Triangulation.Internal.FloodFillIterator
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Dynamic.hs b/src-dcel/Moonlight/Triangulation/Handles/Dynamic.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Dynamic.hs
+++ /dev/null
@@ -1,434 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE EmptyDataDecls #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE KindSignatures #-}
-{-# LANGUAGE RoleAnnotations #-}
-
--- | Owning handles that prevent identifiers from crossing mesh boundaries.
-module Moonlight.Triangulation.Handles.Dynamic
-  ( InnerTag
-  , PossiblyOuterTag
-  , FixedFaceHandle
-  , asPossiblyOuter
-  , fixedFaceId
-  , VertexHandle
-  , DirectedEdgeHandle
-  , UndirectedEdgeHandle
-  , FaceHandle
-  , vertexHandle
-  , directedEdgeHandle
-  , undirectedEdgeHandle
-  , faceHandle
-  , innerFaceHandle
-  , outerFaceHandle
-  , fixVertex
-  , fixDirectedEdge
-  , fixUndirectedEdge
-  , fixFace
-  , vertexHandleData
-  , vertexHandlePosition
-  , vertexHandleOutEdge
-  , vertexHandleOutEdges
-  , directedEdgeDataH
-  , directedEdgeFrom
-  , directedEdgeTo
-  , directedEdgeVertices
-  , directedEdgePositions
-  , directedEdgeReverse
-  , directedEdgeNext
-  , directedEdgePrevious
-  , directedEdgeClockwise
-  , directedEdgeCounterClockwise
-  , directedEdgeFace
-  , directedEdgeAsUndirected
-  , directedEdgeIsOuter
-  , directedEdgeSideQuery
-  , directedEdgeOppositeVertex
-  , directedEdgeOppositePosition
-  , directedEdgeProjectionFactor
-  , directedEdgeNearestPoint
-  , undirectedEdgeDataH
-  , undirectedEdgeAsDirected
-  , undirectedEdgeVertices
-  , undirectedEdgeIsConstraint
-  , undirectedEdgeIsBoundary
-  , faceDataH
-  , faceIsOuter
-  , faceAsInner
-  , faceAdjacentEdge
-  , faceAdjacentEdges
-  , innerFaceVertices
-  , innerFaceCircumcenter
-  , innerFacePositions
-  , innerFaceBarycentric
-  ) where
-
-import Moonlight.Triangulation.Dcel qualified as Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Predicates qualified as Admitted
-import Moonlight.Triangulation.LineSideInfo (LineSideInfo)
-import Moonlight.Triangulation.Math qualified as Math
-import Moonlight.Triangulation.Types
-
--- | A face handle that is statically known not to denote the outer face.
-data InnerTag
-
--- | A face handle that may denote the unique outer face.
-data PossiblyOuterTag
-
-type role FixedFaceHandle nominal
--- | Fixed face identifier refined by whether it may denote the outer face.
-newtype FixedFaceHandle tag = FixedFaceHandle { unFixedFaceHandle :: FaceId }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord)
-
--- | Forget the proof that a fixed face is bounded.
-asPossiblyOuter :: FixedFaceHandle InnerTag -> FixedFaceHandle PossiblyOuterTag
-asPossiblyOuter (FixedFaceHandle face) = FixedFaceHandle face
-{-# INLINE asPossiblyOuter #-}
-
--- | Recover the unrefined face identifier.
-fixedFaceId :: FixedFaceHandle tag -> FaceId
-fixedFaceId (FixedFaceHandle face) = face
-{-# INLINE fixedFaceId #-}
-
--- | Admitted vertex paired with its owning triangulation.
-data VertexHandle mode vertex directed undirected face = VertexHandle
-  !(Triangulation mode vertex directed undirected face)
-  !VertexId
-
--- | Admitted directed edge paired with its owning triangulation.
-data DirectedEdgeHandle mode vertex directed undirected face = DirectedEdgeHandle
-  !(Triangulation mode vertex directed undirected face)
-  !DirectedEdgeId
-
--- | Admitted undirected edge paired with its owning triangulation.
-data UndirectedEdgeHandle mode vertex directed undirected face = UndirectedEdgeHandle
-  !(Triangulation mode vertex directed undirected face)
-  !UndirectedEdgeId
-
--- | Admitted face paired with its owning triangulation and outer-face proof.
-data FaceHandle tag mode vertex directed undirected face = FaceHandle
-  !(Triangulation mode vertex directed undirected face)
-  !(FixedFaceHandle tag)
-
-instance Show (VertexHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixVertex
-
-instance Show (DirectedEdgeHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixDirectedEdge
-
-instance Show (UndirectedEdgeHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixUndirectedEdge
-
-instance Show (FaceHandle tag mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixFace
-
--- | Admit a vertex identifier into a triangulation.
-vertexHandle
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> Maybe (VertexHandle mode vertex directed undirected face)
-vertexHandle triangulation vertex
-  | vertexIdIndex vertex < Dcel.numVertices triangulation = Just (VertexHandle triangulation vertex)
-  | otherwise = Nothing
-
--- | Admit a directed-edge identifier into a triangulation.
-directedEdgeHandle
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedEdgeId
-  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
-directedEdgeHandle triangulation edge
-  | directedEdgeIdIndex edge < Dcel.numDirectedEdges triangulation = Just (DirectedEdgeHandle triangulation edge)
-  | otherwise = Nothing
-
--- | Admit an undirected-edge identifier into a triangulation.
-undirectedEdgeHandle
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> Maybe (UndirectedEdgeHandle mode vertex directed undirected face)
-undirectedEdgeHandle triangulation edge
-  | undirectedEdgeIdIndex edge < Dcel.numUndirectedEdges triangulation = Just (UndirectedEdgeHandle triangulation edge)
-  | otherwise = Nothing
-
--- | Admit a face identifier that may denote the outer face.
-faceHandle
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> Maybe (FaceHandle PossiblyOuterTag mode vertex directed undirected face)
-faceHandle triangulation face
-  | faceIdIndex face < Dcel.numFaces triangulation = Just (FaceHandle triangulation (FixedFaceHandle face))
-  | otherwise = Nothing
-
--- | Admit a face identifier while proving that it is bounded.
-innerFaceHandle
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
-innerFaceHandle triangulation face
-  | face == Dcel.outerFace = Nothing
-  | otherwise = do
-      FaceHandle _ (FixedFaceHandle valid) <- faceHandle triangulation face
-      pure (FaceHandle triangulation (FixedFaceHandle valid))
-
--- | Owning handle to the unique unbounded face.
-outerFaceHandle
-  :: Triangulation mode vertex directed undirected face
-  -> FaceHandle PossiblyOuterTag mode vertex directed undirected face
-outerFaceHandle triangulation = FaceHandle triangulation (FixedFaceHandle Dcel.outerFace)
-
--- | Forget ownership and recover the vertex identifier.
-fixVertex :: VertexHandle mode vertex directed undirected face -> VertexId
-fixVertex (VertexHandle _ vertex) = vertex
-{-# INLINE fixVertex #-}
-
--- | Forget ownership and recover the directed-edge identifier.
-fixDirectedEdge :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeId
-fixDirectedEdge (DirectedEdgeHandle _ edge) = edge
-{-# INLINE fixDirectedEdge #-}
-
--- | Forget ownership and recover the undirected-edge identifier.
-fixUndirectedEdge :: UndirectedEdgeHandle mode vertex directed undirected face -> UndirectedEdgeId
-fixUndirectedEdge (UndirectedEdgeHandle _ edge) = edge
-{-# INLINE fixUndirectedEdge #-}
-
--- | Forget ownership while retaining the outer-face refinement.
-fixFace :: FaceHandle tag mode vertex directed undirected face -> FixedFaceHandle tag
-fixFace (FaceHandle _ face) = face
-{-# INLINE fixFace #-}
-
--- | Vertex annotation through an owning handle.
-vertexHandleData :: VertexHandle mode vertex directed undirected face -> vertex
-vertexHandleData (VertexHandle triangulation vertex) = Dcel.vertexData triangulation vertex
-{-# INLINE vertexHandleData #-}
-
--- | Authoritative vertex position through an owning handle.
-vertexHandlePosition
-  :: VertexHandle mode vertex directed undirected face
-  -> Point
-vertexHandlePosition (VertexHandle triangulation vertex) = (Dcel.vertexPoint triangulation vertex)
-{-# INLINE vertexHandlePosition #-}
-
--- | One outgoing edge of a connected vertex.
-vertexHandleOutEdge
-  :: VertexHandle mode vertex directed undirected face
-  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
-vertexHandleOutEdge (VertexHandle triangulation vertex) = DirectedEdgeHandle triangulation <$> Dcel.vertexOutEdge triangulation vertex
-
--- | Directed edges leaving a vertex in ring order.
-vertexHandleOutEdges
-  :: VertexHandle mode vertex directed undirected face
-  -> [DirectedEdgeHandle mode vertex directed undirected face]
-vertexHandleOutEdges (VertexHandle triangulation vertex) = map (DirectedEdgeHandle triangulation) (Dcel.vertexOutgoingEdges triangulation vertex)
-
--- | Directed-edge annotation through an owning handle.
-directedEdgeDataH :: DirectedEdgeHandle mode vertex directed undirected face -> directed
-directedEdgeDataH (DirectedEdgeHandle triangulation edge) = Dcel.directedEdgeData triangulation edge
-{-# INLINE directedEdgeDataH #-}
-
--- | Origin vertex of an owning directed edge.
-directedEdgeFrom :: DirectedEdgeHandle mode vertex directed undirected face -> VertexHandle mode vertex directed undirected face
-directedEdgeFrom (DirectedEdgeHandle triangulation edge) = VertexHandle triangulation (Dcel.origin triangulation edge)
-{-# INLINE directedEdgeFrom #-}
-
--- | Destination vertex of an owning directed edge.
-directedEdgeTo :: DirectedEdgeHandle mode vertex directed undirected face -> VertexHandle mode vertex directed undirected face
-directedEdgeTo (DirectedEdgeHandle triangulation edge) = VertexHandle triangulation (Dcel.destination triangulation edge)
-{-# INLINE directedEdgeTo #-}
-
--- | Origin and destination of an owning directed edge.
-directedEdgeVertices
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> (VertexHandle mode vertex directed undirected face, VertexHandle mode vertex directed undirected face)
-directedEdgeVertices edge = (directedEdgeFrom edge, directedEdgeTo edge)
-
--- | Origin and destination positions of an owning directed edge.
-directedEdgePositions
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> (Point, Point)
-directedEdgePositions edge = (vertexHandlePosition (directedEdgeFrom edge), vertexHandlePosition (directedEdgeTo edge))
-{-# INLINE directedEdgePositions #-}
-
--- | Reverse an owning directed edge.
-directedEdgeReverse :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
-directedEdgeReverse (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (reverseEdge edge)
-{-# INLINE directedEdgeReverse #-}
-
--- | Next owning edge around the incident face.
-directedEdgeNext :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
-directedEdgeNext (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.next triangulation edge)
-{-# INLINE directedEdgeNext #-}
-
--- | Previous owning edge around the incident face.
-directedEdgePrevious :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
-directedEdgePrevious (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.previous triangulation edge)
-{-# INLINE directedEdgePrevious #-}
-
--- | Previous owning edge around its origin vertex.
-directedEdgeClockwise :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
-directedEdgeClockwise (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.clockwise triangulation edge)
-{-# INLINE directedEdgeClockwise #-}
-
--- | Next owning edge around its origin vertex.
-directedEdgeCounterClockwise :: DirectedEdgeHandle mode vertex directed undirected face -> DirectedEdgeHandle mode vertex directed undirected face
-directedEdgeCounterClockwise (DirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (Dcel.counterClockwise triangulation edge)
-{-# INLINE directedEdgeCounterClockwise #-}
-
--- | Owning handle to the incident face.
-directedEdgeFace
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> FaceHandle PossiblyOuterTag mode vertex directed undirected face
-directedEdgeFace (DirectedEdgeHandle triangulation edge) = FaceHandle triangulation (FixedFaceHandle (Dcel.incidentFace triangulation edge))
-
--- | Forget the orientation of an owning edge.
-directedEdgeAsUndirected
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> UndirectedEdgeHandle mode vertex directed undirected face
-directedEdgeAsUndirected (DirectedEdgeHandle triangulation edge) = UndirectedEdgeHandle triangulation (asUndirected edge)
-
--- | Whether the owning edge is incident to the outer face.
-directedEdgeIsOuter :: DirectedEdgeHandle mode vertex directed undirected face -> Bool
-directedEdgeIsOuter = faceIsOuter . directedEdgeFace
-
--- | Exact side of the owning edge's oriented line.
-directedEdgeSideQuery
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> QueryPoint
-  -> LineSideInfo
-directedEdgeSideQuery edge query =
-  let (from, to) = directedEdgePositions edge
-   in Admitted.sideQuery from to (queryPointValue query)
-
--- | Undirected-edge annotation through an owning handle.
-undirectedEdgeDataH :: UndirectedEdgeHandle mode vertex directed undirected face -> undirected
-undirectedEdgeDataH (UndirectedEdgeHandle triangulation edge) = Dcel.undirectedEdgeData triangulation edge
-{-# INLINE undirectedEdgeDataH #-}
-
--- | Normalized directed orientation of an owning undirected edge.
-undirectedEdgeAsDirected
-  :: UndirectedEdgeHandle mode vertex directed undirected face
-  -> DirectedEdgeHandle mode vertex directed undirected face
-undirectedEdgeAsDirected (UndirectedEdgeHandle triangulation edge) = DirectedEdgeHandle triangulation (normalizedDirected edge)
-
--- | Endpoints of an owning undirected edge.
-undirectedEdgeVertices
-  :: UndirectedEdgeHandle mode vertex directed undirected face
-  -> (VertexHandle mode vertex directed undirected face, VertexHandle mode vertex directed undirected face)
-undirectedEdgeVertices = directedEdgeVertices . undirectedEdgeAsDirected
-
--- | Face annotation through an owning handle.
-faceDataH :: FaceHandle tag mode vertex directed undirected face -> face
-faceDataH (FaceHandle triangulation (FixedFaceHandle face)) = Dcel.faceData triangulation face
-{-# INLINE faceDataH #-}
-
--- | Whether the owning face is the unique unbounded face.
-faceIsOuter :: FaceHandle tag mode vertex directed undirected face -> Bool
-faceIsOuter (FaceHandle _ (FixedFaceHandle face)) = face == Dcel.outerFace
-{-# INLINE faceIsOuter #-}
-
--- | Refine an owning face handle by excluding the outer face.
-faceAsInner
-  :: FaceHandle PossiblyOuterTag mode vertex directed undirected face
-  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
-faceAsInner handle@(FaceHandle triangulation (FixedFaceHandle face))
-  | faceIsOuter handle = Nothing
-  | otherwise = Just (FaceHandle triangulation (FixedFaceHandle face))
-
--- | One owning edge on the face boundary.
-faceAdjacentEdge
-  :: FaceHandle tag mode vertex directed undirected face
-  -> Maybe (DirectedEdgeHandle mode vertex directed undirected face)
-faceAdjacentEdge (FaceHandle triangulation (FixedFaceHandle face)) = DirectedEdgeHandle triangulation <$> Dcel.adjacentEdge triangulation face
-
--- | Owning directed boundary of a face.
-faceAdjacentEdges
-  :: FaceHandle tag mode vertex directed undirected face
-  -> [DirectedEdgeHandle mode vertex directed undirected face]
-faceAdjacentEdges (FaceHandle triangulation (FixedFaceHandle face)) = map (DirectedEdgeHandle triangulation) (Dcel.faceDirectedEdges triangulation face)
-
--- | Three owning vertices of a bounded face.
-innerFaceVertices
-  :: FaceHandle InnerTag mode vertex directed undirected face
-  -> Maybe
-       ( VertexHandle mode vertex directed undirected face
-       , VertexHandle mode vertex directed undirected face
-       , VertexHandle mode vertex directed undirected face
-       )
-innerFaceVertices (FaceHandle triangulation (FixedFaceHandle face)) =
-  (\(a, b, c) -> (VertexHandle triangulation a, VertexHandle triangulation b, VertexHandle triangulation c))
-    <$> Dcel.innerFaceVertices triangulation face
-
--- | Circumcenter of an owning bounded face.
-innerFaceCircumcenter
-  :: FaceHandle InnerTag mode vertex directed undirected face
-  -> Maybe (Point)
-innerFaceCircumcenter face = do
-  (a, b, c) <- innerFaceVertices face
-  Math.circumcenter (vertexHandlePosition a) (vertexHandlePosition b) (vertexHandlePosition c)
-
--- | Vertex opposite an owning directed edge in its bounded incident face.
-directedEdgeOppositeVertex
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> Maybe (VertexHandle mode vertex directed undirected face)
-directedEdgeOppositeVertex (DirectedEdgeHandle triangulation edge)
-  | Dcel.incidentFace triangulation edge == Dcel.outerFace = Nothing
-  | otherwise =
-      Just (VertexHandle triangulation (Dcel.destination triangulation (Dcel.next triangulation edge)))
-
--- | Position opposite an owning directed edge in its bounded incident face.
-directedEdgeOppositePosition
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> Maybe (Point)
-directedEdgeOppositePosition = fmap vertexHandlePosition . directedEdgeOppositeVertex
-{-# INLINE directedEdgeOppositePosition #-}
-
--- | Projection parameter of a point onto an owning directed edge's line.
-directedEdgeProjectionFactor
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> Point
-  -> Double
-directedEdgeProjectionFactor edge query =
-  let (from, to) = directedEdgePositions edge
-   in Math.projectionFactor from to query
-
--- | Nearest point on the closed owning directed edge.
-directedEdgeNearestPoint
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> Point
-  -> Point
-directedEdgeNearestPoint edge query =
-  let (from@(Point ax ay), to@(Point bx by)) = directedEdgePositions edge
-      factor = max 0 (min 1 (Math.projectionFactor from to query))
-   in Point (ax + factor * (bx - ax)) (ay + factor * (by - ay))
-
--- | Whether an owning undirected edge is constrained.
-undirectedEdgeIsConstraint
-  :: UndirectedEdgeHandle mode vertex directed undirected face
-  -> Bool
-undirectedEdgeIsConstraint (UndirectedEdgeHandle triangulation edge) =
-  Dcel.isConstraintEdge triangulation edge
-
--- | Whether an owning undirected edge touches the outer face.
-undirectedEdgeIsBoundary
-  :: UndirectedEdgeHandle mode vertex directed undirected face
-  -> Bool
-undirectedEdgeIsBoundary (UndirectedEdgeHandle triangulation edge) =
-  Dcel.isBoundaryEdge triangulation edge
-
--- | Positions of the three vertices of an owning bounded face.
-innerFacePositions
-  :: FaceHandle InnerTag mode vertex directed undirected face
-  -> Maybe (Point, Point, Point)
-innerFacePositions face = do
-  (a, b, c) <- innerFaceVertices face
-  pure (vertexHandlePosition a, vertexHandlePosition b, vertexHandlePosition c)
-{-# INLINE innerFacePositions #-}
-
--- | Barycentric coordinates in an owning bounded face.
-innerFaceBarycentric
-  :: FaceHandle InnerTag mode vertex directed undirected face
-  -> Point
-  -> Maybe (Double, Double, Double)
-innerFaceBarycentric face query = do
-  (a, b, c) <- innerFacePositions face
-  Math.barycentricCoordinates a b c query
diff --git a/src-dcel/Moonlight/Triangulation/Handles/HandleDefs.hs b/src-dcel/Moonlight/Triangulation/Handles/HandleDefs.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/HandleDefs.hs
+++ /dev/null
@@ -1,22 +0,0 @@
--- | Opaque compact resident indices and lawful orientation operations. The
--- indices do not encode mesh ownership: use "Moonlight.Triangulation.Handles.Scoped"
--- for local traversal or "Moonlight.Triangulation.Handles.Dynamic" when a handle
--- must escape its mesh scope.
-module Moonlight.Triangulation.Handles.HandleDefs
-  ( VertexId
-  , unVertexId
-  , FaceId
-  , unFaceId
-  , DirectedEdgeId
-  , unDirectedEdgeId
-  , UndirectedEdgeId
-  , unUndirectedEdgeId
-  , reverseEdge
-  , asUndirected
-  , normalizedDirected
-  , reversedDirected
-  , directedPair
-  , isNormalized
-  ) where
-
-import Moonlight.Triangulation.Internal.HandleDefs
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Iterators/CircularIterator.hs b/src-dcel/Moonlight/Triangulation/Handles/Iterators/CircularIterator.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Iterators/CircularIterator.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Bounded traversal of a cyclic successor relation.
-module Moonlight.Triangulation.Handles.Iterators.CircularIterator
-  ( circularList
-  , foldCircular'
-  ) where
-
-import GHC.Exts (build)
-
--- | The cycle reached from a start by repeated advance, in visit order.
---
--- Emitted forwards, for the reason 'Moonlight.Triangulation.Dcel.circularWalk'
--- is: accumulating in reverse and reversing at the end builds the ring twice
--- and hands back a list no consumer can fuse with.
-circularList :: Eq a => Int -> (a -> a) -> a -> [a]
-circularList limit advance start =
-  build
-    ( \link stop ->
-        let go !remaining !current !visited
-              | remaining <= 0 = stop
-              | visited && current == start = stop
-              | otherwise = link current (go (remaining - 1) (advance current) True)
-         in go limit start False
-    )
-{-# INLINE circularList #-}
-
--- | Strictly fold a bounded cycle in visit order.
-foldCircular' :: Eq a => Int -> (a -> a) -> a -> (b -> a -> b) -> b -> b
-foldCircular' limit advance start step = go limit start False
- where
-  go !remaining !current !visited !accumulator
-    | remaining <= 0 = accumulator
-    | visited && current == start = accumulator
-    | otherwise = go (remaining - 1) (advance current) True (step accumulator current)
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Iterators/DynamicIterators.hs b/src-dcel/Moonlight/Triangulation/Handles/Iterators/DynamicIterators.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Iterators/DynamicIterators.hs
+++ /dev/null
@@ -1,155 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Whole-mesh traversal through topology-bound dynamic handles.
-module Moonlight.Triangulation.Handles.Iterators.DynamicIterators
-  ( vertexHandles
-  , directedEdgeHandles
-  , undirectedEdgeHandles
-  , allFaceHandles
-  , innerFaceHandles
-  , hullEdgeHandles
-  , hullVertexHandles
-  , foldVertexHandles'
-  , foldDirectedEdgeHandles'
-  , foldUndirectedEdgeHandles'
-  , foldAllFaceHandles'
-  , foldInnerFaceHandles'
-  , foldHullEdgeHandles'
-  , foldHullVertexHandles'
-  ) where
-
-import Moonlight.Triangulation.Handles.Dynamic
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators qualified as Fixed
-import Moonlight.Triangulation.Handles.Iterators.HullIterator qualified as Hull
-import Moonlight.Triangulation.Types
-
--- | Dynamic handles in fixed-index order. The list spine is lazy; use the
--- strict folds below for allocation-free traversal in hot paths.
-vertexHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexHandle mode vertex directed undirected face]
-vertexHandles triangulation =
-  mapValid (vertexHandle triangulation) (Fixed.vertices triangulation)
-
--- | Every directed-edge handle in fixed-index order.
-directedEdgeHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [DirectedEdgeHandle mode vertex directed undirected face]
-directedEdgeHandles triangulation =
-  mapValid (directedEdgeHandle triangulation) (Fixed.directedEdges triangulation)
-
--- | Every undirected-edge handle in fixed-index order.
-undirectedEdgeHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [UndirectedEdgeHandle mode vertex directed undirected face]
-undirectedEdgeHandles triangulation =
-  mapValid (undirectedEdgeHandle triangulation) (Fixed.undirectedEdges triangulation)
-
--- | Every face handle, including the outer face.
-allFaceHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [FaceHandle PossiblyOuterTag mode vertex directed undirected face]
-allFaceHandles triangulation =
-  mapValid (faceHandle triangulation) (Fixed.allFaces triangulation)
-
--- | Every inner-face handle.
-innerFaceHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [FaceHandle InnerTag mode vertex directed undirected face]
-innerFaceHandles triangulation =
-  mapValid (innerFaceHandle triangulation) (Fixed.innerFaces triangulation)
-
--- | Hull directed-edge handles in boundary order.
-hullEdgeHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [DirectedEdgeHandle mode vertex directed undirected face]
-hullEdgeHandles triangulation =
-  mapValid (directedEdgeHandle triangulation) (Hull.hullEdges triangulation)
-
--- | Hull vertex handles in boundary order.
-hullVertexHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexHandle mode vertex directed undirected face]
-hullVertexHandles triangulation =
-  map directedEdgeFrom (hullEdgeHandles triangulation)
-
--- | Strict fold over every vertex handle.
-foldVertexHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> VertexHandle mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldVertexHandles' triangulation step =
-  Fixed.foldVertices' triangulation (applyValid (vertexHandle triangulation) step)
-
--- | Strict fold over every directed-edge handle.
-foldDirectedEdgeHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> DirectedEdgeHandle mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldDirectedEdgeHandles' triangulation step =
-  Fixed.foldDirectedEdges' triangulation (applyValid (directedEdgeHandle triangulation) step)
-
--- | Strict fold over every undirected-edge handle.
-foldUndirectedEdgeHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> UndirectedEdgeHandle mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldUndirectedEdgeHandles' triangulation step =
-  Fixed.foldUndirectedEdges' triangulation (applyValid (undirectedEdgeHandle triangulation) step)
-
--- | Strict fold over every face handle, including the outer face.
-foldAllFaceHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> FaceHandle PossiblyOuterTag mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldAllFaceHandles' triangulation step =
-  Fixed.foldAllFaces' triangulation (applyValid (faceHandle triangulation) step)
-
--- | Strict fold over every inner-face handle.
-foldInnerFaceHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> FaceHandle InnerTag mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldInnerFaceHandles' triangulation step =
-  Fixed.foldInnerFaces' triangulation (applyValid (innerFaceHandle triangulation) step)
-
--- | Strict fold over hull directed-edge handles.
-foldHullEdgeHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> DirectedEdgeHandle mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldHullEdgeHandles' triangulation step =
-  Hull.foldHullEdges' triangulation (applyValid (directedEdgeHandle triangulation) step)
-
--- | Strict fold over hull vertex handles.
-foldHullVertexHandles'
-  :: Triangulation mode vertex directed undirected face
-  -> (accumulator -> VertexHandle mode vertex directed undirected face -> accumulator)
-  -> accumulator
-  -> accumulator
-foldHullVertexHandles' triangulation step =
-  foldHullEdgeHandles' triangulation (\accumulator edge -> step accumulator (directedEdgeFrom edge))
-
-mapValid :: (fixed -> Maybe dynamic) -> [fixed] -> [dynamic]
-mapValid make = foldr collect []
- where
-  collect fixed rest = case make fixed of
-    Just dynamic -> dynamic : rest
-    Nothing -> rest
-
-applyValid
-  :: (fixed -> Maybe dynamic)
-  -> (accumulator -> dynamic -> accumulator)
-  -> accumulator
-  -> fixed
-  -> accumulator
-applyValid make step !accumulator fixed = case make fixed of
-  Just dynamic -> step accumulator dynamic
-  Nothing -> accumulator
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Iterators/FixedIterators.hs b/src-dcel/Moonlight/Triangulation/Handles/Iterators/FixedIterators.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Iterators/FixedIterators.hs
+++ /dev/null
@@ -1,87 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Whole-mesh traversal by identifier range, each range paired with the
--- strict fold that consumes it.
-module Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( vertices
-  , directedEdges
-  , undirectedEdges
-  , allFaces
-  , innerFaces
-  , foldVertices'
-  , foldDirectedEdges'
-  , foldUndirectedEdges'
-  , foldAllFaces'
-  , foldInnerFaces'
-  ) where
-
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Types
-
--- | Every vertex identifier, ascending.
---
--- The enumerators are producers, so their bodies have to reach the consumer's
--- module: a fold over one of these should be a loop over an index, not a walk
--- over cons cells built by a call it could not see into.
-vertices :: Triangulation mode vertex directed undirected face -> [VertexId]
-vertices triangulation = [VertexId (fromIntegral index) | index <- [0 .. numVertices triangulation - 1]]
-{-# INLINE vertices #-}
-
--- | Every directed-edge identifier, ascending.
-directedEdges :: Triangulation mode vertex directed undirected face -> [DirectedEdgeId]
-directedEdges triangulation = [DirectedEdgeId (fromIntegral index) | index <- [0 .. numDirectedEdges triangulation - 1]]
-{-# INLINE directedEdges #-}
-
--- | Every undirected-edge identifier, ascending.
-undirectedEdges :: Triangulation mode vertex directed undirected face -> [UndirectedEdgeId]
-undirectedEdges triangulation = [UndirectedEdgeId (fromIntegral index) | index <- [0 .. numUndirectedEdges triangulation - 1]]
-{-# INLINE undirectedEdges #-}
-
--- | Every face identifier, the outer face first.
-allFaces :: Triangulation mode vertex directed undirected face -> [FaceId]
-allFaces triangulation = [FaceId (fromIntegral index) | index <- [0 .. numFaces triangulation - 1]]
-{-# INLINE allFaces #-}
-
--- | Every face identifier except the outer face.
-innerFaces :: Triangulation mode vertex directed undirected face -> [FaceId]
-innerFaces triangulation = [FaceId (fromIntegral index) | index <- [1 .. numFaces triangulation - 1]]
-{-# INLINE innerFaces #-}
-
--- | Strict fold over 'vertices'; the rest of the family follows.
-foldVertices' :: Triangulation mode vertex directed undirected face -> (a -> VertexId -> a) -> a -> a
-foldVertices' triangulation step = foldRange (numVertices triangulation) (VertexId . fromIntegral) step
-{-# INLINE foldVertices' #-}
-
--- | Strict fold over every directed-edge identifier.
-foldDirectedEdges' :: Triangulation mode vertex directed undirected face -> (a -> DirectedEdgeId -> a) -> a -> a
-foldDirectedEdges' triangulation step = foldRange (numDirectedEdges triangulation) (DirectedEdgeId . fromIntegral) step
-{-# INLINE foldDirectedEdges' #-}
-
--- | Strict fold over every undirected-edge identifier.
-foldUndirectedEdges' :: Triangulation mode vertex directed undirected face -> (a -> UndirectedEdgeId -> a) -> a -> a
-foldUndirectedEdges' triangulation step = foldRange (numUndirectedEdges triangulation) (UndirectedEdgeId . fromIntegral) step
-{-# INLINE foldUndirectedEdges' #-}
-
--- | Strict fold over every face, including the outer face.
-foldAllFaces' :: Triangulation mode vertex directed undirected face -> (a -> FaceId -> a) -> a -> a
-foldAllFaces' triangulation step = foldRange (numFaces triangulation) (FaceId . fromIntegral) step
-{-# INLINE foldAllFaces' #-}
-
--- | Strict fold over every inner face.
-foldInnerFaces' :: Triangulation mode vertex directed undirected face -> (a -> FaceId -> a) -> a -> a
-foldInnerFaces' triangulation step initial = go 1 initial
- where
-  !end = numFaces triangulation
-  go !index !accumulator
-    | index >= end = accumulator
-    | otherwise = go (index + 1) (step accumulator (FaceId (fromIntegral index)))
-{-# INLINE foldInnerFaces' #-}
-
-foldRange :: Int -> (Int -> b) -> (a -> b -> a) -> a -> a
-foldRange end make step = go 0
- where
-  go !index !accumulator
-    | index >= end = accumulator
-    | otherwise = go (index + 1) (step accumulator (make index))
-{-# INLINE foldRange #-}
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Iterators/HullIterator.hs b/src-dcel/Moonlight/Triangulation/Handles/Iterators/HullIterator.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Iterators/HullIterator.hs
+++ /dev/null
@@ -1,29 +0,0 @@
--- | The convex hull, walked as its edges or as its vertices.
-module Moonlight.Triangulation.Handles.Iterators.HullIterator
-  ( hullEdges
-  , hullVertices
-  , foldHullEdges'
-  , foldHullVertices'
-  ) where
-
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Types
-
--- | The convex hull as directed edges, in order.
-hullEdges :: Triangulation mode vertex directed undirected face -> [DirectedEdgeId]
-hullEdges triangulation = faceDirectedEdges triangulation outerFace
-{-# INLINE hullEdges #-}
-
--- | The convex hull as vertices, in order.
-hullVertices :: Triangulation mode vertex directed undirected face -> [VertexId]
-hullVertices triangulation = map (origin triangulation) (hullEdges triangulation)
-{-# INLINE hullVertices #-}
-
--- | Strict fold over 'hullEdges'.
-foldHullEdges' :: Triangulation mode vertex directed undirected face -> (a -> DirectedEdgeId -> a) -> a -> a
-foldHullEdges' triangulation = foldFaceDirectedEdges' triangulation outerFace
-
--- | Strict fold over 'hullVertices'.
-foldHullVertices' :: Triangulation mode vertex directed undirected face -> (a -> VertexId -> a) -> a -> a
-foldHullVertices' triangulation step = foldHullEdges' triangulation (\acc edge -> step acc (origin triangulation edge))
diff --git a/src-dcel/Moonlight/Triangulation/Handles/Scoped.hs b/src-dcel/Moonlight/Triangulation/Handles/Scoped.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Handles/Scoped.hs
+++ /dev/null
@@ -1,276 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE RoleAnnotations #-}
-
--- | Zero-cost mesh-scoped identifiers. A scope is introduced only by
--- 'withScopedTriangulation', so an identifier from one mesh cannot be supplied
--- to observations of another mesh. Use owning handles when a handle must
--- escape the continuation.
-module Moonlight.Triangulation.Handles.Scoped
-  ( ScopedTriangulation
-  , ScopedVertex
-  , ScopedDirectedEdge
-  , ScopedUndirectedEdge
-  , ScopedFace
-  , withScopedTriangulation
-  , scopedVertices
-  , scopedDirectedEdges
-  , scopedUndirectedEdges
-  , scopedFaces
-  , scopedInnerFaces
-  , scopedOuterFace
-  , scopedVertexPoint
-  , scopedVertexData
-  , scopedDirectedEdgeData
-  , scopedUndirectedEdgeData
-  , scopedFaceData
-  , scopedVertexOutEdge
-  , scopedAdjacentEdge
-  , scopedOrigin
-  , scopedDestination
-  , scopedReverseEdge
-  , scopedAsUndirected
-  , scopedNext
-  , scopedPrevious
-  , scopedIncidentFace
-  , scopedIsOuterDirectedEdge
-  , scopedIsBoundaryEdge
-  , scopedIsConstraintEdge
-  , scopedUndirectedEndpoints
-  , scopedFaceDirectedEdges
-  , scopedFaceVertices
-  , scopedVertexOutgoingEdges
-  ) where
-
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  , asUndirected
-  , reverseEdge
-  )
-import qualified Moonlight.Triangulation.Handles.Iterators.FixedIterators as Fixed
-import Moonlight.Triangulation.Types
-  ( Point
-  , Triangulation
-  )
-
-type role ScopedTriangulation nominal nominal representational representational representational representational
-newtype ScopedTriangulation scope mode vertex directed undirected face =
-  ScopedTriangulation (Triangulation mode vertex directed undirected face)
-
-type role ScopedVertex nominal
-newtype ScopedVertex scope = ScopedVertex VertexId
-  deriving stock (Eq, Ord, Show)
-
-type role ScopedDirectedEdge nominal
-newtype ScopedDirectedEdge scope = ScopedDirectedEdge DirectedEdgeId
-  deriving stock (Eq, Ord, Show)
-
-type role ScopedUndirectedEdge nominal
-newtype ScopedUndirectedEdge scope = ScopedUndirectedEdge UndirectedEdgeId
-  deriving stock (Eq, Ord, Show)
-
-type role ScopedFace nominal
-newtype ScopedFace scope = ScopedFace FaceId
-  deriving stock (Eq, Ord, Show)
-
--- | Introduce one nominal scope for all identifiers derived from this mesh.
-withScopedTriangulation
-  :: Triangulation mode vertex directed undirected face
-  -> (forall scope. ScopedTriangulation scope mode vertex directed undirected face -> result)
-  -> result
-withScopedTriangulation triangulation use = use (ScopedTriangulation triangulation)
-{-# INLINE withScopedTriangulation #-}
-
-scopedVertices
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> [ScopedVertex scope]
-scopedVertices (ScopedTriangulation triangulation) =
-  fmap ScopedVertex (Fixed.vertices triangulation)
-
-scopedDirectedEdges
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> [ScopedDirectedEdge scope]
-scopedDirectedEdges (ScopedTriangulation triangulation) =
-  fmap ScopedDirectedEdge (Fixed.directedEdges triangulation)
-
-scopedUndirectedEdges
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> [ScopedUndirectedEdge scope]
-scopedUndirectedEdges (ScopedTriangulation triangulation) =
-  fmap ScopedUndirectedEdge (Fixed.undirectedEdges triangulation)
-
-scopedFaces
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> [ScopedFace scope]
-scopedFaces (ScopedTriangulation triangulation) =
-  fmap ScopedFace (Fixed.allFaces triangulation)
-
-scopedInnerFaces
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> [ScopedFace scope]
-scopedInnerFaces (ScopedTriangulation triangulation) =
-  fmap ScopedFace (Fixed.innerFaces triangulation)
-
-scopedOuterFace
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedFace scope
-scopedOuterFace _ = ScopedFace Dcel.outerFace
-{-# INLINE scopedOuterFace #-}
-
-scopedVertexPoint
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedVertex scope
-  -> Point
-scopedVertexPoint (ScopedTriangulation triangulation) (ScopedVertex vertex) =
-  Dcel.vertexPoint triangulation vertex
-{-# INLINE scopedVertexPoint #-}
-
-scopedVertexData
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedVertex scope
-  -> vertex
-scopedVertexData (ScopedTriangulation triangulation) (ScopedVertex vertex) =
-  Dcel.vertexData triangulation vertex
-{-# INLINE scopedVertexData #-}
-
-scopedDirectedEdgeData
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> directed
-scopedDirectedEdgeData (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  Dcel.directedEdgeData triangulation edge
-{-# INLINE scopedDirectedEdgeData #-}
-
-scopedUndirectedEdgeData
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedUndirectedEdge scope
-  -> undirected
-scopedUndirectedEdgeData (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
-  Dcel.undirectedEdgeData triangulation edge
-{-# INLINE scopedUndirectedEdgeData #-}
-
-scopedFaceData
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedFace scope
-  -> face
-scopedFaceData (ScopedTriangulation triangulation) (ScopedFace face) =
-  Dcel.faceData triangulation face
-{-# INLINE scopedFaceData #-}
-
-scopedVertexOutEdge
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedVertex scope
-  -> Maybe (ScopedDirectedEdge scope)
-scopedVertexOutEdge (ScopedTriangulation triangulation) (ScopedVertex vertex) =
-  ScopedDirectedEdge <$> Dcel.vertexOutEdge triangulation vertex
-
-scopedAdjacentEdge
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedFace scope
-  -> Maybe (ScopedDirectedEdge scope)
-scopedAdjacentEdge (ScopedTriangulation triangulation) (ScopedFace face) =
-  ScopedDirectedEdge <$> Dcel.adjacentEdge triangulation face
-
-scopedOrigin
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> ScopedVertex scope
-scopedOrigin (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  ScopedVertex (Dcel.origin triangulation edge)
-{-# INLINE scopedOrigin #-}
-
-scopedDestination
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> ScopedVertex scope
-scopedDestination (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  ScopedVertex (Dcel.destination triangulation edge)
-{-# INLINE scopedDestination #-}
-
-scopedReverseEdge :: ScopedDirectedEdge scope -> ScopedDirectedEdge scope
-scopedReverseEdge (ScopedDirectedEdge edge) = ScopedDirectedEdge (reverseEdge edge)
-{-# INLINE scopedReverseEdge #-}
-
-scopedAsUndirected :: ScopedDirectedEdge scope -> ScopedUndirectedEdge scope
-scopedAsUndirected (ScopedDirectedEdge edge) = ScopedUndirectedEdge (asUndirected edge)
-{-# INLINE scopedAsUndirected #-}
-
-scopedNext
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> ScopedDirectedEdge scope
-scopedNext (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  ScopedDirectedEdge (Dcel.next triangulation edge)
-{-# INLINE scopedNext #-}
-
-scopedPrevious
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> ScopedDirectedEdge scope
-scopedPrevious (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  ScopedDirectedEdge (Dcel.previous triangulation edge)
-{-# INLINE scopedPrevious #-}
-
-scopedIncidentFace
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> ScopedFace scope
-scopedIncidentFace (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  ScopedFace (Dcel.incidentFace triangulation edge)
-{-# INLINE scopedIncidentFace #-}
-
-scopedIsOuterDirectedEdge
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedDirectedEdge scope
-  -> Bool
-scopedIsOuterDirectedEdge (ScopedTriangulation triangulation) (ScopedDirectedEdge edge) =
-  Dcel.isOuterDirectedEdge triangulation edge
-{-# INLINE scopedIsOuterDirectedEdge #-}
-
-scopedIsBoundaryEdge
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedUndirectedEdge scope
-  -> Bool
-scopedIsBoundaryEdge (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
-  Dcel.isBoundaryEdge triangulation edge
-{-# INLINE scopedIsBoundaryEdge #-}
-
-scopedIsConstraintEdge
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedUndirectedEdge scope
-  -> Bool
-scopedIsConstraintEdge (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
-  Dcel.isConstraintEdge triangulation edge
-{-# INLINE scopedIsConstraintEdge #-}
-
-scopedUndirectedEndpoints
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedUndirectedEdge scope
-  -> (ScopedVertex scope, ScopedVertex scope)
-scopedUndirectedEndpoints (ScopedTriangulation triangulation) (ScopedUndirectedEdge edge) =
-  let (firstVertex, secondVertex) = Dcel.undirectedEndpoints triangulation edge
-   in (ScopedVertex firstVertex, ScopedVertex secondVertex)
-
-scopedFaceDirectedEdges
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedFace scope
-  -> [ScopedDirectedEdge scope]
-scopedFaceDirectedEdges (ScopedTriangulation triangulation) (ScopedFace face) =
-  fmap ScopedDirectedEdge (Dcel.faceDirectedEdges triangulation face)
-
-scopedFaceVertices
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedFace scope
-  -> [ScopedVertex scope]
-scopedFaceVertices (ScopedTriangulation triangulation) (ScopedFace face) =
-  fmap ScopedVertex (Dcel.faceVertices triangulation face)
-
-scopedVertexOutgoingEdges
-  :: ScopedTriangulation scope mode vertex directed undirected face
-  -> ScopedVertex scope
-  -> [ScopedDirectedEdge scope]
-scopedVertexOutgoingEdges (ScopedTriangulation triangulation) (ScopedVertex vertex) =
-  fmap ScopedDirectedEdge (Dcel.vertexOutgoingEdges triangulation vertex)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/BoundaryCycle.hs b/src-dcel/Moonlight/Triangulation/Internal/BoundaryCycle.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/BoundaryCycle.hs
+++ /dev/null
@@ -1,303 +0,0 @@
--- | Shared pure algebra for descending oriented boundary graphs into simple
--- cycles, then simplifying and classifying those cycles.
-module Moonlight.Triangulation.Internal.BoundaryCycle
-  ( traceOrientedBoundaryCircuits
-  , simplifyBoundaryCycle
-  , rotateCycleLeast
-  , rotateCycleLeastBy
-  , consecutivePairs
-  , unorderedPairs
-  , orderedPair
-  , cyclePairs
-  , cyclePairsNonEmpty
-  , cyclicTriples
-  , firstNonCounterClockwiseTurn
-  , admitsSimpleCycleEdgeRelation
-  ) where
-
-import Data.List (tails)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Moonlight.Triangulation.Internal.SegmentRelation
-  ( SegmentRelation (..)
-  )
-
--- | Consume every supplied oriented edge exactly once and split point contacts
--- into vertex-simple cycles. The outgoing lists carry the caller's required
--- local angular or identifier order. A malformed graph is translated directly
--- through the caller's obstruction constructor; this shared worker owns no
--- disposable error vocabulary.
-traceOrientedBoundaryCircuits
-  :: (Ord vertex, Ord edge)
-  => (edge -> vertex)
-  -> (edge -> vertex)
-  -> (edge -> edge -> obstruction)
-  -> Map vertex [edge]
-  -> Set edge
-  -> Either obstruction [NonEmpty vertex]
-traceOrientedBoundaryCircuits edgeOrigin edgeDestination obstruction outgoing edges =
-  descend edges []
- where
-  descend remaining cycles =
-    case Set.lookupMin remaining of
-      Nothing -> Right (reverse cycles)
-      Just seed ->
-        let (untraced, circuitEdges) =
-              eulerCircuit remaining (edgeOrigin seed)
-         in case NonEmpty.nonEmpty circuitEdges of
-              Nothing -> Left (obstruction seed seed)
-              Just circuit ->
-                case splitCircuit circuit of
-                  Left failedEdge -> Left (obstruction seed failedEdge)
-                  Right splitCycles ->
-                    descend
-                      untraced
-                      (reverse (NonEmpty.toList splitCycles) <> cycles)
-
-  -- Hierholzer descent: delete each chosen edge, then prepend it while
-  -- backtracking. No graph-state stream or branch search is materialized.
-  eulerCircuit remaining start = walk remaining start [] []
-   where
-    walk untraced current incomingEdges circuit =
-      case
-          List.find
-            (`Set.member` untraced)
-            (Map.findWithDefault [] current outgoing) of
-        Just edge ->
-          walk
-            (Set.delete edge untraced)
-            (edgeDestination edge)
-            (edge : incomingEdges)
-            circuit
-        Nothing ->
-          case incomingEdges of
-            edge : previousEdges ->
-              walk
-                untraced
-                (edgeOrigin edge)
-                previousEdges
-                (edge : circuit)
-            [] -> (untraced, circuit)
-
-  -- The resident path stays simple. Closing at a resident vertex emits and
-  -- removes exactly that suffix, partitioning the Euler circuit into cycles.
-  splitCircuit circuit =
-    walk
-      start
-      0
-      (Map.singleton start 0)
-      []
-      []
-      (NonEmpty.toList circuit)
-   where
-    start = edgeOrigin (NonEmpty.head circuit)
-
-    walk current depth depths reversedEdges cycles remaining =
-      case remaining of
-        [] ->
-          case
-              ( depth
-              , reversedEdges
-              , NonEmpty.nonEmpty (reverse cycles)
-              ) of
-            (0, [], Just simpleCycles) -> Right simpleCycles
-            _ -> Left (NonEmpty.last circuit)
-        edge : rest
-          | edgeOrigin edge /= current -> Left edge
-          | otherwise ->
-              let target = edgeDestination edge
-               in case Map.lookup target depths of
-                    Nothing ->
-                      walk
-                        target
-                        (depth + 1)
-                        (Map.insert target (depth + 1) depths)
-                        (edge : reversedEdges)
-                        cycles
-                        rest
-                    Just repeatedDepth ->
-                      let suffixLength = depth - repeatedDepth
-                          suffixEdges = take suffixLength reversedEdges
-                          simpleCycle =
-                            fmap edgeOrigin
-                              ( NonEmpty.reverse
-                                  (edge :| suffixEdges)
-                              )
-                       in walk
-                            target
-                            repeatedDepth
-                            ( foldr
-                                (Map.delete . edgeDestination)
-                                depths
-                                suffixEdges
-                            )
-                            (drop suffixLength reversedEdges)
-                            (simpleCycle : cycles)
-                            rest
-{-# INLINABLE traceOrientedBoundaryCircuits #-}
-
--- | Remove precisely the vertices admitted by @isRedundant@ until a fixed
--- point is reached, then classify the winding at the least keyed retained
--- vertex. The returned cycle preserves the tracer's start; publication layers
--- may rotate their value-level observation independently. The caller supplies
--- its obstruction constructor so the shared worker does not allocate a
--- disposable intermediate error vocabulary at either specialization.
-simplifyBoundaryCycle
-  :: (Eq value, Ord key)
-  => ([value] -> obstruction)
-  -> (value -> value -> value -> Bool)
-  -> (value -> value -> value -> Ordering)
-  -> (value -> key)
-  -> [value]
-  -> Either obstruction (Ordering, NonEmpty value)
-simplifyBoundaryCycle obstruction isRedundant orientation key = descend
- where
-  descend values@(_ : _ : _ : _) =
-    let triples = cyclicTriples values
-        retained =
-          [ current
-          | (previousValue, current, nextValue) <- triples
-          , not (isRedundant previousValue current nextValue)
-          ]
-     in if retained == values
-          then classify values triples
-          else descend retained
-  descend values = Left (obstruction values)
-
-  classify values triples =
-    case triples of
-      [] -> Left (obstruction values)
-      firstTriple : remainingTriples ->
-        let (previousValue, current, nextValue) =
-              List.foldl' chooseLeast firstTriple remainingTriples
-            winding = orientation previousValue current nextValue
-         in case (winding, values) of
-              (EQ, _) -> Left (obstruction values)
-              (_, initialValue : rest) -> Right (winding, initialValue :| rest)
-              _ -> Left (obstruction values)
-
-  chooseLeast selected@(_, selectedValue, _) candidate@(_, candidateValue, _)
-    | key candidateValue < key selectedValue = candidate
-    | otherwise = selected
-{-# INLINE simplifyBoundaryCycle #-}
-
--- | Choose the least value as a cycle's observational origin without changing
--- its orientation. Boundary publication and generated convex geometry share
--- this one canonical rotation owner.
-rotateCycleLeast :: Ord value => NonEmpty value -> NonEmpty value
-rotateCycleLeast = rotateCycleLeastBy id
-{-# INLINE rotateCycleLeast #-}
-
--- | Choose the least keyed value as a cycle's observational origin.
-rotateCycleLeastBy
-  :: Ord key
-  => (value -> key)
-  -> NonEmpty value
-  -> NonEmpty value
-rotateCycleLeastBy key values =
-  case break ((== minimumKey) . key) asList of
-    (before, selected : after) -> selected :| (after <> before)
-    _ -> values
- where
-  asList = NonEmpty.toList values
-  minimumKey =
-    List.foldl'
-      (\selected candidate -> min selected (key candidate))
-      (key (NonEmpty.head values))
-      (NonEmpty.tail values)
-{-# INLINE rotateCycleLeastBy #-}
-
--- | Every adjacent pair in a linear sequence.
-consecutivePairs :: [value] -> [(value, value)]
-consecutivePairs values = zip values (drop 1 values)
-{-# INLINE consecutivePairs #-}
-
--- | Every unordered pair exactly once.
-unorderedPairs :: [value] -> [(value, value)]
-unorderedPairs values =
-  [(left, right) | left : remaining <- tails values, right <- remaining]
-{-# INLINE unorderedPairs #-}
-
--- | Canonically orient an unordered pair.
-orderedPair :: Ord value => value -> value -> (value, value)
-orderedPair left right
-  | left <= right = (left, right)
-  | otherwise = (right, left)
-{-# INLINE orderedPair #-}
-
--- | Every directed edge of a non-empty cycle in cycle order.
-cyclePairs :: NonEmpty value -> [(value, value)]
-cyclePairs = NonEmpty.toList . cyclePairsNonEmpty
-{-# INLINE cyclePairs #-}
-
--- | The non-empty form of 'cyclePairs'. A singleton cycle has its sole value
--- as both ends of its sole cyclic edge.
-cyclePairsNonEmpty :: NonEmpty value -> NonEmpty (value, value)
-cyclePairsNonEmpty values@(firstValue :| remaining) =
-  NonEmpty.zip values successors
- where
-  successors =
-    case remaining of
-      [] -> firstValue :| []
-      nextValue : rest -> nextValue :| (rest <> [firstValue])
-{-# INLINE cyclePairsNonEmpty #-}
-
--- | Consecutive cyclic triples, one centered at every value.
-cyclicTriples :: [value] -> [(value, value, value)]
-cyclicTriples values =
-  case values of
-    initial : second : remaining ->
-      let final = List.foldl' (\_ current -> current) initial (second : remaining)
-       in zip3
-            (final : values)
-            values
-            (second : remaining <> [initial])
-    _ -> []
-{-# INLINE cyclicTriples #-}
-
--- | Locate the first turn that is not strictly counter-clockwise. Convex
--- boundary admissions share this one indexed obstruction descent.
-firstNonCounterClockwiseTurn
-  :: (value -> value -> value -> Ordering)
-  -> NonEmpty value
-  -> Maybe (Int, Ordering)
-firstNonCounterClockwiseTurn orientation =
-  List.find ((/= GT) . snd)
-    . zip [0 ..]
-    . fmap (\(previousValue, current, nextValue) -> orientation previousValue current nextValue)
-    . cyclicTriples
-    . NonEmpty.toList
-{-# INLINE firstNonCounterClockwiseTurn #-}
-
--- | Whether one exact relation between two indexed cycle edges is compatible
--- with a simple closed boundary. Only adjacent edges may meet, and then only
--- at their shared endpoint. Producers remain free to obtain relation sections
--- through the sweep plan or direct pairwise descent; this is the single
--- admissibility policy glued over both representations.
-admitsSimpleCycleEdgeRelation
-  :: Int
-  -> Int
-  -> Int
-  -> SegmentRelation
-  -> Bool
-admitsSimpleCycleEdgeRelation segmentCount leftIndex rightIndex relation =
-  relation == SegmentsDisjoint
-    || ( cycleEdgesAreAdjacent
-           segmentCount leftIndex rightIndex
-           && relation == SegmentsShareEndpoint
-       )
-{-# INLINE admitsSimpleCycleEdgeRelation #-}
-
-cycleEdgesAreAdjacent :: Int -> Int -> Int -> Bool
-cycleEdgesAreAdjacent segmentCount leftIndex rightIndex =
-  upperIndex == lowerIndex + 1
-    || (lowerIndex == 0 && upperIndex == segmentCount - 1)
- where
-  lowerIndex = min leftIndex rightIndex
-  upperIndex = max leftIndex rightIndex
-{-# INLINE cycleEdgesAreAdjacent #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs b/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Canonical.hs
+++ /dev/null
@@ -1,336 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Canonical publication: the same triangulation, renumbered so that its
--- representation is a function of its geometry alone.
-module Moonlight.Triangulation.Internal.Canonical
-  ( canonicalize
-  , canonicalizeKnownAscending
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (runST)
-import qualified Data.Vector.Algorithms.Intro as Intro
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Dcel (numFaces, numUndirectedEdges, numVertices, vertexData)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Internal.Mutable
-import Moonlight.Triangulation.Internal.Paged (Paged, pagedUnsafeIndex)
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
-import Moonlight.Triangulation.Internal.Types (BuildError)
-
--- | Whether the caller has already established the authoritative vertex-order
--- section. The canonicalizer still owns every edge, directed-edge, face,
--- anchor, and constraint normalization; this witness affects only its vertex
--- rank entrance.
-data VertexOrderAdmission
-  = VertexOrderUnknown
-  | VertexOrderKnownAscending
-
--- | Renumber a triangulation into its canonical representation.
---
--- Two triangulations of the same sites are the same triangulation — Delaunay
--- uniqueness says so, and the tie-break on an exactly cocircular quadrilateral
--- is keyed on coordinates rather than on identifiers so that it stays true.
--- What differs between two builds of one site set is only /numbering/: which
--- vertex got index 0, which half-edge got the even slot, where a face's cycle
--- was anchored. All of that records the schedule the value was constructed by,
--- and none of it is geometry.
---
--- This is what removes it. Every identifier is assigned from the geometry:
---
---     * vertices in lexicographic coordinate rank;
---     * undirected edges in lexicographic rank of their endpoint pair, each
---       taken low first, so the pair is an unordered pair by construction;
---     * of a pair's two half-edges, the even slot is the one leaving the
---       lower-ranked endpoint, which keeps twinning an @xor@ with one;
---     * inner faces in order of the least half-edge on their boundary, which
---       is a single index rather than a vertex tuple and so needs no special
---       case for a cycle that is not a triangle;
---     * every anchor — each vertex's outgoing edge, each face's edge — set to
---       the least admissible half-edge.
---
--- Construction therefore need not run in canonical order. Local insertion and
--- seam fusion may preserve their cheaper schedule-specific numbering; callers
--- invoke this operation only when they require the construction-independent
--- physical representative used to observe the finite-set laws.
---
--- Element payloads are @()@ because a renumbering is a bijection and could
--- carry them, but nothing that wants that exists; the writers a general form
--- would need were retired when their last caller went. Vertex payloads travel
--- with their vertices, and constraint flags with their edges, so a constrained
--- triangulation canonicalizes as readily as an unconstrained one.
-canonicalize
-  :: Triangulation mode vertex () () ()
-  -> Either BuildError (Triangulation mode vertex () () ())
-canonicalize = canonicalizeWithVertexOrder VertexOrderUnknown
-
--- | Canonicalize a triangulation whose vertices were appended in strict
--- lexicographic coordinate order by the admitted SiteSet bulk entrance.
--- This is an execution witness, not a second numbering implementation.
-canonicalizeKnownAscending
-  :: Triangulation mode vertex () () ()
-  -> Either BuildError (Triangulation mode vertex () () ())
-canonicalizeKnownAscending = canonicalizeWithVertexOrder VertexOrderKnownAscending
-
-canonicalizeWithVertexOrder
-  :: VertexOrderAdmission
-  -> Triangulation mode vertex () () ()
-  -> Either BuildError (Triangulation mode vertex () () ())
-canonicalizeWithVertexOrder vertexOrderAdmission source = runST $ do
-  mutable <-
-    newMutableDcel
-      (triElementDefaults source)
-      (exactDcelCapacity vertexTotal directedTotal faceTotal)
-  forRange 0 vertexTotal $ \canonical -> do
-    let !old = vertexOrder `U.unsafeIndex` canonical
-    _ <-
-      appendVertexCoordinates
-        mutable
-        (coordinateX `pagedUnsafeIndex` old)
-        (coordinateY `pagedUnsafeIndex` old)
-        (vertexData source (VertexId (fromIntegral old)))
-    pure ()
-  _ <- addEdgeBlock mutable edgeTotal
-  _ <- addFaceBlock mutable (faceTotal - 1)
-  forRange 0 directedTotal $ \canonical -> do
-    let !old = directedFromCanonical `U.unsafeIndex` canonical
-    writeOrigin mutable canonical (vertexRank `U.unsafeIndex` originOf old)
-    writeNext mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 1))
-    writePrevious mutable canonical (directedToCanonical `U.unsafeIndex` topology (4 * old + 2))
-    writeFace mutable canonical (faceRank `U.unsafeIndex` topology (4 * old + 3))
-  forRange 0 vertexTotal $ \canonical -> do
-    let !least = leastOutgoing `U.unsafeIndex` canonical
-    if least == absent
-      then writeVertexOut mutable canonical (-1)
-      else markConnected mutable canonical least
-  forRange 0 faceTotal $ \canonical -> do
-    let !least = leastOnFace `U.unsafeIndex` oldFaceOf canonical
-    writeFaceEdge mutable canonical (if least == absent then -1 else least)
-  forRange 0 edgeTotal $ \canonicalUndirected ->
-    when (constraintFlag `pagedUnsafeIndex` (edgeOrder `U.unsafeIndex` canonicalUndirected) /= 0) $
-      () <$ setConstraint mutable (2 * canonicalUndirected)
-  freezeTriangulation mutable
- where
-  !vertexTotal = numVertices source
-  !edgeTotal = numUndirectedEdges source
-  !faceTotal = numFaces source
-  !directedTotal = 2 * edgeTotal
-
-  -- Read the arenas rather than the handle accessors. Every one of these is
-  -- indexed a few times per element, and the accessors would box a t'Point' or
-  -- an identifier newtype at each of them.
-  !coordinateX = triPointX source
-  !coordinateY = triPointY source
-  !topologyArena = triHalfTopology source
-  !constraintFlag = triConstraint source
-  topology slot = fromIntegral (topologyArena `pagedUnsafeIndex` slot) :: Int
-  originOf directed = topology (4 * directed)
-
-  -- Vertices, in lexicographic coordinate rank. Sorting a vector of keys with
-  -- the type's own ordering rather than an index vector under a closure: the
-  -- comparison then specializes instead of being an unknown call per step.
-  --
-  -- Sortedness is checked first, because a great many of the meshes handed to
-  -- this function already have it and the check costs one linear scan against a
-  -- sort's @n log n@. A seam merge is the reason: it copies two canonically
-  -- numbered operands into one arena, lower abscissa first, and the sites of
-  -- the result are then already in rank order by construction. Nothing about
-  -- the schedule is assumed here — the coordinates are simply read and
-  -- believed, so a mesh that arrives sorted for any other reason is served just
-  -- as well.
-  !alreadyRanked =
-    case vertexOrderAdmission of
-      VertexOrderUnknown -> coordinatesAscend vertexTotal coordinateX coordinateY
-      VertexOrderKnownAscending -> True
-  !vertexOrder
-    | alreadyRanked = U.enumFromN 0 vertexTotal
-    | otherwise =
-        thirdColumn $
-          sortedVector
-            ( U.generate
-                vertexTotal
-                ( \index ->
-                    ( coordinateX `pagedUnsafeIndex` index
-                    , coordinateY `pagedUnsafeIndex` index
-                    , index
-                    )
-                )
-            )
-  !vertexRank
-    | alreadyRanked = vertexOrder
-    | otherwise = invertPermutation vertexTotal vertexOrder
-
-  -- Undirected edges, in lexicographic rank of their endpoint pair.
-  --
-  -- Both components of the key are vertex ranks, so they are already dense
-  -- indices into a range this function knows: comparing them is a counting
-  -- sort's job, not a comparison sort's. Two stable passes — the high endpoint
-  -- first, then the low one — leave the pairs in lexicographic order, in time
-  -- linear in the edges and the vertices rather than @E log E@.
-  !edgeLow =
-    U.generate edgeTotal $ \index ->
-      min
-        (vertexRank `U.unsafeIndex` originOf (2 * index))
-        (vertexRank `U.unsafeIndex` originOf (2 * index + 1))
-  !edgeHigh =
-    U.generate edgeTotal $ \index ->
-      max
-        (vertexRank `U.unsafeIndex` originOf (2 * index))
-        (vertexRank `U.unsafeIndex` originOf (2 * index + 1))
-  !edgeOrder =
-    countingSortOn vertexTotal edgeLow $
-      countingSortOn vertexTotal edgeHigh (U.enumFromN 0 edgeTotal)
-
-  -- The even half of each canonical pair leaves the lower-ranked endpoint.
-  !directedFromCanonical =
-    U.generate directedTotal $ \canonical ->
-      let !oldEdge = edgeOrder `U.unsafeIndex` (canonical `quot` 2)
-          !evenHalf = 2 * oldEdge
-          !leavesLower =
-            vertexRank `U.unsafeIndex` originOf evenHalf
-              <= vertexRank `U.unsafeIndex` originOf (evenHalf + 1)
-       in if even canonical == leavesLower then evenHalf else evenHalf + 1
-  !directedToCanonical = invertPermutation directedTotal directedFromCanonical
-
-  -- The least canonical half-edge on each old face and leaving each canonical
-  -- vertex, in one pass. Anchors have to be a function of the geometry too, or
-  -- two builds of one site set would publish the same cycles anchored in
-  -- different places.
-  (!leastOnFace, !leastOutgoing) = runST $ do
-    faces <- MUV.replicate (max 1 faceTotal) absent
-    vertices <- MUV.replicate (max 1 vertexTotal) absent
-    forRange 0 directedTotal $ \canonical -> do
-      let !old = directedFromCanonical `U.unsafeIndex` canonical
-          !face = topology (4 * old + 3)
-          !rank = vertexRank `U.unsafeIndex` originOf old
-      onFace <- MUV.unsafeRead faces face
-      when (canonical < onFace) (MUV.unsafeWrite faces face canonical)
-      leaving <- MUV.unsafeRead vertices rank
-      when (canonical < leaving) (MUV.unsafeWrite vertices rank canonical)
-    (,) <$> U.unsafeFreeze faces <*> U.unsafeFreeze vertices
-
-  -- Inner faces, in order of the least canonical half-edge on their boundary.
-  -- The outer face keeps index zero, which the arena reserves for it anyway.
-  --
-  -- No two faces share a least half-edge, so this key is injective and the
-  -- ordering can be read off by inverting it: mark each face at its own least
-  -- half-edge, then scan the half-edges in order. That is one linear pass and
-  -- no comparisons at all. A face the scan never reaches has no boundary — only
-  -- reachable in a mesh with no edges — and follows in old index order so that
-  -- the result stays a permutation whatever it is handed.
-  !innerFaceOrder = U.create $ do
-    owner <- MUV.replicate (max 1 directedTotal) absent
-    forRange 1 faceTotal $ \face -> do
-      let !least = leastOnFace `U.unsafeIndex` face
-      when (least /= absent) (MUV.unsafeWrite owner least face)
-    emitted <- MUV.replicate (max 1 faceTotal) False
-    out <- MUV.new (max 0 (faceTotal - 1))
-    let scan !slot !filled
-          | slot >= directedTotal = pure filled
-          | otherwise = do
-              !face <- MUV.unsafeRead owner slot
-              if face == absent
-                then scan (slot + 1) filled
-                else do
-                  MUV.unsafeWrite out filled face
-                  MUV.unsafeWrite emitted face True
-                  scan (slot + 1) (filled + 1)
-        sweep !face !filled
-          | face >= faceTotal = pure ()
-          | otherwise = do
-              !done <- MUV.unsafeRead emitted face
-              if done
-                then sweep (face + 1) filled
-                else do
-                  MUV.unsafeWrite out filled face
-                  sweep (face + 1) (filled + 1)
-    scan 0 0 >>= sweep 1
-    pure out
-  !faceRank = U.create $ do
-    ranks <- MUV.replicate (max 1 faceTotal) 0
-    U.iforM_ innerFaceOrder $ \rank old -> MUV.unsafeWrite ranks old (rank + 1)
-    pure ranks
-  oldFaceOf canonical
-    | canonical == 0 = 0
-    | otherwise = innerFaceOrder `U.unsafeIndex` (canonical - 1)
-
-
--- | No half-edge reaches this face or vertex. A one-site mesh has such a
--- vertex and an edgeless outer face; the arena spells the same absence as a
--- packed sentinel, which is not a value an index may take.
-absent :: Int
-absent = maxBound
-
--- | @[from, to)@, without materializing the range as a list.
-forRange :: Monad m => Int -> Int -> (Int -> m ()) -> m ()
-forRange from to action = go from
- where
-  go !index
-    | index >= to = pure ()
-    | otherwise = action index >> go (index + 1)
-{-# INLINE forRange #-}
-
-sortedVector :: (U.Unbox key, Ord key) => U.Vector key -> U.Vector key
-sortedVector = U.modify Intro.sort
-{-# INLINE sortedVector #-}
-
--- | Stably reorder @items@ by a key that is already a dense index below
--- @range@, in time linear in both.
---
--- Applied least-significant key first, repeated application leaves the items in
--- lexicographic order of the whole key — which is what makes a two-component
--- ordering over vertex ranks cost @O(V + E)@ instead of @O(E log E)@.
-countingSortOn :: Int -> U.Vector Int -> U.Vector Int -> U.Vector Int
-countingSortOn range keys items = U.create $ do
-  counts <- MUV.replicate (range + 1) 0
-  U.forM_ items $ \item ->
-    MUV.unsafeModify counts (+ 1) (keys `U.unsafeIndex` item)
-  let prefix !key !running
-        | key > range = pure ()
-        | otherwise = do
-            !count <- MUV.unsafeRead counts key
-            MUV.unsafeWrite counts key running
-            prefix (key + 1) (running + count)
-  prefix 0 0
-  out <- MUV.new (max 1 (U.length items))
-  U.forM_ items $ \item -> do
-    let !key = keys `U.unsafeIndex` item
-    !slot <- MUV.unsafeRead counts key
-    MUV.unsafeWrite counts key (slot + 1)
-    MUV.unsafeWrite out slot item
-  pure (MUV.slice 0 (U.length items) out)
-
-thirdColumn :: (U.Unbox a, U.Unbox b) => U.Vector (a, b, Int) -> U.Vector Int
-thirdColumn = U.map (\(_, _, index) -> index)
-{-# INLINE thirdColumn #-}
-
--- | Whether the stored sites are already in strict lexicographic order, in
--- which case ranking them is the identity and both permutations are free.
---
--- Strict rather than non-strict: a triangulation stores each site once, so
--- equal adjacent coordinates would mean a mesh this function has no ordering
--- for, and it is the sort's business to say so rather than this predicate's.
-coordinatesAscend :: Int -> Paged Double -> Paged Double -> Bool
-coordinatesAscend total x y = go 1
- where
-  go !index
-    | index >= total = True
-    | otherwise =
-        let !previousX = x `pagedUnsafeIndex` (index - 1)
-            !currentX = x `pagedUnsafeIndex` index
-         in case compare previousX currentX of
-              LT -> go (index + 1)
-              GT -> False
-              EQ ->
-                y `pagedUnsafeIndex` (index - 1) < y `pagedUnsafeIndex` index
-                  && go (index + 1)
-
--- | @inverse ! (order ! i) == i@: the rank each element was given.
-invertPermutation :: Int -> U.Vector Int -> U.Vector Int
-invertPermutation count order = U.create $ do
-  inverse <- MUV.replicate (max 1 count) 0
-  U.iforM_ order $ \rank element -> MUV.unsafeWrite inverse element rank
-  pure inverse
diff --git a/src-dcel/Moonlight/Triangulation/Internal/CellSet.hs b/src-dcel/Moonlight/Triangulation/Internal/CellSet.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/CellSet.hs
+++ /dev/null
@@ -1,330 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE GADTs #-}
-
--- | Invariant-bearing finite closed cell selections. The resident DCEL and
--- the exact coordinates of precisely the selected vertices remain sealed
--- together; no detached handle mask or whole-mesh projection exists.
-module Moonlight.Triangulation.Internal.CellSet
-  ( ExactCellSet (..)
-  , CellSelectionError (..)
-  , exactCellSet
-  , residentExactCellSet
-  , closeFaceCellSet
-  , closeExactCellSetWith
-  , exactCellSetVertexCount
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , foldExactCellVertices
-  , foldExactCellEdges
-  , foldExactCellFaces
-  , exactCellSetIsFaceClosure
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.IntSet as IntSet
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Dcel
-  ( faceDirectedEdges
-  , numFaces
-  , numUndirectedEdges
-  , numVertices
-  , undirectedEndpoints
-  , vertexPoint
-  )
-import Moonlight.Triangulation.Exact (ExactPoint, exactPointFromPoint)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  , asUndirected
-  , faceIdIndex
-  , undirectedEdgeIdIndex
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types (PointValidationError)
-
--- | The keys of the exact-point map are the selected vertices; a second
--- vertex set would merely be a disagreeable copy.
-data ExactCellSet where
-  ExactCellSet
-    :: !(Triangulation mode vertex directed undirected face)
-    -> !(IntMap.IntMap ExactPoint)
-    -> !IntSet.IntSet
-    -> !IntSet.IntSet
-    -> ExactCellSet
-
-data ClosedCellIds = ClosedCellIds
-  { closedVertexIds :: !IntSet.IntSet
-  , closedEdgeIds :: !IntSet.IntSet
-  , closedFaceIds :: !IntSet.IntSet
-  }
-
-data CellSelectionError
-  = CellVertexOutOfRange !VertexId !Int
-  | CellEdgeOutOfRange !UndirectedEdgeId !Int
-  | CellFaceOutOfRange !FaceId !Int
-  | CellOuterFaceSelected
-  | CellCoordinateInvalid !VertexId !PointValidationError
-  | CellEdgeBoundaryMissing !UndirectedEdgeId !VertexId
-  | CellFaceEdgeMissing !FaceId !UndirectedEdgeId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The strict authoring boundary validates handles and closure once, then
--- converts only selected vertices.
-exactCellSet
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexId]
-  -> [UndirectedEdgeId]
-  -> [FaceId]
-  -> Either CellSelectionError ExactCellSet
-exactCellSet triangulation selectedVertices selectedEdges selectedFaces = do
-  validateHandles triangulation selectedVertices selectedEdges selectedFaces
-  let verticesSet = vertexSet selectedVertices
-      edgesSet = edgeSet selectedEdges
-      facesSet = faceSet selectedFaces
-  traverse_ (validateEdgeClosure triangulation verticesSet) selectedEdges
-  traverse_ (validateFaceClosure triangulation edgesSet) selectedFaces
-  sealCellSet
-    (ordinaryExactPoint triangulation)
-    triangulation
-    (ClosedCellIds verticesSet edgesSet facesSet)
-
--- | Seal the complete resident DCEL without rechecking ranges or closure
--- already established by its dense handle families. Exact coordinates are
--- still admitted at this representation boundary; only impossible handle
--- refusals and repeated incidence walks disappear.
-residentExactCellSet
-  :: Triangulation mode vertex directed undirected face
-  -> Either CellSelectionError ExactCellSet
-residentExactCellSet triangulation = do
-  let vertexIndices = [0 .. numVertices triangulation - 1]
-      edgeIndices = [0 .. numUndirectedEdges triangulation - 1]
-      faceIndices = [1 .. numFaces triangulation - 1]
-  exactPoints <-
-    IntMap.fromDistinctAscList
-      <$> traverse
-        ( \residentIndex -> do
-            point <- ordinaryExactPoint triangulation (VertexId (fromIntegral residentIndex))
-            pure (residentIndex, point)
-        )
-        vertexIndices
-  pure
-    ( ExactCellSet
-        triangulation
-        exactPoints
-        (IntSet.fromDistinctAscList edgeIndices)
-        (IntSet.fromDistinctAscList faceIndices)
-    )
-
--- | Close a bounded face selection over all resident boundary cells. Face
--- handles are author input and are checked; constructed closure is not then
--- pointlessly proved a second time.
-closeFaceCellSet
-  :: Triangulation mode vertex directed undirected face
-  -> [FaceId]
-  -> Either CellSelectionError ExactCellSet
-closeFaceCellSet triangulation selectedFaces = do
-  validateHandles triangulation [] [] selectedFaces
-  sealCellSet
-    (ordinaryExactPoint triangulation)
-    triangulation
-    (closeCellIds triangulation [] [] selectedFaces)
-
--- | Seal closure derived by a trusted downstream owner. Overlay supplies
--- handles from this very DCEL, so repeating range and closure validation would
--- protect against an impossible phase while charging every selection for it.
-closeExactCellSetWith
-  :: (VertexId -> Either CellSelectionError ExactPoint)
-  -> Triangulation mode vertex directed undirected face
-  -> [VertexId]
-  -> [UndirectedEdgeId]
-  -> [FaceId]
-  -> Either CellSelectionError ExactCellSet
-closeExactCellSetWith exactPointAt triangulation explicitVertices explicitEdges selectedFaces =
-  sealCellSet
-    exactPointAt
-    triangulation
-    (closeCellIds triangulation explicitVertices explicitEdges selectedFaces)
-
-ordinaryExactPoint
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> Either CellSelectionError ExactPoint
-ordinaryExactPoint triangulation vertex =
-  first (CellCoordinateInvalid vertex)
-    (exactPointFromPoint (vertexPoint triangulation vertex))
-
-sealCellSet
-  :: (VertexId -> Either CellSelectionError ExactPoint)
-  -> Triangulation mode vertex directed undirected face
-  -> ClosedCellIds
-  -> Either CellSelectionError ExactCellSet
-sealCellSet exactPointAt triangulation closed = do
-  exactPoints <- exactPointsFor exactPointAt (closedVertexIds closed)
-  pure
-    ( ExactCellSet
-        triangulation
-        exactPoints
-        (closedEdgeIds closed)
-        (closedFaceIds closed)
-    )
-
-closeCellIds
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexId]
-  -> [UndirectedEdgeId]
-  -> [FaceId]
-  -> ClosedCellIds
-closeCellIds triangulation explicitVertices explicitEdges selectedFaces =
-  let faceEdges =
-        concatMap
-          (map asUndirected . faceDirectedEdges triangulation)
-          selectedFaces
-      edges = edgeSet (explicitEdges <> faceEdges)
-      closedEdges = map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList edges)
-      edgeVertices =
-        concatMap
-          (\edge ->
-             let (from, to) = undirectedEndpoints triangulation edge
-              in [from, to])
-          closedEdges
-      vertices =
-        vertexSet
-          ( explicitVertices
-              <> edgeVertices
-          )
-   in ClosedCellIds
-        { closedVertexIds = vertices
-        , closedEdgeIds = edges
-        , closedFaceIds = faceSet selectedFaces
-        }
-
-exactPointsFor
-  :: (VertexId -> Either CellSelectionError ExactPoint)
-  -> IntSet.IntSet
-  -> Either CellSelectionError (IntMap.IntMap ExactPoint)
-exactPointsFor exactPointAt selected =
-  IntMap.fromAscList
-    <$> traverse
-      (\index -> do
-         point <- exactPointAt (VertexId (fromIntegral index))
-         pure (index, point))
-      (IntSet.toAscList selected)
-
-validateHandles
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexId]
-  -> [UndirectedEdgeId]
-  -> [FaceId]
-  -> Either CellSelectionError ()
-validateHandles triangulation selectedVertices selectedEdges selectedFaces = do
-  traverse_ validateVertex selectedVertices
-  traverse_ validateEdge selectedEdges
-  traverse_ validateFace selectedFaces
- where
-  validateVertex vertex
-    | vertexIdIndex vertex < numVertices triangulation = Right ()
-    | otherwise = Left (CellVertexOutOfRange vertex (numVertices triangulation))
-  validateEdge edge
-    | undirectedEdgeIdIndex edge < numUndirectedEdges triangulation = Right ()
-    | otherwise = Left (CellEdgeOutOfRange edge (numUndirectedEdges triangulation))
-  validateFace face
-    | faceIdIndex face == 0 = Left CellOuterFaceSelected
-    | faceIdIndex face < numFaces triangulation = Right ()
-    | otherwise = Left (CellFaceOutOfRange face (numFaces triangulation))
-
-validateEdgeClosure
-  :: Triangulation mode vertex directed undirected face
-  -> IntSet.IntSet
-  -> UndirectedEdgeId
-  -> Either CellSelectionError ()
-validateEdgeClosure triangulation selectedVertices edge =
-  traverse_ requireVertex [from, to]
- where
-  (from, to) = undirectedEndpoints triangulation edge
-  requireVertex vertex
-    | IntSet.member (vertexIdIndex vertex) selectedVertices = Right ()
-    | otherwise = Left (CellEdgeBoundaryMissing edge vertex)
-
-validateFaceClosure
-  :: Triangulation mode vertex directed undirected face
-  -> IntSet.IntSet
-  -> FaceId
-  -> Either CellSelectionError ()
-validateFaceClosure triangulation selectedEdges face =
-  traverse_ (requireEdge . asUndirected) (faceDirectedEdges triangulation face)
- where
-  requireEdge edge
-    | IntSet.member (undirectedEdgeIdIndex edge) selectedEdges = Right ()
-    | otherwise = Left (CellFaceEdgeMissing face edge)
-
-vertexSet :: [VertexId] -> IntSet.IntSet
-vertexSet = IntSet.fromList . map vertexIdIndex
-
-edgeSet :: [UndirectedEdgeId] -> IntSet.IntSet
-edgeSet = IntSet.fromList . map undirectedEdgeIdIndex
-
-faceSet :: [FaceId] -> IntSet.IntSet
-faceSet = IntSet.fromList . map faceIdIndex
-
-exactCellSetVertexCount :: ExactCellSet -> Int
-exactCellSetVertexCount (ExactCellSet _ selected _ _) = IntMap.size selected
-
-exactCellSetEdgeCount :: ExactCellSet -> Int
-exactCellSetEdgeCount (ExactCellSet _ _ selected _) = IntSet.size selected
-
-exactCellSetFaceCount :: ExactCellSet -> Int
-exactCellSetFaceCount (ExactCellSet _ _ _ selected) = IntSet.size selected
-
-foldExactCellVertices
-  :: (accumulator -> VertexId -> ExactPoint -> accumulator)
-  -> accumulator
-  -> ExactCellSet
-  -> accumulator
-foldExactCellVertices step initial (ExactCellSet _ selected _ _) =
-  IntMap.foldlWithKey'
-    (\accumulator index point ->
-       step accumulator (VertexId (fromIntegral index)) point)
-    initial
-    selected
-
-foldExactCellEdges
-  :: (accumulator -> UndirectedEdgeId -> accumulator)
-  -> accumulator
-  -> ExactCellSet
-  -> accumulator
-foldExactCellEdges step initial (ExactCellSet _ _ selected _) =
-  IntSet.foldl'
-    (\accumulator index -> step accumulator (UndirectedEdgeId (fromIntegral index)))
-    initial
-    selected
-
-foldExactCellFaces
-  :: (accumulator -> FaceId -> accumulator)
-  -> accumulator
-  -> ExactCellSet
-  -> accumulator
-foldExactCellFaces step initial (ExactCellSet _ _ _ selected) =
-  IntSet.foldl'
-    (\accumulator index -> step accumulator (FaceId (fromIntegral index)))
-    initial
-    selected
-
--- | Whether the value contains exactly the downward closure of its selected
--- faces, with no additional isolated vertex or edge cells. This is the precise
--- admission condition for the conventional polygonal perimeter projection.
-exactCellSetIsFaceClosure :: ExactCellSet -> Bool
-exactCellSetIsFaceClosure (ExactCellSet triangulation points edges faces) =
-  let selectedFaces =
-        map (FaceId . fromIntegral) (IntSet.toAscList faces)
-      closed = closeCellIds triangulation [] [] selectedFaces
-   in IntMap.keysSet points == closedVertexIds closed
-        && edges == closedEdgeIds closed
-        && faces == closedFaceIds closed
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/CandidateArena.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/CandidateArena.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/CandidateArena.hs
+++ /dev/null
@@ -1,92 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | The legalization candidate stack: discipline, growth, and seeding.
-module Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-  ( CandidateDiscipline (..)
-  , growLegalizationArena
-  , seedStarScratch
-  , seedGenericPairInArena
-  , seedGenericEdges
-  ) where
-
-import Control.Monad (forM_, when)
-import Control.Monad.ST (ST)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Internal.OperationState
-  ( LegalizationArena (..)
-  , OperationState
-  , legalizationArena
-  , legalizationArenaLength
-  , readScratch
-  , storeLegalizationArena
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (packIndex)
-
--- | Every normalization epoch is homogeneous. A star epoch turns each edge so
--- the inserted vertex is the opposite apex; a generic epoch consumes the
--- directed edge exactly as seeded. Keeping that fact at the epoch boundary
--- prevents every candidate from carrying and decoding a tag for a distinction
--- that cannot vary inside the stack.
-data CandidateDiscipline
-  = StarCandidates !Int
-  | GenericCandidates
-
--- | Seed candidates from the scratch arena, returning the new stack top.
--- Transaction-sized preallocation covers the normal path; rare adversarial
--- overflow grows the operation-owned vector without changing LIFO order.
-seedStarScratch :: OperationState s -> Int -> Int -> ST s Int
-seedStarScratch operation top candidateCount = do
-  initialArena <- legalizationArena operation
-  arena <- growLegalizationArena initialArena (top + candidateCount)
-  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
-  let LegalizationArena values = arena
-  forM_ [0 .. candidateCount - 1] $ \index -> do
-    edge <- readScratch operation index
-    MUV.unsafeWrite values (top + index) (packIndex edge)
-  pure (top + candidateCount)
-
--- | Append the fixed two-edge section produced by one closed hull turn.
--- Materializing @[left, right]@ only to count, zip and traverse it made the
--- dominant sweep rewrite pay list traffic for an arity known by construction.
--- The arena is explicit because a circle sweep borrows it once and glues it
--- back to the operation once, rather than performing three reference lookups
--- around every inserted point.
-seedGenericPairInArena
-  :: LegalizationArena s
-  -> Int
-  -> Int
-  -> Int
-  -> ST s (LegalizationArena s, Int)
-seedGenericPairInArena initialArena top left right = do
-  let !nextTop = top + 2
-  arena <- growLegalizationArena initialArena nextTop
-  let LegalizationArena values = arena
-  MUV.unsafeWrite values top (packIndex left)
-  MUV.unsafeWrite values (top + 1) (packIndex right)
-  pure (arena, nextTop)
-{-# INLINE seedGenericPairInArena #-}
-
--- | Seed generic candidates from a list, returning the new stack top.
-seedGenericEdges :: OperationState s -> Int -> [Int] -> ST s Int
-seedGenericEdges operation top edges = do
-  initialArena <- legalizationArena operation
-  let !count = length edges
-  arena <- growLegalizationArena initialArena (top + count)
-  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
-  let LegalizationArena values = arena
-  forM_ (zip [0 ..] edges) $ \(!index, !edge) ->
-    MUV.unsafeWrite values (top + index) (packIndex edge)
-  pure (top + count)
-
-growLegalizationArena :: LegalizationArena s -> Int -> ST s (LegalizationArena s)
-growLegalizationArena arena@(LegalizationArena values) required
-  | required <= current = pure arena
-  | otherwise = LegalizationArena <$> MUV.grow values (max (required - current) (max 1 current))
- where
-  !current = MUV.length values
-{-# INLINE growLegalizationArena #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Chain.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Chain.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Chain.hs
+++ /dev/null
@@ -1,422 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | The degenerate dimensions: a point, a segment chain, and its promotion.
-module Moonlight.Triangulation.Internal.DcelOperations.Chain
-  ( setupFirstVertex
-  , setupSecondVertex
-  , splitLineEdge
-  , extendLine
-  , prepareAscendingLineArena
-  , connectAscendingLineArena
-  , collectLineChain
-  , lineToArea
-  ) where
-
-import Control.Monad (forM_, when)
-import Control.Monad.ST (ST)
-import Data.STRef (writeSTRef)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Internal.HandleDefs (VertexId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeScratch)
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.PackedIndex (unpackIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel (..)
-  , DenseMutableDcel
-  , addEdge
-  , addEdgeBlock
-  , addFaceBlock
-  , denseAddEdgeBlock
-  , denseCommitFreshConnections
-  , denseMarkFreshConnected
-  , denseMutableOwner
-  , denseReadPointX
-  , denseReadPointY
-  , denseWriteFace
-  , denseWriteFaceEdge
-  , denseLinkEdges
-  , denseWriteOrigin
-  , directedEdgeCount
-  , edgeOriginPoint
-  , ensureCellCapacity
-  , isConnected
-  , linkEdges
-  , markConnected
-  , pointAt
-  , pointCount
-  , readConstraint
-  , readNext
-  , readOrigin
-  , readPrevious
-  , readVertexOut
-  , resetEdgeData
-  , setConstraint
-  , setCycle3
-  , writeFace
-  , writeFaceEdge
-  , writeOrigin
-  , writeVertexOut
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , readScratch
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.Probe (KnownProbe)
-import Moonlight.Triangulation.Internal.Types (BuildError (..))
-import Moonlight.Triangulation.Internal.Predicates (orient2d, orient2dCoordinates)
-
-setupFirstVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-setupFirstVertex mutable vertex = markConnected mutable vertex (-1)
-
-setupSecondVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
-setupSecondVertex mutable vertex = do
-  vertices <- pointCount mutable
-  connected <- findConnected mutable vertices 0
-  case connected of
-    Left obstruction -> pure (Left obstruction)
-    Right first -> do
-      capacity <- ensureCellCapacity mutable 1 0
-      case capacity of
-        Left obstruction -> pure (Left obstruction)
-        Right () -> do
-          (edge, reverseEdgeEdge) <- addEdge mutable first vertex
-          linkEdges mutable edge reverseEdgeEdge
-          linkEdges mutable reverseEdgeEdge edge
-          writeFace mutable edge 0
-          writeFace mutable reverseEdgeEdge 0
-          writeFaceEdge mutable 0 edge
-          writeVertexOut mutable first edge
-          markConnected mutable vertex reverseEdgeEdge
-          pure (Right ())
-
-splitLineEdge :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-splitLineEdge mutable operation edge vertex = do
-  capacity <- ensureCellCapacity mutable 1 0
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> do
-      protected <- readConstraint mutable edge
-      let !reverseEdgeEdge = reverseIndex edge
-      destinationVertex <- readOrigin mutable reverseEdgeEdge
-      oldNextEdge <- readNext mutable edge
-      oldPreviousTwin <- readPrevious mutable reverseEdgeEdge
-      oldNextTwin <- readNext mutable reverseEdgeEdge
-      writeOrigin mutable reverseEdgeEdge vertex
-      (newEdge, newTwin) <- addEdge mutable vertex destinationVertex
-      writeFace mutable newEdge 0
-      writeFace mutable newTwin 0
-      if oldNextEdge == reverseEdgeEdge
-        then do
-          -- The split segment reaches a line endpoint. Its forward edge and twin
-          -- are adjacent, so the replacement is one contiguous four-edge run.
-          -- This also covers the initial two-vertex topology.
-          linkEdges mutable edge newEdge
-          linkEdges mutable newEdge newTwin
-          linkEdges mutable newTwin reverseEdgeEdge
-          linkEdges mutable reverseEdgeEdge oldNextTwin
-        else do
-          -- Replace the two occurrences independently: [edge] becomes
-          -- [edge,newEdge], while [twin] becomes [newTwin,twin].
-          linkEdges mutable edge newEdge
-          linkEdges mutable newEdge oldNextEdge
-          linkEdges mutable oldPreviousTwin newTwin
-          linkEdges mutable newTwin reverseEdgeEdge
-      writeVertexOut mutable destinationVertex newTwin
-      markConnected mutable vertex newEdge
-      -- AB became AV. The new half is a fresh slot and already carries the
-      -- default; the truncated half is a different edge in an old slot.
-      resetEdgeData mutable (edge `quot` 2)
-      when protected $ do
-        _ <- setConstraint mutable newEdge
-        pure ()
-      addCounter operation CounterLineSplits 1
-      pure (Right ())
-
-extendLine :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-extendLine mutable operation endpoint vertex = do
-  outgoing <- readVertexOut mutable endpoint
-  if outgoing < 0
-    then pure (Left (DegenerateLineEndpointMissingOutgoing (VertexId (fromIntegral endpoint))))
-    else do
-      capacity <- ensureCellCapacity mutable 1 0
-      case capacity of
-        Left obstruction -> pure (Left obstruction)
-        Right () -> do
-          let !incoming = reverseIndex outgoing
-          (newEdge, newTwin) <- addEdge mutable endpoint vertex
-          writeFace mutable newEdge 0
-          writeFace mutable newTwin 0
-          linkEdges mutable incoming newEdge
-          linkEdges mutable newEdge newTwin
-          linkEdges mutable newTwin outgoing
-          writeVertexOut mutable endpoint newEdge
-          markConnected mutable vertex newTwin
-          addCounter operation CounterLineExtensions 1
-          pure (Right ())
-
--- | A consumed radial arena whose sites have been proved collinear and placed
--- in ascending coordinate order. The constructor stays here with the chain
--- topology it licenses; the sweep can only obtain the witness through
--- 'prepareAscendingLineArena'.
-newtype AscendingLineArena s = AscendingLineArena (MUV.MVector s Word32)
-
--- | Recognize a one-dimensional fresh section and derive its chain order from
--- the radial order already paid for by bulk admission. The nearest radial site
--- is a lawful pivot: distances increase monotonically on each side of it, so
--- reversing the lower fibre and retaining the upper fibre yields total line
--- order without another sort.
---
--- The third point is inspected before the remaining fold. General-position
--- input therefore pays one exact orientation, not a speculative linear scan.
-prepareAscendingLineArena
-  :: forall s vertex directed undirected face
-   . DenseMutableDcel s vertex directed undirected face
-  -> MUV.MVector s Word32
-  -> ST s (Maybe (AscendingLineArena s))
-prepareAscendingLineArena dense radial
-  | MUV.length radial < 3 = pure Nothing
-  | otherwise = do
-      first <- MUV.unsafeRead radial 0
-      second <- MUV.unsafeRead radial 1
-      third <- MUV.unsafeRead radial 2
-      firstX <- coordinateX first
-      firstY <- coordinateY first
-      secondX <- coordinateX second
-      secondY <- coordinateY second
-      thirdX <- coordinateX third
-      thirdY <- coordinateY third
-      let !useX = firstX /= secondX
-          pointCoordinate :: Double -> Double -> Double
-          pointCoordinate x y = if useX then x else y
-          coordinateAt :: Word32 -> ST s Double
-          coordinateAt = if useX then coordinateX else coordinateY
-          !pivotCoordinate = pointCoordinate firstX firstY
-          !initialLowerCount =
-            fromEnum (pointCoordinate secondX secondY < pivotCoordinate)
-              + fromEnum (pointCoordinate thirdX thirdY < pivotCoordinate)
-      if orient2dCoordinates firstX firstY secondX secondY thirdX thirdY /= EQ
-        then pure Nothing
-        else do
-          (!collinear, !lowerCount) <-
-            MUV.foldM'
-              (\(!admitted, !count) raw ->
-                 if admitted
-                   then do
-                     x <- coordinateX raw
-                     y <- coordinateY raw
-                     pure
-                       ( orient2dCoordinates firstX firstY secondX secondY x y == EQ
-                       , count + fromEnum (pointCoordinate x y < pivotCoordinate)
-                       )
-                   else pure (False, count)
-              )
-              (True, initialLowerCount)
-              (MUV.unsafeSlice 3 (MUV.length radial - 3) radial)
-          if collinear
-            then Just <$> orderAlongLine coordinateAt pivotCoordinate lowerCount
-            else pure Nothing
- where
-  coordinateX :: Word32 -> ST s Double
-  coordinateX = denseReadPointX dense . fromIntegral
-  coordinateY :: Word32 -> ST s Double
-  coordinateY = denseReadPointY dense . fromIntegral
-
-  orderAlongLine
-    :: (Word32 -> ST s Double)
-    -> Double
-    -> Int
-    -> ST s (AscendingLineArena s)
-  orderAlongLine coordinateAt pivotCoordinate lowerCount = do
-    ascending <- MUV.new (MUV.length radial)
-    _ <-
-      MUV.foldM'
-        (\(!lowerSeen, !upperSeen) raw -> do
-           coordinate <- coordinateAt raw
-           if coordinate < pivotCoordinate
-             then do
-               MUV.unsafeWrite ascending (lowerCount - lowerSeen - 1) raw
-               pure (lowerSeen + 1, upperSeen)
-             else do
-               MUV.unsafeWrite ascending (lowerCount + upperSeen) raw
-               pure (lowerSeen, upperSeen + 1)
-        )
-        (0, 0)
-        radial
-    pure (AscendingLineArena ascending)
-
--- | Glue a proved ascending line as one outer-face cycle. Every site and cell
--- is fresh, so the dense section initializes each topology slot exactly once
--- and commits connectivity once after the local pieces agree on their shared
--- endpoints.
-connectAscendingLineArena
-  :: DenseMutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> AscendingLineArena s
-  -> ST s (Either BuildError Int)
-connectAscendingLineArena dense operation (AscendingLineArena vertices) = do
-  let !vertexCount = MUV.length vertices
-      !segmentCount = vertexCount - 1
-      mutable = denseMutableOwner dense
-  capacity <- ensureCellCapacity mutable segmentCount 0
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> do
-      edgeBase <- denseAddEdgeBlock dense segmentCount
-      MUV.imapM_
-        (\index fromRaw -> do
-           toRaw <- MUV.unsafeRead vertices (index + 1)
-           let !forward = edgeBase + 2 * index
-               !backward = forward + 1
-               !nextForward = if index == segmentCount - 1 then backward else forward + 2
-               !nextBackward = if index == 0 then edgeBase else backward - 2
-           denseWriteOrigin dense forward (unpackIndex fromRaw)
-           denseWriteOrigin dense backward (unpackIndex toRaw)
-           denseLinkEdges dense forward nextForward
-           denseLinkEdges dense backward nextBackward
-           denseWriteFace dense forward 0
-           denseWriteFace dense backward 0
-        )
-        (MUV.unsafeSlice 0 segmentCount vertices)
-      MUV.imapM_
-        (\index raw ->
-           denseMarkFreshConnected
-             dense
-             (unpackIndex raw)
-             ( if index < segmentCount
-                 then edgeBase + 2 * index
-                 else edgeBase + 2 * segmentCount - 1
-             )
-        )
-        vertices
-      denseCommitFreshConnections dense vertexCount
-      denseWriteFaceEdge dense 0 edgeBase
-      addCounter operation CounterLineExtensions (max 0 (vertexCount - 2))
-      pure (Right vertexCount)
-
-collectLineChain :: MutableDcel s vertex directed undirected face -> OperationState s -> ST s (Either BuildError Int)
-collectLineChain mutable operation = do
-  halfEdges <- directedEdgeCount mutable
-  turn <- findTurn halfEdges 0
-  case turn of
-    Left obstruction -> pure (Left obstruction)
-    Right incoming -> do
-      let !first = reverseIndex incoming
-          go !count !edge = do
-            writeScratch operation count edge
-            edgeNext <- readNext mutable edge
-            if edgeNext == reverseIndex edge
-              then pure (Right (count + 1))
-              else go (count + 1) edgeNext
-      go 0 first
- where
-  findTurn halfEdges !edge
-    | edge >= halfEdges = pure (Left (DegenerateLineEndpointTurnMissing halfEdges))
-    | otherwise = do
-        edgeNext <- readNext mutable edge
-        if edgeNext == reverseIndex edge then pure (Right edge) else findTurn halfEdges (edge + 1)
-
-lineToArea :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> ST s (Either BuildError ())
-lineToArea mutable operation vertex = do
-  collected <- collectLineChain mutable operation
-  case collected of
-    Left obstruction -> pure (Left obstruction)
-    Right segmentCount -> lineToAreaCollected @p mutable operation vertex segmentCount
-
-lineToAreaCollected :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-lineToAreaCollected mutable@MutableDcel{mdLastFace} operation vertex segmentCount = do
-  capacity <- ensureCellCapacity mutable (segmentCount + 1) segmentCount
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> lineToAreaWithCapacity
- where
-  lineToAreaWithCapacity = do
-   firstSegment <- readScratch operation 0
-   lastSegment <- readScratch operation (segmentCount - 1)
-   firstPoint <- edgeOriginPoint mutable firstSegment
-   lastPoint <- edgeOriginPoint mutable (reverseIndex lastSegment)
-   insertedPoint <- pointAt mutable vertex
-   when (orient2d firstPoint lastPoint insertedPoint == LT) $ reverseScratchDirections operation segmentCount
-   spokeBase <- addEdgeBlock mutable (segmentCount + 1)
-   faceBase <- addFaceBlock mutable segmentCount
-   forM_ [0 .. segmentCount] $ \index -> do
-    chainVertex <-
-      if index < segmentCount
-        then readScratch operation index >>= readOrigin mutable
-        else readScratch operation (segmentCount - 1) >>= readOrigin mutable . reverseIndex
-    let !forward = spokeBase + 2 * index
-        !backward = forward + 1
-    writeOrigin mutable forward chainVertex
-    writeOrigin mutable backward vertex
-   forM_ [0 .. segmentCount - 1] $ \index -> do
-    segment <- readScratch operation index
-    let !face = faceBase + index
-        !nextSpoke = spokeBase + 2 * (index + 1)
-        !previousSpoke = spokeBase + 2 * index + 1
-    setCycle3 mutable face segment nextSpoke previousSpoke
-   lastInnerSegment <- readScratch operation (segmentCount - 1)
-   let !outerStart = reverseIndex lastInnerSegment
-       !firstOuterSpoke = spokeBase
-       !lastOuterSpoke = spokeBase + 2 * segmentCount + 1
-   linkOuterTwins mutable operation segmentCount
-   firstInnerSegment <- readScratch operation 0
-   linkEdges mutable (reverseIndex firstInnerSegment) firstOuterSpoke
-   linkEdges mutable firstOuterSpoke lastOuterSpoke
-   linkEdges mutable lastOuterSpoke outerStart
-   writeFace mutable firstOuterSpoke 0
-   writeFace mutable lastOuterSpoke 0
-   writeFaceEdge mutable 0 outerStart
-   forM_ [0 .. segmentCount - 1] $ \index -> do
-    segment <- readScratch operation index
-    chainVertex <- readOrigin mutable segment
-    writeVertexOut mutable chainVertex segment
-   finalVertex <- readOrigin mutable (reverseIndex lastInnerSegment)
-   writeVertexOut mutable finalVertex (spokeBase + 2 * segmentCount)
-   markConnected mutable vertex (spokeBase + 1)
-   writeSTRef mdLastFace faceBase
-   addCounter operation CounterLineToAreaTransitions 1
-   legalizeScratch @p mutable operation vertex segmentCount
-   pure (Right ())
-
-  linkOuterTwins :: MutableDcel s vertex directed undirected face -> OperationState s -> Int -> ST s ()
-  linkOuterTwins target ops count =
-    let go !index
-          | index <= 0 = pure ()
-          | otherwise = do
-              right <- readScratch ops index
-              left <- readScratch ops (index - 1)
-              linkEdges target (reverseIndex right) (reverseIndex left)
-              writeFace target (reverseIndex right) 0
-              go (index - 1)
-     in do
-          go (count - 1)
-          first <- readScratch ops 0
-          writeFace target (reverseIndex first) 0
-
-reverseScratchDirections :: OperationState s -> Int -> ST s ()
-reverseScratchDirections operation count = do
-  forM_ [0 .. count `quot` 2 - 1] $ \left -> do
-    let !right = count - 1 - left
-    leftEdge <- readScratch operation left
-    rightEdge <- readScratch operation right
-    writeScratch operation left (reverseIndex rightEdge)
-    writeScratch operation right (reverseIndex leftEdge)
-  when (odd count) $ do
-    let !middle = count `quot` 2
-    edge <- readScratch operation middle
-    writeScratch operation middle (reverseIndex edge)
-
-findConnected :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Either BuildError Int)
-findConnected mutable limit !vertex
-  | vertex >= limit = pure (Left (DegenerateLineConnectedVertexMissing limit))
-  | otherwise = do
-      connected <- isConnected mutable vertex
-      if connected then pure (Right vertex) else findConnected mutable limit (vertex + 1)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRewrite.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRewrite.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRewrite.hs
+++ /dev/null
@@ -1,107 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | The flip rewrite itself, on a quadrilateral.
-module Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite
-  ( flipEdge
-  , applyFlip
-  ) where
-
-import Control.Monad (unless)
-import Control.Monad.ST (ST)
-import Moonlight.Triangulation.Internal.HandleDefs (UndirectedEdgeId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel
-  , MutableTopology (..)
-  , payloadsPristine
-  , readConstraint
-  , readFace
-  , readNext
-  , readOrigin
-  , readPrevious
-  , resetEdgeData
-  , resetFaceData
-  )
-import Moonlight.Triangulation.Internal.Types (BuildError (ConstrainedEdgeFlipRefused))
-
-flipEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
-flipEdge mutable edge = do
-  protected <- readConstraint mutable edge
-  if protected
-    then pure (Left (ConstrainedEdgeFlipRefused (UndirectedEdgeId (fromIntegral (edge `quot` 2)))))
-    else do
-      let !twin = reverseIndex edge
-      edgeNext <- readNext mutable edge
-      edgePrevious <- readPrevious mutable edge
-      twinNext <- readNext mutable twin
-      twinPrevious <- readPrevious mutable twin
-      leftFace <- readFace mutable edge
-      rightFace <- readFace mutable twin
-      a <- readOrigin mutable edge
-      b <- readOrigin mutable twin
-      c <- readOrigin mutable edgePrevious
-      d <- readOrigin mutable twinPrevious
-      applyFlip mutable edge twin edgeNext edgePrevious twinNext twinPrevious leftFace rightFace a b c d
-      pure (Right ())
-
--- | The rewrite itself, from a quadrilateral the caller already holds. The
--- decision that licenses a flip reads the same two half-edge records the
--- rewrite consumes, so the drain hands its neighbourhood straight here rather
--- than making 'flipEdge' fetch it a second time; 'flipEdge' is that fetch, for
--- callers arriving with nothing but an index.
-applyFlip
-  :: MutableTopology mutable
-  => mutable s vertex directed undirected face
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> ST s ()
-applyFlip topology edge twin edgeNext edgePrevious twinNext twinPrevious leftFace rightFace a b c d = do
-  let !mutable = topologyOwner topology
-  -- Rewrite only fields whose denotation changes. Re-stating both complete
-  -- triangles writes the two unchanged face labels and face anchors, then
-  -- needlessly redirects the two new diagonal endpoints even though each
-  -- already owns another live outgoing edge. The local quadrilateral proof
-  -- above names every changed adjacency, so the minimal section is exact.
-  topologyWriteOrigin topology edge c
-  topologyWriteOrigin topology twin d
-  topologyWriteNext topology edgeNext edge
-  topologyWritePrevious topology edgeNext twinPrevious
-  topologyWriteNext topology edge twinPrevious
-  topologyWritePrevious topology edge edgeNext
-  topologyWriteNext topology twinPrevious edgeNext
-  topologyWritePrevious topology twinPrevious edge
-  topologyWriteFace topology twinPrevious leftFace
-  topologyWriteFaceEdge topology leftFace edge
-  topologyWriteNext topology twinNext twin
-  topologyWritePrevious topology twinNext edgePrevious
-  topologyWriteNext topology twin edgePrevious
-  topologyWritePrevious topology twin twinNext
-  topologyWriteNext topology edgePrevious twinNext
-  topologyWritePrevious topology edgePrevious twin
-  topologyWriteFace topology edgePrevious rightFace
-  topologyWriteFaceEdge topology rightFace twin
-  -- The diagonal AB is gone and CD stands in its slot; both triangles have
-  -- swapped a corner. Three elements changed what they are, so three labels go.
-  -- Each reset carries the same test; a site doing several states it once.
-  unless (payloadsPristine mutable) $ do
-    resetEdgeData mutable (edge `quot` 2)
-    resetFaceData mutable leftFace
-    resetFaceData mutable rightFace
-  topologyWriteVertexOut topology a twinNext
-  topologyWriteVertexOut topology b edgeNext
-{-# INLINE applyFlip #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRule.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRule.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/FlipRule.hs
+++ /dev/null
@@ -1,136 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | The firing condition of the flip rule, and the laws restricting it.
-module Moonlight.Triangulation.Internal.DcelOperations.FlipRule
-  ( LegalizationLaw (..)
-  , diagonalFires
-  , illegalDiagonal
-  , isFlippableEdge
-  ) where
-
-import Control.Monad.ST (ST)
-import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel
-  , readConstraint
-  , readFace
-  , readOrigin
-  , readPointX
-  , readPointY
-  , readPrevious
-  )
-import Moonlight.Triangulation.Internal.Predicates
-  ( inCircleCoordinates
-  , orient2dCoordinates
-  )
-
--- | Whether the flip rule may fire on a quadrilateral @a b c d@ under a law,
--- where @ab@ is the diagonal and @c@, @d@ the apexes opposite it. The
--- coordinates arrive raw, in the order the mutable accessors carry.
---
--- Both interpreters reach the same incircle rule with different eligibility
--- proofs. 'ValidMesh' arrives from two consistently oriented incident faces;
--- 'CavityRepair' restricts firing to the new fan through its floor witness.
--- The drain reaches this with the quadrilateral already in hand; the firing
--- condition itself has one owner.
-diagonalFires
-  :: LegalizationLaw
-  -> Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double
-  -> Bool
-diagonalFires law ax ay bx by cx cy dx dy =
-  case law of
-    -- A valid triangulation already proves the two incident triangles are
-    -- consistently oriented. If their union is concave, the opposite apex is
-    -- outside the first triangle's circumcircle and the in-circle rule refuses
-    -- the flip; re-running two exact orientation predicates merely re-proves
-    -- that premise for every candidate. This is the same lawful section used
-    -- by Spade's removal legalizer: the diagonal rule alone decides.
-    ValidMesh -> illegalDiagonal ax ay bx by cx cy dx dy
-    CavityRepair _ -> illegalDiagonal ax ay bx by cx cy dx dy
-    SeamPreserving _ -> illegalDiagonal ax ay bx by cx cy dx dy
-{-# INLINE diagonalFires #-}
-
--- | Whether a diagonal is locally illegal — the firing condition of the flip
--- rule @drainLegalization@ normalizes under, and the step at which its
--- potential strictly decreases.
---
--- Lift the quadrilateral to @z = x² + y²@ and the in-circle sign is the
--- orientation of the lifted tetrahedron: 'GT' says the fourth point is below
--- the plane of the other three, so the current diagonal spans a fold the flip
--- pushes downward. 'EQ' says the four lifted points are coplanar and the flip
--- changes nothing about the surface — so the ordering on diagonal keys stands
--- in as the potential, and the rule fires only downward in it. That tie-break
--- is not a convention for picking among equals; it is what stops a cocircular
--- quadrilateral from flipping forever.
-illegalDiagonal
-  :: Double -> Double -> Double -> Double -> Double -> Double -> Double -> Double -> Bool
-illegalDiagonal ax ay bx by cx cy dx dy =
-  case inCircleCoordinates ax ay bx by cx cy dx dy of
-    GT -> True
-    LT -> False
-    EQ -> orderedPair (cx, cy) (dx, dy) < orderedPair (ax, ay) (bx, by)
-{-# INLINE illegalDiagonal #-}
-
-isFlippableEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
-isFlippableEdge mutable edge = do
-  protected <- readConstraint mutable edge
-  if protected
-    then pure False
-    else do
-      let !reverseEdgeEdge = reverseIndex edge
-      leftFace <- readFace mutable edge
-      rightFace <- readFace mutable reverseEdgeEdge
-      if leftFace == 0 || rightFace == 0
-        then pure False
-        else do
-          edgePrevious <- readPrevious mutable edge
-          reverseEdgePrevious <- readPrevious mutable reverseEdgeEdge
-          a <- readOrigin mutable edge
-          b <- readOrigin mutable reverseEdgeEdge
-          c <- readOrigin mutable edgePrevious
-          d <- readOrigin mutable reverseEdgePrevious
-          ax <- readPointX mutable a
-          ay <- readPointY mutable a
-          bx <- readPointX mutable b
-          by <- readPointY mutable b
-          cx <- readPointX mutable c
-          cy <- readPointY mutable c
-          dx <- readPointX mutable d
-          dy <- readPointY mutable d
-          pure
-            ( orient2dCoordinates cx cy dx dy bx by == GT
-                && orient2dCoordinates dx dy cx cy ax ay == GT
-            )
-{-# INLINE isFlippableEdge #-}
-
--- | The rewrite strategy: which candidates the flip rule is allowed to fire
--- on. All laws leave the normal form alone — they restrict where the rule may
--- be applied, never what a legal diagonal is.
---
--- 'ValidMesh' legalizes a mesh that is already a triangulation. There a
--- non-convex quadrilateral's diagonal is necessarily locally Delaunay, so the
--- incircle rule itself declines the flip; separately re-running two exact
--- orientation predicates would only re-prove the incident-face premise.
---
--- 'CavityRepair' legalizes the fan that fills a removed vertex's hole, which is
--- not yet a triangulation. That implication fails on an inverted quadrilateral,
--- and the inverted one is exactly the one that must flip, so the short-circuit
--- becomes a refusal to perform the repair. The carried index is the undirected
--- edge count taken before the fan was built: every edge at or above it was
--- appended by the fan, and only those may flip, so a transiently inverted
--- neighbourhood cannot flip the cavity's border away.
---
--- 'SeamPreserving' admits a candidate exactly when neither incident target
--- face handle is selected by the caller. Face handles survive a flip, so this
--- protection is inductive while legalization propagates through the complete
--- unselected seam cavity.
-data LegalizationLaw
-  = ValidMesh
-  | CavityRepair {-# UNPACK #-} !Int
-  | SeamPreserving !(Int -> Bool)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Hull.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Hull.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Hull.hs
+++ /dev/null
@@ -1,551 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | Growth outside the hull: visible ranges, turn closure, and convexity repair.
-module Moonlight.Triangulation.Internal.DcelOperations.Hull
-  ( ReservedSweepCells
-  , SweepCellCursor
-  , SweepInsertion (..)
-  , reserveSweepCells
-  , initialSweepCellCursor
-  , commitReservedSweepConnections
-  , insertOutsideHull
-  , insertOutsideHullAtEdge
-  , closeOuterTurn
-  , closeOuterTurnReserved
-  , fixHullConvexity
-  ) where
-
-import Control.Monad (forM_, when)
-import Control.Monad.ST (ST)
-import Data.STRef (writeSTRef)
-import Moonlight.Triangulation.Internal.HandleDefs (DirectedEdgeId (..), FaceId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-  ( seedGenericPairInArena
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize
-  ( legalizeScratch
-  , legalizeDenseStarEdgeInArena
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Normalize
-  ( LegalizationDrain (..)
-  , drainDenseUnconstrainedGenericLegalization
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( DenseMutableDcel
-  , MutableDcel (..)
-  , addEdge
-  , addEdgeBlock
-  , addFaceBlock
-  , directedEdgeCount
-  , denseCommitFreshConnections
-  , denseInitializeUnconstrainedEdgeBlock
-  , denseLinkEdges
-  , denseMarkFreshConnected
-  , denseMutableOwner
-  , denseReadFace
-  , denseReadFaceEdge
-  , denseReadNext
-  , denseReadOrigin
-  , denseReadPointX
-  , denseReadPointY
-  , denseReadPrevious
-  , denseSetCycle3
-  , denseWriteFace
-  , denseWriteFaceEdge
-  , denseWriteNext
-  , denseWriteOrigin
-  , denseWritePrevious
-  , denseWriteVertexOut
-  , ensureCellCapacity
-  , faceCount
-  , linkEdges
-  , markConnected
-  , pointAt
-  , readFace
-  , readNext
-  , readOrigin
-  , readPointX
-  , readPointY
-  , readPrevious
-  , setCycle3
-  , writeFace
-  , writeFaceEdge
-  , writeNext
-  , writeOrigin
-  , writePrevious
-  , writeVertexOut
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , LegalizationArena
-  , OperationState
-  , addCounter
-  , readScratch
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.Probe (KnownProbe)
-import Moonlight.Triangulation.Types (BuildError (..), Point (..))
-import Moonlight.Triangulation.Internal.Predicates (orient2dCoordinates)
-
--- | Proof that the remaining circle-sweep program fits the particular mutable
--- arena carried here. The constructor is private: only 'reserveSweepCells' can
--- pair a DCEL with the one-time capacity check. Carrying the DCEL inside the
--- witness prevents a proof for one mutable mesh from being applied to another
--- mesh that happens to share the same @ST@ region.
-data ReservedSweepCells s vertex directed undirected face = ReservedSweepCells
-  !(DenseMutableDcel s vertex directed undirected face)
-  {-# UNPACK #-} !SweepCellCursor
-
--- | The uncommitted directed-edge and face cardinalities of one reserved
--- sweep section. Its constructor is private: only the reservation can mint the
--- initial cursor, and only reserved topology rewrites can advance it.
-data SweepCellCursor = SweepCellCursor
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-
--- | The outer edges glued by one direct sweep insertion and the normalization
--- work discharged while its star still existed. The sweep accumulates these
--- strict metrics and charges the operation counters once, instead of mutating
--- diagnostic cells once per point.
-data SweepInsertion s
-  = SweepInsertionFailure !BuildError
-  | SweepInsertion
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      !(LegalizationArena s)
-      {-# UNPACK #-} !SweepCellCursor
-
--- | Discharge the monotone circle-sweep allocation budget once. Every
--- remaining point can contribute at most three undirected edges and two faces
--- to a planar triangulation. The seed already occupies its own cells, so this
--- bound is stated only over the points the sweep has not connected yet.
-reserveSweepCells
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError (ReservedSweepCells s vertex directed undirected face))
-reserveSweepCells dense remainingPoints = do
-  let !mutable = denseMutableOwner dense
-  halfEdges <- directedEdgeCount mutable
-  faces <- faceCount mutable
-  capacity <- ensureCellCapacity mutable (3 * remainingPoints) (2 * remainingPoints)
-  pure (ReservedSweepCells dense (SweepCellCursor halfEdges faces) <$ capacity)
-
-initialSweepCellCursor
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-initialSweepCellCursor (ReservedSweepCells _ cursor) = cursor
-{-# INLINE initialSweepCellCursor #-}
-
--- | Commit the cardinality of the fresh connectivity sections materialized by
--- direct sweep insertions. Their per-vertex bits and outgoing edges are already
--- present; this is the one global descent step before any skipped point enters
--- the ordinary insertion interpreter.
-commitReservedSweepConnections
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-  -> Int
-  -> ST s ()
-commitReservedSweepConnections (ReservedSweepCells dense _) (SweepCellCursor halfEdges faces) inserted = do
-  denseCommitFreshConnections dense inserted
-  let !MutableDcel{mdHalfCount, mdFaceCount, mdLastFace} = denseMutableOwner dense
-  writeSTRef mdHalfCount halfEdges
-  writeSTRef mdFaceCount faces
-  when (inserted > 0 && faces > 1) (writeSTRef mdLastFace (faces - 1))
-{-# INLINE commitReservedSweepConnections #-}
-
-insertOutsideHull :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-insertOutsideHull mutable operation start vertex = do
-  query <- pointAt mutable vertex
-  visibleStart <- visibleOuter mutable start query
-  if not visibleStart
-    then pure (Left (HullStartNotVisible (DirectedEdgeId (fromIntegral start))))
-    else do
-      halfEdges <- directedEdgeCount mutable
-      left <- expandPrevious halfEdges start start query
-      right <- expandNext halfEdges start left query
-      collected <- collectOuterChain mutable operation left right
-      case collected of
-        Left obstruction -> pure (Left obstruction)
-        Right chainCount -> do
-          inserted <- insertOutsideHullCollected @p mutable operation vertex chainCount
-          case inserted of
-            Left obstruction -> pure (Left obstruction)
-            Right _ -> do
-              -- The regular insertion path owns its own hull-insertion count; the sweep
-              -- path through 'insertOutsideHullBetween' counts its own instead.
-              addCounter operation CounterHullInsertions 1
-              pure (Right ())
- where
-  expandPrevious !bound !stopAt !current !query
-    | bound <= 0 = pure current
-    | otherwise = do
-        candidate <- readPrevious mutable current
-        if candidate == stopAt
-          then pure current
-          else do
-            visible <- visibleOuter mutable candidate query
-            if visible then expandPrevious (bound - 1) stopAt candidate query else pure current
-
-  expandNext !bound !current !left !query
-    | bound <= 0 = pure current
-    | otherwise = do
-        candidate <- readNext mutable current
-        if candidate == left
-          then pure current
-          else do
-            visible <- visibleOuter mutable candidate query
-            if visible then expandNext (bound - 1) candidate left query else pure current
-
--- | Replace one selected outer edge @a->b@ by @a->v, v->b@ and materialize
--- the covered triangle. This is the circle sweep's actual local section: the
--- ordinary outside-hull path continues to own arbitrary visible ranges, while
--- the sweep no longer collects a singleton range into shared scratch merely to
--- rediscover its first and last edge.
-insertOutsideHullAtEdge
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-  -> LegalizationArena s
-  -> Int
-  -> Int
-  -> Int
-  -> Int
-  -> ST s (SweepInsertion s)
-insertOutsideHullAtEdge (ReservedSweepCells dense _) (SweepCellCursor nextHalf nextFace) arena outerEdge from to vertex = do
-  incident <- denseReadFace dense outerEdge
-  if incident /= 0
-    then
-      pure
-        ( SweepInsertionFailure
-            ( OuterRangeContainsInnerEdge
-                (DirectedEdgeId (fromIntegral outerEdge))
-                (FaceId (fromIntegral incident))
-            )
-        )
-    else do
-      oldPrevious <- denseReadPrevious dense outerEdge
-      oldNext <- denseReadNext dense outerEdge
-      denseInitializeUnconstrainedEdgeBlock dense nextHalf 2
-      let !edgeBase = nextHalf
-          !face = nextFace
-          !firstOuterSpoke = edgeBase
-          !firstInnerSpoke = edgeBase + 1
-          !secondInnerSpoke = edgeBase + 2
-          !lastOuterSpoke = edgeBase + 3
-      denseWriteOrigin dense firstOuterSpoke from
-      denseWriteOrigin dense firstInnerSpoke vertex
-      denseWriteOrigin dense secondInnerSpoke to
-      denseWriteOrigin dense lastOuterSpoke vertex
-      denseSetCycle3 dense face outerEdge secondInnerSpoke firstInnerSpoke
-      denseWriteFace dense firstOuterSpoke 0
-      denseWriteFace dense lastOuterSpoke 0
-      denseLinkEdges dense oldPrevious firstOuterSpoke
-      denseLinkEdges dense firstOuterSpoke lastOuterSpoke
-      denseLinkEdges dense lastOuterSpoke oldNext
-      denseWriteFaceEdge dense 0 firstOuterSpoke
-      denseWriteVertexOut dense from outerEdge
-      denseWriteVertexOut dense to secondInnerSpoke
-      denseMarkFreshConnected dense vertex lastOuterSpoke
-      LegalizationDrain flips maxDepth () finalArena <-
-        legalizeDenseStarEdgeInArena dense arena vertex outerEdge
-      pure
-        ( SweepInsertion
-            firstOuterSpoke
-            lastOuterSpoke
-            flips
-            maxDepth
-            finalArena
-            (SweepCellCursor (nextHalf + 4) (nextFace + 1))
-        )
-{-# INLINE insertOutsideHullAtEdge #-}
-
-collectOuterChain
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Either BuildError Int)
-collectOuterChain mutable operation left right = do
-  halfEdges <- directedEdgeCount mutable
-  go (halfEdges + 1) left 0
- where
-  go !remaining !current !count
-    | remaining <= 0 =
-        pure
-          ( Left
-              ( OuterRangeDidNotTerminate
-                  (DirectedEdgeId (fromIntegral left))
-                  (DirectedEdgeId (fromIntegral right))
-                  count
-              )
-          )
-    | otherwise = do
-        incident <- readFace mutable current
-        if incident /= 0
-          then
-            pure
-              ( Left
-                  ( OuterRangeContainsInnerEdge
-                      (DirectedEdgeId (fromIntegral current))
-                      (FaceId (fromIntegral incident))
-                  )
-              )
-          else do
-            writeScratch operation count current
-            if current == right
-              then pure (Right (count + 1))
-              else readNext mutable current >>= \following -> go (remaining - 1) following (count + 1)
-
-insertOutsideHullCollected
-  :: forall p s vertex directed undirected face
-   . KnownProbe p
-  => MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Either BuildError (Int, Int))
-insertOutsideHullCollected mutable@MutableDcel{mdLastFace} operation vertex chainCount = do
-  capacity <- ensureCellCapacity mutable (chainCount + 1) chainCount
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> Right <$> insertOutsideHullWithCapacity
- where
-  insertOutsideHullWithCapacity = do
-    left <- readScratch operation 0
-    right <- readScratch operation (chainCount - 1)
-    oldPrevious <- readPrevious mutable left
-    oldNext <- readNext mutable right
-    edgeBase <- addEdgeBlock mutable (chainCount + 1)
-    faceBase <- addFaceBlock mutable chainCount
-    forM_ [0 .. chainCount] $ \index -> do
-      chainVertex <-
-        if index == 0
-          then readScratch operation 0 >>= readOrigin mutable
-          else readScratch operation (index - 1) >>= readOrigin mutable . reverseIndex
-      let !forward = edgeBase + 2 * index
-          !backward = forward + 1
-      writeOrigin mutable forward chainVertex
-      writeOrigin mutable backward vertex
-    forM_ [0 .. chainCount - 1] $ \index -> do
-      outerEdge <- readScratch operation index
-      let !face = faceBase + index
-          !nextSpoke = edgeBase + 2 * (index + 1)
-          !previousSpoke = edgeBase + 2 * index + 1
-      setCycle3 mutable face outerEdge nextSpoke previousSpoke
-    let !firstOuterSpoke = edgeBase
-        !lastOuterSpoke = edgeBase + 2 * chainCount + 1
-    writeFace mutable firstOuterSpoke 0
-    writeFace mutable lastOuterSpoke 0
-    linkEdges mutable oldPrevious firstOuterSpoke
-    linkEdges mutable firstOuterSpoke lastOuterSpoke
-    linkEdges mutable lastOuterSpoke oldNext
-    writeFaceEdge mutable 0 firstOuterSpoke
-    forM_ [0 .. chainCount - 1] $ \index -> do
-      chainEdge <- readScratch operation index
-      chainVertex <- readOrigin mutable chainEdge
-      writeVertexOut mutable chainVertex chainEdge
-    lastChain <- readScratch operation (chainCount - 1)
-    lastVertex <- readOrigin mutable (reverseIndex lastChain)
-    writeVertexOut mutable lastVertex (edgeBase + 2 * chainCount)
-    markConnected mutable vertex lastOuterSpoke
-    writeSTRef mdLastFace faceBase
-    legalizeScratch @p mutable operation vertex chainCount
-    pure (firstOuterSpoke, lastOuterSpoke)
-
--- | Replace two consecutive outer edges @a->b, b->c@ by @a->c@ and
--- materialize the triangle they bound. The returned edge is the new outer
--- diagonal. This is the sole turn-closing primitive used both by deferred
--- circle sweep and the final Graham repair. Topology mutation only: the
--- caller owns the legalization epoch, and seeds the two closed edges itself
--- once the replacement's links are in place.
-closeOuterTurn
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError Int)
-closeOuterTurn mutable first = do
-  capacity <- ensureCellCapacity mutable 1 1
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> Right <$> closeOuterTurnWithCapacity mutable first
-{-# INLINE closeOuterTurn #-}
-
--- | The circle-sweep form of 'closeOuterTurn'. Its capacity obstruction was
--- discharged by 'reserveSweepCells'; topology mutation is otherwise identical
--- to the checked public-internal operation used by joins.
-closeOuterTurnReserved
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-  -> Int
-  -> ST s (Int, SweepCellCursor)
-closeOuterTurnReserved
-  (ReservedSweepCells dense _)
-  (SweepCellCursor nextHalf nextFace)
-  first = do
-    denseInitializeUnconstrainedEdgeBlock dense nextHalf 1
-    replacement <- closeOuterTurnDenseAt dense nextHalf nextFace first
-    pure (replacement, SweepCellCursor (nextHalf + 2) (nextFace + 1))
-{-# INLINE closeOuterTurnReserved #-}
-
--- | Materialize one turn closure at cells already owned by the caller's
--- allocation section. Ordinary edits obtain those cells from the checked
--- allocator; the reserved sweep obtains them from its immutable cursor.
-closeOuterTurnDenseAt
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> Int
-  -> ST s Int
-closeOuterTurnDenseAt dense edgeBase newFace first = do
-  second <- denseReadNext dense first
-  oldPrevious <- denseReadPrevious dense first
-  oldNext <- denseReadNext dense second
-  from <- denseReadOrigin dense first
-  to <- denseReadOrigin dense (reverseIndex second)
-  let !outer = edgeBase
-      !inner = edgeBase + 1
-  denseWriteOrigin dense outer from
-  denseWriteOrigin dense inner to
-  denseWriteFace dense outer 0
-  -- @first -> second@ is the authoritative outer adjacency that licensed this
-  -- closure. Preserve those two already-correct cells and write only the six
-  -- changed links plus the new face section.
-  denseWriteNext dense second inner
-  denseWritePrevious dense inner second
-  denseWriteNext dense inner first
-  denseWritePrevious dense first inner
-  denseWriteFace dense first newFace
-  denseWriteFace dense second newFace
-  denseWriteFace dense inner newFace
-  denseWriteFaceEdge dense newFace first
-  denseLinkEdges dense oldPrevious outer
-  denseLinkEdges dense outer oldNext
-  denseWriteFaceEdge dense 0 outer
-  denseWriteVertexOut dense from outer
-  denseWriteVertexOut dense to inner
-  pure outer
-{-# INLINE closeOuterTurnDenseAt #-}
-
-closeOuterTurnWithCapacity
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s Int
-closeOuterTurnWithCapacity mutable first = do
-  second <- readNext mutable first
-  oldPrevious <- readPrevious mutable first
-  oldNext <- readNext mutable second
-  from <- readOrigin mutable first
-  to <- readOrigin mutable (reverseIndex second)
-  (outer, inner) <- addEdge mutable from to
-  newFace <- addFaceBlock mutable 1
-  writeFace mutable outer 0
-  writeNext mutable second inner
-  writePrevious mutable inner second
-  writeNext mutable inner first
-  writePrevious mutable first inner
-  writeFace mutable first newFace
-  writeFace mutable second newFace
-  writeFace mutable inner newFace
-  writeFaceEdge mutable newFace first
-  linkEdges mutable oldPrevious outer
-  linkEdges mutable outer oldNext
-  writeFaceEdge mutable 0 outer
-  writeVertexOut mutable from outer
-  writeVertexOut mutable to inner
-  pure outer
-{-# INLINE closeOuterTurnWithCapacity #-}
-
--- | Close every remaining left turn of the star-shaped sweep hull in one
--- Graham-style pass. Each closure strictly decreases the outer-edge count, so
--- the pass is linear in the visited hull plus the local Delaunay legalization
--- work it causes. Turn closures are seeded into one shared epoch as they
--- happen and drained once at the end: closure never deletes an edge and never
--- touches the outer cycle's legality, so which turns close does not depend on
--- when the interior is repaired. The pass counts its own closures and returns
--- the drain's tallies; nothing is reported behind its back.
-fixHullConvexity
-  :: ReservedSweepCells s vertex directed undirected face
-  -> SweepCellCursor
-  -> OperationState s
-  -> LegalizationArena s
-  -> ST s (Either BuildError (Int, Int, Int, LegalizationArena s, SweepCellCursor))
-fixHullConvexity reserved@(ReservedSweepCells dense _) initialCursor operation initialArena = do
-  start <- denseReadFaceEdge dense 0
-  if start < 0
-    then pure (Right (0, 0, 0, initialArena, initialCursor))
-    else do
-      walked <- walk start start 0 0 0 0 initialArena initialCursor
-      case walked of
-        Left obstruction -> pure (Left obstruction)
-        Right (top, closures, seededArena, finalCursor) -> do
-          LegalizationDrain flips maxDepth () finalArena <-
-            drainDenseUnconstrainedGenericLegalization dense seededArena top
-          pure (Right (closures, flips, maxDepth, finalArena, finalCursor))
- where
-  walk !start !current !stackSize !steps !top !closures !arena cursor@(SweepCellCursor halfEdges _) = do
-    if steps > halfEdges + 2
-      then
-        pure
-          ( Left
-              ( OuterCycleDidNotTerminate
-                  (DirectedEdgeId (fromIntegral start))
-                  (DirectedEdgeId (fromIntegral current))
-                  steps
-              )
-          )
-      else do
-        following <- denseReadNext dense current
-        writeScratch operation stackSize current
-        reduction <- reduce (stackSize + 1) top closures arena cursor
-        case reduction of
-          Left obstruction -> pure (Left obstruction)
-          Right (reduced, nextTop, nextClosures, nextArena, nextCursor) -> do
-            finished <-
-              if reduced < 2
-                then pure False
-                else (== following) <$> readScratch operation 1
-            if finished
-              then pure (Right (nextTop, nextClosures, nextArena, nextCursor))
-              else walk start following reduced (steps + 1) nextTop nextClosures nextArena nextCursor
-
-  reduce !count !top !closures !arena !cursor
-    | count < 2 = pure (Right (count, top, closures, arena, cursor))
-    | otherwise = do
-        first <- readScratch operation (count - 2)
-        second <- readScratch operation (count - 1)
-        fromVertex <- denseReadOrigin dense first
-        middleVertex <- denseReadOrigin dense (reverseIndex first)
-        targetVertex <- denseReadOrigin dense (reverseIndex second)
-        fromX <- denseReadPointX dense fromVertex
-        fromY <- denseReadPointY dense fromVertex
-        middleX <- denseReadPointX dense middleVertex
-        middleY <- denseReadPointY dense middleVertex
-        targetX <- denseReadPointX dense targetVertex
-        targetY <- denseReadPointY dense targetVertex
-        if orient2dCoordinates fromX fromY middleX middleY targetX targetY == GT
-          then do
-            (replacement, nextCursor) <- closeOuterTurnReserved reserved cursor first
-            writeScratch operation (count - 2) replacement
-            (nextArena, nextTop) <- seedGenericPairInArena arena top first second
-            reduce (count - 1) nextTop (closures + 1) nextArena nextCursor
-          else pure (Right (count, top, closures, arena, cursor))
-
-visibleOuter :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s Bool
-visibleOuter mutable edge query = do
-  fromVertex <- readOrigin mutable edge
-  toVertex <- readOrigin mutable (reverseIndex edge)
-  case query of
-    Point queryX queryY -> do
-      fromX <- readPointX mutable fromVertex
-      fromY <- readPointY mutable fromVertex
-      toX <- readPointX mutable toVertex
-      toY <- readPointY mutable toVertex
-      pure (orient2dCoordinates fromX fromY toX toY queryX queryY == GT)
-{-# INLINE visibleOuter #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Legalize.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Legalize.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Legalize.hs
+++ /dev/null
@@ -1,164 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | The seeding entry points that drive one legalization epoch.
-module Moonlight.Triangulation.Internal.DcelOperations.Legalize
-  ( legalizeScratch
-  , legalizeStarEdge
-  , legalizeDenseStarEdgeInArena
-  , legalizeEdges
-  , legalizeEdgesPinned
-  , legalizeCavityFanScratch
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.Foldable (traverse_)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-  ( CandidateDiscipline (..)
-  , growLegalizationArena
-  , seedGenericEdges
-  , seedStarScratch
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRule
-  ( LegalizationLaw (..)
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Normalize
-  ( LegalizationDrain
-  , drainDenseUnconstrainedStarLegalization
-  , drainLegalization
-  )
-import Moonlight.Triangulation.Internal.Mutable
-  ( DenseMutableDcel
-  , MutableDcel
-  , MutableTopology
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , LegalizationArena (..)
-  , OperationState
-  , addCounter
-  , legalizationArena
-  , legalizationArenaLength
-  , maxCounter
-  , readScratch
-  , storeLegalizationArena
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (packIndex)
-import Moonlight.Triangulation.Internal.Probe (KnownProbe, Probe (..))
-
-legalizeEdges :: MutableDcel s vertex directed undirected face -> OperationState s -> [Int] -> ST s ()
-legalizeEdges mutable operation initial = do
-  top <- seedGenericEdges operation 0 initial
-  (flips, maxDepth) <- drainLegalization @'ProbeOff mutable operation top GenericCandidates ValidMesh
-  addCounter operation CounterEdgeFlips flips
-  maxCounter operation CounterLegalizationMaxStack maxDepth
-
--- | Legalize the new seam and the source-prefix cavity whose current incident
--- face handles are both unprotected. The selector is evaluated dynamically as
--- flips propagate; it therefore admits the complete local exterior cavity
--- rather than only the zipper's first seeds. The seam owner certifies the
--- resulting cavity boundary after immutable publication.
-legalizeEdgesPinned
-  :: forall s vertex directed undirected face.
-     (Int -> Bool)
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> [Int]
-  -> ST s ()
-legalizeEdgesPinned targetFaceProtected mutable operation initial = do
-  top <- seedGenericEdges operation 0 initial
-  (flips, maxDepth) <-
-    drainLegalization
-      @'ProbeOff
-      mutable
-      operation
-      top
-      GenericCandidates
-      (SeamPreserving targetFaceProtected)
-  addCounter operation CounterEdgeFlips flips
-  maxCounter operation CounterLegalizationMaxStack maxDepth
-
--- | Repair the fan that fills a removed vertex's hole, draining cavity
--- candidates already written into the operation-owned scratch section. The
--- fan is a valid combinatorial filling but not yet a triangulation — a link
--- polygon that is non-convex at the fan origin yields an inverted triangle —
--- so this drain carries 'CavityRepair' rather than the insertion law: it
--- flips on the incircle determinant alone, and only the fan's own edges may
--- flip. Removal discovers and constructs the cavity inside that same
--- transaction; materializing a list merely to seed the legalization arena
--- would duplicate the local program.
-legalizeCavityFanScratch
-  :: MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> Int
-  -> ST s ()
-legalizeCavityFanScratch mutable operation cavityFloor scratchOffset candidateCount = do
-  initialArena <- legalizationArena operation
-  arena <- growLegalizationArena initialArena candidateCount
-  when (legalizationArenaLength arena /= legalizationArenaLength initialArena) (storeLegalizationArena operation arena)
-  let LegalizationArena values = arena
-  traverse_
-    (\index -> do
-       edge <- readScratch operation (scratchOffset + index)
-       MUV.unsafeWrite values index (packIndex edge)
-    )
-    [0 .. candidateCount - 1]
-  (flips, maxDepth) <-
-    drainLegalization
-      @'ProbeOff
-      mutable
-      operation
-      candidateCount
-      GenericCandidates
-      (CavityRepair cavityFloor)
-  addCounter operation CounterEdgeFlips flips
-  maxCounter operation CounterLegalizationMaxStack maxDepth
-
-legalizeScratch :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
-legalizeScratch mutable operation vertex candidateCount = do
-  top <- seedStarScratch operation 0 candidateCount
-  (flips, maxDepth) <- drainLegalization @p mutable operation top (StarCandidates vertex) ValidMesh
-  addCounter operation CounterEdgeFlips flips
-  maxCounter operation CounterLegalizationMaxStack maxDepth
-
--- | Normalize one newly covered outer edge against the inserted star vertex.
--- The circle sweep always covers exactly one edge; routing that singleton
--- through the shared scratch section merely materializes a one-element range
--- before immediately copying it into this arena. This is the same star-law
--- section written at its actual arity.
-legalizeStarEdge
-  :: forall p mutable s vertex directed undirected face
-   . (KnownProbe p, MutableTopology mutable)
-  => mutable s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Int, Int)
-legalizeStarEdge mutable operation vertex edge = do
-  arena <- legalizationArena operation
-  let LegalizationArena values = arena
-  MUV.unsafeWrite values 0 (packIndex edge)
-  drainLegalization @p mutable operation 1 (StarCandidates vertex) ValidMesh
-{-# INLINE legalizeStarEdge #-}
-
--- | The monomorphic fresh-build interpreter for the singleton star epoch,
--- consuming and returning the sweep's borrowed candidate section.
-legalizeDenseStarEdgeInArena
-  :: DenseMutableDcel s vertex directed undirected face
-  -> LegalizationArena s
-  -> Int
-  -> Int
-  -> ST s (LegalizationDrain s ())
-legalizeDenseStarEdgeInArena dense arena vertex edge = do
-  let LegalizationArena values = arena
-  MUV.unsafeWrite values 0 (packIndex edge)
-  drainDenseUnconstrainedStarLegalization dense arena 1 vertex
-{-# NOINLINE legalizeDenseStarEdgeInArena #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Normalize.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Normalize.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Normalize.hs
+++ /dev/null
@@ -1,362 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | The normalization procedure of the flip rewrite system.
-module Moonlight.Triangulation.Internal.DcelOperations.Normalize
-  ( drainLegalization
-  , LegalizationDrain (..)
-  , drainDenseUnconstrainedStarLegalization
-  , drainDenseUnconstrainedGenericLegalization
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.Bits (shiftR)
-import Data.STRef (readSTRef)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Internal.DcelOperations.CandidateArena
-  ( CandidateDiscipline (..)
-  , growLegalizationArena
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRewrite (applyFlip)
-import Moonlight.Triangulation.Internal.DcelOperations.FlipRule
-  ( LegalizationLaw (..)
-  , diagonalFires
-  , illegalDiagonal
-  )
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( DenseMutableDcel
-  , MutableDcel (..)
-  , MutableTopology (..)
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , LegalizationArena (..)
-  , OperationState
-  , legalizationArena
-  , legalizationArenaLength
-  , storeLegalizationArena
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (packIndex)
-import Moonlight.Triangulation.Internal.Probe
-  ( KnownProbe (..)
-  , Probe (..)
-  , ProbeCounter
-  )
-
--- | The authoritative result of one normalization section. The arena is part
--- of the result because an adversarial frontier may grow it; callers that
--- borrow the section for several local rewrites can therefore compose those
--- rewrites without bouncing through the operation's reference between them.
-data LegalizationDrain s counter = LegalizationDrain
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-  !counter
-  !(LegalizationArena s)
-
--- | The normalization procedure of a confluent terminating rewrite system, and
--- one canonical legalization engine because a normalization procedure is what
--- it is.
---
--- The objects are the triangulations of a fixed point set. The single rule is
--- the Lawson flip: a locally illegal diagonal is replaced by the other
--- diagonal of its quadrilateral. A normal form is a mesh with no illegal
--- diagonal left to fire on.
---
--- /Termination/ is by the lifted-paraboloid potential. Send each point to
--- @(x, y, x² + y²)@ and read a triangulation as a piecewise-linear surface
--- over the point set; @illegalDiagonal@ is exactly the test that the flip
--- lowers that surface, so every rewrite strictly decreases it, and a finite
--- point set has finitely many triangulations. Exact cocircularity is the one
--- case where the surface does not move — the four lifted points are coplanar
--- and both diagonals give the same surface — so there the potential is the
--- diagonal's own key order, and the rule fires only downward in it. Without
--- that tie-break a cocircular quadrilateral flips forever.
---
--- /Confluence/ is Delaunay's theorem, in its strong form: a triangulation with
--- no locally illegal diagonal is globally Delaunay. Local normality is thus
--- global normality, the normal form is unique, and every rewrite order reaches
--- it. That is what licenses the arena below to be a LIFO stack rather than a
--- priority queue, and it is why callers may seed it in whatever order is
--- cheapest to produce — the fan first, or the hull turns first, or both
--- interleaved — without any of them changing the mesh that comes out.
---
--- All callers differ only in how they seed the arena; topology mutation and
--- propagation have exactly one owner.
--- The stack top, the maximum top, and the flip count are strict loop
--- variables, returned once when the drain finishes — the mesh reports nothing
--- per candidate, and 'applyFlip' reports nothing at all. The phantom
--- 'KnownProbe' parameter counts popped candidates for the instrumented lane
--- and is erased everywhere else.
---
--- A popped candidate is read once. Turning it against the star vertex, judging
--- it, and rewriting it are three questions about the same two half-edge
--- records, and the apex the turn looks for is the apex the judgement needs, so
--- one pass over the quadrilateral answers all three.
-drainLegalization
-  :: forall p mutable s vertex directed undirected face
-   . (KnownProbe p, MutableTopology mutable)
-  => mutable s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> CandidateDiscipline
-  -> LegalizationLaw
-  -> ST s (Int, Int)
-drainLegalization topology operation seededTop discipline law = do
-  let !mutable = topologyOwner topology
-  -- No constraint can appear during a drain, so a mesh holding none at entry
-  -- never needs the per-candidate protection read.
-  constrained <- readSTRef (mdConstraintCount mutable)
-  initialArena <- legalizationArena operation
-  LegalizationDrain flips maxTop candidates finalArena <-
-    drainLegalizationInArena
-      @p
-      topology
-      initialArena
-      (constrained /= 0)
-      seededTop
-      discipline
-      law
-  when (legalizationArenaLength finalArena /= legalizationArenaLength initialArena) $
-    storeLegalizationArena operation finalArena
-  probeCharge @p operation CounterDiagLegalizationCandidates candidates
-  pure (flips, maxTop)
-{-# INLINE drainLegalization #-}
-
--- | Interpret the single normalization law in an explicitly borrowed arena.
--- The boolean is the already-established constraint obstruction: a fresh
--- unconstrained build passes 'False', while the general entry derives it once
--- from the mutable owner above.
-drainLegalizationInArena
-  :: forall p mutable s vertex directed undirected face
-   . (KnownProbe p, MutableTopology mutable)
-  => mutable s vertex directed undirected face
-  -> LegalizationArena s
-  -> Bool
-  -> Int
-  -> CandidateDiscipline
-  -> LegalizationLaw
-  -> ST s (LegalizationDrain s (ProbeCounter p))
-drainLegalizationInArena topology (LegalizationArena initialArena) guarded seededTop discipline law = do
-  let -- The cavity-repair floor pins the fan border. Ordinary legalization
-      -- pins nothing; seam legalization instead uses its incident-face
-      -- selector below, so both copied and appended pairs obey the same
-      -- protection law.
-      !floorPair = case law of
-        ValidMesh -> 0
-        CavityRepair floorEdge -> floorEdge
-        SeamPreserving _ -> 0
-
-      eligible rawEdge = do
-        unconstrained <-
-          if guarded
-            then not <$> topologyReadConstraint topology rawEdge
-            else pure True
-        if not unconstrained
-          then pure False
-          else
-            case law of
-              SeamPreserving targetFaceProtected -> do
-                leftFace <- topologyReadFace topology rawEdge
-                rightFace <- topologyReadFace topology (reverseIndex rawEdge)
-                pure
-                  ( not (targetFaceProtected leftFace)
-                      && not (targetFaceProtected rightFace)
-                  )
-              _ -> pure (rawEdge `shiftR` 1 >= floorPair)
-
-      loopStar !starVertex !starX !starY !arena !top !maxTop !flips !candidates
-        | top <= 0 = pure (flips, maxTop, candidates, arena)
-        | otherwise = do
-            let !nextTop = top - 1
-            packedWord <- MUV.unsafeRead arena nextTop
-            let !rawEdge = fromIntegral packedWord :: Int
-            mayFire <- eligible rawEdge
-            if not mayFire
-              then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
-              else do
-                let !rawTwin = reverseIndex rawEdge
-                rawFace <- topologyReadFace topology rawEdge
-                rawTwinFace <- topologyReadFace topology rawTwin
-                if rawFace == 0 || rawTwinFace == 0
-                  then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
-                  else do
-                    rawBefore <- topologyReadPrevious topology rawEdge
-                    rawTwinBefore <- topologyReadPrevious topology rawTwin
-                    rawTwinApex <- topologyReadOrigin topology rawTwinBefore
-                    a <- topologyReadOrigin topology rawEdge
-                    b <- topologyReadOrigin topology rawTwin
-                    ax <- topologyReadPointX topology a
-                    ay <- topologyReadPointY topology a
-                    bx <- topologyReadPointX topology b
-                    by <- topologyReadPointY topology b
-                    dx <- topologyReadPointX topology rawTwinApex
-                    dy <- topologyReadPointY topology rawTwinApex
-                    -- A star candidate is oriented against the inserted apex,
-                    -- exactly the premise used by the ordinary insertion
-                    -- legalizer. The two convexity predicates in the generic
-                    -- valid-mesh law merely re-prove that premise for every
-                    -- pop; the in-circle rule alone owns this section.
-                    if not (illegalDiagonal ax ay bx by starX starY dx dy)
-                      then loopStar starVertex starX starY arena nextTop maxTop flips (probeBump @p candidates)
-                      else do
-                        -- Star seeding is directional: the inserted vertex is
-                        -- the previous-origin apex of every candidate. A flip
-                        -- preserves that proof for precisely these two
-                        -- directed neighbours, so neither an undirected tag nor
-                        -- a rediscovery read belongs in this epoch.
-                        rawNext <- topologyReadNext topology rawEdge
-                        rawTwinNext <- topologyReadNext topology rawTwin
-                        applyFlip
-                          topology
-                          rawEdge
-                          rawTwin
-                          rawNext
-                          rawBefore
-                          rawTwinNext
-                          rawTwinBefore
-                          rawFace
-                          rawTwinFace
-                          a
-                          b
-                          starVertex
-                          rawTwinApex
-                        let !addedTop = nextTop + 2
-                        grown <- growValues arena addedTop
-                        MUV.unsafeWrite grown nextTop (packIndex rawTwinBefore)
-                        MUV.unsafeWrite grown (nextTop + 1) (packIndex rawTwinNext)
-                        loopStar
-                          starVertex
-                          starX
-                          starY
-                          grown
-                          addedTop
-                          (max maxTop addedTop)
-                          (flips + 1)
-                          (probeBump @p candidates)
-
-      loopGeneric !arena !top !maxTop !flips !candidates
-        | top <= 0 = pure (flips, maxTop, candidates, arena)
-        | otherwise = do
-            let !nextTop = top - 1
-            packedWord <- MUV.unsafeRead arena nextTop
-            let !edge = fromIntegral packedWord :: Int
-            mayFire <- eligible edge
-            if not mayFire
-              then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
-              else do
-                let !twin = reverseIndex edge
-                leftFace <- topologyReadFace topology edge
-                rightFace <- topologyReadFace topology twin
-                if leftFace == 0 || rightFace == 0
-                  then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
-                  else do
-                    edgePrevious <- topologyReadPrevious topology edge
-                    twinPrevious <- topologyReadPrevious topology twin
-                    c <- topologyReadOrigin topology edgePrevious
-                    d <- topologyReadOrigin topology twinPrevious
-                    a <- topologyReadOrigin topology edge
-                    b <- topologyReadOrigin topology twin
-                    ax <- topologyReadPointX topology a
-                    ay <- topologyReadPointY topology a
-                    bx <- topologyReadPointX topology b
-                    by <- topologyReadPointY topology b
-                    cx <- topologyReadPointX topology c
-                    cy <- topologyReadPointY topology c
-                    dx <- topologyReadPointX topology d
-                    dy <- topologyReadPointY topology d
-                    if not (diagonalFires law ax ay bx by cx cy dx dy)
-                      then loopGeneric arena nextTop maxTop flips (probeBump @p candidates)
-                      else do
-                        edgeNext <- topologyReadNext topology edge
-                        twinNext <- topologyReadNext topology twin
-                        applyFlip
-                          topology
-                          edge
-                          twin
-                          edgeNext
-                          edgePrevious
-                          twinNext
-                          twinPrevious
-                          leftFace
-                          rightFace
-                          a
-                          b
-                          c
-                          d
-                        let !addedTop = nextTop + 4
-                        grown <- growValues arena addedTop
-                        MUV.unsafeWrite grown nextTop (packIndex edgeNext)
-                        MUV.unsafeWrite grown (nextTop + 1) (packIndex edgePrevious)
-                        MUV.unsafeWrite grown (nextTop + 2) (packIndex twinNext)
-                        MUV.unsafeWrite grown (nextTop + 3) (packIndex twinPrevious)
-                        loopGeneric
-                          grown
-                          addedTop
-                          (max maxTop addedTop)
-                          (flips + 1)
-                          (probeBump @p candidates)
-  drained <-
-    case discipline of
-      StarCandidates starVertex -> do
-        starX <- topologyReadPointX topology starVertex
-        starY <- topologyReadPointY topology starVertex
-        loopStar starVertex starX starY initialArena seededTop seededTop 0 (probeZero @p)
-      GenericCandidates ->
-        loopGeneric initialArena seededTop seededTop 0 (probeZero @p)
-  let (!flips, !maxTop, !candidates, !finalArena) = drained
-  pure (LegalizationDrain flips maxTop candidates (LegalizationArena finalArena))
- where
-  growValues :: MUV.MVector s Word32 -> Int -> ST s (MUV.MVector s Word32)
-  growValues values required = do
-    LegalizationArena grown <-
-      growLegalizationArena (LegalizationArena values) required
-    pure grown
-{-# INLINE drainLegalizationInArena #-}
-
--- | The dense, fresh-build interpreter for a star epoch. Fresh construction
--- proves the absence of constrained edges; the borrowed arena is returned so
--- the surrounding sweep can glue several epochs before restoring operation
--- ownership.
-drainDenseUnconstrainedStarLegalization
-  :: DenseMutableDcel s vertex directed undirected face
-  -> LegalizationArena s
-  -> Int
-  -> Int
-  -> ST s (LegalizationDrain s ())
-drainDenseUnconstrainedStarLegalization dense arena top starVertex =
-  drainLegalizationInArena
-    @'ProbeOff
-    dense
-    arena
-    False
-    top
-    (StarCandidates starVertex)
-    ValidMesh
-{-# INLINE drainDenseUnconstrainedStarLegalization #-}
-
--- | The matching dense interpreter for a generic legalization epoch. Keeping
--- both monomorphic boundaries out of the circle-sweep worker prevents the
--- normalizer and flip rewrite from being copied into every insertion
--- continuation; both still descend through 'drainLegalizationInArena'.
-drainDenseUnconstrainedGenericLegalization
-  :: DenseMutableDcel s vertex directed undirected face
-  -> LegalizationArena s
-  -> Int
-  -> ST s (LegalizationDrain s ())
-drainDenseUnconstrainedGenericLegalization dense arena top =
-  drainLegalizationInArena
-    @'ProbeOff
-    dense
-    arena
-    False
-    top
-    GenericCandidates
-    ValidMesh
-{-# INLINE drainDenseUnconstrainedGenericLegalization #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Subdivide.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Subdivide.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Subdivide.hs
+++ /dev/null
@@ -1,284 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Insertion into an existing element: face split and edge split.
-module Moonlight.Triangulation.Internal.DcelOperations.Subdivide
-  ( insertIntoFace
-  , insertOnEdge
-  , splitBoundaryEdgeWithLegalization
-  ) where
-
-import Control.Monad (unless, void, when)
-import Control.Monad.ST (ST)
-import Moonlight.Triangulation.Internal.HandleDefs (UndirectedEdgeId (..))
-import Moonlight.Triangulation.Internal.DcelOperations.Legalize (legalizeScratch)
-import Moonlight.Triangulation.Internal.DcelOperations.Twin (reverseIndex)
-import Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel
-  , addEdgeBlock
-  , addFaceBlock
-  , ensureCellCapacity
-  , faceEdges
-  , linkEdges
-  , markConnected
-  , payloadsPristine
-  , readConstraint
-  , readFace
-  , readNext
-  , readOrigin
-  , readPrevious
-  , resetEdgeData
-  , resetFaceData
-  , setConstraint
-  , setCycle3
-  , writeFace
-  , writeFaceEdge
-  , writeOrigin
-  , writeVertexOut
-  )
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , OperationState
-  , addCounter
-  , writeScratch
-  )
-import Moonlight.Triangulation.Internal.Probe (KnownProbe)
-import Moonlight.Triangulation.Internal.Types (BuildError (..))
-
-insertIntoFace :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-insertIntoFace mutable operation face vertex = do
-  capacity <- ensureCellCapacity mutable 3 2
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> do
-      insertIntoFaceWithCapacity @p mutable operation face vertex
-      pure (Right ())
-
-insertIntoFaceWithCapacity :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
-insertIntoFaceWithCapacity mutable operation face vertex = do
-  (eAB, eBC, eCA) <- faceEdges mutable face
-  a <- readOrigin mutable eAB
-  b <- readOrigin mutable eBC
-  c <- readOrigin mutable eCA
-  edgeBase <- addEdgeBlock mutable 3
-  let !eAV = edgeBase
-      !eVA = edgeBase + 1
-      !eBV = edgeBase + 2
-      !eVB = edgeBase + 3
-      !eCV = edgeBase + 4
-      !eVC = edgeBase + 5
-  writeOrigin mutable eAV a
-  writeOrigin mutable eVA vertex
-  writeOrigin mutable eBV b
-  writeOrigin mutable eVB vertex
-  writeOrigin mutable eCV c
-  writeOrigin mutable eVC vertex
-  faceBase <- addFaceBlock mutable 2
-  setCycle3 mutable face eAB eBV eVA
-  setCycle3 mutable faceBase eBC eCV eVB
-  setCycle3 mutable (faceBase + 1) eCA eAV eVC
-  writeVertexOut mutable a eAB
-  writeVertexOut mutable b eBC
-  writeVertexOut mutable c eCA
-  markConnected mutable vertex eVA
-  -- ABC has become ABV; the other two thirds of it are fresh faces. The three
-  -- boundary edges keep their endpoints and so keep their labels.
-  resetFaceData mutable face
-  writeScratch operation 0 eAB
-  writeScratch operation 1 eBC
-  writeScratch operation 2 eCA
-  addCounter operation CounterFaceSplits 1
-  legalizeScratch @p mutable operation vertex 3
-
-insertOnEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-insertOnEdge mutable operation suppliedEdge vertex = do
-  suppliedFace <- readFace mutable suppliedEdge
-  reverseFace <- readFace mutable (reverseIndex suppliedEdge)
-  if suppliedFace == 0 || reverseFace == 0
-    then splitBoundaryEdge @p mutable operation suppliedEdge vertex
-    else splitInteriorEdge @p mutable operation suppliedEdge vertex
-
-splitInteriorEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-splitInteriorEdge mutable operation suppliedEdge vertex = do
-  capacity <- ensureCellCapacity mutable 3 2
-  case capacity of
-    Left obstruction -> pure (Left obstruction)
-    Right () -> do
-      splitInteriorEdgeWithCapacity @p mutable operation suppliedEdge vertex
-      pure (Right ())
-
-splitInteriorEdgeWithCapacity :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s ()
-splitInteriorEdgeWithCapacity mutable operation suppliedEdge vertex = do
-  protected <- readConstraint mutable suppliedEdge
-  suppliedFace <- readFace mutable suppliedEdge
-  let !edge = if suppliedFace == 0 then reverseIndex suppliedEdge else suppliedEdge
-      !reverseEdgeEdge = reverseIndex edge
-  leftFace <- readFace mutable edge
-  rightFace <- readFace mutable reverseEdgeEdge
-  eBC <- readNext mutable edge
-  eCA <- readPrevious mutable edge
-  eAD <- readNext mutable reverseEdgeEdge
-  eDB <- readPrevious mutable reverseEdgeEdge
-  a <- readOrigin mutable edge
-  b <- readOrigin mutable reverseEdgeEdge
-  c <- readOrigin mutable eCA
-  d <- readOrigin mutable eDB
-  writeOrigin mutable reverseEdgeEdge vertex
-  edgeBase <- addEdgeBlock mutable 3
-  let !eVB = edgeBase
-      !eBV = edgeBase + 1
-      !eVC = edgeBase + 2
-      !eCV = edgeBase + 3
-      !eVD = edgeBase + 4
-      !eDV = edgeBase + 5
-  writeOrigin mutable eVB vertex
-  writeOrigin mutable eBV b
-  writeOrigin mutable eVC vertex
-  writeOrigin mutable eCV c
-  writeOrigin mutable eVD vertex
-  writeOrigin mutable eDV d
-  faceBase <- addFaceBlock mutable 2
-  setCycle3 mutable leftFace edge eVC eCA
-  setCycle3 mutable faceBase eVB eBC eCV
-  setCycle3 mutable rightFace eBV eVD eDB
-  setCycle3 mutable (faceBase + 1) reverseEdgeEdge eAD eDV
-  writeVertexOut mutable a edge
-  writeVertexOut mutable b eBC
-  writeVertexOut mutable c eCA
-  writeVertexOut mutable d eDB
-  markConnected mutable vertex reverseEdgeEdge
-  -- AB became AV, and both incident triangles lost a corner to the new vertex.
-  unless (payloadsPristine mutable) $ do
-    resetEdgeData mutable (edge `quot` 2)
-    resetFaceData mutable leftFace
-    resetFaceData mutable rightFace
-  when protected $ do
-    _ <- setConstraint mutable eVB
-    pure ()
-  writeScratch operation 0 eCA
-  writeScratch operation 1 eBC
-  writeScratch operation 2 eDB
-  writeScratch operation 3 eAD
-  addCounter operation CounterInteriorEdgeSplits 1
-  legalizeScratch @p mutable operation vertex 4
-
-splitBoundaryEdge :: forall p s vertex directed undirected face. KnownProbe p => MutableDcel s vertex directed undirected face -> OperationState s -> Int -> Int -> ST s (Either BuildError ())
-splitBoundaryEdge mutable operation suppliedEdge vertex = do
-  outcome <-
-    splitBoundaryEdgeWithLegalization
-      (\target targetOperation insertedVertex _ _ ->
-         legalizeScratch @p target targetOperation insertedVertex 2
-      )
-      mutable
-      operation
-      suppliedEdge
-      vertex
-  pure (void outcome)
-
--- | Split one hull edge and return its two replacement outer edges in the
--- original outer-cycle order.  Topology and constraint inheritance have one
--- owner; callers choose only the lawful legalization boundary.  Ordinary
--- insertion drains the new vertex's scratch star, while a source-preserving
--- seam can pin its immutable face section.
-splitBoundaryEdgeWithLegalization
-  :: ( MutableDcel s vertex directed undirected face
-       -> OperationState s
-       -> Int
-       -> Int
-       -> Int
-       -> ST s ()
-     )
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Either BuildError (Int, Int))
-splitBoundaryEdgeWithLegalization legalize mutable operation suppliedEdge vertex = do
-  suppliedFace <- readFace mutable suppliedEdge
-  reverseFace <- readFace mutable (reverseIndex suppliedEdge)
-  if suppliedFace /= 0 && reverseFace /= 0
-    then
-      pure
-        ( Left
-            ( BoundarySplitRequiresBoundaryEdge
-                (UndirectedEdgeId (fromIntegral (suppliedEdge `quot` 2)))
-            )
-        )
-    else do
-      capacity <- ensureCellCapacity mutable 2 1
-      case capacity of
-        Left obstruction -> pure (Left obstruction)
-        Right () ->
-          Right
-            <$> splitBoundaryEdgeWithCapacity
-              legalize
-              mutable
-              operation
-              suppliedEdge
-              vertex
-
-splitBoundaryEdgeWithCapacity
-  :: ( MutableDcel s vertex directed undirected face
-       -> OperationState s
-       -> Int
-       -> Int
-       -> Int
-       -> ST s ()
-     )
-  -> MutableDcel s vertex directed undirected face
-  -> OperationState s
-  -> Int
-  -> Int
-  -> ST s (Int, Int)
-splitBoundaryEdgeWithCapacity legalize mutable operation suppliedEdge vertex = do
-  protected <- readConstraint mutable suppliedEdge
-  suppliedFace <- readFace mutable suppliedEdge
-  let !edge = if suppliedFace == 0 then reverseIndex suppliedEdge else suppliedEdge
-      !outerEdge = reverseIndex edge
-  innerFace <- readFace mutable edge
-  eBC <- readNext mutable edge
-  eCA <- readPrevious mutable edge
-  a <- readOrigin mutable edge
-  b <- readOrigin mutable outerEdge
-  c <- readOrigin mutable eCA
-  oldOuterPrevious <- readPrevious mutable outerEdge
-  oldOuterNext <- readNext mutable outerEdge
-  writeOrigin mutable outerEdge vertex
-  edgeBase <- addEdgeBlock mutable 2
-  let !eVB = edgeBase
-      !eBV = edgeBase + 1
-      !eVC = edgeBase + 2
-      !eCV = edgeBase + 3
-  writeOrigin mutable eVB vertex
-  writeOrigin mutable eBV b
-  writeOrigin mutable eVC vertex
-  writeOrigin mutable eCV c
-  newFace <- addFaceBlock mutable 1
-  setCycle3 mutable innerFace edge eVC eCA
-  setCycle3 mutable newFace eVB eBC eCV
-  writeFace mutable outerEdge 0
-  writeFace mutable eBV 0
-  linkEdges mutable oldOuterPrevious eBV
-  linkEdges mutable eBV outerEdge
-  linkEdges mutable outerEdge oldOuterNext
-  writeFaceEdge mutable 0 outerEdge
-  writeVertexOut mutable a edge
-  writeVertexOut mutable b eBC
-  writeVertexOut mutable c eCA
-  markConnected mutable vertex eVB
-  -- AB became AV and the one interior triangle lost a corner. The outer face
-  -- is not an element and keeps nothing to lose.
-  unless (payloadsPristine mutable) $ do
-    resetEdgeData mutable (edge `quot` 2)
-    resetFaceData mutable innerFace
-  when protected $ do
-    _ <- setConstraint mutable eVB
-    pure ()
-  writeScratch operation 0 eCA
-  writeScratch operation 1 eBC
-  addCounter operation CounterBoundaryEdgeSplits 1
-  legalize mutable operation vertex eCA eBC
-  pure (eBV, outerEdge)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Twin.hs b/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Twin.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/DcelOperations/Twin.hs
+++ /dev/null
@@ -1,17 +0,0 @@
-{-# LANGUAGE AllowAmbiguousTypes #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | The pairing involution on half-edge indices.
-module Moonlight.Triangulation.Internal.DcelOperations.Twin
-  ( reverseIndex
-  ) where
-
-import Data.Bits (xor)
-
-reverseIndex :: Int -> Int
-reverseIndex edge = edge `xor` 1
-{-# INLINE reverseIndex #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/ExactSegmentEvents.hs b/src-dcel/Moonlight/Triangulation/Internal/ExactSegmentEvents.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/ExactSegmentEvents.hs
+++ /dev/null
@@ -1,890 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | One exact owner for non-disjoint relations and split points in a finite
--- segment family. Collinear intervals descend by supporting-line sections;
--- non-collinear intersections descend through an immutable Bentley--Ottmann
--- status tree. Callers attach provenance only after this geometry glues.
-module Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSweepSegmentId (..)
-  , ExactSegmentEvent (..)
-  , ExactSegmentEventObstruction (..)
-  , ExactSegmentEventPlan
-  , exactSegmentEventPlan
-  , exactSegmentEvents
-  , exactSegmentSplitPoints
-  , exactSegmentRelationMap
-  , exactSegmentPairChecks
-  , exactSegmentSweepMaximumHeight
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Applicative ((<|>))
-import Control.Monad (filterM, foldM)
-import Data.List (sortBy)
-import qualified Data.List as List
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as V
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactIntersectionError
-  , ExactPoint
-  , ExactSegment
-  , SegmentRelation (..)
-  , exactOnClosedSegment
-  , exactPointCoordinates
-  , exactSegmentEndpoints
-  , exactSegmentRelation
-  , exactSupportingLineIntersection
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( orderedPair
-  , unorderedPairs
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRationalDenominator
-  , exactRationalNumerator
-  )
-
-newtype ExactSweepSegmentId = ExactSweepSegmentId Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data ExactSegmentEvent
-  = ExactProperCrossing !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
-  | ExactEndpointTouch !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
-  | ExactSharedEndpoint !ExactSweepSegmentId !ExactSweepSegmentId !ExactPoint
-  | ExactDuplicateSegments !ExactSweepSegmentId !ExactSweepSegmentId
-  | ExactCollinearOverlap
-      !ExactSweepSegmentId
-      !ExactSweepSegmentId
-      !ExactPoint
-      !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data ExactSegmentEventObstruction
-  = ExactSweepIntersectionObstruction
-      !ExactSweepSegmentId
-      !ExactSweepSegmentId
-      !ExactIntersectionError
-  | ExactSweepRelationWitnessMissing
-      !ExactSweepSegmentId
-      !ExactSweepSegmentId
-      !SegmentRelation
-  | ExactSweepSegmentMissing !ExactSweepSegmentId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data ExactSegmentEventPlan = ExactSegmentEventPlan
-  { plannedEvents :: !(Map (ExactSweepSegmentId, ExactSweepSegmentId) ExactSegmentEvent)
-  , plannedSplitPoints :: !(V.Vector [ExactPoint])
-  , plannedPairChecks :: !Int
-  , plannedMaximumHeight :: !Int
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data SegmentMeta = SegmentMeta
-  { metaId :: !ExactSweepSegmentId
-  , metaSegment :: !ExactSegment
-  , metaLow :: !ExactPoint
-  , metaHigh :: !ExactPoint
-  , metaVertical :: !Bool
-  }
-
-data SupportingLine = SupportingLine !Integer !Integer !Integer
-  deriving stock (Eq, Ord, Show)
-
-data EventBundle = EventBundle
-  { eventStarts :: !(Set ExactSweepSegmentId)
-  , eventEnds :: !(Set ExactSweepSegmentId)
-  , eventScheduled :: !(Set (ExactSweepSegmentId, ExactSweepSegmentId))
-  }
-
-emptyEventBundle :: EventBundle
-emptyEventBundle = EventBundle Set.empty Set.empty Set.empty
-
-mergeEventBundle :: EventBundle -> EventBundle -> EventBundle
-mergeEventBundle left right =
-  EventBundle
-    { eventStarts = eventStarts left <> eventStarts right
-    , eventEnds = eventEnds left <> eventEnds right
-    , eventScheduled = eventScheduled left <> eventScheduled right
-    }
-
-data EventAccumulation = EventAccumulation
-  { accumulatedEvents :: !(Map (ExactSweepSegmentId, ExactSweepSegmentId) ExactSegmentEvent)
-  , accumulatedSplits :: !(IntMap.IntMap (Set ExactPoint))
-  , accumulatedPairChecks :: !Int
-  , accumulatedMaximumHeight :: !Int
-  }
-
-data StatusTree
-  = StatusEmpty
-  | StatusNode !Int !StatusTree !SegmentMeta !StatusTree
-
-data PointChange = PointChange
-  { changedPoint :: !ExactPoint
-  , changedContinuing :: !(Set ExactSweepSegmentId)
-  }
-
-data StatusSide = StatusBefore | StatusAfter
-  deriving stock (Eq)
-
--- | Build the complete exact event plan. The private sweep reports its maximum
--- AVL height and relation checks so benchmarks can distinguish output growth
--- from residual orchestration.
-exactSegmentEventPlan
-  :: V.Vector ExactSegment
-  -> Either ExactSegmentEventObstruction ExactSegmentEventPlan
-exactSegmentEventPlan segments = do
-  let metas = V.imap segmentMeta segments
-      initialAccumulation =
-        EventAccumulation
-          { accumulatedEvents = Map.empty
-          , accumulatedSplits = IntMap.empty
-          , accumulatedPairChecks = 0
-          , accumulatedMaximumHeight = 0
-          }
-  let lineGroups = supportingLineGroups metas
-  afterCollinear <- foldM recordCollinearGroup initialAccumulation (Map.elems lineGroups)
-  let initialQueue = V.foldl' insertEndpointEvents Map.empty metas
-  completed <- sweep metas initialQueue StatusEmpty afterCollinear
-  pure
-    ExactSegmentEventPlan
-      { plannedEvents = accumulatedEvents completed
-      , plannedSplitPoints = finalizeSplitPoints segments (accumulatedSplits completed)
-      , plannedPairChecks = accumulatedPairChecks completed
-      , plannedMaximumHeight = accumulatedMaximumHeight completed
-      }
-
-finalizeSplitPoints
-  :: V.Vector ExactSegment
-  -> IntMap.IntMap (Set ExactPoint)
-  -> V.Vector [ExactPoint]
-finalizeSplitPoints segments splitPoints =
-  V.imap
-    (\index segment ->
-       let (from, to) = exactSegmentEndpoints segment
-           eventPoints = IntMap.findWithDefault Set.empty index splitPoints
-        in sortAlong segment (Set.toList (Set.insert from (Set.insert to eventPoints))))
-    segments
-
-segmentMeta :: Int -> ExactSegment -> SegmentMeta
-segmentMeta index segment =
-  let (firstPoint, secondPoint) = exactSegmentEndpoints segment
-      low = min firstPoint secondPoint
-      high = max firstPoint secondPoint
-      (lowX, _) = exactPointCoordinates low
-      (highX, _) = exactPointCoordinates high
-   in SegmentMeta
-        { metaId = ExactSweepSegmentId index
-        , metaSegment = segment
-        , metaLow = low
-        , metaHigh = high
-        , metaVertical = lowX == highX
-        }
-
-supportingLineGroups
-  :: V.Vector SegmentMeta
-  -> Map SupportingLine [SegmentMeta]
-supportingLineGroups =
-  V.foldl'
-    (\groups meta -> Map.insertWith (<>) (supportingLine meta) [meta] groups)
-    Map.empty
-
-supportingLine :: SegmentMeta -> SupportingLine
-supportingLine meta =
-  let (from, to) = exactSegmentEndpoints (metaSegment meta)
-      (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-      deltaX = toX - fromX
-      deltaY = toY - fromY
-      a = deltaY
-      b = negate deltaX
-      c = deltaX * fromY - deltaY * fromX
-      denominators = map exactRationalDenominator [a, b, c]
-      commonDenominator = List.foldl' lcm 1 denominators
-      integerCoefficient coefficient =
-        exactRationalNumerator coefficient
-          * (commonDenominator `quot` exactRationalDenominator coefficient)
-      integerA = integerCoefficient a
-      integerB = integerCoefficient b
-      integerC = integerCoefficient c
-      commonDivisor = gcd (abs integerA) (gcd (abs integerB) (abs integerC))
-      sign
-        | integerA < 0 = -1
-        | integerA == 0 && integerB < 0 = -1
-        | otherwise = 1
-      normalize coefficient = sign * (coefficient `quot` commonDivisor)
-   in SupportingLine (normalize integerA) (normalize integerB) (normalize integerC)
-
-recordCollinearGroup
-  :: EventAccumulation
-  -> [SegmentMeta]
-  -> Either ExactSegmentEventObstruction EventAccumulation
-recordCollinearGroup initial metas =
-  fst <$> foldM descend (initial, []) ordered
- where
-  ordered = sortBy compareInterval metas
-  compareInterval left right =
-    compare (metaLow left, metaHigh left, metaId left) (metaLow right, metaHigh right, metaId right)
-  descend (accumulation, active) current = do
-    let retained = filter (\candidate -> metaHigh candidate >= metaLow current) active
-    updated <-
-      foldM
-        (\accumulated candidate -> recordRelation accumulated candidate current)
-        accumulation
-        retained
-    pure (updated, current : retained)
-
-insertEndpointEvents
-  :: Map ExactPoint EventBundle
-  -> SegmentMeta
-  -> Map ExactPoint EventBundle
-insertEndpointEvents queue meta =
-  insertBundle (metaHigh meta) (emptyEventBundle{eventEnds = Set.singleton (metaId meta)})
-    (insertBundle (metaLow meta) (emptyEventBundle{eventStarts = Set.singleton (metaId meta)}) queue)
-
-insertBundle
-  :: ExactPoint
-  -> EventBundle
-  -> Map ExactPoint EventBundle
-  -> Map ExactPoint EventBundle
-insertBundle = Map.insertWith mergeEventBundle
-
-sweep
-  :: V.Vector SegmentMeta
-  -> Map ExactPoint EventBundle
-  -> StatusTree
-  -> EventAccumulation
-  -> Either ExactSegmentEventObstruction EventAccumulation
-sweep metas queue status accumulation =
-  case Map.lookupMin queue of
-    Nothing -> Right accumulation
-    Just (firstPoint, _) -> do
-      let (currentX, _) = exactPointCoordinates firstPoint
-          (batch, laterQueue) =
-            Map.spanAntitone
-              (\point -> fst (exactPointCoordinates point) == currentX)
-              queue
-          startedIds =
-            Set.toAscList
-              (Map.foldl' (\ids bundle -> ids <> eventStarts bundle) Set.empty batch)
-      started <- traverse (requireMeta metas) startedIds
-      let startingIds = map metaId (filter (not . metaVertical) started)
-          verticalIds = map metaId (filter metaVertical started)
-      withVerticals <-
-        recordVerticalRelations
-          metas
-          currentX
-          startingIds
-          verticalIds
-          status
-          accumulation
-      (withEvents, removals, insertions, changes) <-
-        foldM
-          (processPoint metas status)
-          (withVerticals, Set.empty, Set.empty, [])
-          (Map.toAscList batch)
-      statusWithout <-
-        foldM
-          (deleteStatus metas StatusBefore currentX)
-          status
-          (Set.toAscList removals)
-      statusAfter <-
-        foldM
-          (insertStatus metas StatusAfter currentX)
-          statusWithout
-          (Set.toAscList insertions)
-      (scheduledQueue, scheduledAccumulation) <-
-        foldM
-          (scheduleAroundChange metas currentX statusAfter)
-          (laterQueue, withEvents)
-          changes
-      let measured =
-            scheduledAccumulation
-              { accumulatedMaximumHeight =
-                  max
-                    (accumulatedMaximumHeight scheduledAccumulation)
-                    (statusHeight statusAfter)
-              }
-      sweep metas scheduledQueue statusAfter measured
-
-recordVerticalRelations
-  :: V.Vector SegmentMeta
-  -> ExactRational
-  -> [ExactSweepSegmentId]
-  -> [ExactSweepSegmentId]
-  -> StatusTree
-  -> EventAccumulation
-  -> Either ExactSegmentEventObstruction EventAccumulation
-recordVerticalRelations metas currentX startingIds verticalIds status accumulation = do
-  temporaryStatus <-
-    foldM (insertStatus metas StatusAfter currentX) status startingIds
-  foldM (recordVertical temporaryStatus) accumulation verticalIds
- where
-  recordVertical temporaryStatus accumulated verticalId = do
-    vertical <- requireMeta metas verticalId
-    let (_, lowY) = exactPointCoordinates (metaLow vertical)
-        (_, highY) = exactPointCoordinates (metaHigh vertical)
-        candidates = statusRangeByY currentX lowY highY temporaryStatus
-    foldM
-      (\current candidateId -> do
-         candidate <- requireMeta metas candidateId
-         recordRelation current vertical candidate)
-      accumulated
-      candidates
-
-processPoint
-  :: V.Vector SegmentMeta
-  -> StatusTree
-  -> ( EventAccumulation
-     , Set ExactSweepSegmentId
-     , Set ExactSweepSegmentId
-     , [PointChange]
-     )
-  -> (ExactPoint, EventBundle)
-  -> Either
-      ExactSegmentEventObstruction
-      ( EventAccumulation
-      , Set ExactSweepSegmentId
-      , Set ExactSweepSegmentId
-      , [PointChange]
-      )
-processPoint metas status (accumulation, removals, insertions, changes) (point, bundle) = do
-  let (x, y) = exactPointCoordinates point
-      activeAtPoint = Set.fromList (statusAtY x y status)
-      scheduledIds =
-        Set.fromList
-          [ segmentId
-          | (leftId, rightId) <- Set.toList (eventScheduled bundle)
-          , segmentId <- [leftId, rightId]
-          ]
-      candidates = eventStarts bundle <> eventEnds bundle <> activeAtPoint <> scheduledIds
-  incident <-
-    Set.fromList
-      <$> filterM
-        (\segmentId -> do
-           meta <- requireMeta metas segmentId
-           let (from, to) = exactSegmentEndpoints (metaSegment meta)
-           pure (exactOnClosedSegment from to point))
-        (Set.toAscList candidates)
-  withRelations <- recordIncidentPairs metas accumulation incident
-  metasAtPoint <- traverse (requireMeta metas) (Set.toAscList incident)
-  let removable =
-        Set.fromList
-          [ metaId meta
-          | meta <- metasAtPoint
-          , not (metaVertical meta)
-          , Set.member (metaId meta) activeAtPoint
-              || Set.member (metaId meta) (eventEnds bundle)
-          ]
-      continuing =
-        Set.fromList
-          [ metaId meta
-          | meta <- metasAtPoint
-          , not (metaVertical meta)
-          , fst (exactPointCoordinates (metaHigh meta)) > x
-          ]
-  pure
-    ( withRelations
-    , removals <> removable
-    , insertions <> continuing
-    , PointChange point continuing : changes
-    )
-
-recordIncidentPairs
-  :: V.Vector SegmentMeta
-  -> EventAccumulation
-  -> Set ExactSweepSegmentId
-  -> Either ExactSegmentEventObstruction EventAccumulation
-recordIncidentPairs metas initial incident =
-  foldM
-    (\accumulation (leftId, rightId) -> do
-       left <- requireMeta metas leftId
-       right <- requireMeta metas rightId
-       recordRelation accumulation left right)
-    initial
-    (unorderedPairs (Set.toAscList incident))
-
-recordRelation
-  :: EventAccumulation
-  -> SegmentMeta
-  -> SegmentMeta
-  -> Either ExactSegmentEventObstruction EventAccumulation
-recordRelation accumulation firstMeta secondMeta =
-  let (leftMeta, rightMeta) =
-        if metaId firstMeta <= metaId secondMeta
-          then (firstMeta, secondMeta)
-          else (secondMeta, firstMeta)
-      leftId = metaId leftMeta
-      rightId = metaId rightMeta
-      relation = relationOf (metaSegment leftMeta) (metaSegment rightMeta)
-      checked = accumulation{accumulatedPairChecks = accumulatedPairChecks accumulation + 1}
-   in case relation of
-        SegmentsDisjoint -> Right checked
-        _ -> do
-          (event, splitPoints) <-
-            relationEvent leftId rightId (metaSegment leftMeta) (metaSegment rightMeta) relation
-          let ExactSweepSegmentId leftIndex = leftId
-              ExactSweepSegmentId rightIndex = rightId
-              splitSet = Set.fromList splitPoints
-              withLeft =
-                IntMap.insertWith Set.union leftIndex splitSet (accumulatedSplits checked)
-              withBoth = IntMap.insertWith Set.union rightIndex splitSet withLeft
-          Right
-            checked
-              { accumulatedEvents =
-                  Map.insert
-                    (leftId, rightId)
-                    event
-                    (accumulatedEvents checked)
-              , accumulatedSplits = withBoth
-              }
-
-relationOf :: ExactSegment -> ExactSegment -> SegmentRelation
-relationOf left right =
-  let (a, b) = exactSegmentEndpoints left
-      (c, d) = exactSegmentEndpoints right
-   in exactSegmentRelation a b c d
-
-relationEvent
-  :: ExactSweepSegmentId
-  -> ExactSweepSegmentId
-  -> ExactSegment
-  -> ExactSegment
-  -> SegmentRelation
-  -> Either ExactSegmentEventObstruction (ExactSegmentEvent, [ExactPoint])
-relationEvent leftId rightId left right relation =
-  case relation of
-    SegmentsDisjoint -> missing
-    SegmentsDuplicate -> Right (ExactDuplicateSegments leftId rightId, [])
-    SegmentsProperlyCross -> do
-      crossing <-
-        either
-          (Left . ExactSweepIntersectionObstruction leftId rightId)
-          Right
-          (exactSupportingLineIntersection left right)
-      Right (ExactProperCrossing leftId rightId crossing, [crossing])
-    SegmentsShareEndpoint ->
-      uniqueWitness (ExactSharedEndpoint leftId rightId) (uniqueShared left right)
-    SegmentEndpointTouchesInterior ->
-      uniqueWitness (ExactEndpointTouch leftId rightId) (uniqueTouch left right)
-    SegmentsCollinearlyOverlap ->
-      let lower = max (min a b) (min c d)
-          upper = min (max a b) (max c d)
-       in if lower < upper
-            then Right (ExactCollinearOverlap leftId rightId lower upper, [lower, upper])
-            else missing
- where
-  (a, b) = exactSegmentEndpoints left
-  (c, d) = exactSegmentEndpoints right
-  uniqueWitness make witness =
-    case witness of
-      Just point -> Right (make point, [point])
-      Nothing -> missing
-  missing = Left (ExactSweepRelationWitnessMissing leftId rightId relation)
-
-scheduleAroundChange
-  :: V.Vector SegmentMeta
-  -> ExactRational
-  -> StatusTree
-  -> (Map ExactPoint EventBundle, EventAccumulation)
-  -> PointChange
-  -> Either
-      ExactSegmentEventObstruction
-      (Map ExactPoint EventBundle, EventAccumulation)
-scheduleAroundChange metas currentX status state change =
-  case Set.toAscList (changedContinuing change) of
-    [] ->
-      let (_, y) = exactPointCoordinates (changedPoint change)
-          (below, above) = statusBelowAbove currentX y status
-       in scheduleMaybePair metas currentX below above state
-    continuingIds -> do
-      continuing <- traverse (requireMeta metas) continuingIds
-      let ordered = sortBy (statusCompare StatusAfter currentX) continuing
-      case ordered of
-        [] -> Right state
-        lowest : remaining ->
-          let highest = List.foldl' (\_ current -> current) lowest remaining
-              below = statusPredecessor currentX lowest status
-              above = statusSuccessor currentX highest status
-           in scheduleMaybePair metas currentX below (Just (metaId lowest)) state
-                >>= scheduleMaybePair metas currentX (Just (metaId highest)) above
-
-scheduleMaybePair
-  :: V.Vector SegmentMeta
-  -> ExactRational
-  -> Maybe ExactSweepSegmentId
-  -> Maybe ExactSweepSegmentId
-  -> (Map ExactPoint EventBundle, EventAccumulation)
-  -> Either
-      ExactSegmentEventObstruction
-      (Map ExactPoint EventBundle, EventAccumulation)
-scheduleMaybePair _ _ Nothing _ state = Right state
-scheduleMaybePair _ _ _ Nothing state = Right state
-scheduleMaybePair metas currentX (Just firstId) (Just secondId) (queue, accumulation)
-  | firstId == secondId = Right (queue, accumulation)
-  | otherwise = do
-      firstMeta <- requireMeta metas firstId
-      secondMeta <- requireMeta metas secondId
-      let relation = relationOf (metaSegment firstMeta) (metaSegment secondMeta)
-          checked = accumulation{accumulatedPairChecks = accumulatedPairChecks accumulation + 1}
-      witness <-
-        either
-          (Left . ExactSweepIntersectionObstruction firstId secondId)
-          Right
-          (relationWitnessPoint (metaSegment firstMeta) (metaSegment secondMeta) relation)
-      case witness of
-        Just point
-          | fst (exactPointCoordinates point) > currentX ->
-              let pair = orderedPair firstId secondId
-                  bundle = emptyEventBundle{eventScheduled = Set.singleton pair}
-               in Right (insertBundle point bundle queue, checked)
-        _ -> Right (queue, checked)
-
-relationWitnessPoint
-  :: ExactSegment
-  -> ExactSegment
-  -> SegmentRelation
-  -> Either ExactIntersectionError (Maybe ExactPoint)
-relationWitnessPoint left right relation =
-  case relation of
-    SegmentsProperlyCross -> Just <$> exactSupportingLineIntersection left right
-    SegmentEndpointTouchesInterior -> Right (uniqueTouch left right)
-    SegmentsShareEndpoint -> Right (uniqueShared left right)
-    _ -> Right Nothing
-
-uniqueShared :: ExactSegment -> ExactSegment -> Maybe ExactPoint
-uniqueShared left right =
-  let (a, b) = exactSegmentEndpoints left
-      (c, d) = exactSegmentEndpoints right
-   in case Set.toAscList (Set.intersection (Set.fromList [a, b]) (Set.fromList [c, d])) of
-        [point] -> Just point
-        _ -> Nothing
-
-uniqueTouch :: ExactSegment -> ExactSegment -> Maybe ExactPoint
-uniqueTouch left right =
-  let (a, b) = exactSegmentEndpoints left
-      (c, d) = exactSegmentEndpoints right
-      points =
-        Set.toAscList
-          ( Set.fromList
-              ( [point | point <- [a, b], exactOnClosedSegment c d point]
-                  <> [point | point <- [c, d], exactOnClosedSegment a b point]
-              )
-          )
-   in case points of
-        [point] -> Just point
-        _ -> Nothing
-
-statusCompare
-  :: StatusSide
-  -> ExactRational
-  -> SegmentMeta
-  -> SegmentMeta
-  -> Ordering
-statusCompare side x left right =
-  case compareOrdinateAt x left right of
-    EQ ->
-      case compareSlope left right of
-        EQ -> compare (metaId left) (metaId right)
-        slopeOrder -> if side == StatusAfter then slopeOrder else invertOrdering slopeOrder
-    order -> order
-
-compareOrdinateAt :: ExactRational -> SegmentMeta -> SegmentMeta -> Ordering
-compareOrdinateAt x left right =
-  let (leftNumerator, leftDenominator) = ordinateFraction x left
-      (rightNumerator, rightDenominator) = ordinateFraction x right
-   in compare
-        (leftNumerator * rightDenominator)
-        (rightNumerator * leftDenominator)
-
-compareSlope :: SegmentMeta -> SegmentMeta -> Ordering
-compareSlope left right =
-  let (leftRise, leftRun) = slopeFraction left
-      (rightRise, rightRun) = slopeFraction right
-   in compare
-        (leftRise * rightRun)
-        (rightRise * leftRun)
-
-ordinateFraction :: ExactRational -> SegmentMeta -> (ExactRational, ExactRational)
-ordinateFraction x meta =
-  let (fromX, fromY) = exactPointCoordinates (metaLow meta)
-      (toX, toY) = exactPointCoordinates (metaHigh meta)
-      run = toX - fromX
-      numerator = fromY * run + (x - fromX) * (toY - fromY)
-   in (numerator, run)
-
-slopeFraction :: SegmentMeta -> (ExactRational, ExactRational)
-slopeFraction meta =
-  let (fromX, fromY) = exactPointCoordinates (metaLow meta)
-      (toX, toY) = exactPointCoordinates (metaHigh meta)
-   in (toY - fromY, toX - fromX)
-
-invertOrdering :: Ordering -> Ordering
-invertOrdering LT = GT
-invertOrdering EQ = EQ
-invertOrdering GT = LT
-
-insertStatus
-  :: V.Vector SegmentMeta
-  -> StatusSide
-  -> ExactRational
-  -> StatusTree
-  -> ExactSweepSegmentId
-  -> Either ExactSegmentEventObstruction StatusTree
-insertStatus metas side x tree segmentId = do
-  meta <- requireMeta metas segmentId
-  pure (statusInsert (statusCompare side x) meta tree)
-
-deleteStatus
-  :: V.Vector SegmentMeta
-  -> StatusSide
-  -> ExactRational
-  -> StatusTree
-  -> ExactSweepSegmentId
-  -> Either ExactSegmentEventObstruction StatusTree
-deleteStatus metas side x tree segmentId = do
-  meta <- requireMeta metas segmentId
-  pure (statusDelete (statusCompare side x) meta tree)
-
-statusInsert
-  :: (SegmentMeta -> SegmentMeta -> Ordering)
-  -> SegmentMeta
-  -> StatusTree
-  -> StatusTree
-statusInsert compareIds value tree =
-  case tree of
-    StatusEmpty -> statusNode StatusEmpty value StatusEmpty
-    StatusNode _ left current right ->
-      case compareIds value current of
-        LT -> statusBalance (statusNode (statusInsert compareIds value left) current right)
-        GT -> statusBalance (statusNode left current (statusInsert compareIds value right))
-        EQ -> tree
-
-statusDelete
-  :: (SegmentMeta -> SegmentMeta -> Ordering)
-  -> SegmentMeta
-  -> StatusTree
-  -> StatusTree
-statusDelete compareIds value tree =
-  case tree of
-    StatusEmpty -> StatusEmpty
-    StatusNode _ left current right ->
-      case compareIds value current of
-        LT -> statusBalance (statusNode (statusDelete compareIds value left) current right)
-        GT -> statusBalance (statusNode left current (statusDelete compareIds value right))
-        EQ -> statusMerge left right
-
-statusMerge :: StatusTree -> StatusTree -> StatusTree
-statusMerge left StatusEmpty = left
-statusMerge left right =
-  case statusDeleteLeast right of
-    Nothing -> left
-    Just (least, remaining) -> statusBalance (statusNode left least remaining)
-
-statusDeleteLeast :: StatusTree -> Maybe (SegmentMeta, StatusTree)
-statusDeleteLeast StatusEmpty = Nothing
-statusDeleteLeast (StatusNode _ StatusEmpty value right) = Just (value, right)
-statusDeleteLeast (StatusNode _ left value right) = do
-  (least, remaining) <- statusDeleteLeast left
-  pure (least, statusBalance (statusNode remaining value right))
-
-statusHeight :: StatusTree -> Int
-statusHeight StatusEmpty = 0
-statusHeight (StatusNode height _ _ _) = height
-
-statusNode :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
-statusNode left value right =
-  StatusNode
-    (1 + max (statusHeight left) (statusHeight right))
-    left
-    value
-    right
-
-statusBalance :: StatusTree -> StatusTree
-statusBalance tree =
-  case tree of
-    StatusEmpty -> StatusEmpty
-    StatusNode _ left value right
-      | statusHeight left - statusHeight right > 1 -> balanceLeft left value right
-      | statusHeight right - statusHeight left > 1 -> balanceRight left value right
-      | otherwise -> statusNode left value right
-
-balanceLeft :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
-balanceLeft left value right =
-  case left of
-    StatusNode _ leftLeft leftValue leftRight
-      | statusHeight leftLeft >= statusHeight leftRight ->
-          statusNode leftLeft leftValue (statusNode leftRight value right)
-      | otherwise ->
-          case leftRight of
-            StatusNode _ middleLeft middleValue middleRight ->
-              statusNode
-                (statusNode leftLeft leftValue middleLeft)
-                middleValue
-                (statusNode middleRight value right)
-            StatusEmpty -> statusNode left value right
-    StatusEmpty -> statusNode left value right
-
-balanceRight :: StatusTree -> SegmentMeta -> StatusTree -> StatusTree
-balanceRight left value right =
-  case right of
-    StatusNode _ rightLeft rightValue rightRight
-      | statusHeight rightRight >= statusHeight rightLeft ->
-          statusNode (statusNode left value rightLeft) rightValue rightRight
-      | otherwise ->
-          case rightLeft of
-            StatusNode _ middleLeft middleValue middleRight ->
-              statusNode
-                (statusNode left value middleLeft)
-                middleValue
-                (statusNode middleRight rightValue rightRight)
-            StatusEmpty -> statusNode left value right
-    StatusEmpty -> statusNode left value right
-
-statusAtY
-  :: ExactRational
-  -> ExactRational
-  -> StatusTree
-  -> [ExactSweepSegmentId]
-statusAtY x y = descend
- where
-  descend StatusEmpty = []
-  descend (StatusNode _ left meta right) =
-    case compareMetaToY x y meta of
-      LT -> descend right
-      GT -> descend left
-      EQ -> descend left <> [metaId meta] <> descend right
-
-statusRangeByY
-  :: ExactRational
-  -> ExactRational
-  -> ExactRational
-  -> StatusTree
-  -> [ExactSweepSegmentId]
-statusRangeByY x lower upper = descend
- where
-  descend StatusEmpty = []
-  descend (StatusNode _ left meta right) =
-    let below = compareMetaToY x lower meta == LT
-        above = compareMetaToY x upper meta == GT
-     in if below
-          then descend right
-          else
-            if above
-              then descend left
-              else descend left <> [metaId meta] <> descend right
-
-compareMetaToY :: ExactRational -> ExactRational -> SegmentMeta -> Ordering
-compareMetaToY x y meta =
-  let (numerator, denominator) = ordinateFraction x meta
-   in compare numerator (y * denominator)
-
-statusBelowAbove
-  :: ExactRational
-  -> ExactRational
-  -> StatusTree
-  -> (Maybe ExactSweepSegmentId, Maybe ExactSweepSegmentId)
-statusBelowAbove x y = descend Nothing Nothing
- where
-  descend below above StatusEmpty = (below, above)
-  descend below above (StatusNode _ left meta right) =
-    case compareMetaToY x y meta of
-      LT -> descend (Just (metaId meta)) above right
-      GT -> descend below (Just (metaId meta)) left
-      EQ -> (statusGreatest left <|> below, statusLeast right <|> above)
-
-statusPredecessor
-  :: ExactRational
-  -> SegmentMeta
-  -> StatusTree
-  -> Maybe ExactSweepSegmentId
-statusPredecessor x target = descend Nothing
- where
-  descend candidate StatusEmpty = candidate
-  descend candidate (StatusNode _ left current right) =
-    case statusCompare StatusAfter x target current of
-      LT -> descend candidate left
-      GT -> descend (Just (metaId current)) right
-      EQ -> statusGreatest left <|> candidate
-
-statusSuccessor
-  :: ExactRational
-  -> SegmentMeta
-  -> StatusTree
-  -> Maybe ExactSweepSegmentId
-statusSuccessor x target = descend Nothing
- where
-  descend candidate StatusEmpty = candidate
-  descend candidate (StatusNode _ left current right) =
-    case statusCompare StatusAfter x target current of
-      LT -> descend (Just (metaId current)) left
-      GT -> descend candidate right
-      EQ -> statusLeast right <|> candidate
-
-statusLeast :: StatusTree -> Maybe ExactSweepSegmentId
-statusLeast StatusEmpty = Nothing
-statusLeast (StatusNode _ StatusEmpty value _) = Just (metaId value)
-statusLeast (StatusNode _ left _ _) = statusLeast left
-
-statusGreatest :: StatusTree -> Maybe ExactSweepSegmentId
-statusGreatest StatusEmpty = Nothing
-statusGreatest (StatusNode _ _ value StatusEmpty) = Just (metaId value)
-statusGreatest (StatusNode _ _ _ right) = statusGreatest right
-
-lookupMeta :: V.Vector SegmentMeta -> ExactSweepSegmentId -> Maybe SegmentMeta
-lookupMeta metas (ExactSweepSegmentId index) = metas V.!? index
-
-requireMeta
-  :: V.Vector SegmentMeta
-  -> ExactSweepSegmentId
-  -> Either ExactSegmentEventObstruction SegmentMeta
-requireMeta metas segmentId =
-  case lookupMeta metas segmentId of
-    Just meta -> Right meta
-    Nothing -> Left (ExactSweepSegmentMissing segmentId)
-
-exactSegmentEvents :: ExactSegmentEventPlan -> [ExactSegmentEvent]
-exactSegmentEvents = Map.elems . plannedEvents
-
-exactSegmentSplitPoints
-  :: ExactSegmentEventPlan
-  -> ExactSweepSegmentId
-  -> [ExactPoint]
-exactSegmentSplitPoints plan (ExactSweepSegmentId segmentIndex) =
-  maybe [] id (plannedSplitPoints plan V.!? segmentIndex)
-
-exactSegmentRelationMap
-  :: ExactSegmentEventPlan
-  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-exactSegmentRelationMap = Map.map eventRelation . plannedEvents
-
-eventRelation :: ExactSegmentEvent -> SegmentRelation
-eventRelation event =
-  case event of
-    ExactProperCrossing {} -> SegmentsProperlyCross
-    ExactEndpointTouch {} -> SegmentEndpointTouchesInterior
-    ExactSharedEndpoint {} -> SegmentsShareEndpoint
-    ExactDuplicateSegments {} -> SegmentsDuplicate
-    ExactCollinearOverlap {} -> SegmentsCollinearlyOverlap
-
-exactSegmentPairChecks :: ExactSegmentEventPlan -> Int
-exactSegmentPairChecks = plannedPairChecks
-
-exactSegmentSweepMaximumHeight :: ExactSegmentEventPlan -> Int
-exactSegmentSweepMaximumHeight = plannedMaximumHeight
-
-sortAlong :: ExactSegment -> [ExactPoint] -> [ExactPoint]
-sortAlong segment =
-  let (from, to) = exactSegmentEndpoints segment
-   in if from <= to then Set.toAscList . Set.fromList else Set.toDescList . Set.fromList
diff --git a/src-dcel/Moonlight/Triangulation/Internal/FaceProbe.hs b/src-dcel/Moonlight/Triangulation/Internal/FaceProbe.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/FaceProbe.hs
+++ /dev/null
@@ -1,38 +0,0 @@
-module Moonlight.Triangulation.Internal.FaceProbe
-  ( BoundaryProbe (..)
-  , probeBoundary
-  ) where
-
-import Moonlight.Triangulation.Internal.Predicates (onClosedSegment, orient2d)
-import Moonlight.Triangulation.Types (Point)
-
--- | Classification of one oriented boundary of a triangular face. A
--- 'BoundaryCrossing' carries the half-edge whose incident face is the
--- destination of the walk; consumers must not reverse it again.
-data BoundaryProbe edge vertex
-  = BoundaryClear
-  | BoundaryOnVertex !vertex
-  | BoundaryOnEdge !edge
-  | BoundaryCrossing !edge
-  deriving stock (Eq, Ord, Show)
-
-probeBoundary
-  :: (edge -> edge)
-  -> Point
-  -> edge
-  -> vertex
-  -> Point
-  -> vertex
-  -> Point
-  -> BoundaryProbe edge vertex
-probeBoundary reverseBoundary query edge fromVertex from toVertex to
-  | query == from = BoundaryOnVertex fromVertex
-  | query == to = BoundaryOnVertex toVertex
-  | otherwise =
-      case orient2d from to query of
-        EQ
-          | onClosedSegment from to query -> BoundaryOnEdge edge
-          | otherwise -> BoundaryClear
-        LT -> BoundaryCrossing (reverseBoundary edge)
-        GT -> BoundaryClear
-{-# INLINE probeBoundary #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/FloodFillIterator.hs b/src-dcel/Moonlight/Triangulation/Internal/FloodFillIterator.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/FloodFillIterator.hs
+++ /dev/null
@@ -1,743 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleInstances #-}
-
--- | Shape queries and face flood fills over immutable triangulations.
-module Moonlight.Triangulation.Internal.FloodFillIterator
-  ( DistanceMetric (..)
-  , CircleMetric
-  , CircleMetricError (..)
-  , RectangleMetric
-  , RectangleMetricError (..)
-  , circleMetric
-  , rectangleMetric
-  , edgesInShape
-  , verticesInShape
-  , edgesInCircle
-  , verticesInCircle
-  , edgesInRectangle
-  , verticesInRectangle
-  , floodFillFaces
-  , floodFillFacesWithRejectedEdges
-  , outerFaceFloodFill
-  , facesAtEvenBarrierDepth
-  , FaceComponent
-  , faceComponentFaces
-  , BoundaryOrientation (..)
-  , BoundaryLoop
-  , boundaryLoopOrientation
-  , boundaryLoopResidentVertices
-  , boundaryLoopVertices
-  , RegionBoundary
-  , regionBoundaryOuterLoop
-  , regionBoundaryHoleLoops
-  , BoundaryObstruction (..)
-  , faceComponents
-  , faceComponentsBy
-  , labelledRegionBoundaries
-  , componentBoundaryLoops
-  , componentBoundary
-  , componentBoundaryForFaces
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.IntSet as IntSet
-import Data.List (partition, unfoldr)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import qualified Data.Sequence as Seq
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (undirectedEdges)
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( simplifyBoundaryCycle
-  , traceOrientedBoundaryCircuits
-  )
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Math
-  ( mkQueryPoint
-  , segmentDistanceSquaredWide
-  , squaredDistanceWide
-  )
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Types
-
--- | One non-empty connected set of equally labelled bounded face indices.
-newtype FaceComponent = FaceComponent IntSet.IntSet
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Component faces in ascending DCEL order.
-faceComponentFaces :: FaceComponent -> [FaceId]
-faceComponentFaces (FaceComponent faces) =
-  fmap (FaceId . fromIntegral) (IntSet.toAscList faces)
-
--- | Winding carried explicitly by a simple boundary loop.
-data BoundaryOrientation
-  = BoundaryCounterClockwise
-  | BoundaryClockwise
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One non-empty simple boundary loop. Counter-clockwise loops contribute
--- filled area under nonzero winding; clockwise loops subtract holes.
-data BoundaryLoop = BoundaryLoop
-  { boundaryLoopOrientation :: !BoundaryOrientation
-  -- | Exact resident DCEL boundary chain. Consecutive vertices, including
-  -- the closing pair, are endpoints of one boundary half-edge.
-  , boundaryLoopResidentVertices :: !(NonEmpty VertexId)
-  -- | Collinearity-reduced polygon observation of the same resident chain.
-  , boundaryLoopVertices :: !(NonEmpty VertexId)
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The authoritative boundary of one face component.
-data RegionBoundary = RegionBoundary
-  { regionBoundaryOuterLoop :: !BoundaryLoop
-  , regionBoundaryHoleLoops :: ![BoundaryLoop]
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed failure to descend boundary half-edges or project their oriented
--- loops into one strict polygon component.
-data BoundaryObstruction
-  = BoundaryComponentFaceOutOfRange !FaceId {-# UNPACK #-} !Int
-  | BoundaryPinch !VertexId !DirectedEdgeId !DirectedEdgeId
-  | BoundaryCycleDidNotClose !DirectedEdgeId !DirectedEdgeId
-  | BoundaryLoopDegenerate ![VertexId]
-  | BoundaryOuterLoopCardinality !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Complete boundary edges refined by whether each vertex has one successor.
-data BoundaryGraph
-  = SimpleBoundaryGraph !IntSet.IntSet !(IntMap.IntMap DirectedEdgeId)
-  | PinchedBoundaryGraph
-      !IntSet.IntSet
-      !VertexId
-      !DirectedEdgeId
-      !DirectedEdgeId
-
--- | A query shape that can admit points, test edges, and supply a location
--- seed.
-class DistanceMetric metric where
-  metricContainsPoint :: metric -> Point -> Bool
-  metricIntersectsEdge :: metric -> Point -> Point -> Bool
-  metricStartPoint :: metric -> QueryPoint
-
--- | An admitted center and squared radius.
-data CircleMetric = CircleMetric !QueryPoint !RadiusSquared
-  deriving stock (Eq, Ord, Show)
-
--- | Typed refusal for an invalid circle query.
-data CircleMetricError
-  = InvalidCircleCenter !PointValidationError
-  | InvalidCircleRadius !RadiusSquaredError
-  deriving stock (Eq, Ord, Show)
-
--- | Admitted lower corner, upper corner, and center of an axis-aligned box.
-data RectangleMetric = RectangleMetric !(QueryPoint) !(QueryPoint) !(QueryPoint)
-  deriving stock (Eq, Ord, Show)
-
--- | Typed refusal for an invalid rectangle query.
-data RectangleMetricError
-  = InvalidRectangleLower !PointValidationError
-  | InvalidRectangleUpper !PointValidationError
-  | InvalidRectangleCenter !PointValidationError
-  deriving stock (Eq, Ord, Show)
-
--- | A circle metric, or why the radius is unusable.
-circleMetric :: Point -> Double -> Either CircleMetricError CircleMetric
-circleMetric center radiusSquared =
-  CircleMetric
-    <$> first InvalidCircleCenter (mkQueryPoint center)
-    <*> first InvalidCircleRadius (mkRadiusSquared radiusSquared)
-
--- | An axis-aligned rectangle metric, or why the corners are unusable.
-rectangleMetric :: Point -> Point -> Either RectangleMetricError RectangleMetric
-rectangleMetric lower@(Point lowerX lowerY) upper@(Point upperX upperY) = do
-  queryLower <- either (Left . InvalidRectangleLower) Right (mkQueryPoint lower)
-  queryUpper <- either (Left . InvalidRectangleUpper) Right (mkQueryPoint upper)
-  queryCenter <-
-    either
-      (Left . InvalidRectangleCenter)
-      Right
-      (mkQueryPoint (Point ((lowerX + upperX) * 0.5) ((lowerY + upperY) * 0.5)))
-  Right (RectangleMetric queryLower queryUpper queryCenter)
-
-instance DistanceMetric CircleMetric where
-  metricContainsPoint (CircleMetric center radius) point =
-    squaredDistanceWide (queryPointValue center) point <= radiusSquaredValue radius
-  metricIntersectsEdge (CircleMetric center radius) from to =
-    segmentDistanceSquaredWide from to (queryPointValue center) <= radiusSquaredValue radius
-  metricStartPoint (CircleMetric center _) = center
-
-instance DistanceMetric RectangleMetric where
-  metricContainsPoint (RectangleMetric lower upper _) (Point x y) =
-    lowerX <= upperX && lowerY <= upperY && x >= lowerX && x <= upperX && y >= lowerY && y <= upperY
-   where
-    Point lowerX lowerY = queryPointValue lower
-    Point upperX upperY = queryPointValue upper
-  metricIntersectsEdge rectangle from to =
-    metricContainsPoint rectangle from
-      || metricContainsPoint rectangle to
-      || segmentRectangleIntersection rectangle from to
-  metricStartPoint (RectangleMetric _ _ center) = center
-
--- | Edges meeting a circle.
-edgesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [UndirectedEdgeId]
-edgesInCircle triangulation center radiusSquared =
-  edgesInShape triangulation <$> circleMetric center radiusSquared
-
--- | Vertices inside a circle.
-verticesInCircle :: Triangulation mode vertex directed undirected face -> Point -> Double -> Either CircleMetricError [VertexId]
-verticesInCircle triangulation center radiusSquared =
-  verticesInShape triangulation <$> circleMetric center radiusSquared
-
--- | Edges meeting an axis-aligned rectangle.
-edgesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [UndirectedEdgeId]
-edgesInRectangle triangulation lower upper = edgesInShape triangulation <$> rectangleMetric lower upper
-
--- | Vertices inside an axis-aligned rectangle.
-verticesInRectangle :: Triangulation mode vertex directed undirected face -> Point -> Point -> Either RectangleMetricError [VertexId]
-verticesInRectangle triangulation lower upper = verticesInShape triangulation <$> rectangleMetric lower upper
-
--- | Edges meeting any metric shape.
-edgesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [UndirectedEdgeId]
-edgesInShape triangulation metric
-  | numVertices triangulation <= 1 = []
-  | numInnerFaces triangulation == 0 =
-      [edge | edge <- undirectedEdges triangulation, edgeInside edge]
-  | otherwise =
-      let starts = shapeStartFaces triangulation metric
-          (_, accepted, _) = floodFillFacesWithEdges triangulation starts edgeInside
-       in map (UndirectedEdgeId . fromIntegral) (IntSet.toAscList accepted)
- where
-  edgeInside edge =
-    let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-     in metricIntersectsEdge metric (vertexPoint triangulation fromVertex) (vertexPoint triangulation toVertex)
-
--- | Vertices inside any metric shape.
-verticesInShape :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [VertexId]
-verticesInShape triangulation metric =
-  [ vertex
-  | vertex <- candidateVertices
-  , metricContainsPoint metric (vertexPoint triangulation vertex)
-  ]
- where
-  edges = edgesInShape triangulation metric
-  set = List.foldl' addEndpoints IntSet.empty edges
-  addEndpoints acc edge =
-    let (from, to) = undirectedEndpoints triangulation edge
-     in IntSet.insert (vertexIdIndex from) (IntSet.insert (vertexIdIndex to) acc)
-  candidateVertices
-    | numVertices triangulation == 1 = [VertexId 0]
-    | otherwise = map (VertexId . fromIntegral) (IntSet.toAscList set)
-
--- | Reach inner faces from the supplied seeds by crossing only admitted edges.
-floodFillFaces
-  :: Triangulation mode vertex directed undirected face -> [FaceId]
-  -> (UndirectedEdgeId -> Bool)
-  -> [FaceId]
-floodFillFaces triangulation starts canCross =
-  let (faces, _, _) = floodFillFacesWithEdges triangulation starts canCross
-   in faces
-
--- | The reached section and the edge pairs whose crossing was rejected while
--- descending it. This package-internal observation retains the boundary
--- witness instead of requiring a later face-wide rediscovery.
-floodFillFacesWithRejectedEdges
-  :: Triangulation mode vertex directed undirected face -> [FaceId]
-  -> (UndirectedEdgeId -> Bool)
-  -> ([FaceId], [UndirectedEdgeId])
-floodFillFacesWithRejectedEdges triangulation starts canCross =
-  let (faces, _, rejected) = floodFillFacesWithEdges triangulation starts canCross
-   in (faces, fmap (UndirectedEdgeId . fromIntegral) (IntSet.toAscList rejected))
-
-floodFillFacesWithEdges
-  :: Triangulation mode vertex directed undirected face -> [FaceId]
-  -> (UndirectedEdgeId -> Bool)
-  -> ([FaceId], IntSet.IntSet, IntSet.IntSet)
-floodFillFacesWithEdges triangulation starts canCross =
-  let (faces, accepted, rejected) = go initialStack initialVisited IntSet.empty IntSet.empty []
-   in (reverse faces, accepted, rejected)
- where
-  valid face = face /= outerFace && faceIdIndex face < numFaces triangulation
-  (initialStack, initialVisited) = List.foldl' enqueueStart ([], IntSet.empty) starts
-
-  enqueueStart state face
-    | valid face = enqueue face state
-    | otherwise = state
-
-  go [] _ accepted rejected result = (result, accepted, rejected)
-  go (face : stack) visited accepted rejected result =
-    let (stack', visited', accepted', rejected') =
-          foldFaceDirectedEdges'
-            triangulation
-            face
-            expand
-            (stack, visited, accepted, rejected)
-     in go stack' visited' accepted' rejected' (face : result)
-
-  expand (stack, visited, accepted, rejected) edge =
-    let undirected = asUndirected edge
-        edgeIndex = undirectedEdgeIdIndex undirected
-        adjacent = incidentFace triangulation (reverseEdge edge)
-        edgeAdmission
-          | IntSet.member edgeIndex accepted = (True, accepted, rejected)
-          | IntSet.member edgeIndex rejected = (False, accepted, rejected)
-          | canCross undirected = (True, IntSet.insert edgeIndex accepted, rejected)
-          | otherwise = (False, accepted, IntSet.insert edgeIndex rejected)
-        (crosses, accepted', rejected') = edgeAdmission
-        (stack', visited') =
-          if crosses && valid adjacent
-            then enqueue adjacent (stack, visited)
-            else (stack, visited)
-     in (stack', visited', accepted', rejected')
-
-  enqueue face (stack, visited)
-    | IntSet.member index visited = (stack, visited)
-    | otherwise = (face : stack, IntSet.insert index visited)
-   where
-    index = faceIdIndex face
-
--- | Connected components of equally labelled bounded faces. Labels are
--- evaluated once; the component carrier is the same 'IntSet' used by descent.
-faceComponents
-  :: Eq label
-  => Triangulation mode vertex directed undirected face
-  -> (FaceId -> label)
-  -> [(label, FaceComponent)]
-faceComponents triangulation labelFace =
-  faceComponentsFromLabels triangulation labelAt (const True)
- where
-  labels =
-    V.generate
-      (numInnerFaces triangulation)
-      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
-  labelAt faceIndex = labels V.!? (faceIndex - 1)
-
--- | Connected components over a selected face section and admitted adjacency.
--- A missing label removes a face from the section; the edge predicate states
--- which overlaps glue. This is the common descent used by ordinary labelled
--- regions and exact-overlay charts whose representation diagonals alone may
--- connect collapsed resident faces.
-faceComponentsBy
-  :: Eq label
-  => Triangulation mode vertex directed undirected face
-  -> (FaceId -> Maybe label)
-  -> (UndirectedEdgeId -> Bool)
-  -> [(label, FaceComponent)]
-faceComponentsBy triangulation labelFace =
-  faceComponentsFromLabels triangulation labelAt
- where
-  labels =
-    V.generate
-      (numInnerFaces triangulation)
-      (\index -> labelFace (FaceId (fromIntegral (index + 1))))
-  labelAt faceIndex = labels V.!? (faceIndex - 1) >>= id
-
-faceComponentsFromLabels
-  :: Eq label
-  => Triangulation mode vertex directed undirected face
-  -> (Int -> Maybe label)
-  -> (UndirectedEdgeId -> Bool)
-  -> [(label, FaceComponent)]
-faceComponentsFromLabels triangulation labelAt canCross =
-  unfoldr descend initialUnvisited
- where
-  initialUnvisited =
-    IntSet.fromRange (1, numFaces triangulation - 1)
-
-  descend remaining =
-    case IntSet.minView remaining of
-      Nothing -> Nothing
-      Just (seedIndex, unseeded) ->
-        case labelAt seedIndex of
-          Nothing -> descend unseeded
-          Just componentLabel ->
-            let unvisited =
-                  collectComponent componentLabel (Seq.singleton seedIndex) unseeded
-                componentFaces = IntSet.difference remaining unvisited
-             in Just ((componentLabel, FaceComponent componentFaces), unvisited)
-
-  collectComponent componentLabel queued unvisited =
-    case Seq.viewl queued of
-      Seq.EmptyL -> unvisited
-      faceIndex Seq.:< remainingQueue ->
-        let face = FaceId (fromIntegral faceIndex)
-            (expandedQueue, remainingUnvisited) =
-              foldFaceDirectedEdges'
-                triangulation
-                face
-                (admitAdjacent componentLabel)
-                (remainingQueue, unvisited)
-         in collectComponent
-              componentLabel
-              expandedQueue
-              remainingUnvisited
-
-  admitAdjacent componentLabel (queued, unvisited) edge =
-    if
-      canCross (asUndirected edge)
-        && adjacent /= outerFace
-        && IntSet.member adjacentIndex unvisited
-        && labelAt adjacentIndex == Just componentLabel
-      then
-        ( queued Seq.|> adjacentIndex
-        , IntSet.delete adjacentIndex unvisited
-        )
-      else (queued, unvisited)
-   where
-    adjacent = incidentFace triangulation (reverseEdge edge)
-    adjacentIndex = faceIdIndex adjacent
-{-# INLINE faceComponentsFromLabels #-}
-
--- | Descend every equally labelled bounded-face component through the one
--- authoritative boundary tracer. Components are converted independently;
--- callers may group equal labels only after this descent has succeeded.
-labelledRegionBoundaries
-  :: Eq label
-  => Triangulation mode vertex directed undirected face
-  -> (FaceId -> label)
-  -> Either BoundaryObstruction [(label, RegionBoundary)]
-labelledRegionBoundaries triangulation labelFace =
-  traverse
-    (\(label, component) -> (label,) <$> componentBoundary triangulation component)
-    (faceComponents triangulation labelFace)
-
--- | Extract the non-empty oriented boundary chain of one component. Boundary
--- half-edges retain their incident component face on the left. If several
--- boundary arms meet at one vertex, Euler descent consumes every half-edge
--- once and repeated-vertex splitting publishes finitely many simple loops.
-componentBoundaryLoops
-  :: Triangulation mode vertex directed undirected face
-  -> FaceComponent
-  -> Either BoundaryObstruction (NonEmpty BoundaryLoop)
-componentBoundaryLoops triangulation component = do
-  graph <- componentBoundaryGraph triangulation component
-  loops <-
-    case graph of
-      SimpleBoundaryGraph edges outgoing ->
-        traceBoundaryLoops triangulation outgoing edges
-      PinchedBoundaryGraph edges _ _ _ ->
-        tracePinchedBoundaryLoops triangulation edges
-  case NonEmpty.nonEmpty loops of
-    Just nonEmptyLoops -> Right nonEmptyLoops
-    Nothing -> Left (BoundaryOuterLoopCardinality 0)
-
--- | Project one component into a strict polygon boundary. A pinched oriented
--- chain remains available through 'componentBoundaryLoops', but it is not one
--- lawful 'RegionBoundary' and is refused here with its first pinch witness.
-componentBoundary
-  :: Triangulation mode vertex directed undirected face
-  -> FaceComponent
-  -> Either BoundaryObstruction RegionBoundary
-componentBoundary triangulation component = do
-  graph <- componentBoundaryGraph triangulation component
-  case graph of
-    PinchedBoundaryGraph _ vertex firstEdge secondEdge ->
-      Left (BoundaryPinch vertex firstEdge secondEdge)
-    SimpleBoundaryGraph edges outgoing ->
-      traceBoundaryLoops triangulation outgoing edges
-        >>= regionBoundaryFromLoops
-
--- | Project exactly the supplied face section into one boundary. This is the
--- selected-section descent used by local extension: it does not label or
--- flood the complement, and therefore its work is proportional to the faces
--- named by the caller plus their constant-degree boundary edges.
-componentBoundaryForFaces
-  :: Triangulation mode vertex directed undirected face
-  -> Set.Set FaceId
-  -> Either BoundaryObstruction RegionBoundary
-componentBoundaryForFaces triangulation selectedFaces = do
-  component <- selectedFaceComponent triangulation selectedFaces
-  componentBoundary triangulation component
-
-selectedFaceComponent
-  :: Triangulation mode vertex directed undirected face
-  -> Set.Set FaceId
-  -> Either BoundaryObstruction FaceComponent
-selectedFaceComponent triangulation selectedFaces =
-  case IntSet.lookupMin indices of
-    Just 0 -> Left (BoundaryComponentFaceOutOfRange (FaceId 0) (numFaces triangulation))
-    _ ->
-      case IntSet.lookupGE (numFaces triangulation) indices of
-        Just invalid ->
-          Left
-            ( BoundaryComponentFaceOutOfRange
-                (FaceId (fromIntegral invalid))
-                (numFaces triangulation)
-            )
-        Nothing -> Right (FaceComponent indices)
- where
-  indices = IntSet.fromList (fmap faceIdIndex (Set.toAscList selectedFaces))
-
-componentBoundaryGraph
-  :: Triangulation mode vertex directed undirected face
-  -> FaceComponent
-  -> Either BoundaryObstruction BoundaryGraph
-componentBoundaryGraph triangulation (FaceComponent componentFaces) =
-  case IntSet.lookupGE (numFaces triangulation) componentFaces of
-    Just invalid ->
-      Left
-        ( BoundaryComponentFaceOutOfRange
-            (FaceId (fromIntegral invalid))
-            (numFaces triangulation)
-        )
-    Nothing ->
-      Right
-        ( IntSet.foldl'
-            collectFace
-            (SimpleBoundaryGraph IntSet.empty IntMap.empty)
-            componentFaces
-        )
- where
-  collectFace graph (face :: Int) =
-    foldFaceDirectedEdges'
-      triangulation
-      (FaceId (fromIntegral face))
-      insertBoundaryEdge
-      graph
-
-  insertBoundaryEdge graph edge
-    | IntSet.member adjacentIndex componentFaces = graph
-    | otherwise =
-        case graph of
-          PinchedBoundaryGraph edges pinchVertex firstEdge secondEdge ->
-            PinchedBoundaryGraph
-              (insertEdge edges)
-              pinchVertex
-              firstEdge
-              secondEdge
-          SimpleBoundaryGraph edges outgoing ->
-            case IntMap.lookup residentIndex outgoing of
-              Nothing ->
-                SimpleBoundaryGraph
-                  (insertEdge edges)
-                  (IntMap.insert residentIndex edge outgoing)
-              Just previousEdge ->
-                PinchedBoundaryGraph
-                  (insertEdge edges)
-                  vertex
-                  previousEdge
-                  edge
-   where
-    adjacentIndex = faceIdIndex (incidentFace triangulation (reverseEdge edge))
-    vertex = origin triangulation edge
-    residentIndex = vertexIdIndex vertex
-    insertEdge = IntSet.insert (directedEdgeIdIndex edge)
-
-regionBoundaryFromLoops
-  :: [BoundaryLoop]
-  -> Either BoundaryObstruction RegionBoundary
-regionBoundaryFromLoops loops =
-  case outerLoops of
-    [outerLoop] ->
-      Right
-        RegionBoundary
-          { regionBoundaryOuterLoop = outerLoop
-          , regionBoundaryHoleLoops = holeLoops
-          }
-    _ -> Left (BoundaryOuterLoopCardinality (length outerLoops))
- where
-  (outerLoops, holeLoops) =
-    partition
-      ((== BoundaryCounterClockwise) . boundaryLoopOrientation)
-      loops
-
-tracePinchedBoundaryLoops
-  :: Triangulation mode vertex directed undirected face
-  -> IntSet.IntSet
-  -> Either BoundaryObstruction [BoundaryLoop]
-tracePinchedBoundaryLoops triangulation boundaryEdges = do
-  cycles <-
-    traceOrientedBoundaryCircuits
-      (origin triangulation)
-      (destination triangulation)
-      BoundaryCycleDidNotClose
-      outgoingByVertex
-      orientedEdges
-  traverse
-    (simplifyBoundaryLoop triangulation . NonEmpty.toList)
-    cycles
- where
-  orientedEdges =
-    Set.fromDistinctAscList
-      ( fmap
-          (DirectedEdgeId . fromIntegral)
-          (IntSet.toAscList boundaryEdges)
-      )
-  outgoingByVertex =
-    Set.foldr
-      (\edge -> Map.insertWith (<>) (origin triangulation edge) [edge])
-      Map.empty
-      orientedEdges
-
-traceBoundaryLoops
-  :: Triangulation mode vertex directed undirected face
-  -> IntMap.IntMap DirectedEdgeId
-  -> IntSet.IntSet
-  -> Either BoundaryObstruction [BoundaryLoop]
-traceBoundaryLoops triangulation outgoingByVertex = descend []
- where
-  descend loops unvisited =
-    case IntSet.minView unvisited of
-      Nothing -> Right (reverse loops)
-      Just (rawStart, _) -> do
-        let start = DirectedEdgeId (fromIntegral rawStart)
-        (vertices, remaining) <- traceCycle start start unvisited []
-        loop <- simplifyBoundaryLoop triangulation vertices
-        descend (loop : loops) remaining
-
-  traceCycle start current unvisited reversedVertices =
-    let remaining = IntSet.delete (directedEdgeIdIndex current) unvisited
-        accumulated = origin triangulation current : reversedVertices
-        target = destination triangulation current
-     in case IntMap.lookup (vertexIdIndex target) outgoingByVertex of
-          Just successor
-            | successor == start -> Right (reverse accumulated, remaining)
-            | IntSet.member (directedEdgeIdIndex successor) remaining ->
-                traceCycle start successor remaining accumulated
-            | otherwise -> Left (BoundaryCycleDidNotClose start successor)
-          Nothing -> Left (BoundaryCycleDidNotClose start current)
-
-simplifyBoundaryLoop
-  :: Triangulation mode vertex directed undirected face
-  -> [VertexId]
-  -> Either BoundaryObstruction BoundaryLoop
-simplifyBoundaryLoop triangulation vertices = do
-  (windingOrder, simplifiedVertices) <-
-    simplifyBoundaryCycle
-      BoundaryLoopDegenerate
-      redundant
-      winding
-      key
-      vertices
-  residentVertices <-
-    maybe
-      (Left (BoundaryLoopDegenerate vertices))
-      Right
-      (NonEmpty.nonEmpty vertices)
-  case windingOrder of
-    GT -> Right (BoundaryLoop BoundaryCounterClockwise residentVertices simplifiedVertices)
-    LT -> Right (BoundaryLoop BoundaryClockwise residentVertices simplifiedVertices)
-    EQ -> Left (BoundaryLoopDegenerate (NonEmpty.toList simplifiedVertices))
- where
-  point vertex = vertexPoint triangulation vertex
-  redundant previousVertex current nextVertex =
-    orient2d (point previousVertex) (point current) (point nextVertex) == EQ
-      && onClosedSegment (point previousVertex) (point nextVertex) (point current)
-  winding previousVertex current nextVertex =
-    orient2d (point previousVertex) (point current) (point nextVertex)
-  key vertex = (point vertex, vertex)
-
--- | Inner faces separated from the outer face by an even minimum number of
--- barriers. A 0–1 BFS floods freely within one depth before crossing a barrier,
--- so a free-ended barrier can be walked around at depth zero while nested
--- closed barriers alternate outside and inside.
-facesAtEvenBarrierDepth
-  :: Triangulation mode vertex directed undirected face
-  -> (UndirectedEdgeId -> Bool)
-  -> [FaceId]
-facesAtEvenBarrierDepth triangulation isBarrier =
-  concat (evenLayers (barrierDepthLayers triangulation isBarrier))
- where
-  evenLayers :: [[FaceId]] -> [[FaceId]]
-  evenLayers (outsideLayer : _insideLayer : deeper) =
-    outsideLayer : evenLayers deeper
-  evenLayers shallow = shallow
-
-barrierDepthLayers
-  :: Triangulation mode vertex directed undirected face
-  -> (UndirectedEdgeId -> Bool)
-  -> [[FaceId]]
-barrierDepthLayers triangulation isBarrier =
-  map (filter (/= outerFace)) (layers IntSet.empty [outerFace])
- where
-  known face = faceIdIndex face < numFaces triangulation
-
-  layers visited frontier = case flood visited [] frontier of
-    ([], _) -> []
-    (layer, visited') -> layer : layers visited' (concatMap (neighbours isBarrier) layer)
-
-  flood visited acc [] = (reverse acc, visited)
-  flood visited acc (face : rest)
-    | not (known face) || IntSet.member (faceIdIndex face) visited = flood visited acc rest
-    | otherwise =
-        flood
-          (IntSet.insert (faceIdIndex face) visited)
-          (face : acc)
-          (neighbours (not . isBarrier) face <> rest)
-
-  neighbours admit face =
-    [ incidentFace triangulation (reverseEdge edge)
-    | edge <- faceDirectedEdges triangulation face
-    , admit (asUndirected edge)
-    ]
-
--- | Faces reachable from the outer face without crossing a barrier edge.
-outerFaceFloodFill :: Triangulation mode vertex directed undirected face -> (UndirectedEdgeId -> Bool) -> [FaceId]
-outerFaceFloodFill triangulation canCross = floodFillFaces triangulation starts canCross
- where
-  starts =
-    [ face
-    | outerEdge <- faceDirectedEdges triangulation outerFace
-    , let edge = asUndirected outerEdge
-    , canCross edge
-    , let face = incidentFace triangulation (reverseEdge outerEdge)
-    , face /= outerFace
-    ]
-
--- | The faces a shape's start point lands in.
-shapeStartFaces :: DistanceMetric metric => Triangulation mode vertex directed undirected face -> metric -> [FaceId]
-shapeStartFaces triangulation metric =
-  case locatePoint triangulation (metricStartPoint metric) of
-    InFace face -> [face]
-    OnEdge edge -> filter (/= outerFace) [incidentFace triangulation edge, incidentFace triangulation (reverseEdge edge)]
-    OnVertex vertex ->
-      intSetToFaces
-        ( List.foldl'
-            (\set edge ->
-              let face = incidentFace triangulation edge
-               in if face == outerFace then set else IntSet.insert (faceIdIndex face) set
-            )
-            IntSet.empty
-            (vertexOutgoingEdges triangulation vertex)
-        )
-    OutsideConvexHull _ ->
-      [ incidentFace triangulation (reverseEdge edge)
-      | edge <- faceDirectedEdges triangulation outerFace
-      , let from = vertexPoint triangulation (origin triangulation edge)
-      , let to = vertexPoint triangulation (destination triangulation edge)
-      , metricIntersectsEdge metric from to
-      , incidentFace triangulation (reverseEdge edge) /= outerFace
-      ]
-    EmptyTriangulation -> []
- where
-  intSetToFaces = map (FaceId . fromIntegral) . IntSet.toAscList
-
-segmentRectangleIntersection :: RectangleMetric -> Point -> Point -> Bool
-segmentRectangleIntersection (RectangleMetric lowerQuery upperQuery _) from to
-  | lx > ux || ly > uy = False
-  | lower == upper = onClosedSegment from to lower
-  | otherwise = any (uncurry (segmentsIntersect from to)) boundaries
- where
-  lower@(Point lx ly) = queryPointValue lowerQuery
-  upper@(Point ux uy) = queryPointValue upperQuery
-  boundaries =
-    [ (Point lx ly, Point lx uy)
-    , (Point lx uy, Point ux uy)
-    , (Point ux uy, Point ux ly)
-    , (Point ux ly, Point lx ly)
-    ]
diff --git a/src-dcel/Moonlight/Triangulation/Internal/HandleDefs.hs b/src-dcel/Moonlight/Triangulation/Internal/HandleDefs.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/HandleDefs.hs
+++ /dev/null
@@ -1,89 +0,0 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
--- | The identifier family and the arithmetic on it: a directed edge's twin is
--- its index complement, so orientation is a bit rather than a lookup.
-module Moonlight.Triangulation.Internal.HandleDefs
-  ( VertexId (..)
-  , vertexIdIndex
-  , FaceId (..)
-  , faceIdIndex
-  , DirectedEdgeId (..)
-  , directedEdgeIdIndex
-  , UndirectedEdgeId (..)
-  , undirectedEdgeIdIndex
-  , reverseEdge
-  , asUndirected
-  , normalizedDirected
-  , reversedDirected
-  , directedPair
-  , isNormalized
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bits (shiftL, shiftR, xor, (.&.))
-import Data.Word (Word32)
-import Moonlight.Triangulation.Internal.PackedIndex (unpackIndex)
-
--- | Index of a vertex in the immutable DCEL.
-newtype VertexId = VertexId { unVertexId :: Word32 }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | Index of a face in the immutable DCEL; zero denotes the outer face.
-newtype FaceId = FaceId { unFaceId :: Word32 }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | Oriented half-edge index. Twin orientations differ only in the low bit.
-newtype DirectedEdgeId = DirectedEdgeId { unDirectedEdgeId :: Word32 }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | Index of a twin pair, with orientation forgotten.
-newtype UndirectedEdgeId = UndirectedEdgeId { unUndirectedEdgeId :: Word32 }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | Project compact identifiers into resident vector indices.
-vertexIdIndex :: VertexId -> Int
-vertexIdIndex = unpackIndex . unVertexId
-{-# INLINE vertexIdIndex #-}
-faceIdIndex :: FaceId -> Int
-faceIdIndex = unpackIndex . unFaceId
-{-# INLINE faceIdIndex #-}
-directedEdgeIdIndex :: DirectedEdgeId -> Int
-directedEdgeIdIndex = unpackIndex . unDirectedEdgeId
-{-# INLINE directedEdgeIdIndex #-}
-undirectedEdgeIdIndex :: UndirectedEdgeId -> Int
-undirectedEdgeIdIndex = unpackIndex . unUndirectedEdgeId
-{-# INLINE undirectedEdgeIdIndex #-}
-
--- | Select the opposite orientation of the same undirected edge.
-reverseEdge :: DirectedEdgeId -> DirectedEdgeId
-reverseEdge (DirectedEdgeId edge) = DirectedEdgeId (edge `xor` 1)
-{-# INLINE reverseEdge #-}
-
--- | Forget a directed edge's orientation.
-asUndirected :: DirectedEdgeId -> UndirectedEdgeId
-asUndirected (DirectedEdgeId edge) = UndirectedEdgeId (edge `shiftR` 1)
-{-# INLINE asUndirected #-}
-
--- | Select the even-indexed orientation of an undirected edge.
-normalizedDirected :: UndirectedEdgeId -> DirectedEdgeId
-normalizedDirected (UndirectedEdgeId edge) = DirectedEdgeId (edge `shiftL` 1)
-{-# INLINE normalizedDirected #-}
-
--- | Select the odd-indexed orientation of an undirected edge.
-reversedDirected :: UndirectedEdgeId -> DirectedEdgeId
-reversedDirected edge = reverseEdge (normalizedDirected edge)
-{-# INLINE reversedDirected #-}
-
--- | Both orientations, normalized first and reversed second.
-directedPair :: UndirectedEdgeId -> (DirectedEdgeId, DirectedEdgeId)
-directedPair edge = (normalizedDirected edge, reversedDirected edge)
-{-# INLINE directedPair #-}
-
--- | Whether a directed edge is the normalized orientation of its pair.
-isNormalized :: DirectedEdgeId -> Bool
-isNormalized (DirectedEdgeId edge) = edge .&. 1 == 0
-{-# INLINE isNormalized #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Mutable.hs b/src-dcel/Moonlight/Triangulation/Internal/Mutable.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Mutable.hs
+++ /dev/null
@@ -1,1574 +0,0 @@
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE RecordWildCards #-}
-
-module Moonlight.Triangulation.Internal.Mutable
-  ( MutableDcel (..)
-  , DenseMutableDcel
-  , MutableTopology (..)
-  , denseMutableDcel
-  , denseMutableOwner
-  , DcelCapacity
-  , generalDcelCapacity
-  , planarDcelCapacity
-  , exactDcelCapacity
-  , newMutableDcel
-  , DefaultedVertexDcel
-  , newMutableDcelWithVertexDefault
-  , defaultedVertexDcel
-  , defaultedDenseVertexDcel
-  , thawTriangulation
-  , thawTriangulationDense
-  , freezeTriangulation
-  , freezeTriangulationWithStats
-  , pointCapacity
-  , halfEdgeCapacity
-  , pointCount
-  , connectedCount
-  , directedEdgeCount
-  , faceCount
-  , pointAt
-  , lookupPointVertex
-  , activatePointIndex
-  , activateBatchPointIndex
-  , discardBatchPointIndex
-  , identityIndexActive
-  , readPointX
-  , readPointY
-  , writePoint
-  , vertexDataAt
-  , writeVertexData
-  , payloadsPristine
-  , resetEdgeData
-  , resetFaceData
-  , edgeOriginPoint
-  , appendVertex
-  , appendVertexCoordinates
-  , appendDefaultVertexCoordinates
-  , NextVertexSlot
-  , nextVertexSlot
-  , nextVertexSlotIndex
-  , appendVertexCoordinatesAtSlot
-  , ensurePointCapacity
-  , markConnected
-  , isConnected
-  , addEdge
-  , addEdgeBlock
-  , addFace
-  , addFaceBlock
-  , denseAddEdgeBlock
-  , denseAddFaceBlock
-  , denseInitializeUnconstrainedEdgeBlock
-  , ensureCellCapacity
-  , truncatePoints
-  , truncateDirectedEdges
-  , truncateFaces
-  , swapRemoveUndirectedEdge
-  , swapRemoveFace
-  , swapRemoveVertex
-  , linkEdges
-  , setCycle3
-  , faceEdges
-  , readOrigin
-  , writeOrigin
-  , readNext
-  , writeNext
-  , readPrevious
-  , writePrevious
-  , readFace
-  , writeFace
-  , readVertexOut
-  , writeVertexOut
-  , readFaceEdge
-  , writeFaceEdge
-  , readConstraint
-  , denseReadPointX
-  , denseReadPointY
-  , denseWriteFreshDefaultPoint
-  , denseCommitFreshDefaultPoints
-  , denseReadOrigin
-  , denseWriteOrigin
-  , denseReadNext
-  , denseWriteNext
-  , denseReadPrevious
-  , denseWritePrevious
-  , denseReadFace
-  , denseWriteFace
-  , denseWriteVertexOut
-  , denseMarkFreshConnected
-  , denseCommitFreshConnections
-  , denseReadFaceEdge
-  , denseWriteFaceEdge
-  , denseReadConstraint
-  , denseLinkEdges
-  , denseSetCycle3
-  , denseFaceEdges
-  , setConstraint
-  , clearConstraint
-  ) where
-
-import Control.Monad (foldM, forM_, unless, when)
-import Data.Bits (xor)
-import Control.Monad.ST (ST)
-import qualified Data.IntSet as IntSet
-import Data.STRef
-  ( STRef
-  , modifySTRef'
-  , newSTRef
-  , readSTRef
-  , writeSTRef
-  )
-import Data.Word (Word8, Word32)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId (..)
-  , FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  )
-import Moonlight.Triangulation.Internal.BoxedPaged
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, packIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Paged
-import Moonlight.Triangulation.Internal.PointIndex
-  ( MutablePointIndex
-  , MutablePointIndexUpdate (..)
-  , PointIndex
-  , buildPointIndex
-  , emptyPointIndex
-  , insertPointIndex
-  , lookupMutablePoint
-  , newMutablePointIndex
-  , pointIndexCandidates
-  , relocateMutablePoint
-  , relocatePointIndex
-  , removeMutablePoint
-  , removePointIndex
-  , seedMutablePointIndex
-  )
-import Moonlight.Triangulation.Math (canonicalPoint)
-import Moonlight.Triangulation.Internal.Representation (Triangulation (..))
-import Moonlight.Triangulation.Internal.Types (BuildError (..), ElementDefaults (..), Point (..))
--- | Dormant carries the inherited index through mutations as unforced pure
--- updates; persistent Active is the same index forced and kept strict because
--- a singleton lookup already proved someone is asking. Batch Active is the
--- existing open-addressed owner scoped to one dense identity program. Missing
--- means publication owes a lazy whole-mesh rebuild to any eventual asker.
-data MutablePointIndexState s
-  = DormantPointIndex PointIndex
-  | ActivePersistentPointIndex !PointIndex
-  | ActiveBatchPointIndex !(MutablePointIndex s)
-  | MissingPointIndex
-
-data MutableDcel s vertex directed undirected face = MutableDcel
-  { mdPointX :: !(MutablePaged s Double)
-  , mdPointY :: !(MutablePaged s Double)
-  , mdPointIndex :: !(STRef s (MutablePointIndexState s))
-  , mdVertexOut :: !(MutablePaged s Word32)
-  , mdVertexData :: !(MutableBoxedPaged s vertex)
-  , mdNewConnected :: !(MutablePaged s Word8)
-  , mdRecycledNew :: !(STRef s IntSet.IntSet)
-  , mdHalfTopology :: !(MutablePaged s Word32)
-  , mdDirectedData :: !(MutableBoxedPaged s directed)
-  , mdUndirectedData :: !(MutableBoxedPaged s undirected)
-  , mdConstraint :: !(MutablePaged s Word8)
-  , mdFaceEdge :: !(MutablePaged s Word32)
-  , mdFaceData :: !(MutableBoxedPaged s face)
-  , mdPointCount :: !(STRef s Int)
-  , mdConnectedCount :: !(STRef s Int)
-  , mdHalfCount :: !(STRef s Int)
-  , mdFaceCount :: !(STRef s Int)
-  , mdConstraintCount :: !(STRef s Int)
-  , mdConstraintEdges :: !(STRef s IntSet.IntSet)
-  , mdLastFace :: !(STRef s Int)
-  , mdInitialPointCount :: {-# UNPACK #-} !Int
-  , mdPointCapacity :: {-# UNPACK #-} !Int
-  , mdHalfCapacity :: {-# UNPACK #-} !Int
-  , mdFaceCapacity :: {-# UNPACK #-} !Int
-  , -- | No element payload plane held a materialized page at thaw. Nothing
-    -- inside a transaction can write one: the three planes are written only
-    -- through the persistent setters, which run outside one, and both the
-    -- rewrite reset and the swap-compaction relocation below are no-ops while
-    -- this holds. So it is constant for the transaction's whole life, and a
-    -- rewrite decides in a predictable branch that it has no label to move.
-    mdPayloadsPristine :: !Bool
-  , mdElementDefaults :: !(ElementDefaults directed undirected face)
-  }
-
--- | Physical bounds for one fresh mutable DCEL. Vertex, directed-edge and
--- face sections are stated independently because a known planar construction
--- and an arbitrary append program obey different allocation laws. Keeping the
--- law in the constructor input prevents every fresh caller from silently
--- inheriting the loosest reservation and then copying its dead tail at freeze.
-data DcelCapacity = DcelCapacity
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-  {-# UNPACK #-} !Int
-
--- | Conservative append-program capacity, preserving the historical slack
--- for operations whose intermediate cells may outlive their final topology.
-generalDcelCapacity :: Int -> DcelCapacity
-generalDcelCapacity maximumVertices =
-  let !vertices = max 1 maximumVertices
-   in DcelCapacity vertices (max 2 (8 * vertices + 16)) (max 1 (3 * vertices + 8))
-
--- | Tight fresh planar capacity. Circle sweep allocates monotonically and no
--- rewrite abandons a cell, so Euler's bounds plus seed slack are authoritative:
--- at most @6n@ directed edges and @2n@ faces.
---
--- The separated seam merge is the second lawful caller. It copies two planar
--- sources and then only adds: stitching creates seam cells, legalization
--- rewires without allocating, and nothing is abandoned. Its peak is therefore
--- its published result, a planar triangulation on the summed vertex count.
-planarDcelCapacity :: Int -> DcelCapacity
-planarDcelCapacity maximumVertices =
-  let !vertices = max 1 maximumVertices
-   in DcelCapacity vertices (max 2 (6 * vertices + 16)) (max 1 (2 * vertices + 8))
-
--- | Exact section bounds for reconstruction programs that already know the
--- published cardinalities they will materialize.
-exactDcelCapacity :: Int -> Int -> Int -> DcelCapacity
-exactDcelCapacity vertices directedEdges faces =
-  DcelCapacity (max 1 vertices) (max 2 directedEdges) (max 1 faces)
-
--- | The contiguous physical section of one mutable DCEL. It is a refinement
--- of the canonical owner, not a second mesh: every plane below is the exact
--- flat vector already held by 'denseMutableOwner'. Circle sweep and dense
--- sessions discharge this section once, then interpret their hot local
--- rewrites without re-testing each 'MutablePaged' sum at every cell.
-data DenseMutableDcel s vertex directed undirected face = DenseMutableDcel
-  { dmdOwner :: !(MutableDcel s vertex directed undirected face)
-  , dmdPointX :: !(FlatMutablePaged s Double)
-  , dmdPointY :: !(FlatMutablePaged s Double)
-  , dmdVertexOut :: !(FlatMutablePaged s Word32)
-  , dmdNewConnected :: !(FlatMutablePaged s Word8)
-  , dmdHalfTopology :: !(FlatMutablePaged s Word32)
-  , dmdConstraint :: !(FlatMutablePaged s Word8)
-  , dmdFaceEdge :: !(FlatMutablePaged s Word32)
-  }
-
-denseMutableDcel
-  :: MutableDcel s vertex directed undirected face
-  -> Maybe (DenseMutableDcel s vertex directed undirected face)
-denseMutableDcel owner@MutableDcel{mdPointX, mdPointY, mdVertexOut, mdNewConnected, mdHalfTopology, mdConstraint, mdFaceEdge} =
-  DenseMutableDcel owner
-    <$> flatMutableSection mdPointX
-    <*> flatMutableSection mdPointY
-    <*> flatMutableSection mdVertexOut
-    <*> flatMutableSection mdNewConnected
-    <*> flatMutableSection mdHalfTopology
-    <*> flatMutableSection mdConstraint
-    <*> flatMutableSection mdFaceEdge
-{-# INLINE denseMutableDcel #-}
-
-denseMutableOwner
-  :: DenseMutableDcel s vertex directed undirected face
-  -> MutableDcel s vertex directed undirected face
-denseMutableOwner = dmdOwner
-{-# INLINE denseMutableOwner #-}
-
--- | The physical interpretation required by local topology rewrites. The
--- semantic owner remains 'MutableDcel'; this algebra merely preserves the
--- storage refinement a caller has already proved, so one normalization law
--- specializes to either paged or flat cells instead of growing a sibling
--- rewrite engine.
-class MutableTopology mutable where
-  topologyOwner
-    :: mutable s vertex directed undirected face
-    -> MutableDcel s vertex directed undirected face
-  topologyReadPointX
-    :: mutable s vertex directed undirected face -> Int -> ST s Double
-  topologyReadPointY
-    :: mutable s vertex directed undirected face -> Int -> ST s Double
-  topologyReadOrigin
-    :: mutable s vertex directed undirected face -> Int -> ST s Int
-  topologyReadNext
-    :: mutable s vertex directed undirected face -> Int -> ST s Int
-  topologyReadPrevious
-    :: mutable s vertex directed undirected face -> Int -> ST s Int
-  topologyReadFace
-    :: mutable s vertex directed undirected face -> Int -> ST s Int
-  topologyReadConstraint
-    :: mutable s vertex directed undirected face -> Int -> ST s Bool
-  topologyWriteOrigin
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-  topologyWriteNext
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-  topologyWritePrevious
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-  topologyWriteFace
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-  topologyWriteFaceEdge
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-  topologyWriteVertexOut
-    :: mutable s vertex directed undirected face -> Int -> Int -> ST s ()
-
-instance MutableTopology MutableDcel where
-  topologyOwner = id
-  topologyReadPointX = readPointX
-  topologyReadPointY = readPointY
-  topologyReadOrigin = readOrigin
-  topologyReadNext = readNext
-  topologyReadPrevious = readPrevious
-  topologyReadFace = readFace
-  topologyReadConstraint = readConstraint
-  topologyWriteOrigin = writeOrigin
-  topologyWriteNext = writeNext
-  topologyWritePrevious = writePrevious
-  topologyWriteFace = writeFace
-  topologyWriteFaceEdge = writeFaceEdge
-  topologyWriteVertexOut = writeVertexOut
-  {-# INLINE topologyOwner #-}
-  {-# INLINE topologyReadPointX #-}
-  {-# INLINE topologyReadPointY #-}
-  {-# INLINE topologyReadOrigin #-}
-  {-# INLINE topologyReadNext #-}
-  {-# INLINE topologyReadPrevious #-}
-  {-# INLINE topologyReadFace #-}
-  {-# INLINE topologyReadConstraint #-}
-  {-# INLINE topologyWriteOrigin #-}
-  {-# INLINE topologyWriteNext #-}
-  {-# INLINE topologyWritePrevious #-}
-  {-# INLINE topologyWriteFace #-}
-  {-# INLINE topologyWriteFaceEdge #-}
-  {-# INLINE topologyWriteVertexOut #-}
-
-instance MutableTopology DenseMutableDcel where
-  topologyOwner = denseMutableOwner
-  topologyReadPointX = denseReadPointX
-  topologyReadPointY = denseReadPointY
-  topologyReadOrigin = denseReadOrigin
-  topologyReadNext = denseReadNext
-  topologyReadPrevious = denseReadPrevious
-  topologyReadFace = denseReadFace
-  topologyReadConstraint = denseReadConstraint
-  topologyWriteOrigin = denseWriteOrigin
-  topologyWriteNext = denseWriteNext
-  topologyWritePrevious = denseWritePrevious
-  topologyWriteFace = denseWriteFace
-  topologyWriteFaceEdge = denseWriteFaceEdge
-  topologyWriteVertexOut = denseWriteVertexOut
-  {-# INLINE topologyOwner #-}
-  {-# INLINE topologyReadPointX #-}
-  {-# INLINE topologyReadPointY #-}
-  {-# INLINE topologyReadOrigin #-}
-  {-# INLINE topologyReadNext #-}
-  {-# INLINE topologyReadPrevious #-}
-  {-# INLINE topologyReadFace #-}
-  {-# INLINE topologyReadConstraint #-}
-  {-# INLINE topologyWriteOrigin #-}
-  {-# INLINE topologyWriteNext #-}
-  {-# INLINE topologyWritePrevious #-}
-  {-# INLINE topologyWriteFace #-}
-  {-# INLINE topologyWriteFaceEdge #-}
-  {-# INLINE topologyWriteVertexOut #-}
-
-newMutableDcel :: ElementDefaults directed undirected face -> DcelCapacity -> ST s (MutableDcel s vertex directed undirected face)
-newMutableDcel defaults capacity = newMutableDcelFrom DenseTransaction Nothing defaults capacity Nothing
-
--- | A fresh dense mutable DCEL whose vertex payload plane is uniformly filled.
--- The witness keeps the flat section tied to its canonical owner and permits
--- geometry-only ingress to extend the payload plane without materializing one
--- boxed value per site.
-data DefaultedVertexDcel s vertex directed undirected face = DefaultedVertexDcel
-  { defaultedVertexDcel :: !(MutableDcel s vertex directed undirected face)
-  , defaultedDenseVertexDcel :: !(DenseMutableDcel s vertex directed undirected face)
-  }
-
-newMutableDcelWithVertexDefault
-  :: vertex
-  -> ElementDefaults directed undirected face
-  -> DcelCapacity
-  -> ST s (Either BuildError (DefaultedVertexDcel s vertex directed undirected face))
-newMutableDcelWithVertexDefault vertexDefault defaults capacity = do
-  mutable <-
-    newMutableDcelFrom
-      DenseTransaction
-      (Just vertexDefault)
-      defaults
-      capacity
-      Nothing
-  pure $
-    case denseMutableDcel mutable of
-      Nothing -> Left CircleSweepRequiresDenseStorage
-      Just dense -> Right (DefaultedVertexDcel mutable dense)
-
--- | Open a local-edit transaction: copy-on-write pages, publication
--- proportional to dirtied pages. The section for singleton persistent verbs.
-thawTriangulation
-  :: Int
-  -> Triangulation mode vertex directed undirected face
-  -> ST s (MutableDcel s vertex directed undirected face)
-thawTriangulation maximumVertices triangulation =
-  newMutableDcelFrom
-    LocalTransaction
-    Nothing
-    (triElementDefaults triangulation)
-    (generalDcelCapacity maximumVertices)
-    (Just triangulation)
-
--- | Open a batch transaction: one dense copy up front, flat reads and writes
--- thereafter. The section for sessions and every other many-edit operation.
-thawTriangulationDense
-  :: Int
-  -> Triangulation mode vertex directed undirected face
-  -> ST s (MutableDcel s vertex directed undirected face)
-thawTriangulationDense maximumVertices triangulation =
-  newMutableDcelFrom
-    DenseTransaction
-    Nothing
-    (triElementDefaults triangulation)
-    (generalDcelCapacity maximumVertices)
-    (Just triangulation)
-
-newMutableDcelFrom
-  :: TransactionShape
-  -> Maybe vertex
-  -> ElementDefaults directed undirected face
-  -> DcelCapacity
-  -> Maybe (Triangulation mode vertex directed undirected face)
-  -> ST s (MutableDcel s vertex directed undirected face)
-newMutableDcelFrom shape vertexDefault mdElementDefaults (DcelCapacity requestedVertices requestedHalfEdges requestedFaces) source = do
-  let !existingVertices = maybe 0 (pagedLength . triPointX) source
-      !existingHalfEdges = maybe 0 ((`quot` 4) . pagedLength . triHalfTopology) source
-      !existingFaces = maybe 1 (pagedLength . triFaceEdge) source
-      !vertexCapacity = max existingVertices requestedVertices
-      !halfCapacity = max existingHalfEdges requestedHalfEdges
-      !faceCapacity = max existingFaces requestedFaces
-      vertexDataBase = maybe (emptyBoxedPaged vertexDefault) triVertexData source
-      directedDataBase = maybe (emptyBoxedPaged (Just (defaultDirectedEdgeData mdElementDefaults))) triDirectedData source
-      undirectedDataBase = maybe (emptyBoxedPaged (Just (defaultUndirectedEdgeData mdElementDefaults))) triUndirectedData source
-      faceDataBase = maybe (emptyBoxedPaged (Just (defaultFaceData mdElementDefaults))) triFaceData source
-      constraintBaseCount = maybe 0 triConstraintCount source
-      constraintBaseEdges = maybe IntSet.empty triConstraintEdges source
-      pointIndexBase = maybe MissingPointIndex (DormantPointIndex . triPointIndex) source
-  mdPointX <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointX) source
-  mdPointY <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triPointY) source
-  mdVertexOut <- maybe (newLocalMutablePaged vertexCapacity) (thawPagedShaped shape vertexCapacity . triVertexOut) source
-  mdVertexData <- thawBoxedPaged shape vertexDataBase
-  mdNewConnected <- newLocalMutablePaged (vertexCapacity - existingVertices)
-  mdRecycledNew <- newSTRef IntSet.empty
-  mdHalfTopology <- maybe (newMutablePaged (4 * halfCapacity)) (thawPagedShaped shape (4 * halfCapacity) . triHalfTopology) source
-  mdDirectedData <- thawBoxedPaged shape directedDataBase
-  mdUndirectedData <- thawBoxedPaged shape undirectedDataBase
-  mdConstraint <- maybe (newMutablePaged (halfCapacity `quot` 2)) (thawPagedShaped shape (halfCapacity `quot` 2) . triConstraint) source
-  mdFaceEdge <-
-    case source of
-      Just triangulation -> thawPagedShaped shape faceCapacity (triFaceEdge triangulation)
-      Nothing -> do
-        freshFaceEdges <- newLocalMutablePaged faceCapacity
-        writePaged freshFaceEdges 0 noIndex
-        pure freshFaceEdges
-  mdFaceData <- thawBoxedPaged shape faceDataBase
-  mdPointCount <- newSTRef existingVertices
-  mdPointIndex <- newSTRef pointIndexBase
-  mdConnectedCount <- newSTRef existingVertices
-  mdHalfCount <- newSTRef existingHalfEdges
-  mdFaceCount <- newSTRef existingFaces
-  mdConstraintCount <- newSTRef constraintBaseCount
-  mdConstraintEdges <- newSTRef constraintBaseEdges
-  mdLastFace <- newSTRef (if existingFaces > 1 then 1 else 0)
-  let !mdPayloadsPristine =
-        boxedThawPristine mdDirectedData
-          && boxedThawPristine mdUndirectedData
-          && boxedThawPristine mdFaceData
-  pure
-    MutableDcel
-      { mdInitialPointCount = existingVertices
-      , mdPointCapacity = vertexCapacity
-      , mdHalfCapacity = halfCapacity
-      , mdFaceCapacity = faceCapacity
-      , ..
-      }
-
-freezeTriangulation :: MutableDcel s vertex directed undirected face -> ST s (Either BuildError (Triangulation mode vertex directed undirected face))
-freezeTriangulation mutable =
-  fmap (fmap fst) (freezeTriangulationCore (const (pure emptyPublicationStats)) mutable)
-
-freezeTriangulationWithStats
-  :: MutableDcel s vertex directed undirected face
-  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
-freezeTriangulationWithStats = freezeTriangulationCore readMutableDcelPublicationStats
-
-freezeTriangulationCore
-  :: (MutableDcel s vertex directed undirected face -> ST s PublicationStats)
-  -> MutableDcel s vertex directed undirected face
-  -> ST s (Either BuildError (Triangulation mode vertex directed undirected face, PublicationStats))
-freezeTriangulationCore readPublicationStats mutable@MutableDcel
-  { mdPointX
-  , mdPointY
-  , mdPointIndex
-  , mdVertexOut
-  , mdVertexData
-  , mdHalfTopology
-  , mdDirectedData
-  , mdUndirectedData
-  , mdConstraint
-  , mdFaceEdge
-  , mdFaceData
-  , mdPointCount
-  , mdHalfCount
-  , mdFaceCount
-  , mdConstraintCount
-  , mdConstraintEdges
-  , mdElementDefaults
-  } = do
-    vertices <- readSTRef mdPointCount
-    halfEdges <- readSTRef mdHalfCount
-    faces <- readSTRef mdFaceCount
-    triConstraintCount <- readSTRef mdConstraintCount
-    triConstraintEdges <- readSTRef mdConstraintEdges
-    vertexDataOutcome <- freezeBoxedPaged vertices mdVertexData
-    directedDataOutcome <- freezeBoxedPaged halfEdges mdDirectedData
-    undirectedDataOutcome <- freezeBoxedPaged (halfEdges `quot` 2) mdUndirectedData
-    faceDataOutcome <- freezeBoxedPaged faces mdFaceData
-    case
-        (,,,)
-          <$> vertexDataOutcome
-          <*> directedDataOutcome
-          <*> undirectedDataOutcome
-          <*> faceDataOutcome
-      of
-        Left obstruction -> pure (Left (PayloadStorageFailure obstruction))
-        Right (triVertexData, triDirectedData, triUndirectedData, triFaceData) -> do
-          triPointX <- freezePaged vertices mdPointX
-          triPointY <- freezePaged vertices mdPointY
-          pointIndexState <- readSTRef mdPointIndex
-          let triPointIndex =
-                case pointIndexState of
-                  DormantPointIndex residentIndex -> residentIndex
-                  ActivePersistentPointIndex residentIndex -> residentIndex
-                  ActiveBatchPointIndex _ -> buildPointIndex triPointX triPointY
-                  MissingPointIndex -> buildPointIndex triPointX triPointY
-          triVertexOut <- freezePaged vertices mdVertexOut
-          triHalfTopology <- freezePaged (4 * halfEdges) mdHalfTopology
-          triConstraint <- freezePaged (halfEdges `quot` 2) mdConstraint
-          triFaceEdge <- freezePaged faces mdFaceEdge
-          publicationStats <- readPublicationStats mutable
-          let triElementDefaults = mdElementDefaults
-          pure (Right (Triangulation{triSeamFrontier = Nothing, ..}, publicationStats))
-
-readMutableDcelPublicationStats
-  :: MutableDcel s vertex directed undirected face
-  -> ST s PublicationStats
-readMutableDcelPublicationStats MutableDcel
-  { mdPointX
-  , mdPointY
-  , mdVertexOut
-  , mdHalfTopology
-  , mdConstraint
-  , mdFaceEdge
-  , mdVertexData
-  , mdDirectedData
-  , mdUndirectedData
-  , mdFaceData
-  } =
-  foldl'
-    addPublicationStats
-    emptyPublicationStats
-    <$> sequence
-      [ readMutablePagedPublicationStats mdPointX
-      , readMutablePagedPublicationStats mdPointY
-      , readMutablePagedPublicationStats mdVertexOut
-      , readMutablePagedPublicationStats mdHalfTopology
-      , readMutablePagedPublicationStats mdConstraint
-      , readMutablePagedPublicationStats mdFaceEdge
-      , readMutableBoxedPublicationStats mdVertexData
-      , readMutableBoxedPublicationStats mdDirectedData
-      , readMutableBoxedPublicationStats mdUndirectedData
-      , readMutableBoxedPublicationStats mdFaceData
-      ]
-
-pointCapacity :: MutableDcel s vertex directed undirected face -> Int
-pointCapacity = mdPointCapacity
-{-# INLINE pointCapacity #-}
-
-halfEdgeCapacity :: MutableDcel s vertex directed undirected face -> Int
-halfEdgeCapacity = mdHalfCapacity
-{-# INLINE halfEdgeCapacity #-}
-
-pointCount :: MutableDcel s vertex directed undirected face -> ST s Int
-pointCount = readSTRef . mdPointCount
-{-# INLINE pointCount #-}
-
-connectedCount :: MutableDcel s vertex directed undirected face -> ST s Int
-connectedCount = readSTRef . mdConnectedCount
-{-# INLINE connectedCount #-}
-
-directedEdgeCount :: MutableDcel s vertex directed undirected face -> ST s Int
-directedEdgeCount = readSTRef . mdHalfCount
-{-# INLINE directedEdgeCount #-}
-
-faceCount :: MutableDcel s vertex directed undirected face -> ST s Int
-faceCount = readSTRef . mdFaceCount
-{-# INLINE faceCount #-}
-
-pointAt :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
-pointAt MutableDcel{mdPointX, mdPointY} index =
-  Point <$> readPaged mdPointX index <*> readPaged mdPointY index
-{-# INLINE pointAt #-}
-
--- | Resolve a canonical site through the derived handle index, confirming
--- every hash candidate against the authoritative coordinate planes. A mesh
--- created from scratch derives the index only if a caller actually asks; a
--- thawed published mesh inherits its structurally shared index.
-lookupPointVertex
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> ST s (Maybe Int)
-lookupPointVertex mutable@MutableDcel{mdPointIndex} rawPoint = do
-  indexState <- readSTRef mdPointIndex
-  case canonicalPoint rawPoint of
-    Point x y ->
-      case indexState of
-        ActiveBatchPointIndex table ->
-          lookupMutablePoint table (readPointX mutable) (readPointY mutable) x y
-        ActivePersistentPointIndex residentIndex ->
-          resolvePersistent x y residentIndex
-        DormantPointIndex residentIndex -> do
-          writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
-          resolvePersistent x y residentIndex
-        MissingPointIndex -> do
-          derived <- deriveMutablePointIndex mutable
-          writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
-          resolvePersistent x y derived
- where
-  resolvePersistent x y pointIndex =
-    foldM
-      (confirmCandidate mutable x y)
-      Nothing
-      (pointIndexCandidates x y pointIndex)
-
--- | Whether the transaction has already committed to incremental identity
--- transport. Answering does not force a dormant index's lazy rebuild, which
--- is the point: a per-question caller must not buy a whole-mesh build.
-identityIndexActive :: MutableDcel s vertex directed undirected face -> ST s Bool
-identityIndexActive MutableDcel{mdPointIndex} = do
-  indexState <- readSTRef mdPointIndex
-  pure $ case indexState of
-    ActivePersistentPointIndex _ -> True
-    ActiveBatchPointIndex _ -> True
-    _ -> False
-{-# INLINE identityIndexActive #-}
-
--- | Declare that a singleton handle-keyed rewrite must transport the resident
--- immutable identity section strictly. Dense point-keyed removal uses
--- 'activateBatchPointIndex' instead, so it neither forces nor incrementally
--- allocates the published 'PointIndex'.
-activatePointIndex
-  :: MutableDcel s vertex directed undirected face
-  -> ST s ()
-activatePointIndex mutable@MutableDcel{mdPointIndex} = do
-  indexState <- readSTRef mdPointIndex
-  case indexState of
-    ActivePersistentPointIndex _ -> pure ()
-    ActiveBatchPointIndex _ -> pure ()
-    DormantPointIndex residentIndex ->
-      writeSTRef mdPointIndex (ActivePersistentPointIndex residentIndex)
-    MissingPointIndex -> do
-      derived <- deriveMutablePointIndex mutable
-      writeSTRef mdPointIndex (ActivePersistentPointIndex derived)
-
--- | Open an identity section for one dense removal program. The table derives
--- only from coordinate authority, and its extent is deliberately narrower
--- than the surrounding session: publication returns to the lazy immutable
--- derivation instead of retaining a second mutable identity owner.
-activateBatchPointIndex
-  :: MutableDcel s vertex directed undirected face
-  -> ST s (Either BuildError ())
-activateBatchPointIndex mutable@MutableDcel{mdPointIndex} = do
-  vertices <- pointCount mutable
-  table <- newMutablePointIndex vertices
-  seeded <-
-    seedMutablePointIndex
-      table
-      vertices
-      (readPointX mutable)
-      (readPointY mutable)
-  case seeded of
-    Left failure -> pure (Left failure)
-    Right () -> do
-      writeSTRef mdPointIndex (ActiveBatchPointIndex table)
-      pure (Right ())
--- | Close the batch-local identity section after its removal program. Its
--- contents cannot escape @ST@; marking the cache missing makes freeze glue a
--- lazy immutable derivation from the final coordinate arenas.
-discardBatchPointIndex
-  :: MutableDcel s vertex directed undirected face
-  -> ST s ()
-discardBatchPointIndex MutableDcel{mdPointIndex} =
-  modifySTRef'
-    mdPointIndex
-    (\indexState ->
-       case indexState of
-         ActiveBatchPointIndex _ -> MissingPointIndex
-         retained -> retained
-    )
-
-deriveMutablePointIndex
-  :: MutableDcel s vertex directed undirected face
-  -> ST s PointIndex
-deriveMutablePointIndex mutable = do
-  vertices <- pointCount mutable
-  foldM insertResident emptyPointIndex [0 .. vertices - 1]
- where
-  insertResident pointIndex vertex = do
-    x <- readPointX mutable vertex
-    y <- readPointY mutable vertex
-    pure (insertPointIndex x y vertex pointIndex)
-
-confirmCandidate
-  :: MutableDcel s vertex directed undirected face
-  -> Double
-  -> Double
-  -> Maybe Int
-  -> Int
-  -> ST s (Maybe Int)
-confirmCandidate _ _ _ resident@(Just _) _ = pure resident
-confirmCandidate mutable x y Nothing candidate = do
-  heldX <- readPointX mutable candidate
-  heldY <- readPointY mutable candidate
-  pure (if heldX == x && heldY == y then Just candidate else Nothing)
-{-# INLINE confirmCandidate #-}
-
--- | Read one stored coordinate without building a t'Point'. Coordinates are
--- the authoritative state owned by 'mdPointX'/'mdPointY'; the t'Point'
--- constructor is the cold accessor's packaging, and the construction kernel
--- reads these arenas directly so a specialized sweep never boxes one.
-readPointX :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
-readPointX MutableDcel{mdPointX} index = readPaged mdPointX index
-{-# INLINE readPointX #-}
-
-readPointY :: MutableDcel s vertex directed undirected face -> Int -> ST s Double
-readPointY MutableDcel{mdPointY} index = readPaged mdPointY index
-{-# INLINE readPointY #-}
-
-writePoint :: MutableDcel s vertex directed undirected face -> Int -> Point -> ST s ()
-writePoint MutableDcel{mdPointX, mdPointY} index rawPoint =
-  case canonicalPoint rawPoint of
-    Point x y -> do
-      writePaged mdPointX index x
-      writePaged mdPointY index y
-{-# INLINE writePoint #-}
-
-vertexDataAt :: MutableDcel s vertex directed undirected face -> Int -> ST s vertex
-vertexDataAt MutableDcel{mdVertexData} = readBoxedPaged mdVertexData
-{-# INLINE vertexDataAt #-}
-
-writeVertexData :: MutableDcel s vertex directed undirected face -> Int -> vertex -> ST s ()
-writeVertexData MutableDcel{mdVertexData} = writeBoxedPaged mdVertexData
-{-# INLINE writeVertexData #-}
-
--- | Whether no element payload plane can be holding anything. Constant for the
--- transaction: see 'mdPayloadsPristine'. A rewrite site that performs several
--- resets together tests this once rather than paying the test inside each.
-payloadsPristine :: MutableDcel s vertex directed undirected face -> Bool
-payloadsPristine = mdPayloadsPristine
-{-# INLINE payloadsPristine #-}
-
--- | Return one undirected edge and both its half-edges to the element
--- defaults. A payload labels the element occupying a slot, and an element is
--- its geometry: a rewrite that gives a slot new endpoints has put a different
--- edge there, and the label the old one carried does not describe it. Leaving
--- it would also make the payload plane depend on the flip order that reached
--- the normal form, while the topology does not.
---
--- The constraint flag is deliberately not reset with it. A flag states that a
--- segment of the input is present, and a segment that gets split is still
--- present as its two halves; a payload states what an element is.
-resetEdgeData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-resetEdgeData MutableDcel{mdDirectedData, mdUndirectedData, mdPayloadsPristine, mdElementDefaults} pair =
-  unless mdPayloadsPristine $ do
-    resetBoxedRange mdDirectedData (defaultDirectedEdgeData mdElementDefaults) (2 * pair) 2
-    resetBoxedRange mdUndirectedData (defaultUndirectedEdgeData mdElementDefaults) pair 1
-{-# INLINE resetEdgeData #-}
-
--- | Return one face to the element default. See 'resetEdgeData'.
-resetFaceData :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-resetFaceData MutableDcel{mdFaceData, mdPayloadsPristine, mdElementDefaults} face =
-  unless mdPayloadsPristine (resetBoxedRange mdFaceData (defaultFaceData mdElementDefaults) face 1)
-{-# INLINE resetFaceData #-}
-
-edgeOriginPoint :: MutableDcel s vertex directed undirected face -> Int -> ST s (Point)
-edgeOriginPoint mutable edge = readOrigin mutable edge >>= pointAt mutable
-{-# INLINE edgeOriginPoint #-}
-
-appendVertex
-  :: MutableDcel s vertex directed undirected face
-  -> Point
-  -> vertex
-  -> ST s Int
-appendVertex mutable rawPoint vertexData =
-  case canonicalPoint rawPoint of
-    Point x y -> appendVertexCoordinates mutable x y vertexData
-{-# INLINE appendVertex #-}
-
--- | The next unmaterialized vertex record in one mutable DCEL. The constructor
--- stays private: a caller may transport the candidate to a derived identity
--- index, but cannot fabricate a different arena position for the append that
--- follows. The slot remains lawful only while no intervening append occurs.
-newtype NextVertexSlot s = NextVertexSlot Int
-
-nextVertexSlot
-  :: MutableDcel s vertex directed undirected face
-  -> ST s (NextVertexSlot s)
-nextVertexSlot MutableDcel{mdPointCount} = NextVertexSlot <$> readSTRef mdPointCount
-{-# INLINE nextVertexSlot #-}
-
-nextVertexSlotIndex :: NextVertexSlot s -> Int
-nextVertexSlotIndex (NextVertexSlot vertex) = vertex
-{-# INLINE nextVertexSlotIndex #-}
-
--- | Append a vertex whose coordinates are already canonical, by components.
--- This is the one owner of the append record; 'appendVertex' is its
--- t'Point'-carrying form for callers holding a point. Raw capacity means the
--- appender initializes every field of the record it exposes.
-appendVertexCoordinates
-  :: MutableDcel s vertex directed undirected face
-  -> Double
-  -> Double
-  -> vertex
-  -> ST s Int
-appendVertexCoordinates mutable x y vertexData = do
-  slot <- nextVertexSlot mutable
-  appendVertexCoordinatesAtSlot mutable slot x y vertexData
-{-# INLINE appendVertexCoordinates #-}
-
--- | Append the uniform vertex payload carried by a defaulted-vertex witness.
--- No boxed page is written: extending the authoritative point count extends
--- the defaulted payload plane at freeze.
-appendDefaultVertexCoordinates
-  :: DefaultedVertexDcel s vertex directed undirected face
-  -> Double
-  -> Double
-  -> ST s Int
-appendDefaultVertexCoordinates DefaultedVertexDcel{defaultedVertexDcel = mutable} x y = do
-  slot <- nextVertexSlot mutable
-  initializeVertexCoordinatesAtSlot mutable slot x y
-{-# INLINE appendDefaultVertexCoordinates #-}
-
--- | Materialize the exact fresh record named by a previously acquired slot.
--- Keeping the slot typed and adjacent to identity resolution lets bulk ingress
--- share one point-count read between the identity candidate and the append.
-appendVertexCoordinatesAtSlot
-  :: MutableDcel s vertex directed undirected face
-  -> NextVertexSlot s
-  -> Double
-  -> Double
-  -> vertex
-  -> ST s Int
-appendVertexCoordinatesAtSlot mutable slot@(NextVertexSlot vertex) x y vertexData = do
-  writeVertexData mutable vertex vertexData
-  initializeVertexCoordinatesAtSlot mutable slot x y
-{-# INLINE appendVertexCoordinatesAtSlot #-}
-
-initializeVertexCoordinatesAtSlot
-  :: MutableDcel s vertex directed undirected face
-  -> NextVertexSlot s
-  -> Double
-  -> Double
-  -> ST s Int
-initializeVertexCoordinatesAtSlot mutable@MutableDcel{mdPointCount, mdPointX, mdPointY, mdPointIndex, mdNewConnected, mdRecycledNew} (NextVertexSlot vertex) x y = do
-  writePaged mdPointX vertex x
-  writePaged mdPointY vertex y
-  writeVertexOut mutable vertex (-1)
-  if vertex >= mdInitialPointCount mutable
-    then writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 0
-    else modifySTRef' mdRecycledNew (IntSet.insert vertex)
-  pointIndexState <- readSTRef mdPointIndex
-  case pointIndexState of
-    ActivePersistentPointIndex pointIndex ->
-      writeSTRef mdPointIndex (ActivePersistentPointIndex (insertPointIndex x y vertex pointIndex))
-    -- The batch table is only lawful over the removal subprogram that opened
-    -- it. An insertion before that scope is closed invalidates the derived
-    -- cache rather than pretending an unregistered handle exists.
-    ActiveBatchPointIndex _ -> writeSTRef mdPointIndex MissingPointIndex
-    -- Transported lazily: the field holds a pure update thunk, so a batch that
-    -- never asks an identity question pays one allocation per append, while a
-    -- persistent chain that asks every publication forces a depth-one thunk
-    -- instead of rebuilding the index over the whole mesh.
-    DormantPointIndex pointIndex ->
-      writeSTRef mdPointIndex (DormantPointIndex (insertPointIndex x y vertex pointIndex))
-    -- A missing derived view stays missing without writing its cell once per
-    -- bulk vertex. Freeze already descends from the coordinate authority when
-    -- a future identity query demands the view.
-    MissingPointIndex -> pure ()
-  writeSTRef mdPointCount (vertex + 1)
-  pure vertex
-{-# INLINE initializeVertexCoordinatesAtSlot #-}
-
--- | Check the point arena before a local rewrite materializes vertices. The
--- caller performs this before the first write, so refusal needs no rollback.
-ensurePointCapacity
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s (Either BuildError ())
-ensurePointCapacity MutableDcel{mdPointCount, mdPointCapacity} additional
-  | additional < 0 = pure (Left (CapacityExceeded additional))
-  | otherwise = do
-      current <- readSTRef mdPointCount
-      let !required = current + additional
-      pure $
-        if required > mdPointCapacity
-          then Left (CapacityExceeded required)
-          else Right ()
-{-# INLINE ensurePointCapacity #-}
-
-markConnected :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-markConnected mutable@MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew, mdConnectedCount} vertex outgoing = do
-  if vertex >= mdInitialPointCount
-    then do
-      let !newVertex = vertex - mdInitialPointCount
-      connected <- readPaged mdNewConnected newVertex
-      unless (connected /= 0) $ do
-        writePaged mdNewConnected newVertex 1
-        modifySTRef' mdConnectedCount (+ 1)
-    else do
-      recycled <- readSTRef mdRecycledNew
-      when (IntSet.member vertex recycled) $ do
-        writeSTRef mdRecycledNew (IntSet.delete vertex recycled)
-        modifySTRef' mdConnectedCount (+ 1)
-  writeVertexOut mutable vertex outgoing
-{-# INLINE markConnected #-}
-
-isConnected :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
-isConnected MutableDcel{mdInitialPointCount, mdNewConnected, mdRecycledNew} vertex
-  | vertex < mdInitialPointCount = IntSet.notMember vertex <$> readSTRef mdRecycledNew
-  | otherwise = (/= 0) <$> readPaged mdNewConnected (vertex - mdInitialPointCount)
-{-# INLINE isConnected #-}
-
-addEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s (Int, Int)
-addEdge mutable from to = do
-  base <- addEdgeBlock mutable 1
-  writeOrigin mutable base from
-  writeOrigin mutable (base + 1) to
-  pure (base, base + 1)
-{-# INLINE addEdge #-}
-
--- Each appended pair owns the initialization of its exposed topology and
--- constraint records; reserved capacity remains untouched.
-addEdgeBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-addEdgeBlock mutable@MutableDcel{mdHalfCount, mdConstraint} pairs = do
-  base <- readSTRef mdHalfCount
-  let !required = base + 2 * pairs
-      !firstUndirected = base `quot` 2
-      !lastUndirected = firstUndirected + pairs - 1
-  forM_ [4 * base .. 4 * required - 1] $ \slot ->
-    writePaged (mdHalfTopology mutable) slot noIndex
-  forM_ [firstUndirected .. lastUndirected] $ \edge -> writePaged mdConstraint edge 0
-  writeSTRef mdHalfCount required
-  pure base
-{-# INLINE addEdgeBlock #-}
-
--- | Append initialized cells through the already-proved contiguous section.
--- The semantic allocator and its tail invariant are identical to
--- 'addEdgeBlock'; only the physical interpreter is selected once rather than
--- once per topology slot.
-denseAddEdgeBlock :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseAddEdgeBlock dense@DenseMutableDcel{dmdOwner = MutableDcel{mdHalfCount}} pairs = do
-  base <- readSTRef mdHalfCount
-  let !required = base + 2 * pairs
-  denseInitializeUnconstrainedEdgeBlock dense base pairs
-  writeSTRef mdHalfCount required
-  pure base
-{-# INLINE denseAddEdgeBlock #-}
-
--- | Initialize the constraint section of a proved fresh directed-edge block
--- without advancing the global arena count. A reserved bulk program threads
--- its allocation cursor immutably and commits the count once after gluing;
--- the ordinary allocator above shares this exact record initializer.
-denseInitializeUnconstrainedEdgeBlock
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> ST s ()
-denseInitializeUnconstrainedEdgeBlock DenseMutableDcel{dmdConstraint} directedBase pairs =
-  forM_ [directedBase `quot` 2 .. directedBase `quot` 2 + pairs - 1] $ \edge ->
-    writeFlatMutable dmdConstraint edge 0
-{-# INLINE denseInitializeUnconstrainedEdgeBlock #-}
-
-addFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-addFace mutable anchor = do
-  base <- addFaceBlock mutable 1
-  writeFaceEdge mutable base anchor
-  pure base
-{-# INLINE addFace #-}
-
-addFaceBlock :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-addFaceBlock mutable@MutableDcel{mdFaceCount} count = do
-  base <- readSTRef mdFaceCount
-  let !required = base + count
-  mapM_ (\face -> writeFaceEdge mutable face (-1)) [base .. required - 1]
-  writeSTRef mdFaceCount required
-  pure base
-{-# INLINE addFaceBlock #-}
-
-denseAddFaceBlock :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseAddFaceBlock DenseMutableDcel{dmdOwner = MutableDcel{mdFaceCount}} count = do
-  base <- readSTRef mdFaceCount
-  let !required = base + count
-  writeSTRef mdFaceCount required
-  pure base
-{-# INLINE denseAddFaceBlock #-}
-
--- | Check the local allocation section before any topology rewrite begins.
--- A refusal leaves every mutable plane untouched, so the enclosing transaction
--- can abandon publication without rollback machinery.
-ensureCellCapacity
-  :: MutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> ST s (Either BuildError ())
-ensureCellCapacity MutableDcel{mdHalfCount, mdHalfCapacity, mdFaceCount, mdFaceCapacity} additionalPairs additionalFaces
-  | additionalPairs < 0 = pure (Left (HalfEdgeCapacityExceeded additionalPairs mdHalfCapacity))
-  | additionalFaces < 0 = pure (Left (FaceCapacityExceeded additionalFaces mdFaceCapacity))
-  | otherwise = do
-      currentHalfEdges <- readSTRef mdHalfCount
-      currentFaces <- readSTRef mdFaceCount
-      let !requiredHalfEdges = currentHalfEdges + 2 * additionalPairs
-          !requiredFaces = currentFaces + additionalFaces
-      if requiredHalfEdges > mdHalfCapacity
-        then pure (Left (HalfEdgeCapacityExceeded requiredHalfEdges mdHalfCapacity))
-        else
-          if requiredFaces > mdFaceCapacity
-            then pure (Left (FaceCapacityExceeded requiredFaces mdFaceCapacity))
-            else pure (Right ())
-{-# INLINE ensureCellCapacity #-}
-
-truncatePoints :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-truncatePoints MutableDcel{mdPointCount, mdConnectedCount} count = do
-  writeSTRef mdPointCount count
-  connected <- readSTRef mdConnectedCount
-  when (connected > count) (writeSTRef mdConnectedCount count)
-
-truncateDirectedEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-truncateDirectedEdges MutableDcel{mdHalfCount} = writeSTRef mdHalfCount
-
-truncateFaces :: MutableDcel s vertex directed undirected face -> Int -> ST s ()
-truncateFaces MutableDcel{mdFaceCount, mdLastFace} count = do
-  writeSTRef mdFaceCount count
-  lastFace <- readSTRef mdLastFace
-  when (lastFace >= count) (writeSTRef mdLastFace (if count > 1 then 1 else 0))
-
--- | Remove one undirected edge by moving the last pair into its slot. Only the
--- two neighboring links, one vertex representative, and one face
--- representative per moved half-edge can reference the old handles.
---
--- The slot the tail vacates is returned to the defaults here rather than when
--- the allocator hands it back out. That keeps one invariant — every slot at or
--- above the live count holds the fill — which the thaw establishes, this
--- preserves, and 'addEdgeBlock' may therefore assume without testing anything.
--- The cost lands on removal, which is where the element was retired, instead of
--- on every allocation a pure insertion makes.
-swapRemoveUndirectedEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
-swapRemoveUndirectedEdge mutable@MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} pair = do
-  halfEdges <- directedEdgeCount mutable
-  let !pairs = halfEdges `quot` 2
-      !lastPair = pairs - 1
-  if pair < 0 || pair > lastPair
-    then
-      pure
-        ( Left
-            ( RemovalEdgeOutOfRange
-                (UndirectedEdgeId (fromIntegral pair))
-                pairs
-            )
-        )
-    else do
-      swapRemoveUndirectedEdgeInRange lastPair
-      pure (Right ())
- where
-  swapRemoveUndirectedEdgeInRange lastPair = do
-    removedFlag <- readPaged mdConstraint pair
-    lastFlag <- readPaged mdConstraint lastPair
-    when (removedFlag /= 0) (modifySTRef' mdConstraintCount (subtract 1))
-    when (pair /= lastPair) $ do
-      let !oldBase = 2 * lastPair
-          !newBase = 2 * pair
-          remap !handle
-            | handle == oldBase = newBase
-            | handle == oldBase + 1 = newBase + 1
-            | otherwise = handle
-      -- Both records are taken before either is republished: the pair's two
-      -- half-edges can name each other, so a read after the first write would
-      -- see the new handle where the old one belongs.
-      !forwardOrigin <- readOrigin mutable oldBase
-      !forwardNext <- remap <$> readNext mutable oldBase
-      !forwardPrevious <- remap <$> readPrevious mutable oldBase
-      !forwardFace <- readFace mutable oldBase
-      !backwardOrigin <- readOrigin mutable (oldBase + 1)
-      !backwardNext <- remap <$> readNext mutable (oldBase + 1)
-      !backwardPrevious <- remap <$> readPrevious mutable (oldBase + 1)
-      !backwardFace <- readFace mutable (oldBase + 1)
-      writePaged mdConstraint pair lastFlag
-      unless (mdPayloadsPristine mutable) $ do
-        readBoxedPaged (mdUndirectedData mutable) lastPair >>= writeBoxedPaged (mdUndirectedData mutable) pair
-        readBoxedPaged (mdDirectedData mutable) oldBase >>= writeBoxedPaged (mdDirectedData mutable) newBase
-        readBoxedPaged (mdDirectedData mutable) (oldBase + 1) >>= writeBoxedPaged (mdDirectedData mutable) (newBase + 1)
-      writeOrigin mutable newBase forwardOrigin
-      writeNext mutable newBase forwardNext
-      writePrevious mutable newBase forwardPrevious
-      writeFace mutable newBase forwardFace
-      writeOrigin mutable (newBase + 1) backwardOrigin
-      writeNext mutable (newBase + 1) backwardNext
-      writePrevious mutable (newBase + 1) backwardPrevious
-      writeFace mutable (newBase + 1) backwardFace
-      writeNext mutable forwardPrevious newBase
-      writePrevious mutable forwardNext newBase
-      writeVertexOut mutable forwardOrigin newBase
-      writeFaceEdge mutable forwardFace newBase
-      writeNext mutable backwardPrevious (newBase + 1)
-      writePrevious mutable backwardNext (newBase + 1)
-      writeVertexOut mutable backwardOrigin (newBase + 1)
-      writeFaceEdge mutable backwardFace (newBase + 1)
-    modifySTRef'
-      mdConstraintEdges
-      (\edges ->
-         let withoutRetired = IntSet.delete pair (IntSet.delete lastPair edges)
-          in if pair /= lastPair && lastFlag /= 0
-               then IntSet.insert pair withoutRetired
-               else withoutRetired
-      )
-    resetEdgeData mutable lastPair
-    truncateDirectedEdges mutable (2 * lastPair)
-
-swapRemoveFace :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError ())
-swapRemoveFace mutable face = do
-  faces <- faceCount mutable
-  let !lastFace = faces - 1
-  if face <= 0 || face > lastFace
-    then
-      pure
-        ( Left
-            (RemovalFaceOutOfRange (FaceId (fromIntegral face)) faces)
-        )
-    else do
-      relocated <-
-        if face == lastFace
-          then pure (Right ())
-          else relocateLastFace lastFace
-      case relocated of
-        Left obstruction -> pure (Left obstruction)
-        Right () -> do
-          resetFaceData mutable lastFace
-          truncateFaces mutable lastFace
-          pure (Right ())
- where
-  relocateLastFace lastFace = do
-    start <- readFaceEdge mutable lastFace
-    writeFaceEdge mutable face start
-    -- Swap-compaction moves the last face's index, not the face. When the
-    -- locator's cached start is that face, following it here is the difference
-    -- between a batch of removals resuming where the previous one settled and
-    -- 'truncateFaces' finding the cached index out of range and resetting it to
-    -- the first inner face. The cache is a start, never an answer.
-    cachedFace <- readSTRef (mdLastFace mutable)
-    when (cachedFace == lastFace) (writeSTRef (mdLastFace mutable) face)
-    unless (mdPayloadsPristine mutable) $
-      readBoxedPaged (mdFaceData mutable) lastFace >>= writeBoxedPaged (mdFaceData mutable) face
-    halfEdges <- directedEdgeCount mutable
-    let go !remaining !current !seen
-          | remaining <= 0 =
-              pure
-                ( Left
-                    ( RemovalFaceCycleDidNotTerminate
-                        (FaceId (fromIntegral lastFace))
-                        (DirectedEdgeId (fromIntegral current))
-                        (halfEdges + 1)
-                    )
-                )
-          | seen && current == start = pure (Right ())
-          | otherwise = do
-              writeFace mutable current face
-              nextEdge <- readNext mutable current
-              go (remaining - 1) nextEdge True
-    go (halfEdges + 1) start False
-
--- | Retire a vertex by moving the arena's last into its slot. The relocation is
--- reported as the slot together with the position now standing in it: the two
--- are one fact, and a caller told only the slot has to consult the mesh to
--- learn what landed there.
-swapRemoveVertex :: MutableDcel s vertex directed undirected face -> Int -> ST s (Either BuildError (Point, vertex, Maybe (Int, Point)))
-swapRemoveVertex mutable@MutableDcel{mdConnectedCount, mdPointIndex, mdRecycledNew, mdNewConnected} vertex = do
-  vertices <- pointCount mutable
-  let !lastVertex = vertices - 1
-  if vertex < 0 || vertex > lastVertex
-    then
-      pure
-        ( Left
-            (RemovalVertexOutOfRange (VertexId (fromIntegral vertex)) vertices)
-        )
-    else swapRemoveVertexInRange lastVertex
- where
-  swapRemoveVertexInRange lastVertex = do
-    removedPoint <- pointAt mutable vertex
-    removedPayload <- vertexDataAt mutable vertex
-    movedOutcome <-
-      if vertex == lastVertex
-        then pure (Right Nothing)
-        else moveTailVertex lastVertex
-    case movedOutcome of
-      Left obstruction -> pure (Left obstruction)
-      Right moved -> do
-        indexState <- readSTRef mdPointIndex
-        updatedIndexState <-
-          updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved
-        writeSTRef mdPointIndex updatedIndexState
-        -- The connectivity companions must agree with the aggregate assertion two
-        -- lines down: after a swap removal every surviving vertex is connected. The
-        -- retired slot's entry and the relocated occupant's old entry are both
-        -- stale, and a relocated occupant landing in the appended region must read
-        -- connected through the offset store, not through its predecessor's bit.
-        modifySTRef' mdRecycledNew (IntSet.delete vertex . IntSet.delete lastVertex)
-        case moved of
-          Just _
-            | vertex >= mdInitialPointCount mutable ->
-                writePaged mdNewConnected (vertex - mdInitialPointCount mutable) 1
-          _ -> pure ()
-        truncatePoints mutable lastVertex
-        writeSTRef mdConnectedCount lastVertex
-        pure (Right (removedPoint, removedPayload, moved))
-
-  moveTailVertex lastVertex = do
-    movedPoint <- pointAt mutable lastVertex
-    movedPayload <- vertexDataAt mutable lastVertex
-    movedOut <- readVertexOut mutable lastVertex
-    writePoint mutable vertex movedPoint
-    writeVertexData mutable vertex movedPayload
-    writeVertexOut mutable vertex movedOut
-    relocated <-
-      if movedOut < 0
-        then pure (Right ())
-        else relocateOutgoingCycle lastVertex movedOut
-    pure (Just (vertex, movedPoint) <$ relocated)
-
-  relocateOutgoingCycle lastVertex movedOut = do
-    halfEdges <- directedEdgeCount mutable
-    let go !remaining !current !seen
-          | remaining <= 0 =
-              pure
-                ( Left
-                    ( RemovalOutgoingCycleDidNotTerminate
-                        (VertexId (fromIntegral lastVertex))
-                        (DirectedEdgeId (fromIntegral current))
-                        (halfEdges + 1)
-                    )
-                )
-          | seen && current == movedOut = pure (Right ())
-          | otherwise = do
-              writeOrigin mutable current vertex
-              previousEdge <- readPrevious mutable current
-              go (remaining - 1) (previousEdge `xor` 1) True
-    go (halfEdges + 1) movedOut False
-
--- | Transport the identity view across one vertex swap. Persistent sections
--- retain their existing pure update law. A batch table mutates in place, then
--- deliberately falls back to @MissingPointIndex@ if either local proof cannot
--- be completed; geometry remains authoritative and subsequent operations walk
--- rather than observe a stale cache.
-updatePointIndexAfterSwap
-  :: MutableDcel s vertex directed undirected face
-  -> MutablePointIndexState s
-  -> Point
-  -> Int
-  -> Int
-  -> Maybe (Int, Point)
-  -> ST s (MutablePointIndexState s)
-updatePointIndexAfterSwap mutable indexState removedPoint vertex lastVertex moved =
-  case indexState of
-    DormantPointIndex pointIndex ->
-      pure (DormantPointIndex (updatePersistentPointIndex pointIndex))
-    ActivePersistentPointIndex pointIndex ->
-      pure (ActivePersistentPointIndex (updatePersistentPointIndex pointIndex))
-    ActiveBatchPointIndex table ->
-      transportBatchPointIndex table
-    MissingPointIndex -> pure MissingPointIndex
- where
-  updatePersistentPointIndex pointIndex =
-    case removedPoint of
-      Point removedX removedY ->
-        let withoutRemoved = removePointIndex removedX removedY vertex pointIndex
-         in case moved of
-              Nothing -> withoutRemoved
-              Just (_, Point movedX movedY) ->
-                relocatePointIndex movedX movedY lastVertex vertex withoutRemoved
-
-  transportBatchPointIndex table =
-    case removedPoint of
-      Point removedX removedY -> do
-        removed <-
-          removeMutablePoint
-            table
-            (readPointX mutable)
-            (readPointY mutable)
-            removedX
-            removedY
-            vertex
-        case (removed, moved) of
-          (MutablePointIndexUpdated, Nothing) ->
-            pure (ActiveBatchPointIndex table)
-          (MutablePointIndexUpdated, Just (_, Point movedX movedY)) -> do
-            relocated <- relocateMutablePoint table movedX movedY lastVertex vertex
-            pure $
-              case relocated of
-                MutablePointIndexUpdated -> ActiveBatchPointIndex table
-                MutablePointIndexInvalidated -> MissingPointIndex
-          (MutablePointIndexInvalidated, _) -> pure MissingPointIndex
-
-  -- 'swapRemoveVertex' calls us before truncation. The tail's coordinate cells
-  -- still carry the moved point, so backward-shift repair can derive every
-  -- occupant home from the same canonical storage that the table indexes.
-linkEdges :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-linkEdges mutable left right = do
-  writeNext mutable left right
-  writePrevious mutable right left
-{-# INLINE linkEdges #-}
-
-setCycle3 :: MutableDcel s vertex directed undirected face -> Int -> Int -> Int -> Int -> ST s ()
-setCycle3 mutable face e0 e1 e2 = do
-  writeNext mutable e0 e1
-  writeNext mutable e1 e2
-  writeNext mutable e2 e0
-  writePrevious mutable e0 e2
-  writePrevious mutable e1 e0
-  writePrevious mutable e2 e1
-  writeFace mutable e0 face
-  writeFace mutable e1 face
-  writeFace mutable e2 face
-  writeFaceEdge mutable face e0
-{-# INLINE setCycle3 #-}
-
-faceEdges :: MutableDcel s vertex directed undirected face -> Int -> ST s (Int, Int, Int)
-faceEdges mutable face = do
-  e0 <- readFaceEdge mutable face
-  e1 <- readNext mutable e0
-  e2 <- readNext mutable e1
-  pure (e0, e1, e2)
-{-# INLINE faceEdges #-}
-
-denseLinkEdges :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseLinkEdges dense left right = do
-  denseWriteNext dense left right
-  denseWritePrevious dense right left
-{-# INLINE denseLinkEdges #-}
-
-denseSetCycle3 :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> Int -> Int -> ST s ()
-denseSetCycle3 dense face e0 e1 e2 = do
-  denseWriteNext dense e0 e1
-  denseWriteNext dense e1 e2
-  denseWriteNext dense e2 e0
-  denseWritePrevious dense e0 e2
-  denseWritePrevious dense e1 e0
-  denseWritePrevious dense e2 e1
-  denseWriteFace dense e0 face
-  denseWriteFace dense e1 face
-  denseWriteFace dense e2 face
-  denseWriteFaceEdge dense face e0
-{-# INLINE denseSetCycle3 #-}
-
-denseFaceEdges :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s (Int, Int, Int)
-denseFaceEdges dense face = do
-  e0 <- denseReadFaceEdge dense face
-  e1 <- denseReadNext dense e0
-  e2 <- denseReadNext dense e1
-  pure (e0, e1, e2)
-{-# INLINE denseFaceEdges #-}
-
-denseReadPointX :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Double
-denseReadPointX DenseMutableDcel{dmdPointX} = readFlatMutable dmdPointX
-denseReadPointY :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Double
-denseReadPointY DenseMutableDcel{dmdPointY} = readFlatMutable dmdPointY
-{-# INLINE denseReadPointX #-}
-{-# INLINE denseReadPointY #-}
-
--- | Materialize one coordinate slot in a fresh dense point section. The
--- caller owns the capacity witness and supplies the final cardinality once;
--- the point index remains the fresh owner's missing derived view until
--- publication rebuilds it from these authoritative coordinate planes.
-denseWriteFreshDefaultPoint
-  :: DefaultedVertexDcel s vertex directed undirected face
-  -> Int
-  -> Double
-  -> Double
-  -> ST s ()
-denseWriteFreshDefaultPoint
-  DefaultedVertexDcel
-    { defaultedDenseVertexDcel =
-        dense@DenseMutableDcel
-          { dmdOwner = MutableDcel{mdInitialPointCount}
-          , dmdPointX
-          , dmdPointY
-          , dmdNewConnected
-          }
-    }
-  vertex
-  x
-  y = do
-  writeFlatMutable dmdPointX vertex x
-  writeFlatMutable dmdPointY vertex y
-  denseWriteVertexOut dense vertex (-1)
-  writeFlatMutable dmdNewConnected (vertex - mdInitialPointCount) 0
-{-# INLINE denseWriteFreshDefaultPoint #-}
-
--- | Glue a completed fresh point section into the mutable owner's logical
--- extent. Per-site writes remain local; the aggregate cardinality descends
--- once after the section is complete.
-denseCommitFreshDefaultPoints
-  :: DefaultedVertexDcel s vertex directed undirected face
-  -> Int
-  -> ST s ()
-denseCommitFreshDefaultPoints
-  DefaultedVertexDcel
-    { defaultedDenseVertexDcel = DenseMutableDcel
-      { dmdOwner = MutableDcel{mdPointCount}
-      }
-    }
-  = writeSTRef mdPointCount
-{-# INLINE denseCommitFreshDefaultPoints #-}
-
-denseReadOrigin :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseReadOrigin DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index)
-denseReadNext :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseReadNext DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 1)
-denseReadPrevious :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseReadPrevious DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 2)
-denseReadFace :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseReadFace DenseMutableDcel{dmdHalfTopology} index = unpackIndex <$> readFlatMutable dmdHalfTopology (4 * index + 3)
-{-# INLINE denseReadOrigin #-}
-{-# INLINE denseReadNext #-}
-{-# INLINE denseReadPrevious #-}
-{-# INLINE denseReadFace #-}
-
-denseWriteOrigin :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWriteOrigin DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index) (packIndex value)
-denseWriteNext :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWriteNext DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 1) (packIndex value)
-denseWritePrevious :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWritePrevious DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 2) (packIndex value)
-denseWriteFace :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWriteFace DenseMutableDcel{dmdHalfTopology} index value = writeFlatMutable dmdHalfTopology (4 * index + 3) (packIndex value)
-{-# INLINE denseWriteOrigin #-}
-{-# INLINE denseWriteNext #-}
-{-# INLINE denseWritePrevious #-}
-{-# INLINE denseWriteFace #-}
-
-denseWriteVertexOut :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWriteVertexOut DenseMutableDcel{dmdVertexOut} index value =
-  writeFlatMutable dmdVertexOut index (if value < 0 then noIndex else packIndex value)
-{-# INLINE denseWriteVertexOut #-}
-
--- | Materialize the connectivity section for a vertex proven fresh by the
--- circle-sweep reservation. Arbitrary insertion retains 'markConnected'; this
--- refined write has no resident case to rediscover, and its aggregate count is
--- committed once after the sweep's local sections glue.
-denseMarkFreshConnected
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> Int
-  -> ST s ()
-denseMarkFreshConnected dense@DenseMutableDcel{dmdOwner = MutableDcel{mdInitialPointCount}, dmdNewConnected} vertex outgoing = do
-  writeFlatMutable dmdNewConnected (vertex - mdInitialPointCount) 1
-  denseWriteVertexOut dense vertex outgoing
-{-# INLINE denseMarkFreshConnected #-}
-
-denseCommitFreshConnections
-  :: DenseMutableDcel s vertex directed undirected face
-  -> Int
-  -> ST s ()
-denseCommitFreshConnections DenseMutableDcel{dmdOwner = MutableDcel{mdConnectedCount}} count =
-  modifySTRef' mdConnectedCount (+ count)
-{-# INLINE denseCommitFreshConnections #-}
-
-denseReadFaceEdge :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Int
-denseReadFaceEdge DenseMutableDcel{dmdFaceEdge} index = do
-  value <- readFlatMutable dmdFaceEdge index
-  pure (if value == noIndex then -1 else unpackIndex value)
-{-# INLINE denseReadFaceEdge #-}
-
-denseWriteFaceEdge :: DenseMutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-denseWriteFaceEdge DenseMutableDcel{dmdFaceEdge} index value =
-  writeFlatMutable dmdFaceEdge index (if value < 0 then noIndex else packIndex value)
-{-# INLINE denseWriteFaceEdge #-}
-
-denseReadConstraint :: DenseMutableDcel s vertex directed undirected face -> Int -> ST s Bool
-denseReadConstraint DenseMutableDcel{dmdConstraint} directed =
-  (/= 0) <$> readFlatMutable dmdConstraint (directed `quot` 2)
-{-# INLINE denseReadConstraint #-}
-
-readOrigin :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readOrigin MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index)
-readNext :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readNext MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 1)
-readPrevious :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readPrevious MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 2)
-readFace :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readFace MutableDcel{mdHalfTopology} index = unpackIndex <$> readPaged mdHalfTopology (4 * index + 3)
-{-# INLINE readOrigin #-}
-{-# INLINE readNext #-}
-{-# INLINE readPrevious #-}
-{-# INLINE readFace #-}
-
-writeOrigin :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writeOrigin MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index) (packIndex value)
-writeNext :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writeNext MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 1) (packIndex value)
-writePrevious :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writePrevious MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 2) (packIndex value)
-writeFace :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writeFace MutableDcel{mdHalfTopology} index value = writePaged mdHalfTopology (4 * index + 3) (packIndex value)
-{-# INLINE writeOrigin #-}
-{-# INLINE writeNext #-}
-{-# INLINE writePrevious #-}
-{-# INLINE writeFace #-}
-
-readVertexOut :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readVertexOut MutableDcel{mdVertexOut} index = do
-  value <- readPaged mdVertexOut index
-  pure (if value == noIndex then -1 else unpackIndex value)
-{-# INLINE readVertexOut #-}
-
-writeVertexOut :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writeVertexOut MutableDcel{mdVertexOut} index value =
-  writePaged mdVertexOut index (if value < 0 then noIndex else packIndex value)
-{-# INLINE writeVertexOut #-}
-
-readFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> ST s Int
-readFaceEdge MutableDcel{mdFaceEdge} index = do
-  value <- readPaged mdFaceEdge index
-  pure (if value == noIndex then -1 else unpackIndex value)
-{-# INLINE readFaceEdge #-}
-
-writeFaceEdge :: MutableDcel s vertex directed undirected face -> Int -> Int -> ST s ()
-writeFaceEdge MutableDcel{mdFaceEdge} index value =
-  writePaged mdFaceEdge index (if value < 0 then noIndex else packIndex value)
-{-# INLINE writeFaceEdge #-}
-
-readConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
-readConstraint MutableDcel{mdConstraint} directed = (/= 0) <$> readPaged mdConstraint (directed `quot` 2)
-{-# INLINE readConstraint #-}
-
-setConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
-setConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
-  let !index = directed `quot` 2
-  current <- readPaged mdConstraint index
-  if current /= 0
-    then pure False
-    else do
-      writePaged mdConstraint index 1
-      modifySTRef' mdConstraintCount (+ 1)
-      modifySTRef' mdConstraintEdges (IntSet.insert index)
-      pure True
-{-# INLINE setConstraint #-}
-
-clearConstraint :: MutableDcel s vertex directed undirected face -> Int -> ST s Bool
-clearConstraint MutableDcel{mdConstraint, mdConstraintCount, mdConstraintEdges} directed = do
-  let !index = directed `quot` 2
-  current <- readPaged mdConstraint index
-  if current == 0
-    then pure False
-    else do
-      writePaged mdConstraint index 0
-      modifySTRef' mdConstraintCount (subtract 1)
-      modifySTRef' mdConstraintEdges (IntSet.delete index)
-      pure True
-{-# INLINE clearConstraint #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/OperationState.hs b/src-dcel/Moonlight/Triangulation/Internal/OperationState.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/OperationState.hs
+++ /dev/null
@@ -1,205 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | The state one operation owns while it works: the legalization arena, the
--- shared scratch arena, and the instrumentation cells. None of it is
--- topology, so none of it lives on @MutableDcel@ — a transaction allocates
--- this record when it thaws, hands it down to the operations it runs, and
--- reads the counters back once when it freezes. The hot loops thread their
--- stack top, maximum depth and flip count as strict loop variables and charge
--- these cells once per drain; cold events (one per insertion, one per walk
--- probe) charge them where they happen.
-module Moonlight.Triangulation.Internal.OperationState
-  ( Counter (..)
-  , LegalizationArena (..)
-  , OperationState
-  , newOperationState
-  , legalizationArena
-  , legalizationArenaLength
-  , storeLegalizationArena
-  , writeScratch
-  , readScratch
-  , addCounter
-  , setCounter
-  , maxCounter
-  , readCounter
-  , freezeBuildStats
-  ) where
-
-import Control.Monad (when)
-import Control.Monad.ST (ST)
-import Data.STRef (STRef, newSTRef, readSTRef, writeSTRef)
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32, Word64)
-import Moonlight.Triangulation.Internal.Growable
-  ( GrowableWord32
-  , newGrowableWord32
-  , readGrowable
-  , writeGrowable
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (packIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Types (BuildStats (..))
-
--- | The typed candidate section owned by one operation. The newtype prevents
--- unrelated scratch vectors from being handed to the normalizer while
--- erasing to the same contiguous Word32 arena in the hot path.
-newtype LegalizationArena s = LegalizationArena
-  (MUV.MVector s Word32)
-
--- | Instrumentation cells. The first twenty constructors are exactly the
--- t'BuildStats' fields, in t'BuildStats' order; anything after
--- 'CounterRefinementQueuePops' is a diagnostic with no t'BuildStats' field and
--- is read only by instrumented entries, so 'freezeBuildStats' enumerates the
--- leading block and never sees the rest.
-data Counter
-  = CounterInputPoints
-  | CounterUniquePoints
-  | CounterExistingPoints
-  | CounterDuplicatePoints
-  | CounterSpatialSeedPoints
-  | CounterFaceSplits
-  | CounterInteriorEdgeSplits
-  | CounterBoundaryEdgeSplits
-  | CounterHullInsertions
-  | CounterLineSplits
-  | CounterLineExtensions
-  | CounterLineToAreaTransitions
-  | CounterEdgeFlips
-  | CounterLocationWalkSteps
-  | CounterLocationFallbacks
-  | CounterLocationMaxWalk
-  | CounterLegalizationMaxStack
-  | CounterSteinerPoints
-  | CounterRefinementFaceChecks
-  | CounterRefinementQueuePops
-  | CounterSweepFastPoints
-  | CounterSweepSkippedPoints
-  | CounterDiagLegalizationCandidates
-  | CounterDiagHullBucketProbeSteps
-  | CounterDiagHullKeyRebuilds
-  | CounterCount
-  deriving stock (Eq, Ord, Enum, Bounded, Show)
-
--- | One operation's working state. Scratch is fixed and raw: writes always
--- precede reads within an epoch. The legalization vector is held behind one
--- reference solely so an adversarial generic drain can grow it without
--- reintroducing mesh-global work state. A drain reads that reference once,
--- carries the vector and all stack metrics strictly, and stores it once when
--- finished; there is no per-candidate reference traffic.
-data OperationState s = OperationState
-  { osLegalizationArena :: !(STRef s (LegalizationArena s))
-  , osScratchArena :: !(GrowableWord32 s)
-  , osCounters :: !(MUV.MVector s Word64)
-  }
-
--- | Allocate transaction-sized working state. The legalization reservation is a
--- starting size, not a semantic limit: a generic flip pops one candidate and may
--- push four, no linear worst-case depth follows from the input size, and
--- overflow grows the operation-owned vector by doubling. So the reservation is
--- capped. A transaction-sized one charges every singleton verb a fresh block
--- group whose tail no drain reaches, and the doublings that reach a real peak
--- copy less in total than reserving that tail costs.
---
--- Scratch retains four disjoint half-edge sections as its semantic limit: the
--- removal kernel locally glues border, retired-edge, retired-face, and new-fan
--- sections there. Its physical storage still grows only with the star or strip
--- actually observed. Reserving the full mesh bound made every singleton
--- persistent edit allocate an arena whose untouched tail was orders of
--- magnitude larger than the edit.
-newOperationState :: Int -> ST s (OperationState s)
-newOperationState halfEdgeCapacity = do
-  initialArena <- LegalizationArena <$> MUV.new (min (2 * halfEdgeCapacity + 64) initialLegalizationReservation)
-  arena <- newSTRef initialArena
-  scratch <- newGrowableWord32 (min 64 (halfEdgeCapacity + 8))
-  counters <- MUV.replicate (fromEnum CounterCount) 0
-  pure
-    OperationState
-      { osLegalizationArena = arena
-      , osScratchArena = scratch
-      , osCounters = counters
-      }
-
--- | Ordinary cavities are tiny; exceptional stars and recovered strips grow
--- geometrically behind the sealed arena rather than taxing every singleton
--- edit for a pathological frontier it never visits.
-initialLegalizationReservation :: Int
-initialLegalizationReservation = 64
-
-legalizationArena :: OperationState s -> ST s (LegalizationArena s)
-legalizationArena = readSTRef . osLegalizationArena
-{-# INLINE legalizationArena #-}
-
-legalizationArenaLength :: LegalizationArena s -> Int
-legalizationArenaLength (LegalizationArena values) = MUV.length values
-{-# INLINE legalizationArenaLength #-}
-
-storeLegalizationArena :: OperationState s -> LegalizationArena s -> ST s ()
-storeLegalizationArena = writeSTRef . osLegalizationArena
-{-# INLINE storeLegalizationArena #-}
-
--- | Write a scratch cell. Collection walks carry their topology-derived
--- termination budgets; the growable arena is physical storage, not a second
--- semantic bound capable of disagreeing with those typed obstructions.
-writeScratch :: OperationState s -> Int -> Int -> ST s ()
-writeScratch OperationState{osScratchArena} index value =
-  writeGrowable osScratchArena index (packIndex value)
-{-# INLINE writeScratch #-}
-
-readScratch :: OperationState s -> Int -> ST s Int
-readScratch OperationState{osScratchArena} index = unpackIndex <$> readGrowable osScratchArena index
-{-# INLINE readScratch #-}
-
-addCounter :: OperationState s -> Counter -> Int -> ST s ()
-addCounter OperationState{osCounters} counter amount = do
-  let !index = fromEnum counter
-  current <- MUV.unsafeRead osCounters index
-  MUV.unsafeWrite osCounters index (current + fromIntegral amount)
-{-# INLINE addCounter #-}
-
-setCounter :: OperationState s -> Counter -> Int -> ST s ()
-setCounter OperationState{osCounters} counter value =
-  MUV.unsafeWrite osCounters (fromEnum counter) (fromIntegral value)
-{-# INLINE setCounter #-}
-
-maxCounter :: OperationState s -> Counter -> Int -> ST s ()
-maxCounter OperationState{osCounters} counter value = do
-  let !index = fromEnum counter
-  current <- MUV.unsafeRead osCounters index
-  when (fromIntegral value > current) (MUV.unsafeWrite osCounters index (fromIntegral value))
-{-# INLINE maxCounter #-}
-
-readCounter :: OperationState s -> Counter -> ST s Int
-readCounter OperationState{osCounters} counter =
-  fromIntegral <$> MUV.unsafeRead osCounters (fromEnum counter)
-{-# INLINE readCounter #-}
-
--- | Materialize the public statistics once, at freeze time, from the cells the
--- operation's subsystems charged while it ran. Diagnostic cells past
--- 'CounterRefinementQueuePops' have no t'BuildStats' field and are not read
--- here.
-freezeBuildStats :: OperationState s -> ST s BuildStats
-freezeBuildStats operation =
-  BuildStats
-    <$> readCounter operation CounterInputPoints
-    <*> readCounter operation CounterUniquePoints
-    <*> readCounter operation CounterExistingPoints
-    <*> readCounter operation CounterDuplicatePoints
-    <*> readCounter operation CounterSpatialSeedPoints
-    <*> readCounter operation CounterFaceSplits
-    <*> readCounter operation CounterInteriorEdgeSplits
-    <*> readCounter operation CounterBoundaryEdgeSplits
-    <*> readCounter operation CounterHullInsertions
-    <*> readCounter operation CounterLineSplits
-    <*> readCounter operation CounterLineExtensions
-    <*> readCounter operation CounterLineToAreaTransitions
-    <*> readCounter operation CounterEdgeFlips
-    <*> readCounter operation CounterLocationWalkSteps
-    <*> readCounter operation CounterLocationFallbacks
-    <*> readCounter operation CounterLocationMaxWalk
-    <*> readCounter operation CounterLegalizationMaxStack
-    <*> readCounter operation CounterSteinerPoints
-    <*> readCounter operation CounterRefinementFaceChecks
-    <*> readCounter operation CounterRefinementQueuePops
-    <*> readCounter operation CounterSweepFastPoints
-    <*> readCounter operation CounterSweepSkippedPoints
-{-# INLINE freezeBuildStats #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/PointIndex.hs b/src-dcel/Moonlight/Triangulation/Internal/PointIndex.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/PointIndex.hs
+++ /dev/null
@@ -1,351 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | The one derived identity index from canonical position hashes to the
--- vertices holding them. Geometry remains solely in the coordinate arenas:
--- both forms store handles only, and every hit is confirmed against those
--- authoritative coordinates. The persistent form follows a published mesh;
--- the open-addressed form is the denser ingress representation used while a
--- bulk load is still claiming its vertices.
-module Moonlight.Triangulation.Internal.PointIndex
-  ( PointIndex
-  , emptyPointIndex
-  , buildPointIndex
-  , pointIndexCandidates
-  , lookupPointIndex
-  , insertPointIndex
-  , removePointIndex
-  , relocatePointIndex
-  , MutablePointIndex
-  , MutablePointIndexUpdate (..)
-  , newMutablePointIndex
-  , seedMutablePointIndex
-  , lookupMutablePoint
-  , removeMutablePoint
-  , relocateMutablePoint
-  , resolveMutablePoint
-  ) where
-
-import Control.DeepSeq (NFData (..))
-import Control.Monad.ST (ST)
-import Data.Bits (shiftR, xor, (.&.))
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32, Word64)
-import GHC.Float (castDoubleToWord64)
-import Moonlight.Triangulation.Internal.Types (BuildError (PointIndexCapacityExhausted), Point (..))
-import Moonlight.Triangulation.Internal.Paged
-  ( Paged
-  , pagedUnsafeIndex
-  , toVector
-  )
-import Moonlight.Triangulation.Scalar (canonicalScalarZero)
-import qualified Data.Vector.Unboxed as U
-
--- | Hash buckets containing only vertex handles. Callers confirm candidates
--- against the authoritative coordinate planes.
-newtype PointIndex = PointIndex (IntMap.IntMap [Int])
-  deriving stock (Eq, Show)
-
--- A point index is a memoized derived view. Forcing a triangulation forces the
--- geometry and topology that determine it, not a cache no read has demanded.
--- This is the same semantic boundary as omitting the cache from serialization.
-instance NFData PointIndex where
-  rnf _ = ()
-
-emptyPointIndex :: PointIndex
-emptyPointIndex = PointIndex IntMap.empty
-
--- | Derive an index from the authoritative structure-of-arrays geometry.
-buildPointIndex :: Paged Double -> Paged Double -> PointIndex
-buildPointIndex pointXs pointYs =
-  U.ifoldl'
-    (\index vertex x ->
-       insertPointIndex x (pagedUnsafeIndex pointYs vertex) vertex index
-    )
-    emptyPointIndex
-    (toVector pointXs)
-
--- | Candidate vertices sharing a position hash. Exact coordinate comparison
--- belongs to the caller that owns the coordinate reader.
-pointIndexCandidates :: Double -> Double -> PointIndex -> [Int]
-pointIndexCandidates x y (PointIndex buckets) =
-  IntMap.findWithDefault [] (pointHash x y) buckets
-{-# INLINE pointIndexCandidates #-}
-
--- | Resolve one exact position against the authoritative coordinate planes.
--- Hashes reject; coordinates prove. Keeping this here makes the persistent
--- removal and constraint schedules consume the same identity law as a mutable
--- session instead of paying a topological point-location walk for a key lookup.
-lookupPointIndex
-  :: Paged Double
-  -> Paged Double
-  -> PointIndex
-  -> Point
-  -> Maybe Int
-lookupPointIndex pointXs pointYs pointIndex (Point x y) =
-  foldr confirm Nothing (pointIndexCandidates x y pointIndex)
- where
-  confirm candidate resolved
-    | pagedUnsafeIndex pointXs candidate == x
-        && pagedUnsafeIndex pointYs candidate == y = Just candidate
-    | otherwise = resolved
-
-insertPointIndex :: Double -> Double -> Int -> PointIndex -> PointIndex
-insertPointIndex x y vertex (PointIndex buckets) =
-  PointIndex (IntMap.insertWith (++) (pointHash x y) [vertex] buckets)
-{-# INLINE insertPointIndex #-}
-
--- | Forget one proved handle without touching geometry. Empty collision
--- buckets disappear, so the index remains a derived finite view rather than a
--- history of retired sites.
-removePointIndex :: Double -> Double -> Int -> PointIndex -> PointIndex
-removePointIndex x y vertex (PointIndex buckets) =
-  PointIndex (IntMap.update retainOthers (pointHash x y) buckets)
- where
-  retainOthers candidates = case candidates of
-    [candidate]
-      | candidate == vertex -> Nothing
-    _ ->
-      case removeCandidate candidates of
-        [] -> Nothing
-        remaining -> Just remaining
-
-  removeCandidate [] = []
-  removeCandidate (candidate : remaining)
-    | candidate == vertex = remaining
-    | otherwise = candidate : removeCandidate remaining
-{-# INLINE removePointIndex #-}
-
--- | Transport the tail vertex into the slot vacated by swap compaction.
--- Coordinate storage remains authoritative; this updates handles only.
-relocatePointIndex
-  :: Double
-  -> Double
-  -> Int
-  -> Int
-  -> PointIndex
-  -> PointIndex
-relocatePointIndex x y previousVertex currentVertex (PointIndex buckets) =
-  PointIndex
-    (IntMap.alter (Just . relocateCandidate . maybe [] id) (pointHash x y) buckets)
- where
-  relocateCandidate [] = [currentVertex]
-  relocateCandidate (candidate : remaining)
-    | candidate == previousVertex = currentVertex : remaining
-    | otherwise = candidate : relocateCandidate remaining
-{-# INLINE relocatePointIndex #-}
-
--- The arena already holds every key, so a slot is the vertex that owns the
--- position and nothing else. Every lookup rechecks coordinates; a failed local
--- transport invalidates the table rather than letting it outlive that proof.
-data MutablePointIndex s = MutablePointIndex
-  { tableSlots :: !(MUV.MVector s Word32)
-  , tableMask :: {-# UNPACK #-} !Int
-  }
-
--- | A local cache transport either preserves the proof that every slot agrees
--- with the coordinate arenas, or explicitly gives that proof up. Callers must
--- fall back to the lazy immutable derivation after 'MutablePointIndexInvalidated';
--- they never publish a table whose handle correspondence was not established.
-data MutablePointIndexUpdate
-  = MutablePointIndexUpdated
-  | MutablePointIndexInvalidated
-
-vacant :: Word32
-vacant = maxBound
-
--- | Size a table for a stated number of distinct positions.
-newMutablePointIndex :: Int -> ST s (MutablePointIndex s)
-newMutablePointIndex expected = do
-  tableSlots <- MUV.replicate capacity vacant
-  pure MutablePointIndex{tableSlots, tableMask = capacity - 1}
- where
-  -- Two slots per key, rounded up to a power of two: linear probing stays in a
-  -- short run and the mask stands in for a division.
-  !capacity = grow 16
-  !wanted = 2 * max 1 expected
-  grow !size
-    | size >= wanted = size
-    | otherwise = grow (size * 2)
-
--- | Seed the open-addressed section from authoritative coordinates. This is
--- the shared ingress/batch operation: no point payload or parallel identity
--- store crosses the boundary.
-seedMutablePointIndex
-  :: MutablePointIndex s
-  -> Int
-  -> (Int -> ST s Double)
-  -> (Int -> ST s Double)
-  -> ST s (Either BuildError ())
-seedMutablePointIndex table count readX readY = seed 0
- where
-  seed !vertex
-    | vertex >= count = pure (Right ())
-    | otherwise = do
-        x <- readX vertex
-        y <- readY vertex
-        claimed <- resolveMutablePoint table readX readY x y vertex
-        case claimed of
-          Left failure -> pure (Left failure)
-          Right _ -> seed (vertex + 1)
-
--- | Look up a canonical position in the mutable section. A vacant slot proves
--- absence; all occupied candidates are confirmed against coordinate authority.
-lookupMutablePoint
-  :: MutablePointIndex s
-  -> (Int -> ST s Double)
-  -> (Int -> ST s Double)
-  -> Double
-  -> Double
-  -> ST s (Maybe Int)
-lookupMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y =
-  probe (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
- where
-  probe !slot !budget
-    | budget <= 0 = pure Nothing
-    | otherwise = do
-        occupant <- MUV.unsafeRead tableSlots slot
-        if occupant == vacant
-          then pure Nothing
-          else do
-            heldX <- readX (fromIntegral occupant)
-            heldY <- readY (fromIntegral occupant)
-            if heldX == x && heldY == y
-              then pure (Just (fromIntegral occupant))
-              else probe ((slot + 1) .&. tableMask) (budget - 1)
-{-# INLINE lookupMutablePoint #-}
-
--- | Forget a retired handle and repair its linear-probe cluster by backward
--- shifting only entries whose home run crosses the resulting hole. The reader
--- is needed for the surviving entries' homes; the retiring entry itself is
--- identified by handle because swap compaction may already have overwritten
--- its coordinate slot. An exhausted or absent proof invalidates the derived
--- cache instead of manufacturing a lookup result.
-removeMutablePoint
-  :: MutablePointIndex s
-  -> (Int -> ST s Double)
-  -> (Int -> ST s Double)
-  -> Double
-  -> Double
-  -> Int
-  -> ST s MutablePointIndexUpdate
-removeMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y retiredVertex =
-  findRetired (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
- where
-  findRetired !slot !budget
-    | budget <= 0 = pure MutablePointIndexInvalidated
-    | otherwise = do
-        occupant <- MUV.unsafeRead tableSlots slot
-        if occupant == vacant
-          then pure MutablePointIndexInvalidated
-          else
-            if fromIntegral occupant == retiredVertex
-              then do
-                MUV.unsafeWrite tableSlots slot vacant
-                closeProbeHole slot ((slot + 1) .&. tableMask) (budget - 1)
-              else findRetired ((slot + 1) .&. tableMask) (budget - 1)
-
-  closeProbeHole !hole !slot !budget
-    | budget <= 0 =
-        MutablePointIndexInvalidated <$ MUV.unsafeWrite tableSlots hole vacant
-    | otherwise = do
-        occupant <- MUV.unsafeRead tableSlots slot
-        if occupant == vacant
-          then MutablePointIndexUpdated <$ MUV.unsafeWrite tableSlots hole vacant
-          else do
-            heldX <- readX (fromIntegral occupant)
-            heldY <- readY (fromIntegral occupant)
-            let !home = fromIntegral (mixCoordinates heldX heldY) .&. tableMask
-                !distanceToHole = (hole - home) .&. tableMask
-                !distanceToSlot = (slot - home) .&. tableMask
-            if distanceToHole < distanceToSlot
-              then do
-                MUV.unsafeWrite tableSlots hole occupant
-                closeProbeHole slot ((slot + 1) .&. tableMask) (budget - 1)
-              else closeProbeHole hole ((slot + 1) .&. tableMask) (budget - 1)
-{-# INLINE removeMutablePoint #-}
-
--- | Rename the tail handle after DCEL swap compaction. Failure remains a
--- typed cache obstruction, so its caller drops the table and later identity
--- questions descend from geometry rather than trusting an unproved cache.
-relocateMutablePoint
-  :: MutablePointIndex s
-  -> Double
-  -> Double
-  -> Int
-  -> Int
-  -> ST s MutablePointIndexUpdate
-relocateMutablePoint MutablePointIndex{tableSlots, tableMask} x y previousVertex currentVertex =
-  findPrevious (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
- where
-  findPrevious !slot !budget
-    | budget <= 0 = pure MutablePointIndexInvalidated
-    | otherwise = do
-        occupant <- MUV.unsafeRead tableSlots slot
-        if occupant == vacant
-          then pure MutablePointIndexInvalidated
-          else
-            if fromIntegral occupant == previousVertex
-              then
-                MutablePointIndexUpdated
-                  <$ MUV.unsafeWrite tableSlots slot (fromIntegral currentVertex)
-              else findPrevious ((slot + 1) .&. tableMask) (budget - 1)
-{-# INLINE relocateMutablePoint #-}
-
--- | Answer the vertex already holding a canonical position, or claim the
--- position for @candidate@ and answer 'Nothing'. The position travels as raw
--- coordinates — the v'Point' constructor is the cold boundary's packaging and
--- has no business on the ingress path. The caller supplies the arena's
--- coordinate reader, so a claim is only sound if @candidate@ is the very next
--- vertex the arena will append.
-resolveMutablePoint
-  :: MutablePointIndex s
-  -> (Int -> ST s Double)
-  -> (Int -> ST s Double)
-  -> Double
-  -> Double
-  -> Int
-  -> ST s (Either BuildError (Maybe Int))
-resolveMutablePoint MutablePointIndex{tableSlots, tableMask} readX readY x y candidate =
-  probe (fromIntegral (mixCoordinates x y) .&. tableMask) (tableMask + 1)
- where
-  probe !slot !budget
-    | budget <= 0 = pure (Left (PointIndexCapacityExhausted (MUV.length tableSlots)))
-    | otherwise = do
-        occupant <- MUV.unsafeRead tableSlots slot
-        if occupant == vacant
-          then Right Nothing <$ MUV.unsafeWrite tableSlots slot (fromIntegral candidate)
-          else do
-            heldX <- readX (fromIntegral occupant)
-            heldY <- readY (fromIntegral occupant)
-            if heldX == x && heldY == y
-              then pure (Right (Just (fromIntegral occupant)))
-              else probe ((slot + 1) .&. tableMask) (budget - 1)
-
--- Inlined rather than merely specialized: the coordinate reader arrives as an
--- argument, so until the probe loop lands at its call site every collision pays
--- an unknown call and a boxed pair for a read the caller could have made
--- directly.
-{-# INLINE resolveMutablePoint #-}
-
-mixCoordinates :: Double -> Double -> Word64
-mixCoordinates x y =
-  mix
-    ( castDoubleToWord64 (canonicalScalarZero x)
-        `xor` mix (castDoubleToWord64 (canonicalScalarZero y))
-    )
-{-# INLINE mixCoordinates #-}
-
-mix :: Word64 -> Word64
-mix raw =
-  let !z0 = raw + 0x9e3779b97f4a7c15
-      !z1 = (z0 `xor` (z0 `shiftR` 30)) * 0xbf58476d1ce4e5b9
-      !z2 = (z1 `xor` (z1 `shiftR` 27)) * 0x94d049bb133111eb
-   in z2 `xor` (z2 `shiftR` 31)
-{-# INLINE mix #-}
-
-pointHash :: Double -> Double -> Int
-pointHash x y = fromIntegral (mixCoordinates x y)
-{-# INLINE pointHash #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Predicates.hs b/src-dcel/Moonlight/Triangulation/Internal/Predicates.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Predicates.hs
+++ /dev/null
@@ -1,143 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# OPTIONS_GHC -O3 -fllvm -optlo-O3 -optlc-O3 #-}
-
--- | Exact predicates over points already admitted by a construction or query
--- boundary.  This module is package-private: public callers must carry
--- 'QueryPoint' evidence through "Moonlight.Triangulation.Math" instead of
--- presenting arbitrary IEEE-754 payloads to an exact classifier.
-module Moonlight.Triangulation.Internal.Predicates
-  ( orient2dCoordinates
-  , inCircleCoordinates
-  , orient2d
-  , sideQuery
-  , inCircle
-  , onClosedSegment
-  , segmentRelation
-  , segmentsProperlyCross
-  , segmentsIntersect
-  , inDiametralCircle
-  ) where
-
-import Moonlight.Triangulation.Internal.Dyadic
-  ( exactDiametralDot
-  , exactInCircleDet
-  , exactOrientSignDouble
-  )
-import Moonlight.Triangulation.Internal.SegmentRelation
-  ( SegmentRelation (..)
-  , segmentRelationWith
-  )
-import Moonlight.Triangulation.LineSideInfo (LineSideInfo, fromOrdering)
-import Moonlight.Triangulation.Scalar
-  ( isFinite
-  , scalarCcwErrorBound
-  , scalarInCircleErrorBound
-  )
-import Moonlight.Triangulation.Internal.Types (Point (..))
-
--- The coordinate kernels are package-private because their raw 'Double'
--- arguments carry no finite-range evidence. Every public exact classifier
--- descends through 'QueryPoint'; internal hot paths call these only after their
--- construction boundary has admitted the stored coordinates.
-orient2dCoordinates
-  :: Double -> Double -> Double -> Double -> Double -> Double
-  -> Ordering
-orient2dCoordinates ax ay bx by cx cy
-  | abs determinant > scalarCcwErrorBound * determinantSum = compare determinant 0
-  | otherwise = exactOrientSignDouble ax ay bx by cx cy
- where
-  !left = (ax - cx) * (by - cy)
-  !right = (ay - cy) * (bx - cx)
-  !determinant = left - right
-  !determinantSum = abs left + abs right
-{-# INLINE orient2dCoordinates #-}
-
-inCircleCoordinates
-  :: Double -> Double -> Double -> Double
-  -> Double -> Double -> Double -> Double
-  -> Ordering
-inCircleCoordinates ax ay bx by cx cy dx dy
-  | abs determinant > scalarInCircleErrorBound * permanent = compare determinant 0
-  | otherwise = compare (exactInCircleDet ax ay bx by cx cy dx dy) 0
- where
-  !adx = ax - dx
-  !ady = ay - dy
-  !bdx = bx - dx
-  !bdy = by - dy
-  !cdx = cx - dx
-  !cdy = cy - dy
-  !abdet = adx * bdy - bdx * ady
-  !bcdet = bdx * cdy - cdx * bdy
-  !cadet = cdx * ady - adx * cdy
-  !alift = adx * adx + ady * ady
-  !blift = bdx * bdx + bdy * bdy
-  !clift = cdx * cdx + cdy * cdy
-  !determinant = alift * bcdet + blift * cadet + clift * abdet
-  !permanent =
-    (abs (bdx * cdy) + abs (cdx * bdy)) * alift
-      + (abs (cdx * ady) + abs (adx * cdy)) * blift
-      + (abs (adx * bdy) + abs (bdx * ady)) * clift
-{-# INLINE inCircleCoordinates #-}
-
--- | Exact orientation of points whose coordinates have already been admitted.
-orient2d :: Point -> Point -> Point -> Ordering
-orient2d (Point ax ay) (Point bx by) (Point cx cy) =
-  orient2dCoordinates ax ay bx by cx cy
-{-# INLINE orient2d #-}
-
--- | Exact side of an admitted oriented line.
-sideQuery :: Point -> Point -> Point -> LineSideInfo
-sideQuery from to query = fromOrdering (orient2d from to query)
-{-# INLINE sideQuery #-}
-
--- | Ordering of the admitted oriented incircle determinant.
-inCircle :: Point -> Point -> Point -> Point -> Ordering
-inCircle
-  (Point ax ay)
-  (Point bx by)
-  (Point cx cy)
-  (Point dx dy) =
-    inCircleCoordinates ax ay bx by cx cy dx dy
-{-# INLINE inCircle #-}
-
--- | Whether an admitted point lies on an admitted closed segment.
-onClosedSegment :: Point -> Point -> Point -> Bool
-onClosedSegment from@(Point fromX fromY) to@(Point toX toY) query@(Point queryX queryY) =
-  orient2d from to query == EQ
-    && queryX >= min fromX toX
-    && queryX <= max fromX toX
-    && queryY >= min fromY toY
-    && queryY <= max fromY toY
-{-# INLINE onClosedSegment #-}
-
--- | Exact relation between admitted closed segments.
-segmentRelation :: Point -> Point -> Point -> Point -> SegmentRelation
-segmentRelation = segmentRelationWith (==) compare orient2d onClosedSegment
-
--- | Whether two admitted closed segments share any point.
-segmentsIntersect :: Point -> Point -> Point -> Point -> Bool
-segmentsIntersect firstFrom firstTo secondFrom secondTo =
-  segmentRelation firstFrom firstTo secondFrom secondTo /= SegmentsDisjoint
-
--- | Whether two admitted segments cross away from their endpoints.
-segmentsProperlyCross :: Point -> Point -> Point -> Point -> Bool
-segmentsProperlyCross firstFrom firstTo secondFrom secondTo =
-  opposite (orient2d firstFrom firstTo secondFrom) (orient2d firstFrom firstTo secondTo)
-    && opposite (orient2d secondFrom secondTo firstFrom) (orient2d secondFrom secondTo firstTo)
- where
-  opposite LT GT = True
-  opposite GT LT = True
-  opposite _ _ = False
-
--- | Whether an admitted point lies in the closed diametral disk of an admitted
--- segment.
-inDiametralCircle :: Point -> Point -> Point -> Bool
-inDiametralCircle (Point ax ay) (Point bx by) (Point px py)
-  | isFinite dot && abs dot > scalarCcwErrorBound * dotSum = dot < 0
-  | otherwise = exactDiametralDot ax ay bx by px py <= 0
- where
-  !left = (ax - px) * (bx - px)
-  !right = (ay - py) * (by - py)
-  !dot = left + right
-  !dotSum = abs left + abs right
-{-# INLINE inDiametralCircle #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Probe.hs b/src-dcel/Moonlight/Triangulation/Internal/Probe.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Probe.hs
+++ /dev/null
@@ -1,60 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE TypeFamilyDependencies #-}
-{-# LANGUAGE TypeOperators #-}
-
--- | Compile-time instrumentation switch. The bulk board is the measuring
--- instrument, so per-candidate probe counters must not exist on its path at
--- all: gating behind a flag the compiler can eliminate is the only honest way
--- to have both one sweep implementation and an undistorted measurement. The
--- flag is a type, not a value: at 'ProbeOff the counter type is @()@, so no
--- arithmetic on it can survive in the worker — erasure is a property of the
--- kind, not of the optimizer's mood. The instrumented entry instantiates the
--- same code at 'ProbeOn and pays the cold counter writes itself.
-module Moonlight.Triangulation.Internal.Probe
-  ( Probe (..)
-  , ProbeCounter
-  , KnownProbe (..)
-  ) where
-
-import Control.Monad.ST (ST)
-import Data.Kind (Type)
-import Moonlight.Triangulation.Internal.OperationState
-  ( Counter
-  , OperationState
-  , addCounter
-  )
-
--- | The instrumentation switch, promoted to a kind by @DataKinds@.
-data Probe = ProbeOff | ProbeOn
-
--- | The counter a probe site threads. At 'ProbeOff there is nothing to
--- thread; at 'ProbeOn it is a strict @Int@.
-type family ProbeCounter (probe :: Probe) = (counter :: Type) | counter -> probe where
-  ProbeCounter 'ProbeOff = ()
-  ProbeCounter 'ProbeOn = Int
-
-class KnownProbe probe where
-  probeZero :: ProbeCounter probe
-  probeBump :: ProbeCounter probe -> ProbeCounter probe
-  -- | Charge one finished counter to the operation's diagnostic cell. At
-  -- 'ProbeOff this is @pure ()@ and disappears with the dictionary; at
-  -- 'ProbeOn it is one cold vector write per drain or probe site, on the
-  -- instrumented entry that asked for it.
-  probeCharge :: OperationState s -> Counter -> ProbeCounter probe -> ST s ()
-
-instance KnownProbe 'ProbeOff where
-  probeZero = ()
-  probeBump = id
-  probeCharge _ _ _ = pure ()
-  {-# INLINE probeZero #-}
-  {-# INLINE probeBump #-}
-  {-# INLINE probeCharge #-}
-
-instance KnownProbe 'ProbeOn where
-  probeZero = 0
-  probeBump counter = counter + 1
-  probeCharge operation counter value = addCounter operation counter value
-  {-# INLINE probeZero #-}
-  {-# INLINE probeBump #-}
-  {-# INLINE probeCharge #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Region/Bounds.hs b/src-dcel/Moonlight/Triangulation/Internal/Region/Bounds.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Region/Bounds.hs
+++ /dev/null
@@ -1,206 +0,0 @@
--- | Exact axis-aligned candidate bounds for admitted region geometry. Bounds
--- prune impossible overlap obligations; exact predicates remain authoritative.
-module Moonlight.Triangulation.Internal.Region.Bounds
-  ( ExactBounds
-  , exactLoopBounds
-  , componentBounds
-  , regionBounds
-  , boundsOverlap
-  , pointInBounds
-  , overlappingPairs
-  , overlappingOptionalPairs
-  , overlappingPairsBetween
-  , overlappingPredecessors
-  ) where
-
-import Data.List (sortOn, tails)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.IntMap.Strict as IntMap
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactPointCoordinates
-  )
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  )
-
-data ExactBounds = ExactBounds
-  !ExactRational
-  !ExactRational
-  !ExactRational
-  !ExactRational
-
-exactLoopBounds :: ExactLoop -> ExactBounds
-exactLoopBounds (ExactLoop (firstPoint :| remaining)) =
-  List.foldl' extend (pointBounds firstPoint) remaining
- where
-  extend bounds point = boundsUnion bounds (pointBounds point)
-
-componentBounds :: PolygonComponent -> ExactBounds
-componentBounds = exactLoopBounds . polygonOuterLoop
-
-regionBounds :: PlanarRegion -> Maybe ExactBounds
-regionBounds (PlanarRegion components) =
-  case components of
-    [] -> Nothing
-    firstComponent : remaining ->
-      Just
-        ( List.foldl'
-            (\bounds component -> boundsUnion bounds (componentBounds component))
-            (componentBounds firstComponent)
-            remaining
-        )
-
-pointBounds :: ExactPoint -> ExactBounds
-pointBounds point =
-  let (x, y) = exactPointCoordinates point
-   in ExactBounds x y x y
-
-boundsUnion :: ExactBounds -> ExactBounds -> ExactBounds
-boundsUnion
-  (ExactBounds leftMinimumX leftMinimumY leftMaximumX leftMaximumY)
-  (ExactBounds rightMinimumX rightMinimumY rightMaximumX rightMaximumY) =
-    ExactBounds
-      (min leftMinimumX rightMinimumX)
-      (min leftMinimumY rightMinimumY)
-      (max leftMaximumX rightMaximumX)
-      (max leftMaximumY rightMaximumY)
-
-boundsOverlap :: ExactBounds -> ExactBounds -> Bool
-boundsOverlap
-  (ExactBounds leftMinimumX leftMinimumY leftMaximumX leftMaximumY)
-  (ExactBounds rightMinimumX rightMinimumY rightMaximumX rightMaximumY) =
-    not
-      ( leftMaximumX < rightMinimumX
-          || rightMaximumX < leftMinimumX
-          || leftMaximumY < rightMinimumY
-          || rightMaximumY < leftMinimumY
-      )
-
-pointInBounds :: ExactPoint -> ExactBounds -> Bool
-pointInBounds point (ExactBounds minimumX minimumY maximumX maximumY) =
-  let (x, y) = exactPointCoordinates point
-   in minimumX <= x && x <= maximumX && minimumY <= y && y <= maximumY
-
-overlappingPairs :: (value -> ExactBounds) -> [value] -> [(value, value)]
-overlappingPairs boundsOf =
-  overlappingIndexedValuePairs
-    . zipWith (\index value -> (index, boundsOf value, value)) [0 ..]
-
-overlappingOptionalPairs
-  :: (value -> Maybe ExactBounds)
-  -> [value]
-  -> [(value, value)]
-overlappingOptionalPairs boundsOf =
-  overlappingIndexedValuePairs
-    . concatMap
-      (\(index, value) ->
-         case boundsOf value of
-           Nothing -> []
-           Just bounds -> [(index, bounds, value)])
-    . zip [0 ..]
-
-overlappingPairsBetween
-  :: (value -> ExactBounds)
-  -> [value]
-  -> [value]
-  -> [(value, value)]
-overlappingPairsBetween boundsOf leftValues rightValues =
-  [ pair
-  | (firstBounds, firstValue) : remaining <- tails ordered
-  , (secondBounds, secondValue) <-
-      takeWhile
-        (\(bounds, _) -> boundsMinimumX bounds <= boundsMaximumX firstBounds)
-        remaining
-  , pair <- crossPair firstValue secondValue
-  , boundsOverlap firstBounds secondBounds
-  ]
- where
-  ordered =
-    sortOn
-      (boundsMinimumX . fst)
-      ( map
-          (\value -> (either boundsOf boundsOf value, value))
-          (map Left leftValues <> map Right rightValues)
-      )
-  crossPair :: Either value value -> Either value value -> [(value, value)]
-  crossPair (Left leftValue) (Right rightValue) = [(leftValue, rightValue)]
-  crossPair (Right rightValue) (Left leftValue) = [(leftValue, rightValue)]
-  crossPair _ _ = []
-
--- | Each value together with only the earlier input values whose exact bounds
--- overlap it. This is the local cover used when a fold glues one component at
--- a time and needs the complete overlap section accumulated so far.
-overlappingPredecessors
-  :: (value -> ExactBounds)
-  -> [value]
-  -> [(value, [value])]
-overlappingPredecessors boundsOf values =
-  [ ( value
-    , map snd
-        ( sortOn fst
-            (IntMap.findWithDefault [] index predecessorsByIndex)
-        )
-    )
-  | (index, value) <- zip [0 ..] values
-  ]
- where
-  indexed = zipWith (\index value -> (index, boundsOf value, value)) [0 ..] values
-  predecessorsByIndex =
-    IntMap.fromListWith (<>)
-      [ (rightIndex, [(leftIndex, leftValue)])
-      | (leftIndex, rightIndex, leftValue, _) <- overlappingIndexedEntries indexed
-      ]
-
--- The authoring path retains its nested result pair because the flattened
--- four-field candidate increased allocation on the registered 1,024-component
--- workload. The predecessor path below needs the indices after selection and
--- therefore carries the indexed specialization separately.
-overlappingIndexedValuePairs
-  :: [(Int, ExactBounds, value)]
-  -> [(value, value)]
-overlappingIndexedValuePairs indexed =
-  map (\(_, _, pair) -> pair)
-    ( sortOn (\(leftIndex, rightIndex, _) -> (leftIndex, rightIndex))
-        [ if leftIndex <= rightIndex
-            then (leftIndex, rightIndex, (leftValue, rightValue))
-            else (rightIndex, leftIndex, (rightValue, leftValue))
-        | (leftIndex, leftBounds, leftValue) : remaining <- tails ordered
-        , (rightIndex, rightBounds, rightValue) <-
-            takeWhile
-              (\(_, bounds, _) -> boundsMinimumX bounds <= boundsMaximumX leftBounds)
-              remaining
-        , boundsOverlap leftBounds rightBounds
-        ]
-    )
- where
-  ordered = sortOn (\(_, bounds, _) -> boundsMinimumX bounds) indexed
-
-overlappingIndexedEntries
-  :: [(Int, ExactBounds, value)]
-  -> [(Int, Int, value, value)]
-overlappingIndexedEntries indexed =
-  sortOn
-    (\(leftIndex, rightIndex, _, _) -> (leftIndex, rightIndex))
-    [ if leftIndex <= rightIndex
-        then (leftIndex, rightIndex, leftValue, rightValue)
-        else (rightIndex, leftIndex, rightValue, leftValue)
-    | (leftIndex, leftBounds, leftValue) : remaining <- tails ordered
-    , (rightIndex, rightBounds, rightValue) <-
-        takeWhile
-          (\(_, bounds, _) -> boundsMinimumX bounds <= boundsMaximumX leftBounds)
-          remaining
-    , boundsOverlap leftBounds rightBounds
-    ]
- where
-  ordered = sortOn (\(_, bounds, _) -> boundsMinimumX bounds) indexed
-
-boundsMinimumX :: ExactBounds -> ExactRational
-boundsMinimumX (ExactBounds minimumX _ _ _) = minimumX
-
-boundsMaximumX :: ExactBounds -> ExactRational
-boundsMaximumX (ExactBounds _ _ maximumX _) = maximumX
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Region/Publication.hs b/src-dcel/Moonlight/Triangulation/Internal/Region/Publication.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Region/Publication.hs
+++ /dev/null
@@ -1,153 +0,0 @@
--- | Trusted exact-coordinate publication from the one resident DCEL boundary
--- owner. The callback-bearing entrances are internal because only a sealed
--- downstream carrier may prove that its exact coordinate section belongs to
--- the supplied topology.
-module Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayer
-  , labelledPlanarLayerFromExactCoordinates
-  , planarLayerFromAdmittedComponents
-  , polygonComponentFromBoundaryCoordinates
-  ) where
-
-import Data.Bifunctor (first)
-import Data.List (sort)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Moonlight.Triangulation.Dcel (vertexPoint)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactOnClosedSegment
-  , exactOrient2d
-  , exactPointFromPoint
-  )
-import Moonlight.Triangulation.FloodFillIterator
-  ( BoundaryLoop
-  , RegionBoundary
-  , boundaryLoopVertices
-  , componentBoundary
-  , faceComponents
-  , labelledRegionBoundaries
-  , regionBoundaryHoleLoops
-  , regionBoundaryOuterLoop
-  )
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId, VertexId)
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( rotateCycleLeast
-  , simplifyBoundaryCycle
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarLayer (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  , RegionPublicationError (..)
-  , RegionValidationError (..)
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-
--- | Publish bounded resident face labels through the existing component and
--- boundary owners, then lift only traced boundary coordinates exactly.
-labelledPlanarLayer
-  :: Ord label
-  => label
-  -> Triangulation mode vertex directed undirected face
-  -> (FaceId -> label)
-  -> Either RegionPublicationError (PlanarLayer label)
-labelledPlanarLayer outside triangulation labelFace = do
-  labelledBoundaries <-
-    first RegionBoundaryObstruction
-      (labelledRegionBoundaries triangulation labelFace)
-  labelledComponents <-
-    traverse
-      (\(label, boundary) ->
-         (label,) <$> polygonComponentFromBoundaryCoordinates exactPointAt boundary)
-      [ pair
-      | pair@(label, _) <- labelledBoundaries
-      , label /= outside
-      ]
-  pure (planarLayerFromAdmittedComponents outside labelledComponents)
- where
-  exactPointAt vertex =
-    first (RegionCoordinateObstruction vertex)
-      (exactPointFromPoint (vertexPoint triangulation vertex))
-
--- | Publish bounded face labels using the exact-coordinate section belonging
--- to the resident carrier. Topology has already been proved by
--- 'labelledRegionBoundaries'; this path performs exact simplification but does
--- not send derived loops back through authoring event sweeps.
-labelledPlanarLayerFromExactCoordinates
-  :: Ord label
-  => label
-  -> Triangulation mode vertex directed undirected face
-  -> (VertexId -> Either RegionPublicationError ExactPoint)
-  -> (FaceId -> Either RegionPublicationError label)
-  -> Either RegionPublicationError (PlanarLayer label)
-labelledPlanarLayerFromExactCoordinates outside triangulation exactPointAt labelFace = do
-  labelledComponents <-
-    traverse
-      (\(labelResult, component) -> do
-         label <- labelResult
-         boundary <-
-           first RegionBoundaryObstruction
-             (componentBoundary triangulation component)
-         (label,) <$> polygonComponentFromBoundaryCoordinates exactPointAt boundary)
-      [ pair
-      | pair@(labelResult, _) <- faceComponents triangulation labelFace
-      , labelResult /= Right outside
-      ]
-  pure (planarLayerFromAdmittedComponents outside labelledComponents)
-
--- | Glue already-admitted, pairwise interior-disjoint components by label.
--- Both DCEL publication and exact overlay cells reach this point only after
--- their topology owner has proved those obligations.
-planarLayerFromAdmittedComponents
-  :: Ord label
-  => label
-  -> [(label, PolygonComponent)]
-  -> PlanarLayer label
-planarLayerFromAdmittedComponents outside labelledComponents =
-  PlanarLayer
-    outside
-    ( Map.map
-        (PlanarRegion . sort)
-        ( Map.fromListWith (<>)
-            [(label, [component]) | (label, component) <- labelledComponents]
-        )
-    )
-
--- | Convert one already-traced resident component boundary against the exact
--- coordinate carrier admitted for that same resident topology.
-polygonComponentFromBoundaryCoordinates
-  :: (VertexId -> Either RegionPublicationError ExactPoint)
-  -> RegionBoundary
-  -> Either RegionPublicationError PolygonComponent
-polygonComponentFromBoundaryCoordinates exactPointAt boundary = do
-  outer <- convertLoop (regionBoundaryOuterLoop boundary)
-  holes <- traverse convertLoop (regionBoundaryHoleLoops boundary)
-  pure (PolygonComponent outer (sort holes))
- where
-  convertLoop :: BoundaryLoop -> Either RegionPublicationError ExactLoop
-  convertLoop loop = do
-    points <- traverse exactPointAt (boundaryLoopVertices loop)
-    admittedDerivedLoop points
-
--- | Boundary descent already proves simplicity, winding, and component
--- compatibility. Exact simplification remains necessary because the exact
--- carrier may expose a collinearity that the embedded boundary retained.
-admittedDerivedLoop
-  :: NonEmpty ExactPoint
-  -> Either RegionPublicationError ExactLoop
-admittedDerivedLoop points = do
-  (_, simplified) <-
-    first RegionValidationObstruction
-      ( simplifyBoundaryCycle
-          RegionLoopDegenerate
-          (\previous current next ->
-             exactOrient2d previous current next == EQ
-               && exactOnClosedSegment previous next current)
-          exactOrient2d
-          id
-          (NonEmpty.toList points)
-      )
-  pure (ExactLoop (rotateCycleLeast simplified))
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Region/Types.hs b/src-dcel/Moonlight/Triangulation/Internal/Region/Types.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Region/Types.hs
+++ /dev/null
@@ -1,97 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | The invariant-bearing exact region carriers. Public construction and
--- validation remain in "Moonlight.Triangulation.Region"; downstream build-tier
--- algorithms import this owner only when their algebra proves the constructors'
--- obligations directly.
-module Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PolygonComponent (..)
-  , PlanarRegion (..)
-  , RegionPointLocation (..)
-  , PlanarLayer (..)
-  , RegionValidationError (..)
-  , RegionPublicationError (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import Data.Map.Strict (Map)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , SegmentRelation
-  )
-import Moonlight.Triangulation.FloodFillIterator (BoundaryObstruction)
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId, VertexId)
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEventObstruction
-  )
-import Moonlight.Triangulation.Internal.Types (PointValidationError)
-
--- | One admitted, simple exact cycle. Its first point is the least exact point
--- on the cycle, so equality does not retain an authoring rotation.
-newtype ExactLoop = ExactLoop (NonEmpty ExactPoint)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One connected polygonal component: a counter-clockwise outer cycle and
--- zero or more clockwise holes.
-data PolygonComponent = PolygonComponent
-  { polygonOuterLoop :: !ExactLoop
-  , polygonHoleLoops :: ![ExactLoop]
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A finite union of components with pairwise-disjoint interiors.
-newtype PlanarRegion = PlanarRegion [PolygonComponent]
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact point position relative to a closed region.
-data RegionPointLocation
-  = RegionExterior
-  | RegionOnBoundary
-  | RegionInterior
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A finite labelled planar layer. The outside label is implicit and may not
--- also own a bounded region.
-data PlanarLayer label = PlanarLayer
-  { planarLayerOutsideLabel :: !label
-  , planarLayerRegions :: !(Map label PlanarRegion)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed authoring obstructions. Indices are cycle/component positions in the
--- submitted value and every geometric relation preserves its exact witness.
-data RegionValidationError
-  = RegionLoopDegenerate ![ExactPoint]
-  | RegionLoopSelfRelation !Int !Int !SegmentRelation
-  | RegionOuterLoopWinding !Ordering
-  | RegionHoleLoopWinding !Int !Ordering
-  | RegionHoleLocation !Int !RegionPointLocation
-  | RegionBoundaryRelation !Int !Int !SegmentRelation
-  | RegionComponentInteriorOverlap !Int !Int
-  | RegionLayerInteriorOverlap !Int !Int
-  | RegionOutsideLabelUsed
-  | RegionSegmentEventsInvalid !ExactSegmentEventObstruction
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed failures while publishing an existing triangulation as exact region
--- values. The handle is retained when a resident coordinate is inadmissible.
-data RegionPublicationError
-  = RegionBoundaryObstruction !BoundaryObstruction
-  | RegionCoordinateObstruction !VertexId !PointValidationError
-  | RegionCoordinateMissing !VertexId
-  | RegionFaceLabelMissing !FaceId
-  | RegionValidationObstruction !RegionValidationError
-  | RegionUnboundedSelection
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Representation.hs b/src-dcel/Moonlight/Triangulation/Internal/Representation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Representation.hs
+++ /dev/null
@@ -1,641 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE RoleAnnotations #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The stored representation: the structure-of-arrays mesh, the payload
--- traversals that reach its four free parameters, and the records that carry a
--- built mesh beside its telemetry.
-module Moonlight.Triangulation.Internal.Representation
-  ( Triangulation (..)
-  , SeamFrontierIndex (..)
-  , prepareSeamFrontierIndex
-  , summarizeSeamChart
-  , seamQuarterTurn
-  , geometryOnlyPublication
-  , promoteConstrained
-  , PayloadTraversal
-  , vertexPayloads
-  , directedPayloads
-  , undirectedPayloads
-  , facePayloads
-  , mapVertices
-  , mapDirectedEdges
-  , mapUndirectedEdges
-  , mapFaces
-  , imapUndirectedEdges
-  , imapFaces
-  , DelaunayTriangulation
-  , ConstrainedDelaunayTriangulation
-  , BuildResult (..)
-  , InsertionResult (..)
-  , RefinementReceipt (..)
-  , RefinementDomainResult (..)
-  , RefinementResult (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Foldable (toList)
-import qualified Data.IntSet as IntSet
-import Data.List (unfoldr)
-import qualified Data.List as List
-import Data.Primitive.PrimArray (PrimArray)
-import qualified Data.Sequence as Seq
-import Data.Traversable (foldMapDefault)
-import qualified Data.Vector as V
-import Data.Word (Word8, Word32)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  )
-import Moonlight.Triangulation.Internal.PackedIndex (unpackIndex, unpackOptionalIndex)
-import Moonlight.Triangulation.Internal.BoxedPaged (BoxedPaged, boxedDefaulted, boxedFromVector, boxedToVector)
-import Moonlight.Triangulation.Internal.Paged
-  ( Paged
-  , PublicationStats
-  , pagedLength
-  , pagedUnsafeIndex
-  )
-import Moonlight.Triangulation.Internal.PointIndex (PointIndex)
-import Moonlight.Triangulation.Internal.Types
-  ( BuildStats
-  , ConstraintMode (..)
-  , ElementDefaults (..)
-  , InsertionDisposition
-  , ValidationClosureStats
-  , unitElementDefaults
-  )
-import GHC.Generics (Generic)
-
--- | Immutable finite DCEL. The coordinate pages own the geometry; vertex
--- payloads are free annotations carried alongside it.
--- 'Moonlight.Triangulation.Internal.Types.HasPosition' is how a point is read
--- out of a payload at the moment of
--- ingestion and is not consulted again, so a payload whose instance later
--- disagrees with where its vertex sits is not a corrupt triangulation — it is a
--- payload nobody asks about position. Half-edge topology is one interleaved
--- arena: edge @e@ owns slots @4e..4e+3@ holding origin, next, previous and
--- face, so a twin pair is one contiguous eight-word record.
--- Directed edges are adjacent twin pairs, so reversal is an XOR with one.
--- Face zero is the unique outer face. Constraint flags are stored once per
--- undirected edge and are zero for ordinary Delaunay triangulations.
--- All four payload components are therefore representational: coercing a
--- newtype through any of them is a coercion, not a rebuild.
-type role Triangulation nominal representational representational representational representational
-
-data Triangulation (mode :: ConstraintMode) vertex directed undirected face = Triangulation
-  { triPointX :: !(Paged Double)
-  , triPointY :: !(Paged Double)
-  , -- | Derived position-hash buckets containing vertex handles only. This is
-    -- deliberately lazy: geometry is authoritative, so a workload that never
-    -- asks an identity question owes no cache construction.
-    triPointIndex :: PointIndex
-  , triVertexOut :: !(Paged Word32)
-  , triVertexData :: !(BoxedPaged vertex)
-  , triHalfTopology :: !(Paged Word32)
-  , triDirectedData :: !(BoxedPaged directed)
-  , triUndirectedData :: !(BoxedPaged undirected)
-  , triFaceEdge :: !(Paged Word32)
-  , triFaceData :: !(BoxedPaged face)
-  , triConstraint :: !(Paged Word8)
-  , triConstraintCount :: {-# UNPACK #-} !Int
-  , -- | Derived exact membership for the sparse constrained edge section.
-    -- The flag plane remains authoritative and serializable; this index is
-    -- transported with edge rewrites so constraint-only queries need not scan
-    -- every ordinary Delaunay edge.
-    triConstraintEdges :: !IntSet.IntSet
-  , -- | Derived outer-frontier handles for persistent separated joins. The
-    -- geometry/topology planes remain authoritative; this cache is omitted
-    -- from equality and serialization and is rebuilt only at an explicit
-    -- geometry-only preparation boundary.
-    triSeamFrontier :: !(Maybe SeamFrontierIndex)
-  , triElementDefaults :: !(ElementDefaults directed undirected face)
-  }
-  deriving stock (Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact immutable outer cycle plus four deterministic extreme anchors per
--- chart used to seed separated tangents. The sequence is persistent: seam
--- joins compose retained source sections without copying their elements.
-data SeamFrontierIndex = SeamFrontierIndex
-  { seamFrontierEdges :: !(Seq.Seq Int)
-  , seamFrontierXMinimum :: !Double
-  , seamFrontierXMaximum :: !Double
-  , seamFrontierLowerRightmost :: {-# UNPACK #-} !Int
-  , seamFrontierLowerLeftmost :: {-# UNPACK #-} !Int
-  , seamFrontierUpperRightmost :: {-# UNPACK #-} !Int
-  , seamFrontierUpperLeftmost :: {-# UNPACK #-} !Int
-  , -- | The same four extreme anchors in the orientation-preserving
-    -- quarter-turn chart @(u,v) = (y,-x)@.  Keeping this alongside the
-    -- existing x chart lets a later seam admit a north/south extension
-    -- without walking the resident frontier again.
-    seamFrontierYMinimum :: !Double
-  , seamFrontierYMaximum :: !Double
-  , seamFrontierYLowerRightmost :: {-# UNPACK #-} !Int
-  , seamFrontierYLowerLeftmost :: {-# UNPACK #-} !Int
-  , seamFrontierYUpperRightmost :: {-# UNPACK #-} !Int
-  , seamFrontierYUpperLeftmost :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Derive the exact frontier once from an admitted DCEL. This is intentionally
--- an explicit preparation operation: ordinary mutable publication clears the
--- cache instead of silently paying an outer-cycle traversal.
-prepareSeamFrontierIndex
-  :: Triangulation mode vertex directed undirected face
-  -> Maybe SeamFrontierIndex
-prepareSeamFrontierIndex triangulation = do
-  start <- frontierStart triangulation
-  let !directedCount = numDirectedEdges triangulation
-      !edges =
-        unfoldr
-          (frontierStep start directedCount)
-          (start, True, directedCount + 1)
-      terminal = List.foldl' (\_ edge -> Just edge) Nothing edges
-      closesAtStart = do
-        lastEdge <- terminal
-        pure
-          ( unpackIndex
-              (pagedUnsafeIndex (triHalfTopology triangulation) (4 * lastEdge + 1))
-              == start
-          )
-  if closesAtStart == Just True
-    then summarizeFrontier triangulation (Seq.fromList edges)
-    else Nothing
- where
-  numDirectedEdges
-    :: Triangulation mode vertex directed undirected face
-    -> Int
-  numDirectedEdges mesh = pagedLength (triHalfTopology mesh) `quot` 4
-
-  frontierStart
-    :: Triangulation mode vertex directed undirected face
-    -> Maybe Int
-  frontierStart mesh =
-    unpackOptionalIndex (pagedUnsafeIndex (triFaceEdge mesh) 0)
-
-  frontierStep
-    :: Int
-    -> Int
-    -> (Int, Bool, Int)
-    -> Maybe (Int, (Int, Bool, Int))
-  frontierStep start directedCount (edge, first, remaining)
-    | remaining <= 0 = Nothing
-    | edge == start && not first = Nothing
-    | edge < 0 || edge >= directedCount = Nothing
-    | otherwise =
-        let next =
-              fromIntegral
-                (pagedUnsafeIndex (triHalfTopology triangulation) (4 * edge + 1))
-         in Just (edge, (next, False, remaining - 1))
-
-summarizeFrontier
-  :: Triangulation mode vertex directed undirected face
-  -> Seq.Seq Int
-  -> Maybe SeamFrontierIndex
-summarizeFrontier triangulation edges =
-  case fmap (frontierPoint triangulation) (toList edges) of
-    [] -> Nothing
-    points -> do
-      (minimumX, maximumX, lowerRightmost, lowerLeftmost, upperRightmost, upperLeftmost) <-
-        summarizeSeamChart id (zip [0 ..] points)
-      (minimumY, maximumY, yLowerRightmost, yLowerLeftmost, yUpperRightmost, yUpperLeftmost) <-
-        summarizeSeamChart seamQuarterTurn (zip [0 ..] points)
-      pure
-        SeamFrontierIndex
-          { seamFrontierEdges = edges
-          , seamFrontierXMinimum = minimumX
-          , seamFrontierXMaximum = maximumX
-          , seamFrontierLowerRightmost = lowerRightmost
-          , seamFrontierLowerLeftmost = lowerLeftmost
-          , seamFrontierUpperRightmost = upperRightmost
-          , seamFrontierUpperLeftmost = upperLeftmost
-          , seamFrontierYMinimum = minimumY
-          , seamFrontierYMaximum = maximumY
-          , seamFrontierYLowerRightmost = yLowerRightmost
-          , seamFrontierYLowerLeftmost = yLowerLeftmost
-          , seamFrontierYUpperRightmost = yUpperRightmost
-          , seamFrontierYUpperLeftmost = yUpperLeftmost
-          }
- where
-  frontierPoint
-    :: Triangulation mode vertex directed undirected face
-    -> Int
-    -> (Double, Double)
-  frontierPoint mesh rawEdge =
-    let rawVertex = pagedUnsafeIndex (triHalfTopology mesh) (4 * rawEdge)
-        vertex = unpackIndex rawVertex
-     in ( pagedUnsafeIndex (triPointX mesh) vertex
-        , pagedUnsafeIndex (triPointY mesh) vertex
-        )
-
--- | Summarize one chart from a bounded set of frontier points.  The index is
--- retained beside the transformed point so the seam can carry the witness
--- through a residual interval without re-reading the source frontier.
-summarizeSeamChart
-  :: ((Double, Double) -> (Double, Double))
-  -> [(Int, (Double, Double))]
-  -> Maybe (Double, Double, Int, Int, Int, Int)
-summarizeSeamChart transform points =
-  case fmap (\(index, point) -> (index, transform point)) points of
-    [] -> Nothing
-    first : remaining ->
-      let (!minimumValue, !maximumValue) =
-            List.foldl'
-              (\(!minimumSoFar, !maximumSoFar) (_, (u, _)) ->
-                 (min minimumSoFar u, max maximumSoFar u))
-              (chartU (snd first), chartU (snd first))
-              remaining
-          choose preference =
-            snd
-              ( List.foldl'
-                  (\(!bestPoint, !bestIndex) (index, candidate) ->
-                     if preference bestPoint candidate
-                       then (candidate, index)
-                       else (bestPoint, bestIndex))
-                  (snd first, fst first)
-                  remaining
-              )
-       in Just
-            ( minimumValue
-            , maximumValue
-            , choose preferRightmost
-            , choose preferLeftmost
-            , choose preferRightmostUpper
-            , choose preferLeftmostUpper
-            )
- where
-  chartU :: (Double, Double) -> Double
-  preferRightmost :: (Double, Double) -> (Double, Double) -> Bool
-  preferLeftmost :: (Double, Double) -> (Double, Double) -> Bool
-  preferRightmostUpper :: (Double, Double) -> (Double, Double) -> Bool
-  preferLeftmostUpper :: (Double, Double) -> (Double, Double) -> Bool
-  chartU (u, _) = u
-  preferRightmost (bestU, bestV) (candidateU, candidateV) =
-    candidateU > bestU || (candidateU == bestU && candidateV < bestV)
-  preferLeftmost (bestU, bestV) (candidateU, candidateV) =
-    candidateU < bestU || (candidateU == bestU && candidateV < bestV)
-  preferRightmostUpper (bestU, bestV) (candidateU, candidateV) =
-    candidateU > bestU || (candidateU == bestU && candidateV > bestV)
-  preferLeftmostUpper (bestU, bestV) (candidateU, candidateV) =
-    candidateU < bestU || (candidateU == bestU && candidateV > bestV)
-
--- | Orientation-preserving quarter turn used by the north/south seam chart.
-seamQuarterTurn :: (Double, Double) -> (Double, Double)
-seamQuarterTurn (x, y) = (y, -x)
-
--- | Forget every payload while preparing a geometry-only mesh for persistent
--- extension. Each payload plane is replaced directly with a defaulted,
--- zero-page unit store; no source payload is traversed or densely rebuilt.
--- Coordinates, topology, constraints, counts, and derived point identity
--- remain authoritative, while future elements inherit the unit defaults.
-geometryOnlyPublication
-  :: Triangulation mode vertex directed undirected face
-  -> Triangulation mode () () () ()
-geometryOnlyPublication triangulation =
-  triangulation
-    { triVertexData = boxedDefaulted () (pagedLength (triPointX triangulation))
-    , triDirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 4)
-    , triUndirectedData = boxedDefaulted () (pagedLength (triHalfTopology triangulation) `quot` 8)
-    , triFaceData = boxedDefaulted () (pagedLength (triFaceEdge triangulation))
-    , triElementDefaults = unitElementDefaults
-    , triSeamFrontier =
-        case triSeamFrontier triangulation of
-          Just frontier -> Just frontier
-          Nothing -> prepareSeamFrontierIndex triangulation
-    }
-
--- The point index is a derived cache and therefore not an observable part of
--- the mesh value. Structural equality compares every semantic plane and
--- default while deliberately refusing to construct or compare that cache.
-instance
-  ( Eq vertex
-  , Eq directed
-  , Eq undirected
-  , Eq face
-  ) => Eq (Triangulation mode vertex directed undirected face) where
-  left == right =
-    triPointX left == triPointX right
-      && triPointY left == triPointY right
-      && triVertexOut left == triVertexOut right
-      && triVertexData left == triVertexData right
-      && triHalfTopology left == triHalfTopology right
-      && triDirectedData left == triDirectedData right
-      && triUndirectedData left == triUndirectedData right
-      && triFaceEdge left == triFaceEdge right
-      && triFaceData left == triFaceData right
-      && triConstraint left == triConstraint right
-      && triConstraintCount left == triConstraintCount right
-      && triElementDefaults left == triElementDefaults right
-
-promoteConstrained
-  :: Triangulation 'Unconstrained vertex directed undirected face
-  -> Triangulation 'Constrained vertex directed undirected face
-promoteConstrained Triangulation{
-  triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,
-  triDirectedData, triUndirectedData, triFaceEdge, triFaceData,
-  triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier,
-  triElementDefaults
-  } =
-  Triangulation{
-    triPointX, triPointY, triPointIndex, triVertexOut, triVertexData, triHalfTopology,
-    triDirectedData, triUndirectedData, triFaceEdge, triFaceData,
-    triConstraint, triConstraintCount, triConstraintEdges, triSeamFrontier,
-    triElementDefaults
-    }
-
--- | A traversal of every occurrence of one payload parameter, in the van
--- Laarhoven encoding: an effectful visit that may change the payload's type.
--- The 'Applicative' belongs to the caller, so one traversal per parameter
--- serves relabeling, collection and genuinely effectful annotation alike
--- instead of a separate function for each.
-type PayloadTraversal source target payload payload' =
-  forall f. Applicative f => (payload -> f payload') -> source -> f target
-
--- | Every stored vertex payload, in vertex order.
---
--- The vertex store carries no fill — a vertex's payload arrives with the
--- vertex, and no slot is read before it is written — so the visits are exactly
--- the stored payloads and nothing besides.
-vertexPayloads
-  :: PayloadTraversal
-      (Triangulation mode vertex directed undirected face)
-      (Triangulation mode vertex' directed undirected face)
-      vertex
-      vertex'
-vertexPayloads visit triangulation =
-  (\payloads -> triangulation{triVertexData = boxedFromVector Nothing payloads})
-    <$> traverse visit (boxedToVector (triVertexData triangulation))
-
--- | Every stored directed-edge payload, then the default a later directed edge
--- will inherit.
---
--- The default is visited because it is a payload the structure carries, and it
--- is visited /once/: its single image is written both to t'ElementDefaults' and
--- to the store's fill, which every slot of an unmaterialized page reports.
--- Visiting the two positions separately would let an effect with more than one
--- answer hand them different values, and a triangulation whose future elements
--- disagree with its present ones is not a triangulation anyone asked for.
-directedPayloads
-  :: PayloadTraversal
-      (Triangulation mode vertex directed undirected face)
-      (Triangulation mode vertex directed' undirected face)
-      directed
-      directed'
-directedPayloads visit triangulation =
-  (\payloads fallback ->
-     triangulation
-       { triDirectedData = boxedFromVector (Just fallback) payloads
-       , triElementDefaults = defaults{defaultDirectedEdgeData = fallback}
-       })
-    <$> traverse visit (boxedToVector (triDirectedData triangulation))
-    <*> visit (defaultDirectedEdgeData defaults)
- where
-  defaults = triElementDefaults triangulation
-
--- | Every stored undirected-edge payload, then the default a later undirected
--- edge will inherit.
-undirectedPayloads
-  :: PayloadTraversal
-      (Triangulation mode vertex directed undirected face)
-      (Triangulation mode vertex directed undirected' face)
-      undirected
-      undirected'
-undirectedPayloads visit triangulation =
-  (\payloads fallback ->
-     triangulation
-       { triUndirectedData = boxedFromVector (Just fallback) payloads
-       , triElementDefaults = defaults{defaultUndirectedEdgeData = fallback}
-       })
-    <$> traverse visit (boxedToVector (triUndirectedData triangulation))
-    <*> visit (defaultUndirectedEdgeData defaults)
- where
-  defaults = triElementDefaults triangulation
-
--- | Every stored face payload, then the default a later face will inherit.
-facePayloads
-  :: PayloadTraversal
-      (Triangulation mode vertex directed undirected face)
-      (Triangulation mode vertex directed undirected face')
-      face
-      face'
-facePayloads visit triangulation =
-  (\payloads fallback ->
-     triangulation
-       { triFaceData = boxedFromVector (Just fallback) payloads
-       , triElementDefaults = defaults{defaultFaceData = fallback}
-       })
-    <$> traverse visit (boxedToVector (triFaceData triangulation))
-    <*> visit (defaultFaceData defaults)
- where
-  defaults = triElementDefaults triangulation
-
--- | Ranges over the face payload, which is the last parameter and so the only
--- one a class of this kind can reach. The other three payloads have exactly
--- the same structure under 'vertexPayloads', 'directedPayloads' and
--- 'undirectedPayloads'; they are simply not spellable as instances here.
---
--- 'mapFaces' rather than the traversal, because it leaves an unmaterialized
--- page unmaterialized. The two agree on everything a 'BoxedPaged' lets anyone
--- observe, which is what the coherence law asks and all it asks.
-instance Functor (Triangulation mode vertex directed undirected) where
-  fmap = mapFaces
-  {-# INLINE fmap #-}
-
--- | Folds the stored face payloads and then the default, so 'length' is one
--- greater than the number of stored faces. A fold that skipped the default
--- would report a triangulation as holding a value it does hold.
-instance Foldable (Triangulation mode vertex directed undirected) where
-  foldMap = foldMapDefault
-  {-# INLINE foldMap #-}
-
-instance Traversable (Triangulation mode vertex directed undirected) where
-  traverse = facePayloads
-  {-# INLINE traverse #-}
-
--- | An element payload map carries the element defaults with it. A new
--- insertion hands its new elements the default, so a map that reindexed the
--- stored payloads and left the default behind would produce a triangulation
--- whose future elements disagree with its present ones. The type system very
--- nearly forces this on its own — the image type is inhabited here only
--- through the mapping function — but only a test can insist the argument is
--- the /default/ rather than some other payload of the right type.
-mapDirectedEdges
-  :: (directed -> directed')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed' undirected face
-mapDirectedEdges f triangulation =
-  triangulation
-    { triDirectedData = fmap f (triDirectedData triangulation)
-    , triElementDefaults = defaults{defaultDirectedEdgeData = f (defaultDirectedEdgeData defaults)}
-    }
- where
-  defaults = triElementDefaults triangulation
-
--- | Map every undirected-edge annotation and its future-element default.
-mapUndirectedEdges
-  :: (undirected -> undirected')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected' face
-mapUndirectedEdges f triangulation =
-  triangulation
-    { triUndirectedData = fmap f (triUndirectedData triangulation)
-    , triElementDefaults = defaults{defaultUndirectedEdgeData = f (defaultUndirectedEdgeData defaults)}
-    }
- where
-  defaults = triElementDefaults triangulation
-
--- | Map every face annotation and its future-element default.
-mapFaces
-  :: (face -> face')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face'
-mapFaces f triangulation =
-  triangulation
-    { triFaceData = fmap f (triFaceData triangulation)
-    , triElementDefaults = defaults{defaultFaceData = f (defaultFaceData defaults)}
-    }
- where
-  defaults = triElementDefaults triangulation
-
--- | The vertex component is free, like the other three. Geometry owns the
--- points, so a payload map cannot move one — the image type need not even have
--- a position to speak of. There is no vertex default to carry: vertices arrive
--- with their payloads.
-mapVertices
-  :: (vertex -> vertex')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex' directed undirected face
-mapVertices f triangulation =
-  triangulation{triVertexData = fmap f (triVertexData triangulation)}
-
--- | Materialize every resident undirected-edge annotation in handle order
--- while installing the declared fallback for edges created by a later edit.
-imapUndirectedEdges
-  :: undirected'
-  -> (UndirectedEdgeId -> undirected -> undirected')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected' face
-imapUndirectedEdges fallback relabel triangulation =
-  triangulation
-    { triUndirectedData =
-        boxedFromVector (Just fallback)
-          (V.imap (\index -> relabel (UndirectedEdgeId (fromIntegral index))) payloads)
-    , triElementDefaults = defaults{defaultUndirectedEdgeData = fallback}
-    }
- where
-  payloads = boxedToVector (triUndirectedData triangulation)
-  defaults = triElementDefaults triangulation
-
--- | Materialize every resident face annotation in handle order while
--- installing the declared fallback for faces created by a later edit.
-imapFaces
-  :: face'
-  -> (FaceId -> face -> face')
-  -> Triangulation mode vertex directed undirected face
-  -> Triangulation mode vertex directed undirected face'
-imapFaces fallback relabel triangulation =
-  triangulation
-    { triFaceData =
-        boxedFromVector (Just fallback)
-          (V.imap (\index -> relabel (FaceId (fromIntegral index))) payloads)
-    , triElementDefaults = defaults{defaultFaceData = fallback}
-    }
- where
-  payloads = boxedToVector (triFaceData triangulation)
-  defaults = triElementDefaults triangulation
-
--- | Geometry-only unconstrained Delaunay triangulation.
-type DelaunayTriangulation vertex = Triangulation 'Unconstrained vertex () () ()
-
--- | Geometry-only constrained Delaunay triangulation.
-type ConstrainedDelaunayTriangulation vertex = Triangulation 'Constrained vertex () () ()
-
--- | A constructed triangulation and the canonical handle chosen for each input.
---
--- The result is a value, not a history: derived 'Eq'/'Show' would observe
--- 'buildStats' through a facade that hides it, so neither instance exists.
-data BuildResult mode vertex directed undirected face = BuildResult
-  { -- | The immutable constructed mesh.
-    buildTriangulation :: !(Triangulation mode vertex directed undirected face)
-  , -- | Canonical vertex handle for each input position, including duplicates.
-    buildInputVertices :: !(PrimArray Word32)
-  , buildStats :: !BuildStats
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
--- | Published insertion result, selected vertex, disposition, and work receipt.
-data InsertionResult mode vertex directed undirected face = InsertionResult
-  { insertionTriangulation :: !(Triangulation mode vertex directed undirected face)
-  , insertionVertex :: !VertexId
-  , insertionDisposition :: !InsertionDisposition
-  , insertionStats :: !BuildStats
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-deriving stock instance
-  (Eq vertex, Eq directed, Eq undirected, Eq face)
-  => Eq (InsertionResult mode vertex directed undirected face)
-deriving stock instance
-  (Show vertex, Show directed, Show undirected, Show face)
-  => Show (InsertionResult mode vertex directed undirected face)
-
--- | Exact support touched by one refinement publication. Checked local
--- refinement uses this as the positive receipt accompanying its typed
--- obstruction surface; unrestricted refinement deliberately avoids the
--- additional support scan.
-data RefinementReceipt = RefinementReceipt
-  { refinementVisitedJoinFaces :: !(V.Vector FaceId)
-  , refinementVisitedProtectedFaces :: !(V.Vector FaceId)
-  , refinementCreatedFaces :: !(V.Vector FaceId)
-    -- ^ Face slots whose published triangle changed or was appended.
-  , refinementFinalPermittedFaces :: !(V.Vector FaceId)
-    -- ^ Exact final face lineage admitted by a checked local domain.
-  , refinementFinalInterfaceIncidence :: !(V.Vector (UndirectedEdgeId, FaceId, FaceId))
-  , refinementTouchedEdges :: !(V.Vector UndirectedEdgeId)
-  , refinementRemovedEdges :: !(V.Vector UndirectedEdgeId)
-  , refinementInterfaceBoundaryReads :: {-# UNPACK #-} !Int
-  , refinementAttemptedBoundaryCrossings :: {-# UNPACK #-} !Int
-  , refinementPublicationStats :: !PublicationStats
-    -- ^ Page publication work measured by the transaction owner.
-  , refinementValidationClosureStats :: !ValidationClosureStats
-    -- ^ Typed support counts emitted by the selected validation descent.
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A locally refined section paired with the proof of what the checked
--- interpreter observed and rewrote. Ordinary refinement does not pay to
--- construct this proof.
-data RefinementDomainResult mode vertex directed undirected face = RefinementDomainResult
-  { refinementDomainResult :: !(RefinementResult mode vertex directed undirected face)
-  , refinementDomainReceipt :: !RefinementReceipt
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
--- | Refined mesh together with the budget, exclusion, and support outcome.
-data RefinementResult mode vertex directed undirected face = RefinementResult
-  { -- | The immutable mesh after all admitted refinement steps.
-    refinedTriangulation :: !(Triangulation mode vertex directed undirected face)
-  , refinementStats :: !BuildStats
-  , refinementAddedVertices :: {-# UNPACK #-} !Int
-  -- | Whether the quality worklist drained. 'False' means the vertex budget
-  -- stopped the run with work outstanding. A drained worklist can still leave
-  -- faces the quality bounds condemn but no admissible Steiner point can fix;
-  -- auditing the result is the caller's to ask for, not a cost every run pays.
-  , refinementComplete :: !Bool
-  , -- | Faces deliberately excluded by barrier-depth policy.
-    refinementExcludedFaces :: !(V.Vector FaceId)
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/SegmentRelation.hs b/src-dcel/Moonlight/Triangulation/Internal/SegmentRelation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/SegmentRelation.hs
+++ /dev/null
@@ -1,91 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | The complete closed-segment relation vocabulary and its one policy owner.
-module Moonlight.Triangulation.Internal.SegmentRelation
-  ( SegmentRelation (..)
-  , allSegmentRelations
-  , segmentRelationWith
-  ) where
-
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
-
--- | The complete exact-predicate relation between two closed segments. There
--- is one vocabulary owner; traversal and constrained-union consumers derive
--- their booleans and obstruction policy from it rather than cloning slightly
--- different orientation formulae.
-data SegmentRelation
-  = SegmentsDisjoint
-  | SegmentsDuplicate
-  | SegmentsShareEndpoint
-  | SegmentsProperlyCross
-  | SegmentEndpointTouchesInterior
-  | SegmentsCollinearlyOverlap
-  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Every segment relation in constructor order.
-allSegmentRelations :: [SegmentRelation]
-allSegmentRelations = [minBound .. maxBound]
-
--- | Classify two closed segments using the supplied point observations.
-segmentRelationWith
-  :: (point -> point -> Bool)
-  -- ^ Point equality.
-  -> (point -> point -> Ordering)
-  -- ^ Lexicographic point ordering.
-  -> (point -> point -> point -> Ordering)
-  -- ^ Orientation of an ordered triple.
-  -> (point -> point -> point -> Bool)
-  -- ^ Membership of the third point in the closed segment.
-  -> point
-  -> point
-  -> point
-  -> point
-  -> SegmentRelation
-segmentRelationWith equalPoint comparePoint orientation onSegment a b c d
-  | sameUndirectedSegment = SegmentsDuplicate
-  | sharesEndpoint = SegmentsShareEndpoint
-  | opposite abC abD && opposite cdA cdB = SegmentsProperlyCross
-  | abC == EQ && abD == EQ && cdA == EQ && cdB == EQ = collinearRelation
-  | endpointTouches = SegmentEndpointTouchesInterior
-  | otherwise = SegmentsDisjoint
- where
-  !abC = orientation a b c
-  !abD = orientation a b d
-  !cdA = orientation c d a
-  !cdB = orientation c d b
-  sameUndirectedSegment =
-    (equalPoint a c && equalPoint b d)
-      || (equalPoint a d && equalPoint b c)
-  sharesEndpoint =
-    equalPoint a c
-      || equalPoint a d
-      || equalPoint b c
-      || equalPoint b d
-  endpointTouches =
-    (abC == EQ && onSegment a b c)
-      || (abD == EQ && onSegment a b d)
-      || (cdA == EQ && onSegment c d a)
-      || (cdB == EQ && onSegment c d b)
-  collinearRelation =
-    let !overlapLower = maximumPoint (minimumPoint a b) (minimumPoint c d)
-        !overlapUpper = minimumPoint (maximumPoint a b) (maximumPoint c d)
-     in case comparePoint overlapLower overlapUpper of
-          LT -> SegmentsCollinearlyOverlap
-          EQ -> SegmentEndpointTouchesInterior
-          GT -> SegmentsDisjoint
-  opposite left right =
-    (left == LT && right == GT) || (left == GT && right == LT)
-  minimumPoint left right =
-    case comparePoint left right of
-      GT -> right
-      _ -> left
-  maximumPoint left right =
-    case comparePoint left right of
-      LT -> right
-      _ -> left
-{-# INLINE segmentRelationWith #-}
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Tournament.hs b/src-dcel/Moonlight/Triangulation/Internal/Tournament.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Tournament.hs
+++ /dev/null
@@ -1,45 +0,0 @@
--- | One deterministic balanced binary descent shared by every pure
--- associative schedule in the package.  Leaves retain their source values;
--- interpreters own only their binary operation.
-module Moonlight.Triangulation.Internal.Tournament
-  ( TournamentPlan (..)
-  , planTournament
-  , interpretTournament
-  ) where
-
-import Data.List.NonEmpty (NonEmpty (..))
-
-data TournamentPlan value
-  = TournamentLeaf !value
-  | TournamentNode !(TournamentPlan value) !(TournamentPlan value)
-
-planTournament :: NonEmpty value -> TournamentPlan value
-planTournament = descendTournament . fmap TournamentLeaf
-
-interpretTournament
-  :: (value -> value -> Either failure value)
-  -> TournamentPlan value
-  -> Either failure value
-interpretTournament combine tournament =
-  case tournament of
-    TournamentLeaf value -> Right value
-    TournamentNode left right -> do
-      leftValue <- interpretTournament combine left
-      rightValue <- interpretTournament combine right
-      combine leftValue rightValue
-
-descendTournament :: NonEmpty (TournamentPlan value) -> TournamentPlan value
-descendTournament (single :| []) = single
-descendTournament plans = descendTournament (pairTournamentRound plans)
-
-pairTournamentRound
-  :: NonEmpty (TournamentPlan value)
-  -> NonEmpty (TournamentPlan value)
-pairTournamentRound (left :| right : rest) =
-  TournamentNode left right :| pairTournamentTail rest
-pairTournamentRound (single :| []) = single :| []
-
-pairTournamentTail :: [TournamentPlan value] -> [TournamentPlan value]
-pairTournamentTail (left : right : rest) =
-  TournamentNode left right : pairTournamentTail rest
-pairTournamentTail rest = rest
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Types.hs b/src-dcel/Moonlight/Triangulation/Internal/Types.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Types.hs
+++ /dev/null
@@ -1,470 +0,0 @@
-{-# LANGUAGE FunctionalDependencies #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE UndecidableInstances #-}
-{-# LANGUAGE UndecidableSuperClasses #-}
-{-# LANGUAGE TypeFamilies #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE NamedFieldPuns #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE RoleAnnotations #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The vocabulary: the types the surface names, none of which mentions the
--- stored representation.
-module Moonlight.Triangulation.Internal.Types
-  ( Point (..)
-  , SiteRelation (..)
-  , QueryPoint (..)
-  , PointValidationError (..)
-  , HasPosition (..)
-  , ElementDefaults (..)
-  , unitElementDefaults
-  , ConstraintMode (..)
-  , KnownConstraintMode (..)
-  , InsertionDisposition (..)
-  , BuildStats (..)
-  , emptyBuildStats
-  , ValidationClosureStats (..)
-  , CoordinateError (..)
-  , NonFiniteValue (..)
-  , classifyNonFinite
-  , RadiusSquared
-  , RadiusSquaredError (..)
-  , mkRadiusSquared
-  , radiusSquaredValue
-  , BuildError (..)
-  , Location (..)
-  , LocationHint (..)
-  , LocationStats (..)
-  , emptyLocationStats
-  , NearestStats (..)
-  , RefinementParameters (..)
-  , defaultRefinementParameters
-  , InvariantViolation (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  )
-import Data.Word (Word8)
-import Foreign.Ptr (castPtr)
-import Foreign.Storable (Storable (..), peekElemOff, pokeElemOff)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Internal.BoxedPaged (BoxedStorageError)
-
--- | Type-level witness for whether constraint flags may be present.
-data ConstraintMode = Unconstrained | Constrained
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Recover a type-level constraint mode as a value.
-class KnownConstraintMode (mode :: ConstraintMode) where
-  constraintModeValue :: proxy mode -> ConstraintMode
-
-instance KnownConstraintMode 'Unconstrained where
-  constraintModeValue _ = Unconstrained
-
-instance KnownConstraintMode 'Constrained where
-  constraintModeValue _ = Constrained
-
--- | Exact support visited by the selected topology-validation descent.  The
--- validator constructs this from its admitted closure sets; consumers do not
--- reconstruct it from a later receipt or a global mesh census.
-data ValidationClosureStats = ValidationClosureStats
-  { validationClosureFaces :: {-# UNPACK #-} !Int
-  , validationClosureDirectedEdges :: {-# UNPACK #-} !Int
-  , validationClosureVertices :: {-# UNPACK #-} !Int
-  , validationClosureInterfacePairs :: {-# UNPACK #-} !Int
-  , validationClosureConstraintPairs :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Cartesian binary64 point.
-data Point = Point
-  { pointX :: !Double
-  , pointY :: !Double
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact geometric relation between two finite coordinate supports. The
--- overlap count is strictly positive in 'PartialOverlap'; equality, subset and
--- disjointness have already been excluded before that constructor is chosen.
-data SiteRelation
-  = -- | Both supports contain exactly the same coordinates.
-    EqualSites
-  | -- | Every left coordinate occurs on the right, which has at least one more.
-    LeftProperSubset
-  | -- | Every right coordinate occurs on the left, which has at least one more.
-    RightProperSubset
-  | -- | The supports share no coordinate.
-    DisjointSites
-  | -- | Neither support contains the other; the field is the positive number
-    -- of coordinates they share.
-    PartialOverlap {-# UNPACK #-} !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A coordinate pair admitted to the exact-predicate domain and normalized
--- at its construction boundary. Query algorithms consume this phase rather
--- than each inventing a fallback for invalid floating-point input.
-newtype QueryPoint = QueryPoint
-  { -- | The admitted, canonically normalized point.
-    queryPointValue :: Point
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Coordinate axis and reason that kept a point outside the query domain.
-data PointValidationError
-  = InvalidPointX !CoordinateError
-  | InvalidPointY !CoordinateError
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-instance Storable (Point) where
-  sizeOf _ = 2 * sizeOf (0 :: Double)
-  alignment _ = alignment (0 :: Double)
-  peek pointer = do
-    x <- peekElemOff (castPtr pointer) 0
-    y <- peekElemOff (castPtr pointer) 1
-    pure (Point x y)
-  poke pointer (Point x y) = do
-    pokeElemOff (castPtr pointer) 0 x
-    pokeElemOff (castPtr pointer) 1 y
-
--- | Extract a vertex's position once, at the construction boundary.
-class HasPosition vertex where
-  position :: vertex -> Point
-
-instance HasPosition (Point) where
-  position = id
-
--- | Payloads inherited by topology elements created after initial loading.
-data ElementDefaults directed undirected face = ElementDefaults
-  { defaultDirectedEdgeData :: !directed
-  , defaultUndirectedEdgeData :: !undirected
-  , defaultFaceData :: !face
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Payload defaults for geometry-only triangulations.
-unitElementDefaults :: ElementDefaults () () ()
-unitElementDefaults = ElementDefaults () () ()
-
--- | Construction, location, legalization, and refinement work counters.
-data BuildStats = BuildStats
-  { statInputPoints :: {-# UNPACK #-} !Int
-  , statUniquePoints :: {-# UNPACK #-} !Int
-  , statExistingPoints :: {-# UNPACK #-} !Int
-  , statDuplicatePoints :: {-# UNPACK #-} !Int
-  , statSpatialSeedPoints :: {-# UNPACK #-} !Int
-  , statFaceSplits :: {-# UNPACK #-} !Int
-  , statInteriorEdgeSplits :: {-# UNPACK #-} !Int
-  , statBoundaryEdgeSplits :: {-# UNPACK #-} !Int
-  , statHullInsertions :: {-# UNPACK #-} !Int
-  , statLineSplits :: {-# UNPACK #-} !Int
-  , statLineExtensions :: {-# UNPACK #-} !Int
-  , statLineToAreaTransitions :: {-# UNPACK #-} !Int
-  , statEdgeFlips :: {-# UNPACK #-} !Int
-  , statLocationWalkSteps :: {-# UNPACK #-} !Int
-  , statLocationFallbacks :: {-# UNPACK #-} !Int
-  , statLocationMaxWalk :: {-# UNPACK #-} !Int
-  , statLegalizationMaxStack :: {-# UNPACK #-} !Int
-  , statSteinerPoints :: {-# UNPACK #-} !Int
-  , statRefinementFaceChecks :: {-# UNPACK #-} !Int
-  , statRefinementQueuePops :: {-# UNPACK #-} !Int
-  , statSweepFastPoints :: {-# UNPACK #-} !Int
-  , statSweepSkippedPoints :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Show, Read, Generic)
-  deriving anyclass (NFData)
-
--- | The additive identity for construction telemetry.
-emptyBuildStats :: BuildStats
-emptyBuildStats =
-  BuildStats
-    { statInputPoints = 0
-    , statUniquePoints = 0
-    , statExistingPoints = 0
-    , statDuplicatePoints = 0
-    , statSpatialSeedPoints = 0
-    , statFaceSplits = 0
-    , statInteriorEdgeSplits = 0
-    , statBoundaryEdgeSplits = 0
-    , statHullInsertions = 0
-    , statLineSplits = 0
-    , statLineExtensions = 0
-    , statLineToAreaTransitions = 0
-    , statEdgeFlips = 0
-    , statLocationWalkSteps = 0
-    , statLocationFallbacks = 0
-    , statLocationMaxWalk = 0
-    , statLegalizationMaxStack = 0
-    , statSteinerPoints = 0
-    , statRefinementFaceChecks = 0
-    , statRefinementQueuePops = 0
-    , statSweepFastPoints = 0
-    , statSweepSkippedPoints = 0
-    }
-
--- | Whether an insertion published a new site or selected an existing one.
-data InsertionDisposition = Inserted | AlreadyPresent
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Reason a floating-point coordinate cannot enter the exact-predicate domain.
-data CoordinateError
-  = CoordinateNaN
-  | CoordinateInfinite
-  | CoordinateTooSmall
-  | CoordinateTooLarge
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Classification retained when a numeric parameter is not finite.
-data NonFiniteValue
-  = ValueNaN
-  | ValuePositiveInfinity
-  | ValueNegativeInfinity
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Classify NaN and signed infinity, leaving finite values unclassified.
-classifyNonFinite :: Double -> Maybe NonFiniteValue
-classifyNonFinite value
-  | isNaN value = Just ValueNaN
-  | isInfinite value && value < 0 = Just ValueNegativeInfinity
-  | isInfinite value = Just ValuePositiveInfinity
-  | otherwise = Nothing
-
--- | An admitted finite, non-negative squared radius. This belongs to the
--- shared geometric vocabulary because both circle queries and alpha
--- filtration consume it.
-newtype RadiusSquared = RadiusSquared Double
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed refusal shared by every squared-radius consumer.
-data RadiusSquaredError
-  = NonFiniteRadiusSquared !NonFiniteValue
-  | NegativeRadiusSquared !Double
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Admit a finite, non-negative squared radius.
-mkRadiusSquared :: Double -> Either RadiusSquaredError RadiusSquared
-mkRadiusSquared value =
-  case classifyNonFinite value of
-    Just nonFinite -> Left (NonFiniteRadiusSquared nonFinite)
-    Nothing
-      | value < 0 -> Left (NegativeRadiusSquared value)
-      | otherwise -> Right (RadiusSquared value)
-
--- | The admitted binary64 threshold.
-radiusSquaredValue :: RadiusSquared -> Double
-radiusSquaredValue (RadiusSquared value) = value
-
--- | Total construction and rewrite obstruction surface.
-data BuildError
-  = InvalidCoordinate !(Maybe Int) {-# UNPACK #-} !Double !CoordinateError
-  | PointLocationFailed !(Point)
-  | LocationWalkExhausted !(Point) {-# UNPACK #-} !Int
-  | RefinementInputTopologyInvalid !InvariantViolation
-  | SeamFrontierUnavailable
-  | SeamSourceEdgeRequiresFlip !UndirectedEdgeId
-  | FreshInsertionMatchedExistingVertex !VertexId !VertexId
-  | DegenerateLineEndpointMissingOutgoing !VertexId
-  | DegenerateLineEndpointTurnMissing {-# UNPACK #-} !Int
-  | DegenerateLineConnectedVertexMissing {-# UNPACK #-} !Int
-  | HullStartNotVisible !DirectedEdgeId
-  | OuterRangeDidNotTerminate !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
-  | OuterRangeContainsInnerEdge !DirectedEdgeId !FaceId
-  | ConstrainedEdgeFlipRefused !UndirectedEdgeId
-  | RemovalVertexOutOfRange !VertexId {-# UNPACK #-} !Int
-  | RemovalEdgeOutOfRange !UndirectedEdgeId {-# UNPACK #-} !Int
-  | RemovalFaceOutOfRange !FaceId {-# UNPACK #-} !Int
-  | RemovalFaceCycleDidNotTerminate
-      !FaceId
-      !DirectedEdgeId
-      {-# UNPACK #-} !Int
-  | RemovalEmptyTriangulation !VertexId
-  | RemovalTwoPointDegreeMismatch !VertexId {-# UNPACK #-} !Int
-  | RemovalCollinearDegreeMismatch !VertexId {-# UNPACK #-} !Int
-  | RemovalBorderTooShort {-# UNPACK #-} !Int
-  | RemovalBorderArityMismatch {-# UNPACK #-} !Int
-  | RemovalOutgoingCycleDidNotTerminate
-      !VertexId
-      !DirectedEdgeId
-      {-# UNPACK #-} !Int
-  | CircleSweepRequiresDenseStorage
-  | CircleSweepHullEmpty
-  | OuterCycleDidNotTerminate
-      !DirectedEdgeId
-      !DirectedEdgeId
-      {-# UNPACK #-} !Int
-  | HierarchyLevelPopulationMismatch
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  | HierarchyInsertionHandleMismatch !VertexId !VertexId
-  | PointIndexCapacityExhausted {-# UNPACK #-} !Int
-  | RefinementMinimumAngleNotFinite !NonFiniteValue
-  | RefinementMinimumAngleOutOfRange {-# UNPACK #-} !Double
-  | RefinementMinimumAngleDerivedRatioNotFinite !NonFiniteValue
-  | RefinementMaximumAdditionalVerticesNegative {-# UNPACK #-} !Int
-  | RefinementMinimumAreaNotFinite !NonFiniteValue
-  | RefinementMinimumAreaNegative {-# UNPACK #-} !Double
-  | RefinementMaximumAreaNotFinite !NonFiniteValue
-  | RefinementMaximumAreaNotPositive {-# UNPACK #-} !Double
-  | RefinementMaximumRadiusEdgeRatioNotFinite !NonFiniteValue
-  | RefinementMaximumRadiusEdgeRatioNotPositive {-# UNPACK #-} !Double
-  | RefinementMaximumEdgeLengthNotFinite !NonFiniteValue
-  | RefinementMaximumEdgeLengthNotPositive {-# UNPACK #-} !Double
-  | RefinementMinimumAreaExceedsMaximum
-      {-# UNPACK #-} !Double
-      {-# UNPACK #-} !Double
-  | RefinementSeedFaceNotActive !FaceId {-# UNPACK #-} !Int
-  | RefinementDomainTopologyChanged
-  | RefinementDomainRequiresConvexHullPreservation
-  | RefinementDomainRequiresConstraintPreservation
-  | RefinementDomainForbidsOuterFaceExclusion
-  | RefinementDomainRequiresFiniteVertexBudget
-  | RefinementSeamBridgeBudgetExceeded
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  | RefinementSeamBridgeMidpointCollapsed !UndirectedEdgeId
-  | BoundarySplitRequiresBoundaryEdge !UndirectedEdgeId
-  | RefinementDomainWouldCrossInterface !UndirectedEdgeId !FaceId
-  | RefinementDomainWouldRewriteProtectedFace !FaceId
-  | RefinementDomainProtectedFaceChanged !FaceId
-  | RefinementDomainInterfaceOppositeFaceNotPermitted !UndirectedEdgeId !FaceId
-  | RefinementOversizedEdge
-      !FaceId
-      !UndirectedEdgeId
-      {-# UNPACK #-} !Double
-      {-# UNPACK #-} !Double
-  | CapacityExceeded {-# UNPACK #-} !Int
-  | HalfEdgeCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | FaceCapacityExceeded {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | PayloadStorageFailure !BoxedStorageError
-  | CoordinatePayloadCountMismatch
-      {-# UNPACK #-} !Int
-      {-# UNPACK #-} !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact position of a query relative to the finite triangulation.
-data Location
-  = EmptyTriangulation
-  | OnVertex !VertexId
-  | OnEdge !DirectedEdgeId
-  | InFace !FaceId
-  | OutsideConvexHull !(Maybe DirectedEdgeId)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Optional starting cell for point-location descent.
-data LocationHint
-  = VertexHint !VertexId
-  | FaceHint !FaceId
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Work performed by point-location descent.
-data LocationStats = LocationStats
-  { locationWalkSteps :: {-# UNPACK #-} !Int
-  , locationUsedFallback :: !Bool
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Location telemetry for a query that required no descent.
-emptyLocationStats :: LocationStats
-emptyLocationStats = LocationStats 0 False
-
--- | Work performed by a nearest-neighbor query.
-data NearestStats = NearestStats
-  { nearestWalkSteps :: {-# UNPACK #-} !Int
-  , nearestDistanceTests :: {-# UNPACK #-} !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Independent quality bounds and a finite Steiner-vertex budget.
-data RefinementParameters = RefinementParameters
-  { refineMaxAdditionalVertices :: !(Maybe Int)
-  , refineMinArea :: !(Maybe Double)
-  , refineMaxArea :: !(Maybe Double)
-  , refineMaxRadiusEdgeRatio :: !(Maybe Double)
-  , refineMaxEdgeLength :: !(Maybe Double)
-  , refinePreserveConvexHull :: !Bool
-  , refineKeepConstraintEdges :: !Bool
-  , refineExcludeOuterFaces :: !Bool
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Conservative refinement defaults with no explicit area or edge bounds.
-defaultRefinementParameters :: RefinementParameters
-defaultRefinementParameters =
-  RefinementParameters
-    { refineMaxAdditionalVertices = Nothing
-    , refineMinArea = Nothing
-    , refineMaxArea = Nothing
-    , refineMaxRadiusEdgeRatio = Just 1
-    , refineMaxEdgeLength = Nothing
-    , refinePreserveConvexHull = True
-    , refineKeepConstraintEdges = False
-    , refineExcludeOuterFaces = False
-    }
-
--- | A concrete witness that an immutable DCEL law does not hold.
-data InvariantViolation
-  = CoordinatePlaneLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | VertexOutgoingLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | VertexPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | TopologyArenaLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | DirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | UndirectedPayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | DirectedEdgeCountOdd {-# UNPACK #-} !Int
-  | ConstraintLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | NonCanonicalConstraintFlag !UndirectedEdgeId !Word8
-  | CachedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | CachedConstraintIndexMismatch
-  | MissingOuterFace
-  | FacePayloadLengthMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | EdgeOriginOutOfRange !DirectedEdgeId !VertexId {-# UNPACK #-} !Int
-  | EdgeNextOutOfRange !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
-  | EdgePreviousOutOfRange !DirectedEdgeId !DirectedEdgeId {-# UNPACK #-} !Int
-  | EdgeFaceOutOfRange !DirectedEdgeId !FaceId {-# UNPACK #-} !Int
-  | VertexOutgoingOutOfRange !VertexId !DirectedEdgeId {-# UNPACK #-} !Int
-  | FaceAdjacentOutOfRange !FaceId !DirectedEdgeId {-# UNPACK #-} !Int
-  | EdgeNextPreviousMismatch !DirectedEdgeId !DirectedEdgeId
-  | EdgePreviousNextMismatch !DirectedEdgeId !DirectedEdgeId
-  | EdgeDoubleReversalMismatch !DirectedEdgeId
-  | EdgeSelfLinkedNext !DirectedEdgeId
-  | EdgeSelfLinkedPrevious !DirectedEdgeId
-  | InnerFaceNotTriangularAtEdge !DirectedEdgeId
-  | FaceMissingAdjacentEdge !FaceId
-  | FaceRepresentativeMismatch !FaceId !DirectedEdgeId !FaceId
-  | InnerFaceVertexCardinalityMismatch !FaceId {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | ConnectedVertexMissingOutgoing !VertexId
-  | VertexOutgoingOriginMismatch !VertexId !DirectedEdgeId !VertexId
-  | CollinearEdgeCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | EulerCharacteristicMismatch {-# UNPACK #-} !Int
-  | InnerFaceNotCounterClockwise !FaceId
-  | LocallyIllegalDelaunayEdge !UndirectedEdgeId
-  | DelaunayIncidentFaceNotTriangular !UndirectedEdgeId
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-dcel/Moonlight/Triangulation/Internal/Validation.hs b/src-dcel/Moonlight/Triangulation/Internal/Validation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Internal/Validation.hs
+++ /dev/null
@@ -1,526 +0,0 @@
--- | Discharge: the invariants the constructors guarantee, checkable on a value
--- built by any route.
-module Moonlight.Triangulation.Internal.Validation
-  ( validateTopology
-  , ValidationClosureStats (..)
-  , validateTopologyClosure
-  , validateTopologyClosureWithStats
-  , validateDelaunay
-  , validateTriangulation
-  , triangulationIsValid
-  , faceArea
-  , faceMinimumAngleDegrees
-  ) where
-
-import Data.List (nub)
-import qualified Data.IntSet as IntSet
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedPagedLength)
-import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
-import Moonlight.Triangulation.Internal.Paged (pagedFoldl', pagedLength, pagedUnsafeIndex)
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (allFaces, directedEdges, undirectedEdges, vertices)
-import Moonlight.Triangulation.Internal.PackedIndex (noIndex, unpackIndex)
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Math
-  ( squaredDistance
-  , triangleArea
-  )
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-
--- | Every structural invariant violated, not the first.
-validateTopology :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
-validateTopology triangulation =
-  structuralViolations ++ orientationViolations
- where
-  verticesCount = numVertices triangulation
-  halfCount = numDirectedEdges triangulation
-  edgeCount = numUndirectedEdges triangulation
-  facesCount = numFaces triangulation
-
-  -- Geometry descends only after the finite DCEL has glued structurally.
-  -- Reading triangle coordinates through malformed links would turn a typed
-  -- validation failure into an indexing crash.
-  structuralViolations =
-    cardinalityViolations
-      ++ rangeViolations
-      ++ edgeViolations
-      ++ faceViolations
-      ++ vertexViolations
-      ++ eulerViolations
-
-  orientationViolations
-    | not (null structuralViolations) = []
-    | otherwise =
-        [ InnerFaceNotCounterClockwise face
-        | face <- allFaces triangulation
-        , face /= outerFace
-        , Just (first, second, third) <- [innerFaceVertices triangulation face]
-        , orient2d
-            (vertexPoint triangulation first)
-            (vertexPoint triangulation second)
-            (vertexPoint triangulation third)
-            /= GT
-        ]
-
-  cardinalityViolations =
-    [ CoordinatePlaneLengthMismatch pointXCount pointYCount
-    | pointXCount /= pointYCount
-    ]
-      ++ [VertexOutgoingLengthMismatch vertexOutCount verticesCount | vertexOutCount /= verticesCount]
-      ++ [VertexPayloadLengthMismatch vertexPayloadCount verticesCount | vertexPayloadCount /= verticesCount]
-      ++ [TopologyArenaLengthMismatch topologyLength (4 * halfCount) | not halfArraysEqual]
-      ++ [DirectedPayloadLengthMismatch directedPayloadCount halfCount | directedPayloadCount /= halfCount]
-      ++ [UndirectedPayloadLengthMismatch undirectedPayloadCount edgeCount | undirectedPayloadCount /= edgeCount]
-      ++ [DirectedEdgeCountOdd halfCount | odd halfCount]
-      ++ [ConstraintLengthMismatch constraintLength edgeCount | constraintLength /= edgeCount]
-      ++ [ NonCanonicalConstraintFlag (UndirectedEdgeId (fromIntegral index)) flag
-         | index <- [0 .. pagedLength (triConstraint triangulation) - 1]
-         , let flag = pagedUnsafeIndex (triConstraint triangulation) index
-         , flag /= 0 && flag /= 1
-         ]
-      ++ [CachedConstraintCountMismatch (triConstraintCount triangulation) actualConstraintCount | triConstraintCount triangulation /= actualConstraintCount]
-      ++ [ CachedConstraintIndexMismatch
-         | constraintLength == edgeCount
-         , triConstraintEdges triangulation /= indexedConstraintEdges
-         ]
-      ++ [MissingOuterFace | facesCount == 0]
-      ++ [FacePayloadLengthMismatch facePayloadCount facesCount | facePayloadCount /= facesCount]
-
-  pointXCount = pagedLength (triPointX triangulation)
-  pointYCount = pagedLength (triPointY triangulation)
-  vertexOutCount = pagedLength (triVertexOut triangulation)
-  vertexPayloadCount = boxedPagedLength (triVertexData triangulation)
-  topologyLength = pagedLength (triHalfTopology triangulation)
-  directedPayloadCount = boxedPagedLength (triDirectedData triangulation)
-  undirectedPayloadCount = boxedPagedLength (triUndirectedData triangulation)
-  constraintLength = pagedLength (triConstraint triangulation)
-  facePayloadCount = boxedPagedLength (triFaceData triangulation)
-  actualConstraintCount = pagedFoldl' (\count flag -> if flag == 1 then count + 1 else count) 0 (triConstraint triangulation)
-  halfArraysEqual = topologyLength == 4 * halfCount
-
-  indexedConstraintEdges =
-    IntSet.fromAscList
-      [ index
-      | index <- [0 .. edgeCount - 1]
-      , pagedUnsafeIndex (triConstraint triangulation) index == 1
-      ]
-
-  rangeViolations =
-    [ EdgeOriginOutOfRange (DirectedEdgeId (fromIntegral index)) (VertexId value) verticesCount
-    | index <- [0 .. halfCount - 1]
-    , let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index)
-    , unpackIndex value >= verticesCount
-    ]
-      ++ [ EdgeNextOutOfRange (DirectedEdgeId (fromIntegral index)) (DirectedEdgeId value) halfCount
-         | index <- [0 .. halfCount - 1]
-         , let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 1)
-         , unpackIndex value >= halfCount
-         ]
-      ++ [ EdgePreviousOutOfRange (DirectedEdgeId (fromIntegral index)) (DirectedEdgeId value) halfCount
-         | index <- [0 .. halfCount - 1]
-         , let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 2)
-         , unpackIndex value >= halfCount
-         ]
-      ++ [ EdgeFaceOutOfRange (DirectedEdgeId (fromIntegral index)) (FaceId value) facesCount
-         | index <- [0 .. halfCount - 1]
-         , let value = pagedUnsafeIndex (triHalfTopology triangulation) (4 * index + 3)
-         , unpackIndex value >= facesCount
-         ]
-      ++ [ VertexOutgoingOutOfRange (VertexId (fromIntegral index)) (DirectedEdgeId value) halfCount
-         | index <- [0 .. pagedLength (triVertexOut triangulation) - 1]
-         , let value = pagedUnsafeIndex (triVertexOut triangulation) index
-         , value /= noIndex
-         , unpackIndex value >= halfCount
-         ]
-      ++ [ FaceAdjacentOutOfRange (FaceId (fromIntegral index)) (DirectedEdgeId value) halfCount
-         | index <- [0 .. facesCount - 1]
-         , let value = pagedUnsafeIndex (triFaceEdge triangulation) index
-         , value /= noIndex
-         , unpackIndex value >= halfCount
-         ]
-
-  edgeViolations
-    | not halfArraysEqual || odd halfCount || not (null rangeViolations) = []
-    | otherwise = concatMap validateEdge (directedEdges triangulation)
-
-  validateEdge edge =
-    let index = directedEdgeIdIndex edge
-        nextEdge = next triangulation edge
-        previousEdge = previous triangulation edge
-        twinEdge = reverseEdge edge
-        local =
-          [ EdgeNextPreviousMismatch edge nextEdge
-          | validEdge nextEdge && previous triangulation nextEdge /= edge
-          ]
-            ++ [ EdgePreviousNextMismatch edge previousEdge
-               | validEdge previousEdge && next triangulation previousEdge /= edge
-               ]
-            ++ [EdgeDoubleReversalMismatch edge | reverseEdge twinEdge /= edge]
-            ++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]
-            ++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]
-        innerCycle =
-          if incidentFace triangulation edge /= outerFace && validEdge nextEdge && validEdge previousEdge
-            then
-              [ InnerFaceNotTriangularAtEdge edge
-              | next triangulation (next triangulation nextEdge) /= edge
-              ]
-            else []
-     in if index < halfCount then local ++ innerCycle else []
-
-  validEdge edge = directedEdgeIdIndex edge < halfCount
-
-  faceViolations
-    | not (null rangeViolations) = []
-    | otherwise = concatMap validateFace (allFaces triangulation)
-  validateFace face@(FaceId _) =
-    case adjacentEdge triangulation face of
-      Nothing
-        | face == outerFace && halfCount == 0 -> []
-        | otherwise -> [FaceMissingAdjacentEdge face]
-      Just edge ->
-        [ FaceRepresentativeMismatch face edge representedFace
-        | let representedFace = incidentFace triangulation edge
-        , representedFace /= face
-        ]
-          ++ [ InnerFaceVertexCardinalityMismatch face (length faceVertexIds) (length (nub faceVertexIds))
-             | face /= outerFace
-             , let faceVertexIds = faceVertices triangulation face
-             , length faceVertexIds /= 3 || length (nub faceVertexIds) /= 3
-             ]
-
-  vertexViolations
-    | vertexOutCount /= verticesCount || not (null rangeViolations) = []
-    | otherwise = concatMap validateVertex (vertices triangulation)
-  validateVertex vertex = case vertexOutEdge triangulation vertex of
-    Nothing
-      | verticesCount <= 1 -> []
-      | otherwise -> [ConnectedVertexMissingOutgoing vertex]
-    Just edge ->
-      [ VertexOutgoingOriginMismatch vertex edge actualOrigin
-      | let actualOrigin = origin triangulation edge
-      , actualOrigin /= vertex
-      ]
-
-  eulerViolations
-    | not (null cardinalityViolations) || verticesCount < 2 = []
-    | numInnerFaces triangulation == 0 =
-        [ CollinearEdgeCountMismatch (verticesCount - 1) edgeCount
-        | edgeCount /= verticesCount - 1
-        ]
-    | otherwise =
-        [ EulerCharacteristicMismatch eulerCharacteristic
-        | eulerCharacteristic /= 2
-        ]
-   where
-    eulerCharacteristic = verticesCount - edgeCount + facesCount
-
--- | Apply the same DCEL edge, face, vertex, and orientation laws as
--- 'validateTopology', but only to a certified local closure.  The caller
--- supplies the admitted inner faces and the interface pairs; the collar faces
--- on the other side of those pairs are included here so a protected source
--- cannot be changed behind the local transaction.  Cardinality and Euler
--- observations remain global scalar invariants and are deliberately not
--- rebuilt from the resident mesh.
-validateTopologyClosure
-  :: IntSet.IntSet
-  -> IntSet.IntSet
-  -> Triangulation mode vertex directed undirected face
-  -> [InvariantViolation]
-validateTopologyClosure admittedFaces interfacePairs triangulation =
-  snd (validateTopologyClosureWithStats admittedFaces interfacePairs triangulation)
-
-validateTopologyClosureWithStats
-  :: IntSet.IntSet
-  -> IntSet.IntSet
-  -> Triangulation mode vertex directed undirected face
-  -> (ValidationClosureStats, [InvariantViolation])
-validateTopologyClosureWithStats admittedFaces interfacePairs triangulation =
-  ( ValidationClosureStats
-      { validationClosureFaces = IntSet.size selectedFaces
-      , validationClosureDirectedEdges = IntSet.size selectedEdges
-      , validationClosureVertices = IntSet.size selectedVertices
-      , validationClosureInterfacePairs = IntSet.size interfacePairs
-      , validationClosureConstraintPairs = IntSet.size selectedPairs
-      }
-  , rangeViolations ++ edgeViolations ++ faceViolations ++ vertexViolations ++ orientationViolations ++ constraintViolations
-  )
- where
-  verticesCount = numVertices triangulation
-  halfCount = numDirectedEdges triangulation
-  facesCount = numFaces triangulation
-
-  collarFaces =
-    IntSet.fromList
-      [ rawFace
-      | rawPair <- IntSet.toAscList interfacePairs
-      , rawPair >= 0
-      , rawPair < numUndirectedEdges triangulation
-      , let edge = UndirectedEdgeId (fromIntegral rawPair)
-      , let (forward, backward) = directedPair edge
-      , rawFace <- fmap faceIdIndex [incidentFace triangulation forward, incidentFace triangulation backward]
-      , rawFace > 0
-      ]
-  selectedFaces = IntSet.union admittedFaces collarFaces
-
-  selectedFaceEdges =
-    IntSet.fromList
-      [ rawEdge
-      | rawFace <- IntSet.toAscList selectedFaces
-      , edge <- faceEdgesBounded (FaceId (fromIntegral rawFace))
-      , rawEdge <- [directedEdgeIdIndex edge, directedEdgeIdIndex (reverseEdge edge)]
-      ]
-  selectedInterfaceEdges =
-    IntSet.fromList
-      [ rawEdge
-      | rawPair <- IntSet.toAscList interfacePairs
-      , rawPair >= 0
-      , rawPair <= (maxBound - 1) `quot` 2
-      , rawEdge <- [2 * rawPair, 2 * rawPair + 1]
-      ]
-  selectedEdges = IntSet.union selectedFaceEdges selectedInterfaceEdges
-  selectedVertices =
-    IntSet.fromList
-      [ rawVertex
-      | rawEdge <- IntSet.toAscList selectedEdges
-      , rawEdge >= 0
-      , rawEdge < halfCount
-      , directedEdgeIdIndex (reverseEdge (DirectedEdgeId (fromIntegral rawEdge))) < halfCount
-      , rawVertex <-
-          [ vertexIdIndex (origin triangulation (DirectedEdgeId (fromIntegral rawEdge)))
-          , vertexIdIndex (destination triangulation (DirectedEdgeId (fromIntegral rawEdge)))
-          ]
-      , rawVertex < verticesCount
-      ]
-
-  rangeViolations =
-    [ EdgeOriginOutOfRange edge vertex verticesCount
-    | rawEdge <- IntSet.toAscList selectedEdges
-    , rawEdge >= 0
-    , let edge = DirectedEdgeId (fromIntegral rawEdge)
-    , rawEdge < halfCount
-    , let vertex = origin triangulation edge
-    , vertexIdIndex vertex >= verticesCount
-    ]
-      ++ [ EdgeNextOutOfRange edge nextEdge halfCount
-         | rawEdge <- IntSet.toAscList selectedEdges
-         , rawEdge >= 0
-         , rawEdge < halfCount
-         , let edge = DirectedEdgeId (fromIntegral rawEdge)
-         , let nextEdge = next triangulation edge
-         , directedEdgeIdIndex nextEdge >= halfCount
-         ]
-      ++ [ EdgePreviousOutOfRange edge previousEdge halfCount
-         | rawEdge <- IntSet.toAscList selectedEdges
-         , rawEdge >= 0
-         , rawEdge < halfCount
-         , let edge = DirectedEdgeId (fromIntegral rawEdge)
-         , let previousEdge = previous triangulation edge
-         , directedEdgeIdIndex previousEdge >= halfCount
-         ]
-      ++ [ EdgeFaceOutOfRange edge face facesCount
-         | rawEdge <- IntSet.toAscList selectedEdges
-         , rawEdge >= 0
-         , rawEdge < halfCount
-         , let edge = DirectedEdgeId (fromIntegral rawEdge)
-         , let face = incidentFace triangulation edge
-         , faceIdIndex face >= facesCount
-         ]
-      ++ [ FaceAdjacentOutOfRange face edge halfCount
-         | rawFace <- IntSet.toAscList selectedFaces
-         , rawFace > 0
-         , rawFace < facesCount
-         , let face = FaceId (fromIntegral rawFace)
-         , Just edge <- [adjacentEdge triangulation face]
-         , directedEdgeIdIndex edge >= halfCount
-         ]
-
-  edgeViolations =
-    concatMap validateEdge (IntSet.toAscList selectedEdges)
-
-  validateEdge rawEdge
-    | rawEdge < 0 || rawEdge >= halfCount = []
-    | otherwise =
-        let edge = DirectedEdgeId (fromIntegral rawEdge)
-            nextEdge = next triangulation edge
-            previousEdge = previous triangulation edge
-            twinEdge = reverseEdge edge
-            nextValid = directedEdgeIdIndex nextEdge < halfCount
-            previousValid = directedEdgeIdIndex previousEdge < halfCount
-            nextNextValid = nextValid && directedEdgeIdIndex (next triangulation nextEdge) < halfCount
-            innerCycle =
-              if incidentFace triangulation edge /= outerFace && nextValid && previousValid && nextNextValid
-                then [InnerFaceNotTriangularAtEdge edge | next triangulation (next triangulation nextEdge) /= edge]
-                else []
-         in [ EdgeNextPreviousMismatch edge nextEdge
-            | nextValid && previous triangulation nextEdge /= edge
-            ]
-              ++ [ EdgePreviousNextMismatch edge previousEdge
-                 | previousValid && next triangulation previousEdge /= edge
-                 ]
-              ++ [EdgeDoubleReversalMismatch edge | reverseEdge twinEdge /= edge]
-              ++ [EdgeSelfLinkedNext edge | nextEdge == edge && halfCount > 2]
-              ++ [EdgeSelfLinkedPrevious edge | previousEdge == edge && halfCount > 2]
-              ++ innerCycle
-
-  faceViolations = concatMap validateFace (IntSet.toAscList selectedFaces)
-
-  validateFace rawFace
-    | rawFace <= 0 || rawFace >= facesCount = []
-    | otherwise =
-      let face = FaceId (fromIntegral rawFace)
-       in case adjacentEdge triangulation face of
-          Nothing -> [FaceMissingAdjacentEdge face]
-          Just edge
-            | directedEdgeIdIndex edge >= halfCount -> []
-            | otherwise ->
-                let representedFace = incidentFace triangulation edge
-                    (faceEdges, faceVertices') = triangleEdgesAndVertices face
-                 in [FaceRepresentativeMismatch face edge representedFace | representedFace /= face]
-                      ++ [ InnerFaceVertexCardinalityMismatch face (length faceVertices') (length (nub faceVertices'))
-                         | rawFace > 0
-                         , length faceVertices' /= 3 || length (nub faceVertices') /= 3
-                         ]
-                      ++ [InnerFaceNotTriangularAtEdge edge
-                         | rawFace > 0
-                         , not (triangleClosed faceEdges)
-                         ]
-
-  triangleEdgesAndVertices face =
-    let edges = faceEdgesBounded face
-        vertices' = fmap (vertexIdIndex . origin triangulation) edges
-     in (edges, vertices')
-
-  triangleClosed edges =
-    case edges of
-      [first, _, third] -> next triangulation third == first
-      _ -> False
-
-  vertexViolations = concatMap validateVertex (IntSet.toAscList selectedVertices)
-
-  validateVertex rawVertex =
-    let vertex = VertexId (fromIntegral rawVertex)
-     in case vertexOutEdge triangulation vertex of
-          Nothing
-            | verticesCount > 1 -> [ConnectedVertexMissingOutgoing vertex]
-            | otherwise -> []
-          Just edge
-            | directedEdgeIdIndex edge >= halfCount ->
-                [VertexOutgoingOutOfRange vertex edge halfCount]
-            | otherwise ->
-                [ VertexOutgoingOriginMismatch vertex edge actualOrigin
-                | let actualOrigin = origin triangulation edge
-                , actualOrigin /= vertex
-                ]
-
-  orientationViolations =
-    [ InnerFaceNotCounterClockwise face
-    | rawFace <- IntSet.toAscList selectedFaces
-    , rawFace > 0
-    , let face = FaceId (fromIntegral rawFace)
-    , let (_, vertices') = triangleEdgesAndVertices face
-    , all (< verticesCount) vertices'
-    , [first, second, third] <- [fmap (VertexId . fromIntegral) vertices']
-    , orient2d (vertexPoint triangulation first) (vertexPoint triangulation second) (vertexPoint triangulation third) /= GT
-    ]
-
-  constraintViolations =
-    [ NonCanonicalConstraintFlag edge flag
-    | rawPair <- IntSet.toAscList selectedPairs
-    , rawPair >= 0
-    , let edge = UndirectedEdgeId (fromIntegral rawPair)
-    , rawPair < numUndirectedEdges triangulation
-    , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
-    , flag /= 0 && flag /= 1
-    ]
-      ++ [ CachedConstraintIndexMismatch
-         | rawPair <- IntSet.toAscList selectedPairs
-         , rawPair >= 0
-         , rawPair < numUndirectedEdges triangulation
-         , let flag = pagedUnsafeIndex (triConstraint triangulation) rawPair
-         , (flag == 1) /= IntSet.member rawPair (triConstraintEdges triangulation)
-         ]
-
-  selectedPairs = IntSet.fromList [rawEdge `quot` 2 | rawEdge <- IntSet.toAscList selectedEdges]
-
-  faceEdgesBounded face =
-    case face of
-      FaceId raw
-        | toInteger raw <= 0 || toInteger raw >= toInteger facesCount -> []
-      _ -> adjacentEdges
-   where
-    adjacentEdges = case adjacentEdge triangulation face of
-      Nothing -> []
-      Just start
-        | directedEdgeIdIndex start >= halfCount -> []
-        | otherwise ->
-            let second = next triangulation start
-             in if directedEdgeIdIndex second >= halfCount
-                  then [start]
-                  else
-                    let third = next triangulation second
-                     in if directedEdgeIdIndex third >= halfCount
-                          then [start, second]
-                          else [start, second, third]
-
--- | Every edge whose circumcircle is not empty.
-validateDelaunay :: Triangulation mode vertex directed undirected face -> [InvariantViolation]
-validateDelaunay triangulation = concatMap validateEdge (undirectedEdges triangulation)
- where
-  validateEdge edge
-    | isConstraintEdge triangulation edge = []
-    | isBoundaryEdge triangulation edge = []
-    | otherwise =
-        let directed = normalizedDirected edge
-            twin = reverseEdge directed
-         in case (innerFaceDirectedEdges triangulation (incidentFace triangulation directed), innerFaceDirectedEdges triangulation (incidentFace triangulation twin)) of
-              (Just _, Just _) ->
-                let a = vertexPoint triangulation (origin triangulation directed)
-                    b = vertexPoint triangulation (destination triangulation directed)
-                    c = vertexPoint triangulation (origin triangulation (previous triangulation directed))
-                    d = vertexPoint triangulation (origin triangulation (previous triangulation twin))
-                    convex = orient2d c d b == GT && orient2d d c a == GT
-                    circle = inCircle a b c d
-                    illegal = convex && (circle == GT || (circle == EQ && orderedPair c d < orderedPair a b))
-                 in [LocallyIllegalDelaunayEdge edge | illegal]
-              _ -> [DelaunayIncidentFaceNotTriangular edge]
-
--- | Topology first; the Delaunay property only if the topology holds.
-validateTriangulation
-  :: Triangulation mode vertex directed undirected face
-  -> [InvariantViolation]
-validateTriangulation triangulation =
-  let topology = validateTopology triangulation
-   in if null topology
-        then validateDelaunay triangulation
-        else topology
-
--- | Whether 'validateTriangulation' is empty.
-triangulationIsValid
-  :: Triangulation mode vertex directed undirected face
-  -> Bool
-triangulationIsValid = null . validateTriangulation
-
--- | Signed area, or 'Nothing' where the face is not a triangle.
-faceArea :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
-faceArea triangulation face = do
-  (v0, v1, v2) <- innerFaceVertices triangulation face
-  pure (triangleArea (vertexPoint triangulation v0) (vertexPoint triangulation v1) (vertexPoint triangulation v2))
-
--- | Smallest interior angle, in degrees.
-faceMinimumAngleDegrees :: Triangulation mode vertex directed undirected face -> FaceId -> Maybe Double
-faceMinimumAngleDegrees triangulation face = do
-  (v0, v1, v2) <- innerFaceVertices triangulation face
-  let p0 = vertexPoint triangulation v0
-      p1 = vertexPoint triangulation v1
-      p2 = vertexPoint triangulation v2
-      a = sqrt (squaredDistance p1 p2)
-      b = sqrt (squaredDistance p2 p0)
-      c = sqrt (squaredDistance p0 p1)
-  if min a (min b c) <= 0
-    then Nothing
-    else Just (minimum [angle b c a, angle c a b, angle a b c])
- where
-  angle left right opposite = acos (clamp ((left * left + right * right - opposite * opposite) / (2 * left * right))) * 180 / pi
-  clamp = max (-1) . min 1
diff --git a/src-dcel/Moonlight/Triangulation/Interop.hs b/src-dcel/Moonlight/Triangulation/Interop.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Interop.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-{-# LANGUAGE FlexibleInstances #-}
-
--- | Boundary conversions between Moonlight points and common coordinate pairs.
-module Moonlight.Triangulation.Interop
-  ( Coordinate2 (..)
-  , mapCoordinate2
-  ) where
-
-import Data.Complex (Complex ((:+)))
-import Moonlight.Triangulation.Types (Point (..))
-
--- | Minimal mint-like interchange class. It owns no geometry and introduces no
--- second point representation inside the triangulation; conversion happens only
--- at an ecosystem boundary.
-class Coordinate2 value where
-  toPoint :: value -> Point
-  fromPoint :: Point -> value
-
-instance Coordinate2 (Point) where
-  toPoint = id
-  fromPoint = id
-
-instance Coordinate2 (Double, Double) where
-  toPoint (x, y) = Point x y
-  fromPoint (Point x y) = (x, y)
-
-instance Coordinate2 (Complex Double) where
-  toPoint (x :+ y) = Point x y
-  fromPoint (Point x y) = x :+ y
-
--- | Transform a coordinate through the canonical 'Point' representation.
-mapCoordinate2
-  :: (Coordinate2 input, Coordinate2 output)
-  => (Point -> Point)
-  -> input
-  -> output
-mapCoordinate2 transform = fromPoint . transform . toPoint
diff --git a/src-dcel/Moonlight/Triangulation/IntersectionIterator.hs b/src-dcel/Moonlight/Triangulation/IntersectionIterator.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/IntersectionIterator.hs
+++ /dev/null
@@ -1,465 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Ordered traversal of the mesh features met by a line segment.
-module Moonlight.Triangulation.IntersectionIterator
-  ( Intersection (..)
-  , lineIntersections
-  , lineIntersectionsBetweenVertices
-  , foldCorridorBetweenPoints
-  , foldCorridorBetweenVertices
-  , conflictingEdges
-  , segmentIntersectsNonCollinear
-  ) where
-
-import Data.List (sortBy)
-import Data.Maybe (mapMaybe)
-import Data.Ord (comparing)
-import Data.Void (Void, absurd)
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (undirectedEdges, vertices)
-import Moonlight.Triangulation.Math
-  ( projectionFactor
-  , squaredDistanceWide
-  )
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Types
-
--- | One crossing: an edge cut, a vertex hit, or a collinear overlap.
-data Intersection
-  = EdgeIntersection !DirectedEdgeId
-  | VertexIntersection !VertexId
-  | EdgeOverlap !DirectedEdgeId
-  deriving stock (Eq, Ord, Show)
-
--- | Every crossing between two points, ordered along the segment.
-lineIntersections
-  :: Triangulation mode vertex directed undirected face
-  -> QueryPoint
-  -> QueryPoint
-  -> [Intersection]
-lineIntersections triangulation queryFrom queryTo =
-  let !from = queryPointValue queryFrom
-      !to = queryPointValue queryTo
-   in if endpointNearerToWalkSeed triangulation to from
-        then case firstIntersection triangulation queryTo queryFrom of
-          Nothing -> []
-          Just first -> walkIntersections AgainstQuery triangulation to from first
-        else case firstIntersection triangulation queryFrom queryTo of
-          Nothing -> []
-          Just first -> walkIntersections AlongQuery triangulation from to first
-
--- | 'lineIntersections' between two existing vertices.
-lineIntersectionsBetweenVertices :: Triangulation mode vertex directed undirected face -> VertexId -> VertexId -> [Intersection]
-lineIntersectionsBetweenVertices triangulation fromVertex toVertex =
-  let from = vertexPoint triangulation fromVertex
-      to = vertexPoint triangulation toVertex
-   in walkIntersections AlongQuery triangulation from to (VertexIntersection fromVertex)
-
--- | The directed edges a crossing list cuts.
-conflictingEdges :: [Intersection] -> [DirectedEdgeId]
-conflictingEdges = mapMaybe asConflict
- where
-  asConflict (EdgeIntersection edge) = Just edge
-  asConflict _ = Nothing
-
--- | Fold the corridor one crossing at a time, stopping the instant the step
--- function answers.
---
--- The list-producing walks cannot stop early: their step budget is only known
--- to have been respected once the walk ends, so the whole corridor is
--- materialized before the first event is visible. A caller whose answer is
--- settled by a prefix — anything asking whether some crossing exists — should
--- not pay for the suffix. 'Nothing' reports a walk that outran its budget and
--- is the caller's signal to fall back to the materialized walk, which
--- substitutes the exact scan.
-foldCorridorBetweenVertices
-  :: Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> VertexId
-  -> (state -> Intersection -> Either answer state)
-  -> state
-  -> Maybe (Either answer state)
-foldCorridorBetweenVertices triangulation fromVertex toVertex =
-  foldCorridor
-    triangulation
-    (vertexPoint triangulation fromVertex)
-    (vertexPoint triangulation toVertex)
-    (VertexIntersection fromVertex)
-
--- | As 'foldCorridorBetweenVertices', for a corridor given by its endpoints.
--- A segment that meets nothing at all folds to the initial state.
-foldCorridorBetweenPoints
-  :: Triangulation mode vertex directed undirected face
-  -> QueryPoint
-  -> QueryPoint
-  -> (state -> Intersection -> Either answer state)
-  -> state
-  -> Maybe (Either answer state)
-foldCorridorBetweenPoints triangulation queryFrom queryTo step state =
-  case firstIntersection triangulation queryFrom queryTo of
-    Nothing -> Just (Right state)
-    Just first ->
-      foldCorridor
-        triangulation
-        (queryPointValue queryFrom)
-        (queryPointValue queryTo)
-        first
-        step
-        state
-
-foldCorridor
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> Intersection
-  -> (state -> Intersection -> Either answer state)
-  -> state
-  -> Maybe (Either answer state)
-foldCorridor triangulation from to first step =
-  go (2 * numDirectedEdges triangulation + numVertices triangulation + 8) first
- where
-  go !remaining !current !state
-    | remaining <= 0 = Nothing
-    | otherwise = case step state current of
-        Left answer -> Just (Left answer)
-        Right advanced ->
-          case nextIntersection triangulation from to current of
-            Nothing -> Just (Right advanced)
-            Just following -> go (remaining - 1) following advanced
--- The early-answer 'Either' exists to let a caller stop at the first event it
--- cares about; it should not survive to runtime. Inlining the non-recursive
--- wrapper puts the worker at each call site with 'step' statically known, so
--- the constructor is matched where it is built — and at 'walkIntersections',
--- where the answer is 'Void', the left branch is erased outright.
-{-# INLINE foldCorridor #-}
-
-data QueryDirection = AlongQuery | AgainstQuery
-
-walkIntersections
-  :: QueryDirection
-  -> Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> Intersection
-  -> [Intersection]
-walkIntersections direction triangulation from to first =
-  case foldCorridor triangulation from to first collect [] of
-    Nothing -> orientExact (exactIntersectionScan triangulation from to)
-    Just (Left impossible) -> absurd impossible
-    Just (Right events) -> orientCollected events
- where
-  collect :: [Intersection] -> Intersection -> Either Void [Intersection]
-  collect accumulated event = Right (event : accumulated)
-  orientExact = case direction of
-    AlongQuery -> id
-    AgainstQuery -> map reverseIntersection . reverse
-  orientCollected = case direction of
-    AlongQuery -> reverse
-    AgainstQuery -> map reverseIntersection
-
-reverseIntersection :: Intersection -> Intersection
-reverseIntersection event = case event of
-  VertexIntersection vertex -> VertexIntersection vertex
-  EdgeIntersection edge -> EdgeIntersection (reverseEdge edge)
-  EdgeOverlap edge -> EdgeOverlap (reverseEdge edge)
-
--- The unhinted point locator starts at face one. Choosing the segment endpoint
--- decisively nearer one of that face's vertices shortens the same exact walk
--- without making ordinary interior chords pay to reverse similar paths.
-endpointNearerToWalkSeed
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> Bool
-endpointNearerToWalkSeed triangulation candidate incumbent =
-  case innerFaceVertices triangulation (FaceId 1) of
-    Just (anchor, _, _) ->
-      let !seed = vertexPoint triangulation anchor
-       in 4 * squaredDistanceWide seed candidate < squaredDistanceWide seed incumbent
-    Nothing -> False
-
-nextIntersection :: Triangulation mode vertex directed undirected face -> Point -> Point -> Intersection -> Maybe Intersection
-nextIntersection triangulation lineFrom lineTo current = case current of
-  EdgeIntersection edge -> case traceDirectionOutOfEdge triangulation edge lineFrom lineTo of
-    EdgeOutHull -> Nothing
-    EdgeOutVertex vertex -> Just (VertexIntersection vertex)
-    EdgeOutEdge nextEdge -> Just (EdgeIntersection nextEdge)
-    EdgeOutNone -> Nothing
-  VertexIntersection vertex
-    | vertexPoint triangulation vertex == lineTo -> Nothing
-    | otherwise -> case traceDirectionOutOfVertex triangulation vertex lineTo of
-        VertexOutHull -> Nothing
-        VertexOutOverlap edge -> Just (EdgeOverlap edge)
-        VertexOutEdge edge ->
-          let from = vertexPoint triangulation (origin triangulation edge)
-              to = vertexPoint triangulation (destination triangulation edge)
-           in if orient2d from to lineTo == LT then Nothing else Just (EdgeIntersection edge)
-  EdgeOverlap edge
-    | lineFrom == lineTo -> Nothing
-    | onClosedSegment lineFrom lineTo (vertexPoint triangulation (destination triangulation edge)) ->
-        Just (VertexIntersection (destination triangulation edge))
-    | otherwise -> Nothing
-
-firstIntersection :: Triangulation mode vertex directed undirected face -> QueryPoint -> QueryPoint -> Maybe Intersection
-firstIntersection triangulation queryFrom queryTo =
-  case locatePoint triangulation queryFrom of
-    EmptyTriangulation -> singleVertexHit
-    OnVertex vertex -> Just (VertexIntersection vertex)
-    OnEdge edge -> Just (classifyStartingEdge edge)
-    InFace face -> firstFromFace face
-    OutsideConvexHull entry -> firstFromOutside entry
- where
-  !lineFrom = queryPointValue queryFrom
-  !lineTo = queryPointValue queryTo
-  singleVertexHit = case vertices triangulation of
-    [vertex]
-      | onClosedSegment lineFrom lineTo (vertexPoint triangulation vertex) -> Just (VertexIntersection vertex)
-    _ -> Nothing
-
-  classifyStartingEdge edge =
-    let a = vertexPoint triangulation (origin triangulation edge)
-        b = vertexPoint triangulation (destination triangulation edge)
-     in if orient2d lineFrom lineTo a == EQ && orient2d lineFrom lineTo b == EQ
-          then EdgeOverlap (orientAlongLine edge)
-          else EdgeIntersection (orientTowardTarget edge)
-
-  firstFromFace face = firstEdgeFromRing (faceDirectedEdges triangulation face)
-
-  firstEdgeFromRing [] = Nothing
-  firstEdgeFromRing (edge : remaining) =
-    let a = vertexPoint triangulation (origin triangulation edge)
-        b = vertexPoint triangulation (destination triangulation edge)
-     in if segmentIntersectsNonCollinear lineFrom lineTo a b
-          then
-            if orient2d lineFrom lineTo a == EQ
-              then Just (VertexIntersection (origin triangulation edge))
-              else
-                if orient2d lineFrom lineTo b == EQ
-                  then Just (VertexIntersection (destination triangulation edge))
-                  else Just (EdgeIntersection (reverseEdge edge))
-          else firstEdgeFromRing remaining
-
-  -- Outside the region the segment's first contact with it lies on the ring
-  -- the locator's edge sits on, so only that ring can carry the earliest
-  -- event. A ring that outran its budget, an absent locator edge, and
-  -- endpoints that leave the parameter comparison without a total order all
-  -- keep the exact scan.
-  firstFromOutside (Just edge) =
-    case hullEntryIntersection triangulation lineFrom lineTo edge of
-      Just entry -> entry
-      Nothing -> firstFromScan
-  firstFromOutside _ = firstFromScan
-
-  firstFromScan = case exactIntersectionScan triangulation lineFrom lineTo of
-    event : _ -> Just event
-    [] -> Nothing
-
-  orientAlongLine edge =
-    let a = vertexPoint triangulation (origin triangulation edge)
-        b = vertexPoint triangulation (destination triangulation edge)
-     in if projectionFactor lineFrom lineTo a <= projectionFactor lineFrom lineTo b then edge else reverseEdge edge
-
-  orientTowardTarget edge =
-    let a = vertexPoint triangulation (origin triangulation edge)
-        b = vertexPoint triangulation (destination triangulation edge)
-     in if orient2d a b lineTo == LT then reverseEdge edge else edge
-
-data VertexOut
-  = VertexOutHull
-  | VertexOutOverlap !DirectedEdgeId
-  | VertexOutEdge !DirectedEdgeId
-
-data EdgeOut
-  = EdgeOutHull
-  | EdgeOutVertex !VertexId
-  | EdgeOutEdge !DirectedEdgeId
-  | EdgeOutNone
-
-traceDirectionOutOfVertex :: Triangulation mode vertex directed undirected face -> VertexId -> Point -> VertexOut
-traceDirectionOutOfVertex triangulation vertex target =
-  case vertexOutEdge triangulation vertex of
-    Nothing -> VertexOutHull
-    Just start ->
-      let !startSide = sideOf start
-          !rotateCounterClockwise = startSide == GT
-       in go rotateCounterClockwise (numDirectedEdges triangulation + 1) start startSide
- where
-  go !rotateCounterClockwise !remaining !current !currentSide
-    | remaining <= 0 = VertexOutHull
-    | currentSide == EQ && projectionFactor currentPoint (edgeTarget current) target >= 0 =
-        VertexOutOverlap current
-    | otherwise =
-        let following = if rotateCounterClockwise then counterClockwise triangulation current else clockwise triangulation current
-            followingSide = sideOf following
-         in if followingSide == EQ && projectionFactor currentPoint (edgeTarget following) target >= 0
-              then VertexOutOverlap following
-              else
-                let faceBetween = if rotateCounterClockwise then incidentFace triangulation current else incidentFace triangulation following
-                 in if faceBetween == outerFace
-                      then VertexOutHull
-                      else
-                        if rotateCounterClockwise == (followingSide == LT)
-                          then
-                            let segment = if rotateCounterClockwise then next triangulation current else previous triangulation (reverseEdge current)
-                             in VertexOutEdge (reverseEdge segment)
-                          else go rotateCounterClockwise (remaining - 1) following followingSide
-
-  currentPoint = vertexPoint triangulation vertex
-  edgeTarget edge = vertexPoint triangulation (destination triangulation edge)
-  sideOf edge = orient2d currentPoint (edgeTarget edge) target
-
-traceDirectionOutOfEdge :: Triangulation mode vertex directed undirected face -> DirectedEdgeId -> Point -> Point -> EdgeOut
-traceDirectionOutOfEdge triangulation edge lineFrom lineTo
-  | incidentFace triangulation edge == outerFace = EdgeOutHull
-  | otherwise =
-      case (previousIntersects, nextIntersects) of
-        (True, False) -> EdgeOutEdge (reverseEdge edgePrevious)
-        (False, True) -> EdgeOutEdge (reverseEdge edgeNext)
-        (True, True) -> EdgeOutVertex (origin triangulation edgePrevious)
-        (False, False) -> EdgeOutNone
- where
-  edgePrevious = previous triangulation edge
-  edgeNext = next triangulation edge
-
-  -- The face runs @edge@ A->B, @edgeNext@ B->C, @edgePrevious@ C->A, so the
-  -- two candidates share C and each vertex's side of the line is read once
-  -- rather than once per candidate. Whether the segment reaches a candidate is
-  -- then asked only of one the line already separates, and a walk that entered
-  -- across A->B leaves that true for exactly one of the two.
-  pointA = vertexPoint triangulation (origin triangulation edge)
-  pointB = vertexPoint triangulation (origin triangulation edgeNext)
-  pointC = vertexPoint triangulation (origin triangulation edgePrevious)
-
-  sideA = orient2d lineFrom lineTo pointA
-  sideB = orient2d lineFrom lineTo pointB
-  sideC = orient2d lineFrom lineTo pointC
-
-  previousIntersects = sideC /= sideA && reaches pointC pointA
-  nextIntersects = sideB /= sideC && reaches pointB pointC
-
-  reaches from to = orient2d from to lineFrom /= orient2d from to lineTo
-
--- | Whether two segments properly cross; collinear touching does not.
-segmentIntersectsNonCollinear :: Point -> Point -> Point -> Point -> Bool
-segmentIntersectsNonCollinear p0 p1 q0 q1 =
-  -- Equality admits an endpoint on the opposite segment; four equal sides
-  -- reject the collinear case without recomputing either orientation pair.
-  p0Side /= p1Side
-    && q0Side /= q1Side
- where
-  !p0Side = orient2d q0 q1 p0
-  !p1Side = orient2d q0 q1 p1
-  !q0Side = orient2d p0 p1 q0
-  !q1Side = orient2d p0 p1 q1
-{-# INLINE segmentIntersectsNonCollinear #-}
-
-exactIntersectionScan :: Triangulation mode vertex directed undirected face -> Point -> Point -> [Intersection]
-exactIntersectionScan triangulation lineFrom lineTo =
-  map eventValue . sortBy compareEvent $ vertexEvents ++ edgeEvents
- where
-  vertexEvents = mapMaybe (vertexEvent triangulation lineFrom lineTo) (vertices triangulation)
-  edgeEvents = mapMaybe (edgeEvent triangulation lineFrom lineTo) (undirectedEdges triangulation)
-
-data Event = Event
-  { eventParameter :: !Double
-  , eventPriority :: {-# UNPACK #-} !Int
-  , eventValue :: !Intersection
-  }
-
-compareEvent :: Event -> Event -> Ordering
-compareEvent = comparing (\event -> (eventParameter event, eventPriority event, eventValue event))
-
--- Ties in this order are the same 'Intersection', so a traversal that reaches
--- the minimum by a different route still reports the value the sorted scan's
--- head reports.
-vertexEvent
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> VertexId
-  -> Maybe (Event)
-vertexEvent triangulation lineFrom lineTo vertex
-  | onClosedSegment lineFrom lineTo point = Just (Event (projectionFactor lineFrom lineTo point) 0 (VertexIntersection vertex))
-  | otherwise = Nothing
- where
-  point = vertexPoint triangulation vertex
-
-edgeEvent
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> UndirectedEdgeId
-  -> Maybe (Event)
-edgeEvent triangulation lineFrom lineTo edge
-  | oa == EQ && ob == EQ =
-      if low < high
-        then Just (Event low 1 (EdgeOverlap (if projectedA <= projectedB then directed else reverseEdge directed)))
-        else Nothing
-  | segmentsIntersect lineFrom lineTo a b && oa /= EQ && ob /= EQ =
-      Just (Event (segmentIntersectionParameter lineFrom lineTo a b) 2 (EdgeIntersection oriented))
-  | otherwise = Nothing
- where
-  directed = normalizedDirected edge
-  a = vertexPoint triangulation (origin triangulation directed)
-  b = vertexPoint triangulation (destination triangulation directed)
-  oa = orient2d lineFrom lineTo a
-  ob = orient2d lineFrom lineTo b
-  projectedA = projectionFactor lineFrom lineTo a
-  projectedB = projectionFactor lineFrom lineTo b
-  low = max 0 (min projectedA projectedB)
-  high = min 1 (max projectedA projectedB)
-  oriented = if orient2d a b lineTo == LT then reverseEdge directed else directed
-
--- | Restrict an outside segment to the hull edge supplied by point location,
--- then follow only the side on which its entry can lie. The outer 'Maybe'
--- witnesses a hull walk that closed within its topology budget; the inner one
--- is the geometric answer. An exhausted or impossible local section descends
--- to the exact scan owned by 'firstIntersection'.
-hullEntryIntersection
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> Point
-  -> DirectedEdgeId
-  -> Maybe (Maybe Intersection)
-hullEntryIntersection triangulation lineFrom lineTo entry
-  | orient2d entryFrom entryTo lineFrom == EQ =
-      Just
-        ( Just
-            ( VertexIntersection
-                ( if squaredDistanceWide entryTo lineFrom < squaredDistanceWide entryFrom lineFrom
-                    then destination triangulation entry
-                    else origin triangulation entry
-                )
-            )
-        )
-  | otherwise = go (numDirectedEdges triangulation + 1) entry
- where
-  entryFrom = vertexPoint triangulation (origin triangulation entry)
-  entryTo = vertexPoint triangulation (destination triangulation entry)
-
-  go !remaining !edge
-    | remaining <= 0 = Nothing
-    | otherwise =
-        let !edgeFrom = vertexPoint triangulation (origin triangulation edge)
-            !edgeTo = vertexPoint triangulation (destination triangulation edge)
-            !fromSide = orient2d lineFrom lineTo edgeFrom
-            !toSide = orient2d lineFrom lineTo edgeTo
-         in if orient2d edgeFrom edgeTo lineTo == GT
-              then Just Nothing
-              else case (fromSide == GT, toSide /= LT) of
-                (True, True) -> go (remaining - 1) (previous triangulation edge)
-                (False, False) -> go (remaining - 1) (next triangulation edge)
-                (False, True)
-                  | toSide == EQ -> Just (Just (VertexIntersection (destination triangulation edge)))
-                  | fromSide == EQ -> Just (Just (VertexIntersection (origin triangulation edge)))
-                  | otherwise -> Just (Just (EdgeIntersection (reverseEdge edge)))
-                (True, False) -> Nothing
-
-segmentIntersectionParameter :: Point -> Point -> Point -> Point -> Double
-segmentIntersectionParameter (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy)
-  | denominator == 0 = 0
-  | otherwise = ((cx - ax) * (dy - cy) - (cy - ay) * (dx - cx)) / denominator
- where
-  denominator = (bx - ax) * (dy - cy) - (by - ay) * (dx - cx)
diff --git a/src-dcel/Moonlight/Triangulation/JoinSemilattice.hs b/src-dcel/Moonlight/Triangulation/JoinSemilattice.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/JoinSemilattice.hs
+++ /dev/null
@@ -1,20 +0,0 @@
--- | Payloads that can be glued coordinate-wise when triangulation site sets
--- overlap. The laws are the contract: implementations must be commutative,
--- associative, and idempotent.
-module Moonlight.Triangulation.JoinSemilattice
-  ( JoinSemilattice (..)
-  ) where
-
--- | A join-semilattice carried by vertex annotations.
---
--- @
--- joinAnnotations left right == joinAnnotations right left
--- joinAnnotations (joinAnnotations a b) c == joinAnnotations a (joinAnnotations b c)
--- joinAnnotations value value == value
--- @
-class Eq annotation => JoinSemilattice annotation where
-  joinAnnotations :: annotation -> annotation -> annotation
-
-instance JoinSemilattice () where
-  joinAnnotations _ _ = ()
-  {-# INLINE joinAnnotations #-}
diff --git a/src-dcel/Moonlight/Triangulation/Math.hs b/src-dcel/Moonlight/Triangulation/Math.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Math.hs
+++ /dev/null
@@ -1,446 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Robust planar predicates and derived Euclidean constructions.
-module Moonlight.Triangulation.Math
-  ( orient2d
-  , sideQuery
-  , inCircle
-  , orientDetApprox
-  , inCircleDetApprox
-  , onClosedSegment
-  , SegmentRelation (..)
-  , allSegmentRelations
-  , segmentRelation
-  , segmentsProperlyCross
-  , segmentsIntersect
-  , squaredDistance
-  , squaredDistanceWide
-  , segmentDistanceSquared
-  , segmentDistanceSquaredWide
-  , distance
-  , negateVector
-  , normalizeVector
-  , midpoint
-  , centroid
-  , triangleArea
-  , triangleRadiusEdgeRatio
-  , triangleRadiusEdgeRatioSquaredWithArea
-  , triangleRadiusEdgeRatioSquaredWithAreaAndSides
-  , circumcenter
-  , barycentricCoordinates
-  , inDiametralCircle
-  , projectionFactor
-  , canonicalPoint
-  , canonicalCoordinate
-  , validateCoordinate
-  , mkQueryPoint
-  , validatePoint
-  , mitigateUnderflow
-  , isFinite
-  ) where
-
-import Moonlight.Triangulation.Internal.Dyadic
-  ( exactBarycentricDeterminants
-  , integerRatioToDouble
-  )
-import Moonlight.Triangulation.Internal.Predicates qualified as Admitted
-import Moonlight.Triangulation.Internal.SegmentRelation
-  ( SegmentRelation (..)
-  , allSegmentRelations
-  )
-import Moonlight.Triangulation.LineSideInfo (LineSideInfo)
-import Moonlight.Triangulation.Scalar
-  ( canonicalScalarZero
-  , isFinite
-  , maximumAllowedCoordinate
-  , minimumAllowedCoordinate
-  , scalarCcwErrorBound
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( BuildError (..)
-  , CoordinateError (..)
-  , Point (..)
-  , PointValidationError (..)
-  , QueryPoint (..)
-  )
-
--- | Exact relation between two admitted closed segments.
-segmentRelation
-  :: QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> SegmentRelation
-segmentRelation firstFrom firstTo secondFrom secondTo =
-  Admitted.segmentRelation
-    (queryPointValue firstFrom)
-    (queryPointValue firstTo)
-    (queryPointValue secondFrom)
-    (queryPointValue secondTo)
-
--- | Whether two admitted closed segments share any point.
-segmentsIntersect
-  :: QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> Bool
-segmentsIntersect firstFrom firstTo secondFrom secondTo =
-  Admitted.segmentsIntersect
-    (queryPointValue firstFrom)
-    (queryPointValue firstTo)
-    (queryPointValue secondFrom)
-    (queryPointValue secondTo)
-
--- | The proper-crossing section of 'segmentRelation'. Consumers which reject
--- only that constructor need not compute the collinear and endpoint-touch
--- distinctions required by the complete ADT after either side already proves
--- separation.
-segmentsProperlyCross
-  :: QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> QueryPoint
-  -> Bool
-segmentsProperlyCross firstFrom firstTo secondFrom secondTo =
-  Admitted.segmentsProperlyCross
-    (queryPointValue firstFrom)
-    (queryPointValue firstTo)
-    (queryPointValue secondFrom)
-    (queryPointValue secondTo)
-
--- | Classify a coordinate outside the exact-predicate input domain.
-validateCoordinate :: Double -> Maybe CoordinateError
-validateCoordinate value
-  | isNaN value = Just CoordinateNaN
-  | isInfinite value = Just CoordinateInfinite
-  | value /= 0 && abs value < minimumAllowedCoordinate = Just CoordinateTooSmall
-  | abs value > maximumAllowedCoordinate = Just CoordinateTooLarge
-  | otherwise = Nothing
-
--- | Admit and normalize a finite point for read-only geometric queries.
-mkQueryPoint :: Point -> Either PointValidationError (QueryPoint)
-mkQueryPoint point@(Point x y) = do
-  maybe (Right ()) (Left . InvalidPointX) (validateCoordinate x)
-  maybe (Right ()) (Left . InvalidPointY) (validateCoordinate y)
-  Right (QueryPoint (canonicalPoint point))
-
--- | Validate a construction point while retaining its optional input slot.
-validatePoint :: Maybe Int -> Point -> Either BuildError (QueryPoint)
-validatePoint slot point@(Point x y) =
-  case mkQueryPoint point of
-    Left (InvalidPointX reason) -> Left (InvalidCoordinate slot x reason)
-    Left (InvalidPointY reason) -> Left (InvalidCoordinate slot y reason)
-    Right queryPoint -> Right queryPoint
-
--- | Round coordinates below the robust-predicate input floor toward zero.
--- The operation never changes a coordinate already accepted by
--- 'validateCoordinate'.
-mitigateUnderflow :: Point -> Point
-mitigateUnderflow (Point x y) = Point (mitigate x) (mitigate y)
- where
-  mitigate :: Double -> Double
-  mitigate value
-    | value /= 0 && abs value < minimumAllowedCoordinate = 0
-    | otherwise = value
-
--- | Canonicalize both coordinate components for point identity.
-canonicalPoint :: Point -> Point
-canonicalPoint (Point x y) = Point (canonicalCoordinate x) (canonicalCoordinate y)
-{-# INLINE canonicalPoint #-}
-
--- | Round a signed zero to the canonical zero. The law that makes two points
--- at the same position compare equal lives here; @canonicalPoint@ is its
--- component-wise form and coordinate-carrying callers use it directly so no
--- t'Point' is built only to be taken apart again.
-canonicalCoordinate :: Double -> Double
-canonicalCoordinate = canonicalScalarZero
-{-# INLINE canonicalCoordinate #-}
-
--- | Fast approximate signed orientation determinant.
-orientDetApprox :: Point -> Point -> Point -> Double
-orientDetApprox (Point ax ay) (Point bx by) (Point cx cy) =
-  (ax - cx) * (by - cy) - (ay - cy) * (bx - cx)
-{-# INLINE orientDetApprox #-}
-
--- | Exact orientation ordering of three admitted points.
-orient2d :: QueryPoint -> QueryPoint -> QueryPoint -> Ordering
-orient2d first second third =
-  Admitted.orient2d
-    (queryPointValue first)
-    (queryPointValue second)
-    (queryPointValue third)
-{-# INLINE orient2d #-}
-
--- | Exact side of an admitted oriented line.
-sideQuery :: QueryPoint -> QueryPoint -> QueryPoint -> LineSideInfo
-sideQuery from to query =
-  Admitted.sideQuery
-    (queryPointValue from)
-    (queryPointValue to)
-    (queryPointValue query)
-{-# INLINE sideQuery #-}
-
--- | Fast approximate oriented in-circle determinant.
-inCircleDetApprox
-  :: Point -> Point -> Point -> Point -> Double
-inCircleDetApprox
-  (Point ax ay)
-  (Point bx by)
-  (Point cx cy)
-  (Point dx dy) =
-    alift * bcdet + blift * cadet + clift * abdet
- where
-  !adx = ax - dx
-  !ady = ay - dy
-  !bdx = bx - dx
-  !bdy = by - dy
-  !cdx = cx - dx
-  !cdy = cy - dy
-  !abdet = adx * bdy - bdx * ady
-  !bcdet = bdx * cdy - cdx * bdy
-  !cadet = cdx * ady - adx * cdy
-  !alift = adx * adx + ady * ady
-  !blift = bdx * bdx + bdy * bdy
-  !clift = cdx * cdx + cdy * cdy
-{-# INLINE inCircleDetApprox #-}
-
--- | Ordering of the admitted oriented incircle determinant. For a
--- counter-clockwise triangle, 'GT' means the fourth point lies strictly inside
--- its circumcircle.
-inCircle
-  :: QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering
-inCircle first second third query =
-  Admitted.inCircle
-    (queryPointValue first)
-    (queryPointValue second)
-    (queryPointValue third)
-    (queryPointValue query)
-{-# INLINE inCircle #-}
-
--- | Whether an admitted point lies on an admitted closed segment.
-onClosedSegment :: QueryPoint -> QueryPoint -> QueryPoint -> Bool
-onClosedSegment from to query =
-  Admitted.onClosedSegment
-    (queryPointValue from)
-    (queryPointValue to)
-    (queryPointValue query)
-{-# INLINE onClosedSegment #-}
-
--- | Squared Euclidean distance.
-squaredDistance :: Point -> Point -> Double
-squaredDistance (Point ax ay) (Point bx by) =
-  let !dx = ax - bx
-      !dy = ay - by
-   in dx * dx + dy * dy
-{-# INLINE squaredDistance #-}
-
--- | Squared Euclidean distance in the mesh's Binary64 coordinate domain.
-squaredDistanceWide :: Point -> Point -> Double
-squaredDistanceWide = squaredDistance
-{-# INLINE squaredDistanceWide #-}
-
--- | Squared distance from a point to a closed segment.
-segmentDistanceSquared
-  :: Point -> Point -> Point -> Double
-segmentDistanceSquared from@(Point ax ay) to@(Point bx by) point@(Point px py)
-  | lengthSquared == 0 = squaredDistance from point
-  | factor <= 0 = squaredDistance from point
-  | factor >= 1 = squaredDistance to point
-  | otherwise = squaredDistance point (Point (ax + factor * dx) (ay + factor * dy))
- where
-  !dx = bx - ax
-  !dy = by - ay
-  !lengthSquared = dx * dx + dy * dy
-  !factor = ((px - ax) * dx + (py - ay) * dy) / lengthSquared
-{-# INLINE segmentDistanceSquared #-}
-
--- | Comparison form retained beside 'segmentDistanceSquared' for callers that
--- state metric intent explicitly.
-segmentDistanceSquaredWide
-  :: Point -> Point -> Point -> Double
-segmentDistanceSquaredWide = segmentDistanceSquared
-{-# INLINE segmentDistanceSquaredWide #-}
-
--- | Euclidean distance.
-distance :: Point -> Point -> Double
-distance left right = sqrt (squaredDistance left right)
-{-# INLINE distance #-}
-
--- | Negate a point interpreted as a vector.
-negateVector :: Point -> Point
-negateVector (Point x y) = Point (-x) (-y)
-{-# INLINE negateVector #-}
-
--- | Normalize a point interpreted as a vector without overflowing its length.
-normalizeVector :: Point -> Point
-normalizeVector (Point x y)
-  | scale == 0 = Point 0 0
-  | otherwise =
-      let !scaledX = x / scale
-          !scaledY = y / scale
-          !length' = sqrt (scaledX * scaledX + scaledY * scaledY)
-       in Point (scaledX / length') (scaledY / length')
- where
-  !scale = max (abs x) (abs y)
-{-# INLINE normalizeVector #-}
-
--- | Midpoint of two points.
-midpoint :: Point -> Point -> Point
-midpoint (Point ax ay) (Point bx by) = Point (0.5 * ax + 0.5 * bx) (0.5 * ay + 0.5 * by)
-{-# INLINE midpoint #-}
-
--- | Centroid of three points.
-centroid :: Point -> Point -> Point -> Point
-centroid (Point ax ay) (Point bx by) (Point cx cy) =
-  Point (ax + (bx - ax) / 3 + (cx - ax) / 3) (ay + (by - ay) / 3 + (cy - ay) / 3)
-{-# INLINE centroid #-}
-
--- | Unsigned area of a triangle.
-triangleArea :: Point -> Point -> Point -> Double
-triangleArea a b c = 0.5 * abs (orientDetApprox a b c)
-{-# INLINE triangleArea #-}
-
--- | Circumradius divided by shortest edge length.
-triangleRadiusEdgeRatio
-  :: Point -> Point -> Point -> Maybe Double
-triangleRadiusEdgeRatio p0 p1 p2
-  | area <= 0 || shortest <= 0 = Nothing
-  | not (isFinite ratio) = Nothing
-  | otherwise = Just ratio
- where
-  !area = triangleArea p0 p1 p2
-  !side01 = distance p0 p1
-  !side12 = distance p1 p2
-  !side20 = distance p2 p0
-  !shortest = min side01 (min side12 side20)
-  !otherProduct
-    | side01 <= side12 && side01 <= side20 = side12 * side20
-    | side12 <= side20 = side20 * side01
-    | otherwise = side01 * side12
-  !ratio = otherProduct / (4 * area)
-
--- | The square of 'triangleRadiusEdgeRatio', for a triangle whose area the
--- caller already has.
---
--- The ratio is only ever compared against a bound, and both sides are
--- non-negative, so the comparison can be made between squares. That is the
--- whole reason to have this: it settles the same question without the three
--- square roots the lengths would need, on the path taken by every face
--- refinement considers.
---
--- The area is a parameter and a degenerate triangle answers with an infinity
--- rather than an absence, because the caller on that path has already computed
--- the area to ask the area question and does nothing with the absence but
--- compare an infinity in its place.
-triangleRadiusEdgeRatioSquaredWithArea
-  :: Double -> Point -> Point -> Point -> Double
-triangleRadiusEdgeRatioSquaredWithArea area p0 p1 p2 =
-  triangleRadiusEdgeRatioSquaredWithAreaAndSides
-    area
-    (squaredDistance p0 p1)
-    (squaredDistance p1 p2)
-    (squaredDistance p2 p0)
-
--- | The same squared ratio from side squares already paid for by a caller.
-triangleRadiusEdgeRatioSquaredWithAreaAndSides
-  :: Double -> Double -> Double -> Double -> Double
-triangleRadiusEdgeRatioSquaredWithAreaAndSides area side01 side12 side20
-  | area <= 0 || shortest <= 0 = 1 / 0
-  | not (isFinite ratio) = 1 / 0
-  | otherwise = ratio
- where
-  !shortest = min side01 (min side12 side20)
-  !otherProduct
-    | side01 <= side12 && side01 <= side20 = side12 * side20
-    | side12 <= side20 = side20 * side01
-    | otherwise = side01 * side12
-  !ratio = otherProduct / (16 * area * area)
-
--- The scale the determinants are divided by cancels out of the quotient
--- exactly, so the computation works on the unscaled differences and divides
--- once per coordinate. Scaling would only matter against overflow, and the
--- validated coordinate domain (|x| <= 3.3e60) keeps every intermediate below
--- 1e183, five orders below the Double ceiling; the four divisions it cost
--- are the circumcentre's hot-path price. Identical points answer through the
--- denominator, which is exactly zero exactly when they are collinear.
--- | Circumcenter of a nondegenerate triangle.
-circumcenter
-  :: Point -> Point -> Point -> Maybe (Point)
-circumcenter (Point ax ay) (Point bx by) (Point cx cy)
-  | denominator == 0 = Nothing
-  | not (isFinite resultX && isFinite resultY) = Nothing
-  | otherwise = Just (canonicalPoint (Point resultX resultY))
- where
-  !bax = bx - ax
-  !bay = by - ay
-  !cax = cx - ax
-  !cay = cy - ay
-  !bLength = bax * bax + bay * bay
-  !cLength = cax * cax + cay * cay
-  !denominator = 2 * (bax * cay - bay * cax)
-  !offsetX = (cay * bLength - bay * cLength) / denominator
-  !offsetY = (bax * cLength - cax * bLength) / denominator
-  !resultX = ax + offsetX
-  !resultY = ay + offsetY
-
--- | Barycentric coordinates of a point in a nondegenerate triangle.
-barycentricCoordinates
-  :: Point -> Point -> Point -> Point
-  -> Maybe (Double, Double, Double)
-barycentricCoordinates a@(Point ax ay) b@(Point bx by) c@(Point cx cy) query@(Point qx qy)
-  | all reliable [denominatorInfo, weightAInfo, weightBInfo, weightCInfo] =
-      if denominator == 0
-        then Nothing
-        else Just (weightA / denominator, weightB / denominator, weightC / denominator)
-  | exactDenominator == 0 = Nothing
-  | otherwise =
-      Just
-        ( integerRatioToDouble exactWeightA exactDenominator
-        , integerRatioToDouble exactWeightB exactDenominator
-        , integerRatioToDouble exactWeightC exactDenominator
-        )
- where
-  !denominatorInfo@(denominator, _) = determinantInfo a b c
-  !weightAInfo@(weightA, _) = determinantInfo query b c
-  !weightBInfo@(weightB, _) = determinantInfo a query c
-  !weightCInfo@(weightC, _) = determinantInfo a b query
-  (!exactDenominator, !exactWeightA, !exactWeightB, !exactWeightC) =
-    exactBarycentricDeterminants ax ay bx by cx cy qx qy
-
-  reliable (determinant, determinantSum) =
-    isFinite determinant && abs determinant > scalarCcwErrorBound * determinantSum
-
-  determinantInfo :: Point -> Point -> Point -> (Double, Double)
-  determinantInfo (Point px py) (Point rx ry) (Point sx sy) =
-    let !left = (px - sx) * (ry - sy)
-        !right = (py - sy) * (rx - sx)
-     in (left - right, abs left + abs right)
-
--- | Whether an admitted point lies in the closed diametral disk of an admitted
--- segment, decided by the sign of @(a-p)·(b-p)@.
---
--- The approximation is two coordinate differences and one product per term
--- combined additively, which is the arithmetic shape 'orient2d' is analysed
--- under: the forward error of @fl(fl(a⊖b) ⊗ fl(c⊖d))@ combined by one rounded
--- addition does not depend on the sign of that combination. The orientation
--- coefficient therefore transfers unchanged, and the exact dot product remains
--- the oracle for the uncertain band.
-inDiametralCircle :: QueryPoint -> QueryPoint -> QueryPoint -> Bool
-inDiametralCircle from to query =
-  Admitted.inDiametralCircle
-    (queryPointValue from)
-    (queryPointValue to)
-    (queryPointValue query)
-{-# INLINE inDiametralCircle #-}
-
--- | Projection parameter of a point onto an oriented segment line.
-projectionFactor :: Point -> Point -> Point -> Double
-projectionFactor (Point ax ay) (Point bx by) (Point qx qy)
-  | lengthSquared == 0 = 0
-  | otherwise = ((qx - ax) * dx + (qy - ay) * dy) / lengthSquared
- where
-  !dx = bx - ax
-  !dy = by - ay
-  !lengthSquared = dx * dx + dy * dy
-{-# INLINE projectionFactor #-}
diff --git a/src-dcel/Moonlight/Triangulation/Payload.hs b/src-dcel/Moonlight/Triangulation/Payload.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Payload.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-{-# LANGUAGE RankNTypes #-}
-
--- | The payload parameters' functorial and traversable structure: relabeling a
--- payload touches no geometry, so every law here holds for the reason that a
--- triangulation's points and its annotations are separate things.
-module Moonlight.Triangulation.Payload
-  ( PayloadTraversal
-  , vertexPayloads
-  , directedPayloads
-  , undirectedPayloads
-  , facePayloads
-  , mapVertices
-  , mapDirectedEdges
-  , mapUndirectedEdges
-  , mapFaces
-  , overPayloads
-  , foldPayloads
-  , payloadList
-  ) where
-
-import Data.Functor.Const (Const (..))
-import Data.Functor.Identity (Identity (..))
-import Data.Monoid (Endo (..))
-import Moonlight.Triangulation.Internal.Representation
-  ( PayloadTraversal
-  , directedPayloads
-  , facePayloads
-  , mapDirectedEdges
-  , mapFaces
-  , mapUndirectedEdges
-  , mapVertices
-  , undirectedPayloads
-  , vertexPayloads
-  )
-
--- | Relabel every payload a traversal reaches.
---
--- The 'mapVertices' family is this at each parameter and cheaper: a pure map
--- leaves an unmaterialized page unmaterialized, where a traversal must visit
--- every slot the page would have reported and so materializes it. Reach for
--- this one when the traversal is chosen at runtime, and for the named map when
--- the parameter is known where you stand.
-overPayloads
-  :: PayloadTraversal source target payload payload'
-  -> (payload -> payload')
-  -> source
-  -> target
-overPayloads traversal relabel = runIdentity . traversal (Identity . relabel)
-{-# INLINE overPayloads #-}
-
--- | Summarize every payload a traversal reaches.
-foldPayloads
-  :: Monoid summary
-  => PayloadTraversal source source payload payload
-  -> (payload -> summary)
-  -> source
-  -> summary
-foldPayloads traversal measure = getConst . traversal (Const . measure)
-{-# INLINE foldPayloads #-}
-
--- | Every payload a traversal reaches, in visit order. Accumulated through
--- t'Endo' so the list is built by a right fold rather than by repeated append.
-payloadList :: PayloadTraversal source source payload payload -> source -> [payload]
-payloadList traversal source = appEndo (foldPayloads traversal (Endo . (:)) source) []
-{-# INLINE payloadList #-}
diff --git a/src-dcel/Moonlight/Triangulation/PointLocation.hs b/src-dcel/Moonlight/Triangulation/PointLocation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/PointLocation.hs
+++ /dev/null
@@ -1,129 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | Walk location over the finite mesh; the hinted form takes a starting point
--- and reports the steps it spent.
-module Moonlight.Triangulation.PointLocation
-  ( locatePoint
-  , locatePointWithHint
-  ) where
-
-import Data.List (find)
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-import Moonlight.Triangulation.Internal.FaceProbe
-import Moonlight.Triangulation.Internal.Predicates
-import Moonlight.Triangulation.Types
-
--- | Locate an admitted point without an initial topology hint.
-locatePoint :: Triangulation mode vertex directed undirected face -> QueryPoint -> Location
-locatePoint triangulation query = fst (locatePointWithHint triangulation Nothing query)
-
--- | Locate an admitted point from an optional starting cell and report the
--- descent work.
-locatePointWithHint :: Triangulation mode vertex directed undirected face -> Maybe LocationHint -> QueryPoint -> (Location, LocationStats)
-locatePointWithHint triangulation hint queryPoint
-  | numVertices triangulation == 0 = (EmptyTriangulation, emptyLocationStats)
-  | numInnerFaces triangulation == 0 = (locateDegenerate triangulation query, emptyLocationStats)
-  | otherwise = walk budget 0 startFace
- where
-  !query = queryPointValue queryPoint
-  !budget = max 8 (numFaces triangulation + numUndirectedEdges triangulation + 4)
-  !startFace = chooseStart triangulation hint
-
-  walk !remaining !steps !face
-    | remaining <= 0 = scanAll steps
-    | face == outerFace =
-        (outsideLocation triangulation query, LocationStats steps False)
-    | otherwise =
-        case probeFace triangulation face query of
-          FaceHit location -> (location, LocationStats (steps + 1) False)
-          FaceCross edge ->
-            let adjacent = incidentFace triangulation edge
-             in if adjacent == outerFace
-                  then (OutsideConvexHull (Just edge), LocationStats (steps + 1) False)
-                  else walk (remaining - 1) (steps + 1) adjacent
-          FaceMiss -> scanAll (steps + 1)
-
-  scanAll !steps =
-    case findHit (innerFaces triangulation) of
-      Just location -> (location, LocationStats steps True)
-      Nothing -> (outsideLocation triangulation query, LocationStats steps True)
-
-  findHit [] = Nothing
-  findHit (face : remaining) =
-    case probeFace triangulation face query of
-      FaceHit location -> Just location
-      _ -> findHit remaining
-
-data FaceProbe = FaceHit !Location | FaceCross !DirectedEdgeId | FaceMiss
-
-probeFace :: Triangulation mode vertex directed undirected face -> FaceId -> Point -> FaceProbe
-probeFace triangulation face query =
-  case innerFaceDirectedEdges triangulation face of
-    Nothing -> FaceMiss
-    Just (e0, e1, e2) -> classify [e0, e1, e2] Nothing
- where
-  classify [] Nothing = FaceHit (InFace face)
-  classify [] (Just edge) = FaceCross edge
-  classify (edge : remaining) crossing =
-    let fromVertex = origin triangulation edge
-        toVertex = destination triangulation edge
-        from = vertexPoint triangulation fromVertex
-        to = vertexPoint triangulation toVertex
-     in case probeBoundary reverseEdge query edge fromVertex from toVertex to of
-          BoundaryClear -> classify remaining crossing
-          BoundaryOnVertex vertex -> FaceHit (OnVertex vertex)
-          BoundaryOnEdge boundary -> FaceHit (OnEdge boundary)
-          BoundaryCrossing boundary -> classify remaining (Just boundary)
-
-locateDegenerate :: Triangulation mode vertex directed undirected face -> Point -> Location
-locateDegenerate triangulation query =
-  case find ((== query) . vertexPoint triangulation) (vertices triangulation) of
-    Just vertex -> OnVertex vertex
-    Nothing ->
-      case find contains (undirectedEdges triangulation) of
-        Just edge -> OnEdge (normalizedDirected edge)
-        Nothing ->
-          case undirectedEdges triangulation of
-            [] -> OutsideConvexHull Nothing
-            edge : _ ->
-              let forward = normalizedDirected edge
-                  from = vertexPoint triangulation (origin triangulation forward)
-                  to = vertexPoint triangulation (destination triangulation forward)
-                  directed = case orient2d from to query of
-                    GT -> forward
-                    LT -> reverseEdge forward
-                    EQ -> forward
-               in OutsideConvexHull (Just directed)
- where
-  contains edge =
-    let forward = normalizedDirected edge
-     in onClosedSegment
-          (vertexPoint triangulation (origin triangulation forward))
-          (vertexPoint triangulation (destination triangulation forward))
-          query
-
-outsideLocation :: Triangulation mode vertex directed undirected face -> Point -> Location
-outsideLocation triangulation query =
-  OutsideConvexHull (find visible (faceDirectedEdges triangulation outerFace))
- where
-  visible edge =
-    orient2d
-      (vertexPoint triangulation (origin triangulation edge))
-      (vertexPoint triangulation (destination triangulation edge))
-      query
-      /= LT
-
-chooseStart :: Triangulation mode vertex directed undirected face -> Maybe LocationHint -> FaceId
-chooseStart triangulation hint =
-  case hint of
-    Just (FaceHint face)
-      | face /= outerFace && faceIdIndex face < numFaces triangulation -> face
-    Just (VertexHint vertex@(VertexId index))
-      | toInteger index < toInteger (numVertices triangulation) ->
-          case [face | edge <- vertexOutgoingEdges triangulation vertex, let face = incidentFace triangulation edge, face /= outerFace] of
-            face : _ -> face
-            [] -> FaceId 1
-      | otherwise -> FaceId 1
-    _ -> FaceId 1
diff --git a/src-dcel/Moonlight/Triangulation/Region.hs b/src-dcel/Moonlight/Triangulation/Region.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Region.hs
+++ /dev/null
@@ -1,430 +0,0 @@
--- | Exact polygon authoring and grouped publication through the resident DCEL
--- region traversal. Construction validates topology once; downstream overlay
--- consumes only admitted layers.
-module Moonlight.Triangulation.Region
-  ( ExactLoop
-  , exactLoop
-  , exactLoopPoints
-  , PolygonComponent
-  , polygonComponent
-  , polygonOuterLoop
-  , polygonHoleLoops
-  , PlanarRegion
-  , planarRegion
-  , planarRegionComponents
-  , emptyPlanarRegion
-  , RegionPointLocation (..)
-  , regionPointLocation
-  , PlanarLayer
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarLayer
-  , planarLayerLabelAt
-  , RegionValidationError (..)
-  , RegionPublicationError (..)
-  , labelledPlanarLayer
-  ) where
-
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import Data.List (sort)
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , ExactSegment
-  , SegmentRelation (..)
-  , exactOnClosedSegment
-  , exactOrient2d
-  , exactPointCross
-  , exactPointCoordinates
-  , exactSegment
-  , exactSegmentEndpoints
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( admitsSimpleCycleEdgeRelation
-  , cyclePairs
-  , rotateCycleLeast
-  , simplifyBoundaryCycle
-  )
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEventObstruction (..)
-  , ExactSweepSegmentId (..)
-  , exactSegmentEventPlan
-  , exactSegmentRelationMap
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( exactSignum )
-import Moonlight.Triangulation.Internal.Region.Publication (labelledPlanarLayer)
-import Moonlight.Triangulation.Internal.Region.Bounds
-  ( boundsOverlap
-  , componentBounds
-  , exactLoopBounds
-  , overlappingOptionalPairs
-  , overlappingPairs
-  , overlappingPairsBetween
-  , pointInBounds
-  , regionBounds
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarLayer (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  , RegionPointLocation (..)
-  , RegionPublicationError (..)
-  , RegionValidationError (..)
-  )
-
-data LoopRelationWitness = LoopRelationWitness
-  !Int
-  !Int
-  !ExactSegment
-  !ExactSegment
-  !SegmentRelation
-
--- | Admit and canonicalize one simple exact cycle.
-exactLoop :: NonEmpty ExactPoint -> Either RegionValidationError ExactLoop
-exactLoop submitted = do
-  let withoutRepeatedTerminal = removeRepeatedTerminal (NonEmpty.toList submitted)
-  (_, simplified) <-
-    simplifyBoundaryCycle
-      RegionLoopDegenerate
-      exactRedundant
-      exactOrient2d
-      id
-      withoutRepeatedTerminal
-  let canonical = rotateCycleLeast simplified
-  validateSimpleLoop canonical
-  pure (ExactLoop canonical)
-
--- | Read the canonical cycle points.
-exactLoopPoints :: ExactLoop -> NonEmpty ExactPoint
-exactLoopPoints (ExactLoop points) = points
-
--- | Admit one component after checking orientation, containment, boundary
--- relations, and pairwise-disjoint hole interiors.
-polygonComponent
-  :: ExactLoop
-  -> [ExactLoop]
-  -> Either RegionValidationError PolygonComponent
-polygonComponent outer holes = do
-  let outerWinding = loopWinding outer
-  if outerWinding == GT
-    then Right ()
-    else Left (RegionOuterLoopWinding outerWinding)
-  traverse_ validateHoleWinding (zip [0 ..] holes)
-  traverse_ validateHole (zip [0 ..] holes)
-  traverse_
-    validateHolePair
-    (overlappingPairs (exactLoopBounds . snd) (zip [0 ..] holes))
-  pure (PolygonComponent outer (sort holes))
- where
-  validateHoleWinding (holeIndex, hole) =
-    let winding = loopWinding hole
-     in if winding == LT
-          then Right ()
-          else Left (RegionHoleLoopWinding holeIndex winding)
-  validateHole (holeIndex, hole) = do
-    relations <- crossLoopRelations outer hole
-    case relations of
-      LoopRelationWitness outerEdge holeEdge _ _ relation : _ ->
-        Left (RegionBoundaryRelation outerEdge holeEdge relation)
-      [] -> Right ()
-    let location = pointLocationInLoop outer (firstExactLoopPoint hole)
-    if location == RegionInterior
-      then Right ()
-      else Left (RegionHoleLocation holeIndex location)
-  validateHolePair ((leftIndex, left), (rightIndex, right)) =
-    do
-      relations <- crossLoopRelations left right
-      case relations of
-        LoopRelationWitness _ _ _ _ relation : _ ->
-          Left (RegionBoundaryRelation leftIndex rightIndex relation)
-        []
-          | loopContainsInteriorPoint left right || loopContainsInteriorPoint right left ->
-              Left (RegionComponentInteriorOverlap leftIndex rightIndex)
-          | otherwise -> Right ()
-
--- | Admit a finite region after checking that component interiors do not
--- overlap. Boundary-only contact remains lawful.
-planarRegion
-  :: [PolygonComponent]
-  -> Either RegionValidationError PlanarRegion
-planarRegion components = do
-  traverse_
-    validatePair
-    (overlappingPairs (componentBounds . snd) (zip [0 ..] components))
-  pure (PlanarRegion (sort components))
- where
-  validatePair ((leftIndex, left), (rightIndex, right)) = do
-    overlaps <- componentInteriorsOverlap left right
-    if overlaps
-      then Left (RegionComponentInteriorOverlap leftIndex rightIndex)
-      else Right ()
-
--- | Observe the canonically ordered components.
-planarRegionComponents :: PlanarRegion -> [PolygonComponent]
-planarRegionComponents (PlanarRegion components) = components
-
--- | The empty finite region.
-emptyPlanarRegion :: PlanarRegion
-emptyPlanarRegion = PlanarRegion []
-
--- | Locate an exact point against all components. Boundary membership has
--- priority over interior membership.
-regionPointLocation :: PlanarRegion -> ExactPoint -> RegionPointLocation
-regionPointLocation (PlanarRegion components) query =
-  foldr
-    (combineLocation . (`componentPointLocation` query))
-    RegionExterior
-    components
-
--- | Admit one labelled layer. Different labels may touch but their interiors
--- may not overlap, and the outside label cannot also name a bounded region.
-planarLayer
-  :: Ord label
-  => label
-  -> Map label PlanarRegion
-  -> Either RegionValidationError (PlanarLayer label)
-planarLayer outside regions
-  | Map.member outside regions = Left RegionOutsideLabelUsed
-  | otherwise = do
-      let indexed = zip [0 ..] (Map.toAscList regions)
-      traverse_
-        (\((leftIndex, (_, left)), (rightIndex, (_, right))) -> do
-           overlaps <- regionsInteriorsOverlap left right
-           if overlaps
-             then Left (RegionLayerInteriorOverlap leftIndex rightIndex)
-             else Right ())
-        (overlappingOptionalPairs (regionBounds . snd . snd) indexed)
-      pure (PlanarLayer outside regions)
-
--- | Label an exact point known to lie in a relatively open two-cell. Boundary
--- points conservatively retain the outside label.
-planarLayerLabelAt :: PlanarLayer label -> ExactPoint -> label
-planarLayerLabelAt layer query =
-  case
-    [ label
-    | (label, region) <- Map.toAscList (planarLayerRegions layer)
-    , regionPointLocation region query == RegionInterior
-    ] of
-    label : _ -> label
-    [] -> planarLayerOutsideLabel layer
-
-removeRepeatedTerminal :: Eq value => [value] -> [value]
-removeRepeatedTerminal values =
-  case values of
-    [] -> []
-    firstValue : remaining ->
-      case reverse remaining of
-        finalValue : reversedMiddle
-          | firstValue == finalValue -> firstValue : reverse reversedMiddle
-        _ -> values
-
-exactRedundant :: ExactPoint -> ExactPoint -> ExactPoint -> Bool
-exactRedundant previous current next =
-  exactOrient2d previous current next == EQ
-    && exactOnClosedSegment previous next current
-
-validateSimpleLoop :: NonEmpty ExactPoint -> Either RegionValidationError ()
-validateSimpleLoop points = do
-  segments <- loopSegments points
-  plan <- first RegionSegmentEventsInvalid (exactSegmentEventPlan segments)
-  case
-    [ (leftIndex, rightIndex, relation)
-    | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
-        Map.toAscList (exactSegmentRelationMap plan)
-    , not
-        ( admitsSimpleCycleEdgeRelation
-            segmentCount
-            leftIndex
-            rightIndex
-            relation
-        )
-    ] of
-    (leftIndex, rightIndex, relation) : _ ->
-      Left (RegionLoopSelfRelation leftIndex rightIndex relation)
-    [] -> Right ()
- where
-  segmentCount = NonEmpty.length points
-
-loopSegments
-  :: NonEmpty ExactPoint
-  -> Either RegionValidationError (V.Vector ExactSegment)
-loopSegments points =
-  V.fromList
-    <$> traverse
-      (\(from, to) ->
-         first (const (RegionLoopDegenerate (NonEmpty.toList points)))
-           (exactSegment from to))
-      (cyclePairs points)
-
-loopWinding :: ExactLoop -> Ordering
-loopWinding (ExactLoop points) =
-  exactSignum
-    ( List.foldl'
-        (\signedArea (from, to) ->
-           signedArea + exactPointCross from to)
-        0
-        (cyclePairs points)
-    )
-
-pointLocationInLoop :: ExactLoop -> ExactPoint -> RegionPointLocation
-pointLocationInLoop loop query
-  | not (pointInBounds query (exactLoopBounds loop)) = RegionExterior
-  | any (\(from, to) -> exactOnClosedSegment from to query) edges = RegionOnBoundary
-  | odd (length (filter crossesRay edges)) = RegionInterior
-  | otherwise = RegionExterior
- where
-  edges = cyclePairs (exactLoopPoints loop)
-  (_, py) = exactPointCoordinates query
-  crossesRay (from, to) =
-    let (_, ay) = exactPointCoordinates from
-        (_, by) = exactPointCoordinates to
-        orientation = exactOrient2d from to query
-     in (ay <= py && py < by && orientation == GT)
-          || (by <= py && py < ay && orientation == LT)
-
-componentPointLocation :: PolygonComponent -> ExactPoint -> RegionPointLocation
-componentPointLocation component query =
-  case pointLocationInLoop (polygonOuterLoop component) query of
-    RegionExterior -> RegionExterior
-    RegionOnBoundary -> RegionOnBoundary
-    RegionInterior -> foldr classifyHole RegionInterior (polygonHoleLoops component)
- where
-  classifyHole hole remaining =
-    case pointLocationInLoop hole query of
-      RegionExterior -> remaining
-      RegionOnBoundary -> RegionOnBoundary
-      RegionInterior -> RegionExterior
-
-crossLoopRelations
-  :: ExactLoop
-  -> ExactLoop
-  -> Either RegionValidationError [LoopRelationWitness]
-crossLoopRelations left right
-  | not (boundsOverlap (exactLoopBounds left) (exactLoopBounds right)) = Right []
-  | otherwise = do
-      leftSegments <- loopSegments (exactLoopPoints left)
-      rightSegments <- loopSegments (exactLoopPoints right)
-      let leftCount = V.length leftSegments
-          relationWitness (leftIndex, rightIndex, relation) =
-            LoopRelationWitness leftIndex rightIndex
-              <$> requireLoopSegment leftSegments leftIndex
-              <*> requireLoopSegment rightSegments rightIndex
-              <*> pure relation
-      plan <-
-        first RegionSegmentEventsInvalid
-          (exactSegmentEventPlan (leftSegments <> rightSegments))
-      traverse relationWitness
-        [ (leftIndex, rightIndex - leftCount, relation)
-        | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
-            Map.toAscList (exactSegmentRelationMap plan)
-        , leftIndex < leftCount
-        , rightIndex >= leftCount
-        ]
-
-requireLoopSegment
-  :: V.Vector ExactSegment
-  -> Int
-  -> Either RegionValidationError ExactSegment
-requireLoopSegment segments index =
-  maybe
-    ( Left
-        (RegionSegmentEventsInvalid (ExactSweepSegmentMissing (ExactSweepSegmentId index)))
-    )
-    Right
-    (segments V.!? index)
-
-loopContainsInteriorPoint :: ExactLoop -> ExactLoop -> Bool
-loopContainsInteriorPoint container candidate =
-  pointLocationInLoop container (firstExactLoopPoint candidate) == RegionInterior
-
-componentInteriorsOverlap
-  :: PolygonComponent
-  -> PolygonComponent
-  -> Either RegionValidationError Bool
-componentInteriorsOverlap left right =
-  boundariesProperlyCross left right
-    >>= \crossing ->
-      pure
-        ( crossing
-            || componentContainsInteriorPoint left right
-            || componentContainsInteriorPoint right left
-        )
-
-boundariesProperlyCross
-  :: PolygonComponent
-  -> PolygonComponent
-  -> Either RegionValidationError Bool
-boundariesProperlyCross left right =
-  anyEither
-    (uncurry loopPairInteriorsOverlap)
-    ( overlappingPairsBetween
-        exactLoopBounds
-        (componentLoops left)
-        (componentLoops right)
-    )
- where
-  componentLoops component =
-    polygonOuterLoop component : polygonHoleLoops component
-
-loopPairInteriorsOverlap
-  :: ExactLoop
-  -> ExactLoop
-  -> Either RegionValidationError Bool
-loopPairInteriorsOverlap leftLoop rightLoop = do
-  relations <- crossLoopRelations leftLoop rightLoop
-  pure (any relationOverlapsInteriors relations)
- where
-  relationOverlapsInteriors
-    (LoopRelationWitness _ _ leftSegment rightSegment relation) =
-    case relation of
-      SegmentsProperlyCross -> True
-      SegmentsDuplicate -> collinearInteriorsCoincide leftSegment rightSegment
-      SegmentsCollinearlyOverlap -> collinearInteriorsCoincide leftSegment rightSegment
-      _ -> False
-  collinearInteriorsCoincide leftSegment rightSegment =
-    canonicalDirection leftSegment == canonicalDirection rightSegment
-  canonicalDirection segment =
-    uncurry (<=) (exactSegmentEndpoints segment)
-
-componentContainsInteriorPoint :: PolygonComponent -> PolygonComponent -> Bool
-componentContainsInteriorPoint container candidate =
-  any
-    ((== RegionInterior) . componentPointLocation container)
-    (NonEmpty.toList (exactLoopPoints (polygonOuterLoop candidate)))
-
-firstExactLoopPoint :: ExactLoop -> ExactPoint
-firstExactLoopPoint (ExactLoop (point :| _)) = point
-
-regionsInteriorsOverlap
-  :: PlanarRegion
-  -> PlanarRegion
-  -> Either RegionValidationError Bool
-regionsInteriorsOverlap (PlanarRegion left) (PlanarRegion right) =
-  anyEither
-    (uncurry componentInteriorsOverlap)
-    (overlappingPairsBetween componentBounds left right)
-
-combineLocation :: RegionPointLocation -> RegionPointLocation -> RegionPointLocation
-combineLocation RegionOnBoundary _ = RegionOnBoundary
-combineLocation RegionExterior accumulated = accumulated
-combineLocation RegionInterior RegionOnBoundary = RegionOnBoundary
-combineLocation RegionInterior _ = RegionInterior
-
-anyEither
-  :: (value -> Either obstruction Bool)
-  -> [value]
-  -> Either obstruction Bool
-anyEither predicate =
-  foldr
-    (\value remaining -> do
-       matches <- predicate value
-       if matches then Right True else remaining)
-    (Right False)
diff --git a/src-dcel/Moonlight/Triangulation/Simplex.hs b/src-dcel/Moonlight/Triangulation/Simplex.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Simplex.hs
+++ /dev/null
@@ -1,226 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE TypeApplications #-}
-
--- | Canonical labelled simplices and downward-closed planar complexes.
--- Labels, rather than resident DCEL handles, own identity across independently
--- constructed meshes.
-module Moonlight.Triangulation.Simplex
-  ( SimplexDimension (..)
-  , PlanarSimplex
-  , PlanarSimplexError (..)
-  , planarVertex
-  , planarEdge
-  , planarFace
-  , planarSimplexDimension
-  , planarSimplexVertices
-  , planarSimplexBoundary
-  , PlanarComplex
-  , PlanarComplexError (..)
-  , planarComplex
-  , planarComplexCells
-  , planarComplexBasisAt
-  , unionPlanarComplex
-  , intersectPlanarComplex
-  )
-where
-
-import Control.DeepSeq (NFData)
-import Data.List qualified as List
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.Set (Set)
-import Data.Set qualified as Set
-import Data.Vector (Vector)
-import Data.Vector qualified as Vector
-import GHC.Generics (Generic)
-
--- | The closed dimension vocabulary for planar simplicial complexes.
-data SimplexDimension
-  = SimplexDimension0
-  | SimplexDimension1
-  | SimplexDimension2
-  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
-  deriving anyclass (NFData)
-
--- | One canonical simplex. Edge and face labels are strictly ascending.
-data PlanarSimplex label
-  = PlanarVertex !label
-  | PlanarEdge !label !label
-  | PlanarFace !label !label !label
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A repeated vertex would lower the requested simplex's dimension.
-data PlanarSimplexError label
-  = PlanarSimplexRepeatedVertex !label
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-planarVertex :: label -> PlanarSimplex label
-planarVertex = PlanarVertex
-
-planarEdge
-  :: Ord label
-  => label
-  -> label
-  -> Either (PlanarSimplexError label) (PlanarSimplex label)
-planarEdge firstLabel secondLabel =
-  case compare firstLabel secondLabel of
-    LT -> Right (PlanarEdge firstLabel secondLabel)
-    EQ -> Left (PlanarSimplexRepeatedVertex firstLabel)
-    GT -> Right (PlanarEdge secondLabel firstLabel)
-
-planarFace
-  :: Ord label
-  => label
-  -> label
-  -> label
-  -> Either (PlanarSimplexError label) (PlanarSimplex label)
-planarFace firstLabel secondLabel thirdLabel =
-  let (firstLow, firstHigh) = ascendingPair firstLabel secondLabel
-      (secondLow, finalHigh) = ascendingPair firstHigh thirdLabel
-      (finalLow, finalMiddle) = ascendingPair firstLow secondLow
-   in if finalLow == finalMiddle
-        then Left (PlanarSimplexRepeatedVertex finalLow)
-        else
-          if finalMiddle == finalHigh
-            then Left (PlanarSimplexRepeatedVertex finalMiddle)
-            else Right (PlanarFace finalLow finalMiddle finalHigh)
-
-planarSimplexDimension :: PlanarSimplex label -> SimplexDimension
-planarSimplexDimension simplex =
-  case simplex of
-    PlanarVertex _ -> SimplexDimension0
-    PlanarEdge _ _ -> SimplexDimension1
-    PlanarFace _ _ _ -> SimplexDimension2
-
-planarSimplexVertices :: PlanarSimplex label -> NonEmpty label
-planarSimplexVertices simplex =
-  case simplex of
-    PlanarVertex label -> label :| []
-    PlanarEdge firstLabel secondLabel -> firstLabel :| [secondLabel]
-    PlanarFace firstLabel secondLabel thirdLabel ->
-      firstLabel :| [secondLabel, thirdLabel]
-
--- | The canonical oriented simplicial boundary.
-planarSimplexBoundary
-  :: Num coefficient
-  => PlanarSimplex label
-  -> [(coefficient, PlanarSimplex label)]
-planarSimplexBoundary simplex =
-  case simplex of
-    PlanarVertex _ -> []
-    PlanarEdge firstLabel secondLabel ->
-      [ (-1, PlanarVertex firstLabel)
-      , (1, PlanarVertex secondLabel)
-      ]
-    PlanarFace firstLabel secondLabel thirdLabel ->
-      [ (1, PlanarEdge secondLabel thirdLabel)
-      , (-1, PlanarEdge firstLabel thirdLabel)
-      , (1, PlanarEdge firstLabel secondLabel)
-      ]
-
--- | A requested cell whose boundary is absent from the same complex.
-data PlanarComplexError label
-  = PlanarComplexBoundaryMissing
-      !(PlanarSimplex label)
-      !(PlanarSimplex label)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An admitted downward-closed complex and its canonical dense bases.
-data PlanarComplex label = PlanarComplex
-  { storedPlanarComplexCells :: !(Set (PlanarSimplex label))
-  , storedPlanarVertexBasis :: !(Vector (PlanarSimplex label))
-  , storedPlanarEdgeBasis :: !(Vector (PlanarSimplex label))
-  , storedPlanarFaceBasis :: !(Vector (PlanarSimplex label))
-  }
-  deriving stock (Show, Generic)
-  deriving anyclass (NFData)
-
-instance Eq label => Eq (PlanarComplex label) where
-  left == right =
-    storedPlanarComplexCells left == storedPlanarComplexCells right
-
-planarComplex
-  :: Ord label
-  => Set (PlanarSimplex label)
-  -> Either (PlanarComplexError label) (PlanarComplex label)
-planarComplex cells =
-  case List.find (\(_, boundary) -> Set.notMember boundary cells) boundaryPairs of
-    Just (simplex, boundary) ->
-      Left (PlanarComplexBoundaryMissing simplex boundary)
-    Nothing -> Right (admittedPlanarComplex cells)
- where
-  boundaryPairs =
-    [ (simplex, boundary)
-    | simplex <- Set.toAscList cells
-    , (_, boundary) <- planarSimplexBoundary @Int simplex
-    ]
-
-planarComplexCells :: PlanarComplex label -> Set (PlanarSimplex label)
-planarComplexCells = storedPlanarComplexCells
-
-planarComplexBasisAt
-  :: SimplexDimension
-  -> PlanarComplex label
-  -> Vector (PlanarSimplex label)
-planarComplexBasisAt dimension complexValue =
-  case dimension of
-    SimplexDimension0 -> storedPlanarVertexBasis complexValue
-    SimplexDimension1 -> storedPlanarEdgeBasis complexValue
-    SimplexDimension2 -> storedPlanarFaceBasis complexValue
-
--- | Union preserves downward closure, so no second validation is needed.
-unionPlanarComplex
-  :: Ord label
-  => PlanarComplex label
-  -> PlanarComplex label
-  -> PlanarComplex label
-unionPlanarComplex left right =
-  admittedPlanarComplex
-    (Set.union (planarComplexCells left) (planarComplexCells right))
-
--- | Intersection also preserves downward closure.
-intersectPlanarComplex
-  :: Ord label
-  => PlanarComplex label
-  -> PlanarComplex label
-  -> PlanarComplex label
-intersectPlanarComplex left right =
-  admittedPlanarComplex
-    (Set.intersection (planarComplexCells left) (planarComplexCells right))
-
-admittedPlanarComplex :: Set (PlanarSimplex label) -> PlanarComplex label
-admittedPlanarComplex cells =
-  let (vertices, edges, faces) =
-        foldr partitionSimplex ([], [], []) (Set.toAscList cells)
-   in PlanarComplex
-        { storedPlanarComplexCells = cells
-        , storedPlanarVertexBasis = Vector.fromList vertices
-        , storedPlanarEdgeBasis = Vector.fromList edges
-        , storedPlanarFaceBasis = Vector.fromList faces
-        }
-
-partitionSimplex
-  :: PlanarSimplex label
-  -> ( [PlanarSimplex label]
-     , [PlanarSimplex label]
-     , [PlanarSimplex label]
-     )
-  -> ( [PlanarSimplex label]
-     , [PlanarSimplex label]
-     , [PlanarSimplex label]
-     )
-partitionSimplex simplex (vertices, edges, faces) =
-  case simplex of
-    PlanarVertex _ -> (simplex : vertices, edges, faces)
-    PlanarEdge _ _ -> (vertices, simplex : edges, faces)
-    PlanarFace _ _ _ -> (vertices, edges, simplex : faces)
-
-ascendingPair :: Ord value => value -> value -> (value, value)
-ascendingPair firstValue secondValue =
-  if firstValue <= secondValue
-    then (firstValue, secondValue)
-    else (secondValue, firstValue)
diff --git a/src-dcel/Moonlight/Triangulation/Types.hs b/src-dcel/Moonlight/Triangulation/Types.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Types.hs
+++ /dev/null
@@ -1,45 +0,0 @@
--- | The package vocabulary: sites and validated queries, the mode-indexed
--- triangulation, what construction returns, and the closed failure types.
-module Moonlight.Triangulation.Types
-  ( Point (..)
-  , SiteRelation (..)
-  , QueryPoint
-  , queryPointValue
-  , PointValidationError (..)
-  , HasPosition (..)
-  , ElementDefaults (..)
-  , unitElementDefaults
-  , ConstraintMode (..)
-  , KnownConstraintMode (..)
-  , Triangulation
-  , DelaunayTriangulation
-  , ConstrainedDelaunayTriangulation
-  , BuildResult (..)
-  , InsertionDisposition (..)
-  , InsertionResult (..)
-  , BuildStats (..)
-  , emptyBuildStats
-  , CoordinateError (..)
-  , NonFiniteValue (..)
-  , classifyNonFinite
-  , RadiusSquared
-  , RadiusSquaredError (..)
-  , mkRadiusSquared
-  , radiusSquaredValue
-  , BuildError (..)
-  , Location (..)
-  , LocationHint (..)
-  , LocationStats (..)
-  , emptyLocationStats
-  , NearestStats (..)
-  , RefinementParameters (..)
-  , defaultRefinementParameters
-  , ValidationClosureStats (..)
-  , RefinementReceipt (..)
-  , RefinementDomainResult (..)
-  , RefinementResult (..)
-  , InvariantViolation (..)
-  ) where
-
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
diff --git a/src-dcel/Moonlight/Triangulation/Validation.hs b/src-dcel/Moonlight/Triangulation/Validation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Validation.hs
+++ /dev/null
@@ -1,11 +0,0 @@
--- | Whole-value DCEL validation and face-quality observations.
-module Moonlight.Triangulation.Validation
-  ( validateTopology
-  , validateDelaunay
-  , validateTriangulation
-  , triangulationIsValid
-  , faceArea
-  , faceMinimumAngleDegrees
-  ) where
-
-import Moonlight.Triangulation.Internal.Validation
diff --git a/src-dcel/Moonlight/Triangulation/Valuation.hs b/src-dcel/Moonlight/Triangulation/Valuation.hs
deleted file mode 100644
--- a/src-dcel/Moonlight/Triangulation/Valuation.hs
+++ /dev/null
@@ -1,795 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE GADTs #-}
-
--- | Intrinsic valuations of exact closed cell selections and admitted planar
--- regions. Euler characteristic and area remain exact; Euclidean length is an
--- exact radical expression accompanied by outward-rounded binary64 bounds.
-module Moonlight.Triangulation.Valuation
-  ( EulerCharacteristic
-  , eulerCharacteristicValue
-  , ExactArea
-  , exactAreaValue
-  , ExactPlanarMoments
-  , exactPlanarMeasure
-  , exactPlanarFirstX
-  , exactPlanarFirstY
-  , exactPlanarSecondXX
-  , exactPlanarSecondXY
-  , exactPlanarSecondYY
-  , scaleExactPlanarMoments
-  , polygonComponentMoments
-  , polygonComponentArea
-  , ExactLengthTerm
-  , lengthCoefficient
-  , squaredLength
-  , ExactLengthExpression
-  , exactLengthTerms
-  , CertifiedInterval (..)
-  , ExactLengthMeasurement
-  , exactLengthExpression
-  , exactLengthBounds
-  , PlanarValuations
-  , valuationEuler
-  , valuationArea
-  , valuationIntrinsic1
-  , ValuationError (..)
-  , cellValuations
-  , regionValuations
-  , planarValuationsPerimeter
-  , cellSetPerimeter
-  , regionPerimeter
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.Bits (shiftL)
-import Data.Foldable (foldlM)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.IntSet as IntSet
-import qualified Data.Map.Strict as Map
-import Data.Maybe (catMaybes)
-import qualified Data.Ratio as Ratio
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import GHC.Float (castDoubleToWord64, castWord64ToDouble)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Dcel
-  ( faceVertices
-  , incidentFace
-  , undirectedEndpoints
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError
-  , ExactPoint
-  , ExactSegment
-  , exactOnClosedSegment
-  , exactPointCross
-  , exactPointCoordinates
-  , exactSegment
-  , exactSegmentEndpoints
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId (..)
-  , UndirectedEdgeId (..)
-  , VertexId (..)
-  , directedPair
-  , faceIdIndex
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Internal.CellSet
-  ( ExactCellSet (..)
-  , exactCellSetIsFaceClosure
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( consecutivePairs
-  , cyclePairs
-  , orderedPair
-  )
-import Moonlight.Triangulation.Internal.Dyadic (integerBitLength)
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRationalDenominator
-  , exactRationalFromDyadic
-  , exactRationalFromFiniteDouble
-  , exactRationalFromNormalizedRatio
-  , exactRationalIsZero
-  , exactRationalNumerator
-  , exactSignum
-  )
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEvent (..)
-  , ExactSegmentEventObstruction
-  , ExactSweepSegmentId (..)
-  , exactSegmentEventPlan
-  , exactSegmentEvents
-  , exactSegmentSplitPoints
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  )
-import Moonlight.Triangulation.Internal.Region.Bounds
-  ( ExactBounds
-  , componentBounds
-  , overlappingPredecessors
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-
-newtype EulerCharacteristic = EulerCharacteristic Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-eulerCharacteristicValue :: EulerCharacteristic -> Int
-eulerCharacteristicValue (EulerCharacteristic value) = value
-
-newtype ExactArea = ExactArea ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-instance Semigroup ExactArea where
-  ExactArea left <> ExactArea right = ExactArea (left + right)
-
-instance Monoid ExactArea where
-  mempty = ExactArea 0
-
-exactAreaValue :: ExactArea -> ExactRational
-exactAreaValue (ExactArea value) = value
-
--- | Exact moments of a bounded planar measure through total degree two.
-data ExactPlanarMoments = ExactPlanarMoments
-  { exactPlanarMeasure :: !ExactRational
-    -- ^ Total signed measure.
-  , exactPlanarFirstX :: !ExactRational
-    -- ^ Raw first moment integral of @x@.
-  , exactPlanarFirstY :: !ExactRational
-    -- ^ Raw first moment integral of @y@.
-  , exactPlanarSecondXX :: !ExactRational
-    -- ^ Raw second moment integral of @x^2@.
-  , exactPlanarSecondXY :: !ExactRational
-    -- ^ Raw mixed moment integral of @x*y@.
-  , exactPlanarSecondYY :: !ExactRational
-    -- ^ Raw second moment integral of @y^2@.
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-instance Semigroup ExactPlanarMoments where
-  left <> right =
-    ExactPlanarMoments
-      { exactPlanarMeasure = exactPlanarMeasure left + exactPlanarMeasure right
-      , exactPlanarFirstX = exactPlanarFirstX left + exactPlanarFirstX right
-      , exactPlanarFirstY = exactPlanarFirstY left + exactPlanarFirstY right
-      , exactPlanarSecondXX = exactPlanarSecondXX left + exactPlanarSecondXX right
-      , exactPlanarSecondXY = exactPlanarSecondXY left + exactPlanarSecondXY right
-      , exactPlanarSecondYY = exactPlanarSecondYY left + exactPlanarSecondYY right
-      }
-
-instance Monoid ExactPlanarMoments where
-  mempty = ExactPlanarMoments 0 0 0 0 0 0
-
--- | Scale every moment by one exact coefficient.
-scaleExactPlanarMoments
-  :: ExactRational
-  -> ExactPlanarMoments
-  -> ExactPlanarMoments
-scaleExactPlanarMoments scalar moments =
-  ExactPlanarMoments
-    { exactPlanarMeasure = scalar * exactPlanarMeasure moments
-    , exactPlanarFirstX = scalar * exactPlanarFirstX moments
-    , exactPlanarFirstY = scalar * exactPlanarFirstY moments
-    , exactPlanarSecondXX = scalar * exactPlanarSecondXX moments
-    , exactPlanarSecondXY = scalar * exactPlanarSecondXY moments
-    , exactPlanarSecondYY = scalar * exactPlanarSecondYY moments
-    }
-
--- | Exact area and raw moments of one admitted component. The six unscaled
--- boundary sums are accumulated together and normalized once.
-polygonComponentMoments :: PolygonComponent -> ExactPlanarMoments
-polygonComponentMoments component =
-  let raw =
-        foldMap
-          loopMomentSums
-          (polygonOuterLoop component : polygonHoleLoops component)
-   in ExactPlanarMoments
-        { exactPlanarMeasure = oneHalf * rawDoubleArea raw
-        , exactPlanarFirstX = oneSixth * rawFirstX raw
-        , exactPlanarFirstY = oneSixth * rawFirstY raw
-        , exactPlanarSecondXX = oneTwelfth * rawSecondXX raw
-        , exactPlanarSecondXY = oneTwentyFourth * rawSecondXY raw
-        , exactPlanarSecondYY = oneTwelfth * rawSecondYY raw
-        }
-
--- | Exact unsigned area of one already-admitted polygon component.  Winding
--- and hole containment were discharged by 'polygonComponent', so this
--- observation performs no second geometric validation.
-polygonComponentArea :: PolygonComponent -> ExactArea
-polygonComponentArea = ExactArea . (oneHalf *) . componentDoubleArea
-
-data RawPlanarMomentSums = RawPlanarMomentSums
-  { rawDoubleArea :: !ExactRational
-  , rawFirstX :: !ExactRational
-  , rawFirstY :: !ExactRational
-  , rawSecondXX :: !ExactRational
-  , rawSecondXY :: !ExactRational
-  , rawSecondYY :: !ExactRational
-  }
-
-instance Semigroup RawPlanarMomentSums where
-  left <> right =
-    RawPlanarMomentSums
-      { rawDoubleArea = rawDoubleArea left + rawDoubleArea right
-      , rawFirstX = rawFirstX left + rawFirstX right
-      , rawFirstY = rawFirstY left + rawFirstY right
-      , rawSecondXX = rawSecondXX left + rawSecondXX right
-      , rawSecondXY = rawSecondXY left + rawSecondXY right
-      , rawSecondYY = rawSecondYY left + rawSecondYY right
-      }
-
-instance Monoid RawPlanarMomentSums where
-  mempty = RawPlanarMomentSums 0 0 0 0 0 0
-
-loopMomentSums :: ExactLoop -> RawPlanarMomentSums
-loopMomentSums (ExactLoop points) =
-  List.foldl' accumulateMomentEdge mempty (cyclePairs points)
-
-accumulateMomentEdge
-  :: RawPlanarMomentSums
-  -> (ExactPoint, ExactPoint)
-  -> RawPlanarMomentSums
-accumulateMomentEdge accumulated (from, to) =
-  let (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-      cross = exactPointCross from to
-   in RawPlanarMomentSums
-        { rawDoubleArea = rawDoubleArea accumulated + cross
-        , rawFirstX = rawFirstX accumulated + (fromX + toX) * cross
-        , rawFirstY = rawFirstY accumulated + (fromY + toY) * cross
-        , rawSecondXX =
-            rawSecondXX accumulated
-              + (fromX * fromX + fromX * toX + toX * toX) * cross
-        , rawSecondXY =
-            rawSecondXY accumulated
-              + (2 * fromX * fromY + fromX * toY + toX * fromY + 2 * toX * toY) * cross
-        , rawSecondYY =
-            rawSecondYY accumulated
-              + (fromY * fromY + fromY * toY + toY * toY) * cross
-        }
-
-data ExactLengthTerm = ExactLengthTerm
-  { lengthCoefficient :: !ExactRational
-  , squaredLength :: !ExactRational
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A normalized sum of rational coefficients times square roots of rational
--- squared lengths. It intentionally has no 'Eq' instance: syntactic radical
--- normalization is not algebraic-number equality.
-newtype ExactLengthExpression = ExactLengthExpression [ExactLengthTerm]
-  deriving stock (Show, Generic)
-  deriving anyclass (NFData)
-
-exactLengthTerms :: ExactLengthExpression -> [ExactLengthTerm]
-exactLengthTerms (ExactLengthExpression terms) = terms
-
-data CertifiedInterval = CertifiedInterval
-  { intervalLower :: !Double
-  , intervalUpper :: !Double
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data ExactLengthMeasurement = ExactLengthMeasurement
-  { exactLengthExpression :: !ExactLengthExpression
-  , exactLengthBounds :: !CertifiedInterval
-  }
-  deriving stock (Show, Generic)
-  deriving anyclass (NFData)
-
-data PlanarValuations = PlanarValuations
-  { valuationEuler :: !EulerCharacteristic
-  , valuationArea :: !ExactArea
-  , valuationIntrinsic1 :: !ExactLengthMeasurement
-  }
-  deriving stock (Show, Generic)
-  deriving anyclass (NFData)
-
-data ValuationError
-  = ValuationCoordinateMissing !VertexId
-  | ValuationFaceArity !FaceId !Int
-  | ValuationInvalidRegionSegment !ExactGeometryError
-  | ValuationSegmentEventsInvalid !ExactSegmentEventObstruction
-  | ValuationSegmentMissing !ExactSweepSegmentId
-  | ValuationBoundaryMultiplicity !ExactPoint !ExactPoint !Int
-  | ValuationNegativeSquaredLength !ExactRational
-  | ValuationCellSetNotPureRegion
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-cellValuations :: ExactCellSet -> Either ValuationError PlanarValuations
-cellValuations (ExactCellSet triangulation points selectedEdges selectedFaces) = do
-  faceDoubleAreas <-
-    traverse
-      (cellFaceDoubleArea triangulation points . FaceId . fromIntegral)
-      (IntSet.toAscList selectedFaces)
-  edgeContributions <-
-    traverse
-      ( cellEdgeLengthContribution triangulation points selectedFaces
-          . UndirectedEdgeId
-          . fromIntegral
-      )
-      (IntSet.toAscList selectedEdges)
-  assembleValuations
-    (IntMap.size points - IntSet.size selectedEdges + IntSet.size selectedFaces)
-    (List.foldl' (+) 0 faceDoubleAreas)
-    (normalizeLengthContributions id edgeContributions)
-
-regionValuations :: PlanarRegion -> Either ValuationError PlanarValuations
-regionValuations (PlanarRegion components) = do
-  componentBoundaries <- traverse componentBoundaryData components
-  let boundaryCover =
-        overlappingPredecessors componentBoundaryBounds componentBoundaries
-      hasPotentialBoundaryContacts = not (all (null . snd) boundaryCover)
-  euler <- regionEuler boundaryCover
-  boundaryAtoms <-
-    normalizedRegionBoundaryAtoms
-      hasPotentialBoundaryContacts
-      componentBoundaries
-  let doubleArea =
-        List.foldl'
-          (\area component -> area + componentDoubleArea component)
-          0
-          components
-  assembleValuations
-    euler
-    doubleArea
-    ( normalizeLengthContributions
-        (\(from, to) -> (oneHalf, segmentSquaredLength from to))
-        boundaryAtoms
-    )
-
-assembleValuations
-  :: Int
-  -> ExactRational
-  -> ExactLengthExpression
-  -> Either ValuationError PlanarValuations
-assembleValuations euler doubleArea lengthExpression =
-  PlanarValuations (EulerCharacteristic euler) (ExactArea (oneHalf * doubleArea))
-    <$> measureLength lengthExpression
-cellSetPerimeter
-  :: ExactCellSet
-  -> Either ValuationError ExactLengthMeasurement
-cellSetPerimeter cellSet
-  | exactCellSetIsFaceClosure cellSet =
-      cellValuations cellSet >>= planarValuationsPerimeter
-  | otherwise = Left ValuationCellSetNotPureRegion
-
-regionPerimeter
-  :: PlanarRegion
-  -> Either ValuationError ExactLengthMeasurement
-regionPerimeter region = regionValuations region >>= planarValuationsPerimeter
-
--- | Derive conventional boundary length from an already-computed intrinsic
--- valuation without traversing the source geometry again.
-planarValuationsPerimeter
-  :: PlanarValuations
-  -> Either ValuationError ExactLengthMeasurement
-planarValuationsPerimeter valuations =
-  measureLength
-    (scaleLengthExpression 2 (exactLengthExpression (valuationIntrinsic1 valuations)))
-
-cellFaceDoubleArea
-  :: Triangulation mode vertex directed undirected face
-  -> IntMap.IntMap ExactPoint
-  -> FaceId
-  -> Either ValuationError ExactRational
-cellFaceDoubleArea triangulation points face =
-  case faceVertices triangulation face of
-    [firstVertex, secondVertex, thirdVertex] -> do
-      firstPoint <- cellPoint points firstVertex
-      secondPoint <- cellPoint points secondVertex
-      thirdPoint <- cellPoint points thirdVertex
-      pure (triangleDoubleArea firstPoint secondPoint thirdPoint)
-    vertices -> Left (ValuationFaceArity face (length vertices))
-
-cellEdgeLengthContribution
-  :: Triangulation mode vertex directed undirected face
-  -> IntMap.IntMap ExactPoint
-  -> IntSet.IntSet
-  -> UndirectedEdgeId
-  -> Either ValuationError (ExactRational, ExactRational)
-cellEdgeLengthContribution triangulation points selectedFaces edge = do
-  let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-      (forward, backward) = directedPair edge
-      selected face = IntSet.member (faceIdIndex face) selectedFaces
-      coefficient = case (selected (incidentFace triangulation forward), selected (incidentFace triangulation backward)) of
-        (False, False) -> 1
-        (True, True) -> 0
-        _ -> oneHalf
-  from <- cellPoint points fromVertex
-  to <- cellPoint points toVertex
-  pure (coefficient, segmentSquaredLength from to)
-
-cellPoint
-  :: IntMap.IntMap ExactPoint
-  -> VertexId
-  -> Either ValuationError ExactPoint
-cellPoint points vertex =
-  maybe
-    (Left (ValuationCoordinateMissing vertex))
-    Right
-    (IntMap.lookup (vertexIdIndex vertex) points)
-
-triangleDoubleArea :: ExactPoint -> ExactPoint -> ExactPoint -> ExactRational
-triangleDoubleArea firstPoint secondPoint thirdPoint =
-  exactPointCross firstPoint secondPoint
-    + exactPointCross secondPoint thirdPoint
-    + exactPointCross thirdPoint firstPoint
-
-componentDoubleArea :: PolygonComponent -> ExactRational
-componentDoubleArea component =
-  List.foldl'
-    (\area loop -> area + loopDoubleArea loop)
-    0
-    (polygonOuterLoop component : polygonHoleLoops component)
-
-loopDoubleArea :: ExactLoop -> ExactRational
-loopDoubleArea (ExactLoop points) =
-  List.foldl'
-    (\area (from, to) -> area + exactPointCross from to)
-    0
-    (cyclePairs points)
-
-segmentSquaredLength :: ExactPoint -> ExactPoint -> ExactRational
-segmentSquaredLength from to =
-  let (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-      deltaX = toX - fromX
-      deltaY = toY - fromY
-   in deltaX * deltaX + deltaY * deltaY
-
-normalizeLengthContributions
-  :: Foldable collection
-  => (value -> (ExactRational, ExactRational))
-  -> collection value
-  -> ExactLengthExpression
-normalizeLengthContributions contribution contributions =
-  ExactLengthExpression
-    [ ExactLengthTerm coefficient square
-    | (square, coefficient) <- Map.toAscList coefficientsBySquare
-    , not (exactRationalIsZero coefficient)
-    ]
- where
-  coefficientsBySquare =
-    List.foldl' accumulateContribution Map.empty contributions
-  accumulateContribution coefficients value =
-    case contribution value of
-      (coefficient, square)
-        | exactRationalIsZero coefficient -> coefficients
-        | otherwise -> Map.insertWith (+) square coefficient coefficients
-
-scaleLengthExpression
-  :: Integer
-  -> ExactLengthExpression
-  -> ExactLengthExpression
-scaleLengthExpression scalar (ExactLengthExpression terms) =
-  let exactScalar = fromInteger scalar
-   in ExactLengthExpression
-        [ term
-            { lengthCoefficient =
-                exactScalar * lengthCoefficient term
-            }
-        | term <- terms
-        ]
-
-measureLength
-  :: ExactLengthExpression
-  -> Either ValuationError ExactLengthMeasurement
-measureLength expression@(ExactLengthExpression terms) = do
-  (lower, upper) <-
-    foldlM
-      addTermBounds
-      (0, 0)
-      terms
-  pure
-    ExactLengthMeasurement
-      { exactLengthExpression = expression
-      , exactLengthBounds =
-          CertifiedInterval
-            { intervalLower = directedLowerDouble lower
-            , intervalUpper = directedUpperDouble upper
-            }
-      }
- where
-  addTermBounds (lowerTotal, upperTotal) term = do
-    (lowerRoot, upperRoot) <- exactSquareRootBounds (squaredLength term)
-    let coefficient = lengthCoefficient term
-    pure
-      ( lowerTotal + coefficient * lowerRoot
-      , upperTotal + coefficient * upperRoot
-      )
-
-exactSquareRootBounds
-  :: ExactRational
-  -> Either ValuationError (ExactRational, ExactRational)
-exactSquareRootBounds value =
-  case exactSignum value of
-    LT -> Left (ValuationNegativeSquaredLength value)
-    _ ->
-      let numerator = exactRationalNumerator value
-          denominator = exactRationalDenominator value
-          scale = 1 `shiftL` radicalPrecisionBits
-          scaledNumerator = numerator * scale * scale
-          root = integerSquareRoot (scaledNumerator `div` denominator)
-          exact = root * root * denominator == scaledNumerator
-          dyadicPower = negate radicalPrecisionBits
-       in Right
-            ( exactRationalFromDyadic root dyadicPower
-            , exactRationalFromDyadic (if exact then root else root + 1) dyadicPower
-            )
-
-radicalPrecisionBits :: Int
-radicalPrecisionBits = 128
-
-integerSquareRoot :: Integer -> Integer
-integerSquareRoot value
-  | value < 2 = value
-  | otherwise = descend initial
- where
-  initial = 1 `shiftL` ((integerBitLength value + 1) `div` 2)
-  descend estimate =
-    let refined = (estimate + value `div` estimate) `div` 2
-     in if refined >= estimate then estimate else descend refined
-
-directedLowerDouble :: ExactRational -> Double
-directedLowerDouble value =
-  let candidate = rationalToDouble value
-   in if isInfinite candidate
-        then maximumFiniteDouble
-        else
-          if exactRationalFromFiniteDouble candidate <= value
-            then candidate
-            else previousPositiveDouble candidate
-
-directedUpperDouble :: ExactRational -> Double
-directedUpperDouble value =
-  let candidate = rationalToDouble value
-   in if isInfinite candidate
-        || exactRationalFromFiniteDouble candidate >= value
-        then candidate
-        else nextPositiveDouble candidate
-
-rationalToDouble :: ExactRational -> Double
-rationalToDouble value =
-  fromRational
-    ( exactRationalNumerator value
-        Ratio.% exactRationalDenominator value
-    )
-
-previousPositiveDouble :: Double -> Double
-previousPositiveDouble value
-  | value <= 0 = 0
-  | otherwise = castWord64ToDouble (castDoubleToWord64 value - 1)
-
-nextPositiveDouble :: Double -> Double
-nextPositiveDouble value
-  | value == 0 = castWord64ToDouble 1
-  | otherwise = castWord64ToDouble (castDoubleToWord64 value + 1)
-
-maximumFiniteDouble :: Double
-maximumFiniteDouble = castWord64ToDouble 0x7fefffffffffffff
-
-data ComponentBoundaryData = ComponentBoundaryData
-  { componentBoundaryEuler :: !Int
-  , componentBoundaryBounds :: !ExactBounds
-  , componentBoundarySegments :: !(V.Vector ExactSegment)
-  }
-
-componentBoundaryData
-  :: PolygonComponent
-  -> Either ValuationError ComponentBoundaryData
-componentBoundaryData component = do
-  segments <-
-    V.fromList
-      <$> traverse
-        admittedSegment
-        ( concatMap
-            (cyclePairs . loopPoints)
-            (polygonOuterLoop component : polygonHoleLoops component)
-        )
-  pure
-    ComponentBoundaryData
-      { componentBoundaryEuler = 1 - length (polygonHoleLoops component)
-      , componentBoundaryBounds = componentBounds component
-      , componentBoundarySegments = segments
-      }
-
-regionEuler
-  :: [(ComponentBoundaryData, [ComponentBoundaryData])]
-  -> Either ValuationError Int
-regionEuler = foldlM attachComponent 0
- where
-  attachComponent accumulatedEuler (current, priorCandidates) = do
-    let priorSegments = V.concat (map componentBoundarySegments priorCandidates)
-    intersectionEuler <-
-      boundaryIntersectionEuler
-        (componentBoundarySegments current)
-        priorSegments
-    pure
-      ( accumulatedEuler
-          + componentBoundaryEuler current
-          - intersectionEuler
-      )
-
-boundaryIntersectionEuler
-  :: V.Vector ExactSegment
-  -> V.Vector ExactSegment
-  -> Either ValuationError Int
-boundaryIntersectionEuler current prior
-  | V.null prior = Right 0
-  | otherwise = do
-      let currentCount = V.length current
-          segments = current <> prior
-      plan <- first ValuationSegmentEventsInvalid (exactSegmentEventPlan segments)
-      contacts <-
-        traverse
-          (contactFromEvent segments)
-          [ event
-          | event <- exactSegmentEvents plan
-          , crossPartition currentCount event
-          ]
-      let contactPoints =
-            Set.fromList
-              [ point
-              | ContactPoint point <- contacts
-              ]
-          intervals =
-            [ interval
-            | ContactInterval interval <- contacts
-            ]
-          allSplitPoints =
-            Set.fromList
-              ( concatMap
-                  (exactSegmentSplitPoints plan)
-                  [ ExactSweepSegmentId index
-                  | index <- [0 .. V.length segments - 1]
-                  ]
-              )
-          contactEdges =
-            Set.fromList
-              [ orderedPair from to
-              | interval <- intervals
-              , let (lower, upper) = interval
-                    points =
-                      Set.toAscList
-                        ( Set.filter
-                            (exactOnClosedSegment lower upper)
-                            allSplitPoints
-                        )
-              , (from, to) <- consecutivePairs points
-              , from /= to
-              ]
-          vertices =
-            Set.unions
-              [ contactPoints
-              , Set.fromList
-                  [ point
-                  | (from, to) <- Set.toAscList contactEdges
-                  , point <- [from, to]
-                  ]
-              ]
-      pure (Set.size vertices - Set.size contactEdges)
-
-data BoundaryContact
-  = ContactPoint !ExactPoint
-  | ContactInterval !(ExactPoint, ExactPoint)
-
-contactFromEvent
-  :: V.Vector ExactSegment
-  -> ExactSegmentEvent
-  -> Either ValuationError BoundaryContact
-contactFromEvent _ (ExactProperCrossing _ _ point) = Right (ContactPoint point)
-contactFromEvent _ (ExactEndpointTouch _ _ point) = Right (ContactPoint point)
-contactFromEvent _ (ExactSharedEndpoint _ _ point) = Right (ContactPoint point)
-contactFromEvent segments (ExactDuplicateSegments leftId _) =
-  ContactInterval . canonicalSegmentEndpoints
-    <$> requireSegment segments leftId
-contactFromEvent _ (ExactCollinearOverlap _ _ lower upper) =
-  Right (ContactInterval (orderedPair lower upper))
-
-crossPartition :: Int -> ExactSegmentEvent -> Bool
-crossPartition boundary event =
-  let (ExactSweepSegmentId left, ExactSweepSegmentId right) = eventIds event
-   in (left < boundary) /= (right < boundary)
-
-eventIds
-  :: ExactSegmentEvent
-  -> (ExactSweepSegmentId, ExactSweepSegmentId)
-eventIds (ExactProperCrossing left right _) = (left, right)
-eventIds (ExactEndpointTouch left right _) = (left, right)
-eventIds (ExactSharedEndpoint left right _) = (left, right)
-eventIds (ExactDuplicateSegments left right) = (left, right)
-eventIds (ExactCollinearOverlap left right _ _) = (left, right)
-
-normalizedRegionBoundaryAtoms
-  :: Bool
-  -> [ComponentBoundaryData]
-  -> Either ValuationError (Set.Set (ExactPoint, ExactPoint))
-normalizedRegionBoundaryAtoms hasPotentialBoundaryContacts boundaries
-  | V.null segments = Right Set.empty
-  | not hasPotentialBoundaryContacts =
-      Right
-        ( Set.fromList
-            (map canonicalSegmentEndpoints (V.toList segments))
-        )
-  | otherwise = do
-      plan <- first ValuationSegmentEventsInvalid (exactSegmentEventPlan segments)
-      let orientedAtoms =
-            concatMap
-              segmentAtoms
-              [ exactSegmentSplitPoints plan (ExactSweepSegmentId index)
-              | index <- [0 .. V.length segments - 1]
-              ]
-      traverseMultiplicity
-        (Map.toAscList (Map.fromListWith (+) orientedAtoms))
- where
-  segments = V.concat (map componentBoundarySegments boundaries)
-  segmentAtoms :: [ExactPoint] -> [((ExactPoint, ExactPoint), Int)]
-  segmentAtoms points =
-    [ ( orderedPair firstPoint secondPoint
-      , if firstPoint <= secondPoint then 1 else -1
-      )
-    | (firstPoint, secondPoint) <- consecutivePairs points
-    , firstPoint /= secondPoint
-    ]
-  traverseMultiplicity
-    :: [((ExactPoint, ExactPoint), Int)]
-    -> Either ValuationError (Set.Set (ExactPoint, ExactPoint))
-  traverseMultiplicity entries = do
-    retained <-
-      traverse
-        (\(edge@(from, to), multiplicity) ->
-           case abs multiplicity of
-             0 -> Right Nothing
-             1 -> Right (Just edge)
-             _ -> Left (ValuationBoundaryMultiplicity from to multiplicity))
-        entries
-    pure (Set.fromList (catMaybes retained))
-
-admittedSegment
-  :: (ExactPoint, ExactPoint)
-  -> Either ValuationError ExactSegment
-admittedSegment (from, to) = first ValuationInvalidRegionSegment (exactSegment from to)
-
-requireSegment
-  :: V.Vector ExactSegment
-  -> ExactSweepSegmentId
-  -> Either ValuationError ExactSegment
-requireSegment segments segmentId@(ExactSweepSegmentId index) =
-  maybe
-    (Left (ValuationSegmentMissing segmentId))
-    Right
-    (segments V.!? index)
-
-canonicalSegmentEndpoints :: ExactSegment -> (ExactPoint, ExactPoint)
-canonicalSegmentEndpoints = uncurry orderedPair . exactSegmentEndpoints
-
-loopPoints :: ExactLoop -> NonEmpty ExactPoint
-loopPoints (ExactLoop points) = points
-
-oneHalf :: ExactRational
-oneHalf = exactRationalFromDyadic 1 (-1)
-
-oneSixth :: ExactRational
-oneSixth = exactRationalFromNormalizedRatio (1 Ratio.% 6)
-
-oneTwelfth :: ExactRational
-oneTwelfth = exactRationalFromNormalizedRatio (1 Ratio.% 12)
-
-oneTwentyFourth :: ExactRational
-oneTwentyFourth = exactRationalFromNormalizedRatio (1 Ratio.% 24)
diff --git a/src-dual/Moonlight/Planar/Internal/InterpolationWorkspace.hs b/src-dual/Moonlight/Planar/Internal/InterpolationWorkspace.hs
new file mode 100644
--- /dev/null
+++ b/src-dual/Moonlight/Planar/Internal/InterpolationWorkspace.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE TypeFamilies #-}
+
+module Moonlight.Planar.Internal.InterpolationWorkspace
+  ( NaturalNeighborWorkspace (..)
+  , newNaturalNeighborWorkspace
+  , nextFaceGeneration
+  , nextOriginGeneration
+  , workspaceBytes
+  ) where
+
+import Control.Monad.Primitive (PrimMonad, PrimState)
+import Control.Monad.ST (ST)
+import Data.Primitive.MutVar
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Dcel (numDirectedEdges, numFaces, numVertices)
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Internal.Types (ConstraintMode)
+import Moonlight.Planar.Scalar (scalarByteSize)
+
+-- | Reusable scratch space for Sibson interpolation. It borrows no mutable
+-- topology: the immutable triangulation is the sole owner, while these arrays
+-- are query-local marks, queues and numeric work buffers.
+data NaturalNeighborWorkspace state (mode :: ConstraintMode) vertex directed undirected face = NaturalNeighborWorkspace
+  { nnTriangulation :: !(Triangulation mode vertex directed undirected face)
+  , nnFaceMarks :: !(MUV.MVector state Word32)
+  , nnFaceSeenMarks :: !(MUV.MVector state Word32)
+    -- The circumcentre plane is stamped by the same generation as the two mark
+    -- planes above, and that is what makes it sound: a generation is minted
+    -- once per Sibson query, and the circumcentre a face contributes is a
+    -- function of the face and the query point together. Holding the plane to
+    -- exactly one generation's lifetime holds the query point fixed for as long
+    -- as an entry can be read.
+  , nnCircumcenterMarks :: !(MUV.MVector state Word32)
+  , nnFaceGeneration :: !(MutVar state Word32)
+  , nnCircumcenterX :: !(MUV.MVector state Double)
+  , nnCircumcenterY :: !(MUV.MVector state Double)
+  , nnFaceQueue :: !(MUV.MVector state Word32)
+  , nnCavityFaces :: !(MUV.MVector state Word32)
+  , nnBoundaryEdges :: !(MUV.MVector state Word32)
+  , nnOrderedEdges :: !(MUV.MVector state Word32)
+  , nnOriginMarks :: !(MUV.MVector state Word32)
+  , nnOriginGeneration :: !(MutVar state Word32)
+  , nnOriginEdge :: !(MUV.MVector state Word32)
+  , nnInsertionX :: !(MUV.MVector state Double)
+  , nnInsertionY :: !(MUV.MVector state Double)
+  , nnWeightVertex :: !(MUV.MVector state Word32)
+  , nnWeightValue :: !(MUV.MVector state Double)
+  }
+
+-- | Allocate reusable interpolation storage sized to one triangulation.
+newNaturalNeighborWorkspace
+  :: PrimMonad m
+  => Triangulation mode vertex directed undirected face
+  -> m (NaturalNeighborWorkspace (PrimState m) mode vertex directed undirected face)
+newNaturalNeighborWorkspace triangulation = do
+  let !faceCapacity = max 1 (numFaces triangulation)
+      !edgeCapacity = max 1 (numDirectedEdges triangulation)
+      !vertexCapacity = max 1 (numVertices triangulation)
+  faceMarks <- MUV.replicate faceCapacity 0
+  faceSeenMarks <- MUV.replicate faceCapacity 0
+  circumcenterMarks <- MUV.replicate faceCapacity 0
+  faceGeneration <- newMutVar 0
+  circumcenterX <- MUV.new faceCapacity
+  circumcenterY <- MUV.new faceCapacity
+  faceQueue <- MUV.new faceCapacity
+  cavityFaces <- MUV.new faceCapacity
+  boundaryEdges <- MUV.new edgeCapacity
+  orderedEdges <- MUV.new edgeCapacity
+  originMarks <- MUV.replicate vertexCapacity 0
+  originGeneration <- newMutVar 0
+  originEdge <- MUV.new vertexCapacity
+  insertionX <- MUV.new edgeCapacity
+  insertionY <- MUV.new edgeCapacity
+  weightVertex <- MUV.new edgeCapacity
+  weightValue <- MUV.new edgeCapacity
+  pure NaturalNeighborWorkspace
+    { nnTriangulation = triangulation
+    , nnFaceMarks = faceMarks
+    , nnFaceSeenMarks = faceSeenMarks
+    , nnCircumcenterMarks = circumcenterMarks
+    , nnFaceGeneration = faceGeneration
+    , nnCircumcenterX = circumcenterX
+    , nnCircumcenterY = circumcenterY
+    , nnFaceQueue = faceQueue
+    , nnCavityFaces = cavityFaces
+    , nnBoundaryEdges = boundaryEdges
+    , nnOrderedEdges = orderedEdges
+    , nnOriginMarks = originMarks
+    , nnOriginGeneration = originGeneration
+    , nnOriginEdge = originEdge
+    , nnInsertionX = insertionX
+    , nnInsertionY = insertionY
+    , nnWeightVertex = weightVertex
+    , nnWeightValue = weightValue
+    }
+
+nextFaceGeneration :: NaturalNeighborWorkspace s mode vertex directed undirected face -> ST s Word32
+nextFaceGeneration workspace = do
+  current <- readMutVar (nnFaceGeneration workspace)
+  let !next = current + 1
+  if next == 0
+    then do
+      MUV.set (nnFaceMarks workspace) 0
+      MUV.set (nnFaceSeenMarks workspace) 0
+      MUV.set (nnCircumcenterMarks workspace) 0
+      writeMutVar (nnFaceGeneration workspace) 1
+      pure 1
+    else writeMutVar (nnFaceGeneration workspace) next >> pure next
+
+nextOriginGeneration :: NaturalNeighborWorkspace s mode vertex directed undirected face -> ST s Word32
+nextOriginGeneration workspace = nextGeneration (nnOriginMarks workspace) (nnOriginGeneration workspace)
+
+nextGeneration :: MUV.MVector s Word32 -> MutVar s Word32 -> ST s Word32
+nextGeneration marks reference = do
+  current <- readMutVar reference
+  let !next = current + 1
+  if next == 0
+    then do
+      MUV.set marks 0
+      writeMutVar reference 1
+      pure 1
+    else writeMutVar reference next >> pure next
+
+-- | Bytes owned by the reusable numeric and index planes.
+workspaceBytes
+  :: NaturalNeighborWorkspace state mode vertex directed undirected face
+  -> Integer
+workspaceBytes workspace =
+  4 * toInteger wordSlots
+    + toInteger scalarByteSize * toInteger scalarSlots
+ where
+  wordSlots =
+    MUV.length (nnFaceMarks workspace)
+      + MUV.length (nnFaceSeenMarks workspace)
+      + MUV.length (nnCircumcenterMarks workspace)
+      + MUV.length (nnFaceQueue workspace)
+      + MUV.length (nnCavityFaces workspace)
+      + MUV.length (nnBoundaryEdges workspace)
+      + MUV.length (nnOrderedEdges workspace)
+      + MUV.length (nnOriginMarks workspace)
+      + MUV.length (nnOriginEdge workspace)
+      + MUV.length (nnWeightVertex workspace)
+  scalarSlots =
+    MUV.length (nnCircumcenterX workspace)
+      + MUV.length (nnCircumcenterY workspace)
+      + MUV.length (nnInsertionX workspace)
+      + MUV.length (nnInsertionY workspace)
+      + MUV.length (nnWeightValue workspace)
diff --git a/src-dual/Moonlight/Planar/Interpolation.hs b/src-dual/Moonlight/Planar/Interpolation.hs
new file mode 100644
--- /dev/null
+++ b/src-dual/Moonlight/Planar/Interpolation.hs
@@ -0,0 +1,886 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | Nearest, barycentric, and natural-neighbor interpolation over one
+-- authoritative triangulation.
+module Moonlight.Planar.Interpolation
+  ( BarycentricWeights (..)
+  , InterpolationStats (..)
+  , NaturalNeighborResult (..)
+  , NaturalNeighborWorkspace
+  , newNaturalNeighborWorkspace
+  , workspaceBytes
+  , nearestNeighbor
+  , barycentricWeights
+  , naturalNeighborWeights
+  , foldNaturalNeighborWeights
+  , interpolateNearest
+  , interpolateBarycentric
+  , interpolateNaturalNeighbor
+  , estimateGradient
+  , estimateGradients
+  , interpolateNaturalNeighborGradient
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad.ST (ST)
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import Data.Word (Word32)
+import Moonlight.Planar.Dcel
+  ( destination
+  , counterClockwise
+  , incidentFace
+  , innerFaceDirectedEdges
+  , innerFaceVertices
+  , isBoundaryEdge
+  , next
+  , numConstraints
+  , numDirectedEdges
+  , numVertices
+  , origin
+  , outerFace
+  , vertexOutEdge
+  , vertexOutgoingEdges
+  , vertexPoint
+  )
+import Moonlight.Planar.Handles.Iterators.FixedIterators (foldVertices')
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , asUndirected
+  , FaceId (..)
+  , faceIdIndex
+  , VertexId (..)
+  , reverseEdge
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.InterpolationWorkspace
+import Moonlight.Planar.Internal.Predicates (compareSquaredDistances, inCircle)
+import Moonlight.Planar.Math (barycentricCoordinates, circumcenter, projectionFactor)
+import Moonlight.Planar.Scalar (isFinite)
+import Moonlight.Planar.PointLocation (locatePointWithHint)
+import Moonlight.Planar.Scalar (scalarEpsilon)
+import Moonlight.Planar.Types (Location (..), LocationHint, LocationStats, NearestStats (..), Triangulation)
+import Moonlight.Planar.Point (Point (..), QueryPoint, queryPointValue)
+import GHC.Generics (Generic)
+
+-- | Barycentric weights use the mesh's binary64 coordinate domain.
+data BarycentricWeights
+  = NoWeights
+  | OneWeight !VertexId
+  | TwoWeights !VertexId !Double !VertexId !Double
+  | ThreeWeights !VertexId !Double !VertexId !Double !VertexId !Double
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Work performed by one natural-neighbor query.
+data InterpolationStats = InterpolationStats
+  { interpolationCavityFaces :: {-# UNPACK #-} !Int
+    -- ^ Faces in the query insertion cavity.
+  , interpolationNaturalNeighbors :: {-# UNPACK #-} !Int
+    -- ^ Sites contributing nonzero Sibson weight.
+  , interpolationFaceTests :: {-# UNPACK #-} !Int
+    -- ^ Faces tested while discovering the cavity.
+    -- | 'True' exactly when the Sibson pipeline declined and the returned
+    -- weights are the barycentric coordinates of the located face instead.
+    -- A silent degradation from Sibson to barycentric is a defect that hides;
+    -- this bit is the announcement.
+  , interpolationUsedFallback :: !Bool
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Weights and receipts produced by one natural-neighbor query.
+data NaturalNeighborResult = NaturalNeighborResult
+  { naturalNeighborValues :: !(V.Vector (VertexId, Double))
+    -- ^ Nonzero weights keyed by source vertex.
+  , naturalNeighborLocationStats :: !LocationStats
+    -- ^ Work performed while locating the query.
+  , naturalNeighborStats :: !InterpolationStats
+    -- ^ Work performed while constructing the Sibson coordinates.
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Minimum resident vertex by exact squared distance, then by resident handle,
+-- using an optional admitted vertex as the descent seed. Empty triangulations
+-- have no nearest vertex. Handle ties are deterministic within this mesh, not
+-- an identity invariant across differently constructed meshes.
+nearestNeighbor
+  :: Triangulation mode vertex directed undirected face
+  -> Maybe VertexId
+  -> QueryPoint
+  -> Maybe (VertexId, NearestStats)
+nearestNeighbor triangulation hint queryPoint
+  | numVertices triangulation == 0 = Nothing
+  | numConstraints triangulation == 0 = Just (descend start 0 0)
+  | otherwise = Just (completeArgmin start 0 0)
+ where
+  !query = queryPointValue queryPoint
+  !start = validateHint hint
+
+  validateHint (Just vertex)
+    | vertexIdIndex vertex < numVertices triangulation = vertex
+  validateHint _ = VertexId 0
+
+  compareDistance firstSite secondSite =
+    compareSquaredDistances
+      query
+      (vertexPoint triangulation firstSite)
+      (vertexPoint triangulation secondSite)
+
+  -- A Delaunay Voronoi cell is the intersection of its neighbour half-planes.
+  -- Outside it, some neighbour is strictly closer; the first such witness is
+  -- sufficient. Exact strict improvement is well-founded on the finite site
+  -- inventory, so no arbitrary walk budget can publish an unproved incumbent.
+  descend !current !steps !tests =
+    foldr
+      (inspectStar current steps)
+      (settle current steps)
+      (vertexOutgoingEdges triangulation current)
+      False
+      tests
+
+  inspectStar current steps edge remaining tied !tests =
+    let !candidate = destination triangulation edge
+        !nextTests = tests + 1
+     in case compareDistance candidate current of
+          LT -> descend candidate (steps + 1) nextTests
+          EQ -> remaining True nextTests
+          GT -> remaining tied nextTests
+
+  -- A local lower-handle tie is not the global minimum: a cocircular plateau
+  -- can hide it behind larger handles. Resolve ties by the complete inventory,
+  -- which is also the required schedule when constraints break the Voronoi
+  -- neighbour argument.
+  settle current steps tied tests
+    | tied = completeArgmin current steps tests
+    | otherwise = (current, NearestStats steps tests)
+
+  completeArgmin seed steps tests =
+    ( foldVertices' triangulation (chooseNearest seed) seed
+    , NearestStats steps (tests + numVertices triangulation - 1)
+    )
+
+  chooseNearest seed !best candidate
+    | candidate == seed = best
+    | otherwise =
+        case compareDistance candidate best of
+          LT -> candidate
+          EQ -> min candidate best
+          GT -> best
+
+-- | Barycentric weights at a validated query, plus point-location work.
+barycentricWeights
+  :: Triangulation mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> (BarycentricWeights, LocationStats)
+barycentricWeights triangulation hint queryPoint =
+  let !query = queryPointValue queryPoint
+      (!location, !stats) = locatePointWithHint triangulation hint queryPoint
+   in (weightsFor query location, stats)
+ where
+  weightsFor _ (OnVertex vertex) = OneWeight vertex
+  weightsFor query (OnEdge edge) =
+    let !factor = edgeDestinationWeight triangulation edge query
+     in TwoWeights (origin triangulation edge) (1 - factor) (destination triangulation edge) factor
+  weightsFor query (InFace face) =
+    case innerFaceVertices triangulation face of
+      Nothing -> NoWeights
+      Just (a, b, c) ->
+        case barycentricCoordinates
+          (vertexPoint triangulation a)
+          (vertexPoint triangulation b)
+          (vertexPoint triangulation c)
+          query of
+          Nothing -> NoWeights
+          Just (wa, wb, wc) -> ThreeWeights a wa b wb c wc
+  weightsFor _ EmptyTriangulation = NoWeights
+  weightsFor _ (OutsideConvexHull _) = NoWeights
+
+-- | Calculate Sibson coordinates using fixed-capacity reusable scratch storage.
+-- The only per-query heap object is the returned boxed vector.
+naturalNeighborWeights
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> ST s (NaturalNeighborResult)
+naturalNeighborWeights workspace hint queryPoint = do
+  (!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
+  values <- V.generateM count $ \index -> do
+    rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
+    weight <- MUV.unsafeRead (nnWeightValue workspace) index
+    pure (VertexId rawVertex, weight)
+  pure NaturalNeighborResult
+    { naturalNeighborValues = values
+    , naturalNeighborLocationStats = locationStats
+    , naturalNeighborStats = stats
+    }
+
+-- | Strictly fold the Sibson coordinates held in a reusable workspace. Unlike
+-- 'naturalNeighborWeights', this does not allocate a result vector. It is the
+-- canonical path for repeated interpolation and other reductions.
+foldNaturalNeighborWeights
+  :: (accumulator -> VertexId -> Double -> accumulator)
+  -> accumulator
+  -> NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> ST s (accumulator, LocationStats, InterpolationStats)
+foldNaturalNeighborWeights combine initial workspace hint query = do
+  (!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint query
+  value <- go 0 initial count
+  pure (value, locationStats, stats)
+ where
+  go !index !accumulator !count
+    | index >= count = pure accumulator
+    | otherwise = do
+        rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
+        weight <- MUV.unsafeRead (nnWeightValue workspace) index
+        let !nextAccumulator = combine accumulator (VertexId rawVertex) weight
+        go (index + 1) nextAccumulator count
+
+queryNaturalNeighborWorkspace
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> ST s (Int, LocationStats, InterpolationStats)
+queryNaturalNeighborWorkspace workspace hint queryPoint = do
+  let !triangulation = nnTriangulation workspace
+      !query = queryPointValue queryPoint
+      (!location, !locationStats) = locatePointWithHint triangulation hint queryPoint
+  (!count, !stats) <- case location of
+    OnVertex vertex -> do
+      writeWeight workspace 0 vertex 1
+      pure (1, InterpolationStats 0 1 0 False)
+    OnEdge edge
+      | isBoundaryEdge triangulation (asUndirected edge) -> do
+          let !factor = edgeDestinationWeight triangulation edge query
+          writeWeight workspace 0 (origin triangulation edge) (1 - factor)
+          writeWeight workspace 1 (destination triangulation edge) factor
+          pure (2, InterpolationStats 0 2 0 False)
+      | otherwise ->
+          let !left = incidentFace triangulation edge
+              !right = incidentFace triangulation (reverseEdge edge)
+              !start = if left /= outerFace then left else right
+           in sibsonQuery workspace query start
+    InFace face -> sibsonQuery workspace query face
+    EmptyTriangulation -> pure (0, InterpolationStats 0 0 0 False)
+    OutsideConvexHull _ -> pure (0, InterpolationStats 0 0 0 False)
+  pure (count, locationStats, stats)
+
+sibsonQuery
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Point
+  -> FaceId
+  -> ST s (Int, InterpolationStats)
+sibsonQuery workspace query startFace = do
+  generation <- nextFaceGeneration workspace
+  (cavityCount, faceTests) <- discoverCavity workspace generation query startFace
+  boundaryCount <- collectBoundary workspace generation cavityCount
+  orderedCount <- orderBoundary workspace boundaryCount
+  if orderedCount < 3
+    then fallbackBarycentric workspace query startFace faceTests cavityCount
+    else do
+      cellOkay <- buildInsertionCell workspace query orderedCount
+      if not cellOkay
+        then fallbackBarycentric workspace query startFace faceTests cavityCount
+        else do
+          weightCount <- buildStolenAreas workspace generation query orderedCount
+          normalized <- normalizeWeights workspace weightCount
+          if normalized
+            then pure (weightCount, InterpolationStats cavityCount weightCount faceTests False)
+            else fallbackBarycentric workspace query startFace faceTests cavityCount
+
+-- The query cavity is exactly the set of faces whose circumcircles contain the
+-- inserted point. Generation marks make clearing O(1).
+discoverCavity
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Point
+  -> FaceId
+  -> ST s (Int, Int)
+discoverCavity workspace generation query startFace@(FaceId rawStart) = do
+  MUV.unsafeWrite (nnFaceSeenMarks workspace) (faceIdIndex startFace) generation
+  MUV.unsafeWrite (nnFaceQueue workspace) 0 rawStart
+  go 1 0 0
+ where
+  !triangulation = nnTriangulation workspace
+  go !queueSize !cavitySize !tests
+    | queueSize <= 0 = pure (cavitySize, tests)
+    | otherwise = do
+        let !slot = queueSize - 1
+        rawFace <- MUV.unsafeRead (nnFaceQueue workspace) slot
+        let !face = FaceId rawFace
+            !inside = containsQuery triangulation query face
+            !tests' = tests + 1
+        if not inside
+          then go slot cavitySize tests'
+          else do
+            MUV.unsafeWrite (nnFaceMarks workspace) (faceIdIndex face) generation
+            MUV.unsafeWrite (nnCavityFaces workspace) cavitySize rawFace
+            nextQueue <- pushNeighbors triangulation workspace generation slot face
+            go nextQueue (cavitySize + 1) tests'
+
+containsQuery
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> FaceId
+  -> Bool
+containsQuery triangulation query face =
+  case innerFaceVertices triangulation face of
+    Nothing -> False
+    Just (a, b, c) ->
+      -- Strictly inside, matching spade's contained_in_circumference: an
+      -- exactly cocircular face is NOT part of the cavity, so its opposite
+      -- vertex is not a natural neighbour. A '/= LT' reading would include
+      -- such a vertex with a zero stolen area and diverge from spade's
+      -- neighbour set on cocircular queries.
+      inCircle
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        query
+        == GT
+
+pushNeighbors
+  :: Triangulation mode vertex directed undirected face
+  -> NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Int
+  -> FaceId
+  -> ST s Int
+pushNeighbors triangulation workspace generation start face =
+  case innerFaceDirectedEdges triangulation face of
+    Nothing -> pure start
+    Just (e0, e1, e2) -> do
+      size1 <- pushOne start e0
+      size2 <- pushOne size1 e1
+      pushOne size2 e2
+ where
+  pushOne !size edge =
+    let !adjacent@(FaceId raw) = incidentFace triangulation (reverseEdge edge)
+        !index = faceIdIndex adjacent
+     in if adjacent == outerFace
+          then pure size
+          else do
+            seen <- MUV.unsafeRead (nnFaceSeenMarks workspace) index
+            if seen == generation
+              then pure size
+              else do
+                MUV.unsafeWrite (nnFaceSeenMarks workspace) index generation
+                MUV.unsafeWrite (nnFaceQueue workspace) size raw
+                pure (size + 1)
+
+collectBoundary
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Int
+  -> ST s Int
+collectBoundary workspace generation cavityCount = goFaces 0 0
+ where
+  !triangulation = nnTriangulation workspace
+  goFaces !index !boundarySize
+    | index >= cavityCount = pure boundarySize
+    | otherwise = do
+        rawFace <- MUV.unsafeRead (nnCavityFaces workspace) index
+        nextSize <- case innerFaceDirectedEdges triangulation (FaceId rawFace) of
+          Nothing -> pure boundarySize
+          Just (e0, e1, e2) -> do
+            size1 <- appendIfBoundary boundarySize e0
+            size2 <- appendIfBoundary size1 e1
+            appendIfBoundary size2 e2
+        goFaces (index + 1) nextSize
+  appendIfBoundary !size edge = do
+    let adjacent = incidentFace triangulation (reverseEdge edge)
+    outside <- if adjacent == outerFace
+      then pure True
+      else (/= generation) <$> MUV.unsafeRead (nnFaceMarks workspace) (faceIdIndex adjacent)
+    if outside
+      then case edge of
+        DirectedEdgeId raw -> MUV.unsafeWrite (nnBoundaryEdges workspace) size raw >> pure (size + 1)
+      else pure size
+
+orderBoundary
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Int
+  -> ST s Int
+orderBoundary _ 0 = pure 0
+orderBoundary workspace count = do
+  generation <- nextOriginGeneration workspace
+  install generation 0
+  firstRaw <- MUV.unsafeRead (nnBoundaryEdges workspace) 0
+  follow generation firstRaw firstRaw 0
+ where
+  !triangulation = nnTriangulation workspace
+  install !generation !index
+    | index >= count = pure ()
+    | otherwise = do
+        rawEdge <- MUV.unsafeRead (nnBoundaryEdges workspace) index
+        let !from = vertexIdIndex (origin triangulation (DirectedEdgeId rawEdge))
+        MUV.unsafeWrite (nnOriginMarks workspace) from generation
+        MUV.unsafeWrite (nnOriginEdge workspace) from rawEdge
+        install generation (index + 1)
+  follow !generation !firstRaw !currentRaw !index
+    | index >= count =
+        if currentRaw == firstRaw then pure count else pure 0
+    | otherwise = do
+        MUV.unsafeWrite (nnOrderedEdges workspace) index currentRaw
+        let !to = vertexIdIndex (destination triangulation (DirectedEdgeId currentRaw))
+        marked <- MUV.unsafeRead (nnOriginMarks workspace) to
+        if marked /= generation
+          then pure 0
+          else do
+            nextRaw <- MUV.unsafeRead (nnOriginEdge workspace) to
+            if nextRaw == firstRaw && index + 1 == count
+              then pure count
+              else if nextRaw == firstRaw
+                then pure 0
+                else follow generation firstRaw nextRaw (index + 1)
+
+buildInsertionCell
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Point
+  -> Int
+  -> ST s Bool
+buildInsertionCell workspace query count = go 0
+ where
+  !triangulation = nnTriangulation workspace
+  go !index
+    | index >= count = pure True
+    | otherwise = do
+        rawEdge <- MUV.unsafeRead (nnOrderedEdges workspace) index
+        let !edge = DirectedEdgeId rawEdge
+            !from = vertexPoint triangulation (origin triangulation edge)
+            !to = vertexPoint triangulation (destination triangulation edge)
+        case circumcenter (subtractPoint to query) (subtractPoint from query) (Point 0 0) of
+          Nothing -> pure False
+          Just (Point x y)
+            | isFinite x && isFinite y -> do
+                MUV.unsafeWrite (nnInsertionX workspace) index x
+                MUV.unsafeWrite (nnInsertionY workspace) index y
+                go (index + 1)
+            | otherwise -> pure False
+
+buildStolenAreas
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Point
+  -> Int
+  -> ST s Int
+buildStolenAreas workspace generation query count = do
+  lastRaw <- MUV.unsafeRead (nnOrderedEdges workspace) (count - 1)
+  lastX <- MUV.unsafeRead (nnInsertionX workspace) (count - 1)
+  lastY <- MUV.unsafeRead (nnInsertionY workspace) (count - 1)
+  go 0 (DirectedEdgeId lastRaw) (Point lastX lastY)
+ where
+  !triangulation = nnTriangulation workspace
+  go !index !lastEdge !lastPoint
+    | index >= count = pure count
+    | otherwise = do
+        rawStop <- MUV.unsafeRead (nnOrderedEdges workspace) index
+        firstX <- MUV.unsafeRead (nnInsertionX workspace) index
+        firstY <- MUV.unsafeRead (nnInsertionY workspace) index
+        let !stopEdge = DirectedEdgeId rawStop
+            !first = Point firstX firstY
+        area <- stolenArea workspace generation query stopEdge first lastEdge lastPoint
+        case area of
+          Nothing -> pure 0
+          Just polygonArea -> do
+            writeWeight workspace index (origin triangulation stopEdge) polygonArea
+            go (index + 1) stopEdge first
+
+stolenArea
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Point
+  -> DirectedEdgeId
+  -> Point
+  -> DirectedEdgeId
+  -> Point
+  -> ST s (Maybe Double)
+stolenArea workspace generation query stopEdge first initialEdge initialPoint =
+  walk initialEdge initialPoint initialPositive initialNegative 0
+ where
+  !triangulation = nnTriangulation workspace
+  -- The boundary loop runs counterclockwise around the cavity, so the fan
+  -- walk around each boundary vertex necessarily runs clockwise: the
+  -- shoelace sequence first, lastPoint, circumcenters... is the clockwise
+  -- traversal of the stolen polygon, and positive - negative is the
+  -- NEGATED twice-area. The result is negated here so the returned value is
+  -- the positive twice-area 'normalizeWeights' requires. (spade's identical
+  -- walk leaves the sum negative and cancels the sign in its total; its
+  -- "ordered ccw" comment is wrong.)
+  !initialPositive = pointX first * pointY initialPoint
+  !initialNegative = pointY first * pointX initialPoint
+  !target = reverseEdge stopEdge
+  !limit = numDirectedEdges triangulation + 1
+  walk !lastEdge !lastPoint !positive !negative !steps
+    | steps >= limit = pure Nothing
+    | face == outerFace = pure Nothing
+    | otherwise = do
+        center <- cachedFaceCircumcenter workspace generation query face
+        case center of
+          Nothing -> pure Nothing
+          Just current ->
+            let !positive' = positive + pointX lastPoint * pointY current
+                !negative' = negative + pointY lastPoint * pointX current
+                !nextEdge = reverseEdge (next triangulation lastEdge)
+             in if nextEdge == target
+                  then
+                    let !closedPositive = positive' + pointX current * pointY first
+                        !closedNegative = negative' + pointY current * pointX first
+                     in pure (Just (closedNegative - closedPositive))
+                  else walk nextEdge current positive' negative' (steps + 1)
+   where
+    face = incidentFace triangulation lastEdge
+
+-- Each fan turns around the vertex two consecutive boundary edges share, so a
+-- cavity face is walked once per vertex it has on the boundary loop: twice for
+-- two, three times for a face whose whole triangle is on the loop, which is
+-- every face of a single-face cavity. Without this plane each of those visits
+-- pays two divisions and a dozen multiply-adds for a value already in hand.
+--
+-- The cached value is the one 'faceCircumcenterRelative' returned, stored as
+-- its own coordinates and handed back unchanged; nothing is recomputed from a
+-- rearrangement, so a hit and a miss are the same bits. Soundness needs only
+-- that the query point cannot change while an entry is readable, which the
+-- generation supplies: it is minted once per Sibson query and every stamp from
+-- an earlier query is strictly smaller.
+--
+-- A 'Nothing' is not recorded. It cannot repeat: the first one aborts this
+-- fan, and 'buildStolenAreas' abandons the query on the spot.
+cachedFaceCircumcenter
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Word32
+  -> Point
+  -> FaceId
+  -> ST s (Maybe (Point))
+cachedFaceCircumcenter workspace generation query face = do
+  stamp <- MUV.unsafeRead (nnCircumcenterMarks workspace) slot
+  if stamp == generation
+    then do
+      x <- MUV.unsafeRead (nnCircumcenterX workspace) slot
+      y <- MUV.unsafeRead (nnCircumcenterY workspace) slot
+      pure (Just (Point x y))
+    else case faceCircumcenterRelative (nnTriangulation workspace) query face of
+      Nothing -> pure Nothing
+      Just center@(Point x y) -> do
+        MUV.unsafeWrite (nnCircumcenterX workspace) slot x
+        MUV.unsafeWrite (nnCircumcenterY workspace) slot y
+        MUV.unsafeWrite (nnCircumcenterMarks workspace) slot generation
+        pure (Just center)
+ where
+  !slot = faceIdIndex face
+
+faceCircumcenterRelative
+  :: Triangulation mode vertex directed undirected face
+  -> Point
+  -> FaceId
+  -> Maybe (Point)
+faceCircumcenterRelative triangulation query face = do
+  (a, b, c) <- innerFaceVertices triangulation face
+  center <- circumcenter
+    (subtractPoint (vertexPoint triangulation a) query)
+    (subtractPoint (vertexPoint triangulation b) query)
+    (subtractPoint (vertexPoint triangulation c) query)
+  if finitePoint center then Just center else Nothing
+
+normalizeWeights
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Int
+  -> ST s Bool
+normalizeWeights _ 0 = pure False
+normalizeWeights workspace count = do
+  (!total, !minimumWeight, !finite) <- firstPass 0 0 0 True
+  let !tolerance = max 1.0e-12 (128 * scalarEpsilon)
+  if not finite || total == 0 || minimumWeight < negate tolerance
+    then pure False
+    else do
+      clampedTotal <- clampPass 0 0
+      if clampedTotal <= 0 || not (isFinite clampedTotal)
+        then pure False
+        else normalizePass 0 clampedTotal >> pure True
+ where
+  firstPass !index !total !minimumWeight !finite
+    | index >= count = pure (total, minimumWeight, finite)
+    | otherwise = do
+        weight <- MUV.unsafeRead (nnWeightValue workspace) index
+        let !minimumWeight' = if index == 0 then weight else min minimumWeight weight
+        firstPass (index + 1) (total + weight) minimumWeight' (finite && isFinite weight)
+  clampPass !index !total
+    | index >= count = pure total
+    | otherwise = do
+        weight <- MUV.unsafeRead (nnWeightValue workspace) index
+        let !clamped = max 0 weight
+        MUV.unsafeWrite (nnWeightValue workspace) index clamped
+        clampPass (index + 1) (total + clamped)
+  normalizePass !index !total
+    | index >= count = pure ()
+    | otherwise = do
+        weight <- MUV.unsafeRead (nnWeightValue workspace) index
+        MUV.unsafeWrite (nnWeightValue workspace) index (weight / total)
+        normalizePass (index + 1) total
+
+fallbackBarycentric
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Point
+  -> FaceId
+  -> Int
+  -> Int
+  -> ST s (Int, InterpolationStats)
+fallbackBarycentric workspace query face faceTests cavityCount =
+  case innerFaceVertices triangulation face of
+    Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
+    Just (a, b, c) ->
+      case barycentricCoordinates
+        (vertexPoint triangulation a)
+        (vertexPoint triangulation b)
+        (vertexPoint triangulation c)
+        query of
+        Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
+        Just (wa, wb, wc) -> do
+          writeWeight workspace 0 a wa
+          writeWeight workspace 1 b wb
+          writeWeight workspace 2 c wc
+          pure (3, InterpolationStats cavityCount 3 faceTests True)
+ where
+  triangulation = nnTriangulation workspace
+
+writeWeight
+  :: NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Int
+  -> VertexId
+  -> Double
+  -> ST s ()
+writeWeight workspace index (VertexId vertex) weight = do
+  MUV.unsafeWrite (nnWeightVertex workspace) index vertex
+  MUV.unsafeWrite (nnWeightValue workspace) index weight
+
+-- | Sample the nearest vertex, or return 'Nothing' for an empty mesh.
+interpolateNearest
+  :: (VertexId -> value)
+  -> Triangulation mode vertex directed undirected face
+  -> Maybe VertexId
+  -> QueryPoint
+  -> Maybe value
+interpolateNearest sample triangulation hint query = sample . fst <$> nearestNeighbor triangulation hint query
+
+-- | Interpolate scalar vertex samples in the cell containing the query.
+interpolateBarycentric
+  :: (VertexId -> Double)
+  -> Triangulation mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> Maybe Double
+interpolateBarycentric sample triangulation hint query =
+  case fst (barycentricWeights triangulation hint query) of
+    NoWeights -> Nothing
+    OneWeight vertex -> Just (sample vertex)
+    TwoWeights a wa b wb -> Just (wa * sample a + wb * sample b)
+    ThreeWeights a wa b wb c wc -> Just (wa * sample a + wb * sample b + wc * sample c)
+
+-- | Interpolate from reusable Sibson weights.
+interpolateNaturalNeighbor
+  :: (VertexId -> Double)
+  -> NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> ST s (Maybe Double, InterpolationStats)
+interpolateNaturalNeighbor sample workspace hint query = do
+  (total, _, stats) <-
+    foldNaturalNeighborWeights
+      (\accumulator vertex weight -> accumulator + weight * sample vertex)
+      0
+      workspace
+      hint
+      query
+  pure (if interpolationNaturalNeighbors stats == 0 then Nothing else Just total, stats)
+
+-- | Estimate one vertex gradient from its Delaunay neighbors.
+estimateGradient
+  :: (VertexId -> Double)
+  -> Triangulation mode vertex directed undirected face
+  -> VertexId
+  -> (Double, Double)
+estimateGradient sample triangulation vertex =
+  case vertexOutEdge triangulation vertex of
+    Nothing -> (0, 0)
+    Just start ->
+      let !nextEdge = counterClockwise triangulation start
+       in if nextEdge == start
+            then (0, 0)
+            else
+              let (!nx, !ny, !nz) = walk start start 0 0 0 0
+               in if nz /= 0 && isFinite nz then (-nx / nz, -ny / nz) else (0, 0)
+ where
+  Point vx vy = vertexPoint triangulation vertex
+  !vz = sample vertex
+  !bound = numDirectedEdges triangulation + 1
+
+  walk !start !current !steps !sumX !sumY !sumZ
+    | steps >= bound = (sumX, sumY, sumZ)
+    | otherwise =
+        let !nextEdge = counterClockwise triangulation current
+            !leftVertex = destination triangulation current
+            !rightVertex = destination triangulation nextEdge
+            (!nextX, !nextY, !nextZ) = accumulate sumX sumY sumZ leftVertex rightVertex
+         in if nextEdge == start
+              then (nextX, nextY, nextZ)
+              else walk start nextEdge (steps + 1) nextX nextY nextZ
+
+  accumulate !sumX !sumY !sumZ leftVertex rightVertex =
+    let Point lx ly = vertexPoint triangulation leftVertex
+        Point rx ry = vertexPoint triangulation rightVertex
+        !lz = sample leftVertex
+        !rz = sample rightVertex
+        !d0x = lx - vx
+        !d0y = ly - vy
+        !d0z = lz - vz
+        !d1x = rx - vx
+        !d1y = ry - vy
+        !d1z = rz - vz
+        !normalX = d0y * d1z - d0z * d1y
+        !normalY = d0z * d1x - d0x * d1z
+        !normalZ = d0x * d1y - d0y * d1x
+     in if normalZ > 0
+          then (sumX + normalX, sumY + normalY, sumZ + normalZ)
+          else (sumX, sumY, sumZ)
+
+-- | Estimate gradients for every vertex in handle order.
+estimateGradients
+  :: (VertexId -> Double)
+  -> Triangulation mode vertex directed undirected face
+  -> V.Vector (Double, Double)
+estimateGradients sample triangulation =
+  V.generate (numVertices triangulation) $ \index ->
+    estimateGradient sample triangulation (VertexId (fromIntegral index))
+
+-- | Natural-neighbor interpolation with nodal-gradient correction.
+interpolateNaturalNeighborGradient
+  :: (VertexId -> Double)
+  -> (VertexId -> (Double, Double))
+  -> Double
+  -> NaturalNeighborWorkspace s mode vertex directed undirected face
+  -> Maybe LocationHint
+  -> QueryPoint
+  -> ST s (Maybe Double, InterpolationStats)
+interpolateNaturalNeighborGradient sample gradient flatness workspace hint queryPoint
+  | flatness < 0 || not (isFinite flatness) = pure (Nothing, InterpolationStats 0 0 0 False)
+  | otherwise = do
+      (!count, _, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
+      if count == 0
+        then pure (Nothing, stats)
+        else do
+          accumulation <- accumulate 0 count Nothing (0, 0, 0, 0, 0)
+          pure (finish accumulation, stats)
+ where
+  !query = queryPointValue queryPoint
+  !triangulation = nnTriangulation workspace
+
+  -- 'flatness' is 0.5 or 1 in practice and @**@ is an exp/log pair per natural
+  -- neighbour per query, so both are answered directly. The two shortcuts do
+  -- not stand on the same ground.
+  --
+  -- The identity is exact. @x@ raised to 1 is @x@, which is already a float,
+  -- and rounding an exactly representable result admits only that result, so
+  -- any implementation faithful to within an ulp returns it.
+  --
+  -- The square root is not exact and does not need to be. IEEE-754 mandates a
+  -- correctly rounded 'sqrt'; @pow@ is a recommended operation carrying no such
+  -- requirement. Where the two disagree it is therefore in the last ulp, and it
+  -- is 'sqrt' that holds the correctly rounded answer — this substitution can
+  -- only move the result toward it. Against this platform's libm they in fact
+  -- agree on every non-negative binary32 and on every non-negative binary64
+  -- sampled, which is a measurement and not a proof.
+  --
+  -- They part company at negative zero, where @pow@ answers @+0@ and 'sqrt'
+  -- answers @-0@. A sum of two squares is never negative zero, and were it one
+  -- the sole consumer below tests @== 0@, which both zeroes satisfy alike.
+  raiseToFlatness squared
+    | flatness == 0.5 = sqrt squared
+    | flatness == 1 = squared
+    | otherwise = squared ** flatness
+
+  accumulate !index !count !exact !totals
+    | index >= count = pure (exact, totals)
+    | otherwise = do
+        rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
+        weight <- MUV.unsafeRead (nnWeightValue workspace) index
+        let !vertex = VertexId rawVertex
+            !point = vertexPoint triangulation vertex
+            !exact' =
+              case exact of
+                Just _ -> exact
+                Nothing
+                  | query == point -> Just (sample vertex)
+                  | otherwise -> Nothing
+            !totals' = contribution totals vertex weight
+        accumulate (index + 1) count exact' totals'
+
+  finish
+    :: (Maybe Double, (Double, Double, Double, Double, Double))
+    -> Maybe Double
+  finish (Just value, _) = Just value
+  finish (Nothing, (!sumC0, !sumC1, !sumC1Weights, !alphaNumerator, !beta))
+    | sumC1Weights == 0 = Just sumC0
+    | otherwise =
+        let !alpha = alphaNumerator / sumC1Weights
+            !c1 = sumC1 / sumC1Weights
+            !denominator = alpha + beta
+         in if denominator == 0 || not (isFinite denominator)
+              then Just sumC0
+              else Just ((alpha * sumC0 + beta * c1) / denominator)
+
+  contribution (!sumC0, !sumC1, !sumC1Weights, !alpha, !beta) vertex weight =
+    let Point qx qy = query
+        Point vx vy = vertexPoint triangulation vertex
+        !dx = qx - vx
+        !dy = qy - vy
+        !radiusSquared = dx * dx + dy * dy
+        !radiusPower = raiseToFlatness radiusSquared
+        !c1Weight = if radiusPower == 0 then 0 else weight / radiusPower
+        (!gx, !gy) = gradient vertex
+        !height = sample vertex
+        !zeta = height + dx * gx + dy * gy
+     in ( sumC0 + height * weight
+        , sumC1 + zeta * c1Weight
+        , sumC1Weights + c1Weight
+        , alpha + c1Weight * radiusSquared
+        , beta + weight * radiusSquared
+        )
+
+subtractPoint :: Point -> Point -> Point
+subtractPoint (Point ax ay) (Point bx by) = Point (ax - bx) (ay - by)
+{-# INLINE subtractPoint #-}
+
+finitePoint :: Point -> Bool
+finitePoint (Point x y) = isFinite x && isFinite y
+{-# INLINE finitePoint #-}
+
+-- | Weight of a directed edge's destination for a query on that edge: the
+-- projection factor clamped into the segment, so the origin carries the rest.
+edgeDestinationWeight
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedEdgeId
+  -> Point
+  -> Double
+edgeDestinationWeight triangulation edge query =
+  clamp
+    0
+    1
+    ( projectionFactor
+        (vertexPoint triangulation (origin triangulation edge))
+        (vertexPoint triangulation (destination triangulation edge))
+        query
+    )
+
+clamp :: Ord value => value -> value -> value -> value
+clamp low high = max low . min high
+{-# INLINE clamp #-}
diff --git a/src-dual/Moonlight/Planar/Voronoi.hs b/src-dual/Moonlight/Planar/Voronoi.hs
new file mode 100644
--- /dev/null
+++ b/src-dual/Moonlight/Planar/Voronoi.hs
@@ -0,0 +1,277 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | The Voronoi dual, addressed by its own handle family: every cell, edge and
+-- vertex is a view of the Delaunay mesh rather than a second structure.
+module Moonlight.Planar.Voronoi
+  ( VoronoiFaceId (..)
+  , DirectedVoronoiEdgeId (..)
+  , UndirectedVoronoiEdgeId (..)
+  , VoronoiVertexId (..)
+  , VoronoiEdgeGeometry (..)
+  , voronoiFaces
+  , directedVoronoiEdges
+  , undirectedVoronoiEdges
+  , asDelaunayDirectedEdge
+  , asDelaunayUndirectedEdge
+  , asDirectedVoronoiEdge
+  , asUndirectedVoronoiEdge
+  , reverseVoronoiEdge
+  , voronoiNext
+  , voronoiPrevious
+  , voronoiFrom
+  , voronoiTo
+  , voronoiIncidentFace
+  , voronoiFaceSite
+  , voronoiFaceAdjacentEdges
+  , voronoiVertexPosition
+  , voronoiVertexOutgoingEdges
+  , voronoiDirectionVector
+  , voronoiEdgeGeometry
+  , faceCircumcenter
+  ) where
+
+import Control.DeepSeq (NFData)
+import Moonlight.Planar.Dcel
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Handles.Iterators.FixedIterators (directedEdges, undirectedEdges, vertices)
+import Moonlight.Planar.Math
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..))
+
+-- | A Voronoi face, identified by its primal site.
+--
+-- The dual uses the exact same fixed-index space as the primal DCEL. These
+-- newtypes add semantic separation without allocating or owning any dual mesh.
+newtype VoronoiFaceId = VoronoiFaceId { unVoronoiFaceId :: VertexId }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | An oriented Voronoi edge, identified by its primal directed edge.
+newtype DirectedVoronoiEdgeId = DirectedVoronoiEdgeId { unDirectedVoronoiEdgeId :: DirectedEdgeId }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | An unoriented Voronoi edge, identified by its primal undirected edge.
+newtype UndirectedVoronoiEdgeId = UndirectedVoronoiEdgeId { unUndirectedVoronoiEdgeId :: UndirectedEdgeId }
+  deriving stock (Show)
+  deriving newtype (Eq, Ord, NFData)
+
+-- | A finite dual vertex or an ideal endpoint of an unbounded dual edge.
+data VoronoiVertexId
+  = InnerVoronoiVertex !FaceId
+  | OuterVoronoiVertex !DirectedVoronoiEdgeId
+  deriving stock (Eq, Ord, Show)
+
+-- | Finite segment, half-infinite ray, or infinite line in the dual.
+data VoronoiEdgeGeometry
+  = VoronoiSegment !(Point) !(Point)
+  | VoronoiRay !(Point) !(Point)
+  | VoronoiLine !(Point) !(Point)
+  deriving stock (Eq, Ord, Show)
+
+-- | Voronoi faces in primal vertex-handle order.
+voronoiFaces :: Triangulation mode vertex directed undirected face -> [VoronoiFaceId]
+voronoiFaces triangulation = map VoronoiFaceId (vertices triangulation)
+{-# INLINE voronoiFaces #-}
+
+-- | Directed Voronoi edges in primal directed-edge order.
+--
+-- Every primal directed edge is one directed edge in the dual. Boundary
+-- handles naturally represent half-infinite edges through OuterVoronoiVertex.
+directedVoronoiEdges :: Triangulation mode vertex directed undirected face -> [DirectedVoronoiEdgeId]
+directedVoronoiEdges triangulation = map DirectedVoronoiEdgeId (directedEdges triangulation)
+{-# INLINE directedVoronoiEdges #-}
+
+-- | Undirected Voronoi edges in primal undirected-edge order.
+undirectedVoronoiEdges :: Triangulation mode vertex directed undirected face -> [UndirectedVoronoiEdgeId]
+undirectedVoronoiEdges triangulation = map UndirectedVoronoiEdgeId (undirectedEdges triangulation)
+{-# INLINE undirectedVoronoiEdges #-}
+
+-- | Recover the primal directed edge.
+asDelaunayDirectedEdge :: DirectedVoronoiEdgeId -> DirectedEdgeId
+asDelaunayDirectedEdge = unDirectedVoronoiEdgeId
+{-# INLINE asDelaunayDirectedEdge #-}
+
+-- | Recover the primal undirected edge.
+asDelaunayUndirectedEdge :: UndirectedVoronoiEdgeId -> UndirectedEdgeId
+asDelaunayUndirectedEdge = unUndirectedVoronoiEdgeId
+{-# INLINE asDelaunayUndirectedEdge #-}
+
+-- | View a primal directed edge in the dual handle family.
+asDirectedVoronoiEdge :: DirectedEdgeId -> DirectedVoronoiEdgeId
+asDirectedVoronoiEdge = DirectedVoronoiEdgeId
+{-# INLINE asDirectedVoronoiEdge #-}
+
+-- | View a primal undirected edge in the dual handle family.
+asUndirectedVoronoiEdge :: UndirectedEdgeId -> UndirectedVoronoiEdgeId
+asUndirectedVoronoiEdge = UndirectedVoronoiEdgeId
+{-# INLINE asUndirectedVoronoiEdge #-}
+
+-- | Reverse a directed Voronoi edge.
+reverseVoronoiEdge :: DirectedVoronoiEdgeId -> DirectedVoronoiEdgeId
+reverseVoronoiEdge (DirectedVoronoiEdgeId edge) = DirectedVoronoiEdgeId (reverseEdge edge)
+{-# INLINE reverseVoronoiEdge #-}
+
+-- | Next dual edge around the incident Voronoi face.
+--
+-- Dual next/previous rotate around the primal origin site.
+voronoiNext
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> DirectedVoronoiEdgeId
+voronoiNext triangulation (DirectedVoronoiEdgeId edge) =
+  DirectedVoronoiEdgeId (counterClockwise triangulation edge)
+{-# INLINE voronoiNext #-}
+
+-- | Previous dual edge around the incident Voronoi face.
+voronoiPrevious
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> DirectedVoronoiEdgeId
+voronoiPrevious triangulation (DirectedVoronoiEdgeId edge) =
+  DirectedVoronoiEdgeId (clockwise triangulation edge)
+{-# INLINE voronoiPrevious #-}
+
+-- | Origin dual vertex, including ideal endpoints on the hull.
+voronoiFrom
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> VoronoiVertexId
+voronoiFrom triangulation edge@(DirectedVoronoiEdgeId primal)
+  | face == outerFace = OuterVoronoiVertex edge
+  | otherwise = InnerVoronoiVertex face
+ where
+  face = incidentFace triangulation primal
+{-# INLINE voronoiFrom #-}
+
+-- | Destination dual vertex, including ideal endpoints on the hull.
+voronoiTo
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> VoronoiVertexId
+voronoiTo triangulation = voronoiFrom triangulation . reverseVoronoiEdge
+{-# INLINE voronoiTo #-}
+
+-- | Voronoi face to the left of a directed dual edge.
+voronoiIncidentFace
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> VoronoiFaceId
+voronoiIncidentFace triangulation (DirectedVoronoiEdgeId edge) =
+  VoronoiFaceId (origin triangulation edge)
+{-# INLINE voronoiIncidentFace #-}
+
+-- | Primal site represented by a Voronoi face.
+voronoiFaceSite :: VoronoiFaceId -> VertexId
+voronoiFaceSite = unVoronoiFaceId
+{-# INLINE voronoiFaceSite #-}
+
+-- | Directed boundary of a Voronoi face.
+voronoiFaceAdjacentEdges
+  :: Triangulation mode vertex directed undirected face
+  -> VoronoiFaceId
+  -> [DirectedVoronoiEdgeId]
+voronoiFaceAdjacentEdges triangulation (VoronoiFaceId site) =
+  map DirectedVoronoiEdgeId (vertexOutgoingEdges triangulation site)
+{-# INLINE voronoiFaceAdjacentEdges #-}
+
+-- | Position of a finite dual vertex; ideal hull endpoints have no position.
+--
+-- A caller reaches this through 'voronoiFrom' or 'voronoiTo', which build the
+-- endpoint sum immediately before it is taken apart again. Only an unfolding at
+-- the consumer lets the two meet, so the constructor never reaches the heap.
+voronoiVertexPosition
+  :: Triangulation mode vertex directed undirected face
+  -> VoronoiVertexId
+  -> Maybe (Point)
+voronoiVertexPosition triangulation vertex = case vertex of
+  InnerVoronoiVertex face -> faceCircumcenter triangulation face
+  OuterVoronoiVertex _ -> Nothing
+{-# INLINE voronoiVertexPosition #-}
+
+-- | Directed dual edges leaving a finite dual vertex.
+voronoiVertexOutgoingEdges
+  :: Triangulation mode vertex directed undirected face
+  -> VoronoiVertexId
+  -> Maybe [DirectedVoronoiEdgeId]
+voronoiVertexOutgoingEdges triangulation vertex = case vertex of
+  OuterVoronoiVertex _ -> Nothing
+  InnerVoronoiVertex face ->
+    map DirectedVoronoiEdgeId . faceDirectedEdges triangulation <$> nonOuter face
+ where
+  nonOuter face
+    | face == outerFace = Nothing
+    | otherwise = Just face
+{-# INLINE voronoiVertexOutgoingEdges #-}
+
+-- | Unit direction of the dual edge orthogonal to a primal edge.
+voronoiDirectionVector
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> Point
+voronoiDirectionVector triangulation (DirectedVoronoiEdgeId edge) =
+  case vertexPoint triangulation (origin triangulation edge) of
+    Point ax ay -> case vertexPoint triangulation (destination triangulation edge) of
+      Point bx by -> Point (ay - by) (bx - ax)
+
+-- | Circumcenter of a bounded primal face.
+--
+-- The circumcentre stands on the absolute vertex positions, which is a
+-- different value from the query-relative one the Sibson pipeline caches: that
+-- one rescales the differences it was handed, so translating the inputs moves
+-- the rounding and the two do not differ by the translation. The interpolation
+-- workspace's plane therefore cannot serve this function, and the repetition
+-- here is across calls rather than within one — a sweep recomputes each face
+-- once per incident dual edge, while a single call touches two distinct faces.
+-- That threefold repetition is what a dual which is a view rather than a
+-- structure costs, and the referent pays it identically, so no cache is owed.
+faceCircumcenter
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> Maybe (Point)
+faceCircumcenter triangulation face
+  | face == outerFace = Nothing
+  | otherwise = case adjacentEdge triangulation face of
+      Nothing -> Nothing
+      Just e0 ->
+        let !e1 = next triangulation e0
+            !e2 = next triangulation e1
+         in if next triangulation e2 /= e0
+              then Nothing
+              else
+                circumcenter
+                  (vertexPoint triangulation (origin triangulation e0))
+                  (vertexPoint triangulation (origin triangulation e1))
+                  (vertexPoint triangulation (origin triangulation e2))
+{-# INLINE faceCircumcenter #-}
+
+-- | Geometric realization of one dual edge.
+--
+-- The endpoint classification 'voronoiFrom' and 'voronoiTo' publish is two face
+-- reads and two comparisons; taken through those observations it is also two
+-- sum values built and immediately scrutinized. The geometry reads the faces
+-- itself so that the classification stays in registers.
+voronoiEdgeGeometry
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> Maybe (VoronoiEdgeGeometry)
+voronoiEdgeGeometry triangulation edge@(DirectedVoronoiEdgeId primal)
+  | innerFrom, innerTo =
+      VoronoiSegment <$> faceCircumcenter triangulation fromFace <*> faceCircumcenter triangulation toFace
+  | innerFrom = do
+      start <- faceCircumcenter triangulation fromFace
+      pure (VoronoiRay start (normalizeVector (voronoiDirectionVector triangulation edge)))
+  | innerTo = do
+      end <- faceCircumcenter triangulation toFace
+      pure (VoronoiRay end (normalizeVector (negateVector (voronoiDirectionVector triangulation edge))))
+  | otherwise =
+      let !center = midpoint
+            (vertexPoint triangulation (origin triangulation primal))
+            (vertexPoint triangulation (destination triangulation primal))
+       in Just (VoronoiLine center (normalizeVector (voronoiDirectionVector triangulation edge)))
+ where
+  !fromFace = incidentFace triangulation primal
+  !toFace = incidentFace triangulation (reverseEdge primal)
+  !innerFrom = fromFace /= outerFace
+  !innerTo = toFace /= outerFace
diff --git a/src-dual/Moonlight/Planar/Voronoi/Handles.hs b/src-dual/Moonlight/Planar/Voronoi/Handles.hs
new file mode 100644
--- /dev/null
+++ b/src-dual/Moonlight/Planar/Voronoi/Handles.hs
@@ -0,0 +1,284 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Zero-copy Voronoi views that retain the owning triangulation.
+module Moonlight.Planar.Voronoi.Handles
+  ( VoronoiFaceHandle
+  , DirectedVoronoiEdgeHandle
+  , UndirectedVoronoiEdgeHandle
+  , VoronoiVertexHandle
+  , voronoiFaceHandle
+  , directedVoronoiEdgeHandle
+  , undirectedVoronoiEdgeHandle
+  , vertexAsVoronoiFaceH
+  , directedEdgeAsVoronoiH
+  , undirectedEdgeAsVoronoiH
+  , innerFaceAsVoronoiVertexH
+  , fixVoronoiFace
+  , fixDirectedVoronoiEdge
+  , fixUndirectedVoronoiEdge
+  , fixVoronoiVertex
+  , voronoiEdgeReverseH
+  , voronoiEdgeNextH
+  , voronoiEdgePreviousH
+  , voronoiEdgeFromH
+  , voronoiEdgeToH
+  , voronoiEdgeFaceH
+  , voronoiEdgeAsUndirectedH
+  , voronoiEdgeAsDelaunayH
+  , voronoiFaceSiteH
+  , voronoiFaceAdjacentEdgesH
+  , voronoiVertexPositionH
+  , voronoiVertexOutEdgesH
+  , voronoiVertexAsDelaunayFaceH
+  , voronoiVertexAsOuterEdgeH
+  , voronoiEdgeDirectionH
+  , voronoiEdgeGeometryH
+  ) where
+
+import Moonlight.Planar.Handles.Dynamic
+import Moonlight.Planar.Math (midpoint, negateVector, normalizeVector)
+import Moonlight.Planar.Types
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Voronoi
+
+-- | Zero-copy dual handles. Each dual value contains exactly one primal handle,
+-- so it cannot accidentally combine an identifier from one triangulation with
+-- the storage of another.
+newtype VoronoiFaceHandle mode vertex directed undirected face =
+  VoronoiFaceHandle (VertexHandle mode vertex directed undirected face)
+
+-- | An owning handle to a directed dual edge.
+newtype DirectedVoronoiEdgeHandle mode vertex directed undirected face =
+  DirectedVoronoiEdgeHandle (DirectedEdgeHandle mode vertex directed undirected face)
+
+-- | An owning handle to an undirected dual edge.
+newtype UndirectedVoronoiEdgeHandle mode vertex directed undirected face =
+  UndirectedVoronoiEdgeHandle (UndirectedEdgeHandle mode vertex directed undirected face)
+
+-- | An owning handle to a finite or ideal dual vertex.
+data VoronoiVertexHandle mode vertex directed undirected face
+  = InnerVoronoiVertexHandle
+      !(FaceHandle InnerTag mode vertex directed undirected face)
+  | OuterVoronoiVertexHandle
+      !(DirectedVoronoiEdgeHandle mode vertex directed undirected face)
+
+instance Show (VoronoiFaceHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixVoronoiFace
+
+instance Show (DirectedVoronoiEdgeHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixDirectedVoronoiEdge
+
+instance Show (UndirectedVoronoiEdgeHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixUndirectedVoronoiEdge
+
+instance Show (VoronoiVertexHandle mode vertex directed undirected face) where
+  showsPrec precedence = showsPrec precedence . fixVoronoiVertex
+
+-- | Admit a fixed Voronoi face identifier into a triangulation.
+voronoiFaceHandle
+  :: Triangulation mode vertex directed undirected face
+  -> VoronoiFaceId
+  -> Maybe (VoronoiFaceHandle mode vertex directed undirected face)
+voronoiFaceHandle triangulation (VoronoiFaceId site) =
+  VoronoiFaceHandle <$> vertexHandle triangulation site
+
+-- | View an admitted primal vertex as its Voronoi face.
+vertexAsVoronoiFaceH
+  :: VertexHandle mode vertex directed undirected face
+  -> VoronoiFaceHandle mode vertex directed undirected face
+vertexAsVoronoiFaceH = VoronoiFaceHandle
+
+-- | Admit a fixed directed Voronoi edge identifier into a triangulation.
+directedVoronoiEdgeHandle
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+  -> Maybe (DirectedVoronoiEdgeHandle mode vertex directed undirected face)
+directedVoronoiEdgeHandle triangulation (DirectedVoronoiEdgeId edge) =
+  DirectedVoronoiEdgeHandle <$> directedEdgeHandle triangulation edge
+
+-- | View an admitted primal directed edge in the dual handle family.
+directedEdgeAsVoronoiH
+  :: DirectedEdgeHandle mode vertex directed undirected face
+  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
+directedEdgeAsVoronoiH = DirectedVoronoiEdgeHandle
+
+-- | Admit a fixed undirected Voronoi edge identifier into a triangulation.
+undirectedVoronoiEdgeHandle
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedVoronoiEdgeId
+  -> Maybe (UndirectedVoronoiEdgeHandle mode vertex directed undirected face)
+undirectedVoronoiEdgeHandle triangulation (UndirectedVoronoiEdgeId edge) =
+  UndirectedVoronoiEdgeHandle <$> undirectedEdgeHandle triangulation edge
+
+-- | View an admitted primal undirected edge in the dual handle family.
+undirectedEdgeAsVoronoiH
+  :: UndirectedEdgeHandle mode vertex directed undirected face
+  -> UndirectedVoronoiEdgeHandle mode vertex directed undirected face
+undirectedEdgeAsVoronoiH = UndirectedVoronoiEdgeHandle
+
+-- | View an admitted bounded primal face as a finite dual vertex.
+innerFaceAsVoronoiVertexH
+  :: FaceHandle InnerTag mode vertex directed undirected face
+  -> VoronoiVertexHandle mode vertex directed undirected face
+innerFaceAsVoronoiVertexH = InnerVoronoiVertexHandle
+
+-- | Forget ownership and recover a fixed Voronoi face identifier.
+fixVoronoiFace :: VoronoiFaceHandle mode vertex directed undirected face -> VoronoiFaceId
+fixVoronoiFace (VoronoiFaceHandle site) = VoronoiFaceId (fixVertex site)
+{-# INLINE fixVoronoiFace #-}
+
+-- | Forget ownership and recover a fixed directed dual edge identifier.
+fixDirectedVoronoiEdge
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> DirectedVoronoiEdgeId
+fixDirectedVoronoiEdge (DirectedVoronoiEdgeHandle edge) =
+  DirectedVoronoiEdgeId (fixDirectedEdge edge)
+{-# INLINE fixDirectedVoronoiEdge #-}
+
+-- | Forget ownership and recover a fixed undirected dual edge identifier.
+fixUndirectedVoronoiEdge
+  :: UndirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> UndirectedVoronoiEdgeId
+fixUndirectedVoronoiEdge (UndirectedVoronoiEdgeHandle edge) =
+  UndirectedVoronoiEdgeId (fixUndirectedEdge edge)
+{-# INLINE fixUndirectedVoronoiEdge #-}
+
+-- | Forget ownership and recover a fixed dual vertex identifier.
+fixVoronoiVertex
+  :: VoronoiVertexHandle mode vertex directed undirected face
+  -> VoronoiVertexId
+fixVoronoiVertex (InnerVoronoiVertexHandle face) =
+  InnerVoronoiVertex (fixedFaceId (fixFace face))
+fixVoronoiVertex (OuterVoronoiVertexHandle edge) =
+  OuterVoronoiVertex (fixDirectedVoronoiEdge edge)
+{-# INLINE fixVoronoiVertex #-}
+
+-- | Reverse an admitted directed dual edge.
+voronoiEdgeReverseH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
+voronoiEdgeReverseH (DirectedVoronoiEdgeHandle edge) =
+  DirectedVoronoiEdgeHandle (directedEdgeReverse edge)
+
+-- | Next admitted dual edge around its Voronoi face.
+voronoiEdgeNextH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
+voronoiEdgeNextH (DirectedVoronoiEdgeHandle edge) =
+  DirectedVoronoiEdgeHandle (directedEdgeCounterClockwise edge)
+
+-- | Previous admitted dual edge around its Voronoi face.
+voronoiEdgePreviousH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
+voronoiEdgePreviousH (DirectedVoronoiEdgeHandle edge) =
+  DirectedVoronoiEdgeHandle (directedEdgeClockwise edge)
+
+-- | Origin finite or ideal vertex of an admitted dual edge.
+voronoiEdgeFromH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> VoronoiVertexHandle mode vertex directed undirected face
+voronoiEdgeFromH edge@(DirectedVoronoiEdgeHandle primal) =
+  case faceAsInner (directedEdgeFace primal) of
+    Just inner -> InnerVoronoiVertexHandle inner
+    Nothing -> OuterVoronoiVertexHandle edge
+
+-- | Destination finite or ideal vertex of an admitted dual edge.
+voronoiEdgeToH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> VoronoiVertexHandle mode vertex directed undirected face
+voronoiEdgeToH = voronoiEdgeFromH . voronoiEdgeReverseH
+
+-- | Voronoi face to the left of an admitted directed dual edge.
+voronoiEdgeFaceH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> VoronoiFaceHandle mode vertex directed undirected face
+voronoiEdgeFaceH (DirectedVoronoiEdgeHandle edge) =
+  VoronoiFaceHandle (directedEdgeFrom edge)
+
+-- | Forget orientation while retaining ownership.
+voronoiEdgeAsUndirectedH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> UndirectedVoronoiEdgeHandle mode vertex directed undirected face
+voronoiEdgeAsUndirectedH (DirectedVoronoiEdgeHandle edge) =
+  UndirectedVoronoiEdgeHandle (directedEdgeAsUndirected edge)
+
+-- | Recover the owning primal directed-edge handle.
+voronoiEdgeAsDelaunayH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> DirectedEdgeHandle mode vertex directed undirected face
+voronoiEdgeAsDelaunayH (DirectedVoronoiEdgeHandle edge) = edge
+
+-- | Recover the owning primal site handle.
+voronoiFaceSiteH
+  :: VoronoiFaceHandle mode vertex directed undirected face
+  -> VertexHandle mode vertex directed undirected face
+voronoiFaceSiteH (VoronoiFaceHandle site) = site
+
+-- | Admitted directed boundary of a Voronoi face.
+voronoiFaceAdjacentEdgesH
+  :: VoronoiFaceHandle mode vertex directed undirected face
+  -> [DirectedVoronoiEdgeHandle mode vertex directed undirected face]
+voronoiFaceAdjacentEdgesH (VoronoiFaceHandle site) =
+  map DirectedVoronoiEdgeHandle (vertexHandleOutEdges site)
+
+-- | Position of a finite admitted dual vertex.
+voronoiVertexPositionH
+  :: VoronoiVertexHandle mode vertex directed undirected face
+  -> Maybe (Point)
+voronoiVertexPositionH (InnerVoronoiVertexHandle face) = innerFaceCircumcenter face
+voronoiVertexPositionH (OuterVoronoiVertexHandle _) = Nothing
+
+-- | Directed dual edges leaving a finite admitted dual vertex.
+voronoiVertexOutEdgesH
+  :: VoronoiVertexHandle mode vertex directed undirected face
+  -> Maybe [DirectedVoronoiEdgeHandle mode vertex directed undirected face]
+voronoiVertexOutEdgesH (InnerVoronoiVertexHandle face) =
+  Just (map DirectedVoronoiEdgeHandle (faceAdjacentEdges face))
+voronoiVertexOutEdgesH (OuterVoronoiVertexHandle _) = Nothing
+
+-- | Recover the primal bounded face underlying a finite dual vertex.
+voronoiVertexAsDelaunayFaceH
+  :: VoronoiVertexHandle mode vertex directed undirected face
+  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
+voronoiVertexAsDelaunayFaceH (InnerVoronoiVertexHandle face) = Just face
+voronoiVertexAsDelaunayFaceH (OuterVoronoiVertexHandle _) = Nothing
+
+-- | Recover the primal hull edge underlying an ideal dual vertex.
+voronoiVertexAsOuterEdgeH
+  :: VoronoiVertexHandle mode vertex directed undirected face
+  -> Maybe (DirectedVoronoiEdgeHandle mode vertex directed undirected face)
+voronoiVertexAsOuterEdgeH (InnerVoronoiVertexHandle _) = Nothing
+voronoiVertexAsOuterEdgeH (OuterVoronoiVertexHandle edge) = Just edge
+
+-- | Unit direction of the dual edge orthogonal to the primal edge.
+voronoiEdgeDirectionH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> Point
+voronoiEdgeDirectionH (DirectedVoronoiEdgeHandle primal) =
+  let (Point ax ay, Point bx by) = directedEdgePositions primal
+   in Point (ay - by) (bx - ax)
+
+-- | Geometric realization of an admitted dual edge.
+voronoiEdgeGeometryH
+  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
+  -> Maybe (VoronoiEdgeGeometry)
+voronoiEdgeGeometryH edge@(DirectedVoronoiEdgeHandle primal) =
+  case (voronoiEdgeFromH edge, voronoiEdgeToH edge) of
+    (InnerVoronoiVertexHandle fromFace, InnerVoronoiVertexHandle toFace) ->
+      VoronoiSegment <$> innerFaceCircumcenter fromFace <*> innerFaceCircumcenter toFace
+    (InnerVoronoiVertexHandle fromFace, OuterVoronoiVertexHandle _) -> do
+      start <- innerFaceCircumcenter fromFace
+      pure (VoronoiRay start (normalizeVector (voronoiEdgeDirectionH edge)))
+    (OuterVoronoiVertexHandle _, InnerVoronoiVertexHandle toFace) -> do
+      end <- innerFaceCircumcenter toFace
+      pure (VoronoiRay end (normalizeVector (negateVector (voronoiEdgeDirectionH edge))))
+    (OuterVoronoiVertexHandle _, OuterVoronoiVertexHandle _) ->
+      let (from, to) = directedEdgePositions primal
+       in Just (VoronoiLine (midpoint from to) (normalizeVector (voronoiEdgeDirectionH edge)))
+
+-- The handle verbs cross the same component boundary as the fixed-index ones
+-- and carry their unfoldings for the same reason. 'voronoiVertexPositionH'
+-- reaches its arithmetic through 'innerFaceCircumcenter', whose own worker
+-- publishes no unfolding, so it stops at that call until the dcel layer says
+-- otherwise.
diff --git a/src-dual/Moonlight/Triangulation/Internal/InterpolationWorkspace.hs b/src-dual/Moonlight/Triangulation/Internal/InterpolationWorkspace.hs
deleted file mode 100644
--- a/src-dual/Moonlight/Triangulation/Internal/InterpolationWorkspace.hs
+++ /dev/null
@@ -1,152 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE KindSignatures #-}
-{-# LANGUAGE TypeFamilies #-}
-
-module Moonlight.Triangulation.Internal.InterpolationWorkspace
-  ( NaturalNeighborWorkspace (..)
-  , newNaturalNeighborWorkspace
-  , nextFaceGeneration
-  , nextOriginGeneration
-  , workspaceBytes
-  ) where
-
-import Control.Monad.Primitive (PrimMonad, PrimState)
-import Control.Monad.ST (ST)
-import Data.Primitive.MutVar
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Dcel (numDirectedEdges, numFaces, numVertices)
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types (ConstraintMode)
-import Moonlight.Triangulation.Scalar (scalarByteSize)
-
--- | Reusable scratch space for Sibson interpolation. It borrows no mutable
--- topology: the immutable triangulation is the sole owner, while these arrays
--- are query-local marks, queues and numeric work buffers.
-data NaturalNeighborWorkspace state (mode :: ConstraintMode) vertex directed undirected face = NaturalNeighborWorkspace
-  { nnTriangulation :: !(Triangulation mode vertex directed undirected face)
-  , nnFaceMarks :: !(MUV.MVector state Word32)
-  , nnFaceSeenMarks :: !(MUV.MVector state Word32)
-    -- The circumcentre plane is stamped by the same generation as the two mark
-    -- planes above, and that is what makes it sound: a generation is minted
-    -- once per Sibson query, and the circumcentre a face contributes is a
-    -- function of the face and the query point together. Holding the plane to
-    -- exactly one generation's lifetime holds the query point fixed for as long
-    -- as an entry can be read.
-  , nnCircumcenterMarks :: !(MUV.MVector state Word32)
-  , nnFaceGeneration :: !(MutVar state Word32)
-  , nnCircumcenterX :: !(MUV.MVector state Double)
-  , nnCircumcenterY :: !(MUV.MVector state Double)
-  , nnFaceQueue :: !(MUV.MVector state Word32)
-  , nnCavityFaces :: !(MUV.MVector state Word32)
-  , nnBoundaryEdges :: !(MUV.MVector state Word32)
-  , nnOrderedEdges :: !(MUV.MVector state Word32)
-  , nnOriginMarks :: !(MUV.MVector state Word32)
-  , nnOriginGeneration :: !(MutVar state Word32)
-  , nnOriginEdge :: !(MUV.MVector state Word32)
-  , nnInsertionX :: !(MUV.MVector state Double)
-  , nnInsertionY :: !(MUV.MVector state Double)
-  , nnWeightVertex :: !(MUV.MVector state Word32)
-  , nnWeightValue :: !(MUV.MVector state Double)
-  }
-
--- | Allocate reusable interpolation storage sized to one triangulation.
-newNaturalNeighborWorkspace
-  :: PrimMonad m
-  => Triangulation mode vertex directed undirected face
-  -> m (NaturalNeighborWorkspace (PrimState m) mode vertex directed undirected face)
-newNaturalNeighborWorkspace triangulation = do
-  let !faceCapacity = max 1 (numFaces triangulation)
-      !edgeCapacity = max 1 (numDirectedEdges triangulation)
-      !vertexCapacity = max 1 (numVertices triangulation)
-  faceMarks <- MUV.replicate faceCapacity 0
-  faceSeenMarks <- MUV.replicate faceCapacity 0
-  circumcenterMarks <- MUV.replicate faceCapacity 0
-  faceGeneration <- newMutVar 0
-  circumcenterX <- MUV.new faceCapacity
-  circumcenterY <- MUV.new faceCapacity
-  faceQueue <- MUV.new faceCapacity
-  cavityFaces <- MUV.new faceCapacity
-  boundaryEdges <- MUV.new edgeCapacity
-  orderedEdges <- MUV.new edgeCapacity
-  originMarks <- MUV.replicate vertexCapacity 0
-  originGeneration <- newMutVar 0
-  originEdge <- MUV.new vertexCapacity
-  insertionX <- MUV.new edgeCapacity
-  insertionY <- MUV.new edgeCapacity
-  weightVertex <- MUV.new edgeCapacity
-  weightValue <- MUV.new edgeCapacity
-  pure NaturalNeighborWorkspace
-    { nnTriangulation = triangulation
-    , nnFaceMarks = faceMarks
-    , nnFaceSeenMarks = faceSeenMarks
-    , nnCircumcenterMarks = circumcenterMarks
-    , nnFaceGeneration = faceGeneration
-    , nnCircumcenterX = circumcenterX
-    , nnCircumcenterY = circumcenterY
-    , nnFaceQueue = faceQueue
-    , nnCavityFaces = cavityFaces
-    , nnBoundaryEdges = boundaryEdges
-    , nnOrderedEdges = orderedEdges
-    , nnOriginMarks = originMarks
-    , nnOriginGeneration = originGeneration
-    , nnOriginEdge = originEdge
-    , nnInsertionX = insertionX
-    , nnInsertionY = insertionY
-    , nnWeightVertex = weightVertex
-    , nnWeightValue = weightValue
-    }
-
-nextFaceGeneration :: NaturalNeighborWorkspace s mode vertex directed undirected face -> ST s Word32
-nextFaceGeneration workspace = do
-  current <- readMutVar (nnFaceGeneration workspace)
-  let !next = current + 1
-  if next == 0
-    then do
-      MUV.set (nnFaceMarks workspace) 0
-      MUV.set (nnFaceSeenMarks workspace) 0
-      MUV.set (nnCircumcenterMarks workspace) 0
-      writeMutVar (nnFaceGeneration workspace) 1
-      pure 1
-    else writeMutVar (nnFaceGeneration workspace) next >> pure next
-
-nextOriginGeneration :: NaturalNeighborWorkspace s mode vertex directed undirected face -> ST s Word32
-nextOriginGeneration workspace = nextGeneration (nnOriginMarks workspace) (nnOriginGeneration workspace)
-
-nextGeneration :: MUV.MVector s Word32 -> MutVar s Word32 -> ST s Word32
-nextGeneration marks reference = do
-  current <- readMutVar reference
-  let !next = current + 1
-  if next == 0
-    then do
-      MUV.set marks 0
-      writeMutVar reference 1
-      pure 1
-    else writeMutVar reference next >> pure next
-
--- | Bytes owned by the reusable numeric and index planes.
-workspaceBytes
-  :: NaturalNeighborWorkspace state mode vertex directed undirected face
-  -> Integer
-workspaceBytes workspace =
-  4 * toInteger wordSlots
-    + toInteger scalarByteSize * toInteger scalarSlots
- where
-  wordSlots =
-    MUV.length (nnFaceMarks workspace)
-      + MUV.length (nnFaceSeenMarks workspace)
-      + MUV.length (nnCircumcenterMarks workspace)
-      + MUV.length (nnFaceQueue workspace)
-      + MUV.length (nnCavityFaces workspace)
-      + MUV.length (nnBoundaryEdges workspace)
-      + MUV.length (nnOrderedEdges workspace)
-      + MUV.length (nnOriginMarks workspace)
-      + MUV.length (nnOriginEdge workspace)
-      + MUV.length (nnWeightVertex workspace)
-  scalarSlots =
-    MUV.length (nnCircumcenterX workspace)
-      + MUV.length (nnCircumcenterY workspace)
-      + MUV.length (nnInsertionX workspace)
-      + MUV.length (nnInsertionY workspace)
-      + MUV.length (nnWeightValue workspace)
diff --git a/src-dual/Moonlight/Triangulation/Interpolation.hs b/src-dual/Moonlight/Triangulation/Interpolation.hs
deleted file mode 100644
--- a/src-dual/Moonlight/Triangulation/Interpolation.hs
+++ /dev/null
@@ -1,899 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TypeFamilies #-}
-
--- | Nearest, barycentric, and natural-neighbor interpolation over one
--- authoritative triangulation.
-module Moonlight.Triangulation.Interpolation
-  ( BarycentricWeights (..)
-  , InterpolationStats (..)
-  , NaturalNeighborResult (..)
-  , NaturalNeighborWorkspace
-  , newNaturalNeighborWorkspace
-  , workspaceBytes
-  , nearestNeighbor
-  , barycentricWeights
-  , naturalNeighborWeights
-  , foldNaturalNeighborWeights
-  , interpolateNearest
-  , interpolateBarycentric
-  , interpolateNaturalNeighbor
-  , estimateGradient
-  , estimateGradients
-  , interpolateNaturalNeighborGradient
-  ) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad.ST (ST)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32)
-import Moonlight.Triangulation.Dcel
-  ( destination
-  , counterClockwise
-  , foldVertexOutgoingEdges'
-  , incidentFace
-  , innerFaceDirectedEdges
-  , innerFaceVertices
-  , isBoundaryEdge
-  , next
-  , numConstraints
-  , numDirectedEdges
-  , numVertices
-  , origin
-  , outerFace
-  , vertexOutEdge
-  , vertexPoint
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId (..)
-  , asUndirected
-  , FaceId (..)
-  , faceIdIndex
-  , VertexId (..)
-  , reverseEdge
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Internal.InterpolationWorkspace
-import Moonlight.Triangulation.Internal.Predicates (inCircle)
-import Moonlight.Triangulation.Math
-  ( barycentricCoordinates
-  , circumcenter
-  , isFinite
-  , projectionFactor
-  , squaredDistanceWide
-  )
-import Moonlight.Triangulation.PointLocation (locatePointWithHint)
-import Moonlight.Triangulation.Scalar (scalarEpsilon)
-import Moonlight.Triangulation.Types
-  ( Location (..)
-  , LocationHint
-  , LocationStats
-  , NearestStats (..)
-  , Point (..)
-  , QueryPoint
-  , queryPointValue
-  , Triangulation
-  )
-import GHC.Generics (Generic)
-
--- | Barycentric weights use the mesh's binary64 coordinate domain.
-data BarycentricWeights
-  = NoWeights
-  | OneWeight !VertexId
-  | TwoWeights !VertexId !Double !VertexId !Double
-  | ThreeWeights !VertexId !Double !VertexId !Double !VertexId !Double
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Work performed by one natural-neighbor query.
-data InterpolationStats = InterpolationStats
-  { interpolationCavityFaces :: {-# UNPACK #-} !Int
-    -- ^ Faces in the query insertion cavity.
-  , interpolationNaturalNeighbors :: {-# UNPACK #-} !Int
-    -- ^ Sites contributing nonzero Sibson weight.
-  , interpolationFaceTests :: {-# UNPACK #-} !Int
-    -- ^ Faces tested while discovering the cavity.
-    -- | 'True' exactly when the Sibson pipeline declined and the returned
-    -- weights are the barycentric coordinates of the located face instead.
-    -- A silent degradation from Sibson to barycentric is a defect that hides;
-    -- this bit is the announcement.
-  , interpolationUsedFallback :: !Bool
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Weights and receipts produced by one natural-neighbor query.
-data NaturalNeighborResult = NaturalNeighborResult
-  { naturalNeighborValues :: !(V.Vector (VertexId, Double))
-    -- ^ Nonzero weights keyed by source vertex.
-  , naturalNeighborLocationStats :: !LocationStats
-    -- ^ Work performed while locating the query.
-  , naturalNeighborStats :: !InterpolationStats
-    -- ^ Work performed while constructing the Sibson coordinates.
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Nearest vertex and query work, using an optional admitted vertex as the
--- descent seed. Empty triangulations have no nearest vertex.
-nearestNeighbor
-  :: Triangulation mode vertex directed undirected face
-  -> Maybe VertexId
-  -> QueryPoint
-  -> Maybe (VertexId, NearestStats)
-nearestNeighbor triangulation hint queryPoint
-  | numVertices triangulation == 0 = Nothing
-  | numConstraints triangulation == 0 = Just (descend start startDistance 0 0)
-  | otherwise = Just (walk start startDistance 0 0)
- where
-  !query = queryPointValue queryPoint
-  !start = validateHint hint
-  !startDistance = squaredDistanceWide query (vertexPoint triangulation start)
-  !bound = numDirectedEdges triangulation + numVertices triangulation + 1
-
-  validateHint (Just vertex)
-    | vertexIdIndex vertex < numVertices triangulation = vertex
-  validateHint _ = VertexId 0
-
-  -- A vertex's Voronoi cell is the intersection of the half-planes its
-  -- Delaunay neighbours induce, so a query outside that cell violates one of
-  -- them and some neighbour is strictly closer. Descending on the /first/
-  -- strict improvement therefore lands on the same vertex as descending on the
-  -- best one, having tested about half of the star on average instead of all
-  -- of it. The argument needs the Delaunay property, so a constrained
-  -- triangulation keeps the exhaustive scan below.
-  descend !current !currentDistance !steps !tests
-    | steps >= bound = (current, NearestStats steps tests)
-    | otherwise =
-        case vertexOutEdge triangulation current of
-          Nothing -> (current, NearestStats steps tests)
-          Just ring -> revolve current currentDistance steps ring ring Nothing tests
-
-  -- Equal-distance neighbours are not an improvement, so they cannot end the
-  -- revolution early; the lowest such handle is carried to the end and taken
-  -- only if nothing strictly closer appeared. That is the same deterministic
-  -- tie the exhaustive scan settles on.
-  revolve !current !currentDistance !steps !edge !ring !tie !tests
-    | candidateDistance < currentDistance =
-        descend candidate candidateDistance (steps + 1) nextTests
-    | nextEdge == ring =
-        case nextTie of
-          Just settled -> descend settled currentDistance (steps + 1) nextTests
-          Nothing -> (current, NearestStats steps nextTests)
-    | otherwise = revolve current currentDistance steps nextEdge ring nextTie nextTests
-   where
-    !candidate = destination triangulation edge
-    !candidateDistance = squaredDistanceWide query (vertexPoint triangulation candidate)
-    !nextTests = tests + 1
-    !nextEdge = counterClockwise triangulation edge
-    !nextTie
-      | candidateDistance == currentDistance && candidate < current =
-          case tie of
-            Just held | held <= candidate -> tie
-            _ -> Just candidate
-      | otherwise = tie
-
-  walk !current !currentDistance !steps !tests
-    | steps >= bound = (current, NearestStats steps tests)
-    | otherwise =
-        let (!candidate, !candidateDistance, !newTests) =
-              foldVertexOutgoingEdges'
-                triangulation
-                current
-                inspect
-                (current, currentDistance, tests)
-         in if candidate == current
-              then (current, NearestStats steps newTests)
-              else walk candidate candidateDistance (steps + 1) newTests
-
-  inspect (!best, !bestDistance, !tests) edge =
-    let !candidate = destination triangulation edge
-        !candidateDistance = squaredDistanceWide query (vertexPoint triangulation candidate)
-        !isBetter =
-          candidateDistance < bestDistance
-            || (candidateDistance == bestDistance && candidate < best)
-     in if isBetter
-          then (candidate, candidateDistance, tests + 1)
-          else (best, bestDistance, tests + 1)
-
--- | Barycentric weights at a validated query, plus point-location work.
-barycentricWeights
-  :: Triangulation mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> (BarycentricWeights, LocationStats)
-barycentricWeights triangulation hint queryPoint =
-  let !query = queryPointValue queryPoint
-      (!location, !stats) = locatePointWithHint triangulation hint queryPoint
-   in (weightsFor query location, stats)
- where
-  weightsFor _ (OnVertex vertex) = OneWeight vertex
-  weightsFor query (OnEdge edge) =
-    let !from = origin triangulation edge
-        !to = destination triangulation edge
-        !factor = clamp 0 1 (projectionFactor (vertexPoint triangulation from) (vertexPoint triangulation to) query)
-     in TwoWeights from (1 - factor) to factor
-  weightsFor query (InFace face) =
-    case innerFaceVertices triangulation face of
-      Nothing -> NoWeights
-      Just (a, b, c) ->
-        case barycentricCoordinates
-          (vertexPoint triangulation a)
-          (vertexPoint triangulation b)
-          (vertexPoint triangulation c)
-          query of
-          Nothing -> NoWeights
-          Just (wa, wb, wc) -> ThreeWeights a wa b wb c wc
-  weightsFor _ EmptyTriangulation = NoWeights
-  weightsFor _ (OutsideConvexHull _) = NoWeights
-
--- | Calculate Sibson coordinates using fixed-capacity reusable scratch storage.
--- The only per-query heap object is the returned boxed vector.
-naturalNeighborWeights
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> ST s (NaturalNeighborResult)
-naturalNeighborWeights workspace hint queryPoint = do
-  (!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
-  values <- V.generateM count $ \index -> do
-    rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
-    weight <- MUV.unsafeRead (nnWeightValue workspace) index
-    pure (VertexId rawVertex, weight)
-  pure NaturalNeighborResult
-    { naturalNeighborValues = values
-    , naturalNeighborLocationStats = locationStats
-    , naturalNeighborStats = stats
-    }
-
--- | Strictly fold the Sibson coordinates held in a reusable workspace. Unlike
--- 'naturalNeighborWeights', this does not allocate a result vector. It is the
--- canonical path for repeated interpolation and other reductions.
-foldNaturalNeighborWeights
-  :: (accumulator -> VertexId -> Double -> accumulator)
-  -> accumulator
-  -> NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> ST s (accumulator, LocationStats, InterpolationStats)
-foldNaturalNeighborWeights combine initial workspace hint query = do
-  (!count, !locationStats, !stats) <- queryNaturalNeighborWorkspace workspace hint query
-  value <- go 0 initial count
-  pure (value, locationStats, stats)
- where
-  go !index !accumulator !count
-    | index >= count = pure accumulator
-    | otherwise = do
-        rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
-        weight <- MUV.unsafeRead (nnWeightValue workspace) index
-        let !nextAccumulator = combine accumulator (VertexId rawVertex) weight
-        go (index + 1) nextAccumulator count
-
-queryNaturalNeighborWorkspace
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> ST s (Int, LocationStats, InterpolationStats)
-queryNaturalNeighborWorkspace workspace hint queryPoint = do
-  let !triangulation = nnTriangulation workspace
-      !query = queryPointValue queryPoint
-      (!location, !locationStats) = locatePointWithHint triangulation hint queryPoint
-  (!count, !stats) <- case location of
-    OnVertex vertex -> do
-      writeWeight workspace 0 vertex 1
-      pure (1, InterpolationStats 0 1 0 False)
-    OnEdge edge
-      | isBoundaryEdge triangulation (asUndirected edge) -> do
-          let !from = origin triangulation edge
-              !to = destination triangulation edge
-              !factor = clamp 0 1 (projectionFactor (vertexPoint triangulation from) (vertexPoint triangulation to) query)
-          writeWeight workspace 0 from (1 - factor)
-          writeWeight workspace 1 to factor
-          pure (2, InterpolationStats 0 2 0 False)
-      | otherwise ->
-          let !left = incidentFace triangulation edge
-              !right = incidentFace triangulation (reverseEdge edge)
-              !start = if left /= outerFace then left else right
-           in sibsonQuery workspace query start
-    InFace face -> sibsonQuery workspace query face
-    EmptyTriangulation -> pure (0, InterpolationStats 0 0 0 False)
-    OutsideConvexHull _ -> pure (0, InterpolationStats 0 0 0 False)
-  pure (count, locationStats, stats)
-
-sibsonQuery
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Point
-  -> FaceId
-  -> ST s (Int, InterpolationStats)
-sibsonQuery workspace query startFace = do
-  generation <- nextFaceGeneration workspace
-  (cavityCount, faceTests) <- discoverCavity workspace generation query startFace
-  boundaryCount <- collectBoundary workspace generation cavityCount
-  orderedCount <- orderBoundary workspace boundaryCount
-  if orderedCount < 3
-    then fallbackBarycentric workspace query startFace faceTests cavityCount
-    else do
-      cellOkay <- buildInsertionCell workspace query orderedCount
-      if not cellOkay
-        then fallbackBarycentric workspace query startFace faceTests cavityCount
-        else do
-          weightCount <- buildStolenAreas workspace generation query orderedCount
-          normalized <- normalizeWeights workspace weightCount
-          if normalized
-            then pure (weightCount, InterpolationStats cavityCount weightCount faceTests False)
-            else fallbackBarycentric workspace query startFace faceTests cavityCount
-
--- The query cavity is exactly the set of faces whose circumcircles contain the
--- inserted point. Generation marks make clearing O(1).
-discoverCavity
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Point
-  -> FaceId
-  -> ST s (Int, Int)
-discoverCavity workspace generation query startFace@(FaceId rawStart) = do
-  MUV.unsafeWrite (nnFaceSeenMarks workspace) (faceIdIndex startFace) generation
-  MUV.unsafeWrite (nnFaceQueue workspace) 0 rawStart
-  go 1 0 0
- where
-  !triangulation = nnTriangulation workspace
-  go !queueSize !cavitySize !tests
-    | queueSize <= 0 = pure (cavitySize, tests)
-    | otherwise = do
-        let !slot = queueSize - 1
-        rawFace <- MUV.unsafeRead (nnFaceQueue workspace) slot
-        let !face = FaceId rawFace
-            !inside = containsQuery triangulation query face
-            !tests' = tests + 1
-        if not inside
-          then go slot cavitySize tests'
-          else do
-            MUV.unsafeWrite (nnFaceMarks workspace) (faceIdIndex face) generation
-            MUV.unsafeWrite (nnCavityFaces workspace) cavitySize rawFace
-            nextQueue <- pushNeighbors triangulation workspace generation slot face
-            go nextQueue (cavitySize + 1) tests'
-
-containsQuery
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> FaceId
-  -> Bool
-containsQuery triangulation query face =
-  case innerFaceVertices triangulation face of
-    Nothing -> False
-    Just (a, b, c) ->
-      -- Strictly inside, matching spade's contained_in_circumference: an
-      -- exactly cocircular face is NOT part of the cavity, so its opposite
-      -- vertex is not a natural neighbour. A '/= LT' reading would include
-      -- such a vertex with a zero stolen area and diverge from spade's
-      -- neighbour set on cocircular queries.
-      inCircle
-        (vertexPoint triangulation a)
-        (vertexPoint triangulation b)
-        (vertexPoint triangulation c)
-        query
-        == GT
-
-pushNeighbors
-  :: Triangulation mode vertex directed undirected face
-  -> NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Int
-  -> FaceId
-  -> ST s Int
-pushNeighbors triangulation workspace generation start face =
-  case innerFaceDirectedEdges triangulation face of
-    Nothing -> pure start
-    Just (e0, e1, e2) -> do
-      size1 <- pushOne start e0
-      size2 <- pushOne size1 e1
-      pushOne size2 e2
- where
-  pushOne !size edge =
-    let !adjacent@(FaceId raw) = incidentFace triangulation (reverseEdge edge)
-        !index = faceIdIndex adjacent
-     in if adjacent == outerFace
-          then pure size
-          else do
-            seen <- MUV.unsafeRead (nnFaceSeenMarks workspace) index
-            if seen == generation
-              then pure size
-              else do
-                MUV.unsafeWrite (nnFaceSeenMarks workspace) index generation
-                MUV.unsafeWrite (nnFaceQueue workspace) size raw
-                pure (size + 1)
-
-collectBoundary
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Int
-  -> ST s Int
-collectBoundary workspace generation cavityCount = goFaces 0 0
- where
-  !triangulation = nnTriangulation workspace
-  goFaces !index !boundarySize
-    | index >= cavityCount = pure boundarySize
-    | otherwise = do
-        rawFace <- MUV.unsafeRead (nnCavityFaces workspace) index
-        nextSize <- case innerFaceDirectedEdges triangulation (FaceId rawFace) of
-          Nothing -> pure boundarySize
-          Just (e0, e1, e2) -> do
-            size1 <- appendIfBoundary boundarySize e0
-            size2 <- appendIfBoundary size1 e1
-            appendIfBoundary size2 e2
-        goFaces (index + 1) nextSize
-  appendIfBoundary !size edge = do
-    let adjacent = incidentFace triangulation (reverseEdge edge)
-    outside <- if adjacent == outerFace
-      then pure True
-      else (/= generation) <$> MUV.unsafeRead (nnFaceMarks workspace) (faceIdIndex adjacent)
-    if outside
-      then case edge of
-        DirectedEdgeId raw -> MUV.unsafeWrite (nnBoundaryEdges workspace) size raw >> pure (size + 1)
-      else pure size
-
-orderBoundary
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Int
-  -> ST s Int
-orderBoundary _ 0 = pure 0
-orderBoundary workspace count = do
-  generation <- nextOriginGeneration workspace
-  install generation 0
-  firstRaw <- MUV.unsafeRead (nnBoundaryEdges workspace) 0
-  follow generation firstRaw firstRaw 0
- where
-  !triangulation = nnTriangulation workspace
-  install !generation !index
-    | index >= count = pure ()
-    | otherwise = do
-        rawEdge <- MUV.unsafeRead (nnBoundaryEdges workspace) index
-        let !from = vertexIdIndex (origin triangulation (DirectedEdgeId rawEdge))
-        MUV.unsafeWrite (nnOriginMarks workspace) from generation
-        MUV.unsafeWrite (nnOriginEdge workspace) from rawEdge
-        install generation (index + 1)
-  follow !generation !firstRaw !currentRaw !index
-    | index >= count =
-        if currentRaw == firstRaw then pure count else pure 0
-    | otherwise = do
-        MUV.unsafeWrite (nnOrderedEdges workspace) index currentRaw
-        let !to = vertexIdIndex (destination triangulation (DirectedEdgeId currentRaw))
-        marked <- MUV.unsafeRead (nnOriginMarks workspace) to
-        if marked /= generation
-          then pure 0
-          else do
-            nextRaw <- MUV.unsafeRead (nnOriginEdge workspace) to
-            if nextRaw == firstRaw && index + 1 == count
-              then pure count
-              else if nextRaw == firstRaw
-                then pure 0
-                else follow generation firstRaw nextRaw (index + 1)
-
-buildInsertionCell
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Point
-  -> Int
-  -> ST s Bool
-buildInsertionCell workspace query count = go 0
- where
-  !triangulation = nnTriangulation workspace
-  go !index
-    | index >= count = pure True
-    | otherwise = do
-        rawEdge <- MUV.unsafeRead (nnOrderedEdges workspace) index
-        let !edge = DirectedEdgeId rawEdge
-            !from = vertexPoint triangulation (origin triangulation edge)
-            !to = vertexPoint triangulation (destination triangulation edge)
-        case circumcenter (subtractPoint to query) (subtractPoint from query) (Point 0 0) of
-          Nothing -> pure False
-          Just (Point x y)
-            | isFinite x && isFinite y -> do
-                MUV.unsafeWrite (nnInsertionX workspace) index x
-                MUV.unsafeWrite (nnInsertionY workspace) index y
-                go (index + 1)
-            | otherwise -> pure False
-
-buildStolenAreas
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Point
-  -> Int
-  -> ST s Int
-buildStolenAreas workspace generation query count = do
-  lastRaw <- MUV.unsafeRead (nnOrderedEdges workspace) (count - 1)
-  lastX <- MUV.unsafeRead (nnInsertionX workspace) (count - 1)
-  lastY <- MUV.unsafeRead (nnInsertionY workspace) (count - 1)
-  go 0 (DirectedEdgeId lastRaw) (Point lastX lastY)
- where
-  !triangulation = nnTriangulation workspace
-  go !index !lastEdge !lastPoint
-    | index >= count = pure count
-    | otherwise = do
-        rawStop <- MUV.unsafeRead (nnOrderedEdges workspace) index
-        firstX <- MUV.unsafeRead (nnInsertionX workspace) index
-        firstY <- MUV.unsafeRead (nnInsertionY workspace) index
-        let !stopEdge = DirectedEdgeId rawStop
-            !first = Point firstX firstY
-        area <- stolenArea workspace generation query stopEdge first lastEdge lastPoint
-        case area of
-          Nothing -> pure 0
-          Just polygonArea -> do
-            writeWeight workspace index (origin triangulation stopEdge) polygonArea
-            go (index + 1) stopEdge first
-
-stolenArea
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Point
-  -> DirectedEdgeId
-  -> Point
-  -> DirectedEdgeId
-  -> Point
-  -> ST s (Maybe Double)
-stolenArea workspace generation query stopEdge first initialEdge initialPoint =
-  walk initialEdge initialPoint initialPositive initialNegative 0
- where
-  !triangulation = nnTriangulation workspace
-  -- The boundary loop runs counterclockwise around the cavity, so the fan
-  -- walk around each boundary vertex necessarily runs clockwise: the
-  -- shoelace sequence first, lastPoint, circumcenters... is the clockwise
-  -- traversal of the stolen polygon, and positive - negative is the
-  -- NEGATED twice-area. The result is negated here so the returned value is
-  -- the positive twice-area 'normalizeWeights' requires. (spade's identical
-  -- walk leaves the sum negative and cancels the sign in its total; its
-  -- "ordered ccw" comment is wrong.)
-  !initialPositive = pointX first * pointY initialPoint
-  !initialNegative = pointY first * pointX initialPoint
-  !target = reverseEdge stopEdge
-  !limit = numDirectedEdges triangulation + 1
-  walk !lastEdge !lastPoint !positive !negative !steps
-    | steps >= limit = pure Nothing
-    | face == outerFace = pure Nothing
-    | otherwise = do
-        center <- cachedFaceCircumcenter workspace generation query face
-        case center of
-          Nothing -> pure Nothing
-          Just current ->
-            let !positive' = positive + pointX lastPoint * pointY current
-                !negative' = negative + pointY lastPoint * pointX current
-                !nextEdge = reverseEdge (next triangulation lastEdge)
-             in if nextEdge == target
-                  then
-                    let !closedPositive = positive' + pointX current * pointY first
-                        !closedNegative = negative' + pointY current * pointX first
-                     in pure (Just (closedNegative - closedPositive))
-                  else walk nextEdge current positive' negative' (steps + 1)
-   where
-    face = incidentFace triangulation lastEdge
-
--- Each fan turns around the vertex two consecutive boundary edges share, so a
--- cavity face is walked once per vertex it has on the boundary loop: twice for
--- two, three times for a face whose whole triangle is on the loop, which is
--- every face of a single-face cavity. Without this plane each of those visits
--- pays two divisions and a dozen multiply-adds for a value already in hand.
---
--- The cached value is the one 'faceCircumcenterRelative' returned, stored as
--- its own coordinates and handed back unchanged; nothing is recomputed from a
--- rearrangement, so a hit and a miss are the same bits. Soundness needs only
--- that the query point cannot change while an entry is readable, which the
--- generation supplies: it is minted once per Sibson query and every stamp from
--- an earlier query is strictly smaller.
---
--- A 'Nothing' is not recorded. It cannot repeat: the first one aborts this
--- fan, and 'buildStolenAreas' abandons the query on the spot.
-cachedFaceCircumcenter
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Word32
-  -> Point
-  -> FaceId
-  -> ST s (Maybe (Point))
-cachedFaceCircumcenter workspace generation query face = do
-  stamp <- MUV.unsafeRead (nnCircumcenterMarks workspace) slot
-  if stamp == generation
-    then do
-      x <- MUV.unsafeRead (nnCircumcenterX workspace) slot
-      y <- MUV.unsafeRead (nnCircumcenterY workspace) slot
-      pure (Just (Point x y))
-    else case faceCircumcenterRelative (nnTriangulation workspace) query face of
-      Nothing -> pure Nothing
-      Just center@(Point x y) -> do
-        MUV.unsafeWrite (nnCircumcenterX workspace) slot x
-        MUV.unsafeWrite (nnCircumcenterY workspace) slot y
-        MUV.unsafeWrite (nnCircumcenterMarks workspace) slot generation
-        pure (Just center)
- where
-  !slot = faceIdIndex face
-
-faceCircumcenterRelative
-  :: Triangulation mode vertex directed undirected face
-  -> Point
-  -> FaceId
-  -> Maybe (Point)
-faceCircumcenterRelative triangulation query face = do
-  (a, b, c) <- innerFaceVertices triangulation face
-  center <- circumcenter
-    (subtractPoint (vertexPoint triangulation a) query)
-    (subtractPoint (vertexPoint triangulation b) query)
-    (subtractPoint (vertexPoint triangulation c) query)
-  if finitePoint center then Just center else Nothing
-
-normalizeWeights
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Int
-  -> ST s Bool
-normalizeWeights _ 0 = pure False
-normalizeWeights workspace count = do
-  (!total, !minimumWeight, !finite) <- firstPass 0 0 0 True
-  let !tolerance = max 1.0e-12 (128 * scalarEpsilon)
-  if not finite || total == 0 || minimumWeight < negate tolerance
-    then pure False
-    else do
-      clampedTotal <- clampPass 0 0
-      if clampedTotal <= 0 || not (isFinite clampedTotal)
-        then pure False
-        else normalizePass 0 clampedTotal >> pure True
- where
-  firstPass !index !total !minimumWeight !finite
-    | index >= count = pure (total, minimumWeight, finite)
-    | otherwise = do
-        weight <- MUV.unsafeRead (nnWeightValue workspace) index
-        let !minimumWeight' = if index == 0 then weight else min minimumWeight weight
-        firstPass (index + 1) (total + weight) minimumWeight' (finite && isFinite weight)
-  clampPass !index !total
-    | index >= count = pure total
-    | otherwise = do
-        weight <- MUV.unsafeRead (nnWeightValue workspace) index
-        let !clamped = max 0 weight
-        MUV.unsafeWrite (nnWeightValue workspace) index clamped
-        clampPass (index + 1) (total + clamped)
-  normalizePass !index !total
-    | index >= count = pure ()
-    | otherwise = do
-        weight <- MUV.unsafeRead (nnWeightValue workspace) index
-        MUV.unsafeWrite (nnWeightValue workspace) index (weight / total)
-        normalizePass (index + 1) total
-
-fallbackBarycentric
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Point
-  -> FaceId
-  -> Int
-  -> Int
-  -> ST s (Int, InterpolationStats)
-fallbackBarycentric workspace query face faceTests cavityCount =
-  case innerFaceVertices triangulation face of
-    Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
-    Just (a, b, c) ->
-      case barycentricCoordinates
-        (vertexPoint triangulation a)
-        (vertexPoint triangulation b)
-        (vertexPoint triangulation c)
-        query of
-        Nothing -> pure (0, InterpolationStats cavityCount 0 faceTests True)
-        Just (wa, wb, wc) -> do
-          writeWeight workspace 0 a wa
-          writeWeight workspace 1 b wb
-          writeWeight workspace 2 c wc
-          pure (3, InterpolationStats cavityCount 3 faceTests True)
- where
-  triangulation = nnTriangulation workspace
-
-writeWeight
-  :: NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Int
-  -> VertexId
-  -> Double
-  -> ST s ()
-writeWeight workspace index (VertexId vertex) weight = do
-  MUV.unsafeWrite (nnWeightVertex workspace) index vertex
-  MUV.unsafeWrite (nnWeightValue workspace) index weight
-
--- | Sample the nearest vertex, or return 'Nothing' for an empty mesh.
-interpolateNearest
-  :: (VertexId -> value)
-  -> Triangulation mode vertex directed undirected face
-  -> Maybe VertexId
-  -> QueryPoint
-  -> Maybe value
-interpolateNearest sample triangulation hint query = sample . fst <$> nearestNeighbor triangulation hint query
-
--- | Interpolate scalar vertex samples in the cell containing the query.
-interpolateBarycentric
-  :: (VertexId -> Double)
-  -> Triangulation mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> Maybe Double
-interpolateBarycentric sample triangulation hint query =
-  case fst (barycentricWeights triangulation hint query) of
-    NoWeights -> Nothing
-    OneWeight vertex -> Just (sample vertex)
-    TwoWeights a wa b wb -> Just (wa * sample a + wb * sample b)
-    ThreeWeights a wa b wb c wc -> Just (wa * sample a + wb * sample b + wc * sample c)
-
--- | Interpolate from reusable Sibson weights.
-interpolateNaturalNeighbor
-  :: (VertexId -> Double)
-  -> NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> ST s (Maybe Double, InterpolationStats)
-interpolateNaturalNeighbor sample workspace hint query = do
-  (total, _, stats) <-
-    foldNaturalNeighborWeights
-      (\accumulator vertex weight -> accumulator + weight * sample vertex)
-      0
-      workspace
-      hint
-      query
-  pure (if interpolationNaturalNeighbors stats == 0 then Nothing else Just total, stats)
-
--- | Estimate one vertex gradient from its Delaunay neighbors.
-estimateGradient
-  :: (VertexId -> Double)
-  -> Triangulation mode vertex directed undirected face
-  -> VertexId
-  -> (Double, Double)
-estimateGradient sample triangulation vertex =
-  case vertexOutEdge triangulation vertex of
-    Nothing -> (0, 0)
-    Just start ->
-      let !nextEdge = counterClockwise triangulation start
-       in if nextEdge == start
-            then (0, 0)
-            else
-              let (!nx, !ny, !nz) = walk start start 0 0 0 0
-               in if nz /= 0 && isFinite nz then (-nx / nz, -ny / nz) else (0, 0)
- where
-  Point vx vy = vertexPoint triangulation vertex
-  !vz = sample vertex
-  !bound = numDirectedEdges triangulation + 1
-
-  walk !start !current !steps !sumX !sumY !sumZ
-    | steps >= bound = (sumX, sumY, sumZ)
-    | otherwise =
-        let !nextEdge = counterClockwise triangulation current
-            !leftVertex = destination triangulation current
-            !rightVertex = destination triangulation nextEdge
-            (!nextX, !nextY, !nextZ) = accumulate sumX sumY sumZ leftVertex rightVertex
-         in if nextEdge == start
-              then (nextX, nextY, nextZ)
-              else walk start nextEdge (steps + 1) nextX nextY nextZ
-
-  accumulate !sumX !sumY !sumZ leftVertex rightVertex =
-    let Point lx ly = vertexPoint triangulation leftVertex
-        Point rx ry = vertexPoint triangulation rightVertex
-        !lz = sample leftVertex
-        !rz = sample rightVertex
-        !d0x = lx - vx
-        !d0y = ly - vy
-        !d0z = lz - vz
-        !d1x = rx - vx
-        !d1y = ry - vy
-        !d1z = rz - vz
-        !normalX = d0y * d1z - d0z * d1y
-        !normalY = d0z * d1x - d0x * d1z
-        !normalZ = d0x * d1y - d0y * d1x
-     in if normalZ > 0
-          then (sumX + normalX, sumY + normalY, sumZ + normalZ)
-          else (sumX, sumY, sumZ)
-
--- | Estimate gradients for every vertex in handle order.
-estimateGradients
-  :: (VertexId -> Double)
-  -> Triangulation mode vertex directed undirected face
-  -> V.Vector (Double, Double)
-estimateGradients sample triangulation =
-  V.generate (numVertices triangulation) $ \index ->
-    estimateGradient sample triangulation (VertexId (fromIntegral index))
-
--- | Natural-neighbor interpolation with nodal-gradient correction.
-interpolateNaturalNeighborGradient
-  :: (VertexId -> Double)
-  -> (VertexId -> (Double, Double))
-  -> Double
-  -> NaturalNeighborWorkspace s mode vertex directed undirected face
-  -> Maybe LocationHint
-  -> QueryPoint
-  -> ST s (Maybe Double, InterpolationStats)
-interpolateNaturalNeighborGradient sample gradient flatness workspace hint queryPoint
-  | flatness < 0 || not (isFinite flatness) = pure (Nothing, InterpolationStats 0 0 0 False)
-  | otherwise = do
-      (!count, _, !stats) <- queryNaturalNeighborWorkspace workspace hint queryPoint
-      if count == 0
-        then pure (Nothing, stats)
-        else do
-          accumulation <- accumulate 0 count Nothing (0, 0, 0, 0, 0)
-          pure (finish accumulation, stats)
- where
-  !query = queryPointValue queryPoint
-  !triangulation = nnTriangulation workspace
-
-  -- 'flatness' is 0.5 or 1 in practice and @**@ is an exp/log pair per natural
-  -- neighbour per query, so both are answered directly. The two shortcuts do
-  -- not stand on the same ground.
-  --
-  -- The identity is exact. @x@ raised to 1 is @x@, which is already a float,
-  -- and rounding an exactly representable result admits only that result, so
-  -- any implementation faithful to within an ulp returns it.
-  --
-  -- The square root is not exact and does not need to be. IEEE-754 mandates a
-  -- correctly rounded 'sqrt'; @pow@ is a recommended operation carrying no such
-  -- requirement. Where the two disagree it is therefore in the last ulp, and it
-  -- is 'sqrt' that holds the correctly rounded answer — this substitution can
-  -- only move the result toward it. Against this platform's libm they in fact
-  -- agree on every non-negative binary32 and on every non-negative binary64
-  -- sampled, which is a measurement and not a proof.
-  --
-  -- They part company at negative zero, where @pow@ answers @+0@ and 'sqrt'
-  -- answers @-0@. A sum of two squares is never negative zero, and were it one
-  -- the sole consumer below tests @== 0@, which both zeroes satisfy alike.
-  raiseToFlatness squared
-    | flatness == 0.5 = sqrt squared
-    | flatness == 1 = squared
-    | otherwise = squared ** flatness
-
-  accumulate !index !count !exact !totals
-    | index >= count = pure (exact, totals)
-    | otherwise = do
-        rawVertex <- MUV.unsafeRead (nnWeightVertex workspace) index
-        weight <- MUV.unsafeRead (nnWeightValue workspace) index
-        let !vertex = VertexId rawVertex
-            !point = vertexPoint triangulation vertex
-            !exact' =
-              case exact of
-                Just _ -> exact
-                Nothing
-                  | query == point -> Just (sample vertex)
-                  | otherwise -> Nothing
-            !totals' = contribution totals vertex weight
-        accumulate (index + 1) count exact' totals'
-
-  finish
-    :: (Maybe Double, (Double, Double, Double, Double, Double))
-    -> Maybe Double
-  finish (Just value, _) = Just value
-  finish (Nothing, (!sumC0, !sumC1, !sumC1Weights, !alphaNumerator, !beta))
-    | sumC1Weights == 0 = Just sumC0
-    | otherwise =
-        let !alpha = alphaNumerator / sumC1Weights
-            !c1 = sumC1 / sumC1Weights
-            !denominator = alpha + beta
-         in if denominator == 0 || not (isFinite denominator)
-              then Just sumC0
-              else Just ((alpha * sumC0 + beta * c1) / denominator)
-
-  contribution (!sumC0, !sumC1, !sumC1Weights, !alpha, !beta) vertex weight =
-    let Point qx qy = query
-        Point vx vy = vertexPoint triangulation vertex
-        !dx = qx - vx
-        !dy = qy - vy
-        !radiusSquared = dx * dx + dy * dy
-        !radiusPower = raiseToFlatness radiusSquared
-        !c1Weight = if radiusPower == 0 then 0 else weight / radiusPower
-        (!gx, !gy) = gradient vertex
-        !height = sample vertex
-        !zeta = height + dx * gx + dy * gy
-     in ( sumC0 + height * weight
-        , sumC1 + zeta * c1Weight
-        , sumC1Weights + c1Weight
-        , alpha + c1Weight * radiusSquared
-        , beta + weight * radiusSquared
-        )
-
-subtractPoint :: Point -> Point -> Point
-subtractPoint (Point ax ay) (Point bx by) = Point (ax - bx) (ay - by)
-{-# INLINE subtractPoint #-}
-
-finitePoint :: Point -> Bool
-finitePoint (Point x y) = isFinite x && isFinite y
-{-# INLINE finitePoint #-}
-
-clamp :: Ord value => value -> value -> value -> value
-clamp low high = max low . min high
-{-# INLINE clamp #-}
diff --git a/src-dual/Moonlight/Triangulation/Voronoi.hs b/src-dual/Moonlight/Triangulation/Voronoi.hs
deleted file mode 100644
--- a/src-dual/Moonlight/Triangulation/Voronoi.hs
+++ /dev/null
@@ -1,276 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-
--- | The Voronoi dual, addressed by its own handle family: every cell, edge and
--- vertex is a view of the Delaunay mesh rather than a second structure.
-module Moonlight.Triangulation.Voronoi
-  ( VoronoiFaceId (..)
-  , DirectedVoronoiEdgeId (..)
-  , UndirectedVoronoiEdgeId (..)
-  , VoronoiVertexId (..)
-  , VoronoiEdgeGeometry (..)
-  , voronoiFaces
-  , directedVoronoiEdges
-  , undirectedVoronoiEdges
-  , asDelaunayDirectedEdge
-  , asDelaunayUndirectedEdge
-  , asDirectedVoronoiEdge
-  , asUndirectedVoronoiEdge
-  , reverseVoronoiEdge
-  , voronoiNext
-  , voronoiPrevious
-  , voronoiFrom
-  , voronoiTo
-  , voronoiIncidentFace
-  , voronoiFaceSite
-  , voronoiFaceAdjacentEdges
-  , voronoiVertexPosition
-  , voronoiVertexOutgoingEdges
-  , voronoiDirectionVector
-  , voronoiEdgeGeometry
-  , faceCircumcenter
-  ) where
-
-import Control.DeepSeq (NFData)
-import Moonlight.Triangulation.Dcel
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (directedEdges, undirectedEdges, vertices)
-import Moonlight.Triangulation.Math
-import Moonlight.Triangulation.Types
-
--- | A Voronoi face, identified by its primal site.
---
--- The dual uses the exact same fixed-index space as the primal DCEL. These
--- newtypes add semantic separation without allocating or owning any dual mesh.
-newtype VoronoiFaceId = VoronoiFaceId { unVoronoiFaceId :: VertexId }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | An oriented Voronoi edge, identified by its primal directed edge.
-newtype DirectedVoronoiEdgeId = DirectedVoronoiEdgeId { unDirectedVoronoiEdgeId :: DirectedEdgeId }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | An unoriented Voronoi edge, identified by its primal undirected edge.
-newtype UndirectedVoronoiEdgeId = UndirectedVoronoiEdgeId { unUndirectedVoronoiEdgeId :: UndirectedEdgeId }
-  deriving stock (Show)
-  deriving newtype (Eq, Ord, NFData)
-
--- | A finite dual vertex or an ideal endpoint of an unbounded dual edge.
-data VoronoiVertexId
-  = InnerVoronoiVertex !FaceId
-  | OuterVoronoiVertex !DirectedVoronoiEdgeId
-  deriving stock (Eq, Ord, Show)
-
--- | Finite segment, half-infinite ray, or infinite line in the dual.
-data VoronoiEdgeGeometry
-  = VoronoiSegment !(Point) !(Point)
-  | VoronoiRay !(Point) !(Point)
-  | VoronoiLine !(Point) !(Point)
-  deriving stock (Eq, Ord, Show)
-
--- | Voronoi faces in primal vertex-handle order.
-voronoiFaces :: Triangulation mode vertex directed undirected face -> [VoronoiFaceId]
-voronoiFaces triangulation = map VoronoiFaceId (vertices triangulation)
-{-# INLINE voronoiFaces #-}
-
--- | Directed Voronoi edges in primal directed-edge order.
---
--- Every primal directed edge is one directed edge in the dual. Boundary
--- handles naturally represent half-infinite edges through OuterVoronoiVertex.
-directedVoronoiEdges :: Triangulation mode vertex directed undirected face -> [DirectedVoronoiEdgeId]
-directedVoronoiEdges triangulation = map DirectedVoronoiEdgeId (directedEdges triangulation)
-{-# INLINE directedVoronoiEdges #-}
-
--- | Undirected Voronoi edges in primal undirected-edge order.
-undirectedVoronoiEdges :: Triangulation mode vertex directed undirected face -> [UndirectedVoronoiEdgeId]
-undirectedVoronoiEdges triangulation = map UndirectedVoronoiEdgeId (undirectedEdges triangulation)
-{-# INLINE undirectedVoronoiEdges #-}
-
--- | Recover the primal directed edge.
-asDelaunayDirectedEdge :: DirectedVoronoiEdgeId -> DirectedEdgeId
-asDelaunayDirectedEdge = unDirectedVoronoiEdgeId
-{-# INLINE asDelaunayDirectedEdge #-}
-
--- | Recover the primal undirected edge.
-asDelaunayUndirectedEdge :: UndirectedVoronoiEdgeId -> UndirectedEdgeId
-asDelaunayUndirectedEdge = unUndirectedVoronoiEdgeId
-{-# INLINE asDelaunayUndirectedEdge #-}
-
--- | View a primal directed edge in the dual handle family.
-asDirectedVoronoiEdge :: DirectedEdgeId -> DirectedVoronoiEdgeId
-asDirectedVoronoiEdge = DirectedVoronoiEdgeId
-{-# INLINE asDirectedVoronoiEdge #-}
-
--- | View a primal undirected edge in the dual handle family.
-asUndirectedVoronoiEdge :: UndirectedEdgeId -> UndirectedVoronoiEdgeId
-asUndirectedVoronoiEdge = UndirectedVoronoiEdgeId
-{-# INLINE asUndirectedVoronoiEdge #-}
-
--- | Reverse a directed Voronoi edge.
-reverseVoronoiEdge :: DirectedVoronoiEdgeId -> DirectedVoronoiEdgeId
-reverseVoronoiEdge (DirectedVoronoiEdgeId edge) = DirectedVoronoiEdgeId (reverseEdge edge)
-{-# INLINE reverseVoronoiEdge #-}
-
--- | Next dual edge around the incident Voronoi face.
---
--- Dual next/previous rotate around the primal origin site.
-voronoiNext
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> DirectedVoronoiEdgeId
-voronoiNext triangulation (DirectedVoronoiEdgeId edge) =
-  DirectedVoronoiEdgeId (counterClockwise triangulation edge)
-{-# INLINE voronoiNext #-}
-
--- | Previous dual edge around the incident Voronoi face.
-voronoiPrevious
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> DirectedVoronoiEdgeId
-voronoiPrevious triangulation (DirectedVoronoiEdgeId edge) =
-  DirectedVoronoiEdgeId (clockwise triangulation edge)
-{-# INLINE voronoiPrevious #-}
-
--- | Origin dual vertex, including ideal endpoints on the hull.
-voronoiFrom
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> VoronoiVertexId
-voronoiFrom triangulation edge@(DirectedVoronoiEdgeId primal)
-  | face == outerFace = OuterVoronoiVertex edge
-  | otherwise = InnerVoronoiVertex face
- where
-  face = incidentFace triangulation primal
-{-# INLINE voronoiFrom #-}
-
--- | Destination dual vertex, including ideal endpoints on the hull.
-voronoiTo
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> VoronoiVertexId
-voronoiTo triangulation = voronoiFrom triangulation . reverseVoronoiEdge
-{-# INLINE voronoiTo #-}
-
--- | Voronoi face to the left of a directed dual edge.
-voronoiIncidentFace
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> VoronoiFaceId
-voronoiIncidentFace triangulation (DirectedVoronoiEdgeId edge) =
-  VoronoiFaceId (origin triangulation edge)
-{-# INLINE voronoiIncidentFace #-}
-
--- | Primal site represented by a Voronoi face.
-voronoiFaceSite :: VoronoiFaceId -> VertexId
-voronoiFaceSite = unVoronoiFaceId
-{-# INLINE voronoiFaceSite #-}
-
--- | Directed boundary of a Voronoi face.
-voronoiFaceAdjacentEdges
-  :: Triangulation mode vertex directed undirected face
-  -> VoronoiFaceId
-  -> [DirectedVoronoiEdgeId]
-voronoiFaceAdjacentEdges triangulation (VoronoiFaceId site) =
-  map DirectedVoronoiEdgeId (vertexOutgoingEdges triangulation site)
-{-# INLINE voronoiFaceAdjacentEdges #-}
-
--- | Position of a finite dual vertex; ideal hull endpoints have no position.
---
--- A caller reaches this through 'voronoiFrom' or 'voronoiTo', which build the
--- endpoint sum immediately before it is taken apart again. Only an unfolding at
--- the consumer lets the two meet, so the constructor never reaches the heap.
-voronoiVertexPosition
-  :: Triangulation mode vertex directed undirected face
-  -> VoronoiVertexId
-  -> Maybe (Point)
-voronoiVertexPosition triangulation vertex = case vertex of
-  InnerVoronoiVertex face -> faceCircumcenter triangulation face
-  OuterVoronoiVertex _ -> Nothing
-{-# INLINE voronoiVertexPosition #-}
-
--- | Directed dual edges leaving a finite dual vertex.
-voronoiVertexOutgoingEdges
-  :: Triangulation mode vertex directed undirected face
-  -> VoronoiVertexId
-  -> Maybe [DirectedVoronoiEdgeId]
-voronoiVertexOutgoingEdges triangulation vertex = case vertex of
-  OuterVoronoiVertex _ -> Nothing
-  InnerVoronoiVertex face ->
-    map DirectedVoronoiEdgeId . faceDirectedEdges triangulation <$> nonOuter face
- where
-  nonOuter face
-    | face == outerFace = Nothing
-    | otherwise = Just face
-{-# INLINE voronoiVertexOutgoingEdges #-}
-
--- | Unit direction of the dual edge orthogonal to a primal edge.
-voronoiDirectionVector
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> Point
-voronoiDirectionVector triangulation (DirectedVoronoiEdgeId edge) =
-  case vertexPoint triangulation (origin triangulation edge) of
-    Point ax ay -> case vertexPoint triangulation (destination triangulation edge) of
-      Point bx by -> Point (ay - by) (bx - ax)
-
--- | Circumcenter of a bounded primal face.
---
--- The circumcentre stands on the absolute vertex positions, which is a
--- different value from the query-relative one the Sibson pipeline caches: that
--- one rescales the differences it was handed, so translating the inputs moves
--- the rounding and the two do not differ by the translation. The interpolation
--- workspace's plane therefore cannot serve this function, and the repetition
--- here is across calls rather than within one — a sweep recomputes each face
--- once per incident dual edge, while a single call touches two distinct faces.
--- That threefold repetition is what a dual which is a view rather than a
--- structure costs, and the referent pays it identically, so no cache is owed.
-faceCircumcenter
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> Maybe (Point)
-faceCircumcenter triangulation face
-  | face == outerFace = Nothing
-  | otherwise = case adjacentEdge triangulation face of
-      Nothing -> Nothing
-      Just e0 ->
-        let !e1 = next triangulation e0
-            !e2 = next triangulation e1
-         in if next triangulation e2 /= e0
-              then Nothing
-              else
-                circumcenter
-                  (vertexPoint triangulation (origin triangulation e0))
-                  (vertexPoint triangulation (origin triangulation e1))
-                  (vertexPoint triangulation (origin triangulation e2))
-{-# INLINE faceCircumcenter #-}
-
--- | Geometric realization of one dual edge.
---
--- The endpoint classification 'voronoiFrom' and 'voronoiTo' publish is two face
--- reads and two comparisons; taken through those observations it is also two
--- sum values built and immediately scrutinized. The geometry reads the faces
--- itself so that the classification stays in registers.
-voronoiEdgeGeometry
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> Maybe (VoronoiEdgeGeometry)
-voronoiEdgeGeometry triangulation edge@(DirectedVoronoiEdgeId primal)
-  | innerFrom, innerTo =
-      VoronoiSegment <$> faceCircumcenter triangulation fromFace <*> faceCircumcenter triangulation toFace
-  | innerFrom = do
-      start <- faceCircumcenter triangulation fromFace
-      pure (VoronoiRay start (normalizeVector (voronoiDirectionVector triangulation edge)))
-  | innerTo = do
-      end <- faceCircumcenter triangulation toFace
-      pure (VoronoiRay end (normalizeVector (negateVector (voronoiDirectionVector triangulation edge))))
-  | otherwise =
-      let !center = midpoint
-            (vertexPoint triangulation (origin triangulation primal))
-            (vertexPoint triangulation (destination triangulation primal))
-       in Just (VoronoiLine center (normalizeVector (voronoiDirectionVector triangulation edge)))
- where
-  !fromFace = incidentFace triangulation primal
-  !toFace = incidentFace triangulation (reverseEdge primal)
-  !innerFrom = fromFace /= outerFace
-  !innerTo = toFace /= outerFace
diff --git a/src-dual/Moonlight/Triangulation/Voronoi/Handles.hs b/src-dual/Moonlight/Triangulation/Voronoi/Handles.hs
deleted file mode 100644
--- a/src-dual/Moonlight/Triangulation/Voronoi/Handles.hs
+++ /dev/null
@@ -1,283 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Zero-copy Voronoi views that retain the owning triangulation.
-module Moonlight.Triangulation.Voronoi.Handles
-  ( VoronoiFaceHandle
-  , DirectedVoronoiEdgeHandle
-  , UndirectedVoronoiEdgeHandle
-  , VoronoiVertexHandle
-  , voronoiFaceHandle
-  , directedVoronoiEdgeHandle
-  , undirectedVoronoiEdgeHandle
-  , vertexAsVoronoiFaceH
-  , directedEdgeAsVoronoiH
-  , undirectedEdgeAsVoronoiH
-  , innerFaceAsVoronoiVertexH
-  , fixVoronoiFace
-  , fixDirectedVoronoiEdge
-  , fixUndirectedVoronoiEdge
-  , fixVoronoiVertex
-  , voronoiEdgeReverseH
-  , voronoiEdgeNextH
-  , voronoiEdgePreviousH
-  , voronoiEdgeFromH
-  , voronoiEdgeToH
-  , voronoiEdgeFaceH
-  , voronoiEdgeAsUndirectedH
-  , voronoiEdgeAsDelaunayH
-  , voronoiFaceSiteH
-  , voronoiFaceAdjacentEdgesH
-  , voronoiVertexPositionH
-  , voronoiVertexOutEdgesH
-  , voronoiVertexAsDelaunayFaceH
-  , voronoiVertexAsOuterEdgeH
-  , voronoiEdgeDirectionH
-  , voronoiEdgeGeometryH
-  ) where
-
-import Moonlight.Triangulation.Handles.Dynamic
-import Moonlight.Triangulation.Math (midpoint, negateVector, normalizeVector)
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Voronoi
-
--- | Zero-copy dual handles. Each dual value contains exactly one primal handle,
--- so it cannot accidentally combine an identifier from one triangulation with
--- the storage of another.
-newtype VoronoiFaceHandle mode vertex directed undirected face =
-  VoronoiFaceHandle (VertexHandle mode vertex directed undirected face)
-
--- | An owning handle to a directed dual edge.
-newtype DirectedVoronoiEdgeHandle mode vertex directed undirected face =
-  DirectedVoronoiEdgeHandle (DirectedEdgeHandle mode vertex directed undirected face)
-
--- | An owning handle to an undirected dual edge.
-newtype UndirectedVoronoiEdgeHandle mode vertex directed undirected face =
-  UndirectedVoronoiEdgeHandle (UndirectedEdgeHandle mode vertex directed undirected face)
-
--- | An owning handle to a finite or ideal dual vertex.
-data VoronoiVertexHandle mode vertex directed undirected face
-  = InnerVoronoiVertexHandle
-      !(FaceHandle InnerTag mode vertex directed undirected face)
-  | OuterVoronoiVertexHandle
-      !(DirectedVoronoiEdgeHandle mode vertex directed undirected face)
-
-instance Show (VoronoiFaceHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixVoronoiFace
-
-instance Show (DirectedVoronoiEdgeHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixDirectedVoronoiEdge
-
-instance Show (UndirectedVoronoiEdgeHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixUndirectedVoronoiEdge
-
-instance Show (VoronoiVertexHandle mode vertex directed undirected face) where
-  showsPrec precedence = showsPrec precedence . fixVoronoiVertex
-
--- | Admit a fixed Voronoi face identifier into a triangulation.
-voronoiFaceHandle
-  :: Triangulation mode vertex directed undirected face
-  -> VoronoiFaceId
-  -> Maybe (VoronoiFaceHandle mode vertex directed undirected face)
-voronoiFaceHandle triangulation (VoronoiFaceId site) =
-  VoronoiFaceHandle <$> vertexHandle triangulation site
-
--- | View an admitted primal vertex as its Voronoi face.
-vertexAsVoronoiFaceH
-  :: VertexHandle mode vertex directed undirected face
-  -> VoronoiFaceHandle mode vertex directed undirected face
-vertexAsVoronoiFaceH = VoronoiFaceHandle
-
--- | Admit a fixed directed Voronoi edge identifier into a triangulation.
-directedVoronoiEdgeHandle
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-  -> Maybe (DirectedVoronoiEdgeHandle mode vertex directed undirected face)
-directedVoronoiEdgeHandle triangulation (DirectedVoronoiEdgeId edge) =
-  DirectedVoronoiEdgeHandle <$> directedEdgeHandle triangulation edge
-
--- | View an admitted primal directed edge in the dual handle family.
-directedEdgeAsVoronoiH
-  :: DirectedEdgeHandle mode vertex directed undirected face
-  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
-directedEdgeAsVoronoiH = DirectedVoronoiEdgeHandle
-
--- | Admit a fixed undirected Voronoi edge identifier into a triangulation.
-undirectedVoronoiEdgeHandle
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedVoronoiEdgeId
-  -> Maybe (UndirectedVoronoiEdgeHandle mode vertex directed undirected face)
-undirectedVoronoiEdgeHandle triangulation (UndirectedVoronoiEdgeId edge) =
-  UndirectedVoronoiEdgeHandle <$> undirectedEdgeHandle triangulation edge
-
--- | View an admitted primal undirected edge in the dual handle family.
-undirectedEdgeAsVoronoiH
-  :: UndirectedEdgeHandle mode vertex directed undirected face
-  -> UndirectedVoronoiEdgeHandle mode vertex directed undirected face
-undirectedEdgeAsVoronoiH = UndirectedVoronoiEdgeHandle
-
--- | View an admitted bounded primal face as a finite dual vertex.
-innerFaceAsVoronoiVertexH
-  :: FaceHandle InnerTag mode vertex directed undirected face
-  -> VoronoiVertexHandle mode vertex directed undirected face
-innerFaceAsVoronoiVertexH = InnerVoronoiVertexHandle
-
--- | Forget ownership and recover a fixed Voronoi face identifier.
-fixVoronoiFace :: VoronoiFaceHandle mode vertex directed undirected face -> VoronoiFaceId
-fixVoronoiFace (VoronoiFaceHandle site) = VoronoiFaceId (fixVertex site)
-{-# INLINE fixVoronoiFace #-}
-
--- | Forget ownership and recover a fixed directed dual edge identifier.
-fixDirectedVoronoiEdge
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> DirectedVoronoiEdgeId
-fixDirectedVoronoiEdge (DirectedVoronoiEdgeHandle edge) =
-  DirectedVoronoiEdgeId (fixDirectedEdge edge)
-{-# INLINE fixDirectedVoronoiEdge #-}
-
--- | Forget ownership and recover a fixed undirected dual edge identifier.
-fixUndirectedVoronoiEdge
-  :: UndirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> UndirectedVoronoiEdgeId
-fixUndirectedVoronoiEdge (UndirectedVoronoiEdgeHandle edge) =
-  UndirectedVoronoiEdgeId (fixUndirectedEdge edge)
-{-# INLINE fixUndirectedVoronoiEdge #-}
-
--- | Forget ownership and recover a fixed dual vertex identifier.
-fixVoronoiVertex
-  :: VoronoiVertexHandle mode vertex directed undirected face
-  -> VoronoiVertexId
-fixVoronoiVertex (InnerVoronoiVertexHandle face) =
-  InnerVoronoiVertex (fixedFaceId (fixFace face))
-fixVoronoiVertex (OuterVoronoiVertexHandle edge) =
-  OuterVoronoiVertex (fixDirectedVoronoiEdge edge)
-{-# INLINE fixVoronoiVertex #-}
-
--- | Reverse an admitted directed dual edge.
-voronoiEdgeReverseH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
-voronoiEdgeReverseH (DirectedVoronoiEdgeHandle edge) =
-  DirectedVoronoiEdgeHandle (directedEdgeReverse edge)
-
--- | Next admitted dual edge around its Voronoi face.
-voronoiEdgeNextH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
-voronoiEdgeNextH (DirectedVoronoiEdgeHandle edge) =
-  DirectedVoronoiEdgeHandle (directedEdgeCounterClockwise edge)
-
--- | Previous admitted dual edge around its Voronoi face.
-voronoiEdgePreviousH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> DirectedVoronoiEdgeHandle mode vertex directed undirected face
-voronoiEdgePreviousH (DirectedVoronoiEdgeHandle edge) =
-  DirectedVoronoiEdgeHandle (directedEdgeClockwise edge)
-
--- | Origin finite or ideal vertex of an admitted dual edge.
-voronoiEdgeFromH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> VoronoiVertexHandle mode vertex directed undirected face
-voronoiEdgeFromH edge@(DirectedVoronoiEdgeHandle primal) =
-  case faceAsInner (directedEdgeFace primal) of
-    Just inner -> InnerVoronoiVertexHandle inner
-    Nothing -> OuterVoronoiVertexHandle edge
-
--- | Destination finite or ideal vertex of an admitted dual edge.
-voronoiEdgeToH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> VoronoiVertexHandle mode vertex directed undirected face
-voronoiEdgeToH = voronoiEdgeFromH . voronoiEdgeReverseH
-
--- | Voronoi face to the left of an admitted directed dual edge.
-voronoiEdgeFaceH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> VoronoiFaceHandle mode vertex directed undirected face
-voronoiEdgeFaceH (DirectedVoronoiEdgeHandle edge) =
-  VoronoiFaceHandle (directedEdgeFrom edge)
-
--- | Forget orientation while retaining ownership.
-voronoiEdgeAsUndirectedH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> UndirectedVoronoiEdgeHandle mode vertex directed undirected face
-voronoiEdgeAsUndirectedH (DirectedVoronoiEdgeHandle edge) =
-  UndirectedVoronoiEdgeHandle (directedEdgeAsUndirected edge)
-
--- | Recover the owning primal directed-edge handle.
-voronoiEdgeAsDelaunayH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> DirectedEdgeHandle mode vertex directed undirected face
-voronoiEdgeAsDelaunayH (DirectedVoronoiEdgeHandle edge) = edge
-
--- | Recover the owning primal site handle.
-voronoiFaceSiteH
-  :: VoronoiFaceHandle mode vertex directed undirected face
-  -> VertexHandle mode vertex directed undirected face
-voronoiFaceSiteH (VoronoiFaceHandle site) = site
-
--- | Admitted directed boundary of a Voronoi face.
-voronoiFaceAdjacentEdgesH
-  :: VoronoiFaceHandle mode vertex directed undirected face
-  -> [DirectedVoronoiEdgeHandle mode vertex directed undirected face]
-voronoiFaceAdjacentEdgesH (VoronoiFaceHandle site) =
-  map DirectedVoronoiEdgeHandle (vertexHandleOutEdges site)
-
--- | Position of a finite admitted dual vertex.
-voronoiVertexPositionH
-  :: VoronoiVertexHandle mode vertex directed undirected face
-  -> Maybe (Point)
-voronoiVertexPositionH (InnerVoronoiVertexHandle face) = innerFaceCircumcenter face
-voronoiVertexPositionH (OuterVoronoiVertexHandle _) = Nothing
-
--- | Directed dual edges leaving a finite admitted dual vertex.
-voronoiVertexOutEdgesH
-  :: VoronoiVertexHandle mode vertex directed undirected face
-  -> Maybe [DirectedVoronoiEdgeHandle mode vertex directed undirected face]
-voronoiVertexOutEdgesH (InnerVoronoiVertexHandle face) =
-  Just (map DirectedVoronoiEdgeHandle (faceAdjacentEdges face))
-voronoiVertexOutEdgesH (OuterVoronoiVertexHandle _) = Nothing
-
--- | Recover the primal bounded face underlying a finite dual vertex.
-voronoiVertexAsDelaunayFaceH
-  :: VoronoiVertexHandle mode vertex directed undirected face
-  -> Maybe (FaceHandle InnerTag mode vertex directed undirected face)
-voronoiVertexAsDelaunayFaceH (InnerVoronoiVertexHandle face) = Just face
-voronoiVertexAsDelaunayFaceH (OuterVoronoiVertexHandle _) = Nothing
-
--- | Recover the primal hull edge underlying an ideal dual vertex.
-voronoiVertexAsOuterEdgeH
-  :: VoronoiVertexHandle mode vertex directed undirected face
-  -> Maybe (DirectedVoronoiEdgeHandle mode vertex directed undirected face)
-voronoiVertexAsOuterEdgeH (InnerVoronoiVertexHandle _) = Nothing
-voronoiVertexAsOuterEdgeH (OuterVoronoiVertexHandle edge) = Just edge
-
--- | Unit direction of the dual edge orthogonal to the primal edge.
-voronoiEdgeDirectionH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> Point
-voronoiEdgeDirectionH (DirectedVoronoiEdgeHandle primal) =
-  let (Point ax ay, Point bx by) = directedEdgePositions primal
-   in Point (ay - by) (bx - ax)
-
--- | Geometric realization of an admitted dual edge.
-voronoiEdgeGeometryH
-  :: DirectedVoronoiEdgeHandle mode vertex directed undirected face
-  -> Maybe (VoronoiEdgeGeometry)
-voronoiEdgeGeometryH edge@(DirectedVoronoiEdgeHandle primal) =
-  case (voronoiEdgeFromH edge, voronoiEdgeToH edge) of
-    (InnerVoronoiVertexHandle fromFace, InnerVoronoiVertexHandle toFace) ->
-      VoronoiSegment <$> innerFaceCircumcenter fromFace <*> innerFaceCircumcenter toFace
-    (InnerVoronoiVertexHandle fromFace, OuterVoronoiVertexHandle _) -> do
-      start <- innerFaceCircumcenter fromFace
-      pure (VoronoiRay start (normalizeVector (voronoiEdgeDirectionH edge)))
-    (OuterVoronoiVertexHandle _, InnerVoronoiVertexHandle toFace) -> do
-      end <- innerFaceCircumcenter toFace
-      pure (VoronoiRay end (normalizeVector (negateVector (voronoiEdgeDirectionH edge))))
-    (OuterVoronoiVertexHandle _, OuterVoronoiVertexHandle _) ->
-      let (from, to) = directedEdgePositions primal
-       in Just (VoronoiLine (midpoint from to) (normalizeVector (voronoiEdgeDirectionH edge)))
-
--- The handle verbs cross the same component boundary as the fixed-index ones
--- and carry their unfoldings for the same reason. 'voronoiVertexPositionH'
--- reaches its arithmetic through 'innerFaceCircumcenter', whose own worker
--- publishes no unfolding, so it stops at that call until the dcel layer says
--- otherwise.
diff --git a/src-hex/Moonlight/Hex/Region.hs b/src-hex/Moonlight/Hex/Region.hs
--- a/src-hex/Moonlight/Hex/Region.hs
+++ b/src-hex/Moonlight/Hex/Region.hs
@@ -2,6 +2,7 @@
 {-# LANGUAGE DeriveAnyClass #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 -- | Bounded axial layouts and native packed finite-set algebra.
 module Moonlight.Hex.Region
@@ -17,11 +18,17 @@
   , hexLayoutCoordAt
   , HexRegion
   , HexRegionObstruction (..)
+  , HexPackedRegionObstruction (..)
   , emptyHexRegion
   , fullHexRegion
   , singletonHexRegion
   , hexRegionFromCoords
+  , hexRegionFromPackedWords
   , hexRegionGenerate
+  , hexRegionGenerateM
+  , HexRowSpan
+  , hexRowSpan
+  , hexRegionGenerateRowSpans
   , hexRegionLayout
   , hexRegionMember
   , hexRegionCardinality
@@ -42,29 +49,38 @@
   ) where
 
 import Control.DeepSeq (NFData)
+import Control.Monad.Trans.Class (lift)
 import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
 import Control.Monad.ST (ST, runST)
-import Data.Bits ((.&.), (.|.), complement, countTrailingZeros, popCount, shiftL, shiftR, xor)
+import Data.Bits ((.&.), (.|.), complement, countTrailingZeros, popCount, shiftL, xor)
 import Data.Foldable (traverse_)
 import Data.List.NonEmpty (NonEmpty (..))
+import Data.Maybe (isNothing)
 import Data.Vector (Vector)
 import Data.Vector qualified as Vector
 import Data.Vector.Unboxed qualified as U
 import Data.Vector.Unboxed.Mutable qualified as MU
 import Data.Word (Word64)
 import GHC.Generics (Generic)
-import Moonlight.Hex.Coordinate (HexCoord (..), HexDirection, hexStepCoord)
-
--- | A nonempty axial parallelogram with an overflow-safe dense row-major index.
-data HexLayout = HexLayout
-  { layoutOrigin :: !HexCoord
-  , layoutWidth :: !Int
-  , layoutHeight :: !Int
-  , layoutCellCount :: !Int
-  , layoutWordCount :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
+import Moonlight.Hex.Coordinate
+  ( HexCoord (..)
+  , HexDirection
+  , hexStepCoord
+  )
+import Moonlight.Hex.Region.Internal
+  ( HexLayout (..)
+  , HexRegion (..)
+  , extractBits
+  , finalWordMask
+  , firstSameLayoutDifferenceIndex
+  , generateHexRegionIndexed
+  , generateHexRegionIndexedM
+  , hexCoordAtIndex
+  , hexCoordIndex
+  , hexRegionMemberAtIndex
+  , lowBitMask
+  , setBitAt
+  )
 
 data HexLayoutObstruction
   = HexLayoutWidthNotPositive !Int
@@ -128,11 +144,6 @@
   | otherwise = Just (hexCoordAtIndex layout index)
 {-# INLINE hexLayoutCoordAt #-}
 
--- | Canonical packed membership: one bit per layout cell and zero padding.
-data HexRegion = HexRegion !HexLayout !(U.Vector Word64)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
 data HexRegionObstruction
   = HexRegionCoordinateOutsideLayout !HexLayout !HexCoord
   | HexRegionLayoutMismatch !HexLayout !HexLayout
@@ -140,6 +151,16 @@
   deriving stock (Eq, Show, Generic)
   deriving anyclass (NFData)
 
+-- | A rejected packed representation. Word count and zero padding are the
+-- complete representation invariant; admitted vectors are retained without a
+-- copy.
+data HexPackedRegionObstruction
+  -- | Expected word count, then observed word count.
+  = HexPackedRegionWordCountMismatch !Int !Int
+  | HexPackedRegionNonCanonicalPadding !Word64
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
 emptyHexRegion :: HexLayout -> HexRegion
 emptyHexRegion layout = HexRegion layout (U.replicate (layoutWordCount layout) 0)
 
@@ -181,36 +202,94 @@
         let (wordIndex, bitIndex) = cellIndex `quotRem` 64
          in MU.modify mutableWords (`setBitAt` bitIndex) wordIndex
 
+hexRegionFromPackedWords
+  :: HexLayout
+  -> U.Vector Word64
+  -> Either HexPackedRegionObstruction HexRegion
+hexRegionFromPackedWords layout wordsValue
+  | actualWordCount /= expectedWordCount =
+      Left (HexPackedRegionWordCountMismatch expectedWordCount actualWordCount)
+  | nonCanonicalPadding /= 0 =
+      Left (HexPackedRegionNonCanonicalPadding nonCanonicalPadding)
+  | otherwise = Right (HexRegion layout wordsValue)
+ where
+  expectedWordCount = layoutWordCount layout
+  actualWordCount = U.length wordsValue
+  nonCanonicalPadding =
+    (wordsValue `U.unsafeIndex` (expectedWordCount - 1))
+      .&. complement (finalWordMask layout)
+
 -- | Generate membership directly into packed words. This is the total bulk
 -- authoring path when membership is already a function rather than a sparse
 -- coordinate collection.
 hexRegionGenerate :: HexLayout -> (HexCoord -> Bool) -> HexRegion
 hexRegionGenerate layout predicate =
-  HexRegion layout (U.generate (layoutWordCount layout) generateWord)
+  generateHexRegionIndexed layout (predicate . hexCoordAtIndex layout)
+{-# INLINE hexRegionGenerate #-}
+
+-- | A nonempty closed interval of global axial q coordinates. Row ownership
+-- comes from 'hexRegionGenerateRowSpans'; arbitrary-size endpoints are clipped
+-- to its admitted layout before conversion to resident indices.
+data HexRowSpan = HexRowSpan !Integer !Integer
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+hexRowSpan :: Integer -> Integer -> Maybe HexRowSpan
+hexRowSpan firstQ finalQ
+  | firstQ <= finalQ = Just (HexRowSpan firstQ finalQ)
+  | otherwise = Nothing
+
+-- | Generate a packed section from closed q spans at each global axial row.
+-- Spans may overlap or extend beyond the layout: their clipped union is the
+-- result. Each span contributes whole word masks, never one predicate call
+-- per interior cell. The existing layout index and mask owners alone lower
+-- coordinates to bits, and clipping establishes canonical zero padding.
+hexRegionGenerateRowSpans :: HexLayout -> (Int -> [HexRowSpan]) -> HexRegion
+hexRegionGenerateRowSpans layout spansAtRow =
+  HexRegion layout
+    ( U.accum (.|.) (U.replicate (layoutWordCount layout) 0)
+        [ (wordIndex, lowBitMask bitCount `shiftL` firstBit)
+        | localRow <- [0 .. layoutHeight layout - 1]
+        , let row = originR + localRow
+        , HexRowSpan firstQ finalQ <- spansAtRow row
+        , let clippedFirst = max minimumQ firstQ
+        , let clippedFinal = min maximumQ finalQ
+        , clippedFirst <= clippedFinal
+        , Just firstIndex <- [hexCoordIndex layout (HexCoord (fromInteger clippedFirst) row)]
+        , Just finalIndex <- [hexCoordIndex layout (HexCoord (fromInteger clippedFinal) row)]
+        , wordIndex <- [firstIndex `quot` 64 .. finalIndex `quot` 64]
+        , let wordBase = wordIndex * 64
+        , let firstBit = max 0 (firstIndex - wordBase)
+        , let finalBit = min 63 (finalIndex - wordBase)
+        , let bitCount = finalBit - firstBit + 1
+        ]
+    )
  where
-  generateWord wordIndex =
-    let baseIndex = wordIndex * 64
-        bitCount = min 64 (layoutCellCount layout - baseIndex)
-     in gatherBits baseIndex bitCount 0 0
+  HexCoord originQ originR = layoutOrigin layout
+  minimumQ = toInteger originQ
+  maximumQ = minimumQ + toInteger (layoutWidth layout) - 1
 
-  gatherBits !baseIndex !bitCount !bitIndex !word
-    | bitIndex >= bitCount = word
-    | predicate (hexCoordAtIndex layout (baseIndex + bitIndex)) =
-        gatherBits baseIndex bitCount (bitIndex + 1) (setBitAt word bitIndex)
-    | otherwise = gatherBits baseIndex bitCount (bitIndex + 1) word
-{-# INLINE hexRegionGenerate #-}
+-- | Generate canonical packed membership while accumulating one caller-owned
+-- effect. Predicate refusal is preserved directly; packed admission cannot
+-- fail because this constructor owns every emitted bit, including padding.
+hexRegionGenerateM
+  :: Monad effect
+  => HexLayout
+  -> (HexCoord -> effect Bool)
+  -> effect HexRegion
+hexRegionGenerateM layout predicate =
+  generateHexRegionIndexedM layout (predicate . hexCoordAtIndex layout)
+{-# INLINE hexRegionGenerateM #-}
 
 hexRegionLayout :: HexRegion -> HexLayout
 hexRegionLayout (HexRegion layout _) = layout
 {-# INLINE hexRegionLayout #-}
 
 hexRegionMember :: HexCoord -> HexRegion -> Bool
-hexRegionMember coordinate (HexRegion layout wordsValue) =
+hexRegionMember coordinate region@(HexRegion layout _) =
   case hexCoordIndex layout coordinate of
     Nothing -> False
-    Just cellIndex ->
-      let (wordIndex, bitIndex) = cellIndex `quotRem` 64
-       in wordsValue `U.unsafeIndex` wordIndex .&. (1 `shiftL` bitIndex) /= 0
+    Just cellIndex -> hexRegionMemberAtIndex region cellIndex
 {-# INLINE hexRegionMember #-}
 
 hexRegionCardinality :: HexRegion -> Int
@@ -290,16 +369,10 @@
 hexRegionSubsetOf :: HexRegion -> HexRegion -> Either HexRegionObstruction Bool
 hexRegionSubsetOf (HexRegion leftLayout leftWords) (HexRegion rightLayout rightWords)
   | leftLayout /= rightLayout = Left (HexRegionLayoutMismatch leftLayout rightLayout)
-  | otherwise =
-      Right
-        ( U.ifoldl'
-            (\isSubset index leftWord ->
-                isSubset
-                  && leftWord .&. complement (rightWords `U.unsafeIndex` index) == 0
-            )
-            True
-            leftWords
-        )
+  | otherwise = Right (isNothing (firstSameLayoutDifferenceIndex left right))
+ where
+  left = HexRegion leftLayout leftWords
+  right = HexRegion rightLayout rightWords
 
 restrictHexRegion :: HexLayout -> HexRegion -> Either HexRegionObstruction HexRegion
 restrictHexRegion target source
@@ -324,7 +397,9 @@
   deriving anyclass (NFData)
 
 -- | Glue local sections only when their truth values agree on every overlap.
--- This is deliberately distinct from set union.
+-- This is deliberately distinct from set union. Space is mostly unglued;
+-- stars are the rare places where enough local sections agreed to make a
+-- global one and lit up from the sheer relief of consistency.
 glueCompatibleHexRegions :: NonEmpty HexRegion -> Either HexGluingObstruction HexRegion
 glueCompatibleHexRegions regions = do
   target <- boundingLayout regions
@@ -339,15 +414,15 @@
             (layoutWordCount target)
             (reframedCoverageWord target (hexRegionLayout firstRegion))
         )
-    obstruction <-
-      descendHexSections
-        target
-        mutableValues
-        mutableCoverage
-        remainingRegions
-    case obstruction of
-      Just coordinate -> pure (Left (HexOverlapDisagreement coordinate))
-      Nothing -> Right . HexRegion target <$> U.unsafeFreeze mutableValues
+    admitted <-
+      runExceptT
+        ( traverse_
+            (descendHexSection target mutableValues mutableCoverage)
+            remainingRegions
+        )
+    case admitted of
+      Left coordinate -> pure (Left (HexOverlapDisagreement coordinate))
+      Right () -> Right . HexRegion target <$> U.unsafeFreeze mutableValues
 
 hexNeighbourCoord :: HexLayout -> HexCoord -> HexDirection -> Maybe HexCoord
 hexNeighbourCoord layout coordinate direction =
@@ -367,45 +442,58 @@
   | otherwise = Right (HexRegion leftLayout (U.zipWith combine leftWords rightWords))
 {-# INLINE combineHexRegionsWith #-}
 
-descendHexSections
-  :: HexLayout
-  -> MU.MVector s Word64
-  -> MU.MVector s Word64
-  -> [HexRegion]
-  -> ST s (Maybe HexCoord)
-descendHexSections _ _ _ [] = pure Nothing
-descendHexSections target mutableValues mutableCoverage (local : remaining) = do
-  obstruction <- descendHexSection target mutableValues mutableCoverage local 0
-  case obstruction of
-    Just coordinate -> pure (Just coordinate)
-    Nothing -> descendHexSections target mutableValues mutableCoverage remaining
-
 descendHexSection
-  :: HexLayout
+  :: forall s. HexLayout
   -> MU.MVector s Word64
   -> MU.MVector s Word64
   -> HexRegion
-  -> Int
-  -> ST s (Maybe HexCoord)
-descendHexSection target mutableValues mutableCoverage local !wordIndex
-  | wordIndex >= layoutWordCount target = pure Nothing
-  | otherwise = do
-      values <- MU.unsafeRead mutableValues wordIndex
-      coverage <- MU.unsafeRead mutableCoverage wordIndex
-      let localValues = reframedWord target local wordIndex
-          localCoverage = reframedCoverageWord target (hexRegionLayout local) wordIndex
-          disagreement = (values `xor` localValues) .&. coverage .&. localCoverage
-      if disagreement == 0
-        then do
-          MU.unsafeWrite mutableValues wordIndex (values .|. localValues)
-          MU.unsafeWrite mutableCoverage wordIndex (coverage .|. localCoverage)
-          descendHexSection target mutableValues mutableCoverage local (wordIndex + 1)
-        else
-          pure
-            ( Just
-                (hexCoordAtIndex target (wordIndex * 64 + countTrailingZeros disagreement))
-            )
+  -> ExceptT HexCoord (ST s) ()
+descendHexSection target mutableValues mutableCoverage local =
+  traverse_
+    (\(firstWord, finalWord) -> descendHexWordSpan firstWord finalWord)
+    (localTargetWordSpans target (hexRegionLayout local))
+ where
+  descendHexWordSpan :: Int -> Int -> ExceptT HexCoord (ST s) ()
+  descendHexWordSpan !wordIndex !finalWord
+    | wordIndex > finalWord = pure ()
+    | otherwise = do
+        values <- lift (MU.unsafeRead mutableValues wordIndex)
+        coverage <- lift (MU.unsafeRead mutableCoverage wordIndex)
+        let localValues = reframedWord target local wordIndex
+            localCoverage = reframedCoverageWord target (hexRegionLayout local) wordIndex
+            disagreement = (values `xor` localValues) .&. coverage .&. localCoverage
+        if disagreement == 0
+          then do
+            lift (MU.unsafeWrite mutableValues wordIndex (values .|. localValues))
+            lift (MU.unsafeWrite mutableCoverage wordIndex (coverage .|. localCoverage))
+            descendHexWordSpan (wordIndex + 1) finalWord
+          else
+            throwE
+              (hexCoordAtIndex target (wordIndex * 64 + countTrailingZeros disagreement))
 
+localTargetWordSpans :: HexLayout -> HexLayout -> [(Int, Int)]
+localTargetWordSpans target local =
+  foldr mergeWordSpan [] (fmap rowWordSpan [0 .. layoutHeight local - 1])
+ where
+  HexCoord targetQ targetR = layoutOrigin target
+  HexCoord localQ localR = layoutOrigin local
+  targetColumn = localQ - targetQ
+  firstTargetRow = localR - targetR
+
+  rowWordSpan :: Int -> (Int, Int)
+  rowWordSpan localRow =
+    let firstCell =
+          (firstTargetRow + localRow) * layoutWidth target + targetColumn
+        finalCell = firstCell + layoutWidth local - 1
+     in (firstCell `quot` 64, finalCell `quot` 64)
+
+  mergeWordSpan :: (Int, Int) -> [(Int, Int)] -> [(Int, Int)]
+  mergeWordSpan wordSpan [] = [wordSpan]
+  mergeWordSpan wordSpan@(firstWord, finalWord) ((nextFirstWord, nextFinalWord) : remaining)
+    | nextFirstWord <= finalWord + 1 =
+        (firstWord, max finalWord nextFinalWord) : remaining
+    | otherwise = wordSpan : (nextFirstWord, nextFinalWord) : remaining
+
 boundingLayout :: NonEmpty HexRegion -> Either HexGluingObstruction HexLayout
 boundingLayout regions =
   let layouts = fmap hexRegionLayout regions
@@ -473,37 +561,6 @@
               (accumulator .|. copied)
 {-# INLINE reframedSourceWord #-}
 
-extractBits :: U.Vector Word64 -> Int -> Int -> Word64
-extractBits wordsValue bitIndex count
-  | count <= 0 = 0
-  | otherwise =
-      let (wordIndex, bitOffset) = bitIndex `quotRem` 64
-          low = wordsValue `U.unsafeIndex` wordIndex `shiftR` bitOffset
-          high =
-            if bitOffset == 0 || bitOffset + count <= 64
-              then 0
-              else wordsValue `U.unsafeIndex` (wordIndex + 1) `shiftL` (64 - bitOffset)
-       in (low .|. high) .&. lowBitMask count
-{-# INLINE extractBits #-}
-
-hexCoordIndex :: HexLayout -> HexCoord -> Maybe Int
-hexCoordIndex layout (HexCoord q r)
-  | q < originQ || r < originR = Nothing
-  | q > maximumQ || r > maximumR = Nothing
-  | otherwise = Just ((r - originR) * layoutWidth layout + (q - originQ))
- where
-  HexCoord originQ originR = layoutOrigin layout
-  maximumQ = originQ + layoutWidth layout - 1
-  maximumR = originR + layoutHeight layout - 1
-{-# INLINE hexCoordIndex #-}
-
-hexCoordAtIndex :: HexLayout -> Int -> HexCoord
-hexCoordAtIndex layout index =
-  let (row, column) = index `quotRem` layoutWidth layout
-      HexCoord originQ originR = layoutOrigin layout
-   in HexCoord (originQ + column) (originR + row)
-{-# INLINE hexCoordAtIndex #-}
-
 coordinateRangeFits :: HexCoord -> Int -> Int -> Bool
 coordinateRangeFits (HexCoord originQ originR) width height =
   let maximumQ = toInteger originQ + toInteger width - 1
@@ -528,22 +585,6 @@
 layoutMaximumR :: HexLayout -> Integer
 layoutMaximumR layout =
   toInteger (hexR (layoutOrigin layout)) + toInteger (layoutHeight layout) - 1
-
-finalWordMask :: HexLayout -> Word64
-finalWordMask layout =
-  lowBitMask (((layoutCellCount layout - 1) `rem` 64) + 1)
-{-# INLINE finalWordMask #-}
-
-lowBitMask :: Int -> Word64
-lowBitMask count
-  | count >= 64 = maxBound
-  | count <= 0 = 0
-  | otherwise = (1 `shiftL` count) - 1
-{-# INLINE lowBitMask #-}
-
-setBitAt :: Word64 -> Int -> Word64
-setBitAt word bitIndex = word .|. (1 `shiftL` bitIndex)
-{-# INLINE setBitAt #-}
 
 integerToInt :: Integer -> Maybe Int
 integerToInt value
diff --git a/src-hex/Moonlight/Hex/Region/Internal.hs b/src-hex/Moonlight/Hex/Region/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src-hex/Moonlight/Hex/Region/Internal.hs
@@ -0,0 +1,157 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Trusted packed representation shared by the public region algebra and its
+-- topology interpretation. This module is private to the @hex@ component.
+module Moonlight.Hex.Region.Internal
+  ( HexLayout (..)
+  , HexRegion (..)
+  , extractBits
+  , finalWordMask
+  , firstSameLayoutDifferenceIndex
+  , generateHexRegionIndexed
+  , generateHexRegionIndexedM
+  , hexCoordAtIndex
+  , hexCoordIndex
+  , hexRegionMemberAtIndex
+  , lowBitMask
+  , setBitAt
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bits ((.&.), (.|.), complement, countTrailingZeros, shiftL, shiftR)
+import Data.Vector.Unboxed qualified as U
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+import Moonlight.Hex.Coordinate (HexCoord (..))
+
+-- | A nonempty axial parallelogram with an overflow-safe dense row-major index.
+data HexLayout = HexLayout
+  { layoutOrigin :: !HexCoord
+  , layoutWidth :: !Int
+  , layoutHeight :: !Int
+  , layoutCellCount :: !Int
+  , layoutWordCount :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Canonical packed membership: one bit per layout cell and zero padding.
+data HexRegion = HexRegion !HexLayout !(U.Vector Word64)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Construct canonical packed membership from a dense row-major predicate.
+-- The index is private representation vocabulary; public callers author by
+-- coordinate through 'Moonlight.Hex.Region.hexRegionGenerateM'.
+generateHexRegionIndexedM
+  :: Monad effect
+  => HexLayout
+  -> (Int -> effect Bool)
+  -> effect HexRegion
+generateHexRegionIndexedM layout predicate =
+  HexRegion layout <$> U.generateM (layoutWordCount layout) generateWord
+ where
+  generateWord wordIndex =
+    let baseIndex = wordIndex * 64
+        bitCount = min 64 (layoutCellCount layout - baseIndex)
+     in gatherBits baseIndex bitCount 0 0
+
+  gatherBits !baseIndex !bitCount !bitIndex !word
+    | bitIndex >= bitCount = pure word
+    | otherwise = do
+        selected <- predicate (baseIndex + bitIndex)
+        gatherBits
+          baseIndex
+          bitCount
+          (bitIndex + 1)
+          (if selected then setBitAt word bitIndex else word)
+{-# INLINE generateHexRegionIndexedM #-}
+
+generateHexRegionIndexed :: HexLayout -> (Int -> Bool) -> HexRegion
+generateHexRegionIndexed layout predicate =
+  HexRegion layout (U.generate (layoutWordCount layout) generateWord)
+ where
+  generateWord wordIndex =
+    let baseIndex = wordIndex * 64
+        bitCount = min 64 (layoutCellCount layout - baseIndex)
+     in gatherBits baseIndex bitCount 0 0
+
+  gatherBits !baseIndex !bitCount !bitIndex !word
+    | bitIndex >= bitCount = word
+    | predicate (baseIndex + bitIndex) =
+        gatherBits baseIndex bitCount (bitIndex + 1) (setBitAt word bitIndex)
+    | otherwise = gatherBits baseIndex bitCount (bitIndex + 1) word
+{-# INLINE generateHexRegionIndexed #-}
+
+-- | Membership at an index already proved to belong to the region's layout.
+hexRegionMemberAtIndex :: HexRegion -> Int -> Bool
+hexRegionMemberAtIndex (HexRegion _ wordsValue) index =
+  let (wordIndex, bitIndex) = index `quotRem` 64
+   in wordsValue `U.unsafeIndex` wordIndex .&. (1 `shiftL` bitIndex) /= 0
+{-# INLINE hexRegionMemberAtIndex #-}
+
+-- | First selected cell in the left region absent from the right region.
+-- Callers establish equal layouts, which entails equal canonical word counts.
+firstSameLayoutDifferenceIndex :: HexRegion -> HexRegion -> Maybe Int
+firstSameLayoutDifferenceIndex (HexRegion _ left) (HexRegion _ right) = descend 0
+ where
+  descend wordIndex
+    | wordIndex >= U.length left = Nothing
+    | otherwise =
+        let difference =
+              left `U.unsafeIndex` wordIndex
+                .&. complement (right `U.unsafeIndex` wordIndex)
+         in if difference == 0
+              then descend (wordIndex + 1)
+              else Just (wordIndex * 64 + countTrailingZeros difference)
+{-# INLINE firstSameLayoutDifferenceIndex #-}
+
+extractBits :: U.Vector Word64 -> Int -> Int -> Word64
+extractBits wordsValue bitIndex count
+  | count <= 0 = 0
+  | otherwise =
+      let (wordIndex, bitOffset) = bitIndex `quotRem` 64
+          low = wordsValue `U.unsafeIndex` wordIndex `shiftR` bitOffset
+          high =
+            if bitOffset == 0 || bitOffset + count <= 64
+              then 0
+              else wordsValue `U.unsafeIndex` (wordIndex + 1) `shiftL` (64 - bitOffset)
+       in (low .|. high) .&. lowBitMask count
+{-# INLINE extractBits #-}
+
+hexCoordIndex :: HexLayout -> HexCoord -> Maybe Int
+hexCoordIndex layout (HexCoord q r)
+  | q < originQ || r < originR = Nothing
+  | q > maximumQ || r > maximumR = Nothing
+  | otherwise = Just ((r - originR) * layoutWidth layout + (q - originQ))
+ where
+  HexCoord originQ originR = layoutOrigin layout
+  maximumQ = originQ + layoutWidth layout - 1
+  maximumR = originR + layoutHeight layout - 1
+{-# INLINE hexCoordIndex #-}
+
+hexCoordAtIndex :: HexLayout -> Int -> HexCoord
+hexCoordAtIndex layout index =
+  let (row, column) = index `quotRem` layoutWidth layout
+      HexCoord originQ originR = layoutOrigin layout
+   in HexCoord (originQ + column) (originR + row)
+{-# INLINE hexCoordAtIndex #-}
+
+finalWordMask :: HexLayout -> Word64
+finalWordMask layout =
+  lowBitMask (((layoutCellCount layout - 1) `rem` 64) + 1)
+{-# INLINE finalWordMask #-}
+
+lowBitMask :: Int -> Word64
+lowBitMask count
+  | count >= 64 = maxBound
+  | count <= 0 = 0
+  | otherwise = (1 `shiftL` count) - 1
+{-# INLINE lowBitMask #-}
+
+setBitAt :: Word64 -> Int -> Word64
+setBitAt word bitIndex = word .|. (1 `shiftL` bitIndex)
+{-# INLINE setBitAt #-}
diff --git a/src-hex/Moonlight/Hex/Topology.hs b/src-hex/Moonlight/Hex/Topology.hs
new file mode 100644
--- /dev/null
+++ b/src-hex/Moonlight/Hex/Topology.hs
@@ -0,0 +1,373 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Packed neighbourhood morphology and graph topology over native hex regions.
+-- Dilation and erosion form an adjunction on each bounded layout; components
+-- and distances are opaque views derived from one admitted selection.
+module Moonlight.Hex.Topology
+  ( hexRegionDilate
+  , hexRegionErode
+  , hexRegionOpening
+  , hexRegionClosing
+  , hexRegionInnerFrontier
+  , hexRegionOuterFrontier
+  , HexComponentLabels
+  , hexRegionComponentLabels
+  , hexComponentCount
+  , hexComponentIndexAt
+  , hexComponentRegion
+  , HexTraversalObstruction (..)
+  , HexDistanceMap
+  , hexRegionDistancesWithin
+  , hexDistanceMapLayout
+  , hexDistanceAt
+  , hexDistanceMaximum
+  , hexDistanceReachableRegion
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad.ST (ST, runST)
+import Data.Bits ((.&.), (.|.), complement, shiftL)
+import Data.Vector.Unboxed qualified as U
+import Data.Vector.Unboxed.Mutable qualified as MU
+import Data.Word (Word64)
+import GHC.Generics (Generic)
+import Moonlight.Hex.Coordinate
+  ( HexCoord (..)
+  , HexDirection
+  , allHexDirections
+  , hexDirectionDelta
+  )
+import Moonlight.Hex.Region (complementHexRegion)
+import Moonlight.Hex.Region.Internal
+  ( HexLayout (..)
+  , HexRegion (..)
+  , extractBits
+  , firstSameLayoutDifferenceIndex
+  , generateHexRegionIndexed
+  , hexCoordAtIndex
+  , hexCoordIndex
+  , hexRegionMemberAtIndex
+  )
+
+-- | One closed-neighbourhood step, clipped to the owning layout.
+hexRegionDilate :: HexRegion -> HexRegion
+hexRegionDilate region@(HexRegion layout _) =
+  HexRegion layout (U.generate (layoutWordCount layout) (dilatedHexWord region))
+{-# INLINE hexRegionDilate #-}
+
+-- | Retain exactly the cells whose six neighbours are selected. A neighbour
+-- outside the layout imposes no obligation: this is the right adjoint of
+-- clipped dilation, so opening and closing remain lawful on bounded layouts.
+hexRegionErode :: HexRegion -> HexRegion
+hexRegionErode region@(HexRegion layout wordsValue) =
+  let absent = complementHexRegion region
+   in HexRegion layout
+        (U.generate (layoutWordCount layout) (erodedHexWord absent wordsValue))
+{-# INLINE hexRegionErode #-}
+
+dilatedHexWord :: HexRegion -> Int -> Word64
+dilatedHexWord region@(HexRegion _ wordsValue) wordIndex =
+  foldl'
+    (\word direction -> word .|. translatedHexWord region direction wordIndex)
+    (wordsValue `U.unsafeIndex` wordIndex)
+    allHexDirections
+{-# INLINE dilatedHexWord #-}
+
+erodedHexWord :: HexRegion -> U.Vector Word64 -> Int -> Word64
+erodedHexWord absent selectedWords wordIndex =
+  foldl'
+    (\word direction ->
+       word .&. complement (translatedHexWord absent direction wordIndex))
+    (selectedWords `U.unsafeIndex` wordIndex)
+    allHexDirections
+{-# INLINE erodedHexWord #-}
+
+-- | Erosion followed by dilation in the same bounded layout.
+hexRegionOpening :: HexRegion -> HexRegion
+hexRegionOpening = hexRegionDilate . hexRegionErode
+
+-- | Dilation followed by erosion in the same bounded layout.
+hexRegionClosing :: HexRegion -> HexRegion
+hexRegionClosing = hexRegionErode . hexRegionDilate
+
+-- | Selected cells adjacent to absence, including absence beyond the layout.
+hexRegionInnerFrontier :: HexRegion -> HexRegion
+hexRegionInnerFrontier source@(HexRegion layout sourceWords) =
+  let absent = complementHexRegion source
+   in HexRegion layout
+        ( U.generate
+            (layoutWordCount layout)
+            (\wordIndex ->
+               sourceWords `U.unsafeIndex` wordIndex
+                 .&. complement (erodedHexWord absent sourceWords wordIndex))
+        )
+
+-- | Unselected cells inside the layout adjacent to a selected cell.
+hexRegionOuterFrontier :: HexRegion -> HexRegion
+hexRegionOuterFrontier source@(HexRegion layout sourceWords) =
+  HexRegion layout
+    ( U.generate
+        (layoutWordCount layout)
+        (\wordIndex ->
+           dilatedHexWord source wordIndex
+             .&. complement (sourceWords `U.unsafeIndex` wordIndex))
+    )
+
+-- | Canonical zero-based component labels in row-major seed order. Unselected
+-- cells carry no public label and the view owns no independent membership.
+data HexComponentLabels = HexComponentLabels !HexLayout {-# UNPACK #-} !Int !(U.Vector Int)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+hexRegionComponentLabels :: HexRegion -> HexComponentLabels
+hexRegionComponentLabels region@(HexRegion layout _) = runST $ do
+  labels <- MU.replicate (layoutCellCount layout) (-1)
+  queue <- MU.new (layoutCellCount layout)
+  count <- labelHexComponents region labels queue 0 0
+  HexComponentLabels layout count <$> U.unsafeFreeze labels
+
+hexComponentCount :: HexComponentLabels -> Int
+hexComponentCount (HexComponentLabels _ count _) = count
+{-# INLINE hexComponentCount #-}
+
+-- | The component of a selected coordinate. Absence means either an
+-- unselected coordinate or one outside the labels' layout.
+hexComponentIndexAt :: HexCoord -> HexComponentLabels -> Maybe Int
+hexComponentIndexAt coordinate (HexComponentLabels layout _ labels) = do
+  index <- hexCoordIndex layout coordinate
+  let component = labels `U.unsafeIndex` index
+  if component < 0 then Nothing else Just component
+{-# INLINE hexComponentIndexAt #-}
+
+-- | Materialize one component on demand; invalid component indices are
+-- rejected without fabricating an empty component.
+hexComponentRegion :: Int -> HexComponentLabels -> Maybe HexRegion
+hexComponentRegion requested (HexComponentLabels layout count labels)
+  | requested < 0 || requested >= count = Nothing
+  | otherwise = Just (generateHexRegionIndexed layout ((== requested) . (labels `U.unsafeIndex`)))
+
+-- | Typed incompatibilities for traversal inside a selected domain.
+data HexTraversalObstruction
+  = HexTraversalLayoutMismatch !HexLayout !HexLayout
+  | HexTraversalSourceOutsideDomain !HexCoord
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Dense shortest-path distances through one hex region. Negative internal
+-- entries are unreachable and never escape through the public observation.
+data HexDistanceMap = HexDistanceMap !HexLayout !(U.Vector Int)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Multi-source unit distances through selected domain cells.
+hexRegionDistancesWithin
+  :: HexRegion
+  -> HexRegion
+  -> Either HexTraversalObstruction HexDistanceMap
+hexRegionDistancesWithin domain@(HexRegion domainLayout _) sources@(HexRegion sourceLayout _)
+  | domainLayout /= sourceLayout = Left (HexTraversalLayoutMismatch domainLayout sourceLayout)
+  | Just sourceIndex <- firstSameLayoutDifferenceIndex sources domain =
+      Left (HexTraversalSourceOutsideDomain (hexCoordAtIndex domainLayout sourceIndex))
+  | otherwise = Right (runST buildDistances)
+ where
+  buildDistances :: forall s. ST s HexDistanceMap
+  buildDistances = do
+    distances <- MU.replicate (layoutCellCount domainLayout) (-1)
+    queue <- MU.new (layoutCellCount domainLayout)
+    queued <- seedHexDistances sources distances queue 0 0
+    descendHexDistances domain distances queue 0 queued
+    HexDistanceMap domainLayout <$> U.unsafeFreeze distances
+
+hexDistanceMapLayout :: HexDistanceMap -> HexLayout
+hexDistanceMapLayout (HexDistanceMap layout _) = layout
+{-# INLINE hexDistanceMapLayout #-}
+
+hexDistanceAt :: HexCoord -> HexDistanceMap -> Maybe Int
+hexDistanceAt coordinate (HexDistanceMap layout distances) = do
+  index <- hexCoordIndex layout coordinate
+  let distance = distances `U.unsafeIndex` index
+  if distance < 0 then Nothing else Just distance
+{-# INLINE hexDistanceAt #-}
+
+hexDistanceMaximum :: HexDistanceMap -> Maybe Int
+hexDistanceMaximum (HexDistanceMap _ distances) =
+  let maximumValue = U.foldl' max (-1) distances
+   in if maximumValue < 0 then Nothing else Just maximumValue
+
+hexDistanceReachableRegion :: HexDistanceMap -> HexRegion
+hexDistanceReachableRegion (HexDistanceMap layout distances) =
+  generateHexRegionIndexed layout ((>= 0) . (distances `U.unsafeIndex`))
+
+translatedHexWord :: HexRegion -> HexDirection -> Int -> Word64
+translatedHexWord (HexRegion layout wordsValue) direction wordIndex =
+  gather 0 (wordIndex * 64) 0
+ where
+  HexCoord deltaQ deltaR = hexDirectionDelta direction
+
+  gather !destinationShift !targetIndex !accumulator
+    | destinationShift >= 64 || targetIndex >= layoutCellCount layout = accumulator
+    | otherwise =
+        let (targetRow, targetColumn) = targetIndex `quotRem` layoutWidth layout
+            rowRun = min (64 - destinationShift) (layoutWidth layout - targetColumn)
+            sourceRow = targetRow - deltaR
+            finalTargetColumn = targetColumn + rowRun - 1
+            copiedTargetColumn = max targetColumn deltaQ
+            copiedFinalTargetColumn = min finalTargetColumn (layoutWidth layout - 1 + deltaQ)
+            hasRow = sourceRow >= 0 && sourceRow < layoutHeight layout
+            hasColumns = copiedTargetColumn <= copiedFinalTargetColumn
+            copied =
+              if hasRow && hasColumns
+                then
+                  let leading = copiedTargetColumn - targetColumn
+                      copiedCount = copiedFinalTargetColumn - copiedTargetColumn + 1
+                      sourceColumn = copiedTargetColumn - deltaQ
+                      sourceBitIndex = sourceRow * layoutWidth layout + sourceColumn
+                   in extractBits wordsValue sourceBitIndex copiedCount
+                        `shiftL` (destinationShift + leading)
+                else 0
+         in gather
+              (destinationShift + rowRun)
+              (targetIndex + rowRun)
+              (accumulator .|. copied)
+{-# INLINE translatedHexWord #-}
+
+labelHexComponents
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> ST s Int
+labelHexComponents region@(HexRegion layout _) labels queue index component
+  | index >= layoutCellCount layout = pure component
+  | not (hexRegionMemberAtIndex region index) =
+      labelHexComponents region labels queue (index + 1) component
+  | otherwise = do
+      existing <- MU.unsafeRead labels index
+      if existing >= 0
+        then labelHexComponents region labels queue (index + 1) component
+        else do
+          MU.unsafeWrite labels index component
+          MU.unsafeWrite queue 0 index
+          descendHexComponent region labels queue component 0 1
+          labelHexComponents region labels queue (index + 1) (component + 1)
+
+descendHexComponent
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s ()
+descendHexComponent region@(HexRegion layout _) labels queue component readIndex queued
+  | readIndex >= queued = pure ()
+  | otherwise = do
+      cellIndex <- MU.unsafeRead queue readIndex
+      nextQueued <-
+        foldHexNeighbourIndicesM
+          layout
+          cellIndex
+          (admitComponentCell region labels queue component)
+          queued
+      descendHexComponent region labels queue component (readIndex + 1) nextQueued
+
+admitComponentCell
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Int
+admitComponentCell region labels queue component queued candidate
+  | not (hexRegionMemberAtIndex region candidate) = pure queued
+  | otherwise = do
+      existing <- MU.unsafeRead labels candidate
+      if existing >= 0
+        then pure queued
+        else do
+          MU.unsafeWrite labels candidate component
+          MU.unsafeWrite queue queued candidate
+          pure (queued + 1)
+
+seedHexDistances
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> ST s Int
+seedHexDistances region@(HexRegion layout _) distances queue index queued
+  | index >= layoutCellCount layout = pure queued
+  | hexRegionMemberAtIndex region index = do
+      MU.unsafeWrite distances index 0
+      MU.unsafeWrite queue queued index
+      seedHexDistances region distances queue (index + 1) (queued + 1)
+  | otherwise =
+      seedHexDistances region distances queue (index + 1) queued
+
+descendHexDistances
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> ST s ()
+descendHexDistances domain@(HexRegion layout _) distances queue readIndex queued
+  | readIndex >= queued = pure ()
+  | otherwise = do
+      cellIndex <- MU.unsafeRead queue readIndex
+      distance <- MU.unsafeRead distances cellIndex
+      nextQueued <-
+        foldHexNeighbourIndicesM
+          layout
+          cellIndex
+          (admitDistanceCell domain distances queue (distance + 1))
+          queued
+      descendHexDistances domain distances queue (readIndex + 1) nextQueued
+
+admitDistanceCell
+  :: HexRegion
+  -> MU.MVector s Int
+  -> MU.MVector s Int
+  -> Int
+  -> Int
+  -> Int
+  -> ST s Int
+admitDistanceCell region distances queue distance queued candidate
+  | not (hexRegionMemberAtIndex region candidate) = pure queued
+  | otherwise = do
+      existing <- MU.unsafeRead distances candidate
+      if existing >= 0
+        then pure queued
+        else do
+          MU.unsafeWrite distances candidate distance
+          MU.unsafeWrite queue queued candidate
+          pure (queued + 1)
+
+foldHexNeighbourIndicesM
+  :: HexLayout
+  -> Int
+  -> (accumulator -> Int -> ST s accumulator)
+  -> accumulator
+  -> ST s accumulator
+foldHexNeighbourIndicesM layout index step initial = do
+  let (row, column) = index `quotRem` layoutWidth layout
+      hasEast = column + 1 < layoutWidth layout
+      hasNorth = row > 0
+      hasWest = column > 0
+      hasSouth = row + 1 < layoutHeight layout
+      applyIf condition candidate accumulator =
+        if condition then step accumulator candidate else pure accumulator
+  east <- applyIf hasEast (index + 1) initial
+  northEast <- applyIf (hasNorth && hasEast) (index - layoutWidth layout + 1) east
+  northWest <- applyIf hasNorth (index - layoutWidth layout) northEast
+  west <- applyIf hasWest (index - 1) northWest
+  southWest <- applyIf (hasSouth && hasWest) (index + layoutWidth layout - 1) west
+  applyIf hasSouth (index + layoutWidth layout) southWest
+{-# INLINE foldHexNeighbourIndicesM #-}
diff --git a/src-illustration/Moonlight/Planar/Illustration.hs b/src-illustration/Moonlight/Planar/Illustration.hs
new file mode 100644
--- /dev/null
+++ b/src-illustration/Moonlight/Planar/Illustration.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE DeriveFunctor #-}
+
+-- | Ordered painting of canonical curves. Parts remain ordinary author-owned
+-- values; annotations neither resolve references nor change geometry.
+module Moonlight.Planar.Illustration
+  ( Picture
+  , Color (..)
+  , Paint (..)
+  , GradientStop (..)
+  , GradientStops
+  , gradientStops
+  , gradientStopValues
+  , FillRule (..)
+  , StrokeUnits (..)
+  , LineCap (..)
+  , LineJoin (..)
+  , MiterLimit
+  , miterLimit
+  , miterLimitValue
+  , miterLimitFour
+  , IllustrationError (..)
+  , StrokeStyle (..)
+  , fill
+  , stroke
+  , clip
+  , place
+  , opacity
+  , annotate
+  , PictureAlgebra (..)
+  , foldPicture
+  ) where
+
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Sequence (Seq)
+import qualified Data.Sequence as Seq
+import Data.Word (Word8)
+import Moonlight.Planar.Affine (Affine2)
+import Moonlight.Planar.Curve (ClosedTrail, Located, Path)
+import Moonlight.Planar.Exact (ExactPoint, ExactRational, PositiveExact, UnitInterval)
+
+data Color = RGB !Word8 !Word8 !Word8
+  deriving stock (Eq, Ord, Show)
+
+-- | Gradient positions use the same local coordinates as their painted path.
+-- Opacity is explicit, rather than encoded in a color string.
+data GradientStop = GradientStop !UnitInterval !Color !UnitInterval
+  deriving stock (Eq, Show)
+
+newtype GradientStops = GradientStops (NonEmpty GradientStop)
+  deriving stock (Eq, Show)
+
+-- | One-time authoring canonicalization: increasing offsets, with stable
+-- author order at coincident offsets (which express a discontinuous edge).
+-- Browser-specific clamping of descending offsets is never needed.
+gradientStops :: NonEmpty GradientStop -> GradientStops
+gradientStops = GradientStops . NonEmpty.sortWith (\(GradientStop offset _ _) -> offset)
+
+gradientStopValues :: GradientStops -> NonEmpty GradientStop
+gradientStopValues (GradientStops stops) = stops
+
+data Paint
+  = Solid !Color
+  | LinearGradient !ExactPoint !ExactPoint !GradientStops
+  | RadialGradient !ExactPoint !PositiveExact !GradientStops
+  deriving stock (Eq, Show)
+
+data FillRule = NonZero | EvenOdd
+  deriving stock (Eq, Ord, Show)
+
+data StrokeUnits = LocalUnits | OutputUnits
+  deriving stock (Eq, Ord, Show)
+
+data LineCap = ButtCap | RoundCap | SquareCap
+  deriving stock (Eq, Ord, Show)
+
+newtype MiterLimit = MiterLimit ExactRational
+  deriving stock (Eq, Show)
+
+data IllustrationError = InvalidMiterLimit !ExactRational
+  deriving stock (Eq, Show)
+
+-- | SVG's miter ratio has a lower bound of one, not merely positivity.
+miterLimit :: ExactRational -> Either IllustrationError MiterLimit
+miterLimit value
+  | value < 1 = Left (InvalidMiterLimit value)
+  | otherwise = Right (MiterLimit value)
+
+miterLimitValue :: MiterLimit -> ExactRational
+miterLimitValue (MiterLimit value) = value
+
+miterLimitFour :: MiterLimit
+miterLimitFour = MiterLimit 4
+
+data LineJoin = MiterJoin !MiterLimit | RoundJoin | BevelJoin
+  deriving stock (Eq, Show)
+
+data StrokeStyle = StrokeStyle
+  { strokePaint :: !Paint
+  , strokeWidth :: !PositiveExact
+  , strokeUnits :: !StrokeUnits
+  , strokeCap :: !LineCap
+  , strokeJoin :: !LineJoin
+  }
+  deriving stock (Eq, Show)
+
+-- The sequence, rather than a binary composition tree, makes the monoid's
+-- associativity structural and gives publication stable sibling addresses.
+newtype Picture part = Picture (Seq (Mark part))
+  deriving stock (Eq, Show, Functor)
+
+data Mark part
+  = Filled !FillRule !Paint !(NonEmpty (Located ClosedTrail))
+  | Stroked !StrokeStyle !Path
+  | Clipped !FillRule !(NonEmpty (Located ClosedTrail)) !(Picture part)
+  | Placed !Affine2 !(Picture part)
+  | Transparent !UnitInterval !(Picture part)
+  | Annotated !part !(Picture part)
+  deriving stock (Eq, Show, Functor)
+
+instance Semigroup (Picture part) where
+  Picture behind <> Picture inFront = Picture (behind <> inFront)
+
+instance Monoid (Picture part) where
+  mempty = Picture Seq.empty
+
+fill :: FillRule -> Paint -> NonEmpty (Located ClosedTrail) -> Picture part
+fill rule paint contours = Picture (Seq.singleton (Filled rule paint contours))
+
+stroke :: StrokeStyle -> Path -> Picture part
+stroke style curve = Picture (Seq.singleton (Stroked style curve))
+
+clip :: FillRule -> NonEmpty (Located ClosedTrail) -> Picture part -> Picture part
+clip rule contours picture = Picture (Seq.singleton (Clipped rule contours picture))
+
+place :: Affine2 -> Picture part -> Picture part
+place affine picture = Picture (Seq.singleton (Placed affine picture))
+
+-- | Opacity applies once to the composite, not independently to its children.
+opacity :: UnitInterval -> Picture part -> Picture part
+opacity amount picture = Picture (Seq.singleton (Transparent amount picture))
+
+annotate :: part -> Picture part -> Picture part
+annotate part picture = Picture (Seq.singleton (Annotated part picture))
+
+-- | The interpreter boundary exposes painting operations, never SVG syntax or
+-- a second geometry AST. A carrier may be a pure environment-reading function.
+data PictureAlgebra part a = PictureAlgebra
+  { paintSequence :: Seq a -> a
+  , paintFill :: FillRule -> Paint -> NonEmpty (Located ClosedTrail) -> a
+  , paintStroke :: StrokeStyle -> Path -> a
+  , paintClip :: FillRule -> NonEmpty (Located ClosedTrail) -> a -> a
+  , paintPlace :: Affine2 -> a -> a
+  , paintOpacity :: UnitInterval -> a -> a
+  , paintAnnotation :: part -> a -> a
+  }
+
+foldPicture :: PictureAlgebra part a -> Picture part -> a
+foldPicture algebra = fold
+ where
+  fold (Picture marks) = paintSequence algebra (interpret <$> marks)
+  interpret mark = case mark of
+    Filled rule paint contours -> paintFill algebra rule paint contours
+    Stroked style curve -> paintStroke algebra style curve
+    Clipped rule contours picture -> paintClip algebra rule contours (fold picture)
+    Placed affine picture -> paintPlace algebra affine (fold picture)
+    Transparent amount picture -> paintOpacity algebra amount (fold picture)
+    Annotated part picture -> paintAnnotation algebra part (fold picture)
diff --git a/src-illustration/Moonlight/Planar/Illustration/Layout.hs b/src-illustration/Moonlight/Planar/Illustration/Layout.hs
new file mode 100644
--- /dev/null
+++ b/src-illustration/Moonlight/Planar/Illustration/Layout.hs
@@ -0,0 +1,126 @@
+-- | Conservative geometry layout, not painted-ink bounds. Stroke centerlines
+-- contribute geometry; width, clipping and opacity do not change this view.
+module Moonlight.Planar.Illustration.Layout
+  ( GeometryEnvelope
+  , geometryEnvelope
+  , transformGeometryEnvelope
+  , geometrySupport
+  , geometryBounds
+  , LayoutAxis (..)
+  , alignMotif
+  , arrangeMotifs
+  ) where
+
+import Data.Foldable (toList)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Semigroup (Max (..))
+import Data.Sequence (Seq, ViewL (..))
+import qualified Data.Sequence as Seq
+import Data.Traversable (mapAccumL)
+import Moonlight.Planar.Affine
+  ( Affine2, composeAffine2, identityAffine2, transformPoint, translationAffineIso2 )
+import Moonlight.Planar.Curve
+  ( CurveStep, Subpath (..), closedTrailSteps, curveStepEnd, locatedValue
+  , location, pathSubpaths, stepControlPoints, trailSteps )
+import Moonlight.Planar.Exact
+  ( ExactBounds, ExactPoint, ExactRational, ExactVector (..), UnitInterval
+  , boundsMaximumX, boundsMaximumY, boundsMinimumX, boundsMinimumY
+  , exactPointCoordinates, exactPointsBounds, translateExactPoint, unitIntervalValue )
+import Moonlight.Planar.Illustration (Picture, PictureAlgebra (..), foldPicture)
+import Moonlight.Planar.Illustration.Motif (Motif, motifPicture, transformMotif)
+
+-- Finite control support. Representation equality is not envelope equality.
+newtype GeometryEnvelope = GeometryEnvelope (Seq ExactPoint)
+
+instance Semigroup GeometryEnvelope where
+  GeometryEnvelope a <> GeometryEnvelope b = GeometryEnvelope (a <> b)
+
+instance Monoid GeometryEnvelope where
+  mempty = GeometryEnvelope Seq.empty
+
+-- | Each control point is transformed once after accumulating enclosing maps.
+-- A located empty trail retains its anchor; an empty path/picture stays empty.
+geometryEnvelope :: Picture part -> GeometryEnvelope
+geometryEnvelope picture = foldPicture algebra picture identityAffine2
+ where
+  algebra :: PictureAlgebra part (Affine2 -> GeometryEnvelope)
+  algebra = PictureAlgebra
+    { paintSequence = \children frame -> foldMap ($ frame) children
+    , paintFill = \_ _ contours frame -> foldMap (subpathEnvelope frame . ClosedSubpath) contours
+    , paintStroke = \_ curve frame -> foldMap (subpathEnvelope frame) (pathSubpaths curve)
+    , paintClip = \_ _ child -> child
+    , paintPlace = \local child outer -> child (composeAffine2 outer local)
+    , paintOpacity = \_ child -> child
+    , paintAnnotation = \_ child -> child
+    }
+
+subpathEnvelope :: Affine2 -> Subpath -> GeometryEnvelope
+subpathEnvelope frame subpath = case subpath of
+  OpenSubpath value -> controlsEnvelope frame (location value) (trailSteps (locatedValue value))
+  ClosedSubpath value -> controlsEnvelope frame (location value) (closedTrailSteps (locatedValue value))
+
+controlsEnvelope :: Affine2 -> ExactPoint -> Seq CurveStep -> GeometryEnvelope
+controlsEnvelope frame anchor steps = GeometryEnvelope $
+  Seq.singleton (transformPoint frame anchor) <> foldMap id (snd (mapAccumL advance anchor steps))
+ where
+  advance :: ExactPoint -> CurveStep -> (ExactPoint, Seq ExactPoint)
+  advance origin step =
+    ( translateExactPoint origin (curveStepEnd step)
+    , Seq.fromList (toList (transformPoint frame . translateExactPoint origin <$> stepControlPoints step))
+    )
+
+transformGeometryEnvelope :: Affine2 -> GeometryEnvelope -> GeometryEnvelope
+transformGeometryEnvelope frame (GeometryEnvelope points) =
+  GeometryEnvelope (transformPoint frame <$> points)
+
+-- | Maximum unnormalised directional projection; Nothing is the empty case.
+geometrySupport :: ExactVector -> GeometryEnvelope -> Maybe ExactRational
+geometrySupport (ExactVector x y) (GeometryEnvelope points) =
+  getMax <$> foldMap (Just . Max . project) points
+ where
+  project :: ExactPoint -> ExactRational
+  project point = let (px, py) = exactPointCoordinates point in x * px + y * py
+
+geometryBounds :: GeometryEnvelope -> Maybe ExactBounds
+geometryBounds (GeometryEnvelope points) = case Seq.viewl points of
+  EmptyL -> Nothing
+  first :< rest -> Just (exactPointsBounds (first :| toList rest))
+
+data LayoutAxis = Horizontal | Vertical
+  deriving stock (Eq, Ord, Show)
+
+-- | Align a minimum (0), midpoint (1/2), maximum (1), or interpolated edge.
+-- Empty geometry remains unchanged; the perpendicular coordinate is retained.
+alignMotif :: LayoutAxis -> UnitInterval -> ExactRational -> Motif port part -> Motif port part
+alignMotif axis fraction target value = case geometryBounds (geometryEnvelope (motifPicture value)) of
+  Nothing -> value
+  Just bounds ->
+    let (lo, hi) = axisInterval axis bounds
+        q = unitIntervalValue fraction
+     in shiftMotif axis (target - ((1 - q) * lo + q * hi)) value
+
+-- | Measure each child once, preserving order and the first nonempty position.
+-- Negative gaps overlap. Empty geometry neither consumes nor advances a gap.
+arrangeMotifs :: LayoutAxis -> ExactRational -> Seq (Motif port part) -> Seq (Motif port part)
+arrangeMotifs axis gap values = snd (mapAccumL arrange Nothing (measure <$> values))
+ where
+  measure :: Motif port part -> (Motif port part, Maybe (ExactRational, ExactRational))
+  measure value = (value, axisInterval axis <$> geometryBounds (geometryEnvelope (motifPicture value)))
+  arrange :: Maybe ExactRational -> (Motif port part, Maybe (ExactRational, ExactRational))
+          -> (Maybe ExactRational, Motif port part)
+  arrange edge (value, Nothing) = (edge, value)
+  arrange Nothing (value, Just (_, hi)) = (Just hi, value)
+  arrange (Just edge) (value, Just (lo, hi)) =
+    let delta = edge + gap - lo
+     in (Just (hi + delta), shiftMotif axis delta value)
+
+axisInterval :: LayoutAxis -> ExactBounds -> (ExactRational, ExactRational)
+axisInterval Horizontal bounds = (boundsMinimumX bounds, boundsMaximumX bounds)
+axisInterval Vertical bounds = (boundsMinimumY bounds, boundsMaximumY bounds)
+
+shiftMotif :: LayoutAxis -> ExactRational -> Motif port part -> Motif port part
+shiftMotif axis delta = transformMotif (translationAffineIso2 displacement)
+ where
+  displacement = case axis of
+    Horizontal -> ExactVector delta 0
+    Vertical -> ExactVector 0 delta
diff --git a/src-illustration/Moonlight/Planar/Illustration/Motif.hs b/src-illustration/Moonlight/Planar/Illustration/Motif.hs
new file mode 100644
--- /dev/null
+++ b/src-illustration/Moonlight/Planar/Illustration/Motif.hs
@@ -0,0 +1,47 @@
+-- | A picture and its semantic attachment frames share one affine action.
+-- Port vocabularies belong to authors: lookup is total, with no registration,
+-- missing-name case, or global namespace. Initial frame meaning is supplied
+-- by the author; this owner preserves coherence under subsequent placement.
+module Moonlight.Planar.Illustration.Motif
+  ( Motif
+  , motif
+  , motifPicture
+  , motifPort
+  , transformMotif
+  , attachMotif
+  ) where
+
+import Moonlight.Planar.Affine
+  ( AffineIso2, affineIsoMap, composeAffineIso2, inverseAffineIso2 )
+import Moonlight.Planar.Illustration (Picture, place)
+
+data Motif port part = Motif !(Picture part) (port -> AffineIso2)
+
+-- | Each frame maps attachment-local coordinates into the picture's space.
+-- This constructor does not prove that an authored port lies on its geometry.
+motif :: Picture part -> (port -> AffineIso2) -> Motif port part
+motif = Motif
+
+motifPicture :: Motif port part -> Picture part
+motifPicture (Motif picture _) = picture
+
+motifPort :: Motif port part -> port -> AffineIso2
+motifPort (Motif _ ports) = ports
+
+-- | Transform geometry and all ports together. Shear, scale, and reflection
+-- are allowed: this is an affine action, not a rigid-mounting promise.
+transformMotif :: AffineIso2 -> Motif port part -> Motif port part
+transformMotif transform (Motif picture ports) =
+  Motif (place (affineIsoMap transform) picture) (composeAffineIso2 transform . ports)
+{-# INLINE transformMotif #-}
+
+-- | Align the chosen source frame with the target using @target . inverse
+-- source@. This guarantees frame matching, not tangent continuity or clearance.
+attachMotif :: port -> AffineIso2 -> Motif port part -> Motif port part
+attachMotif sourcePort target value =
+  transformMotif
+    (composeAffineIso2 target (inverseAffineIso2 (motifPort value sourcePort)))
+    value
+-- Expose the source-port projection to callers so repeated attachments can
+-- share its inverse instead of retaining an opaque per-instance calculation.
+{-# INLINE attachMotif #-}
diff --git a/src-illustration/Moonlight/Planar/Illustration/Svg.hs b/src-illustration/Moonlight/Planar/Illustration/Svg.hs
new file mode 100644
--- /dev/null
+++ b/src-illustration/Moonlight/Planar/Illustration/Svg.hs
@@ -0,0 +1,410 @@
+-- | Pure, bounded SVG publication. Native polynomial curves are retained;
+-- rational conics descend through the canonical certified lowering owner.
+-- Decimal rounding is checked per scalar, separately from the output-pixel
+-- curve approximation bound. Neither receipt claims browser raster equivalence.
+module Moonlight.Planar.Illustration.Svg
+  ( SvgViewport
+  , svgViewport
+  , SvgPrecision
+  , svgPrecision
+  , SvgOptions
+  , svgOptions
+  , SvgError (..)
+  , renderSvg
+  , renderDiagnosticSvg
+  ) where
+
+import Control.Applicative ((<|>))
+import Data.Bifunctor (first)
+import Data.Char (ord)
+import Data.Foldable (toList)
+import Data.List (intercalate, mapAccumL)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Sequence (Seq)
+import Data.Word (Word8)
+import Moonlight.Planar.Affine
+  ( Affine2, affine2, affineColumns, identityAffine2, composeAffine2, transformPoint )
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, CurveStep, CurveShapeView (..), Subpath (..)
+  , location, locatedValue, locate, pathSubpaths, closedTrailSteps, trailSteps
+  , curveStepShape, curveStepEnd, shapeView )
+import Moonlight.Planar.Curve.Lowering
+  ( LoweringError, loweringPolicy, lowerStep, loweredPoints )
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactVector (..), ExactRational, PositiveExact
+  , exactPoint, exactPointCoordinates, translateExactPoint
+  , exactRationalNumerator, exactRationalDenominator, exactRationalBitWidth
+  , positiveExactValue, unitIntervalValue, divideByPositive )
+import Moonlight.Planar.Illustration
+  ( Picture, PictureAlgebra (..), foldPicture, Color (..), Paint (..)
+  , GradientStop (..), gradientStopValues, FillRule (..), StrokeStyle (..), StrokeUnits (..)
+  , LineCap (..), LineJoin (..), miterLimitValue )
+import Numeric (showHex)
+
+data SvgViewport = SvgViewport !Int !Int !ExactPoint !PositiveExact !PositiveExact
+  deriving stock (Eq, Show)
+
+-- | Pixel dimensions and an explicit, fixed view box. The interpreter uses
+-- preserveAspectRatio="none", making the declared x/y pixel metrics literal.
+svgViewport
+  :: Int -> Int -> ExactPoint -> PositiveExact -> PositiveExact
+  -> Either SvgError SvgViewport
+svgViewport width height origin extentX extentY
+  | width <= 0 || height <= 0 = Left (InvalidPixelDimensions width height)
+  | otherwise = Right (SvgViewport width height origin extentX extentY)
+
+data SvgPrecision = SvgPrecision !Int !PositiveExact
+  deriving stock (Eq, Show)
+
+-- | Maximum absolute error of each emitted decimal scalar. This is not a
+-- claim about accumulated transform error or the browser's numeric parser.
+svgPrecision :: Int -> PositiveExact -> Either SvgError SvgPrecision
+svgPrecision digits tolerance
+  | digits < 0 || digits > 30 = Left (InvalidDecimalPlaces digits)
+  | otherwise = Right (SvgPrecision digits tolerance)
+
+data SvgOptions = SvgOptions !SvgViewport !SvgPrecision !PositiveExact !Int !Int
+  deriving stock (Eq, Show)
+
+-- | Conic tolerance is in output pixels. Depth and leaves are hard budgets
+-- per source conic; exhaustion is a typed refusal, never a coarse fallback.
+svgOptions
+  :: SvgViewport -> SvgPrecision -> PositiveExact -> Int -> Int
+  -> Either SvgError SvgOptions
+svgOptions viewport precision tolerance depth leaves =
+  SvgOptions viewport precision tolerance depth leaves
+    <$ first SvgLoweringRefused (loweringPolicy tolerance identityAffine2 depth leaves)
+
+data SvgError
+  = InvalidPixelDimensions !Int !Int
+  | InvalidDecimalPlaces !Int
+  | SvgNumericPrecisionRefused !ExactRational !Int
+  | SvgNumericMagnitudeRefused !Int
+  | SvgNumericRangeRefused !ExactRational
+  | SvgPositiveValueRoundedToZero !ExactRational
+  | SvgInvalidXmlCharacter !Char
+  | SvgLoweringRefused !LoweringError
+  deriving stock (Eq, Show)
+
+data Rendered = Rendered
+  { artwork :: String
+  , guides :: String
+  , firstAnchor :: Maybe ExactPoint
+  }
+
+instance Semigroup Rendered where
+  Rendered a g p <> Rendered b h q = Rendered (a <> b) (g <> h) (p <|> q)
+
+instance Monoid Rendered where
+  mempty = Rendered "" "" Nothing
+
+type Render = [Int] -> Affine2 -> Either SvgError Rendered
+
+renderSvg :: (part -> String) -> SvgOptions -> Picture part -> Either SvgError String
+renderSvg = renderDocument False
+
+-- | The same picture and fixed camera, with an additional non-clipped guide
+-- layer containing control polygons, anchors, local axes, and part labels.
+renderDiagnosticSvg :: (part -> String) -> SvgOptions -> Picture part -> Either SvgError String
+renderDiagnosticSvg = renderDocument True
+
+renderDocument :: Bool -> (part -> String) -> SvgOptions -> Picture part -> Either SvgError String
+renderDocument diagnostic label options@(SvgOptions viewport _ _ _ _) picture = do
+  rendered <- foldPicture (svgAlgebra diagnostic label options) picture [] identityAffine2
+  opening <- documentOpening options viewport
+  pure $ opening <> artwork rendered
+    <> (if diagnostic then "<g aria-label=\"authoring guides\">" <> guides rendered <> "</g>" else "")
+    <> "</svg>"
+
+svgAlgebra :: Bool -> (part -> String) -> SvgOptions -> PictureAlgebra part Render
+svgAlgebra diagnostic label options = PictureAlgebra
+  { paintSequence = \children address metric ->
+      mconcat <$> traverse
+        (\(index, child) -> child (address <> [index]) metric)
+        (zip [0 ..] (toList children))
+  , paintFill = \rule paint contours address metric -> do
+      (definition, painted) <- renderPaint options (identifier "paint" address) paint
+      (commands, controlGuides) <- closedCommands diagnostic options metric contours
+      pure $ Rendered
+        (definition <> element "path"
+          [("d", commands), ("fill", painted), ("fill-rule", fillRuleText rule)])
+        controlGuides (Just (location (NonEmpty.head contours)))
+  , paintStroke = \style curve address metric -> do
+      (definition, painted) <- renderPaint options (identifier "paint" address) (strokePaint style)
+      width <- positiveNumber options (strokeWidth style)
+      joins <- joinAttributes options (strokeJoin style)
+      pieces <- traverse (subpathCommands diagnostic options metric) (toList (pathSubpaths curve))
+      let commands = intercalate " " (fmap commandText pieces)
+          anchors = foldr ((<|>) . commandAnchor) Nothing pieces
+      pure $ Rendered
+        (definition <> element "path"
+          ([("d", commands), ("fill", "none"), ("stroke", painted)
+           , ("stroke-width", width), ("stroke-linecap", capText (strokeCap style))]
+           <> joins <> case strokeUnits style of
+                LocalUnits -> []
+                OutputUnits -> [("vector-effect", "non-scaling-stroke")]))
+        (foldMap commandGuides pieces) anchors
+  , paintClip = \rule contours child address metric -> do
+      (commands, _) <- closedCommands False options metric contours
+      body <- child (address <> [0]) metric
+      let name = identifier "clip" address
+          definition = "<defs><clipPath id=\"" <> name <> "\" clipPathUnits=\"userSpaceOnUse\">"
+            <> element "path" [("d", commands), ("clip-rule", fillRuleText rule)]
+            <> "</clipPath></defs>"
+      pure body { artwork = definition <> group [("clip-path", "url(#" <> name <> ")")] (artwork body) }
+  , paintPlace = \affine child address metric -> do
+      matrix <- affineText options affine
+      body <- child (address <> [0]) (composeAffine2 metric affine)
+      axes <- if diagnostic then localAxes options else pure ""
+      pure $ Rendered
+        (group [("transform", matrix)] (artwork body))
+        (group [("transform", matrix)] (guides body <> axes))
+        (transformPoint affine <$> firstAnchor body)
+  , paintOpacity = \amount child address metric -> do
+      value <- number options (unitIntervalValue amount)
+      body <- child (address <> [0]) metric
+      pure body { artwork = group [("opacity", value)] (artwork body) }
+  , paintAnnotation = \part child address metric -> do
+      name <- escapeXml (label part)
+      body <- child (address <> [0]) metric
+      caption <- if diagnostic
+        then maybe (pure "") (partCaption options name) (firstAnchor body)
+        else pure ""
+      pure body
+        { artwork = group [("id", identifier "part" address), ("data-part", name)]
+            ("<title>" <> name <> "</title>" <> artwork body)
+        , guides = guides body <> caption
+        }
+  }
+
+data Commands = Commands
+  { commandText :: String
+  , commandGuides :: String
+  , commandAnchor :: Maybe ExactPoint
+  }
+
+closedCommands
+  :: Bool -> SvgOptions -> Affine2 -> NonEmpty (Located ClosedTrail)
+  -> Either SvgError (String, String)
+closedCommands diagnostic options metric contours = do
+  pieces <- traverse (subpathCommands diagnostic options metric . ClosedSubpath) (NonEmpty.toList contours)
+  pure (intercalate " " (fmap commandText pieces), foldMap commandGuides pieces)
+
+subpathCommands :: Bool -> SvgOptions -> Affine2 -> Subpath -> Either SvgError Commands
+subpathCommands diagnostic options metric subpath = case subpath of
+  OpenSubpath value -> renderTrail False (location value) (trailSteps (locatedValue value))
+  ClosedSubpath value -> renderTrail True (location value) (closedTrailSteps (locatedValue value))
+ where
+  renderTrail :: Bool -> ExactPoint -> Seq CurveStep -> Either SvgError Commands
+  renderTrail closed origin steps = do
+    start <- pointText options origin
+    let (_, locatedSteps) = mapAccumL
+          (\point step -> (translateExactPoint point (curveStepEnd step), (point, step)))
+          origin (toList steps)
+    rendered <- traverse (uncurry (stepCommands options metric)) locatedSteps
+    handles <- if diagnostic
+      then foldMap id <$> traverse (uncurry (stepGuides options)) locatedSteps
+      else pure ""
+    pure $ Commands
+      ("M " <> start <> foldMap (" " <>) rendered <> if closed then " Z" else "")
+      handles (Just origin)
+
+stepCommands :: SvgOptions -> Affine2 -> ExactPoint -> CurveStep -> Either SvgError String
+stepCommands options@(SvgOptions _ _ tolerance depth leaves) metric origin step =
+  case shapeView (curveStepShape step) of
+    LinearView -> ("L " <>) <$> pointText options endpoint
+    QuadraticView control -> polynomial "Q" [control] endpoint
+    CubicView control1 control2 -> polynomial "C" [control1, control2] endpoint
+    RationalQuadraticView _ _ _ -> do
+      policy <- first SvgLoweringRefused $
+        loweringPolicy tolerance (composeAffine2 (viewportMetric options) metric) depth leaves
+      samples <- first SvgLoweringRefused (lowerStep policy (locate origin step))
+      published <- traverse (pointText options) (NonEmpty.tail (loweredPoints samples))
+      pure (intercalate " " (fmap ("L " <>) published))
+ where
+  endpoint = translateExactPoint origin (curveStepEnd step)
+  polynomial opcode controls end =
+    ((opcode <> " ") <>) . unwords <$> traverse (pointText options)
+      (fmap (translateExactPoint origin) controls <> [end])
+
+stepGuides :: SvgOptions -> ExactPoint -> CurveStep -> Either SvgError String
+stepGuides options origin step = do
+  let endpoint = translateExactPoint origin (curveStepEnd step)
+      controls = case shapeView (curveStepShape step) of
+        LinearView -> []
+        QuadraticView c -> [c]
+        CubicView c d -> [c, d]
+        RationalQuadraticView c _ _ -> [c]
+      points = [origin] <> fmap (translateExactPoint origin) controls <> [endpoint]
+  coordinates <- traverse (pointText options) points
+  markers <- traverse marker points
+  pure $ element "polyline"
+      [("points", unwords coordinates), ("fill", "none"), ("stroke", "#d52a96")
+      , ("stroke-width", "0.7"), ("vector-effect", "non-scaling-stroke")]
+    <> concat markers
+ where
+  marker point = do
+    (x, y) <- pointAttributes options point
+    pure (element "circle" [("cx", x), ("cy", y), ("r", "1.5"), ("fill", "#d52a96")])
+
+renderPaint :: SvgOptions -> String -> Paint -> Either SvgError (String, String)
+renderPaint options name paint = case paint of
+  Solid color -> pure ("", colorText color)
+  LinearGradient start end stops -> do
+    (x1, y1) <- pointAttributes options start
+    (x2, y2) <- pointAttributes options end
+    renderedStops <- traverse (renderStop options) (gradientStopValues stops)
+    pure (definition "linearGradient" [("x1", x1), ("y1", y1), ("x2", x2), ("y2", y2)]
+      (foldMap id renderedStops), reference)
+  RadialGradient center radius stops -> do
+    (x, y) <- pointAttributes options center
+    r <- positiveNumber options radius
+    renderedStops <- traverse (renderStop options) (gradientStopValues stops)
+    pure (definition "radialGradient" [("cx", x), ("cy", y), ("r", r)]
+      (foldMap id renderedStops), reference)
+ where
+  reference = "url(#" <> name <> ")"
+  definition tag attributes content =
+    "<defs><" <> tag <> attributesText
+      ([("id", name), ("gradientUnits", "userSpaceOnUse")] <> attributes)
+      <> ">" <> content <> "</" <> tag <> "></defs>"
+
+renderStop :: SvgOptions -> GradientStop -> Either SvgError String
+renderStop options (GradientStop offset color alpha) = do
+  position <- number options (unitIntervalValue offset)
+  amount <- number options (unitIntervalValue alpha)
+  pure (element "stop" [("offset", position), ("stop-color", colorText color), ("stop-opacity", amount)])
+
+joinAttributes :: SvgOptions -> LineJoin -> Either SvgError [(String, String)]
+joinAttributes options join = case join of
+  RoundJoin -> pure [("stroke-linejoin", "round")]
+  BevelJoin -> pure [("stroke-linejoin", "bevel")]
+  MiterJoin limit -> do
+    value <- number options (miterLimitValue limit)
+    pure [("stroke-linejoin", "miter"), ("stroke-miterlimit", value)]
+
+fillRuleText :: FillRule -> String
+fillRuleText rule = case rule of NonZero -> "nonzero"; EvenOdd -> "evenodd"
+
+capText :: LineCap -> String
+capText cap = case cap of ButtCap -> "butt"; RoundCap -> "round"; SquareCap -> "square"
+
+colorText :: Color -> String
+colorText (RGB red green blue) = "#" <> foldMap channel [red, green, blue]
+ where
+  channel :: Word8 -> String
+  channel value = let hex = showHex value "" in replicate (2 - length hex) '0' <> hex
+
+identifier :: String -> [Int] -> String
+identifier category address = category <> "-" <> intercalate "-" (fmap show address)
+
+affineText :: SvgOptions -> Affine2 -> Either SvgError String
+affineText options affine = do
+  let (ExactVector a b, ExactVector c d, ExactVector e f) = affineColumns affine
+  values <- traverse (number options) [a, b, c, d, e, f]
+  pure ("matrix(" <> unwords values <> ")")
+
+viewportMetric :: SvgOptions -> Affine2
+viewportMetric (SvgOptions (SvgViewport width height _ extentX extentY) _ _ _ _) =
+  affine2
+    (ExactVector (divideByPositive (fromIntegral width) extentX) 0)
+    (ExactVector 0 (divideByPositive (fromIntegral height) extentY))
+    (ExactVector 0 0)
+
+documentOpening :: SvgOptions -> SvgViewport -> Either SvgError String
+documentOpening options (SvgViewport width height origin extentX extentY) = do
+  xy <- pointText options origin
+  x <- positiveNumber options extentX
+  y <- positiveNumber options extentY
+  pure $ "<svg xmlns=\"http://www.w3.org/2000/svg\""
+    <> attributesText [("width", show width), ("height", show height)
+       , ("viewBox", xy <> " " <> x <> " " <> y), ("preserveAspectRatio", "none")]
+    <> ">"
+
+partCaption :: SvgOptions -> String -> ExactPoint -> Either SvgError String
+partCaption options name point = do
+  (x, y) <- pointAttributes options point
+  pure $ "<text" <> attributesText
+    [("x", x), ("y", y), ("dx", "4"), ("dy", "-4"), ("fill", "#991b75")
+    , ("font-size", "8"), ("font-family", "monospace")]
+    <> ">" <> name <> "</text>"
+
+localAxes :: SvgOptions -> Either SvgError String
+localAxes options = do
+  origin <- pointText options (exactPoint 0 0)
+  x <- pointText options (exactPoint 12 0)
+  y <- pointText options (exactPoint 0 12)
+  pure $ element "path" [("d", "M " <> origin <> " L " <> x <> " M " <> origin <> " L " <> y)
+    , ("fill", "none"), ("stroke", "#288e94"), ("stroke-width", "0.7")
+    , ("vector-effect", "non-scaling-stroke")]
+
+pointAttributes :: SvgOptions -> ExactPoint -> Either SvgError (String, String)
+pointAttributes options point =
+  let (x, y) = exactPointCoordinates point
+   in (,) <$> number options x <*> number options y
+
+pointText :: SvgOptions -> ExactPoint -> Either SvgError String
+pointText options point = (\(x, y) -> x <> " " <> y) <$> pointAttributes options point
+
+positiveNumber :: SvgOptions -> PositiveExact -> Either SvgError String
+positiveNumber options value = do
+  text <- number options (positiveExactValue value)
+  if all (`elem` ("0." :: String)) text
+    then Left (SvgPositiveValueRoundedToZero (positiveExactValue value))
+    else Right text
+
+-- Integer-only decimal publication. Ties round away from zero; no Double
+-- conversion, exponent overflow, negative zero, or unchecked printf occurs.
+number :: SvgOptions -> ExactRational -> Either SvgError String
+number (SvgOptions _ (SvgPrecision digits tolerance) _ _ _) value
+  | bits > 4096 = Left (SvgNumericMagnitudeRefused bits)
+  | rounded > largestFinite * scale = Left (SvgNumericRangeRefused value)
+  | errorNumerator * exactRationalDenominator allowed
+      > exactRationalNumerator allowed * denominator * scale =
+      Left (SvgNumericPrecisionRefused value digits)
+  | otherwise = Right (sign <> whole <> fraction)
+ where
+  bits = exactRationalBitWidth value
+  largestFinite = (2 ^ (53 :: Int) - 1) * 2 ^ (971 :: Int)
+  numerator = exactRationalNumerator value
+  denominator = exactRationalDenominator value
+  scale = 10 ^ digits
+  (quotient, remainder) = (abs numerator * scale) `quotRem` denominator
+  rounded = quotient + if 2 * remainder >= denominator then 1 else 0
+  signed = if numerator < 0 then negate rounded else rounded
+  errorNumerator = abs (numerator * scale - signed * denominator)
+  allowed = positiveExactValue tolerance
+  (integerPart, fractionalPart) = rounded `quotRem` scale
+  whole = show integerPart
+  fractionalText = show fractionalPart
+  fraction = if digits == 0 then "" else "." <> replicate (digits - length fractionalText) '0' <> fractionalText
+  sign = if signed < 0 then "-" else ""
+
+-- Attributes reaching these private printers are generated tokens, certified
+-- numbers, or text already passed through escapeXml. No public attribute bag.
+attributesText :: [(String, String)] -> String
+attributesText = foldMap (\(name, value) -> " " <> name <> "=\"" <> value <> "\"")
+
+element :: String -> [(String, String)] -> String
+element tag attributes = "<" <> tag <> attributesText attributes <> "/>"
+
+group :: [(String, String)] -> String -> String
+group attributes body = "<g" <> attributesText attributes <> ">" <> body <> "</g>"
+
+escapeXml :: String -> Either SvgError String
+escapeXml = fmap concat . traverse escape
+ where
+  escape char = case char of
+    '&' -> Right "&amp;"
+    '<' -> Right "&lt;"
+    '>' -> Right "&gt;"
+    '"' -> Right "&quot;"
+    '\'' -> Right "&apos;"
+    _ | validXml char -> Right [char]
+      | otherwise -> Left (SvgInvalidXmlCharacter char)
+  validXml char = let code = ord char in
+    code == 9 || code == 10 || code == 13
+      || code >= 0x20 && code <= 0xd7ff
+      || code >= 0xe000 && code <= 0xfffd
+      || code >= 0x10000 && code <= 0x10ffff
diff --git a/src-overlay/Moonlight/Planar/Embedding.hs b/src-overlay/Moonlight/Planar/Embedding.hs
new file mode 100644
--- /dev/null
+++ b/src-overlay/Moonlight/Planar/Embedding.hs
@@ -0,0 +1,546 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Bounded certification that projection of a declared exact arrangement to
+-- binary64 preserves its local incidence and rotation data.
+module Moonlight.Planar.Embedding
+  ( DraftId (..)
+  , DraftVertexId
+  , DraftSegmentId
+  , DraftSourceId
+  , DraftIncidence (..)
+  , DraftNeighborhood (..)
+  , ExactArrangementDraft (..)
+  , DraftReference (..)
+  , OverlayEmbeddingObstruction (..)
+  , EmbeddingObligation (..)
+  , EmbeddingResidual
+  , residualUndischargedObligations
+  , milestoneOneResidual
+  , LocalEmbeddingCertificate
+  , certificateDistinctRoundedVertexCount
+  , certificateSplitOrderPreservationCount
+  , certificateIncidenceRelationPreservationCount
+  , certificateNeighborhoodRotationPreservationCount
+  , certificateRoundedVertices
+  , certificateResidual
+  , certifyLocalEmbedding
+  ) where
+
+import Control.DeepSeq (NFData)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , SegmentRelation
+  , exactPointToEmbeddingCandidate
+  , exactSegmentRelation
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( cyclePairs
+  , unorderedPairs
+  )
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import qualified Moonlight.Planar.Internal.Predicates as Admitted
+  ( orient2d
+  , segmentRelation
+  )
+import Moonlight.Planar.Point (Point (..), PointValidationError, QueryPoint, queryPointValue)
+
+-- | Phantom draft kind; three incompatible identifiers share one scalar owner.
+data DraftEntity = DraftVertexEntity | DraftSegmentEntity | DraftSourceEntity
+
+newtype DraftId (entity :: DraftEntity) = DraftId Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+type DraftVertexId = DraftId 'DraftVertexEntity
+type DraftSegmentId = DraftId 'DraftSegmentEntity
+type DraftSourceId = DraftId 'DraftSourceEntity
+
+-- | A declared relation between two atomic draft segments.
+data DraftIncidence = DraftIncidence
+  { -- | First declared atomic segment.
+    draftIncidenceFirstSegment :: !DraftSegmentId
+  , -- | Second declared atomic segment.
+    draftIncidenceSecondSegment :: !DraftSegmentId
+  , -- | Relation declared to hold in both exact and rounded geometry.
+    draftIncidenceRelation :: !SegmentRelation
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One exact vertex and the cyclic neighbor order declared around it.
+data DraftNeighborhood = DraftNeighborhood
+  { -- | Center of the declared local rotation.
+    draftNeighborhoodCenter :: !DraftVertexId
+  , -- | Neighbors in cyclic rotation order.
+    draftNeighborhoodNeighbors :: !(NonEmpty DraftVertexId)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A hand-built exact arrangement draft. It contains only declared local
+-- structure; no arrangement or global crossing search is derived here.
+data ExactArrangementDraft = ExactArrangementDraft
+  { -- | Exact coordinates keyed by draft-local vertex identity.
+    draftVertices :: !(Map DraftVertexId ExactPoint)
+  , -- | Atomic segment endpoint identities.
+    draftSegments :: !(Map DraftSegmentId (DraftVertexId, DraftVertexId))
+  , -- | Exact split parameters and vertices in source-segment order.
+    draftSourceMemberships :: !(Map DraftSourceId [(ExactRational, DraftVertexId)])
+  , -- | Segment relations declared to remain invariant under projection.
+    draftIncidences :: ![DraftIncidence]
+  , -- | Cyclic local rotations declared to retain their neighbor order.
+    draftNeighborhoods :: ![DraftNeighborhood]
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The typed identity of a missing draft reference.
+data DraftReference
+  = DraftVertexReference !DraftVertexId
+  | DraftSegmentReference !DraftSegmentId
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A witness that prevents local embedding certification.
+data OverlayEmbeddingObstruction
+  = DraftReferenceMissing !DraftReference
+  | VertexProjectionRefused !DraftVertexId !PointValidationError
+  | RoundedVerticesCollide !DraftVertexId !DraftVertexId !Point
+  | SplitOrderNotPreserved !DraftSourceId !DraftVertexId !DraftVertexId
+  | IncidenceRelationChanged
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+      !SegmentRelation
+      !SegmentRelation
+  | NeighborhoodRotationChanged
+      !DraftVertexId
+      !(NonEmpty DraftVertexId)
+      !(NonEmpty DraftVertexId)
+  | GlobalRelationAdded
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+  | GlobalRelationRemoved
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+  | GlobalRelationChanged
+      !DraftSegmentId
+      !DraftSegmentId
+      !SegmentRelation
+      !SegmentRelation
+  | ProjectedSegmentCollapsed
+      !DraftSegmentId
+      !DraftVertexId
+      !DraftVertexId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | An embedding obligation not discharged by the bounded local certifier.
+data EmbeddingObligation
+  = GlobalNoNewCrossing
+  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A nonempty collection of obligations deferred to a later owner.
+newtype EmbeddingResidual = EmbeddingResidual (NonEmpty EmbeddingObligation)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Read the obligations that remain explicitly undischarged.
+residualUndischargedObligations
+  :: EmbeddingResidual
+  -> NonEmpty EmbeddingObligation
+residualUndischargedObligations (EmbeddingResidual obligations) = obligations
+
+-- | The Milestone 1 residual: the arrangement sweep has not yet proved global
+-- absence of new crossings.
+milestoneOneResidual :: EmbeddingResidual
+milestoneOneResidual = EmbeddingResidual (GlobalNoNewCrossing :| [])
+
+-- | A certificate for exactly four local obligations on a declared draft:
+-- vertex distinctness, source split order, declared incidences, and local
+-- neighborhood rotation. Global absence of new crossings is unproved until
+-- the Milestone 2 arrangement sweep supplies the complete obligation set.
+data LocalEmbeddingCertificate = LocalEmbeddingCertificate
+  { -- | Number of rounded vertices confirmed distinct. Each admitted vertex
+    -- is entered once into the rounded-point grouping and the grouping has
+    -- no collision, so this is the number of rounded groups: the vertex
+    -- count of the admitted projection, not a pair count.
+    certificateDistinctRoundedVertexCount :: !Int
+  , -- | Number of adjacent source split-order checks discharged.
+    certificateSplitOrderPreservationCount :: !Int
+  , -- | Number of declared incidence checks discharged.
+    certificateIncidenceRelationPreservationCount :: !Int
+  , -- | Number of neighborhood rotation entries certified.
+    certificateNeighborhoodRotationPreservationCount :: !Int
+  , -- | The admitted candidate projection certified by the four local obligations.
+    certificateRoundedVertices :: !(Map DraftVertexId QueryPoint)
+  , -- | The necessarily nonempty global obligation residual.
+    certificateResidual :: !EmbeddingResidual
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Certify the four bounded local embedding obligations, collecting every
+-- witness within each obligation and preserving obligation order.
+certifyLocalEmbedding
+  :: ExactArrangementDraft
+  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
+certifyLocalEmbedding draft =
+  case NonEmpty.nonEmpty (structuralObstructions draft) of
+    Just obstructions -> Left obstructions
+    Nothing ->
+      case projectVertices (draftVertices draft) of
+        Invalid obstructions -> Left obstructions
+        Valid projectedVertices ->
+          certifyProjectedDraft draft projectedVertices
+
+data Validation value
+  = Invalid !(NonEmpty OverlayEmbeddingObstruction)
+  | Valid value
+
+instance Functor Validation where
+  fmap _ (Invalid obstructions) = Invalid obstructions
+  fmap transform (Valid value) = Valid (transform value)
+
+instance Applicative Validation where
+  pure = Valid
+  Invalid left <*> Invalid right = Invalid (left <> right)
+  Invalid obstructions <*> Valid _ = Invalid obstructions
+  Valid _ <*> Invalid obstructions = Invalid obstructions
+  Valid transform <*> Valid value = Valid (transform value)
+
+invalid :: OverlayEmbeddingObstruction -> Validation value
+invalid obstruction = Invalid (obstruction :| [])
+
+data ProjectedVertex = ProjectedVertex
+  { projectedExactPoint :: !ExactPoint
+  , projectedRoundedPoint :: !QueryPoint
+  }
+
+data ResolvedSegment = ResolvedSegment
+  { resolvedSegmentId :: !DraftSegmentId
+  , resolvedSegmentFrom :: !ResolvedVertex
+  , resolvedSegmentTo :: !ResolvedVertex
+  }
+
+data ResolvedVertex = ResolvedVertex
+  { resolvedVertexId :: !DraftVertexId
+  , resolvedProjectedVertex :: !ProjectedVertex
+  }
+
+data ResolvedMembership = ResolvedMembership
+  { resolvedMembershipParameter :: !ExactRational
+  , resolvedMembershipVertex :: !ResolvedVertex
+  }
+
+structuralObstructions
+  :: ExactArrangementDraft
+  -> [OverlayEmbeddingObstruction]
+structuralObstructions draft =
+  segmentEndpointObstructions
+    <> sourceMembershipObstructions
+    <> incidenceObstructions
+    <> neighborhoodObstructions
+ where
+  vertices = draftVertices draft
+  segments = draftSegments draft
+  missingVertex vertexId =
+    [ DraftReferenceMissing (DraftVertexReference vertexId)
+    | Map.notMember vertexId vertices
+    ]
+  missingSegment segmentId =
+    [ DraftReferenceMissing (DraftSegmentReference segmentId)
+    | Map.notMember segmentId segments
+    ]
+  segmentEndpointObstructions =
+    concatMap
+      (\(_, (from, to)) -> missingVertex from <> missingVertex to)
+      (Map.toAscList segments)
+  sourceMembershipObstructions =
+    concatMap
+      (concatMap (missingVertex . snd) . snd)
+      (Map.toAscList (draftSourceMemberships draft))
+  incidenceObstructions =
+    concatMap
+      ( \incidence ->
+          missingSegment (draftIncidenceFirstSegment incidence)
+            <> missingSegment (draftIncidenceSecondSegment incidence)
+      )
+      (draftIncidences draft)
+  neighborhoodObstructions =
+    concatMap
+      ( \neighborhood ->
+          missingVertex (draftNeighborhoodCenter neighborhood)
+            <> concatMap missingVertex (draftNeighborhoodNeighbors neighborhood)
+      )
+      (draftNeighborhoods draft)
+
+projectVertices
+  :: Map DraftVertexId ExactPoint
+  -> Validation (Map DraftVertexId ProjectedVertex)
+projectVertices =
+  Map.traverseWithKey
+    ( \vertexId point ->
+        case exactPointToEmbeddingCandidate point of
+          Left projectionError ->
+            invalid (VertexProjectionRefused vertexId projectionError)
+          Right roundedPoint -> Valid (ProjectedVertex point roundedPoint)
+    )
+
+certifyProjectedDraft
+  :: ExactArrangementDraft
+  -> Map DraftVertexId ProjectedVertex
+  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
+certifyProjectedDraft draft projectedVertices =
+  case resolvedFailures of
+    Left obstruction -> Left (obstruction :| [])
+    Right localFailures ->
+      case NonEmpty.nonEmpty (collisionFailures <> localFailures) of
+        Just obstructions -> Left obstructions
+        Nothing ->
+          Right
+            LocalEmbeddingCertificate
+              { certificateDistinctRoundedVertexCount = distinctRoundedCount
+              , certificateSplitOrderPreservationCount = splitOrderCount
+              , certificateIncidenceRelationPreservationCount = incidenceCount
+              , certificateNeighborhoodRotationPreservationCount = neighborhoodCount
+              , certificateRoundedVertices = Map.map projectedRoundedPoint projectedVertices
+              , certificateResidual = milestoneOneResidual
+              }
+ where
+  (distinctRoundedCount, collisionFailures) = roundedVertexCollisionObstructions projectedVertices
+  resolvedFailures = do
+    splitFailures <- traverse (uncurry resolveSource) (Map.toAscList (draftSourceMemberships draft))
+    incidenceFailures <- traverse resolveIncidence (draftIncidences draft)
+    neighborhoodFailures <- traverse resolveNeighborhood (draftNeighborhoods draft)
+    pure (concat splitFailures <> concat incidenceFailures <> concat neighborhoodFailures)
+  resolveVertex vertexId =
+    case Map.lookup vertexId projectedVertices of
+      Nothing -> Left (DraftReferenceMissing (DraftVertexReference vertexId))
+      Just projectedVertex -> Right (ResolvedVertex vertexId projectedVertex)
+  resolveSegment segmentId =
+    case Map.lookup segmentId (draftSegments draft) of
+      Nothing -> Left (DraftReferenceMissing (DraftSegmentReference segmentId))
+      Just (from, to) ->
+        ResolvedSegment segmentId
+          <$> resolveVertex from
+          <*> resolveVertex to
+  resolveSource sourceId memberships =
+    splitOrderObstructions sourceId
+      <$> traverse
+        ( \(parameter, vertexId) ->
+            ResolvedMembership parameter <$> resolveVertex vertexId
+        )
+        memberships
+  resolveIncidence incidence =
+    incidenceRelationObstructions
+      (draftIncidenceRelation incidence)
+      <$> resolveSegment (draftIncidenceFirstSegment incidence)
+      <*> resolveSegment (draftIncidenceSecondSegment incidence)
+  resolveNeighborhood neighborhood =
+    neighborhoodRotationObstructions
+      <$> resolveVertex (draftNeighborhoodCenter neighborhood)
+      <*> traverse resolveVertex (draftNeighborhoodNeighbors neighborhood)
+  splitOrderCount =
+    sum
+      ( map
+          (max 0 . subtract 1 . length)
+          (Map.elems (draftSourceMemberships draft))
+      )
+  incidenceCount = length (draftIncidences draft)
+  neighborhoodCount =
+    sum
+      ( map
+          (length . draftNeighborhoodNeighbors)
+          (draftNeighborhoods draft)
+      )
+
+-- | The rounded-point grouping: how many distinct rounded points the
+-- projection occupies, and every pair of vertices that share one.
+roundedVertexCollisionObstructions
+  :: Map DraftVertexId ProjectedVertex
+  -> (Int, [OverlayEmbeddingObstruction])
+roundedVertexCollisionObstructions projectedVertices =
+  ( Map.size verticesByRoundedPoint
+  , [ RoundedVerticesCollide leftId rightId (queryPointValue roundedPoint)
+    | (roundedPoint, vertexIds) <- Map.toAscList verticesByRoundedPoint
+    , (leftId, rightId) <- unorderedPairs (List.sort vertexIds)
+    ]
+  )
+ where
+  verticesByRoundedPoint =
+    Map.fromListWith (<>)
+      [ (projectedRoundedPoint projectedVertex, [vertexId])
+      | (vertexId, projectedVertex) <- Map.toAscList projectedVertices
+      ]
+
+splitOrderObstructions
+  :: DraftSourceId
+  -> [ResolvedMembership]
+  -> [OverlayEmbeddingObstruction]
+splitOrderObstructions sourceId memberships =
+  case memberships of
+    firstMembership : secondMembership : remainingMemberships ->
+      let finalMembership =
+            List.foldl' (\_ current -> current) secondMembership remainingMemberships
+          sourceFrom = roundedMembershipPoint firstMembership
+          sourceTo = roundedMembershipPoint finalMembership
+       in [ SplitOrderNotPreserved
+              sourceId
+              (resolvedVertexId (resolvedMembershipVertex leftMembership))
+              (resolvedVertexId (resolvedMembershipVertex rightMembership))
+          | (leftMembership, rightMembership) <-
+              zip
+                memberships
+                (drop 1 memberships)
+          , compare
+              (resolvedMembershipParameter leftMembership)
+              (resolvedMembershipParameter rightMembership)
+              /= roundedOrderAlong
+                sourceFrom
+                sourceTo
+                (roundedMembershipPoint leftMembership)
+                (roundedMembershipPoint rightMembership)
+          ]
+    _ -> []
+
+roundedMembershipPoint :: ResolvedMembership -> Point
+roundedMembershipPoint =
+  queryPointValue
+    . projectedRoundedPoint
+    . resolvedProjectedVertex
+    . resolvedMembershipVertex
+
+roundedOrderAlong :: Point -> Point -> Point -> Point -> Ordering
+roundedOrderAlong
+  (Point sourceFromX sourceFromY)
+  (Point sourceToX sourceToY)
+  (Point leftX leftY)
+  (Point rightX rightY) =
+    let directionX = sourceToX - sourceFromX
+        directionY = sourceToY - sourceFromY
+     in if abs directionX >= abs directionY
+          then
+            if directionX >= 0
+              then compare leftX rightX
+              else compare rightX leftX
+          else
+            if directionY >= 0
+              then compare leftY rightY
+              else compare rightY leftY
+
+incidenceRelationObstructions
+  :: SegmentRelation
+  -> ResolvedSegment
+  -> ResolvedSegment
+  -> [OverlayEmbeddingObstruction]
+incidenceRelationObstructions declaredRelation firstSegment secondSegment =
+  [ IncidenceRelationChanged
+      (resolvedSegmentId firstSegment)
+      (resolvedSegmentId secondSegment)
+      declaredRelation
+      exactRelation
+      roundedRelation
+ | exactRelation /= declaredRelation || roundedRelation /= declaredRelation
+  ]
+ where
+  exactRelation =
+    relationFor exactSegmentRelation projectedExactPoint firstSegment secondSegment
+  roundedRelation =
+    relationFor Admitted.segmentRelation roundedProjectedPoint firstSegment secondSegment
+
+relationFor
+  :: (point -> point -> point -> point -> SegmentRelation)
+  -> (ProjectedVertex -> point)
+  -> ResolvedSegment
+  -> ResolvedSegment
+  -> SegmentRelation
+relationFor relation project firstSegment secondSegment =
+  relation
+    (project (resolvedProjectedVertex (resolvedSegmentFrom firstSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentTo firstSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentFrom secondSegment)))
+    (project (resolvedProjectedVertex (resolvedSegmentTo secondSegment)))
+
+neighborhoodRotationObstructions
+  :: ResolvedVertex
+  -> NonEmpty ResolvedVertex
+  -> [OverlayEmbeddingObstruction]
+neighborhoodRotationObstructions center exactRotation =
+  [ NeighborhoodRotationChanged
+      (resolvedVertexId center)
+      exactVertexRotation
+      roundedVertexRotation
+  | roundedRotationDescents > 1
+  ]
+ where
+  exactVertexRotation = fmap resolvedVertexId exactRotation
+  roundedRotationDescents =
+    List.foldl'
+      (\descentCount (left, right) ->
+         descentCount
+           + if compareRoundedAround center left right == GT
+               then 1
+               else 0)
+      (0 :: Int)
+      (cyclePairs exactRotation)
+  roundedVertexRotation =
+    fmap resolvedVertexId
+      (NonEmpty.sortBy (compareRoundedAround center) exactRotation)
+
+compareRoundedAround
+  :: ResolvedVertex
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> Ordering
+compareRoundedAround center left right =
+  compareRoundedAroundWith
+    ( Admitted.orient2d
+        (roundedVertexPoint center)
+        (roundedVertexPoint left)
+        (roundedVertexPoint right)
+    )
+    center
+    left
+    right
+
+compareRoundedAroundWith
+  :: Ordering
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> ResolvedVertex
+  -> Ordering
+compareRoundedAroundWith roundedOrientation center left right =
+  case compare (roundedVectorHalf center left) (roundedVectorHalf center right) of
+    EQ ->
+      case roundedOrientation of
+        GT -> LT
+        LT -> GT
+        EQ -> compare (roundedVertexPoint left) (roundedVertexPoint right)
+    ordering -> ordering
+
+roundedVectorHalf :: ResolvedVertex -> ResolvedVertex -> Bool
+roundedVectorHalf center neighbor =
+  let Point centerX centerY = roundedVertexPoint center
+      Point neighborX neighborY = roundedVertexPoint neighbor
+      deltaX = neighborX - centerX
+      deltaY = neighborY - centerY
+   in deltaY < 0 || (deltaY == 0 && deltaX < 0)
+
+roundedVertexPoint :: ResolvedVertex -> Point
+roundedVertexPoint = roundedProjectedPoint . resolvedProjectedVertex
+
+roundedProjectedPoint :: ProjectedVertex -> Point
+roundedProjectedPoint = queryPointValue . projectedRoundedPoint
diff --git a/src-overlay/Moonlight/Planar/Internal/Overlay/Arrangement.hs b/src-overlay/Moonlight/Planar/Internal/Overlay/Arrangement.hs
new file mode 100644
--- /dev/null
+++ b/src-overlay/Moonlight/Planar/Internal/Overlay/Arrangement.hs
@@ -0,0 +1,863 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | One exact source-family arrangement. The event owner splits and merges
+-- source supports; exact rotations descend to boundary orbits, containment
+-- glues those orbits into faces, and sparse source transitions label the
+-- resulting incidence. No binary64 realization participates in construction.
+module Moonlight.Planar.Internal.Overlay.Arrangement
+  ( certifyArrangement
+  , overlayFaceInteriorPoint
+  , canonicalEdgeKey
+  , compareAround
+  ) where
+
+import Control.Monad (foldM)
+import Data.Bifunctor (first)
+import qualified Data.IntMap.Strict as IntMap
+import Data.IntMap.Strict (IntMap)
+import qualified Data.IntSet as IntSet
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import Data.Maybe (catMaybes)
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word32)
+import Moonlight.Planar.Exact
+  ( ExactBounds
+  , exactPointsBounds
+  , pointInBounds
+  , ExactPoint
+  , ExactSegment
+  , ExactVector (..)
+  , compareExactVectorAngle
+  , exactCross
+  , exactOnClosedSegment
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , exactPointCross
+  , exactSegment
+  , exactSegmentEndpoints
+  , exactVectorFromPoints
+  , translateExactPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( consecutivePairs
+  , cyclePairs
+  , orderedPair
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactDivide
+  , exactRationalFromDyadic
+  )
+import Moonlight.Planar.Internal.ExactSegmentEvents
+  ( ExactSegmentEvent (..)
+  , ExactSegmentEventPlan
+  , ExactSweepSegmentId (..)
+  , exactSegmentEventPlan
+  , exactSegmentEvents
+  , exactSegmentPairChecks
+  , exactSegmentSplitPoints
+  , exactSegmentSweepMaximumHeight
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
+  , directedPair
+  , faceIdIndex
+  , reverseEdge
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , admitPlanarIncidence
+  , faceBoundaryComponents
+  , incidenceDestination
+  , incidenceEdgeCount
+  , incidenceFaceCount
+  , incidenceOrigin
+  , incidenceUndirectedEndpoints
+  , incidenceVertexOutEdge
+  )
+import Moonlight.Planar.Internal.Overlay.Types
+import Moonlight.Planar.Internal.PackedIndex (indexLimit, noIndex)
+import Moonlight.Planar.Internal.Paged (fromVector)
+import Moonlight.Planar.Region
+  ( ExactLoop
+  , PlanarLayer
+  , PolygonComponent
+  , exactLoopPoints
+  , planarLayerOutsideLabel
+  , planarLayerRegions
+  , planarRegionComponents
+  , polygonHoleLoops
+  , polygonOuterLoop
+  )
+
+type ExactEdgeKey = (ExactPoint, ExactPoint)
+
+data SourceBoundary = SourceBoundary
+  { sourceSegment :: !ExactSegment
+  , sourceLabel :: !OverlayLabelId
+  , sourceFromRef :: !BoundaryVertexRef
+  , sourceToRef :: !BoundaryVertexRef
+  , sourceEdgeRef :: !BoundaryEdgeRef
+  }
+
+data AtomicContribution = AtomicContribution !SourceBoundary !Bool
+
+data AtomicEdge = AtomicEdge
+  { atomicSegment :: !ExactSegment
+  , atomicOrigin :: !OverlayEdgeOrigin
+  , atomicTransitions :: !(IntMap (OverlayLabelId, OverlayLabelId))
+  }
+
+data SourceSides = SourceSides !(Set OverlayLabelId) !(Set OverlayLabelId)
+
+instance Semigroup SourceSides where
+  SourceSides leftBefore rightBefore <> SourceSides leftAfter rightAfter =
+    SourceSides (leftBefore <> leftAfter) (rightBefore <> rightAfter)
+
+data OriginAccumulation = OriginAccumulation !(Set BoundaryVertexRef) !(Set BoundaryEdgeRef)
+
+instance Semigroup OriginAccumulation where
+  OriginAccumulation beforeVertices beforeEdges <> OriginAccumulation afterVertices afterEdges =
+    OriginAccumulation (beforeVertices <> afterVertices) (beforeEdges <> afterEdges)
+
+data ArrangementDart = ArrangementDart
+  { dartHandle :: !DirectedEdgeId
+  , dartOrigin :: !VertexId
+  , dartFrom :: !ExactPoint
+  , dartTo :: !ExactPoint
+  }
+
+-- | A rotation-permutation orbit, not an admitted polygon loop. A bridge is
+-- present twice with opposite orientation; a cut vertex may occur repeatedly.
+data BoundaryOrbit = BoundaryOrbit
+  { orbitDarts :: !(NonEmpty ArrangementDart)
+  , orbitDoubleArea :: !ExactRational
+  , orbitBounds :: !ExactBounds
+  }
+
+data FaceTransition = FaceTransition
+  { transitionEdge :: !UndirectedEdgeId
+  , transitionTarget :: !FaceId
+  , transitionSources :: !(IntMap (OverlayLabelId, OverlayLabelId))
+  }
+
+data FaceLabelling = FaceLabelling
+  { assignedFaceLabels :: !(Map FaceId (IntMap OverlayLabelId))
+  , pendingLabelFaces :: !(Seq.Seq FaceId)
+  }
+
+outsideLabel :: OverlayLabelId
+outsideLabel = OverlayLabelId 0
+
+outerFace :: FaceId
+outerFace = FaceId 0
+
+oneHalf :: ExactRational
+oneHalf = exactRationalFromDyadic 1 (-1)
+
+-- | Application labels have already been registered into source-local IDs.
+-- One shared event plan handles binary and n-ary requests alike; the labels
+-- returned here are sparse source states with outside entries omitted.
+certifyArrangement
+  :: NonEmpty (PlanarLayer OverlayLabelId)
+  -> Either OverlayError (OverlayResult (IntMap OverlayLabelId))
+certifyArrangement layers = do
+  sources <- flattenLayers layers
+  sourcePlan <-
+    first OverlaySegmentEventsInvalid
+      (exactSegmentEventPlan (V.map sourceSegment sources))
+  atomics <- normalizeAtomicEdges sources sourcePlan
+  let origins = vertexOrigins sources sourcePlan
+      pointIds =
+        Map.fromAscList
+          (zip (Map.keys origins) (map (VertexId . fromIntegral) [0 :: Int ..]))
+      vertices = V.fromList (map publishVertex (Map.toAscList origins))
+  admitCapacity (Map.size pointIds) (V.length atomics) 1
+  darts <- arrangementDarts pointIds atomics
+  let outgoing = outgoingDarts darts
+      successors = rotationSuccessors outgoing
+      predecessors = Map.fromList [(next, previous) | (previous, next) <- Map.toAscList successors]
+      isolatedPoints =
+        [(point, vertex) | (point, vertex) <- Map.toAscList pointIds, Map.notMember vertex outgoing]
+  orbits <- boundaryOrbits darts successors
+  let positiveOrbits = filter ((> 0) . orbitDoubleArea) orbits
+      anchors = zip (map (FaceId . fromIntegral) [1 :: Int ..]) positiveOrbits
+      faceCount = length anchors + 1
+      anchorByRoot = Map.fromList [(orbitRoot orbit, face) | (face, orbit) <- anchors]
+  admitCapacity (Map.size pointIds) (V.length atomics) faceCount
+  let assignedOrbits = map (assignOrbit anchors anchorByRoot) orbits
+      dartFaces =
+        Map.fromList
+          [ (dartHandle dart, face)
+          | (face, orbit) <- assignedOrbits
+          , dart <- NonEmpty.toList (orbitDarts orbit)
+          ]
+      groupedOrbits =
+        Map.fromListWith (<>)
+          [(face, [orbit]) | (face, orbit) <- assignedOrbits]
+      isolated =
+        IntMap.fromListWith IntSet.union
+          [ (faceIdIndex (containingFace anchors point), IntSet.singleton (vertexIdIndex vertex))
+          | (point, vertex) <- isolatedPoints
+          ]
+  incidence <-
+    assembleIncidence
+      pointIds darts outgoing successors predecessors dartFaces
+      anchors groupedOrbits isolated
+  adjacency <- faceTransitions atomics dartFaces
+  labels <- labelFaces faceCount adjacency
+  let events = exactSegmentEvents sourcePlan
+      edgeOrigins = V.map atomicOrigin atomics
+      receipt =
+        OverlayReceipt
+          { overlayInputSources = NonEmpty.length layers
+          , overlayInputSegments = V.length sources
+          , overlayRelationEvents = length events
+          , overlayExactCrossings = length [() | ExactProperCrossing {} <- events]
+          , overlayOverlapIntervals = length [() | ExactCollinearOverlap {} <- events]
+          , overlayAtomicEdges = V.length atomics
+          , overlayOutputVertices = V.length vertices
+          , overlayArrangementCells = faceCount
+          , overlayBoundaryOrbits = length orbits
+          , overlayProvenanceIncidences =
+              V.foldl' (\count origin -> count + NonEmpty.length (overlayEdgeSources origin)) 0 edgeOrigins
+                + V.foldl' vertexProvenanceCount 0 vertices
+          , overlayTotalRelationChecks = exactSegmentPairChecks sourcePlan
+          , overlaySweepMaximumHeight = exactSegmentSweepMaximumHeight sourcePlan
+          }
+  pure
+    OverlayResult
+      { overlayResultIncidence = incidence
+      , overlayResultVertices = vertices
+      , overlayResultEdges = edgeOrigins
+      , overlayResultLabels = labels
+      , overlayResultReceipt = receipt
+      }
+ where
+  publishVertex (point, OriginAccumulation vertexRefs edgeRefs) =
+    OverlayVertex point (OverlayVertexOrigin (Set.toAscList vertexRefs) (Set.toAscList edgeRefs))
+  vertexProvenanceCount count vertex =
+    let origin = overlayVertexOrigin vertex
+     in count + length (overlayOriginVertices origin) + length (overlayOriginEdges origin)
+
+admitCapacity :: Int -> Int -> Int -> Either OverlayError ()
+admitCapacity vertexCount edgeCount faceCount
+  | toInteger vertexCount <= limit
+      && 2 * toInteger edgeCount <= limit
+      && toInteger faceCount <= limit = Right ()
+  | otherwise =
+      Left
+        (OverlayArrangementInvalid (OverlayPackedCapacityExceeded vertexCount edgeCount faceCount))
+ where
+  limit = toInteger indexLimit
+
+flattenLayers
+  :: NonEmpty (PlanarLayer OverlayLabelId)
+  -> Either OverlayError (V.Vector SourceBoundary)
+flattenLayers layers =
+  V.fromList . concat
+    <$> traverse flattenSource (zip [0 ..] (NonEmpty.toList layers))
+ where
+  flattenSource (sourceIndex, layer) = do
+    let source = OverlaySourceId sourceIndex
+        outside = planarLayerOutsideLabel layer
+    if outside == outsideLabel
+      then Right ()
+      else Left (OverlayArrangementInvalid (OverlaySourceOutsideLabelInvalid source outside))
+    concat
+      <$> traverse
+        (\(componentIndex, (label, component)) -> flattenComponent source componentIndex label component)
+        (zip [0 ..] (labelledComponents layer))
+
+labelledComponents :: PlanarLayer label -> [(label, PolygonComponent)]
+labelledComponents layer =
+  [ (label, component)
+  | (label, region) <- Map.toAscList (planarLayerRegions layer)
+  , component <- planarRegionComponents region
+  ]
+
+flattenComponent
+  :: OverlaySourceId
+  -> Int
+  -> OverlayLabelId
+  -> PolygonComponent
+  -> Either OverlayError [SourceBoundary]
+flattenComponent source componentIndex label component =
+  concat
+    <$> traverse
+      (uncurry (flattenLoop source componentIndex label))
+      ( (BoundaryOuterLoop, polygonOuterLoop component)
+          : zipWith (\index loop -> (BoundaryHoleLoop index, loop)) [0 ..] (polygonHoleLoops component)
+      )
+
+flattenLoop
+  :: OverlaySourceId
+  -> Int
+  -> OverlayLabelId
+  -> BoundaryLoopRef
+  -> ExactLoop
+  -> Either OverlayError [SourceBoundary]
+flattenLoop source componentIndex label loopRef loop =
+  traverse constructSource (cyclePairs indexedPoints)
+ where
+  indexedPoints = NonEmpty.zip (0 :| [1 ..]) (exactLoopPoints loop)
+  reference :: Int -> BoundaryRef feature
+  reference = BoundaryRef source componentIndex loopRef
+  constructSource ((fromIndex, from), (toIndex, to)) = do
+    segment <- first OverlayExactGeometryInvalid (exactSegment from to)
+    pure
+      SourceBoundary
+        { sourceSegment = segment
+        , sourceLabel = label
+        , sourceFromRef = reference fromIndex
+        , sourceToRef = reference toIndex
+        , sourceEdgeRef = reference fromIndex
+        }
+
+normalizeAtomicEdges
+  :: V.Vector SourceBoundary
+  -> ExactSegmentEventPlan
+  -> Either OverlayError (V.Vector AtomicEdge)
+normalizeAtomicEdges sources plan =
+  V.fromList . catMaybes
+    <$> traverse resolveAtomicContributions (Map.toAscList grouped)
+ where
+  grouped =
+    V.ifoldl'
+      (\groups sourceIndex source ->
+         List.foldl' (insertAtomic source) groups
+           (consecutivePairs (exactSegmentSplitPoints plan (ExactSweepSegmentId sourceIndex))))
+      Map.empty
+      sources
+  insertAtomic source groups (from, to)
+    | from == to = groups
+    | otherwise =
+        let key@(canonicalFrom, _) = canonicalEdgeKey from to
+         in Map.insertWith (<>) key
+              (AtomicContribution source (from == canonicalFrom) :| []) groups
+
+resolveAtomicContributions
+  :: (ExactEdgeKey, NonEmpty AtomicContribution)
+  -> Either OverlayError (Maybe AtomicEdge)
+resolveAtomicContributions ((from, to), contributions) = do
+  let origin = OverlayEdgeOrigin (NonEmpty.sort (fmap contributionReference contributions))
+      sourceSides =
+        IntMap.fromListWith (<>)
+          [ (overlaySourceIndex (boundaryRefSource (sourceEdgeRef source)), sides source follows)
+          | AtomicContribution source follows <- NonEmpty.toList contributions
+          ]
+  transitions <-
+    IntMap.fromAscList . catMaybes
+      <$> traverse (resolveSourceTransition origin) (IntMap.toAscList sourceSides)
+  if IntMap.null transitions
+    then Right Nothing
+    else do
+      segment <- first OverlayExactGeometryInvalid (exactSegment from to)
+      pure (Just (AtomicEdge segment origin transitions))
+ where
+  contributionReference (AtomicContribution source _) = sourceEdgeRef source
+  sides source True = SourceSides (Set.singleton (sourceLabel source)) Set.empty
+  sides source False = SourceSides Set.empty (Set.singleton (sourceLabel source))
+
+resolveSourceTransition
+  :: OverlayEdgeOrigin
+  -> (Int, SourceSides)
+  -> Either OverlayError (Maybe (Int, (OverlayLabelId, OverlayLabelId)))
+resolveSourceTransition origin (sourceIndex, SourceSides leftLabels rightLabels) = do
+  leftLabel <- resolveSide leftLabels
+  rightLabel <- resolveSide rightLabels
+  pure
+    (if leftLabel == rightLabel then Nothing else Just (sourceIndex, (leftLabel, rightLabel)))
+ where
+  resolveSide labels =
+    case Set.toAscList labels of
+      [] -> Right outsideLabel
+      [label] -> Right label
+      label : remaining ->
+        Left
+          ( OverlayArrangementInvalid
+              (OverlaySourceSideConflict (OverlaySourceId sourceIndex) origin (label :| remaining))
+          )
+
+-- | Keep all exact source events, including a source vertex that becomes
+-- isolated when every incident identity-transition seam disappears.
+vertexOrigins
+  :: V.Vector SourceBoundary
+  -> ExactSegmentEventPlan
+  -> Map ExactPoint OriginAccumulation
+vertexOrigins sources plan = V.ifoldl' collectSource Map.empty sources
+ where
+  collectSource origins sourceIndex source =
+    let (from, to) = exactSegmentEndpoints (sourceSegment source)
+        withVertices =
+          insertOrigin to (OriginAccumulation (Set.singleton (sourceToRef source)) Set.empty)
+            (insertOrigin from (OriginAccumulation (Set.singleton (sourceFromRef source)) Set.empty) origins)
+        edgeOrigin = OriginAccumulation Set.empty (Set.singleton (sourceEdgeRef source))
+     in List.foldl'
+          (\current point -> insertOrigin point edgeOrigin current)
+          withVertices
+          (exactSegmentSplitPoints plan (ExactSweepSegmentId sourceIndex))
+  insertOrigin = Map.insertWith (<>)
+
+arrangementDarts
+  :: Map ExactPoint VertexId
+  -> V.Vector AtomicEdge
+  -> Either OverlayError (Map DirectedEdgeId ArrangementDart)
+arrangementDarts pointIds atomics =
+  Map.fromList . concat
+    <$> traverse makeDarts (V.toList (V.indexed atomics))
+ where
+  makeDarts :: (Int, AtomicEdge) -> Either OverlayError [(DirectedEdgeId, ArrangementDart)]
+  makeDarts (index, atomic) = do
+    let (from, to) = exactSegmentEndpoints (atomicSegment atomic)
+        (forward, backward) = directedPair (UndirectedEdgeId (fromIntegral index))
+    fromId <- requireExactVertex pointIds from
+    toId <- requireExactVertex pointIds to
+    pure
+      [ (forward, ArrangementDart forward fromId from to)
+      , (backward, ArrangementDart backward toId to from)
+      ]
+
+requireExactVertex :: Map ExactPoint VertexId -> ExactPoint -> Either OverlayError VertexId
+requireExactVertex pointIds point =
+  maybe
+    (Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point)))
+    Right
+    (Map.lookup point pointIds)
+
+outgoingDarts :: Map DirectedEdgeId ArrangementDart -> Map VertexId (NonEmpty ArrangementDart)
+outgoingDarts darts =
+  Map.map orderOutgoing
+    (Map.fromListWith (<>) [(dartOrigin dart, dart :| []) | dart <- Map.elems darts])
+ where
+  orderOutgoing outgoing@(initial :| _) =
+    NonEmpty.sortBy
+      (\left right -> compareAround (dartFrom initial) (dartTo left) (dartTo right))
+      outgoing
+
+-- | At the destination, the clockwise predecessor of the reverse dart keeps
+-- the face on the left. Construct the permutation directly from cyclic pairs;
+-- there is no angular index search or numerical angle conversion.
+rotationSuccessors
+  :: Map VertexId (NonEmpty ArrangementDart)
+  -> Map DirectedEdgeId DirectedEdgeId
+rotationSuccessors outgoing =
+  Map.fromList
+    [ (reverseEdge (dartHandle current), dartHandle previous)
+    | aroundVertex <- Map.elems outgoing
+    , (previous, current) <- cyclePairs aroundVertex
+    ]
+
+boundaryOrbits
+  :: Map DirectedEdgeId ArrangementDart
+  -> Map DirectedEdgeId DirectedEdgeId
+  -> Either OverlayError [BoundaryOrbit]
+boundaryOrbits darts successors = descend (Map.keysSet darts)
+ where
+  descend remaining =
+    case Set.lookupMin remaining of
+      Nothing -> Right []
+      Just seed -> do
+        (untraced, handles) <- traceOrbit seed remaining seed []
+        orbit <- makeOrbit <$> traverse requireDart handles
+        (orbit :) <$> descend untraced
+
+  -- This is descent of a finite permutation: every nonclosing step removes
+  -- exactly one dart. Returning to another consumed dart is a typed failure,
+  -- never a walk-budget success or a vertex-simple cycle substitution.
+  traceOrbit seed remaining current reversedHandles
+    | current == seed, previous : rest <- reversedHandles =
+        Right (remaining, NonEmpty.reverse (previous :| rest))
+    | not (Set.member current remaining) =
+        Left (OverlayArrangementInvalid (OverlayBoundaryOrbitNotClosed seed current))
+    | otherwise = do
+        next <- requireSuccessor successors current
+        traceOrbit seed (Set.delete current remaining) next (current : reversedHandles)
+
+  requireDart handle =
+    maybe
+      (Left (OverlayArrangementInvalid (OverlayBoundaryOrbitMissing handle)))
+      Right
+      (Map.lookup handle darts)
+
+makeOrbit :: NonEmpty ArrangementDart -> BoundaryOrbit
+makeOrbit darts =
+  BoundaryOrbit
+    { orbitDarts = darts
+    , orbitDoubleArea =
+        List.foldl' (\area dart -> area + exactPointCross (dartFrom dart) (dartTo dart))
+          0 (NonEmpty.toList darts)
+    , orbitBounds = exactPointsBounds (fmap dartFrom darts)
+    }
+
+orbitRoot :: BoundaryOrbit -> DirectedEdgeId
+orbitRoot (BoundaryOrbit (initial :| _) _ _) = dartHandle initial
+
+assignOrbit
+  :: [(FaceId, BoundaryOrbit)]
+  -> Map DirectedEdgeId FaceId
+  -> BoundaryOrbit
+  -> (FaceId, BoundaryOrbit)
+assignOrbit anchors anchorByRoot orbit =
+  case Map.lookup (orbitRoot orbit) anchorByRoot of
+    Just face -> (face, orbit)
+    Nothing -> (containingFace anchors (leastOrbitPoint orbit), orbit)
+
+-- | A nonpositive orbit is its connected support component's exterior
+-- boundary. Exact event normalization has already glued every touching
+-- component, so this component lies in one face of every other component.
+-- Its least exact vertex represents that containing face without constructing
+-- an interior probe against every atomic segment. Strict containment below
+-- excludes the component's own positive faces at this exposed vertex.
+leastOrbitPoint :: BoundaryOrbit -> ExactPoint
+leastOrbitPoint orbit =
+  let initial :| remaining = orbitDarts orbit
+   in List.foldl' (\point dart -> min point (dartFrom dart)) (dartFrom initial) remaining
+
+-- | Positive orbits of one connected graph have disjoint interiors; across
+-- disconnected components their interiors are disjoint or nested. The least
+-- positive area strictly containing a point is its nearest enclosing face.
+-- Bounds reject remote candidates before exact boundary or winding work;
+-- raw-walk winding handles repeated vertices and cancels bridge darts.
+containingFace :: [(FaceId, BoundaryOrbit)] -> ExactPoint -> FaceId
+containingFace anchors point =
+  maybe outerFace fst (List.foldl' choose Nothing anchors)
+ where
+  choose
+    :: Maybe (FaceId, BoundaryOrbit)
+    -> (FaceId, BoundaryOrbit)
+    -> Maybe (FaceId, BoundaryOrbit)
+  choose selected candidate@(_, orbit)
+    | not (pointInBounds point (orbitBounds orbit)) = selected
+    | any (\dart -> exactOnClosedSegment (dartFrom dart) (dartTo dart) point) (orbitDarts orbit) = selected
+    | orbitWinding point orbit == 0 = selected
+    | otherwise =
+        case selected of
+          Nothing -> Just candidate
+          Just (_, previous)
+            | orbitDoubleArea orbit < orbitDoubleArea previous -> Just candidate
+            | otherwise -> selected
+
+orbitWinding :: ExactPoint -> BoundaryOrbit -> Int
+orbitWinding query orbit = List.foldl' crossing 0 (NonEmpty.toList (orbitDarts orbit))
+ where
+  (_, queryY) = exactPointCoordinates query
+  crossing :: Int -> ArrangementDart -> Int
+  crossing count dart =
+    let from = dartFrom dart
+        to = dartTo dart
+        (_, fromY) = exactPointCoordinates from
+        (_, toY) = exactPointCoordinates to
+        orientation = exactOrient2d from to query
+     in if fromY <= queryY && queryY < toY && orientation == GT
+          then count + 1
+          else
+            if toY <= queryY && queryY < fromY && orientation == LT
+              then count - 1
+              else count
+
+-- | A rational clearance witness, not an epsilon. Starting at an atomic
+-- midpoint, the open interval before the first exact left-normal ray hit
+-- lies wholly in the dart's left face. Halving that parameter gives an exact
+-- interior probe even when distinct vertices collide under binary64.
+certifiedLeftProbe
+  :: [ExactSegment]
+  -> [ExactPoint]
+  -> ExactPoint
+  -> ExactPoint
+  -> Either OverlayError ExactPoint
+certifiedLeftProbe segments isolatedPoints from to = do
+  let (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+      midpoint = exactPoint (oneHalf * (fromX + toX)) (oneHalf * (fromY + toY))
+      normal = ExactVector (fromY - toY) (toX - fromX)
+  intersections <- traverse (raySegmentParameter midpoint normal) segments
+  isolatedIntersections <- traverse (rayIsolatedParameter midpoint normal) isolatedPoints
+  let parameter = maybe 1 (oneHalf *) (leastParameter (intersections <> isolatedIntersections))
+      ExactVector normalX normalY = normal
+  pure (translateExactPoint midpoint (ExactVector (parameter * normalX) (parameter * normalY)))
+
+-- | Derive a genuine interior witness when an observation, such as erosion,
+-- requires one. Face assembly needs only strict component containment and
+-- does not pay for this all-support clearance query.
+overlayFaceInteriorPoint
+  :: OverlayResult labels
+  -> FaceId
+  -> Either OverlayError ExactPoint
+overlayFaceInteriorPoint result face
+  | faceIdIndex face >= incidenceFaceCount incidence =
+      Left (OverlayProvenanceIncomplete (OverlayFaceMissing face))
+  | face == outerFace = unavailable
+  | otherwise =
+      case faceBoundaryComponents incidence face of
+        (root : _) : _ -> do
+          from <- pointAt (incidenceOrigin incidence root)
+          to <- pointAt (incidenceDestination incidence root)
+          segments <-
+            traverse
+              (\index -> do
+                 let (fromVertex, toVertex) =
+                       incidenceUndirectedEndpoints incidence (UndirectedEdgeId (fromIntegral index))
+                 firstPoint <- pointAt fromVertex
+                 secondPoint <- pointAt toVertex
+                 first OverlayExactGeometryInvalid (exactSegment firstPoint secondPoint))
+              [0 .. incidenceEdgeCount incidence - 1]
+          let isolatedPoints =
+                [ overlayExactPoint vertex
+                | (index, vertex) <- V.toList (V.indexed (overlayResultVertices result))
+                , incidenceVertexOutEdge incidence (VertexId (fromIntegral index)) == Nothing
+                ]
+          certifiedLeftProbe segments isolatedPoints from to
+        _ -> unavailable
+ where
+  incidence = overlayResultIncidence result
+  unavailable :: Either OverlayError ExactPoint
+  unavailable = Left (OverlayProvenanceIncomplete (OverlayFaceInteriorUnavailable face))
+  pointAt vertex =
+    maybe
+      (Left (OverlayProvenanceIncomplete (OverlayVertexMissing vertex)))
+      (Right . overlayExactPoint)
+      (overlayResultVertices result V.!? vertexIdIndex vertex)
+
+rayIsolatedParameter
+  :: ExactPoint
+  -> ExactVector
+  -> ExactPoint
+  -> Either OverlayError (Maybe ExactRational)
+rayIsolatedParameter origin direction point
+  | exactCross (exactVectorFromPoints origin point) direction /= 0 = Right Nothing
+  | otherwise = do
+      parameter <- rayPointParameter origin direction point
+      pure (if parameter > 0 then Just parameter else Nothing)
+
+leastParameter :: [Maybe ExactRational] -> Maybe ExactRational
+leastParameter = List.foldl' choose Nothing
+ where
+  choose :: Maybe ExactRational -> Maybe ExactRational -> Maybe ExactRational
+  choose Nothing candidate = candidate
+  choose selected Nothing = selected
+  choose (Just previous) (Just candidate) = Just (min previous candidate)
+
+raySegmentParameter
+  :: ExactPoint
+  -> ExactVector
+  -> ExactSegment
+  -> Either OverlayError (Maybe ExactRational)
+raySegmentParameter origin direction segment =
+  let (from, to) = exactSegmentEndpoints segment
+      segmentDirection = exactVectorFromPoints from to
+      displacement = exactVectorFromPoints origin from
+      determinant = exactCross direction segmentDirection
+   in if determinant == 0
+        then
+          if exactCross displacement direction /= 0
+            then Right Nothing
+            else do
+              fromParameter <- rayPointParameter origin direction from
+              toParameter <- rayPointParameter origin direction to
+              pure (leastParameter [positiveParameter fromParameter, positiveParameter toParameter])
+        else do
+          parameter <- divide (exactCross displacement segmentDirection) determinant
+          segmentParameter <- divide (exactCross displacement direction) determinant
+          pure
+            (if parameter > 0 && 0 <= segmentParameter && segmentParameter <= 1 then Just parameter else Nothing)
+ where
+  divide numerator denominator = first OverlayExactArithmetic (exactDivide numerator denominator)
+  positiveParameter :: ExactRational -> Maybe ExactRational
+  positiveParameter parameter = if parameter > 0 then Just parameter else Nothing
+
+rayPointParameter :: ExactPoint -> ExactVector -> ExactPoint -> Either OverlayError ExactRational
+rayPointParameter origin (ExactVector directionX directionY) point =
+  let (originX, originY) = exactPointCoordinates origin
+      (pointX, pointY) = exactPointCoordinates point
+      (numerator, denominator) =
+        if directionX /= 0 then (pointX - originX, directionX) else (pointY - originY, directionY)
+   in first OverlayExactArithmetic (exactDivide numerator denominator)
+
+requireSuccessor
+  :: Map DirectedEdgeId DirectedEdgeId
+  -> DirectedEdgeId
+  -> Either OverlayError DirectedEdgeId
+requireSuccessor successors dart =
+  maybe
+    (Left (OverlayArrangementInvalid (OverlayBoundaryOrbitMissing dart)))
+    Right
+    (Map.lookup dart successors)
+
+requireDartFace :: Map DirectedEdgeId FaceId -> DirectedEdgeId -> Either OverlayError FaceId
+requireDartFace faces dart =
+  maybe
+    (Left (OverlayArrangementInvalid (OverlayBoundaryOrbitMissing dart)))
+    Right
+    (Map.lookup dart faces)
+
+assembleIncidence
+  :: Map ExactPoint VertexId
+  -> Map DirectedEdgeId ArrangementDart
+  -> Map VertexId (NonEmpty ArrangementDart)
+  -> Map DirectedEdgeId DirectedEdgeId
+  -> Map DirectedEdgeId DirectedEdgeId
+  -> Map DirectedEdgeId FaceId
+  -> [(FaceId, BoundaryOrbit)]
+  -> Map FaceId [BoundaryOrbit]
+  -> IntMap IntSet.IntSet
+  -> Either OverlayError PlanarIncidence
+assembleIncidence pointIds darts outgoing successors predecessors dartFaces anchors groupedOrbits isolated = do
+  packedDarts <- concat <$> traverse packDart (Map.elems darts)
+  let outerOrbits = Map.findWithDefault [] outerFace groupedOrbits
+      outerRoot = case outerOrbits of
+        [] -> Nothing
+        orbit : _ -> Just (orbitRoot orbit)
+      primaryRoots = outerRoot : map (Just . orbitRoot . snd) anchors
+      faceHandles = outerFace : map fst anchors
+      extraRoots =
+        IntMap.fromList
+          [ (faceIdIndex face, roots)
+          | (face, primary) <- zip faceHandles primaryRoots
+          , let roots =
+                  [ root
+                  | orbit <- Map.findWithDefault [] face groupedOrbits
+                  , let root = orbitRoot orbit
+                  , Just root /= primary
+                  ]
+          , not (null roots)
+          ]
+      vertexOutgoing =
+        [ case Map.lookup vertex outgoing of
+            Nothing -> noIndex
+            Just (initial :| _) -> unDirectedEdgeId (dartHandle initial)
+        | vertex <- Map.elems pointIds
+        ]
+  first OverlayIncidenceInvalid
+    ( admitPlanarIncidence
+        (fromVector noIndex (U.fromList vertexOutgoing))
+        (fromVector noIndex (U.fromList packedDarts))
+        (fromVector noIndex (U.fromList (map (maybe noIndex unDirectedEdgeId) primaryRoots)))
+        extraRoots
+        isolated
+    )
+ where
+  packDart :: ArrangementDart -> Either OverlayError [Word32]
+  packDart dart = do
+    next <- requireSuccessor successors (dartHandle dart)
+    previous <- requireSuccessor predecessors (dartHandle dart)
+    face <- requireDartFace dartFaces (dartHandle dart)
+    pure
+      [ unVertexId (dartOrigin dart)
+      , unDirectedEdgeId next
+      , unDirectedEdgeId previous
+      , unFaceId face
+      ]
+
+faceTransitions
+  :: V.Vector AtomicEdge
+  -> Map DirectedEdgeId FaceId
+  -> Either OverlayError (Map FaceId [FaceTransition])
+faceTransitions atomics dartFaces =
+  Map.fromListWith (<>) . concat
+    <$> traverse edgeTransitions (V.toList (V.indexed atomics))
+ where
+  edgeTransitions :: (Int, AtomicEdge) -> Either OverlayError [(FaceId, [FaceTransition])]
+  edgeTransitions (index, atomic) = do
+    let edge = UndirectedEdgeId (fromIntegral index)
+        (forward, backward) = directedPair edge
+        transitions = atomicTransitions atomic
+    leftFace <- requireDartFace dartFaces forward
+    rightFace <- requireDartFace dartFaces backward
+    pure
+      [ (leftFace, [FaceTransition edge rightFace transitions])
+      , (rightFace, [FaceTransition edge leftFace (fmap (\(left, right) -> (right, left)) transitions)])
+      ]
+
+labelFaces
+  :: Int
+  -> Map FaceId [FaceTransition]
+  -> Either OverlayError (V.Vector (IntMap OverlayLabelId))
+labelFaces faceCount adjacency = do
+  labels <- descend (FaceLabelling (Map.singleton outerFace IntMap.empty) (Seq.singleton outerFace))
+  V.fromList
+    <$> traverse
+      (\index -> requireFaceLabels labels (FaceId (fromIntegral index)))
+      [0 .. faceCount - 1]
+ where
+  descend labelling =
+    case Seq.viewl (pendingLabelFaces labelling) of
+      Seq.EmptyL -> Right (assignedFaceLabels labelling)
+      face Seq.:< remaining -> do
+        current <- requireFaceLabels (assignedFaceLabels labelling) face
+        next <-
+          foldM
+            (descendTransition face current)
+            (labelling{pendingLabelFaces = remaining})
+            (Map.findWithDefault [] face adjacency)
+        descend next
+
+  descendTransition face current labelling transition = do
+    derived <-
+      foldM (applySourceTransition face (transitionEdge transition)) current
+        (IntMap.toAscList (transitionSources transition))
+    let target = transitionTarget transition
+    case Map.lookup target (assignedFaceLabels labelling) of
+      Nothing ->
+        Right
+          FaceLabelling
+            { assignedFaceLabels = Map.insert target derived (assignedFaceLabels labelling)
+            , pendingLabelFaces = pendingLabelFaces labelling Seq.|> target
+            }
+      Just existing
+        | existing == derived -> Right labelling
+        | otherwise ->
+            Left
+              ( OverlayArrangementInvalid
+                  (OverlayFaceLabelConflict target (transitionEdge transition) existing derived)
+              )
+
+requireFaceLabels
+  :: Map FaceId (IntMap OverlayLabelId)
+  -> FaceId
+  -> Either OverlayError (IntMap OverlayLabelId)
+requireFaceLabels labels face =
+  maybe
+    (Left (OverlayProvenanceIncomplete (OverlayFaceMissing face)))
+    Right
+    (Map.lookup face labels)
+
+applySourceTransition
+  :: FaceId
+  -> UndirectedEdgeId
+  -> IntMap OverlayLabelId
+  -> (Int, (OverlayLabelId, OverlayLabelId))
+  -> Either OverlayError (IntMap OverlayLabelId)
+applySourceTransition face edge current (sourceIndex, (expected, destination))
+  | actual /= expected =
+      Left
+        ( OverlayArrangementInvalid
+            (OverlayTransitionSourceMismatch face edge (OverlaySourceId sourceIndex) actual expected)
+        )
+  | destination == outsideLabel = Right (IntMap.delete sourceIndex current)
+  | otherwise = Right (IntMap.insert sourceIndex destination current)
+ where
+  actual = IntMap.findWithDefault outsideLabel sourceIndex current
+
+canonicalEdgeKey :: ExactPoint -> ExactPoint -> ExactEdgeKey
+canonicalEdgeKey = orderedPair
+
+compareAround :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
+compareAround center left right =
+  case compareExactVectorAngle (exactVectorFromPoints center left) (exactVectorFromPoints center right) of
+    EQ -> compare left right
+    ordering -> ordering
diff --git a/src-overlay/Moonlight/Planar/Internal/Overlay/Types.hs b/src-overlay/Moonlight/Planar/Internal/Overlay/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-overlay/Moonlight/Planar/Internal/Overlay/Types.hs
@@ -0,0 +1,194 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveFoldable #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE KindSignatures #-}
+
+-- | The one exact overlay carrier. Incidence belongs to the shared planar
+-- owner; geometry, source provenance, and application labels occupy separate
+-- immutable payload planes. No resident binary64 realization is retained.
+module Moonlight.Planar.Internal.Overlay.Types
+  ( BoundaryLoopRef (..)
+  , BoundaryFeature (..)
+  , OverlaySourceId (..)
+  , OverlayLabelId (..)
+  , BoundaryRef (..)
+  , BoundaryVertexRef
+  , BoundaryEdgeRef
+  , OverlayVertexOrigin (..)
+  , OverlayEdgeOrigin (..)
+  , OverlayVertex (..)
+  , OverlayReceipt (..)
+  , OverlayArrangementObstruction (..)
+  , OverlayCellWitness (..)
+  , OverlayError (..)
+  , OverlayResult (..)
+  , OverlaySelectionKind (..)
+  , OverlaySelectionError (..)
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.IntMap.Strict (IntMap)
+import Data.List.NonEmpty (NonEmpty)
+import Data.Vector (Vector)
+import GHC.Generics (Generic)
+import Moonlight.Planar.CellSet (CellSelectionError)
+import Moonlight.Planar.Exact (ExactGeometryError, ExactPoint)
+import Moonlight.Planar.Internal.ExactRational (ExactArithmeticError)
+import Moonlight.Planar.Internal.ExactSegmentEvents (ExactSegmentEventObstruction)
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId
+  , FaceId
+  , UndirectedEdgeId
+  , VertexId
+  )
+import Moonlight.Planar.Internal.Incidence
+  ( PlanarIncidence
+  , PlanarIncidenceError
+  )
+import Moonlight.Planar.Internal.Region.Types (RegionPublicationError)
+
+-- | Position in the submitted source family, independent of application labels.
+newtype OverlaySourceId = OverlaySourceId {overlaySourceIndex :: Int}
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A source-local label coordinate. Zero is the source's outside state;
+-- public authoring retains the typed table that interprets all other values.
+newtype OverlayLabelId = OverlayLabelId {overlayLabelIndex :: Int}
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data BoundaryLoopRef
+  = BoundaryOuterLoop
+  | BoundaryHoleLoop !Int
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data BoundaryFeature = BoundaryVertexFeature | BoundaryEdgeFeature
+
+-- | Provenance uses source ordinals, not a binary operand or a universal sum
+-- of the application's label types.
+data BoundaryRef (feature :: BoundaryFeature) = BoundaryRef
+  { boundaryRefSource :: !OverlaySourceId
+  , boundaryRefComponent :: !Int
+  , boundaryRefLoop :: !BoundaryLoopRef
+  , boundaryRefLocalIndex :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+type BoundaryVertexRef = BoundaryRef 'BoundaryVertexFeature
+type BoundaryEdgeRef = BoundaryRef 'BoundaryEdgeFeature
+
+data OverlayVertexOrigin = OverlayVertexOrigin
+  { overlayOriginVertices :: ![BoundaryVertexRef]
+  , overlayOriginEdges :: ![BoundaryEdgeRef]
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+newtype OverlayEdgeOrigin = OverlayEdgeOrigin
+  { overlayEdgeSources :: NonEmpty BoundaryEdgeRef
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayVertex = OverlayVertex
+  { overlayExactPoint :: !ExactPoint
+  , overlayVertexOrigin :: !OverlayVertexOrigin
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayReceipt = OverlayReceipt
+  { overlayInputSources :: !Int
+  , overlayInputSegments :: !Int
+  , overlayRelationEvents :: !Int
+  , overlayExactCrossings :: !Int
+  , overlayOverlapIntervals :: !Int
+  , overlayAtomicEdges :: !Int
+  , overlayOutputVertices :: !Int
+  , overlayArrangementCells :: !Int
+  , overlayBoundaryOrbits :: !Int
+  , overlayProvenanceIncidences :: !Int
+  , overlayTotalRelationChecks :: !Int
+  , overlaySweepMaximumHeight :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayArrangementObstruction
+  = OverlaySourceOutsideLabelInvalid !OverlaySourceId !OverlayLabelId
+  | OverlaySourceSideConflict
+      !OverlaySourceId !OverlayEdgeOrigin !(NonEmpty OverlayLabelId)
+  | OverlayRotationDegenerate !ExactPoint
+  | OverlayBoundaryOrbitMissing !DirectedEdgeId
+  | OverlayBoundaryOrbitNotClosed !DirectedEdgeId !DirectedEdgeId
+  | OverlayTransitionSourceMismatch
+      !FaceId !UndirectedEdgeId !OverlaySourceId !OverlayLabelId !OverlayLabelId
+  | OverlayFaceLabelConflict
+      !FaceId !UndirectedEdgeId !(IntMap OverlayLabelId) !(IntMap OverlayLabelId)
+  | OverlayPackedCapacityExceeded !Int !Int !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayCellWitness
+  = OverlayVertexMissing !VertexId
+  | OverlayEdgeMissing !UndirectedEdgeId
+  | OverlayFaceMissing !FaceId
+  | OverlayFaceInteriorUnavailable !FaceId
+  | OverlayExactVertexMissing !ExactPoint
+  | OverlaySourceLabelMissing !OverlaySourceId !OverlayLabelId
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlayError
+  = OverlayExactArithmetic !ExactArithmeticError
+  | OverlayExactGeometryInvalid !ExactGeometryError
+  | OverlaySegmentEventsInvalid !ExactSegmentEventObstruction
+  | OverlayArrangementInvalid !OverlayArrangementObstruction
+  | OverlayIncidenceInvalid !PlanarIncidenceError
+  | OverlayRegionPublicationFailed !RegionPublicationError
+  | OverlayProvenanceIncomplete !OverlayCellWitness
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Face zero is the single unbounded face and carries the outside label.
+-- Polygon cells and binary64 triangulations are not construction prerequisites
+-- or competing stored representations.
+-- Functor/Foldable/Traversable act only on this label plane: relabelling and
+-- fallible decoding cannot change geometry, incidence or source provenance.
+data OverlayResult labels = OverlayResult
+  { overlayResultIncidence :: !PlanarIncidence
+  , overlayResultVertices :: !(Vector OverlayVertex)
+  , overlayResultEdges :: !(Vector OverlayEdgeOrigin)
+  , overlayResultLabels :: !(Vector labels)
+  , overlayResultReceipt :: !OverlayReceipt
+  }
+  deriving stock (Functor, Foldable, Traversable)
+
+instance NFData labels => NFData (OverlayResult labels) where
+  rnf result =
+    rnf (overlayResultIncidence result)
+      `seq` rnf (overlayResultVertices result)
+      `seq` rnf (overlayResultEdges result)
+      `seq` rnf (overlayResultLabels result)
+      `seq` rnf (overlayResultReceipt result)
+
+data OverlaySelectionKind
+  = ClosedUnionSelection
+  | ClosedIntersectionSelection
+  | RegularizedDifferenceSelection
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data OverlaySelectionError
+  = OverlaySelectionContainsUnboundedCell !OverlaySelectionKind
+  | OverlaySelectionProvenance !OverlayCellWitness
+  | OverlaySelectionInvalid !CellSelectionError
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-overlay/Moonlight/Triangulation/Embedding.hs b/src-overlay/Moonlight/Triangulation/Embedding.hs
deleted file mode 100644
--- a/src-overlay/Moonlight/Triangulation/Embedding.hs
+++ /dev/null
@@ -1,546 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Bounded certification that projection of a declared exact arrangement to
--- binary64 preserves its local incidence and rotation data.
-module Moonlight.Triangulation.Embedding
-  ( DraftId (..)
-  , DraftVertexId
-  , DraftSegmentId
-  , DraftSourceId
-  , DraftIncidence (..)
-  , DraftNeighborhood (..)
-  , ExactArrangementDraft (..)
-  , DraftReference (..)
-  , OverlayEmbeddingObstruction (..)
-  , EmbeddingObligation (..)
-  , EmbeddingResidual
-  , residualUndischargedObligations
-  , milestoneOneResidual
-  , LocalEmbeddingCertificate
-  , certificateRoundedVertexDistinctnessCount
-  , certificateSplitOrderPreservationCount
-  , certificateIncidenceRelationPreservationCount
-  , certificateNeighborhoodRotationPreservationCount
-  , certificateRoundedVertices
-  , certificateResidual
-  , certifyLocalEmbedding
-  ) where
-
-import Control.DeepSeq (NFData)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , SegmentRelation
-  , exactPointToEmbeddingCandidate
-  , exactSegmentRelation
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( cyclePairs
-  , unorderedPairs
-  )
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import qualified Moonlight.Triangulation.Internal.Predicates as Admitted
-  ( orient2d
-  , segmentRelation
-  )
-import Moonlight.Triangulation.Types
-  ( Point (..)
-  , PointValidationError
-  , QueryPoint
-  , queryPointValue
-  )
-
--- | Phantom draft kind; three incompatible identifiers share one scalar owner.
-data DraftEntity = DraftVertexEntity | DraftSegmentEntity | DraftSourceEntity
-
-newtype DraftId (entity :: DraftEntity) = DraftId Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-type DraftVertexId = DraftId 'DraftVertexEntity
-type DraftSegmentId = DraftId 'DraftSegmentEntity
-type DraftSourceId = DraftId 'DraftSourceEntity
-
--- | A declared relation between two atomic draft segments.
-data DraftIncidence = DraftIncidence
-  { -- | First declared atomic segment.
-    draftIncidenceFirstSegment :: !DraftSegmentId
-  , -- | Second declared atomic segment.
-    draftIncidenceSecondSegment :: !DraftSegmentId
-  , -- | Relation declared to hold in both exact and rounded geometry.
-    draftIncidenceRelation :: !SegmentRelation
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One exact vertex and the cyclic neighbor order declared around it.
-data DraftNeighborhood = DraftNeighborhood
-  { -- | Center of the declared local rotation.
-    draftNeighborhoodCenter :: !DraftVertexId
-  , -- | Neighbors in cyclic rotation order.
-    draftNeighborhoodNeighbors :: !(NonEmpty DraftVertexId)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A hand-built exact arrangement draft. It contains only declared local
--- structure; no arrangement or global crossing search is derived here.
-data ExactArrangementDraft = ExactArrangementDraft
-  { -- | Exact coordinates keyed by draft-local vertex identity.
-    draftVertices :: !(Map DraftVertexId ExactPoint)
-  , -- | Atomic segment endpoint identities.
-    draftSegments :: !(Map DraftSegmentId (DraftVertexId, DraftVertexId))
-  , -- | Exact split parameters and vertices in source-segment order.
-    draftSourceMemberships :: !(Map DraftSourceId [(ExactRational, DraftVertexId)])
-  , -- | Segment relations declared to remain invariant under projection.
-    draftIncidences :: ![DraftIncidence]
-  , -- | Cyclic local rotations declared to retain their neighbor order.
-    draftNeighborhoods :: ![DraftNeighborhood]
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The typed identity of a missing draft reference.
-data DraftReference
-  = DraftVertexReference !DraftVertexId
-  | DraftSegmentReference !DraftSegmentId
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A witness that prevents local embedding certification.
-data OverlayEmbeddingObstruction
-  = DraftReferenceMissing !DraftReference
-  | VertexProjectionRefused !DraftVertexId !PointValidationError
-  | RoundedVerticesCollide !DraftVertexId !DraftVertexId !Point
-  | SplitOrderNotPreserved !DraftSourceId !DraftVertexId !DraftVertexId
-  | IncidenceRelationChanged
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-      !SegmentRelation
-      !SegmentRelation
-  | NeighborhoodRotationChanged
-      !DraftVertexId
-      !(NonEmpty DraftVertexId)
-      !(NonEmpty DraftVertexId)
-  | GlobalRelationAdded
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-  | GlobalRelationRemoved
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-  | GlobalRelationChanged
-      !DraftSegmentId
-      !DraftSegmentId
-      !SegmentRelation
-      !SegmentRelation
-  | ProjectedSegmentCollapsed
-      !DraftSegmentId
-      !DraftVertexId
-      !DraftVertexId
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | An embedding obligation not discharged by the bounded local certifier.
-data EmbeddingObligation
-  = GlobalNoNewCrossing
-  deriving stock (Bounded, Enum, Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A nonempty collection of obligations deferred to a later owner.
-newtype EmbeddingResidual = EmbeddingResidual (NonEmpty EmbeddingObligation)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Read the obligations that remain explicitly undischarged.
-residualUndischargedObligations
-  :: EmbeddingResidual
-  -> NonEmpty EmbeddingObligation
-residualUndischargedObligations (EmbeddingResidual obligations) = obligations
-
--- | The Milestone 1 residual: the arrangement sweep has not yet proved global
--- absence of new crossings.
-milestoneOneResidual :: EmbeddingResidual
-milestoneOneResidual = EmbeddingResidual (GlobalNoNewCrossing :| [])
-
--- | A certificate for exactly four local obligations on a declared draft:
--- vertex distinctness, source split order, declared incidences, and local
--- neighborhood rotation. Global absence of new crossings is unproved until
--- the Milestone 2 arrangement sweep supplies the complete obligation set.
-data LocalEmbeddingCertificate = LocalEmbeddingCertificate
-  { -- | Number of rounded vertex-pair distinctness checks discharged.
-    certificateRoundedVertexDistinctnessCount :: !Int
-  , -- | Number of adjacent source split-order checks discharged.
-    certificateSplitOrderPreservationCount :: !Int
-  , -- | Number of declared incidence checks discharged.
-    certificateIncidenceRelationPreservationCount :: !Int
-  , -- | Number of neighborhood rotation entries certified.
-    certificateNeighborhoodRotationPreservationCount :: !Int
-  , -- | The admitted candidate projection certified by the four local obligations.
-    certificateRoundedVertices :: !(Map DraftVertexId QueryPoint)
-  , -- | The necessarily nonempty global obligation residual.
-    certificateResidual :: !EmbeddingResidual
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Certify the four bounded local embedding obligations, collecting every
--- witness within each obligation and preserving obligation order.
-certifyLocalEmbedding
-  :: ExactArrangementDraft
-  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
-certifyLocalEmbedding draft =
-  case NonEmpty.nonEmpty (structuralObstructions draft) of
-    Just obstructions -> Left obstructions
-    Nothing ->
-      case projectVertices (draftVertices draft) of
-        Invalid obstructions -> Left obstructions
-        Valid projectedVertices ->
-          certifyProjectedDraft draft projectedVertices
-
-data Validation value
-  = Invalid !(NonEmpty OverlayEmbeddingObstruction)
-  | Valid value
-
-instance Functor Validation where
-  fmap _ (Invalid obstructions) = Invalid obstructions
-  fmap transform (Valid value) = Valid (transform value)
-
-instance Applicative Validation where
-  pure = Valid
-  Invalid left <*> Invalid right = Invalid (left <> right)
-  Invalid obstructions <*> Valid _ = Invalid obstructions
-  Valid _ <*> Invalid obstructions = Invalid obstructions
-  Valid transform <*> Valid value = Valid (transform value)
-
-invalid :: OverlayEmbeddingObstruction -> Validation value
-invalid obstruction = Invalid (obstruction :| [])
-
-data ProjectedVertex = ProjectedVertex
-  { projectedExactPoint :: !ExactPoint
-  , projectedRoundedPoint :: !QueryPoint
-  }
-
-data ResolvedSegment = ResolvedSegment
-  { resolvedSegmentId :: !DraftSegmentId
-  , resolvedSegmentFrom :: !ResolvedVertex
-  , resolvedSegmentTo :: !ResolvedVertex
-  }
-
-data ResolvedVertex = ResolvedVertex
-  { resolvedVertexId :: !DraftVertexId
-  , resolvedProjectedVertex :: !ProjectedVertex
-  }
-
-data ResolvedMembership = ResolvedMembership
-  { resolvedMembershipParameter :: !ExactRational
-  , resolvedMembershipVertex :: !ResolvedVertex
-  }
-
-structuralObstructions
-  :: ExactArrangementDraft
-  -> [OverlayEmbeddingObstruction]
-structuralObstructions draft =
-  segmentEndpointObstructions
-    <> sourceMembershipObstructions
-    <> incidenceObstructions
-    <> neighborhoodObstructions
- where
-  vertices = draftVertices draft
-  segments = draftSegments draft
-  missingVertex vertexId =
-    [ DraftReferenceMissing (DraftVertexReference vertexId)
-    | Map.notMember vertexId vertices
-    ]
-  missingSegment segmentId =
-    [ DraftReferenceMissing (DraftSegmentReference segmentId)
-    | Map.notMember segmentId segments
-    ]
-  segmentEndpointObstructions =
-    concatMap
-      (\(_, (from, to)) -> missingVertex from <> missingVertex to)
-      (Map.toAscList segments)
-  sourceMembershipObstructions =
-    concatMap
-      (concatMap (missingVertex . snd) . snd)
-      (Map.toAscList (draftSourceMemberships draft))
-  incidenceObstructions =
-    concatMap
-      ( \incidence ->
-          missingSegment (draftIncidenceFirstSegment incidence)
-            <> missingSegment (draftIncidenceSecondSegment incidence)
-      )
-      (draftIncidences draft)
-  neighborhoodObstructions =
-    concatMap
-      ( \neighborhood ->
-          missingVertex (draftNeighborhoodCenter neighborhood)
-            <> concatMap missingVertex (draftNeighborhoodNeighbors neighborhood)
-      )
-      (draftNeighborhoods draft)
-
-projectVertices
-  :: Map DraftVertexId ExactPoint
-  -> Validation (Map DraftVertexId ProjectedVertex)
-projectVertices =
-  Map.traverseWithKey
-    ( \vertexId point ->
-        case exactPointToEmbeddingCandidate point of
-          Left projectionError ->
-            invalid (VertexProjectionRefused vertexId projectionError)
-          Right roundedPoint -> Valid (ProjectedVertex point roundedPoint)
-    )
-
-certifyProjectedDraft
-  :: ExactArrangementDraft
-  -> Map DraftVertexId ProjectedVertex
-  -> Either (NonEmpty OverlayEmbeddingObstruction) LocalEmbeddingCertificate
-certifyProjectedDraft draft projectedVertices =
-  case resolvedFailures of
-    Left obstruction -> Left (obstruction :| [])
-    Right localFailures ->
-      case NonEmpty.nonEmpty (collisionFailures <> localFailures) of
-        Just obstructions -> Left obstructions
-        Nothing ->
-          Right
-            LocalEmbeddingCertificate
-              { certificateRoundedVertexDistinctnessCount = distinctnessCount
-              , certificateSplitOrderPreservationCount = splitOrderCount
-              , certificateIncidenceRelationPreservationCount = incidenceCount
-              , certificateNeighborhoodRotationPreservationCount = neighborhoodCount
-              , certificateRoundedVertices = Map.map projectedRoundedPoint projectedVertices
-              , certificateResidual = milestoneOneResidual
-              }
- where
-  collisionFailures = roundedVertexCollisionObstructions projectedVertices
-  resolvedFailures = do
-    splitFailures <- traverse (uncurry resolveSource) (Map.toAscList (draftSourceMemberships draft))
-    incidenceFailures <- traverse resolveIncidence (draftIncidences draft)
-    neighborhoodFailures <- traverse resolveNeighborhood (draftNeighborhoods draft)
-    pure (concat splitFailures <> concat incidenceFailures <> concat neighborhoodFailures)
-  resolveVertex vertexId =
-    case Map.lookup vertexId projectedVertices of
-      Nothing -> Left (DraftReferenceMissing (DraftVertexReference vertexId))
-      Just projectedVertex -> Right (ResolvedVertex vertexId projectedVertex)
-  resolveSegment segmentId =
-    case Map.lookup segmentId (draftSegments draft) of
-      Nothing -> Left (DraftReferenceMissing (DraftSegmentReference segmentId))
-      Just (from, to) ->
-        ResolvedSegment segmentId
-          <$> resolveVertex from
-          <*> resolveVertex to
-  resolveSource sourceId memberships =
-    splitOrderObstructions sourceId
-      <$> traverse
-        ( \(parameter, vertexId) ->
-            ResolvedMembership parameter <$> resolveVertex vertexId
-        )
-        memberships
-  resolveIncidence incidence =
-    incidenceRelationObstructions
-      (draftIncidenceRelation incidence)
-      <$> resolveSegment (draftIncidenceFirstSegment incidence)
-      <*> resolveSegment (draftIncidenceSecondSegment incidence)
-  resolveNeighborhood neighborhood =
-    neighborhoodRotationObstructions
-      <$> resolveVertex (draftNeighborhoodCenter neighborhood)
-      <*> traverse resolveVertex (draftNeighborhoodNeighbors neighborhood)
-  vertexCount = Map.size projectedVertices
-  distinctnessCount = vertexCount * (vertexCount - 1) `quot` 2
-  splitOrderCount =
-    sum
-      ( map
-          (max 0 . subtract 1 . length)
-          (Map.elems (draftSourceMemberships draft))
-      )
-  incidenceCount = length (draftIncidences draft)
-  neighborhoodCount =
-    sum
-      ( map
-          (length . draftNeighborhoodNeighbors)
-          (draftNeighborhoods draft)
-      )
-
-roundedVertexCollisionObstructions
-  :: Map DraftVertexId ProjectedVertex
-  -> [OverlayEmbeddingObstruction]
-roundedVertexCollisionObstructions projectedVertices =
-  [ RoundedVerticesCollide leftId rightId (queryPointValue roundedPoint)
-  | (roundedPoint, vertexIds) <- Map.toAscList verticesByRoundedPoint
-  , (leftId, rightId) <- unorderedPairs (List.sort vertexIds)
-  ]
- where
-  verticesByRoundedPoint =
-    Map.fromListWith (<>)
-      [ (projectedRoundedPoint projectedVertex, [vertexId])
-      | (vertexId, projectedVertex) <- Map.toAscList projectedVertices
-      ]
-
-splitOrderObstructions
-  :: DraftSourceId
-  -> [ResolvedMembership]
-  -> [OverlayEmbeddingObstruction]
-splitOrderObstructions sourceId memberships =
-  case memberships of
-    firstMembership : secondMembership : remainingMemberships ->
-      let finalMembership =
-            List.foldl' (\_ current -> current) secondMembership remainingMemberships
-          sourceFrom = roundedMembershipPoint firstMembership
-          sourceTo = roundedMembershipPoint finalMembership
-       in [ SplitOrderNotPreserved
-              sourceId
-              (resolvedVertexId (resolvedMembershipVertex leftMembership))
-              (resolvedVertexId (resolvedMembershipVertex rightMembership))
-          | (leftMembership, rightMembership) <-
-              zip
-                memberships
-                (drop 1 memberships)
-          , compare
-              (resolvedMembershipParameter leftMembership)
-              (resolvedMembershipParameter rightMembership)
-              /= roundedOrderAlong
-                sourceFrom
-                sourceTo
-                (roundedMembershipPoint leftMembership)
-                (roundedMembershipPoint rightMembership)
-          ]
-    _ -> []
-
-roundedMembershipPoint :: ResolvedMembership -> Point
-roundedMembershipPoint =
-  queryPointValue
-    . projectedRoundedPoint
-    . resolvedProjectedVertex
-    . resolvedMembershipVertex
-
-roundedOrderAlong :: Point -> Point -> Point -> Point -> Ordering
-roundedOrderAlong
-  (Point sourceFromX sourceFromY)
-  (Point sourceToX sourceToY)
-  (Point leftX leftY)
-  (Point rightX rightY) =
-    let directionX = sourceToX - sourceFromX
-        directionY = sourceToY - sourceFromY
-     in if abs directionX >= abs directionY
-          then
-            if directionX >= 0
-              then compare leftX rightX
-              else compare rightX leftX
-          else
-            if directionY >= 0
-              then compare leftY rightY
-              else compare rightY leftY
-
-incidenceRelationObstructions
-  :: SegmentRelation
-  -> ResolvedSegment
-  -> ResolvedSegment
-  -> [OverlayEmbeddingObstruction]
-incidenceRelationObstructions declaredRelation firstSegment secondSegment =
-  [ IncidenceRelationChanged
-      (resolvedSegmentId firstSegment)
-      (resolvedSegmentId secondSegment)
-      declaredRelation
-      exactRelation
-      roundedRelation
- | exactRelation /= declaredRelation || roundedRelation /= declaredRelation
-  ]
- where
-  exactRelation =
-    relationFor exactSegmentRelation projectedExactPoint firstSegment secondSegment
-  roundedRelation =
-    relationFor Admitted.segmentRelation roundedProjectedPoint firstSegment secondSegment
-
-relationFor
-  :: (point -> point -> point -> point -> SegmentRelation)
-  -> (ProjectedVertex -> point)
-  -> ResolvedSegment
-  -> ResolvedSegment
-  -> SegmentRelation
-relationFor relation project firstSegment secondSegment =
-  relation
-    (project (resolvedProjectedVertex (resolvedSegmentFrom firstSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentTo firstSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentFrom secondSegment)))
-    (project (resolvedProjectedVertex (resolvedSegmentTo secondSegment)))
-
-neighborhoodRotationObstructions
-  :: ResolvedVertex
-  -> NonEmpty ResolvedVertex
-  -> [OverlayEmbeddingObstruction]
-neighborhoodRotationObstructions center exactRotation =
-  [ NeighborhoodRotationChanged
-      (resolvedVertexId center)
-      exactVertexRotation
-      roundedVertexRotation
-  | roundedRotationDescents > 1
-  ]
- where
-  exactVertexRotation = fmap resolvedVertexId exactRotation
-  roundedRotationDescents =
-    List.foldl'
-      (\descentCount (left, right) ->
-         descentCount
-           + if compareRoundedAround center left right == GT
-               then 1
-               else 0)
-      (0 :: Int)
-      (cyclePairs exactRotation)
-  roundedVertexRotation =
-    fmap resolvedVertexId
-      (NonEmpty.sortBy (compareRoundedAround center) exactRotation)
-
-compareRoundedAround
-  :: ResolvedVertex
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> Ordering
-compareRoundedAround center left right =
-  compareRoundedAroundWith
-    ( Admitted.orient2d
-        (roundedVertexPoint center)
-        (roundedVertexPoint left)
-        (roundedVertexPoint right)
-    )
-    center
-    left
-    right
-
-compareRoundedAroundWith
-  :: Ordering
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> ResolvedVertex
-  -> Ordering
-compareRoundedAroundWith roundedOrientation center left right =
-  case compare (roundedVectorHalf center left) (roundedVectorHalf center right) of
-    EQ ->
-      case roundedOrientation of
-        GT -> LT
-        LT -> GT
-        EQ -> compare (roundedVertexPoint left) (roundedVertexPoint right)
-    ordering -> ordering
-
-roundedVectorHalf :: ResolvedVertex -> ResolvedVertex -> Bool
-roundedVectorHalf center neighbor =
-  let Point centerX centerY = roundedVertexPoint center
-      Point neighborX neighborY = roundedVertexPoint neighbor
-      deltaX = neighborX - centerX
-      deltaY = neighborY - centerY
-   in deltaY < 0 || (deltaY == 0 && deltaX < 0)
-
-roundedVertexPoint :: ResolvedVertex -> Point
-roundedVertexPoint = roundedProjectedPoint . resolvedProjectedVertex
-
-roundedProjectedPoint :: ProjectedVertex -> Point
-roundedProjectedPoint = queryPointValue . projectedRoundedPoint
diff --git a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs b/src-overlay/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
deleted file mode 100644
--- a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Arrangement.hs
+++ /dev/null
@@ -1,812 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact source normalization and binary64 embedding certification. This
--- module ends at the arrangement/resident seam: no DCEL face state crosses it.
-module Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( ExactEdgeKey
-  , AtomicEdge (..)
-  , atomicEdgeFrom
-  , atomicEdgeTo
-  , OverlayVertexSeed (..)
-  , ArrangementMetrics (..)
-  , CertifiedArrangement (..)
-  , certifyArrangement
-  , canonicalEdgeKey
-  , atomicKey
-  , compareAround
-  ) where
-
-import Data.Bifunctor (first)
-import Data.List (sortBy)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as V
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError
-  , ExactPoint
-  , ExactSegment
-  , SegmentRelation (SegmentsShareEndpoint)
-  , compareExactVectorAngle
-  , exactPointCoordinates
-  , exactPointFromQueryPoint
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactVectorFromPoints
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( consecutivePairs
-  , orderedPair
-  , unorderedPairs
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactDivide
-  , exactRationalIsZero
-  )
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEvent (..)
-  , ExactSegmentEventPlan
-  , ExactSweepSegmentId (..)
-  , exactSegmentEventPlan
-  , exactSegmentEvents
-  , exactSegmentPairChecks
-  , exactSegmentRelationMap
-  , exactSegmentSplitPoints
-  , exactSegmentSweepMaximumHeight
-  )
-import Moonlight.Triangulation.Embedding
-  ( DraftId (..)
-  , DraftIncidence (..)
-  , DraftNeighborhood (..)
-  , DraftReference (..)
-  , DraftVertexId
-  , ExactArrangementDraft (..)
-  , LocalEmbeddingCertificate
-  , certificateRoundedVertices
-  , OverlayEmbeddingObstruction (..)
-  , certifyLocalEmbedding
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Types
-  ( HasPosition (..)
-  , Point
-  , queryPointValue
-  )
-import Moonlight.Triangulation.Region
-  ( ExactLoop
-  , PlanarLayer
-  , PolygonComponent
-  , exactLoopPoints
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarRegionComponents
-  , polygonHoleLoops
-  , polygonOuterLoop
-  )
-
-type ExactEdgeKey = (ExactPoint, ExactPoint)
-
-data SourceBoundary leftLabel rightLabel
-  = LeftSourceBoundary
-      !ExactSegment
-      !leftLabel
-      !(BoundaryVertexRef 'LeftOverlayOperand)
-      !(BoundaryVertexRef 'LeftOverlayOperand)
-      !(BoundaryEdgeRef 'LeftOverlayOperand)
-  | RightSourceBoundary
-      !ExactSegment
-      !rightLabel
-      !(BoundaryVertexRef 'RightOverlayOperand)
-      !(BoundaryVertexRef 'RightOverlayOperand)
-      !(BoundaryEdgeRef 'RightOverlayOperand)
-
-type SourceBoundaryConstructor operand label leftLabel rightLabel =
-  ExactSegment
-  -> label
-  -> BoundaryVertexRef operand
-  -> BoundaryVertexRef operand
-  -> BoundaryEdgeRef operand
-  -> SourceBoundary leftLabel rightLabel
-
-data AtomicContribution leftLabel rightLabel =
-  AtomicContribution !(SourceBoundary leftLabel rightLabel) !Bool
-
-data AtomicEdge leftLabel rightLabel = AtomicEdge
-  { atomicEdgeSegment :: !ExactSegment
-  , atomicEdgeOrigin :: !OverlayEdgeOrigin
-  , atomicEdgeLeftTransition :: !(Maybe (leftLabel, leftLabel))
-  , atomicEdgeRightTransition :: !(Maybe (rightLabel, rightLabel))
-  }
-
-data OriginAccumulation = OriginAccumulation
-  { accumulatedLeftVertices :: !(Set (BoundaryVertexRef 'LeftOverlayOperand))
-  , accumulatedRightVertices :: !(Set (BoundaryVertexRef 'RightOverlayOperand))
-  , accumulatedLeftEdges :: !(Set (BoundaryEdgeRef 'LeftOverlayOperand))
-  , accumulatedRightEdges :: !(Set (BoundaryEdgeRef 'RightOverlayOperand))
-  }
-
-data OverlayVertexSeed = OverlayVertexSeed
-  { seedExactPoint :: !ExactPoint
-  , seedEmbeddedPoint :: !Point
-  , seedOrigin :: !OverlayVertexOrigin
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-
-instance HasPosition OverlayVertexSeed where
-  position = seedEmbeddedPoint
-
-data ArrangementMetrics = ArrangementMetrics
-  { arrangementInputSegments :: !Int
-  , arrangementRelationEvents :: !Int
-  , arrangementExactCrossings :: !Int
-  , arrangementOverlapIntervals :: !Int
-  , arrangementEmbeddingCandidates :: !Int
-  , arrangementTotalRelationChecks :: !Int
-  , arrangementSweepMaximumHeight :: !Int
-  }
-
-data CertifiedArrangement leftLabel rightLabel = CertifiedArrangement
-  { certifiedAtomicEdges :: !(V.Vector (AtomicEdge leftLabel rightLabel))
-  , certifiedVertexSeeds :: !(V.Vector OverlayVertexSeed)
-  , certifiedConstraints :: !(V.Vector (Int, Int))
-  , certifiedInexactEmbeddingPoints :: !(Set ExactPoint)
-  , certifiedMetrics :: !ArrangementMetrics
-  }
-
-certifyArrangement
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (CertifiedArrangement leftLabel rightLabel)
-certifyArrangement leftLayer rightLayer = do
-  sources <- flattenLayers leftLayer rightLayer
-  sourcePlan <-
-    first OverlaySegmentEventsInvalid
-      (exactSegmentEventPlan (V.map sourceExactSegment sources))
-  atomicEdges <- normalizeAtomicEdges leftLayer rightLayer sources sourcePlan
-  let atomicVector = V.fromList atomicEdges
-      pointIds = exactPointIds atomicVector
-      origins = vertexOrigins sources atomicVector
-      atomicRelations = atomicEndpointRelations atomicVector
-  draft <- exactArrangementDraft sources sourcePlan atomicVector pointIds atomicRelations
-  localCertificate <- first OverlayEmbeddingRefused (certifyLocalEmbedding draft)
-  let projectedPoints = projectedExactPoints localCertificate
-      inexactEmbeddingPoints =
-        Set.fromList
-          [ exactPoint
-          | (exactPoint, vertexId) <- Map.toAscList pointIds
-          , Map.lookup vertexId projectedPoints /= Just exactPoint
-          ]
-  projectedSegments <- projectedAtomicSegments atomicVector pointIds projectedPoints
-  projectedPlan <-
-    first OverlaySegmentEventsInvalid
-      (exactSegmentEventPlan projectedSegments)
-  dischargeGlobalRelations atomicRelations projectedPlan
-  seeds <- overlayVertexSeeds pointIds origins localCertificate
-  constraints <- atomicConstraints pointIds atomicVector
-  let events = exactSegmentEvents sourcePlan
-  pure
-    CertifiedArrangement
-      { certifiedAtomicEdges = atomicVector
-      , certifiedVertexSeeds = seeds
-      , certifiedConstraints = constraints
-      , certifiedInexactEmbeddingPoints = inexactEmbeddingPoints
-      , certifiedMetrics =
-          ArrangementMetrics
-            { arrangementInputSegments = V.length sources
-            , arrangementRelationEvents = length events
-            , arrangementExactCrossings =
-                length [() | ExactProperCrossing {} <- events]
-            , arrangementOverlapIntervals =
-                length [() | ExactCollinearOverlap {} <- events]
-            , arrangementEmbeddingCandidates = Map.size pointIds
-            , arrangementTotalRelationChecks =
-                exactSegmentPairChecks sourcePlan
-                  + exactSegmentPairChecks projectedPlan
-            , arrangementSweepMaximumHeight =
-                max
-                  (exactSegmentSweepMaximumHeight sourcePlan)
-                  (exactSegmentSweepMaximumHeight projectedPlan)
-            }
-      }
-flattenLayers
-  :: PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector (SourceBoundary leftLabel rightLabel))
-flattenLayers leftLayer rightLayer = do
-  leftSources <- flattenLayer LeftSourceBoundary leftLayer
-  rightSources <- flattenLayer RightSourceBoundary rightLayer
-  pure (V.fromList (leftSources <> rightSources))
-
-flattenLayer
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> PlanarLayer label
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenLayer makeBoundary layer =
-  fmap concat
-    ( traverse
-        (\(componentIndex, (label, component)) ->
-           flattenComponent makeBoundary componentIndex label component)
-        (zip [0 ..] (labelledComponents layer))
-    )
-{-# INLINE flattenLayer #-}
-
-labelledComponents :: PlanarLayer label -> [(label, PolygonComponent)]
-labelledComponents layer =
-  [ (label, component)
-  | (label, region) <- Map.toAscList (planarLayerRegions layer)
-  , component <- planarRegionComponents region
-  ]
-
-flattenComponent
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> Int
-  -> label
-  -> PolygonComponent
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenComponent makeBoundary componentIndex label component = do
-  outer <-
-    flattenLoop
-      makeBoundary
-      componentIndex
-      BoundaryOuterLoop
-      label
-      (polygonOuterLoop component)
-  holes <-
-    fmap concat
-      ( traverse
-          (\(holeIndex, loop) ->
-             flattenLoop
-               makeBoundary
-               componentIndex
-               (BoundaryHoleLoop holeIndex)
-               label
-               loop)
-          (zip [0 ..] (polygonHoleLoops component))
-      )
-  pure (outer <> holes)
-{-# INLINE flattenComponent #-}
-
-flattenLoop
-  :: SourceBoundaryConstructor operand label leftLabel rightLabel
-  -> Int
-  -> BoundaryLoopRef
-  -> label
-  -> ExactLoop
-  -> Either (OverlayError leftLabel rightLabel) [SourceBoundary leftLabel rightLabel]
-flattenLoop makeBoundary componentIndex loopRef label loop =
-  traverse constructSource (indexedCycle (exactLoopPoints loop))
- where
-  constructSource (edgeIndex, fromIndex, from, toIndex, to) = do
-    segment <- first (sourceGeometryError from) (exactSegment from to)
-    pure
-      ( makeBoundary
-          segment
-          label
-          (BoundaryRef componentIndex loopRef fromIndex)
-          (BoundaryRef componentIndex loopRef toIndex)
-          (BoundaryRef componentIndex loopRef edgeIndex)
-      )
-{-# INLINE flattenLoop #-}
-
-sourceGeometryError
-  :: ExactPoint
-  -> ExactGeometryError
-  -> OverlayError leftLabel rightLabel
-sourceGeometryError point _ = OverlayArrangementInvalid (OverlayRotationDegenerate point)
-
-indexedCycle :: NonEmpty value -> [(Int, Int, value, Int, value)]
-indexedCycle (firstValue :| remaining) =
-  let values = firstValue : remaining
-      count = length values
-   in [ (index, index, from, (index + 1) `mod` count, to)
-      | (index, (from, to)) <- zip [0 ..] (zip values (remaining <> [firstValue]))
-      ]
-
-sourceExactSegment :: SourceBoundary leftLabel rightLabel -> ExactSegment
-sourceExactSegment (LeftSourceBoundary segment _ _ _ _) = segment
-sourceExactSegment (RightSourceBoundary segment _ _ _ _) = segment
-
-normalizeAtomicEdges
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> V.Vector (SourceBoundary leftLabel rightLabel)
-  -> ExactSegmentEventPlan
-  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
-normalizeAtomicEdges leftLayer rightLayer sources plan =
-  fmap concat
-    ( traverse
-        (resolveAtomicContributions leftOutside rightOutside)
-        (Map.toAscList grouped)
-    )
- where
-  leftOutside = planarLayerOutsideLabel leftLayer
-  rightOutside = planarLayerOutsideLabel rightLayer
-  grouped =
-    V.ifoldl'
-      (\groups sourceIndex source ->
-         List.foldl'
-           (insertAtomic source)
-           groups
-           (consecutivePairs (exactSegmentSplitPoints plan (ExactSweepSegmentId sourceIndex))))
-      Map.empty
-      sources
-  insertAtomic
-    :: SourceBoundary leftLabel' rightLabel'
-    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
-    -> (ExactPoint, ExactPoint)
-    -> Map ExactEdgeKey [AtomicContribution leftLabel' rightLabel']
-  insertAtomic source groups (from, to)
-    | from == to = groups
-    | otherwise =
-        let key@(canonicalFrom, _) = canonicalEdgeKey from to
-            contribution = AtomicContribution source (from == canonicalFrom)
-         in Map.insertWith (<>) key [contribution] groups
-
-resolveAtomicContributions
-  :: (Ord leftLabel, Ord rightLabel)
-  => leftLabel
-  -> rightLabel
-  -> (ExactEdgeKey, [AtomicContribution leftLabel rightLabel])
-  -> Either (OverlayError leftLabel rightLabel) [AtomicEdge leftLabel rightLabel]
-resolveAtomicContributions leftOutside rightOutside ((from, to), contributions) = do
-  let
-      ( leftLabelsOnLeft
-        , leftLabelsOnRight
-        , rightLabelsOnLeft
-        , rightLabelsOnRight
-        , leftSources
-        , rightSources
-        ) =
-          List.foldl'
-            collectContribution
-            (Set.empty, Set.empty, Set.empty, Set.empty, Set.empty, Set.empty)
-            contributions
-      origin =
-        OverlayEdgeOrigin
-          (Set.toAscList leftSources)
-          (Set.toAscList rightSources)
-  leftTransition <-
-    resolveTransition
-      OverlayLeftSourceSideConflict
-      origin
-      leftOutside
-      leftLabelsOnLeft
-      leftLabelsOnRight
-  rightTransition <-
-    resolveTransition
-      OverlayRightSourceSideConflict
-      origin
-      rightOutside
-      rightLabelsOnLeft
-      rightLabelsOnRight
-  if transitionIsIdentity leftTransition && transitionIsIdentity rightTransition
-    then Right []
-    else do
-      segment <- first (sourceGeometryError from) (exactSegment from to)
-      Right
-        [ AtomicEdge
-            { atomicEdgeSegment = segment
-            , atomicEdgeOrigin = origin
-            , atomicEdgeLeftTransition = leftTransition
-            , atomicEdgeRightTransition = rightTransition
-            }
-        ]
- where
-  collectContribution
-    :: (Ord leftLabel', Ord rightLabel')
-    => ( Set leftLabel'
-       , Set leftLabel'
-       , Set rightLabel'
-       , Set rightLabel'
-       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
-       , Set (BoundaryEdgeRef 'RightOverlayOperand)
-       )
-    -> AtomicContribution leftLabel' rightLabel'
-    -> ( Set leftLabel'
-       , Set leftLabel'
-       , Set rightLabel'
-       , Set rightLabel'
-       , Set (BoundaryEdgeRef 'LeftOverlayOperand)
-       , Set (BoundaryEdgeRef 'RightOverlayOperand)
-       )
-  collectContribution
-    ( !leftLeft
-      , !leftRight
-      , !rightLeft
-      , !rightRight
-      , !leftSources
-      , !rightSources
-      )
-    contribution =
-    case contribution of
-      AtomicContribution (LeftSourceBoundary _ label _ _ reference) follows ->
-        ( if follows then Set.insert label leftLeft else leftLeft
-        , if follows then leftRight else Set.insert label leftRight
-        , rightLeft
-        , rightRight
-        , Set.insert reference leftSources
-        , rightSources
-        )
-      AtomicContribution (RightSourceBoundary _ label _ _ reference) follows ->
-        ( leftLeft
-        , leftRight
-        , if follows then Set.insert label rightLeft else rightLeft
-        , if follows then rightRight else Set.insert label rightRight
-        , leftSources
-        , Set.insert reference rightSources
-        )
-
-resolveTransition
-  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
-  -> OverlayEdgeOrigin
-  -> label
-  -> Set label
-  -> Set label
-  -> Either (OverlayError leftLabel rightLabel) (Maybe (label, label))
-resolveTransition sideConflict origin outside labelsOnLeft labelsOnRight
-  | Set.null labelsOnLeft && Set.null labelsOnRight = Right Nothing
-  | otherwise =
-      Just
-        <$> ((,)
-               <$> resolveSide sideConflict origin outside labelsOnLeft
-               <*> resolveSide sideConflict origin outside labelsOnRight)
-{-# INLINE resolveTransition #-}
-
-transitionIsIdentity :: Eq label => Maybe (label, label) -> Bool
-transitionIsIdentity Nothing = True
-transitionIsIdentity (Just (leftLabel, rightLabel)) = leftLabel == rightLabel
-
-resolveSide
-  :: (OverlayEdgeOrigin -> NonEmpty label -> OverlayArrangementObstruction leftLabel rightLabel)
-  -> OverlayEdgeOrigin
-  -> label
-  -> Set label
-  -> Either (OverlayError leftLabel rightLabel) label
-resolveSide sideConflict origin outside labels =
-  case Set.toAscList labels of
-    [] -> Right outside
-    [label] -> Right label
-    firstLabel : remaining ->
-      Left
-        ( OverlayArrangementInvalid
-            (sideConflict origin (firstLabel :| remaining))
-        )
-{-# INLINE resolveSide #-}
-
-exactPointIds
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-exactPointIds edges =
-  Map.fromAscList
-    ( zip
-        (Set.toAscList (V.foldl' collect Set.empty edges))
-      (map DraftId [0 ..])
-    )
- where
-  collect
-    :: Set ExactPoint
-    -> AtomicEdge leftLabel rightLabel
-    -> Set ExactPoint
-  collect points edge = Set.insert (atomicEdgeFrom edge) (Set.insert (atomicEdgeTo edge) points)
-
-vertexOrigins
-  :: V.Vector (SourceBoundary leftLabel rightLabel)
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint OverlayVertexOrigin
-vertexOrigins sources atomicEdges =
-  Map.map finalizeOrigin
-    ( V.foldl'
-        addAtomicOrigin
-        (V.foldl' addSourceEndpoints Map.empty sources)
-        atomicEdges
-    )
-
-emptyOrigin :: OriginAccumulation
-emptyOrigin = OriginAccumulation Set.empty Set.empty Set.empty Set.empty
-
-mergeOrigin :: OriginAccumulation -> OriginAccumulation -> OriginAccumulation
-mergeOrigin left right =
-  OriginAccumulation
-    { accumulatedLeftVertices = accumulatedLeftVertices left <> accumulatedLeftVertices right
-    , accumulatedRightVertices = accumulatedRightVertices left <> accumulatedRightVertices right
-    , accumulatedLeftEdges = accumulatedLeftEdges left <> accumulatedLeftEdges right
-    , accumulatedRightEdges = accumulatedRightEdges left <> accumulatedRightEdges right
-    }
-
-addSourceEndpoints
-  :: Map ExactPoint OriginAccumulation
-  -> SourceBoundary leftLabel rightLabel
-  -> Map ExactPoint OriginAccumulation
-addSourceEndpoints origins source =
-  case source of
-    LeftSourceBoundary segment _ fromReference toReference _ ->
-      let (from, to) = exactSegmentEndpoints segment
-       in insertOrigin to (emptyOrigin{accumulatedLeftVertices = Set.singleton toReference})
-            (insertOrigin from (emptyOrigin{accumulatedLeftVertices = Set.singleton fromReference}) origins)
-    RightSourceBoundary segment _ fromReference toReference _ ->
-      let (from, to) = exactSegmentEndpoints segment
-       in insertOrigin to (emptyOrigin{accumulatedRightVertices = Set.singleton toReference})
-            (insertOrigin from (emptyOrigin{accumulatedRightVertices = Set.singleton fromReference}) origins)
-
-addAtomicOrigin
-  :: Map ExactPoint OriginAccumulation
-  -> AtomicEdge leftLabel rightLabel
-  -> Map ExactPoint OriginAccumulation
-addAtomicOrigin origins edge =
-  let origin = atomicEdgeOrigin edge
-      accumulation =
-        emptyOrigin
-          { accumulatedLeftEdges = Set.fromList (overlayEdgeLeftSources origin)
-          , accumulatedRightEdges = Set.fromList (overlayEdgeRightSources origin)
-          }
-   in insertOrigin (atomicEdgeTo edge) accumulation
-        (insertOrigin (atomicEdgeFrom edge) accumulation origins)
-
-insertOrigin
-  :: ExactPoint
-  -> OriginAccumulation
-  -> Map ExactPoint OriginAccumulation
-  -> Map ExactPoint OriginAccumulation
-insertOrigin = Map.insertWith mergeOrigin
-
-finalizeOrigin :: OriginAccumulation -> OverlayVertexOrigin
-finalizeOrigin accumulated =
-  OverlayVertexOrigin
-    { overlayOriginLeftVertices = Set.toAscList (accumulatedLeftVertices accumulated)
-    , overlayOriginRightVertices = Set.toAscList (accumulatedRightVertices accumulated)
-    , overlayOriginLeftEdges = Set.toAscList (accumulatedLeftEdges accumulated)
-    , overlayOriginRightEdges = Set.toAscList (accumulatedRightEdges accumulated)
-    }
-
-exactArrangementDraft
-  :: V.Vector (SourceBoundary leftLabel rightLabel)
-  -> ExactSegmentEventPlan
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-  -> Either (OverlayError leftLabel rightLabel) ExactArrangementDraft
-exactArrangementDraft sources sourcePlan atomicEdges pointIds atomicRelations = do
-  draftSegmentsMap <-
-    Map.fromList
-      <$> traverse
-        (\(segmentIndex, edge) -> do
-           from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-           to <- requireDraftVertex pointIds (atomicEdgeTo edge)
-           pure (DraftId segmentIndex, (from, to)))
-        (V.toList (V.indexed atomicEdges))
-  memberships <-
-    Map.fromList
-      <$> traverse
-        (\(sourceIndex, source) -> do
-           values <-
-             traverse
-               (\point -> do
-                  parameter <- exactSourceParameter source point
-                  vertex <- requireDraftVertex pointIds point
-                  pure (parameter, vertex))
-               (exactSegmentSplitPoints sourcePlan (ExactSweepSegmentId sourceIndex))
-           pure (DraftId sourceIndex, values))
-        (V.toList (V.indexed sources))
-  neighborhoods <- buildDraftNeighborhoods atomicEdges pointIds
-  let incidences =
-        [ DraftIncidence
-            (DraftId leftIndex)
-            (DraftId rightIndex)
-            relation
-        | ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation) <-
-            Map.toAscList atomicRelations
-        ]
-  pure
-    ExactArrangementDraft
-      { draftVertices = Map.fromList [(vertexId, point) | (point, vertexId) <- Map.toAscList pointIds]
-      , draftSegments = draftSegmentsMap
-      , draftSourceMemberships = memberships
-      , draftIncidences = incidences
-      , draftNeighborhoods = neighborhoods
-      }
-
-requireDraftVertex
-  :: Map ExactPoint DraftVertexId
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) DraftVertexId
-requireDraftVertex pointIds point =
-  case Map.lookup point pointIds of
-    Just vertex -> Right vertex
-    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-
-exactSourceParameter
-  :: SourceBoundary leftLabel rightLabel
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) ExactRational
-exactSourceParameter source point =
-  let (from, to) = exactSegmentEndpoints (sourceExactSegment source)
-      (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-      (pointX, pointY) = exactPointCoordinates point
-      (numerator, denominator) =
-        if exactRationalIsZero (toX - fromX)
-          then (pointY - fromY, toY - fromY)
-          else (pointX - fromX, toX - fromX)
-   in first OverlayExactArithmetic (exactDivide numerator denominator)
-
-buildDraftNeighborhoods
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Either (OverlayError leftLabel rightLabel) [DraftNeighborhood]
-buildDraftNeighborhoods edges pointIds =
-  traverse neighborhood (Map.toAscList pointIds)
- where
-  neighborhood (centerPoint, centerId) = do
-    let neighbors = Map.findWithDefault Set.empty centerPoint adjacency
-    orderedPoints <-
-      case NonEmpty.nonEmpty (sortBy (compareAround centerPoint) (Set.toList neighbors)) of
-        Just points -> Right points
-        Nothing -> Left (OverlayArrangementInvalid (OverlayRotationDegenerate centerPoint))
-    orderedIds <- traverse (requireDraftVertex pointIds) orderedPoints
-    pure (DraftNeighborhood centerId orderedIds)
-  adjacency =
-    V.foldl'
-      (\graph edge ->
-         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton (atomicEdgeFrom edge))
-           (Map.insertWith Set.union (atomicEdgeFrom edge) (Set.singleton (atomicEdgeTo edge)) graph))
-      Map.empty
-      edges
-
-compareAround :: ExactPoint -> ExactPoint -> ExactPoint -> Ordering
-compareAround center left right =
-  case
-    compareExactVectorAngle
-      (exactVectorFromPoints center left)
-      (exactVectorFromPoints center right) of
-    EQ -> compare left right
-    ordering -> ordering
-
--- | Normalization splits every source at every exact event and coalesces
--- duplicate intervals. Distinct atomics can therefore meet only at a stored
--- endpoint; their entire relation section is the endpoint-incidence index.
-atomicEndpointRelations
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-atomicEndpointRelations edges =
-  Map.fromList
-    [ ( orderedPair
-          (ExactSweepSegmentId leftIndex)
-          (ExactSweepSegmentId rightIndex)
-      , SegmentsShareEndpoint
-      )
-    | incident <- Map.elems incidenceByPoint
-    , (leftIndex, rightIndex) <- unorderedPairs (Set.toAscList incident)
-    ]
- where
-  incidenceByPoint =
-    V.ifoldl'
-      (\incidence index edge ->
-         Map.insertWith Set.union (atomicEdgeTo edge) (Set.singleton index)
-           ( Map.insertWith Set.union
-               (atomicEdgeFrom edge)
-               (Set.singleton index)
-               incidence
-           ))
-      Map.empty
-      edges
-projectedExactPoints
-  :: LocalEmbeddingCertificate
-  -> Map DraftVertexId ExactPoint
-projectedExactPoints =
-  Map.map exactPointFromQueryPoint . certificateRoundedVertices
-
-projectedAtomicSegments
-  :: V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Map ExactPoint DraftVertexId
-  -> Map DraftVertexId ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector ExactSegment)
-projectedAtomicSegments edges pointIds projected =
-  V.imapM project edges
- where
-  project segmentIndex edge = do
-    fromId <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-    toId <- requireDraftVertex pointIds (atomicEdgeTo edge)
-    from <- requireProjected fromId
-    to <- requireProjected toId
-    first
-      (\_ ->
-         OverlayEmbeddingRefused
-           ( ProjectedSegmentCollapsed
-               (DraftId segmentIndex)
-               fromId
-               toId
-               :| []
-           ))
-      (exactSegment from to)
-  requireProjected vertex =
-    case Map.lookup vertex projected of
-      Just point -> Right point
-      Nothing ->
-        Left
-          ( OverlayEmbeddingRefused
-              (DraftReferenceMissing (DraftVertexReference vertex) :| [])
-          )
-
-dischargeGlobalRelations
-  :: Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-  -> ExactSegmentEventPlan
-  -> Either (OverlayError leftLabel rightLabel) ()
-dischargeGlobalRelations exactRelations projectedPlan =
-  case NonEmpty.nonEmpty obstructions of
-    Nothing -> Right ()
-    Just failures -> Left (OverlayEmbeddingRefused failures)
- where
-  projectedRelations = exactSegmentRelationMap projectedPlan
-  keys = Set.toAscList (Map.keysSet exactRelations <> Map.keysSet projectedRelations)
-  obstructions = concatMap compareRelation keys
-  compareRelation key@(ExactSweepSegmentId leftId, ExactSweepSegmentId rightId) =
-    case (Map.lookup key exactRelations, Map.lookup key projectedRelations) of
-      (Nothing, Just projected) ->
-        [GlobalRelationAdded (DraftId leftId) (DraftId rightId) projected]
-      (Just exact, Nothing) ->
-        [GlobalRelationRemoved (DraftId leftId) (DraftId rightId) exact]
-      (Just exact, Just projected)
-        | exact /= projected ->
-            [GlobalRelationChanged (DraftId leftId) (DraftId rightId) exact projected]
-      _ -> []
-
-overlayVertexSeeds
-  :: Map ExactPoint DraftVertexId
-  -> Map ExactPoint OverlayVertexOrigin
-  -> LocalEmbeddingCertificate
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector OverlayVertexSeed)
-overlayVertexSeeds pointIds origins certificate =
-  V.fromList
-    <$> traverse
-      (\(point, vertexId) -> do
-         embedded <-
-           case Map.lookup vertexId (certificateRoundedVertices certificate) of
-             Just value -> Right value
-             Nothing ->
-               Left
-                 ( OverlayEmbeddingRefused
-                     (DraftReferenceMissing (DraftVertexReference vertexId) :| [])
-                 )
-         origin <-
-           case Map.lookup point origins of
-             Just value -> Right value
-             Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-         pure (OverlayVertexSeed point (queryPointValue embedded) origin))
-      (Map.toAscList pointIds)
-
-atomicConstraints
-  :: Map ExactPoint DraftVertexId
-  -> V.Vector (AtomicEdge leftLabel rightLabel)
-  -> Either (OverlayError leftLabel rightLabel) (V.Vector (Int, Int))
-atomicConstraints pointIds =
-  V.mapM
-    (\edge -> do
-       DraftId from <- requireDraftVertex pointIds (atomicEdgeFrom edge)
-       DraftId to <- requireDraftVertex pointIds (atomicEdgeTo edge)
-       pure (from, to))
-
-
-canonicalEdgeKey :: ExactPoint -> ExactPoint -> ExactEdgeKey
-canonicalEdgeKey = orderedPair
-
-atomicEdgeFrom :: AtomicEdge leftLabel rightLabel -> ExactPoint
-atomicEdgeFrom = fst . exactSegmentEndpoints . atomicEdgeSegment
-
-atomicEdgeTo :: AtomicEdge leftLabel rightLabel -> ExactPoint
-atomicEdgeTo = snd . exactSegmentEndpoints . atomicEdgeSegment
-
-atomicKey :: AtomicEdge leftLabel rightLabel -> ExactEdgeKey
-atomicKey edge = (atomicEdgeFrom edge, atomicEdgeTo edge)
diff --git a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Resident.hs b/src-overlay/Moonlight/Triangulation/Internal/Overlay/Resident.hs
deleted file mode 100644
--- a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Resident.hs
+++ /dev/null
@@ -1,923 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | Resident DCEL descent and exact-cell gluing. The certified arrangement is
--- authoritative at this seam; diagonal schedules may change only its resident
--- triangulation, never its exact cell descriptors.
-module Moonlight.Triangulation.Internal.Overlay.Resident
-  ( OverlayDiagonalSchedule (..)
-  , residentOverlay
-  , faceLabels
-  , faceCarriesExactArea
-  , regionFaceLabels
-  , vertexSupport
-  , edgeSupport
-  ) where
-
-import Control.Monad (foldM)
-import Data.Bifunctor (first)
-import Data.Foldable (traverse_)
-import Data.List (partition, sort, sortBy)
-import qualified Data.List as List
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Maybe (listToMaybe, mapMaybe)
-import qualified Data.Sequence as Seq
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as V
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Dcel
-  ( faceData
-  , faceDirectedEdges
-  , faceVertices
-  , imapUndirectedEdges
-  , incidentFace
-  , isConstraintEdge
-  , numInnerFaces
-  , numVertices
-  , outerFace
-  , undirectedEndpoints
-  , vertexData
-  , vertexOutgoingEdges
-  )
-import Moonlight.Triangulation.Exact (ExactPoint, exactOrient2d)
-import Moonlight.Triangulation.FloodFillIterator
-  ( FaceComponent
-  , componentBoundary
-  , faceComponentFaces
-  , faceComponentsBy
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  , asUndirected
-  , directedPair
-  , reverseEdge
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( allFaces
-  , innerFaces
-  , undirectedEdges
-  )
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedFromVector)
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( consecutivePairs
-  , traceOrientedBoundaryCircuits
-  )
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Internal.Cdt.Build (constrainedDelaunay)
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError (..))
-import Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( ArrangementMetrics (..)
-  , AtomicEdge (..)
-  , atomicEdgeFrom
-  , atomicEdgeTo
-  , CertifiedArrangement (..)
-  , ExactEdgeKey
-  , OverlayVertexSeed (..)
-  , atomicKey
-  , canonicalEdgeKey
-  , compareAround
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( polygonComponentFromBoundaryCoordinates
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , PolygonComponent
-  , RegionPublicationError (..)
-  )
-import Moonlight.Triangulation.Internal.Representation
-  ( BuildResult (..)
-  , Triangulation (..)
-  )
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  , ElementDefaults (..)
-  )
-import Moonlight.Triangulation.Region (exactLoop)
-
-data OverlayDiagonalSchedule
-  = CanonicalOverlayDiagonals
-  | FlipFirstAdmissibleDiagonal
-  deriving stock (Eq, Ord, Show)
-
-data ComponentDraft leftLabel rightLabel = ComponentDraft
-  { componentDraftLabels :: !(leftLabel, rightLabel)
-  , componentDraftFaces :: !FaceComponent
-  , componentDraftPolygon :: !PolygonComponent
-  , componentDraftTouchesOuter :: !Bool
-  }
-
-residentOverlay
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayDiagonalSchedule
-  -> (leftLabel, rightLabel)
-  -> CertifiedArrangement leftLabel rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (OverlayResult leftLabel rightLabel)
-residentOverlay diagonalSchedule outsidePair certified = do
-  let atomicVector = certifiedAtomicEdges certified
-      seeds = certifiedVertexSeeds certified
-      constraints = certifiedConstraints certified
-      metrics = certifiedMetrics certified
-      defaults = ElementDefaults () () ()
-  built <- first OverlayBuildFailed (constrainedDelaunay defaults seeds constraints)
-  let resident = buildTriangulation built
-  if numVertices resident == V.length seeds
-    then Right ()
-    else
-      Left
-        ( OverlayProvenanceIncomplete
-            (OverlayEmbeddedVertexCountMismatch (V.length seeds) (numVertices resident))
-        )
-  canonicalResident <-
-    first (OverlayBuildFailed . CdtBuildError) (canonicalize resident)
-  let atomicByKey =
-        Map.fromList
-          [ (atomicKey edge, edge)
-          | edge <- V.toList atomicVector
-          ]
-  validateAtomicConstraints canonicalResident atomicByKey
-  scheduledResident <-
-    applyDiagonalSchedule
-      diagonalSchedule
-      defaults
-      seeds
-      constraints
-      canonicalResident
-  labelledFaces <- labelResidentFaces outsidePair scheduledResident atomicByKey
-  (fullDimensionalFaces, collapsedFaces) <-
-    if Set.null (certifiedInexactEmbeddingPoints certified)
-      then
-        Right
-          ( Set.fromList (innerFaces scheduledResident)
-          , Set.empty
-          )
-      else
-        exactFaceDimensions
-          (certifiedInexactEmbeddingPoints certified)
-          scheduledResident
-  componentDrafts <-
-    residentComponentDrafts
-      scheduledResident
-      labelledFaces
-      fullDimensionalFaces
-  let (unboundedDrafts, boundedDrafts) =
-        partitionDrafts outsidePair componentDrafts
-  numberedBounded <- numberBoundedComponents boundedDrafts
-  let fullCellIdByFace =
-        Map.fromList
-          ( [ (face, OverlayCellId 0)
-            | draftComponent <- unboundedDrafts
-            , face <- faceComponentFaces (componentDraftFaces draftComponent)
-            ]
-              <> [ (face, cellId)
-                 | (cellId, draftComponent) <- numberedBounded
-                 , face <- faceComponentFaces (componentDraftFaces draftComponent)
-                 ]
-          )
-  collapsedCellIds <-
-    descendCollapsedFaces
-      scheduledResident
-      atomicByKey
-      fullCellIdByFace
-      collapsedFaces
-  let cellIdByFace = fullCellIdByFace <> collapsedCellIds
-  unboundedLoops <-
-    unboundedCellBoundaryLoops scheduledResident atomicByKey cellIdByFace
-  let cells =
-        V.fromList
-          ( OverlayCell
-              { overlayCellLeft = fst outsidePair
-              , overlayCellRight = snd outsidePair
-              , overlayCellGeometry = UnboundedOverlayCell unboundedLoops
-              }
-              : [ OverlayCell
-                    { overlayCellLeft = fst (componentDraftLabels draftComponent)
-                    , overlayCellRight = snd (componentDraftLabels draftComponent)
-                    , overlayCellGeometry =
-                        BoundedOverlayCell (componentDraftPolygon draftComponent)
-                    }
-                | (_, draftComponent) <- numberedBounded
-                ]
-          )
-  withFaces <- attachFaceCells collapsedFaces cellIdByFace scheduledResident
-  let withVertices = attachOverlayVertices withFaces
-      withEdges =
-        imapUndirectedEdges
-          OverlayDiagonal
-          (\edge _ ->
-             case Map.lookup (residentEdgeKey withVertices edge) atomicByKey of
-               Nothing -> OverlayDiagonal
-               Just atomic -> OverlayBoundary (atomicEdgeOrigin atomic))
-          withVertices
-      receipt =
-        OverlayReceipt
-          { overlayInputSegments = arrangementInputSegments metrics
-          , overlayRelationEvents = arrangementRelationEvents metrics
-          , overlayExactCrossings = arrangementExactCrossings metrics
-          , overlayOverlapIntervals = arrangementOverlapIntervals metrics
-          , overlayAtomicEdges = V.length atomicVector
-          , overlayOutputVertices = numVertices withEdges
-          , overlayArrangementCells = V.length cells
-          , overlayResidentFaces = numInnerFaces withEdges
-          , overlayEmbeddingCandidates = arrangementEmbeddingCandidates metrics
-          , overlayTotalRelationChecks = arrangementTotalRelationChecks metrics
-          , overlaySweepMaximumHeight = arrangementSweepMaximumHeight metrics
-          }
-  pure
-    OverlayResult
-      { overlayResultTriangulation = withEdges
-      , overlayResultCells = cells
-      , overlayResultOutsideLabels = outsidePair
-      , overlayResultReceipt = receipt
-      }
-
-applyDiagonalSchedule
-  :: OverlayDiagonalSchedule
-  -> ElementDefaults () () ()
-  -> V.Vector OverlayVertexSeed
-  -> V.Vector (Int, Int)
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Triangulation 'Constrained OverlayVertexSeed () () ())
-applyDiagonalSchedule CanonicalOverlayDiagonals _ _ _ triangulation =
-  Right triangulation
-applyDiagonalSchedule
-  FlipFirstAdmissibleDiagonal
-  defaults
-  seeds
-  constraints
-  triangulation =
-    case firstAlternativeDiagonal triangulation of
-      Nothing -> Right triangulation
-      Just (from, to) -> do
-        let indexByPoint =
-              Map.fromList
-                [ (seedExactPoint seed, index)
-                | (index, seed) <- V.toList (V.indexed seeds)
-                ]
-        fromIndex <- requireSeedIndex indexByPoint from
-        toIndex <- requireSeedIndex indexByPoint to
-        rebuilt <-
-          first OverlayBuildFailed
-            (constrainedDelaunay defaults seeds (V.snoc constraints (fromIndex, toIndex)))
-        first (OverlayBuildFailed . CdtBuildError)
-          (canonicalize (buildTriangulation rebuilt))
-
-requireSeedIndex
-  :: Map ExactPoint Int
-  -> ExactPoint
-  -> Either (OverlayError leftLabel rightLabel) Int
-requireSeedIndex indexByPoint point =
-  case Map.lookup point indexByPoint of
-    Just index -> Right index
-    Nothing -> Left (OverlayProvenanceIncomplete (OverlayExactVertexMissing point))
-
-firstAlternativeDiagonal
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Maybe ExactEdgeKey
-firstAlternativeDiagonal triangulation =
-  listToMaybe
-    (mapMaybe (alternativeDiagonal triangulation) (undirectedEdges triangulation))
-
-alternativeDiagonal
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> UndirectedEdgeId
-  -> Maybe ExactEdgeKey
-alternativeDiagonal triangulation edge
-  | isConstraintEdge triangulation edge = Nothing
-  | leftFace == outerFace || rightFace == outerFace = Nothing
-  | exactOrient2d c d b == GT && exactOrient2d d c a == GT =
-      Just (canonicalEdgeKey c d)
-  | otherwise = Nothing
- where
-  (forward, backward) = directedPair edge
-  leftFace = incidentFace triangulation forward
-  rightFace = incidentFace triangulation backward
-  a = seedExactPoint (vertexData triangulation (Dcel.origin triangulation forward))
-  b = seedExactPoint (vertexData triangulation (Dcel.origin triangulation backward))
-  c =
-    seedExactPoint
-      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation forward)))
-  d =
-    seedExactPoint
-      (vertexData triangulation (Dcel.origin triangulation (Dcel.previous triangulation backward)))
-labelResidentFaces
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel, rightLabel)
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel))
-labelResidentFaces outsideLabels triangulation atomicByKey = do
-  labelled <-
-    descendFaceTransitions
-      triangulation
-      atomicByKey
-      (Map.singleton outerFace outsideLabels)
-      (Seq.singleton outerFace)
-  case
-    [ face
-    | face <- innerFaces triangulation
-    , Map.notMember face labelled
-    ] of
-    missing : _ ->
-      Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned missing))
-    [] -> Right (Map.delete outerFace labelled)
-
-seedDirectedEndpoints
-  :: Triangulation mode OverlayVertexSeed directed undirected face
-  -> DirectedEdgeId
-  -> ExactEdgeKey
-seedDirectedEndpoints triangulation edge =
-  ( seedExactPoint (vertexData triangulation (Dcel.origin triangulation edge))
-  , seedExactPoint (vertexData triangulation (Dcel.destination triangulation edge))
-  )
-
-swapTransition :: (label, label) -> (label, label)
-swapTransition (fromLabel, toLabel) = (toLabel, fromLabel)
-
-descendFaceTransitions
-  :: (Eq leftLabel, Eq rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId (leftLabel, rightLabel)
-  -> Seq.Seq FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel))
-descendFaceTransitions triangulation atomicByKey labelled queued =
-  case Seq.viewl queued of
-    Seq.EmptyL -> Right labelled
-    face Seq.:< remaining ->
-      case Map.lookup face labelled of
-        Nothing ->
-          Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
-        Just current -> do
-          (nextLabels, nextQueue) <-
-            foldM
-              (descendFaceTransition triangulation atomicByKey face current)
-              (labelled, remaining)
-              (faceDirectedEdges triangulation face)
-          descendFaceTransitions triangulation atomicByKey nextLabels nextQueue
-
-descendFaceTransition
-  :: (Eq leftLabel, Eq rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> FaceId
-  -> (leftLabel, rightLabel)
-  -> (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
-  -> DirectedEdgeId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId (leftLabel, rightLabel), Seq.Seq FaceId)
-descendFaceTransition triangulation atomicByKey source current (labelled, queued) directed = do
-  let edge = asUndirected directed
-      destinationFace = incidentFace triangulation (reverseEdge directed)
-  (leftTransition, rightTransition) <-
-    case Map.lookup (residentSeedEdgeKey triangulation edge) atomicByKey of
-      Nothing -> Right (Nothing, Nothing)
-      Just atomic -> orientedTransitions edge directed atomic
-  let
-      expected =
-        ( maybe (fst current) fst leftTransition
-        , maybe (snd current) fst rightTransition
-        )
-      derived =
-        ( maybe (fst current) snd leftTransition
-        , maybe (snd current) snd rightTransition
-        )
-  if current == expected
-    then
-      case Map.lookup destinationFace labelled of
-        Nothing ->
-          Right
-            ( Map.insert destinationFace derived labelled
-            , queued Seq.|> destinationFace
-            )
-        Just existing
-          | existing == derived -> Right (labelled, queued)
-          | otherwise ->
-              Left
-                ( OverlayArrangementInvalid
-                    ( OverlayResidentFaceLabelConflict
-                        destinationFace
-                        edge
-                        existing
-                        derived
-                    )
-                )
-    else
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayTransitionSourceMismatch source edge current expected)
-        )
- where
-  orientedTransitions edge candidateDirection atomic
-    | directedEndpoints == (atomicEdgeFrom atomic, atomicEdgeTo atomic) =
-        Right
-          ( atomicEdgeLeftTransition atomic
-          , atomicEdgeRightTransition atomic
-          )
-    | directedEndpoints == (atomicEdgeTo atomic, atomicEdgeFrom atomic) =
-        Right
-          ( swapTransition <$> atomicEdgeLeftTransition atomic
-          , swapTransition <$> atomicEdgeRightTransition atomic
-          )
-    | otherwise =
-        Left
-          ( OverlayProvenanceIncomplete
-              ( OverlayAtomicConstraintOrientationMismatch
-                  edge
-                  (atomicEdgeFrom atomic)
-                  (atomicEdgeTo atomic)
-              )
-          )
-   where
-    directedEndpoints = seedDirectedEndpoints triangulation candidateDirection
-
-exactFaceDimensions
-  :: Set ExactPoint
-  -> Triangulation 'Constrained OverlayVertexSeed directed undirected face
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Set FaceId, Set FaceId)
-exactFaceDimensions inexactEmbeddingPoints triangulation = do
-  orientations <-
-    traverse
-      ( \face ->
-          (face,)
-            <$> exactFaceOrientation
-              inexactEmbeddingPoints
-              triangulation
-              face
-      )
-      (innerFaces triangulation)
-  case [face | (face, LT) <- orientations] of
-    reversed : _ ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayResidentFaceOrientationReversed reversed)
-        )
-    [] ->
-      Right
-        ( Set.fromList [face | (face, GT) <- orientations]
-        , Set.fromList [face | (face, EQ) <- orientations]
-        )
-
-exactFaceOrientation
-  :: Set ExactPoint
-  -> Triangulation mode OverlayVertexSeed directed undirected face
-  -> FaceId
-  -> Either (OverlayError leftLabel rightLabel) Ordering
-exactFaceOrientation inexactEmbeddingPoints triangulation face =
-  case
-    map
-      (seedExactPoint . vertexData triangulation)
-      (faceVertices triangulation face) of
-    points@[firstPoint, secondPoint, thirdPoint]
-      | any (`Set.member` inexactEmbeddingPoints) points ->
-          Right (exactOrient2d firstPoint secondPoint thirdPoint)
-      | otherwise -> Right GT
-    vertices ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayResidentFaceArity face (length vertices))
-        )
-
-residentComponentDrafts
-  :: forall leftLabel rightLabel.
-     (Ord leftLabel, Ord rightLabel)
-  => Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map FaceId (leftLabel, rightLabel)
-  -> Set FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      [ComponentDraft leftLabel rightLabel]
-residentComponentDrafts triangulation labels fullDimensionalFaces =
-  traverse
-    convert
-    (faceComponentsBy triangulation labelFullDimensionalFace (const True))
- where
-  exactPointAt :: VertexId -> Either RegionPublicationError ExactPoint
-  exactPointAt vertex =
-    Right (seedExactPoint (vertexData triangulation vertex))
-  labelFullDimensionalFace :: FaceId -> Maybe (leftLabel, rightLabel)
-  labelFullDimensionalFace face
-    | Set.member face fullDimensionalFaces = Map.lookup face labels
-    | otherwise = Nothing
-  convert
-    :: ((leftLabel, rightLabel), FaceComponent)
-    -> Either
-        (OverlayError leftLabel rightLabel)
-        (ComponentDraft leftLabel rightLabel)
-  convert (componentLabels, component) = do
-    boundary <-
-      first
-        (OverlayRegionPublicationFailed . RegionBoundaryObstruction)
-        (componentBoundary triangulation component)
-    polygon <-
-      first OverlayRegionPublicationFailed
-        (polygonComponentFromBoundaryCoordinates exactPointAt boundary)
-    pure
-      ComponentDraft
-        { componentDraftLabels = componentLabels
-        , componentDraftFaces = component
-        , componentDraftPolygon = polygon
-        , componentDraftTouchesOuter = touchesOuter triangulation component
-        }
-
-touchesOuter
-  :: Triangulation mode vertex directed undirected face
-  -> FaceComponent
-  -> Bool
-touchesOuter triangulation component =
-  any
-    ( any ((== outerFace) . incidentFace triangulation . reverseEdge)
-        . faceDirectedEdges triangulation
-    )
-    (faceComponentFaces component)
-
-partitionDrafts
-  :: (Eq leftLabel, Eq rightLabel)
-  => (leftLabel, rightLabel)
-  -> [ComponentDraft leftLabel rightLabel]
-  -> ([ComponentDraft leftLabel rightLabel], [ComponentDraft leftLabel rightLabel])
-partitionDrafts outsidePair =
-  partition
-    (\draftComponent ->
-       componentDraftTouchesOuter draftComponent
-         && componentDraftLabels draftComponent == outsidePair)
-
-sortComponentDrafts
-  :: (Ord leftLabel, Ord rightLabel)
-  => [ComponentDraft leftLabel rightLabel]
-  -> [ComponentDraft leftLabel rightLabel]
-sortComponentDrafts =
-  sortBy
-    (\left right ->
-       compare
-         (componentDraftPolygon left, componentDraftLabels left)
-         (componentDraftPolygon right, componentDraftLabels right))
-
-numberBoundedComponents
-  :: (Ord leftLabel, Ord rightLabel)
-  => [ComponentDraft leftLabel rightLabel]
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      [(OverlayCellId, ComponentDraft leftLabel rightLabel)]
-numberBoundedComponents drafts =
-  case
-    [ componentDraftPolygon left
-    | (left, right) <- consecutivePairs ordered
-    , componentSignature left == componentSignature right
-    ] of
-    duplicate : _ ->
-      Left
-        ( OverlayArrangementInvalid
-            (OverlayDuplicateCellSignature duplicate)
-        )
-    [] ->
-      Right
-        ( zipWith
-            (\index component -> (OverlayCellId index, component))
-            [1 ..]
-            ordered
-        )
- where
-  ordered = sortComponentDrafts drafts
-  componentSignature
-    :: ComponentDraft leftLabel' rightLabel'
-    -> (PolygonComponent, (leftLabel', rightLabel'))
-  componentSignature component =
-    (componentDraftPolygon component, componentDraftLabels component)
-
--- | A binary64 triangle whose authoritative vertices are collinear is not an
--- exact two-cell. It may remain in the resident triangulation only when its
--- representation diagonals descend uniquely to one full-dimensional exact
--- cell; this prevents the artifact from gluing exact components through a
--- zero-area wedge.
-descendCollapsedFaces
-  :: forall leftLabel rightLabel.
-     Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId OverlayCellId
-  -> Set FaceId
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Map FaceId OverlayCellId)
-descendCollapsedFaces triangulation atomicByKey fullCellIds collapsedFaces =
-  Map.fromList . concat <$> traverse resolveComponent collapsedComponents
- where
-  exactCellIds = Map.insert outerFace (OverlayCellId 0) fullCellIds
-  collapsedComponents =
-    fmap snd
-      ( faceComponentsBy
-          triangulation
-          labelCollapsedFace
-          isRepresentationDiagonal
-      )
-
-  labelCollapsedFace face
-    | Set.member face collapsedFaces = Just ()
-    | otherwise = Nothing
-
-  isRepresentationDiagonal edge =
-    Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey
-
-  resolveComponent component =
-    case faceComponentFaces component of
-      [] ->
-        Left
-          ( OverlayArrangementInvalid
-              OverlayFaceComponentEmpty
-          )
-      componentFaces@(firstFace : remainingFaces) -> do
-        let componentWitness = firstFace NonEmpty.:| remainingFaces
-            adjacentCellIds =
-              Set.fromList
-                [ cellId
-                | face <- componentFaces
-                , directed <- faceDirectedEdges triangulation face
-                , isRepresentationDiagonal (asUndirected directed)
-                , Just cellId <-
-                    [Map.lookup (incidentFace triangulation (reverseEdge directed)) exactCellIds]
-                ]
-        cellId <-
-          case Set.toAscList adjacentCellIds of
-            [singleCellId] -> Right singleCellId
-            [] ->
-              Left
-                ( OverlayArrangementInvalid
-                    (OverlayCollapsedFacesUnowned componentWitness)
-                )
-            firstCellId : remainingCellIds ->
-              Left
-                ( OverlayArrangementInvalid
-                    ( OverlayCollapsedFacesAmbiguous
-                        componentWitness
-                        (firstCellId NonEmpty.:| remainingCellIds)
-                    )
-                )
-        pure (fmap (,cellId) componentFaces)
-
--- | Trace the finite boundary cycles of the unbounded exact cell from atomic
--- edges only. Resident hull edges and Delaunay diagonals are absent by
--- construction; the DCEL contributes only the already-proved incident-cell
--- gluing needed to orient each atomic edge with cell zero on its left.
-unboundedCellBoundaryLoops
-  :: Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Map FaceId OverlayCellId
-  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
-unboundedCellBoundaryLoops triangulation atomicByKey cellIdByFace = do
-  orientedEdges <-
-    Set.fromList . concat
-      <$> traverse orientAtomicEdge (undirectedEdges triangulation)
-  traceBoundaryCycles orientedEdges
- where
-  orientAtomicEdge edge
-    | Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey = Right []
-    | otherwise = do
-        let (forward, backward) = directedPair edge
-        forwardCell <- faceCellId (incidentFace triangulation forward)
-        backwardCell <- faceCellId (incidentFace triangulation backward)
-        pure
-          ( case (forwardCell == OverlayCellId 0, backwardCell == OverlayCellId 0) of
-              (True, False) -> [seedDirectedEndpoints triangulation forward]
-              (False, True) -> [seedDirectedEndpoints triangulation backward]
-              _ -> []
-          )
-  faceCellId face
-    | face == outerFace = Right (OverlayCellId 0)
-    | otherwise =
-        case Map.lookup face cellIdByFace of
-          Just cellId -> Right cellId
-          Nothing -> Left (OverlayProvenanceIncomplete (OverlayResidentFaceUnassigned face))
-
-traceBoundaryCycles
-  :: Set ExactEdgeKey
-  -> Either (OverlayError leftLabel rightLabel) [ExactLoop]
-traceBoundaryCycles orientedEdges = do
-  pointCycles <-
-    traceOrientedBoundaryCircuits
-      fst
-      snd
-      (\seed _ ->
-         OverlayArrangementInvalid
-           (uncurry OverlayCellCycleDidNotClose seed))
-      outgoing
-      orientedEdges
-  sort
-    <$> traverse
-      ( first
-          (OverlayRegionPublicationFailed . RegionValidationObstruction)
-          . exactLoop
-      )
-      pointCycles
- where
-  outgoing =
-    Map.mapWithKey orderAroundOrigin
-      ( Set.foldl'
-          (\byOrigin edge@(from, _) -> Map.insertWith (<>) from [edge] byOrigin)
-          Map.empty
-          orientedEdges
-      )
-  orderAroundOrigin :: ExactPoint -> [ExactEdgeKey] -> [ExactEdgeKey]
-  orderAroundOrigin origin =
-    sortBy (\(_, left) (_, right) -> compareAround origin left right)
-
-attachFaceCells
-  :: forall leftLabel rightLabel.
-     Set FaceId
-  -> Map FaceId OverlayCellId
-  -> Triangulation 'Constrained OverlayVertexSeed () () ()
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (Triangulation 'Constrained OverlayVertexSeed () () OverlayFace)
-attachFaceCells collapsedFaces cellIdByFace triangulation = do
-  payloads <- V.fromList <$> traverse facePayload (allFaces triangulation)
-  let outerPayload = OverlayCellFace (OverlayCellId 0)
-      defaults = triElementDefaults triangulation
-  pure
-    triangulation
-      { triFaceData = boxedFromVector (Just outerPayload) payloads
-      , triElementDefaults = defaults{defaultFaceData = outerPayload}
-      }
- where
-  facePayload
-    :: FaceId
-    -> Either (OverlayError leftLabel rightLabel) OverlayFace
-  facePayload face
-    | face == outerFace = Right (OverlayCellFace (OverlayCellId 0))
-    | otherwise =
-        case Map.lookup face cellIdByFace of
-          Just cellId
-            | Set.member face collapsedFaces ->
-                Right (OverlayCollapsedFace cellId)
-            | otherwise -> Right (OverlayCellFace cellId)
-          Nothing ->
-            Left
-              ( OverlayProvenanceIncomplete
-                  (OverlayResidentFaceUnassigned face)
-              )
-
-attachOverlayVertices
-  :: Triangulation 'Constrained OverlayVertexSeed () () face
-  -> Triangulation 'Constrained OverlayVertex () () face
-attachOverlayVertices =
-  Dcel.mapVertices
-    (\seed -> OverlayVertex (seedExactPoint seed) (seedOrigin seed))
-
-cellPayload
-  :: V.Vector (OverlayCell leftLabel rightLabel)
-  -> OverlayCellId
-  -> Either OverlayCellWitness (OverlayCell leftLabel rightLabel)
-cellPayload cells cellId@(OverlayCellId index) =
-  case cells V.!? index of
-    Just cell -> Right cell
-    Nothing -> Left (OverlayCellPayloadMissing cellId)
-
-faceLabels
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either OverlayCellWitness (leftLabel, rightLabel)
-faceLabels result face = do
-  cell <-
-    cellPayload
-      (overlayResultCells result)
-      (overlayFaceCellId (faceData (overlayResultTriangulation result) face))
-  pure (overlayCellLeft cell, overlayCellRight cell)
-
--- | Whether a resident triangle has nonzero area under the authoritative
--- exact coordinate section. Binary64-only wedges are representation charts,
--- never exact two-cells.
-faceCarriesExactArea
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Bool
-faceCarriesExactArea result face =
-  case faceData (overlayResultTriangulation result) face of
-    OverlayCellFace {} -> True
-    OverlayCollapsedFace {} -> False
-
-regionFaceLabels
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either RegionPublicationError (leftLabel, rightLabel)
-regionFaceLabels result face =
-  case faceLabels result face of
-    Right labels -> Right labels
-    Left _ -> Left (RegionFaceLabelMissing face)
-
-vertexSupport
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> VertexId
-  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
-vertexSupport result vertex = do
-  labels <-
-    traverse
-      (faceLabels result . incidentFace triangulation)
-      (vertexOutgoingEdges triangulation vertex)
-  case supportFromPairs labels of
-    Just support -> Right support
-    Nothing -> Left (OverlayVertexSupportMissing vertex)
- where
-  triangulation = overlayResultTriangulation result
-
-edgeSupport
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> UndirectedEdgeId
-  -> Either OverlayCellWitness (OverlayCellSupport leftLabel rightLabel)
-edgeSupport result edge = do
-  pairs <- traverse (faceLabels result . incidentFace triangulation) [forward, backward]
-  case supportFromPairs pairs of
-    Just support -> Right support
-    Nothing -> Left (OverlayEdgeSupportMissing edge)
- where
-  triangulation = overlayResultTriangulation result
-  (forward, backward) = directedPair edge
-
-supportFromPairs
-  :: (Ord leftLabel, Ord rightLabel)
-  => [(leftLabel, rightLabel)]
-  -> Maybe (OverlayCellSupport leftLabel rightLabel)
-supportFromPairs pairs = do
-  let (leftLabels, rightLabels) =
-        List.foldl'
-          (\(left, right) (leftLabel, rightLabel) ->
-             (Set.insert leftLabel left, Set.insert rightLabel right))
-          (Set.empty, Set.empty)
-          pairs
-  leftSupport <- nonEmptySupport leftLabels
-  rightSupport <- nonEmptySupport rightLabels
-  pure (OverlayCellSupport leftSupport rightSupport)
-
-nonEmptySupport :: Set label -> Maybe (OverlaySupport label)
-nonEmptySupport labels = OverlaySupport <$> NonEmpty.nonEmpty (Set.toAscList labels)
-
-validateAtomicConstraints
-  :: Triangulation
-      'Constrained
-      OverlayVertexSeed
-      ()
-      ()
-      ()
-  -> Map ExactEdgeKey (AtomicEdge leftLabel rightLabel)
-  -> Either (OverlayError leftLabel rightLabel) ()
-validateAtomicConstraints triangulation atomicByKey = do
-  traverse_ requireAtomic (Map.keys atomicByKey)
-  traverse_ requireExpectedConstraint (undirectedEdges triangulation)
- where
-  residentByKey =
-    Map.fromList
-      [ (residentSeedEdgeKey triangulation edge, edge)
-      | edge <- undirectedEdges triangulation
-      ]
-  requireAtomic key@(from, to) =
-    case Map.lookup key residentByKey of
-      Nothing -> Left (OverlayProvenanceIncomplete (OverlayAtomicConstraintMissing from to))
-      Just edge
-        | isConstraintEdge triangulation edge -> Right ()
-        | otherwise -> Left (OverlayProvenanceIncomplete (OverlayBoundaryEdgeNotConstrained edge))
-  requireExpectedConstraint edge
-    | isConstraintEdge triangulation edge
-        && Map.notMember (residentSeedEdgeKey triangulation edge) atomicByKey =
-        Left (OverlayProvenanceIncomplete (OverlayUnexpectedConstraint edge))
-    | otherwise = Right ()
-
-residentSeedEdgeKey
-  :: Triangulation mode OverlayVertexSeed directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentSeedEdgeKey = residentEdgeKeyBy seedExactPoint
-
-residentEdgeKey
-  :: Triangulation mode OverlayVertex directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentEdgeKey = residentEdgeKeyBy overlayExactPoint
-
-residentEdgeKeyBy
-  :: (vertex -> ExactPoint)
-  -> Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> ExactEdgeKey
-residentEdgeKeyBy exactPointAt triangulation edge =
-  let (from, to) = undirectedEndpoints triangulation edge
-   in canonicalEdgeKey
-        (exactPointAt (vertexData triangulation from))
-        (exactPointAt (vertexData triangulation to))
diff --git a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Types.hs b/src-overlay/Moonlight/Triangulation/Internal/Overlay/Types.hs
deleted file mode 100644
--- a/src-overlay/Moonlight/Triangulation/Internal/Overlay/Types.hs
+++ /dev/null
@@ -1,274 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE KindSignatures #-}
-
--- | Closed vocabulary and opaque carrier representation for exact labelled
--- overlay. Construction lives in the public build-tier module; this module
--- exists so every invariant-bearing payload shares one owner.
-module Moonlight.Triangulation.Internal.Overlay.Types
-  ( BoundaryLoopRef (..)
-  , OverlayOperand (..)
-  , BoundaryRef (..)
-  , BoundaryVertexRef
-  , BoundaryEdgeRef
-  , OverlayVertexOrigin (..)
-  , OverlayEdgeOrigin (..)
-  , OverlaySupport (..)
-  , overlaySupportLabels
-  , OverlayCellSupport (..)
-  , OverlayCellId (..)
-  , OverlayCellGeometry (..)
-  , OverlayCell (..)
-  , OverlayFace (..)
-  , OverlayVertex (..)
-  , OverlayEdge (..)
-  , OverlayReceipt (..)
-  , OverlayArrangementObstruction (..)
-  , OverlayCellWitness (..)
-  , OverlayError (..)
-  , OverlayResult (..)
-  , OverlaySelectionKind (..)
-  , OverlaySelectionError (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.List.NonEmpty (NonEmpty)
-import Data.Vector (Vector)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.CellSet (CellSelectionError)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId
-  , UndirectedEdgeId
-  , VertexId
-  )
-import Moonlight.Triangulation.Internal.Cdt.Types (CdtError)
-import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
-import Moonlight.Triangulation.Internal.ExactSegmentEvents (ExactSegmentEventObstruction)
-import Moonlight.Triangulation.Embedding (OverlayEmbeddingObstruction)
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , PolygonComponent
-  , RegionPublicationError
-  )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  )
-
--- | Which cycle inside a polygon component supplied a source reference.
-data BoundaryLoopRef
-  = BoundaryOuterLoop
-  | BoundaryHoleLoop !Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayOperand
-  = LeftOverlayOperand
-  | RightOverlayOperand
-
--- | Phantom source cell kind; both provenance axes share one physical payload.
-data BoundaryFeature = BoundaryVertexFeature | BoundaryEdgeFeature
-
-data BoundaryRef (feature :: BoundaryFeature) (operand :: OverlayOperand) = BoundaryRef
-  { boundaryRefComponent :: !Int
-  , boundaryRefLoop :: !BoundaryLoopRef
-  , boundaryRefLocalIndex :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-type BoundaryVertexRef operand = BoundaryRef 'BoundaryVertexFeature operand
-type BoundaryEdgeRef operand = BoundaryRef 'BoundaryEdgeFeature operand
-
--- | Complete typed source closure of one arrangement vertex.
-data OverlayVertexOrigin = OverlayVertexOrigin
-  { overlayOriginLeftVertices :: ![BoundaryVertexRef 'LeftOverlayOperand]
-  , overlayOriginRightVertices :: ![BoundaryVertexRef 'RightOverlayOperand]
-  , overlayOriginLeftEdges :: ![BoundaryEdgeRef 'LeftOverlayOperand]
-  , overlayOriginRightEdges :: ![BoundaryEdgeRef 'RightOverlayOperand]
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Nonempty typed source-edge provenance of one exact atomic interval.
-data OverlayEdgeOrigin = OverlayEdgeOrigin
-  { overlayEdgeLeftSources :: ![BoundaryEdgeRef 'LeftOverlayOperand]
-  , overlayEdgeRightSources :: ![BoundaryEdgeRef 'RightOverlayOperand]
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Sorted nonempty labels whose closures contain one relatively open cell.
-newtype OverlaySupport label = OverlaySupport (NonEmpty label)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-overlaySupportLabels :: OverlaySupport label -> NonEmpty label
-overlaySupportLabels (OverlaySupport labels) = labels
-
-data OverlayCellSupport leftLabel rightLabel = OverlayCellSupport
-  { overlaySupportLeft :: !(OverlaySupport leftLabel)
-  , overlaySupportRight :: !(OverlaySupport rightLabel)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-newtype OverlayCellId = OverlayCellId Int
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCellGeometry
-  = BoundedOverlayCell !PolygonComponent
-  | UnboundedOverlayCell ![ExactLoop]
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCell leftLabel rightLabel = OverlayCell
-  { overlayCellLeft :: !leftLabel
-  , overlayCellRight :: !rightLabel
-  , overlayCellGeometry :: !OverlayCellGeometry
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A resident triangle either realizes an exact two-cell or collapses under
--- the authoritative exact coordinates. A collapsed triangle descends to the
--- unique adjacent exact cell through representation diagonals, but exact
--- region operations must not mistake its binary64 area for exact area.
-data OverlayFace
-  = OverlayCellFace
-      { overlayFaceCellId :: !OverlayCellId
-      }
-  | OverlayCollapsedFace
-      { overlayFaceCellId :: !OverlayCellId
-      }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact geometry and provenance are intrinsic to a vertex. Label support is
--- the finite union of incident cell descriptors and is therefore a view.
-data OverlayVertex = OverlayVertex
-  { overlayExactPoint :: !ExactPoint
-  , overlayVertexOrigin :: !OverlayVertexOrigin
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Boundary provenance is intrinsic; label support is derived from the two
--- incident face-cell references.
-data OverlayEdge
-  = OverlayBoundary !OverlayEdgeOrigin
-  | OverlayDiagonal
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayReceipt = OverlayReceipt
-  { overlayInputSegments :: !Int
-  , overlayRelationEvents :: !Int
-  , overlayExactCrossings :: !Int
-  , overlayOverlapIntervals :: !Int
-  , overlayAtomicEdges :: !Int
-  , overlayOutputVertices :: !Int
-  , overlayArrangementCells :: !Int
-  , overlayResidentFaces :: !Int
-  , overlayEmbeddingCandidates :: !Int
-  , overlayTotalRelationChecks :: !Int
-  , overlaySweepMaximumHeight :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayArrangementObstruction leftLabel rightLabel
-  = OverlayLeftSourceSideConflict
-      !OverlayEdgeOrigin
-      !(NonEmpty leftLabel)
-  | OverlayRightSourceSideConflict
-      !OverlayEdgeOrigin
-      !(NonEmpty rightLabel)
-  | OverlayRotationDegenerate !ExactPoint
-  | OverlayFaceComponentEmpty
-  | OverlayCellCycleDidNotClose !ExactPoint !ExactPoint
-  | OverlayTransitionSourceMismatch
-      !FaceId
-      !UndirectedEdgeId
-      !(leftLabel, rightLabel)
-      !(leftLabel, rightLabel)
-  | OverlayResidentFaceLabelConflict
-      !FaceId
-      !UndirectedEdgeId
-      !(leftLabel, rightLabel)
-      !(leftLabel, rightLabel)
-  | OverlayResidentFaceArity !FaceId !Int
-  | OverlayResidentFaceOrientationReversed !FaceId
-  | OverlayCollapsedFacesUnowned !(NonEmpty FaceId)
-  | OverlayCollapsedFacesAmbiguous
-      !(NonEmpty FaceId)
-      !(NonEmpty OverlayCellId)
-  | OverlayDuplicateCellSignature !PolygonComponent
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayCellWitness
-  = OverlayAtomicConstraintMissing !ExactPoint !ExactPoint
-  | OverlayAtomicConstraintOrientationMismatch
-      !UndirectedEdgeId
-      !ExactPoint
-      !ExactPoint
-  | OverlayUnexpectedConstraint !UndirectedEdgeId
-  | OverlayBoundaryEdgeNotConstrained !UndirectedEdgeId
-  | OverlayResidentFaceUnassigned !FaceId
-  | OverlayCellPayloadMissing !OverlayCellId
-  | OverlayVertexSupportMissing !VertexId
-  | OverlayEdgeSupportMissing !UndirectedEdgeId
-  | OverlayEmbeddedVertexCountMismatch !Int !Int
-  | OverlayExactVertexMissing !ExactPoint
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlayError leftLabel rightLabel
-  = OverlayExactArithmetic !ExactArithmeticError
-  | OverlaySegmentEventsInvalid !ExactSegmentEventObstruction
-  | OverlayArrangementInvalid !(OverlayArrangementObstruction leftLabel rightLabel)
-  | OverlayEmbeddingRefused !(NonEmpty OverlayEmbeddingObstruction)
-  | OverlayBuildFailed !CdtError
-  | OverlayRegionPublicationFailed !RegionPublicationError
-  | OverlayProvenanceIncomplete !OverlayCellWitness
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The singular exact subdivision carrier. Its embedded DCEL is a derived
--- binary64 realization; exact coordinates remain in the vertex payload plane.
-data OverlayResult leftLabel rightLabel = OverlayResult
-  { overlayResultTriangulation
-      :: !( Triangulation
-              'Constrained
-              OverlayVertex
-              ()
-              OverlayEdge
-              OverlayFace
-          )
-  , overlayResultCells :: !(Vector (OverlayCell leftLabel rightLabel))
-  , overlayResultOutsideLabels :: !(leftLabel, rightLabel)
-  , overlayResultReceipt :: !OverlayReceipt
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-data OverlaySelectionKind
-  = ClosedUnionSelection
-  | ClosedIntersectionSelection
-  | RegularizedDifferenceSelection
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data OverlaySelectionError
-  = OverlaySelectionContainsUnboundedCell !OverlaySelectionKind
-  | OverlaySelectionProvenance !OverlayCellWitness
-  | OverlaySelectionInvalid !CellSelectionError
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-parallel/Moonlight/Planar/Parallel.hs b/src-parallel/Moonlight/Planar/Parallel.hs
new file mode 100644
--- /dev/null
+++ b/src-parallel/Moonlight/Planar/Parallel.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+
+-- | Bounded concurrent interpretation of the canonical join tournament. This
+-- layer owns effects only: pairing and pair-schedule semantics remain in the
+-- pure build planner.
+module Moonlight.Planar.Parallel
+  ( unionsConcurrently
+  ) where
+
+import Control.Concurrent.Async (concurrently)
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import Data.List.NonEmpty (NonEmpty)
+import Moonlight.Planar.Internal.Join
+  ( joinNormalForm )
+import Moonlight.Planar.Internal.Representation (Triangulation)
+import Moonlight.Planar.Internal.Tournament
+  ( TournamentPlan (..)
+  , planTournament
+  )
+import Moonlight.Planar.Internal.Types (BuildError, ConstraintMode (Unconstrained))
+import Moonlight.Algebra.Pure.Join (JoinSemilattice)
+
+-- | Execute the same deterministic tournament as sequential @unions@, with
+-- no more than the requested number of leaf computations live at once. A
+-- non-positive request means one worker; requesting more workers than leaves
+-- is harmless. Each child is fully evaluated before its parent becomes
+-- runnable, so thunks do not smuggle unbounded work across the boundary.
+unionsConcurrently
+  :: (Eq annotation, JoinSemilattice annotation, NFData annotation)
+  => Int
+  -> NonEmpty (Triangulation 'Unconstrained annotation () () ())
+  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
+unionsConcurrently requestedWorkers operands =
+  executeTournamentConcurrently
+    (max 1 requestedWorkers)
+    (planTournament operands)
+
+executeTournamentConcurrently
+  :: (Eq annotation, JoinSemilattice annotation, NFData annotation)
+  => Int
+  -> TournamentPlan (Triangulation 'Unconstrained annotation () () ())
+  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
+executeTournamentConcurrently workers tournament =
+  case tournament of
+    TournamentLeaf mesh -> Right <$> evaluate (force mesh)
+    TournamentNode left right
+      | workers <= 1 ->
+            publishUnion
+            =<< ((,)
+                  <$> executeTournamentConcurrently 1 left
+                  <*> executeTournamentConcurrently 1 right
+                )
+      | otherwise -> do
+          let !leftLeaves = tournamentLeaves left
+              !rightLeaves = tournamentLeaves right
+              !available = min workers (leftLeaves + rightLeaves)
+              !leftWorkers =
+                max 1 (min leftLeaves (available * leftLeaves `quot` (leftLeaves + rightLeaves)))
+              !rightWorkers = max 1 (available - leftWorkers)
+          concurrently
+            (executeTournamentConcurrently leftWorkers left)
+            (executeTournamentConcurrently rightWorkers right)
+            >>= publishUnion
+
+publishUnion
+  :: (Eq annotation, JoinSemilattice annotation, NFData annotation)
+  => ( Either BuildError (Triangulation 'Unconstrained annotation () () ())
+     , Either BuildError (Triangulation 'Unconstrained annotation () () ())
+     )
+  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
+publishUnion (Left failure, _) = pure (Left failure)
+publishUnion (Right _, Left failure) = pure (Left failure)
+publishUnion (Right leftMesh, Right rightMesh) =
+  evaluate (force (joinNormalForm leftMesh rightMesh))
+
+tournamentLeaves :: TournamentPlan mesh -> Int
+tournamentLeaves tournament =
+  case tournament of
+    TournamentLeaf _ -> 1
+    TournamentNode left right -> tournamentLeaves left + tournamentLeaves right
+{-# INLINE tournamentLeaves #-}
diff --git a/src-parallel/Moonlight/Triangulation/Parallel.hs b/src-parallel/Moonlight/Triangulation/Parallel.hs
deleted file mode 100644
--- a/src-parallel/Moonlight/Triangulation/Parallel.hs
+++ /dev/null
@@ -1,83 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-
--- | Bounded concurrent interpretation of the canonical join tournament. This
--- layer owns effects only: pairing and pair-schedule semantics remain in the
--- pure build planner.
-module Moonlight.Triangulation.Parallel
-  ( unionsConcurrently
-  ) where
-
-import Control.Concurrent.Async (concurrently)
-import Control.DeepSeq (NFData, force)
-import Control.Exception (evaluate)
-import Data.List.NonEmpty (NonEmpty)
-import Moonlight.Triangulation.Internal.Join
-  ( joinNormalForm )
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Tournament
-  ( TournamentPlan (..)
-  , planTournament
-  )
-import Moonlight.Triangulation.Internal.Types (BuildError, ConstraintMode (Unconstrained))
-import Moonlight.Triangulation.JoinSemilattice (JoinSemilattice)
-
--- | Execute the same deterministic tournament as sequential @unions@, with
--- no more than the requested number of leaf computations live at once. A
--- non-positive request means one worker; requesting more workers than leaves
--- is harmless. Each child is fully evaluated before its parent becomes
--- runnable, so thunks do not smuggle unbounded work across the boundary.
-unionsConcurrently
-  :: (JoinSemilattice annotation, NFData annotation)
-  => Int
-  -> NonEmpty (Triangulation 'Unconstrained annotation () () ())
-  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
-unionsConcurrently requestedWorkers operands =
-  executeTournamentConcurrently
-    (max 1 requestedWorkers)
-    (planTournament operands)
-
-executeTournamentConcurrently
-  :: (JoinSemilattice annotation, NFData annotation)
-  => Int
-  -> TournamentPlan (Triangulation 'Unconstrained annotation () () ())
-  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
-executeTournamentConcurrently workers tournament =
-  case tournament of
-    TournamentLeaf mesh -> Right <$> evaluate (force mesh)
-    TournamentNode left right
-      | workers <= 1 ->
-            publishUnion
-            =<< ((,)
-                  <$> executeTournamentConcurrently 1 left
-                  <*> executeTournamentConcurrently 1 right
-                )
-      | otherwise -> do
-          let !leftLeaves = tournamentLeaves left
-              !rightLeaves = tournamentLeaves right
-              !available = min workers (leftLeaves + rightLeaves)
-              !leftWorkers =
-                max 1 (min leftLeaves (available * leftLeaves `quot` (leftLeaves + rightLeaves)))
-              !rightWorkers = max 1 (available - leftWorkers)
-          concurrently
-            (executeTournamentConcurrently leftWorkers left)
-            (executeTournamentConcurrently rightWorkers right)
-            >>= publishUnion
-
-publishUnion
-  :: (JoinSemilattice annotation, NFData annotation)
-  => ( Either BuildError (Triangulation 'Unconstrained annotation () () ())
-     , Either BuildError (Triangulation 'Unconstrained annotation () () ())
-     )
-  -> IO (Either BuildError (Triangulation 'Unconstrained annotation () () ()))
-publishUnion (Left failure, _) = pure (Left failure)
-publishUnion (Right _, Left failure) = pure (Left failure)
-publishUnion (Right leftMesh, Right rightMesh) =
-  evaluate (force (joinNormalForm leftMesh rightMesh))
-
-tournamentLeaves :: TournamentPlan mesh -> Int
-tournamentLeaves tournament =
-  case tournament of
-    TournamentLeaf _ -> 1
-    TournamentNode left right -> tournamentLeaves left + tournamentLeaves right
-{-# INLINE tournamentLeaves #-}
diff --git a/src-public/Moonlight/Hex/Planar.hs b/src-public/Moonlight/Hex/Planar.hs
--- a/src-public/Moonlight/Hex/Planar.hs
+++ b/src-public/Moonlight/Hex/Planar.hs
@@ -7,21 +7,28 @@
   ( HexPlanarObstruction (..)
   , hexCellExactLoop
   , hexRegionPlanarRegion
+  , hexRegionByCenterInPlanarRegion
+  , hexRegionFullyCoveredByPlanarRegion
+  , hexRegionIntersectingPlanarRegion
   ) where
 
 import Control.DeepSeq (NFData)
+import Control.Monad ((<=<))
 import Data.Bifunctor (first)
 import Data.Either (isRight)
 import Data.Foldable (foldlM)
+import Data.List qualified as List
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Map.Strict (Map)
 import Data.Map.Strict qualified as Map
+import Data.Maybe (mapMaybe)
+import Data.Ratio ((%))
 import Data.Set (Set)
 import Data.Set qualified as Set
 import GHC.Generics (Generic)
 import Moonlight.Hex.Coordinate
-  ( HexCoord
+  ( HexCoord (..)
   , HexDirection
   , allHexDirections
   , hexStepCoord
@@ -35,19 +42,43 @@
   , hexVertexCoordinates
   )
 import Moonlight.Hex.Region
-  ( HexRegion
+  ( HexLayout
+  , HexRegion
+  , HexRowSpan
   , foldHexRegionCoords
+  , hexLayoutCellCount
+  , hexLayoutOrigin
+  , hexLayoutWidth
+  , hexRegionGenerate
+  , hexRegionGenerateRowSpans
   , hexRegionMember
+  , hexRowSpan
   )
-import Moonlight.Triangulation.Exact (ExactPoint, exactPoint)
-import Moonlight.Triangulation.Region
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , SegmentRelation (SegmentsDisjoint)
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , exactRationalDenominator
+  , exactRationalNumerator
+  , exactSegmentRelation
+  )
+import Moonlight.Planar.Region
   ( ExactLoop
   , PlanarRegion
+  , RegionPointLocation (RegionExterior)
   , RegionValidationError
   , emptyPlanarRegion
   , exactLoop
+  , exactLoopPoints
   , planarRegion
+  , planarRegionComponents
   , polygonComponent
+  , polygonHoleLoops
+  , polygonOuterLoop
+  , regionPointLocation
   )
 
 -- | Every typed obstruction in boundary descent and exact publication.
@@ -91,6 +122,308 @@
         fmap
           (\loop -> (area, loop))
           (first HexPlanarRegionInvalid (exactLoop (fmap exactVertexPoint vertices)))
+
+-- | Select cells whose exact centre belongs to the closed planar region.
+hexRegionByCenterInPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionByCenterInPlanarRegion layout region
+  | hexLayoutCellCount layout == 1 =
+      hexRegionGenerate layout ((/= RegionExterior) . regionPointLocation region . exactCellCentre)
+  | otherwise =
+      let segments = axialBoundarySegments (planarBoundary region)
+       in hexRegionGenerateRowSpans layout
+            (mapMaybe (uncurry hexRowSpan) . centreRowRanges segments)
+
+-- | Select cells whose entire closed hexagon is covered by the closed planar
+-- region. A closed convex cell lies in the region exactly when its centre
+-- does and no boundary segment of the region meets the cell's interior: the
+-- interior is connected, so once the boundary is known to miss it, it lies
+-- wholly on the centre's side. Boundary-only contact is therefore coverage,
+-- and no overlay or gap publication is involved.
+hexRegionFullyCoveredByPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionFullyCoveredByPlanarRegion layout region
+  | hexLayoutCellCount layout == 1 =
+      let boundary = planarBoundary region
+       in hexRegionGenerate layout (cellCoveredByRegion region (boundarySegments boundary))
+  | otherwise = restrictPlanarBoundary layout region (cellCoveredByRegion region . boundarySegments)
+
+-- | Select cells whose closed hexagon has any point in common with the closed
+-- planar region. The region boundary is prepared once as an ephemeral view;
+-- the admitted 'PlanarRegion' remains the sole geometry authority.
+hexRegionIntersectingPlanarRegion :: HexLayout -> PlanarRegion -> HexRegion
+hexRegionIntersectingPlanarRegion layout region =
+  restrictPlanarBoundary layout region (cellIntersectsRegion region)
+
+-- | Descent from whole centre runs away from the boundary, with the original
+-- detailed classifier retained on a conservative exact boundary supercover.
+-- A singleton context has no shared run work: it uses the original direct
+-- observation rather than constructing an event plan around one predicate.
+restrictPlanarBoundary
+  :: HexLayout
+  -> PlanarRegion
+  -> (PlanarBoundary -> HexCoord -> Bool)
+  -> HexRegion
+restrictPlanarBoundary layout region classify
+  | hexLayoutCellCount layout == 1 = hexRegionGenerate layout (classify boundary)
+  | otherwise = hexRegionGenerateRowSpans layout selectedSpans
+ where
+  boundary = planarBoundary region
+  segments = axialBoundarySegments boundary
+  firstQ = toInteger (hexQ (hexLayoutOrigin layout))
+  finalQ = firstQ + toInteger (hexLayoutWidth layout) - 1
+
+  selectedSpans :: Int -> [HexRowSpan]
+  selectedSpans row =
+    let boundaryRanges = boundaryRowRanges layout segments row
+        interiorRanges =
+          if boundaryRanges == [(firstQ, finalQ)]
+            then []
+            else subtractRowRanges (centreRowRanges segments row) boundaryRanges
+        selectedBoundary =
+          [ (q, q)
+          | (firstBoundaryQ, finalBoundaryQ) <- boundaryRanges
+          , q <- [firstBoundaryQ .. finalBoundaryQ]
+          , classify boundary (HexCoord (fromInteger q) row)
+          ]
+     in mapMaybe (uncurry hexRowSpan) (mergeRowRanges (interiorRanges <> selectedBoundary))
+{-# INLINE restrictPlanarBoundary #-}
+
+-- Query-local scalar observations of the existing exact boundary. The
+-- positive-determinant affine coordinates are (u,v)=(x,3*y-x), placing axial
+-- centres at (3*q,6*r). They author no second polygon or geometry inventory.
+type AxialBoundarySegment = ((Rational, Rational), (Rational, Rational))
+type RowRange = (Integer, Integer)
+
+axialBoundarySegments :: PlanarBoundary -> [AxialBoundarySegment]
+axialBoundarySegments = fmap axialSegment . boundarySegments
+ where
+  axialSegment :: (Extent, ExactPoint, ExactPoint) -> AxialBoundarySegment
+  axialSegment (_, from, to) = (axialPoint from, axialPoint to)
+  axialPoint :: ExactPoint -> (Rational, Rational)
+  axialPoint point =
+    let (x, y) = exactPointCoordinates point
+        rational :: ExactRational -> Rational
+        rational value = exactRationalNumerator value % exactRationalDenominator value
+        u = rational x
+     in (u, 3 * rational y - u)
+
+-- | Pair half-open crossings of admitted closed loops, then separately add
+-- closed boundary intersections and collinear spans. The former has even
+-- parity; the latter preserves tangencies and vertices on integer centres.
+centreRowRanges :: [AxialBoundarySegment] -> Int -> [RowRange]
+centreRowRanges segments row =
+  mergeRowRanges (mapMaybe quantizeRowRange (interiorSpans <> closedBoundarySpans))
+ where
+  rowV = fromInteger (6 * toInteger row)
+  crossings = List.sort
+    [ crossing
+    | segment@((_, fromV), (_, toV)) <- segments
+    , min fromV toV <= rowV && rowV < max fromV toV
+    , Just crossing <- [crossingAt rowV segment]
+    ]
+  interiorSpans =
+    [ pair
+    | (index, pair) <- zip [0 :: Int ..] (zip crossings (drop 1 crossings))
+    , even index
+    ]
+  closedBoundarySpans =
+    [ boundarySpan
+    | segment@((fromU, fromV), (toU, toV)) <- segments
+    , min fromV toV <= rowV && rowV <= max fromV toV
+    , boundarySpan <-
+        if fromV == toV
+          then [(min fromU toU, max fromU toU)]
+          else maybe [] (\crossing -> [(crossing, crossing)]) (crossingAt rowV segment)
+    ]
+
+-- A horizontal support has no transverse crossing. All arithmetic remains
+-- exact; neither row events nor their order are projected through binary64.
+crossingAt :: Rational -> AxialBoundarySegment -> Maybe Rational
+crossingAt rowV ((fromU, fromV), (toU, toV))
+  | fromV == toV = Nothing
+  | otherwise = Just (fromU + (rowV - fromV) * (toU - fromU) / (toV - fromV))
+
+quantizeRowRange :: (Rational, Rational) -> Maybe RowRange
+quantizeRowRange (firstU, finalU) =
+  let firstQ = ceiling (firstU / 3)
+      finalQ = floor (finalU / 3)
+   in if firstQ <= finalQ then Just (firstQ, finalQ) else Nothing
+
+-- | Every point of a hexagon lies in u +/- 2 and v +/- 4 about its centre.
+-- Clip a segment to the CLOSED v strip before expanding its u interval; this
+-- avoids the rectangular-area over-enumeration of an untrimmed diagonal.
+-- Endpoints and loops contained inside a cell are retained by construction.
+boundaryRowRanges :: HexLayout -> [AxialBoundarySegment] -> Int -> [RowRange]
+boundaryRowRanges layout segments row =
+  mergeRowRanges (mapMaybe (clipToLayout <=< quantizeRowRange <=< stripSpan) segments)
+ where
+  rowV = fromInteger (6 * toInteger row)
+  lowerV = rowV - 4
+  upperV = rowV + 4
+  minimumQ = toInteger (hexQ (hexLayoutOrigin layout))
+  maximumQ = minimumQ + toInteger (hexLayoutWidth layout) - 1
+
+  stripSpan :: AxialBoundarySegment -> Maybe (Rational, Rational)
+  stripSpan ((fromU, fromV), (toU, toV))
+    | max fromV toV < lowerV || min fromV toV > upperV = Nothing
+    | fromV == toV = Just (min fromU toU - 2, max fromU toU + 2)
+    | otherwise =
+        let firstParameter = (lowerV - fromV) / (toV - fromV)
+            finalParameter = (upperV - fromV) / (toV - fromV)
+            lowerParameter = max 0 (min firstParameter finalParameter)
+            upperParameter = min 1 (max firstParameter finalParameter)
+            firstU = fromU + lowerParameter * (toU - fromU)
+            finalU = fromU + upperParameter * (toU - fromU)
+         in Just (min firstU finalU - 2, max firstU finalU + 2)
+
+  clipToLayout :: RowRange -> Maybe RowRange
+  clipToLayout (firstQ, finalQ) =
+    let clippedFirst = max minimumQ firstQ
+        clippedFinal = min maximumQ finalQ
+     in if clippedFirst <= clippedFinal then Just (clippedFirst, clippedFinal) else Nothing
+
+mergeRowRanges :: [RowRange] -> [RowRange]
+mergeRowRanges = reverse . foldl' merge [] . List.sortOn fst
+ where
+  merge :: [RowRange] -> RowRange -> [RowRange]
+  merge [] range = [range]
+  merge ((previousFirst, previousFinal) : remaining) range@(firstQ, finalQ)
+    | firstQ <= previousFinal + 1 = (previousFirst, max previousFinal finalQ) : remaining
+    | otherwise = range : (previousFirst, previousFinal) : remaining
+
+subtractRowRanges :: [RowRange] -> [RowRange] -> [RowRange]
+subtractRowRanges = foldl' (\remaining cut -> concatMap (subtractRange cut) remaining)
+ where
+  subtractRange :: RowRange -> RowRange -> [RowRange]
+  subtractRange (cutFirst, cutFinal) range@(firstQ, finalQ)
+    | cutFinal < firstQ || finalQ < cutFirst = [range]
+    | otherwise =
+        [(firstQ, cutFirst - 1) | firstQ < cutFirst]
+          <> [(cutFinal + 1, finalQ) | cutFinal < finalQ]
+
+exactCellCentre :: HexCoord -> ExactPoint
+exactCellCentre (HexCoord q r) =
+  exactPoint
+    (fromInteger (3 * toInteger q))
+    (fromInteger (2 * toInteger r + toInteger q))
+
+-- | Closed axis-aligned extent, the cheap separation test in front of every
+-- exact predicate: two closed sets with disjoint extents are disjoint.
+data Extent = Extent !ExactRational !ExactRational !ExactRational !ExactRational
+
+pointExtent :: ExactPoint -> Extent
+pointExtent point =
+  let (x, y) = exactPointCoordinates point
+   in Extent x x y y
+
+pointsExtent :: NonEmpty ExactPoint -> Extent
+pointsExtent = foldr1 joinExtent . fmap pointExtent
+ where
+  joinExtent (Extent leftLow leftHigh leftBottom leftTop) (Extent rightLow rightHigh rightBottom rightTop) =
+    Extent (min leftLow rightLow) (max leftHigh rightHigh) (min leftBottom rightBottom) (max leftTop rightTop)
+
+extentsMeet :: Extent -> Extent -> Bool
+extentsMeet (Extent leftLow leftHigh leftBottom leftTop) (Extent rightLow rightHigh rightBottom rightTop) =
+  leftLow <= rightHigh && rightLow <= leftHigh && leftBottom <= rightTop && rightBottom <= leftTop
+
+-- | The region boundary as an ephemeral view: every boundary vertex and every
+-- boundary segment, each with its extent.
+data PlanarBoundary = PlanarBoundary
+  { boundaryPoints :: ![(Extent, ExactPoint)]
+  , boundarySegments :: ![(Extent, ExactPoint, ExactPoint)]
+  }
+
+planarBoundary :: PlanarRegion -> PlanarBoundary
+planarBoundary region =
+  let loops = planarRegionComponents region >>= componentLoops
+      points = loops >>= NonEmpty.toList . exactLoopPoints
+   in PlanarBoundary
+        { boundaryPoints = fmap (\point -> (pointExtent point, point)) points
+        , boundarySegments =
+            [ (pointsExtent (from :| [to]), from, to)
+            | loop <- loops
+            , (from, to) <- exactPointCycleSegments (NonEmpty.toList (exactLoopPoints loop))
+            ]
+        }
+ where
+  componentLoops component =
+    polygonOuterLoop component : polygonHoleLoops component
+
+cellCoveredByRegion :: PlanarRegion -> [(Extent, ExactPoint, ExactPoint)] -> HexCoord -> Bool
+cellCoveredByRegion region segments coordinate =
+  regionPointLocation region (exactCellCentre coordinate) /= RegionExterior
+    && not
+      ( any
+          (\(extent, from, to) -> extentsMeet cellExtent extent && segmentMeetsConvexInterior cellPoints (from, to))
+          segments
+      )
+ where
+  cellVertices = fmap exactVertexPoint (hexCellVertices coordinate)
+  cellPoints = NonEmpty.toList cellVertices
+  cellExtent = pointsExtent cellVertices
+
+-- | Whether a segment has a point strictly inside the convex polygon whose
+-- vertices are listed counter-clockwise. A segment on a side's line never
+-- enters; otherwise it enters when an endpoint is strictly inside, when it
+-- properly crosses a side, or when it touches the boundary at two distinct
+-- points, since the chord between them is interior to a convex polygon.
+segmentMeetsConvexInterior :: [ExactPoint] -> (ExactPoint, ExactPoint) -> Bool
+segmentMeetsConvexInterior vertices (from, to)
+  | any onSideLine sides = False
+  | strictlyInside fromSides || strictlyInside toSides = True
+  | any properlyCrosses sides = True
+  | otherwise = length (List.nub contacts) >= 2
+ where
+  edges = exactPointCycleSegments vertices
+  fromSides = fmap (\(a, b) -> exactOrient2d a b from) edges
+  toSides = fmap (\(a, b) -> exactOrient2d a b to) edges
+  vertexSides = fmap (exactOrient2d from to) vertices
+  sides = zip (zip fromSides toSides) (zip vertexSides (drop 1 (cycle vertexSides)))
+  onSideLine :: ((Ordering, Ordering), (Ordering, Ordering)) -> Bool
+  onSideLine ((fromSide, toSide), _) = fromSide == EQ && toSide == EQ
+  strictlyInside :: [Ordering] -> Bool
+  strictlyInside = all (== GT)
+  onBoundary :: [Ordering] -> Bool
+  onBoundary orientations = all (/= LT) orientations && elem EQ orientations
+  properlyCrosses :: ((Ordering, Ordering), (Ordering, Ordering)) -> Bool
+  properlyCrosses ((fromSide, toSide), (startSide, endSide)) =
+    opposite fromSide toSide && opposite startSide endSide
+  opposite :: Ordering -> Ordering -> Bool
+  opposite left right = (left == GT && right == LT) || (left == LT && right == GT)
+  contacts =
+    [point | (point, orientations) <- [(from, fromSides), (to, toSides)], onBoundary orientations]
+      <> [vertex | (vertex, side) <- zip vertices vertexSides, side == EQ, withinSpan vertex]
+  withinSpan :: ExactPoint -> Bool
+  withinSpan vertex = extentsMeet (pointExtent vertex) (pointsExtent (from :| [to]))
+
+cellIntersectsRegion :: PlanarRegion -> PlanarBoundary -> HexCoord -> Bool
+cellIntersectsRegion region boundary coordinate =
+  any ((/= RegionExterior) . regionPointLocation region) cellPoints
+    || any
+      (\(extent, point) -> extentsMeet cellExtent extent && pointInClosedHex cellSegments point)
+      (boundaryPoints boundary)
+    || any
+      (\(extent, regionFrom, regionTo) ->
+         extentsMeet cellExtent extent
+           && any
+             (\(cellFrom, cellTo) ->
+                exactSegmentRelation cellFrom cellTo regionFrom regionTo /= SegmentsDisjoint)
+             cellSegments)
+      (boundarySegments boundary)
+ where
+  cellVertices = fmap exactVertexPoint (hexCellVertices coordinate)
+  cellPoints = NonEmpty.toList cellVertices
+  cellSegments = exactPointCycleSegments cellPoints
+  cellExtent = pointsExtent cellVertices
+
+pointInClosedHex :: [(ExactPoint, ExactPoint)] -> ExactPoint -> Bool
+pointInClosedHex cellSegments query =
+  all (\(from, to) -> exactOrient2d from to query /= LT) cellSegments
+
+exactPointCycleSegments :: [ExactPoint] -> [(ExactPoint, ExactPoint)]
+exactPointCycleSegments points =
+  case points of
+    [] -> []
+    firstPoint : remaining -> zip points (remaining <> [firstPoint])
 
 -- One oriented edge with selected area on its left.
 data BoundaryEdge = BoundaryEdge !HexVertex !HexVertex
diff --git a/src-public/Moonlight/Planar/Internal/Minkowski/Operations.hs b/src-public/Moonlight/Planar/Internal/Minkowski/Operations.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/Minkowski/Operations.hs
@@ -0,0 +1,192 @@
+-- | Exact morphology on admitted convex polygons: origin anchoring, linear
+-- edge-angle convolution, exact slab decomposition, and support-half-plane erosion.
+module Moonlight.Planar.Internal.Minkowski.Operations
+  ( structuringElement
+  , structuringElementPolygon
+  , convexMinkowskiSum
+  , convexMinkowskiPolygon
+  , erodeConvexBy
+  , addExactPoints
+  , subtractExactPoints
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Convex
+  ( convexPolygonPoints
+  , convexPolygonRegion
+  , convexPolygonFromRetained
+  , retainTranslatedConvexPolygon
+  )
+import Moonlight.Planar.Exact
+  ( ExactClipDisposition (..)
+  , ExactClipReceipt
+  , ExactClosedHalfPlane
+  , ExactPoint
+  , ExactVector (..)
+  , addExactVectors
+  , exactClipRetainedPolygon
+  , exactClosedHalfPlaneFromDirectedEdge
+  , compareExactVectorAngle
+  , exactVectorFromPoints
+  , exactPoint
+  , exactPointCoordinates
+  , translateExactPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( cyclePairs
+  , cyclePairsNonEmpty
+  , rotateCycleLeast
+  , rotateCycleLeastBy
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  )
+import Moonlight.Planar.Internal.Minkowski.Types
+  ( MinkowskiError (..)
+  , StructuringElement (..)
+  )
+import Moonlight.Planar.Internal.Region.Types
+  ( ConvexPolygon (..)
+  , ExactLoop (..)
+  , PlanarRegion
+  , RegionPointLocation (..)
+  )
+import Moonlight.Planar.Region
+  ( regionPointLocation
+  )
+
+structuringElement
+  :: ConvexPolygon
+  -> Either MinkowskiError StructuringElement
+structuringElement polygon =
+  let origin = exactPoint 0 0
+      location = regionPointLocation (convexPolygonRegion polygon) origin
+   in case location of
+        RegionExterior -> Left (MinkowskiOriginOutside location)
+        _ -> Right (StructuringElement polygon)
+
+structuringElementPolygon :: StructuringElement -> ConvexPolygon
+structuringElementPolygon (StructuringElement polygon) = polygon
+
+convexMinkowskiSum
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> PlanarRegion
+convexMinkowskiSum left right =
+  convexPolygonRegion (convexMinkowskiPolygon left right)
+
+convexMinkowskiPolygon
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> ConvexPolygon
+convexMinkowskiPolygon left right =
+  let leftPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints left)
+      rightPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints right)
+      start = addExactPoints (NonEmpty.head leftPoints) (NonEmpty.head rightPoints)
+      directions =
+        mergeDirections
+          (edgeDirections leftPoints)
+          (edgeDirections rightPoints)
+      directionList = NonEmpty.toList directions
+      scanned = scanl translateExactPoint start directionList
+      resultPoints = start :| take (length directionList - 1) (drop 1 scanned)
+   in ConvexPolygon (ExactLoop (rotateCycleLeast resultPoints))
+
+-- | Erode one convex polygon by another through the strongest translated
+-- support half-plane for each source edge. The canonical sorted exact descent
+-- retains those source lines; a lower-dimensional residual is represented by
+-- the empty polygonal region at this two-dimensional publication boundary.
+erodeConvexBy
+  :: ConvexPolygon
+  -> ConvexPolygon
+  -> Either MinkowskiError (Maybe ConvexPolygon, ExactClipReceipt)
+erodeConvexBy source kernel = do
+  let sourcePoints = convexPolygonPoints source
+      kernelPoints = convexPolygonPoints kernel
+      firstKernel = NonEmpty.head kernelPoints
+      retained = retainTranslatedConvexPolygon
+        (exactVectorFromPoints firstKernel (exactPoint 0 0)) source
+  halfPlanes <-
+    traverse
+      (uncurry (strongestHalfPlane kernelPoints))
+      (cyclePairs sourcePoints)
+  (disposition, clipReceipt) <-
+    first MinkowskiClipFailed (exactClipRetainedPolygon retained halfPlanes)
+  let eroded =
+        case disposition of
+          ExactClipFullDimensional clipped ->
+            Just (convexPolygonFromRetained clipped)
+          ExactClipLowerDimensional _ -> Nothing
+          ExactClipEmpty -> Nothing
+  pure (eroded, clipReceipt)
+
+edgeDirections :: NonEmpty ExactPoint -> NonEmpty ExactVector
+edgeDirections = fmap (uncurry exactVectorFromPoints) . cyclePairsNonEmpty
+
+mergeDirections
+  :: NonEmpty ExactVector
+  -> NonEmpty ExactVector
+  -> NonEmpty ExactVector
+mergeDirections (left :| leftTail) (right :| rightTail) =
+  case compareExactVectorAngle left right of
+    LT -> left :| mergeRemaining leftTail (right : rightTail)
+    GT -> right :| mergeRemaining (left : leftTail) rightTail
+    EQ -> addExactVectors left right :| mergeRemaining leftTail rightTail
+
+mergeRemaining :: [ExactVector] -> [ExactVector] -> [ExactVector]
+mergeRemaining [] right = right
+mergeRemaining left [] = left
+mergeRemaining left@(leftHead : leftTail) right@(rightHead : rightTail) =
+  case compareExactVectorAngle leftHead rightHead of
+    LT -> leftHead : mergeRemaining leftTail right
+    GT -> rightHead : mergeRemaining left rightTail
+    EQ -> addExactVectors leftHead rightHead : mergeRemaining leftTail rightTail
+
+pointSweepKey :: ExactPoint -> (ExactRational, ExactRational)
+pointSweepKey point =
+  let (x, y) = exactPointCoordinates point
+   in (y, x)
+
+strongestHalfPlane
+  :: NonEmpty ExactPoint
+  -> ExactPoint
+  -> ExactPoint
+  -> Either MinkowskiError ExactClosedHalfPlane
+strongestHalfPlane kernelPoints from to =
+  let direction = exactVectorFromPoints from to
+      supportPoint =
+        case kernelPoints of
+          initial :| remaining ->
+            List.foldl'
+              (\selected candidate ->
+                 if directionPointCross direction candidate
+                      < directionPointCross direction selected
+                   then candidate
+                   else selected)
+              initial
+              remaining
+   in first MinkowskiInvalidHalfPlane
+        ( exactClosedHalfPlaneFromDirectedEdge
+            (subtractExactPoints from supportPoint)
+            (subtractExactPoints to supportPoint)
+        )
+
+directionPointCross :: ExactVector -> ExactPoint -> ExactRational
+directionPointCross (ExactVector directionX directionY) point =
+  let (x, y) = exactPointCoordinates point
+   in directionX * y - directionY * x
+
+addExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
+addExactPoints left right =
+  let (leftX, leftY) = exactPointCoordinates left
+      (rightX, rightY) = exactPointCoordinates right
+   in exactPoint (leftX + rightX) (leftY + rightY)
+
+subtractExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
+subtractExactPoints left right =
+  let (leftX, leftY) = exactPointCoordinates left
+      (rightX, rightY) = exactPointCoordinates right
+   in exactPoint (leftX - rightX) (leftY - rightY)
diff --git a/src-public/Moonlight/Planar/Internal/Minkowski/Types.hs b/src-public/Moonlight/Planar/Internal/Minkowski/Types.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/Minkowski/Types.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Closed vocabulary and invariant carriers for exact polygonal morphology.
+module Moonlight.Planar.Internal.Minkowski.Types
+  ( StructuringElement (..)
+  , MinkowskiOperation (..)
+  , MinkowskiError (..)
+  , MinkowskiReceipt (..)
+  ) where
+
+import Control.DeepSeq (NFData)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Convex (ConvexError, ConvexPolygon)
+import Moonlight.Planar.Exact
+  ( ExactClipError
+  , ExactHalfPlaneError
+  , ExactPoint
+  )
+import Moonlight.Planar.Internal.HandleDefs (FaceId)
+import Moonlight.Planar.Internal.Overlay.Types (OverlayError)
+import Moonlight.Planar.Internal.Region.Types
+  ( RegionPublicationError
+  , RegionPointLocation
+  )
+
+newtype StructuringElement = StructuringElement ConvexPolygon
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiOperation
+  = MinkowskiAddition
+  | MinkowskiErosion
+  | MinkowskiOpening
+  | MinkowskiClosing
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiError
+  = MinkowskiConvexFailed !ConvexError
+  | MinkowskiInvalidHalfPlane !ExactHalfPlaneError
+  | MinkowskiClipFailed !ExactClipError
+  | MinkowskiOriginOutside !RegionPointLocation
+  | MinkowskiOverlayFailed !OverlayError
+  | MinkowskiPublicationFailed !RegionPublicationError
+  | MinkowskiInclusionAmbiguous !FaceId !ExactPoint
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data MinkowskiReceipt = MinkowskiReceipt
+  { minkowskiOperation :: !MinkowskiOperation
+  , minkowskiInputComponents :: !Int
+  , minkowskiConvexPieces :: !Int
+  , minkowskiGeneratedPieces :: !Int
+  , minkowskiGeneratedConvolutionEdges :: !Int
+  , minkowskiOverlayPasses :: !Int
+  , minkowskiExactCrossings :: !Int
+  , minkowskiOutputCells :: !Int
+  , minkowskiExactCoordinateBitGrowth :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Edit.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Edit.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Edit.hs
@@ -0,0 +1,1374 @@
+-- | Pure topology-changing regular edits by exact conflict-cavity descent and gluing.
+module Moonlight.Planar.Internal.PowerDiagram.Edit where
+
+import Control.Monad (when)
+import Data.Bifunctor (first)
+import qualified Data.Foldable as Foldable
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import Moonlight.Planar.Exact
+  ( exactOrient2d
+  , exactPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+import Moonlight.Planar.Internal.PowerDiagram.Hull (regularGeneratorTopology)
+import Moonlight.Planar.Internal.PowerDiagram.Locality
+import Moonlight.Planar.Internal.PowerDiagram.Model
+import Moonlight.Planar.Internal.PowerDiagram.Section
+
+-- | Insert one stable-labelled site. Repeating the identical site is
+-- idempotent; reusing its label for different geometry is a typed conflict.
+insertRegularSite
+  :: Ord label
+  => PowerSite label
+  -> RegularTriangulation label
+  -> Either
+      (RegularEditError label)
+      (RegularEditResult label)
+insertRegularSite site triangulation =
+  let label = powerSiteLabel site
+   in case Map.lookup label (storedRegularSites triangulation) of
+        Nothing ->
+          withRegularEditFallback
+            triangulation
+            (Set.singleton label)
+            ( normalizeRegularEdit
+                triangulation
+                (Map.insert label site (storedRegularSites triangulation))
+                (Set.singleton label)
+            )
+            (insertRegularSiteLocally site triangulation)
+        Just resident
+          | resident == site ->
+              Right (unchangedRegularEdit triangulation)
+          | otherwise -> Left (RegularEditSiteConflict label resident site)
+
+-- | Remove one labelled site. Absence is an idempotent successful result.
+removeRegularSite
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Either
+      (RegularEditError label)
+      (RegularEditResult label)
+removeRegularSite label triangulation =
+  case Map.lookup label (storedRegularSites triangulation) of
+    Nothing ->
+      Right (unchangedRegularEdit triangulation)
+    Just _ ->
+      case removeTopologyPreservingSite label triangulation of
+        Just (edited, transitions) ->
+          Right
+            ( RegularEditResult
+                edited
+                (Set.singleton label)
+                transitions
+            )
+        Nothing ->
+          withRegularEditFallback
+            triangulation
+            (Set.singleton label)
+            ( normalizeRegularEdit
+                triangulation
+                (Map.delete label (storedRegularSites triangulation))
+                (Set.singleton label)
+            )
+            (removeRegularSiteLocally label triangulation)
+
+-- | Replace many weights in one normalization. Every label must already
+-- belong to the triangulation; an empty or unchanged patch is idempotent.
+reweightRegularSites
+  :: Ord label
+  => Map label PowerWeight
+  -> RegularTriangulation label
+  -> Either
+      (RegularEditError label)
+      (RegularEditResult label)
+reweightRegularSites requested triangulation =
+  case NonEmpty.nonEmpty unknownLabels of
+    Just unknown -> Left (RegularEditUnknownSites unknown)
+    Nothing
+      | Map.null changedSites ->
+          Right (unchangedRegularEdit triangulation)
+      | otherwise ->
+          case reweightTopologyPreservingSites changedLabels changedSites reweightedSites triangulation of
+            Just (edited, transitions) ->
+              Right
+                ( RegularEditResult
+                    edited
+                    changedLabels
+                    transitions
+                )
+            Nothing ->
+              case commonRegularWeightShift requested sites of
+                Just shift ->
+                  Right
+                    ( translateRegularWeights
+                        shift
+                        reweightedSites
+                        changedLabels
+                        triangulation
+                    )
+                Nothing ->
+                  case Map.elems changedSites of
+                    [revised] ->
+                      withRegularEditFallback
+                        triangulation
+                        changedLabels
+                        normalizeGlobally
+                        (reweightRegularSiteLocally revised triangulation)
+                    _ -> normalizeGlobally
+ where
+  sites = storedRegularSites triangulation
+  unknownLabels = Map.keys (Map.difference requested sites)
+  changedSites =
+    Map.mapMaybeWithKey
+      (\label weight ->
+          case Map.lookup label sites of
+            Just site
+              | powerSiteWeight site /= weight ->
+                  Just site{powerSiteWeight = weight}
+            _ -> Nothing
+      )
+      requested
+  reweightedSites = Map.union changedSites sites
+  changedLabels = Map.keysSet changedSites
+  normalizeGlobally =
+    normalizeRegularEdit triangulation reweightedSites changedLabels
+
+withRegularEditFallback
+  :: Ord label
+  => RegularTriangulation label
+  -> Set label
+  -> Either (RegularEditError label) (RegularEditResult label)
+  -> Either obstruction (RegularTriangulation label, Set label)
+  -> Either (RegularEditError label) (RegularEditResult label)
+withRegularEditFallback before changed fallback =
+  either (const fallback) (Right . publishLocalRegularEdit before changed)
+{-# INLINE withRegularEditFallback #-}
+
+publishLocalRegularEdit
+  :: Ord label
+  => RegularTriangulation label
+  -> Set label
+  -> (RegularTriangulation label, Set label)
+  -> RegularEditResult label
+publishLocalRegularEdit before changed (edited, transitionSupport) =
+  RegularEditResult
+    edited
+    changed
+    ( dispositionTransitionsOn
+        transitionSupport
+        (regularDispositionSection before)
+        (regularDispositionSection edited)
+    )
+{-# INLINE publishLocalRegularEdit #-}
+
+insertRegularSiteLocally
+  :: Ord label
+  => PowerSite label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+insertRegularSiteLocally site triangulation = do
+  section <-
+    maybe (Left LocalRegularSectionUnavailable) Right
+      (storedRegularSection triangulation)
+  let generator = prepareExactPowerGenerator site
+      slope = exactGeneratorSlope generator
+  case Map.lookup slope (sectionSlopeRepresentatives section) of
+    Just representative ->
+      insertCoincidentRegularSite site generator representative section triangulation
+    Nothing ->
+      insertDistinctRegularSite site generator section triangulation
+
+insertCoincidentRegularSite
+  :: Ord label
+  => PowerSite label
+  -> ExactPowerGenerator label
+  -> label
+  -> RegularSection label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+insertCoincidentRegularSite site generator representativeLabel section triangulation = do
+  representative <-
+    requireGeneratorFromMap (sectionGenerators section) representativeLabel
+  let label = powerSiteLabel site
+  case
+    compareCoincidentPriority
+      (exactPowerGeneratorConstant generator, label)
+      (exactPowerGeneratorConstant representative, representativeLabel) of
+    LT ->
+      let coincident = classifyCoincidentGenerator representative generator
+          disposition = coincidentRegularDisposition coincident
+          editedSection =
+            section
+              { sectionCoincidentDispositions =
+                  Map.insert label coincident (sectionCoincidentDispositions section)
+              , sectionRegularDispositions =
+                  Map.insert label disposition (sectionRegularDispositions section)
+              , sectionRegularStars =
+                  Map.insert label emptyRegularSiteStar (sectionRegularStars section)
+              }
+       in Right
+            ( replaceRegularSiteSection
+                (Map.insert label site (storedRegularSites triangulation))
+                editedSection
+                triangulation
+            , Set.singleton label
+            )
+    _ -> Left (LocalRegularSlopeCoincidence representativeLabel)
+
+insertDistinctRegularSite
+  :: Ord label
+  => PowerSite label
+  -> ExactPowerGenerator label
+  -> RegularSection label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+insertDistinctRegularSite site generator section triangulation = do
+  locality <-
+    maybe (Left LocalRegularLocalityUnavailable) Right
+      (sectionRegularLocality section)
+  location <-
+    maybe (Left LocalRegularSlopeLocationFailed) Right
+      (locateRegularSlope (localityFaceSeed locality) section generator)
+  case location of
+    RegularSlopeOutside edgeKey -> Left (LocalRegularSlopeOutside edgeKey)
+    RegularSlopeInside faceKey -> do
+      volumeSign <- regularFaceSign section faceKey generator
+      case volumeSign of
+        LT -> insertHiddenRegularSite site generator faceKey locality section triangulation
+        EQ -> Left (LocalRegularDegenerateFace faceKey)
+        GT -> insertVisibleRegularSite site generator faceKey locality section triangulation
+
+insertHiddenRegularSite
+  :: Ord label
+  => PowerSite label
+  -> ExactPowerGenerator label
+  -> RegularFaceKey label
+  -> RegularLocality label
+  -> RegularSection label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+insertHiddenRegularSite site generator supportFace locality section triangulation =
+  let label = powerSiteLabel site
+      receipt = sectionRegularReceipt section
+      editedLocality = attachRegularHidden label supportFace locality
+      editedSection =
+        section
+          { sectionGenerators = Map.insert label generator (sectionGenerators section)
+          , sectionSlopeRepresentatives =
+              Map.insert (exactGeneratorSlope generator) label (sectionSlopeRepresentatives section)
+          , sectionRegularDispositions =
+              Map.insert label RegularSiteHidden (sectionRegularDispositions section)
+          , sectionRegularStars =
+              Map.insert label emptyRegularSiteStar (sectionRegularStars section)
+          , sectionRegularReceipt =
+              receipt
+                { generatorRegularInputSites = generatorRegularInputSites receipt + 1
+                , generatorRegularHiddenSites = generatorRegularHiddenSites receipt + 1
+                }
+          , sectionRegularLocality = Just editedLocality
+          }
+   in Right
+        ( replaceRegularSiteSection
+            (Map.insert label site (storedRegularSites triangulation))
+            editedSection
+            triangulation
+        , Set.singleton label
+        )
+
+insertVisibleRegularSite
+  :: Ord label
+  => PowerSite label
+  -> ExactPowerGenerator label
+  -> RegularFaceKey label
+  -> RegularLocality label
+  -> RegularSection label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+insertVisibleRegularSite site generator seedFace locality section triangulation = do
+  conflictFaces <- regularConflictCavity section generator seedFace
+  horizon <- regularConflictHorizon section conflictFaces
+  when (Map.null horizon) (Left LocalRegularEmptyHorizon)
+  newFaces <- traverse (regularHorizonFace section generator) (Map.elems horizon)
+  let label = powerSiteLabel site
+      generators = Map.insert label generator (sectionGenerators section)
+  facePatch <-
+    applyRegularFacePatch
+      generators
+      conflictFaces
+      newFaces
+      section
+  certifyRegularFacePatch generators facePatch
+  let changedTopologyLabels = Set.insert label (patchedRegularLabels facePatch)
+      provisionalDispositions =
+        Set.foldl'
+          (publishPatchedRegularDisposition (patchedRegularStars facePatch))
+          (Map.insert label RegularSiteVisible (sectionRegularDispositions section))
+          changedTopologyLabels
+      newlyHidden =
+        Set.filter
+          (becameRegularHidden (sectionRegularDispositions section) provisionalDispositions)
+          changedTopologyLabels
+      hiddenOnConflict =
+        Set.unions
+          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
+          | faceKey <- Set.toAscList conflictFaces
+          ]
+      hiddenToAttach = Set.union newlyHidden hiddenOnConflict
+      receipt = sectionRegularReceipt section
+      provisionalSection =
+        section
+          { sectionGenerators = generators
+          , sectionSlopeRepresentatives =
+              Map.insert (exactGeneratorSlope generator) label (sectionSlopeRepresentatives section)
+          , sectionRegularDispositions = provisionalDispositions
+          , sectionRegularFaces = patchedRegularFaces facePatch
+          , sectionRegularEdges = patchedRegularEdges facePatch
+          , sectionRegularStars = patchedRegularStars facePatch
+          , sectionRegularReceipt =
+              receipt
+                { generatorRegularInputSites = generatorRegularInputSites receipt + 1
+                , generatorRegularVisibleSites =
+                    generatorRegularVisibleSites receipt + 1 - Set.size newlyHidden
+                , generatorRegularHiddenSites =
+                    generatorRegularHiddenSites receipt + Set.size newlyHidden
+                , generatorRegularFaceCount = Map.size (patchedRegularFaces facePatch)
+                , generatorRegularEdgeCount = Map.size (patchedRegularEdges facePatch)
+                }
+          , sectionRegularLocality = Nothing
+          }
+  editedLocality <-
+    patchRegularLocality locality conflictFaces hiddenToAttach provisionalSection
+  let editedSection = provisionalSection{sectionRegularLocality = Just editedLocality}
+  pure
+    ( replaceRegularSiteSection
+        (Map.insert label site (storedRegularSites triangulation))
+        editedSection
+        triangulation
+    , Set.insert label newlyHidden
+    )
+
+
+regularFaceSign
+  :: Ord label
+  => RegularSection label
+  -> RegularFaceKey label
+  -> ExactPowerGenerator label
+  -> Either (LocalRegularEditObstruction label) Ordering
+regularFaceSign section faceKey candidate = do
+  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
+  (firstGenerator, secondGenerator, thirdGenerator) <-
+    regularFaceGenerators (sectionGenerators section) face
+  pure
+    ( exactGeneratorLiftedSign
+        firstGenerator
+        secondGenerator
+        thirdGenerator
+        candidate
+    )
+
+regularConflictCavity
+  :: Ord label
+  => RegularSection label
+  -> ExactPowerGenerator label
+  -> RegularFaceKey label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Set (RegularFaceKey label))
+regularConflictCavity section candidate seed =
+  let descended =
+        descendRegularConflict
+          section
+          candidate
+          RegularConflictDescent
+            { conflictPendingFaces = Set.singleton seed
+            , conflictVisitedFaces = Set.empty
+            , conflictVisibleFaces = Set.empty
+            , conflictObstruction = Nothing
+            }
+   in case conflictObstruction descended of
+        Just obstruction -> Left obstruction
+        Nothing
+          | Set.null (conflictVisibleFaces descended) ->
+              Left (LocalRegularDegenerateFace seed)
+          | otherwise -> Right (conflictVisibleFaces descended)
+
+-- | Every step retires one pending face into the visited set and enqueues only
+-- unvisited neighbours, so the descent halts within one step per section face.
+descendRegularConflict
+  :: Ord label
+  => RegularSection label
+  -> ExactPowerGenerator label
+  -> RegularConflictDescent label
+  -> RegularConflictDescent label
+descendRegularConflict section candidate descent =
+  case Set.minView (conflictPendingFaces descent) of
+    Nothing -> descent
+    Just (faceKey, remaining) ->
+      case regularFaceSign section faceKey candidate of
+        Left obstruction ->
+          descent
+            { conflictPendingFaces = remaining
+            , conflictObstruction = Just obstruction
+            }
+        Right volumeSign ->
+          let visited = Set.insert faceKey (conflictVisitedFaces descent)
+           in case volumeSign of
+                LT ->
+                  descendRegularConflict
+                    section
+                    candidate
+                    descent
+                      { conflictPendingFaces = remaining
+                      , conflictVisitedFaces = visited
+                      }
+                EQ ->
+                  descent
+                    { conflictPendingFaces = remaining
+                    , conflictVisitedFaces = visited
+                    , conflictObstruction = Just (LocalRegularDegenerateFace faceKey)
+                    }
+                GT ->
+                  case regularFaceNeighbours section faceKey of
+                    Left obstruction ->
+                      descent
+                        { conflictPendingFaces = remaining
+                        , conflictVisitedFaces = visited
+                        , conflictObstruction = Just obstruction
+                        }
+                    Right neighbours ->
+                      descendRegularConflict
+                        section
+                        candidate
+                        descent
+                          { conflictPendingFaces =
+                              Set.union remaining (Set.difference neighbours visited)
+                          , conflictVisitedFaces = visited
+                          , conflictVisibleFaces =
+                              Set.insert faceKey (conflictVisibleFaces descent)
+                          }
+
+
+regularConflictHorizon
+  :: Ord label
+  => RegularSection label
+  -> Set (RegularFaceKey label)
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Map (RegularEdgeKey label) (label, label))
+regularConflictHorizon section conflictFaces = do
+  boundarySections <-
+    traverse
+      (regularConflictFaceHorizon section conflictFaces)
+      (Set.toAscList conflictFaces)
+  pure (Map.unions boundarySections)
+
+regularConflictFaceHorizon
+  :: Ord label
+  => RegularSection label
+  -> Set (RegularFaceKey label)
+  -> RegularFaceKey label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Map (RegularEdgeKey label) (label, label))
+regularConflictFaceHorizon section conflictFaces faceKey = do
+  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
+  associations <-
+    traverse
+      (classifyConflictEdge section conflictFaces)
+      (regularFaceDirectedEdgeKeys face)
+  pure (Map.fromList (concat associations))
+
+classifyConflictEdge
+  :: Ord label
+  => RegularSection label
+  -> Set (RegularFaceKey label)
+  -> (label, label)
+  -> Either
+      (LocalRegularEditObstruction label)
+      [(RegularEdgeKey label, (label, label))]
+classifyConflictEdge section conflictFaces directed@(fromLabel, toLabel) = do
+  let edgeKey = orderedPair fromLabel toLabel
+  incident <- requireRegularEdgeIncidence section edgeKey
+  if Set.size incident > 2
+    then Left (LocalRegularNonManifoldEdge edgeKey (Set.size incident))
+    else
+      if Set.size (Set.intersection incident conflictFaces) == 1
+        then Right [(edgeKey, directed)]
+        else Right []
+
+regularHorizonFace
+  :: Ord label
+  => RegularSection label
+  -> ExactPowerGenerator label
+  -> (label, label)
+  -> Either (LocalRegularEditObstruction label) (RegularFace label)
+regularHorizonFace section candidate (fromLabel, toLabel) = do
+  fromGenerator <- requireGeneratorFromMap (sectionGenerators section) fromLabel
+  toGenerator <- requireGeneratorFromMap (sectionGenerators section) toLabel
+  let candidateLabel = exactPowerGeneratorLabel candidate
+      faceKey = sortedRegularFaceLabels fromLabel toLabel candidateLabel
+      fromPoint = uncurry exactPoint (exactGeneratorSlope fromGenerator)
+      toPoint = uncurry exactPoint (exactGeneratorSlope toGenerator)
+      candidatePoint = uncurry exactPoint (exactGeneratorSlope candidate)
+  case exactOrient2d fromPoint toPoint candidatePoint of
+    GT -> do
+      dual <-
+        first LocalRegularTopologyObstruction
+          (exactGeneratorFaceDual fromGenerator toGenerator candidate)
+      pure (canonicalRegularFace fromLabel toLabel candidateLabel dual)
+    _ -> Left (LocalRegularDegenerateFace faceKey)
+
+applyRegularFacePatch
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> Set (RegularFaceKey label)
+  -> [RegularFace label]
+  -> RegularSection label
+  -> Either (LocalRegularEditObstruction label) (RegularFacePatch label)
+applyRegularFacePatch generators removedFaceKeys newFaces section = do
+  removedFaces <-
+    traverse
+      (requireFaceFromMap (sectionRegularFaces section))
+      (Set.toAscList removedFaceKeys)
+  let newFaceSection =
+        Map.fromList [(regularFaceKey face, face) | face <- newFaces]
+      faces =
+        Map.union newFaceSection
+          (Map.withoutKeys (sectionRegularFaces section) removedFaceKeys)
+      touchedEdges =
+        Set.unions
+          [ Set.unions (fmap (Set.fromList . regularFaceCanonicalEdgeKeys) removedFaces)
+          , Set.unions (fmap (Set.fromList . regularFaceCanonicalEdgeKeys) newFaces)
+          ]
+      newIncidence =
+        List.foldl'
+          (Map.unionWith Set.union)
+          Map.empty
+          (fmap regularFaceEdgeIncidence newFaces)
+      affectedLabels =
+        Set.unions
+          [ Set.unions (fmap regularFaceLabelSet removedFaces)
+          , Set.unions (fmap regularFaceLabelSet newFaces)
+          ]
+  edges <-
+    Foldable.foldlM
+      (patchRegularEdge generators faces removedFaceKeys newIncidence)
+      (sectionRegularEdges section)
+      touchedEdges
+  let stars =
+        patchRegularStars
+          affectedLabels
+          removedFaceKeys
+          newFaceSection
+          touchedEdges
+          edges
+          section
+  pure
+    RegularFacePatch
+      { patchedRegularFaces = faces
+      , patchedRegularEdges = edges
+      , patchedRegularStars = stars
+      , patchedRegularLabels = affectedLabels
+      , patchedRegularTouchedEdges = touchedEdges
+      }
+
+patchRegularEdge
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> Map (RegularFaceKey label) (RegularFace label)
+  -> Set (RegularFaceKey label)
+  -> Map (RegularEdgeKey label) (Set (RegularFaceKey label))
+  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
+  -> RegularEdgeKey label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Map (RegularEdgeKey label) (RegularEdgeSection label))
+patchRegularEdge generators faces removedFaceKeys newIncidence edges edgeKey =
+  let oldIncidence =
+        maybe Set.empty sectionRegularEdgeFaces (Map.lookup edgeKey edges)
+      incidence =
+        Set.union
+          (Set.difference oldIncidence removedFaceKeys)
+          (Map.findWithDefault Set.empty edgeKey newIncidence)
+   in if Set.null incidence
+        then Right (Map.delete edgeKey edges)
+        else do
+          edgeSection <- publishRegularEdgeSection generators faces edgeKey incidence
+          pure (Map.insert edgeKey edgeSection edges)
+
+publishRegularEdgeSection
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> Map (RegularFaceKey label) (RegularFace label)
+  -> RegularEdgeKey label
+  -> Set (RegularFaceKey label)
+  -> Either (LocalRegularEditObstruction label) (RegularEdgeSection label)
+publishRegularEdgeSection generators faces edgeKey@(firstLabel, secondLabel) incidence =
+  case Set.toAscList incidence of
+    [faceKey] -> do
+      face <- requireFaceFromMap faces faceKey
+      (fromLabel, toLabel, oppositeLabel) <-
+        requireFaceEdgeWitness edgeKey face
+      fromGenerator <- requireGeneratorFromMap generators fromLabel
+      toGenerator <- requireGeneratorFromMap generators toLabel
+      oppositeGenerator <- requireGeneratorFromMap generators oppositeLabel
+      ray <-
+        first LocalRegularTopologyObstruction
+          ( exactGeneratorBoundaryDualRay
+              (regularFaceDualPoint face)
+              fromGenerator
+              toGenerator
+              oppositeGenerator
+          )
+      pure
+        ( RegularEdgeSection
+            (RegularEdge firstLabel secondLabel (UnboundedPowerDual ray))
+            incidence
+        )
+    [firstFaceKey, secondFaceKey] -> do
+      firstFace <- requireFaceFromMap faces firstFaceKey
+      secondFace <- requireFaceFromMap faces secondFaceKey
+      let firstDual = regularFaceDualPoint firstFace
+          secondDual = regularFaceDualPoint secondFace
+      dual <-
+        first LocalRegularTopologyObstruction
+          ( exactGeneratorDualBetween
+              firstLabel
+              secondLabel
+              firstDual
+              secondDual
+          )
+      pure
+        ( RegularEdgeSection
+            (RegularEdge firstLabel secondLabel dual)
+            incidence
+        )
+    incidentFaces ->
+      Left (LocalRegularNonManifoldEdge edgeKey (length incidentFaces))
+
+patchRegularStars
+  :: Ord label
+  => Set label
+  -> Set (RegularFaceKey label)
+  -> Map (RegularFaceKey label) (RegularFace label)
+  -> Set (RegularEdgeKey label)
+  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
+  -> RegularSection label
+  -> Map label (RegularSiteStar label)
+patchRegularStars affectedLabels removedFaceKeys newFaceSection touchedEdges edges section =
+  Set.foldl' patchStar (sectionRegularStars section) affectedLabels
+ where
+  affectedSupport = Map.fromSet (const ()) affectedLabels
+  touchedStars =
+    regularSiteStars
+      affectedSupport
+      Map.empty
+      ( Map.restrictKeys
+          (Map.union edges (sectionRegularEdges section))
+          touchedEdges
+      )
+  addedStars =
+    regularSiteStars
+      affectedSupport
+      newFaceSection
+      (Map.restrictKeys edges touchedEdges)
+  patchStar stars label =
+    let oldStar = Map.findWithDefault emptyRegularSiteStar label stars
+        touchedStar = Map.findWithDefault emptyRegularSiteStar label touchedStars
+        addedStar = Map.findWithDefault emptyRegularSiteStar label addedStars
+        incidentFaces =
+          Set.union
+            (Set.difference (sectionIncidentFaces oldStar) removedFaceKeys)
+            (sectionIncidentFaces addedStar)
+        retainedNeighbours =
+          Set.difference
+            (sectionSiteNeighbours oldStar)
+            (sectionSiteNeighbours touchedStar)
+     in Map.insert
+          label
+          ( RegularSiteStar
+              incidentFaces
+              (Set.union retainedNeighbours (sectionSiteNeighbours addedStar))
+          )
+          stars
+
+certifyRegularFacePatch
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> RegularFacePatch label
+  -> Either (LocalRegularEditObstruction label) ()
+certifyRegularFacePatch generators patch =
+  Foldable.traverse_
+    (\edgeKey ->
+       case Map.lookup edgeKey (patchedRegularEdges patch) of
+         Nothing -> Right ()
+         Just edgeSection ->
+           certifyRegularEdgeConvexity
+             generators
+             (patchedRegularFaces patch)
+             (edgeKey, edgeSection))
+    (patchedRegularTouchedEdges patch)
+
+certifyRegularEdgeConvexity
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> Map (RegularFaceKey label) (RegularFace label)
+  -> (RegularEdgeKey label, RegularEdgeSection label)
+  -> Either (LocalRegularEditObstruction label) ()
+certifyRegularEdgeConvexity generators faces (edgeKey, edgeSection) =
+  case Set.toAscList (sectionRegularEdgeFaces edgeSection) of
+    [_] -> Right ()
+    [firstFaceKey, secondFaceKey] -> do
+      firstFace <- requireFaceFromMap faces firstFaceKey
+      secondFace <- requireFaceFromMap faces secondFaceKey
+      (_, _, secondOpposite) <- requireFaceEdgeWitness edgeKey secondFace
+      secondOppositeGenerator <- requireGeneratorFromMap generators secondOpposite
+      (firstGenerator, secondGenerator, thirdGenerator) <-
+        regularFaceGenerators generators firstFace
+      if exactGeneratorLiftedSign
+          firstGenerator
+          secondGenerator
+          thirdGenerator
+          secondOppositeGenerator
+          == LT
+        then Right ()
+        else Left (LocalRegularNonConvexEdge edgeKey)
+    incident -> Left (LocalRegularNonManifoldEdge edgeKey (length incident))
+
+publishPatchedRegularDisposition
+  :: Ord label
+  => Map label (RegularSiteStar label)
+  -> Map label (RegularSiteDisposition label)
+  -> label
+  -> Map label (RegularSiteDisposition label)
+publishPatchedRegularDisposition stars dispositions label =
+  case Map.lookup label dispositions of
+    Just RegularSiteVisible
+      | maybe True (Set.null . sectionIncidentFaces) (Map.lookup label stars) ->
+          Map.insert label RegularSiteHidden dispositions
+    _ -> dispositions
+
+becameRegularHidden
+  :: Ord label
+  => Map label (RegularSiteDisposition label)
+  -> Map label (RegularSiteDisposition label)
+  -> label
+  -> Bool
+becameRegularHidden before after label =
+  Map.lookup label before == Just RegularSiteVisible
+    && Map.lookup label after == Just RegularSiteHidden
+
+requireRegularEdgeIncidence
+  :: Ord label
+  => RegularSection label
+  -> RegularEdgeKey label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Set (RegularFaceKey label))
+requireRegularEdgeIncidence section edgeKey =
+  maybe
+    (Left (LocalRegularNonManifoldEdge edgeKey 0))
+    (Right . sectionRegularEdgeFaces)
+    (Map.lookup edgeKey (sectionRegularEdges section))
+
+regularFaceGenerators
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> RegularFace label
+  -> Either
+      (LocalRegularEditObstruction label)
+      ( ExactPowerGenerator label
+      , ExactPowerGenerator label
+      , ExactPowerGenerator label
+      )
+regularFaceGenerators generators face = do
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+  (,,)
+    <$> requireGeneratorFromMap generators firstLabel
+    <*> requireGeneratorFromMap generators secondLabel
+    <*> requireGeneratorFromMap generators thirdLabel
+
+removeRegularSiteLocally
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+removeRegularSiteLocally label triangulation = do
+  section <-
+    maybe (Left LocalRegularSectionUnavailable) Right
+      (storedRegularSection triangulation)
+  locality <-
+    maybe (Left LocalRegularLocalityUnavailable) Right
+      (sectionRegularLocality section)
+  disposition <-
+    maybe (Left (LocalRegularGeneratorMissing label)) Right
+      (Map.lookup label (sectionRegularDispositions section))
+  when
+    (disposition /= RegularSiteVisible || hasCoincidentAliases label section)
+    (Left (LocalRegularBoundaryRemoval label))
+  star <-
+    maybe (Left (LocalRegularGeneratorMissing label)) Right
+      (Map.lookup label (sectionRegularStars section))
+  let removedFaces = sectionIncidentFaces star
+  when (Set.null removedFaces) (Left (LocalRegularBoundaryRemoval label))
+  Foldable.traverse_
+    (requireInteriorRemovalEdge section label)
+    (sectionSiteNeighbours star)
+  horizon <- regularConflictHorizon section removedFaces
+  when (Map.null horizon) (Left LocalRegularEmptyHorizon)
+  let hiddenCandidates =
+        Set.unions
+          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
+          | faceKey <- Set.toAscList removedFaces
+          ]
+      horizonLabels =
+        Set.fromList
+          [ endpoint
+          | (firstLabel, secondLabel) <- Map.keys horizon
+          , endpoint <- [firstLabel, secondLabel]
+          ]
+      candidateLabels = Set.union horizonLabels hiddenCandidates
+      generators = Map.delete label (sectionGenerators section)
+  candidateGenerators <-
+    traverse
+      (requireGeneratorFromMap generators)
+      (Set.toAscList candidateLabels)
+  nonEmptyCandidates <-
+    maybe (Left LocalRegularCandidateSectionEmpty) Right
+      (NonEmpty.nonEmpty candidateGenerators)
+  candidateTopology <-
+    first LocalRegularTopologyObstruction
+      (regularGeneratorTopology (DistinctSlopeGenerators nonEmptyCandidates))
+  when
+    ( any
+        ((== RegularGeneratorLowerDimensional) . snd)
+        (NonEmpty.toList (generatorRegularDispositions candidateTopology))
+    )
+    (Left LocalRegularCavityBoundaryMismatch)
+  removedFaceValues <-
+    traverse
+      (requireFaceFromMap (sectionRegularFaces section))
+      (Set.toAscList removedFaces)
+  let proposedFaces = fmap publishGeneratorFace (generatorRegularFaces candidateTopology)
+      newFaces =
+        filter
+          (regularFaceCentroidInside generators removedFaceValues)
+          proposedFaces
+      proposedBoundary = regularFaceSectionBoundary newFaces
+  when
+    (proposedBoundary /= Map.keysSet horizon)
+    (Left LocalRegularCavityBoundaryMismatch)
+  facePatch <- applyRegularFacePatch generators removedFaces newFaces section
+  certifyRegularFacePatch generators facePatch
+  let candidateDispositions =
+        Set.foldl'
+          (publishRemovalCandidateDisposition (patchedRegularStars facePatch))
+          (Map.delete label (sectionRegularDispositions section))
+          candidateLabels
+      exposed =
+        Set.filter
+          (becameRegularVisible (sectionRegularDispositions section) candidateDispositions)
+          candidateLabels
+      stillHidden =
+        Set.filter
+          (\candidate -> Map.lookup candidate candidateDispositions == Just RegularSiteHidden)
+          hiddenCandidates
+      receipt = sectionRegularReceipt section
+      provisionalSection =
+        section
+          { sectionGenerators = generators
+          , sectionSlopeRepresentatives =
+              Map.filter (/= label) (sectionSlopeRepresentatives section)
+          , sectionRegularDispositions = candidateDispositions
+          , sectionRegularFaces = patchedRegularFaces facePatch
+          , sectionRegularEdges = patchedRegularEdges facePatch
+          , sectionRegularStars = Map.delete label (patchedRegularStars facePatch)
+          , sectionRegularReceipt =
+              receipt
+                { generatorRegularInputSites = generatorRegularInputSites receipt - 1
+                , generatorRegularVisibleSites =
+                    generatorRegularVisibleSites receipt - 1 + Set.size exposed
+                , generatorRegularHiddenSites =
+                    generatorRegularHiddenSites receipt - Set.size exposed
+                , generatorRegularFaceCount = Map.size (patchedRegularFaces facePatch)
+                , generatorRegularEdgeCount = Map.size (patchedRegularEdges facePatch)
+                }
+          , sectionRegularLocality = Nothing
+          }
+  editedLocality <-
+    patchRegularLocality locality removedFaces stillHidden provisionalSection
+  let editedSection = provisionalSection{sectionRegularLocality = Just editedLocality}
+  pure
+    ( replaceRegularSiteSection
+        (Map.delete label (storedRegularSites triangulation))
+        editedSection
+        triangulation
+    , Set.insert label exposed
+    )
+
+requireInteriorRemovalEdge
+  :: Ord label
+  => RegularSection label
+  -> label
+  -> label
+  -> Either (LocalRegularEditObstruction label) ()
+requireInteriorRemovalEdge section label neighbour = do
+  let edgeKey = orderedPair label neighbour
+  incidence <- requireRegularEdgeIncidence section edgeKey
+  if Set.size incidence == 2
+    then Right ()
+    else Left (LocalRegularBoundaryRemoval label)
+
+regularFaceCentroidInside
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> [RegularFace label]
+  -> RegularFace label
+  -> Bool
+regularFaceCentroidInside generators domainFaces candidate =
+  case regularFaceSlopeSum generators candidate of
+    Left _ -> False
+    Right slopeSum -> any (regularSlopeSumInsideFace generators slopeSum) domainFaces
+
+regularFaceSlopeSum
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> RegularFace label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (ExactRational, ExactRational)
+regularFaceSlopeSum generators face = do
+  (firstGenerator, secondGenerator, thirdGenerator) <-
+    regularFaceGenerators generators face
+  let (firstX, firstY) = exactGeneratorSlope firstGenerator
+      (secondX, secondY) = exactGeneratorSlope secondGenerator
+      (thirdX, thirdY) = exactGeneratorSlope thirdGenerator
+  pure (firstX + secondX + thirdX, firstY + secondY + thirdY)
+
+regularSlopeSumInsideFace
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> (ExactRational, ExactRational)
+  -> RegularFace label
+  -> Bool
+regularSlopeSumInsideFace generators slopeSum =
+  not
+    . any (generatorEdgeExcludesScaled 3 slopeSum generators)
+    . regularFaceDirectedEdgeKeys
+
+regularFaceSectionBoundary
+  :: Ord label
+  => [RegularFace label]
+  -> Set (RegularEdgeKey label)
+regularFaceSectionBoundary =
+  Map.keysSet
+    . Map.filter ((== 1) . Set.size)
+    . List.foldl'
+        (Map.unionWith Set.union)
+        Map.empty
+    . fmap regularFaceEdgeIncidence
+
+publishRemovalCandidateDisposition
+  :: Ord label
+  => Map label (RegularSiteStar label)
+  -> Map label (RegularSiteDisposition label)
+  -> label
+  -> Map label (RegularSiteDisposition label)
+publishRemovalCandidateDisposition stars dispositions label =
+  Map.insert
+    label
+    ( if maybe True (Set.null . sectionIncidentFaces) (Map.lookup label stars)
+        then RegularSiteHidden
+        else RegularSiteVisible
+    )
+    dispositions
+
+becameRegularVisible
+  :: Ord label
+  => Map label (RegularSiteDisposition label)
+  -> Map label (RegularSiteDisposition label)
+  -> label
+  -> Bool
+becameRegularVisible before after label =
+  Map.lookup label before == Just RegularSiteHidden
+    && Map.lookup label after == Just RegularSiteVisible
+
+reweightRegularSiteLocally
+  :: Ord label
+  => PowerSite label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+reweightRegularSiteLocally revised triangulation = do
+  let label = powerSiteLabel revised
+  (removed, removalSupport) <- removeAnyRegularSiteLocally label triangulation
+  (inserted, insertionSupport) <- insertRegularSiteLocally revised removed
+  pure (inserted, Set.union removalSupport insertionSupport)
+
+commonRegularWeightShift
+  :: Ord label
+  => Map label PowerWeight
+  -> Map label (PowerSite label)
+  -> Maybe ExactRational
+commonRegularWeightShift requested sites
+  | Map.size requested /= Map.size sites = Nothing
+  | otherwise = do
+      (firstLabel, firstWeight) <- Map.lookupMin requested
+      firstSite <- Map.lookup firstLabel sites
+      let shift =
+            powerWeightExact firstWeight
+              - powerWeightExact (powerSiteWeight firstSite)
+      if Map.isSubmapOfBy (powerSiteHasWeightShift shift) requested sites
+        then Just shift
+        else Nothing
+
+powerSiteHasWeightShift
+  :: ExactRational
+  -> PowerWeight
+  -> PowerSite label
+  -> Bool
+powerSiteHasWeightShift shift weight site =
+  powerWeightExact weight
+    - powerWeightExact (powerSiteWeight site)
+    == shift
+
+translateRegularWeights
+  :: ExactRational
+  -> Map label (PowerSite label)
+  -> Set label
+  -> RegularTriangulation label
+  -> RegularEditResult label
+translateRegularWeights shift sites changedLabels triangulation =
+  let shiftedSection =
+        fmap
+          (\section ->
+             section
+               { sectionGenerators =
+                   fmap
+                     (translateExactPowerGenerator shift)
+                     (sectionGenerators section)
+               })
+          (storedRegularSection triangulation)
+   in RegularEditResult
+        { regularEditTriangulation =
+            triangulation
+              { storedRegularSites = sites
+              , storedRegularSection = shiftedSection
+              }
+        , regularEditChangedSites = changedLabels
+        , regularEditTransitions = Vector.empty
+        }
+
+translateExactPowerGenerator
+  :: ExactRational
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+translateExactPowerGenerator shift generator =
+  generator
+    { exactPowerGeneratorConstant =
+        exactPowerGeneratorConstant generator + shift
+    }
+
+removeAnyRegularSiteLocally
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (RegularTriangulation label, Set label)
+removeAnyRegularSiteLocally label triangulation =
+  case removeTopologyPreservingSite label triangulation of
+    Just (edited, _) -> Right (edited, Set.singleton label)
+    Nothing -> removeRegularSiteLocally label triangulation
+
+-- Exact topology-preserving edits descend through the existing regular
+-- section. A failed classification is not an error; it selects the canonical
+-- global normalization below.
+
+classifyCoincidentSubordinate
+  :: Ord label
+  => PowerSite label
+  -> PowerSite label
+  -> Maybe (CoincidentGeneratorDisposition label)
+classifyCoincidentSubordinate representative candidate =
+  if powerSiteQueryPoint candidate /= powerSiteQueryPoint representative
+    then Nothing
+    else
+      case
+        compareCoincidentPriority
+          (powerSiteWeight candidate, powerSiteLabel candidate)
+          (powerSiteWeight representative, powerSiteLabel representative) of
+        LT ->
+          Just
+            ( classifyCoincidentValue
+                (powerSiteLabel representative)
+                (powerSiteWeight representative)
+                (powerSiteWeight candidate)
+            )
+        _ -> Nothing
+
+removeTopologyPreservingSite
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Maybe
+      ( RegularTriangulation label
+      , Vector.Vector (RegularSiteTransition label)
+      )
+removeTopologyPreservingSite label triangulation = do
+  section <- storedRegularSection triangulation
+  disposition <- Map.lookup label (sectionRegularDispositions section)
+  editedSection <-
+    case disposition of
+      RegularSiteCoincidentEquivalentTo _ -> removeCoincident section
+      RegularSiteCoincidentDominatedBy _ -> removeCoincident section
+      RegularSiteHidden
+        | not (hasCoincidentAliases label section) -> do
+            generator <- Map.lookup label (sectionGenerators section)
+            let receipt = sectionRegularReceipt section
+            pure
+              section
+                { sectionGenerators = Map.delete label (sectionGenerators section)
+                , sectionSlopeRepresentatives =
+                    Map.delete (exactGeneratorSlope generator) (sectionSlopeRepresentatives section)
+                , sectionRegularDispositions =
+                    Map.delete label (sectionRegularDispositions section)
+                , sectionRegularStars = Map.delete label (sectionRegularStars section)
+                , sectionRegularReceipt =
+                    receipt
+                      { generatorRegularInputSites = generatorRegularInputSites receipt - 1
+                      , generatorRegularHiddenSites = generatorRegularHiddenSites receipt - 1
+                      }
+                , sectionRegularLocality = fmap (detachRegularHidden label) (sectionRegularLocality section)
+                }
+      _ -> Nothing
+  let edited =
+        replaceRegularSiteSection
+          (Map.delete label (storedRegularSites triangulation))
+          editedSection
+          triangulation
+  pure
+    ( edited
+    , Vector.singleton (RegularSiteDisappeared label disposition)
+    )
+ where
+  removeCoincident section = do
+    _ <- Map.lookup label (sectionCoincidentDispositions section)
+    pure
+      section
+        { sectionCoincidentDispositions =
+            Map.delete label (sectionCoincidentDispositions section)
+        , sectionRegularDispositions =
+            Map.delete label (sectionRegularDispositions section)
+        , sectionRegularStars = Map.delete label (sectionRegularStars section)
+        }
+
+data RegularReweightDescent label = RegularReweightDescent
+  { descendedCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
+  , descendedGenerators :: !(Map label (ExactPowerGenerator label))
+  , descendedRegularDispositions :: !(Map label (RegularSiteDisposition label))
+  , descendedTransitions :: ![RegularSiteTransition label]
+  }
+
+reweightTopologyPreservingSites
+  :: Ord label
+  => Set label
+  -> Map label (PowerSite label)
+  -> Map label (PowerSite label)
+  -> RegularTriangulation label
+  -> Maybe
+      ( RegularTriangulation label
+      , Vector.Vector (RegularSiteTransition label)
+      )
+reweightTopologyPreservingSites changedLabels changedSites reweightedSites triangulation = do
+  section <- storedRegularSection triangulation
+  let coincidentRepresentatives =
+        Map.foldl'
+          (flip (Set.insert . coincidentRepresentativeLabel))
+          Set.empty
+          (sectionCoincidentDispositions section)
+  descent <-
+    Foldable.foldlM
+      ( descendTopologyPreservingReweight
+          changedLabels
+          reweightedSites
+          coincidentRepresentatives
+          triangulation
+      )
+      RegularReweightDescent
+        { descendedCoincidentDispositions = sectionCoincidentDispositions section
+        , descendedGenerators = sectionGenerators section
+        , descendedRegularDispositions = sectionRegularDispositions section
+        , descendedTransitions = []
+        }
+      changedSites
+  let editedSection =
+        section
+          { sectionCoincidentDispositions = descendedCoincidentDispositions descent
+          , sectionGenerators = descendedGenerators descent
+          , sectionRegularDispositions = descendedRegularDispositions descent
+          }
+      edited =
+        replaceRegularSiteSection
+          reweightedSites
+          editedSection
+          triangulation
+  pure (edited, Vector.fromList (reverse (descendedTransitions descent)))
+
+descendTopologyPreservingReweight
+  :: Ord label
+  => Set label
+  -> Map label (PowerSite label)
+  -> Set label
+  -> RegularTriangulation label
+  -> RegularReweightDescent label
+  -> PowerSite label
+  -> Maybe (RegularReweightDescent label)
+descendTopologyPreservingReweight changedLabels reweightedSites coincidentRepresentatives triangulation descent revised = do
+  resident <- Map.lookup label (storedRegularSites triangulation)
+  disposition <-
+    storedRegularSection triangulation
+      >>= Map.lookup label . sectionRegularDispositions
+  case regularCoincidentRepresentative disposition of
+    Just representative
+      | Set.notMember representative changedLabels -> do
+          representativeSite <- Map.lookup representative reweightedSites
+          coincident <- classifyCoincidentSubordinate representativeSite revised
+          let after = coincidentRegularDisposition coincident
+          pure
+            descent
+              { descendedCoincidentDispositions =
+                  Map.insert label coincident (descendedCoincidentDispositions descent)
+              , descendedRegularDispositions =
+                  Map.insert label after (descendedRegularDispositions descent)
+              , descendedTransitions =
+                  if disposition == after
+                    then descendedTransitions descent
+                    else RegularSiteTransitioned label disposition after : descendedTransitions descent
+              }
+    _
+      | disposition == RegularSiteHidden
+          && powerSiteWeight revised < powerSiteWeight resident
+          && Set.notMember label coincidentRepresentatives ->
+          let generator = prepareExactPowerGenerator revised
+           in Just
+                descent
+                  { descendedGenerators =
+                      Map.insert label generator (descendedGenerators descent)
+                  }
+      | otherwise -> Nothing
+ where
+  label = powerSiteLabel revised
+
+hasCoincidentAliases
+  :: Eq label
+  => label
+  -> RegularSection label
+  -> Bool
+hasCoincidentAliases label =
+  any ((== label) . coincidentRepresentativeLabel)
+    . Map.elems
+    . sectionCoincidentDispositions
+
+regularCoincidentRepresentative :: RegularSiteDisposition label -> Maybe label
+regularCoincidentRepresentative disposition =
+  case disposition of
+    RegularSiteCoincidentEquivalentTo label -> Just label
+    RegularSiteCoincidentDominatedBy label -> Just label
+    _ -> Nothing
+
+coincidentRepresentativeLabel :: CoincidentGeneratorDisposition label -> label
+coincidentRepresentativeLabel disposition =
+  case disposition of
+    CoincidentGeneratorEquivalentTo label -> label
+    CoincidentGeneratorDominatedBy label -> label
+
+normalizeRegularEdit
+  :: Ord label
+  => RegularTriangulation label
+  -> Map label (PowerSite label)
+  -> Set label
+  -> Either
+      (RegularEditError label)
+      (RegularEditResult label)
+normalizeRegularEdit previous sites changedSites = do
+  triangulation <-
+    first RegularEditTopologyFailed
+      (constructRegularTriangulation sites)
+  pure
+    ( RegularEditResult
+        triangulation
+        changedSites
+        ( dispositionTransitions
+            (regularDispositionSection previous)
+            (regularDispositionSection triangulation)
+        )
+    )
+
+unchangedRegularEdit
+  :: RegularTriangulation label
+  -> RegularEditResult label
+unchangedRegularEdit triangulation =
+  RegularEditResult triangulation Set.empty Vector.empty
+
+
+dispositionTransitions
+  :: Ord label
+  => Map label (RegularSiteDisposition label)
+  -> Map label (RegularSiteDisposition label)
+  -> Vector.Vector (RegularSiteTransition label)
+dispositionTransitions previous current =
+  Vector.fromList
+    ( Map.elems
+        ( Map.mergeWithKey
+            changedDispositionTransition
+            (Map.mapWithKey RegularSiteDisappeared)
+            (Map.mapWithKey RegularSiteAppeared)
+            previous
+            current
+        )
+    )
+
+dispositionTransitionsOn
+  :: Ord label
+  => Set label
+  -> Map label (RegularSiteDisposition label)
+  -> Map label (RegularSiteDisposition label)
+  -> Vector.Vector (RegularSiteTransition label)
+dispositionTransitionsOn support previous current =
+  dispositionTransitions
+    (Map.restrictKeys previous support)
+    (Map.restrictKeys current support)
+
+changedDispositionTransition
+  :: Eq label
+  => label
+  -> RegularSiteDisposition label
+  -> RegularSiteDisposition label
+  -> Maybe (RegularSiteTransition label)
+changedDispositionTransition label before after
+  | before == after = Nothing
+  | otherwise = Just (RegularSiteTransitioned label before after)
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Generator.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Generator.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Generator.hs
@@ -0,0 +1,339 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact affine generators and their dual geometry. This module owns the
+-- algebra shared by batch hull construction, local edits, and bounded
+-- projection; it does not choose a construction schedule.
+module Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator (..)
+  , DistinctSlopeGenerators (..)
+  , RegularGeneratorDisposition (..)
+  , GeneratorRegularFace (..)
+  , PowerDualEdge (..)
+  , GeneratorRegularEdge (..)
+  , GeneratorRegularTopology (..)
+  , GeneratorRegularReceipt (..)
+  , RegularTopologyError (..)
+  , exactGeneratorAxis
+  , exactGeneratorLiftedVolume
+  , exactGeneratorLiftedSign
+  , exactGeneratorFaceDual
+  , exactGeneratorBoundaryDualRay
+  , exactGeneratorDualBetween
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactAffineLine
+  , ExactGeometryError
+  , ExactHalfPlaneError
+  , ExactIntersectionError
+  , ExactPoint
+  , ExactRay
+  , ExactSegment
+  , ExactVector (..)
+  , exactAffineLine
+  , exactAffineLineCoefficients
+  , exactAffineLineIntersection
+  , exactRay
+  , exactSegment
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRationalDenominator
+  , exactRationalNumerator
+  )
+import Moonlight.Planar.Convex (ConvexError)
+data ExactPowerGenerator label = ExactPowerGenerator
+  { exactPowerGeneratorLabel :: !label
+  , exactPowerGeneratorXCoefficient :: !ExactRational
+  , exactPowerGeneratorYCoefficient :: !ExactRational
+  , exactPowerGeneratorConstant :: !ExactRational
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Package-internal witness that coincident affine slopes were resolved once.
+newtype DistinctSlopeGenerators label =
+  DistinctSlopeGenerators (NonEmpty (ExactPowerGenerator label))
+
+data RegularGeneratorDisposition
+  = RegularGeneratorVisible
+  | RegularGeneratorLowerDimensional
+  | RegularGeneratorHidden
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data GeneratorRegularFace label = GeneratorRegularFace
+  { generatorRegularFaceFirst :: !label
+  , generatorRegularFaceSecond :: !label
+  , generatorRegularFaceThird :: !label
+  , generatorRegularFaceDualPoint :: !ExactPoint
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact weighted Voronoi geometry dual to one regular edge. Generator
+-- construction and public regular topology share this one carrier.
+data PowerDualEdge
+  = BoundedPowerDual !ExactSegment
+  | UnboundedPowerDual !ExactRay
+  | FullLinePowerDual !ExactAffineLine
+  | CollapsedPowerDual !ExactPoint
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data GeneratorRegularEdge label = GeneratorRegularEdge
+  { generatorRegularEdgeFirst :: !(ExactPowerGenerator label)
+  , generatorRegularEdgeSecond :: !(ExactPowerGenerator label)
+  , generatorRegularEdgeDual :: !PowerDualEdge
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data GeneratorRegularTopology label = GeneratorRegularTopology
+  { generatorRegularDispositions :: !(NonEmpty (ExactPowerGenerator label, RegularGeneratorDisposition))
+  , generatorRegularFaces :: ![GeneratorRegularFace label]
+  , generatorRegularEdges :: ![GeneratorRegularEdge label]
+  , generatorRegularReceipt :: !GeneratorRegularReceipt
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data GeneratorRegularReceipt = GeneratorRegularReceipt
+  { generatorRegularInputSites :: !Int
+  , generatorRegularVisibleSites :: !Int
+  , generatorRegularLowerDimensionalSites :: !Int
+  , generatorRegularHiddenSites :: !Int
+  , generatorRegularFaceCount :: !Int
+  , generatorRegularEdgeCount :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact slope-hull, incidence, and dual-construction obstructions.
+data RegularTopologyError label
+  = RegularTopologySlopeHullInvalid !ConvexError
+  | RegularTopologySlopeVertexMissing !ExactPoint
+  | RegularTopologyAxisInvalid !label !label !ExactHalfPlaneError
+  | RegularTopologyDualVertexInvalid !label !label !label !ExactIntersectionError
+  | RegularTopologyDualSegmentInvalid !label !label !ExactGeometryError
+  | RegularTopologyDualRayInvalid !label !label !ExactGeometryError
+  | RegularTopologyFlatBoundary !label !label !label
+  | RegularTopologyNonManifoldEdge !label !label !Int
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+exactGeneratorAxis
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either ExactHalfPlaneError ExactAffineLine
+exactGeneratorAxis firstGenerator secondGenerator =
+  exactAffineLine
+    ( exactPowerGeneratorXCoefficient firstGenerator
+        - exactPowerGeneratorXCoefficient secondGenerator
+    )
+    ( exactPowerGeneratorYCoefficient firstGenerator
+        - exactPowerGeneratorYCoefficient secondGenerator
+    )
+    ( exactPowerGeneratorConstant firstGenerator
+        - exactPowerGeneratorConstant secondGenerator
+    )
+
+-- | Signed exact lifted volume of four affine generators. For a
+-- counter-clockwise slope face, a positive result lies above its supporting
+-- plane and therefore belongs to the insertion conflict cavity.
+exactGeneratorLiftedVolume
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactRational
+exactGeneratorLiftedVolume firstGenerator secondGenerator thirdGenerator fourthGenerator =
+  let (firstX, firstY, firstZ) = generatorCoordinates firstGenerator
+      (secondX, secondY, secondZ) = generatorCoordinates secondGenerator
+      (thirdX, thirdY, thirdZ) = generatorCoordinates thirdGenerator
+      (fourthX, fourthY, fourthZ) = generatorCoordinates fourthGenerator
+      secondX' = secondX - firstX
+      secondY' = secondY - firstY
+      secondZ' = secondZ - firstZ
+      thirdX' = thirdX - firstX
+      thirdY' = thirdY - firstY
+      thirdZ' = thirdZ - firstZ
+      fourthX' = fourthX - firstX
+      fourthY' = fourthY - firstY
+      fourthZ' = fourthZ - firstZ
+   in secondX' * (thirdY' * fourthZ' - thirdZ' * fourthY')
+        - secondY' * (thirdX' * fourthZ' - thirdZ' * fourthX')
+        + secondZ' * (thirdX' * fourthY' - thirdY' * fourthX')
+
+-- | Sign of the lifted volume without normalizing rational intermediates.
+-- Each homogeneous row @(1,x,y,z)@ is multiplied by its own positive least
+-- common denominator. The determinant is therefore scaled by a positive
+-- product and keeps the upper-affine-hull sign convention of
+-- 'exactGeneratorLiftedVolume'. Magnitude consumers must retain that worker.
+exactGeneratorLiftedSign
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Ordering
+exactGeneratorLiftedSign firstGenerator secondGenerator thirdGenerator fourthGenerator =
+  let (firstD, firstX, firstY, firstZ) = integerRow firstGenerator
+      (secondD, secondX, secondY, secondZ) = integerRow secondGenerator
+      (thirdD, thirdX, thirdY, thirdZ) = integerRow thirdGenerator
+      (fourthD, fourthX, fourthY, fourthZ) = integerRow fourthGenerator
+      determinant =
+        if firstD == 1
+          then
+            -- A unit leading entry eliminates the homogeneous column with
+            -- no division or extra denominator factor. The six homogeneous
+            -- cofactor products allocate more on integral input than the
+            -- existing rational magnitude schedule.
+            let secondX' = secondX - firstX * secondD
+                secondY' = secondY - firstY * secondD
+                secondZ' = secondZ - firstZ * secondD
+                thirdX' = thirdX - firstX * thirdD
+                thirdY' = thirdY - firstY * thirdD
+                thirdZ' = thirdZ - firstZ * thirdD
+                fourthX' = fourthX - firstX * fourthD
+                fourthY' = fourthY - firstY * fourthD
+                fourthZ' = fourthZ - firstZ * fourthD
+             in secondX' * (thirdY' * fourthZ' - thirdZ' * fourthY')
+                  - secondY' * (thirdX' * fourthZ' - thirdZ' * fourthX')
+                  + secondZ' * (thirdX' * fourthY' - thirdY' * fourthX')
+          else
+            (firstD * secondX - firstX * secondD) * (thirdY * fourthZ - thirdZ * fourthY)
+              - (firstD * secondY - firstY * secondD) * (thirdX * fourthZ - thirdZ * fourthX)
+              + (firstD * secondZ - firstZ * secondD) * (thirdX * fourthY - thirdY * fourthX)
+              + (firstX * secondY - firstY * secondX) * (thirdD * fourthZ - thirdZ * fourthD)
+              - (firstX * secondZ - firstZ * secondX) * (thirdD * fourthY - thirdY * fourthD)
+              + (firstY * secondZ - firstZ * secondY) * (thirdD * fourthX - thirdX * fourthD)
+   in compare determinant 0
+  where
+    integerRow
+      :: ExactPowerGenerator rowLabel
+      -> (Integer, Integer, Integer, Integer)
+    integerRow generator =
+      let (coordinateX, coordinateY, coordinateZ) = generatorCoordinates generator
+          denominatorX = exactRationalDenominator coordinateX
+          denominatorY = exactRationalDenominator coordinateY
+          denominatorZ = exactRationalDenominator coordinateZ
+          denominator = lcm denominatorX (lcm denominatorY denominatorZ)
+       in if denominatorX == 1 && denominatorY == 1 && denominatorZ == 1
+            then
+              ( 1
+              , exactRationalNumerator coordinateX
+              , exactRationalNumerator coordinateY
+              , exactRationalNumerator coordinateZ
+              )
+            else
+              ( denominator
+              , exactRationalNumerator coordinateX * (denominator `quot` denominatorX)
+              , exactRationalNumerator coordinateY * (denominator `quot` denominatorY)
+              , exactRationalNumerator coordinateZ * (denominator `quot` denominatorZ)
+              )
+    {-# INLINE integerRow #-}
+
+-- | Exact dual vertex shared by three affine generators.
+exactGeneratorFaceDual
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either (RegularTopologyError label) ExactPoint
+exactGeneratorFaceDual firstGenerator secondGenerator thirdGenerator = do
+  firstAxis <-
+    first
+      (RegularTopologyAxisInvalid (exactPowerGeneratorLabel firstGenerator) (exactPowerGeneratorLabel secondGenerator))
+      (exactGeneratorAxis firstGenerator secondGenerator)
+  secondAxis <-
+    first
+      (RegularTopologyAxisInvalid (exactPowerGeneratorLabel firstGenerator) (exactPowerGeneratorLabel thirdGenerator))
+      (exactGeneratorAxis firstGenerator thirdGenerator)
+  first
+    ( RegularTopologyDualVertexInvalid
+        (exactPowerGeneratorLabel firstGenerator)
+        (exactPowerGeneratorLabel secondGenerator)
+        (exactPowerGeneratorLabel thirdGenerator)
+    )
+    (exactAffineLineIntersection firstAxis secondAxis)
+
+-- | Exact outward dual ray of an oriented boundary edge and its incident
+-- counter-clockwise face.
+exactGeneratorBoundaryDualRay
+  :: ExactPoint
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either (RegularTopologyError label) ExactRay
+exactGeneratorBoundaryDualRay originPoint firstGenerator secondGenerator thirdGenerator = do
+  axis <-
+    first
+      ( RegularTopologyAxisInvalid
+          (exactPowerGeneratorLabel firstGenerator)
+          (exactPowerGeneratorLabel secondGenerator)
+      )
+      (exactGeneratorAxis firstGenerator secondGenerator)
+  let (axisX, axisY, _) = exactAffineLineCoefficients axis
+      candidate = ExactVector axisY (negate axisX)
+      advantageX =
+        exactPowerGeneratorXCoefficient firstGenerator
+          - exactPowerGeneratorXCoefficient thirdGenerator
+      advantageY =
+        exactPowerGeneratorYCoefficient firstGenerator
+          - exactPowerGeneratorYCoefficient thirdGenerator
+      ExactVector candidateX candidateY = candidate
+      derivative = advantageX * candidateX + advantageY * candidateY
+      direction =
+        if derivative > 0
+          then candidate
+          else ExactVector (negate candidateX) (negate candidateY)
+  if derivative == 0
+    then
+      Left
+        ( RegularTopologyFlatBoundary
+            (exactPowerGeneratorLabel firstGenerator)
+            (exactPowerGeneratorLabel secondGenerator)
+            (exactPowerGeneratorLabel thirdGenerator)
+        )
+    else
+      first
+        ( RegularTopologyDualRayInvalid
+            (exactPowerGeneratorLabel firstGenerator)
+            (exactPowerGeneratorLabel secondGenerator)
+        )
+        (exactRay originPoint direction)
+
+-- | Canonical undirected dual geometry between two incident face vertices.
+exactGeneratorDualBetween
+  :: label
+  -> label
+  -> ExactPoint
+  -> ExactPoint
+  -> Either (RegularTopologyError label) PowerDualEdge
+exactGeneratorDualBetween firstLabel secondLabel firstPoint secondPoint
+  | firstPoint == secondPoint = Right (CollapsedPowerDual firstPoint)
+  | otherwise =
+      fmap BoundedPowerDual
+        ( first
+            (RegularTopologyDualSegmentInvalid firstLabel secondLabel)
+            ( uncurry exactSegment
+                ( if firstPoint <= secondPoint
+                    then (firstPoint, secondPoint)
+                    else (secondPoint, firstPoint)
+                )
+            )
+        )
+
+generatorCoordinates
+  :: ExactPowerGenerator label
+  -> (ExactRational, ExactRational, ExactRational)
+generatorCoordinates generator =
+  ( exactPowerGeneratorXCoefficient generator
+  , exactPowerGeneratorYCoefficient generator
+  , exactPowerGeneratorConstant generator
+  )
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Hull.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Hull.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Hull.hs
@@ -0,0 +1,925 @@
+-- | Exact upper-hull descent for affine generators. The public power owner
+-- supplies admitted, distinct slopes; this module returns only topology and
+-- exact dual geometry.
+module Moonlight.Planar.Internal.PowerDiagram.Hull
+  ( regularGeneratorTopology
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.Foldable as Foldable
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (mapMaybe)
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Moonlight.Planar.Exact
+  ( ExactAffineLine
+  , ExactPoint
+  , ExactRay
+  , exactPoint
+  , exactPointCoordinates
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (consecutivePairs)
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import Moonlight.Planar.Convex (convexHullPolygon, convexPolygonPoints)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+
+-- | Construct exact regular topology from distinct affine slopes. Full-rank
+-- input uses one conflict graph; collinear slopes use the one-dimensional
+-- upper chain; coplanar lifted input reuses the exact planar hull owner.
+regularGeneratorTopology
+  :: Ord label
+  => DistinctSlopeGenerators label
+  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
+regularGeneratorTopology (DistinctSlopeGenerators generators) =
+  let points = admittedLiftedPoints generators
+   in case points of
+        singleton :| [] ->
+          pure (topologyFromParts points (Set.singleton singleton) Set.empty [] [])
+        firstPoint :| secondPoint : remaining ->
+          case List.find (not . slopeCollinear firstPoint secondPoint) remaining of
+            Nothing -> collinearSlopeTopology points firstPoint secondPoint
+            Just thirdPoint ->
+              case List.find (not . liftedCoplanar firstPoint secondPoint thirdPoint) remaining of
+                Nothing -> coplanarLiftedTopology points
+                Just fourthPoint -> fullRankTopology points firstPoint secondPoint thirdPoint fourthPoint
+
+data LiftedPoint label = LiftedPoint
+  { liftedPointId :: !Int
+  , liftedPointGenerator :: !(ExactPowerGenerator label)
+  }
+
+instance Eq (LiftedPoint label) where
+  left == right = liftedPointId left == liftedPointId right
+
+instance Ord (LiftedPoint label) where
+  compare left right = compare (liftedPointId left) (liftedPointId right)
+
+admittedLiftedPoints
+  :: Ord label
+  => NonEmpty (ExactPowerGenerator label)
+  -> NonEmpty (LiftedPoint label)
+admittedLiftedPoints generators =
+  let ordered = NonEmpty.sortWith exactPowerGeneratorLabel generators
+   in NonEmpty.zipWith
+        (\identifier generator -> LiftedPoint identifier generator)
+        (0 :| [1 ..])
+        ordered
+
+liftedCoordinates
+  :: LiftedPoint label
+  -> (ExactRational, ExactRational, ExactRational)
+liftedCoordinates point =
+  let generator = liftedPointGenerator point
+   in ( exactPowerGeneratorXCoefficient generator
+      , exactPowerGeneratorYCoefficient generator
+      , exactPowerGeneratorConstant generator
+      )
+
+slopePoint :: LiftedPoint label -> ExactPoint
+slopePoint point =
+  let (coordinateX, coordinateY, _) = liftedCoordinates point
+   in exactPoint coordinateX coordinateY
+
+slopeCollinear
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> Bool
+slopeCollinear firstPoint secondPoint thirdPoint =
+  slopeOrientation firstPoint secondPoint thirdPoint == 0
+
+slopeOrientation
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> ExactRational
+slopeOrientation firstPoint secondPoint thirdPoint =
+  let (firstX, firstY, _) = liftedCoordinates firstPoint
+      (secondX, secondY, _) = liftedCoordinates secondPoint
+      (thirdX, thirdY, _) = liftedCoordinates thirdPoint
+   in (secondX - firstX) * (thirdY - firstY)
+        - (secondY - firstY) * (thirdX - firstX)
+
+liftedCoplanar
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> Bool
+liftedCoplanar firstPoint secondPoint thirdPoint fourthPoint =
+  exactGeneratorLiftedSign
+    (liftedPointGenerator firstPoint)
+    (liftedPointGenerator secondPoint)
+    (liftedPointGenerator thirdPoint)
+    (liftedPointGenerator fourthPoint)
+    == EQ
+
+liftedVolume
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> ExactRational
+liftedVolume firstPoint secondPoint thirdPoint fourthPoint =
+  exactGeneratorLiftedVolume
+    (liftedPointGenerator firstPoint)
+    (liftedPointGenerator secondPoint)
+    (liftedPointGenerator thirdPoint)
+    (liftedPointGenerator fourthPoint)
+
+data HullFace label = HullFace
+  { hullFaceId :: !Int
+  , hullFaceFirst :: !(LiftedPoint label)
+  , hullFaceSecond :: !(LiftedPoint label)
+  , hullFaceThird :: !(LiftedPoint label)
+  , hullFaceOutside :: !(Map (LiftedPoint label) ExactRational)
+    -- ^ Each outside point with its exact lifted volume against this face.
+  , hullFaceCoplanar :: !(Set (LiftedPoint label))
+  }
+
+type HullEdge = (Int, Int)
+
+data HullState label = HullState
+  { hullFaces :: !(IntMap.IntMap (HullFace label))
+  , hullEdgeFaces :: !(Map HullEdge IntSet.IntSet)
+  , hullConflictFaces :: !IntSet.IntSet
+  , hullNextFaceId :: !Int
+  }
+
+fullRankTopology
+  :: NonEmpty (LiftedPoint label)
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
+fullRankTopology points firstPoint secondPoint thirdPoint fourthPoint = do
+  let interior = liftedInteriorSum firstPoint secondPoint thirdPoint fourthPoint
+      simplex = Set.fromList [firstPoint, secondPoint, thirdPoint, fourthPoint]
+      initialFaces =
+        zipWith
+          (orientedHullFace interior)
+          [0 ..]
+          [ (firstPoint, secondPoint, thirdPoint)
+          , (firstPoint, fourthPoint, secondPoint)
+          , (firstPoint, thirdPoint, fourthPoint)
+          , (secondPoint, fourthPoint, thirdPoint)
+          ]
+      initialState =
+        List.foldl'
+          insertHullFace
+          (HullState IntMap.empty Map.empty IntSet.empty 4)
+          initialFaces
+      seededState =
+        Foldable.foldl'
+          (flip assignPointToHull)
+          initialState
+          (filter (`Set.notMember` simplex) (NonEmpty.toList points))
+      finalState = descendHull interior seededState
+      upperFaces = filter ((> 0) . hullFaceNormalZ) (IntMap.elems (hullFaces finalState))
+  (canonicalFaces, visible, lowerDimensional) <- canonicalUpperFaces upperFaces
+  (faces, edges) <- topologyFromHullFaces canonicalFaces
+  pure
+    (topologyFromParts points visible lowerDimensional faces edges)
+
+liftedInteriorSum
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> (ExactRational, ExactRational, ExactRational)
+liftedInteriorSum firstPoint secondPoint thirdPoint fourthPoint =
+  let (firstX, firstY, firstZ) = liftedCoordinates firstPoint
+      (secondX, secondY, secondZ) = liftedCoordinates secondPoint
+      (thirdX, thirdY, thirdZ) = liftedCoordinates thirdPoint
+      (fourthX, fourthY, fourthZ) = liftedCoordinates fourthPoint
+   in ( firstX + secondX + thirdX + fourthX
+      , firstY + secondY + thirdY + fourthY
+      , firstZ + secondZ + thirdZ + fourthZ
+      )
+
+orientedHullFace
+  :: (ExactRational, ExactRational, ExactRational)
+  -> Int
+  -> (LiftedPoint label, LiftedPoint label, LiftedPoint label)
+  -> HullFace label
+orientedHullFace interior identifier (firstPoint, secondPoint, thirdPoint) =
+  let provisional = HullFace identifier firstPoint secondPoint thirdPoint Map.empty Set.empty
+   in if hullFaceVolumeAtInterior interior provisional < 0
+        then provisional
+        else HullFace identifier firstPoint thirdPoint secondPoint Map.empty Set.empty
+
+hullFaceVolumeAtInterior
+  :: (ExactRational, ExactRational, ExactRational)
+  -> HullFace label
+  -> ExactRational
+hullFaceVolumeAtInterior (sumX, sumY, sumZ) face =
+  let (firstX, firstY, firstZ) = liftedCoordinates (hullFaceFirst face)
+      (secondX, secondY, secondZ) = liftedCoordinates (hullFaceSecond face)
+      (thirdX, thirdY, thirdZ) = liftedCoordinates (hullFaceThird face)
+      secondX' = secondX - firstX
+      secondY' = secondY - firstY
+      secondZ' = secondZ - firstZ
+      thirdX' = thirdX - firstX
+      thirdY' = thirdY - firstY
+      thirdZ' = thirdZ - firstZ
+      interiorX' = sumX - 4 * firstX
+      interiorY' = sumY - 4 * firstY
+      interiorZ' = sumZ - 4 * firstZ
+   in secondX' * (thirdY' * interiorZ' - thirdZ' * interiorY')
+        - secondY' * (thirdX' * interiorZ' - thirdZ' * interiorX')
+        + secondZ' * (thirdX' * interiorY' - thirdY' * interiorX')
+
+liftedVolumeAtFace :: HullFace label -> LiftedPoint label -> ExactRational
+liftedVolumeAtFace face =
+  liftedVolume
+    (hullFaceFirst face)
+    (hullFaceSecond face)
+    (hullFaceThird face)
+
+hullFaceNormalZ :: HullFace label -> ExactRational
+hullFaceNormalZ face =
+  slopeOrientation
+    (hullFaceFirst face)
+    (hullFaceSecond face)
+    (hullFaceThird face)
+
+hullFacePoints :: HullFace label -> [LiftedPoint label]
+hullFacePoints face =
+  [hullFaceFirst face, hullFaceSecond face, hullFaceThird face]
+
+hullFaceDirectedEdges
+  :: HullFace label
+  -> [(LiftedPoint label, LiftedPoint label)]
+hullFaceDirectedEdges face =
+  [ (hullFaceFirst face, hullFaceSecond face)
+  , (hullFaceSecond face, hullFaceThird face)
+  , (hullFaceThird face, hullFaceFirst face)
+  ]
+
+hullEdgeKey :: LiftedPoint label -> LiftedPoint label -> HullEdge
+hullEdgeKey firstPoint secondPoint =
+  let firstId = liftedPointId firstPoint
+      secondId = liftedPointId secondPoint
+   in if firstId <= secondId then (firstId, secondId) else (secondId, firstId)
+
+insertHullFace :: HullState label -> HullFace label -> HullState label
+insertHullFace state face =
+  state
+    { hullFaces = IntMap.insert (hullFaceId face) face (hullFaces state)
+    , hullEdgeFaces =
+        List.foldl'
+          (\incidence (fromPoint, toPoint) ->
+             Map.insertWith
+               IntSet.union
+               (hullEdgeKey fromPoint toPoint)
+               (IntSet.singleton (hullFaceId face))
+               incidence)
+          (hullEdgeFaces state)
+          (hullFaceDirectedEdges face)
+    }
+
+assignPointToHull :: LiftedPoint label -> HullState label -> HullState label
+assignPointToHull point state =
+  attachSelectedPoint point state
+    (selectHullFace point (IntMap.elems (hullFaces state)))
+
+data HullFaceSelection label
+  = HullFaceInterior
+  | HullFaceCoplanar !(HullFace label)
+  | HullFaceVisible !(HullFace label) !ExactRational
+
+selectHullFace
+  :: LiftedPoint label
+  -> [HullFace label]
+  -> HullFaceSelection label
+selectHullFace point = List.foldl' choose HullFaceInterior
+ where
+  choose selected face =
+    let volume = liftedVolumeAtFace face point
+     in case compare volume 0 of
+          LT -> selected
+          EQ ->
+            case selected of
+              HullFaceInterior -> HullFaceCoplanar face
+              _ -> selected
+          GT ->
+            case selected of
+              HullFaceVisible current currentVolume
+                | volume < currentVolume
+                    || (volume == currentVolume && hullFaceId current < hullFaceId face) ->
+                    selected
+              _ -> HullFaceVisible face volume
+
+attachSelectedPoint
+  :: LiftedPoint label
+  -> HullState label
+  -> HullFaceSelection label
+  -> HullState label
+attachSelectedPoint point state selection =
+  case selection of
+    HullFaceInterior -> state
+    HullFaceCoplanar face ->
+      state
+        { hullFaces =
+            IntMap.adjust
+              (\selected -> selected{hullFaceCoplanar = Set.insert point (hullFaceCoplanar selected)})
+              (hullFaceId face)
+              (hullFaces state)
+        }
+    HullFaceVisible face volume ->
+      state
+        { hullFaces =
+            IntMap.adjust
+              (\selected -> selected{hullFaceOutside = Map.insert point volume (hullFaceOutside selected)})
+              (hullFaceId face)
+              (hullFaces state)
+        , hullConflictFaces = IntSet.insert (hullFaceId face) (hullConflictFaces state)
+        }
+
+descendHull
+  :: (ExactRational, ExactRational, ExactRational)
+  -> HullState label
+  -> HullState label
+descendHull interior state =
+  case nextHullExpansion state of
+    Nothing -> state
+    Just (seedFace, apex) ->
+      let visible = visibleHullPatch state apex (IntSet.singleton (hullFaceId seedFace)) [hullFaceId seedFace]
+          visibleFaces =
+            mapMaybe (`IntMap.lookup` hullFaces state) (IntSet.toAscList visible)
+          horizon = hullHorizon state visible visibleFaces
+          candidates =
+            Set.difference
+              ( Set.delete apex
+                  ( Set.unions
+                      [ Set.unions
+                          [ Map.keysSet (hullFaceOutside face)
+                          , hullFaceCoplanar face
+                          , Set.fromList (hullFacePoints face)
+                          ]
+                      | face <- visibleFaces
+                      ]
+                  )
+              )
+              ( Set.fromList
+                  [ point
+                  | (firstPoint, secondPoint) <- Map.elems horizon
+                  , point <- [firstPoint, secondPoint]
+                  ]
+              )
+          withoutVisible = removeHullFaces visibleFaces state
+          (withHorizon, newFaceIds) =
+            Map.foldl'
+              (insertHorizonFace interior apex)
+              (withoutVisible, [])
+              horizon
+          redistributed =
+            Foldable.foldl'
+              (assignPointToFaces newFaceIds)
+              withHorizon
+              candidates
+       in descendHull interior redistributed
+
+nextHullExpansion
+  :: HullState label
+  -> Maybe (HullFace label, LiftedPoint label)
+nextHullExpansion state = do
+  identifier <- IntSet.lookupMin (hullConflictFaces state)
+  face <- IntMap.lookup identifier (hullFaces state)
+  apex <- farthestOutsidePoint face
+  pure (face, apex)
+
+-- | The lowest-id point among those of maximal stored volume.
+farthestOutsidePoint :: HullFace label -> Maybe (LiftedPoint label)
+farthestOutsidePoint face =
+  fst
+    <$> Map.foldlWithKey'
+      (\selected candidate candidateVolume ->
+         case selected of
+           Just (_, selectedVolume)
+             | candidateVolume <= selectedVolume -> selected
+           _ -> Just (candidate, candidateVolume))
+      Nothing
+      (hullFaceOutside face)
+
+visibleHullPatch
+  :: HullState label
+  -> LiftedPoint label
+  -> IntSet.IntSet
+  -> [Int]
+  -> IntSet.IntSet
+visibleHullPatch _ _ visited [] = visited
+visibleHullPatch state apex visited (identifier : remaining) =
+  case IntMap.lookup identifier (hullFaces state) of
+    Nothing -> visibleHullPatch state apex visited remaining
+    Just face ->
+      let neighbours =
+            IntSet.unions
+              [ Map.findWithDefault IntSet.empty (hullEdgeKey fromPoint toPoint) (hullEdgeFaces state)
+              | (fromPoint, toPoint) <- hullFaceDirectedEdges face
+              ]
+          newlyVisible =
+            IntSet.filter
+              (\candidateId ->
+                 IntSet.notMember candidateId visited
+                   && maybe
+                     False
+                     ((> 0) . (`liftedVolumeAtFace` apex))
+                     (IntMap.lookup candidateId (hullFaces state)))
+              neighbours
+       in visibleHullPatch
+            state
+            apex
+            (IntSet.union visited newlyVisible)
+            (IntSet.toList newlyVisible <> remaining)
+
+hullHorizon
+  :: HullState label
+  -> IntSet.IntSet
+  -> [HullFace label]
+  -> Map HullEdge (LiftedPoint label, LiftedPoint label)
+hullHorizon state visible visibleFaces =
+  Map.fromList
+    [ (hullEdgeKey fromPoint toPoint, (fromPoint, toPoint))
+    | face <- visibleFaces
+    , (fromPoint, toPoint) <- hullFaceDirectedEdges face
+    , not
+        ( IntSet.null
+            ( IntSet.difference
+                (Map.findWithDefault IntSet.empty (hullEdgeKey fromPoint toPoint) (hullEdgeFaces state))
+                visible
+            )
+        )
+    ]
+
+removeHullFaces :: [HullFace label] -> HullState label -> HullState label
+removeHullFaces removed state = List.foldl' removeHullFace state removed
+
+removeHullFace :: HullState label -> HullFace label -> HullState label
+removeHullFace state face =
+  state
+    { hullFaces = IntMap.delete identifier (hullFaces state)
+    , hullEdgeFaces =
+        List.foldl'
+          (removeHullFaceFromEdge identifier)
+          (hullEdgeFaces state)
+          (hullFaceDirectedEdges face)
+    , hullConflictFaces = IntSet.delete identifier (hullConflictFaces state)
+    }
+ where
+  identifier = hullFaceId face
+
+removeHullFaceFromEdge
+  :: Int
+  -> Map HullEdge IntSet.IntSet
+  -> (LiftedPoint label, LiftedPoint label)
+  -> Map HullEdge IntSet.IntSet
+removeHullFaceFromEdge identifier incidence (fromPoint, toPoint) =
+  Map.update
+    (\incident ->
+       let retained = IntSet.delete identifier incident
+        in if IntSet.null retained then Nothing else Just retained)
+    (hullEdgeKey fromPoint toPoint)
+    incidence
+
+insertHorizonFace
+  :: (ExactRational, ExactRational, ExactRational)
+  -> LiftedPoint label
+  -> (HullState label, [Int])
+  -> (LiftedPoint label, LiftedPoint label)
+  -> (HullState label, [Int])
+insertHorizonFace interior apex (state, identifiers) (firstPoint, secondPoint) =
+  let identifier = hullNextFaceId state
+      face = orientedHullFace interior identifier (firstPoint, secondPoint, apex)
+      nextState =
+        (insertHullFace state face)
+          { hullNextFaceId = identifier + 1 }
+   in (nextState, identifier : identifiers)
+
+assignPointToFaces
+  :: [Int]
+  -> HullState label
+  -> LiftedPoint label
+  -> HullState label
+assignPointToFaces identifiers state point =
+  attachSelectedPoint point state
+    (selectHullFace point (mapMaybe (`IntMap.lookup` hullFaces state) identifiers))
+
+-- | Glue the triangular implementation facets on each exact supporting plane,
+-- then recover only that plane's extreme slope vertices. This removes
+-- simplex/horizon diagonals from the visibility decision while retaining a
+-- deterministic triangulation for incidence and collapsed duals.
+canonicalUpperFaces
+  :: [HullFace label]
+  -> Either
+      (RegularTopologyError label)
+      ( [(HullFace label, ExactPoint)]
+      , Set (LiftedPoint label)
+      , Set (LiftedPoint label)
+      )
+canonicalUpperFaces faces = do
+  associations <- traverse upperFaceAssociation faces
+  sections <-
+    traverse canonicalUpperFaceGroup
+      ( Map.elems
+          ( List.foldl'
+              (\groups (planeKey, dualFace) ->
+                 Map.insertWith (<>) planeKey (dualFace :| []) groups)
+              Map.empty
+              associations
+          )
+      )
+  let (sectionFaces, visible, lowerDimensional) = mconcat sections
+      canonicalFaces =
+        zipWith
+          (\identifier (face, dualPoint) ->
+             (face{hullFaceId = identifier}, dualPoint))
+          [0 ..]
+          sectionFaces
+  pure (canonicalFaces, visible, lowerDimensional)
+
+canonicalUpperFaceGroup
+  :: NonEmpty (HullFace label, ExactPoint)
+  -> Either
+      (RegularTopologyError label)
+      ( [(HullFace label, ExactPoint)]
+      , Set (LiftedPoint label)
+      , Set (LiftedPoint label)
+      )
+canonicalUpperFaceGroup ((face, dualPoint) :| [])
+  | Set.null (hullFaceCoplanar face) =
+      Right ([(face, dualPoint)], Set.fromList (hullFacePoints face), Set.empty)
+canonicalUpperFaceGroup ((firstFace, dualPoint) :| remainingDualFaces) = do
+  let groupFaces = firstFace : fmap fst remainingDualFaces
+      groupPoints =
+        Set.unions
+          [ Set.union
+              (Set.fromList (hullFacePoints face))
+              (hullFaceCoplanar face)
+          | face <- groupFaces
+          ]
+      initialPoint = hullFaceFirst firstFace
+      points = initialPoint :| Set.toAscList (Set.delete initialPoint groupPoints)
+      pointsBySlope =
+        Map.fromList [(slopePoint point, point) | point <- NonEmpty.toList points]
+  hull <-
+    first RegularTopologySlopeHullInvalid
+      (convexHullPolygon (fmap slopePoint points))
+  visiblePoints <-
+    traverse
+      (\point ->
+         maybe
+           (Left (RegularTopologySlopeVertexMissing point))
+           Right
+           (Map.lookup point pointsBySlope))
+      (NonEmpty.toList (convexPolygonPoints hull))
+  let visible = Set.fromList visiblePoints
+  pure
+    ( fmap (\fanFace -> (fanFace, dualPoint)) (coplanarFanFaces visiblePoints)
+    , visible
+    , Set.difference groupPoints visible
+    )
+
+hullFacePlaneKey
+  :: HullFace label
+  -> ExactPoint
+  -> (ExactRational, ExactRational, ExactRational)
+hullFacePlaneKey face dualPoint =
+  let (dualX, dualY) = exactPointCoordinates dualPoint
+      generator = liftedPointGenerator (hullFaceFirst face)
+      support =
+        exactPowerGeneratorXCoefficient generator * dualX
+          + exactPowerGeneratorYCoefficient generator * dualY
+          + exactPowerGeneratorConstant generator
+   in (dualX, dualY, support)
+
+upperFaceAssociation
+  :: HullFace label
+  -> Either
+      (RegularTopologyError label)
+      ( (ExactRational, ExactRational, ExactRational)
+      , (HullFace label, ExactPoint)
+      )
+upperFaceAssociation face = do
+  (_, dualPoint) <- hullFaceDual face
+  pure (hullFacePlaneKey face dualPoint, (face, dualPoint))
+
+coplanarLiftedTopology
+  :: NonEmpty (LiftedPoint label)
+  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
+coplanarLiftedTopology points = do
+  hull <-
+    first RegularTopologySlopeHullInvalid
+      (convexHullPolygon (fmap slopePoint points))
+  let pointsBySlope = Map.fromList [(slopePoint point, point) | point <- NonEmpty.toList points]
+  hullPoints <-
+    traverse
+      (\point -> maybe (Left (RegularTopologySlopeVertexMissing point)) Right (Map.lookup point pointsBySlope))
+      (NonEmpty.toList (convexPolygonPoints hull))
+  let visible = Set.fromList hullPoints
+      lowerDimensional = Set.difference (Set.fromList (NonEmpty.toList points)) visible
+      hullFaces' = coplanarFanFaces hullPoints
+  dualFaces <- coplanarFanDualFaces hullFaces'
+  (faces, edges) <- topologyFromHullFaces dualFaces
+  pure
+    (topologyFromParts points visible lowerDimensional faces edges)
+
+coplanarFanDualFaces
+  :: [HullFace label]
+  -> Either
+      (RegularTopologyError label)
+      [(HullFace label, ExactPoint)]
+coplanarFanDualFaces [] = Right []
+coplanarFanDualFaces faces@(firstFace : _) = do
+  (_, dualPoint) <- hullFaceDual firstFace
+  pure (fmap (\face -> (face, dualPoint)) faces)
+
+coplanarFanFaces :: [LiftedPoint label] -> [HullFace label]
+coplanarFanFaces (firstPoint : secondPoint : thirdPoint : remaining) =
+  zipWith
+    (\identifier (leftPoint, rightPoint) ->
+       HullFace identifier firstPoint leftPoint rightPoint Map.empty Set.empty)
+    [0 ..]
+    (consecutivePairs (secondPoint : thirdPoint : remaining))
+coplanarFanFaces _ = []
+
+collinearSlopeTopology
+  :: NonEmpty (LiftedPoint label)
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
+collinearSlopeTopology points firstPoint secondPoint = do
+  let direction = slopeDifference firstPoint secondPoint
+      ordered = List.sortOn (slopeParameter direction) (NonEmpty.toList points)
+      visibleList = upperSlopeChain direction ordered
+      visible = Set.fromList visibleList
+      lowerDimensional =
+        Set.difference
+          (upperChainPointSet direction visibleList ordered)
+          visible
+  edges <- traverse fullLineEdge (consecutivePairs visibleList)
+  pure (topologyFromParts points visible lowerDimensional [] edges)
+
+slopeDifference
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> (ExactRational, ExactRational)
+slopeDifference firstPoint secondPoint =
+  let (firstX, firstY, _) = liftedCoordinates firstPoint
+      (secondX, secondY, _) = liftedCoordinates secondPoint
+   in (secondX - firstX, secondY - firstY)
+
+slopeParameter
+  :: (ExactRational, ExactRational)
+  -> LiftedPoint label
+  -> ExactRational
+slopeParameter (directionX, directionY) point =
+  let (coordinateX, coordinateY, _) = liftedCoordinates point
+   in directionX * coordinateX + directionY * coordinateY
+
+upperSlopeChain
+  :: (ExactRational, ExactRational)
+  -> [LiftedPoint label]
+  -> [LiftedPoint label]
+upperSlopeChain direction = reverse . List.foldl' insertUpper []
+ where
+  insertUpper
+    :: [LiftedPoint label]
+    -> LiftedPoint label
+    -> [LiftedPoint label]
+  insertUpper (secondPoint : firstPoint : remaining) candidate
+    | slopeChainTurn direction firstPoint secondPoint candidate >= 0 =
+        insertUpper (firstPoint : remaining) candidate
+  insertUpper retained candidate = candidate : retained
+
+slopeChainTurn
+  :: (ExactRational, ExactRational)
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> ExactRational
+slopeChainTurn direction firstPoint secondPoint thirdPoint =
+  let firstT = slopeParameter direction firstPoint
+      secondT = slopeParameter direction secondPoint
+      thirdT = slopeParameter direction thirdPoint
+      (_, _, firstConstant) = liftedCoordinates firstPoint
+      (_, _, secondConstant) = liftedCoordinates secondPoint
+      (_, _, thirdConstant) = liftedCoordinates thirdPoint
+   in (secondT - firstT) * (thirdConstant - firstConstant)
+        - (secondConstant - firstConstant) * (thirdT - firstT)
+
+upperChainPointSet
+  :: (ExactRational, ExactRational)
+  -> [LiftedPoint label]
+  -> [LiftedPoint label]
+  -> Set (LiftedPoint label)
+upperChainPointSet direction chain =
+  snd
+    . List.foldl'
+        classifyPoint
+        (consecutivePairs chain, Set.empty)
+ where
+  classifyPoint
+    :: ([(LiftedPoint label, LiftedPoint label)], Set (LiftedPoint label))
+    -> LiftedPoint label
+    -> ([(LiftedPoint label, LiftedPoint label)], Set (LiftedPoint label))
+  classifyPoint (remainingEdges, members) point =
+    let pointParameter = slopeParameter direction point
+        candidateEdges =
+          List.dropWhile
+            ((< pointParameter) . slopeParameter direction . snd)
+            remainingEdges
+        retainedMembers =
+          case candidateEdges of
+            (firstPoint, secondPoint) : _
+              | slopeParameter direction firstPoint <= pointParameter
+                  && slopeChainTurn direction firstPoint secondPoint point == 0 ->
+                  Set.insert point members
+            _ -> members
+     in (candidateEdges, retainedMembers)
+
+fullLineEdge
+  :: (LiftedPoint label, LiftedPoint label)
+  -> Either (RegularTopologyError label) (GeneratorRegularEdge label)
+fullLineEdge (firstPoint, secondPoint) = do
+  axis <- generatorAxis firstPoint secondPoint
+  pure (regularEdge firstPoint secondPoint (FullLinePowerDual axis))
+
+topologyFromHullFaces
+  :: [(HullFace label, ExactPoint)]
+  -> Either
+      (RegularTopologyError label)
+      ([GeneratorRegularFace label], [GeneratorRegularEdge label])
+topologyFromHullFaces dualFaces = do
+  let dualByFace = IntMap.fromList [(hullFaceId face, dual) | (face, dual) <- dualFaces]
+      edgeIncidence =
+        List.foldl'
+          (\incidence (face, _) ->
+             List.foldl'
+               (\current (firstPoint, secondPoint, thirdPoint) ->
+                  Map.insertWith
+                    (<>)
+                    (hullEdgeKey firstPoint secondPoint)
+                    ((face, firstPoint, secondPoint, thirdPoint) :| [])
+                    current)
+               incidence
+               (faceEdgesWithOpposite face))
+          Map.empty
+          dualFaces
+  edges <- traverse (dualEdge dualByFace) (Map.elems edgeIncidence)
+  pure
+    ( [ GeneratorRegularFace
+          (pointLabel (hullFaceFirst face))
+          (pointLabel (hullFaceSecond face))
+          (pointLabel (hullFaceThird face))
+          dual
+      | (face, dual) <- dualFaces
+      ]
+    , edges
+    )
+
+hullFaceDual
+  :: HullFace label
+  -> Either (RegularTopologyError label) (HullFace label, ExactPoint)
+hullFaceDual face = do
+  let firstPoint = hullFaceFirst face
+      secondPoint = hullFaceSecond face
+      thirdPoint = hullFaceThird face
+  dual <-
+    exactGeneratorFaceDual
+      (liftedPointGenerator firstPoint)
+      (liftedPointGenerator secondPoint)
+      (liftedPointGenerator thirdPoint)
+  pure (face, dual)
+
+faceEdgesWithOpposite
+  :: HullFace label
+  -> [(LiftedPoint label, LiftedPoint label, LiftedPoint label)]
+faceEdgesWithOpposite face =
+  [ (hullFaceFirst face, hullFaceSecond face, hullFaceThird face)
+  , (hullFaceSecond face, hullFaceThird face, hullFaceFirst face)
+  , (hullFaceThird face, hullFaceFirst face, hullFaceSecond face)
+  ]
+
+dualEdge
+  :: IntMap.IntMap ExactPoint
+  -> NonEmpty (HullFace label, LiftedPoint label, LiftedPoint label, LiftedPoint label)
+  -> Either (RegularTopologyError label) (GeneratorRegularEdge label)
+dualEdge dualByFace incidences =
+  case incidences of
+    (face, firstPoint, secondPoint, thirdPoint) :| [] -> do
+      dual <-
+        maybe
+          (Left (RegularTopologyFlatBoundary (pointLabel firstPoint) (pointLabel secondPoint) (pointLabel thirdPoint)))
+          Right
+          (IntMap.lookup (hullFaceId face) dualByFace)
+      ray <- boundaryDualRay dual firstPoint secondPoint thirdPoint
+      pure (regularEdge firstPoint secondPoint (UnboundedPowerDual ray))
+    (firstFace, firstPoint, secondPoint, _) :| [(secondFace, _, _, _)] -> do
+        firstDual <- requiredDual firstPoint secondPoint firstFace
+        secondDual <- requiredDual firstPoint secondPoint secondFace
+        dual <-
+          exactGeneratorDualBetween
+            (pointLabel firstPoint)
+            (pointLabel secondPoint)
+            firstDual
+            secondDual
+        pure (regularEdge firstPoint secondPoint dual)
+    (_, firstPoint, secondPoint, _) :| _ ->
+      Left
+        ( RegularTopologyNonManifoldEdge
+            (pointLabel firstPoint)
+            (pointLabel secondPoint)
+            (NonEmpty.length incidences)
+        )
+ where
+  requiredDual
+    :: LiftedPoint label
+    -> LiftedPoint label
+    -> HullFace label
+    -> Either (RegularTopologyError label) ExactPoint
+  requiredDual firstPoint secondPoint face =
+    maybe
+      (Left (RegularTopologyFlatBoundary (pointLabel firstPoint) (pointLabel secondPoint) (pointLabel (hullFaceThird face))))
+      Right
+      (IntMap.lookup (hullFaceId face) dualByFace)
+
+boundaryDualRay
+  :: ExactPoint
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> LiftedPoint label
+  -> Either (RegularTopologyError label) ExactRay
+boundaryDualRay originPoint firstPoint secondPoint thirdPoint = do
+  exactGeneratorBoundaryDualRay
+    originPoint
+    (liftedPointGenerator firstPoint)
+    (liftedPointGenerator secondPoint)
+    (liftedPointGenerator thirdPoint)
+
+generatorAxis
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> Either (RegularTopologyError label) ExactAffineLine
+generatorAxis firstPoint secondPoint =
+  first
+    (RegularTopologyAxisInvalid (pointLabel firstPoint) (pointLabel secondPoint))
+    (exactGeneratorAxis (liftedPointGenerator firstPoint) (liftedPointGenerator secondPoint))
+
+regularEdge
+  :: LiftedPoint label
+  -> LiftedPoint label
+  -> PowerDualEdge
+  -> GeneratorRegularEdge label
+regularEdge firstPoint secondPoint dual =
+  if liftedPointId firstPoint <= liftedPointId secondPoint
+    then GeneratorRegularEdge (liftedPointGenerator firstPoint) (liftedPointGenerator secondPoint) dual
+    else GeneratorRegularEdge (liftedPointGenerator secondPoint) (liftedPointGenerator firstPoint) dual
+
+pointLabel :: LiftedPoint label -> label
+pointLabel = exactPowerGeneratorLabel . liftedPointGenerator
+
+topologyFromParts
+  :: NonEmpty (LiftedPoint label)
+  -> Set (LiftedPoint label)
+  -> Set (LiftedPoint label)
+  -> [GeneratorRegularFace label]
+  -> [GeneratorRegularEdge label]
+  -> GeneratorRegularTopology label
+topologyFromParts points visible lowerDimensional faces edges =
+  let dispositions =
+        fmap
+          (\point ->
+             ( liftedPointGenerator point
+             , if Set.member point visible
+                 then RegularGeneratorVisible
+                 else
+                   if Set.member point lowerDimensional
+                     then RegularGeneratorLowerDimensional
+                     else RegularGeneratorHidden
+             ))
+          points
+      visibleCount = Set.size visible
+      lowerCount = Set.size lowerDimensional
+      inputCount = NonEmpty.length points
+   in GeneratorRegularTopology
+        { generatorRegularDispositions = dispositions
+        , generatorRegularFaces = faces
+        , generatorRegularEdges = edges
+        , generatorRegularReceipt =
+            GeneratorRegularReceipt
+              { generatorRegularInputSites = inputCount
+              , generatorRegularVisibleSites = visibleCount
+              , generatorRegularLowerDimensionalSites = lowerCount
+              , generatorRegularHiddenSites = inputCount - visibleCount - lowerCount
+              , generatorRegularFaceCount = length faces
+              , generatorRegularEdgeCount = length edges
+              }
+        }
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Locality.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Locality.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Locality.hs
@@ -0,0 +1,235 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | The discardable hidden-support index and exact face walk for one regular section.
+module Moonlight.Planar.Internal.PowerDiagram.Locality where
+
+import qualified Data.Foldable as Foldable
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , exactOrient2d
+  , exactPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Model
+
+patchRegularLocality
+  :: Ord label
+  => RegularLocality label
+  -> Set (RegularFaceKey label)
+  -> Set label
+  -> RegularSection label
+  -> Either (LocalRegularEditObstruction label) (RegularLocality label)
+patchRegularLocality locality removedFaces hiddenToAttach section = do
+  seed <-
+    case Map.lookupMin (sectionRegularFaces section) of
+      Nothing -> Left LocalRegularLocalityUnavailable
+      Just (minimumFace, _) ->
+        Right
+          ( if Map.member (localityFaceSeed locality) (sectionRegularFaces section)
+              then localityFaceSeed locality
+              else minimumFace
+          )
+  let detachedForFaces =
+        Set.unions
+          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
+          | faceKey <- Set.toAscList removedFaces
+          ]
+      detachedLabels = Set.union detachedForFaces hiddenToAttach
+      detached = Set.foldl' (flip detachRegularHidden) locality detachedLabels
+  attachments <-
+    traverse
+      (locateHiddenSupport seed section)
+      (Set.toAscList hiddenToAttach)
+  pure
+    ( Foldable.foldl'
+        (\current (label, faceKey) -> attachRegularHidden label faceKey current)
+        detached{localityFaceSeed = seed}
+        attachments
+    )
+
+locateHiddenSupport
+  :: Ord label
+  => RegularFaceKey label
+  -> RegularSection label
+  -> label
+  -> Either (LocalRegularEditObstruction label) (label, RegularFaceKey label)
+locateHiddenSupport seed section label = do
+  generator <- requireGeneratorFromMap (sectionGenerators section) label
+  location <-
+    maybe (Left LocalRegularSlopeLocationFailed) Right
+      (locateRegularSlope seed section generator)
+  case location of
+    RegularSlopeInside faceKey -> Right (label, faceKey)
+    RegularSlopeOutside edgeKey -> Left (LocalRegularSlopeOutside edgeKey)
+
+attachRegularHidden
+  :: Ord label
+  => label
+  -> RegularFaceKey label
+  -> RegularLocality label
+  -> RegularLocality label
+attachRegularHidden label faceKey locality =
+  locality
+    { localityHiddenSupport = Map.insert label faceKey (localityHiddenSupport locality)
+    , localitySupportHidden =
+        Map.insertWith Set.union faceKey (Set.singleton label) (localitySupportHidden locality)
+    }
+
+detachRegularHidden
+  :: Ord label
+  => label
+  -> RegularLocality label
+  -> RegularLocality label
+detachRegularHidden label locality =
+  case Map.lookup label (localityHiddenSupport locality) of
+    Nothing -> locality
+    Just faceKey ->
+      locality
+        { localityHiddenSupport = Map.delete label (localityHiddenSupport locality)
+        , localitySupportHidden =
+            Map.update
+              (\labels ->
+                 let retained = Set.delete label labels
+                  in if Set.null retained then Nothing else Just retained)
+              faceKey
+              (localitySupportHidden locality)
+        }
+
+buildRegularLocality
+  :: Ord label
+  => RegularSection label
+  -> Maybe (RegularLocality label)
+buildRegularLocality section = do
+  (seed, _) <- Map.lookupMin (sectionRegularFaces section)
+  Foldable.foldlM
+    (attachHiddenGenerator section)
+    RegularLocality
+      { localityFaceSeed = seed
+      , localityHiddenSupport = Map.empty
+      , localitySupportHidden = Map.empty
+      }
+    [ label
+    | (_, label) <- Map.toAscList (sectionSlopeRepresentatives section)
+    , Map.lookup label (sectionRegularDispositions section)
+        == Just RegularSiteHidden
+    ]
+
+attachHiddenGenerator
+  :: Ord label
+  => RegularSection label
+  -> RegularLocality label
+  -> label
+  -> Maybe (RegularLocality label)
+attachHiddenGenerator section locality label = do
+  generator <- Map.lookup label (sectionGenerators section)
+  location <- locateRegularSlope (localityFaceSeed locality) section generator
+  case location of
+    RegularSlopeInside faceKey ->
+      Just
+        (attachRegularHidden label faceKey locality)
+          { localityFaceSeed = faceKey
+          }
+    RegularSlopeOutside _ -> Nothing
+
+locateRegularSlope
+  :: Ord label
+  => RegularFaceKey label
+  -> RegularSection label
+  -> ExactPowerGenerator label
+  -> Maybe (RegularSlopeLocation label)
+locateRegularSlope seed section generator =
+  descendRegularSlope
+    (exactPoint queryX queryY)
+    section
+    (Map.size (sectionRegularFaces section) + 1)
+    seed
+ where
+  (queryX, queryY) = exactGeneratorSlope generator
+
+descendRegularSlope
+  :: Ord label
+  => ExactPoint
+  -> RegularSection label
+  -> Int
+  -> RegularFaceKey label
+  -> Maybe (RegularSlopeLocation label)
+descendRegularSlope query section !remaining faceKey
+  | remaining <= 0 = Nothing
+  | otherwise = do
+      face <- Map.lookup faceKey (sectionRegularFaces section)
+      case firstExteriorFaceEdge query section face of
+        Nothing -> Just (RegularSlopeInside faceKey)
+        Just edgeKey -> do
+          edgeSection <- Map.lookup edgeKey (sectionRegularEdges section)
+          case Set.lookupMin (Set.delete faceKey (sectionRegularEdgeFaces edgeSection)) of
+            Just adjacent ->
+              descendRegularSlope query section (remaining - 1) adjacent
+            Nothing -> Just (RegularSlopeOutside edgeKey)
+
+firstExteriorFaceEdge
+  :: Ord label
+  => ExactPoint
+  -> RegularSection label
+  -> RegularFace label
+  -> Maybe (RegularEdgeKey label)
+firstExteriorFaceEdge query section face =
+  fmap
+    (uncurry orderedPair)
+    ( List.find
+        (faceEdgeExcludes query section)
+        (regularFaceDirectedEdgeKeys face)
+    )
+
+faceEdgeExcludes
+  :: Ord label
+  => ExactPoint
+  -> RegularSection label
+  -> RegularEdgeKey label
+  -> Bool
+faceEdgeExcludes query section (fromLabel, toLabel) =
+  generatorEdgeExcludes
+    query
+    (sectionGenerators section)
+    (fromLabel, toLabel)
+
+generatorEdgeExcludes
+  :: Ord label
+  => ExactPoint
+  -> Map label (ExactPowerGenerator label)
+  -> RegularEdgeKey label
+  -> Bool
+generatorEdgeExcludes query generators (fromLabel, toLabel) =
+  case (Map.lookup fromLabel generators, Map.lookup toLabel generators) of
+    (Just fromGenerator, Just toGenerator) ->
+      exactOrient2d
+        (uncurry exactPoint (exactGeneratorSlope fromGenerator))
+        (uncurry exactPoint (exactGeneratorSlope toGenerator))
+        query
+        == LT
+    _ -> False
+
+generatorEdgeExcludesScaled
+  :: Ord label
+  => ExactRational
+  -> (ExactRational, ExactRational)
+  -> Map label (ExactPowerGenerator label)
+  -> RegularEdgeKey label
+  -> Bool
+generatorEdgeExcludesScaled scale (queryX, queryY) generators (fromLabel, toLabel) =
+  case (Map.lookup fromLabel generators, Map.lookup toLabel generators) of
+    (Just fromGenerator, Just toGenerator) ->
+      let (fromX, fromY) = exactGeneratorSlope fromGenerator
+          (toX, toY) = exactGeneratorSlope toGenerator
+       in (toX - fromX) * (queryY - scale * fromY)
+            - (toY - fromY) * (queryX - scale * fromX)
+            < 0
+    _ -> False
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Mass.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Mass.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Mass.hs
@@ -0,0 +1,501 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Restricted exact observations of fixed-position, linearly reweighted power
+-- cells. Preparation descends through the existing all-constraint clipper;
+-- active source lines, strict omitted slacks and directed edge lengths glue
+-- those local cells into one sufficient open parameter interval. This is not
+-- an event-transition engine or a replacement for regular-site dispositions.
+module Moonlight.Planar.Internal.PowerDiagram.Mass
+  ( PreparedPowerMassSection
+  , PowerMassPolynomial (..)
+  , PowerMassBoundary (..)
+  , PowerMassObligation (..)
+  , PowerMassCertificate
+  , powerMassCertificateObligation
+  , powerMassCertificateConstant
+  , powerMassCertificateRate
+  , PowerMassBound
+  , powerMassBoundParameter
+  , powerMassBoundCertificates
+  , PowerMassInterval
+  , powerMassIntervalLower
+  , powerMassIntervalUpper
+  , PowerMassError (..)
+  , preparePowerMassSection
+  , powerMassSectionWindow
+  , powerMassSectionSites
+  , powerMassSectionDirection
+  , powerMassSectionInterval
+  , powerMassSectionCertificates
+  , powerMassSectionPolynomials
+  , evaluatePowerMasses
+  ) where
+
+import Control.DeepSeq (NFData (..))
+import Data.Bifunctor (first)
+import qualified Data.Foldable as Foldable
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Data.Maybe (mapMaybe)
+import Data.Ratio ((%))
+import Data.Set (Set)
+import qualified Data.Set as Set
+import GHC.Generics (Generic)
+import Moonlight.Planar.Convex (ConvexPolygon, convexPolygonPoints, retainConvexPolygon)
+import Moonlight.Planar.Exact
+  ( ExactAffineLine
+  , ExactArithmeticError
+  , ExactClipDisposition (..)
+  , ExactClipError
+  , ExactClosedHalfPlane
+  , ExactHalfPlaneError
+  , ExactPoint
+  , ExactRational
+  , ExactRetainedPolygon
+  , ExactVector (..)
+  , exactAffineLineCoefficients
+  , exactClipRetainedPolygon
+  , exactClosedHalfPlane
+  , exactClosedHalfPlaneFromDirectedEdge
+  , exactClosedHalfPlaneLine
+  , exactCross
+  , exactDivide
+  , exactPointCoordinates
+  , exactPointCross
+  , exactRetainedPolygonPoints
+  , exactVectorFromPoints
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (cyclePairsNonEmpty, unorderedPairs)
+import Moonlight.Planar.Internal.ExactRational (exactRationalFromNormalizedRatio)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator
+  , exactGeneratorAxis
+  , exactPowerGeneratorLabel
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Model
+  ( PowerDiagramError
+  , PowerSite
+  , powerSiteExactPosition
+  , powerSiteLabel
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Section
+  ( prepareExactPowerGenerator
+  , validateAndSortSites
+  )
+
+-- | Coefficients of @constant + parameter * linear + parameter^2 * quadratic@.
+-- The constructor is an observation, not an admission path to a section.
+data PowerMassPolynomial = PowerMassPolynomial !ExactRational !ExactRational !ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+instance Semigroup PowerMassPolynomial where
+  PowerMassPolynomial a b c <> PowerMassPolynomial d e f =
+    PowerMassPolynomial (a + d) (b + e) (c + f)
+
+instance Monoid PowerMassPolynomial where
+  mempty = PowerMassPolynomial 0 0 0
+
+-- | Stable source identity within one cell's complete inequality family.
+data PowerMassBoundary label
+  = PowerMassWindowBoundary !Int
+  | PowerMassSiteBoundary !label
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data PowerMassObligation label
+  = PowerMassVertexSlack !label !ExactPoint !(PowerMassBoundary label)
+  | PowerMassDirectedEdge !label !(PowerMassBoundary label)
+  | PowerMassEmptyWitness !label !(NonEmpty (PowerMassBoundary label, ExactRational))
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A retained strict affine inequality @constant + t * rate > 0@.
+data PowerMassCertificate label =
+  PowerMassCertificate !(PowerMassObligation label) !ExactRational !ExactRational
+  deriving stock (Eq, Ord, Show)
+
+instance NFData label => NFData (PowerMassCertificate label) where
+  rnf (PowerMassCertificate obligation constant rate) = rnf obligation `seq` rnf constant `seq` rnf rate
+
+powerMassCertificateObligation :: PowerMassCertificate label -> PowerMassObligation label
+powerMassCertificateObligation (PowerMassCertificate obligation _ _) = obligation
+
+powerMassCertificateConstant :: PowerMassCertificate label -> ExactRational
+powerMassCertificateConstant (PowerMassCertificate _ constant _) = constant
+
+powerMassCertificateRate :: PowerMassCertificate label -> ExactRational
+powerMassCertificateRate (PowerMassCertificate _ _ rate) = rate
+
+-- | Every retained certificate tied at this extremal root, not an arbitrary
+-- event chosen from the simultaneous set.
+data PowerMassBound label =
+  PowerMassBound !ExactRational !(NonEmpty (PowerMassCertificate label))
+  deriving stock (Eq, Ord, Show)
+
+instance NFData label => NFData (PowerMassBound label) where
+  rnf (PowerMassBound parameter certificates) = rnf parameter `seq` rnf certificates
+
+powerMassBoundParameter :: PowerMassBound label -> ExactRational
+powerMassBoundParameter (PowerMassBound parameter _) = parameter
+
+powerMassBoundCertificates :: PowerMassBound label -> NonEmpty (PowerMassCertificate label)
+powerMassBoundCertificates (PowerMassBound _ certificates) = certificates
+
+data PowerMassInterval label =
+  PowerMassInterval !(Maybe (PowerMassBound label)) !(Maybe (PowerMassBound label))
+  deriving stock (Eq, Ord, Show)
+
+instance NFData label => NFData (PowerMassInterval label) where
+  rnf (PowerMassInterval lower upper) = rnf lower `seq` rnf upper
+
+powerMassIntervalLower :: PowerMassInterval label -> Maybe (PowerMassBound label)
+powerMassIntervalLower (PowerMassInterval lower _) = lower
+
+powerMassIntervalUpper :: PowerMassInterval label -> Maybe (PowerMassBound label)
+powerMassIntervalUpper (PowerMassInterval _ upper) = upper
+
+-- No exported constructor or record-update surface can detach a polynomial
+-- from its source, direction, active-line witnesses or validity interval.
+data PreparedPowerMassSection label = PreparedPowerMassSection
+  !ConvexPolygon
+  !(NonEmpty (PowerSite label))
+  !(Map label ExactRational)
+  !(Map label (CertifiedMassCell label))
+  ![PowerMassCertificate label]
+  !(PowerMassInterval label)
+  deriving stock (Eq, Show)
+
+instance NFData label => NFData (PreparedPowerMassSection label) where
+  rnf (PreparedPowerMassSection window sites direction cells certificates interval) =
+    rnf window `seq` rnf sites `seq` rnf direction `seq` rnf cells `seq` rnf certificates `seq` rnf interval
+
+data MassConstraint label = MassConstraint !(PowerMassBoundary label) !ExactAffineLine !ExactRational
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data AffineMassVertex label = AffineMassVertex
+  !ExactPoint !ExactVector !(MassConstraint label) !(MassConstraint label)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data CertifiedMassCell label
+  = FullDimensionalMassCell !PowerMassPolynomial !(NonEmpty (AffineMassVertex label))
+  | EmptyMassCell !(PowerMassCertificate label)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Restrictions belong only to this prepared observation. Public regular
+-- triangulations continue to support coincident and lower-dimensional sites.
+data PowerMassError label
+  = PowerMassSiteAdmission !(PowerDiagramError label)
+  | PowerMassDirectionMismatch !(Set label) !(Set label)
+  | PowerMassCoincidentSites !label !label
+  | PowerMassWindowAdmission !ExactHalfPlaneError
+  | PowerMassAxisAdmission !label !label !ExactHalfPlaneError
+  | PowerMassCellClip !label !ExactClipError
+  | PowerMassLowerDimensionalSeed !label !(NonEmpty ExactPoint)
+  | PowerMassActiveIncidence !label !ExactPoint ![PowerMassBoundary label]
+  | PowerMassParallelActiveBoundaries !label !ExactPoint
+  | PowerMassEdgeIncidence !label ![PowerMassBoundary label]
+  | PowerMassNonStrictSeed !(PowerMassObligation label) !ExactRational
+  | PowerMassEmptyWitnessUnavailable !label
+  | PowerMassArithmetic !ExactArithmeticError
+  | PowerMassParameterOutsideCertificate !ExactRational !(PowerMassInterval label)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+powerMassSectionWindow :: PreparedPowerMassSection label -> ConvexPolygon
+powerMassSectionWindow (PreparedPowerMassSection window _ _ _ _ _) = window
+
+powerMassSectionSites :: PreparedPowerMassSection label -> NonEmpty (PowerSite label)
+powerMassSectionSites (PreparedPowerMassSection _ sites _ _ _ _) = sites
+
+powerMassSectionDirection :: PreparedPowerMassSection label -> Map label ExactRational
+powerMassSectionDirection (PreparedPowerMassSection _ _ direction _ _ _) = direction
+
+powerMassSectionInterval :: PreparedPowerMassSection label -> PowerMassInterval label
+powerMassSectionInterval (PreparedPowerMassSection _ _ _ _ _ interval) = interval
+
+powerMassSectionCertificates :: PreparedPowerMassSection label -> [PowerMassCertificate label]
+powerMassSectionCertificates (PreparedPowerMassSection _ _ _ _ certificates _) = certificates
+
+powerMassSectionPolynomials :: PreparedPowerMassSection label -> Map label PowerMassPolynomial
+powerMassSectionPolynomials (PreparedPowerMassSection _ _ _ cells _ _) = fmap massCellPolynomial cells
+
+massCellPolynomial :: CertifiedMassCell label -> PowerMassPolynomial
+massCellPolynomial (FullDimensionalMassCell coefficients _) = coefficients
+massCellPolynomial (EmptyMassCell _) = mempty
+
+-- | Prepare exact area observations for fixed positions, rational affine
+-- weights and a fixed bounded convex window. Every label needs an explicit
+-- rate, including zero. The expensive small-subset empty-witness search runs
+-- only here; evaluation performs one exact interval check and O(n) polynomials.
+preparePowerMassSection
+  :: Ord label
+  => ConvexPolygon
+  -> NonEmpty (PowerSite label)
+  -> Map label ExactRational
+  -> Either (PowerMassError label) (PreparedPowerMassSection label)
+preparePowerMassSection window submitted direction = do
+  sites <- first PowerMassSiteAdmission (validateAndSortSites submitted)
+  let labels = Set.fromList (fmap powerSiteLabel (NonEmpty.toList sites))
+      directionLabels = Map.keysSet direction
+      missing = labels Set.\\ directionLabels
+      extra = directionLabels Set.\\ labels
+  if Set.null missing && Set.null extra
+    then Right ()
+    else Left (PowerMassDirectionMismatch missing extra)
+  case List.find samePosition (unorderedPairs (NonEmpty.toList sites)) of
+    Just (left, right) -> Left (PowerMassCoincidentSites (powerSiteLabel left) (powerSiteLabel right))
+    Nothing -> Right ()
+  let retained = retainConvexPolygon window
+  windowConstraints <- traverse windowConstraint (zip [0 ..] (NonEmpty.toList (cyclePairsNonEmpty (convexPolygonPoints window))))
+  let generators = fmap prepareExactPowerGenerator sites
+      -- Exact key equality was discharged above; pairing carries each rate
+      -- through all construction rather than inventing a missing-rate default.
+      trajectories = zip (NonEmpty.toList generators) (Map.elems direction)
+  prepared <- traverse (prepareMassCell retained windowConstraints trajectories) trajectories
+  let cells = Map.fromList (fmap (\(label, cell, _) -> (label, cell)) prepared)
+      certificates = concatMap (\(_, _, obligations) -> obligations) prepared
+  interval <- certificateInterval certificates
+  pure (PreparedPowerMassSection window sites direction cells certificates interval)
+ where
+  samePosition :: (PowerSite label, PowerSite label) -> Bool
+  samePosition (left, right) = powerSiteExactPosition left == powerSiteExactPosition right
+  windowConstraint
+    :: (Int, (ExactPoint, ExactPoint))
+    -> Either (PowerMassError label) (MassConstraint label)
+  windowConstraint (index, (from, to)) = do
+    halfPlane <- first PowerMassWindowAdmission (exactClosedHalfPlaneFromDirectedEdge from to)
+    pure (MassConstraint (PowerMassWindowBoundary index) (exactClosedHalfPlaneLine halfPlane) 0)
+
+prepareMassCell
+  :: Ord label
+  => ExactRetainedPolygon
+  -> [MassConstraint label]
+  -> [(ExactPowerGenerator label, ExactRational)]
+  -> (ExactPowerGenerator label, ExactRational)
+  -> Either (PowerMassError label) (label, CertifiedMassCell label, [PowerMassCertificate label])
+prepareMassCell retained windowConstraints trajectories (owner, ownerRate) = do
+  let label = exactPowerGeneratorLabel owner
+  siteConstraints <- traverse (siteConstraint label) (filter ((/= label) . exactPowerGeneratorLabel . fst) trajectories)
+  let constraints = windowConstraints <> siteConstraints
+  (disposition, _) <- first (PowerMassCellClip label)
+    (exactClipRetainedPolygon retained (fmap constraintHalfPlane siteConstraints))
+  case disposition of
+    ExactClipLowerDimensional points -> Left (PowerMassLowerDimensionalSeed label points)
+    ExactClipEmpty -> do
+      witness <- maybe (Left (PowerMassEmptyWitnessUnavailable label)) Right (emptyWitness label constraints)
+      pure (label, EmptyMassCell witness, [witness])
+    ExactClipFullDimensional polygon -> do
+      vertices <- traverse (prepareMassVertex label constraints) (exactRetainedPolygonPoints polygon)
+      vertexCertificates <- traverse (vertexSlacks label constraints) vertices
+      edgeCertificates <- traverse (directedEdgeCertificate label) (cyclePairsNonEmpty vertices)
+      pure
+        ( label
+        , FullDimensionalMassCell (areaPolynomial vertices) vertices
+        , concat (NonEmpty.toList vertexCertificates) <> NonEmpty.toList edgeCertificates
+        )
+ where
+  siteConstraint label (competitor, competitorRate) = do
+    let competitorLabel = exactPowerGeneratorLabel competitor
+    axis <- first (PowerMassAxisAdmission label competitorLabel) (exactGeneratorAxis owner competitor)
+    pure (MassConstraint (PowerMassSiteBoundary competitorLabel) axis (ownerRate - competitorRate))
+  constraintHalfPlane :: MassConstraint label -> ExactClosedHalfPlane
+  constraintHalfPlane (MassConstraint _ line _) = exactClosedHalfPlane line
+
+constraintBoundary :: MassConstraint label -> PowerMassBoundary label
+constraintBoundary (MassConstraint boundary _ _) = boundary
+
+constraintCoefficients :: MassConstraint label -> (ExactRational, ExactRational, ExactRational, ExactRational)
+constraintCoefficients (MassConstraint _ line rate) =
+  let (a, b, c) = exactAffineLineCoefficients line in (a, b, c, rate)
+
+constraintAtPoint :: ExactPoint -> MassConstraint label -> ExactRational
+constraintAtPoint point constraint =
+  let (x, y) = exactPointCoordinates point
+      (a, b, c, _) = constraintCoefficients constraint
+   in a * x + b * y + c
+
+prepareMassVertex
+  :: label
+  -> [MassConstraint label]
+  -> ExactPoint
+  -> Either (PowerMassError label) (AffineMassVertex label)
+prepareMassVertex label constraints point =
+  case filter ((== 0) . constraintAtPoint point) constraints of
+    [left, right] -> do
+      let (a, b, _, leftRate) = constraintCoefficients left
+          (c, d, _, rightRate) = constraintCoefficients right
+          determinant = a * d - b * c
+      if determinant == 0
+        then Left (PowerMassParallelActiveBoundaries label point)
+        else do
+          velocityX <- first PowerMassArithmetic (exactDivide (b * rightRate - leftRate * d) determinant)
+          velocityY <- first PowerMassArithmetic (exactDivide (leftRate * c - a * rightRate) determinant)
+          pure (AffineMassVertex point (ExactVector velocityX velocityY) left right)
+    active -> Left (PowerMassActiveIncidence label point (fmap constraintBoundary active))
+
+strictCertificate
+  :: PowerMassObligation label
+  -> ExactRational
+  -> ExactRational
+  -> Either (PowerMassError label) (PowerMassCertificate label)
+strictCertificate obligation constant rate
+  | constant > 0 = Right (PowerMassCertificate obligation constant rate)
+  | otherwise = Left (PowerMassNonStrictSeed obligation constant)
+
+vertexSlacks
+  :: Eq label
+  => label
+  -> [MassConstraint label]
+  -> AffineMassVertex label
+  -> Either (PowerMassError label) [PowerMassCertificate label]
+vertexSlacks label constraints (AffineMassVertex point (ExactVector dx dy) left right) =
+  traverse certificate (filter inactive constraints)
+ where
+  inactive constraint = constraintBoundary constraint /= constraintBoundary left
+    && constraintBoundary constraint /= constraintBoundary right
+  certificate constraint =
+    let (a, b, _, rate) = constraintCoefficients constraint
+     in strictCertificate
+          (PowerMassVertexSlack label point (constraintBoundary constraint))
+          (constraintAtPoint point constraint)
+          (a * dx + b * dy + rate)
+
+directedEdgeCertificate
+  :: Eq label
+  => label
+  -> (AffineMassVertex label, AffineMassVertex label)
+  -> Either (PowerMassError label) (PowerMassCertificate label)
+directedEdgeCertificate label
+    ( AffineMassVertex from (ExactVector fromDX fromDY) fromLeft fromRight
+    , AffineMassVertex to (ExactVector toDX toDY) toLeft toRight
+    ) =
+  case filter shared [fromLeft, fromRight] of
+    [edge] ->
+      let (a, b, _, _) = constraintCoefficients edge
+          ExactVector edgeX edgeY = exactVectorFromPoints from to
+       in strictCertificate
+            (PowerMassDirectedEdge label (constraintBoundary edge))
+            (b * edgeX - a * edgeY)
+            (b * (toDX - fromDX) - a * (toDY - fromDY))
+    edges -> Left (PowerMassEdgeIncidence label (fmap constraintBoundary edges))
+ where
+  shared constraint = constraintBoundary constraint == constraintBoundary toLeft
+    || constraintBoundary constraint == constraintBoundary toRight
+
+areaPolynomial :: NonEmpty (AffineMassVertex label) -> PowerMassPolynomial
+areaPolynomial = foldMap edgePolynomial . cyclePairsNonEmpty
+ where
+  half = exactRationalFromNormalizedRatio (1 % 2)
+  edgePolynomial :: (AffineMassVertex label, AffineMassVertex label) -> PowerMassPolynomial
+  edgePolynomial (AffineMassVertex from fromVelocity _ _, AffineMassVertex to toVelocity _ _) =
+    let (fromX, fromY) = exactPointCoordinates from
+        (toX, toY) = exactPointCoordinates to
+        fromVector = ExactVector fromX fromY
+        toVector = ExactVector toX toY
+     in PowerMassPolynomial
+          (half * exactPointCross from to)
+          (half * (exactCross fromVelocity toVector + exactCross fromVector toVelocity))
+          (half * exactCross fromVelocity toVelocity)
+
+-- Small-subset Farkas search is deliberately preparation-only. For source
+-- equations a*x+b*y+c(t)>=0, cancelling normals and sum lambda*c(t)<0
+-- certifies emptiness, including contacts involving only the window.
+emptyWitness :: label -> [MassConstraint label] -> Maybe (PowerMassCertificate label)
+emptyWitness label constraints =
+  Foldable.asum (fmap witness (pairWeights <> tripleWeights))
+ where
+  pairWeights = fmap oppositeWeights (unorderedPairs constraints)
+  tripleWeights =
+    [ cyclicWeights left middle right
+    | left : remaining <- List.tails constraints
+    , (middle, right) <- unorderedPairs remaining
+    ]
+  oppositeWeights
+    :: (MassConstraint label, MassConstraint label)
+    -> [(MassConstraint label, ExactRational)]
+  oppositeWeights (left, right) =
+    let (a, b, _, _) = constraintCoefficients left
+        (c, d, _, _) = constraintCoefficients right
+        (leftWeight, rightWeight) = if a /= 0 || c /= 0 then (abs c, abs a) else (abs d, abs b)
+     in [(left, leftWeight), (right, rightWeight)]
+  cyclicWeights
+    :: MassConstraint label
+    -> MassConstraint label
+    -> MassConstraint label
+    -> [(MassConstraint label, ExactRational)]
+  cyclicWeights left middle right =
+    let crossNormals :: MassConstraint label -> MassConstraint label -> ExactRational
+        crossNormals firstConstraint secondConstraint =
+          let (a, b, _, _) = constraintCoefficients firstConstraint
+              (c, d, _, _) = constraintCoefficients secondConstraint
+           in a * d - b * c
+        weighted = [(left, crossNormals middle right), (middle, crossNormals right left), (right, crossNormals left middle)]
+     in if all ((<= 0) . snd) weighted then fmap (fmap negate) weighted else weighted
+  witness weighted =
+    let positive = filter ((> 0) . snd) weighted
+        normalX = sum (fmap (\(constraint, weight) -> let (a, _, _, _) = constraintCoefficients constraint in weight * a) weighted)
+        normalY = sum (fmap (\(constraint, weight) -> let (_, b, _, _) = constraintCoefficients constraint in weight * b) weighted)
+        constant = sum (fmap (\(constraint, weight) -> let (_, _, c, _) = constraintCoefficients constraint in weight * c) weighted)
+        rate = sum (fmap (\(constraint, weight) -> let (_, _, _, d) = constraintCoefficients constraint in weight * d) weighted)
+     in if all ((>= 0) . snd) weighted && normalX == 0 && normalY == 0 && constant < 0
+          then fmap
+            (\sources -> PowerMassCertificate (PowerMassEmptyWitness label sources) (negate constant) (negate rate))
+            (NonEmpty.nonEmpty (fmap (\(constraint, weight) -> (constraintBoundary constraint, weight)) positive))
+          else Nothing
+
+certificateInterval
+  :: [PowerMassCertificate label]
+  -> Either (PowerMassError label) (PowerMassInterval label)
+certificateInterval certificates = do
+  bounds <- traverse root certificates
+  pure (Foldable.foldl' intersectInterval (PowerMassInterval Nothing Nothing) (mapMaybe id bounds))
+ where
+  root
+    :: PowerMassCertificate label
+    -> Either (PowerMassError label) (Maybe (PowerMassInterval label))
+  root certificate@(PowerMassCertificate _ constant rate)
+    | rate == 0 = Right Nothing
+    | otherwise = do
+        parameter <- first PowerMassArithmetic (exactDivide (negate constant) rate)
+        let bound = Just (PowerMassBound parameter (certificate :| []))
+        pure (Just (if rate > 0 then PowerMassInterval bound Nothing else PowerMassInterval Nothing bound))
+  intersectInterval :: PowerMassInterval label -> PowerMassInterval label -> PowerMassInterval label
+  intersectInterval (PowerMassInterval lower upper) (PowerMassInterval otherLower otherUpper) =
+    PowerMassInterval (tightest GT lower otherLower) (tightest LT upper otherUpper)
+  tightest
+    :: Ordering
+    -> Maybe (PowerMassBound label)
+    -> Maybe (PowerMassBound label)
+    -> Maybe (PowerMassBound label)
+  tightest _ Nothing right = right
+  tightest _ left Nothing = left
+  tightest direction left@(Just (PowerMassBound leftRoot leftCertificates)) right@(Just (PowerMassBound rightRoot rightCertificates)) =
+    case compare leftRoot rightRoot of
+      EQ -> Just (PowerMassBound leftRoot (leftCertificates <> rightCertificates))
+      ordering -> if ordering == direction then left else right
+
+-- | Only exact parameters strictly inside the retained sufficient interval
+-- are admitted. Endpoint expiry requests recertification; it does not claim a
+-- visibility event. No floating epsilon or implicit binary64 conversion exists.
+evaluatePowerMasses
+  :: ExactRational
+  -> PreparedPowerMassSection label
+  -> Either (PowerMassError label) (Map label ExactRational)
+evaluatePowerMasses parameter section@(PreparedPowerMassSection _ _ _ cells _ _) =
+  let interval = powerMassSectionInterval section
+      aboveLower = maybe True ((parameter >) . powerMassBoundParameter) (powerMassIntervalLower interval)
+      belowUpper = maybe True ((parameter <) . powerMassBoundParameter) (powerMassIntervalUpper interval)
+      evaluatePolynomial (PowerMassPolynomial constant linear quadratic) =
+        constant + parameter * (linear + parameter * quadratic)
+   in if aboveLower && belowUpper
+        then Right (fmap (evaluatePolynomial . massCellPolynomial) cells)
+        else Left (PowerMassParameterOutsideCertificate parameter interval)
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Model.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Model.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Model.hs
@@ -0,0 +1,547 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Shared opaque carriers for exact power topology. This module owns
+-- representation only; construction and interpretation live in sibling modules.
+module Moonlight.Planar.Internal.PowerDiagram.Model where
+
+import Control.DeepSeq (NFData (..))
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty)
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactClipError
+  , ExactHalfPlaneError
+  , ExactPoint
+  , exactPointFromQueryPoint
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRationalFromFiniteDouble
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (orderedPair)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator (..)
+  , GeneratorRegularReceipt
+  , GeneratorRegularTopology
+  , PowerDualEdge
+  , RegularTopologyError
+  )
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Convex (ConvexError, ConvexPolygon)
+import Moonlight.Planar.Internal.Overlay.Types (OverlayError)
+import Moonlight.Planar.Region (RegionPublicationError)
+import Moonlight.Planar.Scalar (NonFiniteValue, classifyNonFinite)
+import Moonlight.Planar.Point (Point, PointValidationError, QueryPoint, queryPointValue)
+
+-- | An admitted signed additive power offset.  Power distance is
+-- @||x-p||^2-w@, so negative values are lawful and this is deliberately not a
+-- squared-radius refinement.
+newtype PowerWeight = PowerWeight ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The sole obstruction to admitting a signed binary64 power offset.
+data PowerWeightError
+  = PowerWeightNonFinite !NonFiniteValue
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Admit a finite binary64 power offset exactly.
+powerWeight :: Double -> Either PowerWeightError PowerWeight
+powerWeight value =
+  case classifyNonFinite value of
+    Just obstruction -> Left (PowerWeightNonFinite obstruction)
+    Nothing -> Right (PowerWeight (exactRationalFromFiniteDouble value))
+
+-- | Admit an already-exact signed power offset without a binary64 round trip.
+powerWeightFromExact :: ExactRational -> PowerWeight
+powerWeightFromExact = PowerWeight
+
+-- | Exact rational value of an admitted power offset.
+powerWeightExact :: PowerWeight -> ExactRational
+powerWeightExact (PowerWeight value) = value
+
+-- | One labelled, admitted weighted site.  Construction validates and
+-- canonicalizes the binary64 position once.
+data PowerSite label = PowerSite
+  { powerSiteLabel :: !label
+    -- ^ Stable identity used by edits and canonical topology.
+  , powerSiteQueryPoint :: !QueryPoint
+  , powerSiteWeight :: !PowerWeight
+    -- ^ Signed additive power offset.
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Admit one stable-labelled site at a finite position.
+powerSite
+  :: label
+  -> Point
+  -> PowerWeight
+  -> Either (PowerDiagramError label) (PowerSite label)
+powerSite label point weight =
+  PowerSite label <$> first (PowerSitePositionInvalid label) (mkQueryPoint point) <*> pure weight
+
+-- | Binary64 position originally admitted for the site.
+powerSitePosition :: PowerSite label -> Point
+powerSitePosition = queryPointValue . powerSiteQueryPoint
+
+-- | Exact coordinates retained by an admitted site.
+powerSiteExactPosition :: PowerSite label -> ExactPoint
+powerSiteExactPosition = exactPointFromQueryPoint . powerSiteQueryPoint
+
+-- | Exactly one authoritative result for each submitted label.
+data PowerCellDisposition label
+  = PublishedPowerCell !ConvexPolygon
+  | LowerDimensionalPowerCell !(NonEmpty ExactPoint)
+  | EmptyPowerCell
+  | CoincidentEquivalentTo !label
+  | CoincidentDominatedBy !label
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Visibility of one submitted label in the exact regular subdivision.
+data RegularSiteDisposition label
+  = RegularSiteVisible
+  | RegularSiteLowerDimensional
+  | RegularSiteHidden
+  | RegularSiteCoincidentEquivalentTo !label
+  | RegularSiteCoincidentDominatedBy !label
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One oriented regular face and its exact weighted-dual vertex.
+data RegularFace label = RegularFace !label !label !label !ExactPoint
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Canonically oriented labels of the face.
+regularFaceLabels :: RegularFace label -> (label, label, label)
+regularFaceLabels (RegularFace firstLabel secondLabel thirdLabel _) =
+  (firstLabel, secondLabel, thirdLabel)
+
+-- | Exact power vertex dual to the face.
+regularFaceDualPoint :: RegularFace label -> ExactPoint
+regularFaceDualPoint (RegularFace _ _ _ dualPoint) = dualPoint
+
+-- | One unordered regular edge and its exact dual geometry.
+data RegularEdge label = RegularEdge !label !label !PowerDualEdge
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Ascending endpoint labels of the unordered edge.
+regularEdgeLabels :: RegularEdge label -> (label, label)
+regularEdgeLabels (RegularEdge firstLabel secondLabel _) =
+  (firstLabel, secondLabel)
+
+-- | Exact power-dual carrier of the edge.
+regularEdgeDual :: RegularEdge label -> PowerDualEdge
+regularEdgeDual (RegularEdge _ _ dual) = dual
+
+type RegularFaceKey label = (label, label, label)
+type RegularEdgeKey label = (label, label)
+
+data RegularEdgeSection label = RegularEdgeSection
+  { sectionRegularEdge :: !(RegularEdge label)
+  , sectionRegularEdgeFaces :: !(Set (RegularFaceKey label))
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data RegularSiteStar label = RegularSiteStar
+  { sectionIncidentFaces :: !(Set (RegularFaceKey label))
+  , sectionSiteNeighbours :: !(Set label)
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data RegularLocality label = RegularLocality
+  { localityFaceSeed :: !(RegularFaceKey label)
+  , localityHiddenSupport :: !(Map label (RegularFaceKey label))
+  , localitySupportHidden :: !(Map (RegularFaceKey label) (Set label))
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+data RegularSlopeLocation label
+  = RegularSlopeInside !(RegularFaceKey label)
+  | RegularSlopeOutside !(RegularEdgeKey label)
+
+data LocalRegularEditObstruction label
+  = LocalRegularSectionUnavailable
+  | LocalRegularLocalityUnavailable
+  | LocalRegularSlopeCoincidence !label
+  | LocalRegularSlopeOutside !(RegularEdgeKey label)
+  | LocalRegularSlopeLocationFailed
+  | LocalRegularDegenerateFace !(RegularFaceKey label)
+  | LocalRegularFaceMissing !(RegularFaceKey label)
+  | LocalRegularGeneratorMissing !label
+  | LocalRegularNonManifoldEdge !(RegularEdgeKey label) !Int
+  | LocalRegularNonConvexEdge !(RegularEdgeKey label)
+  | LocalRegularEmptyHorizon
+  | LocalRegularBoundaryRemoval !label
+  | LocalRegularCandidateSectionEmpty
+  | LocalRegularCavityBoundaryMismatch
+  | LocalRegularTopologyObstruction !(RegularTopologyError label)
+
+data RegularConflictDescent label = RegularConflictDescent
+  { conflictPendingFaces :: !(Set (RegularFaceKey label))
+  , conflictVisitedFaces :: !(Set (RegularFaceKey label))
+  , conflictVisibleFaces :: !(Set (RegularFaceKey label))
+  , conflictObstruction :: !(Maybe (LocalRegularEditObstruction label))
+  }
+
+data RegularFacePatch label = RegularFacePatch
+  { patchedRegularFaces :: !(Map (RegularFaceKey label) (RegularFace label))
+  , patchedRegularEdges :: !(Map (RegularEdgeKey label) (RegularEdgeSection label))
+  , patchedRegularStars :: !(Map label (RegularSiteStar label))
+  , patchedRegularLabels :: !(Set label)
+  , patchedRegularTouchedEdges :: !(Set (RegularEdgeKey label))
+  }
+
+data RegularSection label = RegularSection
+  { sectionGenerators :: !(Map label (ExactPowerGenerator label))
+  , sectionSlopeRepresentatives :: !(Map (ExactRational, ExactRational) label)
+  , sectionCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
+  , sectionRegularDispositions :: !(Map label (RegularSiteDisposition label))
+  , sectionRegularFaces :: !(Map (RegularFaceKey label) (RegularFace label))
+  , sectionRegularEdges :: !(Map (RegularEdgeKey label) (RegularEdgeSection label))
+  , sectionRegularStars :: !(Map label (RegularSiteStar label))
+  , sectionRegularReceipt :: !GeneratorRegularReceipt
+  , sectionRegularLocality :: !(Maybe (RegularLocality label))
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact regular subdivision. Every weighted site remains resident in this
+-- opaque value; one normalized section owns all derived topology and incidence.
+data RegularTriangulation label = RegularTriangulation
+  { storedRegularSites :: !(Map label (PowerSite label))
+  , storedRegularSection :: !(Maybe (RegularSection label))
+  }
+
+instance NFData label => NFData (RegularTriangulation label) where
+  rnf (RegularTriangulation sites section) = rnf sites `seq` rnf section
+  {-# INLINE rnf #-}
+
+-- | Semantic equality is equality of the complete weighted-site section.
+-- Topology and visibility are sealed derived caches of that section.
+instance Eq label => Eq (RegularTriangulation label) where
+  left == right = storedRegularSites left == storedRegularSites right
+
+instance Show label => Show (RegularTriangulation label) where
+  showsPrec precedence triangulation =
+    showParen (precedence > 10)
+      ( showString "RegularTriangulation "
+          . shows (storedRegularSites triangulation)
+      )
+
+-- | Strict cardinality receipt for one normalized regular section.
+data RegularTriangulationReceipt = RegularTriangulationReceipt
+  { regularTriangulationInputSites :: !Int
+  , regularTriangulationRepresentativeSites :: !Int
+  , regularTriangulationVisibleSites :: !Int
+  , regularTriangulationLowerDimensionalSites :: !Int
+  , regularTriangulationHiddenSites :: !Int
+  , regularTriangulationCoincidentSites :: !Int
+  , regularTriangulationFaces :: !Int
+  , regularTriangulationEdges :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One exact change in the exhaustive visibility section. Appearance and
+-- disappearance are explicit rather than encoded with sentinel dispositions.
+data RegularSiteTransition label
+  = RegularSiteAppeared !label !(RegularSiteDisposition label)
+  | RegularSiteDisappeared !label !(RegularSiteDisposition label)
+  | RegularSiteTransitioned
+      !label
+      !(RegularSiteDisposition label)
+      !(RegularSiteDisposition label)
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | A regular edit either conflicts with an existing stable identity, names
+-- absent patch identities, or carries the exact normalization obstruction.
+data RegularEditError label
+  = RegularEditSiteConflict
+      !label
+      !(PowerSite label)
+      !(PowerSite label)
+  | RegularEditUnknownSites !(NonEmpty label)
+  | RegularEditTopologyFailed !(RegularTopologyError label)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | One common publication for every pure regular-site edit. The changed-site
+-- support is empty precisely for an idempotent edit.
+data RegularEditResult label = RegularEditResult
+  { regularEditTriangulation :: !(RegularTriangulation label)
+  , regularEditChangedSites :: !(Set label)
+  , regularEditTransitions :: !(Vector.Vector (RegularSiteTransition label))
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Total labelled result, opaque so callers cannot omit a submitted label.
+newtype BoundedPowerDiagram label =
+  BoundedPowerDiagram (Map label (PowerCellDisposition label))
+  deriving stock (Eq, Show)
+
+instance NFData label => NFData (BoundedPowerDiagram label) where
+  rnf (BoundedPowerDiagram dispositions) = rnf dispositions
+  {-# INLINE rnf #-}
+
+-- | Typed admission, topology, and clipping obstructions.
+data PowerDiagramError label
+  = PowerSitePositionInvalid !label !PointValidationError
+  | DuplicatePowerSiteLabel !label
+  | PowerBisectorInvalid !label !label !ExactHalfPlaneError
+  | PowerRegularTopologyFailed !(RegularTopologyError label)
+  | PowerRegularDispositionMissing !label
+  | PowerRegularGeneratorMissing !label
+  | PowerCellClipFailed !label !ExactClipError
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Exact construction counts and rational-width evidence for a bounded diagram.
+data PowerDiagramReceipt = PowerDiagramReceipt
+  { powerDiagramDomainVertices :: !Int
+  , powerDiagramSubmittedSiteConstraints :: !Int
+  , powerDiagramActiveBoundaries :: !Int
+  , powerDiagramBoundaryCompatibilityChecks :: !Int
+  , powerDiagramExactIntersections :: !Int
+  , powerDiagramPublishedCells :: !Int
+  , powerDiagramLowerDimensionalCells :: !Int
+  , powerDiagramEmptyCells :: !Int
+  , powerDiagramCoincidentEquivalentCells :: !Int
+  , powerDiagramCoincidentDominatedCells :: !Int
+  , powerDiagramRegularFaces :: !Int
+  , powerDiagramRegularEdges :: !Int
+  , powerDiagramOracleCells :: !Int
+  , powerDiagramMaximumCellConstraints :: !Int
+  , powerDiagramMaximumInputBits :: !Int
+  , powerDiagramMaximumAffineCoefficientBits :: !Int
+  , powerDiagramPeakIntermediateCoordinateBits :: !Int
+  , powerDiagramFinalCoordinateBits :: !Int
+  , powerDiagramFinalDenominatorBits :: !Int
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Submitted sites, derived from the exhaustive disposition partition.
+powerDiagramInputSites :: PowerDiagramReceipt -> Int
+powerDiagramInputSites receipt =
+  powerDiagramPublishedCells receipt
+    + powerDiagramLowerDimensionalCells receipt
+    + powerDiagramEmptyCells receipt
+    + powerDiagramCoincidentEquivalentCells receipt
+    + powerDiagramCoincidentDominatedCells receipt
+{-# INLINE powerDiagramInputSites #-}
+
+-- | Peak exact-coordinate width beyond the widest admitted input.
+powerDiagramPeakIntermediateBitGrowth :: PowerDiagramReceipt -> Int
+powerDiagramPeakIntermediateBitGrowth receipt =
+  max
+    0
+    ( powerDiagramPeakIntermediateCoordinateBits receipt
+        - powerDiagramMaximumInputBits receipt
+    )
+{-# INLINE powerDiagramPeakIntermediateBitGrowth #-}
+
+-- | Published coordinate width beyond the widest admitted input.
+powerDiagramFinalCoordinateBitGrowth :: PowerDiagramReceipt -> Int
+powerDiagramFinalCoordinateBitGrowth receipt =
+  max
+    0
+    ( powerDiagramFinalCoordinateBits receipt
+        - powerDiagramMaximumInputBits receipt
+    )
+{-# INLINE powerDiagramFinalCoordinateBitGrowth #-}
+
+-- | One exact affine form @c0 + cx*x + cy*y@.
+data AffineForm = AffineForm
+  { affineFormConstant :: !ExactRational
+  , affineFormXCoefficient :: !ExactRational
+  , affineFormYCoefficient :: !ExactRational
+  }
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Typed obstructions from exact affine argmax decomposition.  Power-cell
+-- construction remains the canonical geometric owner; window restriction is
+-- the only additional boundary.
+data UpperEnvelopeError label
+  = UpperEnvelopeEmptyForms
+  | UpperEnvelopeWindowHullFailed !ConvexError
+  | UpperEnvelopePowerConstructionFailed !(PowerDiagramError label)
+  | UpperEnvelopeWindowOverlayFailed !OverlayError
+  | UpperEnvelopeWindowPublicationFailed !RegionPublicationError
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+
+data CoincidentGeneratorDisposition label
+  = CoincidentGeneratorEquivalentTo !label
+  | CoincidentGeneratorDominatedBy !label
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+data ResolvedGeneratorSection label = ResolvedGeneratorSection
+  { resolvedCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
+  , resolvedRegularTopology :: !(GeneratorRegularTopology label)
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+foldRegularFaceBoundary
+  :: (label -> label -> label -> result -> result)
+  -> result
+  -> RegularFace label
+  -> result
+foldRegularFaceBoundary step initial face =
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+   in step firstLabel secondLabel thirdLabel
+        ( step secondLabel thirdLabel firstLabel
+            (step thirdLabel firstLabel secondLabel initial)
+        )
+{-# INLINE foldRegularFaceBoundary #-}
+
+regularFaceDirectedEdgeKeys :: RegularFace label -> [RegularEdgeKey label]
+regularFaceDirectedEdgeKeys =
+  foldRegularFaceBoundary
+    (\fromLabel toLabel _ -> ((fromLabel, toLabel) :))
+    []
+
+regularFaceCanonicalEdgeKeys
+  :: Ord label
+  => RegularFace label
+  -> [RegularEdgeKey label]
+regularFaceCanonicalEdgeKeys =
+  foldRegularFaceBoundary
+    (\fromLabel toLabel _ -> (orderedPair fromLabel toLabel :))
+    []
+
+regularFaceLabelSet :: Ord label => RegularFace label -> Set label
+regularFaceLabelSet =
+  foldRegularFaceBoundary (\label _ _ -> Set.insert label) Set.empty
+
+requireFaceEdgeWitness
+  :: Ord label
+  => RegularEdgeKey label
+  -> RegularFace label
+  -> Either (LocalRegularEditObstruction label) (label, label, label)
+requireFaceEdgeWitness edgeKey face =
+  maybe
+    (Left (LocalRegularFaceMissing (regularFaceKey face)))
+    Right
+    ( foldRegularFaceBoundary
+        (\fromLabel toLabel oppositeLabel later ->
+            if orderedPair fromLabel toLabel == edgeKey
+              then Just (fromLabel, toLabel, oppositeLabel)
+              else later)
+        Nothing
+        face
+    )
+
+regularFaceKey :: Ord label => RegularFace label -> RegularFaceKey label
+regularFaceKey face =
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+   in sortedRegularFaceLabels firstLabel secondLabel thirdLabel
+
+sortedRegularFaceLabels
+  :: Ord label
+  => label
+  -> label
+  -> label
+  -> RegularFaceKey label
+sortedRegularFaceLabels firstLabel secondLabel thirdLabel =
+  let (firstLow, firstHigh) = orderedPair firstLabel secondLabel
+      (secondLow, finalHigh) = orderedPair firstHigh thirdLabel
+      (finalLow, finalMiddle) = orderedPair firstLow secondLow
+   in (finalLow, finalMiddle, finalHigh)
+
+regularEdgeKey :: Ord label => RegularEdge label -> RegularEdgeKey label
+regularEdgeKey = uncurry orderedPair . regularEdgeLabels
+
+regularFaceEdgeIncidence
+  :: Ord label
+  => RegularFace label
+  -> Map (RegularEdgeKey label) (Set (RegularFaceKey label))
+regularFaceEdgeIncidence face =
+  let incidentFace = Set.singleton (regularFaceKey face)
+   in foldRegularFaceBoundary
+        (\fromLabel toLabel _ ->
+            Map.insert (orderedPair fromLabel toLabel) incidentFace)
+        Map.empty
+        face
+
+emptyRegularSiteStar :: RegularSiteStar label
+emptyRegularSiteStar = RegularSiteStar Set.empty Set.empty
+
+requireFaceFromMap
+  :: Ord label
+  => Map (RegularFaceKey label) (RegularFace label)
+  -> RegularFaceKey label
+  -> Either (LocalRegularEditObstruction label) (RegularFace label)
+requireFaceFromMap faces faceKey =
+  maybe (Left (LocalRegularFaceMissing faceKey)) Right (Map.lookup faceKey faces)
+
+requireGeneratorFromMap
+  :: Ord label
+  => Map label (ExactPowerGenerator label)
+  -> label
+  -> Either (LocalRegularEditObstruction label) (ExactPowerGenerator label)
+requireGeneratorFromMap generators label =
+  maybe (Left (LocalRegularGeneratorMissing label)) Right (Map.lookup label generators)
+
+regularFaceNeighbours
+  :: Ord label
+  => RegularSection label
+  -> RegularFaceKey label
+  -> Either
+      (LocalRegularEditObstruction label)
+      (Set (RegularFaceKey label))
+regularFaceNeighbours section faceKey = do
+  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
+  incident <- traverse requireIncidence (regularFaceCanonicalEdgeKeys face)
+  pure (Set.delete faceKey (Set.unions incident))
+ where
+  requireIncidence edgeKey =
+    maybe
+      (Left (LocalRegularNonManifoldEdge edgeKey 0))
+      (Right . sectionRegularEdgeFaces)
+      (Map.lookup edgeKey (sectionRegularEdges section))
+
+replaceRegularSiteSection
+  :: Map label (PowerSite label)
+  -> RegularSection label
+  -> RegularTriangulation label
+  -> RegularTriangulation label
+replaceRegularSiteSection sites section triangulation =
+  triangulation
+    { storedRegularSites = sites
+    , storedRegularSection = Just section
+    }
+
+regularDispositionSection
+  :: RegularTriangulation label
+  -> Map label (RegularSiteDisposition label)
+regularDispositionSection =
+  maybe Map.empty sectionRegularDispositions . storedRegularSection
+
+exactGeneratorSlope
+  :: ExactPowerGenerator label
+  -> (ExactRational, ExactRational)
+exactGeneratorSlope generator =
+  ( exactPowerGeneratorXCoefficient generator
+  , exactPowerGeneratorYCoefficient generator
+  )
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Projection.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Projection.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Projection.hs
@@ -0,0 +1,593 @@
+{-# LANGUAGE BangPatterns #-}
+-- | Bounded clipping and planar publication of exact regular sections.
+module Moonlight.Planar.Internal.PowerDiagram.Projection
+  ( boundedPowerDiagram
+  , boundedPowerDiagramFromRegular
+  , powerCellDisposition
+  , powerCellDispositions
+  , powerDiagramPlanarLayer
+  , upperEnvelope
+  ) where
+
+import Data.Bifunctor (first)
+import qualified Data.Foldable as Foldable
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import Moonlight.Planar.Exact
+  ( ExactClipDisposition (..)
+  , ExactClipReceipt (..)
+  , emptyExactClipReceipt
+  , ExactAffineLine
+  , ExactClosedHalfPlane
+  , ExactRetainedPolygon
+  , exactClipRetainedPolygon
+  , exactClosedHalfPlane
+  , oppositeExactAffineLine
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator (..)
+  , GeneratorRegularEdge (..)
+  , GeneratorRegularReceipt (..)
+  , GeneratorRegularTopology (..)
+  , RegularGeneratorDisposition (..)
+  , exactGeneratorAxis
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Model
+import Moonlight.Planar.Internal.PowerDiagram.Section
+import Moonlight.Planar.Convex
+  ( ConvexPolygon
+  , convexPolygonPoints
+  , convexHullPolygon
+  , convexPolygonComponent
+  , convexPolygonFromRetained
+  , retainConvexPolygon
+  )
+import Moonlight.Planar.Internal.Region.Publication
+  ( planarLayerFromAdmittedComponents
+  )
+import Moonlight.Planar.Overlay
+  ( overlayLayers
+  , overlayPlanarLayer
+  )
+import Moonlight.Planar.Region
+  ( PlanarLayer
+  , PolygonComponent
+  , exactLoopPoints
+  , planarLayerRegions
+  , planarRegionComponents
+  , polygonOuterLoop
+  )
+
+-- | Construct and clip a normalized regular subdivision to one convex domain.
+boundedPowerDiagram
+  :: Ord label
+  => ConvexPolygon
+  -> NonEmpty (PowerSite label)
+  -> Either (PowerDiagramError label) (BoundedPowerDiagram label, PowerDiagramReceipt)
+boundedPowerDiagram domain submitted = do
+  sortedSites <- validateAndSortSites submitted
+  let generators = fmap prepareExactPowerGenerator sortedSites
+      siteInputBits = maximumPowerSiteInputBits sortedSites
+  (dispositions, clipReceipt, regularReceipt, maximumCellConstraints) <-
+    exactGeneratorDispositionsWith (<>) emptyExactClipReceipt domain generators
+  let receipt =
+        aggregateReceipt
+          domain
+          dispositions
+          siteInputBits
+          clipReceipt
+          regularReceipt
+          maximumCellConstraints
+  pure (BoundedPowerDiagram dispositions, receipt)
+
+-- | Clip an already normalized regular value without rebuilding its lifted
+-- hull. The empty regular value yields an empty labelled diagram.
+boundedPowerDiagramFromRegular
+  :: Ord label
+  => ConvexPolygon
+  -> RegularTriangulation label
+  -> Either (PowerDiagramError label) (BoundedPowerDiagram label, PowerDiagramReceipt)
+boundedPowerDiagramFromRegular domain triangulation =
+  case storedRegularSection triangulation of
+    Nothing ->
+      Right
+        ( BoundedPowerDiagram Map.empty
+        , aggregateReceipt
+            domain
+            Map.empty
+            0
+            emptyExactClipReceipt
+            emptyGeneratorRegularReceipt
+            0
+        )
+    Just section -> do
+      (dispositions, clipReceipt, generatorReceipt, maximumCellConstraints) <-
+        exactRegularSectionDispositionsWith (<>) emptyExactClipReceipt domain section
+      let receipt =
+            aggregateReceipt
+              domain
+              dispositions
+              (maximumPowerSiteInputBits (storedRegularSites triangulation))
+              clipReceipt
+              generatorReceipt
+              maximumCellConstraints
+      pure (BoundedPowerDiagram dispositions, receipt)
+
+-- | Full-dimensional labelled argmax regions for affine forms
+-- @c0 + cx*x + cy*y@ inside an admitted polygonal window. The result is a
+-- planar projection: lower-dimensional and empty winners intentionally have
+-- no region. Use 'regularTriangulation' on corresponding weighted sites when
+-- those dispositions or exact unbounded duals are required. Identical forms
+-- choose the least label, independent of map construction order.
+upperEnvelope
+  :: Ord label
+  => PolygonComponent
+  -> Map label AffineForm
+  -> Either (UpperEnvelopeError label) (PlanarLayer (Maybe label))
+upperEnvelope window forms = do
+  generators <- affineFormGenerators forms
+  domain <-
+    first UpperEnvelopeWindowHullFailed
+      (convexHullPolygon (exactLoopPoints (polygonOuterLoop window)))
+  (dispositions, _, _, _) <-
+    first UpperEnvelopePowerConstructionFailed
+      (exactGeneratorDispositionsWith discardClipReceipt () domain generators)
+  let envelopeLayer = affineDispositionLayer dispositions
+  if convexPolygonComponent domain == window
+    then Right envelopeLayer
+    else restrictEnvelopeToWindow window envelopeLayer
+
+-- | Look up the exhaustive bounded-cell disposition for one submitted label.
+powerCellDisposition
+  :: Ord label
+  => label
+  -> BoundedPowerDiagram label
+  -> Maybe (PowerCellDisposition label)
+powerCellDisposition label (BoundedPowerDiagram dispositions) =
+  Map.lookup label dispositions
+
+-- | Enumerate every submitted label and bounded-cell disposition in order.
+powerCellDispositions
+  :: BoundedPowerDiagram label
+  -> [(label, PowerCellDisposition label)]
+powerCellDispositions (BoundedPowerDiagram dispositions) = Map.toAscList dispositions
+
+-- | Publish the full-dimensional cell layer with a canonical exterior.
+powerDiagramPlanarLayer
+  :: Ord label
+  => BoundedPowerDiagram label
+  -> PlanarLayer (Maybe label)
+powerDiagramPlanarLayer (BoundedPowerDiagram dispositions) =
+  affineDispositionLayer dispositions
+
+affineDispositionLayer
+  :: Ord label
+  => Map label (PowerCellDisposition label)
+  -> PlanarLayer (Maybe label)
+affineDispositionLayer = publishedPowerLayer Nothing Just
+
+publishedPowerLayer
+  :: Ord publishedLabel
+  => publishedLabel
+  -> (label -> publishedLabel)
+  -> Map label (PowerCellDisposition label)
+  -> PlanarLayer publishedLabel
+publishedPowerLayer outside publishLabel dispositions =
+  planarLayerFromAdmittedComponents
+    outside
+    [ (publishLabel label, convexPolygonComponent polygon)
+    | (label, PublishedPowerCell polygon) <- Map.toAscList dispositions
+    ]
+
+restrictEnvelopeToWindow
+  :: Ord label
+  => PolygonComponent
+  -> PlanarLayer (Maybe label)
+  -> Either (UpperEnvelopeError label) (PlanarLayer (Maybe label))
+restrictEnvelopeToWindow window envelopeLayer = do
+  let windowLayer = planarLayerFromAdmittedComponents False [(True, window)]
+  clipped <-
+    first UpperEnvelopeWindowOverlayFailed
+      (overlayLayers envelopeLayer windowLayer)
+  published <- first UpperEnvelopeWindowPublicationFailed (overlayPlanarLayer clipped)
+  pure
+    ( planarLayerFromAdmittedComponents
+        Nothing
+        [ (Just label, component)
+        | ((Just label, True), region) <- Map.toAscList (planarLayerRegions published)
+        , component <- planarRegionComponents region
+        ]
+    )
+
+
+affineFormGenerators
+  :: Map label AffineForm
+  -> Either (UpperEnvelopeError label) (NonEmpty (ExactPowerGenerator label))
+affineFormGenerators forms =
+  case Map.minViewWithKey forms of
+    Nothing -> Left UpperEnvelopeEmptyForms
+    Just ((firstLabel, firstForm), remaining) ->
+      Right
+        ( affineFormGenerator firstLabel firstForm
+            :| fmap (uncurry affineFormGenerator) (Map.toAscList remaining)
+        )
+
+affineFormGenerator
+  :: label
+  -> AffineForm
+  -> ExactPowerGenerator label
+affineFormGenerator label form =
+  ExactPowerGenerator
+    { exactPowerGeneratorLabel = label
+    , exactPowerGeneratorXCoefficient = affineFormXCoefficient form
+    , exactPowerGeneratorYCoefficient = affineFormYCoefficient form
+    , exactPowerGeneratorConstant = affineFormConstant form
+    }
+
+exactGeneratorDispositionsWith
+  :: Ord label
+  => (summary -> ExactClipReceipt -> summary)
+  -> summary
+  -> ConvexPolygon
+  -> NonEmpty (ExactPowerGenerator label)
+  -> Either
+      (PowerDiagramError label)
+      ( Map label (PowerCellDisposition label)
+      , summary
+      , GeneratorRegularReceipt
+      , Int
+      )
+exactGeneratorDispositionsWith summarizeReceipt initialSummary domain generators = do
+  section <- first PowerRegularTopologyFailed (resolvedGeneratorSection generators)
+  exactResolvedGeneratorDispositionsWith
+    summarizeReceipt
+    initialSummary
+    domain
+    section
+
+exactResolvedGeneratorDispositionsWith
+  :: Ord label
+  => (summary -> ExactClipReceipt -> summary)
+  -> summary
+  -> ConvexPolygon
+  -> ResolvedGeneratorSection label
+  -> Either
+      (PowerDiagramError label)
+      ( Map label (PowerCellDisposition label)
+      , summary
+      , GeneratorRegularReceipt
+      , Int
+      )
+exactResolvedGeneratorDispositionsWith summarizeReceipt initialSummary domain section = do
+  let retainedDomain = retainConvexPolygon domain
+      topology = resolvedRegularTopology section
+  constraints <- prepareRegularConstraintSection topology
+  clipped <-
+    traverse
+      (clipPowerCell retainedDomain section constraints)
+      (generatorRegularDispositions topology)
+  let (dispositions, summary, maximumCellConstraints) =
+        summarizeClippedPowerCells
+          summarizeReceipt
+          initialSummary
+          (resolvedCoincidentDispositions section)
+          (NonEmpty.toList clipped)
+  pure
+    ( dispositions
+    , summary
+    , generatorRegularReceipt topology
+    , maximumCellConstraints
+    )
+
+exactRegularSectionDispositionsWith
+  :: Ord label
+  => (summary -> ExactClipReceipt -> summary)
+  -> summary
+  -> ConvexPolygon
+  -> RegularSection label
+  -> Either
+      (PowerDiagramError label)
+      ( Map label (PowerCellDisposition label)
+      , summary
+      , GeneratorRegularReceipt
+      , Int
+      )
+exactRegularSectionDispositionsWith summarizeReceipt initialSummary domain section = do
+  let retainedDomain = retainConvexPolygon domain
+  constraints <- preparePublishedRegularConstraints section
+  representativeDispositions <-
+    traverse
+      (\(label, generator) -> do
+         disposition <-
+           maybe
+             (Left (PowerRegularDispositionMissing label))
+             Right
+             (Map.lookup label (sectionRegularDispositions section))
+         clipPublishedPowerCell
+           retainedDomain
+           section
+           constraints
+           generator
+           disposition)
+      (Map.toAscList (sectionGenerators section))
+  let (dispositions, summary, maximumCellConstraints) =
+        summarizeClippedPowerCells
+          summarizeReceipt
+          initialSummary
+          (sectionCoincidentDispositions section)
+          representativeDispositions
+  pure
+    ( dispositions
+    , summary
+    , sectionRegularReceipt section
+    , maximumCellConstraints
+    )
+
+summarizeClippedPowerCells
+  :: Ord label
+  => (summary -> ExactClipReceipt -> summary)
+  -> summary
+  -> Map label (CoincidentGeneratorDisposition label)
+  -> [(label, PowerCellDisposition label, ExactClipReceipt, Int)]
+  -> (Map label (PowerCellDisposition label), summary, Int)
+summarizeClippedPowerCells summarizeReceipt initialSummary coincident clipped =
+  let (publishedAssociations, summary, maximumCellConstraints) =
+        Foldable.foldl'
+          (\(associations, accumulatedSummary, peak) (label, disposition, cellReceipt, cellAxes) ->
+             let !combinedSummary = summarizeReceipt accumulatedSummary cellReceipt
+              in ( (label, disposition) : associations
+                 , combinedSummary
+                 , max peak cellAxes
+                 ))
+          ([], initialSummary, 0)
+          clipped
+   in ( fmap coincidentPowerDisposition coincident
+          <> Map.fromList publishedAssociations
+      , summary
+      , maximumCellConstraints
+      )
+
+preparePublishedRegularConstraints
+  :: Ord label
+  => RegularSection label
+  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
+preparePublishedRegularConstraints section =
+  Foldable.foldlM prepare Map.empty (Map.elems (sectionRegularEdges section))
+ where
+  prepare constraints edgeSection = do
+    let edge = sectionRegularEdge edgeSection
+        (firstLabel, secondLabel) = regularEdgeLabels edge
+    firstGenerator <- requireSectionGenerator section firstLabel
+    secondGenerator <- requireSectionGenerator section secondLabel
+    insertRegularConstraintEdge constraints firstGenerator secondGenerator
+
+clipPublishedPowerCell
+  :: Ord label
+  => ExactRetainedPolygon
+  -> RegularSection label
+  -> Map label [ExactClosedHalfPlane]
+  -> ExactPowerGenerator label
+  -> RegularSiteDisposition label
+  -> Either
+      (PowerDiagramError label)
+      (label, PowerCellDisposition label, ExactClipReceipt, Int)
+clipPublishedPowerCell retainedDomain section constraints ownerGenerator disposition =
+  let ownerLabel = exactPowerGeneratorLabel ownerGenerator
+      competitors =
+        filter
+          ((/= ownerLabel) . exactPowerGeneratorLabel)
+          (Map.elems (sectionGenerators section))
+      clipAs =
+        clipRegularGeneratorCell
+          retainedDomain
+          ownerGenerator
+          (Map.findWithDefault [] ownerLabel constraints)
+          competitors
+   in case disposition of
+        RegularSiteHidden -> clipAs RegularGeneratorHidden
+        RegularSiteVisible -> clipAs RegularGeneratorVisible
+        RegularSiteLowerDimensional -> clipAs RegularGeneratorLowerDimensional
+        RegularSiteCoincidentEquivalentTo _ ->
+          Left (PowerRegularDispositionMissing ownerLabel)
+        RegularSiteCoincidentDominatedBy _ ->
+          Left (PowerRegularDispositionMissing ownerLabel)
+
+requireSectionGenerator
+  :: Ord label
+  => RegularSection label
+  -> label
+  -> Either (PowerDiagramError label) (ExactPowerGenerator label)
+requireSectionGenerator section label =
+  maybe
+    (Left (PowerRegularGeneratorMissing label))
+    Right
+    (Map.lookup label (sectionGenerators section))
+
+discardClipReceipt :: () -> ExactClipReceipt -> ()
+discardClipReceipt _ _ = ()
+
+prepareRadicalAxis
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either (PowerDiagramError label) ExactAffineLine
+prepareRadicalAxis firstGenerator secondGenerator =
+  first
+    ( PowerBisectorInvalid
+        (exactPowerGeneratorLabel firstGenerator)
+        (exactPowerGeneratorLabel secondGenerator)
+    )
+    (exactGeneratorAxis firstGenerator secondGenerator)
+
+prepareRegularConstraintSection
+  :: Ord label
+  => GeneratorRegularTopology label
+  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
+prepareRegularConstraintSection =
+  Foldable.foldlM prepareRegularConstraintEdge Map.empty . generatorRegularEdges
+
+prepareRegularConstraintEdge
+  :: Ord label
+  => Map label [ExactClosedHalfPlane]
+  -> GeneratorRegularEdge label
+  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
+prepareRegularConstraintEdge constraints edge = do
+  let firstGenerator = generatorRegularEdgeFirst edge
+      secondGenerator = generatorRegularEdgeSecond edge
+  insertRegularConstraintEdge constraints firstGenerator secondGenerator
+
+insertRegularConstraintEdge
+  :: Ord label
+  => Map label [ExactClosedHalfPlane]
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
+insertRegularConstraintEdge constraints firstGenerator secondGenerator = do
+  let firstLabel = exactPowerGeneratorLabel firstGenerator
+      secondLabel = exactPowerGeneratorLabel secondGenerator
+  axis <- prepareRadicalAxis firstGenerator secondGenerator
+  pure
+    ( Map.insertWith (<>) secondLabel [exactClosedHalfPlane (oppositeExactAffineLine axis)]
+        (Map.insertWith (<>) firstLabel [exactClosedHalfPlane axis] constraints)
+    )
+
+clipPowerCell
+  :: Ord label
+  => ExactRetainedPolygon
+  -> ResolvedGeneratorSection label
+  -> Map label [ExactClosedHalfPlane]
+  -> (ExactPowerGenerator label, RegularGeneratorDisposition)
+  -> Either
+      (PowerDiagramError label)
+      (label, PowerCellDisposition label, ExactClipReceipt, Int)
+clipPowerCell retainedDomain section constraints (ownerGenerator, disposition) =
+  let ownerLabel = exactPowerGeneratorLabel ownerGenerator
+      topology = resolvedRegularTopology section
+      competitors =
+        [ generator
+        | (generator, _) <-
+            NonEmpty.toList (generatorRegularDispositions topology)
+        , exactPowerGeneratorLabel generator /= ownerLabel
+        ]
+   in clipRegularGeneratorCell
+        retainedDomain
+        ownerGenerator
+        (Map.findWithDefault [] ownerLabel constraints)
+        competitors
+        disposition
+
+clipRegularGeneratorCell
+  :: ExactRetainedPolygon
+  -> ExactPowerGenerator label
+  -> [ExactClosedHalfPlane]
+  -> [ExactPowerGenerator label]
+  -> RegularGeneratorDisposition
+  -> Either
+      (PowerDiagramError label)
+      (label, PowerCellDisposition label, ExactClipReceipt, Int)
+clipRegularGeneratorCell retainedDomain ownerGenerator activeConstraints competitors disposition =
+  case disposition of
+    RegularGeneratorHidden ->
+      pure (exactPowerGeneratorLabel ownerGenerator, EmptyPowerCell, emptyExactClipReceipt, 0)
+    RegularGeneratorVisible ->
+      finishPowerCell retainedDomain ownerGenerator activeConstraints
+    RegularGeneratorLowerDimensional -> do
+      halfPlanes <- traverse (preparedDirectHalfPlane ownerGenerator) competitors
+      finishPowerCell retainedDomain ownerGenerator halfPlanes
+
+preparedDirectHalfPlane
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> Either (PowerDiagramError label) ExactClosedHalfPlane
+preparedDirectHalfPlane owner competitor =
+  exactClosedHalfPlane <$> prepareRadicalAxis owner competitor
+
+finishPowerCell
+  :: ExactRetainedPolygon
+  -> ExactPowerGenerator label
+  -> [ExactClosedHalfPlane]
+  -> Either
+      (PowerDiagramError label)
+      (label, PowerCellDisposition label, ExactClipReceipt, Int)
+finishPowerCell retainedDomain ownerGenerator halfPlanes = do
+  (exactDisposition, receipt) <-
+    first (PowerCellClipFailed (exactPowerGeneratorLabel ownerGenerator))
+      (exactClipRetainedPolygon retainedDomain halfPlanes)
+  pure
+    ( exactPowerGeneratorLabel ownerGenerator
+    , case exactDisposition of
+        ExactClipFullDimensional retained ->
+          PublishedPowerCell (convexPolygonFromRetained retained)
+        ExactClipLowerDimensional points -> LowerDimensionalPowerCell points
+        ExactClipEmpty -> EmptyPowerCell
+    , receipt
+    , length halfPlanes
+    )
+
+aggregateReceipt
+  :: ConvexPolygon
+  -> Map label (PowerCellDisposition label)
+  -> Int
+  -> ExactClipReceipt
+  -> GeneratorRegularReceipt
+  -> Int
+  -> PowerDiagramReceipt
+aggregateReceipt domain dispositions siteInputBits clipReceipt regularReceipt maximumCellConstraints =
+  let inputBits = max siteInputBits (exactClipInputCoordinateBits clipReceipt)
+      peakBits = exactClipPeakIntermediateCoordinateBits clipReceipt
+      finalBits = exactClipFinalCoordinateBits clipReceipt
+      dispositionCounts = countPowerDispositions dispositions
+   in PowerDiagramReceipt
+        { powerDiagramDomainVertices = NonEmpty.length (convexPolygonPoints domain)
+        , powerDiagramSubmittedSiteConstraints = exactClipSubmittedHalfPlanes clipReceipt
+        , powerDiagramActiveBoundaries = exactClipActiveBoundaries clipReceipt
+        , powerDiagramBoundaryCompatibilityChecks = exactClipBoundaryCompatibilityChecks clipReceipt
+        , powerDiagramExactIntersections = exactClipExactIntersections clipReceipt
+        , powerDiagramPublishedCells = countedPublishedCells dispositionCounts
+        , powerDiagramLowerDimensionalCells = countedLowerDimensionalCells dispositionCounts
+        , powerDiagramEmptyCells = countedEmptyCells dispositionCounts
+        , powerDiagramCoincidentEquivalentCells = countedCoincidentEquivalentCells dispositionCounts
+        , powerDiagramCoincidentDominatedCells = countedCoincidentDominatedCells dispositionCounts
+        , powerDiagramRegularFaces = generatorRegularFaceCount regularReceipt
+        , powerDiagramRegularEdges = generatorRegularEdgeCount regularReceipt
+        , powerDiagramOracleCells = generatorRegularLowerDimensionalSites regularReceipt
+        , powerDiagramMaximumCellConstraints = maximumCellConstraints
+        , powerDiagramMaximumInputBits = inputBits
+        , powerDiagramMaximumAffineCoefficientBits = exactClipMaximumAffineCoefficientBits clipReceipt
+        , powerDiagramPeakIntermediateCoordinateBits = peakBits
+        , powerDiagramFinalCoordinateBits = finalBits
+        , powerDiagramFinalDenominatorBits = exactClipFinalDenominatorBits clipReceipt
+        }
+
+data PowerDispositionCounts = PowerDispositionCounts
+  { countedPublishedCells :: !Int
+  , countedLowerDimensionalCells :: !Int
+  , countedEmptyCells :: !Int
+  , countedCoincidentEquivalentCells :: !Int
+  , countedCoincidentDominatedCells :: !Int
+  }
+
+countPowerDispositions
+  :: Map label (PowerCellDisposition label)
+  -> PowerDispositionCounts
+countPowerDispositions =
+  Map.foldl'
+    (\counts disposition -> case disposition of
+        PublishedPowerCell _ ->
+          counts {countedPublishedCells = countedPublishedCells counts + 1}
+        LowerDimensionalPowerCell _ ->
+          counts {countedLowerDimensionalCells = countedLowerDimensionalCells counts + 1}
+        EmptyPowerCell ->
+          counts {countedEmptyCells = countedEmptyCells counts + 1}
+        CoincidentEquivalentTo _ ->
+          counts {countedCoincidentEquivalentCells = countedCoincidentEquivalentCells counts + 1}
+        CoincidentDominatedBy _ ->
+          counts {countedCoincidentDominatedCells = countedCoincidentDominatedCells counts + 1})
+    PowerDispositionCounts
+      { countedPublishedCells = 0
+      , countedLowerDimensionalCells = 0
+      , countedEmptyCells = 0
+      , countedCoincidentEquivalentCells = 0
+      , countedCoincidentDominatedCells = 0
+      }
diff --git a/src-public/Moonlight/Planar/Internal/PowerDiagram/Section.hs b/src-public/Moonlight/Planar/Internal/PowerDiagram/Section.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Internal/PowerDiagram/Section.hs
@@ -0,0 +1,479 @@
+-- | Batch normalization and publication of one exact regular section.
+module Moonlight.Planar.Internal.PowerDiagram.Section where
+
+import Data.Bifunctor (first)
+import qualified Data.Foldable as Foldable
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Map.Strict (Map)
+import Data.Ord (comparing)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , exactPointBitWidth
+  , exactPointCoordinates
+  , exactPointFromQueryPoint
+  )
+import Moonlight.Planar.Internal.BoundaryCycle (consecutivePairs)
+import Moonlight.Planar.Internal.ExactRational (exactRationalBitWidth)
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+import Moonlight.Planar.Internal.PowerDiagram.Hull (regularGeneratorTopology)
+import Moonlight.Planar.Internal.PowerDiagram.Locality (buildRegularLocality)
+import Moonlight.Planar.Internal.PowerDiagram.Model
+
+regularTriangulation
+  :: Ord label
+  => NonEmpty (PowerSite label)
+  -> Either
+      (PowerDiagramError label)
+      (RegularTriangulation label, RegularTriangulationReceipt)
+regularTriangulation submitted = do
+  sortedSites <- validateAndSortSites submitted
+  triangulation <-
+    first PowerRegularTopologyFailed
+      ( constructRegularTriangulation
+          (Map.fromDistinctAscList (fmap (\site -> (powerSiteLabel site, site)) (NonEmpty.toList sortedSites)))
+      )
+  pure (triangulation, regularTriangulationReceipt triangulation)
+
+-- | The empty all-site section. It is the identity for insertion and the
+-- result of removing the final site.
+emptyRegularTriangulation :: RegularTriangulation label
+emptyRegularTriangulation =
+  RegularTriangulation
+    { storedRegularSites = Map.empty
+    , storedRegularSection = Nothing
+    }
+
+-- | Look up one canonical site by its stable label.
+regularSite
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Maybe (PowerSite label)
+regularSite label = Map.lookup label . storedRegularSites
+
+-- | Enumerate every canonical site in ascending label order, including hidden
+-- and coincident sites.
+regularSites :: RegularTriangulation label -> [PowerSite label]
+regularSites = Map.elems . storedRegularSites
+
+-- | Number of canonical sites, independent of their visibility.
+regularSiteCount :: RegularTriangulation label -> Int
+regularSiteCount = Map.size . storedRegularSites
+
+-- | Observe one site's exhaustive derived visibility disposition.
+regularSiteDisposition
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Maybe (RegularSiteDisposition label)
+regularSiteDisposition label triangulation =
+  storedRegularSection triangulation
+    >>= Map.lookup label . sectionRegularDispositions
+
+-- | Enumerate canonical visible faces in ascending key order.
+regularFaces :: RegularTriangulation label -> [RegularFace label]
+regularFaces =
+  maybe [] (Map.elems . sectionRegularFaces) . storedRegularSection
+
+-- | Enumerate canonical regular edges in ascending endpoint order.
+regularEdges :: RegularTriangulation label -> [RegularEdge label]
+regularEdges =
+  maybe [] (fmap sectionRegularEdge . Map.elems . sectionRegularEdges)
+    . storedRegularSection
+
+-- | Exact neighbours of one visible site; nonvisible and absent sites have none.
+regularNeighbours
+  :: Ord label
+  => label
+  -> RegularTriangulation label
+  -> Set label
+regularNeighbours label =
+  maybe
+    Set.empty
+    ( maybe Set.empty sectionSiteNeighbours
+        . Map.lookup label
+        . sectionRegularStars
+    )
+    . storedRegularSection
+
+-- | Construction statistics derived from the sealed normalized section.
+regularTriangulationReceipt
+  :: RegularTriangulation label
+  -> RegularTriangulationReceipt
+regularTriangulationReceipt triangulation =
+  case storedRegularSection triangulation of
+    Nothing -> emptyRegularTriangulationReceipt
+    Just section ->
+      regularReceiptFromSection (regularSiteCount triangulation) section
+
+validateAndSortSites
+  :: Ord label
+  => NonEmpty (PowerSite label)
+  -> Either (PowerDiagramError label) (NonEmpty (PowerSite label))
+validateAndSortSites submitted =
+  let sorted = NonEmpty.sortBy (comparing powerSiteLabel) submitted
+   in case List.find (uncurry sameLabel) (consecutivePairs (NonEmpty.toList sorted)) of
+        Just (duplicate, _) -> Left (DuplicatePowerSiteLabel (powerSiteLabel duplicate))
+        Nothing -> Right sorted
+ where
+  sameLabel :: Eq label => PowerSite label -> PowerSite label -> Bool
+  sameLabel left right = powerSiteLabel left == powerSiteLabel right
+
+maximumPowerSiteInputBits :: Foldable sites => sites (PowerSite label) -> Int
+maximumPowerSiteInputBits =
+  Foldable.foldl' (\bits site -> max bits (powerSiteInputBitWidth site)) 0
+
+constructRegularTriangulation
+  :: Ord label
+  => Map label (PowerSite label)
+  -> Either (RegularTopologyError label) (RegularTriangulation label)
+constructRegularTriangulation sites =
+  case NonEmpty.nonEmpty (Map.elems sites) of
+    Nothing -> Right emptyRegularTriangulation
+    Just sortedSites -> do
+      let generators = fmap prepareExactPowerGenerator sortedSites
+      section <- resolvedGeneratorSection generators
+      pure (publishRegularSection sites section)
+
+prepareExactPowerGenerator :: PowerSite label -> ExactPowerGenerator label
+prepareExactPowerGenerator site =
+  let point = exactPointFromQueryPoint (powerSiteQueryPoint site)
+      (coordinateX, coordinateY) = exactPointCoordinates point
+      weight = powerWeightExact (powerSiteWeight site)
+   in ExactPowerGenerator
+        { exactPowerGeneratorLabel = powerSiteLabel site
+        , exactPowerGeneratorXCoefficient = 2 * coordinateX
+        , exactPowerGeneratorYCoefficient = 2 * coordinateY
+        , exactPowerGeneratorConstant =
+            weight - coordinateX * coordinateX - coordinateY * coordinateY
+        }
+
+powerSiteInputBitWidth :: PowerSite label -> Int
+powerSiteInputBitWidth site =
+  max
+    (exactPointBitWidth (exactPointFromQueryPoint (powerSiteQueryPoint site)))
+    (exactRationalBitWidth (powerWeightExact (powerSiteWeight site)))
+
+groupGeneratorsBySlope
+  :: NonEmpty (ExactPowerGenerator label)
+  -> NonEmpty (NonEmpty (ExactPowerGenerator label))
+groupGeneratorsBySlope (initial :| remaining) =
+  let initialSlope = exactGeneratorSlope initial
+      (sameInitialSlope, otherGenerators) =
+        List.partition ((== initialSlope) . exactGeneratorSlope) remaining
+      otherGroups =
+        Map.fromListWith
+          (<>)
+          [ (exactGeneratorSlope generator, generator :| [])
+          | generator <- otherGenerators
+          ]
+   in (initial :| sameInitialSlope) :| Map.elems otherGroups
+
+resolveCoincidentGeneratorGroup
+  :: Ord label
+  => NonEmpty (ExactPowerGenerator label)
+  -> (ExactPowerGenerator label, [(label, CoincidentGeneratorDisposition label)])
+resolveCoincidentGeneratorGroup generators@(initial :| remaining) =
+  let representative = List.foldl' chooseCoincidentRepresentative initial remaining
+      representativeLabel = exactPowerGeneratorLabel representative
+   in ( representative
+      , [ ( exactPowerGeneratorLabel generator
+          , classifyCoincidentGenerator representative generator
+          )
+        | generator <- NonEmpty.toList generators
+        , exactPowerGeneratorLabel generator /= representativeLabel
+        ]
+      )
+
+chooseCoincidentRepresentative
+  :: Ord label
+  => ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> ExactPowerGenerator label
+chooseCoincidentRepresentative selected candidate =
+  case
+    compareCoincidentPriority
+      (exactPowerGeneratorConstant candidate, exactPowerGeneratorLabel candidate)
+      (exactPowerGeneratorConstant selected, exactPowerGeneratorLabel selected) of
+    GT -> candidate
+    _ -> selected
+
+compareCoincidentPriority
+  :: (Ord label, Ord value)
+  => (value, label)
+  -> (value, label)
+  -> Ordering
+compareCoincidentPriority (candidateValue, candidateLabel) (selectedValue, selectedLabel) =
+  compare candidateValue selectedValue <> compare selectedLabel candidateLabel
+
+classifyCoincidentGenerator
+  :: ExactPowerGenerator label
+  -> ExactPowerGenerator label
+  -> CoincidentGeneratorDisposition label
+classifyCoincidentGenerator representative candidate =
+  classifyCoincidentValue
+    (exactPowerGeneratorLabel representative)
+    (exactPowerGeneratorConstant representative)
+    (exactPowerGeneratorConstant candidate)
+
+classifyCoincidentValue
+  :: Eq value
+  => label
+  -> value
+  -> value
+  -> CoincidentGeneratorDisposition label
+classifyCoincidentValue representativeLabel representativeValue candidateValue
+  | candidateValue == representativeValue =
+      CoincidentGeneratorEquivalentTo representativeLabel
+  | otherwise = CoincidentGeneratorDominatedBy representativeLabel
+
+
+coincidentPowerDisposition
+  :: CoincidentGeneratorDisposition label
+  -> PowerCellDisposition label
+coincidentPowerDisposition disposition =
+  case disposition of
+    CoincidentGeneratorEquivalentTo label -> CoincidentEquivalentTo label
+    CoincidentGeneratorDominatedBy label -> CoincidentDominatedBy label
+
+coincidentRegularDisposition
+  :: CoincidentGeneratorDisposition label
+  -> RegularSiteDisposition label
+coincidentRegularDisposition disposition =
+  case disposition of
+    CoincidentGeneratorEquivalentTo label -> RegularSiteCoincidentEquivalentTo label
+    CoincidentGeneratorDominatedBy label -> RegularSiteCoincidentDominatedBy label
+
+
+resolvedGeneratorSection
+  :: Ord label
+  => NonEmpty (ExactPowerGenerator label)
+  -> Either (RegularTopologyError label) (ResolvedGeneratorSection label)
+resolvedGeneratorSection generators = do
+  let resolvedGroups = fmap resolveCoincidentGeneratorGroup (groupGeneratorsBySlope generators)
+      representatives = fmap fst resolvedGroups
+      coincidentDispositions =
+        Map.fromList (concatMap snd (NonEmpty.toList resolvedGroups))
+  topology <- regularGeneratorTopology (DistinctSlopeGenerators representatives)
+  pure
+    ResolvedGeneratorSection
+      { resolvedCoincidentDispositions = coincidentDispositions
+      , resolvedRegularTopology = topology
+      }
+
+publishRegularSection
+  :: Ord label
+  => Map label (PowerSite label)
+  -> ResolvedGeneratorSection label
+  -> RegularTriangulation label
+publishRegularSection sites resolvedSection =
+  let topology = resolvedRegularTopology resolvedSection
+      coincident = resolvedCoincidentDispositions resolvedSection
+      representativeDispositions = publishRepresentativeDispositions topology
+      dispositions =
+        representativeDispositions <> fmap coincidentRegularDisposition coincident
+      generators =
+        Map.fromList
+          [ (exactPowerGeneratorLabel generator, generator)
+          | (generator, _) <- NonEmpty.toList (generatorRegularDispositions topology)
+          ]
+      faces = fmap publishGeneratorFace (generatorRegularFaces topology)
+      faceSection = Map.fromList (fmap (\face -> (regularFaceKey face, face)) faces)
+      edgeFaceIncidence =
+        List.foldl'
+          (Map.unionWith Set.union)
+          Map.empty
+          (fmap regularFaceEdgeIncidence faces)
+      edges = fmap publishGeneratorEdge (generatorRegularEdges topology)
+      edgeSection =
+        Map.fromList
+          [ ( key
+            , RegularEdgeSection
+                edge
+                (Map.findWithDefault Set.empty key edgeFaceIncidence)
+            )
+          | edge <- edges
+          , let key = regularEdgeKey edge
+          ]
+      stars = regularSiteStars dispositions faceSection edgeSection
+      baseSection =
+        RegularSection
+          { sectionGenerators = generators
+          , sectionSlopeRepresentatives =
+              Map.fromList
+                [ (exactGeneratorSlope generator, label)
+                | (label, generator) <- Map.toAscList generators
+                ]
+          , sectionCoincidentDispositions = coincident
+          , sectionRegularDispositions = dispositions
+          , sectionRegularFaces = faceSection
+          , sectionRegularEdges = edgeSection
+          , sectionRegularStars = stars
+          , sectionRegularReceipt = generatorRegularReceipt topology
+          , sectionRegularLocality = Nothing
+          }
+      section =
+        baseSection
+          { sectionRegularLocality = buildRegularLocality baseSection
+          }
+   in RegularTriangulation
+        { storedRegularSites = sites
+        , storedRegularSection = Just section
+        }
+
+regularReceiptFromSection
+  :: Int
+  -> RegularSection label
+  -> RegularTriangulationReceipt
+regularReceiptFromSection inputSites section =
+  let generatorReceipt = sectionRegularReceipt section
+      coincidentCount = Map.size (sectionCoincidentDispositions section)
+   in RegularTriangulationReceipt
+        { regularTriangulationInputSites = inputSites
+        , regularTriangulationRepresentativeSites =
+            generatorRegularInputSites generatorReceipt
+        , regularTriangulationVisibleSites =
+            generatorRegularVisibleSites generatorReceipt
+        , regularTriangulationLowerDimensionalSites =
+            generatorRegularLowerDimensionalSites generatorReceipt
+        , regularTriangulationHiddenSites =
+            generatorRegularHiddenSites generatorReceipt
+        , regularTriangulationCoincidentSites = coincidentCount
+        , regularTriangulationFaces = generatorRegularFaceCount generatorReceipt
+        , regularTriangulationEdges = generatorRegularEdgeCount generatorReceipt
+        }
+
+emptyRegularTriangulationReceipt :: RegularTriangulationReceipt
+emptyRegularTriangulationReceipt =
+  RegularTriangulationReceipt
+    { regularTriangulationInputSites = 0
+    , regularTriangulationRepresentativeSites = 0
+    , regularTriangulationVisibleSites = 0
+    , regularTriangulationLowerDimensionalSites = 0
+    , regularTriangulationHiddenSites = 0
+    , regularTriangulationCoincidentSites = 0
+    , regularTriangulationFaces = 0
+    , regularTriangulationEdges = 0
+    }
+
+emptyGeneratorRegularReceipt :: GeneratorRegularReceipt
+emptyGeneratorRegularReceipt =
+  GeneratorRegularReceipt
+    { generatorRegularInputSites = 0
+    , generatorRegularVisibleSites = 0
+    , generatorRegularLowerDimensionalSites = 0
+    , generatorRegularHiddenSites = 0
+    , generatorRegularFaceCount = 0
+    , generatorRegularEdgeCount = 0
+    }
+
+publishGeneratorDisposition
+  :: RegularGeneratorDisposition
+  -> RegularSiteDisposition label
+publishGeneratorDisposition disposition =
+  case disposition of
+    RegularGeneratorVisible -> RegularSiteVisible
+    RegularGeneratorLowerDimensional -> RegularSiteLowerDimensional
+    RegularGeneratorHidden -> RegularSiteHidden
+
+publishGeneratorFace
+  :: Ord label
+  => GeneratorRegularFace label
+  -> RegularFace label
+publishGeneratorFace face =
+  canonicalRegularFace
+    (generatorRegularFaceFirst face)
+    (generatorRegularFaceSecond face)
+    (generatorRegularFaceThird face)
+    (generatorRegularFaceDualPoint face)
+
+canonicalRegularFace
+  :: Ord label
+  => label
+  -> label
+  -> label
+  -> ExactPoint
+  -> RegularFace label
+canonicalRegularFace firstLabel secondLabel thirdLabel dualPoint
+  | firstLabel <= secondLabel && firstLabel <= thirdLabel =
+      RegularFace firstLabel secondLabel thirdLabel dualPoint
+  | secondLabel <= thirdLabel =
+      RegularFace secondLabel thirdLabel firstLabel dualPoint
+  | otherwise =
+      RegularFace thirdLabel firstLabel secondLabel dualPoint
+
+publishGeneratorEdge :: GeneratorRegularEdge label -> RegularEdge label
+publishGeneratorEdge edge =
+  RegularEdge
+    (exactPowerGeneratorLabel (generatorRegularEdgeFirst edge))
+    (exactPowerGeneratorLabel (generatorRegularEdgeSecond edge))
+    (generatorRegularEdgeDual edge)
+
+regularSiteStars
+  :: Ord label
+  => Map label disposition
+  -> Map (RegularFaceKey label) (RegularFace label)
+  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
+  -> Map label (RegularSiteStar label)
+regularSiteStars dispositions faces =
+  Map.foldl'
+    insertRegularEdgeStar
+    (Map.foldlWithKey' insertRegularFaceStar (emptyRegularStars dispositions) faces)
+
+emptyRegularStars
+  :: Map label disposition
+  -> Map label (RegularSiteStar label)
+emptyRegularStars = Map.map (const emptyRegularSiteStar)
+
+publishRepresentativeDispositions
+  :: Ord label
+  => GeneratorRegularTopology label
+  -> Map label (RegularSiteDisposition label)
+publishRepresentativeDispositions topology =
+  Map.fromList
+    [ ( exactPowerGeneratorLabel generator
+      , publishGeneratorDisposition disposition
+      )
+    | (generator, disposition) <-
+        NonEmpty.toList (generatorRegularDispositions topology)
+    ]
+
+insertRegularFaceStar
+  :: Ord label
+  => Map label (RegularSiteStar label)
+  -> RegularFaceKey label
+  -> RegularFace label
+  -> Map label (RegularSiteStar label)
+insertRegularFaceStar stars faceKey face =
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+   in Foldable.foldl'
+        (\current label -> Map.adjust (addIncidentFace faceKey) label current)
+        stars
+        [firstLabel, secondLabel, thirdLabel]
+
+insertRegularEdgeStar
+  :: Ord label
+  => Map label (RegularSiteStar label)
+  -> RegularEdgeSection label
+  -> Map label (RegularSiteStar label)
+insertRegularEdgeStar stars edgeSection =
+  let (firstLabel, secondLabel) = regularEdgeLabels (sectionRegularEdge edgeSection)
+   in Map.adjust (addNeighbour secondLabel) firstLabel
+        (Map.adjust (addNeighbour firstLabel) secondLabel stars)
+
+addIncidentFace
+  :: Ord label
+  => RegularFaceKey label
+  -> RegularSiteStar label
+  -> RegularSiteStar label
+addIncidentFace faceKey star =
+  star{sectionIncidentFaces = Set.insert faceKey (sectionIncidentFaces star)}
+
+addNeighbour :: Ord label => label -> RegularSiteStar label -> RegularSiteStar label
+addNeighbour label star =
+  star{sectionSiteNeighbours = Set.insert label (sectionSiteNeighbours star)}
diff --git a/src-public/Moonlight/Planar/Minkowski.hs b/src-public/Moonlight/Planar/Minkowski.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Minkowski.hs
@@ -0,0 +1,456 @@
+-- | Exact polygonal Minkowski addition and regularized two-dimensional
+-- morphology. Convex convolution is direct; nonconvex construction descends
+-- through exact vertical slabs, the exact overlay, and grouped publication
+-- owners. Lower-dimensional erosion residuals cannot inhabit 'PlanarRegion'
+-- and therefore publish as empty rather than being forged as polygons.
+module Moonlight.Planar.Minkowski
+  ( StructuringElement
+  , structuringElement
+  , MinkowskiOperation (..)
+  , MinkowskiError (..)
+  , MinkowskiReceipt (..)
+  , convexMinkowskiSum
+  , minkowskiSum
+  , erodeBy
+  , openWith
+  , closeWith
+  , polygonOffset
+  ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad (filterM)
+import Data.Bifunctor (first)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Maybe (fromMaybe)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Moonlight.Planar.Convex
+  ( ConvexPolygon
+  , convexPolygonFromLoop
+  , convexHullPolygon
+  , convexPolygonCentroid
+  , convexPolygonPoints
+  , convexPolygonRegion
+  , reflectConvexPolygon
+  , decomposeRegionIntoConvexSlabs
+  )
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , exactClipExactIntersections
+  , exactPointCoordinates
+  )
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..)
+  , vertexIdIndex
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( exactRationalBitWidth
+  )
+import Moonlight.Planar.Internal.Minkowski.Operations
+  ( addExactPoints
+  , convexMinkowskiPolygon
+  , convexMinkowskiSum
+  , erodeConvexBy
+  , structuringElement
+  , structuringElementPolygon
+  )
+import Moonlight.Planar.Internal.Minkowski.Types
+import Moonlight.Planar.Internal.Overlay.Arrangement (overlayFaceInteriorPoint)
+import Moonlight.Planar.Internal.Overlay.Types
+  ( OverlayResult (..)
+  , OverlayVertex (..)
+  )
+import Moonlight.Planar.Overlay
+  ( OverlayReceipt (..)
+  , overlayAll
+  , overlayLayers
+  , overlayReceipt
+  , overlaySelectedRegion
+  )
+import Moonlight.Planar.Region
+  ( PlanarLayer
+  , PlanarRegion
+  , PolygonComponent
+  , RegionPublicationError (..)
+  , RegionPointLocation (..)
+  , emptyPlanarRegion
+  , exactLoopPoints
+  , planarRegionComponents
+  , polygonHoleLoops
+  , polygonOuterLoop
+  , regionPointLocation
+  , regionBoundaryEdges
+  )
+import Moonlight.Planar.Internal.Region.Publication
+  ( planarRegionFromSelectedIncidence
+  , planarLayerFromAdmittedComponents
+  )
+
+data MorphologyMetrics = MorphologyMetrics
+  { metricOverlayPasses :: !Int
+  , metricExactCrossings :: !Int
+  , metricOutputCells :: !(Maybe Int)
+  }
+
+emptyMetrics :: MorphologyMetrics
+emptyMetrics = MorphologyMetrics 0 0 Nothing
+
+appendMetrics :: MorphologyMetrics -> MorphologyMetrics -> MorphologyMetrics
+appendMetrics left right =
+  MorphologyMetrics
+    { metricOverlayPasses = metricOverlayPasses left + metricOverlayPasses right
+    , metricExactCrossings = metricExactCrossings left + metricExactCrossings right
+    , metricOutputCells = metricOutputCells right <|> metricOutputCells left
+    }
+
+minkowskiSum
+  :: PlanarRegion
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+minkowskiSum left right = do
+  (leftPieces, leftMetrics) <- decomposeRegion left
+  (rightPieces, rightMetrics) <- decomposeRegion right
+  let generated =
+        [ convexMinkowskiPolygon leftPiece rightPiece
+        | leftPiece <- leftPieces
+        , rightPiece <- rightPieces
+        ]
+      generatedRegions = map convexPolygonRegion generated
+      convolutionEdges = sum (map (NonEmpty.length . convexPolygonPoints) generated)
+  (result, unionMetrics) <- unionRegions generatedRegions
+  let metrics = leftMetrics `appendMetrics` rightMetrics `appendMetrics` unionMetrics
+  pure
+    ( result
+    , MinkowskiReceipt
+        { minkowskiOperation = MinkowskiAddition
+        , minkowskiInputComponents =
+            length (planarRegionComponents left)
+              + length (planarRegionComponents right)
+        , minkowskiConvexPieces = length leftPieces + length rightPieces
+        , minkowskiGeneratedPieces = length generated
+        , minkowskiGeneratedConvolutionEdges = convolutionEdges
+        , minkowskiOverlayPasses = metricOverlayPasses metrics
+        , minkowskiExactCrossings = metricExactCrossings metrics
+        , minkowskiOutputCells = fromMaybe 0 (metricOutputCells metrics)
+        , minkowskiExactCoordinateBitGrowth =
+            coordinateBitGrowth [left, right] result
+        }
+    )
+
+-- | Erode a polygonal region by an origin-anchored convex kernel and publish
+-- the regularized full-dimensional result. A residual consisting only of
+-- points or segments is represented by 'emptyPlanarRegion'.
+erodeBy
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+erodeBy element source =
+  case singleConvexRegion source of
+    Just sourcePolygon -> convexErosion element source sourcePolygon
+    Nothing -> generalErosion element source
+
+openWith
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+openWith element source = do
+  (eroded, erosionReceipt) <- erodeBy element source
+  (opened, additionReceipt) <- polygonOffset element eroded
+  pure
+    ( opened
+    , composeReceipts
+        MinkowskiOpening
+        (length (planarRegionComponents source))
+        erosionReceipt
+        additionReceipt
+    )
+
+closeWith
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+closeWith element source = do
+  (expanded, additionReceipt) <- polygonOffset element source
+  (closed, erosionReceipt) <- erodeBy element expanded
+  pure
+    ( closed
+    , composeReceipts
+        MinkowskiClosing
+        (length (planarRegionComponents source))
+        additionReceipt
+        erosionReceipt
+    )
+
+polygonOffset
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+polygonOffset element source =
+  minkowskiSum source (convexPolygonRegion (structuringElementPolygon element))
+
+decomposeRegion
+  :: PlanarRegion
+  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
+decomposeRegion region =
+  case traverse convexComponent (planarRegionComponents region) of
+    Just convexPieces -> Right (convexPieces, emptyMetrics)
+    Nothing -> (\pieces -> (pieces, emptyMetrics)) <$> first MinkowskiConvexFailed (decomposeRegionIntoConvexSlabs region)
+
+convexComponent :: PolygonComponent -> Maybe ConvexPolygon
+convexComponent component =
+  case polygonHoleLoops component of
+    [] -> convexPolygonFromLoop (polygonOuterLoop component)
+    _ -> Nothing
+
+singleConvexRegion :: PlanarRegion -> Maybe ConvexPolygon
+singleConvexRegion region =
+  case planarRegionComponents region of
+    [component] -> convexComponent component
+    _ -> Nothing
+
+unionRegions
+  :: [PlanarRegion]
+  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
+unionRegions [] = Right (emptyPlanarRegion, emptyMetrics)
+unionRegions [region] =
+  Right
+    ( region
+    , emptyMetrics{metricOutputCells = Just (length (planarRegionComponents region))}
+    )
+unionRegions (initial : remaining) = do
+  result <- first MinkowskiOverlayFailed
+    (overlayAll (fmap morphologyLayer (initial :| remaining)))
+  published <- first MinkowskiPublicationFailed (overlaySelectedRegion or result)
+  let selectedCells = V.ifoldl'
+        (\count index labels -> if index > 0 && or labels then count + 1 else count)
+        0 (overlayResultLabels result)
+  pure (published, metricsFromOverlay result selectedCells)
+
+convexErosion
+  :: StructuringElement
+  -> PlanarRegion
+  -> ConvexPolygon
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+convexErosion element source sourcePolygon = do
+  (eroded, clipReceipt) <-
+    erodeConvexBy sourcePolygon (structuringElementPolygon element)
+  let result = maybe emptyPlanarRegion convexPolygonRegion eroded
+      outputCells = maybe 0 (const 1) eroded
+      generatedEdges = maybe 0 (NonEmpty.length . convexPolygonPoints) eroded
+  pure
+    ( result
+    , MinkowskiReceipt
+        { minkowskiOperation = MinkowskiErosion
+        , minkowskiInputComponents = 1
+        , minkowskiConvexPieces = 2
+        , minkowskiGeneratedPieces = outputCells
+        , minkowskiGeneratedConvolutionEdges = generatedEdges
+        , minkowskiOverlayPasses = 0
+        , minkowskiExactCrossings =
+            exactClipExactIntersections clipReceipt
+        , minkowskiOutputCells = outputCells
+        , minkowskiExactCoordinateBitGrowth =
+            coordinateBitGrowth
+              [ source
+              , convexPolygonRegion (structuringElementPolygon element)
+              ]
+              result
+        }
+    )
+
+generalErosion
+  :: StructuringElement
+  -> PlanarRegion
+  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
+generalErosion element source
+  | null sourceEdges = Right (emptyPlanarRegion, emptyErosionReceipt)
+  | otherwise = do
+      sweptPolygons <- traverse (sweepBoundaryEdge reflectedKernel) sourceEdges
+      let sweptRegions = map convexPolygonRegion sweptPolygons
+          generatedEdges =
+            sum (map (NonEmpty.length . convexPolygonPoints) sweptPolygons)
+      (contactRegion, unionMetrics) <- unionRegions sweptRegions
+      candidateOverlay <-
+        first MinkowskiOverlayFailed
+          (overlayLayers (morphologyLayer contactRegion) emptyMorphologyLayer)
+      kernelWitness <- first MinkowskiConvexFailed (convexPolygonCentroid kernel)
+      selectedCellIds <-
+        Set.fromList
+          <$> filterM
+            ( classifyCandidateCell
+                source
+                kernelWitness
+                candidateOverlay
+            )
+            (boundedOutsideCellIds candidateOverlay)
+      published <- publishCellSelection selectedCellIds candidateOverlay
+      let candidateMetrics =
+            metricsFromOverlay candidateOverlay (Set.size selectedCellIds)
+          metrics = unionMetrics `appendMetrics` candidateMetrics
+      pure
+        ( published
+        , MinkowskiReceipt
+            { minkowskiOperation = MinkowskiErosion
+            , minkowskiInputComponents = length (planarRegionComponents source)
+            , minkowskiConvexPieces = 1
+            , minkowskiGeneratedPieces = length sweptPolygons
+            , minkowskiGeneratedConvolutionEdges = generatedEdges
+            , minkowskiOverlayPasses = metricOverlayPasses metrics
+            , minkowskiExactCrossings = metricExactCrossings metrics
+            , minkowskiOutputCells = Set.size selectedCellIds
+            , minkowskiExactCoordinateBitGrowth =
+                coordinateBitGrowth
+                  [source, convexPolygonRegion kernel]
+                  published
+            }
+        )
+ where
+  kernel = structuringElementPolygon element
+  reflectedKernel = reflectConvexPolygon kernel
+  sourceEdges = regionBoundaryEdges source
+  emptyErosionReceipt =
+    MinkowskiReceipt
+      { minkowskiOperation = MinkowskiErosion
+      , minkowskiInputComponents = 0
+      , minkowskiConvexPieces = 1
+      , minkowskiGeneratedPieces = 0
+      , minkowskiGeneratedConvolutionEdges = 0
+      , minkowskiOverlayPasses = 0
+      , minkowskiExactCrossings = 0
+      , minkowskiOutputCells = 0
+      , minkowskiExactCoordinateBitGrowth = 0
+      }
+
+sweepBoundaryEdge
+  :: ConvexPolygon
+  -> (ExactPoint, ExactPoint)
+  -> Either MinkowskiError ConvexPolygon
+sweepBoundaryEdge reflectedKernel (from, to) =
+  case convexPolygonPoints reflectedKernel of
+    firstKernelPoint :| remainingKernelPoints ->
+      first MinkowskiConvexFailed $ convexHullPolygon
+        ( addExactPoints from firstKernelPoint
+            :| ( map (addExactPoints from) remainingKernelPoints
+                   <> map (addExactPoints to) kernelPoints
+               )
+        )
+ where
+  kernelPoints = NonEmpty.toList (convexPolygonPoints reflectedKernel)
+
+boundedOutsideCellIds
+  :: OverlayResult (Bool, Bool)
+  -> [FaceId]
+boundedOutsideCellIds result =
+  V.ifoldr
+    (\index (leftLabel, rightLabel) selected ->
+       if index > 0 && not leftLabel && not rightLabel
+         then FaceId (fromIntegral index) : selected
+         else selected)
+    []
+    (overlayResultLabels result)
+
+classifyCandidateCell
+  :: PlanarRegion
+  -> ExactPoint
+  -> OverlayResult (Bool, Bool)
+  -> FaceId
+  -> Either MinkowskiError Bool
+classifyCandidateCell source kernelWitness result cellId = do
+  candidate <- first MinkowskiOverlayFailed (overlayFaceInteriorPoint result cellId)
+  let inclusionWitness = addExactPoints candidate kernelWitness
+  case regionPointLocation source inclusionWitness of
+    RegionInterior -> Right True
+    RegionExterior -> Right False
+    RegionOnBoundary -> Left (MinkowskiInclusionAmbiguous cellId inclusionWitness)
+
+publishCellSelection
+  :: Set.Set FaceId
+  -> OverlayResult labels
+  -> Either MinkowskiError PlanarRegion
+publishCellSelection selected result =
+  first MinkowskiPublicationFailed
+    (planarRegionFromSelectedIncidence (overlayResultIncidence result) exactPointAt (`Set.member` selected))
+ where
+  exactPointAt vertex = maybe
+    (Left (RegionCoordinateMissing vertex))
+    (Right . overlayExactPoint)
+    (overlayResultVertices result V.!? vertexIdIndex vertex)
+
+morphologyLayer
+  :: PlanarRegion
+  -> PlanarLayer Bool
+morphologyLayer region =
+  planarLayerFromAdmittedComponents
+    False
+    [(True, component) | component <- planarRegionComponents region]
+
+emptyMorphologyLayer :: PlanarLayer Bool
+emptyMorphologyLayer = morphologyLayer emptyPlanarRegion
+
+metricsFromOverlay
+  :: OverlayResult labels
+  -> Int
+  -> MorphologyMetrics
+metricsFromOverlay result outputCells =
+  MorphologyMetrics
+    { metricOverlayPasses = 1
+    , metricExactCrossings = overlayExactCrossings (overlayReceipt result)
+    , metricOutputCells = Just outputCells
+    }
+
+composeReceipts
+  :: MinkowskiOperation
+  -> Int
+  -> MinkowskiReceipt
+  -> MinkowskiReceipt
+  -> MinkowskiReceipt
+composeReceipts operation inputComponents firstReceipt secondReceipt =
+  MinkowskiReceipt
+    { minkowskiOperation = operation
+    , minkowskiInputComponents = inputComponents
+    , minkowskiConvexPieces =
+        minkowskiConvexPieces firstReceipt
+          + minkowskiConvexPieces secondReceipt
+    , minkowskiGeneratedPieces =
+        minkowskiGeneratedPieces firstReceipt
+          + minkowskiGeneratedPieces secondReceipt
+    , minkowskiGeneratedConvolutionEdges =
+        minkowskiGeneratedConvolutionEdges firstReceipt
+          + minkowskiGeneratedConvolutionEdges secondReceipt
+    , minkowskiOverlayPasses =
+        minkowskiOverlayPasses firstReceipt
+          + minkowskiOverlayPasses secondReceipt
+    , minkowskiExactCrossings =
+        minkowskiExactCrossings firstReceipt
+          + minkowskiExactCrossings secondReceipt
+    , minkowskiOutputCells = minkowskiOutputCells secondReceipt
+    , minkowskiExactCoordinateBitGrowth =
+        max
+          (minkowskiExactCoordinateBitGrowth firstReceipt)
+          (minkowskiExactCoordinateBitGrowth secondReceipt)
+    }
+
+coordinateBitGrowth :: [PlanarRegion] -> PlanarRegion -> Int
+coordinateBitGrowth inputs output =
+  max 0
+    ( regionCoordinateBits output
+        - List.foldl' (\maximumBits -> max maximumBits . regionCoordinateBits) 0 inputs
+    )
+
+regionCoordinateBits :: PlanarRegion -> Int
+regionCoordinateBits = List.foldl' componentBits 0 . planarRegionComponents
+ where
+  componentBits maximumBits component =
+    List.foldl'
+      loopBits
+      maximumBits
+      (polygonOuterLoop component : polygonHoleLoops component)
+  loopBits maximumBits =
+    List.foldl' pointBits maximumBits . exactLoopPoints
+  pointBits maximumBits point =
+    let (x, y) = exactPointCoordinates point
+     in max
+          maximumBits
+          (max (exactRationalBitWidth x) (exactRationalBitWidth y))
diff --git a/src-public/Moonlight/Planar/Overlay.hs b/src-public/Moonlight/Planar/Overlay.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/Overlay.hs
@@ -0,0 +1,330 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | Exact labelled common refinement. Source sections descend through one
+-- segment-event arrangement; incidence, coordinates and provenance glue before
+-- any polygon publication. Binary64 is not an admission prerequisite.
+module Moonlight.Planar.Overlay
+  ( BoundaryLoopRef (..)
+  , OverlaySourceId (..)
+  , BoundaryRef
+  , BoundaryVertexRef
+  , BoundaryEdgeRef
+  , boundaryRefSource
+  , boundaryRefComponent
+  , boundaryRefLoop
+  , boundaryRefLocalIndex
+  , OverlayVertexOrigin
+  , overlayOriginVertices
+  , overlayOriginEdges
+  , OverlayEdgeOrigin
+  , overlayEdgeSources
+  , OverlayVertex (..)
+  , OverlayReceipt (..)
+  , OverlayArrangementObstruction (..)
+  , OverlayCellWitness (..)
+  , OverlayError (..)
+  , OverlayResult
+  , OverlaySelectionKind (..)
+  , OverlaySelectionError (..)
+  , CoverGap
+  , coverGapRegion
+  , LayerCoverageError (..)
+  , overlayLayers
+  , overlayAll
+  , overlayReceipt
+  , overlayCells
+  , foldBoundedOverlayCells
+  , overlayArrangementVertices
+  , overlayArrangementEdges
+  , overlayPlanarLayer
+  , overlaySelectedRegion
+  , overlayMass
+  , overlayConfusion
+  , layerCovers
+  , overlayClosedUnion
+  , overlayClosedIntersection
+  , overlayRegularizedDifference
+  ) where
+
+import Control.DeepSeq (NFData)
+import Control.Monad (filterM)
+import Data.Bifunctor (first)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import GHC.Generics (Generic)
+import Moonlight.Planar.CellSet (CellSelectionError (..), ExactCellSet)
+import Moonlight.Planar.Exact (ExactPoint)
+import Moonlight.Planar.Internal.HandleDefs
+  ( FaceId (..), UndirectedEdgeId (..), VertexId (..)
+  , faceIdIndex, normalizedDirected, reverseEdge, vertexIdIndex
+  )
+import Moonlight.Planar.Internal.CellSet (closeExactCellSetWith)
+import Moonlight.Planar.Internal.Incidence
+  ( faceBoundaryComponents, faceIsolatedVertices, incidenceDestination
+  , incidenceFaceCount, incidenceIncidentFace, incidenceOrigin
+  , incidenceVertexOutgoingEdges
+  )
+import Moonlight.Planar.Internal.Overlay.Arrangement (certifyArrangement)
+import Moonlight.Planar.Internal.Overlay.Types
+import Moonlight.Planar.Region
+  ( PlanarLayer, PlanarRegion, PolygonComponent, RegionPublicationError (..)
+  , planarLayerOutsideLabel, planarLayerRegions, planarRegionComponents
+  )
+import Moonlight.Planar.Internal.Region.Publication
+  ( planarLayerFromAdmittedComponents, planarRegionFromSelectedIncidence )
+import Moonlight.Planar.Valuation (ExactArea, orientedBoundaryArea)
+
+newtype CoverGap = CoverGap PlanarRegion
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+coverGapRegion :: CoverGap -> PlanarRegion
+coverGapRegion (CoverGap region) = region
+
+data LayerCoverageError
+  = LayerCoverageOverlayFailed !OverlayError
+  | LayerCoverageGapPublicationFailed !RegionPublicationError
+  | LayerCoverageGap !CoverGap
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | The binary entrance is a typed two-source view of the same fused owner.
+overlayLayers
+  :: PlanarLayer left -> PlanarLayer right
+  -> Either OverlayError (OverlayResult (left, right))
+overlayLayers left right = do
+  let (leftSource, leftLabels) = encodeSource left
+      (rightSource, rightLabels) = encodeSource right
+  result <- certifyArrangement (leftSource :| [rightSource])
+  traverse
+    (\state -> (,)
+      <$> decodeSource (OverlaySourceId 0) leftLabels state
+      <*> decodeSource (OverlaySourceId 1) rightLabels state)
+    result
+
+-- | Submit every source once. Labels are decoded in input order only after
+-- sparse source-local transitions agree around the complete arrangement.
+overlayAll
+  :: NonEmpty (PlanarLayer label)
+  -> Either OverlayError (OverlayResult (NonEmpty label))
+overlayAll layers = do
+  let sources = fmap encodeSource layers
+      tables = NonEmpty.zip (OverlaySourceId 0 :| fmap OverlaySourceId [1 ..]) (fmap snd sources)
+  result <- certifyArrangement (fmap fst sources)
+  traverse
+    (\state -> traverse (\(source, table) -> decodeSource source table state) tables)
+    result
+
+encodeSource :: PlanarLayer label -> (PlanarLayer OverlayLabelId, V.Vector label)
+encodeSource layer =
+  let regions = Map.toAscList (planarLayerRegions layer)
+   in ( planarLayerFromAdmittedComponents (OverlayLabelId 0)
+          [ (OverlayLabelId index, component)
+          | (index, (_, region)) <- zip [1 ..] regions
+          , component <- planarRegionComponents region
+          ]
+      , V.fromList (planarLayerOutsideLabel layer : fmap fst regions)
+      )
+
+decodeSource
+  :: OverlaySourceId -> V.Vector label -> IntMap.IntMap OverlayLabelId
+  -> Either OverlayError label
+decodeSource source table state =
+  let ordinal = IntMap.findWithDefault (OverlayLabelId 0) (overlaySourceIndex source) state
+   in maybe
+        (Left (OverlayProvenanceIncomplete (OverlaySourceLabelMissing source ordinal)))
+        Right (table V.!? overlayLabelIndex ordinal)
+
+overlayReceipt :: OverlayResult labels -> OverlayReceipt
+overlayReceipt = overlayResultReceipt
+
+overlayCells :: OverlayResult labels -> [(FaceId, labels)]
+overlayCells = V.toList . V.imap (\index labels -> (FaceId (fromIntegral index), labels)) . overlayResultLabels
+
+overlayArrangementVertices :: OverlayResult labels -> [(VertexId, OverlayVertex)]
+overlayArrangementVertices = V.toList . V.imap (\index vertex -> (VertexId (fromIntegral index), vertex)) . overlayResultVertices
+
+overlayArrangementEdges :: OverlayResult labels -> [(UndirectedEdgeId, OverlayEdgeOrigin)]
+overlayArrangementEdges = V.toList . V.imap (\index origin -> (UndirectedEdgeId (fromIntegral index), origin)) . overlayResultEdges
+
+-- | Polygon publication is a requested derived view, not eager cell storage.
+overlayPlanarLayer
+  :: Ord labels
+  => OverlayResult labels -> Either RegionPublicationError (PlanarLayer labels)
+overlayPlanarLayer result = do
+  outside <- outsideLabels result
+  components <- traverse
+    (\labels -> fmap (fmap (labels,) . planarRegionComponents)
+      (overlaySelectedRegion (== labels) result))
+    (Set.toAscList (Set.delete outside (Set.fromList (V.toList (overlayResultLabels result)))))
+  pure (planarLayerFromAdmittedComponents outside (concat components))
+
+overlaySelectedRegion
+  :: (labels -> Bool) -> OverlayResult labels
+  -> Either RegionPublicationError PlanarRegion
+overlaySelectedRegion selected result = do
+  outside <- outsideLabels result
+  if selected outside
+    then Left RegionUnboundedSelection
+    else planarRegionFromSelectedIncidence
+      (overlayResultIncidence result)
+      (exactPointAt RegionCoordinateMissing result)
+      (\face -> IntSet.member (faceIdIndex face) selectedFaces)
+ where
+  selectedFaces = selectedFaceIndices selected result
+
+overlayMass
+  :: (labels -> Bool) -> OverlayResult labels
+  -> Either RegionPublicationError ExactArea
+overlayMass selected result = do
+  outside <- outsideLabels result
+  if selected outside
+    then Left RegionUnboundedSelection
+    else foldOverlayBoundariesWith (exactPointAt RegionCoordinateMissing result)
+      selected
+      (\area _ boundary -> area <> orientedBoundaryArea boundary)
+      mempty result
+
+overlayConfusion
+  :: Ord labels
+  => OverlayResult labels -> Either OverlayCellWitness (Map.Map labels ExactArea)
+overlayConfusion result = do
+  outside <- faceLabels result (FaceId 0)
+  foldOverlayBoundariesWith (exactPointAt OverlayVertexMissing result) (/= outside)
+    (\masses labels boundary -> Map.insertWith (<>) labels (orientedBoundaryArea boundary) masses)
+    Map.empty result
+
+-- | Direct exact oriented-boundary observation. Every CCB participates; bridge
+-- darts cancel algebraically. No polygon or resident triangulation is created.
+foldBoundedOverlayCells
+  :: (accumulator -> labels -> [(ExactPoint, ExactPoint)] -> accumulator)
+  -> accumulator -> OverlayResult labels
+  -> Either OverlayCellWitness accumulator
+foldBoundedOverlayCells step initial result =
+  foldOverlayBoundariesWith (exactPointAt OverlayVertexMissing result) (const True) step initial result
+{-# INLINE foldBoundedOverlayCells #-}
+
+foldOverlayBoundariesWith
+  :: forall obstruction accumulator labels. (VertexId -> Either obstruction ExactPoint)
+  -> (labels -> Bool)
+  -> (accumulator -> labels -> [(ExactPoint, ExactPoint)] -> accumulator)
+  -> accumulator -> OverlayResult labels -> Either obstruction accumulator
+foldOverlayBoundariesWith coordinate selected step initial result =
+  V.ifoldM descend initial (overlayResultLabels result)
+ where
+  incidence = overlayResultIncidence result
+  descend :: accumulator -> Int -> labels -> Either obstruction accumulator
+  descend accumulator index labels
+    | index == 0 || not (selected labels) = Right accumulator
+    | otherwise = do
+        boundary <- traverse
+          (\edge -> (,) <$> coordinate (incidenceOrigin incidence edge)
+            <*> coordinate (incidenceDestination incidence edge))
+          (concat (faceBoundaryComponents incidence (FaceId (fromIntegral index))))
+        pure $! step accumulator labels boundary
+{-# INLINE foldOverlayBoundariesWith #-}
+
+outsideLabels :: OverlayResult labels -> Either RegionPublicationError labels
+outsideLabels result = maybe (Left (RegionFaceLabelMissing (FaceId 0))) Right
+  (overlayResultLabels result V.!? 0)
+
+faceLabels :: OverlayResult labels -> FaceId -> Either OverlayCellWitness labels
+faceLabels result face = maybe (Left (OverlayFaceMissing face)) Right
+  (overlayResultLabels result V.!? faceIdIndex face)
+
+exactPointAt
+  :: (VertexId -> obstruction) -> OverlayResult labels -> VertexId
+  -> Either obstruction ExactPoint
+exactPointAt obstruction result vertex = maybe (Left (obstruction vertex)) (Right . overlayExactPoint)
+  (overlayResultVertices result V.!? vertexIdIndex vertex)
+
+selectedFaceIndices :: (labels -> Bool) -> OverlayResult labels -> IntSet.IntSet
+selectedFaceIndices selected = IntSet.fromDistinctAscList
+  . V.ifoldr (\index labels rest -> if index /= 0 && selected labels then index : rest else rest) []
+  . overlayResultLabels
+
+layerCovers :: Eq label => PlanarLayer label -> PolygonComponent -> Either LayerCoverageError ()
+layerCovers layer window = do
+  let outside = planarLayerOutsideLabel layer
+      windowLayer = planarLayerFromAdmittedComponents False [(True, window)]
+      isGap (inside, label) = inside && label == outside
+  result <- first LayerCoverageOverlayFailed (overlayLayers windowLayer layer)
+  if IntSet.null (selectedFaceIndices isGap result)
+    then Right ()
+    else do
+      gap <- first LayerCoverageGapPublicationFailed (overlaySelectedRegion isGap result)
+      Left (LayerCoverageGap (CoverGap gap))
+
+overlayClosedUnion
+  :: (left -> Bool) -> (right -> Bool) -> OverlayResult (left, right)
+  -> Either OverlaySelectionError ExactCellSet
+overlayClosedUnion selectLeft selectRight = selectClosedCells ClosedUnionSelection
+  (\support -> any (selectLeft . fst) support || any (selectRight . snd) support)
+  (\(left, right) -> selectLeft left || selectRight right)
+
+-- | Intersect marginal closures, not merely full-dimensional label pairs:
+-- edge and vertex contacts survive even when no incident face satisfies both.
+overlayClosedIntersection
+  :: (left -> Bool) -> (right -> Bool) -> OverlayResult (left, right)
+  -> Either OverlaySelectionError ExactCellSet
+overlayClosedIntersection selectLeft selectRight = selectClosedCells ClosedIntersectionSelection
+  (\support -> any (selectLeft . fst) support && any (selectRight . snd) support)
+  (\(left, right) -> selectLeft left && selectRight right)
+
+overlayRegularizedDifference
+  :: (left -> Bool) -> (right -> Bool) -> OverlayResult (left, right)
+  -> Either OverlaySelectionError ExactCellSet
+overlayRegularizedDifference selectLeft selectRight result = do
+  let selected (left, right) = selectLeft left && not (selectRight right)
+  outside <- first OverlaySelectionProvenance (faceLabels result (FaceId 0))
+  if selected outside
+    then Left (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
+    else closeSelectedCells [] [] selected result
+
+selectClosedCells
+  :: OverlaySelectionKind -> (NonEmpty labels -> Bool) -> (labels -> Bool)
+  -> OverlayResult labels -> Either OverlaySelectionError ExactCellSet
+selectClosedCells selectionKind selectSupport selectFace result = do
+  outside <- first OverlaySelectionProvenance (faceLabels result (FaceId 0))
+  if selectFace outside
+    then Left (OverlaySelectionContainsUnboundedCell selectionKind)
+    else do
+      selectedVertices <- filterM (fmap selectSupport . first OverlaySelectionProvenance . vertexSupport)
+        (fmap fst (overlayArrangementVertices result))
+      selectedEdges <- filterM (fmap selectSupport . first OverlaySelectionProvenance . edgeSupport)
+        (fmap fst (overlayArrangementEdges result))
+      closeSelectedCells selectedVertices selectedEdges selectFace result
+ where
+  incidence = overlayResultIncidence result
+  isolatedFaces = IntMap.fromList
+    [ (vertexIdIndex vertex, face)
+    | index <- [0 .. incidenceFaceCount incidence - 1]
+    , let face = FaceId (fromIntegral index)
+    , vertex <- faceIsolatedVertices incidence face
+    ]
+  vertexSupport vertex = case NonEmpty.nonEmpty (incidenceVertexOutgoingEdges incidence vertex) of
+    Just outgoing -> traverse (faceLabels result . incidenceIncidentFace incidence) outgoing
+    Nothing -> do
+      face <- maybe (Left (OverlayVertexMissing vertex)) Right (IntMap.lookup (vertexIdIndex vertex) isolatedFaces)
+      (:| []) <$> faceLabels result face
+  edgeSupport edge =
+    let forward = normalizedDirected edge
+     in traverse (faceLabels result . incidenceIncidentFace incidence) (forward :| [reverseEdge forward])
+
+closeSelectedCells
+  :: [VertexId] -> [UndirectedEdgeId] -> (labels -> Bool) -> OverlayResult labels
+  -> Either OverlaySelectionError ExactCellSet
+closeSelectedCells selectedVertices selectedEdges selectFace result =
+  first OverlaySelectionInvalid
+    (closeExactCellSetWith
+      (exactPointAt (\vertex -> CellVertexOutOfRange vertex (V.length (overlayResultVertices result))) result)
+      (overlayResultIncidence result) selectedVertices selectedEdges
+      (fmap (FaceId . fromIntegral) (IntSet.toAscList (selectedFaceIndices selectFace result))))
diff --git a/src-public/Moonlight/Planar/PowerDiagram.hs b/src-public/Moonlight/Planar/PowerDiagram.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/PowerDiagram.hs
@@ -0,0 +1,91 @@
+-- | Exact regular topology and power cells. The complete labelled weighted-site
+-- section is authoritative; visibility, incidence, and bounded planar layers
+-- are derived views.
+module Moonlight.Planar.PowerDiagram
+  ( PowerWeight
+  , PowerWeightError (..)
+  , powerWeight
+  , powerWeightFromExact
+  , powerWeightExact
+  , PowerSite
+  , powerSite
+  , powerSiteLabel
+  , powerSitePosition
+  , powerSiteExactPosition
+  , powerSiteWeight
+  , PowerCellDisposition (..)
+  , RegularSiteDisposition (..)
+  , RegularFace
+  , regularFaceLabels
+  , regularFaceDualPoint
+  , PowerDualEdge (..)
+  , RegularEdge
+  , regularEdgeLabels
+  , regularEdgeDual
+  , RegularTriangulation
+  , emptyRegularTriangulation
+  , regularTriangulation
+  , regularSite
+  , regularSites
+  , regularSiteCount
+  , regularSiteDisposition
+  , regularFaces
+  , regularEdges
+  , regularNeighbours
+  , regularTriangulationReceipt
+  , RegularTriangulationReceipt (..)
+  , RegularSiteTransition (..)
+  , RegularEditError (..)
+  , RegularEditResult (..)
+  , insertRegularSite
+  , removeRegularSite
+  , reweightRegularSites
+  , BoundedPowerDiagram
+  , boundedPowerDiagram
+  , boundedPowerDiagramFromRegular
+  , powerCellDisposition
+  , powerCellDispositions
+  , powerDiagramPlanarLayer
+  , PowerDiagramError (..)
+  , RegularTopologyError (..)
+  , PowerDiagramReceipt (..)
+  , powerDiagramInputSites
+  , powerDiagramPeakIntermediateBitGrowth
+  , powerDiagramFinalCoordinateBitGrowth
+  , AffineForm (..)
+  , UpperEnvelopeError (..)
+  , upperEnvelope
+  , PreparedPowerMassSection
+  , PowerMassPolynomial (..)
+  , PowerMassBoundary (..)
+  , PowerMassObligation (..)
+  , PowerMassCertificate
+  , powerMassCertificateObligation
+  , powerMassCertificateConstant
+  , powerMassCertificateRate
+  , PowerMassBound
+  , powerMassBoundParameter
+  , powerMassBoundCertificates
+  , PowerMassInterval
+  , powerMassIntervalLower
+  , powerMassIntervalUpper
+  , PowerMassError (..)
+  , preparePowerMassSection
+  , powerMassSectionWindow
+  , powerMassSectionSites
+  , powerMassSectionDirection
+  , powerMassSectionInterval
+  , powerMassSectionCertificates
+  , powerMassSectionPolynomials
+  , evaluatePowerMasses
+  ) where
+
+import Moonlight.Planar.Internal.PowerDiagram.Edit
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( PowerDualEdge (..)
+  , RegularTopologyError (..)
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Model
+import Moonlight.Planar.Internal.PowerDiagram.Mass
+import Moonlight.Planar.Internal.PowerDiagram.Projection
+import Moonlight.Planar.Internal.PowerDiagram.Section
diff --git a/src-public/Moonlight/Planar/RegularAlpha.hs b/src-public/Moonlight/Planar/RegularAlpha.hs
new file mode 100644
--- /dev/null
+++ b/src-public/Moonlight/Planar/RegularAlpha.hs
@@ -0,0 +1,393 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Exact weighted alpha filtration of a full-dimensional regular
+-- subdivision. Births are signed power radii; Homology lowering remains in
+-- @moonlight-planar:cell-complex@.
+module Moonlight.Planar.RegularAlpha
+  ( PowerAlphaBirth
+  , powerAlphaBirthExact
+  , powerAlphaBirthNumerator
+  , powerAlphaBirthDenominator
+  , RegularAlphaFiltration
+  , RegularAlphaError (..)
+  , regularAlphaFiltration
+  , regularAlphaComplex
+  , regularAlphaBirths
+  , regularAlphaSimplexBirth
+  , regularAlphaCriticalBirths
+  , regularAlphaComplexAtBirth
+  )
+where
+
+import Control.DeepSeq (NFData)
+import Data.Bifunctor (first)
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Map.Strict (Map)
+import Data.Map.Strict qualified as Map
+import Data.Set (Set)
+import Data.Set qualified as Set
+import GHC.Generics (Generic)
+import Moonlight.Planar.Exact
+  ( ExactAffineLine
+  , ExactArithmeticError
+  , ExactPoint
+  , ExactRational
+  , ExactVector (..)
+  , exactAffineLineCoefficients
+  , exactDivide
+  , exactPoint
+  , exactPointCoordinates
+  , exactRayDirection
+  , exactRayOrigin
+  , exactRationalDenominator
+  , exactRationalNumerator
+  , exactSegmentEndpoints
+  )
+import Moonlight.Planar.PowerDiagram
+  ( PowerDualEdge (..)
+  , PowerSite
+  , RegularEdge
+  , RegularFace
+  , RegularSiteDisposition (..)
+  , RegularTriangulation
+  , powerSiteExactPosition
+  , powerSiteLabel
+  , powerSiteWeight
+  , powerWeightExact
+  , regularEdgeDual
+  , regularEdgeLabels
+  , regularEdges
+  , regularFaceDualPoint
+  , regularFaceLabels
+  , regularFaces
+  , regularNeighbours
+  , regularSite
+  , regularSiteDisposition
+  , regularSites
+  )
+import Moonlight.Planar.Simplex
+  ( PlanarComplex
+  , PlanarComplexError
+  , PlanarSimplex
+  , PlanarSimplexError
+  , planarComplex
+  , planarEdge
+  , planarFace
+  , planarSimplexVertices
+  , planarVertex
+  )
+
+-- | Signed exact power radius. Unlike ordinary alpha birth, negative values
+-- are lawful when a positive site weight already contains the simplex at
+-- negative power level.
+newtype PowerAlphaBirth = PowerAlphaBirth ExactRational
+  deriving stock (Eq, Ord, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Recover the reduced exact power radius.
+powerAlphaBirthExact :: PowerAlphaBirth -> ExactRational
+powerAlphaBirthExact (PowerAlphaBirth value) = value
+
+-- | Numerator of the reduced exact power radius.
+powerAlphaBirthNumerator :: PowerAlphaBirth -> Integer
+powerAlphaBirthNumerator = exactRationalNumerator . powerAlphaBirthExact
+
+-- | Positive denominator of the reduced exact power radius.
+powerAlphaBirthDenominator :: PowerAlphaBirth -> Integer
+powerAlphaBirthDenominator = exactRationalDenominator . powerAlphaBirthExact
+
+-- | One admitted labelled regular complex and its total exact birth section.
+data RegularAlphaFiltration label = RegularAlphaFiltration
+  { regularAlphaComplex :: !(PlanarComplex label)
+    -- ^ Canonical full-dimensional regular complex.
+  , regularAlphaBirths :: !(Map (PlanarSimplex label) PowerAlphaBirth)
+    -- ^ Total exact birth section over that complex.
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Typed topology, dual-consistency, projection, and sublevel obstructions.
+data RegularAlphaError label
+  = RegularAlphaSiteMissing !label
+  | RegularAlphaSimplexInvalid !(PlanarSimplexError label)
+  | RegularAlphaComplexInvalid !(PlanarComplexError label)
+  | RegularAlphaDualMismatch
+      !(PlanarSimplex label)
+      !ExactRational
+      !ExactRational
+  | RegularAlphaProjectionFailed
+      !(PlanarSimplex label)
+      !ExactArithmeticError
+  | RegularAlphaVertexBirthMissing !label
+  | RegularAlphaSublevelInvalid !(PlanarComplexError label)
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+-- | Derive one exact weighted-alpha birth section from the resident regular dual.
+regularAlphaFiltration
+  :: Ord label
+  => RegularTriangulation label
+  -> Either (RegularAlphaError label) (RegularAlphaFiltration label)
+regularAlphaFiltration triangulation = do
+  let visibleLabels =
+        Set.fromAscList
+          [ powerSiteLabel site
+          | site <- regularSites triangulation
+          , regularSiteDisposition (powerSiteLabel site) triangulation
+              == Just RegularSiteVisible
+          ]
+      visibleEdges = filter (edgeIsVisible visibleLabels) (regularEdges triangulation)
+      visibleFaces = filter (faceIsVisible visibleLabels) (regularFaces triangulation)
+  edgeSimplices <- traverse regularEdgeSimplex visibleEdges
+  faceSimplices <- traverse regularFaceSimplex visibleFaces
+  complexValue <-
+    first RegularAlphaComplexInvalid
+      ( planarComplex
+          ( Set.unions
+              [ Set.map planarVertex visibleLabels
+              , Set.fromList edgeSimplices
+              , Set.fromList faceSimplices
+              ]
+          )
+      )
+  faceBirthSection <-
+    Map.fromList <$> traverse (regularFaceBirth triangulation) visibleFaces
+  edgeBirthEntries <- traverse (regularEdgeBirth triangulation) visibleEdges
+  let edgeBirthSection = Map.fromList edgeBirthEntries
+      incidentEdgeBirths =
+        Map.fromListWith min (edgeBirthEntries >>= incidentEdgeBirthsFor)
+  vertexBirthSection <-
+    Map.fromList
+      <$> traverse
+        (regularVertexBirth triangulation incidentEdgeBirths)
+        (Set.toAscList visibleLabels)
+  pure
+    RegularAlphaFiltration
+      { regularAlphaComplex = complexValue
+      , regularAlphaBirths =
+          vertexBirthSection <> edgeBirthSection <> faceBirthSection
+      }
+
+incidentEdgeBirthsFor
+  :: (PlanarSimplex label, PowerAlphaBirth)
+  -> [(label, PowerAlphaBirth)]
+incidentEdgeBirthsFor (simplex, birthValue) =
+  case NonEmpty.toList (planarSimplexVertices simplex) of
+    [firstLabel, secondLabel] ->
+      [ (firstLabel, birthValue)
+      , (secondLabel, birthValue)
+        ]
+    _ -> []
+
+-- | Look up the exact birth of one admitted simplex.
+regularAlphaSimplexBirth
+  :: Ord label
+  => PlanarSimplex label
+  -> RegularAlphaFiltration label
+  -> Maybe PowerAlphaBirth
+regularAlphaSimplexBirth simplex = Map.lookup simplex . regularAlphaBirths
+
+-- | Distinct births in ascending exact order.
+regularAlphaCriticalBirths
+  :: RegularAlphaFiltration label
+  -> [PowerAlphaBirth]
+regularAlphaCriticalBirths =
+  Set.toAscList . Set.fromList . Map.elems . regularAlphaBirths
+
+-- | Reconstruct the closed subcomplex born no later than the threshold.
+regularAlphaComplexAtBirth
+  :: Ord label
+  => PowerAlphaBirth
+  -> RegularAlphaFiltration label
+  -> Either (RegularAlphaError label) (PlanarComplex label)
+regularAlphaComplexAtBirth threshold filtration =
+  first RegularAlphaSublevelInvalid
+    ( planarComplex
+        ( Map.keysSet
+            (Map.filter (<= threshold) (regularAlphaBirths filtration))
+        )
+    )
+
+edgeIsVisible :: Ord label => Set label -> RegularEdge label -> Bool
+edgeIsVisible visible edge =
+  let (firstLabel, secondLabel) = regularEdgeLabels edge
+   in Set.member firstLabel visible && Set.member secondLabel visible
+
+faceIsVisible :: Ord label => Set label -> RegularFace label -> Bool
+faceIsVisible visible face =
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+   in all (`Set.member` visible) [firstLabel, secondLabel, thirdLabel]
+
+regularEdgeSimplex
+  :: Ord label
+  => RegularEdge label
+  -> Either (RegularAlphaError label) (PlanarSimplex label)
+regularEdgeSimplex edge =
+  let (firstLabel, secondLabel) = regularEdgeLabels edge
+   in first RegularAlphaSimplexInvalid (planarEdge firstLabel secondLabel)
+
+regularFaceSimplex
+  :: Ord label
+  => RegularFace label
+  -> Either (RegularAlphaError label) (PlanarSimplex label)
+regularFaceSimplex face =
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+   in first RegularAlphaSimplexInvalid
+        (planarFace firstLabel secondLabel thirdLabel)
+
+regularFaceBirth
+  :: Ord label
+  => RegularTriangulation label
+  -> RegularFace label
+  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
+regularFaceBirth triangulation face = do
+  simplex <- regularFaceSimplex face
+  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+      dualPoint = regularFaceDualPoint face
+  firstSite <- requireRegularSite triangulation firstLabel
+  secondSite <- requireRegularSite triangulation secondLabel
+  thirdSite <- requireRegularSite triangulation thirdLabel
+  let firstBirth = powerDistance firstSite dualPoint
+      secondBirth = powerDistance secondSite dualPoint
+      thirdBirth = powerDistance thirdSite dualPoint
+  requireEqualDual simplex firstBirth secondBirth
+  requireEqualDual simplex firstBirth thirdBirth
+  pure (simplex, PowerAlphaBirth firstBirth)
+
+regularEdgeBirth
+  :: Ord label
+  => RegularTriangulation label
+  -> RegularEdge label
+  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
+regularEdgeBirth triangulation edge = do
+  simplex <- regularEdgeSimplex edge
+  let (firstLabel, secondLabel) = regularEdgeLabels edge
+  firstSite <- requireRegularSite triangulation firstLabel
+  secondSite <- requireRegularSite triangulation secondLabel
+  minimizingPoint <- minimizePowerOnDual simplex firstSite (regularEdgeDual edge)
+  let firstBirth = powerDistance firstSite minimizingPoint
+      secondBirth = powerDistance secondSite minimizingPoint
+  requireEqualDual simplex firstBirth secondBirth
+  pure (simplex, PowerAlphaBirth firstBirth)
+
+regularVertexBirth
+  :: Ord label
+  => RegularTriangulation label
+  -> Map label PowerAlphaBirth
+  -> label
+  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
+regularVertexBirth triangulation incidentBirths label = do
+  owner <- requireRegularSite triangulation label
+  competitors <-
+    traverse (requireRegularSite triangulation) (Set.toAscList (regularNeighbours label triangulation))
+  let ownerPoint = powerSiteExactPosition owner
+      ownerBirth = powerDistance owner ownerPoint
+  if all ((ownerBirth <=) . (`powerDistance` ownerPoint)) competitors
+    then Right (planarVertex label, PowerAlphaBirth ownerBirth)
+    else
+      case Map.lookup label incidentBirths of
+        Just birthValue -> Right (planarVertex label, birthValue)
+        Nothing -> Left (RegularAlphaVertexBirthMissing label)
+
+minimizePowerOnDual
+  :: PlanarSimplex label
+  -> PowerSite label
+  -> PowerDualEdge
+  -> Either (RegularAlphaError label) ExactPoint
+minimizePowerOnDual simplex site dual =
+  case dual of
+    BoundedPowerDual segment -> do
+      let (origin, terminal) = exactSegmentEndpoints segment
+      projected <- projectAlong simplex (powerSiteExactPosition site) origin (differenceVector origin terminal)
+      pure (pointAtClampedParameter 0 1 origin (differenceVector origin terminal) projected)
+    UnboundedPowerDual ray -> do
+      let origin = exactRayOrigin ray
+          direction = exactRayDirection ray
+      projected <- projectAlong simplex (powerSiteExactPosition site) origin direction
+      pure (pointAtParameter origin direction (max 0 projected))
+    FullLinePowerDual line -> projectOntoLine simplex (powerSiteExactPosition site) line
+    CollapsedPowerDual point -> Right point
+
+projectAlong
+  :: PlanarSimplex label
+  -> ExactPoint
+  -> ExactPoint
+  -> ExactVector
+  -> Either (RegularAlphaError label) ExactRational
+projectAlong simplex query origin direction =
+  let (queryX, queryY) = exactPointCoordinates query
+      (originX, originY) = exactPointCoordinates origin
+      ExactVector directionX directionY = direction
+      numerator = (queryX - originX) * directionX + (queryY - originY) * directionY
+      denominator = directionX * directionX + directionY * directionY
+   in first (RegularAlphaProjectionFailed simplex)
+        (exactDivide numerator denominator)
+
+pointAtClampedParameter
+  :: ExactRational
+  -> ExactRational
+  -> ExactPoint
+  -> ExactVector
+  -> ExactRational
+  -> ExactPoint
+pointAtClampedParameter lower upper origin direction parameter =
+  pointAtParameter origin direction (max lower (min upper parameter))
+
+pointAtParameter :: ExactPoint -> ExactVector -> ExactRational -> ExactPoint
+pointAtParameter origin (ExactVector directionX directionY) parameter =
+  let (originX, originY) = exactPointCoordinates origin
+   in exactPoint
+        (originX + parameter * directionX)
+        (originY + parameter * directionY)
+
+projectOntoLine
+  :: PlanarSimplex label
+  -> ExactPoint
+  -> ExactAffineLine
+  -> Either (RegularAlphaError label) ExactPoint
+projectOntoLine simplex query line = do
+  let (normalX, normalY, constant) = exactAffineLineCoefficients line
+      (queryX, queryY) = exactPointCoordinates query
+      lineValue = normalX * queryX + normalY * queryY + constant
+      squaredNormal = normalX * normalX + normalY * normalY
+  displacement <-
+    first (RegularAlphaProjectionFailed simplex)
+      (exactDivide lineValue squaredNormal)
+  pure
+    ( exactPoint
+        (queryX - displacement * normalX)
+        (queryY - displacement * normalY)
+    )
+
+powerDistance :: PowerSite label -> ExactPoint -> ExactRational
+powerDistance site point =
+  let (pointX, pointY) = exactPointCoordinates point
+      (siteX, siteY) = exactPointCoordinates (powerSiteExactPosition site)
+      deltaX = pointX - siteX
+      deltaY = pointY - siteY
+   in deltaX * deltaX + deltaY * deltaY - powerWeightExact (powerSiteWeight site)
+
+differenceVector :: ExactPoint -> ExactPoint -> ExactVector
+differenceVector from to =
+  let (fromX, fromY) = exactPointCoordinates from
+      (toX, toY) = exactPointCoordinates to
+   in ExactVector (toX - fromX) (toY - fromY)
+
+requireRegularSite
+  :: Ord label
+  => RegularTriangulation label
+  -> label
+  -> Either (RegularAlphaError label) (PowerSite label)
+requireRegularSite triangulation label =
+  maybe (Left (RegularAlphaSiteMissing label)) Right (regularSite label triangulation)
+
+requireEqualDual
+  :: PlanarSimplex label
+  -> ExactRational
+  -> ExactRational
+  -> Either (RegularAlphaError label) ()
+requireEqualDual simplex expected actual
+  | expected == actual = Right ()
+  | otherwise = Left (RegularAlphaDualMismatch simplex expected actual)
diff --git a/src-public/Moonlight/Triangulation.hs b/src-public/Moonlight/Triangulation.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation.hs
+++ /dev/null
@@ -1,1239 +0,0 @@
--- | The public surface for Delaunay triangulations as values of finite
--- coordinate supports. Its exports are grouped by object, generation,
--- annotations, finite-set algebra, constraints, refinement, observations and
--- validation.
---
--- Geometry-only set laws are observed after erasing vertex annotations with
--- @mapVertices (const ())@ and applying 'canonicalize'. Successful construction
--- may retain schedule-dependent resident numbering, so structural 'Eq' compares
--- stored representations rather than silently normalizing them. Annotations may
--- change independently through 'mapVertices' and 'setVertexData'; they never
--- determine geometry.
---
--- The algebra is deliberately partial. Finite arena exhaustion is returned as
--- 'BuildError', while unrealizable constrained compositions return
--- 'ConstrainedUnionError'. These typed obstructions are why the surface exposes
--- explicit operations rather than total 'Semigroup' instances. Refinement then
--- composes over any admitted result without introducing a second mesh type.
-module Moonlight.Triangulation
-  ( -- * The object — a triangulation is a value of its site set
-    Triangulation
-  , DelaunayTriangulation
-  , ConstrainedDelaunayTriangulation
-  , ConstraintMode (..)
-  , Point (..)
-  , QueryPoint
-  , queryPointValue
-  , PointValidationError (..)
-  , mkQueryPoint
-  , HasPosition (..)
-  , ElementDefaults (..)
-  , unitElementDefaults
-  , JoinSemilattice (..)
-  , Coordinate2 (..)
-  , mapCoordinate2
-    -- | Fixed identifiers are compact resident indices. Their observations
-    -- deliberately avoid a second bounds check and therefore require the
-    -- issuing mesh; use the scoped section below or owning handles whenever
-    -- mesh provenance is not already carried by the surrounding type.
-  , VertexId
-  , unVertexId
-  , FaceId
-  , unFaceId
-  , DirectedEdgeId
-  , unDirectedEdgeId
-  , UndirectedEdgeId
-  , unUndirectedEdgeId
-  , reverseEdge
-  , asUndirected
-  , normalizedDirected
-  , reversedDirected
-  , directedPair
-  , isNormalized
-
-    -- * Mesh-scoped observations — zero-cost nominal ownership; identifiers
-    -- from distinct scopes cannot be mixed
-  , ScopedTriangulation
-  , ScopedVertex
-  , ScopedDirectedEdge
-  , ScopedUndirectedEdge
-  , ScopedFace
-  , withScopedTriangulation
-  , scopedVertices
-  , scopedDirectedEdges
-  , scopedUndirectedEdges
-  , scopedFaces
-  , scopedInnerFaces
-  , scopedOuterFace
-  , scopedVertexPoint
-  , scopedVertexData
-  , scopedDirectedEdgeData
-  , scopedUndirectedEdgeData
-  , scopedFaceData
-  , scopedVertexOutEdge
-  , scopedAdjacentEdge
-  , scopedOrigin
-  , scopedDestination
-  , scopedReverseEdge
-  , scopedAsUndirected
-  , scopedNext
-  , scopedPrevious
-  , scopedIncidentFace
-  , scopedIsOuterDirectedEdge
-  , scopedIsBoundaryEdge
-  , scopedIsConstraintEdge
-  , scopedUndirectedEndpoints
-  , scopedFaceDirectedEdges
-  , scopedFaceVertices
-  , scopedVertexOutgoingEdges
-
-    -- * Generation and editing — persistent verbs publish one new value;
-    -- sessions compose many edits behind one sealed thaw/freeze boundary
-  , empty
-  , clear
-  , delaunay
-  , delaunayGeometry
-  , DuplicatePayloadPolicy (..)
-  , delaunayFromCoordinates
-  , BuildResult
-  , buildTriangulation
-  , buildInputVertices
-  , buildStats
-  , BuildStats (..)
-  , emptyBuildStats
-  , InsertionDisposition (..)
-  , InsertionResult
-  , insertionTriangulation
-  , insertionVertex
-  , insertionDisposition
-  , insertionStats
-  , insert
-  , insertAt
-  , insertMany
-  , RemovalOutcome (..)
-  , RemovalResult (..)
-  , removeVertex
-  , locateAndRemove
-  , Session
-  , withSession
-  , insertVertex
-  , insertVertexAt
-  , insertVertexAtNear
-  , insertVertexAtNearVertex
-  , removeAt
-  , removeAtNear
-  , removeManyAt
-  , removeManyAtNear
-  , refuse
-  , BuildError (..)
-  , PublicationStats (..)
-  , ValidationClosureStats (..)
-  , CoordinateError (..)
-  , NonFiniteValue (..)
-
-    -- * The annotation functor — payloads annotate geometry, never author it;
-    -- the mesh is the same mesh before and after
-  , mapVertices
-  , mapDirectedEdges
-  , mapUndirectedEdges
-  , mapFaces
-  , imapUndirectedEdges
-  , imapFaces
-  , PayloadTraversal
-  , vertexPayloads
-  , directedPayloads
-  , undirectedPayloads
-  , facePayloads
-  , overPayloads
-  , foldPayloads
-  , payloadList
-  , geometryOnlyPublication
-  , vertexData
-  , directedEdgeData
-  , undirectedEdgeData
-  , faceData
-  , setVertexData
-  , setDirectedEdgeData
-  , setUndirectedEdgeData
-  , setFaceData
-
-    -- * Finite-set algebra and its normal form — support order is observable,
-    -- intersection can combine heterogeneous annotations, and each
-    -- construction returns its obstruction rather than an instance that lies
-  , canonicalize
-  , SiteRelation (..)
-  , siteRelation
-  , union
-  , unions
-  , intersection
-  , intersectionWith
-  , difference
-  , symmetricDifference
-
-    -- * The constraint layer — 'constrainedDelaunay' is the partial map from
-    -- (sites, segments), defined exactly on realizable segment sets and
-    -- naming the corridor that blocked it where it is not
-  , constrainedDelaunay
-  , constrainedDelaunayMaximal
-  , fromDelaunay
-  , constraintEdges
-  , isConstraintEdge
-  , boundedRegionFaces
-  , CdtError (..)
-  , CorridorObstruction (..)
-  , CanonicalSegment
-  , segmentStart
-  , segmentEnd
-  , ConstraintConflict
-  , conflictFirstSegment
-  , conflictSecondSegment
-  , ConstrainedUnionError (..)
-  , ConstraintBatchResult
-  , constraintBatchTriangulation
-  , constraintBatchOutcomes
-  , constraintBatchStats
-  , ConstrainedExtensionResult
-  , constrainedExtensionConstraintBatch
-  , constrainedExtensionBuildStats
-  , ConstraintRecoveryResult (..)
-  , ConstraintResult
-  , ConstraintOutcome (..)
-  , ConstraintBatchStats (..)
-  , ConstraintSplitBatchResult
-  , CdtBuildResult (..)
-  , ConstrainedSeamSide (..)
-  , ConstrainedSeamFaceEvidence
-  , constrainedSeamSourceFace
-  , constrainedSeamTargetFace
-  , constrainedSeamFaceFirstPoint
-  , constrainedSeamFaceSecondPoint
-  , constrainedSeamFaceThirdPoint
-  , ConstrainedSeamResult
-  , constrainedSeamResultTriangulation
-  , constrainedSeamLeftFaceCount
-  , constrainedSeamRightFaceEvidence
-  , constrainedSeamJoinFaces
-  , constrainedSeamLeftConstraintCount
-  , constrainedSeamConstraintStats
-  , constrainedSeamBuildStats
-  , constrainedSeamPublicationStats
-  , constrainedSeamCachedFrontierPointReads
-  , constraintSegments
-  , unionConstrainedWith
-  , unionConstrained
-  , joinSeparatedConstrained
-  , extendConstrainedWith
-  , constraintStorageBytes
-  , existsConstraint
-  , canAddConstraint
-  , intersectsConstraint
-  , getConflictingEdgesBetweenPoints
-  , getConflictingEdgesBetweenVertices
-  , recoverConstraints
-  , addConstraintEdge
-  , addConstraintEdges
-  , addConstraintAndSplit
-  , addConstraintsAndSplit
-  , removeConstraintEdge
-  , outerRegionFaces
-
-    -- * Refinement — budget-bounded, composed after any operation above rather
-    -- than configured into it; 'refinementComplete' reports sufficiency, not
-    -- effort
-  , refine
-  , refineWithinDomain
-  , validateRefinementParameters
-  , RefinementParameters (..)
-  , defaultRefinementParameters
-  , RefinementReceipt
-  , RefinementDomainResult
-  , RefinementResult
-  , refinementDomainResult
-  , refinementDomainReceipt
-  , refinedTriangulation
-  , refinementStats
-  , refinementVisitedJoinFaces
-  , refinementVisitedProtectedFaces
-  , refinementCreatedFaces
-  , refinementFinalPermittedFaces
-  , refinementFinalInterfaceIncidence
-  , refinementTouchedEdges
-  , refinementRemovedEdges
-  , refinementInterfaceBoundaryReads
-  , refinementAttemptedBoundaryCrossings
-  , refinementPublicationStats
-  , refinementValidationClosureStats
-  , refinementAddedVertices
-  , refinementExcludedFaces
-  , refinementComplete
-  , withMinimumAngle
-  , radiusEdgeRatioForAngle
-
-    -- * Observations — pure functions of the value: incidence, location,
-    -- interpolation and barrier parity, none of which build a second mesh
-  , numVertices
-  , vertices
-  , foldVertices'
-  , vertexPoint
-  , vertexPoints
-  , numFaces
-  , numInnerFaces
-  , allFaces
-  , innerFaces
-  , foldAllFaces'
-  , foldInnerFaces'
-  , outerFace
-  , adjacentEdge
-  , faceDirectedEdges
-  , faceVertices
-  , innerFaceDirectedEdges
-  , innerFaceVertices
-  , innerFaceDirectedEdgeTriples
-  , innerFaceVertexTriples
-  , foldFaceDirectedEdges'
-  , vertexOutEdge
-  , vertexOutgoingEdges
-  , foldVertexOutgoingEdges'
-  , numDirectedEdges
-  , directedEdges
-  , foldDirectedEdges'
-  , numUndirectedEdges
-  , undirectedEdges
-  , foldUndirectedEdges'
-  , hullEdges
-  , hullVertices
-  , undirectedEndpoints
-  , origin
-  , destination
-  , next
-  , previous
-  , clockwise
-  , counterClockwise
-  , incidentFace
-  , isOuterDirectedEdge
-  , isBoundaryEdge
-  , numConstraints
-  , locatePoint
-  , locatePointWithHint
-  , Location (..)
-  , nearestNeighbor
-  , NearestStats (..)
-  , interpolateNearest
-  , interpolateBarycentric
-  , LocationHint (..)
-  , LocationStats (..)
-  , emptyLocationStats
-  , Intersection (..)
-  , lineIntersections
-  , lineIntersectionsBetweenVertices
-  , foldCorridorBetweenPoints
-  , foldCorridorBetweenVertices
-  , conflictingEdges
-  , segmentIntersectsNonCollinear
-  , DistanceMetric (..)
-  , CircleMetric
-  , CircleMetricError (..)
-  , RectangleMetric
-  , RectangleMetricError (..)
-  , circleMetric
-  , rectangleMetric
-  , edgesInShape
-  , verticesInShape
-  , edgesInCircle
-  , verticesInCircle
-  , edgesInRectangle
-  , verticesInRectangle
-  , floodFillFaces
-  , outerFaceFloodFill
-  , facesAtEvenBarrierDepth
-
-    -- ** Robust planar predicates and constructions
-  , orient2d
-  , sideQuery
-  , inCircle
-  , segmentRelation
-  , segmentsProperlyCross
-  , segmentsIntersect
-  , squaredDistance
-  , distance
-  , midpoint
-  , centroid
-  , triangleArea
-  , circumcenter
-  , barycentricCoordinates
-
-    -- ** Face regions and alpha filtration
-  , FaceComponent
-  , faceComponentFaces
-  , BoundaryOrientation (..)
-  , BoundaryLoop
-  , boundaryLoopOrientation
-  , boundaryLoopResidentVertices
-  , boundaryLoopVertices
-  , RegionBoundary
-  , regionBoundaryOuterLoop
-  , regionBoundaryHoleLoops
-  , BoundaryObstruction (..)
-  , faceComponents
-  , faceComponentsBy
-  , labelledRegionBoundaries
-  , componentBoundaryLoops
-  , componentBoundary
-  , componentBoundaryForFaces
-  , RadiusSquared
-  , RadiusSquaredError (..)
-  , mkRadiusSquared
-  , radiusSquaredValue
-  , AlphaBirth
-  , alphaBirthNumerator
-  , alphaBirthDenominator
-  , alphaBirthToDouble
-  , alphaBirthFromRadiusSquared
-  , AlphaFiltration
-  , AlphaFiltrationError (..)
-  , alphaFiltration
-  , alphaFiltrationCellSet
-  , alphaVertexBirth
-  , alphaEdgeBirth
-  , alphaFaceBirth
-  , alphaFiltrationCriticalBirths
-  , alphaComplexAtBirth
-  , alphaComplexAtRadius
-  , alphaShapeContainsFace
-
-    -- * Exact planar regions — authoritative rational geometry, labelled
-    -- overlay, closed cell selections, valuations, and polygonal morphology
-  , ExactRational
-  , ExactArithmeticError (..)
-  , exactRational
-  , exactRationalNumerator
-  , exactRationalDenominator
-  , ExactPoint
-  , exactPoint
-  , exactPointCoordinates
-  , exactPointCross
-  , ExactVector (..)
-  , exactVectorFromPoints
-  , addExactVectors
-  , exactCross
-  , compareExactVectorAngle
-  , translateExactPoint
-  , ExactRay
-  , exactRay
-  , exactRayOrigin
-  , exactRayDirection
-  , ExactSegment
-  , ExactGeometryError (..)
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactPointFromPoint
-  , exactPointFromQueryPoint
-  , exactPointToEmbeddingCandidate
-  , exactOrient2d
-  , exactOnClosedSegment
-  , SegmentRelation (..)
-  , allSegmentRelations
-  , exactSegmentRelation
-  , ExactIntersectionError (..)
-  , exactLineIntersection
-  , exactSupportingLineIntersection
-  , ExactLoop
-  , exactLoop
-  , exactLoopPoints
-  , PolygonComponent
-  , polygonComponent
-  , polygonOuterLoop
-  , polygonHoleLoops
-  , PlanarRegion
-  , planarRegion
-  , planarRegionComponents
-  , emptyPlanarRegion
-  , RegionPointLocation (..)
-  , regionPointLocation
-  , PlanarLayer
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarLayer
-  , planarLayerLabelAt
-  , RegionValidationError (..)
-  , RegionPublicationError (..)
-  , labelledPlanarLayer
-  , ExactCellSet
-  , CellSelectionError (..)
-  , exactCellSet
-  , closeFaceCellSet
-  , exactCellSetVertexCount
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , foldExactCellVertices
-  , foldExactCellEdges
-  , foldExactCellFaces
-  , OverlayResult
-  , OverlayReceipt (..)
-  , OverlayError (..)
-  , OverlaySelectionKind (..)
-  , OverlaySelectionError (..)
-  , CoverGap
-  , coverGapRegion
-  , LayerCoverageError (..)
-  , overlayLayers
-  , overlayAll
-  , overlayEmbeddedTriangulation
-  , overlayReceipt
-  , overlayCells
-  , overlayArrangementVertices
-  , overlayArrangementEdges
-  , overlayPlanarLayer
-  , overlaySelectedRegion
-  , overlayMass
-  , overlayConfusion
-  , layerCovers
-  , overlayClosedUnion
-  , overlayClosedIntersection
-  , overlayRegularizedDifference
-  , EulerCharacteristic
-  , eulerCharacteristicValue
-  , ExactArea
-  , ExactPlanarMoments
-  , exactAreaValue
-  , exactPlanarMeasure
-  , exactPlanarFirstX
-  , exactPlanarFirstY
-  , exactPlanarSecondXX
-  , exactPlanarSecondXY
-  , exactPlanarSecondYY
-  , scaleExactPlanarMoments
-  , polygonComponentMoments
-  , polygonComponentArea
-  , ExactLengthTerm
-  , lengthCoefficient
-  , squaredLength
-  , ExactLengthExpression
-  , exactLengthTerms
-  , CertifiedInterval (..)
-  , ExactLengthMeasurement
-  , exactLengthExpression
-  , exactLengthBounds
-  , PlanarValuations
-  , valuationEuler
-  , valuationArea
-  , valuationIntrinsic1
-  , ValuationError (..)
-  , cellValuations
-  , regionValuations
-  , planarValuationsPerimeter
-  , cellSetPerimeter
-  , regionPerimeter
-  , ConvexPolygon
-  , convexPolygon
-  , convexPolygonPoints
-  , StructuringElement
-  , structuringElement
-  , MinkowskiOperation (..)
-  , MinkowskiError (..)
-  , MinkowskiReceipt (..)
-  , convexMinkowskiSum
-  , reflectConvexPolygon
-  , minkowskiSum
-  , erodeBy
-  , openWith
-  , closeWith
-  , polygonOffset
-  , polygonInset
-  , PowerWeight
-  , PowerWeightError (..)
-  , powerWeight
-  , powerWeightFromExact
-  , powerWeightExact
-  , PowerSite
-  , powerSite
-  , powerSiteLabel
-  , powerSitePosition
-  , powerSiteExactPosition
-  , powerSiteWeight
-  , PowerCellDisposition (..)
-  , RegularSiteDisposition (..)
-  , RegularFace
-  , regularFaceLabels
-  , regularFaceDualPoint
-  , PowerDualEdge (..)
-  , RegularEdge
-  , regularEdgeLabels
-  , regularEdgeDual
-  , RegularTriangulation
-  , emptyRegularTriangulation
-  , regularTriangulation
-  , regularSite
-  , regularSites
-  , regularSiteCount
-  , regularSiteDisposition
-  , regularFaces
-  , regularEdges
-  , regularNeighbours
-  , regularTriangulationReceipt
-  , RegularTriangulationReceipt (..)
-  , RegularSiteTransition (..)
-  , RegularEditError (..)
-  , RegularEditResult (..)
-  , insertRegularSite
-  , removeRegularSite
-  , reweightRegularSites
-  , BoundedPowerDiagram
-  , boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , powerCellDisposition
-  , powerCellDispositions
-  , powerDiagramPlanarLayer
-  , PowerDiagramError (..)
-  , RegularTopologyError (..)
-  , PowerDiagramReceipt (..)
-  , powerDiagramInputSites
-  , powerDiagramPeakIntermediateBitGrowth
-  , powerDiagramFinalCoordinateBitGrowth
-  , AffineForm (..)
-  , UpperEnvelopeError (..)
-  , upperEnvelope
-  , PowerAlphaBirth
-  , powerAlphaBirthExact
-  , powerAlphaBirthNumerator
-  , powerAlphaBirthDenominator
-  , RegularAlphaFiltration
-  , RegularAlphaError (..)
-  , regularAlphaFiltration
-  , regularAlphaComplex
-  , regularAlphaBirths
-  , regularAlphaSimplexBirth
-  , regularAlphaCriticalBirths
-  , regularAlphaComplexAtBirth
-
-    -- * Discharge — the invariants the constructors guarantee, checkable on a
-    -- value built by any route; every violation is a value carrying its witness
-  , validateTopology
-  , validateDelaunay
-  , validateTriangulation
-  , triangulationIsValid
-  , faceArea
-  , faceMinimumAngleDegrees
-  , InvariantViolation (..)
-  ) where
-
-import Moonlight.Triangulation.BulkLoad
-  ( DuplicatePayloadPolicy (..)
-  , clear
-  , delaunay
-  , delaunayFromCoordinates
-  , delaunayGeometry
-  , empty
-  , insert
-  , insertAt
-  , insertMany
-  )
-import Moonlight.Triangulation.CellSet
-  ( CellSelectionError (..)
-  , ExactCellSet
-  , closeFaceCellSet
-  , exactCellSet
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , exactCellSetVertexCount
-  , foldExactCellEdges
-  , foldExactCellFaces
-  , foldExactCellVertices
-  )
-import Moonlight.Triangulation.Dcel
-  ( adjacentEdge
-  , clockwise
-  , counterClockwise
-  , destination
-  , directedEdgeData
-  , faceData
-  , faceDirectedEdges
-  , faceVertices
-  , foldFaceDirectedEdges'
-  , foldVertexOutgoingEdges'
-  , imapFaces
-  , imapUndirectedEdges
-  , innerFaceDirectedEdges
-  , innerFaceDirectedEdgeTriples
-  , innerFaceVertices
-  , innerFaceVertexTriples
-  , incidentFace
-  , isBoundaryEdge
-  , isConstraintEdge
-  , isOuterDirectedEdge
-  , next
-  , numConstraints
-  , numDirectedEdges
-  , numFaces
-  , numInnerFaces
-  , numUndirectedEdges
-  , numVertices
-  , origin
-  , outerFace
-  , previous
-  , setDirectedEdgeData
-  , setFaceData
-  , setUndirectedEdgeData
-  , setVertexData
-  , undirectedEdgeData
-  , undirectedEndpoints
-  , vertexData
-  , vertexOutEdge
-  , vertexOutgoingEdges
-  , vertexPoint
-  , vertexPoints
-  )
-import Moonlight.Triangulation.Alpha
-  ( AlphaBirth
-  , AlphaFiltration
-  , AlphaFiltrationError (..)
-  , alphaBirthDenominator
-  , alphaBirthFromRadiusSquared
-  , alphaBirthNumerator
-  , alphaBirthToDouble
-  , alphaComplexAtBirth
-  , alphaComplexAtRadius
-  , alphaEdgeBirth
-  , alphaFaceBirth
-  , alphaFiltration
-  , alphaFiltrationCellSet
-  , alphaFiltrationCriticalBirths
-  , alphaShapeContainsFace
-  , alphaVertexBirth
-  )
-import Moonlight.Triangulation.FloodFillIterator
-  ( BoundaryLoop
-  , BoundaryObstruction (..)
-  , BoundaryOrientation (..)
-  , CircleMetric
-  , CircleMetricError (..)
-  , DistanceMetric (..)
-  , FaceComponent
-  , RectangleMetric
-  , RectangleMetricError (..)
-  , RegionBoundary
-  , boundaryLoopOrientation
-  , boundaryLoopResidentVertices
-  , boundaryLoopVertices
-  , circleMetric
-  , componentBoundary
-  , componentBoundaryLoops
-  , componentBoundaryForFaces
-  , edgesInCircle
-  , edgesInRectangle
-  , edgesInShape
-  , faceComponentFaces
-  , faceComponents
-  , faceComponentsBy
-  , facesAtEvenBarrierDepth
-  , floodFillFaces
-  , labelledRegionBoundaries
-  , outerFaceFloodFill
-  , rectangleMetric
-  , regionBoundaryHoleLoops
-  , regionBoundaryOuterLoop
-  , verticesInCircle
-  , verticesInRectangle
-  , verticesInShape
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError (..)
-  , ExactIntersectionError (..)
-  , ExactPoint
-  , ExactRay
-  , ExactSegment
-  , ExactVector (..)
-  , SegmentRelation (..)
-  , addExactVectors
-  , allSegmentRelations
-  , compareExactVectorAngle
-  , exactCross
-  , exactLineIntersection
-  , exactOnClosedSegment
-  , exactOrient2d
-  , exactPoint
-  , exactPointCoordinates
-  , exactPointCross
-  , exactPointFromPoint
-  , exactPointFromQueryPoint
-  , exactPointToEmbeddingCandidate
-  , exactRay
-  , exactRayDirection
-  , exactRayOrigin
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactSegmentRelation
-  , exactSupportingLineIntersection
-  , exactVectorFromPoints
-  , translateExactPoint
-  )
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( DirectedEdgeId
-  , FaceId
-  , UndirectedEdgeId
-  , VertexId
-  , asUndirected
-  , directedPair
-  , isNormalized
-  , normalizedDirected
-  , reverseEdge
-  , reversedDirected
-  , unDirectedEdgeId
-  , unFaceId
-  , unUndirectedEdgeId
-  , unVertexId
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( allFaces
-  , directedEdges
-  , foldAllFaces'
-  , foldDirectedEdges'
-  , foldInnerFaces'
-  , foldUndirectedEdges'
-  , foldVertices'
-  , innerFaces
-  , undirectedEdges
-  , vertices
-  )
-import Moonlight.Triangulation.Handles.Iterators.HullIterator
-  ( hullEdges
-  , hullVertices
-  )
-import Moonlight.Triangulation.Handles.Scoped
-  ( ScopedDirectedEdge
-  , ScopedFace
-  , ScopedTriangulation
-  , ScopedUndirectedEdge
-  , ScopedVertex
-  , scopedAdjacentEdge
-  , scopedAsUndirected
-  , scopedDestination
-  , scopedDirectedEdgeData
-  , scopedDirectedEdges
-  , scopedFaceData
-  , scopedFaceDirectedEdges
-  , scopedFaceVertices
-  , scopedFaces
-  , scopedIncidentFace
-  , scopedInnerFaces
-  , scopedIsBoundaryEdge
-  , scopedIsConstraintEdge
-  , scopedIsOuterDirectedEdge
-  , scopedNext
-  , scopedOrigin
-  , scopedOuterFace
-  , scopedPrevious
-  , scopedReverseEdge
-  , scopedUndirectedEdgeData
-  , scopedUndirectedEdges
-  , scopedUndirectedEndpoints
-  , scopedVertexData
-  , scopedVertexOutEdge
-  , scopedVertexOutgoingEdges
-  , scopedVertexPoint
-  , scopedVertices
-  , withScopedTriangulation
-  )
-import Moonlight.Triangulation.Canonical (canonicalize)
-import Moonlight.Triangulation.Cdt
-  ( CanonicalSegment
-  , CdtBuildResult (..)
-  , CdtError (..)
-  , ConstrainedExtensionResult
-  , ConstrainedSeamFaceEvidence
-  , ConstrainedSeamResult
-  , ConstrainedSeamSide (..)
-  , ConstrainedUnionError (..)
-  , ConstraintBatchStats (..)
-  , ConstraintBatchResult
-  , ConstraintConflict
-  , ConstraintOutcome (..)
-  , ConstraintRecoveryResult (..)
-  , ConstraintResult
-  , ConstraintSplitBatchResult
-  , CorridorObstruction (..)
-  , addConstraintAndSplit
-  , addConstraintEdge
-  , addConstraintEdges
-  , addConstraintsAndSplit
-  , boundedRegionFaces
-  , canAddConstraint
-  , constrainedDelaunay
-  , constrainedDelaunayMaximal
-  , constrainedExtensionBuildStats
-  , constrainedExtensionConstraintBatch
-  , constraintEdges
-  , constraintBatchOutcomes
-  , constraintBatchStats
-  , constraintBatchTriangulation
-  , constraintSegments
-  , constraintStorageBytes
-  , constrainedSeamBuildStats
-  , constrainedSeamConstraintStats
-  , constrainedSeamCachedFrontierPointReads
-  , constrainedSeamFaceFirstPoint
-  , constrainedSeamFaceSecondPoint
-  , constrainedSeamFaceThirdPoint
-  , constrainedSeamLeftConstraintCount
-  , constrainedSeamLeftFaceCount
-  , constrainedSeamJoinFaces
-  , constrainedSeamPublicationStats
-  , constrainedSeamResultTriangulation
-  , constrainedSeamRightFaceEvidence
-  , constrainedSeamSourceFace
-  , constrainedSeamTargetFace
-  , conflictFirstSegment
-  , conflictSecondSegment
-  , existsConstraint
-  , extendConstrainedWith
-  , fromDelaunay
-  , getConflictingEdgesBetweenPoints
-  , getConflictingEdgesBetweenVertices
-  , intersectsConstraint
-  , joinSeparatedConstrained
-  , outerRegionFaces
-  , recoverConstraints
-  , removeConstraintEdge
-  , segmentEnd
-  , segmentStart
-  , unionConstrained
-  , unionConstrainedWith
-  )
-import Moonlight.Triangulation.Internal.Paged (PublicationStats (..))
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactArithmeticError (..)
-  , ExactRational
-  , exactRational
-  , exactRationalDenominator
-  , exactRationalNumerator
-  )
-import Moonlight.Triangulation.Internal.Representation (geometryOnlyPublication)
-import Moonlight.Triangulation.Interop
-  ( Coordinate2 (..)
-  , mapCoordinate2
-  )
-import Moonlight.Triangulation.IntersectionIterator
-  ( Intersection (..)
-  , conflictingEdges
-  , foldCorridorBetweenPoints
-  , foldCorridorBetweenVertices
-  , lineIntersections
-  , lineIntersectionsBetweenVertices
-  , segmentIntersectsNonCollinear
-  )
-import Moonlight.Triangulation.Types
-  ( BuildError (..)
-  , BuildResult
-  , BuildStats (..)
-  , ConstrainedDelaunayTriangulation
-  , ConstraintMode (..)
-  , CoordinateError (..)
-  , DelaunayTriangulation
-  , ElementDefaults (..)
-  , HasPosition (..)
-  , InsertionDisposition (..)
-  , InsertionResult
-  , InvariantViolation (..)
-  , Location (..)
-  , LocationHint (..)
-  , LocationStats (..)
-  , NearestStats (..)
-  , NonFiniteValue (..)
-  , Point (..)
-  , PointValidationError (..)
-  , QueryPoint
-  , RadiusSquared
-  , RadiusSquaredError (..)
-  , RefinementDomainResult
-  , RefinementParameters (..)
-  , RefinementReceipt
-  , RefinementResult
-  , SiteRelation (..)
-  , Triangulation
-  , ValidationClosureStats (..)
-  , buildInputVertices
-  , buildStats
-  , buildTriangulation
-  , defaultRefinementParameters
-  , emptyBuildStats
-  , emptyLocationStats
-  , insertionDisposition
-  , insertionStats
-  , insertionTriangulation
-  , insertionVertex
-  , mkRadiusSquared
-  , queryPointValue
-  , radiusSquaredValue
-  , refinedTriangulation
-  , refinementAddedVertices
-  , refinementAttemptedBoundaryCrossings
-  , refinementComplete
-  , refinementCreatedFaces
-  , refinementDomainReceipt
-  , refinementDomainResult
-  , refinementExcludedFaces
-  , refinementFinalInterfaceIncidence
-  , refinementFinalPermittedFaces
-  , refinementInterfaceBoundaryReads
-  , refinementPublicationStats
-  , refinementRemovedEdges
-  , refinementStats
-  , refinementTouchedEdges
-  , refinementValidationClosureStats
-  , refinementVisitedJoinFaces
-  , refinementVisitedProtectedFaces
-  , unitElementDefaults
-  )
-import Moonlight.Triangulation.Interpolation
-  ( interpolateBarycentric
-  , interpolateNearest
-  , nearestNeighbor
-  )
-import Moonlight.Triangulation.Minkowski
-  ( ConvexPolygon
-  , MinkowskiError (..)
-  , MinkowskiOperation (..)
-  , MinkowskiReceipt (..)
-  , StructuringElement
-  , closeWith
-  , convexMinkowskiSum
-  , reflectConvexPolygon
-  , convexPolygon
-  , convexPolygonPoints
-  , erodeBy
-  , minkowskiSum
-  , openWith
-  , polygonInset
-  , polygonOffset
-  , structuringElement
-  )
-import Moonlight.Triangulation.Math
-  ( barycentricCoordinates
-  , centroid
-  , circumcenter
-  , distance
-  , inCircle
-  , midpoint
-  , mkQueryPoint
-  , orient2d
-  , segmentRelation
-  , segmentsIntersect
-  , segmentsProperlyCross
-  , sideQuery
-  , squaredDistance
-  , triangleArea
-  )
-import Moonlight.Triangulation.Overlay
-  ( CoverGap
-  , LayerCoverageError (..)
-  , OverlayError (..)
-  , OverlayReceipt (..)
-  , OverlayResult
-  , OverlaySelectionError (..)
-  , OverlaySelectionKind (..)
-  , coverGapRegion
-  , layerCovers
-  , overlayAll
-  , overlayArrangementEdges
-  , overlayArrangementVertices
-  , overlayCells
-  , overlayClosedIntersection
-  , overlayClosedUnion
-  , overlayEmbeddedTriangulation
-  , overlayLayers
-  , overlayMass
-  , overlayConfusion
-  , overlayPlanarLayer
-  , overlayReceipt
-  , overlayRegularizedDifference
-  , overlaySelectedRegion
-  )
-import Moonlight.Triangulation.PowerDiagram
-  ( AffineForm (..)
-  , BoundedPowerDiagram
-  , PowerDualEdge (..)
-  , PowerCellDisposition (..)
-  , PowerDiagramError (..)
-  , PowerDiagramReceipt (..)
-  , PowerSite
-  , PowerWeight
-  , PowerWeightError (..)
-  , RegularEdge
-  , RegularEditError (..)
-  , RegularEditResult (..)
-  , RegularFace
-  , RegularSiteDisposition (..)
-  , RegularSiteTransition (..)
-  , RegularTriangulation
-  , RegularTriangulationReceipt (..)
-  , RegularTopologyError (..)
-  , UpperEnvelopeError (..)
-  , boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , emptyRegularTriangulation
-  , insertRegularSite
-  , powerCellDisposition
-  , powerCellDispositions
-  , powerDiagramPlanarLayer
-  , powerDiagramInputSites
-  , powerDiagramPeakIntermediateBitGrowth
-  , powerDiagramFinalCoordinateBitGrowth
-  , powerSite
-  , powerSiteLabel
-  , powerSitePosition
-  , powerSiteExactPosition
-  , powerSiteWeight
-  , powerWeight
-  , powerWeightFromExact
-  , powerWeightExact
-  , regularEdgeDual
-  , regularEdgeLabels
-  , regularEdges
-  , regularFaceDualPoint
-  , regularFaceLabels
-  , regularFaces
-  , regularNeighbours
-  , regularSite
-  , regularSiteCount
-  , regularSiteDisposition
-  , regularSites
-  , regularTriangulation
-  , regularTriangulationReceipt
-  , removeRegularSite
-  , reweightRegularSites
-  , upperEnvelope
-  )
-import Moonlight.Triangulation.RegularAlpha
-  ( PowerAlphaBirth
-  , RegularAlphaError (..)
-  , RegularAlphaFiltration
-  , powerAlphaBirthDenominator
-  , powerAlphaBirthExact
-  , powerAlphaBirthNumerator
-  , regularAlphaBirths
-  , regularAlphaComplex
-  , regularAlphaComplexAtBirth
-  , regularAlphaCriticalBirths
-  , regularAlphaFiltration
-  , regularAlphaSimplexBirth
-  )
-import Moonlight.Triangulation.Payload
-  ( PayloadTraversal
-  , directedPayloads
-  , facePayloads
-  , foldPayloads
-  , mapDirectedEdges
-  , mapFaces
-  , mapUndirectedEdges
-  , mapVertices
-  , overPayloads
-  , payloadList
-  , undirectedPayloads
-  , vertexPayloads
-  )
-import Moonlight.Triangulation.PointLocation
-  ( locatePoint
-  , locatePointWithHint
-  )
-import Moonlight.Triangulation.Removal
-  ( RemovalOutcome (..)
-  , RemovalResult (..)
-  , locateAndRemove
-  , removeVertex
-  )
-import Moonlight.Triangulation.JoinSemilattice (JoinSemilattice (..))
-import Moonlight.Triangulation.Refinement
-  ( radiusEdgeRatioForAngle
-  , refine
-  , refineWithinDomain
-  , validateRefinementParameters
-  , withMinimumAngle
-  )
-import Moonlight.Triangulation.Region
-  ( ExactLoop
-  , PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , RegionPointLocation (..)
-  , RegionPublicationError (..)
-  , RegionValidationError (..)
-  , emptyPlanarRegion
-  , exactLoop
-  , exactLoopPoints
-  , labelledPlanarLayer
-  , planarLayer
-  , planarLayerLabelAt
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarRegion
-  , planarRegionComponents
-  , polygonComponent
-  , polygonHoleLoops
-  , polygonOuterLoop
-  , regionPointLocation
-  )
-import Moonlight.Triangulation.SetAlgebra
-  ( difference
-  , intersection
-  , intersectionWith
-  , siteRelation
-  , symmetricDifference
-  , union
-  , unions
-  )
-import Moonlight.Triangulation.Session
-  ( Session
-  , insertVertex
-  , insertVertexAt
-  , insertVertexAtNear
-  , insertVertexAtNearVertex
-  , refuse
-  , removeAt
-  , removeAtNear
-  , removeManyAt
-  , removeManyAtNear
-  , withSession
-  )
-import Moonlight.Triangulation.Validation
-  ( faceArea
-  , faceMinimumAngleDegrees
-  , triangulationIsValid
-  , validateDelaunay
-  , validateTopology
-  , validateTriangulation
-  )
-import Moonlight.Triangulation.Valuation
-  ( CertifiedInterval (..)
-  , EulerCharacteristic
-  , ExactArea
-  , ExactPlanarMoments
-  , ExactLengthExpression
-  , ExactLengthMeasurement
-  , ExactLengthTerm
-  , PlanarValuations
-  , ValuationError (..)
-  , cellSetPerimeter
-  , cellValuations
-  , eulerCharacteristicValue
-  , exactAreaValue
-  , exactPlanarFirstX
-  , exactPlanarFirstY
-  , exactPlanarMeasure
-  , exactPlanarSecondXX
-  , exactPlanarSecondXY
-  , exactPlanarSecondYY
-  , polygonComponentArea
-  , polygonComponentMoments
-  , exactLengthBounds
-  , exactLengthExpression
-  , exactLengthTerms
-  , lengthCoefficient
-  , planarValuationsPerimeter
-  , regionPerimeter
-  , regionValuations
-  , scaleExactPlanarMoments
-  , squaredLength
-  , valuationArea
-  , valuationEuler
-  , valuationIntrinsic1
-  )
diff --git a/src-public/Moonlight/Triangulation/HintGenerator.hs b/src-public/Moonlight/Triangulation/HintGenerator.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/HintGenerator.hs
+++ /dev/null
@@ -1,493 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE NamedFieldPuns #-}
-
--- | Reusable point-location hints and their topology-preserving maintenance.
-module Moonlight.Triangulation.HintGenerator
-  ( LastUsedHint
-  , emptyLastUsedHint
-  , lastUsedHint
-  , rememberVertex
-  , HierarchyHint
-  , defaultHierarchyBranchFactor
-  , buildHierarchyHint
-  , hierarchyHint
-  , hierarchyBranchFactor
-  , hierarchyBaseCount
-  , hierarchyLevelCount
-  , hierarchyVertexCount
-  , updateHierarchyAfterInsertion
-  , updateHierarchyAfterRemoval
-  , rebuildHierarchyHint
-  , removeManyWithHierarchy
-  ) where
-
-import qualified Data.Vector as V
-import Control.DeepSeq (NFData)
-import Data.List (sort)
-import Data.Word (Word32)
-import Moonlight.Triangulation.BulkLoad (delaunay, insert)
-import Moonlight.Triangulation.Dcel (numUndirectedEdges, numVertices, undirectedEndpoints, vertexPoint)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( UndirectedEdgeId (..)
-  , VertexId (..)
-  , vertexIdIndex
-  )
-import Moonlight.Triangulation.Interpolation (nearestNeighbor)
-import Moonlight.Triangulation.Math (canonicalPoint, validatePoint)
-import Moonlight.Triangulation.Removal (RemovalOutcome, removalTriangulation, removeVertex)
-import Moonlight.Triangulation.Session (removeManyAtNear, withSession)
-import Moonlight.Triangulation.Types
-import GHC.Generics (Generic)
-
--- | Most recently admitted vertex, suitable as the next descent seed.
-newtype LastUsedHint = LastUsedHint (Maybe VertexId)
-  deriving stock (Show)
-  deriving newtype (Eq, Ord)
-
--- | A last-used hint with no remembered vertex.
-emptyLastUsedHint :: LastUsedHint
-emptyLastUsedHint = LastUsedHint Nothing
-
--- | Project a remembered vertex into a location hint.
-lastUsedHint :: LastUsedHint -> Maybe LocationHint
-lastUsedHint (LastUsedHint vertex) = VertexHint <$> vertex
-
--- | Replace the remembered vertex.
-rememberVertex :: VertexId -> LastUsedHint -> LastUsedHint
-rememberVertex vertex _ = LastUsedHint (Just vertex)
-
--- | A Delaunay hierarchy for logarithmic expected random point location.
--- The first vector element is the finest sparse level; the final element is
--- the coarsest. The base triangulation is not duplicated.
---
--- The levels are nested by a single arithmetic law rather than by a stored
--- correspondence. Level @i@ holds every @branch^(i+1)@-th base vertex in base
--- order, so a level-local handle @j@ names handle @j * branch@ one level finer
--- — and at level 0 the finer level is the base mesh itself, under the very
--- same multiplication. Descent therefore never rediscovers a handle it has
--- already computed, and no level carries an index vector.
---
--- 'hierarchyBaseCount' is the base cardinality the levels were sampled from.
--- It is the hierarchy's claim about which triangulation it answers for, and
--- every maintenance entry is stated against it: a base whose cardinality is not
--- the one recorded here plus the movement the operation performs is not the
--- base this hierarchy describes, and is rebuilt for rather than patched.
-data HierarchyHint = HierarchyHint
-  { hierarchyBranchFactor :: {-# UNPACK #-} !Int
-    -- ^ Sampling stride between adjacent hierarchy levels.
-  , hierarchyBaseCount :: {-# UNPACK #-} !Int
-    -- ^ Cardinality of the base mesh described by the hierarchy.
-  , hierarchyLevels :: !(V.Vector (Triangulation 'Unconstrained (Point) () () ()))
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
-instance Show (HierarchyHint) where
-  showsPrec precedence hierarchy =
-    showParen (precedence > 10) $
-      showString "HierarchyHint "
-        . shows (hierarchyBranchFactor hierarchy)
-        . showString " "
-        . shows (hierarchyBaseCount hierarchy)
-        . showString " "
-        . shows (hierarchyLevelCount hierarchy)
-        . showString " "
-        . shows (hierarchyVertexCount hierarchy)
-
-instance Eq (HierarchyHint) where
-  left == right =
-    hierarchyBranchFactor left == hierarchyBranchFactor right
-      && hierarchyBaseCount left == hierarchyBaseCount right
-      && hierarchyLevelCount left == hierarchyLevelCount right
-      && V.and (V.zipWith sameSparseMesh (hierarchyLevels left) (hierarchyLevels right))
-
--- Two sparse levels are the same hint when they carry the same points and the
--- same undirected edges. Half-edge index labelling records the order
--- construction happened to visit, so a level rebuilt from scratch and a level
--- extended in place are structurally equal while their arrays are not.
-sameSparseMesh
-  :: Triangulation 'Unconstrained (Point) () () ()
-  -> Triangulation 'Unconstrained (Point) () () ()
-  -> Bool
-sameSparseMesh left right =
-  numVertices left == numVertices right
-    && numUndirectedEdges left == numUndirectedEdges right
-    && meshPoints left == meshPoints right
-    && meshEdges left == meshEdges right
- where
-  meshPoints
-    :: Triangulation mode vertex directed undirected face
-    -> [Point]
-  meshPoints triangulation =
-    sort [vertexPoint triangulation (VertexId (fromIntegral index)) | index <- [0 .. numVertices triangulation - 1]]
-
-  meshEdges
-    :: Triangulation mode vertex directed undirected face
-    -> [(Point, Point)]
-  meshEdges triangulation =
-    sort
-      [ if from <= to then (from, to) else (to, from)
-      | index <- [0 .. numUndirectedEdges triangulation - 1]
-      , let (fromVertex, toVertex) = undirectedEndpoints triangulation (UndirectedEdgeId (fromIntegral index))
-            from = vertexPoint triangulation fromVertex
-            to = vertexPoint triangulation toVertex
-      ]
-
--- | Default sampling stride between hierarchy levels.
-defaultHierarchyBranchFactor :: Int
-defaultHierarchyBranchFactor = 16
-
--- | Number of stored sparse levels.
-hierarchyLevelCount :: HierarchyHint -> Int
-hierarchyLevelCount = V.length . hierarchyLevels
-
--- | Total vertices retained across every sparse level.
-hierarchyVertexCount :: HierarchyHint -> Int
-hierarchyVertexCount =
-  V.foldl' (\total level -> total + numVertices level) 0 . hierarchyLevels
-
--- | Build nested sparse Delaunay levels. A branch factor of 16 mirrors Spade's
--- default and gives O(log n) expected descent on uniformly distributed input.
---
--- Each level is bulk loaded from the sampled points in base order. Base
--- vertices carry pairwise distinct positions, so the load deduplicates nothing
--- and assigns local handle @j@ to sample @j@ — which is what makes the nesting
--- law on 'hierarchyLevels' an identity rather than a lookup.
-buildHierarchyHint
-  :: Int
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError (HierarchyHint)
-buildHierarchyHint requestedBranch triangulation =
-  HierarchyHint branch count <$> V.mapM buildLevel levelDivisors
- where
-  !branch = max 2 requestedBranch
-  !count = numVertices triangulation
-  levelDivisors =
-    V.unfoldr
-      (\candidate ->
-         case candidate of
-           Nothing -> Nothing
-           Just divisor ->
-             let !population = samplePopulation count divisor
-                 next =
-                   if population <= 1
-                     then Nothing
-                     else Just (safeMultiply divisor branch)
-              in Just (divisor, next)
-      )
-      (if count <= 0 then Nothing else Just branch)
-
-  buildLevel divisor =
-    buildTriangulation
-      <$> delaunay
-        unitElementDefaults
-        ( V.generate
-            (samplePopulation count divisor)
-            (\index ->
-               vertexPoint
-                 triangulation
-                 (VertexId (fromIntegral (index * divisor)))
-            )
-        )
-
--- | Descend from the coarsest sparse triangulation. The handle a level returns
--- is carried to the next finer level by one multiplication, and the same
--- multiplication at level 0 names the base vertex. Nothing is relocated: the
--- coarse answer is not searched for again, it is computed.
-hierarchyHint :: HierarchyHint -> QueryPoint -> Maybe LocationHint
-hierarchyHint HierarchyHint{hierarchyBranchFactor, hierarchyLevels} query =
-  VertexHint <$> descend (V.length hierarchyLevels - 1) Nothing
- where
-  !branch = fromIntegral hierarchyBranchFactor :: Word32
-
-  descend !levelIndex !coarse
-    | levelIndex < 0 = Nothing
-    | otherwise =
-        case nearestNeighbor (hierarchyLevels V.! levelIndex) coarse query of
-          Nothing -> Nothing
-          Just (VertexId local, _) ->
-            let !finer = VertexId (local * branch)
-             in if levelIndex == 0 then Just finer else descend (levelIndex - 1) (Just finer)
-
-
--- | Update the nested hierarchy from an insertion's own report: the point the
--- insertion was asked for, the handle it answered, and whether it created a
--- site. Only the levels selected by the branch divisibility rule are changed.
--- Unaffected levels are structurally shared.
---
--- No triangulation is named. The hierarchy walks its own levels and nothing
--- else, so a base was only ever a lookup table for three facts — its
--- cardinality, the stored position of the new vertex, and the position of
--- vertex zero — and every one of them is in the report or already in the
--- levels, because level-local handle zero is base handle zero at every level.
--- A caller maintaining the hierarchy across a run of insertions therefore
--- never has to publish a mesh to be allowed to speak to it, which is the whole
--- cost of the arrangement this replaces: one full arena copy per step, paid
--- only to name the thing that was just edited.
---
--- An insertion that found its point already present created no site, and a
--- hierarchy valid for a triangulation is valid for that same triangulation, so
--- the answer is the argument, unexamined.
-updateHierarchyAfterInsertion
-  :: HierarchyHint
-  -> Point
-  -> VertexId
-  -> InsertionDisposition
-  -> Either BuildError (HierarchyHint)
-updateHierarchyAfterInsertion hierarchy requested vertex disposition =
-  case disposition of
-    AlreadyPresent -> Right hierarchy
-    Inserted
-      | vertexIdIndex vertex /= baseIndex ->
-          Left
-            ( HierarchyInsertionHandleMismatch
-                (VertexId (fromIntegral baseIndex))
-                vertex
-            )
-      | otherwise -> do
-          updated <- updateLevels branch (V.toList (hierarchyLevels hierarchy))
-          topped <- ensureSingletonTop updated
-          pure
-            hierarchy
-              { hierarchyBaseCount = baseIndex + 1
-              , hierarchyLevels = V.fromList topped
-              }
- where
-  !branch = hierarchyBranchFactor hierarchy
-  !baseIndex = hierarchyBaseCount hierarchy
-  -- A level holds what the mesh stores, not what the caller wrote: storage
-  -- rounds a signed zero, and a level built from the unrounded point would
-  -- settle a distance tie against a different handle.
-  !point = canonicalPoint requested
-
-  -- An update is the only operation that can break the nesting law, so the law
-  -- is stated here as an equation rather than trusted: a level admitted to the
-  -- new base vertex must already hold exactly @baseIndex / divisor@ samples,
-  -- and must receive the vertex at its end.
-  updateLevels !_ [] = Right []
-  updateLevels !divisor levels@(level : rest)
-    | baseIndex `rem` divisor /= 0 = Right levels
-    | safeMultiply (numVertices level) divisor /= baseIndex =
-        Left
-          ( HierarchyLevelPopulationMismatch
-              divisor
-              (numVertices level)
-              baseIndex
-          )
-    | otherwise = do
-        inserted <- insert level point
-        if vertexIdIndex (insertionVertex inserted) /= numVertices level
-          then
-            Left
-              ( HierarchyInsertionHandleMismatch
-                  (VertexId (fromIntegral (numVertices level)))
-                  (insertionVertex inserted)
-              )
-          else (insertionTriangulation inserted :) <$> updateLevels (safeMultiply divisor branch) rest
-
-  -- With no levels at all the base was empty, so the vertex just appended is
-  -- vertex zero. Otherwise vertex zero is the finest level's local vertex
-  -- zero, under the same law that makes the descent a multiplication.
-  ensureSingletonTop [] = pure <$> singletonLevel point
-  ensureSingletonTop levels@(finest : _) =
-    case reverse levels of
-      top : _
-        | numVertices top <= 1 -> Right levels
-        | otherwise ->
-            (\first -> levels ++ [first])
-              <$> singletonLevel (vertexPoint finest (VertexId 0))
-      [] -> Right levels
-
--- | Repair the nested hierarchy from a removal's swap report — the slot
--- compaction freed and the position of the vertex it moved into that slot, or
--- 'Nothing' when the removal took the last vertex and compaction moved
--- nothing — rather than rebuilding because removal renumbers.
---
--- No triangulation is named, for the reason 'updateHierarchyAfterInsertion'
--- gives: the only base position this repair cannot find in its own levels is
--- the relocated vertex's, and that is what the report carries.
---
--- Swap compaction moves exactly one vertex — the former last one, into the slot
--- the removed vertex vacated — so a level's sample sequence changes in at most
--- one place, and which place is decided by the two divisibilities the level's
--- divisor gives the freed slot and the vacated last index:
---
--- * neither is sampled: the level, and every coarser level above it, is
---   untouched, because a divisor that divides neither index is divided by no
---   multiple of itself either;
--- * the vacated index is sampled and the freed slot is not: the level loses its
---   last sample and nothing else;
--- * both are sampled: the level loses its last sample and that sample's point
---   lands in the freed slot's local position — which is the level's own swap
---   removal, mirroring the base's;
--- * the freed slot is sampled and the vacated index is not: the level keeps its
---   population and substitutes the relocated position at an interior local
---   slot. No removal expresses a substitution, so that level is rebuilt — from
---   its own points and the reported one, never from a mesh.
---
--- The last case is the only one that pays a build, and it is the rarest: it
--- needs the freed slot to be sampled and the vacated index not to be.
-updateHierarchyAfterRemoval
-  :: HierarchyHint
-  -> Maybe (VertexId, Point)
-  -> Either BuildError (HierarchyHint)
-updateHierarchyAfterRemoval hierarchy swap
-  | baseCount <= 0 = Left (RemovalEmptyTriangulation (maybe (VertexId 0) fst swap))
-  | otherwise = do
-      repaired <- repairLevels branch (V.toList (hierarchyLevels hierarchy))
-      pure
-        hierarchy
-          { hierarchyBaseCount = surviving
-          , hierarchyLevels = V.fromList (levelsThroughSingleton repaired)
-          }
- where
-  !branch = hierarchyBranchFactor hierarchy
-  !baseCount = hierarchyBaseCount hierarchy
-  !surviving = baseCount - 1
-  -- The freed slot is the removed vertex's own index, and it is where the
-  -- former last vertex now stands. A removal that took the last vertex frees
-  -- no slot and reports none, and its removed index is that last index.
-  !freedSlot = maybe surviving (vertexIdIndex . fst) swap
-  !vacatedIndex = surviving
-
-  -- The nesting law is stated here as an equation for the same reason the
-  -- insertion path states it: a repair is the other operation that can break
-  -- it. A level the removal reaches must hold exactly the samples the
-  -- pre-removal base owed it.
-  repairLevels !_ [] = Right []
-  repairLevels !divisor levels@(level : rest)
-    | not freedSampled && not vacatedSampled = Right levels
-    | numVertices level /= population =
-        Left
-          ( HierarchyLevelPopulationMismatch
-              divisor
-              (numVertices level)
-              baseCount
-          )
-    | otherwise =
-        case swap of
-          -- The two divisibilities differ only when the two indices do, so a
-          -- level reaching the substitution has a relocation to substitute:
-          -- the guard cannot hold while compaction moved nothing.
-          Just (_, relocated)
-            | freedSampled && not vacatedSampled -> do
-                substituted <- substituteSample level (freedSlot `quot` divisor) relocated
-                (substituted :) <$> repairLevels (safeMultiply divisor branch) rest
-          _ -> do
-            shrunk <- removeVertex level (VertexId (fromIntegral localSample))
-            (removalTriangulation shrunk :) <$> repairLevels (safeMultiply divisor branch) rest
-   where
-    !freedSampled = freedSlot `rem` divisor == 0
-    !vacatedSampled = vacatedIndex `rem` divisor == 0
-    !population = samplePopulation baseCount divisor
-    !localSample
-      | freedSampled = freedSlot `quot` divisor
-      | otherwise = population - 1
-
-  -- A build stops at the first level holding one sample, and removal only
-  -- shrinks populations, so the shape a rebuild would answer with is this list
-  -- cut after its first singleton.
-  levelsThroughSingleton
-    :: [Triangulation mode vertex directed undirected face]
-    -> [Triangulation mode vertex directed undirected face]
-  levelsThroughSingleton [] = []
-  levelsThroughSingleton (level : rest)
-    | numVertices level <= 0 = []
-    | numVertices level <= 1 = [level]
-    | otherwise = level : levelsThroughSingleton rest
-
--- | The coarsest level a growing hierarchy needs: one sample, the base's
--- vertex zero.
-singletonLevel
-  :: Point
-  -> Either BuildError (Triangulation 'Unconstrained (Point) () () ())
-singletonLevel origin =
-  buildTriangulation <$> delaunay unitElementDefaults (V.singleton origin)
-
--- | The level a substitution asks for: the same samples in the same local
--- order, one slot carrying the relocated position instead of the one that
--- left. Stated over the level's own points, so no mesh is consulted.
-substituteSample
-  :: Triangulation 'Unconstrained (Point) () () ()
-  -> Int
-  -> Point
-  -> Either BuildError (Triangulation 'Unconstrained (Point) () () ())
-substituteSample level localSlot relocated =
-  buildTriangulation
-    <$> delaunay
-      unitElementDefaults
-      ( V.generate
-          (numVertices level)
-          (\index ->
-            if index == localSlot
-              then relocated
-              else vertexPoint level (VertexId (fromIntegral index))
-          )
-      )
-
--- | Rebuild a hierarchy after an operation that may renumber vertices, such as
--- removal. The branch factor remains canonical.
-rebuildHierarchyHint
-  :: HierarchyHint
-  -> Triangulation mode vertex directed undirected face
-  -> Either BuildError (HierarchyHint)
-rebuildHierarchyHint hierarchy = buildHierarchyHint (hierarchyBranchFactor hierarchy)
-
--- | Remove many points, each locate starting from the hierarchy's nearest
--- sample instead of the mesh boundary. One session publishes once; the
--- hierarchy is rebuilt against the surviving mesh and returned alongside it.
-removeManyWithHierarchy
-  :: HierarchyHint
-  -> Triangulation mode vertex directed undirected face
-  -> V.Vector (Point)
-  -> Either
-      BuildError
-      ( V.Vector (Maybe (RemovalOutcome vertex))
-      , Triangulation mode vertex directed undirected face
-      , HierarchyHint
-      )
-removeManyWithHierarchy hierarchy triangulation points = do
-  queryPoints <- traverse (validatePoint Nothing) points
-  let guesses = fmap hierarchyGuess queryPoints
-  (outcomes, surviving, _) <-
-    withSession triangulation 0 (removeManyAtNear guesses points)
-  repaired <- rebuildHierarchyHint hierarchy surviving
-  pure (outcomes, surviving, repaired)
- where
-  hierarchyGuess queryPoint =
-    case hierarchyHint hierarchy queryPoint of
-      Just (VertexHint vertex) -> Just vertex
-      _ -> Nothing
-
-safeMultiply :: Int -> Int -> Int
-safeMultiply left right
-  | left > maxBound `quot` right = maxBound
-  | otherwise = left * right
-
--- | How many samples a divisor takes from a base of this size. Sampling takes
--- index zero and every @divisor@-th index after it; the direct
--- vector generator above states the same ceiling without constructing an
--- intermediate handle list.
-samplePopulation :: Int -> Int -> Int
-samplePopulation count divisor
-  | count <= 0 = 0
-  | otherwise = 1 + (count - 1) `quot` divisor
-
--- The hierarchy is consulted once per query from another package, so its
--- polymorphic entries expose their unfoldings for the same reason the search
--- itself does.
---
--- The two level constructors are listed for a second reason, and the list is
--- not complete without them: they are overloaded and the entries call them, so
--- an entry specialised in the consumer that reaches an unspecialised
--- constructor threads the dictionary right back into the build it was
--- specialised to avoid. Only a stable unfolding is a specialisation candidate
--- across a package boundary; the optimised one GHC publishes on its own is not.
--- These two were 'where' bindings before they were named, and a 'where' binding
--- is specialised with the function that encloses it — so naming them is what
--- put the dictionary in, and this is what takes it back out.
diff --git a/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs b/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/Minkowski/Convex.hs
+++ /dev/null
@@ -1,304 +0,0 @@
--- | Pure exact convex-polygon algebra: admission, linear edge-angle
--- convolution, reflection, hull construction, and support-half-plane erosion.
-module Moonlight.Triangulation.Internal.Minkowski.Convex
-  ( convexPolygon
-  , convexPolygonPoints
-  , convexPolygonComponent
-  , convexPolygonRegion
-  , convexPolygonFromRetained
-  , admittedConvexLoop
-  , structuringElement
-  , structuringElementPolygon
-  , convexMinkowskiSum
-  , convexMinkowskiPolygon
-  , convexHullPolygon
-  , reflectConvexPolygon
-  , convexPolygonCentroid
-  , erodeConvexBy
-  , addExactPoints
-  , subtractExactPoints
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Set as Set
-import Moonlight.Triangulation.Exact
-  ( ExactClipDisposition (..)
-  , ExactClipReceipt
-  , ExactClosedHalfPlane
-  , ExactPoint
-  , ExactRetainedPolygon
-  , ExactVector (..)
-  , addExactVectors
-  , exactClipRetainedPolygon
-  , exactClosedHalfPlaneFromDirectedEdge
-  , compareExactVectorAngle
-  , exactVectorFromPoints
-  , exactOrient2d
-  , exactPoint
-  , exactPointCoordinates
-  , exactRetainedPolygon
-  , exactRetainedPolygonPoints
-  , translateExactPoint
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( cyclePairs
-  , cyclePairsNonEmpty
-  , firstNonCounterClockwiseTurn
-  , rotateCycleLeast
-  , rotateCycleLeastBy
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactDivide
-  )
-import Moonlight.Triangulation.Internal.Minkowski.Types
-  ( ConvexPolygon (..)
-  , MinkowskiError (..)
-  , StructuringElement (..)
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop (..)
-  , PlanarRegion (..)
-  , PolygonComponent (..)
-  , RegionPointLocation (..)
-  )
-import Moonlight.Triangulation.Region
-  ( exactLoop
-  , exactLoopPoints
-  , regionPointLocation
-  )
-
-convexPolygon
-  :: NonEmpty ExactPoint
-  -> Either MinkowskiError ConvexPolygon
-convexPolygon submitted = do
-  loop <- first MinkowskiInvalidConvexLoop (exactLoop submitted)
-  let points = exactLoopPoints loop
-  case firstNonCounterClockwiseTurn exactOrient2d points of
-    Just (index, turn) -> Left (MinkowskiNonConvexTurn index turn)
-    Nothing -> Right (ConvexPolygon loop)
-
-convexPolygonPoints :: ConvexPolygon -> NonEmpty ExactPoint
-convexPolygonPoints (ConvexPolygon loop) = exactLoopPoints loop
-
-convexPolygonComponent :: ConvexPolygon -> PolygonComponent
-convexPolygonComponent (ConvexPolygon loop) = PolygonComponent loop []
-
-convexPolygonRegion :: ConvexPolygon -> PlanarRegion
-convexPolygonRegion = PlanarRegion . pure . convexPolygonComponent
-
--- | Total projection from the retained-line carrier. Strict convexity and
--- simplicity are already invariants of 'ExactRetainedPolygon'; only canonical
--- cycle rotation remains.
-convexPolygonFromRetained :: ExactRetainedPolygon -> ConvexPolygon
-convexPolygonFromRetained = admittedConvexPolygon . exactRetainedPolygonPoints
-
-admittedConvexPolygon :: NonEmpty ExactPoint -> ConvexPolygon
-admittedConvexPolygon = ConvexPolygon . ExactLoop . rotateCycleLeast
-
-admittedConvexLoop :: ExactLoop -> Maybe ConvexPolygon
-admittedConvexLoop loop =
-  case firstNonCounterClockwiseTurn exactOrient2d (exactLoopPoints loop) of
-    Nothing -> Just (ConvexPolygon loop)
-    Just _ -> Nothing
-
-structuringElement
-  :: ConvexPolygon
-  -> Either MinkowskiError StructuringElement
-structuringElement polygon =
-  let origin = exactPoint 0 0
-      location = regionPointLocation (convexPolygonRegion polygon) origin
-   in case location of
-        RegionExterior -> Left (MinkowskiOriginOutside location)
-        _ -> Right (StructuringElement polygon)
-
-structuringElementPolygon :: StructuringElement -> ConvexPolygon
-structuringElementPolygon (StructuringElement polygon) = polygon
-
-convexMinkowskiSum
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> PlanarRegion
-convexMinkowskiSum left right =
-  convexPolygonRegion (convexMinkowskiPolygon left right)
-
-convexMinkowskiPolygon
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> ConvexPolygon
-convexMinkowskiPolygon left right =
-  let leftPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints left)
-      rightPoints = rotateCycleLeastBy pointSweepKey (convexPolygonPoints right)
-      start = addExactPoints (NonEmpty.head leftPoints) (NonEmpty.head rightPoints)
-      directions =
-        mergeDirections
-          (edgeDirections leftPoints)
-          (edgeDirections rightPoints)
-      directionList = NonEmpty.toList directions
-      scanned = scanl translateExactPoint start directionList
-      resultPoints = start :| take (length directionList - 1) (drop 1 scanned)
-   in admittedConvexPolygon resultPoints
-
-convexHullPolygon
-  :: NonEmpty ExactPoint
-  -> Either MinkowskiError ConvexPolygon
-convexHullPolygon submitted =
-  let points = NonEmpty.toList submitted
-   in case convexHullPoints points of
-    Nothing -> Left (MinkowskiConvexHullDegenerate points)
-    Just hullPoints -> Right (admittedConvexPolygon hullPoints)
-
-reflectConvexPolygon :: ConvexPolygon -> ConvexPolygon
-reflectConvexPolygon =
-  admittedConvexPolygon
-    . fmap negateExactPoint
-    . convexPolygonPoints
-
-convexPolygonCentroid
-  :: ConvexPolygon
-  -> Either MinkowskiError ExactPoint
-convexPolygonCentroid polygon = do
-  let points = convexPolygonPoints polygon
-      count = fromIntegral (NonEmpty.length points)
-      (sumX, sumY) =
-        List.foldl'
-          (\(accumulatedX, accumulatedY) point ->
-             let (x, y) = exactPointCoordinates point
-              in (accumulatedX + x, accumulatedY + y))
-          (0, 0)
-          points
-  x <- first MinkowskiExactArithmetic (exactDivide sumX count)
-  y <- first MinkowskiExactArithmetic (exactDivide sumY count)
-  pure (exactPoint x y)
-
--- | Erode one convex polygon by another through the strongest translated
--- support half-plane for each source edge. The canonical sorted exact descent
--- retains those source lines; a lower-dimensional residual is represented by
--- the empty polygonal region at this two-dimensional publication boundary.
-erodeConvexBy
-  :: ConvexPolygon
-  -> ConvexPolygon
-  -> Either MinkowskiError (Maybe ConvexPolygon, ExactClipReceipt)
-erodeConvexBy source kernel = do
-  let sourcePoints = convexPolygonPoints source
-      kernelPoints = convexPolygonPoints kernel
-      firstKernel = NonEmpty.head kernelPoints
-      initial =
-        fmap
-          (`subtractExactPoints` firstKernel)
-          sourcePoints
-  retained <- first MinkowskiInvalidHalfPlane (exactRetainedPolygon initial)
-  halfPlanes <-
-    traverse
-      (uncurry (strongestHalfPlane kernelPoints))
-      (cyclePairs sourcePoints)
-  (disposition, clipReceipt) <-
-    first MinkowskiClipFailed (exactClipRetainedPolygon retained halfPlanes)
-  let eroded =
-        case disposition of
-          ExactClipFullDimensional clipped ->
-            Just (convexPolygonFromRetained clipped)
-          ExactClipLowerDimensional _ -> Nothing
-          ExactClipEmpty -> Nothing
-  pure (eroded, clipReceipt)
-
-edgeDirections :: NonEmpty ExactPoint -> NonEmpty ExactVector
-edgeDirections = fmap (uncurry exactVectorFromPoints) . cyclePairsNonEmpty
-
-mergeDirections
-  :: NonEmpty ExactVector
-  -> NonEmpty ExactVector
-  -> NonEmpty ExactVector
-mergeDirections (left :| leftTail) (right :| rightTail) =
-  case compareExactVectorAngle left right of
-    LT -> left :| mergeRemaining leftTail (right : rightTail)
-    GT -> right :| mergeRemaining (left : leftTail) rightTail
-    EQ -> addExactVectors left right :| mergeRemaining leftTail rightTail
-
-mergeRemaining :: [ExactVector] -> [ExactVector] -> [ExactVector]
-mergeRemaining [] right = right
-mergeRemaining left [] = left
-mergeRemaining left@(leftHead : leftTail) right@(rightHead : rightTail) =
-  case compareExactVectorAngle leftHead rightHead of
-    LT -> leftHead : mergeRemaining leftTail right
-    GT -> rightHead : mergeRemaining left rightTail
-    EQ -> addExactVectors leftHead rightHead : mergeRemaining leftTail rightTail
-
-pointSweepKey :: ExactPoint -> (ExactRational, ExactRational)
-pointSweepKey point =
-  let (x, y) = exactPointCoordinates point
-   in (y, x)
-
-convexHullPoints :: [ExactPoint] -> Maybe (NonEmpty ExactPoint)
-convexHullPoints submitted =
-  case Set.toAscList (Set.fromList submitted) of
-    firstPoint : secondPoint : thirdPoint : remaining ->
-      let ordered = firstPoint : secondPoint : thirdPoint : remaining
-          lower = dropFinal (reverse (List.foldl' hullStep [] ordered))
-          upper = dropFinal (reverse (List.foldl' hullStep [] (reverse ordered)))
-       in case lower <> upper of
-            firstHullPoint : secondHullPoint : thirdHullPoint : hullTail ->
-              Just (firstHullPoint :| (secondHullPoint : thirdHullPoint : hullTail))
-            _ -> Nothing
-    _ -> Nothing
-
-hullStep :: [ExactPoint] -> ExactPoint -> [ExactPoint]
-hullStep (current : previous : remaining) candidate
-  | exactOrient2d previous current candidate /= GT =
-      hullStep (previous : remaining) candidate
-hullStep hull candidate = candidate : hull
-
-dropFinal :: [value] -> [value]
-dropFinal values =
-  case reverse values of
-    _ : remaining -> reverse remaining
-    [] -> []
-
-strongestHalfPlane
-  :: NonEmpty ExactPoint
-  -> ExactPoint
-  -> ExactPoint
-  -> Either MinkowskiError ExactClosedHalfPlane
-strongestHalfPlane kernelPoints from to =
-  let direction = exactVectorFromPoints from to
-      supportPoint =
-        case kernelPoints of
-          initial :| remaining ->
-            List.foldl'
-              (\selected candidate ->
-                 if directionPointCross direction candidate
-                      < directionPointCross direction selected
-                   then candidate
-                   else selected)
-              initial
-              remaining
-   in first MinkowskiInvalidHalfPlane
-        ( exactClosedHalfPlaneFromDirectedEdge
-            (subtractExactPoints from supportPoint)
-            (subtractExactPoints to supportPoint)
-        )
-
-directionPointCross :: ExactVector -> ExactPoint -> ExactRational
-directionPointCross (ExactVector directionX directionY) point =
-  let (x, y) = exactPointCoordinates point
-   in directionX * y - directionY * x
-
-addExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
-addExactPoints left right =
-  let (leftX, leftY) = exactPointCoordinates left
-      (rightX, rightY) = exactPointCoordinates right
-   in exactPoint (leftX + rightX) (leftY + rightY)
-
-subtractExactPoints :: ExactPoint -> ExactPoint -> ExactPoint
-subtractExactPoints left right =
-  let (leftX, leftY) = exactPointCoordinates left
-      (rightX, rightY) = exactPointCoordinates right
-   in exactPoint (leftX - rightX) (leftY - rightY)
-
-negateExactPoint :: ExactPoint -> ExactPoint
-negateExactPoint point =
-  let (x, y) = exactPointCoordinates point
-   in exactPoint (negate x) (negate y)
diff --git a/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs b/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/Minkowski/Types.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Closed vocabulary and invariant carriers for exact polygonal morphology.
-module Moonlight.Triangulation.Internal.Minkowski.Types
-  ( ConvexPolygon (..)
-  , StructuringElement (..)
-  , MinkowskiOperation (..)
-  , MinkowskiError (..)
-  , MinkowskiReceipt (..)
-  ) where
-
-import Control.DeepSeq (NFData)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactClipError
-  , ExactHalfPlaneError
-  , ExactPoint
-  )
-import Moonlight.Triangulation.Internal.HandleDefs (FaceId)
-import Moonlight.Triangulation.Internal.ExactRational (ExactArithmeticError)
-import Moonlight.Triangulation.Internal.Overlay.Types
-  ( OverlayCellId
-  , OverlayCellWitness
-  , OverlayError
-  )
-import Moonlight.Triangulation.Internal.Region.Types
-  ( ExactLoop
-  , RegionPublicationError
-  , RegionPointLocation
-  , RegionValidationError
-  )
-
-newtype ConvexPolygon = ConvexPolygon ExactLoop
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-newtype StructuringElement = StructuringElement ConvexPolygon
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiOperation
-  = MinkowskiAddition
-  | MinkowskiErosion
-  | MinkowskiOpening
-  | MinkowskiClosing
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiError
-  = MinkowskiInvalidConvexLoop !RegionValidationError
-  | MinkowskiInvalidHalfPlane !ExactHalfPlaneError
-  | MinkowskiClipFailed !ExactClipError
-  | MinkowskiNonConvexTurn !Int !Ordering
-  | MinkowskiOriginOutside !RegionPointLocation
-  | MinkowskiExactArithmetic !ExactArithmeticError
-  | MinkowskiOverlayFailed !(OverlayError Bool Bool)
-  | MinkowskiPublicationFailed !RegionPublicationError
-  | MinkowskiOverlayCellWitness !OverlayCellWitness
-  | MinkowskiFaceArity !FaceId !Int
-  | MinkowskiCandidateCellMissing !OverlayCellId
-  | MinkowskiInclusionAmbiguous !OverlayCellId !ExactPoint
-  | MinkowskiConvexHullDegenerate ![ExactPoint]
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data MinkowskiReceipt = MinkowskiReceipt
-  { minkowskiOperation :: !MinkowskiOperation
-  , minkowskiInputComponents :: !Int
-  , minkowskiConvexPieces :: !Int
-  , minkowskiGeneratedPieces :: !Int
-  , minkowskiGeneratedConvolutionEdges :: !Int
-  , minkowskiOverlayPasses :: !Int
-  , minkowskiExactCrossings :: !Int
-  , minkowskiOutputCells :: !Int
-  , minkowskiExactCoordinateBitGrowth :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Edit.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Edit.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Edit.hs
+++ /dev/null
@@ -1,1376 +0,0 @@
--- | Pure topology-changing regular edits by exact conflict-cavity descent and gluing.
-module Moonlight.Triangulation.Internal.PowerDiagram.Edit where
-
-import Control.Monad (when)
-import Data.Bifunctor (first)
-import qualified Data.Foldable as Foldable
-import qualified Data.List as List
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Maybe (isJust)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation.Exact
-  ( exactOrient2d
-  , exactPoint
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-import Moonlight.Triangulation.Internal.PowerDiagram.Hull (regularGeneratorTopology)
-import Moonlight.Triangulation.Internal.PowerDiagram.Locality
-import Moonlight.Triangulation.Internal.PowerDiagram.Model
-import Moonlight.Triangulation.Internal.PowerDiagram.Section
-
--- | Insert one stable-labelled site. Repeating the identical site is
--- idempotent; reusing its label for different geometry is a typed conflict.
-insertRegularSite
-  :: Ord label
-  => PowerSite label
-  -> RegularTriangulation label
-  -> Either
-      (RegularEditError label)
-      (RegularEditResult label)
-insertRegularSite site triangulation =
-  let label = powerSiteLabel site
-   in case Map.lookup label (storedRegularSites triangulation) of
-        Nothing ->
-          withRegularEditFallback
-            triangulation
-            (Set.singleton label)
-            ( normalizeRegularEdit
-                triangulation
-                (Map.insert label site (storedRegularSites triangulation))
-                (Set.singleton label)
-            )
-            (insertRegularSiteLocally site triangulation)
-        Just resident
-          | resident == site ->
-              Right (unchangedRegularEdit triangulation)
-          | otherwise -> Left (RegularEditSiteConflict label resident site)
-
--- | Remove one labelled site. Absence is an idempotent successful result.
-removeRegularSite
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Either
-      (RegularEditError label)
-      (RegularEditResult label)
-removeRegularSite label triangulation =
-  case Map.lookup label (storedRegularSites triangulation) of
-    Nothing ->
-      Right (unchangedRegularEdit triangulation)
-    Just _ ->
-      case removeTopologyPreservingSite label triangulation of
-        Just (edited, transitions) ->
-          Right
-            ( RegularEditResult
-                edited
-                (Set.singleton label)
-                transitions
-            )
-        Nothing ->
-          withRegularEditFallback
-            triangulation
-            (Set.singleton label)
-            ( normalizeRegularEdit
-                triangulation
-                (Map.delete label (storedRegularSites triangulation))
-                (Set.singleton label)
-            )
-            (removeRegularSiteLocally label triangulation)
-
--- | Replace many weights in one normalization. Every label must already
--- belong to the triangulation; an empty or unchanged patch is idempotent.
-reweightRegularSites
-  :: Ord label
-  => Map label PowerWeight
-  -> RegularTriangulation label
-  -> Either
-      (RegularEditError label)
-      (RegularEditResult label)
-reweightRegularSites requested triangulation =
-  case NonEmpty.nonEmpty unknownLabels of
-    Just unknown -> Left (RegularEditUnknownSites unknown)
-    Nothing
-      | Map.null changedSites ->
-          Right (unchangedRegularEdit triangulation)
-      | otherwise ->
-          case reweightTopologyPreservingSites changedLabels changedSites reweightedSites triangulation of
-            Just (edited, transitions) ->
-              Right
-                ( RegularEditResult
-                    edited
-                    changedLabels
-                    transitions
-                )
-            Nothing ->
-              case commonRegularWeightShift requested sites of
-                Just shift ->
-                  Right
-                    ( translateRegularWeights
-                        shift
-                        reweightedSites
-                        changedLabels
-                        triangulation
-                    )
-                Nothing ->
-                  case Map.elems changedSites of
-                    [revised] ->
-                      withRegularEditFallback
-                        triangulation
-                        changedLabels
-                        normalizeGlobally
-                        (reweightRegularSiteLocally revised triangulation)
-                    _ -> normalizeGlobally
- where
-  sites = storedRegularSites triangulation
-  unknownLabels = Map.keys (Map.difference requested sites)
-  changedSites =
-    Map.mapMaybeWithKey
-      (\label weight ->
-          case Map.lookup label sites of
-            Just site
-              | powerSiteWeight site /= weight ->
-                  Just site{powerSiteWeight = weight}
-            _ -> Nothing
-      )
-      requested
-  reweightedSites = Map.union changedSites sites
-  changedLabels = Map.keysSet changedSites
-  normalizeGlobally =
-    normalizeRegularEdit triangulation reweightedSites changedLabels
-
-withRegularEditFallback
-  :: Ord label
-  => RegularTriangulation label
-  -> Set label
-  -> Either (RegularEditError label) (RegularEditResult label)
-  -> Either obstruction (RegularTriangulation label, Set label)
-  -> Either (RegularEditError label) (RegularEditResult label)
-withRegularEditFallback before changed fallback =
-  either (const fallback) (Right . publishLocalRegularEdit before changed)
-{-# INLINE withRegularEditFallback #-}
-
-publishLocalRegularEdit
-  :: Ord label
-  => RegularTriangulation label
-  -> Set label
-  -> (RegularTriangulation label, Set label)
-  -> RegularEditResult label
-publishLocalRegularEdit before changed (edited, transitionSupport) =
-  RegularEditResult
-    edited
-    changed
-    ( dispositionTransitionsOn
-        transitionSupport
-        (regularDispositionSection before)
-        (regularDispositionSection edited)
-    )
-{-# INLINE publishLocalRegularEdit #-}
-
-insertRegularSiteLocally
-  :: Ord label
-  => PowerSite label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-insertRegularSiteLocally site triangulation = do
-  section <-
-    maybe (Left LocalRegularSectionUnavailable) Right
-      (storedRegularSection triangulation)
-  let (generator, _) = prepareExactPowerGenerator site
-      slope = exactGeneratorSlope generator
-  case Map.lookup slope (sectionSlopeRepresentatives section) of
-    Just representative ->
-      insertCoincidentRegularSite site generator representative section triangulation
-    Nothing ->
-      insertDistinctRegularSite site generator section triangulation
-
-insertCoincidentRegularSite
-  :: Ord label
-  => PowerSite label
-  -> ExactPowerGenerator label
-  -> label
-  -> RegularSection label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-insertCoincidentRegularSite site generator representativeLabel section triangulation = do
-  representative <-
-    requireGeneratorFromMap (sectionGenerators section) representativeLabel
-  let label = powerSiteLabel site
-  case
-    compareCoincidentPriority
-      (exactPowerGeneratorConstant generator, label)
-      (exactPowerGeneratorConstant representative, representativeLabel) of
-    LT ->
-      let coincident = classifyCoincidentGenerator representative generator
-          disposition = coincidentRegularDisposition coincident
-          editedSection =
-            section
-              { sectionCoincidentDispositions =
-                  Map.insert label coincident (sectionCoincidentDispositions section)
-              , sectionRegularDispositions =
-                  Map.insert label disposition (sectionRegularDispositions section)
-              , sectionRegularStars =
-                  Map.insert label emptyRegularSiteStar (sectionRegularStars section)
-              }
-       in Right
-            ( replaceRegularSiteSection
-                (Map.insert label site (storedRegularSites triangulation))
-                editedSection
-                triangulation
-            , Set.singleton label
-            )
-    _ -> Left (LocalRegularSlopeCoincidence representativeLabel)
-
-insertDistinctRegularSite
-  :: Ord label
-  => PowerSite label
-  -> ExactPowerGenerator label
-  -> RegularSection label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-insertDistinctRegularSite site generator section triangulation = do
-  locality <-
-    maybe (Left LocalRegularLocalityUnavailable) Right
-      (sectionRegularLocality section)
-  location <-
-    maybe (Left LocalRegularSlopeLocationFailed) Right
-      (locateRegularSlope (localityFaceSeed locality) section generator)
-  case location of
-    RegularSlopeOutside edgeKey -> Left (LocalRegularSlopeOutside edgeKey)
-    RegularSlopeInside faceKey -> do
-      volume <- regularFaceVolume section faceKey generator
-      case compare volume 0 of
-        LT -> insertHiddenRegularSite site generator faceKey locality section triangulation
-        EQ -> Left (LocalRegularDegenerateFace faceKey)
-        GT -> insertVisibleRegularSite site generator faceKey locality section triangulation
-
-insertHiddenRegularSite
-  :: Ord label
-  => PowerSite label
-  -> ExactPowerGenerator label
-  -> RegularFaceKey label
-  -> RegularLocality label
-  -> RegularSection label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-insertHiddenRegularSite site generator supportFace locality section triangulation =
-  let label = powerSiteLabel site
-      receipt = sectionRegularReceipt section
-      editedLocality = attachRegularHidden label supportFace locality
-      editedSection =
-        section
-          { sectionGenerators = Map.insert label generator (sectionGenerators section)
-          , sectionSlopeRepresentatives =
-              Map.insert (exactGeneratorSlope generator) label (sectionSlopeRepresentatives section)
-          , sectionRegularDispositions =
-              Map.insert label RegularSiteHidden (sectionRegularDispositions section)
-          , sectionRegularStars =
-              Map.insert label emptyRegularSiteStar (sectionRegularStars section)
-          , sectionRegularReceipt =
-              receipt
-                { generatorRegularInputSites = generatorRegularInputSites receipt + 1
-                , generatorRegularHiddenSites = generatorRegularHiddenSites receipt + 1
-                }
-          , sectionRegularLocality = Just editedLocality
-          }
-   in Right
-        ( replaceRegularSiteSection
-            (Map.insert label site (storedRegularSites triangulation))
-            editedSection
-            triangulation
-        , Set.singleton label
-        )
-
-insertVisibleRegularSite
-  :: Ord label
-  => PowerSite label
-  -> ExactPowerGenerator label
-  -> RegularFaceKey label
-  -> RegularLocality label
-  -> RegularSection label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-insertVisibleRegularSite site generator seedFace locality section triangulation = do
-  conflictFaces <- regularConflictCavity section generator seedFace
-  horizon <- regularConflictHorizon section conflictFaces
-  when (Map.null horizon) (Left LocalRegularEmptyHorizon)
-  newFaces <- traverse (regularHorizonFace section generator) (Map.elems horizon)
-  let label = powerSiteLabel site
-      generators = Map.insert label generator (sectionGenerators section)
-  facePatch <-
-    applyRegularFacePatch
-      generators
-      conflictFaces
-      newFaces
-      section
-  certifyRegularFacePatch generators facePatch
-  let changedTopologyLabels = Set.insert label (patchedRegularLabels facePatch)
-      provisionalDispositions =
-        Set.foldl'
-          (publishPatchedRegularDisposition (patchedRegularStars facePatch))
-          (Map.insert label RegularSiteVisible (sectionRegularDispositions section))
-          changedTopologyLabels
-      newlyHidden =
-        Set.filter
-          (becameRegularHidden (sectionRegularDispositions section) provisionalDispositions)
-          changedTopologyLabels
-      hiddenOnConflict =
-        Set.unions
-          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
-          | faceKey <- Set.toAscList conflictFaces
-          ]
-      hiddenToAttach = Set.union newlyHidden hiddenOnConflict
-      receipt = sectionRegularReceipt section
-      provisionalSection =
-        section
-          { sectionGenerators = generators
-          , sectionSlopeRepresentatives =
-              Map.insert (exactGeneratorSlope generator) label (sectionSlopeRepresentatives section)
-          , sectionRegularDispositions = provisionalDispositions
-          , sectionRegularFaces = patchedRegularFaces facePatch
-          , sectionRegularEdges = patchedRegularEdges facePatch
-          , sectionRegularStars = patchedRegularStars facePatch
-          , sectionRegularReceipt =
-              receipt
-                { generatorRegularInputSites = generatorRegularInputSites receipt + 1
-                , generatorRegularVisibleSites =
-                    generatorRegularVisibleSites receipt + 1 - Set.size newlyHidden
-                , generatorRegularHiddenSites =
-                    generatorRegularHiddenSites receipt + Set.size newlyHidden
-                , generatorRegularFaceCount = Map.size (patchedRegularFaces facePatch)
-                , generatorRegularEdgeCount = Map.size (patchedRegularEdges facePatch)
-                }
-          , sectionRegularLocality = Nothing
-          }
-  editedLocality <-
-    patchRegularLocality locality conflictFaces hiddenToAttach provisionalSection
-  let editedSection = provisionalSection{sectionRegularLocality = Just editedLocality}
-  pure
-    ( replaceRegularSiteSection
-        (Map.insert label site (storedRegularSites triangulation))
-        editedSection
-        triangulation
-    , Set.insert label newlyHidden
-    )
-
-
-regularFaceVolume
-  :: Ord label
-  => RegularSection label
-  -> RegularFaceKey label
-  -> ExactPowerGenerator label
-  -> Either (LocalRegularEditObstruction label) ExactRational
-regularFaceVolume section faceKey candidate = do
-  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
-  (firstGenerator, secondGenerator, thirdGenerator) <-
-    regularFaceGenerators (sectionGenerators section) face
-  pure
-    ( exactGeneratorLiftedVolume
-        firstGenerator
-        secondGenerator
-        thirdGenerator
-        candidate
-    )
-
-regularConflictCavity
-  :: Ord label
-  => RegularSection label
-  -> ExactPowerGenerator label
-  -> RegularFaceKey label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Set (RegularFaceKey label))
-regularConflictCavity section candidate seed =
-  let initial =
-        RegularConflictDescent
-          { conflictPendingFaces = Set.singleton seed
-          , conflictVisitedFaces = Set.empty
-          , conflictVisibleFaces = Set.empty
-          , conflictObstruction = Nothing
-          }
-      descended =
-        Map.foldl'
-          (\current _ -> advanceRegularConflict section candidate current)
-          initial
-          (sectionRegularFaces section)
-   in case conflictObstruction descended of
-        Just obstruction -> Left obstruction
-        Nothing
-          | not (Set.null (conflictPendingFaces descended)) ->
-              Left LocalRegularSlopeLocationFailed
-          | Set.null (conflictVisibleFaces descended) ->
-              Left (LocalRegularDegenerateFace seed)
-          | otherwise -> Right (conflictVisibleFaces descended)
-
-advanceRegularConflict
-  :: Ord label
-  => RegularSection label
-  -> ExactPowerGenerator label
-  -> RegularConflictDescent label
-  -> RegularConflictDescent label
-advanceRegularConflict _ _ descent
-  | Set.null (conflictPendingFaces descent)
-      || isJust (conflictObstruction descent) = descent
-advanceRegularConflict section candidate descent =
-  case Set.minView (conflictPendingFaces descent) of
-    Nothing -> descent
-    Just (faceKey, remaining) ->
-      case regularFaceVolume section faceKey candidate of
-        Left obstruction ->
-          descent
-            { conflictPendingFaces = remaining
-            , conflictObstruction = Just obstruction
-            }
-        Right volume ->
-          let visited = Set.insert faceKey (conflictVisitedFaces descent)
-           in case compare volume 0 of
-                LT ->
-                  descent
-                    { conflictPendingFaces = remaining
-                    , conflictVisitedFaces = visited
-                    }
-                EQ ->
-                  descent
-                    { conflictPendingFaces = remaining
-                    , conflictVisitedFaces = visited
-                    , conflictObstruction = Just (LocalRegularDegenerateFace faceKey)
-                    }
-                GT ->
-                  case regularFaceNeighbours section faceKey of
-                    Left obstruction ->
-                      descent
-                        { conflictPendingFaces = remaining
-                        , conflictVisitedFaces = visited
-                        , conflictObstruction = Just obstruction
-                        }
-                    Right neighbours ->
-                      descent
-                        { conflictPendingFaces =
-                            Set.union remaining (Set.difference neighbours visited)
-                        , conflictVisitedFaces = visited
-                        , conflictVisibleFaces =
-                            Set.insert faceKey (conflictVisibleFaces descent)
-                        }
-
-
-regularConflictHorizon
-  :: Ord label
-  => RegularSection label
-  -> Set (RegularFaceKey label)
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Map (RegularEdgeKey label) (label, label))
-regularConflictHorizon section conflictFaces = do
-  boundarySections <-
-    traverse
-      (regularConflictFaceHorizon section conflictFaces)
-      (Set.toAscList conflictFaces)
-  pure (Map.unions boundarySections)
-
-regularConflictFaceHorizon
-  :: Ord label
-  => RegularSection label
-  -> Set (RegularFaceKey label)
-  -> RegularFaceKey label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Map (RegularEdgeKey label) (label, label))
-regularConflictFaceHorizon section conflictFaces faceKey = do
-  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
-  associations <-
-    traverse
-      (classifyConflictEdge section conflictFaces)
-      (regularFaceDirectedEdgeKeys face)
-  pure (Map.fromList (concat associations))
-
-classifyConflictEdge
-  :: Ord label
-  => RegularSection label
-  -> Set (RegularFaceKey label)
-  -> (label, label)
-  -> Either
-      (LocalRegularEditObstruction label)
-      [(RegularEdgeKey label, (label, label))]
-classifyConflictEdge section conflictFaces directed@(fromLabel, toLabel) = do
-  let edgeKey = orderedPair fromLabel toLabel
-  incident <- requireRegularEdgeIncidence section edgeKey
-  if Set.size incident > 2
-    then Left (LocalRegularNonManifoldEdge edgeKey (Set.size incident))
-    else
-      if Set.size (Set.intersection incident conflictFaces) == 1
-        then Right [(edgeKey, directed)]
-        else Right []
-
-regularHorizonFace
-  :: Ord label
-  => RegularSection label
-  -> ExactPowerGenerator label
-  -> (label, label)
-  -> Either (LocalRegularEditObstruction label) (RegularFace label)
-regularHorizonFace section candidate (fromLabel, toLabel) = do
-  fromGenerator <- requireGeneratorFromMap (sectionGenerators section) fromLabel
-  toGenerator <- requireGeneratorFromMap (sectionGenerators section) toLabel
-  let candidateLabel = exactPowerGeneratorLabel candidate
-      faceKey = sortedRegularFaceLabels fromLabel toLabel candidateLabel
-      fromPoint = uncurry exactPoint (exactGeneratorSlope fromGenerator)
-      toPoint = uncurry exactPoint (exactGeneratorSlope toGenerator)
-      candidatePoint = uncurry exactPoint (exactGeneratorSlope candidate)
-  case exactOrient2d fromPoint toPoint candidatePoint of
-    GT -> do
-      dual <-
-        first LocalRegularTopologyObstruction
-          (exactGeneratorFaceDual fromGenerator toGenerator candidate)
-      pure (canonicalRegularFace fromLabel toLabel candidateLabel dual)
-    _ -> Left (LocalRegularDegenerateFace faceKey)
-
-applyRegularFacePatch
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> Set (RegularFaceKey label)
-  -> [RegularFace label]
-  -> RegularSection label
-  -> Either (LocalRegularEditObstruction label) (RegularFacePatch label)
-applyRegularFacePatch generators removedFaceKeys newFaces section = do
-  removedFaces <-
-    traverse
-      (requireFaceFromMap (sectionRegularFaces section))
-      (Set.toAscList removedFaceKeys)
-  let newFaceSection =
-        Map.fromList [(regularFaceKey face, face) | face <- newFaces]
-      faces =
-        Map.union newFaceSection
-          (Map.withoutKeys (sectionRegularFaces section) removedFaceKeys)
-      touchedEdges =
-        Set.unions
-          [ Set.unions (fmap (Set.fromList . regularFaceCanonicalEdgeKeys) removedFaces)
-          , Set.unions (fmap (Set.fromList . regularFaceCanonicalEdgeKeys) newFaces)
-          ]
-      newIncidence =
-        List.foldl'
-          (Map.unionWith Set.union)
-          Map.empty
-          (fmap regularFaceEdgeIncidence newFaces)
-      affectedLabels =
-        Set.unions
-          [ Set.unions (fmap regularFaceLabelSet removedFaces)
-          , Set.unions (fmap regularFaceLabelSet newFaces)
-          ]
-  edges <-
-    Foldable.foldlM
-      (patchRegularEdge generators faces removedFaceKeys newIncidence)
-      (sectionRegularEdges section)
-      touchedEdges
-  let stars =
-        patchRegularStars
-          affectedLabels
-          removedFaceKeys
-          newFaceSection
-          touchedEdges
-          edges
-          section
-  pure
-    RegularFacePatch
-      { patchedRegularFaces = faces
-      , patchedRegularEdges = edges
-      , patchedRegularStars = stars
-      , patchedRegularLabels = affectedLabels
-      , patchedRegularTouchedEdges = touchedEdges
-      }
-
-patchRegularEdge
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> Map (RegularFaceKey label) (RegularFace label)
-  -> Set (RegularFaceKey label)
-  -> Map (RegularEdgeKey label) (Set (RegularFaceKey label))
-  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
-  -> RegularEdgeKey label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Map (RegularEdgeKey label) (RegularEdgeSection label))
-patchRegularEdge generators faces removedFaceKeys newIncidence edges edgeKey =
-  let oldIncidence =
-        maybe Set.empty sectionRegularEdgeFaces (Map.lookup edgeKey edges)
-      incidence =
-        Set.union
-          (Set.difference oldIncidence removedFaceKeys)
-          (Map.findWithDefault Set.empty edgeKey newIncidence)
-   in if Set.null incidence
-        then Right (Map.delete edgeKey edges)
-        else do
-          edgeSection <- publishRegularEdgeSection generators faces edgeKey incidence
-          pure (Map.insert edgeKey edgeSection edges)
-
-publishRegularEdgeSection
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> Map (RegularFaceKey label) (RegularFace label)
-  -> RegularEdgeKey label
-  -> Set (RegularFaceKey label)
-  -> Either (LocalRegularEditObstruction label) (RegularEdgeSection label)
-publishRegularEdgeSection generators faces edgeKey@(firstLabel, secondLabel) incidence =
-  case Set.toAscList incidence of
-    [faceKey] -> do
-      face <- requireFaceFromMap faces faceKey
-      (fromLabel, toLabel, oppositeLabel) <-
-        requireFaceEdgeWitness edgeKey face
-      fromGenerator <- requireGeneratorFromMap generators fromLabel
-      toGenerator <- requireGeneratorFromMap generators toLabel
-      oppositeGenerator <- requireGeneratorFromMap generators oppositeLabel
-      ray <-
-        first LocalRegularTopologyObstruction
-          ( exactGeneratorBoundaryDualRay
-              (regularFaceDualPoint face)
-              fromGenerator
-              toGenerator
-              oppositeGenerator
-          )
-      pure
-        ( RegularEdgeSection
-            (RegularEdge firstLabel secondLabel (UnboundedPowerDual ray))
-            incidence
-        )
-    [firstFaceKey, secondFaceKey] -> do
-      firstFace <- requireFaceFromMap faces firstFaceKey
-      secondFace <- requireFaceFromMap faces secondFaceKey
-      let firstDual = regularFaceDualPoint firstFace
-          secondDual = regularFaceDualPoint secondFace
-      dual <-
-        fmap publishGeneratorDualGeometry
-          ( first LocalRegularTopologyObstruction
-              ( exactGeneratorDualBetween
-                  firstLabel
-                  secondLabel
-                  firstDual
-                  secondDual
-              )
-          )
-      pure
-        ( RegularEdgeSection
-            (RegularEdge firstLabel secondLabel dual)
-            incidence
-        )
-    incidentFaces ->
-      Left (LocalRegularNonManifoldEdge edgeKey (length incidentFaces))
-
-patchRegularStars
-  :: Ord label
-  => Set label
-  -> Set (RegularFaceKey label)
-  -> Map (RegularFaceKey label) (RegularFace label)
-  -> Set (RegularEdgeKey label)
-  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
-  -> RegularSection label
-  -> Map label (RegularSiteStar label)
-patchRegularStars affectedLabels removedFaceKeys newFaceSection touchedEdges edges section =
-  Set.foldl' patchStar (sectionRegularStars section) affectedLabels
- where
-  affectedSupport = Map.fromSet (const ()) affectedLabels
-  touchedStars =
-    regularSiteStars
-      affectedSupport
-      Map.empty
-      ( Map.restrictKeys
-          (Map.union edges (sectionRegularEdges section))
-          touchedEdges
-      )
-  addedStars =
-    regularSiteStars
-      affectedSupport
-      newFaceSection
-      (Map.restrictKeys edges touchedEdges)
-  patchStar stars label =
-    let oldStar = Map.findWithDefault emptyRegularSiteStar label stars
-        touchedStar = Map.findWithDefault emptyRegularSiteStar label touchedStars
-        addedStar = Map.findWithDefault emptyRegularSiteStar label addedStars
-        incidentFaces =
-          Set.union
-            (Set.difference (sectionIncidentFaces oldStar) removedFaceKeys)
-            (sectionIncidentFaces addedStar)
-        retainedNeighbours =
-          Set.difference
-            (sectionSiteNeighbours oldStar)
-            (sectionSiteNeighbours touchedStar)
-     in Map.insert
-          label
-          ( RegularSiteStar
-              incidentFaces
-              (Set.union retainedNeighbours (sectionSiteNeighbours addedStar))
-          )
-          stars
-
-certifyRegularFacePatch
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> RegularFacePatch label
-  -> Either (LocalRegularEditObstruction label) ()
-certifyRegularFacePatch generators patch =
-  Foldable.traverse_
-    (\edgeKey ->
-       case Map.lookup edgeKey (patchedRegularEdges patch) of
-         Nothing -> Right ()
-         Just edgeSection ->
-           certifyRegularEdgeConvexity
-             generators
-             (patchedRegularFaces patch)
-             (edgeKey, edgeSection))
-    (patchedRegularTouchedEdges patch)
-
-certifyRegularEdgeConvexity
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> Map (RegularFaceKey label) (RegularFace label)
-  -> (RegularEdgeKey label, RegularEdgeSection label)
-  -> Either (LocalRegularEditObstruction label) ()
-certifyRegularEdgeConvexity generators faces (edgeKey, edgeSection) =
-  case Set.toAscList (sectionRegularEdgeFaces edgeSection) of
-    [_] -> Right ()
-    [firstFaceKey, secondFaceKey] -> do
-      firstFace <- requireFaceFromMap faces firstFaceKey
-      secondFace <- requireFaceFromMap faces secondFaceKey
-      (_, _, secondOpposite) <- requireFaceEdgeWitness edgeKey secondFace
-      secondOppositeGenerator <- requireGeneratorFromMap generators secondOpposite
-      (firstGenerator, secondGenerator, thirdGenerator) <-
-        regularFaceGenerators generators firstFace
-      if exactGeneratorLiftedVolume
-          firstGenerator
-          secondGenerator
-          thirdGenerator
-          secondOppositeGenerator
-          < 0
-        then Right ()
-        else Left (LocalRegularNonConvexEdge edgeKey)
-    incident -> Left (LocalRegularNonManifoldEdge edgeKey (length incident))
-
-publishPatchedRegularDisposition
-  :: Ord label
-  => Map label (RegularSiteStar label)
-  -> Map label (RegularSiteDisposition label)
-  -> label
-  -> Map label (RegularSiteDisposition label)
-publishPatchedRegularDisposition stars dispositions label =
-  case Map.lookup label dispositions of
-    Just RegularSiteVisible
-      | maybe True (Set.null . sectionIncidentFaces) (Map.lookup label stars) ->
-          Map.insert label RegularSiteHidden dispositions
-    _ -> dispositions
-
-becameRegularHidden
-  :: Ord label
-  => Map label (RegularSiteDisposition label)
-  -> Map label (RegularSiteDisposition label)
-  -> label
-  -> Bool
-becameRegularHidden before after label =
-  Map.lookup label before == Just RegularSiteVisible
-    && Map.lookup label after == Just RegularSiteHidden
-
-requireRegularEdgeIncidence
-  :: Ord label
-  => RegularSection label
-  -> RegularEdgeKey label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Set (RegularFaceKey label))
-requireRegularEdgeIncidence section edgeKey =
-  maybe
-    (Left (LocalRegularNonManifoldEdge edgeKey 0))
-    (Right . sectionRegularEdgeFaces)
-    (Map.lookup edgeKey (sectionRegularEdges section))
-
-regularFaceGenerators
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> RegularFace label
-  -> Either
-      (LocalRegularEditObstruction label)
-      ( ExactPowerGenerator label
-      , ExactPowerGenerator label
-      , ExactPowerGenerator label
-      )
-regularFaceGenerators generators face = do
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-  (,,)
-    <$> requireGeneratorFromMap generators firstLabel
-    <*> requireGeneratorFromMap generators secondLabel
-    <*> requireGeneratorFromMap generators thirdLabel
-
-removeRegularSiteLocally
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-removeRegularSiteLocally label triangulation = do
-  section <-
-    maybe (Left LocalRegularSectionUnavailable) Right
-      (storedRegularSection triangulation)
-  locality <-
-    maybe (Left LocalRegularLocalityUnavailable) Right
-      (sectionRegularLocality section)
-  disposition <-
-    maybe (Left (LocalRegularGeneratorMissing label)) Right
-      (Map.lookup label (sectionRegularDispositions section))
-  when
-    (disposition /= RegularSiteVisible || hasCoincidentAliases label section)
-    (Left (LocalRegularBoundaryRemoval label))
-  star <-
-    maybe (Left (LocalRegularGeneratorMissing label)) Right
-      (Map.lookup label (sectionRegularStars section))
-  let removedFaces = sectionIncidentFaces star
-  when (Set.null removedFaces) (Left (LocalRegularBoundaryRemoval label))
-  Foldable.traverse_
-    (requireInteriorRemovalEdge section label)
-    (sectionSiteNeighbours star)
-  horizon <- regularConflictHorizon section removedFaces
-  when (Map.null horizon) (Left LocalRegularEmptyHorizon)
-  let hiddenCandidates =
-        Set.unions
-          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
-          | faceKey <- Set.toAscList removedFaces
-          ]
-      horizonLabels =
-        Set.fromList
-          [ endpoint
-          | (firstLabel, secondLabel) <- Map.keys horizon
-          , endpoint <- [firstLabel, secondLabel]
-          ]
-      candidateLabels = Set.union horizonLabels hiddenCandidates
-      generators = Map.delete label (sectionGenerators section)
-  candidateGenerators <-
-    traverse
-      (requireGeneratorFromMap generators)
-      (Set.toAscList candidateLabels)
-  nonEmptyCandidates <-
-    maybe (Left LocalRegularCandidateSectionEmpty) Right
-      (NonEmpty.nonEmpty candidateGenerators)
-  candidateTopology <-
-    first LocalRegularTopologyObstruction
-      (regularGeneratorTopology (DistinctSlopeGenerators nonEmptyCandidates))
-  when
-    ( any
-        ((== RegularGeneratorLowerDimensional) . snd)
-        (NonEmpty.toList (generatorRegularDispositions candidateTopology))
-    )
-    (Left LocalRegularCavityBoundaryMismatch)
-  removedFaceValues <-
-    traverse
-      (requireFaceFromMap (sectionRegularFaces section))
-      (Set.toAscList removedFaces)
-  let proposedFaces = fmap publishGeneratorFace (generatorRegularFaces candidateTopology)
-      newFaces =
-        filter
-          (regularFaceCentroidInside generators removedFaceValues)
-          proposedFaces
-      proposedBoundary = regularFaceSectionBoundary newFaces
-  when
-    (proposedBoundary /= Map.keysSet horizon)
-    (Left LocalRegularCavityBoundaryMismatch)
-  facePatch <- applyRegularFacePatch generators removedFaces newFaces section
-  certifyRegularFacePatch generators facePatch
-  let candidateDispositions =
-        Set.foldl'
-          (publishRemovalCandidateDisposition (patchedRegularStars facePatch))
-          (Map.delete label (sectionRegularDispositions section))
-          candidateLabels
-      exposed =
-        Set.filter
-          (becameRegularVisible (sectionRegularDispositions section) candidateDispositions)
-          candidateLabels
-      stillHidden =
-        Set.filter
-          (\candidate -> Map.lookup candidate candidateDispositions == Just RegularSiteHidden)
-          hiddenCandidates
-      receipt = sectionRegularReceipt section
-      provisionalSection =
-        section
-          { sectionGenerators = generators
-          , sectionSlopeRepresentatives =
-              Map.filter (/= label) (sectionSlopeRepresentatives section)
-          , sectionRegularDispositions = candidateDispositions
-          , sectionRegularFaces = patchedRegularFaces facePatch
-          , sectionRegularEdges = patchedRegularEdges facePatch
-          , sectionRegularStars = Map.delete label (patchedRegularStars facePatch)
-          , sectionRegularReceipt =
-              receipt
-                { generatorRegularInputSites = generatorRegularInputSites receipt - 1
-                , generatorRegularVisibleSites =
-                    generatorRegularVisibleSites receipt - 1 + Set.size exposed
-                , generatorRegularHiddenSites =
-                    generatorRegularHiddenSites receipt - Set.size exposed
-                , generatorRegularFaceCount = Map.size (patchedRegularFaces facePatch)
-                , generatorRegularEdgeCount = Map.size (patchedRegularEdges facePatch)
-                }
-          , sectionRegularLocality = Nothing
-          }
-  editedLocality <-
-    patchRegularLocality locality removedFaces stillHidden provisionalSection
-  let editedSection = provisionalSection{sectionRegularLocality = Just editedLocality}
-  pure
-    ( replaceRegularSiteSection
-        (Map.delete label (storedRegularSites triangulation))
-        editedSection
-        triangulation
-    , Set.insert label exposed
-    )
-
-requireInteriorRemovalEdge
-  :: Ord label
-  => RegularSection label
-  -> label
-  -> label
-  -> Either (LocalRegularEditObstruction label) ()
-requireInteriorRemovalEdge section label neighbour = do
-  let edgeKey = orderedPair label neighbour
-  incidence <- requireRegularEdgeIncidence section edgeKey
-  if Set.size incidence == 2
-    then Right ()
-    else Left (LocalRegularBoundaryRemoval label)
-
-regularFaceCentroidInside
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> [RegularFace label]
-  -> RegularFace label
-  -> Bool
-regularFaceCentroidInside generators domainFaces candidate =
-  case regularFaceSlopeSum generators candidate of
-    Left _ -> False
-    Right slopeSum -> any (regularSlopeSumInsideFace generators slopeSum) domainFaces
-
-regularFaceSlopeSum
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> RegularFace label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (ExactRational, ExactRational)
-regularFaceSlopeSum generators face = do
-  (firstGenerator, secondGenerator, thirdGenerator) <-
-    regularFaceGenerators generators face
-  let (firstX, firstY) = exactGeneratorSlope firstGenerator
-      (secondX, secondY) = exactGeneratorSlope secondGenerator
-      (thirdX, thirdY) = exactGeneratorSlope thirdGenerator
-  pure (firstX + secondX + thirdX, firstY + secondY + thirdY)
-
-regularSlopeSumInsideFace
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> (ExactRational, ExactRational)
-  -> RegularFace label
-  -> Bool
-regularSlopeSumInsideFace generators slopeSum =
-  not
-    . any (generatorEdgeExcludesScaled 3 slopeSum generators)
-    . regularFaceDirectedEdgeKeys
-
-regularFaceSectionBoundary
-  :: Ord label
-  => [RegularFace label]
-  -> Set (RegularEdgeKey label)
-regularFaceSectionBoundary =
-  Map.keysSet
-    . Map.filter ((== 1) . Set.size)
-    . List.foldl'
-        (Map.unionWith Set.union)
-        Map.empty
-    . fmap regularFaceEdgeIncidence
-
-publishRemovalCandidateDisposition
-  :: Ord label
-  => Map label (RegularSiteStar label)
-  -> Map label (RegularSiteDisposition label)
-  -> label
-  -> Map label (RegularSiteDisposition label)
-publishRemovalCandidateDisposition stars dispositions label =
-  Map.insert
-    label
-    ( if maybe True (Set.null . sectionIncidentFaces) (Map.lookup label stars)
-        then RegularSiteHidden
-        else RegularSiteVisible
-    )
-    dispositions
-
-becameRegularVisible
-  :: Ord label
-  => Map label (RegularSiteDisposition label)
-  -> Map label (RegularSiteDisposition label)
-  -> label
-  -> Bool
-becameRegularVisible before after label =
-  Map.lookup label before == Just RegularSiteHidden
-    && Map.lookup label after == Just RegularSiteVisible
-
-reweightRegularSiteLocally
-  :: Ord label
-  => PowerSite label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-reweightRegularSiteLocally revised triangulation = do
-  let label = powerSiteLabel revised
-  (removed, removalSupport) <- removeAnyRegularSiteLocally label triangulation
-  (inserted, insertionSupport) <- insertRegularSiteLocally revised removed
-  pure (inserted, Set.union removalSupport insertionSupport)
-
-commonRegularWeightShift
-  :: Ord label
-  => Map label PowerWeight
-  -> Map label (PowerSite label)
-  -> Maybe ExactRational
-commonRegularWeightShift requested sites
-  | Map.size requested /= Map.size sites = Nothing
-  | otherwise = do
-      (firstLabel, firstWeight) <- Map.lookupMin requested
-      firstSite <- Map.lookup firstLabel sites
-      let shift =
-            powerWeightExact firstWeight
-              - powerWeightExact (powerSiteWeight firstSite)
-      if Map.isSubmapOfBy (powerSiteHasWeightShift shift) requested sites
-        then Just shift
-        else Nothing
-
-powerSiteHasWeightShift
-  :: ExactRational
-  -> PowerWeight
-  -> PowerSite label
-  -> Bool
-powerSiteHasWeightShift shift weight site =
-  powerWeightExact weight
-    - powerWeightExact (powerSiteWeight site)
-    == shift
-
-translateRegularWeights
-  :: ExactRational
-  -> Map label (PowerSite label)
-  -> Set label
-  -> RegularTriangulation label
-  -> RegularEditResult label
-translateRegularWeights shift sites changedLabels triangulation =
-  let shiftedSection =
-        fmap
-          (\section ->
-             section
-               { sectionGenerators =
-                   fmap
-                     (translateExactPowerGenerator shift)
-                     (sectionGenerators section)
-               })
-          (storedRegularSection triangulation)
-   in RegularEditResult
-        { regularEditTriangulation =
-            triangulation
-              { storedRegularSites = sites
-              , storedRegularSection = shiftedSection
-              }
-        , regularEditChangedSites = changedLabels
-        , regularEditTransitions = Vector.empty
-        }
-
-translateExactPowerGenerator
-  :: ExactRational
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-translateExactPowerGenerator shift generator =
-  generator
-    { exactPowerGeneratorConstant =
-        exactPowerGeneratorConstant generator + shift
-    }
-
-removeAnyRegularSiteLocally
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (RegularTriangulation label, Set label)
-removeAnyRegularSiteLocally label triangulation =
-  case removeTopologyPreservingSite label triangulation of
-    Just (edited, _) -> Right (edited, Set.singleton label)
-    Nothing -> removeRegularSiteLocally label triangulation
-
--- Exact topology-preserving edits descend through the existing regular
--- section. A failed classification is not an error; it selects the canonical
--- global normalization below.
-
-classifyCoincidentSubordinate
-  :: Ord label
-  => PowerSite label
-  -> PowerSite label
-  -> Maybe (CoincidentGeneratorDisposition label)
-classifyCoincidentSubordinate representative candidate =
-  if powerSiteQueryPoint candidate /= powerSiteQueryPoint representative
-    then Nothing
-    else
-      case
-        compareCoincidentPriority
-          (powerSiteWeight candidate, powerSiteLabel candidate)
-          (powerSiteWeight representative, powerSiteLabel representative) of
-        LT ->
-          Just
-            ( classifyCoincidentValue
-                (powerSiteLabel representative)
-                (powerSiteWeight representative)
-                (powerSiteWeight candidate)
-            )
-        _ -> Nothing
-
-removeTopologyPreservingSite
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Maybe
-      ( RegularTriangulation label
-      , Vector.Vector (RegularSiteTransition label)
-      )
-removeTopologyPreservingSite label triangulation = do
-  section <- storedRegularSection triangulation
-  disposition <- Map.lookup label (sectionRegularDispositions section)
-  editedSection <-
-    case disposition of
-      RegularSiteCoincidentEquivalentTo _ -> removeCoincident section
-      RegularSiteCoincidentDominatedBy _ -> removeCoincident section
-      RegularSiteHidden
-        | not (hasCoincidentAliases label section) -> do
-            generator <- Map.lookup label (sectionGenerators section)
-            let receipt = sectionRegularReceipt section
-            pure
-              section
-                { sectionGenerators = Map.delete label (sectionGenerators section)
-                , sectionSlopeRepresentatives =
-                    Map.delete (exactGeneratorSlope generator) (sectionSlopeRepresentatives section)
-                , sectionRegularDispositions =
-                    Map.delete label (sectionRegularDispositions section)
-                , sectionRegularStars = Map.delete label (sectionRegularStars section)
-                , sectionRegularReceipt =
-                    receipt
-                      { generatorRegularInputSites = generatorRegularInputSites receipt - 1
-                      , generatorRegularHiddenSites = generatorRegularHiddenSites receipt - 1
-                      }
-                , sectionRegularLocality = fmap (detachRegularHidden label) (sectionRegularLocality section)
-                }
-      _ -> Nothing
-  let edited =
-        replaceRegularSiteSection
-          (Map.delete label (storedRegularSites triangulation))
-          editedSection
-          triangulation
-  pure
-    ( edited
-    , Vector.singleton (RegularSiteDisappeared label disposition)
-    )
- where
-  removeCoincident section = do
-    _ <- Map.lookup label (sectionCoincidentDispositions section)
-    pure
-      section
-        { sectionCoincidentDispositions =
-            Map.delete label (sectionCoincidentDispositions section)
-        , sectionRegularDispositions =
-            Map.delete label (sectionRegularDispositions section)
-        , sectionRegularStars = Map.delete label (sectionRegularStars section)
-        }
-
-data RegularReweightDescent label = RegularReweightDescent
-  { descendedCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
-  , descendedGenerators :: !(Map label (ExactPowerGenerator label))
-  , descendedRegularDispositions :: !(Map label (RegularSiteDisposition label))
-  , descendedTransitions :: ![RegularSiteTransition label]
-  }
-
-reweightTopologyPreservingSites
-  :: Ord label
-  => Set label
-  -> Map label (PowerSite label)
-  -> Map label (PowerSite label)
-  -> RegularTriangulation label
-  -> Maybe
-      ( RegularTriangulation label
-      , Vector.Vector (RegularSiteTransition label)
-      )
-reweightTopologyPreservingSites changedLabels changedSites reweightedSites triangulation = do
-  section <- storedRegularSection triangulation
-  let coincidentRepresentatives =
-        Map.foldl'
-          (flip (Set.insert . coincidentRepresentativeLabel))
-          Set.empty
-          (sectionCoincidentDispositions section)
-  descent <-
-    Foldable.foldlM
-      ( descendTopologyPreservingReweight
-          changedLabels
-          reweightedSites
-          coincidentRepresentatives
-          triangulation
-      )
-      RegularReweightDescent
-        { descendedCoincidentDispositions = sectionCoincidentDispositions section
-        , descendedGenerators = sectionGenerators section
-        , descendedRegularDispositions = sectionRegularDispositions section
-        , descendedTransitions = []
-        }
-      changedSites
-  let editedSection =
-        section
-          { sectionCoincidentDispositions = descendedCoincidentDispositions descent
-          , sectionGenerators = descendedGenerators descent
-          , sectionRegularDispositions = descendedRegularDispositions descent
-          }
-      edited =
-        replaceRegularSiteSection
-          reweightedSites
-          editedSection
-          triangulation
-  pure (edited, Vector.fromList (reverse (descendedTransitions descent)))
-
-descendTopologyPreservingReweight
-  :: Ord label
-  => Set label
-  -> Map label (PowerSite label)
-  -> Set label
-  -> RegularTriangulation label
-  -> RegularReweightDescent label
-  -> PowerSite label
-  -> Maybe (RegularReweightDescent label)
-descendTopologyPreservingReweight changedLabels reweightedSites coincidentRepresentatives triangulation descent revised = do
-  resident <- Map.lookup label (storedRegularSites triangulation)
-  disposition <-
-    storedRegularSection triangulation
-      >>= Map.lookup label . sectionRegularDispositions
-  case regularCoincidentRepresentative disposition of
-    Just representative
-      | Set.notMember representative changedLabels -> do
-          representativeSite <- Map.lookup representative reweightedSites
-          coincident <- classifyCoincidentSubordinate representativeSite revised
-          let after = coincidentRegularDisposition coincident
-          pure
-            descent
-              { descendedCoincidentDispositions =
-                  Map.insert label coincident (descendedCoincidentDispositions descent)
-              , descendedRegularDispositions =
-                  Map.insert label after (descendedRegularDispositions descent)
-              , descendedTransitions =
-                  if disposition == after
-                    then descendedTransitions descent
-                    else RegularSiteTransitioned label disposition after : descendedTransitions descent
-              }
-    _
-      | disposition == RegularSiteHidden
-          && powerSiteWeight revised < powerSiteWeight resident
-          && Set.notMember label coincidentRepresentatives ->
-          let (generator, _) = prepareExactPowerGenerator revised
-           in Just
-                descent
-                  { descendedGenerators =
-                      Map.insert label generator (descendedGenerators descent)
-                  }
-      | otherwise -> Nothing
- where
-  label = powerSiteLabel revised
-
-hasCoincidentAliases
-  :: Eq label
-  => label
-  -> RegularSection label
-  -> Bool
-hasCoincidentAliases label =
-  any ((== label) . coincidentRepresentativeLabel)
-    . Map.elems
-    . sectionCoincidentDispositions
-
-regularCoincidentRepresentative :: RegularSiteDisposition label -> Maybe label
-regularCoincidentRepresentative disposition =
-  case disposition of
-    RegularSiteCoincidentEquivalentTo label -> Just label
-    RegularSiteCoincidentDominatedBy label -> Just label
-    _ -> Nothing
-
-coincidentRepresentativeLabel :: CoincidentGeneratorDisposition label -> label
-coincidentRepresentativeLabel disposition =
-  case disposition of
-    CoincidentGeneratorEquivalentTo label -> label
-    CoincidentGeneratorDominatedBy label -> label
-
-normalizeRegularEdit
-  :: Ord label
-  => RegularTriangulation label
-  -> Map label (PowerSite label)
-  -> Set label
-  -> Either
-      (RegularEditError label)
-      (RegularEditResult label)
-normalizeRegularEdit previous sites changedSites = do
-  triangulation <-
-    first RegularEditTopologyFailed
-      (constructRegularTriangulation sites)
-  pure
-    ( RegularEditResult
-        triangulation
-        changedSites
-        ( dispositionTransitions
-            (regularDispositionSection previous)
-            (regularDispositionSection triangulation)
-        )
-    )
-
-unchangedRegularEdit
-  :: RegularTriangulation label
-  -> RegularEditResult label
-unchangedRegularEdit triangulation =
-  RegularEditResult triangulation Set.empty Vector.empty
-
-
-dispositionTransitions
-  :: Ord label
-  => Map label (RegularSiteDisposition label)
-  -> Map label (RegularSiteDisposition label)
-  -> Vector.Vector (RegularSiteTransition label)
-dispositionTransitions previous current =
-  Vector.fromList
-    ( Map.elems
-        ( Map.mergeWithKey
-            changedDispositionTransition
-            (Map.mapWithKey RegularSiteDisappeared)
-            (Map.mapWithKey RegularSiteAppeared)
-            previous
-            current
-        )
-    )
-
-dispositionTransitionsOn
-  :: Ord label
-  => Set label
-  -> Map label (RegularSiteDisposition label)
-  -> Map label (RegularSiteDisposition label)
-  -> Vector.Vector (RegularSiteTransition label)
-dispositionTransitionsOn support previous current =
-  dispositionTransitions
-    (Map.restrictKeys previous support)
-    (Map.restrictKeys current support)
-
-changedDispositionTransition
-  :: Eq label
-  => label
-  -> RegularSiteDisposition label
-  -> RegularSiteDisposition label
-  -> Maybe (RegularSiteTransition label)
-changedDispositionTransition label before after
-  | before == after = Nothing
-  | otherwise = Just (RegularSiteTransitioned label before after)
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Generator.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Generator.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Generator.hs
+++ /dev/null
@@ -1,264 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact affine generators and their dual geometry. This module owns the
--- algebra shared by batch hull construction, local edits, and bounded
--- projection; it does not choose a construction schedule.
-module Moonlight.Triangulation.Internal.PowerDiagram.Generator
-  ( ExactPowerGenerator (..)
-  , DistinctSlopeGenerators (..)
-  , RegularGeneratorDisposition (..)
-  , GeneratorRegularFace (..)
-  , GeneratorDualGeometry (..)
-  , GeneratorRegularEdge (..)
-  , GeneratorRegularTopology (..)
-  , GeneratorRegularReceipt (..)
-  , RegularTopologyError (..)
-  , exactGeneratorAxis
-  , exactGeneratorLiftedVolume
-  , exactGeneratorFaceDual
-  , exactGeneratorBoundaryDualRay
-  , exactGeneratorDualBetween
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactAffineLine
-  , ExactGeometryError
-  , ExactHalfPlaneError
-  , ExactIntersectionError
-  , ExactPoint
-  , ExactRay
-  , ExactSegment
-  , ExactVector (..)
-  , exactAffineLine
-  , exactAffineLineCoefficients
-  , exactAffineLineIntersection
-  , exactRay
-  , exactSegment
-  )
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import Moonlight.Triangulation.Minkowski (MinkowskiError)
-data ExactPowerGenerator label = ExactPowerGenerator
-  { exactPowerGeneratorLabel :: !label
-  , exactPowerGeneratorXCoefficient :: !ExactRational
-  , exactPowerGeneratorYCoefficient :: !ExactRational
-  , exactPowerGeneratorConstant :: !ExactRational
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Package-internal witness that coincident affine slopes were resolved once.
-newtype DistinctSlopeGenerators label =
-  DistinctSlopeGenerators (NonEmpty (ExactPowerGenerator label))
-
-data RegularGeneratorDisposition
-  = RegularGeneratorVisible
-  | RegularGeneratorLowerDimensional
-  | RegularGeneratorHidden
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data GeneratorRegularFace label = GeneratorRegularFace
-  { generatorRegularFaceFirst :: !label
-  , generatorRegularFaceSecond :: !label
-  , generatorRegularFaceThird :: !label
-  , generatorRegularFaceDualPoint :: !ExactPoint
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data GeneratorDualGeometry
-  = GeneratorDualSegment !ExactSegment
-  | GeneratorDualRay !ExactRay
-  | GeneratorDualLine !ExactAffineLine
-  | GeneratorDualCollapsed !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data GeneratorRegularEdge label = GeneratorRegularEdge
-  { generatorRegularEdgeFirst :: !(ExactPowerGenerator label)
-  , generatorRegularEdgeSecond :: !(ExactPowerGenerator label)
-  , generatorRegularEdgeDual :: !GeneratorDualGeometry
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data GeneratorRegularTopology label = GeneratorRegularTopology
-  { generatorRegularDispositions :: !(NonEmpty (ExactPowerGenerator label, RegularGeneratorDisposition))
-  , generatorRegularFaces :: ![GeneratorRegularFace label]
-  , generatorRegularEdges :: ![GeneratorRegularEdge label]
-  , generatorRegularReceipt :: !GeneratorRegularReceipt
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data GeneratorRegularReceipt = GeneratorRegularReceipt
-  { generatorRegularInputSites :: !Int
-  , generatorRegularVisibleSites :: !Int
-  , generatorRegularLowerDimensionalSites :: !Int
-  , generatorRegularHiddenSites :: !Int
-  , generatorRegularFaceCount :: !Int
-  , generatorRegularEdgeCount :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact slope-hull, incidence, and dual-construction obstructions.
-data RegularTopologyError label
-  = RegularTopologySlopeHullInvalid !MinkowskiError
-  | RegularTopologySlopeVertexMissing !ExactPoint
-  | RegularTopologyAxisInvalid !label !label !ExactHalfPlaneError
-  | RegularTopologyDualVertexInvalid !label !label !label !ExactIntersectionError
-  | RegularTopologyDualSegmentInvalid !label !label !ExactGeometryError
-  | RegularTopologyDualRayInvalid !label !label !ExactGeometryError
-  | RegularTopologyFlatBoundary !label !label !label
-  | RegularTopologyNonManifoldEdge !label !label !Int
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-exactGeneratorAxis
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either ExactHalfPlaneError ExactAffineLine
-exactGeneratorAxis firstGenerator secondGenerator =
-  exactAffineLine
-    ( exactPowerGeneratorXCoefficient firstGenerator
-        - exactPowerGeneratorXCoefficient secondGenerator
-    )
-    ( exactPowerGeneratorYCoefficient firstGenerator
-        - exactPowerGeneratorYCoefficient secondGenerator
-    )
-    ( exactPowerGeneratorConstant firstGenerator
-        - exactPowerGeneratorConstant secondGenerator
-    )
-
--- | Signed exact lifted volume of four affine generators. For a
--- counter-clockwise slope face, a positive result lies above its supporting
--- plane and therefore belongs to the insertion conflict cavity.
-exactGeneratorLiftedVolume
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactRational
-exactGeneratorLiftedVolume firstGenerator secondGenerator thirdGenerator fourthGenerator =
-  let (firstX, firstY, firstZ) = generatorCoordinates firstGenerator
-      (secondX, secondY, secondZ) = generatorCoordinates secondGenerator
-      (thirdX, thirdY, thirdZ) = generatorCoordinates thirdGenerator
-      (fourthX, fourthY, fourthZ) = generatorCoordinates fourthGenerator
-      secondX' = secondX - firstX
-      secondY' = secondY - firstY
-      secondZ' = secondZ - firstZ
-      thirdX' = thirdX - firstX
-      thirdY' = thirdY - firstY
-      thirdZ' = thirdZ - firstZ
-      fourthX' = fourthX - firstX
-      fourthY' = fourthY - firstY
-      fourthZ' = fourthZ - firstZ
-   in secondX' * (thirdY' * fourthZ' - thirdZ' * fourthY')
-        - secondY' * (thirdX' * fourthZ' - thirdZ' * fourthX')
-        + secondZ' * (thirdX' * fourthY' - thirdY' * fourthX')
-
--- | Exact dual vertex shared by three affine generators.
-exactGeneratorFaceDual
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either (RegularTopologyError label) ExactPoint
-exactGeneratorFaceDual firstGenerator secondGenerator thirdGenerator = do
-  firstAxis <-
-    first
-      (RegularTopologyAxisInvalid (exactPowerGeneratorLabel firstGenerator) (exactPowerGeneratorLabel secondGenerator))
-      (exactGeneratorAxis firstGenerator secondGenerator)
-  secondAxis <-
-    first
-      (RegularTopologyAxisInvalid (exactPowerGeneratorLabel firstGenerator) (exactPowerGeneratorLabel thirdGenerator))
-      (exactGeneratorAxis firstGenerator thirdGenerator)
-  first
-    ( RegularTopologyDualVertexInvalid
-        (exactPowerGeneratorLabel firstGenerator)
-        (exactPowerGeneratorLabel secondGenerator)
-        (exactPowerGeneratorLabel thirdGenerator)
-    )
-    (exactAffineLineIntersection firstAxis secondAxis)
-
--- | Exact outward dual ray of an oriented boundary edge and its incident
--- counter-clockwise face.
-exactGeneratorBoundaryDualRay
-  :: ExactPoint
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either (RegularTopologyError label) ExactRay
-exactGeneratorBoundaryDualRay originPoint firstGenerator secondGenerator thirdGenerator = do
-  axis <-
-    first
-      ( RegularTopologyAxisInvalid
-          (exactPowerGeneratorLabel firstGenerator)
-          (exactPowerGeneratorLabel secondGenerator)
-      )
-      (exactGeneratorAxis firstGenerator secondGenerator)
-  let (axisX, axisY, _) = exactAffineLineCoefficients axis
-      candidate = ExactVector axisY (negate axisX)
-      advantageX =
-        exactPowerGeneratorXCoefficient firstGenerator
-          - exactPowerGeneratorXCoefficient thirdGenerator
-      advantageY =
-        exactPowerGeneratorYCoefficient firstGenerator
-          - exactPowerGeneratorYCoefficient thirdGenerator
-      ExactVector candidateX candidateY = candidate
-      derivative = advantageX * candidateX + advantageY * candidateY
-      direction =
-        if derivative > 0
-          then candidate
-          else ExactVector (negate candidateX) (negate candidateY)
-  if derivative == 0
-    then
-      Left
-        ( RegularTopologyFlatBoundary
-            (exactPowerGeneratorLabel firstGenerator)
-            (exactPowerGeneratorLabel secondGenerator)
-            (exactPowerGeneratorLabel thirdGenerator)
-        )
-    else
-      first
-        ( RegularTopologyDualRayInvalid
-            (exactPowerGeneratorLabel firstGenerator)
-            (exactPowerGeneratorLabel secondGenerator)
-        )
-        (exactRay originPoint direction)
-
--- | Canonical undirected dual geometry between two incident face vertices.
-exactGeneratorDualBetween
-  :: label
-  -> label
-  -> ExactPoint
-  -> ExactPoint
-  -> Either (RegularTopologyError label) GeneratorDualGeometry
-exactGeneratorDualBetween firstLabel secondLabel firstPoint secondPoint
-  | firstPoint == secondPoint = Right (GeneratorDualCollapsed firstPoint)
-  | otherwise =
-      fmap GeneratorDualSegment
-        ( first
-            (RegularTopologyDualSegmentInvalid firstLabel secondLabel)
-            ( uncurry exactSegment
-                ( if firstPoint <= secondPoint
-                    then (firstPoint, secondPoint)
-                    else (secondPoint, firstPoint)
-                )
-            )
-        )
-
-generatorCoordinates
-  :: ExactPowerGenerator label
-  -> (ExactRational, ExactRational, ExactRational)
-generatorCoordinates generator =
-  ( exactPowerGeneratorXCoefficient generator
-  , exactPowerGeneratorYCoefficient generator
-  , exactPowerGeneratorConstant generator
-  )
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Hull.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Hull.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Hull.hs
+++ /dev/null
@@ -1,925 +0,0 @@
--- | Exact upper-hull descent for affine generators. The public power owner
--- supplies admitted, distinct slopes; this module returns only topology and
--- exact dual geometry.
-module Moonlight.Triangulation.Internal.PowerDiagram.Hull
-  ( regularGeneratorTopology
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.Foldable as Foldable
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.IntSet as IntSet
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Map.Strict (Map)
-import qualified Data.Map.Strict as Map
-import Data.Maybe (mapMaybe)
-import Data.Set (Set)
-import qualified Data.Set as Set
-import Moonlight.Triangulation.Exact
-  ( ExactAffineLine
-  , ExactPoint
-  , ExactRay
-  , exactPoint
-  , exactPointCoordinates
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (consecutivePairs)
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import Moonlight.Triangulation.Internal.Minkowski.Convex (convexHullPolygon)
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-import Moonlight.Triangulation.Minkowski (convexPolygonPoints)
-
--- | Construct exact regular topology from distinct affine slopes. Full-rank
--- input uses one conflict graph; collinear slopes use the one-dimensional
--- upper chain; coplanar lifted input reuses the exact planar hull owner.
-regularGeneratorTopology
-  :: Ord label
-  => DistinctSlopeGenerators label
-  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
-regularGeneratorTopology (DistinctSlopeGenerators generators) =
-  let points = admittedLiftedPoints generators
-   in case points of
-        singleton :| [] ->
-          pure (topologyFromParts points (Set.singleton singleton) Set.empty [] [])
-        firstPoint :| secondPoint : remaining ->
-          case List.find (not . slopeCollinear firstPoint secondPoint) remaining of
-            Nothing -> collinearSlopeTopology points firstPoint secondPoint
-            Just thirdPoint ->
-              case List.find (not . liftedCoplanar firstPoint secondPoint thirdPoint) remaining of
-                Nothing -> coplanarLiftedTopology points
-                Just fourthPoint -> fullRankTopology points firstPoint secondPoint thirdPoint fourthPoint
-
-data LiftedPoint label = LiftedPoint
-  { liftedPointId :: !Int
-  , liftedPointGenerator :: !(ExactPowerGenerator label)
-  }
-
-instance Eq (LiftedPoint label) where
-  left == right = liftedPointId left == liftedPointId right
-
-instance Ord (LiftedPoint label) where
-  compare left right = compare (liftedPointId left) (liftedPointId right)
-
-admittedLiftedPoints
-  :: Ord label
-  => NonEmpty (ExactPowerGenerator label)
-  -> NonEmpty (LiftedPoint label)
-admittedLiftedPoints generators =
-  let ordered = NonEmpty.sortWith exactPowerGeneratorLabel generators
-   in NonEmpty.zipWith
-        (\identifier generator -> LiftedPoint identifier generator)
-        (0 :| [1 ..])
-        ordered
-
-liftedCoordinates
-  :: LiftedPoint label
-  -> (ExactRational, ExactRational, ExactRational)
-liftedCoordinates point =
-  let generator = liftedPointGenerator point
-   in ( exactPowerGeneratorXCoefficient generator
-      , exactPowerGeneratorYCoefficient generator
-      , exactPowerGeneratorConstant generator
-      )
-
-slopePoint :: LiftedPoint label -> ExactPoint
-slopePoint point =
-  let (coordinateX, coordinateY, _) = liftedCoordinates point
-   in exactPoint coordinateX coordinateY
-
-slopeCollinear
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> Bool
-slopeCollinear firstPoint secondPoint thirdPoint =
-  slopeOrientation firstPoint secondPoint thirdPoint == 0
-
-slopeOrientation
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> ExactRational
-slopeOrientation firstPoint secondPoint thirdPoint =
-  let (firstX, firstY, _) = liftedCoordinates firstPoint
-      (secondX, secondY, _) = liftedCoordinates secondPoint
-      (thirdX, thirdY, _) = liftedCoordinates thirdPoint
-   in (secondX - firstX) * (thirdY - firstY)
-        - (secondY - firstY) * (thirdX - firstX)
-
-liftedCoplanar
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> Bool
-liftedCoplanar firstPoint secondPoint thirdPoint fourthPoint =
-  liftedVolume firstPoint secondPoint thirdPoint fourthPoint == 0
-
-liftedVolume
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> ExactRational
-liftedVolume firstPoint secondPoint thirdPoint fourthPoint =
-  exactGeneratorLiftedVolume
-    (liftedPointGenerator firstPoint)
-    (liftedPointGenerator secondPoint)
-    (liftedPointGenerator thirdPoint)
-    (liftedPointGenerator fourthPoint)
-
-data HullFace label = HullFace
-  { hullFaceId :: !Int
-  , hullFaceFirst :: !(LiftedPoint label)
-  , hullFaceSecond :: !(LiftedPoint label)
-  , hullFaceThird :: !(LiftedPoint label)
-  , hullFaceOutside :: !(Set (LiftedPoint label))
-  , hullFaceCoplanar :: !(Set (LiftedPoint label))
-  }
-
-type HullEdge = (Int, Int)
-
-data HullState label = HullState
-  { hullFaces :: !(IntMap.IntMap (HullFace label))
-  , hullEdgeFaces :: !(Map HullEdge IntSet.IntSet)
-  , hullConflictFaces :: !IntSet.IntSet
-  , hullNextFaceId :: !Int
-  }
-
-fullRankTopology
-  :: NonEmpty (LiftedPoint label)
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
-fullRankTopology points firstPoint secondPoint thirdPoint fourthPoint = do
-  let interior = liftedInteriorSum firstPoint secondPoint thirdPoint fourthPoint
-      simplex = Set.fromList [firstPoint, secondPoint, thirdPoint, fourthPoint]
-      initialFaces =
-        zipWith
-          (orientedHullFace interior)
-          [0 ..]
-          [ (firstPoint, secondPoint, thirdPoint)
-          , (firstPoint, fourthPoint, secondPoint)
-          , (firstPoint, thirdPoint, fourthPoint)
-          , (secondPoint, fourthPoint, thirdPoint)
-          ]
-      initialState =
-        List.foldl'
-          insertHullFace
-          (HullState IntMap.empty Map.empty IntSet.empty 4)
-          initialFaces
-      seededState =
-        Foldable.foldl'
-          (flip assignPointToHull)
-          initialState
-          (filter (`Set.notMember` simplex) (NonEmpty.toList points))
-      finalState = descendHull interior seededState
-      upperFaces = filter ((> 0) . hullFaceNormalZ) (IntMap.elems (hullFaces finalState))
-  (canonicalFaces, visible, lowerDimensional) <- canonicalUpperFaces upperFaces
-  (faces, edges) <- topologyFromHullFaces canonicalFaces
-  pure
-    (topologyFromParts points visible lowerDimensional faces edges)
-
-liftedInteriorSum
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> (ExactRational, ExactRational, ExactRational)
-liftedInteriorSum firstPoint secondPoint thirdPoint fourthPoint =
-  let (firstX, firstY, firstZ) = liftedCoordinates firstPoint
-      (secondX, secondY, secondZ) = liftedCoordinates secondPoint
-      (thirdX, thirdY, thirdZ) = liftedCoordinates thirdPoint
-      (fourthX, fourthY, fourthZ) = liftedCoordinates fourthPoint
-   in ( firstX + secondX + thirdX + fourthX
-      , firstY + secondY + thirdY + fourthY
-      , firstZ + secondZ + thirdZ + fourthZ
-      )
-
-orientedHullFace
-  :: (ExactRational, ExactRational, ExactRational)
-  -> Int
-  -> (LiftedPoint label, LiftedPoint label, LiftedPoint label)
-  -> HullFace label
-orientedHullFace interior identifier (firstPoint, secondPoint, thirdPoint) =
-  let provisional = HullFace identifier firstPoint secondPoint thirdPoint Set.empty Set.empty
-   in if hullFaceVolumeAtInterior interior provisional < 0
-        then provisional
-        else HullFace identifier firstPoint thirdPoint secondPoint Set.empty Set.empty
-
-hullFaceVolumeAtInterior
-  :: (ExactRational, ExactRational, ExactRational)
-  -> HullFace label
-  -> ExactRational
-hullFaceVolumeAtInterior (sumX, sumY, sumZ) face =
-  let (firstX, firstY, firstZ) = liftedCoordinates (hullFaceFirst face)
-      (secondX, secondY, secondZ) = liftedCoordinates (hullFaceSecond face)
-      (thirdX, thirdY, thirdZ) = liftedCoordinates (hullFaceThird face)
-      secondX' = secondX - firstX
-      secondY' = secondY - firstY
-      secondZ' = secondZ - firstZ
-      thirdX' = thirdX - firstX
-      thirdY' = thirdY - firstY
-      thirdZ' = thirdZ - firstZ
-      interiorX' = sumX - 4 * firstX
-      interiorY' = sumY - 4 * firstY
-      interiorZ' = sumZ - 4 * firstZ
-   in secondX' * (thirdY' * interiorZ' - thirdZ' * interiorY')
-        - secondY' * (thirdX' * interiorZ' - thirdZ' * interiorX')
-        + secondZ' * (thirdX' * interiorY' - thirdY' * interiorX')
-
-liftedVolumeAtFace :: HullFace label -> LiftedPoint label -> ExactRational
-liftedVolumeAtFace face =
-  liftedVolume
-    (hullFaceFirst face)
-    (hullFaceSecond face)
-    (hullFaceThird face)
-
-hullFaceNormalZ :: HullFace label -> ExactRational
-hullFaceNormalZ face =
-  slopeOrientation
-    (hullFaceFirst face)
-    (hullFaceSecond face)
-    (hullFaceThird face)
-
-hullFacePoints :: HullFace label -> [LiftedPoint label]
-hullFacePoints face =
-  [hullFaceFirst face, hullFaceSecond face, hullFaceThird face]
-
-hullFaceDirectedEdges
-  :: HullFace label
-  -> [(LiftedPoint label, LiftedPoint label)]
-hullFaceDirectedEdges face =
-  [ (hullFaceFirst face, hullFaceSecond face)
-  , (hullFaceSecond face, hullFaceThird face)
-  , (hullFaceThird face, hullFaceFirst face)
-  ]
-
-hullEdgeKey :: LiftedPoint label -> LiftedPoint label -> HullEdge
-hullEdgeKey firstPoint secondPoint =
-  let firstId = liftedPointId firstPoint
-      secondId = liftedPointId secondPoint
-   in if firstId <= secondId then (firstId, secondId) else (secondId, firstId)
-
-insertHullFace :: HullState label -> HullFace label -> HullState label
-insertHullFace state face =
-  state
-    { hullFaces = IntMap.insert (hullFaceId face) face (hullFaces state)
-    , hullEdgeFaces =
-        List.foldl'
-          (\incidence (fromPoint, toPoint) ->
-             Map.insertWith
-               IntSet.union
-               (hullEdgeKey fromPoint toPoint)
-               (IntSet.singleton (hullFaceId face))
-               incidence)
-          (hullEdgeFaces state)
-          (hullFaceDirectedEdges face)
-    }
-
-assignPointToHull :: LiftedPoint label -> HullState label -> HullState label
-assignPointToHull point state =
-  attachSelectedPoint point state
-    (selectHullFace point (IntMap.elems (hullFaces state)))
-
-data HullFaceSelection label
-  = HullFaceInterior
-  | HullFaceCoplanar !(HullFace label)
-  | HullFaceVisible !(HullFace label) !ExactRational
-
-selectHullFace
-  :: LiftedPoint label
-  -> [HullFace label]
-  -> HullFaceSelection label
-selectHullFace point = List.foldl' choose HullFaceInterior
- where
-  choose selected face =
-    let volume = liftedVolumeAtFace face point
-     in case compare volume 0 of
-          LT -> selected
-          EQ ->
-            case selected of
-              HullFaceInterior -> HullFaceCoplanar face
-              _ -> selected
-          GT ->
-            case selected of
-              HullFaceVisible current currentVolume
-                | volume < currentVolume
-                    || (volume == currentVolume && hullFaceId current < hullFaceId face) ->
-                    selected
-              _ -> HullFaceVisible face volume
-
-attachSelectedPoint
-  :: LiftedPoint label
-  -> HullState label
-  -> HullFaceSelection label
-  -> HullState label
-attachSelectedPoint point state selection =
-  case selection of
-    HullFaceInterior -> state
-    HullFaceCoplanar face ->
-      state
-        { hullFaces =
-            IntMap.adjust
-              (\selected -> selected{hullFaceCoplanar = Set.insert point (hullFaceCoplanar selected)})
-              (hullFaceId face)
-              (hullFaces state)
-        }
-    HullFaceVisible face _ ->
-      state
-        { hullFaces =
-            IntMap.adjust
-              (\selected -> selected{hullFaceOutside = Set.insert point (hullFaceOutside selected)})
-              (hullFaceId face)
-              (hullFaces state)
-        , hullConflictFaces = IntSet.insert (hullFaceId face) (hullConflictFaces state)
-        }
-
-descendHull
-  :: (ExactRational, ExactRational, ExactRational)
-  -> HullState label
-  -> HullState label
-descendHull interior state =
-  case nextHullExpansion state of
-    Nothing -> state
-    Just (seedFace, apex) ->
-      let visible = visibleHullPatch state apex (IntSet.singleton (hullFaceId seedFace)) [hullFaceId seedFace]
-          visibleFaces =
-            mapMaybe (`IntMap.lookup` hullFaces state) (IntSet.toAscList visible)
-          horizon = hullHorizon state visible visibleFaces
-          candidates =
-            Set.difference
-              ( Set.delete apex
-                  ( Set.unions
-                      [ Set.unions
-                          [ hullFaceOutside face
-                          , hullFaceCoplanar face
-                          , Set.fromList (hullFacePoints face)
-                          ]
-                      | face <- visibleFaces
-                      ]
-                  )
-              )
-              ( Set.fromList
-                  [ point
-                  | (firstPoint, secondPoint) <- Map.elems horizon
-                  , point <- [firstPoint, secondPoint]
-                  ]
-              )
-          withoutVisible = removeHullFaces visibleFaces state
-          (withHorizon, newFaceIds) =
-            Map.foldl'
-              (insertHorizonFace interior apex)
-              (withoutVisible, [])
-              horizon
-          redistributed =
-            Foldable.foldl'
-              (assignPointToFaces newFaceIds)
-              withHorizon
-              candidates
-       in descendHull interior redistributed
-
-nextHullExpansion
-  :: HullState label
-  -> Maybe (HullFace label, LiftedPoint label)
-nextHullExpansion state = do
-  identifier <- IntSet.lookupMin (hullConflictFaces state)
-  face <- IntMap.lookup identifier (hullFaces state)
-  apex <- farthestOutsidePoint face
-  pure (face, apex)
-
-farthestOutsidePoint :: HullFace label -> Maybe (LiftedPoint label)
-farthestOutsidePoint face =
-  case Set.minView (hullFaceOutside face) of
-    Nothing -> Nothing
-    Just (initial, remaining) ->
-      Just
-        ( fst
-            ( Foldable.foldl'
-                (\(selected, selectedVolume) candidate ->
-                   let candidateVolume = liftedVolumeAtFace face candidate
-                    in if candidateVolume > selectedVolume
-                         then (candidate, candidateVolume)
-                         else (selected, selectedVolume))
-                (initial, liftedVolumeAtFace face initial)
-                remaining
-            )
-        )
-
-visibleHullPatch
-  :: HullState label
-  -> LiftedPoint label
-  -> IntSet.IntSet
-  -> [Int]
-  -> IntSet.IntSet
-visibleHullPatch _ _ visited [] = visited
-visibleHullPatch state apex visited (identifier : remaining) =
-  case IntMap.lookup identifier (hullFaces state) of
-    Nothing -> visibleHullPatch state apex visited remaining
-    Just face ->
-      let neighbours =
-            IntSet.unions
-              [ Map.findWithDefault IntSet.empty (hullEdgeKey fromPoint toPoint) (hullEdgeFaces state)
-              | (fromPoint, toPoint) <- hullFaceDirectedEdges face
-              ]
-          newlyVisible =
-            IntSet.filter
-              (\candidateId ->
-                 IntSet.notMember candidateId visited
-                   && maybe
-                     False
-                     ((> 0) . (`liftedVolumeAtFace` apex))
-                     (IntMap.lookup candidateId (hullFaces state)))
-              neighbours
-       in visibleHullPatch
-            state
-            apex
-            (IntSet.union visited newlyVisible)
-            (IntSet.toList newlyVisible <> remaining)
-
-hullHorizon
-  :: HullState label
-  -> IntSet.IntSet
-  -> [HullFace label]
-  -> Map HullEdge (LiftedPoint label, LiftedPoint label)
-hullHorizon state visible visibleFaces =
-  Map.fromList
-    [ (hullEdgeKey fromPoint toPoint, (fromPoint, toPoint))
-    | face <- visibleFaces
-    , (fromPoint, toPoint) <- hullFaceDirectedEdges face
-    , not
-        ( IntSet.null
-            ( IntSet.difference
-                (Map.findWithDefault IntSet.empty (hullEdgeKey fromPoint toPoint) (hullEdgeFaces state))
-                visible
-            )
-        )
-    ]
-
-removeHullFaces :: [HullFace label] -> HullState label -> HullState label
-removeHullFaces removed state = List.foldl' removeHullFace state removed
-
-removeHullFace :: HullState label -> HullFace label -> HullState label
-removeHullFace state face =
-  state
-    { hullFaces = IntMap.delete identifier (hullFaces state)
-    , hullEdgeFaces =
-        List.foldl'
-          (removeHullFaceFromEdge identifier)
-          (hullEdgeFaces state)
-          (hullFaceDirectedEdges face)
-    , hullConflictFaces = IntSet.delete identifier (hullConflictFaces state)
-    }
- where
-  identifier = hullFaceId face
-
-removeHullFaceFromEdge
-  :: Int
-  -> Map HullEdge IntSet.IntSet
-  -> (LiftedPoint label, LiftedPoint label)
-  -> Map HullEdge IntSet.IntSet
-removeHullFaceFromEdge identifier incidence (fromPoint, toPoint) =
-  Map.update
-    (\incident ->
-       let retained = IntSet.delete identifier incident
-        in if IntSet.null retained then Nothing else Just retained)
-    (hullEdgeKey fromPoint toPoint)
-    incidence
-
-insertHorizonFace
-  :: (ExactRational, ExactRational, ExactRational)
-  -> LiftedPoint label
-  -> (HullState label, [Int])
-  -> (LiftedPoint label, LiftedPoint label)
-  -> (HullState label, [Int])
-insertHorizonFace interior apex (state, identifiers) (firstPoint, secondPoint) =
-  let identifier = hullNextFaceId state
-      face = orientedHullFace interior identifier (firstPoint, secondPoint, apex)
-      nextState =
-        (insertHullFace state face)
-          { hullNextFaceId = identifier + 1 }
-   in (nextState, identifier : identifiers)
-
-assignPointToFaces
-  :: [Int]
-  -> HullState label
-  -> LiftedPoint label
-  -> HullState label
-assignPointToFaces identifiers state point =
-  attachSelectedPoint point state
-    (selectHullFace point (mapMaybe (`IntMap.lookup` hullFaces state) identifiers))
-
--- | Glue the triangular implementation facets on each exact supporting plane,
--- then recover only that plane's extreme slope vertices. This removes
--- simplex/horizon diagonals from the visibility decision while retaining a
--- deterministic triangulation for incidence and collapsed duals.
-canonicalUpperFaces
-  :: [HullFace label]
-  -> Either
-      (RegularTopologyError label)
-      ( [(HullFace label, ExactPoint)]
-      , Set (LiftedPoint label)
-      , Set (LiftedPoint label)
-      )
-canonicalUpperFaces faces = do
-  associations <- traverse upperFaceAssociation faces
-  sections <-
-    traverse canonicalUpperFaceGroup
-      ( Map.elems
-          ( List.foldl'
-              (\groups (planeKey, dualFace) ->
-                 Map.insertWith (<>) planeKey (dualFace :| []) groups)
-              Map.empty
-              associations
-          )
-      )
-  let (sectionFaces, visible, lowerDimensional) = mconcat sections
-      canonicalFaces =
-        zipWith
-          (\identifier (face, dualPoint) ->
-             (face{hullFaceId = identifier}, dualPoint))
-          [0 ..]
-          sectionFaces
-  pure (canonicalFaces, visible, lowerDimensional)
-
-canonicalUpperFaceGroup
-  :: NonEmpty (HullFace label, ExactPoint)
-  -> Either
-      (RegularTopologyError label)
-      ( [(HullFace label, ExactPoint)]
-      , Set (LiftedPoint label)
-      , Set (LiftedPoint label)
-      )
-canonicalUpperFaceGroup ((face, dualPoint) :| [])
-  | Set.null (hullFaceCoplanar face) =
-      Right ([(face, dualPoint)], Set.fromList (hullFacePoints face), Set.empty)
-canonicalUpperFaceGroup ((firstFace, dualPoint) :| remainingDualFaces) = do
-  let groupFaces = firstFace : fmap fst remainingDualFaces
-      groupPoints =
-        Set.unions
-          [ Set.union
-              (Set.fromList (hullFacePoints face))
-              (hullFaceCoplanar face)
-          | face <- groupFaces
-          ]
-      initialPoint = hullFaceFirst firstFace
-      points = initialPoint :| Set.toAscList (Set.delete initialPoint groupPoints)
-      pointsBySlope =
-        Map.fromList [(slopePoint point, point) | point <- NonEmpty.toList points]
-  hull <-
-    first RegularTopologySlopeHullInvalid
-      (convexHullPolygon (fmap slopePoint points))
-  visiblePoints <-
-    traverse
-      (\point ->
-         maybe
-           (Left (RegularTopologySlopeVertexMissing point))
-           Right
-           (Map.lookup point pointsBySlope))
-      (NonEmpty.toList (convexPolygonPoints hull))
-  let visible = Set.fromList visiblePoints
-  pure
-    ( fmap (\fanFace -> (fanFace, dualPoint)) (coplanarFanFaces visiblePoints)
-    , visible
-    , Set.difference groupPoints visible
-    )
-
-hullFacePlaneKey
-  :: HullFace label
-  -> ExactPoint
-  -> (ExactRational, ExactRational, ExactRational)
-hullFacePlaneKey face dualPoint =
-  let (dualX, dualY) = exactPointCoordinates dualPoint
-      generator = liftedPointGenerator (hullFaceFirst face)
-      support =
-        exactPowerGeneratorXCoefficient generator * dualX
-          + exactPowerGeneratorYCoefficient generator * dualY
-          + exactPowerGeneratorConstant generator
-   in (dualX, dualY, support)
-
-upperFaceAssociation
-  :: HullFace label
-  -> Either
-      (RegularTopologyError label)
-      ( (ExactRational, ExactRational, ExactRational)
-      , (HullFace label, ExactPoint)
-      )
-upperFaceAssociation face = do
-  (_, dualPoint) <- hullFaceDual face
-  pure (hullFacePlaneKey face dualPoint, (face, dualPoint))
-
-coplanarLiftedTopology
-  :: NonEmpty (LiftedPoint label)
-  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
-coplanarLiftedTopology points = do
-  hull <-
-    first RegularTopologySlopeHullInvalid
-      (convexHullPolygon (fmap slopePoint points))
-  let pointsBySlope = Map.fromList [(slopePoint point, point) | point <- NonEmpty.toList points]
-  hullPoints <-
-    traverse
-      (\point -> maybe (Left (RegularTopologySlopeVertexMissing point)) Right (Map.lookup point pointsBySlope))
-      (NonEmpty.toList (convexPolygonPoints hull))
-  let visible = Set.fromList hullPoints
-      lowerDimensional = Set.difference (Set.fromList (NonEmpty.toList points)) visible
-      hullFaces' = coplanarFanFaces hullPoints
-  dualFaces <- coplanarFanDualFaces hullFaces'
-  (faces, edges) <- topologyFromHullFaces dualFaces
-  pure
-    (topologyFromParts points visible lowerDimensional faces edges)
-
-coplanarFanDualFaces
-  :: [HullFace label]
-  -> Either
-      (RegularTopologyError label)
-      [(HullFace label, ExactPoint)]
-coplanarFanDualFaces [] = Right []
-coplanarFanDualFaces faces@(firstFace : _) = do
-  (_, dualPoint) <- hullFaceDual firstFace
-  pure (fmap (\face -> (face, dualPoint)) faces)
-
-coplanarFanFaces :: [LiftedPoint label] -> [HullFace label]
-coplanarFanFaces (firstPoint : secondPoint : thirdPoint : remaining) =
-  zipWith
-    (\identifier (leftPoint, rightPoint) ->
-       HullFace identifier firstPoint leftPoint rightPoint Set.empty Set.empty)
-    [0 ..]
-    (consecutivePairs (secondPoint : thirdPoint : remaining))
-coplanarFanFaces _ = []
-
-collinearSlopeTopology
-  :: NonEmpty (LiftedPoint label)
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> Either (RegularTopologyError label) (GeneratorRegularTopology label)
-collinearSlopeTopology points firstPoint secondPoint = do
-  let direction = slopeDifference firstPoint secondPoint
-      ordered = List.sortOn (slopeParameter direction) (NonEmpty.toList points)
-      visibleList = upperSlopeChain direction ordered
-      visible = Set.fromList visibleList
-      lowerDimensional =
-        Set.difference
-          (upperChainPointSet direction visibleList ordered)
-          visible
-  edges <- traverse fullLineEdge (consecutivePairs visibleList)
-  pure (topologyFromParts points visible lowerDimensional [] edges)
-
-slopeDifference
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> (ExactRational, ExactRational)
-slopeDifference firstPoint secondPoint =
-  let (firstX, firstY, _) = liftedCoordinates firstPoint
-      (secondX, secondY, _) = liftedCoordinates secondPoint
-   in (secondX - firstX, secondY - firstY)
-
-slopeParameter
-  :: (ExactRational, ExactRational)
-  -> LiftedPoint label
-  -> ExactRational
-slopeParameter (directionX, directionY) point =
-  let (coordinateX, coordinateY, _) = liftedCoordinates point
-   in directionX * coordinateX + directionY * coordinateY
-
-upperSlopeChain
-  :: (ExactRational, ExactRational)
-  -> [LiftedPoint label]
-  -> [LiftedPoint label]
-upperSlopeChain direction = reverse . List.foldl' insertUpper []
- where
-  insertUpper
-    :: [LiftedPoint label]
-    -> LiftedPoint label
-    -> [LiftedPoint label]
-  insertUpper (secondPoint : firstPoint : remaining) candidate
-    | slopeChainTurn direction firstPoint secondPoint candidate >= 0 =
-        insertUpper (firstPoint : remaining) candidate
-  insertUpper retained candidate = candidate : retained
-
-slopeChainTurn
-  :: (ExactRational, ExactRational)
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> ExactRational
-slopeChainTurn direction firstPoint secondPoint thirdPoint =
-  let firstT = slopeParameter direction firstPoint
-      secondT = slopeParameter direction secondPoint
-      thirdT = slopeParameter direction thirdPoint
-      (_, _, firstConstant) = liftedCoordinates firstPoint
-      (_, _, secondConstant) = liftedCoordinates secondPoint
-      (_, _, thirdConstant) = liftedCoordinates thirdPoint
-   in (secondT - firstT) * (thirdConstant - firstConstant)
-        - (secondConstant - firstConstant) * (thirdT - firstT)
-
-upperChainPointSet
-  :: (ExactRational, ExactRational)
-  -> [LiftedPoint label]
-  -> [LiftedPoint label]
-  -> Set (LiftedPoint label)
-upperChainPointSet direction chain =
-  snd
-    . List.foldl'
-        classifyPoint
-        (consecutivePairs chain, Set.empty)
- where
-  classifyPoint
-    :: ([(LiftedPoint label, LiftedPoint label)], Set (LiftedPoint label))
-    -> LiftedPoint label
-    -> ([(LiftedPoint label, LiftedPoint label)], Set (LiftedPoint label))
-  classifyPoint (remainingEdges, members) point =
-    let pointParameter = slopeParameter direction point
-        candidateEdges =
-          List.dropWhile
-            ((< pointParameter) . slopeParameter direction . snd)
-            remainingEdges
-        retainedMembers =
-          case candidateEdges of
-            (firstPoint, secondPoint) : _
-              | slopeParameter direction firstPoint <= pointParameter
-                  && slopeChainTurn direction firstPoint secondPoint point == 0 ->
-                  Set.insert point members
-            _ -> members
-     in (candidateEdges, retainedMembers)
-
-fullLineEdge
-  :: (LiftedPoint label, LiftedPoint label)
-  -> Either (RegularTopologyError label) (GeneratorRegularEdge label)
-fullLineEdge (firstPoint, secondPoint) = do
-  axis <- generatorAxis firstPoint secondPoint
-  pure (regularEdge firstPoint secondPoint (GeneratorDualLine axis))
-
-topologyFromHullFaces
-  :: [(HullFace label, ExactPoint)]
-  -> Either
-      (RegularTopologyError label)
-      ([GeneratorRegularFace label], [GeneratorRegularEdge label])
-topologyFromHullFaces dualFaces = do
-  let dualByFace = IntMap.fromList [(hullFaceId face, dual) | (face, dual) <- dualFaces]
-      edgeIncidence =
-        List.foldl'
-          (\incidence (face, _) ->
-             List.foldl'
-               (\current (firstPoint, secondPoint, thirdPoint) ->
-                  Map.insertWith
-                    (<>)
-                    (hullEdgeKey firstPoint secondPoint)
-                    ((face, firstPoint, secondPoint, thirdPoint) :| [])
-                    current)
-               incidence
-               (faceEdgesWithOpposite face))
-          Map.empty
-          dualFaces
-  edges <- traverse (dualEdge dualByFace) (Map.elems edgeIncidence)
-  pure
-    ( [ GeneratorRegularFace
-          (pointLabel (hullFaceFirst face))
-          (pointLabel (hullFaceSecond face))
-          (pointLabel (hullFaceThird face))
-          dual
-      | (face, dual) <- dualFaces
-      ]
-    , edges
-    )
-
-hullFaceDual
-  :: HullFace label
-  -> Either (RegularTopologyError label) (HullFace label, ExactPoint)
-hullFaceDual face = do
-  let firstPoint = hullFaceFirst face
-      secondPoint = hullFaceSecond face
-      thirdPoint = hullFaceThird face
-  dual <-
-    exactGeneratorFaceDual
-      (liftedPointGenerator firstPoint)
-      (liftedPointGenerator secondPoint)
-      (liftedPointGenerator thirdPoint)
-  pure (face, dual)
-
-faceEdgesWithOpposite
-  :: HullFace label
-  -> [(LiftedPoint label, LiftedPoint label, LiftedPoint label)]
-faceEdgesWithOpposite face =
-  [ (hullFaceFirst face, hullFaceSecond face, hullFaceThird face)
-  , (hullFaceSecond face, hullFaceThird face, hullFaceFirst face)
-  , (hullFaceThird face, hullFaceFirst face, hullFaceSecond face)
-  ]
-
-dualEdge
-  :: IntMap.IntMap ExactPoint
-  -> NonEmpty (HullFace label, LiftedPoint label, LiftedPoint label, LiftedPoint label)
-  -> Either (RegularTopologyError label) (GeneratorRegularEdge label)
-dualEdge dualByFace incidences =
-  case incidences of
-    (face, firstPoint, secondPoint, thirdPoint) :| [] -> do
-      dual <-
-        maybe
-          (Left (RegularTopologyFlatBoundary (pointLabel firstPoint) (pointLabel secondPoint) (pointLabel thirdPoint)))
-          Right
-          (IntMap.lookup (hullFaceId face) dualByFace)
-      ray <- boundaryDualRay dual firstPoint secondPoint thirdPoint
-      pure (regularEdge firstPoint secondPoint (GeneratorDualRay ray))
-    (firstFace, firstPoint, secondPoint, _) :| [(secondFace, _, _, _)] -> do
-        firstDual <- requiredDual firstPoint secondPoint firstFace
-        secondDual <- requiredDual firstPoint secondPoint secondFace
-        dual <-
-          exactGeneratorDualBetween
-            (pointLabel firstPoint)
-            (pointLabel secondPoint)
-            firstDual
-            secondDual
-        pure (regularEdge firstPoint secondPoint dual)
-    (_, firstPoint, secondPoint, _) :| _ ->
-      Left
-        ( RegularTopologyNonManifoldEdge
-            (pointLabel firstPoint)
-            (pointLabel secondPoint)
-            (NonEmpty.length incidences)
-        )
- where
-  requiredDual
-    :: LiftedPoint label
-    -> LiftedPoint label
-    -> HullFace label
-    -> Either (RegularTopologyError label) ExactPoint
-  requiredDual firstPoint secondPoint face =
-    maybe
-      (Left (RegularTopologyFlatBoundary (pointLabel firstPoint) (pointLabel secondPoint) (pointLabel (hullFaceThird face))))
-      Right
-      (IntMap.lookup (hullFaceId face) dualByFace)
-
-boundaryDualRay
-  :: ExactPoint
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> LiftedPoint label
-  -> Either (RegularTopologyError label) ExactRay
-boundaryDualRay originPoint firstPoint secondPoint thirdPoint = do
-  exactGeneratorBoundaryDualRay
-    originPoint
-    (liftedPointGenerator firstPoint)
-    (liftedPointGenerator secondPoint)
-    (liftedPointGenerator thirdPoint)
-
-generatorAxis
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> Either (RegularTopologyError label) ExactAffineLine
-generatorAxis firstPoint secondPoint =
-  first
-    (RegularTopologyAxisInvalid (pointLabel firstPoint) (pointLabel secondPoint))
-    (exactGeneratorAxis (liftedPointGenerator firstPoint) (liftedPointGenerator secondPoint))
-
-regularEdge
-  :: LiftedPoint label
-  -> LiftedPoint label
-  -> GeneratorDualGeometry
-  -> GeneratorRegularEdge label
-regularEdge firstPoint secondPoint dual =
-  if liftedPointId firstPoint <= liftedPointId secondPoint
-    then GeneratorRegularEdge (liftedPointGenerator firstPoint) (liftedPointGenerator secondPoint) dual
-    else GeneratorRegularEdge (liftedPointGenerator secondPoint) (liftedPointGenerator firstPoint) dual
-
-pointLabel :: LiftedPoint label -> label
-pointLabel = exactPowerGeneratorLabel . liftedPointGenerator
-
-topologyFromParts
-  :: NonEmpty (LiftedPoint label)
-  -> Set (LiftedPoint label)
-  -> Set (LiftedPoint label)
-  -> [GeneratorRegularFace label]
-  -> [GeneratorRegularEdge label]
-  -> GeneratorRegularTopology label
-topologyFromParts points visible lowerDimensional faces edges =
-  let dispositions =
-        fmap
-          (\point ->
-             ( liftedPointGenerator point
-             , if Set.member point visible
-                 then RegularGeneratorVisible
-                 else
-                   if Set.member point lowerDimensional
-                     then RegularGeneratorLowerDimensional
-                     else RegularGeneratorHidden
-             ))
-          points
-      visibleCount = Set.size visible
-      lowerCount = Set.size lowerDimensional
-      inputCount = NonEmpty.length points
-   in GeneratorRegularTopology
-        { generatorRegularDispositions = dispositions
-        , generatorRegularFaces = faces
-        , generatorRegularEdges = edges
-        , generatorRegularReceipt =
-            GeneratorRegularReceipt
-              { generatorRegularInputSites = inputCount
-              , generatorRegularVisibleSites = visibleCount
-              , generatorRegularLowerDimensionalSites = lowerCount
-              , generatorRegularHiddenSites = inputCount - visibleCount - lowerCount
-              , generatorRegularFaceCount = length faces
-              , generatorRegularEdgeCount = length edges
-              }
-        }
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Locality.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Locality.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Locality.hs
+++ /dev/null
@@ -1,235 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-
--- | The discardable hidden-support index and exact face walk for one regular section.
-module Moonlight.Triangulation.Internal.PowerDiagram.Locality where
-
-import qualified Data.Foldable as Foldable
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactOrient2d
-  , exactPoint
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
-import Moonlight.Triangulation.Internal.ExactRational (ExactRational)
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-  ( ExactPowerGenerator
-  )
-import Moonlight.Triangulation.Internal.PowerDiagram.Model
-
-patchRegularLocality
-  :: Ord label
-  => RegularLocality label
-  -> Set (RegularFaceKey label)
-  -> Set label
-  -> RegularSection label
-  -> Either (LocalRegularEditObstruction label) (RegularLocality label)
-patchRegularLocality locality removedFaces hiddenToAttach section = do
-  seed <-
-    case Map.lookupMin (sectionRegularFaces section) of
-      Nothing -> Left LocalRegularLocalityUnavailable
-      Just (minimumFace, _) ->
-        Right
-          ( if Map.member (localityFaceSeed locality) (sectionRegularFaces section)
-              then localityFaceSeed locality
-              else minimumFace
-          )
-  let detachedForFaces =
-        Set.unions
-          [ Map.findWithDefault Set.empty faceKey (localitySupportHidden locality)
-          | faceKey <- Set.toAscList removedFaces
-          ]
-      detachedLabels = Set.union detachedForFaces hiddenToAttach
-      detached = Set.foldl' (flip detachRegularHidden) locality detachedLabels
-  attachments <-
-    traverse
-      (locateHiddenSupport seed section)
-      (Set.toAscList hiddenToAttach)
-  pure
-    ( Foldable.foldl'
-        (\current (label, faceKey) -> attachRegularHidden label faceKey current)
-        detached{localityFaceSeed = seed}
-        attachments
-    )
-
-locateHiddenSupport
-  :: Ord label
-  => RegularFaceKey label
-  -> RegularSection label
-  -> label
-  -> Either (LocalRegularEditObstruction label) (label, RegularFaceKey label)
-locateHiddenSupport seed section label = do
-  generator <- requireGeneratorFromMap (sectionGenerators section) label
-  location <-
-    maybe (Left LocalRegularSlopeLocationFailed) Right
-      (locateRegularSlope seed section generator)
-  case location of
-    RegularSlopeInside faceKey -> Right (label, faceKey)
-    RegularSlopeOutside edgeKey -> Left (LocalRegularSlopeOutside edgeKey)
-
-attachRegularHidden
-  :: Ord label
-  => label
-  -> RegularFaceKey label
-  -> RegularLocality label
-  -> RegularLocality label
-attachRegularHidden label faceKey locality =
-  locality
-    { localityHiddenSupport = Map.insert label faceKey (localityHiddenSupport locality)
-    , localitySupportHidden =
-        Map.insertWith Set.union faceKey (Set.singleton label) (localitySupportHidden locality)
-    }
-
-detachRegularHidden
-  :: Ord label
-  => label
-  -> RegularLocality label
-  -> RegularLocality label
-detachRegularHidden label locality =
-  case Map.lookup label (localityHiddenSupport locality) of
-    Nothing -> locality
-    Just faceKey ->
-      locality
-        { localityHiddenSupport = Map.delete label (localityHiddenSupport locality)
-        , localitySupportHidden =
-            Map.update
-              (\labels ->
-                 let retained = Set.delete label labels
-                  in if Set.null retained then Nothing else Just retained)
-              faceKey
-              (localitySupportHidden locality)
-        }
-
-buildRegularLocality
-  :: Ord label
-  => RegularSection label
-  -> Maybe (RegularLocality label)
-buildRegularLocality section = do
-  (seed, _) <- Map.lookupMin (sectionRegularFaces section)
-  Foldable.foldlM
-    (attachHiddenGenerator section)
-    RegularLocality
-      { localityFaceSeed = seed
-      , localityHiddenSupport = Map.empty
-      , localitySupportHidden = Map.empty
-      }
-    [ label
-    | (_, label) <- Map.toAscList (sectionSlopeRepresentatives section)
-    , Map.lookup label (sectionRegularDispositions section)
-        == Just RegularSiteHidden
-    ]
-
-attachHiddenGenerator
-  :: Ord label
-  => RegularSection label
-  -> RegularLocality label
-  -> label
-  -> Maybe (RegularLocality label)
-attachHiddenGenerator section locality label = do
-  generator <- Map.lookup label (sectionGenerators section)
-  location <- locateRegularSlope (localityFaceSeed locality) section generator
-  case location of
-    RegularSlopeInside faceKey ->
-      Just
-        (attachRegularHidden label faceKey locality)
-          { localityFaceSeed = faceKey
-          }
-    RegularSlopeOutside _ -> Nothing
-
-locateRegularSlope
-  :: Ord label
-  => RegularFaceKey label
-  -> RegularSection label
-  -> ExactPowerGenerator label
-  -> Maybe (RegularSlopeLocation label)
-locateRegularSlope seed section generator =
-  descendRegularSlope
-    (exactPoint queryX queryY)
-    section
-    (Map.size (sectionRegularFaces section) + 1)
-    seed
- where
-  (queryX, queryY) = exactGeneratorSlope generator
-
-descendRegularSlope
-  :: Ord label
-  => ExactPoint
-  -> RegularSection label
-  -> Int
-  -> RegularFaceKey label
-  -> Maybe (RegularSlopeLocation label)
-descendRegularSlope query section !remaining faceKey
-  | remaining <= 0 = Nothing
-  | otherwise = do
-      face <- Map.lookup faceKey (sectionRegularFaces section)
-      case firstExteriorFaceEdge query section face of
-        Nothing -> Just (RegularSlopeInside faceKey)
-        Just edgeKey -> do
-          edgeSection <- Map.lookup edgeKey (sectionRegularEdges section)
-          case Set.lookupMin (Set.delete faceKey (sectionRegularEdgeFaces edgeSection)) of
-            Just adjacent ->
-              descendRegularSlope query section (remaining - 1) adjacent
-            Nothing -> Just (RegularSlopeOutside edgeKey)
-
-firstExteriorFaceEdge
-  :: Ord label
-  => ExactPoint
-  -> RegularSection label
-  -> RegularFace label
-  -> Maybe (RegularEdgeKey label)
-firstExteriorFaceEdge query section face =
-  fmap
-    (uncurry orderedPair)
-    ( List.find
-        (faceEdgeExcludes query section)
-        (regularFaceDirectedEdgeKeys face)
-    )
-
-faceEdgeExcludes
-  :: Ord label
-  => ExactPoint
-  -> RegularSection label
-  -> RegularEdgeKey label
-  -> Bool
-faceEdgeExcludes query section (fromLabel, toLabel) =
-  generatorEdgeExcludes
-    query
-    (sectionGenerators section)
-    (fromLabel, toLabel)
-
-generatorEdgeExcludes
-  :: Ord label
-  => ExactPoint
-  -> Map label (ExactPowerGenerator label)
-  -> RegularEdgeKey label
-  -> Bool
-generatorEdgeExcludes query generators (fromLabel, toLabel) =
-  case (Map.lookup fromLabel generators, Map.lookup toLabel generators) of
-    (Just fromGenerator, Just toGenerator) ->
-      exactOrient2d
-        (uncurry exactPoint (exactGeneratorSlope fromGenerator))
-        (uncurry exactPoint (exactGeneratorSlope toGenerator))
-        query
-        == LT
-    _ -> False
-
-generatorEdgeExcludesScaled
-  :: Ord label
-  => ExactRational
-  -> (ExactRational, ExactRational)
-  -> Map label (ExactPowerGenerator label)
-  -> RegularEdgeKey label
-  -> Bool
-generatorEdgeExcludesScaled scale (queryX, queryY) generators (fromLabel, toLabel) =
-  case (Map.lookup fromLabel generators, Map.lookup toLabel generators) of
-    (Just fromGenerator, Just toGenerator) ->
-      let (fromX, fromY) = exactGeneratorSlope fromGenerator
-          (toX, toY) = exactGeneratorSlope toGenerator
-       in (toX - fromX) * (queryY - scale * fromY)
-            - (toY - fromY) * (queryX - scale * fromX)
-            < 0
-    _ -> False
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Model.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Model.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Model.hs
+++ /dev/null
@@ -1,558 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Shared opaque carriers for exact power topology. This module owns
--- representation only; construction and interpretation live in sibling modules.
-module Moonlight.Triangulation.Internal.PowerDiagram.Model where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty)
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import qualified Data.Vector as Vector
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactAffineLine
-  , ExactClipError
-  , ExactHalfPlaneError
-  , ExactPoint
-  , ExactRay
-  , ExactSegment
-  , exactPointFromQueryPoint
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRationalFromFiniteDouble
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (orderedPair)
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-  ( ExactPowerGenerator (..)
-  , GeneratorRegularReceipt
-  , GeneratorRegularTopology
-  , RegularTopologyError
-  )
-import Moonlight.Triangulation.Math (mkQueryPoint)
-import Moonlight.Triangulation.Minkowski (ConvexPolygon, MinkowskiError)
-import Moonlight.Triangulation.Internal.Overlay.Types (OverlayError)
-import Moonlight.Triangulation.Types
-  ( NonFiniteValue
-  , Point
-  , PointValidationError
-  , QueryPoint
-  , classifyNonFinite
-  , queryPointValue
-  )
-
--- | An admitted signed additive power offset.  Power distance is
--- @||x-p||^2-w@, so negative values are lawful and this is deliberately not a
--- squared-radius refinement.
-newtype PowerWeight = PowerWeight ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | The sole obstruction to admitting a signed binary64 power offset.
-data PowerWeightError
-  = PowerWeightNonFinite !NonFiniteValue
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Admit a finite binary64 power offset exactly.
-powerWeight :: Double -> Either PowerWeightError PowerWeight
-powerWeight value =
-  case classifyNonFinite value of
-    Just obstruction -> Left (PowerWeightNonFinite obstruction)
-    Nothing -> Right (PowerWeight (exactRationalFromFiniteDouble value))
-
--- | Admit an already-exact signed power offset without a binary64 round trip.
-powerWeightFromExact :: ExactRational -> PowerWeight
-powerWeightFromExact = PowerWeight
-
--- | Exact rational value of an admitted power offset.
-powerWeightExact :: PowerWeight -> ExactRational
-powerWeightExact (PowerWeight value) = value
-
--- | One labelled, admitted weighted site.  Construction validates and
--- canonicalizes the binary64 position once.
-data PowerSite label = PowerSite
-  { powerSiteLabel :: !label
-    -- ^ Stable identity used by edits and canonical topology.
-  , powerSiteQueryPoint :: !QueryPoint
-  , powerSiteWeight :: !PowerWeight
-    -- ^ Signed additive power offset.
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Admit one stable-labelled site at a finite position.
-powerSite
-  :: label
-  -> Point
-  -> PowerWeight
-  -> Either (PowerDiagramError label) (PowerSite label)
-powerSite label point weight =
-  PowerSite label <$> first (PowerSitePositionInvalid label) (mkQueryPoint point) <*> pure weight
-
--- | Binary64 position originally admitted for the site.
-powerSitePosition :: PowerSite label -> Point
-powerSitePosition = queryPointValue . powerSiteQueryPoint
-
--- | Exact coordinates retained by an admitted site.
-powerSiteExactPosition :: PowerSite label -> ExactPoint
-powerSiteExactPosition = exactPointFromQueryPoint . powerSiteQueryPoint
-
--- | Exactly one authoritative result for each submitted label.
-data PowerCellDisposition label
-  = PublishedPowerCell !ConvexPolygon
-  | LowerDimensionalPowerCell !(NonEmpty ExactPoint)
-  | EmptyPowerCell
-  | CoincidentEquivalentTo !label
-  | CoincidentDominatedBy !label
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Visibility of one submitted label in the exact regular subdivision.
-data RegularSiteDisposition label
-  = RegularSiteVisible
-  | RegularSiteLowerDimensional
-  | RegularSiteHidden
-  | RegularSiteCoincidentEquivalentTo !label
-  | RegularSiteCoincidentDominatedBy !label
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One oriented regular face and its exact weighted-dual vertex.
-data RegularFace label = RegularFace !label !label !label !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Canonically oriented labels of the face.
-regularFaceLabels :: RegularFace label -> (label, label, label)
-regularFaceLabels (RegularFace firstLabel secondLabel thirdLabel _) =
-  (firstLabel, secondLabel, thirdLabel)
-
--- | Exact power vertex dual to the face.
-regularFaceDualPoint :: RegularFace label -> ExactPoint
-regularFaceDualPoint (RegularFace _ _ _ dualPoint) = dualPoint
-
--- | Exact weighted Voronoi geometry dual to one regular edge.
-data PowerDualEdge
-  = BoundedPowerDual !ExactSegment
-  | UnboundedPowerDual !ExactRay
-  | FullLinePowerDual !ExactAffineLine
-  | CollapsedPowerDual !ExactPoint
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One unordered regular edge and its exact dual geometry.
-data RegularEdge label = RegularEdge !label !label !PowerDualEdge
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Ascending endpoint labels of the unordered edge.
-regularEdgeLabels :: RegularEdge label -> (label, label)
-regularEdgeLabels (RegularEdge firstLabel secondLabel _) =
-  (firstLabel, secondLabel)
-
--- | Exact power-dual carrier of the edge.
-regularEdgeDual :: RegularEdge label -> PowerDualEdge
-regularEdgeDual (RegularEdge _ _ dual) = dual
-
-type RegularFaceKey label = (label, label, label)
-type RegularEdgeKey label = (label, label)
-
-data RegularEdgeSection label = RegularEdgeSection
-  { sectionRegularEdge :: !(RegularEdge label)
-  , sectionRegularEdgeFaces :: !(Set (RegularFaceKey label))
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data RegularSiteStar label = RegularSiteStar
-  { sectionIncidentFaces :: !(Set (RegularFaceKey label))
-  , sectionSiteNeighbours :: !(Set label)
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data RegularLocality label = RegularLocality
-  { localityFaceSeed :: !(RegularFaceKey label)
-  , localityHiddenSupport :: !(Map label (RegularFaceKey label))
-  , localitySupportHidden :: !(Map (RegularFaceKey label) (Set label))
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-data RegularSlopeLocation label
-  = RegularSlopeInside !(RegularFaceKey label)
-  | RegularSlopeOutside !(RegularEdgeKey label)
-
-data LocalRegularEditObstruction label
-  = LocalRegularSectionUnavailable
-  | LocalRegularLocalityUnavailable
-  | LocalRegularSlopeCoincidence !label
-  | LocalRegularSlopeOutside !(RegularEdgeKey label)
-  | LocalRegularSlopeLocationFailed
-  | LocalRegularDegenerateFace !(RegularFaceKey label)
-  | LocalRegularFaceMissing !(RegularFaceKey label)
-  | LocalRegularGeneratorMissing !label
-  | LocalRegularNonManifoldEdge !(RegularEdgeKey label) !Int
-  | LocalRegularNonConvexEdge !(RegularEdgeKey label)
-  | LocalRegularEmptyHorizon
-  | LocalRegularBoundaryRemoval !label
-  | LocalRegularCandidateSectionEmpty
-  | LocalRegularCavityBoundaryMismatch
-  | LocalRegularTopologyObstruction !(RegularTopologyError label)
-
-data RegularConflictDescent label = RegularConflictDescent
-  { conflictPendingFaces :: !(Set (RegularFaceKey label))
-  , conflictVisitedFaces :: !(Set (RegularFaceKey label))
-  , conflictVisibleFaces :: !(Set (RegularFaceKey label))
-  , conflictObstruction :: !(Maybe (LocalRegularEditObstruction label))
-  }
-
-data RegularFacePatch label = RegularFacePatch
-  { patchedRegularFaces :: !(Map (RegularFaceKey label) (RegularFace label))
-  , patchedRegularEdges :: !(Map (RegularEdgeKey label) (RegularEdgeSection label))
-  , patchedRegularStars :: !(Map label (RegularSiteStar label))
-  , patchedRegularLabels :: !(Set label)
-  , patchedRegularTouchedEdges :: !(Set (RegularEdgeKey label))
-  }
-
-data RegularSection label = RegularSection
-  { sectionGenerators :: !(Map label (ExactPowerGenerator label))
-  , sectionSlopeRepresentatives :: !(Map (ExactRational, ExactRational) label)
-  , sectionCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
-  , sectionRegularDispositions :: !(Map label (RegularSiteDisposition label))
-  , sectionRegularFaces :: !(Map (RegularFaceKey label) (RegularFace label))
-  , sectionRegularEdges :: !(Map (RegularEdgeKey label) (RegularEdgeSection label))
-  , sectionRegularStars :: !(Map label (RegularSiteStar label))
-  , sectionRegularReceipt :: !GeneratorRegularReceipt
-  , sectionRegularLocality :: !(Maybe (RegularLocality label))
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact regular subdivision. Every weighted site remains resident in this
--- opaque value; one normalized section owns all derived topology and incidence.
-data RegularTriangulation label = RegularTriangulation
-  { storedRegularSites :: !(Map label (PowerSite label))
-  , storedRegularSection :: !(Maybe (RegularSection label))
-  }
-  deriving stock (Generic)
-  deriving anyclass (NFData)
-
--- | Semantic equality is equality of the complete weighted-site section.
--- Topology and visibility are sealed derived caches of that section.
-instance Eq label => Eq (RegularTriangulation label) where
-  left == right = storedRegularSites left == storedRegularSites right
-
-instance Show label => Show (RegularTriangulation label) where
-  showsPrec precedence triangulation =
-    showParen (precedence > 10)
-      ( showString "RegularTriangulation "
-          . shows (storedRegularSites triangulation)
-      )
-
--- | Strict cardinality receipt for one normalized regular section.
-data RegularTriangulationReceipt = RegularTriangulationReceipt
-  { regularTriangulationInputSites :: !Int
-  , regularTriangulationRepresentativeSites :: !Int
-  , regularTriangulationVisibleSites :: !Int
-  , regularTriangulationLowerDimensionalSites :: !Int
-  , regularTriangulationHiddenSites :: !Int
-  , regularTriangulationCoincidentSites :: !Int
-  , regularTriangulationFaces :: !Int
-  , regularTriangulationEdges :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One exact change in the exhaustive visibility section. Appearance and
--- disappearance are explicit rather than encoded with sentinel dispositions.
-data RegularSiteTransition label
-  = RegularSiteAppeared !label !(RegularSiteDisposition label)
-  | RegularSiteDisappeared !label !(RegularSiteDisposition label)
-  | RegularSiteTransitioned
-      !label
-      !(RegularSiteDisposition label)
-      !(RegularSiteDisposition label)
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | A regular edit either conflicts with an existing stable identity, names
--- absent patch identities, or carries the exact normalization obstruction.
-data RegularEditError label
-  = RegularEditSiteConflict
-      !label
-      !(PowerSite label)
-      !(PowerSite label)
-  | RegularEditUnknownSites !(NonEmpty label)
-  | RegularEditTopologyFailed !(RegularTopologyError label)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | One common publication for every pure regular-site edit. The changed-site
--- support is empty precisely for an idempotent edit.
-data RegularEditResult label = RegularEditResult
-  { regularEditTriangulation :: !(RegularTriangulation label)
-  , regularEditChangedSites :: !(Set label)
-  , regularEditTransitions :: !(Vector.Vector (RegularSiteTransition label))
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Total labelled result, opaque so callers cannot omit a submitted label.
-newtype BoundedPowerDiagram label =
-  BoundedPowerDiagram (Map label (PowerCellDisposition label))
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed admission, topology, and clipping obstructions.
-data PowerDiagramError label
-  = PowerSitePositionInvalid !label !PointValidationError
-  | DuplicatePowerSiteLabel !label
-  | PowerDomainInvalid !ExactHalfPlaneError
-  | PowerBisectorInvalid !label !label !ExactHalfPlaneError
-  | PowerRegularTopologyFailed !(RegularTopologyError label)
-  | PowerRegularDispositionMissing !label
-  | PowerRegularGeneratorMissing !label
-  | PowerCellClipFailed !label !ExactClipError
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Exact construction counts and rational-width evidence for a bounded diagram.
-data PowerDiagramReceipt = PowerDiagramReceipt
-  { powerDiagramDomainVertices :: !Int
-  , powerDiagramSubmittedSiteConstraints :: !Int
-  , powerDiagramActiveBoundaries :: !Int
-  , powerDiagramBoundaryCompatibilityChecks :: !Int
-  , powerDiagramExactIntersections :: !Int
-  , powerDiagramPublishedCells :: !Int
-  , powerDiagramLowerDimensionalCells :: !Int
-  , powerDiagramEmptyCells :: !Int
-  , powerDiagramCoincidentEquivalentCells :: !Int
-  , powerDiagramCoincidentDominatedCells :: !Int
-  , powerDiagramRegularFaces :: !Int
-  , powerDiagramRegularEdges :: !Int
-  , powerDiagramOracleCells :: !Int
-  , powerDiagramMaximumCellConstraints :: !Int
-  , powerDiagramMaximumInputBits :: !Int
-  , powerDiagramMaximumAffineCoefficientBits :: !Int
-  , powerDiagramPeakIntermediateCoordinateBits :: !Int
-  , powerDiagramFinalCoordinateBits :: !Int
-  , powerDiagramFinalDenominatorBits :: !Int
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Submitted sites, derived from the exhaustive disposition partition.
-powerDiagramInputSites :: PowerDiagramReceipt -> Int
-powerDiagramInputSites receipt =
-  powerDiagramPublishedCells receipt
-    + powerDiagramLowerDimensionalCells receipt
-    + powerDiagramEmptyCells receipt
-    + powerDiagramCoincidentEquivalentCells receipt
-    + powerDiagramCoincidentDominatedCells receipt
-{-# INLINE powerDiagramInputSites #-}
-
--- | Peak exact-coordinate width beyond the widest admitted input.
-powerDiagramPeakIntermediateBitGrowth :: PowerDiagramReceipt -> Int
-powerDiagramPeakIntermediateBitGrowth receipt =
-  max
-    0
-    ( powerDiagramPeakIntermediateCoordinateBits receipt
-        - powerDiagramMaximumInputBits receipt
-    )
-{-# INLINE powerDiagramPeakIntermediateBitGrowth #-}
-
--- | Published coordinate width beyond the widest admitted input.
-powerDiagramFinalCoordinateBitGrowth :: PowerDiagramReceipt -> Int
-powerDiagramFinalCoordinateBitGrowth receipt =
-  max
-    0
-    ( powerDiagramFinalCoordinateBits receipt
-        - powerDiagramMaximumInputBits receipt
-    )
-{-# INLINE powerDiagramFinalCoordinateBitGrowth #-}
-
--- | One exact affine form @c0 + cx*x + cy*y@.
-data AffineForm = AffineForm
-  { affineFormConstant :: !ExactRational
-  , affineFormXCoefficient :: !ExactRational
-  , affineFormYCoefficient :: !ExactRational
-  }
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed obstructions from exact affine argmax decomposition.  Power-cell
--- construction remains the canonical geometric owner; window restriction is
--- the only additional boundary.
-data UpperEnvelopeError label
-  = UpperEnvelopeEmptyForms
-  | UpperEnvelopeWindowHullFailed !MinkowskiError
-  | UpperEnvelopePowerConstructionFailed !(PowerDiagramError label)
-  | UpperEnvelopeWindowOverlayFailed !(OverlayError (Maybe label) Bool)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-
-data CoincidentGeneratorDisposition label
-  = CoincidentGeneratorEquivalentTo !label
-  | CoincidentGeneratorDominatedBy !label
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-data ResolvedGeneratorSection label = ResolvedGeneratorSection
-  { resolvedCoincidentDispositions :: !(Map label (CoincidentGeneratorDisposition label))
-  , resolvedRegularTopology :: !(GeneratorRegularTopology label)
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-foldRegularFaceBoundary
-  :: (label -> label -> label -> result -> result)
-  -> result
-  -> RegularFace label
-  -> result
-foldRegularFaceBoundary step initial face =
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-   in step firstLabel secondLabel thirdLabel
-        ( step secondLabel thirdLabel firstLabel
-            (step thirdLabel firstLabel secondLabel initial)
-        )
-{-# INLINE foldRegularFaceBoundary #-}
-
-regularFaceDirectedEdgeKeys :: RegularFace label -> [RegularEdgeKey label]
-regularFaceDirectedEdgeKeys =
-  foldRegularFaceBoundary
-    (\fromLabel toLabel _ -> ((fromLabel, toLabel) :))
-    []
-
-regularFaceCanonicalEdgeKeys
-  :: Ord label
-  => RegularFace label
-  -> [RegularEdgeKey label]
-regularFaceCanonicalEdgeKeys =
-  foldRegularFaceBoundary
-    (\fromLabel toLabel _ -> (orderedPair fromLabel toLabel :))
-    []
-
-regularFaceLabelSet :: Ord label => RegularFace label -> Set label
-regularFaceLabelSet =
-  foldRegularFaceBoundary (\label _ _ -> Set.insert label) Set.empty
-
-requireFaceEdgeWitness
-  :: Ord label
-  => RegularEdgeKey label
-  -> RegularFace label
-  -> Either (LocalRegularEditObstruction label) (label, label, label)
-requireFaceEdgeWitness edgeKey face =
-  maybe
-    (Left (LocalRegularFaceMissing (regularFaceKey face)))
-    Right
-    ( foldRegularFaceBoundary
-        (\fromLabel toLabel oppositeLabel later ->
-            if orderedPair fromLabel toLabel == edgeKey
-              then Just (fromLabel, toLabel, oppositeLabel)
-              else later)
-        Nothing
-        face
-    )
-
-regularFaceKey :: Ord label => RegularFace label -> RegularFaceKey label
-regularFaceKey face =
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-   in sortedRegularFaceLabels firstLabel secondLabel thirdLabel
-
-sortedRegularFaceLabels
-  :: Ord label
-  => label
-  -> label
-  -> label
-  -> RegularFaceKey label
-sortedRegularFaceLabels firstLabel secondLabel thirdLabel =
-  let (firstLow, firstHigh) = orderedPair firstLabel secondLabel
-      (secondLow, finalHigh) = orderedPair firstHigh thirdLabel
-      (finalLow, finalMiddle) = orderedPair firstLow secondLow
-   in (finalLow, finalMiddle, finalHigh)
-
-regularEdgeKey :: Ord label => RegularEdge label -> RegularEdgeKey label
-regularEdgeKey = uncurry orderedPair . regularEdgeLabels
-
-regularFaceEdgeIncidence
-  :: Ord label
-  => RegularFace label
-  -> Map (RegularEdgeKey label) (Set (RegularFaceKey label))
-regularFaceEdgeIncidence face =
-  let incidentFace = Set.singleton (regularFaceKey face)
-   in foldRegularFaceBoundary
-        (\fromLabel toLabel _ ->
-            Map.insert (orderedPair fromLabel toLabel) incidentFace)
-        Map.empty
-        face
-
-emptyRegularSiteStar :: RegularSiteStar label
-emptyRegularSiteStar = RegularSiteStar Set.empty Set.empty
-
-requireFaceFromMap
-  :: Ord label
-  => Map (RegularFaceKey label) (RegularFace label)
-  -> RegularFaceKey label
-  -> Either (LocalRegularEditObstruction label) (RegularFace label)
-requireFaceFromMap faces faceKey =
-  maybe (Left (LocalRegularFaceMissing faceKey)) Right (Map.lookup faceKey faces)
-
-requireGeneratorFromMap
-  :: Ord label
-  => Map label (ExactPowerGenerator label)
-  -> label
-  -> Either (LocalRegularEditObstruction label) (ExactPowerGenerator label)
-requireGeneratorFromMap generators label =
-  maybe (Left (LocalRegularGeneratorMissing label)) Right (Map.lookup label generators)
-
-regularFaceNeighbours
-  :: Ord label
-  => RegularSection label
-  -> RegularFaceKey label
-  -> Either
-      (LocalRegularEditObstruction label)
-      (Set (RegularFaceKey label))
-regularFaceNeighbours section faceKey = do
-  face <- requireFaceFromMap (sectionRegularFaces section) faceKey
-  incident <- traverse requireIncidence (regularFaceCanonicalEdgeKeys face)
-  pure (Set.delete faceKey (Set.unions incident))
- where
-  requireIncidence edgeKey =
-    maybe
-      (Left (LocalRegularNonManifoldEdge edgeKey 0))
-      (Right . sectionRegularEdgeFaces)
-      (Map.lookup edgeKey (sectionRegularEdges section))
-
-replaceRegularSiteSection
-  :: Map label (PowerSite label)
-  -> RegularSection label
-  -> RegularTriangulation label
-  -> RegularTriangulation label
-replaceRegularSiteSection sites section triangulation =
-  triangulation
-    { storedRegularSites = sites
-    , storedRegularSection = Just section
-    }
-
-regularDispositionSection
-  :: RegularTriangulation label
-  -> Map label (RegularSiteDisposition label)
-regularDispositionSection =
-  maybe Map.empty sectionRegularDispositions . storedRegularSection
-
-exactGeneratorSlope
-  :: ExactPowerGenerator label
-  -> (ExactRational, ExactRational)
-exactGeneratorSlope generator =
-  ( exactPowerGeneratorXCoefficient generator
-  , exactPowerGeneratorYCoefficient generator
-  )
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Projection.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Projection.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Projection.hs
+++ /dev/null
@@ -1,601 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
--- | Bounded clipping and planar publication of exact regular sections.
-module Moonlight.Triangulation.Internal.PowerDiagram.Projection
-  ( boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , powerCellDisposition
-  , powerCellDispositions
-  , powerDiagramPlanarLayer
-  , upperEnvelope
-  ) where
-
-import Data.Bifunctor (first)
-import qualified Data.Foldable as Foldable
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation.Exact
-  ( ExactClipDisposition (..)
-  , ExactClipReceipt (..)
-  , ExactAffineLine
-  , ExactClosedHalfPlane
-  , ExactRetainedPolygon
-  , exactClipRetainedPolygon
-  , exactClosedHalfPlane
-  , exactRetainedPolygon
-  , oppositeExactAffineLine
-  )
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-  ( ExactPowerGenerator (..)
-  , GeneratorRegularEdge (..)
-  , GeneratorRegularReceipt (..)
-  , GeneratorRegularTopology (..)
-  , RegularGeneratorDisposition (..)
-  , exactGeneratorAxis
-  )
-import Moonlight.Triangulation.Internal.PowerDiagram.Model
-import Moonlight.Triangulation.Internal.PowerDiagram.Section
-import Moonlight.Triangulation.Internal.Minkowski.Convex
-  ( convexHullPolygon
-  , convexPolygonComponent
-  , convexPolygonFromRetained
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-  ( OverlayCell (..)
-  , OverlayCellGeometry (..)
-  , OverlayResult (..)
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( planarLayerFromAdmittedComponents
-  )
-import Moonlight.Triangulation.Minkowski
-  ( ConvexPolygon
-  , convexPolygonPoints
-  )
-import Moonlight.Triangulation.Overlay
-  ( overlayLayers )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PolygonComponent
-  , exactLoopPoints
-  , polygonOuterLoop
-  )
-
--- | Construct and clip a normalized regular subdivision to one convex domain.
-boundedPowerDiagram
-  :: Ord label
-  => ConvexPolygon
-  -> NonEmpty (PowerSite label)
-  -> Either (PowerDiagramError label) (BoundedPowerDiagram label, PowerDiagramReceipt)
-boundedPowerDiagram domain submitted = do
-  sortedSites <- validateAndSortSites submitted
-  let preparedGenerators = fmap prepareExactPowerGenerator sortedSites
-      generators = fmap fst preparedGenerators
-      siteInputBits =
-        Foldable.foldl' (\bits preparation -> max bits (snd preparation)) 0 preparedGenerators
-  (dispositions, clipReceipt, regularReceipt, maximumCellConstraints) <-
-    exactGeneratorDispositionsWith (<>) mempty domain generators
-  let receipt =
-        aggregateReceipt
-          domain
-          dispositions
-          siteInputBits
-          clipReceipt
-          regularReceipt
-          maximumCellConstraints
-  pure (BoundedPowerDiagram dispositions, receipt)
-
--- | Clip an already normalized regular value without rebuilding its lifted
--- hull. The empty regular value yields an empty labelled diagram.
-boundedPowerDiagramFromRegular
-  :: Ord label
-  => ConvexPolygon
-  -> RegularTriangulation label
-  -> Either (PowerDiagramError label) (BoundedPowerDiagram label, PowerDiagramReceipt)
-boundedPowerDiagramFromRegular domain triangulation =
-  case storedRegularSection triangulation of
-    Nothing ->
-      Right
-        ( BoundedPowerDiagram Map.empty
-        , aggregateReceipt
-            domain
-            Map.empty
-            0
-            mempty
-            emptyGeneratorRegularReceipt
-            0
-        )
-    Just section -> do
-      (dispositions, clipReceipt, generatorReceipt, maximumCellConstraints) <-
-        exactRegularSectionDispositionsWith (<>) mempty domain section
-      let receipt =
-            aggregateReceipt
-              domain
-              dispositions
-              (maximumPowerSiteInputBits (storedRegularSites triangulation))
-              clipReceipt
-              generatorReceipt
-              maximumCellConstraints
-      pure (BoundedPowerDiagram dispositions, receipt)
-
--- | Full-dimensional labelled argmax regions for affine forms
--- @c0 + cx*x + cy*y@ inside an admitted polygonal window. The result is a
--- planar projection: lower-dimensional and empty winners intentionally have
--- no region. Use 'regularTriangulation' on corresponding weighted sites when
--- those dispositions or exact unbounded duals are required. Identical forms
--- choose the least label, independent of map construction order.
-upperEnvelope
-  :: Ord label
-  => PolygonComponent
-  -> Map label AffineForm
-  -> Either (UpperEnvelopeError label) (PlanarLayer (Maybe label))
-upperEnvelope window forms = do
-  generators <- affineFormGenerators forms
-  domain <-
-    first UpperEnvelopeWindowHullFailed
-      (convexHullPolygon (exactLoopPoints (polygonOuterLoop window)))
-  (dispositions, _, _, _) <-
-    first UpperEnvelopePowerConstructionFailed
-      (exactGeneratorDispositionsWith discardClipReceipt () domain generators)
-  let envelopeLayer = affineDispositionLayer dispositions
-  if convexPolygonComponent domain == window
-    then Right envelopeLayer
-    else restrictEnvelopeToWindow window envelopeLayer
-
--- | Look up the exhaustive bounded-cell disposition for one submitted label.
-powerCellDisposition
-  :: Ord label
-  => label
-  -> BoundedPowerDiagram label
-  -> Maybe (PowerCellDisposition label)
-powerCellDisposition label (BoundedPowerDiagram dispositions) =
-  Map.lookup label dispositions
-
--- | Enumerate every submitted label and bounded-cell disposition in order.
-powerCellDispositions
-  :: BoundedPowerDiagram label
-  -> [(label, PowerCellDisposition label)]
-powerCellDispositions (BoundedPowerDiagram dispositions) = Map.toAscList dispositions
-
--- | Publish the full-dimensional cell layer with a canonical exterior.
-powerDiagramPlanarLayer
-  :: Ord label
-  => BoundedPowerDiagram label
-  -> PlanarLayer (Maybe label)
-powerDiagramPlanarLayer (BoundedPowerDiagram dispositions) =
-  affineDispositionLayer dispositions
-
-affineDispositionLayer
-  :: Ord label
-  => Map label (PowerCellDisposition label)
-  -> PlanarLayer (Maybe label)
-affineDispositionLayer = publishedPowerLayer Nothing Just
-
-publishedPowerLayer
-  :: Ord publishedLabel
-  => publishedLabel
-  -> (label -> publishedLabel)
-  -> Map label (PowerCellDisposition label)
-  -> PlanarLayer publishedLabel
-publishedPowerLayer outside publishLabel dispositions =
-  planarLayerFromAdmittedComponents
-    outside
-    [ (publishLabel label, convexPolygonComponent polygon)
-    | (label, PublishedPowerCell polygon) <- Map.toAscList dispositions
-    ]
-
-restrictEnvelopeToWindow
-  :: Ord label
-  => PolygonComponent
-  -> PlanarLayer (Maybe label)
-  -> Either (UpperEnvelopeError label) (PlanarLayer (Maybe label))
-restrictEnvelopeToWindow window envelopeLayer = do
-  let windowLayer = planarLayerFromAdmittedComponents False [(True, window)]
-  clipped <-
-    first UpperEnvelopeWindowOverlayFailed
-      (overlayLayers envelopeLayer windowLayer)
-  pure
-    ( planarLayerFromAdmittedComponents
-        Nothing
-        [ (Just label, component)
-        | cell <- Vector.toList (overlayResultCells clipped)
-        , overlayCellRight cell
-        , Just label <- [overlayCellLeft cell]
-        , BoundedOverlayCell component <- [overlayCellGeometry cell]
-        ]
-    )
-
-
-affineFormGenerators
-  :: Map label AffineForm
-  -> Either (UpperEnvelopeError label) (NonEmpty (ExactPowerGenerator label))
-affineFormGenerators forms =
-  case Map.minViewWithKey forms of
-    Nothing -> Left UpperEnvelopeEmptyForms
-    Just ((firstLabel, firstForm), remaining) ->
-      Right
-        ( affineFormGenerator firstLabel firstForm
-            :| fmap (uncurry affineFormGenerator) (Map.toAscList remaining)
-        )
-
-affineFormGenerator
-  :: label
-  -> AffineForm
-  -> ExactPowerGenerator label
-affineFormGenerator label form =
-  ExactPowerGenerator
-    { exactPowerGeneratorLabel = label
-    , exactPowerGeneratorXCoefficient = affineFormXCoefficient form
-    , exactPowerGeneratorYCoefficient = affineFormYCoefficient form
-    , exactPowerGeneratorConstant = affineFormConstant form
-    }
-
-exactGeneratorDispositionsWith
-  :: Ord label
-  => (summary -> ExactClipReceipt -> summary)
-  -> summary
-  -> ConvexPolygon
-  -> NonEmpty (ExactPowerGenerator label)
-  -> Either
-      (PowerDiagramError label)
-      ( Map label (PowerCellDisposition label)
-      , summary
-      , GeneratorRegularReceipt
-      , Int
-      )
-exactGeneratorDispositionsWith summarizeReceipt initialSummary domain generators = do
-  section <- first PowerRegularTopologyFailed (resolvedGeneratorSection generators)
-  exactResolvedGeneratorDispositionsWith
-    summarizeReceipt
-    initialSummary
-    domain
-    section
-
-exactResolvedGeneratorDispositionsWith
-  :: Ord label
-  => (summary -> ExactClipReceipt -> summary)
-  -> summary
-  -> ConvexPolygon
-  -> ResolvedGeneratorSection label
-  -> Either
-      (PowerDiagramError label)
-      ( Map label (PowerCellDisposition label)
-      , summary
-      , GeneratorRegularReceipt
-      , Int
-      )
-exactResolvedGeneratorDispositionsWith summarizeReceipt initialSummary domain section = do
-  retainedDomain <-
-    first PowerDomainInvalid (exactRetainedPolygon (convexPolygonPoints domain))
-  let topology = resolvedRegularTopology section
-  constraints <- prepareRegularConstraintSection topology
-  clipped <-
-    traverse
-      (clipPowerCell retainedDomain section constraints)
-      (generatorRegularDispositions topology)
-  let (dispositions, summary, maximumCellConstraints) =
-        summarizeClippedPowerCells
-          summarizeReceipt
-          initialSummary
-          (resolvedCoincidentDispositions section)
-          (NonEmpty.toList clipped)
-  pure
-    ( dispositions
-    , summary
-    , generatorRegularReceipt topology
-    , maximumCellConstraints
-    )
-
-exactRegularSectionDispositionsWith
-  :: Ord label
-  => (summary -> ExactClipReceipt -> summary)
-  -> summary
-  -> ConvexPolygon
-  -> RegularSection label
-  -> Either
-      (PowerDiagramError label)
-      ( Map label (PowerCellDisposition label)
-      , summary
-      , GeneratorRegularReceipt
-      , Int
-      )
-exactRegularSectionDispositionsWith summarizeReceipt initialSummary domain section = do
-  retainedDomain <-
-    first PowerDomainInvalid (exactRetainedPolygon (convexPolygonPoints domain))
-  constraints <- preparePublishedRegularConstraints section
-  representativeDispositions <-
-    traverse
-      (\(label, generator) -> do
-         disposition <-
-           maybe
-             (Left (PowerRegularDispositionMissing label))
-             Right
-             (Map.lookup label (sectionRegularDispositions section))
-         clipPublishedPowerCell
-           retainedDomain
-           section
-           constraints
-           generator
-           disposition)
-      (Map.toAscList (sectionGenerators section))
-  let (dispositions, summary, maximumCellConstraints) =
-        summarizeClippedPowerCells
-          summarizeReceipt
-          initialSummary
-          (sectionCoincidentDispositions section)
-          representativeDispositions
-  pure
-    ( dispositions
-    , summary
-    , sectionRegularReceipt section
-    , maximumCellConstraints
-    )
-
-summarizeClippedPowerCells
-  :: Ord label
-  => (summary -> ExactClipReceipt -> summary)
-  -> summary
-  -> Map label (CoincidentGeneratorDisposition label)
-  -> [(label, PowerCellDisposition label, ExactClipReceipt, Int)]
-  -> (Map label (PowerCellDisposition label), summary, Int)
-summarizeClippedPowerCells summarizeReceipt initialSummary coincident clipped =
-  let (publishedAssociations, summary, maximumCellConstraints) =
-        Foldable.foldl'
-          (\(associations, accumulatedSummary, peak) (label, disposition, cellReceipt, cellAxes) ->
-             let !combinedSummary = summarizeReceipt accumulatedSummary cellReceipt
-              in ( (label, disposition) : associations
-                 , combinedSummary
-                 , max peak cellAxes
-                 ))
-          ([], initialSummary, 0)
-          clipped
-   in ( fmap coincidentPowerDisposition coincident
-          <> Map.fromList publishedAssociations
-      , summary
-      , maximumCellConstraints
-      )
-
-preparePublishedRegularConstraints
-  :: Ord label
-  => RegularSection label
-  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
-preparePublishedRegularConstraints section =
-  Foldable.foldlM prepare Map.empty (Map.elems (sectionRegularEdges section))
- where
-  prepare constraints edgeSection = do
-    let edge = sectionRegularEdge edgeSection
-        (firstLabel, secondLabel) = regularEdgeLabels edge
-    firstGenerator <- requireSectionGenerator section firstLabel
-    secondGenerator <- requireSectionGenerator section secondLabel
-    insertRegularConstraintEdge constraints firstGenerator secondGenerator
-
-clipPublishedPowerCell
-  :: Ord label
-  => ExactRetainedPolygon
-  -> RegularSection label
-  -> Map label [ExactClosedHalfPlane]
-  -> ExactPowerGenerator label
-  -> RegularSiteDisposition label
-  -> Either
-      (PowerDiagramError label)
-      (label, PowerCellDisposition label, ExactClipReceipt, Int)
-clipPublishedPowerCell retainedDomain section constraints ownerGenerator disposition =
-  let ownerLabel = exactPowerGeneratorLabel ownerGenerator
-      competitors =
-        filter
-          ((/= ownerLabel) . exactPowerGeneratorLabel)
-          (Map.elems (sectionGenerators section))
-      clipAs =
-        clipRegularGeneratorCell
-          retainedDomain
-          ownerGenerator
-          (Map.findWithDefault [] ownerLabel constraints)
-          competitors
-   in case disposition of
-        RegularSiteHidden -> clipAs RegularGeneratorHidden
-        RegularSiteVisible -> clipAs RegularGeneratorVisible
-        RegularSiteLowerDimensional -> clipAs RegularGeneratorLowerDimensional
-        RegularSiteCoincidentEquivalentTo _ ->
-          Left (PowerRegularDispositionMissing ownerLabel)
-        RegularSiteCoincidentDominatedBy _ ->
-          Left (PowerRegularDispositionMissing ownerLabel)
-
-requireSectionGenerator
-  :: Ord label
-  => RegularSection label
-  -> label
-  -> Either (PowerDiagramError label) (ExactPowerGenerator label)
-requireSectionGenerator section label =
-  maybe
-    (Left (PowerRegularGeneratorMissing label))
-    Right
-    (Map.lookup label (sectionGenerators section))
-
-discardClipReceipt :: () -> ExactClipReceipt -> ()
-discardClipReceipt _ _ = ()
-
-prepareRadicalAxis
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either (PowerDiagramError label) ExactAffineLine
-prepareRadicalAxis firstGenerator secondGenerator =
-  first
-    ( PowerBisectorInvalid
-        (exactPowerGeneratorLabel firstGenerator)
-        (exactPowerGeneratorLabel secondGenerator)
-    )
-    (exactGeneratorAxis firstGenerator secondGenerator)
-
-prepareRegularConstraintSection
-  :: Ord label
-  => GeneratorRegularTopology label
-  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
-prepareRegularConstraintSection =
-  Foldable.foldlM prepareRegularConstraintEdge Map.empty . generatorRegularEdges
-
-prepareRegularConstraintEdge
-  :: Ord label
-  => Map label [ExactClosedHalfPlane]
-  -> GeneratorRegularEdge label
-  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
-prepareRegularConstraintEdge constraints edge = do
-  let firstGenerator = generatorRegularEdgeFirst edge
-      secondGenerator = generatorRegularEdgeSecond edge
-  insertRegularConstraintEdge constraints firstGenerator secondGenerator
-
-insertRegularConstraintEdge
-  :: Ord label
-  => Map label [ExactClosedHalfPlane]
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either (PowerDiagramError label) (Map label [ExactClosedHalfPlane])
-insertRegularConstraintEdge constraints firstGenerator secondGenerator = do
-  let firstLabel = exactPowerGeneratorLabel firstGenerator
-      secondLabel = exactPowerGeneratorLabel secondGenerator
-  axis <- prepareRadicalAxis firstGenerator secondGenerator
-  pure
-    ( Map.insertWith (<>) secondLabel [exactClosedHalfPlane (oppositeExactAffineLine axis)]
-        (Map.insertWith (<>) firstLabel [exactClosedHalfPlane axis] constraints)
-    )
-
-clipPowerCell
-  :: Ord label
-  => ExactRetainedPolygon
-  -> ResolvedGeneratorSection label
-  -> Map label [ExactClosedHalfPlane]
-  -> (ExactPowerGenerator label, RegularGeneratorDisposition)
-  -> Either
-      (PowerDiagramError label)
-      (label, PowerCellDisposition label, ExactClipReceipt, Int)
-clipPowerCell retainedDomain section constraints (ownerGenerator, disposition) =
-  let ownerLabel = exactPowerGeneratorLabel ownerGenerator
-      topology = resolvedRegularTopology section
-      competitors =
-        [ generator
-        | (generator, _) <-
-            NonEmpty.toList (generatorRegularDispositions topology)
-        , exactPowerGeneratorLabel generator /= ownerLabel
-        ]
-   in clipRegularGeneratorCell
-        retainedDomain
-        ownerGenerator
-        (Map.findWithDefault [] ownerLabel constraints)
-        competitors
-        disposition
-
-clipRegularGeneratorCell
-  :: ExactRetainedPolygon
-  -> ExactPowerGenerator label
-  -> [ExactClosedHalfPlane]
-  -> [ExactPowerGenerator label]
-  -> RegularGeneratorDisposition
-  -> Either
-      (PowerDiagramError label)
-      (label, PowerCellDisposition label, ExactClipReceipt, Int)
-clipRegularGeneratorCell retainedDomain ownerGenerator activeConstraints competitors disposition =
-  case disposition of
-    RegularGeneratorHidden ->
-      pure (exactPowerGeneratorLabel ownerGenerator, EmptyPowerCell, mempty, 0)
-    RegularGeneratorVisible ->
-      finishPowerCell retainedDomain ownerGenerator activeConstraints
-    RegularGeneratorLowerDimensional -> do
-      halfPlanes <- traverse (preparedDirectHalfPlane ownerGenerator) competitors
-      finishPowerCell retainedDomain ownerGenerator halfPlanes
-
-preparedDirectHalfPlane
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> Either (PowerDiagramError label) ExactClosedHalfPlane
-preparedDirectHalfPlane owner competitor =
-  exactClosedHalfPlane <$> prepareRadicalAxis owner competitor
-
-finishPowerCell
-  :: ExactRetainedPolygon
-  -> ExactPowerGenerator label
-  -> [ExactClosedHalfPlane]
-  -> Either
-      (PowerDiagramError label)
-      (label, PowerCellDisposition label, ExactClipReceipt, Int)
-finishPowerCell retainedDomain ownerGenerator halfPlanes = do
-  (exactDisposition, receipt) <-
-    first (PowerCellClipFailed (exactPowerGeneratorLabel ownerGenerator))
-      (exactClipRetainedPolygon retainedDomain halfPlanes)
-  pure
-    ( exactPowerGeneratorLabel ownerGenerator
-    , case exactDisposition of
-        ExactClipFullDimensional retained ->
-          PublishedPowerCell (convexPolygonFromRetained retained)
-        ExactClipLowerDimensional points -> LowerDimensionalPowerCell points
-        ExactClipEmpty -> EmptyPowerCell
-    , receipt
-    , length halfPlanes
-    )
-
-aggregateReceipt
-  :: ConvexPolygon
-  -> Map label (PowerCellDisposition label)
-  -> Int
-  -> ExactClipReceipt
-  -> GeneratorRegularReceipt
-  -> Int
-  -> PowerDiagramReceipt
-aggregateReceipt domain dispositions siteInputBits clipReceipt regularReceipt maximumCellConstraints =
-  let inputBits = max siteInputBits (exactClipInputCoordinateBits clipReceipt)
-      peakBits = exactClipPeakIntermediateCoordinateBits clipReceipt
-      finalBits = exactClipFinalCoordinateBits clipReceipt
-      dispositionCounts = countPowerDispositions dispositions
-   in PowerDiagramReceipt
-        { powerDiagramDomainVertices = NonEmpty.length (convexPolygonPoints domain)
-        , powerDiagramSubmittedSiteConstraints = exactClipSubmittedHalfPlanes clipReceipt
-        , powerDiagramActiveBoundaries = exactClipActiveBoundaries clipReceipt
-        , powerDiagramBoundaryCompatibilityChecks = exactClipBoundaryCompatibilityChecks clipReceipt
-        , powerDiagramExactIntersections = exactClipExactIntersections clipReceipt
-        , powerDiagramPublishedCells = countedPublishedCells dispositionCounts
-        , powerDiagramLowerDimensionalCells = countedLowerDimensionalCells dispositionCounts
-        , powerDiagramEmptyCells = countedEmptyCells dispositionCounts
-        , powerDiagramCoincidentEquivalentCells = countedCoincidentEquivalentCells dispositionCounts
-        , powerDiagramCoincidentDominatedCells = countedCoincidentDominatedCells dispositionCounts
-        , powerDiagramRegularFaces = generatorRegularFaceCount regularReceipt
-        , powerDiagramRegularEdges = generatorRegularEdgeCount regularReceipt
-        , powerDiagramOracleCells = generatorRegularLowerDimensionalSites regularReceipt
-        , powerDiagramMaximumCellConstraints = maximumCellConstraints
-        , powerDiagramMaximumInputBits = inputBits
-        , powerDiagramMaximumAffineCoefficientBits = exactClipMaximumAffineCoefficientBits clipReceipt
-        , powerDiagramPeakIntermediateCoordinateBits = peakBits
-        , powerDiagramFinalCoordinateBits = finalBits
-        , powerDiagramFinalDenominatorBits = exactClipFinalDenominatorBits clipReceipt
-        }
-
-data PowerDispositionCounts = PowerDispositionCounts
-  { countedPublishedCells :: !Int
-  , countedLowerDimensionalCells :: !Int
-  , countedEmptyCells :: !Int
-  , countedCoincidentEquivalentCells :: !Int
-  , countedCoincidentDominatedCells :: !Int
-  }
-
-countPowerDispositions
-  :: Map label (PowerCellDisposition label)
-  -> PowerDispositionCounts
-countPowerDispositions =
-  Map.foldl'
-    (\counts disposition -> case disposition of
-        PublishedPowerCell _ ->
-          counts {countedPublishedCells = countedPublishedCells counts + 1}
-        LowerDimensionalPowerCell _ ->
-          counts {countedLowerDimensionalCells = countedLowerDimensionalCells counts + 1}
-        EmptyPowerCell ->
-          counts {countedEmptyCells = countedEmptyCells counts + 1}
-        CoincidentEquivalentTo _ ->
-          counts {countedCoincidentEquivalentCells = countedCoincidentEquivalentCells counts + 1}
-        CoincidentDominatedBy _ ->
-          counts {countedCoincidentDominatedCells = countedCoincidentDominatedCells counts + 1})
-    PowerDispositionCounts
-      { countedPublishedCells = 0
-      , countedLowerDimensionalCells = 0
-      , countedEmptyCells = 0
-      , countedCoincidentEquivalentCells = 0
-      , countedCoincidentDominatedCells = 0
-      }
diff --git a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Section.hs b/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Section.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Internal/PowerDiagram/Section.hs
+++ /dev/null
@@ -1,489 +0,0 @@
--- | Batch normalization and publication of one exact regular section.
-module Moonlight.Triangulation.Internal.PowerDiagram.Section where
-
-import Data.Bifunctor (first)
-import qualified Data.Foldable as Foldable
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Data.Map.Strict (Map)
-import Data.Ord (comparing)
-import qualified Data.Set as Set
-import Data.Set (Set)
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactPointBitWidth
-  , exactPointCoordinates
-  , exactPointFromQueryPoint
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (consecutivePairs)
-import Moonlight.Triangulation.Internal.ExactRational (exactRationalBitWidth)
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator
-import Moonlight.Triangulation.Internal.PowerDiagram.Hull (regularGeneratorTopology)
-import Moonlight.Triangulation.Internal.PowerDiagram.Locality (buildRegularLocality)
-import Moonlight.Triangulation.Internal.PowerDiagram.Model
-
-regularTriangulation
-  :: Ord label
-  => NonEmpty (PowerSite label)
-  -> Either
-      (PowerDiagramError label)
-      (RegularTriangulation label, RegularTriangulationReceipt)
-regularTriangulation submitted = do
-  sortedSites <- validateAndSortSites submitted
-  triangulation <-
-    first PowerRegularTopologyFailed
-      ( constructRegularTriangulation
-          (Map.fromDistinctAscList (fmap (\site -> (powerSiteLabel site, site)) (NonEmpty.toList sortedSites)))
-      )
-  pure (triangulation, regularTriangulationReceipt triangulation)
-
--- | The empty all-site section. It is the identity for insertion and the
--- result of removing the final site.
-emptyRegularTriangulation :: RegularTriangulation label
-emptyRegularTriangulation =
-  RegularTriangulation
-    { storedRegularSites = Map.empty
-    , storedRegularSection = Nothing
-    }
-
--- | Look up one canonical site by its stable label.
-regularSite
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Maybe (PowerSite label)
-regularSite label = Map.lookup label . storedRegularSites
-
--- | Enumerate every canonical site in ascending label order, including hidden
--- and coincident sites.
-regularSites :: RegularTriangulation label -> [PowerSite label]
-regularSites = Map.elems . storedRegularSites
-
--- | Number of canonical sites, independent of their visibility.
-regularSiteCount :: RegularTriangulation label -> Int
-regularSiteCount = Map.size . storedRegularSites
-
--- | Observe one site's exhaustive derived visibility disposition.
-regularSiteDisposition
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Maybe (RegularSiteDisposition label)
-regularSiteDisposition label triangulation =
-  storedRegularSection triangulation
-    >>= Map.lookup label . sectionRegularDispositions
-
--- | Enumerate canonical visible faces in ascending key order.
-regularFaces :: RegularTriangulation label -> [RegularFace label]
-regularFaces =
-  maybe [] (Map.elems . sectionRegularFaces) . storedRegularSection
-
--- | Enumerate canonical regular edges in ascending endpoint order.
-regularEdges :: RegularTriangulation label -> [RegularEdge label]
-regularEdges =
-  maybe [] (fmap sectionRegularEdge . Map.elems . sectionRegularEdges)
-    . storedRegularSection
-
--- | Exact neighbours of one visible site; nonvisible and absent sites have none.
-regularNeighbours
-  :: Ord label
-  => label
-  -> RegularTriangulation label
-  -> Set label
-regularNeighbours label =
-  maybe
-    Set.empty
-    ( maybe Set.empty sectionSiteNeighbours
-        . Map.lookup label
-        . sectionRegularStars
-    )
-    . storedRegularSection
-
--- | Construction statistics derived from the sealed normalized section.
-regularTriangulationReceipt
-  :: RegularTriangulation label
-  -> RegularTriangulationReceipt
-regularTriangulationReceipt triangulation =
-  case storedRegularSection triangulation of
-    Nothing -> emptyRegularTriangulationReceipt
-    Just section ->
-      regularReceiptFromSection (regularSiteCount triangulation) section
-
-validateAndSortSites
-  :: Ord label
-  => NonEmpty (PowerSite label)
-  -> Either (PowerDiagramError label) (NonEmpty (PowerSite label))
-validateAndSortSites submitted =
-  let sorted = NonEmpty.sortBy (comparing powerSiteLabel) submitted
-   in case List.find (uncurry sameLabel) (consecutivePairs (NonEmpty.toList sorted)) of
-        Just (duplicate, _) -> Left (DuplicatePowerSiteLabel (powerSiteLabel duplicate))
-        Nothing -> Right sorted
- where
-  sameLabel :: Eq label => PowerSite label -> PowerSite label -> Bool
-  sameLabel left right = powerSiteLabel left == powerSiteLabel right
-
-maximumPowerSiteInputBits :: Map label (PowerSite label) -> Int
-maximumPowerSiteInputBits =
-  Map.foldl' (\bits site -> max bits (powerSiteInputBitWidth site)) 0
-
-constructRegularTriangulation
-  :: Ord label
-  => Map label (PowerSite label)
-  -> Either (RegularTopologyError label) (RegularTriangulation label)
-constructRegularTriangulation sites =
-  case NonEmpty.nonEmpty (Map.elems sites) of
-    Nothing -> Right emptyRegularTriangulation
-    Just sortedSites -> do
-      let generators = fmap (fst . prepareExactPowerGenerator) sortedSites
-      section <- resolvedGeneratorSection generators
-      pure (publishRegularSection sites section)
-
-prepareExactPowerGenerator :: PowerSite label -> (ExactPowerGenerator label, Int)
-prepareExactPowerGenerator site =
-  let point = exactPointFromQueryPoint (powerSiteQueryPoint site)
-      (coordinateX, coordinateY) = exactPointCoordinates point
-      weight = powerWeightExact (powerSiteWeight site)
-   in ( ExactPowerGenerator
-          { exactPowerGeneratorLabel = powerSiteLabel site
-          , exactPowerGeneratorXCoefficient = 2 * coordinateX
-          , exactPowerGeneratorYCoefficient = 2 * coordinateY
-          , exactPowerGeneratorConstant =
-              weight - coordinateX * coordinateX - coordinateY * coordinateY
-          }
-      , max (exactPointBitWidth point) (exactRationalBitWidth weight)
-      )
-
-powerSiteInputBitWidth :: PowerSite label -> Int
-powerSiteInputBitWidth site =
-  max
-    (exactPointBitWidth (exactPointFromQueryPoint (powerSiteQueryPoint site)))
-    (exactRationalBitWidth (powerWeightExact (powerSiteWeight site)))
-
-groupGeneratorsBySlope
-  :: NonEmpty (ExactPowerGenerator label)
-  -> NonEmpty (NonEmpty (ExactPowerGenerator label))
-groupGeneratorsBySlope (initial :| remaining) =
-  let initialSlope = exactGeneratorSlope initial
-      (sameInitialSlope, otherGenerators) =
-        List.partition ((== initialSlope) . exactGeneratorSlope) remaining
-      otherGroups =
-        Map.fromListWith
-          (<>)
-          [ (exactGeneratorSlope generator, generator :| [])
-          | generator <- otherGenerators
-          ]
-   in (initial :| sameInitialSlope) :| Map.elems otherGroups
-
-resolveCoincidentGeneratorGroup
-  :: Ord label
-  => NonEmpty (ExactPowerGenerator label)
-  -> (ExactPowerGenerator label, [(label, CoincidentGeneratorDisposition label)])
-resolveCoincidentGeneratorGroup generators@(initial :| remaining) =
-  let representative = List.foldl' chooseCoincidentRepresentative initial remaining
-      representativeLabel = exactPowerGeneratorLabel representative
-   in ( representative
-      , [ ( exactPowerGeneratorLabel generator
-          , classifyCoincidentGenerator representative generator
-          )
-        | generator <- NonEmpty.toList generators
-        , exactPowerGeneratorLabel generator /= representativeLabel
-        ]
-      )
-
-chooseCoincidentRepresentative
-  :: Ord label
-  => ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> ExactPowerGenerator label
-chooseCoincidentRepresentative selected candidate =
-  case
-    compareCoincidentPriority
-      (exactPowerGeneratorConstant candidate, exactPowerGeneratorLabel candidate)
-      (exactPowerGeneratorConstant selected, exactPowerGeneratorLabel selected) of
-    GT -> candidate
-    _ -> selected
-
-compareCoincidentPriority
-  :: (Ord label, Ord value)
-  => (value, label)
-  -> (value, label)
-  -> Ordering
-compareCoincidentPriority (candidateValue, candidateLabel) (selectedValue, selectedLabel) =
-  compare candidateValue selectedValue <> compare selectedLabel candidateLabel
-
-classifyCoincidentGenerator
-  :: ExactPowerGenerator label
-  -> ExactPowerGenerator label
-  -> CoincidentGeneratorDisposition label
-classifyCoincidentGenerator representative candidate =
-  classifyCoincidentValue
-    (exactPowerGeneratorLabel representative)
-    (exactPowerGeneratorConstant representative)
-    (exactPowerGeneratorConstant candidate)
-
-classifyCoincidentValue
-  :: Eq value
-  => label
-  -> value
-  -> value
-  -> CoincidentGeneratorDisposition label
-classifyCoincidentValue representativeLabel representativeValue candidateValue
-  | candidateValue == representativeValue =
-      CoincidentGeneratorEquivalentTo representativeLabel
-  | otherwise = CoincidentGeneratorDominatedBy representativeLabel
-
-
-coincidentPowerDisposition
-  :: CoincidentGeneratorDisposition label
-  -> PowerCellDisposition label
-coincidentPowerDisposition disposition =
-  case disposition of
-    CoincidentGeneratorEquivalentTo label -> CoincidentEquivalentTo label
-    CoincidentGeneratorDominatedBy label -> CoincidentDominatedBy label
-
-coincidentRegularDisposition
-  :: CoincidentGeneratorDisposition label
-  -> RegularSiteDisposition label
-coincidentRegularDisposition disposition =
-  case disposition of
-    CoincidentGeneratorEquivalentTo label -> RegularSiteCoincidentEquivalentTo label
-    CoincidentGeneratorDominatedBy label -> RegularSiteCoincidentDominatedBy label
-
-
-resolvedGeneratorSection
-  :: Ord label
-  => NonEmpty (ExactPowerGenerator label)
-  -> Either (RegularTopologyError label) (ResolvedGeneratorSection label)
-resolvedGeneratorSection generators = do
-  let resolvedGroups = fmap resolveCoincidentGeneratorGroup (groupGeneratorsBySlope generators)
-      representatives = fmap fst resolvedGroups
-      coincidentDispositions =
-        Map.fromList (concatMap snd (NonEmpty.toList resolvedGroups))
-  topology <- regularGeneratorTopology (DistinctSlopeGenerators representatives)
-  pure
-    ResolvedGeneratorSection
-      { resolvedCoincidentDispositions = coincidentDispositions
-      , resolvedRegularTopology = topology
-      }
-
-publishRegularSection
-  :: Ord label
-  => Map label (PowerSite label)
-  -> ResolvedGeneratorSection label
-  -> RegularTriangulation label
-publishRegularSection sites resolvedSection =
-  let topology = resolvedRegularTopology resolvedSection
-      coincident = resolvedCoincidentDispositions resolvedSection
-      representativeDispositions = publishRepresentativeDispositions topology
-      dispositions =
-        representativeDispositions <> fmap coincidentRegularDisposition coincident
-      generators =
-        Map.fromList
-          [ (exactPowerGeneratorLabel generator, generator)
-          | (generator, _) <- NonEmpty.toList (generatorRegularDispositions topology)
-          ]
-      faces = fmap publishGeneratorFace (generatorRegularFaces topology)
-      faceSection = Map.fromList (fmap (\face -> (regularFaceKey face, face)) faces)
-      edgeFaceIncidence =
-        List.foldl'
-          (Map.unionWith Set.union)
-          Map.empty
-          (fmap regularFaceEdgeIncidence faces)
-      edges = fmap publishGeneratorEdge (generatorRegularEdges topology)
-      edgeSection =
-        Map.fromList
-          [ ( key
-            , RegularEdgeSection
-                edge
-                (Map.findWithDefault Set.empty key edgeFaceIncidence)
-            )
-          | edge <- edges
-          , let key = regularEdgeKey edge
-          ]
-      stars = regularSiteStars dispositions faceSection edgeSection
-      baseSection =
-        RegularSection
-          { sectionGenerators = generators
-          , sectionSlopeRepresentatives =
-              Map.fromList
-                [ (exactGeneratorSlope generator, label)
-                | (label, generator) <- Map.toAscList generators
-                ]
-          , sectionCoincidentDispositions = coincident
-          , sectionRegularDispositions = dispositions
-          , sectionRegularFaces = faceSection
-          , sectionRegularEdges = edgeSection
-          , sectionRegularStars = stars
-          , sectionRegularReceipt = generatorRegularReceipt topology
-          , sectionRegularLocality = Nothing
-          }
-      section =
-        baseSection
-          { sectionRegularLocality = buildRegularLocality baseSection
-          }
-   in RegularTriangulation
-        { storedRegularSites = sites
-        , storedRegularSection = Just section
-        }
-
-regularReceiptFromSection
-  :: Int
-  -> RegularSection label
-  -> RegularTriangulationReceipt
-regularReceiptFromSection inputSites section =
-  let generatorReceipt = sectionRegularReceipt section
-      coincidentCount = Map.size (sectionCoincidentDispositions section)
-   in RegularTriangulationReceipt
-        { regularTriangulationInputSites = inputSites
-        , regularTriangulationRepresentativeSites =
-            generatorRegularInputSites generatorReceipt
-        , regularTriangulationVisibleSites =
-            generatorRegularVisibleSites generatorReceipt
-        , regularTriangulationLowerDimensionalSites =
-            generatorRegularLowerDimensionalSites generatorReceipt
-        , regularTriangulationHiddenSites =
-            generatorRegularHiddenSites generatorReceipt
-        , regularTriangulationCoincidentSites = coincidentCount
-        , regularTriangulationFaces = generatorRegularFaceCount generatorReceipt
-        , regularTriangulationEdges = generatorRegularEdgeCount generatorReceipt
-        }
-
-emptyRegularTriangulationReceipt :: RegularTriangulationReceipt
-emptyRegularTriangulationReceipt =
-  RegularTriangulationReceipt
-    { regularTriangulationInputSites = 0
-    , regularTriangulationRepresentativeSites = 0
-    , regularTriangulationVisibleSites = 0
-    , regularTriangulationLowerDimensionalSites = 0
-    , regularTriangulationHiddenSites = 0
-    , regularTriangulationCoincidentSites = 0
-    , regularTriangulationFaces = 0
-    , regularTriangulationEdges = 0
-    }
-
-emptyGeneratorRegularReceipt :: GeneratorRegularReceipt
-emptyGeneratorRegularReceipt =
-  GeneratorRegularReceipt
-    { generatorRegularInputSites = 0
-    , generatorRegularVisibleSites = 0
-    , generatorRegularLowerDimensionalSites = 0
-    , generatorRegularHiddenSites = 0
-    , generatorRegularFaceCount = 0
-    , generatorRegularEdgeCount = 0
-    }
-
-publishGeneratorDisposition
-  :: RegularGeneratorDisposition
-  -> RegularSiteDisposition label
-publishGeneratorDisposition disposition =
-  case disposition of
-    RegularGeneratorVisible -> RegularSiteVisible
-    RegularGeneratorLowerDimensional -> RegularSiteLowerDimensional
-    RegularGeneratorHidden -> RegularSiteHidden
-
-publishGeneratorFace
-  :: Ord label
-  => GeneratorRegularFace label
-  -> RegularFace label
-publishGeneratorFace face =
-  canonicalRegularFace
-    (generatorRegularFaceFirst face)
-    (generatorRegularFaceSecond face)
-    (generatorRegularFaceThird face)
-    (generatorRegularFaceDualPoint face)
-
-canonicalRegularFace
-  :: Ord label
-  => label
-  -> label
-  -> label
-  -> ExactPoint
-  -> RegularFace label
-canonicalRegularFace firstLabel secondLabel thirdLabel dualPoint
-  | firstLabel <= secondLabel && firstLabel <= thirdLabel =
-      RegularFace firstLabel secondLabel thirdLabel dualPoint
-  | secondLabel <= thirdLabel =
-      RegularFace secondLabel thirdLabel firstLabel dualPoint
-  | otherwise =
-      RegularFace thirdLabel firstLabel secondLabel dualPoint
-
-publishGeneratorEdge :: GeneratorRegularEdge label -> RegularEdge label
-publishGeneratorEdge edge =
-  RegularEdge
-    (exactPowerGeneratorLabel (generatorRegularEdgeFirst edge))
-    (exactPowerGeneratorLabel (generatorRegularEdgeSecond edge))
-    (publishGeneratorDualGeometry (generatorRegularEdgeDual edge))
-
-publishGeneratorDualGeometry :: GeneratorDualGeometry -> PowerDualEdge
-publishGeneratorDualGeometry dual =
-  case dual of
-    GeneratorDualSegment segment -> BoundedPowerDual segment
-    GeneratorDualRay ray -> UnboundedPowerDual ray
-    GeneratorDualLine line -> FullLinePowerDual line
-    GeneratorDualCollapsed point -> CollapsedPowerDual point
-
-regularSiteStars
-  :: Ord label
-  => Map label disposition
-  -> Map (RegularFaceKey label) (RegularFace label)
-  -> Map (RegularEdgeKey label) (RegularEdgeSection label)
-  -> Map label (RegularSiteStar label)
-regularSiteStars dispositions faces =
-  Map.foldl'
-    insertRegularEdgeStar
-    (Map.foldlWithKey' insertRegularFaceStar (emptyRegularStars dispositions) faces)
-
-emptyRegularStars
-  :: Map label disposition
-  -> Map label (RegularSiteStar label)
-emptyRegularStars = Map.map (const emptyRegularSiteStar)
-
-publishRepresentativeDispositions
-  :: Ord label
-  => GeneratorRegularTopology label
-  -> Map label (RegularSiteDisposition label)
-publishRepresentativeDispositions topology =
-  Map.fromList
-    [ ( exactPowerGeneratorLabel generator
-      , publishGeneratorDisposition disposition
-      )
-    | (generator, disposition) <-
-        NonEmpty.toList (generatorRegularDispositions topology)
-    ]
-
-insertRegularFaceStar
-  :: Ord label
-  => Map label (RegularSiteStar label)
-  -> RegularFaceKey label
-  -> RegularFace label
-  -> Map label (RegularSiteStar label)
-insertRegularFaceStar stars faceKey face =
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-   in Foldable.foldl'
-        (\current label -> Map.adjust (addIncidentFace faceKey) label current)
-        stars
-        [firstLabel, secondLabel, thirdLabel]
-
-insertRegularEdgeStar
-  :: Ord label
-  => Map label (RegularSiteStar label)
-  -> RegularEdgeSection label
-  -> Map label (RegularSiteStar label)
-insertRegularEdgeStar stars edgeSection =
-  let (firstLabel, secondLabel) = regularEdgeLabels (sectionRegularEdge edgeSection)
-   in Map.adjust (addNeighbour secondLabel) firstLabel
-        (Map.adjust (addNeighbour firstLabel) secondLabel stars)
-
-addIncidentFace
-  :: Ord label
-  => RegularFaceKey label
-  -> RegularSiteStar label
-  -> RegularSiteStar label
-addIncidentFace faceKey star =
-  star{sectionIncidentFaces = Set.insert faceKey (sectionIncidentFaces star)}
-
-addNeighbour :: Ord label => label -> RegularSiteStar label -> RegularSiteStar label
-addNeighbour label star =
-  star{sectionSiteNeighbours = Set.insert label (sectionSiteNeighbours star)}
diff --git a/src-public/Moonlight/Triangulation/Minkowski.hs b/src-public/Moonlight/Triangulation/Minkowski.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Minkowski.hs
+++ /dev/null
@@ -1,592 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-
--- | Exact polygonal Minkowski addition and regularized two-dimensional
--- morphology. Convex convolution is direct; nonconvex construction descends
--- through the existing exact overlay, resident CDT, and grouped publication
--- owners. Lower-dimensional erosion residuals cannot inhabit 'PlanarRegion'
--- and therefore publish as empty rather than being forged as polygons.
-module Moonlight.Triangulation.Minkowski
-  ( ConvexPolygon
-  , convexPolygon
-  , convexPolygonPoints
-  , StructuringElement
-  , structuringElement
-  , MinkowskiOperation (..)
-  , MinkowskiError (..)
-  , MinkowskiReceipt (..)
-  , convexMinkowskiSum
-  , reflectConvexPolygon
-  , minkowskiSum
-  , erodeBy
-  , openWith
-  , closeWith
-  , polygonOffset
-  , polygonInset
-  ) where
-
-import Control.Applicative ((<|>))
-import Control.Monad (filterM)
-import Data.Bifunctor (first)
-import qualified Data.IntMap.Strict as IntMap
-import qualified Data.List as List
-import qualified Data.Map.Strict as Map
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Maybe (fromMaybe)
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Moonlight.Triangulation.Dcel
-  ( faceData
-  , faceVertices
-  , vertexData
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , exactClipExactIntersections
-  , exactPointCoordinates
-  )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( FaceId
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators (innerFaces)
-import Moonlight.Triangulation.Internal.ExactRational
-  ( exactRationalBitWidth
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle (cyclePairs)
-import Moonlight.Triangulation.Internal.Minkowski.Convex
-  ( addExactPoints
-  , admittedConvexLoop
-  , convexHullPolygon
-  , convexMinkowskiPolygon
-  , convexMinkowskiSum
-  , convexPolygon
-  , convexPolygonCentroid
-  , convexPolygonPoints
-  , convexPolygonRegion
-  , erodeConvexBy
-  , reflectConvexPolygon
-  , structuringElement
-  , structuringElementPolygon
-  )
-import Moonlight.Triangulation.Internal.Minkowski.Types
-import Moonlight.Triangulation.Internal.Overlay.Resident
-  ( faceCarriesExactArea
-  , faceLabels
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-  ( OverlayCell (..)
-  , OverlayCellGeometry (..)
-  , OverlayCellId (..)
-  , OverlayFace (..)
-  , OverlayResult (..)
-  , OverlayVertex (..)
-  )
-import Moonlight.Triangulation.Overlay
-  ( OverlayReceipt (..)
-  , overlayLayers
-  , overlayReceipt
-  , overlaySelectedRegion
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , RegionPointLocation (..)
-  , emptyPlanarRegion
-  , exactLoopPoints
-  , planarLayerRegions
-  , planarRegionComponents
-  , polygonHoleLoops
-  , polygonOuterLoop
-  , regionPointLocation
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayerFromExactCoordinates
-  , planarLayerFromAdmittedComponents
-  )
-
-data MorphologyMetrics = MorphologyMetrics
-  { metricOverlayPasses :: !Int
-  , metricExactCrossings :: !Int
-  , metricOutputCells :: !(Maybe Int)
-  }
-
-emptyMetrics :: MorphologyMetrics
-emptyMetrics = MorphologyMetrics 0 0 Nothing
-
-appendMetrics :: MorphologyMetrics -> MorphologyMetrics -> MorphologyMetrics
-appendMetrics left right =
-  MorphologyMetrics
-    { metricOverlayPasses = metricOverlayPasses left + metricOverlayPasses right
-    , metricExactCrossings = metricExactCrossings left + metricExactCrossings right
-    , metricOutputCells = metricOutputCells right <|> metricOutputCells left
-    }
-
-minkowskiSum
-  :: PlanarRegion
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-minkowskiSum left right = do
-  (leftPieces, leftMetrics) <- decomposeRegion left
-  (rightPieces, rightMetrics) <- decomposeRegion right
-  let generated =
-        [ convexMinkowskiPolygon leftPiece rightPiece
-        | leftPiece <- leftPieces
-        , rightPiece <- rightPieces
-        ]
-      generatedRegions = map convexPolygonRegion generated
-      convolutionEdges = sum (map (NonEmpty.length . convexPolygonPoints) generated)
-  (result, unionMetrics) <- unionRegions generatedRegions
-  let metrics = leftMetrics `appendMetrics` rightMetrics `appendMetrics` unionMetrics
-  pure
-    ( result
-    , MinkowskiReceipt
-        { minkowskiOperation = MinkowskiAddition
-        , minkowskiInputComponents =
-            length (planarRegionComponents left)
-              + length (planarRegionComponents right)
-        , minkowskiConvexPieces = length leftPieces + length rightPieces
-        , minkowskiGeneratedPieces = length generated
-        , minkowskiGeneratedConvolutionEdges = convolutionEdges
-        , minkowskiOverlayPasses = metricOverlayPasses metrics
-        , minkowskiExactCrossings = metricExactCrossings metrics
-        , minkowskiOutputCells = fromMaybe 0 (metricOutputCells metrics)
-        , minkowskiExactCoordinateBitGrowth =
-            coordinateBitGrowth [left, right] result
-        }
-    )
-
--- | Erode a polygonal region by an origin-anchored convex kernel and publish
--- the regularized full-dimensional result. A residual consisting only of
--- points or segments is represented by 'emptyPlanarRegion'.
-erodeBy
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-erodeBy element source =
-  case singleConvexRegion source of
-    Just sourcePolygon -> convexErosion element source sourcePolygon
-    Nothing -> generalErosion element source
-
-openWith
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-openWith element source = do
-  (eroded, erosionReceipt) <- erodeBy element source
-  (opened, additionReceipt) <- polygonOffset element eroded
-  pure
-    ( opened
-    , composeReceipts
-        MinkowskiOpening
-        (length (planarRegionComponents source))
-        erosionReceipt
-        additionReceipt
-    )
-
-closeWith
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-closeWith element source = do
-  (expanded, additionReceipt) <- polygonOffset element source
-  (closed, erosionReceipt) <- erodeBy element expanded
-  pure
-    ( closed
-    , composeReceipts
-        MinkowskiClosing
-        (length (planarRegionComponents source))
-        additionReceipt
-        erosionReceipt
-    )
-
-polygonOffset
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-polygonOffset element source =
-  minkowskiSum source (convexPolygonRegion (structuringElementPolygon element))
-
-polygonInset
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-polygonInset = erodeBy
-
-decomposeRegion
-  :: PlanarRegion
-  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
-decomposeRegion region =
-  case traverse convexComponent (planarRegionComponents region) of
-    Just convexPieces -> Right (convexPieces, emptyMetrics)
-    Nothing -> triangulatedPieces region
-
-convexComponent :: PolygonComponent -> Maybe ConvexPolygon
-convexComponent component =
-  case polygonHoleLoops component of
-    [] -> admittedConvexLoop (polygonOuterLoop component)
-    _ -> Nothing
-
-singleConvexRegion :: PlanarRegion -> Maybe ConvexPolygon
-singleConvexRegion region =
-  case planarRegionComponents region of
-    [component] -> convexComponent component
-    _ -> Nothing
-
-triangulatedPieces
-  :: PlanarRegion
-  -> Either MinkowskiError ([ConvexPolygon], MorphologyMetrics)
-triangulatedPieces region = do
-  let sourceLayer = morphologyLayer region
-  result <- first MinkowskiOverlayFailed (overlayLayers sourceLayer emptyMorphologyLayer)
-  selectedFaces <-
-    filterM
-      (\face ->
-         if faceCarriesExactArea result face
-           then
-             fmap fst
-               ( first MinkowskiOverlayCellWitness
-                   (faceLabels result face)
-               )
-           else Right False)
-      (innerFaces (overlayResultTriangulation result))
-  pieces <- traverse (faceConvexPolygon result) selectedFaces
-  pure (pieces, metricsFromOverlay result (length pieces))
-
-faceConvexPolygon
-  :: OverlayResult leftLabel rightLabel
-  -> FaceId
-  -> Either MinkowskiError ConvexPolygon
-faceConvexPolygon result face =
-  case
-    map
-      (overlayExactPoint . vertexData triangulation)
-      (faceVertices triangulation face) of
-    [firstPoint, secondPoint, thirdPoint] ->
-      convexHullPolygon (firstPoint :| [secondPoint, thirdPoint])
-    vertices -> Left (MinkowskiFaceArity face (length vertices))
- where
-  triangulation = overlayResultTriangulation result
-
-unionRegions
-  :: [PlanarRegion]
-  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
-unionRegions [] = Right (emptyPlanarRegion, emptyMetrics)
-unionRegions [region] =
-  Right
-    ( region
-    , emptyMetrics{metricOutputCells = Just (length (planarRegionComponents region))}
-    )
-unionRegions regions = do
-  let (leftRegions, rightRegions) = splitAt (length regions `div` 2) regions
-  left <- unionRegions leftRegions
-  right <- unionRegions rightRegions
-  glueRegionUnion left right
-
-glueRegionUnion
-  :: (PlanarRegion, MorphologyMetrics)
-  -> (PlanarRegion, MorphologyMetrics)
-  -> Either MinkowskiError (PlanarRegion, MorphologyMetrics)
-glueRegionUnion (left, leftMetrics) (right, rightMetrics)
-  | null (planarRegionComponents left) = Right (right, leftMetrics `appendMetrics` rightMetrics)
-  | null (planarRegionComponents right) = Right (left, leftMetrics `appendMetrics` rightMetrics)
-  | otherwise = do
-      result <-
-        first MinkowskiOverlayFailed
-          (overlayLayers (morphologyLayer left) (morphologyLayer right))
-      published <-
-        first MinkowskiPublicationFailed
-          ( overlaySelectedRegion
-              (uncurry (||))
-              result
-          )
-      let selectedCells =
-            V.foldl'
-              (\count cell ->
-                 case overlayCellGeometry cell of
-                   BoundedOverlayCell _
-                     | overlayCellLeft cell || overlayCellRight cell ->
-                         count + 1
-                   _ -> count)
-              0
-              (overlayResultCells result)
-      pure
-        ( published
-        , leftMetrics
-            `appendMetrics` rightMetrics
-            `appendMetrics` metricsFromOverlay result selectedCells
-        )
-
-convexErosion
-  :: StructuringElement
-  -> PlanarRegion
-  -> ConvexPolygon
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-convexErosion element source sourcePolygon = do
-  (eroded, clipReceipt) <-
-    erodeConvexBy sourcePolygon (structuringElementPolygon element)
-  let result = maybe emptyPlanarRegion convexPolygonRegion eroded
-      outputCells = maybe 0 (const 1) eroded
-      generatedEdges = maybe 0 (NonEmpty.length . convexPolygonPoints) eroded
-  pure
-    ( result
-    , MinkowskiReceipt
-        { minkowskiOperation = MinkowskiErosion
-        , minkowskiInputComponents = 1
-        , minkowskiConvexPieces = 2
-        , minkowskiGeneratedPieces = outputCells
-        , minkowskiGeneratedConvolutionEdges = generatedEdges
-        , minkowskiOverlayPasses = 0
-        , minkowskiExactCrossings =
-            exactClipExactIntersections clipReceipt
-        , minkowskiOutputCells = outputCells
-        , minkowskiExactCoordinateBitGrowth =
-            coordinateBitGrowth
-              [ source
-              , convexPolygonRegion (structuringElementPolygon element)
-              ]
-              result
-        }
-    )
-
-generalErosion
-  :: StructuringElement
-  -> PlanarRegion
-  -> Either MinkowskiError (PlanarRegion, MinkowskiReceipt)
-generalErosion element source
-  | null sourceEdges = Right (emptyPlanarRegion, emptyErosionReceipt)
-  | otherwise = do
-      sweptPolygons <- traverse (sweepBoundaryEdge reflectedKernel) sourceEdges
-      let sweptRegions = map convexPolygonRegion sweptPolygons
-          generatedEdges =
-            sum (map (NonEmpty.length . convexPolygonPoints) sweptPolygons)
-      (contactRegion, unionMetrics) <- unionRegions sweptRegions
-      candidateOverlay <-
-        first MinkowskiOverlayFailed
-          (overlayLayers (morphologyLayer contactRegion) emptyMorphologyLayer)
-      kernelWitness <- convexPolygonCentroid kernel
-      let representativeFaceByCell = representativeFaces candidateOverlay
-      selectedCellIds <-
-        Set.fromList
-          <$> filterM
-            ( classifyCandidateCell
-                source
-                kernelWitness
-                representativeFaceByCell
-                candidateOverlay
-            )
-            (boundedOutsideCellIds candidateOverlay)
-      published <- publishCellSelection selectedCellIds candidateOverlay
-      let candidateMetrics =
-            metricsFromOverlay candidateOverlay (Set.size selectedCellIds)
-          metrics = unionMetrics `appendMetrics` candidateMetrics
-      pure
-        ( published
-        , MinkowskiReceipt
-            { minkowskiOperation = MinkowskiErosion
-            , minkowskiInputComponents = length (planarRegionComponents source)
-            , minkowskiConvexPieces = 1
-            , minkowskiGeneratedPieces = length sweptPolygons
-            , minkowskiGeneratedConvolutionEdges = generatedEdges
-            , minkowskiOverlayPasses = metricOverlayPasses metrics
-            , minkowskiExactCrossings = metricExactCrossings metrics
-            , minkowskiOutputCells = Set.size selectedCellIds
-            , minkowskiExactCoordinateBitGrowth =
-                coordinateBitGrowth
-                  [source, convexPolygonRegion kernel]
-                  published
-            }
-        )
- where
-  kernel = structuringElementPolygon element
-  reflectedKernel = reflectConvexPolygon kernel
-  sourceEdges = regionBoundaryEdges source
-  emptyErosionReceipt =
-    MinkowskiReceipt
-      { minkowskiOperation = MinkowskiErosion
-      , minkowskiInputComponents = 0
-      , minkowskiConvexPieces = 1
-      , minkowskiGeneratedPieces = 0
-      , minkowskiGeneratedConvolutionEdges = 0
-      , minkowskiOverlayPasses = 0
-      , minkowskiExactCrossings = 0
-      , minkowskiOutputCells = 0
-      , minkowskiExactCoordinateBitGrowth = 0
-      }
-
-sweepBoundaryEdge
-  :: ConvexPolygon
-  -> (ExactPoint, ExactPoint)
-  -> Either MinkowskiError ConvexPolygon
-sweepBoundaryEdge reflectedKernel (from, to) =
-  case convexPolygonPoints reflectedKernel of
-    firstKernelPoint :| remainingKernelPoints ->
-      convexHullPolygon
-        ( addExactPoints from firstKernelPoint
-            :| ( map (addExactPoints from) remainingKernelPoints
-                   <> map (addExactPoints to) kernelPoints
-               )
-        )
- where
-  kernelPoints = NonEmpty.toList (convexPolygonPoints reflectedKernel)
-
-boundedOutsideCellIds
-  :: OverlayResult Bool Bool
-  -> [OverlayCellId]
-boundedOutsideCellIds result =
-  V.ifoldr
-    (\index cell selected ->
-       case overlayCellGeometry cell of
-         BoundedOverlayCell _
-           | not (overlayCellLeft cell)
-               && not (overlayCellRight cell) ->
-               OverlayCellId index : selected
-         _ -> selected)
-    []
-    (overlayResultCells result)
-
-classifyCandidateCell
-  :: PlanarRegion
-  -> ExactPoint
-  -> IntMap.IntMap FaceId
-  -> OverlayResult Bool Bool
-  -> OverlayCellId
-  -> Either MinkowskiError Bool
-classifyCandidateCell source kernelWitness representativeFaceByCell result cellId = do
-  face <-
-    maybe
-      (Left (MinkowskiCandidateCellMissing cellId))
-      Right
-      (IntMap.lookup (overlayCellIndex cellId) representativeFaceByCell)
-  candidate <- convexPolygonCentroid =<< faceConvexPolygon result face
-  let inclusionWitness = addExactPoints candidate kernelWitness
-  case regionPointLocation source inclusionWitness of
-    RegionInterior -> Right True
-    RegionExterior -> Right False
-    RegionOnBoundary -> Left (MinkowskiInclusionAmbiguous cellId inclusionWitness)
-
-representativeFaces
-  :: OverlayResult leftLabel rightLabel
-  -> IntMap.IntMap FaceId
-representativeFaces result =
-  IntMap.fromListWith min
-    [ (overlayCellIndex (overlayFaceCellId (faceData triangulation face)), face)
-    | face <- innerFaces triangulation
-    , faceCarriesExactArea result face
-    ]
- where
-  triangulation = overlayResultTriangulation result
-
-overlayCellIndex :: OverlayCellId -> Int
-overlayCellIndex (OverlayCellId index) = index
-
-publishCellSelection
-  :: Set.Set OverlayCellId
-  -> OverlayResult leftLabel rightLabel
-  -> Either MinkowskiError PlanarRegion
-publishCellSelection selected result = do
-  layer <-
-    first MinkowskiPublicationFailed
-      ( labelledPlanarLayerFromExactCoordinates
-          False
-          triangulation
-          (\vertex -> Right (overlayExactPoint (vertexData triangulation vertex)))
-          (\face ->
-             if faceCarriesExactArea result face
-               then
-                 Right
-                   ( Set.member
-                       (overlayFaceCellId (faceData triangulation face))
-                       selected
-                   )
-               else Right False)
-      )
-  pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions layer))
- where
-  triangulation = overlayResultTriangulation result
-
-morphologyLayer
-  :: PlanarRegion
-  -> PlanarLayer Bool
-morphologyLayer region =
-  planarLayerFromAdmittedComponents
-    False
-    [(True, component) | component <- planarRegionComponents region]
-
-emptyMorphologyLayer :: PlanarLayer Bool
-emptyMorphologyLayer = morphologyLayer emptyPlanarRegion
-
-regionBoundaryEdges :: PlanarRegion -> [(ExactPoint, ExactPoint)]
-regionBoundaryEdges region =
-  concatMap
-    (\component ->
-       concatMap
-         (cyclePairs . exactLoopPoints)
-         (polygonOuterLoop component : polygonHoleLoops component))
-    (planarRegionComponents region)
-
-metricsFromOverlay
-  :: OverlayResult leftLabel rightLabel
-  -> Int
-  -> MorphologyMetrics
-metricsFromOverlay result outputCells =
-  MorphologyMetrics
-    { metricOverlayPasses = 1
-    , metricExactCrossings = overlayExactCrossings (overlayReceipt result)
-    , metricOutputCells = Just outputCells
-    }
-
-composeReceipts
-  :: MinkowskiOperation
-  -> Int
-  -> MinkowskiReceipt
-  -> MinkowskiReceipt
-  -> MinkowskiReceipt
-composeReceipts operation inputComponents firstReceipt secondReceipt =
-  MinkowskiReceipt
-    { minkowskiOperation = operation
-    , minkowskiInputComponents = inputComponents
-    , minkowskiConvexPieces =
-        minkowskiConvexPieces firstReceipt
-          + minkowskiConvexPieces secondReceipt
-    , minkowskiGeneratedPieces =
-        minkowskiGeneratedPieces firstReceipt
-          + minkowskiGeneratedPieces secondReceipt
-    , minkowskiGeneratedConvolutionEdges =
-        minkowskiGeneratedConvolutionEdges firstReceipt
-          + minkowskiGeneratedConvolutionEdges secondReceipt
-    , minkowskiOverlayPasses =
-        minkowskiOverlayPasses firstReceipt
-          + minkowskiOverlayPasses secondReceipt
-    , minkowskiExactCrossings =
-        minkowskiExactCrossings firstReceipt
-          + minkowskiExactCrossings secondReceipt
-    , minkowskiOutputCells = minkowskiOutputCells secondReceipt
-    , minkowskiExactCoordinateBitGrowth =
-        max
-          (minkowskiExactCoordinateBitGrowth firstReceipt)
-          (minkowskiExactCoordinateBitGrowth secondReceipt)
-    }
-
-coordinateBitGrowth :: [PlanarRegion] -> PlanarRegion -> Int
-coordinateBitGrowth inputs output =
-  max 0
-    ( regionCoordinateBits output
-        - List.foldl' (\maximumBits -> max maximumBits . regionCoordinateBits) 0 inputs
-    )
-
-regionCoordinateBits :: PlanarRegion -> Int
-regionCoordinateBits = List.foldl' componentBits 0 . planarRegionComponents
- where
-  componentBits maximumBits component =
-    List.foldl'
-      loopBits
-      maximumBits
-      (polygonOuterLoop component : polygonHoleLoops component)
-  loopBits maximumBits =
-    List.foldl' pointBits maximumBits . exactLoopPoints
-  pointBits maximumBits point =
-    let (x, y) = exactPointCoordinates point
-     in max
-          maximumBits
-          (max (exactRationalBitWidth x) (exactRationalBitWidth y))
diff --git a/src-public/Moonlight/Triangulation/Overlay.hs b/src-public/Moonlight/Triangulation/Overlay.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/Overlay.hs
+++ /dev/null
@@ -1,468 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact labelled common refinement. Source boundaries descend through one
--- exact segment-event plan, glue into canonical atomic constraints, and are
--- admitted only when their binary64 DCEL realization preserves every exact
--- relation.
-module Moonlight.Triangulation.Overlay
-  ( BoundaryLoopRef (..)
-  , OverlayOperand
-  , BoundaryRef
-  , BoundaryVertexRef
-  , BoundaryEdgeRef
-  , boundaryRefComponent
-  , boundaryRefLoop
-  , boundaryRefLocalIndex
-  , OverlayVertexOrigin
-  , overlayOriginLeftVertices
-  , overlayOriginRightVertices
-  , overlayOriginLeftEdges
-  , overlayOriginRightEdges
-  , OverlayEdgeOrigin
-  , overlayEdgeLeftSources
-  , overlayEdgeRightSources
-  , OverlaySupport
-  , overlaySupportLabels
-  , OverlayCellSupport (..)
-  , OverlayCellId (..)
-  , OverlayCellGeometry (..)
-  , OverlayCell (..)
-  , OverlayFace (..)
-  , OverlayVertex (..)
-  , OverlayEdge (..)
-  , OverlayReceipt (..)
-  , OverlayArrangementObstruction (..)
-  , OverlayCellWitness (..)
-  , OverlayError (..)
-  , OverlayResult
-  , OverlaySelectionKind (..)
-  , OverlaySelectionError (..)
-  , CoverGap
-  , coverGapRegion
-  , LayerCoverageError (..)
-  , overlayLayers
-  , overlayAll
-  , overlayEmbeddedTriangulation
-  , overlayReceipt
-  , overlayCells
-  , foldBoundedOverlayCells
-  , overlayArrangementVertices
-  , overlayArrangementEdges
-  , overlayPlanarLayer
-  , overlaySelectedRegion
-  , overlayMass
-  , overlayConfusion
-  , layerCovers
-  , overlayClosedUnion
-  , overlayClosedIntersection
-  , overlayRegularizedDifference
-  ) where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Control.Monad (filterM)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.CellSet
-  ( CellSelectionError
-  , ExactCellSet
-  )
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.Dcel (vertexData)
-import Moonlight.Triangulation.Exact (ExactPoint)
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( UndirectedEdgeId
-  , VertexId
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( innerFaces
-  , undirectedEdges
-  , vertices
-  )
-import Moonlight.Triangulation.Internal.CellSet (closeExactCellSetWith)
-import Moonlight.Triangulation.Internal.Overlay.Arrangement
-  ( certifyArrangement
-  )
-import Moonlight.Triangulation.Internal.Overlay.Resident
-  ( OverlayDiagonalSchedule (CanonicalOverlayDiagonals)
-  , edgeSupport
-  , faceCarriesExactArea
-  , faceLabels
-  , regionFaceLabels
-  , residentOverlay
-  , vertexSupport
-  )
-import Moonlight.Triangulation.Internal.Overlay.Types
-import Moonlight.Triangulation.Internal.Representation (Triangulation)
-import Moonlight.Triangulation.Internal.Types
-  ( ConstraintMode (Constrained)
-  )
-import Moonlight.Triangulation.Internal.Tournament
-  ( interpretTournament
-  , planTournament
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , RegionPublicationError (..)
-  , emptyPlanarRegion
-  , planarLayerOutsideLabel
-  , planarLayerRegions
-  , planarRegionComponents
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayerFromExactCoordinates
-  , planarLayerFromAdmittedComponents
-  )
-import Moonlight.Triangulation.Valuation
-  ( ExactArea
-  , polygonComponentArea
-  )
-
--- | Exact full-dimensional part of a requested window not owned by any
--- bounded label region in the candidate layer.
-newtype CoverGap = CoverGap PlanarRegion
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
-coverGapRegion :: CoverGap -> PlanarRegion
-coverGapRegion (CoverGap region) = region
-
-data LayerCoverageError label
-  = LayerCoverageOverlayFailed !(OverlayError Bool label)
-  | LayerCoverageGapPublicationFailed !RegionPublicationError
-  | LayerCoverageGap !CoverGap
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Construct the exact common refinement and its one faithful resident DCEL.
-overlayLayers
-  :: (Ord leftLabel, Ord rightLabel)
-  => PlanarLayer leftLabel
-  -> PlanarLayer rightLabel
-  -> Either
-      (OverlayError leftLabel rightLabel)
-      (OverlayResult leftLabel rightLabel)
-overlayLayers leftLayer rightLayer = do
-  certified <- certifyArrangement leftLayer rightLayer
-  residentOverlay
-    CanonicalOverlayDiagonals
-    (planarLayerOutsideLabel leftLayer, planarLayerOutsideLabel rightLayer)
-    certified
-
--- | Construct the common refinement of a nonempty family in input order.
--- Adjacent layers glue in a balanced tree, so an early boundary participates
--- in logarithmically many intermediate arrangements rather than every later
--- left-associated step. Each intermediate layer is projected directly from
--- its certified binary result.
-overlayAll
-  :: Ord label
-  => NonEmpty (PlanarLayer label)
-  -> Either
-      (OverlayError (NonEmpty label) (NonEmpty label))
-      (PlanarLayer (NonEmpty label))
-overlayAll =
-  interpretTournament overlayLayerPair
-    . planTournament
-    . fmap singletonLabelLayer
-
-overlayLayerPair
-  :: Ord label
-  => PlanarLayer (NonEmpty label)
-  -> PlanarLayer (NonEmpty label)
-  -> Either
-      (OverlayError (NonEmpty label) (NonEmpty label))
-      (PlanarLayer (NonEmpty label))
-overlayLayerPair leftLayer rightLayer =
-  flattenNaryOverlay <$> overlayLayers leftLayer rightLayer
-
-singletonLabelLayer :: Ord label => PlanarLayer label -> PlanarLayer (NonEmpty label)
-singletonLabelLayer layer =
-  planarLayerFromAdmittedComponents
-    (planarLayerOutsideLabel layer :| [])
-    [ (label :| [], component)
-    | (label, region) <- Map.toAscList (planarLayerRegions layer)
-    , component <- planarRegionComponents region
-    ]
-
-flattenNaryOverlay
-  :: Ord label
-  => OverlayResult (NonEmpty label) (NonEmpty label)
-  -> PlanarLayer (NonEmpty label)
-flattenNaryOverlay = projectOverlayLayerWith (uncurry (<>))
-
-projectOverlayLayerWith
-  :: (Eq leftLabel, Eq rightLabel, Ord projectedLabel)
-  => ((leftLabel, rightLabel) -> projectedLabel)
-  -> OverlayResult leftLabel rightLabel
-  -> PlanarLayer projectedLabel
-projectOverlayLayerWith projectLabel result =
-  planarLayerFromAdmittedComponents
-    (projectLabel (overlayResultOutsideLabels result))
-    [ (projectLabel (overlayCellLabels cell), component)
-    | cell <- V.toList (overlayResultCells result)
-    , overlayCellLabels cell /= overlayResultOutsideLabels result
-    , BoundedOverlayCell component <- [overlayCellGeometry cell]
-    ]
-
-overlayCellLabels :: OverlayCell leftLabel rightLabel -> (leftLabel, rightLabel)
-overlayCellLabels cell = (overlayCellLeft cell, overlayCellRight cell)
-{-# INLINE overlayCellLabels #-}
-
--- | The binary64 realization used by existing DCEL observations. Exact overlay
--- operations accept 'OverlayResult', never this projection.
-overlayEmbeddedTriangulation
-  :: OverlayResult leftLabel rightLabel
-  -> Triangulation
-      'Constrained
-      OverlayVertex
-      ()
-      OverlayEdge
-      OverlayFace
-overlayEmbeddedTriangulation = overlayResultTriangulation
-
-overlayReceipt :: OverlayResult leftLabel rightLabel -> OverlayReceipt
-overlayReceipt = overlayResultReceipt
-
-overlayCells
-  :: OverlayResult leftLabel rightLabel
-  -> [(OverlayCellId, OverlayCell leftLabel rightLabel)]
-overlayCells result =
-  V.toList
-    (V.imap (\index cell -> (OverlayCellId index, cell)) (overlayResultCells result))
-
-overlayArrangementVertices
-  :: OverlayResult leftLabel rightLabel
-  -> [(VertexId, OverlayVertex)]
-overlayArrangementVertices result =
-  let triangulation = overlayResultTriangulation result
-   in [(vertex, vertexData triangulation vertex) | vertex <- vertices triangulation]
-
-overlayArrangementEdges
-  :: OverlayResult leftLabel rightLabel
-  -> [(UndirectedEdgeId, OverlayEdgeOrigin)]
-overlayArrangementEdges result =
-  let triangulation = overlayResultTriangulation result
-   in [ (edge, origin)
-      | edge <- undirectedEdges triangulation
-      , OverlayBoundary origin <- [Dcel.undirectedEdgeData triangulation edge]
-      ]
-
--- | Publish the already-admitted bounded cell geometry. The resident DCEL is
--- a realization of these exact cells, not a second authoring source.
-overlayPlanarLayer
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> PlanarLayer (leftLabel, rightLabel)
-overlayPlanarLayer = projectOverlayLayerWith id
-
--- | Publish the selected two-dimensional cells. Internal arrangement edges
--- between differently labelled but jointly selected cells dissolve because
--- selection precedes component descent.
-overlaySelectedRegion
-  :: ((leftLabel, rightLabel) -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either RegionPublicationError PlanarRegion
-overlaySelectedRegion selected result
-  | selected (overlayResultOutsideLabels result) = Left RegionUnboundedSelection
-  | otherwise = do
-      published <-
-        labelledPlanarLayerFromExactCoordinates
-          False
-          triangulation
-          exactPointAt
-          labelFace
-      pure (Map.findWithDefault emptyPlanarRegion True (planarLayerRegions published))
- where
- triangulation = overlayResultTriangulation result
- exactPointAt :: VertexId -> Either RegionPublicationError ExactPoint
- exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
- labelFace face
-   | faceCarriesExactArea result face =
-       selected <$> regionFaceLabels result face
-   | otherwise = Right False
-
--- | Sum selected exact bounded cells without publishing a polygonal union or
--- computing its perimeter. Selecting the unbounded outside pair is refused by
--- the same obstruction used by 'overlaySelectedRegion'.
-overlayMass
-  :: ((leftLabel, rightLabel) -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either RegionPublicationError ExactArea
-overlayMass selected result
-  | selected (overlayResultOutsideLabels result) = Left RegionUnboundedSelection
-  | otherwise =
-      Right (foldBoundedOverlayCells selectedCellArea mempty result)
- where
-  selectedCellArea area labels component
-    | selected labels = area <> polygonComponentArea component
-    | otherwise = area
-
--- | Exact finite confusion masses, grouped by label pair in one traversal.
--- The outside pair is absent because its complete planar cell is unbounded;
--- bounded cavities carrying that same pair are not misreported as its total.
-overlayConfusion
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlayResult leftLabel rightLabel
-  -> Map.Map (leftLabel, rightLabel) ExactArea
-overlayConfusion result =
-  foldBoundedOverlayCells accumulateCell Map.empty result
- where
-  outsideLabels = overlayResultOutsideLabels result
-  accumulateCell masses labels component
-    | labels == outsideLabels = masses
-    | otherwise = Map.insertWith (<>) labels (polygonComponentArea component) masses
-
-foldBoundedOverlayCells
-  :: (accumulator -> (leftLabel, rightLabel) -> PolygonComponent -> accumulator)
-  -> accumulator
-  -> OverlayResult leftLabel rightLabel
-  -> accumulator
-foldBoundedOverlayCells accumulate initial result =
-  V.foldl' descend initial (overlayResultCells result)
- where
-  descend accumulator cell =
-    case overlayCellGeometry cell of
-      BoundedOverlayCell component ->
-        accumulate accumulator (overlayCellLabels cell) component
-      UnboundedOverlayCell _ -> accumulator
-{-# INLINE foldBoundedOverlayCells #-}
-
--- | Certify full-dimensional coverage of an admitted polygonal window. The
--- success path inspects certified cells only; an exact gap region is glued
--- solely when the obstruction actually exists.
-layerCovers
-  :: Ord label
-  => PlanarLayer label
-  -> PolygonComponent
-  -> Either (LayerCoverageError label) ()
-layerCovers layer window = do
-  let outside = planarLayerOutsideLabel layer
-      windowLayer = planarLayerFromAdmittedComponents False [(True, window)]
-  result <- first LayerCoverageOverlayFailed (overlayLayers windowLayer layer)
-  if V.any (isGapCell outside) (overlayResultCells result)
-    then do
-      gap <-
-        first LayerCoverageGapPublicationFailed
-          ( overlaySelectedRegion
-              (\(insideWindow, label) ->
-                 insideWindow && label == outside)
-              result
-          )
-      Left (LayerCoverageGap (CoverGap gap))
-    else Right ()
-
-isGapCell :: Eq label => label -> OverlayCell Bool label -> Bool
-isGapCell outside cell =
-  overlayCellLeft cell
-    && overlayCellRight cell == outside
-    && case overlayCellGeometry cell of
-      BoundedOverlayCell _ -> True
-      UnboundedOverlayCell _ -> False
-
-overlayClosedUnion
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayClosedUnion selectLeft selectRight =
-  selectClosedCells
-    ClosedUnionSelection
-    (\support -> supportAny selectLeft (overlaySupportLeft support) || supportAny selectRight (overlaySupportRight support))
-    (\leftLabel rightLabel -> selectLeft leftLabel || selectRight rightLabel)
-
-overlayClosedIntersection
-  :: (Ord leftLabel, Ord rightLabel)
-  => (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayClosedIntersection selectLeft selectRight =
-  selectClosedCells
-    ClosedIntersectionSelection
-    (\support -> supportAny selectLeft (overlaySupportLeft support) && supportAny selectRight (overlaySupportRight support))
-    (\leftLabel rightLabel -> selectLeft leftLabel && selectRight rightLabel)
-
-overlayRegularizedDifference
-  :: (leftLabel -> Bool)
-  -> (rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-overlayRegularizedDifference selectLeft selectRight result =
-  let outsidePair = overlayResultOutsideLabels result
-      selectFace leftLabel rightLabel = selectLeft leftLabel && not (selectRight rightLabel)
-   in if uncurry selectFace outsidePair
-        then Left (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
-        else closeSelectedCells [] [] selectFace result
-
-selectClosedCells
-  :: (Ord leftLabel, Ord rightLabel)
-  => OverlaySelectionKind
-  -> (OverlayCellSupport leftLabel rightLabel -> Bool)
-  -> (leftLabel -> rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-selectClosedCells selectionKind selectSupport selectFace result =
-  let outsidePair = overlayResultOutsideLabels result
-      triangulation = overlayResultTriangulation result
-   in if uncurry selectFace outsidePair
-        then Left (OverlaySelectionContainsUnboundedCell selectionKind)
-        else do
-          selectedVertices <-
-            filterM
-              ( fmap selectSupport
-                  . first OverlaySelectionProvenance
-                  . vertexSupport result
-              )
-              (vertices triangulation)
-          selectedEdges <-
-            filterM
-              (\edge ->
-                 case Dcel.undirectedEdgeData triangulation edge of
-                   OverlayDiagonal -> Right False
-                   OverlayBoundary _ ->
-                     selectSupport
-                       <$> first OverlaySelectionProvenance (edgeSupport result edge))
-              (undirectedEdges triangulation)
-          closeSelectedCells selectedVertices selectedEdges selectFace result
-
-closeSelectedCells
-  :: [VertexId]
-  -> [UndirectedEdgeId]
-  -> (leftLabel -> rightLabel -> Bool)
-  -> OverlayResult leftLabel rightLabel
-  -> Either OverlaySelectionError ExactCellSet
-closeSelectedCells selectedVertices selectedEdges selectFace result =
-  let triangulation = overlayResultTriangulation result
-      exactPointAt :: VertexId -> Either CellSelectionError ExactPoint
-      exactPointAt vertex = Right (overlayExactPoint (vertexData triangulation vertex))
-   in do
-        selectedFaces <-
-          filterM
-            (\face ->
-               if faceCarriesExactArea result face
-                 then
-                   fmap (uncurry selectFace)
-                     ( first OverlaySelectionProvenance
-                         (faceLabels result face)
-                     )
-                 else Right False)
-            (innerFaces triangulation)
-        first OverlaySelectionInvalid
-          ( closeExactCellSetWith
-              exactPointAt
-              triangulation
-              selectedVertices
-              selectedEdges
-              selectedFaces
-          )
-
-supportAny :: (label -> Bool) -> OverlaySupport label -> Bool
-supportAny predicate = any predicate . overlaySupportLabels
diff --git a/src-public/Moonlight/Triangulation/PowerDiagram.hs b/src-public/Moonlight/Triangulation/PowerDiagram.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/PowerDiagram.hs
+++ /dev/null
@@ -1,64 +0,0 @@
--- | Exact regular topology and power cells. The complete labelled weighted-site
--- section is authoritative; visibility, incidence, and bounded planar layers
--- are derived views.
-module Moonlight.Triangulation.PowerDiagram
-  ( PowerWeight
-  , PowerWeightError (..)
-  , powerWeight
-  , powerWeightFromExact
-  , powerWeightExact
-  , PowerSite
-  , powerSite
-  , powerSiteLabel
-  , powerSitePosition
-  , powerSiteExactPosition
-  , powerSiteWeight
-  , PowerCellDisposition (..)
-  , RegularSiteDisposition (..)
-  , RegularFace
-  , regularFaceLabels
-  , regularFaceDualPoint
-  , PowerDualEdge (..)
-  , RegularEdge
-  , regularEdgeLabels
-  , regularEdgeDual
-  , RegularTriangulation
-  , emptyRegularTriangulation
-  , regularTriangulation
-  , regularSite
-  , regularSites
-  , regularSiteCount
-  , regularSiteDisposition
-  , regularFaces
-  , regularEdges
-  , regularNeighbours
-  , regularTriangulationReceipt
-  , RegularTriangulationReceipt (..)
-  , RegularSiteTransition (..)
-  , RegularEditError (..)
-  , RegularEditResult (..)
-  , insertRegularSite
-  , removeRegularSite
-  , reweightRegularSites
-  , BoundedPowerDiagram
-  , boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , powerCellDisposition
-  , powerCellDispositions
-  , powerDiagramPlanarLayer
-  , PowerDiagramError (..)
-  , RegularTopologyError (..)
-  , PowerDiagramReceipt (..)
-  , powerDiagramInputSites
-  , powerDiagramPeakIntermediateBitGrowth
-  , powerDiagramFinalCoordinateBitGrowth
-  , AffineForm (..)
-  , UpperEnvelopeError (..)
-  , upperEnvelope
-  ) where
-
-import Moonlight.Triangulation.Internal.PowerDiagram.Edit
-import Moonlight.Triangulation.Internal.PowerDiagram.Generator (RegularTopologyError (..))
-import Moonlight.Triangulation.Internal.PowerDiagram.Model
-import Moonlight.Triangulation.Internal.PowerDiagram.Projection
-import Moonlight.Triangulation.Internal.PowerDiagram.Section
diff --git a/src-public/Moonlight/Triangulation/RegularAlpha.hs b/src-public/Moonlight/Triangulation/RegularAlpha.hs
deleted file mode 100644
--- a/src-public/Moonlight/Triangulation/RegularAlpha.hs
+++ /dev/null
@@ -1,393 +0,0 @@
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingStrategies #-}
-
--- | Exact weighted alpha filtration of a full-dimensional regular
--- subdivision. Births are signed power radii; Homology lowering remains in
--- @moonlight-planar:cell-complex@.
-module Moonlight.Triangulation.RegularAlpha
-  ( PowerAlphaBirth
-  , powerAlphaBirthExact
-  , powerAlphaBirthNumerator
-  , powerAlphaBirthDenominator
-  , RegularAlphaFiltration
-  , RegularAlphaError (..)
-  , regularAlphaFiltration
-  , regularAlphaComplex
-  , regularAlphaBirths
-  , regularAlphaSimplexBirth
-  , regularAlphaCriticalBirths
-  , regularAlphaComplexAtBirth
-  )
-where
-
-import Control.DeepSeq (NFData)
-import Data.Bifunctor (first)
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Map.Strict (Map)
-import Data.Map.Strict qualified as Map
-import Data.Set (Set)
-import Data.Set qualified as Set
-import GHC.Generics (Generic)
-import Moonlight.Triangulation.Exact
-  ( ExactAffineLine
-  , ExactArithmeticError
-  , ExactPoint
-  , ExactRational
-  , ExactVector (..)
-  , exactAffineLineCoefficients
-  , exactDivide
-  , exactPoint
-  , exactPointCoordinates
-  , exactRayDirection
-  , exactRayOrigin
-  , exactRationalDenominator
-  , exactRationalNumerator
-  , exactSegmentEndpoints
-  )
-import Moonlight.Triangulation.PowerDiagram
-  ( PowerDualEdge (..)
-  , PowerSite
-  , RegularEdge
-  , RegularFace
-  , RegularSiteDisposition (..)
-  , RegularTriangulation
-  , powerSiteExactPosition
-  , powerSiteLabel
-  , powerSiteWeight
-  , powerWeightExact
-  , regularEdgeDual
-  , regularEdgeLabels
-  , regularEdges
-  , regularFaceDualPoint
-  , regularFaceLabels
-  , regularFaces
-  , regularNeighbours
-  , regularSite
-  , regularSiteDisposition
-  , regularSites
-  )
-import Moonlight.Triangulation.Simplex
-  ( PlanarComplex
-  , PlanarComplexError
-  , PlanarSimplex
-  , PlanarSimplexError
-  , planarComplex
-  , planarEdge
-  , planarFace
-  , planarSimplexVertices
-  , planarVertex
-  )
-
--- | Signed exact power radius. Unlike ordinary alpha birth, negative values
--- are lawful when a positive site weight already contains the simplex at
--- negative power level.
-newtype PowerAlphaBirth = PowerAlphaBirth ExactRational
-  deriving stock (Eq, Ord, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Recover the reduced exact power radius.
-powerAlphaBirthExact :: PowerAlphaBirth -> ExactRational
-powerAlphaBirthExact (PowerAlphaBirth value) = value
-
--- | Numerator of the reduced exact power radius.
-powerAlphaBirthNumerator :: PowerAlphaBirth -> Integer
-powerAlphaBirthNumerator = exactRationalNumerator . powerAlphaBirthExact
-
--- | Positive denominator of the reduced exact power radius.
-powerAlphaBirthDenominator :: PowerAlphaBirth -> Integer
-powerAlphaBirthDenominator = exactRationalDenominator . powerAlphaBirthExact
-
--- | One admitted labelled regular complex and its total exact birth section.
-data RegularAlphaFiltration label = RegularAlphaFiltration
-  { regularAlphaComplex :: !(PlanarComplex label)
-    -- ^ Canonical full-dimensional regular complex.
-  , regularAlphaBirths :: !(Map (PlanarSimplex label) PowerAlphaBirth)
-    -- ^ Total exact birth section over that complex.
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Typed topology, dual-consistency, projection, and sublevel obstructions.
-data RegularAlphaError label
-  = RegularAlphaSiteMissing !label
-  | RegularAlphaSimplexInvalid !(PlanarSimplexError label)
-  | RegularAlphaComplexInvalid !(PlanarComplexError label)
-  | RegularAlphaDualMismatch
-      !(PlanarSimplex label)
-      !ExactRational
-      !ExactRational
-  | RegularAlphaProjectionFailed
-      !(PlanarSimplex label)
-      !ExactArithmeticError
-  | RegularAlphaVertexBirthMissing !label
-  | RegularAlphaSublevelInvalid !(PlanarComplexError label)
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
--- | Derive one exact weighted-alpha birth section from the resident regular dual.
-regularAlphaFiltration
-  :: Ord label
-  => RegularTriangulation label
-  -> Either (RegularAlphaError label) (RegularAlphaFiltration label)
-regularAlphaFiltration triangulation = do
-  let visibleLabels =
-        Set.fromAscList
-          [ powerSiteLabel site
-          | site <- regularSites triangulation
-          , regularSiteDisposition (powerSiteLabel site) triangulation
-              == Just RegularSiteVisible
-          ]
-      visibleEdges = filter (edgeIsVisible visibleLabels) (regularEdges triangulation)
-      visibleFaces = filter (faceIsVisible visibleLabels) (regularFaces triangulation)
-  edgeSimplices <- traverse regularEdgeSimplex visibleEdges
-  faceSimplices <- traverse regularFaceSimplex visibleFaces
-  complexValue <-
-    first RegularAlphaComplexInvalid
-      ( planarComplex
-          ( Set.unions
-              [ Set.map planarVertex visibleLabels
-              , Set.fromList edgeSimplices
-              , Set.fromList faceSimplices
-              ]
-          )
-      )
-  faceBirthSection <-
-    Map.fromList <$> traverse (regularFaceBirth triangulation) visibleFaces
-  edgeBirthEntries <- traverse (regularEdgeBirth triangulation) visibleEdges
-  let edgeBirthSection = Map.fromList edgeBirthEntries
-      incidentEdgeBirths =
-        Map.fromListWith min (edgeBirthEntries >>= incidentEdgeBirthsFor)
-  vertexBirthSection <-
-    Map.fromList
-      <$> traverse
-        (regularVertexBirth triangulation incidentEdgeBirths)
-        (Set.toAscList visibleLabels)
-  pure
-    RegularAlphaFiltration
-      { regularAlphaComplex = complexValue
-      , regularAlphaBirths =
-          vertexBirthSection <> edgeBirthSection <> faceBirthSection
-      }
-
-incidentEdgeBirthsFor
-  :: (PlanarSimplex label, PowerAlphaBirth)
-  -> [(label, PowerAlphaBirth)]
-incidentEdgeBirthsFor (simplex, birthValue) =
-  case NonEmpty.toList (planarSimplexVertices simplex) of
-    [firstLabel, secondLabel] ->
-      [ (firstLabel, birthValue)
-      , (secondLabel, birthValue)
-        ]
-    _ -> []
-
--- | Look up the exact birth of one admitted simplex.
-regularAlphaSimplexBirth
-  :: Ord label
-  => PlanarSimplex label
-  -> RegularAlphaFiltration label
-  -> Maybe PowerAlphaBirth
-regularAlphaSimplexBirth simplex = Map.lookup simplex . regularAlphaBirths
-
--- | Distinct births in ascending exact order.
-regularAlphaCriticalBirths
-  :: RegularAlphaFiltration label
-  -> [PowerAlphaBirth]
-regularAlphaCriticalBirths =
-  Set.toAscList . Set.fromList . Map.elems . regularAlphaBirths
-
--- | Reconstruct the closed subcomplex born no later than the threshold.
-regularAlphaComplexAtBirth
-  :: Ord label
-  => PowerAlphaBirth
-  -> RegularAlphaFiltration label
-  -> Either (RegularAlphaError label) (PlanarComplex label)
-regularAlphaComplexAtBirth threshold filtration =
-  first RegularAlphaSublevelInvalid
-    ( planarComplex
-        ( Map.keysSet
-            (Map.filter (<= threshold) (regularAlphaBirths filtration))
-        )
-    )
-
-edgeIsVisible :: Ord label => Set label -> RegularEdge label -> Bool
-edgeIsVisible visible edge =
-  let (firstLabel, secondLabel) = regularEdgeLabels edge
-   in Set.member firstLabel visible && Set.member secondLabel visible
-
-faceIsVisible :: Ord label => Set label -> RegularFace label -> Bool
-faceIsVisible visible face =
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-   in all (`Set.member` visible) [firstLabel, secondLabel, thirdLabel]
-
-regularEdgeSimplex
-  :: Ord label
-  => RegularEdge label
-  -> Either (RegularAlphaError label) (PlanarSimplex label)
-regularEdgeSimplex edge =
-  let (firstLabel, secondLabel) = regularEdgeLabels edge
-   in first RegularAlphaSimplexInvalid (planarEdge firstLabel secondLabel)
-
-regularFaceSimplex
-  :: Ord label
-  => RegularFace label
-  -> Either (RegularAlphaError label) (PlanarSimplex label)
-regularFaceSimplex face =
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-   in first RegularAlphaSimplexInvalid
-        (planarFace firstLabel secondLabel thirdLabel)
-
-regularFaceBirth
-  :: Ord label
-  => RegularTriangulation label
-  -> RegularFace label
-  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
-regularFaceBirth triangulation face = do
-  simplex <- regularFaceSimplex face
-  let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
-      dualPoint = regularFaceDualPoint face
-  firstSite <- requireRegularSite triangulation firstLabel
-  secondSite <- requireRegularSite triangulation secondLabel
-  thirdSite <- requireRegularSite triangulation thirdLabel
-  let firstBirth = powerDistance firstSite dualPoint
-      secondBirth = powerDistance secondSite dualPoint
-      thirdBirth = powerDistance thirdSite dualPoint
-  requireEqualDual simplex firstBirth secondBirth
-  requireEqualDual simplex firstBirth thirdBirth
-  pure (simplex, PowerAlphaBirth firstBirth)
-
-regularEdgeBirth
-  :: Ord label
-  => RegularTriangulation label
-  -> RegularEdge label
-  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
-regularEdgeBirth triangulation edge = do
-  simplex <- regularEdgeSimplex edge
-  let (firstLabel, secondLabel) = regularEdgeLabels edge
-  firstSite <- requireRegularSite triangulation firstLabel
-  secondSite <- requireRegularSite triangulation secondLabel
-  minimizingPoint <- minimizePowerOnDual simplex firstSite (regularEdgeDual edge)
-  let firstBirth = powerDistance firstSite minimizingPoint
-      secondBirth = powerDistance secondSite minimizingPoint
-  requireEqualDual simplex firstBirth secondBirth
-  pure (simplex, PowerAlphaBirth firstBirth)
-
-regularVertexBirth
-  :: Ord label
-  => RegularTriangulation label
-  -> Map label PowerAlphaBirth
-  -> label
-  -> Either (RegularAlphaError label) (PlanarSimplex label, PowerAlphaBirth)
-regularVertexBirth triangulation incidentBirths label = do
-  owner <- requireRegularSite triangulation label
-  competitors <-
-    traverse (requireRegularSite triangulation) (Set.toAscList (regularNeighbours label triangulation))
-  let ownerPoint = powerSiteExactPosition owner
-      ownerBirth = powerDistance owner ownerPoint
-  if all ((ownerBirth <=) . (`powerDistance` ownerPoint)) competitors
-    then Right (planarVertex label, PowerAlphaBirth ownerBirth)
-    else
-      case Map.lookup label incidentBirths of
-        Just birthValue -> Right (planarVertex label, birthValue)
-        Nothing -> Left (RegularAlphaVertexBirthMissing label)
-
-minimizePowerOnDual
-  :: PlanarSimplex label
-  -> PowerSite label
-  -> PowerDualEdge
-  -> Either (RegularAlphaError label) ExactPoint
-minimizePowerOnDual simplex site dual =
-  case dual of
-    BoundedPowerDual segment -> do
-      let (origin, terminal) = exactSegmentEndpoints segment
-      projected <- projectAlong simplex (powerSiteExactPosition site) origin (differenceVector origin terminal)
-      pure (pointAtClampedParameter 0 1 origin (differenceVector origin terminal) projected)
-    UnboundedPowerDual ray -> do
-      let origin = exactRayOrigin ray
-          direction = exactRayDirection ray
-      projected <- projectAlong simplex (powerSiteExactPosition site) origin direction
-      pure (pointAtParameter origin direction (max 0 projected))
-    FullLinePowerDual line -> projectOntoLine simplex (powerSiteExactPosition site) line
-    CollapsedPowerDual point -> Right point
-
-projectAlong
-  :: PlanarSimplex label
-  -> ExactPoint
-  -> ExactPoint
-  -> ExactVector
-  -> Either (RegularAlphaError label) ExactRational
-projectAlong simplex query origin direction =
-  let (queryX, queryY) = exactPointCoordinates query
-      (originX, originY) = exactPointCoordinates origin
-      ExactVector directionX directionY = direction
-      numerator = (queryX - originX) * directionX + (queryY - originY) * directionY
-      denominator = directionX * directionX + directionY * directionY
-   in first (RegularAlphaProjectionFailed simplex)
-        (exactDivide numerator denominator)
-
-pointAtClampedParameter
-  :: ExactRational
-  -> ExactRational
-  -> ExactPoint
-  -> ExactVector
-  -> ExactRational
-  -> ExactPoint
-pointAtClampedParameter lower upper origin direction parameter =
-  pointAtParameter origin direction (max lower (min upper parameter))
-
-pointAtParameter :: ExactPoint -> ExactVector -> ExactRational -> ExactPoint
-pointAtParameter origin (ExactVector directionX directionY) parameter =
-  let (originX, originY) = exactPointCoordinates origin
-   in exactPoint
-        (originX + parameter * directionX)
-        (originY + parameter * directionY)
-
-projectOntoLine
-  :: PlanarSimplex label
-  -> ExactPoint
-  -> ExactAffineLine
-  -> Either (RegularAlphaError label) ExactPoint
-projectOntoLine simplex query line = do
-  let (normalX, normalY, constant) = exactAffineLineCoefficients line
-      (queryX, queryY) = exactPointCoordinates query
-      lineValue = normalX * queryX + normalY * queryY + constant
-      squaredNormal = normalX * normalX + normalY * normalY
-  displacement <-
-    first (RegularAlphaProjectionFailed simplex)
-      (exactDivide lineValue squaredNormal)
-  pure
-    ( exactPoint
-        (queryX - displacement * normalX)
-        (queryY - displacement * normalY)
-    )
-
-powerDistance :: PowerSite label -> ExactPoint -> ExactRational
-powerDistance site point =
-  let (pointX, pointY) = exactPointCoordinates point
-      (siteX, siteY) = exactPointCoordinates (powerSiteExactPosition site)
-      deltaX = pointX - siteX
-      deltaY = pointY - siteY
-   in deltaX * deltaX + deltaY * deltaY - powerWeightExact (powerSiteWeight site)
-
-differenceVector :: ExactPoint -> ExactPoint -> ExactVector
-differenceVector from to =
-  let (fromX, fromY) = exactPointCoordinates from
-      (toX, toY) = exactPointCoordinates to
-   in ExactVector (toX - fromX) (toY - fromY)
-
-requireRegularSite
-  :: Ord label
-  => RegularTriangulation label
-  -> label
-  -> Either (RegularAlphaError label) (PowerSite label)
-requireRegularSite triangulation label =
-  maybe (Left (RegularAlphaSiteMissing label)) Right (regularSite label triangulation)
-
-requireEqualDual
-  :: PlanarSimplex label
-  -> ExactRational
-  -> ExactRational
-  -> Either (RegularAlphaError label) ()
-requireEqualDual simplex expected actual
-  | expected == actual = Right ()
-  | otherwise = Left (RegularAlphaDualMismatch simplex expected actual)
diff --git a/src-serialize/Moonlight/Hex/Serialization.hs b/src-serialize/Moonlight/Hex/Serialization.hs
--- a/src-serialize/Moonlight/Hex/Serialization.hs
+++ b/src-serialize/Moonlight/Hex/Serialization.hs
@@ -12,11 +12,11 @@
   ) where
 
 import Control.DeepSeq (NFData)
-import Control.Monad (replicateM)
 import Control.Monad.Trans.Class (lift)
 import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
 import Data.Binary.Get
   ( Get
+  , getByteString
   , getInt64be
   , getWord16be
   , getWord64be
@@ -28,7 +28,9 @@
   , putWord64be
   , runPut
   )
-import Data.Bits ((.&.), complement, shiftL)
+import Data.Bits ((.|.), shiftL)
+import Data.ByteString qualified as BS
+import Data.ByteString.Unsafe qualified as BSU
 import Data.ByteString.Lazy qualified as BL
 import Data.Int (Int64)
 import Data.Vector.Unboxed qualified as U
@@ -36,8 +38,8 @@
 import GHC.Generics (Generic)
 import Moonlight.Hex.Coordinate (HexCoord (..))
 import Moonlight.Hex.Region
-  ( HexLayout
-  , HexLayoutObstruction
+  ( HexLayoutObstruction
+  , HexPackedRegionObstruction (..)
   , HexRegion
   , hexLayout
   , hexLayoutCellCount
@@ -46,7 +48,7 @@
   , hexLayoutWidth
   , hexLayoutWordCount
   , foldHexRegionPackedWords
-  , hexRegionGenerate
+  , hexRegionFromPackedWords
   , hexRegionLayout
   )
 
@@ -68,7 +70,7 @@
   | HexCoordinateOutsideInt !Int64 !Int64
   | HexExtentOutsideInt !Word64 !Word64
   | HexSerializedLayoutInvalid !HexLayoutObstruction
-  | HexNonCanonicalPadding !Word64
+  | HexSerializedPackedRegionInvalid !HexPackedRegionObstruction
   deriving stock (Eq, Show, Generic)
   deriving anyclass (NFData)
 
@@ -138,45 +140,30 @@
   if cellCount > hexDecodingMaximumCells budget
     then throwE (HexCellBudgetExceeded cellCount (hexDecodingMaximumCells budget))
     else pure ()
-  serializedWords <- U.fromList <$> lift (replicateM (hexLayoutWordCount layout) getWord64be)
-  validatePadding layout serializedWords
-  pure (hexRegionGenerate layout (serializedMember layout serializedWords))
-
-validatePadding :: HexLayout -> U.Vector Word64 -> HexDecoder ()
-validatePadding layout wordsValue =
-  let mask = finalWordMask layout
-      canonical =
-        U.ifoldl'
-          (\valid index word ->
-              valid
-                && ( index + 1 < hexLayoutWordCount layout
-                       || word .&. complement mask == 0
-                   )
-          )
-          True
-          wordsValue
-   in if canonical
-        then pure ()
-        else
-          let padding = U.foldl' (\_ word -> word .&. complement mask) 0 wordsValue
-           in throwE (HexNonCanonicalPadding padding)
-
-serializedMember :: HexLayout -> U.Vector Word64 -> HexCoord -> Bool
-serializedMember layout wordsValue (HexCoord q r) =
-  let HexCoord originQ originR = hexLayoutOrigin layout
-      index = (r - originR) * hexLayoutWidth layout + (q - originQ)
-      (wordIndex, bitIndex) = index `quotRem` 64
-   in wordsValue `U.unsafeIndex` wordIndex .&. (1 `shiftL` bitIndex) /= 0
-{-# INLINE serializedMember #-}
-
-finalWordMask :: HexLayout -> Word64
-finalWordMask layout = lowBitMask (((hexLayoutCellCount layout - 1) `rem` 64) + 1)
+  let wordCount = hexLayoutWordCount layout
+  serializedBytes <- lift (getByteString (wordCount * 8))
+  let serializedWords = decodePackedWords wordCount serializedBytes
+  either (throwE . HexSerializedPackedRegionInvalid) pure (hexRegionFromPackedWords layout serializedWords)
 
-lowBitMask :: Int -> Word64
-lowBitMask count
-  | count >= 64 = maxBound
-  | count <= 0 = 0
-  | otherwise = (1 `shiftL` count) - 1
+decodePackedWords :: Int -> BS.ByteString -> U.Vector Word64
+decodePackedWords wordCount bytes = U.generate wordCount decodeWord
+ where
+  decodeWord :: Int -> Word64
+  decodeWord wordIndex =
+    -- 'getByteString' admitted exactly @wordCount * 8@ bytes before this
+    -- representation boundary; the eight indexed bytes therefore exist.
+    let byteOffset = wordIndex * 8
+        byte :: Int -> Word64
+        byte index = fromIntegral (BSU.unsafeIndex bytes (byteOffset + index))
+     in byte 0 `shiftL` 56
+          .|. byte 1 `shiftL` 48
+          .|. byte 2 `shiftL` 40
+          .|. byte 3 `shiftL` 32
+          .|. byte 4 `shiftL` 24
+          .|. byte 5 `shiftL` 16
+          .|. byte 6 `shiftL` 8
+          .|. byte 7
+{-# INLINE decodePackedWords #-}
 
 int64PairToCoord :: Int64 -> Int64 -> Maybe HexCoord
 int64PairToCoord q r = HexCoord <$> int64ToInt q <*> int64ToInt r
diff --git a/src-serialize/Moonlight/Planar/Serialization.hs b/src-serialize/Moonlight/Planar/Serialization.hs
new file mode 100644
--- /dev/null
+++ b/src-serialize/Moonlight/Planar/Serialization.hs
@@ -0,0 +1,446 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The versioned binary surface: encode a triangulation to bytes and read it
+-- back. Decoding refuses a payload whose format version or coordinate encoding this
+-- build does not own, rather than reinterpreting it.
+module Moonlight.Planar.Serialization
+  ( DecodingBudget (..)
+  , TrustedPayloadDecoders
+  , trustedBinaryPayloadDecoders
+  , SerializedCountKind (..)
+  , SerializationError (..)
+  , serializationVersion
+  , encodeTriangulation
+  , decodeTriangulation
+  ) where
+
+import Control.Monad (unless, when)
+import Control.Monad.Trans.Class (lift)
+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
+import Data.Binary (Binary (..))
+import Data.Binary.Get
+  ( Get
+  , bytesRead
+  , getDoublebe
+  , getWord16be
+  , getWord32be
+  , getWord64be
+  , getWord8
+  , runGetOrFail
+  )
+import Data.Binary.Put
+  ( putDoublebe
+  , putWord16be
+  , putWord32be
+  , putWord64be
+  , putWord8
+  , runPut
+  )
+import qualified Data.ByteString.Lazy as BL
+import Data.Foldable (traverse_)
+import qualified Data.IntSet as IntSet
+import Data.Int (Int64)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Proxy (Proxy (..))
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word16, Word64, Word8)
+import Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedFill (..)
+  , boxedFill
+  , boxedFoldr
+  , boxedFromVector
+  )
+import Moonlight.Planar.Internal.Validation (canonicalAdmission)
+import Moonlight.Planar.Internal.Paged (fromLocalVector, fromVector, pagedFoldr, pagedIFoldr, pagedLength)
+import Moonlight.Planar.Internal.PackedIndex (indexLimit)
+import Moonlight.Planar.Internal.PointIndex (buildPointIndex)
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Representation
+import Moonlight.Planar.Internal.Types
+import Moonlight.Planar.Point (Point (..), PointValidationError (..))
+import Moonlight.Planar.Scalar (canonicalScalarZero)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Validation (validateTriangulation)
+
+instance Binary (Point) where
+  put (Point x y) = putDoublebe x >> putDoublebe y
+  get = Point <$> getDoublebe <*> getDoublebe
+
+-- | The finite resource envelope admitted by the canonical decoder. The byte
+-- budget bounds the complete input before parsing; the element budget bounds
+-- the total number of library-owned serialized section elements before any
+-- section is allocated.
+data DecodingBudget = DecodingBudget
+  { decodingMaximumInputBytes :: !Word64
+  , decodingMaximumSectionElements :: !Word64
+  }
+  deriving stock (Eq, Show)
+
+-- | Evidence that the caller accepts the internal resource behavior of all
+-- four payload decoders. The structural budget governs only containers owned
+-- by this module; executable payload decoders require this separate trust law.
+data TrustedPayloadDecoders vertex directed undirected face where
+  TrustedBinaryPayloadDecoders
+    :: ( Binary vertex
+       , Binary directed
+       , Binary undirected
+       , Binary face
+       )
+    => TrustedPayloadDecoders vertex directed undirected face
+
+-- | Explicitly trust the selected 'Binary' payload instances. This witness is
+-- required because arbitrary instances may allocate independently of input
+-- bytes; constructing it declares that the caller has audited that behavior.
+trustedBinaryPayloadDecoders
+  :: ( Binary vertex
+     , Binary directed
+     , Binary undirected
+     , Binary face
+     )
+  => TrustedPayloadDecoders vertex directed undirected face
+trustedBinaryPayloadDecoders = TrustedBinaryPayloadDecoders
+
+-- | The structural count whose encoded value was outside the resident index
+-- or allocation domain.
+data SerializedCountKind
+  = SerializedVertexCount
+  | SerializedDirectedEdgeCount
+  | SerializedFaceCount
+  | SerializedConstraintCount
+  deriving stock (Eq, Ord, Show)
+
+-- | Every way serialization refuses, each naming its witness.
+data SerializationError
+  = BinaryDecodeFailure !Int64 !String
+  | TrailingBytes !Int64
+  | InvalidFormatMagic !Word64
+  | UnsupportedFormatVersion !Word16
+  | ConstraintModeTagMismatch !Word8 !Word8
+  | CoordinateEncodingTagMismatch !Word8 !Word8
+  | InputByteBudgetExceeded !Word64 !Word64
+  | DecodedSectionBudgetExceeded !Word64 !Word64
+  | EncodedCountExceedsInt !SerializedCountKind !Word64
+  | EncodedCountExceedsPackedIndex !SerializedCountKind !Word64 !Word64
+  | SerializedDirectedEdgeCountOdd !Word64
+  | SerializedConstraintCountExceedsEdges !Word64 !Word64
+  | SerializedPlanarCardinalityMismatch !Word64 !Word64 !Word64
+  | SerializedFixedBodyTooShort !Word64 !Word64
+  | SerializedMissingOuterFace
+  | InvalidSerializedPoint {-# UNPACK #-} !Int !PointValidationError
+  | NonCanonicalSerializedConstraintFlag !UndirectedEdgeId !Word8
+  | SerializedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | UnconstrainedSerializedConstraints {-# UNPACK #-} !Int
+  | DuplicateSerializedCoordinates {-# UNPACK #-} !Int {-# UNPACK #-} !Int
+  | DecodedInvariantViolations !(NonEmpty InvariantViolation)
+  deriving stock (Eq, Show)
+
+type Decoder = ExceptT SerializationError Get
+
+-- | The envelope version this module writes.
+serializationVersion :: Word16
+serializationVersion = 6
+
+formatMagic :: Word64
+formatMagic = 0x5350414445485307 -- "SPADEHS" + canonical geometry-owned format family
+
+binary64EncodingTag :: Word8
+binary64EncodingTag = 2
+
+-- | Write the versioned binary envelope.
+encodeTriangulation
+  :: forall mode vertex directed undirected face. (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
+  => Triangulation mode vertex directed undirected face
+  -> BL.ByteString
+encodeTriangulation triangulation = runPut $ do
+  putWord64be formatMagic
+  putWord16be serializationVersion
+  putWord8 (modeTag (constraintModeValue (modeProxy triangulation)))
+  putWord8 binary64EncodingTag
+  let ElementDefaults directedDefault undirectedDefault faceDefault = authoringElementDefaults triangulation
+      vertexDefault = case boxedFill (triVertexData triangulation) of
+        NoFill -> Nothing
+        Fill value -> Just value
+      vertexCount = pagedLength (triPointX triangulation)
+      directedEdgeCount = pagedLength (triHalfTopology triangulation) `quot` 4
+      faceCount = pagedLength (triFaceEdge triangulation)
+  -- Version 6 commits every structural count in one prefix. The decoder can
+  -- prove their relationships and resource bounds before defaults, payloads,
+  -- or section bodies are evaluated.
+  putWord64be (fromIntegral vertexCount)
+  putWord64be (fromIntegral directedEdgeCount)
+  putWord64be (fromIntegral faceCount)
+  putWord64be (fromIntegral (triConstraintCount triangulation))
+  put directedDefault
+  put undirectedDefault
+  put faceDefault
+  -- Geometry and payloads are independent components. Persist the authoritative
+  -- coordinate pages rather than attempting to recover them from annotations.
+  pagedFoldr ((>>) . putDoublebe) (pure ()) (triPointX triangulation)
+  pagedFoldr ((>>) . putDoublebe) (pure ()) (triPointY triangulation)
+  put vertexDefault
+  boxedFoldr ((>>) . put) (pure ()) (triVertexData triangulation)
+  pagedFoldr ((>>) . putWord32be) (pure ()) (triVertexOut triangulation)
+  -- The wire format stores the four topology planes separately; the interleaved
+  -- arena is a resident layout, not a serialization concern.
+  let plane field =
+        pagedIFoldr
+          (\index value rest -> if index `rem` 4 == field then putWord32be value >> rest else rest)
+          (pure ())
+          (triHalfTopology triangulation)
+  plane 0
+  plane 1
+  plane 2
+  plane 3
+  boxedFoldr ((>>) . put) (pure ()) (triDirectedData triangulation)
+  boxedFoldr ((>>) . put) (pure ()) (triUndirectedData triangulation)
+  pagedFoldr ((>>) . putWord32be) (pure ()) (triFaceEdge triangulation)
+  boxedFoldr ((>>) . put) (pure ()) (triFaceData triangulation)
+  pagedFoldr ((>>) . putWord8) (pure ()) (triConstraint triangulation)
+
+-- | Decode one exact, versioned finite DCEL inside an explicit resource
+-- envelope. Structural counts are read and reconciled before any default,
+-- payload, or section body is decoded. Coordinate uniqueness and the complete
+-- topology, geometry, and Delaunay/CDT invariants are checked before the opaque
+-- value is returned.
+--
+-- The budget bounds the input and the containers owned by this module. The
+-- required 'TrustedPayloadDecoders' witness separately records the caller's
+-- decision that every selected payload decoder is internally resource-safe.
+decodeTriangulation
+  :: forall mode vertex directed undirected face.
+     KnownConstraintMode mode
+  => DecodingBudget
+  -> TrustedPayloadDecoders vertex directed undirected face
+  -> BL.ByteString
+  -> Either SerializationError (Triangulation mode vertex directed undirected face)
+decodeTriangulation budget TrustedBinaryPayloadDecoders bytes
+  | inputByteCount > decodingMaximumInputBytes budget =
+      Left
+        ( InputByteBudgetExceeded
+            inputByteCount
+            (decodingMaximumInputBytes budget)
+        )
+  | otherwise =
+      case runGetOrFail (runExceptT getTriangulation) bytes of
+        Left (_, offset, message) -> Left (BinaryDecodeFailure offset message)
+        Right (_, _, Left failure) -> Left failure
+        Right (trailing, _, Right triangulation)
+          | not (BL.null trailing) -> Left (TrailingBytes (BL.length trailing))
+          | otherwise ->
+              case validateTriangulation triangulation of
+                [] -> Right triangulation{triCanonical = canonicalAdmission triangulation}
+                firstViolation : remainingViolations ->
+                  Left (DecodedInvariantViolations (firstViolation :| remainingViolations))
+ where
+  inputByteCount = fromIntegral (BL.length bytes)
+
+  getTriangulation :: Decoder (Triangulation mode vertex directed undirected face)
+  getTriangulation = do
+    magic <- lift getWord64be
+    unless (magic == formatMagic) (throwE (InvalidFormatMagic magic))
+    version <- lift getWord16be
+    unless (version == serializationVersion) (throwE (UnsupportedFormatVersion version))
+    encodedMode <- lift getWord8
+    let expectedMode = modeTag (constraintModeValue (Proxy :: Proxy mode))
+    unless (encodedMode == expectedMode) (throwE (ConstraintModeTagMismatch expectedMode encodedMode))
+    encodedScalar <- lift getWord8
+    let expectedScalar = binary64EncodingTag
+    unless (encodedScalar == expectedScalar) (throwE (CoordinateEncodingTagMismatch expectedScalar encodedScalar))
+    encodedVertexCount <- lift getWord64be
+    encodedDirectedEdgeCount <- lift getWord64be
+    encodedFaceCount <- lift getWord64be
+    encodedConstraintCount <- lift getWord64be
+    validateStructuralPrefix
+      budget
+      encodedVertexCount
+      encodedDirectedEdgeCount
+      encodedFaceCount
+      encodedConstraintCount
+    prefixByteCount <- fromIntegral <$> lift bytesRead
+    let bodyByteCount = inputByteCount - prefixByteCount
+        minimumBodyByteCount =
+          minimumFixedBodyBytes
+            encodedVertexCount
+            encodedDirectedEdgeCount
+            encodedFaceCount
+    unless (bodyByteCount >= minimumBodyByteCount) $
+      throwE (SerializedFixedBodyTooShort bodyByteCount minimumBodyByteCount)
+
+    let vertexCount = fromIntegral encodedVertexCount
+        halfCount = fromIntegral encodedDirectedEdgeCount
+        edgeCount = halfCount `quot` 2
+        faceCount = fromIntegral encodedFaceCount
+        cachedConstraintCount = fromIntegral encodedConstraintCount
+    defaults <- ElementDefaults <$> lift get <*> lift get <*> lift get
+    pointXs <- U.replicateM vertexCount (canonicalScalarZero <$> lift getDoublebe)
+    pointYs <- U.replicateM vertexCount (canonicalScalarZero <$> lift getDoublebe)
+    vertexDefault <- lift get
+    vertexDataVector <- V.replicateM vertexCount (lift get)
+    vertexOut <- U.replicateM vertexCount (lift getWord32be)
+    halfOrigin <- U.replicateM halfCount (lift getWord32be)
+    halfNext <- U.replicateM halfCount (lift getWord32be)
+    halfPrev <- U.replicateM halfCount (lift getWord32be)
+    halfFace <- U.replicateM halfCount (lift getWord32be)
+    directedDataVector <- V.replicateM halfCount (lift get)
+    undirectedDataVector <- V.replicateM edgeCount (lift get)
+    faceEdge <- U.replicateM faceCount (lift getWord32be)
+    faceDataVector <- V.replicateM faceCount (lift get)
+    constraints <- U.replicateM edgeCount (lift getWord8)
+
+    let points = V.generate vertexCount (\index -> Point (pointXs U.! index) (pointYs U.! index))
+    traverse_ (uncurry validateStoredPoint) (V.indexed points)
+
+    case U.ifoldr (\index flag found -> if flag /= 0 && flag /= 1 then Just (index, flag) else found) Nothing constraints of
+      Nothing -> pure ()
+      Just (index, flag) ->
+        throwE
+          ( NonCanonicalSerializedConstraintFlag
+              (UndirectedEdgeId (fromIntegral index))
+              flag
+          )
+    let actualConstraintCount = U.foldl' (\count flag -> if flag == 1 then count + 1 else count) 0 constraints
+    unless (cachedConstraintCount == actualConstraintCount) (throwE (SerializedConstraintCountMismatch cachedConstraintCount actualConstraintCount))
+    when (expectedMode == 0 && actualConstraintCount /= 0) (throwE (UnconstrainedSerializedConstraints actualConstraintCount))
+
+    let distinctPointCount = Set.size (V.foldl' (flip Set.insert) Set.empty points)
+    unless (distinctPointCount == vertexCount) (throwE (DuplicateSerializedCoordinates vertexCount distinctPointCount))
+
+    let pointXStore = fromLocalVector 0 pointXs
+        pointYStore = fromLocalVector 0 pointYs
+        topologyStore =
+          fromVector maxBound $
+            U.generate (4 * halfCount) $ \slot ->
+              let (edge, field) = slot `quotRem` 4
+               in case field of
+                    0 -> halfOrigin U.! edge
+                    1 -> halfNext U.! edge
+                    2 -> halfPrev U.! edge
+                    _ -> halfFace U.! edge
+        constraintEdgeIndex =
+          U.ifoldl'
+            (\edges index flag ->
+               if flag == 1 then IntSet.insert index edges else edges
+            )
+            IntSet.empty
+            constraints
+    -- Canonical admission reads topology through unchecked packed indices.
+    -- Keep this value unadmitted until outer validation has discharged them.
+    let decoded =
+          Triangulation
+            { triPointX = pointXStore
+            , triPointY = pointYStore
+            , triPointIndex = buildPointIndex pointXStore pointYStore
+            , triVertexOut = fromLocalVector maxBound vertexOut
+            , triVertexData = boxedFromVector (maybe NoFill Fill vertexDefault) vertexDataVector
+            , triHalfTopology = topologyStore
+            , triDirectedData = boxedFromVector (Fill (defaultDirectedEdgeData defaults)) directedDataVector
+            , triUndirectedData = boxedFromVector (Fill (defaultUndirectedEdgeData defaults)) undirectedDataVector
+            , triFaceEdge = fromLocalVector maxBound faceEdge
+            , triFaceData = boxedFromVector (Fill (defaultFaceData defaults)) faceDataVector
+            , triConstraint = fromVector 0 constraints
+            , triConstraintCount = cachedConstraintCount
+            , triConstraintEdges = constraintEdgeIndex
+            , triSeamFrontier = Nothing
+            , triCanonical = CanonicalUnknown
+            }
+    pure decoded
+
+validateStoredPoint :: Int -> Point -> Decoder ()
+validateStoredPoint index point =
+  case mkQueryPoint point of
+    Left failure -> throwE (InvalidSerializedPoint index failure)
+    Right _ -> pure ()
+
+modeProxy :: Triangulation mode vertex directed undirected face -> Proxy mode
+modeProxy _ = Proxy
+
+modeTag :: ConstraintMode -> Word8
+modeTag Unconstrained = 0
+modeTag Constrained = 1
+
+validateStructuralPrefix
+  :: DecodingBudget
+  -> Word64
+  -> Word64
+  -> Word64
+  -> Word64
+  -> Decoder ()
+validateStructuralPrefix budget vertexCount directedEdgeCount faceCount constraintCount = do
+  traverse_
+    (uncurry validateEncodedCount)
+    [ (SerializedVertexCount, vertexCount)
+    , (SerializedDirectedEdgeCount, directedEdgeCount)
+    , (SerializedFaceCount, faceCount)
+    , (SerializedConstraintCount, constraintCount)
+    ]
+  unless (even directedEdgeCount) $
+    throwE (SerializedDirectedEdgeCountOdd directedEdgeCount)
+  unless (faceCount >= 1) (throwE SerializedMissingOuterFace)
+  let undirectedEdgeCount = directedEdgeCount `quot` 2
+  unless (constraintCount <= undirectedEdgeCount) $
+    throwE
+      ( SerializedConstraintCountExceedsEdges
+          constraintCount
+          undirectedEdgeCount
+      )
+  unless
+    (planarCardinalityHolds vertexCount directedEdgeCount faceCount)
+    ( throwE
+        ( SerializedPlanarCardinalityMismatch
+            vertexCount
+            directedEdgeCount
+            faceCount
+        )
+    )
+  let sectionElements =
+        4 * vertexCount
+          + 6 * directedEdgeCount
+          + 2 * faceCount
+      maximumElements = decodingMaximumSectionElements budget
+  when (sectionElements > maximumElements) $
+    throwE (DecodedSectionBudgetExceeded sectionElements maximumElements)
+
+validateEncodedCount :: SerializedCountKind -> Word64 -> Decoder ()
+validateEncodedCount kind count = do
+  when (count > fromIntegral (maxBound :: Int)) $
+    throwE (EncodedCountExceedsInt kind count)
+  when (count > maximumPackedElementCount) $
+    throwE
+      ( EncodedCountExceedsPackedIndex
+          kind
+          count
+          maximumPackedElementCount
+      )
+
+-- Each resident handle is a Word32 with @maxBound@ withheld as the optional
+-- no-index marker. A count may include every remaining representable handle.
+maximumPackedElementCount :: Word64
+maximumPackedElementCount = fromIntegral indexLimit + 1
+
+-- Every resident triangulation is a connected planar straight-line graph.
+-- Empty input has only the outer face; a bounded-face-free nonempty graph is the
+-- collinear chain; otherwise Euler's law determines the edge count. All
+-- arithmetic is safe after 'validateEncodedCount' bounds each term to the
+-- packed-index domain.
+planarCardinalityHolds :: Word64 -> Word64 -> Word64 -> Bool
+planarCardinalityHolds vertexCount directedEdgeCount faceCount
+  | vertexCount == 0 = directedEdgeCount == 0 && faceCount == 1
+  | faceCount == 1 = directedEdgeCount == 2 * (vertexCount - 1)
+  | vertexCount < 3 = False
+  | otherwise =
+      directedEdgeCount == 2 * (vertexCount + faceCount - 2)
+
+-- The packed-index proof above bounds every term far below Word64 overflow.
+-- Payloads and defaults have no format-level lower bound because lawful
+-- 'Binary' decoders such as that for @()@ may consume zero bytes.
+minimumFixedBodyBytes :: Word64 -> Word64 -> Word64 -> Word64
+minimumFixedBodyBytes vertexCount directedEdgeCount faceCount =
+  20 * vertexCount
+    + 16 * directedEdgeCount
+    + 4 * faceCount
+    + directedEdgeCount `quot` 2
diff --git a/src-serialize/Moonlight/Triangulation/Serialization.hs b/src-serialize/Moonlight/Triangulation/Serialization.hs
deleted file mode 100644
--- a/src-serialize/Moonlight/Triangulation/Serialization.hs
+++ /dev/null
@@ -1,443 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE GADTs #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The versioned binary surface: encode a triangulation to bytes and read it
--- back. Decoding refuses a payload whose format version or coordinate encoding this
--- build does not own, rather than reinterpreting it.
-module Moonlight.Triangulation.Serialization
-  ( DecodingBudget (..)
-  , TrustedPayloadDecoders
-  , trustedBinaryPayloadDecoders
-  , SerializedCountKind (..)
-  , SerializationError (..)
-  , serializationVersion
-  , encodeTriangulation
-  , decodeTriangulation
-  ) where
-
-import Control.Monad (unless, when)
-import Control.Monad.Trans.Class (lift)
-import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
-import Data.Binary (Binary (..))
-import Data.Binary.Get
-  ( Get
-  , bytesRead
-  , getDoublebe
-  , getWord16be
-  , getWord32be
-  , getWord64be
-  , getWord8
-  , runGetOrFail
-  )
-import Data.Binary.Put
-  ( putDoublebe
-  , putWord16be
-  , putWord32be
-  , putWord64be
-  , putWord8
-  , runPut
-  )
-import qualified Data.ByteString.Lazy as BL
-import Data.Foldable (traverse_)
-import qualified Data.IntSet as IntSet
-import Data.Int (Int64)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.Proxy (Proxy (..))
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as U
-import Data.Word (Word16, Word64, Word8)
-import Moonlight.Triangulation.Internal.BoxedPaged
-  ( boxedFill
-  , boxedFromVector
-  , boxedToVector
-  )
-import Moonlight.Triangulation.Internal.Paged (fromLocalVector, fromVector, toVector)
-import Moonlight.Triangulation.Internal.PackedIndex (indexLimit)
-import Moonlight.Triangulation.Internal.PointIndex (buildPointIndex)
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Internal.Representation
-import Moonlight.Triangulation.Internal.Types
-import Moonlight.Triangulation.Math (canonicalCoordinate, mkQueryPoint)
-import Moonlight.Triangulation.Validation (validateTriangulation)
-
-instance Binary (Point) where
-  put (Point x y) = putDoublebe x >> putDoublebe y
-  get = Point <$> getDoublebe <*> getDoublebe
-
--- | The finite resource envelope admitted by the canonical decoder. The byte
--- budget bounds the complete input before parsing; the element budget bounds
--- the total number of library-owned serialized section elements before any
--- section is allocated.
-data DecodingBudget = DecodingBudget
-  { decodingMaximumInputBytes :: !Word64
-  , decodingMaximumSectionElements :: !Word64
-  }
-  deriving stock (Eq, Show)
-
--- | Evidence that the caller accepts the internal resource behavior of all
--- four payload decoders. The structural budget governs only containers owned
--- by this module; executable payload decoders require this separate trust law.
-data TrustedPayloadDecoders vertex directed undirected face where
-  TrustedBinaryPayloadDecoders
-    :: ( Binary vertex
-       , Binary directed
-       , Binary undirected
-       , Binary face
-       )
-    => TrustedPayloadDecoders vertex directed undirected face
-
--- | Explicitly trust the selected 'Binary' payload instances. This witness is
--- required because arbitrary instances may allocate independently of input
--- bytes; constructing it declares that the caller has audited that behavior.
-trustedBinaryPayloadDecoders
-  :: ( Binary vertex
-     , Binary directed
-     , Binary undirected
-     , Binary face
-     )
-  => TrustedPayloadDecoders vertex directed undirected face
-trustedBinaryPayloadDecoders = TrustedBinaryPayloadDecoders
-
--- | The structural count whose encoded value was outside the resident index
--- or allocation domain.
-data SerializedCountKind
-  = SerializedVertexCount
-  | SerializedDirectedEdgeCount
-  | SerializedFaceCount
-  | SerializedConstraintCount
-  deriving stock (Eq, Ord, Show)
-
--- | Every way serialization refuses, each naming its witness.
-data SerializationError
-  = BinaryDecodeFailure !Int64 !String
-  | TrailingBytes !Int64
-  | InvalidFormatMagic !Word64
-  | UnsupportedFormatVersion !Word16
-  | ConstraintModeTagMismatch !Word8 !Word8
-  | CoordinateEncodingTagMismatch !Word8 !Word8
-  | InputByteBudgetExceeded !Word64 !Word64
-  | DecodedSectionBudgetExceeded !Word64 !Word64
-  | EncodedCountExceedsInt !SerializedCountKind !Word64
-  | EncodedCountExceedsPackedIndex !SerializedCountKind !Word64 !Word64
-  | SerializedDirectedEdgeCountOdd !Word64
-  | SerializedConstraintCountExceedsEdges !Word64 !Word64
-  | SerializedPlanarCardinalityMismatch !Word64 !Word64 !Word64
-  | SerializedFixedBodyTooShort !Word64 !Word64
-  | SerializedMissingOuterFace
-  | InvalidSerializedPoint {-# UNPACK #-} !Int !PointValidationError
-  | NonCanonicalSerializedConstraintFlag !UndirectedEdgeId !Word8
-  | SerializedConstraintCountMismatch {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | UnconstrainedSerializedConstraints {-# UNPACK #-} !Int
-  | DuplicateSerializedCoordinates {-# UNPACK #-} !Int {-# UNPACK #-} !Int
-  | DecodedInvariantViolations !(NonEmpty InvariantViolation)
-  deriving stock (Eq, Show)
-
-type Decoder = ExceptT SerializationError Get
-
--- | The envelope version this module writes.
-serializationVersion :: Word16
-serializationVersion = 6
-
-formatMagic :: Word64
-formatMagic = 0x5350414445485307 -- "SPADEHS" + canonical geometry-owned format family
-
-binary64EncodingTag :: Word8
-binary64EncodingTag = 2
-
--- | Write the versioned binary envelope.
-encodeTriangulation
-  :: forall mode vertex directed undirected face. (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
-  => Triangulation mode vertex directed undirected face
-  -> BL.ByteString
-encodeTriangulation triangulation = runPut $ do
-  putWord64be formatMagic
-  putWord16be serializationVersion
-  putWord8 (modeTag (constraintModeValue (modeProxy triangulation)))
-  putWord8 binary64EncodingTag
-  let ElementDefaults directedDefault undirectedDefault faceDefault = triElementDefaults triangulation
-      pointXs = toVector (triPointX triangulation)
-      pointYs = toVector (triPointY triangulation)
-      vertexDefault = boxedFill (triVertexData triangulation)
-      vertexDataVector = boxedToVector (triVertexData triangulation)
-      vertexOut = toVector (triVertexOut triangulation)
-      topology = toVector (triHalfTopology triangulation)
-      directedDataVector = boxedToVector (triDirectedData triangulation)
-      undirectedDataVector = boxedToVector (triUndirectedData triangulation)
-      faceEdge = toVector (triFaceEdge triangulation)
-      faceDataVector = boxedToVector (triFaceData triangulation)
-      constraints = toVector (triConstraint triangulation)
-      vertexCount = U.length pointXs
-      directedEdgeCount = U.length topology `quot` 4
-      faceCount = U.length faceEdge
-  -- Version 6 commits every structural count in one prefix. The decoder can
-  -- prove their relationships and resource bounds before defaults, payloads,
-  -- or section bodies are evaluated.
-  putWord64be (fromIntegral vertexCount)
-  putWord64be (fromIntegral directedEdgeCount)
-  putWord64be (fromIntegral faceCount)
-  putWord64be (fromIntegral (triConstraintCount triangulation))
-  put directedDefault
-  put undirectedDefault
-  put faceDefault
-  -- Geometry and payloads are independent components. Persist the authoritative
-  -- coordinate pages rather than attempting to recover them from annotations.
-  U.mapM_ putDoublebe pointXs
-  U.mapM_ putDoublebe pointYs
-  put vertexDefault
-  V.mapM_ put vertexDataVector
-  U.mapM_ putWord32be vertexOut
-  -- The wire format stores the four topology planes separately; the interleaved
-  -- arena is a resident layout, not a serialization concern.
-  let plane field = U.generate directedEdgeCount (\edge -> topology U.! (4 * edge + field))
-  U.mapM_ putWord32be (plane 0)
-  U.mapM_ putWord32be (plane 1)
-  U.mapM_ putWord32be (plane 2)
-  U.mapM_ putWord32be (plane 3)
-  V.mapM_ put directedDataVector
-  V.mapM_ put undirectedDataVector
-  U.mapM_ putWord32be faceEdge
-  V.mapM_ put faceDataVector
-  U.mapM_ putWord8 constraints
-
--- | Decode one exact, versioned finite DCEL inside an explicit resource
--- envelope. Structural counts are read and reconciled before any default,
--- payload, or section body is decoded. Coordinate uniqueness and the complete
--- topology, geometry, and Delaunay/CDT invariants are checked before the opaque
--- value is returned.
---
--- The budget bounds the input and the containers owned by this module. The
--- required 'TrustedPayloadDecoders' witness separately records the caller's
--- decision that every selected payload decoder is internally resource-safe.
-decodeTriangulation
-  :: forall mode vertex directed undirected face.
-     KnownConstraintMode mode
-  => DecodingBudget
-  -> TrustedPayloadDecoders vertex directed undirected face
-  -> BL.ByteString
-  -> Either SerializationError (Triangulation mode vertex directed undirected face)
-decodeTriangulation budget TrustedBinaryPayloadDecoders bytes
-  | inputByteCount > decodingMaximumInputBytes budget =
-      Left
-        ( InputByteBudgetExceeded
-            inputByteCount
-            (decodingMaximumInputBytes budget)
-        )
-  | otherwise =
-      case runGetOrFail (runExceptT getTriangulation) bytes of
-        Left (_, offset, message) -> Left (BinaryDecodeFailure offset message)
-        Right (_, _, Left failure) -> Left failure
-        Right (trailing, _, Right triangulation)
-          | not (BL.null trailing) -> Left (TrailingBytes (BL.length trailing))
-          | otherwise ->
-              case validateTriangulation triangulation of
-                [] -> Right triangulation
-                firstViolation : remainingViolations ->
-                  Left (DecodedInvariantViolations (firstViolation :| remainingViolations))
- where
-  inputByteCount = fromIntegral (BL.length bytes)
-
-  getTriangulation :: Decoder (Triangulation mode vertex directed undirected face)
-  getTriangulation = do
-    magic <- lift getWord64be
-    unless (magic == formatMagic) (throwE (InvalidFormatMagic magic))
-    version <- lift getWord16be
-    unless (version == serializationVersion) (throwE (UnsupportedFormatVersion version))
-    encodedMode <- lift getWord8
-    let expectedMode = modeTag (constraintModeValue (Proxy :: Proxy mode))
-    unless (encodedMode == expectedMode) (throwE (ConstraintModeTagMismatch expectedMode encodedMode))
-    encodedScalar <- lift getWord8
-    let expectedScalar = binary64EncodingTag
-    unless (encodedScalar == expectedScalar) (throwE (CoordinateEncodingTagMismatch expectedScalar encodedScalar))
-    encodedVertexCount <- lift getWord64be
-    encodedDirectedEdgeCount <- lift getWord64be
-    encodedFaceCount <- lift getWord64be
-    encodedConstraintCount <- lift getWord64be
-    validateStructuralPrefix
-      budget
-      encodedVertexCount
-      encodedDirectedEdgeCount
-      encodedFaceCount
-      encodedConstraintCount
-    prefixByteCount <- fromIntegral <$> lift bytesRead
-    let bodyByteCount = inputByteCount - prefixByteCount
-        minimumBodyByteCount =
-          minimumFixedBodyBytes
-            encodedVertexCount
-            encodedDirectedEdgeCount
-            encodedFaceCount
-    unless (bodyByteCount >= minimumBodyByteCount) $
-      throwE (SerializedFixedBodyTooShort bodyByteCount minimumBodyByteCount)
-
-    let vertexCount = fromIntegral encodedVertexCount
-        halfCount = fromIntegral encodedDirectedEdgeCount
-        edgeCount = halfCount `quot` 2
-        faceCount = fromIntegral encodedFaceCount
-        cachedConstraintCount = fromIntegral encodedConstraintCount
-    defaults <- ElementDefaults <$> lift get <*> lift get <*> lift get
-    pointXs <- U.replicateM vertexCount (canonicalCoordinate <$> lift getDoublebe)
-    pointYs <- U.replicateM vertexCount (canonicalCoordinate <$> lift getDoublebe)
-    vertexDefault <- lift get
-    vertexDataVector <- V.replicateM vertexCount (lift get)
-    vertexOut <- U.replicateM vertexCount (lift getWord32be)
-    halfOrigin <- U.replicateM halfCount (lift getWord32be)
-    halfNext <- U.replicateM halfCount (lift getWord32be)
-    halfPrev <- U.replicateM halfCount (lift getWord32be)
-    halfFace <- U.replicateM halfCount (lift getWord32be)
-    directedDataVector <- V.replicateM halfCount (lift get)
-    undirectedDataVector <- V.replicateM edgeCount (lift get)
-    faceEdge <- U.replicateM faceCount (lift getWord32be)
-    faceDataVector <- V.replicateM faceCount (lift get)
-    constraints <- U.replicateM edgeCount (lift getWord8)
-
-    let points = V.generate vertexCount (\index -> Point (pointXs U.! index) (pointYs U.! index))
-    traverse_ (uncurry validateStoredPoint) (V.indexed points)
-
-    case U.ifoldr (\index flag found -> if flag /= 0 && flag /= 1 then Just (index, flag) else found) Nothing constraints of
-      Nothing -> pure ()
-      Just (index, flag) ->
-        throwE
-          ( NonCanonicalSerializedConstraintFlag
-              (UndirectedEdgeId (fromIntegral index))
-              flag
-          )
-    let actualConstraintCount = U.foldl' (\count flag -> if flag == 1 then count + 1 else count) 0 constraints
-    unless (cachedConstraintCount == actualConstraintCount) (throwE (SerializedConstraintCountMismatch cachedConstraintCount actualConstraintCount))
-    when (expectedMode == 0 && actualConstraintCount /= 0) (throwE (UnconstrainedSerializedConstraints actualConstraintCount))
-
-    let distinctPointCount = Set.size (V.foldl' (flip Set.insert) Set.empty points)
-    unless (distinctPointCount == vertexCount) (throwE (DuplicateSerializedCoordinates vertexCount distinctPointCount))
-
-    let pointXStore = fromLocalVector 0 pointXs
-        pointYStore = fromLocalVector 0 pointYs
-        topologyStore =
-          fromVector maxBound $
-            U.generate (4 * halfCount) $ \slot ->
-              let (edge, field) = slot `quotRem` 4
-               in case field of
-                    0 -> halfOrigin U.! edge
-                    1 -> halfNext U.! edge
-                    2 -> halfPrev U.! edge
-                    _ -> halfFace U.! edge
-        constraintEdgeIndex =
-          U.ifoldl'
-            (\edges index flag ->
-               if flag == 1 then IntSet.insert index edges else edges
-            )
-            IntSet.empty
-            constraints
-    pure
-      Triangulation
-        { triPointX = pointXStore
-        , triPointY = pointYStore
-        , triPointIndex = buildPointIndex pointXStore pointYStore
-        , triVertexOut = fromLocalVector maxBound vertexOut
-        , triVertexData = boxedFromVector vertexDefault vertexDataVector
-        , triHalfTopology = topologyStore
-        , triDirectedData = boxedFromVector (Just (defaultDirectedEdgeData defaults)) directedDataVector
-        , triUndirectedData = boxedFromVector (Just (defaultUndirectedEdgeData defaults)) undirectedDataVector
-        , triFaceEdge = fromLocalVector maxBound faceEdge
-        , triFaceData = boxedFromVector (Just (defaultFaceData defaults)) faceDataVector
-        , triConstraint = fromVector 0 constraints
-        , triConstraintCount = cachedConstraintCount
-        , triConstraintEdges = constraintEdgeIndex
-        , triSeamFrontier = Nothing
-        , triElementDefaults = defaults
-        }
-
-validateStoredPoint :: Int -> Point -> Decoder ()
-validateStoredPoint index point =
-  case mkQueryPoint point of
-    Left failure -> throwE (InvalidSerializedPoint index failure)
-    Right _ -> pure ()
-
-modeProxy :: Triangulation mode vertex directed undirected face -> Proxy mode
-modeProxy _ = Proxy
-
-modeTag :: ConstraintMode -> Word8
-modeTag Unconstrained = 0
-modeTag Constrained = 1
-
-validateStructuralPrefix
-  :: DecodingBudget
-  -> Word64
-  -> Word64
-  -> Word64
-  -> Word64
-  -> Decoder ()
-validateStructuralPrefix budget vertexCount directedEdgeCount faceCount constraintCount = do
-  traverse_
-    (uncurry validateEncodedCount)
-    [ (SerializedVertexCount, vertexCount)
-    , (SerializedDirectedEdgeCount, directedEdgeCount)
-    , (SerializedFaceCount, faceCount)
-    , (SerializedConstraintCount, constraintCount)
-    ]
-  unless (even directedEdgeCount) $
-    throwE (SerializedDirectedEdgeCountOdd directedEdgeCount)
-  unless (faceCount >= 1) (throwE SerializedMissingOuterFace)
-  let undirectedEdgeCount = directedEdgeCount `quot` 2
-  unless (constraintCount <= undirectedEdgeCount) $
-    throwE
-      ( SerializedConstraintCountExceedsEdges
-          constraintCount
-          undirectedEdgeCount
-      )
-  unless
-    (planarCardinalityHolds vertexCount directedEdgeCount faceCount)
-    ( throwE
-        ( SerializedPlanarCardinalityMismatch
-            vertexCount
-            directedEdgeCount
-            faceCount
-        )
-    )
-  let sectionElements =
-        4 * vertexCount
-          + 6 * directedEdgeCount
-          + 2 * faceCount
-      maximumElements = decodingMaximumSectionElements budget
-  when (sectionElements > maximumElements) $
-    throwE (DecodedSectionBudgetExceeded sectionElements maximumElements)
-
-validateEncodedCount :: SerializedCountKind -> Word64 -> Decoder ()
-validateEncodedCount kind count = do
-  when (count > fromIntegral (maxBound :: Int)) $
-    throwE (EncodedCountExceedsInt kind count)
-  when (count > maximumPackedElementCount) $
-    throwE
-      ( EncodedCountExceedsPackedIndex
-          kind
-          count
-          maximumPackedElementCount
-      )
-
--- Each resident handle is a Word32 with @maxBound@ withheld as the optional
--- no-index marker. A count may include every remaining representable handle.
-maximumPackedElementCount :: Word64
-maximumPackedElementCount = fromIntegral indexLimit + 1
-
--- Every resident triangulation is a connected planar straight-line graph.
--- Empty input has only the outer face; a bounded-face-free nonempty graph is the
--- collinear chain; otherwise Euler's law determines the edge count. All
--- arithmetic is safe after 'validateEncodedCount' bounds each term to the
--- packed-index domain.
-planarCardinalityHolds :: Word64 -> Word64 -> Word64 -> Bool
-planarCardinalityHolds vertexCount directedEdgeCount faceCount
-  | vertexCount == 0 = directedEdgeCount == 0 && faceCount == 1
-  | faceCount == 1 = directedEdgeCount == 2 * (vertexCount - 1)
-  | vertexCount < 3 = False
-  | otherwise =
-      directedEdgeCount == 2 * (vertexCount + faceCount - 2)
-
--- The packed-index proof above bounds every term far below Word64 overflow.
--- Payloads and defaults have no format-level lower bound because lawful
--- 'Binary' decoders such as that for @()@ may consume zero bytes.
-minimumFixedBodyBytes :: Word64 -> Word64 -> Word64 -> Word64
-minimumFixedBodyBytes vertexCount directedEdgeCount faceCount =
-  20 * vertexCount
-    + 16 * directedEdgeCount
-    + 4 * faceCount
-    + directedEdgeCount `quot` 2
diff --git a/src-zigzag/Moonlight/Planar/Zigzag.hs b/src-zigzag/Moonlight/Planar/Zigzag.hs
new file mode 100644
--- /dev/null
+++ b/src-zigzag/Moonlight/Planar/Zigzag.hs
@@ -0,0 +1,445 @@
+-- | Exact alpha-complex persistence across non-nested activation depths.
+--
+-- Each observed depth is rebuilt independently from stable labelled points.
+-- Consecutive complexes are compared through the canonical union cospan
+-- @K_i -> K_i union K_(i+1) <- K_(i+1)@.  Stable simplex labels, never DCEL
+-- handles, own cross-mesh identity.  Homology remains the sole owner of the
+-- zigzag interval decomposition.
+module Moonlight.Planar.Zigzag
+  ( ActivationPoint (..),
+    ActivationSlice (..),
+    ActivationStage (..),
+    ActivationZigzagError (..),
+    ActivationComplex,
+    activationComplexStage,
+    activationComplexCells,
+    activationComplexBasisAt,
+    activationComplexChainComplex,
+    AdjacentUnionWitness,
+    adjacentUnionStage,
+    adjacentCommonCells,
+    adjacentUnionComplex,
+    adjacentLeftInclusion,
+    adjacentRightInclusion,
+    ActivationZigzag,
+    activationZigzagStages,
+    activationZigzagComplexes,
+    activationZigzagCorrespondences,
+    activationZigzagDiagram,
+    ZigzagInterval (..),
+    ActivationInterval,
+    activationAlphaZigzag,
+    activationZigzagIntervals,
+    activationAlphaPersistence,
+  )
+where
+
+import Data.Bifunctor (first)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.List.NonEmpty qualified as NonEmpty
+import Data.Set (Set)
+import Data.Set qualified as Set
+import Data.Vector (Vector)
+import Data.Vector qualified as Vector
+import Moonlight.Core (firstDuplicate)
+import Moonlight.Homology.Boundary
+  ( BoundaryIncidence,
+    FiniteChainComplex,
+    overlapBoundaryIncidence,
+  )
+import Moonlight.Homology.Chain
+  ( HomologicalDegree (..),
+  )
+import Moonlight.Homology.Persistence
+  ( FiniteChainMap,
+    FiniteChainZigzag,
+    ZigzagArrow (..),
+    ZigzagFailure,
+    ZigzagInterval (..),
+    mkFiniteChainMapChecked,
+    mkFiniteChainZigzag,
+    rationalZigzagIntervals,
+  )
+import Moonlight.Planar.Alpha
+  ( AlphaFiltrationError,
+    alphaComplexAtRadius,
+    alphaFiltration,
+  )
+import Moonlight.Planar.BulkLoad
+  ( DuplicatePayloadPolicy (KeepFirstPayload),
+    delaunayFromCoordinates,
+  )
+import Moonlight.Planar.CellSet
+  ( ExactCellSet,
+    foldExactCellEdges,
+    foldExactCellFaces,
+    foldExactCellVertices,
+  )
+import Moonlight.Planar.LabelledComplex
+  ( PlanarComplexInterpretationError,
+    finitePlanarComplex,
+  )
+import Moonlight.Planar.Simplex
+  ( PlanarComplex,
+    PlanarComplexError,
+    PlanarSimplex,
+    PlanarSimplexError,
+    SimplexDimension (..),
+    intersectPlanarComplex,
+    planarComplex,
+    planarComplexBasisAt,
+    planarComplexCells,
+    planarEdge,
+    planarFace,
+    planarVertex,
+    unionPlanarComplex,
+  )
+import Moonlight.Planar.Dcel qualified as Dcel
+import Moonlight.Planar.Handles.HandleDefs
+  ( FaceId,
+    UndirectedEdgeId,
+  )
+import Moonlight.Planar.Types (BuildError, BuildResult (..), DelaunayTriangulation, unitElementDefaults)
+import Moonlight.Planar.BuildStats (BuildMetric (UniquePoints), buildStat)
+import Moonlight.Planar.Point (Point)
+import Moonlight.Planar.Scalar (RadiusSquared)
+
+-- | One stable observation identity and its two-dimensional embedding.  The
+-- coordinates may change at every depth; the label is the correspondence.
+data ActivationPoint label = ActivationPoint
+  { activationPointLabel :: !label,
+    activationPointCoordinates :: !Point
+  }
+  deriving stock (Eq, Show)
+
+-- | One independently sampled depth and the exact squared-radius alpha
+-- threshold to observe there.
+data ActivationSlice depth label = ActivationSlice
+  { activationSliceDepth :: !depth,
+    activationSliceRadiusSquared :: !RadiusSquared,
+    activationSlicePoints :: !(Vector (ActivationPoint label))
+  }
+  deriving stock (Eq, Show)
+
+-- | Vertices of the expanded zigzag alternate between observed depths and
+-- their adjacent common ambient unions.
+data ActivationStage depth
+  = ObservedDepth !depth
+  | AdjacentDepthUnion !depth !depth
+  deriving stock (Eq, Ord, Show)
+
+data ActivationZigzagError depth label
+  = ActivationDuplicateDepth !depth
+  | ActivationDuplicateLabel !depth !label
+  | ActivationCoordinateCollapse !depth !Int !Int
+  | ActivationBuildFailed !depth !BuildError
+  | ActivationFiltrationFailed !depth !AlphaFiltrationError
+  | ActivationInnerFaceInvalid !(ActivationStage depth) !FaceId
+  | ActivationSimplexInvalid !(ActivationStage depth) !(PlanarSimplexError label)
+  | ActivationComplexInvalid !(ActivationStage depth) !(PlanarComplexError label)
+  | ActivationChainComplexFailed
+      !(ActivationStage depth)
+      !(PlanarComplexInterpretationError label)
+  | ActivationZigzagFailed !ZigzagFailure
+  | ActivationStageIndexMissing !Int
+  deriving stock (Eq, Show)
+
+-- | A checked, canonically labelled simplicial interpretation of one observed
+-- alpha complex or adjacent union.
+data ActivationComplex depth label = ActivationComplex
+  { activationComplexStage :: !(ActivationStage depth),
+    storedActivationPlanarComplex :: !(PlanarComplex label),
+    activationComplexChainComplex :: !(FiniteChainComplex Int)
+  }
+
+activationComplexCells
+  :: ActivationComplex depth label
+  -> Set (PlanarSimplex label)
+activationComplexCells = planarComplexCells . storedActivationPlanarComplex
+
+-- | The canonical labelled basis corresponding to the numeric basis of
+-- 'activationComplexChainComplex' at one degree.
+activationComplexBasisAt ::
+  ActivationComplex depth label ->
+  HomologicalDegree ->
+  [PlanarSimplex label]
+activationComplexBasisAt complexValue degreeValue =
+  maybe
+    []
+    (Vector.toList . (`planarComplexBasisAt` storedActivationPlanarComplex complexValue))
+    (simplexDimensionAt degreeValue)
+
+-- | The two checked inclusion legs proving how adjacent observations meet in
+-- their union.  The common-cell set is a derived inspection surface.
+data AdjacentUnionWitness depth label = AdjacentUnionWitness
+  { adjacentCommonCells :: !(Set (PlanarSimplex label)),
+    adjacentUnionComplex :: !(ActivationComplex depth label),
+    adjacentLeftInclusion :: !(FiniteChainMap Int),
+    adjacentRightInclusion :: !(FiniteChainMap Int)
+  }
+
+adjacentUnionStage :: AdjacentUnionWitness depth label -> ActivationStage depth
+adjacentUnionStage = activationComplexStage . adjacentUnionComplex
+
+-- | The expanded stages, admitted observations, correspondence witnesses, and
+-- authoritative Homology diagram travel together.
+data ActivationZigzag depth label = ActivationZigzag
+  { activationZigzagComplexes :: !(NonEmpty (ActivationComplex depth label)),
+    activationZigzagCorrespondences :: !(Vector (AdjacentUnionWitness depth label)),
+    activationZigzagDiagram :: !(FiniteChainZigzag Int)
+  }
+
+-- | The labelled view of the Homology diagram.  It is derived from the
+-- admitted observations and their union witnesses rather than stored as a
+-- second sequence that could disagree with them.
+activationZigzagStages :: ActivationZigzag depth label -> Vector (ActivationStage depth)
+activationZigzagStages zigzag =
+  let firstComplex :| remainingComplexes = activationZigzagComplexes zigzag
+   in Vector.fromList
+        ( activationComplexStage firstComplex
+            : concat
+              ( zipWith
+                  (\witness rightComplex ->
+                      [adjacentUnionStage witness, activationComplexStage rightComplex]
+                  )
+                  (Vector.toList (activationZigzagCorrespondences zigzag))
+                  remainingComplexes
+              )
+        )
+
+-- | The Homology interval carrier with endpoints resolved into the activation
+-- stage vocabulary.  This is a specialization, not a parallel record.
+type ActivationInterval depth = ZigzagInterval (ActivationStage depth)
+
+activationAlphaZigzag ::
+  (Ord depth, Ord label) =>
+  NonEmpty (ActivationSlice depth label) ->
+  Either (ActivationZigzagError depth label) (ActivationZigzag depth label)
+activationAlphaZigzag slices = do
+  case firstDuplicate (fmap activationSliceDepth (NonEmpty.toList slices)) of
+    Just duplicateDepth -> Left (ActivationDuplicateDepth duplicateDepth)
+    Nothing -> Right ()
+  observedComplexes <- traverse activationComplexFromSlice slices
+  buildActivationZigzag
+    (NonEmpty.zip (fmap activationSliceDepth slices) observedComplexes)
+
+activationZigzagIntervals ::
+  ActivationZigzag depth label ->
+  Either (ActivationZigzagError depth label) [ActivationInterval depth]
+activationZigzagIntervals zigzag = do
+  intervals <-
+    first ActivationZigzagFailed
+      (rationalZigzagIntervals (activationZigzagDiagram zigzag))
+  traverse (traverse (requireStage (activationZigzagStages zigzag))) intervals
+
+activationAlphaPersistence ::
+  (Ord depth, Ord label) =>
+  NonEmpty (ActivationSlice depth label) ->
+  Either (ActivationZigzagError depth label) [ActivationInterval depth]
+activationAlphaPersistence slices =
+  activationAlphaZigzag slices >>= activationZigzagIntervals
+
+activationComplexFromSlice ::
+  Ord label =>
+  ActivationSlice depth label ->
+  Either (ActivationZigzagError depth label) (ActivationComplex depth label)
+activationComplexFromSlice slice = do
+  let depthValue = activationSliceDepth slice
+      points = activationSlicePoints slice
+      labels = fmap activationPointLabel points
+  case firstDuplicate (Vector.toList labels) of
+    Just duplicateLabel -> Left (ActivationDuplicateLabel depthValue duplicateLabel)
+    Nothing -> Right ()
+  buildResult <-
+    first (ActivationBuildFailed depthValue)
+      ( delaunayFromCoordinates
+          unitElementDefaults
+          (fmap activationPointCoordinates points)
+          labels
+          KeepFirstPayload
+      )
+  let inputCount = Vector.length points
+      uniqueCount = (buildStat UniquePoints) (buildStats buildResult)
+  if uniqueCount /= inputCount
+    then Left (ActivationCoordinateCollapse depthValue inputCount uniqueCount)
+    else do
+      let triangulation = buildTriangulation buildResult
+      filtration <-
+        first (ActivationFiltrationFailed depthValue) (alphaFiltration triangulation)
+      let selectedCells = alphaComplexAtRadius (activationSliceRadiusSquared slice) filtration
+          stage = ObservedDepth depthValue
+      cellSet <- activationCellsFromDcel stage triangulation selectedCells
+      compileActivationComplex stage cellSet
+
+activationCellsFromDcel ::
+  Ord label =>
+  ActivationStage depth ->
+  DelaunayTriangulation label ->
+  ExactCellSet ->
+  Either (ActivationZigzagError depth label) (PlanarComplex label)
+activationCellsFromDcel stage triangulation selectedCells = do
+  let vertexCells =
+        fmap
+          (planarVertex . Dcel.vertexData triangulation)
+          (reverse (foldExactCellVertices (\vertices vertex _ -> vertex : vertices) [] selectedCells))
+  edgeCells <-
+    traverse
+      (activationEdgeFromDcel stage triangulation)
+      (reverse (foldExactCellEdges (flip (:)) [] selectedCells))
+  faceCells <-
+    traverse
+      (activationFaceFromDcel stage triangulation)
+      (reverse (foldExactCellFaces (flip (:)) [] selectedCells))
+  first (ActivationComplexInvalid stage)
+    (planarComplex (Set.fromList (vertexCells <> edgeCells <> faceCells)))
+
+activationEdgeFromDcel ::
+  Ord label =>
+  ActivationStage depth ->
+  DelaunayTriangulation label ->
+  UndirectedEdgeId ->
+  Either (ActivationZigzagError depth label) (PlanarSimplex label)
+activationEdgeFromDcel stage triangulation edgeValue =
+  let (firstVertex, secondVertex) = Dcel.undirectedEndpoints triangulation edgeValue
+   in first (ActivationSimplexInvalid stage)
+        ( planarEdge
+            (Dcel.vertexData triangulation firstVertex)
+            (Dcel.vertexData triangulation secondVertex)
+        )
+
+activationFaceFromDcel ::
+  Ord label =>
+  ActivationStage depth ->
+  DelaunayTriangulation label ->
+  FaceId ->
+  Either (ActivationZigzagError depth label) (PlanarSimplex label)
+activationFaceFromDcel stage triangulation faceValue =
+  case Dcel.innerFaceVertices triangulation faceValue of
+    Nothing -> Left (ActivationInnerFaceInvalid stage faceValue)
+    Just (firstVertex, secondVertex, thirdVertex) ->
+      first (ActivationSimplexInvalid stage)
+        ( planarFace
+            (Dcel.vertexData triangulation firstVertex)
+            (Dcel.vertexData triangulation secondVertex)
+            (Dcel.vertexData triangulation thirdVertex)
+        )
+
+compileActivationComplex ::
+  Ord label =>
+  ActivationStage depth ->
+  PlanarComplex label ->
+  Either (ActivationZigzagError depth label) (ActivationComplex depth label)
+compileActivationComplex stage complexValue = do
+  chainComplex <-
+    first (ActivationChainComplexFailed stage)
+      (finitePlanarComplex complexValue)
+  pure
+    ActivationComplex
+      { activationComplexStage = stage,
+        storedActivationPlanarComplex = complexValue,
+        activationComplexChainComplex = chainComplex
+      }
+
+buildActivationZigzag ::
+  Ord label =>
+  NonEmpty (depth, ActivationComplex depth label) ->
+  Either (ActivationZigzagError depth label) (ActivationZigzag depth label)
+buildActivationZigzag observedEntries@((_, firstComplex) :| remainingEntries) = do
+  correspondences <-
+    traverse
+      ( \((leftDepth, leftComplex), (rightDepth, rightComplex)) ->
+          adjacentUnionWitness leftDepth rightDepth leftComplex rightComplex
+      )
+      (zip (NonEmpty.toList observedEntries) remainingEntries)
+  let arrows =
+        correspondences
+          >>= ( \witness ->
+                  [ ForwardArrow (adjacentLeftInclusion witness),
+                    BackwardArrow (adjacentRightInclusion witness)
+                  ]
+              )
+  diagram <-
+    first ActivationZigzagFailed
+      ( mkFiniteChainZigzag
+          (activationComplexChainComplex firstComplex)
+          arrows
+      )
+  pure
+    ActivationZigzag
+      { activationZigzagComplexes = fmap snd observedEntries,
+        activationZigzagCorrespondences = Vector.fromList correspondences,
+        activationZigzagDiagram = diagram
+      }
+
+adjacentUnionWitness ::
+  Ord label =>
+  depth ->
+  depth ->
+  ActivationComplex depth label ->
+  ActivationComplex depth label ->
+  Either (ActivationZigzagError depth label) (AdjacentUnionWitness depth label)
+adjacentUnionWitness leftDepth rightDepth leftComplex rightComplex = do
+  let unionStage =
+        AdjacentDepthUnion leftDepth rightDepth
+      unionPlanar =
+        unionPlanarComplex
+          (storedActivationPlanarComplex leftComplex)
+          (storedActivationPlanarComplex rightComplex)
+  unionComplex <- compileActivationComplex unionStage unionPlanar
+  leftInclusion <- activationInclusion leftComplex unionComplex
+  rightInclusion <- activationInclusion rightComplex unionComplex
+  pure
+    AdjacentUnionWitness
+      { adjacentCommonCells =
+          planarComplexCells
+            ( intersectPlanarComplex
+                (storedActivationPlanarComplex leftComplex)
+                (storedActivationPlanarComplex rightComplex)
+            ),
+        adjacentUnionComplex = unionComplex,
+        adjacentLeftInclusion = leftInclusion,
+        adjacentRightInclusion = rightInclusion
+      }
+
+activationInclusion ::
+  Ord label =>
+  ActivationComplex depth label ->
+  ActivationComplex depth label ->
+  Either (ActivationZigzagError depth label) (FiniteChainMap Int)
+activationInclusion sourceComplex targetComplex =
+  first ActivationZigzagFailed
+    ( mkFiniteChainMapChecked
+        (activationComplexChainComplex sourceComplex)
+        (activationComplexChainComplex targetComplex)
+        (inclusionAtDegree sourceComplex targetComplex)
+    )
+
+inclusionAtDegree ::
+  Ord label =>
+  ActivationComplex depth label ->
+  ActivationComplex depth label ->
+  HomologicalDegree ->
+  BoundaryIncidence Int
+inclusionAtDegree sourceComplex targetComplex degreeValue =
+  overlapBoundaryIncidence
+    (1 :: Int)
+    (activationComplexBasisAt sourceComplex degreeValue)
+    (activationComplexBasisAt targetComplex degreeValue)
+
+simplexDimensionAt :: HomologicalDegree -> Maybe SimplexDimension
+simplexDimensionAt (HomologicalDegree degreeIndex) =
+  case degreeIndex of
+    0 -> Just SimplexDimension0
+    1 -> Just SimplexDimension1
+    2 -> Just SimplexDimension2
+    _ -> Nothing
+
+requireStage ::
+  Vector (ActivationStage depth) ->
+  Int ->
+  Either (ActivationZigzagError depth label) (ActivationStage depth)
+requireStage stages stageIndex =
+  maybe
+    (Left (ActivationStageIndexMissing stageIndex))
+    Right
+    (stages Vector.!? stageIndex)
diff --git a/src-zigzag/Moonlight/Triangulation/Zigzag.hs b/src-zigzag/Moonlight/Triangulation/Zigzag.hs
deleted file mode 100644
--- a/src-zigzag/Moonlight/Triangulation/Zigzag.hs
+++ /dev/null
@@ -1,454 +0,0 @@
--- | Exact alpha-complex persistence across non-nested activation depths.
---
--- Each observed depth is rebuilt independently from stable labelled points.
--- Consecutive complexes are compared through the canonical union cospan
--- @K_i -> K_i union K_(i+1) <- K_(i+1)@.  Stable simplex labels, never DCEL
--- handles, own cross-mesh identity.  Homology remains the sole owner of the
--- zigzag interval decomposition.
-module Moonlight.Triangulation.Zigzag
-  ( ActivationPoint (..),
-    ActivationSlice (..),
-    ActivationStage (..),
-    ActivationZigzagError (..),
-    ActivationComplex,
-    activationComplexStage,
-    activationComplexCells,
-    activationComplexBasisAt,
-    activationComplexChainComplex,
-    AdjacentUnionWitness,
-    adjacentUnionStage,
-    adjacentCommonCells,
-    adjacentUnionComplex,
-    adjacentLeftInclusion,
-    adjacentRightInclusion,
-    ActivationZigzag,
-    activationZigzagStages,
-    activationZigzagComplexes,
-    activationZigzagCorrespondences,
-    activationZigzagDiagram,
-    ZigzagInterval (..),
-    ActivationInterval,
-    activationAlphaZigzag,
-    activationZigzagIntervals,
-    activationAlphaPersistence,
-  )
-where
-
-import Data.Bifunctor (first)
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.List.NonEmpty qualified as NonEmpty
-import Data.Set (Set)
-import Data.Set qualified as Set
-import Data.Vector (Vector)
-import Data.Vector qualified as Vector
-import Moonlight.Core (firstDuplicate)
-import Moonlight.Homology.Boundary
-  ( BoundaryIncidence,
-    FiniteChainComplex,
-    overlapBoundaryIncidence,
-  )
-import Moonlight.Homology.Chain
-  ( HomologicalDegree (..),
-  )
-import Moonlight.Homology.Persistence
-  ( FiniteChainMap,
-    FiniteChainZigzag,
-    ZigzagArrow (..),
-    ZigzagFailure,
-    ZigzagInterval (..),
-    mkFiniteChainMapChecked,
-    mkFiniteChainZigzag,
-    rationalZigzagIntervals,
-  )
-import Moonlight.Homology.Topology qualified as CellComplex
-import Moonlight.Triangulation.Alpha
-  ( AlphaFiltrationError,
-    alphaComplexAtRadius,
-    alphaFiltration,
-  )
-import Moonlight.Triangulation.BulkLoad
-  ( DuplicatePayloadPolicy (KeepFirstPayload),
-    delaunayFromCoordinates,
-  )
-import Moonlight.Triangulation.CellSet (CellSelectionError)
-import Moonlight.Triangulation.CellComplex
-  ( DCELComplex,
-    fromExactCellSet,
-  )
-import Moonlight.Triangulation.LabelledComplex
-  ( PlanarComplexInterpretationError,
-    finitePlanarComplex,
-  )
-import Moonlight.Triangulation.Simplex
-  ( PlanarComplex,
-    PlanarComplexError,
-    PlanarSimplex,
-    PlanarSimplexError,
-    SimplexDimension (..),
-    intersectPlanarComplex,
-    planarComplex,
-    planarComplexBasisAt,
-    planarComplexCells,
-    planarEdge,
-    planarFace,
-    planarVertex,
-    unionPlanarComplex,
-  )
-import Moonlight.Triangulation.Dcel qualified as Dcel
-import Moonlight.Triangulation.Handles.HandleDefs
-  ( FaceId,
-    UndirectedEdgeId,
-  )
-import Moonlight.Triangulation.Types
-  ( BuildError,
-    BuildResult (..),
-    BuildStats (..),
-    DelaunayTriangulation,
-    Point,
-    RadiusSquared,
-    unitElementDefaults,
-  )
-
--- | One stable observation identity and its two-dimensional embedding.  The
--- coordinates may change at every depth; the label is the correspondence.
-data ActivationPoint label = ActivationPoint
-  { activationPointLabel :: !label,
-    activationPointCoordinates :: !Point
-  }
-  deriving stock (Eq, Show)
-
--- | One independently sampled depth and the exact squared-radius alpha
--- threshold to observe there.
-data ActivationSlice depth label = ActivationSlice
-  { activationSliceDepth :: !depth,
-    activationSliceRadiusSquared :: !RadiusSquared,
-    activationSlicePoints :: !(Vector (ActivationPoint label))
-  }
-  deriving stock (Eq, Show)
-
--- | Vertices of the expanded zigzag alternate between observed depths and
--- their adjacent common ambient unions.
-data ActivationStage depth
-  = ObservedDepth !depth
-  | AdjacentDepthUnion !depth !depth
-  deriving stock (Eq, Ord, Show)
-
-data ActivationZigzagError depth label
-  = ActivationDuplicateDepth !depth
-  | ActivationDuplicateLabel !depth !label
-  | ActivationCoordinateCollapse !depth !Int !Int
-  | ActivationBuildFailed !depth !BuildError
-  | ActivationFiltrationFailed !depth !AlphaFiltrationError
-  | ActivationSelectionFailed !depth !CellSelectionError
-  | ActivationInnerFaceInvalid !(ActivationStage depth) !FaceId
-  | ActivationSimplexInvalid !(ActivationStage depth) !(PlanarSimplexError label)
-  | ActivationComplexInvalid !(ActivationStage depth) !(PlanarComplexError label)
-  | ActivationChainComplexFailed
-      !(ActivationStage depth)
-      !(PlanarComplexInterpretationError label)
-  | ActivationZigzagFailed !ZigzagFailure
-  | ActivationStageIndexMissing !Int
-  deriving stock (Eq, Show)
-
--- | A checked, canonically labelled simplicial interpretation of one observed
--- alpha complex or adjacent union.
-data ActivationComplex depth label = ActivationComplex
-  { activationComplexStage :: !(ActivationStage depth),
-    storedActivationPlanarComplex :: !(PlanarComplex label),
-    activationComplexChainComplex :: !(FiniteChainComplex Int)
-  }
-
-activationComplexCells
-  :: ActivationComplex depth label
-  -> Set (PlanarSimplex label)
-activationComplexCells = planarComplexCells . storedActivationPlanarComplex
-
--- | The canonical labelled basis corresponding to the numeric basis of
--- 'activationComplexChainComplex' at one degree.
-activationComplexBasisAt ::
-  ActivationComplex depth label ->
-  HomologicalDegree ->
-  [PlanarSimplex label]
-activationComplexBasisAt complexValue degreeValue =
-  maybe
-    []
-    (Vector.toList . (`planarComplexBasisAt` storedActivationPlanarComplex complexValue))
-    (simplexDimensionAt degreeValue)
-
--- | The two checked inclusion legs proving how adjacent observations meet in
--- their union.  The common-cell set is a derived inspection surface.
-data AdjacentUnionWitness depth label = AdjacentUnionWitness
-  { adjacentCommonCells :: !(Set (PlanarSimplex label)),
-    adjacentUnionComplex :: !(ActivationComplex depth label),
-    adjacentLeftInclusion :: !(FiniteChainMap Int),
-    adjacentRightInclusion :: !(FiniteChainMap Int)
-  }
-
-adjacentUnionStage :: AdjacentUnionWitness depth label -> ActivationStage depth
-adjacentUnionStage = activationComplexStage . adjacentUnionComplex
-
--- | The expanded stages, admitted observations, correspondence witnesses, and
--- authoritative Homology diagram travel together.
-data ActivationZigzag depth label = ActivationZigzag
-  { activationZigzagComplexes :: !(NonEmpty (ActivationComplex depth label)),
-    activationZigzagCorrespondences :: !(Vector (AdjacentUnionWitness depth label)),
-    activationZigzagDiagram :: !(FiniteChainZigzag Int)
-  }
-
--- | The labelled view of the Homology diagram.  It is derived from the
--- admitted observations and their union witnesses rather than stored as a
--- second sequence that could disagree with them.
-activationZigzagStages :: ActivationZigzag depth label -> Vector (ActivationStage depth)
-activationZigzagStages zigzag =
-  let firstComplex :| remainingComplexes = activationZigzagComplexes zigzag
-   in Vector.fromList
-        ( activationComplexStage firstComplex
-            : concat
-              ( zipWith
-                  (\witness rightComplex ->
-                      [adjacentUnionStage witness, activationComplexStage rightComplex]
-                  )
-                  (Vector.toList (activationZigzagCorrespondences zigzag))
-                  remainingComplexes
-              )
-        )
-
--- | The Homology interval carrier with endpoints resolved into the activation
--- stage vocabulary.  This is a specialization, not a parallel record.
-type ActivationInterval depth = ZigzagInterval (ActivationStage depth)
-
-activationAlphaZigzag ::
-  (Ord depth, Ord label) =>
-  NonEmpty (ActivationSlice depth label) ->
-  Either (ActivationZigzagError depth label) (ActivationZigzag depth label)
-activationAlphaZigzag slices = do
-  case firstDuplicate (fmap activationSliceDepth (NonEmpty.toList slices)) of
-    Just duplicateDepth -> Left (ActivationDuplicateDepth duplicateDepth)
-    Nothing -> Right ()
-  observedComplexes <- traverse activationComplexFromSlice slices
-  buildActivationZigzag
-    (NonEmpty.zip (fmap activationSliceDepth slices) observedComplexes)
-
-activationZigzagIntervals ::
-  ActivationZigzag depth label ->
-  Either (ActivationZigzagError depth label) [ActivationInterval depth]
-activationZigzagIntervals zigzag = do
-  intervals <-
-    first ActivationZigzagFailed
-      (rationalZigzagIntervals (activationZigzagDiagram zigzag))
-  traverse (traverse (requireStage (activationZigzagStages zigzag))) intervals
-
-activationAlphaPersistence ::
-  (Ord depth, Ord label) =>
-  NonEmpty (ActivationSlice depth label) ->
-  Either (ActivationZigzagError depth label) [ActivationInterval depth]
-activationAlphaPersistence slices =
-  activationAlphaZigzag slices >>= activationZigzagIntervals
-
-activationComplexFromSlice ::
-  Ord label =>
-  ActivationSlice depth label ->
-  Either (ActivationZigzagError depth label) (ActivationComplex depth label)
-activationComplexFromSlice slice = do
-  let depthValue = activationSliceDepth slice
-      points = activationSlicePoints slice
-      labels = fmap activationPointLabel points
-  case firstDuplicate (Vector.toList labels) of
-    Just duplicateLabel -> Left (ActivationDuplicateLabel depthValue duplicateLabel)
-    Nothing -> Right ()
-  buildResult <-
-    first (ActivationBuildFailed depthValue)
-      ( delaunayFromCoordinates
-          unitElementDefaults
-          (fmap activationPointCoordinates points)
-          labels
-          KeepFirstPayload
-      )
-  let inputCount = Vector.length points
-      uniqueCount = statUniquePoints (buildStats buildResult)
-  if uniqueCount /= inputCount
-    then Left (ActivationCoordinateCollapse depthValue inputCount uniqueCount)
-    else do
-      let triangulation = buildTriangulation buildResult
-      filtration <-
-        first (ActivationFiltrationFailed depthValue) (alphaFiltration triangulation)
-      selectedCells <-
-        first (ActivationSelectionFailed depthValue)
-          (alphaComplexAtRadius (activationSliceRadiusSquared slice) filtration)
-      let stage = ObservedDepth depthValue
-      cellSet <- activationCellsFromDcel stage triangulation (fromExactCellSet selectedCells)
-      compileActivationComplex stage cellSet
-
-activationCellsFromDcel ::
-  Ord label =>
-  ActivationStage depth ->
-  DelaunayTriangulation label ->
-  DCELComplex ->
-  Either (ActivationZigzagError depth label) (PlanarComplex label)
-activationCellsFromDcel stage triangulation complexValue = do
-  let vertexCells =
-        fmap
-          (planarVertex . Dcel.vertexData triangulation)
-          (CellComplex.vertices complexValue)
-  edgeCells <-
-    traverse
-      (activationEdgeFromDcel stage triangulation complexValue)
-      (CellComplex.edges complexValue)
-  faceCells <-
-    traverse
-      (activationFaceFromDcel stage triangulation)
-      (CellComplex.faces complexValue)
-  first (ActivationComplexInvalid stage)
-    (planarComplex (Set.fromList (vertexCells <> edgeCells <> faceCells)))
-
-activationEdgeFromDcel ::
-  Ord label =>
-  ActivationStage depth ->
-  DelaunayTriangulation label ->
-  DCELComplex ->
-  UndirectedEdgeId ->
-  Either (ActivationZigzagError depth label) (PlanarSimplex label)
-activationEdgeFromDcel stage triangulation complexValue edgeValue =
-  let (firstVertex, secondVertex) = CellComplex.edgeBoundary complexValue edgeValue
-   in first (ActivationSimplexInvalid stage)
-        ( planarEdge
-            (Dcel.vertexData triangulation firstVertex)
-            (Dcel.vertexData triangulation secondVertex)
-        )
-
-activationFaceFromDcel ::
-  Ord label =>
-  ActivationStage depth ->
-  DelaunayTriangulation label ->
-  FaceId ->
-  Either (ActivationZigzagError depth label) (PlanarSimplex label)
-activationFaceFromDcel stage triangulation faceValue =
-  case Dcel.innerFaceVertices triangulation faceValue of
-    Nothing -> Left (ActivationInnerFaceInvalid stage faceValue)
-    Just (firstVertex, secondVertex, thirdVertex) ->
-      first (ActivationSimplexInvalid stage)
-        ( planarFace
-            (Dcel.vertexData triangulation firstVertex)
-            (Dcel.vertexData triangulation secondVertex)
-            (Dcel.vertexData triangulation thirdVertex)
-        )
-
-compileActivationComplex ::
-  Ord label =>
-  ActivationStage depth ->
-  PlanarComplex label ->
-  Either (ActivationZigzagError depth label) (ActivationComplex depth label)
-compileActivationComplex stage complexValue = do
-  chainComplex <-
-    first (ActivationChainComplexFailed stage)
-      (finitePlanarComplex complexValue)
-  pure
-    ActivationComplex
-      { activationComplexStage = stage,
-        storedActivationPlanarComplex = complexValue,
-        activationComplexChainComplex = chainComplex
-      }
-
-buildActivationZigzag ::
-  Ord label =>
-  NonEmpty (depth, ActivationComplex depth label) ->
-  Either (ActivationZigzagError depth label) (ActivationZigzag depth label)
-buildActivationZigzag observedEntries@((_, firstComplex) :| remainingEntries) = do
-  correspondences <-
-    traverse
-      ( \((leftDepth, leftComplex), (rightDepth, rightComplex)) ->
-          adjacentUnionWitness leftDepth rightDepth leftComplex rightComplex
-      )
-      (zip (NonEmpty.toList observedEntries) remainingEntries)
-  let arrows =
-        correspondences
-          >>= ( \witness ->
-                  [ ForwardArrow (adjacentLeftInclusion witness),
-                    BackwardArrow (adjacentRightInclusion witness)
-                  ]
-              )
-  diagram <-
-    first ActivationZigzagFailed
-      ( mkFiniteChainZigzag
-          (activationComplexChainComplex firstComplex)
-          arrows
-      )
-  pure
-    ActivationZigzag
-      { activationZigzagComplexes = fmap snd observedEntries,
-        activationZigzagCorrespondences = Vector.fromList correspondences,
-        activationZigzagDiagram = diagram
-      }
-
-adjacentUnionWitness ::
-  Ord label =>
-  depth ->
-  depth ->
-  ActivationComplex depth label ->
-  ActivationComplex depth label ->
-  Either (ActivationZigzagError depth label) (AdjacentUnionWitness depth label)
-adjacentUnionWitness leftDepth rightDepth leftComplex rightComplex = do
-  let unionStage =
-        AdjacentDepthUnion leftDepth rightDepth
-      unionPlanar =
-        unionPlanarComplex
-          (storedActivationPlanarComplex leftComplex)
-          (storedActivationPlanarComplex rightComplex)
-  unionComplex <- compileActivationComplex unionStage unionPlanar
-  leftInclusion <- activationInclusion leftComplex unionComplex
-  rightInclusion <- activationInclusion rightComplex unionComplex
-  pure
-    AdjacentUnionWitness
-      { adjacentCommonCells =
-          planarComplexCells
-            ( intersectPlanarComplex
-                (storedActivationPlanarComplex leftComplex)
-                (storedActivationPlanarComplex rightComplex)
-            ),
-        adjacentUnionComplex = unionComplex,
-        adjacentLeftInclusion = leftInclusion,
-        adjacentRightInclusion = rightInclusion
-      }
-
-activationInclusion ::
-  Ord label =>
-  ActivationComplex depth label ->
-  ActivationComplex depth label ->
-  Either (ActivationZigzagError depth label) (FiniteChainMap Int)
-activationInclusion sourceComplex targetComplex =
-  first ActivationZigzagFailed
-    ( mkFiniteChainMapChecked
-        (activationComplexChainComplex sourceComplex)
-        (activationComplexChainComplex targetComplex)
-        (inclusionAtDegree sourceComplex targetComplex)
-    )
-
-inclusionAtDegree ::
-  Ord label =>
-  ActivationComplex depth label ->
-  ActivationComplex depth label ->
-  HomologicalDegree ->
-  BoundaryIncidence Int
-inclusionAtDegree sourceComplex targetComplex degreeValue =
-  overlapBoundaryIncidence
-    (1 :: Int)
-    (activationComplexBasisAt sourceComplex degreeValue)
-    (activationComplexBasisAt targetComplex degreeValue)
-
-simplexDimensionAt :: HomologicalDegree -> Maybe SimplexDimension
-simplexDimensionAt (HomologicalDegree degreeIndex) =
-  case degreeIndex of
-    0 -> Just SimplexDimension0
-    1 -> Just SimplexDimension1
-    2 -> Just SimplexDimension2
-    _ -> Nothing
-
-requireStage ::
-  Vector (ActivationStage depth) ->
-  Int ->
-  Either (ActivationZigzagError depth label) (ActivationStage depth)
-requireStage stages stageIndex =
-  maybe
-    (Left (ActivationStageIndexMissing stageIndex))
-    Right
-    (stages Vector.!? stageIndex)
diff --git a/test/algebra/Main.hs b/test/algebra/Main.hs
--- a/test/algebra/Main.hs
+++ b/test/algebra/Main.hs
@@ -1,9 +1,9 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.AlgebraSpec as AlgebraSpec
-import qualified Moonlight.Triangulation.RegionAlgebraSpec as RegionAlgebraSpec
-import qualified Moonlight.Triangulation.ScheduleAgreementSpec as ScheduleAgreementSpec
-import qualified Moonlight.Triangulation.ValuationSpec as ValuationSpec
+import qualified Moonlight.Planar.AlgebraSpec as AlgebraSpec
+import qualified Moonlight.Planar.RegionAlgebraSpec as RegionAlgebraSpec
+import qualified Moonlight.Planar.ScheduleAgreementSpec as ScheduleAgreementSpec
+import qualified Moonlight.Planar.ValuationSpec as ValuationSpec
 
 main :: IO ()
 main = do
diff --git a/test/algebra/Moonlight/Planar/AlgebraFixtures.hs b/test/algebra/Moonlight/Planar/AlgebraFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/AlgebraFixtures.hs
@@ -0,0 +1,247 @@
+-- | The operand meshes and mesh observations both algebra slices are stated
+-- over, built against the facade alone.
+module Moonlight.Planar.AlgebraFixtures
+  ( Mesh
+  , meshOf
+  , inputOrderedMeshOf
+  , operands
+  , separatedOperands
+  , cocircularRing
+  , collinearSites
+  , latticeSites
+  , pointsOf
+  , dedupeAscending
+  , siteList
+  , siteSet
+  , assertMesh
+  , assertMeshEquivalent
+  , pairs
+  , triples
+  , advance
+  , scramble
+  , randomSites
+  ) where
+
+import Data.List (sortBy)
+import Data.Maybe (fromMaybe)
+import Data.Ord (comparing)
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Data.Word (Word64)
+import Moonlight.Planar.BulkLoad (delaunay, delaunayGeometry)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.Dcel (numFaces, numUndirectedEdges, numVertices)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Payload (geometryOnlyPublication, mapVertices)
+import Moonlight.Planar.Types (DelaunayTriangulation, buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (Point (Point))
+import Moonlight.Planar.MeshFixtures (edgeKeys, siteKey)
+import Support (requireRight)
+
+-- | The carrier. Geometry and nothing else: no vertex payload to need a
+-- commutative combining rule, no element payloads to survive a rewrite that
+-- destroys the elements they labelled.
+type Mesh = DelaunayTriangulation ()
+
+-- ── operands ─────────────────────────────────────────────────────────────────
+
+-- | The site families the laws are exercised over.
+--
+-- Sized so that the cubic sweep in the associativity law stays cheap. Coverage
+-- here is by /kind/ of degeneracy, not by count: what breaks a join is a
+-- cocircular quad whose tie-break went the other way, or a mesh with no faces
+-- at all, and neither of those becomes more likely at ten thousand sites.
+families :: [(String, [Point])]
+families =
+  [ ("void", [])
+  , ("single", [Point 3 (-7)])
+  , ("pair", [Point 0 0, Point 4 1])
+  , ("collinear", collinearSites)
+  , ("cocircular", cocircularRing)
+  , ("lattice", latticeSites)
+  , ("repeated", concatMap (replicate 3) [Point 0 0, Point 5 0, Point 0 5, Point 5 5, Point 2 3])
+  , ("scattered", randomSites 0xC0FFEEBABE 32)
+  , ("extreme", [Point 1.0e-8 1.0e-8, Point 1.0e8 (-1.0e8), Point (-1.0e8) 1.0e8, Point 0 0, Point 1 1])
+  ]
+
+-- | Exactly cocircular lattice points on @x² + y² = 625@.
+--
+-- Trigonometric points would be cocircular only to within rounding, and the
+-- rule this is here to exercise — the diagonal tie-break that fires when the
+-- lifted quadrilateral is exactly flat — would then never fire at all.
+cocircularRing :: [Point]
+cocircularRing =
+  [ Point (fromIntegral (signX * x)) (fromIntegral (signY * y))
+  | (x, y) <- [(25, 0), (0, 25), (7, 24), (24, 7), (15, 20), (20, 15)] :: [(Int, Int)]
+  , signX <- [1, -1]
+  , signY <- [1, -1]
+  ]
+
+collinearSites :: [Point]
+collinearSites = [Point (fromIntegral k) (2 * fromIntegral k - 1) | k <- [0 .. 9 :: Int]]
+
+latticeSites :: [Point]
+latticeSites = [Point (fromIntegral i) (fromIntegral j) | i <- [0 .. 4 :: Int], j <- [0 .. 4 :: Int]]
+
+-- | The meshes every pairwise and triple law runs over.
+--
+-- Chosen for overlap structure rather than for size: disjoint operands, nested
+-- ones, partially overlapping ones, the empty one, and — the case that matters
+-- most — two meshes standing on the /same/ sites built in different orders, so
+-- they are geometrically identical and structurally distinct.
+operands :: IO [(String, Mesh)]
+operands = do
+  let sites name = fromMaybe [] (lookup name families)
+      lattice = sites "lattice"
+  void' <- meshOf "void" []
+  single <- meshOf "single" (sites "single")
+  collinear <- meshOf "collinear" (sites "collinear")
+  ring <- meshOf "cocircular" (sites "cocircular")
+  lower <- meshOf "lattice-lower" (take 15 lattice)
+  upper <- meshOf "lattice-upper" (drop 10 lattice)
+  repeated <- meshOf "repeated" (sites "repeated")
+  scatterA <- meshOf "scattered" (sites "scattered")
+  scatterB <- meshOf "scattered-scrambled" (scramble 0x5EED (sites "scattered"))
+  pure
+    [ ("void", void')
+    , ("single", single)
+    , ("collinear", collinear)
+    , ("cocircular", ring)
+    , ("lattice-lower", lower)
+    , ("lattice-upper", upper)
+    , ("repeated", repeated)
+    , ("scattered", scatterA)
+    , ("scattered-scrambled", scatterB)
+    ]
+
+-- | Operand pairs whose sites are separated by a vertical line, which is the
+-- stratum a seam schedule is defined on and the one the old-edge identity is
+-- sharpest over: with no shared site, every edge is unambiguously left, right
+-- or cross.
+separatedOperands :: IO [((String, Mesh), (String, Mesh))]
+separatedOperands =
+  traverse
+    ( \(name, seed, count, shift) -> do
+        let sites = randomSites seed count
+        left <- meshOf (name <> "-left") sites
+        right <- meshOf (name <> "-right") [Point (x + shift) y | Point x y <- sites]
+        pure ((name <> "-left", left), (name <> "-right", right))
+    )
+    [ ("split-distant", 0x51DE1, 60, 1000)
+    , ("split-near", 0x51DE2, 60, 3)
+    , ("split-abutting", 0x51DE3, 60, 2.05)
+    ]
+
+-- ── construction ─────────────────────────────────────────────────────────────
+
+meshOf :: String -> [Point] -> IO Mesh
+meshOf label points =
+  geometryOnlyPublication
+    <$> requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))
+
+-- | Build through the annotated entrance when a law deliberately needs the
+-- physical numbering induced by input order. Geometry-only construction is
+-- free to choose the cheaper numbering because it publishes no input mapping.
+inputOrderedMeshOf :: String -> [Point] -> IO Mesh
+inputOrderedMeshOf label points =
+  mapVertices (const ()) . buildTriangulation
+    <$> requireRight
+          ("build input-ordered geometry " <> label)
+          (delaunay unitElementDefaults (V.fromList points))
+
+pointsOf :: [(Double, Double)] -> [Point]
+pointsOf keys = [Point x y | (x, y) <- keys]
+
+dedupeAscending :: [(Double, Double)] -> [Point]
+dedupeAscending sorted = [Point x y | (x, y) <- dropAdjacentDuplicates sorted]
+
+dropAdjacentDuplicates :: Eq a => [a] -> [a]
+dropAdjacentDuplicates (first : second : rest)
+  | first == second = dropAdjacentDuplicates (second : rest)
+  | otherwise = first : dropAdjacentDuplicates (second : rest)
+dropAdjacentDuplicates rest = rest
+
+-- ── observation ──────────────────────────────────────────────────────────────
+
+siteList :: Mesh -> [(Double, Double)]
+siteList mesh = [siteKey mesh vertex | vertex <- vertices mesh]
+
+siteSet :: Mesh -> Set.Set (Double, Double)
+siteSet = Set.fromList . siteList
+
+-- | What a mesh looks like when two of them were supposed to be equal.
+--
+-- The interesting failure is the one where the coordinate-keyed edge sets
+-- agree and the values do not: that is the join having become correct only up
+-- to DCEL isomorphism, which is precisely what these laws exist to forbid, and
+-- a report that only printed counts would hide it.
+assertMesh :: String -> Mesh -> Mesh -> IO ()
+assertMesh label expected actual
+  | expected == actual = pure ()
+  | otherwise = fail (label <> ": " <> report)
+ where
+  report
+    | expectedEdges == actualEdges =
+        "same geometry, different representation — "
+          <> meshCounts expected
+          <> " vs "
+          <> meshCounts actual
+    | otherwise =
+        meshCounts expected
+          <> " vs "
+          <> meshCounts actual
+          <> "; edges only in expected: "
+          <> show (take 4 (Set.toList (Set.difference expectedEdges actualEdges)))
+          <> "; only in actual: "
+          <> show (take 4 (Set.toList (Set.difference actualEdges expectedEdges)))
+  expectedEdges = edgeKeys expected
+  actualEdges = edgeKeys actual
+
+assertMeshEquivalent :: String -> Mesh -> Mesh -> IO ()
+assertMeshEquivalent label expected actual = do
+  canonicalExpected <-
+    requireRight (label <> ": canonical expected") (canonicalize expected)
+  canonicalActual <-
+    requireRight (label <> ": canonical actual") (canonicalize actual)
+  assertMesh label canonicalExpected canonicalActual
+
+meshCounts :: Mesh -> String
+meshCounts mesh =
+  show (numVertices mesh)
+    <> "v/"
+    <> show (numUndirectedEdges mesh)
+    <> "e/"
+    <> show (numFaces mesh)
+    <> "f"
+
+-- ── combinatorics and pseudo-randomness ──────────────────────────────────────
+
+pairs :: [a] -> [(a, a)]
+pairs values = [(left, right) | left <- values, right <- values]
+
+triples :: [a] -> [(a, a, a)]
+triples values = [(a, b, c) | a <- values, b <- values, c <- values]
+
+advance :: Word64 -> Word64
+advance state = state * 6364136223846793005 + 1442695040888963407
+
+randomWords :: Word64 -> Int -> [Word64]
+randomWords seed count = take count (drop 1 (iterate advance seed))
+
+-- | A deterministic permutation: decorate, sort by the key, discard it.
+scramble :: Word64 -> [a] -> [a]
+scramble seed values =
+  map snd (sortBy (comparing fst) (zip (randomWords seed (length values)) values))
+
+randomSites :: Word64 -> Int -> [Point]
+randomSites seed count =
+  [ Point (unitCoordinate first) (unitCoordinate second)
+  | (first, second) <- take count (chunkPairs (randomWords seed (2 * count)))
+  ]
+
+chunkPairs :: [a] -> [(a, a)]
+chunkPairs (first : second : rest) = (first, second) : chunkPairs rest
+chunkPairs _ = []
+
+unitCoordinate :: Word64 -> Double
+unitCoordinate value = 2 * fromIntegral (value `div` 2048) / 9007199254740992 - 1
diff --git a/test/algebra/Moonlight/Planar/AlgebraSpec.hs b/test/algebra/Moonlight/Planar/AlgebraSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/AlgebraSpec.hs
@@ -0,0 +1,906 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | The laws of finite union, stated against the typed facade a caller has.
+--
+-- Algebraic laws are observed through 'canonicalize'. Structural 'Eq' remains
+-- the exact physical-representation observation used by round trips and caches.
+module Moonlight.Planar.AlgebraSpec (tests) where
+
+import Control.Monad (forM_, unless, when)
+import Data.Foldable (traverse_)
+import Data.IORef (modifyIORef', newIORef, readIORef)
+import Data.List (sort)
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import Data.Word (Word64)
+import qualified Data.Vector as V
+import Moonlight.Algebra.Pure.Join (JoinSemilattice (..))
+import Moonlight.Planar.BulkLoad (DuplicatePayloadPolicy (..), delaunay, delaunayFromCoordinates, delaunayGeometry)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.Dcel (vertexData, vertexPoint, numVertices)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Payload (mapVertices)
+import Moonlight.Planar.SetAlgebra (difference, intersection, intersectionWith, siteRelation, symmetricDifference, union, unions)
+import Moonlight.Planar.Types (BuildError, ConstraintMode (..), DelaunayTriangulation, Triangulation, buildTriangulation, unitElementDefaults, SiteRelation (..))
+import Moonlight.Planar.Point (HasPosition (..), Point (Point))
+import Moonlight.Planar.Validation (validateTriangulation)
+import Moonlight.Planar.AlgebraFixtures
+  ( Mesh
+  , advance
+  , assertMesh
+  , assertMeshEquivalent
+  , cocircularRing
+  , collinearSites
+  , dedupeAscending
+  , inputOrderedMeshOf
+  , latticeSites
+  , meshOf
+  , operands
+  , pairs
+  , pointsOf
+  , randomSites
+  , scramble
+  , separatedOperands
+  , siteList
+  , siteSet
+  , triples
+  )
+import Moonlight.Planar.MeshFixtures (edgeKeys, pointMeshOf)
+import Support (requireRight)
+
+tests :: IO ()
+tests = do
+  testConstructionEntrances
+  testJoinIdentity
+  testJoinCommutative
+  testJoinAssociative
+  testJoinIdempotent
+  testJoinGeneralPathIdempotent
+  testJoinSiteUnion
+  testJoinGluesAnnotations
+  testCentralJoinInstances
+  testJoinIsUnionRebuild
+  testJoinResultValid
+  testSkewedJoinPreservesBaseHandles
+  testJoinBalancedFold
+  testJoinPartitionTrees
+  testConstructionOrderIndependent
+  testCanonicalPublication
+  testSiteRelationCoherence
+  testMeetLaws
+  testEmptySetIdentitiesPreserveVerbatim
+  testSkewedSetOperationsRemainValid
+  testDifferenceLaws
+  testSymmetricDifferenceLaws
+  testLatticeLaws
+  testSetOperationsPublishCanonical
+  testAnnotationPreservation
+  testOldEdgeAccounting
+  putStrLn "algebra: ok"
+
+testCentralJoinInstances :: IO ()
+testCentralJoinInstances = do
+  let expected = Set.fromList [1 :: Int, 2, 3]
+      actual = join (Set.fromList [1, 2]) (Set.fromList [2, 3])
+  unless (actual == expected) $
+    fail ("triangulation did not expose the canonical set join instance: " <> show actual)
+
+testConstructionEntrances :: IO ()
+testConstructionEntrances = do
+  let coordinates = V.fromList [Point 0 0, Point 3 0, Point 0 4, Point 0 0]
+      payloads = V.fromList [2 :: Int, 3, 5, 7]
+  geometry <- requireRight "geometry-only construction" (delaunayGeometry coordinates)
+  legacy <-
+    mapVertices (const ()) . buildTriangulation
+      <$> requireRight "annotated point construction" (delaunay unitElementDefaults coordinates)
+  assertMeshEquivalent
+    "geometry-only construction agrees with annotated point construction"
+    legacy
+    geometry
+  annotated <-
+    buildTriangulation
+      <$> requireRight
+            "coordinate/payload construction"
+            ( delaunayFromCoordinates
+                unitElementDefaults
+                coordinates
+                payloads
+                (CombineDuplicatePayload (+))
+            )
+  unless (validateTriangulation geometry == [] && validateTriangulation annotated == []) $
+    fail "public construction entrance produced an invalid triangulation"
+  let observed =
+        Map.fromList
+          [ ((x, y), vertexData annotated vertex)
+          | vertex <- vertices annotated
+          , let Point x y = vertexPoint annotated vertex
+          ]
+      expected = Map.fromList [((0, 0), 9), ((3, 0), 3), ((0, 4), 5)]
+  unless (observed == expected) $
+    fail "delaunayFromCoordinates did not apply its duplicate payload policy"
+
+-- ── laws ─────────────────────────────────────────────────────────────────────
+
+testJoinIdentity :: IO ()
+testJoinIdentity = do
+  values <- operands
+  identity <- requireRight "union identity" (unions [])
+  forM_ values $ \(name, mesh) -> do
+    left <- requireRight ("left identity at " <> name) (union identity mesh)
+    right <- requireRight ("right identity at " <> name) (union mesh identity)
+    assertMesh ("left identity at " <> name) mesh left
+    assertMesh ("right identity at " <> name) mesh right
+  idempotent <- requireRight "identity is idempotent" (union identity identity)
+  assertMesh "identity is idempotent" identity idempotent
+
+testJoinCommutative :: IO ()
+testJoinCommutative = do
+  values <- operands
+  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
+    leftRight <- requireRight "commutative union left/right" (union left right)
+    rightLeft <- requireRight "commutative union right/left" (union right left)
+    assertMeshEquivalent
+      ("commutativity at " <> leftName <> " ⋄ " <> rightName)
+      leftRight
+      rightLeft
+
+testJoinAssociative :: IO ()
+testJoinAssociative = do
+  values <- operands
+  forM_ (triples values) $ \((aName, a), (bName, b), (cName, c)) -> do
+    leftAssociated <- requireRight "left-associated union" (union a b >>= (`union` c))
+    rightAssociated <- requireRight "right-associated union" (union b c >>= union a)
+    assertMeshEquivalent
+      ("associativity at " <> aName <> " ⋄ " <> bName <> " ⋄ " <> cName)
+      leftAssociated
+      rightAssociated
+
+testJoinIdempotent :: IO ()
+testJoinIdempotent = do
+  values <- operands
+  forM_ values $ \(name, mesh) -> do
+    result <- requireRight ("idempotence at " <> name) (union mesh mesh)
+    assertMesh ("idempotence at " <> name) mesh result
+
+-- | Idempotence with the structural-equality shortcut deliberately disarmed.
+--
+-- @a \<\> a@ is answered by a shortcut that compares the operands and returns
+-- one verbatim, so on its own it says nothing about the operator underneath.
+-- Three meshes standing on the same sites, built in three different orders, are
+-- pairwise distinct as values — the test asserts that before relying on it —
+-- so every join below takes the general path, and joining a fourth
+-- representation of the same site set onto the result must still change
+-- nothing.
+testJoinGeneralPathIdempotent :: IO ()
+testJoinGeneralPathIdempotent = do
+  let base = randomSites 0xA11CE 40
+  first <- inputOrderedMeshOf "order-1" base
+  second <- inputOrderedMeshOf "order-2" (scramble 0x1111 base)
+  third <- inputOrderedMeshOf "order-3" (scramble 0x2222 base)
+  fourth <- inputOrderedMeshOf "order-4" (reverse base)
+  unless (first /= second && second /= third && third /= fourth) $
+    fail "general-path idempotence: the four builds are not distinct values, so the shortcut is not disarmed"
+  joined <- requireRight "general-path union" (union first second)
+  thirdResult <- requireRight "absorbing a third representation" (union joined third)
+  fourthResult <- requireRight "absorbing a fourth representation" (union joined fourth)
+  symmetric <- requireRight "the union of two representations is symmetric" (union second first)
+  assertMeshEquivalent "absorbing a third representation" joined thirdResult
+  assertMeshEquivalent "absorbing a fourth representation" joined fourthResult
+  assertMeshEquivalent "the union of two representations is symmetric" joined symmetric
+
+-- | The sites of a join are exactly the union of the operands' sites: none
+-- dropped, none invented, each stored once.
+testJoinSiteUnion :: IO ()
+testJoinSiteUnion = do
+  values <- operands
+  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
+    let label = leftName <> " ⋄ " <> rightName
+        expected = Set.union (siteSet left) (siteSet right)
+    joined <- requireRight ("site union at " <> label) (union left right)
+    let actual = siteSet joined
+    unless (expected == actual) $
+      fail
+        ( "site union at "
+            <> label
+            <> ": dropped "
+            <> show (Set.toList (Set.difference expected actual))
+            <> ", invented "
+            <> show (Set.toList (Set.difference actual expected))
+        )
+    unless (Set.size actual == numVertices joined) $
+      fail ("site union at " <> label <> ": a site is stored more than once")
+
+-- | The reference semantics: a join /is/ a rebuild of the union when both are
+-- observed canonically.
+--
+-- The union is rebuilt three times, in three unrelated orders, and all three
+-- must canonicalize to the canonical observation of the join. Comparing
+-- against a single rebuild would only say the two agree; comparing against
+-- three says that what they agree on is a function of the site set and not of
+-- any build schedule, which is the actual claim.
+--
+-- Both shortcut cases are excluded, because a shortcut returns an operand
+-- verbatim and an operand need not be canonically published. The count of
+-- pairs that actually reached the general path is asserted, so this cannot
+-- quietly become a test of nothing.
+testJoinIsUnionRebuild :: IO ()
+testJoinIsUnionRebuild = do
+  values <- operands
+  exercised <- newIORef (0 :: Int)
+  forM_ (pairs values) $ \((leftName, left), (rightName, right)) ->
+    when (numVertices left > 0 && numVertices right > 0 && left /= right) $ do
+      let label = leftName <> " ⋄ " <> rightName
+          unionSites = siteList left <> siteList right
+      joined <- requireRight ("union of " <> label) (union left right)
+      canonicalJoined <- requireRight ("canonical union of " <> label) (canonicalize joined)
+      forM_ [("ranked", dedupeAscending (sort unionSites)), ("reversed", pointsOf (reverse unionSites)), ("scrambled", pointsOf (scramble 0x7A57E unionSites))] $
+        \(order, sites) -> do
+          rebuilt <- meshOf ("rebuild of " <> label <> " in " <> order <> " order") sites
+          canonical <- requireRight ("canonical rebuild of " <> label <> " in " <> order <> " order") (canonicalize rebuilt)
+          assertMesh
+            ("join equals canonical rebuild at " <> label <> " (" <> order <> ")")
+            canonical
+            canonicalJoined
+      modifyIORef' exercised (+ 1)
+  count <- readIORef exercised
+  unless (count >= 40) $
+    fail ("join-equals-rebuild exercised only " <> show count <> " general-path pairs")
+
+-- | Canonical publication in its own right, since the union laws lean on it.
+--
+-- Renumbering must not move geometry, must be a fixed point, must leave a
+-- valid triangulation, and — the load-bearing one — must send every build
+-- order of a site set to the same value. The distinctness of the inputs is
+-- asserted first, so a canonicalization that did nothing at all would fail
+-- here rather than pass everything.
+testCanonicalPublication :: IO ()
+testCanonicalPublication = do
+  values <- operands
+  forM_ values $ \(name, mesh) -> do
+    canonical <- requireRight ("canonicalization at " <> name) (canonicalize mesh)
+    unless (siteSet canonical == siteSet mesh) $
+      fail ("canonicalization at " <> name <> " moved the site set")
+    unless (edgeKeys canonical == edgeKeys mesh) $
+      fail ("canonicalization at " <> name <> " changed the triangulation")
+    case validateTriangulation canonical of
+      [] -> pure ()
+      violations -> fail ("canonicalization at " <> name <> " is invalid: " <> show violations)
+    fixedPoint <- requireRight ("canonicalization fixed point at " <> name) (canonicalize canonical)
+    assertMesh ("canonicalization at " <> name <> " is a fixed point") canonical fixedPoint
+  forM_ [("scattered", randomSites 0xB0A710 48), ("cocircular", cocircularRing), ("lattice", latticeSites), ("collinear", collinearSites)] $
+    \(name, base) -> do
+      built <-
+        traverse
+          (\(order, sites) -> (,) order <$> inputOrderedMeshOf (name <> "/" <> order) sites)
+          [ ("input", base)
+          , ("ranked", dedupeAscending (sort [(x, y) | Point x y <- base]))
+          , ("reversed", reverse base)
+          , ("scrambled", scramble 0x6666 base)
+          ]
+      case built of
+        [] -> fail "canonical publication: nothing built"
+        (_, reference) : rest -> do
+          unless (any (\(_, mesh) -> mesh /= reference) rest) $
+            fail ("canonical publication at " <> name <> ": every order already agreed, so this asserts nothing")
+          canonicalReference <- requireRight ("canonical reference at " <> name) (canonicalize reference)
+          forM_ rest $ \(order, mesh) -> do
+            canonicalMesh <- requireRight ("canonical publication at " <> name <> " from " <> order <> " order") (canonicalize mesh)
+            assertMesh
+              ("canonical publication at " <> name <> " from " <> order <> " order")
+              canonicalReference
+              canonicalMesh
+
+testJoinResultValid :: IO ()
+testJoinResultValid = do
+  values <- operands
+  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
+    joined <- requireRight ("valid union at " <> leftName <> " ⋄ " <> rightName) (union left right)
+    case validateTriangulation joined of
+      [] -> pure ()
+      violations ->
+        fail ("join at " <> leftName <> " ⋄ " <> rightName <> " is invalid: " <> show violations)
+
+testSkewedJoinPreservesBaseHandles :: IO ()
+testSkewedJoinPreservesBaseHandles = do
+  let baseSites = randomSites 0x5A71E 256
+      extensionSites =
+        fmap
+          (\(Point x y) -> Point (x + 4) y)
+          (randomSites 0xE71E 16)
+  base <- meshOf "persistent base" baseSites
+  extension <- meshOf "persistent extension" extensionSites
+  joined <- requireRight "persistent skewed union" (union base extension)
+  let baseObservations = fmap (\vertex -> (vertex, vertexPoint base vertex)) (vertices base)
+  forM_ baseObservations $ \(vertex, expectedPoint) ->
+    unless (vertexPoint joined vertex == expectedPoint) $
+      fail
+        ( "persistent skewed union moved base handle "
+            <> show vertex
+            <> " from "
+            <> show expectedPoint
+            <> " to "
+            <> show (vertexPoint joined vertex)
+        )
+  canonicalJoined <- requireRight "canonical persistent skewed union" (canonicalize joined)
+  reference <- meshOf "persistent union reference" (baseSites <> extensionSites)
+  canonicalReference <- requireRight "canonical persistent union reference" (canonicalize reference)
+  assertMesh "persistent skewed union agrees with canonical reference" canonicalReference canonicalJoined
+
+-- | The balanced tournament agrees with every typed fold, in both directions.
+testJoinBalancedFold :: IO ()
+testJoinBalancedFold = do
+  shards <- shardMeshes 6 (randomSites 0xBA5EBA11 48)
+  identity <- requireRight "empty union" (unions [])
+  case shards of
+    [] -> fail "balanced fold: no shards were built"
+    firstShard : _ -> do
+      expected <- requireRight "balanced unions" (unions shards)
+      foldRight <- requireRight "right-folded unions" (foldr (\shard result -> result >>= union shard) (Right identity) shards)
+      foldLeft <- requireRight "left-folded unions" (foldl (\result shard -> result >>= (`union` shard)) (Right identity) shards)
+      singleton <- requireRight "singleton unions" (unions [firstShard])
+      repeated <- requireRight "repeated unions" (unions [firstShard, firstShard])
+      assertMeshEquivalent "balanced unions agree with foldr" expected foldRight
+      assertMeshEquivalent "balanced unions agree with foldl" expected foldLeft
+      assertMesh "unions of one shard is that shard" firstShard singleton
+      assertMesh "unions respects the idempotence shortcut" firstShard repeated
+
+-- | The document's partition test: one site set, many shardings, many
+-- bracketings, one canonical observation.
+testJoinPartitionTrees :: IO ()
+testJoinPartitionTrees = do
+  let base = randomSites 0xD15EA5E 54
+  whole <- meshOf "whole" base
+  reference <- requireRight "canonical whole partition reference" (canonicalize whole)
+  forM_ ([2, 3, 5, 7] :: [Int]) $ \shardCount -> do
+    shards <- shardMeshes shardCount base
+    forM_ ([0 .. 7] :: [Int]) $ \shape -> do
+      let scrambled = scramble (fromIntegral shape * 7919 + 13) shards
+      result <-
+        requireRight
+          ("partition tree " <> show shardCount <> "/" <> show shape)
+          (bracketRandomly (fromIntegral shape * 104729 + 7) scrambled)
+      assertMeshEquivalent
+        ("partition tree " <> show shardCount <> "/" <> show shape)
+        reference
+        result
+
+-- | Delaunay uniqueness, stated as a law about this construction: the same
+-- sites in any order give the same triangulation, and differ only in how it is
+-- numbered.
+--
+-- Both halves are asserted, and the second is the one that carries weight. If
+-- the numbering did /not/ differ, canonical publication would be a no-op and
+-- the quotient this test names would be trivial. Because it does differ, this
+-- law licenses construction in whichever order is cheapest and an explicit
+-- canonical observation only where physical agreement is required.
+--
+-- The cocircular ring and the lattice are here because they are where it could
+-- fail: an exactly flat lifted quadrilateral has two legal diagonals, and the
+-- rule that picks between them is keyed on coordinates rather than on vertex
+-- identifiers precisely so that this law holds.
+testConstructionOrderIndependent :: IO ()
+testConstructionOrderIndependent = do
+  forM_ [("scattered", randomSites 0x0DDBA11 60), ("cocircular", cocircularRing), ("lattice", latticeSites)] $
+    \(name, base) -> do
+      let ranked = dedupeAscending (sort [(x, y) | Point x y <- base])
+      meshes <-
+        traverse
+          (\(order, sites) -> (,) order <$> inputOrderedMeshOf (name <> "/" <> order) sites)
+          [ ("input", base)
+          , ("ranked", ranked)
+          , ("reversed", reverse base)
+          , ("scrambled-a", scramble 0x3333 base)
+          , ("scrambled-b", scramble 0x4444 base)
+          ]
+      case meshes of
+        [] -> fail "order independence: nothing built"
+        (referenceOrder, reference) : rest -> do
+          forM_ rest $ \(order, mesh) ->
+            unless (edgeKeys reference == edgeKeys mesh) $
+              fail
+                ( "order independence at "
+                    <> name
+                    <> ": "
+                    <> referenceOrder
+                    <> " and "
+                    <> order
+                    <> " reached different triangulations"
+                )
+          unless (any (\(_, mesh) -> mesh /= reference) rest) $
+            fail
+              ( "order independence at "
+                  <> name
+                  <> ": every build order produced the identical value, so the"
+                  <> " numbering quotient is trivial and this law asserts nothing"
+              )
+
+-- | The Guibas–Stolfi old-edge theorem, asserted as a law of the operator.
+--
+-- Adding sites to a Delaunay triangulation cannot create an edge between two
+-- sites that were already there. So every edge of a join whose endpoints both
+-- stood in one operand must already have been an edge of that operand, and any
+-- other edge is bichromatic — it joins a site exclusive to the left to a site
+-- exclusive to the right.
+--
+-- This is strictly stronger than checking that the result is a valid Delaunay
+-- triangulation, and it is stronger in exactly the direction a merge schedule
+-- fails in. A seam that stitches the wrong pair of hull vertices, or that
+-- retracts one edge too far before it stops deleting, produces a mesh that is
+-- still Delaunay for /some/ site set and still passes validation; what it does
+-- not do is leave the two interiors alone. The identity names that.
+--
+-- It holds for every schedule, including the rebuild the operator uses today,
+-- because it is a property of the Delaunay triangulation of the union and not
+-- of the route taken to it. That is why it can be asserted before a second
+-- schedule exists: it is the gate one would have to pass, green on the path
+-- that is already trusted.
+--
+-- Degeneracy does not weaken it. The tie-break on an exactly cocircular
+-- quadrilateral is keyed on the four coordinates alone, so the same quad
+-- resolves the same way whatever else stands nearby; a cocircular diagonal can
+-- therefore be lost when a site lands inside its circle, which the theorem
+-- permits, but cannot be exchanged for the other diagonal, which it forbids.
+testOldEdgeAccounting :: IO ()
+testOldEdgeAccounting = do
+  overlapping <- operands
+  separated <- separatedOperands
+  census <- newIORef (0 :: Int, 0 :: Int)
+  forM_ ([(l, r) | l <- overlapping, r <- overlapping] <> separated) $ \((leftName, left), (rightName, right)) -> do
+    let label = "old-edge " <> leftName <> " / " <> rightName
+    joined <- requireRight label (union left right)
+    let leftSites = siteSet left
+        rightSites = siteSet right
+        leftEdges = edgeKeys left
+        rightEdges = edgeKeys right
+    forM_ (Set.toList (edgeKeys joined)) $ \edge -> do
+      let (from, to) = edge
+          spans sites = Set.member from sites && Set.member to sites
+          monochromeLeft = spans leftSites
+          monochromeRight = spans rightSites
+      when (monochromeLeft && not (Set.member edge leftEdges)) $
+        fail (label <> ": join created " <> show edge <> ", an edge between two sites of the left operand")
+      when (monochromeRight && not (Set.member edge rightEdges)) $
+        fail (label <> ": join created " <> show edge <> ", an edge between two sites of the right operand")
+      modifyIORef' census $ \(retained, cross) ->
+        if monochromeLeft || monochromeRight
+          then (retained + 1, cross)
+          else (retained, cross + 1)
+  (retained, cross) <- readIORef census
+  when (retained < 1000 || cross < 100) $
+    fail
+      ( "old-edge accounting: the census is too thin to have asserted anything — "
+          <> show retained
+          <> " retained and "
+          <> show cross
+          <> " cross edges"
+      )
+
+-- ── finite-set descent laws ─────────────────────────────────────────────────
+
+-- | The public support relation is the exact order observation of the same
+-- site sets consumed by union, meet and relative complement. Its overlap
+-- witness must therefore agree with every operation's cardinality rather than
+-- merely with another classification routine.
+testSiteRelationCoherence :: IO ()
+testSiteRelationCoherence = do
+  values <- operands
+  traverse_ (uncurry checkRelation) (pairs values)
+ where
+  checkRelation (leftName, left) (rightName, right) = do
+    let label = "site relation at " <> leftName <> " / " <> rightName
+        leftSites = siteSet left
+        rightSites = siteSet right
+        overlap = Set.size (Set.intersection leftSites rightSites)
+        expected = referenceSiteRelation leftSites rightSites
+        coldActual = siteRelation left right
+    unless (coldActual == expected) $
+      fail (label <> ": expected " <> show expected <> ", got " <> show coldActual)
+    let repeatedActual = siteRelation left right
+    unless (repeatedActual == coldActual) $
+      fail (label <> ": repeated observation changed the relation")
+    met <- requireRight (label <> " intersection") (intersection left right)
+    removed <- requireRight (label <> " difference") (difference left right)
+    joined <- requireRight (label <> " union") (union left right)
+    unless (numVertices met == overlap) $
+      fail (label <> ": intersection cardinality disagrees with overlap")
+    unless (numVertices removed == Set.size leftSites - overlap) $
+      fail (label <> ": difference cardinality disagrees with overlap")
+    unless (numVertices joined == Set.size leftSites + Set.size rightSites - overlap) $
+      fail (label <> ": union cardinality disagrees with overlap")
+
+referenceSiteRelation
+  :: Set.Set (Double, Double)
+  -> Set.Set (Double, Double)
+  -> SiteRelation
+referenceSiteRelation left right
+  | left == right = EqualSites
+  | left `Set.isProperSubsetOf` right = LeftProperSubset
+  | right `Set.isProperSubsetOf` left = RightProperSubset
+  | Set.null overlap = DisjointSites
+  | otherwise = PartialOverlap (Set.size overlap)
+ where
+  overlap = Set.intersection left right
+
+testMeetLaws :: IO ()
+testMeetLaws = do
+  values <- setLawOperands
+  traverse_
+    ( \(name, mesh) ->
+        assertSetEquation
+          ("meet idempotence at " <> name)
+          (intersection mesh mesh)
+          (canonicalize mesh)
+    )
+    values
+  traverse_
+    ( \((leftName, left), (rightName, right)) ->
+        assertSetEquation
+          ("meet commutativity at " <> leftName <> " / " <> rightName)
+          (intersection left right)
+          (intersection right left)
+    )
+    (pairs values)
+  traverse_
+    ( \((aName, a), (bName, b), (cName, c)) ->
+        assertSetEquation
+          ("meet associativity at " <> aName <> " / " <> bName <> " / " <> cName)
+          (intersection a b >>= (`intersection` c))
+          (intersection b c >>= intersection a)
+    )
+    (triples values)
+
+testEmptySetIdentitiesPreserveVerbatim :: IO ()
+testEmptySetIdentitiesPreserveVerbatim = do
+  let sites = scramble 0xE771D3 (randomSites 0xE771D4 48)
+  mesh <- inputOrderedMeshOf "verbatim empty identity source" sites
+  canonical <- requireRight "canonical verbatim empty identity source" (canonicalize mesh)
+  unless (mesh /= canonical) $
+    fail "verbatim empty identities: the source was already canonical, so this asserts nothing"
+  empty <- requireRight "verbatim empty identity" (unions [])
+  differenceRightIdentity <- requireRight "verbatim difference right identity" (difference mesh empty)
+  differenceLeftZero <- requireRight "verbatim difference left zero" (difference empty mesh)
+  symmetricRightIdentity <- requireRight "verbatim symmetric difference right identity" (symmetricDifference mesh empty)
+  symmetricLeftIdentity <- requireRight "verbatim symmetric difference left identity" (symmetricDifference empty mesh)
+  assertMesh "difference by empty preserves the left representative verbatim" mesh differenceRightIdentity
+  assertMesh "empty difference remains the empty representative" empty differenceLeftZero
+  assertMesh "symmetric difference by empty preserves the left representative verbatim" mesh symmetricRightIdentity
+  assertMesh "empty symmetric difference preserves the right representative verbatim" mesh symmetricLeftIdentity
+
+testSkewedSetOperationsRemainValid :: IO ()
+testSkewedSetOperationsRemainValid = do
+  let baseSites = randomSites 0xD1FFE7 1024
+      removedSites = take 4 (scramble 0xD1FFE8 baseSites)
+      retainedSites = filter (`notElem` removedSites) baseSites
+      disjointIncomingSites = fmap (\(Point x y) -> Point (x + 4) y) (randomSites 0xD1FFE9 4)
+      nearFullIntersectionSites = retainedSites <> disjointIncomingSites
+      overlappingRemovedSites = take 2 removedSites
+      overlappingIncomingSites = overlappingRemovedSites <> take 2 disjointIncomingSites
+      xorDisjointSites = baseSites <> disjointIncomingSites
+      xorOverlappingSites = filter (`notElem` overlappingRemovedSites) baseSites <> take 2 disjointIncomingSites
+  base <- meshOf "skewed set-operation base" baseSites
+  removed <- meshOf "skewed difference subset" removedSites
+  disjointIncoming <- meshOf "skewed difference disjoint" disjointIncomingSites
+  nearFullIntersection <- meshOf "skewed geometry-only intersection" nearFullIntersectionSites
+  overlappingIncoming <- meshOf "skewed xor overlapping" overlappingIncomingSites
+  differenceSubset <- requireRight "skewed difference subset" (difference base removed)
+  differenceDisjoint <- requireRight "skewed difference disjoint" (difference base disjointIncoming)
+  intersectionNearFull <- requireRight "skewed geometry-only intersection" (intersection base nearFullIntersection)
+  xorDisjoint <- requireRight "skewed xor disjoint" (symmetricDifference base disjointIncoming)
+  xorOverlapping <- requireRight "skewed xor overlapping" (symmetricDifference base overlappingIncoming)
+  assertRawSetResult "skewed difference subset" retainedSites differenceSubset
+  assertRawSetResult "skewed difference disjoint" baseSites differenceDisjoint
+  assertMesh "skewed difference disjoint preserves the base verbatim" base differenceDisjoint
+  assertRawSetResult "skewed geometry-only intersection" retainedSites intersectionNearFull
+  assertRawSetResult "skewed xor disjoint" xorDisjointSites xorDisjoint
+  assertRawSetResult "skewed xor overlapping" xorOverlappingSites xorOverlapping
+
+assertRawSetResult :: String -> [Point] -> Mesh -> IO ()
+assertRawSetResult label expectedSites result = do
+  case validateTriangulation result of
+    [] -> pure ()
+    violations -> fail (label <> " raw result is invalid: " <> show violations)
+  reference <- meshOf (label <> " fresh rebuild") (scramble 0xD1FFEA expectedSites)
+  assertMeshEquivalent (label <> " agrees canonically with a fresh rebuild") reference result
+
+testDifferenceLaws :: IO ()
+testDifferenceLaws = do
+  values <- setLawOperands
+  identity <- requireRight "difference identity" (unions [])
+  traverse_
+    ( \(name, mesh) -> do
+        assertSetEquation
+          ("difference cancellation at " <> name)
+          (difference mesh mesh)
+          (Right identity)
+        assertSetEquation
+          ("difference right identity at " <> name)
+          (difference mesh identity)
+          (canonicalize mesh)
+    )
+    values
+  traverse_
+    ( \((leftName, left), (rightName, right)) -> do
+        let label = "difference partition at " <> leftName <> " / " <> rightName
+            commonAndRemainder = do
+              common <- intersection left right
+              remainder <- difference left right
+              union common remainder
+            remainderMeetRight = difference left right >>= (`intersection` right)
+        assertSetEquation label commonAndRemainder (canonicalize left)
+        assertSetEquation (label <> " disjointness") remainderMeetRight (Right identity)
+    )
+    (pairs values)
+
+testSymmetricDifferenceLaws :: IO ()
+testSymmetricDifferenceLaws = do
+  values <- setLawOperands
+  identity <- requireRight "symmetric-difference identity" (unions [])
+  traverse_
+    ( \(name, mesh) -> do
+        assertSetEquation
+          ("symmetric-difference cancellation at " <> name)
+          (symmetricDifference mesh mesh)
+          (Right identity)
+        assertSetEquation
+          ("symmetric-difference identity at " <> name)
+          (symmetricDifference mesh identity)
+          (canonicalize mesh)
+    )
+    values
+  traverse_
+    ( \((leftName, left), (rightName, right)) -> do
+        let label = "symmetric difference at " <> leftName <> " / " <> rightName
+            decomposed = do
+              leftOnly <- difference left right
+              rightOnly <- difference right left
+              union leftOnly rightOnly
+        assertSetEquation
+          (label <> " commutativity")
+          (symmetricDifference left right)
+          (symmetricDifference right left)
+        assertSetEquation
+          (label <> " decomposition")
+          (symmetricDifference left right)
+          decomposed
+    )
+    (pairs values)
+  traverse_
+    ( \((aName, a), (bName, b), (cName, c)) ->
+        assertSetEquation
+          ("symmetric-difference associativity at " <> aName <> " / " <> bName <> " / " <> cName)
+          (symmetricDifference a b >>= (`symmetricDifference` c))
+          (symmetricDifference b c >>= symmetricDifference a)
+    )
+    (triples values)
+
+testLatticeLaws :: IO ()
+testLatticeLaws = do
+  values <- setLawOperands
+  traverse_
+    ( \((leftName, left), (rightName, right)) -> do
+        let label = "absorption at " <> leftName <> " / " <> rightName
+        assertSetEquation
+          (label <> " meet-over-join")
+          (union left right >>= intersection left)
+          (canonicalize left)
+        assertSetEquation
+          (label <> " join-over-meet")
+          (intersection left right >>= union left)
+          (canonicalize left)
+    )
+    (pairs values)
+  traverse_
+    ( \((aName, a), (bName, b), (cName, c)) -> do
+        let label = "distributivity at " <> aName <> " / " <> bName <> " / " <> cName
+            meetOverJoin = union b c >>= intersection a
+            joinedMeets = do
+              left <- intersection a b
+              right <- intersection a c
+              union left right
+            joinOverMeet = intersection b c >>= union a
+            metJoins = do
+              left <- union a b
+              right <- union a c
+              intersection left right
+        assertSetEquation (label <> " meet-over-join") meetOverJoin joinedMeets
+        assertSetEquation (label <> " join-over-meet") joinOverMeet metJoins
+    )
+    (triples values)
+
+testSetOperationsPublishCanonical :: IO ()
+testSetOperationsPublishCanonical = do
+  values <- setLawOperands
+  traverse_
+    ( \((leftName, left), (rightName, right)) -> do
+        let label = "canonical set publication at " <> leftName <> " / " <> rightName
+        case siteRelation left right of
+          DisjointSites -> assertCanonicalFixedPoint (label <> " intersection") (intersection left right)
+          PartialOverlap _ -> assertCanonicalFixedPoint (label <> " intersection") (intersection left right)
+          EqualSites -> pure ()
+          LeftProperSubset -> pure ()
+          RightProperSubset -> pure ()
+        when (numVertices left > 0 && numVertices right > 0) $ do
+          assertCanonicalFixedPoint (label <> " difference") (difference left right)
+          assertCanonicalFixedPoint (label <> " symmetric difference") (symmetricDifference left right)
+    )
+    (pairs values)
+
+-- | Payloads descend with their coordinates. Intersection combines only the
+-- common sections, difference preserves the left section, and symmetric
+-- difference preserves whichever unique section survives. The independently
+-- built expected meshes ensure this is not a restatement of the implementation.
+testAnnotationPreservation :: IO ()
+testAnnotationPreservation = do
+  let shared = randomSites 0xA6607A7E 12
+      leftOnly = randomSites 0x1EF7 8
+      rightOnly = randomSites 0xA1167 10
+      leftPoints = leftOnly <> shared
+      rightPoints = shared <> rightOnly
+  left <- pointMeshOf "annotation-left" leftPoints
+  rightPointsMesh <- pointMeshOf "annotation-right" rightPoints
+  sharedReference <- canonicalPointMesh "annotation-shared" shared
+  leftOnlyReference <- canonicalPointMesh "annotation-left-only" leftOnly
+  exclusiveReference <- canonicalPointMesh "annotation-exclusive" (leftOnly <> rightOnly)
+  let right = mapVertices rightAnnotation rightPointsMesh
+      expectedCombined = mapVertices (\point -> (point, rightAnnotation point)) sharedReference
+      taggedLeft = mapVertices (\point -> ExclusiveAnnotation point LeftOperand) left
+      taggedRight = mapVertices (\point -> ExclusiveAnnotation point RightOperand) rightPointsMesh
+      taggedExclusive =
+        mapVertices
+          (\point -> ExclusiveAnnotation point (if point `elem` leftOnly then LeftOperand else RightOperand))
+          exclusiveReference
+  unless (siteRelation left right == PartialOverlap (length shared)) $
+    fail "siteRelation changed when the vertex annotation type changed"
+  combined <- requireRight "annotation intersectionWith" (intersectionWith (,) left right)
+  restricted <- requireRight "annotation restriction" (intersectionWith const left right)
+  removed <- requireRight "annotation difference" (difference left right)
+  exclusive <- requireRight "annotation symmetric difference" (symmetricDifference taggedLeft taggedRight)
+  assertAnnotatedMesh "intersectionWith combines left then right" expectedCombined combined
+  assertAnnotatedMesh "intersectionWith const restricts the left section" sharedReference restricted
+  assertAnnotatedMesh "difference preserves left annotations" leftOnlyReference removed
+  assertAnnotatedMesh "symmetric difference preserves the annotation of each exclusive owner" taggedExclusive exclusive
+
+data ExclusiveOperand
+  = LeftOperand
+  | RightOperand
+  deriving stock (Eq)
+
+data ExclusiveAnnotation = ExclusiveAnnotation !(Point) !ExclusiveOperand
+  deriving stock (Eq)
+
+rightAnnotation :: Point -> (Double, Double)
+rightAnnotation (Point x y) = (x + y, x - y)
+
+canonicalPointMesh :: String -> [Point] -> IO (DelaunayTriangulation (Point))
+canonicalPointMesh label points =
+  pointMeshOf label points
+    >>= requireRight ("canonicalize " <> label) . canonicalize
+
+assertAnnotatedMesh :: Eq mesh => String -> mesh -> mesh -> IO ()
+assertAnnotatedMesh label expected actual =
+  unless (expected == actual) (fail (label <> ": structural values differ"))
+
+assertSetEquation
+  :: String
+  -> Either BuildError Mesh
+  -> Either BuildError Mesh
+  -> IO ()
+assertSetEquation label leftExpression rightExpression = do
+  left <- requireRight (label <> " left") leftExpression
+  right <- requireRight (label <> " right") rightExpression
+  leftCanonical <- requireRight (label <> " canonical left") (canonicalize left)
+  rightCanonical <- requireRight (label <> " canonical right") (canonicalize right)
+  assertMesh label leftCanonical rightCanonical
+
+assertCanonicalFixedPoint :: String -> Either BuildError Mesh -> IO ()
+assertCanonicalFixedPoint label expression = do
+  result <- requireRight label expression
+  fixedPoint <- requireRight (label <> " fixed point") (canonicalize result)
+  assertMesh label result fixedPoint
+
+-- | Six representatives retain empty, degenerate, nested, partially
+-- overlapping and equal-support/different-publication cases without turning
+-- every ternary law into the full nine-cubed join corpus already exercised
+-- above.
+setLawOperands :: IO [(String, Mesh)]
+setLawOperands =
+  filter
+    (\(name, _) -> name `elem` ["void", "single", "lattice-lower", "lattice-upper", "scattered", "scattered-scrambled"])
+    <$> operands
+
+-- ── construction ─────────────────────────────────────────────────────────────
+
+-- | Deal the sites round-robin into @count@ shards, so every shard spans the
+-- whole extent and the joins are genuinely overlapping rather than separable.
+shardMeshes :: Int -> [Point] -> IO [Mesh]
+shardMeshes count sites =
+  traverse
+    (\shard -> meshOf ("shard " <> show shard) [site | (index, site) <- indexed, index `mod` count == shard])
+    [0 .. count - 1]
+ where
+  indexed = zip [0 :: Int ..] sites
+
+-- | Combine by repeatedly joining a pseudo-randomly chosen adjacent pair, so
+-- that successive seeds give genuinely different bracketings.
+bracketRandomly :: Word64 -> [Mesh] -> Either BuildError Mesh
+bracketRandomly _ [] = unions []
+bracketRandomly _ [single] = Right single
+bracketRandomly seed values =
+  let !stepped = advance seed
+      !cut = fromIntegral (stepped `mod` fromIntegral (length values - 1))
+   in joinAdjacent cut values >>= bracketRandomly stepped
+
+-- | Replace the pair at @index@ with its join, leaving everything else alone.
+joinAdjacent :: Int -> [Mesh] -> Either BuildError [Mesh]
+joinAdjacent index (left : right : rest)
+  | index <= 0 = (: rest) <$> union left right
+  | otherwise = (left :) <$> joinAdjacent (index - 1) (right : rest)
+joinAdjacent _ rest = Right rest
+
+-- | A site is a coordinate carrying a tag. The join acts on tags alone: two
+-- annotations only ever meet at a shared coordinate, where the positions
+-- already agree, so geometry is never authored by the payload.
+data Site = Site !(Point) !Int
+  deriving stock (Eq, Show)
+
+instance HasPosition Site where
+  position (Site point _) = point
+
+instance JoinSemilattice Site where
+  join (Site point left) (Site _ right) = Site point (max left right)
+
+type AnnotatedMesh = Triangulation 'Unconstrained Site () () ()
+
+annotatedMesh :: String -> [Site] -> IO AnnotatedMesh
+annotatedMesh label sites =
+  buildTriangulation <$> requireRight label (delaunay unitElementDefaults (V.fromList sites))
+
+-- Keyed on the coordinate plane rather than on the payload's own point,
+-- because geometry is authoritative and a payload that disagreed with it must
+-- not be able to hide behind itself.
+tagsOf :: AnnotatedMesh -> Map.Map (Double, Double) Int
+tagsOf mesh =
+  Map.fromList
+    [ ((x, y), tag)
+    | vertex <- vertices mesh
+    , let Point x y = vertexPoint mesh vertex
+    , let Site _ tag = vertexData mesh vertex
+    ]
+
+-- The whole law in one equation: the union's annotation at every site is the
+-- join of whatever the operands carried there.
+assertGluing :: String -> [Site] -> [Site] -> IO ()
+assertGluing label leftSites rightSites = do
+  left <- annotatedMesh (label <> " left") leftSites
+  right <- annotatedMesh (label <> " right") rightSites
+  glued <- requireRight (label <> " union") (union left right)
+  assertEqualTags
+    (label <> ": union glues shared annotations by join")
+    (Map.unionWith max (tagsOf left) (tagsOf right))
+    (tagsOf glued)
+
+assertEqualTags :: String -> Map.Map (Double, Double) Int -> Map.Map (Double, Double) Int -> IO ()
+assertEqualTags label expected actual =
+  unless (expected == actual) $
+    fail (label <> ": expected " <> show (Map.toList expected) <> ", got " <> show (Map.toList actual))
+
+-- The plan is chosen by size: a small addition is inserted into the larger
+-- operand, and only comparably sized operands are rebuilt canonically. Both
+-- paths glue, and only running both shows it.
+testJoinGluesAnnotations :: IO ()
+testJoinGluesAnnotations = do
+  assertGluing
+    "insertion plan"
+    [Site (Point 0 0) 1, Site (Point 6 0) 1, Site (Point 0 6) 1, Site (Point 2 1) 7]
+    [Site (Point 0 0) 5, Site (Point 6 0) 2, Site (Point 0 6) 3, Site (Point 4 3) 9]
+  assertGluing "rebuild plan" (grid 0 1) (grid 6 2)
+ where
+  -- Two hundred-odd sites overlapping in half, so neither operand is small
+  -- enough to be inserted into the other and the canonical rebuild is taken.
+  grid offset tag =
+    [ Site (Point (fromIntegral column) (fromIntegral row)) (tag * (column + row))
+    | column <- [offset .. offset + 11 :: Int]
+    , row <- [0 .. 9 :: Int]
+    ]
diff --git a/test/algebra/Moonlight/Planar/ConvexSpec.hs b/test/algebra/Moonlight/Planar/ConvexSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/ConvexSpec.hs
@@ -0,0 +1,103 @@
+-- | Convex admission and publication laws at the exact-geometry owner.
+module Moonlight.Planar.ConvexSpec (tests) where
+
+import Data.Foldable (traverse_)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Convex
+  ( ConvexError (..)
+  , convexHullPolygon
+  , convexPolygon
+  , convexPolygonCentroid
+  , convexPolygonFromLoop
+  , convexPolygonFromRetained
+  , convexPolygonPoints
+  , convexPolygonRegion
+  , reflectConvexPolygon
+  )
+import Moonlight.Planar.Exact (exactOrient2d, exactPoint, exactRetainedPolygon)
+import Moonlight.Planar.Region
+  ( RegionPointLocation (..)
+  , exactLoop
+  , regionPointLocation
+  )
+import Support (assertEqual, requireRight)
+
+tests :: IO ()
+tests = sequence_
+  [ testConvexAdmission
+  , testConvexHull
+  , testRetainedPublication
+  , testConvexReflection
+  , putStrLn "convex: ok"
+  ]
+
+testConvexAdmission :: IO ()
+testConvexAdmission = do
+  let points = exactPoint 0 0 :| [exactPoint 3 0, exactPoint 3 2, exactPoint 0 2]
+  polygon <- requireRight "convex rectangle" (convexPolygon points)
+  rotated <- requireRight "rotated rectangle"
+    (convexPolygon (exactPoint 3 2 :| [exactPoint 0 2, exactPoint 0 0, exactPoint 3 0]))
+  assertEqual "convex admission canonicalizes rotation" polygon rotated
+  loop <- requireRight "admitted rectangle loop" (exactLoop points)
+  assertEqual "admitted loop strengthens without readmission" (Just polygon) (convexPolygonFromLoop loop)
+  centroid <- requireRight "convex interior witness" (convexPolygonCentroid polygon)
+  assertEqual "convex witness lies inside" RegionInterior (regionPointLocation (convexPolygonRegion polygon) centroid)
+  case convexPolygon (exactPoint 0 0 :| [exactPoint 0 2, exactPoint 3 2, exactPoint 3 0]) of
+    Left (ConvexNonConvexTurn _ LT) -> pure ()
+    outcome -> fail ("clockwise convex cycle was not refused: " <> show outcome)
+  let top = exactPoint 0 3
+      lowerLeft = exactPoint (-2) (-3)
+      right = exactPoint 3 1
+      left = exactPoint (-3) 1
+      lowerRight = exactPoint 2 (-3)
+      star = top :| [lowerLeft, right, left, lowerRight]
+  assertEqual "star has strictly positive local turns" (replicate 5 GT)
+    [ exactOrient2d top lowerLeft right
+    , exactOrient2d lowerLeft right left
+    , exactOrient2d right left lowerRight
+    , exactOrient2d left lowerRight top
+    , exactOrient2d lowerRight top lowerLeft
+    ]
+  case convexPolygon star of
+    Left (ConvexInvalidLoop _) -> pure ()
+    outcome -> fail ("local turns must not admit a self-intersecting star: " <> show outcome)
+
+testConvexHull :: IO ()
+testConvexHull = do
+  let points = exactPoint 0 0 :| [exactPoint 2 0, exactPoint 2 2, exactPoint 0 2]
+  expected <- requireRight "canonical square hull" (convexPolygon points)
+  traverse_
+    (\permutation ->
+       case NonEmpty.nonEmpty permutation of
+         Nothing -> fail "a nonempty permutation became empty"
+         Just submitted -> do
+           hull <- requireRight "permuted convex hull" (convexHullPolygon submitted)
+           assertEqual "convex hull is permutation invariant" expected hull)
+    (List.permutations (NonEmpty.toList points))
+  hull <- requireRight "hull discards interior points and repeated vertices"
+    (convexHullPolygon (points <> (exactPoint 1 1 :| [exactPoint 0 0])))
+  assertEqual "convex hull keeps only extreme points" expected hull
+  let collinear = exactPoint 0 0 :| [exactPoint 1 0, exactPoint 2 0]
+  assertEqual "degenerate hull is a convex error"
+    (Left (ConvexHullDegenerate (NonEmpty.toList collinear)))
+    (convexHullPolygon collinear)
+
+testRetainedPublication :: IO ()
+testRetainedPublication = do
+  let rotated = exactPoint 2 2 :| [exactPoint 0 2, exactPoint 0 0, exactPoint 2 0]
+  retained <- requireRight "retained convex polygon" (exactRetainedPolygon rotated)
+  checked <- requireRight "checked retained polygon" (convexPolygon rotated)
+  assertEqual "trusted retained publication agrees with checked admission"
+    checked (convexPolygonFromRetained retained)
+
+testConvexReflection :: IO ()
+testConvexReflection = do
+  triangle <- requireRight "asymmetric convex triangle"
+    (convexPolygon (exactPoint 0 0 :| [exactPoint 3 0, exactPoint 1 2]))
+  let reflected = reflectConvexPolygon triangle
+  readmitted <- requireRight "reflection preserves counter-clockwise orientation"
+    (convexPolygon (convexPolygonPoints reflected))
+  assertEqual "reflected carrier remains admissible" reflected readmitted
+  assertEqual "convex reflection is involutive" triangle (reflectConvexPolygon reflected)
diff --git a/test/algebra/Moonlight/Planar/ExactClipRetentionSpec.hs b/test/algebra/Moonlight/Planar/ExactClipRetentionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/ExactClipRetentionSpec.hs
@@ -0,0 +1,581 @@
+-- | Independent endpoint-reconstruction oracle for the retained-line clipper.
+--
+-- The oracle is deliberately test-only. Production has one clipping authority:
+-- 'exactClipRetainedPolygon'.
+module Moonlight.Planar.ExactClipRetentionSpec (tests) where
+
+import Control.Monad (foldM, unless)
+import Data.Foldable (traverse_)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Maybe (maybeToList)
+import Moonlight.Planar.Convex
+  ( convexPolygon, convexPolygonPoints, convexPolygonFromRetained
+  , retainConvexPolygon, retainTranslatedConvexPolygon )
+import Moonlight.Planar.Exact
+  ( ExactAffineLine
+  , ExactClipDisposition (..)
+  , ExactClipReceipt (..)
+  , ExactClosedHalfPlane
+  , ExactHalfPlaneError (..)
+  , ExactIntersectionError
+  , ExactPoint
+  , ExactRetainedPolygon
+  , ExactVector (..)
+  , SegmentRelation (..)
+  , classifyExactPoint
+  , exactAffineLine
+  , exactAffineLineCoefficients
+  , exactAffineLineIntersection
+  , exactClipRetainedPolygon
+  , exactClosedHalfPlane
+  , exactClosedHalfPlaneFromDirectedEdge
+  , exactClosedHalfPlaneLine
+  , exactOrient2d
+  , exactPoint
+  , exactPointCoordinates
+  , exactRetainedPolygon
+  , exactRetainedPolygonPoints
+  , translateExactPoint
+  , emptyExactClipReceipt
+  )
+import Moonlight.Planar.Internal.BoundaryCycle
+  ( cyclePairsNonEmpty
+  , cyclicTriples
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRational
+  , exactRationalBitWidth
+  , exactRationalDenominatorBitWidth
+  )
+import Support (assertEqual, integerPoint, requireRight)
+
+tests :: IO ()
+tests = do
+  testTrustedConvexRetention
+  testSignedReceiptCombination
+  testAffineIntersectionCrossProducts
+  testSelfIntersectingAllLeftCycleRefused
+  testRetainedClosingLineSurvivesSecondClip
+  testAngularHalfPlanePermutation
+  testClosedDimensionalDispositions
+  initialPolygon <-
+    requireRight
+      "retained-line fixture polygon"
+      ( exactRetainedPolygon
+          ( integerPoint (-1000) (-1000)
+              :| [ integerPoint 1000 (-1000)
+                 , integerPoint 1000 1000
+                 , integerPoint (-1000) 1000
+                 ]
+          )
+      )
+  halfPlanes <- traverse fixtureHalfPlane fixtureCoefficients
+  (retainedDisposition, retainedReceipt) <-
+    requireRight
+      "retained-line fixture clipping"
+      (exactClipRetainedPolygon initialPolygon halfPlanes)
+  retainedPoints <-
+    case retainedDisposition of
+      ExactClipFullDimensional polygon -> pure (exactRetainedPolygonPoints polygon)
+      other -> fail ("retained-line fixture lost full dimension: " <> show other)
+  (endpointPoints, endpointReceipt) <-
+    requireRight
+      "endpoint-reconstruction oracle"
+      (endpointClipSequence (exactRetainedPolygonPoints initialPolygon) halfPlanes)
+
+  assertExactPointSet "final exact polygon" retainedPoints endpointPoints
+  assertEqual
+    "final reduced denominator width is representation invariant"
+    (exactClipFinalDenominatorBits retainedReceipt)
+    (endpointFinalDenominatorBits endpointReceipt)
+  assertEqual "measured retained coefficient width" 21 (exactClipMaximumAffineCoefficientBits retainedReceipt)
+  assertEqual "measured endpoint coefficient width" 32 (endpointPeakAffineCoefficientBits endpointReceipt)
+  unless
+    ( endpointPeakAffineCoefficientBits endpointReceipt
+        > exactClipMaximumAffineCoefficientBits retainedReceipt
+    )
+    ( fail
+        "endpoint reconstruction did not exhibit strictly larger affine coefficients"
+    )
+
+  -- Equality is checked at every local section, not merely after global gluing.
+  -- This is the reason no honest coordinate-width counterexample can exist.
+  traverse_
+    (assertPrefixAgreement initialPolygon)
+    (List.inits halfPlanes)
+  putStrLn "exact retained-line clipping: ok (source coefficients 21 bits; endpoint oracle 32 bits)"
+
+testTrustedConvexRetention :: IO ()
+testTrustedConvexRetention = do
+  oneThird <- requireRight "translation x" (exactRational 1 3)
+  twoFifths <- requireRight "translation y" (exactRational (-2) 5)
+  let shift = ExactVector oneThird twoFifths
+      pointAt :: Int -> ExactPoint
+      pointAt index = exactPoint (fromIntegral index) (fromIntegral (index * index))
+      fixtures =
+        [ pointAt 0 :| fmap pointAt [1 .. count - 1]
+        | count <- [3, 4, 8, 32]
+        ]
+      check points = do
+        polygon <- requireRight "trusted convex fixture" (convexPolygon points)
+        checked <- requireRight "checked retained oracle" (exactRetainedPolygon (convexPolygonPoints polygon))
+        checkedShift <- requireRight "checked translated oracle"
+          (exactRetainedPolygon (fmap (`translateExactPoint` shift) (convexPolygonPoints polygon)))
+        assertEqual "trusted retention preserves complete lines and one-vertex rotation"
+          checked (retainConvexPolygon polygon)
+        assertEqual "trusted translated retention preserves complete lines and rotation"
+          checkedShift (retainTranslatedConvexPolygon shift polygon)
+        assertEqual "convex publication retracts retention"
+          polygon (convexPolygonFromRetained (retainConvexPolygon polygon))
+  traverse_ check fixtures
+
+-- Signed public receipts form a Semigroup, not a Monoid under zero/max.
+testSignedReceiptCombination :: IO ()
+testSignedReceiptCombination = do
+  let left = ExactClipReceipt 1 2 3 4 (-9) (-8) (-7) (-6) (-5)
+      middle = ExactClipReceipt 2 3 4 5 (-4) (-3) (-2) (-1) (-9)
+      right = ExactClipReceipt 3 4 5 6 (-7) (-6) (-5) (-4) (-3)
+  assertEqual "signed clip receipt associativity"
+    ((left <> middle) <> right) (left <> (middle <> right))
+  assertEqual "empty clip work is explicitly zero"
+    (ExactClipReceipt 0 0 0 0 0 0 0 0 0) emptyExactClipReceipt
+  unless (left <> emptyExactClipReceipt /= left) $
+    fail "negative-width witness no longer refutes the old Monoid identity"
+
+testAffineIntersectionCrossProducts :: IO ()
+testAffineIntersectionCrossProducts = do
+  oneHalf <- rationalCoefficient 1 2
+  oneThird <- rationalCoefficient 1 3
+  twoFifths <- rationalCoefficient 2 5
+  negativeOneSeventh <- rationalCoefficient (-1) 7
+  negativeOne <- rationalCoefficient (-1) 1
+  oneEleventh <- rationalCoefficient 1 11
+  firstLine <-
+    requireRight
+      "first rational intersection line"
+      (exactAffineLine oneHalf oneThird negativeOne)
+  secondLine <-
+    requireRight
+      "second rational intersection line"
+      (exactAffineLine twoFifths negativeOneSeventh oneEleventh)
+  intersection <-
+    requireRight
+      "homogeneous integer affine intersection"
+      (exactAffineLineIntersection firstLine secondLine)
+  expectedX <- requireRight "expected rational x" (exactRational 260 473)
+  expectedY <- requireRight "expected rational y" (exactRational 1029 473)
+  assertEqual
+    "homogeneous integer intersection agrees with rational Cramer's rule"
+    (exactPoint expectedX expectedY)
+    intersection
+  assertEqual
+    "intersection remains on the first source line"
+    EQ
+    (classifyExactPoint (exactClosedHalfPlane firstLine) intersection)
+  assertEqual
+    "intersection remains on the second source line"
+    EQ
+    (classifyExactPoint (exactClosedHalfPlane secondLine) intersection)
+ where
+  rationalCoefficient :: Integer -> Integer -> IO ExactRational
+  rationalCoefficient numerator denominator =
+    requireRight
+      "rational intersection coefficient"
+      (exactRational numerator denominator)
+
+testAngularHalfPlanePermutation :: IO ()
+testAngularHalfPlanePermutation = do
+  square <- retainedSquare
+  halfPlanes <-
+    traverse
+      fixtureHalfPlane
+      [ (1, 0, -2)
+      , (2, 0, -4)
+      , (-1, 0, 8)
+      , (0, 1, -1)
+      , (0, -1, 9)
+      ]
+  (expected, expectedReceipt) <-
+    requireRight
+      "angular half-plane canonical result"
+      (exactClipRetainedPolygon square halfPlanes)
+  case expected of
+    ExactClipFullDimensional polygon ->
+      assertExactPointSet
+        "parallel descent retains the strongest boundaries"
+        ( integerPoint 2 1
+            :| [integerPoint 8 1, integerPoint 8 9, integerPoint 2 9]
+        )
+        (exactRetainedPolygonPoints polygon)
+    other -> fail ("expected a full-dimensional canonical section, got " <> show other)
+  traverse_
+    (\permutation -> do
+       (actual, receipt) <-
+         requireRight
+           "permuted angular half-plane intersection"
+           (exactClipRetainedPolygon square permutation)
+       assertEqual "angular half-plane permutation" expected actual
+       assertEqual
+         "submitted half-plane count"
+         (length halfPlanes)
+         (exactClipSubmittedHalfPlanes receipt))
+    (List.permutations halfPlanes)
+  unless (exactClipBoundaryCompatibilityChecks expectedReceipt > 0) $
+    fail "angular descent reported no boundary compatibility work"
+
+testClosedDimensionalDispositions :: IO ()
+testClosedDimensionalDispositions = do
+  square <- retainedSquare
+  xAtOne <- traverse fixtureHalfPlane [(1, 0, -1), (-1, 0, 1)]
+  (segmentDisposition, _) <-
+    requireRight
+      "closed segment half-plane intersection"
+      (exactClipRetainedPolygon square xAtOne)
+  case segmentDisposition of
+    ExactClipLowerDimensional points ->
+      assertEqual
+        "opposing closed half-planes retain their shared segment"
+        (List.sort [integerPoint 1 0, integerPoint 1 10])
+        (List.sort (NonEmpty.toList points))
+    other -> fail ("expected a closed segment, got " <> show other)
+
+  yAtThree <- traverse fixtureHalfPlane [(0, 1, -3), (0, -1, 3)]
+  (pointDisposition, _) <-
+    requireRight
+      "closed point half-plane intersection"
+      (exactClipRetainedPolygon square (xAtOne <> yAtThree))
+  case pointDisposition of
+    ExactClipLowerDimensional points ->
+      assertEqual
+        "two zero-width closed sections retain their shared point"
+        [integerPoint 1 3]
+        (List.sort (NonEmpty.toList points))
+    other -> fail ("expected a closed point, got " <> show other)
+
+  incompatible <- traverse fixtureHalfPlane [(1, 0, -6), (-1, 0, 5)]
+  (emptyDisposition, _) <-
+    requireRight
+      "incompatible closed half-plane intersection"
+      (exactClipRetainedPolygon square incompatible)
+  assertEqual "separated opposing boundaries are empty" ExactClipEmpty emptyDisposition
+
+retainedSquare :: IO ExactRetainedPolygon
+retainedSquare =
+  requireRight
+    "retained square"
+    ( exactRetainedPolygon
+        ( integerPoint 0 0
+            :| [integerPoint 10 0, integerPoint 10 10, integerPoint 0 10]
+        )
+    )
+
+testSelfIntersectingAllLeftCycleRefused :: IO ()
+testSelfIntersectingAllLeftCycleRefused = do
+  let points =
+        integerPoint 0 0
+          :| [ integerPoint 5 3
+             , integerPoint (-1) 3
+             , integerPoint 4 0
+             , integerPoint 2 5
+             ]
+      turns =
+        fmap
+          (\(previous, current, next) -> exactOrient2d previous current next)
+          (cyclicTriples (NonEmpty.toList points))
+  assertEqual "self-intersection fixture has only local left turns" (replicate 5 GT) turns
+  case exactRetainedPolygon points of
+    Left (ExactRetainedPolygonSelfRelation 0 2 SegmentsProperlyCross) -> pure ()
+    other ->
+      fail
+        ( "self-intersecting all-left retained polygon: expected edge 0/2 crossing, got "
+            <> show other
+        )
+
+-- The first clip emits its first point twice. The closing occurrence carries
+-- the source line of the retained bottom edge; preserving the opening record
+-- instead corrupts that edge into the square's stale right boundary and makes
+-- the second clip spuriously report parallel lines.
+testRetainedClosingLineSurvivesSecondClip :: IO ()
+testRetainedClosingLineSurvivesSecondClip = do
+  square <-
+    requireRight
+      "retained square"
+      ( exactRetainedPolygon
+          ( integerPoint 0 0
+              :| [ integerPoint 10 0
+                 , integerPoint 10 10
+                 , integerPoint 0 10
+                 ]
+          )
+      )
+  diagonalLine <- requireRight "diagonal clipping line" (exactAffineLine (-1) (-1) 10)
+  verticalLine <- requireRight "vertical clipping line" (exactAffineLine (-1) 0 5)
+  let diagonalHalfPlane = exactClosedHalfPlane diagonalLine
+      verticalHalfPlane = exactClosedHalfPlane verticalLine
+      retainedHalfPlanes = diagonalHalfPlane :| [verticalHalfPlane]
+  (diagonalDisposition, _) <-
+    requireRight
+      "retained square diagonal clip"
+      (exactClipRetainedPolygon square [diagonalHalfPlane])
+  diagonalPolygon <-
+    requireFullDimensionalClip
+      "retained square diagonal clip"
+      diagonalDisposition
+  assertExactPointSet
+    "first clip retains the expected triangle"
+    (integerPoint 0 0 :| [integerPoint 10 0, integerPoint 0 10])
+    (exactRetainedPolygonPoints diagonalPolygon)
+  (sequentialDisposition, _) <-
+    requireRight
+      "second clip over the retained first result"
+      (exactClipRetainedPolygon diagonalPolygon [verticalHalfPlane])
+  sequentialPolygon <-
+    requireFullDimensionalClip
+      "second clip over the retained first result"
+      sequentialDisposition
+  (combinedDisposition, combinedReceipt) <-
+    requireRight
+      "combined retained two-clip descent"
+      (exactClipRetainedPolygon square (NonEmpty.toList retainedHalfPlanes))
+  combinedPolygon <-
+    requireFullDimensionalClip
+      "combined retained two-clip descent"
+      combinedDisposition
+  let expectedFinalPoints =
+        integerPoint 0 0
+          :| [integerPoint 5 0, integerPoint 5 5, integerPoint 0 10]
+  assertExactPointSet
+    "second clip intersects the retained bottom and diagonal source lines"
+    expectedFinalPoints
+    (exactRetainedPolygonPoints sequentialPolygon)
+  assertEqual
+    "sequential and combined clipping preserve the same retained polygon"
+    sequentialPolygon
+    combinedPolygon
+  assertEqual
+    "combined clipping records both submitted half-planes"
+    2
+    (exactClipSubmittedHalfPlanes combinedReceipt)
+  assertRetainedVerticesInside
+    retainedHalfPlanes
+    (exactRetainedPolygonPoints combinedPolygon)
+
+requireFullDimensionalClip
+  :: String
+  -> ExactClipDisposition
+  -> IO ExactRetainedPolygon
+requireFullDimensionalClip _ (ExactClipFullDimensional polygon) = pure polygon
+requireFullDimensionalClip label disposition =
+  fail (label <> ": expected a full-dimensional polygon, got " <> show disposition)
+
+assertExactPointSet
+  :: String
+  -> NonEmpty ExactPoint
+  -> NonEmpty ExactPoint
+  -> IO ()
+assertExactPointSet label expected actual =
+  assertEqual
+    label
+    (List.sort (NonEmpty.toList expected))
+    (List.sort (NonEmpty.toList actual))
+
+assertRetainedVerticesInside
+  :: NonEmpty ExactClosedHalfPlane
+  -> NonEmpty ExactPoint
+  -> IO ()
+assertRetainedVerticesInside halfPlanes points =
+  traverse_
+    (\halfPlane ->
+       traverse_
+         (\point ->
+            unless (classifyExactPoint halfPlane point /= LT) $
+              fail ("retained vertex violates clipping half-plane: " <> show point))
+         points)
+    halfPlanes
+
+data EndpointOracleError
+  = EndpointOracleHalfPlane !ExactHalfPlaneError
+  | EndpointOracleIntersection !ExactIntersectionError
+  | EndpointOracleLostFullDimension ![ExactPoint]
+  deriving stock (Eq, Show)
+
+data EndpointOracleReceipt = EndpointOracleReceipt
+  { endpointPeakAffineCoefficientBits :: !Int
+  , endpointPeakCoordinateBits :: !Int
+  , endpointFinalDenominatorBits :: !Int
+  }
+  deriving stock (Eq, Show)
+
+data EndpointEdgeClip = EndpointEdgeClip
+  { endpointEdgePoints :: ![ExactPoint]
+  , endpointEdgeReconstructedCoefficientBits :: !(Maybe Int)
+  }
+  deriving stock (Eq, Show)
+
+endpointClipSequence
+  :: NonEmpty ExactPoint
+  -> [ExactClosedHalfPlane]
+  -> Either EndpointOracleError (NonEmpty ExactPoint, EndpointOracleReceipt)
+endpointClipSequence initialPoints halfPlanes = do
+  let initialReceipt =
+        EndpointOracleReceipt
+          { endpointPeakAffineCoefficientBits = 0
+          , endpointPeakCoordinateBits = pointCycleBitWidth initialPoints
+          , endpointFinalDenominatorBits = pointCycleDenominatorBitWidth initialPoints
+          }
+  (finalPoints, accumulatedReceipt) <-
+    foldM endpointClipStep (initialPoints, initialReceipt) halfPlanes
+  pure
+    ( finalPoints
+    , accumulatedReceipt
+        { endpointFinalDenominatorBits =
+            pointCycleDenominatorBitWidth finalPoints
+        }
+    )
+
+endpointClipStep
+  :: (NonEmpty ExactPoint, EndpointOracleReceipt)
+  -> ExactClosedHalfPlane
+  -> Either EndpointOracleError (NonEmpty ExactPoint, EndpointOracleReceipt)
+endpointClipStep (points, receipt) halfPlane = do
+  clippedEdges <-
+    traverse (clipEndpointEdge halfPlane) (cyclePairsNonEmpty points)
+  let clippedPoints = concatMap endpointEdgePoints clippedEdges
+      stepCoefficientBits =
+        maximumOrZero
+          ( foldMap
+              (maybeToList . endpointEdgeReconstructedCoefficientBits)
+              clippedEdges
+          )
+  nextPoints <-
+    maybe
+      (Left (EndpointOracleLostFullDimension clippedPoints))
+      Right
+      (NonEmpty.nonEmpty clippedPoints)
+  if NonEmpty.length nextPoints < 3
+    then Left (EndpointOracleLostFullDimension clippedPoints)
+    else
+      Right
+        ( nextPoints
+        , receipt
+            { endpointPeakAffineCoefficientBits =
+                max
+                  (endpointPeakAffineCoefficientBits receipt)
+                  stepCoefficientBits
+            , endpointPeakCoordinateBits =
+                max
+                  (endpointPeakCoordinateBits receipt)
+                  (pointCycleBitWidth nextPoints)
+            }
+        )
+
+clipEndpointEdge
+  :: ExactClosedHalfPlane
+  -> (ExactPoint, ExactPoint)
+  -> Either EndpointOracleError EndpointEdgeClip
+clipEndpointEdge halfPlane (from, to) =
+  let fromInside = classifyExactPoint halfPlane from /= LT
+      toInside = classifyExactPoint halfPlane to /= LT
+      crossing = do
+        reconstructedBoundary <-
+          either
+            (Left . EndpointOracleHalfPlane)
+            (Right . exactClosedHalfPlaneLine)
+            (exactClosedHalfPlaneFromDirectedEdge from to)
+        point <-
+          either
+            (Left . EndpointOracleIntersection)
+            Right
+            ( exactAffineLineIntersection
+                reconstructedBoundary
+                (exactClosedHalfPlaneLine halfPlane)
+            )
+        pure (point, affineLineBitWidth reconstructedBoundary)
+   in case (fromInside, toInside) of
+    (True, True) -> Right (EndpointEdgeClip [to] Nothing)
+    (True, False) ->
+      (\(point, coefficientBits) ->
+         EndpointEdgeClip [point] (Just coefficientBits))
+        <$> crossing
+    (False, True) ->
+      (\(point, coefficientBits) ->
+         EndpointEdgeClip [point, to] (Just coefficientBits))
+        <$> crossing
+    (False, False) -> Right (EndpointEdgeClip [] Nothing)
+
+assertPrefixAgreement
+  :: ExactRetainedPolygon
+  -> [ExactClosedHalfPlane]
+  -> IO ()
+assertPrefixAgreement initialPolygon prefix = do
+  (retainedDisposition, retainedReceipt) <-
+    requireRight
+      "retained-line prefix"
+      (exactClipRetainedPolygon initialPolygon prefix)
+  retainedPoints <-
+    case retainedDisposition of
+      ExactClipFullDimensional polygon -> pure (exactRetainedPolygonPoints polygon)
+      other -> fail ("retained-line prefix lost full dimension: " <> show other)
+  (endpointPoints, endpointReceipt) <-
+    requireRight
+      "endpoint prefix"
+      (endpointClipSequence (exactRetainedPolygonPoints initialPolygon) prefix)
+  assertExactPointSet "prefix exact polygon" retainedPoints endpointPoints
+  assertEqual
+    "prefix denominator width"
+    (exactClipFinalDenominatorBits retainedReceipt)
+    (endpointFinalDenominatorBits endpointReceipt)
+
+fixtureHalfPlane
+  :: (Integer, Integer, Integer)
+  -> IO ExactClosedHalfPlane
+fixtureHalfPlane (coefficientX, coefficientY, constant) =
+  exactClosedHalfPlane
+    <$> requireRight
+      "fixture affine half-plane"
+      ( exactAffineLine
+          (fromInteger coefficientX)
+          (fromInteger coefficientY)
+          (fromInteger constant)
+      )
+
+fixtureCoefficients :: [(Integer, Integer, Integer)]
+fixtureCoefficients =
+  [ (1, 2, 1300)
+  , (-3, 5, 1700)
+  , (-7, -2, 1900)
+  , (4, -9, 2200)
+  , (11, 3, 1800)
+  , (-5, 13, 2100)
+  , (-17, -4, 2300)
+  , (6, -19, 2400)
+  , (23, 7, 2500)
+  , (-8, 29, 2600)
+  ]
+
+affineLineBitWidth :: ExactAffineLine -> Int
+affineLineBitWidth line =
+  let (coefficientX, coefficientY, constant) = exactAffineLineCoefficients line
+   in maximumOrZero (fmap exactRationalBitWidth [coefficientX, coefficientY, constant])
+
+pointCycleBitWidth :: NonEmpty ExactPoint -> Int
+pointCycleBitWidth =
+  maximumOrZero
+    . concatMap
+      (\point ->
+         let (x, y) = exactPointCoordinates point
+          in fmap exactRationalBitWidth [x, y])
+
+pointCycleDenominatorBitWidth :: NonEmpty ExactPoint -> Int
+pointCycleDenominatorBitWidth =
+  maximumOrZero
+    . concatMap
+      (\point ->
+         let (x, y) = exactPointCoordinates point
+          in fmap exactRationalDenominatorBitWidth [x, y])
+
+maximumOrZero :: Foldable collection => collection Int -> Int
+maximumOrZero = List.foldl' max 0
diff --git a/test/algebra/Moonlight/Planar/ExactIncidencePublicationSpec.hs b/test/algebra/Moonlight/Planar/ExactIncidencePublicationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/ExactIncidencePublicationSpec.hs
@@ -0,0 +1,194 @@
+-- | Strict polygon observations of exact, non-triangular planar incidence.
+-- Raw cycles include multiple boundary components, bridges and pinches.
+module Moonlight.Planar.ExactIncidencePublicationSpec (tests) where
+
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import Data.Maybe (listToMaybe)
+import qualified Data.Set as Set
+import qualified Data.Vector.Unboxed as Vector
+import Moonlight.Planar.Exact (ExactPoint, exactPoint, exactPointCoordinates)
+import Moonlight.Planar.FloodFillIterator (BoundaryObstruction (..))
+import Moonlight.Planar.Internal.BoundaryCycle (cyclePairsNonEmpty, cyclicTriples, orderedPair)
+import Moonlight.Planar.Internal.HandleDefs (DirectedEdgeId (..), FaceId (..), VertexId (..))
+import Moonlight.Planar.Internal.Incidence (PlanarIncidence, admitPlanarIncidence)
+import Moonlight.Planar.Internal.PackedIndex (noIndex)
+import Moonlight.Planar.Internal.Paged (fromVector)
+import Moonlight.Planar.Internal.Region.Publication (planarRegionFromSelectedIncidence)
+import Moonlight.Planar.Region
+  ( PlanarRegion
+  , RegionPublicationError (..)
+  , exactLoop
+  , planarRegion
+  , polygonComponent
+  )
+import Support (assertEqual, integerPoint, requireRight)
+
+tests :: IO ()
+tests = do
+  testHoleCancellationAndInteriorDebris
+  testNestedIsland
+  testVertexContact
+  testConnectedPinchRefusal
+  testExactCoordinateAndRefusalBoundaries
+
+testHoleCancellationAndInteriorDebris :: IO ()
+testHoleCancellationAndInteriorDebris = do
+  let outer = square 0 0 6 6
+      inner = square 2 2 4 4
+      points = NonEmpty.toList outer <> NonEmpty.toList inner <> [integerPoint 1 1, integerPoint 5 5]
+  fixture <- admitFixture "annulus with bridge and isolated vertex" points
+    [ (0, 0 :| [3, 2, 1])
+    , (1, 0 :| [8, 0, 1, 2, 3])
+    , (1, 4 :| [7, 6, 5])
+    , (2, 4 :| [5, 6, 7])
+    ]
+    [(1, [9])]
+  annulus <- publishFixture "annulus publication" fixture (== FaceId 1)
+  expectedAnnulus <- expectedRegion [(outer, [NonEmpty.reverse inner])]
+  assertEqual "hole, bridge and isolated point publication" expectedAnnulus annulus
+  filled <- publishFixture "hole cancellation publication" fixture (/= FaceId 0)
+  expectedFilled <- expectedRegion [(outer, [])]
+  assertEqual "selected hole face cancels shared boundary darts" expectedFilled filled
+
+testNestedIsland :: IO ()
+testNestedIsland = do
+  let outer = square 0 0 10 10
+      hole = square 2 2 8 8
+      island = square 4 4 6 6
+  fixture <- admitFixture "nested island" (concatMap NonEmpty.toList [outer, hole, island])
+    [ (0, 0 :| [3, 2, 1])
+    , (1, 0 :| [1, 2, 3])
+    , (1, 4 :| [7, 6, 5])
+    , (2, 4 :| [5, 6, 7])
+    , (2, 8 :| [11, 10, 9])
+    , (3, 8 :| [9, 10, 11])
+    ]
+    []
+  published <- publishFixture "nested island publication" fixture (\face -> face == FaceId 1 || face == FaceId 3)
+  expected <- expectedRegion [(outer, [NonEmpty.reverse hole]), (island, [])]
+  assertEqual "disconnected island retains the containing component's hole" expected published
+
+testVertexContact :: IO ()
+testVertexContact = do
+  let left = square 0 0 2 2
+      right = square 2 2 4 4
+      points = NonEmpty.toList left <> [integerPoint 4 2, integerPoint 4 4, integerPoint 2 4]
+  fixture <- admitFixture "vertex-contact components" points
+    [ (0, 0 :| [3, 2, 6, 5, 4, 2, 1])
+    , (1, 0 :| [1, 2, 3])
+    , (2, 2 :| [4, 5, 6])
+    ]
+    []
+  published <- publishFixture "vertex contact publication" fixture (/= FaceId 0)
+  expected <- expectedRegion [(left, []), (right, [])]
+  assertEqual "vertex contact is two polygon components, not a pinch" expected published
+
+testConnectedPinchRefusal :: IO ()
+testConnectedPinchRefusal = do
+  fixture <- admitFixture "pinched annulus"
+    (NonEmpty.toList (square 0 0 6 6) <> [integerPoint 2 1, integerPoint 1 2])
+    [ (0, 0 :| [3, 2, 1])
+    , (1, 0 :| [1, 2, 3, 0, 5, 4])
+    , (2, 0 :| [4, 5])
+    ]
+    []
+  case uncurry planarRegionFromSelectedIncidence fixture (== FaceId 1) of
+    Left (RegionBoundaryObstruction (BoundaryPinch (VertexId 0) _ _)) -> pure ()
+    result -> fail ("connected pinch must be a publication refusal: " <> show result)
+  filled <- publishFixture "filled pinch publication" fixture (/= FaceId 0)
+  expected <- expectedRegion [(square 0 0 6 6, [])]
+  assertEqual "a refused observation does not invalidate exact incidence" expected filled
+
+testExactCoordinateAndRefusalBoundaries :: IO ()
+testExactCoordinateAndRefusalBoundaries = do
+  let shifted = fmap
+        (\point -> let (x, y) = exactPointCoordinates point in exactPoint (x + 2 ^ (60 :: Int)) y)
+        (square 0 0 1 1)
+  fixture@(incidence, exactPointAt) <- admitFixture "binary64-colliding coordinates"
+    (NonEmpty.toList shifted)
+    [(0, 0 :| [3, 2, 1]), (1, 0 :| [1, 2, 3])]
+    []
+  published <- publishFixture "exact-only coordinate publication" fixture (== FaceId 1)
+  expected <- expectedRegion [(shifted, [])]
+  assertEqual "no binary64 coordinate admission is required" expected published
+  assertEqual "unbounded selection refuses before publication"
+    (Left RegionUnboundedSelection)
+    (planarRegionFromSelectedIncidence incidence exactPointAt (const True))
+  empty <- requireRight "empty region" (planarRegion [])
+  assertEqual "empty selection never asks for coordinates"
+    (Right empty)
+    (planarRegionFromSelectedIncidence incidence (Left . RegionCoordinateMissing) (const False))
+  case planarRegionFromSelectedIncidence incidence (Left . RegionCoordinateMissing) (== FaceId 1) of
+    Left (RegionCoordinateMissing _) -> pure ()
+    result -> fail ("missing selected boundary coordinate must remain typed: " <> show result)
+
+square :: Integer -> Integer -> Integer -> Integer -> NonEmpty ExactPoint
+square minimumX minimumY maximumX maximumY =
+  integerPoint minimumX minimumY
+    :| [integerPoint maximumX minimumY, integerPoint maximumX maximumY, integerPoint minimumX maximumY]
+
+expectedRegion :: [(NonEmpty ExactPoint, [NonEmpty ExactPoint])] -> IO PlanarRegion
+expectedRegion boundaries = do
+  components <- traverse
+    (\(outer, holes) -> do
+      admittedOuter <- requireRight "expected outer loop" (exactLoop outer)
+      admittedHoles <- traverse (requireRight "expected hole loop" . exactLoop) holes
+      requireRight "expected polygon component" (polygonComponent admittedOuter admittedHoles))
+    boundaries
+  requireRight "expected region" (planarRegion components)
+
+type PublicationFixture =
+  (PlanarIncidence, VertexId -> Either RegionPublicationError ExactPoint)
+
+publishFixture :: String -> PublicationFixture -> (FaceId -> Bool) -> IO PlanarRegion
+publishFixture label fixture selected =
+  requireRight label (uncurry planarRegionFromSelectedIncidence fixture selected)
+
+-- | Test-only packing of explicitly supplied oriented face walks. It does not
+-- discover faces: the fixture supplies all incidence and containment choices.
+admitFixture
+  :: String
+  -> [ExactPoint]
+  -> [(Int, NonEmpty Int)]
+  -> [(Int, [Int])]
+  -> IO PublicationFixture
+admitFixture label points submittedCycles isolatedVertices = do
+  cycles <- maybe (fail (label <> ": missing fixture edge")) pure
+    (traverse numberCycle submittedCycles)
+  let records = Map.fromList
+        [ (edge, (vertex, [fromIntegral vertex, fromIntegral nextEdge, fromIntegral previousEdge, fromIntegral face]))
+        | (face, cycleVertices) <- cycles
+        , ((previousEdge, _), (edge, vertex), (nextEdge, _)) <- cyclicTriples (NonEmpty.toList cycleVertices)
+        ]
+      vertexRoots = Map.fromListWith min [(vertex, edge) | (edge, (vertex, _)) <- Map.toAscList records]
+      faceRoots = Map.fromListWith (flip (<>)) [(face, [fst (NonEmpty.head cycleVertices)]) | (face, cycleVertices) <- cycles]
+      packed = fromVector noIndex . Vector.fromList
+      rootAt index roots = maybe noIndex fromIntegral (Map.lookup index roots)
+      primaryFaceRoots = Map.mapMaybe listToMaybe faceRoots
+  incidence <- requireRight (label <> ": incidence admission")
+    ( admitPlanarIncidence
+        (packed [rootAt index vertexRoots | index <- [0 .. length points - 1]])
+        (packed (concatMap (snd . snd) (Map.toAscList records)))
+        (packed [rootAt index primaryFaceRoots | index <- [0 .. maximumFace]])
+        (IntMap.fromList [(face, map (DirectedEdgeId . fromIntegral) remaining) | (face, _ : remaining) <- Map.toAscList faceRoots, not (null remaining)])
+        (IntMap.fromList [(face, IntSet.fromList vertices) | (face, vertices) <- isolatedVertices])
+    )
+  let exactPoints = Map.fromList (zip (map (VertexId . fromIntegral) ([0 ..] :: [Int])) points)
+  pure (incidence, \vertex -> maybe (Left (RegionCoordinateMissing vertex)) Right (Map.lookup vertex exactPoints))
+ where
+  maximumFace = foldr (max . fst) 0 submittedCycles
+  undirected = Map.fromList
+    (zip
+      (Set.toAscList (Set.fromList
+        [orderedPair origin target | (_, vertices) <- submittedCycles, (origin, target) <- NonEmpty.toList (cyclePairsNonEmpty vertices)]))
+      ([0 ..] :: [Int]))
+  numberCycle (face, vertices) =
+    (face,) <$> traverse
+      (\(origin, target) ->
+        (\index -> (2 * index + (if origin < target then 0 else 1), origin))
+          <$> Map.lookup (orderedPair origin target) undirected)
+      (cyclePairsNonEmpty vertices)
diff --git a/test/algebra/Moonlight/Planar/LayerOperationsSpec.hs b/test/algebra/Moonlight/Planar/LayerOperationsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/LayerOperationsSpec.hs
@@ -0,0 +1,658 @@
+-- | Exact affine-envelope and compositional layer laws through the public
+-- facade. Every comparison is against an independently composed public view.
+module Moonlight.Planar.LayerOperationsSpec (tests) where
+
+import Control.Monad (unless)
+import Data.Foldable (toList, traverse_)
+import Data.Functor.Compose (Compose (..))
+import Data.Functor.Identity (Identity (..))
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import Moonlight.Planar.Exact (ExactPoint, exactPoint, exactPointCoordinates)
+import Moonlight.Planar.Internal.ExactRational (ExactRational)
+import Moonlight.Planar.Overlay (CoverGap, LayerCoverageError (..), OverlayResult, coverGapRegion, layerCovers, overlayAll, overlayConfusion, overlayLayers, overlayMass, overlaySelectedRegion)
+import Moonlight.Planar.PowerDiagram (AffineForm (..), UpperEnvelopeError (UpperEnvelopeEmptyForms), upperEnvelope)
+import Moonlight.Planar.Region (PlanarLayer, PlanarRegion, PolygonComponent, RegionPublicationError (RegionUnboundedSelection), exactLoop, exactLoopPoints, planarLayer, planarLayerLabelAt, planarLayerRegions, planarRegion, planarRegionComponents, polygonComponent, polygonHoleLoops, polygonOuterLoop)
+import Moonlight.Planar.Valuation (exactAreaValue, regionValuations, valuationArea)
+import Moonlight.Planar.CellSet
+  ( ExactCellSet
+  , exactCellSetVertexCount
+  , exactCellSetEdgeCount
+  , exactCellSetFaceCount
+  , foldExactCellVertices
+  )
+import Moonlight.Planar.Exact (exactDivide, exactPointToEmbeddingCandidate, exactRational)
+import Moonlight.Planar.Overlay
+  ( OverlaySourceId (..)
+  , boundaryRefSource
+  , foldBoundedOverlayCells
+  , overlayArrangementEdges
+  , overlayArrangementVertices
+  , overlayCells
+  , overlayClosedIntersection
+  , overlayClosedUnion
+  , overlayEdgeSources
+  , overlayInputSources
+  , overlayReceipt
+  , overlayPlanarLayer
+  , overlayRegularizedDifference
+  )
+import Moonlight.Planar.Point (queryPointValue)
+import Moonlight.Planar.Valuation
+  ( ExactPlanarMoments
+  , cellValuations
+  , eulerCharacteristicValue
+  , exactLengthExpression
+  , exactLengthTerms
+  , exactPlanarMeasure
+  , exactPlanarFirstX
+  , exactPlanarFirstY
+  , exactPlanarSecondXX
+  , exactPlanarSecondXY
+  , exactPlanarSecondYY
+  , polygonComponentMoments
+  , orientedBoundaryMoments
+  , valuationEuler
+  , valuationIntrinsic1
+  )
+import Moonlight.Planar.OverlayFixtures (singletonLayer)
+import Support (rectangleRegion, rectangleComponent, assertEqual, integerPoint, requireRight)
+
+tests :: IO ()
+tests = do
+  testConvexUpperEnvelope
+  testNonconvexAndHoledUpperEnvelope
+  testEmptyUpperEnvelope
+  testLayerCoverage
+  testNaryOverlayLabels
+  testFusedOverlayFamilyLaws
+  testOverlayMassAndConfusion
+  testExactOverlayCollapsedEmbedding
+  testExactOverlayTinyCrossings
+  testExactOverlayDisconnectedCells
+  testExactOverlayNestedHolesAndIslands
+  testExactOverlayReversedSupportAndTJunction
+  testExactOverlayVertexContact
+  putStrLn "layer operations: ok"
+
+testConvexUpperEnvelope :: IO ()
+testConvexUpperEnvelope = do
+  window <- rectangleComponent 0 0 10 10
+  let forms =
+        Map.fromList
+          [ (0 :: Int, AffineForm 0 (-1) 0)
+          , (1, AffineForm 0 (-1) 0)
+          , (2, AffineForm (-10) 1 0)
+          , (3, AffineForm (-11) 1 0)
+          ]
+  layer <- requireRight "convex exact upper envelope" (upperEnvelope window forms)
+  reordered <-
+    requireRight
+      "upper envelope is independent of map construction order"
+      (upperEnvelope window (Map.fromList (reverse (Map.toList forms))))
+  assertEqual "upper envelope map-order invariance" layer reordered
+  assertEqual "identical and dominated forms own no cell" [Just 0, Just 2] (Map.keys (planarLayerRegions layer))
+  assertEnvelopeDominance forms layer
+  assertEqual "left affine winner" (Just 0) (planarLayerLabelAt layer (exactPoint 2 5))
+  assertEqual "right affine winner" (Just 2) (planarLayerLabelAt layer (exactPoint 8 5))
+  assertEqual "outside finite window" Nothing (planarLayerLabelAt layer (exactPoint (-1) 5))
+  requireCoverage "upper envelope covers its convex window" layer window
+
+testNonconvexAndHoledUpperEnvelope :: IO ()
+testNonconvexAndHoledUpperEnvelope = do
+  nonconvex <- lShapedComponent
+  holed <- holedComponent
+  let constantForm = Map.singleton (7 :: Int) (AffineForm 1 0 0)
+  assertEnvelopeEqualsWindow "nonconvex envelope restriction" nonconvex constantForm
+  assertEnvelopeEqualsWindow "holed envelope restriction" holed constantForm
+
+testEmptyUpperEnvelope :: IO ()
+testEmptyUpperEnvelope = do
+  window <- rectangleComponent 0 0 1 1
+  case upperEnvelope window (Map.empty :: Map.Map Int AffineForm) of
+    Left UpperEnvelopeEmptyForms -> pure ()
+    result -> fail ("empty upper envelope: unexpected result " <> show result)
+
+testLayerCoverage :: IO ()
+testLayerCoverage = do
+  window <- rectangleComponent 0 0 10 10
+  half <- rectangleRegion 0 0 5 10
+  incomplete <- singletonLayer False True half
+  case layerCovers incomplete window of
+    Left (LayerCoverageGap gap) -> assertGapArea 50 gap
+    result -> fail ("incomplete layer coverage: unexpected result " <> show result)
+  completeRegion <- requireRight "complete coverage region" (planarRegion [window])
+  complete <- requireRight "complete coverage layer" (planarLayer False (Map.singleton True completeRegion))
+  requireCoverage "complete layer" complete window
+
+testNaryOverlayLabels :: IO ()
+testNaryOverlayLabels = do
+  vertical <- rectangleRegion 0 0 6 10 >>= singletonLayer False True
+  horizontal <- rectangleRegion 0 0 10 6 >>= singletonLayer False True
+  central <- rectangleRegion 2 2 8 8 >>= singletonLayer False True
+  let sourceLayers = vertical :| [horizontal, central]
+  result <- requireRight "fused n-ary overlay" (overlayAll sourceLayers)
+  refined <- requireRight "publish fused n-ary overlay" (overlayPlanarLayer result)
+  assertNaryLabel sourceLayers refined (exactPoint 3 3)
+  assertNaryLabel sourceLayers refined (exactPoint 7 3)
+  assertNaryLabel sourceLayers refined (exactPoint 9 9)
+  assertNaryLabel sourceLayers refined (exactPoint (-1) (-1))
+  singletonResult <- requireRight "singleton n-ary overlay" (overlayAll (vertical :| []))
+  singleton <- requireRight "publish singleton n-ary overlay" (overlayPlanarLayer singletonResult)
+  assertNaryLabel (vertical :| []) singleton (exactPoint 3 3)
+
+testFusedOverlayFamilyLaws :: IO ()
+testFusedOverlayFamilyLaws = do
+  vertical <- rectangleRegion 0 0 6 10 >>= singletonLayer False True
+  horizontal <- rectangleRegion 0 0 10 6 >>= singletonLayer False True
+  central <- rectangleRegion 2 2 8 8 >>= singletonLayer False True
+  traverse_ checkPermutation (List.permutations [vertical, horizontal, central])
+  duplicate <- requireRight "fused duplicate source boundaries" (overlayAll (vertical :| [vertical, vertical]))
+  assertEqual "fused construction records every source" 3 (overlayInputSources (overlayReceipt duplicate))
+  assertEqual "duplicate boundaries retain every source ordinal" True
+    (all
+      ((== Set.fromList [OverlaySourceId 0, OverlaySourceId 1, OverlaySourceId 2])
+        . Set.fromList . fmap boundaryRefSource . NonEmpty.toList . overlayEdgeSources . snd)
+      (overlayArrangementEdges duplicate))
+  duplicateMasses <- requireRight "fused duplicate direct confusion" (overlayConfusion duplicate)
+  assertEqual "duplicate provenance does not multiply area"
+    (Map.singleton (True :| [True, True]) 60) (fmap exactAreaValue duplicateMasses)
+ where
+  checkPermutation [firstLayer, secondLayer, thirdLayer] = do
+    let sources = firstLayer :| [secondLayer, thirdLayer]
+    fused <- requireRight "fused source permutation" (overlayAll sources)
+    firstPair <- requireRight "grouped first binary construction" (overlayLayers firstLayer secondLayer)
+    pairLayer <- requireRight "publish grouped first binary result" (overlayPlanarLayer firstPair)
+    grouped <- requireRight "grouped second binary construction" (overlayLayers pairLayer thirdLayer)
+    fusedMasses <- requireRight "fused direct masses" (overlayConfusion fused)
+    groupedMasses <- requireRight "grouped direct masses" (overlayConfusion grouped)
+    assertEqual "fused and grouped binary exact masses agree"
+      (Map.mapKeys flattenLabels groupedMasses) fusedMasses
+    fusedMoments <- directOverlayMoments fused
+    groupedMoments <- directOverlayMoments grouped
+    assertEqual "fused and grouped binary direct moments agree"
+      (Map.mapKeys flattenLabels groupedMoments) fusedMoments
+    secondMoment <- requireRight "independent three-way rectangle second moment" (exactDivide 832 3)
+    assertEqual "direct fused moments equal independent Cartesian integrals"
+      (16, 64, 64, secondMoment, 256, secondMoment)
+      (momentDigest (Map.findWithDefault mempty (True :| [True, True]) fusedMoments))
+    fusedLayer <- requireRight "publish fused source permutation" (overlayPlanarLayer fused)
+    let flattenedResult = fmap flattenLabels grouped
+    assertOverlayFixedGeometry "n-ary flattening changes labels only" grouped flattenedResult
+    flattened <- requireRight "publish grouped labels flattened without reconstruction" (overlayPlanarLayer flattenedResult)
+    assertEqual "fused publication agrees with independently grouped binary publication" flattened fusedLayer
+    traverse_ (assertNaryLabel sources fusedLayer)
+      [exactPoint 3 3, exactPoint 7 3, exactPoint 3 7, exactPoint 9 9, exactPoint (-1) (-1)]
+  checkPermutation _ = fail "source-permutation fixture lost its three-source arity"
+  flattenLabels :: ((Bool, Bool), Bool) -> NonEmpty Bool
+  flattenLabels ((firstLabel, secondLabel), thirdLabel) = firstLabel :| [secondLabel, thirdLabel]
+
+directOverlayMoments :: Ord labels => OverlayResult labels -> IO (Map.Map labels ExactPlanarMoments)
+directOverlayMoments result = requireRight "direct exact boundary moments" $
+  foldBoundedOverlayCells
+    (\moments labels boundary -> Map.insertWith (<>) labels (orientedBoundaryMoments boundary) moments)
+    Map.empty result
+
+testOverlayMassAndConfusion :: IO ()
+testOverlayMassAndConfusion = do
+  left <- rectangleRegion 0 0 6 10 >>= singletonLayer False True
+  right <- rectangleRegion 4 0 10 10 >>= singletonLayer False True
+  result <- requireRight "mass overlay" (overlayLayers left right)
+  assertOverlayLabelAlgebra result
+  direct <- requireRight "direct overlap mass" (overlayMass (== (True, True)) result)
+  published <-
+    requireRight
+      "published overlap region"
+      (overlaySelectedRegion (== (True, True)) result)
+  publishedValues <- requireRight "published overlap valuation" (regionValuations published)
+  assertEqual
+    "direct mass equals published-region valuation"
+    (exactAreaValue (valuationArea publishedValues))
+    (exactAreaValue direct)
+  assertEqual "overlap mass" 20 (exactAreaValue direct)
+  case overlayMass (== (False, False)) result of
+    Left RegionUnboundedSelection -> pure ()
+    value -> fail ("unbounded overlay mass: unexpected result " <> show value)
+  confusion <- requireRight "direct finite confusion" (overlayConfusion result)
+  assertEqual
+    "one-pass finite confusion masses"
+    ( Map.fromList
+        [ ((False, True), 40)
+        , ((True, False), 40)
+        , ((True, True), 20)
+        ]
+    )
+    (fmap exactAreaValue confusion)
+  traverse_
+    (\(labels, expected) -> do
+       measured <- requireRight "direct confusion-cell mass" (overlayMass (== labels) result)
+       assertEqual "confusion entry equals direct mass" expected (exactAreaValue measured))
+    (Map.toAscList (fmap exactAreaValue confusion))
+
+-- The result's functor/traversal ranges over its one face-label vector. Exact
+-- incidence, source provenance and construction work are fixed observations,
+-- not reconstructed payloads or a second label authoring surface.
+assertOverlayLabelAlgebra :: OverlayResult (Bool, Bool) -> IO ()
+assertOverlayLabelAlgebra result = do
+  assertOverlayObservationsEqual "overlay Functor identity" result (fmap id result)
+  assertOverlayObservationsEqual "overlay Functor composition"
+    (fmap (markLabels . relabel) result) (fmap markLabels (fmap relabel result))
+  assertOverlayObservationsEqual "overlay Traversable identity" result
+    (runIdentity (traverse Identity result))
+  assertEqual "overlay Foldable follows the authoritative face-label order"
+    (fmap snd (overlayCells result)) (toList result)
+
+  let composed = getCompose (traverse (Compose . fmap branchLabels . admitLabels) result)
+      nested = fmap (traverse branchLabels) (traverse admitLabels result)
+  assertEqual "overlay Traversable composition preserves effects and label order"
+    (fmap (fmap overlayCells) composed) (fmap (fmap overlayCells) nested)
+  traverse_ (traverse_ (assertOverlayFixedGeometry "composed label traversal fixes geometry" result)) composed
+  case traverse rejectOverlap result of
+    Nothing -> pure ()
+    Just _ -> fail "overlay label traversal swallowed an explicit decoder refusal"
+
+  let relabelled = fmap relabel result
+  assertOverlayFixedGeometry "bijective relabeling preserves exact construction" result relabelled
+  sourceMoments <- directOverlayMoments result
+  relabelledMoments <- directOverlayMoments relabelled
+  assertEqual "direct exact moments commute with bijective relabeling"
+    (Map.mapKeys relabel sourceMoments) relabelledMoments
+  sourceMass <- requireRight "source selected mass" (overlayMass (== (True, True)) result)
+  relabelledMass <- requireRight "relabelled selected mass" (overlayMass (== (GT, GT)) relabelled)
+  assertEqual "exact selected mass is independent of label encoding" sourceMass relabelledMass
+  sourceRegion <- requireRight "source selected region" (overlaySelectedRegion (== (True, True)) result)
+  relabelledRegion <- requireRight "relabelled selected region" (overlaySelectedRegion (== (GT, GT)) relabelled)
+  assertEqual "exact selected publication is independent of label encoding" sourceRegion relabelledRegion
+  sourceCells <- requireRight "source closed selected cells" (overlayClosedIntersection id id result)
+  relabelledCells <- requireRight "relabelled closed selected cells" (overlayClosedIntersection (== GT) (== GT) relabelled)
+  assertEqual "closed selection incidence survives transported predicates" (cellCounts sourceCells) (cellCounts relabelledCells)
+  assertEqual "closed selection exact coordinates survive transported predicates" (cellPointSet sourceCells) (cellPointSet relabelledCells)
+  sourceValues <- requireRight "source closed valuations" (cellValuations sourceCells)
+  relabelledValues <- requireRight "relabelled closed valuations" (cellValuations relabelledCells)
+  -- Relabeling fixes these integer boundary segments, so their term
+  -- presentations agree. This is not a general Eq law for radical expressions.
+  assertEqual "closed selection exact valuations commute with relabeling"
+    (valuationEuler sourceValues, valuationArea sourceValues, exactLengthTerms (exactLengthExpression (valuationIntrinsic1 sourceValues)))
+    (valuationEuler relabelledValues, valuationArea relabelledValues, exactLengthTerms (exactLengthExpression (valuationIntrinsic1 relabelledValues)))
+ where
+  relabel :: (Bool, Bool) -> (Ordering, Ordering)
+  relabel (left, right) = (if left then GT else LT, if right then GT else LT)
+  markLabels :: (Ordering, Ordering) -> (Maybe Ordering, Maybe Ordering)
+  markLabels (left, right) = (Just left, Just right)
+  admitLabels :: (Bool, Bool) -> Maybe (Ordering, Ordering)
+  admitLabels = Just . relabel
+  branchLabels :: (Ordering, Ordering) -> [(Maybe Ordering, Maybe Ordering)]
+  branchLabels labels@(_, right) = [markLabels labels, (Nothing, Just right)]
+  rejectOverlap :: (Bool, Bool) -> Maybe (Ordering, Ordering)
+  rejectOverlap labels
+    | labels == (True, True) = Nothing
+    | otherwise = admitLabels labels
+
+assertOverlayObservationsEqual
+  :: (Eq labels, Show labels)
+  => String -> OverlayResult labels -> OverlayResult labels -> IO ()
+assertOverlayObservationsEqual label expected actual = do
+  assertEqual (label <> " face labels") (overlayCells expected) (overlayCells actual)
+  assertOverlayFixedGeometry label expected actual
+
+assertOverlayFixedGeometry
+  :: String -> OverlayResult leftLabels -> OverlayResult rightLabels -> IO ()
+assertOverlayFixedGeometry label expected actual = do
+  assertEqual (label <> " exact vertices and source provenance")
+    (overlayArrangementVertices expected) (overlayArrangementVertices actual)
+  assertEqual (label <> " atomic edges and source provenance")
+    (overlayArrangementEdges expected) (overlayArrangementEdges actual)
+  assertEqual (label <> " construction receipt") (overlayReceipt expected) (overlayReceipt actual)
+  assertEqual (label <> " face identity") (fmap fst (overlayCells expected)) (fmap fst (overlayCells actual))
+  expectedBoundaries <- requireRight (label <> " source oriented face boundaries")
+    (foldBoundedOverlayCells (\boundaries _ boundary -> boundary : boundaries) [] expected)
+  actualBoundaries <- requireRight (label <> " mapped oriented face boundaries")
+    (foldBoundedOverlayCells (\boundaries _ boundary -> boundary : boundaries) [] actual)
+  assertEqual (label <> " exact oriented face geometry") expectedBoundaries actualBoundaries
+
+-- These corners are distinct exact points but all four project to (1,1).
+-- Exact semantics must not depend on whether a faithful native DCEL exists.
+testExactOverlayCollapsedEmbedding :: IO ()
+testExactOverlayCollapsedEmbedding = do
+  epsilon <- requireRight "exact overlay epsilon" (exactRational 1 (2 ^ (80 :: Int)))
+  tiny <- exactRectangleRegion 1 1 (1 + epsilon) (1 + epsilon)
+  let corners = concatMap componentBoundaryPoints (planarRegionComponents tiny)
+  projected <- traverse (requireRight "tiny optional embedding" . exactPointToEmbeddingCandidate) corners
+  assertEqual "tiny exact square has four distinct corners" 4 (Set.size (Set.fromList corners))
+  assertEqual "tiny exact square has only one binary64 candidate" 1
+    (Set.size (Set.fromList (fmap queryPointValue projected)))
+  layer <- singletonLayer False True tiny
+  overlay <- requireRight "exact overlay survives collapsed embedding" (overlayLayers layer layer)
+  selected <- requireRight "publish tiny exact intersection" (overlaySelectedRegion (== (True, True)) overlay)
+  assertEqual "tiny exact support is not rounded away" tiny selected
+  assertRectangleMoments "tiny exact intersection moments" 1 1 (1 + epsilon) (1 + epsilon) selected
+  mass <- requireRight "tiny exact intersection mass" (overlayMass (== (True, True)) overlay)
+  confusion <- requireRight "tiny exact confusion" (overlayConfusion overlay)
+  assertEqual "tiny exact area is nonzero" (epsilon * epsilon) (exactAreaValue mass)
+  assertEqual "coincident source supports do not double-count exact area"
+    (Map.singleton (True, True) (epsilon * epsilon)) (fmap exactAreaValue confusion)
+  closed <- requireRight "tiny exact closed intersection" (overlayClosedIntersection id id overlay)
+  assertCellValuations "tiny closed intersection" 1 (epsilon * epsilon) closed
+  assertEqual "tiny closed selection retains all exact corners" (Set.fromList corners) (cellPointSet closed)
+  naryResult <- requireRight "n-ary overlay retains tiny exact cell" (overlayAll (layer :| [layer, layer]))
+  nary <- requireRight "publish n-ary tiny exact cell" (overlayPlanarLayer naryResult)
+  assertEqual "n-ary tiny exact region" (Just tiny)
+    (Map.lookup (True :| [True, True]) (planarLayerRegions nary))
+
+testExactOverlayTinyCrossings :: IO ()
+testExactOverlayTinyCrossings = do
+  epsilon <- requireRight "exact crossing epsilon" (exactRational 1 (2 ^ (80 :: Int)))
+  horizontal <- exactRectangleRegion 0 1 3 (1 + epsilon) >>= singletonLayer False True
+  vertical <- exactRectangleRegion 1 0 (1 + epsilon) 3 >>= singletonLayer False True
+  overlay <- requireRight "tiny rational proper crossings" (overlayLayers horizontal vertical)
+  intersection <- requireRight "publish tiny crossing cell" (overlaySelectedRegion (== (True, True)) overlay)
+  expected <- exactRectangleRegion 1 1 (1 + epsilon) (1 + epsilon)
+  assertEqual "crossing points retain their exact offsets" expected intersection
+  assertRectangleMoments "tiny crossing moments" 1 1 (1 + epsilon) (1 + epsilon) intersection
+  let overlapArea = epsilon * epsilon
+      exclusiveArea = 3 * epsilon - overlapArea
+  confusion <- requireRight "tiny crossing direct confusion" (overlayConfusion overlay)
+  assertEqual "tiny crossing partitions both exact strips"
+    (Map.fromList [((True, False), exclusiveArea), ((False, True), exclusiveArea), ((True, True), overlapArea)])
+    (fmap exactAreaValue confusion)
+  halfEpsilon <- requireRight "half crossing epsilon" (exactDivide epsilon 2)
+  labels <- requireRight "publish tiny crossing labels" (overlayPlanarLayer overlay)
+  assertEqual "tiny exact crossing interior label" (True, True)
+    (planarLayerLabelAt labels (exactPoint (1 + halfEpsilon) (1 + halfEpsilon)))
+  assertEqual "tiny exact strip-only label" (True, False)
+    (planarLayerLabelAt labels (exactPoint 2 (1 + halfEpsilon)))
+
+  -- A non-axis-aligned event constructs (1-epsilon/2,1+epsilon/2),
+  -- rather than reusing a submitted corner or a rounded line intersection.
+  descending <- exactPolygonRegion
+    (exactPoint 0 0 :| [exactPoint 2 0, exactPoint 0 2]) >>= singletonLayer False True
+  ascending <- exactPolygonRegion
+    (exactPoint 0 epsilon :| [exactPoint 2 epsilon, exactPoint 2 (2 + epsilon)]) >>= singletonLayer False True
+  slanted <- requireRight "tiny-offset slanted crossing" (overlayLayers descending ascending)
+  slantedIntersection <- requireRight "publish tiny-offset slanted crossing"
+    (overlaySelectedRegion (== (True, True)) slanted)
+  slantedExpected <- exactPolygonRegion
+    (exactPoint 0 epsilon :| [exactPoint (2 - epsilon) epsilon, exactPoint (1 - halfEpsilon) (1 + halfEpsilon)])
+  assertEqual "slanted event has its algebraic exact coordinates" slantedExpected slantedIntersection
+  slantedArea <- requireRight "slanted exact triangle area" (exactDivide ((2 - epsilon) * (2 - epsilon)) 4)
+  observedArea <- requireRight "slanted direct area" (overlayMass (== (True, True)) slanted)
+  assertEqual "slanted exact triangle area formula" slantedArea (exactAreaValue observedArea)
+
+testExactOverlayDisconnectedCells :: IO ()
+testExactOverlayDisconnectedCells = do
+  firstIsland <- rectangleComponent 0 0 2 2
+  secondIsland <- rectangleComponent 5 0 7 2
+  islands <- requireRight "disconnected source region" (planarRegion [secondIsland, firstIsland])
+  left <- singletonLayer False True islands
+  right <- rectangleRegion 1 (-1) 6 1 >>= singletonLayer False True
+  overlay <- requireRight "disconnected exact refinement" (overlayLayers left right)
+  selected <- requireRight "publish disconnected intersections" (overlaySelectedRegion (== (True, True)) overlay)
+  firstExpected <- rectangleComponent 1 0 2 1
+  secondExpected <- rectangleComponent 5 0 6 1
+  expected <- requireRight "disconnected exact expected region" (planarRegion [firstExpected, secondExpected])
+  assertEqual "disconnected cells do not share an artificial bridge" expected selected
+  assertEqual "disconnected intersection component count" 2 (length (planarRegionComponents selected))
+  assertEqual "disconnected exact moments"
+    (momentDigest (polygonComponentMoments firstExpected <> polygonComponentMoments secondExpected))
+    (momentDigest (foldMap polygonComponentMoments (planarRegionComponents selected)))
+  closed <- requireRight "disconnected closed intersection" (overlayClosedIntersection id id overlay)
+  assertCellValuations "disconnected closed intersection" 2 2 closed
+  labels <- requireRight "publish disconnected labels" (overlayPlanarLayer overlay)
+  assertEqual "gap retains the right-only label" (False, True)
+    (planarLayerLabelAt labels (exactPoint 3 0))
+
+testExactOverlayNestedHolesAndIslands :: IO ()
+testExactOverlayNestedHolesAndIslands = do
+  outer <- requireRight "nested outer loop" (exactLoop (rectanglePoints 0 0 10 10))
+  hole <- requireRight "nested clockwise hole" (exactLoop (NonEmpty.reverse (rectanglePoints 2 2 8 8)))
+  annulus <- requireRight "nested annulus component" (polygonComponent outer [hole])
+  island <- rectangleComponent 4 4 6 6
+  annularRegion <- requireRight "nested annulus region" (planarRegion [annulus])
+  islandRegion <- requireRight "nested island region" (planarRegion [island])
+  left <- singletonLayer False True annularRegion
+  right <- singletonLayer False True islandRegion
+  overlay <- requireRight "nested disconnected exact refinement" (overlayLayers left right)
+  published <- requireRight "publish annulus with nested island" (overlaySelectedRegion (uncurry (||)) overlay)
+  expected <- requireRight "annulus and nested island expected region" (planarRegion [annulus, island])
+  assertEqual "nested CCBs belong to their actual planar faces" expected published
+  assertEqual "nested hole is not mistaken for an independent disk"
+    (2, 1) (length (planarRegionComponents published), sum (fmap (length . polygonHoleLoops) (planarRegionComponents published)))
+  confusion <- requireRight "nested direct confusion" (overlayConfusion overlay)
+  assertEqual "nested finite label masses"
+    (Map.fromList [((True, False), 64), ((False, True), 4)]) (fmap exactAreaValue confusion)
+  finiteOutsideArea <- requireRight "direct bounded outside-label cavity area" $
+    foldBoundedOverlayCells
+      (\area labels boundary -> if labels == (False, False) then area + exactPlanarMeasure (orientedBoundaryMoments boundary) else area)
+      0 overlay
+  assertEqual "bounded outside-label cavity excludes the nested island" 32 finiteOutsideArea
+  union <- requireRight "nested closed union" (overlayClosedUnion id id overlay)
+  assertCellValuations "nested closed union has annulus plus disk topology" 1 68 union
+  emptyIntersection <- requireRight "nested closed intersection is empty" (overlayClosedIntersection id id overlay)
+  assertCellValuations "nested disjoint closed intersection" 0 0 emptyIntersection
+  assertEqual "nested disjoint closed intersection has no cells" (0, 0, 0) (cellCounts emptyIntersection)
+
+  -- The hole boundary is clockwise in one source and counter-clockwise in
+  -- the filler. Its closed intersection is a cycle, not a disk or four gaps.
+  filler <- rectangleRegion 2 2 8 8 >>= singletonLayer False True
+  filled <- requireRight "oppositely wound shared hole support" (overlayLayers left filler)
+  sharedCycle <- requireRight "closed shared hole boundary" (overlayClosedIntersection id id filled)
+  assertCellValuations "shared hole boundary is one closed cycle" 0 0 sharedCycle
+  assertEqual "shared hole boundary retains exactly its four sides" (4, 4, 0) (cellCounts sharedCycle)
+  assertEqual "shared hole support retains its exact vertices"
+    (Set.fromList (NonEmpty.toList (rectanglePoints 2 2 8 8))) (cellPointSet sharedCycle)
+  filledRegion <- requireRight "publish filled hole" (overlaySelectedRegion (uncurry (||)) filled)
+  fullWindow <- rectangleRegion 0 0 10 10
+  assertEqual "shared boundary dissolves when both incident faces are selected" fullWindow filledRegion
+  difference <- requireRight "regularized annulus difference" (overlayRegularizedDifference id id filled)
+  assertCellValuations "regularized difference retains annular topology" 0 64 difference
+
+testExactOverlayReversedSupportAndTJunction :: IO ()
+testExactOverlayReversedSupportAndTJunction = do
+  leftRegion <- rectangleRegion 0 0 2 2
+  rightRegion <- rectangleRegion 2 0 4 1
+  left <- singletonLayer False True leftRegion
+  right <- singletonLayer False True rightRegion
+  overlay <- requireRight "reversed shared support with T-junction" (overlayLayers left right)
+  reversed <- requireRight "operand-reversed T-junction" (overlayLayers right left)
+  traverse_
+    (\result -> do
+      shared <- requireRight "T-junction closed shared segment" (overlayClosedIntersection id id result)
+      assertEqual "T-junction splits an edge at the exact contact endpoint" (2, 1, 0) (cellCounts shared)
+      assertEqual "opposite source directions glue onto the same support"
+        (Set.fromList [integerPoint 2 0, integerPoint 2 1]) (cellPointSet shared)
+      assertCellValuations "T-junction shared edge" 1 0 shared)
+    [overlay, reversed]
+  regularized <- requireRight "T-junction regularized difference" (overlayRegularizedDifference id id overlay)
+  assertCellValuations "subtracting an adjacent area does not puncture the closed left region" 1 4 regularized
+  published <- requireRight "publish regularized T-junction difference"
+    (overlaySelectedRegion (\(insideLeft, insideRight) -> insideLeft && not insideRight) overlay)
+  assertEqual "regularized difference restores the shared boundary" leftRegion published
+  assertEqual "regularized closure includes both contact endpoints" True
+    (Set.fromList [integerPoint 2 0, integerPoint 2 1] `Set.isSubsetOf` cellPointSet regularized)
+
+testExactOverlayVertexContact :: IO ()
+testExactOverlayVertexContact = do
+  leftRegion <- rectangleRegion 0 0 1 1
+  rightRegion <- rectangleRegion 1 1 2 2
+  left <- singletonLayer False True leftRegion
+  right <- singletonLayer False True rightRegion
+  overlay <- requireRight "exact vertex-only contact" (overlayLayers left right)
+  contact <- requireRight "closed vertex-only intersection" (overlayClosedIntersection id id overlay)
+  assertEqual "closed intersection retains only the shared vertex" (1, 0, 0) (cellCounts contact)
+  assertEqual "shared vertex has exact source coordinates" (Set.singleton (integerPoint 1 1)) (cellPointSet contact)
+  assertCellValuations "a point has Euler one and area zero" 1 0 contact
+  areaIntersection <- requireRight "regularized vertex-only intersection" (overlaySelectedRegion (== (True, True)) overlay)
+  assertEqual "publishing only full-dimensional intersection does not fabricate a point polygon" [] (planarRegionComponents areaIntersection)
+  difference <- requireRight "regularized vertex-contact difference" (overlayRegularizedDifference id id overlay)
+  assertCellValuations "regularized point-contact difference remains a disk" 1 1 difference
+  assertEqual "regularized closure keeps the contact vertex" True
+    (Set.member (integerPoint 1 1) (cellPointSet difference))
+  union <- requireRight "closed vertex-contact union" (overlayClosedUnion id id overlay)
+  assertCellValuations "two closed disks glued at a point are connected" 1 2 union
+
+rectanglePoints :: ExactRational -> ExactRational -> ExactRational -> ExactRational -> NonEmpty ExactPoint
+rectanglePoints minimumX minimumY maximumX maximumY =
+  exactPoint minimumX minimumY :|
+    [exactPoint maximumX minimumY, exactPoint maximumX maximumY, exactPoint minimumX maximumY]
+
+exactPolygonRegion :: NonEmpty ExactPoint -> IO PlanarRegion
+exactPolygonRegion points = do
+  loop <- requireRight "exact test polygon loop" (exactLoop points)
+  component <- requireRight "exact test polygon component" (polygonComponent loop [])
+  requireRight "exact test polygon region" (planarRegion [component])
+
+exactRectangleRegion :: ExactRational -> ExactRational -> ExactRational -> ExactRational -> IO PlanarRegion
+exactRectangleRegion minimumX minimumY maximumX maximumY =
+  exactPolygonRegion (rectanglePoints minimumX minimumY maximumX maximumY)
+
+-- Independent Cartesian integrals, not the production boundary-moment sum.
+assertRectangleMoments
+  :: String -> ExactRational -> ExactRational -> ExactRational -> ExactRational -> PlanarRegion -> IO ()
+assertRectangleMoments label minimumX minimumY maximumX maximumY region = do
+  firstX <- requireRight label (exactDivide (maximumX * maximumX - minimumX * minimumX) 2)
+  firstY <- requireRight label (exactDivide (maximumY * maximumY - minimumY * minimumY) 2)
+  secondX <- requireRight label (exactDivide (maximumX ^ (3 :: Int) - minimumX ^ (3 :: Int)) 3)
+  secondY <- requireRight label (exactDivide (maximumY ^ (3 :: Int) - minimumY ^ (3 :: Int)) 3)
+  let width = maximumX - minimumX
+      height = maximumY - minimumY
+  assertEqual label
+    (width * height, firstX * height, width * firstY, secondX * height, firstX * firstY, width * secondY)
+    (momentDigest (foldMap polygonComponentMoments (planarRegionComponents region)))
+
+momentDigest :: ExactPlanarMoments -> (ExactRational, ExactRational, ExactRational, ExactRational, ExactRational, ExactRational)
+momentDigest moments =
+  ( exactPlanarMeasure moments, exactPlanarFirstX moments, exactPlanarFirstY moments
+  , exactPlanarSecondXX moments, exactPlanarSecondXY moments, exactPlanarSecondYY moments
+  )
+
+cellCounts :: ExactCellSet -> (Int, Int, Int)
+cellCounts cells = (exactCellSetVertexCount cells, exactCellSetEdgeCount cells, exactCellSetFaceCount cells)
+
+cellPointSet :: ExactCellSet -> Set.Set ExactPoint
+cellPointSet = foldExactCellVertices (\points _ point -> Set.insert point points) Set.empty
+
+assertCellValuations :: String -> Int -> ExactRational -> ExactCellSet -> IO ()
+assertCellValuations label expectedEuler expectedArea cells = do
+  values <- requireRight label (cellValuations cells)
+  assertEqual (label <> " Euler and area") (expectedEuler, expectedArea)
+    (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))
+
+assertEnvelopeDominance
+  :: Map.Map Int AffineForm
+  -> PlanarLayer (Maybe Int)
+  -> IO ()
+assertEnvelopeDominance forms layer =
+  traverse_ checkRegion (Map.toAscList (planarLayerRegions layer))
+ where
+  checkRegion :: (Maybe Int, PlanarRegion) -> IO ()
+  checkRegion (Nothing, _) = fail "bounded upper-envelope region has the outside label"
+  checkRegion (Just winner, region) =
+    case Map.lookup winner forms of
+      Nothing -> fail ("upper-envelope winner is absent from its input map: " <> show winner)
+      Just winnerForm ->
+        traverse_
+          (checkWinnerAtPoint winner winnerForm)
+          (concatMap componentBoundaryPoints (planarRegionComponents region))
+
+  checkWinnerAtPoint
+    :: Int
+    -> AffineForm
+    -> ExactPoint
+    -> IO ()
+  checkWinnerAtPoint winner winnerForm point =
+    let winnerValue = affineValue winnerForm point
+     in traverse_
+          (\(candidate, candidateForm) ->
+             unless (winnerValue >= affineValue candidateForm point) $
+               fail
+                 ( "upper-envelope dominance failed: winner "
+                     <> show winner
+                     <> ", candidate "
+                     <> show candidate
+                 ))
+          (Map.toAscList forms)
+
+componentBoundaryPoints :: PolygonComponent -> [ExactPoint]
+componentBoundaryPoints component =
+  NonEmpty.toList (exactLoopPoints (polygonOuterLoop component))
+    <> concatMap (NonEmpty.toList . exactLoopPoints) (polygonHoleLoops component)
+
+affineValue
+  :: AffineForm
+  -> ExactPoint
+  -> ExactRational
+affineValue form point =
+  let (coordinateX, coordinateY) = exactPointCoordinates point
+   in affineFormConstant form
+        + affineFormXCoefficient form * coordinateX
+        + affineFormYCoefficient form * coordinateY
+
+assertEnvelopeEqualsWindow
+  :: String
+  -> PolygonComponent
+  -> Map.Map Int AffineForm
+  -> IO ()
+assertEnvelopeEqualsWindow label window forms = do
+  expected <- requireRight (label <> " expected region") (planarRegion [window])
+  layer <- requireRight label (upperEnvelope window forms)
+  assertEqual label (Just expected) (Map.lookup (Just 7) (planarLayerRegions layer))
+  requireCoverage (label <> " coverage") layer window
+
+assertGapArea :: Integer -> CoverGap -> IO ()
+assertGapArea expected gap = do
+  values <- requireRight "gap valuation" (regionValuations (coverGapRegion gap))
+  assertEqual "coverage obstruction retains exact gap" (fromInteger expected) (exactAreaValue (valuationArea values))
+
+assertNaryLabel
+  :: NonEmpty (PlanarLayer Bool)
+  -> PlanarLayer (NonEmpty Bool)
+  -> ExactPoint
+  -> IO ()
+assertNaryLabel sourceLayers refined point =
+  assertEqual
+    "n-ary label tuple follows source order"
+    (fmap (`planarLayerLabelAt` point) sourceLayers)
+    (planarLayerLabelAt refined point)
+
+requireCoverage
+  :: Ord label
+  => String
+  -> PlanarLayer label
+  -> PolygonComponent
+  -> IO ()
+requireCoverage label layer window =
+  case layerCovers layer window of
+    Right () -> pure ()
+    Left failure -> fail (label <> ": " <> show failure)
+
+lShapedComponent :: IO PolygonComponent
+lShapedComponent = do
+  outer <-
+    requireRight
+      "L-shaped outer loop"
+      ( exactLoop
+          ( integerPoint 0 0
+              :| [ integerPoint 4 0
+                 , integerPoint 4 1
+                 , integerPoint 1 1
+                 , integerPoint 1 4
+                 , integerPoint 0 4
+                 ]
+          )
+      )
+  requireRight "L-shaped component" (polygonComponent outer [])
+
+holedComponent :: IO PolygonComponent
+holedComponent = do
+  outer <-
+    requireRight
+      "holed outer loop"
+      (exactLoop (integerPoint 0 0 :| [integerPoint 6 0, integerPoint 6 6, integerPoint 0 6]))
+  hole <-
+    requireRight
+      "clockwise hole loop"
+      (exactLoop (integerPoint 2 2 :| [integerPoint 2 4, integerPoint 4 4, integerPoint 4 2]))
+  requireRight "holed component" (polygonComponent outer [hole])
diff --git a/test/algebra/Moonlight/Planar/MinkowskiSpec.hs b/test/algebra/Moonlight/Planar/MinkowskiSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/MinkowskiSpec.hs
@@ -0,0 +1,434 @@
+-- | Exact convex convolution and residual-morphology laws.
+module Moonlight.Planar.MinkowskiSpec (tests) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , exactPoint
+  , exactPointToEmbeddingCandidate
+  , exactRational
+  )
+import Moonlight.Planar.Convex (convexPolygon)
+import Moonlight.Planar.Minkowski
+  ( MinkowskiOperation (..)
+  , closeWith
+  , convexMinkowskiSum
+  , erodeBy
+  , minkowskiGeneratedPieces
+  , minkowskiExactCrossings
+  , minkowskiOperation
+  , minkowskiOverlayPasses
+  , minkowskiSum
+  , openWith
+  , polygonOffset
+  , structuringElement
+  )
+import Moonlight.Planar.Region
+  ( PlanarRegion
+  , PolygonComponent
+  , emptyPlanarRegion
+  , exactLoop
+  , exactLoopPoints
+  , planarRegion
+  , planarRegionComponents
+  , polygonComponent
+  , polygonHoleLoops
+  , polygonOuterLoop
+  )
+import Moonlight.Planar.Point (queryPointValue)
+import Moonlight.Planar.Valuation
+  ( exactAreaValue
+  , regionValuations
+  , valuationArea
+  )
+import Support (annulusRegion, polygonRegion, rectangleRegion, rectangleComponent, assertEqual, requireRight)
+
+tests :: IO ()
+tests = do
+  testConvexConvolution
+  testGeneralAddition
+  testOffsetClosure
+  testConvexMorphology
+  testGeneralErosion
+  testHoledAndNeckedErosion
+  testTinyExactNonconvexMorphology
+  testTinyExactAnnulusMorphology
+  testTinyExactDisconnectedMorphology
+  testTinyRationalSlantedMorphology
+  putStrLn "minkowski: ok"
+
+testConvexConvolution :: IO ()
+testConvexConvolution = do
+  leftComponent <- rectangleComponent 0 0 1 1
+  rightComponent <- rectangleComponent 0 0 1 1
+  left <-
+    requireRight
+      "left convex polygon"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop leftComponent)))
+  right <-
+    requireRight
+      "right convex polygon"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop rightComponent)))
+  let result = convexMinkowskiSum left right
+  assertRegionArea "convex square sum" 4 result
+  assertEqual
+    "convex convolution is commutative after canonical publication"
+    result
+    (convexMinkowskiSum right left)
+
+testGeneralAddition :: IO ()
+testGeneralAddition = do
+  first <- rectangleComponent 0 0 1 1
+  second <- rectangleComponent 3 0 4 1
+  disconnected <- requireRight "disconnected source" (planarRegion [first, second])
+  kernel <- rectangleRegion 0 0 1 1
+  (sumRegion, receipt) <-
+    requireRight "general disconnected sum" (minkowskiSum disconnected kernel)
+  assertEqual "general sum operation receipt" MinkowskiAddition (minkowskiOperation receipt)
+  assertEqual "general sum generated one piece per source component" 2 (minkowskiGeneratedPieces receipt)
+  assertEqual "general sum remains disconnected" 2 (length (planarRegionComponents sumRegion))
+  assertRegionArea "general disconnected sum" 8 sumRegion
+  (swapped, _) <- requireRight "swapped disconnected sum" (minkowskiSum kernel disconnected)
+  assertEqual "general Minkowski sum is commutative" sumRegion swapped
+  (annihilated, _) <-
+    requireRight "empty Minkowski annihilator" (minkowskiSum disconnected emptyPlanarRegion)
+  assertEqual "empty region annihilates Minkowski addition" emptyPlanarRegion annihilated
+  firstRegion <- requireRight "first distributive operand" (planarRegion [first])
+  secondRegion <- requireRight "second distributive operand" (planarRegion [second])
+  (firstSum, _) <- requireRight "first distributed sum" (minkowskiSum firstRegion kernel)
+  (secondSum, _) <- requireRight "second distributed sum" (minkowskiSum secondRegion kernel)
+  distributed <-
+    requireRight
+      "distributed union"
+      (planarRegion (planarRegionComponents firstSum <> planarRegionComponents secondSum))
+  assertEqual "Minkowski addition distributes over disjoint union" distributed sumRegion
+
+  associativityThird <- rectangleRegion (-2) 0 0 1
+  (leftPair, _) <- requireRight "associative left pair" (minkowskiSum firstRegion kernel)
+  (leftAssociated, _) <-
+    requireRight "left-associated Minkowski sum" (minkowskiSum leftPair associativityThird)
+  (rightPair, _) <- requireRight "associative right pair" (minkowskiSum kernel associativityThird)
+  (rightAssociated, _) <-
+    requireRight "right-associated Minkowski sum" (minkowskiSum firstRegion rightPair)
+  assertEqual "Minkowski addition is associative" leftAssociated rightAssociated
+  concave <-
+    polygonRegion
+      [ (0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3) ]
+  (concaveSum, concaveReceipt) <-
+    requireRight "concave exact sum" (minkowskiSum concave kernel)
+  expectedConcaveSum <-
+    polygonRegion
+      [ (0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4) ]
+  assertEqual "decomposition-independent nonconvex sum" expectedConcaveSum concaveSum
+  if minkowskiOverlayPasses concaveReceipt > 0
+    then pure ()
+    else fail "nonconvex addition bypassed exact piece union"
+
+-- Dilation must be closed over its own exact published image. This fixture
+-- used to admit the first offset and then refuse the second when rounded
+-- resident wedges were mistaken for exact two-cells.
+testOffsetClosure :: IO ()
+testOffsetClosure = do
+  source <-
+    polygonRegion
+      [ (-9, -1)
+      , (-5, -6)
+      , (0, -13)
+      , (7, -9)
+      , (9, 1)
+      , (10, 10)
+      , (5, 12)
+      , (-7, 8)
+      ]
+  shoulder <-
+    requireRight
+      "unit octagon shoulder"
+      (exactRational 1592262918131443 2251799813685248)
+  kernel <-
+    requireRight
+      "unit octagon kernel"
+      (convexPolygon (centeredOctagon 1 shoulder))
+  element <- requireRight "unit octagon element" (structuringElement kernel)
+  (firstOffset, _) <-
+    requireRight "first closure-regression offset" (polygonOffset element source)
+  (secondOffset, secondReceipt) <-
+    requireRight
+      "offset remains closed over its own published image"
+      (polygonOffset element firstOffset)
+  assertEqual
+    "offset-of-offset remains one full-dimensional component"
+    1
+    (length (planarRegionComponents secondOffset))
+  if minkowskiOverlayPasses secondReceipt > 0
+    then pure ()
+    else fail "offset closure regression did not exercise overlay descent"
+
+centeredOctagon :: ExactRational -> ExactRational -> NonEmpty ExactPoint
+centeredOctagon radius shoulder =
+  exactPoint radius 0
+    :| [ exactPoint shoulder shoulder
+       , exactPoint 0 radius
+       , exactPoint (-shoulder) shoulder
+       , exactPoint (-radius) 0
+       , exactPoint (-shoulder) (-shoulder)
+       , exactPoint 0 (-radius)
+       , exactPoint shoulder (-shoulder)
+       ]
+
+testConvexMorphology :: IO ()
+testConvexMorphology = do
+  source <- rectangleRegion (-2) (-2) 2 2
+  kernelComponent <- rectangleComponent (-1) (-1) 1 1
+  kernelPolygon <-
+    requireRight
+      "centred square kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
+  element <- requireRight "centred structuring element" (structuringElement kernelPolygon)
+  (eroded, erosionReceipt) <- requireRight "convex erosion" (erodeBy element source)
+  expectedErosion <- rectangleRegion (-1) (-1) 1 1
+  assertEqual "convex support-half-plane erosion" expectedErosion eroded
+  assertEqual "convex erosion needs no overlay" 0 (minkowskiOverlayPasses erosionReceipt)
+  assertEqual
+    "convex erosion reports exact HPI line intersections"
+    4
+    (minkowskiExactCrossings erosionReceipt)
+  exactFitSource <- rectangleRegion (-1) (-1) 1 1
+  (lowerDimensionalResidual, _) <-
+    requireRight "exact-fit regularized erosion" (erodeBy element exactFitSource)
+  assertEqual
+    "point-only erosion residual regularizes to the empty polygonal region"
+    emptyPlanarRegion
+    lowerDimensionalResidual
+  (emptyResidual, _) <- requireRight "empty source erosion" (erodeBy element emptyPlanarRegion)
+  assertEqual "empty source erodes to empty" emptyPlanarRegion emptyResidual
+  (expanded, _) <- requireRight "convex offset" (polygonOffset element source)
+  expectedExpansion <- rectangleRegion (-3) (-3) 3 3
+  assertEqual "convex offset" expectedExpansion expanded
+  (opened, _) <- requireRight "convex opening" (openWith element source)
+  assertEqual "convex opening is idempotent on the square" source opened
+  (openedTwice, _) <- requireRight "second convex opening" (openWith element opened)
+  assertEqual "opening is idempotent" opened openedTwice
+  (closed, _) <- requireRight "convex closing" (closeWith element source)
+  assertEqual "convex closing is idempotent on the square" source closed
+  (closedTwice, _) <- requireRight "second convex closing" (closeWith element closed)
+  assertEqual "closing is idempotent" closed closedTwice
+  cornerKernelComponent <- rectangleComponent 0 0 1 1
+  cornerKernel <-
+    requireRight
+      "corner-anchored kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop cornerKernelComponent)))
+  cornerElement <- requireRight "corner-anchored element" (structuringElement cornerKernel)
+  cornerSource <- rectangleRegion 0 0 4 4
+  (cornerErosion, _) <- requireRight "corner-anchored erosion" (erodeBy cornerElement cornerSource)
+  expectedCornerErosion <- rectangleRegion 0 0 3 3
+  assertEqual "kernel origin controls erosion translation" expectedCornerErosion cornerErosion
+
+testGeneralErosion :: IO ()
+testGeneralErosion = do
+  left <- rectangleComponent 0 0 4 4
+  right <- rectangleComponent 6 0 10 4
+  source <- requireRight "two-component erosion source" (planarRegion [left, right])
+  kernelComponent <- rectangleComponent (-1) (-1) 1 1
+  kernelPolygon <-
+    requireRight
+      "general erosion kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
+  element <- requireRight "general erosion element" (structuringElement kernelPolygon)
+  (eroded, receipt) <- requireRight "general erosion" (erodeBy element source)
+  expectedLeft <- rectangleComponent 1 1 3 3
+  expectedRight <- rectangleComponent 7 1 9 3
+  expected <- requireRight "expected general erosion" (planarRegion [expectedLeft, expectedRight])
+  assertEqual "general residual erosion" expected eroded
+  if minkowskiOverlayPasses receipt > 0
+    then pure ()
+    else fail "general erosion bypassed the overlay candidate arrangement"
+
+testHoledAndNeckedErosion :: IO ()
+testHoledAndNeckedErosion = do
+  kernelComponent <- rectangleComponent (-1) (-1) 1 1
+  kernelPolygon <-
+    requireRight
+      "holed erosion kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
+  element <- requireRight "holed erosion element" (structuringElement kernelPolygon)
+  sourceAnnulus <- annulusRegion (0, 0, 10, 10) (4, 4, 6, 6)
+  expectedAnnulus <- annulusRegion (1, 1, 9, 9) (3, 3, 7, 7)
+  (erodedAnnulus, _) <- requireRight "annulus erosion" (erodeBy element sourceAnnulus)
+  assertEqual "erosion shrinks the outer boundary and expands holes" expectedAnnulus erodedAnnulus
+
+  necked <-
+    polygonRegion
+      [ (0, 0)
+      , (8, 0)
+      , (8, 3)
+      , (12, 3)
+      , (12, 0)
+      , (20, 0)
+      , (20, 8)
+      , (12, 8)
+      , (12, 5)
+      , (8, 5)
+      , (8, 8)
+      , (0, 8)
+      ]
+  wideKernelComponent <- rectangleComponent (-2) (-2) 2 2
+  wideKernelPolygon <-
+    requireRight
+      "neck erosion kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop wideKernelComponent)))
+  wideElement <- requireRight "neck erosion element" (structuringElement wideKernelPolygon)
+  (separated, _) <- requireRight "narrow-neck erosion" (erodeBy wideElement necked)
+  expectedLeft <- rectangleComponent 2 2 6 6
+  expectedRight <- rectangleComponent 14 2 18 6
+  expectedSeparated <-
+    requireRight "expected separated erosion" (planarRegion [expectedLeft, expectedRight])
+  assertEqual "erosion removes a neck narrower than the kernel" expectedSeparated separated
+
+-- Scaling and an invertible rational shear commute with both Minkowski
+-- addition and erosion. The expected L boundaries are authored analytically;
+-- no production decomposition, clipping, or overlay builds this oracle.
+testTinyExactNonconvexMorphology :: IO ()
+testTinyExactNonconvexMorphology = do
+  epsilon <- requireRight "tiny exact morphology scale" (exactRational 1 (2 ^ (80 :: Int)))
+  checkNonconvexAffineMorphology "tiny axis-aligned nonconvex morphology"
+    (scaledPoint 1 1 epsilon)
+    (scaledPoint 0 0 epsilon)
+    (epsilon * epsilon)
+
+testTinyRationalSlantedMorphology :: IO ()
+testTinyRationalSlantedMorphology = do
+  epsilon <- requireRight "tiny slanted morphology scale" (exactRational 1 (2 ^ (80 :: Int)))
+  oneThird <- requireRight "rational shear" (exactRational 1 3)
+  oneFifth <- requireRight "rational height scale" (exactRational 1 5)
+  let skewPoint :: ExactRational -> ExactRational -> (Integer, Integer) -> ExactPoint
+      skewPoint offsetX offsetY (x, y) =
+        exactPoint
+          (offsetX + epsilon * (fromInteger x + oneThird * fromInteger y))
+          (offsetY + epsilon * oneFifth * fromInteger y)
+  checkNonconvexAffineMorphology "tiny rational-slanted nonconvex morphology"
+    (skewPoint 1 1) (skewPoint 0 0) (epsilon * epsilon * oneFifth)
+
+checkNonconvexAffineMorphology
+  :: String
+  -> ((Integer, Integer) -> ExactPoint)
+  -> ((Integer, Integer) -> ExactPoint)
+  -> ExactRational
+  -> IO ()
+checkNonconvexAffineMorphology label sourcePoint kernelPoint determinant = do
+  source <- regionFromMappedPoints sourcePoint
+    ((0, 0) :| [(6, 0), (6, 2), (2, 2), (2, 6), (0, 6)])
+  kernel <- regionFromMappedPoints kernelPoint (rectangleCoordinates 0 0 1 1)
+  assertNoFaithfulBinary64Boundary label source
+  expectedSum <- regionFromMappedPoints sourcePoint
+    ((0, 0) :| [(7, 0), (7, 3), (3, 3), (3, 7), (0, 7)])
+  (summed, _) <- requireRight (label <> " sum") (minkowskiSum source kernel)
+  (commuted, _) <- requireRight (label <> " commuted sum") (minkowskiSum kernel source)
+  assertEqual (label <> " exact analytic sum boundary") expectedSum summed
+  assertEqual (label <> " exact sum commutativity") summed commuted
+  assertRegionArea (label <> " exact sum area") (33 * determinant) summed
+  polygon <- requireRight (label <> " convex kernel")
+    (convexPolygon (fmap kernelPoint (rectangleCoordinates 0 0 1 1)))
+  element <- requireRight (label <> " corner-anchored kernel") (structuringElement polygon)
+  expectedErosion <- regionFromMappedPoints sourcePoint
+    ((0, 0) :| [(5, 0), (5, 1), (1, 1), (1, 5), (0, 5)])
+  (eroded, _) <- requireRight (label <> " erosion") (erodeBy element source)
+  assertEqual (label <> " exact analytic erosion boundary") expectedErosion eroded
+  assertRegionArea (label <> " exact erosion area") (9 * determinant) eroded
+
+testTinyExactAnnulusMorphology :: IO ()
+testTinyExactAnnulusMorphology = do
+  epsilon <- requireRight "tiny annulus scale" (exactRational 1 (2 ^ (80 :: Int)))
+  let sourcePoint = scaledPoint 1 1 epsilon
+      kernelPoint = scaledPoint 0 0 epsilon
+  annulus <- mappedComponent sourcePoint (rectangleCoordinates 0 0 10 10)
+    [NonEmpty.reverse (rectangleCoordinates 3 3 7 7)]
+  source <- requireRight "tiny annulus region" (planarRegion [annulus])
+  assertNoFaithfulBinary64Boundary "tiny annulus" source
+  kernelPolygon <- requireRight "tiny annulus centred kernel"
+    (convexPolygon (fmap kernelPoint (rectangleCoordinates (-1) (-1) 1 1)))
+  element <- requireRight "tiny annulus structuring element" (structuringElement kernelPolygon)
+  expectedDilatedComponent <- mappedComponent sourcePoint (rectangleCoordinates (-1) (-1) 11 11)
+    [NonEmpty.reverse (rectangleCoordinates 4 4 6 6)]
+  expectedDilated <- requireRight "tiny annulus expected dilation" (planarRegion [expectedDilatedComponent])
+  (dilated, _) <- requireRight "tiny annulus exact dilation" (polygonOffset element source)
+  assertEqual "tiny annulus dilation shrinks the open hole exactly" expectedDilated dilated
+  assertRegionArea "tiny annulus dilated exact area" (140 * epsilon * epsilon) dilated
+  expectedErodedComponent <- mappedComponent sourcePoint (rectangleCoordinates 1 1 9 9)
+    [NonEmpty.reverse (rectangleCoordinates 2 2 8 8)]
+  expectedEroded <- requireRight "tiny annulus expected erosion" (planarRegion [expectedErodedComponent])
+  (eroded, _) <- requireRight "tiny annulus exact erosion" (erodeBy element source)
+  assertEqual "tiny annulus erosion expands the hole and shrinks the outer loop" expectedEroded eroded
+  assertRegionArea "tiny annulus eroded exact area" (28 * epsilon * epsilon) eroded
+
+testTinyExactDisconnectedMorphology :: IO ()
+testTinyExactDisconnectedMorphology = do
+  epsilon <- requireRight "tiny disconnected scale" (exactRational 1 (2 ^ (80 :: Int)))
+  let sourcePoint = scaledPoint 1 1 epsilon
+      kernelPoint = scaledPoint 0 0 epsilon
+  left <- mappedComponent sourcePoint (rectangleCoordinates 0 0 4 4) []
+  right <- mappedComponent sourcePoint (rectangleCoordinates 8 0 12 4) []
+  source <- requireRight "tiny disconnected source" (planarRegion [right, left])
+  assertNoFaithfulBinary64Boundary "tiny disconnected source" source
+  kernelPolygon <- requireRight "tiny disconnected centred kernel"
+    (convexPolygon (fmap kernelPoint (rectangleCoordinates (-1) (-1) 1 1)))
+  element <- requireRight "tiny disconnected structuring element" (structuringElement kernelPolygon)
+  expectedExpandedLeft <- mappedComponent sourcePoint (rectangleCoordinates (-1) (-1) 5 5) []
+  expectedExpandedRight <- mappedComponent sourcePoint (rectangleCoordinates 7 (-1) 13 5) []
+  expectedExpanded <- requireRight "tiny disconnected expected dilation"
+    (planarRegion [expectedExpandedLeft, expectedExpandedRight])
+  (expanded, _) <- requireRight "tiny disconnected exact dilation" (polygonOffset element source)
+  assertEqual "tiny dilation preserves the exact gap between components" expectedExpanded expanded
+  assertEqual "tiny disconnected dilation component count" 2 (length (planarRegionComponents expanded))
+  assertRegionArea "tiny disconnected dilated area" (72 * epsilon * epsilon) expanded
+  expectedErodedLeft <- mappedComponent sourcePoint (rectangleCoordinates 1 1 3 3) []
+  expectedErodedRight <- mappedComponent sourcePoint (rectangleCoordinates 9 1 11 3) []
+  expectedEroded <- requireRight "tiny disconnected expected erosion"
+    (planarRegion [expectedErodedLeft, expectedErodedRight])
+  (eroded, _) <- requireRight "tiny disconnected exact erosion" (erodeBy element source)
+  assertEqual "tiny erosion retains both exact components" expectedEroded eroded
+  assertRegionArea "tiny disconnected eroded area" (8 * epsilon * epsilon) eroded
+
+scaledPoint :: ExactRational -> ExactRational -> ExactRational -> (Integer, Integer) -> ExactPoint
+scaledPoint offsetX offsetY scale (x, y) =
+  exactPoint (offsetX + scale * fromInteger x) (offsetY + scale * fromInteger y)
+
+rectangleCoordinates :: Integer -> Integer -> Integer -> Integer -> NonEmpty (Integer, Integer)
+rectangleCoordinates minimumX minimumY maximumX maximumY =
+  (minimumX, minimumY) :| [(maximumX, minimumY), (maximumX, maximumY), (minimumX, maximumY)]
+
+mappedComponent
+  :: ((Integer, Integer) -> ExactPoint)
+  -> NonEmpty (Integer, Integer)
+  -> [NonEmpty (Integer, Integer)]
+  -> IO PolygonComponent
+mappedComponent pointAt outer holes = do
+  outerLoop <- requireRight "exact morphology outer loop" (exactLoop (fmap pointAt outer))
+  holeLoops <- traverse (requireRight "exact morphology hole loop" . exactLoop . fmap pointAt) holes
+  requireRight "exact morphology component" (polygonComponent outerLoop holeLoops)
+
+regionFromMappedPoints :: ((Integer, Integer) -> ExactPoint) -> NonEmpty (Integer, Integer) -> IO PlanarRegion
+regionFromMappedPoints pointAt points = do
+  component <- mappedComponent pointAt points []
+  requireRight "exact morphology region" (planarRegion [component])
+
+assertNoFaithfulBinary64Boundary :: String -> PlanarRegion -> IO ()
+assertNoFaithfulBinary64Boundary label region = do
+  let points = concatMap
+        (\component -> concatMap (NonEmpty.toList . exactLoopPoints)
+          (polygonOuterLoop component : polygonHoleLoops component))
+        (planarRegionComponents region)
+  projected <- traverse (requireRight (label <> " optional projection") . exactPointToEmbeddingCandidate) points
+  assertEqual (label <> " has genuinely distinct exact boundary points") True (Set.size (Set.fromList points) > 1)
+  assertEqual (label <> " cannot use a faithful binary64 boundary") 1
+    (Set.size (Set.fromList (fmap queryPointValue projected)))
+
+assertRegionArea :: String -> ExactRational -> PlanarRegion -> IO ()
+assertRegionArea label expected region = do
+  valuations <- requireRight label (regionValuations region)
+  assertEqual
+    label
+    expected
+    (exactAreaValue (valuationArea valuations))
diff --git a/test/algebra/Moonlight/Planar/PowerDiagramSpec.hs b/test/algebra/Moonlight/Planar/PowerDiagramSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/PowerDiagramSpec.hs
@@ -0,0 +1,496 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Exact bounded power-cell laws against a complete half-plane oracle.
+module Moonlight.Planar.PowerDiagramSpec
+  ( tests
+  ) where
+
+import Control.Monad ( unless )
+import Data.Foldable ( traverse_ )
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Convex ( ConvexPolygon, convexPolygon, convexPolygonPoints )
+import Moonlight.Planar.Exact ( ExactPoint, exactPointCoordinates, exactPointFromPoint,
+  ExactClipDisposition(..), ExactClosedHalfPlane, exactAffineLine, exactClipRetainedPolygon,
+  exactClosedHalfPlane, exactRetainedPolygon, exactRetainedPolygonPoints )
+import Moonlight.Planar.Exact (ExactRational)
+import Moonlight.Planar.PowerDiagram ( powerDiagramInputSites, powerSitePosition, powerWeight,
+  powerWeightExact, boundedPowerDiagram, boundedPowerDiagramFromRegular, powerCellDisposition,
+  powerCellDispositions, powerDiagramPlanarLayer, emptyRegularTriangulation, regularTriangulation,
+  BoundedPowerDiagram, PowerCellDisposition(..), PowerDiagramError(DuplicatePowerSiteLabel),
+  PowerDiagramReceipt(powerDiagramPublishedCells, powerDiagramMaximumCellConstraints,
+  powerDiagramRegularEdges, powerDiagramSubmittedSiteConstraints, powerDiagramActiveBoundaries,
+  powerDiagramCoincidentDominatedCells, powerDiagramCoincidentEquivalentCells,
+  powerDiagramLowerDimensionalCells, powerDiagramEmptyCells), PowerSite(..),
+  PowerWeightError(PowerWeightNonFinite), RegularTriangulation )
+import Moonlight.Planar.PowerFixtures ( admittedSite, admittedWeight, admittedWeightedSite,
+  squareDomain, dispositionTag, powerDistance )
+import Moonlight.Planar.Region ( planarLayerRegions )
+import Moonlight.Planar.Point (Point(..))
+import Support ( assertEqual, integerPoint, requireRight )
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testEqualWeightsAndCommonShift
+    , testThreeSiteExactPartitionCoverage
+    , testBoundedPowerFromRegular
+    , testSparsePowerMatchesCompleteOracle
+    , testEqualCoincidentResolution
+    , testDominantCoincidentResolution
+    , testLowerDimensionalCell
+    , testDistinctEmptyCell
+    , testDyadicNearParallelBisectors
+    , testBinary64PrecisionSites
+    , testDuplicateLabels
+    , testNonFiniteWeight
+    ]
+
+testBoundedPowerFromRegular :: IO ()
+testBoundedPowerFromRegular = do
+  domain <- squareDomain
+  fixtures <-
+    traverse
+      (\(name, submitted) -> (,) name <$> traverse admittedWeightedSite submitted)
+      [ ( "ordinary"
+        , ("a", Point 1 1, 0.25)
+            :| [("b", Point 3 1, -0.125), ("c", Point 2 3, 0.5)]
+        )
+      , ( "lower-dimensional"
+        , ("a", Point 0 0, 0)
+            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -1.5)]
+        )
+      , ( "hidden"
+        , ("a", Point 0 0, 0)
+            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -2)]
+        )
+      , ( "coincident"
+        , ("a", Point 1 1, 0)
+            :| [("b", Point 1 1, 0), ("c", Point 3 1, 0)]
+        )
+      ]
+  traverse_ (assertPreparedPowerMatches domain) fixtures
+  (emptyDiagram, emptyReceipt) <-
+    requireRight
+      "bounded power diagram from empty regular source"
+      (boundedPowerDiagramFromRegular domain (emptyRegularTriangulation :: RegularTriangulation String))
+  assertEqual "empty regular source has no cell dispositions" [] (powerCellDispositions emptyDiagram)
+  assertEqual "empty regular source has no input sites" 0 (powerDiagramInputSites emptyReceipt)
+
+assertPreparedPowerMatches
+  :: ConvexPolygon
+  -> (String, NonEmpty (PowerSite String))
+  -> IO ()
+assertPreparedPowerMatches domain (name, sites) = do
+  (regular, _) <-
+    requireRight (name <> " regular source") (regularTriangulation sites)
+  oneShot <-
+    requireRight (name <> " one-shot power diagram") (boundedPowerDiagram domain sites)
+  fromRegular <-
+    requireRight
+      (name <> " prepared power diagram")
+      (boundedPowerDiagramFromRegular domain regular)
+  assertEqual (name <> " prepared clipping") oneShot fromRegular
+
+testSparsePowerMatchesCompleteOracle :: IO ()
+testSparsePowerMatchesCompleteOracle = do
+  domain <- squareDomain
+  sites <-
+    traverse
+      admittedWeightedSite
+      ( ("a", Point 1.0 1.0, 0.25)
+          :| [ ("b", Point 3.0 0.8, -0.125)
+             , ("c", Point 5.2 1.4, 0.375)
+             , ("d", Point 8.5 1.0, 0.0)
+             , ("e", Point 1.7 3.8, -0.25)
+             , ("f", Point 4.1 4.4, 0.125)
+             , ("g", Point 7.6 3.5, -0.375)
+             , ("h", Point 9.1 5.4, 0.25)
+             , ("i", Point 1.0 7.8, 0.0)
+             , ("j", Point 3.6 8.9, 0.5)
+             , ("k", Point 6.4 7.5, -0.125)
+             , ("l", Point 8.8 9.0, 0.375)
+             ]
+      )
+  (diagram, receipt) <-
+    requireRight "sparse regular-neighbour power diagram" (boundedPowerDiagram domain sites)
+  traverse_
+    (\site -> do
+       oracle <- completeOraclePowerCell domain sites site
+       assertEqual
+         ("regular-neighbour cell equals complete HPI oracle for " <> powerSiteLabel site)
+         (Just oracle)
+         (powerCellDisposition (powerSiteLabel site) diagram))
+    sites
+  let siteCount = NonEmpty.length sites
+      completeConstraintCount = siteCount * (siteCount - 1)
+  unless (powerDiagramSubmittedSiteConstraints receipt < completeConstraintCount) $
+    fail "regular-neighbour construction did not eliminate the complete pairwise cell schedule"
+  unless (powerDiagramMaximumCellConstraints receipt <= powerDiagramRegularEdges receipt) $
+    fail "regular-neighbour construction retained a non-topological radical axis"
+
+data OracleScore = OracleScore
+  { oracleXCoefficient :: !ExactRational
+  , oracleYCoefficient :: !ExactRational
+  , oracleConstant :: !ExactRational
+  }
+
+oracleScore :: PowerSite label -> IO OracleScore
+oracleScore site = do
+  point <- requireRight "oracle source point" (exactPointFromPoint (powerSitePosition site))
+  let (coordinateX, coordinateY) = exactPointCoordinates point
+  pure
+    OracleScore
+      { oracleXCoefficient = 2 * coordinateX
+      , oracleYCoefficient = 2 * coordinateY
+      , oracleConstant =
+          powerWeightExact (powerSiteWeight site)
+            - coordinateX * coordinateX
+            - coordinateY * coordinateY
+      }
+
+completeOraclePowerCell
+  :: ConvexPolygon
+  -> NonEmpty (PowerSite String)
+  -> PowerSite String
+  -> IO (PowerCellDisposition String)
+completeOraclePowerCell domain sites owner = do
+  retainedDomain <-
+    requireRight "oracle retained domain" (exactRetainedPolygon (convexPolygonPoints domain))
+  ownerScore <- oracleScore owner
+  halfPlanes <-
+    traverse
+      (oracleWinningHalfPlane ownerScore)
+      ( filter
+          ((/= powerSiteLabel owner) . powerSiteLabel)
+          (NonEmpty.toList sites)
+      )
+  (disposition, _) <-
+    requireRight
+      ("complete HPI oracle for " <> powerSiteLabel owner)
+      (exactClipRetainedPolygon retainedDomain halfPlanes)
+  case disposition of
+    ExactClipFullDimensional retained ->
+      PublishedPowerCell
+        <$> requireRight
+          "oracle convex publication"
+          (convexPolygon (exactRetainedPolygonPoints retained))
+    ExactClipLowerDimensional points ->
+      pure (LowerDimensionalPowerCell points)
+    ExactClipEmpty -> pure EmptyPowerCell
+
+oracleWinningHalfPlane
+  :: OracleScore
+  -> PowerSite label
+  -> IO ExactClosedHalfPlane
+oracleWinningHalfPlane owner competitor = do
+  competitorScore <- oracleScore competitor
+  line <-
+    requireRight
+      "oracle radical axis"
+      ( exactAffineLine
+          (oracleXCoefficient owner - oracleXCoefficient competitorScore)
+          (oracleYCoefficient owner - oracleYCoefficient competitorScore)
+          (oracleConstant owner - oracleConstant competitorScore)
+      )
+  pure (exactClosedHalfPlane line)
+
+testEqualWeightsAndCommonShift :: IO ()
+testEqualWeightsAndCommonShift = do
+  domain <- squareDomain
+  zero <- admittedWeight 0
+  shifted <- admittedWeight 7
+  zeroSites <-
+    ( :| )
+      <$> admittedSite "left" (Point 2 5) zero
+      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, zero)]
+  shiftedSites <-
+    ( :| )
+      <$> admittedSite "left" (Point 2 5) shifted
+      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, shifted)]
+  (diagram, receipt) <- requireRight "equal-weight power diagram" (boundedPowerDiagram domain zeroSites)
+  (shiftedDiagram, _) <- requireRight "common-shift power diagram" (boundedPowerDiagram domain shiftedSites)
+  assertEqual "common additive power shift" (powerCellDispositions diagram) (powerCellDispositions shiftedDiagram)
+  assertPermutationInvariant "equal-weight power diagram" domain zeroSites diagram
+  assertPermutationInvariant "common-shift power diagram" domain shiftedSites shiftedDiagram
+  expectedLeft <-
+    requireRight
+      "expected left power cell"
+      (convexPolygon (integerPoint 0 0 :| [integerPoint 5 0, integerPoint 5 10, integerPoint 0 10]))
+  expectedRight <-
+    requireRight
+      "expected right power cell"
+      (convexPolygon (integerPoint 5 0 :| [integerPoint 10 0, integerPoint 10 10, integerPoint 5 10]))
+  assertEqual "equal-weight left bisector cell" (Just (PublishedPowerCell expectedLeft)) (powerCellDisposition "left" diagram)
+  assertEqual "equal-weight right bisector cell" (Just (PublishedPowerCell expectedRight)) (powerCellDisposition "right" diagram)
+  assertEqual "receipt input sites" 2 (powerDiagramInputSites receipt)
+  assertEqual
+    "one submitted site constraint per cell"
+    2
+    (powerDiagramSubmittedSiteConstraints receipt)
+  assertEqual
+    "parallel domain and site boundaries coalesce"
+    8
+    (powerDiagramActiveBoundaries receipt)
+  assertEqual "receipt published cells" 2 (powerDiagramPublishedCells receipt)
+  assertEqual "receipt empty cells" 0 (powerDiagramEmptyCells receipt)
+  traverse_ (assertPublishedVerticesWin zeroSites) (powerCellDispositions diagram)
+  let layer = powerDiagramPlanarLayer diagram
+  assertEqual "derived layer contains both cells" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))
+
+testThreeSiteExactPartitionCoverage :: IO ()
+testThreeSiteExactPartitionCoverage = do
+  domain <- squareDomain
+  weight <- admittedWeight 0
+  left <- admittedSite "left" (Point 2 5) weight
+  middle <- admittedSite "middle" (Point 5 5) weight
+  right <- admittedSite "right" (Point 8 5) weight
+  let sites = left :| [middle, right]
+  (diagram, receipt) <-
+    requireRight "three-site exact partition" (boundedPowerDiagram domain sites)
+  assertEqual "three-site partition publishes every cell" 3 (powerDiagramPublishedCells receipt)
+  assertEqual
+    "three-site exact cell areas cover the square"
+    200
+    (publishedTwiceAreaSum diagram)
+  traverse_
+    (\permutation -> do
+       (permuted, _) <-
+         requireRight
+           "permuted three-site exact partition"
+           (boundedPowerDiagram domain permutation)
+       assertEqual
+         "three-site partition is permutation invariant"
+         (powerCellDispositions diagram)
+         (powerCellDispositions permuted)
+       assertEqual
+         "permuted exact cell areas cover the square"
+         200
+         (publishedTwiceAreaSum permuted))
+    (nonEmptyPermutations sites)
+
+nonEmptyPermutations :: NonEmpty value -> [NonEmpty value]
+nonEmptyPermutations =
+  foldMap (maybe [] pure . NonEmpty.nonEmpty)
+    . List.permutations
+    . NonEmpty.toList
+
+assertPermutationInvariant
+  :: String
+  -> ConvexPolygon
+  -> NonEmpty (PowerSite String)
+  -> BoundedPowerDiagram String
+  -> IO ()
+assertPermutationInvariant label domain sites expected =
+  traverse_
+    (\permutation -> do
+       (permuted, _) <-
+         requireRight
+           (label <> " permutation")
+           (boundedPowerDiagram domain permutation)
+       assertEqual
+         (label <> " is permutation invariant")
+         (powerCellDispositions expected)
+         (powerCellDispositions permuted))
+    (nonEmptyPermutations sites)
+
+publishedTwiceAreaSum :: BoundedPowerDiagram label -> ExactRational
+publishedTwiceAreaSum =
+  List.foldl'
+    (\total (_, disposition) -> case disposition of
+        PublishedPowerCell polygon -> total + exactPolygonTwiceArea (convexPolygonPoints polygon)
+        LowerDimensionalPowerCell _ -> total
+        EmptyPowerCell -> total
+        CoincidentEquivalentTo _ -> total
+        CoincidentDominatedBy _ -> total)
+    0
+    . powerCellDispositions
+
+exactPolygonTwiceArea :: NonEmpty ExactPoint -> ExactRational
+exactPolygonTwiceArea points =
+  abs
+    ( List.foldl'
+        (\twiceArea (firstPoint, secondPoint) ->
+           let (firstX, firstY) = exactPointCoordinates firstPoint
+               (secondX, secondY) = exactPointCoordinates secondPoint
+            in twiceArea + firstX * secondY - firstY * secondX)
+        0
+        (cyclicPairs points)
+    )
+
+cyclicPairs :: NonEmpty value -> [(value, value)]
+cyclicPairs (firstValue :| remainingValues) =
+  zip
+    (firstValue : remainingValues)
+    (remainingValues <> [firstValue])
+
+testEqualCoincidentResolution :: IO ()
+testEqualCoincidentResolution = do
+  domain <- squareDomain
+  weight <- admittedWeight 3
+  a <- admittedSite "a" (Point 4 4) weight
+  b <- admittedSite "b" (Point 4 4) weight
+  (diagram, receipt) <- requireRight "equivalent coincident power sites" (boundedPowerDiagram domain (b :| [a]))
+  assertPublished "coincident canonical representative" (powerCellDisposition "a" diagram)
+  assertEqual "equal-function coincident alias" (Just (CoincidentEquivalentTo "a")) (powerCellDisposition "b" diagram)
+  assertEqual "equivalent coincidence receipt" 1 (powerDiagramCoincidentEquivalentCells receipt)
+  assertEqual "equivalent coincidence is not dominance" 0 (powerDiagramCoincidentDominatedCells receipt)
+  (permuted, _) <- requireRight "permuted equivalent sites" (boundedPowerDiagram domain (a :| [b]))
+  assertEqual "equivalent resolution permutation invariance" (powerCellDispositions diagram) (powerCellDispositions permuted)
+
+testDominantCoincidentResolution :: IO ()
+testDominantCoincidentResolution = do
+  domain <- squareDomain
+  high <- admittedWeight 3
+  low <- admittedWeight 2
+  winner <- admittedSite "winner" (Point 4 4) high
+  dominated <- admittedSite "dominated" (Point 4 4) low
+  (diagram, receipt) <- requireRight "dominant coincident power sites" (boundedPowerDiagram domain (dominated :| [winner]))
+  assertPublished "dominant coincident representative" (powerCellDisposition "winner" diagram)
+  assertEqual "lower-weight coincident site" (Just (CoincidentDominatedBy "winner")) (powerCellDisposition "dominated" diagram)
+  assertEqual "dominant coincidence receipt" 1 (powerDiagramCoincidentDominatedCells receipt)
+  assertEqual "dominance is not equivalence" 0 (powerDiagramCoincidentEquivalentCells receipt)
+  assertPermutationInvariant "dominant coincident power sites" domain (dominated :| [winner]) diagram
+
+testLowerDimensionalCell :: IO ()
+testLowerDimensionalCell = do
+  domain <- squareDomain
+  zero <- admittedWeight 0
+  suppressed <- admittedWeight (-1)
+  left <- admittedSite "left" (Point 0 5) zero
+  right <- admittedSite "right" (Point 2 5) zero
+  middle <- admittedSite "middle" (Point 1 5) suppressed
+  let sites = left :| [right, middle]
+  (diagram, receipt) <- requireRight "one-dimensional bounded power cell" (boundedPowerDiagram domain sites)
+  case powerCellDisposition "middle" diagram of
+    Just (LowerDimensionalPowerCell points) ->
+      assertEqual
+        "middle cell is the exact x=1 segment"
+        (List.sort [integerPoint 1 0, integerPoint 1 10])
+        (List.sort (NonEmpty.toList points))
+    other -> fail ("one-dimensional power cell: expected retained segment, got " <> dispositionTag other)
+  assertEqual "lower-dimensional receipt" 1 (powerDiagramLowerDimensionalCells receipt)
+  assertEqual "lower-dimensional cell is not empty" 0 (powerDiagramEmptyCells receipt)
+  assertEqual "two full-dimensional neighbours" 2 (powerDiagramPublishedCells receipt)
+  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
+  let layer = powerDiagramPlanarLayer diagram
+  assertEqual "derived layer omits the one-dimensional cell" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))
+  assertPermutationInvariant "one-dimensional bounded power cell" domain sites diagram
+
+testDistinctEmptyCell :: IO ()
+testDistinctEmptyCell = do
+  domain <- squareDomain
+  ordinary <- admittedWeight 0
+  suppressed <- admittedWeight (-1000)
+  winner <- admittedSite "winner" (Point 0 0) ordinary
+  hidden <- admittedSite "hidden" (Point 5 5) suppressed
+  (diagram, receipt) <- requireRight "distinct empty power cell" (boundedPowerDiagram domain (winner :| [hidden]))
+  assertPublished "dominant distinct site" (powerCellDisposition "winner" diagram)
+  assertEqual "distinct site may have empty bounded cell" (Just EmptyPowerCell) (powerCellDisposition "hidden" diagram)
+  assertEqual "empty disposition receipt" 1 (powerDiagramEmptyCells receipt)
+  assertPermutationInvariant "distinct empty power cell" domain (winner :| [hidden]) diagram
+
+testDyadicNearParallelBisectors :: IO ()
+testDyadicNearParallelBisectors = do
+  domain <- squareDomain
+  weight <- admittedWeight 0
+  let epsilon = 2 ** (-20) :: Double
+  origin <- admittedSite "origin" (Point 0 0) weight
+  horizontal <- admittedSite "horizontal" (Point 2 0) weight
+  tilted <- admittedSite "tilted" (Point 2 epsilon) weight
+  let sites = origin :| [horizontal, tilted]
+  (diagram, receipt) <-
+    requireRight
+      "dyadic near-parallel power bisectors"
+      (boundedPowerDiagram domain sites)
+  assertEqual "near-parallel cells retain full dimension" 3 (powerDiagramPublishedCells receipt)
+  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
+  assertPermutationInvariant "dyadic near-parallel power bisectors" domain sites diagram
+
+testBinary64PrecisionSites :: IO ()
+testBinary64PrecisionSites = do
+  domain <- squareDomain
+  firstWeight <- admittedWeight 0.2
+  secondWeight <- admittedWeight (-0.3)
+  thirdWeight <- admittedWeight 0.7
+  firstSite <- admittedSite "first" (Point 0.1 0.3) firstWeight
+  secondSite <- admittedSite "second" (Point 9.7 0.2) secondWeight
+  thirdSite <- admittedSite "third" (Point 4.9 9.6) thirdWeight
+  let sites = firstSite :| [secondSite, thirdSite]
+  (diagram, receipt) <-
+    requireRight
+      "exact binary64 power sites"
+      (boundedPowerDiagram domain sites)
+  assertEqual "binary64 fixture publishes every cell" 3 (powerDiagramPublishedCells receipt)
+  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
+  assertPermutationInvariant "exact binary64 power sites" domain sites diagram
+
+testDuplicateLabels :: IO ()
+testDuplicateLabels = do
+  domain <- squareDomain
+  weight <- admittedWeight 0
+  firstSite <- admittedSite "duplicate" (Point 1 1) weight
+  secondSite <- admittedSite "duplicate" (Point 9 9) weight
+  case boundedPowerDiagram domain (firstSite :| [secondSite]) of
+    Left (DuplicatePowerSiteLabel "duplicate") -> pure ()
+    other -> fail ("duplicate power-site label: expected typed refusal, got " <> show other)
+
+testNonFiniteWeight :: IO ()
+testNonFiniteWeight =
+  case powerWeight (0 / 0) of
+    Left (PowerWeightNonFinite _) -> pure ()
+    other -> fail ("non-finite power weight: expected typed refusal, got " <> show other)
+
+uncurry3 :: (a -> b -> c -> result) -> (a, b, c) -> result
+uncurry3 function (firstValue, secondValue, thirdValue) = function firstValue secondValue thirdValue
+
+assertPublished :: String -> Maybe (PowerCellDisposition label) -> IO ()
+assertPublished _ (Just (PublishedPowerCell _)) = pure ()
+assertPublished label other = fail (label <> ": expected published cell, got " <> dispositionTag other)
+
+assertPublishedVerticesWin
+  :: NonEmpty (PowerSite String)
+  -> (String, PowerCellDisposition String)
+  -> IO ()
+assertPublishedVerticesWin sites (ownerLabel, disposition) =
+  case disposition of
+    PublishedPowerCell polygon ->
+      assertCellPointsWin ownerLabel sites (convexPolygonPoints polygon)
+    LowerDimensionalPowerCell points ->
+      assertCellPointsWin ownerLabel sites points
+    EmptyPowerCell -> pure ()
+    CoincidentEquivalentTo _ -> pure ()
+    CoincidentDominatedBy _ -> pure ()
+
+assertCellPointsWin
+  :: String
+  -> NonEmpty (PowerSite String)
+  -> NonEmpty ExactPoint
+  -> IO ()
+assertCellPointsWin ownerLabel sites points =
+  case lookupOwner ownerLabel (NonEmpty.toList sites) of
+    Nothing -> fail ("power-cell owner missing: " <> ownerLabel)
+    Just owner ->
+      traverse_
+        (\point -> traverse_ (assertOwnerWinsAt point owner) sites)
+        points
+
+lookupOwner :: Eq label => label -> [PowerSite label] -> Maybe (PowerSite label)
+lookupOwner label = List.find ((== label) . powerSiteLabel)
+
+assertOwnerWinsAt
+  :: ExactPoint
+  -> PowerSite String
+  -> PowerSite String
+  -> IO ()
+assertOwnerWinsAt point owner competitor = do
+  ownerValue <- powerDistance point owner
+  competitorValue <- powerDistance point competitor
+  unless (ownerValue <= competitorValue) $
+    fail
+      ( "power-cell vertex violates source inequality: "
+          <> show (powerSiteLabel owner, powerSiteLabel competitor, ownerValue, competitorValue)
+      )
diff --git a/test/algebra/Moonlight/Planar/PowerFixtures.hs b/test/algebra/Moonlight/Planar/PowerFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/PowerFixtures.hs
@@ -0,0 +1,59 @@
+-- | Admitted sites and exact observations shared by regular, alpha, and bounded-cell laws.
+module Moonlight.Planar.PowerFixtures
+  ( admittedWeight
+  , admittedSite
+  , admittedWeightedSite
+  , squareDomain
+  , dispositionTag
+  , powerDistance
+  ) where
+
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Convex ( ConvexPolygon, convexPolygon )
+import Moonlight.Planar.Exact ( ExactPoint, exactPointCoordinates, exactPointFromPoint )
+import Moonlight.Planar.Exact (ExactRational)
+import Moonlight.Planar.PowerDiagram ( powerSite, powerSitePosition, powerWeight, powerWeightExact,
+  PowerCellDisposition(..), PowerSite(powerSiteWeight), PowerWeight )
+import Moonlight.Planar.Point (Point(..))
+import Support ( integerPoint, requireRight )
+
+
+squareDomain :: IO ConvexPolygon
+squareDomain =
+  requireRight
+    "square clipping domain"
+    (convexPolygon (integerPoint 0 0 :| [integerPoint 10 0, integerPoint 10 10, integerPoint 0 10]))
+
+admittedWeight :: Double -> IO PowerWeight
+admittedWeight value =
+  requireRight
+    "finite power weight"
+    (powerWeight value)
+
+admittedSite :: String -> Point -> PowerWeight -> IO (PowerSite String)
+admittedSite label point weight = requireRight "admitted power site" (powerSite label point weight)
+
+admittedWeightedSite
+  :: (String, Point, Double)
+  -> IO (PowerSite String)
+admittedWeightedSite (label, point, weightValue) = do
+  weight <- admittedWeight weightValue
+  admittedSite label point weight
+
+dispositionTag :: Maybe (PowerCellDisposition label) -> String
+dispositionTag Nothing = "missing"
+dispositionTag (Just (PublishedPowerCell _)) = "published"
+dispositionTag (Just (LowerDimensionalPowerCell _)) = "lower-dimensional"
+dispositionTag (Just EmptyPowerCell) = "empty"
+dispositionTag (Just (CoincidentEquivalentTo _)) = "coincident-equivalent"
+dispositionTag (Just (CoincidentDominatedBy _)) = "coincident-dominated"
+
+-- | Independent exact squared-distance specification, not the regular-neighbour implementation.
+powerDistance :: ExactPoint -> PowerSite label -> IO ExactRational
+powerDistance point site = do
+  exactSite <- requireRight "site exact position" (exactPointFromPoint (powerSitePosition site))
+  let (x, y) = exactPointCoordinates point
+      (siteX, siteY) = exactPointCoordinates exactSite
+      deltaX = x - siteX
+      deltaY = y - siteY
+  pure (deltaX * deltaX + deltaY * deltaY - powerWeightExact (powerSiteWeight site))
diff --git a/test/algebra/Moonlight/Planar/PowerMassSpec.hs b/test/algebra/Moonlight/Planar/PowerMassSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/PowerMassSpec.hs
@@ -0,0 +1,287 @@
+-- | Exact certificates and reconstruction laws through the public power owner.
+module Moonlight.Planar.PowerMassSpec (tests) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import Moonlight.Planar.Convex (ConvexPolygon, convexPolygon, convexPolygonPoints)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , exactDivide
+  , exactPointCross
+  , exactRational
+  )
+import Moonlight.Planar.PowerDiagram
+  ( PowerCellDisposition (..)
+  , PowerMassBoundary (..)
+  , PowerMassCertificate
+  , PowerMassError (..)
+  , PowerMassObligation (..)
+  , PowerMassPolynomial (..)
+  , PowerSite
+  , PreparedPowerMassSection
+  , boundedPowerDiagram
+  , evaluatePowerMasses
+  , powerCellDispositions
+  , powerMassBoundCertificates
+  , powerMassBoundParameter
+  , powerMassCertificateConstant
+  , powerMassCertificateObligation
+  , powerMassCertificateRate
+  , powerMassIntervalLower
+  , powerMassIntervalUpper
+  , powerMassSectionCertificates
+  , powerMassSectionDirection
+  , powerMassSectionInterval
+  , powerMassSectionPolynomials
+  , powerMassSectionSites
+  , powerMassSectionWindow
+  , powerSite
+  , powerSiteLabel
+  , powerSitePosition
+  , powerSiteWeight
+  , powerWeightExact
+  , powerWeightFromExact
+  , preparePowerMassSection
+  )
+import Moonlight.Planar.Point (Point (..))
+import Support (assertEqual, integerPoint, requireRight)
+
+tests :: IO ()
+tests = do
+  _ <- sequenceA
+    [ testThreeSiteCoefficients
+    , testWindowContact
+    , testUniformShifts
+    , testRestrictions
+    , testThreeConstraintEmptyWitness
+    , testDifferentialFamilies
+    ]
+  putStrLn "certified power masses: ok"
+
+testThreeSiteCoefficients :: IO ()
+testThreeSiteCoefficients = do
+  section <- threeSiteSection
+  firstCoefficients <- polynomial (9, 2) (15, 16) (3, 64)
+  secondCoefficients <- polynomial (9, 2) (-7, 16) (-1, 64)
+  thirdCoefficients <- polynomial (7, 1) (-1, 2) (-1, 32)
+  let coefficients = powerMassSectionPolynomials section
+      interval = powerMassSectionInterval section
+  assertEqual "three-site exact coefficients"
+    (Map.fromList [(0, firstCoefficients), (1, secondCoefficients), (2, thirdCoefficients)]) coefficients
+  assertEqual "all mass coefficients conserve the window" (PowerMassPolynomial 16 0 0) (foldMap id coefficients)
+  assertEqual "three-site lower endpoint" (Just (-8)) (powerMassBoundParameter <$> powerMassIntervalLower interval)
+  assertEqual "three-site upper endpoint" (Just 5) (powerMassBoundParameter <$> powerMassIntervalUpper interval)
+  assertEqual "all lower simultaneous roots retained"
+    (Set.fromList (filter (vanishesAt (-8)) (powerMassSectionCertificates section)))
+    (maybe Set.empty (Set.fromList . NonEmpty.toList . powerMassBoundCertificates) (powerMassIntervalLower interval))
+  assertEqual "all upper simultaneous roots retained"
+    (Set.fromList (filter (vanishesAt 5) (powerMassSectionCertificates section)))
+    (maybe Set.empty (Set.fromList . NonEmpty.toList . powerMassBoundCertificates) (powerMassIntervalUpper interval))
+  _ <- traverse (compareReconstruction section) [-7, -1, 0, 1, 4]
+  _ <- traverse (assertExpired section) [-9, -8, 5, 6]
+  pure ()
+ where
+  vanishesAt :: ExactRational -> PowerMassCertificate Int -> Bool
+  vanishesAt parameter certificate =
+    powerMassCertificateConstant certificate + parameter * powerMassCertificateRate certificate == 0
+
+testWindowContact :: IO ()
+testWindowContact = do
+  window <- rectangle (-1) (-1) 1 1
+  sites <- traverse admittedSite ((0, Point 0 0, 0) :| [(1, Point 2 0, -4)])
+  section <- requireRight "window-contact initial section"
+    (preparePowerMassSection window sites (Map.fromList [(0, 0), (1, 4)]))
+  assertEqual "window-hidden cell has zero coefficients" (Just mempty) (Map.lookup 1 (powerMassSectionPolynomials section))
+  assertEqual "window contact expires at one" (Just 1)
+    (powerMassBoundParameter <$> powerMassIntervalUpper (powerMassSectionInterval section))
+  assertEqual "empty cell retains window support" True
+    (any windowWitness (powerMassSectionCertificates section))
+  assertExpired section 1
+  threeHalves <- rational 3 2
+  assertExpired section threeHalves
+  shifted <- reweightedSites section threeHalves
+  nextSection <- requireRight "recertify reconstructed section after contact"
+    (preparePowerMassSection window shifted (powerMassSectionDirection section))
+  masses <- requireRight "post-contact exact masses" (evaluatePowerMasses 0 nextSection)
+  assertEqual "window entry occurs without a two-site regular flip" (Map.fromList [(0, 3), (1, 1)]) masses
+  compareReconstruction nextSection 0
+ where
+  windowWitness :: PowerMassCertificate Int -> Bool
+  windowWitness certificate = case powerMassCertificateObligation certificate of
+    PowerMassEmptyWitness 1 sources -> any isWindow (fmap fst sources)
+    _ -> False
+  isWindow :: PowerMassBoundary Int -> Bool
+  isWindow (PowerMassWindowBoundary _) = True
+  isWindow (PowerMassSiteBoundary _) = False
+
+testUniformShifts :: IO ()
+testUniformShifts = do
+  section <- threeSiteSection
+  shifted <- traverse (shiftSiteWeight 23) (powerMassSectionSites section)
+  weightShifted <- requireRight "uniform source-weight shift"
+    (preparePowerMassSection (powerMassSectionWindow section) shifted (powerMassSectionDirection section))
+  rateShifted <- requireRight "uniform weight-direction shift"
+    (preparePowerMassSection (powerMassSectionWindow section) (powerMassSectionSites section) (fmap (+ 7) (powerMassSectionDirection section)))
+  _ <- traverse
+    (\candidate -> do
+      assertEqual "uniform shift preserves coefficients" (powerMassSectionPolynomials section) (powerMassSectionPolynomials candidate)
+      assertEqual "uniform shift preserves interval and certificates" (powerMassSectionInterval section) (powerMassSectionInterval candidate)
+      compareReconstruction candidate 4)
+    [weightShifted, rateShifted]
+  constant <- requireRight "constant masses under common weight direction"
+    (preparePowerMassSection (powerMassSectionWindow section) (powerMassSectionSites section) (Map.fromList [(0, 9), (1, 9), (2, 9)]))
+  assertEqual "common weight direction has no lower bound" Nothing (powerMassIntervalLower (powerMassSectionInterval constant))
+  assertEqual "common weight direction has no upper bound" Nothing (powerMassIntervalUpper (powerMassSectionInterval constant))
+  compareReconstruction constant 1000
+ where
+  shiftSiteWeight :: ExactRational -> PowerSite Int -> IO (PowerSite Int)
+  shiftSiteWeight shift site = requireRight "uniformly shifted admitted site"
+    (powerSite (powerSiteLabel site) (powerSitePosition site) (powerWeightFromExact (powerWeightExact (powerSiteWeight site) + shift)))
+
+testRestrictions :: IO ()
+testRestrictions = do
+  window <- rectangle (-2) (-2) 2 2
+  coincident <- traverse admittedSite ((0, Point 0 0, 0) :| [(1, Point 0 0, 0)])
+  assertEqual "coincident restriction belongs to observation" True
+    (case preparePowerMassSection window coincident (Map.fromList [(0, 0), (1, 0)]) of
+      Left (PowerMassCoincidentSites 0 1) -> True
+      _ -> False)
+  lower <- traverse admittedSite ((0, Point 0 0, 0) :| [(1, Point (-1) 0, 1), (2, Point 1 0, 1)])
+  assertEqual "lower-dimensional seed is explicit" True
+    (case preparePowerMassSection window lower (Map.fromList [(0, 0), (1, 0), (2, 0)]) of
+      Left (PowerMassLowerDimensionalSeed 0 _) -> True
+      _ -> False)
+  highIncidence <- traverse admittedSite
+    ((0, Point (-1) (-1), 0) :| [(1, Point 1 (-1), 0), (2, Point 1 1, 0), (3, Point (-1) 1, 0)])
+  assertEqual "fourfold power vertex refuses silent perturbation" True
+    (case preparePowerMassSection window highIncidence (Map.fromList [(0, 0), (1, 0), (2, 0), (3, 0)]) of
+      Left (PowerMassActiveIncidence _ _ sources) -> length sources > 2
+      _ -> False)
+  section <- threeSiteSection
+  assertEqual "direction must have exactly the source label section" True
+    (case preparePowerMassSection window (powerMassSectionSites section) (Map.fromList [(0, 1), (1, 0), (7, 0)]) of
+      Left (PowerMassDirectionMismatch missing extra) -> missing == Set.singleton 2 && extra == Set.singleton 7
+      _ -> False)
+
+testThreeConstraintEmptyWitness :: IO ()
+testThreeConstraintEmptyWitness = do
+  window <- rectangle (-10) (-10) 10 10
+  sites <- traverse admittedSite
+    ((0, Point 0 0, -10) :| [(1, Point (-1) (-1), 0), (2, Point 1 (-1), 0), (3, Point 0 1, 0)])
+  section <- requireRight "strict three-constraint empty witness"
+    (preparePowerMassSection window sites (Map.fromList [(0, 1), (1, 0), (2, 0), (3, 0)]))
+  assertEqual "empty witness needs all three site constraints" True
+    (any threeSites (powerMassSectionCertificates section))
+  compareReconstruction section 0
+ where
+  threeSites :: PowerMassCertificate Int -> Bool
+  threeSites certificate = case powerMassCertificateObligation certificate of
+    PowerMassEmptyWitness 0 sources ->
+      Set.fromList (fmap fst (NonEmpty.toList sources)) == Set.fromList (fmap PowerMassSiteBoundary [1, 2, 3])
+    _ -> False
+
+-- Deterministic rational families, not sampled floating certificates. Every
+-- admitted family is reconstructed at exact interior values on both sides of
+-- zero. Restricted seed refusals remain visible rather than coerced to cells.
+testDifferentialFamilies :: IO ()
+testDifferentialFamilies = do
+  window <- rectangle (-30) (-30) 30 30
+  admitted <- traverse (checkFamily window) [1 .. 48 :: Int]
+  assertEqual "differential corpus admits at least forty families" True (sum admitted >= 40)
+  putStrLn
+    ( "certified power masses differential: " <> show (sum admitted)
+        <> " admitted families, " <> show (length admitted - sum admitted)
+        <> " restricted seed refusals, " <> show (18 * sum admitted)
+        <> " exact per-cell mass comparisons"
+    )
+ where
+  checkFamily window seed = do
+    sites <- traverse (familySite seed) (0 :| [1 .. 5])
+    rates <- traverse (\index -> do
+      rate <- rational (fromIntegral ((seed * 7 + index * 11) `mod` 13 - 6)) 5
+      pure (index, rate)) [0 .. 5]
+    case preparePowerMassSection window sites (Map.fromList rates) of
+      Right section -> do
+        negative <- maybe (pure (-1)) (\bound -> requireRight "interior lower half" (exactDivide (powerMassBoundParameter bound) 2))
+          (powerMassIntervalLower (powerMassSectionInterval section))
+        positive <- maybe (pure 1) (\bound -> requireRight "interior upper half" (exactDivide (powerMassBoundParameter bound) 2))
+          (powerMassIntervalUpper (powerMassSectionInterval section))
+        _ <- traverse (compareReconstruction section) [negative, 0, positive]
+        assertEqual "differential coefficient conservation" (PowerMassPolynomial 3600 0 0)
+          (foldMap id (powerMassSectionPolynomials section))
+        pure (1 :: Int)
+      Left (PowerMassActiveIncidence _ _ _) -> pure 0
+      Left (PowerMassLowerDimensionalSeed _ _) -> pure 0
+      Left obstruction -> fail ("differential preparation obstruction: " <> show obstruction)
+  familySite seed index = do
+    weight <- rational (fromIntegral ((seed * 17 + index * 19) `mod` 31 - 15)) 7
+    admittedSite
+      ( index
+      , Point (fromIntegral (3 * index - 7)) (fromIntegral ((seed * 5 + index * index * 3) `mod` 19 - 9))
+      , weight
+      )
+
+threeSiteSection :: IO (PreparedPowerMassSection Int)
+threeSiteSection = do
+  window <- rectangle (-2) (-2) 2 2
+  sites <- traverse admittedSite
+    ((0, Point (-1) (-1), 0) :| [(1, Point 1 (-1), 0), (2, Point 0 1, 0)])
+  requireRight "three-site prepared section" (preparePowerMassSection window sites (Map.fromList [(0, 1), (1, 0), (2, 0)]))
+
+rectangle :: Integer -> Integer -> Integer -> Integer -> IO ConvexPolygon
+rectangle minimumX minimumY maximumX maximumY = requireRight "convex mass window"
+  (convexPolygon
+    (integerPoint minimumX minimumY :|
+      [integerPoint maximumX minimumY, integerPoint maximumX maximumY, integerPoint minimumX maximumY]))
+
+admittedSite :: (Int, Point, ExactRational) -> IO (PowerSite Int)
+admittedSite (label, position, weight) = requireRight "admitted mass site"
+  (powerSite label position (powerWeightFromExact weight))
+
+rational :: Integer -> Integer -> IO ExactRational
+rational numerator denominator = requireRight "exact test coefficient" (exactRational numerator denominator)
+
+polynomial :: (Integer, Integer) -> (Integer, Integer) -> (Integer, Integer) -> IO PowerMassPolynomial
+polynomial constant linear quadratic =
+  PowerMassPolynomial <$> uncurry rational constant <*> uncurry rational linear <*> uncurry rational quadratic
+
+assertExpired :: PreparedPowerMassSection Int -> ExactRational -> IO ()
+assertExpired section parameter = assertEqual "expired parameter is refused" True
+  (case evaluatePowerMasses parameter section of
+    Left (PowerMassParameterOutsideCertificate rejected interval) ->
+      rejected == parameter && interval == powerMassSectionInterval section
+    _ -> False)
+
+reweightedSites :: PreparedPowerMassSection Int -> ExactRational -> IO (NonEmpty (PowerSite Int))
+reweightedSites section parameter = traverse reweight (powerMassSectionSites section)
+ where
+  reweight site = do
+    rate <- maybe (fail "section direction lost its site") pure (Map.lookup (powerSiteLabel site) (powerMassSectionDirection section))
+    requireRight "exact reconstruction weight"
+      (powerSite (powerSiteLabel site) (powerSitePosition site)
+        (powerWeightFromExact (powerWeightExact (powerSiteWeight site) + parameter * rate)))
+
+compareReconstruction :: PreparedPowerMassSection Int -> ExactRational -> IO ()
+compareReconstruction section parameter = do
+  observed <- requireRight "evaluate exact mass section" (evaluatePowerMasses parameter section)
+  sites <- reweightedSites section parameter
+  (diagram, _) <- requireRight "reconstruct public bounded power cells"
+    (boundedPowerDiagram (powerMassSectionWindow section) sites)
+  reconstructed <- traverse cellArea (powerCellDispositions diagram)
+  assertEqual "certificate agrees with exact clipping reconstruction" (Map.fromList reconstructed) observed
+ where
+  cellArea :: (Int, PowerCellDisposition Int) -> IO (Int, ExactRational)
+  cellArea (label, disposition) = case disposition of
+    PublishedPowerCell polygon -> do
+      area <- polygonArea (convexPolygonPoints polygon)
+      pure (label, area)
+    EmptyPowerCell -> pure (label, 0)
+    other -> fail ("unexpected reconstructed disposition: " <> show other)
+
+polygonArea :: NonEmpty ExactPoint -> IO ExactRational
+polygonArea points@(initial :| remaining) =
+  requireRight "exact reconstructed area"
+    (exactDivide (sum (zipWith exactPointCross (NonEmpty.toList points) (remaining <> [initial]))) 2)
diff --git a/test/algebra/Moonlight/Planar/RegionAlgebraSpec.hs b/test/algebra/Moonlight/Planar/RegionAlgebraSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/RegionAlgebraSpec.hs
@@ -0,0 +1,90 @@
+-- | Facade-only laws for exact planar Boolean composition.
+module Moonlight.Planar.RegionAlgebraSpec (tests) where
+
+import Moonlight.Planar.Convex (convexPolygon)
+import Moonlight.Planar.Minkowski (MinkowskiOperation (..), MinkowskiReceipt (..), polygonOffset, structuringElement)
+import Moonlight.Planar.Overlay (overlaySelectedRegion)
+import Moonlight.Planar.Region (PlanarRegion, exactLoopPoints, polygonOuterLoop)
+import Moonlight.Planar.Valuation (eulerCharacteristicValue, exactAreaValue, regionValuations, valuationArea, valuationEuler)
+import Moonlight.Planar.OverlayFixtures (overlayRegions)
+import Support (rectangleRegion, rectangleComponent, assertEqual, requireRight)
+
+tests :: IO ()
+tests = do
+  testRegionBooleanLaws
+  testFacadeComposition
+  putStrLn "region algebra: ok"
+
+testRegionBooleanLaws :: IO ()
+testRegionBooleanLaws = do
+  left <- rectangleRegion 0 0 2 2
+  middle <- rectangleRegion 1 0 3 2
+  right <- rectangleRegion 2 0 4 2
+  expectedUnion <- rectangleRegion 0 0 3 2
+  expectedIntersection <- rectangleRegion 1 0 2 2
+  expectedDifference <- rectangleRegion 0 0 1 2
+
+  leftUnionMiddle <- regionUnion left middle
+  middleUnionLeft <- regionUnion middle left
+  assertEqual "region union result" expectedUnion leftUnionMiddle
+  assertEqual "region union commutativity" leftUnionMiddle middleUnionLeft
+  assertEqual "region union idempotence" left =<< regionUnion left left
+  assertEqual
+    "region intersection result"
+    expectedIntersection
+    =<< regionIntersection left middle
+  assertEqual
+    "regularized region difference"
+    expectedDifference
+    =<< regionDifference left middle
+
+  leftAssociated <- regionUnion leftUnionMiddle right
+  middleUnionRight <- regionUnion middle right
+  rightAssociated <- regionUnion left middleUnionRight
+  assertEqual "region union associativity" leftAssociated rightAssociated
+
+testFacadeComposition :: IO ()
+testFacadeComposition = do
+  left <- rectangleRegion 0 0 2 2
+  right <- rectangleRegion 1 0 3 2
+  overlay <- overlayRegions left right
+  selectedUnion <-
+    requireRight
+      "facade selected region"
+      (overlaySelectedRegion (uncurry (||)) overlay)
+  valuations <- requireRight "facade region valuations" (regionValuations selectedUnion)
+  assertEqual
+    "facade valuation digest"
+    (1, 6)
+    ( eulerCharacteristicValue (valuationEuler valuations)
+    , exactAreaValue (valuationArea valuations)
+    )
+
+  kernelComponent <- rectangleComponent 0 0 1 1
+  kernel <-
+    requireRight
+      "facade convex kernel"
+      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
+  element <- requireRight "facade structuring element" (structuringElement kernel)
+  (expanded, receipt) <- requireRight "facade polygon offset" (polygonOffset element selectedUnion)
+  expectedExpanded <- rectangleRegion 0 0 4 3
+  assertEqual "facade morphology result" expectedExpanded expanded
+  assertEqual "facade morphology receipt" MinkowskiAddition (minkowskiOperation receipt)
+
+regionUnion :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
+regionUnion = combineRegions (uncurry (||))
+
+regionIntersection :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
+regionIntersection = combineRegions (uncurry (&&))
+
+regionDifference :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
+regionDifference = combineRegions (\(insideLeft, insideRight) -> insideLeft && not insideRight)
+
+combineRegions
+  :: ((Bool, Bool) -> Bool)
+  -> PlanarRegion
+  -> PlanarRegion
+  -> IO PlanarRegion
+combineRegions selected left right = do
+  overlay <- overlayRegions left right
+  requireRight "region Boolean publication" (overlaySelectedRegion selected overlay)
diff --git a/test/algebra/Moonlight/Planar/RegularAlphaSpec.hs b/test/algebra/Moonlight/Planar/RegularAlphaSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/RegularAlphaSpec.hs
@@ -0,0 +1,102 @@
+-- | Signed weighted-alpha births, closed sublevels, and common weight shifts.
+module Moonlight.Planar.RegularAlphaSpec
+  ( tests
+  ) where
+
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.PowerDiagram ( reweightRegularSites, regularTriangulation,
+  RegularEditResult(regularEditTransitions, regularEditTriangulation) )
+import Moonlight.Planar.PowerFixtures ( admittedSite, admittedWeight )
+import Moonlight.Planar.RegularAlpha ( powerAlphaBirthExact, regularAlphaBirths,
+  regularAlphaComplex, regularAlphaComplexAtBirth, regularAlphaFiltration )
+import Moonlight.Planar.Simplex ( planarComplexCells, planarEdge, planarFace, planarVertex )
+import Moonlight.Planar.Point (Point(..))
+import Support ( assertEqual, requireRight, requireJust )
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import qualified Data.Vector as Vector
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testRegularAlphaBirths
+    , testRegularAlphaCommonShift
+    ]
+
+testRegularAlphaBirths :: IO ()
+testRegularAlphaBirths = do
+  positive <- admittedWeight 3
+  singleton <- admittedSite "only" (Point 0 0) positive
+  (singleRegular, _) <-
+    requireRight "singleton regular topology" (regularTriangulation (singleton :| []))
+  singleAlpha <- requireRight "singleton weighted alpha" (regularAlphaFiltration singleRegular)
+  assertEqual
+    "positive weight gives signed vertex birth"
+    (Just (-3))
+    (fmap powerAlphaBirthExact (Map.lookup (planarVertex "only") (regularAlphaBirths singleAlpha)))
+
+  zero <- admittedWeight 0
+  firstSite <- admittedSite "first" (Point 0 0) zero
+  secondSite <- admittedSite "second" (Point 2 0) zero
+  thirdSite <- admittedSite "third" (Point 0 2) zero
+  dominated <- admittedSite "dominated" (Point 0 0) =<< admittedWeight (-1)
+  (regular, _) <-
+    requireRight
+      "triangle regular topology for weighted alpha"
+      (regularTriangulation (firstSite :| [secondSite, thirdSite, dominated]))
+  filtration <- requireRight "triangle weighted alpha" (regularAlphaFiltration regular)
+  firstSecond <- requireRight "first-second simplex" (planarEdge "first" "second")
+  firstThird <- requireRight "first-third simplex" (planarEdge "first" "third")
+  secondThird <- requireRight "second-third simplex" (planarEdge "second" "third")
+  face <- requireRight "triangle simplex" (planarFace "first" "second" "third")
+  let birthAt simplex = fmap powerAlphaBirthExact (Map.lookup simplex (regularAlphaBirths filtration))
+  assertEqual "first leg birth" (Just 1) (birthAt firstSecond)
+  assertEqual "second leg birth" (Just 1) (birthAt firstThird)
+  assertEqual "hypotenuse birth" (Just 2) (birthAt secondThird)
+  assertEqual "face birth" (Just 2) (birthAt face)
+  assertEqual
+    "coincident subordinate has no alpha simplex"
+    False
+    (Set.member (planarVertex "dominated") (planarComplexCells (regularAlphaComplex filtration)))
+  threshold <- requireJust "edge threshold" (Map.lookup firstSecond (regularAlphaBirths filtration))
+  _ <- requireRight "weighted alpha sublevel is closed" (regularAlphaComplexAtBirth threshold filtration)
+  pure ()
+
+testRegularAlphaCommonShift :: IO ()
+testRegularAlphaCommonShift = do
+  zero <- admittedWeight 0
+  shifted <- admittedWeight 3
+  let labelledPoints =
+        ("south-west", Point 0 0)
+          :| [ ("south-east", Point 4 0)
+             , ("north-east", Point 3 3)
+             , ("north-west", Point 0 4)
+             ]
+  baseSites <- traverse (\(label, point) -> admittedSite label point zero) labelledPoints
+  shiftedSites <- traverse (\(label, point) -> admittedSite label point shifted) labelledPoints
+  (baseRegular, _) <- requireRight "base regular alpha topology" (regularTriangulation baseSites)
+  (shiftedRegular, _) <- requireRight "shifted regular alpha topology" (regularTriangulation shiftedSites)
+  shiftedEdit <-
+    requireRight
+      "common regular weight shift"
+      ( reweightRegularSites
+          (Map.fromList [(label, shifted) | (label, _) <- NonEmpty.toList labelledPoints])
+          baseRegular
+      )
+  assertEqual
+    "common shift reuses the regular topology"
+    shiftedRegular
+    (regularEditTriangulation shiftedEdit)
+  assertEqual "common shift changes no disposition" [] (Vector.toList (regularEditTransitions shiftedEdit))
+  baseAlpha <- requireRight "base regular alpha" (regularAlphaFiltration baseRegular)
+  shiftedAlpha <- requireRight "shifted regular alpha" (regularAlphaFiltration shiftedRegular)
+  assertEqual
+    "common weight shift preserves the complex"
+    (regularAlphaComplex baseAlpha)
+    (regularAlphaComplex shiftedAlpha)
+  assertEqual
+    "common weight shift subtracts from every birth"
+    (Map.map ((\birth -> birth - 3) . powerAlphaBirthExact) (regularAlphaBirths baseAlpha))
+    (Map.map powerAlphaBirthExact (regularAlphaBirths shiftedAlpha))
diff --git a/test/algebra/Moonlight/Planar/RegularEditSpec.hs b/test/algebra/Moonlight/Planar/RegularEditSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/RegularEditSpec.hs
@@ -0,0 +1,387 @@
+-- | Regular insertion, removal, and reweighting against reconstruction.
+module Moonlight.Planar.RegularEditSpec
+  ( tests
+  ) where
+
+import Data.Foldable ( traverse_ )
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Convex ( ConvexPolygon )
+import Moonlight.Planar.PowerDiagram ( insertRegularSite, removeRegularSite, reweightRegularSites,
+  powerSitePosition, regularEdgeLabels, regularFaceLabels, boundedPowerDiagramFromRegular,
+  emptyRegularTriangulation, regularEdges, regularFaces, regularNeighbours, regularSite,
+  regularSiteCount, regularSiteDisposition, regularSites, regularTriangulation,
+  regularTriangulationReceipt, PowerSite(..), RegularEditError(RegularEditUnknownSites,
+  RegularEditSiteConflict), RegularEditResult(regularEditTransitions, regularEditChangedSites,
+  regularEditTriangulation), RegularSiteDisposition(RegularSiteVisible,
+  RegularSiteCoincidentDominatedBy, RegularSiteCoincidentEquivalentTo, RegularSiteHidden),
+  RegularSiteTransition(RegularSiteDisappeared, RegularSiteAppeared, RegularSiteTransitioned),
+  RegularTriangulation )
+import Moonlight.Planar.PowerFixtures ( admittedSite, admittedWeight, squareDomain )
+import Moonlight.Planar.RegularAlpha ( regularAlphaBirths, regularAlphaComplexAtBirth,
+  regularAlphaFiltration )
+import Moonlight.Planar.Point (Point(..))
+import Support ( assertEqual, requireRight )
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import qualified Data.Vector as Vector
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testHiddenInsertionAndExposure
+    , testRegularInsertionAndRemovalTransitions
+    , testRegularReweightTransitions
+    , testTopologyPreservingEdits
+    , testRegularEditDifferentialLaws
+    , testRegularEditObstructionsAndIdempotence
+    ]
+
+testHiddenInsertionAndExposure :: IO ()
+testHiddenInsertionAndExposure = do
+  zero <- admittedWeight 0
+  hiddenWeight <- admittedWeight (-2)
+  first <- admittedSite "first" (Point 0 0) zero
+  second <- admittedSite "second" (Point 2 0) zero
+  third <- admittedSite "third" (Point 0 2) zero
+  hidden <- admittedSite "hidden" (Point 0.5 0.5) hiddenWeight
+  (initial, _) <-
+    requireRight
+      "regular hidden-insertion base"
+      (regularTriangulation (first :| [second, third]))
+  inserted <-
+    requireRight "insert hidden regular site" (insertRegularSite hidden initial)
+  assertEqual
+    "hidden insertion is successful resident publication"
+    (Just hidden)
+    (regularSite "hidden" (regularEditTriangulation inserted))
+  assertEqual
+    "hidden insertion disposition"
+    [RegularSiteAppeared "hidden" RegularSiteHidden]
+    (Vector.toList (regularEditTransitions inserted))
+  removed <-
+    requireRight
+      "remove face-defining site"
+      (removeRegularSite "first" (regularEditTriangulation inserted))
+  assertEqual
+    "removal re-exposes a hidden resident"
+    (Just RegularSiteVisible)
+    (regularSiteDisposition "hidden" (regularEditTriangulation removed))
+  assertEqual
+    "hidden exposure receipt"
+    [ RegularSiteDisappeared "first" RegularSiteVisible
+    , RegularSiteTransitioned "hidden" RegularSiteHidden RegularSiteVisible
+    ]
+    (Vector.toList (regularEditTransitions removed))
+
+testRegularInsertionAndRemovalTransitions :: IO ()
+testRegularInsertionAndRemovalTransitions = do
+  zero <- admittedWeight 0
+  dominantWeight <- admittedWeight 1
+  resident <- admittedSite "a-resident" (Point 0 0) zero
+  dominant <- admittedSite "z-dominant" (Point 0 0) dominantWeight
+  (initial, _) <-
+    requireRight "regular insertion base" (regularTriangulation (resident :| []))
+  inserted <-
+    requireRight "dominant regular insertion" (insertRegularSite dominant initial)
+  assertEqual
+    "insert changed-site support"
+    (Set.singleton "z-dominant")
+    (regularEditChangedSites inserted)
+  assertEqual
+    "dominant insertion transitions"
+    [ RegularSiteTransitioned
+        "a-resident"
+        RegularSiteVisible
+        (RegularSiteCoincidentDominatedBy "z-dominant")
+    , RegularSiteAppeared "z-dominant" RegularSiteVisible
+    ]
+    (Vector.toList (regularEditTransitions inserted))
+  removed <-
+    requireRight
+      "dominant regular removal"
+      (removeRegularSite "z-dominant" (regularEditTriangulation inserted))
+  assertEqual
+    "remove changed-site support"
+    (Set.singleton "z-dominant")
+    (regularEditChangedSites removed)
+  assertEqual
+    "dominant removal re-exposes resident"
+    [ RegularSiteTransitioned
+        "a-resident"
+        (RegularSiteCoincidentDominatedBy "z-dominant")
+        RegularSiteVisible
+    , RegularSiteDisappeared "z-dominant" RegularSiteVisible
+    ]
+    (Vector.toList (regularEditTransitions removed))
+  assertEqual
+    "insert then remove returns the canonical site value"
+    initial
+    (regularEditTriangulation removed)
+
+testRegularReweightTransitions :: IO ()
+testRegularReweightTransitions = do
+  dominantWeight <- admittedWeight 1
+  subordinateWeight <- admittedWeight 0
+  promotedWeight <- admittedWeight 2
+  first <- admittedSite "a" (Point 0 0) dominantWeight
+  second <- admittedSite "b" (Point 0 0) subordinateWeight
+  (initial, _) <-
+    requireRight "regular reweight base" (regularTriangulation (first :| [second]))
+  reweighted <-
+    requireRight
+      "regular batch reweight"
+      (reweightRegularSites (Map.singleton "b" promotedWeight) initial)
+  assertEqual
+    "reweight changed-site support"
+    (Set.singleton "b")
+    (regularEditChangedSites reweighted)
+  assertEqual
+    "reweight transitions preserve identities"
+    [ RegularSiteTransitioned
+        "a"
+        RegularSiteVisible
+        (RegularSiteCoincidentDominatedBy "b")
+    , RegularSiteTransitioned
+        "b"
+        (RegularSiteCoincidentDominatedBy "a")
+        RegularSiteVisible
+    ]
+    (Vector.toList (regularEditTransitions reweighted))
+  let revised = regularEditTriangulation reweighted
+  assertEqual
+    "reweight preserves the site position"
+    (Just (Point 0 0))
+    (powerSitePosition <$> regularSite "b" revised)
+  assertEqual
+    "reweight replaces only the requested weight"
+    (Just promotedWeight)
+    (powerSiteWeight <$> regularSite "b" revised)
+
+testTopologyPreservingEdits :: IO ()
+testTopologyPreservingEdits = do
+  domain <- squareDomain
+  zero <- admittedWeight 0
+  one <- admittedWeight 1
+  hiddenWeight <- admittedWeight (-2)
+  lowerHiddenWeight <- admittedWeight (-3)
+
+  representative <- admittedSite "a" (Point 0 0) one
+  subordinate <- admittedSite "b" (Point 0 0) zero
+  (coincidentBase, _) <-
+    requireRight "coincident edit base" (regularTriangulation (representative :| []))
+  inserted <-
+    requireRight
+      "canonical coincident insertion"
+      (insertRegularSite subordinate coincidentBase)
+  assertEqual
+    "coincident subordinate insertion transition"
+    [RegularSiteAppeared "b" (RegularSiteCoincidentDominatedBy "a")]
+    (Vector.toList (regularEditTransitions inserted))
+  assertRegularReconstructs domain "canonical coincident insertion" (regularEditTriangulation inserted)
+
+  reweighted <-
+    requireRight
+      "topology-preserving coincident reweight"
+      (reweightRegularSites (Map.singleton "b" one) (regularEditTriangulation inserted))
+  assertEqual
+    "coincident subordinate reweight transition"
+    [ RegularSiteTransitioned
+        "b"
+        (RegularSiteCoincidentDominatedBy "a")
+        (RegularSiteCoincidentEquivalentTo "a")
+    ]
+    (Vector.toList (regularEditTransitions reweighted))
+  assertRegularReconstructs domain "coincident reweight" (regularEditTriangulation reweighted)
+
+  removedSubordinate <-
+    requireRight
+      "topology-preserving coincident removal"
+      (removeRegularSite "b" (regularEditTriangulation reweighted))
+  assertEqual
+    "coincident subordinate removal transition"
+    [RegularSiteDisappeared "b" (RegularSiteCoincidentEquivalentTo "a")]
+    (Vector.toList (regularEditTransitions removedSubordinate))
+  assertRegularReconstructs domain "coincident removal" (regularEditTriangulation removedSubordinate)
+
+  first <- admittedSite "first" (Point 0 0) zero
+  second <- admittedSite "second" (Point 2 0) zero
+  third <- admittedSite "third" (Point 0 2) zero
+  hidden <- admittedSite "hidden" (Point 0.5 0.5) hiddenWeight
+  secondHidden <- admittedSite "hidden-second" (Point 0.75 0.5) hiddenWeight
+  (hiddenBase, _) <-
+    requireRight
+      "hidden edit base"
+      (regularTriangulation (first :| [second, third, hidden, secondHidden]))
+  lowered <-
+    requireRight
+      "topology-preserving hidden reweight"
+      ( reweightRegularSites
+          (Map.fromList [("hidden", lowerHiddenWeight), ("hidden-second", lowerHiddenWeight)])
+          hiddenBase
+      )
+  assertEqual
+    "hidden batch changed-site support"
+    (Set.fromList ["hidden", "hidden-second"])
+    (regularEditChangedSites lowered)
+  assertEqual "hidden downward reweight has no visibility transition" [] (Vector.toList (regularEditTransitions lowered))
+  assertRegularReconstructs domain "hidden downward reweight" (regularEditTriangulation lowered)
+
+  removedHidden <-
+    requireRight
+      "topology-preserving hidden removal"
+      (removeRegularSite "hidden" (regularEditTriangulation lowered))
+  assertEqual
+    "hidden removal transition"
+    [RegularSiteDisappeared "hidden" RegularSiteHidden]
+    (Vector.toList (regularEditTransitions removedHidden))
+  assertRegularReconstructs domain "hidden removal" (regularEditTriangulation removedHidden)
+
+assertRegularReconstructs
+  :: (Ord label, Show label)
+  => ConvexPolygon
+  -> String
+  -> RegularTriangulation label
+  -> IO ()
+assertRegularReconstructs domain name edited =
+  case NonEmpty.nonEmpty (regularSites edited) of
+    Nothing -> assertEqual (name <> " remains empty") 0 (regularSiteCount edited)
+    Just sites -> do
+      (rebuilt, _) <-
+        requireRight (name <> " reconstruction") (regularTriangulation sites)
+      let labels = fmap powerSiteLabel (NonEmpty.toList sites)
+      assertEqual
+        (name <> " receipt")
+        (regularTriangulationReceipt rebuilt)
+        (regularTriangulationReceipt edited)
+      assertEqual
+        (name <> " face labels")
+        (fmap regularFaceLabels (regularFaces rebuilt))
+        (fmap regularFaceLabels (regularFaces edited))
+      assertEqual (name <> " faces") (regularFaces rebuilt) (regularFaces edited)
+      assertEqual
+        (name <> " edge labels")
+        (fmap regularEdgeLabels (regularEdges rebuilt))
+        (fmap regularEdgeLabels (regularEdges edited))
+      assertEqual (name <> " edges") (regularEdges rebuilt) (regularEdges edited)
+      traverse_
+        (\label -> do
+           assertEqual
+             (name <> " disposition " <> show label)
+             (regularSiteDisposition label rebuilt)
+             (regularSiteDisposition label edited)
+           assertEqual
+             (name <> " neighbours " <> show label)
+             (regularNeighbours label rebuilt)
+             (regularNeighbours label edited))
+        labels
+      rebuiltDiagram <-
+        requireRight (name <> " rebuilt clipping") (boundedPowerDiagramFromRegular domain rebuilt)
+      editedDiagram <-
+        requireRight (name <> " edited clipping") (boundedPowerDiagramFromRegular domain edited)
+      assertEqual (name <> " bounded cells") rebuiltDiagram editedDiagram
+
+testRegularEditDifferentialLaws :: IO ()
+testRegularEditDifferentialLaws = do
+  domain <- squareDomain
+  sites <- traverse prepareDifferentialSite [0 .. 47]
+  nonEmptySites <-
+    maybe (fail "differential regular fixture is empty") pure (NonEmpty.nonEmpty sites)
+  baseSites <-
+    maybe (fail "differential insertion fixture is empty") pure
+      (NonEmpty.nonEmpty (NonEmpty.init nonEmptySites))
+  (base, _) <- requireRight "differential insertion base" (regularTriangulation baseSites)
+  inserted <-
+    requireRight
+      "differential local insertion"
+      (insertRegularSite (NonEmpty.last nonEmptySites) base)
+  assertRegularAlphaClosed
+    "differential weighted alpha"
+    (regularEditTriangulation inserted)
+  assertRegularReconstructs
+    domain
+    "differential insertion"
+    (regularEditTriangulation inserted)
+  removed <-
+    requireRight
+      "differential local removal"
+      (removeRegularSite "s23" (regularEditTriangulation inserted))
+  assertRegularReconstructs
+    domain
+    "differential removal"
+    (regularEditTriangulation removed)
+  raised <- admittedWeight 2
+  reweighted <-
+    requireRight
+      "differential local reweight"
+      (reweightRegularSites (Map.singleton "s17" raised) (regularEditTriangulation removed))
+  assertRegularReconstructs
+    domain
+    "differential reweight"
+    (regularEditTriangulation reweighted)
+
+assertRegularAlphaClosed
+  :: (Ord label, Show label)
+  => String
+  -> RegularTriangulation label
+  -> IO ()
+assertRegularAlphaClosed name regular = do
+  filtration <- requireRight name (regularAlphaFiltration regular)
+  traverse_
+    (requireRight (name <> " sublevel") . (`regularAlphaComplexAtBirth` filtration))
+    (Map.elems (regularAlphaBirths filtration))
+
+prepareDifferentialSite :: Int -> IO (PowerSite String)
+prepareDifferentialSite label = do
+  let coordinateX = fromIntegral ((label * 37 + 11) `mod` 97) / 10
+      coordinateY = fromIntegral ((label * 61 + 7) `mod` 89) / 10
+      weightValue = fromIntegral ((label * 17) `mod` 13 - 6) / 64
+  weight <- admittedWeight weightValue
+  admittedSite ("s" <> show label) (Point coordinateX coordinateY) weight
+
+testRegularEditObstructionsAndIdempotence :: IO ()
+testRegularEditObstructionsAndIdempotence = do
+  zero <- admittedWeight 0
+  one <- admittedWeight 1
+  site <- admittedSite "site" (Point 0 0) zero
+  conflicting <- admittedSite "site" (Point 0 0) one
+  inserted <-
+    requireRight
+      "insert into empty regular topology"
+      (insertRegularSite site emptyRegularTriangulation)
+  repeated <-
+    requireRight
+      "repeat identical regular insertion"
+      (insertRegularSite site (regularEditTriangulation inserted))
+  assertEqual
+    "identical insertion changes no site"
+    Set.empty
+    (regularEditChangedSites repeated)
+  assertEqual "identical insertion has no transitions" [] (Vector.toList (regularEditTransitions repeated))
+  case insertRegularSite conflicting (regularEditTriangulation inserted) of
+    Left (RegularEditSiteConflict "site" resident requested) -> do
+      assertEqual "conflict retains resident" site resident
+      assertEqual "conflict retains requested site" conflicting requested
+    other -> fail ("expected regular edit site conflict, got " <> show other)
+  case reweightRegularSites (Map.singleton "missing" zero) (regularEditTriangulation inserted) of
+    Left (RegularEditUnknownSites ("missing" :| [])) -> pure ()
+    other -> fail ("expected unknown reweight site, got " <> show other)
+  absent <-
+    requireRight
+      "remove absent regular site"
+      (removeRegularSite "missing" (regularEditTriangulation inserted))
+  assertEqual
+    "absent removal changes no site"
+    Set.empty
+    (regularEditChangedSites absent)
+  assertEqual "absent removal has no transitions" [] (Vector.toList (regularEditTransitions absent))
+  removed <-
+    requireRight
+      "remove final regular site"
+      (removeRegularSite "site" (regularEditTriangulation inserted))
+  assertEqual "final removal returns empty" 0 (regularSiteCount (regularEditTriangulation removed))
+  assertEqual
+    "final removal transition"
+    [RegularSiteDisappeared "site" RegularSiteVisible]
+    (Vector.toList (regularEditTransitions removed))
diff --git a/test/algebra/Moonlight/Planar/RegularTopologySpec.hs b/test/algebra/Moonlight/Planar/RegularTopologySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/RegularTopologySpec.hs
@@ -0,0 +1,282 @@
+-- | Regular topology, dual rays, and visible/hidden site classification laws.
+module Moonlight.Planar.RegularTopologySpec
+  ( tests
+  ) where
+
+import Control.Monad ( unless )
+import Data.Foldable ( traverse_ )
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Exact ( ExactPoint, ExactVector(..), exactRayDirection, exactRayOrigin,
+  exactSegmentEndpoints, translateExactPoint )
+import Moonlight.Planar.PowerDiagram ( regularEdgeDual, regularEdgeLabels, regularFaceDualPoint,
+  regularFaceLabels, boundedPowerDiagram, powerCellDisposition, emptyRegularTriangulation,
+  regularEdges, regularFaces, regularNeighbours, regularSite, regularSiteCount,
+  regularSiteDisposition, regularSites, regularTriangulation, PowerDualEdge(UnboundedPowerDual,
+  BoundedPowerDual, CollapsedPowerDual), PowerCellDisposition(EmptyPowerCell,
+  LowerDimensionalPowerCell), PowerDiagramReceipt(powerDiagramRegularEdges, powerDiagramOracleCells,
+  powerDiagramMaximumCellConstraints), PowerSite(powerSiteLabel), RegularEdge,
+  RegularSiteDisposition(RegularSiteHidden, RegularSiteLowerDimensional, RegularSiteVisible),
+  RegularTriangulationReceipt(regularTriangulationInputSites, regularTriangulationEdges,
+  regularTriangulationVisibleSites, regularTriangulationFaces) )
+import Moonlight.Planar.PowerFixtures ( admittedSite, admittedWeight, admittedWeightedSite,
+  squareDomain, dispositionTag, powerDistance )
+import Moonlight.Planar.Point (Point(..))
+import Support ( assertEqual, requireRight )
+import qualified Data.List as List
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testRegularTriangleDualRays
+    , testRegularBoundedDualSegment
+    , testRegularCoplanarUpperFacet
+    , testRegularCollinearClassification
+    , testRegularLowerDimensionalAndHiddenSites
+    , testRegularSiteOwnership
+    ]
+
+testRegularTriangleDualRays :: IO ()
+testRegularTriangleDualRays = do
+  zero <- admittedWeight 0
+  firstSite <- admittedSite "first" (Point 0 0) zero
+  secondSite <- admittedSite "second" (Point 2 0) zero
+  thirdSite <- admittedSite "third" (Point 0 2) zero
+  let sites = firstSite :| [secondSite, thirdSite]
+  (regular, receipt) <-
+    requireRight "three-site regular topology" (regularTriangulation sites)
+  assertEqual "three visible regular sites" 3 (regularTriangulationVisibleSites receipt)
+  assertEqual "one regular face" 1 (length (regularFaces regular))
+  assertEqual "three regular boundary edges" 3 (length (regularEdges regular))
+  dualPoint <-
+    case regularFaces regular of
+      [face] -> pure (regularFaceDualPoint face)
+      faces -> fail ("three-site topology expected one face, got " <> show (length faces))
+  traverse_ (assertBoundaryDualRay sites dualPoint) (regularEdges regular)
+  traverse_
+    (\site ->
+       let label = powerSiteLabel site
+        in do
+          assertEqual
+            ("visible regular disposition for " <> label)
+            (Just RegularSiteVisible)
+            (regularSiteDisposition label regular)
+          assertEqual
+            ("two regular neighbours for " <> label)
+            2
+            (Set.size (regularNeighbours label regular)))
+    sites
+
+testRegularBoundedDualSegment :: IO ()
+testRegularBoundedDualSegment = do
+  zero <- admittedWeight 0
+  sites <-
+    traverse
+      (\(label, point) -> admittedSite label point zero)
+      ( ("south-west", Point 0 0)
+          :| [ ("south-east", Point 4 0)
+             , ("north-east", Point 3 3)
+             , ("north-west", Point 0 4)
+             ]
+      )
+  (regular, receipt) <-
+    requireRight "four-site regular topology" (regularTriangulation sites)
+  assertEqual
+    "regular planar Euler equation"
+    1
+    ( regularTriangulationVisibleSites receipt
+        - regularTriangulationEdges receipt
+        + regularTriangulationFaces receipt
+    )
+  let faceDuals = Set.fromList (fmap regularFaceDualPoint (regularFaces regular))
+      bounded =
+        [ segment
+        | edge <- regularEdges regular
+        , BoundedPowerDual segment <- [regularEdgeDual edge]
+        ]
+  case bounded of
+    [segment] -> do
+      let (firstEndpoint, secondEndpoint) = exactSegmentEndpoints segment
+      unless
+        (Set.member firstEndpoint faceDuals && Set.member secondEndpoint faceDuals)
+        (fail "bounded regular dual does not join its two incident face duals")
+      assertEqual "bounded regular dual has canonical ascending endpoints" LT
+        (compare firstEndpoint secondEndpoint)
+    segments ->
+      fail ("four-site topology expected one bounded dual, got " <> show (length segments))
+
+testRegularCoplanarUpperFacet :: IO ()
+testRegularCoplanarUpperFacet = do
+  sites <-
+    traverse
+      admittedWeightedSite
+      ( ("a-bottom", Point 0.25 0.25, -0.875)
+          :| [ ("b-south-west", Point 0 0, 0)
+             , ("c-south-east", Point 1 0, 1)
+             , ("d-center", Point 0.5 0.5, 0.5)
+             , ("e-north-east", Point 1 1, 2)
+             , ("f-north-west", Point 0 1, 1)
+             ]
+      )
+  (regular, receipt) <-
+    requireRight "coplanar upper regular facet" (regularTriangulation sites)
+  traverse_
+    (\label ->
+       assertEqual
+         ("upper-facet vertex remains visible: " <> label)
+         (Just RegularSiteVisible)
+         (regularSiteDisposition label regular))
+    ["b-south-west", "c-south-east", "e-north-east", "f-north-west"]
+  assertEqual
+    "upper-facet interior generator remains lower-dimensional"
+    (Just RegularSiteLowerDimensional)
+    (regularSiteDisposition "d-center" regular)
+  assertEqual
+    "strictly lower lifted generator remains hidden"
+    (Just RegularSiteHidden)
+    (regularSiteDisposition "a-bottom" regular)
+  assertEqual "coplanar upper facet has four visible vertices" 4 (regularTriangulationVisibleSites receipt)
+  assertEqual "coplanar upper facet receives one deterministic diagonal" 2 (regularTriangulationFaces receipt)
+  let collapsed =
+        [ point
+        | edge <- regularEdges regular
+        , CollapsedPowerDual point <- [regularEdgeDual edge]
+        ]
+      faceDuals = Set.fromList (fmap regularFaceDualPoint (regularFaces regular))
+  assertEqual "coplanar diagonal is published as one collapsed dual" 1 (length collapsed)
+  assertEqual "collapsed dual coincides with every incident face dual" faceDuals (Set.fromList collapsed)
+  traverse_
+    (\face ->
+       let (firstLabel, secondLabel, thirdLabel) = regularFaceLabels face
+        in assertEqual "regular face rotation begins at least label" firstLabel
+             (min firstLabel (min secondLabel thirdLabel)))
+    (regularFaces regular)
+
+testRegularLowerDimensionalAndHiddenSites :: IO ()
+testRegularLowerDimensionalAndHiddenSites = do
+  domain <- squareDomain
+  zero <- admittedWeight 0
+  lowerWeight <- admittedWeight (-1.5)
+  hiddenWeight <- admittedWeight (-2)
+  firstSite <- admittedSite "first" (Point 0 0) zero
+  secondSite <- admittedSite "second" (Point 2 0) zero
+  thirdSite <- admittedSite "third" (Point 0 2) zero
+  lowerSite <- admittedSite "center" (Point 0.5 0.5) lowerWeight
+  hiddenSite <- admittedSite "center" (Point 0.5 0.5) hiddenWeight
+  let lowerSites = firstSite :| [secondSite, thirdSite, lowerSite]
+      hiddenSites = firstSite :| [secondSite, thirdSite, hiddenSite]
+  (lowerRegular, _) <-
+    requireRight "coplanar regular topology" (regularTriangulation lowerSites)
+  assertEqual
+    "coplanar interior generator remains lower-dimensional"
+    (Just RegularSiteLowerDimensional)
+    (regularSiteDisposition "center" lowerRegular)
+  (lowerDiagram, lowerReceipt) <-
+    requireRight "bounded lower-dimensional power cell" (boundedPowerDiagram domain lowerSites)
+  case powerCellDisposition "center" lowerDiagram of
+    Just (LowerDimensionalPowerCell _) -> pure ()
+    other -> fail ("expected lower-dimensional center cell, got " <> dispositionTag other)
+  assertEqual "one lower-dimensional oracle cell" 1 (powerDiagramOracleCells lowerReceipt)
+  (hiddenRegular, _) <-
+    requireRight "hidden regular topology" (regularTriangulation hiddenSites)
+  assertEqual
+    "strictly interior lifted generator is hidden"
+    (Just RegularSiteHidden)
+    (regularSiteDisposition "center" hiddenRegular)
+  (hiddenDiagram, hiddenReceipt) <-
+    requireRight "bounded hidden power cell" (boundedPowerDiagram domain hiddenSites)
+  assertEqual "hidden generator has empty cell" (Just EmptyPowerCell) (powerCellDisposition "center" hiddenDiagram)
+  assertEqual "hidden generator needs no HPI oracle" 0 (powerDiagramOracleCells hiddenReceipt)
+  unless
+    ( powerDiagramMaximumCellConstraints hiddenReceipt <= powerDiagramRegularEdges hiddenReceipt
+    )
+    (fail "power construction retained more axes than the regular graph")
+
+testRegularSiteOwnership :: IO ()
+testRegularSiteOwnership = do
+  zero <- admittedWeight 0
+  sites <-
+    traverse
+      (\(label, point) -> admittedSite label point zero)
+      ( ("c", Point 0 2)
+          :| [("a", Point 0 0), ("b", Point 2 0)]
+      )
+  (regular, receipt) <-
+    requireRight "site-owning regular topology" (regularTriangulation sites)
+  assertEqual "regular site count" 3 (regularSiteCount regular)
+  assertEqual
+    "regular sites are the canonical ascending section"
+    ["a", "b", "c"]
+    (fmap powerSiteLabel (regularSites regular))
+  traverse_
+    (\site ->
+       assertEqual
+         ("regular site lookup for " <> powerSiteLabel site)
+         (Just site)
+         (regularSite (powerSiteLabel site) regular))
+    sites
+  case NonEmpty.nonEmpty (regularSites regular) of
+    Nothing -> fail "nonempty regular topology lost its canonical site section"
+    Just retainedSites -> do
+      (reconstructed, _) <-
+        requireRight "regular reconstruction from owned sites" (regularTriangulation retainedSites)
+      assertEqual "owned sites reconstruct the same semantic value" regular reconstructed
+  assertEqual "receipt counts every owned site" 3 (regularTriangulationInputSites receipt)
+  assertEqual "empty regular site count" 0 (regularSiteCount emptyRegularTriangulation)
+  assertEqual
+    "empty regular site section"
+    ([] :: [PowerSite String])
+    (regularSites emptyRegularTriangulation)
+
+testRegularCollinearClassification :: IO ()
+testRegularCollinearClassification = do
+  sites <- traverse prepareCollinearSite (0 :| [1 .. 8])
+  (regular, _) <-
+    requireRight "collinear regular topology" (regularTriangulation sites)
+  traverse_
+    (\index ->
+       assertEqual
+         ("collinear disposition for " <> show index)
+         (Just (if even index then RegularSiteVisible else RegularSiteHidden))
+         (regularSiteDisposition (show index) regular))
+    ([0 .. 8] :: [Int])
+ where
+  prepareCollinearSite :: Int -> IO (PowerSite String)
+  prepareCollinearSite index = do
+    weight <- admittedWeight (if odd index then -(2 / 256) else 0)
+    admittedSite (show index) (Point (fromIntegral index / 16) 0) weight
+
+assertBoundaryDualRay
+  :: NonEmpty (PowerSite String)
+  -> ExactPoint
+  -> RegularEdge String
+  -> IO ()
+assertBoundaryDualRay sites expectedOrigin edge =
+  case regularEdgeDual edge of
+    UnboundedPowerDual ray -> do
+      assertEqual "regular ray starts at incident face dual" expectedOrigin (exactRayOrigin ray)
+      let ExactVector directionX directionY = exactRayDirection ray
+      unless (directionX /= 0 || directionY /= 0) $
+        fail ("regular edge has a zero dual-ray direction: " <> show (regularEdgeLabels edge))
+      let sample = translateExactPoint expectedOrigin (exactRayDirection ray)
+          (firstLabel, secondLabel) = regularEdgeLabels edge
+      firstSite <- requireSite firstLabel sites
+      secondSite <- requireSite secondLabel sites
+      firstDistance <- powerDistance sample firstSite
+      secondDistance <- powerDistance sample secondSite
+      assertEqual "regular ray remains on its radical axis" firstDistance secondDistance
+      traverse_
+        (\competitor -> do
+           competitorDistance <- powerDistance sample competitor
+           unless (firstDistance <= competitorDistance) $
+             fail ("regular ray points outside the common winning cone: " <> show (firstLabel, secondLabel)))
+        sites
+    other -> fail ("regular triangle boundary expected a ray, got " <> show other)
+
+requireSite :: Eq label => label -> NonEmpty (PowerSite label) -> IO (PowerSite label)
+requireSite label sites =
+  case List.find ((== label) . powerSiteLabel) (NonEmpty.toList sites) of
+    Nothing -> fail "regular topology references a missing source site"
+    Just site -> pure site
diff --git a/test/algebra/Moonlight/Planar/ScheduleAgreementSpec.hs b/test/algebra/Moonlight/Planar/ScheduleAgreementSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/ScheduleAgreementSpec.hs
@@ -0,0 +1,74 @@
+-- | The schedule-agreement slice: the seam merge publishes what the reference
+-- rebuild publishes. It travels and dies with the schedule it names.
+module Moonlight.Planar.ScheduleAgreementSpec (tests) where
+
+import Control.Monad (forM_, when)
+import Data.IORef (modifyIORef', newIORef, readIORef)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.SetAlgebra (union)
+import Moonlight.Planar.Point (Point (Point))
+import Moonlight.Planar.AlgebraFixtures
+  ( assertMesh
+  , meshOf
+  , operands
+  , separatedOperands
+  , siteList
+  )
+import Moonlight.Planar.Internal.Join.Seam (executeSeam, planSeam)
+import Support (requireRight)
+
+tests :: IO ()
+tests = do
+  testSeamAgreesWithReference
+  putStrLn "schedule agreement: ok"
+
+-- | The seam schedule and the reference schedule are the same function.
+--
+-- A join has one meaning and several internal schedules, so this is the law
+-- that lets a second one exist at all: for every input where the seam applies,
+-- it must publish the value the rebuild publishes — not an isomorphic mesh, the
+-- same value, once both are canonically numbered.
+--
+-- The guard matters more than the equality. 'executeSeam' can only be reached
+-- with the opaque proof returned by 'planSeam'; the run fails if too few cases
+-- obtain that proof, so the reference cannot quietly be compared with itself.
+testSeamAgreesWithReference :: IO ()
+testSeamAgreesWithReference = do
+  separable <- separatedOperands
+  overlapping <- operands
+  taken <- newIORef (0 :: Int)
+  forM_ separable $ \((leftName, left), (rightName, right)) -> do
+    let label = "seam " <> leftName <> " / " <> rightName
+    seamPlan <- case planSeam left right of
+      Nothing ->
+        fail (label <> ": the operands are separated but planSeam refused them")
+      Just admitted -> do
+        modifyIORef' taken (+ 1)
+        pure admitted
+    rawRebuild <- meshOf label [Point x y | (x, y) <- siteList left <> siteList right]
+    rebuilt <- requireRight (label <> ", canonical rebuild") (canonicalize rawRebuild)
+    seamed <- requireRight (label <> ", direct seam") (executeSeam seamPlan left right)
+    canonicalSeam <- requireRight (label <> ", canonical seam") (canonicalize seamed)
+    assertMesh (label <> ", against a rebuild") rebuilt canonicalSeam
+    joined <- requireRight (label <> ", through union") (union left right)
+    canonicalJoined <- requireRight (label <> ", canonical union") (canonicalize joined)
+    assertMesh (label <> ", through the operator") rebuilt canonicalJoined
+  forM_ [(l, r) | l <- overlapping, r <- overlapping] $ \((leftName, left), (rightName, right)) ->
+    case planSeam left right of
+      Nothing -> pure ()
+      Just seamPlan -> do
+        rawRebuild <-
+          meshOf
+            ("seam admission " <> leftName <> " / " <> rightName)
+            [Point x y | (x, y) <- siteList left <> siteList right]
+        rebuilt <- requireRight "seam admission canonical rebuild" (canonicalize rawRebuild)
+        seamed <- requireRight "seam admission" (executeSeam seamPlan left right)
+        canonicalSeam <- requireRight "seam admission canonical seam" (canonicalize seamed)
+        assertMesh
+          ("seam admission " <> leftName <> " / " <> rightName)
+          rebuilt
+          canonicalSeam
+        modifyIORef' taken (+ 1)
+  count <- readIORef taken
+  when (count < 3) $
+    fail ("seam agreement: only " <> show count <> " cases entered the seam, so the law asserted nothing")
diff --git a/test/algebra/Moonlight/Planar/ValuationSpec.hs b/test/algebra/Moonlight/Planar/ValuationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/algebra/Moonlight/Planar/ValuationSpec.hs
@@ -0,0 +1,450 @@
+-- | Exact intrinsic-volume fixtures and common-subdivision inclusion-exclusion.
+module Moonlight.Planar.ValuationSpec (tests) where
+
+import Data.Foldable (traverse_)
+import Data.Ratio ((%), numerator, denominator)
+import Moonlight.Planar.Exact (ExactPoint, exactPoint, exactRationalNumerator, exactRationalDenominator)
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactRational
+  , exactRational
+  )
+import Moonlight.Planar.Overlay
+  ( overlayClosedIntersection
+  , overlayClosedUnion
+  , overlayLayers
+  , overlaySelectedRegion
+  )
+import Moonlight.Planar.Region
+  ( PlanarRegion
+  , emptyPlanarRegion
+  , planarRegion
+  , planarRegionComponents
+  )
+import Moonlight.Planar.Valuation
+  ( CertifiedInterval (..)
+  , ExactPlanarMoments
+  , ExactLengthTerm
+  , PlanarValuations
+  , ValuationError (ValuationCellSetNotPureRegion)
+  , cellSetPerimeter
+  , cellValuations
+  , exactAreaValue
+  , exactLengthBounds
+  , exactLengthExpression
+  , exactLengthTerms
+  , exactPlanarFirstX
+  , exactPlanarFirstY
+  , exactPlanarMeasure
+  , exactPlanarSecondXX
+  , exactPlanarSecondXY
+  , exactPlanarSecondYY
+  , eulerCharacteristicValue
+  , lengthCoefficient
+  , polygonComponentArea
+  , polygonComponentMoments
+  , orientedBoundaryMoments
+  , regionPerimeter
+  , regionValuations
+  , scaleExactPlanarMoments
+  , lengthRadicand
+  , valuationArea
+  , valuationEuler
+  , valuationIntrinsic1
+  )
+import Moonlight.Planar.OverlayFixtures (singletonLayer)
+import Support
+  ( annulusRegion, assertEqual, polygonComponentOf, polygonRegion
+  , rectangleComponent, rectangleRegion, requireRight
+  )
+
+tests :: IO ()
+tests = do
+  testRegionGoldenValues
+  testExactPlanarMoments
+  testOrientedMomentRationalOracle
+  testClosedCellInclusionExclusion
+  testDimensionalCellFixtures
+  testMetricInvariance
+  testRadicalNormalForm
+  putStrLn "valuation: ok"
+
+testExactPlanarMoments :: IO ()
+testExactPlanarMoments = do
+  unit <- rectangleComponent 0 0 1 1
+  oneHalfValue <- exactValue 1 2
+  oneThird <- exactValue 1 3
+  oneQuarter <- exactValue 1 4
+  let unitMoments = polygonComponentMoments unit
+  assertEqual
+    "area is the measure projection"
+    (exactPlanarMeasure unitMoments)
+    (exactAreaValue (polygonComponentArea unit))
+  assertEqual
+    "unit-square moments"
+    (1, oneHalfValue, oneHalfValue, oneThird, oneQuarter, oneThird)
+    (momentDigest unitMoments)
+
+  translated <- rectangleComponent 2 (-1) 4 2
+  assertEqual
+    "translated-rectangle moments"
+    (6, 18, 3, 56, 9, 6)
+    (momentDigest (polygonComponentMoments translated))
+
+  annulus <- annulusRegion (0, 0, 3, 3) (1, 1, 2, 2)
+  seventyFourThirds <- exactValue 74 3
+  let annulusMoments = foldMap polygonComponentMoments (planarRegionComponents annulus)
+  assertEqual
+    "annulus moments respect clockwise holes"
+    (8, 12, 12, seventyFourThirds, 18, seventyFourThirds)
+    (momentDigest annulusMoments)
+  assertEqual
+    "moment monoid and scalar action"
+    (momentDigest (unitMoments <> unitMoments))
+    (momentDigest (scaleExactPlanarMoments 2 unitMoments))
+
+momentDigest
+  :: ExactPlanarMoments
+  -> (ExactRational, ExactRational, ExactRational, ExactRational, ExactRational, ExactRational)
+momentDigest moments =
+  ( exactPlanarMeasure moments
+  , exactPlanarFirstX moments
+  , exactPlanarFirstY moments
+  , exactPlanarSecondXX moments
+  , exactPlanarSecondXY moments
+  , exactPlanarSecondYY moments
+  )
+
+exactValue :: Integer -> Integer -> IO ExactRational
+exactValue numeratorValue denominatorValue =
+  requireRight "exact valuation fixture" (exactRational numeratorValue denominatorValue)
+
+testRegionGoldenValues :: IO ()
+testRegionGoldenValues = do
+  assertRegionValuations "empty" emptyPlanarRegion 0 0 []
+
+  unit <- rectangleRegion 0 0 1 1
+  assertRegionValuations "unit square" unit 1 1 [(2, 1)]
+  unitPerimeter <- requireRight "unit-square perimeter" (regionPerimeter unit)
+  assertLength "unit-square perimeter" [(4, 1)] (exactLengthTerms (exactLengthExpression unitPerimeter))
+  assertContains "unit-square perimeter bounds" 4 (exactLengthBounds unitPerimeter)
+
+  -- Outer 3×3 square: four edges of length √9 = 3, half-perimeter 6. Hole:
+  -- half-perimeter 2. The unnormalized shape 2·√1 + 2·√9 folds to 8.
+  annulus <- annulusRegion (0, 0, 3, 3) (1, 1, 2, 2)
+  assertRegionValuations "annulus" annulus 0 8 [(8, 1)]
+
+  lowerLeft <- rectangleComponent 0 0 1 1
+  upperRight <- rectangleComponent 1 1 2 2
+  cornerTouch <- requireRight "corner-touch region" (planarRegion [lowerLeft, upperRight])
+  assertRegionValuations "corner-touch squares" cornerTouch 1 2 [(4, 1)]
+
+  right <- rectangleComponent 1 0 2 1
+  edgeTouch <- requireRight "edge-touch region" (planarRegion [lowerLeft, right])
+  assertRegionValuations "edge-sharing squares" edgeTouch 1 2 [(3, 1)]
+
+  -- The double cell's bottom edge covers the unit cell's top edge from a
+  -- shared corner: collinear, positive overlap, shared endpoint. The contact
+  -- is one segment (V − E = 2 − 1), so Euler 1 + 1 − 1. Area 1 + 2. Intrinsic
+  -- one-volume: half-perimeters 2 + 3 minus the shared unit length, which is
+  -- 2·√1 + (1·√1 + 1·√4) − 1·√1 before normalization and 4 after.
+  doubleAbove <- rectangleComponent 0 1 2 2
+  overlapTouch <-
+    requireRight "shared-endpoint overlap region" (planarRegion [lowerLeft, doubleAbove])
+  assertRegionValuations "shared-endpoint collinear overlap" overlapTouch 1 3 [(4, 1)]
+
+testClosedCellInclusionExclusion :: IO ()
+testClosedCellInclusionExclusion = do
+  leftRegion <- rectangleRegion 0 0 1 1
+  rightRegion <- rectangleRegion 1 0 2 1
+  leftLayer <- singletonLayer False True leftRegion
+  rightLayer <- singletonLayer False True rightRegion
+  overlay <- requireRight "edge-sharing valuation overlay" (overlayLayers leftLayer rightLayer)
+  left <- requireRight "left closed cells" (overlayClosedUnion id (const False) overlay)
+  right <- requireRight "right closed cells" (overlayClosedUnion (const False) id overlay)
+  union <- requireRight "union closed cells" (overlayClosedUnion id id overlay)
+  intersection <-
+    requireRight
+      "intersection closed cells"
+      (overlayClosedIntersection id id overlay)
+  leftValues <- requireRight "left valuations" (cellValuations left)
+  rightValues <- requireRight "right valuations" (cellValuations right)
+  unionValues <- requireRight "union valuations" (cellValuations union)
+  sharedEdgeValues <- requireRight "intersection valuations" (cellValuations intersection)
+  assertEqual
+    "Euler inclusion-exclusion retains the shared edge"
+    (1, 1, 1, 1)
+    ( eulerCharacteristicValue (valuationEuler leftValues)
+    , eulerCharacteristicValue (valuationEuler rightValues)
+    , eulerCharacteristicValue (valuationEuler unionValues)
+    , eulerCharacteristicValue (valuationEuler sharedEdgeValues)
+    )
+  assertEqual
+    "area inclusion-exclusion retains zero-dimensional measure"
+    ( 1
+    , 1
+    , 2
+    , 0
+    )
+    ( areaExact leftValues
+    , areaExact rightValues
+    , areaExact unionValues
+    , areaExact sharedEdgeValues
+    )
+  assertLength
+    "left intrinsic one-volume"
+    [(2, 1)]
+    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 leftValues)))
+  assertLength
+    "right intrinsic one-volume"
+    [(2, 1)]
+    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 rightValues)))
+  assertLength
+    "union intrinsic one-volume"
+    [(3, 1)]
+    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 unionValues)))
+  assertLength
+    "shared-edge intrinsic one-volume"
+    [(1, 1)]
+    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 sharedEdgeValues)))
+  case cellSetPerimeter intersection of
+    Left ValuationCellSetNotPureRegion -> pure ()
+    other ->
+      fail
+        ( "an isolated selected edge was accepted as a perimeter: "
+            <> show other
+        )
+  published <-
+    requireRight
+      "published edge-sharing union"
+      (overlaySelectedRegion (uncurry (||)) overlay)
+  publishedValues <- requireRight "published union valuations" (regionValuations published)
+  assertEqual
+    "cell and published region valuations agree"
+    (valuationDigest unionValues)
+    (valuationDigest publishedValues)
+
+testDimensionalCellFixtures :: IO ()
+testDimensionalCellFixtures = do
+  disjoint <- intersectionValues (0, 0, 1, 1) (2, 0, 3, 1)
+  assertEqual
+    "empty intersection valuations"
+    (0, 0, [])
+    (exactDigest disjoint)
+
+  point <- intersectionValues (0, 0, 1, 1) (1, 1, 2, 2)
+  assertEqual
+    "point intersection valuations"
+    (1, 0, [])
+    (exactDigest point)
+
+  -- The intersection is the 1×2 rectangle (1,0)–(2,2): half-perimeter 3,
+  -- which is 1·√1 + 1·√4 before normalization.
+  area <- intersectionValues (0, 0, 2, 2) (1, 0, 3, 2)
+  assertEqual
+    "two-dimensional intersection valuations"
+    (1, 2, [(3, 1)])
+    (exactDigest area)
+
+testMetricInvariance :: IO ()
+testMetricInvariance = do
+  original <- polygonRegion [(0, 0), (1, 0), (0, 1)]
+  translated <- polygonRegion [(5, -3), (6, -3), (5, -2)]
+  quarterTurned <- polygonRegion [(0, 0), (0, 1), (-1, 0)]
+  originalValues <- requireRight "original triangle valuations" (regionValuations original)
+  translatedValues <- requireRight "translated triangle valuations" (regionValuations translated)
+  quarterTurnedValues <- requireRight "quarter-turned triangle valuations" (regionValuations quarterTurned)
+  assertEqual "translation invariance" (exactDigest originalValues) (exactDigest translatedValues)
+  assertEqual "quarter-turn invariance" (exactDigest originalValues) (exactDigest quarterTurnedValues)
+  perimeter <- requireRight "irrational triangle perimeter" (regionPerimeter original)
+  assertContains
+    "certified radical perimeter"
+    (2 + sqrt 2)
+    (exactLengthBounds perimeter)
+
+testRadicalNormalForm :: IO ()
+testRadicalNormalForm = do
+  -- Diamonds with edge vectors (1, 1) and (2, 2): half-perimeters 2·√2 and
+  -- 2·√8 = 4·√2. The square factor of the trial prime 2 folds, and the two
+  -- lengths are one term. Areas 2 + 8.
+  small <- polygonComponentOf [(0, 1), (1, 0), (2, 1), (1, 2)]
+  large <- polygonComponentOf [(4, 2), (6, 0), (8, 2), (6, 4)]
+  diamonds <- requireRight "diamond region" (planarRegion [small, large])
+  assertRegionValuations "diamonds share one radical" diamonds 2 10 [(6, 2)]
+
+  -- Rhombi with edge vectors (536, 201) and (8, 3): squared lengths
+  -- 327697 = 67²·73 and 73. The prime 67 is beyond the trial primes and
+  -- 327697 is not a square, so only the class test (327697·73 = 4891²) can
+  -- fold 2·√327697 into 134·√73; with the second rhombus's 2·√73 the sum is
+  -- 136·√73. Areas 327697 + 73.
+  wide <- polygonComponentOf [(0, 0), (536, 201), (335, 737), (-201, 536)]
+  narrow <- polygonComponentOf [(1000, 0), (1008, 3), (1005, 11), (997, 8)]
+  rhombi <- requireRight "rhombus region" (planarRegion [wide, narrow])
+  assertRegionValuations
+    "radical classes merge beyond the trial primes"
+    rhombi
+    2
+    327770
+    [(136, 73)]
+
+intersectionValues
+  :: (Integer, Integer, Integer, Integer)
+  -> (Integer, Integer, Integer, Integer)
+  -> IO PlanarValuations
+intersectionValues leftBounds rightBounds = do
+  leftRegion <- uncurryRectangle leftBounds
+  rightRegion <- uncurryRectangle rightBounds
+  leftLayer <- singletonLayer False True leftRegion
+  rightLayer <- singletonLayer False True rightRegion
+  overlay <- requireRight "dimensional valuation overlay" (overlayLayers leftLayer rightLayer)
+  selected <-
+    requireRight
+      "dimensional closed intersection"
+      (overlayClosedIntersection id id overlay)
+  requireRight "dimensional cell valuations" (cellValuations selected)
+
+uncurryRectangle
+  :: (Integer, Integer, Integer, Integer)
+  -> IO PlanarRegion
+uncurryRectangle (minimumX, minimumY, maximumX, maximumY) =
+  rectangleRegion minimumX minimumY maximumX maximumY
+
+assertRegionValuations
+  :: String
+  -> PlanarRegion
+  -> Int
+  -> Integer
+  -> [(Integer, Integer)]
+  -> IO ()
+assertRegionValuations label region expectedEuler expectedArea expectedLength = do
+  values <- requireRight label (regionValuations region)
+  assertEqual
+    (label <> " Euler")
+    expectedEuler
+    (eulerCharacteristicValue (valuationEuler values))
+  assertEqual
+    (label <> " area")
+    (fromInteger expectedArea)
+    (exactAreaValue (valuationArea values))
+  assertLength
+    (label <> " intrinsic one-volume")
+    expectedLength
+    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 values)))
+
+assertLength :: String -> [(Integer, Integer)] -> [ExactLengthTerm] -> IO ()
+assertLength label expected actual =
+  assertEqual
+    label
+    [ (fromInteger coefficient, radicand)
+    | (coefficient, radicand) <- expected
+    ]
+    [ (lengthCoefficient term, lengthRadicand term)
+    | term <- actual
+    ]
+
+assertContains :: String -> Double -> CertifiedInterval -> IO ()
+assertContains label expected interval =
+  if intervalLower interval <= expected && expected <= intervalUpper interval
+    then pure ()
+    else fail (label <> ": interval does not contain " <> show expected <> ": " <> show interval)
+
+valuationDigest
+  :: PlanarValuations
+  -> (Int, ExactRational, CertifiedInterval)
+valuationDigest values =
+  ( eulerCharacteristicValue (valuationEuler values)
+  , exactAreaValue (valuationArea values)
+  , exactLengthBounds (valuationIntrinsic1 values)
+  )
+
+exactDigest
+  :: PlanarValuations
+  -> (Int, ExactRational, [(ExactRational, Integer)])
+exactDigest values =
+  ( eulerCharacteristicValue (valuationEuler values)
+  , exactAreaValue (valuationArea values)
+  , [ (lengthCoefficient term, lengthRadicand term)
+    | term <- exactLengthTerms (exactLengthExpression (valuationIntrinsic1 values))
+    ]
+  )
+
+areaExact :: PlanarValuations -> ExactRational
+areaExact values =
+  exactAreaValue (valuationArea values)
+
+-- Independent normalized Rational formulas, deliberately not expressed through
+-- the production raw-sum carrier or its homogeneous-integer implementation.
+rationalEdgeMoments :: ((Rational, Rational), (Rational, Rational)) -> [Rational]
+rationalEdgeMoments ((x0, y0), (x1, y1)) =
+  let cross = x0 * y1 - y0 * x1
+   in [ cross / 2
+      , (x0 + x1) * cross / 6
+      , (y0 + y1) * cross / 6
+      , (x0 * x0 + x0 * x1 + x1 * x1) * cross / 12
+      , (2 * x0 * y0 + x0 * y1 + x1 * y0 + 2 * x1 * y1) * cross / 24
+      , (y0 * y0 + y0 * y1 + y1 * y1) * cross / 12
+      ]
+
+rationalBoundaryMoments :: [((Rational, Rational), (Rational, Rational))] -> [Rational]
+rationalBoundaryMoments = foldr (zipWith (+) . rationalEdgeMoments) [0, 0, 0, 0, 0, 0]
+
+rationalMomentValues :: ExactPlanarMoments -> [Rational]
+rationalMomentValues moments =
+  map (\value -> exactRationalNumerator value % exactRationalDenominator value)
+    [ exactPlanarMeasure moments, exactPlanarFirstX moments, exactPlanarFirstY moments
+    , exactPlanarSecondXX moments, exactPlanarSecondXY moments, exactPlanarSecondYY moments
+    ]
+
+momentEdgeFamilies :: [(String, [((Rational, Rational), (Rational, Rational))])]
+momentEdgeFamilies =
+  [ ("integer", [((n, 2*n+1), (n+3, 1-n)) | n <- [-4 .. 3]])
+  , ("common denominator", [((n/64, (2*n+1)/64), ((n+3)/64, (1-n)/64)) | n <- [-4 .. 3]])
+  , ("mixed coprime denominators", [((n/101, (2*n+1)/103), ((n+3)/107, (1-n)/109)) | n <- [-4 .. 3]])
+  , ("high bit width", [(((huge+n)/p, (huge-n)/q), ((huge+2*n+1)/r, (n-huge)/s)) | n <- [-4 .. 3]])
+  , ("zero cross", [((n/101, 2*n/101), ((n+3)/107, 2*(n+3)/107)) | n <- [-4 .. 3]])
+  ]
+ where
+  huge = fromInteger (2 ^ (191 :: Int))
+  p = fromInteger (2 ^ (127 :: Int) - 1)
+  q = fromInteger (2 ^ (107 :: Int) - 1)
+  r = fromInteger (2 ^ (89 :: Int) - 1)
+  s = fromInteger (2 ^ (61 :: Int) - 1)
+
+admitRationalMomentPoint :: (Rational, Rational) -> IO ExactPoint
+admitRationalMomentPoint (x, y) =
+  exactPoint <$> exactValue (numerator x) (denominator x)
+    <*> exactValue (numerator y) (denominator y)
+
+admitRationalMomentEdge :: ((Rational, Rational), (Rational, Rational)) -> IO (ExactPoint, ExactPoint)
+admitRationalMomentEdge (from, to) =
+  (,) <$> admitRationalMomentPoint from <*> admitRationalMomentPoint to
+
+testOrientedMomentRationalOracle :: IO ()
+testOrientedMomentRationalOracle = do
+  traverse_ checkFamily momentEdgeFamilies
+  putStrLn "oriented moments: 40 Rational edge oracles, reversal, subdivision and gluing passed"
+ where
+  checkFamily :: (String, [((Rational, Rational), (Rational, Rational))]) -> IO ()
+  checkFamily (label, edges) = do
+    admitted <- traverse admitRationalMomentEdge edges
+    let actual = orientedBoundaryMoments admitted
+    assertEqual (label <> " six independent Rational formulas")
+      (rationalBoundaryMoments edges) (rationalMomentValues actual)
+    traverse_
+      (\(edge, exactEdge) ->
+        assertEqual (label <> " individual edge")
+          (rationalEdgeMoments edge)
+          (rationalMomentValues (orientedBoundaryMoments [exactEdge])))
+      (zip edges admitted)
+    assertEqual (label <> " orientation reversal")
+      (map negate (rationalMomentValues actual))
+      (rationalMomentValues (orientedBoundaryMoments (map (\(from, to) -> (to, from)) (reverse admitted))))
+    subdivided <- traverse admitRationalMomentEdge
+      (concatMap (\(from@(x0,y0), to@(x1,y1)) ->
+        let middle = ((x0+x1)/2, (y0+y1)/2)
+         in [(from,middle),(middle,to)]) edges)
+    assertEqual (label <> " exact midpoint subdivision") actual (orientedBoundaryMoments subdivided)
+    let (prefix, suffix) = splitAt 3 admitted
+    assertEqual (label <> " finite gluing") actual
+      (orientedBoundaryMoments prefix <> orientedBoundaryMoments suffix)
+    assertEqual (label <> " empty boundary identity") actual
+      (orientedBoundaryMoments [] <> actual)
diff --git a/test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs b/test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/AlgebraFixtures.hs
+++ /dev/null
@@ -1,353 +0,0 @@
--- | The operand meshes and mesh observations both algebra slices are stated
--- over, built against the facade alone.
-module Moonlight.Triangulation.AlgebraFixtures
-  ( Mesh
-  , PointMesh
-  , meshOf
-  , inputOrderedMeshOf
-  , pointMeshOf
-  , integerPoint
-  , rectangleComponent
-  , rectangleRegion
-  , polygonRegion
-  , annulusRegion
-  , insideLayer
-  , overlayRegions
-  , operands
-  , separatedOperands
-  , cocircularRing
-  , collinearSites
-  , latticeSites
-  , pointsOf
-  , dedupeAscending
-  , siteList
-  , siteSet
-  , edgeKeys
-  , assertMesh
-  , assertMeshEquivalent
-  , pairs
-  , triples
-  , advance
-  , scramble
-  , randomSites
-  ) where
-
-import Data.List (sortBy)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe)
-import Data.Ord (comparing)
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Data.Word (Word64)
-import Moonlight.Triangulation
-  ( DelaunayTriangulation
-  , OverlayResult
-  , PlanarLayer
-  , PlanarRegion
-  , Point (Point)
-  , VertexId
-  , buildTriangulation
-  , canonicalize
-  , delaunay
-  , delaunayGeometry
-  , exactLoop
-  , geometryOnlyPublication
-  , mapVertices
-  , numFaces
-  , numUndirectedEdges
-  , numVertices
-  , overlayLayers
-  , planarLayer
-  , planarRegion
-  , polygonComponent
-  , undirectedEdges
-  , undirectedEndpoints
-  , unitElementDefaults
-  , vertexPoint
-  , vertices
-  )
-import Support (integerPoint, rectangleComponent, rectangleLoop, requireRight)
-
--- | The carrier. Geometry and nothing else: no vertex payload to need a
--- commutative combining rule, no element payloads to survive a rewrite that
--- destroys the elements they labelled.
-type Mesh = DelaunayTriangulation ()
-
--- | The same carrier with its exact coordinate retained as the vertex
--- annotation. Annotation-preserving set laws use it to construct their
--- expected values without reaching below the public facade.
-type PointMesh = DelaunayTriangulation Point
-
--- ── operands ─────────────────────────────────────────────────────────────────
-
--- | The site families the laws are exercised over.
---
--- Sized so that the cubic sweep in the associativity law stays cheap. Coverage
--- here is by /kind/ of degeneracy, not by count: what breaks a join is a
--- cocircular quad whose tie-break went the other way, or a mesh with no faces
--- at all, and neither of those becomes more likely at ten thousand sites.
-families :: [(String, [Point])]
-families =
-  [ ("void", [])
-  , ("single", [Point 3 (-7)])
-  , ("pair", [Point 0 0, Point 4 1])
-  , ("collinear", collinearSites)
-  , ("cocircular", cocircularRing)
-  , ("lattice", latticeSites)
-  , ("repeated", concatMap (replicate 3) [Point 0 0, Point 5 0, Point 0 5, Point 5 5, Point 2 3])
-  , ("scattered", randomSites 0xC0FFEEBABE 32)
-  , ("extreme", [Point 1.0e-8 1.0e-8, Point 1.0e8 (-1.0e8), Point (-1.0e8) 1.0e8, Point 0 0, Point 1 1])
-  ]
-
--- | Exactly cocircular lattice points on @x² + y² = 625@.
---
--- Trigonometric points would be cocircular only to within rounding, and the
--- rule this is here to exercise — the diagonal tie-break that fires when the
--- lifted quadrilateral is exactly flat — would then never fire at all.
-cocircularRing :: [Point]
-cocircularRing =
-  [ Point (fromIntegral (signX * x)) (fromIntegral (signY * y))
-  | (x, y) <- [(25, 0), (0, 25), (7, 24), (24, 7), (15, 20), (20, 15)] :: [(Int, Int)]
-  , signX <- [1, -1]
-  , signY <- [1, -1]
-  ]
-
-collinearSites :: [Point]
-collinearSites = [Point (fromIntegral k) (2 * fromIntegral k - 1) | k <- [0 .. 9 :: Int]]
-
-latticeSites :: [Point]
-latticeSites = [Point (fromIntegral i) (fromIntegral j) | i <- [0 .. 4 :: Int], j <- [0 .. 4 :: Int]]
-
--- | The meshes every pairwise and triple law runs over.
---
--- Chosen for overlap structure rather than for size: disjoint operands, nested
--- ones, partially overlapping ones, the empty one, and — the case that matters
--- most — two meshes standing on the /same/ sites built in different orders, so
--- they are geometrically identical and structurally distinct.
-operands :: IO [(String, Mesh)]
-operands = do
-  let sites name = fromMaybe [] (lookup name families)
-      lattice = sites "lattice"
-  void' <- meshOf "void" []
-  single <- meshOf "single" (sites "single")
-  collinear <- meshOf "collinear" (sites "collinear")
-  ring <- meshOf "cocircular" (sites "cocircular")
-  lower <- meshOf "lattice-lower" (take 15 lattice)
-  upper <- meshOf "lattice-upper" (drop 10 lattice)
-  repeated <- meshOf "repeated" (sites "repeated")
-  scatterA <- meshOf "scattered" (sites "scattered")
-  scatterB <- meshOf "scattered-scrambled" (scramble 0x5EED (sites "scattered"))
-  pure
-    [ ("void", void')
-    , ("single", single)
-    , ("collinear", collinear)
-    , ("cocircular", ring)
-    , ("lattice-lower", lower)
-    , ("lattice-upper", upper)
-    , ("repeated", repeated)
-    , ("scattered", scatterA)
-    , ("scattered-scrambled", scatterB)
-    ]
-
--- | Operand pairs whose sites are separated by a vertical line, which is the
--- stratum a seam schedule is defined on and the one the old-edge identity is
--- sharpest over: with no shared site, every edge is unambiguously left, right
--- or cross.
-separatedOperands :: IO [((String, Mesh), (String, Mesh))]
-separatedOperands =
-  traverse
-    ( \(name, seed, count, shift) -> do
-        let sites = randomSites seed count
-        left <- meshOf (name <> "-left") sites
-        right <- meshOf (name <> "-right") [Point (x + shift) y | Point x y <- sites]
-        pure ((name <> "-left", left), (name <> "-right", right))
-    )
-    [ ("split-distant", 0x51DE1, 60, 1000)
-    , ("split-near", 0x51DE2, 60, 3)
-    , ("split-abutting", 0x51DE3, 60, 2.05)
-    ]
-
--- ── construction ─────────────────────────────────────────────────────────────
-
-meshOf :: String -> [Point] -> IO Mesh
-meshOf label points =
-  geometryOnlyPublication
-    <$> requireRight ("build geometry " <> label) (delaunayGeometry (V.fromList points))
-
--- | Build through the annotated entrance when a law deliberately needs the
--- physical numbering induced by input order. Geometry-only construction is
--- free to choose the cheaper numbering because it publishes no input mapping.
-inputOrderedMeshOf :: String -> [Point] -> IO Mesh
-inputOrderedMeshOf label points =
-  mapVertices (const ()) . buildTriangulation
-    <$> requireRight
-          ("build input-ordered geometry " <> label)
-          (delaunay unitElementDefaults (V.fromList points))
-
-pointMeshOf :: String -> [Point] -> IO PointMesh
-pointMeshOf label points =
-  buildTriangulation
-    <$> requireRight ("build " <> label) (delaunay unitElementDefaults (V.fromList points))
-
-rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
-rectangleRegion minimumX minimumY maximumX maximumY =
-  rectangleComponent minimumX minimumY maximumX maximumY
-    >>= requireRight "rectangle region" . planarRegion . (: [])
-
-polygonRegion :: [(Integer, Integer)] -> IO PlanarRegion
-polygonRegion coordinates =
-  case map (uncurry integerPoint) coordinates of
-    firstPoint : secondPoint : thirdPoint : remaining -> do
-      loop <-
-        requireRight
-          "polygon loop"
-          (exactLoop (firstPoint :| (secondPoint : thirdPoint : remaining)))
-      component <- requireRight "polygon component" (polygonComponent loop [])
-      requireRight "polygon region" (planarRegion [component])
-    _ -> fail "polygon fixture requires at least three points"
-
-annulusRegion
-  :: (Integer, Integer, Integer, Integer)
-  -> (Integer, Integer, Integer, Integer)
-  -> IO PlanarRegion
-annulusRegion
-  (outerMinX, outerMinY, outerMaxX, outerMaxY)
-  (holeMinX, holeMinY, holeMaxX, holeMaxY) = do
-    outer <- rectangleLoop outerMinX outerMinY outerMaxX outerMaxY
-    hole <-
-      requireRight
-        "annulus hole loop"
-        ( exactLoop
-            ( integerPoint holeMinX holeMinY
-                :| [ integerPoint holeMinX holeMaxY
-                   , integerPoint holeMaxX holeMaxY
-                   , integerPoint holeMaxX holeMinY
-                   ]
-            )
-        )
-    component <- requireRight "annulus component" (polygonComponent outer [hole])
-    requireRight "annulus region" (planarRegion [component])
-
-insideLayer :: PlanarRegion -> IO (PlanarLayer Bool)
-insideLayer region =
-  requireRight "inside layer" (planarLayer False (Map.singleton True region))
-
-overlayRegions
-  :: PlanarRegion
-  -> PlanarRegion
-  -> IO (OverlayResult Bool Bool)
-overlayRegions left right = do
-  leftLayer <- insideLayer left
-  rightLayer <- insideLayer right
-  requireRight "region algebra overlay" (overlayLayers leftLayer rightLayer)
-
-pointsOf :: [(Double, Double)] -> [Point]
-pointsOf keys = [Point x y | (x, y) <- keys]
-
-dedupeAscending :: [(Double, Double)] -> [Point]
-dedupeAscending sorted = [Point x y | (x, y) <- dropAdjacentDuplicates sorted]
-
-dropAdjacentDuplicates :: Eq a => [a] -> [a]
-dropAdjacentDuplicates (first : second : rest)
-  | first == second = dropAdjacentDuplicates (second : rest)
-  | otherwise = first : dropAdjacentDuplicates (second : rest)
-dropAdjacentDuplicates rest = rest
-
--- ── observation ──────────────────────────────────────────────────────────────
-
-siteKey :: Mesh -> VertexId -> (Double, Double)
-siteKey mesh vertex = let Point x y = vertexPoint mesh vertex in (x, y)
-
-siteList :: Mesh -> [(Double, Double)]
-siteList mesh = [siteKey mesh vertex | vertex <- vertices mesh]
-
-siteSet :: Mesh -> Set.Set (Double, Double)
-siteSet = Set.fromList . siteList
-
-edgeKeys :: Mesh -> Set.Set ((Double, Double), (Double, Double))
-edgeKeys mesh =
-  Set.fromList
-    [ if left <= right then (left, right) else (right, left)
-    | edge <- undirectedEdges mesh
-    , let (from, to) = undirectedEndpoints mesh edge
-    , let left = siteKey mesh from
-    , let right = siteKey mesh to
-    ]
-
--- | What a mesh looks like when two of them were supposed to be equal.
---
--- The interesting failure is the one where the coordinate-keyed edge sets
--- agree and the values do not: that is the join having become correct only up
--- to DCEL isomorphism, which is precisely what these laws exist to forbid, and
--- a report that only printed counts would hide it.
-assertMesh :: String -> Mesh -> Mesh -> IO ()
-assertMesh label expected actual
-  | expected == actual = pure ()
-  | otherwise = fail (label <> ": " <> report)
- where
-  report
-    | expectedEdges == actualEdges =
-        "same geometry, different representation — "
-          <> meshCounts expected
-          <> " vs "
-          <> meshCounts actual
-    | otherwise =
-        meshCounts expected
-          <> " vs "
-          <> meshCounts actual
-          <> "; edges only in expected: "
-          <> show (take 4 (Set.toList (Set.difference expectedEdges actualEdges)))
-          <> "; only in actual: "
-          <> show (take 4 (Set.toList (Set.difference actualEdges expectedEdges)))
-  expectedEdges = edgeKeys expected
-  actualEdges = edgeKeys actual
-
-assertMeshEquivalent :: String -> Mesh -> Mesh -> IO ()
-assertMeshEquivalent label expected actual = do
-  canonicalExpected <-
-    requireRight (label <> ": canonical expected") (canonicalize expected)
-  canonicalActual <-
-    requireRight (label <> ": canonical actual") (canonicalize actual)
-  assertMesh label canonicalExpected canonicalActual
-
-meshCounts :: Mesh -> String
-meshCounts mesh =
-  show (numVertices mesh)
-    <> "v/"
-    <> show (numUndirectedEdges mesh)
-    <> "e/"
-    <> show (numFaces mesh)
-    <> "f"
-
--- ── combinatorics and pseudo-randomness ──────────────────────────────────────
-
-pairs :: [a] -> [(a, a)]
-pairs values = [(left, right) | left <- values, right <- values]
-
-triples :: [a] -> [(a, a, a)]
-triples values = [(a, b, c) | a <- values, b <- values, c <- values]
-
-advance :: Word64 -> Word64
-advance state = state * 6364136223846793005 + 1442695040888963407
-
-randomWords :: Word64 -> Int -> [Word64]
-randomWords seed count = take count (drop 1 (iterate advance seed))
-
--- | A deterministic permutation: decorate, sort by the key, discard it.
-scramble :: Word64 -> [a] -> [a]
-scramble seed values =
-  map snd (sortBy (comparing fst) (zip (randomWords seed (length values)) values))
-
-randomSites :: Word64 -> Int -> [Point]
-randomSites seed count =
-  [ Point (unitCoordinate first) (unitCoordinate second)
-  | (first, second) <- take count (chunkPairs (randomWords seed (2 * count)))
-  ]
-
-chunkPairs :: [a] -> [(a, a)]
-chunkPairs (first : second : rest) = (first, second) : chunkPairs rest
-chunkPairs _ = []
-
-unitCoordinate :: Word64 -> Double
-unitCoordinate value = 2 * fromIntegral (value `div` 2048) / 9007199254740992 - 1
diff --git a/test/algebra/Moonlight/Triangulation/AlgebraSpec.hs b/test/algebra/Moonlight/Triangulation/AlgebraSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/AlgebraSpec.hs
+++ /dev/null
@@ -1,919 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-
--- | The laws of finite union, stated against the typed facade a caller has.
---
--- Algebraic laws are observed through 'canonicalize'. Structural 'Eq' remains
--- the exact physical-representation observation used by round trips and caches.
-module Moonlight.Triangulation.AlgebraSpec (tests) where
-
-import Control.Monad (forM_, unless, when)
-import Data.Foldable (traverse_)
-import Data.IORef (modifyIORef', newIORef, readIORef)
-import Data.List (sort)
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import Data.Word (Word64)
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-  ( BuildError
-  , ConstraintMode (..)
-  , DelaunayTriangulation
-  , DuplicatePayloadPolicy (..)
-  , HasPosition (..)
-  , JoinSemilattice (..)
-  , Point (Point)
-  , Triangulation
-  , buildTriangulation
-  , delaunay
-  , delaunayFromCoordinates
-  , delaunayGeometry
-  , unitElementDefaults
-  , vertexData
-  , vertexPoint
-  , vertices
-  , SiteRelation (..)
-  , canonicalize
-  , difference
-  , intersection
-  , intersectionWith
-  , mapVertices
-  , numVertices
-  , siteRelation
-  , symmetricDifference
-  , union
-  , unions
-  , validateTriangulation
-  )
-import Moonlight.Triangulation.AlgebraFixtures
-  ( Mesh
-  , advance
-  , assertMesh
-  , assertMeshEquivalent
-  , cocircularRing
-  , collinearSites
-  , dedupeAscending
-  , edgeKeys
-  , inputOrderedMeshOf
-  , latticeSites
-  , meshOf
-  , operands
-  , pairs
-  , pointMeshOf
-  , pointsOf
-  , randomSites
-  , scramble
-  , separatedOperands
-  , siteList
-  , siteSet
-  , triples
-  )
-import Support (requireRight)
-
-tests :: IO ()
-tests = do
-  testConstructionEntrances
-  testJoinIdentity
-  testJoinCommutative
-  testJoinAssociative
-  testJoinIdempotent
-  testJoinGeneralPathIdempotent
-  testJoinSiteUnion
-  testJoinGluesAnnotations
-  testJoinIsUnionRebuild
-  testJoinResultValid
-  testSkewedJoinPreservesBaseHandles
-  testJoinBalancedFold
-  testJoinPartitionTrees
-  testConstructionOrderIndependent
-  testCanonicalPublication
-  testSiteRelationCoherence
-  testMeetLaws
-  testEmptySetIdentitiesPreserveVerbatim
-  testSkewedSetOperationsRemainValid
-  testDifferenceLaws
-  testSymmetricDifferenceLaws
-  testLatticeLaws
-  testSetOperationsPublishCanonical
-  testAnnotationPreservation
-  testOldEdgeAccounting
-  putStrLn "algebra: ok"
-
-testConstructionEntrances :: IO ()
-testConstructionEntrances = do
-  let coordinates = V.fromList [Point 0 0, Point 3 0, Point 0 4, Point 0 0]
-      payloads = V.fromList [2 :: Int, 3, 5, 7]
-  geometry <- requireRight "geometry-only construction" (delaunayGeometry coordinates)
-  legacy <-
-    mapVertices (const ()) . buildTriangulation
-      <$> requireRight "annotated point construction" (delaunay unitElementDefaults coordinates)
-  assertMeshEquivalent
-    "geometry-only construction agrees with annotated point construction"
-    legacy
-    geometry
-  annotated <-
-    buildTriangulation
-      <$> requireRight
-            "coordinate/payload construction"
-            ( delaunayFromCoordinates
-                unitElementDefaults
-                coordinates
-                payloads
-                (CombineDuplicatePayload (+))
-            )
-  unless (validateTriangulation geometry == [] && validateTriangulation annotated == []) $
-    fail "public construction entrance produced an invalid triangulation"
-  let observed =
-        Map.fromList
-          [ ((x, y), vertexData annotated vertex)
-          | vertex <- vertices annotated
-          , let Point x y = vertexPoint annotated vertex
-          ]
-      expected = Map.fromList [((0, 0), 9), ((3, 0), 3), ((0, 4), 5)]
-  unless (observed == expected) $
-    fail "delaunayFromCoordinates did not apply its duplicate payload policy"
-
--- ── laws ─────────────────────────────────────────────────────────────────────
-
-testJoinIdentity :: IO ()
-testJoinIdentity = do
-  values <- operands
-  identity <- requireRight "union identity" (unions [])
-  forM_ values $ \(name, mesh) -> do
-    left <- requireRight ("left identity at " <> name) (union identity mesh)
-    right <- requireRight ("right identity at " <> name) (union mesh identity)
-    assertMesh ("left identity at " <> name) mesh left
-    assertMesh ("right identity at " <> name) mesh right
-  idempotent <- requireRight "identity is idempotent" (union identity identity)
-  assertMesh "identity is idempotent" identity idempotent
-
-testJoinCommutative :: IO ()
-testJoinCommutative = do
-  values <- operands
-  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
-    leftRight <- requireRight "commutative union left/right" (union left right)
-    rightLeft <- requireRight "commutative union right/left" (union right left)
-    assertMeshEquivalent
-      ("commutativity at " <> leftName <> " ⋄ " <> rightName)
-      leftRight
-      rightLeft
-
-testJoinAssociative :: IO ()
-testJoinAssociative = do
-  values <- operands
-  forM_ (triples values) $ \((aName, a), (bName, b), (cName, c)) -> do
-    leftAssociated <- requireRight "left-associated union" (union a b >>= (`union` c))
-    rightAssociated <- requireRight "right-associated union" (union b c >>= union a)
-    assertMeshEquivalent
-      ("associativity at " <> aName <> " ⋄ " <> bName <> " ⋄ " <> cName)
-      leftAssociated
-      rightAssociated
-
-testJoinIdempotent :: IO ()
-testJoinIdempotent = do
-  values <- operands
-  forM_ values $ \(name, mesh) -> do
-    result <- requireRight ("idempotence at " <> name) (union mesh mesh)
-    assertMesh ("idempotence at " <> name) mesh result
-
--- | Idempotence with the structural-equality shortcut deliberately disarmed.
---
--- @a \<\> a@ is answered by a shortcut that compares the operands and returns
--- one verbatim, so on its own it says nothing about the operator underneath.
--- Three meshes standing on the same sites, built in three different orders, are
--- pairwise distinct as values — the test asserts that before relying on it —
--- so every join below takes the general path, and joining a fourth
--- representation of the same site set onto the result must still change
--- nothing.
-testJoinGeneralPathIdempotent :: IO ()
-testJoinGeneralPathIdempotent = do
-  let base = randomSites 0xA11CE 40
-  first <- inputOrderedMeshOf "order-1" base
-  second <- inputOrderedMeshOf "order-2" (scramble 0x1111 base)
-  third <- inputOrderedMeshOf "order-3" (scramble 0x2222 base)
-  fourth <- inputOrderedMeshOf "order-4" (reverse base)
-  unless (first /= second && second /= third && third /= fourth) $
-    fail "general-path idempotence: the four builds are not distinct values, so the shortcut is not disarmed"
-  joined <- requireRight "general-path union" (union first second)
-  thirdResult <- requireRight "absorbing a third representation" (union joined third)
-  fourthResult <- requireRight "absorbing a fourth representation" (union joined fourth)
-  symmetric <- requireRight "the union of two representations is symmetric" (union second first)
-  assertMeshEquivalent "absorbing a third representation" joined thirdResult
-  assertMeshEquivalent "absorbing a fourth representation" joined fourthResult
-  assertMeshEquivalent "the union of two representations is symmetric" joined symmetric
-
--- | The sites of a join are exactly the union of the operands' sites: none
--- dropped, none invented, each stored once.
-testJoinSiteUnion :: IO ()
-testJoinSiteUnion = do
-  values <- operands
-  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
-    let label = leftName <> " ⋄ " <> rightName
-        expected = Set.union (siteSet left) (siteSet right)
-    joined <- requireRight ("site union at " <> label) (union left right)
-    let actual = siteSet joined
-    unless (expected == actual) $
-      fail
-        ( "site union at "
-            <> label
-            <> ": dropped "
-            <> show (Set.toList (Set.difference expected actual))
-            <> ", invented "
-            <> show (Set.toList (Set.difference actual expected))
-        )
-    unless (Set.size actual == numVertices joined) $
-      fail ("site union at " <> label <> ": a site is stored more than once")
-
--- | The reference semantics: a join /is/ a rebuild of the union when both are
--- observed canonically.
---
--- The union is rebuilt three times, in three unrelated orders, and all three
--- must canonicalize to the canonical observation of the join. Comparing
--- against a single rebuild would only say the two agree; comparing against
--- three says that what they agree on is a function of the site set and not of
--- any build schedule, which is the actual claim.
---
--- Both shortcut cases are excluded, because a shortcut returns an operand
--- verbatim and an operand need not be canonically published. The count of
--- pairs that actually reached the general path is asserted, so this cannot
--- quietly become a test of nothing.
-testJoinIsUnionRebuild :: IO ()
-testJoinIsUnionRebuild = do
-  values <- operands
-  exercised <- newIORef (0 :: Int)
-  forM_ (pairs values) $ \((leftName, left), (rightName, right)) ->
-    when (numVertices left > 0 && numVertices right > 0 && left /= right) $ do
-      let label = leftName <> " ⋄ " <> rightName
-          unionSites = siteList left <> siteList right
-      joined <- requireRight ("union of " <> label) (union left right)
-      canonicalJoined <- requireRight ("canonical union of " <> label) (canonicalize joined)
-      forM_ [("ranked", dedupeAscending (sort unionSites)), ("reversed", pointsOf (reverse unionSites)), ("scrambled", pointsOf (scramble 0x7A57E unionSites))] $
-        \(order, sites) -> do
-          rebuilt <- meshOf ("rebuild of " <> label <> " in " <> order <> " order") sites
-          canonical <- requireRight ("canonical rebuild of " <> label <> " in " <> order <> " order") (canonicalize rebuilt)
-          assertMesh
-            ("join equals canonical rebuild at " <> label <> " (" <> order <> ")")
-            canonical
-            canonicalJoined
-      modifyIORef' exercised (+ 1)
-  count <- readIORef exercised
-  unless (count >= 40) $
-    fail ("join-equals-rebuild exercised only " <> show count <> " general-path pairs")
-
--- | Canonical publication in its own right, since the union laws lean on it.
---
--- Renumbering must not move geometry, must be a fixed point, must leave a
--- valid triangulation, and — the load-bearing one — must send every build
--- order of a site set to the same value. The distinctness of the inputs is
--- asserted first, so a canonicalization that did nothing at all would fail
--- here rather than pass everything.
-testCanonicalPublication :: IO ()
-testCanonicalPublication = do
-  values <- operands
-  forM_ values $ \(name, mesh) -> do
-    canonical <- requireRight ("canonicalization at " <> name) (canonicalize mesh)
-    unless (siteSet canonical == siteSet mesh) $
-      fail ("canonicalization at " <> name <> " moved the site set")
-    unless (edgeKeys canonical == edgeKeys mesh) $
-      fail ("canonicalization at " <> name <> " changed the triangulation")
-    case validateTriangulation canonical of
-      [] -> pure ()
-      violations -> fail ("canonicalization at " <> name <> " is invalid: " <> show violations)
-    fixedPoint <- requireRight ("canonicalization fixed point at " <> name) (canonicalize canonical)
-    assertMesh ("canonicalization at " <> name <> " is a fixed point") canonical fixedPoint
-  forM_ [("scattered", randomSites 0xB0A710 48), ("cocircular", cocircularRing), ("lattice", latticeSites), ("collinear", collinearSites)] $
-    \(name, base) -> do
-      built <-
-        traverse
-          (\(order, sites) -> (,) order <$> inputOrderedMeshOf (name <> "/" <> order) sites)
-          [ ("input", base)
-          , ("ranked", dedupeAscending (sort [(x, y) | Point x y <- base]))
-          , ("reversed", reverse base)
-          , ("scrambled", scramble 0x6666 base)
-          ]
-      case built of
-        [] -> fail "canonical publication: nothing built"
-        (_, reference) : rest -> do
-          unless (any (\(_, mesh) -> mesh /= reference) rest) $
-            fail ("canonical publication at " <> name <> ": every order already agreed, so this asserts nothing")
-          canonicalReference <- requireRight ("canonical reference at " <> name) (canonicalize reference)
-          forM_ rest $ \(order, mesh) -> do
-            canonicalMesh <- requireRight ("canonical publication at " <> name <> " from " <> order <> " order") (canonicalize mesh)
-            assertMesh
-              ("canonical publication at " <> name <> " from " <> order <> " order")
-              canonicalReference
-              canonicalMesh
-
-testJoinResultValid :: IO ()
-testJoinResultValid = do
-  values <- operands
-  forM_ (pairs values) $ \((leftName, left), (rightName, right)) -> do
-    joined <- requireRight ("valid union at " <> leftName <> " ⋄ " <> rightName) (union left right)
-    case validateTriangulation joined of
-      [] -> pure ()
-      violations ->
-        fail ("join at " <> leftName <> " ⋄ " <> rightName <> " is invalid: " <> show violations)
-
-testSkewedJoinPreservesBaseHandles :: IO ()
-testSkewedJoinPreservesBaseHandles = do
-  let baseSites = randomSites 0x5A71E 256
-      extensionSites =
-        fmap
-          (\(Point x y) -> Point (x + 4) y)
-          (randomSites 0xE71E 16)
-  base <- meshOf "persistent base" baseSites
-  extension <- meshOf "persistent extension" extensionSites
-  joined <- requireRight "persistent skewed union" (union base extension)
-  let baseObservations = fmap (\vertex -> (vertex, vertexPoint base vertex)) (vertices base)
-  forM_ baseObservations $ \(vertex, expectedPoint) ->
-    unless (vertexPoint joined vertex == expectedPoint) $
-      fail
-        ( "persistent skewed union moved base handle "
-            <> show vertex
-            <> " from "
-            <> show expectedPoint
-            <> " to "
-            <> show (vertexPoint joined vertex)
-        )
-  canonicalJoined <- requireRight "canonical persistent skewed union" (canonicalize joined)
-  reference <- meshOf "persistent union reference" (baseSites <> extensionSites)
-  canonicalReference <- requireRight "canonical persistent union reference" (canonicalize reference)
-  assertMesh "persistent skewed union agrees with canonical reference" canonicalReference canonicalJoined
-
--- | The balanced tournament agrees with every typed fold, in both directions.
-testJoinBalancedFold :: IO ()
-testJoinBalancedFold = do
-  shards <- shardMeshes 6 (randomSites 0xBA5EBA11 48)
-  identity <- requireRight "empty union" (unions [])
-  case shards of
-    [] -> fail "balanced fold: no shards were built"
-    firstShard : _ -> do
-      expected <- requireRight "balanced unions" (unions shards)
-      foldRight <- requireRight "right-folded unions" (foldr (\shard result -> result >>= union shard) (Right identity) shards)
-      foldLeft <- requireRight "left-folded unions" (foldl (\result shard -> result >>= (`union` shard)) (Right identity) shards)
-      singleton <- requireRight "singleton unions" (unions [firstShard])
-      repeated <- requireRight "repeated unions" (unions [firstShard, firstShard])
-      assertMeshEquivalent "balanced unions agree with foldr" expected foldRight
-      assertMeshEquivalent "balanced unions agree with foldl" expected foldLeft
-      assertMesh "unions of one shard is that shard" firstShard singleton
-      assertMesh "unions respects the idempotence shortcut" firstShard repeated
-
--- | The document's partition test: one site set, many shardings, many
--- bracketings, one canonical observation.
-testJoinPartitionTrees :: IO ()
-testJoinPartitionTrees = do
-  let base = randomSites 0xD15EA5E 54
-  whole <- meshOf "whole" base
-  reference <- requireRight "canonical whole partition reference" (canonicalize whole)
-  forM_ ([2, 3, 5, 7] :: [Int]) $ \shardCount -> do
-    shards <- shardMeshes shardCount base
-    forM_ ([0 .. 7] :: [Int]) $ \shape -> do
-      let scrambled = scramble (fromIntegral shape * 7919 + 13) shards
-      result <-
-        requireRight
-          ("partition tree " <> show shardCount <> "/" <> show shape)
-          (bracketRandomly (fromIntegral shape * 104729 + 7) scrambled)
-      assertMeshEquivalent
-        ("partition tree " <> show shardCount <> "/" <> show shape)
-        reference
-        result
-
--- | Delaunay uniqueness, stated as a law about this construction: the same
--- sites in any order give the same triangulation, and differ only in how it is
--- numbered.
---
--- Both halves are asserted, and the second is the one that carries weight. If
--- the numbering did /not/ differ, canonical publication would be a no-op and
--- the quotient this test names would be trivial. Because it does differ, this
--- law licenses construction in whichever order is cheapest and an explicit
--- canonical observation only where physical agreement is required.
---
--- The cocircular ring and the lattice are here because they are where it could
--- fail: an exactly flat lifted quadrilateral has two legal diagonals, and the
--- rule that picks between them is keyed on coordinates rather than on vertex
--- identifiers precisely so that this law holds.
-testConstructionOrderIndependent :: IO ()
-testConstructionOrderIndependent = do
-  forM_ [("scattered", randomSites 0x0DDBA11 60), ("cocircular", cocircularRing), ("lattice", latticeSites)] $
-    \(name, base) -> do
-      let ranked = dedupeAscending (sort [(x, y) | Point x y <- base])
-      meshes <-
-        traverse
-          (\(order, sites) -> (,) order <$> inputOrderedMeshOf (name <> "/" <> order) sites)
-          [ ("input", base)
-          , ("ranked", ranked)
-          , ("reversed", reverse base)
-          , ("scrambled-a", scramble 0x3333 base)
-          , ("scrambled-b", scramble 0x4444 base)
-          ]
-      case meshes of
-        [] -> fail "order independence: nothing built"
-        (referenceOrder, reference) : rest -> do
-          forM_ rest $ \(order, mesh) ->
-            unless (edgeKeys reference == edgeKeys mesh) $
-              fail
-                ( "order independence at "
-                    <> name
-                    <> ": "
-                    <> referenceOrder
-                    <> " and "
-                    <> order
-                    <> " reached different triangulations"
-                )
-          unless (any (\(_, mesh) -> mesh /= reference) rest) $
-            fail
-              ( "order independence at "
-                  <> name
-                  <> ": every build order produced the identical value, so the"
-                  <> " numbering quotient is trivial and this law asserts nothing"
-              )
-
--- | The Guibas–Stolfi old-edge theorem, asserted as a law of the operator.
---
--- Adding sites to a Delaunay triangulation cannot create an edge between two
--- sites that were already there. So every edge of a join whose endpoints both
--- stood in one operand must already have been an edge of that operand, and any
--- other edge is bichromatic — it joins a site exclusive to the left to a site
--- exclusive to the right.
---
--- This is strictly stronger than checking that the result is a valid Delaunay
--- triangulation, and it is stronger in exactly the direction a merge schedule
--- fails in. A seam that stitches the wrong pair of hull vertices, or that
--- retracts one edge too far before it stops deleting, produces a mesh that is
--- still Delaunay for /some/ site set and still passes validation; what it does
--- not do is leave the two interiors alone. The identity names that.
---
--- It holds for every schedule, including the rebuild the operator uses today,
--- because it is a property of the Delaunay triangulation of the union and not
--- of the route taken to it. That is why it can be asserted before a second
--- schedule exists: it is the gate one would have to pass, green on the path
--- that is already trusted.
---
--- Degeneracy does not weaken it. The tie-break on an exactly cocircular
--- quadrilateral is keyed on the four coordinates alone, so the same quad
--- resolves the same way whatever else stands nearby; a cocircular diagonal can
--- therefore be lost when a site lands inside its circle, which the theorem
--- permits, but cannot be exchanged for the other diagonal, which it forbids.
-testOldEdgeAccounting :: IO ()
-testOldEdgeAccounting = do
-  overlapping <- operands
-  separated <- separatedOperands
-  census <- newIORef (0 :: Int, 0 :: Int)
-  forM_ ([(l, r) | l <- overlapping, r <- overlapping] <> separated) $ \((leftName, left), (rightName, right)) -> do
-    let label = "old-edge " <> leftName <> " / " <> rightName
-    joined <- requireRight label (union left right)
-    let leftSites = siteSet left
-        rightSites = siteSet right
-        leftEdges = edgeKeys left
-        rightEdges = edgeKeys right
-    forM_ (Set.toList (edgeKeys joined)) $ \edge -> do
-      let (from, to) = edge
-          spans sites = Set.member from sites && Set.member to sites
-          monochromeLeft = spans leftSites
-          monochromeRight = spans rightSites
-      when (monochromeLeft && not (Set.member edge leftEdges)) $
-        fail (label <> ": join created " <> show edge <> ", an edge between two sites of the left operand")
-      when (monochromeRight && not (Set.member edge rightEdges)) $
-        fail (label <> ": join created " <> show edge <> ", an edge between two sites of the right operand")
-      modifyIORef' census $ \(retained, cross) ->
-        if monochromeLeft || monochromeRight
-          then (retained + 1, cross)
-          else (retained, cross + 1)
-  (retained, cross) <- readIORef census
-  when (retained < 1000 || cross < 100) $
-    fail
-      ( "old-edge accounting: the census is too thin to have asserted anything — "
-          <> show retained
-          <> " retained and "
-          <> show cross
-          <> " cross edges"
-      )
-
--- ── finite-set descent laws ─────────────────────────────────────────────────
-
--- | The public support relation is the exact order observation of the same
--- site sets consumed by union, meet and relative complement. Its overlap
--- witness must therefore agree with every operation's cardinality rather than
--- merely with another classification routine.
-testSiteRelationCoherence :: IO ()
-testSiteRelationCoherence = do
-  values <- operands
-  traverse_ (uncurry checkRelation) (pairs values)
- where
-  checkRelation (leftName, left) (rightName, right) = do
-    let label = "site relation at " <> leftName <> " / " <> rightName
-        leftSites = siteSet left
-        rightSites = siteSet right
-        overlap = Set.size (Set.intersection leftSites rightSites)
-        expected = referenceSiteRelation leftSites rightSites
-        coldActual = siteRelation left right
-    unless (coldActual == expected) $
-      fail (label <> ": expected " <> show expected <> ", got " <> show coldActual)
-    let repeatedActual = siteRelation left right
-    unless (repeatedActual == coldActual) $
-      fail (label <> ": repeated observation changed the relation")
-    met <- requireRight (label <> " intersection") (intersection left right)
-    removed <- requireRight (label <> " difference") (difference left right)
-    joined <- requireRight (label <> " union") (union left right)
-    unless (numVertices met == overlap) $
-      fail (label <> ": intersection cardinality disagrees with overlap")
-    unless (numVertices removed == Set.size leftSites - overlap) $
-      fail (label <> ": difference cardinality disagrees with overlap")
-    unless (numVertices joined == Set.size leftSites + Set.size rightSites - overlap) $
-      fail (label <> ": union cardinality disagrees with overlap")
-
-referenceSiteRelation
-  :: Set.Set (Double, Double)
-  -> Set.Set (Double, Double)
-  -> SiteRelation
-referenceSiteRelation left right
-  | left == right = EqualSites
-  | left `Set.isProperSubsetOf` right = LeftProperSubset
-  | right `Set.isProperSubsetOf` left = RightProperSubset
-  | Set.null overlap = DisjointSites
-  | otherwise = PartialOverlap (Set.size overlap)
- where
-  overlap = Set.intersection left right
-
-testMeetLaws :: IO ()
-testMeetLaws = do
-  values <- setLawOperands
-  traverse_
-    ( \(name, mesh) ->
-        assertSetEquation
-          ("meet idempotence at " <> name)
-          (intersection mesh mesh)
-          (canonicalize mesh)
-    )
-    values
-  traverse_
-    ( \((leftName, left), (rightName, right)) ->
-        assertSetEquation
-          ("meet commutativity at " <> leftName <> " / " <> rightName)
-          (intersection left right)
-          (intersection right left)
-    )
-    (pairs values)
-  traverse_
-    ( \((aName, a), (bName, b), (cName, c)) ->
-        assertSetEquation
-          ("meet associativity at " <> aName <> " / " <> bName <> " / " <> cName)
-          (intersection a b >>= (`intersection` c))
-          (intersection b c >>= intersection a)
-    )
-    (triples values)
-
-testEmptySetIdentitiesPreserveVerbatim :: IO ()
-testEmptySetIdentitiesPreserveVerbatim = do
-  let sites = scramble 0xE771D3 (randomSites 0xE771D4 48)
-  mesh <- inputOrderedMeshOf "verbatim empty identity source" sites
-  canonical <- requireRight "canonical verbatim empty identity source" (canonicalize mesh)
-  unless (mesh /= canonical) $
-    fail "verbatim empty identities: the source was already canonical, so this asserts nothing"
-  empty <- requireRight "verbatim empty identity" (unions [])
-  differenceRightIdentity <- requireRight "verbatim difference right identity" (difference mesh empty)
-  differenceLeftZero <- requireRight "verbatim difference left zero" (difference empty mesh)
-  symmetricRightIdentity <- requireRight "verbatim symmetric difference right identity" (symmetricDifference mesh empty)
-  symmetricLeftIdentity <- requireRight "verbatim symmetric difference left identity" (symmetricDifference empty mesh)
-  assertMesh "difference by empty preserves the left representative verbatim" mesh differenceRightIdentity
-  assertMesh "empty difference remains the empty representative" empty differenceLeftZero
-  assertMesh "symmetric difference by empty preserves the left representative verbatim" mesh symmetricRightIdentity
-  assertMesh "empty symmetric difference preserves the right representative verbatim" mesh symmetricLeftIdentity
-
-testSkewedSetOperationsRemainValid :: IO ()
-testSkewedSetOperationsRemainValid = do
-  let baseSites = randomSites 0xD1FFE7 1024
-      removedSites = take 4 (scramble 0xD1FFE8 baseSites)
-      retainedSites = filter (`notElem` removedSites) baseSites
-      disjointIncomingSites = fmap (\(Point x y) -> Point (x + 4) y) (randomSites 0xD1FFE9 4)
-      nearFullIntersectionSites = retainedSites <> disjointIncomingSites
-      overlappingRemovedSites = take 2 removedSites
-      overlappingIncomingSites = overlappingRemovedSites <> take 2 disjointIncomingSites
-      xorDisjointSites = baseSites <> disjointIncomingSites
-      xorOverlappingSites = filter (`notElem` overlappingRemovedSites) baseSites <> take 2 disjointIncomingSites
-  base <- meshOf "skewed set-operation base" baseSites
-  removed <- meshOf "skewed difference subset" removedSites
-  disjointIncoming <- meshOf "skewed difference disjoint" disjointIncomingSites
-  nearFullIntersection <- meshOf "skewed geometry-only intersection" nearFullIntersectionSites
-  overlappingIncoming <- meshOf "skewed xor overlapping" overlappingIncomingSites
-  differenceSubset <- requireRight "skewed difference subset" (difference base removed)
-  differenceDisjoint <- requireRight "skewed difference disjoint" (difference base disjointIncoming)
-  intersectionNearFull <- requireRight "skewed geometry-only intersection" (intersection base nearFullIntersection)
-  xorDisjoint <- requireRight "skewed xor disjoint" (symmetricDifference base disjointIncoming)
-  xorOverlapping <- requireRight "skewed xor overlapping" (symmetricDifference base overlappingIncoming)
-  assertRawSetResult "skewed difference subset" retainedSites differenceSubset
-  assertRawSetResult "skewed difference disjoint" baseSites differenceDisjoint
-  assertMesh "skewed difference disjoint preserves the base verbatim" base differenceDisjoint
-  assertRawSetResult "skewed geometry-only intersection" retainedSites intersectionNearFull
-  assertRawSetResult "skewed xor disjoint" xorDisjointSites xorDisjoint
-  assertRawSetResult "skewed xor overlapping" xorOverlappingSites xorOverlapping
-
-assertRawSetResult :: String -> [Point] -> Mesh -> IO ()
-assertRawSetResult label expectedSites result = do
-  case validateTriangulation result of
-    [] -> pure ()
-    violations -> fail (label <> " raw result is invalid: " <> show violations)
-  reference <- meshOf (label <> " fresh rebuild") (scramble 0xD1FFEA expectedSites)
-  assertMeshEquivalent (label <> " agrees canonically with a fresh rebuild") reference result
-
-testDifferenceLaws :: IO ()
-testDifferenceLaws = do
-  values <- setLawOperands
-  identity <- requireRight "difference identity" (unions [])
-  traverse_
-    ( \(name, mesh) -> do
-        assertSetEquation
-          ("difference cancellation at " <> name)
-          (difference mesh mesh)
-          (Right identity)
-        assertSetEquation
-          ("difference right identity at " <> name)
-          (difference mesh identity)
-          (canonicalize mesh)
-    )
-    values
-  traverse_
-    ( \((leftName, left), (rightName, right)) -> do
-        let label = "difference partition at " <> leftName <> " / " <> rightName
-            commonAndRemainder = do
-              common <- intersection left right
-              remainder <- difference left right
-              union common remainder
-            remainderMeetRight = difference left right >>= (`intersection` right)
-        assertSetEquation label commonAndRemainder (canonicalize left)
-        assertSetEquation (label <> " disjointness") remainderMeetRight (Right identity)
-    )
-    (pairs values)
-
-testSymmetricDifferenceLaws :: IO ()
-testSymmetricDifferenceLaws = do
-  values <- setLawOperands
-  identity <- requireRight "symmetric-difference identity" (unions [])
-  traverse_
-    ( \(name, mesh) -> do
-        assertSetEquation
-          ("symmetric-difference cancellation at " <> name)
-          (symmetricDifference mesh mesh)
-          (Right identity)
-        assertSetEquation
-          ("symmetric-difference identity at " <> name)
-          (symmetricDifference mesh identity)
-          (canonicalize mesh)
-    )
-    values
-  traverse_
-    ( \((leftName, left), (rightName, right)) -> do
-        let label = "symmetric difference at " <> leftName <> " / " <> rightName
-            decomposed = do
-              leftOnly <- difference left right
-              rightOnly <- difference right left
-              union leftOnly rightOnly
-        assertSetEquation
-          (label <> " commutativity")
-          (symmetricDifference left right)
-          (symmetricDifference right left)
-        assertSetEquation
-          (label <> " decomposition")
-          (symmetricDifference left right)
-          decomposed
-    )
-    (pairs values)
-  traverse_
-    ( \((aName, a), (bName, b), (cName, c)) ->
-        assertSetEquation
-          ("symmetric-difference associativity at " <> aName <> " / " <> bName <> " / " <> cName)
-          (symmetricDifference a b >>= (`symmetricDifference` c))
-          (symmetricDifference b c >>= symmetricDifference a)
-    )
-    (triples values)
-
-testLatticeLaws :: IO ()
-testLatticeLaws = do
-  values <- setLawOperands
-  traverse_
-    ( \((leftName, left), (rightName, right)) -> do
-        let label = "absorption at " <> leftName <> " / " <> rightName
-        assertSetEquation
-          (label <> " meet-over-join")
-          (union left right >>= intersection left)
-          (canonicalize left)
-        assertSetEquation
-          (label <> " join-over-meet")
-          (intersection left right >>= union left)
-          (canonicalize left)
-    )
-    (pairs values)
-  traverse_
-    ( \((aName, a), (bName, b), (cName, c)) -> do
-        let label = "distributivity at " <> aName <> " / " <> bName <> " / " <> cName
-            meetOverJoin = union b c >>= intersection a
-            joinedMeets = do
-              left <- intersection a b
-              right <- intersection a c
-              union left right
-            joinOverMeet = intersection b c >>= union a
-            metJoins = do
-              left <- union a b
-              right <- union a c
-              intersection left right
-        assertSetEquation (label <> " meet-over-join") meetOverJoin joinedMeets
-        assertSetEquation (label <> " join-over-meet") joinOverMeet metJoins
-    )
-    (triples values)
-
-testSetOperationsPublishCanonical :: IO ()
-testSetOperationsPublishCanonical = do
-  values <- setLawOperands
-  traverse_
-    ( \((leftName, left), (rightName, right)) -> do
-        let label = "canonical set publication at " <> leftName <> " / " <> rightName
-        case siteRelation left right of
-          DisjointSites -> assertCanonicalFixedPoint (label <> " intersection") (intersection left right)
-          PartialOverlap _ -> assertCanonicalFixedPoint (label <> " intersection") (intersection left right)
-          EqualSites -> pure ()
-          LeftProperSubset -> pure ()
-          RightProperSubset -> pure ()
-        when (numVertices left > 0 && numVertices right > 0) $ do
-          assertCanonicalFixedPoint (label <> " difference") (difference left right)
-          assertCanonicalFixedPoint (label <> " symmetric difference") (symmetricDifference left right)
-    )
-    (pairs values)
-
--- | Payloads descend with their coordinates. Intersection combines only the
--- common sections, difference preserves the left section, and symmetric
--- difference preserves whichever unique section survives. The independently
--- built expected meshes ensure this is not a restatement of the implementation.
-testAnnotationPreservation :: IO ()
-testAnnotationPreservation = do
-  let shared = randomSites 0xA6607A7E 12
-      leftOnly = randomSites 0x1EF7 8
-      rightOnly = randomSites 0xA1167 10
-      leftPoints = leftOnly <> shared
-      rightPoints = shared <> rightOnly
-  left <- pointMeshOf "annotation-left" leftPoints
-  rightPointsMesh <- pointMeshOf "annotation-right" rightPoints
-  sharedReference <- canonicalPointMesh "annotation-shared" shared
-  leftOnlyReference <- canonicalPointMesh "annotation-left-only" leftOnly
-  exclusiveReference <- canonicalPointMesh "annotation-exclusive" (leftOnly <> rightOnly)
-  let right = mapVertices rightAnnotation rightPointsMesh
-      expectedCombined = mapVertices (\point -> (point, rightAnnotation point)) sharedReference
-      taggedLeft = mapVertices (\point -> ExclusiveAnnotation point LeftOperand) left
-      taggedRight = mapVertices (\point -> ExclusiveAnnotation point RightOperand) rightPointsMesh
-      taggedExclusive =
-        mapVertices
-          (\point -> ExclusiveAnnotation point (if point `elem` leftOnly then LeftOperand else RightOperand))
-          exclusiveReference
-  unless (siteRelation left right == PartialOverlap (length shared)) $
-    fail "siteRelation changed when the vertex annotation type changed"
-  combined <- requireRight "annotation intersectionWith" (intersectionWith (,) left right)
-  restricted <- requireRight "annotation restriction" (intersectionWith const left right)
-  removed <- requireRight "annotation difference" (difference left right)
-  exclusive <- requireRight "annotation symmetric difference" (symmetricDifference taggedLeft taggedRight)
-  assertAnnotatedMesh "intersectionWith combines left then right" expectedCombined combined
-  assertAnnotatedMesh "intersectionWith const restricts the left section" sharedReference restricted
-  assertAnnotatedMesh "difference preserves left annotations" leftOnlyReference removed
-  assertAnnotatedMesh "symmetric difference preserves the annotation of each exclusive owner" taggedExclusive exclusive
-
-data ExclusiveOperand
-  = LeftOperand
-  | RightOperand
-  deriving stock (Eq)
-
-data ExclusiveAnnotation = ExclusiveAnnotation !(Point) !ExclusiveOperand
-  deriving stock (Eq)
-
-rightAnnotation :: Point -> (Double, Double)
-rightAnnotation (Point x y) = (x + y, x - y)
-
-canonicalPointMesh :: String -> [Point] -> IO (DelaunayTriangulation (Point))
-canonicalPointMesh label points =
-  pointMeshOf label points
-    >>= requireRight ("canonicalize " <> label) . canonicalize
-
-assertAnnotatedMesh :: Eq mesh => String -> mesh -> mesh -> IO ()
-assertAnnotatedMesh label expected actual =
-  unless (expected == actual) (fail (label <> ": structural values differ"))
-
-assertSetEquation
-  :: String
-  -> Either BuildError Mesh
-  -> Either BuildError Mesh
-  -> IO ()
-assertSetEquation label leftExpression rightExpression = do
-  left <- requireRight (label <> " left") leftExpression
-  right <- requireRight (label <> " right") rightExpression
-  leftCanonical <- requireRight (label <> " canonical left") (canonicalize left)
-  rightCanonical <- requireRight (label <> " canonical right") (canonicalize right)
-  assertMesh label leftCanonical rightCanonical
-
-assertCanonicalFixedPoint :: String -> Either BuildError Mesh -> IO ()
-assertCanonicalFixedPoint label expression = do
-  result <- requireRight label expression
-  fixedPoint <- requireRight (label <> " fixed point") (canonicalize result)
-  assertMesh label result fixedPoint
-
--- | Six representatives retain empty, degenerate, nested, partially
--- overlapping and equal-support/different-publication cases without turning
--- every ternary law into the full nine-cubed join corpus already exercised
--- above.
-setLawOperands :: IO [(String, Mesh)]
-setLawOperands =
-  filter
-    (\(name, _) -> name `elem` ["void", "single", "lattice-lower", "lattice-upper", "scattered", "scattered-scrambled"])
-    <$> operands
-
--- ── construction ─────────────────────────────────────────────────────────────
-
--- | Deal the sites round-robin into @count@ shards, so every shard spans the
--- whole extent and the joins are genuinely overlapping rather than separable.
-shardMeshes :: Int -> [Point] -> IO [Mesh]
-shardMeshes count sites =
-  traverse
-    (\shard -> meshOf ("shard " <> show shard) [site | (index, site) <- indexed, index `mod` count == shard])
-    [0 .. count - 1]
- where
-  indexed = zip [0 :: Int ..] sites
-
--- | Combine by repeatedly joining a pseudo-randomly chosen adjacent pair, so
--- that successive seeds give genuinely different bracketings.
-bracketRandomly :: Word64 -> [Mesh] -> Either BuildError Mesh
-bracketRandomly _ [] = unions []
-bracketRandomly _ [single] = Right single
-bracketRandomly seed values =
-  let !stepped = advance seed
-      !cut = fromIntegral (stepped `mod` fromIntegral (length values - 1))
-   in joinAdjacent cut values >>= bracketRandomly stepped
-
--- | Replace the pair at @index@ with its join, leaving everything else alone.
-joinAdjacent :: Int -> [Mesh] -> Either BuildError [Mesh]
-joinAdjacent index (left : right : rest)
-  | index <= 0 = (: rest) <$> union left right
-  | otherwise = (left :) <$> joinAdjacent (index - 1) (right : rest)
-joinAdjacent _ rest = Right rest
-
--- | A site is a coordinate carrying a tag. The join acts on tags alone: two
--- annotations only ever meet at a shared coordinate, where the positions
--- already agree, so geometry is never authored by the payload.
-data Site = Site !(Point) !Int
-  deriving stock (Eq, Show)
-
-instance HasPosition Site where
-  position (Site point _) = point
-
-instance JoinSemilattice Site where
-  joinAnnotations (Site point left) (Site _ right) = Site point (max left right)
-
-type AnnotatedMesh = Triangulation 'Unconstrained Site () () ()
-
-annotatedMesh :: String -> [Site] -> IO AnnotatedMesh
-annotatedMesh label sites =
-  buildTriangulation <$> requireRight label (delaunay unitElementDefaults (V.fromList sites))
-
--- Keyed on the coordinate plane rather than on the payload's own point,
--- because geometry is authoritative and a payload that disagreed with it must
--- not be able to hide behind itself.
-tagsOf :: AnnotatedMesh -> Map.Map (Double, Double) Int
-tagsOf mesh =
-  Map.fromList
-    [ ((x, y), tag)
-    | vertex <- vertices mesh
-    , let Point x y = vertexPoint mesh vertex
-    , let Site _ tag = vertexData mesh vertex
-    ]
-
--- The whole law in one equation: the union's annotation at every site is the
--- join of whatever the operands carried there.
-assertGluing :: String -> [Site] -> [Site] -> IO ()
-assertGluing label leftSites rightSites = do
-  left <- annotatedMesh (label <> " left") leftSites
-  right <- annotatedMesh (label <> " right") rightSites
-  glued <- requireRight (label <> " union") (union left right)
-  assertEqualTags
-    (label <> ": union glues shared annotations by join")
-    (Map.unionWith max (tagsOf left) (tagsOf right))
-    (tagsOf glued)
-
-assertEqualTags :: String -> Map.Map (Double, Double) Int -> Map.Map (Double, Double) Int -> IO ()
-assertEqualTags label expected actual =
-  unless (expected == actual) $
-    fail (label <> ": expected " <> show (Map.toList expected) <> ", got " <> show (Map.toList actual))
-
--- The plan is chosen by size: a small addition is inserted into the larger
--- operand, and only comparably sized operands are rebuilt canonically. Both
--- paths glue, and only running both shows it.
-testJoinGluesAnnotations :: IO ()
-testJoinGluesAnnotations = do
-  assertGluing
-    "insertion plan"
-    [Site (Point 0 0) 1, Site (Point 6 0) 1, Site (Point 0 6) 1, Site (Point 2 1) 7]
-    [Site (Point 0 0) 5, Site (Point 6 0) 2, Site (Point 0 6) 3, Site (Point 4 3) 9]
-  assertGluing "rebuild plan" (grid 0 1) (grid 6 2)
- where
-  -- Two hundred-odd sites overlapping in half, so neither operand is small
-  -- enough to be inserted into the other and the canonical rebuild is taken.
-  grid offset tag =
-    [ Site (Point (fromIntegral column) (fromIntegral row)) (tag * (column + row))
-    | column <- [offset .. offset + 11 :: Int]
-    , row <- [0 .. 9 :: Int]
-    ]
diff --git a/test/algebra/Moonlight/Triangulation/ExactClipRetentionSpec.hs b/test/algebra/Moonlight/Triangulation/ExactClipRetentionSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/ExactClipRetentionSpec.hs
+++ /dev/null
@@ -1,536 +0,0 @@
--- | Independent endpoint-reconstruction oracle for the retained-line clipper.
---
--- The oracle is deliberately test-only. Production has one clipping authority:
--- 'exactClipRetainedPolygon'.
-module Moonlight.Triangulation.ExactClipRetentionSpec (tests) where
-
-import Control.Monad (foldM, unless)
-import Data.Foldable (traverse_)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import Data.Maybe (maybeToList)
-import Moonlight.Triangulation.Exact
-  ( ExactAffineLine
-  , ExactClipDisposition (..)
-  , ExactClipReceipt (..)
-  , ExactClosedHalfPlane
-  , ExactHalfPlaneError (..)
-  , ExactIntersectionError
-  , ExactPoint
-  , ExactRetainedPolygon
-  , SegmentRelation (..)
-  , classifyExactPoint
-  , exactAffineLine
-  , exactAffineLineCoefficients
-  , exactAffineLineIntersection
-  , exactClipRetainedPolygon
-  , exactClosedHalfPlane
-  , exactClosedHalfPlaneFromDirectedEdge
-  , exactClosedHalfPlaneLine
-  , exactOrient2d
-  , exactPoint
-  , exactPointCoordinates
-  , exactRetainedPolygon
-  , exactRetainedPolygonPoints
-  )
-import Moonlight.Triangulation.Internal.BoundaryCycle
-  ( cyclePairsNonEmpty
-  , cyclicTriples
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRational
-  , exactRationalBitWidth
-  , exactRationalDenominatorBitWidth
-  )
-import Support (assertEqual, integerPoint, requireRight)
-
-tests :: IO ()
-tests = do
-  testAffineIntersectionCrossProducts
-  testSelfIntersectingAllLeftCycleRefused
-  testRetainedClosingLineSurvivesSecondClip
-  testAngularHalfPlanePermutation
-  testClosedDimensionalDispositions
-  initialPolygon <-
-    requireRight
-      "retained-line fixture polygon"
-      ( exactRetainedPolygon
-          ( integerPoint (-1000) (-1000)
-              :| [ integerPoint 1000 (-1000)
-                 , integerPoint 1000 1000
-                 , integerPoint (-1000) 1000
-                 ]
-          )
-      )
-  halfPlanes <- traverse fixtureHalfPlane fixtureCoefficients
-  (retainedDisposition, retainedReceipt) <-
-    requireRight
-      "retained-line fixture clipping"
-      (exactClipRetainedPolygon initialPolygon halfPlanes)
-  retainedPoints <-
-    case retainedDisposition of
-      ExactClipFullDimensional polygon -> pure (exactRetainedPolygonPoints polygon)
-      other -> fail ("retained-line fixture lost full dimension: " <> show other)
-  (endpointPoints, endpointReceipt) <-
-    requireRight
-      "endpoint-reconstruction oracle"
-      (endpointClipSequence (exactRetainedPolygonPoints initialPolygon) halfPlanes)
-
-  assertExactPointSet "final exact polygon" retainedPoints endpointPoints
-  assertEqual
-    "final reduced denominator width is representation invariant"
-    (exactClipFinalDenominatorBits retainedReceipt)
-    (endpointFinalDenominatorBits endpointReceipt)
-  assertEqual "measured retained coefficient width" 21 (exactClipMaximumAffineCoefficientBits retainedReceipt)
-  assertEqual "measured endpoint coefficient width" 32 (endpointPeakAffineCoefficientBits endpointReceipt)
-  unless
-    ( endpointPeakAffineCoefficientBits endpointReceipt
-        > exactClipMaximumAffineCoefficientBits retainedReceipt
-    )
-    ( fail
-        "endpoint reconstruction did not exhibit strictly larger affine coefficients"
-    )
-
-  -- Equality is checked at every local section, not merely after global gluing.
-  -- This is the reason no honest coordinate-width counterexample can exist.
-  traverse_
-    (assertPrefixAgreement initialPolygon)
-    (List.inits halfPlanes)
-  putStrLn "exact retained-line clipping: ok (source coefficients 21 bits; endpoint oracle 32 bits)"
-
-testAffineIntersectionCrossProducts :: IO ()
-testAffineIntersectionCrossProducts = do
-  oneHalf <- rationalCoefficient 1 2
-  oneThird <- rationalCoefficient 1 3
-  twoFifths <- rationalCoefficient 2 5
-  negativeOneSeventh <- rationalCoefficient (-1) 7
-  negativeOne <- rationalCoefficient (-1) 1
-  oneEleventh <- rationalCoefficient 1 11
-  firstLine <-
-    requireRight
-      "first rational intersection line"
-      (exactAffineLine oneHalf oneThird negativeOne)
-  secondLine <-
-    requireRight
-      "second rational intersection line"
-      (exactAffineLine twoFifths negativeOneSeventh oneEleventh)
-  intersection <-
-    requireRight
-      "homogeneous integer affine intersection"
-      (exactAffineLineIntersection firstLine secondLine)
-  expectedX <- requireRight "expected rational x" (exactRational 260 473)
-  expectedY <- requireRight "expected rational y" (exactRational 1029 473)
-  assertEqual
-    "homogeneous integer intersection agrees with rational Cramer's rule"
-    (exactPoint expectedX expectedY)
-    intersection
-  assertEqual
-    "intersection remains on the first source line"
-    EQ
-    (classifyExactPoint (exactClosedHalfPlane firstLine) intersection)
-  assertEqual
-    "intersection remains on the second source line"
-    EQ
-    (classifyExactPoint (exactClosedHalfPlane secondLine) intersection)
- where
-  rationalCoefficient :: Integer -> Integer -> IO ExactRational
-  rationalCoefficient numerator denominator =
-    requireRight
-      "rational intersection coefficient"
-      (exactRational numerator denominator)
-
-testAngularHalfPlanePermutation :: IO ()
-testAngularHalfPlanePermutation = do
-  square <- retainedSquare
-  halfPlanes <-
-    traverse
-      fixtureHalfPlane
-      [ (1, 0, -2)
-      , (2, 0, -4)
-      , (-1, 0, 8)
-      , (0, 1, -1)
-      , (0, -1, 9)
-      ]
-  (expected, expectedReceipt) <-
-    requireRight
-      "angular half-plane canonical result"
-      (exactClipRetainedPolygon square halfPlanes)
-  case expected of
-    ExactClipFullDimensional polygon ->
-      assertExactPointSet
-        "parallel descent retains the strongest boundaries"
-        ( integerPoint 2 1
-            :| [integerPoint 8 1, integerPoint 8 9, integerPoint 2 9]
-        )
-        (exactRetainedPolygonPoints polygon)
-    other -> fail ("expected a full-dimensional canonical section, got " <> show other)
-  traverse_
-    (\permutation -> do
-       (actual, receipt) <-
-         requireRight
-           "permuted angular half-plane intersection"
-           (exactClipRetainedPolygon square permutation)
-       assertEqual "angular half-plane permutation" expected actual
-       assertEqual
-         "submitted half-plane count"
-         (length halfPlanes)
-         (exactClipSubmittedHalfPlanes receipt))
-    (List.permutations halfPlanes)
-  unless (exactClipBoundaryCompatibilityChecks expectedReceipt > 0) $
-    fail "angular descent reported no boundary compatibility work"
-
-testClosedDimensionalDispositions :: IO ()
-testClosedDimensionalDispositions = do
-  square <- retainedSquare
-  xAtOne <- traverse fixtureHalfPlane [(1, 0, -1), (-1, 0, 1)]
-  (segmentDisposition, _) <-
-    requireRight
-      "closed segment half-plane intersection"
-      (exactClipRetainedPolygon square xAtOne)
-  case segmentDisposition of
-    ExactClipLowerDimensional points ->
-      assertEqual
-        "opposing closed half-planes retain their shared segment"
-        (List.sort [integerPoint 1 0, integerPoint 1 10])
-        (List.sort (NonEmpty.toList points))
-    other -> fail ("expected a closed segment, got " <> show other)
-
-  yAtThree <- traverse fixtureHalfPlane [(0, 1, -3), (0, -1, 3)]
-  (pointDisposition, _) <-
-    requireRight
-      "closed point half-plane intersection"
-      (exactClipRetainedPolygon square (xAtOne <> yAtThree))
-  case pointDisposition of
-    ExactClipLowerDimensional points ->
-      assertEqual
-        "two zero-width closed sections retain their shared point"
-        [integerPoint 1 3]
-        (List.sort (NonEmpty.toList points))
-    other -> fail ("expected a closed point, got " <> show other)
-
-  incompatible <- traverse fixtureHalfPlane [(1, 0, -6), (-1, 0, 5)]
-  (emptyDisposition, _) <-
-    requireRight
-      "incompatible closed half-plane intersection"
-      (exactClipRetainedPolygon square incompatible)
-  assertEqual "separated opposing boundaries are empty" ExactClipEmpty emptyDisposition
-
-retainedSquare :: IO ExactRetainedPolygon
-retainedSquare =
-  requireRight
-    "retained square"
-    ( exactRetainedPolygon
-        ( integerPoint 0 0
-            :| [integerPoint 10 0, integerPoint 10 10, integerPoint 0 10]
-        )
-    )
-
-testSelfIntersectingAllLeftCycleRefused :: IO ()
-testSelfIntersectingAllLeftCycleRefused = do
-  let points =
-        integerPoint 0 0
-          :| [ integerPoint 5 3
-             , integerPoint (-1) 3
-             , integerPoint 4 0
-             , integerPoint 2 5
-             ]
-      turns =
-        fmap
-          (\(previous, current, next) -> exactOrient2d previous current next)
-          (cyclicTriples (NonEmpty.toList points))
-  assertEqual "self-intersection fixture has only local left turns" (replicate 5 GT) turns
-  case exactRetainedPolygon points of
-    Left (ExactRetainedPolygonSelfRelation 0 2 SegmentsProperlyCross) -> pure ()
-    other ->
-      fail
-        ( "self-intersecting all-left retained polygon: expected edge 0/2 crossing, got "
-            <> show other
-        )
-
--- The first clip emits its first point twice. The closing occurrence carries
--- the source line of the retained bottom edge; preserving the opening record
--- instead corrupts that edge into the square's stale right boundary and makes
--- the second clip spuriously report parallel lines.
-testRetainedClosingLineSurvivesSecondClip :: IO ()
-testRetainedClosingLineSurvivesSecondClip = do
-  square <-
-    requireRight
-      "retained square"
-      ( exactRetainedPolygon
-          ( integerPoint 0 0
-              :| [ integerPoint 10 0
-                 , integerPoint 10 10
-                 , integerPoint 0 10
-                 ]
-          )
-      )
-  diagonalLine <- requireRight "diagonal clipping line" (exactAffineLine (-1) (-1) 10)
-  verticalLine <- requireRight "vertical clipping line" (exactAffineLine (-1) 0 5)
-  let diagonalHalfPlane = exactClosedHalfPlane diagonalLine
-      verticalHalfPlane = exactClosedHalfPlane verticalLine
-      retainedHalfPlanes = diagonalHalfPlane :| [verticalHalfPlane]
-  (diagonalDisposition, _) <-
-    requireRight
-      "retained square diagonal clip"
-      (exactClipRetainedPolygon square [diagonalHalfPlane])
-  diagonalPolygon <-
-    requireFullDimensionalClip
-      "retained square diagonal clip"
-      diagonalDisposition
-  assertExactPointSet
-    "first clip retains the expected triangle"
-    (integerPoint 0 0 :| [integerPoint 10 0, integerPoint 0 10])
-    (exactRetainedPolygonPoints diagonalPolygon)
-  (sequentialDisposition, _) <-
-    requireRight
-      "second clip over the retained first result"
-      (exactClipRetainedPolygon diagonalPolygon [verticalHalfPlane])
-  sequentialPolygon <-
-    requireFullDimensionalClip
-      "second clip over the retained first result"
-      sequentialDisposition
-  (combinedDisposition, combinedReceipt) <-
-    requireRight
-      "combined retained two-clip descent"
-      (exactClipRetainedPolygon square (NonEmpty.toList retainedHalfPlanes))
-  combinedPolygon <-
-    requireFullDimensionalClip
-      "combined retained two-clip descent"
-      combinedDisposition
-  let expectedFinalPoints =
-        integerPoint 0 0
-          :| [integerPoint 5 0, integerPoint 5 5, integerPoint 0 10]
-  assertExactPointSet
-    "second clip intersects the retained bottom and diagonal source lines"
-    expectedFinalPoints
-    (exactRetainedPolygonPoints sequentialPolygon)
-  assertEqual
-    "sequential and combined clipping preserve the same retained polygon"
-    sequentialPolygon
-    combinedPolygon
-  assertEqual
-    "combined clipping records both submitted half-planes"
-    2
-    (exactClipSubmittedHalfPlanes combinedReceipt)
-  assertRetainedVerticesInside
-    retainedHalfPlanes
-    (exactRetainedPolygonPoints combinedPolygon)
-
-requireFullDimensionalClip
-  :: String
-  -> ExactClipDisposition
-  -> IO ExactRetainedPolygon
-requireFullDimensionalClip _ (ExactClipFullDimensional polygon) = pure polygon
-requireFullDimensionalClip label disposition =
-  fail (label <> ": expected a full-dimensional polygon, got " <> show disposition)
-
-assertExactPointSet
-  :: String
-  -> NonEmpty ExactPoint
-  -> NonEmpty ExactPoint
-  -> IO ()
-assertExactPointSet label expected actual =
-  assertEqual
-    label
-    (List.sort (NonEmpty.toList expected))
-    (List.sort (NonEmpty.toList actual))
-
-assertRetainedVerticesInside
-  :: NonEmpty ExactClosedHalfPlane
-  -> NonEmpty ExactPoint
-  -> IO ()
-assertRetainedVerticesInside halfPlanes points =
-  traverse_
-    (\halfPlane ->
-       traverse_
-         (\point ->
-            unless (classifyExactPoint halfPlane point /= LT) $
-              fail ("retained vertex violates clipping half-plane: " <> show point))
-         points)
-    halfPlanes
-
-data EndpointOracleError
-  = EndpointOracleHalfPlane !ExactHalfPlaneError
-  | EndpointOracleIntersection !ExactIntersectionError
-  | EndpointOracleLostFullDimension ![ExactPoint]
-  deriving stock (Eq, Show)
-
-data EndpointOracleReceipt = EndpointOracleReceipt
-  { endpointPeakAffineCoefficientBits :: !Int
-  , endpointPeakCoordinateBits :: !Int
-  , endpointFinalDenominatorBits :: !Int
-  }
-  deriving stock (Eq, Show)
-
-data EndpointEdgeClip = EndpointEdgeClip
-  { endpointEdgePoints :: ![ExactPoint]
-  , endpointEdgeReconstructedCoefficientBits :: !(Maybe Int)
-  }
-  deriving stock (Eq, Show)
-
-endpointClipSequence
-  :: NonEmpty ExactPoint
-  -> [ExactClosedHalfPlane]
-  -> Either EndpointOracleError (NonEmpty ExactPoint, EndpointOracleReceipt)
-endpointClipSequence initialPoints halfPlanes = do
-  let initialReceipt =
-        EndpointOracleReceipt
-          { endpointPeakAffineCoefficientBits = 0
-          , endpointPeakCoordinateBits = pointCycleBitWidth initialPoints
-          , endpointFinalDenominatorBits = pointCycleDenominatorBitWidth initialPoints
-          }
-  (finalPoints, accumulatedReceipt) <-
-    foldM endpointClipStep (initialPoints, initialReceipt) halfPlanes
-  pure
-    ( finalPoints
-    , accumulatedReceipt
-        { endpointFinalDenominatorBits =
-            pointCycleDenominatorBitWidth finalPoints
-        }
-    )
-
-endpointClipStep
-  :: (NonEmpty ExactPoint, EndpointOracleReceipt)
-  -> ExactClosedHalfPlane
-  -> Either EndpointOracleError (NonEmpty ExactPoint, EndpointOracleReceipt)
-endpointClipStep (points, receipt) halfPlane = do
-  clippedEdges <-
-    traverse (clipEndpointEdge halfPlane) (cyclePairsNonEmpty points)
-  let clippedPoints = concatMap endpointEdgePoints clippedEdges
-      stepCoefficientBits =
-        maximumOrZero
-          ( foldMap
-              (maybeToList . endpointEdgeReconstructedCoefficientBits)
-              clippedEdges
-          )
-  nextPoints <-
-    maybe
-      (Left (EndpointOracleLostFullDimension clippedPoints))
-      Right
-      (NonEmpty.nonEmpty clippedPoints)
-  if NonEmpty.length nextPoints < 3
-    then Left (EndpointOracleLostFullDimension clippedPoints)
-    else
-      Right
-        ( nextPoints
-        , receipt
-            { endpointPeakAffineCoefficientBits =
-                max
-                  (endpointPeakAffineCoefficientBits receipt)
-                  stepCoefficientBits
-            , endpointPeakCoordinateBits =
-                max
-                  (endpointPeakCoordinateBits receipt)
-                  (pointCycleBitWidth nextPoints)
-            }
-        )
-
-clipEndpointEdge
-  :: ExactClosedHalfPlane
-  -> (ExactPoint, ExactPoint)
-  -> Either EndpointOracleError EndpointEdgeClip
-clipEndpointEdge halfPlane (from, to) =
-  let fromInside = classifyExactPoint halfPlane from /= LT
-      toInside = classifyExactPoint halfPlane to /= LT
-      crossing = do
-        reconstructedBoundary <-
-          either
-            (Left . EndpointOracleHalfPlane)
-            (Right . exactClosedHalfPlaneLine)
-            (exactClosedHalfPlaneFromDirectedEdge from to)
-        point <-
-          either
-            (Left . EndpointOracleIntersection)
-            Right
-            ( exactAffineLineIntersection
-                reconstructedBoundary
-                (exactClosedHalfPlaneLine halfPlane)
-            )
-        pure (point, affineLineBitWidth reconstructedBoundary)
-   in case (fromInside, toInside) of
-    (True, True) -> Right (EndpointEdgeClip [to] Nothing)
-    (True, False) ->
-      (\(point, coefficientBits) ->
-         EndpointEdgeClip [point] (Just coefficientBits))
-        <$> crossing
-    (False, True) ->
-      (\(point, coefficientBits) ->
-         EndpointEdgeClip [point, to] (Just coefficientBits))
-        <$> crossing
-    (False, False) -> Right (EndpointEdgeClip [] Nothing)
-
-assertPrefixAgreement
-  :: ExactRetainedPolygon
-  -> [ExactClosedHalfPlane]
-  -> IO ()
-assertPrefixAgreement initialPolygon prefix = do
-  (retainedDisposition, retainedReceipt) <-
-    requireRight
-      "retained-line prefix"
-      (exactClipRetainedPolygon initialPolygon prefix)
-  retainedPoints <-
-    case retainedDisposition of
-      ExactClipFullDimensional polygon -> pure (exactRetainedPolygonPoints polygon)
-      other -> fail ("retained-line prefix lost full dimension: " <> show other)
-  (endpointPoints, endpointReceipt) <-
-    requireRight
-      "endpoint prefix"
-      (endpointClipSequence (exactRetainedPolygonPoints initialPolygon) prefix)
-  assertExactPointSet "prefix exact polygon" retainedPoints endpointPoints
-  assertEqual
-    "prefix denominator width"
-    (exactClipFinalDenominatorBits retainedReceipt)
-    (endpointFinalDenominatorBits endpointReceipt)
-
-fixtureHalfPlane
-  :: (Integer, Integer, Integer)
-  -> IO ExactClosedHalfPlane
-fixtureHalfPlane (coefficientX, coefficientY, constant) =
-  exactClosedHalfPlane
-    <$> requireRight
-      "fixture affine half-plane"
-      ( exactAffineLine
-          (fromInteger coefficientX)
-          (fromInteger coefficientY)
-          (fromInteger constant)
-      )
-
-fixtureCoefficients :: [(Integer, Integer, Integer)]
-fixtureCoefficients =
-  [ (1, 2, 1300)
-  , (-3, 5, 1700)
-  , (-7, -2, 1900)
-  , (4, -9, 2200)
-  , (11, 3, 1800)
-  , (-5, 13, 2100)
-  , (-17, -4, 2300)
-  , (6, -19, 2400)
-  , (23, 7, 2500)
-  , (-8, 29, 2600)
-  ]
-
-affineLineBitWidth :: ExactAffineLine -> Int
-affineLineBitWidth line =
-  let (coefficientX, coefficientY, constant) = exactAffineLineCoefficients line
-   in maximumOrZero (fmap exactRationalBitWidth [coefficientX, coefficientY, constant])
-
-pointCycleBitWidth :: NonEmpty ExactPoint -> Int
-pointCycleBitWidth =
-  maximumOrZero
-    . concatMap
-      (\point ->
-         let (x, y) = exactPointCoordinates point
-          in fmap exactRationalBitWidth [x, y])
-
-pointCycleDenominatorBitWidth :: NonEmpty ExactPoint -> Int
-pointCycleDenominatorBitWidth =
-  maximumOrZero
-    . concatMap
-      (\point ->
-         let (x, y) = exactPointCoordinates point
-          in fmap exactRationalDenominatorBitWidth [x, y])
-
-maximumOrZero :: Foldable collection => collection Int -> Int
-maximumOrZero = List.foldl' max 0
diff --git a/test/algebra/Moonlight/Triangulation/LayerOperationsSpec.hs b/test/algebra/Moonlight/Triangulation/LayerOperationsSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/LayerOperationsSpec.hs
+++ /dev/null
@@ -1,275 +0,0 @@
--- | Exact affine-envelope and compositional layer laws through the public
--- facade. Every comparison is against an independently composed public view.
-module Moonlight.Triangulation.LayerOperationsSpec (tests) where
-
-import Control.Monad (unless)
-import Data.Foldable (traverse_)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import Moonlight.Triangulation
-  ( AffineForm (..)
-  , CoverGap
-  , ExactPoint
-  , ExactRational
-  , LayerCoverageError (..)
-  , PlanarLayer
-  , PlanarRegion
-  , PolygonComponent
-  , RegionPublicationError (RegionUnboundedSelection)
-  , UpperEnvelopeError (UpperEnvelopeEmptyForms)
-  , coverGapRegion
-  , exactAreaValue
-  , exactLoop
-  , exactLoopPoints
-  , exactPoint
-  , exactPointCoordinates
-  , layerCovers
-  , overlayAll
-  , overlayConfusion
-  , overlayLayers
-  , overlayMass
-  , overlaySelectedRegion
-  , planarLayer
-  , planarLayerLabelAt
-  , planarLayerRegions
-  , planarRegion
-  , planarRegionComponents
-  , polygonComponent
-  , polygonHoleLoops
-  , polygonOuterLoop
-  , regionValuations
-  , upperEnvelope
-  , valuationArea
-  )
-import Moonlight.Triangulation.AlgebraFixtures
-  ( insideLayer
-  , rectangleComponent
-  , rectangleRegion
-  )
-import Support (assertEqual, integerPoint, requireRight)
-
-tests :: IO ()
-tests = do
-  testConvexUpperEnvelope
-  testNonconvexAndHoledUpperEnvelope
-  testEmptyUpperEnvelope
-  testLayerCoverage
-  testNaryOverlayLabels
-  testOverlayMassAndConfusion
-  putStrLn "layer operations: ok"
-
-testConvexUpperEnvelope :: IO ()
-testConvexUpperEnvelope = do
-  window <- rectangleComponent 0 0 10 10
-  let forms =
-        Map.fromList
-          [ (0 :: Int, AffineForm 0 (-1) 0)
-          , (1, AffineForm 0 (-1) 0)
-          , (2, AffineForm (-10) 1 0)
-          , (3, AffineForm (-11) 1 0)
-          ]
-  layer <- requireRight "convex exact upper envelope" (upperEnvelope window forms)
-  reordered <-
-    requireRight
-      "upper envelope is independent of map construction order"
-      (upperEnvelope window (Map.fromList (reverse (Map.toList forms))))
-  assertEqual "upper envelope map-order invariance" layer reordered
-  assertEqual "identical and dominated forms own no cell" [Just 0, Just 2] (Map.keys (planarLayerRegions layer))
-  assertEnvelopeDominance forms layer
-  assertEqual "left affine winner" (Just 0) (planarLayerLabelAt layer (exactPoint 2 5))
-  assertEqual "right affine winner" (Just 2) (planarLayerLabelAt layer (exactPoint 8 5))
-  assertEqual "outside finite window" Nothing (planarLayerLabelAt layer (exactPoint (-1) 5))
-  requireCoverage "upper envelope covers its convex window" layer window
-
-testNonconvexAndHoledUpperEnvelope :: IO ()
-testNonconvexAndHoledUpperEnvelope = do
-  nonconvex <- lShapedComponent
-  holed <- holedComponent
-  let constantForm = Map.singleton (7 :: Int) (AffineForm 1 0 0)
-  assertEnvelopeEqualsWindow "nonconvex envelope restriction" nonconvex constantForm
-  assertEnvelopeEqualsWindow "holed envelope restriction" holed constantForm
-
-testEmptyUpperEnvelope :: IO ()
-testEmptyUpperEnvelope = do
-  window <- rectangleComponent 0 0 1 1
-  case upperEnvelope window (Map.empty :: Map.Map Int AffineForm) of
-    Left UpperEnvelopeEmptyForms -> pure ()
-    result -> fail ("empty upper envelope: unexpected result " <> show result)
-
-testLayerCoverage :: IO ()
-testLayerCoverage = do
-  window <- rectangleComponent 0 0 10 10
-  half <- rectangleRegion 0 0 5 10
-  incomplete <- insideLayer half
-  case layerCovers incomplete window of
-    Left (LayerCoverageGap gap) -> assertGapArea 50 gap
-    result -> fail ("incomplete layer coverage: unexpected result " <> show result)
-  completeRegion <- requireRight "complete coverage region" (planarRegion [window])
-  complete <- requireRight "complete coverage layer" (planarLayer False (Map.singleton True completeRegion))
-  requireCoverage "complete layer" complete window
-
-testNaryOverlayLabels :: IO ()
-testNaryOverlayLabels = do
-  vertical <- rectangleRegion 0 0 6 10 >>= insideLayer
-  horizontal <- rectangleRegion 0 0 10 6 >>= insideLayer
-  central <- rectangleRegion 2 2 8 8 >>= insideLayer
-  let sourceLayers = vertical :| [horizontal, central]
-  refined <- requireRight "balanced n-ary overlay" (overlayAll sourceLayers)
-  assertNaryLabel sourceLayers refined (exactPoint 3 3)
-  assertNaryLabel sourceLayers refined (exactPoint 7 3)
-  assertNaryLabel sourceLayers refined (exactPoint 9 9)
-  assertNaryLabel sourceLayers refined (exactPoint (-1) (-1))
-  singleton <- requireRight "singleton n-ary overlay" (overlayAll (vertical :| []))
-  assertNaryLabel (vertical :| []) singleton (exactPoint 3 3)
-
-testOverlayMassAndConfusion :: IO ()
-testOverlayMassAndConfusion = do
-  left <- rectangleRegion 0 0 6 10 >>= insideLayer
-  right <- rectangleRegion 4 0 10 10 >>= insideLayer
-  result <- requireRight "mass overlay" (overlayLayers left right)
-  direct <- requireRight "direct overlap mass" (overlayMass (== (True, True)) result)
-  published <-
-    requireRight
-      "published overlap region"
-      (overlaySelectedRegion (== (True, True)) result)
-  publishedValues <- requireRight "published overlap valuation" (regionValuations published)
-  assertEqual
-    "direct mass equals published-region valuation"
-    (exactAreaValue (valuationArea publishedValues))
-    (exactAreaValue direct)
-  assertEqual "overlap mass" 20 (exactAreaValue direct)
-  case overlayMass (== (False, False)) result of
-    Left RegionUnboundedSelection -> pure ()
-    value -> fail ("unbounded overlay mass: unexpected result " <> show value)
-  assertEqual
-    "one-pass finite confusion masses"
-    ( Map.fromList
-        [ ((False, True), 40)
-        , ((True, False), 40)
-        , ((True, True), 20)
-        ]
-    )
-    (fmap exactAreaValue (overlayConfusion result))
-  traverse_
-    (\(labels, expected) -> do
-       measured <- requireRight "direct confusion-cell mass" (overlayMass (== labels) result)
-       assertEqual "confusion entry equals direct mass" expected (exactAreaValue measured))
-    (Map.toAscList (fmap exactAreaValue (overlayConfusion result)))
-
-assertEnvelopeDominance
-  :: Map.Map Int AffineForm
-  -> PlanarLayer (Maybe Int)
-  -> IO ()
-assertEnvelopeDominance forms layer =
-  traverse_ checkRegion (Map.toAscList (planarLayerRegions layer))
- where
-  checkRegion :: (Maybe Int, PlanarRegion) -> IO ()
-  checkRegion (Nothing, _) = fail "bounded upper-envelope region has the outside label"
-  checkRegion (Just winner, region) =
-    case Map.lookup winner forms of
-      Nothing -> fail ("upper-envelope winner is absent from its input map: " <> show winner)
-      Just winnerForm ->
-        traverse_
-          (checkWinnerAtPoint winner winnerForm)
-          (concatMap componentBoundaryPoints (planarRegionComponents region))
-
-  checkWinnerAtPoint
-    :: Int
-    -> AffineForm
-    -> ExactPoint
-    -> IO ()
-  checkWinnerAtPoint winner winnerForm point =
-    let winnerValue = affineValue winnerForm point
-     in traverse_
-          (\(candidate, candidateForm) ->
-             unless (winnerValue >= affineValue candidateForm point) $
-               fail
-                 ( "upper-envelope dominance failed: winner "
-                     <> show winner
-                     <> ", candidate "
-                     <> show candidate
-                 ))
-          (Map.toAscList forms)
-
-componentBoundaryPoints :: PolygonComponent -> [ExactPoint]
-componentBoundaryPoints component =
-  NonEmpty.toList (exactLoopPoints (polygonOuterLoop component))
-    <> concatMap (NonEmpty.toList . exactLoopPoints) (polygonHoleLoops component)
-
-affineValue
-  :: AffineForm
-  -> ExactPoint
-  -> ExactRational
-affineValue form point =
-  let (coordinateX, coordinateY) = exactPointCoordinates point
-   in affineFormConstant form
-        + affineFormXCoefficient form * coordinateX
-        + affineFormYCoefficient form * coordinateY
-
-assertEnvelopeEqualsWindow
-  :: String
-  -> PolygonComponent
-  -> Map.Map Int AffineForm
-  -> IO ()
-assertEnvelopeEqualsWindow label window forms = do
-  expected <- requireRight (label <> " expected region") (planarRegion [window])
-  layer <- requireRight label (upperEnvelope window forms)
-  assertEqual label (Just expected) (Map.lookup (Just 7) (planarLayerRegions layer))
-  requireCoverage (label <> " coverage") layer window
-
-assertGapArea :: Integer -> CoverGap -> IO ()
-assertGapArea expected gap = do
-  values <- requireRight "gap valuation" (regionValuations (coverGapRegion gap))
-  assertEqual "coverage obstruction retains exact gap" (fromInteger expected) (exactAreaValue (valuationArea values))
-
-assertNaryLabel
-  :: NonEmpty (PlanarLayer Bool)
-  -> PlanarLayer (NonEmpty Bool)
-  -> ExactPoint
-  -> IO ()
-assertNaryLabel sourceLayers refined point =
-  assertEqual
-    "n-ary label tuple follows source order"
-    (fmap (`planarLayerLabelAt` point) sourceLayers)
-    (planarLayerLabelAt refined point)
-
-requireCoverage
-  :: (Ord label, Show label)
-  => String
-  -> PlanarLayer label
-  -> PolygonComponent
-  -> IO ()
-requireCoverage label layer window =
-  case layerCovers layer window of
-    Right () -> pure ()
-    Left failure -> fail (label <> ": " <> show failure)
-
-lShapedComponent :: IO PolygonComponent
-lShapedComponent = do
-  outer <-
-    requireRight
-      "L-shaped outer loop"
-      ( exactLoop
-          ( integerPoint 0 0
-              :| [ integerPoint 4 0
-                 , integerPoint 4 1
-                 , integerPoint 1 1
-                 , integerPoint 1 4
-                 , integerPoint 0 4
-                 ]
-          )
-      )
-  requireRight "L-shaped component" (polygonComponent outer [])
-
-holedComponent :: IO PolygonComponent
-holedComponent = do
-  outer <-
-    requireRight
-      "holed outer loop"
-      (exactLoop (integerPoint 0 0 :| [integerPoint 6 0, integerPoint 6 6, integerPoint 0 6]))
-  hole <-
-    requireRight
-      "clockwise hole loop"
-      (exactLoop (integerPoint 2 2 :| [integerPoint 2 4, integerPoint 4 4, integerPoint 4 2]))
-  requireRight "holed component" (polygonComponent outer [hole])
diff --git a/test/algebra/Moonlight/Triangulation/MinkowskiSpec.hs b/test/algebra/Moonlight/Triangulation/MinkowskiSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/MinkowskiSpec.hs
+++ /dev/null
@@ -1,309 +0,0 @@
--- | Exact convex convolution and residual-morphology laws.
-module Moonlight.Triangulation.MinkowskiSpec (tests) where
-
-import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Triangulation.AlgebraFixtures
-  ( annulusRegion
-  , polygonRegion
-  , rectangleComponent
-  , rectangleRegion
-  )
-import Moonlight.Triangulation.Exact (ExactPoint, exactPoint)
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRational
-  )
-import Moonlight.Triangulation.Minkowski
-  ( MinkowskiOperation (..)
-  , closeWith
-  , convexMinkowskiSum
-  , convexPolygon
-  , convexPolygonPoints
-  , erodeBy
-  , minkowskiGeneratedPieces
-  , minkowskiExactCrossings
-  , minkowskiOperation
-  , minkowskiOverlayPasses
-  , minkowskiSum
-  , openWith
-  , polygonOffset
-  , reflectConvexPolygon
-  , structuringElement
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarRegion
-  , emptyPlanarRegion
-  , exactLoopPoints
-  , planarRegion
-  , planarRegionComponents
-  , polygonOuterLoop
-  )
-import Moonlight.Triangulation.Valuation
-  ( exactAreaValue
-  , regionValuations
-  , valuationArea
-  )
-import Support (assertEqual, requireRight)
-
-tests :: IO ()
-tests = do
-  testConvexConvolution
-  testConvexReflection
-  testGeneralAddition
-  testOffsetClosure
-  testConvexMorphology
-  testGeneralErosion
-  testHoledAndNeckedErosion
-  putStrLn "minkowski: ok"
-
-testConvexReflection :: IO ()
-testConvexReflection = do
-  triangle <-
-    requireRight
-      "asymmetric convex triangle"
-      (convexPolygon (exactPoint 0 0 :| [exactPoint 3 0, exactPoint 1 2]))
-  let reflected = reflectConvexPolygon triangle
-  readmitted <-
-    requireRight
-      "reflected convex polygon preserves counter-clockwise orientation"
-      (convexPolygon (convexPolygonPoints reflected))
-  assertEqual "reflected carrier remains admissible" reflected readmitted
-  assertEqual
-    "convex reflection is involutive"
-    triangle
-    (reflectConvexPolygon reflected)
-
-testConvexConvolution :: IO ()
-testConvexConvolution = do
-  leftComponent <- rectangleComponent 0 0 1 1
-  rightComponent <- rectangleComponent 0 0 1 1
-  left <-
-    requireRight
-      "left convex polygon"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop leftComponent)))
-  right <-
-    requireRight
-      "right convex polygon"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop rightComponent)))
-  let result = convexMinkowskiSum left right
-  assertRegionArea "convex square sum" 4 result
-  assertEqual
-    "convex convolution is commutative after canonical publication"
-    result
-    (convexMinkowskiSum right left)
-
-testGeneralAddition :: IO ()
-testGeneralAddition = do
-  first <- rectangleComponent 0 0 1 1
-  second <- rectangleComponent 3 0 4 1
-  disconnected <- requireRight "disconnected source" (planarRegion [first, second])
-  kernel <- rectangleRegion 0 0 1 1
-  (sumRegion, receipt) <-
-    requireRight "general disconnected sum" (minkowskiSum disconnected kernel)
-  assertEqual "general sum operation receipt" MinkowskiAddition (minkowskiOperation receipt)
-  assertEqual "general sum generated one piece per source component" 2 (minkowskiGeneratedPieces receipt)
-  assertEqual "general sum remains disconnected" 2 (length (planarRegionComponents sumRegion))
-  assertRegionArea "general disconnected sum" 8 sumRegion
-  (swapped, _) <- requireRight "swapped disconnected sum" (minkowskiSum kernel disconnected)
-  assertEqual "general Minkowski sum is commutative" sumRegion swapped
-  (annihilated, _) <-
-    requireRight "empty Minkowski annihilator" (minkowskiSum disconnected emptyPlanarRegion)
-  assertEqual "empty region annihilates Minkowski addition" emptyPlanarRegion annihilated
-  firstRegion <- requireRight "first distributive operand" (planarRegion [first])
-  secondRegion <- requireRight "second distributive operand" (planarRegion [second])
-  (firstSum, _) <- requireRight "first distributed sum" (minkowskiSum firstRegion kernel)
-  (secondSum, _) <- requireRight "second distributed sum" (minkowskiSum secondRegion kernel)
-  distributed <-
-    requireRight
-      "distributed union"
-      (planarRegion (planarRegionComponents firstSum <> planarRegionComponents secondSum))
-  assertEqual "Minkowski addition distributes over disjoint union" distributed sumRegion
-
-  associativityThird <- rectangleRegion (-2) 0 0 1
-  (leftPair, _) <- requireRight "associative left pair" (minkowskiSum firstRegion kernel)
-  (leftAssociated, _) <-
-    requireRight "left-associated Minkowski sum" (minkowskiSum leftPair associativityThird)
-  (rightPair, _) <- requireRight "associative right pair" (minkowskiSum kernel associativityThird)
-  (rightAssociated, _) <-
-    requireRight "right-associated Minkowski sum" (minkowskiSum firstRegion rightPair)
-  assertEqual "Minkowski addition is associative" leftAssociated rightAssociated
-  concave <-
-    polygonRegion
-      [ (0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3) ]
-  (concaveSum, concaveReceipt) <-
-    requireRight "concave triangulated sum" (minkowskiSum concave kernel)
-  expectedConcaveSum <-
-    polygonRegion
-      [ (0, 0), (4, 0), (4, 2), (2, 2), (2, 4), (0, 4) ]
-  assertEqual "triangulated nonconvex sum" expectedConcaveSum concaveSum
-  if minkowskiOverlayPasses concaveReceipt > 0
-    then pure ()
-    else fail "nonconvex addition bypassed CDT decomposition and overlay union"
-
--- Dilation must be closed over its own exact published image. This fixture
--- used to admit the first offset and then refuse the second when rounded
--- resident wedges were mistaken for exact two-cells.
-testOffsetClosure :: IO ()
-testOffsetClosure = do
-  source <-
-    polygonRegion
-      [ (-9, -1)
-      , (-5, -6)
-      , (0, -13)
-      , (7, -9)
-      , (9, 1)
-      , (10, 10)
-      , (5, 12)
-      , (-7, 8)
-      ]
-  shoulder <-
-    requireRight
-      "unit octagon shoulder"
-      (exactRational 1592262918131443 2251799813685248)
-  kernel <-
-    requireRight
-      "unit octagon kernel"
-      (convexPolygon (centeredOctagon 1 shoulder))
-  element <- requireRight "unit octagon element" (structuringElement kernel)
-  (firstOffset, _) <-
-    requireRight "first closure-regression offset" (polygonOffset element source)
-  (secondOffset, secondReceipt) <-
-    requireRight
-      "offset remains closed over its own published image"
-      (polygonOffset element firstOffset)
-  assertEqual
-    "offset-of-offset remains one full-dimensional component"
-    1
-    (length (planarRegionComponents secondOffset))
-  if minkowskiOverlayPasses secondReceipt > 0
-    then pure ()
-    else fail "offset closure regression did not exercise overlay descent"
-
-centeredOctagon :: ExactRational -> ExactRational -> NonEmpty ExactPoint
-centeredOctagon radius shoulder =
-  exactPoint radius 0
-    :| [ exactPoint shoulder shoulder
-       , exactPoint 0 radius
-       , exactPoint (-shoulder) shoulder
-       , exactPoint (-radius) 0
-       , exactPoint (-shoulder) (-shoulder)
-       , exactPoint 0 (-radius)
-       , exactPoint shoulder (-shoulder)
-       ]
-
-testConvexMorphology :: IO ()
-testConvexMorphology = do
-  source <- rectangleRegion (-2) (-2) 2 2
-  kernelComponent <- rectangleComponent (-1) (-1) 1 1
-  kernelPolygon <-
-    requireRight
-      "centred square kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
-  element <- requireRight "centred structuring element" (structuringElement kernelPolygon)
-  (eroded, erosionReceipt) <- requireRight "convex erosion" (erodeBy element source)
-  expectedErosion <- rectangleRegion (-1) (-1) 1 1
-  assertEqual "convex support-half-plane erosion" expectedErosion eroded
-  assertEqual "convex erosion needs no overlay" 0 (minkowskiOverlayPasses erosionReceipt)
-  assertEqual
-    "convex erosion reports exact HPI line intersections"
-    4
-    (minkowskiExactCrossings erosionReceipt)
-  exactFitSource <- rectangleRegion (-1) (-1) 1 1
-  (lowerDimensionalResidual, _) <-
-    requireRight "exact-fit regularized erosion" (erodeBy element exactFitSource)
-  assertEqual
-    "point-only erosion residual regularizes to the empty polygonal region"
-    emptyPlanarRegion
-    lowerDimensionalResidual
-  (emptyResidual, _) <- requireRight "empty source erosion" (erodeBy element emptyPlanarRegion)
-  assertEqual "empty source erodes to empty" emptyPlanarRegion emptyResidual
-  (expanded, _) <- requireRight "convex offset" (polygonOffset element source)
-  expectedExpansion <- rectangleRegion (-3) (-3) 3 3
-  assertEqual "convex offset" expectedExpansion expanded
-  (opened, _) <- requireRight "convex opening" (openWith element source)
-  assertEqual "convex opening is idempotent on the square" source opened
-  (openedTwice, _) <- requireRight "second convex opening" (openWith element opened)
-  assertEqual "opening is idempotent" opened openedTwice
-  (closed, _) <- requireRight "convex closing" (closeWith element source)
-  assertEqual "convex closing is idempotent on the square" source closed
-  (closedTwice, _) <- requireRight "second convex closing" (closeWith element closed)
-  assertEqual "closing is idempotent" closed closedTwice
-  cornerKernelComponent <- rectangleComponent 0 0 1 1
-  cornerKernel <-
-    requireRight
-      "corner-anchored kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop cornerKernelComponent)))
-  cornerElement <- requireRight "corner-anchored element" (structuringElement cornerKernel)
-  cornerSource <- rectangleRegion 0 0 4 4
-  (cornerErosion, _) <- requireRight "corner-anchored erosion" (erodeBy cornerElement cornerSource)
-  expectedCornerErosion <- rectangleRegion 0 0 3 3
-  assertEqual "kernel origin controls erosion translation" expectedCornerErosion cornerErosion
-
-testGeneralErosion :: IO ()
-testGeneralErosion = do
-  left <- rectangleComponent 0 0 4 4
-  right <- rectangleComponent 6 0 10 4
-  source <- requireRight "two-component erosion source" (planarRegion [left, right])
-  kernelComponent <- rectangleComponent (-1) (-1) 1 1
-  kernelPolygon <-
-    requireRight
-      "general erosion kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
-  element <- requireRight "general erosion element" (structuringElement kernelPolygon)
-  (eroded, receipt) <- requireRight "general erosion" (erodeBy element source)
-  expectedLeft <- rectangleComponent 1 1 3 3
-  expectedRight <- rectangleComponent 7 1 9 3
-  expected <- requireRight "expected general erosion" (planarRegion [expectedLeft, expectedRight])
-  assertEqual "general residual erosion" expected eroded
-  if minkowskiOverlayPasses receipt > 0
-    then pure ()
-    else fail "general erosion bypassed the overlay candidate arrangement"
-
-testHoledAndNeckedErosion :: IO ()
-testHoledAndNeckedErosion = do
-  kernelComponent <- rectangleComponent (-1) (-1) 1 1
-  kernelPolygon <-
-    requireRight
-      "holed erosion kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
-  element <- requireRight "holed erosion element" (structuringElement kernelPolygon)
-  sourceAnnulus <- annulusRegion (0, 0, 10, 10) (4, 4, 6, 6)
-  expectedAnnulus <- annulusRegion (1, 1, 9, 9) (3, 3, 7, 7)
-  (erodedAnnulus, _) <- requireRight "annulus erosion" (erodeBy element sourceAnnulus)
-  assertEqual "erosion shrinks the outer boundary and expands holes" expectedAnnulus erodedAnnulus
-
-  necked <-
-    polygonRegion
-      [ (0, 0)
-      , (8, 0)
-      , (8, 3)
-      , (12, 3)
-      , (12, 0)
-      , (20, 0)
-      , (20, 8)
-      , (12, 8)
-      , (12, 5)
-      , (8, 5)
-      , (8, 8)
-      , (0, 8)
-      ]
-  wideKernelComponent <- rectangleComponent (-2) (-2) 2 2
-  wideKernelPolygon <-
-    requireRight
-      "neck erosion kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop wideKernelComponent)))
-  wideElement <- requireRight "neck erosion element" (structuringElement wideKernelPolygon)
-  (separated, _) <- requireRight "narrow-neck erosion" (erodeBy wideElement necked)
-  expectedLeft <- rectangleComponent 2 2 6 6
-  expectedRight <- rectangleComponent 14 2 18 6
-  expectedSeparated <-
-    requireRight "expected separated erosion" (planarRegion [expectedLeft, expectedRight])
-  assertEqual "erosion removes a neck narrower than the kernel" expectedSeparated separated
-
-assertRegionArea :: String -> Integer -> PlanarRegion -> IO ()
-assertRegionArea label expected region = do
-  valuations <- requireRight label (regionValuations region)
-  assertEqual
-    label
-    (fromInteger expected)
-    (exactAreaValue (valuationArea valuations))
diff --git a/test/algebra/Moonlight/Triangulation/PowerDiagramSpec.hs b/test/algebra/Moonlight/Triangulation/PowerDiagramSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/PowerDiagramSpec.hs
+++ /dev/null
@@ -1,1282 +0,0 @@
--- | Exact bounded power-cell laws through the public owner.
-module Moonlight.Triangulation.PowerDiagramSpec (tests) where
-
-import Control.Monad (unless)
-import Data.Foldable (traverse_)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.List.NonEmpty as NonEmpty
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Vector as Vector
-import Moonlight.Triangulation
-  ( ExactPoint
-  , ExactRational
-  , ExactVector (..)
-  , BoundedPowerDiagram
-  , ConvexPolygon
-  , Point (..)
-  , PowerCellDisposition (..)
-  , PowerDualEdge (..)
-  , PowerDiagramError (..)
-  , PowerSite
-  , PowerWeight
-  , PowerWeightError (..)
-  , powerAlphaBirthExact
-  , RegularEditError (..)
-  , RegularEditResult (..)
-  , RegularSiteDisposition (..)
-  , RegularSiteTransition (..)
-  , RegularEdge
-  , RegularTriangulation
-  , boundedPowerDiagram
-  , boundedPowerDiagramFromRegular
-  , convexPolygon
-  , convexPolygonPoints
-  , exactPointCoordinates
-  , exactPointFromPoint
-  , exactRayDirection
-  , exactRayOrigin
-  , exactSegmentEndpoints
-  , planarLayerRegions
-  , powerCellDisposition
-  , powerCellDispositions
-  , powerDiagramActiveBoundaries
-  , powerDiagramCoincidentEquivalentCells
-  , powerDiagramCoincidentDominatedCells
-  , powerDiagramEmptyCells
-  , powerDiagramInputSites
-  , powerDiagramLowerDimensionalCells
-  , powerDiagramPlanarLayer
-  , powerDiagramPublishedCells
-  , powerDiagramOracleCells
-  , powerDiagramMaximumCellConstraints
-  , powerDiagramRegularEdges
-  , powerDiagramSubmittedSiteConstraints
-  , powerSite
-  , powerSiteLabel
-  , powerSitePosition
-  , powerSiteWeight
-  , powerWeight
-  , powerWeightExact
-  , regularAlphaBirths
-  , regularAlphaComplex
-  , regularAlphaComplexAtBirth
-  , regularAlphaFiltration
-  , emptyRegularTriangulation
-  , insertRegularSite
-  , regularEdgeDual
-  , regularEdgeLabels
-  , regularEdges
-  , regularFaceDualPoint
-  , regularFaceLabels
-  , regularFaces
-  , regularNeighbours
-  , regularSite
-  , regularSiteCount
-  , regularSiteDisposition
-  , regularSites
-  , regularTriangulation
-  , regularTriangulationReceipt
-  , regularTriangulationEdges
-  , regularTriangulationFaces
-  , regularTriangulationInputSites
-  , regularTriangulationVisibleSites
-  , removeRegularSite
-  , reweightRegularSites
-  , translateExactPoint
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactClipDisposition (..)
-  , ExactClosedHalfPlane
-  , exactAffineLine
-  , exactClipRetainedPolygon
-  , exactClosedHalfPlane
-  , exactRetainedPolygon
-  , exactRetainedPolygonPoints
-  )
-import Moonlight.Triangulation.Simplex
-  ( planarComplexCells
-  , planarEdge
-  , planarFace
-  , planarVertex
-  )
-import Support (assertEqual, integerPoint, requireRight)
-
-tests :: IO ()
-tests = do
-  testEqualWeightsAndCommonShift
-  testThreeSiteExactPartitionCoverage
-  testRegularTriangleDualRays
-  testRegularAlphaBirths
-  testRegularAlphaCommonShift
-  testRegularBoundedDualSegment
-  testRegularCoplanarUpperFacet
-  testRegularCollinearClassification
-  testRegularLowerDimensionalAndHiddenSites
-  testRegularSiteOwnership
-  testHiddenInsertionAndExposure
-  testRegularInsertionAndRemovalTransitions
-  testRegularReweightTransitions
-  testTopologyPreservingEdits
-  testRegularEditDifferentialLaws
-  testRegularEditObstructionsAndIdempotence
-  testBoundedPowerFromRegular
-  testSparsePowerMatchesCompleteOracle
-  testEqualCoincidentResolution
-  testDominantCoincidentResolution
-  testLowerDimensionalCell
-  testDistinctEmptyCell
-  testDyadicNearParallelBisectors
-  testBinary64PrecisionSites
-  testDuplicateLabels
-  testNonFiniteWeight
-  putStrLn "power diagram: ok"
-
-testRegularAlphaBirths :: IO ()
-testRegularAlphaBirths = do
-  positive <- admittedWeight 3
-  singleton <- admittedSite "only" (Point 0 0) positive
-  (singleRegular, _) <-
-    requireRight "singleton regular topology" (regularTriangulation (singleton :| []))
-  singleAlpha <- requireRight "singleton weighted alpha" (regularAlphaFiltration singleRegular)
-  assertEqual
-    "positive weight gives signed vertex birth"
-    (Just (-3))
-    (fmap powerAlphaBirthExact (Map.lookup (planarVertex "only") (regularAlphaBirths singleAlpha)))
-
-  zero <- admittedWeight 0
-  firstSite <- admittedSite "first" (Point 0 0) zero
-  secondSite <- admittedSite "second" (Point 2 0) zero
-  thirdSite <- admittedSite "third" (Point 0 2) zero
-  dominated <- admittedSite "dominated" (Point 0 0) =<< admittedWeight (-1)
-  (regular, _) <-
-    requireRight
-      "triangle regular topology for weighted alpha"
-      (regularTriangulation (firstSite :| [secondSite, thirdSite, dominated]))
-  filtration <- requireRight "triangle weighted alpha" (regularAlphaFiltration regular)
-  firstSecond <- requireRight "first-second simplex" (planarEdge "first" "second")
-  firstThird <- requireRight "first-third simplex" (planarEdge "first" "third")
-  secondThird <- requireRight "second-third simplex" (planarEdge "second" "third")
-  face <- requireRight "triangle simplex" (planarFace "first" "second" "third")
-  let birthAt simplex = fmap powerAlphaBirthExact (Map.lookup simplex (regularAlphaBirths filtration))
-  assertEqual "first leg birth" (Just 1) (birthAt firstSecond)
-  assertEqual "second leg birth" (Just 1) (birthAt firstThird)
-  assertEqual "hypotenuse birth" (Just 2) (birthAt secondThird)
-  assertEqual "face birth" (Just 2) (birthAt face)
-  assertEqual
-    "coincident subordinate has no alpha simplex"
-    False
-    (Set.member (planarVertex "dominated") (planarComplexCells (regularAlphaComplex filtration)))
-  threshold <- requireSome "edge threshold" (Map.lookup firstSecond (regularAlphaBirths filtration))
-  _ <- requireRight "weighted alpha sublevel is closed" (regularAlphaComplexAtBirth threshold filtration)
-  pure ()
-
-testRegularAlphaCommonShift :: IO ()
-testRegularAlphaCommonShift = do
-  zero <- admittedWeight 0
-  shifted <- admittedWeight 3
-  let labelledPoints =
-        ("south-west", Point 0 0)
-          :| [ ("south-east", Point 4 0)
-             , ("north-east", Point 3 3)
-             , ("north-west", Point 0 4)
-             ]
-  baseSites <- traverse (\(label, point) -> admittedSite label point zero) labelledPoints
-  shiftedSites <- traverse (\(label, point) -> admittedSite label point shifted) labelledPoints
-  (baseRegular, _) <- requireRight "base regular alpha topology" (regularTriangulation baseSites)
-  (shiftedRegular, _) <- requireRight "shifted regular alpha topology" (regularTriangulation shiftedSites)
-  shiftedEdit <-
-    requireRight
-      "common regular weight shift"
-      ( reweightRegularSites
-          (Map.fromList [(label, shifted) | (label, _) <- NonEmpty.toList labelledPoints])
-          baseRegular
-      )
-  assertEqual
-    "common shift reuses the regular topology"
-    shiftedRegular
-    (regularEditTriangulation shiftedEdit)
-  assertEqual "common shift changes no disposition" [] (editTransitions shiftedEdit)
-  baseAlpha <- requireRight "base regular alpha" (regularAlphaFiltration baseRegular)
-  shiftedAlpha <- requireRight "shifted regular alpha" (regularAlphaFiltration shiftedRegular)
-  assertEqual
-    "common weight shift preserves the complex"
-    (regularAlphaComplex baseAlpha)
-    (regularAlphaComplex shiftedAlpha)
-  assertEqual
-    "common weight shift subtracts from every birth"
-    (Map.map ((\birth -> birth - 3) . powerAlphaBirthExact) (regularAlphaBirths baseAlpha))
-    (Map.map powerAlphaBirthExact (regularAlphaBirths shiftedAlpha))
-
-testRegularTriangleDualRays :: IO ()
-testRegularTriangleDualRays = do
-  zero <- admittedWeight 0
-  firstSite <- admittedSite "first" (Point 0 0) zero
-  secondSite <- admittedSite "second" (Point 2 0) zero
-  thirdSite <- admittedSite "third" (Point 0 2) zero
-  let sites = firstSite :| [secondSite, thirdSite]
-  (regular, receipt) <-
-    requireRight "three-site regular topology" (regularTriangulation sites)
-  assertEqual "three visible regular sites" 3 (regularTriangulationVisibleSites receipt)
-  assertEqual "one regular face" 1 (length (regularFaces regular))
-  assertEqual "three regular boundary edges" 3 (length (regularEdges regular))
-  dualPoint <-
-    case regularFaces regular of
-      [face] -> pure (regularFaceDualPoint face)
-      faces -> fail ("three-site topology expected one face, got " <> show (length faces))
-  traverse_ (assertBoundaryDualRay sites dualPoint) (regularEdges regular)
-  traverse_
-    (\site ->
-       let label = powerSiteLabel site
-        in do
-          assertEqual
-            ("visible regular disposition for " <> label)
-            (Just RegularSiteVisible)
-            (regularSiteDisposition label regular)
-          assertEqual
-            ("two regular neighbours for " <> label)
-            2
-            (Set.size (regularNeighbours label regular)))
-    sites
-
-testRegularBoundedDualSegment :: IO ()
-testRegularBoundedDualSegment = do
-  zero <- admittedWeight 0
-  sites <-
-    traverse
-      (\(label, point) -> admittedSite label point zero)
-      ( ("south-west", Point 0 0)
-          :| [ ("south-east", Point 4 0)
-             , ("north-east", Point 3 3)
-             , ("north-west", Point 0 4)
-             ]
-      )
-  (regular, receipt) <-
-    requireRight "four-site regular topology" (regularTriangulation sites)
-  assertEqual
-    "regular planar Euler equation"
-    1
-    ( regularTriangulationVisibleSites receipt
-        - regularTriangulationEdges receipt
-        + regularTriangulationFaces receipt
-    )
-  let faceDuals = Set.fromList (fmap regularFaceDualPoint (regularFaces regular))
-      bounded =
-        [ segment
-        | edge <- regularEdges regular
-        , BoundedPowerDual segment <- [regularEdgeDual edge]
-        ]
-  case bounded of
-    [segment] ->
-      let (firstEndpoint, secondEndpoint) = exactSegmentEndpoints segment
-       in unless
-            (Set.member firstEndpoint faceDuals && Set.member secondEndpoint faceDuals)
-            (fail "bounded regular dual does not join its two incident face duals")
-    segments ->
-      fail ("four-site topology expected one bounded dual, got " <> show (length segments))
-
-testRegularCoplanarUpperFacet :: IO ()
-testRegularCoplanarUpperFacet = do
-  sites <-
-    traverse
-      prepareOracleSite
-      ( ("a-bottom", Point 0.25 0.25, -0.875)
-          :| [ ("b-south-west", Point 0 0, 0)
-             , ("c-south-east", Point 1 0, 1)
-             , ("d-center", Point 0.5 0.5, 0.5)
-             , ("e-north-east", Point 1 1, 2)
-             , ("f-north-west", Point 0 1, 1)
-             ]
-      )
-  (regular, receipt) <-
-    requireRight "coplanar upper regular facet" (regularTriangulation sites)
-  traverse_
-    (\label ->
-       assertEqual
-         ("upper-facet vertex remains visible: " <> label)
-         (Just RegularSiteVisible)
-         (regularSiteDisposition label regular))
-    ["b-south-west", "c-south-east", "e-north-east", "f-north-west"]
-  assertEqual
-    "upper-facet interior generator remains lower-dimensional"
-    (Just RegularSiteLowerDimensional)
-    (regularSiteDisposition "d-center" regular)
-  assertEqual
-    "strictly lower lifted generator remains hidden"
-    (Just RegularSiteHidden)
-    (regularSiteDisposition "a-bottom" regular)
-  assertEqual "coplanar upper facet has four visible vertices" 4 (regularTriangulationVisibleSites receipt)
-  assertEqual "coplanar upper facet receives one deterministic diagonal" 2 (regularTriangulationFaces receipt)
-
-testRegularLowerDimensionalAndHiddenSites :: IO ()
-testRegularLowerDimensionalAndHiddenSites = do
-  domain <- squareDomain
-  zero <- admittedWeight 0
-  lowerWeight <- admittedWeight (-1.5)
-  hiddenWeight <- admittedWeight (-2)
-  firstSite <- admittedSite "first" (Point 0 0) zero
-  secondSite <- admittedSite "second" (Point 2 0) zero
-  thirdSite <- admittedSite "third" (Point 0 2) zero
-  lowerSite <- admittedSite "center" (Point 0.5 0.5) lowerWeight
-  hiddenSite <- admittedSite "center" (Point 0.5 0.5) hiddenWeight
-  let lowerSites = firstSite :| [secondSite, thirdSite, lowerSite]
-      hiddenSites = firstSite :| [secondSite, thirdSite, hiddenSite]
-  (lowerRegular, _) <-
-    requireRight "coplanar regular topology" (regularTriangulation lowerSites)
-  assertEqual
-    "coplanar interior generator remains lower-dimensional"
-    (Just RegularSiteLowerDimensional)
-    (regularSiteDisposition "center" lowerRegular)
-  (lowerDiagram, lowerReceipt) <-
-    requireRight "bounded lower-dimensional power cell" (boundedPowerDiagram domain lowerSites)
-  case powerCellDisposition "center" lowerDiagram of
-    Just (LowerDimensionalPowerCell _) -> pure ()
-    other -> fail ("expected lower-dimensional center cell, got " <> dispositionTag other)
-  assertEqual "one lower-dimensional oracle cell" 1 (powerDiagramOracleCells lowerReceipt)
-  (hiddenRegular, _) <-
-    requireRight "hidden regular topology" (regularTriangulation hiddenSites)
-  assertEqual
-    "strictly interior lifted generator is hidden"
-    (Just RegularSiteHidden)
-    (regularSiteDisposition "center" hiddenRegular)
-  (hiddenDiagram, hiddenReceipt) <-
-    requireRight "bounded hidden power cell" (boundedPowerDiagram domain hiddenSites)
-  assertEqual "hidden generator has empty cell" (Just EmptyPowerCell) (powerCellDisposition "center" hiddenDiagram)
-  assertEqual "hidden generator needs no HPI oracle" 0 (powerDiagramOracleCells hiddenReceipt)
-  unless
-    ( powerDiagramMaximumCellConstraints hiddenReceipt <= powerDiagramRegularEdges hiddenReceipt
-    )
-    (fail "power construction retained more axes than the regular graph")
-
-testRegularSiteOwnership :: IO ()
-testRegularSiteOwnership = do
-  zero <- admittedWeight 0
-  sites <-
-    traverse
-      (\(label, point) -> admittedSite label point zero)
-      ( ("c", Point 0 2)
-          :| [("a", Point 0 0), ("b", Point 2 0)]
-      )
-  (regular, receipt) <-
-    requireRight "site-owning regular topology" (regularTriangulation sites)
-  assertEqual "regular site count" 3 (regularSiteCount regular)
-  assertEqual
-    "regular sites are the canonical ascending section"
-    ["a", "b", "c"]
-    (fmap powerSiteLabel (regularSites regular))
-  traverse_
-    (\site ->
-       assertEqual
-         ("regular site lookup for " <> powerSiteLabel site)
-         (Just site)
-         (regularSite (powerSiteLabel site) regular))
-    sites
-  case NonEmpty.nonEmpty (regularSites regular) of
-    Nothing -> fail "nonempty regular topology lost its canonical site section"
-    Just retainedSites -> do
-      (reconstructed, _) <-
-        requireRight "regular reconstruction from owned sites" (regularTriangulation retainedSites)
-      assertEqual "owned sites reconstruct the same semantic value" regular reconstructed
-  assertEqual "receipt counts every owned site" 3 (regularTriangulationInputSites receipt)
-  assertEqual "empty regular site count" 0 (regularSiteCount emptyRegularTriangulation)
-  assertEqual
-    "empty regular site section"
-    ([] :: [PowerSite String])
-    (regularSites emptyRegularTriangulation)
-
-testHiddenInsertionAndExposure :: IO ()
-testHiddenInsertionAndExposure = do
-  zero <- admittedWeight 0
-  hiddenWeight <- admittedWeight (-2)
-  first <- admittedSite "first" (Point 0 0) zero
-  second <- admittedSite "second" (Point 2 0) zero
-  third <- admittedSite "third" (Point 0 2) zero
-  hidden <- admittedSite "hidden" (Point 0.5 0.5) hiddenWeight
-  (initial, _) <-
-    requireRight
-      "regular hidden-insertion base"
-      (regularTriangulation (first :| [second, third]))
-  inserted <-
-    requireRight "insert hidden regular site" (insertRegularSite hidden initial)
-  assertEqual
-    "hidden insertion is successful resident publication"
-    (Just hidden)
-    (regularSite "hidden" (regularEditTriangulation inserted))
-  assertEqual
-    "hidden insertion disposition"
-    [RegularSiteAppeared "hidden" RegularSiteHidden]
-    (editTransitions inserted)
-  removed <-
-    requireRight
-      "remove face-defining site"
-      (removeRegularSite "first" (regularEditTriangulation inserted))
-  assertEqual
-    "removal re-exposes a hidden resident"
-    (Just RegularSiteVisible)
-    (regularSiteDisposition "hidden" (regularEditTriangulation removed))
-  assertEqual
-    "hidden exposure receipt"
-    [ RegularSiteDisappeared "first" RegularSiteVisible
-    , RegularSiteTransitioned "hidden" RegularSiteHidden RegularSiteVisible
-    ]
-    (editTransitions removed)
-
-testRegularInsertionAndRemovalTransitions :: IO ()
-testRegularInsertionAndRemovalTransitions = do
-  zero <- admittedWeight 0
-  dominantWeight <- admittedWeight 1
-  resident <- admittedSite "a-resident" (Point 0 0) zero
-  dominant <- admittedSite "z-dominant" (Point 0 0) dominantWeight
-  (initial, _) <-
-    requireRight "regular insertion base" (regularTriangulation (resident :| []))
-  inserted <-
-    requireRight "dominant regular insertion" (insertRegularSite dominant initial)
-  assertEqual
-    "insert changed-site support"
-    (Set.singleton "z-dominant")
-    (regularEditChangedSites inserted)
-  assertEqual
-    "dominant insertion transitions"
-    [ RegularSiteTransitioned
-        "a-resident"
-        RegularSiteVisible
-        (RegularSiteCoincidentDominatedBy "z-dominant")
-    , RegularSiteAppeared "z-dominant" RegularSiteVisible
-    ]
-    (editTransitions inserted)
-  removed <-
-    requireRight
-      "dominant regular removal"
-      (removeRegularSite "z-dominant" (regularEditTriangulation inserted))
-  assertEqual
-    "remove changed-site support"
-    (Set.singleton "z-dominant")
-    (regularEditChangedSites removed)
-  assertEqual
-    "dominant removal re-exposes resident"
-    [ RegularSiteTransitioned
-        "a-resident"
-        (RegularSiteCoincidentDominatedBy "z-dominant")
-        RegularSiteVisible
-    , RegularSiteDisappeared "z-dominant" RegularSiteVisible
-    ]
-    (editTransitions removed)
-  assertEqual
-    "insert then remove returns the canonical site value"
-    initial
-    (regularEditTriangulation removed)
-
-testRegularReweightTransitions :: IO ()
-testRegularReweightTransitions = do
-  dominantWeight <- admittedWeight 1
-  subordinateWeight <- admittedWeight 0
-  promotedWeight <- admittedWeight 2
-  first <- admittedSite "a" (Point 0 0) dominantWeight
-  second <- admittedSite "b" (Point 0 0) subordinateWeight
-  (initial, _) <-
-    requireRight "regular reweight base" (regularTriangulation (first :| [second]))
-  reweighted <-
-    requireRight
-      "regular batch reweight"
-      (reweightRegularSites (Map.singleton "b" promotedWeight) initial)
-  assertEqual
-    "reweight changed-site support"
-    (Set.singleton "b")
-    (regularEditChangedSites reweighted)
-  assertEqual
-    "reweight transitions preserve identities"
-    [ RegularSiteTransitioned
-        "a"
-        RegularSiteVisible
-        (RegularSiteCoincidentDominatedBy "b")
-    , RegularSiteTransitioned
-        "b"
-        (RegularSiteCoincidentDominatedBy "a")
-        RegularSiteVisible
-    ]
-    (editTransitions reweighted)
-  let revised = regularEditTriangulation reweighted
-  assertEqual
-    "reweight preserves the site position"
-    (Just (Point 0 0))
-    (powerSitePosition <$> regularSite "b" revised)
-  assertEqual
-    "reweight replaces only the requested weight"
-    (Just promotedWeight)
-    (powerSiteWeight <$> regularSite "b" revised)
-
-testTopologyPreservingEdits :: IO ()
-testTopologyPreservingEdits = do
-  domain <- squareDomain
-  zero <- admittedWeight 0
-  one <- admittedWeight 1
-  hiddenWeight <- admittedWeight (-2)
-  lowerHiddenWeight <- admittedWeight (-3)
-
-  representative <- admittedSite "a" (Point 0 0) one
-  subordinate <- admittedSite "b" (Point 0 0) zero
-  (coincidentBase, _) <-
-    requireRight "coincident edit base" (regularTriangulation (representative :| []))
-  inserted <-
-    requireRight
-      "canonical coincident insertion"
-      (insertRegularSite subordinate coincidentBase)
-  assertEqual
-    "coincident subordinate insertion transition"
-    [RegularSiteAppeared "b" (RegularSiteCoincidentDominatedBy "a")]
-    (editTransitions inserted)
-  assertRegularReconstructs domain "canonical coincident insertion" (regularEditTriangulation inserted)
-
-  reweighted <-
-    requireRight
-      "topology-preserving coincident reweight"
-      (reweightRegularSites (Map.singleton "b" one) (regularEditTriangulation inserted))
-  assertEqual
-    "coincident subordinate reweight transition"
-    [ RegularSiteTransitioned
-        "b"
-        (RegularSiteCoincidentDominatedBy "a")
-        (RegularSiteCoincidentEquivalentTo "a")
-    ]
-    (editTransitions reweighted)
-  assertRegularReconstructs domain "coincident reweight" (regularEditTriangulation reweighted)
-
-  removedSubordinate <-
-    requireRight
-      "topology-preserving coincident removal"
-      (removeRegularSite "b" (regularEditTriangulation reweighted))
-  assertEqual
-    "coincident subordinate removal transition"
-    [RegularSiteDisappeared "b" (RegularSiteCoincidentEquivalentTo "a")]
-    (editTransitions removedSubordinate)
-  assertRegularReconstructs domain "coincident removal" (regularEditTriangulation removedSubordinate)
-
-  first <- admittedSite "first" (Point 0 0) zero
-  second <- admittedSite "second" (Point 2 0) zero
-  third <- admittedSite "third" (Point 0 2) zero
-  hidden <- admittedSite "hidden" (Point 0.5 0.5) hiddenWeight
-  secondHidden <- admittedSite "hidden-second" (Point 0.75 0.5) hiddenWeight
-  (hiddenBase, _) <-
-    requireRight
-      "hidden edit base"
-      (regularTriangulation (first :| [second, third, hidden, secondHidden]))
-  lowered <-
-    requireRight
-      "topology-preserving hidden reweight"
-      ( reweightRegularSites
-          (Map.fromList [("hidden", lowerHiddenWeight), ("hidden-second", lowerHiddenWeight)])
-          hiddenBase
-      )
-  assertEqual
-    "hidden batch changed-site support"
-    (Set.fromList ["hidden", "hidden-second"])
-    (regularEditChangedSites lowered)
-  assertEqual "hidden downward reweight has no visibility transition" [] (editTransitions lowered)
-  assertRegularReconstructs domain "hidden downward reweight" (regularEditTriangulation lowered)
-
-  removedHidden <-
-    requireRight
-      "topology-preserving hidden removal"
-      (removeRegularSite "hidden" (regularEditTriangulation lowered))
-  assertEqual
-    "hidden removal transition"
-    [RegularSiteDisappeared "hidden" RegularSiteHidden]
-    (editTransitions removedHidden)
-  assertRegularReconstructs domain "hidden removal" (regularEditTriangulation removedHidden)
-
-assertRegularReconstructs
-  :: (Ord label, Show label)
-  => ConvexPolygon
-  -> String
-  -> RegularTriangulation label
-  -> IO ()
-assertRegularReconstructs domain name edited =
-  case NonEmpty.nonEmpty (regularSites edited) of
-    Nothing -> assertEqual (name <> " remains empty") 0 (regularSiteCount edited)
-    Just sites -> do
-      (rebuilt, _) <-
-        requireRight (name <> " reconstruction") (regularTriangulation sites)
-      let labels = fmap powerSiteLabel (NonEmpty.toList sites)
-      assertEqual
-        (name <> " receipt")
-        (regularTriangulationReceipt rebuilt)
-        (regularTriangulationReceipt edited)
-      assertEqual
-        (name <> " face labels")
-        (fmap regularFaceLabels (regularFaces rebuilt))
-        (fmap regularFaceLabels (regularFaces edited))
-      assertEqual (name <> " faces") (regularFaces rebuilt) (regularFaces edited)
-      assertEqual
-        (name <> " edge labels")
-        (fmap regularEdgeLabels (regularEdges rebuilt))
-        (fmap regularEdgeLabels (regularEdges edited))
-      assertEqual (name <> " edges") (regularEdges rebuilt) (regularEdges edited)
-      traverse_
-        (\label -> do
-           assertEqual
-             (name <> " disposition " <> show label)
-             (regularSiteDisposition label rebuilt)
-             (regularSiteDisposition label edited)
-           assertEqual
-             (name <> " neighbours " <> show label)
-             (regularNeighbours label rebuilt)
-             (regularNeighbours label edited))
-        labels
-      rebuiltDiagram <-
-        requireRight (name <> " rebuilt clipping") (boundedPowerDiagramFromRegular domain rebuilt)
-      editedDiagram <-
-        requireRight (name <> " edited clipping") (boundedPowerDiagramFromRegular domain edited)
-      assertEqual (name <> " bounded cells") rebuiltDiagram editedDiagram
-
-testRegularEditDifferentialLaws :: IO ()
-testRegularEditDifferentialLaws = do
-  domain <- squareDomain
-  sites <- traverse prepareDifferentialSite [0 .. 47]
-  nonEmptySites <-
-    maybe (fail "differential regular fixture is empty") pure (NonEmpty.nonEmpty sites)
-  baseSites <-
-    maybe (fail "differential insertion fixture is empty") pure
-      (NonEmpty.nonEmpty (NonEmpty.init nonEmptySites))
-  (base, _) <- requireRight "differential insertion base" (regularTriangulation baseSites)
-  inserted <-
-    requireRight
-      "differential local insertion"
-      (insertRegularSite (NonEmpty.last nonEmptySites) base)
-  assertRegularAlphaClosed
-    "differential weighted alpha"
-    (regularEditTriangulation inserted)
-  assertRegularReconstructs
-    domain
-    "differential insertion"
-    (regularEditTriangulation inserted)
-  removed <-
-    requireRight
-      "differential local removal"
-      (removeRegularSite "s23" (regularEditTriangulation inserted))
-  assertRegularReconstructs
-    domain
-    "differential removal"
-    (regularEditTriangulation removed)
-  raised <- admittedWeight 2
-  reweighted <-
-    requireRight
-      "differential local reweight"
-      (reweightRegularSites (Map.singleton "s17" raised) (regularEditTriangulation removed))
-  assertRegularReconstructs
-    domain
-    "differential reweight"
-    (regularEditTriangulation reweighted)
-
-assertRegularAlphaClosed
-  :: (Ord label, Show label)
-  => String
-  -> RegularTriangulation label
-  -> IO ()
-assertRegularAlphaClosed name regular = do
-  filtration <- requireRight name (regularAlphaFiltration regular)
-  traverse_
-    (requireRight (name <> " sublevel") . (`regularAlphaComplexAtBirth` filtration))
-    (Map.elems (regularAlphaBirths filtration))
-
-prepareDifferentialSite :: Int -> IO (PowerSite String)
-prepareDifferentialSite label = do
-  let coordinateX = fromIntegral ((label * 37 + 11) `mod` 97) / 10
-      coordinateY = fromIntegral ((label * 61 + 7) `mod` 89) / 10
-      weightValue = fromIntegral ((label * 17) `mod` 13 - 6) / 64
-  weight <- admittedWeight weightValue
-  admittedSite ("s" <> show label) (Point coordinateX coordinateY) weight
-
-testRegularEditObstructionsAndIdempotence :: IO ()
-testRegularEditObstructionsAndIdempotence = do
-  zero <- admittedWeight 0
-  one <- admittedWeight 1
-  site <- admittedSite "site" (Point 0 0) zero
-  conflicting <- admittedSite "site" (Point 0 0) one
-  inserted <-
-    requireRight
-      "insert into empty regular topology"
-      (insertRegularSite site emptyRegularTriangulation)
-  repeated <-
-    requireRight
-      "repeat identical regular insertion"
-      (insertRegularSite site (regularEditTriangulation inserted))
-  assertEqual
-    "identical insertion changes no site"
-    Set.empty
-    (regularEditChangedSites repeated)
-  assertEqual "identical insertion has no transitions" [] (editTransitions repeated)
-  case insertRegularSite conflicting (regularEditTriangulation inserted) of
-    Left (RegularEditSiteConflict "site" resident requested) -> do
-      assertEqual "conflict retains resident" site resident
-      assertEqual "conflict retains requested site" conflicting requested
-    other -> fail ("expected regular edit site conflict, got " <> show other)
-  case reweightRegularSites (Map.singleton "missing" zero) (regularEditTriangulation inserted) of
-    Left (RegularEditUnknownSites ("missing" :| [])) -> pure ()
-    other -> fail ("expected unknown reweight site, got " <> show other)
-  absent <-
-    requireRight
-      "remove absent regular site"
-      (removeRegularSite "missing" (regularEditTriangulation inserted))
-  assertEqual
-    "absent removal changes no site"
-    Set.empty
-    (regularEditChangedSites absent)
-  assertEqual "absent removal has no transitions" [] (editTransitions absent)
-  removed <-
-    requireRight
-      "remove final regular site"
-      (removeRegularSite "site" (regularEditTriangulation inserted))
-  assertEqual "final removal returns empty" 0 (regularSiteCount (regularEditTriangulation removed))
-  assertEqual
-    "final removal transition"
-    [RegularSiteDisappeared "site" RegularSiteVisible]
-    (editTransitions removed)
-
-testBoundedPowerFromRegular :: IO ()
-testBoundedPowerFromRegular = do
-  domain <- squareDomain
-  fixtures <-
-    traverse
-      (\(name, submitted) -> (,) name <$> traverse prepareOracleSite submitted)
-      [ ( "ordinary"
-        , ("a", Point 1 1, 0.25)
-            :| [("b", Point 3 1, -0.125), ("c", Point 2 3, 0.5)]
-        )
-      , ( "lower-dimensional"
-        , ("a", Point 0 0, 0)
-            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -1.5)]
-        )
-      , ( "hidden"
-        , ("a", Point 0 0, 0)
-            :| [("b", Point 2 0, 0), ("c", Point 0 2, 0), ("center", Point 0.5 0.5, -2)]
-        )
-      , ( "coincident"
-        , ("a", Point 1 1, 0)
-            :| [("b", Point 1 1, 0), ("c", Point 3 1, 0)]
-        )
-      ]
-  traverse_ (assertPreparedPowerMatches domain) fixtures
-  (emptyDiagram, emptyReceipt) <-
-    requireRight
-      "bounded power diagram from empty regular source"
-      (boundedPowerDiagramFromRegular domain (emptyRegularTriangulation :: RegularTriangulation String))
-  assertEqual "empty regular source has no cell dispositions" [] (powerCellDispositions emptyDiagram)
-  assertEqual "empty regular source has no input sites" 0 (powerDiagramInputSites emptyReceipt)
-
-assertPreparedPowerMatches
-  :: ConvexPolygon
-  -> (String, NonEmpty (PowerSite String))
-  -> IO ()
-assertPreparedPowerMatches domain (name, sites) = do
-  (regular, _) <-
-    requireRight (name <> " regular source") (regularTriangulation sites)
-  oneShot <-
-    requireRight (name <> " one-shot power diagram") (boundedPowerDiagram domain sites)
-  fromRegular <-
-    requireRight
-      (name <> " prepared power diagram")
-      (boundedPowerDiagramFromRegular domain regular)
-  assertEqual (name <> " prepared clipping") oneShot fromRegular
-
-testRegularCollinearClassification :: IO ()
-testRegularCollinearClassification = do
-  sites <- traverse prepareCollinearSite (0 :| [1 .. 8])
-  (regular, _) <-
-    requireRight "collinear regular topology" (regularTriangulation sites)
-  traverse_
-    (\index ->
-       assertEqual
-         ("collinear disposition for " <> show index)
-         (Just (if even index then RegularSiteVisible else RegularSiteHidden))
-         (regularSiteDisposition (show index) regular))
-    ([0 .. 8] :: [Int])
- where
-  prepareCollinearSite :: Int -> IO (PowerSite String)
-  prepareCollinearSite index = do
-    weight <- admittedWeight (if odd index then -(2 / 256) else 0)
-    admittedSite (show index) (Point (fromIntegral index / 16) 0) weight
-
-testSparsePowerMatchesCompleteOracle :: IO ()
-testSparsePowerMatchesCompleteOracle = do
-  domain <- squareDomain
-  sites <-
-    traverse
-      prepareOracleSite
-      ( ("a", Point 1.0 1.0, 0.25)
-          :| [ ("b", Point 3.0 0.8, -0.125)
-             , ("c", Point 5.2 1.4, 0.375)
-             , ("d", Point 8.5 1.0, 0.0)
-             , ("e", Point 1.7 3.8, -0.25)
-             , ("f", Point 4.1 4.4, 0.125)
-             , ("g", Point 7.6 3.5, -0.375)
-             , ("h", Point 9.1 5.4, 0.25)
-             , ("i", Point 1.0 7.8, 0.0)
-             , ("j", Point 3.6 8.9, 0.5)
-             , ("k", Point 6.4 7.5, -0.125)
-             , ("l", Point 8.8 9.0, 0.375)
-             ]
-      )
-  (diagram, receipt) <-
-    requireRight "sparse regular-neighbour power diagram" (boundedPowerDiagram domain sites)
-  traverse_
-    (\site -> do
-       oracle <- completeOraclePowerCell domain sites site
-       assertEqual
-         ("regular-neighbour cell equals complete HPI oracle for " <> powerSiteLabel site)
-         (Just oracle)
-         (powerCellDisposition (powerSiteLabel site) diagram))
-    sites
-  let siteCount = NonEmpty.length sites
-      completeConstraintCount = siteCount * (siteCount - 1)
-  unless (powerDiagramSubmittedSiteConstraints receipt < completeConstraintCount) $
-    fail "regular-neighbour construction did not eliminate the complete pairwise cell schedule"
-  unless (powerDiagramMaximumCellConstraints receipt <= powerDiagramRegularEdges receipt) $
-    fail "regular-neighbour construction retained a non-topological radical axis"
-
-prepareOracleSite
-  :: (String, Point, Double)
-  -> IO (PowerSite String)
-prepareOracleSite (label, point, weightValue) = do
-  weight <- admittedWeight weightValue
-  admittedSite label point weight
-
-data OracleScore = OracleScore
-  { oracleXCoefficient :: !ExactRational
-  , oracleYCoefficient :: !ExactRational
-  , oracleConstant :: !ExactRational
-  }
-
-oracleScore :: PowerSite label -> IO OracleScore
-oracleScore site = do
-  point <- requireRight "oracle source point" (exactPointFromPoint (powerSitePosition site))
-  let (coordinateX, coordinateY) = exactPointCoordinates point
-  pure
-    OracleScore
-      { oracleXCoefficient = 2 * coordinateX
-      , oracleYCoefficient = 2 * coordinateY
-      , oracleConstant =
-          powerWeightExact (powerSiteWeight site)
-            - coordinateX * coordinateX
-            - coordinateY * coordinateY
-      }
-
-completeOraclePowerCell
-  :: ConvexPolygon
-  -> NonEmpty (PowerSite String)
-  -> PowerSite String
-  -> IO (PowerCellDisposition String)
-completeOraclePowerCell domain sites owner = do
-  retainedDomain <-
-    requireRight "oracle retained domain" (exactRetainedPolygon (convexPolygonPoints domain))
-  ownerScore <- oracleScore owner
-  halfPlanes <-
-    traverse
-      (oracleWinningHalfPlane ownerScore)
-      ( filter
-          ((/= powerSiteLabel owner) . powerSiteLabel)
-          (NonEmpty.toList sites)
-      )
-  (disposition, _) <-
-    requireRight
-      ("complete HPI oracle for " <> powerSiteLabel owner)
-      (exactClipRetainedPolygon retainedDomain halfPlanes)
-  case disposition of
-    ExactClipFullDimensional retained ->
-      PublishedPowerCell
-        <$> requireRight
-          "oracle convex publication"
-          (convexPolygon (exactRetainedPolygonPoints retained))
-    ExactClipLowerDimensional points ->
-      pure (LowerDimensionalPowerCell points)
-    ExactClipEmpty -> pure EmptyPowerCell
-
-oracleWinningHalfPlane
-  :: OracleScore
-  -> PowerSite label
-  -> IO ExactClosedHalfPlane
-oracleWinningHalfPlane owner competitor = do
-  competitorScore <- oracleScore competitor
-  line <-
-    requireRight
-      "oracle radical axis"
-      ( exactAffineLine
-          (oracleXCoefficient owner - oracleXCoefficient competitorScore)
-          (oracleYCoefficient owner - oracleYCoefficient competitorScore)
-          (oracleConstant owner - oracleConstant competitorScore)
-      )
-  pure (exactClosedHalfPlane line)
-
-assertBoundaryDualRay
-  :: NonEmpty (PowerSite String)
-  -> ExactPoint
-  -> RegularEdge String
-  -> IO ()
-assertBoundaryDualRay sites expectedOrigin edge =
-  case regularEdgeDual edge of
-    UnboundedPowerDual ray -> do
-      assertEqual "regular ray starts at incident face dual" expectedOrigin (exactRayOrigin ray)
-      let ExactVector directionX directionY = exactRayDirection ray
-      unless (directionX /= 0 || directionY /= 0) $
-        fail ("regular edge has a zero dual-ray direction: " <> show (regularEdgeLabels edge))
-      let sample = translateExactPoint expectedOrigin (exactRayDirection ray)
-          (firstLabel, secondLabel) = regularEdgeLabels edge
-      firstSite <- requireSite firstLabel sites
-      secondSite <- requireSite secondLabel sites
-      firstDistance <- powerDistance sample firstSite
-      secondDistance <- powerDistance sample secondSite
-      assertEqual "regular ray remains on its radical axis" firstDistance secondDistance
-      traverse_
-        (\competitor -> do
-           competitorDistance <- powerDistance sample competitor
-           unless (firstDistance <= competitorDistance) $
-             fail ("regular ray points outside the common winning cone: " <> show (firstLabel, secondLabel)))
-        sites
-    other -> fail ("regular triangle boundary expected a ray, got " <> show other)
-
-requireSite :: Eq label => label -> NonEmpty (PowerSite label) -> IO (PowerSite label)
-requireSite label sites =
-  case List.find ((== label) . powerSiteLabel) (NonEmpty.toList sites) of
-    Nothing -> fail "regular topology references a missing source site"
-    Just site -> pure site
-
-testEqualWeightsAndCommonShift :: IO ()
-testEqualWeightsAndCommonShift = do
-  domain <- squareDomain
-  zero <- admittedWeight 0
-  shifted <- admittedWeight 7
-  zeroSites <-
-    ( :| )
-      <$> admittedSite "left" (Point 2 5) zero
-      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, zero)]
-  shiftedSites <-
-    ( :| )
-      <$> admittedSite "left" (Point 2 5) shifted
-      <*> traverse (uncurry3 admittedSite) [("right", Point 8 5, shifted)]
-  (diagram, receipt) <- requireRight "equal-weight power diagram" (boundedPowerDiagram domain zeroSites)
-  (shiftedDiagram, _) <- requireRight "common-shift power diagram" (boundedPowerDiagram domain shiftedSites)
-  assertEqual "common additive power shift" (powerCellDispositions diagram) (powerCellDispositions shiftedDiagram)
-  assertPermutationInvariant "equal-weight power diagram" domain zeroSites diagram
-  assertPermutationInvariant "common-shift power diagram" domain shiftedSites shiftedDiagram
-  expectedLeft <-
-    requireRight
-      "expected left power cell"
-      (convexPolygon (integerPoint 0 0 :| [integerPoint 5 0, integerPoint 5 10, integerPoint 0 10]))
-  expectedRight <-
-    requireRight
-      "expected right power cell"
-      (convexPolygon (integerPoint 5 0 :| [integerPoint 10 0, integerPoint 10 10, integerPoint 5 10]))
-  assertEqual "equal-weight left bisector cell" (Just (PublishedPowerCell expectedLeft)) (powerCellDisposition "left" diagram)
-  assertEqual "equal-weight right bisector cell" (Just (PublishedPowerCell expectedRight)) (powerCellDisposition "right" diagram)
-  assertEqual "receipt input sites" 2 (powerDiagramInputSites receipt)
-  assertEqual
-    "one submitted site constraint per cell"
-    2
-    (powerDiagramSubmittedSiteConstraints receipt)
-  assertEqual
-    "parallel domain and site boundaries coalesce"
-    8
-    (powerDiagramActiveBoundaries receipt)
-  assertEqual "receipt published cells" 2 (powerDiagramPublishedCells receipt)
-  assertEqual "receipt empty cells" 0 (powerDiagramEmptyCells receipt)
-  traverse_ (assertPublishedVerticesWin zeroSites) (powerCellDispositions diagram)
-  let layer = powerDiagramPlanarLayer diagram
-  assertEqual "derived layer contains both cells" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))
-
-testThreeSiteExactPartitionCoverage :: IO ()
-testThreeSiteExactPartitionCoverage = do
-  domain <- squareDomain
-  weight <- admittedWeight 0
-  left <- admittedSite "left" (Point 2 5) weight
-  middle <- admittedSite "middle" (Point 5 5) weight
-  right <- admittedSite "right" (Point 8 5) weight
-  let sites = left :| [middle, right]
-  (diagram, receipt) <-
-    requireRight "three-site exact partition" (boundedPowerDiagram domain sites)
-  assertEqual "three-site partition publishes every cell" 3 (powerDiagramPublishedCells receipt)
-  assertEqual
-    "three-site exact cell areas cover the square"
-    200
-    (publishedTwiceAreaSum diagram)
-  traverse_
-    (\permutation -> do
-       (permuted, _) <-
-         requireRight
-           "permuted three-site exact partition"
-           (boundedPowerDiagram domain permutation)
-       assertEqual
-         "three-site partition is permutation invariant"
-         (powerCellDispositions diagram)
-         (powerCellDispositions permuted)
-       assertEqual
-         "permuted exact cell areas cover the square"
-         200
-         (publishedTwiceAreaSum permuted))
-    (nonEmptyPermutations sites)
-
-nonEmptyPermutations :: NonEmpty value -> [NonEmpty value]
-nonEmptyPermutations =
-  foldMap (maybe [] pure . NonEmpty.nonEmpty)
-    . List.permutations
-    . NonEmpty.toList
-
-assertPermutationInvariant
-  :: String
-  -> ConvexPolygon
-  -> NonEmpty (PowerSite String)
-  -> BoundedPowerDiagram String
-  -> IO ()
-assertPermutationInvariant label domain sites expected =
-  traverse_
-    (\permutation -> do
-       (permuted, _) <-
-         requireRight
-           (label <> " permutation")
-           (boundedPowerDiagram domain permutation)
-       assertEqual
-         (label <> " is permutation invariant")
-         (powerCellDispositions expected)
-         (powerCellDispositions permuted))
-    (nonEmptyPermutations sites)
-
-publishedTwiceAreaSum :: BoundedPowerDiagram label -> ExactRational
-publishedTwiceAreaSum =
-  List.foldl'
-    (\total (_, disposition) -> case disposition of
-        PublishedPowerCell polygon -> total + exactPolygonTwiceArea (convexPolygonPoints polygon)
-        LowerDimensionalPowerCell _ -> total
-        EmptyPowerCell -> total
-        CoincidentEquivalentTo _ -> total
-        CoincidentDominatedBy _ -> total)
-    0
-    . powerCellDispositions
-
-exactPolygonTwiceArea :: NonEmpty ExactPoint -> ExactRational
-exactPolygonTwiceArea points =
-  abs
-    ( List.foldl'
-        (\twiceArea (firstPoint, secondPoint) ->
-           let (firstX, firstY) = exactPointCoordinates firstPoint
-               (secondX, secondY) = exactPointCoordinates secondPoint
-            in twiceArea + firstX * secondY - firstY * secondX)
-        0
-        (cyclicPairs points)
-    )
-
-cyclicPairs :: NonEmpty value -> [(value, value)]
-cyclicPairs (firstValue :| remainingValues) =
-  zip
-    (firstValue : remainingValues)
-    (remainingValues <> [firstValue])
-
-testEqualCoincidentResolution :: IO ()
-testEqualCoincidentResolution = do
-  domain <- squareDomain
-  weight <- admittedWeight 3
-  a <- admittedSite "a" (Point 4 4) weight
-  b <- admittedSite "b" (Point 4 4) weight
-  (diagram, receipt) <- requireRight "equivalent coincident power sites" (boundedPowerDiagram domain (b :| [a]))
-  assertPublished "coincident canonical representative" (powerCellDisposition "a" diagram)
-  assertEqual "equal-function coincident alias" (Just (CoincidentEquivalentTo "a")) (powerCellDisposition "b" diagram)
-  assertEqual "equivalent coincidence receipt" 1 (powerDiagramCoincidentEquivalentCells receipt)
-  assertEqual "equivalent coincidence is not dominance" 0 (powerDiagramCoincidentDominatedCells receipt)
-  (permuted, _) <- requireRight "permuted equivalent sites" (boundedPowerDiagram domain (a :| [b]))
-  assertEqual "equivalent resolution permutation invariance" (powerCellDispositions diagram) (powerCellDispositions permuted)
-
-testDominantCoincidentResolution :: IO ()
-testDominantCoincidentResolution = do
-  domain <- squareDomain
-  high <- admittedWeight 3
-  low <- admittedWeight 2
-  winner <- admittedSite "winner" (Point 4 4) high
-  dominated <- admittedSite "dominated" (Point 4 4) low
-  (diagram, receipt) <- requireRight "dominant coincident power sites" (boundedPowerDiagram domain (dominated :| [winner]))
-  assertPublished "dominant coincident representative" (powerCellDisposition "winner" diagram)
-  assertEqual "lower-weight coincident site" (Just (CoincidentDominatedBy "winner")) (powerCellDisposition "dominated" diagram)
-  assertEqual "dominant coincidence receipt" 1 (powerDiagramCoincidentDominatedCells receipt)
-  assertEqual "dominance is not equivalence" 0 (powerDiagramCoincidentEquivalentCells receipt)
-  assertPermutationInvariant "dominant coincident power sites" domain (dominated :| [winner]) diagram
-
-testLowerDimensionalCell :: IO ()
-testLowerDimensionalCell = do
-  domain <- squareDomain
-  zero <- admittedWeight 0
-  suppressed <- admittedWeight (-1)
-  left <- admittedSite "left" (Point 0 5) zero
-  right <- admittedSite "right" (Point 2 5) zero
-  middle <- admittedSite "middle" (Point 1 5) suppressed
-  let sites = left :| [right, middle]
-  (diagram, receipt) <- requireRight "one-dimensional bounded power cell" (boundedPowerDiagram domain sites)
-  case powerCellDisposition "middle" diagram of
-    Just (LowerDimensionalPowerCell points) ->
-      assertEqual
-        "middle cell is the exact x=1 segment"
-        (List.sort [integerPoint 1 0, integerPoint 1 10])
-        (List.sort (NonEmpty.toList points))
-    other -> fail ("one-dimensional power cell: expected retained segment, got " <> dispositionTag other)
-  assertEqual "lower-dimensional receipt" 1 (powerDiagramLowerDimensionalCells receipt)
-  assertEqual "lower-dimensional cell is not empty" 0 (powerDiagramEmptyCells receipt)
-  assertEqual "two full-dimensional neighbours" 2 (powerDiagramPublishedCells receipt)
-  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
-  let layer = powerDiagramPlanarLayer diagram
-  assertEqual "derived layer omits the one-dimensional cell" [Just "left", Just "right"] (Map.keys (planarLayerRegions layer))
-  assertPermutationInvariant "one-dimensional bounded power cell" domain sites diagram
-
-testDistinctEmptyCell :: IO ()
-testDistinctEmptyCell = do
-  domain <- squareDomain
-  ordinary <- admittedWeight 0
-  suppressed <- admittedWeight (-1000)
-  winner <- admittedSite "winner" (Point 0 0) ordinary
-  hidden <- admittedSite "hidden" (Point 5 5) suppressed
-  (diagram, receipt) <- requireRight "distinct empty power cell" (boundedPowerDiagram domain (winner :| [hidden]))
-  assertPublished "dominant distinct site" (powerCellDisposition "winner" diagram)
-  assertEqual "distinct site may have empty bounded cell" (Just EmptyPowerCell) (powerCellDisposition "hidden" diagram)
-  assertEqual "empty disposition receipt" 1 (powerDiagramEmptyCells receipt)
-  assertPermutationInvariant "distinct empty power cell" domain (winner :| [hidden]) diagram
-
-testDyadicNearParallelBisectors :: IO ()
-testDyadicNearParallelBisectors = do
-  domain <- squareDomain
-  weight <- admittedWeight 0
-  let epsilon = 2 ** (-20) :: Double
-  origin <- admittedSite "origin" (Point 0 0) weight
-  horizontal <- admittedSite "horizontal" (Point 2 0) weight
-  tilted <- admittedSite "tilted" (Point 2 epsilon) weight
-  let sites = origin :| [horizontal, tilted]
-  (diagram, receipt) <-
-    requireRight
-      "dyadic near-parallel power bisectors"
-      (boundedPowerDiagram domain sites)
-  assertEqual "near-parallel cells retain full dimension" 3 (powerDiagramPublishedCells receipt)
-  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
-  assertPermutationInvariant "dyadic near-parallel power bisectors" domain sites diagram
-
-testBinary64PrecisionSites :: IO ()
-testBinary64PrecisionSites = do
-  domain <- squareDomain
-  firstWeight <- admittedWeight 0.2
-  secondWeight <- admittedWeight (-0.3)
-  thirdWeight <- admittedWeight 0.7
-  firstSite <- admittedSite "first" (Point 0.1 0.3) firstWeight
-  secondSite <- admittedSite "second" (Point 9.7 0.2) secondWeight
-  thirdSite <- admittedSite "third" (Point 4.9 9.6) thirdWeight
-  let sites = firstSite :| [secondSite, thirdSite]
-  (diagram, receipt) <-
-    requireRight
-      "exact binary64 power sites"
-      (boundedPowerDiagram domain sites)
-  assertEqual "binary64 fixture publishes every cell" 3 (powerDiagramPublishedCells receipt)
-  traverse_ (assertPublishedVerticesWin sites) (powerCellDispositions diagram)
-  assertPermutationInvariant "exact binary64 power sites" domain sites diagram
-
-testDuplicateLabels :: IO ()
-testDuplicateLabels = do
-  domain <- squareDomain
-  weight <- admittedWeight 0
-  firstSite <- admittedSite "duplicate" (Point 1 1) weight
-  secondSite <- admittedSite "duplicate" (Point 9 9) weight
-  case boundedPowerDiagram domain (firstSite :| [secondSite]) of
-    Left (DuplicatePowerSiteLabel "duplicate") -> pure ()
-    other -> fail ("duplicate power-site label: expected typed refusal, got " <> show other)
-
-testNonFiniteWeight :: IO ()
-testNonFiniteWeight =
-  case powerWeight (0 / 0) of
-    Left (PowerWeightNonFinite _) -> pure ()
-    other -> fail ("non-finite power weight: expected typed refusal, got " <> show other)
-
-editTransitions
-  :: RegularEditResult label
-  -> [RegularSiteTransition label]
-editTransitions =
-  Vector.toList . regularEditTransitions
-
-squareDomain :: IO ConvexPolygon
-squareDomain =
-  requireRight
-    "square clipping domain"
-    (convexPolygon (integerPoint 0 0 :| [integerPoint 10 0, integerPoint 10 10, integerPoint 0 10]))
-
-admittedWeight :: Double -> IO PowerWeight
-admittedWeight value =
-  requireRight
-    "finite power weight"
-    (powerWeight value)
-
-admittedSite :: String -> Point -> PowerWeight -> IO (PowerSite String)
-admittedSite label point weight = requireRight "admitted power site" (powerSite label point weight)
-
-uncurry3 :: (a -> b -> c -> result) -> (a, b, c) -> result
-uncurry3 function (firstValue, secondValue, thirdValue) = function firstValue secondValue thirdValue
-
-assertPublished :: String -> Maybe (PowerCellDisposition label) -> IO ()
-assertPublished _ (Just (PublishedPowerCell _)) = pure ()
-assertPublished label other = fail (label <> ": expected published cell, got " <> dispositionTag other)
-
-dispositionTag :: Maybe (PowerCellDisposition label) -> String
-dispositionTag Nothing = "missing"
-dispositionTag (Just (PublishedPowerCell _)) = "published"
-dispositionTag (Just (LowerDimensionalPowerCell _)) = "lower-dimensional"
-dispositionTag (Just EmptyPowerCell) = "empty"
-dispositionTag (Just (CoincidentEquivalentTo _)) = "coincident-equivalent"
-dispositionTag (Just (CoincidentDominatedBy _)) = "coincident-dominated"
-
-requireSome :: String -> Maybe value -> IO value
-requireSome label value =
-  case value of
-    Just present -> pure present
-    Nothing -> fail (label <> ": missing value")
-
-assertPublishedVerticesWin
-  :: NonEmpty (PowerSite String)
-  -> (String, PowerCellDisposition String)
-  -> IO ()
-assertPublishedVerticesWin sites (ownerLabel, disposition) =
-  case disposition of
-    PublishedPowerCell polygon ->
-      assertCellPointsWin ownerLabel sites (convexPolygonPoints polygon)
-    LowerDimensionalPowerCell points ->
-      assertCellPointsWin ownerLabel sites points
-    EmptyPowerCell -> pure ()
-    CoincidentEquivalentTo _ -> pure ()
-    CoincidentDominatedBy _ -> pure ()
-
-assertCellPointsWin
-  :: String
-  -> NonEmpty (PowerSite String)
-  -> NonEmpty ExactPoint
-  -> IO ()
-assertCellPointsWin ownerLabel sites points =
-  case lookupOwner ownerLabel (NonEmpty.toList sites) of
-    Nothing -> fail ("power-cell owner missing: " <> ownerLabel)
-    Just owner ->
-      traverse_
-        (\point -> traverse_ (assertOwnerWinsAt point owner) sites)
-        points
-
-lookupOwner :: Eq label => label -> [PowerSite label] -> Maybe (PowerSite label)
-lookupOwner label = List.find ((== label) . powerSiteLabel)
-
-assertOwnerWinsAt
-  :: ExactPoint
-  -> PowerSite String
-  -> PowerSite String
-  -> IO ()
-assertOwnerWinsAt point owner competitor = do
-  ownerValue <- powerDistance point owner
-  competitorValue <- powerDistance point competitor
-  unless (ownerValue <= competitorValue) $
-    fail
-      ( "power-cell vertex violates source inequality: "
-          <> show (powerSiteLabel owner, powerSiteLabel competitor, ownerValue, competitorValue)
-      )
-
-powerDistance :: ExactPoint -> PowerSite label -> IO ExactRational
-powerDistance point site = do
-  exactSite <- requireRight "site exact position" (exactPointFromPoint (powerSitePosition site))
-  let (x, y) = exactPointCoordinates point
-      (siteX, siteY) = exactPointCoordinates exactSite
-      deltaX = x - siteX
-      deltaY = y - siteY
-  pure (deltaX * deltaX + deltaY * deltaY - powerWeightExact (powerSiteWeight site))
diff --git a/test/algebra/Moonlight/Triangulation/RegionAlgebraSpec.hs b/test/algebra/Moonlight/Triangulation/RegionAlgebraSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/RegionAlgebraSpec.hs
+++ /dev/null
@@ -1,90 +0,0 @@
--- | Facade-only laws for exact planar Boolean composition.
-module Moonlight.Triangulation.RegionAlgebraSpec (tests) where
-
-import Moonlight.Triangulation
-import Moonlight.Triangulation.AlgebraFixtures
-  ( overlayRegions
-  , rectangleComponent
-  , rectangleRegion
-  )
-import Support (assertEqual, requireRight)
-
-tests :: IO ()
-tests = do
-  testRegionBooleanLaws
-  testFacadeComposition
-  putStrLn "region algebra: ok"
-
-testRegionBooleanLaws :: IO ()
-testRegionBooleanLaws = do
-  left <- rectangleRegion 0 0 2 2
-  middle <- rectangleRegion 1 0 3 2
-  right <- rectangleRegion 2 0 4 2
-  expectedUnion <- rectangleRegion 0 0 3 2
-  expectedIntersection <- rectangleRegion 1 0 2 2
-  expectedDifference <- rectangleRegion 0 0 1 2
-
-  leftUnionMiddle <- regionUnion left middle
-  middleUnionLeft <- regionUnion middle left
-  assertEqual "region union result" expectedUnion leftUnionMiddle
-  assertEqual "region union commutativity" leftUnionMiddle middleUnionLeft
-  assertEqual "region union idempotence" left =<< regionUnion left left
-  assertEqual
-    "region intersection result"
-    expectedIntersection
-    =<< regionIntersection left middle
-  assertEqual
-    "regularized region difference"
-    expectedDifference
-    =<< regionDifference left middle
-
-  leftAssociated <- regionUnion leftUnionMiddle right
-  middleUnionRight <- regionUnion middle right
-  rightAssociated <- regionUnion left middleUnionRight
-  assertEqual "region union associativity" leftAssociated rightAssociated
-
-testFacadeComposition :: IO ()
-testFacadeComposition = do
-  left <- rectangleRegion 0 0 2 2
-  right <- rectangleRegion 1 0 3 2
-  overlay <- overlayRegions left right
-  selectedUnion <-
-    requireRight
-      "facade selected region"
-      (overlaySelectedRegion (uncurry (||)) overlay)
-  valuations <- requireRight "facade region valuations" (regionValuations selectedUnion)
-  assertEqual
-    "facade valuation digest"
-    (1, 6)
-    ( eulerCharacteristicValue (valuationEuler valuations)
-    , exactAreaValue (valuationArea valuations)
-    )
-
-  kernelComponent <- rectangleComponent 0 0 1 1
-  kernel <-
-    requireRight
-      "facade convex kernel"
-      (convexPolygon (exactLoopPoints (polygonOuterLoop kernelComponent)))
-  element <- requireRight "facade structuring element" (structuringElement kernel)
-  (expanded, receipt) <- requireRight "facade polygon offset" (polygonOffset element selectedUnion)
-  expectedExpanded <- rectangleRegion 0 0 4 3
-  assertEqual "facade morphology result" expectedExpanded expanded
-  assertEqual "facade morphology receipt" MinkowskiAddition (minkowskiOperation receipt)
-
-regionUnion :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
-regionUnion = combineRegions (uncurry (||))
-
-regionIntersection :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
-regionIntersection = combineRegions (uncurry (&&))
-
-regionDifference :: PlanarRegion -> PlanarRegion -> IO PlanarRegion
-regionDifference = combineRegions (\(insideLeft, insideRight) -> insideLeft && not insideRight)
-
-combineRegions
-  :: ((Bool, Bool) -> Bool)
-  -> PlanarRegion
-  -> PlanarRegion
-  -> IO PlanarRegion
-combineRegions selected left right = do
-  overlay <- overlayRegions left right
-  requireRight "region Boolean publication" (overlaySelectedRegion selected overlay)
diff --git a/test/algebra/Moonlight/Triangulation/ScheduleAgreementSpec.hs b/test/algebra/Moonlight/Triangulation/ScheduleAgreementSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/ScheduleAgreementSpec.hs
+++ /dev/null
@@ -1,72 +0,0 @@
--- | The schedule-agreement slice: the seam merge publishes what the reference
--- rebuild publishes. It travels and dies with the schedule it names.
-module Moonlight.Triangulation.ScheduleAgreementSpec (tests) where
-
-import Control.Monad (forM_, when)
-import Data.IORef (modifyIORef', newIORef, readIORef)
-import Moonlight.Triangulation (Point (Point), canonicalize, union)
-import Moonlight.Triangulation.AlgebraFixtures
-  ( assertMesh
-  , meshOf
-  , operands
-  , separatedOperands
-  , siteList
-  )
-import Moonlight.Triangulation.Internal.Join.Seam (executeSeam, planSeam)
-import Support (requireRight)
-
-tests :: IO ()
-tests = do
-  testSeamAgreesWithReference
-  putStrLn "schedule agreement: ok"
-
--- | The seam schedule and the reference schedule are the same function.
---
--- A join has one meaning and several internal schedules, so this is the law
--- that lets a second one exist at all: for every input where the seam applies,
--- it must publish the value the rebuild publishes — not an isomorphic mesh, the
--- same value, once both are canonically numbered.
---
--- The guard matters more than the equality. 'executeSeam' can only be reached
--- with the opaque proof returned by 'planSeam'; the run fails if too few cases
--- obtain that proof, so the reference cannot quietly be compared with itself.
-testSeamAgreesWithReference :: IO ()
-testSeamAgreesWithReference = do
-  separable <- separatedOperands
-  overlapping <- operands
-  taken <- newIORef (0 :: Int)
-  forM_ separable $ \((leftName, left), (rightName, right)) -> do
-    let label = "seam " <> leftName <> " / " <> rightName
-    seamPlan <- case planSeam left right of
-      Nothing ->
-        fail (label <> ": the operands are separated but planSeam refused them")
-      Just admitted -> do
-        modifyIORef' taken (+ 1)
-        pure admitted
-    rawRebuild <- meshOf label [Point x y | (x, y) <- siteList left <> siteList right]
-    rebuilt <- requireRight (label <> ", canonical rebuild") (canonicalize rawRebuild)
-    seamed <- requireRight (label <> ", direct seam") (executeSeam seamPlan left right)
-    canonicalSeam <- requireRight (label <> ", canonical seam") (canonicalize seamed)
-    assertMesh (label <> ", against a rebuild") rebuilt canonicalSeam
-    joined <- requireRight (label <> ", through union") (union left right)
-    canonicalJoined <- requireRight (label <> ", canonical union") (canonicalize joined)
-    assertMesh (label <> ", through the operator") rebuilt canonicalJoined
-  forM_ [(l, r) | l <- overlapping, r <- overlapping] $ \((leftName, left), (rightName, right)) ->
-    case planSeam left right of
-      Nothing -> pure ()
-      Just seamPlan -> do
-        rawRebuild <-
-          meshOf
-            ("seam admission " <> leftName <> " / " <> rightName)
-            [Point x y | (x, y) <- siteList left <> siteList right]
-        rebuilt <- requireRight "seam admission canonical rebuild" (canonicalize rawRebuild)
-        seamed <- requireRight "seam admission" (executeSeam seamPlan left right)
-        canonicalSeam <- requireRight "seam admission canonical seam" (canonicalize seamed)
-        assertMesh
-          ("seam admission " <> leftName <> " / " <> rightName)
-          rebuilt
-          canonicalSeam
-        modifyIORef' taken (+ 1)
-  count <- readIORef taken
-  when (count < 3) $
-    fail ("seam agreement: only " <> show count <> " cases entered the seam, so the law asserted nothing")
diff --git a/test/algebra/Moonlight/Triangulation/ValuationSpec.hs b/test/algebra/Moonlight/Triangulation/ValuationSpec.hs
deleted file mode 100644
--- a/test/algebra/Moonlight/Triangulation/ValuationSpec.hs
+++ /dev/null
@@ -1,335 +0,0 @@
--- | Exact intrinsic-volume fixtures and common-subdivision inclusion-exclusion.
-module Moonlight.Triangulation.ValuationSpec (tests) where
-
-import Moonlight.Triangulation.AlgebraFixtures
-  ( annulusRegion
-  , insideLayer
-  , polygonRegion
-  , rectangleComponent
-  , rectangleRegion
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactRational
-  , exactRational
-  )
-import Moonlight.Triangulation.Overlay
-  ( overlayClosedIntersection
-  , overlayClosedUnion
-  , overlayLayers
-  , overlaySelectedRegion
-  )
-import Moonlight.Triangulation.Region
-  ( PlanarRegion
-  , emptyPlanarRegion
-  , planarRegion
-  , planarRegionComponents
-  )
-import Moonlight.Triangulation.Valuation
-  ( CertifiedInterval (..)
-  , ExactPlanarMoments
-  , ExactLengthTerm
-  , PlanarValuations
-  , ValuationError (ValuationCellSetNotPureRegion)
-  , cellSetPerimeter
-  , cellValuations
-  , exactAreaValue
-  , exactLengthBounds
-  , exactLengthExpression
-  , exactLengthTerms
-  , exactPlanarFirstX
-  , exactPlanarFirstY
-  , exactPlanarMeasure
-  , exactPlanarSecondXX
-  , exactPlanarSecondXY
-  , exactPlanarSecondYY
-  , eulerCharacteristicValue
-  , lengthCoefficient
-  , polygonComponentArea
-  , polygonComponentMoments
-  , regionPerimeter
-  , regionValuations
-  , scaleExactPlanarMoments
-  , squaredLength
-  , valuationArea
-  , valuationEuler
-  , valuationIntrinsic1
-  )
-import Support (assertEqual, requireRight)
-
-tests :: IO ()
-tests = do
-  testRegionGoldenValues
-  testExactPlanarMoments
-  testClosedCellInclusionExclusion
-  testDimensionalCellFixtures
-  testMetricInvariance
-  putStrLn "valuation: ok"
-
-testExactPlanarMoments :: IO ()
-testExactPlanarMoments = do
-  unit <- rectangleComponent 0 0 1 1
-  oneHalfValue <- exactValue 1 2
-  oneThird <- exactValue 1 3
-  oneQuarter <- exactValue 1 4
-  let unitMoments = polygonComponentMoments unit
-  assertEqual
-    "area is the measure projection"
-    (exactPlanarMeasure unitMoments)
-    (exactAreaValue (polygonComponentArea unit))
-  assertEqual
-    "unit-square moments"
-    (1, oneHalfValue, oneHalfValue, oneThird, oneQuarter, oneThird)
-    (momentDigest unitMoments)
-
-  translated <- rectangleComponent 2 (-1) 4 2
-  assertEqual
-    "translated-rectangle moments"
-    (6, 18, 3, 56, 9, 6)
-    (momentDigest (polygonComponentMoments translated))
-
-  annulus <- annulusRegion (0, 0, 3, 3) (1, 1, 2, 2)
-  seventyFourThirds <- exactValue 74 3
-  let annulusMoments = foldMap polygonComponentMoments (planarRegionComponents annulus)
-  assertEqual
-    "annulus moments respect clockwise holes"
-    (8, 12, 12, seventyFourThirds, 18, seventyFourThirds)
-    (momentDigest annulusMoments)
-  assertEqual
-    "moment monoid and scalar action"
-    (momentDigest (unitMoments <> unitMoments))
-    (momentDigest (scaleExactPlanarMoments 2 unitMoments))
-
-momentDigest
-  :: ExactPlanarMoments
-  -> (ExactRational, ExactRational, ExactRational, ExactRational, ExactRational, ExactRational)
-momentDigest moments =
-  ( exactPlanarMeasure moments
-  , exactPlanarFirstX moments
-  , exactPlanarFirstY moments
-  , exactPlanarSecondXX moments
-  , exactPlanarSecondXY moments
-  , exactPlanarSecondYY moments
-  )
-
-exactValue :: Integer -> Integer -> IO ExactRational
-exactValue numerator denominator =
-  requireRight "exact valuation fixture" (exactRational numerator denominator)
-
-testRegionGoldenValues :: IO ()
-testRegionGoldenValues = do
-  assertRegionValuations "empty" emptyPlanarRegion 0 0 []
-
-  unit <- rectangleRegion 0 0 1 1
-  assertRegionValuations "unit square" unit 1 1 [(2, 1)]
-  unitPerimeter <- requireRight "unit-square perimeter" (regionPerimeter unit)
-  assertLength "unit-square perimeter" [(4, 1)] (exactLengthTerms (exactLengthExpression unitPerimeter))
-  assertContains "unit-square perimeter bounds" 4 (exactLengthBounds unitPerimeter)
-
-  annulus <- annulusRegion (0, 0, 3, 3) (1, 1, 2, 2)
-  assertRegionValuations "annulus" annulus 0 8 [(2, 1), (2, 9)]
-
-  lowerLeft <- rectangleComponent 0 0 1 1
-  upperRight <- rectangleComponent 1 1 2 2
-  cornerTouch <- requireRight "corner-touch region" (planarRegion [lowerLeft, upperRight])
-  assertRegionValuations "corner-touch squares" cornerTouch 1 2 [(4, 1)]
-
-  right <- rectangleComponent 1 0 2 1
-  edgeTouch <- requireRight "edge-touch region" (planarRegion [lowerLeft, right])
-  assertRegionValuations "edge-sharing squares" edgeTouch 1 2 [(3, 1)]
-
-testClosedCellInclusionExclusion :: IO ()
-testClosedCellInclusionExclusion = do
-  leftRegion <- rectangleRegion 0 0 1 1
-  rightRegion <- rectangleRegion 1 0 2 1
-  leftLayer <- insideLayer leftRegion
-  rightLayer <- insideLayer rightRegion
-  overlay <- requireRight "edge-sharing valuation overlay" (overlayLayers leftLayer rightLayer)
-  left <- requireRight "left closed cells" (overlayClosedUnion id (const False) overlay)
-  right <- requireRight "right closed cells" (overlayClosedUnion (const False) id overlay)
-  union <- requireRight "union closed cells" (overlayClosedUnion id id overlay)
-  intersection <-
-    requireRight
-      "intersection closed cells"
-      (overlayClosedIntersection id id overlay)
-  leftValues <- requireRight "left valuations" (cellValuations left)
-  rightValues <- requireRight "right valuations" (cellValuations right)
-  unionValues <- requireRight "union valuations" (cellValuations union)
-  sharedEdgeValues <- requireRight "intersection valuations" (cellValuations intersection)
-  assertEqual
-    "Euler inclusion-exclusion retains the shared edge"
-    (1, 1, 1, 1)
-    ( eulerCharacteristicValue (valuationEuler leftValues)
-    , eulerCharacteristicValue (valuationEuler rightValues)
-    , eulerCharacteristicValue (valuationEuler unionValues)
-    , eulerCharacteristicValue (valuationEuler sharedEdgeValues)
-    )
-  assertEqual
-    "area inclusion-exclusion retains zero-dimensional measure"
-    ( 1
-    , 1
-    , 2
-    , 0
-    )
-    ( areaExact leftValues
-    , areaExact rightValues
-    , areaExact unionValues
-    , areaExact sharedEdgeValues
-    )
-  assertLength
-    "left intrinsic one-volume"
-    [(2, 1)]
-    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 leftValues)))
-  assertLength
-    "right intrinsic one-volume"
-    [(2, 1)]
-    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 rightValues)))
-  assertLength
-    "union intrinsic one-volume"
-    [(3, 1)]
-    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 unionValues)))
-  assertLength
-    "shared-edge intrinsic one-volume"
-    [(1, 1)]
-    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 sharedEdgeValues)))
-  case cellSetPerimeter intersection of
-    Left ValuationCellSetNotPureRegion -> pure ()
-    other ->
-      fail
-        ( "an isolated selected edge was accepted as a perimeter: "
-            <> show other
-        )
-  published <-
-    requireRight
-      "published edge-sharing union"
-      (overlaySelectedRegion (uncurry (||)) overlay)
-  publishedValues <- requireRight "published union valuations" (regionValuations published)
-  assertEqual
-    "cell and published region valuations agree"
-    (valuationDigest unionValues)
-    (valuationDigest publishedValues)
-
-testDimensionalCellFixtures :: IO ()
-testDimensionalCellFixtures = do
-  disjoint <- intersectionValues (0, 0, 1, 1) (2, 0, 3, 1)
-  assertEqual
-    "empty intersection valuations"
-    (0, 0, [])
-    (exactDigest disjoint)
-
-  point <- intersectionValues (0, 0, 1, 1) (1, 1, 2, 2)
-  assertEqual
-    "point intersection valuations"
-    (1, 0, [])
-    (exactDigest point)
-
-  area <- intersectionValues (0, 0, 2, 2) (1, 0, 3, 2)
-  assertEqual
-    "two-dimensional intersection valuations"
-    ( 1
-    , 2
-    , [ (1, 1)
-      , (1, 4)
-      ]
-    )
-    (exactDigest area)
-
-testMetricInvariance :: IO ()
-testMetricInvariance = do
-  original <- polygonRegion [(0, 0), (1, 0), (0, 1)]
-  translated <- polygonRegion [(5, -3), (6, -3), (5, -2)]
-  quarterTurned <- polygonRegion [(0, 0), (0, 1), (-1, 0)]
-  originalValues <- requireRight "original triangle valuations" (regionValuations original)
-  translatedValues <- requireRight "translated triangle valuations" (regionValuations translated)
-  quarterTurnedValues <- requireRight "quarter-turned triangle valuations" (regionValuations quarterTurned)
-  assertEqual "translation invariance" (exactDigest originalValues) (exactDigest translatedValues)
-  assertEqual "quarter-turn invariance" (exactDigest originalValues) (exactDigest quarterTurnedValues)
-  perimeter <- requireRight "irrational triangle perimeter" (regionPerimeter original)
-  assertContains
-    "certified radical perimeter"
-    (2 + sqrt 2)
-    (exactLengthBounds perimeter)
-
-intersectionValues
-  :: (Integer, Integer, Integer, Integer)
-  -> (Integer, Integer, Integer, Integer)
-  -> IO PlanarValuations
-intersectionValues leftBounds rightBounds = do
-  leftRegion <- uncurryRectangle leftBounds
-  rightRegion <- uncurryRectangle rightBounds
-  leftLayer <- insideLayer leftRegion
-  rightLayer <- insideLayer rightRegion
-  overlay <- requireRight "dimensional valuation overlay" (overlayLayers leftLayer rightLayer)
-  selected <-
-    requireRight
-      "dimensional closed intersection"
-      (overlayClosedIntersection id id overlay)
-  requireRight "dimensional cell valuations" (cellValuations selected)
-
-uncurryRectangle
-  :: (Integer, Integer, Integer, Integer)
-  -> IO PlanarRegion
-uncurryRectangle (minimumX, minimumY, maximumX, maximumY) =
-  rectangleRegion minimumX minimumY maximumX maximumY
-
-assertRegionValuations
-  :: String
-  -> PlanarRegion
-  -> Int
-  -> Integer
-  -> [(Integer, Integer)]
-  -> IO ()
-assertRegionValuations label region expectedEuler expectedArea expectedLength = do
-  values <- requireRight label (regionValuations region)
-  assertEqual
-    (label <> " Euler")
-    expectedEuler
-    (eulerCharacteristicValue (valuationEuler values))
-  assertEqual
-    (label <> " area")
-    (fromInteger expectedArea)
-    (exactAreaValue (valuationArea values))
-  assertLength
-    (label <> " intrinsic one-volume")
-    expectedLength
-    (exactLengthTerms (exactLengthExpression (valuationIntrinsic1 values)))
-
-assertLength :: String -> [(Integer, Integer)] -> [ExactLengthTerm] -> IO ()
-assertLength label expected actual =
-  assertEqual
-    label
-    [ (fromInteger coefficient, fromInteger square)
-    | (coefficient, square) <- expected
-    ]
-    [ (lengthCoefficient term, squaredLength term)
-    | term <- actual
-    ]
-
-assertContains :: String -> Double -> CertifiedInterval -> IO ()
-assertContains label expected interval =
-  if intervalLower interval <= expected && expected <= intervalUpper interval
-    then pure ()
-    else fail (label <> ": interval does not contain " <> show expected <> ": " <> show interval)
-
-valuationDigest
-  :: PlanarValuations
-  -> (Int, ExactRational, CertifiedInterval)
-valuationDigest values =
-  ( eulerCharacteristicValue (valuationEuler values)
-  , exactAreaValue (valuationArea values)
-  , exactLengthBounds (valuationIntrinsic1 values)
-  )
-
-exactDigest
-  :: PlanarValuations
-  -> (Int, ExactRational, [(ExactRational, ExactRational)])
-exactDigest values =
-  ( eulerCharacteristicValue (valuationEuler values)
-  , exactAreaValue (valuationArea values)
-  , [ (lengthCoefficient term, squaredLength term)
-    | term <- exactLengthTerms (exactLengthExpression (valuationIntrinsic1 values))
-    ]
-  )
-
-areaExact :: PlanarValuations -> ExactRational
-areaExact values =
-  exactAreaValue (valuationArea values)
diff --git a/test/cell-complex/Main.hs b/test/cell-complex/Main.hs
--- a/test/cell-complex/Main.hs
+++ b/test/cell-complex/Main.hs
@@ -1,15 +1,20 @@
 module Main (main) where
 
 import Data.Bits (shiftL)
+import Data.Bifunctor (first)
+import Data.Either (rights)
 import Data.Function ((&))
 import Data.Foldable (traverse_)
 import Data.List qualified as List
+import Data.IntMap.Strict qualified as IntMap
+import Data.IntSet qualified as IntSet
 import Data.List.NonEmpty qualified as NonEmpty
 import Data.Map.Strict qualified as Map
 import Data.Maybe (mapMaybe)
 import Data.Ratio ((%))
 import Data.Set qualified as Set
 import Data.Vector qualified as Vector
+import Data.Vector.Unboxed qualified as Unboxed
 import Moonlight.Hex.CellComplex (finiteHexCellComplex)
 import Moonlight.Hex.Coordinate (HexCoord (..), allHexDirections, hexStepCoord)
 import Moonlight.Hex.Region (hexLayout, hexRegionFromCoords)
@@ -44,60 +49,77 @@
     eulerCharacteristic,
     isBoundaryEdge,
   )
-import Moonlight.Triangulation.Alpha
+import Moonlight.Planar.Alpha
   ( AlphaBirth
   , AlphaFiltration
   , alphaBirthDenominator
   , alphaBirthNumerator
+  , alphaBirthFromRadiusSquared
+  , alphaComplexAtBirth
   , alphaEdgeBirth
   , alphaFaceBirth
   , alphaFiltration
   , alphaFiltrationCellSet
+  , alphaFiltrationCriticalBirths
   , alphaVertexBirth
   )
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.CellSet
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.CellSet
   ( ExactCellSet,
+    CellSelectionError (..),
     closeFaceCellSet,
+    exactCellSet,
     exactCellSetEdgeCount,
     exactCellSetFaceCount,
     exactCellSetVertexCount,
+    foldExactCellEdges,
+    foldExactCellFaces,
+    foldExactCellVertices,
   )
-import Moonlight.Triangulation.CellComplex
+import Moonlight.Planar.CellComplex
   ( DCELComplex
+  , DCELCellEdge (..)
   , filteredAlphaComplex
   , finiteChainComplex
   , fromExactCellSet
   )
-import Moonlight.Triangulation.Dcel
+import Moonlight.Planar.Dcel
   ( undirectedEndpoints
   , vertexPoint
   )
-import Moonlight.Triangulation.Internal.HandleDefs
-  ( UndirectedEdgeId
+import Moonlight.Planar.Internal.HandleDefs
+  ( DirectedEdgeId (..)
+  , FaceId (..)
+  , UndirectedEdgeId (..)
+  , VertexId (..)
   )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
+import Moonlight.Planar.Internal.CellSet (closeExactCellSetWith)
+import Moonlight.Planar.Internal.Incidence (admitPlanarIncidence)
+import Moonlight.Planar.Internal.PackedIndex (noIndex)
+import Moonlight.Planar.Internal.Paged qualified as Paged
+import Moonlight.Planar.Exact (ExactPoint, exactPointFromPoint)
+import Moonlight.Planar.Handles.Iterators.FixedIterators qualified as Resident
+import Moonlight.Planar.Handles.Iterators.FixedIterators
   ( innerFaces
   , undirectedEdges
   )
-import Moonlight.Triangulation.Internal.Dyadic (exactDiametralDot)
-import Moonlight.Triangulation.Internal.ExactRational
+import Moonlight.Planar.Internal.Dyadic (exactDiametralDot)
+import Moonlight.Planar.Internal.ExactRational
   ( exactRational
   , exactRationalFromDyadic
   , exactRationalFromDyadicRatio
   )
-import Moonlight.Triangulation.Internal.Predicates (inDiametralCircle)
-import Moonlight.Triangulation.LabelledComplex (filteredPlanarComplex)
-import Moonlight.Triangulation.Simplex
+import Moonlight.Planar.Internal.Predicates (inDiametralCircle)
+import Moonlight.Planar.LabelledComplex (filteredPlanarComplex)
+import Moonlight.Planar.Simplex
   ( planarComplex
   , planarEdge
   , planarFace
   , planarVertex
   )
-import Moonlight.Triangulation.Types
-  ( DelaunayTriangulation
-  , Point (..)
-  )
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (mkRadiusSquared)
 import Test.Tasty (TestTree, defaultMain, testGroup)
 import Test.Tasty.HUnit ((@?=), Assertion, assertBool, assertFailure, testCase)
 
@@ -110,10 +132,13 @@
     "ExactCellSet bridge"
     [ testCase "preserves the admitted cell inventory" preserveCellInventory,
       testCase "preserves downward-closed incidence" preserveClosedIncidence,
+      testCase "preserves ascending incidence for every square subcomplex and absent handle" exhaustiveSelectedIncidence,
+      testCase "preserves sparse selections around a high-degree resident vertex" sparseHighDegreeIncidence,
       testCase "marks the triangular exterior as absent" preserveExteriorAdjacency,
       testCase "lowers oriented DCEL incidence to a checked integral chain complex" lowerIntegralChainComplex,
       testCase "lowers a native hexagonal ring through the same chain owner" lowerHexagonalRing,
       testCase "assigns exact Gabriel and non-Gabriel alpha births" exactAlphaBirths,
+      testCase "derives the constant vertex birth section over exactly its resident support" constantAlphaVertexSection,
       testCase "preserves exact dyadic rational normalization" exactDyadicRationalNormalization,
       testCase "orders cached alpha keys by their exact rational births" alphaBirthOrderMatchesExactRationals,
       testCase "agrees with the exact diametral predicate" diametralPredicateMatchesExactSign,
@@ -121,8 +146,161 @@
       testCase "finds the square alpha hole from radius one to radius two" squareAlphaPersistence,
       testCase "lowers a labelled planar filtration through the same Homology owner" labelledPlanarPersistence,
       testCase "keeps point and collinear alpha filtrations total" degenerateSupportFiltrations
+    , testCase "refines annular faces without confusing strata with disk cells" exactAnnulusRefinement
+    , testCase "refines isolated vertices and floating tree boundary components" exactDisconnectedBoundaryRefinement
+    , testCase "retains repeated slit darts and vertex-only face contacts" exactSlitAndContactRefinement
     ]
 
+-- Literal embedded planar maps, independent of the arrangement constructor.
+-- Darts use (origin, next, previous, incident face); even/odd darts are twins.
+data CellularFixture = CellularFixture
+  { fixturePoints :: ![Point]
+  , fixtureVertexRoots :: ![Maybe Int]
+  , fixtureDarts :: ![(Int, Int, Int, Int)]
+  , fixtureFaceRoots :: ![Maybe Int]
+  , fixtureInnerRoots :: ![(Int, [Int])]
+  , fixtureIsolatedVertices :: ![(Int, [Int])]
+  }
+
+refineCellularFixture
+  :: CellularFixture
+  -> [Int]
+  -> [Int]
+  -> [Int]
+  -> IO (ExactCellSet, DCELComplex)
+refineCellularFixture fixture selectedVertices selectedEdges selectedFaces = do
+  incidence <- requireRight "literal planar incidence"
+    (admitPlanarIncidence
+      (Paged.fromVector noIndex (Unboxed.fromList (fmap (maybe noIndex fromIntegral) (fixtureVertexRoots fixture))))
+      (Paged.fromVector noIndex (Unboxed.fromList
+        (concatMap (\(origin, next, previous, face) -> fmap fromIntegral [origin, next, previous, face]) (fixtureDarts fixture))))
+      (Paged.fromVector noIndex (Unboxed.fromList (fmap (maybe noIndex fromIntegral) (fixtureFaceRoots fixture))))
+      (IntMap.fromList (fmap (fmap (fmap (DirectedEdgeId . fromIntegral))) (fixtureInnerRoots fixture)))
+      (IntMap.fromList (fmap (fmap IntSet.fromList) (fixtureIsolatedVertices fixture))))
+  let points = Vector.fromList (fixturePoints fixture)
+      exactPointAt vertex@(VertexId rawVertex) =
+        case points Vector.!? fromIntegral rawVertex of
+          Nothing -> Left (CellVertexOutOfRange vertex (Vector.length points))
+          Just point -> first (CellCoordinateInvalid vertex) (exactPointFromPoint point)
+  selection <- requireRight "literal closed cell selection"
+    (closeExactCellSetWith exactPointAt incidence
+      (fmap (VertexId . fromIntegral) selectedVertices)
+      (fmap (UndirectedEdgeId . fromIntegral) selectedEdges)
+      (fmap (FaceId . fromIntegral) selectedFaces))
+  refined <- requireRight "literal cellular refinement" (fromExactCellSet selection)
+  pure (selection, refined)
+
+assertCellularBetti :: [Int] -> DCELComplex -> Assertion
+assertCellularBetti expected complexValue = do
+  finite <- requireRight "refined cellular chains" (finiteChainComplex complexValue)
+  freeBettiVector finite @?= expected
+  eulerCharacteristic complexValue @?= sum (zipWith (*) (cycle [1, -1]) expected)
+  assertIncidenceQueries complexValue (vertices complexValue)
+
+exactAnnulusRefinement :: Assertion
+exactAnnulusRefinement = do
+  (selection, annulus) <- refineCellularFixture annulusFixture [] [] [1]
+  (exactCellSetVertexCount selection, exactCellSetEdgeCount selection, exactCellSetFaceCount selection) @?= (8, 8, 1)
+  (length (vertices annulus), length (edges annulus), length (faces annulus)) @?= (8, 9, 1)
+  assertCellularBetti [1, 1, 0] annulus
+  (_, filled) <- refineCellularFixture annulusFixture [] [] [1, 2]
+  assertCellularBetti [1, 0, 0] filled
+  let cuts :: DCELComplex -> [DCELCellEdge]
+      cuts complexValue = [edge | edge@FaceBoundaryCut {} <- edges complexValue]
+  cuts annulus @?= cuts filled
+  traverse_ (\edge -> facesAtEdge annulus edge @?= (Just (FaceId 1), Just (FaceId 1))) (cuts annulus)
+  (_, boundaryOnly) <- refineCellularFixture annulusFixture [] [0 .. 7] []
+  assertCellularBetti [2, 2, 0] boundaryOnly
+  (_, emptySelection) <- refineCellularFixture annulusFixture [] [] []
+  assertCellularBetti [0, 0, 0] emptySelection
+
+exactDisconnectedBoundaryRefinement :: Assertion
+exactDisconnectedBoundaryRefinement = do
+  (_, markedDisk) <- refineCellularFixture isolatedPointFixture [] [] [1]
+  (length (vertices markedDisk), length (edges markedDisk), length (faces markedDisk)) @?= (5, 5, 1)
+  assertCellularBetti [1, 0, 0] markedDisk
+  (_, floatingSlitDisk) <- refineCellularFixture floatingSlitFixture [] [] [1]
+  (length (vertices floatingSlitDisk), length (edges floatingSlitDisk), length (faces floatingSlitDisk)) @?= (6, 6, 1)
+  assertCellularBetti [1, 0, 0] floatingSlitDisk
+  (_, isolatedSelection) <- refineCellularFixture isolatedPointFixture [4] [] []
+  assertCellularBetti [1, 0, 0] isolatedSelection
+  (_, slitSelection) <- refineCellularFixture floatingSlitFixture [] [4] []
+  assertCellularBetti [1, 0, 0] slitSelection
+
+exactSlitAndContactRefinement :: Assertion
+exactSlitAndContactRefinement = do
+  (_, slitDisk) <- refineCellularFixture attachedSlitFixture [] [] [1]
+  length (edges slitDisk) @?= 5
+  length (faceBoundary slitDisk (FaceId 1)) @?= 6
+  assertCellularBetti [1, 0, 0] slitDisk
+  (_, pointContact) <- refineCellularFixture pointContactFixture [] [] [1, 2]
+  (length (vertices pointContact), length (edges pointContact), length (faces pointContact)) @?= (7, 8, 2)
+  assertCellularBetti [1, 0, 0] pointContact
+
+outerSquarePoints :: [Point]
+outerSquarePoints = [Point (-3) (-3), Point 3 (-3), Point 3 3, Point (-3) 3]
+
+outerSquareDarts :: [(Int, Int, Int, Int)]
+outerSquareDarts =
+  [ (0, 2, 6, 1), (1, 7, 3, 0), (1, 4, 0, 1), (2, 1, 5, 0)
+  , (2, 6, 2, 1), (3, 3, 7, 0), (3, 0, 4, 1), (0, 5, 1, 0)
+  ]
+
+annulusFixture :: CellularFixture
+annulusFixture = CellularFixture
+  (outerSquarePoints <> [Point (-1) (-1), Point 1 (-1), Point 1 1, Point (-1) 1])
+  (fmap Just [0, 2, 4, 6, 8, 10, 12, 14])
+  (outerSquareDarts <>
+    [ (4, 10, 14, 2), (5, 15, 11, 1), (5, 12, 8, 2), (6, 9, 13, 1)
+    , (6, 14, 10, 2), (7, 11, 15, 1), (7, 8, 12, 2), (4, 13, 9, 1)
+    ])
+  (fmap Just [1, 0, 8])
+  [(1, [9])]
+  []
+
+isolatedPointFixture :: CellularFixture
+isolatedPointFixture = CellularFixture
+  (outerSquarePoints <> [Point 0 0])
+  (fmap Just [0, 2, 4, 6] <> [Nothing])
+  outerSquareDarts
+  (fmap Just [1, 0])
+  []
+  [(1, [4])]
+
+floatingSlitFixture :: CellularFixture
+floatingSlitFixture = CellularFixture
+  (outerSquarePoints <> [Point (-1) 0, Point 1 0])
+  (fmap Just [0, 2, 4, 6, 8, 9])
+  (outerSquareDarts <> [(4, 9, 9, 1), (5, 8, 8, 1)])
+  (fmap Just [1, 0])
+  [(1, [8])]
+  []
+
+attachedSlitFixture :: CellularFixture
+attachedSlitFixture = CellularFixture
+  (outerSquarePoints <> [Point 0 0])
+  (fmap Just [0, 2, 4, 6, 9])
+  [ (0, 2, 9, 1), (1, 7, 3, 0), (1, 4, 0, 1), (2, 1, 5, 0)
+  , (2, 6, 2, 1), (3, 3, 7, 0), (3, 8, 4, 1), (0, 5, 1, 0)
+  , (0, 9, 6, 1), (4, 0, 8, 1)
+  ]
+  (fmap Just [1, 0])
+  []
+  []
+
+pointContactFixture :: CellularFixture
+pointContactFixture = CellularFixture
+  [Point (-2) (-2), Point 0 (-2), Point 0 0, Point (-2) 0, Point 2 0, Point 2 2, Point 0 2]
+  (fmap Just [0, 2, 4, 6, 10, 12, 14])
+  [ (0, 2, 6, 1), (1, 7, 3, 0), (1, 4, 0, 1), (2, 1, 9, 0)
+  , (2, 6, 2, 1), (3, 15, 7, 0), (3, 0, 4, 1), (0, 5, 1, 0)
+  , (2, 10, 14, 2), (4, 3, 11, 0), (4, 12, 8, 2), (5, 9, 13, 0)
+  , (5, 14, 10, 2), (6, 11, 15, 0), (6, 8, 12, 2), (2, 13, 5, 0)
+  ]
+  (fmap Just [1, 0, 8])
+  []
+  []
+
 preserveCellInventory :: Assertion
 preserveCellInventory =
   withTriangleComplex $ \cellSet complexValue -> do
@@ -141,6 +319,66 @@
     traverse_ (assertSelectedFaceBoundary complexValue) (faces complexValue)
     fmap (length . edgesAtVertex complexValue) (vertices complexValue) @?= [2, 2, 2]
 
+exhaustiveSelectedIncidence :: Assertion
+exhaustiveSelectedIncidence = do
+  triangulation <- requireDelaunay "incidence square" squarePoints
+  let selections =
+        rights
+          [ exactCellSet triangulation selectedVertices selectedEdges selectedFaces
+          | selectedVertices <- List.subsequences (Resident.vertices triangulation)
+          , selectedEdges <- List.subsequences (undirectedEdges triangulation)
+          , selectedFaces <- List.subsequences (innerFaces triangulation)
+          ]
+      queries = Resident.vertices triangulation <> [VertexId 4, VertexId maxBound]
+  length selections @?= 83
+  traverse_
+    (\selection -> do
+      complexValue <- requireRight "selected cellular refinement" (fromExactCellSet selection)
+      assertIncidenceQueries complexValue queries)
+    selections
+
+sparseHighDegreeIncidence :: Assertion
+sparseHighDegreeIncidence = do
+  triangulation <- requireDelaunay "high-degree incidence fan" fanPoints
+  centralVertex <-
+    requireSome "fan central resident handle"
+      (List.find ((== Point 0 0) . vertexPoint triangulation) (Resident.vertices triangulation))
+  let centralEdges =
+        filter
+          (\edge -> let (source, target) = undirectedEndpoints triangulation edge in centralVertex == source || centralVertex == target)
+          (undirectedEdges triangulation)
+      selectedFamilies = [[], take 1 centralEdges, take 3 centralEdges, centralEdges]
+  assertBool "fixture retains a high-degree central star" (length centralEdges >= 64)
+  traverse_
+    (\selectedEdges -> do
+        let selectedVertices =
+              Set.toAscList
+                ( Set.fromList
+                    (centralVertex : concatMap (\edge -> let (source, target) = undirectedEndpoints triangulation edge in [source, target]) selectedEdges)
+                )
+        selection <- requireRight "sparse fan selection" (exactCellSet triangulation selectedVertices selectedEdges [])
+        complexValue <- requireRight "sparse fan cellular refinement" (fromExactCellSet selection)
+        assertIncidenceQueries complexValue (Resident.vertices triangulation <> [VertexId maxBound]))
+    selectedFamilies
+ where
+  fanPoints =
+    Vector.fromList
+      ( Point 0 0
+          : [ Point (100 * cos angle) (100 * sin angle)
+            | index <- [0 .. 127 :: Int]
+            , let angle = 2 * pi * fromIntegral index / 128
+            ]
+      )
+
+assertIncidenceQueries :: DCELComplex -> [VertexId] -> Assertion
+assertIncidenceQueries complexValue =
+  traverse_
+    (\vertex ->
+        edgesAtVertex complexValue vertex
+          @?= filter
+            (\edge -> let (source, target) = edgeBoundary complexValue edge in vertex == source || vertex == target)
+            (edges complexValue))
+
 preserveExteriorAdjacency :: Assertion
 preserveExteriorAdjacency =
   withTriangleComplex $ \_ complexValue ->
@@ -170,22 +408,55 @@
 exactAlphaBirths = do
   triangulation <- requireDelaunay "obtuse triangle" obtuseTrianglePoints
   filtration <- requireRight "obtuse triangle alpha filtration" (alphaFiltration triangulation)
+  complexValue <- requireRight "obtuse triangle cellular refinement" (fromExactCellSet (alphaFiltrationCellSet filtration))
   let vertexBirthRatios =
-        vertices (fromExactCellSet (alphaFiltrationCellSet filtration))
+        vertices complexValue
           & fmap (fmap alphaBirthRatio . alphaVertexBirth filtration)
       faceBirthRatios =
-        faces (fromExactCellSet (alphaFiltrationCellSet filtration))
+        faces complexValue
           & fmap (fmap alphaBirthRatio . alphaFaceBirth filtration)
   assertBool "every vertex is born at zero" (all (== Just (0, 1)) vertexBirthRatios)
   faceBirthRatios @?= [Just (25, 4)]
   longEdge <- requireSome "obtuse triangle long edge" (edgeBetween triangulation (Point (-2) 0) (Point 2 0))
   fmap alphaBirthRatio (alphaEdgeBirth filtration longEdge) @?= Just (25, 4)
   let shortEdgeBirths =
-        edges (fromExactCellSet (alphaFiltrationCellSet filtration))
+        undirectedEdges triangulation
           & filter (/= longEdge)
           & fmap (fmap alphaBirthRatio . alphaEdgeBirth filtration)
   shortEdgeBirths @?= [Just (5, 4), Just (5, 4)]
 
+constantAlphaVertexSection :: Assertion
+constantAlphaVertexSection = do
+  zeroBirth <- alphaBirthFromRadiusSquared <$> requireRight "zero alpha radius" (mkRadiusSquared 0)
+  traverse_ (assertConstantSection zeroBirth)
+    [ ("empty alpha support", Vector.empty)
+    , ("singleton alpha support", Vector.singleton (Point 3 7))
+    , ("collinear alpha support", Vector.fromList [Point 0 0, Point 1 0, Point 3 0])
+    , ("square alpha support", squarePoints)
+    ]
+ where
+  assertConstantSection :: AlphaBirth -> (String, Vector.Vector Point) -> Assertion
+  assertConstantSection zeroBirth (label, points) = do
+    triangulation <- requireDelaunay label points
+    filtration <- requireRight (label <> " filtration") (alphaFiltration triangulation)
+    let fullCells = alphaFiltrationCellSet filtration
+        vertexCount = exactCellSetVertexCount fullCells
+        zeroCells = alphaComplexAtBirth zeroBirth filtration
+        exactVertices :: ExactCellSet -> [(VertexId, ExactPoint)]
+        exactVertices = foldExactCellVertices (\values vertex point -> (vertex, point) : values) []
+    traverse_
+      (\index -> alphaVertexBirth filtration (VertexId (fromIntegral index)) @?= Just zeroBirth)
+      [0 .. vertexCount - 1]
+    alphaVertexBirth filtration (VertexId (fromIntegral vertexCount)) @?= Nothing
+    alphaVertexBirth filtration (VertexId maxBound) @?= Nothing
+    alphaFiltrationCriticalBirths filtration @?= Set.toAscList (Set.fromList (allAlphaBirths filtration))
+    exactVertices zeroCells @?= exactVertices fullCells
+    exactCellSetEdgeCount zeroCells @?= 0
+    exactCellSetFaceCount zeroCells @?= 0
+    traverse_
+      (\birth -> exactVertices (alphaComplexAtBirth birth filtration) @?= exactVertices fullCells)
+      (alphaFiltrationCriticalBirths filtration)
+
 exactDyadicRationalNormalization :: Assertion
 exactDyadicRationalNormalization = do
   traverse_ assertDyadic
@@ -248,10 +519,10 @@
 
 allAlphaBirths :: AlphaFiltration -> [AlphaBirth]
 allAlphaBirths filtration =
-  let complexValue = fromExactCellSet (alphaFiltrationCellSet filtration)
-   in mapMaybe (alphaVertexBirth filtration) (vertices complexValue)
-        <> mapMaybe (alphaEdgeBirth filtration) (edges complexValue)
-        <> mapMaybe (alphaFaceBirth filtration) (faces complexValue)
+  let cellSet = alphaFiltrationCellSet filtration
+   in mapMaybe (alphaVertexBirth filtration) (foldExactCellVertices (\values vertex _ -> vertex : values) [] cellSet)
+        <> mapMaybe (alphaEdgeBirth filtration) (foldExactCellEdges (flip (:)) [] cellSet)
+        <> mapMaybe (alphaFaceBirth filtration) (foldExactCellFaces (flip (:)) [] cellSet)
 
 alphaBirthRational :: AlphaBirth -> Rational
 alphaBirthRational birth =
@@ -473,7 +744,9 @@
     Right triangulation ->
       case closeFaceCellSet triangulation (innerFaces triangulation) of
         Left selectionFailure -> assertFailure ("triangle selection failed: " <> show selectionFailure)
-        Right cellSet -> assertion cellSet (fromExactCellSet cellSet)
+        Right cellSet -> do
+          complexValue <- requireRight "triangle cellular refinement" (fromExactCellSet cellSet)
+          assertion cellSet complexValue
 
 trianglePoints :: Vector.Vector Point
 trianglePoints =
diff --git a/test/coherence/Main.hs b/test/coherence/Main.hs
--- a/test/coherence/Main.hs
+++ b/test/coherence/Main.hs
@@ -3,17 +3,43 @@
 -- behavioral suites twice.
 module Main (main) where
 
-import Moonlight.Triangulation.AlgebraSpec ()
-import Moonlight.Triangulation.ExactClipRetentionSpec ()
-import Moonlight.Triangulation.LayerOperationsSpec ()
-import Moonlight.Triangulation.MinkowskiSpec ()
-import Moonlight.Triangulation.NativeSpec ()
-import Moonlight.Triangulation.ParallelSpec ()
-import Moonlight.Triangulation.PowerDiagramSpec ()
-import Moonlight.Triangulation.RegionAlgebraSpec ()
-import Moonlight.Triangulation.ScheduleAgreementSpec ()
-import Moonlight.Triangulation.SerializationSpec ()
-import Moonlight.Triangulation.ValuationSpec ()
+import Moonlight.Planar.AlgebraSpec ()
+import Moonlight.Planar.ExactClipRetentionSpec ()
+import Moonlight.Planar.ExactIncidencePublicationSpec ()
+import Moonlight.Planar.IncidenceSpec ()
+import Moonlight.Planar.ConvexSpec ()
+import Moonlight.Planar.LayerOperationsSpec ()
+import Moonlight.Planar.MinkowskiSpec ()
+import Moonlight.Planar.ConstructionSpec ()
+import Moonlight.Planar.ValidationSpec ()
+import Moonlight.Planar.PredicateSpec ()
+import Moonlight.Planar.NearestSpec ()
+import Moonlight.Planar.LocationSpec ()
+import Moonlight.Planar.EditSpec ()
+import Moonlight.Planar.DcelSpec ()
+import Moonlight.Planar.PayloadSpec ()
+import Moonlight.Planar.ConstraintSpec ()
+import Moonlight.Planar.ConstrainedJoinSpec ()
+import Moonlight.Planar.ConstrainedSeamSpec ()
+import Moonlight.Planar.RefinementSpec ()
+import Moonlight.Planar.AlphaSpec ()
+import Moonlight.Planar.RegionSpec ()
+import Moonlight.Planar.ExactEmbeddingSpec ()
+import Moonlight.Planar.ExactSegmentEventsSpec ()
+import Moonlight.Planar.OverlaySpec ()
+import Moonlight.Planar.OverlaySelectionSpec ()
+import Moonlight.Planar.OverlayPublicationSpec ()
+import Moonlight.Planar.FilteredPredicateOptimizationSpec ()
+import Moonlight.Planar.ParallelSpec ()
+import Moonlight.Planar.RegularTopologySpec ()
+import Moonlight.Planar.RegularEditSpec ()
+import Moonlight.Planar.RegularAlphaSpec ()
+import Moonlight.Planar.PowerDiagramSpec ()
+import Moonlight.Planar.PowerMassSpec ()
+import Moonlight.Planar.RegionAlgebraSpec ()
+import Moonlight.Planar.ScheduleAgreementSpec ()
+import Moonlight.Planar.SerializationSpec ()
+import Moonlight.Planar.ValuationSpec ()
 
 main :: IO ()
 main = pure ()
diff --git a/test/curve/Main.hs b/test/curve/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Main.hs
@@ -0,0 +1,10 @@
+module Main (main) where
+
+import qualified Moonlight.Planar.AffineSpec as Affine
+import qualified Moonlight.Planar.CurveSpec as Curve
+import qualified Moonlight.Planar.Curve.AuthoringSpec as Authoring
+import qualified Moonlight.Planar.CurveLoweringSpec as Lowering
+import qualified Moonlight.Planar.RegionAdmissionSpec as Region
+
+main :: IO ()
+main = () <$ sequenceA [Curve.tests, Authoring.tests, Affine.tests, Lowering.tests, Region.tests]
diff --git a/test/curve/Moonlight/Planar/AffineSpec.hs b/test/curve/Moonlight/Planar/AffineSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/AffineSpec.hs
@@ -0,0 +1,204 @@
+-- | Exact action laws and witness-preserving polygon transport.
+module Moonlight.Planar.AffineSpec (tests) where
+
+import Data.Foldable (traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Affine
+import Moonlight.Planar.Convex (convexPolygon, convexPolygonPoints)
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , ExactRational
+  , ExactVector (..)
+  , exactHalf
+  , exactPoint
+  , exactThird
+  , translateExactPoint
+  )
+import Moonlight.Planar.Region
+  ( exactLoop
+  , exactLoopPoints
+  , planarRegion
+  , planarRegionComponents
+  , polygonComponent
+  , polygonHoleLoops
+  , polygonOuterLoop
+  , regionPointLocation
+  )
+import Support (assertEqual, requireRight)
+
+tests :: IO ()
+tests = sequence_
+  [ testAffineActions
+  , testIsoAdmission
+  , testIsoInverse
+  , testTranslationIso
+  , testRegionTransport
+  , testConvexTransport
+  , putStrLn "affine: ok"
+  ]
+
+reflection :: Affine2
+reflection = affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 7 3)
+
+shear :: Affine2
+shear = affine2 (ExactVector 2 1) (ExactVector 1 1) (ExactVector (-4) 5)
+
+collapse :: Affine2
+collapse = affine2 (ExactVector 1 2) (ExactVector 2 4) (ExactVector 3 7)
+
+testAffineActions :: IO ()
+testAffineActions = do
+  let transforms = [identityAffine2, reflection, shear, collapse]
+      point = exactPoint 3 (-2)
+      vector = ExactVector (-1) 4
+  traverse_
+    (\transform -> do
+       assertEqual "left affine identity" transform (composeAffine2 identityAffine2 transform)
+       assertEqual "right affine identity" transform (composeAffine2 transform identityAffine2)
+       assertEqual "affine preserves point displacement"
+         (transformPoint transform (translateExactPoint point vector))
+         (translateExactPoint (transformPoint transform point) (transformVector transform vector)))
+    transforms
+  traverse_
+    (\(outer, inner) -> do
+       let composed = composeAffine2 outer inner
+       assertEqual "point composition order"
+         (transformPoint outer (transformPoint inner point))
+         (transformPoint composed point)
+       assertEqual "vector composition order"
+         (transformVector outer (transformVector inner vector))
+         (transformVector composed vector)
+       assertEqual "determinants multiply"
+         (affineDeterminant outer * affineDeterminant inner)
+         (affineDeterminant composed))
+    [(outer, inner) | outer <- transforms, inner <- transforms]
+  traverse_
+    (\(a, b, c) -> assertEqual "affine composition associativity"
+      (composeAffine2 (composeAffine2 a b) c)
+      (composeAffine2 a (composeAffine2 b c)))
+    [(a, b, c) | a <- transforms, b <- transforms, c <- transforms]
+  assertEqual "singular point action is still lawful"
+    (exactPoint 2 5) (transformPoint collapse point)
+  assertEqual "column observation"
+    (ExactVector 2 1, ExactVector 1 1, ExactVector (-4) 5)
+    (affineColumns shear)
+
+testTranslationIso :: IO ()
+testTranslationIso = do
+  let offset = ExactVector exactThird (-7)
+      other = ExactVector 4 exactHalf
+      translation = translationAffineIso2 offset
+      point = exactPoint 2 3
+      vector = ExactVector 5 6
+  assertEqual "zero translation is identity" identityAffineIso2
+    (translationAffineIso2 (ExactVector 0 0))
+  assertEqual "translation moves points" (translateExactPoint point offset)
+    (transformPoint (affineIsoMap translation) point)
+  assertEqual "translation leaves vectors unchanged" vector
+    (transformVector (affineIsoMap translation) vector)
+  assertEqual "translation determinant" 1 (affineDeterminant (affineIsoMap translation))
+  assertEqual "translation inverse" (translationAffineIso2 (ExactVector (negate exactThird) 7))
+    (inverseAffineIso2 translation)
+  assertEqual "translation composition"
+    (translationAffineIso2 (ExactVector (4 + exactThird) (exactHalf - 7)))
+    (composeAffineIso2 translation (translationAffineIso2 other))
+
+testIsoAdmission :: IO ()
+testIsoAdmission = do
+  assertEqual "singular map cannot transport topology" Nothing (affineIso2 collapse)
+  assertEqual "identity iso admission" (Just identityAffineIso2) (affineIso2 identityAffine2)
+  reversing <- requireIso reflection
+  preserving <- requireIso shear
+  traverse_
+    (\(outer, inner) -> assertEqual "iso composition agrees with checked admission"
+      (affineIso2 (composeAffine2 (affineIsoMap outer) (affineIsoMap inner)))
+      (Just (composeAffineIso2 outer inner)))
+    [(outer, inner) | outer <- [reversing, preserving], inner <- [reversing, preserving]]
+
+testIsoInverse :: IO ()
+testIsoInverse = do
+  let rationalFrame = affine2
+        (ExactVector 2 1) (ExactVector exactThird exactHalf) (ExactVector 5 (-3))
+      point = exactPoint exactHalf (negate exactThird)
+      vector = ExactVector exactThird 7
+  frames <- traverse requireIso
+    [identityAffine2, reflection, shear, rationalFrame, composeAffine2 reflection rationalFrame]
+  traverse_
+    (\frame -> do
+       let inverse = inverseAffineIso2 frame
+       assertEqual "inverse on the left" identityAffineIso2 (composeAffineIso2 inverse frame)
+       assertEqual "inverse on the right" identityAffineIso2 (composeAffineIso2 frame inverse)
+       assertEqual "inverse involution" frame (inverseAffineIso2 inverse)
+       assertEqual "inverse agrees with checked admission"
+         (Just inverse) (affineIso2 (affineIsoMap inverse))
+       assertEqual "point inverse roundtrip" point
+         (transformPoint (affineIsoMap inverse) (transformPoint (affineIsoMap frame) point))
+       assertEqual "vector inverse roundtrip" vector
+         (transformVector (affineIsoMap inverse) (transformVector (affineIsoMap frame) vector)))
+    frames
+  traverse_
+    (\(outer, inner) -> assertEqual "inverse reverses composition order"
+      (composeAffineIso2 (inverseAffineIso2 inner) (inverseAffineIso2 outer))
+      (inverseAffineIso2 (composeAffineIso2 outer inner)))
+    [(outer, inner) | outer <- frames, inner <- frames]
+
+testRegionTransport :: IO ()
+testRegionTransport = do
+  outer <- requireRight "outer loop" (exactLoop (rectangle 0 0 8 8))
+  holeLeft <- requireRight "left clockwise hole" (exactLoop (NonEmpty.reverse (rectangle 1 1 3 3)))
+  holeRight <- requireRight "right clockwise hole" (exactLoop (NonEmpty.reverse (rectangle 5 1 7 3)))
+  component <- requireRight "pierced component" (polygonComponent outer [holeLeft, holeRight])
+  islandLoop <- requireRight "separate island" (exactLoop (rectangle 10 0 12 2))
+  island <- requireRight "island component" (polygonComponent islandLoop [])
+  source <- requireRight "disjoint region" (planarRegion [component, island])
+  reversing <- requireIso reflection
+  preserving <- requireIso shear
+  assertEqual "region transport identity" source (transformPlanarRegion identityAffineIso2 source)
+  traverse_
+    (\iso -> do
+       let mapped = transformPlanarRegion iso source
+           transform = affineIsoMap iso
+           expectedLoop loop = exactLoop
+             ((if affineDeterminant transform < 0 then NonEmpty.reverse else id)
+               (fmap (transformPoint transform) (exactLoopPoints loop)))
+       checkedComponents <- traverse
+         (\original -> do
+           checkedOuter <- requireRight "mapped outer admission" (expectedLoop (polygonOuterLoop original))
+           checkedHoles <- traverse
+             (requireRight "mapped hole admission" . expectedLoop)
+             (polygonHoleLoops original)
+           requireRight "mapped component admission" (polygonComponent checkedOuter checkedHoles))
+         (planarRegionComponents source)
+       checked <- requireRight "mapped region admission" (planarRegion checkedComponents)
+       assertEqual "transport equals checked canonical region" checked mapped
+       assertEqual "inverse restores canonical region with sorted holes and components" source
+         (transformPlanarRegion (inverseAffineIso2 iso) mapped)
+       traverse_
+         (\query -> assertEqual "point location commutes with iso transport"
+           (regionPointLocation source query)
+           (regionPointLocation mapped (transformPoint transform query)))
+         [exactPoint 0 0, exactPoint 1 1, exactPoint 1 2, exactPoint 2 2, exactPoint 4 4, exactPoint 9 9, exactPoint 11 1])
+    [reversing, preserving]
+  assertEqual "canonical region action composition"
+    (transformPlanarRegion reversing (transformPlanarRegion preserving source))
+    (transformPlanarRegion (composeAffineIso2 reversing preserving) source)
+
+testConvexTransport :: IO ()
+testConvexTransport = do
+  source <- requireRight "convex rectangle" (convexPolygon (rectangle 0 0 4 3))
+  reversing <- requireIso reflection
+  let transformed = transformConvexPolygon reversing source
+  expected <- requireRight "reflected convex admission"
+    (convexPolygon (NonEmpty.reverse (fmap (transformPoint reflection) (convexPolygonPoints source))))
+  assertEqual "reflection retains strict counterclockwise convexity" expected transformed
+  assertEqual "convex transport identity" source (transformConvexPolygon identityAffineIso2 source)
+  assertEqual "convex inverse restores canonical cycle" source
+    (transformConvexPolygon (inverseAffineIso2 reversing) transformed)
+
+rectangle :: ExactRational -> ExactRational -> ExactRational -> ExactRational -> NonEmpty ExactPoint
+rectangle x0 y0 x1 y1 =
+  exactPoint x0 y0 :| [exactPoint x1 y0, exactPoint x1 y1, exactPoint x0 y1]
+
+requireIso :: Affine2 -> IO AffineIso2
+requireIso = maybe (fail "fixture affine map is singular") pure . affineIso2
diff --git a/test/curve/Moonlight/Planar/Curve/AuthoringSpec.hs b/test/curve/Moonlight/Planar/Curve/AuthoringSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/Curve/AuthoringSpec.hs
@@ -0,0 +1,189 @@
+module Moonlight.Planar.Curve.AuthoringSpec (tests) where
+
+import Data.Foldable (toList, traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Moonlight.Planar.Affine
+  ( affine2, transformPoint, transformVector )
+import Moonlight.Planar.Curve
+  ( ClosedTrail, CurveStep, Located, OpenTrail, closedTrailSteps, curveStepEnd
+  , endJet, evaluateStep, joinContinuity, JoinContinuity (..), locatedValue
+  , location, openTrail, reverseClosedTrail, reverseLocatedTrail, startJet, trailDisplacement
+  , trailSteps, transformLocatedClosedTrail, transformLocatedTrail )
+import Moonlight.Planar.Curve.Authoring
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactVector (..), exactHalf, exactPoint, exactPointCoordinates
+  , translateExactPoint, unitHalf, unitOne, unitZero )
+import Support (assertEqual)
+
+tests :: IO ()
+tests = sequence_
+  [ testInterpolation
+  , testExplicitCorners
+  , testTensionAndReversal
+  , testAffineNaturality
+  , testProfiles
+  , testPolygonAndBow
+  , putStrLn "curve-authoring: ok"
+  ]
+
+landmarks :: NonEmpty ExactPoint
+landmarks = exactPoint 0 0 :| [exactPoint 3 5, exactPoint 8 6, exactPoint 12 0]
+
+testInterpolation :: IO ()
+testInterpolation = do
+  let knots = Interpolating <$> landmarks
+      authored = cardinalOpen unitZero knots
+      closed = cardinalClosed unitZero knots
+      openSteps = toList (trailSteps (locatedValue authored))
+      cycleSteps = toList (closedTrailSteps (locatedValue closed))
+  assertEqual "open interpolation visits every landmark" (NonEmpty.toList landmarks) (anchors authored)
+  assertEqual "one span per consecutive open knot" 3 (length openSteps)
+  assertEqual "closed interpolation visits every landmark and closes"
+    (NonEmpty.toList landmarks <> [exactPoint 0 0]) (closedAnchors closed)
+  assertEqual "one span per cyclic knot" 4 (length cycleSteps)
+  assertEqual "regular interior C1 joins" [ParametricJoin, ParametricJoin]
+    (zipWith joinContinuity openSteps (drop 1 openSteps))
+  assertEqual "regular periodic C1 joins" (replicate 4 ParametricJoin)
+    (zipWith joinContinuity cycleSteps (drop 1 cycleSteps <> take 1 cycleSteps))
+  let single = cardinalOpen unitZero (Interpolating (exactPoint 7 9) :| [])
+      singleClosed = cardinalClosed unitZero (Interpolating (exactPoint 7 9) :| [])
+  assertEqual "singleton open retains location" [exactPoint 7 9] (anchors single)
+  assertEqual "singleton closed is constant" [exactPoint 7 9, exactPoint 7 9] (closedAnchors singleClosed)
+  assertEqual "repeated points require no divide or admission"
+    [exactPoint 2 3, exactPoint 2 3]
+    (anchors (cardinalOpen unitZero (Interpolating (exactPoint 2 3) :| [Interpolating (exactPoint 2 3)])))
+
+testExplicitCorners :: IO ()
+testExplicitCorners = do
+  let incoming = ExactVector 3 0
+      outgoing = ExactVector 0 6
+      knots = Interpolating (exactPoint 0 0) :|
+        [ExplicitJets (exactPoint 2 2) incoming outgoing, Interpolating (exactPoint 4 7)]
+      spans = toList (trailSteps (locatedValue (cardinalOpen unitOne knots)))
+  assertEqual "explicit corner survives maximum automatic tension" [CornerJoin]
+    (zipWith joinContinuity spans (drop 1 spans))
+  assertEqual "explicit incoming jet" [incoming] (endJet <$> take 1 spans)
+  assertEqual "explicit outgoing jet" [outgoing] (startJet <$> drop 1 spans)
+  let returning = cardinalClosed unitZero (ExplicitJets (exactPoint 0 0) incoming outgoing :| [])
+  assertEqual "single explicit closed knot retains returning cubic jets"
+    [(outgoing,incoming)]
+    ((\step -> (startJet step,endJet step)) <$> toList (closedTrailSteps (locatedValue returning)))
+
+testTensionAndReversal :: IO ()
+testTensionAndReversal = do
+  let knots = Interpolating <$> landmarks
+      smooth = toList (trailSteps (locatedValue (cardinalOpen unitZero knots)))
+      tense = toList (trailSteps (locatedValue (cardinalOpen unitOne knots)))
+      half = toList (trailSteps (locatedValue (cardinalOpen unitHalf knots)))
+  assertEqual "maximum tension zeros automatic jets" (replicate 3 (zero,zero))
+    ((\step -> (startJet step,endJet step)) <$> tense)
+  assertEqual "tension scales derived jets"
+    ((\step -> (halfVector (startJet step),halfVector (endJet step))) <$> smooth)
+    ((\step -> (startJet step,endJet step)) <$> half)
+  assertEqual "open interpolation commutes with reversal"
+    (reverseLocatedTrail (cardinalOpen unitHalf knots))
+    (cardinalOpen unitHalf (NonEmpty.reverse knots))
+  let initial :| remaining = knots
+      closed = cardinalClosed unitHalf knots
+      reversed = cardinalClosed unitHalf (initial :| reverse remaining)
+  assertEqual "closed reversal preserves authored first landmark" (location closed) (location reversed)
+  assertEqual "periodic interpolation commutes with anchored reversal"
+    (reverseClosedTrail (locatedValue closed)) (locatedValue reversed)
+
+testAffineNaturality :: IO ()
+testAffineNaturality = do
+  let transforms =
+        [ affine2 (ExactVector 2 1) (ExactVector (-3) 4) (ExactVector 7 8)
+        , affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 2 3)
+        , affine2 (ExactVector 1 2) (ExactVector 2 4) zero
+        ]
+      knots = Interpolating (exactPoint 0 0) :|
+        [ExplicitJets (exactPoint 4 6) (ExactVector 3 2) (ExactVector (-2) 5), Interpolating (exactPoint 8 0)]
+  traverse_ (\transform -> do
+    let mapped = fmap (\knot -> case knot of
+          Interpolating point -> Interpolating (transformPoint transform point)
+          ExplicitJets point incoming outgoing -> ExplicitJets (transformPoint transform point)
+            (transformVector transform incoming) (transformVector transform outgoing)) knots
+        mappedStations = fmap (\(ProfileStation center width) ->
+          ProfileStation (transformPoint transform center) (transformVector transform width)) stations
+    assertEqual "cardinal open affine naturality"
+      (transformLocatedTrail transform (cardinalOpen unitHalf knots)) (cardinalOpen unitHalf mapped)
+    assertEqual "cardinal closed affine naturality"
+      (transformLocatedClosedTrail transform (cardinalClosed unitHalf knots)) (cardinalClosed unitHalf mapped)
+    assertEqual "profile affine naturality"
+      (transformLocatedClosedTrail transform (profileOutline unitHalf stations))
+      (profileOutline unitHalf mappedStations)) transforms
+
+stations :: NonEmpty ProfileStation
+stations = ProfileStation (exactPoint 0 0) zero :|
+  [ProfileStation (exactPoint 3 6) (ExactVector 4 1), ProfileStation (exactPoint 8 10) zero]
+
+testProfiles :: IO ()
+testProfiles = do
+  let (positive,negative) = profileRails unitZero stations
+      outline = profileOutline unitZero stations
+      centerKnots = fmap (\(ProfileStation center _) -> Interpolating center) stations
+      centerline = cardinalOpen unitZero centerKnots
+      samples = zip3 (locatedSteps positive) (locatedSteps negative) (locatedSteps centerline)
+  assertEqual "positive station interpolation"
+    [exactPoint 0 0, exactPoint 7 7, exactPoint 8 10] (anchors positive)
+  assertEqual "negative station interpolation"
+    [exactPoint 0 0, exactPoint (-1) 5, exactPoint 8 10] (anchors negative)
+  assertEqual "tapered root rails meet" (location positive) (location negative)
+  assertEqual "tapered tip rails meet" (endpoint positive) (endpoint negative)
+  assertEqual "profile outline derives closure" zero
+    (trailDisplacement (openTrail (closedTrailSteps (locatedValue outline))))
+  traverse_ (\(p,n,c) -> assertEqual "rail midpoint is interpolated centerline"
+    (twicePoint (sample c)) (sumPoints (sample p) (sample n))) samples
+  let reversedStations = NonEmpty.reverse stations
+      (reversePositive,reverseNegative) = profileRails unitZero reversedStations
+  assertEqual "positive rail reversal" (reverseLocatedTrail positive) reversePositive
+  assertEqual "negative rail reversal" (reverseLocatedTrail negative) reverseNegative
+ where
+  sample :: (ExactPoint,CurveStep) -> ExactPoint
+  sample (origin,step) = translateExactPoint origin (evaluateStep unitHalf step)
+  sumPoints :: ExactPoint -> ExactPoint -> ExactVector
+  sumPoints a b = let (ax,ay) = exactPointCoordinates a; (bx,by) = exactPointCoordinates b
+                  in ExactVector (ax+bx) (ay+by)
+  twicePoint :: ExactPoint -> ExactVector
+  twicePoint point = sumPoints point point
+
+testPolygonAndBow :: IO ()
+testPolygonAndBow = do
+  let points = exactPoint 1 2 :| [exactPoint 5 2, exactPoint 5 7]
+      outline = polygonTrail points
+      positiveBow = bowedTrail exactHalf (exactPoint 1 2) (exactPoint 5 2)
+      negativeBow = bowedTrail (negate exactHalf) (exactPoint 1 2) (exactPoint 5 2)
+  assertEqual "polygon preserves input order and closes"
+    [exactPoint 1 2, exactPoint 5 2, exactPoint 5 7, exactPoint 1 2] (closedAnchors outline)
+  assertEqual "bow retains endpoints" [exactPoint 1 2,exactPoint 5 2] (anchors positiveBow)
+  assertEqual "positive bow exact midpoint sagitta" [exactPoint 3 4] (midpoints positiveBow)
+  assertEqual "negative bow exact midpoint sagitta" [exactPoint 3 0] (midpoints negativeBow)
+  assertEqual "bow reversal negates signed local bow" (reverseLocatedTrail positiveBow)
+    (bowedTrail (negate exactHalf) (exactPoint 5 2) (exactPoint 1 2))
+  assertEqual "zero chord remains constant" [exactPoint 2 3]
+    (midpoints (bowedTrail 100 (exactPoint 2 3) (exactPoint 2 3)))
+
+anchors :: Located OpenTrail -> [ExactPoint]
+anchors value = scanl (\point step -> translateExactPoint point (curveStepEnd step))
+  (location value) (toList (trailSteps (locatedValue value)))
+
+closedAnchors :: Located ClosedTrail -> [ExactPoint]
+closedAnchors value = scanl (\point step -> translateExactPoint point (curveStepEnd step))
+  (location value) (toList (closedTrailSteps (locatedValue value)))
+
+locatedSteps :: Located OpenTrail -> [(ExactPoint,CurveStep)]
+locatedSteps value = zip (anchors value) (toList (trailSteps (locatedValue value)))
+
+endpoint :: Located OpenTrail -> ExactPoint
+endpoint value = translateExactPoint (location value) (trailDisplacement (locatedValue value))
+
+midpoints :: Located OpenTrail -> [ExactPoint]
+midpoints = fmap (\(origin,step) -> translateExactPoint origin (evaluateStep unitHalf step)) . locatedSteps
+
+halfVector :: ExactVector -> ExactVector
+halfVector (ExactVector x y) = ExactVector (exactHalf*x) (exactHalf*y)
+
+zero :: ExactVector
+zero = ExactVector 0 0
diff --git a/test/curve/Moonlight/Planar/CurveLoweringSpec.hs b/test/curve/Moonlight/Planar/CurveLoweringSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/CurveLoweringSpec.hs
@@ -0,0 +1,169 @@
+module Moonlight.Planar.CurveLoweringSpec (tests) where
+
+import Control.Monad (unless)
+import Control.Exception (TypeError, displayException, evaluate, try)
+import Data.List (isInfixOf)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine (Affine2, affine2, identityAffine2)
+import Moonlight.Planar.Curve
+  ( CurveStep, ClosedTrail, Located, curveStep, line, quadratic, cubic
+  , rationalQuadratic, locate, openTrail, closeWith )
+import Moonlight.Planar.Curve.Lowering
+import Moonlight.Planar.Curve.Region
+import qualified Moonlight.Planar.CurveLoweringTypeErrors as Rejected
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactVector (..), exactPoint, exactPointCoordinates
+  , exactHalf, positiveExact, positiveOne, positiveTwo )
+import Moonlight.Planar.Region (RegionPointLocation (..), regionPointLocation)
+
+tests :: IO ()
+tests = do
+  testPolicyRefusals
+  testFiniteChord
+  testSubdivision
+  testMetric
+  testConic
+  testRegion
+  testCertificateAdmission
+  putStrLn "curve lowering: ok"
+
+testPolicyRefusals :: IO ()
+testPolicyRefusals = do
+  assertRefusal "negative depth" (loweringPolicy positiveOne identityAffine2 (-1) 10)
+  assertRefusal "zero leaves" (loweringPolicy positiveOne identityAffine2 10 0)
+  assertRefusal "zero tolerance" (positiveExact 0)
+
+testFiniteChord :: IO ()
+testFiniteChord = do
+  policy <- makePolicy 1 identityAffine2 0 10
+  straight <- requireRight "line" (lowerStep policy (atOrigin (curveStep line (vector 1 0))))
+  assertEqual "line has no deviation" [0] (map spanSquaredBound (loweredSpans straight))
+  assertRefusal "finite chord catches collinear overshoot"
+    (lowerStep policy (atOrigin (curveStep (quadratic (vector 10 0)) (vector 1 0))))
+  assertRefusal "zero chord is not a constant curve"
+    (lowerStep policy (atOrigin (curveStep (cubic (vector 0 10) (vector 10 0)) (vector 0 0))))
+  constant <- requireRight "constant source" (lowerStep policy (atOrigin (curveStep line (vector 0 0))))
+  assertEqual "constant bound" [0] (map spanSquaredBound (loweredSpans constant))
+
+testSubdivision :: IO ()
+testSubdivision = do
+  policy <- makePolicy exactHalf identityAffine2 12 1024
+  let curve = atOrigin (curveStep (cubic (vector 0 8) (vector 8 8)) (vector 8 0))
+  lowered <- requireRight "adaptive cubic" (lowerStep policy curve)
+  let spans = loweredSpans lowered
+      points = loweredPoints lowered
+  assert "adaptive output subdivides" (length spans > 1)
+  assert "every leaf carries its admitted bound" (all ((<= exactHalf * exactHalf) . spanSquaredBound) spans)
+  assertEqual "start retained" (point 0 0) (NonEmpty.head points)
+  assertEqual "end retained" (point 8 0) (NonEmpty.last points)
+  assertEqual "parameter start" [0] (take 1 (map spanParameterFrom spans))
+  assertEqual "parameter end" [1] (take 1 (reverse (map spanParameterTo spans)))
+  assert "source intervals glue exactly"
+    (and (zipWith (\left right -> spanParameterTo left == spanParameterFrom right) spans (drop 1 spans)))
+  assertEqual "shared seam reversal reuses samples" (NonEmpty.reverse points)
+    (loweredPoints (reverseLoweredPath lowered))
+  assertEqual "reversal involution" lowered (reverseLoweredPath (reverseLoweredPath lowered))
+  smallBudget <- makePolicy exactHalf identityAffine2 12 1
+  case lowerStep smallBudget curve of
+    Left (LeafBudgetExhausted _ _ _) -> pure ()
+    other -> fail ("expected leaf budget refusal, received " <> show other)
+  let twoLines = locate (point 0 0) (openTrail (Seq.fromList
+        [curveStep line (vector 1 0), curveStep line (vector 0 1)]))
+  case lowerOpenTrail smallBudget twoLines of
+    Left (LeafBudgetExhausted 1 0 1) -> pure ()
+    other -> fail ("leaf budget must span the whole trail: " <> show other)
+  empty <- requireRight "empty open trail" (lowerOpenTrail policy (locate (point 3 4) (openTrail Seq.empty)))
+  assertEqual "empty open trail retains location" [point 3 4] (NonEmpty.toList (loweredPoints empty))
+
+testMetric :: IO ()
+testMetric = do
+  let curve = atOrigin (curveStep (quadratic (vector exactHalf exactHalf)) (vector 1 0))
+      stretch = affine2 (vector 1 0) (vector 0 100) (vector 0 0)
+  local <- makePolicy 1 identityAffine2 0 10
+  output <- makePolicy 1 stretch 0 10
+  _ <- requireRight "local bound admits" (lowerStep local curve)
+  assertRefusal "output metric sees anisotropic magnification" (lowerStep output curve)
+  translated <- makePolicy 1 (affine2 (vector 1 0) (vector 0 1) (vector 100 200)) 0 10
+  localResult <- requireRight "local result" (lowerStep local curve)
+  translatedResult <- requireRight "translated metric" (lowerStep translated curve)
+  assertEqual "metric does not relocate samples" (loweredPoints localResult) (loweredPoints translatedResult)
+  assertEqual "metric translation preserves bounds" (loweredSpans localResult) (loweredSpans translatedResult)
+  assertEqual "receipt retains its metric" identityAffine2 (loweredMetric localResult)
+
+testConic :: IO ()
+testConic = do
+  policy <- makePolicy (exactHalf * exactHalf) identityAffine2 12 1024
+  let quarter = locate (point 1 0)
+        (curveStep (rationalQuadratic (vector 0 1) positiveOne positiveTwo) (vector (-1) 1))
+  lowered <- requireRight "positive rational circular arc" (lowerStep policy quarter)
+  assert "exact conic samples lie on the unit circle"
+    (all (\p -> let (x, y) = exactPointCoordinates p in x*x + y*y == 1) (loweredPoints lowered))
+
+testRegion :: IO ()
+testRegion = do
+  policy <- makePolicy 1 identityAffine2 8 100
+  let outer = closed (point 0 0) [vector 4 0, vector 0 4, vector (-4) 0]
+      hole = closed (point 1 1) [vector 0 1, vector 1 0, vector 0 (-1)]
+  (region, receipts) <- requireRight "explicit polygon with hole"
+    (lowerSimpleRegion policy [CurveComponent outer [hole]])
+  assertEqual "one receipt per contour" 2 (length receipts)
+  assertEqual "outer interior" RegionInterior (regionPointLocation region (point 3 3))
+  assertEqual "hole exterior" RegionExterior
+    (regionPointLocation region (point (1 + exactHalf) (1 + exactHalf)))
+  let crossing = closed (point 0 0) [vector 2 2, vector (-2) 0, vector 2 (-2)]
+  assertRefusal "sampled crossing refuses simple region"
+    (lowerSimpleRegion policy [CurveComponent crossing []])
+  assertRefusal "outer winding cannot masquerade as a hole"
+    (lowerSimpleRegion policy [CurveComponent outer [outer]])
+
+testCertificateAdmission :: IO ()
+testCertificateAdmission = do
+  policy <- makePolicy 1 identityAffine2 0 1
+  lowered <- requireRight "admitted span for public-client probe"
+    (lowerStep policy (atOrigin (curveStep line (vector 1 0))))
+  case loweredSpans lowered of
+    [sample] -> do
+      let (observed, forgedField) = Rejected.recordSpanBound sample
+      assertEqual "ordinary bound observation remains lawful" 0 observed
+      result <- try (evaluate (forgedField `seq` ())) :: IO (Either TypeError ())
+      case result of
+        Left failure ->
+          assert "record capability rejected for the expected reason"
+            ("HasField" `isInfixOf` displayException failure
+              && "spanSquaredBound" `isInfixOf` displayException failure)
+        Right () -> fail "LoweredSpan leaked its record-update capability"
+    _ -> fail "one linear step must produce one admitted span"
+
+closed :: ExactPoint -> [ExactVector] -> Located ClosedTrail
+closed origin directions = locate origin (closeWith line (openTrail (Seq.fromList (map (curveStep line) directions))))
+
+atOrigin :: CurveStep -> Located CurveStep
+atOrigin = locate (point 0 0)
+
+point :: ExactRational -> ExactRational -> ExactPoint
+point = exactPoint
+
+vector :: ExactRational -> ExactRational -> ExactVector
+vector = ExactVector
+
+makePolicy :: ExactRational -> Affine2 -> Int -> Int -> IO LoweringPolicy
+makePolicy epsilon metric depth leaves = do
+  tolerance <- requireRight "positive test tolerance" (positiveExact epsilon)
+  requireRight "test lowering policy" (loweringPolicy tolerance metric depth leaves)
+
+requireRight :: Show obstruction => String -> Either obstruction value -> IO value
+requireRight label = either (fail . ((label <> ": ") <>) . show) pure
+
+assertRefusal :: String -> Either obstruction value -> IO ()
+assertRefusal label result = case result of
+  Left _ -> pure ()
+  Right _ -> fail (label <> ": unexpectedly admitted")
+
+assertEqual :: (Eq value, Show value) => String -> value -> value -> IO ()
+assertEqual label expected actual = assert
+  (label <> ": expected " <> show expected <> ", received " <> show actual)
+  (expected == actual)
+
+assert :: String -> Bool -> IO ()
+assert label condition = unless condition (fail label)
diff --git a/test/curve/Moonlight/Planar/CurveLoweringTypeErrors.hs b/test/curve/Moonlight/Planar/CurveLoweringTypeErrors.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/CurveLoweringTypeErrors.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors #-}
+
+-- | Only this deliberately ill-typed public client defers errors. A lawful
+-- observer must not also supply the record-field capability that enables updates.
+module Moonlight.Planar.CurveLoweringTypeErrors (recordSpanBound) where
+
+import GHC.Records (getField)
+import Moonlight.Planar.Curve.Lowering (LoweredSpan, spanSquaredBound)
+import Moonlight.Planar.Exact (ExactRational)
+
+recordSpanBound :: LoweredSpan -> (ExactRational, ExactRational)
+recordSpanBound sample = (spanSquaredBound sample, getField @"spanSquaredBound" sample)
diff --git a/test/curve/Moonlight/Planar/CurveSpec.hs b/test/curve/Moonlight/Planar/CurveSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/CurveSpec.hs
@@ -0,0 +1,195 @@
+-- | Exact laws of the authored curve algebra, independently of flattening.
+module Moonlight.Planar.CurveSpec (tests) where
+
+import Data.Foldable (toList, traverse_)
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+  ( affine2, composeAffine2, identityAffine2, transformPoint, transformVector )
+import Moonlight.Planar.Curve
+import Moonlight.Planar.Exact
+  ( ExactVector (..), ScalarRefinementError (..), UnitInterval
+  , addExactVectors, blendPositive, divideByPositive, exactHalf, exactPoint
+  , exactPointCoordinates, exactRational, positiveExact, positiveExactValue
+  , positiveOne, positiveSumSquares, positiveTwo, ratioPositive
+  , translateExactPoint, unitHalf, unitInterval, unitIntervalValue, unitOne, unitZero
+  )
+import Support (assertEqual, requireRight)
+
+tests :: IO ()
+tests = sequence_
+  [ testRefinements
+  , testParameterizedLaws
+  , testTrails
+  , testCircles
+  , testJets
+  , testAffineActions
+  , putStrLn "curve: ok"
+  ]
+
+testRefinements :: IO ()
+testRefinements = do
+  assertEqual "zero is not positive" (Left (ExactNotPositive 0)) (positiveExact 0)
+  assertEqual "negative is not positive" (Left (ExactNotPositive (-1))) (positiveExact (-1))
+  assertEqual "negative parameter refused" (Left (ExactOutsideUnitInterval (-1))) (unitInterval (-1))
+  assertEqual "parameter above one refused" (Left (ExactOutsideUnitInterval 2)) (unitInterval 2)
+  assertEqual "zero parameter admitted" (Right unitZero) (unitInterval 0)
+  assertEqual "one parameter admitted" (Right unitOne) (unitInterval 1)
+  assertEqual "positive blend at zero" positiveOne (blendPositive unitZero positiveOne positiveTwo)
+  assertEqual "positive blend at one" positiveTwo (blendPositive unitOne positiveOne positiveTwo)
+  assertEqual "positive blend at half" (1 + exactHalf)
+    (positiveExactValue (blendPositive unitHalf positiveOne positiveTwo))
+  assertEqual "positive division" exactHalf (divideByPositive 1 positiveTwo)
+  assertEqual "positive ratio" exactHalf (positiveExactValue (ratioPositive positiveOne positiveTwo))
+  assertEqual "zero norm" Nothing (positiveSumSquares 0 0)
+  assertEqual "nonzero norm has witness" (Just 25) (positiveExactValue <$> positiveSumSquares (-3) 4)
+  assertEqual "polynomial quadratic canonicalization"
+    (quadratic (ExactVector 2 3))
+    (rationalQuadratic (ExactVector 2 3) positiveOne positiveOne)
+
+parameters :: IO [UnitInterval]
+parameters = traverse admit [(0,1), (1,7), (1,3), (1,2), (4,5), (1,1)]
+ where
+  admit (n,d) = requireRight "rational parameter" (exactRational n d)
+    >>= requireRight "unit parameter" . unitInterval
+
+steps :: [CurveStep]
+steps =
+  [ curveStep line (ExactVector 3 (-2))
+  , curveStep (quadratic (ExactVector 4 5)) (ExactVector (-2) 3)
+  , curveStep (cubic (ExactVector 2 8) (ExactVector (-5) 4)) (ExactVector 4 1)
+  , curveStep (rationalQuadratic (ExactVector 0 1) positiveOne positiveTwo) (ExactVector (-1) 1)
+  , curveStep (rationalQuadratic (ExactVector 5 (-3)) positiveTwo positiveOne) (ExactVector 2 7)
+  , curveStep (cubic (ExactVector 5 4) (ExactVector (-5) 4)) zero
+  , curveStep line zero
+  , curveStep (rationalQuadratic zero positiveOne positiveTwo) zero
+  ]
+
+testParameterizedLaws :: IO ()
+testParameterizedLaws = do
+  ts <- parameters
+  traverse_ (checkStep ts) steps
+ where
+  checkStep :: [UnitInterval] -> CurveStep -> IO ()
+  checkStep ts step = do
+    assertEqual "evaluation starts at zero" zero (evaluateStep unitZero step)
+    assertEqual "evaluation ends at displacement" (curveStepEnd step) (evaluateStep unitOne step)
+    assertEqual "reversal involution" step (reverseStep (reverseStep step))
+    let (left, right) = splitStepHalf step
+    assertEqual "split endpoint" (evaluateStep unitHalf step) (curveStepEnd left)
+    assertEqual "split displacement"
+      (curveStepEnd step) (addExactVectors (curveStepEnd left) (curveStepEnd right))
+    traverse_ (checkParameter step left right) ts
+  checkParameter :: CurveStep -> CurveStep -> CurveStep -> UnitInterval -> IO ()
+  checkParameter step left right t = do
+    let value = unitIntervalValue t
+    reversed <- requireRight "reversed parameter" (unitInterval (1 - value))
+    firstHalf <- requireRight "first half parameter" (unitInterval (exactHalf * value))
+    secondHalf <- requireRight "second half parameter" (unitInterval (exactHalf * (1 + value)))
+    assertEqual "reversal parameterization"
+      (evaluateStep reversed step)
+      (addExactVectors (curveStepEnd step) (evaluateStep t (reverseStep step)))
+    assertEqual "left split reconstruction" (evaluateStep firstHalf step) (evaluateStep t left)
+    assertEqual "right split reconstruction" (evaluateStep secondHalf step)
+      (addExactVectors (curveStepEnd left) (evaluateStep t right))
+
+testTrails :: IO ()
+testTrails = do
+  let a = openTrail (Seq.fromList steps)
+      b = openTrail (Seq.singleton (curveStep line (ExactVector 4 2)))
+      c = openTrail (Seq.singleton (curveStep (quadratic (ExactVector 2 3)) zero))
+      closed = closeWith (cubic (ExactVector 5 3) (ExactVector 2 1)) a
+      stationaryClosed = closeWith (quadratic (ExactVector 3 2)) mempty
+      anchored = locate (exactPoint 7 11) a
+      p = path (Seq.singleton (OpenSubpath anchored))
+      q = path (Seq.singleton (ClosedSubpath (locate (exactPoint 2 3) closed)))
+  assertEqual "left trail identity" a (mempty <> a)
+  assertEqual "right trail identity" a (a <> mempty)
+  assertEqual "trail associativity" ((a <> b) <> c) (a <> (b <> c))
+  assertEqual "displacement homomorphism"
+    (addExactVectors (trailDisplacement a) (trailDisplacement b)) (trailDisplacement (a <> b))
+  assertEqual "trail reversal involution" a (reverseTrail (reverseTrail a))
+  assertEqual "located reversal involution" anchored (reverseLocatedTrail (reverseLocatedTrail anchored))
+  assertEqual "closed displacement" zero (trailDisplacement (openTrail (closedTrailSteps closed)))
+  assertEqual "closed reversal involution" closed (reverseClosedTrail (reverseClosedTrail closed))
+  assertEqual "single closing shape reversal involution" stationaryClosed
+    (reverseClosedTrail (reverseClosedTrail stationaryClosed))
+  assertEqual "path identity" p (mempty <> p <> mempty)
+  assertEqual "path associativity" ((p <> q) <> p) (p <> (q <> p))
+  assertEqual "path retains separate placements" 2 (Seq.length (pathSubpaths (p <> q)))
+
+testCircles :: IO ()
+testCircles = do
+  ts <- parameters
+  let located = circle positiveTwo
+      segments = toList (closedTrailSteps (locatedValue located))
+      anchors = scanl (\anchor step -> translateExactPoint anchor (curveStepEnd step))
+        (location located) segments
+  assertEqual "circle closure" zero
+    (trailDisplacement (openTrail (closedTrailSteps (locatedValue located))))
+  traverse_ (\(anchor, step) -> traverse_ (checkCircle anchor step) ts) (zip anchors segments)
+  assertEqual "collapsed ellipse remains lawful authored geometry" zero
+    (trailDisplacement (openTrail (closedTrailSteps (locatedValue (ellipse zero zero)))))
+ where
+  checkCircle anchor step t =
+    let (x,y) = exactPointCoordinates (translateExactPoint anchor (evaluateStep t step))
+     in assertEqual "exact circle equation" 4 (x*x + y*y)
+
+testJets :: IO ()
+testJets = do
+  let start = ExactVector 3 9
+      finish = ExactVector (-6) 12
+      hermite = hermiteStep (ExactVector 5 7) start finish
+      segment = curveStep line (ExactVector 1 0)
+  assertEqual "Hermite initial jet" start (startJet hermite)
+  assertEqual "Hermite final jet" finish (endJet hermite)
+  assertEqual "C1 join" ParametricJoin (joinContinuity segment segment)
+  assertEqual "G1 not C1" GeometricJoin
+    (joinContinuity segment (curveStep line (ExactVector 2 0)))
+  assertEqual "opposite jets are corner" CornerJoin
+    (joinContinuity segment (curveStep line (ExactVector (-1) 0)))
+  assertEqual "zero jet is not smoothness evidence" StationaryJoin
+    (joinContinuity segment (curveStep line zero))
+
+testAffineActions :: IO ()
+testAffineActions = do
+  ts <- parameters
+  let a = affine2 (ExactVector (-2) 1) (ExactVector 3 4) (ExactVector 8 5)
+      b = affine2 (ExactVector 1 2) (ExactVector 2 4) (ExactVector (-3) 2)
+      translation = affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector 8 5)
+      anchor = exactPoint 2 7
+      open = openTrail (Seq.fromList steps)
+      closed = locatedValue (circle positiveOne)
+      picturePath = path (Seq.fromList
+        [OpenSubpath (locate anchor open), ClosedSubpath (locate anchor closed)])
+  assertEqual "path affine identity" picturePath (transformPath identityAffine2 picturePath)
+  assertEqual "path affine composition" (transformPath (composeAffine2 a b) picturePath)
+    (transformPath a (transformPath b picturePath))
+  assertEqual "relative trail ignores translation" open (transformTrail translation open)
+  assertEqual "relative closed trail ignores translation" closed (transformClosedTrail translation closed)
+  assertEqual "located action translates anchor only"
+    (locate (transformPoint translation anchor) open)
+    (transformLocatedTrail translation (locate anchor open))
+  assertEqual "located closed action translates anchor only"
+    (locate (transformPoint translation anchor) closed)
+    (transformLocatedClosedTrail translation (locate anchor closed))
+  assertEqual "affine trail action preserves concatenation"
+    (transformTrail a open <> transformTrail a open)
+    (transformTrail a (open <> open))
+  assertEqual "closed action retains closure" zero
+    (trailDisplacement (openTrail (closedTrailSteps (transformClosedTrail b closed))))
+  traverse_ (\step -> do
+    let transformed = transformStep a step
+        (left,right) = splitStepHalf step
+    assertEqual "relative step ignores translation" step (transformStep translation step)
+    assertEqual "subdivision commutes with affine action"
+      (transformStep a left, transformStep a right)
+      (splitStepHalf transformed)
+    traverse_ (\t -> do
+      assertEqual "evaluation commutes with affine action"
+        (transformVector a (evaluateStep t step)) (evaluateStep t transformed)
+      assertEqual "point and vector actions agree at every placed sample"
+        (transformPoint a (translateExactPoint anchor (evaluateStep t step)))
+        (translateExactPoint (transformPoint a anchor) (evaluateStep t transformed))) ts) steps
+
+zero :: ExactVector
+zero = ExactVector 0 0
diff --git a/test/curve/Moonlight/Planar/RegionAdmissionSpec.hs b/test/curve/Moonlight/Planar/RegionAdmissionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/curve/Moonlight/Planar/RegionAdmissionSpec.hs
@@ -0,0 +1,70 @@
+-- | Pin the polygon admission semantics consumed by curve-region lowering.
+module Moonlight.Planar.RegionAdmissionSpec (tests) where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import Moonlight.Planar.Region
+  ( RegionPointLocation (..)
+  , RegionValidationError (..)
+  , exactLoop
+  , planarLayer
+  , planarRegion
+  , planarRegionComponents
+  , polygonComponent
+  , regionPointLocation
+  )
+import Support (assertEqual, integerPoint, rectangleComponent, rectangleLoop, requireRight)
+
+tests :: IO ()
+tests = sequence_
+  [ testBatchRefusalOrder
+  , testBoundaryContactAndHoles
+  , putStrLn "region admission: ok"
+  ]
+
+testBatchRefusalOrder :: IO ()
+testBatchRefusalOrder = do
+  first <- rectangleComponent 20 0 24 4
+  second <- rectangleComponent 21 1 25 5
+  third <- rectangleComponent 0 0 4 4
+  fourth <- rectangleComponent 1 1 5 5
+  assertEqual "component refusal uses input order rather than x sweep order"
+    (Left (RegionComponentInteriorOverlap 0 1))
+    (planarRegion [first, second, third, fourth])
+  regions <- traverse (requireRight "singleton region" . planarRegion . pure)
+    [first, second, third, fourth]
+  assertEqual "layer refusal uses label order rather than x sweep order"
+    (Left (RegionLayerInteriorOverlap 0 1))
+    (planarLayer (-1 :: Int) (Map.fromList (zip [0 ..] regions)))
+  outer <- rectangleLoop 0 0 8 8
+  outsideHole <- requireRight "clockwise outside hole" (exactLoop
+    (integerPoint 20 0 :| [integerPoint 20 2, integerPoint 22 2, integerPoint 22 0]))
+  wrongWindingHole <- rectangleLoop 2 2 3 3
+  assertEqual "all hole windings precede containment admission"
+    (Left (RegionHoleLoopWinding 1 GT))
+    (polygonComponent outer [outsideHole, wrongWindingHole])
+  assertEqual "disjoint bounds still report outside hole location"
+    (Left (RegionHoleLocation 0 RegionExterior))
+    (polygonComponent outer [outsideHole])
+
+testBoundaryContactAndHoles :: IO ()
+testBoundaryContactAndHoles = do
+  left <- rectangleComponent 0 0 2 2
+  right <- rectangleComponent 2 0 4 2
+  touching <- requireRight "oppositely directed shared boundary" (planarRegion [left, right])
+  assertEqual "boundary-only contact retains both components"
+    2 (length (planarRegionComponents touching))
+  assertEqual "shared edge remains boundary" RegionOnBoundary
+    (regionPointLocation touching (integerPoint 2 1))
+  assertEqual "identically directed duplicate cycles overlap"
+    (Left (RegionComponentInteriorOverlap 0 1)) (planarRegion [left, left])
+  outer <- rectangleLoop 0 0 10 10
+  hole <- requireRight "clockwise central hole" (exactLoop
+    (integerPoint 2 2 :| [integerPoint 2 8, integerPoint 8 8, integerPoint 8 2]))
+  annulus <- requireRight "annulus" (polygonComponent outer [hole])
+  island <- rectangleComponent 4 4 6 6
+  nested <- requireRight "island within another component's hole" (planarRegion [annulus, island])
+  assertEqual "annulus interior" RegionInterior (regionPointLocation nested (integerPoint 1 1))
+  assertEqual "unfilled hole" RegionExterior (regionPointLocation nested (integerPoint 3 3))
+  assertEqual "separate island interior" RegionInterior (regionPointLocation nested (integerPoint 5 5))
+  assertEqual "hole boundary" RegionOnBoundary (regionPointLocation nested (integerPoint 2 5))
diff --git a/test/exact-planar/Main.hs b/test/exact-planar/Main.hs
--- a/test/exact-planar/Main.hs
+++ b/test/exact-planar/Main.hs
@@ -1,15 +1,30 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.ExactClipRetentionSpec as ExactClipRetentionSpec
-import qualified Moonlight.Triangulation.LayerOperationsSpec as LayerOperationsSpec
-import qualified Moonlight.Triangulation.MinkowskiSpec as MinkowskiSpec
-import qualified Moonlight.Triangulation.PowerDiagramSpec as PowerDiagramSpec
+import qualified Moonlight.Planar.ExactClipRetentionSpec as ExactClipRetentionSpec
+import qualified Moonlight.Planar.ExactIncidencePublicationSpec as ExactIncidencePublicationSpec
+import qualified Moonlight.Planar.ConvexSpec as ConvexSpec
+import qualified Moonlight.Planar.InCircleFixedWidthSpec as InCircleFixedWidthSpec
+import qualified Moonlight.Planar.LayerOperationsSpec as LayerOperationsSpec
+import qualified Moonlight.Planar.MinkowskiSpec as MinkowskiSpec
+import qualified Moonlight.Planar.RegularTopologySpec as RegularTopologySpec
+import qualified Moonlight.Planar.RegularEditSpec as RegularEditSpec
+import qualified Moonlight.Planar.RegularAlphaSpec as RegularAlphaSpec
+import qualified Moonlight.Planar.PowerDiagramSpec as PowerDiagramSpec
+import qualified Moonlight.Planar.PowerMassSpec as PowerMassSpec
 
 main :: IO ()
 main =
   sequence_
     [ ExactClipRetentionSpec.tests
+    , ExactIncidencePublicationSpec.tests
+    , ConvexSpec.tests
+    , InCircleFixedWidthSpec.tests
     , MinkowskiSpec.tests
+    , RegularTopologySpec.tests
+    , RegularEditSpec.tests
+    , RegularAlphaSpec.tests
     , PowerDiagramSpec.tests
+    , putStrLn "power diagram: ok"
+    , PowerMassSpec.tests
     , LayerOperationsSpec.tests
     ]
diff --git a/test/exact-planar/Moonlight/Planar/InCircleFixedWidthSpec.hs b/test/exact-planar/Moonlight/Planar/InCircleFixedWidthSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/exact-planar/Moonlight/Planar/InCircleFixedWidthSpec.hs
@@ -0,0 +1,315 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | The fixed-width in-circle worker against the arbitrary-precision
+-- determinant it stands in for. Agreement on every case is the law; the
+-- count of cases the worker resolved by itself is the receipt that the law
+-- was observed on the worker and not on the fallback it declines into.
+module Moonlight.Planar.InCircleFixedWidthSpec (tests) where
+
+import Control.Monad (unless)
+import Data.Bits (finiteBitSize, shiftL, shiftR, xor, (.&.), (.|.))
+import qualified Data.List as List
+import Data.Word (Word64)
+import GHC.Float (castDoubleToWord64, castWord64ToDouble)
+import Moonlight.Planar.Internal.Dyadic
+  ( exactInCircleDet
+  , exactInCircleSignFixedWidth
+  )
+
+data Quad = Quad !Double !Double !Double !Double !Double !Double !Double !Double
+  deriving stock (Show)
+
+-- | What the worker owes a family on a 64-bit word: every case, because the
+-- family's exponents always align within the width, or at least one refusal,
+-- because the family exists to keep the fallback on the record.
+data Expectation = ResolvesEvery | DeclinesSome
+
+data Family = Family
+  { familyName :: String
+  , familyQuads :: [Quad]
+  , familyExpectation :: Expectation
+  , familyCocircular :: Bool
+  }
+
+data Tally = Tally
+  { tallyCases :: !Int
+  , tallyResolved :: !Int
+  , tallyDeclined :: !Int
+  , tallyDisagreement :: Maybe Quad
+  , tallyNotCocircular :: Maybe Quad
+  }
+
+tests :: IO ()
+tests = do
+  tallies <- traverse observe families
+  let total = sum (map tallyCases tallies)
+  unless (total >= 10_000) $
+    fail ("in-circle fixed-width law observed on too few cases: " <> show total)
+
+observe :: Family -> IO Tally
+observe family = do
+  let tally = List.foldl' (record (familyCocircular family)) emptyTally (familyQuads family)
+  putStrLn
+    ( "in-circle fixed width " <> familyName family
+        <> ": cases=" <> show (tallyCases tally)
+        <> " resolved=" <> show (tallyResolved tally)
+        <> " declined=" <> show (tallyDeclined tally)
+    )
+  case tallyNotCocircular tally of
+    Just quad -> fail (familyName family <> ": fixture is not exactly cocircular: " <> show quad)
+    Nothing -> pure ()
+  case tallyDisagreement tally of
+    Just quad ->
+      fail
+        ( familyName family <> ": fixed-width sign disagrees with the determinant on "
+            <> show quad <> " (determinant " <> show (determinant quad) <> ")"
+        )
+    Nothing -> pure ()
+  case familyExpectation family of
+    ResolvesEvery
+      | wideWord && tallyDeclined tally /= 0 ->
+          fail (familyName family <> ": the worker declined " <> show (tallyDeclined tally) <> " cases it should resolve")
+      | not wideWord && tallyResolved tally /= 0 ->
+          fail (familyName family <> ": a narrow word resolved cases in fixed width")
+    DeclinesSome
+      | tallyDeclined tally == 0 ->
+          fail (familyName family <> ": the fallback was never taken")
+    _ -> pure ()
+  pure tally
+ where
+  wideWord = finiteBitSize (0 :: Word) == 64
+
+emptyTally :: Tally
+emptyTally = Tally 0 0 0 Nothing Nothing
+
+record :: Bool -> Tally -> Quad -> Tally
+record cocircular tally quad =
+  let !exact = determinant quad
+      !expected = compare exact 0
+      counted = tally{tallyCases = tallyCases tally + 1}
+      checked
+        | cocircular && exact /= 0 =
+            counted{tallyNotCocircular = maybe (Just quad) Just (tallyNotCocircular counted)}
+        | otherwise = counted
+   in case fixedWidth quad of
+        Nothing -> checked{tallyDeclined = tallyDeclined checked + 1}
+        Just verdict
+          | verdict == expected -> checked{tallyResolved = tallyResolved checked + 1}
+          | otherwise ->
+              checked
+                { tallyResolved = tallyResolved checked + 1
+                , tallyDisagreement = maybe (Just quad) Just (tallyDisagreement checked)
+                }
+
+determinant :: Quad -> Integer
+determinant (Quad ax ay bx by cx cy dx dy) = exactInCircleDet ax ay bx by cx cy dx dy
+
+fixedWidth :: Quad -> Maybe Ordering
+fixedWidth (Quad ax ay bx by cx cy dx dy) = exactInCircleSignFixedWidth ax ay bx by cx cy dx dy
+
+families :: [Family]
+families =
+  [ Family "uniform" (take 3_000 (uniformQuads (stream 0x1ed1_b2ad_5c0f_9e31))) ResolvesEvery False
+  , Family "wide exponents" (take 2_000 (wideQuads (stream 0x7a5f_3c21_9b8e_d604))) DeclinesSome False
+  , Family "subnormal floor" (take 1_000 (subnormalQuads (stream 0x3f8c_2d17_a9e4_b055))) ResolvesEvery False
+  , Family "grid cocircular" (take 1_500 (gridCocircularQuads (stream 0x9c4e_71b3_5d2a_f086))) ResolvesEvery True
+  , Family "grid one unit off" (take 1_000 (gridOffQuads (stream 0x2b7d_e8a1_4f63_c917))) ResolvesEvery False
+  , Family "integer circle cocircular" (take 1_500 (circleCocircularQuads (stream 0x6e2a_9d5c_1b74_38f0))) ResolvesEvery True
+  , Family "integer circle one unit off" (take 1_000 (circleOffQuads (stream 0xd41b_7c8e_2a95_6f13))) ResolvesEvery False
+  , Family "hex lattice hexagon" (take 1_500 (hexHexagonQuads (stream 0x58f3_a6d2_c9e1_74b8))) ResolvesEvery False
+  , Family "hex lattice random" (take 500 (hexRandomQuads (stream 0xa7c4_2e91_6d3b_f508))) ResolvesEvery False
+  , Family "one ulp off cocircular" (take 1_500 (ulpOffQuads (stream 0x14e9_b7d6_3a2c_85f1))) ResolvesEvery False
+  , Family "degenerate" (take 500 (degenerateQuads (stream 0xc3a8_5f1d_7e26_49b0))) ResolvesEvery False
+  ]
+
+-- SplitMix64: one deterministic word stream per family.
+stream :: Word64 -> [Word64]
+stream seed =
+  let !state = seed + 0x9E37_79B9_7F4A_7C15
+      !z1 = (state `xor` (state `shiftR` 30)) * 0xBF58_476D_1CE4_E5B9
+      !z2 = (z1 `xor` (z1 `shiftR` 27)) * 0x94D0_49BB_1331_11EB
+   in (z2 `xor` (z2 `shiftR` 31)) : stream state
+
+chunks :: Int -> [a] -> [[a]]
+chunks width values =
+  case splitAt width values of
+    (chunk, rest) | length chunk == width -> chunk : chunks width rest
+    _ -> []
+
+quadOf :: [Double] -> Quad
+quadOf [ax, ay, bx, by, cx, cy, dx, dy] = Quad ax ay bx by cx cy dx dy
+quadOf other = error ("quadOf: " <> show (length other) <> " coordinates")
+
+unit :: Word64 -> Double
+unit word = fromIntegral (word `shiftR` 11) / 9_007_199_254_740_992
+
+signed :: Word64 -> Double
+signed word = 2 * unit word - 1
+
+modulo :: Word64 -> Int -> Int
+modulo word bound = fromIntegral (word `mod` fromIntegral bound)
+
+integerIn :: Int -> Word64 -> Double
+integerIn bound word = fromIntegral (modulo word (2 * bound + 1) - bound)
+
+uniformQuads :: [Word64] -> [Quad]
+uniformQuads = map (quadOf . map signed) . chunks 8
+
+-- Any finite bit pattern: sign, exponent field in [0, 2046], random mantissa.
+wideQuads :: [Word64] -> [Quad]
+wideQuads = map (quadOf . map (fromBits 2047)) . chunks 8
+
+-- Exponent fields in [0, 74]: subnormals and the normals within 73 bits of
+-- their floor, so every alignment shift fits the width.
+subnormalQuads :: [Word64] -> [Quad]
+subnormalQuads = map (quadOf . map (fromBits 75)) . chunks 8
+
+fromBits :: Int -> Word64 -> Double
+fromBits exponentBound word =
+  let signBit = (word `shiftR` 63) `shiftL` 63
+      exponentField = fromIntegral (modulo (word `shiftR` 52) exponentBound) `shiftL` 52
+      mantissa = word .&. 0x000F_FFFF_FFFF_FFFF
+   in castWord64ToDouble (signBit .|. exponentField .|. mantissa)
+
+-- The four corners of a rectangle centred on an integer point: all on one
+-- circle, in a random order.
+gridCocircularQuads :: [Word64] -> [Quad]
+gridCocircularQuads = map corners . chunks 5
+ where
+  corners [cxWord, cyWord, aWord, bWord, orderWord] =
+    let cx = integerIn 1_048_576 cxWord
+        cy = integerIn 1_048_576 cyWord
+        a = fromIntegral (1 + modulo aWord 1_048_576)
+        b = fromIntegral (1 + modulo bWord 1_048_576)
+     in permuted orderWord [(cx + a, cy + b), (cx - a, cy + b), (cx - a, cy - b), (cx + a, cy - b)]
+  corners _ = error "gridCocircularQuads"
+
+gridOffQuads :: [Word64] -> [Quad]
+gridOffQuads source =
+  zipWith nudge (gridCocircularQuads source) (stream 0x71c5_2d9e_b384_a6f0)
+
+permuted :: Word64 -> [(Double, Double)] -> Quad
+permuted word points =
+  quadOf (concatMap (\(x, y) -> [x, y]) (List.permutations points !! modulo word 24))
+
+-- Move one coordinate of the fourth point by one unit; on integer fixtures
+-- that is an exact step off the circle.
+nudge :: Quad -> Word64 -> Quad
+nudge (Quad ax ay bx by cx cy dx dy) word =
+  case modulo word 4 of
+    0 -> Quad ax ay bx by cx cy (dx + 1) dy
+    1 -> Quad ax ay bx by cx cy (dx - 1) dy
+    2 -> Quad ax ay bx by cx cy dx (dy + 1)
+    _ -> Quad ax ay bx by cx cy dx (dy - 1)
+
+-- Every lattice point on the circle of radius 5525 = 5^2 * 13 * 17, a radius
+-- chosen for its many representations as a sum of two squares.
+circlePoints :: [(Integer, Integer)]
+circlePoints =
+  List.nub
+    [ (sx * x, sy * y)
+    | x <- [0 .. radius]
+    , let remaining = radius * radius - x * x
+          y = integerSquareRoot remaining
+    , y * y == remaining
+    , sx <- [1, -1]
+    , sy <- [1, -1]
+    ]
+ where
+  radius = 5525 :: Integer
+
+integerSquareRoot :: Integer -> Integer
+integerSquareRoot value =
+  let guess = floor (sqrt (fromIntegral value :: Double))
+   in List.foldl' (\best candidate -> if candidate * candidate <= value then max best candidate else best) 0 [guess - 1, guess, guess + 1]
+
+circleCocircularQuads :: [Word64] -> [Quad]
+circleCocircularQuads = map pick . filter distinct . chunks 7
+ where
+  count = length circlePoints
+  distinct [a, b, c, d, _, _, _] =
+    let indices = map (`modulo` count) [a, b, c, d]
+     in length (List.nub indices) == 4
+  distinct _ = False
+  pick [a, b, c, d, txWord, tyWord, orderWord] =
+    let tx = integerIn 1_073_741_824 txWord
+        ty = integerIn 1_073_741_824 tyWord
+        point index =
+          let (x, y) = circlePoints !! modulo index count
+           in (fromIntegral x + tx, fromIntegral y + ty)
+     in permuted orderWord (map point [a, b, c, d])
+  pick _ = error "circleCocircularQuads"
+
+circleOffQuads :: [Word64] -> [Quad]
+circleOffQuads source =
+  zipWith nudge (circleCocircularQuads source) (stream 0x5d2e_8a47_c1f9_63b0)
+
+-- The hexagonal lattice as binary64: x exact, y a rounded multiple of
+-- sqrt 3 / 2. No four of its points are exactly cocircular in binary64, so
+-- these are the near-cocircular quadrilaterals a Delaunay flip meets.
+hexPoint :: Int -> Int -> (Double, Double)
+hexPoint i j = (fromIntegral i + fromIntegral j / 2, fromIntegral j * (sqrt 3 / 2))
+
+-- Four of the six lattice points around a lattice point: the corners of a
+-- lattice hexagon, cocircular up to the rounding of the ordinate.
+hexHexagonQuads :: [Word64] -> [Quad]
+hexHexagonQuads = map hexagon . chunks 4
+ where
+  hexagon [iWord, jWord, skipWord, orderWord] =
+    let i = modulo iWord 101 - 50
+        j = modulo jWord 101 - 50
+        ring = [(i + 1, j), (i, j + 1), (i - 1, j + 1), (i - 1, j), (i, j - 1), (i + 1, j - 1)]
+        skipped = modulo skipWord 6
+        kept = [corner | (position, corner) <- zip [0 :: Int ..] ring, position /= skipped, position /= (skipped + 3) `mod` 6]
+     in permuted orderWord (map (uncurry hexPoint) kept)
+  hexagon _ = error "hexHexagonQuads"
+
+hexRandomQuads :: [Word64] -> [Quad]
+hexRandomQuads = map (quadOf . concatMap coordinate . pairs) . chunks 8
+ where
+  pairs :: [Word64] -> [(Word64, Word64)]
+  pairs (i : j : rest) = (i, j) : pairs rest
+  pairs _ = []
+  coordinate :: (Word64, Word64) -> [Double]
+  coordinate (iWord, jWord) =
+    let (x, y) = hexPoint (modulo iWord 101 - 50) (modulo jWord 101 - 50)
+     in [x, y]
+
+-- A cocircular grid quadrilateral with one nonzero coordinate moved by a
+-- single ulp: the determinant is tiny against the magnitudes it is built
+-- from, which is where a sign is hardest to resolve.
+ulpOffQuads :: [Word64] -> [Quad]
+ulpOffQuads source =
+  zipWith ulpNudge (gridCocircularQuads source) (stream 0x8b3f_6e1a_d47c_2905)
+
+ulpNudge :: Quad -> Word64 -> Quad
+ulpNudge quad@(Quad ax ay bx by cx cy dx dy) word =
+  let coordinates = [ax, ay, bx, by, cx, cy, dx, dy]
+      start = modulo word 8
+      candidates = [index | offset <- [0 .. 7], let index = (start + offset) `mod` 8, coordinates !! index /= 0]
+      step value
+        | even (word `shiftR` 3) = castWord64ToDouble (castDoubleToWord64 value + 1)
+        | otherwise = castWord64ToDouble (castDoubleToWord64 value - 1)
+   in case candidates of
+        chosen : _ ->
+          quadOf [if index == chosen then step value else value | (index, value) <- zip [0 :: Int ..] coordinates]
+        [] -> quad
+
+-- Repeated points, collinear triples, zeros of both signs, and mixtures of
+-- those with ordinary coordinates.
+degenerateQuads :: [Word64] -> [Quad]
+degenerateQuads = map shape . chunks 9
+ where
+  shape (selector : rest) =
+    case quadOf (map signed rest) of
+      Quad ax ay bx by cx cy dx dy ->
+        case modulo selector 6 of
+          0 -> Quad ax ay ax ay cx cy dx dy
+          1 -> Quad ax ay bx by cx cy ax ay
+          2 -> Quad ax ay (ax + 1) (ay + 1) (ax + 2) (ay + 2) dx dy
+          3 -> Quad 0 0 0 negativeZero negativeZero 0 dx dy
+          4 -> Quad ax 0 bx 0 cx 0 dx 0
+          _ -> Quad ax ay bx by cx cy negativeZero negativeZero
+  shape _ = error "degenerateQuads"
+  negativeZero = -0.0 :: Double
diff --git a/test/examples/Main.hs b/test/examples/Main.hs
--- a/test/examples/Main.hs
+++ b/test/examples/Main.hs
@@ -1,12 +1,12 @@
 module Main (main) where
 
-import Moonlight.Triangulation.Example.AlphaBoundary
-import Moonlight.Triangulation.Example.ConstrainedRegion
-import Moonlight.Triangulation.Example.PlanarRegion
-import Moonlight.Triangulation.Example.SetAlgebra
-import Moonlight.Triangulation.Handles.Dynamic ()
-import Moonlight.Triangulation.Session ()
-import Moonlight.Triangulation.Voronoi ()
+import Moonlight.Planar.Example.AlphaBoundary
+import Moonlight.Planar.Example.ConstrainedRegion
+import Moonlight.Planar.Example.PlanarRegion
+import Moonlight.Planar.Example.SetAlgebra
+import Moonlight.Planar.Handles.Dynamic ()
+import Moonlight.Planar.Session ()
+import Moonlight.Planar.Voronoi ()
 import System.Exit (die)
 
 main :: IO ()
diff --git a/test/ffi/Main.hs b/test/ffi/Main.hs
--- a/test/ffi/Main.hs
+++ b/test/ffi/Main.hs
@@ -2,20 +2,21 @@
 
 import Control.Exception (bracket)
 import Control.Monad (unless)
+import Data.Foldable (traverse_)
 import Data.Int (Int64)
 import Data.List (nub)
 import Data.Maybe (mapMaybe)
-import Data.Word (Word32)
+import Data.Word (Word32, Word64)
 import Foreign.C.String (peekCString)
 import Foreign.C.Types (CDouble (..), CSize (..), CUInt)
 import Foreign.Marshal.Alloc (alloca)
 import Foreign.Marshal.Array (allocaArray, peekArray, withArray)
 import Foreign.Ptr (Ptr, nullPtr)
 import Foreign.Storable (Storable (sizeOf), peek)
-import Moonlight.Triangulation.Foreign.Contract
-import Moonlight.Triangulation.Foreign.Mesh
-import Moonlight.Triangulation.Foreign.Morphology
-import Moonlight.Triangulation.Foreign.Region
+import Moonlight.Planar.Foreign.Contract
+import Moonlight.Planar.Foreign.Mesh
+import Moonlight.Planar.Foreign.Morphology
+import Moonlight.Planar.Foreign.Region
 
 type MeshPointer = Ptr CMesh
 type RegionPointer = Ptr CRegion
@@ -43,6 +44,7 @@
       requireMeshCount "left triangle count" meshTriangleCount left 2
       testDenseCopies left
       testImmutableBatch left
+      testBatchExtents left
       testBatchAdmission left
       testSiteSetAlgebra left right
   testExactRegionSurface
@@ -50,6 +52,7 @@
   testCoordinateCountOverflow
   testNullPointerRefusal
   testRegionLayoutRefusal
+  testConvexAndMorphologyRefusals
   putStrLn "ffi: ok"
 
 testStableWireContract :: IO ()
@@ -74,6 +77,7 @@
         && obstructionCodeId ObstructionCountOverflow == 101
         && obstructionCodeId ObstructionBufferTooSmall == 102
         && obstructionCodeId ObstructionRegionLayoutInvalid == 200
+        && obstructionCodeId ObstructionConvexFailed == 207
         && fmap regionLocationCodeId allRegionLocationCodes == [0, 1, 2]
         && fmap minkowskiOperationCodeId allMinkowskiOperationCodes == [0, 1, 2, 3]
     )
@@ -139,24 +143,69 @@
         requireMeshCount "original after batch" meshVertexCount original 4
         requireMeshCount "revised after batch" meshVertexCount revised 6)
 
+testBatchExtents :: MeshPointer -> IO ()
+testBatchExtents original = do
+  originalCoordinates <- copyMeshCoordinates original 4
+  traverse_
+    (\(label, points, expectedCount) ->
+       withPointArray points $ \coordinates ->
+         bracket
+           (produceHandle label (meshInsertManyF64 original coordinates (fromIntegral (length points))))
+           meshFree
+           (\revised -> do
+              requireMeshCount label meshVertexCount revised expectedCount
+              revisedCoordinates <- copyMeshCoordinates revised expectedCount
+              unless (take 8 revisedCoordinates == originalCoordinates) $
+                fail (label <> " changed a resident vertex handle")
+              retainedCoordinates <- copyMeshCoordinates original 4
+              unless (retainedCoordinates == originalCoordinates) $
+                fail (label <> " mutated the original mesh"))
+    )
+    [ ("empty batch", [], 4)
+    , ("singleton batch", [(1, 1)], 5)
+    , ("resident-only batch", replicate 32 (0, 0) <> replicate 32 (2, 2), 4)
+    , ("repeated-new batch", replicate 64 (1, 1), 5)
+    , ("mixed-duplicate batch", [(-0.0, 0), (1, 1), (2, 2), (1, 1), (0, 0)], 5)
+    ]
+
+copyMeshCoordinates :: MeshPointer -> Int -> IO [CDouble]
+copyMeshCoordinates mesh count =
+  allocaArray (2 * count) $ \coordinates ->
+    alloca $ \written ->
+      alloca $ \obstruction -> do
+        status <- meshCopyVerticesF64 mesh coordinates (fromIntegral count) written obstruction
+        requireStatus "batch coordinate witness" AbiStatusOk status obstruction
+        observed <- peek written
+        unless (observed == fromIntegral count) $
+          fail "batch coordinate witness has the wrong extent"
+        peekArray (2 * count) coordinates
+
 testBatchAdmission :: MeshPointer -> IO ()
 testBatchAdmission original = do
-  testLateInvalidPoint
+  traverse_
+    testInvalidPoints
+    [ ("late invalid batch point", [(1, 1), (3, 1), (0 / 0, 1)], 2)
+    , ("first invalid batch point", [(0 / 0, 1), (1, 1), (1, 1 / 0)], 0)
+    ]
   testCanonicalZeros
  where
-  testLateInvalidPoint =
-    withPointArray [(1, 1), (3, 1), (0 / 0, 1)] $ \coordinates ->
+  testInvalidPoints :: (String, [Point], Word64) -> IO ()
+  testInvalidPoints (label, points, expectedIndex) =
+    withPointArray points $ \coordinates ->
       alloca $ \output ->
         alloca $ \obstruction -> do
-          status <- meshInsertManyF64 original coordinates 3 output obstruction
-          requireStatus "late invalid batch point" AbiStatusGeometryObstruction status obstruction
+          status <- meshInsertManyF64 original coordinates (fromIntegral (length points)) output obstruction
+          requireStatus label AbiStatusGeometryObstruction status obstruction
           refusal <- peek obstruction
           unless
             ( obstructionCode refusal == obstructionCodeId ObstructionInvalidCoordinate
                 && obstructionCoordinateError refusal == coordinateErrorCodeId CoordinateErrorNaN
-                && obstructionInputIndex refusal == 2
+                && obstructionInputIndex refusal == expectedIndex
             )
-            (fail "late invalid batch point lost its raw input-index obstruction")
+            (fail (label <> " lost its raw input-index obstruction"))
+          unpublished <- peek output
+          unless (unpublished == nullPtr) $
+            fail (label <> " published a partial batch")
           requireMeshCount "original after refused batch" meshVertexCount original 4
   testCanonicalZeros =
     let negativeZero = -0.0 :: Double
@@ -372,7 +421,30 @@
               && obstructionCoordinateError refusal == coordinateErrorCodeId CoordinateErrorNaN
               && obstructionInputIndex refusal == 1
           ) $
-          fail "invalid coordinate lost its typed witness"
+            fail "invalid coordinate lost its typed witness"
+
+testConvexAndMorphologyRefusals :: IO ()
+testConvexAndMorphologyRefusals =
+  sequence_
+    [ requireRefusal "self-intersecting convex input" ObstructionConvexFailed
+        [(0, 3), (-2, -3), (3, 1), (-3, 1), (2, -3)]
+    , requireRefusal "convex input not anchored at origin" ObstructionMinkowskiFailed
+        [(1, 1), (2, 1), (2, 2), (1, 2)]
+    ]
+ where
+  requireRefusal :: String -> ObstructionCode -> [Point] -> IO ()
+  requireRefusal label expected points =
+    withPointArray points $ \coordinates ->
+      alloca $ \output ->
+        alloca $ \obstruction -> do
+          status <- structuringElementCreateF64 coordinates (fromIntegral (length points)) output obstruction
+          requireStatus label AbiStatusGeometryObstruction status obstruction
+          refusal <- peek obstruction
+          unless (obstructionCode refusal == obstructionCodeId expected) $
+            fail (label <> " crossed the convex/morphology error boundary")
+          unpublished <- peek output
+          unless (unpublished == nullPtr) $
+            fail (label <> " published an invalid structuring element")
 
 testNullPointerRefusal :: IO ()
 testNullPointerRefusal =
diff --git a/test/hex/Main.hs b/test/hex/Main.hs
--- a/test/hex/Main.hs
+++ b/test/hex/Main.hs
@@ -2,18 +2,36 @@
 
 import Data.ByteString.Lazy qualified as BL
 import Data.Foldable (traverse_)
+import Data.List qualified as List
 import Data.List.NonEmpty qualified as NonEmpty
+import Data.Maybe (mapMaybe)
+import Data.Ratio ((%), denominator, numerator)
 import Data.Set (Set)
 import Data.Set qualified as Set
 import Data.Vector qualified as Vector
+import Data.Vector.Unboxed qualified as U
+import Data.Word (Word64)
 import Moonlight.Hex.Coordinate
 import Moonlight.Hex.Element
 import Moonlight.Hex.Planar
 import Moonlight.Hex.Region
+import Moonlight.Hex.Topology
 import Moonlight.Hex.Serialization
-import Moonlight.Triangulation.Region
-  ( exactLoopPoints
+import Moonlight.Planar.Exact
+  ( ExactPoint
+  , exactPoint
+  , exactPointCoordinates
+  , exactRational
+  , exactRationalDenominator
+  , exactRationalNumerator
+  )
+import Moonlight.Planar.Region
+  ( PlanarRegion
+  , exactLoop
+  , exactLoopPoints
+  , planarRegion
   , planarRegionComponents
+  , polygonComponent
   , polygonHoleLoops
   , polygonOuterLoop
   )
@@ -21,15 +39,43 @@
 main = do
   coordinateAndElementLaws
   packedRegionLaws
+  packedRowSpanLaws
+  nativeTopologyLaws
   restrictionLaws
   gluingLaws
   planarLaws
+  planarRestrictionOracleLaws
   serializationLaws
   wordBoundaryLaws
-  putStrLn "hex-laws: coordinate element boolean restriction gluing planar serialization word-boundary=passed"
+  putStrLn "hex-laws: coordinate element boolean topology restriction gluing planar serialization word-boundary=passed"
 
 type Assertion = IO ()
 
+packedRowSpanLaws :: Assertion
+packedRowSpanLaws = do
+  assertEqual "reversed row span is absent" Nothing (hexRowSpan 1 0)
+  traverse_ checkLayout
+    [ (origin, width, height)
+    | origin <- [HexCoord (-73) (-5), HexCoord (2 ^ (55 :: Int)) (-11)]
+    , width <- [1, 2, 63, 64, 65, 127, 128, 129]
+    , height <- [1, 2, 3]
+    ]
+ where
+  checkLayout :: (HexCoord, Int, Int) -> Assertion
+  checkLayout (origin@(HexCoord originQ _), width, height) = do
+    layout <- requireRight (hexLayout origin width height)
+    let ranges :: Int -> [(Integer, Integer)]
+        ranges row =
+          if even row
+            then [(negate (2 ^ (100 :: Int)), toInteger originQ + 1), (toInteger originQ, toInteger originQ + 4), (toInteger originQ + 8, toInteger originQ + 8)]
+            else [(toInteger originQ + 3, 2 ^ (100 :: Int)), (toInteger originQ + 5, toInteger originQ + 7)]
+        expected = hexRegionGenerate layout
+          (\(HexCoord q r) -> any (\(firstQ, finalQ) -> firstQ <= toInteger q && toInteger q <= finalQ) (ranges r))
+        actual = hexRegionGenerateRowSpans layout (mapMaybe (uncurry hexRowSpan) . ranges)
+    assertEqual ("row-span packed identity " <> show (origin, width, height)) expected actual
+    assertEqual "row-span canonical padding round trip" (Right actual)
+      (hexRegionFromPackedWords layout (packedWords actual))
+
 coordinateAndElementLaws :: Assertion
 coordinateAndElementLaws = do
   layout <- requireRight (hexLayout (HexCoord (-2) (-2)) 5 5)
@@ -54,6 +100,14 @@
     )
     coordinates
   assertEqual "east overflow is typed absence" Nothing (hexStepCoord (HexCoord maxBound 0) HexEast)
+  assertEqual
+    "coordinate-range overflow is typed"
+    (Left (HexLayoutCoordinateRangeOverflow (HexCoord maxBound 0) 2 1))
+    (hexLayout (HexCoord maxBound 0) 2 1)
+  assertEqual
+    "cell-count overflow is typed"
+    (Left (HexLayoutCellCountOverflow (toInteger (maxBound :: Int) * 2)))
+    (hexLayout (HexCoord minBound 0) maxBound 2)
 
 packedRegionLaws :: Assertion
 packedRegionLaws = do
@@ -84,6 +138,34 @@
           regions
     )
     regions
+  packedLayout <- requireRight (hexLayout (HexCoord 0 0) 65 1)
+  let packedSource = hexRegionGenerate packedLayout (\(HexCoord q _) -> even q)
+      sourceWords = packedWords packedSource
+      effectfulSource =
+        hexRegionGenerateM packedLayout (\(HexCoord q _) -> Right (even q))
+          :: Either HexCoord HexRegion
+      refusedSource =
+        hexRegionGenerateM
+          packedLayout
+          (\coordinate ->
+             if coordinate == HexCoord 2 0
+               then Left coordinate
+               else Right False)
+          :: Either HexCoord HexRegion
+  assertEqual "effectful generation agrees with pure generation" (Right packedSource) effectfulSource
+  assertEqual "effectful generation preserves predicate refusal" (Left (HexCoord 2 0)) refusedSource
+  assertEqual
+    "canonical packed words admit without changing the region"
+    (Right packedSource)
+    (hexRegionFromPackedWords packedLayout sourceWords)
+  assertEqual
+    "packed word-count mismatch is typed"
+    (Left (HexPackedRegionWordCountMismatch 2 1))
+    (hexRegionFromPackedWords packedLayout (U.singleton 0))
+  assertEqual
+    "nonzero packed padding is rejected"
+    (Left (HexPackedRegionNonCanonicalPadding (maxBound - 1)))
+    (hexRegionFromPackedWords packedLayout (U.fromList [0, maxBound]))
   traverse_
     (\(_, left) ->
         traverse_
@@ -100,6 +182,124 @@
     )
     regions
 
+nativeTopologyLaws :: Assertion
+nativeTopologyLaws = do
+  layout <- requireRight (hexLayout (HexCoord (-1) (-1)) 4 4)
+  let selectedSet = Set.fromList [HexCoord 0 0, HexCoord 1 0, HexCoord 0 1]
+  selected <- requireRight (hexRegionFromCoords layout selectedSet)
+  let dilatedSet = referenceDilation layout selectedSet
+      erodedSet = referenceErosion layout selectedSet
+  assertRegion "native dilation" dilatedSet (hexRegionDilate selected)
+  assertRegion "native erosion" erodedSet (hexRegionErode selected)
+  assertRegion "native inner frontier" (selectedSet Set.\\ erodedSet) (hexRegionInnerFrontier selected)
+  assertRegion "native outer frontier" (dilatedSet Set.\\ selectedSet) (hexRegionOuterFrontier selected)
+  let opened = hexRegionOpening selected
+      closed = hexRegionClosing selected
+  assertEqual "opening is idempotent" opened (hexRegionOpening opened)
+  assertEqual "closing is idempotent" closed (hexRegionClosing closed)
+  assertEqual "opening is contractive" (Right True) (hexRegionSubsetOf opened selected)
+  assertEqual "closing is extensive" (Right True) (hexRegionSubsetOf selected closed)
+
+  boundaryLayout <- requireRight (hexLayout (HexCoord 0 0) 2 2)
+  let boundaryUniverse = Set.fromList (Vector.toList (layoutCoords boundaryLayout))
+  boundaryRegions <- traverse (regionFromSet boundaryLayout) (powerset boundaryUniverse)
+  traverse_
+    (\(boundarySet, boundaryRegion) -> do
+       let referenceOpened = referenceDilation boundaryLayout (referenceErosion boundaryLayout boundarySet)
+           referenceClosed = referenceErosion boundaryLayout (referenceDilation boundaryLayout boundarySet)
+           nativeOpened = hexRegionOpening boundaryRegion
+           nativeClosed = hexRegionClosing boundaryRegion
+       assertRegion "bounded opening reference" referenceOpened nativeOpened
+       assertRegion "bounded closing reference" referenceClosed nativeClosed
+       assertEqual "bounded opening idempotence" nativeOpened (hexRegionOpening nativeOpened)
+       assertEqual "bounded closing idempotence" nativeClosed (hexRegionClosing nativeClosed)
+       assertEqual "bounded opening contraction" (Right True) (hexRegionSubsetOf nativeOpened boundaryRegion)
+       assertEqual "bounded closing extension" (Right True) (hexRegionSubsetOf boundaryRegion nativeClosed))
+    boundaryRegions
+  traverse_
+    (\(_, leftRegion) ->
+       traverse_
+         (\(_, rightRegion) ->
+            assertEqual
+              "bounded dilation/erosion adjunction"
+              (hexRegionSubsetOf (hexRegionDilate leftRegion) rightRegion)
+              (hexRegionSubsetOf leftRegion (hexRegionErode rightRegion)))
+         boundaryRegions)
+    boundaryRegions
+
+  componentRegionValue <-
+    requireRight
+      ( hexRegionFromCoords
+          layout
+          [HexCoord (-1) (-1), HexCoord 0 (-1), HexCoord 2 0, HexCoord (-1) 2]
+      )
+  let components = hexRegionComponentLabels componentRegionValue
+  assertEqual "three native components" 3 (hexComponentCount components)
+  assertEqual "least selected cell seeds component zero" (Just 0) (hexComponentIndexAt (HexCoord (-1) (-1)) components)
+  assertEqual "adjacent selected cell shares component" (Just 0) (hexComponentIndexAt (HexCoord 0 (-1)) components)
+  assertEqual "next selected cell seeds component one" (Just 1) (hexComponentIndexAt (HexCoord 2 0) components)
+  assertEqual "unselected cell has no component" Nothing (hexComponentIndexAt (HexCoord 0 0) components)
+  firstComponent <- maybe (fail "expected component zero") pure (hexComponentRegion 0 components)
+  assertRegion "component materialization" (Set.fromList [HexCoord (-1) (-1), HexCoord 0 (-1)]) firstComponent
+  assertEqual "invalid component is refused" Nothing (hexComponentRegion 3 components)
+
+  domain <-
+    requireRight
+      ( hexRegionFromCoords
+          layout
+          [ HexCoord (-1) (-1)
+          , HexCoord 0 (-1)
+          , HexCoord 1 (-1)
+          , HexCoord 0 0
+          , HexCoord 1 0
+          , HexCoord 2 0
+          ]
+      )
+  sources <- requireRight (hexRegionFromCoords layout [HexCoord (-1) (-1), HexCoord 2 0])
+  distances <- requireRight (hexRegionDistancesWithin domain sources)
+  assertEqual "source distance" (Just 0) (hexDistanceAt (HexCoord (-1) (-1)) distances)
+  assertEqual "multi-source minimum distance" (Just 1) (hexDistanceAt (HexCoord 1 0) distances)
+  assertEqual "unreachable coordinate has no distance" Nothing (hexDistanceAt (HexCoord (-1) 2) distances)
+  assertEqual "maximum finite distance" (Just 2) (hexDistanceMaximum distances)
+  assertEqual "distance map preserves its layout" layout (hexDistanceMapLayout distances)
+  assertEqual "reachable region is the domain" domain (hexDistanceReachableRegion distances)
+  invalidSources <- requireRight (hexRegionFromCoords layout [HexCoord (-1) 2])
+  assertEqual
+    "source outside traversal domain is typed"
+    (Left (HexTraversalSourceOutsideDomain (HexCoord (-1) 2)))
+    (hexRegionDistancesWithin domain invalidSources)
+
+  traverse_
+    (\cellCount -> do
+       rowLayout <- requireRight (hexLayout (HexCoord 0 0) cellCount 2)
+       let rowSource = hexRegionGenerate rowLayout (\(HexCoord q r) -> q == cellCount - 1 && r == 0)
+           expected =
+             Set.fromList
+               [ HexCoord (cellCount - 1) 0
+               , HexCoord (cellCount - 2) 0
+               , HexCoord (cellCount - 1) 1
+               , HexCoord (cellCount - 2) 1
+               ]
+       assertRegion ("packed row boundary dilation " <> show cellCount) expected (hexRegionDilate rowSource))
+    [63, 64, 65, 127, 128]
+ where
+  referenceDilation :: HexLayout -> Set HexCoord -> Set HexCoord
+  referenceDilation layout selected =
+    Set.foldl'
+      (\expanded coordinate ->
+         foldl'
+           (\result direction -> maybe result (`Set.insert` result) (hexNeighbourCoord layout coordinate direction))
+           expanded
+           allHexDirections)
+      selected
+      selected
+
+  referenceErosion :: HexLayout -> Set HexCoord -> Set HexCoord
+  referenceErosion layout selected =
+    Set.filter
+      (\coordinate -> all (maybe True (`Set.member` selected) . hexNeighbourCoord layout coordinate) allHexDirections)
+      selected
+
 restrictionLaws :: Assertion
 restrictionLaws = do
   outer <- requireRight (hexLayout (HexCoord (-2) (-2)) 6 5)
@@ -136,11 +336,54 @@
     "overlap disagreement names its coordinate"
     (Left (HexOverlapDisagreement (HexCoord 1 0)))
     (glueCompatibleHexRegions (left NonEmpty.:| [disagreeing]))
+  wideTarget <- requireRight (hexLayout (HexCoord (-5) (-7)) 130 67)
+  leftStripLayout <- requireRight (hexLayout (HexCoord (-5) (-7)) 3 67)
+  middleLayout <- requireRight (hexLayout (HexCoord (-4) (-5)) 65 31)
+  rightStripLayout <- requireRight (hexLayout (HexCoord 122 (-7)) 3 67)
+  let layouts = [leftStripLayout, middleLayout, rightStripLayout]
+      membership (HexCoord q r) = (q + 3 * r) `mod` 7 <= 2
+      localSection localLayout = hexRegionGenerate localLayout membership
+      expected =
+        hexRegionGenerate
+          wideTarget
+          (\coordinate -> any (`hexLayoutContains` coordinate) layouts && membership coordinate)
+  wideGlued <-
+    requireRight
+      ( glueCompatibleHexRegions
+          ( localSection leftStripLayout
+              NonEmpty.:| [localSection middleLayout, localSection rightStripLayout]
+          )
+      )
+  assertEqual "gluing visits disjoint local word spans without changing descent" expected wideGlued
 
 planarLaws :: Assertion
 planarLaws = do
   cellLoop <- requireRight (hexCellExactLoop (HexCoord 0 0))
   assertEqual "one cell has six exact vertices" 6 (NonEmpty.length (exactLoopPoints cellLoop))
+  restrictionLayout <- requireRight (hexLayout (HexCoord (-1) (-1)) 3 3)
+  singleCell <- requireRight (singletonHexRegion restrictionLayout (HexCoord 0 0))
+  singleCellPlanar <- requireRight (hexRegionPlanarRegion singleCell)
+  assertRegion
+    "exact centre restriction"
+    (Set.singleton (HexCoord 0 0))
+    (hexRegionByCenterInPlanarRegion restrictionLayout singleCellPlanar)
+  assertRegion
+    "exact full-cell restriction"
+    (Set.singleton (HexCoord 0 0))
+    (hexRegionFullyCoveredByPlanarRegion restrictionLayout singleCellPlanar)
+  let touchingCells =
+        Set.fromList
+          ( HexCoord 0 0
+              : [ neighbour
+                | direction <- NonEmpty.toList allHexDirections
+                , Just neighbour <- [hexNeighbourCoord restrictionLayout (HexCoord 0 0) direction]
+                ]
+          )
+  assertRegion
+    "exact closed-intersection restriction retains edge-touching cells"
+    touchingCells
+    (hexRegionIntersectingPlanarRegion restrictionLayout singleCellPlanar)
+
   layout <- requireRight (hexLayout (HexCoord 0 0) 2 1)
   adjacent <- requireRight (hexRegionFromCoords layout [HexCoord 0 0, HexCoord 1 0])
   adjacentRegion <- requireRight (hexRegionPlanarRegion adjacent)
@@ -176,14 +419,153 @@
     2
     (sum (fmap (length . polygonHoleLoops) (planarRegionComponents nestedRegion)))
 
+-- Independent rational arithmetic oracle. It deliberately neither calls the
+-- production point/segment predicates nor uses the row candidate index.
+type RationalPoint = (Rational, Rational)
+
+planarRestrictionOracleLaws :: Assertion
+planarRestrictionOracleLaws = do
+  traverse_ checkFixture
+    [ ("empty region", [], [])
+    , ("tiny island", [[(2 % 5, 1 % 10), (3 % 5, 1 % 10), (3 % 5, 3 % 10), (2 % 5, 3 % 10)]], [])
+    , ("tiny hole", [rectangle (-12) (-12) 12 12], [rectangle (2 % 5) (1 % 10) (3 % 5) (3 % 10)])
+    , ("vertex contact", [rectangle 2 0 3 1], [])
+    , ("full side contact", [rectangle (-1) 1 1 2], [])
+    , ("oblique collinear row", [[(0, 0), (9, 3), (9, 7), (0, 4)]], [])
+    , ("thin corridor", [[(-22, -7), (22, 7), (22, 71 % 10), (-22, -69 % 10)]], [])
+    , ("separated islands", [rectangle (-19) (-5) (-17) (-3), rectangle 17 3 19 5], [])
+    , ("hole tangent to centre row", [rectangle (-12) (-12) 12 12], [[(0, 0), (1, 1), (-1, 1)]])
+    , ("concave row events", [[(-6, -2), (6, -2), (6, 2), (3, 1), (0, 2), (-3, 1), (-6, 2)]], [])
+    ]
+ where
+  rectangle :: Rational -> Rational -> Rational -> Rational -> [RationalPoint]
+  rectangle left bottom right top = [(left, bottom), (right, bottom), (right, top), (left, top)]
+  checkFixture (label, outers, holes) =
+    traverse_
+      (\translation@(translationQ, translationR) -> do
+          layout <- requireRight (hexLayout (HexCoord (translationQ - 8) (translationR - 2)) 17 5)
+          oneCell <- requireRight (hexLayout (HexCoord translationQ translationR) 1 1)
+          region <- rationalRegion (fmap (fmap (translatePoint translation)) outers) (fmap (fmap (translatePoint translation)) holes)
+          traverse_ (checkSelections label region) [layout, oneCell])
+      [(0, 0), (-13, 7), (2 ^ (55 :: Int), negate (2 ^ (54 :: Int)))]
+  translatePoint :: (Int, Int) -> RationalPoint -> RationalPoint
+  translatePoint (q, r) (x, y) =
+    (x + fromInteger (3 * toInteger q), y + fromInteger (2 * toInteger r + toInteger q))
+  checkSelections label region layout = do
+    let coordinates = Vector.toList (layoutCoords layout)
+        loops = rationalRegionLoops region
+        boundaries = concatMap (concatMap rationalCycle) loops
+        centre :: HexCoord -> RationalPoint
+        centre (HexCoord q r) = (fromInteger (3 * toInteger q), fromInteger (2 * toInteger r + toInteger q))
+        cellPoints = fmap (\vertex -> let (x, y) = hexVertexCoordinates vertex in (fromInteger x, fromInteger y)) . NonEmpty.toList . hexCellVertices
+        centreSelected = rationalRegionContains loops . centre
+        covered coordinate = centreSelected coordinate && not (any (segmentHasInteriorPoint (cellPoints coordinate)) boundaries)
+        intersecting coordinate =
+          let points = cellPoints coordinate
+              sides = rationalCycle points
+           in any (rationalRegionContains loops) points
+                || any (\(from, to) -> rationalConvexContains points from || any (rationalSegmentsMeet (from, to)) sides) boundaries
+        expected predicate = Set.fromList (filter predicate coordinates)
+    assertRegion (label <> " centre oracle") (expected centreSelected) (hexRegionByCenterInPlanarRegion layout region)
+    assertRegion (label <> " coverage oracle") (expected covered) (hexRegionFullyCoveredByPlanarRegion layout region)
+    assertRegion (label <> " intersection oracle") (expected intersecting) (hexRegionIntersectingPlanarRegion layout region)
+
+rationalRegion :: [[RationalPoint]] -> [[RationalPoint]] -> IO PlanarRegion
+rationalRegion outers holes = do
+  admittedHoles <- traverse (admitLoop . reverse) holes
+  components <- traverse (\outer -> admitLoop outer >>= \loop -> requireRight (polygonComponent loop admittedHoles)) outers
+  requireRight (planarRegion components)
+ where
+  admitLoop points = do
+    exactPoints <- traverse exactRationalPoint points
+    maybe (fail "oracle fixture loop is empty") (requireRight . exactLoop) (NonEmpty.nonEmpty exactPoints)
+  exactRationalPoint (x, y) =
+    exactPoint <$> requireRight (exactRational (numerator x) (denominator x)) <*> requireRight (exactRational (numerator y) (denominator y))
+
+rationalRegionLoops :: PlanarRegion -> [[[RationalPoint]]]
+rationalRegionLoops =
+  fmap (\component -> fmap (fmap rationalPoint . NonEmpty.toList . exactLoopPoints) (polygonOuterLoop component : polygonHoleLoops component))
+    . planarRegionComponents
+ where
+  rationalPoint :: ExactPoint -> RationalPoint
+  rationalPoint point =
+    let (x, y) = exactPointCoordinates point
+        rational value = exactRationalNumerator value % exactRationalDenominator value
+     in (rational x, rational y)
+
+rationalCycle :: [point] -> [(point, point)]
+rationalCycle points = case points of
+  [] -> []
+  firstPoint : remaining -> zip points (remaining <> [firstPoint])
+
+rationalCross :: RationalPoint -> RationalPoint -> RationalPoint -> Rational
+rationalCross (ax, ay) (bx, by) (px, py) = (bx - ax) * (py - ay) - (by - ay) * (px - ax)
+
+rationalOnSegment :: RationalPoint -> (RationalPoint, RationalPoint) -> Bool
+rationalOnSegment point@(x, y) (from@(ax, ay), to@(bx, by)) =
+  rationalCross from to point == 0 && min ax bx <= x && x <= max ax bx && min ay by <= y && y <= max ay by
+
+rationalLoopLocation :: [RationalPoint] -> RationalPoint -> Ordering
+rationalLoopLocation points query@(x, y)
+  | any (rationalOnSegment query) sides = EQ
+  | odd (length (filter crosses sides)) = GT
+  | otherwise = LT
+ where
+  sides = rationalCycle points
+  crosses ((ax, ay), (bx, by)) = (ay > y) /= (by > y) && x < ax + (y - ay) * (bx - ax) / (by - ay)
+
+rationalRegionContains :: [[[RationalPoint]]] -> RationalPoint -> Bool
+rationalRegionContains components point =
+  any (\loops -> case loops of
+      [] -> False
+      outer : holes -> rationalLoopLocation outer point /= LT && all ((/= GT) . (`rationalLoopLocation` point)) holes)
+    components
+
+rationalConvexContains :: [RationalPoint] -> RationalPoint -> Bool
+rationalConvexContains points query = all (\(from, to) -> rationalCross from to query >= 0) (rationalCycle points)
+
+rationalSegmentsMeet :: (RationalPoint, RationalPoint) -> (RationalPoint, RationalPoint) -> Bool
+rationalSegmentsMeet left@(a, b) right@(c, d) =
+  any (`rationalOnSegment` right) [a, b]
+    || any (`rationalOnSegment` left) [c, d]
+    || (rationalCross a b c * rationalCross a b d < 0 && rationalCross c d a * rationalCross c d b < 0)
+
+segmentHasInteriorPoint :: [RationalPoint] -> (RationalPoint, RationalPoint) -> Bool
+segmentHasInteriorPoint polygon (from@(ax, ay), to@(bx, by)) =
+  any strictlyInside (fmap pointAt parameters)
+ where
+  sides = rationalCycle polygon
+  pointAt parameter = (ax + parameter * (bx - ax), ay + parameter * (by - ay))
+  strictlyInside point = all (\(a, b) -> rationalCross a b point > 0) sides
+  cuts = List.sort (0 : 1 :
+    [ parameter
+    | (a, b) <- sides
+    , let fromSide = rationalCross a b from
+    , let toSide = rationalCross a b to
+    , fromSide /= toSide
+    , let parameter = fromSide / (fromSide - toSide)
+    , parameter > 0 && parameter < 1
+    ])
+  parameters = cuts <> zipWith (\left right -> (left + right) / 2) cuts (drop 1 cuts)
+
 serializationLaws :: Assertion
 serializationLaws = do
   layout <- requireRight (hexLayout (HexCoord (-3) 4) 13 7)
   let source = hexRegionGenerate layout (\(HexCoord q r) -> (q + 2 * r) `mod` 5 == 0)
       bytes = encodeHexRegion source
       budget = HexDecodingBudget 4096 1024
+      (prefix, finalWord) = BL.splitAt (BL.length bytes - 8) bytes
+      nonCanonicalBytes = prefix <> BL.cons 0x80 (BL.drop 1 finalWord)
   assertEqual "round trip" (Right source) (decodeHexRegion budget bytes)
   assertEqual
+    "serialized padding is rejected by the region owner"
+    ( Left
+        ( HexSerializedPackedRegionInvalid
+            (HexPackedRegionNonCanonicalPadding 0x8000000000000000)
+        )
+    )
+    (decodeHexRegion budget nonCanonicalBytes)
+  assertEqual
     "input byte budget is checked before parsing"
     (Left (HexInputByteBudgetExceeded (fromIntegral (BL.length bytes)) 1))
     (decodeHexRegion (HexDecodingBudget 1 1024) bytes)
@@ -227,3 +609,6 @@
 
 assertBool :: String -> Bool -> Assertion
 assertBool label condition = if condition then pure () else fail label
+
+packedWords :: HexRegion -> U.Vector Word64
+packedWords = U.fromList . reverse . foldHexRegionPackedWords (flip (:)) []
diff --git a/test/illustration/Main.hs b/test/illustration/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Main.hs
@@ -0,0 +1,12 @@
+module Main (main) where
+
+import qualified Moonlight.Planar.IllustrationSpec as Illustration
+import qualified Moonlight.Planar.IllustrationStudySpec as Study
+import qualified Moonlight.Planar.EquationalGardenSpec as Garden
+import qualified Moonlight.Planar.MotifSpec as Motif
+import qualified Moonlight.Planar.LayoutSpec as Layout
+import qualified Moonlight.Planar.SpaceSwordSpec as SpaceSword
+
+main :: IO ()
+main = () <$ sequenceA
+  [Illustration.tests, Motif.tests, Layout.tests, Study.tests, Garden.tests, SpaceSword.tests]
diff --git a/test/illustration/Moonlight/Planar/EquationalGardenSpec.hs b/test/illustration/Moonlight/Planar/EquationalGardenSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/EquationalGardenSpec.hs
@@ -0,0 +1,107 @@
+module Moonlight.Planar.EquationalGardenSpec (tests) where
+
+import Control.Monad (unless)
+import Data.Bifunctor (first)
+import Data.Foldable (toList, traverse_)
+import Data.List (isInfixOf)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Curve (Path, Subpath (..), location, path, transformPath)
+import Moonlight.Planar.Exact
+  ( exactHalf, exactPoint, positiveExact, unitOne, unitZero )
+import Moonlight.Planar.Exhibit.EquationalGarden
+  ( GardenControls (..), GardenPart (..), defaultGardenControls, gardenPartName
+  , gardenPicture, leafOutline, petalOutline, gardenPlants, GardenPlant (..)
+  , StemPort (..), LeafPort (..), BlossomPort (..) )
+import Moonlight.Planar.Illustration (Picture, PictureAlgebra (..), foldPicture)
+import Moonlight.Planar.Illustration.Motif (motifPort)
+import Moonlight.Planar.Illustration.Svg
+  ( renderSvg, svgOptions, svgPrecision, svgViewport )
+
+-- These are artistic dependency and publication tests. Curve interpolation
+-- laws belong to the authoring tests, not a second implementation here.
+tests :: IO ()
+tests = do
+  baseline <- either (fail . show) pure (gardenPicture defaultGardenControls)
+  opened <- either (fail . show) pure (gardenPicture defaultGardenControls { blossomOpening = unitOne })
+  wind <- either (fail . show) pure (gardenPicture defaultGardenControls { gardenBreeze = unitOne })
+  traverse_ checkAttachments [defaultGardenControls,
+    defaultGardenControls { blossomOpening = unitOne, gardenBreeze = unitOne }]
+  traverse_ (checkLayer baseline opened [Blossoms]) parts
+  traverse_ (checkLayer baseline wind [Stems, Leaves]) parts
+  check "petal width edit changes the real contour"
+    (petalOutline unitZero /= petalOutline unitOne)
+  check "petal attachment remains fixed"
+    (location (petalOutline unitZero) == location (petalOutline unitOne))
+  check "leaf bend changes the real contour"
+    (leafOutline unitZero /= leafOutline unitOne)
+  check "leaf attachment remains fixed"
+    (location (leafOutline unitZero) == location (leafOutline unitOne))
+  case publishedGarden of
+    Left failure -> fail failure
+    Right svg -> do
+      check "garden retains a clipped even-odd moon cutout"
+        ("fill-rule=\"evenodd\"" `isInfixOf` svg && "<clipPath" `isInfixOf` svg)
+      check "garden publishes both kinds of gradient"
+        ("<linearGradient" `isInfixOf` svg && "<radialGradient" `isInfixOf` svg)
+      traverse_ (\part -> check ("published layer: " <> gardenPartName part)
+        (gardenPartName part `isInfixOf` svg)) parts
+  putStrLn "equational garden: independent controls, stable attachments and full-scene publication ok"
+
+checkAttachments :: GardenControls -> IO ()
+checkAttachments controls = do
+  assemblies <- either (fail . show) pure (gardenPlants controls)
+  traverse_ (\plant -> do
+    check "lower leaf matches its full stem socket frame"
+      (motifPort (gardenLowerLeaf plant) LeafRoot == motifPort (gardenStem plant) LowerLeafSocket)
+    check "upper leaf matches its full stem socket frame"
+      (motifPort (gardenUpperLeaf plant) LeafRoot == motifPort (gardenStem plant) UpperLeafSocket)
+    check "blossom matches its full stem socket frame"
+      (motifPort (gardenBlossom plant) BlossomRoot == motifPort (gardenStem plant) BlossomSocket)) assemblies
+
+parts :: [GardenPart]
+parts = [NightSky, Moon, Ground, Stems, Leaves, Blossoms, Ribbon, Fireflies]
+
+checkLayer :: Picture GardenPart -> Picture GardenPart -> [GardenPart] -> GardenPart -> IO ()
+checkLayer baseline edited affected part = do
+  let original = observedPaths part baseline
+      candidate = observedPaths part edited
+  check ("nonempty garden layer: " <> gardenPartName part) (not (null original))
+  check ("semantic edit dependency: " <> gardenPartName part)
+    ((original /= candidate) == (part `elem` affected))
+
+-- A read-only projection of the existing algebra into canonical paths.
+-- It preserves affine placement and sees clip boundaries; no geometry oracle
+-- or alternate scene representation is introduced.
+observedPaths :: GardenPart -> Picture GardenPart -> [Path]
+observedPaths selected picture = foldPicture algebra picture Nothing
+ where
+  algebra :: PictureAlgebra GardenPart (Maybe GardenPart -> [Path])
+  algebra = PictureAlgebra
+    { paintSequence = \children inherited -> concatMap ($ inherited) (toList children)
+    , paintFill = \_ _ contours inherited ->
+        keep inherited (path (Seq.fromList (ClosedSubpath <$> NonEmpty.toList contours)))
+    , paintStroke = \_ curve inherited -> keep inherited curve
+    , paintClip = \_ contours child inherited ->
+        keep inherited (path (Seq.fromList (ClosedSubpath <$> NonEmpty.toList contours))) <> child inherited
+    , paintPlace = \frame child inherited -> transformPath frame <$> child inherited
+    , paintOpacity = \_ child -> child
+    , paintAnnotation = \part child _ -> child (Just part)
+    }
+  keep :: Maybe GardenPart -> Path -> [Path]
+  keep inherited curve = if inherited == Just selected then [curve] else []
+
+publishedGarden :: Either String String
+publishedGarden = do
+  width <- first show (positiveExact 1200)
+  height <- first show (positiveExact 1000)
+  numeric <- first show (positiveExact (exactHalf ^ (18 :: Int)))
+  tolerance <- first show (positiveExact exactHalf)
+  viewport <- first show (svgViewport 1200 1000 (exactPoint 0 0) width height)
+  precision <- first show (svgPrecision 6 numeric)
+  options <- first show (svgOptions viewport precision tolerance 20 16384)
+  picture <- first show (gardenPicture defaultGardenControls)
+  first show (renderSvg gardenPartName options picture)
+
+check :: String -> Bool -> IO ()
+check label condition = unless condition (fail label)
diff --git a/test/illustration/Moonlight/Planar/IllustrationSpec.hs b/test/illustration/Moonlight/Planar/IllustrationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/IllustrationSpec.hs
@@ -0,0 +1,213 @@
+module Moonlight.Planar.IllustrationSpec (tests) where
+
+import Data.Bifunctor (first)
+import Data.Either (isLeft)
+import Data.Foldable (traverse_)
+import Data.List (isInfixOf, isPrefixOf, tails)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine (affine2, identityAffine2)
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, Subpath (..), circle, closeWith, cubic, curveStep
+  , line, locate, openTrail, path, quadratic )
+import Moonlight.Planar.Exact
+  ( ExactRational, ExactVector (..), exactPoint, exactHalf
+  , exactThird, positiveExact, positiveOne, positiveTwo, unitHalf, unitOne, unitZero )
+import Moonlight.Planar.Illustration
+  ( Color (..), FillRule (..), GradientStop (..), IllustrationError (..)
+  , LineCap (..), LineJoin (..), Paint (..), Picture, StrokeStyle (..)
+  , StrokeUnits (..), annotate, clip, fill, miterLimit, miterLimitFour, opacity
+  , place, stroke, gradientStops, gradientStopValues )
+import Moonlight.Planar.Illustration.Svg
+  ( SvgError (..), SvgOptions, renderDiagnosticSvg, renderSvg
+  , svgOptions, svgPrecision, svgViewport )
+
+tests :: IO ()
+tests = traverse_ run cases
+ where
+  run :: (String, Either String ()) -> IO ()
+  run (name, result) = either (fail . ((name <> ": ") <>)) pure result
+
+cases :: [(String, Either String ())]
+cases =
+  [ ("ordered monoid", orderedMonoid)
+  , ("native polynomial paths", nativePaths)
+  , ("compound fill and scoped clip", compoundClip)
+  , ("group opacity is not distributed", groupedOpacity)
+  , ("stroke units and joins", strokeSemantics)
+  , ("gradient definitions and stable IDs", gradientSemantics)
+  , ("annotations escaped and diagnosed", annotations)
+  , ("XML control characters refused", invalidXml)
+  , ("conics bounded or refused", conicPublication)
+  , ("affine output metric controls subdivision", affineSubdivision)
+  , ("decimal precision and range refusals", precisionRefusals)
+  , ("miter refinement", miterRefinement)
+  ]
+
+assert :: String -> Bool -> Either String ()
+assert message condition = if condition then Right () else Left message
+
+checked :: Show e => Either e a -> Either String a
+checked = first show
+
+options :: Int -> ExactRational -> ExactRational -> Int -> Int -> Either String SvgOptions
+options digits numericTolerance curveTolerance depth leaves = do
+  extent <- checked (positiveExact 100)
+  viewport <- checked (svgViewport 100 100 (exactPoint 0 0) extent extent)
+  numeric <- checked (positiveExact numericTolerance)
+  precision <- checked (svgPrecision digits numeric)
+  curves <- checked (positiveExact curveTolerance)
+  checked (svgOptions viewport precision curves depth leaves)
+
+standard :: Either String SvgOptions
+standard = options 6 (exactHalf ^ (18 :: Int)) (exactHalf ^ (3 :: Int)) 18 10000
+
+render :: Picture String -> Either String String
+render picture = standard >>= \config -> checked (renderSvg id config picture)
+
+square :: Located ClosedTrail
+square = locate (exactPoint 0 0) $ closeWith line $ openTrail $ Seq.fromList
+  [ curveStep line (ExactVector 10 0)
+  , curveStep line (ExactVector 0 10)
+  , curveStep line (ExactVector (-10) 0)
+  ]
+
+red, blue :: Picture String
+red = fill NonZero (Solid (RGB 255 0 0)) (square :| [])
+blue = fill NonZero (Solid (RGB 0 0 255)) (square :| [])
+
+orderedMonoid :: Either String ()
+orderedMonoid = do
+  assert "picture monoid associativity" ((red <> blue) <> red == red <> (blue <> red))
+  assert "picture monoid identities" (mempty <> red == red && red <> mempty == red)
+  let named = annotate "horn" red
+  assert "annotation functor identity" (fmap id named == named)
+  assert "annotation functor composition"
+    (fmap (("left." <>) . (<> ".tip")) named == (fmap ("left." <>) . fmap (<> ".tip")) named)
+  forward <- render (red <> blue)
+  backward <- render (blue <> red)
+  assert "painter order was discarded" (forward /= backward)
+  assert "red must paint before blue" ("fill=\"#ff0000\" fill-rule=\"nonzero\"/><path" `isInfixOf` forward)
+
+style :: StrokeUnits -> StrokeStyle
+style units = StrokeStyle (Solid (RGB 0 0 0)) positiveOne units RoundCap (MiterJoin miterLimitFour)
+
+nativePaths :: Either String ()
+nativePaths = do
+  let curve = path $ Seq.singleton $ OpenSubpath $ locate (exactPoint 0 0) $ openTrail $ Seq.fromList
+        [ curveStep (quadratic (ExactVector 3 4)) (ExactVector 6 0)
+        , curveStep (cubic (ExactVector 1 2) (ExactVector 3 2)) (ExactVector 4 0)
+        ]
+  svg <- render (stroke (style LocalUnits) curve)
+  assert "quadratic was not retained" ("Q 3.000000 4.000000 6.000000 0.000000" `isInfixOf` svg)
+  assert "cubic controls lost their local origin" ("C 7.000000 2.000000 9.000000 2.000000 10.000000 0.000000" `isInfixOf` svg)
+
+compoundClip :: Either String ()
+compoundClip = do
+  let hole = locate (exactPoint 3 3) $ closeWith line $ openTrail $ Seq.fromList
+        [curveStep line (ExactVector 2 0), curveStep line (ExactVector 0 2), curveStep line (ExactVector (-2) 0)]
+      contours = square :| [hole]
+  svg <- render (clip EvenOdd contours (fill EvenOdd (Solid (RGB 4 5 6)) contours) <> blue)
+  assert "compound path lost a contour" (count "M 3.000000 3.000000" svg == 2)
+  assert "clip and fill winding differ" ("clip-rule=\"evenodd\"" `isInfixOf` svg && "fill-rule=\"evenodd\"" `isInfixOf` svg)
+  assert "clip was not scoped" ("</g><path" `isInfixOf` svg)
+
+groupedOpacity :: Either String ()
+groupedOpacity = do
+  svg <- render (opacity unitHalf (red <> blue))
+  assert "opacity duplicated onto children" (count "opacity=" svg == 1)
+  assert "opacity lost its group" ("<g opacity=\"0.500000\"><path" `isInfixOf` svg)
+
+strokeSemantics :: Either String ()
+strokeSemantics = do
+  let curve = path (Seq.singleton (ClosedSubpath square))
+      transform = affine2 (ExactVector 2 0) (ExactVector 0 3) (ExactVector 7 11)
+  svg <- render (place transform (stroke (style LocalUnits) curve <> stroke (style OutputUnits) curve))
+  assert "output stroke scaling not isolated" (count "non-scaling-stroke" svg == 1)
+  assert "cap or miter lost" (count "stroke-linecap=\"round\"" svg == 2 && count "stroke-miterlimit=\"4.000000\"" svg == 2)
+  assert "affine coefficient order incorrect" ("matrix(2.000000 0.000000 0.000000 3.000000 7.000000 11.000000)" `isInfixOf` svg)
+
+gradientSemantics :: Either String ()
+gradientSemantics = do
+  let start = GradientStop unitZero (RGB 1 2 3) unitOne
+      end = GradientStop unitOne (RGB 4 5 6) unitHalf
+      final = GradientStop unitOne (RGB 7 8 9) unitOne
+      stops = gradientStops (end :| [start, final])
+      linear :: Picture String
+      linear = fill NonZero (LinearGradient (exactPoint 0 0) (exactPoint 10 0) stops) (square :| [])
+      radial :: Picture String
+      radial = fill NonZero (RadialGradient (exactPoint 5 5) positiveTwo stops) (square :| [])
+      picture = annotate "same" linear <> annotate "same" radial
+  assert "gradient canonicalization unstable at equal offsets"
+    (gradientStopValues stops == start :| [end, final])
+  svg <- render picture
+  again <- render picture
+  assert "render nondeterministic" (svg == again)
+  assert "gradient units not local user space" (count "gradientUnits=\"userSpaceOnUse\"" svg == 2)
+  assert "gradient IDs not structural and unique"
+    (count "id=\"paint-0-0-0\"" svg == 1 && count "id=\"paint-1-0-0\"" svg == 1)
+  assert "gradient reference mismatch" ("fill=\"url(#paint-1-0-0)\"" `isInfixOf` svg)
+
+annotations :: Either String ()
+annotations = do
+  config <- standard
+  let picture = annotate "horn<&\"'" (place identityAffine2 red)
+  svg <- checked (renderSvg id config picture)
+  diagnostic <- checked (renderDiagnosticSvg id config picture)
+  assert "label was not escaped" ("horn&lt;&amp;&quot;&apos;" `isInfixOf` svg)
+  assert "diagnostic did not retain artwork" ("fill=\"#ff0000\"" `isInfixOf` diagnostic)
+  assert "diagnostic lacks controls/axes/part caption" (all (`isInfixOf` diagnostic) ["polyline", "#288e94", "<text", "authoring guides"])
+  assert "final artifact leaked guides" (not ("authoring guides" `isInfixOf` svg))
+
+invalidXml :: Either String ()
+invalidXml = do
+  config <- standard
+  case renderSvg id config (annotate "bad\NULlabel" red) of
+    Left (SvgInvalidXmlCharacter '\NUL') -> Right ()
+    outcome -> Left ("expected XML character refusal, got " <> show outcome)
+
+conicPublication :: Either String ()
+conicPublication = do
+  let disc :: Picture String
+      disc = fill NonZero (Solid (RGB 1 1 1)) (circle positiveTwo :| [])
+  svg <- render disc
+  assert "conic fallback not polygonal" ("L " `isInfixOf` svg && not ("Q " `isInfixOf` svg))
+  config <- options 6 (exactHalf ^ (18 :: Int)) (exactHalf ^ (10 :: Int)) 0 1
+  case renderSvg id config disc of
+    Left (SvgLoweringRefused _) -> Right ()
+    outcome -> Left ("expected conic budget refusal, got " <> show outcome)
+
+affineSubdivision :: Either String ()
+affineSubdivision = do
+  let disc :: Picture String
+      disc = fill NonZero (Solid (RGB 1 1 1)) (circle positiveTwo :| [])
+      enlarge = affine2 (ExactVector 100 0) (ExactVector 0 100) (ExactVector 0 0)
+  local <- render disc
+  scaled <- render (place enlarge disc)
+  assert "conic budget ignored output transform" (count "L " scaled > count "L " local)
+
+precisionRefusals :: Either String ()
+precisionRefusals = do
+  strict <- options 2 (exactHalf ^ (20 :: Int)) 1 18 10000
+  let shifted = affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector exactThird 0)
+  assert "inadequate decimal precision accepted" $ case renderSvg id strict (place shifted red) of
+    Left (SvgNumericPrecisionRefused _ 2) -> True
+    _ -> False
+  config <- standard
+  let huge = affine2 (ExactVector 1 0) (ExactVector 0 1) (ExactVector (10 ^ (310 :: Int)) 0)
+  assert "nonfinite browser scalar accepted" $ case renderSvg id config (place huge red) of
+    Left (SvgNumericRangeRefused _) -> True
+    _ -> False
+  tiny <- checked (positiveExact (exactHalf ^ (30 :: Int)))
+  let tinyStroke = (style LocalUnits) { strokeWidth = tiny }
+  assert "positive stroke rounded to zero" $ case renderSvg id config (stroke tinyStroke (path (Seq.singleton (ClosedSubpath square)))) of
+    Left (SvgPositiveValueRoundedToZero _) -> True
+    _ -> False
+
+miterRefinement :: Either String ()
+miterRefinement = do
+  assert "miter limit below one accepted" (miterLimit exactHalf == Left (InvalidMiterLimit exactHalf))
+  assert "miter limit one refused" (not (isLeft (miterLimit 1)))
+
+count :: String -> String -> Int
+count needle = length . filter (isPrefixOf needle) . tails
diff --git a/test/illustration/Moonlight/Planar/IllustrationStudySpec.hs b/test/illustration/Moonlight/Planar/IllustrationStudySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/IllustrationStudySpec.hs
@@ -0,0 +1,118 @@
+module Moonlight.Planar.IllustrationStudySpec (tests) where
+
+import Control.Monad (unless)
+import Data.Foldable (toList, traverse_)
+import Data.List (nub)
+import Moonlight.Planar.Affine (Affine2, affine2, affineIsoMap, composeAffine2, identityAffine2, transformPoint)
+import Moonlight.Planar.Curve (ClosedTrail, Located, location, transformLocatedClosedTrail)
+import Moonlight.Planar.Curve.Lowering (loweringPolicy)
+import Moonlight.Planar.Curve.Region (CurveComponent (..), lowerSimpleRegion)
+import Moonlight.Planar.Exhibit.IllustrationStudy
+import Moonlight.Planar.Exact (ExactPoint, ExactRational, ExactVector (..), exactPoint, positiveOne)
+import Moonlight.Planar.Region (planarRegionComponents, polygonHoleLoops)
+import Moonlight.Planar.Illustration (PictureAlgebra (..), Paint (..), foldPicture)
+
+-- These are artistic dependency tests, not another curve-equivalence oracle.
+tests :: IO ()
+tests = do
+  let parts = [minBound..maxBound] :: [StudyPart]
+  check "part names uniquely encode the author's vocabulary"
+    (length (nub (partName <$> parts)) == length parts)
+  check "horn pair shares one profile through exact reflection"
+    (partContour defaultControls RightHorn == transformLocatedClosedTrail
+      (affine2 (ExactVector (-1) 0) (ExactVector 0 1) (ExactVector 820 0))
+      (partContour defaultControls LeftHorn))
+  check "left horn port is profile root, not contour seam"
+    (portPoint defaultControls LeftHorn == exactPoint 336 359
+      && portPoint defaultControls LeftHorn /= location (partContour defaultControls LeftHorn))
+  check "right horn root follows actual reflection"
+    (portPoint defaultControls RightHorn == exactPoint 484 359)
+  check "eye port is ellipse center, not seam"
+    (portPoint defaultControls LeftEye == exactPoint 353 420
+      && portPoint defaultControls LeftEye /= location (partContour defaultControls LeftEye))
+  traverse_ (\part -> check ("actual attachment paints canonical contour: " <> show part)
+    (any (\(name,contour,_) -> name == Just part && contour == partContour defaultControls part)
+      (paintedContours defaultControls))) [LeftHorn,RightHorn,LeftEye,RightEye]
+  check "attached horns retain world porcelain gradient coordinates"
+    (all (\(_,_,paint) -> case paint of
+      LinearGradient start end _ -> start == exactPoint 300 330 && end == exactPoint 520 500
+      _ -> False)
+      (filter (\(name,_,_) -> name == Just LeftHorn || name == Just RightHorn)
+        (paintedContours defaultControls)))
+  traverse_ checkEdit
+    [(LeftHornSweep,30,[LeftHorn]),(EyeSpacing,66,[LeftEye,RightEye])
+    ,(EyeTilt,12,[LeftEye,RightEye]),(CloakFullness,30,[Cloak]),(FullerWidth,20,[Fuller])]
+  traverse_ checkRejected
+    [(LeftHornSweep,46),(EyeSpacing,39),(EyeTilt,16),(CloakFullness,56),(FullerWidth,0)]
+  checkBladeHole defaultControls
+  case editControl FullerWidth 24 defaultControls of
+    Left refusal -> ioError (userError (show refusal))
+    Right wide -> checkBladeHole wide
+  putStrLn "illustration study: semantic edits, stable anchors and admitted blade hole ok"
+
+checkEdit :: (StudyControl, ExactRational, [StudyPart]) -> IO ()
+checkEdit (control,value,affected) = case editControl control value defaultControls of
+  Left refusal -> ioError (userError (show refusal))
+  Right edited -> do
+    traverse_ (checkPart edited) [minBound..maxBound]
+    check (show control <> " changes the actual picture")
+      (studyPicture defaultControls /= studyPicture edited)
+    check (show control <> " eye centers follow only spacing")
+      ((partPort defaultControls LeftEye /= partPort edited LeftEye) == (control == EyeSpacing))
+    check (show control <> " horn attachment is fixed")
+      (partPort defaultControls LeftHorn == partPort edited LeftHorn)
+ where
+  checkPart :: StudyControls -> StudyPart -> IO ()
+  checkPart edited part = do
+    let original = partContour defaultControls part
+        candidate = partContour edited part
+    check (show control <> " geometry dependency: " <> show part)
+      ((original /= candidate) == (part `elem` affected))
+    -- Sweep/fullness/width edits preserve the authored attachment location;
+    -- eye edits intentionally move the ellipse seam (spacing moves its center).
+    unless (control `elem` [EyeSpacing,EyeTilt] && part `elem` affected) $
+      check (show control <> " retains local anchor: " <> show part)
+        (location original == location candidate)
+
+checkRejected :: (StudyControl,ExactRational) -> IO ()
+checkRejected (control,value) = check (show control <> " rejects out-of-family values") $
+  case editControl control value defaultControls of
+    Left (ControlOutsideRange rejected rejectedValue) -> rejected == control && rejectedValue == value
+    Right _ -> False
+
+check :: String -> Bool -> IO ()
+check label result = unless result (ioError (userError label))
+
+portPoint :: StudyControls -> StudyPart -> ExactPoint
+portPoint controls part = transformPoint (affineIsoMap (partPort controls part)) (exactPoint 0 0)
+
+-- Observe world contours and translated paint anchors, not nested Picture Eq.
+paintedContours :: StudyControls -> [(Maybe StudyPart, Located ClosedTrail, Paint)]
+paintedContours controls = foldPicture algebra (studyPicture controls) identityAffine2 Nothing
+ where
+  algebra :: PictureAlgebra StudyPart (Affine2 -> Maybe StudyPart -> [(Maybe StudyPart, Located ClosedTrail, Paint)])
+  algebra = PictureAlgebra
+    { paintSequence = \children frame name -> concatMap (\child -> child frame name) (toList children)
+    , paintFill = \_ paint contours frame name ->
+        fmap (\contour -> (name,transformLocatedClosedTrail frame contour,worldPaint frame paint)) (toList contours)
+    , paintStroke = \_ _ _ _ -> []
+    , paintClip = \_ _ child -> child
+    , paintPlace = \local child frame -> child (composeAffine2 frame local)
+    , paintOpacity = \_ child -> child
+    , paintAnnotation = \part child frame _ -> child frame (Just part)
+    }
+  worldPaint :: Affine2 -> Paint -> Paint
+  worldPaint frame paint = case paint of
+    LinearGradient a b stops -> LinearGradient (transformPoint frame a) (transformPoint frame b) stops
+    other -> other
+
+-- At the study's output scale, the actual outer/hole curves must lower into
+-- one admitted polygon with one hole, including the widest allowed fuller.
+checkBladeHole :: StudyControls -> IO ()
+checkBladeHole controls = case loweringPolicy positiveOne identityAffine2 20 8192 of
+  Left refusal -> ioError (userError (show refusal))
+  Right policy -> case lowerSimpleRegion policy
+    [CurveComponent (partContour controls Blade) [partContour controls Fuller]] of
+      Left refusal -> ioError (userError (show refusal))
+      Right (region,_) -> check "pierced blade retains exactly one admitted hole"
+        (map (length . polygonHoleLoops) (planarRegionComponents region) == [1])
diff --git a/test/illustration/Moonlight/Planar/LayoutSpec.hs b/test/illustration/Moonlight/Planar/LayoutSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/LayoutSpec.hs
@@ -0,0 +1,133 @@
+module Moonlight.Planar.LayoutSpec (tests) where
+
+import Control.Monad (unless)
+import Data.Foldable (toList, traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import Data.Maybe (isNothing)
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+import Moonlight.Planar.Curve
+import Moonlight.Planar.Curve.Authoring (polygonTrail)
+import Moonlight.Planar.Exact
+import Moonlight.Planar.Illustration
+import Moonlight.Planar.Illustration.Layout
+import Moonlight.Planar.Illustration.Motif
+
+data Port = Root
+
+tests :: IO ()
+tests = do
+  let empty = geometryEnvelope (mempty :: Picture ())
+      e = geometryEnvelope art
+  check "empty support/bounds" (geometrySupport (ExactVector 1 0) empty == Nothing
+    && isNothing (geometryBounds empty))
+  check "zero direction of nonempty envelope" (geometrySupport (ExactVector 0 0) e == Just 0)
+  traverse_ (envelopeLaws e) directions
+  let frame = affine2 (ExactVector 2 1) (ExactVector (-1) 3) (ExactVector 7 11)
+      collapse = affine2 (ExactVector 0 0) (ExactVector 0 0) (ExactVector 4 9)
+      shear = affine2 (ExactVector 1 0) (ExactVector 3 1) (ExactVector 0 0)
+      unshear = affine2 (ExactVector 1 0) (ExactVector (-3) 1) (ExactVector 0 0)
+  traverse_ (affineLaw e frame) directions
+  traverse_ (affineLaw e collapse) directions
+  traverse_ (\u -> check "nested inverse shear must not rebox"
+    (geometrySupport u (geometryEnvelope (place unshear (place shear art))) == geometrySupport u e)) directions
+  check "singular map retains a point"
+    (boundsObservation (transformGeometryEnvelope collapse e) == Just (4,9,4,9))
+  let clipped = clip NonZero (polygonTrail (exactPoint 0 0 :| [exactPoint 1 0,exactPoint 0 1]) :| []) art
+  traverse_ (\u -> check "clips opacity and annotations do not shrink geometry"
+    (geometrySupport u (geometryEnvelope (annotate () (opacity unitZero clipped))) == geometrySupport u e)) directions
+  check "stroke width and units excluded"
+    (boundsObservation (geometryEnvelope (stroke (pen positiveOne LocalUnits) samplePath))
+      == boundsObservation (geometryEnvelope (stroke (pen positiveTwo OutputUnits) samplePath)))
+  check "empty path differs from located empty trail"
+    (boundsObservation (geometryEnvelope (stroke (pen positiveOne LocalUnits) mempty)) == Nothing
+      && boundsObservation (geometryEnvelope (stroke (pen positiveOne LocalUnits)
+        (path (Seq.singleton (OpenSubpath (locate (exactPoint 9 4) mempty))))))
+          == Just (9,4,9,4))
+  traverse_ containment shapes
+  layoutLaws
+  putStrLn "layout: exact support, conservative geometry, coherent linear arrangement ok"
+
+directions :: [ExactVector]
+directions = [ExactVector 1 0, ExactVector (-1) 0, ExactVector 0 1,
+  ExactVector 0 (-1), ExactVector 2 3, ExactVector (-3) 2, ExactVector 0 0]
+
+boundsObservation :: GeometryEnvelope -> Maybe (ExactRational, ExactRational, ExactRational, ExactRational)
+boundsObservation = fmap (\bounds -> (boundsMinimumX bounds, boundsMinimumY bounds,
+  boundsMaximumX bounds, boundsMaximumY bounds)) . geometryBounds
+
+envelopeLaws :: GeometryEnvelope -> ExactVector -> IO ()
+envelopeLaws e u = do
+  let f = geometryEnvelope (motifPicture (box 13))
+      g = geometryEnvelope (motifPicture (box 2))
+      support = geometrySupport u
+  check "envelope identity" (support (mempty <> e) == support e && support (e <> mempty) == support e)
+  check "envelope observational associativity" (support ((e <> f) <> g) == support (e <> (f <> g)))
+  check "envelope observational commutativity/idempotence" (support (e <> f) == support (f <> e)
+    && support (e <> e) == support e)
+  check "union support maximum" (support (e <> f) == max (support e) (support f))
+
+affineLaw :: GeometryEnvelope -> Affine2 -> ExactVector -> IO ()
+affineLaw e frame u@(ExactVector ux uy) = do
+  let (ExactVector a b, ExactVector c d, ExactVector tx ty) = affineColumns frame
+      expected = (+ (ux * tx + uy * ty)) <$> geometrySupport (ExactVector (a*ux+b*uy) (c*ux+d*uy)) e
+  check "exact affine dual support law" (geometrySupport u (transformGeometryEnvelope frame e) == expected)
+  check "picture and envelope transform agree"
+    (geometrySupport u (geometryEnvelope (place frame art)) == expected)
+
+shapes :: [CurveStep]
+shapes = [curveStep line (ExactVector 5 0)
+  ,curveStep (quadratic (ExactVector 3 8)) (ExactVector 5 0)
+  ,curveStep (cubic (ExactVector (-4) 9) (ExactVector 8 (-3))) (ExactVector 5 0)
+  ,curveStep (rationalQuadratic (ExactVector 4 8) positiveOne positiveTwo) (ExactVector 5 0)]
+
+containment :: CurveStep -> IO ()
+containment step = do
+  let picture :: Picture ()
+      picture = stroke (pen positiveOne LocalUnits)
+        (path (Seq.singleton (OpenSubpath (locate (exactPoint 0 0) (openTrail (Seq.singleton step))))))
+      e = geometryEnvelope picture
+      samples = fmap (\t -> evaluateStep t step) [unitZero,unitHalf,unitOne]
+  traverse_ (\u -> traverse_ (\p -> check "control hull and evaluation witnesses"
+    (Just (dot u p) <= geometrySupport u e)) (toList (stepControlPoints step) <> samples)) directions
+ where
+  dot (ExactVector x y) (ExactVector a b) = x*a+y*b
+
+layoutLaws :: IO ()
+layoutLaws = do
+  let original = box 10
+      aligned = alignMotif Horizontal unitHalf 25 original
+      empty = motif mempty (const (translationAffineIso2 (ExactVector 99 17))) :: Motif Port ()
+      baseline = arrangeMotifs Horizontal 5 (Seq.fromList [box 10,empty,box 20,box 3])
+      wider = arrangeMotifs Horizontal 5 (Seq.fromList [box 10,empty,box 40,box 3])
+      ports :: Foldable f => f (Motif Port ()) -> [ExactPoint]
+      ports = fmap (\value -> transformPoint (affineIsoMap (motifPort value Root)) (exactPoint 0 0)) . toList
+  check "midpoint alignment and attached port" (ports [aligned] == [exactPoint 20 0])
+  check "arrangement skips empties" (ports baseline == [exactPoint 0 0,exactPoint 99 17,exactPoint 15 0,exactPoint 40 0])
+  check "width edit changes only subsequent position" (ports wider == [exactPoint 0 0,exactPoint 99 17,exactPoint 15 0,exactPoint 60 0])
+  check "negative gap intentional overlap" (ports (arrangeMotifs Horizontal (-3) (Seq.fromList [box 10,box 20]))
+    == [exactPoint 0 0,exactPoint 7 0])
+  check "vertical maximum alignment" (ports [alignMotif Vertical unitOne 30 original] == [exactPoint 0 22])
+  traverse_ (\value -> case geometryBounds (geometryEnvelope (motifPicture value)) of
+    Nothing -> pure ()
+    Just bounds -> check "ports follow laid-out geometry"
+      (transformPoint (affineIsoMap (motifPort value Root)) (exactPoint 0 0)
+        == exactPoint (boundsMinimumX bounds) (boundsMinimumY bounds))) baseline
+  check "empty layout identity" (ports [alignMotif Horizontal unitOne 0 empty] == ports [empty])
+
+box :: ExactRational -> Motif Port ()
+box width = motif (fill NonZero (Solid (RGB 1 2 3))
+  (polygonTrail (exactPoint 0 0 :| [exactPoint width 0,exactPoint width 8,exactPoint 0 8]) :| []))
+  (const identityAffineIso2)
+
+pen :: PositiveExact -> StrokeUnits -> StrokeStyle
+pen width units = StrokeStyle (Solid (RGB 1 2 3)) width units SquareCap (MiterJoin miterLimitFour)
+
+samplePath :: Path
+samplePath = path (Seq.singleton (ClosedSubpath (ellipse (ExactVector 8 2) (ExactVector 3 6))))
+
+art :: Picture ()
+art = stroke (pen positiveOne LocalUnits) samplePath
+
+check :: String -> Bool -> IO ()
+check label condition = unless condition (fail label)
diff --git a/test/illustration/Moonlight/Planar/MotifSpec.hs b/test/illustration/Moonlight/Planar/MotifSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/MotifSpec.hs
@@ -0,0 +1,105 @@
+-- | Coherent placement laws through exact world-geometry observations, not
+-- structural equality of nested Picture placement syntax.
+module Moonlight.Planar.MotifSpec (tests) where
+
+import Data.Foldable (toList, traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Affine
+import Moonlight.Planar.Curve
+  ( ClosedTrail, Located, Path, Subpath (..), circle, path, transformPath )
+import Moonlight.Planar.Exact
+  ( ExactVector (..), UnitInterval, positiveOne, unitHalf )
+import Moonlight.Planar.Illustration
+  ( Picture, PictureAlgebra (..), Color (..), Paint (..), FillRule (..)
+  , LineCap (..), LineJoin (..), StrokeUnits (..), StrokeStyle (..)
+  , annotate, clip, fill, foldPicture, opacity, place, stroke )
+import Moonlight.Planar.Illustration.Motif
+
+data Port = Root | Tip
+  deriving stock (Eq, Show)
+
+-- Observations retain drawing order, annotations and opacity/clip scopes.
+-- Only affine placement syntax is interpreted away.
+data Observation
+  = Filled !FillRule !Paint !Path
+  | Stroked !StrokeStyle !Path
+  | Clipped !FillRule !Path ![Observation]
+  | Transparent !UnitInterval ![Observation]
+  | Annotated !String ![Observation]
+  deriving stock (Eq, Show)
+
+observe :: Picture String -> [Observation]
+observe picture = foldPicture algebra picture identityAffine2
+ where
+  algebra :: PictureAlgebra String (Affine2 -> [Observation])
+  algebra = PictureAlgebra
+    { paintSequence = \children frame -> concatMap ($ frame) (toList children)
+    , paintFill = \rule paint contours frame ->
+        [Filled rule paint (transformPath frame (contourPath contours))]
+    , paintStroke = \style curves frame -> [Stroked style (transformPath frame curves)]
+    , paintClip = \rule contours child frame ->
+        [Clipped rule (transformPath frame (contourPath contours)) (child frame)]
+    , paintPlace = \local child frame -> child (composeAffine2 frame local)
+    , paintOpacity = \amount child frame -> [Transparent amount (child frame)]
+    , paintAnnotation = \name child frame -> [Annotated name (child frame)]
+    }
+  contourPath :: NonEmpty (Located ClosedTrail) -> Path
+  contourPath = path . Seq.fromList . fmap ClosedSubpath . toList
+
+tests :: IO ()
+tests = do
+  source <- requireIso (affine2 (ExactVector 2 1) (ExactVector 1 3) (ExactVector 4 (-5)))
+  target <- requireIso (affine2 (ExactVector (-2) 1) (ExactVector 0 3) (ExactVector 12 8))
+  rebase <- requireIso (affine2 (ExactVector 1 2) (ExactVector 0 1) (ExactVector (-7) 2))
+  let child = specimen source
+      attached = attachMotif Root target child
+      moved = transformMotif rebase child
+      final = translationAffineIso2 (ExactVector 30 (-20))
+  assertMotif "identity action" child (transformMotif identityAffineIso2 child)
+  assertMotif "composition action"
+    (transformMotif (composeAffineIso2 target rebase) child)
+    (transformMotif target moved)
+  assertEqual "nonidentity source frame aligns exactly" target (motifPort attached Root)
+  assertMotif "self attachment" child (attachMotif Root source child)
+  assertMotif "attachment composition"
+    (attachMotif Root final child) (attachMotif Root final attached)
+  assertMotif "child rebasing leaves attachment unchanged"
+    attached (attachMotif Root target moved)
+  assertEqual "other port transported with source" (composeAffineIso2
+    (composeAffineIso2 target (inverseAffineIso2 source)) (motifPort child Tip))
+    (motifPort attached Tip)
+  assertEqual "attachment picture uses same pose"
+    (observe (place (affineIsoMap (composeAffineIso2 target (inverseAffineIso2 source)))
+      (motifPicture child))) (observe (motifPicture attached))
+  assertEqual "singular frame remains refused" Nothing
+    (affineIso2 (affine2 (ExactVector 1 2) (ExactVector 2 4) (ExactVector 0 0)))
+  putStrLn "motif: ok"
+
+specimen :: AffineIso2 -> Motif Port String
+specimen root = motif picture ports
+ where
+  contour = circle positiveOne
+  curves = path (Seq.singleton (ClosedSubpath contour))
+  picture = annotate "body" (fill NonZero (Solid (RGB 1 2 3)) (contour :| []))
+    <> opacity unitHalf (clip EvenOdd (contour :| [])
+      (stroke (StrokeStyle (Solid (RGB 4 5 6)) positiveOne OutputUnits RoundCap RoundJoin) curves))
+  ports :: Port -> AffineIso2
+  ports Root = root
+  ports Tip = translationAffineIso2 (ExactVector 9 7)
+
+assertMotif :: String -> Motif Port String -> Motif Port String -> IO ()
+assertMotif label expected actual = do
+  assertEqual (label <> " geometry") (observe (motifPicture expected)) (observe (motifPicture actual))
+  traverse_ (\port -> assertEqual (label <> " port " <> show port)
+    (motifPort expected port) (motifPort actual port)) [Root,Tip]
+
+assertEqual :: (Eq a, Show a) => String -> a -> a -> IO ()
+assertEqual label expected actual
+  | expected == actual = pure ()
+  | otherwise = fail (label <> ": expected " <> show expected <> ", got " <> show actual)
+
+requireIso :: Affine2 -> IO AffineIso2
+requireIso value = case affineIso2 value of
+  Nothing -> fail "nonsingular test frame refused"
+  Just frame -> pure frame
diff --git a/test/illustration/Moonlight/Planar/SpaceSwordSpec.hs b/test/illustration/Moonlight/Planar/SpaceSwordSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/illustration/Moonlight/Planar/SpaceSwordSpec.hs
@@ -0,0 +1,54 @@
+module Moonlight.Planar.SpaceSwordSpec (tests) where
+
+import Control.Monad (unless)
+import Data.Foldable (toList, traverse_)
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Sequence as Seq
+import Moonlight.Planar.Curve (Path, Subpath (..), path, transformPath)
+import Moonlight.Planar.Exhibit.SpaceSword
+import Moonlight.Planar.Illustration (Picture, PictureAlgebra (..), foldPicture)
+
+tests :: IO ()
+tests = do
+  let baseline = spaceSwordPicture defaultSpaceSwordControls
+      overcharged = spaceSwordPicture overchargedSpaceSwordControls
+      affected = [BladeAura, BladeShell, BladeCore, BladeRunes, IonWake]
+  traverse_ (checkPart baseline overcharged affected) [minBound .. maxBound]
+  check "semantic charge/sweep edit changes the sword"
+    (baseline /= overcharged)
+  putStrLn "space sword: complete typed composition and selective overcharge edit ok"
+
+checkPart
+  :: Picture SpaceSwordPart
+  -> Picture SpaceSwordPart
+  -> [SpaceSwordPart]
+  -> SpaceSwordPart
+  -> IO ()
+checkPart baseline edited affected part = do
+  let original = observedPaths part baseline
+      candidate = observedPaths part edited
+  check ("nonempty space-sword part: " <> spaceSwordPartName part) (not (null original))
+  check ("space-sword control dependency: " <> spaceSwordPartName part)
+    ((original /= candidate) == (part `elem` affected))
+
+-- A pure test projection of the existing Picture algebra. It preserves
+-- placement and annotation ownership; it is not another geometry evaluator.
+observedPaths :: SpaceSwordPart -> Picture SpaceSwordPart -> [Path]
+observedPaths selected picture = foldPicture algebra picture Nothing
+ where
+  algebra :: PictureAlgebra SpaceSwordPart (Maybe SpaceSwordPart -> [Path])
+  algebra = PictureAlgebra
+    { paintSequence = \children inherited -> concatMap ($ inherited) (toList children)
+    , paintFill = \_ _ contours inherited ->
+        retain inherited (path (Seq.fromList (ClosedSubpath <$> NonEmpty.toList contours)))
+    , paintStroke = \_ curve inherited -> retain inherited curve
+    , paintClip = \_ _ child -> child
+    , paintPlace = \frame child inherited -> transformPath frame <$> child inherited
+    , paintOpacity = \_ child -> child
+    , paintAnnotation = \part child _ -> child (Just part)
+    }
+  retain :: Maybe SpaceSwordPart -> Path -> [Path]
+  retain inherited curve = if inherited == Just selected then [curve] else []
+
+check :: String -> Bool -> IO ()
+check label condition = unless condition (fail label)
diff --git a/test/lifted-sign/Main.hs b/test/lifted-sign/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/lifted-sign/Main.hs
@@ -0,0 +1,42 @@
+module Main (main) where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad (unless)
+import Moonlight.Planar.LiftedSignCases
+
+main :: IO ()
+main = do
+  cases <- either (fail . show) (evaluate . force) liftedSignCases
+  let failures =
+        [ (family, index, candidateSign input, magnitudeSign input)
+        | (family, inputs) <- cases
+        , (index, input) <- zip ([0 ..] :: [Int]) (concatMap permutedInputs inputs)
+        , candidateSign input /= magnitudeSign input
+        ]
+      conventionFailures =
+        [ (expected, candidateSign input)
+        | (expected, input) <- conventionInputs
+        , candidateSign input /= expected || magnitudeSign input /= expected
+        ]
+      coplanarFailures =
+        [ index
+        | (ExactlyCoplanar, inputs) <- cases
+        , (index, input) <- zip ([0 ..] :: [Int]) inputs
+        , candidateSign input /= EQ
+        ]
+      nearCoplanarFailures =
+        [ index
+        | (NearlyCoplanar, inputs) <- cases
+        , (index, input) <- zip ([1 ..] :: [Int]) inputs
+        , candidateSign input /= if even index then GT else LT
+        ]
+  unless (null failures) (fail ("lifted-sign differential disagreement: " <> show (take 8 failures)))
+  unless (null conventionFailures) (fail ("upper-affine sign convention disagreement: " <> show conventionFailures))
+  unless (null coplanarFailures) (fail ("coplanar determinant is nonzero: " <> show coplanarFailures))
+  unless (null nearCoplanarFailures) (fail ("near-coplanar perturbation sign disagreement: " <> show nearCoplanarFailures))
+  putStrLn
+    ( "lifted-sign: "
+        <> show (sum (map (length . concatMap permutedInputs . snd) cases))
+        <> " exact differential permutations; convention, coplanarity and perturbation laws passed"
+    )
diff --git a/test/lifted-sign/Moonlight/Planar/LiftedSignCases.hs b/test/lifted-sign/Moonlight/Planar/LiftedSignCases.hs
new file mode 100644
--- /dev/null
+++ b/test/lifted-sign/Moonlight/Planar/LiftedSignCases.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- | Shared inputs for the exact sign law and its forced timing instrument.
+-- Construction, rational normalization, and distribution naming stay outside
+-- the predicate timings.
+module Moonlight.Planar.LiftedSignCases
+  ( LiftedSignFamily (..)
+  , LiftedSignInput
+  , liftedSignCases
+  , candidateSign
+  , magnitudeSign
+  , permutedInputs
+  , conventionInputs
+  ) where
+
+import Control.DeepSeq (NFData)
+import Data.Bits (shiftL)
+import Data.List (permutations)
+import GHC.Generics (Generic)
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactArithmeticError
+  , exactRational
+  , exactRationalFromDyadic
+  , exactSignum
+  )
+import Moonlight.Planar.Internal.PowerDiagram.Generator
+  ( ExactPowerGenerator (..)
+  , exactGeneratorLiftedSign
+  , exactGeneratorLiftedVolume
+  )
+
+data LiftedSignFamily
+  = IntegralCoordinates
+  | DyadicCoordinates
+  | OddDenominators
+  | DisparateExponents
+  | ExactlyCoplanar
+  | NearlyCoplanar
+  | LargeRationalWeights
+  | PowerSiteGenerators
+  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
+  deriving anyclass (NFData)
+
+type LiftedSignInput =
+  ( ExactPowerGenerator Int
+  , ExactPowerGenerator Int
+  , ExactPowerGenerator Int
+  , ExactPowerGenerator Int
+  )
+
+liftedSignCases
+  :: Either ExactArithmeticError [(LiftedSignFamily, [LiftedSignInput])]
+liftedSignCases =
+  traverse
+    (\family -> (,) family <$> traverse (familyInput family) [1 .. 128])
+    [minBound .. maxBound]
+
+familyInput
+  :: LiftedSignFamily
+  -> Int
+  -> Either ExactArithmeticError LiftedSignInput
+familyInput family seed =
+  (,,,)
+    <$> familyGenerator family seed 0
+    <*> familyGenerator family seed 1
+    <*> familyGenerator family seed 2
+    <*> familyGenerator family seed 3
+
+familyGenerator
+  :: LiftedSignFamily
+  -> Int
+  -> Int
+  -> Either ExactArithmeticError (ExactPowerGenerator Int)
+familyGenerator family seed label =
+  let value axis = toInteger ((seed * (17 + axis * 11) + label * (31 + axis * 7)) `mod` 127 - 63)
+      dyadic axis = exactRationalFromDyadic (value axis) (negate (3 + (seed + axis * 17 + label * 11) `mod` 54))
+      oddCoordinate axis = exactRational (value axis) (toInteger (3 + 2 * ((seed + label * 7 + axis * 11) `mod` 61)))
+      construct = ExactPowerGenerator label
+   in case family of
+        IntegralCoordinates ->
+          pure (construct (fromInteger (value 0)) (fromInteger (value 1)) (fromInteger (value 2)))
+        DyadicCoordinates -> pure (construct (dyadic 0) (dyadic 1) (dyadic 2))
+        OddDenominators -> construct <$> oddCoordinate 0 <*> oddCoordinate 1 <*> oddCoordinate 2
+        DisparateExponents ->
+          let coordinateExponent :: Int -> Int
+              coordinateExponent axis = case (label + axis + seed) `mod` 4 of
+                0 -> -1074
+                1 -> -513
+                2 -> 17
+                _ -> 1023
+           in pure
+                ( construct
+                    (exactRationalFromDyadic (value 0) (coordinateExponent 0))
+                    (exactRationalFromDyadic (value 1) (coordinateExponent 1))
+                    (exactRationalFromDyadic (value 2) (coordinateExponent 2))
+                )
+        ExactlyCoplanar -> do
+          coordinateX <- oddCoordinate 0
+          coordinateY <- oddCoordinate 1
+          pure (construct coordinateX coordinateY (3 * coordinateX - 2 * coordinateY + 7))
+        NearlyCoplanar ->
+          let coordinateX = fromIntegral (label `mod` 2)
+              coordinateY = fromIntegral (label `quot` 2)
+              perturbation =
+                if label == 3
+                  then exactRationalFromDyadic (if even seed then 1 else -1) (negate (seed * 9))
+                  else 0
+           in pure (construct coordinateX coordinateY (3 * coordinateX - 2 * coordinateY + 7 + perturbation))
+        LargeRationalWeights -> do
+          weight <-
+            exactRational
+              (((1 :: Integer) `shiftL` (1024 + seed)) + value 2)
+              (((1 :: Integer) `shiftL` (127 + label * 13)) - 1)
+          pure (construct (dyadic 0) (dyadic 1) weight)
+        PowerSiteGenerators ->
+          let positionX = dyadic 0
+              positionY = dyadic 1
+              weight = dyadic 2
+           in pure (construct (2 * positionX) (2 * positionY) (weight - positionX * positionX - positionY * positionY))
+
+candidateSign :: LiftedSignInput -> Ordering
+candidateSign (firstGenerator, secondGenerator, thirdGenerator, fourthGenerator) =
+  exactGeneratorLiftedSign firstGenerator secondGenerator thirdGenerator fourthGenerator
+
+magnitudeSign :: LiftedSignInput -> Ordering
+magnitudeSign (firstGenerator, secondGenerator, thirdGenerator, fourthGenerator) =
+  exactSignum (exactGeneratorLiftedVolume firstGenerator secondGenerator thirdGenerator fourthGenerator)
+
+-- | All row permutations, without a partial list-to-tuple conversion.
+permutedInputs :: LiftedSignInput -> [LiftedSignInput]
+permutedInputs (firstGenerator, secondGenerator, thirdGenerator, fourthGenerator) =
+  [ (firstPermuted, secondPermuted, thirdPermuted, fourthPermuted)
+  | [firstPermuted, secondPermuted, thirdPermuted, fourthPermuted] <-
+      permutations [firstGenerator, secondGenerator, thirdGenerator, fourthGenerator]
+  ]
+
+-- | Leading homogeneous 1 gives a positive determinant for the unit
+-- tetrahedron, hence an above-plane query conflicts with a CCW slope face.
+conventionInputs :: [(Ordering, LiftedSignInput)]
+conventionInputs =
+  let origin = ExactPowerGenerator 0 0 0 0
+      onX = ExactPowerGenerator 1 1 0 0
+      onY = ExactPowerGenerator 2 0 1 0
+      above = ExactPowerGenerator 3 0 0 1
+      below = ExactPowerGenerator 3 0 0 (-1)
+   in [ (GT, (origin, onX, onY, above))
+      , (LT, (origin, onY, onX, above))
+      , (LT, (origin, onX, onY, below))
+      , (EQ, (origin, onX, onY, origin))
+      ]
diff --git a/test/native/Main.hs b/test/native/Main.hs
--- a/test/native/Main.hs
+++ b/test/native/Main.hs
@@ -1,13 +1,50 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.ExactEmbeddingSpec as ExactEmbeddingSpec
-import qualified Moonlight.Triangulation.NativeSpec as NativeSpec
-import qualified Moonlight.Triangulation.OverlaySpec as OverlaySpec
-import qualified Moonlight.Triangulation.RegionSpec as RegionSpec
+import qualified Moonlight.Planar.ConstructionSpec as ConstructionSpec
+import qualified Moonlight.Planar.ValidationSpec as ValidationSpec
+import qualified Moonlight.Planar.PredicateSpec as PredicateSpec
+import qualified Moonlight.Planar.NearestSpec as NearestSpec
+import qualified Moonlight.Planar.LocationSpec as LocationSpec
+import qualified Moonlight.Planar.EditSpec as EditSpec
+import qualified Moonlight.Planar.DcelSpec as DcelSpec
+import qualified Moonlight.Planar.PayloadSpec as PayloadSpec
+import qualified Moonlight.Planar.ConstraintSpec as ConstraintSpec
+import qualified Moonlight.Planar.ConstrainedJoinSpec as ConstrainedJoinSpec
+import qualified Moonlight.Planar.ConstrainedSeamSpec as ConstrainedSeamSpec
+import qualified Moonlight.Planar.RefinementSpec as RefinementSpec
+import qualified Moonlight.Planar.AlphaSpec as AlphaSpec
+import qualified Moonlight.Planar.IncidenceSpec as IncidenceSpec
+import qualified Moonlight.Planar.ExactEmbeddingSpec as ExactEmbeddingSpec
+import qualified Moonlight.Planar.ExactSegmentEventsSpec as ExactSegmentEventsSpec
+import qualified Moonlight.Planar.OverlaySpec as OverlaySpec
+import qualified Moonlight.Planar.OverlaySelectionSpec as OverlaySelectionSpec
+import qualified Moonlight.Planar.OverlayPublicationSpec as OverlayPublicationSpec
+import qualified Moonlight.Planar.RegionSpec as RegionSpec
+import Moonlight.Planar.FilteredPredicateOptimizationSpec (assertFilteredPredicatesSkipExactOracle)
 
 main :: IO ()
 main =
-  NativeSpec.tests
-    >> ExactEmbeddingSpec.tests
-    >> OverlaySpec.tests
-    >> RegionSpec.tests
+  sequence_
+    [ ConstructionSpec.tests
+    , ValidationSpec.tests
+    , PredicateSpec.tests
+    , NearestSpec.tests
+    , LocationSpec.tests
+    , EditSpec.tests
+    , DcelSpec.tests
+    , PayloadSpec.tests
+    , ConstraintSpec.tests
+    , ConstrainedJoinSpec.tests
+    , ConstrainedSeamSpec.tests
+    , RefinementSpec.tests
+    , AlphaSpec.tests
+    , assertFilteredPredicatesSkipExactOracle
+    , IncidenceSpec.tests
+    , ExactEmbeddingSpec.tests
+    , ExactSegmentEventsSpec.tests
+    , OverlaySpec.tests
+    , OverlaySelectionSpec.tests
+    , OverlayPublicationSpec.tests
+    , RegionSpec.tests
+    , putStrLn "all native core tests passed"
+    ]
diff --git a/test/native/Moonlight/Planar/AlphaSpec.hs b/test/native/Moonlight/Planar/AlphaSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/AlphaSpec.hs
@@ -0,0 +1,123 @@
+-- | Closed alpha thresholds and resident component-boundary laws.
+module Moonlight.Planar.AlphaSpec
+  ( tests
+  ) where
+
+import Data.Foldable ( traverse_ )
+import Moonlight.Planar.Alpha ( alphaShapeContainsFace )
+import Moonlight.Planar.BoundaryFixtures ( requireComponentBoundary, assertLoopWinding,
+  assertBoundaryShape )
+import Moonlight.Planar.Dcel ( outerFace )
+import Moonlight.Planar.FloodFillIterator ( RegionBoundary(regionBoundaryHoleLoops,
+  regionBoundaryOuterLoop) )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( innerFaces )
+import Moonlight.Planar.MeshFixtures ( pointMeshOf )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Scalar (mkRadiusSquared, NonFiniteValue(ValueNegativeInfinity,
+  ValueNaN, ValuePositiveInfinity), RadiusSquaredError(NonFiniteRadiusSquared,
+  NegativeRadiusSquared))
+import Support ( assertEqual, requireRight )
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testAlphaFaceFiltration
+    ]
+
+testAlphaFaceFiltration :: IO ()
+testAlphaFaceFiltration = do
+  triangle <-
+    pointMeshOf
+      "alpha right triangle"
+      [Point 0 0, Point 2 0, Point 0 2]
+  face <- case innerFaces triangle of
+    [singleFace] -> pure singleFace
+    faces -> fail ("alpha fixture faces: " <> show faces)
+  exactThreshold <- requireRight "exact alpha threshold" (mkRadiusSquared 2)
+  lowerThreshold <- requireRight "lower alpha threshold" (mkRadiusSquared 1.999)
+  zeroThreshold <- requireRight "zero alpha threshold" (mkRadiusSquared 0)
+  assertEqual "zero alpha threshold excludes face" False
+    (alphaShapeContainsFace zeroThreshold triangle face)
+  assertEqual "closed alpha threshold includes equality" True (alphaShapeContainsFace exactThreshold triangle face)
+  assertEqual "lower alpha threshold excludes face" False (alphaShapeContainsFace lowerThreshold triangle face)
+  assertEqual "outer face is absent from alpha filtration" False (alphaShapeContainsFace exactThreshold triangle outerFace)
+
+  let roundedPoints =
+        ( Point (-20) (-20)
+        , Point (-19) (-13)
+        , Point (-2) (-19)
+        )
+      (roundedFirst, roundedSecond, roundedThird) = roundedPoints
+      roundedRadiusSquared = 84.5
+  roundedTriangle <-
+    pointMeshOf
+      "rounded alpha equality"
+      [roundedFirst, roundedSecond, roundedThird]
+  roundedFace <- case innerFaces roundedTriangle of
+    [singleFace] -> pure singleFace
+    faces -> fail ("rounded alpha fixture faces: " <> show faces)
+  roundedThreshold <-
+    requireRight
+      "rounded exact alpha threshold"
+      (mkRadiusSquared roundedRadiusSquared)
+  roundedLowerThreshold <-
+    requireRight
+      "rounded lower alpha threshold"
+      (mkRadiusSquared (roundedRadiusSquared - encodeFloat 1 (-46)))
+  assertEqual
+    "closed alpha equality is exact"
+    True
+    (alphaShapeContainsFace roundedThreshold roundedTriangle roundedFace)
+  assertEqual
+    "one binary64 step below exact alpha equality is excluded"
+    False
+    (alphaShapeContainsFace roundedLowerThreshold roundedTriangle roundedFace)
+
+  alphaBoundary <-
+    requireComponentBoundary
+      "alpha"
+      True
+      triangle
+      (alphaShapeContainsFace exactThreshold triangle)
+  assertBoundaryShape "alpha component" triangle 3 [] alphaBoundary
+
+  annulus <-
+    pointMeshOf
+      "alpha annulus"
+      (ringPoints 4 32 <> ringPoints 2 16 <> ringPoints 3 24)
+  annulusThreshold <-
+    requireRight "alpha annulus threshold" (mkRadiusSquared 0.4)
+  annulusBoundary <-
+    requireComponentBoundary
+      "alpha annulus"
+      True
+      annulus
+      (alphaShapeContainsFace annulusThreshold annulus)
+  assertEqual
+    "alpha annulus hole count"
+    1
+    (length (regionBoundaryHoleLoops annulusBoundary))
+  assertLoopWinding
+    "alpha annulus outer winding"
+    GT
+    annulus
+    (regionBoundaryOuterLoop annulusBoundary)
+  traverse_
+    (assertLoopWinding "alpha annulus hole winding" LT annulus)
+    (regionBoundaryHoleLoops annulusBoundary)
+  traverse_
+    (\(label, value, failure) ->
+       assertEqual label (Left failure) (mkRadiusSquared value))
+    [ ("negative alpha threshold refusal", -1, NegativeRadiusSquared (-1))
+    , ("NaN alpha threshold refusal", 0 / 0, NonFiniteRadiusSquared ValueNaN)
+    , ("positive-infinite alpha threshold refusal", 1 / 0, NonFiniteRadiusSquared ValuePositiveInfinity)
+    , ("negative-infinite alpha threshold refusal", (-1) / 0, NonFiniteRadiusSquared ValueNegativeInfinity)
+    ]
+
+ringPoints :: Double -> Int -> [Point]
+ringPoints radius count =
+  [ Point (radius * cos angle) (radius * sin angle)
+  | index <- [0 .. count - 1]
+  , let angle = 2 * pi * fromIntegral index / fromIntegral count
+  ]
diff --git a/test/native/Moonlight/Planar/BoundaryFixtures.hs b/test/native/Moonlight/Planar/BoundaryFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/BoundaryFixtures.hs
@@ -0,0 +1,100 @@
+-- | Resident-boundary observations shared by region and alpha laws.
+module Moonlight.Planar.BoundaryFixtures
+  ( requireLabelledComponent
+  , requireComponentBoundary
+  , assertLoopWinding
+  , assertBoundaryShape
+  ) where
+
+import Data.Foldable ( traverse_ )
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Dcel ( vertexPoint )
+import Moonlight.Planar.FloodFillIterator ( componentBoundary, faceComponents,
+  BoundaryLoop(boundaryLoopVertices, boundaryLoopOrientation),
+  BoundaryOrientation(BoundaryClockwise, BoundaryCounterClockwise), FaceComponent,
+  RegionBoundary(..) )
+import Moonlight.Planar.Internal.HandleDefs ( FaceId )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (Triangulation)
+import Support ( assertEqual, requireRight )
+
+
+requireLabelledComponent
+  :: (Eq label, Show label)
+  => String
+  -> label
+  -> [(label, FaceComponent)]
+  -> IO FaceComponent
+requireLabelledComponent label expected components =
+  case [component | (actual, component) <- components, actual == expected] of
+    [component] -> pure component
+    matches ->
+      fail
+        ( label
+            <> ": expected one component for label "
+            <> show expected
+            <> ", got "
+            <> show (length matches)
+        )
+
+requireComponentBoundary
+  :: (Eq label, Show label)
+  => String
+  -> label
+  -> Triangulation mode vertex directed undirected face
+  -> (FaceId -> label)
+  -> IO RegionBoundary
+requireComponentBoundary label expected triangulation labelFace = do
+  component <-
+    requireLabelledComponent
+      (label <> " component")
+      expected
+      (faceComponents triangulation labelFace)
+  requireRight (label <> " boundary") (componentBoundary triangulation component)
+
+assertLoopWinding
+  :: String
+  -> Ordering
+  -> Triangulation mode vertex directed undirected face
+  -> BoundaryLoop
+  -> IO ()
+assertLoopWinding label expected triangulation loop =
+  let first :| remaining = fmap (vertexPoint triangulation) (boundaryLoopVertices loop)
+      points = first : remaining
+      twiceArea =
+        sum
+          ( zipWith
+              (\(Point ax ay) (Point bx by) -> ax * by - ay * bx)
+              points
+              (remaining <> [first])
+          )
+   in assertEqual label expected (compare twiceArea 0)
+
+assertBoundaryShape
+  :: String
+  -> Triangulation mode vertex directed undirected face
+  -> Int
+  -> [Int]
+  -> RegionBoundary
+  -> IO ()
+assertBoundaryShape label triangulation outerVertexCount holeVertexCounts boundary = do
+  assertEqual
+    (label <> " outer orientation")
+    BoundaryCounterClockwise
+    (boundaryLoopOrientation (regionBoundaryOuterLoop boundary))
+  assertEqual (label <> " outer vertex count") outerVertexCount
+    (length (boundaryLoopVertices (regionBoundaryOuterLoop boundary)))
+  assertLoopWinding (label <> " outer winding") GT triangulation
+    (regionBoundaryOuterLoop boundary)
+  let holes = regionBoundaryHoleLoops boundary
+  assertEqual (label <> " hole count") (length holeVertexCounts) (length holes)
+  traverse_
+    (\(vertexCount, hole) -> do
+       assertEqual
+         (label <> " hole orientation")
+         BoundaryClockwise
+         (boundaryLoopOrientation hole)
+       assertEqual (label <> " hole vertex count") vertexCount
+         (length (boundaryLoopVertices hole))
+       assertLoopWinding (label <> " hole winding") LT triangulation hole)
+    (zip holeVertexCounts holes)
diff --git a/test/native/Moonlight/Planar/ConstrainedJoinSpec.hs b/test/native/Moonlight/Planar/ConstrainedJoinSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ConstrainedJoinSpec.hs
@@ -0,0 +1,408 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Annotated constrained union and asymmetric extension laws.
+module Moonlight.Planar.ConstrainedJoinSpec
+  ( tests
+  ) where
+
+import Control.DeepSeq ( force )
+import Control.Exception ( evaluate )
+import Moonlight.Planar.Cdt ( constrainedDelaunay, constraintSegments, extendConstrainedWith,
+  unionConstrained, unionConstrainedWith, CanonicalSegment, CdtError(ConstraintIntersection),
+  ConstrainedExtensionResult(constrainedExtensionBuildStats, constrainedExtensionConstraintBatch),
+  ConstrainedUnionError(ConstraintUnionConstructionFailed),
+  ConstraintBatchResult(constraintBatchStats, constraintBatchOutcomes,
+  constraintBatchTriangulation), ConstraintBatchStats(constraintBatchRequests,
+  constraintBatchAccepted, constraintBatchRejected) )
+import Moonlight.Planar.Dcel ( mapVertices, vertexData, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( vertices )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (unitElementDefaults, ConstraintMode(Constrained), BuildResult(buildTriangulation), Triangulation)
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, ExistingPoints), buildStat)
+import Support ( assertEqual, assertValid, requireRight )
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testAnnotatedConstrainedUnion
+    , testAsymmetricConstrainedExtension
+    , testLargeAsymmetricConstrainedExtension
+    ]
+
+testAnnotatedConstrainedUnion :: IO ()
+testAnnotatedConstrainedUnion = do
+  let leftPoints :: V.Vector (Point)
+      leftPoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
+      rightPoints :: V.Vector (Point)
+      rightPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]
+      thirdPoints :: V.Vector (Point)
+      thirdPoints = V.fromList [Point 4 0, Point 6 0, Point 6 2, Point 4 2]
+  leftBuild <-
+    requireRight "annotated constrained union left" $
+      constrainedDelaunay
+        unitElementDefaults
+        leftPoints
+        (V.singleton (0, 2))
+  rightBuild <-
+    requireRight "annotated constrained union right" $
+      constrainedDelaunay
+        unitElementDefaults
+        rightPoints
+        (V.singleton (0, 2))
+  thirdBuild <-
+    requireRight "annotated constrained union third" $
+      constrainedDelaunay
+        unitElementDefaults
+        thirdPoints
+        (V.singleton (0, 2))
+  let left =
+        mapVertices
+          (const (Set.singleton "left"))
+          (buildTriangulation leftBuild)
+      right =
+        mapVertices
+          (const (Set.singleton "right"))
+          (buildTriangulation rightBuild)
+      third =
+        mapVertices
+          (const (Set.singleton "third"))
+          (buildTriangulation thirdBuild)
+  joined <-
+    requireRight
+      "annotated constrained union"
+      (unionConstrainedWith Set.union left right)
+  let expectedAnnotations =
+        Map.fromList
+          [ (Point 0 0, Set.singleton "left")
+          , (Point 0 2, Set.singleton "left")
+          , (Point 2 0, Set.fromList ["left", "right"])
+          , (Point 2 2, Set.fromList ["left", "right"])
+          , (Point 4 0, Set.singleton "right")
+          , (Point 4 2, Set.singleton "right")
+          ]
+      actualAnnotations =
+        Map.fromList
+          [ (vertexPoint joined vertex, vertexData joined vertex)
+          | vertex <- vertices joined
+          ]
+      expectedConstraints =
+        Set.union
+          (Set.fromList (V.toList (constraintSegments left)))
+          (Set.fromList (V.toList (constraintSegments right)))
+  assertEqual
+    "annotated constrained union preserves and combines site payloads"
+    expectedAnnotations
+    actualAnnotations
+  assertEqual
+    "annotated constrained union preserves both constraint sections"
+    expectedConstraints
+    (Set.fromList (V.toList (constraintSegments joined)))
+  assertValid "annotated constrained union" joined
+  commuted <-
+    requireRight
+      "annotated constrained union commuted"
+      (unionConstrainedWith Set.union right left)
+  assertEqual
+    "annotated constrained union is commutative under a commutative payload combiner"
+    joined
+    commuted
+  leftAssociated <-
+    requireRight
+      "annotated constrained union left-associated"
+      (unionConstrainedWith Set.union joined third)
+  rightPair <-
+    requireRight
+      "annotated constrained union right pair"
+      (unionConstrainedWith Set.union right third)
+  rightAssociated <-
+    requireRight
+      "annotated constrained union right-associated"
+      (unionConstrainedWith Set.union left rightPair)
+  assertEqual
+    "annotated constrained union is associative wherever both descents are admitted"
+    leftAssociated
+    rightAssociated
+  unitJoined <-
+    requireRight
+      "unit constrained union specialization"
+      ( unionConstrained
+          (mapVertices (const ()) left)
+          (mapVertices (const ()) right)
+      )
+  assertEqual
+    "unit constrained union specializes annotated constrained union"
+    unitJoined
+    (mapVertices (const ()) joined)
+
+-- | Sequential extension retains the base authority as the transaction root.
+-- The semantic result agrees with canonical union on sites and constraint
+-- sections, but its receipt proves that only the incoming constraint section
+-- was interpreted.
+testAsymmetricConstrainedExtension :: IO ()
+testAsymmetricConstrainedExtension = do
+  let basePoints :: V.Vector (Point)
+      basePoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
+      extensionPoints :: V.Vector (Point)
+      extensionPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]
+  baseBuild <-
+    requireRight
+      "asymmetric extension base"
+      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (0, 2)))
+  extensionBuild <-
+    requireRight
+      "asymmetric extension incoming"
+      (constrainedDelaunay unitElementDefaults extensionPoints (V.singleton (0, 2)))
+  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)
+      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)
+      expectedAnnotations =
+        Map.fromList
+          [ (Point 0 0, Set.singleton "base")
+          , (Point 0 2, Set.singleton "base")
+          , (Point 2 0, Set.fromList ["base", "extension"])
+          , (Point 2 2, Set.fromList ["base", "extension"])
+          , (Point 4 0, Set.singleton "extension")
+          , (Point 4 2, Set.singleton "extension")
+          ]
+      expectedConstraints =
+        Set.union
+          (Set.fromList (V.toList (constraintSegments base)))
+          (Set.fromList (V.toList (constraintSegments extension)))
+      baseAnnotations =
+        Map.fromList
+          [ (vertexPoint base vertex, vertexData base vertex)
+          | vertex <- vertices base
+          ]
+      extensionAnnotations =
+        Map.fromList
+          [ (vertexPoint extension vertex, vertexData extension vertex)
+          | vertex <- vertices extension
+          ]
+  baseAnnotationsBefore <- evaluate (force baseAnnotations)
+  baseConstraintsBefore <- evaluate (force (constraintSegments base))
+  extensionAnnotationsBefore <- evaluate (force extensionAnnotations)
+  extensionConstraintsBefore <- evaluate (force (constraintSegments extension))
+  extendedResult <-
+    requireRight
+      "asymmetric constrained extension"
+      (extendConstrainedWith Set.union base extension)
+  let constraintBatch = constrainedExtensionConstraintBatch extendedResult
+      extended = constraintBatchTriangulation constraintBatch
+      receipt = constraintBatchStats constraintBatch
+      buildReceipt = constrainedExtensionBuildStats extendedResult
+      actualAnnotations =
+        Map.fromList
+          [ (vertexPoint extended vertex, vertexData extended vertex)
+          | vertex <- vertices extended
+          ]
+  assertEqual
+    "asymmetric extension preserves base and combines coincident site payloads"
+    expectedAnnotations
+    actualAnnotations
+  assertEqual
+    "asymmetric extension preserves resident and incoming constraint sections"
+    expectedConstraints
+    (Set.fromList (V.toList (constraintSegments extended)))
+  assertEqual
+    "asymmetric extension does not mutate the frozen base predecessor sites"
+    baseAnnotationsBefore
+    ( Map.fromList
+        [ (vertexPoint base vertex, vertexData base vertex)
+        | vertex <- vertices base
+        ]
+    )
+  assertEqual
+    "asymmetric extension does not mutate the frozen base predecessor constraints"
+    baseConstraintsBefore
+    (constraintSegments base)
+  assertEqual
+    "asymmetric extension does not mutate the frozen incoming predecessor sites"
+    extensionAnnotationsBefore
+    ( Map.fromList
+        [ (vertexPoint extension vertex, vertexData extension vertex)
+        | vertex <- vertices extension
+        ]
+    )
+  assertEqual
+    "asymmetric extension does not mutate the frozen incoming predecessor constraints"
+    extensionConstraintsBefore
+    (constraintSegments extension)
+  assertEqual
+    "asymmetric extension recovers only incoming constraints"
+    (V.length (constraintSegments extension))
+    (constraintBatchRequests receipt)
+  assertEqual
+    "asymmetric extension returns one outcome for each incoming constraint"
+    (V.length (constraintSegments extension))
+    (V.length (constraintBatchOutcomes constraintBatch))
+  assertEqual
+    "asymmetric extension admits every incoming constraint"
+    (V.length (constraintSegments extension))
+    (constraintBatchAccepted receipt)
+  assertEqual
+    "asymmetric extension reports no incoming constraint rejection"
+    0
+    (constraintBatchRejected receipt)
+  assertEqual
+    "asymmetric extension charges every incoming site once"
+    (V.length extensionPoints)
+    ((buildStat InputPoints) buildReceipt)
+  assertEqual
+    "asymmetric extension distinguishes occupied incoming sites"
+    2
+    ((buildStat ExistingPoints) buildReceipt)
+  assertEqual
+    "asymmetric extension distinguishes newly materialized incoming sites"
+    2
+    ((buildStat UniquePoints) buildReceipt)
+  assertValid "asymmetric constrained extension" extended
+
+  crossingBuild <-
+    requireRight
+      "asymmetric extension crossing incoming section"
+      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (1, 3)))
+  case
+      extendConstrainedWith
+        Set.union
+        base
+        (mapVertices (const (Set.singleton "crossing")) (buildTriangulation crossingBuild)) of
+    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()
+    other -> fail ("asymmetric extension did not return the corridor intersection witness: " <> show other)
+
+testLargeAsymmetricConstrainedExtension :: IO ()
+testLargeAsymmetricConstrainedExtension = do
+  let width = 40 :: Int
+      height = 28 :: Int
+      basePoints :: V.Vector (Point)
+      basePoints =
+        V.fromList
+          ( [ Point 0 0
+            , Point (fromIntegral (width + 1)) 0
+            , Point (fromIntegral (width + 1)) (fromIntegral (height + 1))
+            , Point 0 (fromIntegral (height + 1))
+            ]
+              <> [ Point
+                     (fromIntegral (column + 1) + fromIntegral ((column * 17 + row * 31) `mod` 13) * 1.0e-3)
+                     (fromIntegral (row + 1) + fromIntegral ((column * 23 + row * 19) `mod` 17) * 1.0e-3)
+                 | row <- [0 .. height - 1]
+                 , column <- [0 .. width - 1]
+                 ]
+          )
+      baseConstraints = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
+      extensionPoints :: V.Vector (Point)
+      extensionPoints = V.fromList [Point 48 8, Point 53.2 8.4, Point 50.4 15.8]
+      extensionConstraints = V.fromList [(0, 1), (1, 2), (2, 0)]
+      conflictPoints :: V.Vector (Point)
+      conflictPoints = V.fromList [Point 8 22, Point 30 5, Point 26 7]
+  baseBuild <-
+    requireRight
+      "large asymmetric extension base"
+      (constrainedDelaunay unitElementDefaults basePoints baseConstraints)
+  extensionBuild <-
+    requireRight
+      "large asymmetric extension incoming"
+      (constrainedDelaunay unitElementDefaults extensionPoints extensionConstraints)
+  conflictBuild <-
+    requireRight
+      "large asymmetric extension conflicting incoming"
+      (constrainedDelaunay unitElementDefaults conflictPoints (V.singleton (0, 1)))
+  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)
+      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)
+      conflicting = mapVertices (const (Set.singleton "conflict")) (buildTriangulation conflictBuild)
+      predecessor
+        :: Triangulation 'Constrained (Set.Set String) () () ()
+        -> ( Triangulation 'Constrained (Set.Set String) () () ()
+           , Map.Map VertexId (Point, Set.Set String)
+           , Set.Set (CanonicalSegment)
+           )
+      predecessor triangulation =
+        ( triangulation
+        , Map.fromList
+            [ (vertex, (vertexPoint triangulation vertex, vertexData triangulation vertex))
+            | vertex <- vertices triangulation
+            ]
+        , Set.fromList (V.toList (constraintSegments triangulation))
+        )
+      assertPredecessor
+        :: String
+        -> ( Triangulation 'Constrained (Set.Set String) () () ()
+           , Map.Map VertexId (Point, Set.Set String)
+           , Set.Set (CanonicalSegment)
+           )
+        -> Triangulation 'Constrained (Set.Set String) () () ()
+        -> IO ()
+      assertPredecessor label snapshot triangulation =
+        assertEqual label snapshot (predecessor triangulation)
+  baseBefore <- evaluate (force (predecessor base))
+  extensionBefore <- evaluate (force (predecessor extension))
+  conflictingBefore <- evaluate (force (predecessor conflicting))
+  let (_, baseVertexSnapshotBefore, baseConstraintSnapshotBefore) = baseBefore
+      (_, _, extensionConstraintSnapshotBefore) = extensionBefore
+  extensionResult <-
+    requireRight
+      "large asymmetric constrained extension"
+      (extendConstrainedWith Set.union base extension)
+  let constraintBatch = constrainedExtensionConstraintBatch extensionResult
+      extended = constraintBatchTriangulation constraintBatch
+      extendedConstraints = Set.fromList (V.toList (constraintSegments extended))
+      extendedVertexSnapshot =
+        Map.fromList
+          [ (vertex, (vertexPoint extended vertex, vertexData extended vertex))
+          | vertex <- vertices extended
+          ]
+  assertEqual
+    "large asymmetric extension retains every base vertex handle, coordinate, and payload"
+    baseVertexSnapshotBefore
+    (Map.restrictKeys extendedVertexSnapshot (Map.keysSet baseVertexSnapshotBefore))
+  assertEqual
+    "large asymmetric extension retains its complete base constraint source section"
+    baseConstraintSnapshotBefore
+    (Set.intersection baseConstraintSnapshotBefore extendedConstraints)
+  assertEqual
+    "large asymmetric extension retains its complete incoming constraint source section"
+    extensionConstraintSnapshotBefore
+    (Set.intersection extensionConstraintSnapshotBefore extendedConstraints)
+  assertEqual
+    "large asymmetric extension preserves exactly both source constraint sections"
+    (Set.union baseConstraintSnapshotBefore extensionConstraintSnapshotBefore)
+    extendedConstraints
+  assertEqual
+    "large asymmetric extension leaves the frozen base predecessor physically unchanged"
+    baseBefore
+    (predecessor base)
+  assertPredecessor
+    "large asymmetric extension leaves the frozen incoming predecessor physically unchanged"
+    extensionBefore
+    extension
+  assertEqual
+    "large asymmetric extension charges only the incoming sites"
+    (V.length extensionPoints)
+    ((buildStat InputPoints) (constrainedExtensionBuildStats extensionResult))
+  assertEqual
+    "large asymmetric extension replays only the incoming constraints"
+    (V.length extensionConstraints)
+    (constraintBatchRequests (constraintBatchStats constraintBatch))
+  assertValid "large asymmetric constrained extension" extended
+  case extendConstrainedWith Set.union base conflicting of
+    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()
+    outcome ->
+      fail
+        ( "large asymmetric extension did not refuse the crossing incoming corridor: "
+            <> show outcome
+        )
+  assertEqual
+    "large asymmetric extension refusal leaves the frozen base predecessor physically unchanged"
+    baseBefore
+    (predecessor base)
+  assertPredecessor
+    "large asymmetric extension refusal leaves the conflicting predecessor physically unchanged"
+    conflictingBefore
+    conflicting
+
+-- | A separated constrained seam is a restriction-preserving operation, not
+-- the canonical site-set union wearing a cheaper costume. Both closed source
+-- sections survive face-for-face; only the corridor contributes new faces.
diff --git a/test/native/Moonlight/Planar/ConstrainedSeamSpec.hs b/test/native/Moonlight/Planar/ConstrainedSeamSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ConstrainedSeamSpec.hs
@@ -0,0 +1,595 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Separated seams, protected interfaces, and subsequent refinement laws.
+module Moonlight.Planar.ConstrainedSeamSpec
+  ( tests
+  ) where
+
+import Control.Monad ( forM_, unless, when )
+import Moonlight.Planar.Cdt ( constrainedDelaunay, boundedRegionFaces, constraintSegments,
+  joinSeparatedConstrained, CanonicalSegment(..), CdtError(CdtBuildError),
+  ConstrainedSeamFaceEvidence(constrainedSeamSourceFace, constrainedSeamTargetFace,
+  constrainedSeamFaceFirstPoint, constrainedSeamFaceSecondPoint, constrainedSeamFaceThirdPoint),
+  ConstrainedSeamResult(constrainedSeamRightFaceEvidence, constrainedSeamLeftFaceCount,
+  constrainedSeamLeftConstraintCount, constrainedSeamConstraintStats,
+  constrainedSeamCachedFrontierPointReads, constrainedSeamPublicationStats,
+  constrainedSeamJoinFaces, constrainedSeamResultTriangulation), ConstrainedSeamSide(SeamIncoming,
+  SeamResident), ConstrainedUnionError(ConstraintUnionConstructionFailed,
+  ConstraintUnionNotSeparated), ConstraintBatchStats(constraintBatchRequests) )
+import Moonlight.Planar.Dcel ( numConstraints, numInnerFaces, numVertices, vertexData, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( vertices, innerFaces )
+import Moonlight.Planar.Internal.Paged ( PublicationStats(..) )
+import Moonlight.Planar.Internal.Types ( RefinementParameters(..) )
+import Moonlight.Planar.Math ( squaredDistanceWide )
+import Moonlight.Planar.MeshFixtures ( faceKeyOf )
+import Moonlight.Planar.Refinement ( refineWithinDomain )
+import Moonlight.Planar.RefinementAssertions ( assertClosureCountsAgree )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (defaultRefinementParameters, unitElementDefaults, BuildError(SeamSourceEdgeRequiresFlip, RefinementSeamBridgeBudgetExceeded), ConstraintMode(Constrained), BuildResult(buildTriangulation), RefinementDomainResult(refinementDomainResult), RefinementResult(refinementComplete,
+  refinedTriangulation), Triangulation)
+import Support ( assertEqual, assertValid, requireRight )
+import Moonlight.Planar.Internal.Representation qualified as Internal
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testSeparatedConstrainedSeam
+    , testAxisSeparatedConstrainedSeams
+    , testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite
+    ]
+
+testSeparatedConstrainedSeam :: IO ()
+testSeparatedConstrainedSeam = do
+  let leftPoints :: V.Vector (Point)
+      leftPoints =
+        V.fromList
+          [ Point (-4) (-1)
+          , Point (-2) (-1)
+          , Point (-2) 1
+          , Point (-4) 1
+          ]
+      rightPoints :: V.Vector (Point)
+      rightPoints =
+        V.fromList
+          [ Point 2 (-1)
+          , Point 4 (-1)
+          , Point 4 1
+          , Point 2 1
+          ]
+      closedContour = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
+  leftBuild <-
+    requireRight
+      "separated constrained seam left"
+      (constrainedDelaunay unitElementDefaults leftPoints closedContour)
+  rightBuild <-
+    requireRight
+      "separated constrained seam right"
+      (constrainedDelaunay unitElementDefaults rightPoints closedContour)
+  let left =
+        Internal.geometryOnlyPublication
+          (buildTriangulation leftBuild)
+      right =
+        Internal.geometryOnlyPublication
+          (buildTriangulation rightBuild)
+      leftFaceKeys = Set.fromList (fmap (faceKeyOf left) (innerFaces left))
+      rightFaceKeys = Set.fromList (fmap (faceKeyOf right) (innerFaces right))
+      expectedConstraints =
+        Set.union
+          (Set.fromList (V.toList (constraintSegments left)))
+          (Set.fromList (V.toList (constraintSegments right)))
+  seam <-
+    requireRight
+      "source-preserving separated constrained seam"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left right)
+  let joined = constrainedSeamResultTriangulation seam
+      joinedFaceKeys = Set.fromList (fmap (faceKeyOf joined) (innerFaces joined))
+      actualAnnotations =
+        Map.fromList
+          [ (vertexPoint joined vertex, vertexData joined vertex)
+          | vertex <- vertices joined
+          ]
+      expectedAnnotations =
+        Map.union
+          ( Map.fromList
+              [ (vertexPoint left vertex, vertexData left vertex)
+              | vertex <- vertices left
+              ]
+          )
+          ( Map.fromList
+              [ (vertexPoint right vertex, vertexData right vertex)
+              | vertex <- vertices right
+              ]
+          )
+      actualConstraints = Set.fromList (V.toList (constraintSegments joined))
+      certifiedPerimeter =
+        fmap
+          (\segmentValue -> (segmentStart segmentValue, segmentEnd segmentValue))
+          (Set.toAscList (Set.difference actualConstraints expectedConstraints))
+  assertValid "source-preserving separated constrained seam" joined
+  assertEqual
+    "separated seam preserves all source annotations"
+    expectedAnnotations
+    actualAnnotations
+  assertEqual
+    "separated seam preserves both source constraint sections"
+    True
+    (expectedConstraints `Set.isSubsetOf` actualConstraints)
+  assertEqual
+    "separated seam certifies exactly its two synthetic perimeter bridges"
+    [ (Point (-2) (-1), Point 2 (-1))
+    , (Point (-2) 1, Point 2 1)
+    ]
+    certifiedPerimeter
+  unless (leftFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
+    fail "separated seam removed or retriangulated a left source face"
+  unless (rightFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
+    fail "separated seam removed or retriangulated a right source face"
+  assertEqual
+    "separated seam records the stable left face witness"
+    (numInnerFaces left)
+    (constrainedSeamLeftFaceCount seam)
+  assertEqual
+    "separated seam records the stable left constraint witness"
+    (numConstraints left)
+    (constrainedSeamLeftConstraintCount seam)
+  assertEqual
+    "separated seam has one right face witness per source face"
+    (numInnerFaces right)
+    (V.length (constrainedSeamRightFaceEvidence seam))
+  forM_ (V.toList (constrainedSeamRightFaceEvidence seam)) $ \evidence ->
+    let sourceFaceKey = faceKeyOf right (constrainedSeamSourceFace evidence)
+        targetFaceKey = faceKeyOf joined (constrainedSeamTargetFace evidence)
+        evidenceFaceKey =
+          [ constrainedSeamFaceFirstPoint evidence
+          , constrainedSeamFaceSecondPoint evidence
+          , constrainedSeamFaceThirdPoint evidence
+          ]
+     in do
+          assertEqual
+            "right face witness names the exact target triangle"
+            sourceFaceKey
+            targetFaceKey
+          assertEqual
+            "right face witness carries the exact target triangle"
+            targetFaceKey
+            evidenceFaceKey
+  when (V.null (constrainedSeamJoinFaces seam)) $
+    fail "separated seam did not identify any new corridor face"
+  assertEqual
+    "copied source constraints require no corridor recovery"
+    0
+    (constraintBatchRequests (constrainedSeamConstraintStats seam))
+  let seamPublication = constrainedSeamPublicationStats seam
+  assertEqual
+    "separated seam does not enumerate resident unboxed base pages"
+    0
+    (publicationUnboxedBasePageEnumerations seamPublication)
+  assertEqual
+    "separated seam does not freeze resident unboxed base pages"
+    0
+    (publicationUnboxedBasePageFreezes seamPublication)
+  assertEqual
+    "separated seam does not enumerate resident boxed base pages"
+    0
+    (publicationBoxedBasePageEnumerations seamPublication)
+  unless
+    ( publicationUnboxedDirtyBasePages seamPublication
+        + publicationUnboxedDirtyAppendedPages seamPublication
+        > 0
+    ) $
+    fail "separated seam publication did not record appended unboxed writes"
+  unless (constrainedSeamCachedFrontierPointReads seam > 0) $
+    fail "separated seam did not record cached frontier point reads"
+
+  let boundedBridgeParameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 6
+          , refineMaxEdgeLength = Just 1
+          }
+  boundedSeam <-
+    requireRight
+      "edge-bounded separated constrained seam"
+      ( joinSeparatedConstrained
+          (\_ _ -> True)
+          boundedBridgeParameters
+          left
+          right
+      )
+  let boundedJoined = constrainedSeamResultTriangulation boundedSeam
+      boundedSyntheticSegments =
+        Set.difference
+          (Set.fromList (V.toList (constraintSegments boundedJoined)))
+          expectedConstraints
+  assertValid "edge-bounded separated constrained seam" boundedJoined
+  assertEqual
+    "edge-bounded seam charges its exact bridge Steiner budget"
+    6
+    (numVertices boundedJoined - numVertices left - numVertices right)
+  assertEqual
+    "edge-bounded seam publishes both four-edge bridge paths"
+    8
+    (Set.size boundedSyntheticSegments)
+  unless (all ((<= 1) . canonicalSegmentSquaredLength) boundedSyntheticSegments) $
+    fail "edge-bounded seam published an oversized synthetic bridge child"
+  assertEqual
+    "edge-bounded seam cache agrees with its explicit frontier observation"
+    (Internal.prepareSeamFrontierIndex boundedJoined)
+    (Internal.triSeamFrontier boundedJoined)
+  case
+      joinSeparatedConstrained
+        (\_ _ -> True)
+        boundedBridgeParameters{refineMaxAdditionalVertices = Just 5}
+        left
+        right of
+    Left
+      ( ConstraintUnionConstructionFailed
+          (CdtBuildError (RefinementSeamBridgeBudgetExceeded 6 5))
+        ) -> pure ()
+    outcome ->
+      fail
+        ( "edge-bounded seam did not return its exact budget obstruction: "
+            <> either show (const "success") outcome
+        )
+
+  reversed <- requireRight "reversed source-preserving separated seam" (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters right left)
+  assertValid "reversed source-preserving separated seam" (constrainedSeamResultTriangulation reversed)
+
+  let localParameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 2
+          , refineMaxArea = Just 0.5
+          , refineKeepConstraintEdges = True
+          }
+      allJoinedFaces = Set.fromList (innerFaces joined)
+  locallyRefined <-
+    requireRight
+      "seam refinement preserves cached hull frontier"
+      (refineWithinDomain (const ()) localParameters allJoinedFaces joined)
+  assertClosureCountsAgree "seam refinement" allJoinedFaces locallyRefined
+  let refinedJoined = refinedTriangulation (refinementDomainResult locallyRefined)
+  unless (refinementComplete (refinementDomainResult locallyRefined)) $
+    fail "seam refinement fixture did not drain its local worklist"
+  assertEqual
+    "local refinement retains the exact cached hull frontier"
+    (Internal.triSeamFrontier joined)
+    (Internal.triSeamFrontier refinedJoined)
+
+  thirdBuild <-
+    requireRight
+      "repeated separated constrained seam source"
+      (constrainedDelaunay unitElementDefaults (V.fromList
+        [ Point 8 (-1)
+        , Point 10 (-1)
+        , Point 10 1
+        , Point 8 1
+        ]) closedContour)
+  let third =
+        Internal.geometryOnlyPublication
+          (buildTriangulation thirdBuild)
+  repeated <-
+    requireRight
+      "repeated separated seam after local refinement"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters refinedJoined third)
+  assertValid
+    "repeated separated seam after local refinement"
+    (constrainedSeamResultTriangulation repeated)
+  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left left of
+    Left ConstraintUnionNotSeparated -> pure ()
+    other -> fail ("non-separated constrained seam was not refused: " <> show other)
+
+-- North/south additions use the orientation-preserving Y chart rather than
+-- rotating or copying the resident mesh.  The sequence then alternates axes,
+-- so a stale one-axis frontier cache cannot accidentally make the third join
+-- succeed.
+testAxisSeparatedConstrainedSeams :: IO ()
+testAxisSeparatedConstrainedSeams = do
+  let closedContour :: Int -> V.Vector (Int, Int)
+      closedContour pointCount =
+        V.fromList
+          [ (index, (index + 1) `mod` pointCount)
+          | index <- [0 .. pointCount - 1]
+          ]
+      buildPublished
+        :: String
+        -> V.Vector Point
+        -> IO (Triangulation 'Constrained () () () ())
+      buildPublished label points = do
+        built <-
+          requireRight
+            label
+            (constrainedDelaunay
+               unitElementDefaults
+               points
+               (closedContour (V.length points)))
+        pure (Internal.geometryOnlyPublication (buildTriangulation built))
+      centralPoints =
+        V.fromList
+          [ Point (-1) (-1)
+          , Point 1 (-1)
+          , Point 1 1
+          , Point (-1) 1
+          ]
+      northPoints =
+        V.fromList
+          [ Point (-1) 3
+          , Point 1 3
+          , Point 1 5
+          , Point (-1) 5
+          ]
+      southPoints =
+        V.fromList
+          [ Point (-1) (-5)
+          , Point 1 (-5)
+          , Point 1 (-3)
+          , Point (-1) (-3)
+          ]
+      eastPoints =
+        V.fromList
+          [ Point 3 (-1)
+          , Point 5 (-1)
+          , Point 5 1
+          , Point 3 1
+          ]
+      overlapPoints =
+        V.fromList
+          [ Point 0 0
+          , Point 2 0
+          , Point 2 2
+          , Point 0 2
+          ]
+      irregularNorthPoints =
+        V.fromList
+          [ Point (-2) 7
+          , Point 0 7
+          , Point 2 7
+          , Point 2.5 9
+          , Point 0 11
+          , Point (-2.5) 9
+          ]
+  central <- buildPublished "axis seam central source" centralPoints
+  north <- buildPublished "axis seam north source" northPoints
+  south <- buildPublished "axis seam south source" southPoints
+  east <- buildPublished "axis seam east source" eastPoints
+  overlap <- buildPublished "axis seam overlapping source" overlapPoints
+
+  let northParameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 2
+          , refineMaxEdgeLength = Just 1
+          }
+  northJoined <-
+    requireRight
+      "edge-bounded north source-preserving separated seam"
+      (joinSeparatedConstrained (\_ _ -> True) northParameters central north)
+  let northTopology = constrainedSeamResultTriangulation northJoined
+      northPublication = constrainedSeamPublicationStats northJoined
+      northSyntheticSegments =
+        Set.difference
+          (Set.fromList (V.toList (constraintSegments northTopology)))
+          ( Set.union
+              (Set.fromList (V.toList (constraintSegments central)))
+              (Set.fromList (V.toList (constraintSegments north)))
+          )
+  assertValid
+    "north source-preserving separated seam"
+    northTopology
+  assertEqual
+    "north seam charges two bridge Steiner vertices"
+    2
+    (numVertices northTopology - numVertices central - numVertices north)
+  assertEqual
+    "north seam certifies two bounded two-edge bridge paths"
+    4
+    (Set.size northSyntheticSegments)
+  unless (all ((<= 1) . canonicalSegmentSquaredLength) northSyntheticSegments) $
+    fail "north seam published an oversized synthetic bridge child"
+  assertEqual
+    "north seam cache agrees with an explicit frontier observation"
+    (Internal.prepareSeamFrontierIndex northTopology)
+    (Internal.triSeamFrontier northTopology)
+  assertEqual
+    "north seam does not enumerate resident unboxed pages"
+    0
+    (publicationUnboxedBasePageEnumerations northPublication)
+  assertEqual
+    "north seam does not freeze resident unboxed pages"
+    0
+    (publicationUnboxedBasePageFreezes northPublication)
+  assertEqual
+    "north seam does not enumerate resident boxed pages"
+    0
+    (publicationBoxedBasePageEnumerations northPublication)
+  unless
+    ( publicationUnboxedDirtyBasePages northPublication
+        + publicationUnboxedDirtyAppendedPages northPublication
+        > 0
+    ) $
+    fail "north seam did not record appended unboxed writes"
+
+  reversed <-
+    requireRight
+      "reversed south/north source-preserving separated seam"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters south central)
+  assertValid
+    "reversed south/north source-preserving separated seam"
+    (constrainedSeamResultTriangulation reversed)
+
+  mixed <-
+    requireRight
+      "mixed-axis east extension after north seam"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
+         (constrainedSeamResultTriangulation northJoined)
+         east)
+  assertValid
+    "mixed-axis east extension after north seam"
+    (constrainedSeamResultTriangulation mixed)
+  assertEqual
+    "mixed-axis seam retains prior perimeter and certifies two new bridges"
+    ( numConstraints (constrainedSeamResultTriangulation northJoined)
+        + numConstraints east
+        + 2
+    )
+    (numConstraints (constrainedSeamResultTriangulation mixed))
+
+  repeated <-
+    requireRight
+      "repeated mixed-axis south extension"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
+         (constrainedSeamResultTriangulation mixed)
+         south)
+  assertValid
+    "repeated mixed-axis south extension"
+    (constrainedSeamResultTriangulation repeated)
+  assertEqual
+    "repeated mixed-axis seam cache agrees with an explicit frontier observation"
+    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation repeated))
+    (Internal.triSeamFrontier (constrainedSeamResultTriangulation repeated))
+
+  irregularNorth <-
+    buildPublished
+      "axis seam irregular north source"
+      irregularNorthPoints
+  irregularJoined <-
+    requireRight
+      "irregular collinear north extension"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
+         (constrainedSeamResultTriangulation repeated)
+         irregularNorth)
+  assertValid
+    "irregular collinear north extension"
+    (constrainedSeamResultTriangulation irregularJoined)
+  assertEqual
+    "irregular collinear north cache agrees with an explicit frontier observation"
+    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation irregularJoined))
+    (Internal.triSeamFrontier (constrainedSeamResultTriangulation irregularJoined))
+
+  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters central overlap of
+    Left ConstraintUnionNotSeparated -> pure ()
+    outcome ->
+      fail
+        ( "overlapping sources were not refused by either-axis admission: "
+            <> either show (const "success") outcome
+        )
+
+-- A separated source frontier may become an interior diagonal when the seam
+-- closes. If that diagonal is unconstrained and locally illegal, preserving
+-- the source and publishing a Delaunay join are incompatible obligations. The
+-- constrained twin is lawful: its contour explicitly authorizes that fixed
+-- diagonal to remain.
+testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite :: IO ()
+testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite = do
+  let leftPoints =
+        V.fromList
+          [ Point (-2) 0
+          , Point 0 0
+          , Point (-1) 10
+          , Point (-3) 5
+          ]
+      rightPoints =
+        V.fromList
+          [ Point 1 5
+          , Point 2 4
+          , Point 3 5
+          , Point 2 6
+          ]
+      closedQuadrilateral = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
+      buildSource
+        :: String
+        -> V.Vector (Int, Int)
+        -> V.Vector Point
+        -> IO (BuildResult 'Constrained Point () () ())
+      buildSource label constraints points =
+        requireRight label (constrainedDelaunay unitElementDefaults points constraints)
+      publish
+        :: BuildResult 'Constrained Point () () ()
+        -> Triangulation 'Constrained () () () ()
+      publish = Internal.geometryOnlyPublication . buildTriangulation
+  unconstrainedLeft <- publish <$> buildSource "incompatible seam left" V.empty leftPoints
+  unconstrainedRight <- publish <$> buildSource "incompatible seam right" V.empty rightPoints
+  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters unconstrainedLeft unconstrainedRight of
+    Left
+      ( ConstraintUnionConstructionFailed
+          (CdtBuildError (SeamSourceEdgeRequiresFlip _))
+        ) -> pure ()
+    outcome ->
+      fail
+        ( "separated seam did not refuse an unconstrained pinned source rewrite: "
+            <> either show (const "success") outcome
+        )
+  selectiveJoin <-
+    requireRight
+      "selective seam rewrites only unselected exterior faces"
+      (joinSeparatedConstrained (\_ _ -> False) defaultRefinementParameters unconstrainedLeft unconstrainedRight)
+  let selectivelyJoined = constrainedSeamResultTriangulation selectiveJoin
+      sourceFaceKeys =
+        Set.union
+          (Set.fromList (fmap (faceKeyOf unconstrainedLeft) (innerFaces unconstrainedLeft)))
+          (Set.fromList (fmap (faceKeyOf unconstrainedRight) (innerFaces unconstrainedRight)))
+      selectiveFaceKeys =
+        Set.fromList (fmap (faceKeyOf selectivelyJoined) (innerFaces selectivelyJoined))
+  assertValid "selective unconstrained seam" selectivelyJoined
+  when (sourceFaceKeys `Set.isSubsetOf` selectiveFaceKeys) $
+    fail "selective seam did not rewrite the incompatible exterior source face"
+  when (V.null (constrainedSeamJoinFaces selectiveJoin)) $
+    fail "selective seam did not publish the exact rewritten J component"
+  unless (V.null (constrainedSeamRightFaceEvidence selectiveJoin)) $
+    fail "selective seam fabricated transport evidence for unselected incoming faces"
+  constrainedLeft <-
+    publish
+      <$> buildSource
+        "compatible constrained seam left"
+        closedQuadrilateral
+        leftPoints
+  constrainedRight <-
+    publish
+      <$> buildSource
+        "compatible constrained seam right"
+        closedQuadrilateral
+        rightPoints
+  let protectedLeftFaces = Set.fromList (boundedRegionFaces constrainedLeft)
+      protectedRightFaces = Set.fromList (boundedRegionFaces constrainedRight)
+      preserveConstrainedSource side faceHandle =
+        case side of
+          SeamResident -> Set.member faceHandle protectedLeftFaces
+          SeamIncoming -> Set.member faceHandle protectedRightFaces
+      protectedFaceKeys =
+        Set.union
+          (Set.fromList (fmap (faceKeyOf constrainedLeft) (Set.toList protectedLeftFaces)))
+          (Set.fromList (fmap (faceKeyOf constrainedRight) (Set.toList protectedRightFaces)))
+  constrainedJoin <-
+    requireRight
+      "compatible selected constrained seam"
+      (joinSeparatedConstrained preserveConstrainedSource defaultRefinementParameters constrainedLeft constrainedRight)
+  let constrainedJoined = constrainedSeamResultTriangulation constrainedJoin
+      joinedFaceKeys =
+        Set.fromList (fmap (faceKeyOf constrainedJoined) (innerFaces constrainedJoined))
+      evidencedIncomingFaces =
+        Set.fromList
+          ( fmap
+              constrainedSeamSourceFace
+              (V.toList (constrainedSeamRightFaceEvidence constrainedJoin))
+          )
+  assertValid
+    "compatible selected constrained seam"
+    constrainedJoined
+  unless (protectedFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
+    fail "selected constrained source face did not survive exactly"
+  assertEqual
+    "selected seam evidences exactly the protected incoming faces"
+    protectedRightFaces
+    evidencedIncomingFaces
+
+-- An obtuse unconstrained source has a hull edge inside the opposite vertex's
+-- diametral circle.  That edge is queued by the refinement seed even though
+-- it is not a source constraint.  The checked local interpreter must refuse
+-- the encroachment split at its authoritative resolver, retain the cached
+-- frontier, and remain joinable afterwards.
+canonicalSegmentSquaredLength :: CanonicalSegment -> Double
+canonicalSegmentSquaredLength segmentValue =
+  squaredDistanceWide
+    (segmentStart segmentValue)
+    (segmentEnd segmentValue)
diff --git a/test/native/Moonlight/Planar/ConstraintSpec.hs b/test/native/Moonlight/Planar/ConstraintSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ConstraintSpec.hs
@@ -0,0 +1,415 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Constraint recovery, batch agreement, and refusal-witness laws.
+module Moonlight.Planar.ConstraintSpec
+  ( tests
+  ) where
+
+import Control.Monad ( foldM, forM_, unless, when )
+import Data.Primitive.PrimArray ( indexPrimArray, sizeofPrimArray )
+import Moonlight.Planar.Cdt ( ConstrainedDelaunayTriangulation, recoverConstraints,
+  constrainedDelaunayMaximal, fromDelaunay, canAddConstraint, constraintEdges,
+  getConflictingEdgesBetweenVertices, addConstraintEdge, addConstraintAndSplit,
+  addConstraintsAndSplit, CdtBuildResult(cdtRejectedConstraints, cdtAcceptedBuild),
+  CdtError(ConstraintIntersection, CdtBuildError, ConstraintBatchCardinalityMismatch),
+  ConstraintBatchResult(constraintBatchTriangulation, constraintBatchStats,
+  constraintBatchOutcomes), ConstraintBatchStats(constraintBatchRejected, constraintBatchRequests,
+  constraintBatchAccepted), ConstraintOutcome(..),
+  ConstraintRecoveryResult(constraintRecoveryTriangulation) )
+import Moonlight.Planar.Dcel ( isConstraintEdge, numConstraints, numVertices, undirectedEndpoints,
+  vertexPoint )
+import Moonlight.Planar.Internal.HandleDefs ( DirectedEdgeId, UndirectedEdgeId, VertexId(VertexId),
+  asUndirected )
+import Moonlight.Planar.MeshFixtures ( requirePointBuild )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (unitElementDefaults, BuildError(InvalidCoordinate), BuildResult(buildTriangulation, buildInputVertices))
+import Moonlight.Planar.Scalar (CoordinateError(CoordinateNaN))
+import Support ( assertEqual, assertValid, requireRight, randomPoints )
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testConstrainedDelaunay
+    , testSingletonRefusalWitness
+    ]
+
+testSingletonRefusalWitness :: IO ()
+testSingletonRefusalWitness = do
+  base <-
+    requirePointBuild
+      "refusal witness base"
+      [Point 0 0, Point 6 0, Point 2 (-2), Point 2 2, Point 4 (-3), Point 4 3]
+  cdt <-
+    foldM
+      (\resident (from, to) ->
+         constraintRecoveryTriangulation
+           <$> requireRight "refusal witness constraint" (addConstraintEdge resident from to))
+      (fromDelaunay (buildTriangulation base))
+      [(Point 2 (-2), Point 2 2), (Point 4 (-3), Point 4 3)]
+  assertEqual "refusal witness fixture holds two constraints" 2 (numConstraints cdt)
+  let constraintAt :: Double -> IO UndirectedEdgeId
+      constraintAt abscissa =
+        case [ edge
+             | edge <- constraintEdges cdt
+             , let (from, to) = undirectedEndpoints cdt edge
+                   Point fromX _ = vertexPoint cdt from
+                   Point toX _ = vertexPoint cdt to
+             , fromX == abscissa && toX == abscissa
+             ] of
+          [edge] -> pure edge
+          edges -> fail ("constraint at x = " <> show abscissa <> " is not unique: " <> show edges)
+      vertexAt :: Point -> IO VertexId
+      vertexAt point =
+        case [ vertex
+             | index <- [0 .. numVertices cdt - 1]
+             , let vertex = VertexId (fromIntegral index)
+             , vertexPoint cdt vertex == point
+             ] of
+          [vertex] -> pure vertex
+          matchingVertices -> fail ("vertex at " <> show point <> " is not unique: " <> show matchingVertices)
+      refusalWitness :: String -> Point -> Point -> IO UndirectedEdgeId
+      refusalWitness label from to =
+        case addConstraintEdge cdt from to of
+          Left (ConstraintIntersection edge) -> pure edge
+          Left other -> fail (label <> " was refused for another reason: " <> show other)
+          Right _ -> fail (label <> " was admitted across two resident constraints")
+  nearSource <- constraintAt 2
+  nearTarget <- constraintAt 4
+  when (nearSource == nearTarget) $
+    fail "the two constraints share an edge; the fixture proves nothing"
+  forward <- refusalWitness "source-to-target request" (Point 0 0) (Point 6 0)
+  backward <- refusalWitness "target-to-source request" (Point 6 0) (Point 0 0)
+  assertEqual
+    "the request from (0,0) witnesses the x = 2 constraint, first along its corridor"
+    nearSource
+    forward
+  assertEqual
+    "the request from (6,0) witnesses the x = 4 constraint, first along its corridor"
+    nearTarget
+    backward
+  source <- vertexAt (Point 0 0)
+  target <- vertexAt (Point 6 0)
+  assertEqual "canAddConstraint refuses the source-to-target request" False (canAddConstraint cdt source target)
+  assertEqual "canAddConstraint refuses the target-to-source request" False (canAddConstraint cdt target source)
+
+testConstrainedDelaunay :: IO ()
+testConstrainedDelaunay = do
+  base <- requirePointBuild "CDT base" [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 1 1, Point 3 3, Point 1 3, Point 3 1]
+  let cdt0 = fromDelaunay (buildTriangulation base)
+      invalidPoint = Point (0 / 0) 0
+  case addConstraintEdge cdt0 invalidPoint (Point 4 0) of
+    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
+    outcome -> fail ("invalid first constraint endpoint was admitted: " <> either show (const "success") outcome)
+  case addConstraintEdge cdt0 (Point 0 0) invalidPoint of
+    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
+    outcome -> fail ("invalid second constraint endpoint was admitted: " <> either show (const "success") outcome)
+  assertEqual
+    "invalid endpoint refusal leaves the immutable source unpublished"
+    (8, 0)
+    (numVertices cdt0, numConstraints cdt0)
+  partlyResident <-
+    requireRight
+      "one resident one new constraint endpoint"
+      (addConstraintEdge cdt0 (Point 0 0) (Point 2 2))
+  let partlyResidentCdt = constraintRecoveryTriangulation partlyResident
+  assertEqual
+    "one resident endpoint reserves only the missing site"
+    (numVertices cdt0 + 1)
+    (numVertices partlyResidentCdt)
+  assertValid "one resident one new constraint endpoint" partlyResidentCdt
+  diagonalBatch <-
+    requireRight
+      "constraint recovery"
+      (recoverConstraints cdt0 (V.singleton (VertexId 0, VertexId 2)))
+  (diagonalPath, diagonalAdded) <-
+    requireAcceptedConstraint "constraint recovery" diagonalBatch
+  let cdt1 = constraintBatchTriangulation diagonalBatch
+  when (V.null diagonalPath) $ fail "constraint recovery returned an empty path"
+  assertEqual "constraint count" diagonalAdded (numConstraints cdt1)
+  assertValid "constraint recovery" cdt1
+  conflictBatch <-
+    requireRight
+      "atomic conflict"
+      (recoverConstraints cdt1 (V.singleton (VertexId 1, VertexId 3)))
+  case V.toList (constraintBatchOutcomes conflictBatch) of
+    [ConstraintRejected _] -> pure ()
+    outcomes ->
+      fail
+        ( "crossing constraint was not rejected atomically: "
+            <> show outcomes
+        )
+  assertEqual
+    "crossing rejection preserves topology"
+    cdt1
+    (constraintBatchTriangulation conflictBatch)
+  let constraintProgram =
+        V.fromList
+          [ (VertexId 0, VertexId 2)
+          , (VertexId 1, VertexId 3)
+          , (VertexId 4, VertexId 6)
+          ]
+  wholeProgram <-
+    requireRight
+      "whole constraint program"
+      (recoverConstraints cdt0 constraintProgram)
+  (singletonTriangulation, singletonOutcomes) <-
+    requireRight
+      "singleton constraint program"
+      (V.foldM' replayConstraintRequest (cdt0, []) constraintProgram)
+  assertEqual
+    "batch outcomes equal singleton descent"
+    (V.toList (constraintBatchOutcomes wholeProgram))
+    (reverse singletonOutcomes)
+  assertEqual
+    "batch topology equals singleton descent"
+    singletonTriangulation
+    (constraintBatchTriangulation wholeProgram)
+  assertBatchStats "whole constraint program" wholeProgram
+  forM_ [0 .. 7 :: Int] $ \batchIndex -> do
+    randomizedBuild <-
+      requirePointBuild
+        ("random constraint batch " <> show batchIndex)
+        (randomPoints (0x6a09_e667_f3bc_c909 + fromIntegral batchIndex) 40)
+    let randomizedBase = fromDelaunay (buildTriangulation randomizedBuild)
+        mapping = buildInputVertices randomizedBuild
+        handles = V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping)
+        requests = V.take 18 (V.zip handles (V.reverse handles))
+    randomizedBatch <-
+      requireRight
+        ("random whole constraint batch " <> show batchIndex)
+        (recoverConstraints randomizedBase requests)
+    (randomizedSingleton, randomizedOutcomes) <-
+      requireRight
+        ("random singleton constraint batch " <> show batchIndex)
+        (V.foldM' replayConstraintRequest (randomizedBase, []) requests)
+    assertEqual
+      ("random batch outcomes equal singleton descent " <> show batchIndex)
+      (V.toList (constraintBatchOutcomes randomizedBatch))
+      (reverse randomizedOutcomes)
+    assertEqual
+      ("random batch topology equals singleton descent " <> show batchIndex)
+      randomizedSingleton
+      (constraintBatchTriangulation randomizedBatch)
+    assertBatchStats
+      ("random constraint batch " <> show batchIndex)
+      randomizedBatch
+    assertValid
+      ("random constraint batch " <> show batchIndex)
+      (constraintBatchTriangulation randomizedBatch)
+  -- Batch admission returns the first typed obstruction. Callers that need a
+  -- complete diagnosis ask the explicit corridor query and alone pay for it.
+  diagnosisBatch <-
+    requireRight
+      "conflict diagnosis"
+      (recoverConstraints cdt1 (V.singleton (VertexId 6, VertexId 7)))
+  case V.toList (constraintBatchOutcomes diagnosisBatch) of
+    [ConstraintRejected blocking] -> do
+      let diagnosed =
+            fmap
+              asUndirected
+              (getConflictingEdgesBetweenVertices cdt1 (VertexId 6) (VertexId 7))
+      case diagnosed of
+        [] -> fail "explicit conflict diagnosis named no edge"
+        firstBlocking : _ ->
+          assertEqual "batch rejection is the first corridor obstruction" firstBlocking blocking
+      assertEqual
+        "explicit conflict diagnosis is deduplicated"
+        (length diagnosed)
+        (length (Set.fromList diagnosed))
+      forM_ diagnosed $ \edge ->
+        unless (isConstraintEdge cdt1 edge) $
+          fail ("explicit conflict diagnosis named a non-constraint edge: " <> show edge)
+    outcomes ->
+      fail
+        ( "a constraint crossing the diagonal was not rejected: "
+            <> show outcomes
+        )
+
+  split <- requireRight "constraint split" (addConstraintAndSplit id cdt1 (VertexId 1) (VertexId 3))
+  let splitCdt = constraintRecoveryTriangulation split
+  unless (numVertices splitCdt > numVertices cdt1) $ fail "constraint split did not insert an intersection vertex"
+  assertValid "constraint split" splitCdt
+
+  -- The batch splitter must agree with singleton descent, including across a
+  -- suspension: every vertical below is constrained only inside the batch
+  -- itself, so no census against the base can reserve for its crossings and
+  -- the chunk's vertex reservation exhausts mid-batch. The driver publishes,
+  -- re-reserves against the published mesh, and resumes; the final mesh must
+  -- not know any of that happened.
+  let bandColumns = [0 .. 7 :: Int]
+      bandVertices =
+        V.fromList
+          ( Point 0 5
+              : Point 90 5
+              : concat
+                  [ [Point x 10, Point x 0]
+                  | column <- bandColumns
+                  , let x = 10 * fromIntegral column + 5
+                  ]
+          )
+  bandBuild <-
+    requireRight
+      "split band base"
+      (constrainedDelaunayMaximal unitElementDefaults bandVertices V.empty)
+  let bandAcceptedBuild = cdtAcceptedBuild bandBuild
+      bandMapping = buildInputVertices bandAcceptedBuild
+      bandHandle input = VertexId (indexPrimArray bandMapping input)
+      bandBase = buildTriangulation bandAcceptedBuild
+      bandRequests =
+        V.fromList
+          ( (bandHandle 0, bandHandle 1)
+              : [ (bandHandle (2 * column + 2), bandHandle (2 * column + 3))
+                | column <- bandColumns
+                ]
+          )
+      replaySplit
+        :: ConstrainedDelaunayTriangulation (Point)
+        -> (VertexId, VertexId)
+        -> Either (CdtError) (ConstrainedDelaunayTriangulation (Point))
+      replaySplit triangulation request =
+        constraintRecoveryTriangulation
+          <$> uncurry (addConstraintAndSplit id triangulation) request
+  bandBatch <-
+    requireRight
+      "split batch"
+      (addConstraintsAndSplit id bandBase bandRequests)
+  bandDescent <-
+    requireRight
+      "split singleton descent"
+      (V.foldM' replaySplit bandBase bandRequests)
+  assertEqual
+    "split batch topology equals singleton descent"
+    bandDescent
+    (constraintRecoveryTriangulation bandBatch)
+  assertEqual
+    "split batch added one vertex per crossing"
+    (numVertices bandBase + length bandColumns)
+    (numVertices (constraintRecoveryTriangulation bandBatch))
+  assertValid "split batch" (constraintRecoveryTriangulation bandBatch)
+
+  -- The same law with the reservation exhausting inside one corridor rather
+  -- than between corridors: the closing horizontal crosses two constraints
+  -- the census can see and fifteen it cannot, so the corridor suspends with
+  -- its cursor mid-walk and the resumed transaction continues from the last
+  -- split vertex, not from the corridor's start.
+  let laceColumns = [0 .. 16 :: Int]
+      laceVertices =
+        V.fromList
+          ( Point 0 5
+              : Point 90 5
+              : concat
+                  [ [Point x 10, Point x 0]
+                  | column <- laceColumns
+                  , let x = 5 * fromIntegral column + 5
+                  ]
+          )
+      laceBuiltIn = V.fromList [(2, 3), (34, 35)]
+  laceBuild <-
+    requireRight
+      "split lace base"
+      (constrainedDelaunayMaximal unitElementDefaults laceVertices laceBuiltIn)
+  let laceAcceptedBuild = cdtAcceptedBuild laceBuild
+      laceMapping = buildInputVertices laceAcceptedBuild
+      laceHandle input = VertexId (indexPrimArray laceMapping input)
+      laceBase = buildTriangulation laceAcceptedBuild
+      laceRequests =
+        V.fromList
+          ( [ (laceHandle (2 * column + 2), laceHandle (2 * column + 3))
+            | column <- [1 .. 15]
+            ]
+              <> [(laceHandle 0, laceHandle 1)]
+          )
+  laceBatch <-
+    requireRight
+      "split lace batch"
+      (addConstraintsAndSplit id laceBase laceRequests)
+  laceDescent <-
+    requireRight
+      "split lace singleton descent"
+      (V.foldM' replaySplit laceBase laceRequests)
+  assertEqual
+    "split lace batch topology equals singleton descent"
+    laceDescent
+    (constraintRecoveryTriangulation laceBatch)
+  assertEqual
+    "split lace batch added one vertex per crossing"
+    (numVertices laceBase + length laceColumns)
+    (numVertices (constraintRecoveryTriangulation laceBatch))
+  assertValid "split lace batch" (constraintRecoveryTriangulation laceBatch)
+
+  let verticesInput :: V.Vector (Point)
+      verticesInput = V.fromList [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 0 0]
+      constraintsInput = V.fromList [(0, 2), (1, 3), (4, 1)]
+  bulk <- requireRight "stable CDT bulk load" (constrainedDelaunayMaximal unitElementDefaults verticesInput constraintsInput)
+  let bulkAcceptedBuild = cdtAcceptedBuild bulk
+      bulkMapping = buildInputVertices bulkAcceptedBuild
+  unless (sizeofPrimArray bulkMapping > 4) $ fail "buildInputVertices out of bounds"
+  assertEqual "stable duplicate reroute" (VertexId (indexPrimArray bulkMapping 0)) (VertexId (indexPrimArray bulkMapping 4))
+  assertEqual "conflict reporting" 1 (V.length (cdtRejectedConstraints bulk))
+  assertValid "stable CDT bulk load" (buildTriangulation bulkAcceptedBuild)
+
+requireAcceptedConstraint
+  :: String
+  -> ConstraintBatchResult vertex directed undirected face
+  -> IO (V.Vector DirectedEdgeId, Int)
+requireAcceptedConstraint label batch =
+  case V.toList (constraintBatchOutcomes batch) of
+    [ConstraintAccepted path added] -> pure (path, added)
+    outcomes ->
+      fail
+        ( label
+            <> ": expected one accepted constraint, got "
+            <> show outcomes
+        )
+
+assertBatchStats
+  :: String
+  -> ConstraintBatchResult vertex directed undirected face
+  -> IO ()
+assertBatchStats label batch = do
+  let stats = constraintBatchStats batch
+      (accepted, rejected) =
+        V.foldl'
+          (\(!acceptedCount, !rejectedCount) outcome ->
+            case outcome of
+              ConstraintAccepted _ _ -> (acceptedCount + 1, rejectedCount)
+              ConstraintRejected _ -> (acceptedCount, rejectedCount + 1)
+          )
+          (0, 0)
+          (constraintBatchOutcomes batch)
+  assertEqual
+    (label <> " request count")
+    (V.length (constraintBatchOutcomes batch))
+    (constraintBatchRequests stats)
+  assertEqual
+    (label <> " accepted count")
+    accepted
+    (constraintBatchAccepted stats)
+  assertEqual
+    (label <> " rejected count")
+    rejected
+    (constraintBatchRejected stats)
+
+replayConstraintRequest
+  :: (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])
+  -> (VertexId, VertexId)
+  -> Either
+      (CdtError)
+      (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])
+replayConstraintRequest (current, outcomes) request = do
+  singleton <- recoverConstraints current (V.singleton request)
+  case V.toList (constraintBatchOutcomes singleton) of
+    [outcome] ->
+      Right
+        ( constraintBatchTriangulation singleton
+        , outcome : outcomes
+        )
+    cardinality ->
+      Left
+        ( ConstraintBatchCardinalityMismatch
+            1
+            (length cardinality)
+        )
diff --git a/test/native/Moonlight/Planar/ConstructionSpec.hs b/test/native/Moonlight/Planar/ConstructionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ConstructionSpec.hs
@@ -0,0 +1,294 @@
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Construction, degenerate cardinality, and admission laws.
+module Moonlight.Planar.ConstructionSpec
+  ( tests
+  ) where
+
+import Control.Monad ( forM_ )
+import Control.Monad.ST (runST)
+import qualified Moonlight.Planar.BuildStats as Build
+import qualified Moonlight.Planar.Internal.OperationState as Operation
+import Data.Foldable ( traverse_ )
+import Data.Primitive.PrimArray ( primArrayFromList, sizeofPrimArray )
+import GHC.Float ( castDoubleToWord64 )
+import Moonlight.Planar.BulkLoad ( clear, delaunay, delaunayGeometry, empty, insert, insertMany )
+import Moonlight.Planar.Dcel ( numDirectedEdges, numFaces, numInnerFaces, numUndirectedEdges,
+  numVertices, vertexPoint )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId) )
+import Moonlight.Planar.Internal.Types ( RefinementParameters(..) )
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.MeshFixtures ( requirePointBuild, pointKey, edgeKeys )
+import Moonlight.Planar.PointLocation ( locatePoint )
+import Moonlight.Planar.Refinement ( refine, withMinimumAngle )
+import Moonlight.Planar.Session ( withSession, insertVertex )
+import Moonlight.Planar.Point (Point(..), PointValidationError(InvalidPointX,
+  InvalidPointY))
+import Moonlight.Planar.Types (defaultRefinementParameters, unitElementDefaults, BuildError(RefinementMinimumAreaExceedsMaximum, InvalidCoordinate,
+  RefinementMinimumAngleNotFinite, RefinementMinimumAngleOutOfRange,
+  RefinementMinimumAngleDerivedRatioNotFinite, RefinementMaximumAdditionalVerticesNegative,
+  RefinementMinimumAreaNotFinite, RefinementMinimumAreaNegative, RefinementMaximumAreaNotFinite,
+  RefinementMaximumAreaNotPositive, RefinementMaximumRadiusEdgeRatioNotFinite,
+  RefinementMaximumRadiusEdgeRatioNotPositive, RefinementMaximumEdgeLengthNotFinite,
+  RefinementMaximumEdgeLengthNotPositive), Location(OnVertex, EmptyTriangulation), BuildResult(buildTriangulation, buildStats, buildInputVertices), DelaunayTriangulation, InsertionResult(insertionTriangulation))
+import Moonlight.Planar.BuildStats (BuildMetric (SpatialSeedPoints, LineExtensions), buildStat)
+import Moonlight.Planar.Scalar (CoordinateError(CoordinateNaN, CoordinateInfinite), NonFiniteValue(ValuePositiveInfinity, ValueNaN))
+import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, randomPoints )
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testBuildStatsSchema
+    , testConstructors
+    , testCircleSweepBulkLoad
+    , testDegenerateConstruction
+    , testRandomizedConstruction
+    , testErrors
+    ]
+
+testBuildStatsSchema :: IO ()
+testBuildStatsSchema = do
+  let metrics = [minBound .. maxBound] :: [Build.BuildMetric]
+      diagnostics = [minBound .. maxBound] :: [Operation.DiagnosticMetric]
+      expected :: [Int]
+      expected = map ((+ 1) . fromEnum) metrics
+      (observed, diagnosticValues, published) = runST $ do
+        operation <- Operation.newOperationState 1
+        traverse_ (\metric -> Operation.setCounter operation (Operation.Reported metric) (fromEnum metric + 1)) metrics
+        traverse_ (\metric -> Operation.setCounter operation (Operation.Diagnostic metric) (100 + fromEnum metric)) diagnostics
+        values <- traverse (Operation.readCounter operation . Operation.Reported) metrics
+        diagnosticSnapshot <- traverse (Operation.readCounter operation . Operation.Diagnostic) diagnostics
+        finalized <- Operation.finalizeBuildStats operation
+        pure (values, diagnosticSnapshot, finalized)
+      valuesOf :: Build.BuildStats -> [Int]
+      valuesOf stats = map (`Build.buildStat` stats) metrics
+  assertEqual "every reported metric owns one counter" expected observed
+  assertEqual "diagnostics own a distinct suffix" (map ((+ 100) . fromEnum) diagnostics) diagnosticValues
+  assertEqual "publication is exactly the reported section" expected (valuesOf published)
+  assertEqual "zero initialization derives from the metric universe" (map (const 0) metrics) (valuesOf Build.emptyBuildStats)
+  assertEqual "tabulation preserves metric identity" expected (valuesOf (Build.tabulateBuildStats ((+ 1) . fromEnum)))
+  assertEqual "indexed updates preserve unselected metrics"
+    (map (\metric -> if metric == minBound then 100 else fromEnum metric + 1) metrics)
+    (valuesOf (Build.updateBuildStats [(minBound, 100)] published))
+
+testConstructors :: IO ()
+testConstructors = do
+  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)
+  originQuery <- requireQueryPoint "empty location query" (Point 0 0)
+  assertValid "empty" vacant
+  assertEqual "empty vertex count" 0 (numVertices vacant)
+  assertEqual "empty directed edge count" 0 (numDirectedEdges vacant)
+  assertEqual "empty face count" 1 (numFaces vacant)
+  assertEqual "empty locates nowhere" EmptyTriangulation (locatePoint vacant originQuery)
+  built <- requirePointBuild "clear source" [Point 0 0, Point 1 0, Point 0 1]
+  assertEqual "clear returns the origin" vacant (clear (buildTriangulation built))
+
+testCircleSweepBulkLoad :: IO ()
+testCircleSweepBulkLoad = do
+  let points = V.fromList (randomPoints 0xc1ac1e 1500)
+  swept <- requireRight "circle-sweep build" (delaunay unitElementDefaults points)
+  (_, sessioned, _) <-
+    requireRight "session build" $
+      withSession (empty unitElementDefaults) (V.length points) $
+        V.mapM_ insertVertex points
+  assertEqual
+    "circle-sweep/session topology"
+    (edgeKeys sessioned)
+    (edgeKeys (buildTriangulation swept))
+  assertValid "circle-sweep build" (buildTriangulation swept)
+  assertValid "session build" sessioned
+
+testDegenerateConstruction :: IO ()
+testDegenerateConstruction = do
+  emptyBuild <- requirePointBuild "empty" []
+  emptyQuery <- requireQueryPoint "empty location" (Point 0 0)
+  assertEqual "empty vertices" 0 (numVertices (buildTriangulation emptyBuild))
+  assertEqual "empty location" EmptyTriangulation (locatePoint (buildTriangulation emptyBuild) emptyQuery)
+  assertValid "empty" (buildTriangulation emptyBuild)
+
+  singleton <- requirePointBuild "singleton" [Point 2 3]
+  singletonQuery <- requireQueryPoint "singleton lookup" (Point 2 3)
+  assertEqual "singleton lookup" (OnVertex (VertexId 0)) (locatePoint (buildTriangulation singleton) singletonQuery)
+  assertValid "singleton" (buildTriangulation singleton)
+
+  duplicates <- requirePointBuild "duplicates" [Point 0 0, Point 1 0, Point (-0.0) 0, Point 1 0, Point 2 0]
+  assertEqual "deduplicated count" 3 (numVertices (buildTriangulation duplicates))
+  assertEqual
+    "stable duplicate mapping"
+    (primArrayFromList [0, 1, 0, 1, 2])
+    (buildInputVertices duplicates)
+  assertValid "duplicates" (buildTriangulation duplicates)
+
+  -- Deduplicating a signed zero is not the same claim as storing it canonically:
+  -- the first only needs '==', which already identifies the two, while anything
+  -- reading the bit pattern — a radix ordering, a byte-for-byte cross-check —
+  -- reads the sign bit and gets a coordinate below every other. Both ingest
+  -- paths canonicalize, and both are held to it here rather than to the weaker
+  -- statement that comparison happens to survive.
+  let bits point = (castDoubleToWord64 (pointX point), castDoubleToWord64 (pointY point))
+  signedZero <- requirePointBuild "signed zero" [Point (-0.0) (-0.0), Point 1 0, Point 0 1]
+  assertEqual "bulk load stores a canonical zero"
+    (0, 0) (bits (vertexPoint (buildTriangulation signedZero) (VertexId 0)))
+  incremental <-
+    requireRight "signed zero insert"
+      (insert (empty unitElementDefaults :: DelaunayTriangulation (Point)) (Point (-0.0) (-0.0)))
+  assertEqual "insertion stores a canonical zero"
+    (0, 0) (bits (vertexPoint (insertionTriangulation incremental) (VertexId 0)))
+
+  traverse_
+    assertBulkLine
+    [ ("vertical", [Point 0 4, Point 0 0, Point 0 3, Point 0 2, Point 0 1])
+    , ("horizontal reversed", reverse [Point x 7 | x <- [0 .. 8]])
+    , ("oblique scrambled", [Point 3 8, Point (-2) (-2), Point 1 4, Point (-1) 0, Point 2 6, Point 0 2])
+    , ("duplicate-bearing", [Point 2 5, Point 0 1, Point 1 3, Point 2 5, Point (-0.0) 1, Point 3 7])
+    ]
+  areaBase <- requirePointBuild "resident area before collinear batch" [Point 0 0, Point 4 0, Point 0 4]
+  areaExtended <-
+    requireRight
+      "collinear batch into resident area"
+      (insertMany (buildTriangulation areaBase) (V.fromList [Point 1 1, Point 2 2, Point 3 3]))
+  assertEqual "resident area collinear batch count" 6 (numVertices (buildTriangulation areaExtended))
+  assertValid "resident area collinear batch" (buildTriangulation areaExtended)
+ where
+  assertBulkLine (label, points) = do
+    annotated <- requirePointBuild (label <> " annotated line") points
+    geometry <- requireRight (label <> " geometry line") (delaunayGeometry (V.fromList points))
+    batched <-
+      requireRight
+        (label <> " empty-base batch line")
+        (insertMany (empty unitElementDefaults) (V.fromList points))
+    let line = buildTriangulation annotated
+        batchLine = buildTriangulation batched
+        ascending = Set.toAscList (Set.fromList (map pointKey points))
+        expectedEdges = Set.fromList (zip ascending (drop 1 ascending))
+        uniqueCount = length ascending
+        stats = buildStats annotated
+    assertEqual (label <> " annotated edge set") expectedEdges (edgeKeys line)
+    assertEqual (label <> " geometry edge set") expectedEdges (edgeKeys geometry)
+    assertEqual (label <> " empty-base batch edge set") expectedEdges (edgeKeys batchLine)
+    assertEqual (label <> " edge count") (max 0 (uniqueCount - 1)) (numUndirectedEdges line)
+    assertEqual (label <> " face count") 0 (numInnerFaces line)
+    assertEqual (label <> " spatial seed count") uniqueCount ((buildStat SpatialSeedPoints) stats)
+    assertEqual (label <> " aggregate extension count") (max 0 (uniqueCount - 2)) ((buildStat LineExtensions) stats)
+    assertValid (label <> " annotated line") line
+    assertValid (label <> " geometry line") geometry
+    assertValid (label <> " empty-base batch line") batchLine
+
+testRandomizedConstruction :: IO ()
+testRandomizedConstruction = do
+  forM_ [0 .. 31] $ \index -> do
+    let points = randomPoints (0x9e37_79b9 + fromIntegral index) (40 + index * 7)
+    built <- requirePointBuild ("random build " <> show index) points
+    let triangulation = buildTriangulation built
+    assertValid ("random build " <> show index) triangulation
+    assertEqual "random input mapping" (length points) (sizeofPrimArray (buildInputVertices built))
+
+testErrors :: IO ()
+testErrors = do
+  assertEqual
+    "NaN query x refusal"
+    (Left (InvalidPointX CoordinateNaN))
+    (mkQueryPoint (Point (0 / 0) 0))
+  assertEqual
+    "infinite query y refusal"
+    (Left (InvalidPointY CoordinateInfinite))
+    (mkQueryPoint (Point 0 (1 / 0)))
+  assertEqual
+    "negative-infinite query x refusal"
+    (Left (InvalidPointX CoordinateInfinite))
+    (mkQueryPoint (Point ((-1) / 0) 0))
+  case delaunay unitElementDefaults (V.singleton (Point (0 / 0) 0 :: Point)) of
+    Left (InvalidCoordinate (Just 0) _ CoordinateNaN) -> pure ()
+    Left other -> fail ("NaN insertion result: " <> show other)
+    Right built ->
+      fail
+        ( "NaN insertion result: built a triangulation of "
+            <> show (numVertices (buildTriangulation built))
+            <> " vertices"
+        )
+  expectBuildFailure
+    "NaN minimum angle"
+    (RefinementMinimumAngleNotFinite ValueNaN)
+    (withMinimumAngle (0 / 0 :: Double) defaultRefinementParameters)
+  expectBuildFailure
+    "infinite minimum angle"
+    (RefinementMinimumAngleNotFinite ValuePositiveInfinity)
+    (withMinimumAngle (1 / 0 :: Double) defaultRefinementParameters)
+  expectBuildFailure
+    "out-of-range minimum angle"
+    (RefinementMinimumAngleOutOfRange 61)
+    (withMinimumAngle (61 :: Double) defaultRefinementParameters)
+  expectBuildFailure
+    "minimum angle whose derived ratio overflows"
+    (RefinementMinimumAngleDerivedRatioNotFinite ValuePositiveInfinity)
+    (withMinimumAngle (encodeFloat 1 (-1074) :: Double) defaultRefinementParameters)
+  square <- requirePointBuild "area validation square" [Point 0 0, Point 1 0, Point 1 1, Point 0 1]
+  let refineWith parameters =
+        refine id parameters (buildTriangulation square)
+      refineWithArea area =
+        refineWith defaultRefinementParameters{refineMaxArea = Just area}
+  expectBuildFailure
+    "negative refinement vertex budget"
+    (RefinementMaximumAdditionalVerticesNegative (-1))
+    (refineWith defaultRefinementParameters{refineMaxAdditionalVertices = Just (-1)})
+  expectBuildFailure
+    "infinite minimum area"
+    (RefinementMinimumAreaNotFinite ValuePositiveInfinity)
+    (refineWith defaultRefinementParameters{refineMinArea = Just (1 / 0)})
+  expectBuildFailure
+    "negative minimum area"
+    (RefinementMinimumAreaNegative (-1))
+    (refineWith defaultRefinementParameters{refineMinArea = Just (-1)})
+  expectBuildFailure
+    "NaN maximum area"
+    (RefinementMaximumAreaNotFinite ValueNaN)
+    (refineWithArea (0 / 0))
+  expectBuildFailure
+    "infinite maximum area"
+    (RefinementMaximumAreaNotFinite ValuePositiveInfinity)
+    (refineWithArea (1 / 0))
+  expectBuildFailure
+    "zero maximum area"
+    (RefinementMaximumAreaNotPositive 0)
+    (refineWithArea 0)
+  expectBuildFailure
+    "negative maximum area"
+    (RefinementMaximumAreaNotPositive (-1))
+    (refineWithArea (-1))
+  expectBuildFailure
+    "infinite maximum radius/edge ratio"
+    (RefinementMaximumRadiusEdgeRatioNotFinite ValuePositiveInfinity)
+    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just (1 / 0)})
+  expectBuildFailure
+    "non-positive maximum radius/edge ratio"
+    (RefinementMaximumRadiusEdgeRatioNotPositive 0)
+    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just 0})
+  expectBuildFailure
+    "infinite maximum edge length"
+    (RefinementMaximumEdgeLengthNotFinite ValuePositiveInfinity)
+    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just (1 / 0)})
+  expectBuildFailure
+    "non-positive maximum edge length"
+    (RefinementMaximumEdgeLengthNotPositive 0)
+    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just 0})
+  expectBuildFailure
+    "minimum area above maximum area"
+    (RefinementMinimumAreaExceedsMaximum 2 1)
+    ( refineWith
+        defaultRefinementParameters
+          { refineMinArea = Just 2
+          , refineMaxArea = Just 1
+          }
+    )
+  case refineWithArea 0.25 of
+    Right _ -> pure ()
+    Left failure -> fail ("positive maximum area rejected: " <> show failure)
+
+expectBuildFailure :: String -> BuildError -> Either BuildError value -> IO ()
+expectBuildFailure label expected outcome =
+  case outcome of
+    Left actual -> assertEqual label expected actual
+    Right _ -> fail (label <> ": expected " <> show expected <> ", got success")
diff --git a/test/native/Moonlight/Planar/DcelSpec.hs b/test/native/Moonlight/Planar/DcelSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/DcelSpec.hs
@@ -0,0 +1,282 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Resident handles, intersection traversal, interpolation, and Voronoi dual laws.
+module Moonlight.Planar.DcelSpec
+  ( tests
+  ) where
+
+import Control.Monad ( forM_, unless, when )
+import Control.Monad.ST ( stToIO )
+import Data.Maybe ( isJust )
+import Moonlight.Planar.Dcel ( geometryTopologyBytes, innerFaceDirectedEdgeTriples,
+  innerFaceVertexTriples, next, numDirectedEdges, numFaces, numInnerFaces, numUndirectedEdges,
+  numVertices, origin, previous, topologyIndexBytes, undirectedEndpoints, vertexPoint, vertexPoints
+  )
+import Moonlight.Planar.FloodFillIterator ( circleMetric, edgesInCircle, verticesInCircle,
+  verticesInRectangle, CircleMetricError(InvalidCircleRadius, InvalidCircleCenter) )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( directedEdges, undirectedEdges, vertices,
+  innerFaces )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId), asUndirected, directedPair,
+  reverseEdge )
+import Moonlight.Planar.Interpolation ( newNaturalNeighborWorkspace, workspaceBytes,
+  estimateGradients, foldNaturalNeighborWeights, interpolateNaturalNeighbor,
+  interpolateNaturalNeighborGradient, naturalNeighborWeights,
+  InterpolationStats(interpolationNaturalNeighbors), NaturalNeighborResult(naturalNeighborValues) )
+import Moonlight.Planar.IntersectionIterator ( lineIntersections, Intersection(..) )
+import Moonlight.Planar.Math ( segmentDistanceSquared )
+import Moonlight.Planar.MeshFixtures ( requirePointBuild )
+import Moonlight.Planar.Point (Point(Point), PointValidationError(InvalidPointX, InvalidPointY))
+import Moonlight.Planar.Scalar (CoordinateError(CoordinateInfinite, CoordinateNaN), NonFiniteValue(ValueNegativeInfinity, ValueNaN, ValuePositiveInfinity), RadiusSquaredError(NonFiniteRadiusSquared,
+  NegativeRadiusSquared))
+import Moonlight.Planar.Types (BuildResult(buildTriangulation))
+import Moonlight.Planar.Voronoi ( asDelaunayDirectedEdge, directedVoronoiEdges, reverseVoronoiEdge,
+  undirectedVoronoiEdges, voronoiEdgeGeometry, voronoiFaceSite, voronoiFaces, voronoiIncidentFace,
+  voronoiNext, voronoiPrevious )
+import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, requireJust )
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Handles.Dynamic qualified as Dynamic
+import Moonlight.Planar.Handles.Iterators.DynamicIterators qualified as DynamicIterators
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+import Moonlight.Planar.Voronoi.Handles qualified as VoronoiDynamic
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testHandlesAndFiniteDcel
+    , testSibsonInterpolation
+    , testVoronoiDual
+    , testTraversal
+    ]
+
+reverseTraversalEvent :: Intersection -> Intersection
+reverseTraversalEvent = \case
+  EdgeIntersection edge -> EdgeIntersection (reverseEdge edge)
+  EdgeOverlap edge -> EdgeOverlap (reverseEdge edge)
+  VertexIntersection vertex -> VertexIntersection vertex
+
+testHandlesAndFiniteDcel :: IO ()
+testHandlesAndFiniteDcel = do
+  built <- requirePointBuild "handle algebra" [Point 0 0, Point 3 0, Point 0 2, Point 0.4 0.7]
+  let triangulation = buildTriangulation built
+  assertValid "handle algebra" triangulation
+  assertEqual "one outer face" (numInnerFaces triangulation + 1) (numFaces triangulation)
+  forM_ (directedEdges triangulation) $ \edge -> do
+    assertEqual "double reversal" edge (reverseEdge (reverseEdge edge))
+    assertEqual "next/previous" edge (previous triangulation (next triangulation edge))
+    assertEqual "previous/next" edge (next triangulation (previous triangulation edge))
+  forM_ (undirectedEdges triangulation) $ \edge -> do
+    let (forward, backward) = directedPair edge
+    assertEqual "pair reversal" backward (reverseEdge forward)
+    assertEqual "undirected projection" edge (asUndirected forward)
+  assertEqual "vertex iterator" (numVertices triangulation) (length (vertices triangulation))
+  assertEqual
+    "dense vertex projection"
+    (V.fromList (fmap (vertexPoint triangulation) (vertices triangulation)))
+    (vertexPoints triangulation)
+  assertEqual "edge iterator" (numUndirectedEdges triangulation) (length (undirectedEdges triangulation))
+  assertEqual "face iterator" (numInnerFaces triangulation) (length (innerFaces triangulation))
+  assertEqual
+    "dense inner-face directed-edge projection"
+    (Just (V.toList (innerFaceDirectedEdgeTriples triangulation)))
+    (traverse (Dcel.innerFaceDirectedEdges triangulation) (innerFaces triangulation))
+  assertEqual
+    "dense inner-face projection"
+    (Just (V.toList (innerFaceVertexTriples triangulation)))
+    (traverse (Dcel.innerFaceVertices triangulation) (innerFaces triangulation))
+  assertEqual "dynamic vertex iterator" (numVertices triangulation) (length (DynamicIterators.vertexHandles triangulation))
+  assertEqual "dynamic edge iterator" (numDirectedEdges triangulation) (length (DynamicIterators.directedEdgeHandles triangulation))
+  assertEqual "dynamic face iterator" (numInnerFaces triangulation) (length (DynamicIterators.innerFaceHandles triangulation))
+  unless (geometryTopologyBytes triangulation > topologyIndexBytes triangulation) $
+    fail "geometry byte accounting omitted coordinates"
+  vertex0 <- requireJust "dynamic vertex handle" (Dynamic.vertexHandle triangulation (VertexId 0))
+  assertEqual "dynamic vertex fix" (VertexId 0) (Dynamic.fixVertex vertex0)
+  assertEqual "dynamic vertex position" (vertexPoint triangulation (VertexId 0)) (Dynamic.vertexHandlePosition vertex0)
+  case Dynamic.vertexHandleOutEdge vertex0 of
+    Nothing -> fail "connected dynamic vertex has no outgoing edge"
+    Just edge -> do
+      assertEqual "dynamic edge reversal" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgeReverse (Dynamic.directedEdgeReverse edge)))
+      assertEqual "dynamic edge next/previous" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgePrevious (Dynamic.directedEdgeNext edge)))
+      let face = Dynamic.directedEdgeFace edge
+      if Dynamic.faceIsOuter face
+        then pure ()
+        else case Dynamic.faceAsInner face of
+          Nothing -> fail "non-outer dynamic face did not refine to InnerTag"
+          Just inner -> do
+            _ <- requireJust "inner dynamic face vertices" (Dynamic.innerFaceVertices inner)
+            case Dynamic.innerFaceCircumcenter inner of
+              Nothing -> fail "inner dynamic face has no circumcenter"
+              Just _ -> pure ()
+
+testSibsonInterpolation :: IO ()
+testSibsonInterpolation = do
+  built <- requirePointBuild "sibson" (gridPoints 9 9)
+  let triangulation = buildTriangulation built
+      queryPoint = Point 3.25 4.4
+      linear vertex = let Point x y = vertexPoint triangulation vertex in 2 * x - 3 * y + 5
+      expected = let Point x y = queryPoint in 2 * x - 3 * y + 5
+  query <- requireQueryPoint "Sibson query" queryPoint
+  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
+  result <- stToIO (naturalNeighborWeights workspace Nothing query)
+  let weights = naturalNeighborValues result
+  unless (V.length weights >= 3) $ fail "Sibson query did not discover a natural-neighbor cavity"
+  assertNear "Sibson partition" 1.0e-11 1 (V.sum (V.map snd weights))
+  unless (V.all ((>= (-1.0e-12)) . snd) weights) $ fail "Sibson produced a negative weight"
+  (folded, _, foldedStats) <- stToIO (foldNaturalNeighborWeights
+    (\total vertex weight -> total + weight * linear vertex)
+    0
+    workspace
+    Nothing
+    query)
+  assertNear "allocation-free Sibson fold" 2.0e-9 expected folded
+  assertEqual "Sibson fold neighbor count" (V.length weights) (interpolationNaturalNeighbors foldedStats)
+  (interpolated, _) <- stToIO (interpolateNaturalNeighbor linear workspace Nothing query)
+  case interpolated of
+    Nothing -> fail "Sibson interpolation rejected an interior query"
+    Just value -> assertNear "Sibson affine precision" 2.0e-9 expected value
+  let gradients = estimateGradients linear triangulation
+  V.forM_ gradients $ \(gx, gy) -> do
+    assertNear "planar gradient x" 1.0e-9 2 gx
+    assertNear "planar gradient y" 1.0e-9 (-3) gy
+  let gradient (VertexId raw) = gradients V.! fromIntegral raw
+  (gradientValue, _) <- stToIO (interpolateNaturalNeighborGradient linear gradient 0.5 workspace Nothing query)
+  case gradientValue of
+    Nothing -> fail "gradient natural-neighbor interpolation rejected an interior query"
+    Just value -> assertNear "gradient affine precision" 2.0e-9 expected value
+  unless (workspaceBytes workspace > 0) $ fail "Sibson workspace byte accounting is empty"
+
+testVoronoiDual :: IO ()
+testVoronoiDual = do
+  built <- requirePointBuild "voronoi" [Point 0 0, Point 2 0, Point 0 2, Point 2 2, Point 1 1]
+  let triangulation = buildTriangulation built
+  assertEqual "Voronoi face count" (numVertices triangulation) (length (voronoiFaces triangulation))
+  assertEqual "directed dual edge count" (numDirectedEdges triangulation) (length (directedVoronoiEdges triangulation))
+  assertEqual "undirected dual edge count" (numUndirectedEdges triangulation) (length (undirectedVoronoiEdges triangulation))
+  forM_ (directedVoronoiEdges triangulation) $ \edge -> do
+    assertEqual "dual double reversal" edge (reverseVoronoiEdge (reverseVoronoiEdge edge))
+    assertEqual "dual next/previous" edge (voronoiPrevious triangulation (voronoiNext triangulation edge))
+    assertEqual
+      "dual face/site"
+      (origin triangulation (asDelaunayDirectedEdge edge))
+      (voronoiFaceSite (voronoiIncidentFace triangulation edge))
+    case voronoiEdgeGeometry triangulation edge of
+      Nothing -> fail "valid dual edge has no geometry"
+      fixedGeometry@(Just _) -> do
+        owning <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation edge)
+        assertEqual "fixed/owning dual geometry" fixedGeometry (VoronoiDynamic.voronoiEdgeGeometryH owning)
+  case directedVoronoiEdges triangulation of
+    [] -> fail "Voronoi test produced no directed dual edge"
+    first : _ -> do
+      handle <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation first)
+      assertEqual "dynamic dual fix" first (VoronoiDynamic.fixDirectedVoronoiEdge handle)
+      assertEqual "dynamic dual reversal" first (VoronoiDynamic.fixDirectedVoronoiEdge (VoronoiDynamic.voronoiEdgeReverseH (VoronoiDynamic.voronoiEdgeReverseH handle)))
+      assertEqual "dynamic dual/primal conversion" (asDelaunayDirectedEdge first) (Dynamic.fixDirectedEdge (VoronoiDynamic.voronoiEdgeAsDelaunayH handle))
+      let dualFace = VoronoiDynamic.voronoiEdgeFaceH handle
+      assertEqual "dynamic dual face site" (origin triangulation (asDelaunayDirectedEdge first)) (Dynamic.fixVertex (VoronoiDynamic.voronoiFaceSiteH dualFace))
+      let source = VoronoiDynamic.voronoiEdgeFromH handle
+      case VoronoiDynamic.voronoiVertexAsDelaunayFaceH source of
+        Just inner -> case VoronoiDynamic.voronoiVertexPositionH source of
+          Nothing -> fail "inner dynamic Voronoi vertex has no position"
+          Just voronoiPosition -> assertEqual "inner dynamic Voronoi position" (Dynamic.innerFaceCircumcenter inner) (Just voronoiPosition)
+        Nothing -> unless (isJust (VoronoiDynamic.voronoiVertexAsOuterEdgeH source)) $
+          fail "outer dynamic Voronoi vertex has no defining edge"
+
+testTraversal :: IO ()
+testTraversal = do
+  built <- requirePointBuild "traversal" [Point (-3) 0, Point (-1) (-2), Point (-1) 2, Point 1 (-2), Point 1 2, Point 3 0]
+  forwardStart <- requireQueryPoint "forward traversal start" (Point (-4) 0)
+  forwardEnd <- requireQueryPoint "forward traversal end" (Point 4 0)
+  let triangulation = buildTriangulation built
+      forward = lineIntersections triangulation forwardStart forwardEnd
+      backward = lineIntersections triangulation forwardEnd forwardStart
+  when (null forward) $ fail "ordered line traversal crossed nothing"
+  assertEqual
+    "reversing the segment reverses crossing order and orientation"
+    (map reverseTraversalEvent forward)
+    (reverse backward)
+  interiorEnd <- requireQueryPoint "outside traversal interior end" (Point 0.25 0.5)
+  let outsideToInterior = lineIntersections triangulation forwardStart interiorEnd
+      interiorToOutside = lineIntersections triangulation interiorEnd forwardStart
+  when (null outsideToInterior) $ fail "outside-to-interior traversal crossed nothing"
+  assertEqual
+    "endpoint-directed outside traversal preserves order and orientation"
+    (map reverseTraversalEvent outsideToInterior)
+    (reverse interiorToOutside)
+  missFrom <- requireQueryPoint "outside traversal miss from" (Point (-4) 4)
+  missTo <- requireQueryPoint "outside traversal miss to" (Point 4 4)
+  assertEqual
+    "outside segment missing the hull reports no intersections"
+    []
+    (lineIntersections triangulation missFrom missTo)
+  circleEdges <- Set.fromList <$> requireRight "circle edge query" (edgesInCircle triangulation (Point 0 0) 4)
+  let bruteCircle = Set.fromList
+        [ edge
+        | edge <- undirectedEdges triangulation
+        , let (a, b) = undirectedEndpoints triangulation edge
+        , segmentDistanceSquared (vertexPoint triangulation a) (vertexPoint triangulation b) (Point 0 0) <= 4
+        ]
+  assertEqual "circle edge flood" bruteCircle circleEdges
+  assertEqual
+    "negative circle metric refusal"
+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
+    (circleMetric (Point 0 0 :: Point) (-1))
+  assertEqual
+    "negative circle edge-query refusal"
+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
+    (edgesInCircle triangulation (Point 0 0) (-1))
+  assertEqual
+    "negative circle vertex-query refusal"
+    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
+    (verticesInCircle triangulation (Point 0 0) (-1))
+  assertEqual
+    "NaN circle metric refusal"
+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNaN)))
+    (circleMetric (Point 0 0 :: Point) (0 / 0))
+  assertEqual
+    "NaN circle center-x refusal"
+    (Left (InvalidCircleCenter (InvalidPointX CoordinateNaN)))
+    (circleMetric (Point (0 / 0) 0 :: Point) 1)
+  assertEqual
+    "positive-infinite circle center-y refusal"
+    (Left (InvalidCircleCenter (InvalidPointY CoordinateInfinite)))
+    (edgesInCircle triangulation (Point 0 (1 / 0)) 1)
+  assertEqual
+    "negative-infinite circle center-x refusal"
+    (Left (InvalidCircleCenter (InvalidPointX CoordinateInfinite)))
+    (verticesInCircle triangulation (Point ((-1) / 0) 0) 1)
+  assertEqual
+    "infinite circle edge-query refusal"
+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValuePositiveInfinity)))
+    (edgesInCircle triangulation (Point 0 0) (1 / 0))
+  assertEqual
+    "negative-infinite circle vertex-query refusal"
+    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNegativeInfinity)))
+    (verticesInCircle triangulation (Point 0 0) ((-1) / 0))
+  rectangleVertices <-
+    Set.fromList
+      <$> requireRight
+        "rectangle query"
+        (verticesInRectangle triangulation (Point (-1.1) (-2.1)) (Point 1.1 2.1))
+  let bruteVertices = Set.fromList
+        [ vertex
+        | vertex <- vertices triangulation
+        , let Point x y = vertexPoint triangulation vertex
+        , x >= (-1.1), x <= 1.1, y >= (-2.1), y <= 2.1
+        ]
+  assertEqual "rectangle vertex flood" bruteVertices rectangleVertices
+
+assertNear :: String -> Double -> Double -> Double -> IO ()
+assertNear label tolerance expected actual =
+  unless (abs (expected - actual) <= tolerance * max 1 (max (abs expected) (abs actual))) $
+    fail (label <> ": expected " <> show expected <> ", got " <> show actual)
+
+gridPoints :: Int -> Int -> [Point]
+gridPoints width height =
+  [ Point (fromIntegral x + jitter x y) (fromIntegral y + jitter y x)
+  | y <- [0 .. height - 1]
+  , x <- [0 .. width - 1]
+  ]
+ where
+  jitter :: Int -> Int -> Double
+  jitter a b = fromIntegral ((a * 17 + b * 31) `mod` 11) * 1.0e-5
diff --git a/test/native/Moonlight/Planar/EditSpec.hs b/test/native/Moonlight/Planar/EditSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/EditSpec.hs
@@ -0,0 +1,505 @@
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Edit sessions, point-index transitions, and persistent publication laws.
+module Moonlight.Planar.EditSpec
+  ( tests
+  ) where
+
+import Control.DeepSeq ( force )
+import Control.Exception ( evaluate )
+import Control.Monad ( unless, void )
+import Control.Monad.ST ( runST )
+import Data.Maybe ( isJust )
+import Data.Primitive.PrimArray ( indexPrimArray, sizeofPrimArray )
+import GHC.Stats ( allocated_bytes, getRTSStats, getRTSStatsEnabled )
+import Moonlight.Planar.BulkLoad ( delaunay, insert )
+import Moonlight.Planar.Dcel ( numVertices, undirectedEndpoints, vertexData, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( undirectedEdges )
+import Moonlight.Planar.HintGenerator ( buildHierarchyHint, defaultHierarchyBranchFactor,
+  rebuildHierarchyHint, removeManyWithHierarchy )
+import Moonlight.Planar.Internal.Canonical ( canonicalize )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId) )
+import Moonlight.Planar.Internal.PointIndex ( MutablePointIndexUpdate(..), lookupMutablePoint,
+  newMutablePointIndex, relocateMutablePoint, removeMutablePoint, seedMutablePointIndex )
+import Moonlight.Planar.Internal.Session ( excise )
+import Moonlight.Planar.MeshFixtures ( requirePointBuild, edgeKeys )
+import Moonlight.Planar.PayloadFixtures ( SampleVertex(..) )
+import Moonlight.Planar.Removal ( removeVertex, RemovalOutcome(removalOutcomeSwap,
+  removalOutcomePoint), RemovalResult(removalTriangulation, removalOutcome, removalStats),
+  locateAndRemove )
+import Moonlight.Planar.Session ( withSession, insertVertex, insertVertexAt, refuse, removeAt,
+  removeManyAt, Session )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (unitElementDefaults, BuildError(RemovalVertexOutOfRange), ElementDefaults(ElementDefaults), InsertionDisposition(AlreadyPresent), BuildResult(buildInputVertices, buildTriangulation), InsertionResult(insertionTriangulation,
+  insertionDisposition))
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, LocationWalkSteps), buildStat)
+import Support ( assertEqual, assertValid, requireRight, randomPoints, requireJust )
+import System.Mem ( performGC )
+import Moonlight.Planar.Internal.Representation qualified as Internal
+import qualified Data.Vector.Unboxed.Mutable as MUV
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testMixedEditSession
+    , testBulkRemovalAgreement
+    , testMutablePointIndexWraparoundBackshift
+    , testBatchIdentityIndexBatchToEmpty
+    , testBatchIdentityIndexToSingletonActive
+    , testBulkIdentityIndexDoesNotEscapeRemovalBatch
+    , testHierarchyRemovalAgreement
+    , testPersistentLocalUpdates
+    , testCanonicalWitness
+    , testRemoval
+    ]
+
+testMixedEditSession :: IO ()
+testMixedEditSession = do
+  let original = V.fromList (randomPoints 0x5eed1e 400)
+      doomed = V.take 120 original
+      survivors = V.drop 120 original
+      arrivals = V.fromList (randomPoints 0xa7717a1 90)
+  (_, edited, stats) <-
+    requireRight "mixed session" $
+      withSession
+        (buildTriangulation (either (error . show) id (delaunay unitElementDefaults original)))
+        (V.length arrivals)
+        ( do
+            V.mapM_ (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))) doomed
+            V.mapM_ insertVertex arrivals
+        )
+  fresh <- requireRight "fresh rebuild" (delaunay unitElementDefaults (survivors <> arrivals))
+  assertEqual
+    "mixed edit equals a fresh build of the surviving set"
+    (edgeKeys (buildTriangulation fresh))
+    (edgeKeys edited)
+  assertValid "mixed edit session" edited
+  assertEqual
+    "the whole transaction charged one counter set"
+    (V.length arrivals)
+    ((buildStat InputPoints) stats)
+
+-- The bulk removal verb must land exactly where the singleton fold lands, on
+-- both sides of its locate-strategy crossover: a small batch keeps the
+-- per-question walk, a large one buys the identity index once. Same removals,
+-- same order, same survivor either way.
+testBulkRemovalAgreement :: IO ()
+testBulkRemovalAgreement = do
+  let original = V.fromList (randomPoints 0xb01dca7 400)
+      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))
+      run label doomed = do
+        (outcomes, survived, _) <-
+          requireRight (label <> " bulk removal") (withSession base 0 (removeManyAt doomed))
+        V.imapM_
+          ( \index outcome ->
+              maybe (fail (label <> " bulk removal missed index " <> show index)) (const (pure ())) outcome
+          )
+          outcomes
+        (_, folded, _) <-
+          requireRight (label <> " singleton removal fold") $
+            withSession
+              base
+              0
+              ( V.mapM_
+                  (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))
+                  doomed
+              )
+        assertEqual
+          (label <> " bulk removal equals singleton descent")
+          (edgeKeys folded)
+          (edgeKeys survived)
+        assertValid (label <> " bulk removal") survived
+  run "walking" (V.take 40 original)
+  run "indexed" (V.take 120 original)
+
+-- The three fixture positions hash to home slot 15 in the 16-slot table made
+-- for three keys. They therefore occupy 15, 0, and 1 in insertion order. The
+-- middle deletion exercises both wraparound and backward shift, while the
+-- coordinate overwrite models the tail move performed before the table handle
+-- is renamed by 'swapRemoveVertex'.
+testMutablePointIndexWraparoundBackshift :: IO ()
+testMutablePointIndexWraparoundBackshift = do
+  (removed, first, movedBeforeRelocation, relocated, movedAfterRelocation) <-
+    requireRight "mutable point-index wraparound/backshift law" wraparoundLaw
+  case removed of
+    MutablePointIndexUpdated -> pure ()
+    MutablePointIndexInvalidated -> fail "mutable point-index middle delete invalidated a valid table"
+  assertEqual "mutable point-index first wraparound occupant" (Just 0) first
+  assertEqual "mutable point-index shifted tail before relocation" (Just 2) movedBeforeRelocation
+  case relocated of
+    MutablePointIndexUpdated -> pure ()
+    MutablePointIndexInvalidated -> fail "mutable point-index tail relocation invalidated a valid table"
+  assertEqual "mutable point-index relocated tail" (Just 1) movedAfterRelocation
+ where
+  wraparoundLaw = runST $ do
+    pointXs <- MUV.replicate 3 (0 :: Double)
+    pointYs <- MUV.replicate 3 (0 :: Double)
+    MUV.unsafeWrite pointXs 0 (-20)
+    MUV.unsafeWrite pointYs 0 (-15)
+    MUV.unsafeWrite pointXs 1 (-20)
+    MUV.unsafeWrite pointYs 1 0
+    MUV.unsafeWrite pointXs 2 (-20)
+    MUV.unsafeWrite pointYs 2 7
+    table <- newMutablePointIndex 3
+    seeded <- seedMutablePointIndex table 3 (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs)
+    case seeded of
+      Left failure -> pure (Left failure)
+      Right () -> do
+        -- The tail has moved into slot one before identity transport begins.
+        MUV.unsafeWrite pointXs 1 (-20)
+        MUV.unsafeWrite pointYs 1 7
+        removed <-
+          removeMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 0 1
+        first <-
+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) (-15)
+        movedBeforeRelocation <-
+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7
+        relocated <- relocateMutablePoint table (-20) 7 2 1
+        movedAfterRelocation <-
+          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7
+        pure
+          ( Right
+              ( removed
+              , first
+              , movedBeforeRelocation
+              , relocated
+              , movedAfterRelocation
+              )
+          )
+
+-- A dense table must also close lawfully when it removes every vertex. The
+-- point-keyed query after freeze forces the empty published derivation rather
+-- than retaining an impossible ST table.
+testBatchIdentityIndexBatchToEmpty :: IO ()
+testBatchIdentityIndexBatchToEmpty = do
+  let points = V.fromList (randomPoints 0x7a110bad 32)
+  built <- requireRight "batch identity empty base" (delaunay unitElementDefaults points)
+  (outcomes, emptied, _) <-
+    requireRight "batch identity removes every point" $
+      withSession (buildTriangulation built) 0 (removeManyAt points)
+  unless (V.all isJust outcomes) $
+    fail "batch identity table missed a point while removing to empty"
+  assertEqual "batch identity empty vertex count" 0 (numVertices emptied)
+  assertValid "batch identity empty result" emptied
+  absent <- requireRight "empty published identity lookup" (locateAndRemove emptied (Point 0 0))
+  case absent of
+    Nothing -> pure ()
+    Just _ -> fail "empty published identity lookup manufactured a removal"
+
+-- A dense batch discards its ST table before the next singleton handle rewrite.
+-- 'excise' must therefore activate and transport the ordinary persistent index
+-- without consulting the expired batch representation.
+testBatchIdentityIndexToSingletonActive :: IO ()
+testBatchIdentityIndexToSingletonActive = do
+  let original = V.fromList (randomPoints 0x51a91e 64)
+      doomed = V.take 32 original
+      survivors = V.drop 32 original
+  built <- requireRight "batch-to-singleton identity base" (delaunay unitElementDefaults original)
+  (singletonOutcome, transitioned, _) <-
+    requireRight "batch-to-singleton identity session" $
+      withSession (buildTriangulation built) 0 $ do
+        _ <- removeManyAt doomed
+        excise (VertexId 0)
+  let singletonPoint = removalOutcomePoint singletonOutcome
+      expected = V.filter (/= singletonPoint) survivors
+  fresh <- requireRight "batch-to-singleton fresh rebuild" (delaunay unitElementDefaults expected)
+  assertEqual
+    "batch-to-singleton identity topology"
+    (edgeKeys (buildTriangulation fresh))
+    (edgeKeys transitioned)
+  assertValid "batch-to-singleton identity result" transitioned
+  case V.find (/= singletonPoint) survivors of
+    Nothing -> fail "batch-to-singleton fixture exhausted every survivor"
+    Just remaining -> do
+      located <- requireRight "batch-to-singleton published lookup" (locateAndRemove transitioned remaining)
+      case located of
+        Nothing -> fail "batch-to-singleton published lookup missed a survivor"
+        Just removal -> assertValid "batch-to-singleton published removal" (removalTriangulation removal)
+
+-- The mutable identity table is an internal section of @removeManyAt@, never a
+-- session-wide owner. An insertion after the dense batch invalidates it, the
+-- subsequent removal walks correctly, and the frozen mesh must rederive the
+-- published identity cache from its final coordinate authority.
+testBulkIdentityIndexDoesNotEscapeRemovalBatch :: IO ()
+testBulkIdentityIndexDoesNotEscapeRemovalBatch = do
+  let original = V.fromList (randomPoints 0x5a11ce 400)
+      doomed = V.take 120 original
+      survivors = V.drop 120 original
+      arrival = Point (-0.25) 0.75
+  baseBuild <- requireRight "bulk identity section base" (delaunay unitElementDefaults original)
+  (_, edited, _) <-
+    requireRight "bulk identity section mixed session" $
+      withSession (buildTriangulation baseBuild) 1 $ do
+        _ <- removeManyAt doomed
+        _ <- insertVertexAt arrival arrival
+        removeAt arrival >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))
+  fresh <- requireRight "bulk identity section fresh survivor rebuild" (delaunay unitElementDefaults survivors)
+  assertEqual
+    "bulk identity section mixed program equals fresh survivors"
+    (edgeKeys (buildTriangulation fresh))
+    (edgeKeys edited)
+  assertValid "bulk identity section mixed session" edited
+  case V.uncons survivors of
+    Nothing -> fail "bulk identity section test has no survivor"
+    Just (survivor, _) -> do
+      located <- requireRight "published lazy identity lookup" (locateAndRemove edited survivor)
+      case located of
+        Nothing -> fail "published lazy identity lookup missed survivor"
+        Just removal -> assertValid "published lazy identity removal" (removalTriangulation removal)
+
+-- The hierarchy-hinted removal program must land exactly where the unhinted
+-- session lands. The guesses are all computed against the original base, so
+-- later removals in the batch answer for guesses whose slots swap-compaction
+-- has renamed — the walk must correct every one of them. The repaired
+-- hierarchy must equal the reference rebuild over the same survivor.
+testHierarchyRemovalAgreement :: IO ()
+testHierarchyRemovalAgreement = do
+  let original = V.fromList (randomPoints 0x5eed1e55 400)
+      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))
+  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint defaultHierarchyBranchFactor base)
+  let run label doomed = do
+        (outcomes, survived, repaired) <-
+          requireRight (label <> " hinted removal") (removeManyWithHierarchy hierarchy base doomed)
+        V.imapM_
+          ( \index outcome ->
+              maybe (fail (label <> " hinted removal missed index " <> show index)) (const (pure ())) outcome
+          )
+          outcomes
+        (_, folded, _) <-
+          requireRight (label <> " unhinted session") (withSession base 0 (removeManyAt doomed))
+        reference <- requireRight (label <> " reference rebuild") (rebuildHierarchyHint hierarchy folded)
+        assertEqual
+          (label <> " hinted removal equals unhinted session")
+          (edgeKeys folded)
+          (edgeKeys survived)
+        assertEqual (label <> " repaired hierarchy equals reference rebuild") reference repaired
+        assertValid (label <> " hinted removal") survived
+  run "sparse" (V.take 40 original)
+  run "dense" (V.take 120 original)
+
+testPersistentLocalUpdates :: IO ()
+testPersistentLocalUpdates = do
+  base <- requirePointBuild "persistent base" (randomPoints 0x5eed 4095)
+  let triangulation = buildTriangulation base
+      query = Point 0.000_123 (-0.000_271)
+  inserted <- requireRight "persistent insert" (insert triangulation query)
+  assertEqual "persistent source remains unchanged" 4095 (numVertices triangulation)
+  assertEqual "persistent result appends one vertex" 4096 (numVertices (insertionTriangulation inserted))
+  assertValid "persistent insertion result" (insertionTriangulation inserted)
+
+  let payloads = V.fromList
+        [ SampleVertex (Point 0 0) 10
+        , SampleVertex (Point 1 0) 20
+        , SampleVertex (Point 0 1) 30
+        ]
+      defaults = ElementDefaults (0 :: Int) False ("face" :: String)
+  payloadBuild <- requireRight "payload base" (delaunay defaults payloads)
+  duplicate <- requireRight "payload-only replacement" (insert (buildTriangulation payloadBuild) (SampleVertex (Point 1 0) 99))
+  assertEqual "payload update disposition" AlreadyPresent (insertionDisposition duplicate)
+  assertEqual "payload replacement" 99 (sampleLabel (vertexData (insertionTriangulation duplicate) (VertexId 1)))
+
+-- The canonical witness: absent from a built mesh, minted by canonical
+-- publication, honoured by a second publication as an identity that
+-- allocates nothing worth the name, and forgotten by the first edit.
+testCanonicalWitness :: IO ()
+testCanonicalWitness = do
+  enabled <- getRTSStatsEnabled
+  unless enabled $
+    fail "canonical witness allocation receipt requires +RTS -T"
+  base <- requirePointBuild "canonical witness base" (randomPoints 0xca70 4095)
+  let triangulation = buildTriangulation base
+  assertEqual
+    "a built mesh carries no canonical witness"
+    Internal.CanonicalUnknown
+    (Internal.triCanonical triangulation)
+  performGC
+  beforeFirst <- allocated_bytes <$> getRTSStats
+  canonical <- requireRight "canonical witness publication" (canonicalize triangulation)
+  _ <- evaluate (force canonical)
+  performGC
+  afterFirst <- allocated_bytes <$> getRTSStats
+  assertEqual
+    "canonical publication mints the witness"
+    Internal.CanonicalKnown
+    (Internal.triCanonical canonical)
+  performGC
+  beforeSecond <- allocated_bytes <$> getRTSStats
+  again <- requireRight "canonical witness fixed point" (canonicalize canonical)
+  _ <- evaluate again
+  performGC
+  afterSecond <- allocated_bytes <$> getRTSStats
+  let firstAllocated = afterFirst - beforeFirst
+      knownAllocated = afterSecond - beforeSecond
+  unless (again == canonical) $
+    fail "canonical publication of a canonical value changed it"
+  unless (firstAllocated >= 1_048_576) $
+    fail ("canonical publication of a built mesh allocated only " <> show firstAllocated <> " bytes")
+  unless (knownAllocated < 16_384) $
+    fail ("canonical publication of a canonical value allocated " <> show knownAllocated <> " bytes")
+  putStrLn
+    ( "canonical witness receipt: unknown-allocated-bytes="
+        <> show firstAllocated
+        <> " known-allocated-bytes="
+        <> show knownAllocated
+    )
+  inserted <- requireRight "canonical witness edit" (insert canonical (Point 0.000_123 (-0.000_271)))
+  assertEqual
+    "an edit forgets the witness"
+    Internal.CanonicalUnknown
+    (Internal.triCanonical (insertionTriangulation inserted))
+
+testRemoval :: IO ()
+testRemoval = do
+  built <- requirePointBuild "removal" [Point 0 0, Point 3 0, Point 3 3, Point 0 3, Point 1.5 1.5]
+  let triangulation = buildTriangulation built
+  assertEqual
+    "out-of-range removal obstruction"
+    (Left (RemovalVertexOutOfRange (VertexId 5) 5))
+    (void (removeVertex triangulation (VertexId 5)))
+  removedInterior <- requireRight "interior removal" (removeVertex triangulation (VertexId 4))
+  assertEqual "interior removed point" (Point 1.5 1.5) (removalOutcomePoint (removalOutcome removedInterior))
+  assertEqual "interior removal count" 4 (numVertices (removalTriangulation removedInterior))
+  assertValid "interior removal" (removalTriangulation removedInterior)
+  let beforeHullRemoval = removalTriangulation removedInterior
+      previousLastVertex = VertexId (fromIntegral (numVertices beforeHullRemoval - 1))
+      swappedPoint = vertexPoint beforeHullRemoval previousLastVertex
+      swappedData = vertexData beforeHullRemoval previousLastVertex
+  removedHull <- requireRight "hull removal" (removeVertex beforeHullRemoval (VertexId 0))
+  assertEqual "hull removal count" 3 (numVertices (removalTriangulation removedHull))
+  case removalOutcomeSwap (removalOutcome removedHull) of
+    Nothing -> fail "hull removal omitted the swap-compacted vertex handle"
+    Just (swappedIn, swappedInPoint) -> do
+      assertEqual "hull swapped-in handle" (VertexId 0) swappedIn
+      assertEqual "hull swapped-in point" swappedPoint (vertexPoint (removalTriangulation removedHull) swappedIn)
+      assertEqual "hull swapped-in payload" swappedData (vertexData (removalTriangulation removedHull) swappedIn)
+      -- The reported position must be the one the arena now holds, bit for
+      -- bit: a caller seeding a search from it is seeding from the mesh.
+      assertEqual "hull swapped-in reported position" swappedPoint swappedInPoint
+  assertValid "hull removal" (removalTriangulation removedHull)
+
+  let afterHullRemoval = removalTriangulation removedHull
+      lastVertex = VertexId (fromIntegral (numVertices afterHullRemoval - 1))
+  removedLast <- requireRight "last-vertex removal" (removeVertex afterHullRemoval lastVertex)
+  assertEqual
+    "removing the last vertex relocates nothing"
+    Nothing
+    (removalOutcomeSwap (removalOutcome removedLast))
+  assertEqual "last-vertex removal count" 2 (numVertices (removalTriangulation removedLast))
+  assertValid "last-vertex removal" (removalTriangulation removedLast)
+
+  lineBuild <- requirePointBuild "line removal" [Point 0 0, Point 1 0, Point 2 0, Point 3 0]
+  lineMiddle <- requireRight "line middle removal" (removeVertex (buildTriangulation lineBuild) (VertexId 1))
+  assertEqual "line middle count" 3 (numVertices (removalTriangulation lineMiddle))
+  assertValid "line middle removal" (removalTriangulation lineMiddle)
+
+  degreeThreeBuild <-
+    requirePointBuild
+      "degree-three removal"
+      [Point 0 0, Point 4 0, Point 0 4, Point 1 1]
+  let degreeThreeMapping = buildInputVertices degreeThreeBuild
+  case if 3 < sizeofPrimArray degreeThreeMapping
+        then Just (VertexId (indexPrimArray degreeThreeMapping 3))
+        else Nothing of
+    Nothing -> fail "degree-three removal input mapping omitted the interior vertex"
+    Just centerVertex -> do
+      degreeThree <-
+        requireRight
+          "degree-three interior removal"
+          (removeVertex (buildTriangulation degreeThreeBuild) centerVertex)
+      assertEqual "degree-three removal count" 3 (numVertices (removalTriangulation degreeThree))
+      assertValid "degree-three interior removal" (removalTriangulation degreeThree)
+
+  -- A removal hands its vertex's whole ring to edge/face cleanup, so a
+  -- high-degree star is the only shape that exercises the ordered-set path
+  -- there; an ordinary mesh keeps degrees near six and never leaves the
+  -- insertion sort. Radii are jittered so no four rim points are cocircular.
+  let rimCount = 48
+      rimPoint
+        :: Int
+        -> Point
+      rimPoint index =
+        let angle = 2 * pi * fromIntegral index / fromIntegral rimCount
+            radius = 1 + 0.001 * fromIntegral (index `mod` 7)
+         in Point (radius * cos angle) (radius * sin angle)
+  starBuild <-
+    requirePointBuild
+      "high-degree removal"
+      (Point 0 0 : map rimPoint [0 .. rimCount - 1])
+  let starTriangulation = buildTriangulation starBuild
+      starCentre = VertexId (indexPrimArray (buildInputVertices starBuild) 0)
+      centreDegree =
+        length
+          [ ()
+          | edge <- undirectedEdges starTriangulation
+          , let (from, to) = undirectedEndpoints starTriangulation edge
+          , from == starCentre || to == starCentre
+          ]
+  assertEqual "high-degree centre ring" rimCount centreDegree
+  starRemoved <-
+    requireRight "high-degree interior removal" (removeVertex starTriangulation starCentre)
+  assertEqual "high-degree removal count" rimCount (numVertices (removalTriangulation starRemoved))
+  assertEqual
+    "high-degree removed point"
+    (Point 0 0)
+    (removalOutcomePoint (removalOutcome starRemoved))
+  assertValid "high-degree interior removal" (removalTriangulation starRemoved)
+
+  -- A transaction that refuses publishes nothing: the refusal is the whole
+  -- answer, so no half-remeshed arena can reach a caller as a triangulation.
+  let sessionRefusal = RemovalVertexOutOfRange (VertexId 99) 5
+  assertEqual
+    "a refused session publishes nothing"
+    (Left sessionRefusal)
+    (void (withSession triangulation 1 (refuse sessionRefusal :: Session s (Point) () () () ())))
+
+  -- Refusal short-circuits: an edit after it never runs, so the mesh the
+  -- transaction abandoned is the mesh it was handed.
+  assertEqual
+    "a refusal abandons the edits behind it"
+    (Left sessionRefusal)
+    ( void
+        ( withSession
+            triangulation
+            1
+            ( ( do
+                  _ <- removeAt (Point 0 0)
+                  _ <- refuse sessionRefusal
+                  removeAt (Point 1 1)
+              ) ::
+                Session s (Point) () () () (Maybe (RemovalOutcome (Point)))
+            )
+        )
+    )
+
+  -- The two point-keyed entries must publish one story. A handle-keyed removal
+  -- locates nothing, while the coordinate-keyed route resolves the same site
+  -- through the exact derived identity section rather than a topological walk.
+  -- Both therefore charge no location steps and retire the same vertex.
+  handleRemoval <- requireRight "handle removal stats" (removeVertex triangulation (VertexId 4))
+  assertEqual
+    "a handle-keyed removal locates nothing"
+    0
+    ((buildStat LocationWalkSteps) (removalStats handleRemoval))
+  locatedRemoval <-
+    requireRight "point removal stats" (locateAndRemove triangulation (Point 1.5 1.5))
+  pointRemoval <- requireJust "point removal located a vertex" locatedRemoval
+  assertEqual
+    "a point-keyed removal performs no topological location walk"
+    0
+    ((buildStat LocationWalkSteps) (removalStats pointRemoval))
+  assertEqual
+    "point-keyed and handle-keyed removal publish the same mesh"
+    (edgeKeys (removalTriangulation handleRemoval))
+    (edgeKeys (removalTriangulation pointRemoval))
+
+-- | A refused singleton add witnesses the first resident constraint the
+-- corridor meets walking from the source vertex, not the lowest-numbered
+-- constraint the request crosses. Two vertical constraints cross the request
+-- y = 0 between (0,0) and (6,0): one at x = 2 from (2,-2) to (2,2) and one at
+-- x = 4 from (4,-3) to (4,3). Along the request from (0,0) the crossings sit
+-- at parameters 1/3 and 2/3, so the walk from (0,0) blocks on the x = 2
+-- constraint and the walk from (6,0) on the x = 4 constraint. A witness
+-- chosen by edge number would name the same edge in both directions, so one
+-- of the two directions refutes it whatever numbering the mesh assigned.
diff --git a/test/native/Moonlight/Planar/ExactEmbeddingSpec.hs b/test/native/Moonlight/Planar/ExactEmbeddingSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ExactEmbeddingSpec.hs
@@ -0,0 +1,829 @@
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Focused exact-geometry and local-embedding milestone acceptance.
+module Moonlight.Planar.ExactEmbeddingSpec (tests) where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad (unless)
+import Data.Bits (shiftR, xor)
+import Data.Foldable (traverse_)
+import Data.List (sort)
+import qualified Data.List as List
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import Data.Word (Word64)
+import GHC.Stats (allocated_bytes, getRTSStats, getRTSStatsEnabled)
+import Moonlight.Planar.Exact
+  ( ExactGeometryError (..)
+  , ExactIntersectionError (..)
+  , ExactPoint
+  , exactLineIntersection
+  , exactPoint
+  , exactPointCoordinates
+  , exactPointFromPoint
+  , exactPointToEmbeddingCandidate
+  , exactSegment
+  , exactSegmentRelation
+  )
+import Moonlight.Planar.Internal.ExactRational
+  ( ExactArithmeticError (..)
+  , ExactRational
+  , exactDivide
+  , exactRational
+  , exactRationalDenominator
+  , exactRationalFromDouble
+  , exactRationalNumerator
+  , exactSignum
+  )
+import Moonlight.Planar.Embedding
+  ( DraftIncidence (..)
+  , DraftId (..)
+  , DraftNeighborhood (..)
+  , DraftSegmentId
+  , DraftVertexId
+  , EmbeddingObligation (..)
+  , ExactArrangementDraft (..)
+  , certificateIncidenceRelationPreservationCount
+  , certificateNeighborhoodRotationPreservationCount
+  , certificateResidual
+  , certificateDistinctRoundedVertexCount
+  , certificateRoundedVertices
+  , certificateSplitOrderPreservationCount
+  , OverlayEmbeddingObstruction (..)
+  , certifyLocalEmbedding
+  , residualUndischargedObligations
+  )
+import Moonlight.Planar.Internal.Predicates (segmentRelation)
+import Moonlight.Planar.Math
+  ( SegmentRelation (..)
+  , allSegmentRelations
+  )
+import Moonlight.Planar.Scalar (CoordinateError (..))
+import Moonlight.Planar.Point (Point (..), PointValidationError (..), queryPointValue)
+import Support (assertEqual, integerPoint, requireRight)
+import System.Mem (performGC)
+
+-- | Run the exact geometry, embedding, and frozen binary64 acceptance actions.
+tests :: IO ()
+tests =
+  sequence_
+    [ testExactNonDyadicCrossing
+    , testAllSegmentRelationsAgree
+    , testSubUlpExactVertexCluster
+    , testContactDuplicateAndOverlapAgreement
+    , testVerticalNestedOverlapRefusal
+    , testCoordinateRefusalsAndRoundtrip
+    , testExactArithmeticReceipt
+    , testEmbeddingAdmissionReceipt
+    , testOrdinaryDraftCertificate
+    , testCollinearNeighborhoodRotation
+    , testFrozenBinary64RelationOracle
+    , testContactFamilyOracle
+    ]
+
+testExactNonDyadicCrossing :: IO ()
+testExactNonDyadicCrossing = do
+  firstSegment <-
+    requireRight
+      "first non-dyadic crossing segment"
+      (exactSegment (integerPoint 0 0) (integerPoint 1 1))
+  secondSegment <-
+    requireRight
+      "second non-dyadic crossing segment"
+      (exactSegment (integerPoint 0 1) (integerPoint 2 0))
+  crossing <-
+    requireRight
+      "non-dyadic exact line intersection"
+      (exactLineIntersection firstSegment secondSegment)
+  let (crossingX, crossingY) = exactPointCoordinates crossing
+  assertRatio "non-dyadic crossing x" 2 3 crossingX
+  assertRatio "non-dyadic crossing y" 2 3 crossingY
+  putStrLn
+    ( "exact non-dyadic crossing coordinates: "
+        <> show (exactRationalNumerator crossingX, exactRationalDenominator crossingX)
+        <> ", "
+        <> show (exactRationalNumerator crossingY, exactRationalDenominator crossingY)
+    )
+
+testAllSegmentRelationsAgree :: IO ()
+testAllSegmentRelationsAgree = do
+  observed <- traverse observeExactAndRounded handFixtures
+  traverse_ assertExpectedAndAgreement observed
+  assertEqual
+    "all exact segment relations covered"
+    allSegmentRelations
+    (sort (map observedExactRelation observed))
+
+testSubUlpExactVertexCluster :: IO ()
+testSubUlpExactVertexCluster = do
+  let base = 2 ^ (52 :: Int)
+  firstCoordinate <- requireRight "first sub-ulp coordinate" (exactRational (base * 4 + 1) 4)
+  secondCoordinate <- requireRight "second sub-ulp coordinate" (exactRational (base * 4 + 2) 4)
+  thirdCoordinate <- requireRight "third sub-ulp coordinate" (exactRational (base * 4 + 3) 4)
+  firstCrossing <- exactCrossingAt firstCoordinate
+  secondCrossing <- exactCrossingAt secondCoordinate
+  thirdCrossing <- exactCrossingAt thirdCoordinate
+  let draft =
+        emptyDraft
+          { draftVertices =
+              Map.fromList
+                [ (DraftId 0, firstCrossing)
+                , (DraftId 1, secondCrossing)
+                , (DraftId 2, thirdCrossing)
+                ]
+          }
+  assertLeftContains
+    "sub-ulp exact vertex cluster"
+    isRoundedCollision
+    (certifyLocalEmbedding draft)
+
+testContactDuplicateAndOverlapAgreement :: IO ()
+testContactDuplicateAndOverlapAgreement =
+  traverse_ assertContactAgreement contactFixtures
+
+testVerticalNestedOverlapRefusal :: IO ()
+testVerticalNestedOverlapRefusal = do
+  let base = 2 ^ (52 :: Int)
+      zero = 0
+      lower = fromInteger base
+      upper = fromInteger (base + 1)
+  innerLower <- requireRight "vertical inner lower" (exactRational (base * 3 + 1) 3)
+  innerUpper <- requireRight "vertical inner upper" (exactRational (base * 3 + 2) 3)
+  let outerFrom = exactPoint zero lower
+      outerTo = exactPoint zero upper
+      innerFrom = exactPoint zero innerLower
+      innerTo = exactPoint zero innerUpper
+      outerSegmentId, innerSegmentId :: DraftSegmentId
+      outerSegmentId = DraftId 0
+      innerSegmentId = DraftId 1
+      draft =
+        emptyDraft
+          { draftVertices =
+              Map.fromList
+                [ (DraftId 0, outerFrom)
+                , (DraftId 1, outerTo)
+                , (DraftId 2, innerFrom)
+                , (DraftId 3, innerTo)
+                ]
+          , draftSegments =
+              Map.fromList
+                [ (outerSegmentId, (DraftId 0, DraftId 1))
+                , (innerSegmentId, (DraftId 2, DraftId 3))
+                ]
+          , draftIncidences =
+              [ DraftIncidence
+                  outerSegmentId
+                  innerSegmentId
+                  SegmentsCollinearlyOverlap
+              ]
+          }
+  assertEqual
+    "vertical nested overlap remains exact"
+    SegmentsCollinearlyOverlap
+    (exactSegmentRelation outerFrom outerTo innerFrom innerTo)
+  assertLeftContains
+    "vertical nested overlap rounded incidence"
+    isIncidenceChange
+    (certifyLocalEmbedding draft)
+
+testCoordinateRefusalsAndRoundtrip :: IO ()
+testCoordinateRefusalsAndRoundtrip = do
+  assertEqual
+    "zero exact denominator refusal"
+    (Left ExactZeroDenominator)
+    (exactRational 1 0)
+  assertEqual
+    "zero exact divisor refusal"
+    (Left ExactZeroDivisor)
+    (exactDivide 1 0)
+  normalized <- requireRight "normalized exact rational" (exactRational (-2) (-4))
+  assertRatio "normalized exact rational" 1 2 normalized
+  canonicalZero <- requireRight "canonical exact zero" (exactRational 0 (-7))
+  assertRatio "canonical exact zero" 0 1 canonicalZero
+  assertEqual
+    "NaN exact rational refusal"
+    (Left ExactNaNInput)
+    (exactRationalFromDouble (0 / 0))
+  assertEqual
+    "infinite exact rational refusal"
+    (Left ExactInfiniteInput)
+    (exactRationalFromDouble (1 / 0))
+  assertEqual
+    "NaN exact point refusal"
+    (Left (InvalidPointX CoordinateNaN))
+    (exactPointFromPoint (Point (0 / 0) 0))
+  assertEqual
+    "infinite exact point refusal"
+    (Left (InvalidPointX CoordinateInfinite))
+    (exactPointFromPoint (Point (1 / 0) 0))
+  let unprojectablePoint =
+        exactPoint
+          (fromInteger (10 ^ (400 :: Int)))
+          0
+      unprojectableDraft =
+        emptyDraft
+          { draftVertices =
+              Map.singleton (DraftId 0) unprojectablePoint
+          }
+  assertLeftContains
+    "unprojectable exact draft vertex"
+    isProjectionRefusal
+    (certifyLocalEmbedding unprojectableDraft)
+  let finitePoint = Point 1.25 (-2.5)
+  exactFinite <- requireRight "finite point exact conversion" (exactPointFromPoint finitePoint)
+  projectedFinite <-
+    requireRight
+      "finite point candidate projection"
+      (exactPointToEmbeddingCandidate exactFinite)
+  assertEqual "finite exact point roundtrip" finitePoint (queryPointValue projectedFinite)
+  assertEqual
+    "coincident exact segment endpoints"
+    (Left (ExactSegmentEndpointsCoincide exactFinite))
+    (exactSegment exactFinite exactFinite)
+  duplicateSegment <-
+    requireRight
+      "duplicate line intersection segment"
+      (exactSegment (integerPoint 0 0) (integerPoint 2 0))
+  assertEqual
+    "duplicate line intersection refusal"
+    (Left (ExactIntersectionNonUnique SegmentsDuplicate))
+    (exactLineIntersection duplicateSegment duplicateSegment)
+
+testOrdinaryDraftCertificate :: IO ()
+testOrdinaryDraftCertificate = do
+  half <- requireRight "ordinary draft half parameter" (exactRational 1 2)
+  let west, center, east, south, north :: DraftVertexId
+      west = DraftId 0
+      center = DraftId 1
+      east = DraftId 2
+      south = DraftId 3
+      north = DraftId 4
+      westCenter, centerEast, southCenter, centerNorth :: DraftSegmentId
+      westCenter = DraftId 0
+      centerEast = DraftId 1
+      southCenter = DraftId 2
+      centerNorth = DraftId 3
+      draft =
+        ExactArrangementDraft
+          { draftVertices =
+              Map.fromList
+                [ (west, integerPoint (-1) 0)
+                , (center, integerPoint 0 0)
+                , (east, integerPoint 1 0)
+                , (south, integerPoint 0 (-1))
+                , (north, integerPoint 0 1)
+                ]
+          , draftSegments =
+              Map.fromList
+                [ (westCenter, (west, center))
+                , (centerEast, (center, east))
+                , (southCenter, (south, center))
+                , (centerNorth, (center, north))
+                ]
+          , draftSourceMemberships =
+              Map.fromList
+                [ ( DraftId 0
+                  , [ (0, west)
+                    , (half, center)
+                    , (1, east)
+                    ]
+                  )
+                , ( DraftId 1
+                  , [ (0, south)
+                    , (half, center)
+                    , (1, north)
+                    ]
+                  )
+                ]
+          , draftIncidences =
+              [ DraftIncidence westCenter southCenter SegmentsShareEndpoint
+              ]
+          , draftNeighborhoods =
+              [ DraftNeighborhood center (east :| [north, west, south])
+              ]
+          }
+  certificate <- requireRight "ordinary local embedding" (certifyLocalEmbedding draft)
+  westEmbedding <-
+    maybe
+      (fail "ordinary local embedding omitted its admitted west candidate")
+      pure
+      (Map.lookup west (certificateRoundedVertices certificate))
+  assertEqual
+    "ordinary certificate retains admitted projection"
+    (Point (-1) 0)
+    (queryPointValue westEmbedding)
+  assertEqual
+    "ordinary distinct rounded vertices"
+    5
+    (certificateDistinctRoundedVertexCount certificate)
+  assertEqual
+    "ordinary split-order obligations"
+    4
+    (certificateSplitOrderPreservationCount certificate)
+  assertEqual
+    "ordinary incidence obligations"
+    1
+    (certificateIncidenceRelationPreservationCount certificate)
+  assertEqual
+    "ordinary neighborhood obligations"
+    4
+    (certificateNeighborhoodRotationPreservationCount certificate)
+  assertEqual
+    "ordinary residual"
+    (GlobalNoNewCrossing :| [])
+    (residualUndischargedObligations (certificateResidual certificate))
+  putStrLn
+    ( "local embedding certificate counts: "
+        <> show
+          ( certificateDistinctRoundedVertexCount certificate
+          , certificateSplitOrderPreservationCount certificate
+          , certificateIncidenceRelationPreservationCount certificate
+          , certificateNeighborhoodRotationPreservationCount certificate
+          )
+    )
+  putStrLn
+    ( "local embedding declared residual: "
+        <> show (residualUndischargedObligations (certificateResidual certificate))
+    )
+
+-- Independent binary64 projection bends the exact straight-through pair at
+-- the center, but the cyclic neighbor order is unchanged. The local topology
+-- obligation is rotation preservation, not literal preservation of a zero
+-- determinant.
+testCollinearNeighborhoodRotation :: IO ()
+testCollinearNeighborhoodRotation = do
+  oneThird <- requireRight "collinear rotation coordinate" (exactRational 1 3)
+  let center, right, branch, left :: DraftVertexId
+      center = DraftId 0
+      right = DraftId 1
+      branch = DraftId 2
+      left = DraftId 3
+      centerRight, centerBranch, centerLeft :: DraftSegmentId
+      centerRight = DraftId 0
+      centerBranch = DraftId 1
+      centerLeft = DraftId 2
+      draft =
+        emptyDraft
+          { draftVertices =
+              Map.fromList
+                [ (center, exactPoint 1 oneThird)
+                , (right, integerPoint 3 1)
+                , (branch, integerPoint 1 2)
+                , (left, integerPoint 0 0)
+                ]
+          , draftSegments =
+              Map.fromList
+                [ (centerRight, (center, right))
+                , (centerBranch, (center, branch))
+                , (centerLeft, (center, left))
+                ]
+          , draftIncidences =
+              [ DraftIncidence centerRight centerBranch SegmentsShareEndpoint
+              , DraftIncidence centerBranch centerLeft SegmentsShareEndpoint
+              , DraftIncidence centerLeft centerRight SegmentsShareEndpoint
+              ]
+          , draftNeighborhoods =
+              [DraftNeighborhood center (right :| [branch, left])]
+          }
+  certificate <-
+    requireRight
+      "collinear neighborhood retains its cyclic rotation"
+      (certifyLocalEmbedding draft)
+  assertEqual
+    "collinear neighborhood rotation obligations"
+    3
+    (certificateNeighborhoodRotationPreservationCount certificate)
+
+testExactArithmeticReceipt :: IO ()
+testExactArithmeticReceipt = do
+  enabled <- getRTSStatsEnabled
+  unless enabled $
+    fail "exact arithmetic allocation receipt requires +RTS -T"
+  operands <-
+    traverse
+      ( \index ->
+          requireRight
+            "exact arithmetic receipt operand"
+            (exactRational (toInteger (index `mod` 89 + 1)) 97)
+      )
+      [0 .. 9_999 :: Int]
+  _ <- evaluate (force operands)
+  performGC
+  before <- allocated_bytes <$> getRTSStats
+  checksum <-
+    evaluate
+      ( force
+          ( List.foldl'
+              ( \accumulated value ->
+                  accumulated
+                    + fromEnum
+                      ( exactSignum
+                          ((value + 1) * value)
+                      )
+              )
+              0
+              operands
+          )
+      )
+  performGC
+  after <- allocated_bytes <$> getRTSStats
+  let operationCount = 3 * length operands
+      allocated = after - before
+  unless (checksum > 0) $
+    fail "exact arithmetic receipt did not force its operation chain"
+  putStrLn
+    ( "exact arithmetic receipt: operations="
+        <> show operationCount
+        <> " allocated-bytes="
+        <> show allocated
+        <> " checksum="
+        <> show checksum
+    )
+
+testEmbeddingAdmissionReceipt :: IO ()
+testEmbeddingAdmissionReceipt = do
+  let base = 2 ^ (52 :: Int)
+  firstCollision <- requireRight "admission receipt collision a" (exactRational (base * 4 + 1) 4)
+  secondCollision <- requireRight "admission receipt collision b" (exactRational (base * 4 + 2) 4)
+  let admittedDraft =
+        emptyDraft
+          { draftVertices =
+              Map.fromList
+                [ (DraftId 0, integerPoint 0 0)
+                , (DraftId 1, integerPoint 1 0)
+                ]
+          }
+      collisionDraft =
+        emptyDraft
+          { draftVertices =
+              Map.fromList
+                [ (DraftId 0, exactPoint firstCollision 0)
+                , (DraftId 1, exactPoint secondCollision 0)
+                ]
+          }
+      projectionDraft =
+        emptyDraft
+          { draftVertices =
+              Map.singleton
+                (DraftId 0)
+                (exactPoint (fromInteger (10 ^ (400 :: Int))) 0)
+          }
+      outcomes =
+        map certifyLocalEmbedding [admittedDraft, collisionDraft, projectionDraft]
+      admitted = length [() | Right _ <- outcomes]
+      refused = length [() | Left _ <- outcomes]
+  assertEqual "embedding admission receipt admissions" 1 admitted
+  assertEqual "embedding admission receipt refusals" 2 refused
+  putStrLn
+    ( "embedding admission receipt: admitted="
+        <> show admitted
+        <> " refused="
+        <> show refused
+    )
+
+testFrozenBinary64RelationOracle :: IO ()
+testFrozenBinary64RelationOracle = do
+  let observed =
+        map
+          ( \(name, expected, (a, b, c, d)) ->
+              (name, expected, segmentRelation a b c d)
+          )
+          handFixtures
+      relations =
+        map (\(_, _, relation) -> relation) observed <> corpusRelations
+  traverse_
+    (\(name, expected, actual) -> assertEqual name expected actual)
+    observed
+  assertEqual "frozen relation count" 16_390 (length relations)
+  assertEqual
+    "frozen relation digest"
+    1_170_735_657_727_369_596
+    (digest relations)
+
+data ObservedRelation = ObservedRelation
+  { observedName :: !String
+  , observedExpectedRelation :: !SegmentRelation
+  , observedRoundedRelation :: !SegmentRelation
+  , observedExactRelation :: !SegmentRelation
+  }
+
+observeExactAndRounded
+  :: (String, SegmentRelation, (Point, Point, Point, Point))
+  -> IO ObservedRelation
+observeExactAndRounded (name, expected, points@(a, b, c, d)) = do
+  (exactA, exactB, exactC, exactD) <- exactPointTuple points
+  pure
+    ObservedRelation
+      { observedName = name
+      , observedExpectedRelation = expected
+      , observedRoundedRelation = segmentRelation a b c d
+      , observedExactRelation = exactSegmentRelation exactA exactB exactC exactD
+      }
+
+assertExpectedAndAgreement :: ObservedRelation -> IO ()
+assertExpectedAndAgreement observed = do
+  assertEqual
+    (observedName observed <> " binary64 expectation")
+    (observedExpectedRelation observed)
+    (observedRoundedRelation observed)
+  assertEqual
+    (observedName observed <> " exact agreement")
+    (observedRoundedRelation observed)
+    (observedExactRelation observed)
+
+contactFixtures
+  :: [(String, SegmentRelation, (Point, Point, Point, Point))]
+contactFixtures =
+  [ ( "endpoint-on-edge"
+    , SegmentEndpointTouchesInterior
+    , (Point 0 0, Point 2 0, Point 1 0, Point 1 1)
+    )
+  , ( "duplicate"
+    , SegmentsDuplicate
+    , (Point 0 0, Point 2 0, Point 2 0, Point 0 0)
+    )
+  , ( "partial-collinear-overlap"
+    , SegmentsCollinearlyOverlap
+    , (Point 0 0, Point 3 0, Point 1 0, Point 2 0)
+    )
+  ]
+
+assertContactAgreement
+  :: (String, SegmentRelation, (Point, Point, Point, Point))
+  -> IO ()
+assertContactAgreement fixture =
+  observeExactAndRounded fixture >>= assertExpectedAndAgreement
+
+exactPointTuple
+  :: (Point, Point, Point, Point)
+  -> IO (ExactPoint, ExactPoint, ExactPoint, ExactPoint)
+exactPointTuple (a, b, c, d) =
+  (,,,)
+    <$> requireRight "exact fixture point a" (exactPointFromPoint a)
+    <*> requireRight "exact fixture point b" (exactPointFromPoint b)
+    <*> requireRight "exact fixture point c" (exactPointFromPoint c)
+    <*> requireRight "exact fixture point d" (exactPointFromPoint d)
+
+exactCrossingAt :: ExactRational -> IO ExactPoint
+exactCrossingAt coordinate = do
+  horizontal <-
+    requireRight
+      "sub-ulp horizontal crossing segment"
+      ( exactSegment
+          (exactPoint (coordinate - 1) coordinate)
+          (exactPoint (coordinate + 1) coordinate)
+      )
+  vertical <-
+    requireRight
+      "sub-ulp vertical crossing segment"
+      ( exactSegment
+          (exactPoint coordinate (coordinate - 1))
+          (exactPoint coordinate (coordinate + 1))
+      )
+  requireRight "sub-ulp exact crossing" (exactLineIntersection horizontal vertical)
+
+assertRatio :: String -> Integer -> Integer -> ExactRational -> IO ()
+assertRatio label numerator denominator value = do
+  assertEqual (label <> " numerator") numerator (exactRationalNumerator value)
+  assertEqual (label <> " denominator") denominator (exactRationalDenominator value)
+
+emptyDraft :: ExactArrangementDraft
+emptyDraft =
+  ExactArrangementDraft
+    { draftVertices = Map.empty
+    , draftSegments = Map.empty
+    , draftSourceMemberships = Map.empty
+    , draftIncidences = []
+    , draftNeighborhoods = []
+    }
+
+assertLeftContains
+  :: String
+  -> (OverlayEmbeddingObstruction -> Bool)
+  -> Either (NonEmpty OverlayEmbeddingObstruction) value
+  -> IO ()
+assertLeftContains label predicate result =
+  case result of
+    Left obstructions ->
+      unless (any predicate obstructions) $
+        fail (label <> ": missing witness in " <> show obstructions)
+    Right _ -> fail (label <> ": unexpectedly certified")
+
+isRoundedCollision :: OverlayEmbeddingObstruction -> Bool
+isRoundedCollision RoundedVerticesCollide {} = True
+isRoundedCollision _ = False
+
+isProjectionRefusal :: OverlayEmbeddingObstruction -> Bool
+isProjectionRefusal VertexProjectionRefused {} = True
+isProjectionRefusal _ = False
+
+isIncidenceChange :: OverlayEmbeddingObstruction -> Bool
+isIncidenceChange IncidenceRelationChanged {} = True
+isIncidenceChange _ = False
+
+handFixtures :: [(String, SegmentRelation, (Point, Point, Point, Point))]
+handFixtures =
+  [ ("disjoint", SegmentsDisjoint, (Point 0 0, Point 1 0, Point 0 2, Point 1 2))
+  , ("duplicate", SegmentsDuplicate, (Point 0 0, Point 2 0, Point 2 0, Point 0 0))
+  , ("shared-endpoint", SegmentsShareEndpoint, (Point 0 0, Point 2 0, Point 2 0, Point 3 1))
+  , ("proper-crossing", SegmentsProperlyCross, (Point 0 0, Point 2 2, Point 0 2, Point 2 0))
+  , ("endpoint-interior", SegmentEndpointTouchesInterior, (Point 0 0, Point 2 0, Point 1 0, Point 1 1))
+  , ("collinear-overlap", SegmentsCollinearlyOverlap, (Point 0 0, Point 3 0, Point 1 0, Point 2 0))
+  ]
+
+corpusPoint :: Int -> Int -> Point
+corpusPoint index salt =
+  Point
+    (fromIntegral (((index * 17 + salt * 11) `mod` 47) - 23))
+    (fromIntegral (((index * 29 + salt * 7) `mod` 43) - 21))
+
+corpusRelations :: [SegmentRelation]
+corpusRelations =
+  [ segmentRelation
+      (corpusPoint index 1)
+      (corpusPoint index 2)
+      (corpusPoint index 3)
+      (corpusPoint index 4)
+  | index <- [0 .. 16_383]
+  ]
+
+digest :: [SegmentRelation] -> Word64
+digest = List.foldl' step 14_695_981_039_346_656_037
+ where
+  step :: Word64 -> SegmentRelation -> Word64
+  step hash relation =
+    (hash `xor` fromIntegral (fromEnum relation + 1)) * 1_099_511_628_211
+
+-- | The frozen corpus above holds only disjoint pairs and proper crossings;
+-- these families are the contact relations it never reaches. Every pair is
+-- built on one integer line through a base point with an integer direction,
+-- its endpoints at integer stations along that line and, where a point must
+-- leave the line, displaced by a nonzero multiple of the perpendicular. All
+-- coordinates stay below 2^10, so every orientation determinant is exact in
+-- binary64 and the derived class is the class, with nothing left to
+-- rounding. The relation is symmetric under reversing either segment and
+-- under swapping the pair, so each draw also flips those three coins.
+testContactFamilyOracle :: IO ()
+testContactFamilyOracle = do
+  relations <-
+    concat <$> traverse (uncurry assertContactFamily) (zip [0 ..] contactFamilies)
+  assertEqual
+    "contact family relation count"
+    (contactFamilySize * length contactFamilies)
+    (length relations)
+  -- FNV of the derived class sequence, computed independently of this
+  -- binary from the family definitions alone (contact-families-replica.py in
+  -- the campaign receipts): eight runs of 512.
+  assertEqual
+    "contact family relation digest"
+    18_184_234_789_390_492_453
+    (digest relations)
+
+data ContactFamily = ContactFamily
+  { contactFamilyName :: !String
+  , contactFamilyExpected :: !SegmentRelation
+  , contactFamilyPair :: ContactDraw -> (Point, Point, Point, Point)
+  }
+
+contactFamilySize :: Int
+contactFamilySize = 512
+
+-- | Stations are ordered t0 < t1 < t2 < t3 along the line, so "between" and
+-- "beyond" below are exact statements about the parameter, and lexicographic
+-- point order along one line is monotone in the parameter, which is what the
+-- classifier's overlap window reads.
+contactFamilies :: [ContactFamily]
+contactFamilies =
+  [ -- C is A itself; D leaves the line, so the segments meet in exactly the
+    -- one shared endpoint and nothing is collinear.
+    ContactFamily "shared-endpoint-non-collinear" SegmentsShareEndpoint $ \draw ->
+      let (t0, t1, t2, _) = drawStations draw
+       in (station draw t0, station draw t1, station draw t0, offLine draw t2)
+  , -- Collinear, C is B, D beyond: the overlap window is the single point B,
+    -- an endpoint of both, so a straight continuation is a shared endpoint.
+    ContactFamily "straight-continuation" SegmentsShareEndpoint $ \draw ->
+      let (t0, t1, t2, _) = drawStations draw
+       in (station draw t0, station draw t1, station draw t1, station draw t2)
+  , -- Collinear, C is A, and D lies strictly between A and B (or strictly
+    -- beyond B, on the fourth coin): the overlap window has positive length,
+    -- so the shared endpoint does not demote it below an overlap.
+    ContactFamily "collinear-overlap-shared-endpoint" SegmentsCollinearlyOverlap $ \draw ->
+      let (t0, t1, t2, _) = drawStations draw
+       in if drawCoin draw 3
+            then (station draw t0, station draw t1, station draw t0, station draw t2)
+            else (station draw t0, station draw t2, station draw t0, station draw t1)
+  , -- Collinear and staggered: A < C < B < D, overlap window [C, B] of
+    -- positive length, no endpoint shared.
+    ContactFamily "collinear-overlap-staggered" SegmentsCollinearlyOverlap $ \draw ->
+      let (t0, t1, t2, t3) = drawStations draw
+       in (station draw t0, station draw t2, station draw t1, station draw t3)
+  , -- Collinear, CD strictly inside AB: overlap window [C, D], no endpoint
+    -- shared.
+    ContactFamily "collinear-containment" SegmentsCollinearlyOverlap $ \draw ->
+      let (t0, t1, t2, t3) = drawStations draw
+       in (station draw t0, station draw t3, station draw t1, station draw t2)
+  , -- C sits strictly between A and B on the line and D leaves the line: C is
+    -- on AB's interior, no endpoint is shared, and with D off the line the
+    -- orientations of C and D about AB are EQ and nonzero, never opposite.
+    ContactFamily "endpoint-touches-interior" SegmentEndpointTouchesInterior $ \draw ->
+      let (t0, t1, t2, _) = drawStations draw
+       in (station draw t0, station draw t2, station draw t1, offLine draw t1)
+  , -- The same segment twice; the orientation coins cover both directions.
+    ContactFamily "duplicate" SegmentsDuplicate $ \draw ->
+      let (t0, t1, _, _) = drawStations draw
+       in (station draw t0, station draw t1, station draw t0, station draw t1)
+  , -- Collinear with a gap: B < C strictly, so the overlap window is empty.
+    ContactFamily "collinear-disjoint" SegmentsDisjoint $ \draw ->
+      let (t0, t1, t2, t3) = drawStations draw
+       in (station draw t0, station draw t1, station draw t2, station draw t3)
+  ]
+
+assertContactFamily :: Int -> ContactFamily -> IO [SegmentRelation]
+assertContactFamily familyIndex family = do
+  let pairs =
+        [ orientPair draw (contactFamilyPair family draw)
+        | index <- [0 .. contactFamilySize - 1]
+        , let draw = contactDraw familyIndex index
+        ]
+      rounded = [segmentRelation a b c d | (a, b, c, d) <- pairs]
+  exact <- traverse exactPointTuple pairs
+  let exactRelations = [exactSegmentRelation a b c d | (a, b, c, d) <- exact]
+      expected = contactFamilyExpected family
+      disagreements = filter ((/= expected) . snd) . zip [0 :: Int ..]
+  assertEqual (name <> " pair count") contactFamilySize (length pairs)
+  assertEqual (name <> " binary64 disagreements") [] (disagreements rounded)
+  assertEqual (name <> " exact disagreements") [] (disagreements exactRelations)
+  pure rounded
+ where
+  name = contactFamilyName family
+
+data ContactDraw = ContactDraw
+  { drawBase :: !(Int, Int)
+  , drawDirection :: !(Int, Int)
+  , drawOffset :: !Int
+  , drawStations :: !(Int, Int, Int, Int)
+  , drawCoins :: !Word64
+  }
+
+-- | Base in [-64, 64]^2, direction in [-8, 8]^2 without the origin, offset in
+-- [-3, 3] without zero, four strictly increasing stations from [-4, 4] with
+-- gaps in [1, 4], and four coins; all read from one SplitMix64 mix of the
+-- family and index.
+contactDraw :: Int -> Int -> ContactDraw
+contactDraw family index =
+  ContactDraw
+    { drawBase = (field 0 129 - 64, field 7 129 - 64)
+    , drawDirection = direction
+    , drawOffset = if offsetField < 3 then offsetField - 3 else offsetField - 2
+    , drawStations = (t0, t1, t2, t3)
+    , drawCoins = seed `shiftR` 44
+    }
+ where
+  seed = splitMix64 (fromIntegral (family * 1_048_576 + index))
+  field :: Int -> Word64 -> Int
+  field offset modulus = fromIntegral ((seed `shiftR` offset) `mod` modulus)
+  direction =
+    case (field 14 17 - 8, field 18 17 - 8) of
+      (0, 0) -> (1, 0)
+      nonzero -> nonzero
+  offsetField = field 22 6
+  t0 = field 25 9 - 4
+  t1 = t0 + field 29 4 + 1
+  t2 = t1 + field 31 4 + 1
+  t3 = t2 + field 33 4 + 1
+
+drawCoin :: ContactDraw -> Int -> Bool
+drawCoin draw bit = (drawCoins draw `shiftR` bit) `mod` 2 == 1
+
+station :: ContactDraw -> Int -> Point
+station draw parameter =
+  let (baseX, baseY) = drawBase draw
+      (directionX, directionY) = drawDirection draw
+   in Point
+        (fromIntegral (baseX + parameter * directionX))
+        (fromIntegral (baseY + parameter * directionY))
+
+-- | The station displaced off the line by the offset times the perpendicular
+-- of the direction; nonzero offset and nonzero direction keep it off.
+offLine :: ContactDraw -> Int -> Point
+offLine draw parameter =
+  let (baseX, baseY) = drawBase draw
+      (directionX, directionY) = drawDirection draw
+      offset = drawOffset draw
+   in Point
+        (fromIntegral (baseX + parameter * directionX - offset * directionY))
+        (fromIntegral (baseY + parameter * directionY + offset * directionX))
+
+orientPair :: ContactDraw -> (Point, Point, Point, Point) -> (Point, Point, Point, Point)
+orientPair draw (a, b, c, d) =
+  let (a', b') = if drawCoin draw 0 then (b, a) else (a, b)
+      (c', d') = if drawCoin draw 1 then (d, c) else (c, d)
+   in if drawCoin draw 2 then (c', d', a', b') else (a', b', c', d')
+
+splitMix64 :: Word64 -> Word64
+splitMix64 input =
+  let z0 = input + 0x9E37_79B9_7F4A_7C15
+      z1 = (z0 `xor` (z0 `shiftR` 30)) * 0xBF58_476D_1CE4_E5B9
+      z2 = (z1 `xor` (z1 `shiftR` 27)) * 0x94D0_49BB_1331_11EB
+   in z2 `xor` (z2 `shiftR` 31)
diff --git a/test/native/Moonlight/Planar/ExactSegmentEventsSpec.hs b/test/native/Moonlight/Planar/ExactSegmentEventsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ExactSegmentEventsSpec.hs
@@ -0,0 +1,190 @@
+-- | Exact segment-event sweep against an independent quadratic oracle.
+module Moonlight.Planar.ExactSegmentEventsSpec (tests) where
+
+import Control.Monad ( foldM )
+import Data.Foldable ( traverse_ )
+import Moonlight.Planar.Exact ( SegmentRelation(..), ExactPoint, exactLineIntersection,
+  exactOnClosedSegment, exactSegment, exactSegmentEndpoints, exactSegmentRelation,
+  ExactIntersectionError, ExactSegment )
+import Moonlight.Planar.Internal.ExactSegmentEvents ( ExactSegmentEventPlan,
+  ExactSweepSegmentId(..), exactSegmentEventPlan, exactSegmentRelationMap, exactSegmentSplitPoints,
+  exactSegmentSweepMaximumHeight )
+import Support ( assertEqual, integerPoint, requireRight )
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+tests :: IO ()
+tests = testExactSegmentEventPlan
+
+testExactSegmentEventPlan :: IO ()
+testExactSegmentEventPlan = do
+  sourceFixture <-
+    traverse
+      (uncurry integerSegment)
+      [ ((0, 0), (4, 0))
+      , ((4, 0), (0, 0))
+      , ((2, 0), (6, 0))
+      , ((4, 0), (4, 4))
+      , ((1, -1), (1, 0))
+      , ((0, -1), (4, 1))
+      , ((10, 10), (11, 10))
+      ]
+  verticalAndMultiway <-
+    traverse
+      (uncurry integerSegment)
+      [ ((0, -4), (0, 4))
+      , ((-4, 0), (4, 0))
+      , ((-3, -3), (3, 3))
+      , ((-3, 3), (3, -3))
+      , ((0, 1), (0, 5))
+      , ((0, 4), (0, 7))
+      ]
+  grid <-
+    traverse
+      (uncurry integerSegment)
+      ( [((-1, y), (5, y)) | y <- [0 .. 4]]
+          <> [((x, -1), (x, 5)) | x <- [0 .. 4]]
+      )
+  collinearOverlaps <-
+    traverse
+      (uncurry integerSegment)
+      [ ((0, 0), (8, 0))
+      , ((1, 0), (3, 0))
+      , ((2, 0), (6, 0))
+      , ((5, 0), (9, 0))
+      , ((8, 0), (10, 0))
+      , ((11, 0), (12, 0))
+      ]
+  traverse_
+    compareEventPlanWithOracle
+    [sourceFixture, verticalAndMultiway, grid, collinearOverlaps]
+  let allRelations =
+        Set.fromList
+          [ exactSegmentRelation a b c d
+          | (leftIndex, left) <- zip [0 :: Int ..] sourceFixture
+          , right <- drop (leftIndex + 1) sourceFixture
+          , let (a, b) = exactSegmentEndpoints left
+                (c, d) = exactSegmentEndpoints right
+          ]
+  assertEqual
+    "source fixture covers every segment relation"
+    ( Set.fromList
+        [ SegmentsDisjoint
+        , SegmentsProperlyCross
+        , SegmentsShareEndpoint
+        , SegmentEndpointTouchesInterior
+        , SegmentsCollinearlyOverlap
+        , SegmentsDuplicate
+        ]
+    )
+    allRelations
+
+compareEventPlanWithOracle :: [ExactSegment] -> IO ()
+compareEventPlanWithOracle segments = do
+  let vector = V.fromList segments
+  plan <- requireRight "exact event sweep" (exactSegmentEventPlan vector)
+  expectedSplits <- requireRight "quadratic split oracle" (quadraticSplitPoints segments)
+  assertEqual
+    "sweep relation map agrees with quadratic oracle"
+    (quadraticRelationMap segments)
+    (exactSegmentRelationMap plan)
+  traverse_
+    (assertSegmentSplits plan expectedSplits)
+    [0 .. length segments - 1]
+  let heightLimit = 2 * ceilingLog2 (length segments + 1)
+  if exactSegmentSweepMaximumHeight plan <= heightLimit
+    then pure ()
+    else
+      fail
+        ( "AVL height exceeded conservative logarithmic bound: "
+            <> show (exactSegmentSweepMaximumHeight plan, heightLimit)
+        )
+
+assertSegmentSplits
+  :: ExactSegmentEventPlan
+  -> Map.Map Int (Set.Set ExactPoint)
+  -> Int
+  -> IO ()
+assertSegmentSplits plan expected segmentIndex =
+  assertEqual
+    ("sweep split points agree for segment " <> show segmentIndex)
+    (Map.findWithDefault Set.empty segmentIndex expected)
+    (Set.fromList (exactSegmentSplitPoints plan (ExactSweepSegmentId segmentIndex)))
+
+quadraticRelationMap
+  :: [ExactSegment]
+  -> Map.Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+quadraticRelationMap segments =
+  Map.fromList
+    [ ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation)
+    | (leftIndex, left) <- zip [0 :: Int ..] segments
+    , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) segments)
+    , let (a, b) = exactSegmentEndpoints left
+          (c, d) = exactSegmentEndpoints right
+          relation = exactSegmentRelation a b c d
+    , relation /= SegmentsDisjoint
+    ]
+
+quadraticSplitPoints
+  :: [ExactSegment]
+  -> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
+quadraticSplitPoints segments =
+  foldM addRelation initial (segmentPairs segments)
+ where
+  initial =
+    Map.fromList
+      [ (index, Set.fromList [from, to])
+      | (index, segment) <- zip [0 :: Int ..] segments
+      , let (from, to) = exactSegmentEndpoints segment
+      ]
+  addRelation
+    :: Map.Map Int (Set.Set ExactPoint)
+    -> (Int, ExactSegment, Int, ExactSegment)
+    -> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
+  addRelation splitPoints (leftIndex, left, rightIndex, right) = do
+    witnesses <- relationSplitWitnesses left right
+    pure
+      ( Map.insertWith Set.union rightIndex (Set.fromList witnesses)
+          (Map.insertWith Set.union leftIndex (Set.fromList witnesses) splitPoints)
+      )
+
+segmentPairs :: [value] -> [(Int, value, Int, value)]
+segmentPairs values =
+  [ (leftIndex, left, rightIndex, right)
+  | (leftIndex, left) <- zip [0 :: Int ..] values
+  , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) values)
+  ]
+
+relationSplitWitnesses
+  :: ExactSegment
+  -> ExactSegment
+  -> Either ExactIntersectionError [ExactPoint]
+relationSplitWitnesses left right =
+  case exactSegmentRelation a b c d of
+    SegmentsDisjoint -> Right []
+    SegmentsProperlyCross -> (: []) <$> exactLineIntersection left right
+    SegmentsDuplicate -> Right []
+    _ ->
+      Right
+        ( Set.toAscList
+            ( Set.fromList
+                [ point
+                | point <- [a, b, c, d]
+                , exactOnClosedSegment a b point
+                , exactOnClosedSegment c d point
+                ]
+            )
+        )
+ where
+  (a, b) = exactSegmentEndpoints left
+  (c, d) = exactSegmentEndpoints right
+
+ceilingLog2 :: Int -> Int
+ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))
+
+integerSegment :: (Integer, Integer) -> (Integer, Integer) -> IO ExactSegment
+integerSegment (fromX, fromY) (toX, toY) =
+  requireRight
+    "integer exact segment"
+    (exactSegment (integerPoint fromX fromY) (integerPoint toX toY))
diff --git a/test/native/Moonlight/Planar/FilteredPredicateOptimizationSpec.hs b/test/native/Moonlight/Planar/FilteredPredicateOptimizationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/FilteredPredicateOptimizationSpec.hs
@@ -0,0 +1,194 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# OPTIONS_GHC -O1 #-}
+
+-- | The allocation law for the optimized filtered-predicate artifact. This
+-- module remains at O1 when the surrounding behavioral body is compiled at O0;
+-- without the simplifier, the loop does not unbox and the witness is vacuous.
+module Moonlight.Planar.FilteredPredicateOptimizationSpec
+  ( assertFilteredPredicatesSkipExactOracle
+  ) where
+
+import Control.DeepSeq (force)
+import Control.Exception (evaluate)
+import Control.Monad (unless)
+import GHC.Stats (allocated_bytes, getRTSStats, getRTSStatsEnabled)
+import Moonlight.Planar.Math
+  ( inCircle
+  , inCircleDetApprox
+  , orient2d
+  , orientDetApprox
+  )
+import Moonlight.Planar.Internal.Point (QueryPoint (..))
+import Moonlight.Planar.Point (Point (..))
+import System.Mem (performGC)
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word64)
+
+-- | A filtered predicate must not evaluate its exact oracle when the floating
+-- approximation already certifies the sign. The exact path allocates
+-- 'Integer's once the operands outgrow its fixed-width worker, so
+-- equal-shaped certified and wide-exponent degenerate folds expose whether
+-- that fallback remains genuinely conditional; a degenerate fold whose
+-- operands fit the worker must then allocate no more than a certified one.
+assertFilteredPredicatesSkipExactOracle :: IO ()
+assertFilteredPredicatesSkipExactOracle = do
+  enabled <- getRTSStatsEnabled
+  unless enabled $ fail "allocation counters unavailable: the suite must run with -T"
+  let count = 20_000
+      bigScale = 2 ^^ (200 :: Int) :: Double
+      smallScale = 2 ^^ (-200 :: Int) :: Double
+      orientCertified =
+        V.generate
+          count
+          (\index -> QueryPoint (Point (fromIntegral index * bigScale) (if even index then 0 else smallScale)))
+      orientDegenerate =
+        V.generate
+          count
+          (\index -> QueryPoint (Point (fromIntegral index * bigScale) (fromIntegral index * smallScale)))
+      zigzag = V.generate count (\index -> QueryPoint (Point (fromIntegral index) (if even index then 0 else 1)))
+      collinear = V.generate count (\index -> QueryPoint (Point (fromIntegral index) (fromIntegral index)))
+      collinearWide =
+        V.generate
+          count
+          (\index -> QueryPoint (Point (fromIntegral index * bigScale) (fromIntegral index * smallScale)))
+      orientationIndices = U.enumFromN 0 (max 0 (count - 2))
+      circleIndices = U.enumFromN 0 (max 0 (count - 3))
+  _ <- evaluate (force orientCertified)
+  _ <- evaluate (force orientDegenerate)
+  _ <- evaluate (force zigzag)
+  _ <- evaluate (force collinear)
+  _ <- evaluate (force collinearWide)
+  _ <- evaluate (force orientationIndices)
+  _ <- evaluate (force circleIndices)
+  certifiedSupport <- allocationOf (sumTripleRelations approximateOrientation orientationIndices orientCertified)
+  fallbackSupport <- allocationOf (sumTripleRelations approximateOrientation orientationIndices orientDegenerate)
+  fixedSupport <- allocationOf (sumTripleRelations approximateOrientation orientationIndices collinear)
+  certified <- allocationOf (sumTripleRelations orient2d orientationIndices orientCertified)
+  fallback <- allocationOf (sumTripleRelations orient2d orientationIndices orientDegenerate)
+  fixed <- allocationOf (sumTripleRelations orient2d orientationIndices collinear)
+  let certifiedOracle = allocationBeyondApproximation certified certifiedSupport
+      fallbackOracle = allocationBeyondApproximation fallback fallbackSupport
+      fixedOracle = allocationBeyondApproximation fixed fixedSupport
+  unless (fallbackOracle > 0) $
+    fail "degenerate orientations allocated nothing: the measurement is not observing the oracle"
+  unless (certifiedOracle * 8 < fallbackOracle) $
+    fail
+      ( "orient2d evaluates its exact oracle on certified input: "
+          <> show certifiedOracle
+          <> " excess bytes certified versus "
+          <> show fallbackOracle
+          <> " excess bytes degenerate (raw "
+          <> show certified
+          <> " versus "
+          <> show fallback
+          <> ")"
+      )
+  unless (fixedOracle * 8 < fallbackOracle) $
+    fail
+      ( "orient2d allocates on degenerate input its fixed-width worker resolves: "
+          <> show fixedOracle
+          <> " excess bytes within the width versus "
+          <> show fallbackOracle
+          <> " excess bytes beyond it"
+      )
+  certifiedCircleSupport <- allocationOf (sumQuadRelations approximateInCircle circleIndices zigzag)
+  fallbackCircleSupport <- allocationOf (sumQuadRelations approximateInCircle circleIndices collinearWide)
+  fixedCircleSupport <- allocationOf (sumQuadRelations approximateInCircle circleIndices collinear)
+  certifiedCircle <- allocationOf (sumQuadRelations inCircle circleIndices zigzag)
+  fallbackCircle <- allocationOf (sumQuadRelations inCircle circleIndices collinearWide)
+  fixedCircle <- allocationOf (sumQuadRelations inCircle circleIndices collinear)
+  let certifiedCircleOracle = allocationBeyondApproximation certifiedCircle certifiedCircleSupport
+      fallbackCircleOracle = allocationBeyondApproximation fallbackCircle fallbackCircleSupport
+      fixedCircleOracle = allocationBeyondApproximation fixedCircle fixedCircleSupport
+  unless (fallbackCircleOracle > 0) $
+    fail "degenerate incircles allocated nothing: the measurement is not observing the oracle"
+  unless (certifiedCircleOracle * 8 < fallbackCircleOracle) $
+    fail
+      ( "inCircle evaluates its exact oracle on certified input: "
+          <> show certifiedCircleOracle
+          <> " excess bytes certified versus "
+          <> show fallbackCircleOracle
+          <> " excess bytes degenerate (raw "
+          <> show certifiedCircle
+          <> " versus "
+          <> show fallbackCircle
+          <> ")"
+      )
+  unless (fixedCircleOracle * 8 < fallbackCircleOracle) $
+    fail
+      ( "inCircle allocates on degenerate input its fixed-width worker resolves: "
+          <> show fixedCircleOracle
+          <> " excess bytes within the width versus "
+          <> show fallbackCircleOracle
+          <> " excess bytes beyond it"
+      )
+  putStrLn
+    ( "filtered predicate allocation receipt: orient certified="
+        <> show certifiedOracle <> " fixed=" <> show fixedOracle <> " fallback=" <> show fallbackOracle
+        <> " incircle certified=" <> show certifiedCircleOracle
+        <> " fixed=" <> show fixedCircleOracle <> " fallback=" <> show fallbackCircleOracle
+    )
+
+allocationOf :: Int -> IO Word64
+allocationOf work = do
+  performGC
+  before <- allocated_bytes <$> getRTSStats
+  _ <- evaluate work
+  performGC
+  after <- allocated_bytes <$> getRTSStats
+  pure (after - before)
+
+allocationBeyondApproximation :: Word64 -> Word64 -> Word64
+allocationBeyondApproximation measured support = measured - min measured support
+
+sumTripleRelations
+  :: (QueryPoint -> QueryPoint -> QueryPoint -> Ordering)
+  -> U.Vector Int
+  -> V.Vector QueryPoint
+  -> Int
+sumTripleRelations relation indices points =
+  U.foldl'
+    (\accumulated index ->
+       accumulated
+         + fromEnum
+           ( relation
+               (V.unsafeIndex points index)
+               (V.unsafeIndex points (index + 1))
+               (V.unsafeIndex points (index + 2))
+           )
+    )
+    0
+    indices
+{-# INLINE sumTripleRelations #-}
+
+sumQuadRelations
+  :: (QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering)
+  -> U.Vector Int
+  -> V.Vector QueryPoint
+  -> Int
+sumQuadRelations relation indices points =
+  U.foldl'
+    (\accumulated index ->
+       accumulated
+         + fromEnum
+           ( relation
+               (V.unsafeIndex points index)
+               (V.unsafeIndex points (index + 1))
+               (V.unsafeIndex points (index + 2))
+               (V.unsafeIndex points (index + 3))
+           )
+    )
+    0
+    indices
+{-# INLINE sumQuadRelations #-}
+
+approximateOrientation :: QueryPoint -> QueryPoint -> QueryPoint -> Ordering
+approximateOrientation (QueryPoint a) (QueryPoint b) (QueryPoint c) =
+  compare (orientDetApprox a b c) 0
+{-# INLINE approximateOrientation #-}
+
+approximateInCircle :: QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering
+approximateInCircle (QueryPoint a) (QueryPoint b) (QueryPoint c) (QueryPoint d) =
+  compare (inCircleDetApprox a b c d) 0
+{-# INLINE approximateInCircle #-}
diff --git a/test/native/Moonlight/Planar/IncidenceSpec.hs b/test/native/Moonlight/Planar/IncidenceSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/IncidenceSpec.hs
@@ -0,0 +1,177 @@
+-- | Shared native packed observations and non-cellular exact face closure.
+module Moonlight.Planar.IncidenceSpec (tests) where
+
+import Data.Foldable (traverse_)
+import qualified Data.IntMap.Strict as IntMap
+import qualified Data.IntSet as IntSet
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word32)
+import Moonlight.Planar.BulkLoad (delaunay)
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Types (buildTriangulation, unitElementDefaults)
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Exact (ExactPoint)
+import Moonlight.Planar.Internal.CellSet
+  ( CellSelectionError (..)
+  , closeExactCellSetWith
+  , exactCellSetEdgeCount
+  , exactCellSetFaceCount
+  , exactCellSetIsFaceClosure
+  , exactCellSetVertexCount
+  )
+import Moonlight.Planar.Internal.HandleDefs
+import Moonlight.Planar.Internal.Incidence
+import Moonlight.Planar.Internal.PackedIndex (noIndex)
+import Moonlight.Planar.Internal.Paged (Paged, fromVector)
+import Moonlight.Planar.Internal.Representation (Triangulation (..))
+import Support (assertEqual, integerPoint, requireRight)
+
+tests :: IO ()
+tests = sequence_
+  [ testNativeProjection
+  , testExactFaceComponents
+  , testPackedRefusals
+  , putStrLn "incidence: ok"
+  ]
+
+testNativeProjection :: IO ()
+testNativeProjection = traverse_ checkNative
+  [ []
+  , [Point 1 1]
+  , [Point 0 0, Point 1 0, Point 2 0]
+  , [Point 0 0, Point 3 0, Point 3 3, Point 0 3, Point 1 1]
+ ]
+ where
+  checkNative :: [Point] -> IO ()
+  checkNative points = do
+    result <- requireRight "native incidence fixture" (delaunay unitElementDefaults (V.fromList points))
+    let mesh = buildTriangulation result
+        incidence = nativePlanarIncidence mesh
+        isolated = if Dcel.numVertices mesh == 1
+          then IntMap.singleton 0 (IntSet.singleton 0)
+          else IntMap.empty
+    checked <- requireRight "native planes satisfy exact packed admission"
+      (admitPlanarIncidence (triVertexOut mesh) (triHalfTopology mesh) (triFaceEdge mesh) IntMap.empty isolated)
+    assertEqual "native projection and checked same-plane publication agree" incidence checked
+    assertEqual "native topology counts"
+      (Dcel.numVertices mesh, Dcel.numDirectedEdges mesh, Dcel.numUndirectedEdges mesh, Dcel.numFaces mesh)
+      (incidenceVertexCount incidence, incidenceDirectedEdgeCount incidence, incidenceEdgeCount incidence, incidenceFaceCount incidence)
+    assertEqual "native non-cellular support is empty" [] (incidenceNonCellularFaces incidence)
+    traverse_
+      (\index ->
+         let edge = DirectedEdgeId (fromIntegral index)
+          in assertEqual "native packed dart observations"
+               (Dcel.origin mesh edge, Dcel.destination mesh edge, Dcel.next mesh edge, Dcel.previous mesh edge, Dcel.incidentFace mesh edge)
+               (incidenceOrigin incidence edge, incidenceDestination incidence edge, incidenceNext incidence edge, incidencePrevious incidence edge, incidenceIncidentFace incidence edge))
+      [0 .. Dcel.numDirectedEdges mesh - 1]
+    traverse_
+      (\index ->
+         let vertex = VertexId (fromIntegral index)
+          in assertEqual "native vertex star observations"
+               (Dcel.vertexOutgoingEdges mesh vertex) (incidenceVertexOutgoingEdges incidence vertex))
+      [0 .. Dcel.numVertices mesh - 1]
+    traverse_
+      (\index ->
+         let face = FaceId (fromIntegral index)
+          in assertEqual "native face cycle observations"
+               (Dcel.faceDirectedEdges mesh face) (concat (faceBoundaryComponents incidence face)))
+      [0 .. Dcel.numFaces mesh - 1]
+
+testExactFaceComponents :: IO ()
+testExactFaceComponents = do
+  incidence <- requireRight "annulus with isolated puncture" fixtureIncidence
+  assertEqual "both oriented annulus boundary components"
+    (fmap (fmap DirectedEdgeId) [[0, 2, 4, 6], [9, 15, 13, 11]])
+    (faceBoundaryComponents incidence (FaceId 1))
+  assertEqual "puncture belongs to annular face" [VertexId 8]
+    (faceIsolatedVertices incidence (FaceId 1))
+  assertEqual "sparse non-cellular support" [FaceId 1] (incidenceNonCellularFaces incidence)
+  assertEqual "one hole and one puncture have two boundary components"
+    [BoundaryCycleRoot (DirectedEdgeId 9), IsolatedBoundaryVertex (VertexId 8)]
+    (faceInnerBoundaryComponents incidence (FaceId 1))
+  assertEqual "open annulus minus one point has Euler contribution minus one" (-1)
+    (faceEulerContribution incidence (FaceId 1))
+  assertEqual "disk face remains a cell" 1 (faceEulerContribution incidence (FaceId 2))
+  closure <- requireRight "exact annular closure"
+    (closeExactCellSetWith fixturePoint incidence [] [] [FaceId 1])
+  assertEqual "closure includes all cycles and isolated boundary vertices" (9, 8, 1)
+    (exactCellSetVertexCount closure, exactCellSetEdgeCount closure, exactCellSetFaceCount closure)
+  assertEqual "arrangement closure is recognized without a native mesh" True
+    (exactCellSetIsFaceClosure closure)
+  singleton <- requireRight "explicit isolated vertex selection"
+    (closeExactCellSetWith fixturePoint incidence [VertexId 8] [] [])
+  assertEqual "an explicit vertex is not a face closure" False (exactCellSetIsFaceClosure singleton)
+
+testPackedRefusals :: IO ()
+testPackedRefusals = sequence_
+  [ expectRefusal "paired-edge layout" isLayout
+      (admitPlanarIncidence fixtureVertices (wordsPlane [0]) fixtureFaces fixtureComponents fixtureIsolated)
+  , expectRefusal "duplicate CCB roots" isBoundaryCoverage
+      (admitPlanarIncidence fixtureVertices fixtureTopology fixtureFaces
+        (IntMap.singleton 1 [DirectedEdgeId 9, DirectedEdgeId 9]) fixtureIsolated)
+  , expectRefusal "unowned isolated point" isIsolatedCoverage
+      (admitPlanarIncidence fixtureVertices fixtureTopology fixtureFaces fixtureComponents IntMap.empty)
+  , expectRefusal "multiply owned isolated point" isIsolatedCoverage
+      (admitPlanarIncidence fixtureVertices fixtureTopology fixtureFaces fixtureComponents
+        (IntMap.insert 0 (IntSet.singleton 8) fixtureIsolated))
+  , expectRefusal "nonexistent isolated point" isIsolatedCoverage
+      (admitPlanarIncidence fixtureVertices fixtureTopology fixtureFaces fixtureComponents
+        (IntMap.singleton 1 (IntSet.fromList [8, 99])))
+  , expectRefusal "missing bounded outer root" isBoundedRoot
+      (admitPlanarIncidence fixtureVertices fixtureTopology (wordsPlane [1, noIndex, 8])
+        fixtureComponents fixtureIsolated)
+  ]
+ where
+  expectRefusal :: String -> (PlanarIncidenceError -> Bool) -> Either PlanarIncidenceError PlanarIncidence -> IO ()
+  expectRefusal label matches outcome = case outcome of
+    Left obstruction | matches obstruction -> pure ()
+    other -> fail (label <> " lost its typed incidence refusal: " <> show other)
+  isLayout, isBoundaryCoverage, isIsolatedCoverage, isBoundedRoot :: PlanarIncidenceError -> Bool
+  isLayout (IncidencePackedLayoutInvalid _ _ _) = True
+  isLayout _ = False
+  isBoundaryCoverage (IncidenceBoundaryCoverageInvalid _ _) = True
+  isBoundaryCoverage _ = False
+  isIsolatedCoverage (IncidenceIsolatedOwnershipInvalid _ _) = True
+  isIsolatedCoverage _ = False
+  isBoundedRoot (IncidenceBoundedFaceWithoutBoundary _) = True
+  isBoundedRoot _ = False
+
+fixtureIncidence :: Either PlanarIncidenceError PlanarIncidence
+fixtureIncidence = admitPlanarIncidence fixtureVertices fixtureTopology fixtureFaces fixtureComponents fixtureIsolated
+
+fixtureVertices, fixtureTopology, fixtureFaces :: Paged Word32
+fixtureVertices = wordsPlane [0, 2, 4, 6, 8, 10, 12, 14, noIndex]
+fixtureFaces = wordsPlane [1, 0, 8]
+fixtureTopology = wordsPlane (concat
+  [ [0, 2, 6, 1], [1, 7, 3, 0]
+  , [1, 4, 0, 1], [2, 1, 5, 0]
+  , [2, 6, 2, 1], [3, 3, 7, 0]
+  , [3, 0, 4, 1], [0, 5, 1, 0]
+  , [4, 10, 14, 2], [5, 15, 11, 1]
+  , [5, 12, 8, 2], [6, 9, 13, 1]
+  , [6, 14, 10, 2], [7, 11, 15, 1]
+  , [7, 8, 12, 2], [4, 13, 9, 1]
+  ])
+
+fixtureComponents :: IntMap.IntMap [DirectedEdgeId]
+fixtureComponents = IntMap.singleton 1 [DirectedEdgeId 9]
+
+fixtureIsolated :: IntMap.IntMap IntSet.IntSet
+fixtureIsolated = IntMap.singleton 1 (IntSet.singleton 8)
+
+wordsPlane :: [Word32] -> Paged Word32
+wordsPlane = fromVector noIndex . U.fromList
+
+fixturePoint :: VertexId -> Either CellSelectionError ExactPoint
+fixturePoint vertex = case vertexIdIndex vertex of
+  0 -> Right (integerPoint 0 0)
+  1 -> Right (integerPoint 8 0)
+  2 -> Right (integerPoint 8 8)
+  3 -> Right (integerPoint 0 8)
+  4 -> Right (integerPoint 2 2)
+  5 -> Right (integerPoint 6 2)
+  6 -> Right (integerPoint 6 6)
+  7 -> Right (integerPoint 2 6)
+  8 -> Right (integerPoint 1 1)
+  _ -> Left (CellVertexOutOfRange vertex 9)
diff --git a/test/native/Moonlight/Planar/LocationSpec.hs b/test/native/Moonlight/Planar/LocationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/LocationSpec.hs
@@ -0,0 +1,382 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Incremental location, frozen-location reuse, and hierarchy laws.
+module Moonlight.Planar.LocationSpec
+  ( tests
+  ) where
+
+import Control.Monad ( unless, forM_ )
+import Data.Foldable ( traverse_ )
+import Moonlight.Planar.BulkLoad ( delaunay, empty, insert )
+import Moonlight.Planar.Dcel ( innerFaceDirectedEdges, numInnerFaces, numVertices, origin,
+  vertexData, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( vertices, innerFaces )
+import Moonlight.Planar.HintGenerator ( buildHierarchyHint, hierarchyHint,
+  updateHierarchyAfterInsertion )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId), reverseEdge )
+import Moonlight.Planar.Math ( midpoint )
+import Moonlight.Planar.MeshFixtures ( requirePointBuild, edgeKeys )
+import Moonlight.Planar.PayloadFixtures ( SampleVertex(..) )
+import Moonlight.Planar.PointLocation ( locatePointWithHint, locatePoint )
+import Moonlight.Planar.Session ( withSession, insertVertexAt, insertVertexAtNearVertex )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (unitElementDefaults, ConstraintMode(Unconstrained), InsertionDisposition(..), Location(..), LocationHint(VertexHint, FaceHint), LocationStats(LocationStats, locationUsedFallback,
+  locationWalkSteps), BuildResult(buildTriangulation), DelaunayTriangulation, InsertionResult(insertionDisposition, insertionStats, insertionTriangulation, insertionVertex), Triangulation)
+import Moonlight.Planar.BuildStats (BuildMetric (InputPoints, UniquePoints, ExistingPoints, DuplicatePoints, LocationWalkSteps, LocationFallbacks, LocationMaxWalk), buildStat)
+import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, randomPoints )
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testIncrementalLocationDescent
+    , testPersistentInsertionReusesFrozenLocation
+    , testVertexSeededSessionInsertion
+    , testPointLocationAndHints
+    , testHierarchyNestingLaw
+    ]
+
+data IncrementalLocationEvidence = IncrementalLocationEvidence
+  { incrementalTriangulation :: !(DelaunayTriangulation (Point))
+  , incrementalWalkSteps :: {-# UNPACK #-} !Int
+  , incrementalFallbacks :: {-# UNPACK #-} !Int
+  }
+
+testIncrementalLocationDescent :: IO ()
+testIncrementalLocationDescent = do
+  small <- collectIncrementalLocationEvidence 500
+  large <- collectIncrementalLocationEvidence 1000
+  assertEqual "incremental location fallbacks/500" 0 (incrementalFallbacks small)
+  assertEqual "incremental location fallbacks/1000" 0 (incrementalFallbacks large)
+  unless (2 * incrementalWalkSteps large < 7 * incrementalWalkSteps small) $
+    fail
+      ( "incremental location approached quadratic growth: "
+          <> show (incrementalWalkSteps small, incrementalWalkSteps large)
+      )
+  assertValid "incremental location descent/1000" (incrementalTriangulation large)
+
+collectIncrementalLocationEvidence :: Int -> IO IncrementalLocationEvidence
+collectIncrementalLocationEvidence count =
+  V.foldM' insertAndAccumulate initialEvidence (V.fromList (randomPoints 0xc1ac_10ca count))
+ where
+  initialEvidence =
+    IncrementalLocationEvidence
+      { incrementalTriangulation = empty unitElementDefaults
+      , incrementalWalkSteps = 0
+      , incrementalFallbacks = 0
+      }
+
+  insertAndAccumulate evidence point = do
+    result <- requireRight "incremental location descent" (insert (incrementalTriangulation evidence) point)
+    let stats = insertionStats result
+    pure
+      IncrementalLocationEvidence
+        { incrementalTriangulation = insertionTriangulation result
+        , incrementalWalkSteps = incrementalWalkSteps evidence + (buildStat LocationWalkSteps) stats
+        , incrementalFallbacks = incrementalFallbacks evidence + (buildStat LocationFallbacks) stats
+        }
+
+-- A persistent insertion locates on the frozen mesh before opening its dense
+-- transaction. The thaw preserves every extant handle, so a lawful frozen
+-- location can be interpreted directly without a second mutable walk. A
+-- degenerate-line outside witness lacks the terminal-edge evidence its mutable
+-- interpreter requires, so that one stratum deliberately retains the mutable
+-- fallback. Exercise every frozen stratum, including the singleton's edge-less
+-- outside witness, rather than testing only the ordinary face case.
+testPersistentInsertionReusesFrozenLocation :: IO ()
+testPersistentInsertionReusesFrozenLocation = do
+  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)
+  emptyLocation <- assertPersistentInsertionFromFrozenLocation "empty" Inserted vacant (Point 0 0)
+  assertEqual "empty insertion frozen location" EmptyTriangulation emptyLocation
+
+  singletonBuild <- requirePointBuild "singleton frozen location" [Point 0 0]
+  singletonLocation <-
+    assertPersistentInsertionFromFrozenLocation
+      "singleton outside insertion"
+      Inserted
+      (buildTriangulation singletonBuild)
+      (Point 2 0)
+  assertEqual "singleton insertion frozen location" (OutsideConvexHull Nothing) singletonLocation
+
+  lineBuild <- requirePointBuild "line frozen locations" [Point 0 0, Point 2 0, Point 4 0]
+  let line = buildTriangulation lineBuild
+  lineEdgeLocation <- assertPersistentInsertionFromFrozenLocation "line edge insertion" Inserted line (Point 1 0)
+  case lineEdgeLocation of
+    OnEdge _ -> pure ()
+    other -> fail ("line edge insertion located " <> show other)
+  lineOutsideLocation <- assertPersistentInsertionFromFrozenLocation "line extension" Inserted line (Point 6 0)
+  case lineOutsideLocation of
+    OutsideConvexHull (Just _) -> pure ()
+    other -> fail ("line extension located " <> show other)
+
+  triangleBuild <- requirePointBuild "area frozen locations" [Point 0 0, Point 4 0, Point 0 4]
+  let triangle = buildTriangulation triangleBuild
+  faceLocation <- assertPersistentInsertionFromFrozenLocation "face insertion" Inserted triangle (Point 1 1)
+  case faceLocation of
+    InFace _ -> pure ()
+    other -> fail ("face insertion located " <> show other)
+  edgeLocation <- assertPersistentInsertionFromFrozenLocation "area edge insertion" Inserted triangle (Point 2 0)
+  case edgeLocation of
+    OnEdge _ -> pure ()
+    other -> fail ("area edge insertion located " <> show other)
+  hullLocation <- assertPersistentInsertionFromFrozenLocation "hull insertion" Inserted triangle (Point 5 1)
+  case hullLocation of
+    OutsideConvexHull (Just _) -> pure ()
+    other -> fail ("hull insertion located " <> show other)
+  duplicateLocation <- assertPersistentInsertionFromFrozenLocation "duplicate insertion" AlreadyPresent triangle (Point 0 0)
+  assertEqual "duplicate insertion frozen location" (OnVertex (VertexId 0)) duplicateLocation
+
+assertPersistentInsertionFromFrozenLocation
+  :: String
+  -> InsertionDisposition
+  -> DelaunayTriangulation (Point)
+  -> Point
+  -> IO Location
+assertPersistentInsertionFromFrozenLocation label expectedDisposition source point = do
+  query <- requireQueryPoint (label <> " frozen query") point
+  let (located, walked) = locatePointWithHint source Nothing query
+      sourceVertices = numVertices source
+      usesMutableFallback =
+        case located of
+          OutsideConvexHull (Just _) -> numInnerFaces source == 0
+          _ -> False
+  result <- requireRight (label <> " insert") (insert source point)
+  ((referenceVertex, referenceDisposition), reference, _) <-
+    requireRight (label <> " session reference") $
+      withSession source 1 (insertVertexAt point point)
+  let stats = insertionStats result
+      expectedVertices =
+        case expectedDisposition of
+          Inserted -> sourceVertices + 1
+          AlreadyPresent -> sourceVertices
+      expectedUnique =
+        case expectedDisposition of
+          Inserted -> 1
+          AlreadyPresent -> 0
+      expectedExisting =
+        case expectedDisposition of
+          Inserted -> 0
+          AlreadyPresent -> 1
+  assertEqual (label <> " disposition") expectedDisposition (insertionDisposition result)
+  assertEqual (label <> " session disposition") referenceDisposition (insertionDisposition result)
+  assertEqual (label <> " session vertex") referenceVertex (insertionVertex result)
+  assertEqual
+    (label <> " exact-location topology matches session")
+    (edgeKeys reference)
+    (edgeKeys (insertionTriangulation result))
+  assertEqual (label <> " source remains unchanged") sourceVertices (numVertices source)
+  assertEqual (label <> " result vertex count") expectedVertices (numVertices (insertionTriangulation result))
+  assertEqual (label <> " input count") 1 ((buildStat InputPoints) stats)
+  assertEqual (label <> " unique count") expectedUnique ((buildStat UniquePoints) stats)
+  assertEqual (label <> " existing count") expectedExisting ((buildStat ExistingPoints) stats)
+  assertEqual (label <> " duplicate count") expectedExisting ((buildStat DuplicatePoints) stats)
+  if usesMutableFallback
+    then do
+      let assertAtLeast
+            :: String
+            -> Int
+            -> Int
+            -> IO ()
+          assertAtLeast counter expected actual =
+            unless
+              (actual >= expected)
+              ( fail
+                  ( label
+                      <> " "
+                      <> counter
+                      <> " includes frozen evidence: expected at least "
+                      <> show expected
+                      <> ", got "
+                      <> show actual
+                  )
+              )
+      assertAtLeast "walk steps" (locationWalkSteps walked) ((buildStat LocationWalkSteps) stats)
+      assertAtLeast "walk maximum" (locationWalkSteps walked) ((buildStat LocationMaxWalk) stats)
+      assertAtLeast
+        "fallback count"
+        (if locationUsedFallback walked then 1 else 0)
+        ((buildStat LocationFallbacks) stats)
+    else do
+      assertEqual (label <> " frozen walk steps") (locationWalkSteps walked) ((buildStat LocationWalkSteps) stats)
+      assertEqual (label <> " frozen walk maximum") (locationWalkSteps walked) ((buildStat LocationMaxWalk) stats)
+      assertEqual
+        (label <> " frozen fallback count")
+        (if locationUsedFallback walked then 1 else 0)
+        ((buildStat LocationFallbacks) stats)
+  assertValid (label <> " result") (insertionTriangulation result)
+  pure located
+
+-- A vertex hint restricts to one incident face and then relinquishes
+-- authority to the exact mutable walk. Valid, stale, occupied, and face-less
+-- local sections must therefore glue to the same published result as the
+-- unhinted entrance.
+testVertexSeededSessionInsertion :: IO ()
+testVertexSeededSessionInsertion = do
+  area <- sampleMesh "vertex-seeded area" [Point 0 0, Point 4 0, Point 0 4]
+  seed <- vertexAt "vertex-seeded area seed" area (Point 0 0)
+  assertVertexSeededInsertionAgreement
+    "vertex-seeded interior insertion"
+    area
+    seed
+    (SampleVertex (Point 1 1) 40)
+  assertVertexSeededInsertionAgreement
+    "vertex-seeded invalid hint"
+    area
+    (VertexId maxBound)
+    (SampleVertex (Point 3 3) 41)
+  assertVertexSeededInsertionAgreement
+    "vertex-seeded duplicate"
+    area
+    seed
+    (SampleVertex (Point 4 0) 42)
+
+  line <- sampleMesh "vertex-seeded line" [Point 0 0, Point 2 0, Point 4 0]
+  lineSeed <- vertexAt "vertex-seeded line seed" line (Point 2 0)
+  assertVertexSeededInsertionAgreement
+    "vertex-seeded face-less fallback"
+    line
+    lineSeed
+    (SampleVertex (Point 3 0) 43)
+ where
+  sampleMesh
+    :: String
+    -> [Point]
+    -> IO (Triangulation 'Unconstrained SampleVertex () () ())
+  sampleMesh label points = do
+    built <-
+      requireRight label $
+        delaunay
+          unitElementDefaults
+          (V.imap (\index point -> SampleVertex point index) (V.fromList points))
+    pure (buildTriangulation built)
+
+  vertexAt
+    :: String
+    -> Triangulation 'Unconstrained SampleVertex () () ()
+    -> Point
+    -> IO VertexId
+  vertexAt label triangulation point = do
+    query <- requireQueryPoint label point
+    case locatePoint triangulation query of
+      OnVertex vertex -> pure vertex
+      location -> fail (label <> " did not locate a vertex: " <> show location)
+
+assertVertexSeededInsertionAgreement
+  :: String
+  -> Triangulation 'Unconstrained SampleVertex () () ()
+  -> VertexId
+  -> SampleVertex
+  -> IO ()
+assertVertexSeededInsertionAgreement label source seed payload = do
+  ((referenceVertex, referenceDisposition), reference, _) <-
+    requireRight (label <> " unhinted") $
+      withSession source 1 (insertVertexAt (samplePosition payload) payload)
+  ((hintedVertex, hintedDisposition), hinted, _) <-
+    requireRight (label <> " hinted") $
+      withSession source 1 (insertVertexAtNearVertex seed (samplePosition payload) payload)
+  assertEqual (label <> " vertex") referenceVertex hintedVertex
+  assertEqual (label <> " disposition") referenceDisposition hintedDisposition
+  assertEqual (label <> " topology") (edgeKeys reference) (edgeKeys hinted)
+  assertEqual (label <> " payload") payload (vertexData hinted hintedVertex)
+  assertValid label hinted
+
+-- Circle sweep must be a construction schedule, not a second topology. It is
+-- compared against the arrival-order session kernel on the same exact inputs.
+-- | One session, both verbs. The reason the two published sessions became one:
+-- a caller who removes and inserts had to thaw twice and pay the O(n)
+-- publication a session exists to delete.
+--
+-- Graded against the oracle that needs no second implementation — the Delaunay
+-- triangulation of a point set in general position is unique, so a mixed edit
+-- must land exactly where a fresh bulk load of the surviving set lands.
+testPointLocationAndHints :: IO ()
+testPointLocationAndHints = do
+  testTriangleLocationPriority
+  built <- requirePointBuild "location" (randomPoints 0x1234_5678 1200)
+  let triangulation = buildTriangulation built
+  queries <- traverse (requireQueryPoint "location query") (take 250 (randomPoints 0xdead_beef 250))
+  let
+      baseline = sum [locationWalkSteps stats | query <- queries, let (_, stats) = locatePointWithHint triangulation Nothing query]
+  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint 16 triangulation)
+  let hinted = sum
+        [ locationWalkSteps stats
+        | query <- queries
+        , let hint = hierarchyHint hierarchy query
+              (_, stats) = locatePointWithHint triangulation hint query
+        ]
+  unless (hinted <= baseline) $
+    fail ("hierarchy hint increased aggregate walking: " <> show (baseline, hinted))
+  staleHintQuery <- requireQueryPoint "stale hint query" (Point 0.125 (-0.375))
+  let staleVertexHint = VertexHint (VertexId (fromIntegral (numVertices triangulation)))
+  assertEqual
+    "stale vertex hint falls back to the canonical start face"
+    (locatePointWithHint triangulation Nothing staleHintQuery)
+    (locatePointWithHint triangulation (Just staleVertexHint) staleHintQuery)
+  forM_ (vertices triangulation) $ \vertex -> do
+    vertexQuery <- requireQueryPoint "vertex lookup" (vertexPoint triangulation vertex)
+    assertEqual "vertex lookup" (OnVertex vertex) (locatePoint triangulation vertexQuery)
+  let insertedPoint = Point 0.1234567 (-0.2345678)
+  inserted <- requireRight "hierarchy incremental source" (insert triangulation insertedPoint)
+  let updatedTriangulation = insertionTriangulation inserted
+  updatedHierarchy <-
+    requireRight
+      "hierarchy incremental update"
+      ( updateHierarchyAfterInsertion
+          hierarchy
+          insertedPoint
+          (insertionVertex inserted)
+          (insertionDisposition inserted)
+      )
+  rebuiltHierarchy <- requireRight "hierarchy reference rebuild" (buildHierarchyHint 16 updatedTriangulation)
+  assertEqual "incremental hierarchy equals canonical rebuild" rebuiltHierarchy updatedHierarchy
+
+-- The immutable triangle already owns edge cyclicity. Pin early boundary hits
+-- and the LAST crossing when a corner query lies beyond two incident edges.
+testTriangleLocationPriority :: IO ()
+testTriangleLocationPriority = do
+  built <- requirePointBuild "location priority triangle" [Point 0 0, Point 4 0, Point 0 4]
+  let mesh = buildTriangulation built
+  traverse_
+    (\face -> case innerFaceDirectedEdges mesh face of
+      Nothing -> fail "location priority fixture lost its triangular face"
+      Just (e0, e1, e2) -> do
+        let v0 = origin mesh e0
+            v1 = origin mesh e1
+            v2 = origin mesh e2
+            p0 = vertexPoint mesh v0
+            p1 = vertexPoint mesh v1
+            p2 = vertexPoint mesh v2
+            hint = Just (FaceHint face)
+            check label expected point = do
+              query <- requireQueryPoint label point
+              assertEqual label (expected, LocationStats 1 False) (locatePointWithHint mesh hint query)
+            beyondCorner (Point ax ay) (Point bx by) (Point cx cy) = Point (3 * ax - bx - cx) (3 * ay - by - cy)
+        traverse_ (\(vertex, point) -> check "triangle vertex priority" (OnVertex vertex) point) [(v0, p0), (v1, p1), (v2, p2)]
+        traverse_ (\(edge, edgeFrom, edgeTo) -> check "triangle edge priority" (OnEdge edge) (midpoint edgeFrom edgeTo)) [(e0, p0, p1), (e1, p1, p2), (e2, p2, p0)]
+        traverse_
+          (\(lastCrossing, cornerPoint, nextPoint, previousPoint) -> check "triangle last crossing" (OutsideConvexHull (Just (reverseEdge lastCrossing))) (beyondCorner cornerPoint nextPoint previousPoint))
+          [(e2, p0, p1, p2), (e1, p1, p2, p0), (e2, p2, p0, p1)])
+    (innerFaces mesh)
+
+-- The hierarchy replaces its level-to-base correspondence with the arithmetic
+-- claim that level-local handle @j@ names base vertex @j * branch@. Query each
+-- sampled vertex with its own position: the descent must return that very
+-- vertex, at every branch factor and along the whole of level 0. A stride the
+-- construction does not actually obey shows up here as a named mismatch.
+testHierarchyNestingLaw :: IO ()
+testHierarchyNestingLaw = do
+  built <- requirePointBuild "hierarchy nesting" (randomPoints 0x0f1e_2d3c 900)
+  let triangulation = buildTriangulation built
+  forM_ [2, 3, 16] $ \branch -> do
+    hierarchy <- requireRight ("hierarchy build at branch " <> show branch) (buildHierarchyHint branch triangulation)
+    let sampled = [0, branch .. numVertices triangulation - 1]
+    forM_ sampled $ \index -> do
+      let vertex = VertexId (fromIntegral index)
+      vertexQuery <- requireQueryPoint "sampled hierarchy vertex" (vertexPoint triangulation vertex)
+      assertEqual
+        ("branch " <> show branch <> " descent onto sampled vertex " <> show index)
+        (Just (VertexHint vertex))
+        (hierarchyHint hierarchy vertexQuery)
diff --git a/test/native/Moonlight/Planar/NearestSpec.hs b/test/native/Moonlight/Planar/NearestSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/NearestSpec.hs
@@ -0,0 +1,264 @@
+-- | Exact nearest-site selection against an independent rational argmin.
+module Moonlight.Planar.NearestSpec
+  ( tests
+  ) where
+
+import Control.Monad ( when )
+import Data.Foldable ( traverse_ )
+import Data.Primitive.PrimArray ( indexPrimArray, primArrayFromList, sizeofPrimArray )
+import Moonlight.Planar.BulkLoad ( delaunay )
+import Moonlight.Planar.Cdt ( constrainedDelaunay, fromDelaunay )
+import Moonlight.Planar.Dcel ( destination, innerFaceVertexTriples, isConstraintEdge,
+  numConstraints, numVertices, undirectedEndpoints, vertexOutgoingEdges, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( undirectedEdges, vertices )
+import Moonlight.Planar.Internal.HandleDefs ( VertexId(VertexId) )
+import Moonlight.Planar.Interpolation ( nearestNeighbor )
+import Moonlight.Planar.Point (canonicalPoint)
+import Moonlight.Planar.Math (squaredDistance)
+import Moonlight.Planar.MeshFixtures ( requirePointBuild, pointKey, edgeKeys )
+import Moonlight.Planar.Scalar ( maximumAllowedCoordinate, minimumAllowedCoordinate )
+import Moonlight.Planar.Point (QueryPoint, Point(Point), queryPointValue)
+import Moonlight.Planar.Types (unitElementDefaults, NearestStats(nearestDistanceTests, nearestWalkSteps), BuildResult(buildTriangulation,
+  buildInputVertices), Triangulation)
+import Support ( assertEqual, assertValid, requireQueryPoint, requireRight, requireJust )
+import Moonlight.Planar.Internal.Predicates qualified as Admitted
+import qualified Data.List as List
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testWideDistanceNearestNeighbor
+    , testExactNearestNeighbor
+    ]
+
+testWideDistanceNearestNeighbor :: IO ()
+testWideDistanceNearestNeighbor = do
+  let largePoints = V.fromList
+        [ Point 0 0
+        , Point 1.0e20 0
+        , Point 2.0e20 0
+        , Point 1.0e20 1.0e19
+        ] :: V.Vector (Point)
+  largeBuild <- requireRight "wide-distance build" (delaunay unitElementDefaults largePoints)
+  let largeTriangulation = buildTriangulation largeBuild
+  assertValid "wide-distance triangulation" largeTriangulation
+  query <- requireQueryPoint "wide-distance query" (Point 1.05e20 0)
+  case nearestNeighbor largeTriangulation Nothing query of
+    Nothing -> fail "wide-distance nearest-neighbor returned Nothing"
+    Just (nearest, _) ->
+      assertEqual "wide-distance nearest-neighbor" (Point 1.0e20 0) (vertexPoint largeTriangulation nearest)
+  assertNearestMatchesExactArgmin "wide-distance" largeTriangulation query
+
+testExactNearestNeighbor :: IO ()
+testExactNearestNeighbor = do
+  traverse_ checkConstrainedPermutation (List.permutations strictSites)
+  traverse_ checkCocircularPermutation (List.permutations squareSites)
+  tieBuild <- requirePointBuild "nearest handle trap" [squareD, squareB, squareA, squareC]
+  assertNearestInputMapping "nearest handle trap" [squareD, squareB, squareA, squareC] tieBuild
+  assertEqual "nearest handle trap retains D=0 B=1 A=2 C=3"
+    (primArrayFromList [0, 1, 2, 3]) (buildInputVertices tieBuild)
+  let tieMesh = buildTriangulation tieBuild
+      tieSeed = VertexId (indexPrimArray (buildInputVertices tieBuild) 1)
+  assertEqual "nearest handle trap retained AC" True (Set.member (pointKey squareA, pointKey squareC) (edgeKeys tieMesh))
+  assertEqual "nearest handle trap B sees only larger handles"
+    (Set.fromList [VertexId 2, VertexId 3])
+    (Set.fromList (fmap (destination tieMesh) (vertexOutgoingEdges tieMesh tieSeed)))
+  centerQuery <- requireQueryPoint "nearest handle trap center" (Point 1 1)
+  assertNearestMatchesExactArgmin "nearest handle trap" tieMesh centerQuery
+  traverse_ checkRoundedPermutation (List.permutations [roundedFarther, roundedNearer])
+  traverse_ checkDomainFixture domainFixtures
+  traverse_ checkIntegerRing
+    [ integerRing
+    , reverse integerRing
+    , drop 3 integerRing <> take 3 integerRing
+    ]
+  hullFrom <- requireJust "nearest hull constraint from input" (List.elemIndex strictA strictSites)
+  hullTo <- requireJust "nearest hull constraint to input" (List.elemIndex strictB strictSites)
+  hullBuild <- requireRight "nearest hull constraint build" $
+    constrainedDelaunay unitElementDefaults (V.fromList strictSites) (V.singleton (hullFrom, hullTo))
+  let hullMesh = buildTriangulation hullBuild
+  assertEqual "nearest hull constraint admitted" 1 (numConstraints hullMesh)
+  strictQuery <- requireQueryPoint "nearest hull constraint query" (Point 17 12)
+  assertNearestMatchesExactArgmin "nearest hull constraint" hullMesh strictQuery
+ where
+  strictA = Point 0 0
+  strictB = Point 32 0
+  strictC = Point 32 32
+  strictD = Point 0 8
+  strictSites = [strictA, strictB, strictC, strictD]
+  squareA = Point 0 0
+  squareB = Point 2 0
+  squareC = Point 2 2
+  squareD = Point 0 2
+  squareSites = [squareA, squareB, squareC, squareD]
+  roundedFarther = Point 1 (encodeFloat 1 (-27))
+  roundedNearer = Point 1 0
+
+  checkConstrainedPermutation points = do
+    fromInput <- requireJust "nearest constrained from input" (List.elemIndex strictA points)
+    toInput <- requireJust "nearest constrained to input" (List.elemIndex strictC points)
+    built <- requireRight "nearest constrained trap build" $
+      constrainedDelaunay unitElementDefaults (V.fromList points) (V.singleton (fromInput, toInput))
+    assertNearestInputMapping "nearest constrained trap" points built
+    let mesh = buildTriangulation built
+        fromVertex = VertexId (indexPrimArray (buildInputVertices built) fromInput)
+        toVertex = VertexId (indexPrimArray (buildInputVertices built) toInput)
+        retainedConstraint =
+          any
+            (\edge ->
+              isConstraintEdge mesh edge
+                && uncurry (\from to -> Set.fromList [from, to] == Set.fromList [fromVertex, toVertex])
+                  (undirectedEndpoints mesh edge)
+            )
+            (undirectedEdges mesh)
+        triangleSupports =
+          Set.fromList
+            (fmap (\(a, b, c) -> Set.fromList (fmap (vertexPoint mesh) [a, b, c]))
+              (V.toList (innerFaceVertexTriples mesh)))
+    assertValid "nearest constrained trap" mesh
+    assertEqual "nearest constrained trap admitted AC" (1, True) (numConstraints mesh, retainedConstraint)
+    assertEqual "nearest constrained trap retained exactly ABC and ACD"
+      (Set.fromList [Set.fromList [strictA, strictB, strictC], Set.fromList [strictA, strictC, strictD]])
+      triangleSupports
+    query <- requireQueryPoint "nearest constrained trap query" (Point 17 12)
+    assertEqual "nearest constrained trap exact distances"
+      [433, 369, 625, 305] (fmap (rationalSquaredDistance (queryPointValue query)) strictSites)
+    seedInput <- requireJust "nearest constrained B input" (List.elemIndex strictB points)
+    let seed = VertexId (indexPrimArray (buildInputVertices built) seedInput)
+    assertEqual "nearest constrained B has no closer neighbour" True $
+      all
+        ((> rationalSquaredDistance (queryPointValue query) strictB)
+          . rationalSquaredDistance (queryPointValue query)
+          . vertexPoint mesh
+          . destination mesh)
+        (vertexOutgoingEdges mesh seed)
+    case nearestNeighbor mesh (Just seed) query of
+      Nothing -> fail "nearest constrained trap returned Nothing"
+      Just (nearest, _) -> assertEqual "nearest constrained trap returns D" strictD (vertexPoint mesh nearest)
+    assertNearestMatchesExactArgmin "nearest constrained trap" mesh query
+
+  checkCocircularPermutation points = do
+    built <- requirePointBuild "nearest cocircular square" points
+    assertNearestInputMapping "nearest cocircular square" points built
+    let mesh = buildTriangulation built
+    assertValid "nearest cocircular square" mesh
+    assertEqual "nearest cocircular square retains canonical AC" True (Set.member (pointKey squareA, pointKey squareC) (edgeKeys mesh))
+    assertEqual "nearest cocircular square excludes BD" False (Set.member (pointKey squareD, pointKey squareB) (edgeKeys mesh))
+    query <- requireQueryPoint "nearest cocircular square center" (Point 1 1)
+    assertNearestMatchesExactArgmin "nearest cocircular square" mesh query
+
+  checkRoundedPermutation points = do
+    built <- requirePointBuild "nearest rounded trap" points
+    assertNearestInputMapping "nearest rounded trap" points built
+    query <- requireQueryPoint "nearest rounded trap query" (Point 0 0)
+    _ <- traverse (requireQueryPoint "nearest rounded trap admitted site") points
+    assertEqual "nearest rounded trap really rounds to a tie"
+      (squaredDistance (queryPointValue query) roundedFarther)
+      (squaredDistance (queryPointValue query) roundedNearer)
+    assertEqual "nearest rounded trap exact gap" (toRational (encodeFloat 1 (-54) :: Double))
+      (rationalSquaredDistance (queryPointValue query) roundedFarther
+        - rationalSquaredDistance (queryPointValue query) roundedNearer)
+    let mesh = buildTriangulation built
+    fartherInput <- requireJust "nearest rounded farther input" (List.elemIndex roundedFarther points)
+    let fartherVertex = VertexId (indexPrimArray (buildInputVertices built) fartherInput)
+    case nearestNeighbor mesh (Just fartherVertex) query of
+      Nothing -> fail "nearest rounded trap returned Nothing"
+      Just (nearest, _) -> assertEqual "nearest rounded trap returns genuinely nearer site" roundedNearer (vertexPoint mesh nearest)
+    assertNearestMatchesExactArgmin "nearest rounded trap" mesh query
+
+  checkDomainFixture (label, points, rawQuery) = do
+    built <- requirePointBuild label points
+    assertNearestInputMapping label points built
+    query <- requireQueryPoint (label <> " query") rawQuery
+    assertNearestMatchesExactArgmin label (buildTriangulation built) query
+    assertNearestMatchesExactArgmin (label <> " promoted without constraints")
+      (fromDelaunay (buildTriangulation built)) query
+
+  domainFixtures =
+    [ ("nearest empty", [], Point 0 0)
+    , ("nearest singleton", [Point 3 4], Point (-1) 2)
+    , ("nearest two-site tie", [Point (-1) 0, Point 1 0], Point 0 0)
+    , ("nearest collinear tie", fmap (\x -> Point x 0) [2, -2, 1, -1, 0], Point 0.5 1)
+    , ("nearest signed-zero admission", [Point (-0.0) 0, Point 0 (-0.0), Point 1 0], Point (-0.0) (-0.0))
+    , ("nearest one-dyadic-unit separation",
+        [Point 1 (encodeFloat 1 (-27) + encodeFloat 1 (-79)), roundedFarther], Point 0 0)
+    , ("nearest admitted maximum exponent", [Point large 0, Point (-large) 0, Point 0 large], Point 0 0)
+    , ("nearest admitted minimum exponent", [Point small 0, Point (-small) 0, Point 0 small], Point 0 0)
+    , ("nearest mixed admitted exponents", [Point large small, Point large 0], Point 0 0)
+    ]
+  large = maximumAllowedCoordinate
+  small = minimumAllowedCoordinate
+  integerRing =
+    [ Point 5 0, Point 4 3, Point 3 4, Point 0 5
+    , Point (-3) 4, Point (-4) 3, Point (-5) 0, Point (-4) (-3)
+    , Point (-3) (-4), Point 0 (-5), Point 3 (-4), Point 4 (-3)
+    ]
+  checkIntegerRing points = do
+    built <- requirePointBuild "nearest integer cocircular ring" points
+    query <- requireQueryPoint "nearest integer cocircular ring center" (Point 0 0)
+    assertNearestMatchesExactArgmin "nearest integer cocircular ring" (buildTriangulation built) query
+
+assertNearestInputMapping
+  :: String
+  -> [Point]
+  -> BuildResult mode Point () () ()
+  -> IO ()
+assertNearestInputMapping label points built = do
+  let mapping = buildInputVertices built
+      mesh = buildTriangulation built
+  assertEqual (label <> " input mapping cardinality") (length points) (sizeofPrimArray mapping)
+  traverse_
+    (\(index, point) ->
+      assertEqual (label <> " input mapping coordinates") (canonicalPoint point)
+        (vertexPoint mesh (VertexId (indexPrimArray mapping index)))
+    )
+    (zip [0 ..] points)
+
+-- Independent oracle: construct Rational distances and sort their values,
+-- rather than reusing the production dyadic difference comparator or argmin.
+rationalSquaredDistance :: Point -> Point -> Rational
+rationalSquaredDistance (Point qx qy) (Point px py) =
+  let dx = toRational px - toRational qx
+      dy = toRational py - toRational qy
+   in dx * dx + dy * dy
+
+assertNearestMatchesExactArgmin
+  :: String
+  -> Triangulation mode vertex directed undirected face
+  -> QueryPoint
+  -> IO ()
+assertNearestMatchesExactArgmin label mesh query = do
+  let residentSites = vertices mesh
+      queryValue = queryPointValue query
+      exactKey vertex = (rationalSquaredDistance queryValue (vertexPoint mesh vertex), vertex)
+      expected = case List.sortOn exactKey residentSites of
+        [] -> Nothing
+        nearest : _ -> Just nearest
+      hints = Nothing : fmap Just residentSites <> [Just (VertexId maxBound)]
+  traverse_
+    (\hint -> do
+      let result = nearestNeighbor mesh hint query
+      assertEqual (label <> " exact argmin from " <> show hint) expected (fmap fst result)
+      case result of
+        Nothing -> pure ()
+        Just (_, stats) -> do
+          assertEqual (label <> " exact descent is well-founded") True
+            (nearestWalkSteps stats < numVertices mesh)
+          when (numConstraints mesh > 0) $
+            assertEqual (label <> " constrained global comparison count") (numVertices mesh - 1)
+              (nearestDistanceTests stats)
+    )
+    hints
+  traverse_
+    (\firstSite -> traverse_
+      (\secondSite ->
+        assertEqual (label <> " exact comparator")
+          (compare (fst (exactKey firstSite)) (fst (exactKey secondSite)))
+          (Admitted.compareSquaredDistances queryValue (vertexPoint mesh firstSite) (vertexPoint mesh secondSite))
+      )
+      residentSites
+    )
+    residentSites
diff --git a/test/native/Moonlight/Planar/OverlayAssertions.hs b/test/native/Moonlight/Planar/OverlayAssertions.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/OverlayAssertions.hs
@@ -0,0 +1,117 @@
+-- | Shared correspondence laws for exact overlay incidence, receipts, and provenance.
+module Moonlight.Planar.OverlayAssertions
+  ( assertOverlayIntegrity
+  , unboundedLoopCount
+  ) where
+
+import Control.Monad ( unless )
+import Moonlight.Planar.Exact ( SegmentRelation(SegmentsShareEndpoint), exactSegment,
+  exactSegmentEndpoints, ExactSegment )
+import Moonlight.Planar.Internal.ExactSegmentEvents ( exactSegmentEventPlan,
+  exactSegmentRelationMap, ExactSweepSegmentId(..) )
+import Moonlight.Planar.Internal.HandleDefs ( FaceId(..) )
+import Moonlight.Planar.Internal.Incidence ( faceBoundaryComponents, incidenceEdgeCount,
+  incidenceFaceCount, incidenceUndirectedEndpoints, incidenceVertexCount )
+import Moonlight.Planar.Internal.Overlay.Types ( overlayResultIncidence )
+import Moonlight.Planar.Overlay ( OverlayEdgeOrigin(overlayEdgeSources),
+  OverlayReceipt(overlayProvenanceIncidences, overlayAtomicEdges, overlayOutputVertices,
+  overlayArrangementCells, overlayBoundaryOrbits), OverlayResult, OverlayVertex(overlayVertexOrigin,
+  overlayExactPoint), OverlayVertexOrigin(..), overlayArrangementEdges, overlayArrangementVertices,
+  overlayCells, overlayReceipt )
+import Support ( assertEqual, requireRight )
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+assertOverlayIntegrity
+  :: String
+  -> OverlayResult labels
+  -> IO ()
+assertOverlayIntegrity label result = do
+  let incidence = overlayResultIncidence result
+      boundaryEdges = overlayArrangementEdges result
+      cellIds = Set.fromList (map fst (overlayCells result))
+      exactVertices = overlayArrangementVertices result
+      pointTable = Map.fromList (fmap (fmap overlayExactPoint) exactVertices)
+      receipt = overlayReceipt result
+  assertEqual
+    (label <> " atomic incidence correspondence")
+    (overlayAtomicEdges receipt)
+    (length boundaryEdges)
+  assertEqual (label <> " incidence cardinalities match exact payload planes")
+    (length exactVertices, length boundaryEdges, length (overlayCells result))
+    (incidenceVertexCount incidence, incidenceEdgeCount incidence, incidenceFaceCount incidence)
+  assertEqual (label <> " all faces including the single outside face are labelled")
+    (Set.fromList [FaceId (fromIntegral index) | index <- [0 .. incidenceFaceCount incidence - 1]]) cellIds
+  assertEqual (label <> " exact receipt face and vertex counts")
+    (length exactVertices, length (overlayCells result))
+    (overlayOutputVertices receipt, overlayArrangementCells receipt)
+  assertEqual (label <> " receipt counts raw boundary orbits, not simple polygon loops")
+    (sum (fmap (length . faceBoundaryComponents incidence . fst) (overlayCells result)))
+    (overlayBoundaryOrbits receipt)
+  unless
+    ( all
+        (vertexOriginIsNonEmpty . overlayVertexOrigin . snd)
+        (overlayArrangementVertices result)
+    )
+    (fail (label <> ": exact vertex lost all typed origins"))
+  let originCount =
+        sum (fmap (NonEmpty.length . overlayEdgeSources . snd) boundaryEdges)
+          + sum (fmap (\(_, vertex) -> let origin = overlayVertexOrigin vertex
+                                       in length (overlayOriginVertices origin) + length (overlayOriginEdges origin)) exactVertices)
+  assertEqual (label <> " all retained source incidences are charged") originCount (overlayProvenanceIncidences receipt)
+  atomicSegments <-
+    traverse
+      (\(edge, _) -> do
+         let (fromVertex, toVertex) = incidenceUndirectedEndpoints incidence edge
+         fromPoint <- maybe (fail (label <> " missing atomic source")) pure (Map.lookup fromVertex pointTable)
+         toPoint <- maybe (fail (label <> " missing atomic destination")) pure (Map.lookup toVertex pointTable)
+         requireRight
+               (label <> " atomic segment")
+               (exactSegment fromPoint toPoint))
+      boundaryEdges
+  atomicPlan <-
+    requireRight
+      (label <> " atomic endpoint-incidence proof")
+      (exactSegmentEventPlan (V.fromList atomicSegments))
+  assertEqual
+    (label <> " atomics have only endpoint-incidence relations")
+    (endpointIncidenceRelations atomicSegments)
+    (exactSegmentRelationMap atomicPlan)
+
+endpointIncidenceRelations
+  :: [ExactSegment]
+  -> Map.Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
+endpointIncidenceRelations segments =
+  Map.fromList
+    [ ( (ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex)
+      , SegmentsShareEndpoint
+      )
+    | (leftIndex, left) <- zip [0 :: Int ..] segments
+    , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) segments)
+    , let (leftFrom, leftTo) = exactSegmentEndpoints left
+          (rightFrom, rightTo) = exactSegmentEndpoints right
+    , not
+        ( Set.null
+            ( Set.intersection
+                (Set.fromList [leftFrom, leftTo])
+                (Set.fromList [rightFrom, rightTo])
+            )
+        )
+    ]
+
+vertexOriginIsNonEmpty :: OverlayVertexOrigin -> Bool
+vertexOriginIsNonEmpty origin =
+  not
+    (null (overlayOriginVertices origin) && null (overlayOriginEdges origin))
+
+unboundedLoopCount :: OverlayResult labels -> Maybe Int
+unboundedLoopCount result =
+  case
+    [ length (faceBoundaryComponents (overlayResultIncidence result) face)
+    | (face, _) <- overlayCells result
+    , face == FaceId 0
+    ] of
+    [count] -> Just count
+    _ -> Nothing
diff --git a/test/native/Moonlight/Planar/OverlayPublicationSpec.hs b/test/native/Moonlight/Planar/OverlayPublicationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/OverlayPublicationSpec.hs
@@ -0,0 +1,149 @@
+-- | Grouped polygon publication, boundary walks, and retained source vertices.
+module Moonlight.Planar.OverlayPublicationSpec (tests) where
+
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.Internal.Incidence ( incidenceEdgeCount, incidenceFaceCount,
+  incidenceVertexCount )
+import Moonlight.Planar.Internal.Overlay.Types ( overlayResultIncidence )
+import Moonlight.Planar.Overlay ( OverlayVertex(overlayExactPoint), overlayArrangementVertices,
+  overlayCells, overlayClosedUnion, overlayLayers, overlayPlanarLayer )
+import Moonlight.Planar.OverlayAssertions ( assertOverlayIntegrity, unboundedLoopCount )
+import Moonlight.Planar.OverlayFixtures ( singletonLayer )
+import Moonlight.Planar.Region ( exactLoop, polygonComponent, PolygonComponent, planarLayerRegions,
+  planarLayer, planarRegion, planarRegionComponents )
+import Moonlight.Planar.Valuation ( cellValuations, eulerCharacteristicValue, exactAreaValue,
+  valuationArea, valuationEuler )
+import Support ( assertEqual, integerPoint, rectangleComponent, requireRight, rectangleRegion,
+  annulusRegion )
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testOutsidePairRemainsImplicit
+    , testDisconnectedGroupedPublication
+    , testMultiwayPointBoundaryCycles
+    , testExactBoundaryWithoutDiagonals
+    , testCancelledSeamsRetainIsolatedVertex
+    ]
+
+testOutsidePairRemainsImplicit :: IO ()
+testOutsidePairRemainsImplicit = do
+  region <- annulusRegion (0, 0, 4, 4) (1, 1, 3, 3)
+  left <-
+    requireRight
+      "annulus layer"
+      (planarLayer "outside" (Map.singleton "annulus" region))
+  right <- requireRight "annulus empty layer" (planarLayer "void" Map.empty)
+  result <- requireRight "annulus overlay" (overlayLayers left right)
+  published <- requireRight "publish annulus exact arrangement" (overlayPlanarLayer result)
+  assertEqual
+    "bounded cavities remain represented by the implicit outside pair"
+    Nothing
+    ( Map.lookup
+        ("outside", "void")
+        (planarLayerRegions published)
+    )
+
+testDisconnectedGroupedPublication :: IO ()
+testDisconnectedGroupedPublication = do
+  first <- rectangleComponent 0 0 1 1
+  second <- rectangleComponent 3 0 4 1
+  region <- requireRight "two-island region" (planarRegion [first, second])
+  left <-
+    requireRight
+      "two-island layer"
+      (planarLayer "outside" (Map.singleton "island" region))
+  right <- requireRight "empty right layer" (planarLayer "void" Map.empty)
+  result <- requireRight "two-island overlay" (overlayLayers left right)
+  assertEqual "two islands give two unbounded boundary cycles" (Just 2) (unboundedLoopCount result)
+  published <- requireRight "publish disconnected exact arrangement" (overlayPlanarLayer result)
+  let components =
+        maybe
+          []
+          planarRegionComponents
+          (Map.lookup ("island", "void") (planarLayerRegions published))
+  assertEqual "equal labels group after component descent" 2 (length components)
+  reversedRegion <- requireRight "reversed two-island region" (planarRegion [second, first])
+  reversedLeft <-
+    requireRight
+      "reversed two-island layer"
+      (planarLayer "outside" (Map.singleton "island" reversedRegion))
+  reversedResult <- requireRight "reversed two-island overlay" (overlayLayers reversedLeft right)
+  assertEqual
+    "component construction order cannot perturb stable cells"
+    (overlayCells result)
+    (overlayCells reversedResult)
+
+testMultiwayPointBoundaryCycles :: IO ()
+testMultiwayPointBoundaryCycles = do
+  components <-
+    traverse
+      triangleComponent
+      [ ((0, 0), (2, 0), (1, 1))
+      , ((0, 0), (-1, 1), (-2, 0))
+      , ((0, 0), (-1, -1), (1, -1))
+      ]
+  region <- requireRight "three point-touching components" (planarRegion components)
+  left <-
+    requireRight
+      "three point-touching layer"
+      (planarLayer "outside" (Map.singleton "inside" region))
+  right <- requireRight "empty multiway right layer" (planarLayer "void" Map.empty)
+  result <- requireRight "three-way point-touching overlay" (overlayLayers left right)
+  assertOverlayIntegrity "three-way point-touching" result
+  assertEqual
+    "multiway point contact retains one unsplit outside boundary walk"
+    (Just 1)
+    (unboundedLoopCount result)
+
+testExactBoundaryWithoutDiagonals :: IO ()
+testExactBoundaryWithoutDiagonals = do
+  left <- rectangleRegion 0 0 2 2 >>= singletonLayer "outside" "inside"
+  right <- requireRight "empty exact-boundary layer" (planarLayer "void" Map.empty)
+  result <- requireRight "exact boundary-only arrangement" (overlayLayers left right)
+  assertOverlayIntegrity "exact boundary-only arrangement" result
+  let incidence = overlayResultIncidence result
+  assertEqual
+    "a square arrangement has no invented diagonal or triangular face"
+    (4, 4, 2)
+    (incidenceVertexCount incidence, incidenceEdgeCount incidence, incidenceFaceCount incidence)
+
+testCancelledSeamsRetainIsolatedVertex :: IO ()
+testCancelledSeamsRetainIsolatedVertex = do
+  components <- traverse triangleComponent
+    [ ((0, 0), (2, 0), (1, 1))
+    , ((2, 0), (2, 2), (1, 1))
+    , ((2, 2), (0, 2), (1, 1))
+    , ((0, 2), (0, 0), (1, 1))
+    ]
+  region <- requireRight "four-triangle source fan" (planarRegion components)
+  left <- requireRight "same-label source fan" (planarLayer "outside" (Map.singleton "inside" region))
+  right <- requireRight "empty fan counterpart" (planarLayer "void" Map.empty)
+  result <- requireRight "cancelled source fan seams" (overlayLayers left right)
+  assertOverlayIntegrity "cancelled source fan" result
+  let incidence = overlayResultIncidence result
+  assertEqual "seam cancellation keeps the isolated center provenance vertex"
+    (5, 4, 2)
+    (incidenceVertexCount incidence, incidenceEdgeCount incidence, incidenceFaceCount incidence)
+  assertEqual "source center survives without a manufactured edge" True
+    (Set.member (integerPoint 1 1) (Set.fromList (fmap (overlayExactPoint . snd) (overlayArrangementVertices result))))
+  cells <- requireRight "closed disk with isolated source vertex" (overlayClosedUnion (== "inside") (const False) result)
+  values <- requireRight "isolated-vertex face Euler contribution" (cellValuations cells)
+  assertEqual "punctured open face plus its selected isolated point is a disk"
+    (1, 4) (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))
+
+triangleComponent
+  :: ((Integer, Integer), (Integer, Integer), (Integer, Integer))
+  -> IO PolygonComponent
+triangleComponent (firstPoint, secondPoint, thirdPoint) = do
+  loop <-
+    requireRight
+      "overlay triangle loop"
+      ( exactLoop
+          ( uncurry integerPoint firstPoint
+              :| [uncurry integerPoint secondPoint, uncurry integerPoint thirdPoint]
+          )
+      )
+  requireRight "overlay triangle component" (polygonComponent loop [])
diff --git a/test/native/Moonlight/Planar/OverlaySelectionSpec.hs b/test/native/Moonlight/Planar/OverlaySelectionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/OverlaySelectionSpec.hs
@@ -0,0 +1,213 @@
+-- | Closed selection, exterior marginals, and lower-dimensional incidence.
+module Moonlight.Planar.OverlaySelectionSpec (tests) where
+
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.CellSet ( ExactCellSet, exactCellSetEdgeCount, exactCellSetFaceCount,
+  exactCellSetVertexCount )
+import Moonlight.Planar.Exact ( ExactPoint )
+import Moonlight.Planar.Internal.Incidence ( faceBoundaryComponents )
+import Moonlight.Planar.Internal.Overlay.Types ( overlayResultIncidence )
+import Moonlight.Planar.Overlay ( BoundaryRef(boundaryRefSource), OverlayResult,
+  OverlaySelectionError(OverlaySelectionContainsUnboundedCell),
+  OverlaySelectionKind(RegularizedDifferenceSelection, ClosedUnionSelection,
+  ClosedIntersectionSelection), OverlaySourceId(OverlaySourceId), OverlayVertex(overlayExactPoint,
+  overlayVertexOrigin), OverlayVertexOrigin(..), overlayArrangementVertices, overlayCells,
+  overlayClosedIntersection, overlayClosedUnion, overlayLayers, overlayRegularizedDifference,
+  overlaySelectedRegion )
+import Moonlight.Planar.OverlayAssertions ( assertOverlayIntegrity, unboundedLoopCount )
+import Moonlight.Planar.OverlayFixtures ( singletonLayer )
+import Moonlight.Planar.Region ( exactLoop, polygonComponent, planarLayer, planarRegion,
+  planarRegionComponents )
+import Moonlight.Planar.Valuation ( cellValuations, eulerCharacteristicValue, exactAreaValue,
+  valuationArea, valuationEuler )
+import Support ( assertEqual, integerPoint, requireRight, rectangleRegion, annulusRegion )
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testLowerDimensionalIntersections
+    , testSelectorRefusalsAndOperandSwap
+    , testExteriorMarginalClosures
+    , testNestedVertexContactClosure
+    ]
+
+testLowerDimensionalIntersections :: IO ()
+testLowerDimensionalIntersections = do
+  left <- rectangleRegion 0 0 1 1 >>= singletonLayer "left-outside" "left"
+  pointTouching <- rectangleRegion 1 1 2 2 >>= singletonLayer "right-outside" "right"
+  pointResult <- requireRight "point-touching overlay" (overlayLayers left pointTouching)
+  assertOverlayIntegrity "point-touching" pointResult
+  assertEqual
+    "point-touching outside is one boundary walk with a repeated contact vertex"
+    (Just 1)
+    (unboundedLoopCount pointResult)
+  pointIntersection <-
+    requireRight
+      "point-only closed intersection"
+      (overlayClosedIntersection (== "left") (== "right") pointResult)
+  assertEqual
+    "point-only closed intersection retains only its zero-cell"
+    (1, 0, 0)
+    ( exactCellSetVertexCount pointIntersection
+    , exactCellSetEdgeCount pointIntersection
+    , exactCellSetFaceCount pointIntersection
+    )
+
+  edgeTouching <- rectangleRegion 1 0 2 1 >>= singletonLayer "right-outside" "right"
+  edgeResult <- requireRight "edge-touching overlay" (overlayLayers left edgeTouching)
+  assertOverlayIntegrity "edge-touching" edgeResult
+  edgeIntersection <-
+    requireRight
+      "edge-only closed intersection"
+      (overlayClosedIntersection (== "left") (== "right") edgeResult)
+  assertEqual
+    "edge-only closed intersection retains its closure"
+    (2, 1, 0)
+    ( exactCellSetVertexCount edgeIntersection
+    , exactCellSetEdgeCount edgeIntersection
+    , exactCellSetFaceCount edgeIntersection
+    )
+  publishedEdgeOnly <-
+    requireRight
+      "edge-only polygon publication"
+      (overlaySelectedRegion (== ("left", "right")) edgeResult)
+  assertEqual
+    "edge-only selection is not fabricated into a polygon"
+    0
+    (length (planarRegionComponents publishedEdgeOnly))
+
+testSelectorRefusalsAndOperandSwap :: IO ()
+testSelectorRefusalsAndOperandSwap = do
+  left <- rectangleRegion 0 0 2 2 >>= singletonLayer "left-outside" "left"
+  right <- rectangleRegion 1 (-1) 3 1 >>= singletonLayer "right-outside" "right"
+  result <- requireRight "selector refusal overlay" (overlayLayers left right)
+  assertSelectionRefusal
+    "closed union refuses selected outside cell"
+    (OverlaySelectionContainsUnboundedCell ClosedUnionSelection)
+    (overlayClosedUnion (== "left-outside") (const False) result)
+  assertSelectionRefusal
+    "closed intersection refuses selected outside cell"
+    (OverlaySelectionContainsUnboundedCell ClosedIntersectionSelection)
+    ( overlayClosedIntersection
+        (== "left-outside")
+        (== "right-outside")
+        result
+    )
+  assertSelectionRefusal
+    "regularized difference refuses selected outside cell"
+    (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
+    ( overlayRegularizedDifference
+        (== "left-outside")
+        (== "right")
+        result
+    )
+
+  swapped <- requireRight "operand-swapped overlay" (overlayLayers right left)
+  assertOverlayIntegrity "operand-swapped" swapped
+  assertEqual
+    "operand swap preserves exact arrangement vertices"
+    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices result)))
+    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices swapped)))
+  assertEqual
+    "operand swap preserves cells and exchanges labels"
+    [ (face, (rightLabel, leftLabel))
+    | (face, (leftLabel, rightLabel)) <- overlayCells result
+    ]
+    (overlayCells swapped)
+  assertEqual
+    "operand swap exchanges typed vertex provenance"
+    (vertexOriginCensus True result)
+    (vertexOriginCensus False swapped)
+
+-- A selected exterior marginal need not select the unbounded joint face.
+-- Its bounded intersection can include an entire lower-dimensional component
+-- that is not in the downward closure of any selected bounded face.
+testExteriorMarginalClosures :: IO ()
+testExteriorMarginalClosures = do
+  square <- rectangleRegion 0 0 4 4 >>= singletonLayer "outside" "inside"
+  sameSquare <- requireRight "same-support exterior intersection" (overlayLayers square square)
+  boundary <- requireRight "outside closure intersected with inside closure"
+    (overlayClosedIntersection (== "outside") (== "inside") sameSquare)
+  assertEqual "exterior/interior marginal intersection retains the closed boundary"
+    (4, 4, 0)
+    (exactCellSetVertexCount boundary, exactCellSetEdgeCount boundary, exactCellSetFaceCount boundary)
+  boundaryValues <- requireRight "exterior/interior boundary valuation" (cellValuations boundary)
+  assertEqual "closed square boundary has Euler zero and no area"
+    (0, 0)
+    (eulerCharacteristicValue (valuationEuler boundaryValues), exactAreaValue (valuationArea boundaryValues))
+
+  region <- annulusRegion (0, 0, 4, 4) (1, 1, 3, 3)
+  annulus <- requireRight "exterior annulus layer" (planarLayer "outside" (Map.singleton "annulus" region))
+  result <- requireRight "annulus exterior against enclosing square" (overlayLayers annulus square)
+  selected <- requireRight "bounded exterior disk plus detached outer boundary"
+    (overlayClosedIntersection (== "outside") (== "inside") result)
+  assertEqual "exterior intersection keeps a face and an independent edge cycle"
+    (8, 8, 1)
+    (exactCellSetVertexCount selected, exactCellSetEdgeCount selected, exactCellSetFaceCount selected)
+  values <- requireRight "exterior disk and boundary valuation" (cellValuations selected)
+  assertEqual "disjoint disk plus edge cycle has Euler one and exact cavity area"
+    (1, 4)
+    (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))
+
+-- Tangency joins the outer and inner boundary walks at a single vertex.
+-- The selected open face is cellular, but its closure is not a disk: the
+-- repeated contact vertex must survive incidence rather than split the walk.
+testNestedVertexContactClosure :: IO ()
+testNestedVertexContactClosure = do
+  square <- rectangleRegion 0 0 4 4 >>= singletonLayer "outside" "inside"
+  diamondLoop <- requireRight "internally tangent diamond loop"
+    (exactLoop (integerPoint 0 2 :| [integerPoint 1 1, integerPoint 2 2, integerPoint 1 3]))
+  diamondComponent <- requireRight "internally tangent diamond component" (polygonComponent diamondLoop [])
+  diamondRegion <- requireRight "internally tangent diamond region" (planarRegion [diamondComponent])
+  diamond <- requireRight "internally tangent diamond layer"
+    (planarLayer "outside" (Map.singleton "inside" diamondRegion))
+  result <- requireRight "nested single-vertex contact overlay" (overlayLayers square diamond)
+  assertOverlayIntegrity "nested single-vertex contact" result
+  assertEqual "tangent complement retains one raw boundary orbit"
+    [1]
+    [length (faceBoundaryComponents (overlayResultIncidence result) face)
+    | (face, labels) <- overlayCells result, labels == ("inside", "outside")]
+  complement <- requireRight "tangent complement exact closure"
+    (overlayRegularizedDifference (== "inside") (== "inside") result)
+  values <- requireRight "tangent complement exact valuation" (cellValuations complement)
+  assertEqual "contact does not fill the diamond hole in the closure"
+    (0, 14)
+    (eulerCharacteristicValue (valuationEuler values), exactAreaValue (valuationArea values))
+
+assertSelectionRefusal
+  :: String
+  -> OverlaySelectionError
+  -> Either OverlaySelectionError ExactCellSet
+  -> IO ()
+assertSelectionRefusal label expected actual =
+  case actual of
+    Left obstruction -> assertEqual label expected obstruction
+    Right _ -> fail (label <> ": unbounded selection was truncated")
+
+vertexOriginCensus
+  :: Bool
+  -> OverlayResult (String, String)
+  -> Map.Map ExactPoint (Int, Int, Int, Int)
+vertexOriginCensus preserveSides result =
+  Map.fromList
+    [ ( overlayExactPoint vertex
+      , if preserveSides
+          then census origin
+          else swapCensus (census origin)
+      )
+    | (_, vertex) <- overlayArrangementVertices result
+    , let origin = overlayVertexOrigin vertex
+    ]
+ where
+  census :: OverlayVertexOrigin -> (Int, Int, Int, Int)
+  census origin =
+    ( length (filter ((== OverlaySourceId 0) . boundaryRefSource) (overlayOriginVertices origin))
+    , length (filter ((== OverlaySourceId 1) . boundaryRefSource) (overlayOriginVertices origin))
+    , length (filter ((== OverlaySourceId 0) . boundaryRefSource) (overlayOriginEdges origin))
+    , length (filter ((== OverlaySourceId 1) . boundaryRefSource) (overlayOriginEdges origin))
+    )
+  swapCensus :: (Int, Int, Int, Int) -> (Int, Int, Int, Int)
+  swapCensus (leftVertices, rightVertices, leftEdges, rightEdges) =
+    (rightVertices, leftVertices, rightEdges, leftEdges)
diff --git a/test/native/Moonlight/Planar/OverlaySpec.hs b/test/native/Moonlight/Planar/OverlaySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/OverlaySpec.hs
@@ -0,0 +1,195 @@
+-- | Labelled arrangement construction, normalization, and source provenance.
+module Moonlight.Planar.OverlaySpec (tests) where
+
+import Control.Monad ( when )
+import Moonlight.Planar.CellSet ( exactCellSetEdgeCount, exactCellSetFaceCount,
+  exactCellSetVertexCount )
+import Moonlight.Planar.Exact ( exactPointCoordinates, exactRationalDenominator )
+import Moonlight.Planar.Overlay ( BoundaryRef(boundaryRefSource),
+  OverlayEdgeOrigin(overlayEdgeSources), OverlayReceipt(overlayExactCrossings, overlayInputSegments,
+  overlayAtomicEdges, overlayOverlapIntervals), OverlaySourceId(OverlaySourceId),
+  OverlayVertex(overlayExactPoint), overlayArrangementEdges, overlayArrangementVertices,
+  overlayCells, overlayClosedIntersection, overlayConfusion, overlayLayers, overlayPlanarLayer,
+  overlayReceipt, overlaySelectedRegion )
+import Moonlight.Planar.OverlayAssertions ( assertOverlayIntegrity, unboundedLoopCount )
+import Moonlight.Planar.OverlayFixtures ( singletonLayer )
+import Moonlight.Planar.Region ( planarLayerRegions, polygonOuterLoop, exactLoopPoints, planarLayer,
+  planarRegion, planarRegionComponents, RegionPublicationError(RegionUnboundedSelection) )
+import Moonlight.Planar.Valuation ( exactAreaValue )
+import Support ( assertEqual, integerPoint, rectangleComponent, requireRight, rectangleRegion,
+  polygonRegion )
+import qualified Data.Map.Strict as Map
+import qualified Data.List.NonEmpty as NonEmpty
+import qualified Data.Set as Set
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testOverlappingSquares
+    , testSeamAndOverlapNormalization
+    , testNestedAndNonDyadicOverlay
+    , testCancelledSeamCrossesLabelTransition
+    ]
+
+testOverlappingSquares :: IO ()
+testOverlappingSquares = do
+  left <- rectangleRegion 0 0 2 2 >>= singletonLayer "left-outside" "left"
+  right <- rectangleRegion 1 (-1) 3 1 >>= singletonLayer "right-outside" "right"
+  result <- requireRight "overlapping square overlay" (overlayLayers left right)
+  assertOverlayIntegrity "overlapping square" result
+  let receipt = overlayReceipt result
+  assertEqual "overlay source segment count" 8 (overlayInputSegments receipt)
+  assertEqual "overlay has two proper crossings" 2 (overlayExactCrossings receipt)
+  when (overlayAtomicEdges receipt < 8) $
+    fail ("overlay lost atomic edges: " <> show receipt)
+  intersection <-
+    requireRight
+      "closed square intersection"
+      (overlayClosedIntersection (== "left") (== "right") result)
+  when
+    ( exactCellSetVertexCount intersection < 4
+        || exactCellSetEdgeCount intersection < 4
+        || exactCellSetFaceCount intersection < 1
+    )
+    (fail "closed intersection omitted incidence closure")
+  published <-
+    requireRight
+      "selected square intersection publication"
+      (overlaySelectedRegion (== ("left", "right")) result)
+  assertEqual
+    "selected square intersection component count"
+    1
+    (length (planarRegionComponents published))
+  assertEqual
+    "unbounded selected publication refusal"
+    (Left RegionUnboundedSelection)
+    (overlaySelectedRegion (const True) result)
+  merged <-
+    requireRight
+      "selected adjacent overlay cells merge"
+      (overlaySelectedRegion (\(leftLabel, _) -> leftLabel == "left") result)
+  assertEqual
+    "selected adjacent overlay cells publish one component"
+    [4]
+    (map (length . exactLoopPoints . polygonOuterLoop) (planarRegionComponents merged))
+
+testSeamAndOverlapNormalization :: IO ()
+testSeamAndOverlapNormalization = do
+  first <- rectangleComponent 0 0 1 1
+  second <- rectangleComponent 1 0 2 1
+  joinedRegion <- requireRight "same-label joined region" (planarRegion [first, second])
+  joinedLayer <-
+    requireRight
+      "same-label joined layer"
+      (planarLayer "outside" (Map.singleton "inside" joinedRegion))
+  emptyLayer <- requireRight "empty normalization layer" (planarLayer "void" Map.empty)
+  seamResult <- requireRight "same-label seam overlay" (overlayLayers joinedLayer emptyLayer)
+  assertOverlayIntegrity "same-label seam" seamResult
+  assertEqual "joined rectangle has one unbounded boundary cycle" (Just 1) (unboundedLoopCount seamResult)
+  assertEqual
+    "same-label shared boundary is removed before topology"
+    6
+    (overlayAtomicEdges (overlayReceipt seamResult))
+  seamPublication <- requireRight "publish same-label seam" (overlayPlanarLayer seamResult)
+  assertEqual
+    "the implicit outside pair is never duplicated as a bounded layer key"
+    Nothing
+    ( Map.lookup
+        ("outside", "void")
+        (planarLayerRegions seamPublication)
+    )
+  assertEqual
+    "same-label seam dissolves to the rectangle boundary"
+    [4]
+    ( map
+        (length . exactLoopPoints . polygonOuterLoop)
+        ( maybe
+            []
+            planarRegionComponents
+            (Map.lookup ("inside", "void") (planarLayerRegions seamPublication))
+        )
+    )
+
+  duplicateLeft <- rectangleRegion 0 0 2 2 >>= singletonLayer "left-outside" "left"
+  duplicateRight <- rectangleRegion 0 0 2 2 >>= singletonLayer "right-outside" "right"
+  duplicateResult <-
+    requireRight "cross-operand duplicate boundary overlay" (overlayLayers duplicateLeft duplicateRight)
+  assertOverlayIntegrity "duplicate boundary" duplicateResult
+  assertEqual
+    "duplicate boundaries normalize to one atomic cycle"
+    4
+    (overlayAtomicEdges (overlayReceipt duplicateResult))
+  assertEqual "duplicate atomic support retains both typed source origins" True
+    (all
+      ((== Set.fromList [OverlaySourceId 0, OverlaySourceId 1])
+        . Set.fromList . fmap boundaryRefSource . NonEmpty.toList . overlayEdgeSources . snd)
+      (overlayArrangementEdges duplicateResult))
+
+  partialLeft <- rectangleRegion 0 0 3 2 >>= singletonLayer "left-outside" "left"
+  partialRight <- rectangleRegion 1 0 4 1 >>= singletonLayer "right-outside" "right"
+  partialResult <-
+    requireRight "partial collinear overlap" (overlayLayers partialLeft partialRight)
+  assertOverlayIntegrity "partial collinear overlap" partialResult
+  if overlayOverlapIntervals (overlayReceipt partialResult) > 0
+    then pure ()
+    else fail "partial collinear overlap emitted no overlap event"
+
+testNestedAndNonDyadicOverlay :: IO ()
+testNestedAndNonDyadicOverlay = do
+  outer <- rectangleRegion 0 0 4 4 >>= singletonLayer "left-outside" "left"
+  inner <- rectangleRegion 1 1 3 3 >>= singletonLayer "right-outside" "right"
+  nested <- requireRight "nested overlay" (overlayLayers outer inner)
+  assertOverlayIntegrity "nested" nested
+  assertEqual "nested overlay unbounded boundary is outermost only" (Just 1) (unboundedLoopCount nested)
+  assertEqual
+    "nested overlay has unbounded, shell, and intersection cells"
+    3
+    (length (overlayCells nested))
+
+  leftTriangle <-
+    polygonRegion [(0, 0), (4, 0), (0, 4)] >>= singletonLayer "left-outside" "left"
+  rightTriangle <-
+    polygonRegion [(1, -1), (3, -1), (2, 2)] >>= singletonLayer "right-outside" "right"
+  nonDyadic <-
+    requireRight "non-dyadic proper-crossing overlay" (overlayLayers leftTriangle rightTriangle)
+  assertOverlayIntegrity "non-dyadic proper crossing" nonDyadic
+  let hasNonDyadicCoordinate =
+        any
+          (\(_, vertex) ->
+             let (x, y) = exactPointCoordinates (overlayExactPoint vertex)
+              in any
+                   (not . isPowerOfTwo . exactRationalDenominator)
+                   [x, y])
+          (overlayArrangementVertices nonDyadic)
+  if overlayExactCrossings (overlayReceipt nonDyadic) > 0 && hasNonDyadicCoordinate
+    then pure ()
+    else fail "proper-crossing overlay lost its non-dyadic exact witness"
+
+-- A cancelled same-source seam crosses a retained label-change support.
+-- The resulting event remains a vertex, and each subinterval must transport
+-- only the source labels whose transition is present on that support.
+testCancelledSeamCrossesLabelTransition :: IO ()
+testCancelledSeamCrossesLabelTransition = do
+  leftComponent <- rectangleComponent 0 0 2 4
+  rightComponent <- rectangleComponent 2 0 4 4
+  leftRegion <- requireRight "left label region" (planarRegion [leftComponent])
+  rightRegion <- requireRight "right label region" (planarRegion [rightComponent])
+  labelled <- requireRight "adjacent distinct labels"
+    (planarLayer "void" (Map.fromList [("a", leftRegion), ("b", rightRegion)]))
+  lower <- rectangleComponent 1 1 3 2
+  upper <- rectangleComponent 1 2 3 3
+  maskRegion <- requireRight "mask with cancellable internal seam" (planarRegion [lower, upper])
+  mask <- requireRight "same-label mask source" (planarLayer "off" (Map.singleton "on" maskRegion))
+  result <- requireRight "cancelled seam across retained transition" (overlayLayers labelled mask)
+  assertOverlayIntegrity "cancelled seam across retained transition" result
+  masses <- requireRight "interval-wise sparse source transitions" (overlayConfusion result)
+  assertEqual "cancellation preserves both labels across all transition intervals"
+    (Map.fromList [(("a", "off"), 6), (("a", "on"), 2), (("b", "off"), 6), (("b", "on"), 2)])
+    (fmap exactAreaValue masses)
+  assertEqual "cancelled seam crossing remains an exact provenance event" True
+    (Set.member (integerPoint 2 2)
+      (Set.fromList (fmap (overlayExactPoint . snd) (overlayArrangementVertices result))))
+
+isPowerOfTwo :: Integer -> Bool
+isPowerOfTwo value =
+  value > 0 && value `elem` takeWhile (<= value) (iterate (* 2) 1)
diff --git a/test/native/Moonlight/Planar/PayloadFixtures.hs b/test/native/Moonlight/Planar/PayloadFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/PayloadFixtures.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+
+{-# LANGUAGE BangPatterns #-}
+
+-- | Position-bearing payload shared by insertion, persistence, and payload laws.
+module Moonlight.Planar.PayloadFixtures
+  ( SampleVertex (..)
+  ) where
+
+import Control.DeepSeq ( NFData )
+import GHC.Generics ( Generic )
+import Moonlight.Planar.Point (Point, HasPosition(..))
+
+
+data SampleVertex = SampleVertex
+  { samplePosition :: !(Point)
+  , sampleLabel :: !Int
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData)
+
+instance HasPosition SampleVertex where
+  position = samplePosition
diff --git a/test/native/Moonlight/Planar/PayloadSpec.hs b/test/native/Moonlight/Planar/PayloadSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/PayloadSpec.hs
@@ -0,0 +1,537 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NumericUnderscores #-}
+
+-- | Payload maps, traversals, geometric projection, and rewrite survival laws.
+module Moonlight.Planar.PayloadSpec
+  ( tests
+  ) where
+
+import Control.Monad ( forM_, unless, when )
+import Control.Exception (TypeError, displayException, evaluate, try)
+import Control.Monad.ST (runST)
+import Control.Monad.Trans.State.Strict (State, runState, state)
+import Data.Coerce (coerce)
+import Data.Foldable (traverse_)
+import Data.Word ( Word8, Word32 )
+import Moonlight.Planar.BulkLoad ( delaunay, insert )
+import Moonlight.Planar.Dcel ( mapDirectedEdges, mapFaces, mapUndirectedEdges, mapVertices,
+  destination, directedEdgeData, faceData, numFaces, numUndirectedEdges, origin,
+  setDirectedEdgeData, setFaceData, setUndirectedEdgeData, setVertexData, undirectedEdgeData,
+  undirectedEndpoints, vertexData, vertexPoint )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( allFaces, directedEdges, undirectedEdges,
+  vertices, innerFaces )
+import Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedPaged, BoxedFill (..), FillRequirement (..), boxedDefaulted, boxedFill
+  , boxedMaterializedPageCount, boxedToVector, boxedUpdate
+  , emptyBoxedPaged, freezeBoxedPaged, requiredBoxedFill, resetBoxedRange, thawBoxedPaged )
+import qualified Moonlight.Planar.PayloadTypeErrors as Rejected
+import Moonlight.Planar.Internal.HandleDefs ( DirectedEdgeId, UndirectedEdgeId, VertexId(VertexId),
+  asUndirected )
+import Moonlight.Planar.Internal.Paged ( Paged, TransactionShape (LocalTransaction) )
+import Moonlight.Planar.MeshFixtures ( faceKeyOf )
+import Moonlight.Planar.Payload ( directedPayloads, facePayloads, undirectedPayloads,
+  vertexPayloads, overPayloads, payloadList )
+import Moonlight.Planar.PayloadFixtures ( SampleVertex(..) )
+import Moonlight.Planar.Session ( withSession, insertVertex, refuse, removeAt )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (unitElementDefaults, BuildError(RemovalVertexOutOfRange), ConstraintMode(Unconstrained), ElementDefaults(ElementDefaults, defaultFaceData), InsertionDisposition(Inserted), BuildResult(buildTriangulation), InsertionResult(insertionDisposition, insertionTriangulation), Triangulation)
+import Support ( assertEqual, assertValid, requireRight, randomPoints )
+import qualified Data.IntSet as IntSet
+import Moonlight.Planar.Internal.Representation qualified as Internal
+import qualified Data.List as List
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testGenericPayloads
+    , testPayloadMaps
+    , testGeometryOnlyPublication
+    , testPayloadTraversals
+    , testRewritePayloadIdentity
+    , testFillOwnership
+    ]
+
+testGenericPayloads :: IO ()
+testGenericPayloads = do
+  let defaults = ElementDefaults (7 :: Int) False ("new-face" :: String)
+      payloads = V.fromList
+        [ SampleVertex (Point 0 0) 1
+        , SampleVertex (Point 2 0) 2
+        , SampleVertex (Point 0 2) 3
+        , SampleVertex (Point 0.5 0.5) 4
+        ]
+  built <- requireRight "generic payload build" (delaunay defaults payloads)
+  let triangulation = buildTriangulation built
+  assertValid "generic payload build" triangulation
+  assertEqual "vertex payload" 4 (sampleLabel (vertexData triangulation (VertexId 3)))
+  forM_ (directedEdges triangulation) $ \edge -> assertEqual "directed default" 7 (directedEdgeData triangulation edge)
+  forM_ (undirectedEdges triangulation) $ \edge -> assertEqual "undirected default" False (undirectedEdgeData triangulation edge)
+  forM_ (allFaces triangulation) $ \face -> assertEqual "face default" "new-face" (faceData triangulation face)
+  (firstEdge, firstFace) <- case (directedEdges triangulation, innerFaces triangulation) of
+    (edge : _, face : _) -> pure (edge, face)
+    _ -> fail "generic payload build produced no inner topology"
+  let firstUndirected = asUndirected firstEdge
+      changed = setFaceData (setUndirectedEdgeData (setDirectedEdgeData triangulation firstEdge 42) firstUndirected True) firstFace "changed"
+  assertEqual "directed payload update" 42 (directedEdgeData changed firstEdge)
+  assertEqual "undirected payload update" True (undirectedEdgeData changed firstUndirected)
+  assertEqual "face payload update" "changed" (faceData changed firstFace)
+  -- Geometry owns the points, so a payload carrying a different position is not
+  -- a contradiction to be refused — it is a payload whose position nobody reads.
+  let moved = setVertexData triangulation (VertexId 0) (SampleVertex (Point 9 9) 0)
+  assertEqual "a payload position does not site a vertex"
+    (vertexPoint triangulation (VertexId 0)) (vertexPoint moved (VertexId 0))
+  assertEqual "the payload is stored as given"
+    (Point 9 9) (samplePosition (vertexData moved (VertexId 0)))
+
+-- The payload layer over a fixed geometry is a product of four free components.
+-- Each is a functor and each is checked as such; nothing in the product can
+-- disturb the geometry underneath it.
+testPayloadMaps :: IO ()
+testPayloadMaps = do
+  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)
+      payloads = V.fromList
+        [ SampleVertex (Point 0 0) 1
+        , SampleVertex (Point 4 0) 2
+        , SampleVertex (Point 4 4) 3
+        , SampleVertex (Point 0 4) 4
+        , SampleVertex (Point 1 2) 5
+        ]
+  built <- requireRight "payload map build" (delaunay defaults payloads)
+  let plain = buildTriangulation built
+      -- Distinct payloads everywhere: a map that permuted its component would
+      -- be invisible against uniform defaults.
+      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))
+      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))
+      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))
+      endpoints ::
+        Triangulation mode vertex directed undirected face ->
+        [(VertexId, VertexId)]
+      endpoints t = [(origin t edge, destination t edge) | edge <- directedEdges t]
+
+  -- Identity. Equality on a triangulation compares geometry, topology,
+  -- constraint flags and element defaults as well as payloads, so this single
+  -- equation states that each map disturbs nothing but the component it names.
+  assertEqual "mapDirectedEdges identity" sample (mapDirectedEdges id sample)
+  assertEqual "mapUndirectedEdges identity" sample (mapUndirectedEdges id sample)
+  assertEqual "mapFaces identity" sample (mapFaces id sample)
+  assertEqual "mapVertices identity" sample (mapVertices id sample)
+
+  assertEqual "mapDirectedEdges composition"
+    (mapDirectedEdges ((* 2) . (+ 1)) sample)
+    (mapDirectedEdges (* 2) (mapDirectedEdges (+ 1) sample))
+  assertEqual "mapFaces composition"
+    (mapFaces (("<" <>) . (<> ">")) sample)
+    (mapFaces ("<" <>) (mapFaces (<> ">") sample))
+
+  -- Commutation with the accessors. Identity and composition are blind to the
+  -- indexing; this is the law that pins each payload to its own handle.
+  let directedMapped = mapDirectedEdges (+ 1) sample
+      undirectedMapped = mapUndirectedEdges ("<" <>) sample
+      facesMapped = mapFaces ("<" <>) sample
+  forM_ (directedEdges sample) $ \edge ->
+    assertEqual "mapDirectedEdges commutes with directedEdgeData"
+      (directedEdgeData sample edge + 1) (directedEdgeData directedMapped edge)
+  forM_ (undirectedEdges sample) $ \edge ->
+    assertEqual "mapUndirectedEdges commutes with undirectedEdgeData"
+      ("<" <> undirectedEdgeData sample edge) (undirectedEdgeData undirectedMapped edge)
+  forM_ (allFaces sample) $ \face ->
+    assertEqual "mapFaces commutes with faceData"
+      ("<" <> faceData sample face) (faceData facesMapped face)
+
+  -- The components are independent, and none of them is geometry.
+  assertEqual "face and directed maps commute"
+    (mapFaces ("<" <>) (mapDirectedEdges (+ 1) sample))
+    (mapDirectedEdges (+ 1) (mapFaces ("<" <>) sample))
+  assertEqual "mapFaces preserves topology" (endpoints sample) (endpoints facesMapped)
+  assertEqual "mapFaces preserves the face count" (numFaces sample) (numFaces facesMapped)
+  forM_ (vertices sample) $ \vertex ->
+    assertEqual "mapFaces preserves geometry" (vertexPoint sample vertex) (vertexPoint facesMapped vertex)
+
+  -- The element default is a payload and must travel with them: every element a
+  -- later insertion creates is handed the default, so a map that reindexed the
+  -- stored payloads and left the default behind would produce a triangulation
+  -- whose future elements disagree with its present ones. Every stored payload
+  -- here differs from the default, so carrying the wrong one is visible.
+  grownFaces <- insertionTriangulation <$> requireRight "insertion into mapped faces" (insert facesMapped (SampleVertex (Point 2 1) 6))
+  grownUndirected <- insertionTriangulation <$> requireRight "insertion into mapped undirected edges" (insert undirectedMapped (SampleVertex (Point 2 1) 6))
+  grownDirected <- insertionTriangulation <$> requireRight "insertion into mapped directed edges" (insert directedMapped (SampleVertex (Point 2 1) 6))
+  unless (numFaces grownFaces > numFaces facesMapped) $ fail "the insertion created no face"
+  unless ("<new-face" `elem` map (faceData grownFaces) (allFaces grownFaces)) $
+    fail ("new faces did not receive the mapped default: " <> show (map (faceData grownFaces) (allFaces grownFaces)))
+  unless ("<new-undirected" `elem` map (undirectedEdgeData grownUndirected) (undirectedEdges grownUndirected)) $
+    fail "new undirected edges did not receive the mapped default"
+  unless (8 `elem` map (directedEdgeData grownDirected) (directedEdges grownDirected)) $
+    fail "new directed edges did not receive the mapped default"
+
+  -- Vertices, the component that used to be special. The map is total, and the
+  -- one thing worth insisting on is that a function which does its worst to the
+  -- stored position still cannot move a vertex.
+  let relabelled = mapVertices (\v -> v{sampleLabel = sampleLabel v * 10}) sample
+      collapsed = mapVertices (\v -> v{samplePosition = Point 9 9}) sample
+  forM_ (vertices sample) $ \vertex -> do
+    assertEqual "mapVertices commutes with vertexData"
+      (sampleLabel (vertexData sample vertex) * 10) (sampleLabel (vertexData relabelled vertex))
+    assertEqual "mapVertices preserves geometry" (vertexPoint sample vertex) (vertexPoint relabelled vertex)
+    assertEqual "a payload map cannot move a vertex"
+      (vertexPoint sample vertex) (vertexPoint collapsed vertex)
+  assertEqual "mapVertices composes"
+    (mapVertices (\v -> v{sampleLabel = sampleLabel v + 1}) relabelled)
+    (mapVertices (\v -> v{sampleLabel = sampleLabel v * 10 + 1}) sample)
+
+  -- The sharpest statement of freedom available: the image type has no
+  -- 'HasPosition' instance at all. This does not typecheck under a vertex
+  -- component that geometry reads through.
+  let projected = mapVertices sampleLabel sample
+  forM_ (vertices sample) $ \vertex -> do
+    assertEqual "a vertex payload need not have a position"
+      (sampleLabel (vertexData sample vertex)) (vertexData projected vertex)
+    assertEqual "projecting payloads away preserves geometry"
+      (vertexPoint sample vertex) (vertexPoint projected vertex)
+
+-- Geometry publication is payload forgetting, not a dense payload map.  The
+-- polymorphic entrance accepts all four non-unit payload planes and publishes
+-- zero materialized pages while retaining the exact structural planes.
+testGeometryOnlyPublication :: IO ()
+testGeometryOnlyPublication = do
+  let defaults = ElementDefaults (17 :: Int) False ("source-face" :: String)
+      payloads =
+        V.fromList
+          [ SampleVertex (Point 0 0) 11
+          , SampleVertex (Point 4 0) 22
+          , SampleVertex (Point 4 4) 33
+          , SampleVertex (Point 0 4) 44
+          , SampleVertex (Point 1 2) 55
+          ]
+  built <- requireRight "geometry-only payload source" (delaunay defaults payloads)
+  let source = buildTriangulation built
+      published = Internal.geometryOnlyPublication source
+      republished = Internal.geometryOnlyPublication published
+      structuralPlanes
+        :: forall vertex directed undirected face.
+           Triangulation 'Unconstrained vertex directed undirected face
+        -> ( Paged Double
+           , Paged Double
+           , Paged Word32
+           , Paged Word32
+           , Paged Word32
+           , Paged Word8
+           , Int
+           , IntSet.IntSet
+           )
+      structuralPlanes triangulation =
+        ( Internal.triPointX triangulation
+        , Internal.triPointY triangulation
+        , Internal.triVertexOut triangulation
+        , Internal.triHalfTopology triangulation
+        , Internal.triFaceEdge triangulation
+        , Internal.triConstraint triangulation
+        , Internal.triConstraintCount triangulation
+        , Internal.triConstraintEdges triangulation
+        )
+      payloadPageCounts
+        :: forall vertex directed undirected face.
+           Triangulation 'Unconstrained vertex directed undirected face
+        -> (Int, Int, Int, Int)
+      payloadPageCounts triangulation =
+        ( boxedMaterializedPageCount (Internal.triVertexData triangulation)
+        , boxedMaterializedPageCount (Internal.triDirectedData triangulation)
+        , boxedMaterializedPageCount (Internal.triUndirectedData triangulation)
+        , boxedMaterializedPageCount (Internal.triFaceData triangulation)
+        )
+  unless (sumPayloadPages (payloadPageCounts source) > 0) $
+    fail "geometry-only payload fixture did not materialize a source payload page"
+  assertEqual
+    "geometry-only publication preserves structural planes"
+    (structuralPlanes source)
+    (structuralPlanes published)
+  assertEqual
+    "geometry-only publication replaces every payload plane with a zero-page store"
+    (0, 0, 0, 0)
+    (payloadPageCounts published)
+  assertEqual
+    "geometry-only publication installs unit element defaults"
+    unitElementDefaults
+    (Internal.authoringElementDefaults published)
+  assertEqual
+    "geometry-only publication prepares the exact seam frontier"
+    (Internal.prepareSeamFrontierIndex source)
+    (Internal.triSeamFrontier published)
+  assertEqual
+    "geometry-only publication preserves an existing seam frontier"
+    (Internal.triSeamFrontier published)
+    (Internal.triSeamFrontier republished)
+  assertValid "geometry-only publication" published
+  assertEqual "geometry-only vertex traversal preserves its optional fill"
+    published (overPayloads vertexPayloads id published)
+  assertEqual "geometry-only vertex traversal agrees with mapping"
+    (mapVertices (const (7 :: Int)) published)
+    (overPayloads vertexPayloads (const (7 :: Int)) published)
+  assertEqual "geometry-only vertex traversal visits residents then one fill"
+    (replicate (V.length payloads + 1) ())
+    (payloadList vertexPayloads published)
+  let (numbered, visitCount) = runState (vertexPayloads numberVisit published) 0
+  assertEqual "stateful traversal visits each vertex exactly once"
+    (V.fromList [0 .. V.length payloads - 1])
+    (fmap snd (boxedToVector (Internal.triVertexData numbered)))
+  assertEqual "stateful traversal puts the optional fill last"
+    (Fill ((), V.length payloads)) (boxedFill (Internal.triVertexData numbered))
+  assertEqual "stateful traversal visits exactly one fill"
+    (V.length payloads + 1) visitCount
+ where
+  sumPayloadPages
+    :: (Int, Int, Int, Int)
+    -> Int
+  sumPayloadPages (vertexPages, directedPages, undirectedPages, facePages) =
+    vertexPages + directedPages + undirectedPages + facePages
+
+numberVisit :: value -> State Int (value, Int)
+numberVisit value = state (\index -> ((value, index), index + 1))
+
+newtype PayloadLabel = PayloadLabel Int
+  deriving stock (Eq, Show)
+
+testFillOwnership :: IO ()
+testFillOwnership = do
+  emptyBuilt <- requireRight "empty geometry publication"
+    (delaunay unitElementDefaults (V.empty :: V.Vector Point))
+  let sparse :: BoxedPaged 'RequiredFill Int
+      sparse = boxedDefaulted 7 600
+      written = boxedUpdate 590 103 . boxedUpdate 257 101 . boxedUpdate 3 99 $ sparse
+      recycled = runST $ do
+        mutable <- thawBoxedPaged LocalTransaction written
+        resetBoxedRange mutable 254 8
+        freezeBoxedPaged 730 mutable
+      untouched = runST $ do
+        mutable <- thawBoxedPaged LocalTransaction sparse
+        resetBoxedRange mutable 0 600
+        freezeBoxedPaged 730 mutable
+      optionalEmpty :: BoxedPaged 'OptionalFill Int
+      optionalEmpty = emptyBoxedPaged NoFill
+      filledEmpty :: BoxedPaged 'RequiredFill Int
+      filledEmpty = emptyBoxedPaged (Fill 7)
+      labelled :: BoxedPaged 'RequiredFill PayloadLabel
+      labelled = coerce sparse
+      (emptyVisited, emptyVisits) = runState
+        (vertexPayloads numberVisit (Internal.geometryOnlyPublication (buildTriangulation emptyBuilt))) 0
+  reused <- requireRight "required-fill range reuse" recycled
+  stayedSparse <- requireRight "untouched required-fill range" untouched
+  assertEqual "reuse reads its owning fill across a page boundary and grown tail"
+    (V.generate 730 (\index -> if index == 3 then 99 else if index == 590 then 103 else 7))
+    (boxedToVector reused)
+  assertEqual "resetting absent pages leaves them absent" 0
+    (boxedMaterializedPageCount stayedSparse)
+  assertEqual "empty required store maps its future fill" 8
+    (requiredBoxedFill (fmap (+ 1) filledEmpty))
+  assertEqual "empty optional store preserves absence" NoFill
+    (boxedFill (fmap (+ 1) optionalEmpty))
+  assertEqual "payload newtypes remain representational" (PayloadLabel 7)
+    (requiredBoxedFill labelled)
+  assertEqual "an empty geometry store still visits its optional fill" 1 emptyVisits
+  assertEqual "the empty store retains the visited fill" (Fill ((), 0))
+    (boxedFill (Internal.triVertexData emptyVisited))
+  let checkRejected (label, attempted) = do
+        result <- try (evaluate attempted) :: IO (Either TypeError ())
+        case result of
+          Left failure -> unless
+            ("RequiredFill" `List.isInfixOf` displayException failure
+              && "OptionalFill" `List.isInfixOf` displayException failure)
+            (fail (label <> ": wrong type error: " <> displayException failure))
+          Right () -> fail (label <> ": required fill was forged")
+  traverse_ checkRejected
+    [ ("construction", Rejected.forgeRequiredFill `seq` ())
+    , ("nominal index", Rejected.coerceRequiredFill optionalEmpty `seq` ())
+    ]
+testPayloadTraversals :: IO ()
+testPayloadTraversals = do
+  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)
+      payloads = V.fromList
+        [ SampleVertex (Point 0 0) 1
+        , SampleVertex (Point 4 0) 2
+        , SampleVertex (Point 4 4) 3
+        , SampleVertex (Point 0 4) 4
+        , SampleVertex (Point 1 2) 5
+        ]
+  built <- requireRight "payload traversal build" (delaunay defaults payloads)
+  let plain = buildTriangulation built
+      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))
+      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))
+      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))
+
+  -- 'overPayloads' is the traversal under 'Identity', so this is the traversal
+  -- identity law. It also says that rebuilding a payload store from its own
+  -- contents is not observable, which is the part a paged store could get
+  -- wrong: the traversal materializes pages the map would have left absent.
+  assertEqual "vertexPayloads identity" sample (overPayloads vertexPayloads id sample)
+  assertEqual "directedPayloads identity" sample (overPayloads directedPayloads id sample)
+  assertEqual "undirectedPayloads identity" sample (overPayloads undirectedPayloads id sample)
+  assertEqual "facePayloads identity" sample (overPayloads facePayloads id sample)
+
+  -- Each traversal and its named map are one function. The effectful
+  -- generalization is not allowed a second opinion about what relabeling means.
+  assertEqual "vertexPayloads agrees with mapVertices"
+    (mapVertices sampleLabel sample) (overPayloads vertexPayloads sampleLabel sample)
+  assertEqual "directedPayloads agrees with mapDirectedEdges"
+    (mapDirectedEdges (* 2) sample) (overPayloads directedPayloads (* 2) sample)
+  assertEqual "undirectedPayloads agrees with mapUndirectedEdges"
+    (mapUndirectedEdges ("<" <>) sample) (overPayloads undirectedPayloads ("<" <>) sample)
+  assertEqual "facePayloads agrees with mapFaces"
+    (mapFaces ("<" <>) sample) (overPayloads facePayloads ("<" <>) sample)
+
+  assertEqual "facePayloads composes"
+    (overPayloads facePayloads (("<" <>) . (<> ">")) sample)
+    (overPayloads facePayloads ("<" <>) (overPayloads facePayloads (<> ">") sample))
+
+  -- The class instances range over the face payload, being the last parameter.
+  assertEqual "fmap is the face payload map" (mapFaces ("<" <>) sample) (fmap ("<" <>) sample)
+  assertEqual "traverse is facePayloads"
+    (Just (overPayloads facePayloads ("<" <>) sample))
+    (traverse (Just . ("<" <>)) sample)
+  assertEqual "the Foldable instance is the face traversal"
+    (payloadList facePayloads sample) (foldr (:) [] sample)
+
+  -- Visit order, and the element default's place in it. A fold that skipped
+  -- the default would report the triangulation as holding one fewer face
+  -- payload than it holds.
+  assertEqual "vertexPayloads visits the vertices in order"
+    (map (vertexData sample) (vertices sample))
+    (payloadList vertexPayloads sample)
+  assertEqual "facePayloads visits the faces and then the default"
+    (map (faceData sample) (allFaces sample) <> [defaultFaceData (Internal.authoringElementDefaults sample)])
+    (payloadList facePayloads sample)
+
+  -- The point of the exercise: relabeling under an effect, with a refusal
+  -- reaching the caller instead of a half-relabelled triangulation.
+  let refuseAtThree :: SampleVertex -> Either String Int
+      refuseAtThree v = if sampleLabel v == 3 then Left "vertex three refuses" else Right (sampleLabel v * 10)
+      keepLabel :: SampleVertex -> Either String Int
+      keepLabel = Right . sampleLabel
+      decorate :: String -> Either String String
+      decorate = Right . ("<" <>)
+  assertEqual "an effectful relabel short-circuits"
+    (Left "vertex three refuses") (vertexPayloads refuseAtThree sample)
+  relabelled <- requireRight "effectful relabel" (vertexPayloads keepLabel sample)
+  assertEqual "a successful effectful relabel is the pure one"
+    (mapVertices sampleLabel sample) relabelled
+
+  -- The default travels through the traversal, and travels exactly once: the
+  -- store's fill and the element defaults are written from a single visit, so
+  -- an element created afterwards inherits precisely what the traversal made.
+  traversedFaces <- requireRight "effectful face relabel" (facePayloads decorate sample)
+  grown <- insertionTriangulation <$> requireRight "insertion after traversal" (insert traversedFaces (SampleVertex (Point 2 1) 6))
+  unless (numFaces grown > numFaces traversedFaces) $ fail "the insertion created no face"
+  unless ("<new-face" `elem` map (faceData grown) (allFaces grown)) $
+    fail ("new faces did not receive the traversed default: " <> show (map (faceData grown) (allFaces grown)))
+
+-- | The in-circle predicate is the orientation of the four points lifted to
+-- the paraboloid @z = x² + y²@. The referent is that 4×4 determinant evaluated
+-- exactly over 'Rational' — deliberately not the translated 3×3 the instances
+-- expand, which would only be the implementation checking its own algebra.
+testRewritePayloadIdentity :: IO ()
+testRewritePayloadIdentity = do
+  let defaults = ElementDefaults (0 :: Int) (0 :: Int) (0 :: Int)
+      target = Point 0.001_37 (-0.002_11)
+      corpus = randomPoints 0x1a2b3c4d 512
+  built <- requireRight "rewrite identity base" (delaunay defaults (V.fromList corpus))
+  let base = buildTriangulation built
+      directedTable = labelTable (map (directedKeyOf base) (directedEdges base))
+      undirectedTable = labelTable (map (undirectedKeyOf base) (undirectedEdges base))
+      faceTable = labelTable (map (faceKeyOf base) (innerFaces base))
+
+  -- Keys identify elements only if they are unique, so the premise is checked
+  -- rather than assumed: a collapsed table would silently weaken everything
+  -- below it into a test of nothing.
+  assertEqual "directed keys are unique" (length (directedEdges base)) (Map.size directedTable)
+  assertEqual "undirected keys are unique" (length (undirectedEdges base)) (Map.size undirectedTable)
+  assertEqual "face keys are unique" (length (innerFaces base)) (Map.size faceTable)
+
+  let withDirected =
+        List.foldl' (\t e -> setDirectedEdgeData t e (directedTable Map.! directedKeyOf base e)) base (directedEdges base)
+      withUndirected =
+        List.foldl' (\t e -> setUndirectedEdgeData t e (undirectedTable Map.! undirectedKeyOf base e)) withDirected (undirectedEdges base)
+      labelled =
+        List.foldl' (\t f -> setFaceData t f (faceTable Map.! faceKeyOf base f)) withUndirected (innerFaces base)
+  inserted <- requireRight "rewrite identity insert" (insert labelled target)
+  let result = insertionTriangulation inserted
+  assertValid "rewrite identity result" result
+  assertEqual "the point was genuinely inserted" Inserted (insertionDisposition inserted)
+
+  -- Both directions, on every plane. A surviving element keeps exactly its
+  -- label; everything else holds the default and nothing else.
+  assertPayloadSurvival "directed" directedTable
+    [(directedKeyOf result e, directedEdgeData result e) | e <- directedEdges result]
+  assertPayloadSurvival "undirected" undirectedTable
+    [(undirectedKeyOf result e, undirectedEdgeData result e) | e <- undirectedEdges result]
+  assertPayloadSurvival "face" faceTable
+    [(faceKeyOf result f, faceData result f) | f <- innerFaces result]
+
+  -- The flip signature. Every flip after an insertion joins the new vertex to
+  -- the far corner of a cavity quad, so a new edge is not evidence of one — a
+  -- split produces those too. A DESTROYED edge is: splitting a face destroys
+  -- nothing and adds exactly three edges and two faces, which is asserted here
+  -- so that the arithmetic holds, and under it every base key that is gone from
+  -- the result was flipped away. Without this the paragraphs above are a claim
+  -- about splits alone.
+  assertEqual "the point landed strictly inside a face"
+    (numUndirectedEdges base + 3, numFaces base + 2)
+    (numUndirectedEdges result, numFaces result)
+  let surviving = Set.fromList (map (undirectedKeyOf result) (undirectedEdges result))
+      flippedAway = filter (`Set.notMember` surviving) (Map.keys undirectedTable)
+  when (null flippedAway) (fail "the insertion caused no flip, so the flip case went unchecked")
+
+  -- The same claim across one transaction that both retires and creates.
+  -- Removal swap-compacts, which leaves a retired element's payload sitting in
+  -- the slot it vacated; the inserts that follow are handed those slots back.
+  -- Nothing else in the suite makes an allocation reissue a used slot.
+  let doomed = take 60 corpus
+      arrivals = randomPoints 0x5f3a19c2 60
+  (_, edited, _) <-
+    requireRight "rewrite identity session" $
+      withSession labelled (length arrivals) $ do
+        mapM_
+          (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))
+          doomed
+        mapM_ insertVertex arrivals
+  assertValid "rewrite identity session" edited
+  assertPayloadSurvival "session directed" directedTable
+    [(directedKeyOf edited e, directedEdgeData edited e) | e <- directedEdges edited]
+  assertPayloadSurvival "session undirected" undirectedTable
+    [(undirectedKeyOf edited e, undirectedEdgeData edited e) | e <- undirectedEdges edited]
+  assertPayloadSurvival "session face" faceTable
+    [(faceKeyOf edited f, faceData edited f) | f <- innerFaces edited]
+
+directedKeyOf
+  :: Triangulation mode vertex directed undirected face
+  -> DirectedEdgeId
+  -> (Point, Point)
+directedKeyOf triangulation edge =
+  ( vertexPoint triangulation (origin triangulation edge)
+  , vertexPoint triangulation (destination triangulation edge)
+  )
+
+undirectedKeyOf
+  :: Triangulation mode vertex directed undirected face
+  -> UndirectedEdgeId
+  -> (Point, Point)
+undirectedKeyOf triangulation edge =
+  case undirectedEndpoints triangulation edge of
+    (from, to) ->
+      let (left, right) = (vertexPoint triangulation from, vertexPoint triangulation to)
+       in if left <= right then (left, right) else (right, left)
+
+labelTable :: Ord key => [key] -> Map.Map key Int
+labelTable keys = Map.fromList (zip keys [1 ..])
+
+-- | Every element carries the label its key was given, or the default if its
+-- key is new. Both counts are asserted too: a store that kept everything and a
+-- store that kept nothing each satisfy one half of this on its own.
+assertPayloadSurvival :: (Ord key, Show key) => String -> Map.Map key Int -> [(key, Int)] -> IO ()
+assertPayloadSurvival plane labels elements = do
+  let kept = length (filter ((`Map.member` labels) . fst) elements)
+  when (kept == 0) (fail (plane <> ": the insertion perturbed every element"))
+  when (kept == length elements) (fail (plane <> ": the insertion perturbed no element"))
+  forM_ elements $ \(key, payload) ->
+    assertEqual (plane <> " label at " <> show key) (Map.findWithDefault 0 key labels) payload
diff --git a/test/native/Moonlight/Planar/PayloadTypeErrors.hs b/test/native/Moonlight/Planar/PayloadTypeErrors.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/PayloadTypeErrors.hs
@@ -0,0 +1,15 @@
+{-# LANGUAGE DataKinds #-}
+{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors #-}
+
+-- | Only the deliberate ill-typed clients defer errors, never the test driver.
+module Moonlight.Planar.PayloadTypeErrors (forgeRequiredFill, coerceRequiredFill) where
+
+import Data.Coerce (coerce)
+import Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedPaged, BoxedFill (..), FillRequirement (..), emptyBoxedPaged )
+
+forgeRequiredFill :: BoxedPaged 'RequiredFill Int
+forgeRequiredFill = emptyBoxedPaged NoFill
+
+coerceRequiredFill :: BoxedPaged 'OptionalFill Int -> BoxedPaged 'RequiredFill Int
+coerceRequiredFill = coerce
diff --git a/test/native/Moonlight/Planar/PredicateSpec.hs b/test/native/Moonlight/Planar/PredicateSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/PredicateSpec.hs
@@ -0,0 +1,146 @@
+-- | Predicate, binary64 format, and paraboloid lifting laws.
+module Moonlight.Planar.PredicateSpec
+  ( tests
+  ) where
+
+import Control.Monad ( forM_, unless )
+import Moonlight.Planar.Math (inCircle, orient2d)
+import Moonlight.Planar.Point (mitigateUnderflow, mkQueryPoint)
+import Moonlight.Planar.Internal.Types (validatePoint)
+import Moonlight.Planar.Scalar ( scalarBinaryFormat, scalarUnitRoundoff,
+  BinaryFormat(formatMantissaDigits, formatRadix) )
+import Moonlight.Planar.Point (Point(Point))
+import Support ( assertEqual, requireRight, randomPoints )
+import Moonlight.Planar.Internal.Predicates qualified as Admitted
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testPredicates
+    , testParaboloidLift
+    , testScalarFormat
+    ]
+
+testScalarFormat :: IO ()
+testScalarFormat = do
+  let binary64 = scalarBinaryFormat
+  assertEqual "binary64 radix" 2 (formatRadix binary64)
+  assertEqual "binary64 mantissa" 53 (formatMantissaDigits binary64)
+  assertEqual "binary64 unit roundoff" (encodeFloat 1 (-53)) (scalarUnitRoundoff :: Double)
+
+testPredicates :: IO ()
+testPredicates = do
+  let rawA, rawB, rawC :: Point
+      rawA = Point 0 0
+      rawB = Point 1 0
+      rawC = Point 0 1
+  a <- requireRight "predicate point a" (mkQueryPoint rawA)
+  b <- requireRight "predicate point b" (mkQueryPoint rawB)
+  c <- requireRight "predicate point c" (mkQueryPoint rawC)
+  midpointQuery <- requireRight "predicate midpoint" (mkQueryPoint (Point 0.5 0))
+  inside <- requireRight "incircle inside point" (mkQueryPoint (Point 0.25 0.25))
+  boundary <- requireRight "incircle boundary point" (mkQueryPoint (Point 1 1))
+  outside <- requireRight "incircle outside point" (mkQueryPoint (Point 2 2))
+  assertEqual "orientation left" GT (orient2d a b c)
+  assertEqual "orientation right" LT (orient2d b a c)
+  assertEqual "orientation collinear" EQ (orient2d a b midpointQuery)
+  assertEqual "incircle inside" GT (inCircle a b c inside)
+  assertEqual "incircle boundary" EQ (inCircle a b c boundary)
+  assertEqual "incircle outside" LT (inCircle a b c outside)
+  assertEqual "underflow mitigation" (Point 0 1) (mitigateUnderflow (Point 1.0e-44 1 :: Point))
+  _ <- requireRight "point validation" (validatePoint Nothing (Point 0 1 :: Point))
+  let large = encodeFloat 1 180 :: Double
+      ulp = encodeFloat 1 128 :: Double
+  largeA <- requireRight "large predicate point a" (mkQueryPoint (Point large large))
+  largeB <- requireRight "large predicate point b" (mkQueryPoint (Point (large + ulp) large))
+  largeC <- requireRight "large predicate point c" (mkQueryPoint (Point large (large + ulp)))
+  largeD <- requireRight "large predicate point d" (mkQueryPoint (Point (large + ulp) (large + ulp)))
+  assertEqual
+    "large exact orientation"
+    GT
+    (orient2d largeA largeB largeC)
+  assertEqual
+    "large exact cocircularity"
+    EQ
+    (inCircle largeA largeB largeC largeD)
+
+testParaboloidLift :: IO ()
+testParaboloidLift = do
+  let lifted :: Point -> (Rational, Rational, Rational)
+      lifted (Point x y) =
+        let (rx, ry) = (toRational x, toRational y) in (rx, ry, rx * rx + ry * ry)
+      minor3
+        :: (Rational, Rational, Rational)
+        -> (Rational, Rational, Rational)
+        -> (Rational, Rational, Rational)
+        -> Rational
+      minor3 (a1, a2, a3) (b1, b2, b3) (c1, c2, c3) =
+        a1 * (b2 * c3 - b3 * c2) - a2 * (b1 * c3 - b3 * c1) + a3 * (b1 * c2 - b2 * c1)
+      -- Laplace expansion of the lifted determinant along its column of ones.
+      liftedOrientation
+        :: (Rational, Rational, Rational)
+        -> (Rational, Rational, Rational)
+        -> (Rational, Rational, Rational)
+        -> (Rational, Rational, Rational)
+        -> Rational
+      liftedOrientation a b c d =
+        negate (minor3 b c d) + minor3 a c d - minor3 a b d + minor3 a b c
+      predicted :: Point -> Point -> Point -> Point -> Ordering
+      predicted a b c d = compare (liftedOrientation (lifted a) (lifted b) (lifted c) (lifted d)) 0
+      measured :: Point -> Point -> Point -> Point -> Ordering
+      measured (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
+        Admitted.inCircleCoordinates ax ay bx by cx cy dx dy
+      quadruples :: [Point] -> [(Point, Point, Point, Point)]
+      quadruples (a : b : c : d : rest) = (a, b, c, d) : quadruples rest
+      quadruples _ = []
+
+  forM_ (quadruples (randomPoints 0x51ca_0b17 1600)) $ \(a, b, c, d) ->
+    assertEqual ("in-circle is the lifted orientation at " <> show (a, b, c, d))
+      (predicted a b c d) (measured a b c d)
+
+  -- Exactly cocircular quadruples, which are exactly the ones the floating
+  -- filter must decline to answer. Every Pythagorean point of the radius-5
+  -- circle is representable without rounding, so 'EQ' here is a fact about the
+  -- geometry rather than about the arithmetic.
+  let ring =
+        [ Point x y
+        | (x, y) <-
+            [ (5, 0), (0, 5), (-5, 0), (0, -5)
+            , (3, 4), (4, 3), (-3, 4), (-4, 3)
+            , (3, -4), (4, -3), (-3, -4), (-4, -3)
+            ]
+        ] :: [Point]
+      indexed = zip [(0 :: Int) ..] ring
+      cocircular =
+        [ (a, b, c, d)
+        | (i, a) <- indexed, (j, b) <- indexed, j > i
+        , (k, c) <- indexed, k > j, (l, d) <- indexed, l > k
+        ]
+  unless (length cocircular == 495) $ fail ("expected 495 cocircular quadruples, got " <> show (length cocircular))
+  forM_ cocircular $ \(a, b, c, d) -> do
+    assertEqual ("cocircular points are cocircular at " <> show (a, b, c, d)) EQ (measured a b c d)
+    assertEqual ("the lift agrees on cocircularity at " <> show (a, b, c, d)) EQ (predicted a b c d)
+
+  -- The geometry the sign means, stated once against a circle anyone can read.
+  let (a, b, c) = (Point 1 0, Point 0 1, Point (-1) 0) :: (Point, Point, Point)
+  assertEqual "the centre is inside the circle" GT (measured a b c (Point 0 0))
+  assertEqual "the antipode is on the circle" EQ (measured a b c (Point 0 (-1)))
+  assertEqual "a distant point is outside" LT (measured a b c (Point 2 2))
+
+-- | A payload labels an element, and an element is its geometry. Every rewrite
+-- an insertion performs — an edge split, a face split, a Lawson flip — hands
+-- some slot a different element to hold, and the label the displaced one
+-- carried does not describe what took its place.
+--
+-- So: label every element of a triangulation by its own key, insert a point,
+-- and demand that an element whose key survives still carries exactly the label
+-- it was given while every element whose key is new carries the default and
+-- nothing else. Both directions are checked; either alone is satisfiable by a
+-- store that throws everything away.
+--
+-- The flip is the load-bearing case. Legalization is confluent, so which flips
+-- fire and in what order is not observable in the topology that comes out. A
+-- payload that rode through a flip would make it observable in the payload
+-- plane, and a triangulation that is a normal form in one component and a
+-- history in another is not a normal form.
diff --git a/test/native/Moonlight/Planar/RefinementAssertions.hs b/test/native/Moonlight/Planar/RefinementAssertions.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/RefinementAssertions.hs
@@ -0,0 +1,38 @@
+-- | Closure-receipt law shared by local refinement and constrained seams.
+module Moonlight.Planar.RefinementAssertions
+  ( assertClosureCountsAgree
+  ) where
+
+import Moonlight.Planar.Internal.HandleDefs ( FaceId, faceIdIndex, undirectedEdgeIdIndex )
+import Moonlight.Planar.Internal.Validation ( topologyClosureSelectionStats )
+import Moonlight.Planar.Types ( RefinementDomainResult(..), RefinementReceipt(..),
+  RefinementResult(..) )
+import Support ( assertEqual )
+import qualified Data.IntSet as IntSet
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+-- | The receipt's closure counts, taken without materializing the closure,
+-- equal the sizes of the closure selection built from the same initial and
+-- final permitted faces and the interface pairs the receipt reports.
+assertClosureCountsAgree
+  :: String
+  -> Set.Set FaceId
+  -> RefinementDomainResult mode vertex directed undirected face
+  -> IO ()
+assertClosureCountsAgree label initialPermitted refined =
+  assertEqual
+    (label <> ": closure counts equal the closure selection's sizes")
+    (topologyClosureSelectionStats (IntSet.union initialFaces finalFaces) interfacePairs after)
+    (refinementClosureStats receipt)
+ where
+  receipt = refinementDomainReceipt refined
+  after = refinedTriangulation (refinementDomainResult refined)
+  initialFaces = IntSet.fromList (fmap faceIdIndex (Set.toList initialPermitted))
+  finalFaces = IntSet.fromList (fmap faceIdIndex (V.toList (refinementFinalPermittedFaces receipt)))
+  interfacePairs =
+    IntSet.fromList
+      [ undirectedEdgeIdIndex edge
+      | (edge, _, _) <- V.toList (refinementFinalInterfaceIncidence receipt)
+      ]
diff --git a/test/native/Moonlight/Planar/RefinementSpec.hs b/test/native/Moonlight/Planar/RefinementSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/RefinementSpec.hs
@@ -0,0 +1,810 @@
+{-# LANGUAGE BangPatterns #-}
+
+-- | Refinement fixpoints, local-domain obligations, and mesh-quality laws.
+module Moonlight.Planar.RefinementSpec
+  ( tests
+  ) where
+
+import Control.Monad ( unless, when )
+import Data.Foldable ( traverse_ )
+import Data.List ( sort )
+import Moonlight.Planar.Cdt ( constrainedDelaunay, outerRegionFaces, constraintSegments,
+  joinSeparatedConstrained, ConstrainedSeamResult(constrainedSeamResultTriangulation) )
+import Moonlight.Planar.Dcel ( faceDirectedEdges, faceVertices, incidentFace, isConstraintEdge,
+  numConstraints, numFaces, numInnerFaces, numVertices, outerFace, undirectedEndpoints, vertexPoint
+  )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( undirectedEdges, innerFaces )
+import Moonlight.Planar.Internal.Canonical ( canonicalize )
+import Moonlight.Planar.Internal.HandleDefs ( FaceId(FaceId), asUndirected, directedPair )
+import Moonlight.Planar.Internal.Paged ( PublicationStats(..) )
+import Moonlight.Planar.Internal.Types ( RefinementParameters(..) )
+import Moonlight.Planar.Math ( squaredDistanceWide )
+import Moonlight.Planar.Refinement ( refine, refineWithinDomain )
+import Moonlight.Planar.RefinementAssertions ( assertClosureCountsAgree )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (defaultRefinementParameters, unitElementDefaults, BuildError(RefinementDomainWouldCrossInterface, RefinementDomainRequiresFiniteVertexBudget,
+  RefinementDomainRequiresConvexHullPreservation, RefinementDomainRequiresConstraintPreservation,
+  RefinementDomainForbidsOuterFaceExclusion, RefinementOversizedEdge), ClosureStats(closureVertices,
+  closureFaces, closureDirectedEdges), BuildResult(buildTriangulation), RefinementDomainResult(refinementDomainReceipt, refinementDomainResult), RefinementReceipt(refinementVisitedProtectedFaces, refinementFinalInterfaceIncidence,
+  refinementClosureStats, refinementPublicationStats, refinementCreatedFaces,
+  refinementInterfaceBoundaryReads, refinementAttemptedBoundaryCrossings,
+  refinementFinalPermittedFaces), RefinementResult(refinementAddedVertices, refinementExcludedFaces,
+  refinementComplete, refinedTriangulation), Triangulation)
+import Support ( assertEqual, assertValid, requireRight )
+import Moonlight.Planar.Internal.Predicates qualified as Admitted
+import qualified Moonlight.Planar.Dcel as Dcel
+import Moonlight.Planar.Internal.Representation qualified as Internal
+import qualified Data.List as List
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testRefinementCompletionIsAFixpoint
+    , testLocalRefinementRejectsEncroachedHullSplit
+    , testConstrainedRefinement
+    , testCheckedLocalRefinement
+    , testMaximumEdgeLengthQuality
+    , testLocalRefinementRejectsTrueInterfaceCrossing
+    , testRepeatedBoundaryAdjacentRefinement
+    ]
+
+-- | What @refinementComplete@ claims is that the quality worklist drained. The
+-- assertable content of that claim is a fixpoint: a second pass under the same
+-- parameters can admit nothing. The complementary run is budget-starved, where
+-- the run must stop short and spend exactly what it was given.
+testRefinementCompletionIsAFixpoint :: IO ()
+testRefinementCompletionIsAFixpoint = do
+  -- Barrier parity needs constraints to bound a domain: on an unconstrained
+  -- mesh every face sits at depth zero, so excluding outer faces excludes all
+  -- of them and the worklist drains having refined nothing.
+  bounded <-
+    requireRight "refinement fixpoint domain" $
+      constrainedDelaunay
+        unitElementDefaults
+        (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8])
+        (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
+  let source = buildTriangulation bounded
+      parameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 500
+          , refineMaxArea = Just 3
+          , refineExcludeOuterFaces = True
+          }
+  drained <- requireRight "drained refinement" (refine id parameters source)
+  unless (refinementComplete drained) $
+    fail "the refinement budget was too small to drain the worklist"
+  -- Without this the fixpoint below is vacuous: a run that refined nothing
+  -- trivially admits nothing on a second pass.
+  unless (refinementAddedVertices drained > 0) $
+    fail "the drained run inserted no Steiner points, so the fixpoint proves nothing"
+  assertValid "drained refinement" (refinedTriangulation drained)
+  again <- requireRight "second refinement pass" (refine id parameters (refinedTriangulation drained))
+  assertEqual
+    "a drained worklist admits nothing on a second pass"
+    0
+    (refinementAddedVertices again)
+  starved <-
+    requireRight
+      "starved refinement"
+      (refine id parameters {refineMaxAdditionalVertices = Just 3} source)
+  when (refinementComplete starved) $
+    fail "a three-vertex budget reported a drained worklist"
+  assertEqual "a starved run spends exactly its budget" 3 (refinementAddedVertices starved)
+
+testLocalRefinementRejectsEncroachedHullSplit :: IO ()
+testLocalRefinementRejectsEncroachedHullSplit = do
+  built <-
+    requireRight
+      "encroached hull source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          (V.fromList [Point 0 0, Point 2 0, Point 0.1 0.1])
+          V.empty
+      )
+  let source =
+        Internal.geometryOnlyPublication
+          (buildTriangulation built)
+      permitted = Set.fromList (innerFaces source)
+      parameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 1
+          , refineMaxArea = Just 0.001
+          , refineKeepConstraintEdges = True
+          }
+      hullEdges =
+        [ edge
+        | edge <- undirectedEdges source
+        , let (forward, backward) = directedPair edge
+        , incidentFace source forward == outerFace || incidentFace source backward == outerFace
+        ]
+      encroachedHull edge =
+        let (fromVertex, toVertex) = Dcel.undirectedEndpoints source edge
+            fromPoint = vertexPoint source fromVertex
+            toPoint = vertexPoint source toVertex
+            oppositeFaceEdge =
+              case directedPair edge of
+                (forward, backward)
+                  | incidentFace source forward == outerFace -> backward
+                  | otherwise -> forward
+            oppositePoint =
+              let innerFace = incidentFace source oppositeFaceEdge
+               in if innerFace == outerFace
+                    then Nothing
+                    else
+                      case Dcel.faceVertices source innerFace of
+                        verticesInFace ->
+                          case filter (/= fromVertex) (filter (/= toVertex) verticesInFace) of
+                            [vertex] -> Just (vertexPoint source vertex)
+                            _ -> Nothing
+         in maybe False (Admitted.inDiametralCircle fromPoint toPoint) oppositePoint
+  unless (any encroachedHull hullEdges) $
+    fail "encroached hull fixture did not produce an encroached hull pair"
+  refined <-
+    requireRight
+      "encroached hull local refinement"
+      (refineWithinDomain (const ()) parameters permitted source)
+  assertClosureCountsAgree "encroached hull local refinement" permitted refined
+  let refinementResult = refinementDomainResult refined
+      result = refinedTriangulation refinementResult
+  unless (refinementComplete refinementResult) $
+    fail "encroached hull local refinement exhausted without a fixpoint"
+  assertEqual "encroached hull split count" (numVertices source) (numVertices result)
+  assertEqual
+    "encroached hull refinement retains cached frontier"
+    (Internal.triSeamFrontier source)
+    (Internal.triSeamFrontier result)
+
+  secondBuild <-
+    requireRight
+      "encroached hull join source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          (V.fromList [Point 4 0, Point 6 0, Point 6 1, Point 4 1])
+          V.empty
+      )
+  let second =
+        Internal.geometryOnlyPublication
+          (buildTriangulation secondBuild)
+  joined <-
+    requireRight
+      "encroached hull repeated join"
+      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters result second)
+  assertValid
+    "encroached hull repeated join"
+    (constrainedSeamResultTriangulation joined)
+
+testConstrainedRefinement :: IO ()
+testConstrainedRefinement = do
+  cdtBuild <- requireRight "bounded domain" $ constrainedDelaunay
+    unitElementDefaults
+    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 4 2, Point 4 6])
+    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
+  let cdt = buildTriangulation cdtBuild
+      parameters = defaultRefinementParameters
+        { refineMaxAdditionalVertices = Just 80
+        , refineMaxArea = Just 3
+        , refineMaxRadiusEdgeRatio = Just 1.4
+        , refineExcludeOuterFaces = True
+        , refineKeepConstraintEdges = False
+        }
+  refined <- requireRight "constrained refinement" (refine id parameters cdt)
+  let result = refinedTriangulation refined
+  canonicalResult <- requireRight "canonical constrained refinement" (canonicalize result)
+  unless (refinementAddedVertices refined > 0) $ fail "constrained refinement inserted no Steiner points"
+  assertValid "constrained refinement" result
+  unless (numConstraints result >= numConstraints cdt) $
+    fail "constraint splitting lost the constrained boundary"
+  assertEqual
+    "canonical publication preserves constraint segments"
+    (constraintSegments result)
+    (constraintSegments canonicalResult)
+
+  -- Refinement maintains the outer-region classification incrementally, from
+  -- the touched patch alone. That is a claim about what an insertion cannot
+  -- reach, so it is gated against an independent flood over the finished mesh
+  -- rather than trusted. The domain here is deliberately narrower than its
+  -- convex hull, so the excluded set is non-empty and the two can disagree.
+  notchBuild <- requireRight "notched domain" $ constrainedDelaunay
+    unitElementDefaults
+    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 13 4, Point 4 4])
+    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
+  let notch = buildTriangulation notchBuild
+      notchMaximumEdgeLength = 6.2
+      notchParameters = defaultRefinementParameters
+        { refineMaxAdditionalVertices = Just 120
+        , refineMaxArea = Just 1.5
+        , refineMaxEdgeLength = Just notchMaximumEdgeLength
+        , refineExcludeOuterFaces = True
+        , refineKeepConstraintEdges = False
+        }
+  notchRefined <- requireRight "notched refinement" (refine id notchParameters notch)
+  let notchResult = refinedTriangulation notchRefined
+      maintained = sort (V.toList (refinementExcludedFaces notchRefined))
+      independent = sort (outerRegionFaces notchResult)
+      excludedEdgeLengthsSquared =
+        [ squaredDistanceWide
+            (vertexPoint notchResult fromVertex)
+            (vertexPoint notchResult toVertex)
+        | face <- maintained
+        , directed <- faceDirectedEdges notchResult face
+        , let (fromVertex, toVertex) =
+                undirectedEndpoints notchResult (asUndirected directed)
+        ]
+  unless (refinementAddedVertices notchRefined > 0) $ fail "notched refinement inserted no Steiner points"
+  unless (refinementComplete notchRefined) $ fail "notched maximum-edge refinement did not complete"
+  when (null independent) $ fail "notched domain produced no outer region: the gate is vacuous"
+  unless
+    (any (> notchMaximumEdgeLength * notchMaximumEdgeLength) excludedEdgeLengthsSquared)
+    (fail "notched fixture has no excluded maximum-edge violation")
+  assertEqual "incremental exclusion agrees with an independent flood" independent maintained
+  assertValid "notched refinement" notchResult
+
+  annulusBuild <- requireRight "annular domain" $ constrainedDelaunay
+    unitElementDefaults
+    ( V.fromList
+        [ Point 0 0
+        , Point 12 0
+        , Point 12 12
+        , Point 0 12
+        , Point 4 4
+        , Point 8 4
+        , Point 8 8
+        , Point 4 8
+        ]
+    )
+    ( V.fromList
+        [ (0, 1)
+        , (1, 2)
+        , (2, 3)
+        , (3, 0)
+        , (4, 5)
+        , (5, 6)
+        , (6, 7)
+        , (7, 4)
+        ]
+    )
+  let annulus = buildTriangulation annulusBuild
+      annulusParameters :: Int -> Maybe Double -> RefinementParameters
+      annulusParameters budget maximumArea =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just budget
+          , refineMaxArea = maximumArea
+          , refineExcludeOuterFaces = True
+          , refineKeepConstraintEdges = False
+          }
+  annulusUnchanged <-
+    requireRight
+      "budget-zero annular refinement"
+      (refine id (annulusParameters 0 Nothing) annulus)
+  let initialAnnulusOutside = sort (outerRegionFaces annulus)
+      budgetZeroOutside =
+        sort (V.toList (refinementExcludedFaces annulusUnchanged))
+  assertEqual "annulus has one two-crossing hole" 2 (length initialAnnulusOutside)
+  assertEqual
+    "budget-zero refinement uses the authoritative annulus classification"
+    initialAnnulusOutside
+    budgetZeroOutside
+
+  annulusRefined <-
+    requireRight
+      "positive-budget annular refinement"
+      (refine id (annulusParameters 40 (Just 4)) annulus)
+  let refinedAnnulus = refinedTriangulation annulusRefined
+      maintainedAnnulus =
+        sort (V.toList (refinementExcludedFaces annulusRefined))
+      independentAnnulus = sort (outerRegionFaces refinedAnnulus)
+  unless (refinementAddedVertices annulusRefined > 0) $
+    fail "annular refinement inserted no Steiner points"
+  assertEqual
+    "incremental annulus exclusion agrees with authoritative barrier depth"
+    independentAnnulus
+    maintainedAnnulus
+  assertValid "annular constrained refinement" refinedAnnulus
+
+  -- A tiny constraint can share a mesh with coordinates of vastly different
+  -- magnitude. Encroachment is discovered by a local cavity walk, so there is
+  -- no broad phase for the range to overflow; the pin is that the scaled
+  -- circumcenter and diametral predicates still produce exactly one vertex.
+  wideGridBuild <- requireRight "wide-grid constrained domain" $ constrainedDelaunay
+    unitElementDefaults
+    ( V.fromList
+        [ Point 0 0
+        , Point 2.0e-43 0
+        , Point 1.0e60 0
+        , Point 0 1.0e60
+        ] :: V.Vector (Point)
+    )
+    (V.singleton (0, 1))
+  wideGridRefined <-
+    requireRight
+      "wide-grid constrained refinement"
+      ( refine
+          id
+          defaultRefinementParameters
+            { refineMaxAdditionalVertices = Just 1
+            , refineMaxArea = Just 1.0e119
+            , refineKeepConstraintEdges = False
+            }
+          (buildTriangulation wideGridBuild)
+      )
+  assertEqual "wide-grid refinement count" 1 (refinementAddedVertices wideGridRefined)
+  assertValid "wide-grid constrained refinement" (refinedTriangulation wideGridRefined)
+
+-- | A local domain is a closed section, not a hopeful initial queue. Its
+-- interface is exact, its protected faces survive point-for-point, and the
+-- receipt contains no visit to the protected side.
+testCheckedLocalRefinement :: IO ()
+testCheckedLocalRefinement = do
+  built <-
+    requireRight
+      "checked local refinement source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          ( V.fromList
+              [ Point 0 0
+              , Point 4.1 0
+              , Point 8 0.2
+              , Point 0.1 4
+              , Point 4 4.2
+              , Point 8.1 4
+              ]
+          )
+          (V.singleton (2, 5))
+      )
+  let source = buildTriangulation built
+      permitted =
+        Set.fromList
+          [ face
+          | face <- innerFaces source
+          , faceCentroidX source face < 4.05
+          ]
+      interface =
+        Set.fromList
+          [ edge
+          | edge <- undirectedEdges source
+          , let (forward, backward) = directedPair edge
+                forwardFace = incidentFace source forward
+                backwardFace = incidentFace source backward
+          , forwardFace /= outerFace
+          , backwardFace /= outerFace
+          , Set.member forwardFace permitted /= Set.member backwardFace permitted
+          ]
+      protected = filter (`Set.notMember` permitted) (innerFaces source)
+      protectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint source) (faceVertices source face)))) protected
+      calmParameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 3
+          , refineMaxRadiusEdgeRatio = Nothing
+          , refineKeepConstraintEdges = True
+          }
+      crossingParameters = calmParameters{refineMaxArea = Just 1}
+  unless (not (Set.null permitted) && not (null protected) && not (Set.null interface)) $
+    fail "checked local refinement fixture did not form a nontrivial cover"
+  case refineWithinDomain id calmParameters{refineMaxAdditionalVertices = Nothing} permitted source of
+    Left RefinementDomainRequiresFiniteVertexBudget -> pure ()
+    outcome -> fail ("checked local refinement accepted an unbounded local budget: " <> either show (const "success") outcome)
+  refined <-
+    requireRight
+      "checked local refinement"
+      (refineWithinDomain id calmParameters permitted source)
+  assertClosureCountsAgree "checked local refinement" permitted refined
+  let localResult = refinementDomainResult refined
+      target = refinedTriangulation localResult
+      targetProtectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint target) (faceVertices target face)))) protected
+      receipt = refinementDomainReceipt refined
+      finalPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))
+      expectedFinalInterfaceIncidence =
+        V.fromList
+          [ if Set.member sourceForwardFace permitted
+              then (edge, sourceBackwardFace, targetForwardFace)
+              else (edge, sourceForwardFace, targetBackwardFace)
+          | edge <- Set.toAscList interface
+          , let (forward, backward) = directedPair edge
+                sourceForwardFace = incidentFace source forward
+                sourceBackwardFace = incidentFace source backward
+                targetForwardFace = incidentFace target forward
+                targetBackwardFace = incidentFace target backward
+          ]
+      actualFinalInterfaceIncidence = refinementFinalInterfaceIncidence receipt
+  assertEqual "checked local protected face restriction" protectedSignatures targetProtectedSignatures
+  assertEqual
+    "checked local final permitted lineage"
+    permitted
+    finalPermitted
+  assertEqual
+    "checked local final interface incidence"
+    expectedFinalInterfaceIncidence
+    actualFinalInterfaceIncidence
+  assertEqual
+    "checked local final interface edge set"
+    interface
+    (Set.fromList [edge | (edge, _, _) <- V.toList actualFinalInterfaceIncidence])
+  traverse_
+    (\(edge, protectedFace, finalPermittedFace) ->
+       unless
+         ( Set.notMember protectedFace permitted
+             && Set.member finalPermittedFace finalPermitted
+         )
+         ( fail
+             ( "checked local final interface incidence misclassified "
+                 <> show (edge, protectedFace, finalPermittedFace)
+             )
+         )
+    )
+    actualFinalInterfaceIncidence
+  assertEqual "checked local protected visit receipt" V.empty (refinementVisitedProtectedFaces receipt)
+  assertEqual "checked local boundary crossing receipt" 0 (refinementAttemptedBoundaryCrossings receipt)
+  let closureStats = refinementClosureStats receipt
+  unless
+    ( closureFaces closureStats > 0
+        && closureDirectedEdges closureStats > 0
+        && closureVertices closureStats > 0
+    ) $
+    fail "checked local refinement did not record its selected closure"
+  assertEqual
+    "checked local refinement does not enumerate resident unboxed base pages"
+    0
+    (publicationUnboxedBasePageEnumerations (refinementPublicationStats receipt))
+  assertEqual
+    "checked local protected constraint restriction"
+    (constraintSegments source)
+    (constraintSegments target)
+  assertValid "checked local refinement" target
+  case refineWithinDomain id crossingParameters permitted source of
+    Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
+    Left obstruction -> fail ("checked local refinement returned the wrong crossing obstruction: " <> show obstruction)
+    Right _ -> fail "checked local refinement silently crossed its immutable interface"
+  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted source of
+    Left RefinementDomainRequiresConvexHullPreservation -> pure ()
+    outcome -> fail ("checked local refinement accepted hull mutation: " <> either show (const "success") outcome)
+  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted source of
+    Left RefinementDomainRequiresConstraintPreservation -> pure ()
+    outcome -> fail ("checked local refinement accepted constraint mutation: " <> either show (const "success") outcome)
+  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted source of
+    Left RefinementDomainForbidsOuterFaceExclusion -> pure ()
+    outcome -> fail ("checked local refinement accepted outer-face exclusion: " <> either show (const "success") outcome)
+  wholeRefined <-
+    requireRight
+      "checked whole-section refinement"
+      ( refineWithinDomain
+          id
+          crossingParameters
+          (Set.fromList (innerFaces source))
+          source
+      )
+  let wholeResult = refinementDomainResult wholeRefined
+      wholeReceipt = refinementDomainReceipt wholeRefined
+  unless (refinementAddedVertices wholeResult > 0) $
+    fail "checked whole-section refinement did not improve its admitted section"
+  unless (not (V.null (refinementCreatedFaces wholeReceipt))) $
+    fail "checked whole-section refinement omitted semantically rewritten face slots"
+  unless
+    ( V.any
+        (\(FaceId raw) -> toInteger raw < toInteger (numFaces source))
+        (refinementCreatedFaces wholeReceipt)
+    ) $
+    fail "checked whole-section refinement omitted recycled face slots"
+  assertEqual
+    "checked whole-section final permitted lineage"
+    (Set.fromList (innerFaces (refinedTriangulation wholeResult)))
+    (Set.fromList (V.toList (refinementFinalPermittedFaces wholeReceipt)))
+  assertValid "checked whole-section refinement" (refinedTriangulation wholeResult)
+ where
+  faceCentroidX
+    :: Triangulation mode vertex directed undirected face
+    -> FaceId
+    -> Double
+  faceCentroidX triangulation face =
+    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of
+      [] -> 0
+      points ->
+        sum [x | Point x _ <- points] / fromIntegral (length points)
+
+-- | A maximum edge bound is a local geometric law, not a rendering preference:
+-- a long, thin face is split along its longest edge even when its area is below
+-- the minimum-area short-circuit. A budget-limited pass remains resumable;
+-- once the longest edge is frozen at the local interface, a completed pass
+-- returns the exact typed quality obstruction instead of crossing it.
+testMaximumEdgeLengthQuality :: IO ()
+testMaximumEdgeLengthQuality = do
+  built <-
+    requireRight
+      "maximum-edge long-thin source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          ( V.fromList
+              [ Point 0 0
+              , Point 10 0
+              , Point 10 1
+              , Point 0 1
+              ]
+          )
+          (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
+      )
+  let source = buildTriangulation built
+      allPermitted = Set.fromList (innerFaces source)
+      parameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 1
+          , refineMinArea = Just 6
+          , refineMaxEdgeLength = Just 10.01
+          , refineMaxRadiusEdgeRatio = Nothing
+          , refineKeepConstraintEdges = True
+          }
+  partial <-
+    requireRight
+      "maximum-edge budget-limited refinement"
+      (refineWithinDomain id parameters allPermitted source)
+  let partialResult = refinementDomainResult partial
+      partialTarget = refinedTriangulation partialResult
+      partialReceipt = refinementDomainReceipt partial
+      resumedPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces partialReceipt))
+  unless (refinementAddedVertices partialResult > 0) $
+    fail "maximum-edge refinement did not split the long edge before the area floor"
+  when (refinementComplete partialResult) $
+    fail "maximum-edge budget-limited refinement reported completion"
+  assertValid "maximum-edge budget-limited refinement" partialTarget
+  resumed <-
+    requireRight
+      "maximum-edge resumed refinement"
+      ( refineWithinDomain
+          id
+          parameters{refineMaxAdditionalVertices = Just 8}
+          resumedPermitted
+          partialTarget
+      )
+  let resumedResult = refinementDomainResult resumed
+      resumedReceipt = refinementDomainReceipt resumed
+  unless (refinementComplete resumedResult) $
+    fail "maximum-edge resumed refinement did not drain its local worklist"
+  assertMaximumEdgeLength
+    "maximum-edge resumed refinement"
+    10.01
+    (refinedTriangulation resumedResult)
+    (V.toList (refinementFinalPermittedFaces resumedReceipt))
+
+  case
+      refine
+        id
+        parameters
+          { refineMaxAdditionalVertices = Just 8
+          , refineMaxEdgeLength = Just 9
+          }
+        source of
+    Left (RefinementOversizedEdge _ edge actual bound)
+      | isConstraintEdge source edge && actual > bound -> pure ()
+    Left obstruction ->
+      fail ("maximum-edge global audit returned the wrong obstruction: " <> show obstruction)
+    Right _ -> fail "maximum-edge global refinement accepted an oversized kept constraint"
+
+  case innerFaces source of
+    firstFace : _ -> do
+      let permitted = Set.singleton firstFace
+          interface =
+            Set.fromList
+              [ edge
+              | edge <- undirectedEdges source
+              , let (forward, backward) = directedPair edge
+                    forwardFace = incidentFace source forward
+                    backwardFace = incidentFace source backward
+              , forwardFace /= outerFace
+              , backwardFace /= outerFace
+              , Set.member forwardFace permitted /= Set.member backwardFace permitted
+              ]
+      let blocked =
+            refineWithinDomain
+              id
+              parameters{refineMaxAdditionalVertices = Just 8}
+              permitted
+              source
+      case blocked of
+        Left (RefinementOversizedEdge face edge actual bound)
+          | Set.member face permitted
+              && Set.member edge interface
+              && actual > bound -> pure ()
+        Left obstruction ->
+          fail ("maximum-edge interface returned the wrong obstruction: " <> show obstruction)
+        Right _ -> fail "maximum-edge interface was silently crossed or accepted"
+    [] -> fail "maximum-edge source has no inner face"
+
+-- | A circumcenter can be inside the admitted face while its legalization
+-- cavity also meets the immutable interface. That is a true crossing demand,
+-- not the boundary-limited locator read discharged by the local interpreter.
+testLocalRefinementRejectsTrueInterfaceCrossing :: IO ()
+testLocalRefinementRejectsTrueInterfaceCrossing = do
+  built <-
+    requireRight
+      "deterministic true interface crossing source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          ( V.fromList
+              [ Point 0 0
+              , Point 2 0
+              , Point 1 (sqrt 3)
+              , Point (-1) (sqrt 3)
+              ]
+          )
+          V.empty
+      )
+  let source = buildTriangulation built
+      equilateralFacePoints = sort [Point 0 0, Point 2 0, Point 1 (sqrt 3)]
+      permittedFace =
+        List.find
+          ( \face ->
+              sort (fmap (vertexPoint source) (faceVertices source face))
+                == equilateralFacePoints
+          )
+          (innerFaces source)
+  assertEqual "deterministic crossing source has two inner faces" 2 (numInnerFaces source)
+  case permittedFace of
+    Nothing -> fail "deterministic crossing source omitted its equilateral face"
+    Just face -> do
+      let permitted = Set.singleton face
+          interface =
+            Set.fromList
+              [ edge
+              | edge <- undirectedEdges source
+              , let (forward, backward) = directedPair edge
+                    forwardFace = incidentFace source forward
+                    backwardFace = incidentFace source backward
+              , forwardFace /= outerFace
+              , backwardFace /= outerFace
+              , Set.member forwardFace permitted /= Set.member backwardFace permitted
+              ]
+          parameters =
+            defaultRefinementParameters
+              { refineMaxAdditionalVertices = Just 1
+              , refineMaxArea = Just 0.5
+              , refineMaxRadiusEdgeRatio = Nothing
+              , refineKeepConstraintEdges = True
+              }
+      assertEqual
+        "deterministic crossing source has one interface edge"
+        1
+        (Set.size interface)
+      case refineWithinDomain id parameters permitted source of
+        Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
+        Left obstruction ->
+          fail
+            ( "deterministic true interface crossing returned the wrong obstruction: "
+                <> show obstruction
+            )
+        Right _ ->
+          fail "deterministic true interface crossing was silently accepted"
+
+testRepeatedBoundaryAdjacentRefinement :: IO ()
+testRepeatedBoundaryAdjacentRefinement = do
+  built <-
+    requireRight
+      "repeated boundary-adjacent constrained source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          ( V.fromList
+              [ Point 0 0
+              , Point 4 0
+              , Point 8 0
+              , Point 12 0
+              , Point 0 4
+              , Point 4 4
+              , Point 8 4
+              , Point 12 4
+              ]
+          )
+          (V.singleton (2, 6))
+      )
+  let source = buildTriangulation built
+      sourcePermitted = leftSection source
+      fullParameters =
+        defaultRefinementParameters
+          { refineMaxAdditionalVertices = Just 20
+          , refineMaxArea = Just 1
+          , refineMaxRadiusEdgeRatio = Nothing
+          , refineKeepConstraintEdges = True
+          }
+  fullRefinement <-
+    requireRight
+      "full boundary-adjacent refinement"
+      (refineWithinDomain id fullParameters sourcePermitted source)
+  let fullResult = refinementDomainResult fullRefinement
+      fullTarget = refinedTriangulation fullResult
+      fullReceipt = refinementDomainReceipt fullRefinement
+  unless (refinementAddedVertices fullResult > 0) $
+    fail "full boundary-adjacent refinement did not refine its admitted section"
+  unless (refinementInterfaceBoundaryReads fullReceipt > 0) $
+    fail "boundary-adjacent refinement did not report its immutable interface read"
+  assertEqual
+    "boundary-adjacent crossing attempts"
+    0
+    (refinementAttemptedBoundaryCrossings fullReceipt)
+  assertEqual
+    "full boundary-adjacent constraint restriction"
+    (constraintSegments source)
+    (constraintSegments fullTarget)
+  assertEqual
+    "completed boundary-adjacent refinement is idempotent"
+    True
+    (refinementComplete fullResult)
+
+  let progressParameters = fullParameters{refineMaxAdditionalVertices = Just 1}
+  progressFirstRefinement <-
+    requireRight
+      "first bounded boundary-adjacent refinement"
+      (refineWithinDomain id progressParameters sourcePermitted source)
+  let progressFirstResult = refinementDomainResult progressFirstRefinement
+      progressFirstTarget = refinedTriangulation progressFirstResult
+      progressFirstReceipt = refinementDomainReceipt progressFirstRefinement
+      progressPermitted =
+        Set.fromList (V.toList (refinementFinalPermittedFaces progressFirstReceipt))
+  unless (refinementAddedVertices progressFirstResult > 0) $
+    fail "first bounded boundary-adjacent refinement did not refine its admitted section"
+  when (refinementComplete progressFirstResult) $
+    fail "first bounded boundary-adjacent refinement unexpectedly reached a fixpoint"
+  progressSecondRefinement <-
+    requireRight
+      "repeated boundary-adjacent refinement"
+      ( refineWithinDomain
+          id
+          progressParameters
+          progressPermitted
+          progressFirstTarget
+      )
+  let progressSecondResult = refinementDomainResult progressSecondRefinement
+      progressSecondTarget = refinedTriangulation progressSecondResult
+      progressSecondReceipt = refinementDomainReceipt progressSecondRefinement
+  unless (refinementAddedVertices progressSecondResult > 0) $
+    fail "repeated boundary-adjacent refinement did not retain its dynamic join-face support"
+  assertEqual
+    "repeated boundary-adjacent protected visits"
+    V.empty
+    (refinementVisitedProtectedFaces progressSecondReceipt)
+  assertEqual
+    "repeated boundary-adjacent constraint restriction"
+    (constraintSegments progressFirstTarget)
+    (constraintSegments progressSecondTarget)
+  assertValid "repeated boundary-adjacent refinement" progressSecondTarget
+ where
+  leftSection
+    :: Triangulation mode vertex directed undirected face
+    -> Set.Set FaceId
+  leftSection triangulation =
+    Set.fromList
+      [ face
+      | face <- innerFaces triangulation
+      , faceCentroidX triangulation face < 8
+      ]
+  faceCentroidX
+    :: Triangulation mode vertex directed undirected face
+    -> FaceId
+    -> Double
+  faceCentroidX triangulation face =
+    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of
+      [] -> 0
+      points ->
+        sum [x | Point x _ <- points] / fromIntegral (length points)
+
+assertMaximumEdgeLength
+  :: String
+  -> Double
+  -> Triangulation mode vertex directed undirected face
+  -> [FaceId]
+  -> IO ()
+assertMaximumEdgeLength label maximumLength triangulation faces =
+  unless (null violations) $
+    fail (label <> " oversized edges: " <> show (take 1 violations))
+ where
+  !maximumSquaredLength = maximumLength * maximumLength
+  violations =
+    [ (face, edge, squaredLength)
+    | face <- faces
+    , directed <- faceDirectedEdges triangulation face
+    , let edge = asUndirected directed
+          (fromVertex, toVertex) = undirectedEndpoints triangulation edge
+          squaredLength =
+            squaredDistanceWide
+              (vertexPoint triangulation fromVertex)
+              (vertexPoint triangulation toVertex)
+    , squaredLength > maximumSquaredLength
+    ]
diff --git a/test/native/Moonlight/Planar/RegionSpec.hs b/test/native/Moonlight/Planar/RegionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/RegionSpec.hs
@@ -0,0 +1,489 @@
+-- | Resident boundary, exact-region authoring, and publication laws.
+module Moonlight.Planar.RegionSpec
+  ( tests
+  ) where
+
+import Control.Monad ( when )
+import Data.Foldable ( toList, traverse_ )
+import Data.List ( sort )
+import Data.List.NonEmpty ( NonEmpty(..) )
+import Moonlight.Planar.BoundaryFixtures ( requireLabelledComponent, requireComponentBoundary,
+  assertLoopWinding, assertBoundaryShape )
+import Moonlight.Planar.BulkLoad ( delaunay )
+import Moonlight.Planar.CellSet ( CellSelectionError(..), closeFaceCellSet, exactCellSet,
+  exactCellSetEdgeCount, exactCellSetFaceCount, exactCellSetVertexCount )
+import Moonlight.Planar.Dcel ( destination, outerFace, vertexOutgoingEdges, vertexPoint )
+import Moonlight.Planar.FloodFillIterator ( BoundaryObstruction(BoundaryPinch,
+  BoundaryComponentFaceOutOfRange), componentBoundary, componentBoundaryLoops, faceComponents,
+  BoundaryLoop(..), BoundaryOrientation(BoundaryClockwise, BoundaryCounterClockwise), FaceComponent,
+  RegionBoundary(regionBoundaryOuterLoop, regionBoundaryHoleLoops) )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( undirectedEdges, innerFaces )
+import Moonlight.Planar.Internal.HandleDefs ( FaceId(FaceId) )
+import Moonlight.Planar.Internal.Region.Publication ( labelledPlanarLayerFromExactCoordinates )
+import Moonlight.Planar.Math ( centroid )
+import Moonlight.Planar.MeshFixtures ( requirePointBuild, pointMeshOf )
+import Moonlight.Planar.Region ( PolygonComponent, labelledPlanarLayer, planarLayerRegions,
+  polygonHoleLoops, polygonOuterLoop, exactLoopPoints, planarLayer, planarRegion,
+  planarRegionComponents, regionPointLocation, PlanarLayer, RegionPointLocation(RegionExterior,
+  RegionInterior), RegionPublicationError(RegionBoundaryObstruction, RegionCoordinateMissing) )
+import Moonlight.Planar.Point (Point(Point, pointX))
+import Moonlight.Planar.Types (unitElementDefaults, BuildResult(buildTriangulation), DelaunayTriangulation, Triangulation)
+import Support ( assertEqual, requireRight, integerPoint, rectangleComponent )
+import qualified Moonlight.Planar.Dcel as Dcel
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testFaceComponentsAndBoundaries
+    , testExactAuthoring
+    , testOrdinaryPublication
+    , testExactCellSetAdmission
+    , testGroupedPublicationFixtures
+    , testPinchPublicationRefusal
+    ]
+
+testExactAuthoring :: IO ()
+testExactAuthoring = do
+  firstComponent <- rectangleComponent 0 0 2 2
+  secondComponent <- rectangleComponent 4 0 5 1
+  region <- requireRight "disconnected exact region" (planarRegion [firstComponent, secondComponent])
+  layer <-
+    requireRight
+      "disconnected labelled layer"
+      (planarLayer "outside" (Map.singleton "land" region))
+  assertEqual
+    "disconnected exact components remain separate"
+    2
+    ( maybe
+        0
+        (length . planarRegionComponents)
+        (Map.lookup "land" (planarLayerRegions layer))
+    )
+  assertEqual
+    "interior exact point location"
+    RegionInterior
+    (regionPointLocation region (integerPoint 1 1))
+  assertEqual
+    "exterior exact point location"
+    RegionExterior
+    (regionPointLocation region (integerPoint 3 1))
+
+testOrdinaryPublication :: IO ()
+testOrdinaryPublication = do
+  built <-
+    requireRight
+      "ordinary square triangulation"
+      ( delaunay
+          unitElementDefaults
+          (V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
+      )
+  layer <-
+    requireRight
+      "ordinary square labelled publication"
+      (labelledPlanarLayer "outside" (buildTriangulation built) (const "inside"))
+  case
+    labelledPlanarLayerFromExactCoordinates
+      "outside"
+      (buildTriangulation built)
+      (\vertex -> Left (RegionCoordinateMissing vertex))
+      (const (Right "inside")) of
+    Left (RegionCoordinateMissing _) -> pure ()
+    other -> fail ("missing exact publication coordinate produced " <> show other)
+  assertEqual
+    "ordinary publication omits outside label"
+    ["inside"]
+    (Map.keys (planarLayerRegions layer))
+  let components =
+        maybe [] planarRegionComponents (Map.lookup "inside" (planarLayerRegions layer))
+  assertEqual "ordinary square component count" 1 (length components)
+  assertEqual
+    "ordinary square drops resident diagonal"
+    [4]
+    (map (length . exactLoopPoints . polygonOuterLoop) components)
+
+testExactCellSetAdmission :: IO ()
+testExactCellSetAdmission = do
+  built <-
+    requireRight
+      "cell-set triangle"
+      (delaunay unitElementDefaults (V.fromList [Point 0 0, Point 2 0, Point 0 2]))
+  let triangulation = buildTriangulation built
+  face <-
+    case innerFaces triangulation of
+      [singleFace] -> pure singleFace
+      faces -> fail ("cell-set triangle faces: " <> show faces)
+  closed <- requireRight "closed face cell set" (closeFaceCellSet triangulation [face])
+  assertEqual
+    "face cell set carries its complete downward closure"
+    (3, 3, 1)
+    ( exactCellSetVertexCount closed
+    , exactCellSetEdgeCount closed
+    , exactCellSetFaceCount closed
+    )
+  case exactCellSet triangulation [] [] [outerFace] of
+    Left CellOuterFaceSelected -> pure ()
+    _ -> fail "cell set admitted the unbounded outer face"
+  edge <-
+    case undirectedEdges triangulation of
+      firstEdge : _ -> pure firstEdge
+      [] -> fail "cell-set triangle has no edge"
+  case exactCellSet triangulation [] [edge] [] of
+    Left (CellEdgeBoundaryMissing failedEdge _) ->
+      assertEqual "edge closure witness" edge failedEdge
+    _ -> fail "cell set admitted an edge without its boundary vertices"
+
+testGroupedPublicationFixtures :: IO ()
+testGroupedPublicationFixtures = do
+  triangle <-
+    pointMeshOf
+      "published single triangle"
+      [Point 0 0, Point 2 0, Point 0 2]
+  triangleLayer <-
+    requireRight
+      "published single triangle layer"
+      (labelledPlanarLayer (0 :: Int) triangle (const 1))
+  assertComponentShape "published single triangle" 1 3 [] triangleLayer
+
+  concave <- regionMesh "published concave L" 2 2
+  let concaveLabel =
+        regionFaceSatisfies concave (\(Point x y) -> not (x > 1 && y > 1))
+  concaveLayer <-
+    requireRight
+      "published concave L layer"
+      (labelledPlanarLayer False concave concaveLabel)
+  assertComponentShape "published concave L" True 6 [] concaveLayer
+
+  annulus <- regionMesh "published annulus" 3 3
+  let annulusLabel =
+        regionFaceSatisfies annulus
+          (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))
+  annulusLayer <-
+    requireRight
+      "published annulus layer"
+      (labelledPlanarLayer False annulus annulusLabel)
+  assertComponentShape "published annulus" True 4 [4] annulusLayer
+
+  twoHoles <- regionMesh "published two holes" 5 3
+  let twoHoleLabel =
+        regionFaceSatisfies twoHoles $ \(Point x y) ->
+          let cell = (floor x :: Int, floor y :: Int)
+           in cell /= (1, 1) && cell /= (3, 1)
+  twoHoleLayer <-
+    requireRight
+      "published two-hole layer"
+      (labelledPlanarLayer False twoHoles twoHoleLabel)
+  assertComponentShape "published two holes" True 4 [4, 4] twoHoleLayer
+
+  disconnected <- regionMesh "published disconnected islands" 3 1
+  let islandLabel =
+        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)
+  disconnectedLayer <-
+    requireRight
+      "published disconnected layer"
+      (labelledPlanarLayer False disconnected islandLabel)
+  assertEqual
+    "published equal label keeps disconnected components"
+    2
+    (length (componentsFor True disconnectedLayer))
+
+  islandInHole <- regionMesh "published island in hole" 3 3
+  let islandInHoleLabel face =
+        if regionFaceSatisfies islandInHole
+             (\(Point x y) -> x > 1 && x < 2 && y > 1 && y < 2)
+             face
+          then (2 :: Int)
+          else 1
+  nestedLayer <-
+    requireRight
+      "published island-in-hole layer"
+      (labelledPlanarLayer 0 islandInHole islandInHoleLabel)
+  assertComponentShape "published shell around island" 1 4 [4] nestedLayer
+  assertComponentShape "published island inside hole" 2 4 [] nestedLayer
+
+testPinchPublicationRefusal :: IO ()
+testPinchPublicationRefusal = do
+  pinched <- regionMesh "published pinch" 3 3
+  let selected =
+        regionFaceSatisfies pinched $ \(Point x y) ->
+          let cell = (floor x :: Int, floor y :: Int)
+           in cell /= (0, 0) && cell /= (1, 1)
+  case labelledPlanarLayer False pinched selected of
+    Left (RegionBoundaryObstruction BoundaryPinch {}) -> pure ()
+    other -> fail ("pinched publication produced " <> show other)
+
+assertComponentShape
+  :: Ord label
+  => String
+  -> label
+  -> Int
+  -> [Int]
+  -> PlanarLayer label
+  -> IO ()
+assertComponentShape label regionLabel expectedOuterVertices expectedHoleVertices layer =
+  case componentsFor regionLabel layer of
+    [component] -> do
+      assertEqual
+        (label <> " outer vertices")
+        expectedOuterVertices
+        (length (exactLoopPoints (polygonOuterLoop component)))
+      assertEqual
+        (label <> " hole vertices")
+        expectedHoleVertices
+        (map (length . exactLoopPoints) (polygonHoleLoops component))
+    components ->
+      fail (label <> ": expected one component, got " <> show (length components))
+
+componentsFor :: Ord label => label -> PlanarLayer label -> [PolygonComponent]
+componentsFor label =
+  maybe [] planarRegionComponents . Map.lookup label . planarLayerRegions
+
+testFaceComponentsAndBoundaries :: IO ()
+testFaceComponentsAndBoundaries = do
+  emptyBuild <- requirePointBuild "empty region components" []
+  collinearBuild <-
+    requirePointBuild
+      "collinear region components"
+      [Point 0 0, Point 1 0, Point 2 0]
+  assertEqual
+    "empty mesh has no face components"
+    ([] :: [(Bool, FaceComponent)])
+    (faceComponents (buildTriangulation emptyBuild) (const True))
+  assertEqual
+    "collinear mesh has no face components"
+    ([] :: [(Bool, FaceComponent)])
+    (faceComponents (buildTriangulation collinearBuild) (const True))
+
+  triangle <-
+    pointMeshOf
+      "single triangle region"
+      [Point 0 0, Point 2 0, Point 0 2]
+  triangleBoundary <-
+    requireComponentBoundary "single triangle" True triangle (const True)
+  assertBoundaryShape "single triangle" triangle 3 [] triangleBoundary
+
+  square <-
+    pointMeshOf
+      "uniform square region"
+      [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
+  squareComponent <-
+    requireLabelledComponent
+      "uniform square component"
+      True
+      (faceComponents square (const True))
+  assertEqual
+    "component provenance mismatch is typed before DCEL lookup"
+    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))
+    (componentBoundary triangle squareComponent)
+  assertEqual
+    "component loop provenance mismatch is typed before DCEL lookup"
+    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))
+    (componentBoundaryLoops triangle squareComponent)
+  squareBoundary <- requireRight "uniform square boundary" (componentBoundary square squareComponent)
+  squareLoops <-
+    requireRight
+      "uniform square oriented boundary loops"
+      (componentBoundaryLoops square squareComponent)
+  repeatedSquareBoundary <-
+    requireRight "repeated uniform square boundary" (componentBoundary square squareComponent)
+  assertEqual "boundary extraction is deterministic" squareBoundary repeatedSquareBoundary
+  assertEqual
+    "ordinary oriented loops agree with strict boundary"
+    (regionBoundaryOuterLoop squareBoundary :| regionBoundaryHoleLoops squareBoundary)
+    squareLoops
+  assertEqual
+    "uniform square drops its Delaunay diagonal"
+    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
+    (loopPointSet square (regionBoundaryOuterLoop squareBoundary))
+  assertBoundaryShape "uniform square" square 4 [] squareBoundary
+
+  let splitComponents =
+        faceComponents square (regionFaceSatisfies square (\point -> pointX point < 1))
+  assertEqual "split square component count" 2 (length splitComponents)
+  traverse_
+    (\(_, component) -> do
+       boundary <- requireRight "split square boundary" (componentBoundary square component)
+       assertBoundaryShape "split square component" square 3 [] boundary)
+    splitComponents
+
+  collinearHull <-
+    pointMeshOf
+      "collinear hull simplification"
+      [ Point 0 0
+      , Point 1 0
+      , Point 2 0
+      , Point 2 2
+      , Point 0 2
+      , Point 1 1
+      ]
+  collinearBoundary <-
+    requireComponentBoundary "collinear hull" True collinearHull (const True)
+  assertEqual
+    "exact simplification drops a redundant hull site"
+    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
+    (loopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))
+  assertEqual
+    "resident boundary retains every hull edge endpoint"
+    (Set.fromList [Point 0 0, Point 1 0, Point 2 0, Point 2 2, Point 0 2])
+    (residentLoopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))
+  assertEqual
+    "resident boundary follows exact DCEL edges"
+    True
+    (residentBoundaryIsEdgeCycle collinearHull (regionBoundaryOuterLoop collinearBoundary))
+
+  lShape <- regionMesh "concave L region" 2 2
+  let inL = regionFaceSatisfies lShape (\(Point x y) -> not (x > 1 && y > 1))
+  lBoundary <- requireComponentBoundary "concave L" True lShape inL
+  assertEqual
+    "concave L boundary"
+    ( Set.fromList
+        [ Point 0 0
+        , Point 2 0
+        , Point 2 1
+        , Point 1 1
+        , Point 1 2
+        , Point 0 2
+        ]
+    )
+    (loopPointSet lShape (regionBoundaryOuterLoop lBoundary))
+  assertBoundaryShape "concave L" lShape 6 [] lBoundary
+
+  annulus <- regionMesh "annulus region" 3 3
+  let outsideCenter =
+        regionFaceSatisfies annulus (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))
+  annulusBoundary <-
+    requireComponentBoundary "annulus outer" True annulus outsideCenter
+  assertBoundaryShape "annulus" annulus 4 [4] annulusBoundary
+
+  twoHoles <- regionMesh "ordered hole regions" 5 3
+  let outsideTwoCells =
+        regionFaceSatisfies twoHoles $ \(Point x y) ->
+          let cell = (floor x :: Int, floor y :: Int)
+           in cell /= (1, 1) && cell /= (3, 1)
+  twoHoleBoundary <-
+    requireComponentBoundary "two-hole outer" True twoHoles outsideTwoCells
+  assertBoundaryShape "two-hole" twoHoles 4 [4, 4] twoHoleBoundary
+
+  disconnected <- regionMesh "disconnected equal labels" 3 1
+  let outsideMiddle =
+        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)
+      disconnectedComponents = faceComponents disconnected outsideMiddle
+      equalLabelComponents =
+        [component | (True, component) <- disconnectedComponents]
+  assertEqual "equal labels remain two disconnected components" 2 (length equalLabelComponents)
+  assertEqual
+    "face component order is deterministic"
+    disconnectedComponents
+    (faceComponents disconnected outsideMiddle)
+
+  pinched <- regionMesh "pinched region" 3 3
+  let pinchedSelection =
+        regionFaceSatisfies pinched $ \(Point x y) ->
+          let cell = (floor x :: Int, floor y :: Int)
+           in cell /= (0, 0) && cell /= (1, 1)
+  pinchedComponent <-
+    requireLabelledComponent
+      "pinched selected component"
+      True
+      (faceComponents pinched pinchedSelection)
+  pinchedLoops <-
+    requireRight
+      "pinched oriented boundary loops"
+      (componentBoundaryLoops pinched pinchedComponent)
+  let pinchedLoopList = toList pinchedLoops
+  assertEqual "pinched simple loop count" 2 (length pinchedLoopList)
+  assertEqual
+    "pinched simple loop vertex counts"
+    [4, 6]
+    (sort (fmap (length . boundaryLoopVertices) pinchedLoopList))
+  assertEqual
+    "pinched loop orientations"
+    [BoundaryCounterClockwise, BoundaryClockwise]
+    (sort (fmap boundaryLoopOrientation pinchedLoopList))
+  traverse_
+    (\loop -> do
+       let loopVertexIds = toList (boundaryLoopVertices loop)
+       assertEqual
+         "pinched loop has no repeated vertex"
+         (length loopVertexIds)
+         (Set.size (Set.fromList loopVertexIds))
+       assertLoopWinding
+         "pinched loop winding agrees with orientation"
+         (case boundaryLoopOrientation loop of
+            BoundaryCounterClockwise -> GT
+            BoundaryClockwise -> LT)
+         pinched
+         loop)
+    pinchedLoopList
+  case componentBoundary pinched pinchedComponent of
+    Left (BoundaryPinch vertex firstEdge secondEdge) -> do
+      assertEqual "pinch vertex" (Point 1 1) (vertexPoint pinched vertex)
+      when (firstEdge == secondEdge) $
+        fail ("pinch repeated one outgoing edge: " <> show firstEdge)
+    other -> fail ("pinched boundary produced " <> show other)
+
+regionMesh :: String -> Int -> Int -> IO (DelaunayTriangulation Point)
+regionMesh label widthInCells heightInCells =
+  pointMeshOf
+    label
+    [ Point (fromIntegral x) (fromIntegral y)
+    | y <- [0 .. heightInCells]
+    , x <- [0 .. widthInCells]
+    ]
+
+regionFaceCentroid
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> Maybe Point
+regionFaceCentroid triangulation face =
+  (\(first, second, third) ->
+     centroid
+       (vertexPoint triangulation first)
+       (vertexPoint triangulation second)
+       (vertexPoint triangulation third))
+    <$> Dcel.innerFaceVertices triangulation face
+
+regionFaceSatisfies
+  :: Triangulation mode vertex directed undirected face
+  -> (Point -> Bool)
+  -> FaceId
+  -> Bool
+regionFaceSatisfies triangulation predicate =
+  maybe False predicate . regionFaceCentroid triangulation
+
+loopPointSet
+  :: Triangulation mode vertex directed undirected face
+  -> BoundaryLoop
+  -> Set.Set Point
+loopPointSet triangulation =
+  Set.fromList
+    . fmap (vertexPoint triangulation)
+    . toList
+    . boundaryLoopVertices
+
+residentLoopPointSet
+  :: Triangulation mode vertex directed undirected face
+  -> BoundaryLoop
+  -> Set.Set Point
+residentLoopPointSet triangulation =
+  Set.fromList
+    . fmap (vertexPoint triangulation)
+    . toList
+    . boundaryLoopResidentVertices
+
+residentBoundaryIsEdgeCycle
+  :: Triangulation mode vertex directed undirected face
+  -> BoundaryLoop
+  -> Bool
+residentBoundaryIsEdgeCycle triangulation loop =
+  let first :| remaining = boundaryLoopResidentVertices loop
+      adjacentPairs = zip (first : remaining) (remaining <> [first])
+   in all
+        (\(fromVertex, toVertex) ->
+           any
+             ((== toVertex) . destination triangulation)
+             (vertexOutgoingEdges triangulation fromVertex))
+        adjacentPairs
diff --git a/test/native/Moonlight/Planar/ValidationSpec.hs b/test/native/Moonlight/Planar/ValidationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/native/Moonlight/Planar/ValidationSpec.hs
@@ -0,0 +1,192 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
+
+-- | Structural and Delaunay rejection against explicit mesh corruptions.
+module Moonlight.Planar.ValidationSpec
+  ( tests
+  ) where
+
+import Control.Monad ( unless, when )
+import Data.Word ( Word32 )
+import Moonlight.Planar.Dcel ( faceDirectedEdges, numDirectedEdges, numVertices )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( innerFaces )
+import Moonlight.Planar.Internal.HandleDefs ( DirectedEdgeId(DirectedEdgeId), FaceId(unFaceId) )
+import Moonlight.Planar.Internal.Paged ( Paged, fromVector, toVector )
+import Moonlight.Planar.Internal.Validation ( validateTopologyClosure )
+import Moonlight.Planar.Internal.Types (PlanarIncidenceError (..))
+import Moonlight.Planar.MeshFixtures ( requirePointBuild )
+import Moonlight.Planar.Point (Point(Point))
+import Moonlight.Planar.Types (ConstraintMode(Unconstrained), InvariantViolation(LocallyIllegalDelaunayEdge,
+  CoordinatePlaneLengthMismatch, InnerFaceNotCounterClockwise, IncidenceViolation), BuildResult(buildTriangulation), Triangulation)
+import Moonlight.Planar.Validation ( validateDelaunay, validateTopology )
+import Support ( assertEqual, assertValid )
+import qualified Data.IntSet as IntSet
+import Moonlight.Planar.Internal.Representation qualified as Internal
+import qualified Data.Vector.Unboxed as U
+
+
+tests :: IO ()
+tests =
+  sequence_
+    [ testValidationRejectsCorruptedMeshes
+    , testRejectsMismatchedTwinEndpoints
+    ]
+
+type NativeMesh = Triangulation 'Unconstrained (Point) () () ()
+
+-- Both face cycles, all representatives and Euler hold. Only the paired
+-- endpoints disagree: XOR involution alone is not the twin law.
+testRejectsMismatchedTwinEndpoints :: IO ()
+testRejectsMismatchedTwinEndpoints = do
+  built <- requirePointBuild "mismatched twin fixture" [Point 0 0, Point 1 0, Point 2 1]
+  let mesh :: NativeMesh
+      mesh = (buildTriangulation built)
+        { Internal.triHalfTopology = fromVector 0 (U.fromList
+            [0,2,4,1, 0,3,5,0, 1,4,0,1, 2,5,1,0, 2,0,2,1, 1,1,3,0])
+        , Internal.triVertexOut = fromVector 0 (U.fromList [0,2,4])
+        , Internal.triFaceEdge = fromVector 0 (U.fromList [1,0])
+        }
+  assertEqual "mismatched twin endpoints rejected"
+    [IncidenceViolation (IncidenceEdgeEndpointMismatch (DirectedEdgeId 0))]
+    (validateTopology mesh)
+  unless (any isEndpointMismatch (validateTopologyClosure (IntSet.singleton 1) IntSet.empty mesh)) $
+    fail "local closure missed the shared endpoint law"
+  let inconsistentFace :: NativeMesh
+      inconsistentFace = mesh
+        { Internal.triHalfTopology = fromVector 0 (U.fromList
+            [0,2,4,1, 1,5,3,0, 1,4,0,0, 2,1,5,0, 2,0,2,1, 0,3,1,0]) }
+  assertEqual "face continuity rejected despite triangular next cycles"
+    [IncidenceViolation (IncidenceEdgeLinkMismatch (DirectedEdgeId 0))]
+    (validateTopology inconsistentFace)
+ where
+  isEndpointMismatch (IncidenceViolation IncidenceEdgeEndpointMismatch {}) = True
+  isEndpointMismatch _ = False
+
+-- | A named corruption of a mesh that was valid one line earlier, paired with
+-- the violation it must provoke. Validation that has never been made to fail is
+-- evidence only that it ran.
+data Corruption = Corruption
+  { corruptionName :: String
+  , corruptMesh :: NativeMesh -> NativeMesh
+  , provokes :: InvariantViolation -> Bool
+  }
+
+-- Sheared so that no four sites are cocircular: on a square grid the diagonal
+-- of every cell is a free choice, and a fixture that admits two answers cannot
+-- witness a wrong one.
+corruptionFixture :: [Point]
+corruptionFixture =
+  [ Point (fromIntegral column + 0.25 * fromIntegral row) (1.3 * fromIntegral row)
+  | column <- [0 .. 3 :: Int]
+  , row <- [0 .. 3 :: Int]
+  ]
+
+rewritePlane :: (U.Unbox a, Num a) => (U.Vector a -> U.Vector a) -> Paged a -> Paged a
+rewritePlane edit = fromVector 0 . edit . toVector
+
+slot :: U.Unbox a => Int -> a -> U.Vector a -> U.Vector a
+slot at value = (U.// [(at, value)])
+
+-- Past the end of every plane in the fixture, and far from the sentinels the
+-- packed representation reserves for absence.
+beyond :: NativeMesh -> Word32
+beyond mesh = fromIntegral (numDirectedEdges mesh + numVertices mesh + 64)
+
+onTopology :: (NativeMesh -> U.Vector Word32 -> U.Vector Word32) -> NativeMesh -> NativeMesh
+onTopology edit mesh =
+  mesh {Internal.triHalfTopology = rewritePlane (edit mesh) (Internal.triHalfTopology mesh)}
+
+-- The half-edge plane has stride four — origin, next, previous, face — so the
+-- first four entries below are one surgery distinguished only by which field
+-- the out-of-range index lands on.
+structuralCorruptions :: [Corruption]
+structuralCorruptions =
+  [ Corruption
+      "edge origin names an absent vertex"
+      (onTopology (slot 0 . beyond))
+      (\case IncidenceViolation IncidenceVertexOriginInvalid {} -> True; _ -> False)
+  , Corruption
+      "edge next names an absent edge"
+      (onTopology (slot 1 . beyond))
+      (\case IncidenceViolation IncidenceEdgeLinksInvalid {} -> True; _ -> False)
+  , Corruption
+      "edge previous names an absent edge"
+      (onTopology (slot 2 . beyond))
+      (\case IncidenceViolation IncidenceEdgeLinksInvalid {} -> True; _ -> False)
+  , Corruption
+      "edge face names an absent face"
+      (onTopology (slot 3 . beyond))
+      (\case IncidenceViolation IncidenceEdgeFaceInvalid {} -> True; _ -> False)
+  , Corruption
+      "vertex outgoing names an absent edge"
+      ( \mesh ->
+          mesh
+            { Internal.triVertexOut =
+                rewritePlane (slot 0 (beyond mesh)) (Internal.triVertexOut mesh)
+            }
+      )
+      (\case IncidenceViolation IncidenceVertexRootInvalid {} -> True; _ -> False)
+  , Corruption
+      "a coordinate plane is one short"
+      (\mesh -> mesh {Internal.triPointX = rewritePlane U.init (Internal.triPointX mesh)})
+      (\case CoordinatePlaneLengthMismatch {} -> True; _ -> False)
+  , Corruption
+      "every inner face wound clockwise"
+      (\mesh -> mesh {Internal.triPointY = rewritePlane (U.map negate) (Internal.triPointY mesh)})
+      (\case InnerFaceNotCounterClockwise {} -> True; _ -> False)
+  ]
+
+-- Structural corruption is caught before geometry is read, so the empty-circle
+-- law needs a mesh that stays well formed and merely stops being Delaunay.
+delaunayCorruptions :: [Corruption]
+delaunayCorruptions =
+  [ Corruption
+      "one site dragged through its neighbours' circumcircles"
+      (\mesh -> mesh {Internal.triPointX = rewritePlane (slot 5 40) (Internal.triPointX mesh)})
+      (\case LocallyIllegalDelaunayEdge {} -> True; _ -> False)
+  ]
+
+-- A surgery that changed nothing would report the oracle as unarmed when in
+-- truth it was never asked anything, so the mutant must differ before its
+-- rejection means a thing.
+assertRejects
+  :: String -> (NativeMesh -> [InvariantViolation]) -> NativeMesh -> Corruption -> IO ()
+assertRejects oracle check pristine corruption = do
+  let mutant = corruptMesh corruption pristine
+      label = oracle <> " / " <> corruptionName corruption
+      reported = check mutant
+  when (mutant == pristine) $ fail (label <> ": the surgery changed nothing")
+  unless (any (provokes corruption) reported) $
+    fail (label <> ": admitted, reporting " <> show reported)
+
+testValidationRejectsCorruptedMeshes :: IO ()
+testValidationRejectsCorruptedMeshes = do
+  built <- requirePointBuild "corruption fixture" corruptionFixture
+  let pristine = buildTriangulation built
+  assertValid "corruption fixture" pristine
+  mapM_ (assertRejects "topology" validateTopology pristine) structuralCorruptions
+  mapM_ (assertRejects "delaunay" validateDelaunay pristine) delaunayCorruptions
+  case innerFaces pristine of
+    [] -> fail "local validation fixture has no inner face"
+    witnessFace : _ ->
+      case faceDirectedEdges pristine witnessFace of
+        [] -> fail "local validation fixture has no face edge"
+        DirectedEdgeId rawEdge : _ -> do
+          let admitted = IntSet.singleton (fromIntegral (unFaceId witnessFace))
+              corrupted
+                :: NativeMesh
+              corrupted =
+                pristine
+                  { Internal.triHalfTopology =
+                      rewritePlane
+                        (slot (4 * fromIntegral rawEdge + 1) rawEdge)
+                        (Internal.triHalfTopology pristine)
+                  }
+          assertEqual
+            "selected validation accepts valid closure"
+            []
+            (validateTopologyClosure admitted IntSet.empty pristine)
+          unless (not (null (validateTopology corrupted))) $
+            fail "global validation missed the locally corrupt face closure"
+          unless (not (null (validateTopologyClosure admitted IntSet.empty corrupted))) $
+            fail "selected validation missed a locally corrupt face closure"
diff --git a/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs b/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs
deleted file mode 100644
--- a/test/native/Moonlight/Triangulation/ExactEmbeddingSpec.hs
+++ /dev/null
@@ -1,659 +0,0 @@
-{-# LANGUAGE NumericUnderscores #-}
-
--- | Focused exact-geometry and local-embedding milestone acceptance.
-module Moonlight.Triangulation.ExactEmbeddingSpec (tests) where
-
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad (unless)
-import Data.Bits (xor)
-import Data.Foldable (traverse_)
-import Data.List (sort)
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Map.Strict as Map
-import Data.Word (Word64)
-import GHC.Stats (allocated_bytes, getRTSStats, getRTSStatsEnabled)
-import Moonlight.Triangulation.Exact
-  ( ExactGeometryError (..)
-  , ExactIntersectionError (..)
-  , ExactPoint
-  , exactLineIntersection
-  , exactPoint
-  , exactPointCoordinates
-  , exactPointFromPoint
-  , exactPointToEmbeddingCandidate
-  , exactSegment
-  , exactSegmentRelation
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( ExactArithmeticError (..)
-  , ExactRational
-  , exactDivide
-  , exactRational
-  , exactRationalDenominator
-  , exactRationalFromDouble
-  , exactRationalNumerator
-  , exactSignum
-  )
-import Moonlight.Triangulation.Embedding
-  ( DraftIncidence (..)
-  , DraftId (..)
-  , DraftNeighborhood (..)
-  , DraftSegmentId
-  , DraftVertexId
-  , EmbeddingObligation (..)
-  , ExactArrangementDraft (..)
-  , certificateIncidenceRelationPreservationCount
-  , certificateNeighborhoodRotationPreservationCount
-  , certificateResidual
-  , certificateRoundedVertexDistinctnessCount
-  , certificateRoundedVertices
-  , certificateSplitOrderPreservationCount
-  , OverlayEmbeddingObstruction (..)
-  , certifyLocalEmbedding
-  , residualUndischargedObligations
-  )
-import Moonlight.Triangulation.Internal.Predicates (segmentRelation)
-import Moonlight.Triangulation.Math
-  ( SegmentRelation (..)
-  , allSegmentRelations
-  )
-import Moonlight.Triangulation.Types
-  ( CoordinateError (..)
-  , Point (..)
-  , PointValidationError (..)
-  , queryPointValue
-  )
-import Support (assertEqual, integerPoint, requireRight)
-import System.Mem (performGC)
-
--- | Run the exact geometry, embedding, and frozen binary64 acceptance actions.
-tests :: IO ()
-tests =
-  sequence_
-    [ testExactNonDyadicCrossing
-    , testAllSegmentRelationsAgree
-    , testSubUlpExactVertexCluster
-    , testContactDuplicateAndOverlapAgreement
-    , testVerticalNestedOverlapRefusal
-    , testCoordinateRefusalsAndRoundtrip
-    , testExactArithmeticReceipt
-    , testEmbeddingAdmissionReceipt
-    , testOrdinaryDraftCertificate
-    , testCollinearNeighborhoodRotation
-    , testFrozenBinary64RelationOracle
-    ]
-
-testExactNonDyadicCrossing :: IO ()
-testExactNonDyadicCrossing = do
-  firstSegment <-
-    requireRight
-      "first non-dyadic crossing segment"
-      (exactSegment (integerPoint 0 0) (integerPoint 1 1))
-  secondSegment <-
-    requireRight
-      "second non-dyadic crossing segment"
-      (exactSegment (integerPoint 0 1) (integerPoint 2 0))
-  crossing <-
-    requireRight
-      "non-dyadic exact line intersection"
-      (exactLineIntersection firstSegment secondSegment)
-  let (crossingX, crossingY) = exactPointCoordinates crossing
-  assertRatio "non-dyadic crossing x" 2 3 crossingX
-  assertRatio "non-dyadic crossing y" 2 3 crossingY
-  putStrLn
-    ( "exact non-dyadic crossing coordinates: "
-        <> show (exactRationalNumerator crossingX, exactRationalDenominator crossingX)
-        <> ", "
-        <> show (exactRationalNumerator crossingY, exactRationalDenominator crossingY)
-    )
-
-testAllSegmentRelationsAgree :: IO ()
-testAllSegmentRelationsAgree = do
-  observed <- traverse observeExactAndRounded handFixtures
-  traverse_ assertExpectedAndAgreement observed
-  assertEqual
-    "all exact segment relations covered"
-    allSegmentRelations
-    (sort (map observedExactRelation observed))
-
-testSubUlpExactVertexCluster :: IO ()
-testSubUlpExactVertexCluster = do
-  let base = 2 ^ (52 :: Int)
-  firstCoordinate <- requireRight "first sub-ulp coordinate" (exactRational (base * 4 + 1) 4)
-  secondCoordinate <- requireRight "second sub-ulp coordinate" (exactRational (base * 4 + 2) 4)
-  thirdCoordinate <- requireRight "third sub-ulp coordinate" (exactRational (base * 4 + 3) 4)
-  firstCrossing <- exactCrossingAt firstCoordinate
-  secondCrossing <- exactCrossingAt secondCoordinate
-  thirdCrossing <- exactCrossingAt thirdCoordinate
-  let draft =
-        emptyDraft
-          { draftVertices =
-              Map.fromList
-                [ (DraftId 0, firstCrossing)
-                , (DraftId 1, secondCrossing)
-                , (DraftId 2, thirdCrossing)
-                ]
-          }
-  assertLeftContains
-    "sub-ulp exact vertex cluster"
-    isRoundedCollision
-    (certifyLocalEmbedding draft)
-
-testContactDuplicateAndOverlapAgreement :: IO ()
-testContactDuplicateAndOverlapAgreement =
-  traverse_ assertContactAgreement contactFixtures
-
-testVerticalNestedOverlapRefusal :: IO ()
-testVerticalNestedOverlapRefusal = do
-  let base = 2 ^ (52 :: Int)
-      zero = 0
-      lower = fromInteger base
-      upper = fromInteger (base + 1)
-  innerLower <- requireRight "vertical inner lower" (exactRational (base * 3 + 1) 3)
-  innerUpper <- requireRight "vertical inner upper" (exactRational (base * 3 + 2) 3)
-  let outerFrom = exactPoint zero lower
-      outerTo = exactPoint zero upper
-      innerFrom = exactPoint zero innerLower
-      innerTo = exactPoint zero innerUpper
-      outerSegmentId, innerSegmentId :: DraftSegmentId
-      outerSegmentId = DraftId 0
-      innerSegmentId = DraftId 1
-      draft =
-        emptyDraft
-          { draftVertices =
-              Map.fromList
-                [ (DraftId 0, outerFrom)
-                , (DraftId 1, outerTo)
-                , (DraftId 2, innerFrom)
-                , (DraftId 3, innerTo)
-                ]
-          , draftSegments =
-              Map.fromList
-                [ (outerSegmentId, (DraftId 0, DraftId 1))
-                , (innerSegmentId, (DraftId 2, DraftId 3))
-                ]
-          , draftIncidences =
-              [ DraftIncidence
-                  outerSegmentId
-                  innerSegmentId
-                  SegmentsCollinearlyOverlap
-              ]
-          }
-  assertEqual
-    "vertical nested overlap remains exact"
-    SegmentsCollinearlyOverlap
-    (exactSegmentRelation outerFrom outerTo innerFrom innerTo)
-  assertLeftContains
-    "vertical nested overlap rounded incidence"
-    isIncidenceChange
-    (certifyLocalEmbedding draft)
-
-testCoordinateRefusalsAndRoundtrip :: IO ()
-testCoordinateRefusalsAndRoundtrip = do
-  assertEqual
-    "zero exact denominator refusal"
-    (Left ExactZeroDenominator)
-    (exactRational 1 0)
-  assertEqual
-    "zero exact divisor refusal"
-    (Left ExactZeroDivisor)
-    (exactDivide 1 0)
-  normalized <- requireRight "normalized exact rational" (exactRational (-2) (-4))
-  assertRatio "normalized exact rational" 1 2 normalized
-  canonicalZero <- requireRight "canonical exact zero" (exactRational 0 (-7))
-  assertRatio "canonical exact zero" 0 1 canonicalZero
-  assertEqual
-    "NaN exact rational refusal"
-    (Left ExactNaNInput)
-    (exactRationalFromDouble (0 / 0))
-  assertEqual
-    "infinite exact rational refusal"
-    (Left ExactInfiniteInput)
-    (exactRationalFromDouble (1 / 0))
-  assertEqual
-    "NaN exact point refusal"
-    (Left (InvalidPointX CoordinateNaN))
-    (exactPointFromPoint (Point (0 / 0) 0))
-  assertEqual
-    "infinite exact point refusal"
-    (Left (InvalidPointX CoordinateInfinite))
-    (exactPointFromPoint (Point (1 / 0) 0))
-  let unprojectablePoint =
-        exactPoint
-          (fromInteger (10 ^ (400 :: Int)))
-          0
-      unprojectableDraft =
-        emptyDraft
-          { draftVertices =
-              Map.singleton (DraftId 0) unprojectablePoint
-          }
-  assertLeftContains
-    "unprojectable exact draft vertex"
-    isProjectionRefusal
-    (certifyLocalEmbedding unprojectableDraft)
-  let finitePoint = Point 1.25 (-2.5)
-  exactFinite <- requireRight "finite point exact conversion" (exactPointFromPoint finitePoint)
-  projectedFinite <-
-    requireRight
-      "finite point candidate projection"
-      (exactPointToEmbeddingCandidate exactFinite)
-  assertEqual "finite exact point roundtrip" finitePoint (queryPointValue projectedFinite)
-  assertEqual
-    "coincident exact segment endpoints"
-    (Left (ExactSegmentEndpointsCoincide exactFinite))
-    (exactSegment exactFinite exactFinite)
-  duplicateSegment <-
-    requireRight
-      "duplicate line intersection segment"
-      (exactSegment (integerPoint 0 0) (integerPoint 2 0))
-  assertEqual
-    "duplicate line intersection refusal"
-    (Left (ExactIntersectionNonUnique SegmentsDuplicate))
-    (exactLineIntersection duplicateSegment duplicateSegment)
-
-testOrdinaryDraftCertificate :: IO ()
-testOrdinaryDraftCertificate = do
-  half <- requireRight "ordinary draft half parameter" (exactRational 1 2)
-  let west, center, east, south, north :: DraftVertexId
-      west = DraftId 0
-      center = DraftId 1
-      east = DraftId 2
-      south = DraftId 3
-      north = DraftId 4
-      westCenter, centerEast, southCenter, centerNorth :: DraftSegmentId
-      westCenter = DraftId 0
-      centerEast = DraftId 1
-      southCenter = DraftId 2
-      centerNorth = DraftId 3
-      draft =
-        ExactArrangementDraft
-          { draftVertices =
-              Map.fromList
-                [ (west, integerPoint (-1) 0)
-                , (center, integerPoint 0 0)
-                , (east, integerPoint 1 0)
-                , (south, integerPoint 0 (-1))
-                , (north, integerPoint 0 1)
-                ]
-          , draftSegments =
-              Map.fromList
-                [ (westCenter, (west, center))
-                , (centerEast, (center, east))
-                , (southCenter, (south, center))
-                , (centerNorth, (center, north))
-                ]
-          , draftSourceMemberships =
-              Map.fromList
-                [ ( DraftId 0
-                  , [ (0, west)
-                    , (half, center)
-                    , (1, east)
-                    ]
-                  )
-                , ( DraftId 1
-                  , [ (0, south)
-                    , (half, center)
-                    , (1, north)
-                    ]
-                  )
-                ]
-          , draftIncidences =
-              [ DraftIncidence westCenter southCenter SegmentsShareEndpoint
-              ]
-          , draftNeighborhoods =
-              [ DraftNeighborhood center (east :| [north, west, south])
-              ]
-          }
-  certificate <- requireRight "ordinary local embedding" (certifyLocalEmbedding draft)
-  westEmbedding <-
-    maybe
-      (fail "ordinary local embedding omitted its admitted west candidate")
-      pure
-      (Map.lookup west (certificateRoundedVertices certificate))
-  assertEqual
-    "ordinary certificate retains admitted projection"
-    (Point (-1) 0)
-    (queryPointValue westEmbedding)
-  assertEqual
-    "ordinary distinctness obligations"
-    10
-    (certificateRoundedVertexDistinctnessCount certificate)
-  assertEqual
-    "ordinary split-order obligations"
-    4
-    (certificateSplitOrderPreservationCount certificate)
-  assertEqual
-    "ordinary incidence obligations"
-    1
-    (certificateIncidenceRelationPreservationCount certificate)
-  assertEqual
-    "ordinary neighborhood obligations"
-    4
-    (certificateNeighborhoodRotationPreservationCount certificate)
-  assertEqual
-    "ordinary residual"
-    (GlobalNoNewCrossing :| [])
-    (residualUndischargedObligations (certificateResidual certificate))
-  putStrLn
-    ( "local embedding certificate counts: "
-        <> show
-          ( certificateRoundedVertexDistinctnessCount certificate
-          , certificateSplitOrderPreservationCount certificate
-          , certificateIncidenceRelationPreservationCount certificate
-          , certificateNeighborhoodRotationPreservationCount certificate
-          )
-    )
-  putStrLn
-    ( "local embedding declared residual: "
-        <> show (residualUndischargedObligations (certificateResidual certificate))
-    )
-
--- Independent binary64 projection bends the exact straight-through pair at
--- the center, but the cyclic neighbor order is unchanged. The local topology
--- obligation is rotation preservation, not literal preservation of a zero
--- determinant.
-testCollinearNeighborhoodRotation :: IO ()
-testCollinearNeighborhoodRotation = do
-  oneThird <- requireRight "collinear rotation coordinate" (exactRational 1 3)
-  let center, right, branch, left :: DraftVertexId
-      center = DraftId 0
-      right = DraftId 1
-      branch = DraftId 2
-      left = DraftId 3
-      centerRight, centerBranch, centerLeft :: DraftSegmentId
-      centerRight = DraftId 0
-      centerBranch = DraftId 1
-      centerLeft = DraftId 2
-      draft =
-        emptyDraft
-          { draftVertices =
-              Map.fromList
-                [ (center, exactPoint 1 oneThird)
-                , (right, integerPoint 3 1)
-                , (branch, integerPoint 1 2)
-                , (left, integerPoint 0 0)
-                ]
-          , draftSegments =
-              Map.fromList
-                [ (centerRight, (center, right))
-                , (centerBranch, (center, branch))
-                , (centerLeft, (center, left))
-                ]
-          , draftIncidences =
-              [ DraftIncidence centerRight centerBranch SegmentsShareEndpoint
-              , DraftIncidence centerBranch centerLeft SegmentsShareEndpoint
-              , DraftIncidence centerLeft centerRight SegmentsShareEndpoint
-              ]
-          , draftNeighborhoods =
-              [DraftNeighborhood center (right :| [branch, left])]
-          }
-  certificate <-
-    requireRight
-      "collinear neighborhood retains its cyclic rotation"
-      (certifyLocalEmbedding draft)
-  assertEqual
-    "collinear neighborhood rotation obligations"
-    3
-    (certificateNeighborhoodRotationPreservationCount certificate)
-
-testExactArithmeticReceipt :: IO ()
-testExactArithmeticReceipt = do
-  enabled <- getRTSStatsEnabled
-  unless enabled $
-    fail "exact arithmetic allocation receipt requires +RTS -T"
-  operands <-
-    traverse
-      ( \index ->
-          requireRight
-            "exact arithmetic receipt operand"
-            (exactRational (toInteger (index `mod` 89 + 1)) 97)
-      )
-      [0 .. 9_999 :: Int]
-  _ <- evaluate (force operands)
-  performGC
-  before <- allocated_bytes <$> getRTSStats
-  checksum <-
-    evaluate
-      ( force
-          ( List.foldl'
-              ( \accumulated value ->
-                  accumulated
-                    + fromEnum
-                      ( exactSignum
-                          ((value + 1) * value)
-                      )
-              )
-              0
-              operands
-          )
-      )
-  performGC
-  after <- allocated_bytes <$> getRTSStats
-  let operationCount = 3 * length operands
-      allocated = after - before
-  unless (checksum > 0) $
-    fail "exact arithmetic receipt did not force its operation chain"
-  putStrLn
-    ( "exact arithmetic receipt: operations="
-        <> show operationCount
-        <> " allocated-bytes="
-        <> show allocated
-        <> " checksum="
-        <> show checksum
-    )
-
-testEmbeddingAdmissionReceipt :: IO ()
-testEmbeddingAdmissionReceipt = do
-  let base = 2 ^ (52 :: Int)
-  firstCollision <- requireRight "admission receipt collision a" (exactRational (base * 4 + 1) 4)
-  secondCollision <- requireRight "admission receipt collision b" (exactRational (base * 4 + 2) 4)
-  let admittedDraft =
-        emptyDraft
-          { draftVertices =
-              Map.fromList
-                [ (DraftId 0, integerPoint 0 0)
-                , (DraftId 1, integerPoint 1 0)
-                ]
-          }
-      collisionDraft =
-        emptyDraft
-          { draftVertices =
-              Map.fromList
-                [ (DraftId 0, exactPoint firstCollision 0)
-                , (DraftId 1, exactPoint secondCollision 0)
-                ]
-          }
-      projectionDraft =
-        emptyDraft
-          { draftVertices =
-              Map.singleton
-                (DraftId 0)
-                (exactPoint (fromInteger (10 ^ (400 :: Int))) 0)
-          }
-      outcomes =
-        map certifyLocalEmbedding [admittedDraft, collisionDraft, projectionDraft]
-      admitted = length [() | Right _ <- outcomes]
-      refused = length [() | Left _ <- outcomes]
-  assertEqual "embedding admission receipt admissions" 1 admitted
-  assertEqual "embedding admission receipt refusals" 2 refused
-  putStrLn
-    ( "embedding admission receipt: admitted="
-        <> show admitted
-        <> " refused="
-        <> show refused
-    )
-
-testFrozenBinary64RelationOracle :: IO ()
-testFrozenBinary64RelationOracle = do
-  let observed =
-        map
-          ( \(name, expected, (a, b, c, d)) ->
-              (name, expected, segmentRelation a b c d)
-          )
-          handFixtures
-      relations =
-        map (\(_, _, relation) -> relation) observed <> corpusRelations
-  traverse_
-    (\(name, expected, actual) -> assertEqual name expected actual)
-    observed
-  assertEqual "frozen relation count" 16_390 (length relations)
-  assertEqual
-    "frozen relation digest"
-    1_170_735_657_727_369_596
-    (digest relations)
-
-data ObservedRelation = ObservedRelation
-  { observedName :: !String
-  , observedExpectedRelation :: !SegmentRelation
-  , observedRoundedRelation :: !SegmentRelation
-  , observedExactRelation :: !SegmentRelation
-  }
-
-observeExactAndRounded
-  :: (String, SegmentRelation, (Point, Point, Point, Point))
-  -> IO ObservedRelation
-observeExactAndRounded (name, expected, points@(a, b, c, d)) = do
-  (exactA, exactB, exactC, exactD) <- exactPointTuple points
-  pure
-    ObservedRelation
-      { observedName = name
-      , observedExpectedRelation = expected
-      , observedRoundedRelation = segmentRelation a b c d
-      , observedExactRelation = exactSegmentRelation exactA exactB exactC exactD
-      }
-
-assertExpectedAndAgreement :: ObservedRelation -> IO ()
-assertExpectedAndAgreement observed = do
-  assertEqual
-    (observedName observed <> " binary64 expectation")
-    (observedExpectedRelation observed)
-    (observedRoundedRelation observed)
-  assertEqual
-    (observedName observed <> " exact agreement")
-    (observedRoundedRelation observed)
-    (observedExactRelation observed)
-
-contactFixtures
-  :: [(String, SegmentRelation, (Point, Point, Point, Point))]
-contactFixtures =
-  [ ( "endpoint-on-edge"
-    , SegmentEndpointTouchesInterior
-    , (Point 0 0, Point 2 0, Point 1 0, Point 1 1)
-    )
-  , ( "duplicate"
-    , SegmentsDuplicate
-    , (Point 0 0, Point 2 0, Point 2 0, Point 0 0)
-    )
-  , ( "partial-collinear-overlap"
-    , SegmentsCollinearlyOverlap
-    , (Point 0 0, Point 3 0, Point 1 0, Point 2 0)
-    )
-  ]
-
-assertContactAgreement
-  :: (String, SegmentRelation, (Point, Point, Point, Point))
-  -> IO ()
-assertContactAgreement fixture =
-  observeExactAndRounded fixture >>= assertExpectedAndAgreement
-
-exactPointTuple
-  :: (Point, Point, Point, Point)
-  -> IO (ExactPoint, ExactPoint, ExactPoint, ExactPoint)
-exactPointTuple (a, b, c, d) =
-  (,,,)
-    <$> requireRight "exact fixture point a" (exactPointFromPoint a)
-    <*> requireRight "exact fixture point b" (exactPointFromPoint b)
-    <*> requireRight "exact fixture point c" (exactPointFromPoint c)
-    <*> requireRight "exact fixture point d" (exactPointFromPoint d)
-
-exactCrossingAt :: ExactRational -> IO ExactPoint
-exactCrossingAt coordinate = do
-  horizontal <-
-    requireRight
-      "sub-ulp horizontal crossing segment"
-      ( exactSegment
-          (exactPoint (coordinate - 1) coordinate)
-          (exactPoint (coordinate + 1) coordinate)
-      )
-  vertical <-
-    requireRight
-      "sub-ulp vertical crossing segment"
-      ( exactSegment
-          (exactPoint coordinate (coordinate - 1))
-          (exactPoint coordinate (coordinate + 1))
-      )
-  requireRight "sub-ulp exact crossing" (exactLineIntersection horizontal vertical)
-
-assertRatio :: String -> Integer -> Integer -> ExactRational -> IO ()
-assertRatio label numerator denominator value = do
-  assertEqual (label <> " numerator") numerator (exactRationalNumerator value)
-  assertEqual (label <> " denominator") denominator (exactRationalDenominator value)
-
-emptyDraft :: ExactArrangementDraft
-emptyDraft =
-  ExactArrangementDraft
-    { draftVertices = Map.empty
-    , draftSegments = Map.empty
-    , draftSourceMemberships = Map.empty
-    , draftIncidences = []
-    , draftNeighborhoods = []
-    }
-
-assertLeftContains
-  :: String
-  -> (OverlayEmbeddingObstruction -> Bool)
-  -> Either (NonEmpty OverlayEmbeddingObstruction) value
-  -> IO ()
-assertLeftContains label predicate result =
-  case result of
-    Left obstructions ->
-      unless (any predicate obstructions) $
-        fail (label <> ": missing witness in " <> show obstructions)
-    Right _ -> fail (label <> ": unexpectedly certified")
-
-isRoundedCollision :: OverlayEmbeddingObstruction -> Bool
-isRoundedCollision RoundedVerticesCollide {} = True
-isRoundedCollision _ = False
-
-isProjectionRefusal :: OverlayEmbeddingObstruction -> Bool
-isProjectionRefusal VertexProjectionRefused {} = True
-isProjectionRefusal _ = False
-
-isIncidenceChange :: OverlayEmbeddingObstruction -> Bool
-isIncidenceChange IncidenceRelationChanged {} = True
-isIncidenceChange _ = False
-
-handFixtures :: [(String, SegmentRelation, (Point, Point, Point, Point))]
-handFixtures =
-  [ ("disjoint", SegmentsDisjoint, (Point 0 0, Point 1 0, Point 0 2, Point 1 2))
-  , ("duplicate", SegmentsDuplicate, (Point 0 0, Point 2 0, Point 2 0, Point 0 0))
-  , ("shared-endpoint", SegmentsShareEndpoint, (Point 0 0, Point 2 0, Point 2 0, Point 3 1))
-  , ("proper-crossing", SegmentsProperlyCross, (Point 0 0, Point 2 2, Point 0 2, Point 2 0))
-  , ("endpoint-interior", SegmentEndpointTouchesInterior, (Point 0 0, Point 2 0, Point 1 0, Point 1 1))
-  , ("collinear-overlap", SegmentsCollinearlyOverlap, (Point 0 0, Point 3 0, Point 1 0, Point 2 0))
-  ]
-
-corpusPoint :: Int -> Int -> Point
-corpusPoint index salt =
-  Point
-    (fromIntegral (((index * 17 + salt * 11) `mod` 47) - 23))
-    (fromIntegral (((index * 29 + salt * 7) `mod` 43) - 21))
-
-corpusRelations :: [SegmentRelation]
-corpusRelations =
-  [ segmentRelation
-      (corpusPoint index 1)
-      (corpusPoint index 2)
-      (corpusPoint index 3)
-      (corpusPoint index 4)
-  | index <- [0 .. 16_383]
-  ]
-
-digest :: [SegmentRelation] -> Word64
-digest = List.foldl' step 14_695_981_039_346_656_037
- where
-  step :: Word64 -> SegmentRelation -> Word64
-  step hash relation =
-    (hash `xor` fromIntegral (fromEnum relation + 1)) * 1_099_511_628_211
diff --git a/test/native/Moonlight/Triangulation/FilteredPredicateOptimizationSpec.hs b/test/native/Moonlight/Triangulation/FilteredPredicateOptimizationSpec.hs
deleted file mode 100644
--- a/test/native/Moonlight/Triangulation/FilteredPredicateOptimizationSpec.hs
+++ /dev/null
@@ -1,159 +0,0 @@
-{-# LANGUAGE NumericUnderscores #-}
-{-# OPTIONS_GHC -O1 #-}
-
--- | The allocation law for the optimized filtered-predicate artifact. This
--- module remains at O1 when the surrounding behavioral body is compiled at O0;
--- without the simplifier, the loop does not unbox and the witness is vacuous.
-module Moonlight.Triangulation.FilteredPredicateOptimizationSpec
-  ( assertFilteredPredicatesSkipExactOracle
-  ) where
-
-import Control.DeepSeq (force)
-import Control.Exception (evaluate)
-import Control.Monad (unless)
-import GHC.Stats (allocated_bytes, getRTSStats, getRTSStatsEnabled)
-import Moonlight.Triangulation.Math
-  ( inCircle
-  , inCircleDetApprox
-  , orient2d
-  , orientDetApprox
-  )
-import Moonlight.Triangulation.Internal.Types (QueryPoint (..))
-import Moonlight.Triangulation.Types (Point (..))
-import System.Mem (performGC)
-import qualified Data.Vector as V
-import qualified Data.Vector.Unboxed as U
-import Data.Word (Word64)
-
--- | A filtered predicate must not evaluate its exact oracle when the floating
--- approximation already certifies the sign. The exact path allocates
--- 'Integer's, so equal-shaped certified and degenerate folds expose whether
--- that fallback remains genuinely conditional.
-assertFilteredPredicatesSkipExactOracle :: IO ()
-assertFilteredPredicatesSkipExactOracle = do
-  enabled <- getRTSStatsEnabled
-  unless enabled $ fail "allocation counters unavailable: the suite must run with -T"
-  let count = 20_000
-      bigScale = 2 ^^ (200 :: Int) :: Double
-      smallScale = 2 ^^ (-200 :: Int) :: Double
-      orientCertified =
-        V.generate
-          count
-          (\index -> QueryPoint (Point (fromIntegral index * bigScale) (if even index then 0 else smallScale)))
-      orientDegenerate =
-        V.generate
-          count
-          (\index -> QueryPoint (Point (fromIntegral index * bigScale) (fromIntegral index * smallScale)))
-      zigzag = V.generate count (\index -> QueryPoint (Point (fromIntegral index) (if even index then 0 else 1)))
-      collinear = V.generate count (\index -> QueryPoint (Point (fromIntegral index) (fromIntegral index)))
-      orientationIndices = U.enumFromN 0 (max 0 (count - 2))
-      circleIndices = U.enumFromN 0 (max 0 (count - 3))
-  _ <- evaluate (force orientCertified)
-  _ <- evaluate (force orientDegenerate)
-  _ <- evaluate (force zigzag)
-  _ <- evaluate (force collinear)
-  _ <- evaluate (force orientationIndices)
-  _ <- evaluate (force circleIndices)
-  certifiedSupport <- allocationOf (sumTripleRelations approximateOrientation orientationIndices orientCertified)
-  fallbackSupport <- allocationOf (sumTripleRelations approximateOrientation orientationIndices orientDegenerate)
-  certified <- allocationOf (sumTripleRelations orient2d orientationIndices orientCertified)
-  fallback <- allocationOf (sumTripleRelations orient2d orientationIndices orientDegenerate)
-  let certifiedOracle = allocationBeyondApproximation certified certifiedSupport
-      fallbackOracle = allocationBeyondApproximation fallback fallbackSupport
-  unless (fallbackOracle > 0) $
-    fail "degenerate orientations allocated nothing: the measurement is not observing the oracle"
-  unless (certifiedOracle * 8 < fallbackOracle) $
-    fail
-      ( "orient2d evaluates its exact oracle on certified input: "
-          <> show certifiedOracle
-          <> " excess bytes certified versus "
-          <> show fallbackOracle
-          <> " excess bytes degenerate (raw "
-          <> show certified
-          <> " versus "
-          <> show fallback
-          <> ")"
-      )
-  certifiedCircleSupport <- allocationOf (sumQuadRelations approximateInCircle circleIndices zigzag)
-  fallbackCircleSupport <- allocationOf (sumQuadRelations approximateInCircle circleIndices collinear)
-  certifiedCircle <- allocationOf (sumQuadRelations inCircle circleIndices zigzag)
-  fallbackCircle <- allocationOf (sumQuadRelations inCircle circleIndices collinear)
-  let certifiedCircleOracle = allocationBeyondApproximation certifiedCircle certifiedCircleSupport
-      fallbackCircleOracle = allocationBeyondApproximation fallbackCircle fallbackCircleSupport
-  unless (fallbackCircleOracle > 0) $
-    fail "degenerate incircles allocated nothing: the measurement is not observing the oracle"
-  unless (certifiedCircleOracle * 8 < fallbackCircleOracle) $
-    fail
-      ( "inCircle evaluates its exact oracle on certified input: "
-          <> show certifiedCircleOracle
-          <> " excess bytes certified versus "
-          <> show fallbackCircleOracle
-          <> " excess bytes degenerate (raw "
-          <> show certifiedCircle
-          <> " versus "
-          <> show fallbackCircle
-          <> ")"
-      )
-
-allocationOf :: Int -> IO Word64
-allocationOf work = do
-  performGC
-  before <- allocated_bytes <$> getRTSStats
-  _ <- evaluate work
-  performGC
-  after <- allocated_bytes <$> getRTSStats
-  pure (after - before)
-
-allocationBeyondApproximation :: Word64 -> Word64 -> Word64
-allocationBeyondApproximation measured support = measured - min measured support
-
-sumTripleRelations
-  :: (QueryPoint -> QueryPoint -> QueryPoint -> Ordering)
-  -> U.Vector Int
-  -> V.Vector QueryPoint
-  -> Int
-sumTripleRelations relation indices points =
-  U.foldl'
-    (\accumulated index ->
-       accumulated
-         + fromEnum
-           ( relation
-               (V.unsafeIndex points index)
-               (V.unsafeIndex points (index + 1))
-               (V.unsafeIndex points (index + 2))
-           )
-    )
-    0
-    indices
-{-# INLINE sumTripleRelations #-}
-
-sumQuadRelations
-  :: (QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering)
-  -> U.Vector Int
-  -> V.Vector QueryPoint
-  -> Int
-sumQuadRelations relation indices points =
-  U.foldl'
-    (\accumulated index ->
-       accumulated
-         + fromEnum
-           ( relation
-               (V.unsafeIndex points index)
-               (V.unsafeIndex points (index + 1))
-               (V.unsafeIndex points (index + 2))
-               (V.unsafeIndex points (index + 3))
-           )
-    )
-    0
-    indices
-{-# INLINE sumQuadRelations #-}
-
-approximateOrientation :: QueryPoint -> QueryPoint -> QueryPoint -> Ordering
-approximateOrientation (QueryPoint a) (QueryPoint b) (QueryPoint c) =
-  compare (orientDetApprox a b c) 0
-{-# INLINE approximateOrientation #-}
-
-approximateInCircle :: QueryPoint -> QueryPoint -> QueryPoint -> QueryPoint -> Ordering
-approximateInCircle (QueryPoint a) (QueryPoint b) (QueryPoint c) (QueryPoint d) =
-  compare (inCircleDetApprox a b c d) 0
-{-# INLINE approximateInCircle #-}
diff --git a/test/native/Moonlight/Triangulation/NativeSpec.hs b/test/native/Moonlight/Triangulation/NativeSpec.hs
deleted file mode 100644
--- a/test/native/Moonlight/Triangulation/NativeSpec.hs
+++ /dev/null
@@ -1,4426 +0,0 @@
-{-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE NumericUnderscores #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
--- | The native core slice: everything that needs no serialization surface.
-module Moonlight.Triangulation.NativeSpec
-  ( tests
-  , regionMesh
-  , regionMeshFromPoints
-  , regionFaceSatisfies
-  ) where
-
-import Control.DeepSeq (NFData, force)
-import Control.Exception (evaluate)
-import Control.Monad (forM_, unless, void, when)
-import Control.Monad.ST (runST, stToIO)
-import GHC.Float (castDoubleToWord64)
-import Data.Foldable (toList, traverse_)
-import qualified Data.IntSet as IntSet
-import qualified Data.List as List
-import Data.List.NonEmpty (NonEmpty (..))
-import Data.Maybe (isJust)
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import Data.List (sort)
-import qualified Data.Vector as V
-import Data.Primitive.PrimArray (indexPrimArray, primArrayFromList, sizeofPrimArray)
-import qualified Data.Vector.Unboxed as U
-import qualified Data.Vector.Unboxed.Mutable as MUV
-import Data.Word (Word32, Word64)
-import Moonlight.Triangulation.Alpha
-import Moonlight.Triangulation.BulkLoad
-import Moonlight.Triangulation.Cdt
-import Moonlight.Triangulation.Dcel
-import qualified Moonlight.Triangulation.Dcel as Dcel
-import Moonlight.Triangulation.FloodFillIterator
-import Moonlight.Triangulation.Internal.HandleDefs
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-import Moonlight.Triangulation.HintGenerator
-import Moonlight.Triangulation.Interpolation
-import Moonlight.Triangulation.IntersectionIterator
-import Moonlight.Triangulation.Internal.Session (excise)
-import Moonlight.Triangulation.Internal.Validation (validateTopologyClosure)
-import Moonlight.Triangulation.Math
-import Moonlight.Triangulation.Payload
-import Moonlight.Triangulation.FilteredPredicateOptimizationSpec
-  ( assertFilteredPredicatesSkipExactOracle
-  )
-import Moonlight.Triangulation.PointLocation
-import Moonlight.Triangulation.Refinement
-import Moonlight.Triangulation.Removal
-import Moonlight.Triangulation.Scalar
-import Moonlight.Triangulation.Session
-import Moonlight.Triangulation.Types
-import Moonlight.Triangulation.Validation
-import Moonlight.Triangulation.Voronoi
-import Moonlight.Triangulation.Handles.Dynamic qualified as Dynamic
-import Moonlight.Triangulation.Handles.Iterators.DynamicIterators qualified as DynamicIterators
-import Moonlight.Triangulation.Internal.BoxedPaged (boxedMaterializedPageCount)
-import Moonlight.Triangulation.Internal.Canonical (canonicalize)
-import Moonlight.Triangulation.Internal.Paged
-  ( Paged
-  , PublicationStats (..)
-  , fromVector
-  , toVector
-  )
-import Moonlight.Triangulation.Internal.Predicates qualified as Admitted
-import Moonlight.Triangulation.Internal.PointIndex
-  ( MutablePointIndexUpdate (..)
-  , lookupMutablePoint
-  , newMutablePointIndex
-  , relocateMutablePoint
-  , removeMutablePoint
-  , seedMutablePointIndex
-  )
-import Moonlight.Triangulation.Internal.Representation qualified as Internal
-import Moonlight.Triangulation.Voronoi.Handles qualified as VoronoiDynamic
-import GHC.Generics (Generic)
-import Support (assertEqual, assertValid, requireQueryPoint, requireRight)
-
-tests :: IO ()
-tests = do
-  testConstructors
-  testPredicates
-  testParaboloidLift
-  assertFilteredPredicatesSkipExactOracle
-  testValidationRejectsCorruptedMeshes
-  testRefinementCompletionIsAFixpoint
-  testScalarFormat
-  testWideDistanceNearestNeighbor
-  testIncrementalLocationDescent
-  testPersistentInsertionReusesFrozenLocation
-  testVertexSeededSessionInsertion
-  testCircleSweepBulkLoad
-  testMixedEditSession
-  testBulkRemovalAgreement
-  testMutablePointIndexWraparoundBackshift
-  testBatchIdentityIndexBatchToEmpty
-  testBatchIdentityIndexToSingletonActive
-  testBulkIdentityIndexDoesNotEscapeRemovalBatch
-  testHierarchyRemovalAgreement
-  testHandlesAndFiniteDcel
-  testDegenerateConstruction
-  testPersistentLocalUpdates
-  testGenericPayloads
-  testPayloadMaps
-  testGeometryOnlyPublication
-  testPayloadTraversals
-  testRewritePayloadIdentity
-  testPointLocationAndHints
-  testHierarchyNestingLaw
-  testSibsonInterpolation
-  testVoronoiDual
-  testRemoval
-  testConstrainedDelaunay
-  testAnnotatedConstrainedUnion
-  testAsymmetricConstrainedExtension
-  testLargeAsymmetricConstrainedExtension
-  testSeparatedConstrainedSeam
-  testAxisSeparatedConstrainedSeams
-  testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite
-  testLocalRefinementRejectsEncroachedHullSplit
-  testConstrainedRefinement
-  testCheckedLocalRefinement
-  testMaximumEdgeLengthQuality
-  testLocalRefinementRejectsTrueInterfaceCrossing
-  testRepeatedBoundaryAdjacentRefinement
-  testFaceComponentsAndBoundaries
-  testAlphaFaceFiltration
-  testTraversal
-  testRandomizedConstruction
-  testErrors
-  putStrLn "all native core tests passed"
-
-data SampleVertex = SampleVertex
-  { samplePosition :: !(Point)
-  , sampleLabel :: !Int
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData)
-
-instance HasPosition SampleVertex where
-  position = samplePosition
-
-
-testConstructors :: IO ()
-testConstructors = do
-  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)
-  originQuery <- requireQueryPoint "empty location query" (Point 0 0)
-  assertValid "empty" vacant
-  assertEqual "empty vertex count" 0 (numVertices vacant)
-  assertEqual "empty directed edge count" 0 (numDirectedEdges vacant)
-  assertEqual "empty face count" 1 (numFaces vacant)
-  assertEqual "empty locates nowhere" EmptyTriangulation (locatePoint vacant originQuery)
-  built <- requirePointBuild "clear source" [Point 0 0, Point 1 0, Point 0 1]
-  assertEqual "clear returns the origin" vacant (clear (buildTriangulation built))
-
-type NativeMesh = Triangulation 'Unconstrained (Point) () () ()
-
--- | A named corruption of a mesh that was valid one line earlier, paired with
--- the violation it must provoke. Validation that has never been made to fail is
--- evidence only that it ran.
-data Corruption = Corruption
-  { corruptionName :: String
-  , corruptMesh :: NativeMesh -> NativeMesh
-  , provokes :: InvariantViolation -> Bool
-  }
-
--- Sheared so that no four sites are cocircular: on a square grid the diagonal
--- of every cell is a free choice, and a fixture that admits two answers cannot
--- witness a wrong one.
-corruptionFixture :: [Point]
-corruptionFixture =
-  [ Point (fromIntegral column + 0.25 * fromIntegral row) (1.3 * fromIntegral row)
-  | column <- [0 .. 3 :: Int]
-  , row <- [0 .. 3 :: Int]
-  ]
-
-rewritePlane :: (U.Unbox a, Num a) => (U.Vector a -> U.Vector a) -> Paged a -> Paged a
-rewritePlane edit = fromVector 0 . edit . toVector
-
-slot :: U.Unbox a => Int -> a -> U.Vector a -> U.Vector a
-slot at value = (U.// [(at, value)])
-
--- Past the end of every plane in the fixture, and far from the sentinels the
--- packed representation reserves for absence.
-beyond :: NativeMesh -> Word32
-beyond mesh = fromIntegral (numDirectedEdges mesh + numVertices mesh + 64)
-
-onTopology :: (NativeMesh -> U.Vector Word32 -> U.Vector Word32) -> NativeMesh -> NativeMesh
-onTopology edit mesh =
-  mesh {Internal.triHalfTopology = rewritePlane (edit mesh) (Internal.triHalfTopology mesh)}
-
--- The half-edge plane has stride four — origin, next, previous, face — so the
--- first four entries below are one surgery distinguished only by which field
--- the out-of-range index lands on.
-structuralCorruptions :: [Corruption]
-structuralCorruptions =
-  [ Corruption
-      "edge origin names an absent vertex"
-      (onTopology (slot 0 . beyond))
-      (\case EdgeOriginOutOfRange {} -> True; _ -> False)
-  , Corruption
-      "edge next names an absent edge"
-      (onTopology (slot 1 . beyond))
-      (\case EdgeNextOutOfRange {} -> True; _ -> False)
-  , Corruption
-      "edge previous names an absent edge"
-      (onTopology (slot 2 . beyond))
-      (\case EdgePreviousOutOfRange {} -> True; _ -> False)
-  , Corruption
-      "edge face names an absent face"
-      (onTopology (slot 3 . beyond))
-      (\case EdgeFaceOutOfRange {} -> True; _ -> False)
-  , Corruption
-      "vertex outgoing names an absent edge"
-      ( \mesh ->
-          mesh
-            { Internal.triVertexOut =
-                rewritePlane (slot 0 (beyond mesh)) (Internal.triVertexOut mesh)
-            }
-      )
-      (\case VertexOutgoingOutOfRange {} -> True; _ -> False)
-  , Corruption
-      "a coordinate plane is one short"
-      (\mesh -> mesh {Internal.triPointX = rewritePlane U.init (Internal.triPointX mesh)})
-      (\case CoordinatePlaneLengthMismatch {} -> True; _ -> False)
-  , Corruption
-      "every inner face wound clockwise"
-      (\mesh -> mesh {Internal.triPointY = rewritePlane (U.map negate) (Internal.triPointY mesh)})
-      (\case InnerFaceNotCounterClockwise {} -> True; _ -> False)
-  ]
-
--- Structural corruption is caught before geometry is read, so the empty-circle
--- law needs a mesh that stays well formed and merely stops being Delaunay.
-delaunayCorruptions :: [Corruption]
-delaunayCorruptions =
-  [ Corruption
-      "one site dragged through its neighbours' circumcircles"
-      (\mesh -> mesh {Internal.triPointX = rewritePlane (slot 5 40) (Internal.triPointX mesh)})
-      (\case LocallyIllegalDelaunayEdge {} -> True; _ -> False)
-  ]
-
--- A surgery that changed nothing would report the oracle as unarmed when in
--- truth it was never asked anything, so the mutant must differ before its
--- rejection means a thing.
-assertRejects
-  :: String -> (NativeMesh -> [InvariantViolation]) -> NativeMesh -> Corruption -> IO ()
-assertRejects oracle check pristine corruption = do
-  let mutant = corruptMesh corruption pristine
-      label = oracle <> " / " <> corruptionName corruption
-      reported = check mutant
-  when (mutant == pristine) $ fail (label <> ": the surgery changed nothing")
-  unless (any (provokes corruption) reported) $
-    fail (label <> ": admitted, reporting " <> show reported)
-
-testValidationRejectsCorruptedMeshes :: IO ()
-testValidationRejectsCorruptedMeshes = do
-  built <- requirePointBuild "corruption fixture" corruptionFixture
-  let pristine = buildTriangulation built
-  assertValid "corruption fixture" pristine
-  mapM_ (assertRejects "topology" validateTopology pristine) structuralCorruptions
-  mapM_ (assertRejects "delaunay" validateDelaunay pristine) delaunayCorruptions
-  case innerFaces pristine of
-    [] -> fail "local validation fixture has no inner face"
-    witnessFace : _ ->
-      case faceDirectedEdges pristine witnessFace of
-        [] -> fail "local validation fixture has no face edge"
-        DirectedEdgeId rawEdge : _ -> do
-          let admitted = IntSet.singleton (fromIntegral (unFaceId witnessFace))
-              corrupted =
-                pristine
-                  { Internal.triHalfTopology =
-                      rewritePlane
-                        (slot (4 * fromIntegral rawEdge + 1) rawEdge)
-                        (Internal.triHalfTopology pristine)
-                  }
-          assertEqual
-            "selected validation accepts valid closure"
-            []
-            (validateTopologyClosure admitted IntSet.empty pristine)
-          unless (not (null (validateTopology corrupted))) $
-            fail "global validation missed the locally corrupt face closure"
-          unless (not (null (validateTopologyClosure admitted IntSet.empty corrupted))) $
-            fail "selected validation missed a locally corrupt face closure"
-
-reverseTraversalEvent :: Intersection -> Intersection
-reverseTraversalEvent = \case
-  EdgeIntersection edge -> EdgeIntersection (reverseEdge edge)
-  EdgeOverlap edge -> EdgeOverlap (reverseEdge edge)
-  VertexIntersection vertex -> VertexIntersection vertex
-
--- | What @refinementComplete@ claims is that the quality worklist drained. The
--- assertable content of that claim is a fixpoint: a second pass under the same
--- parameters can admit nothing. The complementary run is budget-starved, where
--- the run must stop short and spend exactly what it was given.
-testRefinementCompletionIsAFixpoint :: IO ()
-testRefinementCompletionIsAFixpoint = do
-  -- Barrier parity needs constraints to bound a domain: on an unconstrained
-  -- mesh every face sits at depth zero, so excluding outer faces excludes all
-  -- of them and the worklist drains having refined nothing.
-  bounded <-
-    requireRight "refinement fixpoint domain" $
-      constrainedDelaunay
-        unitElementDefaults
-        (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8])
-        (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
-  let source = buildTriangulation bounded
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 500
-          , refineMaxArea = Just 3
-          , refineExcludeOuterFaces = True
-          }
-  drained <- requireRight "drained refinement" (refine id parameters source)
-  unless (refinementComplete drained) $
-    fail "the refinement budget was too small to drain the worklist"
-  -- Without this the fixpoint below is vacuous: a run that refined nothing
-  -- trivially admits nothing on a second pass.
-  unless (refinementAddedVertices drained > 0) $
-    fail "the drained run inserted no Steiner points, so the fixpoint proves nothing"
-  assertValid "drained refinement" (refinedTriangulation drained)
-  again <- requireRight "second refinement pass" (refine id parameters (refinedTriangulation drained))
-  assertEqual
-    "a drained worklist admits nothing on a second pass"
-    0
-    (refinementAddedVertices again)
-  starved <-
-    requireRight
-      "starved refinement"
-      (refine id parameters {refineMaxAdditionalVertices = Just 3} source)
-  when (refinementComplete starved) $
-    fail "a three-vertex budget reported a drained worklist"
-  assertEqual "a starved run spends exactly its budget" 3 (refinementAddedVertices starved)
-
-testScalarFormat :: IO ()
-testScalarFormat = do
-  let binary64 = scalarBinaryFormat
-  assertEqual "binary64 radix" 2 (formatRadix binary64)
-  assertEqual "binary64 mantissa" 53 (formatMantissaDigits binary64)
-  assertEqual "binary64 unit roundoff" (encodeFloat 1 (-53)) (scalarUnitRoundoff :: Double)
-
-testWideDistanceNearestNeighbor :: IO ()
-testWideDistanceNearestNeighbor = do
-  let largePoints = V.fromList
-        [ Point 0 0
-        , Point 1.0e20 0
-        , Point 2.0e20 0
-        , Point 1.0e20 1.0e19
-        ] :: V.Vector (Point)
-  largeBuild <- requireRight "wide-distance build" (delaunay unitElementDefaults largePoints)
-  let largeTriangulation = buildTriangulation largeBuild
-  assertValid "wide-distance triangulation" largeTriangulation
-  query <- requireQueryPoint "wide-distance query" (Point 1.05e20 0)
-  case nearestNeighbor largeTriangulation Nothing query of
-    Nothing -> fail "wide-distance nearest-neighbor returned Nothing"
-    Just (nearest, _) -> assertEqual "wide-distance nearest-neighbor" (VertexId 1) nearest
-
-data IncrementalLocationEvidence = IncrementalLocationEvidence
-  { incrementalTriangulation :: !(DelaunayTriangulation (Point))
-  , incrementalWalkSteps :: {-# UNPACK #-} !Int
-  , incrementalFallbacks :: {-# UNPACK #-} !Int
-  }
-
-testIncrementalLocationDescent :: IO ()
-testIncrementalLocationDescent = do
-  small <- collectIncrementalLocationEvidence 500
-  large <- collectIncrementalLocationEvidence 1000
-  assertEqual "incremental location fallbacks/500" 0 (incrementalFallbacks small)
-  assertEqual "incremental location fallbacks/1000" 0 (incrementalFallbacks large)
-  unless (2 * incrementalWalkSteps large < 7 * incrementalWalkSteps small) $
-    fail
-      ( "incremental location approached quadratic growth: "
-          <> show (incrementalWalkSteps small, incrementalWalkSteps large)
-      )
-  assertValid "incremental location descent/1000" (incrementalTriangulation large)
-
-collectIncrementalLocationEvidence :: Int -> IO IncrementalLocationEvidence
-collectIncrementalLocationEvidence count =
-  V.foldM' insertAndAccumulate initialEvidence (V.fromList (randomPoints 0xc1ac_10ca count))
- where
-  initialEvidence =
-    IncrementalLocationEvidence
-      { incrementalTriangulation = empty unitElementDefaults
-      , incrementalWalkSteps = 0
-      , incrementalFallbacks = 0
-      }
-
-  insertAndAccumulate evidence point = do
-    result <- requireRight "incremental location descent" (insert (incrementalTriangulation evidence) point)
-    let stats = insertionStats result
-    pure
-      IncrementalLocationEvidence
-        { incrementalTriangulation = insertionTriangulation result
-        , incrementalWalkSteps = incrementalWalkSteps evidence + statLocationWalkSteps stats
-        , incrementalFallbacks = incrementalFallbacks evidence + statLocationFallbacks stats
-        }
-
--- A persistent insertion locates on the frozen mesh before opening its dense
--- transaction. The thaw preserves every extant handle, so a lawful frozen
--- location can be interpreted directly without a second mutable walk. A
--- degenerate-line outside witness lacks the terminal-edge evidence its mutable
--- interpreter requires, so that one stratum deliberately retains the mutable
--- fallback. Exercise every frozen stratum, including the singleton's edge-less
--- outside witness, rather than testing only the ordinary face case.
-testPersistentInsertionReusesFrozenLocation :: IO ()
-testPersistentInsertionReusesFrozenLocation = do
-  let vacant = empty unitElementDefaults :: DelaunayTriangulation (Point)
-  emptyLocation <- assertPersistentInsertionFromFrozenLocation "empty" Inserted vacant (Point 0 0)
-  assertEqual "empty insertion frozen location" EmptyTriangulation emptyLocation
-
-  singletonBuild <- requirePointBuild "singleton frozen location" [Point 0 0]
-  singletonLocation <-
-    assertPersistentInsertionFromFrozenLocation
-      "singleton outside insertion"
-      Inserted
-      (buildTriangulation singletonBuild)
-      (Point 2 0)
-  assertEqual "singleton insertion frozen location" (OutsideConvexHull Nothing) singletonLocation
-
-  lineBuild <- requirePointBuild "line frozen locations" [Point 0 0, Point 2 0, Point 4 0]
-  let line = buildTriangulation lineBuild
-  lineEdgeLocation <- assertPersistentInsertionFromFrozenLocation "line edge insertion" Inserted line (Point 1 0)
-  case lineEdgeLocation of
-    OnEdge _ -> pure ()
-    other -> fail ("line edge insertion located " <> show other)
-  lineOutsideLocation <- assertPersistentInsertionFromFrozenLocation "line extension" Inserted line (Point 6 0)
-  case lineOutsideLocation of
-    OutsideConvexHull (Just _) -> pure ()
-    other -> fail ("line extension located " <> show other)
-
-  triangleBuild <- requirePointBuild "area frozen locations" [Point 0 0, Point 4 0, Point 0 4]
-  let triangle = buildTriangulation triangleBuild
-  faceLocation <- assertPersistentInsertionFromFrozenLocation "face insertion" Inserted triangle (Point 1 1)
-  case faceLocation of
-    InFace _ -> pure ()
-    other -> fail ("face insertion located " <> show other)
-  edgeLocation <- assertPersistentInsertionFromFrozenLocation "area edge insertion" Inserted triangle (Point 2 0)
-  case edgeLocation of
-    OnEdge _ -> pure ()
-    other -> fail ("area edge insertion located " <> show other)
-  hullLocation <- assertPersistentInsertionFromFrozenLocation "hull insertion" Inserted triangle (Point 5 1)
-  case hullLocation of
-    OutsideConvexHull (Just _) -> pure ()
-    other -> fail ("hull insertion located " <> show other)
-  duplicateLocation <- assertPersistentInsertionFromFrozenLocation "duplicate insertion" AlreadyPresent triangle (Point 0 0)
-  assertEqual "duplicate insertion frozen location" (OnVertex (VertexId 0)) duplicateLocation
-
-assertPersistentInsertionFromFrozenLocation
-  :: String
-  -> InsertionDisposition
-  -> DelaunayTriangulation (Point)
-  -> Point
-  -> IO Location
-assertPersistentInsertionFromFrozenLocation label expectedDisposition source point = do
-  query <- requireQueryPoint (label <> " frozen query") point
-  let (located, walked) = locatePointWithHint source Nothing query
-      sourceVertices = numVertices source
-      usesMutableFallback =
-        case located of
-          OutsideConvexHull (Just _) -> numInnerFaces source == 0
-          _ -> False
-  result <- requireRight (label <> " insert") (insert source point)
-  ((referenceVertex, referenceDisposition), reference, _) <-
-    requireRight (label <> " session reference") $
-      withSession source 1 (insertVertexAt point point)
-  let stats = insertionStats result
-      expectedVertices =
-        case expectedDisposition of
-          Inserted -> sourceVertices + 1
-          AlreadyPresent -> sourceVertices
-      expectedUnique =
-        case expectedDisposition of
-          Inserted -> 1
-          AlreadyPresent -> 0
-      expectedExisting =
-        case expectedDisposition of
-          Inserted -> 0
-          AlreadyPresent -> 1
-  assertEqual (label <> " disposition") expectedDisposition (insertionDisposition result)
-  assertEqual (label <> " session disposition") referenceDisposition (insertionDisposition result)
-  assertEqual (label <> " session vertex") referenceVertex (insertionVertex result)
-  assertEqual
-    (label <> " exact-location topology matches session")
-    (canonicalEdges reference)
-    (canonicalEdges (insertionTriangulation result))
-  assertEqual (label <> " source remains unchanged") sourceVertices (numVertices source)
-  assertEqual (label <> " result vertex count") expectedVertices (numVertices (insertionTriangulation result))
-  assertEqual (label <> " input count") 1 (statInputPoints stats)
-  assertEqual (label <> " unique count") expectedUnique (statUniquePoints stats)
-  assertEqual (label <> " existing count") expectedExisting (statExistingPoints stats)
-  assertEqual (label <> " duplicate count") expectedExisting (statDuplicatePoints stats)
-  if usesMutableFallback
-    then do
-      let assertAtLeast counter expected actual =
-            unless
-              (actual >= expected)
-              ( fail
-                  ( label
-                      <> " "
-                      <> counter
-                      <> " includes frozen evidence: expected at least "
-                      <> show expected
-                      <> ", got "
-                      <> show actual
-                  )
-              )
-      assertAtLeast "walk steps" (locationWalkSteps walked) (statLocationWalkSteps stats)
-      assertAtLeast "walk maximum" (locationWalkSteps walked) (statLocationMaxWalk stats)
-      assertAtLeast
-        "fallback count"
-        (if locationUsedFallback walked then 1 else 0)
-        (statLocationFallbacks stats)
-    else do
-      assertEqual (label <> " frozen walk steps") (locationWalkSteps walked) (statLocationWalkSteps stats)
-      assertEqual (label <> " frozen walk maximum") (locationWalkSteps walked) (statLocationMaxWalk stats)
-      assertEqual
-        (label <> " frozen fallback count")
-        (if locationUsedFallback walked then 1 else 0)
-        (statLocationFallbacks stats)
-  assertValid (label <> " result") (insertionTriangulation result)
-  pure located
-
--- A vertex hint restricts to one incident face and then relinquishes
--- authority to the exact mutable walk. Valid, stale, occupied, and face-less
--- local sections must therefore glue to the same published result as the
--- unhinted entrance.
-testVertexSeededSessionInsertion :: IO ()
-testVertexSeededSessionInsertion = do
-  area <- sampleMesh "vertex-seeded area" [Point 0 0, Point 4 0, Point 0 4]
-  seed <- vertexAt "vertex-seeded area seed" area (Point 0 0)
-  assertVertexSeededInsertionAgreement
-    "vertex-seeded interior insertion"
-    area
-    seed
-    (SampleVertex (Point 1 1) 40)
-  assertVertexSeededInsertionAgreement
-    "vertex-seeded invalid hint"
-    area
-    (VertexId maxBound)
-    (SampleVertex (Point 3 3) 41)
-  assertVertexSeededInsertionAgreement
-    "vertex-seeded duplicate"
-    area
-    seed
-    (SampleVertex (Point 4 0) 42)
-
-  line <- sampleMesh "vertex-seeded line" [Point 0 0, Point 2 0, Point 4 0]
-  lineSeed <- vertexAt "vertex-seeded line seed" line (Point 2 0)
-  assertVertexSeededInsertionAgreement
-    "vertex-seeded face-less fallback"
-    line
-    lineSeed
-    (SampleVertex (Point 3 0) 43)
- where
-  sampleMesh
-    :: String
-    -> [Point]
-    -> IO (Triangulation 'Unconstrained SampleVertex () () ())
-  sampleMesh label points = do
-    built <-
-      requireRight label $
-        delaunay
-          unitElementDefaults
-          (V.imap (\index point -> SampleVertex point index) (V.fromList points))
-    pure (buildTriangulation built)
-
-  vertexAt
-    :: String
-    -> Triangulation 'Unconstrained SampleVertex () () ()
-    -> Point
-    -> IO VertexId
-  vertexAt label triangulation point = do
-    query <- requireQueryPoint label point
-    case locatePoint triangulation query of
-      OnVertex vertex -> pure vertex
-      location -> fail (label <> " did not locate a vertex: " <> show location)
-
-assertVertexSeededInsertionAgreement
-  :: String
-  -> Triangulation 'Unconstrained SampleVertex () () ()
-  -> VertexId
-  -> SampleVertex
-  -> IO ()
-assertVertexSeededInsertionAgreement label source seed payload = do
-  ((referenceVertex, referenceDisposition), reference, _) <-
-    requireRight (label <> " unhinted") $
-      withSession source 1 (insertVertexAt (samplePosition payload) payload)
-  ((hintedVertex, hintedDisposition), hinted, _) <-
-    requireRight (label <> " hinted") $
-      withSession source 1 (insertVertexAtNearVertex seed (samplePosition payload) payload)
-  assertEqual (label <> " vertex") referenceVertex hintedVertex
-  assertEqual (label <> " disposition") referenceDisposition hintedDisposition
-  assertEqual (label <> " topology") (canonicalEdges reference) (canonicalEdges hinted)
-  assertEqual (label <> " payload") payload (vertexData hinted hintedVertex)
-  assertValid label hinted
-
--- Circle sweep must be a construction schedule, not a second topology. It is
--- compared against the arrival-order session kernel on the same exact inputs.
--- | One session, both verbs. The reason the two published sessions became one:
--- a caller who removes and inserts had to thaw twice and pay the O(n)
--- publication a session exists to delete.
---
--- Graded against the oracle that needs no second implementation — the Delaunay
--- triangulation of a point set in general position is unique, so a mixed edit
--- must land exactly where a fresh bulk load of the surviving set lands.
-testMixedEditSession :: IO ()
-testMixedEditSession = do
-  let original = V.fromList (randomPoints 0x5eed1e 400)
-      doomed = V.take 120 original
-      survivors = V.drop 120 original
-      arrivals = V.fromList (randomPoints 0xa7717a1 90)
-  (_, edited, stats) <-
-    requireRight "mixed session" $
-      withSession
-        (buildTriangulation (either (error . show) id (delaunay unitElementDefaults original)))
-        (V.length arrivals)
-        ( do
-            V.mapM_ (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))) doomed
-            V.mapM_ insertVertex arrivals
-        )
-  fresh <- requireRight "fresh rebuild" (delaunay unitElementDefaults (survivors <> arrivals))
-  assertEqual
-    "mixed edit equals a fresh build of the surviving set"
-    (canonicalEdges (buildTriangulation fresh))
-    (canonicalEdges edited)
-  assertValid "mixed edit session" edited
-  assertEqual
-    "the whole transaction charged one counter set"
-    (V.length arrivals)
-    (statInputPoints stats)
-
--- The bulk removal verb must land exactly where the singleton fold lands, on
--- both sides of its locate-strategy crossover: a small batch keeps the
--- per-question walk, a large one buys the identity index once. Same removals,
--- same order, same survivor either way.
-testBulkRemovalAgreement :: IO ()
-testBulkRemovalAgreement = do
-  let original = V.fromList (randomPoints 0xb01dca7 400)
-      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))
-      run label doomed = do
-        (outcomes, survived, _) <-
-          requireRight (label <> " bulk removal") (withSession base 0 (removeManyAt doomed))
-        V.imapM_
-          ( \index outcome ->
-              maybe (fail (label <> " bulk removal missed index " <> show index)) (const (pure ())) outcome
-          )
-          outcomes
-        (_, folded, _) <-
-          requireRight (label <> " singleton removal fold") $
-            withSession
-              base
-              0
-              ( V.mapM_
-                  (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))
-                  doomed
-              )
-        assertEqual
-          (label <> " bulk removal equals singleton descent")
-          (canonicalEdges folded)
-          (canonicalEdges survived)
-        assertValid (label <> " bulk removal") survived
-  run "walking" (V.take 40 original)
-  run "indexed" (V.take 120 original)
-
--- The three fixture positions hash to home slot 15 in the 16-slot table made
--- for three keys. They therefore occupy 15, 0, and 1 in insertion order. The
--- middle deletion exercises both wraparound and backward shift, while the
--- coordinate overwrite models the tail move performed before the table handle
--- is renamed by 'swapRemoveVertex'.
-testMutablePointIndexWraparoundBackshift :: IO ()
-testMutablePointIndexWraparoundBackshift = do
-  (removed, first, movedBeforeRelocation, relocated, movedAfterRelocation) <-
-    requireRight "mutable point-index wraparound/backshift law" wraparoundLaw
-  case removed of
-    MutablePointIndexUpdated -> pure ()
-    MutablePointIndexInvalidated -> fail "mutable point-index middle delete invalidated a valid table"
-  assertEqual "mutable point-index first wraparound occupant" (Just 0) first
-  assertEqual "mutable point-index shifted tail before relocation" (Just 2) movedBeforeRelocation
-  case relocated of
-    MutablePointIndexUpdated -> pure ()
-    MutablePointIndexInvalidated -> fail "mutable point-index tail relocation invalidated a valid table"
-  assertEqual "mutable point-index relocated tail" (Just 1) movedAfterRelocation
- where
-  wraparoundLaw = runST $ do
-    pointXs <- MUV.replicate 3 (0 :: Double)
-    pointYs <- MUV.replicate 3 (0 :: Double)
-    MUV.unsafeWrite pointXs 0 (-20)
-    MUV.unsafeWrite pointYs 0 (-15)
-    MUV.unsafeWrite pointXs 1 (-20)
-    MUV.unsafeWrite pointYs 1 0
-    MUV.unsafeWrite pointXs 2 (-20)
-    MUV.unsafeWrite pointYs 2 7
-    table <- newMutablePointIndex 3
-    seeded <- seedMutablePointIndex table 3 (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs)
-    case seeded of
-      Left failure -> pure (Left failure)
-      Right () -> do
-        -- The tail has moved into slot one before identity transport begins.
-        MUV.unsafeWrite pointXs 1 (-20)
-        MUV.unsafeWrite pointYs 1 7
-        removed <-
-          removeMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 0 1
-        first <-
-          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) (-15)
-        movedBeforeRelocation <-
-          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7
-        relocated <- relocateMutablePoint table (-20) 7 2 1
-        movedAfterRelocation <-
-          lookupMutablePoint table (MUV.unsafeRead pointXs) (MUV.unsafeRead pointYs) (-20) 7
-        pure
-          ( Right
-              ( removed
-              , first
-              , movedBeforeRelocation
-              , relocated
-              , movedAfterRelocation
-              )
-          )
-
--- A dense table must also close lawfully when it removes every vertex. The
--- point-keyed query after freeze forces the empty published derivation rather
--- than retaining an impossible ST table.
-testBatchIdentityIndexBatchToEmpty :: IO ()
-testBatchIdentityIndexBatchToEmpty = do
-  let points = V.fromList (randomPoints 0x7a110bad 32)
-  built <- requireRight "batch identity empty base" (delaunay unitElementDefaults points)
-  (outcomes, emptied, _) <-
-    requireRight "batch identity removes every point" $
-      withSession (buildTriangulation built) 0 (removeManyAt points)
-  unless (V.all isJust outcomes) $
-    fail "batch identity table missed a point while removing to empty"
-  assertEqual "batch identity empty vertex count" 0 (numVertices emptied)
-  assertValid "batch identity empty result" emptied
-  absent <- requireRight "empty published identity lookup" (locateAndRemove emptied (Point 0 0))
-  case absent of
-    Nothing -> pure ()
-    Just _ -> fail "empty published identity lookup manufactured a removal"
-
--- A dense batch discards its ST table before the next singleton handle rewrite.
--- 'excise' must therefore activate and transport the ordinary persistent index
--- without consulting the expired batch representation.
-testBatchIdentityIndexToSingletonActive :: IO ()
-testBatchIdentityIndexToSingletonActive = do
-  let original = V.fromList (randomPoints 0x51a91e 64)
-      doomed = V.take 32 original
-      survivors = V.drop 32 original
-  built <- requireRight "batch-to-singleton identity base" (delaunay unitElementDefaults original)
-  (singletonOutcome, transitioned, _) <-
-    requireRight "batch-to-singleton identity session" $
-      withSession (buildTriangulation built) 0 $ do
-        _ <- removeManyAt doomed
-        excise (VertexId 0)
-  let singletonPoint = removalOutcomePoint singletonOutcome
-      expected = V.filter (/= singletonPoint) survivors
-  fresh <- requireRight "batch-to-singleton fresh rebuild" (delaunay unitElementDefaults expected)
-  assertEqual
-    "batch-to-singleton identity topology"
-    (canonicalEdges (buildTriangulation fresh))
-    (canonicalEdges transitioned)
-  assertValid "batch-to-singleton identity result" transitioned
-  case V.find (/= singletonPoint) survivors of
-    Nothing -> fail "batch-to-singleton fixture exhausted every survivor"
-    Just remaining -> do
-      located <- requireRight "batch-to-singleton published lookup" (locateAndRemove transitioned remaining)
-      case located of
-        Nothing -> fail "batch-to-singleton published lookup missed a survivor"
-        Just removal -> assertValid "batch-to-singleton published removal" (removalTriangulation removal)
-
--- The mutable identity table is an internal section of @removeManyAt@, never a
--- session-wide owner. An insertion after the dense batch invalidates it, the
--- subsequent removal walks correctly, and the frozen mesh must rederive the
--- published identity cache from its final coordinate authority.
-testBulkIdentityIndexDoesNotEscapeRemovalBatch :: IO ()
-testBulkIdentityIndexDoesNotEscapeRemovalBatch = do
-  let original = V.fromList (randomPoints 0x5a11ce 400)
-      doomed = V.take 120 original
-      survivors = V.drop 120 original
-      arrival = Point (-0.25) 0.75
-  baseBuild <- requireRight "bulk identity section base" (delaunay unitElementDefaults original)
-  (_, edited, _) <-
-    requireRight "bulk identity section mixed session" $
-      withSession (buildTriangulation baseBuild) 1 $ do
-        _ <- removeManyAt doomed
-        _ <- insertVertexAt arrival arrival
-        removeAt arrival >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ()))
-  fresh <- requireRight "bulk identity section fresh survivor rebuild" (delaunay unitElementDefaults survivors)
-  assertEqual
-    "bulk identity section mixed program equals fresh survivors"
-    (canonicalEdges (buildTriangulation fresh))
-    (canonicalEdges edited)
-  assertValid "bulk identity section mixed session" edited
-  case V.uncons survivors of
-    Nothing -> fail "bulk identity section test has no survivor"
-    Just (survivor, _) -> do
-      located <- requireRight "published lazy identity lookup" (locateAndRemove edited survivor)
-      case located of
-        Nothing -> fail "published lazy identity lookup missed survivor"
-        Just removal -> assertValid "published lazy identity removal" (removalTriangulation removal)
-
--- The hierarchy-hinted removal program must land exactly where the unhinted
--- session lands. The guesses are all computed against the original base, so
--- later removals in the batch answer for guesses whose slots swap-compaction
--- has renamed — the walk must correct every one of them. The repaired
--- hierarchy must equal the reference rebuild over the same survivor.
-testHierarchyRemovalAgreement :: IO ()
-testHierarchyRemovalAgreement = do
-  let original = V.fromList (randomPoints 0x5eed1e55 400)
-      base = buildTriangulation (either (error . show) id (delaunay unitElementDefaults original))
-  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint defaultHierarchyBranchFactor base)
-  let run label doomed = do
-        (outcomes, survived, repaired) <-
-          requireRight (label <> " hinted removal") (removeManyWithHierarchy hierarchy base doomed)
-        V.imapM_
-          ( \index outcome ->
-              maybe (fail (label <> " hinted removal missed index " <> show index)) (const (pure ())) outcome
-          )
-          outcomes
-        (_, folded, _) <-
-          requireRight (label <> " unhinted session") (withSession base 0 (removeManyAt doomed))
-        reference <- requireRight (label <> " reference rebuild") (rebuildHierarchyHint hierarchy folded)
-        assertEqual
-          (label <> " hinted removal equals unhinted session")
-          (canonicalEdges folded)
-          (canonicalEdges survived)
-        assertEqual (label <> " repaired hierarchy equals reference rebuild") reference repaired
-        assertValid (label <> " hinted removal") survived
-  run "sparse" (V.take 40 original)
-  run "dense" (V.take 120 original)
-
-testCircleSweepBulkLoad :: IO ()
-testCircleSweepBulkLoad = do
-  let points = V.fromList (randomPoints 0xc1ac1e 1500)
-  swept <- requireRight "circle-sweep build" (delaunay unitElementDefaults points)
-  (_, sessioned, _) <-
-    requireRight "session build" $
-      withSession (empty unitElementDefaults) (V.length points) $
-        V.mapM_ insertVertex points
-  assertEqual
-    "circle-sweep/session topology"
-    (canonicalEdges sessioned)
-    (canonicalEdges (buildTriangulation swept))
-  assertValid "circle-sweep build" (buildTriangulation swept)
-  assertValid "session build" sessioned
-
-testPredicates :: IO ()
-testPredicates = do
-  let rawA, rawB, rawC :: Point
-      rawA = Point 0 0
-      rawB = Point 1 0
-      rawC = Point 0 1
-  a <- requireRight "predicate point a" (mkQueryPoint rawA)
-  b <- requireRight "predicate point b" (mkQueryPoint rawB)
-  c <- requireRight "predicate point c" (mkQueryPoint rawC)
-  midpointQuery <- requireRight "predicate midpoint" (mkQueryPoint (Point 0.5 0))
-  inside <- requireRight "incircle inside point" (mkQueryPoint (Point 0.25 0.25))
-  boundary <- requireRight "incircle boundary point" (mkQueryPoint (Point 1 1))
-  outside <- requireRight "incircle outside point" (mkQueryPoint (Point 2 2))
-  assertEqual "orientation left" GT (orient2d a b c)
-  assertEqual "orientation right" LT (orient2d b a c)
-  assertEqual "orientation collinear" EQ (orient2d a b midpointQuery)
-  assertEqual "incircle inside" GT (inCircle a b c inside)
-  assertEqual "incircle boundary" EQ (inCircle a b c boundary)
-  assertEqual "incircle outside" LT (inCircle a b c outside)
-  assertEqual "underflow mitigation" (Point 0 1) (mitigateUnderflow (Point 1.0e-44 1 :: Point))
-  _ <- requireRight "point validation" (validatePoint Nothing (Point 0 1 :: Point))
-  let large = encodeFloat 1 180 :: Double
-      ulp = encodeFloat 1 128 :: Double
-  largeA <- requireRight "large predicate point a" (mkQueryPoint (Point large large))
-  largeB <- requireRight "large predicate point b" (mkQueryPoint (Point (large + ulp) large))
-  largeC <- requireRight "large predicate point c" (mkQueryPoint (Point large (large + ulp)))
-  largeD <- requireRight "large predicate point d" (mkQueryPoint (Point (large + ulp) (large + ulp)))
-  assertEqual
-    "large exact orientation"
-    GT
-    (orient2d largeA largeB largeC)
-  assertEqual
-    "large exact cocircularity"
-    EQ
-    (inCircle largeA largeB largeC largeD)
-
-testHandlesAndFiniteDcel :: IO ()
-testHandlesAndFiniteDcel = do
-  built <- requirePointBuild "handle algebra" [Point 0 0, Point 3 0, Point 0 2, Point 0.4 0.7]
-  let triangulation = buildTriangulation built
-  assertValid "handle algebra" triangulation
-  assertEqual "one outer face" (numInnerFaces triangulation + 1) (numFaces triangulation)
-  forM_ (directedEdges triangulation) $ \edge -> do
-    assertEqual "double reversal" edge (reverseEdge (reverseEdge edge))
-    assertEqual "next/previous" edge (previous triangulation (next triangulation edge))
-    assertEqual "previous/next" edge (next triangulation (previous triangulation edge))
-  forM_ (undirectedEdges triangulation) $ \edge -> do
-    let (forward, backward) = directedPair edge
-    assertEqual "pair reversal" backward (reverseEdge forward)
-    assertEqual "undirected projection" edge (asUndirected forward)
-  assertEqual "vertex iterator" (numVertices triangulation) (length (vertices triangulation))
-  assertEqual
-    "dense vertex projection"
-    (V.fromList (fmap (vertexPoint triangulation) (vertices triangulation)))
-    (vertexPoints triangulation)
-  assertEqual "edge iterator" (numUndirectedEdges triangulation) (length (undirectedEdges triangulation))
-  assertEqual "face iterator" (numInnerFaces triangulation) (length (innerFaces triangulation))
-  assertEqual
-    "dense inner-face directed-edge projection"
-    (Just (V.toList (innerFaceDirectedEdgeTriples triangulation)))
-    (traverse (Dcel.innerFaceDirectedEdges triangulation) (innerFaces triangulation))
-  assertEqual
-    "dense inner-face projection"
-    (Just (V.toList (innerFaceVertexTriples triangulation)))
-    (traverse (Dcel.innerFaceVertices triangulation) (innerFaces triangulation))
-  assertEqual "dynamic vertex iterator" (numVertices triangulation) (length (DynamicIterators.vertexHandles triangulation))
-  assertEqual "dynamic edge iterator" (numDirectedEdges triangulation) (length (DynamicIterators.directedEdgeHandles triangulation))
-  assertEqual "dynamic face iterator" (numInnerFaces triangulation) (length (DynamicIterators.innerFaceHandles triangulation))
-  unless (geometryTopologyBytes triangulation > topologyIndexBytes triangulation) $
-    fail "geometry byte accounting omitted coordinates"
-  vertex0 <- requireJust "dynamic vertex handle" (Dynamic.vertexHandle triangulation (VertexId 0))
-  assertEqual "dynamic vertex fix" (VertexId 0) (Dynamic.fixVertex vertex0)
-  assertEqual "dynamic vertex position" (vertexPoint triangulation (VertexId 0)) (Dynamic.vertexHandlePosition vertex0)
-  case Dynamic.vertexHandleOutEdge vertex0 of
-    Nothing -> fail "connected dynamic vertex has no outgoing edge"
-    Just edge -> do
-      assertEqual "dynamic edge reversal" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgeReverse (Dynamic.directedEdgeReverse edge)))
-      assertEqual "dynamic edge next/previous" (Dynamic.fixDirectedEdge edge) (Dynamic.fixDirectedEdge (Dynamic.directedEdgePrevious (Dynamic.directedEdgeNext edge)))
-      let face = Dynamic.directedEdgeFace edge
-      if Dynamic.faceIsOuter face
-        then pure ()
-        else case Dynamic.faceAsInner face of
-          Nothing -> fail "non-outer dynamic face did not refine to InnerTag"
-          Just inner -> do
-            _ <- requireJust "inner dynamic face vertices" (Dynamic.innerFaceVertices inner)
-            case Dynamic.innerFaceCircumcenter inner of
-              Nothing -> fail "inner dynamic face has no circumcenter"
-              Just _ -> pure ()
-
-testDegenerateConstruction :: IO ()
-testDegenerateConstruction = do
-  emptyBuild <- requirePointBuild "empty" []
-  emptyQuery <- requireQueryPoint "empty location" (Point 0 0)
-  assertEqual "empty vertices" 0 (numVertices (buildTriangulation emptyBuild))
-  assertEqual "empty location" EmptyTriangulation (locatePoint (buildTriangulation emptyBuild) emptyQuery)
-  assertValid "empty" (buildTriangulation emptyBuild)
-
-  singleton <- requirePointBuild "singleton" [Point 2 3]
-  singletonQuery <- requireQueryPoint "singleton lookup" (Point 2 3)
-  assertEqual "singleton lookup" (OnVertex (VertexId 0)) (locatePoint (buildTriangulation singleton) singletonQuery)
-  assertValid "singleton" (buildTriangulation singleton)
-
-  duplicates <- requirePointBuild "duplicates" [Point 0 0, Point 1 0, Point (-0.0) 0, Point 1 0, Point 2 0]
-  assertEqual "deduplicated count" 3 (numVertices (buildTriangulation duplicates))
-  assertEqual
-    "stable duplicate mapping"
-    (primArrayFromList [0, 1, 0, 1, 2])
-    (buildInputVertices duplicates)
-  assertValid "duplicates" (buildTriangulation duplicates)
-
-  -- Deduplicating a signed zero is not the same claim as storing it canonically:
-  -- the first only needs '==', which already identifies the two, while anything
-  -- reading the bit pattern — a radix ordering, a byte-for-byte cross-check —
-  -- reads the sign bit and gets a coordinate below every other. Both ingest
-  -- paths canonicalize, and both are held to it here rather than to the weaker
-  -- statement that comparison happens to survive.
-  let bits point = (castDoubleToWord64 (pointX point), castDoubleToWord64 (pointY point))
-  signedZero <- requirePointBuild "signed zero" [Point (-0.0) (-0.0), Point 1 0, Point 0 1]
-  assertEqual "bulk load stores a canonical zero"
-    (0, 0) (bits (vertexPoint (buildTriangulation signedZero) (VertexId 0)))
-  incremental <-
-    requireRight "signed zero insert"
-      (insert (empty unitElementDefaults :: DelaunayTriangulation (Point)) (Point (-0.0) (-0.0)))
-  assertEqual "insertion stores a canonical zero"
-    (0, 0) (bits (vertexPoint (insertionTriangulation incremental) (VertexId 0)))
-
-  traverse_
-    assertBulkLine
-    [ ("vertical", [Point 0 4, Point 0 0, Point 0 3, Point 0 2, Point 0 1])
-    , ("horizontal reversed", reverse [Point x 7 | x <- [0 .. 8]])
-    , ("oblique scrambled", [Point 3 8, Point (-2) (-2), Point 1 4, Point (-1) 0, Point 2 6, Point 0 2])
-    , ("duplicate-bearing", [Point 2 5, Point 0 1, Point 1 3, Point 2 5, Point (-0.0) 1, Point 3 7])
-    ]
-  areaBase <- requirePointBuild "resident area before collinear batch" [Point 0 0, Point 4 0, Point 0 4]
-  areaExtended <-
-    requireRight
-      "collinear batch into resident area"
-      (insertMany (buildTriangulation areaBase) (V.fromList [Point 1 1, Point 2 2, Point 3 3]))
-  assertEqual "resident area collinear batch count" 6 (numVertices (buildTriangulation areaExtended))
-  assertValid "resident area collinear batch" (buildTriangulation areaExtended)
- where
-  assertBulkLine (label, points) = do
-    annotated <- requirePointBuild (label <> " annotated line") points
-    geometry <- requireRight (label <> " geometry line") (delaunayGeometry (V.fromList points))
-    batched <-
-      requireRight
-        (label <> " empty-base batch line")
-        (insertMany (empty unitElementDefaults) (V.fromList points))
-    let line = buildTriangulation annotated
-        batchLine = buildTriangulation batched
-        ascending = Set.toAscList (Set.fromList points)
-        expectedEdges = Set.fromList (zip ascending (drop 1 ascending))
-        uniqueCount = length ascending
-        stats = buildStats annotated
-    assertEqual (label <> " annotated edge set") expectedEdges (canonicalEdges line)
-    assertEqual (label <> " geometry edge set") expectedEdges (canonicalEdges geometry)
-    assertEqual (label <> " empty-base batch edge set") expectedEdges (canonicalEdges batchLine)
-    assertEqual (label <> " edge count") (max 0 (uniqueCount - 1)) (numUndirectedEdges line)
-    assertEqual (label <> " face count") 0 (numInnerFaces line)
-    assertEqual (label <> " spatial seed count") uniqueCount (statSpatialSeedPoints stats)
-    assertEqual (label <> " aggregate extension count") (max 0 (uniqueCount - 2)) (statLineExtensions stats)
-    assertValid (label <> " annotated line") line
-    assertValid (label <> " geometry line") geometry
-    assertValid (label <> " empty-base batch line") batchLine
-
-testPersistentLocalUpdates :: IO ()
-testPersistentLocalUpdates = do
-  base <- requirePointBuild "persistent base" (randomPoints 0x5eed 4095)
-  let triangulation = buildTriangulation base
-      query = Point 0.000_123 (-0.000_271)
-  inserted <- requireRight "persistent insert" (insert triangulation query)
-  assertEqual "persistent source remains unchanged" 4095 (numVertices triangulation)
-  assertEqual "persistent result appends one vertex" 4096 (numVertices (insertionTriangulation inserted))
-  assertValid "persistent insertion result" (insertionTriangulation inserted)
-
-  let payloads = V.fromList
-        [ SampleVertex (Point 0 0) 10
-        , SampleVertex (Point 1 0) 20
-        , SampleVertex (Point 0 1) 30
-        ]
-      defaults = ElementDefaults (0 :: Int) False ("face" :: String)
-  payloadBuild <- requireRight "payload base" (delaunay defaults payloads)
-  duplicate <- requireRight "payload-only replacement" (insert (buildTriangulation payloadBuild) (SampleVertex (Point 1 0) 99))
-  assertEqual "payload update disposition" AlreadyPresent (insertionDisposition duplicate)
-  assertEqual "payload replacement" 99 (sampleLabel (vertexData (insertionTriangulation duplicate) (VertexId 1)))
-
-testGenericPayloads :: IO ()
-testGenericPayloads = do
-  let defaults = ElementDefaults (7 :: Int) False ("new-face" :: String)
-      payloads = V.fromList
-        [ SampleVertex (Point 0 0) 1
-        , SampleVertex (Point 2 0) 2
-        , SampleVertex (Point 0 2) 3
-        , SampleVertex (Point 0.5 0.5) 4
-        ]
-  built <- requireRight "generic payload build" (delaunay defaults payloads)
-  let triangulation = buildTriangulation built
-  assertValid "generic payload build" triangulation
-  assertEqual "vertex payload" 4 (sampleLabel (vertexData triangulation (VertexId 3)))
-  forM_ (directedEdges triangulation) $ \edge -> assertEqual "directed default" 7 (directedEdgeData triangulation edge)
-  forM_ (undirectedEdges triangulation) $ \edge -> assertEqual "undirected default" False (undirectedEdgeData triangulation edge)
-  forM_ (allFaces triangulation) $ \face -> assertEqual "face default" "new-face" (faceData triangulation face)
-  (firstEdge, firstFace) <- case (directedEdges triangulation, innerFaces triangulation) of
-    (edge : _, face : _) -> pure (edge, face)
-    _ -> fail "generic payload build produced no inner topology"
-  let firstUndirected = asUndirected firstEdge
-      changed = setFaceData (setUndirectedEdgeData (setDirectedEdgeData triangulation firstEdge 42) firstUndirected True) firstFace "changed"
-  assertEqual "directed payload update" 42 (directedEdgeData changed firstEdge)
-  assertEqual "undirected payload update" True (undirectedEdgeData changed firstUndirected)
-  assertEqual "face payload update" "changed" (faceData changed firstFace)
-  -- Geometry owns the points, so a payload carrying a different position is not
-  -- a contradiction to be refused — it is a payload whose position nobody reads.
-  let moved = setVertexData triangulation (VertexId 0) (SampleVertex (Point 9 9) 0)
-  assertEqual "a payload position does not site a vertex"
-    (vertexPoint triangulation (VertexId 0)) (vertexPoint moved (VertexId 0))
-  assertEqual "the payload is stored as given"
-    (Point 9 9) (samplePosition (vertexData moved (VertexId 0)))
-
--- The payload layer over a fixed geometry is a product of four free components.
--- Each is a functor and each is checked as such; nothing in the product can
--- disturb the geometry underneath it.
-testPayloadMaps :: IO ()
-testPayloadMaps = do
-  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)
-      payloads = V.fromList
-        [ SampleVertex (Point 0 0) 1
-        , SampleVertex (Point 4 0) 2
-        , SampleVertex (Point 4 4) 3
-        , SampleVertex (Point 0 4) 4
-        , SampleVertex (Point 1 2) 5
-        ]
-  built <- requireRight "payload map build" (delaunay defaults payloads)
-  let plain = buildTriangulation built
-      -- Distinct payloads everywhere: a map that permuted its component would
-      -- be invisible against uniform defaults.
-      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))
-      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))
-      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))
-      endpoints ::
-        Triangulation mode vertex directed undirected face ->
-        [(VertexId, VertexId)]
-      endpoints t = [(origin t edge, destination t edge) | edge <- directedEdges t]
-
-  -- Identity. Equality on a triangulation compares geometry, topology,
-  -- constraint flags and element defaults as well as payloads, so this single
-  -- equation states that each map disturbs nothing but the component it names.
-  assertEqual "mapDirectedEdges identity" sample (mapDirectedEdges id sample)
-  assertEqual "mapUndirectedEdges identity" sample (mapUndirectedEdges id sample)
-  assertEqual "mapFaces identity" sample (mapFaces id sample)
-  assertEqual "mapVertices identity" sample (mapVertices id sample)
-
-  assertEqual "mapDirectedEdges composition"
-    (mapDirectedEdges ((* 2) . (+ 1)) sample)
-    (mapDirectedEdges (* 2) (mapDirectedEdges (+ 1) sample))
-  assertEqual "mapFaces composition"
-    (mapFaces (("<" <>) . (<> ">")) sample)
-    (mapFaces ("<" <>) (mapFaces (<> ">") sample))
-
-  -- Commutation with the accessors. Identity and composition are blind to the
-  -- indexing; this is the law that pins each payload to its own handle.
-  let directedMapped = mapDirectedEdges (+ 1) sample
-      undirectedMapped = mapUndirectedEdges ("<" <>) sample
-      facesMapped = mapFaces ("<" <>) sample
-  forM_ (directedEdges sample) $ \edge ->
-    assertEqual "mapDirectedEdges commutes with directedEdgeData"
-      (directedEdgeData sample edge + 1) (directedEdgeData directedMapped edge)
-  forM_ (undirectedEdges sample) $ \edge ->
-    assertEqual "mapUndirectedEdges commutes with undirectedEdgeData"
-      ("<" <> undirectedEdgeData sample edge) (undirectedEdgeData undirectedMapped edge)
-  forM_ (allFaces sample) $ \face ->
-    assertEqual "mapFaces commutes with faceData"
-      ("<" <> faceData sample face) (faceData facesMapped face)
-
-  -- The components are independent, and none of them is geometry.
-  assertEqual "face and directed maps commute"
-    (mapFaces ("<" <>) (mapDirectedEdges (+ 1) sample))
-    (mapDirectedEdges (+ 1) (mapFaces ("<" <>) sample))
-  assertEqual "mapFaces preserves topology" (endpoints sample) (endpoints facesMapped)
-  assertEqual "mapFaces preserves the face count" (numFaces sample) (numFaces facesMapped)
-  forM_ (vertices sample) $ \vertex ->
-    assertEqual "mapFaces preserves geometry" (vertexPoint sample vertex) (vertexPoint facesMapped vertex)
-
-  -- The element default is a payload and must travel with them: every element a
-  -- later insertion creates is handed the default, so a map that reindexed the
-  -- stored payloads and left the default behind would produce a triangulation
-  -- whose future elements disagree with its present ones. Every stored payload
-  -- here differs from the default, so carrying the wrong one is visible.
-  grownFaces <- insertionTriangulation <$> requireRight "insertion into mapped faces" (insert facesMapped (SampleVertex (Point 2 1) 6))
-  grownUndirected <- insertionTriangulation <$> requireRight "insertion into mapped undirected edges" (insert undirectedMapped (SampleVertex (Point 2 1) 6))
-  grownDirected <- insertionTriangulation <$> requireRight "insertion into mapped directed edges" (insert directedMapped (SampleVertex (Point 2 1) 6))
-  unless (numFaces grownFaces > numFaces facesMapped) $ fail "the insertion created no face"
-  unless ("<new-face" `elem` map (faceData grownFaces) (allFaces grownFaces)) $
-    fail ("new faces did not receive the mapped default: " <> show (map (faceData grownFaces) (allFaces grownFaces)))
-  unless ("<new-undirected" `elem` map (undirectedEdgeData grownUndirected) (undirectedEdges grownUndirected)) $
-    fail "new undirected edges did not receive the mapped default"
-  unless (8 `elem` map (directedEdgeData grownDirected) (directedEdges grownDirected)) $
-    fail "new directed edges did not receive the mapped default"
-
-  -- Vertices, the component that used to be special. The map is total, and the
-  -- one thing worth insisting on is that a function which does its worst to the
-  -- stored position still cannot move a vertex.
-  let relabelled = mapVertices (\v -> v{sampleLabel = sampleLabel v * 10}) sample
-      collapsed = mapVertices (\v -> v{samplePosition = Point 9 9}) sample
-  forM_ (vertices sample) $ \vertex -> do
-    assertEqual "mapVertices commutes with vertexData"
-      (sampleLabel (vertexData sample vertex) * 10) (sampleLabel (vertexData relabelled vertex))
-    assertEqual "mapVertices preserves geometry" (vertexPoint sample vertex) (vertexPoint relabelled vertex)
-    assertEqual "a payload map cannot move a vertex"
-      (vertexPoint sample vertex) (vertexPoint collapsed vertex)
-  assertEqual "mapVertices composes"
-    (mapVertices (\v -> v{sampleLabel = sampleLabel v + 1}) relabelled)
-    (mapVertices (\v -> v{sampleLabel = sampleLabel v * 10 + 1}) sample)
-
-  -- The sharpest statement of freedom available: the image type has no
-  -- 'HasPosition' instance at all. This does not typecheck under a vertex
-  -- component that geometry reads through.
-  let projected = mapVertices sampleLabel sample
-  forM_ (vertices sample) $ \vertex -> do
-    assertEqual "a vertex payload need not have a position"
-      (sampleLabel (vertexData sample vertex)) (vertexData projected vertex)
-    assertEqual "projecting payloads away preserves geometry"
-      (vertexPoint sample vertex) (vertexPoint projected vertex)
-
--- Geometry publication is payload forgetting, not a dense payload map.  The
--- polymorphic entrance accepts all four non-unit payload planes and publishes
--- zero materialized pages while retaining the exact structural planes.
-testGeometryOnlyPublication :: IO ()
-testGeometryOnlyPublication = do
-  let defaults = ElementDefaults (17 :: Int) False ("source-face" :: String)
-      payloads =
-        V.fromList
-          [ SampleVertex (Point 0 0) 11
-          , SampleVertex (Point 4 0) 22
-          , SampleVertex (Point 4 4) 33
-          , SampleVertex (Point 0 4) 44
-          , SampleVertex (Point 1 2) 55
-          ]
-  built <- requireRight "geometry-only payload source" (delaunay defaults payloads)
-  let source = buildTriangulation built
-      published = Internal.geometryOnlyPublication source
-      republished = Internal.geometryOnlyPublication published
-      structuralPlanes triangulation =
-        ( Internal.triPointX triangulation
-        , Internal.triPointY triangulation
-        , Internal.triVertexOut triangulation
-        , Internal.triHalfTopology triangulation
-        , Internal.triFaceEdge triangulation
-        , Internal.triConstraint triangulation
-        , Internal.triConstraintCount triangulation
-        , Internal.triConstraintEdges triangulation
-        )
-      payloadPageCounts triangulation =
-        ( boxedMaterializedPageCount (Internal.triVertexData triangulation)
-        , boxedMaterializedPageCount (Internal.triDirectedData triangulation)
-        , boxedMaterializedPageCount (Internal.triUndirectedData triangulation)
-        , boxedMaterializedPageCount (Internal.triFaceData triangulation)
-        )
-  unless (sumPayloadPages (payloadPageCounts source) > 0) $
-    fail "geometry-only payload fixture did not materialize a source payload page"
-  assertEqual
-    "geometry-only publication preserves structural planes"
-    (structuralPlanes source)
-    (structuralPlanes published)
-  assertEqual
-    "geometry-only publication replaces every payload plane with a zero-page store"
-    (0, 0, 0, 0)
-    (payloadPageCounts published)
-  assertEqual
-    "geometry-only publication installs unit element defaults"
-    unitElementDefaults
-    (Internal.triElementDefaults published)
-  assertEqual
-    "geometry-only publication prepares the exact seam frontier"
-    (Internal.prepareSeamFrontierIndex source)
-    (Internal.triSeamFrontier published)
-  assertEqual
-    "geometry-only publication preserves an existing seam frontier"
-    (Internal.triSeamFrontier published)
-    (Internal.triSeamFrontier republished)
-  assertValid "geometry-only publication" published
- where
-  sumPayloadPages (vertexPages, directedPages, undirectedPages, facePages) =
-    vertexPages + directedPages + undirectedPages + facePages
-testPayloadTraversals :: IO ()
-testPayloadTraversals = do
-  let defaults = ElementDefaults (7 :: Int) ("new-undirected" :: String) ("new-face" :: String)
-      payloads = V.fromList
-        [ SampleVertex (Point 0 0) 1
-        , SampleVertex (Point 4 0) 2
-        , SampleVertex (Point 4 4) 3
-        , SampleVertex (Point 0 4) 4
-        , SampleVertex (Point 1 2) 5
-        ]
-  built <- requireRight "payload traversal build" (delaunay defaults payloads)
-  let plain = buildTriangulation built
-      withDirected = List.foldl' (\t (label, edge) -> setDirectedEdgeData t edge label) plain (zip [100 ..] (directedEdges plain))
-      withUndirected = List.foldl' (\t (label, edge) -> setUndirectedEdgeData t edge ("u-" <> show label)) withDirected (zip [(0 :: Int) ..] (undirectedEdges withDirected))
-      sample = List.foldl' (\t (label, face) -> setFaceData t face ("f-" <> show label)) withUndirected (zip [(0 :: Int) ..] (allFaces withUndirected))
-
-  -- 'overPayloads' is the traversal under 'Identity', so this is the traversal
-  -- identity law. It also says that rebuilding a payload store from its own
-  -- contents is not observable, which is the part a paged store could get
-  -- wrong: the traversal materializes pages the map would have left absent.
-  assertEqual "vertexPayloads identity" sample (overPayloads vertexPayloads id sample)
-  assertEqual "directedPayloads identity" sample (overPayloads directedPayloads id sample)
-  assertEqual "undirectedPayloads identity" sample (overPayloads undirectedPayloads id sample)
-  assertEqual "facePayloads identity" sample (overPayloads facePayloads id sample)
-
-  -- Each traversal and its named map are one function. The effectful
-  -- generalization is not allowed a second opinion about what relabeling means.
-  assertEqual "vertexPayloads agrees with mapVertices"
-    (mapVertices sampleLabel sample) (overPayloads vertexPayloads sampleLabel sample)
-  assertEqual "directedPayloads agrees with mapDirectedEdges"
-    (mapDirectedEdges (* 2) sample) (overPayloads directedPayloads (* 2) sample)
-  assertEqual "undirectedPayloads agrees with mapUndirectedEdges"
-    (mapUndirectedEdges ("<" <>) sample) (overPayloads undirectedPayloads ("<" <>) sample)
-  assertEqual "facePayloads agrees with mapFaces"
-    (mapFaces ("<" <>) sample) (overPayloads facePayloads ("<" <>) sample)
-
-  assertEqual "facePayloads composes"
-    (overPayloads facePayloads (("<" <>) . (<> ">")) sample)
-    (overPayloads facePayloads ("<" <>) (overPayloads facePayloads (<> ">") sample))
-
-  -- The class instances range over the face payload, being the last parameter.
-  assertEqual "fmap is the face payload map" (mapFaces ("<" <>) sample) (fmap ("<" <>) sample)
-  assertEqual "traverse is facePayloads"
-    (Just (overPayloads facePayloads ("<" <>) sample))
-    (traverse (Just . ("<" <>)) sample)
-  assertEqual "the Foldable instance is the face traversal"
-    (payloadList facePayloads sample) (foldr (:) [] sample)
-
-  -- Visit order, and the element default's place in it. A fold that skipped
-  -- the default would report the triangulation as holding one fewer face
-  -- payload than it holds.
-  assertEqual "vertexPayloads visits the vertices in order"
-    (map (vertexData sample) (vertices sample))
-    (payloadList vertexPayloads sample)
-  assertEqual "facePayloads visits the faces and then the default"
-    (map (faceData sample) (allFaces sample) <> [defaultFaceData (Internal.triElementDefaults sample)])
-    (payloadList facePayloads sample)
-
-  -- The point of the exercise: relabeling under an effect, with a refusal
-  -- reaching the caller instead of a half-relabelled triangulation.
-  let refuseAtThree :: SampleVertex -> Either String Int
-      refuseAtThree v = if sampleLabel v == 3 then Left "vertex three refuses" else Right (sampleLabel v * 10)
-      keepLabel :: SampleVertex -> Either String Int
-      keepLabel = Right . sampleLabel
-      decorate :: String -> Either String String
-      decorate = Right . ("<" <>)
-  assertEqual "an effectful relabel short-circuits"
-    (Left "vertex three refuses") (vertexPayloads refuseAtThree sample)
-  relabelled <- requireRight "effectful relabel" (vertexPayloads keepLabel sample)
-  assertEqual "a successful effectful relabel is the pure one"
-    (mapVertices sampleLabel sample) relabelled
-
-  -- The default travels through the traversal, and travels exactly once: the
-  -- store's fill and the element defaults are written from a single visit, so
-  -- an element created afterwards inherits precisely what the traversal made.
-  traversedFaces <- requireRight "effectful face relabel" (facePayloads decorate sample)
-  grown <- insertionTriangulation <$> requireRight "insertion after traversal" (insert traversedFaces (SampleVertex (Point 2 1) 6))
-  unless (numFaces grown > numFaces traversedFaces) $ fail "the insertion created no face"
-  unless ("<new-face" `elem` map (faceData grown) (allFaces grown)) $
-    fail ("new faces did not receive the traversed default: " <> show (map (faceData grown) (allFaces grown)))
-
--- | The in-circle predicate is the orientation of the four points lifted to
--- the paraboloid @z = x² + y²@. The referent is that 4×4 determinant evaluated
--- exactly over 'Rational' — deliberately not the translated 3×3 the instances
--- expand, which would only be the implementation checking its own algebra.
-testParaboloidLift :: IO ()
-testParaboloidLift = do
-  let lifted :: Point -> (Rational, Rational, Rational)
-      lifted (Point x y) =
-        let (rx, ry) = (toRational x, toRational y) in (rx, ry, rx * rx + ry * ry)
-      minor3
-        :: (Rational, Rational, Rational)
-        -> (Rational, Rational, Rational)
-        -> (Rational, Rational, Rational)
-        -> Rational
-      minor3 (a1, a2, a3) (b1, b2, b3) (c1, c2, c3) =
-        a1 * (b2 * c3 - b3 * c2) - a2 * (b1 * c3 - b3 * c1) + a3 * (b1 * c2 - b2 * c1)
-      -- Laplace expansion of the lifted determinant along its column of ones.
-      liftedOrientation
-        :: (Rational, Rational, Rational)
-        -> (Rational, Rational, Rational)
-        -> (Rational, Rational, Rational)
-        -> (Rational, Rational, Rational)
-        -> Rational
-      liftedOrientation a b c d =
-        negate (minor3 b c d) + minor3 a c d - minor3 a b d + minor3 a b c
-      predicted :: Point -> Point -> Point -> Point -> Ordering
-      predicted a b c d = compare (liftedOrientation (lifted a) (lifted b) (lifted c) (lifted d)) 0
-      measured :: Point -> Point -> Point -> Point -> Ordering
-      measured (Point ax ay) (Point bx by) (Point cx cy) (Point dx dy) =
-        Admitted.inCircleCoordinates ax ay bx by cx cy dx dy
-      quadruples :: [Point] -> [(Point, Point, Point, Point)]
-      quadruples (a : b : c : d : rest) = (a, b, c, d) : quadruples rest
-      quadruples _ = []
-
-  forM_ (quadruples (randomPoints 0x51ca_0b17 1600)) $ \(a, b, c, d) ->
-    assertEqual ("in-circle is the lifted orientation at " <> show (a, b, c, d))
-      (predicted a b c d) (measured a b c d)
-
-  -- Exactly cocircular quadruples, which are exactly the ones the floating
-  -- filter must decline to answer. Every Pythagorean point of the radius-5
-  -- circle is representable without rounding, so 'EQ' here is a fact about the
-  -- geometry rather than about the arithmetic.
-  let ring =
-        [ Point x y
-        | (x, y) <-
-            [ (5, 0), (0, 5), (-5, 0), (0, -5)
-            , (3, 4), (4, 3), (-3, 4), (-4, 3)
-            , (3, -4), (4, -3), (-3, -4), (-4, -3)
-            ]
-        ] :: [Point]
-      indexed = zip [(0 :: Int) ..] ring
-      cocircular =
-        [ (a, b, c, d)
-        | (i, a) <- indexed, (j, b) <- indexed, j > i
-        , (k, c) <- indexed, k > j, (l, d) <- indexed, l > k
-        ]
-  unless (length cocircular == 495) $ fail ("expected 495 cocircular quadruples, got " <> show (length cocircular))
-  forM_ cocircular $ \(a, b, c, d) -> do
-    assertEqual ("cocircular points are cocircular at " <> show (a, b, c, d)) EQ (measured a b c d)
-    assertEqual ("the lift agrees on cocircularity at " <> show (a, b, c, d)) EQ (predicted a b c d)
-
-  -- The geometry the sign means, stated once against a circle anyone can read.
-  let (a, b, c) = (Point 1 0, Point 0 1, Point (-1) 0) :: (Point, Point, Point)
-  assertEqual "the centre is inside the circle" GT (measured a b c (Point 0 0))
-  assertEqual "the antipode is on the circle" EQ (measured a b c (Point 0 (-1)))
-  assertEqual "a distant point is outside" LT (measured a b c (Point 2 2))
-
--- | A payload labels an element, and an element is its geometry. Every rewrite
--- an insertion performs — an edge split, a face split, a Lawson flip — hands
--- some slot a different element to hold, and the label the displaced one
--- carried does not describe what took its place.
---
--- So: label every element of a triangulation by its own key, insert a point,
--- and demand that an element whose key survives still carries exactly the label
--- it was given while every element whose key is new carries the default and
--- nothing else. Both directions are checked; either alone is satisfiable by a
--- store that throws everything away.
---
--- The flip is the load-bearing case. Legalization is confluent, so which flips
--- fire and in what order is not observable in the topology that comes out. A
--- payload that rode through a flip would make it observable in the payload
--- plane, and a triangulation that is a normal form in one component and a
--- history in another is not a normal form.
-testRewritePayloadIdentity :: IO ()
-testRewritePayloadIdentity = do
-  let defaults = ElementDefaults (0 :: Int) (0 :: Int) (0 :: Int)
-      target = Point 0.001_37 (-0.002_11)
-      corpus = randomPoints 0x1a2b3c4d 512
-  built <- requireRight "rewrite identity base" (delaunay defaults (V.fromList corpus))
-  let base = buildTriangulation built
-      directedTable = labelTable (map (directedKeyOf base) (directedEdges base))
-      undirectedTable = labelTable (map (undirectedKeyOf base) (undirectedEdges base))
-      faceTable = labelTable (map (faceKeyOf base) (innerFaces base))
-
-  -- Keys identify elements only if they are unique, so the premise is checked
-  -- rather than assumed: a collapsed table would silently weaken everything
-  -- below it into a test of nothing.
-  assertEqual "directed keys are unique" (length (directedEdges base)) (Map.size directedTable)
-  assertEqual "undirected keys are unique" (length (undirectedEdges base)) (Map.size undirectedTable)
-  assertEqual "face keys are unique" (length (innerFaces base)) (Map.size faceTable)
-
-  let withDirected =
-        List.foldl' (\t e -> setDirectedEdgeData t e (directedTable Map.! directedKeyOf base e)) base (directedEdges base)
-      withUndirected =
-        List.foldl' (\t e -> setUndirectedEdgeData t e (undirectedTable Map.! undirectedKeyOf base e)) withDirected (undirectedEdges base)
-      labelled =
-        List.foldl' (\t f -> setFaceData t f (faceTable Map.! faceKeyOf base f)) withUndirected (innerFaces base)
-  inserted <- requireRight "rewrite identity insert" (insert labelled target)
-  let result = insertionTriangulation inserted
-  assertValid "rewrite identity result" result
-  assertEqual "the point was genuinely inserted" Inserted (insertionDisposition inserted)
-
-  -- Both directions, on every plane. A surviving element keeps exactly its
-  -- label; everything else holds the default and nothing else.
-  assertPayloadSurvival "directed" directedTable
-    [(directedKeyOf result e, directedEdgeData result e) | e <- directedEdges result]
-  assertPayloadSurvival "undirected" undirectedTable
-    [(undirectedKeyOf result e, undirectedEdgeData result e) | e <- undirectedEdges result]
-  assertPayloadSurvival "face" faceTable
-    [(faceKeyOf result f, faceData result f) | f <- innerFaces result]
-
-  -- The flip signature. Every flip after an insertion joins the new vertex to
-  -- the far corner of a cavity quad, so a new edge is not evidence of one — a
-  -- split produces those too. A DESTROYED edge is: splitting a face destroys
-  -- nothing and adds exactly three edges and two faces, which is asserted here
-  -- so that the arithmetic holds, and under it every base key that is gone from
-  -- the result was flipped away. Without this the paragraphs above are a claim
-  -- about splits alone.
-  assertEqual "the point landed strictly inside a face"
-    (numUndirectedEdges base + 3, numFaces base + 2)
-    (numUndirectedEdges result, numFaces result)
-  let surviving = Set.fromList (map (undirectedKeyOf result) (undirectedEdges result))
-      flippedAway = filter (`Set.notMember` surviving) (Map.keys undirectedTable)
-  when (null flippedAway) (fail "the insertion caused no flip, so the flip case went unchecked")
-
-  -- The same claim across one transaction that both retires and creates.
-  -- Removal swap-compacts, which leaves a retired element's payload sitting in
-  -- the slot it vacated; the inserts that follow are handed those slots back.
-  -- Nothing else in the suite makes an allocation reissue a used slot.
-  let doomed = take 60 corpus
-      arrivals = randomPoints 0x5f3a19c2 60
-  (_, edited, _) <-
-    requireRight "rewrite identity session" $
-      withSession labelled (length arrivals) $ do
-        mapM_
-          (\point -> removeAt point >>= maybe (refuse (RemovalVertexOutOfRange (VertexId 0) 0)) (const (pure ())))
-          doomed
-        mapM_ insertVertex arrivals
-  assertValid "rewrite identity session" edited
-  assertPayloadSurvival "session directed" directedTable
-    [(directedKeyOf edited e, directedEdgeData edited e) | e <- directedEdges edited]
-  assertPayloadSurvival "session undirected" undirectedTable
-    [(undirectedKeyOf edited e, undirectedEdgeData edited e) | e <- undirectedEdges edited]
-  assertPayloadSurvival "session face" faceTable
-    [(faceKeyOf edited f, faceData edited f) | f <- innerFaces edited]
-
-directedKeyOf
-  :: Triangulation mode vertex directed undirected face
-  -> DirectedEdgeId
-  -> (Point, Point)
-directedKeyOf triangulation edge =
-  ( vertexPoint triangulation (origin triangulation edge)
-  , vertexPoint triangulation (destination triangulation edge)
-  )
-
-undirectedKeyOf
-  :: Triangulation mode vertex directed undirected face
-  -> UndirectedEdgeId
-  -> (Point, Point)
-undirectedKeyOf triangulation edge =
-  case undirectedEndpoints triangulation edge of
-    (from, to) ->
-      let (left, right) = (vertexPoint triangulation from, vertexPoint triangulation to)
-       in if left <= right then (left, right) else (right, left)
-
-faceKeyOf
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> [Point]
-faceKeyOf triangulation face = sort (map (vertexPoint triangulation) (faceVertices triangulation face))
-
-labelTable :: Ord key => [key] -> Map.Map key Int
-labelTable keys = Map.fromList (zip keys [1 ..])
-
--- | Every element carries the label its key was given, or the default if its
--- key is new. Both counts are asserted too: a store that kept everything and a
--- store that kept nothing each satisfy one half of this on its own.
-assertPayloadSurvival :: (Ord key, Show key) => String -> Map.Map key Int -> [(key, Int)] -> IO ()
-assertPayloadSurvival plane labels elements = do
-  let kept = length (filter ((`Map.member` labels) . fst) elements)
-  when (kept == 0) (fail (plane <> ": the insertion perturbed every element"))
-  when (kept == length elements) (fail (plane <> ": the insertion perturbed no element"))
-  forM_ elements $ \(key, payload) ->
-    assertEqual (plane <> " label at " <> show key) (Map.findWithDefault 0 key labels) payload
-
-testPointLocationAndHints :: IO ()
-testPointLocationAndHints = do
-  built <- requirePointBuild "location" (randomPoints 0x1234_5678 1200)
-  let triangulation = buildTriangulation built
-  queries <- traverse (requireQueryPoint "location query") (take 250 (randomPoints 0xdead_beef 250))
-  let
-      baseline = sum [locationWalkSteps stats | query <- queries, let (_, stats) = locatePointWithHint triangulation Nothing query]
-  hierarchy <- requireRight "hierarchy build" (buildHierarchyHint 16 triangulation)
-  let hinted = sum
-        [ locationWalkSteps stats
-        | query <- queries
-        , let hint = hierarchyHint hierarchy query
-              (_, stats) = locatePointWithHint triangulation hint query
-        ]
-  unless (hinted <= baseline) $
-    fail ("hierarchy hint increased aggregate walking: " <> show (baseline, hinted))
-  staleHintQuery <- requireQueryPoint "stale hint query" (Point 0.125 (-0.375))
-  let staleVertexHint = VertexHint (VertexId (fromIntegral (numVertices triangulation)))
-  assertEqual
-    "stale vertex hint falls back to the canonical start face"
-    (locatePointWithHint triangulation Nothing staleHintQuery)
-    (locatePointWithHint triangulation (Just staleVertexHint) staleHintQuery)
-  forM_ (vertices triangulation) $ \vertex -> do
-    vertexQuery <- requireQueryPoint "vertex lookup" (vertexPoint triangulation vertex)
-    assertEqual "vertex lookup" (OnVertex vertex) (locatePoint triangulation vertexQuery)
-  let insertedPoint = Point 0.1234567 (-0.2345678)
-  inserted <- requireRight "hierarchy incremental source" (insert triangulation insertedPoint)
-  let updatedTriangulation = insertionTriangulation inserted
-  updatedHierarchy <-
-    requireRight
-      "hierarchy incremental update"
-      ( updateHierarchyAfterInsertion
-          hierarchy
-          insertedPoint
-          (insertionVertex inserted)
-          (insertionDisposition inserted)
-      )
-  rebuiltHierarchy <- requireRight "hierarchy reference rebuild" (buildHierarchyHint 16 updatedTriangulation)
-  assertEqual "incremental hierarchy equals canonical rebuild" rebuiltHierarchy updatedHierarchy
-
--- The hierarchy replaces its level-to-base correspondence with the arithmetic
--- claim that level-local handle @j@ names base vertex @j * branch@. Query each
--- sampled vertex with its own position: the descent must return that very
--- vertex, at every branch factor and along the whole of level 0. A stride the
--- construction does not actually obey shows up here as a named mismatch.
-testHierarchyNestingLaw :: IO ()
-testHierarchyNestingLaw = do
-  built <- requirePointBuild "hierarchy nesting" (randomPoints 0x0f1e_2d3c 900)
-  let triangulation = buildTriangulation built
-  forM_ [2, 3, 16] $ \branch -> do
-    hierarchy <- requireRight ("hierarchy build at branch " <> show branch) (buildHierarchyHint branch triangulation)
-    let sampled = [0, branch .. numVertices triangulation - 1]
-    forM_ sampled $ \index -> do
-      let vertex = VertexId (fromIntegral index)
-      vertexQuery <- requireQueryPoint "sampled hierarchy vertex" (vertexPoint triangulation vertex)
-      assertEqual
-        ("branch " <> show branch <> " descent onto sampled vertex " <> show index)
-        (Just (VertexHint vertex))
-        (hierarchyHint hierarchy vertexQuery)
-
-testSibsonInterpolation :: IO ()
-testSibsonInterpolation = do
-  built <- requirePointBuild "sibson" (gridPoints 9 9)
-  let triangulation = buildTriangulation built
-      queryPoint = Point 3.25 4.4
-      linear vertex = let Point x y = vertexPoint triangulation vertex in 2 * x - 3 * y + 5
-      expected = let Point x y = queryPoint in 2 * x - 3 * y + 5
-  query <- requireQueryPoint "Sibson query" queryPoint
-  workspace <- stToIO (newNaturalNeighborWorkspace triangulation)
-  result <- stToIO (naturalNeighborWeights workspace Nothing query)
-  let weights = naturalNeighborValues result
-  unless (V.length weights >= 3) $ fail "Sibson query did not discover a natural-neighbor cavity"
-  assertNear "Sibson partition" 1.0e-11 1 (V.sum (V.map snd weights))
-  unless (V.all ((>= (-1.0e-12)) . snd) weights) $ fail "Sibson produced a negative weight"
-  (folded, _, foldedStats) <- stToIO (foldNaturalNeighborWeights
-    (\total vertex weight -> total + weight * linear vertex)
-    0
-    workspace
-    Nothing
-    query)
-  assertNear "allocation-free Sibson fold" 2.0e-9 expected folded
-  assertEqual "Sibson fold neighbor count" (V.length weights) (interpolationNaturalNeighbors foldedStats)
-  (interpolated, _) <- stToIO (interpolateNaturalNeighbor linear workspace Nothing query)
-  case interpolated of
-    Nothing -> fail "Sibson interpolation rejected an interior query"
-    Just value -> assertNear "Sibson affine precision" 2.0e-9 expected value
-  let gradients = estimateGradients linear triangulation
-  V.forM_ gradients $ \(gx, gy) -> do
-    assertNear "planar gradient x" 1.0e-9 2 gx
-    assertNear "planar gradient y" 1.0e-9 (-3) gy
-  let gradient (VertexId raw) = gradients V.! fromIntegral raw
-  (gradientValue, _) <- stToIO (interpolateNaturalNeighborGradient linear gradient 0.5 workspace Nothing query)
-  case gradientValue of
-    Nothing -> fail "gradient natural-neighbor interpolation rejected an interior query"
-    Just value -> assertNear "gradient affine precision" 2.0e-9 expected value
-  unless (workspaceBytes workspace > 0) $ fail "Sibson workspace byte accounting is empty"
-
-testVoronoiDual :: IO ()
-testVoronoiDual = do
-  built <- requirePointBuild "voronoi" [Point 0 0, Point 2 0, Point 0 2, Point 2 2, Point 1 1]
-  let triangulation = buildTriangulation built
-  assertEqual "Voronoi face count" (numVertices triangulation) (length (voronoiFaces triangulation))
-  assertEqual "directed dual edge count" (numDirectedEdges triangulation) (length (directedVoronoiEdges triangulation))
-  assertEqual "undirected dual edge count" (numUndirectedEdges triangulation) (length (undirectedVoronoiEdges triangulation))
-  forM_ (directedVoronoiEdges triangulation) $ \edge -> do
-    assertEqual "dual double reversal" edge (reverseVoronoiEdge (reverseVoronoiEdge edge))
-    assertEqual "dual next/previous" edge (voronoiPrevious triangulation (voronoiNext triangulation edge))
-    assertEqual
-      "dual face/site"
-      (origin triangulation (asDelaunayDirectedEdge edge))
-      (voronoiFaceSite (voronoiIncidentFace triangulation edge))
-    case voronoiEdgeGeometry triangulation edge of
-      Nothing -> fail "valid dual edge has no geometry"
-      fixedGeometry@(Just _) -> do
-        owning <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation edge)
-        assertEqual "fixed/owning dual geometry" fixedGeometry (VoronoiDynamic.voronoiEdgeGeometryH owning)
-  case directedVoronoiEdges triangulation of
-    [] -> fail "Voronoi test produced no directed dual edge"
-    first : _ -> do
-      handle <- requireJust "dynamic Voronoi edge" (VoronoiDynamic.directedVoronoiEdgeHandle triangulation first)
-      assertEqual "dynamic dual fix" first (VoronoiDynamic.fixDirectedVoronoiEdge handle)
-      assertEqual "dynamic dual reversal" first (VoronoiDynamic.fixDirectedVoronoiEdge (VoronoiDynamic.voronoiEdgeReverseH (VoronoiDynamic.voronoiEdgeReverseH handle)))
-      assertEqual "dynamic dual/primal conversion" (asDelaunayDirectedEdge first) (Dynamic.fixDirectedEdge (VoronoiDynamic.voronoiEdgeAsDelaunayH handle))
-      let dualFace = VoronoiDynamic.voronoiEdgeFaceH handle
-      assertEqual "dynamic dual face site" (origin triangulation (asDelaunayDirectedEdge first)) (Dynamic.fixVertex (VoronoiDynamic.voronoiFaceSiteH dualFace))
-      let source = VoronoiDynamic.voronoiEdgeFromH handle
-      case VoronoiDynamic.voronoiVertexAsDelaunayFaceH source of
-        Just inner -> case VoronoiDynamic.voronoiVertexPositionH source of
-          Nothing -> fail "inner dynamic Voronoi vertex has no position"
-          Just voronoiPosition -> assertEqual "inner dynamic Voronoi position" (Dynamic.innerFaceCircumcenter inner) (Just voronoiPosition)
-        Nothing -> unless (isJust (VoronoiDynamic.voronoiVertexAsOuterEdgeH source)) $
-          fail "outer dynamic Voronoi vertex has no defining edge"
-
-testRemoval :: IO ()
-testRemoval = do
-  built <- requirePointBuild "removal" [Point 0 0, Point 3 0, Point 3 3, Point 0 3, Point 1.5 1.5]
-  let triangulation = buildTriangulation built
-  assertEqual
-    "out-of-range removal obstruction"
-    (Left (RemovalVertexOutOfRange (VertexId 5) 5))
-    (void (removeVertex triangulation (VertexId 5)))
-  removedInterior <- requireRight "interior removal" (removeVertex triangulation (VertexId 4))
-  assertEqual "interior removed point" (Point 1.5 1.5) (removalOutcomePoint (removalOutcome removedInterior))
-  assertEqual "interior removal count" 4 (numVertices (removalTriangulation removedInterior))
-  assertValid "interior removal" (removalTriangulation removedInterior)
-  let beforeHullRemoval = removalTriangulation removedInterior
-      previousLastVertex = VertexId (fromIntegral (numVertices beforeHullRemoval - 1))
-      swappedPoint = vertexPoint beforeHullRemoval previousLastVertex
-      swappedData = vertexData beforeHullRemoval previousLastVertex
-  removedHull <- requireRight "hull removal" (removeVertex beforeHullRemoval (VertexId 0))
-  assertEqual "hull removal count" 3 (numVertices (removalTriangulation removedHull))
-  case removalOutcomeSwap (removalOutcome removedHull) of
-    Nothing -> fail "hull removal omitted the swap-compacted vertex handle"
-    Just (swappedIn, swappedInPoint) -> do
-      assertEqual "hull swapped-in handle" (VertexId 0) swappedIn
-      assertEqual "hull swapped-in point" swappedPoint (vertexPoint (removalTriangulation removedHull) swappedIn)
-      assertEqual "hull swapped-in payload" swappedData (vertexData (removalTriangulation removedHull) swappedIn)
-      -- The reported position must be the one the arena now holds, bit for
-      -- bit: a caller seeding a search from it is seeding from the mesh.
-      assertEqual "hull swapped-in reported position" swappedPoint swappedInPoint
-  assertValid "hull removal" (removalTriangulation removedHull)
-
-  let afterHullRemoval = removalTriangulation removedHull
-      lastVertex = VertexId (fromIntegral (numVertices afterHullRemoval - 1))
-  removedLast <- requireRight "last-vertex removal" (removeVertex afterHullRemoval lastVertex)
-  assertEqual
-    "removing the last vertex relocates nothing"
-    Nothing
-    (removalOutcomeSwap (removalOutcome removedLast))
-  assertEqual "last-vertex removal count" 2 (numVertices (removalTriangulation removedLast))
-  assertValid "last-vertex removal" (removalTriangulation removedLast)
-
-  lineBuild <- requirePointBuild "line removal" [Point 0 0, Point 1 0, Point 2 0, Point 3 0]
-  lineMiddle <- requireRight "line middle removal" (removeVertex (buildTriangulation lineBuild) (VertexId 1))
-  assertEqual "line middle count" 3 (numVertices (removalTriangulation lineMiddle))
-  assertValid "line middle removal" (removalTriangulation lineMiddle)
-
-  degreeThreeBuild <-
-    requirePointBuild
-      "degree-three removal"
-      [Point 0 0, Point 4 0, Point 0 4, Point 1 1]
-  let degreeThreeMapping = buildInputVertices degreeThreeBuild
-  case if 3 < sizeofPrimArray degreeThreeMapping
-        then Just (VertexId (indexPrimArray degreeThreeMapping 3))
-        else Nothing of
-    Nothing -> fail "degree-three removal input mapping omitted the interior vertex"
-    Just centerVertex -> do
-      degreeThree <-
-        requireRight
-          "degree-three interior removal"
-          (removeVertex (buildTriangulation degreeThreeBuild) centerVertex)
-      assertEqual "degree-three removal count" 3 (numVertices (removalTriangulation degreeThree))
-      assertValid "degree-three interior removal" (removalTriangulation degreeThree)
-
-  -- A removal hands its vertex's whole ring to edge/face cleanup, so a
-  -- high-degree star is the only shape that exercises the ordered-set path
-  -- there; an ordinary mesh keeps degrees near six and never leaves the
-  -- insertion sort. Radii are jittered so no four rim points are cocircular.
-  let rimCount = 48
-      rimPoint index =
-        let angle = 2 * pi * fromIntegral index / fromIntegral rimCount
-            radius = 1 + 0.001 * fromIntegral (index `mod` 7)
-         in Point (radius * cos angle) (radius * sin angle)
-  starBuild <-
-    requirePointBuild
-      "high-degree removal"
-      (Point 0 0 : map rimPoint [0 .. rimCount - 1])
-  let starTriangulation = buildTriangulation starBuild
-      starCentre = VertexId (indexPrimArray (buildInputVertices starBuild) 0)
-      centreDegree =
-        length
-          [ ()
-          | edge <- undirectedEdges starTriangulation
-          , let (from, to) = undirectedEndpoints starTriangulation edge
-          , from == starCentre || to == starCentre
-          ]
-  assertEqual "high-degree centre ring" rimCount centreDegree
-  starRemoved <-
-    requireRight "high-degree interior removal" (removeVertex starTriangulation starCentre)
-  assertEqual "high-degree removal count" rimCount (numVertices (removalTriangulation starRemoved))
-  assertEqual
-    "high-degree removed point"
-    (Point 0 0)
-    (removalOutcomePoint (removalOutcome starRemoved))
-  assertValid "high-degree interior removal" (removalTriangulation starRemoved)
-
-  -- A transaction that refuses publishes nothing: the refusal is the whole
-  -- answer, so no half-remeshed arena can reach a caller as a triangulation.
-  let sessionRefusal = RemovalVertexOutOfRange (VertexId 99) 5
-  assertEqual
-    "a refused session publishes nothing"
-    (Left sessionRefusal)
-    (void (withSession triangulation 1 (refuse sessionRefusal :: Session s (Point) () () () ())))
-
-  -- Refusal short-circuits: an edit after it never runs, so the mesh the
-  -- transaction abandoned is the mesh it was handed.
-  assertEqual
-    "a refusal abandons the edits behind it"
-    (Left sessionRefusal)
-    ( void
-        ( withSession
-            triangulation
-            1
-            ( ( do
-                  _ <- removeAt (Point 0 0)
-                  _ <- refuse sessionRefusal
-                  removeAt (Point 1 1)
-              ) ::
-                Session s (Point) () () () (Maybe (RemovalOutcome (Point)))
-            )
-        )
-    )
-
-  -- The two point-keyed entries must publish one story. A handle-keyed removal
-  -- locates nothing, while the coordinate-keyed route resolves the same site
-  -- through the exact derived identity section rather than a topological walk.
-  -- Both therefore charge no location steps and retire the same vertex.
-  handleRemoval <- requireRight "handle removal stats" (removeVertex triangulation (VertexId 4))
-  assertEqual
-    "a handle-keyed removal locates nothing"
-    0
-    (statLocationWalkSteps (removalStats handleRemoval))
-  locatedRemoval <-
-    requireRight "point removal stats" (locateAndRemove triangulation (Point 1.5 1.5))
-  pointRemoval <- requireJust "point removal located a vertex" locatedRemoval
-  assertEqual
-    "a point-keyed removal performs no topological location walk"
-    0
-    (statLocationWalkSteps (removalStats pointRemoval))
-  assertEqual
-    "point-keyed and handle-keyed removal publish the same mesh"
-    (canonicalEdges (removalTriangulation handleRemoval))
-    (canonicalEdges (removalTriangulation pointRemoval))
-
-testConstrainedDelaunay :: IO ()
-testConstrainedDelaunay = do
-  base <- requirePointBuild "CDT base" [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 1 1, Point 3 3, Point 1 3, Point 3 1]
-  let cdt0 = fromDelaunay (buildTriangulation base)
-      invalidPoint = Point (0 / 0) 0
-  case addConstraintEdge cdt0 invalidPoint (Point 4 0) of
-    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
-    outcome -> fail ("invalid first constraint endpoint was admitted: " <> either show (const "success") outcome)
-  case addConstraintEdge cdt0 (Point 0 0) invalidPoint of
-    Left (CdtBuildError (InvalidCoordinate Nothing _ CoordinateNaN)) -> pure ()
-    outcome -> fail ("invalid second constraint endpoint was admitted: " <> either show (const "success") outcome)
-  assertEqual
-    "invalid endpoint refusal leaves the immutable source unpublished"
-    (8, 0)
-    (numVertices cdt0, numConstraints cdt0)
-  partlyResident <-
-    requireRight
-      "one resident one new constraint endpoint"
-      (addConstraintEdge cdt0 (Point 0 0) (Point 2 2))
-  let partlyResidentCdt = constraintRecoveryTriangulation partlyResident
-  assertEqual
-    "one resident endpoint reserves only the missing site"
-    (numVertices cdt0 + 1)
-    (numVertices partlyResidentCdt)
-  assertCdtValid "one resident one new constraint endpoint" partlyResidentCdt
-  diagonalBatch <-
-    requireRight
-      "constraint recovery"
-      (recoverConstraints cdt0 (V.singleton (VertexId 0, VertexId 2)))
-  (diagonalPath, diagonalAdded) <-
-    requireAcceptedConstraint "constraint recovery" diagonalBatch
-  let cdt1 = constraintBatchTriangulation diagonalBatch
-  when (V.null diagonalPath) $ fail "constraint recovery returned an empty path"
-  assertEqual "constraint count" diagonalAdded (numConstraints cdt1)
-  assertCdtValid "constraint recovery" cdt1
-  conflictBatch <-
-    requireRight
-      "atomic conflict"
-      (recoverConstraints cdt1 (V.singleton (VertexId 1, VertexId 3)))
-  case V.toList (constraintBatchOutcomes conflictBatch) of
-    [ConstraintRejected _] -> pure ()
-    outcomes ->
-      fail
-        ( "crossing constraint was not rejected atomically: "
-            <> show outcomes
-        )
-  assertEqual
-    "crossing rejection preserves topology"
-    cdt1
-    (constraintBatchTriangulation conflictBatch)
-  let constraintProgram =
-        V.fromList
-          [ (VertexId 0, VertexId 2)
-          , (VertexId 1, VertexId 3)
-          , (VertexId 4, VertexId 6)
-          ]
-  wholeProgram <-
-    requireRight
-      "whole constraint program"
-      (recoverConstraints cdt0 constraintProgram)
-  (singletonTriangulation, singletonOutcomes) <-
-    requireRight
-      "singleton constraint program"
-      (V.foldM' replayConstraintRequest (cdt0, []) constraintProgram)
-  assertEqual
-    "batch outcomes equal singleton descent"
-    (V.toList (constraintBatchOutcomes wholeProgram))
-    (reverse singletonOutcomes)
-  assertEqual
-    "batch topology equals singleton descent"
-    singletonTriangulation
-    (constraintBatchTriangulation wholeProgram)
-  assertBatchStats "whole constraint program" wholeProgram
-  forM_ [0 .. 7 :: Int] $ \batchIndex -> do
-    randomizedBuild <-
-      requirePointBuild
-        ("random constraint batch " <> show batchIndex)
-        (randomPoints (0x6a09_e667_f3bc_c909 + fromIntegral batchIndex) 40)
-    let randomizedBase = fromDelaunay (buildTriangulation randomizedBuild)
-        mapping = buildInputVertices randomizedBuild
-        handles = V.generate (sizeofPrimArray mapping) (VertexId . indexPrimArray mapping)
-        requests = V.take 18 (V.zip handles (V.reverse handles))
-    randomizedBatch <-
-      requireRight
-        ("random whole constraint batch " <> show batchIndex)
-        (recoverConstraints randomizedBase requests)
-    (randomizedSingleton, randomizedOutcomes) <-
-      requireRight
-        ("random singleton constraint batch " <> show batchIndex)
-        (V.foldM' replayConstraintRequest (randomizedBase, []) requests)
-    assertEqual
-      ("random batch outcomes equal singleton descent " <> show batchIndex)
-      (V.toList (constraintBatchOutcomes randomizedBatch))
-      (reverse randomizedOutcomes)
-    assertEqual
-      ("random batch topology equals singleton descent " <> show batchIndex)
-      randomizedSingleton
-      (constraintBatchTriangulation randomizedBatch)
-    assertBatchStats
-      ("random constraint batch " <> show batchIndex)
-      randomizedBatch
-    assertCdtValid
-      ("random constraint batch " <> show batchIndex)
-      (constraintBatchTriangulation randomizedBatch)
-  -- Batch admission returns the first typed obstruction. Callers that need a
-  -- complete diagnosis ask the explicit corridor query and alone pay for it.
-  diagnosisBatch <-
-    requireRight
-      "conflict diagnosis"
-      (recoverConstraints cdt1 (V.singleton (VertexId 6, VertexId 7)))
-  case V.toList (constraintBatchOutcomes diagnosisBatch) of
-    [ConstraintRejected blocking] -> do
-      let diagnosed =
-            fmap
-              asUndirected
-              (getConflictingEdgesBetweenVertices cdt1 (VertexId 6) (VertexId 7))
-      case diagnosed of
-        [] -> fail "explicit conflict diagnosis named no edge"
-        firstBlocking : _ ->
-          assertEqual "batch rejection is the first corridor obstruction" firstBlocking blocking
-      assertEqual
-        "explicit conflict diagnosis is deduplicated"
-        (length diagnosed)
-        (length (Set.fromList diagnosed))
-      forM_ diagnosed $ \edge ->
-        unless (isConstraintEdge cdt1 edge) $
-          fail ("explicit conflict diagnosis named a non-constraint edge: " <> show edge)
-    outcomes ->
-      fail
-        ( "a constraint crossing the diagonal was not rejected: "
-            <> show outcomes
-        )
-
-  split <- requireRight "constraint split" (addConstraintAndSplit id cdt1 (VertexId 1) (VertexId 3))
-  let splitCdt = constraintRecoveryTriangulation split
-  unless (numVertices splitCdt > numVertices cdt1) $ fail "constraint split did not insert an intersection vertex"
-  assertCdtValid "constraint split" splitCdt
-
-  -- The batch splitter must agree with singleton descent, including across a
-  -- suspension: every vertical below is constrained only inside the batch
-  -- itself, so no census against the base can reserve for its crossings and
-  -- the chunk's vertex reservation exhausts mid-batch. The driver publishes,
-  -- re-reserves against the published mesh, and resumes; the final mesh must
-  -- not know any of that happened.
-  let bandColumns = [0 .. 7 :: Int]
-      bandVertices =
-        V.fromList
-          ( Point 0 5
-              : Point 90 5
-              : concat
-                  [ [Point x 10, Point x 0]
-                  | column <- bandColumns
-                  , let x = 10 * fromIntegral column + 5
-                  ]
-          )
-  bandBuild <-
-    requireRight
-      "split band base"
-      (constrainedDelaunayMaximal unitElementDefaults bandVertices V.empty)
-  let bandAcceptedBuild = cdtAcceptedBuild bandBuild
-      bandMapping = buildInputVertices bandAcceptedBuild
-      bandHandle input = VertexId (indexPrimArray bandMapping input)
-      bandBase = buildTriangulation bandAcceptedBuild
-      bandRequests =
-        V.fromList
-          ( (bandHandle 0, bandHandle 1)
-              : [ (bandHandle (2 * column + 2), bandHandle (2 * column + 3))
-                | column <- bandColumns
-                ]
-          )
-      replaySplit
-        :: ConstrainedDelaunayTriangulation (Point)
-        -> (VertexId, VertexId)
-        -> Either (CdtError) (ConstrainedDelaunayTriangulation (Point))
-      replaySplit triangulation request =
-        constraintRecoveryTriangulation
-          <$> uncurry (addConstraintAndSplit id triangulation) request
-  bandBatch <-
-    requireRight
-      "split batch"
-      (addConstraintsAndSplit id bandBase bandRequests)
-  bandDescent <-
-    requireRight
-      "split singleton descent"
-      (V.foldM' replaySplit bandBase bandRequests)
-  assertEqual
-    "split batch topology equals singleton descent"
-    bandDescent
-    (constraintRecoveryTriangulation bandBatch)
-  assertEqual
-    "split batch added one vertex per crossing"
-    (numVertices bandBase + length bandColumns)
-    (numVertices (constraintRecoveryTriangulation bandBatch))
-  assertCdtValid "split batch" (constraintRecoveryTriangulation bandBatch)
-
-  -- The same law with the reservation exhausting inside one corridor rather
-  -- than between corridors: the closing horizontal crosses two constraints
-  -- the census can see and fifteen it cannot, so the corridor suspends with
-  -- its cursor mid-walk and the resumed transaction continues from the last
-  -- split vertex, not from the corridor's start.
-  let laceColumns = [0 .. 16 :: Int]
-      laceVertices =
-        V.fromList
-          ( Point 0 5
-              : Point 90 5
-              : concat
-                  [ [Point x 10, Point x 0]
-                  | column <- laceColumns
-                  , let x = 5 * fromIntegral column + 5
-                  ]
-          )
-      laceBuiltIn = V.fromList [(2, 3), (34, 35)]
-  laceBuild <-
-    requireRight
-      "split lace base"
-      (constrainedDelaunayMaximal unitElementDefaults laceVertices laceBuiltIn)
-  let laceAcceptedBuild = cdtAcceptedBuild laceBuild
-      laceMapping = buildInputVertices laceAcceptedBuild
-      laceHandle input = VertexId (indexPrimArray laceMapping input)
-      laceBase = buildTriangulation laceAcceptedBuild
-      laceRequests =
-        V.fromList
-          ( [ (laceHandle (2 * column + 2), laceHandle (2 * column + 3))
-            | column <- [1 .. 15]
-            ]
-              <> [(laceHandle 0, laceHandle 1)]
-          )
-  laceBatch <-
-    requireRight
-      "split lace batch"
-      (addConstraintsAndSplit id laceBase laceRequests)
-  laceDescent <-
-    requireRight
-      "split lace singleton descent"
-      (V.foldM' replaySplit laceBase laceRequests)
-  assertEqual
-    "split lace batch topology equals singleton descent"
-    laceDescent
-    (constraintRecoveryTriangulation laceBatch)
-  assertEqual
-    "split lace batch added one vertex per crossing"
-    (numVertices laceBase + length laceColumns)
-    (numVertices (constraintRecoveryTriangulation laceBatch))
-  assertCdtValid "split lace batch" (constraintRecoveryTriangulation laceBatch)
-
-  let verticesInput :: V.Vector (Point)
-      verticesInput = V.fromList [Point 0 0, Point 4 0, Point 4 4, Point 0 4, Point 0 0]
-      constraintsInput = V.fromList [(0, 2), (1, 3), (4, 1)]
-  bulk <- requireRight "stable CDT bulk load" (constrainedDelaunayMaximal unitElementDefaults verticesInput constraintsInput)
-  let bulkAcceptedBuild = cdtAcceptedBuild bulk
-      bulkMapping = buildInputVertices bulkAcceptedBuild
-  unless (sizeofPrimArray bulkMapping > 4) $ fail "buildInputVertices out of bounds"
-  assertEqual "stable duplicate reroute" (VertexId (indexPrimArray bulkMapping 0)) (VertexId (indexPrimArray bulkMapping 4))
-  assertEqual "conflict reporting" 1 (V.length (cdtRejectedConstraints bulk))
-  assertCdtValid "stable CDT bulk load" (buildTriangulation bulkAcceptedBuild)
-
-testAnnotatedConstrainedUnion :: IO ()
-testAnnotatedConstrainedUnion = do
-  let leftPoints :: V.Vector (Point)
-      leftPoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
-      rightPoints :: V.Vector (Point)
-      rightPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]
-      thirdPoints :: V.Vector (Point)
-      thirdPoints = V.fromList [Point 4 0, Point 6 0, Point 6 2, Point 4 2]
-  leftBuild <-
-    requireRight "annotated constrained union left" $
-      constrainedDelaunay
-        unitElementDefaults
-        leftPoints
-        (V.singleton (0, 2))
-  rightBuild <-
-    requireRight "annotated constrained union right" $
-      constrainedDelaunay
-        unitElementDefaults
-        rightPoints
-        (V.singleton (0, 2))
-  thirdBuild <-
-    requireRight "annotated constrained union third" $
-      constrainedDelaunay
-        unitElementDefaults
-        thirdPoints
-        (V.singleton (0, 2))
-  let left =
-        mapVertices
-          (const (Set.singleton "left"))
-          (buildTriangulation leftBuild)
-      right =
-        mapVertices
-          (const (Set.singleton "right"))
-          (buildTriangulation rightBuild)
-      third =
-        mapVertices
-          (const (Set.singleton "third"))
-          (buildTriangulation thirdBuild)
-  joined <-
-    requireRight
-      "annotated constrained union"
-      (unionConstrainedWith Set.union left right)
-  let expectedAnnotations =
-        Map.fromList
-          [ (Point 0 0, Set.singleton "left")
-          , (Point 0 2, Set.singleton "left")
-          , (Point 2 0, Set.fromList ["left", "right"])
-          , (Point 2 2, Set.fromList ["left", "right"])
-          , (Point 4 0, Set.singleton "right")
-          , (Point 4 2, Set.singleton "right")
-          ]
-      actualAnnotations =
-        Map.fromList
-          [ (vertexPoint joined vertex, vertexData joined vertex)
-          | vertex <- vertices joined
-          ]
-      expectedConstraints =
-        Set.union
-          (Set.fromList (V.toList (constraintSegments left)))
-          (Set.fromList (V.toList (constraintSegments right)))
-  assertEqual
-    "annotated constrained union preserves and combines site payloads"
-    expectedAnnotations
-    actualAnnotations
-  assertEqual
-    "annotated constrained union preserves both constraint sections"
-    expectedConstraints
-    (Set.fromList (V.toList (constraintSegments joined)))
-  assertCdtValid "annotated constrained union" joined
-  commuted <-
-    requireRight
-      "annotated constrained union commuted"
-      (unionConstrainedWith Set.union right left)
-  assertEqual
-    "annotated constrained union is commutative under a commutative payload combiner"
-    joined
-    commuted
-  leftAssociated <-
-    requireRight
-      "annotated constrained union left-associated"
-      (unionConstrainedWith Set.union joined third)
-  rightPair <-
-    requireRight
-      "annotated constrained union right pair"
-      (unionConstrainedWith Set.union right third)
-  rightAssociated <-
-    requireRight
-      "annotated constrained union right-associated"
-      (unionConstrainedWith Set.union left rightPair)
-  assertEqual
-    "annotated constrained union is associative wherever both descents are admitted"
-    leftAssociated
-    rightAssociated
-  unitJoined <-
-    requireRight
-      "unit constrained union specialization"
-      ( unionConstrained
-          (mapVertices (const ()) left)
-          (mapVertices (const ()) right)
-      )
-  assertEqual
-    "unit constrained union specializes annotated constrained union"
-    unitJoined
-    (mapVertices (const ()) joined)
-
--- | Sequential extension retains the base authority as the transaction root.
--- The semantic result agrees with canonical union on sites and constraint
--- sections, but its receipt proves that only the incoming constraint section
--- was interpreted.
-testAsymmetricConstrainedExtension :: IO ()
-testAsymmetricConstrainedExtension = do
-  let basePoints :: V.Vector (Point)
-      basePoints = V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
-      extensionPoints :: V.Vector (Point)
-      extensionPoints = V.fromList [Point 2 0, Point 4 0, Point 4 2, Point 2 2]
-  baseBuild <-
-    requireRight
-      "asymmetric extension base"
-      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (0, 2)))
-  extensionBuild <-
-    requireRight
-      "asymmetric extension incoming"
-      (constrainedDelaunay unitElementDefaults extensionPoints (V.singleton (0, 2)))
-  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)
-      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)
-      expectedAnnotations =
-        Map.fromList
-          [ (Point 0 0, Set.singleton "base")
-          , (Point 0 2, Set.singleton "base")
-          , (Point 2 0, Set.fromList ["base", "extension"])
-          , (Point 2 2, Set.fromList ["base", "extension"])
-          , (Point 4 0, Set.singleton "extension")
-          , (Point 4 2, Set.singleton "extension")
-          ]
-      expectedConstraints =
-        Set.union
-          (Set.fromList (V.toList (constraintSegments base)))
-          (Set.fromList (V.toList (constraintSegments extension)))
-      baseAnnotations =
-        Map.fromList
-          [ (vertexPoint base vertex, vertexData base vertex)
-          | vertex <- vertices base
-          ]
-      extensionAnnotations =
-        Map.fromList
-          [ (vertexPoint extension vertex, vertexData extension vertex)
-          | vertex <- vertices extension
-          ]
-  baseAnnotationsBefore <- evaluate (force baseAnnotations)
-  baseConstraintsBefore <- evaluate (force (constraintSegments base))
-  extensionAnnotationsBefore <- evaluate (force extensionAnnotations)
-  extensionConstraintsBefore <- evaluate (force (constraintSegments extension))
-  extendedResult <-
-    requireRight
-      "asymmetric constrained extension"
-      (extendConstrainedWith Set.union base extension)
-  let constraintBatch = constrainedExtensionConstraintBatch extendedResult
-      extended = constraintBatchTriangulation constraintBatch
-      receipt = constraintBatchStats constraintBatch
-      buildReceipt = constrainedExtensionBuildStats extendedResult
-      actualAnnotations =
-        Map.fromList
-          [ (vertexPoint extended vertex, vertexData extended vertex)
-          | vertex <- vertices extended
-          ]
-  assertEqual
-    "asymmetric extension preserves base and combines coincident site payloads"
-    expectedAnnotations
-    actualAnnotations
-  assertEqual
-    "asymmetric extension preserves resident and incoming constraint sections"
-    expectedConstraints
-    (Set.fromList (V.toList (constraintSegments extended)))
-  assertEqual
-    "asymmetric extension does not mutate the frozen base predecessor sites"
-    baseAnnotationsBefore
-    ( Map.fromList
-        [ (vertexPoint base vertex, vertexData base vertex)
-        | vertex <- vertices base
-        ]
-    )
-  assertEqual
-    "asymmetric extension does not mutate the frozen base predecessor constraints"
-    baseConstraintsBefore
-    (constraintSegments base)
-  assertEqual
-    "asymmetric extension does not mutate the frozen incoming predecessor sites"
-    extensionAnnotationsBefore
-    ( Map.fromList
-        [ (vertexPoint extension vertex, vertexData extension vertex)
-        | vertex <- vertices extension
-        ]
-    )
-  assertEqual
-    "asymmetric extension does not mutate the frozen incoming predecessor constraints"
-    extensionConstraintsBefore
-    (constraintSegments extension)
-  assertEqual
-    "asymmetric extension recovers only incoming constraints"
-    (V.length (constraintSegments extension))
-    (constraintBatchRequests receipt)
-  assertEqual
-    "asymmetric extension returns one outcome for each incoming constraint"
-    (V.length (constraintSegments extension))
-    (V.length (constraintBatchOutcomes constraintBatch))
-  assertEqual
-    "asymmetric extension admits every incoming constraint"
-    (V.length (constraintSegments extension))
-    (constraintBatchAccepted receipt)
-  assertEqual
-    "asymmetric extension reports no incoming constraint rejection"
-    0
-    (constraintBatchRejected receipt)
-  assertEqual
-    "asymmetric extension charges every incoming site once"
-    (V.length extensionPoints)
-    (statInputPoints buildReceipt)
-  assertEqual
-    "asymmetric extension distinguishes occupied incoming sites"
-    2
-    (statExistingPoints buildReceipt)
-  assertEqual
-    "asymmetric extension distinguishes newly materialized incoming sites"
-    2
-    (statUniquePoints buildReceipt)
-  assertCdtValid "asymmetric constrained extension" extended
-
-  crossingBuild <-
-    requireRight
-      "asymmetric extension crossing incoming section"
-      (constrainedDelaunay unitElementDefaults basePoints (V.singleton (1, 3)))
-  case
-      extendConstrainedWith
-        Set.union
-        base
-        (mapVertices (const (Set.singleton "crossing")) (buildTriangulation crossingBuild)) of
-    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()
-    other -> fail ("asymmetric extension did not return the corridor intersection witness: " <> show other)
-
-testLargeAsymmetricConstrainedExtension :: IO ()
-testLargeAsymmetricConstrainedExtension = do
-  let width = 40 :: Int
-      height = 28 :: Int
-      basePoints :: V.Vector (Point)
-      basePoints =
-        V.fromList
-          ( [ Point 0 0
-            , Point (fromIntegral (width + 1)) 0
-            , Point (fromIntegral (width + 1)) (fromIntegral (height + 1))
-            , Point 0 (fromIntegral (height + 1))
-            ]
-              <> [ Point
-                     (fromIntegral (column + 1) + fromIntegral ((column * 17 + row * 31) `mod` 13) * 1.0e-3)
-                     (fromIntegral (row + 1) + fromIntegral ((column * 23 + row * 19) `mod` 17) * 1.0e-3)
-                 | row <- [0 .. height - 1]
-                 , column <- [0 .. width - 1]
-                 ]
-          )
-      baseConstraints = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
-      extensionPoints :: V.Vector (Point)
-      extensionPoints = V.fromList [Point 48 8, Point 53.2 8.4, Point 50.4 15.8]
-      extensionConstraints = V.fromList [(0, 1), (1, 2), (2, 0)]
-      conflictPoints :: V.Vector (Point)
-      conflictPoints = V.fromList [Point 8 22, Point 30 5, Point 26 7]
-  baseBuild <-
-    requireRight
-      "large asymmetric extension base"
-      (constrainedDelaunay unitElementDefaults basePoints baseConstraints)
-  extensionBuild <-
-    requireRight
-      "large asymmetric extension incoming"
-      (constrainedDelaunay unitElementDefaults extensionPoints extensionConstraints)
-  conflictBuild <-
-    requireRight
-      "large asymmetric extension conflicting incoming"
-      (constrainedDelaunay unitElementDefaults conflictPoints (V.singleton (0, 1)))
-  let base = mapVertices (const (Set.singleton "base")) (buildTriangulation baseBuild)
-      extension = mapVertices (const (Set.singleton "extension")) (buildTriangulation extensionBuild)
-      conflicting = mapVertices (const (Set.singleton "conflict")) (buildTriangulation conflictBuild)
-      predecessor
-        :: Triangulation 'Constrained (Set.Set String) () () ()
-        -> ( Triangulation 'Constrained (Set.Set String) () () ()
-           , Map.Map VertexId (Point, Set.Set String)
-           , Set.Set (CanonicalSegment)
-           )
-      predecessor triangulation =
-        ( triangulation
-        , Map.fromList
-            [ (vertex, (vertexPoint triangulation vertex, vertexData triangulation vertex))
-            | vertex <- vertices triangulation
-            ]
-        , Set.fromList (V.toList (constraintSegments triangulation))
-        )
-      assertPredecessor
-        :: String
-        -> ( Triangulation 'Constrained (Set.Set String) () () ()
-           , Map.Map VertexId (Point, Set.Set String)
-           , Set.Set (CanonicalSegment)
-           )
-        -> Triangulation 'Constrained (Set.Set String) () () ()
-        -> IO ()
-      assertPredecessor label snapshot triangulation =
-        assertEqual label snapshot (predecessor triangulation)
-  baseBefore <- evaluate (force (predecessor base))
-  extensionBefore <- evaluate (force (predecessor extension))
-  conflictingBefore <- evaluate (force (predecessor conflicting))
-  let (_, baseVertexSnapshotBefore, baseConstraintSnapshotBefore) = baseBefore
-      (_, _, extensionConstraintSnapshotBefore) = extensionBefore
-  extensionResult <-
-    requireRight
-      "large asymmetric constrained extension"
-      (extendConstrainedWith Set.union base extension)
-  let constraintBatch = constrainedExtensionConstraintBatch extensionResult
-      extended = constraintBatchTriangulation constraintBatch
-      extendedConstraints = Set.fromList (V.toList (constraintSegments extended))
-      extendedVertexSnapshot =
-        Map.fromList
-          [ (vertex, (vertexPoint extended vertex, vertexData extended vertex))
-          | vertex <- vertices extended
-          ]
-  assertEqual
-    "large asymmetric extension retains every base vertex handle, coordinate, and payload"
-    baseVertexSnapshotBefore
-    (Map.restrictKeys extendedVertexSnapshot (Map.keysSet baseVertexSnapshotBefore))
-  assertEqual
-    "large asymmetric extension retains its complete base constraint source section"
-    baseConstraintSnapshotBefore
-    (Set.intersection baseConstraintSnapshotBefore extendedConstraints)
-  assertEqual
-    "large asymmetric extension retains its complete incoming constraint source section"
-    extensionConstraintSnapshotBefore
-    (Set.intersection extensionConstraintSnapshotBefore extendedConstraints)
-  assertEqual
-    "large asymmetric extension preserves exactly both source constraint sections"
-    (Set.union baseConstraintSnapshotBefore extensionConstraintSnapshotBefore)
-    extendedConstraints
-  assertEqual
-    "large asymmetric extension leaves the frozen base predecessor physically unchanged"
-    baseBefore
-    (predecessor base)
-  assertPredecessor
-    "large asymmetric extension leaves the frozen incoming predecessor physically unchanged"
-    extensionBefore
-    extension
-  assertEqual
-    "large asymmetric extension charges only the incoming sites"
-    (V.length extensionPoints)
-    (statInputPoints (constrainedExtensionBuildStats extensionResult))
-  assertEqual
-    "large asymmetric extension replays only the incoming constraints"
-    (V.length extensionConstraints)
-    (constraintBatchRequests (constraintBatchStats constraintBatch))
-  assertCdtValid "large asymmetric constrained extension" extended
-  case extendConstrainedWith Set.union base conflicting of
-    Left (ConstraintUnionConstructionFailed (ConstraintIntersection _)) -> pure ()
-    outcome ->
-      fail
-        ( "large asymmetric extension did not refuse the crossing incoming corridor: "
-            <> show outcome
-        )
-  assertEqual
-    "large asymmetric extension refusal leaves the frozen base predecessor physically unchanged"
-    baseBefore
-    (predecessor base)
-  assertPredecessor
-    "large asymmetric extension refusal leaves the conflicting predecessor physically unchanged"
-    conflictingBefore
-    conflicting
-
--- | A separated constrained seam is a restriction-preserving operation, not
--- the canonical site-set union wearing a cheaper costume. Both closed source
--- sections survive face-for-face; only the corridor contributes new faces.
-testSeparatedConstrainedSeam :: IO ()
-testSeparatedConstrainedSeam = do
-  let leftPoints :: V.Vector (Point)
-      leftPoints =
-        V.fromList
-          [ Point (-4) (-1)
-          , Point (-2) (-1)
-          , Point (-2) 1
-          , Point (-4) 1
-          ]
-      rightPoints :: V.Vector (Point)
-      rightPoints =
-        V.fromList
-          [ Point 2 (-1)
-          , Point 4 (-1)
-          , Point 4 1
-          , Point 2 1
-          ]
-      closedContour = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
-  leftBuild <-
-    requireRight
-      "separated constrained seam left"
-      (constrainedDelaunay unitElementDefaults leftPoints closedContour)
-  rightBuild <-
-    requireRight
-      "separated constrained seam right"
-      (constrainedDelaunay unitElementDefaults rightPoints closedContour)
-  let left =
-        Internal.geometryOnlyPublication
-          (buildTriangulation leftBuild)
-      right =
-        Internal.geometryOnlyPublication
-          (buildTriangulation rightBuild)
-      leftFaceKeys = Set.fromList (fmap (faceKeyOf left) (innerFaces left))
-      rightFaceKeys = Set.fromList (fmap (faceKeyOf right) (innerFaces right))
-      expectedConstraints =
-        Set.union
-          (Set.fromList (V.toList (constraintSegments left)))
-          (Set.fromList (V.toList (constraintSegments right)))
-  seam <-
-    requireRight
-      "source-preserving separated constrained seam"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left right)
-  let joined = constrainedSeamResultTriangulation seam
-      joinedFaceKeys = Set.fromList (fmap (faceKeyOf joined) (innerFaces joined))
-      actualAnnotations =
-        Map.fromList
-          [ (vertexPoint joined vertex, vertexData joined vertex)
-          | vertex <- vertices joined
-          ]
-      expectedAnnotations =
-        Map.union
-          ( Map.fromList
-              [ (vertexPoint left vertex, vertexData left vertex)
-              | vertex <- vertices left
-              ]
-          )
-          ( Map.fromList
-              [ (vertexPoint right vertex, vertexData right vertex)
-              | vertex <- vertices right
-              ]
-          )
-      actualConstraints = Set.fromList (V.toList (constraintSegments joined))
-      certifiedPerimeter =
-        fmap
-          (\segmentValue -> (segmentStart segmentValue, segmentEnd segmentValue))
-          (Set.toAscList (Set.difference actualConstraints expectedConstraints))
-  assertCdtValid "source-preserving separated constrained seam" joined
-  assertEqual
-    "separated seam preserves all source annotations"
-    expectedAnnotations
-    actualAnnotations
-  assertEqual
-    "separated seam preserves both source constraint sections"
-    True
-    (expectedConstraints `Set.isSubsetOf` actualConstraints)
-  assertEqual
-    "separated seam certifies exactly its two synthetic perimeter bridges"
-    [ (Point (-2) (-1), Point 2 (-1))
-    , (Point (-2) 1, Point 2 1)
-    ]
-    certifiedPerimeter
-  unless (leftFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
-    fail "separated seam removed or retriangulated a left source face"
-  unless (rightFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
-    fail "separated seam removed or retriangulated a right source face"
-  assertEqual
-    "separated seam records the stable left face witness"
-    (numInnerFaces left)
-    (constrainedSeamLeftFaceCount seam)
-  assertEqual
-    "separated seam records the stable left constraint witness"
-    (numConstraints left)
-    (constrainedSeamLeftConstraintCount seam)
-  assertEqual
-    "separated seam has one right face witness per source face"
-    (numInnerFaces right)
-    (V.length (constrainedSeamRightFaceEvidence seam))
-  forM_ (V.toList (constrainedSeamRightFaceEvidence seam)) $ \evidence ->
-    let sourceFaceKey = faceKeyOf right (constrainedSeamSourceFace evidence)
-        targetFaceKey = faceKeyOf joined (constrainedSeamTargetFace evidence)
-        evidenceFaceKey =
-          [ constrainedSeamFaceFirstPoint evidence
-          , constrainedSeamFaceSecondPoint evidence
-          , constrainedSeamFaceThirdPoint evidence
-          ]
-     in do
-          assertEqual
-            "right face witness names the exact target triangle"
-            sourceFaceKey
-            targetFaceKey
-          assertEqual
-            "right face witness carries the exact target triangle"
-            targetFaceKey
-            evidenceFaceKey
-  when (V.null (constrainedSeamJoinFaces seam)) $
-    fail "separated seam did not identify any new corridor face"
-  assertEqual
-    "copied source constraints require no corridor recovery"
-    0
-    (constraintBatchRequests (constrainedSeamConstraintStats seam))
-  let seamPublication = constrainedSeamPublicationStats seam
-  assertEqual
-    "separated seam does not enumerate resident unboxed base pages"
-    0
-    (publicationUnboxedBasePageEnumerations seamPublication)
-  assertEqual
-    "separated seam does not freeze resident unboxed base pages"
-    0
-    (publicationUnboxedBasePageFreezes seamPublication)
-  assertEqual
-    "separated seam does not enumerate resident boxed base pages"
-    0
-    (publicationBoxedBasePageEnumerations seamPublication)
-  unless
-    ( publicationUnboxedDirtyBasePages seamPublication
-        + publicationUnboxedDirtyAppendedPages seamPublication
-        > 0
-    ) $
-    fail "separated seam publication did not record appended unboxed writes"
-  unless (constrainedSeamCachedFrontierPointReads seam > 0) $
-    fail "separated seam did not record cached frontier point reads"
-
-  let boundedBridgeParameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 6
-          , refineMaxEdgeLength = Just 1
-          }
-  boundedSeam <-
-    requireRight
-      "edge-bounded separated constrained seam"
-      ( joinSeparatedConstrained
-          (\_ _ -> True)
-          boundedBridgeParameters
-          left
-          right
-      )
-  let boundedJoined = constrainedSeamResultTriangulation boundedSeam
-      boundedSyntheticSegments =
-        Set.difference
-          (Set.fromList (V.toList (constraintSegments boundedJoined)))
-          expectedConstraints
-  assertCdtValid "edge-bounded separated constrained seam" boundedJoined
-  assertEqual
-    "edge-bounded seam charges its exact bridge Steiner budget"
-    6
-    (numVertices boundedJoined - numVertices left - numVertices right)
-  assertEqual
-    "edge-bounded seam publishes both four-edge bridge paths"
-    8
-    (Set.size boundedSyntheticSegments)
-  unless (all ((<= 1) . canonicalSegmentSquaredLength) boundedSyntheticSegments) $
-    fail "edge-bounded seam published an oversized synthetic bridge child"
-  assertEqual
-    "edge-bounded seam cache agrees with its explicit frontier observation"
-    (Internal.prepareSeamFrontierIndex boundedJoined)
-    (Internal.triSeamFrontier boundedJoined)
-  case
-      joinSeparatedConstrained
-        (\_ _ -> True)
-        boundedBridgeParameters{refineMaxAdditionalVertices = Just 5}
-        left
-        right of
-    Left
-      ( ConstraintUnionConstructionFailed
-          (CdtBuildError (RefinementSeamBridgeBudgetExceeded 6 5))
-        ) -> pure ()
-    outcome ->
-      fail
-        ( "edge-bounded seam did not return its exact budget obstruction: "
-            <> either show (const "success") outcome
-        )
-
-  reversed <- requireRight "reversed source-preserving separated seam" (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters right left)
-  assertCdtValid "reversed source-preserving separated seam" (constrainedSeamResultTriangulation reversed)
-
-  let localParameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 2
-          , refineMaxArea = Just 0.5
-          , refineKeepConstraintEdges = True
-          }
-      allJoinedFaces = Set.fromList (innerFaces joined)
-  locallyRefined <-
-    requireRight
-      "seam refinement preserves cached hull frontier"
-      (refineWithinDomain (const ()) localParameters allJoinedFaces joined)
-  let refinedJoined = refinedTriangulation (refinementDomainResult locallyRefined)
-  unless (refinementComplete (refinementDomainResult locallyRefined)) $
-    fail "seam refinement fixture did not drain its local worklist"
-  assertEqual
-    "local refinement retains the exact cached hull frontier"
-    (Internal.triSeamFrontier joined)
-    (Internal.triSeamFrontier refinedJoined)
-
-  thirdBuild <-
-    requireRight
-      "repeated separated constrained seam source"
-      (constrainedDelaunay unitElementDefaults (V.fromList
-        [ Point 8 (-1)
-        , Point 10 (-1)
-        , Point 10 1
-        , Point 8 1
-        ]) closedContour)
-  let third =
-        Internal.geometryOnlyPublication
-          (buildTriangulation thirdBuild)
-  repeated <-
-    requireRight
-      "repeated separated seam after local refinement"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters refinedJoined third)
-  assertCdtValid
-    "repeated separated seam after local refinement"
-    (constrainedSeamResultTriangulation repeated)
-  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters left left of
-    Left ConstraintUnionNotSeparated -> pure ()
-    other -> fail ("non-separated constrained seam was not refused: " <> show other)
-
--- North/south additions use the orientation-preserving Y chart rather than
--- rotating or copying the resident mesh.  The sequence then alternates axes,
--- so a stale one-axis frontier cache cannot accidentally make the third join
--- succeed.
-testAxisSeparatedConstrainedSeams :: IO ()
-testAxisSeparatedConstrainedSeams = do
-  let closedContour :: Int -> V.Vector (Int, Int)
-      closedContour pointCount =
-        V.fromList
-          [ (index, (index + 1) `mod` pointCount)
-          | index <- [0 .. pointCount - 1]
-          ]
-      buildPublished
-        :: String
-        -> V.Vector Point
-        -> IO (Triangulation 'Constrained () () () ())
-      buildPublished label points = do
-        built <-
-          requireRight
-            label
-            (constrainedDelaunay
-               unitElementDefaults
-               points
-               (closedContour (V.length points)))
-        pure (Internal.geometryOnlyPublication (buildTriangulation built))
-      centralPoints =
-        V.fromList
-          [ Point (-1) (-1)
-          , Point 1 (-1)
-          , Point 1 1
-          , Point (-1) 1
-          ]
-      northPoints =
-        V.fromList
-          [ Point (-1) 3
-          , Point 1 3
-          , Point 1 5
-          , Point (-1) 5
-          ]
-      southPoints =
-        V.fromList
-          [ Point (-1) (-5)
-          , Point 1 (-5)
-          , Point 1 (-3)
-          , Point (-1) (-3)
-          ]
-      eastPoints =
-        V.fromList
-          [ Point 3 (-1)
-          , Point 5 (-1)
-          , Point 5 1
-          , Point 3 1
-          ]
-      overlapPoints =
-        V.fromList
-          [ Point 0 0
-          , Point 2 0
-          , Point 2 2
-          , Point 0 2
-          ]
-      irregularNorthPoints =
-        V.fromList
-          [ Point (-2) 7
-          , Point 0 7
-          , Point 2 7
-          , Point 2.5 9
-          , Point 0 11
-          , Point (-2.5) 9
-          ]
-  central <- buildPublished "axis seam central source" centralPoints
-  north <- buildPublished "axis seam north source" northPoints
-  south <- buildPublished "axis seam south source" southPoints
-  east <- buildPublished "axis seam east source" eastPoints
-  overlap <- buildPublished "axis seam overlapping source" overlapPoints
-
-  let northParameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 2
-          , refineMaxEdgeLength = Just 1
-          }
-  northJoined <-
-    requireRight
-      "edge-bounded north source-preserving separated seam"
-      (joinSeparatedConstrained (\_ _ -> True) northParameters central north)
-  let northTopology = constrainedSeamResultTriangulation northJoined
-      northPublication = constrainedSeamPublicationStats northJoined
-      northSyntheticSegments =
-        Set.difference
-          (Set.fromList (V.toList (constraintSegments northTopology)))
-          ( Set.union
-              (Set.fromList (V.toList (constraintSegments central)))
-              (Set.fromList (V.toList (constraintSegments north)))
-          )
-  assertCdtValid
-    "north source-preserving separated seam"
-    northTopology
-  assertEqual
-    "north seam charges two bridge Steiner vertices"
-    2
-    (numVertices northTopology - numVertices central - numVertices north)
-  assertEqual
-    "north seam certifies two bounded two-edge bridge paths"
-    4
-    (Set.size northSyntheticSegments)
-  unless (all ((<= 1) . canonicalSegmentSquaredLength) northSyntheticSegments) $
-    fail "north seam published an oversized synthetic bridge child"
-  assertEqual
-    "north seam cache agrees with an explicit frontier observation"
-    (Internal.prepareSeamFrontierIndex northTopology)
-    (Internal.triSeamFrontier northTopology)
-  assertEqual
-    "north seam does not enumerate resident unboxed pages"
-    0
-    (publicationUnboxedBasePageEnumerations northPublication)
-  assertEqual
-    "north seam does not freeze resident unboxed pages"
-    0
-    (publicationUnboxedBasePageFreezes northPublication)
-  assertEqual
-    "north seam does not enumerate resident boxed pages"
-    0
-    (publicationBoxedBasePageEnumerations northPublication)
-  unless
-    ( publicationUnboxedDirtyBasePages northPublication
-        + publicationUnboxedDirtyAppendedPages northPublication
-        > 0
-    ) $
-    fail "north seam did not record appended unboxed writes"
-
-  reversed <-
-    requireRight
-      "reversed south/north source-preserving separated seam"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters south central)
-  assertCdtValid
-    "reversed south/north source-preserving separated seam"
-    (constrainedSeamResultTriangulation reversed)
-
-  mixed <-
-    requireRight
-      "mixed-axis east extension after north seam"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
-         (constrainedSeamResultTriangulation northJoined)
-         east)
-  assertCdtValid
-    "mixed-axis east extension after north seam"
-    (constrainedSeamResultTriangulation mixed)
-  assertEqual
-    "mixed-axis seam retains prior perimeter and certifies two new bridges"
-    ( numConstraints (constrainedSeamResultTriangulation northJoined)
-        + numConstraints east
-        + 2
-    )
-    (numConstraints (constrainedSeamResultTriangulation mixed))
-
-  repeated <-
-    requireRight
-      "repeated mixed-axis south extension"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
-         (constrainedSeamResultTriangulation mixed)
-         south)
-  assertCdtValid
-    "repeated mixed-axis south extension"
-    (constrainedSeamResultTriangulation repeated)
-  assertEqual
-    "repeated mixed-axis seam cache agrees with an explicit frontier observation"
-    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation repeated))
-    (Internal.triSeamFrontier (constrainedSeamResultTriangulation repeated))
-
-  irregularNorth <-
-    buildPublished
-      "axis seam irregular north source"
-      irregularNorthPoints
-  irregularJoined <-
-    requireRight
-      "irregular collinear north extension"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters
-         (constrainedSeamResultTriangulation repeated)
-         irregularNorth)
-  assertCdtValid
-    "irregular collinear north extension"
-    (constrainedSeamResultTriangulation irregularJoined)
-  assertEqual
-    "irregular collinear north cache agrees with an explicit frontier observation"
-    (Internal.prepareSeamFrontierIndex (constrainedSeamResultTriangulation irregularJoined))
-    (Internal.triSeamFrontier (constrainedSeamResultTriangulation irregularJoined))
-
-  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters central overlap of
-    Left ConstraintUnionNotSeparated -> pure ()
-    outcome ->
-      fail
-        ( "overlapping sources were not refused by either-axis admission: "
-            <> either show (const "success") outcome
-        )
-
--- A separated source frontier may become an interior diagonal when the seam
--- closes. If that diagonal is unconstrained and locally illegal, preserving
--- the source and publishing a Delaunay join are incompatible obligations. The
--- constrained twin is lawful: its contour explicitly authorizes that fixed
--- diagonal to remain.
-testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite :: IO ()
-testSeparatedConstrainedSeamRefusesPinnedFrontierRewrite = do
-  let leftPoints =
-        V.fromList
-          [ Point (-2) 0
-          , Point 0 0
-          , Point (-1) 10
-          , Point (-3) 5
-          ]
-      rightPoints =
-        V.fromList
-          [ Point 1 5
-          , Point 2 4
-          , Point 3 5
-          , Point 2 6
-          ]
-      closedQuadrilateral = V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)]
-      buildSource
-        :: String
-        -> V.Vector (Int, Int)
-        -> V.Vector Point
-        -> IO (BuildResult 'Constrained Point () () ())
-      buildSource label constraints points =
-        requireRight label (constrainedDelaunay unitElementDefaults points constraints)
-      publish
-        :: BuildResult 'Constrained Point () () ()
-        -> Triangulation 'Constrained () () () ()
-      publish = Internal.geometryOnlyPublication . buildTriangulation
-  unconstrainedLeft <- publish <$> buildSource "incompatible seam left" V.empty leftPoints
-  unconstrainedRight <- publish <$> buildSource "incompatible seam right" V.empty rightPoints
-  case joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters unconstrainedLeft unconstrainedRight of
-    Left
-      ( ConstraintUnionConstructionFailed
-          (CdtBuildError (SeamSourceEdgeRequiresFlip _))
-        ) -> pure ()
-    outcome ->
-      fail
-        ( "separated seam did not refuse an unconstrained pinned source rewrite: "
-            <> either show (const "success") outcome
-        )
-  selectiveJoin <-
-    requireRight
-      "selective seam rewrites only unselected exterior faces"
-      (joinSeparatedConstrained (\_ _ -> False) defaultRefinementParameters unconstrainedLeft unconstrainedRight)
-  let selectivelyJoined = constrainedSeamResultTriangulation selectiveJoin
-      sourceFaceKeys =
-        Set.union
-          (Set.fromList (fmap (faceKeyOf unconstrainedLeft) (innerFaces unconstrainedLeft)))
-          (Set.fromList (fmap (faceKeyOf unconstrainedRight) (innerFaces unconstrainedRight)))
-      selectiveFaceKeys =
-        Set.fromList (fmap (faceKeyOf selectivelyJoined) (innerFaces selectivelyJoined))
-  assertCdtValid "selective unconstrained seam" selectivelyJoined
-  when (sourceFaceKeys `Set.isSubsetOf` selectiveFaceKeys) $
-    fail "selective seam did not rewrite the incompatible exterior source face"
-  when (V.null (constrainedSeamJoinFaces selectiveJoin)) $
-    fail "selective seam did not publish the exact rewritten J component"
-  unless (V.null (constrainedSeamRightFaceEvidence selectiveJoin)) $
-    fail "selective seam fabricated transport evidence for unselected incoming faces"
-  constrainedLeft <-
-    publish
-      <$> buildSource
-        "compatible constrained seam left"
-        closedQuadrilateral
-        leftPoints
-  constrainedRight <-
-    publish
-      <$> buildSource
-        "compatible constrained seam right"
-        closedQuadrilateral
-        rightPoints
-  let protectedLeftFaces = Set.fromList (boundedRegionFaces constrainedLeft)
-      protectedRightFaces = Set.fromList (boundedRegionFaces constrainedRight)
-      preserveConstrainedSource side faceHandle =
-        case side of
-          SeamResident -> Set.member faceHandle protectedLeftFaces
-          SeamIncoming -> Set.member faceHandle protectedRightFaces
-      protectedFaceKeys =
-        Set.union
-          (Set.fromList (fmap (faceKeyOf constrainedLeft) (Set.toList protectedLeftFaces)))
-          (Set.fromList (fmap (faceKeyOf constrainedRight) (Set.toList protectedRightFaces)))
-  constrainedJoin <-
-    requireRight
-      "compatible selected constrained seam"
-      (joinSeparatedConstrained preserveConstrainedSource defaultRefinementParameters constrainedLeft constrainedRight)
-  let constrainedJoined = constrainedSeamResultTriangulation constrainedJoin
-      joinedFaceKeys =
-        Set.fromList (fmap (faceKeyOf constrainedJoined) (innerFaces constrainedJoined))
-      evidencedIncomingFaces =
-        Set.fromList
-          ( fmap
-              constrainedSeamSourceFace
-              (V.toList (constrainedSeamRightFaceEvidence constrainedJoin))
-          )
-  assertCdtValid
-    "compatible selected constrained seam"
-    constrainedJoined
-  unless (protectedFaceKeys `Set.isSubsetOf` joinedFaceKeys) $
-    fail "selected constrained source face did not survive exactly"
-  assertEqual
-    "selected seam evidences exactly the protected incoming faces"
-    protectedRightFaces
-    evidencedIncomingFaces
-
--- An obtuse unconstrained source has a hull edge inside the opposite vertex's
--- diametral circle.  That edge is queued by the refinement seed even though
--- it is not a source constraint.  The checked local interpreter must refuse
--- the encroachment split at its authoritative resolver, retain the cached
--- frontier, and remain joinable afterwards.
-testLocalRefinementRejectsEncroachedHullSplit :: IO ()
-testLocalRefinementRejectsEncroachedHullSplit = do
-  built <-
-    requireRight
-      "encroached hull source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          (V.fromList [Point 0 0, Point 2 0, Point 0.1 0.1])
-          V.empty
-      )
-  let source =
-        Internal.geometryOnlyPublication
-          (buildTriangulation built)
-      permitted = Set.fromList (innerFaces source)
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 1
-          , refineMaxArea = Just 0.001
-          , refineKeepConstraintEdges = True
-          }
-      hullEdges =
-        [ edge
-        | edge <- undirectedEdges source
-        , let (forward, backward) = directedPair edge
-        , incidentFace source forward == outerFace || incidentFace source backward == outerFace
-        ]
-      encroachedHull edge =
-        let (fromVertex, toVertex) = Dcel.undirectedEndpoints source edge
-            fromPoint = vertexPoint source fromVertex
-            toPoint = vertexPoint source toVertex
-            oppositeFaceEdge =
-              case directedPair edge of
-                (forward, backward)
-                  | incidentFace source forward == outerFace -> backward
-                  | otherwise -> forward
-            oppositePoint =
-              let innerFace = incidentFace source oppositeFaceEdge
-               in if innerFace == outerFace
-                    then Nothing
-                    else
-                      case Dcel.faceVertices source innerFace of
-                        verticesInFace ->
-                          case filter (/= fromVertex) (filter (/= toVertex) verticesInFace) of
-                            [vertex] -> Just (vertexPoint source vertex)
-                            _ -> Nothing
-         in maybe False (Admitted.inDiametralCircle fromPoint toPoint) oppositePoint
-  unless (any encroachedHull hullEdges) $
-    fail "encroached hull fixture did not produce an encroached hull pair"
-  refined <-
-    requireRight
-      "encroached hull local refinement"
-      (refineWithinDomain (const ()) parameters permitted source)
-  let refinementResult = refinementDomainResult refined
-      result = refinedTriangulation refinementResult
-  unless (refinementComplete refinementResult) $
-    fail "encroached hull local refinement exhausted without a fixpoint"
-  assertEqual "encroached hull split count" (numVertices source) (numVertices result)
-  assertEqual
-    "encroached hull refinement retains cached frontier"
-    (Internal.triSeamFrontier source)
-    (Internal.triSeamFrontier result)
-
-  secondBuild <-
-    requireRight
-      "encroached hull join source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          (V.fromList [Point 4 0, Point 6 0, Point 6 1, Point 4 1])
-          V.empty
-      )
-  let second =
-        Internal.geometryOnlyPublication
-          (buildTriangulation secondBuild)
-  joined <-
-    requireRight
-      "encroached hull repeated join"
-      (joinSeparatedConstrained (\_ _ -> True) defaultRefinementParameters result second)
-  assertCdtValid
-    "encroached hull repeated join"
-    (constrainedSeamResultTriangulation joined)
-
-testConstrainedRefinement :: IO ()
-testConstrainedRefinement = do
-  cdtBuild <- requireRight "bounded domain" $ constrainedDelaunay
-    unitElementDefaults
-    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 4 2, Point 4 6])
-    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
-  let cdt = buildTriangulation cdtBuild
-      parameters = defaultRefinementParameters
-        { refineMaxAdditionalVertices = Just 80
-        , refineMaxArea = Just 3
-        , refineMaxRadiusEdgeRatio = Just 1.4
-        , refineExcludeOuterFaces = True
-        , refineKeepConstraintEdges = False
-        }
-  refined <- requireRight "constrained refinement" (refine id parameters cdt)
-  let result = refinedTriangulation refined
-  canonicalResult <- requireRight "canonical constrained refinement" (canonicalize result)
-  unless (refinementAddedVertices refined > 0) $ fail "constrained refinement inserted no Steiner points"
-  assertCdtValid "constrained refinement" result
-  unless (numConstraints result >= numConstraints cdt) $
-    fail "constraint splitting lost the constrained boundary"
-  assertEqual
-    "canonical publication preserves constraint segments"
-    (constraintSegments result)
-    (constraintSegments canonicalResult)
-
-  -- Refinement maintains the outer-region classification incrementally, from
-  -- the touched patch alone. That is a claim about what an insertion cannot
-  -- reach, so it is gated against an independent flood over the finished mesh
-  -- rather than trusted. The domain here is deliberately narrower than its
-  -- convex hull, so the excluded set is non-empty and the two can disagree.
-  notchBuild <- requireRight "notched domain" $ constrainedDelaunay
-    unitElementDefaults
-    (V.fromList [Point 0 0, Point 8 0, Point 8 8, Point 0 8, Point 13 4, Point 4 4])
-    (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
-  let notch = buildTriangulation notchBuild
-      notchMaximumEdgeLength = 6.2
-      notchParameters = defaultRefinementParameters
-        { refineMaxAdditionalVertices = Just 120
-        , refineMaxArea = Just 1.5
-        , refineMaxEdgeLength = Just notchMaximumEdgeLength
-        , refineExcludeOuterFaces = True
-        , refineKeepConstraintEdges = False
-        }
-  notchRefined <- requireRight "notched refinement" (refine id notchParameters notch)
-  let notchResult = refinedTriangulation notchRefined
-      maintained = sort (V.toList (refinementExcludedFaces notchRefined))
-      independent = sort (outerRegionFaces notchResult)
-      excludedEdgeLengthsSquared =
-        [ squaredDistanceWide
-            (vertexPoint notchResult fromVertex)
-            (vertexPoint notchResult toVertex)
-        | face <- maintained
-        , directed <- faceDirectedEdges notchResult face
-        , let (fromVertex, toVertex) =
-                undirectedEndpoints notchResult (asUndirected directed)
-        ]
-  unless (refinementAddedVertices notchRefined > 0) $ fail "notched refinement inserted no Steiner points"
-  unless (refinementComplete notchRefined) $ fail "notched maximum-edge refinement did not complete"
-  when (null independent) $ fail "notched domain produced no outer region: the gate is vacuous"
-  unless
-    (any (> notchMaximumEdgeLength * notchMaximumEdgeLength) excludedEdgeLengthsSquared)
-    (fail "notched fixture has no excluded maximum-edge violation")
-  assertEqual "incremental exclusion agrees with an independent flood" independent maintained
-  assertCdtValid "notched refinement" notchResult
-
-  annulusBuild <- requireRight "annular domain" $ constrainedDelaunay
-    unitElementDefaults
-    ( V.fromList
-        [ Point 0 0
-        , Point 12 0
-        , Point 12 12
-        , Point 0 12
-        , Point 4 4
-        , Point 8 4
-        , Point 8 8
-        , Point 4 8
-        ]
-    )
-    ( V.fromList
-        [ (0, 1)
-        , (1, 2)
-        , (2, 3)
-        , (3, 0)
-        , (4, 5)
-        , (5, 6)
-        , (6, 7)
-        , (7, 4)
-        ]
-    )
-  let annulus = buildTriangulation annulusBuild
-      annulusParameters :: Int -> Maybe Double -> RefinementParameters
-      annulusParameters budget maximumArea =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just budget
-          , refineMaxArea = maximumArea
-          , refineExcludeOuterFaces = True
-          , refineKeepConstraintEdges = False
-          }
-  annulusUnchanged <-
-    requireRight
-      "budget-zero annular refinement"
-      (refine id (annulusParameters 0 Nothing) annulus)
-  let initialAnnulusOutside = sort (outerRegionFaces annulus)
-      budgetZeroOutside =
-        sort (V.toList (refinementExcludedFaces annulusUnchanged))
-  assertEqual "annulus has one two-crossing hole" 2 (length initialAnnulusOutside)
-  assertEqual
-    "budget-zero refinement uses the authoritative annulus classification"
-    initialAnnulusOutside
-    budgetZeroOutside
-
-  annulusRefined <-
-    requireRight
-      "positive-budget annular refinement"
-      (refine id (annulusParameters 40 (Just 4)) annulus)
-  let refinedAnnulus = refinedTriangulation annulusRefined
-      maintainedAnnulus =
-        sort (V.toList (refinementExcludedFaces annulusRefined))
-      independentAnnulus = sort (outerRegionFaces refinedAnnulus)
-  unless (refinementAddedVertices annulusRefined > 0) $
-    fail "annular refinement inserted no Steiner points"
-  assertEqual
-    "incremental annulus exclusion agrees with authoritative barrier depth"
-    independentAnnulus
-    maintainedAnnulus
-  assertCdtValid "annular constrained refinement" refinedAnnulus
-
-  -- A tiny constraint can share a mesh with coordinates of vastly different
-  -- magnitude. Encroachment is discovered by a local cavity walk, so there is
-  -- no broad phase for the range to overflow; the pin is that the scaled
-  -- circumcenter and diametral predicates still produce exactly one vertex.
-  wideGridBuild <- requireRight "wide-grid constrained domain" $ constrainedDelaunay
-    unitElementDefaults
-    ( V.fromList
-        [ Point 0 0
-        , Point 2.0e-43 0
-        , Point 1.0e60 0
-        , Point 0 1.0e60
-        ] :: V.Vector (Point)
-    )
-    (V.singleton (0, 1))
-  wideGridRefined <-
-    requireRight
-      "wide-grid constrained refinement"
-      ( refine
-          id
-          defaultRefinementParameters
-            { refineMaxAdditionalVertices = Just 1
-            , refineMaxArea = Just 1.0e119
-            , refineKeepConstraintEdges = False
-            }
-          (buildTriangulation wideGridBuild)
-      )
-  assertEqual "wide-grid refinement count" 1 (refinementAddedVertices wideGridRefined)
-  assertCdtValid "wide-grid constrained refinement" (refinedTriangulation wideGridRefined)
-
--- | A local domain is a closed section, not a hopeful initial queue. Its
--- interface is exact, its protected faces survive point-for-point, and the
--- receipt contains no visit to the protected side.
-testCheckedLocalRefinement :: IO ()
-testCheckedLocalRefinement = do
-  built <-
-    requireRight
-      "checked local refinement source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          ( V.fromList
-              [ Point 0 0
-              , Point 4.1 0
-              , Point 8 0.2
-              , Point 0.1 4
-              , Point 4 4.2
-              , Point 8.1 4
-              ]
-          )
-          (V.singleton (2, 5))
-      )
-  let source = buildTriangulation built
-      permitted =
-        Set.fromList
-          [ face
-          | face <- innerFaces source
-          , faceCentroidX source face < 4.05
-          ]
-      interface =
-        Set.fromList
-          [ edge
-          | edge <- undirectedEdges source
-          , let (forward, backward) = directedPair edge
-                forwardFace = incidentFace source forward
-                backwardFace = incidentFace source backward
-          , forwardFace /= outerFace
-          , backwardFace /= outerFace
-          , Set.member forwardFace permitted /= Set.member backwardFace permitted
-          ]
-      protected = filter (`Set.notMember` permitted) (innerFaces source)
-      protectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint source) (faceVertices source face)))) protected
-      calmParameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 3
-          , refineMaxRadiusEdgeRatio = Nothing
-          , refineKeepConstraintEdges = True
-          }
-      crossingParameters = calmParameters{refineMaxArea = Just 1}
-  unless (not (Set.null permitted) && not (null protected) && not (Set.null interface)) $
-    fail "checked local refinement fixture did not form a nontrivial cover"
-  case refineWithinDomain id calmParameters{refineMaxAdditionalVertices = Nothing} permitted source of
-    Left RefinementDomainRequiresFiniteVertexBudget -> pure ()
-    outcome -> fail ("checked local refinement accepted an unbounded local budget: " <> either show (const "success") outcome)
-  refined <-
-    requireRight
-      "checked local refinement"
-      (refineWithinDomain id calmParameters permitted source)
-  let localResult = refinementDomainResult refined
-      target = refinedTriangulation localResult
-      targetProtectedSignatures = fmap (\face -> (face, sort (fmap (vertexPoint target) (faceVertices target face)))) protected
-      receipt = refinementDomainReceipt refined
-      finalPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces receipt))
-      expectedFinalInterfaceIncidence =
-        V.fromList
-          [ if Set.member sourceForwardFace permitted
-              then (edge, sourceBackwardFace, targetForwardFace)
-              else (edge, sourceForwardFace, targetBackwardFace)
-          | edge <- Set.toAscList interface
-          , let (forward, backward) = directedPair edge
-                sourceForwardFace = incidentFace source forward
-                sourceBackwardFace = incidentFace source backward
-                targetForwardFace = incidentFace target forward
-                targetBackwardFace = incidentFace target backward
-          ]
-      actualFinalInterfaceIncidence = refinementFinalInterfaceIncidence receipt
-  assertEqual "checked local protected face restriction" protectedSignatures targetProtectedSignatures
-  assertEqual
-    "checked local final permitted lineage"
-    permitted
-    finalPermitted
-  assertEqual
-    "checked local final interface incidence"
-    expectedFinalInterfaceIncidence
-    actualFinalInterfaceIncidence
-  assertEqual
-    "checked local final interface edge set"
-    interface
-    (Set.fromList [edge | (edge, _, _) <- V.toList actualFinalInterfaceIncidence])
-  traverse_
-    (\(edge, protectedFace, finalPermittedFace) ->
-       unless
-         ( Set.notMember protectedFace permitted
-             && Set.member finalPermittedFace finalPermitted
-         )
-         ( fail
-             ( "checked local final interface incidence misclassified "
-                 <> show (edge, protectedFace, finalPermittedFace)
-             )
-         )
-    )
-    actualFinalInterfaceIncidence
-  assertEqual "checked local protected visit receipt" V.empty (refinementVisitedProtectedFaces receipt)
-  assertEqual "checked local boundary crossing receipt" 0 (refinementAttemptedBoundaryCrossings receipt)
-  let closureStats = refinementValidationClosureStats receipt
-  unless
-    ( validationClosureFaces closureStats > 0
-        && validationClosureDirectedEdges closureStats > 0
-        && validationClosureVertices closureStats > 0
-    ) $
-    fail "checked local refinement did not record its selected validation closure"
-  assertEqual
-    "checked local refinement does not enumerate resident unboxed base pages"
-    0
-    (publicationUnboxedBasePageEnumerations (refinementPublicationStats receipt))
-  assertEqual
-    "checked local protected constraint restriction"
-    (constraintSegments source)
-    (constraintSegments target)
-  assertValid "checked local refinement" target
-  case refineWithinDomain id crossingParameters permitted source of
-    Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
-    Left obstruction -> fail ("checked local refinement returned the wrong crossing obstruction: " <> show obstruction)
-    Right _ -> fail "checked local refinement silently crossed its immutable interface"
-  case refineWithinDomain id calmParameters{refinePreserveConvexHull = False} permitted source of
-    Left RefinementDomainRequiresConvexHullPreservation -> pure ()
-    outcome -> fail ("checked local refinement accepted hull mutation: " <> either show (const "success") outcome)
-  case refineWithinDomain id calmParameters{refineKeepConstraintEdges = False} permitted source of
-    Left RefinementDomainRequiresConstraintPreservation -> pure ()
-    outcome -> fail ("checked local refinement accepted constraint mutation: " <> either show (const "success") outcome)
-  case refineWithinDomain id calmParameters{refineExcludeOuterFaces = True} permitted source of
-    Left RefinementDomainForbidsOuterFaceExclusion -> pure ()
-    outcome -> fail ("checked local refinement accepted outer-face exclusion: " <> either show (const "success") outcome)
-  wholeRefined <-
-    requireRight
-      "checked whole-section refinement"
-      ( refineWithinDomain
-          id
-          crossingParameters
-          (Set.fromList (innerFaces source))
-          source
-      )
-  let wholeResult = refinementDomainResult wholeRefined
-      wholeReceipt = refinementDomainReceipt wholeRefined
-  unless (refinementAddedVertices wholeResult > 0) $
-    fail "checked whole-section refinement did not improve its admitted section"
-  unless (not (V.null (refinementCreatedFaces wholeReceipt))) $
-    fail "checked whole-section refinement omitted semantically rewritten face slots"
-  unless
-    ( V.any
-        (\(FaceId raw) -> toInteger raw < toInteger (numFaces source))
-        (refinementCreatedFaces wholeReceipt)
-    ) $
-    fail "checked whole-section refinement omitted recycled face slots"
-  assertEqual
-    "checked whole-section final permitted lineage"
-    (Set.fromList (innerFaces (refinedTriangulation wholeResult)))
-    (Set.fromList (V.toList (refinementFinalPermittedFaces wholeReceipt)))
-  assertValid "checked whole-section refinement" (refinedTriangulation wholeResult)
- where
-  faceCentroidX
-    :: Triangulation mode vertex directed undirected face
-    -> FaceId
-    -> Double
-  faceCentroidX triangulation face =
-    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of
-      [] -> 0
-      points ->
-        sum [x | Point x _ <- points] / fromIntegral (length points)
-
--- | A maximum edge bound is a local geometric law, not a rendering preference:
--- a long, thin face is split along its longest edge even when its area is below
--- the minimum-area short-circuit. A budget-limited pass remains resumable;
--- once the longest edge is frozen at the local interface, a completed pass
--- returns the exact typed quality obstruction instead of crossing it.
-testMaximumEdgeLengthQuality :: IO ()
-testMaximumEdgeLengthQuality = do
-  built <-
-    requireRight
-      "maximum-edge long-thin source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          ( V.fromList
-              [ Point 0 0
-              , Point 10 0
-              , Point 10 1
-              , Point 0 1
-              ]
-          )
-          (V.fromList [(0, 1), (1, 2), (2, 3), (3, 0)])
-      )
-  let source = buildTriangulation built
-      allPermitted = Set.fromList (innerFaces source)
-      parameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 1
-          , refineMinArea = Just 6
-          , refineMaxEdgeLength = Just 10.01
-          , refineMaxRadiusEdgeRatio = Nothing
-          , refineKeepConstraintEdges = True
-          }
-  partial <-
-    requireRight
-      "maximum-edge budget-limited refinement"
-      (refineWithinDomain id parameters allPermitted source)
-  let partialResult = refinementDomainResult partial
-      partialTarget = refinedTriangulation partialResult
-      partialReceipt = refinementDomainReceipt partial
-      resumedPermitted = Set.fromList (V.toList (refinementFinalPermittedFaces partialReceipt))
-  unless (refinementAddedVertices partialResult > 0) $
-    fail "maximum-edge refinement did not split the long edge before the area floor"
-  when (refinementComplete partialResult) $
-    fail "maximum-edge budget-limited refinement reported completion"
-  assertValid "maximum-edge budget-limited refinement" partialTarget
-  resumed <-
-    requireRight
-      "maximum-edge resumed refinement"
-      ( refineWithinDomain
-          id
-          parameters{refineMaxAdditionalVertices = Just 8}
-          resumedPermitted
-          partialTarget
-      )
-  let resumedResult = refinementDomainResult resumed
-      resumedReceipt = refinementDomainReceipt resumed
-  unless (refinementComplete resumedResult) $
-    fail "maximum-edge resumed refinement did not drain its local worklist"
-  assertMaximumEdgeLength
-    "maximum-edge resumed refinement"
-    10.01
-    (refinedTriangulation resumedResult)
-    (V.toList (refinementFinalPermittedFaces resumedReceipt))
-
-  case
-      refine
-        id
-        parameters
-          { refineMaxAdditionalVertices = Just 8
-          , refineMaxEdgeLength = Just 9
-          }
-        source of
-    Left (RefinementOversizedEdge _ edge actual bound)
-      | isConstraintEdge source edge && actual > bound -> pure ()
-    Left obstruction ->
-      fail ("maximum-edge global audit returned the wrong obstruction: " <> show obstruction)
-    Right _ -> fail "maximum-edge global refinement accepted an oversized kept constraint"
-
-  case innerFaces source of
-    firstFace : _ -> do
-      let permitted = Set.singleton firstFace
-          interface =
-            Set.fromList
-              [ edge
-              | edge <- undirectedEdges source
-              , let (forward, backward) = directedPair edge
-                    forwardFace = incidentFace source forward
-                    backwardFace = incidentFace source backward
-              , forwardFace /= outerFace
-              , backwardFace /= outerFace
-              , Set.member forwardFace permitted /= Set.member backwardFace permitted
-              ]
-      let blocked =
-            refineWithinDomain
-              id
-              parameters{refineMaxAdditionalVertices = Just 8}
-              permitted
-              source
-      case blocked of
-        Left (RefinementOversizedEdge face edge actual bound)
-          | Set.member face permitted
-              && Set.member edge interface
-              && actual > bound -> pure ()
-        Left obstruction ->
-          fail ("maximum-edge interface returned the wrong obstruction: " <> show obstruction)
-        Right _ -> fail "maximum-edge interface was silently crossed or accepted"
-    [] -> fail "maximum-edge source has no inner face"
-
--- | A circumcenter can be inside the admitted face while its legalization
--- cavity also meets the immutable interface. That is a true crossing demand,
--- not the boundary-limited locator read discharged by the local interpreter.
-testLocalRefinementRejectsTrueInterfaceCrossing :: IO ()
-testLocalRefinementRejectsTrueInterfaceCrossing = do
-  built <-
-    requireRight
-      "deterministic true interface crossing source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          ( V.fromList
-              [ Point 0 0
-              , Point 2 0
-              , Point 1 (sqrt 3)
-              , Point (-1) (sqrt 3)
-              ]
-          )
-          V.empty
-      )
-  let source = buildTriangulation built
-      equilateralFacePoints = sort [Point 0 0, Point 2 0, Point 1 (sqrt 3)]
-      permittedFace =
-        List.find
-          ( \face ->
-              sort (fmap (vertexPoint source) (faceVertices source face))
-                == equilateralFacePoints
-          )
-          (innerFaces source)
-  assertEqual "deterministic crossing source has two inner faces" 2 (numInnerFaces source)
-  case permittedFace of
-    Nothing -> fail "deterministic crossing source omitted its equilateral face"
-    Just face -> do
-      let permitted = Set.singleton face
-          interface =
-            Set.fromList
-              [ edge
-              | edge <- undirectedEdges source
-              , let (forward, backward) = directedPair edge
-                    forwardFace = incidentFace source forward
-                    backwardFace = incidentFace source backward
-              , forwardFace /= outerFace
-              , backwardFace /= outerFace
-              , Set.member forwardFace permitted /= Set.member backwardFace permitted
-              ]
-          parameters =
-            defaultRefinementParameters
-              { refineMaxAdditionalVertices = Just 1
-              , refineMaxArea = Just 0.5
-              , refineMaxRadiusEdgeRatio = Nothing
-              , refineKeepConstraintEdges = True
-              }
-      assertEqual
-        "deterministic crossing source has one interface edge"
-        1
-        (Set.size interface)
-      case refineWithinDomain id parameters permitted source of
-        Left (RefinementDomainWouldCrossInterface _ _) -> pure ()
-        Left obstruction ->
-          fail
-            ( "deterministic true interface crossing returned the wrong obstruction: "
-                <> show obstruction
-            )
-        Right _ ->
-          fail "deterministic true interface crossing was silently accepted"
-
-testRepeatedBoundaryAdjacentRefinement :: IO ()
-testRepeatedBoundaryAdjacentRefinement = do
-  built <-
-    requireRight
-      "repeated boundary-adjacent constrained source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          ( V.fromList
-              [ Point 0 0
-              , Point 4 0
-              , Point 8 0
-              , Point 12 0
-              , Point 0 4
-              , Point 4 4
-              , Point 8 4
-              , Point 12 4
-              ]
-          )
-          (V.singleton (2, 6))
-      )
-  let source = buildTriangulation built
-      sourcePermitted = leftSection source
-      fullParameters =
-        defaultRefinementParameters
-          { refineMaxAdditionalVertices = Just 20
-          , refineMaxArea = Just 1
-          , refineMaxRadiusEdgeRatio = Nothing
-          , refineKeepConstraintEdges = True
-          }
-  fullRefinement <-
-    requireRight
-      "full boundary-adjacent refinement"
-      (refineWithinDomain id fullParameters sourcePermitted source)
-  let fullResult = refinementDomainResult fullRefinement
-      fullTarget = refinedTriangulation fullResult
-      fullReceipt = refinementDomainReceipt fullRefinement
-  unless (refinementAddedVertices fullResult > 0) $
-    fail "full boundary-adjacent refinement did not refine its admitted section"
-  unless (refinementInterfaceBoundaryReads fullReceipt > 0) $
-    fail "boundary-adjacent refinement did not report its immutable interface read"
-  assertEqual
-    "boundary-adjacent crossing attempts"
-    0
-    (refinementAttemptedBoundaryCrossings fullReceipt)
-  assertEqual
-    "full boundary-adjacent constraint restriction"
-    (constraintSegments source)
-    (constraintSegments fullTarget)
-  assertEqual
-    "completed boundary-adjacent refinement is idempotent"
-    True
-    (refinementComplete fullResult)
-
-  let progressParameters = fullParameters{refineMaxAdditionalVertices = Just 1}
-  progressFirstRefinement <-
-    requireRight
-      "first bounded boundary-adjacent refinement"
-      (refineWithinDomain id progressParameters sourcePermitted source)
-  let progressFirstResult = refinementDomainResult progressFirstRefinement
-      progressFirstTarget = refinedTriangulation progressFirstResult
-      progressFirstReceipt = refinementDomainReceipt progressFirstRefinement
-      progressPermitted =
-        Set.fromList (V.toList (refinementFinalPermittedFaces progressFirstReceipt))
-  unless (refinementAddedVertices progressFirstResult > 0) $
-    fail "first bounded boundary-adjacent refinement did not refine its admitted section"
-  when (refinementComplete progressFirstResult) $
-    fail "first bounded boundary-adjacent refinement unexpectedly reached a fixpoint"
-  progressSecondRefinement <-
-    requireRight
-      "repeated boundary-adjacent refinement"
-      ( refineWithinDomain
-          id
-          progressParameters
-          progressPermitted
-          progressFirstTarget
-      )
-  let progressSecondResult = refinementDomainResult progressSecondRefinement
-      progressSecondTarget = refinedTriangulation progressSecondResult
-      progressSecondReceipt = refinementDomainReceipt progressSecondRefinement
-  unless (refinementAddedVertices progressSecondResult > 0) $
-    fail "repeated boundary-adjacent refinement did not retain its dynamic join-face support"
-  assertEqual
-    "repeated boundary-adjacent protected visits"
-    V.empty
-    (refinementVisitedProtectedFaces progressSecondReceipt)
-  assertEqual
-    "repeated boundary-adjacent constraint restriction"
-    (constraintSegments progressFirstTarget)
-    (constraintSegments progressSecondTarget)
-  assertValid "repeated boundary-adjacent refinement" progressSecondTarget
- where
-  leftSection
-    :: Triangulation mode vertex directed undirected face
-    -> Set.Set FaceId
-  leftSection triangulation =
-    Set.fromList
-      [ face
-      | face <- innerFaces triangulation
-      , faceCentroidX triangulation face < 8
-      ]
-  faceCentroidX
-    :: Triangulation mode vertex directed undirected face
-    -> FaceId
-    -> Double
-  faceCentroidX triangulation face =
-    case fmap (vertexPoint triangulation) (faceVertices triangulation face) of
-      [] -> 0
-      points ->
-        sum [x | Point x _ <- points] / fromIntegral (length points)
-
-testFaceComponentsAndBoundaries :: IO ()
-testFaceComponentsAndBoundaries = do
-  emptyBuild <- requirePointBuild "empty region components" []
-  collinearBuild <-
-    requirePointBuild
-      "collinear region components"
-      [Point 0 0, Point 1 0, Point 2 0]
-  assertEqual
-    "empty mesh has no face components"
-    ([] :: [(Bool, FaceComponent)])
-    (faceComponents (buildTriangulation emptyBuild) (const True))
-  assertEqual
-    "collinear mesh has no face components"
-    ([] :: [(Bool, FaceComponent)])
-    (faceComponents (buildTriangulation collinearBuild) (const True))
-
-  triangle <-
-    regionMeshFromPoints
-      "single triangle region"
-      [Point 0 0, Point 2 0, Point 0 2]
-  triangleBoundary <-
-    requireComponentBoundary "single triangle" True triangle (const True)
-  assertBoundaryShape "single triangle" triangle 3 [] triangleBoundary
-
-  square <-
-    regionMeshFromPoints
-      "uniform square region"
-      [Point 0 0, Point 2 0, Point 2 2, Point 0 2]
-  squareComponent <-
-    requireLabelledComponent
-      "uniform square component"
-      True
-      (faceComponents square (const True))
-  assertEqual
-    "component provenance mismatch is typed before DCEL lookup"
-    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))
-    (componentBoundary triangle squareComponent)
-  assertEqual
-    "component loop provenance mismatch is typed before DCEL lookup"
-    (Left (BoundaryComponentFaceOutOfRange (FaceId 2) 2))
-    (componentBoundaryLoops triangle squareComponent)
-  squareBoundary <- requireRight "uniform square boundary" (componentBoundary square squareComponent)
-  squareLoops <-
-    requireRight
-      "uniform square oriented boundary loops"
-      (componentBoundaryLoops square squareComponent)
-  repeatedSquareBoundary <-
-    requireRight "repeated uniform square boundary" (componentBoundary square squareComponent)
-  assertEqual "boundary extraction is deterministic" squareBoundary repeatedSquareBoundary
-  assertEqual
-    "ordinary oriented loops agree with strict boundary"
-    (regionBoundaryOuterLoop squareBoundary :| regionBoundaryHoleLoops squareBoundary)
-    squareLoops
-  assertEqual
-    "uniform square drops its Delaunay diagonal"
-    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
-    (loopPointSet square (regionBoundaryOuterLoop squareBoundary))
-  assertBoundaryShape "uniform square" square 4 [] squareBoundary
-
-  let splitComponents =
-        faceComponents square (regionFaceSatisfies square (\point -> pointX point < 1))
-  assertEqual "split square component count" 2 (length splitComponents)
-  traverse_
-    (\(_, component) -> do
-       boundary <- requireRight "split square boundary" (componentBoundary square component)
-       assertBoundaryShape "split square component" square 3 [] boundary)
-    splitComponents
-
-  collinearHull <-
-    regionMeshFromPoints
-      "collinear hull simplification"
-      [ Point 0 0
-      , Point 1 0
-      , Point 2 0
-      , Point 2 2
-      , Point 0 2
-      , Point 1 1
-      ]
-  collinearBoundary <-
-    requireComponentBoundary "collinear hull" True collinearHull (const True)
-  assertEqual
-    "exact simplification drops a redundant hull site"
-    (Set.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
-    (loopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))
-  assertEqual
-    "resident boundary retains every hull edge endpoint"
-    (Set.fromList [Point 0 0, Point 1 0, Point 2 0, Point 2 2, Point 0 2])
-    (residentLoopPointSet collinearHull (regionBoundaryOuterLoop collinearBoundary))
-  assertEqual
-    "resident boundary follows exact DCEL edges"
-    True
-    (residentBoundaryIsEdgeCycle collinearHull (regionBoundaryOuterLoop collinearBoundary))
-
-  lShape <- regionMesh "concave L region" 2 2
-  let inL = regionFaceSatisfies lShape (\(Point x y) -> not (x > 1 && y > 1))
-  lBoundary <- requireComponentBoundary "concave L" True lShape inL
-  assertEqual
-    "concave L boundary"
-    ( Set.fromList
-        [ Point 0 0
-        , Point 2 0
-        , Point 2 1
-        , Point 1 1
-        , Point 1 2
-        , Point 0 2
-        ]
-    )
-    (loopPointSet lShape (regionBoundaryOuterLoop lBoundary))
-  assertBoundaryShape "concave L" lShape 6 [] lBoundary
-
-  annulus <- regionMesh "annulus region" 3 3
-  let outsideCenter =
-        regionFaceSatisfies annulus (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))
-  annulusBoundary <-
-    requireComponentBoundary "annulus outer" True annulus outsideCenter
-  assertBoundaryShape "annulus" annulus 4 [4] annulusBoundary
-
-  twoHoles <- regionMesh "ordered hole regions" 5 3
-  let outsideTwoCells =
-        regionFaceSatisfies twoHoles $ \(Point x y) ->
-          let cell = (floor x :: Int, floor y :: Int)
-           in cell /= (1, 1) && cell /= (3, 1)
-  twoHoleBoundary <-
-    requireComponentBoundary "two-hole outer" True twoHoles outsideTwoCells
-  assertBoundaryShape "two-hole" twoHoles 4 [4, 4] twoHoleBoundary
-
-  disconnected <- regionMesh "disconnected equal labels" 3 1
-  let outsideMiddle =
-        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)
-      disconnectedComponents = faceComponents disconnected outsideMiddle
-      equalLabelComponents =
-        [component | (True, component) <- disconnectedComponents]
-  assertEqual "equal labels remain two disconnected components" 2 (length equalLabelComponents)
-  assertEqual
-    "face component order is deterministic"
-    disconnectedComponents
-    (faceComponents disconnected outsideMiddle)
-
-  pinched <- regionMesh "pinched region" 3 3
-  let pinchedSelection =
-        regionFaceSatisfies pinched $ \(Point x y) ->
-          let cell = (floor x :: Int, floor y :: Int)
-           in cell /= (0, 0) && cell /= (1, 1)
-  pinchedComponent <-
-    requireLabelledComponent
-      "pinched selected component"
-      True
-      (faceComponents pinched pinchedSelection)
-  pinchedLoops <-
-    requireRight
-      "pinched oriented boundary loops"
-      (componentBoundaryLoops pinched pinchedComponent)
-  let pinchedLoopList = toList pinchedLoops
-  assertEqual "pinched simple loop count" 2 (length pinchedLoopList)
-  assertEqual
-    "pinched simple loop vertex counts"
-    [4, 6]
-    (sort (fmap (length . boundaryLoopVertices) pinchedLoopList))
-  assertEqual
-    "pinched loop orientations"
-    [BoundaryCounterClockwise, BoundaryClockwise]
-    (sort (fmap boundaryLoopOrientation pinchedLoopList))
-  traverse_
-    (\loop -> do
-       let loopVertexIds = toList (boundaryLoopVertices loop)
-       assertEqual
-         "pinched loop has no repeated vertex"
-         (length loopVertexIds)
-         (Set.size (Set.fromList loopVertexIds))
-       assertLoopWinding
-         "pinched loop winding agrees with orientation"
-         (case boundaryLoopOrientation loop of
-            BoundaryCounterClockwise -> GT
-            BoundaryClockwise -> LT)
-         pinched
-         loop)
-    pinchedLoopList
-  case componentBoundary pinched pinchedComponent of
-    Left (BoundaryPinch vertex firstEdge secondEdge) -> do
-      assertEqual "pinch vertex" (Point 1 1) (vertexPoint pinched vertex)
-      when (firstEdge == secondEdge) $
-        fail ("pinch repeated one outgoing edge: " <> show firstEdge)
-    other -> fail ("pinched boundary produced " <> show other)
-
-testAlphaFaceFiltration :: IO ()
-testAlphaFaceFiltration = do
-  triangle <-
-    regionMeshFromPoints
-      "alpha right triangle"
-      [Point 0 0, Point 2 0, Point 0 2]
-  face <- case innerFaces triangle of
-    [singleFace] -> pure singleFace
-    faces -> fail ("alpha fixture faces: " <> show faces)
-  exactThreshold <- requireRight "exact alpha threshold" (mkRadiusSquared 2)
-  lowerThreshold <- requireRight "lower alpha threshold" (mkRadiusSquared 1.999)
-  zeroThreshold <- requireRight "zero alpha threshold" (mkRadiusSquared 0)
-  assertEqual "zero alpha threshold excludes face" False
-    (alphaShapeContainsFace zeroThreshold triangle face)
-  assertEqual "closed alpha threshold includes equality" True (alphaShapeContainsFace exactThreshold triangle face)
-  assertEqual "lower alpha threshold excludes face" False (alphaShapeContainsFace lowerThreshold triangle face)
-  assertEqual "outer face is absent from alpha filtration" False (alphaShapeContainsFace exactThreshold triangle outerFace)
-
-  let roundedPoints =
-        ( Point (-20) (-20)
-        , Point (-19) (-13)
-        , Point (-2) (-19)
-        )
-      (roundedFirst, roundedSecond, roundedThird) = roundedPoints
-      roundedRadiusSquared = 84.5
-  roundedTriangle <-
-    regionMeshFromPoints
-      "rounded alpha equality"
-      [roundedFirst, roundedSecond, roundedThird]
-  roundedFace <- case innerFaces roundedTriangle of
-    [singleFace] -> pure singleFace
-    faces -> fail ("rounded alpha fixture faces: " <> show faces)
-  roundedThreshold <-
-    requireRight
-      "rounded exact alpha threshold"
-      (mkRadiusSquared roundedRadiusSquared)
-  roundedLowerThreshold <-
-    requireRight
-      "rounded lower alpha threshold"
-      (mkRadiusSquared (roundedRadiusSquared - encodeFloat 1 (-46)))
-  assertEqual
-    "closed alpha equality is exact"
-    True
-    (alphaShapeContainsFace roundedThreshold roundedTriangle roundedFace)
-  assertEqual
-    "one binary64 step below exact alpha equality is excluded"
-    False
-    (alphaShapeContainsFace roundedLowerThreshold roundedTriangle roundedFace)
-
-  alphaBoundary <-
-    requireComponentBoundary
-      "alpha"
-      True
-      triangle
-      (alphaShapeContainsFace exactThreshold triangle)
-  assertBoundaryShape "alpha component" triangle 3 [] alphaBoundary
-
-  annulus <-
-    regionMeshFromPoints
-      "alpha annulus"
-      (ringPoints 4 32 <> ringPoints 2 16 <> ringPoints 3 24)
-  annulusThreshold <-
-    requireRight "alpha annulus threshold" (mkRadiusSquared 0.4)
-  annulusBoundary <-
-    requireComponentBoundary
-      "alpha annulus"
-      True
-      annulus
-      (alphaShapeContainsFace annulusThreshold annulus)
-  assertEqual
-    "alpha annulus hole count"
-    1
-    (length (regionBoundaryHoleLoops annulusBoundary))
-  assertLoopWinding
-    "alpha annulus outer winding"
-    GT
-    annulus
-    (regionBoundaryOuterLoop annulusBoundary)
-  traverse_
-    (assertLoopWinding "alpha annulus hole winding" LT annulus)
-    (regionBoundaryHoleLoops annulusBoundary)
-  traverse_
-    (\(label, value, failure) ->
-       assertEqual label (Left failure) (mkRadiusSquared value))
-    [ ("negative alpha threshold refusal", -1, NegativeRadiusSquared (-1))
-    , ("NaN alpha threshold refusal", 0 / 0, NonFiniteRadiusSquared ValueNaN)
-    , ("positive-infinite alpha threshold refusal", 1 / 0, NonFiniteRadiusSquared ValuePositiveInfinity)
-    , ("negative-infinite alpha threshold refusal", (-1) / 0, NonFiniteRadiusSquared ValueNegativeInfinity)
-    ]
-
-ringPoints :: Double -> Int -> [Point]
-ringPoints radius count =
-  [ Point (radius * cos angle) (radius * sin angle)
-  | index <- [0 .. count - 1]
-  , let angle = 2 * pi * fromIntegral index / fromIntegral count
-  ]
-
-regionMesh :: String -> Int -> Int -> IO NativeMesh
-regionMesh label widthInCells heightInCells =
-  regionMeshFromPoints
-    label
-    [ Point (fromIntegral x) (fromIntegral y)
-    | y <- [0 .. heightInCells]
-    , x <- [0 .. widthInCells]
-    ]
-
-regionMeshFromPoints :: String -> [Point] -> IO NativeMesh
-regionMeshFromPoints label points =
-  buildTriangulation <$> requirePointBuild label points
-
-regionFaceCentroid
-  :: Triangulation mode vertex directed undirected face
-  -> FaceId
-  -> Maybe Point
-regionFaceCentroid triangulation face =
-  (\(first, second, third) ->
-     centroid
-       (vertexPoint triangulation first)
-       (vertexPoint triangulation second)
-       (vertexPoint triangulation third))
-    <$> Dcel.innerFaceVertices triangulation face
-
-regionFaceSatisfies
-  :: Triangulation mode vertex directed undirected face
-  -> (Point -> Bool)
-  -> FaceId
-  -> Bool
-regionFaceSatisfies triangulation predicate =
-  maybe False predicate . regionFaceCentroid triangulation
-
-requireLabelledComponent
-  :: (Eq label, Show label)
-  => String
-  -> label
-  -> [(label, FaceComponent)]
-  -> IO FaceComponent
-requireLabelledComponent label expected components =
-  case [component | (actual, component) <- components, actual == expected] of
-    [component] -> pure component
-    matches ->
-      fail
-        ( label
-            <> ": expected one component for label "
-            <> show expected
-            <> ", got "
-            <> show (length matches)
-        )
-
-requireComponentBoundary
-  :: (Eq label, Show label)
-  => String
-  -> label
-  -> Triangulation mode vertex directed undirected face
-  -> (FaceId -> label)
-  -> IO RegionBoundary
-requireComponentBoundary label expected triangulation labelFace = do
-  component <-
-    requireLabelledComponent
-      (label <> " component")
-      expected
-      (faceComponents triangulation labelFace)
-  requireRight (label <> " boundary") (componentBoundary triangulation component)
-
-loopPointSet
-  :: Triangulation mode vertex directed undirected face
-  -> BoundaryLoop
-  -> Set.Set Point
-loopPointSet triangulation =
-  Set.fromList
-    . fmap (vertexPoint triangulation)
-    . toList
-    . boundaryLoopVertices
-
-residentLoopPointSet
-  :: Triangulation mode vertex directed undirected face
-  -> BoundaryLoop
-  -> Set.Set Point
-residentLoopPointSet triangulation =
-  Set.fromList
-    . fmap (vertexPoint triangulation)
-    . toList
-    . boundaryLoopResidentVertices
-
-residentBoundaryIsEdgeCycle
-  :: Triangulation mode vertex directed undirected face
-  -> BoundaryLoop
-  -> Bool
-residentBoundaryIsEdgeCycle triangulation loop =
-  let first :| remaining = boundaryLoopResidentVertices loop
-      adjacentPairs = zip (first : remaining) (remaining <> [first])
-   in all
-        (\(fromVertex, toVertex) ->
-           any
-             ((== toVertex) . destination triangulation)
-             (vertexOutgoingEdges triangulation fromVertex))
-        adjacentPairs
-
-assertLoopWinding
-  :: String
-  -> Ordering
-  -> Triangulation mode vertex directed undirected face
-  -> BoundaryLoop
-  -> IO ()
-assertLoopWinding label expected triangulation loop =
-  let first :| remaining = fmap (vertexPoint triangulation) (boundaryLoopVertices loop)
-      points = first : remaining
-      twiceArea =
-        sum
-          ( zipWith
-              (\(Point ax ay) (Point bx by) -> ax * by - ay * bx)
-              points
-              (remaining <> [first])
-          )
-   in assertEqual label expected (compare twiceArea 0)
-
-assertBoundaryShape
-  :: String
-  -> Triangulation mode vertex directed undirected face
-  -> Int
-  -> [Int]
-  -> RegionBoundary
-  -> IO ()
-assertBoundaryShape label triangulation outerVertexCount holeVertexCounts boundary = do
-  assertEqual
-    (label <> " outer orientation")
-    BoundaryCounterClockwise
-    (boundaryLoopOrientation (regionBoundaryOuterLoop boundary))
-  assertEqual (label <> " outer vertex count") outerVertexCount
-    (length (boundaryLoopVertices (regionBoundaryOuterLoop boundary)))
-  assertLoopWinding (label <> " outer winding") GT triangulation
-    (regionBoundaryOuterLoop boundary)
-  let holes = regionBoundaryHoleLoops boundary
-  assertEqual (label <> " hole count") (length holeVertexCounts) (length holes)
-  traverse_
-    (\(vertexCount, hole) -> do
-       assertEqual
-         (label <> " hole orientation")
-         BoundaryClockwise
-         (boundaryLoopOrientation hole)
-       assertEqual (label <> " hole vertex count") vertexCount
-         (length (boundaryLoopVertices hole))
-       assertLoopWinding (label <> " hole winding") LT triangulation hole)
-    (zip holeVertexCounts holes)
-
-testTraversal :: IO ()
-testTraversal = do
-  built <- requirePointBuild "traversal" [Point (-3) 0, Point (-1) (-2), Point (-1) 2, Point 1 (-2), Point 1 2, Point 3 0]
-  forwardStart <- requireQueryPoint "forward traversal start" (Point (-4) 0)
-  forwardEnd <- requireQueryPoint "forward traversal end" (Point 4 0)
-  let triangulation = buildTriangulation built
-      forward = lineIntersections triangulation forwardStart forwardEnd
-      backward = lineIntersections triangulation forwardEnd forwardStart
-  when (null forward) $ fail "ordered line traversal crossed nothing"
-  assertEqual
-    "reversing the segment reverses crossing order and orientation"
-    (map reverseTraversalEvent forward)
-    (reverse backward)
-  interiorEnd <- requireQueryPoint "outside traversal interior end" (Point 0.25 0.5)
-  let outsideToInterior = lineIntersections triangulation forwardStart interiorEnd
-      interiorToOutside = lineIntersections triangulation interiorEnd forwardStart
-  when (null outsideToInterior) $ fail "outside-to-interior traversal crossed nothing"
-  assertEqual
-    "endpoint-directed outside traversal preserves order and orientation"
-    (map reverseTraversalEvent outsideToInterior)
-    (reverse interiorToOutside)
-  missFrom <- requireQueryPoint "outside traversal miss from" (Point (-4) 4)
-  missTo <- requireQueryPoint "outside traversal miss to" (Point 4 4)
-  assertEqual
-    "outside segment missing the hull reports no intersections"
-    []
-    (lineIntersections triangulation missFrom missTo)
-  circleEdges <- Set.fromList <$> requireRight "circle edge query" (edgesInCircle triangulation (Point 0 0) 4)
-  let bruteCircle = Set.fromList
-        [ edge
-        | edge <- undirectedEdges triangulation
-        , let (a, b) = undirectedEndpoints triangulation edge
-        , segmentDistanceSquared (vertexPoint triangulation a) (vertexPoint triangulation b) (Point 0 0) <= 4
-        ]
-  assertEqual "circle edge flood" bruteCircle circleEdges
-  assertEqual
-    "negative circle metric refusal"
-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
-    (circleMetric (Point 0 0 :: Point) (-1))
-  assertEqual
-    "negative circle edge-query refusal"
-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
-    (edgesInCircle triangulation (Point 0 0) (-1))
-  assertEqual
-    "negative circle vertex-query refusal"
-    (Left (InvalidCircleRadius (NegativeRadiusSquared (-1))))
-    (verticesInCircle triangulation (Point 0 0) (-1))
-  assertEqual
-    "NaN circle metric refusal"
-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNaN)))
-    (circleMetric (Point 0 0 :: Point) (0 / 0))
-  assertEqual
-    "NaN circle center-x refusal"
-    (Left (InvalidCircleCenter (InvalidPointX CoordinateNaN)))
-    (circleMetric (Point (0 / 0) 0 :: Point) 1)
-  assertEqual
-    "positive-infinite circle center-y refusal"
-    (Left (InvalidCircleCenter (InvalidPointY CoordinateInfinite)))
-    (edgesInCircle triangulation (Point 0 (1 / 0)) 1)
-  assertEqual
-    "negative-infinite circle center-x refusal"
-    (Left (InvalidCircleCenter (InvalidPointX CoordinateInfinite)))
-    (verticesInCircle triangulation (Point ((-1) / 0) 0) 1)
-  assertEqual
-    "infinite circle edge-query refusal"
-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValuePositiveInfinity)))
-    (edgesInCircle triangulation (Point 0 0) (1 / 0))
-  assertEqual
-    "negative-infinite circle vertex-query refusal"
-    (Left (InvalidCircleRadius (NonFiniteRadiusSquared ValueNegativeInfinity)))
-    (verticesInCircle triangulation (Point 0 0) ((-1) / 0))
-  rectangleVertices <-
-    Set.fromList
-      <$> requireRight
-        "rectangle query"
-        (verticesInRectangle triangulation (Point (-1.1) (-2.1)) (Point 1.1 2.1))
-  let bruteVertices = Set.fromList
-        [ vertex
-        | vertex <- vertices triangulation
-        , let Point x y = vertexPoint triangulation vertex
-        , x >= (-1.1), x <= 1.1, y >= (-2.1), y <= 2.1
-        ]
-  assertEqual "rectangle vertex flood" bruteVertices rectangleVertices
-
-testRandomizedConstruction :: IO ()
-testRandomizedConstruction = do
-  forM_ [0 .. 31] $ \index -> do
-    let points = randomPoints (0x9e37_79b9 + fromIntegral index) (40 + index * 7)
-    built <- requirePointBuild ("random build " <> show index) points
-    let triangulation = buildTriangulation built
-    assertValid ("random build " <> show index) triangulation
-    assertEqual "random input mapping" (length points) (sizeofPrimArray (buildInputVertices built))
-
-testErrors :: IO ()
-testErrors = do
-  assertEqual
-    "NaN query x refusal"
-    (Left (InvalidPointX CoordinateNaN))
-    (mkQueryPoint (Point (0 / 0) 0))
-  assertEqual
-    "infinite query y refusal"
-    (Left (InvalidPointY CoordinateInfinite))
-    (mkQueryPoint (Point 0 (1 / 0)))
-  assertEqual
-    "negative-infinite query x refusal"
-    (Left (InvalidPointX CoordinateInfinite))
-    (mkQueryPoint (Point ((-1) / 0) 0))
-  case delaunay unitElementDefaults (V.singleton (Point (0 / 0) 0 :: Point)) of
-    Left (InvalidCoordinate (Just 0) _ CoordinateNaN) -> pure ()
-    Left other -> fail ("NaN insertion result: " <> show other)
-    Right built ->
-      fail
-        ( "NaN insertion result: built a triangulation of "
-            <> show (numVertices (buildTriangulation built))
-            <> " vertices"
-        )
-  expectBuildFailure
-    "NaN minimum angle"
-    (RefinementMinimumAngleNotFinite ValueNaN)
-    (withMinimumAngle (0 / 0 :: Double) defaultRefinementParameters)
-  expectBuildFailure
-    "infinite minimum angle"
-    (RefinementMinimumAngleNotFinite ValuePositiveInfinity)
-    (withMinimumAngle (1 / 0 :: Double) defaultRefinementParameters)
-  expectBuildFailure
-    "out-of-range minimum angle"
-    (RefinementMinimumAngleOutOfRange 61)
-    (withMinimumAngle (61 :: Double) defaultRefinementParameters)
-  expectBuildFailure
-    "minimum angle whose derived ratio overflows"
-    (RefinementMinimumAngleDerivedRatioNotFinite ValuePositiveInfinity)
-    (withMinimumAngle (encodeFloat 1 (-1074) :: Double) defaultRefinementParameters)
-  square <- requirePointBuild "area validation square" [Point 0 0, Point 1 0, Point 1 1, Point 0 1]
-  let refineWith parameters =
-        refine id parameters (buildTriangulation square)
-      refineWithArea area =
-        refineWith defaultRefinementParameters{refineMaxArea = Just area}
-  expectBuildFailure
-    "negative refinement vertex budget"
-    (RefinementMaximumAdditionalVerticesNegative (-1))
-    (refineWith defaultRefinementParameters{refineMaxAdditionalVertices = Just (-1)})
-  expectBuildFailure
-    "infinite minimum area"
-    (RefinementMinimumAreaNotFinite ValuePositiveInfinity)
-    (refineWith defaultRefinementParameters{refineMinArea = Just (1 / 0)})
-  expectBuildFailure
-    "negative minimum area"
-    (RefinementMinimumAreaNegative (-1))
-    (refineWith defaultRefinementParameters{refineMinArea = Just (-1)})
-  expectBuildFailure
-    "NaN maximum area"
-    (RefinementMaximumAreaNotFinite ValueNaN)
-    (refineWithArea (0 / 0))
-  expectBuildFailure
-    "infinite maximum area"
-    (RefinementMaximumAreaNotFinite ValuePositiveInfinity)
-    (refineWithArea (1 / 0))
-  expectBuildFailure
-    "zero maximum area"
-    (RefinementMaximumAreaNotPositive 0)
-    (refineWithArea 0)
-  expectBuildFailure
-    "negative maximum area"
-    (RefinementMaximumAreaNotPositive (-1))
-    (refineWithArea (-1))
-  expectBuildFailure
-    "infinite maximum radius/edge ratio"
-    (RefinementMaximumRadiusEdgeRatioNotFinite ValuePositiveInfinity)
-    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just (1 / 0)})
-  expectBuildFailure
-    "non-positive maximum radius/edge ratio"
-    (RefinementMaximumRadiusEdgeRatioNotPositive 0)
-    (refineWith defaultRefinementParameters{refineMaxRadiusEdgeRatio = Just 0})
-  expectBuildFailure
-    "infinite maximum edge length"
-    (RefinementMaximumEdgeLengthNotFinite ValuePositiveInfinity)
-    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just (1 / 0)})
-  expectBuildFailure
-    "non-positive maximum edge length"
-    (RefinementMaximumEdgeLengthNotPositive 0)
-    (refineWith defaultRefinementParameters{refineMaxEdgeLength = Just 0})
-  expectBuildFailure
-    "minimum area above maximum area"
-    (RefinementMinimumAreaExceedsMaximum 2 1)
-    ( refineWith
-        defaultRefinementParameters
-          { refineMinArea = Just 2
-          , refineMaxArea = Just 1
-          }
-    )
-  case refineWithArea 0.25 of
-    Right _ -> pure ()
-    Left failure -> fail ("positive maximum area rejected: " <> show failure)
-
-expectBuildFailure :: String -> BuildError -> Either BuildError value -> IO ()
-expectBuildFailure label expected outcome =
-  case outcome of
-    Left actual -> assertEqual label expected actual
-    Right _ -> fail (label <> ": expected " <> show expected <> ", got success")
-
-
-canonicalEdges
-  :: Triangulation mode vertex directed undirected face
-  -> Set.Set (Point, Point)
-canonicalEdges triangulation =
-  Set.fromList
-    [ ordered (vertexPoint triangulation (origin triangulation edge)) (vertexPoint triangulation (destination triangulation edge))
-    | undirected <- undirectedEdges triangulation
-    , let edge = normalizedDirected undirected
-    ]
- where
-  ordered :: Ord value => value -> value -> (value, value)
-  ordered left right = if left <= right then (left, right) else (right, left)
-
-requireJust :: String -> Maybe value -> IO value
-requireJust _ (Just value) = pure value
-requireJust label Nothing = fail (label <> ": expected Just")
-
-requireAcceptedConstraint
-  :: String
-  -> ConstraintBatchResult vertex directed undirected face
-  -> IO (V.Vector DirectedEdgeId, Int)
-requireAcceptedConstraint label batch =
-  case V.toList (constraintBatchOutcomes batch) of
-    [ConstraintAccepted path added] -> pure (path, added)
-    outcomes ->
-      fail
-        ( label
-            <> ": expected one accepted constraint, got "
-            <> show outcomes
-        )
-
-assertBatchStats
-  :: String
-  -> ConstraintBatchResult vertex directed undirected face
-  -> IO ()
-assertBatchStats label batch = do
-  let stats = constraintBatchStats batch
-      (accepted, rejected) =
-        V.foldl'
-          (\(!acceptedCount, !rejectedCount) outcome ->
-            case outcome of
-              ConstraintAccepted _ _ -> (acceptedCount + 1, rejectedCount)
-              ConstraintRejected _ -> (acceptedCount, rejectedCount + 1)
-          )
-          (0, 0)
-          (constraintBatchOutcomes batch)
-  assertEqual
-    (label <> " request count")
-    (V.length (constraintBatchOutcomes batch))
-    (constraintBatchRequests stats)
-  assertEqual
-    (label <> " accepted count")
-    accepted
-    (constraintBatchAccepted stats)
-  assertEqual
-    (label <> " rejected count")
-    rejected
-    (constraintBatchRejected stats)
-
-replayConstraintRequest
-  :: (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])
-  -> (VertexId, VertexId)
-  -> Either
-      (CdtError)
-      (ConstrainedDelaunayTriangulation (Point), [ConstraintOutcome])
-replayConstraintRequest (current, outcomes) request = do
-  singleton <- recoverConstraints current (V.singleton request)
-  case V.toList (constraintBatchOutcomes singleton) of
-    [outcome] ->
-      Right
-        ( constraintBatchTriangulation singleton
-        , outcome : outcomes
-        )
-    cardinality ->
-      Left
-        ( ConstraintBatchCardinalityMismatch
-            1
-            (length cardinality)
-        )
-
-requirePointBuild :: String -> [Point] -> IO (BuildResult 'Unconstrained (Point) () () ())
-requirePointBuild label points = requireRight label (delaunay unitElementDefaults (V.fromList points))
-
-assertCdtValid :: String -> Triangulation 'Constrained vertex () () () -> IO ()
-assertCdtValid label triangulation =
-  case validateTriangulation triangulation of
-    [] -> pure ()
-    violations -> fail (label <> " CDT violations: " <> show violations)
-
-assertMaximumEdgeLength
-  :: String
-  -> Double
-  -> Triangulation mode vertex directed undirected face
-  -> [FaceId]
-  -> IO ()
-assertMaximumEdgeLength label maximumLength triangulation faces =
-  unless (null violations) $
-    fail (label <> " oversized edges: " <> show (take 1 violations))
- where
-  !maximumSquaredLength = maximumLength * maximumLength
-  violations =
-    [ (face, edge, squaredLength)
-    | face <- faces
-    , directed <- faceDirectedEdges triangulation face
-    , let edge = asUndirected directed
-          (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-          squaredLength =
-            squaredDistanceWide
-              (vertexPoint triangulation fromVertex)
-              (vertexPoint triangulation toVertex)
-    , squaredLength > maximumSquaredLength
-    ]
-
-canonicalSegmentSquaredLength :: CanonicalSegment -> Double
-canonicalSegmentSquaredLength segmentValue =
-  squaredDistanceWide
-    (segmentStart segmentValue)
-    (segmentEnd segmentValue)
-
-assertNear :: String -> Double -> Double -> Double -> IO ()
-assertNear label tolerance expected actual =
-  unless (abs (expected - actual) <= tolerance * max 1 (max (abs expected) (abs actual))) $
-    fail (label <> ": expected " <> show expected <> ", got " <> show actual)
-
-gridPoints :: Int -> Int -> [Point]
-gridPoints width height =
-  [ Point (fromIntegral x + jitter x y) (fromIntegral y + jitter y x)
-  | y <- [0 .. height - 1]
-  , x <- [0 .. width - 1]
-  ]
- where
-  jitter :: Int -> Int -> Double
-  jitter a b = fromIntegral ((a * 17 + b * 31) `mod` 11) * 1.0e-5
-
-randomPoints :: Word64 -> Int -> [Point]
-randomPoints seed count = take count (go seed Set.empty)
- where
-  go :: Word64 -> Set.Set (Point) -> [Point]
-  go state seen =
-    let state1 = lcg state
-        state2 = lcg state1
-        x = unit state1 * 2 - 1
-        y = unit state2 * 2 - 1
-        point = Point x y
-     in if Set.member point seen
-          then go state2 seen
-          else point : go state2 (Set.insert point seen)
-
-  unit :: Word64 -> Double
-  unit value = fromIntegral (value `mod` 9_007_199_254_740_881) / 9_007_199_254_740_881
-
-  lcg :: Word64 -> Word64
-  lcg value = value * 6_364_136_223_846_793_005 + 1_442_695_040_888_963_407
diff --git a/test/native/Moonlight/Triangulation/OverlaySpec.hs b/test/native/Moonlight/Triangulation/OverlaySpec.hs
deleted file mode 100644
--- a/test/native/Moonlight/Triangulation/OverlaySpec.hs
+++ /dev/null
@@ -1,776 +0,0 @@
--- | Focused common-refinement, provenance, selector, and grouped-publication
--- acceptance.
-module Moonlight.Triangulation.OverlaySpec (tests) where
-
-import Control.Monad (foldM, unless, when)
-import Data.Foldable (traverse_)
-import qualified Data.Map.Strict as Map
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Set as Set
-import qualified Data.Vector as V
-import Moonlight.Triangulation.CellSet
-  ( ExactCellSet
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , exactCellSetVertexCount
-  )
-import Moonlight.Triangulation.Dcel
-  ( faceData
-  , isConstraintEdge
-  , undirectedEdgeData
-  , undirectedEndpoints
-  , vertexData
-  )
-import Moonlight.Triangulation.Exact
-  ( ExactPoint
-  , ExactIntersectionError
-  , ExactSegment
-  , SegmentRelation (..)
-  , exactLineIntersection
-  , exactOnClosedSegment
-  , exactPointCoordinates
-  , exactSegment
-  , exactSegmentEndpoints
-  , exactSegmentRelation
-  )
-import Moonlight.Triangulation.Internal.ExactRational
-  ( exactRationalDenominator
-  )
-import Moonlight.Triangulation.Internal.ExactSegmentEvents
-  ( ExactSegmentEventPlan
-  , ExactSweepSegmentId (..)
-  , exactSegmentEventPlan
-  , exactSegmentRelationMap
-  , exactSegmentSplitPoints
-  , exactSegmentSweepMaximumHeight
-  )
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( innerFaces
-  , undirectedEdges
-  )
-import Moonlight.Triangulation.Internal.Overlay.Arrangement (certifyArrangement)
-import Moonlight.Triangulation.Internal.Overlay.Resident
-  ( OverlayDiagonalSchedule (..)
-  , residentOverlay
-  )
-import Moonlight.Triangulation.Overlay
-import Moonlight.Triangulation.Region
-import Support
-  ( assertEqual
-  , assertValid
-  , integerPoint
-  , rectangleComponent
-  , requireRight
-  )
-
-tests :: IO ()
-tests = do
-  testExactSegmentEventPlan
-  testOverlappingSquares
-  testLowerDimensionalIntersections
-  testSeamAndOverlapNormalization
-  testOutsidePairRemainsImplicit
-  testNestedAndNonDyadicOverlay
-  testSelectorRefusalsAndOperandSwap
-  testDisconnectedGroupedPublication
-  testMultiwayPointBoundaryCycles
-  testDiagonalScheduleIndependence
-
-testExactSegmentEventPlan :: IO ()
-testExactSegmentEventPlan = do
-  sourceFixture <-
-    traverse
-      (uncurry integerSegment)
-      [ ((0, 0), (4, 0))
-      , ((4, 0), (0, 0))
-      , ((2, 0), (6, 0))
-      , ((4, 0), (4, 4))
-      , ((1, -1), (1, 0))
-      , ((0, -1), (4, 1))
-      , ((10, 10), (11, 10))
-      ]
-  verticalAndMultiway <-
-    traverse
-      (uncurry integerSegment)
-      [ ((0, -4), (0, 4))
-      , ((-4, 0), (4, 0))
-      , ((-3, -3), (3, 3))
-      , ((-3, 3), (3, -3))
-      , ((0, 1), (0, 5))
-      , ((0, 4), (0, 7))
-      ]
-  grid <-
-    traverse
-      (uncurry integerSegment)
-      ( [((-1, y), (5, y)) | y <- [0 .. 4]]
-          <> [((x, -1), (x, 5)) | x <- [0 .. 4]]
-      )
-  collinearOverlaps <-
-    traverse
-      (uncurry integerSegment)
-      [ ((0, 0), (8, 0))
-      , ((1, 0), (3, 0))
-      , ((2, 0), (6, 0))
-      , ((5, 0), (9, 0))
-      , ((8, 0), (10, 0))
-      , ((11, 0), (12, 0))
-      ]
-  traverse_
-    compareEventPlanWithOracle
-    [sourceFixture, verticalAndMultiway, grid, collinearOverlaps]
-  let allRelations =
-        Set.fromList
-          [ exactSegmentRelation a b c d
-          | (leftIndex, left) <- zip [0 :: Int ..] sourceFixture
-          , right <- drop (leftIndex + 1) sourceFixture
-          , let (a, b) = exactSegmentEndpoints left
-                (c, d) = exactSegmentEndpoints right
-          ]
-  assertEqual
-    "source fixture covers every segment relation"
-    ( Set.fromList
-        [ SegmentsDisjoint
-        , SegmentsProperlyCross
-        , SegmentsShareEndpoint
-        , SegmentEndpointTouchesInterior
-        , SegmentsCollinearlyOverlap
-        , SegmentsDuplicate
-        ]
-    )
-    allRelations
-
-compareEventPlanWithOracle :: [ExactSegment] -> IO ()
-compareEventPlanWithOracle segments = do
-  let vector = V.fromList segments
-  plan <- requireRight "exact event sweep" (exactSegmentEventPlan vector)
-  expectedSplits <- requireRight "quadratic split oracle" (quadraticSplitPoints segments)
-  assertEqual
-    "sweep relation map agrees with quadratic oracle"
-    (quadraticRelationMap segments)
-    (exactSegmentRelationMap plan)
-  traverse_
-    (assertSegmentSplits plan expectedSplits)
-    [0 .. length segments - 1]
-  let heightLimit = 2 * ceilingLog2 (length segments + 1)
-  if exactSegmentSweepMaximumHeight plan <= heightLimit
-    then pure ()
-    else
-      fail
-        ( "AVL height exceeded conservative logarithmic bound: "
-            <> show (exactSegmentSweepMaximumHeight plan, heightLimit)
-        )
-
-assertSegmentSplits
-  :: ExactSegmentEventPlan
-  -> Map.Map Int (Set.Set ExactPoint)
-  -> Int
-  -> IO ()
-assertSegmentSplits plan expected segmentIndex =
-  assertEqual
-    ("sweep split points agree for segment " <> show segmentIndex)
-    (Map.findWithDefault Set.empty segmentIndex expected)
-    (Set.fromList (exactSegmentSplitPoints plan (ExactSweepSegmentId segmentIndex)))
-
-quadraticRelationMap
-  :: [ExactSegment]
-  -> Map.Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-quadraticRelationMap segments =
-  Map.fromList
-    [ ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation)
-    | (leftIndex, left) <- zip [0 :: Int ..] segments
-    , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) segments)
-    , let (a, b) = exactSegmentEndpoints left
-          (c, d) = exactSegmentEndpoints right
-          relation = exactSegmentRelation a b c d
-    , relation /= SegmentsDisjoint
-    ]
-
-quadraticSplitPoints
-  :: [ExactSegment]
-  -> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
-quadraticSplitPoints segments =
-  foldM addRelation initial (segmentPairs segments)
- where
-  initial =
-    Map.fromList
-      [ (index, Set.fromList [from, to])
-      | (index, segment) <- zip [0 :: Int ..] segments
-      , let (from, to) = exactSegmentEndpoints segment
-      ]
-  addRelation
-    :: Map.Map Int (Set.Set ExactPoint)
-    -> (Int, ExactSegment, Int, ExactSegment)
-    -> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
-  addRelation splitPoints (leftIndex, left, rightIndex, right) = do
-    witnesses <- relationSplitWitnesses left right
-    pure
-      ( Map.insertWith Set.union rightIndex (Set.fromList witnesses)
-          (Map.insertWith Set.union leftIndex (Set.fromList witnesses) splitPoints)
-      )
-
-segmentPairs :: [value] -> [(Int, value, Int, value)]
-segmentPairs values =
-  [ (leftIndex, left, rightIndex, right)
-  | (leftIndex, left) <- zip [0 :: Int ..] values
-  , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) values)
-  ]
-
-relationSplitWitnesses
-  :: ExactSegment
-  -> ExactSegment
-  -> Either ExactIntersectionError [ExactPoint]
-relationSplitWitnesses left right =
-  case exactSegmentRelation a b c d of
-    SegmentsDisjoint -> Right []
-    SegmentsProperlyCross -> (: []) <$> exactLineIntersection left right
-    SegmentsDuplicate -> Right []
-    _ ->
-      Right
-        ( Set.toAscList
-            ( Set.fromList
-                [ point
-                | point <- [a, b, c, d]
-                , exactOnClosedSegment a b point
-                , exactOnClosedSegment c d point
-                ]
-            )
-        )
- where
-  (a, b) = exactSegmentEndpoints left
-  (c, d) = exactSegmentEndpoints right
-
-ceilingLog2 :: Int -> Int
-ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))
-
-integerSegment :: (Integer, Integer) -> (Integer, Integer) -> IO ExactSegment
-integerSegment (fromX, fromY) (toX, toY) =
-  requireRight
-    "integer exact segment"
-    (exactSegment (integerPoint fromX fromY) (integerPoint toX toY))
-
-testOverlappingSquares :: IO ()
-testOverlappingSquares = do
-  left <- singletonSquareLayer "left-outside" "left" 0 0 2 2
-  right <- singletonSquareLayer "right-outside" "right" 1 (-1) 3 1
-  result <- requireRight "overlapping square overlay" (overlayLayers left right)
-  assertOverlayIntegrity "overlapping square" result
-  let receipt = overlayReceipt result
-  assertEqual "overlay source segment count" 8 (overlayInputSegments receipt)
-  assertEqual "overlay has two proper crossings" 2 (overlayExactCrossings receipt)
-  when (overlayAtomicEdges receipt < 8) $
-    fail ("overlay lost atomic edges: " <> show receipt)
-  intersection <-
-    requireRight
-      "closed square intersection"
-      (overlayClosedIntersection (== "left") (== "right") result)
-  when
-    ( exactCellSetVertexCount intersection < 4
-        || exactCellSetEdgeCount intersection < 4
-        || exactCellSetFaceCount intersection < 1
-    )
-    (fail "closed intersection omitted incidence closure")
-  published <-
-    requireRight
-      "selected square intersection publication"
-      (overlaySelectedRegion (== ("left", "right")) result)
-  assertEqual
-    "selected square intersection component count"
-    1
-    (length (planarRegionComponents published))
-  assertEqual
-    "unbounded selected publication refusal"
-    (Left RegionUnboundedSelection)
-    (overlaySelectedRegion (const True) result)
-  merged <-
-    requireRight
-      "selected adjacent overlay cells merge"
-      (overlaySelectedRegion (\(leftLabel, _) -> leftLabel == "left") result)
-  assertEqual
-    "selected adjacent overlay cells publish one component"
-    [4]
-    (map (length . exactLoopPoints . polygonOuterLoop) (planarRegionComponents merged))
-
-testLowerDimensionalIntersections :: IO ()
-testLowerDimensionalIntersections = do
-  left <- singletonSquareLayer "left-outside" "left" 0 0 1 1
-  pointTouching <- singletonSquareLayer "right-outside" "right" 1 1 2 2
-  pointResult <- requireRight "point-touching overlay" (overlayLayers left pointTouching)
-  assertOverlayIntegrity "point-touching" pointResult
-  assertEqual
-    "point-touching unbounded cell keeps two simple boundary cycles"
-    (Just 2)
-    (unboundedLoopCount pointResult)
-  pointIntersection <-
-    requireRight
-      "point-only closed intersection"
-      (overlayClosedIntersection (== "left") (== "right") pointResult)
-  assertEqual
-    "point-only closed intersection retains only its zero-cell"
-    (1, 0, 0)
-    ( exactCellSetVertexCount pointIntersection
-    , exactCellSetEdgeCount pointIntersection
-    , exactCellSetFaceCount pointIntersection
-    )
-
-  edgeTouching <- singletonSquareLayer "right-outside" "right" 1 0 2 1
-  edgeResult <- requireRight "edge-touching overlay" (overlayLayers left edgeTouching)
-  assertOverlayIntegrity "edge-touching" edgeResult
-  edgeIntersection <-
-    requireRight
-      "edge-only closed intersection"
-      (overlayClosedIntersection (== "left") (== "right") edgeResult)
-  assertEqual
-    "edge-only closed intersection retains its closure"
-    (2, 1, 0)
-    ( exactCellSetVertexCount edgeIntersection
-    , exactCellSetEdgeCount edgeIntersection
-    , exactCellSetFaceCount edgeIntersection
-    )
-  publishedEdgeOnly <-
-    requireRight
-      "edge-only polygon publication"
-      (overlaySelectedRegion (== ("left", "right")) edgeResult)
-  assertEqual
-    "edge-only selection is not fabricated into a polygon"
-    0
-    (length (planarRegionComponents publishedEdgeOnly))
-
-testSeamAndOverlapNormalization :: IO ()
-testSeamAndOverlapNormalization = do
-  first <- rectangleComponent 0 0 1 1
-  second <- rectangleComponent 1 0 2 1
-  joinedRegion <- requireRight "same-label joined region" (planarRegion [first, second])
-  joinedLayer <-
-    requireRight
-      "same-label joined layer"
-      (planarLayer "outside" (Map.singleton "inside" joinedRegion))
-  emptyLayer <- requireRight "empty normalization layer" (planarLayer "void" Map.empty)
-  seamResult <- requireRight "same-label seam overlay" (overlayLayers joinedLayer emptyLayer)
-  assertOverlayIntegrity "same-label seam" seamResult
-  assertEqual "joined rectangle has one unbounded boundary cycle" (Just 1) (unboundedLoopCount seamResult)
-  assertEqual
-    "same-label shared boundary is removed before topology"
-    6
-    (overlayAtomicEdges (overlayReceipt seamResult))
-  let seamPublication = overlayPlanarLayer seamResult
-  assertEqual
-    "the implicit outside pair is never duplicated as a bounded layer key"
-    Nothing
-    ( Map.lookup
-        ("outside", "void")
-        (planarLayerRegions seamPublication)
-    )
-  assertEqual
-    "same-label seam dissolves to the rectangle boundary"
-    [4]
-    ( map
-        (length . exactLoopPoints . polygonOuterLoop)
-        ( maybe
-            []
-            planarRegionComponents
-            (Map.lookup ("inside", "void") (planarLayerRegions seamPublication))
-        )
-    )
-
-  duplicateLeft <- singletonSquareLayer "left-outside" "left" 0 0 2 2
-  duplicateRight <- singletonSquareLayer "right-outside" "right" 0 0 2 2
-  duplicateResult <-
-    requireRight "cross-operand duplicate boundary overlay" (overlayLayers duplicateLeft duplicateRight)
-  assertOverlayIntegrity "duplicate boundary" duplicateResult
-  assertEqual
-    "duplicate boundaries normalize to one atomic cycle"
-    4
-    (overlayAtomicEdges (overlayReceipt duplicateResult))
-
-  partialLeft <- singletonSquareLayer "left-outside" "left" 0 0 3 2
-  partialRight <- singletonSquareLayer "right-outside" "right" 1 0 4 1
-  partialResult <-
-    requireRight "partial collinear overlap" (overlayLayers partialLeft partialRight)
-  assertOverlayIntegrity "partial collinear overlap" partialResult
-  if overlayOverlapIntervals (overlayReceipt partialResult) > 0
-    then pure ()
-    else fail "partial collinear overlap emitted no overlap event"
-
-testOutsidePairRemainsImplicit :: IO ()
-testOutsidePairRemainsImplicit = do
-  outer <-
-    requireRight
-      "annulus outer loop"
-      ( exactLoop
-          ( integerPoint 0 0
-              :| [integerPoint 4 0, integerPoint 4 4, integerPoint 0 4]
-          )
-      )
-  hole <-
-    requireRight
-      "annulus hole loop"
-      ( exactLoop
-          ( integerPoint 1 1
-              :| [integerPoint 1 3, integerPoint 3 3, integerPoint 3 1]
-          )
-      )
-  component <- requireRight "annulus component" (polygonComponent outer [hole])
-  region <- requireRight "annulus region" (planarRegion [component])
-  left <-
-    requireRight
-      "annulus layer"
-      (planarLayer "outside" (Map.singleton "annulus" region))
-  right <- requireRight "annulus empty layer" (planarLayer "void" Map.empty)
-  result <- requireRight "annulus overlay" (overlayLayers left right)
-  let published = overlayPlanarLayer result
-  assertEqual
-    "bounded cavities remain represented by the implicit outside pair"
-    Nothing
-    ( Map.lookup
-        ("outside", "void")
-        (planarLayerRegions published)
-    )
-
-testSelectorRefusalsAndOperandSwap :: IO ()
-testSelectorRefusalsAndOperandSwap = do
-  left <- singletonSquareLayer "left-outside" "left" 0 0 2 2
-  right <- singletonSquareLayer "right-outside" "right" 1 (-1) 3 1
-  result <- requireRight "selector refusal overlay" (overlayLayers left right)
-  assertSelectionRefusal
-    "closed union refuses selected outside cell"
-    (OverlaySelectionContainsUnboundedCell ClosedUnionSelection)
-    (overlayClosedUnion (== "left-outside") (const False) result)
-  assertSelectionRefusal
-    "closed intersection refuses selected outside cell"
-    (OverlaySelectionContainsUnboundedCell ClosedIntersectionSelection)
-    ( overlayClosedIntersection
-        (== "left-outside")
-        (== "right-outside")
-        result
-    )
-  assertSelectionRefusal
-    "regularized difference refuses selected outside cell"
-    (OverlaySelectionContainsUnboundedCell RegularizedDifferenceSelection)
-    ( overlayRegularizedDifference
-        (== "left-outside")
-        (== "right")
-        result
-    )
-
-  swapped <- requireRight "operand-swapped overlay" (overlayLayers right left)
-  assertOverlayIntegrity "operand-swapped" swapped
-  assertEqual
-    "operand swap preserves exact arrangement vertices"
-    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices result)))
-    (Set.fromList (map (overlayExactPoint . snd) (overlayArrangementVertices swapped)))
-  assertEqual
-    "operand swap preserves cells and exchanges labels"
-    [ (overlayCellGeometry cell, (overlayCellRight cell, overlayCellLeft cell))
-    | (_, cell) <- overlayCells result
-    ]
-    [ (overlayCellGeometry cell, (overlayCellLeft cell, overlayCellRight cell))
-    | (_, cell) <- overlayCells swapped
-    ]
-  assertEqual
-    "operand swap exchanges typed vertex provenance"
-    (vertexOriginCensus True result)
-    (vertexOriginCensus False swapped)
-
-testNestedAndNonDyadicOverlay :: IO ()
-testNestedAndNonDyadicOverlay = do
-  outer <- singletonSquareLayer "left-outside" "left" 0 0 4 4
-  inner <- singletonSquareLayer "right-outside" "right" 1 1 3 3
-  nested <- requireRight "nested overlay" (overlayLayers outer inner)
-  assertOverlayIntegrity "nested" nested
-  assertEqual "nested overlay unbounded boundary is outermost only" (Just 1) (unboundedLoopCount nested)
-  assertEqual
-    "nested overlay has unbounded, shell, and intersection cells"
-    3
-    (length (overlayCells nested))
-
-  leftTriangle <-
-    triangleLayer
-      "left-outside"
-      "left"
-      ((0, 0), (4, 0), (0, 4))
-  rightTriangle <-
-    triangleLayer
-      "right-outside"
-      "right"
-      ((1, -1), (3, -1), (2, 2))
-  nonDyadic <-
-    requireRight "non-dyadic proper-crossing overlay" (overlayLayers leftTriangle rightTriangle)
-  assertOverlayIntegrity "non-dyadic proper crossing" nonDyadic
-  let hasNonDyadicCoordinate =
-        any
-          (\(_, vertex) ->
-             let (x, y) = exactPointCoordinates (overlayExactPoint vertex)
-              in any
-                   (not . isPowerOfTwo . exactRationalDenominator)
-                   [x, y])
-          (overlayArrangementVertices nonDyadic)
-  if overlayExactCrossings (overlayReceipt nonDyadic) > 0 && hasNonDyadicCoordinate
-    then pure ()
-    else fail "proper-crossing overlay lost its non-dyadic exact witness"
-
-assertSelectionRefusal
-  :: String
-  -> OverlaySelectionError
-  -> Either OverlaySelectionError ExactCellSet
-  -> IO ()
-assertSelectionRefusal label expected actual =
-  case actual of
-    Left obstruction -> assertEqual label expected obstruction
-    Right _ -> fail (label <> ": unbounded selection was truncated")
-
-testDisconnectedGroupedPublication :: IO ()
-testDisconnectedGroupedPublication = do
-  first <- rectangleComponent 0 0 1 1
-  second <- rectangleComponent 3 0 4 1
-  region <- requireRight "two-island region" (planarRegion [first, second])
-  left <-
-    requireRight
-      "two-island layer"
-      (planarLayer "outside" (Map.singleton "island" region))
-  right <- requireRight "empty right layer" (planarLayer "void" Map.empty)
-  result <- requireRight "two-island overlay" (overlayLayers left right)
-  assertEqual "two islands give two unbounded boundary cycles" (Just 2) (unboundedLoopCount result)
-  let published = overlayPlanarLayer result
-  let components =
-        maybe
-          []
-          planarRegionComponents
-          (Map.lookup ("island", "void") (planarLayerRegions published))
-  assertEqual "equal labels group after component descent" 2 (length components)
-  reversedRegion <- requireRight "reversed two-island region" (planarRegion [second, first])
-  reversedLeft <-
-    requireRight
-      "reversed two-island layer"
-      (planarLayer "outside" (Map.singleton "island" reversedRegion))
-  reversedResult <- requireRight "reversed two-island overlay" (overlayLayers reversedLeft right)
-  assertEqual
-    "component construction order cannot perturb stable cells"
-    (overlayCells result)
-    (overlayCells reversedResult)
-
-testMultiwayPointBoundaryCycles :: IO ()
-testMultiwayPointBoundaryCycles = do
-  components <-
-    traverse
-      triangleComponent
-      [ ((0, 0), (2, 0), (1, 1))
-      , ((0, 0), (-1, 1), (-2, 0))
-      , ((0, 0), (-1, -1), (1, -1))
-      ]
-  region <- requireRight "three point-touching components" (planarRegion components)
-  left <-
-    requireRight
-      "three point-touching layer"
-      (planarLayer "outside" (Map.singleton "inside" region))
-  right <- requireRight "empty multiway right layer" (planarLayer "void" Map.empty)
-  result <- requireRight "three-way point-touching overlay" (overlayLayers left right)
-  assertOverlayIntegrity "three-way point-touching" result
-  assertEqual
-    "multiway point contact descends to three simple unbounded cycles"
-    (Just 3)
-    (unboundedLoopCount result)
-
-testDiagonalScheduleIndependence :: IO ()
-testDiagonalScheduleIndependence = do
-  left <- singletonSquareLayer "outside" "inside" 0 0 2 2
-  right <- requireRight "empty diagonal-policy layer" (planarLayer "void" Map.empty)
-  certified <-
-    requireRight
-      "diagonal-policy arrangement certification"
-      (certifyArrangement left right)
-  canonical <-
-    requireRight
-      "canonical resident diagonal schedule"
-      (residentOverlay CanonicalOverlayDiagonals ("outside", "void") certified)
-  alternate <-
-    requireRight
-      "alternate resident diagonal schedule"
-      (residentOverlay FlipFirstAdmissibleDiagonal ("outside", "void") certified)
-  assertOverlayIntegrity "canonical diagonal schedule" canonical
-  assertOverlayIntegrity "alternate diagonal schedule" alternate
-  assertEqual
-    "exact cells are independent of resident diagonal policy"
-    (overlayCells canonical)
-    (overlayCells alternate)
-  when (residentDiagonalKeys canonical == residentDiagonalKeys alternate) $
-    fail "alternate diagonal fixture did not change the resident topology"
-
-residentDiagonalKeys
-  :: OverlayResult leftLabel rightLabel
-  -> Set.Set (ExactPoint, ExactPoint)
-residentDiagonalKeys result =
-  Set.fromList
-    [ if fromPoint <= toPoint
-        then (fromPoint, toPoint)
-        else (toPoint, fromPoint)
-    | edge <- undirectedEdges triangulation
-    , OverlayDiagonal <- [undirectedEdgeData triangulation edge]
-    , let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-          fromPoint = overlayExactPoint (vertexData triangulation fromVertex)
-          toPoint = overlayExactPoint (vertexData triangulation toVertex)
-    ]
- where
-  triangulation = overlayEmbeddedTriangulation result
-
-assertOverlayIntegrity
-  :: String
-  -> OverlayResult leftLabel rightLabel
-  -> IO ()
-assertOverlayIntegrity label result = do
-  let triangulation = overlayEmbeddedTriangulation result
-      boundaryEdges = overlayArrangementEdges result
-      cellIds = Set.fromList (map fst (overlayCells result))
-      residentFaceIds =
-        Set.fromList
-          [ overlayFaceCellId (faceData triangulation face)
-          | face <- innerFaces triangulation
-          ]
-  assertValid (label <> " embedded triangulation") triangulation
-  assertEqual
-    (label <> " atomic constraint correspondence")
-    (overlayAtomicEdges (overlayReceipt result))
-    (length boundaryEdges)
-  unless (all (isConstraintEdge triangulation . fst) boundaryEdges) $
-    fail (label <> ": boundary payload names an unconstrained edge")
-  unless (residentFaceIds `Set.isSubsetOf` cellIds) $
-    fail (label <> ": resident face names no exact cell")
-  unless
-    ( all
-        (vertexOriginIsNonEmpty . overlayVertexOrigin . snd)
-        (overlayArrangementVertices result)
-    )
-    (fail (label <> ": exact vertex lost all typed origins"))
-  unless (all (edgeOriginIsNonEmpty . snd) boundaryEdges) $
-    fail (label <> ": atomic edge lost all typed origins")
-  atomicSegments <-
-    traverse
-      (\(edge, _) ->
-         let (fromVertex, toVertex) = undirectedEndpoints triangulation edge
-          in requireRight
-               (label <> " atomic segment")
-               ( exactSegment
-                   (overlayExactPoint (vertexData triangulation fromVertex))
-                   (overlayExactPoint (vertexData triangulation toVertex))
-               ))
-      boundaryEdges
-  atomicPlan <-
-    requireRight
-      (label <> " atomic endpoint-incidence proof")
-      (exactSegmentEventPlan (V.fromList atomicSegments))
-  assertEqual
-    (label <> " atomics have only endpoint-incidence relations")
-    (endpointIncidenceRelations atomicSegments)
-    (exactSegmentRelationMap atomicPlan)
-
-endpointIncidenceRelations
-  :: [ExactSegment]
-  -> Map.Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
-endpointIncidenceRelations segments =
-  Map.fromList
-    [ ( (ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex)
-      , SegmentsShareEndpoint
-      )
-    | (leftIndex, left) <- zip [0 :: Int ..] segments
-    , (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) segments)
-    , let (leftFrom, leftTo) = exactSegmentEndpoints left
-          (rightFrom, rightTo) = exactSegmentEndpoints right
-    , not
-        ( Set.null
-            ( Set.intersection
-                (Set.fromList [leftFrom, leftTo])
-                (Set.fromList [rightFrom, rightTo])
-            )
-        )
-    ]
-
-vertexOriginIsNonEmpty :: OverlayVertexOrigin -> Bool
-vertexOriginIsNonEmpty origin =
-  not
-    ( null (overlayOriginLeftVertices origin)
-        && null (overlayOriginRightVertices origin)
-        && null (overlayOriginLeftEdges origin)
-        && null (overlayOriginRightEdges origin)
-    )
-
-edgeOriginIsNonEmpty :: OverlayEdgeOrigin -> Bool
-edgeOriginIsNonEmpty origin =
-  not
-    (null (overlayEdgeLeftSources origin) && null (overlayEdgeRightSources origin))
-
-vertexOriginCensus
-  :: Bool
-  -> OverlayResult String String
-  -> Map.Map ExactPoint (Int, Int, Int, Int)
-vertexOriginCensus preserveSides result =
-  Map.fromList
-    [ ( overlayExactPoint vertex
-      , if preserveSides
-          then census origin
-          else swapCensus (census origin)
-      )
-    | (_, vertex) <- overlayArrangementVertices result
-    , let origin = overlayVertexOrigin vertex
-    ]
- where
-  census :: OverlayVertexOrigin -> (Int, Int, Int, Int)
-  census origin =
-    ( length (overlayOriginLeftVertices origin)
-    , length (overlayOriginRightVertices origin)
-    , length (overlayOriginLeftEdges origin)
-    , length (overlayOriginRightEdges origin)
-    )
-  swapCensus :: (Int, Int, Int, Int) -> (Int, Int, Int, Int)
-  swapCensus (leftVertices, rightVertices, leftEdges, rightEdges) =
-    (rightVertices, leftVertices, rightEdges, leftEdges)
-
-unboundedLoopCount :: OverlayResult leftLabel rightLabel -> Maybe Int
-unboundedLoopCount result =
-  case
-    [ length loops
-    | (cellId, cell) <- overlayCells result
-    , cellId == OverlayCellId 0
-    , UnboundedOverlayCell loops <- [overlayCellGeometry cell]
-    ] of
-    [count] -> Just count
-    _ -> Nothing
-
-singletonSquareLayer
-  :: String
-  -> String
-  -> Integer
-  -> Integer
-  -> Integer
-  -> Integer
-  -> IO (PlanarLayer String)
-singletonSquareLayer outside inside minX minY maxX maxY = do
-  component <- rectangleComponent minX minY maxX maxY
-  region <- requireRight "singleton square region" (planarRegion [component])
-  requireRight "singleton square layer" (planarLayer outside (Map.singleton inside region))
-
-triangleLayer
-  :: String
-  -> String
-  -> ((Integer, Integer), (Integer, Integer), (Integer, Integer))
-  -> IO (PlanarLayer String)
-triangleLayer outside inside (firstPoint, secondPoint, thirdPoint) = do
-  component <- triangleComponent (firstPoint, secondPoint, thirdPoint)
-  region <- requireRight "overlay triangle region" (planarRegion [component])
-  requireRight "overlay triangle layer" (planarLayer outside (Map.singleton inside region))
-
-triangleComponent
-  :: ((Integer, Integer), (Integer, Integer), (Integer, Integer))
-  -> IO PolygonComponent
-triangleComponent (firstPoint, secondPoint, thirdPoint) = do
-  loop <-
-    requireRight
-      "overlay triangle loop"
-      ( exactLoop
-          ( uncurry integerPoint firstPoint
-              :| [uncurry integerPoint secondPoint, uncurry integerPoint thirdPoint]
-          )
-      )
-  requireRight "overlay triangle component" (polygonComponent loop [])
-
-isPowerOfTwo :: Integer -> Bool
-isPowerOfTwo value =
-  value > 0 && value `elem` takeWhile (<= value) (iterate (* 2) 1)
diff --git a/test/native/Moonlight/Triangulation/RegionSpec.hs b/test/native/Moonlight/Triangulation/RegionSpec.hs
deleted file mode 100644
--- a/test/native/Moonlight/Triangulation/RegionSpec.hs
+++ /dev/null
@@ -1,238 +0,0 @@
--- | Focused exact-region authoring and ordinary publication acceptance.
-module Moonlight.Triangulation.RegionSpec (tests) where
-
-import qualified Data.Map.Strict as Map
-import qualified Data.Vector as V
-import Moonlight.Triangulation
-  ( Point (..)
-  , buildTriangulation
-  , delaunay
-  , unitElementDefaults
-  )
-import Moonlight.Triangulation.CellSet
-  ( CellSelectionError (..)
-  , closeFaceCellSet
-  , exactCellSet
-  , exactCellSetEdgeCount
-  , exactCellSetFaceCount
-  , exactCellSetVertexCount
-  )
-import Moonlight.Triangulation.Dcel (outerFace)
-import Moonlight.Triangulation.Handles.Iterators.FixedIterators
-  ( innerFaces
-  , undirectedEdges
-  )
-import Moonlight.Triangulation.Internal.Region.Publication
-  ( labelledPlanarLayerFromExactCoordinates
-  )
-import Moonlight.Triangulation.FloodFillIterator (BoundaryObstruction (BoundaryPinch))
-import Moonlight.Triangulation.NativeSpec
-  ( regionFaceSatisfies
-  , regionMesh
-  , regionMeshFromPoints
-  )
-import Moonlight.Triangulation.Region
-import Support (assertEqual, integerPoint, rectangleComponent, requireRight)
-
-tests :: IO ()
-tests = do
-  testExactAuthoring
-  testOrdinaryPublication
-  testExactCellSetAdmission
-  testGroupedPublicationFixtures
-  testPinchPublicationRefusal
-
-testExactAuthoring :: IO ()
-testExactAuthoring = do
-  firstComponent <- rectangleComponent 0 0 2 2
-  secondComponent <- rectangleComponent 4 0 5 1
-  region <- requireRight "disconnected exact region" (planarRegion [firstComponent, secondComponent])
-  layer <-
-    requireRight
-      "disconnected labelled layer"
-      (planarLayer "outside" (Map.singleton "land" region))
-  assertEqual
-    "disconnected exact components remain separate"
-    2
-    ( maybe
-        0
-        (length . planarRegionComponents)
-        (Map.lookup "land" (planarLayerRegions layer))
-    )
-  assertEqual
-    "interior exact point location"
-    RegionInterior
-    (regionPointLocation region (integerPoint 1 1))
-  assertEqual
-    "exterior exact point location"
-    RegionExterior
-    (regionPointLocation region (integerPoint 3 1))
-
-testOrdinaryPublication :: IO ()
-testOrdinaryPublication = do
-  built <-
-    requireRight
-      "ordinary square triangulation"
-      ( delaunay
-          unitElementDefaults
-          (V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
-      )
-  layer <-
-    requireRight
-      "ordinary square labelled publication"
-      (labelledPlanarLayer "outside" (buildTriangulation built) (const "inside"))
-  case
-    labelledPlanarLayerFromExactCoordinates
-      "outside"
-      (buildTriangulation built)
-      (\vertex -> Left (RegionCoordinateMissing vertex))
-      (const (Right "inside")) of
-    Left (RegionCoordinateMissing _) -> pure ()
-    other -> fail ("missing exact publication coordinate produced " <> show other)
-  assertEqual
-    "ordinary publication omits outside label"
-    ["inside"]
-    (Map.keys (planarLayerRegions layer))
-  let components =
-        maybe [] planarRegionComponents (Map.lookup "inside" (planarLayerRegions layer))
-  assertEqual "ordinary square component count" 1 (length components)
-  assertEqual
-    "ordinary square drops resident diagonal"
-    [4]
-    (map (length . exactLoopPoints . polygonOuterLoop) components)
-
-testExactCellSetAdmission :: IO ()
-testExactCellSetAdmission = do
-  built <-
-    requireRight
-      "cell-set triangle"
-      (delaunay unitElementDefaults (V.fromList [Point 0 0, Point 2 0, Point 0 2]))
-  let triangulation = buildTriangulation built
-  face <-
-    case innerFaces triangulation of
-      [singleFace] -> pure singleFace
-      faces -> fail ("cell-set triangle faces: " <> show faces)
-  closed <- requireRight "closed face cell set" (closeFaceCellSet triangulation [face])
-  assertEqual
-    "face cell set carries its complete downward closure"
-    (3, 3, 1)
-    ( exactCellSetVertexCount closed
-    , exactCellSetEdgeCount closed
-    , exactCellSetFaceCount closed
-    )
-  case exactCellSet triangulation [] [] [outerFace] of
-    Left CellOuterFaceSelected -> pure ()
-    _ -> fail "cell set admitted the unbounded outer face"
-  edge <-
-    case undirectedEdges triangulation of
-      firstEdge : _ -> pure firstEdge
-      [] -> fail "cell-set triangle has no edge"
-  case exactCellSet triangulation [] [edge] [] of
-    Left (CellEdgeBoundaryMissing failedEdge _) ->
-      assertEqual "edge closure witness" edge failedEdge
-    _ -> fail "cell set admitted an edge without its boundary vertices"
-
-testGroupedPublicationFixtures :: IO ()
-testGroupedPublicationFixtures = do
-  triangle <-
-    regionMeshFromPoints
-      "published single triangle"
-      [Point 0 0, Point 2 0, Point 0 2]
-  triangleLayer <-
-    requireRight
-      "published single triangle layer"
-      (labelledPlanarLayer (0 :: Int) triangle (const 1))
-  assertComponentShape "published single triangle" 1 3 [] triangleLayer
-
-  concave <- regionMesh "published concave L" 2 2
-  let concaveLabel =
-        regionFaceSatisfies concave (\(Point x y) -> not (x > 1 && y > 1))
-  concaveLayer <-
-    requireRight
-      "published concave L layer"
-      (labelledPlanarLayer False concave concaveLabel)
-  assertComponentShape "published concave L" True 6 [] concaveLayer
-
-  annulus <- regionMesh "published annulus" 3 3
-  let annulusLabel =
-        regionFaceSatisfies annulus
-          (\(Point x y) -> not (x > 1 && x < 2 && y > 1 && y < 2))
-  annulusLayer <-
-    requireRight
-      "published annulus layer"
-      (labelledPlanarLayer False annulus annulusLabel)
-  assertComponentShape "published annulus" True 4 [4] annulusLayer
-
-  twoHoles <- regionMesh "published two holes" 5 3
-  let twoHoleLabel =
-        regionFaceSatisfies twoHoles $ \(Point x y) ->
-          let cell = (floor x :: Int, floor y :: Int)
-           in cell /= (1, 1) && cell /= (3, 1)
-  twoHoleLayer <-
-    requireRight
-      "published two-hole layer"
-      (labelledPlanarLayer False twoHoles twoHoleLabel)
-  assertComponentShape "published two holes" True 4 [4, 4] twoHoleLayer
-
-  disconnected <- regionMesh "published disconnected islands" 3 1
-  let islandLabel =
-        regionFaceSatisfies disconnected (\(Point x _) -> x < 1 || x > 2)
-  disconnectedLayer <-
-    requireRight
-      "published disconnected layer"
-      (labelledPlanarLayer False disconnected islandLabel)
-  assertEqual
-    "published equal label keeps disconnected components"
-    2
-    (length (componentsFor True disconnectedLayer))
-
-  islandInHole <- regionMesh "published island in hole" 3 3
-  let islandInHoleLabel face =
-        if regionFaceSatisfies islandInHole
-             (\(Point x y) -> x > 1 && x < 2 && y > 1 && y < 2)
-             face
-          then (2 :: Int)
-          else 1
-  nestedLayer <-
-    requireRight
-      "published island-in-hole layer"
-      (labelledPlanarLayer 0 islandInHole islandInHoleLabel)
-  assertComponentShape "published shell around island" 1 4 [4] nestedLayer
-  assertComponentShape "published island inside hole" 2 4 [] nestedLayer
-
-testPinchPublicationRefusal :: IO ()
-testPinchPublicationRefusal = do
-  pinched <- regionMesh "published pinch" 3 3
-  let selected =
-        regionFaceSatisfies pinched $ \(Point x y) ->
-          let cell = (floor x :: Int, floor y :: Int)
-           in cell /= (0, 0) && cell /= (1, 1)
-  case labelledPlanarLayer False pinched selected of
-    Left (RegionBoundaryObstruction BoundaryPinch {}) -> pure ()
-    other -> fail ("pinched publication produced " <> show other)
-
-assertComponentShape
-  :: Ord label
-  => String
-  -> label
-  -> Int
-  -> [Int]
-  -> PlanarLayer label
-  -> IO ()
-assertComponentShape label regionLabel expectedOuterVertices expectedHoleVertices layer =
-  case componentsFor regionLabel layer of
-    [component] -> do
-      assertEqual
-        (label <> " outer vertices")
-        expectedOuterVertices
-        (length (exactLoopPoints (polygonOuterLoop component)))
-      assertEqual
-        (label <> " hole vertices")
-        expectedHoleVertices
-        (map (length . exactLoopPoints) (polygonHoleLoops component))
-    components ->
-      fail (label <> ": expected one component, got " <> show (length components))
-
-componentsFor :: Ord label => label -> PlanarLayer label -> [PolygonComponent]
-componentsFor label =
-  maybe [] planarRegionComponents . Map.lookup label . planarLayerRegions
diff --git a/test/parallel/Main.hs b/test/parallel/Main.hs
--- a/test/parallel/Main.hs
+++ b/test/parallel/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.ParallelSpec as ParallelSpec
+import qualified Moonlight.Planar.ParallelSpec as ParallelSpec
 
 main :: IO ()
 main = ParallelSpec.tests
diff --git a/test/parallel/Moonlight/Planar/ParallelSpec.hs b/test/parallel/Moonlight/Planar/ParallelSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/parallel/Moonlight/Planar/ParallelSpec.hs
@@ -0,0 +1,72 @@
+-- | The one module in the package that spawns threads. Its whole contract is
+-- that scheduling is invisible: the same tournament, the same value, at every
+-- worker count. That is what is asserted here, because a concurrent join whose
+-- result depended on the worker count would be wrong in a way no amount of
+-- validity checking on a single run could see.
+module Moonlight.Planar.ParallelSpec
+  ( tests
+  ) where
+
+import qualified Data.List.NonEmpty as NE
+import Moonlight.Planar.SetAlgebra (unions)
+import Moonlight.Planar.AlgebraFixtures (Mesh, assertMesh, operands)
+import Moonlight.Planar.Parallel (unionsConcurrently)
+import Support (requireRight)
+
+tests :: IO ()
+tests = do
+  testConcurrentJoinAgreesWithSequential
+  testConcurrentJoinIsWorkerCountInvariant
+  putStrLn "parallel: ok"
+
+-- Counts on both sides of the operand count, so the tournament is starved at
+-- one end and saturated at the other, and 1 exercises the sequential-collapse
+-- branch that a purely concurrent test would never reach.
+workerCounts :: [Int]
+workerCounts = [1, 2, 3, 5, 16]
+
+concurrentJoin :: Int -> NE.NonEmpty Mesh -> IO Mesh
+concurrentJoin workers shards = do
+  outcome <- unionsConcurrently workers shards
+  requireRight ("concurrent unions at " <> show workers <> " workers") outcome
+
+-- The sequential tournament is the oracle. It is a different interpreter over
+-- the same plan, not a second copy of this one, so agreement is evidence.
+testConcurrentJoinAgreesWithSequential :: IO ()
+testConcurrentJoinAgreesWithSequential = do
+  shards <- fmap (map snd) operands
+  nonEmptyShards <- case NE.nonEmpty shards of
+    Nothing -> fail "concurrent join agreement: no operands"
+    Just present -> pure present
+  sequential <- requireRight "sequential unions" (unions shards)
+  mapM_
+    ( \workers -> do
+        concurrent <- concurrentJoin workers nonEmptyShards
+        assertMesh
+          ("concurrent tournament at " <> show workers <> " workers")
+          sequential
+          concurrent
+    )
+    workerCounts
+
+-- Stated separately from agreement: even if both interpreters were wrong in
+-- the same way, a result that moved with the worker count would still be a
+-- defect, and this is the assertion that would catch it.
+testConcurrentJoinIsWorkerCountInvariant :: IO ()
+testConcurrentJoinIsWorkerCountInvariant = do
+  shards <- fmap (map snd) operands
+  nonEmptyShards <- case NE.nonEmpty shards of
+    Nothing -> fail "worker-count invariance: no operands"
+    Just present -> pure present
+  results <- traverse (`concurrentJoin` nonEmptyShards) workerCounts
+  case results of
+    [] -> fail "worker-count invariance: no worker counts"
+    (reference : rest) ->
+      mapM_
+        ( \(workers, result) ->
+            assertMesh
+              ("worker count " <> show workers <> " agrees with the first")
+              reference
+              result
+        )
+        (zip (drop 1 workerCounts) rest)
diff --git a/test/parallel/Moonlight/Triangulation/ParallelSpec.hs b/test/parallel/Moonlight/Triangulation/ParallelSpec.hs
deleted file mode 100644
--- a/test/parallel/Moonlight/Triangulation/ParallelSpec.hs
+++ /dev/null
@@ -1,72 +0,0 @@
--- | The one module in the package that spawns threads. Its whole contract is
--- that scheduling is invisible: the same tournament, the same value, at every
--- worker count. That is what is asserted here, because a concurrent join whose
--- result depended on the worker count would be wrong in a way no amount of
--- validity checking on a single run could see.
-module Moonlight.Triangulation.ParallelSpec
-  ( tests
-  ) where
-
-import qualified Data.List.NonEmpty as NE
-import Moonlight.Triangulation (unions)
-import Moonlight.Triangulation.AlgebraFixtures (Mesh, assertMesh, operands)
-import Moonlight.Triangulation.Parallel (unionsConcurrently)
-import Support (requireRight)
-
-tests :: IO ()
-tests = do
-  testConcurrentJoinAgreesWithSequential
-  testConcurrentJoinIsWorkerCountInvariant
-  putStrLn "parallel: ok"
-
--- Counts on both sides of the operand count, so the tournament is starved at
--- one end and saturated at the other, and 1 exercises the sequential-collapse
--- branch that a purely concurrent test would never reach.
-workerCounts :: [Int]
-workerCounts = [1, 2, 3, 5, 16]
-
-concurrentJoin :: Int -> NE.NonEmpty Mesh -> IO Mesh
-concurrentJoin workers shards = do
-  outcome <- unionsConcurrently workers shards
-  requireRight ("concurrent unions at " <> show workers <> " workers") outcome
-
--- The sequential tournament is the oracle. It is a different interpreter over
--- the same plan, not a second copy of this one, so agreement is evidence.
-testConcurrentJoinAgreesWithSequential :: IO ()
-testConcurrentJoinAgreesWithSequential = do
-  shards <- fmap (map snd) operands
-  nonEmptyShards <- case NE.nonEmpty shards of
-    Nothing -> fail "concurrent join agreement: no operands"
-    Just present -> pure present
-  sequential <- requireRight "sequential unions" (unions shards)
-  mapM_
-    ( \workers -> do
-        concurrent <- concurrentJoin workers nonEmptyShards
-        assertMesh
-          ("concurrent tournament at " <> show workers <> " workers")
-          sequential
-          concurrent
-    )
-    workerCounts
-
--- Stated separately from agreement: even if both interpreters were wrong in
--- the same way, a result that moved with the worker count would still be a
--- defect, and this is the assertion that would catch it.
-testConcurrentJoinIsWorkerCountInvariant :: IO ()
-testConcurrentJoinIsWorkerCountInvariant = do
-  shards <- fmap (map snd) operands
-  nonEmptyShards <- case NE.nonEmpty shards of
-    Nothing -> fail "worker-count invariance: no operands"
-    Just present -> pure present
-  results <- traverse (`concurrentJoin` nonEmptyShards) workerCounts
-  case results of
-    [] -> fail "worker-count invariance: no worker counts"
-    (reference : rest) ->
-      mapM_
-        ( \(workers, result) ->
-            assertMesh
-              ("worker count " <> show workers <> " agrees with the first")
-              reference
-              result
-        )
-        (zip (drop 1 workerCounts) rest)
diff --git a/test/persistence-rose/Main.hs b/test/persistence-rose/Main.hs
--- a/test/persistence-rose/Main.hs
+++ b/test/persistence-rose/Main.hs
@@ -3,7 +3,7 @@
 import Data.ByteString qualified as ByteString
 import Data.List (isInfixOf, isPrefixOf, isSuffixOf)
 import Data.Word (Word8)
-import Moonlight.Triangulation.Exhibit.PersistenceRose
+import Moonlight.Planar.Exhibit.PersistenceRose
   ( AlphaEclipseReceipt (..),
     AlphaEclipseScene,
     PersistenceRoseAnimationReceipt (..),
diff --git a/test/public-components/Main.hs b/test/public-components/Main.hs
--- a/test/public-components/Main.hs
+++ b/test/public-components/Main.hs
@@ -2,35 +2,47 @@
 
 import Data.Bifunctor (first)
 import qualified Data.Vector as Vector
-import Moonlight.Triangulation.BulkLoad (delaunayGeometry)
-import Moonlight.Triangulation.Dcel (numVertices)
-import Moonlight.Triangulation.FloodFillIterator
+import qualified Moonlight.Planar.AdmissionSpec as AdmissionSpec
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Dcel (numVertices)
+import Moonlight.Planar.FloodFillIterator
   ( RectangleMetricError
   , verticesInRectangle
   )
-import Moonlight.Triangulation.Handles.Iterators.DynamicIterators
+import Moonlight.Planar.Handles.Iterators.DynamicIterators
   ( vertexHandles
   )
-import Moonlight.Triangulation.Handles.Scoped
+import Moonlight.Planar.Handles.Scoped
   ( scopedVertexPoint
   , scopedVertices
   , withScopedTriangulation
   )
-import Moonlight.Triangulation.PointLocation (locatePoint)
-import Moonlight.Triangulation.Session
+import Moonlight.Planar.HintGenerator (buildHierarchyHint, hierarchyHint)
+import Moonlight.Planar.PointLocation (locatePoint)
+import Moonlight.Planar.Refinement
+  ( refinementAdditionalVertexBudget
+  , refinementExcludesOuterFaces
+  , refinementMaximumArea
+  , refinementMinimumArea
+  , refinementPreservesConstraints
+  , validateRefinementParameters
+  , withAdditionalVertexBudget
+  , withConstraintPreservation
+  , withMaximumArea
+  , withMinimumArea
+  , withOuterFaceExclusion
+  )
+import Moonlight.Planar.Session
   ( insertVertexAtNearVertex
   , removeAt
   , withSession
   )
-import Moonlight.Triangulation.Types
-  ( BuildError
-  , InsertionDisposition (Inserted)
-  , Location (OnVertex)
-  , Point (..)
-  , PointValidationError
-  )
-import Moonlight.Triangulation.Math (mkQueryPoint, orient2d)
-import Moonlight.Triangulation.Validation (validateTriangulation)
+import Moonlight.Planar.Types (BuildError, defaultRefinementParameters, InsertionDisposition (Inserted), Location (OnVertex))
+import Moonlight.Planar.Point (Point (..), PointValidationError)
+import qualified Moonlight.Planar.Telemetry as Telemetry
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Math (orient2d)
+import Moonlight.Planar.Validation (validateTriangulation)
 import System.Exit (die)
 
 data PublicComponentFailure
@@ -47,6 +59,7 @@
   , rectangleVertices :: !Int
   , locatedResidentVertex :: !Bool
   , admittedPredicateCorrect :: !Bool
+  , hierarchyTelemetryNearestCorrect :: !Bool
   , sessionInsertedVertex :: !Bool
   , sessionRemovedVertex :: !Bool
   , finalMeshValid :: !Bool
@@ -54,12 +67,39 @@
   deriving stock (Eq, Show)
 
 main :: IO ()
-main =
+main = do
+  AdmissionSpec.tests
+  assertRefinementParameterAPI
   either
     (die . ("public component workflow refused: " <>) . show)
     assertExpected
     publicComponentWorkflow
 
+assertRefinementParameterAPI :: IO ()
+assertRefinementParameterAPI =
+  case do
+    withAdditionalVertexBudget 32 defaultRefinementParameters
+      >>= withMaximumArea 4
+      >>= withMinimumArea 1
+      >>= pure . withConstraintPreservation True
+      >>= pure . withOuterFaceExclusion True
+  of
+    Left obstruction -> die ("checked refinement authoring refused: " <> show obstruction)
+    Right parameters -> do
+      case validateRefinementParameters parameters of
+        Left obstruction -> die ("admitted refinement parameters failed validation: " <> show obstruction)
+        Right () -> pure ()
+      if refinementAdditionalVertexBudget parameters == Just 32
+          && refinementMinimumArea parameters == Just 1
+          && refinementMaximumArea parameters == Just 4
+          && refinementPreservesConstraints parameters
+          && refinementExcludesOuterFaces parameters
+        then pure ()
+        else die "checked refinement observations disagreed with their setters"
+      case withMaximumArea 2 defaultRefinementParameters >>= withMinimumArea 3 of
+        Left _ -> pure ()
+        Right _ -> die "minimum area greater than maximum area was admitted"
+
 publicComponentWorkflow :: Either PublicComponentFailure PublicComponentSummary
 publicComponentWorkflow = do
   square <-
@@ -77,6 +117,10 @@
   seed <- case locatePoint square query of
     OnVertex vertex -> Right vertex
     _ -> Left PublicSeedVertexMissing
+  hierarchy <- first PublicBuildFailure (buildHierarchyHint 2 square)
+  let hintedVertex = case hierarchyHint hierarchy query of
+        Just (Telemetry.VertexHint vertex) -> Just vertex
+        _ -> Nothing
   ((disposition, removed), restored, _) <-
     first PublicBuildFailure
       ( withSession square 1 $ do
@@ -101,6 +145,9 @@
           OnVertex _ -> True
           _ -> False
       , admittedPredicateCorrect = orient2d query queryRight queryTop == GT
+      , hierarchyTelemetryNearestCorrect =
+          hintedVertex /= Nothing
+            && fmap fst (Telemetry.nearestNeighbor square hintedVertex query) == Just seed
       , sessionInsertedVertex = disposition == Inserted
       , sessionRemovedVertex = maybe False (const True) removed
       , finalMeshValid = null (validateTriangulation restored)
@@ -125,6 +172,7 @@
       , rectangleVertices = 4
       , locatedResidentVertex = True
       , admittedPredicateCorrect = True
+      , hierarchyTelemetryNearestCorrect = True
       , sessionInsertedVertex = True
       , sessionRemovedVertex = True
       , finalMeshValid = True
diff --git a/test/public-components/Moonlight/Planar/AdmissionSpec.hs b/test/public-components/Moonlight/Planar/AdmissionSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/Moonlight/Planar/AdmissionSpec.hs
@@ -0,0 +1,183 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeApplications #-}
+
+-- | Public admission controls and attempted reconstruction of invalid carriers.
+module Moonlight.Planar.AdmissionSpec (tests) where
+
+import Control.DeepSeq (rnf)
+import Control.Exception (TypeError, displayException, evaluate, try)
+import Data.Foldable (traverse_)
+import Data.List (isInfixOf)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Vector as Vector
+import GHC.Generics (from, to)
+import GHC.Records (getField)
+import qualified Moonlight.Planar.AdmissionTypeErrors as Rejected
+import Moonlight.Planar.BulkLoad (delaunayGeometry)
+import Moonlight.Planar.Convex (convexPolygon)
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactClosedHalfPlane, exactPointFromPoint, exactRetainedPolygon, exactRetainedPolygonPoints
+  , exactRational, exactAffineLine, exactClosedHalfPlane, exactSegment, exactRay
+  , ExactVector (..), ExactArithmeticError (..), ExactHalfPlaneError (..), ExactGeometryError (..)
+  )
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.PowerDiagram
+  ( PowerCellDisposition (..), RegularTriangulation, boundedPowerDiagramFromRegular
+  , emptyRegularTriangulation, powerCellDispositions, regularSiteCount
+  , powerSite, powerWeightFromExact
+  )
+import Moonlight.Planar.Region
+  ( exactLoop, exactLoopPoints, polygonComponent, polygonOuterLoop, polygonHoleLoops
+  , planarRegion, planarRegionComponents, planarLayer, planarLayerOutsideLabel
+  , planarLayerRegions, RegionValidationError (..)
+  )
+import Moonlight.Planar.Point (Point (..), queryPointValue)
+import Moonlight.Planar.Scalar (mkRadiusSquared, radiusSquaredValue)
+import Moonlight.Planar.Alpha (alphaBirthFromRadiusSquared, alphaBirthToDouble)
+import Moonlight.Planar.Simplex (planarVertex, planarComplex, planarSimplexVertices, planarComplexCells)
+import System.Exit (die)
+
+tests :: IO ()
+tests = do
+  triangulation <- requireRight (delaunayGeometry (Vector.fromList [Point 0 0, Point 2 0, Point 0 2]))
+  points <- requireRight (traverse exactPointFromPoint (Point 0 0 :| [Point 2 0, Point 0 2]))
+  clockwisePoints <- requireRight (traverse exactPointFromPoint (Point 0 0 :| [Point 0 2, Point 2 0]))
+  loop <- requireRight (exactLoop points)
+  clockwise <- requireRight (exactLoop clockwisePoints)
+  component <- requireRight (polygonComponent loop [])
+  region <- requireRight (planarRegion [component])
+  layer <- requireRight (planarLayer False (Map.singleton True region))
+  domain <- requireRight (convexPolygon points)
+  retained <- requireRight (exactRetainedPolygon points)
+  rational <- requireRight (exactRational 1 2)
+  line <- requireRight (exactAffineLine 1 0 0)
+  segment <- requireRight (exactSegment (firstPoint points) (secondPoint points))
+  ray <- requireRight (exactRay (firstPoint points) (ExactVector 1 0))
+  query <- requireRight (mkQueryPoint (Point 0 0))
+  radius <- requireRight (mkRadiusSquared 4)
+  let birth = alphaBirthFromRadiusSquared radius
+      simplex = planarVertex (7 :: Int)
+  complex <- requireRight (planarComplex (Set.singleton simplex))
+  site <- requireRight (powerSite False (Point 0 0) (powerWeightFromExact 0))
+  let regular = emptyRegularTriangulation :: RegularTriangulation Bool
+  (diagram, _) <- requireRight (boundedPowerDiagramFromRegular domain regular)
+  traverse_ evaluate
+    [ rnf triangulation, rnf rational, rnf line, rnf (exactClosedHalfPlane line), rnf retained
+    , rnf segment, rnf ray, rnf query, rnf loop, rnf component, rnf region
+    , rnf layer, rnf regular, rnf diagram
+    , rnf radius, rnf birth, rnf simplex, rnf complex
+    ]
+  assert "read-only observations preserve admitted values"
+    ( exactLoopPoints loop == points
+        && polygonOuterLoop component == loop
+        && null (polygonHoleLoops component)
+        && planarRegionComponents region == [component]
+        && not (planarLayerOutsideLabel layer)
+        && planarLayerRegions layer == Map.singleton True region
+        && regularSiteCount regular == 0
+        && null (powerCellDispositions diagram)
+        && exactLoop (exactRetainedPolygonPoints retained) == Right loop
+        && queryPointValue query == Point 0 0
+        && radiusSquaredValue radius == 4
+        && alphaBirthToDouble birth == 4
+        && planarSimplexVertices simplex == (7 :| [])
+        && planarComplexCells complex == Set.singleton simplex
+    )
+  assert "unconstrained Point retains its lawful HasField selector"
+    (getField @"pointX" (Point 3 4) == 3)
+  assert "unconstrained Generic carriers remain lawful"
+    ( (to (from (firstPoint points)) :: ExactPoint) == firstPoint points
+        && (to (from (exactClosedHalfPlane line)) :: ExactClosedHalfPlane) == exactClosedHalfPlane line
+    )
+  assert "checked loop constructor rejects one point"
+    (case exactLoop (firstPoint points :| []) of Left (RegionLoopDegenerate _) -> True; _ -> False)
+  assert "checked component constructor rejects clockwise outer"
+    (case polygonComponent clockwise [] of Left (RegionOuterLoopWinding LT) -> True; _ -> False)
+  assert "checked region constructor rejects duplicate interiors"
+    (case planarRegion [component, component] of Left (RegionComponentInteriorOverlap 0 1) -> True; _ -> False)
+  assert "checked layer constructor rejects outside label ownership"
+    (case planarLayer False (Map.singleton False region) of Left RegionOutsideLabelUsed -> True; _ -> False)
+  assert "checked rational constructor rejects zero denominator"
+    (case exactRational 1 0 of Left ExactZeroDenominator -> True; _ -> False)
+  assert "checked affine line constructor rejects zero normal"
+    (case exactAffineLine 0 0 0 of Left (ExactAffineLineZeroNormal 0 0 0) -> True; _ -> False)
+  assert "checked retained polygon rejects singleton"
+    (case exactRetainedPolygon (firstPoint points :| []) of Left (ExactRetainedPolygonTooFewVertices 1) -> True; _ -> False)
+  assert "checked segment rejects coincident endpoints"
+    (case exactSegment (firstPoint points) (firstPoint points) of Left (ExactSegmentEndpointsCoincide _) -> True; _ -> False)
+  assert "checked ray rejects zero direction"
+    (case exactRay (firstPoint points) (ExactVector 0 0) of Left (ExactRayZeroDirection _) -> True; _ -> False)
+  assert "checked query point rejects NaN"
+    (case mkQueryPoint (Point (0 / 0) 0) of Left _ -> True; _ -> False)
+  assert "checked radius rejects negatives"
+    (case mkRadiusSquared (-1) of Left _ -> True; _ -> False)
+  traverse_ (uncurry assertGenericRejected)
+    [ ("Triangulation", Rejected.reconstructTriangulation triangulation `seq` ())
+    , ("ExactLoop", Rejected.forgeExactLoop (fmap (const (firstPoint points)) points) `seq` ())
+    , ("PolygonComponent", Rejected.forgePolygonComponent clockwise `seq` ())
+    , ("PlanarRegion", Rejected.forgePlanarRegion component `seq` ())
+    , ("PlanarLayer", Rejected.forgePlanarLayer region `seq` ())
+    , ("BoundedPowerDiagram", Rejected.forgeBoundedPowerDiagram
+        (Map.fromList [(False, PublishedPowerCell domain), (True, PublishedPowerCell domain)]) `seq` ())
+    , ("RegularTriangulation", Rejected.forgeRegularTriangulation site `seq` ())
+    , ("ExactRational", Rejected.forgeExactRational `seq` ())
+    , ("ExactAffineLine", Rejected.forgeExactAffineLine `seq` ())
+    , ("ExactRetainedPolygon", Rejected.forgeRetainedPolygon retained `seq` ())
+    , ("ExactSegment", Rejected.forgeExactSegment (firstPoint points) `seq` ())
+    , ("ExactRay", Rejected.forgeExactRay (firstPoint points) `seq` ())
+    , ("QueryPoint", Rejected.forgeQueryPoint (Point (0 / 0) 0) `seq` ())
+    , ("BuildStats", Rejected.forgeBuildStats `seq` ())
+    , ("RadiusSquared", Rejected.forgeRadiusSquared `seq` ())
+    , ("AlphaBirth", Rejected.reconstructAlphaBirth birth `seq` ())
+    , ("PlanarSimplex", Rejected.reconstructPlanarSimplex simplex `seq` ())
+    , ("PlanarComplex", Rejected.reconstructPlanarComplex complex `seq` ())
+    ]
+  traverse_ (uncurry assertRecordSelectorRejected)
+    [ ("polygonOuterLoop", snd (Rejected.recordPolygonOuter component) `seq` ())
+    , ("polygonHoleLoops", snd (Rejected.recordPolygonHoles component) `seq` ())
+    , ("planarLayerOutsideLabel", snd (Rejected.recordLayerOutside layer) `seq` ())
+    , ("planarLayerRegions", snd (Rejected.recordLayerRegions layer) `seq` ())
+    , ("queryPointValue", snd (Rejected.recordQueryPoint query) `seq` ())
+    ]
+  assert "ordinary observers remain callable beside rejected HasField dictionaries"
+    ( fst (Rejected.recordPolygonOuter component) == loop
+        && null (fst (Rejected.recordPolygonHoles component))
+        && not (fst (Rejected.recordLayerOutside layer))
+        && fst (Rejected.recordLayerRegions layer) == Map.singleton True region
+        && fst (Rejected.recordQueryPoint query) == Point 0 0
+    )
+  putStrLn "public admission: Generic and HasField reconstruction routes rejected"
+ where
+  firstPoint :: NonEmpty ExactPoint -> ExactPoint
+  firstPoint (point :| _) = point
+  secondPoint :: NonEmpty ExactPoint -> ExactPoint
+  secondPoint (_ :| (point : _)) = point
+  secondPoint (point :| []) = point
+
+requireRight :: Show obstruction => Either obstruction value -> IO value
+requireRight = either (die . ("admission positive control failed: " <>) . show) pure
+
+assert :: String -> Bool -> IO ()
+assert label condition = if condition then pure () else die label
+
+assertGenericRejected :: String -> () -> IO ()
+assertGenericRejected carrier =
+  assertTypeRejected carrier
+    (\diagnostic -> carrier `isInfixOf` diagnostic
+      && ("Generic" `isInfixOf` diagnostic || "Rep" `isInfixOf` diagnostic))
+
+assertRecordSelectorRejected :: String -> () -> IO ()
+assertRecordSelectorRejected field =
+  assertTypeRejected field
+    (\diagnostic -> field `isInfixOf` diagnostic && "HasField" `isInfixOf` diagnostic)
+
+assertTypeRejected :: String -> (String -> Bool) -> () -> IO ()
+assertTypeRejected label expectedDiagnostic attempted = do
+  result <- try (evaluate attempted) :: IO (Either TypeError ())
+  case result of
+    Left failure ->
+      assert (label <> " failed for the wrong reason: " <> displayException failure)
+        (expectedDiagnostic (displayException failure))
+    Right () -> die (label <> " bypassed checked admission")
diff --git a/test/public-components/Moonlight/Planar/AdmissionTypeErrors.hs b/test/public-components/Moonlight/Planar/AdmissionTypeErrors.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/Moonlight/Planar/AdmissionTypeErrors.hs
@@ -0,0 +1,109 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -fdefer-type-errors -Wno-deferred-type-errors #-}
+
+-- | These public-client attempts must be rejected at the admission boundary.
+-- The test driver catches only deferred type errors, never arbitrary exceptions.
+module Moonlight.Planar.AdmissionTypeErrors where
+
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Map.Strict as Map
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word64)
+import GHC.Generics (K1 (..), M1 (..), (:*:) (..), from, to)
+import GHC.Records (getField)
+import GHC.Real (Ratio ((:%)))
+import Moonlight.Planar.Exact
+  ( ExactPoint, ExactRational, ExactAffineLine, ExactRetainedPolygon, ExactSegment, ExactRay
+  , ExactVector (..)
+  )
+import Moonlight.Planar.Point (Point, QueryPoint, queryPointValue)
+import Moonlight.Planar.Types (DelaunayTriangulation)
+import Moonlight.Planar.BuildStats (BuildStats)
+import Moonlight.Planar.Scalar (RadiusSquared)
+import Moonlight.Planar.Alpha (AlphaBirth)
+import Moonlight.Planar.Simplex (PlanarSimplex, PlanarComplex)
+import Moonlight.Planar.PowerDiagram
+  ( BoundedPowerDiagram, PowerCellDisposition, RegularTriangulation, PowerSite )
+import Moonlight.Planar.Region
+  ( ExactLoop, PolygonComponent, PlanarRegion, PlanarLayer
+  , polygonOuterLoop, polygonHoleLoops, planarLayerOutsideLabel, planarLayerRegions
+  )
+
+forgeExactLoop :: NonEmpty ExactPoint -> ExactLoop
+forgeExactLoop points = to (M1 (M1 (M1 (K1 points))))
+
+forgePolygonComponent :: ExactLoop -> PolygonComponent
+forgePolygonComponent clockwise =
+  to (M1 (M1 (M1 (K1 clockwise) :*: M1 (K1 ([] :: [ExactLoop])))))
+
+forgePlanarRegion :: PolygonComponent -> PlanarRegion
+forgePlanarRegion component = to (M1 (M1 (M1 (K1 [component, component]))))
+
+forgePlanarLayer :: PlanarRegion -> PlanarLayer Bool
+forgePlanarLayer region =
+  to (M1 (M1 (M1 (K1 False) :*: M1 (K1 (Map.singleton False region)))))
+
+forgeBoundedPowerDiagram :: Map.Map Bool (PowerCellDisposition Bool) -> BoundedPowerDiagram Bool
+forgeBoundedPowerDiagram cells = to (M1 (M1 (M1 (K1 cells))))
+
+forgeRegularTriangulation :: PowerSite Bool -> RegularTriangulation Bool
+forgeRegularTriangulation site =
+  to (M1 (M1 (M1 (K1 (Map.singleton False site)) :*: M1 (K1 Nothing))))
+
+forgeExactRational :: ExactRational
+forgeExactRational = to (M1 (M1 (M1 (K1 (1 :% 0 :: Ratio Integer)))))
+
+forgeExactAffineLine :: ExactAffineLine
+forgeExactAffineLine =
+  to (M1 (M1 (M1 (K1 (0 :: ExactRational)) :*: (M1 (K1 (0 :: ExactRational)) :*: M1 (K1 (0 :: ExactRational))))))
+
+forgeRetainedPolygon :: ExactRetainedPolygon -> ExactRetainedPolygon
+forgeRetainedPolygon polygon =
+  case from polygon of
+    M1 (M1 (M1 (K1 (vertex :| _)))) -> to (M1 (M1 (M1 (K1 (vertex :| [])))))
+
+forgeExactSegment :: ExactPoint -> ExactSegment
+forgeExactSegment point = to (M1 (M1 (M1 (K1 point) :*: M1 (K1 point))))
+
+forgeExactRay :: ExactPoint -> ExactRay
+forgeExactRay point = to (M1 (M1 (M1 (K1 point) :*: M1 (K1 (ExactVector 0 0)))))
+
+forgeQueryPoint :: Point -> QueryPoint
+forgeQueryPoint point = to (M1 (M1 (M1 (K1 point))))
+
+forgeBuildStats :: BuildStats
+forgeBuildStats = to (M1 (M1 (M1 (K1 (U.empty :: U.Vector Word64)))))
+
+
+-- The left observations are lawful functions. The right attempted HasField
+-- dictionaries would restore the record-selector capability and must fail.
+recordPolygonOuter :: PolygonComponent -> (ExactLoop, ExactLoop)
+recordPolygonOuter component = (polygonOuterLoop component, getField @"polygonOuterLoop" component)
+
+recordPolygonHoles :: PolygonComponent -> ([ExactLoop], [ExactLoop])
+recordPolygonHoles component = (polygonHoleLoops component, getField @"polygonHoleLoops" component)
+
+recordLayerOutside :: PlanarLayer Bool -> (Bool, Bool)
+recordLayerOutside layer = (planarLayerOutsideLabel layer, getField @"planarLayerOutsideLabel" layer)
+
+recordLayerRegions :: PlanarLayer Bool -> (Map.Map Bool PlanarRegion, Map.Map Bool PlanarRegion)
+recordLayerRegions layer = (planarLayerRegions layer, getField @"planarLayerRegions" layer)
+
+recordQueryPoint :: QueryPoint -> (Point, Point)
+recordQueryPoint query = (queryPointValue query, getField @"queryPointValue" query)
+
+reconstructTriangulation :: DelaunayTriangulation () -> DelaunayTriangulation ()
+reconstructTriangulation triangulation = to (from triangulation)
+
+forgeRadiusSquared :: RadiusSquared
+forgeRadiusSquared = to (M1 (M1 (M1 (K1 (-1 :: Double)))))
+
+reconstructAlphaBirth :: AlphaBirth -> AlphaBirth
+reconstructAlphaBirth birth = to (from birth)
+
+reconstructPlanarSimplex :: PlanarSimplex Int -> PlanarSimplex Int
+reconstructPlanarSimplex simplex = to (from simplex)
+
+reconstructPlanarComplex :: PlanarComplex Int -> PlanarComplex Int
+reconstructPlanarComplex complex = to (from complex)
diff --git a/test/public-components/compile-fail/LayerOutsideUpdate.hs b/test/public-components/compile-fail/LayerOutsideUpdate.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/compile-fail/LayerOutsideUpdate.hs
@@ -0,0 +1,10 @@
+-- Expected: GHC-22385, record field planarLayerOutsideLabel not in scope.
+module Client (observe, attempt) where
+
+import Moonlight.Planar.Region (PlanarLayer, planarLayerOutsideLabel)
+
+observe :: PlanarLayer Bool -> Bool
+observe = planarLayerOutsideLabel
+
+attempt :: PlanarLayer Bool -> Bool -> PlanarLayer Bool
+attempt layer outside = layer { planarLayerOutsideLabel = outside }
diff --git a/test/public-components/compile-fail/LayerRegionsUpdate.hs b/test/public-components/compile-fail/LayerRegionsUpdate.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/compile-fail/LayerRegionsUpdate.hs
@@ -0,0 +1,11 @@
+-- Expected: GHC-22385, record field planarLayerRegions not in scope.
+module Client (observe, attempt) where
+
+import Data.Map.Strict (Map)
+import Moonlight.Planar.Region (PlanarLayer, PlanarRegion, planarLayerRegions)
+
+observe :: PlanarLayer Bool -> Map Bool PlanarRegion
+observe = planarLayerRegions
+
+attempt :: PlanarLayer Bool -> Map Bool PlanarRegion -> PlanarLayer Bool
+attempt layer regions = layer { planarLayerRegions = regions }
diff --git a/test/public-components/compile-fail/PolygonHolesUpdate.hs b/test/public-components/compile-fail/PolygonHolesUpdate.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/compile-fail/PolygonHolesUpdate.hs
@@ -0,0 +1,10 @@
+-- Expected: GHC-22385, record field polygonHoleLoops not in scope.
+module Client (observe, attempt) where
+
+import Moonlight.Planar.Region (ExactLoop, PolygonComponent, polygonHoleLoops)
+
+observe :: PolygonComponent -> [ExactLoop]
+observe = polygonHoleLoops
+
+attempt :: PolygonComponent -> [ExactLoop] -> PolygonComponent
+attempt component holes = component { polygonHoleLoops = holes }
diff --git a/test/public-components/compile-fail/PolygonOuterUpdate.hs b/test/public-components/compile-fail/PolygonOuterUpdate.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/compile-fail/PolygonOuterUpdate.hs
@@ -0,0 +1,10 @@
+-- Expected: GHC-22385, record field polygonOuterLoop not in scope.
+module Client (observe, attempt) where
+
+import Moonlight.Planar.Region (ExactLoop, PolygonComponent, polygonOuterLoop)
+
+observe :: PolygonComponent -> ExactLoop
+observe = polygonOuterLoop
+
+attempt :: PolygonComponent -> ExactLoop -> PolygonComponent
+attempt component outer = component { polygonOuterLoop = outer }
diff --git a/test/public-components/compile-fail/QueryPointUpdate.hs b/test/public-components/compile-fail/QueryPointUpdate.hs
new file mode 100644
--- /dev/null
+++ b/test/public-components/compile-fail/QueryPointUpdate.hs
@@ -0,0 +1,10 @@
+-- Expected: GHC-22385, record field queryPointValue not in scope.
+module Client (observe, attempt) where
+
+import Moonlight.Planar.Point (Point, QueryPoint, queryPointValue)
+
+observe :: QueryPoint -> Point
+observe = queryPointValue
+
+attempt :: QueryPoint -> Point -> QueryPoint
+attempt query point = query { queryPointValue = point }
diff --git a/test/serialization/Main.hs b/test/serialization/Main.hs
--- a/test/serialization/Main.hs
+++ b/test/serialization/Main.hs
@@ -1,6 +1,6 @@
 module Main (main) where
 
-import qualified Moonlight.Triangulation.SerializationSpec as SerializationSpec
+import qualified Moonlight.Planar.SerializationSpec as SerializationSpec
 
 main :: IO ()
 main = SerializationSpec.tests
diff --git a/test/serialization/Moonlight/Planar/SerializationFixtures.hs b/test/serialization/Moonlight/Planar/SerializationFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/serialization/Moonlight/Planar/SerializationFixtures.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Storage-shape fixtures shared only by serialization tests and measurements.
+-- The persistent insertion is real public construction; flattening is an
+-- independent test projection of the same logical value, never a wire owner.
+module Moonlight.Planar.SerializationFixtures
+  ( SerializationFixture
+  , serializationFixtures
+  , serializationGridFixtures
+  ) where
+
+import qualified Data.Vector as V
+import Moonlight.Planar.Internal.BulkLoad (delaunay, insertAt)
+import Moonlight.Planar.Internal.BoxedPaged
+  ( BoxedPaged, boxedDefaulted, boxedFill, boxedFromVector, boxedPagedLength
+  , boxedToVector, boxedUpdate
+  )
+import Moonlight.Planar.Internal.Paged (fromLocalVector, fromVector, pagedLength, toVector)
+import Moonlight.Planar.Internal.Representation
+  ( BuildResult (buildTriangulation), InsertionResult (insertionTriangulation)
+  , Triangulation (..), mapVertices
+  )
+import Moonlight.Planar.Internal.Types (BuildError, ConstraintMode (Unconstrained), ElementDefaults (..))
+import Moonlight.Planar.Point (Point (..))
+
+type SerializationFixture = Triangulation 'Unconstrained Int Int Int Int
+
+serializationFixtures :: Int -> Either BuildError [(String, SerializationFixture)]
+serializationFixtures count =
+  serializationFixturesForPoints
+    (V.generate count (\index -> Point (fromIntegral index) 0))
+    (Point (fromIntegral count) 0)
+
+serializationGridFixtures :: Int -> Either BuildError [(String, SerializationFixture)]
+serializationGridFixtures count =
+  serializationFixturesForPoints
+    (V.generate count (\index -> let (row, column) = index `quotRem` 128 in Point (fromIntegral column) (fromIntegral row)))
+    (Point (-1) (-1))
+
+serializationFixturesForPoints
+  :: V.Vector Point
+  -> Point
+  -> Either BuildError [(String, SerializationFixture)]
+serializationFixturesForPoints points arrival = do
+  built <- delaunay (ElementDefaults 11 13 17) points
+  let initial :: SerializationFixture
+      initial = mapVertices (const 3) (buildTriangulation built)
+      defaulted :: SerializationFixture
+      defaulted =
+        initial
+          { triVertexData = boxedDefaulted 3 (pagedLength (triPointX initial))
+          , triDirectedData = boxedDefaulted 11 (pagedLength (triHalfTopology initial) `quot` 4)
+          , triUndirectedData = boxedDefaulted 13 (pagedLength (triConstraint initial))
+          , triFaceData = boxedDefaulted 17 (pagedLength (triFaceEdge initial))
+          }
+      partiallyMaterialized :: SerializationFixture
+      partiallyMaterialized =
+        defaulted
+          { triVertexData = writeBoundaryPayloads (triVertexData defaulted)
+          , triDirectedData = writeBoundaryPayloads (triDirectedData defaulted)
+          , triUndirectedData = writeBoundaryPayloads (triUndirectedData defaulted)
+          , triFaceData = writeBoundaryPayloads (triFaceData defaulted)
+          }
+      materialized :: SerializationFixture
+      materialized =
+        defaulted
+          { triVertexData = materialize (triVertexData defaulted)
+          , triDirectedData = materialize (triDirectedData defaulted)
+          , triUndirectedData = materialize (triUndirectedData defaulted)
+          , triFaceData = materialize (triFaceData defaulted)
+          }
+  shared <- insertionTriangulation <$> insertAt partiallyMaterialized arrival 23
+  shortTail <- insertionTriangulation <$> insertAt materialized arrival 23
+  pure
+    [ ("flat-dense", flatten materialized)
+    , ("flat-vertex-dense", flatten initial)
+    , ("flat-defaulted", flatten defaulted)
+    , ("flat-partial", flatten partiallyMaterialized)
+    , ("persistent-partial", shared)
+    , ("persistent-materialized-tail", shortTail)
+    ]
+ where
+  materialize :: BoxedPaged requirement Int -> BoxedPaged requirement Int
+  materialize payloads = boxedFromVector (boxedFill payloads) (boxedToVector payloads)
+
+  writeBoundaryPayloads :: BoxedPaged requirement Int -> BoxedPaged requirement Int
+  writeBoundaryPayloads payloads
+    | boxedPagedLength payloads == 0 = payloads
+    | otherwise = boxedUpdate 0 101 (boxedUpdate (boxedPagedLength payloads - 1) 103 payloads)
+
+  flatten :: SerializationFixture -> SerializationFixture
+  flatten mesh =
+    mesh
+      { triPointX = fromLocalVector 0 (toVector (triPointX mesh))
+      , triPointY = fromLocalVector 0 (toVector (triPointY mesh))
+      , triVertexOut = fromLocalVector maxBound (toVector (triVertexOut mesh))
+      , triHalfTopology = fromVector maxBound (toVector (triHalfTopology mesh))
+      , triFaceEdge = fromLocalVector maxBound (toVector (triFaceEdge mesh))
+      , triConstraint = fromVector 0 (toVector (triConstraint mesh))
+      }
diff --git a/test/serialization/Moonlight/Planar/SerializationSpec.hs b/test/serialization/Moonlight/Planar/SerializationSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/serialization/Moonlight/Planar/SerializationSpec.hs
@@ -0,0 +1,523 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+
+-- | The serialization slice: the versioned binary envelope and its refusals.
+module Moonlight.Planar.SerializationSpec (tests) where
+
+import Control.DeepSeq (NFData, force)
+import Control.Exception (evaluate)
+import Control.Monad (unless, when)
+import Data.Binary (Binary)
+import Data.Binary.Put (putWord16be, putWord32be, putWord64be, putWord8, runPut)
+import qualified Data.ByteString.Lazy as BL
+import Data.Foldable (traverse_)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word16, Word32, Word64)
+import GHC.Generics (Generic)
+import GHC.Stats (RTSStats (allocated_bytes), getRTSStats, getRTSStatsEnabled)
+import Moonlight.Planar.BulkLoad (delaunay, delaunayGeometry)
+import Moonlight.Planar.Canonical (canonicalize)
+import Moonlight.Planar.Cdt (constrainedDelaunay)
+import Moonlight.Planar.Dcel (setVertexData, vertexData, vertexPoint)
+import Moonlight.Planar.Handles.Iterators.FixedIterators (vertices)
+import Moonlight.Planar.Payload (mapVertices)
+import Moonlight.Planar.Types (ConstraintMode (..), ElementDefaults (..), InvariantViolation (..), buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (HasPosition (..), Point (..))
+import Moonlight.Planar.Internal.Validation (canonicalAdmission)
+import Moonlight.Planar.Types (PlanarIncidenceError (..))
+import Moonlight.Planar.Internal.Paged (Paged, fromVector, toVector)
+import Moonlight.Planar.Internal.HandleDefs (DirectedEdgeId (..), FaceId (..))
+import Moonlight.Planar.Internal.Representation (CanonicalAdmission (..), Triangulation (..))
+import Moonlight.Planar.Serialization
+import Moonlight.Planar.SerializationFixtures (serializationFixtures, serializationGridFixtures)
+import Moonlight.Planar.SerializationV6Oracle (encodeV6Gathered)
+import Moonlight.Planar.Types (KnownConstraintMode)
+import Support (randomPoints, assertEqual, assertValid, requireRight)
+import System.Mem (performGC)
+
+tests :: IO ()
+tests = do
+  testDecodeAllocationReceipt
+  testDecodedCanonicalWitness
+  testCanonicalAdmissionAgreesWithRenumbering
+  testRoundTrip
+  testV6WireOracle
+  testDegenerateCardinalityRoundTrips
+  testConstrainedRoundTrip
+  testIndependentPayloadGeometryRoundTrip
+  testPointPayloadRoundTrip
+  testCanonicalizesSerializedSignedZero
+  testRejectsHostileStructuralPrefixes
+  testRejectsOutOfRangeOuterFaceReference
+  testRejectsMalformedIndicesBeforeCanonicalAdmission
+  testRejectsCorruption
+  putStrLn "all serialization tests passed"
+
+data SerialVertex = SerialVertex
+  { serialPosition :: !(Point)
+  , serialLabel :: !Int
+  }
+  deriving stock (Eq, Show, Generic)
+  deriving anyclass (NFData, Binary)
+
+instance HasPosition SerialVertex where
+  position = serialPosition
+
+type SerialTriangulation = Triangulation 'Unconstrained SerialVertex Int Bool String
+
+testDecodingBudget :: DecodingBudget
+testDecodingBudget =
+  DecodingBudget
+    { decodingMaximumInputBytes = 10_000_000
+    , decodingMaximumSectionElements = 10_000_000
+    }
+
+source :: IO SerialTriangulation
+source = do
+  let defaults = ElementDefaults (3 :: Int) True ("face" :: String)
+      payloads =
+        V.fromList
+          [ SerialVertex (Point 0 0) 10
+          , SerialVertex (Point 2 0) 20
+          , SerialVertex (Point 0 2) 30
+          , SerialVertex (Point 0.5 0.5) 40
+          ]
+  buildTriangulation <$> requireRight "serialization source" (delaunay defaults payloads)
+
+type PointTriangulation = Triangulation 'Unconstrained Point () () ()
+
+testRejectsMalformedIndicesBeforeCanonicalAdmission :: IO ()
+testRejectsMalformedIndicesBeforeCanonicalAdmission = do
+  built <- requireRight "malformed index source"
+    (delaunay unitElementDefaults (V.fromList [Point 0 0, Point 2 0, Point 0 2]))
+  let mesh :: PointTriangulation
+      mesh = buildTriangulation built
+      replaceFirst :: Word32 -> Paged Word32 -> Paged Word32
+      replaceFirst value = fromVector maxBound . (U.// [(0, value)]) . toVector
+      mutations :: [(String, PointTriangulation)]
+      mutations =
+        [ (label, mesh{triHalfTopology = fromVector maxBound
+            (toVector (triHalfTopology mesh) U.// [(offset, 1000)])})
+        | (offset, label) <- zip [0 ..] ["origin", "next", "previous", "face"]
+        ] <>
+        [ ("vertex root", mesh{triVertexOut = replaceFirst 1000 (triVertexOut mesh)})
+        , ("face root", mesh{triFaceEdge = replaceFirst 1000 (triFaceEdge mesh)})
+        ]
+      reject :: (String, PointTriangulation) -> IO ()
+      reject (label, malformed) =
+        case decodePoints (encodeTriangulation malformed) of
+          Left (DecodedInvariantViolations violations) -> do
+            _ <- evaluate (force violations)
+            pure ()
+          result -> fail (label <> ": expected structural refusal, got " <> show result)
+  traverse_ reject mutations
+
+pointBudget :: DecodingBudget
+pointBudget =
+  DecodingBudget
+    { decodingMaximumInputBytes = 100_000_000
+    , decodingMaximumSectionElements = 100_000_000
+    }
+
+decodePoints :: BL.ByteString -> Either SerializationError PointTriangulation
+decodePoints = decodeTriangulation pointBudget trustedBinaryPayloadDecoders
+
+buildRandom :: Word64 -> Int -> IO PointTriangulation
+buildRandom seed count =
+  buildTriangulation
+    <$> requireRight "random build" (delaunay unitElementDefaults (V.fromList (randomPoints seed count)))
+
+-- | Bytes allocated by the action, in the shape of the native suite's
+-- canonical witness receipt.
+allocatedBy :: NFData value => IO value -> IO (value, Word64)
+allocatedBy action = do
+  performGC
+  before <- allocated_bytes <$> getRTSStats
+  value <- action
+  _ <- evaluate (force value)
+  performGC
+  after <- allocated_bytes <$> getRTSStats
+  pure (value, after - before)
+
+requireRtsStats :: String -> IO ()
+requireRtsStats label = do
+  enabled <- getRTSStatsEnabled
+  unless enabled $ fail (label <> " requires +RTS -T")
+
+-- | The numbering of a decoded value, forgetting any witness: what the
+-- renumbering would publish from it.
+forgetWitness :: PointTriangulation -> PointTriangulation
+forgetWitness triangulation = triangulation{triCanonical = CanonicalUnknown}
+
+sameNumbering :: PointTriangulation -> PointTriangulation -> Bool
+sameNumbering left right = encodeTriangulation left == encodeTriangulation right
+
+-- | The 20k-site receipt: what a decode allocates, and what canonical
+-- publication of the decoded value then allocates, for a canonical encoding
+-- and for a built one. The verification the decoder performs must cost less
+-- than the renumbering it saves; the numbers are printed, the laws are the
+-- next test's.
+testDecodeAllocationReceipt :: IO ()
+testDecodeAllocationReceipt = do
+  requireRtsStats "decode allocation receipt"
+  built <- buildRandom 0x5e71a1 20_000
+  canonical <- requireRight "receipt canonical" (canonicalize built)
+  builtBytes <- evaluate (force (encodeTriangulation built))
+  canonicalBytes <- evaluate (force (encodeTriangulation canonical))
+  (decodedCanonical, decodeCanonicalAllocated) <-
+    allocatedBy (requireRight "receipt decode canonical" (decodePoints canonicalBytes))
+  (_, republishAllocated) <-
+    allocatedBy (requireRight "receipt republish" (canonicalize decodedCanonical))
+  (decodedBuilt, decodeBuiltAllocated) <-
+    allocatedBy (requireRight "receipt decode built" (decodePoints builtBytes))
+  (_, renumberAllocated) <-
+    allocatedBy (requireRight "receipt renumber" (canonicalize decodedBuilt))
+  putStrLn
+    ( "decode canonical witness receipt: sites=20000"
+        <> " decode-canonical-allocated-bytes=" <> show decodeCanonicalAllocated
+        <> " decode-canonical-witness=" <> show (triCanonical decodedCanonical)
+        <> " republish-allocated-bytes=" <> show republishAllocated
+        <> " decode-built-allocated-bytes=" <> show decodeBuiltAllocated
+        <> " decode-built-witness=" <> show (triCanonical decodedBuilt)
+        <> " renumber-allocated-bytes=" <> show renumberAllocated
+    )
+
+-- | Decode mints 'CanonicalKnown' only by verifying the numbering: a decoded
+-- canonical encoding carries the witness, publishing it again is the
+-- identity for less than 16 KiB, and forcing the renumbering agrees; a
+-- decoded built encoding carries no witness and publication renumbers it to
+-- the same representative the source reached.
+testDecodedCanonicalWitness :: IO ()
+testDecodedCanonicalWitness = do
+  requireRtsStats "decoded canonical witness receipt"
+  built <- buildRandom 0xca70 4095
+  canonical <- requireRight "witness canonical" (canonicalize built)
+  when (sameNumbering built canonical) $
+    fail "the built fixture is already canonical; it proves nothing"
+  decodedCanonical <-
+    evaluate . force =<< requireRight "witness decode canonical" (decodePoints (encodeTriangulation canonical))
+  assertEqual
+    "decoding a canonical encoding verifies the witness"
+    CanonicalKnown
+    (triCanonical decodedCanonical)
+  (republished, republishAllocated) <-
+    allocatedBy (requireRight "witness republish" (canonicalize decodedCanonical))
+  unless (sameNumbering republished decodedCanonical) $
+    fail "canonical publication of a decoded canonical value changed it"
+  unless (republishAllocated < 16_384) $
+    fail ("canonical publication of a decoded canonical value allocated " <> show republishAllocated <> " bytes")
+  renumbered <- requireRight "witness forced renumbering" (canonicalize (forgetWitness decodedCanonical))
+  unless (sameNumbering renumbered decodedCanonical) $
+    fail "the decoder's verified witness disagrees with the renumbering"
+  decodedBuilt <-
+    evaluate . force =<< requireRight "witness decode built" (decodePoints (encodeTriangulation built))
+  assertEqual
+    "decoding a built encoding yields no witness"
+    CanonicalUnknown
+    (triCanonical decodedBuilt)
+  (renumberedBuilt, renumberAllocated) <-
+    allocatedBy (requireRight "witness renumbering" (canonicalize decodedBuilt))
+  unless (sameNumbering renumberedBuilt canonical) $
+    fail "renumbering the decoded built mesh missed the canonical representative"
+  unless (renumberAllocated >= 1_048_576) $
+    fail ("renumbering a decoded built mesh allocated only " <> show renumberAllocated <> " bytes")
+  putStrLn
+    ( "decoded canonical witness receipt: republish-allocated-bytes="
+        <> show republishAllocated
+        <> " renumber-allocated-bytes="
+        <> show renumberAllocated
+    )
+
+-- | The verification is exactly the renumbering's fixed-point test: over
+-- built, canonical, and decoded meshes of many sizes, 'canonicalAdmission'
+-- says 'CanonicalKnown' precisely when forgetting the witness and
+-- renumbering reproduces the same numbering.
+testCanonicalAdmissionAgreesWithRenumbering :: IO ()
+testCanonicalAdmissionAgreesWithRenumbering =
+  traverse_
+    (\(seed, count) -> do
+       built <- buildRandom seed count
+       canonical <- requireRight "agreement canonical" (canonicalize built)
+       decodedBuilt <- requireRight "agreement decode built" (decodePoints (encodeTriangulation built))
+       decodedCanonical <- requireRight "agreement decode canonical" (decodePoints (encodeTriangulation canonical))
+       traverse_
+         (\(label, mesh) -> do
+            renumbered <- requireRight ("agreement renumbering " <> label) (canonicalize (forgetWitness mesh))
+            let fixedPoint = sameNumbering renumbered mesh
+                verdict = canonicalAdmission mesh
+            assertEqual
+              ("canonical admission of " <> label <> " at " <> show count <> " sites, seed " <> show seed)
+              (if fixedPoint then CanonicalKnown else CanonicalUnknown)
+              verdict)
+         [ ("built", built)
+         , ("canonical", canonical)
+         , ("decoded built", decodedBuilt)
+         , ("decoded canonical", decodedCanonical)
+         ])
+    [ (seed, count)
+    | count <- [1, 2, 3, 4, 5, 8, 13, 50, 200, 1000]
+    , seed <- [0x1a, 0x2b, 0x3c]
+    ]
+
+
+testRoundTrip :: IO ()
+testRoundTrip = do
+  original <- source
+  let bytes = encodeTriangulation original
+  unless (BL.length bytes > 0) $ fail "serialization produced an empty payload"
+  assertSerializationRoundTrip "serialization" original
+
+testDegenerateCardinalityRoundTrips :: IO ()
+testDegenerateCardinalityRoundTrips =
+  traverse_
+    roundTripGeometry
+    [ ("empty", V.empty)
+    , ("singleton", V.singleton (Point 0 0))
+    , ("segment", V.fromList [Point 0 0, Point 1 0])
+    , ("collinear chain", V.fromList [Point 0 0, Point 1 0, Point 2 0, Point 3 0])
+    ]
+ where
+  roundTripGeometry (label, points) = do
+    original <- requireRight (label <> " serialization source") (delaunayGeometry points)
+    assertSerializationRoundTrip (label <> " serialization") original
+
+testConstrainedRoundTrip :: IO ()
+testConstrainedRoundTrip = do
+  built <-
+    requireRight
+      "constrained serialization source"
+      ( constrainedDelaunay
+          unitElementDefaults
+          (V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
+          (V.singleton (0, 2))
+      )
+  let original = buildTriangulation built
+  assertSerializationRoundTrip "constrained serialization" original
+
+-- Vertex payload positions are annotations after ingestion. Serialization must
+-- therefore preserve the fixed geometry and the independently edited payload,
+-- rather than letting the latter reauthor the former on decode.
+testIndependentPayloadGeometryRoundTrip :: IO ()
+testIndependentPayloadGeometryRoundTrip = do
+  geometry <- source
+  vertex <- case vertices geometry of
+    (first : _) -> pure first
+    [] -> fail "independent payload fixture has no vertices"
+  let independentPayload = SerialVertex (Point 91 73) 1010
+      original = setVertexData geometry vertex independentPayload
+      positionless = mapVertices serialLabel original
+  assertEqual "independent payload leaves geometry fixed"
+    (vertexPoint geometry vertex) (vertexPoint original vertex)
+  assertEqual "independent payload position is stored"
+    (Point 91 73) (serialPosition (vertexData original vertex))
+  assertSerializationRoundTrip "independent payload serialization" original
+  assertSerializationRoundTrip "positionless payload serialization" positionless
+
+testPointPayloadRoundTrip :: IO ()
+testPointPayloadRoundTrip = do
+  let points = V.fromList [Point 0 0, Point 2 0, Point 0 2, Point 0.5 0.5] :: V.Vector (Point)
+  built <- requireRight "point payload source" (delaunay unitElementDefaults points)
+  let geometry = buildTriangulation built
+  vertex <- case vertices geometry of
+    (first : _) -> pure first
+    [] -> fail "point payload fixture has no vertices"
+  let original = setVertexData geometry vertex (Point 13 17)
+  assertEqual "point payload leaves geometry fixed"
+    (vertexPoint geometry vertex) (vertexPoint original vertex)
+  assertEqual "point payload is stored"
+    (Point 13 17) (vertexData original vertex)
+  assertSerializationRoundTrip "point payload serialization" original
+
+-- The wire format admits every binary64 bit pattern, but resident point identity
+-- admits only canonical zero. Mutate the first singleton coordinate to negative
+-- zero and require decode to restore the canonical representation.
+testCanonicalizesSerializedSignedZero :: IO ()
+testCanonicalizesSerializedSignedZero = do
+  original <- requireRight "signed-zero source" (delaunayGeometry (V.singleton (Point 0 0)))
+  let bytes = encodeTriangulation original
+      structuralPrefixSize = 8 + 2 + 1 + 1 + 4 * 8
+      signedZeroBytes =
+        BL.concat
+          [ BL.take structuralPrefixSize bytes
+          , BL.singleton 0x80
+          , BL.drop (structuralPrefixSize + 1) bytes
+          ]
+  decoded <-
+    requireRight
+      "signed-zero decode"
+      ( decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders signedZeroBytes
+          :: Either SerializationError (Triangulation 'Unconstrained () () () ())
+      )
+  vertex <- case vertices decoded of
+    [onlyVertex] -> pure onlyVertex
+    unexpected -> fail ("signed-zero fixture produced " <> show (length unexpected) <> " vertices")
+  assertEqual "signed-zero coordinate canonicalization" (Point 0 0) (vertexPoint decoded vertex)
+  assertEqual "signed-zero canonical re-encoding" bytes (encodeTriangulation decoded)
+
+assertSerializationRoundTrip
+  :: ( KnownConstraintMode mode
+     , Binary vertex
+     , Binary directed
+     , Binary undirected
+     , Binary face
+     , Eq vertex
+     , Eq directed
+     , Eq undirected
+     , Eq face
+     , Show vertex
+     , Show directed
+     , Show undirected
+     , Show face
+     )
+  => String
+  -> Triangulation mode vertex directed undirected face
+  -> IO ()
+assertSerializationRoundTrip label original = do
+  assertEqual (label <> " independent V6 bytes") (encodeV6Gathered original) (encodeTriangulation original)
+  decoded <-
+    requireRight
+      (label <> " round trip")
+      (decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders (encodeTriangulation original))
+  assertEqual (label <> " equality") original decoded
+  assertValid (label <> " validity") decoded
+
+testV6WireOracle :: IO ()
+testV6WireOracle = do
+  checkedCounts <-
+    traverse
+      (\(family, requestedFixtures) -> do
+         fixtures <- requireRight "V6 storage fixtures" requestedFixtures
+         traverse_
+           (\(label, mesh) -> assertSerializationRoundTrip (family <> "/" <> label) mesh)
+           fixtures
+         pure (length fixtures))
+      ( [ ("collinear-count=" <> show count, serializationFixtures count)
+        | count <- [0, 1, 2, 3, 255, 256, 257, 1023, 1024, 1025, 10242]
+        ]
+          <> [ ("grid-count=" <> show count, serializationGridFixtures count)
+             | count <- [3, 257, 10242]
+             ]
+      )
+  putStrLn ("V6 independent byte-oracle and decode fixtures passed: " <> show (sum checkedCounts))
+
+-- Counts are one prefix precisely so these refusals precede all default and
+-- payload decoders. The hostile fixtures use @()@, whose lawful decoder consumes
+-- no bytes, to exercise the formerly allocative attack rather than relying on
+-- truncation to save the process.
+testRejectsHostileStructuralPrefixes :: IO ()
+testRejectsHostileStructuralPrefixes =
+  traverse_
+    (\(label, budget, bytes, failure) ->
+       assertDecodeFailure label budget bytes failure)
+    [ ( "input byte budget"
+      , DecodingBudget 43 10_000
+      , structuralPrefix 6 0 0 1 0
+      , InputByteBudgetExceeded 44 43
+      )
+    , ( "section element budget"
+      , DecodingBudget 1_000 1_000_000
+      , structuralPrefix 6 1_000_000 1_999_998 1 0
+      , DecodedSectionBudgetExceeded 15_999_990 1_000_000
+      )
+    , ("directed edge parity", testDecodingBudget, structuralPrefix 6 0 1 1 0, SerializedDirectedEdgeCountOdd 1)
+    , ("missing outer face", testDecodingBudget, structuralPrefix 6 0 0 0 0, SerializedMissingOuterFace)
+    , ("constraint count relationship", testDecodingBudget, structuralPrefix 6 0 2 1 2, SerializedConstraintCountExceedsEdges 2 1)
+    , ("planar cardinality relationship", testDecodingBudget, structuralPrefix 6 2 0 1 0, SerializedPlanarCardinalityMismatch 2 0 1)
+    , ("fixed body lower bound", testDecodingBudget, structuralPrefix 6 1 0 1 0, SerializedFixedBodyTooShort 0 24)
+    , ("host Int vertex count", DecodingBudget 1_000 maxBound, structuralPrefix 6 maxBound 0 1 0, EncodedCountExceedsInt SerializedVertexCount maxBound)
+    , ( "packed vertex count"
+      , DecodingBudget 1_000 maxBound
+      , structuralPrefix 6 4_294_967_296 0 1 0
+      , EncodedCountExceedsPackedIndex SerializedVertexCount 4_294_967_296 4_294_967_295
+      )
+    , ("version 5", testDecodingBudget, structuralPrefix 5 0 0 1 0, UnsupportedFormatVersion 5)
+    ]
+
+-- Range validation must obstruct before relational validation dereferences the
+-- hostile handle. Fully comparing the error forces its complete violation
+-- inventory and therefore pins the absence of a lazy unsafe-index crash.
+testRejectsOutOfRangeOuterFaceReference :: IO ()
+testRejectsOutOfRangeOuterFaceReference =
+  assertDecodeFailure
+    "out-of-range outer-face reference"
+    testDecodingBudget
+    (structuralPrefix 6 0 0 1 0 <> runPut (putWord32be 0 >> putWord8 0))
+    ( DecodedInvariantViolations
+        (IncidenceViolation (IncidenceFaceRootInvalid (FaceId 0) (DirectedEdgeId 0)) :| [])
+    )
+
+assertDecodeFailure
+  :: String
+  -> DecodingBudget
+  -> BL.ByteString
+  -> SerializationError
+  -> IO ()
+assertDecodeFailure label budget bytes expected =
+  assertEqual
+    label
+    (Left expected)
+    ( decodeTriangulation budget trustedBinaryPayloadDecoders bytes
+        :: Either SerializationError (Triangulation 'Unconstrained () () () ())
+    )
+
+structuralPrefix :: Word16 -> Word64 -> Word64 -> Word64 -> Word64 -> BL.ByteString
+structuralPrefix version vertexCount directedEdgeCount faceCount constraintCount =
+  runPut $ do
+    putWord64be 0x5350414445485307
+    putWord16be version
+    putWord16be 2
+    traverse_
+      putWord64be
+      [vertexCount, directedEdgeCount, faceCount, constraintCount]
+
+-- The header is the part of the stream that is structurally constrained: magic,
+-- version, constraint mode and coordinate encoding each have exactly one admissible
+-- byte pattern, so every mutation of them must be refused. Beyond the header
+-- the stream carries payload values, and a byte flipped inside an element
+-- payload names a different but entirely legal value — the guarantee there is
+-- not refusal but soundness: a decoder that rebuilds its indexes rather than
+-- trusting them may never surface a triangulation that violates its invariants,
+-- whatever it is fed.
+testRejectsCorruption :: IO ()
+testRejectsCorruption = do
+  original <- source
+  let bytes = encodeTriangulation original
+      size = BL.length bytes
+      envelopeSize = 8 + 2 + 1 + 1
+      structuralPrefixSize = envelopeSize + 4 * 8
+      decode candidate = decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders candidate :: Either SerializationError SerialTriangulation
+      flipAt offset =
+        BL.concat [BL.take offset bytes, BL.singleton (BL.index bytes offset + 1), BL.drop (offset + 1) bytes]
+      rejects :: String -> BL.ByteString -> IO ()
+      rejects label candidate =
+        case decode candidate of
+          Left _ -> pure ()
+          Right _ -> fail ("decoder accepted " <> label)
+  assertEqual
+    "typed trailing-byte refusal"
+    (Left (TrailingBytes 1))
+    (decode (bytes <> BL.singleton 0))
+  case decode (BL.cons 0 (BL.drop 1 bytes)) of
+    Left (InvalidFormatMagic _) -> pure ()
+    other -> fail ("magic corruption produced " <> show other)
+  rejects "an empty payload" BL.empty
+  traverse_
+    (\dropped -> rejects ("a payload truncated by " <> show dropped) (BL.take (size - dropped) bytes))
+    [1 .. size]
+  traverse_
+    (\offset -> rejects ("an envelope byte flipped at offset " <> show offset) (flipAt offset))
+    [0 .. envelopeSize - 1]
+  traverse_
+    (\offset -> rejects ("a structural-prefix byte flipped at offset " <> show offset) (flipAt offset))
+    [envelopeSize .. structuralPrefixSize - 1]
+  traverse_ (\offset ->
+    case decode (flipAt offset) of
+      Left _ -> pure ()
+      Right decoded ->
+        assertValid ("a byte flipped at offset " <> show offset <> " decoded to") decoded
+    ) [structuralPrefixSize .. size - 1]
diff --git a/test/serialization/Moonlight/Planar/SerializationV6Oracle.hs b/test/serialization/Moonlight/Planar/SerializationV6Oracle.hs
new file mode 100644
--- /dev/null
+++ b/test/serialization/Moonlight/Planar/SerializationV6Oracle.hs
@@ -0,0 +1,95 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE GADTs #-}
+{-# OPTIONS_GHC -fexpose-all-unfoldings #-}
+
+-- | Test-only V6 wire oracle retained from the gathered encoder. It deliberately
+-- does not call the production encoder or the new storage producers.
+-- Its unfolding visibility matches the serialize component so the benchmark
+-- compares both encoders under the same cross-module specialization policy.
+module Moonlight.Planar.SerializationV6Oracle (encodeV6Gathered) where
+
+import Data.Binary (Binary (..))
+import Data.Binary.Put
+  ( putDoublebe, putWord16be, putWord32be, putWord64be, putWord8, runPut )
+import qualified Data.ByteString.Lazy as BL
+import Data.Proxy (Proxy (..))
+import qualified Data.Vector as V
+import qualified Data.Vector.Unboxed as U
+import Data.Word (Word16, Word64, Word8)
+import Moonlight.Planar.Internal.BoxedPaged (BoxedFill (..), boxedFill, boxedToVector)
+import Moonlight.Planar.Internal.Paged (toVector)
+import Moonlight.Planar.Internal.Representation (Triangulation (..), authoringElementDefaults)
+import Moonlight.Planar.Internal.Types
+  ( ConstraintMode (..), ElementDefaults (..), KnownConstraintMode (..) )
+
+formatMagic :: Word64
+formatMagic = 0x5350414445485307
+
+serializationVersion :: Word16
+serializationVersion = 6
+
+binary64EncodingTag :: Word8
+binary64EncodingTag = 2
+
+modeProxy :: Triangulation mode vertex directed undirected face -> Proxy mode
+modeProxy _ = Proxy
+
+modeTag :: ConstraintMode -> Word8
+modeTag Unconstrained = 0
+modeTag Constrained = 1
+
+encodeV6Gathered
+  :: forall mode vertex directed undirected face. (KnownConstraintMode mode, Binary vertex, Binary directed, Binary undirected, Binary face)
+  => Triangulation mode vertex directed undirected face
+  -> BL.ByteString
+encodeV6Gathered triangulation = runPut $ do
+  putWord64be formatMagic
+  putWord16be serializationVersion
+  putWord8 (modeTag (constraintModeValue (modeProxy triangulation)))
+  putWord8 binary64EncodingTag
+  let ElementDefaults directedDefault undirectedDefault faceDefault = authoringElementDefaults triangulation
+      pointXs = toVector (triPointX triangulation)
+      pointYs = toVector (triPointY triangulation)
+      vertexDefault = case boxedFill (triVertexData triangulation) of
+        NoFill -> Nothing
+        Fill value -> Just value
+      vertexDataVector = boxedToVector (triVertexData triangulation)
+      vertexOut = toVector (triVertexOut triangulation)
+      topology = toVector (triHalfTopology triangulation)
+      directedDataVector = boxedToVector (triDirectedData triangulation)
+      undirectedDataVector = boxedToVector (triUndirectedData triangulation)
+      faceEdge = toVector (triFaceEdge triangulation)
+      faceDataVector = boxedToVector (triFaceData triangulation)
+      constraints = toVector (triConstraint triangulation)
+      vertexCount = U.length pointXs
+      directedEdgeCount = U.length topology `quot` 4
+      faceCount = U.length faceEdge
+  -- Version 6 commits every structural count in one prefix. The decoder can
+  -- prove their relationships and resource bounds before defaults, payloads,
+  -- or section bodies are evaluated.
+  putWord64be (fromIntegral vertexCount)
+  putWord64be (fromIntegral directedEdgeCount)
+  putWord64be (fromIntegral faceCount)
+  putWord64be (fromIntegral (triConstraintCount triangulation))
+  put directedDefault
+  put undirectedDefault
+  put faceDefault
+  -- Geometry and payloads are independent components. Persist the authoritative
+  -- coordinate pages rather than attempting to recover them from annotations.
+  U.mapM_ putDoublebe pointXs
+  U.mapM_ putDoublebe pointYs
+  put vertexDefault
+  V.mapM_ put vertexDataVector
+  U.mapM_ putWord32be vertexOut
+  -- The wire format stores the four topology planes separately; the interleaved
+  -- arena is a resident layout, not a serialization concern.
+  let plane field = U.generate directedEdgeCount (\edge -> topology U.! (4 * edge + field))
+  U.mapM_ putWord32be (plane 0)
+  U.mapM_ putWord32be (plane 1)
+  U.mapM_ putWord32be (plane 2)
+  U.mapM_ putWord32be (plane 3)
+  V.mapM_ put directedDataVector
+  V.mapM_ put undirectedDataVector
+  U.mapM_ putWord32be faceEdge
+  V.mapM_ put faceDataVector
+  U.mapM_ putWord8 constraints
diff --git a/test/serialization/Moonlight/Triangulation/SerializationSpec.hs b/test/serialization/Moonlight/Triangulation/SerializationSpec.hs
deleted file mode 100644
--- a/test/serialization/Moonlight/Triangulation/SerializationSpec.hs
+++ /dev/null
@@ -1,305 +0,0 @@
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveAnyClass #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE FlexibleContexts #-}
-{-# LANGUAGE MultiParamTypeClasses #-}
-
--- | The serialization slice: the versioned binary envelope and its refusals.
-module Moonlight.Triangulation.SerializationSpec (tests) where
-
-import Control.DeepSeq (NFData)
-import Control.Monad (unless)
-import Data.Binary (Binary)
-import Data.Binary.Put (putWord16be, putWord32be, putWord64be, putWord8, runPut)
-import qualified Data.ByteString.Lazy as BL
-import Data.Foldable (traverse_)
-import Data.List.NonEmpty (NonEmpty (..))
-import qualified Data.Vector as V
-import Data.Word (Word16, Word64)
-import GHC.Generics (Generic)
-import Moonlight.Triangulation
-import Moonlight.Triangulation.Internal.HandleDefs (DirectedEdgeId (..), FaceId (..))
-import Moonlight.Triangulation.Serialization
-import Moonlight.Triangulation.Types (KnownConstraintMode)
-import Support (assertEqual, assertValid, requireRight)
-
-tests :: IO ()
-tests = do
-  testRoundTrip
-  testDegenerateCardinalityRoundTrips
-  testConstrainedRoundTrip
-  testIndependentPayloadGeometryRoundTrip
-  testPointPayloadRoundTrip
-  testCanonicalizesSerializedSignedZero
-  testRejectsHostileStructuralPrefixes
-  testRejectsOutOfRangeOuterFaceReference
-  testRejectsCorruption
-  putStrLn "all serialization tests passed"
-
-data SerialVertex = SerialVertex
-  { serialPosition :: !(Point)
-  , serialLabel :: !Int
-  }
-  deriving stock (Eq, Show, Generic)
-  deriving anyclass (NFData, Binary)
-
-instance HasPosition SerialVertex where
-  position = serialPosition
-
-type SerialTriangulation = Triangulation 'Unconstrained SerialVertex Int Bool String
-
-testDecodingBudget :: DecodingBudget
-testDecodingBudget =
-  DecodingBudget
-    { decodingMaximumInputBytes = 10_000_000
-    , decodingMaximumSectionElements = 10_000_000
-    }
-
-source :: IO SerialTriangulation
-source = do
-  let defaults = ElementDefaults (3 :: Int) True ("face" :: String)
-      payloads =
-        V.fromList
-          [ SerialVertex (Point 0 0) 10
-          , SerialVertex (Point 2 0) 20
-          , SerialVertex (Point 0 2) 30
-          , SerialVertex (Point 0.5 0.5) 40
-          ]
-  buildTriangulation <$> requireRight "serialization source" (delaunay defaults payloads)
-
-testRoundTrip :: IO ()
-testRoundTrip = do
-  original <- source
-  let bytes = encodeTriangulation original
-  unless (BL.length bytes > 0) $ fail "serialization produced an empty payload"
-  assertSerializationRoundTrip "serialization" original
-
-testDegenerateCardinalityRoundTrips :: IO ()
-testDegenerateCardinalityRoundTrips =
-  traverse_
-    roundTripGeometry
-    [ ("empty", V.empty)
-    , ("singleton", V.singleton (Point 0 0))
-    , ("segment", V.fromList [Point 0 0, Point 1 0])
-    , ("collinear chain", V.fromList [Point 0 0, Point 1 0, Point 2 0, Point 3 0])
-    ]
- where
-  roundTripGeometry (label, points) = do
-    original <- requireRight (label <> " serialization source") (delaunayGeometry points)
-    assertSerializationRoundTrip (label <> " serialization") original
-
-testConstrainedRoundTrip :: IO ()
-testConstrainedRoundTrip = do
-  built <-
-    requireRight
-      "constrained serialization source"
-      ( constrainedDelaunay
-          unitElementDefaults
-          (V.fromList [Point 0 0, Point 2 0, Point 2 2, Point 0 2])
-          (V.singleton (0, 2))
-      )
-  let original = buildTriangulation built
-  assertSerializationRoundTrip "constrained serialization" original
-
--- Vertex payload positions are annotations after ingestion. Serialization must
--- therefore preserve the fixed geometry and the independently edited payload,
--- rather than letting the latter reauthor the former on decode.
-testIndependentPayloadGeometryRoundTrip :: IO ()
-testIndependentPayloadGeometryRoundTrip = do
-  geometry <- source
-  vertex <- case vertices geometry of
-    (first : _) -> pure first
-    [] -> fail "independent payload fixture has no vertices"
-  let independentPayload = SerialVertex (Point 91 73) 1010
-      original = setVertexData geometry vertex independentPayload
-      positionless = mapVertices serialLabel original
-  assertEqual "independent payload leaves geometry fixed"
-    (vertexPoint geometry vertex) (vertexPoint original vertex)
-  assertEqual "independent payload position is stored"
-    (Point 91 73) (serialPosition (vertexData original vertex))
-  assertSerializationRoundTrip "independent payload serialization" original
-  assertSerializationRoundTrip "positionless payload serialization" positionless
-
-testPointPayloadRoundTrip :: IO ()
-testPointPayloadRoundTrip = do
-  let points = V.fromList [Point 0 0, Point 2 0, Point 0 2, Point 0.5 0.5] :: V.Vector (Point)
-  built <- requireRight "point payload source" (delaunay unitElementDefaults points)
-  let geometry = buildTriangulation built
-  vertex <- case vertices geometry of
-    (first : _) -> pure first
-    [] -> fail "point payload fixture has no vertices"
-  let original = setVertexData geometry vertex (Point 13 17)
-  assertEqual "point payload leaves geometry fixed"
-    (vertexPoint geometry vertex) (vertexPoint original vertex)
-  assertEqual "point payload is stored"
-    (Point 13 17) (vertexData original vertex)
-  assertSerializationRoundTrip "point payload serialization" original
-
--- The wire format admits every binary64 bit pattern, but resident point identity
--- admits only canonical zero. Mutate the first singleton coordinate to negative
--- zero and require decode to restore the canonical representation.
-testCanonicalizesSerializedSignedZero :: IO ()
-testCanonicalizesSerializedSignedZero = do
-  original <- requireRight "signed-zero source" (delaunayGeometry (V.singleton (Point 0 0)))
-  let bytes = encodeTriangulation original
-      structuralPrefixSize = 8 + 2 + 1 + 1 + 4 * 8
-      signedZeroBytes =
-        BL.concat
-          [ BL.take structuralPrefixSize bytes
-          , BL.singleton 0x80
-          , BL.drop (structuralPrefixSize + 1) bytes
-          ]
-  decoded <-
-    requireRight
-      "signed-zero decode"
-      ( decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders signedZeroBytes
-          :: Either SerializationError (Triangulation 'Unconstrained () () () ())
-      )
-  vertex <- case vertices decoded of
-    [onlyVertex] -> pure onlyVertex
-    unexpected -> fail ("signed-zero fixture produced " <> show (length unexpected) <> " vertices")
-  assertEqual "signed-zero coordinate canonicalization" (Point 0 0) (vertexPoint decoded vertex)
-  assertEqual "signed-zero canonical re-encoding" bytes (encodeTriangulation decoded)
-
-assertSerializationRoundTrip
-  :: ( KnownConstraintMode mode
-     , Binary vertex
-     , Binary directed
-     , Binary undirected
-     , Binary face
-     , Eq vertex
-     , Eq directed
-     , Eq undirected
-     , Eq face
-     , Show vertex
-     , Show directed
-     , Show undirected
-     , Show face
-     )
-  => String
-  -> Triangulation mode vertex directed undirected face
-  -> IO ()
-assertSerializationRoundTrip label original = do
-  decoded <-
-    requireRight
-      (label <> " round trip")
-      (decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders (encodeTriangulation original))
-  assertEqual (label <> " equality") original decoded
-  assertValid (label <> " validity") decoded
-
--- Counts are one prefix precisely so these refusals precede all default and
--- payload decoders. The hostile fixtures use @()@, whose lawful decoder consumes
--- no bytes, to exercise the formerly allocative attack rather than relying on
--- truncation to save the process.
-testRejectsHostileStructuralPrefixes :: IO ()
-testRejectsHostileStructuralPrefixes =
-  traverse_
-    (\(label, budget, bytes, failure) ->
-       assertDecodeFailure label budget bytes failure)
-    [ ( "input byte budget"
-      , DecodingBudget 43 10_000
-      , structuralPrefix 6 0 0 1 0
-      , InputByteBudgetExceeded 44 43
-      )
-    , ( "section element budget"
-      , DecodingBudget 1_000 1_000_000
-      , structuralPrefix 6 1_000_000 1_999_998 1 0
-      , DecodedSectionBudgetExceeded 15_999_990 1_000_000
-      )
-    , ("directed edge parity", testDecodingBudget, structuralPrefix 6 0 1 1 0, SerializedDirectedEdgeCountOdd 1)
-    , ("missing outer face", testDecodingBudget, structuralPrefix 6 0 0 0 0, SerializedMissingOuterFace)
-    , ("constraint count relationship", testDecodingBudget, structuralPrefix 6 0 2 1 2, SerializedConstraintCountExceedsEdges 2 1)
-    , ("planar cardinality relationship", testDecodingBudget, structuralPrefix 6 2 0 1 0, SerializedPlanarCardinalityMismatch 2 0 1)
-    , ("fixed body lower bound", testDecodingBudget, structuralPrefix 6 1 0 1 0, SerializedFixedBodyTooShort 0 24)
-    , ("host Int vertex count", DecodingBudget 1_000 maxBound, structuralPrefix 6 maxBound 0 1 0, EncodedCountExceedsInt SerializedVertexCount maxBound)
-    , ( "packed vertex count"
-      , DecodingBudget 1_000 maxBound
-      , structuralPrefix 6 4_294_967_296 0 1 0
-      , EncodedCountExceedsPackedIndex SerializedVertexCount 4_294_967_296 4_294_967_295
-      )
-    , ("version 5", testDecodingBudget, structuralPrefix 5 0 0 1 0, UnsupportedFormatVersion 5)
-    ]
-
--- Range validation must obstruct before relational validation dereferences the
--- hostile handle. Fully comparing the error forces its complete violation
--- inventory and therefore pins the absence of a lazy unsafe-index crash.
-testRejectsOutOfRangeOuterFaceReference :: IO ()
-testRejectsOutOfRangeOuterFaceReference =
-  assertDecodeFailure
-    "out-of-range outer-face reference"
-    testDecodingBudget
-    (structuralPrefix 6 0 0 1 0 <> runPut (putWord32be 0 >> putWord8 0))
-    ( DecodedInvariantViolations
-        (FaceAdjacentOutOfRange (FaceId 0) (DirectedEdgeId 0) 0 :| [])
-    )
-
-assertDecodeFailure
-  :: String
-  -> DecodingBudget
-  -> BL.ByteString
-  -> SerializationError
-  -> IO ()
-assertDecodeFailure label budget bytes expected =
-  assertEqual
-    label
-    (Left expected)
-    ( decodeTriangulation budget trustedBinaryPayloadDecoders bytes
-        :: Either SerializationError (Triangulation 'Unconstrained () () () ())
-    )
-
-structuralPrefix :: Word16 -> Word64 -> Word64 -> Word64 -> Word64 -> BL.ByteString
-structuralPrefix version vertexCount directedEdgeCount faceCount constraintCount =
-  runPut $ do
-    putWord64be 0x5350414445485307
-    putWord16be version
-    putWord16be 2
-    traverse_
-      putWord64be
-      [vertexCount, directedEdgeCount, faceCount, constraintCount]
-
--- The header is the part of the stream that is structurally constrained: magic,
--- version, constraint mode and coordinate encoding each have exactly one admissible
--- byte pattern, so every mutation of them must be refused. Beyond the header
--- the stream carries payload values, and a byte flipped inside an element
--- payload names a different but entirely legal value — the guarantee there is
--- not refusal but soundness: a decoder that rebuilds its indexes rather than
--- trusting them may never surface a triangulation that violates its invariants,
--- whatever it is fed.
-testRejectsCorruption :: IO ()
-testRejectsCorruption = do
-  original <- source
-  let bytes = encodeTriangulation original
-      size = BL.length bytes
-      envelopeSize = 8 + 2 + 1 + 1
-      structuralPrefixSize = envelopeSize + 4 * 8
-      decode candidate = decodeTriangulation testDecodingBudget trustedBinaryPayloadDecoders candidate :: Either SerializationError SerialTriangulation
-      flipAt offset =
-        BL.concat [BL.take offset bytes, BL.singleton (BL.index bytes offset + 1), BL.drop (offset + 1) bytes]
-      rejects :: String -> BL.ByteString -> IO ()
-      rejects label candidate =
-        case decode candidate of
-          Left _ -> pure ()
-          Right _ -> fail ("decoder accepted " <> label)
-  assertEqual
-    "typed trailing-byte refusal"
-    (Left (TrailingBytes 1))
-    (decode (bytes <> BL.singleton 0))
-  case decode (BL.cons 0 (BL.drop 1 bytes)) of
-    Left (InvalidFormatMagic _) -> pure ()
-    other -> fail ("magic corruption produced " <> show other)
-  rejects "an empty payload" BL.empty
-  traverse_
-    (\dropped -> rejects ("a payload truncated by " <> show dropped) (BL.take (size - dropped) bytes))
-    [1 .. size]
-  traverse_
-    (\offset -> rejects ("an envelope byte flipped at offset " <> show offset) (flipAt offset))
-    [0 .. envelopeSize - 1]
-  traverse_
-    (\offset -> rejects ("a structural-prefix byte flipped at offset " <> show offset) (flipAt offset))
-    [envelopeSize .. structuralPrefixSize - 1]
-  traverse_ (\offset ->
-    case decode (flipAt offset) of
-      Left _ -> pure ()
-      Right decoded ->
-        assertValid ("a byte flipped at offset " <> show offset <> " decoded to") decoded
-    ) [structuralPrefixSize .. size - 1]
diff --git a/test/support/Moonlight/Planar/MeshFixtures.hs b/test/support/Moonlight/Planar/MeshFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/support/Moonlight/Planar/MeshFixtures.hs
@@ -0,0 +1,53 @@
+{-# LANGUAGE DataKinds #-}
+
+-- | Mesh construction and coordinate-keyed observations shared by native and algebra laws.
+module Moonlight.Planar.MeshFixtures
+  ( requirePointBuild
+  , pointMeshOf
+  , pointKey
+  , siteKey
+  , faceKeyOf
+  , edgeKeys
+  ) where
+
+import Data.List ( sort )
+import Moonlight.Planar.BulkLoad ( delaunay )
+import Moonlight.Planar.Dcel ( faceVertices, undirectedEndpoints, vertexPoint )
+import Moonlight.Planar.Handles.HandleDefs ( FaceId, VertexId )
+import Moonlight.Planar.Handles.Iterators.FixedIterators ( undirectedEdges )
+import Moonlight.Planar.Types (BuildResult, ConstraintMode(..), DelaunayTriangulation, Triangulation, buildTriangulation, unitElementDefaults)
+import Moonlight.Planar.Point (Point(..))
+import Support ( requireRight )
+import qualified Data.Set as Set
+import qualified Data.Vector as V
+
+
+requirePointBuild :: String -> [Point] -> IO (BuildResult 'Unconstrained Point () () ())
+requirePointBuild label points = requireRight label (delaunay unitElementDefaults (V.fromList points))
+
+pointMeshOf :: String -> [Point] -> IO (DelaunayTriangulation Point)
+pointMeshOf label points = buildTriangulation <$> requirePointBuild label points
+
+pointKey :: Point -> (Double, Double)
+pointKey (Point x y) = (x, y)
+
+siteKey :: Triangulation mode vertex directed undirected face -> VertexId -> (Double, Double)
+siteKey mesh vertex = pointKey (vertexPoint mesh vertex)
+
+edgeKeys
+  :: Triangulation mode vertex directed undirected face
+  -> Set.Set ((Double, Double), (Double, Double))
+edgeKeys mesh =
+  Set.fromList
+    [ if left <= right then (left, right) else (right, left)
+    | edge <- undirectedEdges mesh
+    , let (from, to) = undirectedEndpoints mesh edge
+    , let left = siteKey mesh from
+    , let right = siteKey mesh to
+    ]
+
+faceKeyOf
+  :: Triangulation mode vertex directed undirected face
+  -> FaceId
+  -> [Point]
+faceKeyOf triangulation face = sort (map (vertexPoint triangulation) (faceVertices triangulation face))
diff --git a/test/support/Moonlight/Planar/OverlayFixtures.hs b/test/support/Moonlight/Planar/OverlayFixtures.hs
new file mode 100644
--- /dev/null
+++ b/test/support/Moonlight/Planar/OverlayFixtures.hs
@@ -0,0 +1,23 @@
+-- | Labelled-region fixtures shared by native and public algebra laws.
+module Moonlight.Planar.OverlayFixtures
+  ( singletonLayer
+  , overlayRegions
+  ) where
+
+import qualified Data.Map.Strict as Map
+import Moonlight.Planar.Overlay (OverlayResult, overlayLayers)
+import Moonlight.Planar.Region (PlanarLayer, PlanarRegion, planarLayer)
+import Support (requireRight)
+
+singletonLayer :: Ord label => label -> label -> PlanarRegion -> IO (PlanarLayer label)
+singletonLayer outside inside region =
+  requireRight "singleton layer" (planarLayer outside (Map.singleton inside region))
+
+overlayRegions
+  :: PlanarRegion
+  -> PlanarRegion
+  -> IO (OverlayResult (Bool, Bool))
+overlayRegions left right = do
+  leftLayer <- singletonLayer False True left
+  rightLayer <- singletonLayer False True right
+  requireRight "region algebra overlay" (overlayLayers leftLayer rightLayer)
diff --git a/test/support/Support.hs b/test/support/Support.hs
--- a/test/support/Support.hs
+++ b/test/support/Support.hs
@@ -1,31 +1,35 @@
+{-# LANGUAGE NumericUnderscores #-}
+
 -- | Assertions and exact planar fixtures shared by the test slices. Nothing
 -- here may depend on an optional package flag, so the minimal configuration
 -- compiles the same helpers as the full one.
 module Support
   ( requireRight
   , requireQueryPoint
+  , requireJust
+  , randomPoints
   , assertEqual
   , assertValid
   , integerPoint
   , rectangleLoop
   , rectangleComponent
+  , rectangleRegion
+  , polygonComponentOf
+  , polygonRegion
+  , annulusRegion
   ) where
 
 import Control.Monad (unless)
+import qualified Data.Set as Set
+import Data.Word (Word64)
 import Data.List.NonEmpty (NonEmpty (..))
-import Moonlight.Triangulation
-  ( ExactLoop
-  , ExactPoint
-  , Point
-  , PolygonComponent
-  , QueryPoint
-  , Triangulation
-  , exactLoop
-  , exactPoint
-  , mkQueryPoint
-  , polygonComponent
-  , validateTriangulation
-  )
+import Moonlight.Planar.Exact (ExactPoint, exactPoint)
+import Moonlight.Planar.Point (mkQueryPoint)
+import Moonlight.Planar.Region
+  ( ExactLoop, PlanarRegion, PolygonComponent, exactLoop, planarRegion, polygonComponent )
+import Moonlight.Planar.Point (Point (..), QueryPoint)
+import Moonlight.Planar.Types (Triangulation)
+import Moonlight.Planar.Validation (validateTriangulation)
 
 requireRight :: Show error => String -> Either error value -> IO value
 requireRight label value = case value of
@@ -71,3 +75,70 @@
 rectangleComponent minimumX minimumY maximumX maximumY =
   rectangleLoop minimumX minimumY maximumX maximumY
     >>= requireRight "rectangle component" . (`polygonComponent` [])
+
+requireJust :: String -> Maybe value -> IO value
+requireJust _ (Just value) = pure value
+requireJust label Nothing = fail (label <> ": expected Just")
+
+randomPoints :: Word64 -> Int -> [Point]
+randomPoints seed count = take count (go seed Set.empty)
+ where
+  go :: Word64 -> Set.Set (Point) -> [Point]
+  go state seen =
+    let state1 = lcg state
+        state2 = lcg state1
+        x = unit state1 * 2 - 1
+        y = unit state2 * 2 - 1
+        point = Point x y
+     in if Set.member point seen
+          then go state2 seen
+          else point : go state2 (Set.insert point seen)
+
+  unit :: Word64 -> Double
+  unit value = fromIntegral (value `mod` 9_007_199_254_740_881) / 9_007_199_254_740_881
+
+  lcg :: Word64 -> Word64
+  lcg value = value * 6_364_136_223_846_793_005 + 1_442_695_040_888_963_407
+
+rectangleRegion :: Integer -> Integer -> Integer -> Integer -> IO PlanarRegion
+rectangleRegion minimumX minimumY maximumX maximumY =
+  rectangleComponent minimumX minimumY maximumX maximumY
+    >>= requireRight "rectangle region" . planarRegion . (: [])
+
+polygonComponentOf :: [(Integer, Integer)] -> IO PolygonComponent
+polygonComponentOf coordinates =
+  case map (uncurry integerPoint) coordinates of
+    firstPoint : secondPoint : thirdPoint : remaining -> do
+      loop <-
+        requireRight
+          "polygon loop"
+          (exactLoop (firstPoint :| (secondPoint : thirdPoint : remaining)))
+      requireRight "polygon component" (polygonComponent loop [])
+    _ -> fail "polygon fixture requires at least three points"
+
+polygonRegion :: [(Integer, Integer)] -> IO PlanarRegion
+polygonRegion coordinates =
+  polygonComponentOf coordinates
+    >>= requireRight "polygon region" . planarRegion . (: [])
+
+annulusRegion
+  :: (Integer, Integer, Integer, Integer)
+  -> (Integer, Integer, Integer, Integer)
+  -> IO PlanarRegion
+annulusRegion
+  (outerMinX, outerMinY, outerMaxX, outerMaxY)
+  (holeMinX, holeMinY, holeMaxX, holeMaxY) = do
+    outer <- rectangleLoop outerMinX outerMinY outerMaxX outerMaxY
+    hole <-
+      requireRight
+        "annulus hole loop"
+        ( exactLoop
+            ( integerPoint holeMinX holeMinY
+                :| [ integerPoint holeMinX holeMaxY
+                   , integerPoint holeMaxX holeMaxY
+                   , integerPoint holeMaxX holeMinY
+                   ]
+            )
+        )
+    component <- requireRight "annulus component" (polygonComponent outer [hole])
+    requireRight "annulus region" (planarRegion [component])
diff --git a/test/zigzag/Main.hs b/test/zigzag/Main.hs
--- a/test/zigzag/Main.hs
+++ b/test/zigzag/Main.hs
@@ -7,15 +7,12 @@
 import Data.Set qualified as Set
 import Data.Vector qualified as Vector
 import Moonlight.Homology.Chain (HomologicalDegree (..))
-import Moonlight.Triangulation.Example.ActivationZigzag
+import Moonlight.Planar.Example.ActivationZigzag
   ( squareLoopAcrossDepths
   )
-import Moonlight.Triangulation.Types
-  ( Point (..)
-  , RadiusSquared
-  , mkRadiusSquared
-  )
-import Moonlight.Triangulation.Zigzag
+import Moonlight.Planar.Point (Point (..))
+import Moonlight.Planar.Scalar (RadiusSquared, mkRadiusSquared)
+import Moonlight.Planar.Zigzag
   ( ActivationComplex
   , ActivationInterval
   , ActivationPoint (..)
diff --git a/weeder.toml b/weeder.toml
--- a/weeder.toml
+++ b/weeder.toml
@@ -13,46 +13,46 @@
   '^Moonlight\.Hex\.Planar$',
   '^Moonlight\.Hex\.Serialization$',
   '^Moonlight\.Hex\.CellComplex$',
-  '^Moonlight\.Triangulation\.Scalar$',
-  '^Moonlight\.Triangulation\.LineSideInfo$',
-  '^Moonlight\.Triangulation\.Types$',
-  '^Moonlight\.Triangulation\.Math$',
-  '^Moonlight\.Triangulation\.Exact$',
-  '^Moonlight\.Triangulation\.CellSet$',
-  '^Moonlight\.Triangulation\.CellComplex$',
-  '^Moonlight\.Triangulation\.Zigzag$',
-  '^Moonlight\.Triangulation\.Region$',
-  '^Moonlight\.Triangulation\.Valuation$',
-  '^Moonlight\.Triangulation\.Interop$',
-  '^Moonlight\.Triangulation\.Dcel$',
-  '^Moonlight\.Triangulation\.Payload$',
-  '^Moonlight\.Triangulation\.JoinSemilattice$',
-  '^Moonlight\.Triangulation\.Handles\.HandleDefs$',
-  '^Moonlight\.Triangulation\.Handles\.Dynamic$',
-  '^Moonlight\.Triangulation\.Handles\.Iterators\.CircularIterator$',
-  '^Moonlight\.Triangulation\.Handles\.Iterators\.DynamicIterators$',
-  '^Moonlight\.Triangulation\.Handles\.Iterators\.FixedIterators$',
-  '^Moonlight\.Triangulation\.Handles\.Iterators\.HullIterator$',
-  '^Moonlight\.Triangulation\.PointLocation$',
-  '^Moonlight\.Triangulation\.Validation$',
-  '^Moonlight\.Triangulation\.FloodFillIterator$',
-  '^Moonlight\.Triangulation\.IntersectionIterator$',
-  '^Moonlight\.Triangulation\.BulkLoad$',
-  '^Moonlight\.Triangulation\.Removal$',
-  '^Moonlight\.Triangulation\.Session$',
-  '^Moonlight\.Triangulation\.Cdt$',
-  '^Moonlight\.Triangulation\.Minkowski$',
-  '^Moonlight\.Triangulation\.Overlay$',
-  '^Moonlight\.Triangulation\.Refinement$',
-  '^Moonlight\.Triangulation\.SetAlgebra$',
-  '^Moonlight\.Triangulation\.Parallel$',
-  '^Moonlight\.Triangulation\.Serialization$',
-  '^Moonlight\.Triangulation\.Voronoi$',
-  '^Moonlight\.Triangulation\.Voronoi\.Handles$',
-  '^Moonlight\.Triangulation\.Interpolation$',
-  '^Moonlight\.Triangulation\.HintGenerator$',
-  '^Moonlight\.Triangulation\.Foreign\.Exports$',
-  '^Moonlight\.Triangulation$'
+  '^Moonlight\.Planar\.Scalar$',
+  '^Moonlight\.Planar\.LineSideInfo$',
+  '^Moonlight\.Planar\.Types$',
+  '^Moonlight\.Planar\.Math$',
+  '^Moonlight\.Planar\.Exact$',
+  '^Moonlight\.Planar\.CellSet$',
+  '^Moonlight\.Planar\.CellComplex$',
+  '^Moonlight\.Planar\.Zigzag$',
+  '^Moonlight\.Planar\.Region$',
+  '^Moonlight\.Planar\.Valuation$',
+  '^Moonlight\.Planar\.Interop$',
+  '^Moonlight\.Planar\.Dcel$',
+  '^Moonlight\.Planar\.Payload$',
+  '^Moonlight\.Planar\.JoinSemilattice$',
+  '^Moonlight\.Planar\.Handles\.HandleDefs$',
+  '^Moonlight\.Planar\.Handles\.Dynamic$',
+  '^Moonlight\.Planar\.Handles\.Iterators\.CircularIterator$',
+  '^Moonlight\.Planar\.Handles\.Iterators\.DynamicIterators$',
+  '^Moonlight\.Planar\.Handles\.Iterators\.FixedIterators$',
+  '^Moonlight\.Planar\.Handles\.Iterators\.HullIterator$',
+  '^Moonlight\.Planar\.PointLocation$',
+  '^Moonlight\.Planar\.Validation$',
+  '^Moonlight\.Planar\.FloodFillIterator$',
+  '^Moonlight\.Planar\.IntersectionIterator$',
+  '^Moonlight\.Planar\.BulkLoad$',
+  '^Moonlight\.Planar\.Removal$',
+  '^Moonlight\.Planar\.Session$',
+  '^Moonlight\.Planar\.Cdt$',
+  '^Moonlight\.Planar\.Minkowski$',
+  '^Moonlight\.Planar\.Overlay$',
+  '^Moonlight\.Planar\.Refinement$',
+  '^Moonlight\.Planar\.SetAlgebra$',
+  '^Moonlight\.Planar\.Parallel$',
+  '^Moonlight\.Planar\.Serialization$',
+  '^Moonlight\.Planar\.Voronoi$',
+  '^Moonlight\.Planar\.Voronoi\.Handles$',
+  '^Moonlight\.Planar\.Interpolation$',
+  '^Moonlight\.Planar\.HintGenerator$',
+  '^Moonlight\.Planar\.Foreign\.Exports$',
+  '^Moonlight\.Planar$'
 ]
 
 root-instances = []
