            if (++cell_count % 5000000 == 0) {
                // Also check actual file sizes vs tracked positions
                long se_actual = ftell(f_se), ae_actual = ftell(f_ae), ie_actual = ftell(f_ie);
                std::cerr << "    " << cell_count << "/" << cells.size()
                          << " se_pos=" << se_pos << " se_ftell=" << se_actual
                          << " ae_pos=" << ae_pos << " ae_ftell=" << ae_actual
                          << " rss=" << get_rss_mb() << "M" << std::endl;
            }

// geocoder-patch v4: Low-memory streaming patch application.
//
// Key design: mmap old files (zero copy), stream output via fwrite (never accumulate),
// process entry pipeline cell-by-cell. Target: <1 GiB peak RSS for planet.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <malloc.h>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unordered_map>
#include <unordered_set>
#include <unistd.h>
#include <vector>

#include "patch_format.h"

enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

// --- Helpers ---
static double now_ms() {
    struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
static size_t get_rss_mb() {
    FILE* f = fopen("/proc/self/statm", "r");
    if (!f) return 0;
    size_t dummy, rss; fscanf(f, "%zu %zu", &dummy, &rss); fclose(f);
    return rss * 4096 / (1024*1024);
}
static void log_phase(const char* label, double start) {
    std::cerr << "  [" << std::fixed << std::setprecision(1) << (now_ms() - start) / 1000.0
              << "s, " << get_rss_mb() << " MiB] " << label << std::endl;
}

// mmap a file read-only. Returns {pointer, size}. Caller must munmap.
struct MappedFile { const char* data; size_t size; };
static MappedFile mmap_file(const std::string& path) {
    int fd = open(path.c_str(), O_RDONLY);
    if (fd < 0) return {nullptr, 0};
    struct stat st; fstat(fd, &st);
    size_t sz = st.st_size;
    if (sz == 0) { close(fd); return {nullptr, 0}; }
    void* p = mmap(nullptr, sz, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (p == MAP_FAILED) return {nullptr, 0};
    return {static_cast<const char*>(p), sz};
}
static void unmap_file(MappedFile& f) {
    if (f.data) { munmap(const_cast<char*>(f.data), f.size); f.data = nullptr; f.size = 0; }
}

// Detect stride from file size
static size_t detect_stride(const std::string& path, std::initializer_list<size_t> candidates) {
    struct stat st; if (stat(path.c_str(), &st) != 0) return *candidates.begin();
    for (size_t s : candidates) if (st.st_size % s == 0 && st.st_size > 0) return s;
    return *candidates.begin();
}

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);
    double t_start = now_ms();

    // --- Phase 1: Decompress + mmap patch ---
    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" << std::endl; return 1; }
    }
    MappedFile patch_map = mmap_file(raw_path);
    if (!patch_map.data) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }
    remove(raw_path.c_str());
    const char* P = patch_map.data;
    size_t patch_size = patch_map.size;
    std::cerr << "Patch: " << patch_size << " bytes" << std::endl;
    log_phase("Decompress", t_start);

    size_t pos = 0;
    auto ru32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, P+pos, 4); pos += 4; return v; };
    auto ru64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, P+pos, 8); pos += 8; return v; };

    // Header
    if (memcmp(P, GCPATCH_MAGIC, 8) != 0) { std::cerr << "Bad magic" << std::endl; return 1; }
    pos = 8;
    uint32_t ver = ru32(); if (ver != 2) { std::cerr << "Bad version" << std::endl; return 1; }
    ru32(); // flags

    // --- Phase 2: String rebuild ---
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = ru32();
        if (marker == 0xFFFFFFF7) {
            uint32_t n_added = ru32(), n_deleted = ru32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) { const char* s = P+pos; added.push_back(s); pos += strlen(s)+1; }
            std::vector<uint32_t> del_idx(n_deleted);
            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = ru32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size) {
                old_strs.push_back({(uint32_t)sp, std::string(old_pool.data + sp)});
                sp += strlen(old_pool.data + sp) + 1;
            }
            std::vector<std::string> merged;
            for (size_t i = 0; i < old_strs.size(); i++)
                if (!del_set.count(i)) merged.push_back(old_strs[i].second);
            for (auto& s : added) merged.push_back(s);
            std::sort(merged.begin(), merged.end());

            // Write new pool and build remap
            FILE* fp = fopen((out_dir + "/strings.bin").c_str(), "wb");
            std::unordered_map<std::string, uint32_t> new_offs;
            uint32_t wpos = 0;
            for (auto& s : merged) {
                new_offs[s] = wpos;
                fwrite(s.c_str(), 1, s.size() + 1, fp);
                wpos += s.size() + 1;
            }
            fclose(fp);
            for (auto& [old_off, s] : old_strs) {
                auto it = new_offs.find(s);
                if (it != new_offs.end() && it->second != old_off) str_remap[old_off] = it->second;
            }
            unmap_file(old_pool);
            std::cerr << "  Strings: +" << n_added << " -" << n_deleted << " → " << merged.size()
                      << ", " << str_remap.size() << " remapped" << std::endl;
            marker = ru32();
        }
        if (marker == 0xFFFFFFFE) { uint32_t c = ru32(); for (uint32_t i = 0; i < c; i++) { uint32_t a = ru32(), b = ru32(); str_remap[a] = b; } }
        else pos -= 4;
    }
    log_phase("Strings", t_start);

    // Detect strides
    size_t way_stride = detect_stride(cur_dir + "/street_ways.bin", {12, 9});
    size_t interp_stride = detect_stride(cur_dir + "/interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect_stride(cur_dir + "/admin_polygons.bin", {24, 20, 19});

    // String remap applier (works on mutable copy)
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t fid, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> offs;
        if (fid == (uint32_t)PatchFileId::ADDR_POINTS) offs = {8, 12};
        else if (fid == (uint32_t)PatchFileId::STREET_WAYS) offs = {(stride == 12) ? 8ul : 5ul};
        else if (fid == (uint32_t)PatchFileId::INTERP_WAYS) offs = {(stride >= 20) ? 8ul : 5ul};
        else if (fid == (uint32_t)PatchFileId::ADMIN_POLYGONS) offs = {8};
        else return;
        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t o : offs) { uint32_t v; memcpy(&v, data.data()+i+o, 4); auto it = str_remap.find(v); if (it != str_remap.end()) memcpy(data.data()+i+o, &it->second, 4); }
    };

    // --- Phase 3: Merge replays (streaming output) ---
    // ID remaps: file_id → vector<uint32_t> where remap[old_idx] = new_idx
    std::unordered_map<uint32_t, std::vector<uint32_t>> id_remaps;
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;
    std::unordered_map<uint64_t, uint8_t> flag_corrections;
    struct CellCorr { uint64_t cell_id; std::vector<uint32_t> ids; };
    std::unordered_map<uint32_t, std::vector<CellCorr>> entry_corrections;

    while (pos < patch_size) {
        uint32_t file_id = ru32();
        if (file_id == 0xFFFFFFFF) break;

        // --- Metadata sections ---
        if (file_id == 0xFFFFFFF7) {
            // String diff in main loop — process it (may be the only occurrence)
            uint32_t n_added = ru32(), n_deleted = ru32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) { const char* s = P+pos; added.push_back(s); pos += strlen(s)+1; }
            std::vector<uint32_t> del_idx(n_deleted);
            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = ru32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());
            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size) { old_strs.push_back({(uint32_t)sp, std::string(old_pool.data+sp)}); sp += strlen(old_pool.data+sp)+1; }
            std::vector<std::string> merged;
            for (size_t i = 0; i < old_strs.size(); i++) if (!del_set.count(i)) merged.push_back(old_strs[i].second);
            for (auto& s : added) merged.push_back(s);
            std::sort(merged.begin(), merged.end());
            FILE* fp = fopen((out_dir + "/strings.bin").c_str(), "wb");
            std::unordered_map<std::string, uint32_t> new_offs;
            uint32_t wpos = 0;
            for (auto& s : merged) { new_offs[s] = wpos; fwrite(s.c_str(), 1, s.size()+1, fp); wpos += s.size()+1; }
            fclose(fp);
            for (auto& [old_off, s] : old_strs) { auto it = new_offs.find(s); if (it != new_offs.end() && it->second != old_off) str_remap[old_off] = it->second; }
            unmap_file(old_pool);
            std::cerr << "  Strings (loop): +" << n_added << " -" << n_deleted << " → " << merged.size() << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = ru32(), nr = ru32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { memcpy(&geo_added[i], P+pos, 8); pos += 8; }
            for (uint32_t i = 0; i < nr; i++) { memcpy(&geo_removed[i], P+pos, 8); pos += 8; }
            std::cerr << "  Geo cells: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = ru32(), nr = ru32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { memcpy(&admin_added[i], P+pos, 8); pos += 8; }
            for (uint32_t i = 0; i < nr; i++) { memcpy(&admin_removed[i], P+pos, 8); pos += 8; }
            std::cerr << "  Admin cells: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_FLAGS_MARKER) {
            uint32_t c = ru32();
            for (uint32_t i = 0; i < c; i++) { uint64_t cid; memcpy(&cid, P+pos, 8); pos += 8; flag_corrections[cid] = P[pos++]; }
            std::cerr << "  Flag corrections: " << c << std::endl;
            continue;
        }
        if (file_id == SECONDARY_REMAP_MARKER) {
            uint32_t nf = ru32();
            for (uint32_t f = 0; f < nf; f++) {
                uint32_t fid = ru32(), np = ru32();
                auto& rm = id_remaps[fid];
                for (uint32_t i = 0; i < np; i++) { uint32_t o = ru32(), n = ru32(); if (o < rm.size()) rm[o] = n; }
                std::cerr << "  Secondary remap " << fid << ": " << np << " pairs" << std::endl;
            }
            continue;
        }
        if (file_id == ENTRY_CORRECTION_MARKER) {
            uint32_t fid = ru32(), c = ru32();
            auto& list = entry_corrections[fid];
            for (uint32_t i = 0; i < c; i++) {
                uint64_t cid; memcpy(&cid, P+pos, 8); pos += 8;
                uint16_t ec; memcpy(&ec, P+pos, 2); pos += 2;
                std::vector<uint32_t> ids(ec);
                if (ec > 0) { memcpy(ids.data(), P+pos, ec*4); pos += ec*4; }
                list.push_back({cid, std::move(ids)});
            }
            std::cerr << "  Entry corrections " << fid << ": " << c << std::endl;
            continue;
        }

        // --- Merge sequence replay (streaming) ---
        uint32_t stride = ru32();
        uint64_t old_size = ru64(), new_size = ru64();
        if (file_id >= (uint32_t)PatchFileId::COUNT) { std::cerr << "Unknown file " << file_id << std::endl; break; }
        const char* fname = patch_file_names[file_id];

        if (stride == 0) {
            // Full replacement — write directly from patch mmap
            uint32_t nf = ru32(); (void)nf; uint64_t ds = ru64();
            FILE* fp = fopen((out_dir + "/" + fname).c_str(), "wb");
            fwrite(P+pos, 1, ds, fp); fclose(fp);
            pos += ds;
            std::cerr << "  " << fname << ": full replace " << ds << " bytes" << std::endl;
            continue;
        }
        if (stride == 0xFE) { uint32_t nf = ru32(); (void)nf; uint64_t ds = ru64(); pos += ds; continue; }

        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;

        // Load old file into mutable buffer (need to apply remap + fixups)
        // For large files (nodes/vertices), this is the peak memory per file.
        // Files are processed sequentially, so only one is in memory at a time.
        auto old_data = read_file(cur_dir + "/" + std::string(fname));

        // Zero padding
        if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS && actual_stride == 24)
            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) memset(old_data.data()+i+13, 0, 3);
        if (file_id == (uint32_t)PatchFileId::INTERP_WAYS && actual_stride == 24)
            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) { memset(old_data.data()+i+5, 0, 3); memset(old_data.data()+i+21, 0, 3); }

        apply_str_remap(old_data, file_id, actual_stride);

        // Apply fixups
        uint32_t n_fixups = ru32();
        if (n_fixups > 0) {
            uint32_t delta_size = ru32();
            uint32_t pi = 0, pv = 0; size_t fp = pos;
            for (uint32_t i = 0; i < n_fixups; i++) {
                uint32_t idx = pi + read_varint(P, fp);
                uint32_t val = pv + read_varint(P, fp);
                if ((size_t)idx * actual_stride + 4 <= old_data.size())
                    memcpy(old_data.data() + (size_t)idx * actual_stride, &val, 4);
                pi = idx; pv = val;
            }
            pos = fp;
        }

        // Replay merge sequence — stream output directly to file
        uint64_t seq_size = ru64();
        size_t seq_end = pos + seq_size;
        FILE* outf = fopen((out_dir + "/" + fname).c_str(), "wb");

        // Track ID remap (only for data files that need entry reconstruction)
        bool track = (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);
        std::vector<uint32_t> id_map;
        if (track) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        size_t old_rec = 0, new_rec = 0, old_bytes = 0, written = 0;
        while (pos < seq_end) {
            uint8_t op = P[pos++];
            uint32_t count; memcpy(&count, P+pos, 4); pos += 4;
            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                fwrite(old_data.data() + old_bytes, 1, bytes, outf);
                written += bytes;
                if (track) for (uint32_t k = 0; k < count; k++) if (old_rec+k < id_map.size()) id_map[old_rec+k] = new_rec+k;
                old_rec += count; new_rec += count; old_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                fwrite(P+pos, 1, bytes, outf);
                written += bytes; pos += bytes; new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count; old_bytes += count * actual_stride;
            }
        }
        fclose(outf);

        if (track) {
            id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << fname << ": " << written << " bytes (mapped " << matched << "/" << id_remaps[file_id].size() << ")" << std::endl;
        } else {
            std::cerr << "  " << fname << ": " << written << " bytes" << std::endl;
        }

        // Free old data immediately — don't let glibc hold the pages
        { std::vector<char>().swap(old_data); }
        malloc_trim(0);
    }
    log_phase("Merge replays", t_start);

    // Free string remap (no longer needed)
    { std::unordered_map<uint32_t,uint32_t>().swap(str_remap); }
    malloc_trim(0);

    // --- Phase 4: Streaming entry pipeline ---
    // Walk old geo_cells, remap IDs, apply corrections, write output directly to files.
    {
        double t_entry = now_ms();
        std::cerr << "Entry pipeline..." << std::endl;

        auto get_rm = [&](PatchFileId fid) -> const std::vector<uint32_t>& {
            static std::vector<uint32_t> empty;
            auto it = id_remaps.find((uint32_t)fid); return it != id_remaps.end() ? it->second : empty;
        };
        auto& w_rm = get_rm(PatchFileId::STREET_WAYS);
        auto& a_rm = get_rm(PatchFileId::ADDR_POINTS);
        auto& i_rm = get_rm(PatchFileId::INTERP_WAYS);

        // Build correction lookup maps
        std::unordered_map<uint64_t, const std::vector<uint32_t>*> cs, ca, ci;
        for (auto& [fid, list] : entry_corrections) {
            auto* m = (fid == (uint32_t)PatchFileId::STREET_ENTRIES) ? &cs :
                      (fid == (uint32_t)PatchFileId::ADDR_ENTRIES) ? &ca :
                      (fid == (uint32_t)PatchFileId::INTERP_ENTRIES) ? &ci : nullptr;
            if (m) for (auto& c : list) (*m)[c.cell_id] = &c.ids;
        }

        // mmap old files (zero-copy read)
        MappedFile old_geo = mmap_file(cur_dir + "/geo_cells.bin");
        MappedFile old_se = mmap_file(cur_dir + "/street_entries.bin");
        MappedFile old_ae = mmap_file(cur_dir + "/addr_entries.bin");
        MappedFile old_ie = mmap_file(cur_dir + "/interp_entries.bin");

        // Build derived cell list
        std::unordered_set<uint64_t> rm_set(geo_removed.begin(), geo_removed.end());
        struct CR { uint64_t cid; int32_t idx; };
        std::vector<CR> cells;
        cells.reserve(old_geo.size / 20 + geo_added.size());
        for (size_t i = 0; i < old_geo.size / 20; i++) {
            uint64_t cid; memcpy(&cid, old_geo.data + i*20, 8);
            if (!rm_set.count(cid)) cells.push_back({cid, (int32_t)i});
        }
        for (uint64_t cid : geo_added) cells.push_back({cid, -1});
        std::sort(cells.begin(), cells.end(), [](const CR& a, const CR& b) { return a.cid < b.cid; });
        log_phase("  Cell list", t_entry);

        // Open output files for streaming write
        FILE* f_geo = fopen((out_dir + "/geo_cells.bin").c_str(), "wb");
        FILE* f_se = fopen((out_dir + "/street_entries.bin").c_str(), "wb");
        FILE* f_ae = fopen((out_dir + "/addr_entries.bin").c_str(), "wb");
        FILE* f_ie = fopen((out_dir + "/interp_entries.bin").c_str(), "wb");
        uint32_t se_pos = 0, ae_pos = 0, ie_pos = 0;
        uint32_t no_data = 0xFFFFFFFF;

        // Reusable buffer
        std::vector<uint32_t> id_buf;
        id_buf.reserve(256);

        auto parse_ids = [](const MappedFile& f, uint32_t off, std::vector<uint32_t>& buf) {
            buf.clear();
            if (off == 0xFFFFFFFF || off + 2 > f.size) return;
            uint16_t c; memcpy(&c, f.data+off, 2);
            if (c > 10000 || off+2+(size_t)c*4 > f.size) return; // sanity: no cell has >10K entries
            buf.resize(c); memcpy(buf.data(), f.data+off+2, c*4);
        };
        auto remap_buf = [](std::vector<uint32_t>& buf, const std::vector<uint32_t>& rm) {
            for (auto& id : buf) if (id < rm.size() && rm[id] != 0xFFFFFFFF) id = rm[id];
            std::sort(buf.begin(), buf.end());
        };
        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n, uint32_t& fpos) -> uint32_t {
            if (n == 0) return 0xFFFFFFFF;
            uint32_t off = fpos;
            uint16_t c = (uint16_t)n;
            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);
            uint64_t new_fpos = (uint64_t)fpos + 2 + n*4;
            fpos = (uint32_t)new_fpos; // Note: wraps at 4GiB
            return off;
        };
        size_t total_writes_s = 0, total_writes_a = 0, total_writes_i = 0;

        size_t sc = 0, ac = 0, ic = 0;
        size_t cell_count = 0;
        for (auto& [cid, oi] : cells) {
            if (++cell_count % 5000000 == 0) {
                std::cerr << "    " << cell_count << "/" << cells.size() << " cells, se=" << se_pos/1024/1024 << "M ae=" << ae_pos/1024/1024 << "M ie=" << ie_pos/1024/1024 << "M rss=" << get_rss_mb() << "M" << std::endl;
            }
            // Process one entry type at a time using shared id_buf
            auto process = [&](const MappedFile& old_e, size_t geo_off,
                               const std::vector<uint32_t>& rm,
                               const std::unordered_map<uint64_t, const std::vector<uint32_t>*>& corr,
                               FILE* outf, uint32_t& fpos, size_t& cc) -> uint32_t {
                auto cit = corr.find(cid);
                if (cit != corr.end()) { cc++; return write_ids(outf, cit->second->data(), cit->second->size(), fpos); }
                if (oi < 0) return no_data;
                uint32_t off; memcpy(&off, old_geo.data + oi*20 + geo_off, 4);
                parse_ids(old_e, off, id_buf);
                if (id_buf.empty()) return no_data;
                remap_buf(id_buf, rm);
                return write_ids(outf, id_buf.data(), id_buf.size(), fpos);
            };

            // Determine flags
            bool hs = false, ha = false, hi = false;
            if (oi >= 0) {
                uint32_t s, a, ip; memcpy(&s, old_geo.data+oi*20+8, 4); memcpy(&a, old_geo.data+oi*20+12, 4); memcpy(&ip, old_geo.data+oi*20+16, 4);
                hs = (s != no_data); ha = (a != no_data); hi = (ip != no_data);
            }
            auto fc = flag_corrections.find(cid);
            if (fc != flag_corrections.end()) { hs = fc->second & 1; ha = fc->second & 2; hi = fc->second & 4; }
            if (cs.count(cid)) hs = true;
            if (ca.count(cid)) ha = true;
            if (ci.count(cid)) hi = true;

            uint32_t so = hs ? process(old_se, 8, w_rm, cs, f_se, se_pos, sc) : no_data;
            uint32_t ao = ha ? process(old_ae, 12, a_rm, ca, f_ae, ae_pos, ac) : no_data;
            uint32_t io = hi ? process(old_ie, 16, i_rm, ci, f_ie, ie_pos, ic) : no_data;
            if (cell_count == 1) {
                // Count how many old cells have each flag
                size_t os = 0, oa = 0, oi_c = 0;
                for (size_t ci = 0; ci < old_geo.size / 20; ci++) {
                    uint32_t sv, av, iv;
                    memcpy(&sv, old_geo.data + ci*20+8, 4); memcpy(&av, old_geo.data + ci*20+12, 4); memcpy(&iv, old_geo.data + ci*20+16, 4);
                    if (sv != no_data) os++; if (av != no_data) oa++; if (iv != no_data) oi_c++;
                }
                std::cerr << "    DEBUG: old cells=" << old_geo.size/20 << " has_street=" << os << " has_addr=" << oa << " has_interp=" << oi_c << std::endl;
            }

            fwrite(&cid, 8, 1, f_geo); fwrite(&so, 4, 1, f_geo); fwrite(&ao, 4, 1, f_geo); fwrite(&io, 4, 1, f_geo);
        }
        fclose(f_geo); fclose(f_se); fclose(f_ae); fclose(f_ie);
        unmap_file(old_geo); unmap_file(old_se); unmap_file(old_ae); unmap_file(old_ie);
        std::cerr << "  Geo: " << cells.size() << " cells, corrections: " << sc << " st, " << ac << " ad, " << ic << " in" << std::endl;
        log_phase("  Geo entries", t_entry);

        // Admin: small, use existing rebuild + corrections
        {
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            auto ait = id_remaps.find((uint32_t)PatchFileId::ADMIN_POLYGONS);
            if (ait != id_remaps.end()) {
                for (uint32_t i = 0; i < ait->second.size(); i++)
                    if (ait->second[i] != 0xFFFFFFFF) ad_rm[i] = ait->second[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_adme = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_adme, ad_rm, admin_added, admin_removed);

            auto ecit = entry_corrections.find((uint32_t)PatchFileId::ADMIN_ENTRIES);
            if (ecit != entry_corrections.end()) {
                std::unordered_map<uint64_t, const std::vector<uint32_t>*> ac_corr;
                for (auto& c : ecit->second) ac_corr[c.cell_id] = &c.ids;
                size_t n = admin.admin_cells_data.size() / 12;
                FILE* fac = fopen((out_dir + "/admin_cells.bin").c_str(), "wb");
                FILE* fae = fopen((out_dir + "/admin_entries.bin").c_str(), "wb");
                uint32_t ae_wpos = 0;
                for (size_t i = 0; i < n; i++) {
                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data()+i*12, 8);
                    fwrite(&cid, 8, 1, fac);
                    auto cit = ac_corr.find(cid);
                    if (cit != ac_corr.end()) {
                        uint32_t off = cit->second->empty() ? no_data : ae_wpos;
                        fwrite(&off, 4, 1, fac);
                        if (!cit->second->empty()) {
                            uint16_t c = cit->second->size();
                            fwrite(&c, 2, 1, fae); fwrite(cit->second->data(), 4, c, fae);
                            ae_wpos += 2 + c*4;
                        }
                    } else {
                        uint32_t old_off; memcpy(&old_off, admin.admin_cells_data.data()+i*12+8, 4);
                        if (old_off != no_data && old_off+2 <= admin.admin_entries_data.size()) {
                            uint32_t new_off = ae_wpos; fwrite(&new_off, 4, 1, fac);
                            uint16_t c; memcpy(&c, admin.admin_entries_data.data()+old_off, 2);
                            fwrite(admin.admin_entries_data.data()+old_off, 1, 2+c*4, fae);
                            ae_wpos += 2+c*4;
                        } else {
                            fwrite(&no_data, 4, 1, fac);
                        }
                    }
                }
                fclose(fac); fclose(fae);
                std::cerr << "  Admin: " << n << " cells, " << ac_corr.size() << " corrections" << std::endl;
            } else {
                write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
                write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            }
            log_phase("  Admin", t_entry);
        }
    }

    // Cleanup
    unmap_file(patch_map);
    { std::string cmd = "rm -rf '" + tmpdir + "'"; system(cmd.c_str()); }
    log_phase("Total", t_start);
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

+                // Also check actual file sizes vs tracked positions

+                long se_actual = ftell(f_se), ae_actual = ftell(f_ae), ie_actual = ftell(f_ie);

+                std::cerr << "    " << cell_count << "/" << cells.size()

+                          << " se_pos=" << se_pos << " se_ftell=" << se_actual

+                          << " ae_pos=" << ae_pos << " ae_ftell=" << ae_actual


Bash

make -j$(nproc) geocoder-patch 2>&1 | tail -3 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "rm -f /home/michtest/geocoder-patch" 2>&1 && sshpass -p 'elon-is-insane' scp -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no /root/traccar-geocoder/builder/build/geocoder-patch michtest@100.113.88.121:/home/michtest/ 2>&1 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 'cd /home/michtest && rm -rf patched-planet-stream/ && ./geocoder-patch planet-B/ validated-patches/planet-mar16-to-mar23-v2.gcpatch -o patched-planet-stream/ 2>&1 | head -40 && kill -9 $(ps aux | grep "geocoder-patch planet" | grep -v grep | awk "{print \$2}") 2>/dev/null; rm -rf patched-planet-stream/' 2>&1

Run with ftell diagnostic


[ 50%] Building CXX object CMakeFiles/geocoder-patch.dir/tools/geocoder_patch.cpp.o
[100%] Linking CXX executable geocoder-patch
[100%] Built target geocoder-patch
Patch: 423813763 bytes
  [0.3s, 3 MiB] Decompress
  [0.3s, 3 MiB] Strings
  Strings (loop): +10703 -4486 → 11894530
  addr_points.bin: 2497261184 bytes (mapped 155813360/155902060)
  street_ways.bin: 574853820 bytes (mapped 47686691/47864210)
  street_nodes.bin: 3966274352 bytes
  interp_ways.bin: 1749240 bytes (mapped 72656/72839)
  interp_nodes.bin: 3484432 bytes
  admin_polygons.bin: 22763904 bytes (mapped 942932/947905)
  admin_vertices.bin: 717223152 bytes
  Geo cells: +183728 -45783
  Admin cells: +4 -0
  Secondary remap 1: 55091 pairs
  Secondary remap 3: 58838 pairs
  Secondary remap 4: 88 pairs
  Secondary remap 6: 4603 pairs
  Entry corrections 9: 1588748
  Entry corrections 10: 95288
  Entry corrections 11: 256
  Entry corrections 13: 4556
  Flag corrections: 224324
  [26.7s, 1816 MiB] Merge replays
Entry pipeline...
  [14.2s, 12469 MiB]   Cell list
    DEBUG: old cells=322588816 has_street=306918478 has_addr=48969395 has_interp=146493
    5000000/322726761 se_pos=41108116 se_ftell=41108116 ae_pos=4292944 ae_ftell=4292944 rss=12512M
    10000000/322726761 se_pos=81827420 se_ftell=81827420 ae_pos=12855424 ae_ftell=12855424 rss=12559M
    15000000/322726761 se_pos=117090318 se_ftell=117090318 ae_pos=18564430 ae_ftell=18564430 rss=12598M
    20000000/322726761 se_pos=155095046 se_ftell=155095046 ae_pos=28797866 ae_ftell=28797866 rss=12644M
    25000000/322726761 se_pos=195875176 se_ftell=195875176 ae_pos=41363970 ae_ftell=41363970 rss=12695M
    30000000/322726761 se_pos=232160972 se_ftell=232160972 ae_pos=42671202 ae_ftell=42671202 rss=12731M
    35000000/322726761 se_pos=267062570 se_ftell=267062570 ae_pos=43439102 ae_ftell=43439102 rss=12765M
    40000000/322726761 se_pos=306376652 se_ftell=306376652 ae_pos=45349666 ae_ftell=45349666 rss=12804M
    45000000/322726761 se_pos=345042740 se_ftell=345042740 ae_pos=83113690 ae_ftell=83113690 rss=12877M
    50000000/322726761 se_pos=384204276 se_ftell=384204276 ae_pos=85074042 ae_ftell=85074042 rss=12916M
    55000000/322726761 se_pos=424445362 se_ftell=424445362 ae_pos=85124868 ae_ftell=85124868 rss=12954M
    60000000/322726761 se_pos=462195062 se_ftell=462195062 ae_pos=87957186 ae_ftell=87957186 rss=12993M
    65000000/322726761 se_pos=496260618 se_ftell=496260618 ae_pos=88522968 ae_ftell=88522968 rss=13026M
    70000000/322726761 se_pos=535574376 se_ftell=535574376 ae_pos=89899878 ae_ftell=89899878 rss=13065M


**se_pos matches ftell exactly!** The tracked position is correct. At 70M cells, se_pos = 535M, ae_pos = 89M. Projecting to 322M cells:
- street: 535M × (322/70) = 2.46 GiB — matches reference 2.1 GiB
- addr: 89M × (322/70) = 410 MiB — matches reference 706 MiB (addr is bursty)

The output sizes ARE correct! The 62 GiB I saw earlier was definitively from the stale old process, not the new one. My debugging was chasing a ghost. Let me now let the full planet run complete without interference:


sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 'cd /home/michtest && rm -rf patched-planet-stream/ && nohup bash -c "time ./geocoder-patch planet-B/ validated-patches/planet-mar16-to-mar23-v2.gcpatch -o patched-planet-stream/ 2>&1 && echo ---VERIFY--- && for f in planet-C/*; do b=\$(basename \$f); cmp -s patched-planet-stream/\$b planet-C/\$b || echo \"MISMATCH: \$b\"; done && echo All_checked" > planet-patch-final.log 2>&1 &' 2>&1 && echo "Started — will check in 15 min"

Final clean planet run


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


sleep 300 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "grep -E '\\[.*MiB\\]|cells/|Geo:|All_checked|MISMATCH' /home/michtest/planet-patch-final.log | tail -10 && ps aux | grep 'geocoder-patch planet' | grep -v grep | awk '{printf \"RSS: %.0f MiB TIME: %s\\n\", \$6/1024, \$10}'" 2>&1

Check 5 min


  [0.3s, 3 MiB] Decompress
  [0.3s, 3 MiB] Strings
  [26.5s, 1816 MiB] Merge replays
  [14.2s, 12469 MiB]   Cell list
RSS: 2 MiB TIME: 0:00
RSS: 3 MiB TIME: 0:00
RSS: 13477 MiB TIME: 7:11


sleep 300 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "grep -E '\\[.*MiB\\]|Geo:|Admin|Total|All_checked|MISMATCH' /home/michtest/planet-patch-final.log | tail -10 && ps aux | grep 'geocoder-patch planet' | grep -v grep | awk '{printf \"RSS: %.0f MiB TIME: %s\\n\", \$6/1024, \$10}'" 2>&1

Check 10 min


  [0.3s, 3 MiB] Decompress
  [0.3s, 3 MiB] Strings
  Admin cells: +4 -0
  [26.5s, 1816 MiB] Merge replays
  [14.2s, 12469 MiB]   Cell list
RSS: 2 MiB TIME: 0:00
RSS: 3 MiB TIME: 0:00
RSS: 13510 MiB TIME: 12:20


13.5 GiB stable, 12 min CPU. Let me wait for completion:


sleep 600 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "grep -E 'Geo:|Admin|Total|All_checked|MISMATCH|real' /home/michtest/planet-patch-final.log" 2>&1

Check 20 min


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


Read result


Admin cells: +4 -0


Still running. Let me wait more:


sleep 600 && sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "grep -E 'Geo:|Admin pip|Total|All_checked|MISMATCH|real' /home/michtest/planet-patch-final.log && ps aux | grep 'geocoder-patch planet' | grep -v grep | wc -l" 2>&1

Check 30 min


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


Read result


real	31m10.082s
0


Completed! 31 min total, 0 processes remaining. Let me get the full result:


sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "grep -E '\\[.*MiB\\]|Geo:|Admin|Total|All_checked|MISMATCH|real' /home/michtest/planet-patch-final.log" 2>&1

Get full results


  [0.3s, 3 MiB] Decompress
  [0.3s, 3 MiB] Strings
  Admin cells: +4 -0
  [26.5s, 1816 MiB] Merge replays
  [14.2s, 12469 MiB]   Cell list
real	31m10.082s


The Geo entries and Admin pipeline timing didn't print — must have been captured differently. Let me get the tail of the log:


sshpass -p 'elon-is-insane' ssh -o ProxyCommand="tailscale nc %h %p" -o StrictHostKeyChecking=no michtest@100.113.88.121 "tail -20 /home/michtest/planet-patch-final.log" 2>&1

Get full tail


    140000000/322726761 se_pos=83630446 se_ftell=25853434222 ae_pos=471881266 ae_ftell=34831619634 rss=13478M
    145000000/322726761 se_pos=2713403366 se_ftell=28483207142 ae_pos=3372313086 ae_ftell=42027018750 rss=13484M
    150000000/322726761 se_pos=1403534564 se_ftell=31468305636 ae_pos=4011153320 ae_ftell=46960826280 rss=13490M
    155000000/322726761 se_pos=2206972840 se_ftell=36566711208 ae_pos=4251331048 ae_ftell=51495971304 rss=13495M
    160000000/322726761 se_pos=1670098140 se_ftell=40324803804 ae_pos=97131038 ae_ftell=55931705886 rss=13501M
    165000000/322726761 se_pos=645635692 se_ftell=43595308652 ae_pos=350397816 ae_ftell=60479939960 rss=13507M
    170000000/322726761 se_pos=3432528972 se_ftell=46382201932 ae_pos=741294916 ae_ftell=65165804356 rss=13513M
    175000000/322726761 se_pos=1886488342 se_ftell=49131128598 ae_pos=850424906 ae_ftell=69569901642 rss=13521M
    180000000/322726761 se_pos=2969479884 se_ftell=54509087436 ae_pos=4106619114 ae_ftell=77121063146 rss=15324M
    185000000/322726761 se_pos=3915258008 se_ftell=64044800152 ae_pos=1601613974 ae_ftell=87500959894 rss=15328M
    190000000/322726761 se_pos=3408853222 se_ftell=72128329958 ae_pos=1562966386 ae_ftell=100347214194 rss=15328M
    195000000/322726761 se_pos=2666722638 se_ftell=79976133966 ae_pos=1105615204 ae_ftell=112774764900 rss=15328M
    200000000/322726761 se_pos=1544128082 se_ftell=87443474002 ae_pos=1139185328 ae_ftell=125693236912 rss=15328M
    205000000/322726761 se_pos=643531658 se_ftell=95132812170 ae_pos=732633154 ae_ftell=138171586626 rss=15328M
    210000000/322726761 se_pos=4010556206 se_ftell=102794804014 ae_pos=841103950 ae_ftell=151164959310 rss=15328M
bash: line 1: 174593 Segmentation fault      ./geocoder-patch planet-B/ validated-patches/planet-mar16-to-mar23-v2.gcpatch -o patched-planet-stream/ 2>&1

real	31m10.082s
user	29m22.055s
sys	1m45.704s


**Segmentation fault!** And look: `se_ftell` diverges from `se_pos`. At 140M cells: `se_pos=83630446` but `se_ftell=25853434222`. The `se_pos` (uint32_t) wraps around while `se_ftell` (long, 64-bit) keeps growing. At that point, `se_ftell` is 25.8 GiB while `se_pos` says 83 MiB.

The `fpos` is `uint32_t` which overflows at 4 GiB. The geo_cells offset field IS uint32_t in the file format, so offsets >4 GiB are impossible. But on planet, the street_entries file grows beyond 4 GiB!

The fix: the entry offsets in geo_cells ARE uint32_t (max 4 GiB), so the entry files fundamentally can't be >4 GiB. But my streaming write produces entry files that are >4 GiB because the remapped entries are slightly larger than the originals... wait, no. The reference street_entries is 2.1 GiB. My output should be similar. But the `ftell` shows 25+ GiB at 140M cells.

**The addr_entries ftell is 34.8 GiB at 140M cells** — but the reference is only 706 MiB! Something is writing way too much addr data. Looking at `ae_pos` at 140M cells: 471 MiB — normal! But `ae_ftell` is 34.8 GiB. There's a 70x discrepancy.

The `fwrite` is writing more data than `fpos` tracks. The bug is in the `write_ids` function — it writes to `f` but `fpos` only tracks the bytes for THIS entry type. If the FILE* handle is shared or wrong...

Wait — each entry type has its own FILE*: `f_se`, `f_ae`, `f_ie`, `f_geo`. They're all separate files. But the `process_entry` lambda takes `FILE* outf` as a parameter. Let me check if the correct file is passed:

```cpp
uint32_t so = hs ? process(old_se, 8, w_rm, cs, f_se, se_pos, sc) : no_data;
uint32_t ao = ha ? process(old_ae, 12, a_rm, ca, f_ae, ae_pos, ac) : no_data;
```

This looks correct — `f_se` for streets, `f_ae` for addr. But inside `process_entry`, there's a call to `write_ids(outf, ...)` which uses the right file.

OH WAIT — I bet the issue is the `parse_ids` function. It reads from the mmap'd old file and fills `id_buf`. But `id_buf` is SHARED across all 3 process_entry calls per cell. When processing streets, `parse_ids` fills `id_buf`. Then `remap_buf` remaps and sorts. Then `write_ids` writes `id_buf`. Then for addr, `parse_ids` clears and refills `id_buf`...

But what if a CORRECTION is applied for streets? The correction handler calls `write_ids(outf, cit->second->data(), cit->second->size(), fpos)` — this writes the correction data directly, NOT from `id_buf`. Then for addr, `parse_ids` is called which fills `id_buf` with ADDR data. But `id_buf` was also filled during the remap for streets (if no correction). So `id_buf` might still contain stale street data from a previous cell... but `parse_ids` always does `buf.clear()` first.

I think the issue is actually simpler: the `write_ids` calls `fwrite(ids, 4, n, f)`. This writes `n` uint32_t values. For `n = id_buf.size()`, this writes `id_buf.size() * 4` bytes. The `fpos` is incremented by `2 + n*4`. The `ftell` should match `fpos` exactly.

But it doesn't! `se_ftell` diverges from `se_pos` starting around cell 130M. The `se_pos` uint32_t wraps at ~4 GiB. At 130M cells with ~8 bytes/cell, `se_pos` = 130M × 8 = 1.04 GiB. That's well within uint32_t range.

Unless the actual bytes per cell is higher than expected at certain points. Let me check: at 130M cells, `se_pos` = 808M but `se_ftell` = 23.6 GiB. That's 29x more. At 70M cells, `se_pos` = 535M = `se_ftell`. So between 70M and 130M cells, ftell grew from 535M to 23.6 GiB while fpos grew from 535M to 808M. Something wrote 23 GiB of extra data.

WAIT — I see it now in the log output at cell 145M:
```
se_pos=2713403366 se_ftell=28483207142
```
`se_pos` = 2.71 GiB ≈ `se_ftell` = 28.5 GiB. These are TOTALLY different numbers. But at cell 70M, they matched perfectly. The divergence starts somewhere between 70M and 130M cells.

Could the `write_ids` function be writing to the WRONG FILE* sometimes? Maybe a stack corruption or the lambda closure is broken...

Actually, I think I found it: the `process_entry` lambda captures everything by reference `[&]`. The `id_buf` is in the outer scope. When `process_entry` calls `parse_ids(old_e, off, id_buf)`, then `remap_buf(id_buf, rm)`, then `write_ids(outf, id_buf.data(), id_buf.size(), fpos)` — all using the same `id_buf`. But `id_buf` is also used by the OTHER entry type calls in the same cell iteration.

**The bug**: `process_entry` for streets fills `id_buf` with street IDs, writes to `f_se`. Then `process_entry` for addr fills `id_buf` with addr IDs, writes to `f_ae`. BUT — if the street `process_entry` returns a correction (skips parse_ids/remap), then addr's `parse_ids` fills `id_buf` with addr data and writes to `f_ae`. No issue.

But what if TWO corrections exist for the same cell? Then NEITHER calls `parse_ids`, and `id_buf` might have stale data from the PREVIOUS cell. But `write_ids` writes from `cit->second->data()`, not from `id_buf`. So corrections don't touch `id_buf`.

Hmm, but what if a correction exists for streets but not addr? Streets: writes correction data (no id_buf). Addr: calls `parse_ids` which clears id_buf, fills with addr data. OK, that's fine.

Let me look REALLY carefully at the output at the divergence point. Between 70M and 130M cells (where the divergence starts), what's different? Maybe one of the mmap'd files becomes corrupt or a page fault returns garbage...

Actually, I think the segfault at 210M cells IS the clue. A segfault means we're accessing invalid memory. Maybe the mmap'd old entry files are being unmapped or the process is reading past the end. The `parse_ids` function bounds-checks: `if (off+2+c*4 > f.size)`. But if `off` is from a corrupted `se_pos` (after uint32_t overflow in a different context)...

Wait no — `off` comes from the old_geo mmap, not from `se_pos`. The offsets are correct.

I'm going in circles. Let me try a different approach: remove the `uint32_t fpos` tracking entirely and use `ftell()` to get the actual file position:


Edit

        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n, uint32_t& fpos) -> uint32_t {
            if (n == 0) return 0xFFFFFFFF;
            uint32_t off = fpos;
            uint16_t c = (uint16_t)n;
            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);
            uint64_t new_fpos = (uint64_t)fpos + 2 + n*4;
            fpos = (uint32_t)new_fpos; // Note: wraps at 4GiB
            return off;
        };
        size_t total_writes_s = 0, total_writes_a = 0, total_writes_i = 0;

        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n) -> uint32_t {
            if (n == 0) return 0xFFFFFFFF;
            uint32_t off = (uint32_t)ftell(f);
            uint16_t c = (uint16_t)n;
            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);
            return off;
        };


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

        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n) -> uint32_t {
            if (n == 0) return 0xFFFFFFFF;
            uint32_t off = (uint32_t)ftell(f);
            uint16_t c = (uint16_t)n;
            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);
            return off;
        };

// geocoder-patch v4: Low-memory streaming patch application.
//
// Key design: mmap old files (zero copy), stream output via fwrite (never accumulate),
// process entry pipeline cell-by-cell. Target: <1 GiB peak RSS for planet.
//
// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>

#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <iomanip>
#include <iostream>
#include <malloc.h>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unordered_map>
#include <unordered_set>
#include <unistd.h>
#include <vector>

#include "patch_format.h"

enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };

// --- Helpers ---
static double now_ms() {
    struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
static size_t get_rss_mb() {
    FILE* f = fopen("/proc/self/statm", "r");
    if (!f) return 0;
    size_t dummy, rss; fscanf(f, "%zu %zu", &dummy, &rss); fclose(f);
    return rss * 4096 / (1024*1024);
}
static void log_phase(const char* label, double start) {
    std::cerr << "  [" << std::fixed << std::setprecision(1) << (now_ms() - start) / 1000.0
              << "s, " << get_rss_mb() << " MiB] " << label << std::endl;
}

// mmap a file read-only. Returns {pointer, size}. Caller must munmap.
struct MappedFile { const char* data; size_t size; };
static MappedFile mmap_file(const std::string& path) {
    int fd = open(path.c_str(), O_RDONLY);
    if (fd < 0) return {nullptr, 0};
    struct stat st; fstat(fd, &st);
    size_t sz = st.st_size;
    if (sz == 0) { close(fd); return {nullptr, 0}; }
    void* p = mmap(nullptr, sz, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (p == MAP_FAILED) return {nullptr, 0};
    return {static_cast<const char*>(p), sz};
}
static void unmap_file(MappedFile& f) {
    if (f.data) { munmap(const_cast<char*>(f.data), f.size); f.data = nullptr; f.size = 0; }
}

// Detect stride from file size
static size_t detect_stride(const std::string& path, std::initializer_list<size_t> candidates) {
    struct stat st; if (stat(path.c_str(), &st) != 0) return *candidates.begin();
    for (size_t s : candidates) if (st.st_size % s == 0 && st.st_size > 0) return s;
    return *candidates.begin();
}

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);
    double t_start = now_ms();

    // --- Phase 1: Decompress + mmap patch ---
    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" << std::endl; return 1; }
    }
    MappedFile patch_map = mmap_file(raw_path);
    if (!patch_map.data) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }
    remove(raw_path.c_str());
    const char* P = patch_map.data;
    size_t patch_size = patch_map.size;
    std::cerr << "Patch: " << patch_size << " bytes" << std::endl;
    log_phase("Decompress", t_start);

    size_t pos = 0;
    auto ru32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, P+pos, 4); pos += 4; return v; };
    auto ru64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, P+pos, 8); pos += 8; return v; };

    // Header
    if (memcmp(P, GCPATCH_MAGIC, 8) != 0) { std::cerr << "Bad magic" << std::endl; return 1; }
    pos = 8;
    uint32_t ver = ru32(); if (ver != 2) { std::cerr << "Bad version" << std::endl; return 1; }
    ru32(); // flags

    // --- Phase 2: String rebuild ---
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = ru32();
        if (marker == 0xFFFFFFF7) {
            uint32_t n_added = ru32(), n_deleted = ru32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) { const char* s = P+pos; added.push_back(s); pos += strlen(s)+1; }
            std::vector<uint32_t> del_idx(n_deleted);
            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = ru32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size) {
                old_strs.push_back({(uint32_t)sp, std::string(old_pool.data + sp)});
                sp += strlen(old_pool.data + sp) + 1;
            }
            std::vector<std::string> merged;
            for (size_t i = 0; i < old_strs.size(); i++)
                if (!del_set.count(i)) merged.push_back(old_strs[i].second);
            for (auto& s : added) merged.push_back(s);
            std::sort(merged.begin(), merged.end());

            // Write new pool and build remap
            FILE* fp = fopen((out_dir + "/strings.bin").c_str(), "wb");
            std::unordered_map<std::string, uint32_t> new_offs;
            uint32_t wpos = 0;
            for (auto& s : merged) {
                new_offs[s] = wpos;
                fwrite(s.c_str(), 1, s.size() + 1, fp);
                wpos += s.size() + 1;
            }
            fclose(fp);
            for (auto& [old_off, s] : old_strs) {
                auto it = new_offs.find(s);
                if (it != new_offs.end() && it->second != old_off) str_remap[old_off] = it->second;
            }
            unmap_file(old_pool);
            std::cerr << "  Strings: +" << n_added << " -" << n_deleted << " → " << merged.size()
                      << ", " << str_remap.size() << " remapped" << std::endl;
            marker = ru32();
        }
        if (marker == 0xFFFFFFFE) { uint32_t c = ru32(); for (uint32_t i = 0; i < c; i++) { uint32_t a = ru32(), b = ru32(); str_remap[a] = b; } }
        else pos -= 4;
    }
    log_phase("Strings", t_start);

    // Detect strides
    size_t way_stride = detect_stride(cur_dir + "/street_ways.bin", {12, 9});
    size_t interp_stride = detect_stride(cur_dir + "/interp_ways.bin", {24, 20, 18});
    size_t admin_stride = detect_stride(cur_dir + "/admin_polygons.bin", {24, 20, 19});

    // String remap applier (works on mutable copy)
    auto apply_str_remap = [&](std::vector<char>& data, uint32_t fid, size_t stride) {
        if (str_remap.empty()) return;
        std::vector<size_t> offs;
        if (fid == (uint32_t)PatchFileId::ADDR_POINTS) offs = {8, 12};
        else if (fid == (uint32_t)PatchFileId::STREET_WAYS) offs = {(stride == 12) ? 8ul : 5ul};
        else if (fid == (uint32_t)PatchFileId::INTERP_WAYS) offs = {(stride >= 20) ? 8ul : 5ul};
        else if (fid == (uint32_t)PatchFileId::ADMIN_POLYGONS) offs = {8};
        else return;
        for (size_t i = 0; i + stride <= data.size(); i += stride)
            for (size_t o : offs) { uint32_t v; memcpy(&v, data.data()+i+o, 4); auto it = str_remap.find(v); if (it != str_remap.end()) memcpy(data.data()+i+o, &it->second, 4); }
    };

    // --- Phase 3: Merge replays (streaming output) ---
    // ID remaps: file_id → vector<uint32_t> where remap[old_idx] = new_idx
    std::unordered_map<uint32_t, std::vector<uint32_t>> id_remaps;
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;
    std::unordered_map<uint64_t, uint8_t> flag_corrections;
    struct CellCorr { uint64_t cell_id; std::vector<uint32_t> ids; };
    std::unordered_map<uint32_t, std::vector<CellCorr>> entry_corrections;

    while (pos < patch_size) {
        uint32_t file_id = ru32();
        if (file_id == 0xFFFFFFFF) break;

        // --- Metadata sections ---
        if (file_id == 0xFFFFFFF7) {
            // String diff in main loop — process it (may be the only occurrence)
            uint32_t n_added = ru32(), n_deleted = ru32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) { const char* s = P+pos; added.push_back(s); pos += strlen(s)+1; }
            std::vector<uint32_t> del_idx(n_deleted);
            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = ru32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());
            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size) { old_strs.push_back({(uint32_t)sp, std::string(old_pool.data+sp)}); sp += strlen(old_pool.data+sp)+1; }
            std::vector<std::string> merged;
            for (size_t i = 0; i < old_strs.size(); i++) if (!del_set.count(i)) merged.push_back(old_strs[i].second);
            for (auto& s : added) merged.push_back(s);
            std::sort(merged.begin(), merged.end());
            FILE* fp = fopen((out_dir + "/strings.bin").c_str(), "wb");
            std::unordered_map<std::string, uint32_t> new_offs;
            uint32_t wpos = 0;
            for (auto& s : merged) { new_offs[s] = wpos; fwrite(s.c_str(), 1, s.size()+1, fp); wpos += s.size()+1; }
            fclose(fp);
            for (auto& [old_off, s] : old_strs) { auto it = new_offs.find(s); if (it != new_offs.end() && it->second != old_off) str_remap[old_off] = it->second; }
            unmap_file(old_pool);
            std::cerr << "  Strings (loop): +" << n_added << " -" << n_deleted << " → " << merged.size() << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = ru32(), nr = ru32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { memcpy(&geo_added[i], P+pos, 8); pos += 8; }
            for (uint32_t i = 0; i < nr; i++) { memcpy(&geo_removed[i], P+pos, 8); pos += 8; }
            std::cerr << "  Geo cells: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = ru32(), nr = ru32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { memcpy(&admin_added[i], P+pos, 8); pos += 8; }
            for (uint32_t i = 0; i < nr; i++) { memcpy(&admin_removed[i], P+pos, 8); pos += 8; }
            std::cerr << "  Admin cells: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_FLAGS_MARKER) {
            uint32_t c = ru32();
            for (uint32_t i = 0; i < c; i++) { uint64_t cid; memcpy(&cid, P+pos, 8); pos += 8; flag_corrections[cid] = P[pos++]; }
            std::cerr << "  Flag corrections: " << c << std::endl;
            continue;
        }
        if (file_id == SECONDARY_REMAP_MARKER) {
            uint32_t nf = ru32();
            for (uint32_t f = 0; f < nf; f++) {
                uint32_t fid = ru32(), np = ru32();
                auto& rm = id_remaps[fid];
                for (uint32_t i = 0; i < np; i++) { uint32_t o = ru32(), n = ru32(); if (o < rm.size()) rm[o] = n; }
                std::cerr << "  Secondary remap " << fid << ": " << np << " pairs" << std::endl;
            }
            continue;
        }
        if (file_id == ENTRY_CORRECTION_MARKER) {
            uint32_t fid = ru32(), c = ru32();
            auto& list = entry_corrections[fid];
            for (uint32_t i = 0; i < c; i++) {
                uint64_t cid; memcpy(&cid, P+pos, 8); pos += 8;
                uint16_t ec; memcpy(&ec, P+pos, 2); pos += 2;
                std::vector<uint32_t> ids(ec);
                if (ec > 0) { memcpy(ids.data(), P+pos, ec*4); pos += ec*4; }
                list.push_back({cid, std::move(ids)});
            }
            std::cerr << "  Entry corrections " << fid << ": " << c << std::endl;
            continue;
        }

        // --- Merge sequence replay (streaming) ---
        uint32_t stride = ru32();
        uint64_t old_size = ru64(), new_size = ru64();
        if (file_id >= (uint32_t)PatchFileId::COUNT) { std::cerr << "Unknown file " << file_id << std::endl; break; }
        const char* fname = patch_file_names[file_id];

        if (stride == 0) {
            // Full replacement — write directly from patch mmap
            uint32_t nf = ru32(); (void)nf; uint64_t ds = ru64();
            FILE* fp = fopen((out_dir + "/" + fname).c_str(), "wb");
            fwrite(P+pos, 1, ds, fp); fclose(fp);
            pos += ds;
            std::cerr << "  " << fname << ": full replace " << ds << " bytes" << std::endl;
            continue;
        }
        if (stride == 0xFE) { uint32_t nf = ru32(); (void)nf; uint64_t ds = ru64(); pos += ds; continue; }

        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;

        // Load old file into mutable buffer (need to apply remap + fixups)
        // For large files (nodes/vertices), this is the peak memory per file.
        // Files are processed sequentially, so only one is in memory at a time.
        auto old_data = read_file(cur_dir + "/" + std::string(fname));

        // Zero padding
        if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS && actual_stride == 24)
            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) memset(old_data.data()+i+13, 0, 3);
        if (file_id == (uint32_t)PatchFileId::INTERP_WAYS && actual_stride == 24)
            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) { memset(old_data.data()+i+5, 0, 3); memset(old_data.data()+i+21, 0, 3); }

        apply_str_remap(old_data, file_id, actual_stride);

        // Apply fixups
        uint32_t n_fixups = ru32();
        if (n_fixups > 0) {
            uint32_t delta_size = ru32();
            uint32_t pi = 0, pv = 0; size_t fp = pos;
            for (uint32_t i = 0; i < n_fixups; i++) {
                uint32_t idx = pi + read_varint(P, fp);
                uint32_t val = pv + read_varint(P, fp);
                if ((size_t)idx * actual_stride + 4 <= old_data.size())
                    memcpy(old_data.data() + (size_t)idx * actual_stride, &val, 4);
                pi = idx; pv = val;
            }
            pos = fp;
        }

        // Replay merge sequence — stream output directly to file
        uint64_t seq_size = ru64();
        size_t seq_end = pos + seq_size;
        FILE* outf = fopen((out_dir + "/" + fname).c_str(), "wb");

        // Track ID remap (only for data files that need entry reconstruction)
        bool track = (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);
        std::vector<uint32_t> id_map;
        if (track) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

        size_t old_rec = 0, new_rec = 0, old_bytes = 0, written = 0;
        while (pos < seq_end) {
            uint8_t op = P[pos++];
            uint32_t count; memcpy(&count, P+pos, 4); pos += 4;
            if (op == OP_MATCH_RUN) {
                size_t bytes = count * actual_stride;
                fwrite(old_data.data() + old_bytes, 1, bytes, outf);
                written += bytes;
                if (track) for (uint32_t k = 0; k < count; k++) if (old_rec+k < id_map.size()) id_map[old_rec+k] = new_rec+k;
                old_rec += count; new_rec += count; old_bytes += bytes;
            } else if (op == OP_INSERT_RUN) {
                size_t bytes = count * actual_stride;
                fwrite(P+pos, 1, bytes, outf);
                written += bytes; pos += bytes; new_rec += count;
            } else if (op == OP_DELETE_RUN) {
                old_rec += count; old_bytes += count * actual_stride;
            }
        }
        fclose(outf);

        if (track) {
            id_remaps[file_id] = std::move(id_map);
            size_t matched = 0;
            for (auto v : id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
            std::cerr << "  " << fname << ": " << written << " bytes (mapped " << matched << "/" << id_remaps[file_id].size() << ")" << std::endl;
        } else {
            std::cerr << "  " << fname << ": " << written << " bytes" << std::endl;
        }

        // Free old data immediately — don't let glibc hold the pages
        { std::vector<char>().swap(old_data); }
        malloc_trim(0);
    }
    log_phase("Merge replays", t_start);

    // Free string remap (no longer needed)
    { std::unordered_map<uint32_t,uint32_t>().swap(str_remap); }
    malloc_trim(0);

    // --- Phase 4: Streaming entry pipeline ---
    // Walk old geo_cells, remap IDs, apply corrections, write output directly to files.
    {
        double t_entry = now_ms();
        std::cerr << "Entry pipeline..." << std::endl;

        auto get_rm = [&](PatchFileId fid) -> const std::vector<uint32_t>& {
            static std::vector<uint32_t> empty;
            auto it = id_remaps.find((uint32_t)fid); return it != id_remaps.end() ? it->second : empty;
        };
        auto& w_rm = get_rm(PatchFileId::STREET_WAYS);
        auto& a_rm = get_rm(PatchFileId::ADDR_POINTS);
        auto& i_rm = get_rm(PatchFileId::INTERP_WAYS);

        // Build correction lookup maps
        std::unordered_map<uint64_t, const std::vector<uint32_t>*> cs, ca, ci;
        for (auto& [fid, list] : entry_corrections) {
            auto* m = (fid == (uint32_t)PatchFileId::STREET_ENTRIES) ? &cs :
                      (fid == (uint32_t)PatchFileId::ADDR_ENTRIES) ? &ca :
                      (fid == (uint32_t)PatchFileId::INTERP_ENTRIES) ? &ci : nullptr;
            if (m) for (auto& c : list) (*m)[c.cell_id] = &c.ids;
        }

        // mmap old files (zero-copy read)
        MappedFile old_geo = mmap_file(cur_dir + "/geo_cells.bin");
        MappedFile old_se = mmap_file(cur_dir + "/street_entries.bin");
        MappedFile old_ae = mmap_file(cur_dir + "/addr_entries.bin");
        MappedFile old_ie = mmap_file(cur_dir + "/interp_entries.bin");

        // Build derived cell list
        std::unordered_set<uint64_t> rm_set(geo_removed.begin(), geo_removed.end());
        struct CR { uint64_t cid; int32_t idx; };
        std::vector<CR> cells;
        cells.reserve(old_geo.size / 20 + geo_added.size());
        for (size_t i = 0; i < old_geo.size / 20; i++) {
            uint64_t cid; memcpy(&cid, old_geo.data + i*20, 8);
            if (!rm_set.count(cid)) cells.push_back({cid, (int32_t)i});
        }
        for (uint64_t cid : geo_added) cells.push_back({cid, -1});
        std::sort(cells.begin(), cells.end(), [](const CR& a, const CR& b) { return a.cid < b.cid; });
        log_phase("  Cell list", t_entry);

        // Open output files for streaming write
        FILE* f_geo = fopen((out_dir + "/geo_cells.bin").c_str(), "wb");
        FILE* f_se = fopen((out_dir + "/street_entries.bin").c_str(), "wb");
        FILE* f_ae = fopen((out_dir + "/addr_entries.bin").c_str(), "wb");
        FILE* f_ie = fopen((out_dir + "/interp_entries.bin").c_str(), "wb");
        uint32_t se_pos = 0, ae_pos = 0, ie_pos = 0;
        uint32_t no_data = 0xFFFFFFFF;

        // Reusable buffer
        std::vector<uint32_t> id_buf;
        id_buf.reserve(256);

        auto parse_ids = [](const MappedFile& f, uint32_t off, std::vector<uint32_t>& buf) {
            buf.clear();
            if (off == 0xFFFFFFFF || off + 2 > f.size) return;
            uint16_t c; memcpy(&c, f.data+off, 2);
            if (c > 10000 || off+2+(size_t)c*4 > f.size) return; // sanity: no cell has >10K entries
            buf.resize(c); memcpy(buf.data(), f.data+off+2, c*4);
        };
        auto remap_buf = [](std::vector<uint32_t>& buf, const std::vector<uint32_t>& rm) {
            for (auto& id : buf) if (id < rm.size() && rm[id] != 0xFFFFFFFF) id = rm[id];
            std::sort(buf.begin(), buf.end());
        };
        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n, uint32_t& fpos) -> uint32_t {
            if (n == 0) return 0xFFFFFFFF;
            uint32_t off = fpos;
            uint16_t c = (uint16_t)n;
            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);
            uint64_t new_fpos = (uint64_t)fpos + 2 + n*4;
            fpos = (uint32_t)new_fpos; // Note: wraps at 4GiB
            return off;
        };
        size_t total_writes_s = 0, total_writes_a = 0, total_writes_i = 0;

        size_t sc = 0, ac = 0, ic = 0;
        size_t cell_count = 0;
        for (auto& [cid, oi] : cells) {
            if (++cell_count % 5000000 == 0) {
                // Also check actual file sizes vs tracked positions
                long se_actual = ftell(f_se), ae_actual = ftell(f_ae), ie_actual = ftell(f_ie);
                std::cerr << "    " << cell_count << "/" << cells.size()
                          << " se_pos=" << se_pos << " se_ftell=" << se_actual
                          << " ae_pos=" << ae_pos << " ae_ftell=" << ae_actual
                          << " rss=" << get_rss_mb() << "M" << std::endl;
            }
            // Process one entry type at a time using shared id_buf
            auto process = [&](const MappedFile& old_e, size_t geo_off,
                               const std::vector<uint32_t>& rm,
                               const std::unordered_map<uint64_t, const std::vector<uint32_t>*>& corr,
                               FILE* outf, uint32_t& fpos, size_t& cc) -> uint32_t {
                auto cit = corr.find(cid);
                if (cit != corr.end()) { cc++; return write_ids(outf, cit->second->data(), cit->second->size(), fpos); }
                if (oi < 0) return no_data;
                uint32_t off; memcpy(&off, old_geo.data + oi*20 + geo_off, 4);
                parse_ids(old_e, off, id_buf);
                if (id_buf.empty()) return no_data;
                remap_buf(id_buf, rm);
                return write_ids(outf, id_buf.data(), id_buf.size(), fpos);
            };

            // Determine flags
            bool hs = false, ha = false, hi = false;
            if (oi >= 0) {
                uint32_t s, a, ip; memcpy(&s, old_geo.data+oi*20+8, 4); memcpy(&a, old_geo.data+oi*20+12, 4); memcpy(&ip, old_geo.data+oi*20+16, 4);
                hs = (s != no_data); ha = (a != no_data); hi = (ip != no_data);
            }
            auto fc = flag_corrections.find(cid);
            if (fc != flag_corrections.end()) { hs = fc->second & 1; ha = fc->second & 2; hi = fc->second & 4; }
            if (cs.count(cid)) hs = true;
            if (ca.count(cid)) ha = true;
            if (ci.count(cid)) hi = true;

            uint32_t so = hs ? process(old_se, 8, w_rm, cs, f_se, se_pos, sc) : no_data;
            uint32_t ao = ha ? process(old_ae, 12, a_rm, ca, f_ae, ae_pos, ac) : no_data;
            uint32_t io = hi ? process(old_ie, 16, i_rm, ci, f_ie, ie_pos, ic) : no_data;
            if (cell_count == 1) {
                // Count how many old cells have each flag
                size_t os = 0, oa = 0, oi_c = 0;
                for (size_t ci = 0; ci < old_geo.size / 20; ci++) {
                    uint32_t sv, av, iv;
                    memcpy(&sv, old_geo.data + ci*20+8, 4); memcpy(&av, old_geo.data + ci*20+12, 4); memcpy(&iv, old_geo.data + ci*20+16, 4);
                    if (sv != no_data) os++; if (av != no_data) oa++; if (iv != no_data) oi_c++;
                }
                std::cerr << "    DEBUG: old cells=" << old_geo.size/20 << " has_street=" << os << " has_addr=" << oa << " has_interp=" << oi_c << std::endl;
            }

            fwrite(&cid, 8, 1, f_geo); fwrite(&so, 4, 1, f_geo); fwrite(&ao, 4, 1, f_geo); fwrite(&io, 4, 1, f_geo);
        }
        fclose(f_geo); fclose(f_se); fclose(f_ae); fclose(f_ie);
        unmap_file(old_geo); unmap_file(old_se); unmap_file(old_ae); unmap_file(old_ie);
        std::cerr << "  Geo: " << cells.size() << " cells, corrections: " << sc << " st, " << ac << " ad, " << ic << " in" << std::endl;
        log_phase("  Geo entries", t_entry);

        // Admin: small, use existing rebuild + corrections
        {
            std::unordered_map<uint32_t,uint32_t> ad_rm;
            auto ait = id_remaps.find((uint32_t)PatchFileId::ADMIN_POLYGONS);
            if (ait != id_remaps.end()) {
                for (uint32_t i = 0; i < ait->second.size(); i++)
                    if (ait->second[i] != 0xFFFFFFFF) ad_rm[i] = ait->second[i];
            }
            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
            auto old_adme = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_adme, ad_rm, admin_added, admin_removed);

            auto ecit = entry_corrections.find((uint32_t)PatchFileId::ADMIN_ENTRIES);
            if (ecit != entry_corrections.end()) {
                std::unordered_map<uint64_t, const std::vector<uint32_t>*> ac_corr;
                for (auto& c : ecit->second) ac_corr[c.cell_id] = &c.ids;
                size_t n = admin.admin_cells_data.size() / 12;
                FILE* fac = fopen((out_dir + "/admin_cells.bin").c_str(), "wb");
                FILE* fae = fopen((out_dir + "/admin_entries.bin").c_str(), "wb");
                uint32_t ae_wpos = 0;
                for (size_t i = 0; i < n; i++) {
                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data()+i*12, 8);
                    fwrite(&cid, 8, 1, fac);
                    auto cit = ac_corr.find(cid);
                    if (cit != ac_corr.end()) {
                        uint32_t off = cit->second->empty() ? no_data : ae_wpos;
                        fwrite(&off, 4, 1, fac);
                        if (!cit->second->empty()) {
                            uint16_t c = cit->second->size();
                            fwrite(&c, 2, 1, fae); fwrite(cit->second->data(), 4, c, fae);
                            ae_wpos += 2 + c*4;
                        }
                    } else {
                        uint32_t old_off; memcpy(&old_off, admin.admin_cells_data.data()+i*12+8, 4);
                        if (old_off != no_data && old_off+2 <= admin.admin_entries_data.size()) {
                            uint32_t new_off = ae_wpos; fwrite(&new_off, 4, 1, fac);
                            uint16_t c; memcpy(&c, admin.admin_entries_data.data()+old_off, 2);
                            fwrite(admin.admin_entries_data.data()+old_off, 1, 2+c*4, fae);
                            ae_wpos += 2+c*4;
                        } else {
                            fwrite(&no_data, 4, 1, fac);
                        }
                    }
                }
                fclose(fac); fclose(fae);
                std::cerr << "  Admin: " << n << " cells, " << ac_corr.size() << " corrections" << std::endl;
            } else {
                write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
                write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
            }
            log_phase("  Admin", t_entry);
        }
    }

    // Cleanup
    unmap_file(patch_map);
    { std::string cmd = "rm -rf '" + tmpdir + "'"; system(cmd.c_str()); }
    log_phase("Total", t_start);
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}

-        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n, uint32_t& fpos) -> uint32_t {

+        auto write_ids = [](FILE* f, const uint32_t* ids, size_t n) -> uint32_t {

+            uint32_t off = (uint32_t)ftell(f);

-            uint64_t new_fpos = (uint64_t)fpos + 2 + n*4;

-            fpos = (uint32_t)new_fpos; // Note: wraps at 4GiB

-        size_t total_writes_s = 0, total_writes_a = 0, total_writes_i = 0;


Now update all `write_ids` call sites to remove the `fpos` parameter:


No matches found


449:                if (cit != corr.end()) { cc++; return write_ids(outf, cit->second->data(), cit->second->size(), fpos); }
455:                return write_ids(outf, id_buf.data(), id_buf.size(), fpos);