cat > /tmp/detailed_struct.cpp << 'EOF'
#include <iostream>
#include <cstdint>
#include <iomanip>

#define offset_of(T, member) __builtin_offsetof(T, member)

struct WayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};

struct InterpWay {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t street_id;
    uint32_t start_number;
    uint32_t end_number;
    uint8_t interpolation;
};

struct AdminPolygon {
    uint32_t vertex_offset;
    uint32_t vertex_count;
    uint32_t name_id;
    uint8_t admin_level;
    float area;
    uint16_t country_code;
};

int main() {
    std::cout << std::hex << std::setfill('0');

    std::cout << "WayHeader offsets:" << std::endl;
    std::cout << "  node_offset @ 0x" << offset_of(WayHeader, node_offset) << std::endl;
    std::cout << "  node_count @ 0x" << offset_of(WayHeader, node_count) << std::endl;
    std::cout << "  name_id @ 0x" << offset_of(WayHeader, name_id) << std::endl;
    std::cout << "  sizeof = " << sizeof(WayHeader) << std::endl;

    std::cout << "InterpWay offsets:" << std::endl;
    std::cout << "  node_offset @ 0x" << offset_of(InterpWay, node_offset) << std::endl;
    std::cout << "  node_count @ 0x" << offset_of(InterpWay, node_count) << std::endl;
    std::cout << "  street_id @ 0x" << offset_of(InterpWay, street_id) << std::endl;
    std::cout << "  start_number @ 0x" << offset_of(InterpWay, start_number) << std::endl;
    std::cout << "  end_number @ 0x" << offset_of(InterpWay, end_number) << std::endl;
    std::cout << "  interpolation @ 0x" << offset_of(InterpWay, interpolation) << std::endl;
    std::cout << "  sizeof = " << sizeof(InterpWay) << std::endl;

    std::cout << "AdminPolygon offsets:" << std::endl;
    std::cout << "  vertex_offset @ 0x" << offset_of(AdminPolygon, vertex_offset) << std::endl;
    std::cout << "  vertex_count @ 0x" << offset_of(AdminPolygon, vertex_count) << std::endl;
    std::cout << "  name_id @ 0x" << offset_of(AdminPolygon, name_id) << std::endl;
    std::cout << "  admin_level @ 0x" << offset_of(AdminPolygon, admin_level) << std::endl;
    std::cout << "  area @ 0x" << offset_of(AdminPolygon, area) << std::endl;
    std::cout << "  country_code @ 0x" << offset_of(AdminPolygon, country_code) << std::endl;
    std::cout << "  sizeof = " << sizeof(AdminPolygon) << std::endl;

    return 0;
}
EOF
g++ /tmp/detailed_struct.cpp -o /tmp/detailed_struct && /tmp/detailed_struct


WayHeader offsets:
  node_offset @ 0x0
  node_count @ 0x4
  name_id @ 0x8
  sizeof = c
InterpWay offsets:
  node_offset @ 0x0
  node_count @ 0x4
  street_id @ 0x8
  start_number @ 0xc
  end_number @ 0x10
  interpolation @ 0x14
  sizeof = 18
AdminPolygon offsets:
  vertex_offset @ 0x0
  vertex_count @ 0x4
  name_id @ 0x8
  admin_level @ 0xc
  area @ 0x10
  country_code @ 0x14
  sizeof = 18


Now I have all the information needed. Let me create a comprehensive analysis document:

## Geocoder Index File Format Analysis

Based on a thorough examination of the builder code (`/root/traccar-geocoder/builder/src/cell_index.cpp`) and the server reader code (`/root/traccar-geocoder/server/src/main.rs`), here's a complete specification of each binary file format:

---

### Overview of File Organization

The geocoder system uses a **two-level indexing structure**:
1. **Cell Index Files** - Map S2 cell IDs to entry offsets (binary search lookup)
2. **Entry Files** - Variable-length arrays of IDs within each cell
3. **Data Files** - Fixed-size records indexed by ID

Data references strings via **32-bit byte offsets** into a **null-terminated string pool**.

---

### File Specifications

#### 1. **geo_cells.bin** - Merged Geographic Cell Index
**Purpose**: Maps S2 cells (level 17) to street, address, and interpolation data offsets

**Structure**: Fixed-size records, sorted by cell_id
- **Record size**: 20 bytes
- **Entry layout**:
  - `uint64_t cell_id` (8 bytes) - S2 cell ID at configured street level
  - `uint32_t street_offset` (4 bytes) - Offset into `street_entries.bin` (NO_DATA = 0xFFFFFFFF)
  - `uint32_t addr_offset` (4 bytes) - Offset into `addr_entries.bin`
  - `uint32_t interp_offset` (4 bytes) - Offset into `interp_entries.bin`

**Lookup method**: Binary search on cell_id (comments in server confirm 20 bytes/entry)

**Ordering**: Sorted by cell_id in ascending order

**Data references**: String offsets (name_id fields) from all streets/addresses

---

#### 2. **street_entries.bin** - Street IDs by Cell
**Purpose**: Lists street IDs contained in each geographic cell

**Structure**: Variable-length entries, referenced by byte offset from geo_cells.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of street IDs in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of street way IDs (indices into street_ways.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `street_ways.bin` via numeric ID
- No inter-cell ordering; each cell's list is independent

---

#### 3. **street_ways.bin** - Street Way Metadata
**Purpose**: Metadata for each street way (segment)

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 12 bytes (9 bytes data + 3 bytes padding for alignment)
- **Record layout**:
  - `uint32_t node_offset` - Starting index into `street_nodes.bin`
  - `uint8_t node_count` - Number of nodes in this way (max 255)
  - `uint32_t name_id` - String pool offset to street name (u32, read after uint8_t with padding)

**Record size**: 12 bytes (verified via sizeof, includes struct packing)

**Ordering**: Records are indexed sequentially by way ID

**Data references**:
- String offset (`name_id`) for street name lookup
- Node indices into `street_nodes.bin`

---

#### 4. **street_nodes.bin** - Street Coordinates
**Purpose**: Latitude/longitude points for street ways

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Nodes are grouped by way; way headers point to start index

**Data references**: None; pure geometry data

---

#### 5. **addr_entries.bin** - Address Point IDs by Cell
**Purpose**: Lists address point IDs contained in each geographic cell

**Structure**: Identical format to street_entries.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of address points in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of address point IDs (indices into addr_points.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `addr_points.bin` via numeric ID
- Referenced from `geo_cells.bin`

---

#### 6. **addr_points.bin** - Address Points
**Purpose**: Individual address locations with house numbers and streets

**Structure**: Fixed-size coordinate + metadata records, indexed sequentially
- **Record size**: 16 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)
  - `uint32_t housenumber_id` - String pool offset to house number
  - `uint32_t street_id` - String pool offset to street name

**Record size**: 16 bytes

**Ordering**: Records are indexed sequentially by address ID

**Data references**:
- Two string offsets (housenumber, street)
- No geometric hierarchy; only location coordinates

---

#### 7. **interp_entries.bin** - Interpolation Way IDs by Cell
**Purpose**: Lists interpolation ways (address ranges) in each cell

**Structure**: Identical format to street_entries.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of interpolation ways in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of interpolation way IDs (indices into interp_ways.bin)

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within each cell are deduplicated and sorted

**Data references**:
- Links to `interp_ways.bin` via numeric ID
- Referenced from `geo_cells.bin`

---

#### 8. **interp_ways.bin** - Interpolation Way Metadata
**Purpose**: Address ranges (start..end) mapped to street geometry

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 24 bytes (14 bytes data + 10 bytes padding/packing)
- **Record layout**:
  - `uint32_t node_offset` - Starting index into `interp_nodes.bin`
  - `uint8_t node_count` - Number of nodes defining the street line
  - `uint32_t street_id` - String pool offset to street name
  - `uint32_t start_number` - First house number in range
  - `uint32_t end_number` - Last house number in range
  - `uint8_t interpolation` - Interpolation type (1/2 = odd/even, 0 = all)

**Record size**: 24 bytes (struct packing inserts 2 bytes padding after node_count, 6 bytes after interpolation)

**Ordering**: Records indexed sequentially by interpolation way ID

**Data references**:
- String offset (`street_id`) for street name
- Node indices into `interp_nodes.bin`

---

#### 9. **interp_nodes.bin** - Interpolation Coordinates
**Purpose**: Latitude/longitude points defining interpolation way geometry

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Nodes grouped by interpolation way; way headers point to start index

**Data references**: None; pure geometry data

---

#### 10. **admin_cells.bin** - Admin Boundary Cell Index
**Purpose**: Maps S2 cells (level 10) to administrative boundary entries

**Structure**: Fixed-size records, sorted by cell_id
- **Record size**: 12 bytes
- **Entry layout**:
  - `uint64_t cell_id` (8 bytes) - S2 cell ID at configured admin level
  - `uint32_t entry_offset` (4 bytes) - Offset into `admin_entries.bin` (NO_DATA = 0xFFFFFFFF)

**Lookup method**: Binary search on cell_id

**Ordering**: Sorted by cell_id in ascending order

**Data references**: Offsets into admin_entries.bin

---

#### 11. **admin_entries.bin** - Admin Polygon IDs by Cell
**Purpose**: Lists administrative polygons that cover/contain each cell

**Structure**: Variable-length entries, referenced by byte offset from admin_cells.bin
- **Entry header** (2 bytes):
  - `uint16_t count` - Number of polygon IDs in this cell
- **Entry data** (variable, 4 bytes per ID):
  - `uint32_t[count]` - Array of polygon IDs with flags:
    - **Bit 31** (0x80000000): INTERIOR_FLAG - Set if cell is fully interior to polygon
    - **Bits 0-30** (0x7FFFFFFF): ID_MASK - Actual polygon index into admin_polygons.bin

**Record size**: Variable (2 + count*4 bytes)

**Ordering**: IDs within entry are sorted and deduplicated

**Data references**:
- Links to `admin_polygons.bin` via masked ID
- INTERIOR_FLAG optimization: interior cells skip point-in-polygon tests

---

#### 12. **admin_polygons.bin** - Administrative Boundaries
**Purpose**: Metadata for administrative boundary polygons

**Structure**: Fixed-size records, indexed sequentially (0-based)
- **Record size**: 24 bytes
- **Record layout**:
  - `uint32_t vertex_offset` - Starting index into `admin_vertices.bin`
  - `uint32_t vertex_count` - Number of vertices defining boundary
  - `uint32_t name_id` - String pool offset to administrative name
  - `uint8_t admin_level` - OSM admin level (2=country, 4=state, 6=county, 8=city, 11=postcode)
  - `float area` - Polygon area (for smallest-first selection)
  - `uint16_t country_code` - Encoded 2-letter country code (big-endian: (code[0]<<8) | code[1])

**Record size**: 24 bytes (16 bytes data + 8 bytes packing)

**Ordering**: Records indexed sequentially by polygon ID

**Data references**:
- String offset (`name_id`) for boundary name
- Vertex indices into `admin_vertices.bin`
- Country code encoded in uint16_t (ASCII bytes packed)

---

#### 13. **admin_vertices.bin** - Administrative Boundary Vertices
**Purpose**: Latitude/longitude vertices for polygon boundaries

**Structure**: Fixed-size coordinate records, indexed sequentially
- **Record size**: 8 bytes
- **Record layout**:
  - `float lat` (4 bytes)
  - `float lng` (4 bytes)

**Record size**: 8 bytes

**Ordering**: Vertices grouped by polygon; headers point to start index

**Data references**: None; pure geometry data

---

#### 14. **strings.bin** - String Pool
**Purpose**: Centralized storage for all variable-length strings

**Structure**: Null-terminated strings concatenated sequentially
- **Format**: Each string followed by `\0` terminator
- **Encoding**: UTF-8
- **References**: Identified by **byte offset** (uint32_t)

**Example layout**:
```
[Offset]  [Content]
0         "Main Street\0"
12        "New York\0"
21        "USA\0"
25        ...
```

**Key characteristics**:
- **Ordering**: No specific order; offsets are absolute byte positions
- **String types referenced**:
  - Street names (from ways, addresses, interpolation)
  - House numbers (from addresses)
  - Administrative names (from polygons)
- **Compaction**: When writing partial indexes (AdminOnly, NoAddresses), unused strings are removed and offsets remapped
- **Lookup method**: Direct pointer arithmetic; find null terminator by scanning

---

### Cross-File References and Dependencies

**Reference graph**:
```
geo_cells.bin
  ├─> street_entries.bin -> street_ways.bin -> street_nodes.bin
  │                      -> strings.bin (name_id)
  │
  ├─> addr_entries.bin -> addr_points.bin -> strings.bin (housenumber_id, street_id)
  │
  └─> interp_entries.bin -> interp_ways.bin -> interp_nodes.bin
                          -> strings.bin (street_id)

admin_cells.bin
  └─> admin_entries.bin -> admin_polygons.bin -> admin_vertices.bin
                                             -> strings.bin (name_id)
```

### Constants and Special Values

- **NO_DATA**: `0xFFFFFFFF` - Indicates no data for a cell
- **INTERIOR_FLAG**: `0x80000000` - Admin polygon ID flag (interior cell)
- **ID_MASK**: `0x7FFFFFFF` - Mask to extract polygon ID from flagged value
- **MAX_VERTEX_COUNT**: `0x7FFFFFFF` - Never actually reached; administrative limit

### Lookup Process Summary

**Geographic Query (street, address, interpolation)**:
1. Compute S2 cell ID at street level from (lat, lng)
2. Binary search in `geo_cells.bin` for cell_id (20 bytes/entry)
3. Extract three offsets (street, addr, interp)
4. For each offset, if not NO_DATA:
   - Seek to offset in corresponding `*_entries.bin`
   - Read uint16_t count
   - Read count × uint32_t IDs
   - For each ID, index into corresponding data file (ways, points, interps)
   - Dereference pointers to nodes/coordinates and strings

**Admin Query (country, state, city, postcode)**:
1. Compute S2 cell ID at admin level from (lat, lng)
2. Binary search in `admin_cells.bin` for cell_id (12 bytes/entry)
3. Extract entry offset
4. Read uint16_t count from admin_entries.bin at offset
5. Read count × uint32_t IDs (with INTERIOR_FLAG)
6. For each ID (masked), index into admin_polygons.bin
7. If INTERIOR_FLAG set, point is inside polygon; skip point-in-polygon test
8. Otherwise, perform ray-casting test using vertices from admin_vertices.bin
9. Select smallest-area polygon per admin level

---

### Design Implications for Incremental Updates

**Opportunities for Smart Updates**:

1. **Geo Data** (streets/addresses/interpolation):
   - `geo_cells.bin` references offsets in `*_entries.bin` - **MUST rebuild if cell contents change**
   - `street_ways.bin`, `addr_points.bin`, `interp_ways.bin` are **indexed by ID**:
     - Appending new records is safe if IDs are sequential
     - Updating existing records at same indices is safe (same size)
     - **Challenge**: Entry files store variable-length ID lists - hard to incrementally update
   - `*_nodes.bin` files are concatenated arrays - hard to incrementally update without remapping offsets

2. **Admin Data** (boundaries):
   - `admin_cells.bin` is a cell index - can rebuild independently
   - `admin_polygons.bin` is indexed by ID - can append safely
   - `admin_vertices.bin` is concatenated - requires offset remapping if changed

3. **Strings**:
   - String pool is offset-based - **any insertion requires remapping all offsets**
   - Could maintain separate string pools per category (streets, addresses, etc.)
   - String compaction happens at write time

4. **Binary Search Indexes** (geo_cells, admin_cells):
   - Must be kept sorted
   - Must cover all active cells
   - Can be completely rebuilt if cell set changes

**Challenges**:
- Entry files use variable-length encoding (1 uint16_t + N*uint32_t per cell)
- Node offsets are absolute, not relative - cascading updates required
- String offsets are global - adding/removing strings cascades to all data files
- No version markers or checksums for integrity validation

---

This specification should provide a complete foundation for designing an incremental update system. The key insight is that **fixed-size data files can be updated incrementally, but variable-length entry files and global string pools require more sophisticated coordination**.