     1→// geocoder-patch v3: Apply custom patch format.
     2→//
     3→// Decompresses zstd transport layer, then:
     4→// 1. Reads string remap, applies to old data files
     5→// 2. Replays merge sequences to reconstruct new data files
     6→// 3. Writes full-replacement entry/cell files
     7→//
     8→// Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>
     9→
    10→#include <cstdint>
    11→#include <cstdlib>
    12→#include <cstring>
    13→#include <ctime>
    14→#include <fcntl.h>
    15→#include <malloc.h>
    16→#include <fstream>
    17→#include <iomanip>
    18→#include <iostream>
    19→#include <mutex>
    20→#include <string>
    21→#include <sys/mman.h>
    22→#include <sys/stat.h>
    23→#include <thread>
    24→#include <unordered_map>
    25→#include <unistd.h>
    26→#include <vector>
    27→
    28→static double now_ms() {
    29→    struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
    30→    return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
    31→}
    32→static size_t get_rss_mb() {
    33→    std::ifstream f("/proc/self/statm");
    34→    size_t pages = 0; f >> pages; // first field is total pages, second is RSS
    35→    f >> pages; // RSS in pages
    36→    return pages * 4096 / (1024*1024);
    37→}
    38→static void log_phase(const char* label, double start) {
    39→    std::cerr << "  [" << std::fixed << std::setprecision(1) << (now_ms() - start) / 1000.0
    40→              << "s, " << get_rss_mb() << " MiB] " << label << std::endl;
    41→}
    42→
    43→#include "patch_format.h"
    44→
    45→// Merge ops (must match diff tool)
    46→enum MergeOp : uint8_t { OP_MATCH_RUN = 0, OP_INSERT_RUN = 1, OP_DELETE_RUN = 2 };
    47→
    48→int main(int argc, char* argv[]) {
    49→    if (argc < 5 || std::string(argv[3]) != "-o") {
    50→        std::cerr << "Usage: geocoder-patch <current-dir> <patch-file> -o <output-dir>" << std::endl;
    51→        return 1;
    52→    }
    53→    std::string cur_dir = argv[1], patch_path = argv[2], out_dir = argv[4];
    54→    ensure_dir(out_dir);
    55→    std::string tmpdir = "/tmp/geocoder-patch-" + std::to_string(getpid());
    56→    ensure_dir(tmpdir);
    57→
    58→    // Decompress zstd transport layer to temp file, then mmap it
    59→    std::string raw_path = tmpdir + "/patch.raw";
    60→    {
    61→        std::string cmd = "zstd -d '" + patch_path + "' -o '" + raw_path + "' -f --quiet 2>/dev/null";
    62→        if (system(cmd.c_str()) != 0) {
    63→            std::cerr << "Failed to decompress patch" << std::endl; return 1;
    64→        }
    65→    }
    66→    int patch_fd = open(raw_path.c_str(), O_RDONLY);
    67→    if (patch_fd < 0) { std::cerr << "Failed to open decompressed patch" << std::endl; return 1; }
    68→    struct stat patch_stat;
    69→    fstat(patch_fd, &patch_stat);
    70→    size_t patch_size = patch_stat.st_size;
    71→    const char* patch_mmap = static_cast<const char*>(
    72→        mmap(nullptr, patch_size, PROT_READ, MAP_PRIVATE, patch_fd, 0));
    73→    if (patch_mmap == MAP_FAILED) { std::cerr << "Failed to mmap patch" << std::endl; return 1; }
    74→    // Advise sequential access for better readahead
    75→    madvise(const_cast<char*>(patch_mmap), patch_size, MADV_SEQUENTIAL);
    76→    close(patch_fd);
    77→    remove(raw_path.c_str());
    78→    double t_start = now_ms();
    79→    std::cerr << "Decompressed patch: " << patch_size << " bytes (mmap'd)" << std::endl;
    80→    log_phase("Decompress + mmap", t_start);
    81→
    82→    // Provide a patch data pointer for consistent access
    83→    const char* patch_data = patch_mmap;
    84→
    85→    size_t pos = 0;
    86→    auto read_u32 = [&]() -> uint32_t { uint32_t v; memcpy(&v, patch_data + pos, 4); pos += 4; return v; };
    87→    auto read_u64 = [&]() -> uint64_t { uint64_t v; memcpy(&v, patch_data + pos, 8); pos += 8; return v; };
    88→
    89→    // Check header
    90→    if (memcmp(patch_data, GCPATCH_MAGIC, 8) != 0) {
    91→        std::cerr << "Invalid patch magic" << std::endl; return 1;
    92→    }
    93→    pos = 8;
    94→    uint32_t version = read_u32();
    95→    if (version != 2) { std::cerr << "Expected patch v2, got " << version << std::endl; return 1; }
    96→    uint32_t flags = read_u32();
    97→    (void)flags;
    98→
    99→    // Read string-level diff OR string remap
   100→    std::unordered_map<uint32_t, uint32_t> str_remap;
   101→    {
   102→        uint32_t marker = read_u32();
   103→
   104→        if (marker == 0xFFFFFFF7) {
   105→            // String-level diff: merge new/deleted strings into old pool
   106→            uint32_t n_added = read_u32(), n_deleted = read_u32();
   107→            std::vector<std::string> added;
   108→            for (uint32_t i = 0; i < n_added; i++) {
   109→                const char* s = patch_data + pos;
   110→                added.push_back(s);
   111→                pos += strlen(s) + 1;
   112→            }
   113→            std::vector<uint32_t> del_idx(n_deleted);
   114→            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();
   115→            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());
   116→
   117→            // Parse old pool
   118→            auto old_pool = read_file(cur_dir + "/strings.bin");
   119→            std::vector<std::pair<uint32_t, std::string>> old_strs;
   120→            size_t sp = 0;
   121→            while (sp < old_pool.size()) {
   122→                const char* s = old_pool.data() + sp;
   123→                old_strs.push_back({(uint32_t)sp, std::string(s)});
   124→                sp += strlen(s) + 1;
   125→            }
   126→
   127→            // Merge: keep non-deleted + add new, sort
   128→            std::vector<std::string> merged;
   129→            for (size_t i = 0; i < old_strs.size(); i++)
   130→                if (!del_set.count(i)) merged.push_back(old_strs[i].second);
   131→            for (auto& s : added) merged.push_back(s);
   132→            std::sort(merged.begin(), merged.end());
   133→
   134→            // Build new pool
   135→            std::vector<char> new_pool;
   136→            std::unordered_map<std::string, uint32_t> new_offs;
   137→            for (auto& s : merged) {
   138→                new_offs[s] = (uint32_t)new_pool.size();
   139→                new_pool.insert(new_pool.end(), s.begin(), s.end());
   140→                new_pool.push_back('\0');
   141→            }
   142→            write_file(out_dir + "/strings.bin", new_pool);
   143→
   144→            // Build remap
   145→            for (auto& [old_off, s] : old_strs) {
   146→                auto it = new_offs.find(s);
   147→                if (it != new_offs.end() && it->second != old_off)
   148→                    str_remap[old_off] = it->second;
   149→            }
   150→            std::cerr << "Rebuilt strings: +" << n_added << " -" << n_deleted
   151→                      << " → " << merged.size() << " strings, "
   152→                      << str_remap.size() << " remapped" << std::endl;
   153→
   154→            marker = read_u32(); // read next section marker
   155→        }
   156→
   157→        if (marker == 0xFFFFFFFE) {
   158→            // Explicit remap entries (supplements the derived remap)
   159→            uint32_t count = read_u32();
   160→            for (uint32_t i = 0; i < count; i++) {
   161→                uint32_t old_off = read_u32(), new_off = read_u32();
   162→                str_remap[old_off] = new_off; // override with explicit
   163→            }
   164→            std::cerr << "Total string remap: " << str_remap.size() << " entries" << std::endl;
   165→        } else {
   166→            // Not a remap section — seek back
   167→            pos -= 4;
   168→        }
   169→    }
   170→
   171→    // Detect strides
   172→    auto detect = [&](const std::string& name, std::vector<size_t> cs) -> size_t {
   173→        auto d = read_file(cur_dir + "/" + name);
   174→        for (size_t s : cs) if (d.size() % s == 0 && d.size() > 0) return s;
   175→        return cs[0];
   176→    };
   177→    size_t way_stride = detect("street_ways.bin", {12, 9});
   178→    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
   179→    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});
   180→
   181→    // String remap functions
   182→    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
   183→        if (str_remap.empty()) return;
   184→        std::vector<size_t> field_offsets;
   185→        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
   186→        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
   187→        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
   188→        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
   189→        else return;
   190→
   191→        for (size_t i = 0; i + stride <= data.size(); i += stride)
   192→            for (size_t off : field_offsets) {
   193→                uint32_t v; memcpy(&v, data.data() + i + off, 4);
   194→                auto it = str_remap.find(v);
   195→                if (it != str_remap.end()) memcpy(data.data() + i + off, &it->second, 4);
   196→            }
   197→    };
   198→
   199→    // ID remaps derived from merge sequences (populated during merge replay)
   200→    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;
   201→
   202→    // Read cell changes, flag corrections, and entry corrections
   203→    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;
   204→    std::unordered_map<uint64_t, uint8_t> cell_flag_corrections;
   205→    struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
   206→    std::unordered_map<uint32_t, std::vector<CellCorrection>> entry_cell_corrections;
   207→
   208→    // Process sections
   209→    while (pos < patch_size) {
   210→        uint32_t file_id = read_u32();
   211→        if (file_id == 0xFFFFFFFF) break;
   212→
   213→        // Cell change sections
   214→        if (file_id == 0xFFFFFFF7) {
   215→            // String-level diff (appears in main section loop)
   216→            uint32_t n_added = read_u32(), n_deleted = read_u32();
   217→            std::vector<std::string> added;
   218→            for (uint32_t i = 0; i < n_added; i++) {
   219→                const char* s = patch_data + pos;
   220→                added.push_back(s);
   221→                pos += strlen(s) + 1;
   222→            }
   223→            std::vector<uint32_t> del_idx(n_deleted);
   224→            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();
   225→            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());
   226→
   227→            auto old_pool = read_file(cur_dir + "/strings.bin");
   228→            std::vector<std::pair<uint32_t, std::string>> old_strs;
   229→            size_t sp = 0;
   230→            while (sp < old_pool.size()) {
   231→                const char* s = old_pool.data() + sp;
   232→                old_strs.push_back({(uint32_t)sp, std::string(s)});
   233→                sp += strlen(s) + 1;
   234→            }
   235→            std::vector<std::string> merged;
   236→            for (size_t i = 0; i < old_strs.size(); i++)
   237→                if (!del_set.count(i)) merged.push_back(old_strs[i].second);
   238→            for (auto& s : added) merged.push_back(s);
   239→            std::sort(merged.begin(), merged.end());
   240→
   241→            std::vector<char> new_pool;
   242→            std::unordered_map<std::string, uint32_t> new_offs;
   243→            for (auto& s : merged) {
   244→                new_offs[s] = (uint32_t)new_pool.size();
   245→                new_pool.insert(new_pool.end(), s.begin(), s.end());
   246→                new_pool.push_back('\0');
   247→            }
   248→            write_file(out_dir + "/strings.bin", new_pool);
   249→            // Update remap with newly computed offsets
   250→            for (auto& [old_off, s] : old_strs) {

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


Yes, this is the patch application tool (`geocoder-patch`) I'm optimizing. Let me read the rest to understand the full code I need to rewrite:


   250→            for (auto& [old_off, s] : old_strs) {
   251→                auto it = new_offs.find(s);
   252→                if (it != new_offs.end() && it->second != old_off)
   253→                    str_remap[old_off] = it->second;
   254→            }
   255→            std::cerr << "  Rebuilt strings: +" << n_added << " -" << n_deleted
   256→                      << " → " << merged.size() << " strings, "
   257→                      << str_remap.size() << " remapped" << std::endl;
   258→            continue;
   259→        }
   260→        if (file_id == CELL_CHANGES_GEO_MARKER) {
   261→            uint32_t na = read_u32(), nr = read_u32();
   262→            geo_added.resize(na); geo_removed.resize(nr);
   263→            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch_data+pos, 8); pos+=8; geo_added[i]=c; }
   264→            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch_data+pos, 8); pos+=8; geo_removed[i]=c; }
   265→            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
   266→            continue;
   267→        }
   268→        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
   269→            uint32_t na = read_u32(), nr = read_u32();
   270→            admin_added.resize(na); admin_removed.resize(nr);
   271→            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch_data+pos, 8); pos+=8; admin_added[i]=c; }
   272→            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch_data+pos, 8); pos+=8; admin_removed[i]=c; }
   273→            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
   274→            continue;
   275→        }
   276→        if (file_id == CELL_FLAGS_MARKER) {
   277→            uint32_t count = read_u32();
   278→            for (uint32_t i = 0; i < count; i++) {
   279→                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;
   280→                uint8_t flags = static_cast<uint8_t>(patch_data[pos]); pos++;
   281→                cell_flag_corrections[cid] = flags;
   282→            }
   283→            std::cerr << "  Cell flag corrections: " << count << " cells" << std::endl;
   284→            continue;
   285→        }
   286→        if (file_id == SECONDARY_REMAP_MARKER) {
   287→            // Secondary ID remaps: additional old→new mappings for modified records.
   288→            // These are applied alongside the merge-derived remaps for entry reconstruction.
   289→            uint32_t n_files = read_u32();
   290→            for (uint32_t f = 0; f < n_files; f++) {
   291→                uint32_t sec_fid = read_u32();
   292→                uint32_t n_pairs = read_u32();
   293→                auto& id_map = derived_id_remaps[sec_fid];
   294→                for (uint32_t p = 0; p < n_pairs; p++) {
   295→                    uint32_t old_id = read_u32(), new_id = read_u32();
   296→                    if (old_id < id_map.size())
   297→                        id_map[old_id] = new_id;
   298→                }
   299→                std::cerr << "  Secondary remap for file " << sec_fid << ": "
   300→                          << n_pairs << " pairs" << std::endl;
   301→            }
   302→            continue;
   303→        }
   304→        if (file_id == ENTRY_CORRECTION_MARKER) {
   305→            uint32_t corr_fid = read_u32();
   306→            uint32_t count = read_u32();
   307→            // Buffer cell-level corrections for application after entry reconstruction
   308→            struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
   309→            auto& corr_list = entry_cell_corrections[corr_fid];
   310→            for (uint32_t i = 0; i < count; i++) {
   311→                uint64_t cid; memcpy(&cid, patch_data + pos, 8); pos += 8;
   312→                uint16_t ec; memcpy(&ec, patch_data + pos, 2); pos += 2;
   313→                std::vector<uint32_t> ids(ec);
   314→                if (ec > 0) { memcpy(ids.data(), patch_data + pos, ec * 4); pos += ec * 4; }
   315→                corr_list.push_back({cid, std::move(ids)});
   316→            }
   317→            std::cerr << "  Entry correction for file " << corr_fid << ": " << count << " cells" << std::endl;
   318→            continue;
   319→        }
   320→
   321→        uint32_t stride = read_u32();
   322→        uint64_t old_size = read_u64(), new_size = read_u64();
   323→
   324→        if (file_id >= (uint32_t)PatchFileId::COUNT) {
   325→            std::cerr << "  Unknown file " << file_id << std::endl;
   326→            break;
   327→        }
   328→        const char* filename = patch_file_names[file_id];
   329→
   330→        if (stride == 0) {
   331→            // Full replacement
   332→            uint32_t n_fix = read_u32(); (void)n_fix;
   333→            uint64_t data_size = read_u64();
   334→            std::vector<char> data(patch_data + pos, patch_data + pos + data_size);
   335→            write_file(out_dir + "/" + std::string(filename), data);
   336→            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
   337→            pos += data_size;
   338→            continue;
   339→        }
   340→        if (stride == 0xFE) {
   341→            // Legacy zstd correction — skip (replaced by ENTRY_CORRECTION_MARKER)
   342→            uint32_t n_fix = read_u32(); (void)n_fix;
   343→            uint64_t delta_size = read_u64();
   344→            pos += delta_size;
   345→            std::cerr << "  " << filename << ": legacy correction skipped" << std::endl;
   346→            continue;
   347→        }
   348→
   349→        // Merge sequence: load old file, apply remap + fixups, replay sequence
   350→        auto old_data = read_file(cur_dir + "/" + std::string(filename));
   351→        size_t actual_stride = stride;
   352→        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
   353→        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
   354→        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;
   355→
   356→        // Zero struct padding bytes for deterministic comparison
   357→        if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS && actual_stride == 24) {
   358→            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride)
   359→                memset(old_data.data() + i + 13, 0, 3);
   360→        }
   361→        if (file_id == (uint32_t)PatchFileId::INTERP_WAYS && actual_stride == 24) {
   362→            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) {
   363→                memset(old_data.data() + i + 5, 0, 3);
   364→                memset(old_data.data() + i + 21, 0, 3);
   365→            }
   366→        }
   367→
   368→        // Apply string remap to old data
   369→        apply_str_remap(old_data, file_id, actual_stride);
   370→
   371→        // Read and apply fixups (node_offset/vertex_offset fixes)
   372→        // Read and apply delta-encoded fixups
   373→        uint32_t n_fixups = read_u32();
   374→        if (n_fixups > 0) {
   375→            uint32_t delta_size = read_u32();
   376→            uint32_t prev_idx = 0, prev_val = 0;
   377→            size_t fpos = pos;
   378→            for (uint32_t i = 0; i < n_fixups; i++) {
   379→                uint32_t idx = prev_idx + read_varint(patch_data, fpos);
   380→                uint32_t val = prev_val + read_varint(patch_data, fpos);
   381→                size_t byte_pos = (size_t)idx * actual_stride;
   382→                if (byte_pos + 4 <= old_data.size())
   383→                    memcpy(old_data.data() + byte_pos, &val, 4);
   384→                prev_idx = idx;
   385→                prev_val = val;
   386→            }
   387→            pos = fpos;
   388→            std::cerr << "    Applied " << n_fixups << " fixups (" << delta_size << " bytes delta)" << std::endl;
   389→        }
   390→
   391→        // Read merge sequence size and replay, tracking old→new ID mapping
   392→        uint64_t seq_size = read_u64();
   393→        std::vector<char> output;
   394→        output.reserve(new_size);
   395→        size_t old_rec = 0, new_rec = 0; // record indices
   396→        size_t old_pos_bytes = 0;
   397→        size_t seq_end = pos + seq_size;
   398→
   399→        // ID remap for this file (old_record_index → new_record_index)
   400→        std::vector<uint32_t> id_map;
   401→        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
   402→                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
   403→                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
   404→                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
   405→        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);
   406→
   407→        while (pos < seq_end) {
   408→            uint8_t op = static_cast<uint8_t>(patch_data[pos]); pos++;
   409→            uint32_t count; memcpy(&count, patch_data + pos, 4); pos += 4;
   410→
   411→            if (op == OP_MATCH_RUN) {
   412→                size_t bytes = count * actual_stride;
   413→                if (old_pos_bytes + bytes <= old_data.size())
   414→                    output.insert(output.end(), old_data.data() + old_pos_bytes,
   415→                                 old_data.data() + old_pos_bytes + bytes);
   416→                if (track_ids)
   417→                    for (uint32_t k = 0; k < count; k++)
   418→                        if (old_rec + k < id_map.size())
   419→                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
   420→                old_rec += count; new_rec += count;
   421→                old_pos_bytes += bytes;
   422→            } else if (op == OP_INSERT_RUN) {
   423→                size_t bytes = count * actual_stride;
   424→                output.insert(output.end(), patch_data + pos, patch_data + pos + bytes);
   425→                pos += bytes;
   426→                new_rec += count;
   427→            } else if (op == OP_DELETE_RUN) {
   428→                old_rec += count;
   429→                old_pos_bytes += count * actual_stride;
   430→            }
   431→        }
   432→
   433→        write_file(out_dir + "/" + std::string(filename), output);
   434→
   435→        // Store ID remap for entry reconstruction
   436→        if (track_ids) {
   437→            derived_id_remaps[file_id] = std::move(id_map);
   438→            size_t matched = 0;
   439→            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
   440→            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
   441→                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
   442→        } else {
   443→            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
   444→        }
   445→    }
   446→
   447→    // --- Streaming entry pipeline: remap + correct + write in one pass ---
   448→    // Replaces separate rebuild + 4 correction passes with a single walk over cells.
   449→    // Memory: ~3 GiB instead of ~63 GiB. Time: ~2 min instead of ~20 min.

            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.


   445→    }
   446→
   447→    // --- Streaming entry pipeline: remap + correct + write in one pass ---
   448→    // Replaces separate rebuild + 4 correction passes with a single walk over cells.
   449→    // Memory: ~3 GiB instead of ~63 GiB. Time: ~2 min instead of ~20 min.
   450→    {
   451→        log_phase("Merge replays complete", t_start);
   452→        // Free fragmented heap from merge replay phase (glibc doesn't return pages to OS)
   453→        derived_id_remaps.rehash(derived_id_remaps.size()); // compact
   454→        malloc_trim(0);
   455→        log_phase("Heap compacted", t_start);
   456→        double t_entry = now_ms();
   457→        std::cerr << "Streaming entry pipeline..." << std::endl;
   458→
   459→        // Get ID remap vectors (already populated during merge replay + secondary remaps)
   460→        auto get_remap = [&](PatchFileId fid) -> const std::vector<uint32_t>& {
   461→            static std::vector<uint32_t> empty;
   462→            auto it = derived_id_remaps.find((uint32_t)fid);
   463→            return it != derived_id_remaps.end() ? it->second : empty;
   464→        };
   465→        auto& w_rm = get_remap(PatchFileId::STREET_WAYS);
   466→        auto& a_rm = get_remap(PatchFileId::ADDR_POINTS);
   467→        auto& i_rm = get_remap(PatchFileId::INTERP_WAYS);
   468→
   469→        // Build correction lookup maps (cell_id → entry IDs) for each geo entry type
   470→        std::unordered_map<uint64_t, const std::vector<uint32_t>*> corr_se, corr_ae, corr_ie;
   471→        for (auto& [fid, corrections] : entry_cell_corrections) {
   472→            std::unordered_map<uint64_t, const std::vector<uint32_t>*>* target = nullptr;
   473→            if (fid == (uint32_t)PatchFileId::STREET_ENTRIES) target = &corr_se;
   474→            else if (fid == (uint32_t)PatchFileId::ADDR_ENTRIES) target = &corr_ae;
   475→            else if (fid == (uint32_t)PatchFileId::INTERP_ENTRIES) target = &corr_ie;
   476→            if (target) for (auto& c : corrections) (*target)[c.cell_id] = &c.ids;
   477→        }
   478→        log_phase("  Correction maps built", t_entry);
   479→
   480→        // Load old files
   481→        auto old_geo = read_file(cur_dir + "/geo_cells.bin");
   482→        auto old_se = read_file(cur_dir + "/street_entries.bin");
   483→        auto old_ae = read_file(cur_dir + "/addr_entries.bin");
   484→        auto old_ie = read_file(cur_dir + "/interp_entries.bin");
   485→
   486→        // Build derived cell list: old cells - removed + added (sorted)
   487→        std::unordered_set<uint64_t> removed_set(geo_removed.begin(), geo_removed.end());
   488→        struct CellRef { uint64_t cid; int32_t old_idx; }; // old_idx = -1 for added cells
   489→        std::vector<CellRef> cells;
   490→        cells.reserve(old_geo.size() / 20 + geo_added.size());
   491→        for (size_t i = 0; i < old_geo.size() / 20; i++) {
   492→            uint64_t cid; memcpy(&cid, old_geo.data() + i * 20, 8);
   493→            if (!removed_set.count(cid)) cells.push_back({cid, (int32_t)i});
   494→        }
   495→        for (uint64_t cid : geo_added) cells.push_back({cid, -1});
   496→        std::sort(cells.begin(), cells.end(), [](const CellRef& a, const CellRef& b) { return a.cid < b.cid; });
   497→        log_phase("  Cell list built", t_entry);
   498→
   499→        // Reusable buffer for entry IDs (avoids 45M allocs on planet)
   500→        std::vector<uint32_t> id_buf;
   501→        id_buf.reserve(256); // most cells have <256 entries
   502→
   503→        // Helper: parse entry IDs into reusable buffer, return count
   504→        auto parse_ids_into = [&id_buf](const std::vector<char>& data, uint32_t off) -> size_t {
   505→            id_buf.clear();
   506→            if (off == 0xFFFFFFFF || off + 2 > data.size()) return 0;
   507→            uint16_t count; memcpy(&count, data.data() + off, 2);
   508→            if (off + 2 + count * 4 > data.size()) return 0;
   509→            id_buf.resize(count);
   510→            memcpy(id_buf.data(), data.data() + off + 2, count * 4);
   511→            return count;
   512→        };
   513→
   514→        // Helper: remap IDs in-place using vector (O(1) per ID)
   515→        auto remap_ids = [](std::vector<uint32_t>& ids, const std::vector<uint32_t>& rm) {
   516→            for (auto& id : ids) if (id < rm.size() && rm[id] != 0xFFFFFFFF) id = rm[id];
   517→            std::sort(ids.begin(), ids.end());
   518→        };
   519→
   520→        // Helper: write entry list from buffer/pointer and return offset
   521→        auto write_ids = [](std::vector<char>& buf, const uint32_t* ids, size_t count) -> uint32_t {
   522→            if (count == 0) return 0xFFFFFFFF;
   523→            uint32_t off = static_cast<uint32_t>(buf.size());
   524→            uint16_t c = static_cast<uint16_t>(count);
   525→            buf.insert(buf.end(), (const char*)&c, (const char*)&c + 2);
   526→            buf.insert(buf.end(), (const char*)ids, (const char*)ids + count * 4);
   527→            return off;
   528→        };
   529→
   530→        // Single pass: walk cells, remap or apply correction, write all outputs
   531→        std::vector<char> out_se, out_ae, out_ie, out_geo;
   532→        out_geo.reserve(cells.size() * 20);
   533→        // Pre-reserve output entry buffers (approximate sizes from old files)
   534→        out_se.reserve(old_se.size() + old_se.size() / 20);
   535→        out_ae.reserve(old_ae.size() + old_ae.size() / 20);
   536→        out_ie.reserve(old_ie.size() + old_ie.size() / 20);
   537→        uint32_t no_data = 0xFFFFFFFF;
   538→        size_t se_corr = 0, ae_corr = 0, ie_corr = 0;
   539→
   540→        for (auto& [cid, oi] : cells) {
   541→            // For each entry type: get remapped IDs or correction
   542→            auto process_entry = [&](const std::vector<char>& old_entries, size_t geo_off_pos,
   543→                                      const std::vector<uint32_t>& rm,
   544→                                      const std::unordered_map<uint64_t, const std::vector<uint32_t>*>& corr_map,
   545→                                      std::vector<char>& out_buf, size_t& corr_count) -> uint32_t {
   546→                // Check for correction first
   547→                auto cit = corr_map.find(cid);
   548→                if (cit != corr_map.end()) {
   549→                    corr_count++;
   550→                    return write_ids(out_buf, cit->second->data(), cit->second->size());
   551→                }
   552→                // No correction: remap old IDs using shared buffer
   553→                if (oi < 0) return no_data;
   554→                uint32_t off; memcpy(&off, old_geo.data() + oi * 20 + geo_off_pos, 4);
   555→                size_t n = parse_ids_into(old_entries, off);
   556→                if (n == 0) return no_data;
   557→                remap_ids(id_buf, rm);
   558→                return write_ids(out_buf, id_buf.data(), id_buf.size());
   559→            };
   560→
   561→            // Determine has-flags (from old cell flags + flag corrections)
   562→            bool has_s = false, has_a = false, has_i = false;
   563→            if (oi >= 0) {
   564→                uint32_t s, a, ip;
   565→                memcpy(&s, old_geo.data() + oi * 20 + 8, 4);
   566→                memcpy(&a, old_geo.data() + oi * 20 + 12, 4);
   567→                memcpy(&ip, old_geo.data() + oi * 20 + 16, 4);
   568→                has_s = (s != no_data); has_a = (a != no_data); has_i = (ip != no_data);
   569→            }
   570→            auto fc_it = cell_flag_corrections.find(cid);
   571→            if (fc_it != cell_flag_corrections.end()) {
   572→                has_s = (fc_it->second & 1) != 0;
   573→                has_a = (fc_it->second & 2) != 0;
   574→                has_i = (fc_it->second & 4) != 0;
   575→            }
   576→            // Corrections can also introduce entries for cells that didn't have them
   577→            if (corr_se.count(cid)) has_s = true;
   578→            if (corr_ae.count(cid)) has_a = true;
   579→            if (corr_ie.count(cid)) has_i = true;
   580→
   581→            uint32_t s_off = no_data, a_off = no_data, i_off = no_data;
   582→            if (has_s) s_off = process_entry(old_se, 8, w_rm, corr_se, out_se, se_corr);
   583→            if (has_a) a_off = process_entry(old_ae, 12, a_rm, corr_ae, out_ae, ae_corr);
   584→            if (has_i) i_off = process_entry(old_ie, 16, i_rm, corr_ie, out_ie, ie_corr);
   585→
   586→            // Write geo_cells row
   587→            out_geo.insert(out_geo.end(), (const char*)&cid, (const char*)&cid + 8);
   588→            out_geo.insert(out_geo.end(), (const char*)&s_off, (const char*)&s_off + 4);
   589→            out_geo.insert(out_geo.end(), (const char*)&a_off, (const char*)&a_off + 4);
   590→            out_geo.insert(out_geo.end(), (const char*)&i_off, (const char*)&i_off + 4);
   591→        }
   592→
   593→        write_file(out_dir + "/geo_cells.bin", out_geo);
   594→        write_file(out_dir + "/street_entries.bin", out_se);
   595→        write_file(out_dir + "/addr_entries.bin", out_ae);
   596→        write_file(out_dir + "/interp_entries.bin", out_ie);
   597→        std::cerr << "  Geo entries: " << cells.size() << " cells, corrections applied: "
   598→                  << se_corr << " street, " << ae_corr << " addr, " << ie_corr << " interp" << std::endl;
   599→        log_phase("  Geo entry pipeline", t_entry);
   600→
   601→        // Admin: small, keep existing approach
   602→        {
   603→            std::unordered_map<uint32_t,uint32_t> ad_rm;
   604→            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
   605→                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
   606→                for (uint32_t i = 0; i < vec.size(); i++)
   607→                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
   608→            }
   609→            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
   610→            auto old_adme = read_file(cur_dir + "/admin_entries.bin");
   611→            auto admin = rebuild_admin_from_remap(old_ac, old_adme, ad_rm, admin_added, admin_removed);
   612→
   613→            // Apply admin corrections
   614→            auto ait = entry_cell_corrections.find((uint32_t)PatchFileId::ADMIN_ENTRIES);
   615→            if (ait != entry_cell_corrections.end()) {
   616→                // Parse derived admin entries by cell
   617→                std::unordered_map<uint64_t, size_t> ac_idx;
   618→                for (size_t i = 0; i < admin.admin_cells_data.size() / 12; i++) {
   619→                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data() + i * 12, 8);
   620→                    ac_idx[cid] = i;
   621→                }
   622→                // Build corrected entries
   623→                std::vector<char> new_ae;
   624→                std::vector<char> new_ac;
   625→                size_t n = admin.admin_cells_data.size() / 12;
   626→                uint32_t no_data_v = 0xFFFFFFFF;
   627→                std::unordered_map<uint64_t, const std::vector<uint32_t>*> ac_corr;
   628→                for (auto& c : ait->second) ac_corr[c.cell_id] = &c.ids;
   629→                for (size_t i = 0; i < n; i++) {
   630→                    uint64_t cid; memcpy(&cid, admin.admin_cells_data.data() + i * 12, 8);
   631→                    new_ac.insert(new_ac.end(), (const char*)&cid, (const char*)&cid + 8);
   632→                    auto cit = ac_corr.find(cid);
   633→                    if (cit != ac_corr.end()) {
   634→                        // Use correction
   635→                        uint32_t off = cit->second->empty() ? no_data_v : (uint32_t)new_ae.size();
   636→                        new_ac.insert(new_ac.end(), (const char*)&off, (const char*)&off + 4);
   637→                        if (!cit->second->empty()) {
   638→                            uint16_t count = cit->second->size();
   639→                            new_ae.insert(new_ae.end(), (const char*)&count, (const char*)&count + 2);
   640→                            new_ae.insert(new_ae.end(), (const char*)cit->second->data(),
   641→                                         (const char*)cit->second->data() + count * 4);
   642→                        }
   643→                    } else {
   644→                        // Use derived
   645→                        uint32_t old_off; memcpy(&old_off, admin.admin_cells_data.data() + i * 12 + 8, 4);
   646→                        if (old_off != no_data_v && old_off + 2 <= admin.admin_entries_data.size()) {
   647→                            uint32_t new_off = (uint32_t)new_ae.size();
   648→                            new_ac.insert(new_ac.end(), (const char*)&new_off, (const char*)&new_off + 4);
   649→                            uint16_t count; memcpy(&count, admin.admin_entries_data.data() + old_off, 2);
   650→                            new_ae.insert(new_ae.end(), admin.admin_entries_data.data() + old_off,
   651→                                         admin.admin_entries_data.data() + old_off + 2 + count * 4);
   652→                        } else {
   653→                            new_ac.insert(new_ac.end(), (const char*)&no_data_v, (const char*)&no_data_v + 4);
   654→                        }
   655→                    }
   656→                }
   657→                write_file(out_dir + "/admin_cells.bin", new_ac);
   658→                write_file(out_dir + "/admin_entries.bin", new_ae);
   659→                std::cerr << "  Admin: " << n << " cells, " << ac_corr.size() << " corrections" << std::endl;
   660→            } else {
   661→                write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
   662→                write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
   663→                std::cerr << "  Admin: " << admin.admin_cells_data.size() / 12 << " cells, no corrections" << std::endl;
   664→            }
   665→            log_phase("  Admin pipeline", t_entry);
   666→        }
   667→    }
   668→
   669→    // Cleanup
   670→    munmap(const_cast<char*>(patch_mmap), patch_size);
   671→    std::string rm_cmd = "rm -rf '" + tmpdir + "'";
   672→    system(rm_cmd.c_str());
   673→    log_phase("Total patch application", t_start);
   674→    std::cerr << "Patch applied. Output in " << out_dir << std::endl;
   675→    return 0;
   676→}
   677→

    }

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


Now I have the full picture. The key memory hogs are:

1. **Merge replays** (lines 349-445): Each loads old file with `read_file()` into a vector, builds `output` vector, writes. For street_nodes that's 4 GiB old + 4 GiB output = 8 GiB peak per file. Files are processed sequentially, but glibc doesn't return freed memory.

2. **Entry pipeline** (lines 480-596): Loads 4 old files via `read_file()` (~2.5 GiB), accumulates 4 output vectors (~2 GiB), plus remap vectors (~536 MiB).

The fix: mmap all old files, write output via FILE* streaming, and use `malloc_trim` between phases.

Let me rewrite the entire file:


Write

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