Perfect! Now I have all the information needed. Let me compile a comprehensive report on the binary format requirements.

## Summary: Traccar Geocoder Binary Index File Format

I've completed a thorough analysis of how the Rust server reads and uses the binary index files. Here are the critical findings for designing a custom patching tool:

---

### 1. **File Loading Mechanism**

**Method**: Memory-mapped files (mmap) using the `memmap2` crate
- Files are opened with `File::open()` and memory-mapped with `Mmap::map()`
- Safe operation: bounds checking is performed on reads
- No separate "unload" - files remain mapped for the server's lifetime

**Dependency Rules**:
- If `geo_cells.bin` exists, then `street_entries.bin`, `street_ways.bin`, and `street_nodes.bin` are **required**
- `addr_entries.bin`, `addr_points.bin`, `interp_entries.bin`, `interp_ways.bin`, `interp_nodes.bin` are **optional** (depend on index mode)
- `admin_cells.bin`, `admin_entries.bin`, `admin_polygons.bin`, `admin_vertices.bin`, `strings.bin` are **always required**

---

### 2. **Binary File Formats and Lookup Methods**

#### **geo_cells.bin** - Binary-searchable index
**Format**: Fixed-size entries, **sorted by cell_id (ascending)**
- Entry size: 20 bytes per entry
- Structure per entry:
  - Bytes 0-7: `uint64_t cell_id` (cell ID, little-endian)
  - Bytes 8-11: `uint32_t street_offset` (offset into street_entries.bin, little-endian)
  - Bytes 12-15: `uint32_t addr_offset` (offset into addr_entries.bin, little-endian)
  - Bytes 16-19: `uint32_t interp_offset` (offset into interp_entries.bin, little-endian)
- **Lookup**: Binary search (requires **strict ascending sort** by cell_id)
- **Validation**: Server divides file size by 20 to get entry count; no bounds checking on individual offsets

#### **street_entries.bin / addr_entries.bin / interp_entries.bin** - Variable-length entry lists
**Format**: Variable-length records referenced by offset from geo_cells.bin
- No fixed record size; records are variable-length
- Record structure:
  - Bytes 0-1: `uint16_t count` (number of IDs in this entry, little-endian)
  - Bytes 2+: `count * uint32_t` (array of record IDs, little-endian)
- **Offset reference**: Value from geo_cells.bin or admin_cells.bin points to the start of count field
- **Special value**: `0xFFFFFFFF` (NO_DATA) means "no data for this cell"
- **Lookup**: Read count, then iterate through IDs

#### **street_ways.bin** - Fixed-size array of WayHeader
**Format**: Tightly-packed array, indexed by ID from entries files
```
struct WayHeader {
  uint32_t node_offset;    // 4 bytes: offset into street_nodes.bin
  uint8_t node_count;      // 1 byte: number of nodes for this way
  uint32_t name_id;        // 4 bytes: offset into strings.bin
};  // Total: 9 bytes per entry, NO padding
```
- **Size**: File size divided by 9 = number of ways
- **Access**: `all_ways[id as usize]` where `id` comes from street_entries.bin
- **Critical**: No padding between entries; strictly 9 bytes per entry

#### **addr_points.bin** - Fixed-size array of AddrPoint
**Format**: Tightly-packed array, indexed by ID from entries files
```
struct AddrPoint {
  float lat;                  // 4 bytes: latitude
  float lng;                  // 4 bytes: longitude
  uint32_t housenumber_id;   // 4 bytes: offset into strings.bin
  uint32_t street_id;        // 4 bytes: offset into strings.bin
};  // Total: 16 bytes per entry
```
- **Size**: File size divided by 16 = number of address points
- **Access**: `all_points[id as usize]` where `id` comes from addr_entries.bin
- **Float format**: IEEE 754 single-precision (little-endian)

#### **interp_ways.bin** - Fixed-size array of InterpWay
**Format**: Tightly-packed array, indexed by ID from entries files
```
struct InterpWay {
  uint32_t node_offset;     // 4 bytes: offset into interp_nodes.bin
  uint8_t node_count;       // 1 byte: number of nodes for this way
  uint32_t street_id;       // 4 bytes: offset into strings.bin
  uint32_t start_number;    // 4 bytes: start house number
  uint32_t end_number;      // 4 bytes: end house number
  uint8_t interpolation;    // 1 byte: interpolation mode (1=odd, 2=even, else=all)
};  // Total: 18 bytes per entry (C++ may add 1 byte padding for alignment)
```
- **Size**: File size divided by 18 (or 19 if padded) = number of interpolation ways
- **Access**: `all_interps[id as usize]` where `id` comes from interp_entries.bin

#### **street_nodes.bin / interp_nodes.bin** - Fixed-size array of NodeCoord
**Format**: Tightly-packed array, indexed by offset from ways files
```
struct NodeCoord {
  float lat;   // 4 bytes: latitude (IEEE 754)
  float lng;   // 4 bytes: longitude (IEEE 754)
};  // Total: 8 bytes per entry
```
- **Access**: `all_nodes[offset..offset + count]` where offset/count come from WayHeader
- **Critical**: Slicing by offset and count; no validation that slice stays in bounds

#### **admin_cells.bin** - Binary-searchable index
**Format**: Fixed-size entries, **sorted by cell_id (ascending)**
- Entry size: 12 bytes per entry
- Structure per entry:
  - Bytes 0-7: `uint64_t cell_id` (cell ID, little-endian)
  - Bytes 8-11: `uint32_t offset` (offset into admin_entries.bin, little-endian)
- **Lookup**: Binary search (requires **strict ascending sort** by cell_id)
- **Special value**: `0xFFFFFFFF` (NO_DATA) means "no data for this cell"

#### **admin_entries.bin** - Variable-length entry lists
**Format**: Variable-length records referenced by offset from admin_cells.bin
- Record structure:
  - Bytes 0-1: `uint16_t count` (number of IDs in this entry, little-endian)
  - Bytes 2+: `count * uint32_t` (array of polygon IDs with flags, little-endian)
    - Bit 31 (0x80000000): Interior flag (skips point-in-polygon test if set)
    - Bits 0-30 (0x7FFFFFFF): Polygon ID for indexing into admin_polygons.bin
- **Lookup**: Read count, iterate through IDs (masking off flag bits)

#### **admin_polygons.bin** - Fixed-size array of AdminPolygon
**Format**: Tightly-packed array, indexed by ID from admin_entries.bin
```
struct AdminPolygon {
  uint32_t vertex_offset;   // 4 bytes: offset into admin_vertices.bin
  uint32_t vertex_count;    // 4 bytes: number of vertices for this polygon
  uint32_t name_id;         // 4 bytes: offset into strings.bin
  uint8_t admin_level;      // 1 byte: administrative level (2=country, 4=state, 6=county, 8=city, 11=postcode, etc.)
  float area;               // 4 bytes: polygon area (unused for priority)
  uint16_t country_code;    // 2 bytes: ISO 3166-1 alpha-2 code (e.g., 'US')
};  // Total: 19 bytes per entry (may have 1 byte padding for alignment)
```
- **Size**: File size divided by 19 (or 20 if padded) = number of polygons
- **Access**: `all_polygons[id as usize]` where `id` comes from admin_entries.bin (with ID_MASK applied)

#### **admin_vertices.bin** - Fixed-size array of NodeCoord
**Format**: Tightly-packed array, indexed by offset from polygons file
- Same structure as street_nodes.bin and interp_nodes.bin
- **Access**: `all_vertices[offset..offset + count]` where offset/count come from AdminPolygon

#### **strings.bin** - Null-terminated string pool
**Format**: Flat buffer of null-terminated strings, no length prefix
- Strings are concatenated directly without length prefixes
- String boundaries are found by searching for null byte (0x00)
- **Offset reference**: Values in ways, polygons, and points files contain byte offsets into this file
- **Access**: `&strings[offset as usize..]` then read until null terminator
- **String reading**: `std::str::from_utf8(&bytes[..end]).unwrap_or("")` (silent invalid UTF-8 fallback)

---

### 3. **Format Constraints for Patching**

**CRITICAL SORTING REQUIREMENTS:**
1. **geo_cells.bin**: MUST be sorted by cell_id (ascending). Binary search will fail silently if not sorted.
2. **admin_cells.bin**: MUST be sorted by cell_id (ascending). Binary search will fail silently if not sorted.

**CONTIGUITY & ALIGNMENT:**
- **Fixed-size arrays** (ways, points, nodes, polygons) are cast directly from mmap using pointer arithmetic
- Files MUST be contiguous and properly laid out in order
- **No validation**: The server does not check if record counts match file sizes. You can have a 100-byte file interpreted as varying numbers of 9, 16, 18, or 8-byte records.
- **Unaligned access**: The server uses direct pointer casting. Misaligned structs WILL cause undefined behavior on some architectures (though x86 tolerates it).

**BOUNDS CHECKING:**
- For entry lists (entries files): Server checks `offset + 2 > entries.len()` before reading count, and `data_start + id_count * 4 > entries.len()` before iterating.
- For string reads: Server checks `offset as usize < strings.len()` implicitly (Rust slicing), then searches for null.
- For node slices: Server does NOT validate `offset + count <= nodes.len()` - a slice out of bounds will panic at runtime.
- For record access: Server does NOT validate that IDs from entries files are valid indexes into ways/points/polygons arrays - invalid IDs will panic.

**OPTIONAL vs REQUIRED MODES:**
- Server loads files optionally and checks `if let Some(ref m) = self.file` before using
- If a file is missing (None), lookups that depend on it are skipped
- **However**: If geo_cells.bin exists, all related street files are required or the load fails
- No manifest file is read by the server (manifest.json is informational only for clients)

---

### 4. **Version Metadata**

**NO version metadata in binary files.** The builder generates `manifest.json` for informational purposes:
```json
{
  "version": 1,
  "regions": [
    {
      "name": "planet",
      "modes": {
        "full": {"size": ...}
      },
      "qualities": [...]
    }
  ]
}
```

**The server does NOT validate this file.** There is no magic number, version field, or checksum in any binary file. If you patch a file incorrectly, the server will either:
1. Perform incorrect lookups (if data is misaligned but still readable)
2. Panic due to bounds violations (if IDs point out of bounds)
3. Silently skip results (if binary search is corrupted and finds nothing)

---

### 5. **Key Insights for Patch Tool Design**

1. **Binary search files must be strictly sorted**: geo_cells.bin and admin_cells.bin require ascending cell_id order. If you patch these, you must re-sort them.

2. **Entry lists are relative, not absolute**: Offsets in geo_cells.bin and admin_cells.bin are relative to their respective entries files. If you patch an entries file, you must update ALL offsets that reference it.

3. **Fixed-size arrays use direct pointer casting**: Ways, points, nodes, and polygons files are read as contiguous arrays. You cannot add padding or change structure sizes without breaking compatibility.

4. **Strings are referenced by byte offset, not string ID**: String references are raw offsets into strings.bin. If you add/remove strings, all offsets change. You must rebuild strings.bin and remap all references.

5. **No bounds validation on IDs**: The server assumes IDs from entries files are valid array indexes. Patching must ensure IDs stay within bounds.

6. **Float precision matters**: lat/lng values in NodeCoord and AddrPoint are IEEE 754 single-precision floats. Precision loss during conversion to double should be acceptable.

7. **The server is lenient with errors**: It silently skips invalid entries rather than failing. A corrupted patch might cause incomplete results instead of an error.

8. **Partial index support**: You can omit address and interpolation data; the server will gracefully skip lookups that depend on missing files.

---

### File Paths in Codebase:
- **Server code**: `/root/traccar-geocoder/server/src/main.rs`
- **Builder code**: `/root/traccar-geocoder/builder/src/build_index.cpp`, `/root/traccar-geocoder/builder/src/cell_index.cpp`
- **Type definitions**: `/root/traccar-geocoder/builder/src/types.h`