Add ShapeIndex encoding and decoding

golang/geo

Add C++-compatible ShapeIndex Encode/Decode so indices round-trip byte-for-byte without rebuilding.

Task instructions
`ShapeIndex` lacks serialization, forcing full rebuilds on every load. Add `Encode` to `io.Writer` and `Decode` from `io.Reader` on `ShapeIndex`, producing the byte stream format used by the C++ S2 library for `MutableS2ShapeIndex::Encode` (the s2coding "EncodedS2ShapeIndex" format), so that encoded indexes are byte-for-byte compatible with C++ S2. The stream layout is: <encoded shapes> <index header> <encoded cell ids> <encoded cells> Use the package's existing `encoder`/`decoder` primitives from `encode.go`: uvarint (LEB128, little-endian base-128) via `writeUvarint`/`readUvarint`, and little-endian fixed-width values via the existing `write*`/`read*` helpers. Points are three little-endian float64 values in X, Y, Z order. ## Building blocks **EncodedUintVector(E)** encodes an array of unsigned integers of wire width `E` bytes (E = 4 or 8) as: 1. a uvarint holding `size*E | (bytesPer-1)`, where `bytesPer` is the fewest bytes that can represent every element, computed as `(bit_width(OR of all elements | 1) - 1) / 8 + 1` (so an empty array has `bytesPer = 1`), and 2. `size` elements, each `bytesPer` bytes, little-endian, least significant byte first. **EncodedStringVector** encodes a vector of byte strings as: 1. an EncodedUintVector with E = 8 holding `size` end offsets: entry i holds the offset just past the end of entry i in the data blob below (the first entry starts at offset 0, which is not encoded; entries may be empty), and 2. the concatenated entry bytes. Decode of the EncodedStringVector must read exactly `offsets[last]` data bytes. **EncodedS2CellIdVector** encodes a sorted list of cell IDs as a shared high prefix plus fixed-width deltas: - Header byte: bits 0-2 = `base_len` (0-7), bits 3-7 = `shift_code`; if `shift_code == 31`, one extra byte follows holding `shift >> 1` (the shift is then odd). - `base_len` bytes: the `base_len` most significant bytes of the 64-bit base value (`base >> (64 - 8*max(1, base_len))`), written little-endian. `base_len = 0` writes no bytes; the decoded base is shifted left by `64 - 8*max(1, base_len)`. - An EncodedUintVector with E = 8 of deltas, where `cellID[i] = base + (delta_i << shift)`. The encoder computes the parameters from the cell IDs as follows. Let `vOr`, `vAnd`, `vMin`, `vMax` be the bitwise OR, bitwise AND, minimum, and maximum of the IDs. If `vOr == 0` (no cells), use `base = 0`, `base_len = 0`, `shift = 0` and an empty delta vector. Otherwise: 1. `shift = min(56, trailing_zeros(vOr) & ~1)`; then increment `shift` if bit `shift` of `vAnd` is set (all IDs at the same cell level). 2. For each candidate `base_len` from 0 to 7: let `t_base` be `vMin` with its low `8*base_len` bits cleared, `msb = max(0, bit_width((vMax - t_base) >> shift) - 1)`, and `cost = base_len + size * (msb/8 + 1)`. Keep the first candidate with strictly smallest cost. 3. If `shift` is odd and `msb % 8 != 7`, decrement `shift`. 4. Emit the header byte: `shift_code = shift >> 1`, or if the shift is odd, `shift_code = min(31, shift/2 + 29)` (writing the extra byte when `shift_code == 31`). 5. Emit the deltas `(cellID - base) >> shift` as an EncodedUintVector with E = 8. On decode: `shift = 2*shift_code` when `shift_code < 29`; when `shift_code` is 29 or more, `shift = 2*(shift_code - 29) + 1` and bit `shift-1` of `base` is forced to 1. Reject `shift_code > 56`. ## ShapeIndex sections, in order 1. **Shapes**: an EncodedStringVector with one entry per shape ID slot `0..nextID-1`; the number of entries is `nextID`, so shape IDs survive even when shapes have been removed. A present shape's entry is a uvarint type tag followed by the shape body (below). A removed or absent shape is an empty entry. 2. **Header**: a uvarint holding `(max_edges_per_cell << 2) | version`, with version currently 0 and `max_edges_per_cell >= 1`. `Decode` must restore the index's max-edges-per-cell setting from this field. 3. **Cell IDs**: the index's cell IDs, in increasing order, as an EncodedS2CellIdVector. 4. **Cells**: an EncodedStringVector with exactly one entry per cell ID (a count mismatch is an error), each entry encoding that `ShapeIndexCell` as described below. ## Shape bodies The index encoder uses the C++ "FAST" coding hint for all shape bodies; compressed body variants must be rejected on decode (except where noted for Polygon). - Tag 1, `*Polygon`: the existing lossless Polygon encoding already in this package (version byte 1, owns-loops byte, has-holes byte, uint32 loop count, each loop in the existing Loop encoding, then the bound). `Decode` must also accept the compressed Polygon encoding (version 4). - Tag 2, `*Polyline`: the existing Polyline encoding (version byte 1, uint32 vertex count, vertices). - Tag 3, `*PointVector`: EncodedS2PointVector in UNCOMPRESSED form: a uvarint header holding `(numPoints << 3) | format` with format 0, followed by the points. - Tag 4, `*LaxPolyline`: the same EncodedS2PointVector UNCOMPRESSED encoding (no version byte). - Tag 5, `*LaxPolygon`: version byte 1, uvarint `numLoops`, all vertices in EncodedS2PointVector UNCOMPRESSED form, then — only when `numLoops > 1` — the `numLoops+1` cumulative loop start offsets (monotonic, starting at 0 and ending at the vertex count) as an EncodedUintVector with E = 4. - Tag 6, `*Loop` (tag assigned by this port): the existing Loop encoding. - Tag 7, `*LaxLoop` (tag assigned by this port): EncodedS2PointVector UNCOMPRESSED form. `Loop.typeTag` and `LaxLoop.typeTag` change from `typeTagNone` to the new tags 6 and 7, which are added to the `typeTag` constants in `shape.go`. For reference, the empty-index stream (default options) is the 5 bytes `00 28 00 00 00`. ## ShapeIndexCell entries Each cell entry encodes its clipped shapes (edge IDs stored in increasing order). `num_shape_ids` is `nextID`. If `num_shape_ids == 1`, the cell has a single clipped shape and encodes one of: - contiguous run of `n` edges with `2 <= n <= 17` (i.e. `lastEdge == firstEdge + n - 1`): uvarint `edge0 << 6 | (n-2) << 2 | containsCenter << 1` (bit 0 is 0); - a single edge: uvarint `edge0 << 3 | containsCenter << 2 | 1` (bits 0-1 = 01); - otherwise: uvarint `n << 3 | containsCenter << 2 | 3` (bits 0-1 = 11), followed by the edge-run encoding below. Otherwise, if the cell contains more than one clipped shape, first write uvarint `numClipped << 3 | 3`; then for each clipped shape — with shape IDs delta-encoded against the previous shape ID plus one, starting from zero — one of: - contiguous run of `1 <= n <= 16` edges: uvarint `edge0 << 2 | containsCenter << 1` (bit 0 is 0), then uvarint `shapeDelta << 4 | (n-1)`; - no edges: uvarint `shapeDelta << 4 | containsCenter << 3 | 7` (bits 0-2 are 111); - otherwise: uvarint `(n-1) << 3 | containsCenter << 2 | 1` (bits 0-1 are 01), then uvarint `shapeDelta`, then the edge-run encoding. The edge-run encoding writes the edge IDs as uvarint deltas from the previous edge plus run length (previous starts at 0). For every edge except the last, the uvarint packs the length of its contiguous run (1-7) as `delta << 3 | (run-1)`, or, for runs of 8 or more, `(run-8) << 3 | 7` followed by a separate uvarint holding the delta. The final edge is written as a bare uvarint delta with no run length. `Decode` must restore the full spatial cell structure (cell IDs plus each cell's clipped shapes) so queries and iteration work without calling `Build`. A cell may reference a shape ID slot whose shape has been removed; this decodes without error. ## Behavioral requirements - `Encode` applies any pending updates first (like C++ `ForceBuild`), so an index that was never explicitly built still encodes its complete cell structure. - Shape IDs survive encoding so cell references stay valid, including IDs left idle by `Remove`. - Even an empty index encodes to a non-empty byte stream. - `Decode` consumes exactly one encoded index and ignores any trailing bytes in the stream. - Decoding malformed input must return errors rather than panicking, including truncated data, corrupted bytes, unknown type tags, unsupported point-vector formats, and oversized allocation requests (shape and cell counts are capped at 50,000,000; edges per clipped shape at 50,000,000; vertices at the package's existing limit; `max_edges_per_cell` must be at least 1). - Encoding an index containing a shape type without a supported encoding returns an error. - Encoding is deterministic: encoding a decoded index must reproduce the original bytes exactly. IMPORTANT: Please work on this in a new branch from main and commit everything when you are done.
Outcome
  1. Passed 88 steps$0.45135k tokens
  2. Passed 113 steps$0.57175k tokens