// geocoder-patch: Apply a .gcpatch to a geocoder index directory.
//
// For each file: apply string remap to old, write temp, run zstd -d --patch-from.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <unistd.h>
#include "patch_format.h"

// --- String remap application ---

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);
    }
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
        return 1;
    }
    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];
    ensure_dir(out_dir);

    std::string tmpdir = "/tmp/geocoder-patch-" + std::to_string(getpid());
    ensure_dir(tmpdir);

    std::ifstream pf(patch_path, std::ios::binary);
    if (!pf) { std::cerr << "Cannot open " << patch_path << std::endl; return 1; }

    // Read header
    char magic[8]; pf.read(magic, 8);
    if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch file" << std::endl; return 1;
    }
    uint32_t version, flags;
    pf.read(reinterpret_cast<char*>(&version), 4);
    pf.read(reinterpret_cast<char*>(&flags), 4);
    if (version != GCPATCH_VERSION) {
        std::cerr << "Unsupported version " << version << std::endl; return 1;
    }

    // Helper: decompress zstd blob via CLI
    auto zstd_decompress_blob = [&](const std::vector<char>& compressed, size_t expected_size) -> std::vector<char> {
        std::string zst_path = tmpdir + "/blob.zst";
        std::string raw_path = tmpdir + "/blob.raw";
        write_file(zst_path, compressed);
        system(("zstd -d '" + zst_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null").c_str());
        auto result = read_file(raw_path);
        remove(zst_path.c_str()); remove(raw_path.c_str());
        return result;
    };

    // Read string remap (compressed)
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t section_id; pf.read(reinterpret_cast<char*>(&section_id), 4);
        if (section_id == 0xFFFFFFFE) {
            uint32_t count; pf.read(reinterpret_cast<char*>(&count), 4);
            uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
            std::vector<char> compressed(comp_size);
            pf.read(compressed.data(), comp_size);
            auto blob = zstd_decompress_blob(compressed, count * 8);
            for (uint32_t i = 0; i < count; i++) {
                uint32_t old_off, new_off;
                memcpy(&old_off, blob.data() + i * 8, 4);
                memcpy(&new_off, blob.data() + i * 8 + 4, 4);
                if (old_off != new_off) str_remap[old_off] = new_off;
            }
            std::cerr << "Loaded " << str_remap.size() << " string remap entries" << std::endl;
        }
    }

    // Read offset fixup tables
    struct Fixup { uint32_t record_idx; uint32_t new_value; };
    std::unordered_map<uint32_t, std::vector<Fixup>> fixup_tables; // file_id → fixups
    std::unordered_map<uint32_t, uint32_t> fixup_strides; // file_id → stride
    while (pf) {
        uint32_t marker; pf.read(reinterpret_cast<char*>(&marker), 4);
        if (marker != FIXUP_MARKER) {
            pf.seekg(-4, std::ios::cur);
            break;
        }
        uint32_t fid, stride, count;
        pf.read(reinterpret_cast<char*>(&fid), 4);
        pf.read(reinterpret_cast<char*>(&stride), 4);
        pf.read(reinterpret_cast<char*>(&count), 4);
        uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
        std::vector<char> compressed(comp_size);
        pf.read(compressed.data(), comp_size);
        auto blob = zstd_decompress_blob(compressed, count * 8);
        auto& vec = fixup_tables[fid];
        fixup_strides[fid] = stride;
        vec.resize(count);
        for (uint32_t i = 0; i < count; i++) {
            memcpy(&vec[i].record_idx, blob.data() + i * 8, 4);
            memcpy(&vec[i].new_value, blob.data() + i * 8 + 4, 4);
        }
        std::cerr << "Loaded " << count << " fixups for file " << fid << std::endl;
    }

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

    // Helper: apply string remap to old data file
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id) {
        if (str_remap.empty()) return;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) {
            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 = str_remap.find(v);
                    if (it != str_remap.end()) memcpy(data.data() + i + off, &it->second, 4);
                }
        } else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) {
            size_t name_off = (way_stride == 12) ? 8 : 5;
            for (size_t i = 0; i + way_stride <= data.size(); i += way_stride) {
                uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + name_off, &it->second, 4);
            }
        } else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) {
            size_t soff = (interp_stride >= 20) ? 8 : 5;
            for (size_t i = 0; i + interp_stride <= data.size(); i += interp_stride) {
                uint32_t v; memcpy(&v, data.data() + i + soff, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + soff, &it->second, 4);
            }
        } else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) {
            for (size_t i = 0; i + admin_stride <= data.size(); i += admin_stride) {
                uint32_t v; memcpy(&v, data.data() + i + 8, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + 8, &it->second, 4);
            }
        }
    };

    // Helper: apply offset fixups
    auto apply_fixups = [&](std::vector<char>& data, uint32_t file_id) {
        auto it = fixup_tables.find(file_id);
        if (it == fixup_tables.end()) return;
        uint32_t stride = fixup_strides[file_id];
        for (auto& f : it->second) {
            size_t pos = (size_t)f.record_idx * stride;
            if (pos + 4 <= data.size())
                memcpy(data.data() + pos, &f.new_value, 4);
        }
    };

    // Read ID remap tables
    std::unordered_map<uint32_t, std::unordered_map<uint32_t,uint32_t>> id_remaps; // file_id → (old→new)
    while (pf) {
        uint32_t marker; pf.read(reinterpret_cast<char*>(&marker), 4);
        if (marker != 0xFFFFFFFC) {
            pf.seekg(-4, std::ios::cur);
            break;
        }
        uint32_t fid, count;
        pf.read(reinterpret_cast<char*>(&fid), 4);
        pf.read(reinterpret_cast<char*>(&count), 4);
        uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
        std::vector<char> compressed(comp_size);
        pf.read(compressed.data(), comp_size);
        auto blob = zstd_decompress_blob(compressed, count * 8);
        auto& rm = id_remaps[fid];
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_id, new_id;
            memcpy(&old_id, blob.data() + i * 8, 4);
            memcpy(&new_id, blob.data() + i * 8 + 4, 4);
            rm[old_id] = new_id;
        }
        std::cerr << "Loaded " << count << " ID remaps for file " << fid << std::endl;
    }

    // Helper: rebuild entry + cell files using ID remap (LEGACY, unused)
    auto rebuild_entries_legacy = [&](const std::string& cells_file, const std::string& entries_file,
                                size_t cell_stride, size_t offset_pos,
                                const std::unordered_map<uint32_t,uint32_t>& id_rm, bool has_flags)
        -> std::pair<std::vector<char>, std::vector<char>> // (entries_data, cells_data)
    {
        auto old_cells = read_file(cur_dir + "/" + cells_file);
        auto old_entries = read_file(cur_dir + "/" + entries_file);
        size_t n_cells = old_cells.size() / cell_stride;

        // Parse old entries, remap IDs
        struct CellEntry { uint64_t cell_id; std::vector<uint32_t> ids; };
        std::vector<CellEntry> parsed;
        for (size_t i = 0; i < n_cells; i++) {
            uint64_t cid; uint32_t off;
            memcpy(&cid, old_cells.data() + i * cell_stride, 8);
            memcpy(&off, old_cells.data() + i * cell_stride + offset_pos, 4);
            CellEntry ce; ce.cell_id = cid;
            if (off != 0xFFFFFFFF && off + 2 <= old_entries.size()) {
                uint16_t count; memcpy(&count, old_entries.data() + off, 2);
                if (off + 2 + count * 4 <= old_entries.size()) {
                    for (uint16_t j = 0; j < count; j++) {
                        uint32_t id; memcpy(&id, old_entries.data() + off + 2 + j * 4, 4);
                        uint32_t flags = has_flags ? (id & 0x80000000u) : 0;
                        uint32_t masked = id & 0x7FFFFFFFu;
                        auto it = id_rm.find(masked);
                        if (it != id_rm.end())
                            ce.ids.push_back(it->second | flags);
                        else
                            ce.ids.push_back(id); // keep unmapped IDs as-is
                    }
                }
            }
            std::sort(ce.ids.begin(), ce.ids.end());
            parsed.push_back(std::move(ce));
        }

        // Rebuild entries + cells
        std::vector<char> new_entries_buf, new_cells_buf;
        uint32_t no_data = 0xFFFFFFFF;
        for (auto& ce : parsed) {
            new_cells_buf.insert(new_cells_buf.end(), (char*)&ce.cell_id, (char*)&ce.cell_id + 8);
            if (ce.ids.empty()) {
                new_cells_buf.insert(new_cells_buf.end(), (char*)&no_data, (char*)&no_data + 4);
            } else {
                uint32_t off = static_cast<uint32_t>(new_entries_buf.size());
                new_cells_buf.insert(new_cells_buf.end(), (char*)&off, (char*)&off + 4);
                uint16_t count = static_cast<uint16_t>(ce.ids.size());
                new_entries_buf.insert(new_entries_buf.end(), (char*)&count, (char*)&count + 2);
                new_entries_buf.insert(new_entries_buf.end(), (char*)ce.ids.data(),
                                      (char*)ce.ids.data() + ce.ids.size() * 4);
            }
        }
        return {new_entries_buf, new_cells_buf};
    };

    // Pre-rebuild entry/cell files using SHARED rebuild function (identical to diff tool)
    std::unordered_map<std::string, std::string> rebuilt_refs;
    if (!id_remaps.empty()) {
        auto w_rm = id_remaps.count((uint32_t)PatchFileId::STREET_WAYS) ? id_remaps[(uint32_t)PatchFileId::STREET_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto a_rm = id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS) ? id_remaps[(uint32_t)PatchFileId::ADDR_POINTS] : std::unordered_map<uint32_t,uint32_t>{};
        auto i_rm = id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS) ? id_remaps[(uint32_t)PatchFileId::INTERP_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto ad_rm = id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS) ? id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS] : std::unordered_map<uint32_t,uint32_t>{};

        auto old_geo = read_file(cur_dir + "/geo_cells.bin");
        auto old_se = read_file(cur_dir + "/street_entries.bin");
        auto old_ae = read_file(cur_dir + "/addr_entries.bin");
        auto old_ie = read_file(cur_dir + "/interp_entries.bin");

        auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm);
        write_file(tmpdir + "/geo_cells.bin", geo.geo_cells_data);
        write_file(tmpdir + "/street_entries.bin", geo.street_entries_data);
        write_file(tmpdir + "/addr_entries.bin", geo.addr_entries_data);
        write_file(tmpdir + "/interp_entries.bin", geo.interp_entries_data);
        rebuilt_refs["geo_cells.bin"] = tmpdir + "/geo_cells.bin";
        rebuilt_refs["street_entries.bin"] = tmpdir + "/street_entries.bin";
        rebuilt_refs["addr_entries.bin"] = tmpdir + "/addr_entries.bin";
        rebuilt_refs["interp_entries.bin"] = tmpdir + "/interp_entries.bin";

        auto old_ac = read_file(cur_dir + "/admin_cells.bin");
        auto old_admin_e = read_file(cur_dir + "/admin_entries.bin");
        auto admin = rebuild_admin_from_remap(old_ac, old_admin_e, ad_rm);
        write_file(tmpdir + "/admin_cells.bin", admin.admin_cells_data);
        write_file(tmpdir + "/admin_entries.bin", admin.admin_entries_data);
        rebuilt_refs["admin_cells.bin"] = tmpdir + "/admin_cells.bin";
        rebuilt_refs["admin_entries.bin"] = tmpdir + "/admin_entries.bin";

        std::cerr << "Rebuilt " << rebuilt_refs.size() << " entry/cell files (shared function)" << std::endl;
    }
    // (Legacy rebuild code removed — using shared rebuild_geo_from_remap above)
    if (false) { // dead code block start
        // Rebuild geo entries (street, addr, interp)
        auto way_rm = id_remaps.count((uint32_t)PatchFileId::STREET_WAYS) ?
                       id_remaps[(uint32_t)PatchFileId::STREET_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto addr_rm = id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS) ?
                        id_remaps[(uint32_t)PatchFileId::ADDR_POINTS] : std::unordered_map<uint32_t,uint32_t>{};
        auto interp_rm = id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS) ?
                          id_remaps[(uint32_t)PatchFileId::INTERP_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto admin_rm = id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS) ?
                         id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS] : std::unordered_map<uint32_t,uint32_t>{};

        // Rebuild geo: street_entries (off 8), addr_entries (off 12), interp_entries (off 16)
        auto old_geo = read_file(cur_dir + "/geo_cells.bin");
        auto old_se = read_file(cur_dir + "/street_entries.bin");
        auto old_ae = read_file(cur_dir + "/addr_entries.bin");
        auto old_ie = read_file(cur_dir + "/interp_entries.bin");

        // Parse + remap each entry type
        auto parse_and_remap = [&](const std::vector<char>& geo, size_t off_pos,
                                     const std::vector<char>& entries,
                                     const std::unordered_map<uint32_t,uint32_t>& rm) {
            size_t n = geo.size() / 20;
            std::vector<std::pair<uint64_t, std::vector<uint32_t>>> result;
            for (size_t i = 0; i < n; i++) {
                uint64_t cid; uint32_t off;
                memcpy(&cid, geo.data() + i * 20, 8);
                memcpy(&off, geo.data() + i * 20 + off_pos, 4);
                std::vector<uint32_t> ids;
                if (off != 0xFFFFFFFF && off + 2 <= entries.size()) {
                    uint16_t count; memcpy(&count, entries.data() + off, 2);
                    if (off + 2 + count * 4 <= entries.size()) {
                        for (uint16_t j = 0; j < count; j++) {
                            uint32_t id; memcpy(&id, entries.data() + off + 2 + j * 4, 4);
                            auto it = rm.find(id);
                            ids.push_back(it != rm.end() ? it->second : id);
                        }
                    }
                }
                std::sort(ids.begin(), ids.end());
                result.push_back({cid, std::move(ids)});
            }
            return result;
        };

        auto sc = parse_and_remap(old_geo, 8, old_se, way_rm);
        auto ac = parse_and_remap(old_geo, 12, old_ae, addr_rm);
        auto ic = parse_and_remap(old_geo, 16, old_ie, interp_rm);

        // Write rebuilt entries + geo_cells
        auto write_rebuilt = [&](const std::string& name,
                                  const std::vector<std::pair<uint64_t, std::vector<uint32_t>>>& entries) -> std::string {
            std::string path = tmpdir + "/" + name;
            std::vector<char> buf;
            std::unordered_map<uint64_t, uint32_t> offsets;
            for (auto& [cid, ids] : entries) {
                if (ids.empty()) continue;
                offsets[cid] = static_cast<uint32_t>(buf.size());
                uint16_t count = static_cast<uint16_t>(ids.size());
                buf.insert(buf.end(), (char*)&count, (char*)&count + 2);
                buf.insert(buf.end(), (char*)ids.data(), (char*)ids.data() + ids.size() * 4);
            }
            write_file(path, buf);
            return path;
        };

        // Entry files: don't use rebuilt refs (diff uses old→new directly)
        // Only geo_cells uses rebuilt ref (proven to match byte-identical)
        write_rebuilt("street_entries.bin", sc); // write for geo_cells offset calculation
        write_rebuilt("addr_entries.bin", ac);
        write_rebuilt("interp_entries.bin", ic);

        // Rebuild geo_cells from the remapped entries
        {
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<char> buf;
            // Build offset maps for each entry type
            std::unordered_map<uint64_t, uint32_t> s_off, a_off, i_off;
            auto entries_data = [](const std::string& path) {
                auto d = read_file(path);
                std::unordered_map<uint64_t, uint32_t> offsets;
                return d;
            };
            // Re-parse the rebuilt entries to get offsets
            auto get_offsets = [](const std::vector<std::pair<uint64_t, std::vector<uint32_t>>>& entries) {
                std::unordered_map<uint64_t, uint32_t> offsets;
                uint32_t pos = 0;
                for (auto& [cid, ids] : entries) {
                    if (ids.empty()) continue;
                    offsets[cid] = pos;
                    pos += 2 + ids.size() * 4;
                }
                return offsets;
            };
            auto so = get_offsets(sc), ao = get_offsets(ac), io = get_offsets(ic);

            size_t n = old_geo.size() / 20;
            for (size_t i = 0; i < n; i++) {
                uint64_t cid; memcpy(&cid, old_geo.data() + i * 20, 8);
                buf.insert(buf.end(), (char*)&cid, (char*)&cid + 8);
                auto get = [&](const auto& m) -> uint32_t {
                    auto it = m.find(cid); return it != m.end() ? it->second : no_data;
                };
                uint32_t sv = get(so), av = get(ao), iv = get(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);
            }
            std::string path = tmpdir + "/geo_cells.bin";
            write_file(path, buf);
            rebuilt_refs["geo_cells.bin"] = path;
        }

        // Rebuild admin entries
        {
            auto [ae_data, ac_data] = rebuild_entries("admin_cells.bin", "admin_entries.bin",
                                                        12, 8, admin_rm, true);
            std::string ae_path = tmpdir + "/admin_entries.bin";
            std::string ac_path = tmpdir + "/admin_cells.bin";
            write_file(ae_path, ae_data);
            write_file(ac_path, ac_data);
            // Don't add admin entries/cells to rebuilt_refs (diff uses old→new directly)
        }

        std::cerr << "Rebuilt (legacy, skipped)" << std::endl;
    } // end if(false)

    // Process file sections
    while (pf) {
        uint32_t file_id; pf.read(reinterpret_cast<char*>(&file_id), 4);
        if (!pf || file_id == 0xFFFFFFFF) break;
        uint32_t encoding; pf.read(reinterpret_cast<char*>(&encoding), 4);

        if (file_id >= (uint32_t)PatchFileId::COUNT) {
            std::cerr << "  Unknown section " << file_id << std::endl;
            break;
        }
        const char* filename = patch_file_names[file_id];

        auto write_temp = [&](const std::string& path, uint64_t size) {
            std::ofstream zf(path, std::ios::binary);
            std::vector<char> buf(std::min(size, (uint64_t)1024*1024));
            uint64_t remaining = size;
            while (remaining > 0) {
                size_t chunk = std::min(remaining, (uint64_t)buf.size());
                pf.read(buf.data(), chunk);
                zf.write(buf.data(), chunk);
                remaining -= chunk;
            }
        };

        auto zstd_apply = [&](const std::string& ref, const std::string& zst, const std::string& out) -> bool {
            std::string cmd = "zstd --patch-from='" + ref + "' -d '" + zst + "' -o '" + out + "' -f --long=31 --quiet 2>/dev/null";
            return system(cmd.c_str()) == 0;
        };

        if (encoding == (uint32_t)PatchEncoding::ZSTD_DELTA) {
            uint64_t old_size, new_size, delta_size;
            pf.read(reinterpret_cast<char*>(&old_size), 8);
            pf.read(reinterpret_cast<char*>(&new_size), 8);
            pf.read(reinterpret_cast<char*>(&delta_size), 8);

            // Write zstd frame to temp
            std::string zst = tmpdir + "/" + std::string(filename) + ".zst";
            write_temp(zst, delta_size);

            // Check if we have a rebuilt ref (entry/cell files with ID remap)
            std::string ref;
            auto rit = rebuilt_refs.find(std::string(filename));
            if (rit != rebuilt_refs.end()) {
                ref = rit->second;
            } else {
                // Load old file, apply string remap + fixups → write remapped ref
                auto old_data = read_file(cur_dir + "/" + std::string(filename));
                apply_str_remap(old_data, file_id);
                apply_fixups(old_data, file_id);
                ref = tmpdir + "/" + std::string(filename) + ".ref";
                write_file(ref, old_data);
            }

            // Apply zstd delta
            std::string out = out_dir + "/" + std::string(filename);
            if (!zstd_apply(ref, zst, out)) {
                std::cerr << "FAILED: " << filename << std::endl; return 1;
            }
            std::cerr << "  " << filename << ": " << read_file(out).size() << " bytes" << std::endl;
            remove(zst.c_str()); remove(ref.c_str());

        } else {
            std::cerr << "  Unknown encoding " << encoding << " for " << filename << std::endl;
            break;
        }
    }

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

    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-    // (Legacy rebuild code removed — using shared rebuild_geo_from_remap above)

-    if (false) { // dead code block start

+    #if 0 // Legacy rebuild code — replaced by shared rebuild_geo_from_remap


Edit

        std::cerr << "Rebuilt (legacy, skipped)" << std::endl;
    } // end if(false)


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

// geocoder-patch: Apply a .gcpatch to a geocoder index directory.
//
// For each file: apply string remap to old, write temp, run zstd -d --patch-from.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>

#include <unistd.h>
#include "patch_format.h"

// --- String remap application ---

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);
    }
}

int main(int argc, char* argv[]) {
    if (argc < 5 || std::string(argv[3]) != "-o") {
        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
        return 1;
    }
    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];
    ensure_dir(out_dir);

    std::string tmpdir = "/tmp/geocoder-patch-" + std::to_string(getpid());
    ensure_dir(tmpdir);

    std::ifstream pf(patch_path, std::ios::binary);
    if (!pf) { std::cerr << "Cannot open " << patch_path << std::endl; return 1; }

    // Read header
    char magic[8]; pf.read(magic, 8);
    if (memcmp(magic, GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch file" << std::endl; return 1;
    }
    uint32_t version, flags;
    pf.read(reinterpret_cast<char*>(&version), 4);
    pf.read(reinterpret_cast<char*>(&flags), 4);
    if (version != GCPATCH_VERSION) {
        std::cerr << "Unsupported version " << version << std::endl; return 1;
    }

    // Helper: decompress zstd blob via CLI
    auto zstd_decompress_blob = [&](const std::vector<char>& compressed, size_t expected_size) -> std::vector<char> {
        std::string zst_path = tmpdir + "/blob.zst";
        std::string raw_path = tmpdir + "/blob.raw";
        write_file(zst_path, compressed);
        system(("zstd -d '" + zst_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null").c_str());
        auto result = read_file(raw_path);
        remove(zst_path.c_str()); remove(raw_path.c_str());
        return result;
    };

    // Read string remap (compressed)
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t section_id; pf.read(reinterpret_cast<char*>(&section_id), 4);
        if (section_id == 0xFFFFFFFE) {
            uint32_t count; pf.read(reinterpret_cast<char*>(&count), 4);
            uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
            std::vector<char> compressed(comp_size);
            pf.read(compressed.data(), comp_size);
            auto blob = zstd_decompress_blob(compressed, count * 8);
            for (uint32_t i = 0; i < count; i++) {
                uint32_t old_off, new_off;
                memcpy(&old_off, blob.data() + i * 8, 4);
                memcpy(&new_off, blob.data() + i * 8 + 4, 4);
                if (old_off != new_off) str_remap[old_off] = new_off;
            }
            std::cerr << "Loaded " << str_remap.size() << " string remap entries" << std::endl;
        }
    }

    // Read offset fixup tables
    struct Fixup { uint32_t record_idx; uint32_t new_value; };
    std::unordered_map<uint32_t, std::vector<Fixup>> fixup_tables; // file_id → fixups
    std::unordered_map<uint32_t, uint32_t> fixup_strides; // file_id → stride
    while (pf) {
        uint32_t marker; pf.read(reinterpret_cast<char*>(&marker), 4);
        if (marker != FIXUP_MARKER) {
            pf.seekg(-4, std::ios::cur);
            break;
        }
        uint32_t fid, stride, count;
        pf.read(reinterpret_cast<char*>(&fid), 4);
        pf.read(reinterpret_cast<char*>(&stride), 4);
        pf.read(reinterpret_cast<char*>(&count), 4);
        uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
        std::vector<char> compressed(comp_size);
        pf.read(compressed.data(), comp_size);
        auto blob = zstd_decompress_blob(compressed, count * 8);
        auto& vec = fixup_tables[fid];
        fixup_strides[fid] = stride;
        vec.resize(count);
        for (uint32_t i = 0; i < count; i++) {
            memcpy(&vec[i].record_idx, blob.data() + i * 8, 4);
            memcpy(&vec[i].new_value, blob.data() + i * 8 + 4, 4);
        }
        std::cerr << "Loaded " << count << " fixups for file " << fid << std::endl;
    }

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

    // Helper: apply string remap to old data file
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id) {
        if (str_remap.empty()) return;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) {
            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 = str_remap.find(v);
                    if (it != str_remap.end()) memcpy(data.data() + i + off, &it->second, 4);
                }
        } else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) {
            size_t name_off = (way_stride == 12) ? 8 : 5;
            for (size_t i = 0; i + way_stride <= data.size(); i += way_stride) {
                uint32_t v; memcpy(&v, data.data() + i + name_off, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + name_off, &it->second, 4);
            }
        } else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) {
            size_t soff = (interp_stride >= 20) ? 8 : 5;
            for (size_t i = 0; i + interp_stride <= data.size(); i += interp_stride) {
                uint32_t v; memcpy(&v, data.data() + i + soff, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + soff, &it->second, 4);
            }
        } else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) {
            for (size_t i = 0; i + admin_stride <= data.size(); i += admin_stride) {
                uint32_t v; memcpy(&v, data.data() + i + 8, 4);
                auto it = str_remap.find(v);
                if (it != str_remap.end()) memcpy(data.data() + i + 8, &it->second, 4);
            }
        }
    };

    // Helper: apply offset fixups
    auto apply_fixups = [&](std::vector<char>& data, uint32_t file_id) {
        auto it = fixup_tables.find(file_id);
        if (it == fixup_tables.end()) return;
        uint32_t stride = fixup_strides[file_id];
        for (auto& f : it->second) {
            size_t pos = (size_t)f.record_idx * stride;
            if (pos + 4 <= data.size())
                memcpy(data.data() + pos, &f.new_value, 4);
        }
    };

    // Read ID remap tables
    std::unordered_map<uint32_t, std::unordered_map<uint32_t,uint32_t>> id_remaps; // file_id → (old→new)
    while (pf) {
        uint32_t marker; pf.read(reinterpret_cast<char*>(&marker), 4);
        if (marker != 0xFFFFFFFC) {
            pf.seekg(-4, std::ios::cur);
            break;
        }
        uint32_t fid, count;
        pf.read(reinterpret_cast<char*>(&fid), 4);
        pf.read(reinterpret_cast<char*>(&count), 4);
        uint64_t comp_size; pf.read(reinterpret_cast<char*>(&comp_size), 8);
        std::vector<char> compressed(comp_size);
        pf.read(compressed.data(), comp_size);
        auto blob = zstd_decompress_blob(compressed, count * 8);
        auto& rm = id_remaps[fid];
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_id, new_id;
            memcpy(&old_id, blob.data() + i * 8, 4);
            memcpy(&new_id, blob.data() + i * 8 + 4, 4);
            rm[old_id] = new_id;
        }
        std::cerr << "Loaded " << count << " ID remaps for file " << fid << std::endl;
    }

    // Helper: rebuild entry + cell files using ID remap (LEGACY, unused)
    auto rebuild_entries_legacy = [&](const std::string& cells_file, const std::string& entries_file,
                                size_t cell_stride, size_t offset_pos,
                                const std::unordered_map<uint32_t,uint32_t>& id_rm, bool has_flags)
        -> std::pair<std::vector<char>, std::vector<char>> // (entries_data, cells_data)
    {
        auto old_cells = read_file(cur_dir + "/" + cells_file);
        auto old_entries = read_file(cur_dir + "/" + entries_file);
        size_t n_cells = old_cells.size() / cell_stride;

        // Parse old entries, remap IDs
        struct CellEntry { uint64_t cell_id; std::vector<uint32_t> ids; };
        std::vector<CellEntry> parsed;
        for (size_t i = 0; i < n_cells; i++) {
            uint64_t cid; uint32_t off;
            memcpy(&cid, old_cells.data() + i * cell_stride, 8);
            memcpy(&off, old_cells.data() + i * cell_stride + offset_pos, 4);
            CellEntry ce; ce.cell_id = cid;
            if (off != 0xFFFFFFFF && off + 2 <= old_entries.size()) {
                uint16_t count; memcpy(&count, old_entries.data() + off, 2);
                if (off + 2 + count * 4 <= old_entries.size()) {
                    for (uint16_t j = 0; j < count; j++) {
                        uint32_t id; memcpy(&id, old_entries.data() + off + 2 + j * 4, 4);
                        uint32_t flags = has_flags ? (id & 0x80000000u) : 0;
                        uint32_t masked = id & 0x7FFFFFFFu;
                        auto it = id_rm.find(masked);
                        if (it != id_rm.end())
                            ce.ids.push_back(it->second | flags);
                        else
                            ce.ids.push_back(id); // keep unmapped IDs as-is
                    }
                }
            }
            std::sort(ce.ids.begin(), ce.ids.end());
            parsed.push_back(std::move(ce));
        }

        // Rebuild entries + cells
        std::vector<char> new_entries_buf, new_cells_buf;
        uint32_t no_data = 0xFFFFFFFF;
        for (auto& ce : parsed) {
            new_cells_buf.insert(new_cells_buf.end(), (char*)&ce.cell_id, (char*)&ce.cell_id + 8);
            if (ce.ids.empty()) {
                new_cells_buf.insert(new_cells_buf.end(), (char*)&no_data, (char*)&no_data + 4);
            } else {
                uint32_t off = static_cast<uint32_t>(new_entries_buf.size());
                new_cells_buf.insert(new_cells_buf.end(), (char*)&off, (char*)&off + 4);
                uint16_t count = static_cast<uint16_t>(ce.ids.size());
                new_entries_buf.insert(new_entries_buf.end(), (char*)&count, (char*)&count + 2);
                new_entries_buf.insert(new_entries_buf.end(), (char*)ce.ids.data(),
                                      (char*)ce.ids.data() + ce.ids.size() * 4);
            }
        }
        return {new_entries_buf, new_cells_buf};
    };

    // Pre-rebuild entry/cell files using SHARED rebuild function (identical to diff tool)
    std::unordered_map<std::string, std::string> rebuilt_refs;
    if (!id_remaps.empty()) {
        auto w_rm = id_remaps.count((uint32_t)PatchFileId::STREET_WAYS) ? id_remaps[(uint32_t)PatchFileId::STREET_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto a_rm = id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS) ? id_remaps[(uint32_t)PatchFileId::ADDR_POINTS] : std::unordered_map<uint32_t,uint32_t>{};
        auto i_rm = id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS) ? id_remaps[(uint32_t)PatchFileId::INTERP_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto ad_rm = id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS) ? id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS] : std::unordered_map<uint32_t,uint32_t>{};

        auto old_geo = read_file(cur_dir + "/geo_cells.bin");
        auto old_se = read_file(cur_dir + "/street_entries.bin");
        auto old_ae = read_file(cur_dir + "/addr_entries.bin");
        auto old_ie = read_file(cur_dir + "/interp_entries.bin");

        auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm);
        write_file(tmpdir + "/geo_cells.bin", geo.geo_cells_data);
        write_file(tmpdir + "/street_entries.bin", geo.street_entries_data);
        write_file(tmpdir + "/addr_entries.bin", geo.addr_entries_data);
        write_file(tmpdir + "/interp_entries.bin", geo.interp_entries_data);
        rebuilt_refs["geo_cells.bin"] = tmpdir + "/geo_cells.bin";
        rebuilt_refs["street_entries.bin"] = tmpdir + "/street_entries.bin";
        rebuilt_refs["addr_entries.bin"] = tmpdir + "/addr_entries.bin";
        rebuilt_refs["interp_entries.bin"] = tmpdir + "/interp_entries.bin";

        auto old_ac = read_file(cur_dir + "/admin_cells.bin");
        auto old_admin_e = read_file(cur_dir + "/admin_entries.bin");
        auto admin = rebuild_admin_from_remap(old_ac, old_admin_e, ad_rm);
        write_file(tmpdir + "/admin_cells.bin", admin.admin_cells_data);
        write_file(tmpdir + "/admin_entries.bin", admin.admin_entries_data);
        rebuilt_refs["admin_cells.bin"] = tmpdir + "/admin_cells.bin";
        rebuilt_refs["admin_entries.bin"] = tmpdir + "/admin_entries.bin";

        std::cerr << "Rebuilt " << rebuilt_refs.size() << " entry/cell files (shared function)" << std::endl;
    }
    #if 0 // Legacy rebuild code — replaced by shared rebuild_geo_from_remap
        // Rebuild geo entries (street, addr, interp)
        auto way_rm = id_remaps.count((uint32_t)PatchFileId::STREET_WAYS) ?
                       id_remaps[(uint32_t)PatchFileId::STREET_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto addr_rm = id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS) ?
                        id_remaps[(uint32_t)PatchFileId::ADDR_POINTS] : std::unordered_map<uint32_t,uint32_t>{};
        auto interp_rm = id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS) ?
                          id_remaps[(uint32_t)PatchFileId::INTERP_WAYS] : std::unordered_map<uint32_t,uint32_t>{};
        auto admin_rm = id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS) ?
                         id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS] : std::unordered_map<uint32_t,uint32_t>{};

        // Rebuild geo: street_entries (off 8), addr_entries (off 12), interp_entries (off 16)
        auto old_geo = read_file(cur_dir + "/geo_cells.bin");
        auto old_se = read_file(cur_dir + "/street_entries.bin");
        auto old_ae = read_file(cur_dir + "/addr_entries.bin");
        auto old_ie = read_file(cur_dir + "/interp_entries.bin");

        // Parse + remap each entry type
        auto parse_and_remap = [&](const std::vector<char>& geo, size_t off_pos,
                                     const std::vector<char>& entries,
                                     const std::unordered_map<uint32_t,uint32_t>& rm) {
            size_t n = geo.size() / 20;
            std::vector<std::pair<uint64_t, std::vector<uint32_t>>> result;
            for (size_t i = 0; i < n; i++) {
                uint64_t cid; uint32_t off;
                memcpy(&cid, geo.data() + i * 20, 8);
                memcpy(&off, geo.data() + i * 20 + off_pos, 4);
                std::vector<uint32_t> ids;
                if (off != 0xFFFFFFFF && off + 2 <= entries.size()) {
                    uint16_t count; memcpy(&count, entries.data() + off, 2);
                    if (off + 2 + count * 4 <= entries.size()) {
                        for (uint16_t j = 0; j < count; j++) {
                            uint32_t id; memcpy(&id, entries.data() + off + 2 + j * 4, 4);
                            auto it = rm.find(id);
                            ids.push_back(it != rm.end() ? it->second : id);
                        }
                    }
                }
                std::sort(ids.begin(), ids.end());
                result.push_back({cid, std::move(ids)});
            }
            return result;
        };

        auto sc = parse_and_remap(old_geo, 8, old_se, way_rm);
        auto ac = parse_and_remap(old_geo, 12, old_ae, addr_rm);
        auto ic = parse_and_remap(old_geo, 16, old_ie, interp_rm);

        // Write rebuilt entries + geo_cells
        auto write_rebuilt = [&](const std::string& name,
                                  const std::vector<std::pair<uint64_t, std::vector<uint32_t>>>& entries) -> std::string {
            std::string path = tmpdir + "/" + name;
            std::vector<char> buf;
            std::unordered_map<uint64_t, uint32_t> offsets;
            for (auto& [cid, ids] : entries) {
                if (ids.empty()) continue;
                offsets[cid] = static_cast<uint32_t>(buf.size());
                uint16_t count = static_cast<uint16_t>(ids.size());
                buf.insert(buf.end(), (char*)&count, (char*)&count + 2);
                buf.insert(buf.end(), (char*)ids.data(), (char*)ids.data() + ids.size() * 4);
            }
            write_file(path, buf);
            return path;
        };

        // Entry files: don't use rebuilt refs (diff uses old→new directly)
        // Only geo_cells uses rebuilt ref (proven to match byte-identical)
        write_rebuilt("street_entries.bin", sc); // write for geo_cells offset calculation
        write_rebuilt("addr_entries.bin", ac);
        write_rebuilt("interp_entries.bin", ic);

        // Rebuild geo_cells from the remapped entries
        {
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<char> buf;
            // Build offset maps for each entry type
            std::unordered_map<uint64_t, uint32_t> s_off, a_off, i_off;
            auto entries_data = [](const std::string& path) {
                auto d = read_file(path);
                std::unordered_map<uint64_t, uint32_t> offsets;
                return d;
            };
            // Re-parse the rebuilt entries to get offsets
            auto get_offsets = [](const std::vector<std::pair<uint64_t, std::vector<uint32_t>>>& entries) {
                std::unordered_map<uint64_t, uint32_t> offsets;
                uint32_t pos = 0;
                for (auto& [cid, ids] : entries) {
                    if (ids.empty()) continue;
                    offsets[cid] = pos;
                    pos += 2 + ids.size() * 4;
                }
                return offsets;
            };
            auto so = get_offsets(sc), ao = get_offsets(ac), io = get_offsets(ic);

            size_t n = old_geo.size() / 20;
            for (size_t i = 0; i < n; i++) {
                uint64_t cid; memcpy(&cid, old_geo.data() + i * 20, 8);
                buf.insert(buf.end(), (char*)&cid, (char*)&cid + 8);
                auto get = [&](const auto& m) -> uint32_t {
                    auto it = m.find(cid); return it != m.end() ? it->second : no_data;
                };
                uint32_t sv = get(so), av = get(ao), iv = get(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);
            }
            std::string path = tmpdir + "/geo_cells.bin";
            write_file(path, buf);
            rebuilt_refs["geo_cells.bin"] = path;
        }

        // Rebuild admin entries
        {
            auto [ae_data, ac_data] = rebuild_entries("admin_cells.bin", "admin_entries.bin",
                                                        12, 8, admin_rm, true);
            std::string ae_path = tmpdir + "/admin_entries.bin";
            std::string ac_path = tmpdir + "/admin_cells.bin";
            write_file(ae_path, ae_data);
            write_file(ac_path, ac_data);
            // Don't add admin entries/cells to rebuilt_refs (diff uses old→new directly)
        }

        std::cerr << "Rebuilt (legacy, skipped)" << std::endl;
    } // end if(false)

    // Process file sections
    while (pf) {
        uint32_t file_id; pf.read(reinterpret_cast<char*>(&file_id), 4);
        if (!pf || file_id == 0xFFFFFFFF) break;
        uint32_t encoding; pf.read(reinterpret_cast<char*>(&encoding), 4);

        if (file_id >= (uint32_t)PatchFileId::COUNT) {
            std::cerr << "  Unknown section " << file_id << std::endl;
            break;
        }
        const char* filename = patch_file_names[file_id];

        auto write_temp = [&](const std::string& path, uint64_t size) {
            std::ofstream zf(path, std::ios::binary);
            std::vector<char> buf(std::min(size, (uint64_t)1024*1024));
            uint64_t remaining = size;
            while (remaining > 0) {
                size_t chunk = std::min(remaining, (uint64_t)buf.size());
                pf.read(buf.data(), chunk);
                zf.write(buf.data(), chunk);
                remaining -= chunk;
            }
        };

        auto zstd_apply = [&](const std::string& ref, const std::string& zst, const std::string& out) -> bool {
            std::string cmd = "zstd --patch-from='" + ref + "' -d '" + zst + "' -o '" + out + "' -f --long=31 --quiet 2>/dev/null";
            return system(cmd.c_str()) == 0;
        };

        if (encoding == (uint32_t)PatchEncoding::ZSTD_DELTA) {
            uint64_t old_size, new_size, delta_size;
            pf.read(reinterpret_cast<char*>(&old_size), 8);
            pf.read(reinterpret_cast<char*>(&new_size), 8);
            pf.read(reinterpret_cast<char*>(&delta_size), 8);

            // Write zstd frame to temp
            std::string zst = tmpdir + "/" + std::string(filename) + ".zst";
            write_temp(zst, delta_size);

            // Check if we have a rebuilt ref (entry/cell files with ID remap)
            std::string ref;
            auto rit = rebuilt_refs.find(std::string(filename));
            if (rit != rebuilt_refs.end()) {
                ref = rit->second;
            } else {
                // Load old file, apply string remap + fixups → write remapped ref
                auto old_data = read_file(cur_dir + "/" + std::string(filename));
                apply_str_remap(old_data, file_id);
                apply_fixups(old_data, file_id);
                ref = tmpdir + "/" + std::string(filename) + ".ref";
                write_file(ref, old_data);
            }

            // Apply zstd delta
            std::string out = out_dir + "/" + std::string(filename);
            if (!zstd_apply(ref, zst, out)) {
                std::cerr << "FAILED: " << filename << std::endl; return 1;
            }
            std::cerr << "  " << filename << ": " << read_file(out).size() << " bytes" << std::endl;
            remove(zst.c_str()); remove(ref.c_str());

        } else {
            std::cerr << "  Unknown encoding " << encoding << " for " << filename << std::endl;
            break;
        }
    }

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

    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-        std::cerr << "Rebuilt (legacy, skipped)" << std::endl;


Bash

cmake --build build -j$(nproc) --target geocoder-diff --target geocoder-patch 2>&1 | tail -5

Build


[100%] Built target geocoder-diff
[ 50%] Building CXX object CMakeFiles/geocoder-patch.dir/tools/geocoder_patch.cpp.o
[100%] Linking CXX executable geocoder-patch
[100%] Built target geocoder-patch


Now test:


sshpass -p 'elon-is-insane' scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" /root/traccar-geocoder/builder/build/geocoder-diff /root/traccar-geocoder/builder/build/geocoder-patch michtest@100.113.88.121:~/ 2>&1 && sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 '
rm -f /home/michtest/test12.gcpatch && /home/michtest/geocoder-diff /home/michtest/det-A /home/michtest/det-new -o /home/michtest/test12.gcpatch 2>&1 | grep -E "(bytes|Total|Rebuilt)"
echo "Patch: $(numfmt --to=iec $(stat -c%s /home/michtest/test12.gcpatch))"
rm -rf /home/michtest/patched12 && /home/michtest/geocoder-patch /home/michtest/det-A /home/michtest/test12.gcpatch -o /home/michtest/patched12 2>&1 | grep -E "(bytes|Loaded|Rebuilt|FAIL)"
echo "=== VERIFY ==="
all_match=true
for f in /home/michtest/det-new/*.bin; do
    b=$(basename $f); p=/home/michtest/patched12/$b
    [ -f "$p" ] && cmp -s "$f" "$p" && echo "  $b: MATCH" || { echo "  $b: FAIL"; all_match=false; }
done
$all_match && echo "ALL FILES MATCH" || echo "SOME DIFFER"
' 2>&1

Full test with shared rebuild function


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
  Rebuilt entry/cell files
  Remap: 4982052 entries (39856416 -> 11885604 bytes)
  addr_points.bin: 3770551 bytes (0.27%)
  street_ways.bin: 1515482 bytes (0.65%)
  interp_ways.bin: 66381 bytes (19.79%)
  admin_polygons.bin: 39775 bytes (0.37%)
  strings.bin: 71309 bytes (0.09%)
  street_nodes.bin: 6727404 bytes (0.47%)
  interp_nodes.bin: 1228 bytes (0.17%)
  admin_vertices.bin: 535915 bytes (0.18%)
  geo_cells.bin: 396408051 bytes (23.68%)
  street_entries.bin: 3465195 bytes (0.59%)
  addr_entries.bin: 1871879 bytes (0.47%)
  interp_entries.bin: 60561 bytes (38.00%)
  admin_cells.bin: 469093 bytes (27.72%)
  admin_entries.bin: 305870 bytes (5.00%)
Total patch: 656107693 bytes (625 MiB)
Patch: 626M
Loaded 4980054 string remap entries
Loaded 19372891 fixups for file 1
Loaded 1016 fixups for file 4
Loaded 449642 fixups for file 6
Loaded 19372891 ID remaps for file 1
Loaded 85825938 ID remaps for file 3
Loaded 1016 ID remaps for file 4
Loaded 449642 ID remaps for file 6
Rebuilt 6 entry/cell files (shared function)
  addr_points.bin: 1375841776 bytes
  street_ways.bin: 233626176 bytes
  interp_ways.bin: 335424 bytes
  admin_polygons.bin: 10820736 bytes
  strings.bin: 82573397 bytes
  street_nodes.bin: 1445600360 bytes
  interp_nodes.bin: 725184 bytes
  admin_vertices.bin: 291061192 bytes
  geo_cells.bin: 1674295440 bytes
  street_entries.bin: 586571766 bytes
  addr_entries.bin: 395005236 bytes
  interp_entries.bin: 159386 bytes
  admin_cells.bin: 1692000 bytes
  admin_entries.bin: 6122260 bytes
=== VERIFY ===
  addr_entries.bin: MATCH
  addr_points.bin: MATCH
  admin_cells.bin: MATCH
  admin_entries.bin: MATCH
  admin_polygons.bin: MATCH
  admin_vertices.bin: MATCH
  geo_cells.bin: MATCH
  interp_entries.bin: MATCH
  interp_nodes.bin: MATCH
  interp_ways.bin: MATCH
  street_entries.bin: MATCH
  street_nodes.bin: MATCH
  street_ways.bin: MATCH
  strings.bin: MATCH
ALL FILES MATCH


**ALL 14 FILES MATCH!** The shared rebuild function works. Patch is 626 MiB. The remaining bottleneck is geo_cells at 396 MiB — the pure remap (without overlay) doesn't produce geo_cells close enough to new. But everything else is great.

Now the big question: the geo_cells diff is 396 MiB because the rebuilt geo_cells (using old cell set + remapped IDs) differs significantly from new geo_cells (which has different cells). The cell set changes are:
- ~39K new cells (from new records)
- ~11K removed cells
- These shift all subsequent entries, cascading offsets

To fix this: include the new cell set changes in the patch (added/removed cell IDs). The patch tool applies them during rebuild. But that adds complexity.

For now: the patch is 626 MiB. The breakdown:
- Compressed tables: ~240 MiB
- Data deltas: ~13 MiB
- Entry deltas: ~6 MiB
- **geo_cells delta: 396 MiB** (the bottleneck)

If I can get geo_cells to match, total would be ~260 MiB. Let me commit this working state and tackle geo_cells next.


git add tools/geocoder_diff.cpp tools/geocoder_patch.cpp tools/patch_format.h && git commit -m "$(cat <<'EOF'
feat: shared entry rebuild function — all 14 files verified at 626 MiB

Extracted entry/cell rebuild into shared rebuild_geo_from_remap() and
rebuild_admin_from_remap() in patch_format.h, used by both diff and
patch tools. This guarantees identical rebuilt references.

Patch size: 626 MiB (from 7 GiB, 4 days of Europe changes)
  Compressed tables: 240 MiB (string remap + fixups + ID remaps)
  Data file deltas: 13 MiB (0.1-0.7%)
  Entry file deltas: 6 MiB (0.02-5%)
  geo_cells.bin delta: 396 MiB (23.7% — remaining bottleneck)

All 14 files verified byte-identical after round-trip.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)"

Commit: shared rebuild, all verified


[main f90d0f8] feat: shared entry rebuild function — all 14 files verified at 626 MiB
 3 files changed, 210 insertions(+), 9 deletions(-)


Now to tackle geo_cells (396 MiB). The fix: include the **new cell set** in the patch so the patch tool can rebuild geo_cells with the correct cells. The info needed: which cells were added and which were removed.


sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 'python3 -c "
import struct
old = open(\"/home/michtest/det-A/geo_cells.bin\", \"rb\").read()
new = open(\"/home/michtest/det-new/geo_cells.bin\", \"rb\").read()
old_cells = set()
for i in range(len(old)//20):
    cid = struct.unpack_from(\"<Q\", old, i*20)[0]
    old_cells.add(cid)
new_cells = set()
for i in range(len(new)//20):
    cid = struct.unpack_from(\"<Q\", new, i*20)[0]
    new_cells.add(cid)
added = new_cells - old_cells
removed = old_cells - new_cells
print(f\"Old cells: {len(old_cells)}\")
print(f\"New cells: {len(new_cells)}\")
print(f\"Added: {len(added)}\")
print(f\"Removed: {len(removed)}\")
print(f\"Cell diff data: {(len(added) + len(removed)) * 8} bytes ({(len(added) + len(removed)) * 8 // 1024} KB)\")
"' 2>&1

Measure cell set differences


Command running in background with ID: bj9yexytb. Output is being written to: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/bj9yexytb.output


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/bj9yexytb.output

Check cell diff


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.


Still loading. 84M cells takes time to parse in Python. Let me wait:


sleep 60 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/bj9yexytb.output

Check results


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.
Old cells: 83675970
New cells: 83714772
Added: 49840
Removed: 11038
Cell diff data: 487024 bytes (475 KB)


Only **475 KB** of cell diff data (49,840 added + 11,038 removed cell IDs). Tiny! If I include this in the patch and have the rebuild use the new cell set, geo_cells should match.

The approach:
1. Diff tool: include added_cells and removed_cells lists in the patch
2. Diff tool: rebuild with the new cell set (old cells + added - removed)
3. Patch tool: read cell changes, apply to old cell set, rebuild with updated set
4. Both produce identical geo_cells

Let me add a "cell changes" section to the patch format and update both tools:


Edit

static constexpr uint32_t FIXUP_MARKER = 0xFFFFFFFD;

static constexpr uint32_t FIXUP_MARKER = 0xFFFFFFFD;

// Cell changes section marker: 0xFFFFFFFB
// Format: marker, num_added(u32), num_removed(u32),
//         [added_cell_id(u64)] * num_added, [removed_cell_id(u64)] * num_removed
// For both geo and admin cell sets.
static constexpr uint32_t CELL_CHANGES_GEO_MARKER = 0xFFFFFFFB;
static constexpr uint32_t CELL_CHANGES_ADMIN_MARKER = 0xFFFFFFFA;


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

static constexpr uint32_t FIXUP_MARKER = 0xFFFFFFFD;

// Cell changes section marker: 0xFFFFFFFB
// Format: marker, num_added(u32), num_removed(u32),
//         [added_cell_id(u64)] * num_added, [removed_cell_id(u64)] * num_removed
// For both geo and admin cell sets.
static constexpr uint32_t CELL_CHANGES_GEO_MARKER = 0xFFFFFFFB;
static constexpr uint32_t CELL_CHANGES_ADMIN_MARKER = 0xFFFFFFFA;

#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)
    ZSTD_DELTA = 1,   // Single zstd delta frame (zstd --patch-from on remapped old)
};

// 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;

// --- Shared entry rebuild logic ---
// Used by both diff and patch tools to produce identical rebuilt entries.
// Takes old geo_cells + old entries + ID remap → produces rebuilt entries + geo_cells.

struct RebuiltGeo {
    std::vector<char> geo_cells_data;
    std::vector<char> street_entries_data;
    std::vector<char> addr_entries_data;
    std::vector<char> interp_entries_data;
};

inline RebuiltGeo rebuild_geo_from_remap(
    const std::vector<char>& old_geo,
    const std::vector<char>& old_se, const std::vector<char>& old_ae, const std::vector<char>& old_ie,
    const std::unordered_map<uint32_t,uint32_t>& way_rm,
    const std::unordered_map<uint32_t,uint32_t>& addr_rm,
    const std::unordered_map<uint32_t,uint32_t>& interp_rm)
{
    size_t n_cells = old_geo.size() / 20;

    auto parse_entry = [](const std::vector<char>& data, uint32_t off) -> std::vector<uint32_t> {
        if (off == 0xFFFFFFFF || off + 2 > data.size()) return {};
        uint16_t count; memcpy(&count, data.data() + off, 2);
        if (off + 2 + count * 4 > data.size()) return {};
        std::vector<uint32_t> ids(count);
        memcpy(ids.data(), data.data() + off + 2, count * 4);
        return ids;
    };

    auto remap_ids = [](std::vector<uint32_t>& ids, const std::unordered_map<uint32_t,uint32_t>& rm) {
        for (auto& id : ids) {
            auto it = rm.find(id);
            if (it != rm.end()) id = it->second;
        }
        std::sort(ids.begin(), ids.end());
    };

    // Parse all cells and remap IDs
    struct CellData {
        uint64_t cell_id;
        std::vector<uint32_t> streets, addrs, interps;
    };
    std::vector<CellData> cells(n_cells);
    for (size_t i = 0; i < n_cells; i++) {
        memcpy(&cells[i].cell_id, old_geo.data() + i * 20, 8);
        uint32_t s_off, a_off, i_off;
        memcpy(&s_off, old_geo.data() + i * 20 + 8, 4);
        memcpy(&a_off, old_geo.data() + i * 20 + 12, 4);
        memcpy(&i_off, old_geo.data() + i * 20 + 16, 4);
        cells[i].streets = parse_entry(old_se, s_off);
        cells[i].addrs = parse_entry(old_ae, a_off);
        cells[i].interps = parse_entry(old_ie, i_off);
        remap_ids(cells[i].streets, way_rm);
        remap_ids(cells[i].addrs, addr_rm);
        remap_ids(cells[i].interps, interp_rm);
    }

    // Write rebuilt files
    RebuiltGeo result;
    uint32_t no_data = 0xFFFFFFFF;

    auto write_entries = [&](std::vector<char>& buf, const auto& getter) -> std::unordered_map<uint64_t, uint32_t> {
        std::unordered_map<uint64_t, uint32_t> offsets;
        for (auto& c : cells) {
            const auto& ids = getter(c);
            if (ids.empty()) continue;
            offsets[c.cell_id] = static_cast<uint32_t>(buf.size());
            uint16_t count = static_cast<uint16_t>(ids.size());
            buf.insert(buf.end(), (const char*)&count, (const char*)&count + 2);
            buf.insert(buf.end(), (const char*)ids.data(), (const char*)ids.data() + ids.size() * 4);
        }
        return offsets;
    };

    auto s_off = write_entries(result.street_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.streets; });
    auto a_off = write_entries(result.addr_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.addrs; });
    auto i_off = write_entries(result.interp_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.interps; });

    // Write geo_cells
    for (auto& c : cells) {
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&c.cell_id, (const char*)&c.cell_id + 8);
        auto get = [&](const auto& m) -> uint32_t {
            auto it = m.find(c.cell_id); return it != m.end() ? it->second : no_data;
        };
        uint32_t sv = get(s_off), av = get(a_off), iv = get(i_off);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&sv, (const char*)&sv + 4);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&av, (const char*)&av + 4);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&iv, (const char*)&iv + 4);
    }

    return result;
}

struct RebuiltAdmin {
    std::vector<char> admin_cells_data;
    std::vector<char> admin_entries_data;
};

inline RebuiltAdmin rebuild_admin_from_remap(
    const std::vector<char>& old_ac, const std::vector<char>& old_ae,
    const std::unordered_map<uint32_t,uint32_t>& admin_rm)
{
    size_t n_cells = old_ac.size() / 12;

    struct CellData { uint64_t cell_id; std::vector<uint32_t> ids; };
    std::vector<CellData> cells(n_cells);
    for (size_t i = 0; i < n_cells; i++) {
        memcpy(&cells[i].cell_id, old_ac.data() + i * 12, 8);
        uint32_t off; memcpy(&off, old_ac.data() + i * 12 + 8, 4);
        if (off != 0xFFFFFFFF && off + 2 <= old_ae.size()) {
            uint16_t count; memcpy(&count, old_ae.data() + off, 2);
            if (off + 2 + count * 4 <= old_ae.size()) {
                cells[i].ids.resize(count);
                memcpy(cells[i].ids.data(), old_ae.data() + off + 2, count * 4);
                for (auto& id : cells[i].ids) {
                    uint32_t flags = id & 0x80000000u;
                    uint32_t masked = id & 0x7FFFFFFFu;
                    auto it = admin_rm.find(masked);
                    if (it != admin_rm.end()) id = it->second | flags;
                }
                std::sort(cells[i].ids.begin(), cells[i].ids.end());
            }
        }
    }

    RebuiltAdmin result;
    uint32_t no_data = 0xFFFFFFFF;
    std::unordered_map<uint64_t, uint32_t> offsets;
    for (auto& c : cells) {
        if (c.ids.empty()) continue;
        offsets[c.cell_id] = static_cast<uint32_t>(result.admin_entries_data.size());
        uint16_t count = static_cast<uint16_t>(c.ids.size());
        result.admin_entries_data.insert(result.admin_entries_data.end(), (const char*)&count, (const char*)&count + 2);
        result.admin_entries_data.insert(result.admin_entries_data.end(), (const char*)c.ids.data(), (const char*)c.ids.data() + c.ids.size() * 4);
    }
    for (auto& c : cells) {
        result.admin_cells_data.insert(result.admin_cells_data.end(), (const char*)&c.cell_id, (const char*)&c.cell_id + 8);
        auto it = offsets.find(c.cell_id);
        uint32_t off = it != offsets.end() ? it->second : no_data;
        result.admin_cells_data.insert(result.admin_cells_data.end(), (const char*)&off, (const char*)&off + 4);
    }
    return result;
}

// --- 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());
}

+// Cell changes section marker: 0xFFFFFFFB

+// Format: marker, num_added(u32), num_removed(u32),

+//         [added_cell_id(u64)] * num_added, [removed_cell_id(u64)] * num_removed

+static constexpr uint32_t CELL_CHANGES_GEO_MARKER = 0xFFFFFFFB;

+static constexpr uint32_t CELL_CHANGES_ADMIN_MARKER = 0xFFFFFFFA;


Now update the shared rebuild function to accept cell changes:


inline RebuiltGeo rebuild_geo_from_remap(
    const std::vector<char>& old_geo,
    const std::vector<char>& old_se, const std::vector<char>& old_ae, const std::vector<char>& old_ie,
    const std::unordered_map<uint32_t,uint32_t>& way_rm,
    const std::unordered_map<uint32_t,uint32_t>& addr_rm,
    const std::unordered_map<uint32_t,uint32_t>& interp_rm)

inline RebuiltGeo rebuild_geo_from_remap(
    const std::vector<char>& old_geo,
    const std::vector<char>& old_se, const std::vector<char>& old_ae, const std::vector<char>& old_ie,
    const std::unordered_map<uint32_t,uint32_t>& way_rm,
    const std::unordered_map<uint32_t,uint32_t>& addr_rm,
    const std::unordered_map<uint32_t,uint32_t>& interp_rm,
    const std::vector<uint64_t>& added_cells = {},
    const std::vector<uint64_t>& removed_cells = {})


inline RebuiltGeo rebuild_geo_from_remap(
    const std::vector<char>& old_geo,
    const std::vector<char>& old_se, const std::vector<char>& old_ae, const std::vector<char>& old_ie,
    const std::unordered_map<uint32_t,uint32_t>& way_rm,
    const std::unordered_map<uint32_t,uint32_t>& addr_rm,
    const std::unordered_map<uint32_t,uint32_t>& interp_rm,
    const std::vector<uint64_t>& added_cells = {},
    const std::vector<uint64_t>& removed_cells = {})

#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)
    ZSTD_DELTA = 1,   // Single zstd delta frame (zstd --patch-from on remapped old)
};

// 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;

// Cell changes section marker: 0xFFFFFFFB
// Format: marker, num_added(u32), num_removed(u32),
//         [added_cell_id(u64)] * num_added, [removed_cell_id(u64)] * num_removed
// For both geo and admin cell sets.
static constexpr uint32_t CELL_CHANGES_GEO_MARKER = 0xFFFFFFFB;
static constexpr uint32_t CELL_CHANGES_ADMIN_MARKER = 0xFFFFFFFA;

// --- Shared entry rebuild logic ---
// Used by both diff and patch tools to produce identical rebuilt entries.
// Takes old geo_cells + old entries + ID remap → produces rebuilt entries + geo_cells.

struct RebuiltGeo {
    std::vector<char> geo_cells_data;
    std::vector<char> street_entries_data;
    std::vector<char> addr_entries_data;
    std::vector<char> interp_entries_data;
};

inline RebuiltGeo rebuild_geo_from_remap(
    const std::vector<char>& old_geo,
    const std::vector<char>& old_se, const std::vector<char>& old_ae, const std::vector<char>& old_ie,
    const std::unordered_map<uint32_t,uint32_t>& way_rm,
    const std::unordered_map<uint32_t,uint32_t>& addr_rm,
    const std::unordered_map<uint32_t,uint32_t>& interp_rm)
{
    size_t n_cells = old_geo.size() / 20;

    auto parse_entry = [](const std::vector<char>& data, uint32_t off) -> std::vector<uint32_t> {
        if (off == 0xFFFFFFFF || off + 2 > data.size()) return {};
        uint16_t count; memcpy(&count, data.data() + off, 2);
        if (off + 2 + count * 4 > data.size()) return {};
        std::vector<uint32_t> ids(count);
        memcpy(ids.data(), data.data() + off + 2, count * 4);
        return ids;
    };

    auto remap_ids = [](std::vector<uint32_t>& ids, const std::unordered_map<uint32_t,uint32_t>& rm) {
        for (auto& id : ids) {
            auto it = rm.find(id);
            if (it != rm.end()) id = it->second;
        }
        std::sort(ids.begin(), ids.end());
    };

    // Parse all cells and remap IDs
    struct CellData {
        uint64_t cell_id;
        std::vector<uint32_t> streets, addrs, interps;
    };
    std::vector<CellData> cells(n_cells);
    for (size_t i = 0; i < n_cells; i++) {
        memcpy(&cells[i].cell_id, old_geo.data() + i * 20, 8);
        uint32_t s_off, a_off, i_off;
        memcpy(&s_off, old_geo.data() + i * 20 + 8, 4);
        memcpy(&a_off, old_geo.data() + i * 20 + 12, 4);
        memcpy(&i_off, old_geo.data() + i * 20 + 16, 4);
        cells[i].streets = parse_entry(old_se, s_off);
        cells[i].addrs = parse_entry(old_ae, a_off);
        cells[i].interps = parse_entry(old_ie, i_off);
        remap_ids(cells[i].streets, way_rm);
        remap_ids(cells[i].addrs, addr_rm);
        remap_ids(cells[i].interps, interp_rm);
    }

    // Write rebuilt files
    RebuiltGeo result;
    uint32_t no_data = 0xFFFFFFFF;

    auto write_entries = [&](std::vector<char>& buf, const auto& getter) -> std::unordered_map<uint64_t, uint32_t> {
        std::unordered_map<uint64_t, uint32_t> offsets;
        for (auto& c : cells) {
            const auto& ids = getter(c);
            if (ids.empty()) continue;
            offsets[c.cell_id] = static_cast<uint32_t>(buf.size());
            uint16_t count = static_cast<uint16_t>(ids.size());
            buf.insert(buf.end(), (const char*)&count, (const char*)&count + 2);
            buf.insert(buf.end(), (const char*)ids.data(), (const char*)ids.data() + ids.size() * 4);
        }
        return offsets;
    };

    auto s_off = write_entries(result.street_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.streets; });
    auto a_off = write_entries(result.addr_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.addrs; });
    auto i_off = write_entries(result.interp_entries_data, [](const CellData& c) -> const std::vector<uint32_t>& { return c.interps; });

    // Write geo_cells
    for (auto& c : cells) {
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&c.cell_id, (const char*)&c.cell_id + 8);
        auto get = [&](const auto& m) -> uint32_t {
            auto it = m.find(c.cell_id); return it != m.end() ? it->second : no_data;
        };
        uint32_t sv = get(s_off), av = get(a_off), iv = get(i_off);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&sv, (const char*)&sv + 4);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&av, (const char*)&av + 4);
        result.geo_cells_data.insert(result.geo_cells_data.end(), (const char*)&iv, (const char*)&iv + 4);
    }

    return result;
}

struct RebuiltAdmin {
    std::vector<char> admin_cells_data;
    std::vector<char> admin_entries_data;
};

inline RebuiltAdmin rebuild_admin_from_remap(
    const std::vector<char>& old_ac, const std::vector<char>& old_ae,
    const std::unordered_map<uint32_t,uint32_t>& admin_rm)
{
    size_t n_cells = old_ac.size() / 12;

    struct CellData { uint64_t cell_id; std::vector<uint32_t> ids; };
    std::vector<CellData> cells(n_cells);
    for (size_t i = 0; i < n_cells; i++) {
        memcpy(&cells[i].cell_id, old_ac.data() + i * 12, 8);
        uint32_t off; memcpy(&off, old_ac.data() + i * 12 + 8, 4);
        if (off != 0xFFFFFFFF && off + 2 <= old_ae.size()) {
            uint16_t count; memcpy(&count, old_ae.data() + off, 2);
            if (off + 2 + count * 4 <= old_ae.size()) {
                cells[i].ids.resize(count);
                memcpy(cells[i].ids.data(), old_ae.data() + off + 2, count * 4);
                for (auto& id : cells[i].ids) {
                    uint32_t flags = id & 0x80000000u;
                    uint32_t masked = id & 0x7FFFFFFFu;
                    auto it = admin_rm.find(masked);
                    if (it != admin_rm.end()) id = it->second | flags;
                }
                std::sort(cells[i].ids.begin(), cells[i].ids.end());
            }
        }
    }

    RebuiltAdmin result;
    uint32_t no_data = 0xFFFFFFFF;
    std::unordered_map<uint64_t, uint32_t> offsets;
    for (auto& c : cells) {
        if (c.ids.empty()) continue;
        offsets[c.cell_id] = static_cast<uint32_t>(result.admin_entries_data.size());
        uint16_t count = static_cast<uint16_t>(c.ids.size());
        result.admin_entries_data.insert(result.admin_entries_data.end(), (const char*)&count, (const char*)&count + 2);
        result.admin_entries_data.insert(result.admin_entries_data.end(), (const char*)c.ids.data(), (const char*)c.ids.data() + c.ids.size() * 4);
    }
    for (auto& c : cells) {
        result.admin_cells_data.insert(result.admin_cells_data.end(), (const char*)&c.cell_id, (const char*)&c.cell_id + 8);
        auto it = offsets.find(c.cell_id);
        uint32_t off = it != offsets.end() ? it->second : no_data;
        result.admin_cells_data.insert(result.admin_cells_data.end(), (const char*)&off, (const char*)&off + 4);
    }
    return result;
}

// --- 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());
}

-    const std::unordered_map<uint32_t,uint32_t>& interp_rm)

+    const std::unordered_map<uint32_t,uint32_t>& interp_rm,

+    const std::vector<uint64_t>& added_cells = {},

+    const std::vector<uint64_t>& removed_cells = {})


Now inside the function, after parsing old cells, apply cell changes:


   233→};
   234→
   235→// COPY_INSERT opcodes
   236→enum : uint8_t {
   237→    OP_COPY = 0,    // Copy from old file: [u8 tag][u64 offset][u32 length]
   238→    OP_INSERT = 1,  // Insert new data:    [u8 tag][u32 length][data...]
   239→};
   240→
   241→// Offset fixup section marker: 0xFFFFFFFD
   242→// Format: uint32_t marker, uint32_t file_id, uint32_t stride,
   243→//         uint32_t count, [(uint32_t record_index, uint32_t new_offset_value)] * count
   244→// Applied to byte offset 0 of each record (node_offset/vertex_offset field).
   245→
   246→static constexpr uint32_t FIXUP_MARKER = 0xFFFFFFFD;
   247→

};

// 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;


No matches found


Let me read the rebuild function:


That's strange. Let me check what's actually in the file:


Bash

wc -l /root/traccar-geocoder/builder/tools/patch_format.h && grep -n "rebuild\|RebuiltGeo\|RebuiltAdmin" /root/traccar-geocoder/builder/tools/patch_format.h | head -10

Check patch_format.h content


411 /root/traccar-geocoder/builder/tools/patch_format.h
255:// --- Shared entry rebuild logic ---
259:struct RebuiltGeo {
266:inline RebuiltGeo rebuild_geo_from_remap(
315:    RebuiltGeo result;
350:struct RebuiltAdmin {
355:inline RebuiltAdmin rebuild_admin_from_remap(
382:    RebuiltAdmin result;


The file is there! Grep with quotes wasn't matching. Let me read the relevant section:


   275→    size_t n_cells = old_geo.size() / 20;
   276→
   277→    auto parse_entry = [](const std::vector<char>& data, uint32_t off) -> std::vector<uint32_t> {
   278→        if (off == 0xFFFFFFFF || off + 2 > data.size()) return {};
   279→        uint16_t count; memcpy(&count, data.data() + off, 2);
   280→        if (off + 2 + count * 4 > data.size()) return {};
   281→        std::vector<uint32_t> ids(count);
   282→        memcpy(ids.data(), data.data() + off + 2, count * 4);
   283→        return ids;
   284→    };
   285→
   286→    auto remap_ids = [](std::vector<uint32_t>& ids, const std::unordered_map<uint32_t,uint32_t>& rm) {
   287→        for (auto& id : ids) {
   288→            auto it = rm.find(id);
   289→            if (it != rm.end()) id = it->second;
   290→        }
   291→        std::sort(ids.begin(), ids.end());
   292→    };
   293→
   294→    // Parse all cells and remap IDs
   295→    struct CellData {
   296→        uint64_t cell_id;
   297→        std::vector<uint32_t> streets, addrs, interps;
   298→    };
   299→    std::vector<CellData> cells(n_cells);
   300→    for (size_t i = 0; i < n_cells; i++) {
   301→        memcpy(&cells[i].cell_id, old_geo.data() + i * 20, 8);
   302→        uint32_t s_off, a_off, i_off;
   303→        memcpy(&s_off, old_geo.data() + i * 20 + 8, 4);
   304→        memcpy(&a_off, old_geo.data() + i * 20 + 12, 4);
   305→        memcpy(&i_off, old_geo.data() + i * 20 + 16, 4);
   306→        cells[i].streets = parse_entry(old_se, s_off);
   307→        cells[i].addrs = parse_entry(old_ae, a_off);
   308→        cells[i].interps = parse_entry(old_ie, i_off);
   309→        remap_ids(cells[i].streets, way_rm);
   310→        remap_ids(cells[i].addrs, addr_rm);
   311→        remap_ids(cells[i].interps, interp_rm);
   312→    }
   313→
   314→    // Write rebuilt files
   315→    RebuiltGeo result;
   316→    uint32_t no_data = 0xFFFFFFFF;
   317→
   318→    auto write_entries = [&](std::vector<char>& buf, const auto& getter) -> std::unordered_map<uint64_t, uint32_t> {
   319→        std::unordered_map<uint64_t, uint32_t> offsets;

    size_t n_cells = old_geo.size() / 20;

    auto parse_entry = [](const std::vector<char>& data, uint32_t off) -> std::vector<uint32_t> {
        if (off == 0xFFFFFFFF || off + 2 > data.size()) return {};
        uint16_t count; memcpy(&count, data.data() + off, 2);
        if (off + 2 + count * 4 > data.size()) return {};
        std::vector<uint32_t> ids(count);
        memcpy(ids.data(), data.data() + off + 2, count * 4);
        return ids;
    };

    auto remap_ids = [](std::vector<uint32_t>& ids, const std::unordered_map<uint32_t,uint32_t>& rm) {
        for (auto& id : ids) {
            auto it = rm.find(id);
            if (it != rm.end()) id = it->second;
        }
        std::sort(ids.begin(), ids.end());
    };

    // Parse all cells and remap IDs
    struct CellData {
        uint64_t cell_id;
        std::vector<uint32_t> streets, addrs, interps;
    };
    std::vector<CellData> cells(n_cells);
    for (size_t i = 0; i < n_cells; i++) {
        memcpy(&cells[i].cell_id, old_geo.data() + i * 20, 8);
        uint32_t s_off, a_off, i_off;
        memcpy(&s_off, old_geo.data() + i * 20 + 8, 4);
        memcpy(&a_off, old_geo.data() + i * 20 + 12, 4);
        memcpy(&i_off, old_geo.data() + i * 20 + 16, 4);
        cells[i].streets = parse_entry(old_se, s_off);
        cells[i].addrs = parse_entry(old_ae, a_off);
        cells[i].interps = parse_entry(old_ie, i_off);
        remap_ids(cells[i].streets, way_rm);
        remap_ids(cells[i].addrs, addr_rm);
        remap_ids(cells[i].interps, interp_rm);
    }

    // Write rebuilt files
    RebuiltGeo result;
    uint32_t no_data = 0xFFFFFFFF;

    auto write_entries = [&](std::vector<char>& buf, const auto& getter) -> std::unordered_map<uint64_t, uint32_t> {
        std::unordered_map<uint64_t, uint32_t> offsets;


Now I need to add cell changes logic after parsing and before writing. After line 312, add the cell set modifications:


Edit

    // Write rebuilt files
    RebuiltGeo result;
    uint32_t no_data = 0xFFFFFFFF;

    // Apply cell set changes (add new cells, remove old cells)
    if (!removed_cells.empty()) {
        std::unordered_set<uint64_t> removed_set(removed_cells.begin(), removed_cells.end());
        cells.erase(std::remove_if(cells.begin(), cells.end(),
            [&](const CellData& c) { return removed_set.count(c.cell_id); }), cells.end());
    }
    if (!added_cells.empty()) {
        for (uint64_t cid : added_cells) {
            CellData cd; cd.cell_id = cid;
            cells.push_back(cd);
        }
        // Re-sort to maintain cell_id order
        std::sort(cells.begin(), cells.end(),
            [](const CellData& a, const CellData& b) { return a.cell_id < b.cell_id; });
    }

    // Write rebuilt files
    RebuiltGeo result;
    uint32_t no_data = 0xFFFFFFFF;