Skip to content

mortie specification & conventions (v1.0)

This page is the normative record of the mortie 1.x stability contract: the packed-word encodings (the spatial morton word and the temporal toc word), the decimal string grammar, the morton-hive store layout, the coverage-MOC serializations, and the zarr DGGS convention block for morton-declared stores. Everything marked contract here is frozen for the 1.x major-version series; anything not marked contract is informative.

Design rationale — why each decision was made, with trade studies and ratification records — lives in zagg's docs/design/sparse_coverage.md (the decisions registry, D-numbered entries). That document cites this page; this page is the spec. Grammars and constants are normative here only — duplicated normative text drifts.

Contents:

  1. The packed 64-bit morton word
  2. Decimal string representation
  3. Resolution table
  4. Order-29 points: encoding-carried kind and the decimal-parse tie-break
  5. Zarr DGGS convention block
  6. Morton-hive store layout
  7. Coverage MOC serializations
  8. Rank-space (x, y) deinterleave
  9. Latitude convention: authalic on WGS84
  10. Frozen for 1.x
  11. The packed 64-bit toc word

1. The packed 64-bit morton word

Contract. A morton index is one unsigned 64-bit word encoding a HEALPix NESTED cell (or an order-29 point) with its order carried intrinsically. Source of truth in code: mortie-core/src/decimal_morton.rs (MAX_ORDER = 29, BODY_TUPLES = 27), re-exported as mortie_rustie::decimal_morton.

Bit layout (MSB → LSB)

[ 4-bit prefix ][ 54-bit body (27 x 2-bit) ][ 6-bit suffix ]
  63 .. 60        59 ..  6                     5 ..  0
  • prefix (bits 63–60) — the HEALPix base cell stored as base_id + 1, so the 12 base cells occupy 1..=12. 0 is the empty/null sentinel; 13..=15 are invalid. The +1 shift is monotonic, so a raw unsigned sort is preserved as a Z-order curve.
  • body (bits 59–6) — 27 two-bit tuples, one per order 1..=27. Order 1 occupies the highest tuple (bits 59–58), order 27 the lowest (bits 7–6). The stored value is 0..=3, interpreted as 1..=4 (a decode-time +1, matching the decimal grammar in §2).
  • suffix (bits 5–0) — one plain unsigned integer 0..=63, a preorder numbering of the path tail past tuple 27:
suffix meaning
0..=27 variable-length area element; the order is the suffix value (0 = base-cell-only). 27 = order-27 with empty tail.
28..=47 order-28/29 area cells in parent-first preorder: r = t28*5 + (t29_present ? t29 + 1 : 0), suffix = 28 + r, with t28/t29 the stored 0..=3 tuple values. Each t28 owns a 5-block: [t28] then its four [t28,t29] children.
48..=63 order-29 point, max-encoded (no area claim): r2 = t28*4 + t29, suffix = 48 + r2. A point sorts after every area cell sharing its body (highest suffix range) — the finest, last thing there.
  • Canonical zero-fill: every bit below an element's order is zero-filled, so two encodings of the same cell are bit-equal — integer equality, hashing, dedup, and the raw sort all work on the word directly.
  • Unsigned storage: the word is stored and exchanged as uint64. The signed "negative = southern" form is a presentation detail of the decimal string (§2), never a storage form. Reinterpreting the word as int64 is an error: base cells 7–11 set bit 63 and would read back negative.
  • Z-order: a raw unsigned sort of packed words is a Z-order (Morton) curve traversal, parent-before-children across the whole order range 0–29.

Kind: area vs point

A decoded word is either an area element (a real cell with spatial extent, orders 0–29) or an order-29 point (a location cast to maximum resolution with no area claim — e.g. a raw lat/lon conversion). The kind is carried by the suffix range, not by any external flag — the load-bearing convention §4 states normatively — and the only place the two kinds can collide is the order-29 decimal string, resolved by the §4 parse tie-break.

2. Decimal string representation

Contract. The decimal string is the render/interchange external form of a morton index (paths, logs, inventories, display). Packed uint64 words are the storage and compute form; output types are never data-dependent:

  • strings for display/interchange at every order;
  • packed uint64 for storage and compute;
  • legacy signed i64 only via the explicit, capped to_legacy_i64() escape hatch.

Grammar:

morton-decimal = ["-"] base-digit *order-digit [kind-suffix]
base-digit     = "1" / "2" / "3" / "4" / "5" / "6"
order-digit    = "1" / "2" / "3" / "4"
kind-suffix    = "p"    ; POINT ids only; render/interchange form only
  • Sign + base digit form a constant-width component with 12 values (1..6 / -1..-6; the sign renders the southern base cells). One order-digit follows per order; digits are 1-4, never 0. The string length minus the sign/base component is the order.
  • String prefix = spatial ancestor at every level: -31123 is a descendant of -311, and lexicographic grouping under a fixed-width prefix is spatial grouping.
  • The base component grammar is -?[1-6] — reserved wherever names must be distinguishable from morton components (see §6.5).
  • Kind suffix p (espg-ruled in review, 2026-07-21): the render form MAY carry a terminal p on point ids (e.g. -62…21p), and only there — a p is legal solely on a full order-29 string, because points exist only at order 29. Paths never carry it: points don't live in paths, and the §6 path grammar is unchanged. Rationale: no letter occurs anywhere in the base grammar (sign, base digit, digits 1-4), so a letter suffix is maximally distinguishable, greppable, and inert in shells, markup, and URLs; * and a terminal . were considered and rejected (glob/markdown hazards; trailing-dot stripping).

Order-29 kind marking and the unmarked tie-break

With the kind suffix, the decimal round-trip is lossless for both kinds — the normative contract: an area word renders unmarked and parses back to itself; a point word renders p-marked and parses back to itself. The p emit/accept is implemented in PR #121 (issue #120), with golden vectors. The residual ambiguity is only the unmarked order-29 string — path components and legacy renders — which denotes both kinds and parses as the area word (the normative §4 tie-break; every pre-suffix string is an area context, so the rule is fully backward compatible).

3. Resolution table

Informative (the formulas are contract; the rendered values are derived). nside = 2^order; both columns derive from one Earth model — the exact HEALPix sphere at mean radius R = 6371.0088 km. Every order-k HEALPix cell has identical area 4πR² / (12 · 4^order) (HEALPix is equal-area by construction); the cell scale is the square root of that area — the RMS cell spacing sqrt(4πR² / (12 · 4^order)). The table below is regenerated from these formulas and pinned by mortie/tests/test_spec_page.py so it cannot drift.

Note — code and page unified. These are the normative, sphere-derived values, and mortie.orders.order2res now derives from the same sphere: order2res(order) = sqrt(4πR² / (12 · 4^order)) with the single mortie.orders.EARTH_RADIUS_KM = 6371.0088 constant. Its consumers (res2display and the buffer-pad computation in tests/test_coverage_boundary.py) therefore read the cell-scale column below directly. This replaced the historical flat constant 111 km/deg × 58.6323 × 0.5^order (an implied sphere R ≈ 6366 km), a behavioral change of ~0.2% at every order, per mortie #119.

order nside cell scale cell area
0 1 6,519.623 km 4.25055e+07 km2
1 2 3,259.812 km 1.06264e+07 km2
2 4 1,629.906 km 2.65659e+06 km2
3 8 814.953 km 664,148 km2
4 16 407.476 km 166,037 km2
5 32 203.738 km 41,509.3 km2
6 64 101.869 km 10,377.3 km2
7 128 50.935 km 2,594.33 km2
8 256 25.467 km 648.582 km2
9 512 12.734 km 162.146 km2
10 1024 6.367 km 40.5364 km2
11 2048 3.183 km 10.1341 km2
12 4096 1.592 km 2.53352 km2
13 8192 795.852 m 633,381 m2
14 16384 397.926 m 158,345 m2
15 32768 198.963 m 39,586.3 m2
16 65536 99.482 m 9,896.58 m2
17 131072 49.741 m 2,474.15 m2
18 262144 24.870 m 618.536 m2
19 524288 12.435 m 154.634 m2
20 1048576 6.218 m 38.6585 m2
21 2097152 3.109 m 9.66463 m2
22 4194304 1.554 m 2.41616 m2
23 8388608 77.720 cm 0.604039 m2
24 16777216 38.860 cm 0.15101 m2
25 33554432 19.430 cm 0.0377525 m2
26 67108864 9.715 cm 0.00943811 m2
27 134217728 4.857 cm 0.00235953 m2
28 268435456 2.429 cm 0.000589882 m2
29 536870912 1.214 cm 0.000147471 m2

4. Order-29 points

Contract. Kind is carried by the encoding itself — never by store or array metadata. The packed word's suffix region (§1) is the kind:

  • suffix 0..=47 decodes as an area word — an exact cell at its encoded order, at every order 0–29. An order-29 area word is a genuine order-29 cell; nothing is unrepresentable.
  • suffix 48..=63 decodes as an order-29 point — a location with no area claim, full stop.

There is no resolution/kind field in the §5 attrs block and readers never consult a declaration: two encodings, two meanings, one word. Mixed content — exact cells at any orders alongside order-29 points — is well-formed in a single coordinate by construction, because each word carries its own kind. (Provenance: espg-ratified 2026-07-21 on the PR #118 review, superseding the drafted declaration-based designs.)

The decimal-parse tie-break (contract)

Packed words never collide across kinds — the suffix ranges are disjoint. The one ambiguity in the convention is the decimal repr at order 29: a full order-29 string (base component + 29 digits) denotes both the order-29 area cell and the max-encoded point on the same path, because the string carries path only, never kind (§2). Strings at orders 0–28 denote area cells alone (points exist only at order 29). The normative tie-break:

A p-marked string yields the POINT word; an unmarked string always yields the AREA word. For an order-29 path with final two (stored) tuple values t28, t29: unmarked ⇒ the area word (suffix 28 + t28·5 + t29 + 1); p-marked ⇒ the point word (suffix 48 + t28·4 + t29). Round-trip identity holds for both kinds; the unmarked-string rule is the tie-break for the one truly ambiguous form (§2), and since every pre-suffix string is unmarked it is fully backward compatible.

In channels that strip or cannot carry the suffix (paths above all), point-ness does not survive the string: those channels carry the packed word when kind matters. The unmarked tie-break is what mortie's parser has always implemented, golden-pinned by mortie/tests/test_spec_page.py; the p emission/acceptance is implemented in PR #121 (issue #120), with golden vectors.

Parse-side API (normative surface)

The tie-break above is a parse-side contract, so it is worth stating in the terms a parse caller sees. The public entry points, all implementing the same kernel (decimal_morton::from_decimal_repr):

Entry point Shape
mortie.decimal_to_word(s, dtype=np.uint64) scalar; numpy-only (no pandas import), dtype selects np.uint64 / int / MortonIndexScalar
mortie.decimals_to_words(arr) vectorized; the inverse of MortonIndexArray.to_decimal(), shape-preserving, uint64 out
MortonIndexArray.from_decimal(arr) the same parse as an ExtensionArray constructor, the inverse of .to_decimal()

What a parse-side caller must know: an unmarked order-29 id parses to the area word, so a point word does not round-trip through an unmarked string. Emit renders point words p-marked, so word → to_decimal → from_decimal is the identity end to end — but any channel that strips the marker (a path component, a legacy render, a hand-typed id) returns the area word for what may have been a point. At orders 0–28 there is no ambiguity to resolve. Pinned by mortie/tests/test_decimal_parse.py.

Points at coarser levels (informative)

Membership of a point in a coarser cell is the ordinary truncation (coarsen / clip2order: drop the path tail below the target order) — a transient cast computed where needed, never a stored re-encoding.

Viewer-side float64 casts (informative)

IEEE-754 float64 (JavaScript Number, plain JSON parsers) is integer-exact only to 2^53, which covers NESTED ids only through order 24. A display layer in such a runtime (e.g. the gridlook viewer) may transiently cast point ids to order ≤ 24 for Number safety — an implementation detail of that display layer, not encoding semantics; other viewers and future runtimes need no such cast. Area cells are never coarsened this way: coarsening an area cell changes the labelled thing (that is aggregation, cf. zagg D24) — servers fabricate or aggregate instead.

5. Zarr DGGS convention block

Contract. A zarr store whose cell coordinate is packed morton words declares, on the group holding the cell-indexed arrays:

{
  "zarr_conventions": [
    {
      "schema_url": "https://github.com/espg/mortie/blob/main/docs/specification.md#dggs-attrs",
      "spec_url": "https://github.com/espg/mortie/blob/main/docs/specification.md",
      "uuid": "3e22156d-ea9e-4e01-95fe-e3809a4b41e7",
      "name": "morton-dggs",
      "description": "Packed-u64 morton (HEALPix) DGGS convention"
    }
  ],
  "dggs": {
    "name": "morton",
    "coordinate": "morton",
    "latitude": "authalic-wgs84",
    "...": "grid parameters (refinement level, ellipsoid, ...)"
  }
}
  • name: "morton" and coordinate: "morton" — the grid name is distinct, never name: "healpix" + indexing_scheme: "morton". A scheme-blind reader that recognizes healpix but ignores the indexing scheme would silently decode morton words as NESTED ids and mis-place every cell; an unknown grid name makes it hard-reject with a diagnostic instead. This matches moczarr's xdggs registration (grid_name: "morton").
  • There is no kind/resolution field: point-vs-area kind is carried by the word encoding itself (§4), never by attrs.
  • Latitude convention — the grid-parameter block records the latitude convention of §9 under the key latitude, whose two tokens are "authalic-wgs84" (the default convention) and "geodetic-spherical" (the legacy escape). A writer at this spec version MUST record it; see §9 for what an absent marker means and for the reader obligation. Readers must refuse to compose covers across conventions.
  • Convention identity — the zarr_conventions entry above is the self-declared convention record (the zarr-conventions mechanism supports self-declared entries). The UUID 3e22156d-ea9e-4e01-95fe-e3809a4b41e7 is minted once and permanent; readers may key on it. zarr_conventions is a list: a future upstream dggs-registry entry coexists alongside this one rather than replacing it.

6. Morton-hive store layout

Contract. The morton-hive layout stores one self-describing zarr per spatial shard under a morton digit tree. The convention is versioned by the manifest's spec string (morton-hive/1, /2, /3); every frozen grammar remains valid forever for stores declaring its version — readers discriminate by the spec string, never by sniffing names.

6.1 Common structure (all versions)

{store_root}/
  morton_hive.json                 <- static manifest, versioned `spec` string
  coverage.moc                     <- optional root coverage MOC (§7.3)
  {sign+base}/{d1}/{d2}/.../       <- one decimal digit per path level
    {leaf}.zarr/                   <- vanilla zarr v3 leaf (naming per version)
    <declared sidecars>            <- stats record, sub-shardmap (per version)
  • Digit tree: path components are the decimal-string components of §2 — the constant-width {sign+base} first, then one digit ([1-4]) per order. Shards live at mixed orders, so every order is a legal node. A manifest path_grouping parameter (default 1) declares how many digits each component chunks; readers chunk the digit string per the manifest, never by assumption.
  • Grouping remainder (contract): the {sign+base} component stands alone and never participates in groupingpath_grouping chunks only the order-digit string that follows it. When the order is not a multiple of path_grouping, the leading digit components are full-width and the final component carries the remainder (never remainder-first). This is the only split that keeps a coarser shard's grouped prefix a directory-prefix of its finer descendants, so shared ancestor directories survive across mixed shard orders. Worked example — order 8 at path_grouping: 3 chunks the eight order-digits 33142241 as 3+3+2 (331/422/41), leaving {sign+base} 4 as its own leading component:
4/331/422/41/433142241.zarr    <- id 433142241, order 8, path_grouping: 3
  • Node invariant: below a product root, a node contains only digit children, *.zarr objects, and the declared leaf-adjacent sidecar names — nothing else, ever. The walker's child classification depends on the name set being closed.
  • Termination: object stores have no empty prefixes and S3 LIST is strongly consistent, so a delimiter-LIST returning no digit children is a definitive "nothing finer exists". Absence is trustworthy.
  • Commit stamp: presence is not trustworthy without one. A leaf's root metadata is finalized last (a root-group attrs update carrying the stamp); a .zarr/ prefix whose root metadata lacks the stamp is debris — incomplete, ignorable, safe to overwrite on retry.
  • Overview flagging: ancestor-node zarrs must carry role: overview attrs; the role is never inferred from tree position (a shallow zarr may be coarse source in a sparse region).
  • Manifest: morton_hive.json at the (product) root is the reader's bootstrap; with it every shard path is computable arithmetically with zero requests. Its spec string versions the convention.

6.2 morton-hive/1 — bare leaves

Leaf basename is the full morton id: {full_id}.zarr (e.g. .../1/2/3/-31123.zarr/). Self-describing without parsing its path.

6.3 morton-hive/2 — time-windowed leaves

A /2 store's manifest carries a temporal block declaring time encoding/units/epoch/calendar, the membership timestamp field, the window schedule, and the append policy. A /1 store is a /2 store with schedule: none.

Leaf naming:

{full_id}_{window}.zarr        <- windowed leaf (schedule != none)
{full_id}.zarr                 <- bare leaf   (schedule == none only)
  • Separator is _; it never appears in morton ids nor in window labels, so the split is unambiguous. Parse rule: split on the first _.
  • One schedule per store; at most one leaf per (id, window); bare and windowed source leaves never mix in one store.

Window schedules and label grammar (lexicographic order = chronological order within a store):

schedule label grammar example label window
none (default) (no label) unbounded; re-run replaces the leaf
yearly YYYY 2025 [2025-01-01T00:00Z, 2026-01-01T00:00Z)
monthly YYYYMM 202511 calendar month
daily YYYYMMDD 20251103 calendar day
quarterly (grammar-reserved, not implemented) YYYYQ[1-4] 2025Q3 calendar quarter
explicit list opaque, [0-9A-Za-z-]{1,32} melt-2019 declared per label in the manifest
  • Boundaries are UTC calendar terms, half-open [start, end), regardless of the store's native time encoding (the temporal block declares the conversion). An observation stream straddling a boundary contributes to both windows.
  • Explicit labels are opaque: the manifest maps each label to its [start, end); readers never parse semantics out of a custom label. The charset excludes _ by construction.
  • The reserved token all (§6.4) is excluded from the window-label grammar going forward: no store written under any spec version may declare an explicit window labeled all. In /2 the token has no structural role — the schedule: none leaf is the bare {full_id}.zarr, not all.zarr (that is a /3 construct) — so the reservation is a forward-going constraint on new writers, not a retroactive narrowing of the frozen /2 opaque grammar. A pre-existing /2 store whose manifest happened to declare an all label stays readable under its frozen grammar: /2 labels are opaque and manifest-resolved, so all there is just another custom label.
  • Per-shard stats sidecars (when written): stats_{window}.json, or stats.json under schedule: none.

6.4 morton-hive/3 — window-only leaf naming

Completes the axis separation: product = root prefix (§6.5), space = digit path, time = basename — each identity axis in exactly one place.

{window}.zarr                  <- leaf, basename = time window alone
all.zarr                       <- schedule: none (reserved token)
  • The morton id no longer appears in the basename; it is recoverable arithmetically from the digit path and recorded in the leaf's stamp attrs / stats sidecar (shard_key).
  • all is a reserved token: it names the /3 schedule: none leaf (reads as "all time"), cannot collide with the digit-shaped generative labels, and is excluded from the window-label grammar going forward (no store, any spec version, may declare an explicit window labeled all). The token is structural only in /3; §6.3 records how the same forward-going reservation applies to /2, which has no all leaf.
  • Sidecar naming aligns to the leaf: {window}.stats.json / all.stats.json.
  • /1 and /2 stores keep their frozen grammars (§6.2, §6.3) forever; readers discriminate by the manifest spec string.

6.5 Product roots and the product-name grammar

A multi-product store is a directory of stores: each product lives under its own human-readable root prefix {name}/, and a product subtree is a complete, unmodified morton-hive store (bare-named manifest, MOC, digit tree). A bare single-product store (manifest at the store root) remains fully valid; readers distinguish the two root forms by content — a manifest at the root ⇒ bare store; name-shaped prefixes ⇒ product directory.

Product-name grammar (contract):

product-name = 1*192( lowercase-alphanum / "-" / "_" )   ; [a-z0-9_-]{1,192}

with the base-component exclusion: a product name must not match the morton base-component grammar -?[1-6] (§2), so the walker's child classification stays unambiguous. Names are URL-safe by construction (no percent-encoding, no case-folding hazards).

  • Length 1–192 characters. The charset is single-byte ASCII, so 192 chars = 192 bytes. Derivation: the POSIX filename-component ceiling is 255 bytes; a D23 downloader that materializes the tree locally shares the product's path component with a 13-character immutable-provenance decoration ({name}+{catalog-hash}/ — a + plus a 12-hex catalog fingerprint), leaving a hard ceiling of 255 − 13 = 242. The 192 cap sits 50 characters under that ceiling and keeps total-path budgets comfortable (~400 chars of the 1,024-byte S3 total-key budget, and macOS PATH_MAX, at order-24 digit depth under realistic prefixes).

6.6 Repr reminder for paths

Hive paths embed decimal components (§2). The §2 non-injectivity note applies: leaf ids at order 29 could not distinguish point from area — hive stores avoid the ambiguity structurally (shards live at coarse orders, and in-store kind rides the packed words themselves, §4) — and any parsed path string resolves by the §4 tie-break; no implementation may round-trip point-ness through a path string.

7. Coverage MOC serializations

Contract (spec: "morton-moc/1"). Coverage is declared in three tiers; all three share the envelope's encoding discriminator: "ranges" | "bitmap" | "full".

7.1 Stamp envelope: the tier-0 morton box

The leaf commit stamp carries the shard's morton box — the canonical ≤ 4-member MOC covering its occupied cells — padded to exactly four decimal-string slots; the pad sentinel is JSON null. encoding: "full" declares a fully occupied subtree (the shard id itself is the exact MOC; no bitmap sidecar is written).

7.2 Leaf bitmap sidecar (coverage.moc inside the leaf)

Exact cell-order occupancy for one shard subtree:

  • The raw bitmap has 4^depth bits (depth = cell_order - shard_order); raw size is exactly ceil(4^depth / 8) bytes.
  • Bit convention (contract, golden-vector-pinned): bit i is the i-th shard-subtree cell in ascending packed-word order — equivalently, the base-4 rank of the cell's digit tail with digits 1..4 mapped to 0..3 — packed MSB-first within each byte (np.packbits order).
  • The payload is one zstd stream over the raw bitmap. The zstd stream format is contract; the compression level is non-normative (any level decodes identically).
  • A decoder must reject a payload whose decompressed size is not exactly the raw bitmap size — never zero-pad or truncate (a partial cell set is a false negative).

7.3 Root coverage MOC (coverage.moc at the store/product root)

A JSON envelope with encoding: "ranges" listing shard-order coverage.

Required keys (a reader validates the set, never infers it):

  • spec"morton-moc/1" (§7 header), tying the envelope to this serialization;
  • encoding"ranges" for this tier;
  • order — integer, the shard order of every cell in ranges (the common order the decimal endpoints share);
  • ranges — the list of [first, last] runs below.

source, generated_at, and optional time_range are informative carrier fields. Field semantics:

  • A range is an inclusive [first, last] run of same-order cells within one base cell, consecutive in base-4 digit-tail rank (the same ascending packed-word order as §7.2) — range ordering is contract (golden-vector-pinned).
  • Endpoints are decimal strings (§2), never JSON numbers: packed words exceed 2^53 and would be silently mangled by float-based JSON parsers. They are always unmarked area words at the shard order, never p-marked: points live only at order 29 and cannot be range endpoints.
  • The carrier fields above are informative cache metadata; the ranges are a regenerable cache of the leaf-stamp truth.

8. Rank-space (x, y) deinterleave

Contract. A depth-d subtree holds 4^d cells whose ascending packed-word order is a Z-order (morton) curve over a 2^d × 2^d block. The rank of a cell — its position 0..4^d − 1 within the subtree, the same base-4 digit-tail rank the §7.2 bitmaps index by — maps to a face-local (x, y) pair by pure bit deinterleave. Source of truth in code: mortie/rank_xy.py (rank_to_xy / xy_to_rank; the pure-numpy mask ladder retained there is the executable reference and golden-vector generator, while the shipped kernel is the thin Rust binding in src_rust/src/rank_xy.rs over the vendored healpix crate's z-order curve — equivalence is test-pinned elementwise across depths).

  • Bit parity: x is the gather of the rank's even bits (bit 0, 2, 4, …), y the gather of its odd bits (bit 1, 3, 5, …). Equivalently rank = interleave(x, y) with x supplying the LSB.
  • Orientation: the origin (0, 0) is the subtree's south corner; x increases toward the north-east edge, y toward the north-west edge — the HEALPix face-coordinate frame.
  • Subtree-locality: the input is rank-space, never a packed morton word (§1). A word carries base cell, order, and kind; strip the shard prefix down to the digit-tail rank first. Whole-word (base, x, y) decomposition is deliberately out of scope here and reserved for a future layer that composes word → (prefix, rank) → this transform.
  • healpy equivalence (the community convention this attaches to): for nside = 2^d, rank_to_xy(r, d) == healpy.pix2xyf(nside, r, nest=True)[:2] and xy_to_rank(x, y, d) == healpy.xyf2pix(nside, x, y, 0, nest=True) — i.e. HEALPix C++ pix2xyf/xyf2pix restricted to one face. The golden vectors in mortie/tests/test_rank_xy.py pin this at depths 6 and 8.

8.1 Worked example (depth 2)

rank = 6 = 0b0110: even bits (b0, b2) = (0, 1) give x = 0b10 = 2; odd bits (b1, b3) = (1, 0) give y = 0b01 = 1. The full 4 × 4 block, x left→right and y bottom→top (origin at the lower-left / south corner):

y=3 | 10 11 14 15
y=2 |  8  9 12 13
y=1 |  2  3  6  7
y=0 |  0  1  4  5
      x=0 x=1 x=2 x=3

Reading the ranks 0, 1, 2, 3, … traces the familiar Z (self-similar across depths): child tuples order as (x, y) = (0,0), (1,0), (0,1), (1,1) at every level.

9. Latitude convention: authalic on WGS84

Contract. Geographic coordinates crossing the mortie API are WGS84 geodetic latitude / longitude in degrees. Since issue #186, the default convention latitude="authalic" converts geodetic latitude to authalic latitude before the spherical HEALPix kernel (ingress) and back after it (egress), making every cell equal-area on the WGS84 ellipsoid by construction. Longitude never converts. The legacy convention latitude="geodetic-spherical" (geodetic latitude fed to the spherical kernel as-is — the pre-0.10 behavior) remains available as an explicit escape on every crossing.

Reference ellipsoid (pinned, normative). The conversion depends only on the WGS84 defining constants, fixed across all WGS84 realizations — never on the geoid or the epoch:

a   = 6378137            (semi-major axis, metres; exact)
1/f = 298.257223563      (inverse flattening; exact decimal)
e^2 = f (2 - f)          (derived — compute it, do not parse a decimal)

a and 1/f are the two normative inputs; e^2 is derived from them and an implementation MUST compute it rather than parse a printed decimal. Evaluating f = 1/298.257223563 then f (2 - f) in binary64 yields 0.0066943799901413165 (shortest round-tripping decimal; the value src_rust/src/authalic.rs actually uses). The correctly rounded 16-digit decimal of the exact real, 0.006694379990141317, parses to the next double up — a 1-ulp difference, ~1e-19 rad in the series and six orders inside the error bound below, but enough that quoting it as the number to reproduce would be wrong. Implementations may legitimately differ in the last ulp here depending on evaluation order.

The mapping (normative). Both directions are 5-harmonic trigonometric series with coefficients that are exact rationals in powers of e^2 through e^10. The closed form they approximate is Snyder 1987 eqs. 3-11/3-12: q(phi) and beta = asin(q(phi) / q(pi/2)). Snyder's eq. 3-18 is the inverse series (authalic → geodetic) — its published e^2/e^4/e^6 terms are reproduced exactly by I1/I2/I3 below. The forward series F1..F5 is this project's reversion/perturbation of the closed form, and the e^8/e^10 extensions in both directions are likewise derived here rather than published by Snyder:

beta = phi  + F1 sin(2 phi)  + F2 sin(4 phi)  + ... + F5 sin(10 phi)
phi  = beta + I1 sin(2 beta) + I2 sin(4 beta) + ... + I5 sin(10 beta)

F1 = -(1/3) e^2 - (31/180) e^4 - (59/560) e^6 - (42811/604800) e^8
     - (605399/11975040) e^10
F2 =  (17/360) e^4 + (61/1260) e^6 + (76969/1814400) e^8
     + (215431/5987520) e^10
F3 = -(383/45360) e^6 - (3347/259200) e^8 - (1751791/119750400) e^10
F4 =  (6007/3628800) e^8 + (201293/59875200) e^10
F5 = -(5839/17107200) e^10

I1 =  (1/3) e^2 + (31/180) e^4 + (517/5040) e^6 + (120389/1814400) e^8
     + (1362253/29937600) e^10
I2 =  (23/360) e^4 + (251/3780) e^6 + (102287/1814400) e^8
     + (450739/9979200) e^10
I3 =  (761/45360) e^6 + (47561/1814400) e^8 + (434501/14968800) e^10
I4 =  (6059/1209600) e^8 + (625511/59875200) e^10
I5 =  (48017/29937600) e^10

phi (geodetic) and beta (authalic) are in radians throughout this block — the series is only valid in radians, while the degree-facing API of the Contract paragraph converts on both sides (forward_rad(lat.to_radians()) .to_degrees()). Feeding degrees straight into sin(2 phi) silently produces a different partition. The coefficients are named by position here and by harmonic in the implementation: F1..F5 are FWD_S2..FWD_S10 and I1..I5 are INV_S2..INV_S10 in src_rust/src/authalic.rs.

The derivation, high-precision reference values, and the offline generator are mortie/tests/generate_authalic_reference.pymortie/tests/data/authalic_reference.json; the runtime implementation is src_rust/src/authalic.rs.

Error bound (normative). Truncation error of the series against the 60-digit closed form, measured on a 0.01-degree grid: 6.2e-15 rad forward, 8.2e-15 rad inverse. The documented conversion bound is <= 1e-13 rad (~0.6 µm on the ground) per direction (f64 evaluation rounding included); unit tests enforce <= 1e-14 rad against the committed references. The equator and the poles are exact fixed points; the divergence between the two conventions peaks near 45° latitude at |beta - phi| ~= 0.12830° (~14.26 km along the meridian).

The bound is each implementation's conversion-accuracy obligation, not a statement about mortie alone: an implementation conforms if each direction lands within <= 1e-13 rad of the reference values in mortie/tests/data/authalic_reference.json. What it does not promise is identical cell assignment everywhere — §10 freezes the coefficients but not the evaluation (mortie uses a Chebyshev recurrence with one sin_cos and degree↔radian conversions; a different-but-conformant series differs by ulps). Two conformant implementations therefore agree on the cell id of any coordinate farther than the combined bound (~1e-13 rad, ~0.6 µm) from a cell boundary, and may legitimately disagree for coordinates closer than that.

Edge cases (normative). Reproducing the cell ids requires reproducing three rules alongside the series:

  • Pole clamp, conditional. For input in [-90, 90] the converted value is clamped back into [-90, 90] (the inverse series overshoots ±90 by an ulp near the pole). Input outside [-90, 90] is deliberately not clamped and passes through converted-but-out-of-range, so an invalid latitude cannot silently become a valid pole cell — the legacy convention passes it through unconverted for the same reason.
  • Non-finite propagates unchanged. NaN → NaN and ±inf → ±inf; the raw series would return NaN for the infinities, so implementations short- circuit non-finite input before evaluating it.
  • Exact fixed points at 0 and ±90. These hold structurally, for any coefficients, since sin(2k·0) = sin(2k·(π/2)) = 0.

Non-correspondence (normative). Cell ids produced under the two conventions are non-corresponding partitions of the sphere: the same (lat, lon, order) generally hashes to different morton words, and the same word decodes to different geodetic coordinates. Datasets MUST NOT mix conventions, and set operations (occupancy AND/OR across covers) are meaningful only within one convention.

Store-level metadata therefore carries the convention, in the latitude key of the §5 dggs block, with exactly two tokens:

"latitude": "authalic-wgs84"        the default convention of this section
"latitude": "geodetic-spherical"    the legacy escape

A writer at this spec version or later MUST record one of them — the marker is not optional now that authalic is the default, because the most likely producer of an unmarked store is a new writer that took the default and never set the attr, and misreading that as legacy is silent (~0.128° of misplacement, no error). Absent ⇒ legacy geodetic-as-spherical applies only to stores predating this spec version; a reader SHOULD treat an absent marker on a store at this spec version or later as an error rather than assume either convention. This is mortie's recommended vocabulary; a consuming project's own attrs spec (zagg's, for its stores) remains authoritative for that project.

Symmetry rule. The conversion applies at every geodetic lat/lon crossing or nowhere: ingress converts forward before the kernel, egress applies the inverse on the way out. In mortie the ingress surfaces are point binning, polygon and linestring coverage (flat and MOC), WKB ingest (single and batched), the ring predicates, the prefix-trie geodetic entry points, and the dataframe/arrow skins; the egress surfaces are cell centres, boundaries, bounding boxes, polygon/WKB/WKT emit, and dissolve outlines. The rule, not the enumeration, is normative: any surface that accepts or returns geodetic lat/lon converts, and every such surface exposes the same latitude= escape. Below the API boundary the kernel operates purely in the authalic frame — spherical primitives never convert, and nothing converts twice.

10. Frozen for 1.x

The 1.x contract guarantees, immutable within the major version:

  • the §1 bit layout, order range 0–29, canonical zero-fill, unsigned storage, and the raw-sort Z-order property;
  • the §2 decimal grammar, its render/interchange status, and the emit conventions (strings display / uint64 storage / capped legacy i64 escape hatch);
  • the §4 encoding-carried kind convention (suffix 0..=47 = area, exact at every order; 48..=63 = order-29 point) and the decimal parse rules (p-marked string ⇒ point word; unmarked string ⇒ area word — the tie-break; the p kind suffix is render/interchange-only and never appears in paths);
  • the §5 convention identity (UUID) and the name: "morton" / coordinate: "morton" declaration;
  • the §6 hive grammars — /1, /2, /3 each frozen for stores declaring its spec string — including the _ split rule, the window-label grammars, the all reserved token (structural in /3; a forward-going window-label exclusion across all spec versions, §6.3/§6.4), the product-name grammar (charset, base-component exclusion, and the 1–192 character length cap), and the node invariant;
  • the §7 coverage contracts: the 4-slot null-padded box, the encoding discriminator values, the bitmap bit convention, and the root-MOC range ordering (zstd level and other codec parameters stay non-normative);
  • the §8 rank-space deinterleave: bit parity (x = even bits, y = odd bits), the south-corner orientation, and the healpy pix2xyf equivalence;
  • the §9 latitude convention: the pinned WGS84 constants, the series coefficients and their <= 1e-13 rad bound, the "authalic" / "geodetic-spherical" parameter vocabulary with authalic as the default, the store-attr latitude key and its two tokens ("authalic-wgs84" / "geodetic-spherical", §5/§9) — the wire-visible half that external readers key on — and the non-correspondence rule (never mix conventions in one dataset);
  • the §11 toc word: the bit layout (flag at bit 31, polarity 1 = timestamp), the 2³¹/2³² start/end quanta, the 1850 epoch on the leap-free GPS-aligned timescale with its UTC-boundary convention (pre-1972 zero offset; per-step offsets frozen once a step is in the table — leap-table appends are additive), TOC_MAX_NS applied to both encoders, the outward-rounding encode law (floored start, strictly-greater end ceiling), the decode semantics (exclusive envelope end), the valid-domain characterization and the garbage-in-garbage-out posture, the unsigned sort order and its tie-breaks, the merge law with its valid-domain scope and no-identity-element rule, the window-predicate conservatism directions, and the §11.8 conformance vectors (their UTC renderings scoped to the shipped leap table, per §11.8).

Extensions (new schedules, new spec versions, new encodings) are additive under new discriminator values; existing stores never reparse under new rules.

11. The packed 64-bit toc word

Contract. A toc index (temporal order coverage) is one unsigned 64-bit word encoding either an exact nanosecond timestamp or a quantized, conservative time range — the temporal sibling of the §1 morton word: self-describing, sortable as a plain unsigned integer, and closed under a semilattice merge. Source of truth in code: src_rust/src/toc.rs (the word kernels this section specifies — encode, decode, merge, sort order, window predicates) and mortie/_toc.py (the UTC/GPS timescale boundary). Decision provenance: the issue #175 decision ledger (the 1 ns base quantum and 32/31 split; the flag position, polarity, and name) and the design record on englacial/zagg#410.

External stores cite this section as the word grammar behind their own declarations — e.g. zagg's zagg-toc/1 attrs blocks store the grammar revision token mortie-toc/1. This section is the class of citation such a token is meant to resolve to; where a given store's documentation pointer actually points is that store's own release-pinned choice, and zagg's swaps here once this section ships (tracked on issue #193). Informative either way — the token vocabulary is the citing store's own. Two layers sit above this grammar and are out of scope here: the set algebra over covers of words (toc_normalize / toc_and, src_rust/src/toc/set_ops.rs) and the typed Toc object (issue #198). Both operate on words this section defines; neither changes one.

Naming note (informative): "toc" echoes tick/tock and T-MOC, but this is not an IVOA T-MOC and does not conform to the IVOA MOC 2.0 recommendation — different epoch, timescale, and cell model.

11.1 Timescale and epoch

Contract. Internal time is u64 nanoseconds since 1850-01-01T00:00:00 on a continuous, leap-free, GPS-aligned timescale: the scale ticks in SI seconds exactly with GPS time, and leap seconds exist only at the UTC conversion boundary, never inside the scale.

  • GPS interop is a pure constant offset. GPS_EPOCH_NS = 47,486 × 86,400 × 10⁹ = 4,102,790,400,000,000,000 — the GPS epoch 1980-01-06T00:00:00 as internal ns (47,486 proleptic-Gregorian days past 1850-01-01). internal = gps_ns + GPS_EPOCH_NS, exactly.
  • The UTC boundary (from_datetime64 / to_datetime64 in mortie/_toc.py): from 1972 on, the offset from naive UTC day-count time is GPS − UTC = TAI − UTC − 19 seconds, from the static leap-second table in mortie/_toc.py — zero at the GPS epoch, +18 s from the 2017-01-01 step, which is the last step in the table (none further is scheduled). The table's authoritative external source is the IERS leap-second announcements — Bulletin C: every post-1972 row transcribes one announced step (TAI − UTC minus 19 s), so an implementation without Python in reach can reproduce the identical table from Bulletin C alone. Before 1972 the proleptic convention is zero offset (naive day-count seconds, no leap adjustment), pinning the epoch identity 1850-01-01T00:00:00 → 0 ns exactly. Cost, stated as shipped: the mapping steps back 9 s across the 1972-01-01 boundary, so the last 9 SI seconds of 1971 alias into early 1972; the UTC conversion is exact and invertible from 1972 on. Internal instants falling inside an inserted leap second render into the following UTC second (datetime64 cannot express 23:59:60).
  • Leap-table appends are additive. When the IERS announces a step, the table gains a row; the conversion for every instant before a newly appended step is frozen and never moves, and internal-scale words never re-encode — the internal scale, not UTC, is what words persist in.
  • Span ceiling. TOC_MAX_NS = 2⁶³ − 2³² = 9,223,372,032,559,808,512 is the exclusive ceiling on internal times — 2³² ns (~4.3 s) below the 2⁶³ ns mark; the last valid instant renders, under the leap table as of the 2017-01-01 step, as UTC 2142-04-11T23:46:54.559808511 (a derived rendering, per the additive rule above; the internal ns ceiling itself is fixed): the range end code e = (t ≫ 32) + 1 must fit 31 bits. The ceiling is applied to both encoders so that every encodable word is mergeable — a timestamp in the last 2³² ns would encode fine but its merge envelope would overflow the end field, so it is rejected up front rather than wrapping silently.

11.2 Bit layout (MSB → LSB)

[ start: 32 bits, 2^31 ns units ][ flag: 1 bit ][ low: 31 bits ]
  63 .. 32                         31             30 .. 0
  • flag (bit 31) — the variant discriminator: 1 = timestamp, 0 = range.
  • flag = 1 (timestamp) — the word is the instant t_ns with the flag bit spliced in at position 31: bits 63–32 hold t_ns ≫ 31, bits 30–0 hold t_ns & (2³¹ − 1). The splice is monotone: unsigned word order over timestamps is exactly ns order.
  • flag = 0 (range) — bits 63–32 hold the start code s (units of Q_START_NS = 2³¹ ns, ~2.15 s, floored), bits 30–0 hold the end code e (units of Q_END_NS = 2³² ns, ~4.29 s, ceiled). The encoded envelope is the half-open interval [s · 2³¹, e · 2³²) ns.
  • Unsigned storage: the word is stored and exchanged as uint64. Reinterpreting it as int64 is an error: any timestamp at or past internal ns 2⁶² (and any range with start code ≥ 2³¹) sets bit 63 and would read back negative.

11.3 Encoding

Contract. Two encoders, total over their stated domains and erroring outside them (never wrapping):

  • Timestamp (time2toc): domain 0 ≤ t_ns < TOC_MAX_NS. The word is the §11.2 splice of t_ns.
  • Range (span2toc): the input is a real closed interval [start_ns, end_ns] with start_ns ≤ end_ns < TOC_MAX_NS. The codes are

text s = start_ns >> 31 (floor onto the 2^31 ns grid) e = (end_ns >> 32) + 1 (strictly-greater ceiling onto the 2^32 ns grid)

The end ceiling is strictly greater uniformly — including when end_ns sits exactly on the 2³² ns grid — so the half-open envelope [s · 2³¹, e · 2³²) always properly contains end_ns.

The conservative direction is law: encoding only ever widens — the envelope contains the real interval, never the reverse, and a real interval is never narrowed. Every derived operation below (merge, window predicates) preserves this direction.

The grammar also provides an instant an exact form (time2toc), so no instant need be widened into a range. Taking that form is writer discipline, not an encoder property: span2toc(t, t) is in domain and returns a range word (span2toc(0, 0) is the range word 1), and nothing here rejects it. A citing store that wants decoded (t, t) to mean "instant" states the obligation itself — zagg's zagg-toc/1 §8.1 makes it a MUST.

The all-zero word is unreachable. No encoder output is 0: the epoch instant encodes as 0x8000_0000 (the flag bit sits at position 31, not at the bottom of the word), and every range word has end code e ≥ 1, so the smallest range word is 1. External conventions may therefore reserve 0 as a fill/absence sentinel (zagg's zagg-toc/1 §8.2 does); mortie itself assigns 0 no meaning — under §11.4 it is simply out of domain.

Reserving 0 is free of encoder collisions, not of predicate hits. 0 is unreachable by both encoders, so a reserved sentinel can never be confused with a written word; but the operations of §11.7 are total (§11.4), and 0 decodes as the empty range envelope [0, 0). That envelope intersects nothing, so toc_overlaps(0, ·, ·) is false for every window — the property zagg's §8.2 argues over. It is, however, vacuously inside any window anchored at the epoch, so toc_contains(0, 0, q_end) is true: a store that reserves 0 must mask its fills before a containment query rather than rely on the sentinel selecting nothing.

11.4 Decoding, validity, and the garbage posture

Contract. Decoding (toc2time) is variant-dispatched on the flag bit:

  • a timestamp yields its exact instant twice: (t, t);
  • a range yields its half-open envelope bounds (s · 2³¹, e · 2³²) — the end bound is exclusive, strictly greater than every instant the range covers.

Valid domain. A word is valid exactly when it is encoder-reachable:

  • a timestamp word is valid iff its decoded instant t < TOC_MAX_NS — equivalently, iff its high field word ≫ 32 ≤ 2³² − 3;
  • a range word is valid iff its envelope is nonempty: s · 2³¹ < e · 2³², equivalently s ≤ 2e − 1 (which forces e ≥ 1).

Every encoder output satisfies these bounds, and every word satisfying them is producible by the corresponding encoder — the characterization is exact.

Garbage in, garbage out. Decoding and every derived operation are total: an out-of-domain bit pattern decodes, merges, sorts, and windows without complaint, and no guarantee of this section survives it. The operations remain deterministic on junk (no panic, no wrap error), but their results carry no semantics — see the §11.6 merge scoping for the one place this is load-bearing.

11.5 Ordering and equality

Contract. Unsigned u64 order over toc words is order by conservative encoded start — both variants place their start information in the high 32 bits (word ≫ 32 is t ≫ 31 for a timestamp and s for a range), so a raw unsigned sort needs no comparator and no decode. Within a tied start quantum the tie-breaks are, in order:

  1. ranges (flag 0) sort before timestamps (flag 1) — correct, since a range's floored start is ≤ any timestamp inside that quantum;
  2. among ranges: shorter first, by end code;
  3. among timestamps: exact ns order.

Equality is bit equality. The encoding is canonical — one word per (variant, field values) — so integer equality, hashing, and dedup work on the word directly. The timestamp encoding is injective in t_ns; the range encoding is injective in the envelope, not the real interval: distinct real intervals quantizing to the same codes share one word, and word equality asserts envelope equality only.

11.6 The merge law

Contract. The merge (toc_merge) is the semilattice join of two words:

  • bitwise-equal inputs return that word unchanged — required for idempotence, because merging two equal timestamps must NOT produce their range envelope;
  • any other pair takes each word's conservative codes — a range's (s, e) verbatim; a timestamp's merge envelope is s = t ≫ 31 (its high field) and e = (t ≫ 32) + 1 — and emits the range word (min(s_a, s_b) ≪ 32) | max(e_a, e_b). Two unequal valid words always merge to a range word.

The join is closed on the valid domain. For valid inputs the merged codes again satisfy §11.4's range condition — min s ≤ s_a ≤ 2e_a − 1 ≤ 2·max e − 1, with max e ≥ 1 — so the merged word is itself a valid word, and in particular is never 0 (§11.3). Closure is what lets a reduction re-merge its own intermediate results inside the stated scope below.

On the valid domain (§11.4) the join is exactly associative, commutative, and idempotent over the fixed epoch-anchored lattice, so a reduction over any multiset of valid words yields a bit-identical u64 under any fold tree — parallel, segmented, or sequential. The merged envelope contains every input instant and every input envelope (conservatism direction preserved, never narrowed). The join has no identity element: a reduction over zero words is an error, never a sentinel — toc_reduce refuses an empty array, and tocs_reduce refuses an empty group. An empty batch (no words and no groups) asks for no reduction at all and is accepted, returning no words.

Scope of the law (the [PR

192](https://github.com/espg/mortie/pull/192) finding, stated

normatively): the fold-tree bit-identity guarantee covers encoder-produced (valid) words only. Out-of-domain patterns are garbage in, garbage out — an invalid "timestamp" past TOC_MAX_NS can even merge to a word with the timestamp flag set, and past that point two different fold trees may disagree, each deterministically. Implementations are not required to detect junk; they are required not to panic on it.

11.7 Window predicates

Contract. Both predicates test a word's conservative encoded bounds against a half-open query window [q_start, q_end) in internal ns; a timestamp is treated as the one-ns envelope [t, t + 1). An inverted window is an error; an empty window (q_start == q_end) overlaps and contains nothing.

The edge slack is asymmetric, because §11.3 rounds the two ends onto different grids: a range word's envelope starts at most Q_START_NS − 1 = 2³¹ − 1 ns (~2.15 s) before the real interval's start (floored start; zero when that start sits on the 2³¹ ns grid) and ends between 1 ns and Q_END_NS = 2³² ns (~4.29 s) past the interval's last covered instant (strictly-greater ceiling; the full 2³² ns when the end sits on the 2³² ns grid). Timestamp words carry no slack. So the edge misreports below reach < 2³¹ ns on the leading side and ≤ 2³² ns on the trailing side — a reader budgeting "one quantum" symmetrically under-budgets the trailing edge by 2×.

  • overlaps (toc_overlaps): true iff the envelope intersects the window. It may over-report near window edges — a range whose envelope grazes the window without its real interval doing so — and only within the slack: a spurious hit requires real content ending later than q_start − 2³² ns (trailing slack reaching back into the window) or starting earlier than q_end + 2³¹ ns (leading slack reaching forward). It never under-reports: every word whose real time content intersects the window tests true.
  • contains (toc_contains): true iff the envelope fits inside the window. It never over-reports (the real interval lies inside the envelope, so envelope-in-window implies interval-in-window) and may under-report by the same slack: a word whose real interval starts less than 2³¹ ns after q_start, or ends less than 2³² ns before q_end, can test false though its real content fits.

The two conservatism directions are law, and are what external readers key selection semantics on (zagg §8.1's "conservative superset" clause cites them).

11.8 Conformance vectors

Contract (golden, test-pinned). The words below are normative byte values: an independent implementation of this grammar must reproduce every row exactly. Inputs are internal ns (§11.1); start / end are the toc2time decode (end exclusive for ranges, per §11.4); the UTC column renders the decoded start through the §11.1 boundary convention (to_datetime64). The two merge rows join the rows named in their inputs. The table is regenerated from the live kernels by mortie/tests/test_spec_toc.py and compared literally, so the page and the code cannot drift apart; the §11.6 algebraic laws are pinned at volume by the fixture tests in src_rust/src/toc.rs.

What is frozen, exactly. The word and ns columns and the decode they express are the normative, immutable part. The start (UTC) column is derived: it renders the decoded start through the §11.1 boundary convention under the leap table as of the 2017-01-01 step, the table mortie ships. §11.1's additive rule governs the column: when the table gains a row (the IERS announces each step roughly six months ahead via Bulletin C, and a future step may be positive or negative), renderings of instants after that step move (of the rows below, only the 2142 one is after any future step, and the §11.1 sentence rendering that same instant is scoped identically), while every word, ns, and decode value stays byte-for-byte fixed. The regeneration test is what surfaces such an append: it fails, and the UTC cell is deliberately re-rendered.

value inputs word (hex) word (decimal) start (ns) end (ns) start (UTC)
timestamp: epoch time2toc(0) 0x0000000080000000 2147483648 0 0 1850-01-01T00:00:00.000000000
timestamp: GPS epoch time2toc(4102790400000000000) 0x71E014439BE20000 8205580801679687680 4102790400000000000 4102790400000000000 1980-01-06T00:00:00.000000000
timestamp: 2018-01-01 UTC time2toc(5301590418000000000) 0x932610CAE9813400 10603180836377408512 5301590418000000000 5301590418000000000 2018-01-01T00:00:00.000000000
timestamp: last valid instant time2toc(9223372032559808511) 0xFFFFFFFDFFFFFFFF 18446744065119617023 9223372032559808511 9223372032559808511 2142-04-11T23:46:54.559808511
range: straddles the 2^32 grid span2toc(12384901888, 13384901888) 0x0000000500000004 21474836484 10737418240 17179869184 1850-01-01T00:00:10.737418240
range: end exactly on the 2^32 grid span2toc(29064771072, 30064771072) 0x0000000D00000008 55834574856 27917287424 34359738368 1850-01-01T00:00:27.917287424
merge: the two ranges above toc_merge(21474836484, 55834574856) 0x0000000500000008 21474836488 10737418240 34359738368 1850-01-01T00:00:10.737418240
merge: the two epoch timestamps above toc_merge(2147483648, 8205580801679687680) 0x0000000038F00A22 955255330 0 4102790401679687680 1850-01-01T00:00:00.000000000

Reading the rows against the layout (§11.2): the epoch instant is the flag bit alone; the 2018 timestamp's +18 s carries the §11.1 leap offset; the on-grid range's end code is 8, never 7 — the strictly-greater ceiling (§11.3); the range⊔range merge takes min of start codes and max of end codes; and the timestamp⊔timestamp merge is a range word (flag 0) whose envelope contains both instants, while §11.6's equal-input rule keeps a timestamp merged with itself a timestamp.