// geocoder-patch v3: Apply custom patch format.
//
// Decompresses zstd transport layer, then:
// 1. Reads string remap, applies to old data files
// 2. Replays merge sequences to reconstruct new data files
// 3. Writes full-replacement entry/cell files
//
// 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 <unistd.h>
#include <vector>

#include "patch_format.h"

// Merge ops (must match diff tool)
enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

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

    // Decompress zstd transport layer
    std::string raw_path = tmpdir + "/patch.raw";
    {
        std::string cmd = "zstd -d '" + patch_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "Failed to decompress patch" << std::endl; return 1;
        }
    }
    auto patch = read_file(raw_path);
    remove(raw_path.c_str());
    std::cerr << "Decompressed patch: " << patch.size() << " bytes" << std::endl;

    size_t pos = 0;
    auto read_u32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, patch.data() + pos, 4); pos += 4; return v; };
    auto read_u64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, patch.data() + pos, 8); pos += 8; return v; };

    // Check header
    if (memcmp(patch.data(), GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch magic" << std::endl; return 1;
    }
    pos = 8;
    uint32_t version = read_u32();
    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }
    uint32_t flags = read_u32();
    (void)flags;

    // Read string remap
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = read_u32();
        if (marker != 0xFFFFFFFE) { std::cerr << "Expected remap section" << std::endl; return 1; }
        uint32_t count = read_u32();
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_off = read_u32(), new_off = read_u32();
            if (old_off != new_off) str_remap[old_off] = new_off;
        }
        std::cerr << "Loaded " << str_remap.size() << " string remap entries (pos=" << pos << ")" << std::endl;
    }

    // Detect strides
    auto detect = [&](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("street_ways.bin", {12, 9});
    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});

    // String remap functions
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> field_offsets;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
        else return;

        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t off : field_offsets) {
                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);
            }
    };

    // ID remaps derived from merge sequences (populated during merge replay)
    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;

    // Buffered correction sections (applied after entry reconstruction)
    struct PendingCorrection { uint32_t file_id; std::string filename; std::vector<char> delta; };
    std::vector<PendingCorrection> pending_corrections;

    // Read cell changes (may appear before end marker)
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;

    // Process sections
    while (pos < patch.size()) {
        uint32_t file_id = read_u32();
        if (file_id == 0xFFFFFFFF) break;

        // Cell change sections
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }

        uint32_t stride = read_u32();
        uint64_t old_size = read_u64(), new_size = read_u64();

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

        if (stride == 0) {
            // Full replacement
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t data_size = read_u64();
            std::vector<char> data(patch.data() + pos, patch.data() + pos + data_size);
            write_file(out_dir + "/" + std::string(filename), data);
            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
            pos += data_size;
            continue;
        }
        if (stride == 0xFE) {
            // Correction section: buffer for processing AFTER entry reconstruction
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t delta_size = read_u64();
            pending_corrections.push_back({file_id, std::string(filename),
                std::vector<char>(patch.data() + pos, patch.data() + pos + delta_size)});
            pos += delta_size;
            std::cerr << "  " << filename << ": correction buffered (" << delta_size << " bytes)" << std::endl;
            continue;
        }

        // Merge sequence: load old file, apply remap + fixups, replay sequence
        auto old_data = read_file(cur_dir + "/" + std::string(filename));
        size_t actual_stride = stride;
        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;

        // Apply string remap to old data
        apply_str_remap(old_data, file_id, actual_stride);

        // Read and apply fixups (node_offset/vertex_offset fixes)
        // Read and apply fixups (offset field patches for matched records)
        uint32_t n_fixups = read_u32();
        for (uint32_t i = 0; i < n_fixups; i++) {
            uint32_t idx = read_u32(), val = read_u32();
            size_t byte_pos = (size_t)idx * actual_stride;
            if (byte_pos + 4 <= old_data.size())
                memcpy(old_data.data() + byte_pos, &val, 4);
        }
        if (n_fixups > 0) std::cerr << "    Applied " << n_fixups << " fixups" << std::endl;

        // Read merge sequence size and replay, tracking old→new ID mapping
        uint64_t seq_size = read_u64();
        std::vector<char> output;
        output.reserve(new_size);
        size_t old_rec = 0, new_rec = 0; // record indices
        size_t old_pos_bytes = 0;
        size_t seq_end = pos + seq_size;

        // ID remap for this file (old_record_index → new_record_index)
        std::vector<uint32_t> id_map;
        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        while (pos < seq_end) {
            uint8_t op = static_cast<uint8_t>(patch[pos]); pos++;
            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;

            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                if (old_pos_bytes + bytes <= old_data.size())
                    output.insert(output.end(), old_data.data() + old_pos_bytes,
                                 old_data.data() + old_pos_bytes + bytes);
                if (track_ids)
                    for (uint32_t k = 0; k < count; k++)
                        if (old_rec + k < id_map.size())
                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
                old_rec += count; new_rec += count;
                old_pos_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                output.insert(output.end(), patch.data() + pos, patch.data() + pos + bytes);
                pos += bytes;
                new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count;
                old_pos_bytes += count * actual_stride;
            }
        }

        write_file(out_dir + "/" + std::string(filename), output);

        // Store ID remap for entry reconstruction
        if (track_ids) {
            derived_id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
        } else {
            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
        }
    }

    // Reconstruct entry/cell files from derived ID remaps (if not provided as full replacement)
    {
        bool have_geo = false, have_admin = false;
        // Check if geo_cells was already written (full replacement)
        auto check = [&](const std::string& name) -> bool {
            auto d = read_file(out_dir + "/" + name);
            return !d.empty();
        };
        have_geo = check("geo_cells.bin");
        have_admin = check("admin_cells.bin");

        if (!have_geo && !derived_id_remaps.empty()) {
            std::cerr << "Reconstructing entry/cell files from derived ID remaps..." << std::endl;
            auto convert = [](const std::vector<uint32_t>& vec) {
                std::unordered_map<uint32_t,uint32_t> m;
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) m[i] = vec[i];
                return m;
            };
            std::unordered_map<uint32_t,uint32_t> w_rm, a_rm, i_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::STREET_WAYS))
                w_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::STREET_WAYS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS))
                a_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::ADDR_POINTS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS))
                i_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::INTERP_WAYS]);

            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,
                                               geo_added, geo_removed);
            write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;
        }

        if (!have_admin && !derived_id_remaps.empty()) {
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;
        }
    }

    // Apply buffered corrections to derived entry files
    for (auto& corr : pending_corrections) {
        std::string derived_path = out_dir + "/" + corr.filename;
        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
        write_file(zst_path, corr.delta);
        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "  " << corr.filename << ": correction FAILED" << std::endl;
        } else {
            auto corrected = read_file(corrected_path);
            write_file(derived_path, corrected);
            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
        }
        remove(zst_path.c_str()); remove(corrected_path.c_str());
    }

    // Rebuild cell index files from corrected entries (only if not already written as full replacement)
    {
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        bool geo_written = !existing_geo.empty();
        bool admin_written = !existing_ac.empty();
        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;
        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;
    }
    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections
    {
        auto se = read_file(out_dir + "/street_entries.bin");
        auto ae = read_file(out_dir + "/addr_entries.bin");
        auto ie = read_file(out_dir + "/interp_entries.bin");
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

        if (!existing_geo.empty()) {
            // Parse the existing (derived) geo_cells for cell IDs, rebuild offsets from corrected entries
            size_t n = existing_geo.size() / 20;

            // Parse each entry file to build cell→offset maps
            // Walk entries sequentially, matching against geo_cells cell order
            auto build_offsets_from_geo = [](const std::vector<char>& geo, size_t off_pos,
                                              const std::vector<char>& old_entries,
                                              const std::vector<char>& new_entries) -> std::vector<char> {
                // The corrected entries file has the right content.
                // We need to map cell_ids to byte offsets in the corrected entries.
                // The cell order is the same as in the derived geo_cells.
                // We just need to recompute offsets.
                return new_entries; // entries are already correct, just need geo_cells offsets
            };

            // Actually: re-walk the derived geo_cells, find each cell's entries in the corrected files
            // Since the cell IDs are the same and entries are in the same order, we just recompute offsets.
            // The corrected entry files have entries in cell_id order (from the rebuild).
            // We can walk them to compute offsets.

            // For each entry type, build cell_id → offset by scanning the entry file
            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
                // Returns a vector of entry start positions
                std::vector<uint32_t> positions;
                uint32_t pos = 0;
                while (pos + 2 <= entries.size()) {
                    positions.push_back(pos);
                    uint16_t count; memcpy(&count, entries.data() + pos, 2);
                    pos += 2 + count * 4;
                }
                return positions;
            };

            auto s_pos = scan_entries(se);
            auto a_pos = scan_entries(ae);
            auto i_pos = scan_entries(ie);

            // The derived geo_cells has cell_ids + offsets into derived entries.
            // The corrected entries have the same cell order but potentially different byte offsets.
            // Rebuild geo_cells with corrected offsets.
            uint32_t no_data = 0xFFFFFFFF;
            size_t si = 0, ai = 0, ii = 0;
            std::vector<char> new_geo;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_geo.data() + c * 20, 8);
                new_geo.insert(new_geo.end(), (char*)&cid, (char*)&cid + 8);

                // For each entry type, check if this cell has data
                uint32_t old_s, old_a, old_i;
                memcpy(&old_s, existing_geo.data() + c * 20 + 8, 4);
                memcpy(&old_a, existing_geo.data() + c * 20 + 12, 4);
                memcpy(&old_i, existing_geo.data() + c * 20 + 16, 4);

                uint32_t new_s = (old_s != no_data && si < s_pos.size()) ? s_pos[si++] : no_data;
                uint32_t new_a = (old_a != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                uint32_t new_i = (old_i != no_data && ii < i_pos.size()) ? i_pos[ii++] : no_data;

                new_geo.insert(new_geo.end(), (char*)&new_s, (char*)&new_s + 4);
                new_geo.insert(new_geo.end(), (char*)&new_a, (char*)&new_a + 4);
                new_geo.insert(new_geo.end(), (char*)&new_i, (char*)&new_i + 4);
            }
            write_file(out_dir + "/geo_cells.bin", new_geo);
            std::cerr << "  Rebuilt geo_cells.bin from corrected entries (" << new_geo.size() << " bytes)" << std::endl;
        }
    }

    // Similarly for admin_cells from admin_entries
    {
        auto ae = read_file(out_dir + "/admin_entries.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        if (!existing_ac.empty()) {
            size_t n = existing_ac.size() / 12;
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<uint32_t> a_pos;
            uint32_t p = 0;
            while (p + 2 <= ae.size()) {
                a_pos.push_back(p);
                uint16_t count; memcpy(&count, ae.data() + p, 2);
                p += 2 + count * 4;
            }
            size_t ai = 0;
            std::vector<char> new_ac;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_ac.data() + c * 12, 8);
                uint32_t old_off; memcpy(&old_off, existing_ac.data() + c * 12 + 8, 4);
                new_ac.insert(new_ac.end(), (char*)&cid, (char*)&cid + 8);
                uint32_t new_off = (old_off != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                new_ac.insert(new_ac.end(), (char*)&new_off, (char*)&new_off + 4);
            }
            write_file(out_dir + "/admin_cells.bin", new_ac);
            std::cerr << "  Rebuilt admin_cells.bin from corrected entries (" << new_ac.size() << " bytes)" << std::endl;
        }
    }

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-        if (!have_geo && !derived_id_remaps.empty()) {

+        if (!derived_id_remaps.empty()) { // always reconstruct entries (needed for corrections)


Bash

cmake --build build -j$(nproc) --target geocoder-diff --target geocoder-patch 2>&1 | grep -E "(error:|Built)" | head -5

Build


[100%] Built target geocoder-diff
[100%] Built target geocoder-patch


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 -rf /home/michtest/patched-final5
/home/michtest/geocoder-patch /home/michtest/det-new /home/michtest/test-final5.gcpatch -o /home/michtest/patched-final5 2>&1 | tail -10
echo ""
all_match=true
for f in /home/michtest/det-today/*.bin; do
    b=$(basename $f); p=/home/michtest/patched-final5/$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"
echo "Patch: $(ls -lh /home/michtest/test-final5.gcpatch | awk "{print \$5}")"
' 2>&1

Retest patch application


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.
Reconstructing entry/cell files from derived ID remaps...
  Rebuilt geo_cells + 3 entry files
  street_entries.bin: corrected → 586625094 bytes
  addr_entries.bin: corrected → 395061190 bytes
  interp_entries.bin: corrected → 159386 bytes
  admin_entries.bin: correction FAILED
  geo_cells.bin: already written (full replacement)
  admin_cells.bin: already written (full replacement)
  Rebuilt admin_cells.bin from corrected entries (1692000 bytes)
Patch applied. Output in /home/michtest/patched-final5

  addr_entries.bin: MATCH
  addr_points.bin: MATCH
  admin_cells.bin: FAIL
  admin_entries.bin: FAIL
  admin_polygons.bin: MATCH
  admin_vertices.bin: MATCH
  geo_cells.bin: FAIL
  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
SOME DIFFER
Patch: 337M


Street entries and addr entries MATCH now! But admin_entries correction failed, and the admin_cells rebuild overwrites the correct full-replacement. Also geo_cells is overwritten by the rebuild.

The issues:
1. admin_entries correction fails (the derived admin entries don't exist yet when correction runs)
2. The geo_cells rebuild (at line ~280) OVERWRITES the full-replacement geo_cells

I need to: (a) not rebuild admin from scratch since we have admin_cells as full replacement, and (b) not rebuild geo_cells since it's full replacement.

Let me also handle the admin reconstruction better — the admin reconstruction needs to also happen (to produce derived admin_entries for the correction), but it shouldn't overwrite admin_cells:


   277→            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
   278→            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
   279→            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
   280→            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;
   281→        }
   282→
   283→        if (!have_admin && !derived_id_remaps.empty()) {
   284→            std::unordered_map<uint32_t,uint32_t> ad_rm;
   285→            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
   286→                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
   287→                for (uint32_t i = 0; i < vec.size(); i++)
   288→                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
   289→            }
   290→            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
   291→            auto old_ae = read_file(cur_dir + "/admin_entries.bin");

            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;
        }

        if (!have_admin && !derived_id_remaps.empty()) {
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_ae = read_file(cur_dir + "/admin_entries.bin");


I see — the geo reconstruction writes geo_cells.bin (line 276), overwriting the full replacement. And admin reconstruction is guarded by `!have_admin`, so it's skipped when admin_cells was written as full replacement.

Let me fix: don't write geo_cells from reconstruction, and always reconstruct admin entries:


Edit

            auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm,
                                               geo_added, geo_removed);
            write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;
        }

        if (!have_admin && !derived_id_remaps.empty()) {

            auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm,
                                               geo_added, geo_removed);
            // Only write entry files (geo_cells may be full replacement)
            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;
        }

        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections


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

            auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm,
                                               geo_added, geo_removed);
            // Only write entry files (geo_cells may be full replacement)
            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;
        }

        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections

// geocoder-patch v3: Apply custom patch format.
//
// Decompresses zstd transport layer, then:
// 1. Reads string remap, applies to old data files
// 2. Replays merge sequences to reconstruct new data files
// 3. Writes full-replacement entry/cell files
//
// 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 <unistd.h>
#include <vector>

#include "patch_format.h"

// Merge ops (must match diff tool)
enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

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

    // Decompress zstd transport layer
    std::string raw_path = tmpdir + "/patch.raw";
    {
        std::string cmd = "zstd -d '" + patch_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "Failed to decompress patch" << std::endl; return 1;
        }
    }
    auto patch = read_file(raw_path);
    remove(raw_path.c_str());
    std::cerr << "Decompressed patch: " << patch.size() << " bytes" << std::endl;

    size_t pos = 0;
    auto read_u32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, patch.data() + pos, 4); pos += 4; return v; };
    auto read_u64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, patch.data() + pos, 8); pos += 8; return v; };

    // Check header
    if (memcmp(patch.data(), GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch magic" << std::endl; return 1;
    }
    pos = 8;
    uint32_t version = read_u32();
    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }
    uint32_t flags = read_u32();
    (void)flags;

    // Read string remap
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = read_u32();
        if (marker != 0xFFFFFFFE) { std::cerr << "Expected remap section" << std::endl; return 1; }
        uint32_t count = read_u32();
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_off = read_u32(), new_off = read_u32();
            if (old_off != new_off) str_remap[old_off] = new_off;
        }
        std::cerr << "Loaded " << str_remap.size() << " string remap entries (pos=" << pos << ")" << std::endl;
    }

    // Detect strides
    auto detect = [&](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("street_ways.bin", {12, 9});
    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});

    // String remap functions
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> field_offsets;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
        else return;

        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t off : field_offsets) {
                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);
            }
    };

    // ID remaps derived from merge sequences (populated during merge replay)
    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;

    // Buffered correction sections (applied after entry reconstruction)
    struct PendingCorrection { uint32_t file_id; std::string filename; std::vector<char> delta; };
    std::vector<PendingCorrection> pending_corrections;

    // Read cell changes (may appear before end marker)
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;

    // Process sections
    while (pos < patch.size()) {
        uint32_t file_id = read_u32();
        if (file_id == 0xFFFFFFFF) break;

        // Cell change sections
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }

        uint32_t stride = read_u32();
        uint64_t old_size = read_u64(), new_size = read_u64();

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

        if (stride == 0) {
            // Full replacement
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t data_size = read_u64();
            std::vector<char> data(patch.data() + pos, patch.data() + pos + data_size);
            write_file(out_dir + "/" + std::string(filename), data);
            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
            pos += data_size;
            continue;
        }
        if (stride == 0xFE) {
            // Correction section: buffer for processing AFTER entry reconstruction
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t delta_size = read_u64();
            pending_corrections.push_back({file_id, std::string(filename),
                std::vector<char>(patch.data() + pos, patch.data() + pos + delta_size)});
            pos += delta_size;
            std::cerr << "  " << filename << ": correction buffered (" << delta_size << " bytes)" << std::endl;
            continue;
        }

        // Merge sequence: load old file, apply remap + fixups, replay sequence
        auto old_data = read_file(cur_dir + "/" + std::string(filename));
        size_t actual_stride = stride;
        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;

        // Apply string remap to old data
        apply_str_remap(old_data, file_id, actual_stride);

        // Read and apply fixups (node_offset/vertex_offset fixes)
        // Read and apply fixups (offset field patches for matched records)
        uint32_t n_fixups = read_u32();
        for (uint32_t i = 0; i < n_fixups; i++) {
            uint32_t idx = read_u32(), val = read_u32();
            size_t byte_pos = (size_t)idx * actual_stride;
            if (byte_pos + 4 <= old_data.size())
                memcpy(old_data.data() + byte_pos, &val, 4);
        }
        if (n_fixups > 0) std::cerr << "    Applied " << n_fixups << " fixups" << std::endl;

        // Read merge sequence size and replay, tracking old→new ID mapping
        uint64_t seq_size = read_u64();
        std::vector<char> output;
        output.reserve(new_size);
        size_t old_rec = 0, new_rec = 0; // record indices
        size_t old_pos_bytes = 0;
        size_t seq_end = pos + seq_size;

        // ID remap for this file (old_record_index → new_record_index)
        std::vector<uint32_t> id_map;
        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        while (pos < seq_end) {
            uint8_t op = static_cast<uint8_t>(patch[pos]); pos++;
            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;

            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                if (old_pos_bytes + bytes <= old_data.size())
                    output.insert(output.end(), old_data.data() + old_pos_bytes,
                                 old_data.data() + old_pos_bytes + bytes);
                if (track_ids)
                    for (uint32_t k = 0; k < count; k++)
                        if (old_rec + k < id_map.size())
                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
                old_rec += count; new_rec += count;
                old_pos_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                output.insert(output.end(), patch.data() + pos, patch.data() + pos + bytes);
                pos += bytes;
                new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count;
                old_pos_bytes += count * actual_stride;
            }
        }

        write_file(out_dir + "/" + std::string(filename), output);

        // Store ID remap for entry reconstruction
        if (track_ids) {
            derived_id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
        } else {
            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
        }
    }

    // Reconstruct entry/cell files from derived ID remaps (if not provided as full replacement)
    {
        bool have_geo = false, have_admin = false;
        // Check if geo_cells was already written (full replacement)
        auto check = [&](const std::string& name) -> bool {
            auto d = read_file(out_dir + "/" + name);
            return !d.empty();
        };
        have_geo = check("geo_cells.bin");
        have_admin = check("admin_cells.bin");

        if (!derived_id_remaps.empty()) { // always reconstruct entries (needed for corrections)
            std::cerr << "Reconstructing entry/cell files from derived ID remaps..." << std::endl;
            auto convert = [](const std::vector<uint32_t>& vec) {
                std::unordered_map<uint32_t,uint32_t> m;
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) m[i] = vec[i];
                return m;
            };
            std::unordered_map<uint32_t,uint32_t> w_rm, a_rm, i_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::STREET_WAYS))
                w_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::STREET_WAYS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS))
                a_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::ADDR_POINTS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS))
                i_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::INTERP_WAYS]);

            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,
                                               geo_added, geo_removed);
            write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;
        }

        if (!have_admin && !derived_id_remaps.empty()) {
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;
        }
    }

    // Apply buffered corrections to derived entry files
    for (auto& corr : pending_corrections) {
        std::string derived_path = out_dir + "/" + corr.filename;
        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
        write_file(zst_path, corr.delta);
        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "  " << corr.filename << ": correction FAILED" << std::endl;
        } else {
            auto corrected = read_file(corrected_path);
            write_file(derived_path, corrected);
            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
        }
        remove(zst_path.c_str()); remove(corrected_path.c_str());
    }

    // Rebuild cell index files from corrected entries (only if not already written as full replacement)
    {
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        bool geo_written = !existing_geo.empty();
        bool admin_written = !existing_ac.empty();
        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;
        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;
    }
    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections
    {
        auto se = read_file(out_dir + "/street_entries.bin");
        auto ae = read_file(out_dir + "/addr_entries.bin");
        auto ie = read_file(out_dir + "/interp_entries.bin");
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

        if (!existing_geo.empty()) {
            // Parse the existing (derived) geo_cells for cell IDs, rebuild offsets from corrected entries
            size_t n = existing_geo.size() / 20;

            // Parse each entry file to build cell→offset maps
            // Walk entries sequentially, matching against geo_cells cell order
            auto build_offsets_from_geo = [](const std::vector<char>& geo, size_t off_pos,
                                              const std::vector<char>& old_entries,
                                              const std::vector<char>& new_entries) -> std::vector<char> {
                // The corrected entries file has the right content.
                // We need to map cell_ids to byte offsets in the corrected entries.
                // The cell order is the same as in the derived geo_cells.
                // We just need to recompute offsets.
                return new_entries; // entries are already correct, just need geo_cells offsets
            };

            // Actually: re-walk the derived geo_cells, find each cell's entries in the corrected files
            // Since the cell IDs are the same and entries are in the same order, we just recompute offsets.
            // The corrected entry files have entries in cell_id order (from the rebuild).
            // We can walk them to compute offsets.

            // For each entry type, build cell_id → offset by scanning the entry file
            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
                // Returns a vector of entry start positions
                std::vector<uint32_t> positions;
                uint32_t pos = 0;
                while (pos + 2 <= entries.size()) {
                    positions.push_back(pos);
                    uint16_t count; memcpy(&count, entries.data() + pos, 2);
                    pos += 2 + count * 4;
                }
                return positions;
            };

            auto s_pos = scan_entries(se);
            auto a_pos = scan_entries(ae);
            auto i_pos = scan_entries(ie);

            // The derived geo_cells has cell_ids + offsets into derived entries.
            // The corrected entries have the same cell order but potentially different byte offsets.
            // Rebuild geo_cells with corrected offsets.
            uint32_t no_data = 0xFFFFFFFF;
            size_t si = 0, ai = 0, ii = 0;
            std::vector<char> new_geo;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_geo.data() + c * 20, 8);
                new_geo.insert(new_geo.end(), (char*)&cid, (char*)&cid + 8);

                // For each entry type, check if this cell has data
                uint32_t old_s, old_a, old_i;
                memcpy(&old_s, existing_geo.data() + c * 20 + 8, 4);
                memcpy(&old_a, existing_geo.data() + c * 20 + 12, 4);
                memcpy(&old_i, existing_geo.data() + c * 20 + 16, 4);

                uint32_t new_s = (old_s != no_data && si < s_pos.size()) ? s_pos[si++] : no_data;
                uint32_t new_a = (old_a != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                uint32_t new_i = (old_i != no_data && ii < i_pos.size()) ? i_pos[ii++] : no_data;

                new_geo.insert(new_geo.end(), (char*)&new_s, (char*)&new_s + 4);
                new_geo.insert(new_geo.end(), (char*)&new_a, (char*)&new_a + 4);
                new_geo.insert(new_geo.end(), (char*)&new_i, (char*)&new_i + 4);
            }
            write_file(out_dir + "/geo_cells.bin", new_geo);
            std::cerr << "  Rebuilt geo_cells.bin from corrected entries (" << new_geo.size() << " bytes)" << std::endl;
        }
    }

    // Similarly for admin_cells from admin_entries
    {
        auto ae = read_file(out_dir + "/admin_entries.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        if (!existing_ac.empty()) {
            size_t n = existing_ac.size() / 12;
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<uint32_t> a_pos;
            uint32_t p = 0;
            while (p + 2 <= ae.size()) {
                a_pos.push_back(p);
                uint16_t count; memcpy(&count, ae.data() + p, 2);
                p += 2 + count * 4;
            }
            size_t ai = 0;
            std::vector<char> new_ac;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_ac.data() + c * 12, 8);
                uint32_t old_off; memcpy(&old_off, existing_ac.data() + c * 12 + 8, 4);
                new_ac.insert(new_ac.end(), (char*)&cid, (char*)&cid + 8);
                uint32_t new_off = (old_off != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                new_ac.insert(new_ac.end(), (char*)&new_off, (char*)&new_off + 4);
            }
            write_file(out_dir + "/admin_cells.bin", new_ac);
            std::cerr << "  Rebuilt admin_cells.bin from corrected entries (" << new_ac.size() << " bytes)" << std::endl;
        }
    }

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-            write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);

+            // Only write entry files (geo_cells may be full replacement)

+            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);

-            std::cerr << "  Rebuilt geo_cells + 3 entry files" << std::endl;

+            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;

-        if (!have_admin && !derived_id_remaps.empty()) {

+        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections


Also prevent admin_cells from being overwritten:


   292→            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
   293→            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
   294→            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
   295→            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
   296→            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;
   297→        }
   298→    }
   299→

            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;
        }
    }


            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;

            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            if (!have_admin) write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_entries" << (have_admin ? "" : " + admin_cells") << std::endl;


            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            if (!have_admin) write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_entries" << (have_admin ? "" : " + admin_cells") << std::endl;

// geocoder-patch v3: Apply custom patch format.
//
// Decompresses zstd transport layer, then:
// 1. Reads string remap, applies to old data files
// 2. Replays merge sequences to reconstruct new data files
// 3. Writes full-replacement entry/cell files
//
// 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 <unistd.h>
#include <vector>

#include "patch_format.h"

// Merge ops (must match diff tool)
enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

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

    // Decompress zstd transport layer
    std::string raw_path = tmpdir + "/patch.raw";
    {
        std::string cmd = "zstd -d '" + patch_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "Failed to decompress patch" << std::endl; return 1;
        }
    }
    auto patch = read_file(raw_path);
    remove(raw_path.c_str());
    std::cerr << "Decompressed patch: " << patch.size() << " bytes" << std::endl;

    size_t pos = 0;
    auto read_u32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, patch.data() + pos, 4); pos += 4; return v; };
    auto read_u64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, patch.data() + pos, 8); pos += 8; return v; };

    // Check header
    if (memcmp(patch.data(), GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch magic" << std::endl; return 1;
    }
    pos = 8;
    uint32_t version = read_u32();
    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }
    uint32_t flags = read_u32();
    (void)flags;

    // Read string remap
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = read_u32();
        if (marker != 0xFFFFFFFE) { std::cerr << "Expected remap section" << std::endl; return 1; }
        uint32_t count = read_u32();
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_off = read_u32(), new_off = read_u32();
            if (old_off != new_off) str_remap[old_off] = new_off;
        }
        std::cerr << "Loaded " << str_remap.size() << " string remap entries (pos=" << pos << ")" << std::endl;
    }

    // Detect strides
    auto detect = [&](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("street_ways.bin", {12, 9});
    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});

    // String remap functions
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> field_offsets;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
        else return;

        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t off : field_offsets) {
                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);
            }
    };

    // ID remaps derived from merge sequences (populated during merge replay)
    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;

    // Buffered correction sections (applied after entry reconstruction)
    struct PendingCorrection { uint32_t file_id; std::string filename; std::vector<char> delta; };
    std::vector<PendingCorrection> pending_corrections;

    // Read cell changes (may appear before end marker)
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;

    // Process sections
    while (pos < patch.size()) {
        uint32_t file_id = read_u32();
        if (file_id == 0xFFFFFFFF) break;

        // Cell change sections
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }

        uint32_t stride = read_u32();
        uint64_t old_size = read_u64(), new_size = read_u64();

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

        if (stride == 0) {
            // Full replacement
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t data_size = read_u64();
            std::vector<char> data(patch.data() + pos, patch.data() + pos + data_size);
            write_file(out_dir + "/" + std::string(filename), data);
            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
            pos += data_size;
            continue;
        }
        if (stride == 0xFE) {
            // Correction section: buffer for processing AFTER entry reconstruction
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t delta_size = read_u64();
            pending_corrections.push_back({file_id, std::string(filename),
                std::vector<char>(patch.data() + pos, patch.data() + pos + delta_size)});
            pos += delta_size;
            std::cerr << "  " << filename << ": correction buffered (" << delta_size << " bytes)" << std::endl;
            continue;
        }

        // Merge sequence: load old file, apply remap + fixups, replay sequence
        auto old_data = read_file(cur_dir + "/" + std::string(filename));
        size_t actual_stride = stride;
        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;

        // Apply string remap to old data
        apply_str_remap(old_data, file_id, actual_stride);

        // Read and apply fixups (node_offset/vertex_offset fixes)
        // Read and apply fixups (offset field patches for matched records)
        uint32_t n_fixups = read_u32();
        for (uint32_t i = 0; i < n_fixups; i++) {
            uint32_t idx = read_u32(), val = read_u32();
            size_t byte_pos = (size_t)idx * actual_stride;
            if (byte_pos + 4 <= old_data.size())
                memcpy(old_data.data() + byte_pos, &val, 4);
        }
        if (n_fixups > 0) std::cerr << "    Applied " << n_fixups << " fixups" << std::endl;

        // Read merge sequence size and replay, tracking old→new ID mapping
        uint64_t seq_size = read_u64();
        std::vector<char> output;
        output.reserve(new_size);
        size_t old_rec = 0, new_rec = 0; // record indices
        size_t old_pos_bytes = 0;
        size_t seq_end = pos + seq_size;

        // ID remap for this file (old_record_index → new_record_index)
        std::vector<uint32_t> id_map;
        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        while (pos < seq_end) {
            uint8_t op = static_cast<uint8_t>(patch[pos]); pos++;
            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;

            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                if (old_pos_bytes + bytes <= old_data.size())
                    output.insert(output.end(), old_data.data() + old_pos_bytes,
                                 old_data.data() + old_pos_bytes + bytes);
                if (track_ids)
                    for (uint32_t k = 0; k < count; k++)
                        if (old_rec + k < id_map.size())
                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
                old_rec += count; new_rec += count;
                old_pos_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                output.insert(output.end(), patch.data() + pos, patch.data() + pos + bytes);
                pos += bytes;
                new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count;
                old_pos_bytes += count * actual_stride;
            }
        }

        write_file(out_dir + "/" + std::string(filename), output);

        // Store ID remap for entry reconstruction
        if (track_ids) {
            derived_id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
        } else {
            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
        }
    }

    // Reconstruct entry/cell files from derived ID remaps (if not provided as full replacement)
    {
        bool have_geo = false, have_admin = false;
        // Check if geo_cells was already written (full replacement)
        auto check = [&](const std::string& name) -> bool {
            auto d = read_file(out_dir + "/" + name);
            return !d.empty();
        };
        have_geo = check("geo_cells.bin");
        have_admin = check("admin_cells.bin");

        if (!derived_id_remaps.empty()) { // always reconstruct entries (needed for corrections)
            std::cerr << "Reconstructing entry/cell files from derived ID remaps..." << std::endl;
            auto convert = [](const std::vector<uint32_t>& vec) {
                std::unordered_map<uint32_t,uint32_t> m;
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) m[i] = vec[i];
                return m;
            };
            std::unordered_map<uint32_t,uint32_t> w_rm, a_rm, i_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::STREET_WAYS))
                w_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::STREET_WAYS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS))
                a_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::ADDR_POINTS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS))
                i_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::INTERP_WAYS]);

            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,
                                               geo_added, geo_removed);
            // Only write entry files (geo_cells may be full replacement)
            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;
        }

        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;
        }
    }

    // Apply buffered corrections to derived entry files
    for (auto& corr : pending_corrections) {
        std::string derived_path = out_dir + "/" + corr.filename;
        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
        write_file(zst_path, corr.delta);
        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "  " << corr.filename << ": correction FAILED" << std::endl;
        } else {
            auto corrected = read_file(corrected_path);
            write_file(derived_path, corrected);
            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
        }
        remove(zst_path.c_str()); remove(corrected_path.c_str());
    }

    // Rebuild cell index files from corrected entries (only if not already written as full replacement)
    {
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        bool geo_written = !existing_geo.empty();
        bool admin_written = !existing_ac.empty();
        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;
        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;
    }
    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections
    {
        auto se = read_file(out_dir + "/street_entries.bin");
        auto ae = read_file(out_dir + "/addr_entries.bin");
        auto ie = read_file(out_dir + "/interp_entries.bin");
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

        if (!existing_geo.empty()) {
            // Parse the existing (derived) geo_cells for cell IDs, rebuild offsets from corrected entries
            size_t n = existing_geo.size() / 20;

            // Parse each entry file to build cell→offset maps
            // Walk entries sequentially, matching against geo_cells cell order
            auto build_offsets_from_geo = [](const std::vector<char>& geo, size_t off_pos,
                                              const std::vector<char>& old_entries,
                                              const std::vector<char>& new_entries) -> std::vector<char> {
                // The corrected entries file has the right content.
                // We need to map cell_ids to byte offsets in the corrected entries.
                // The cell order is the same as in the derived geo_cells.
                // We just need to recompute offsets.
                return new_entries; // entries are already correct, just need geo_cells offsets
            };

            // Actually: re-walk the derived geo_cells, find each cell's entries in the corrected files
            // Since the cell IDs are the same and entries are in the same order, we just recompute offsets.
            // The corrected entry files have entries in cell_id order (from the rebuild).
            // We can walk them to compute offsets.

            // For each entry type, build cell_id → offset by scanning the entry file
            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
                // Returns a vector of entry start positions
                std::vector<uint32_t> positions;
                uint32_t pos = 0;
                while (pos + 2 <= entries.size()) {
                    positions.push_back(pos);
                    uint16_t count; memcpy(&count, entries.data() + pos, 2);
                    pos += 2 + count * 4;
                }
                return positions;
            };

            auto s_pos = scan_entries(se);
            auto a_pos = scan_entries(ae);
            auto i_pos = scan_entries(ie);

            // The derived geo_cells has cell_ids + offsets into derived entries.
            // The corrected entries have the same cell order but potentially different byte offsets.
            // Rebuild geo_cells with corrected offsets.
            uint32_t no_data = 0xFFFFFFFF;
            size_t si = 0, ai = 0, ii = 0;
            std::vector<char> new_geo;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_geo.data() + c * 20, 8);
                new_geo.insert(new_geo.end(), (char*)&cid, (char*)&cid + 8);

                // For each entry type, check if this cell has data
                uint32_t old_s, old_a, old_i;
                memcpy(&old_s, existing_geo.data() + c * 20 + 8, 4);
                memcpy(&old_a, existing_geo.data() + c * 20 + 12, 4);
                memcpy(&old_i, existing_geo.data() + c * 20 + 16, 4);

                uint32_t new_s = (old_s != no_data && si < s_pos.size()) ? s_pos[si++] : no_data;
                uint32_t new_a = (old_a != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                uint32_t new_i = (old_i != no_data && ii < i_pos.size()) ? i_pos[ii++] : no_data;

                new_geo.insert(new_geo.end(), (char*)&new_s, (char*)&new_s + 4);
                new_geo.insert(new_geo.end(), (char*)&new_a, (char*)&new_a + 4);
                new_geo.insert(new_geo.end(), (char*)&new_i, (char*)&new_i + 4);
            }
            write_file(out_dir + "/geo_cells.bin", new_geo);
            std::cerr << "  Rebuilt geo_cells.bin from corrected entries (" << new_geo.size() << " bytes)" << std::endl;
        }
    }

    // Similarly for admin_cells from admin_entries
    {
        auto ae = read_file(out_dir + "/admin_entries.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        if (!existing_ac.empty()) {
            size_t n = existing_ac.size() / 12;
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<uint32_t> a_pos;
            uint32_t p = 0;
            while (p + 2 <= ae.size()) {
                a_pos.push_back(p);
                uint16_t count; memcpy(&count, ae.data() + p, 2);
                p += 2 + count * 4;
            }
            size_t ai = 0;
            std::vector<char> new_ac;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_ac.data() + c * 12, 8);
                uint32_t old_off; memcpy(&old_off, existing_ac.data() + c * 12 + 8, 4);
                new_ac.insert(new_ac.end(), (char*)&cid, (char*)&cid + 8);
                uint32_t new_off = (old_off != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                new_ac.insert(new_ac.end(), (char*)&new_off, (char*)&new_off + 4);
            }
            write_file(out_dir + "/admin_cells.bin", new_ac);
            std::cerr << "  Rebuilt admin_cells.bin from corrected entries (" << new_ac.size() << " bytes)" << std::endl;
        }
    }

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);

+            if (!have_admin) write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);

-            std::cerr << "  Rebuilt admin_cells + admin_entries" << std::endl;

+            std::cerr << "  Rebuilt admin_entries" << (have_admin ? "" : " + admin_cells") << std::endl;


Also remove the post-correction geo_cells/admin_cells rebuild that was overwriting full replacements:


    // Rebuild cell index files from corrected entries (only if not already written as full replacement)
    {
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        bool geo_written = !existing_geo.empty();
        bool admin_written = !existing_ac.empty();
        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;
        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;
    }
    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections
    {

    // Post-correction cell index rebuild: disabled when cells are full replacement
    if (false) {


    // Post-correction cell index rebuild: disabled when cells are full replacement
    if (false) {

// geocoder-patch v3: Apply custom patch format.
//
// Decompresses zstd transport layer, then:
// 1. Reads string remap, applies to old data files
// 2. Replays merge sequences to reconstruct new data files
// 3. Writes full-replacement entry/cell files
//
// 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 <unistd.h>
#include <vector>

#include "patch_format.h"

// Merge ops (must match diff tool)
enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

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

    // Decompress zstd transport layer
    std::string raw_path = tmpdir + "/patch.raw";
    {
        std::string cmd = "zstd -d '" + patch_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "Failed to decompress patch" << std::endl; return 1;
        }
    }
    auto patch = read_file(raw_path);
    remove(raw_path.c_str());
    std::cerr << "Decompressed patch: " << patch.size() << " bytes" << std::endl;

    size_t pos = 0;
    auto read_u32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, patch.data() + pos, 4); pos += 4; return v; };
    auto read_u64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, patch.data() + pos, 8); pos += 8; return v; };

    // Check header
    if (memcmp(patch.data(), GCPATCH_MAGIC, 8) != 0) {
        std::cerr << "Invalid patch magic" << std::endl; return 1;
    }
    pos = 8;
    uint32_t version = read_u32();
    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }
    uint32_t flags = read_u32();
    (void)flags;

    // Read string remap
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = read_u32();
        if (marker != 0xFFFFFFFE) { std::cerr << "Expected remap section" << std::endl; return 1; }
        uint32_t count = read_u32();
        for (uint32_t i = 0; i < count; i++) {
            uint32_t old_off = read_u32(), new_off = read_u32();
            if (old_off != new_off) str_remap[old_off] = new_off;
        }
        std::cerr << "Loaded " << str_remap.size() << " string remap entries (pos=" << pos << ")" << std::endl;
    }

    // Detect strides
    auto detect = [&](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("street_ways.bin", {12, 9});
    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});

    // String remap functions
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> field_offsets;
        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
        else return;

        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t off : field_offsets) {
                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);
            }
    };

    // ID remaps derived from merge sequences (populated during merge replay)
    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;

    // Buffered correction sections (applied after entry reconstruction)
    struct PendingCorrection { uint32_t file_id; std::string filename; std::vector<char> delta; };
    std::vector<PendingCorrection> pending_corrections;

    // Read cell changes (may appear before end marker)
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;

    // Process sections
    while (pos < patch.size()) {
        uint32_t file_id = read_u32();
        if (file_id == 0xFFFFFFFF) break;

        // Cell change sections
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }

        uint32_t stride = read_u32();
        uint64_t old_size = read_u64(), new_size = read_u64();

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

        if (stride == 0) {
            // Full replacement
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t data_size = read_u64();
            std::vector<char> data(patch.data() + pos, patch.data() + pos + data_size);
            write_file(out_dir + "/" + std::string(filename), data);
            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
            pos += data_size;
            continue;
        }
        if (stride == 0xFE) {
            // Correction section: buffer for processing AFTER entry reconstruction
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t delta_size = read_u64();
            pending_corrections.push_back({file_id, std::string(filename),
                std::vector<char>(patch.data() + pos, patch.data() + pos + delta_size)});
            pos += delta_size;
            std::cerr << "  " << filename << ": correction buffered (" << delta_size << " bytes)" << std::endl;
            continue;
        }

        // Merge sequence: load old file, apply remap + fixups, replay sequence
        auto old_data = read_file(cur_dir + "/" + std::string(filename));
        size_t actual_stride = stride;
        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;

        // Apply string remap to old data
        apply_str_remap(old_data, file_id, actual_stride);

        // Read and apply fixups (node_offset/vertex_offset fixes)
        // Read and apply fixups (offset field patches for matched records)
        uint32_t n_fixups = read_u32();
        for (uint32_t i = 0; i < n_fixups; i++) {
            uint32_t idx = read_u32(), val = read_u32();
            size_t byte_pos = (size_t)idx * actual_stride;
            if (byte_pos + 4 <= old_data.size())
                memcpy(old_data.data() + byte_pos, &val, 4);
        }
        if (n_fixups > 0) std::cerr << "    Applied " << n_fixups << " fixups" << std::endl;

        // Read merge sequence size and replay, tracking old→new ID mapping
        uint64_t seq_size = read_u64();
        std::vector<char> output;
        output.reserve(new_size);
        size_t old_rec = 0, new_rec = 0; // record indices
        size_t old_pos_bytes = 0;
        size_t seq_end = pos + seq_size;

        // ID remap for this file (old_record_index → new_record_index)
        std::vector<uint32_t> id_map;
        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        while (pos < seq_end) {
            uint8_t op = static_cast<uint8_t>(patch[pos]); pos++;
            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;

            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                if (old_pos_bytes + bytes <= old_data.size())
                    output.insert(output.end(), old_data.data() + old_pos_bytes,
                                 old_data.data() + old_pos_bytes + bytes);
                if (track_ids)
                    for (uint32_t k = 0; k < count; k++)
                        if (old_rec + k < id_map.size())
                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
                old_rec += count; new_rec += count;
                old_pos_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                output.insert(output.end(), patch.data() + pos, patch.data() + pos + bytes);
                pos += bytes;
                new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count;
                old_pos_bytes += count * actual_stride;
            }
        }

        write_file(out_dir + "/" + std::string(filename), output);

        // Store ID remap for entry reconstruction
        if (track_ids) {
            derived_id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
        } else {
            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
        }
    }

    // Reconstruct entry/cell files from derived ID remaps (if not provided as full replacement)
    {
        bool have_geo = false, have_admin = false;
        // Check if geo_cells was already written (full replacement)
        auto check = [&](const std::string& name) -> bool {
            auto d = read_file(out_dir + "/" + name);
            return !d.empty();
        };
        have_geo = check("geo_cells.bin");
        have_admin = check("admin_cells.bin");

        if (!derived_id_remaps.empty()) { // always reconstruct entries (needed for corrections)
            std::cerr << "Reconstructing entry/cell files from derived ID remaps..." << std::endl;
            auto convert = [](const std::vector<uint32_t>& vec) {
                std::unordered_map<uint32_t,uint32_t> m;
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) m[i] = vec[i];
                return m;
            };
            std::unordered_map<uint32_t,uint32_t> w_rm, a_rm, i_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::STREET_WAYS))
                w_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::STREET_WAYS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS))
                a_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::ADDR_POINTS]);
            if (derived_id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS))
                i_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::INTERP_WAYS]);

            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,
                                               geo_added, geo_removed);
            // Only write entry files (geo_cells may be full replacement)
            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
            write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
            write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
            write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;
        }

        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
                for (uint32_t i = 0; i < vec.size(); i++)
                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm);
            if (!have_admin) write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
            write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            std::cerr << "  Rebuilt admin_entries" << (have_admin ? "" : " + admin_cells") << std::endl;
        }
    }

    // Apply buffered corrections to derived entry files
    for (auto& corr : pending_corrections) {
        std::string derived_path = out_dir + "/" + corr.filename;
        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
        write_file(zst_path, corr.delta);
        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0) {
            std::cerr << "  " << corr.filename << ": correction FAILED" << std::endl;
        } else {
            auto corrected = read_file(corrected_path);
            write_file(derived_path, corrected);
            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
        }
        remove(zst_path.c_str()); remove(corrected_path.c_str());
    }

    // Rebuild cell index files from corrected entries (only if not already written as full replacement)
    {
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        bool geo_written = !existing_geo.empty();
        bool admin_written = !existing_ac.empty();
        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;
        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;
    }
    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections
    {
        auto se = read_file(out_dir + "/street_entries.bin");
        auto ae = read_file(out_dir + "/addr_entries.bin");
        auto ie = read_file(out_dir + "/interp_entries.bin");
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

        if (!existing_geo.empty()) {
            // Parse the existing (derived) geo_cells for cell IDs, rebuild offsets from corrected entries
            size_t n = existing_geo.size() / 20;

            // Parse each entry file to build cell→offset maps
            // Walk entries sequentially, matching against geo_cells cell order
            auto build_offsets_from_geo = [](const std::vector<char>& geo, size_t off_pos,
                                              const std::vector<char>& old_entries,
                                              const std::vector<char>& new_entries) -> std::vector<char> {
                // The corrected entries file has the right content.
                // We need to map cell_ids to byte offsets in the corrected entries.
                // The cell order is the same as in the derived geo_cells.
                // We just need to recompute offsets.
                return new_entries; // entries are already correct, just need geo_cells offsets
            };

            // Actually: re-walk the derived geo_cells, find each cell's entries in the corrected files
            // Since the cell IDs are the same and entries are in the same order, we just recompute offsets.
            // The corrected entry files have entries in cell_id order (from the rebuild).
            // We can walk them to compute offsets.

            // For each entry type, build cell_id → offset by scanning the entry file
            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
                // Returns a vector of entry start positions
                std::vector<uint32_t> positions;
                uint32_t pos = 0;
                while (pos + 2 <= entries.size()) {
                    positions.push_back(pos);
                    uint16_t count; memcpy(&count, entries.data() + pos, 2);
                    pos += 2 + count * 4;
                }
                return positions;
            };

            auto s_pos = scan_entries(se);
            auto a_pos = scan_entries(ae);
            auto i_pos = scan_entries(ie);

            // The derived geo_cells has cell_ids + offsets into derived entries.
            // The corrected entries have the same cell order but potentially different byte offsets.
            // Rebuild geo_cells with corrected offsets.
            uint32_t no_data = 0xFFFFFFFF;
            size_t si = 0, ai = 0, ii = 0;
            std::vector<char> new_geo;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_geo.data() + c * 20, 8);
                new_geo.insert(new_geo.end(), (char*)&cid, (char*)&cid + 8);

                // For each entry type, check if this cell has data
                uint32_t old_s, old_a, old_i;
                memcpy(&old_s, existing_geo.data() + c * 20 + 8, 4);
                memcpy(&old_a, existing_geo.data() + c * 20 + 12, 4);
                memcpy(&old_i, existing_geo.data() + c * 20 + 16, 4);

                uint32_t new_s = (old_s != no_data && si < s_pos.size()) ? s_pos[si++] : no_data;
                uint32_t new_a = (old_a != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                uint32_t new_i = (old_i != no_data && ii < i_pos.size()) ? i_pos[ii++] : no_data;

                new_geo.insert(new_geo.end(), (char*)&new_s, (char*)&new_s + 4);
                new_geo.insert(new_geo.end(), (char*)&new_a, (char*)&new_a + 4);
                new_geo.insert(new_geo.end(), (char*)&new_i, (char*)&new_i + 4);
            }
            write_file(out_dir + "/geo_cells.bin", new_geo);
            std::cerr << "  Rebuilt geo_cells.bin from corrected entries (" << new_geo.size() << " bytes)" << std::endl;
        }
    }

    // Similarly for admin_cells from admin_entries
    {
        auto ae = read_file(out_dir + "/admin_entries.bin");
        auto existing_ac = read_file(out_dir + "/admin_cells.bin");
        if (!existing_ac.empty()) {
            size_t n = existing_ac.size() / 12;
            uint32_t no_data = 0xFFFFFFFF;
            std::vector<uint32_t> a_pos;
            uint32_t p = 0;
            while (p + 2 <= ae.size()) {
                a_pos.push_back(p);
                uint16_t count; memcpy(&count, ae.data() + p, 2);
                p += 2 + count * 4;
            }
            size_t ai = 0;
            std::vector<char> new_ac;
            for (size_t c = 0; c < n; c++) {
                uint64_t cid; memcpy(&cid, existing_ac.data() + c * 12, 8);
                uint32_t old_off; memcpy(&old_off, existing_ac.data() + c * 12 + 8, 4);
                new_ac.insert(new_ac.end(), (char*)&cid, (char*)&cid + 8);
                uint32_t new_off = (old_off != no_data && ai < a_pos.size()) ? a_pos[ai++] : no_data;
                new_ac.insert(new_ac.end(), (char*)&new_off, (char*)&new_off + 4);
            }
            write_file(out_dir + "/admin_cells.bin", new_ac);
            std::cerr << "  Rebuilt admin_cells.bin from corrected entries (" << new_ac.size() << " bytes)" << std::endl;
        }
    }

    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-    // Rebuild cell index files from corrected entries (only if not already written as full replacement)

-        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

-        auto existing_ac = read_file(out_dir + "/admin_cells.bin");

-        bool geo_written = !existing_geo.empty();

-        bool admin_written = !existing_ac.empty();

-        if (geo_written) std::cerr << "  geo_cells.bin: already written (full replacement)" << std::endl;

-        if (admin_written) std::cerr << "  admin_cells.bin: already written (full replacement)" << std::endl;

-    if (false) // disabled: geo_cells/admin_cells now handled as full replacement or corrections

+    // Post-correction cell index rebuild: disabled when cells are full replacement


Bash

cmake --build build -j$(nproc) --target geocoder-patch 2>&1 | grep -E "(error:|Built)" | head -5

Build


[100%] Built target geocoder-patch


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-patch michtest@100.113.88.121:~/geocoder-patch 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 -rf /home/michtest/patched-final5
/home/michtest/geocoder-patch /home/michtest/det-new /home/michtest/test-final5.gcpatch -o /home/michtest/patched-final5 2>&1 | tail -10
echo ""
all_match=true
for f in /home/michtest/det-today/*.bin; do
    b=$(basename $f); p=/home/michtest/patched-final5/$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"
echo "Patch: $(ls -lh /home/michtest/test-final5.gcpatch | awk "{print \$5}")"
' 2>&1

Retest