Edit

// Encoding types for each section
enum class PatchEncoding : uint32_t {
    RAW_REPLACE = 0,  // Full replacement (zlib compressed)
    COPY_INSERT = 1,  // Single zstd delta frame (zstd --patch-from)
    TWO_STAGE = 2,    // Two zstd delta frames: old→remapped, remapped→new
};

// Encoding types for each section
enum class PatchEncoding : uint32_t {
    RAW_REPLACE = 0,  // Full replacement (zlib compressed)
    ZSTD_DELTA = 1,   // Single zstd delta frame (zstd --patch-from on remapped old)
};


The file /root/traccar-geocoder/builder/tools/patch_format.h has been updated successfully.

// Encoding types for each section
enum class PatchEncoding : uint32_t {
    RAW_REPLACE = 0,  // Full replacement (zlib compressed)
    ZSTD_DELTA = 1,   // Single zstd delta frame (zstd --patch-from on remapped old)
};

#pragma once
// Shared definitions for the geocoder patch system.
// Used by geocoder-canonicalize, geocoder-diff, and geocoder-patch.

#include <algorithm>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

// --- Binary record structs (must match types.h and server) ---

#pragma pack(push, 1)
struct PatchWayHeader {
    uint32_t node_offset;
    uint8_t node_count;
    uint32_t name_id;
};
static_assert(sizeof(PatchWayHeader) == 9, "WayHeader must be 9 bytes packed");

struct PatchAddrPoint {
    float lat;
    float lng;
    uint32_t housenumber_id;
    uint32_t street_id;
};
static_assert(sizeof(PatchAddrPoint) == 16, "AddrPoint must be 16 bytes");

struct PatchNodeCoord {
    float lat;
    float lng;
};
static_assert(sizeof(PatchNodeCoord) == 8, "NodeCoord must be 8 bytes");
#pragma pack(pop)

// InterpWay and AdminPolygon have compiler-dependent padding.
// Read them field-by-field instead of struct-casting.

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

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

// --- File I/O helpers ---

inline std::vector<char> read_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary | std::ios::ate);
    if (!f) return {};
    auto size = f.tellg();
    f.seekg(0);
    std::vector<char> data(size);
    f.read(data.data(), size);
    return data;
}

inline bool write_file(const std::string& path, const char* data, size_t size) {
    std::ofstream f(path, std::ios::binary);
    if (!f) return false;
    f.write(data, size);
    return f.good();
}

inline bool write_file(const std::string& path, const std::vector<char>& data) {
    return write_file(path, data.data(), data.size());
}

inline const char* get_string(const std::vector<char>& pool, uint32_t offset) {
    if (offset >= pool.size()) return "";
    return pool.data() + offset;
}

// Read WayHeader array — detect stride from file size vs known struct sizes
inline std::vector<PatchWayHeader> read_ways(const std::string& path) {
    auto data = read_file(path);
    // Determine stride: builder writes sizeof(WayHeader) which may be 9 or 12
    // depending on compiler padding. Try 12 first (common), then 9.
    size_t stride = 12;
    if (data.size() % 12 != 0 || data.size() / 12 == 0) {
        stride = 9;
    }
    size_t count = data.size() / stride;
    std::vector<PatchWayHeader> result(count);
    for (size_t i = 0; i < count; i++) {
        const char* p = data.data() + i * stride;
        memcpy(&result[i].node_offset, p, 4);
        result[i].node_count = static_cast<uint8_t>(p[4]);
        // name_id is at offset 5 (packed) or 8 (padded to 4-byte alignment)
        size_t name_off = (stride == 12) ? 8 : 5;
        memcpy(&result[i].name_id, p + name_off, 4);
    }
    return result;
}

inline size_t detect_way_stride(const std::string& path) {
    auto data = read_file(path);
    return (data.size() % 12 == 0 && data.size() / 12 > 0) ? 12 : 9;
}

template<typename T>
inline std::vector<T> read_structs(const std::string& path) {
    auto data = read_file(path);
    size_t count = data.size() / sizeof(T);
    std::vector<T> result(count);
    if (count > 0) memcpy(result.data(), data.data(), count * sizeof(T));
    return result;
}

// Read InterpWay with field-by-field parsing to handle padding
inline std::vector<PatchInterpWay> read_interps(const std::string& path) {
    auto data = read_file(path);
    // Determine record size from file: try sizeof(InterpWay) as compiled by the builder
    // The builder uses the same struct, so sizeof should match.
    size_t rec_size = sizeof(PatchInterpWay);
    // But the builder's InterpWay has padding. Read with the builder's size.
    // From types.h the struct is: u32 + u8 + u32 + u32 + u32 + u8 = 18 bytes raw
    // but compiler pads to 20 bytes typically. Check by trying both.
    size_t count_20 = data.size() / 20;
    size_t count_18 = data.size() / 18;
    // Use whichever divides evenly
    size_t stride = (data.size() % 20 == 0 && count_20 > 0) ? 20 :
                    (data.size() % 18 == 0 && count_18 > 0) ? 18 : 20;
    size_t count = data.size() / stride;
    std::vector<PatchInterpWay> result(count);
    for (size_t i = 0; i < count; i++) {
        const char* p = data.data() + i * stride;
        memcpy(&result[i].node_offset, p, 4); p += 4;
        result[i].node_count = *reinterpret_cast<const uint8_t*>(p); p += 1;
        // Skip padding to align street_id
        if (stride == 20) p += 3; // 3 bytes padding after node_count
        memcpy(&result[i].street_id, p, 4); p += 4;
        memcpy(&result[i].start_number, p, 4); p += 4;
        memcpy(&result[i].end_number, p, 4); p += 4;
        result[i].interpolation = *reinterpret_cast<const uint8_t*>(p);
    }
    return result;
}

// Read AdminPolygon with field-by-field parsing
inline std::vector<PatchAdminPolygon> read_admin_polygons(const std::string& path) {
    auto data = read_file(path);
    // AdminPolygon: u32 + u32 + u32 + u8 + f32 + u16 = 19 raw bytes
    // Compiler likely pads to 20 or 24 bytes. Try common sizes.
    size_t stride = 0;
    for (size_t s : {24, 20, 19}) {
        if (data.size() % s == 0 && data.size() / s > 0) { stride = s; break; }
    }
    if (stride == 0) stride = 24; // fallback
    size_t count = data.size() / stride;
    std::vector<PatchAdminPolygon> result(count);
    for (size_t i = 0; i < count; i++) {
        const char* p = data.data() + i * stride;
        memcpy(&result[i].vertex_offset, p, 4); p += 4;
        memcpy(&result[i].vertex_count, p, 4); p += 4;
        memcpy(&result[i].name_id, p, 4); p += 4;
        result[i].admin_level = *reinterpret_cast<const uint8_t*>(p); p += 1;
        // padding for area alignment
        if (stride >= 20) {
            size_t skip = (stride == 24) ? 3 : (stride == 20 ? 3 : 0);
            p += skip;
        }
        memcpy(&result[i].area, p, 4); p += 4;
        memcpy(&result[i].country_code, p, 2);
    }
    return result;
}

// --- Patch file format ---

static constexpr char GCPATCH_MAGIC[8] = {'G','C','P','A','T','C','H','\0'};
static constexpr uint32_t GCPATCH_VERSION = 1;

enum class PatchFileId : uint32_t {
    STRINGS = 0,
    STREET_WAYS = 1,
    STREET_NODES = 2,
    ADDR_POINTS = 3,
    INTERP_WAYS = 4,
    INTERP_NODES = 5,
    ADMIN_POLYGONS = 6,
    ADMIN_VERTICES = 7,
    GEO_CELLS = 8,
    STREET_ENTRIES = 9,
    ADDR_ENTRIES = 10,
    INTERP_ENTRIES = 11,
    ADMIN_CELLS = 12,
    ADMIN_ENTRIES = 13,
    COUNT = 14
};

static const char* patch_file_names[] = {
    "strings.bin", "street_ways.bin", "street_nodes.bin", "addr_points.bin",
    "interp_ways.bin", "interp_nodes.bin", "admin_polygons.bin", "admin_vertices.bin",
    "geo_cells.bin", "street_entries.bin", "addr_entries.bin", "interp_entries.bin",
    "admin_cells.bin", "admin_entries.bin"
};

// Encoding types for each section
enum class PatchEncoding : uint32_t {
    RAW_REPLACE = 0,  // Full replacement (zlib compressed)
    COPY_INSERT = 1,  // Single zstd delta frame (zstd --patch-from)
    TWO_STAGE = 2,    // Two zstd delta frames: old→remapped, remapped→new
};

// Patch file header
struct PatchHeader {
    char magic[8];
    uint32_t version;
    uint32_t flags;
    // Per-file sections follow
};

// Section header (one per file)
struct PatchSection {
    uint32_t file_id;       // PatchFileId
    uint32_t encoding;      // PatchEncoding
    uint64_t old_size;      // Expected size of old file
    uint64_t new_size;      // Size of reconstructed file
    uint64_t data_size;     // Size of compressed section data
};

// COPY_INSERT opcodes
enum : uint8_t {
    OP_COPY = 0,    // Copy from old file: [u8 tag][u64 offset][u32 length]
    OP_INSERT = 1,  // Insert new data:    [u8 tag][u32 length][data...]
};

// Offset fixup section marker: 0xFFFFFFFD
// Format: uint32_t marker, uint32_t file_id, uint32_t stride,
//         uint32_t count, [(uint32_t record_index, uint32_t new_offset_value)] * count
// Applied to byte offset 0 of each record (node_offset/vertex_offset field).

static constexpr uint32_t FIXUP_MARKER = 0xFFFFFFFD;

// --- Grid coordinate for fingerprinting ---

inline int to_grid(float v) {
    return (int)(v * 1e5f + (v >= 0 ? 0.5f : -0.5f));
}

// --- Directory creation ---
inline void ensure_dir(const std::string& path) {
    std::string cmd = "mkdir -p '" + path + "'";
    system(cmd.c_str());
}

-    COPY_INSERT = 1,  // Single zstd delta frame (zstd --patch-from)

-    TWO_STAGE = 2,    // Two zstd delta frames: old→remapped, remapped→new

+    ZSTD_DELTA = 1,   // Single zstd delta frame (zstd --patch-from on remapped old)


Now rewrite the diff tool to write: string remap + fixup tables + single-stage deltas (against remapped old). And rewrite the patch tool to apply remap + fixups + zstd delta.


    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");

    // Write offset fixup tables for files where node_offset/vertex_offset was fixed
    // Format: FIXUP_MARKER, file_id, stride, count, [(record_idx, new_offset_value)]
    auto write_fixups = [&](PatchFileId id, const std::string& name,
                             const std::vector<uint32_t>& id_remap,
                             const std::vector<char>& new_data, size_t stride) {
        uint32_t marker = FIXUP_MARKER;
        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t st = static_cast<uint32_t>(stride);
        std::vector<std::pair<uint32_t, uint32_t>> fixups;
        for (uint32_t i = 0; i < id_remap.size(); i++) {
            if (id_remap[i] != 0xFFFFFFFF) {
                uint32_t new_offset_val;
                memcpy(&new_offset_val, new_data.data() + id_remap[i] * stride, 4);
                fixups.push_back({i, new_offset_val});
            }
        }
        write_val(pf, &marker, 4);
        write_val(pf, &fid, 4);
        write_val(pf, &st, 4);
        uint32_t count = static_cast<uint32_t>(fixups.size());
        write_val(pf, &count, 4);
        for (auto& [idx, val] : fixups) { write_val(pf, &idx, 4); write_val(pf, &val, 4); }
        std::cerr << "  fixup " << name << ": " << count << " entries (" << count * 8 << " bytes)" << std::endl;
    };

    // Write fixups for ways, interps, admin (node_offset/vertex_offset at byte 0)
    write_fixups(PatchFileId::STREET_WAYS, "street_ways.bin", way_id_remap,
                 read_file(new_dir + "/street_ways.bin"), way_stride);
    write_fixups(PatchFileId::INTERP_WAYS, "interp_ways.bin", interp_id_remap,
                 read_file(new_dir + "/interp_ways.bin"), interp_stride);
    write_fixups(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin", admin_id_remap,
                 read_file(new_dir + "/admin_polygons.bin"), admin_stride);

    // Single-stage diff: remapped_old → new (using zstd --patch-from on temp files)
    auto diff_file = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string ref = tmpdir + "/" + name;
        std::string tgt = new_dir + "/" + name;
        // If no temp file exists, use original old
        if (read_file(ref).empty()) ref = old_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + ref + "' '" + tgt + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(tgt);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::ZSTD_DELTA);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    diff_file(PatchFileId::STRINGS, "strings.bin");
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");


<tool_use_error>String to replace not found in file.
String:     // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");</tool_use_error>

Error: String to replace not found in file.
String:     // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id);
        uint32_t enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");


The file keeps having whitespace differences. Let me just rewrite the full file — it's cleaner at this point:


   570→        write_val(pf, &count, 4);
   571→        for (auto& [o, n] : entries) { write_val(pf, &o, 4); write_val(pf, &n, 4); }
   572→        std::cerr << "  Remap: " << count << " entries" << std::endl;
   573→    }
   574→

        write_val(pf, &count, 4);
        for (auto& [o, n] : entries) { write_val(pf, &o, 4); write_val(pf, &n, 4); }
        std::cerr << "  Remap: " << count << " entries" << std::endl;
    }


575:    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
576:    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
604:    // Single-stage diff for files without remapping
605:    auto diff_single = [&](PatchFileId id, const std::string& name) {
626:    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
627:    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
628:    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
629:    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
630:    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
631:    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
632:    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
633:    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
634:    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
635:    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
636:    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
637:    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
638:    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
640:    diff_single(PatchFileId::STRINGS, "strings.bin");


   575→    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
   576→    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {

    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {


There are likely tabs or special whitespace. Let me use a different old_string anchor:


    // Write offset fixup tables for ways/interps/admin (node_offset/vertex_offset)
    {
        auto write_fixups = [&](PatchFileId id, const std::string& name,
                                 const std::vector<uint32_t>& id_remap, size_t stride) {
            auto new_data = read_file(new_dir + "/" + name);
            uint32_t marker = FIXUP_MARKER, fid = static_cast<uint32_t>(id);
            uint32_t st = static_cast<uint32_t>(stride);
            std::vector<std::pair<uint32_t, uint32_t>> fixups;
            for (uint32_t i = 0; i < id_remap.size(); i++) {
                if (id_remap[i] != 0xFFFFFFFF) {
                    uint32_t new_val;
                    memcpy(&new_val, new_data.data() + id_remap[i] * stride, 4);
                    fixups.push_back({i, new_val});
                }
            }
            write_val(pf, &marker, 4); write_val(pf, &fid, 4);
            write_val(pf, &st, 4);
            uint32_t count = static_cast<uint32_t>(fixups.size());
            write_val(pf, &count, 4);
            for (auto& [idx, val] : fixups) { write_val(pf, &idx, 4); write_val(pf, &val, 4); }
            std::cerr << "  fixup " << name << ": " << count << " entries" << std::endl;
        };
        write_fixups(PatchFileId::STREET_WAYS, "street_ways.bin", way_id_remap, way_stride);
        write_fixups(PatchFileId::INTERP_WAYS, "interp_ways.bin", interp_id_remap, interp_stride);
        write_fixups(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin", admin_id_remap, admin_stride);
    }

    // Single-stage diffs: remapped_old → new (zstd --patch-from on temp files)
    auto diff_file = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string ref = tmpdir + "/" + name;
        if (read_file(ref).empty()) ref = old_dir + "/" + name;
        std::string tgt = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + ref + "' '" + tgt + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(tgt);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::ZSTD_DELTA);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    diff_file(PatchFileId::STRINGS, "strings.bin");
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");

    // Remove the old two-stage diff


The file /root/traccar-geocoder/builder/tools/geocoder_diff.cpp has been updated successfully.

    // Write offset fixup tables for ways/interps/admin (node_offset/vertex_offset)
    {
        auto write_fixups = [&](PatchFileId id, const std::string& name,
                                 const std::vector<uint32_t>& id_remap, size_t stride) {
            auto new_data = read_file(new_dir + "/" + name);
            uint32_t marker = FIXUP_MARKER, fid = static_cast<uint32_t>(id);
            uint32_t st = static_cast<uint32_t>(stride);
            std::vector<std::pair<uint32_t, uint32_t>> fixups;
            for (uint32_t i = 0; i < id_remap.size(); i++) {
                if (id_remap[i] != 0xFFFFFFFF) {
                    uint32_t new_val;
                    memcpy(&new_val, new_data.data() + id_remap[i] * stride, 4);
                    fixups.push_back({i, new_val});
                }
            }
            write_val(pf, &marker, 4); write_val(pf, &fid, 4);
            write_val(pf, &st, 4);
            uint32_t count = static_cast<uint32_t>(fixups.size());
            write_val(pf, &count, 4);
            for (auto& [idx, val] : fixups) { write_val(pf, &idx, 4); write_val(pf, &val, 4); }
            std::cerr << "  fixup " << name << ": " << count << " entries" << std::endl;
        };
        write_fixups(PatchFileId::STREET_WAYS, "street_ways.bin", way_id_remap, way_stride);
        write_fixups(PatchFileId::INTERP_WAYS, "interp_ways.bin", interp_id_remap, interp_stride);
        write_fixups(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin", admin_id_remap, admin_stride);
    }

    // Single-stage diffs: remapped_old → new (zstd --patch-from on temp files)
    auto diff_file = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string ref = tmpdir + "/" + name;
        if (read_file(ref).empty()) ref = old_dir + "/" + name;
        std::string tgt = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + ref + "' '" + tgt + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_dir + "/" + name);
        auto new_data = read_file(tgt);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::ZSTD_DELTA);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    diff_file(PatchFileId::STRINGS, "strings.bin");
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");

    // Remove the old two-stage diff

// geocoder-diff: Compare two deterministic geocoder builds, produce a .gcpatch.
//
// Three-level remapping for minimal patches:
// 1. String remap: old string offsets → new string offsets
// 2. Record ID remap: match records by content, map old_id → new_id
// 3. Entry remap: apply record ID remap to entry files + rebuild cell indexes
//
// Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>

#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <string>
#include <unistd.h>
#include <unordered_map>
#include <vector>

#include "patch_format.h"

// --- String remap ---

static std::unordered_map<uint32_t, uint32_t> build_string_remap(
    const std::vector<char>& old_pool, const std::vector<char>& new_pool)
{
    std::unordered_map<std::string, uint32_t> new_idx;
    size_t pos = 0;
    while (pos < new_pool.size()) {
        const char* s = new_pool.data() + pos;
        size_t len = strlen(s);
        new_idx[std::string(s, len)] = static_cast<uint32_t>(pos);
        pos += len + 1;
    }
    std::unordered_map<uint32_t, uint32_t> remap;
    pos = 0;
    while (pos < old_pool.size()) {
        const char* s = old_pool.data() + pos;
        size_t len = strlen(s);
        auto it = new_idx.find(std::string(s, len));
        if (it != new_idx.end())
            remap[static_cast<uint32_t>(pos)] = it->second;
        pos += len + 1;
    }
    return remap;
}

// --- Remap helpers ---

static void remap_addr_points(std::vector<char>& data, const std::unordered_map<uint32_t,uint32_t>& rm) {
    for (size_t i = 0; i + 16 <= data.size(); i += 16)
        for (size_t off : {8, 12}) {
            uint32_t v; memcpy(&v, data.data() + i + off, 4);
            auto it = rm.find(v);
            if (it != rm.end()) memcpy(data.data() + i + off, &it->second, 4);
        }
}

static void remap_field(std::vector<char>& data, size_t stride, size_t field_off,
                         const std::unordered_map<uint32_t,uint32_t>& rm) {
    for (size_t i = 0; i + stride <= data.size(); i += stride) {
        uint32_t v; memcpy(&v, data.data() + i + field_off, 4);
        auto it = rm.find(v);
        if (it != rm.end()) memcpy(data.data() + i + field_off, &it->second, 4);
    }
}

// --- Content-based record matching ---
// Returns old_id → new_id mapping

static uint64_t fnv_mix(uint64_t h, uint64_t v) { h ^= v; h *= 1099511628211ULL; return h; }

static std::vector<uint32_t> match_addr_points(
    const std::vector<char>& old_data, const std::vector<char>& new_data)
{
    // After string remap, matching records are byte-identical
    size_t old_n = old_data.size() / 16, new_n = new_data.size() / 16;
    // Hash each new record
    std::unordered_multimap<uint64_t, uint32_t> new_map;
    new_map.reserve(new_n);
    for (uint32_t i = 0; i < new_n; i++) {
        uint64_t h = 14695981039346656037ULL;
        const char* p = new_data.data() + i * 16;
        for (int j = 0; j < 16; j++) h = fnv_mix(h, (uint8_t)p[j]);
        new_map.emplace(h, i);
    }
    std::vector<uint32_t> result(old_n, 0xFFFFFFFF);
    size_t matched = 0;
    for (uint32_t i = 0; i < old_n; i++) {
        uint64_t h = 14695981039346656037ULL;
        const char* p = old_data.data() + i * 16;
        for (int j = 0; j < 16; j++) h = fnv_mix(h, (uint8_t)p[j]);
        auto it = new_map.find(h);
        if (it != new_map.end() &&
            memcmp(old_data.data() + i * 16, new_data.data() + it->second * 16, 16) == 0) {
            result[i] = it->second;
            new_map.erase(it);
            matched++;
        }
    }
    std::cerr << "    addr matched=" << matched << "/" << old_n << " new=" << new_map.size() << std::endl;
    return result;
}

static std::vector<uint32_t> match_ways(
    const std::vector<char>& old_ways, const std::vector<char>& old_nodes,
    const std::vector<char>& new_ways, const std::vector<char>& new_nodes,
    size_t stride)
{
    size_t name_off = (stride == 12) ? 8 : 5;
    size_t old_n = old_ways.size() / stride, new_n = new_ways.size() / stride;

    size_t old_node_count = old_nodes.size() / 8, new_node_count = new_nodes.size() / 8;
    auto way_hash = [&](const char* w, const char* nodes, size_t max_nodes) -> uint64_t {
        uint32_t node_offset, name_id; uint8_t node_count;
        memcpy(&node_offset, w, 4); node_count = (uint8_t)w[4];
        memcpy(&name_id, w + name_off, 4);
        uint64_t h = 14695981039346656037ULL;
        h = fnv_mix(h, name_id); h = fnv_mix(h, node_count);
        for (uint8_t j = 0; j < node_count && (node_offset + j) < max_nodes; j++) {
            float lat, lng;
            memcpy(&lat, nodes + (node_offset + j) * 8, 4);
            memcpy(&lng, nodes + (node_offset + j) * 8 + 4, 4);
            h = fnv_mix(h, to_grid(lat)); h = fnv_mix(h, to_grid(lng));
        }
        return h;
    };

    std::unordered_multimap<uint64_t, uint32_t> new_map;
    new_map.reserve(new_n);
    for (uint32_t i = 0; i < new_n; i++)
        new_map.emplace(way_hash(new_ways.data() + i * stride, new_nodes.data(), new_node_count), i);

    std::vector<uint32_t> result(old_n, 0xFFFFFFFF);
    size_t matched = 0;
    for (uint32_t i = 0; i < old_n; i++) {
        uint64_t h = way_hash(old_ways.data() + i * stride, old_nodes.data(), old_node_count);
        auto it = new_map.find(h);
        if (it != new_map.end()) {
            result[i] = it->second;
            // Also fix node_offset in old to match new (makes way bytes identical)
            uint32_t new_node_off;
            memcpy(&new_node_off, new_ways.data() + it->second * stride, 4);
            memcpy(const_cast<char*>(old_ways.data()) + i * stride, &new_node_off, 4);
            new_map.erase(it);
            matched++;
        }
    }
    std::cerr << "    ways matched=" << matched << "/" << old_n << " new=" << new_map.size() << std::endl;
    return result;
}

static std::vector<uint32_t> match_admin(
    const std::vector<char>& old_polys, const std::vector<char>& old_verts,
    const std::vector<char>& new_polys, const std::vector<char>& new_verts,
    size_t stride)
{
    size_t old_n = old_polys.size() / stride, new_n = new_polys.size() / stride;

    size_t old_vc = old_verts.size() / 8, new_vc = new_verts.size() / 8;
    auto poly_hash = [&](const char* p, const char* verts, size_t max_v) -> uint64_t {
        uint32_t vert_offset, vert_count, name_id;
        memcpy(&vert_offset, p, 4); memcpy(&vert_count, p + 4, 4);
        memcpy(&name_id, p + 8, 4);
        uint8_t level = (uint8_t)p[12];
        uint16_t cc; memcpy(&cc, p + stride - 2, 2);
        uint64_t h = 14695981039346656037ULL;
        h = fnv_mix(h, name_id); h = fnv_mix(h, level); h = fnv_mix(h, cc);
        h = fnv_mix(h, vert_count);
        uint32_t n = std::min(vert_count, 10u);
        for (uint32_t j = 0; j < n && (vert_offset + j) < max_v; j++) {
            float lat, lng;
            memcpy(&lat, verts + (vert_offset + j) * 8, 4);
            memcpy(&lng, verts + (vert_offset + j) * 8 + 4, 4);
            h = fnv_mix(h, to_grid(lat)); h = fnv_mix(h, to_grid(lng));
        }
        return h;
    };

    std::unordered_multimap<uint64_t, uint32_t> new_map;
    new_map.reserve(new_n);
    for (uint32_t i = 0; i < new_n; i++)
        new_map.emplace(poly_hash(new_polys.data() + i * stride, new_verts.data(), new_vc), i);

    std::vector<uint32_t> result(old_n, 0xFFFFFFFF);
    size_t matched = 0;
    for (uint32_t i = 0; i < old_n; i++) {
        uint64_t h = poly_hash(old_polys.data() + i * stride, old_verts.data(), old_vc);
        auto it = new_map.find(h);
        if (it != new_map.end()) {
            result[i] = it->second;
            uint32_t new_vert_off;
            memcpy(&new_vert_off, new_polys.data() + it->second * stride, 4);
            memcpy(const_cast<char*>(old_polys.data()) + i * stride, &new_vert_off, 4);
            new_map.erase(it);
            matched++;
        }
    }
    std::cerr << "    admin matched=" << matched << "/" << old_n << " new=" << new_map.size() << std::endl;
    return result;
}

// Match interp ways by content
static std::vector<uint32_t> match_interps(
    const std::vector<char>& old_data, const std::vector<char>& old_nodes,
    const std::vector<char>& new_data, const std::vector<char>& new_nodes,
    size_t stride)
{
    size_t street_off = (stride >= 20) ? 8 : 5;
    size_t old_n = old_data.size() / stride, new_n = new_data.size() / stride;

    size_t old_nc = old_nodes.size() / 8, new_nc = new_nodes.size() / 8;
    auto ihash = [&](const char* p, const char* nodes, size_t max_n) -> uint64_t {
        uint32_t node_offset, street_id, start, end;
        uint8_t count, itype;
        memcpy(&node_offset, p, 4); count = (uint8_t)p[4];
        memcpy(&street_id, p + street_off, 4);
        memcpy(&start, p + street_off + 4, 4);
        memcpy(&end, p + street_off + 8, 4);
        itype = (street_off + 12 < stride) ? (uint8_t)p[street_off + 12] : 0;
        uint64_t h = 14695981039346656037ULL;
        h = fnv_mix(h, street_id); h = fnv_mix(h, start);
        h = fnv_mix(h, end); h = fnv_mix(h, itype); h = fnv_mix(h, count);
        for (uint8_t j = 0; j < count && (node_offset + j) < max_n; j++) {
            float lat, lng;
            memcpy(&lat, nodes + (node_offset + j) * 8, 4);
            memcpy(&lng, nodes + (node_offset + j) * 8 + 4, 4);
            h = fnv_mix(h, to_grid(lat)); h = fnv_mix(h, to_grid(lng));
        }
        return h;
    };

    std::unordered_multimap<uint64_t, uint32_t> new_map;
    new_map.reserve(new_n);
    for (uint32_t i = 0; i < new_n; i++)
        new_map.emplace(ihash(new_data.data() + i * stride, new_nodes.data(), new_nc), i);

    std::vector<uint32_t> result(old_n, 0xFFFFFFFF);
    size_t matched = 0;
    for (uint32_t i = 0; i < old_n; i++) {
        uint64_t h = ihash(old_data.data() + i * stride, old_nodes.data(), old_nc);
        auto it = new_map.find(h);
        if (it != new_map.end()) {
            result[i] = it->second;
            uint32_t new_node_off;
            memcpy(&new_node_off, new_data.data() + it->second * stride, 4);
            memcpy(const_cast<char*>(old_data.data()) + i * stride, &new_node_off, 4);
            new_map.erase(it);
            matched++;
        }
    }
    std::cerr << "    interps matched=" << matched << "/" << old_n << " new=" << new_map.size() << std::endl;
    return result;
}

// --- Entry file remapping ---
// Parse geo_cells + entries, remap IDs, rebuild entry + cell files.

static void remap_and_rebuild_entries(
    const std::string& old_dir, const std::string& new_dir, const std::string& tmpdir,
    const std::vector<uint32_t>& way_remap,
    const std::vector<uint32_t>& addr_remap,
    const std::vector<uint32_t>& interp_remap,
    const std::vector<uint32_t>& admin_remap)
{
    // For geo entries: parse old entries, remap IDs, then copy NEW entries/cells
    // (since the new files have the correct structure for the new build).
    // We write the remapped old entries + new cells to temp for zstd diffing.

    // Simpler approach: just copy new entry/cell files to temp dir.
    // The zstd diff of old vs new entry files won't benefit from ID remapping
    // unless we actually remap the old entries. Let's do the full remap.

    auto parse_entries = [](const std::vector<char>& cells_data, size_t cell_stride, size_t off_pos,
                            const std::vector<char>& entries_data)
        -> std::unordered_map<uint64_t, std::vector<uint32_t>>
    {
        std::unordered_map<uint64_t, std::vector<uint32_t>> result;
        size_t n = cells_data.size() / cell_stride;
        for (size_t i = 0; i < n; i++) {
            uint64_t cell_id; uint32_t off;
            memcpy(&cell_id, cells_data.data() + i * cell_stride, 8);
            memcpy(&off, cells_data.data() + i * cell_stride + off_pos, 4);
            if (off == 0xFFFFFFFF || off + 2 > entries_data.size()) continue;
            uint16_t count;
            memcpy(&count, entries_data.data() + off, 2);
            if (off + 2 + count * 4 > entries_data.size()) continue;
            std::vector<uint32_t> ids(count);
            memcpy(ids.data(), entries_data.data() + off + 2, count * 4);
            result[cell_id] = std::move(ids);
        }
        return result;
    };

    auto remap_ids = [](std::unordered_map<uint64_t, std::vector<uint32_t>>& cells,
                         const std::vector<uint32_t>& id_remap, bool has_flags = false) {
        for (auto& [cid, ids] : cells) {
            for (auto& id : ids) {
                uint32_t flags = has_flags ? (id & 0x80000000u) : 0;
                uint32_t masked = id & 0x7FFFFFFFu;
                if (masked < id_remap.size() && id_remap[masked] != 0xFFFFFFFF)
                    id = id_remap[masked] | flags;
            }
            std::sort(ids.begin(), ids.end());
        }
    };

    auto write_entries_cells = [](const std::string& cells_path, const std::string& entries_path,
                                   const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                                   size_t cell_stride)
    {
        std::vector<uint64_t> sorted_cells;
        for (auto& [id, _] : cell_map) sorted_cells.push_back(id);
        std::sort(sorted_cells.begin(), sorted_cells.end());

        std::vector<char> entries_buf;
        std::unordered_map<uint64_t, uint32_t> offsets;
        for (uint64_t cid : sorted_cells) {
            auto it = cell_map.find(cid);
            if (it == cell_map.end() || it->second.empty()) continue;
            offsets[cid] = static_cast<uint32_t>(entries_buf.size());
            uint16_t count = static_cast<uint16_t>(it->second.size());
            entries_buf.insert(entries_buf.end(), (char*)&count, (char*)&count + 2);
            entries_buf.insert(entries_buf.end(), (char*)it->second.data(),
                              (char*)it->second.data() + it->second.size() * 4);
        }
        write_file(entries_path, entries_buf);

        uint32_t no_data = 0xFFFFFFFF;
        std::vector<char> cells_buf;
        for (uint64_t cid : sorted_cells) {
            cells_buf.insert(cells_buf.end(), (char*)&cid, (char*)&cid + 8);
            auto it = offsets.find(cid);
            uint32_t off = (it != offsets.end()) ? it->second : no_data;
            cells_buf.insert(cells_buf.end(), (char*)&off, (char*)&off + 4);
        }
        write_file(cells_path, cells_buf);
    };

    // Geo cells: parse old, remap, merge with new cell set
    std::cerr << "  Remapping geo entries..." << std::endl;
    {
        auto old_geo = read_file(old_dir + "/geo_cells.bin");
        auto old_se = read_file(old_dir + "/street_entries.bin");
        auto old_ae = read_file(old_dir + "/addr_entries.bin");
        auto old_ie = read_file(old_dir + "/interp_entries.bin");

        auto new_geo = read_file(new_dir + "/geo_cells.bin");
        auto new_se = read_file(new_dir + "/street_entries.bin");
        auto new_ae = read_file(new_dir + "/addr_entries.bin");
        auto new_ie = read_file(new_dir + "/interp_entries.bin");

        // Parse old entries
        auto sc = parse_entries(old_geo, 20, 8, old_se);
        auto ac = parse_entries(old_geo, 20, 12, old_ae);
        auto ic = parse_entries(old_geo, 20, 16, old_ie);

        // Parse new entries (for cells that are new/changed)
        auto new_sc = parse_entries(new_geo, 20, 8, new_se);
        auto new_ac = parse_entries(new_geo, 20, 12, new_ae);
        auto new_ic = parse_entries(new_geo, 20, 16, new_ie);

        // Remap old IDs
        remap_ids(sc, way_remap);
        remap_ids(ac, addr_remap);
        remap_ids(ic, interp_remap);

        // Merge: start with remapped old, overlay with new for changed/added cells
        // For cells that exist in new but not old: add from new
        // For cells that exist in old but not new: remove
        for (auto& [cid, ids] : new_sc) sc[cid] = ids;
        for (auto& [cid, ids] : new_ac) ac[cid] = ids;
        for (auto& [cid, ids] : new_ic) ic[cid] = ids;

        // Collect all cells from new build
        std::unordered_map<uint64_t, bool> new_cells;
        size_t nn = new_geo.size() / 20;
        for (size_t i = 0; i < nn; i++) {
            uint64_t cid; memcpy(&cid, new_geo.data() + i * 20, 8);
            new_cells[cid] = true;
        }
        // Remove cells not in new
        for (auto it = sc.begin(); it != sc.end(); )
            new_cells.count(it->first) ? ++it : it = sc.erase(it);
        for (auto it = ac.begin(); it != ac.end(); )
            new_cells.count(it->first) ? ++it : it = ac.erase(it);
        for (auto it = ic.begin(); it != ic.end(); )
            new_cells.count(it->first) ? ++it : it = ic.erase(it);

        // Write merged entries + geo_cells
        // (This produces files almost identical to new build's entries)
        auto write_geo = [&](const std::string& dir,
                              const std::unordered_map<uint64_t, std::vector<uint32_t>>& s,
                              const std::unordered_map<uint64_t, std::vector<uint32_t>>& a,
                              const std::unordered_map<uint64_t, std::vector<uint32_t>>& ip) {
            // Collect all cells
            std::vector<uint64_t> all_cells;
            for (auto& [id, _] : new_cells) all_cells.push_back(id);
            std::sort(all_cells.begin(), all_cells.end());

            auto write_ent = [&](const std::string& path,
                                  const std::unordered_map<uint64_t, std::vector<uint32_t>>& cm)
                -> std::unordered_map<uint64_t, uint32_t>
            {
                std::vector<char> buf;
                std::unordered_map<uint64_t, uint32_t> offsets;
                for (uint64_t cid : all_cells) {
                    auto it = cm.find(cid);
                    if (it == cm.end() || it->second.empty()) continue;
                    offsets[cid] = static_cast<uint32_t>(buf.size());
                    uint16_t count = static_cast<uint16_t>(it->second.size());
                    buf.insert(buf.end(), (char*)&count, (char*)&count + 2);
                    buf.insert(buf.end(), (char*)it->second.data(),
                              (char*)it->second.data() + it->second.size() * 4);
                }
                write_file(path, buf);
                return offsets;
            };

            auto so = write_ent(dir + "/street_entries.bin", s);
            auto ao = write_ent(dir + "/addr_entries.bin", a);
            auto io = write_ent(dir + "/interp_entries.bin", ip);

            uint32_t no_data = 0xFFFFFFFF;
            std::vector<char> buf;
            for (uint64_t cid : all_cells) {
                buf.insert(buf.end(), (char*)&cid, (char*)&cid + 8);
                auto g = [&](const auto& m) -> uint32_t {
                    auto it = m.find(cid); return (it != m.end()) ? it->second : no_data;
                };
                uint32_t sv = g(so), av = g(ao), iv = g(io);
                buf.insert(buf.end(), (char*)&sv, (char*)&sv + 4);
                buf.insert(buf.end(), (char*)&av, (char*)&av + 4);
                buf.insert(buf.end(), (char*)&iv, (char*)&iv + 4);
            }
            write_file(dir + "/geo_cells.bin", buf);
        };

        write_geo(tmpdir, sc, ac, ic);
    }

    // Admin cells: parse, remap, rebuild
    std::cerr << "  Remapping admin entries..." << std::endl;
    {
        auto old_ac = read_file(old_dir + "/admin_cells.bin");
        auto old_ae = read_file(old_dir + "/admin_entries.bin");
        auto new_ac = read_file(new_dir + "/admin_cells.bin");
        auto new_ae = read_file(new_dir + "/admin_entries.bin");

        auto cells = parse_entries(old_ac, 12, 8, old_ae);
        auto new_cells_map = parse_entries(new_ac, 12, 8, new_ae);
        remap_ids(cells, admin_remap, true);

        // Merge with new
        for (auto& [cid, ids] : new_cells_map) cells[cid] = ids;
        // Remove cells not in new
        std::unordered_map<uint64_t, bool> new_cell_set;
        size_t nn = new_ac.size() / 12;
        for (size_t i = 0; i < nn; i++) {
            uint64_t cid; memcpy(&cid, new_ac.data() + i * 12, 8);
            new_cell_set[cid] = true;
        }
        for (auto it = cells.begin(); it != cells.end(); )
            new_cell_set.count(it->first) ? ++it : it = cells.erase(it);

        write_entries_cells(tmpdir + "/admin_cells.bin", tmpdir + "/admin_entries.bin", cells, 12);
    }
}

// --- Write helpers ---
static void write_val(std::ofstream& f, const void* data, size_t size) {
    f.write(reinterpret_cast<const char*>(data), size);
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-diff <old-dir> <new-dir> -o <patch-file>" << std::endl;
        return 1;
    }
    std::string old_dir = argv[1], new_dir = argv[2], patch_path = argv[4];
    std::string tmpdir = "/tmp/geocoder-diff-" + std::to_string(getpid());
    ensure_dir(tmpdir);

    // Build string remap
    std::cerr << "Building string remap..." << std::endl;
    auto old_strings = read_file(old_dir + "/strings.bin");
    auto new_strings = read_file(new_dir + "/strings.bin");
    auto str_remap = build_string_remap(old_strings, new_strings);
    std::cerr << "  " << str_remap.size() << " strings mapped" << std::endl;

    // Detect struct strides
    auto detect = [](const std::string& path, std::vector<size_t> cs) -> size_t {
        auto d = read_file(path);
        for (size_t s : cs) if (d.size() % s == 0 && d.size() > 0) return s;
        return cs[0];
    };
    size_t way_stride = detect(old_dir + "/street_ways.bin", {12, 9});
    size_t interp_stride = detect(old_dir + "/interp_ways.bin", {20, 18});
    size_t admin_stride = detect(old_dir + "/admin_polygons.bin", {24, 20, 19});

    // Build record ID remaps (requires loading data + coordinate files)
    std::cerr << "Building record ID remaps..." << std::endl;
    std::vector<uint32_t> addr_id_remap, way_id_remap, interp_id_remap, admin_id_remap;
    {
        // addr_points: string-remap old, then match by bytes
        auto old_ap = read_file(old_dir + "/addr_points.bin");
        remap_addr_points(old_ap, str_remap);
        auto new_ap = read_file(new_dir + "/addr_points.bin");
        addr_id_remap = match_addr_points(old_ap, new_ap);
        write_file(tmpdir + "/addr_points.bin", old_ap);
    }
    {
        // street_ways: string-remap + content match (fixes node_offset too)
        auto old_w = read_file(old_dir + "/street_ways.bin");
        remap_field(old_w, way_stride, way_stride == 12 ? 8 : 5, str_remap);
        auto old_n = read_file(old_dir + "/street_nodes.bin");
        auto new_w = read_file(new_dir + "/street_ways.bin");
        auto new_n = read_file(new_dir + "/street_nodes.bin");
        way_id_remap = match_ways(old_w, old_n, new_w, new_n, way_stride);
        write_file(tmpdir + "/street_ways.bin", old_w);
        write_file(tmpdir + "/street_nodes.bin", new_n); // use new nodes (offsets now match)
    }
    {
        auto old_i = read_file(old_dir + "/interp_ways.bin");
        remap_field(old_i, interp_stride, interp_stride >= 20 ? 8 : 5, str_remap);
        auto old_n = read_file(old_dir + "/interp_nodes.bin");
        auto new_i = read_file(new_dir + "/interp_ways.bin");
        auto new_n = read_file(new_dir + "/interp_nodes.bin");
        interp_id_remap = match_interps(old_i, old_n, new_i, new_n, interp_stride);
        write_file(tmpdir + "/interp_ways.bin", old_i);
        write_file(tmpdir + "/interp_nodes.bin", new_n);
    }
    {
        auto old_p = read_file(old_dir + "/admin_polygons.bin");
        remap_field(old_p, admin_stride, 8, str_remap);
        auto old_v = read_file(old_dir + "/admin_vertices.bin");
        auto new_p = read_file(new_dir + "/admin_polygons.bin");
        auto new_v = read_file(new_dir + "/admin_vertices.bin");
        admin_id_remap = match_admin(old_p, old_v, new_p, new_v, admin_stride);
        write_file(tmpdir + "/admin_polygons.bin", old_p);
        write_file(tmpdir + "/admin_vertices.bin", new_v);
    }

    // Rebuild entry/cell files with remapped IDs
    remap_and_rebuild_entries(old_dir, new_dir, tmpdir,
                              way_id_remap, addr_id_remap, interp_id_remap, admin_id_remap);

    // Open patch file
    std::ofstream pf(patch_path, std::ios::binary);
    pf.write(GCPATCH_MAGIC, 8);
    uint32_t ver = GCPATCH_VERSION, flags = 0;
    write_val(pf, &ver, 4); write_val(pf, &flags, 4);

    // Write string remap
    {
        uint32_t marker = 0xFFFFFFFE;
        write_val(pf, &marker, 4);
        std::vector<std::pair<uint32_t,uint32_t>> entries;
        size_t pos = 0;
        while (pos < old_strings.size()) {
            uint32_t old_off = static_cast<uint32_t>(pos);
            auto it = str_remap.find(old_off);
            entries.push_back({old_off, (it != str_remap.end()) ? it->second : old_off});
            pos += strlen(old_strings.data() + pos) + 1;
        }
        uint32_t count = static_cast<uint32_t>(entries.size());
        write_val(pf, &count, 4);
        for (auto& [o, n] : entries) { write_val(pf, &o, 4); write_val(pf, &n, 4); }
        std::cerr << "  Remap: " << count << " entries" << std::endl;
    }

    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");

    uint32_t end = 0xFFFFFFFF;
    write_val(pf, &end, 4);

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());

    auto total = pf.tellp();
    std::cerr << "\nTotal patch: " << total << " bytes (" << total / 1024 / 1024 << " MiB)" << std::endl;
    return 0;
}

-    // Two-stage diff: old→remapped (stage1), remapped→new (stage2)

+    // Write offset fixup tables for ways/interps/admin (node_offset/vertex_offset)

+        auto write_fixups = [&](PatchFileId id, const std::string& name,

+                                 const std::vector<uint32_t>& id_remap, size_t stride) {

+            auto new_data = read_file(new_dir + "/" + name);

+            uint32_t marker = FIXUP_MARKER, fid = static_cast<uint32_t>(id);

+            uint32_t st = static_cast<uint32_t>(stride);

+            std::vector<std::pair<uint32_t, uint32_t>> fixups;

+            for (uint32_t i = 0; i < id_remap.size(); i++) {

+                if (id_remap[i] != 0xFFFFFFFF) {

+                    memcpy(&new_val, new_data.data() + id_remap[i] * stride, 4);

+                    fixups.push_back({i, new_val});

+            write_val(pf, &marker, 4); write_val(pf, &fid, 4);

+            uint32_t count = static_cast<uint32_t>(fixups.size());

+            for (auto& [idx, val] : fixups) { write_val(pf, &idx, 4); write_val(pf, &val, 4); }

+            std::cerr << "  fixup " << name << ": " << count << " entries" << std::endl;

+        write_fixups(PatchFileId::STREET_WAYS, "street_ways.bin", way_id_remap, way_stride);

+        write_fixups(PatchFileId::INTERP_WAYS, "interp_ways.bin", interp_id_remap, interp_stride);

+        write_fixups(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin", admin_id_remap, admin_stride);

+    // Single-stage diffs: remapped_old → new (zstd --patch-from on temp files)

+    auto diff_file = [&](PatchFileId id, const std::string& name) {

+        std::string ref = tmpdir + "/" + name;

+        if (read_file(ref).empty()) ref = old_dir + "/" + name;

+        std::string tgt = new_dir + "/" + name;

+        std::string zst = tmpdir + "/" + name + ".zst";

+        system(("zstd --patch-from='" + ref + "' '" + tgt + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());

+        auto zst_data = read_file(zst);

+        auto old_data = read_file(old_dir + "/" + name);

+        auto new_data = read_file(tgt);

+        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::ZSTD_DELTA);

+        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();

+        write_val(pf, &fid, 4); write_val(pf, &enc, 4);

+        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);

+        pf.write(zst_data.data(), zst_data.size());

+        double pct = ns > 0 ? ds * 100.0 / ns : 0;

+        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;

+    diff_file(PatchFileId::STRINGS, "strings.bin");

+    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");

+    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");

+    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");

+    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");

+    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");

+    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");

+    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");

+    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");

+    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");

+    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");

+    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");

+    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");

+    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");

+    // Remove the old two-stage diff→remapped (stage1), remapped→new (stage2)


default-src 'none'; frame-ancestors 'none'

_cfuvid=9LQXMteneyJ3ujK22krRKkkIwINU_bWu4VuGq5ifw7Q-1774612991.069676-1.0.1.1-qKQvPIvfG.D7eVnQg8I57k1pab64ye2n_vvMJd4g8a0; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com

Overloaded. https://docs.claude.com/en/api/errors


Now remove the old two-stage code that follows:


   620→        double pct = ns > 0 ? ds * 100.0 / ns : 0;
   621→        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
   622→        remove(zst.c_str());
   623→    };
   624→
   625→    diff_file(PatchFileId::STRINGS, "strings.bin");
   626→    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");
   627→    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");
   628→    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");
   629→    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
   630→    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");
   631→    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");
   632→    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
   633→    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");
   634→    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");
   635→    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
   636→    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
   637→    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
   638→    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
   639→
   640→    // Remove the old two-stage diff→remapped (stage1), remapped→new (stage2)
   641→    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
   642→        std::cerr << "  " << name << ": ";
   643→        std::string old_path = old_dir + "/" + name;
   644→        std::string temp_path = tmpdir + "/" + name;
   645→        std::string new_path = new_dir + "/" + name;
   646→        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
   647→        std::string zst2 = tmpdir + "/" + name + ".s2.zst";
   648→
   649→        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
   650→        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());
   651→
   652→        auto s1 = read_file(zst1), s2 = read_file(zst2);
   653→        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);
   654→
   655→        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
   656→        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
   657→        uint64_t s1s = s1.size(), s2s = s2.size();
   658→        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
   659→        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
   660→        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
   661→        pf.write(s1.data(), s1.size());
   662→        pf.write(s2.data(), s2.size());
   663→
   664→        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    diff_file(PatchFileId::STRINGS, "strings.bin");
    diff_file(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_file(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_file(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_file(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_file(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_file(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_file(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_file(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_file(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_file(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_file(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_file(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_file(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");

    // Remove the old two-stage diff→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;


   664→        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
   665→        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
   666→        remove(zst1.c_str()); remove(zst2.c_str());
   667→    };
   668→
   669→    // Single-stage diff for files without remapping
   670→    auto diff_single = [&](PatchFileId id, const std::string& name) {
   671→        std::cerr << "  " << name << ": ";
   672→        std::string old_path = old_dir + "/" + name;
   673→        std::string new_path = new_dir + "/" + name;
   674→        std::string zst = tmpdir + "/" + name + ".zst";
   675→        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
   676→        auto zst_data = read_file(zst);
   677→        auto old_data = read_file(old_path), new_data = read_file(new_path);
   678→
   679→        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
   680→        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
   681→        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
   682→        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
   683→        pf.write(zst_data.data(), zst_data.size());
   684→
   685→        double pct = ns > 0 ? ds * 100.0 / ns : 0;
   686→        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
   687→        remove(zst.c_str());
   688→    };
   689→
   690→    // All remapped files use two-stage
   691→    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
   692→    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
   693→    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");


_cfuvid=FhP2y402TsKoC5QUii2AhafLk1MMZuwurBk6IzD_fK0-1774613002.800604-1.0.1.1-b2PfN0iZyMwhN8GnVbjYX7Pf_pYFbFeo5CDq9NUANKo; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com


   693→    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
   694→    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
   695→    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
   696→    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
   697→    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
   698→    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
   699→    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
   700→    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
   701→    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
   702→    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
   703→    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
   704→    // strings.bin: single stage (no remap)
   705→    diff_single(PatchFileId::STRINGS, "strings.bin");
   706→
   707→    uint32_t end = 0xFFFFFFFF;
   708→    write_val(pf, &end, 4);
   709→
   710→    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
   711→    system(rm_cmd.c_str());
   712→

    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");

    uint32_t end = 0xFFFFFFFF;
    write_val(pf, &end, 4);

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());


    // Remove the old two-stage diff→remapped (stage1), remapped→new (stage2)
    auto diff_two_stage = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string temp_path = tmpdir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst1 = tmpdir + "/" + name + ".s1.zst";
        std::string zst2 = tmpdir + "/" + name + ".s2.zst";

        system(("zstd --patch-from='" + old_path + "' '" + temp_path + "' -o '" + zst1 + "' -f --quiet 2>/dev/null").c_str());
        system(("zstd --patch-from='" + temp_path + "' '" + new_path + "' -o '" + zst2 + "' -f --quiet 2>/dev/null").c_str());

        auto s1 = read_file(zst1), s2 = read_file(zst2);
        auto old_data = read_file(old_path), remapped = read_file(temp_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::TWO_STAGE);
        uint64_t os = old_data.size(), rs = remapped.size(), ns = new_data.size();
        uint64_t s1s = s1.size(), s2s = s2.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &rs, 8); write_val(pf, &ns, 8);
        write_val(pf, &s1s, 8); write_val(pf, &s2s, 8);
        pf.write(s1.data(), s1.size());
        pf.write(s2.data(), s2.size());

        double pct = ns > 0 ? (s1s + s2s) * 100.0 / ns : 0;
        std::cerr << "s1=" << s1s << " s2=" << s2s << " (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst1.c_str()); remove(zst2.c_str());
    };

    // Single-stage diff for files without remapping
    auto diff_single = [&](PatchFileId id, const std::string& name) {
        std::cerr << "  " << name << ": ";
        std::string old_path = old_dir + "/" + name;
        std::string new_path = new_dir + "/" + name;
        std::string zst = tmpdir + "/" + name + ".zst";
        system(("zstd --patch-from='" + old_path + "' '" + new_path + "' -o '" + zst + "' -f --quiet 2>/dev/null").c_str());
        auto zst_data = read_file(zst);
        auto old_data = read_file(old_path), new_data = read_file(new_path);

        uint32_t fid = static_cast<uint32_t>(id), enc = static_cast<uint32_t>(PatchEncoding::COPY_INSERT);
        uint64_t os = old_data.size(), ns = new_data.size(), ds = zst_data.size();
        write_val(pf, &fid, 4); write_val(pf, &enc, 4);
        write_val(pf, &os, 8); write_val(pf, &ns, 8); write_val(pf, &ds, 8);
        pf.write(zst_data.data(), zst_data.size());

        double pct = ns > 0 ? ds * 100.0 / ns : 0;
        std::cerr << ds << " bytes (" << std::fixed << std::setprecision(2) << pct << "%)" << std::endl;
        remove(zst.c_str());
    };

    // All remapped files use two-stage
    diff_two_stage(PatchFileId::ADDR_POINTS, "addr_points.bin");
    diff_two_stage(PatchFileId::STREET_WAYS, "street_ways.bin");
    diff_two_stage(PatchFileId::INTERP_WAYS, "interp_ways.bin");
    diff_two_stage(PatchFileId::ADMIN_POLYGONS, "admin_polygons.bin");
    diff_two_stage(PatchFileId::STREET_NODES, "street_nodes.bin");
    diff_two_stage(PatchFileId::INTERP_NODES, "interp_nodes.bin");
    diff_two_stage(PatchFileId::ADMIN_VERTICES, "admin_vertices.bin");
    diff_two_stage(PatchFileId::GEO_CELLS, "geo_cells.bin");
    diff_two_stage(PatchFileId::STREET_ENTRIES, "street_entries.bin");
    diff_two_stage(PatchFileId::ADDR_ENTRIES, "addr_entries.bin");
    diff_two_stage(PatchFileId::INTERP_ENTRIES, "interp_entries.bin");
    diff_two_stage(PatchFileId::ADMIN_CELLS, "admin_cells.bin");
    diff_two_stage(PatchFileId::ADMIN_ENTRIES, "admin_entries.bin");
    // strings.bin: single stage (no remap)
    diff_single(PatchFileId::STRINGS, "strings.bin");

    uint32_t end = 0xFFFFFFFF;