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

// 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) {
            // Duplicate string diff in main loop — skip (already processed)
            uint32_t na = ru32(), nd = ru32();
            for (uint32_t i = 0; i < na; i++) pos += strlen(P+pos)+1;
            for (uint32_t i = 0; i < nd; i++) ru32();
            // Rebuild string pool + remap (same as above)
            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; }
            // Already wrote strings.bin in phase 2
            unmap_file(old_pool);
            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 (off+2+c*4 > f.size) return;
            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);
            fpos += 2 + n*4;
            return off;
        };

        size_t sc = 0, ac = 0, ic = 0;
        for (auto& [cid, oi] : cells) {
            // 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;

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

-// geocoder-patch v3: Apply custom patch format.

+// geocoder-patch v4: Low-memory streaming patch application.

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

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

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

struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);

return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;

-    std::ifstream f("/proc/self/statm");

-    size_t pages = 0; f >> pages; // first field is total pages, second is RSS

+    FILE* f = fopen("/proc/self/statm", "r");

+    size_t dummy, rss; fscanf(f, "%zu %zu", &dummy, &rss); fclose(f);

static void log_phase(const char* label, double start) {

<< "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 (sz == 0) { close(fd); return {nullptr, 0}; }

+    void* p = mmap(nullptr, sz, PROT_READ, MAP_PRIVATE, fd, 0);

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

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

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

if (argc < 5 || std::string(argv[3]) != "-o") {

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

-    // Decompress zstd transport layer to temp file, then mmap it

+    // --- Phase 1: Decompress + mmap patch ---

std::string raw_path = tmpdir + "/patch.raw";

-        if (system(cmd.c_str()) != 0) {

-            std::cerr << "Failed to decompress patch" << std::endl; return 1;

+        if (system(cmd.c_str()) != 0) { std::cerr << "Failed to decompress" << std::endl; return 1; }

-    int patch_fd = open(raw_path.c_str(), O_RDONLY);

-    if (patch_fd < 0) { std::cerr << "Failed to open decompressed patch" << std::endl; return 1; }

-    size_t patch_size = patch_stat.st_size;

-    const char* patch_mmap = static_cast<const char*>(

-        mmap(nullptr, patch_size, PROT_READ, MAP_PRIVATE, patch_fd, 0));

-    if (patch_mmap == MAP_FAILED) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }

-    // Advise sequential access for better readahead

-    madvise(const_cast<char*>(patch_mmap), patch_size, MADV_SEQUENTIAL);

+    MappedFile patch_map = mmap_file(raw_path);

+    if (!patch_map.data) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }

-    std::cerr << "Decompressed patch: " << patch_size << " bytes (mmap'd)" << std::endl;

-    log_phase("Decompress + mmap", t_start);

+    size_t patch_size = patch_map.size;

+    std::cerr << "Patch: " << patch_size << " bytes" << std::endl;

-    // Provide a patch data pointer for consistent access

-    const char* patch_data = patch_mmap;

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

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

-    if (memcmp(patch_data, GCPATCH_MAGIC, 8) != 0) {

-        std::cerr << "Invalid patch magic" << std::endl; return 1;

+    if (memcmp(P, GCPATCH_MAGIC, 8) != 0) { std::cerr << "Bad magic" << std::endl; return 1; }

-    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }

+    uint32_t ver = ru32(); if (ver != 2) { std::cerr << "Bad version" << std::endl; return 1; }

-    // Read string-level diff OR string remap

-            // String-level diff: merge new/deleted strings into old pool

-            uint32_t n_added = read_u32(), n_deleted = read_u32();

+            uint32_t n_added = ru32(), n_deleted = ru32();

-            for (uint32_t i = 0; i < n_added; i++) {

-                const char* s = patch_data + pos;

+            for (uint32_t i = 0; i < n_added; i++) { const char* s = P+pos; added.push_back(s); pos += strlen(s)+1; }

-            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();

+            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = ru32();

-            auto old_pool = read_file(cur_dir + "/strings.bin");

+            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");

-            while (sp < old_pool.size()) {

-                const char* s = old_pool.data() + sp;

-                old_strs.push_back({(uint32_t)sp, std::string(s)});

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

-            // Merge: keep non-deleted + add new, sort

for (size_t i = 0; i < old_strs.size(); i++)

for (auto& s : added) merged.push_back(s);

-            std::vector<char> new_pool;

+            // Write new pool and build remap

+            FILE* fp = fopen((out_dir + "/strings.bin").c_str(), "wb");

-                new_offs[s] = (uint32_t)new_pool.size();

-                new_pool.insert(new_pool.end(), s.begin(), s.end());

-                new_pool.push_back('\0');

+                fwrite(s.c_str(), 1, s.size() + 1, fp);

-            write_file(out_dir + "/strings.bin", new_pool);

-                if (it != new_offs.end() && it->second != old_off)

-                    str_remap[old_off] = it->second;

+                if (it != new_offs.end() && it->second != old_off) str_remap[old_off] = it->second;

-            std::cerr << "Rebuilt strings: +" << n_added << " -" << n_deleted

-                      << " → " << merged.size() << " strings, "

-                      << str_remap.size() << " remapped" << std::endl;

-            marker = read_u32(); // read next section marker

+            std::cerr << "  Strings: +" << n_added << " -" << n_deleted << " → " << merged.size()

+                      << ", " << str_remap.size() << " remapped" << std::endl;

-            // Explicit remap entries (supplements the derived remap)

-            uint32_t count = read_u32();

-            for (uint32_t i = 0; i < count; i++) {

-                uint32_t old_off = read_u32(), new_off = read_u32();

-                str_remap[old_off] = new_off; // override with explicit

-            std::cerr << "Total string remap: " << str_remap.size() << " entries" << std::endl;

-            // Not a remap section — seek back

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

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

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

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

-    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {

+    // String remap applier (works on mutable copy)

+    auto apply_str_remap = [&](std::vector<char>& data, uint32_t fid, size_t stride) {

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

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

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

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

-    // ID remaps derived from merge sequences (populated during merge replay)

-    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;

-    // Read cell changes, flag corrections, and entry corrections

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

-    struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };

-    std::unordered_map<uint32_t, std::vector<CellCorrection>> entry_cell_corrections;

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

-            // String-level diff (appears in main section loop)

-            uint32_t n_added = read_u32(), n_deleted = read_u32();

-            std::vector<std::string> added;

-            for (uint32_t i = 0; i < n_added; i++) {

-                const char* s = patch_data + pos;

-            std::vector<uint32_t> del_idx(n_deleted);

-            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();

-            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

-            auto old_pool = read_file(cur_dir + "/strings.bin");

-            std::vector<std::pair<uint32_t, std::string>> old_strs;

+            // Duplicate string diff in main loop — skip (already processed)

+            for (uint32_t i = 0; i < na; i++) pos += strlen(P+pos)+1;

+            // Rebuild string pool + remap (same as above)

+            MappedFile old_pool = mmap_file(cur_dir + "/strings.bin");

+            std::vector<std::pair<uint32_t,std::string>> old_strs;

-            while (sp < old_pool.size()) {

-                const char* s = old_pool.data() + sp;

-                old_strs.push_back({(uint32_t)sp, std::string(s)});

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

-            std::vector<char> new_pool;

-            std::unordered_map<std::string, uint32_t> new_offs;

-                new_offs[s] = (uint32_t)new_pool.size();

-                new_pool.insert(new_pool.end(), s.begin(), s.end());

-                new_pool.push_back('\0');

-            write_file(out_dir + "/strings.bin", new_pool);

-            // Update remap with newly computed offsets

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

-            std::cerr << "  Rebuilt strings: +" << n_added << " -" << n_deleted

-                      << " → " << merged.size() << " strings, "

-                      << str_remap.size() << " remapped" << std::endl;

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

+            // Already wrote strings.bin in phase 2

if (file_id == CELL_CHANGES_GEO_MARKER) {

-            uint32_t na = read_u32(), nr = read_u32();

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

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

if (file_id == CELL_CHANGES_ADMIN_MARKER) {

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

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

-            uint32_t count = read_u32();

-            for (uint32_t i = 0; i < count; i++) {

-                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;

-                uint8_t flags = static_cast<uint8_t>(patch_data[pos]); pos++;

-                cell_flag_corrections[cid] = flags;

-            std::cerr << "  Cell flag corrections: " << count << " cells" << std::endl;

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

if (file_id == SECONDARY_REMAP_MARKER) {

-            // Secondary ID remaps: additional old→new mappings for modified records.

-            // These are applied alongside the merge-derived remaps for entry reconstruction.

-            uint32_t n_files = read_u32();

-            for (uint32_t f = 0; f < n_files; f++) {

-                uint32_t sec_fid = read_u32();

-                uint32_t n_pairs = read_u32();

-                auto& id_map = derived_id_remaps[sec_fid];

-                for (uint32_t p = 0; p < n_pairs; p++) {

-                    uint32_t old_id = read_u32(), new_id = read_u32();

-                    if (old_id < id_map.size())

-                        id_map[old_id] = new_id;

-                std::cerr << "  Secondary remap for file " << sec_fid << ": "

-                          << n_pairs << " pairs" << std::endl;

+                auto& rm = id_remaps[fid];

+                std::cerr << "  Secondary remap " << fid << ": " << np << " pairs" << std::endl;

if (file_id == ENTRY_CORRECTION_MARKER) {

-            uint32_t corr_fid = read_u32();

-            uint32_t count = read_u32();

-            // Buffer cell-level corrections for application after entry reconstruction

-            struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };

-            auto& corr_list = entry_cell_corrections[corr_fid];

-            for (uint32_t i = 0; i < count; i++) {

-                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;

-                uint16_t ec; memcpy(&ec, patch_data + pos, 2); pos += 2;

+            auto& list = entry_corrections[fid];

+                uint64_t cid; memcpy(&cid, P+pos, 8); pos += 8;

+                uint16_t ec; memcpy(&ec, P+pos, 2); pos += 2;

-                if (ec > 0) { memcpy(ids.data(), patch_data + pos, ec * 4); pos += ec * 4; }

-                corr_list.push_back({cid, std::move(ids)});

+                if (ec > 0) { memcpy(ids.data(), P+pos, ec*4); pos += ec*4; }

+                list.push_back({cid, std::move(ids)});

-            std::cerr << "  Entry correction for file " << corr_fid << ": " << count << " cells" << std::endl;

+            std::cerr << "  Entry corrections " << fid << ": " << c << std::endl;

-        uint64_t old_size = read_u64(), new_size = read_u64();

+        // --- Merge sequence replay (streaming) ---

+        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 (file_id >= (uint32_t)PatchFileId::COUNT) {

-            std::cerr << "  Unknown file " << file_id << std::endl;

-        const char* filename = patch_file_names[file_id];

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

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

+            std::cerr << "  " << fname << ": full replace " << ds << " bytes" << std::endl;

-            // Legacy zstd correction — skip (replaced by ENTRY_CORRECTION_MARKER)

-            uint64_t delta_size = read_u64();

-            std::cerr << "  " << filename << ": legacy correction skipped" << std::endl;

+        if (stride == 0xFE) { uint32_t nf = ru32(); (void)nf; uint64_t ds = ru64(); pos += ds; continue; }

-        // Merge sequence: load old file, apply remap + fixups, replay sequence

-        auto old_data = read_file(cur_dir + "/" + std::string(filename));

if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;

-        // Zero struct padding bytes for deterministic comparison

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

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

-        // Apply string remap to old data

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

-        // Read and apply fixups (node_offset/vertex_offset fixes)

-        // Read and apply delta-encoded fixups

-        uint32_t n_fixups = read_u32();

-            uint32_t delta_size = read_u32();

-            uint32_t prev_idx = 0, prev_val = 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 = prev_idx + read_varint(patch_data, fpos);

-                uint32_t val = prev_val + read_varint(patch_data, fpos);

-                size_t byte_pos = (size_t)idx * actual_stride;

-                if (byte_pos + 4 <= old_data.size())

-                    memcpy(old_data.data() + byte_pos, &val, 4);

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

-            std::cerr << "    Applied " << n_fixups << " fixups (" << delta_size << " bytes delta)" << std::endl;

-        // Read merge sequence size and replay, tracking old→new ID mapping

-        uint64_t seq_size = read_u64();

-        size_t old_rec = 0, new_rec = 0; // record indices

+        // Replay merge sequence — stream output directly to file

+        FILE* outf = fopen((out_dir + "/" + fname).c_str(), "wb");

-        // ID remap for this file (old_record_index → new_record_index)

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

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

+        if (track) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);

+        size_t old_rec = 0, new_rec = 0, old_bytes = 0, written = 0;

-            uint8_t op = static_cast<uint8_t>(patch_data[pos]); pos++;

-            uint32_t count; memcpy(&count, patch_data + pos, 4); pos += 4;

+            uint32_t count; memcpy(&count, P+pos, 4); pos += 4;

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

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

+                fwrite(old_data.data() + old_bytes, 1, bytes, outf);

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

-                output.insert(output.end(), patch_data + pos, patch_data + pos + bytes);

+                fwrite(P+pos, 1, bytes, outf);

+                written += bytes; pos += bytes; new_rec += count;

-                old_pos_bytes += count * actual_stride;

+                old_rec += count; old_bytes += count * actual_stride;

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

-        // Store ID remap for entry reconstruction

-            derived_id_remaps[file_id] = std::move(id_map);

+            id_remaps[file_id] = std::move(id_map);

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

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

-            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;

+            std::cerr << "  " << fname << ": " << written << " bytes" << std::endl;

+        // Free old data immediately — don't let glibc hold the pages

+        { std::vector<char>().swap(old_data); }

+    log_phase("Merge replays", t_start);

-    // --- Streaming entry pipeline: remap + correct + write in one pass ---

-    // Replaces separate rebuild + 4 correction passes with a single walk over cells.

-    // Memory: ~3 GiB instead of ~63 GiB. Time: ~2 min instead of ~20 min.

+    // Free string remap (no longer needed)

+    { std::unordered_map<uint32_t,uint32_t>().swap(str_remap); }

+    // --- Phase 4: Streaming entry pipeline ---

+    // Walk old geo_cells, remap IDs, apply corrections, write output directly to files.

-        log_phase("Merge replays complete", t_start);

-        // Free fragmented heap from merge replay phase (glibc doesn't return pages to OS)

-        derived_id_remaps.rehash(derived_id_remaps.size()); // compact

-        log_phase("Heap compacted", t_start);

-        std::cerr << "Streaming entry pipeline..." << std::endl;

+        std::cerr << "Entry pipeline..." << std::endl;

-        // Get ID remap vectors (already populated during merge replay + secondary remaps)

-        auto get_remap = [&](PatchFileId fid) -> const std::vector<uint32_t>& {

+        auto get_rm = [&](PatchFileId fid) -> const std::vector<uint32_t>& {

-            auto it = derived_id_remaps.find((uint32_t)fid);

-            return it != derived_id_remaps.end() ? it->second : empty;

+            auto it = id_remaps.find((uint32_t)fid); return it != id_remaps.end() ? it->second : empty;

-        auto& w_rm = get_remap(PatchFileId::STREET_WAYS);

-        auto& a_rm = get_remap(PatchFileId::ADDR_POINTS);

-        auto& i_rm = get_remap(PatchFileId::INTERP_WAYS);

+        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 (cell_id → entry IDs) for each geo entry type

-        std::unordered_map<uint64_t, const std::vector<uint32_t>*> corr_se, corr_ae, corr_ie;

-        for (auto& [fid, corrections] : entry_cell_corrections) {

-            std::unordered_map<uint64_t, const std::vector<uint32_t>*>* target = nullptr;

-            if (fid == (uint32_t)PatchFileId::STREET_ENTRIES) target = &corr_se;

-            else if (fid == (uint32_t)PatchFileId::ADDR_ENTRIES) target = &corr_ae;

-            else if (fid == (uint32_t)PatchFileId::INTERP_ENTRIES) target = &corr_ie;

-            if (target) for (auto& c : corrections) (*target)[c.cell_id] = &c.ids;

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

-        log_phase("  Correction maps built", t_entry);

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

+        // 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: old cells - removed + added (sorted)

-        std::unordered_set<uint64_t> removed_set(geo_removed.begin(), geo_removed.end());

-        struct CellRef { uint64_t cid; int32_t old_idx; }; // old_idx = -1 for added 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 (!removed_set.count(cid)) cells.push_back({cid, (int32_t)i});

+        std::unordered_set<uint64_t> rm_set(geo_removed.begin(), geo_removed.end());

+        struct CR { uint64_t cid; int32_t idx; };

+        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 CellRef& a, const CellRef& b) { return a.cid < b.cid; });

-        log_phase("  Cell list built", t_entry);

+        std::sort(cells.begin(), cells.end(), [](const CR& a, const CR& b) { return a.cid < b.cid; });

+        log_phase("  Cell list", t_entry);

-        // Reusable buffer for entry IDs (avoids 45M allocs on planet)

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

-        id_buf.reserve(256); // most cells have <256 entries

-        // Helper: parse entry IDs into reusable buffer, return count

-        auto parse_ids_into = [&id_buf](const std::vector<char>& data, uint32_t off) -> size_t {

-            if (off == 0xFFFFFFFF || off + 2 > data.size()) return 0;

-            uint16_t count; memcpy(&count, data.data() + off, 2);

-            if (off + 2 + count * 4 > data.size()) return 0;

-            memcpy(id_buf.data(), data.data() + off + 2, count * 4);

+        auto parse_ids = [](const MappedFile& f, uint32_t off, std::vector<uint32_t>& buf) {

+            if (off == 0xFFFFFFFF || off + 2 > f.size) return;

+            uint16_t c; memcpy(&c, f.data+off, 2);

+            if (off+2+c*4 > f.size) return;

+            buf.resize(c); memcpy(buf.data(), f.data+off+2, c*4);

-        // Helper: remap IDs in-place using vector (O(1) per ID)

-        auto remap_ids = [](std::vector<uint32_t>& ids, const std::vector<uint32_t>& rm) {

-            for (auto& id : ids) if (id < rm.size() && rm[id] != 0xFFFFFFFF) id = rm[id];

-            std::sort(ids.begin(), ids.end());

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

-        // Helper: write entry list from buffer/pointer and return offset

-        auto write_ids = [](std::vector<char>& buf, const uint32_t* ids, size_t count) -> uint32_t {

-            if (count == 0) return 0xFFFFFFFF;

-            uint32_t off = static_cast<uint32_t>(buf.size());

-            uint16_t c = static_cast<uint16_t>(count);

-            buf.insert(buf.end(), (const char*)&c, (const char*)&c + 2);

-            buf.insert(buf.end(), (const char*)ids, (const char*)ids + count * 4);

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

+            if (n == 0) return 0xFFFFFFFF;

+            fwrite(&c, 2, 1, f); fwrite(ids, 4, n, f);

-        // Single pass: walk cells, remap or apply correction, write all outputs

-        std::vector<char> out_se, out_ae, out_ie, out_geo;

-        out_geo.reserve(cells.size() * 20);

-        // Pre-reserve output entry buffers (approximate sizes from old files)

-        out_se.reserve(old_se.size() + old_se.size() / 20);

-        out_ae.reserve(old_ae.size() + old_ae.size() / 20);

-        out_ie.reserve(old_ie.size() + old_ie.size() / 20);

-        size_t se_corr = 0, ae_corr = 0, ie_corr = 0;

-            // For each entry type: get remapped IDs or correction

-            auto process_entry = [&](const std::vector<char>& old_entries, size_t geo_off_pos,

-                                      const std::vector<uint32_t>& rm,

-                                      const std::unordered_map<uint64_t, const std::vector<uint32_t>*>& corr_map,

-                                      std::vector<char>& out_buf, size_t& corr_count) -> uint32_t {

-                // Check for correction first

-                auto cit = corr_map.find(cid);

-                if (cit != corr_map.end()) {

-                    return write_ids(out_buf, cit->second->data(), cit->second->size());

-                // No correction: remap old IDs using shared buffer

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

-                uint32_t off; memcpy(&off, old_geo.data() + oi * 20 + geo_off_pos, 4);

-                size_t n = parse_ids_into(old_entries, off);

-                if (n == 0) return no_data;

-                return write_ids(out_buf, id_buf.data(), id_buf.size());

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

+                return write_ids(outf, id_buf.data(), id_buf.size(), fpos);

-            // Determine has-flags (from old cell flags + flag corrections)

-            bool has_s = false, has_a = false, has_i = false;

+            bool hs = false, ha = false, hi = false;

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

-                has_s = (s != no_data); has_a = (a != no_data); has_i = (ip != no_data);

+                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_it = cell_flag_corrections.find(cid);

-            if (fc_it != cell_flag_corrections.end()) {

-                has_s = (fc_it->second & 1) != 0;

-                has_a = (fc_it->second & 2) != 0;

-                has_i = (fc_it->second & 4) != 0;

-            // Corrections can also introduce entries for cells that didn't have them

-            if (corr_se.count(cid)) has_s = true;

-            if (corr_ae.count(cid)) has_a = true;

-            if (corr_ie.count(cid)) has_i = true;

+            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 s_off = no_data, a_off = no_data, i_off = no_data;

-            if (has_s) s_off = process_entry(old_se, 8, w_rm, corr_se, out_se, se_corr);

-            if (has_a) a_off = process_entry(old_ae, 12, a_rm, corr_ae, out_ae, ae_corr);

-            if (has_i) i_off = process_entry(old_ie, 16, i_rm, corr_ie, out_ie, ie_corr);

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

-            out_geo.insert(out_geo.end(), (const char*)&cid, (const char*)&cid + 8);

-            out_geo.insert(out_geo.end(), (const char*)&s_off, (const char*)&s_off + 4);

-            out_geo.insert(out_geo.end(), (const char*)&a_off, (const char*)&a_off + 4);

-            out_geo.insert(out_geo.end(), (const char*)&i_off, (const char*)&i_off + 4);

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

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

-        write_file(out_dir + "/street_entries.bin", out_se);

-        write_file(out_dir + "/addr_entries.bin", out_ae);

-        write_file(out_dir + "/interp_entries.bin", out_ie);

-        std::cerr << "  Geo entries: " << cells.size() << " cells, corrections applied: "

-                  << se_corr << " street, " << ae_corr << " addr, " << ie_corr << " interp" << std::endl;

-        log_phase("  Geo entry pipeline", t_entry);

-        // Admin: small, keep existing approach

+        // Admin: small, use existing rebuild + corrections

-            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 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 ait = entry_cell_corrections.find((uint32_t)PatchFileId::ADMIN_ENTRIES);

-            if (ait != entry_cell_corrections.end()) {

-                // Parse derived admin entries by cell

-                std::unordered_map<uint64_t, size_t> ac_idx;

-                for (size_t i = 0; i < admin.admin_cells_data.size() / 12; i++) {

-                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data() + i * 12, 8);

-                // Build corrected entries

-                std::vector<char> new_ae;

-                std::vector<char> new_ac;

-                size_t n = admin.admin_cells_data.size() / 12;

-                uint32_t no_data_v = 0xFFFFFFFF;

+            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 : ait->second) ac_corr[c.cell_id] = &c.ids;

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

-                    new_ac.insert(new_ac.end(), (const char*)&cid, (const char*)&cid + 8);

+                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data()+i*12, 8);

+                    fwrite(&cid, 8, 1, fac);

-                        // Use correction

-                        uint32_t off = cit->second->empty() ? no_data_v : (uint32_t)new_ae.size();

-                        new_ac.insert(new_ac.end(), (const char*)&off, (const char*)&off + 4);

+                        uint32_t off = cit->second->empty() ? no_data : ae_wpos;

+                        fwrite(&off, 4, 1, fac);

-                            uint16_t count = cit->second->size();

-                            new_ae.insert(new_ae.end(), (const char*)&count, (const char*)&count + 2);

-                            new_ae.insert(new_ae.end(), (const char*)cit->second->data(),

-                                         (const char*)cit->second->data() + count * 4);

+                            uint16_t c = cit->second->size();

+                            fwrite(&c, 2, 1, fae); fwrite(cit->second->data(), 4, c, fae);

-                        uint32_t old_off; memcpy(&old_off, admin.admin_cells_data.data() + i * 12 + 8, 4);

-                        if (old_off != no_data_v && old_off + 2 <= admin.admin_entries_data.size()) {

-                            uint32_t new_off = (uint32_t)new_ae.size();

-                            new_ac.insert(new_ac.end(), (const char*)&new_off, (const char*)&new_off + 4);

-                            uint16_t count; memcpy(&count, admin.admin_entries_data.data() + old_off, 2);

-                            new_ae.insert(new_ae.end(), admin.admin_entries_data.data() + old_off,

-                                         admin.admin_entries_data.data() + old_off + 2 + count * 4);

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

-                            new_ac.insert(new_ac.end(), (const char*)&no_data_v, (const char*)&no_data_v + 4);

+                            fwrite(&no_data, 4, 1, fac);

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

-                write_file(out_dir + "/admin_entries.bin", new_ae);

+                fclose(fac); fclose(fae);

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

write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);

-                std::cerr << "  Admin: " << admin.admin_cells_data.size() / 12 << " cells, no corrections" << std::endl;

-            log_phase("  Admin pipeline", t_entry);

+            log_phase("  Admin", t_entry);

-    munmap(const_cast<char*>(patch_mmap), patch_size);

-    std::string rm_cmd = "rm -rf '" + tmpdir + "'";

-    log_phase("Total patch application", t_start);

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

std::cerr << "Patch applied. Output in " << out_dir << 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 <ctime>
#include <fcntl.h>
#include <malloc.h>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <string>
#include <sys/mman.h>
#include <sys/stat.h>
#include <thread>
#include <unordered_map>
#include <unistd.h>
#include <vector>

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() {
    std::ifstream f("/proc/self/statm");
    size_t pages = 0; f >> pages; // first field is total pages, second is RSS
    f >> pages; // RSS in pages
    return pages * 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;
}

#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 to temp file, then mmap it
    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;
        }
    }
    int patch_fd = open(raw_path.c_str(), O_RDONLY);
    if (patch_fd < 0) { std::cerr << "Failed to open decompressed patch" << std::endl; return 1; }
    struct stat patch_stat;
    fstat(patch_fd, &patch_stat);
    size_t patch_size = patch_stat.st_size;
    const char* patch_mmap = static_cast<const char*>(
        mmap(nullptr, patch_size, PROT_READ, MAP_PRIVATE, patch_fd, 0));
    if (patch_mmap == MAP_FAILED) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }
    // Advise sequential access for better readahead
    madvise(const_cast<char*>(patch_mmap), patch_size, MADV_SEQUENTIAL);
    close(patch_fd);
    remove(raw_path.c_str());
    double t_start = now_ms();
    std::cerr << "Decompressed patch: " << patch_size << " bytes (mmap'd)" << std::endl;
    log_phase("Decompress + mmap", t_start);

    // Provide a patch data pointer for consistent access
    const char* patch_data = patch_mmap;

    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-level diff OR string remap
    std::unordered_map<uint32_t, uint32_t> str_remap;
    {
        uint32_t marker = read_u32();

        if (marker == 0xFFFFFFF7) {
            // String-level diff: merge new/deleted strings into old pool
            uint32_t n_added = read_u32(), n_deleted = read_u32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) {
                const char* s = patch_data + 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] = read_u32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

            // Parse old pool
            auto old_pool = read_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size()) {
                const char* s = old_pool.data() + sp;
                old_strs.push_back({(uint32_t)sp, std::string(s)});
                sp += strlen(s) + 1;
            }

            // Merge: keep non-deleted + add new, sort
            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());

            // Build new pool
            std::vector<char> new_pool;
            std::unordered_map<std::string, uint32_t> new_offs;
            for (auto& s : merged) {
                new_offs[s] = (uint32_t)new_pool.size();
                new_pool.insert(new_pool.end(), s.begin(), s.end());
                new_pool.push_back('\0');
            }
            write_file(out_dir + "/strings.bin", new_pool);

            // Build remap
            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;
            }
            std::cerr << "Rebuilt strings: +" << n_added << " -" << n_deleted
                      << " → " << merged.size() << " strings, "
                      << str_remap.size() << " remapped" << std::endl;

            marker = read_u32(); // read next section marker
        }

        if (marker == 0xFFFFFFFE) {
            // Explicit remap entries (supplements the derived remap)
            uint32_t count = read_u32();
            for (uint32_t i = 0; i < count; i++) {
                uint32_t old_off = read_u32(), new_off = read_u32();
                str_remap[old_off] = new_off; // override with explicit
            }
            std::cerr << "Total string remap: " << str_remap.size() << " entries" << std::endl;
        } else {
            // Not a remap section — seek back
            pos -= 4;
        }
    }

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

    // Read cell changes, flag corrections, and entry corrections
    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;
    std::unordered_map<uint64_t, uint8_t> cell_flag_corrections;
    struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
    std::unordered_map<uint32_t, std::vector<CellCorrection>> entry_cell_corrections;

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

        // Cell change sections
        if (file_id == 0xFFFFFFF7) {
            // String-level diff (appears in main section loop)
            uint32_t n_added = read_u32(), n_deleted = read_u32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) {
                const char* s = patch_data + 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] = read_u32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

            auto old_pool = read_file(cur_dir + "/strings.bin");
            std::vector<std::pair<uint32_t, std::string>> old_strs;
            size_t sp = 0;
            while (sp < old_pool.size()) {
                const char* s = old_pool.data() + sp;
                old_strs.push_back({(uint32_t)sp, std::string(s)});
                sp += strlen(s) + 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());

            std::vector<char> new_pool;
            std::unordered_map<std::string, uint32_t> new_offs;
            for (auto& s : merged) {
                new_offs[s] = (uint32_t)new_pool.size();
                new_pool.insert(new_pool.end(), s.begin(), s.end());
                new_pool.push_back('\0');
            }
            write_file(out_dir + "/strings.bin", new_pool);
            // Update remap with newly computed offsets
            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;
            }
            std::cerr << "  Rebuilt strings: +" << n_added << " -" << n_deleted
                      << " → " << merged.size() << " strings, "
                      << str_remap.size() << " remapped" << std::endl;
            continue;
        }
        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;
        }
        if (file_id == CELL_FLAGS_MARKER) {
            uint32_t count = read_u32();
            for (uint32_t i = 0; i < count; i++) {
                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;
                uint8_t flags = static_cast<uint8_t>(patch_data[pos]); pos++;
                cell_flag_corrections[cid] = flags;
            }
            std::cerr << "  Cell flag corrections: " << count << " cells" << std::endl;
            continue;
        }
        if (file_id == SECONDARY_REMAP_MARKER) {
            // Secondary ID remaps: additional old→new mappings for modified records.
            // These are applied alongside the merge-derived remaps for entry reconstruction.
            uint32_t n_files = read_u32();
            for (uint32_t f = 0; f < n_files; f++) {
                uint32_t sec_fid = read_u32();
                uint32_t n_pairs = read_u32();
                auto& id_map = derived_id_remaps[sec_fid];
                for (uint32_t p = 0; p < n_pairs; p++) {
                    uint32_t old_id = read_u32(), new_id = read_u32();
                    if (old_id < id_map.size())
                        id_map[old_id] = new_id;
                }
                std::cerr << "  Secondary remap for file " << sec_fid << ": "
                          << n_pairs << " pairs" << std::endl;
            }
            continue;
        }
        if (file_id == ENTRY_CORRECTION_MARKER) {
            uint32_t corr_fid = read_u32();
            uint32_t count = read_u32();
            // Buffer cell-level corrections for application after entry reconstruction
            struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
            auto& corr_list = entry_cell_corrections[corr_fid];
            for (uint32_t i = 0; i < count; i++) {
                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;
                uint16_t ec; memcpy(&ec, patch_data + pos, 2); pos += 2;
                std::vector<uint32_t> ids(ec);
                if (ec > 0) { memcpy(ids.data(), patch_data + pos, ec * 4); pos += ec * 4; }
                corr_list.push_back({cid, std::move(ids)});
            }
            std::cerr << "  Entry correction for file " << corr_fid << ": " << count << " cells" << 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) {
            // Legacy zstd correction — skip (replaced by ENTRY_CORRECTION_MARKER)
            uint32_t n_fix = read_u32(); (void)n_fix;
            uint64_t delta_size = read_u64();
            pos += delta_size;
            std::cerr << "  " << filename << ": legacy correction skipped" << 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;

        // Zero struct padding bytes for deterministic comparison
        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 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 delta-encoded fixups
        uint32_t n_fixups = read_u32();
        if (n_fixups > 0) {
            uint32_t delta_size = read_u32();
            uint32_t prev_idx = 0, prev_val = 0;
            size_t fpos = pos;
            for (uint32_t i = 0; i < n_fixups; i++) {
                uint32_t idx = prev_idx + read_varint(patch_data, fpos);
                uint32_t val = prev_val + read_varint(patch_data, fpos);
                size_t byte_pos = (size_t)idx * actual_stride;
                if (byte_pos + 4 <= old_data.size())
                    memcpy(old_data.data() + byte_pos, &val, 4);
                prev_idx = idx;
                prev_val = val;
            }
            pos = fpos;
            std::cerr << "    Applied " << n_fixups << " fixups (" << delta_size << " bytes delta)" << 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_data[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;
        }
    }

    // --- Streaming entry pipeline: remap + correct + write in one pass ---
    // Replaces separate rebuild + 4 correction passes with a single walk over cells.
    // Memory: ~3 GiB instead of ~63 GiB. Time: ~2 min instead of ~20 min.
    {
        log_phase("Merge replays complete", t_start);
        // Free fragmented heap from merge replay phase (glibc doesn't return pages to OS)
        derived_id_remaps.rehash(derived_id_remaps.size()); // compact
        malloc_trim(0);
        log_phase("Heap compacted", t_start);
        double t_entry = now_ms();
        std::cerr << "Streaming entry pipeline..." << std::endl;

        // Get ID remap vectors (already populated during merge replay + secondary remaps)
        auto get_remap = [&](PatchFileId fid) -> const std::vector<uint32_t>& {
            static std::vector<uint32_t> empty;
            auto it = derived_id_remaps.find((uint32_t)fid);
            return it != derived_id_remaps.end() ? it->second : empty;
        };
        auto& w_rm = get_remap(PatchFileId::STREET_WAYS);
        auto& a_rm = get_remap(PatchFileId::ADDR_POINTS);
        auto& i_rm = get_remap(PatchFileId::INTERP_WAYS);

        // Build correction lookup maps (cell_id → entry IDs) for each geo entry type
        std::unordered_map<uint64_t, const std::vector<uint32_t>*> corr_se, corr_ae, corr_ie;
        for (auto& [fid, corrections] : entry_cell_corrections) {
            std::unordered_map<uint64_t, const std::vector<uint32_t>*>* target = nullptr;
            if (fid == (uint32_t)PatchFileId::STREET_ENTRIES) target = &corr_se;
            else if (fid == (uint32_t)PatchFileId::ADDR_ENTRIES) target = &corr_ae;
            else if (fid == (uint32_t)PatchFileId::INTERP_ENTRIES) target = &corr_ie;
            if (target) for (auto& c : corrections) (*target)[c.cell_id] = &c.ids;
        }
        log_phase("  Correction maps built", t_entry);

        // Load old files
        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");

        // Build derived cell list: old cells - removed + added (sorted)
        std::unordered_set<uint64_t> removed_set(geo_removed.begin(), geo_removed.end());
        struct CellRef { uint64_t cid; int32_t old_idx; }; // old_idx = -1 for added cells
        std::vector<CellRef> 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 (!removed_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 CellRef& a, const CellRef& b) { return a.cid < b.cid; });
        log_phase("  Cell list built", t_entry);

        // Reusable buffer for entry IDs (avoids 45M allocs on planet)
        std::vector<uint32_t> id_buf;
        id_buf.reserve(256); // most cells have <256 entries

        // Helper: parse entry IDs into reusable buffer, return count
        auto parse_ids_into = [&id_buf](const std::vector<char>& data, uint32_t off) -> size_t {
            id_buf.clear();
            if (off == 0xFFFFFFFF || off + 2 > data.size()) return 0;
            uint16_t count; memcpy(&count, data.data() + off, 2);
            if (off + 2 + count * 4 > data.size()) return 0;
            id_buf.resize(count);
            memcpy(id_buf.data(), data.data() + off + 2, count * 4);
            return count;
        };

        // Helper: remap IDs in-place using vector (O(1) per ID)
        auto remap_ids = [](std::vector<uint32_t>& ids, const std::vector<uint32_t>& rm) {
            for (auto& id : ids) if (id < rm.size() && rm[id] != 0xFFFFFFFF) id = rm[id];
            std::sort(ids.begin(), ids.end());
        };

        // Helper: write entry list from buffer/pointer and return offset
        auto write_ids = [](std::vector<char>& buf, const uint32_t* ids, size_t count) -> uint32_t {
            if (count == 0) return 0xFFFFFFFF;
            uint32_t off = static_cast<uint32_t>(buf.size());
            uint16_t c = static_cast<uint16_t>(count);
            buf.insert(buf.end(), (const char*)&c, (const char*)&c + 2);
            buf.insert(buf.end(), (const char*)ids, (const char*)ids + count * 4);
            return off;
        };

        // Single pass: walk cells, remap or apply correction, write all outputs
        std::vector<char> out_se, out_ae, out_ie, out_geo;
        out_geo.reserve(cells.size() * 20);
        // Pre-reserve output entry buffers (approximate sizes from old files)
        out_se.reserve(old_se.size() + old_se.size() / 20);
        out_ae.reserve(old_ae.size() + old_ae.size() / 20);
        out_ie.reserve(old_ie.size() + old_ie.size() / 20);
        uint32_t no_data = 0xFFFFFFFF;
        size_t se_corr = 0, ae_corr = 0, ie_corr = 0;

        for (auto& [cid, oi] : cells) {
            // For each entry type: get remapped IDs or correction
            auto process_entry = [&](const std::vector<char>& old_entries, size_t geo_off_pos,
                                      const std::vector<uint32_t>& rm,
                                      const std::unordered_map<uint64_t, const std::vector<uint32_t>*>& corr_map,
                                      std::vector<char>& out_buf, size_t& corr_count) -> uint32_t {
                // Check for correction first
                auto cit = corr_map.find(cid);
                if (cit != corr_map.end()) {
                    corr_count++;
                    return write_ids(out_buf, cit->second->data(), cit->second->size());
                }
                // No correction: remap old IDs using shared buffer
                if (oi < 0) return no_data;
                uint32_t off; memcpy(&off, old_geo.data() + oi * 20 + geo_off_pos, 4);
                size_t n = parse_ids_into(old_entries, off);
                if (n == 0) return no_data;
                remap_ids(id_buf, rm);
                return write_ids(out_buf, id_buf.data(), id_buf.size());
            };

            // Determine has-flags (from old cell flags + flag corrections)
            bool has_s = false, has_a = false, has_i = 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);
                has_s = (s != no_data); has_a = (a != no_data); has_i = (ip != no_data);
            }
            auto fc_it = cell_flag_corrections.find(cid);
            if (fc_it != cell_flag_corrections.end()) {
                has_s = (fc_it->second & 1) != 0;
                has_a = (fc_it->second & 2) != 0;
                has_i = (fc_it->second & 4) != 0;
            }
            // Corrections can also introduce entries for cells that didn't have them
            if (corr_se.count(cid)) has_s = true;
            if (corr_ae.count(cid)) has_a = true;
            if (corr_ie.count(cid)) has_i = true;

            uint32_t s_off = no_data, a_off = no_data, i_off = no_data;
            if (has_s) s_off = process_entry(old_se, 8, w_rm, corr_se, out_se, se_corr);
            if (has_a) a_off = process_entry(old_ae, 12, a_rm, corr_ae, out_ae, ae_corr);
            if (has_i) i_off = process_entry(old_ie, 16, i_rm, corr_ie, out_ie, ie_corr);

            // Write geo_cells row
            out_geo.insert(out_geo.end(), (const char*)&cid, (const char*)&cid + 8);
            out_geo.insert(out_geo.end(), (const char*)&s_off, (const char*)&s_off + 4);
            out_geo.insert(out_geo.end(), (const char*)&a_off, (const char*)&a_off + 4);
            out_geo.insert(out_geo.end(), (const char*)&i_off, (const char*)&i_off + 4);
        }

        write_file(out_dir + "/geo_cells.bin", out_geo);
        write_file(out_dir + "/street_entries.bin", out_se);
        write_file(out_dir + "/addr_entries.bin", out_ae);
        write_file(out_dir + "/interp_entries.bin", out_ie);
        std::cerr << "  Geo entries: " << cells.size() << " cells, corrections applied: "
                  << se_corr << " street, " << ae_corr << " addr, " << ie_corr << " interp" << std::endl;
        log_phase("  Geo entry pipeline", t_entry);

        // Admin: small, keep existing approach
        {
            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_adme = read_file(cur_dir + "/admin_entries.bin");
            auto admin = rebuild_admin_from_remap(old_ac, old_adme, ad_rm, admin_added, admin_removed);

            // Apply admin corrections
            auto ait = entry_cell_corrections.find((uint32_t)PatchFileId::ADMIN_ENTRIES);
            if (ait != entry_cell_corrections.end()) {
                // Parse derived admin entries by cell
                std::unordered_map<uint64_t, size_t> ac_idx;
                for (size_t i = 0; i < admin.admin_cells_data.size() / 12; i++) {
                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data() + i * 12, 8);
                    ac_idx[cid] = i;
                }
                // Build corrected entries
                std::vector<char> new_ae;
                std::vector<char> new_ac;
                size_t n = admin.admin_cells_data.size() / 12;
                uint32_t no_data_v = 0xFFFFFFFF;
                std::unordered_map<uint64_t, const std::vector<uint32_t>*> ac_corr;
                for (auto& c : ait->second) ac_corr[c.cell_id] = &c.ids;
                for (size_t i = 0; i < n; i++) {
                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data() + i * 12, 8);
                    new_ac.insert(new_ac.end(), (const char*)&cid, (const char*)&cid + 8);
                    auto cit = ac_corr.find(cid);
                    if (cit != ac_corr.end()) {
                        // Use correction
                        uint32_t off = cit->second->empty() ? no_data_v : (uint32_t)new_ae.size();
                        new_ac.insert(new_ac.end(), (const char*)&off, (const char*)&off + 4);
                        if (!cit->second->empty()) {
                            uint16_t count = cit->second->size();
                            new_ae.insert(new_ae.end(), (const char*)&count, (const char*)&count + 2);
                            new_ae.insert(new_ae.end(), (const char*)cit->second->data(),
                                         (const char*)cit->second->data() + count * 4);
                        }
                    } else {
                        // Use derived
                        uint32_t old_off; memcpy(&old_off, admin.admin_cells_data.data() + i * 12 + 8, 4);
                        if (old_off != no_data_v && old_off + 2 <= admin.admin_entries_data.size()) {
                            uint32_t new_off = (uint32_t)new_ae.size();
                            new_ac.insert(new_ac.end(), (const char*)&new_off, (const char*)&new_off + 4);
                            uint16_t count; memcpy(&count, admin.admin_entries_data.data() + old_off, 2);
                            new_ae.insert(new_ae.end(), admin.admin_entries_data.data() + old_off,
                                         admin.admin_entries_data.data() + old_off + 2 + count * 4);
                        } else {
                            new_ac.insert(new_ac.end(), (const char*)&no_data_v, (const char*)&no_data_v + 4);
                        }
                    }
                }
                write_file(out_dir + "/admin_cells.bin", new_ac);
                write_file(out_dir + "/admin_entries.bin", new_ae);
                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);
                std::cerr << "  Admin: " << admin.admin_cells_data.size() / 12 << " cells, no corrections" << std::endl;
            }
            log_phase("  Admin pipeline", t_entry);
        }
    }

    // Cleanup
    munmap(const_cast<char*>(patch_mmap), patch_size);
    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
    system(rm_cmd.c_str());
    log_phase("Total patch application", t_start);
    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
    return 0;
}