   100→                new_offs[s] = (uint32_t)new_pool.size();
   101→                new_pool.insert(new_pool.end(), s.begin(), s.end());
   102→                new_pool.push_back('\0');
   103→            }
   104→            write_file(out_dir + "/strings.bin", new_pool);
   105→
   106→            // Build remap
   107→            for (auto& [old_off, s] : old_strs) {
   108→                auto it = new_offs.find(s);
   109→                if (it != new_offs.end() && it->second != old_off)
   110→                    str_remap[old_off] = it->second;
   111→            }
   112→            std::cerr << "Rebuilt strings: +" << n_added << " -" << n_deleted
   113→                      << " → " << merged.size() << " strings, "
   114→                      << str_remap.size() << " remapped" << std::endl;
   115→
   116→            marker = read_u32(); // read next section marker
   117→        }
   118→
   119→        if (marker == 0xFFFFFFFE) {
   120→            // Explicit remap entries (supplements the derived remap)
   121→            uint32_t count = read_u32();
   122→            for (uint32_t i = 0; i < count; i++) {
   123→                uint32_t old_off = read_u32(), new_off = read_u32();
   124→                str_remap[old_off] = new_off; // override with explicit
   125→            }
   126→            std::cerr << "Total string remap: " << str_remap.size() << " entries" << std::endl;
   127→        } else {
   128→            // Not a remap section — seek back
   129→            pos -= 4;
   130→        }
   131→    }
   132→
   133→    // Detect strides
   134→    auto detect = [&](const std::string& name, std::vector<size_t> cs) -> size_t {
   135→        auto d = read_file(cur_dir + "/" + name);
   136→        for (size_t s : cs) if (d.size() % s == 0 && d.size() > 0) return s;
   137→        return cs[0];
   138→    };
   139→    size_t way_stride = detect("street_ways.bin", {12, 9});
   140→    size_t interp_stride = detect("interp_ways.bin", {24, 20, 18});
   141→    size_t admin_stride = detect("admin_polygons.bin", {24, 20, 19});
   142→
   143→    // String remap functions
   144→    auto apply_str_remap = [&](std::vector<char>& data, uint32_t file_id, size_t stride) {
   145→        if (str_remap.empty()) return;
   146→        std::vector<size_t> field_offsets;
   147→        if (file_id == (uint32_t)PatchFileId::ADDR_POINTS) field_offsets = {8, 12};
   148→        else if (file_id == (uint32_t)PatchFileId::STREET_WAYS) field_offsets = {(stride == 12) ? (size_t)8 : (size_t)5};
   149→        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) field_offsets = {(stride >= 20) ? (size_t)8 : (size_t)5};
   150→        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) field_offsets = {8};
   151→        else return;
   152→
   153→        for (size_t i = 0; i + stride <= data.size(); i += stride)
   154→            for (size_t off : field_offsets) {
   155→                uint32_t v; memcpy(&v, data.data() + i + off, 4);
   156→                auto it = str_remap.find(v);
   157→                if (it != str_remap.end()) memcpy(data.data() + i + off, &it->second, 4);
   158→            }
   159→    };
   160→
   161→    // ID remaps derived from merge sequences (populated during merge replay)
   162→    std::unordered_map<uint32_t, std::vector<uint32_t>> derived_id_remaps;
   163→
   164→    // Buffered correction sections (applied after entry reconstruction)
   165→    struct PendingCorrection { uint32_t file_id; std::string filename; std::vector<char> delta; };
   166→    std::vector<PendingCorrection> pending_corrections;
   167→
   168→    // Read cell changes, flag corrections, and entry corrections
   169→    std::vector<uint64_t> geo_added, geo_removed, admin_added, admin_removed;
   170→    std::unordered_map<uint64_t, uint8_t> cell_flag_corrections;
   171→    struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
   172→    std::unordered_map<uint32_t, std::vector<CellCorrection>> entry_cell_corrections;
   173→
   174→    // Process sections
   175→    while (pos < patch.size()) {
   176→        uint32_t file_id = read_u32();
   177→        if (file_id == 0xFFFFFFFF) break;
   178→
   179→        // Cell change sections
   180→        if (file_id == 0xFFFFFFF7) {
   181→            // String-level diff (appears in main section loop)
   182→            uint32_t n_added = read_u32(), n_deleted = read_u32();
   183→            std::vector<std::string> added;
   184→            for (uint32_t i = 0; i < n_added; i++) {
   185→                const char* s = patch.data() + pos;
   186→                added.push_back(s);
   187→                pos += strlen(s) + 1;
   188→            }
   189→            std::vector<uint32_t> del_idx(n_deleted);
   190→            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();
   191→            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());
   192→
   193→            auto old_pool = read_file(cur_dir + "/strings.bin");
   194→            std::vector<std::pair<uint32_t, std::string>> old_strs;
   195→            size_t sp = 0;
   196→            while (sp < old_pool.size()) {
   197→                const char* s = old_pool.data() + sp;
   198→                old_strs.push_back({(uint32_t)sp, std::string(s)});
   199→                sp += strlen(s) + 1;
   200→            }
   201→            std::vector<std::string> merged;
   202→            for (size_t i = 0; i < old_strs.size(); i++)
   203→                if (!del_set.count(i)) merged.push_back(old_strs[i].second);
   204→            for (auto& s : added) merged.push_back(s);
   205→            std::sort(merged.begin(), merged.end());
   206→
   207→            std::vector<char> new_pool;
   208→            std::unordered_map<std::string, uint32_t> new_offs;
   209→            for (auto& s : merged) {
   210→                new_offs[s] = (uint32_t)new_pool.size();
   211→                new_pool.insert(new_pool.end(), s.begin(), s.end());
   212→                new_pool.push_back('\0');
   213→            }
   214→            write_file(out_dir + "/strings.bin", new_pool);
   215→            // Update remap with newly computed offsets
   216→            for (auto& [old_off, s] : old_strs) {
   217→                auto it = new_offs.find(s);
   218→                if (it != new_offs.end() && it->second != old_off)
   219→                    str_remap[old_off] = it->second;
   220→            }
   221→            std::cerr << "  Rebuilt strings: +" << n_added << " -" << n_deleted
   222→                      << " → " << merged.size() << " strings, "
   223→                      << str_remap.size() << " remapped" << std::endl;
   224→            continue;
   225→        }
   226→        if (file_id == CELL_CHANGES_GEO_MARKER) {
   227→            uint32_t na = read_u32(), nr = read_u32();
   228→            geo_added.resize(na); geo_removed.resize(nr);
   229→            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
   230→            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
   231→            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
   232→            continue;
   233→        }
   234→        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
   235→            uint32_t na = read_u32(), nr = read_u32();
   236→            admin_added.resize(na); admin_removed.resize(nr);
   237→            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
   238→            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
   239→            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
   240→            continue;
   241→        }
   242→        if (file_id == CELL_FLAGS_MARKER) {
   243→            uint32_t count = read_u32();
   244→            for (uint32_t i = 0; i < count; i++) {
   245→                uint64_t cid; memcpy(&cid, patch.data() + pos, 8); pos += 8;
   246→                uint8_t flags = static_cast<uint8_t>(patch[pos]); pos++;
   247→                cell_flag_corrections[cid] = flags;
   248→            }
   249→            std::cerr << "  Cell flag corrections: " << count << " cells" << std::endl;
   250→            continue;
   251→        }
   252→        if (file_id == ENTRY_CORRECTION_MARKER) {
   253→            uint32_t corr_fid = read_u32();
   254→            uint32_t count = read_u32();
   255→            // Buffer cell-level corrections for application after entry reconstruction
   256→            struct CellCorrection { uint64_t cell_id; std::vector<uint32_t> ids; };
   257→            auto& corr_list = entry_cell_corrections[corr_fid];
   258→            for (uint32_t i = 0; i < count; i++) {
   259→                uint64_t cid; memcpy(&cid, patch.data() + pos, 8); pos += 8;
   260→                uint16_t ec; memcpy(&ec, patch.data() + pos, 2); pos += 2;
   261→                std::vector<uint32_t> ids(ec);
   262→                if (ec > 0) { memcpy(ids.data(), patch.data() + pos, ec * 4); pos += ec * 4; }
   263→                corr_list.push_back({cid, std::move(ids)});
   264→            }
   265→            std::cerr << "  Entry correction for file " << corr_fid << ": " << count << " cells" << std::endl;
   266→            continue;
   267→        }
   268→
   269→        uint32_t stride = read_u32();
   270→        uint64_t old_size = read_u64(), new_size = read_u64();
   271→
   272→        if (file_id >= (uint32_t)PatchFileId::COUNT) {
   273→            std::cerr << "  Unknown file " << file_id << std::endl;
   274→            break;
   275→        }
   276→        const char* filename = patch_file_names[file_id];
   277→
   278→        if (stride == 0) {
   279→            // Full replacement
   280→            uint32_t n_fix = read_u32(); (void)n_fix;
   281→            uint64_t data_size = read_u64();
   282→            std::vector<char> data(patch.data() + pos, patch.data() + pos + data_size);
   283→            write_file(out_dir + "/" + std::string(filename), data);
   284→            std::cerr << "  " << filename << ": full replacement " << data.size() << " bytes" << std::endl;
   285→            pos += data_size;
   286→            continue;
   287→        }
   288→        if (stride == 0xFE) {
   289→            // Legacy zstd correction — skip (replaced by ENTRY_CORRECTION_MARKER)
   290→            uint32_t n_fix = read_u32(); (void)n_fix;
   291→            uint64_t delta_size = read_u64();
   292→            pos += delta_size;
   293→            std::cerr << "  " << filename << ": legacy correction skipped" << std::endl;
   294→            continue;
   295→        }
   296→
   297→        // Merge sequence: load old file, apply remap + fixups, replay sequence
   298→        auto old_data = read_file(cur_dir + "/" + std::string(filename));
   299→        size_t actual_stride = stride;

                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;

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

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

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

        // Cell change sections
        if (file_id == 0xFFFFFFF7) {
            // String-level diff (appears in main section loop)
            uint32_t n_added = read_u32(), n_deleted = read_u32();
            std::vector<std::string> added;
            for (uint32_t i = 0; i < n_added; i++) {
                const char* s = patch.data() + pos;
                added.push_back(s);
                pos += strlen(s) + 1;
            }
            std::vector<uint32_t> del_idx(n_deleted);
            for (uint32_t i = 0; i < n_deleted; i++) del_idx[i] = read_u32();
            std::unordered_set<uint32_t> del_set(del_idx.begin(), del_idx.end());

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

            std::vector<char> new_pool;
            std::unordered_map<std::string, uint32_t> new_offs;
            for (auto& s : merged) {
                new_offs[s] = (uint32_t)new_pool.size();
                new_pool.insert(new_pool.end(), s.begin(), s.end());
                new_pool.push_back('\0');
            }
            write_file(out_dir + "/strings.bin", new_pool);
            // Update remap with newly computed offsets
            for (auto& [old_off, s] : old_strs) {
                auto it = new_offs.find(s);
                if (it != new_offs.end() && it->second != old_off)
                    str_remap[old_off] = it->second;
            }
            std::cerr << "  Rebuilt strings: +" << n_added << " -" << n_deleted
                      << " → " << merged.size() << " strings, "
                      << str_remap.size() << " remapped" << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_GEO_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            geo_added.resize(na); geo_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; geo_removed[i]=c; }
            std::cerr << "  Geo cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_CHANGES_ADMIN_MARKER) {
            uint32_t na = read_u32(), nr = read_u32();
            admin_added.resize(na); admin_removed.resize(nr);
            for (uint32_t i = 0; i < na; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_added[i]=c; }
            for (uint32_t i = 0; i < nr; i++) { uint64_t c; memcpy(&c, patch.data()+pos, 8); pos+=8; admin_removed[i]=c; }
            std::cerr << "  Admin cell changes: +" << na << " -" << nr << std::endl;
            continue;
        }
        if (file_id == CELL_FLAGS_MARKER) {
            uint32_t count = read_u32();
            for (uint32_t i = 0; i < count; i++) {
                uint64_t cid; memcpy(&cid, patch.data() + pos, 8); pos += 8;
                uint8_t flags = static_cast<uint8_t>(patch[pos]); pos++;
                cell_flag_corrections[cid] = flags;
            }
            std::cerr << "  Cell flag corrections: " << count << " cells" << 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;


   299→        size_t actual_stride = stride;
   300→        if (file_id == (uint32_t)PatchFileId::STREET_WAYS) actual_stride = way_stride;
   301→        else if (file_id == (uint32_t)PatchFileId::INTERP_WAYS) actual_stride = interp_stride;
   302→        else if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS) actual_stride = admin_stride;
   303→
   304→        // Zero struct padding bytes for deterministic comparison
   305→        if (file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS && actual_stride == 24) {
   306→            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride)
   307→                memset(old_data.data() + i + 13, 0, 3);
   308→        }
   309→        if (file_id == (uint32_t)PatchFileId::INTERP_WAYS && actual_stride == 24) {
   310→            for (size_t i = 0; i + actual_stride <= old_data.size(); i += actual_stride) {
   311→                memset(old_data.data() + i + 5, 0, 3);
   312→                memset(old_data.data() + i + 21, 0, 3);
   313→            }
   314→        }
   315→
   316→        // Apply string remap to old data
   317→        apply_str_remap(old_data, file_id, actual_stride);
   318→
   319→        // Read and apply fixups (node_offset/vertex_offset fixes)
   320→        // Read and apply delta-encoded fixups
   321→        uint32_t n_fixups = read_u32();
   322→        if (n_fixups > 0) {
   323→            uint32_t delta_size = read_u32();
   324→            uint32_t prev_idx = 0, prev_val = 0;
   325→            size_t fpos = pos;
   326→            for (uint32_t i = 0; i < n_fixups; i++) {
   327→                uint32_t idx = prev_idx + read_varint(patch.data(), fpos);
   328→                uint32_t val = prev_val + read_varint(patch.data(), fpos);
   329→                size_t byte_pos = (size_t)idx * actual_stride;
   330→                if (byte_pos + 4 <= old_data.size())
   331→                    memcpy(old_data.data() + byte_pos, &val, 4);
   332→                prev_idx = idx;
   333→                prev_val = val;
   334→            }
   335→            pos = fpos;
   336→            std::cerr << "    Applied " << n_fixups << " fixups (" << delta_size << " bytes delta)" << std::endl;
   337→        }
   338→
   339→        // Read merge sequence size and replay, tracking old→new ID mapping
   340→        uint64_t seq_size = read_u64();
   341→        std::vector<char> output;
   342→        output.reserve(new_size);
   343→        size_t old_rec = 0, new_rec = 0; // record indices
   344→        size_t old_pos_bytes = 0;
   345→        size_t seq_end = pos + seq_size;
   346→
   347→        // ID remap for this file (old_record_index → new_record_index)
   348→        std::vector<uint32_t> id_map;
   349→        bool track_ids = (file_id == (uint32_t)PatchFileId::ADDR_POINTS ||
   350→                          file_id == (uint32_t)PatchFileId::STREET_WAYS ||
   351→                          file_id == (uint32_t)PatchFileId::INTERP_WAYS ||
   352→                          file_id == (uint32_t)PatchFileId::ADMIN_POLYGONS);
   353→        if (track_ids) id_map.assign(old_data.size() / actual_stride, 0xFFFFFFFF);
   354→
   355→        while (pos < seq_end) {
   356→            uint8_t op = static_cast<uint8_t>(patch[pos]); pos++;
   357→            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;
   358→
   359→            if (op == OP_MATCH_RUN) {
   360→                size_t bytes = count * actual_stride;
   361→                if (old_pos_bytes + bytes <= old_data.size())
   362→                    output.insert(output.end(), old_data.data() + old_pos_bytes,
   363→                                 old_data.data() + old_pos_bytes + bytes);
   364→                if (track_ids)
   365→                    for (uint32_t k = 0; k < count; k++)
   366→                        if (old_rec + k < id_map.size())
   367→                            id_map[old_rec + k] = static_cast<uint32_t>(new_rec + k);
   368→                old_rec += count; new_rec += count;
   369→                old_pos_bytes += bytes;
   370→            } else if (op == OP_INSERT_RUN) {
   371→                size_t bytes = count * actual_stride;
   372→                output.insert(output.end(), patch.data() + pos, patch.data() + pos + bytes);
   373→                pos += bytes;
   374→                new_rec += count;
   375→            } else if (op == OP_DELETE_RUN) {
   376→                old_rec += count;
   377→                old_pos_bytes += count * actual_stride;
   378→            }
   379→        }
   380→
   381→        write_file(out_dir + "/" + std::string(filename), output);
   382→
   383→        // Store ID remap for entry reconstruction
   384→        if (track_ids) {
   385→            derived_id_remaps[file_id] = std::move(id_map);
   386→            size_t matched = 0;
   387→            for (auto v : derived_id_remaps[file_id]) if (v != 0xFFFFFFFF) matched++;
   388→            std::cerr << "  " << filename << ": " << output.size() << " bytes (mapped "
   389→                      << matched << "/" << derived_id_remaps[file_id].size() << " IDs)" << std::endl;
   390→        } else {
   391→            std::cerr << "  " << filename << ": " << output.size() << " bytes" << std::endl;
   392→        }
   393→    }
   394→
   395→    // Reconstruct entry/cell files from derived ID remaps (if not provided as full replacement)
   396→    {
   397→        bool have_geo = false, have_admin = false;
   398→        // Check if geo_cells was already written (full replacement)
   399→        auto check = [&](const std::string& name) -> bool {
   400→            auto d = read_file(out_dir + "/" + name);
   401→            return !d.empty();
   402→        };
   403→        have_geo = check("geo_cells.bin");
   404→        have_admin = check("admin_cells.bin");
   405→        bool have_se = check("street_entries.bin");
   406→        bool have_ae = check("addr_entries.bin");
   407→        bool have_ie = check("interp_entries.bin");
   408→        bool have_adme = check("admin_entries.bin");
   409→
   410→        if (!derived_id_remaps.empty()) { // always reconstruct entries (needed for corrections)
   411→            std::cerr << "Reconstructing entry/cell files from derived ID remaps..." << std::endl;
   412→            auto convert = [](const std::vector<uint32_t>& vec) {
   413→                std::unordered_map<uint32_t,uint32_t> m;
   414→                for (uint32_t i = 0; i < vec.size(); i++)
   415→                    if (vec[i] != 0xFFFFFFFF) m[i] = vec[i];
   416→                return m;
   417→            };
   418→            std::unordered_map<uint32_t,uint32_t> w_rm, a_rm, i_rm;
   419→            if (derived_id_remaps.count((uint32_t)PatchFileId::STREET_WAYS))
   420→                w_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::STREET_WAYS]);
   421→            if (derived_id_remaps.count((uint32_t)PatchFileId::ADDR_POINTS))
   422→                a_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::ADDR_POINTS]);
   423→            if (derived_id_remaps.count((uint32_t)PatchFileId::INTERP_WAYS))
   424→                i_rm = convert(derived_id_remaps[(uint32_t)PatchFileId::INTERP_WAYS]);
   425→
   426→            auto old_geo = read_file(cur_dir + "/geo_cells.bin");
   427→            auto old_se = read_file(cur_dir + "/street_entries.bin");
   428→            auto old_ae = read_file(cur_dir + "/addr_entries.bin");
   429→            auto old_ie = read_file(cur_dir + "/interp_entries.bin");
   430→
   431→            auto geo = rebuild_geo_from_remap(old_geo, old_se, old_ae, old_ie, w_rm, a_rm, i_rm,
   432→                                               geo_added, geo_removed);
   433→            // Only write geo_cells if not already provided as full replacement
   434→            if (!have_geo) write_file(out_dir + "/geo_cells.bin", geo.geo_cells_data);
   435→            if (!have_se) write_file(out_dir + "/street_entries.bin", geo.street_entries_data);
   436→            if (!have_ae) write_file(out_dir + "/addr_entries.bin", geo.addr_entries_data);
   437→            if (!have_ie) write_file(out_dir + "/interp_entries.bin", geo.interp_entries_data);
   438→            std::cerr << "  Rebuilt 3 entry files" << (have_geo ? "" : " + geo_cells") << std::endl;
   439→        }
   440→
   441→        if (!derived_id_remaps.empty()) { // always reconstruct admin entries for corrections
   442→            std::unordered_map<uint32_t,uint32_t> ad_rm;
   443→            if (derived_id_remaps.count((uint32_t)PatchFileId::ADMIN_POLYGONS)) {
   444→                auto& vec = derived_id_remaps[(uint32_t)PatchFileId::ADMIN_POLYGONS];
   445→                for (uint32_t i = 0; i < vec.size(); i++)
   446→                    if (vec[i] != 0xFFFFFFFF) ad_rm[i] = vec[i];
   447→            }
   448→            auto old_ac = read_file(cur_dir + "/admin_cells.bin");
   449→            auto old_ae = read_file(cur_dir + "/admin_entries.bin");
   450→            auto admin = rebuild_admin_from_remap(old_ac, old_ae, ad_rm, admin_added, admin_removed);
   451→            write_file(out_dir + "/admin_cells.bin", admin.admin_cells_data);
   452→            if (!have_adme) write_file(out_dir + "/admin_entries.bin", admin.admin_entries_data);
   453→            std::cerr << "  Rebuilt admin_entries" << (have_admin ? "" : " + admin_cells") << std::endl;
   454→        }
   455→    }
   456→
   457→    // Apply cell-level entry corrections (custom format, no zstd)
   458→    if (!entry_cell_corrections.empty()) {
   459→        std::cerr << "Applying cell-level entry corrections..." << std::endl;
   460→        for (auto& [fid, corrections] : entry_cell_corrections) {
   461→            const char* fname = patch_file_names[fid];
   462→            std::string entry_path = out_dir + "/" + std::string(fname);
   463→            // Determine which cells file to use
   464→            bool is_admin = (fid == (uint32_t)PatchFileId::ADMIN_ENTRIES);
   465→            std::string cells_fname = is_admin ? "admin_cells.bin" : "geo_cells.bin";
   466→            auto cells_data = read_file(out_dir + "/" + cells_fname);
   467→            auto entries_data = read_file(entry_path);
   468→            size_t cell_stride = is_admin ? 12 : 20;
   469→            size_t off_pos = is_admin ? 8 : (fid == (uint32_t)PatchFileId::STREET_ENTRIES ? 8 :
   470→                                              fid == (uint32_t)PatchFileId::ADDR_ENTRIES ? 12 : 16);
   471→
   472→            // Parse current entries into cell map
   473→            size_t n = cells_data.size() / cell_stride;
   474→            struct CellEntry { uint64_t cid; std::vector<uint32_t> ids; };
   475→            std::vector<CellEntry> cell_entries(n);
   476→            for (size_t i = 0; i < n; i++) {
   477→                memcpy(&cell_entries[i].cid, cells_data.data() + i * cell_stride, 8);
   478→                uint32_t off; memcpy(&off, cells_data.data() + i * cell_stride + off_pos, 4);
   479→                if (off != 0xFFFFFFFF && off + 2 <= entries_data.size()) {
   480→                    uint16_t count; memcpy(&count, entries_data.data() + off, 2);
   481→                    if (off + 2 + count * 4 <= entries_data.size()) {
   482→                        cell_entries[i].ids.resize(count);
   483→                        memcpy(cell_entries[i].ids.data(), entries_data.data() + off + 2, count * 4);
   484→                    }
   485→                }
   486→            }
   487→
   488→            // Apply corrections: replace entry data for specific cells
   489→            std::unordered_map<uint64_t, const std::vector<uint32_t>*> corr_map;
   490→            for (auto& c : corrections) corr_map[c.cell_id] = &c.ids;
   491→
   492→            // Apply corrections to existing cells
   493→            size_t applied = 0;
   494→            std::unordered_set<uint64_t> existing_cells;
   495→            for (auto& ce : cell_entries) {
   496→                existing_cells.insert(ce.cid);
   497→                auto it = corr_map.find(ce.cid);
   498→                if (it != corr_map.end()) {

        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[pos]); pos++;
            uint32_t count; memcpy(&count, patch.data() + pos, 4); pos += 4;

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

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

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

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

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

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

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

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

    // Apply cell-level entry corrections (custom format, no zstd)
    if (!entry_cell_corrections.empty()) {
        std::cerr << "Applying cell-level entry corrections..." << std::endl;
        for (auto& [fid, corrections] : entry_cell_corrections) {
            const char* fname = patch_file_names[fid];
            std::string entry_path = out_dir + "/" + std::string(fname);
            // Determine which cells file to use
            bool is_admin = (fid == (uint32_t)PatchFileId::ADMIN_ENTRIES);
            std::string cells_fname = is_admin ? "admin_cells.bin" : "geo_cells.bin";
            auto cells_data = read_file(out_dir + "/" + cells_fname);
            auto entries_data = read_file(entry_path);
            size_t cell_stride = is_admin ? 12 : 20;
            size_t off_pos = is_admin ? 8 : (fid == (uint32_t)PatchFileId::STREET_ENTRIES ? 8 :
                                              fid == (uint32_t)PatchFileId::ADDR_ENTRIES ? 12 : 16);

            // Parse current entries into cell map
            size_t n = cells_data.size() / cell_stride;
            struct CellEntry { uint64_t cid; std::vector<uint32_t> ids; };
            std::vector<CellEntry> cell_entries(n);
            for (size_t i = 0; i < n; i++) {
                memcpy(&cell_entries[i].cid, cells_data.data() + i * cell_stride, 8);
                uint32_t off; memcpy(&off, cells_data.data() + i * cell_stride + off_pos, 4);
                if (off != 0xFFFFFFFF && off + 2 <= entries_data.size()) {
                    uint16_t count; memcpy(&count, entries_data.data() + off, 2);
                    if (off + 2 + count * 4 <= entries_data.size()) {
                        cell_entries[i].ids.resize(count);
                        memcpy(cell_entries[i].ids.data(), entries_data.data() + off + 2, count * 4);
                    }
                }
            }

            // Apply corrections: replace entry data for specific cells
            std::unordered_map<uint64_t, const std::vector<uint32_t>*> corr_map;
            for (auto& c : corrections) corr_map[c.cell_id] = &c.ids;

            // Apply corrections to existing cells
            size_t applied = 0;
            std::unordered_set<uint64_t> existing_cells;
            for (auto& ce : cell_entries) {
                existing_cells.insert(ce.cid);
                auto it = corr_map.find(ce.cid);
                if (it != corr_map.end()) {


   498→                if (it != corr_map.end()) {
   499→                    ce.ids = *it->second;
   500→                    applied++;
   501→                }
   502→            }
   503→            // Add cells from corrections that don't exist in derived
   504→            for (auto& c : corrections) {
   505→                if (!existing_cells.count(c.cell_id) && !c.ids.empty()) {
   506→                    cell_entries.push_back({c.cell_id, c.ids});
   507→                    applied++;
   508→                }
   509→            }
   510→            // Re-sort by cell_id to maintain order
   511→            std::sort(cell_entries.begin(), cell_entries.end(),
   512→                [](const CellEntry& a, const CellEntry& b) { return a.cid < b.cid; });
   513→
   514→            // Rewrite entry file
   515→            std::vector<char> new_entries;
   516→            std::unordered_map<uint64_t, uint32_t> offsets;
   517→            uint32_t no_data = 0xFFFFFFFF;
   518→            for (auto& ce : cell_entries) {
   519→                if (ce.ids.empty()) continue;
   520→                offsets[ce.cid] = static_cast<uint32_t>(new_entries.size());
   521→                uint16_t count = static_cast<uint16_t>(ce.ids.size());
   522→                new_entries.insert(new_entries.end(), (const char*)&count, (const char*)&count + 2);
   523→                new_entries.insert(new_entries.end(), (const char*)ce.ids.data(),
   524→                                  (const char*)ce.ids.data() + ce.ids.size() * 4);
   525→            }
   526→            write_file(entry_path, new_entries);
   527→
   528→            // Rebuild the cells file with corrected offsets
   529→            std::vector<char> new_cells;
   530→            for (auto& ce : cell_entries) {
   531→                new_cells.insert(new_cells.end(), (const char*)&ce.cid, (const char*)&ce.cid + 8);
   532→                if (cell_stride == 20) {
   533→                    // geo_cells: 3 offset columns. Only update the relevant one.
   534→                    // Read old offsets from cells_data for the other columns
   535→                    size_t idx = &ce - &cell_entries[0];
   536→                    for (size_t col_off : {8, 12, 16}) {
   537→                        if (col_off == off_pos) {
   538→                            auto it = offsets.find(ce.cid);
   539→                            uint32_t off = it != offsets.end() ? it->second : no_data;
   540→                            new_cells.insert(new_cells.end(), (const char*)&off, (const char*)&off + 4);
   541→                        } else {
   542→                            // Keep existing offset
   543→                            uint32_t old_off;
   544→                            memcpy(&old_off, cells_data.data() + idx * 20 + col_off, 4);
   545→                            new_cells.insert(new_cells.end(), (const char*)&old_off, (const char*)&old_off + 4);
   546→                        }
   547→                    }
   548→                } else {
   549→                    // admin_cells: single offset
   550→                    auto it = offsets.find(ce.cid);
   551→                    uint32_t off = it != offsets.end() ? it->second : no_data;
   552→                    new_cells.insert(new_cells.end(), (const char*)&off, (const char*)&off + 4);
   553→                }
   554→            }
   555→            write_file(out_dir + "/" + cells_fname, new_cells);
   556→
   557→            std::cerr << "  " << fname << ": applied " << applied << " cell corrections ("
   558→                      << corrections.size() << " total)" << std::endl;
   559→        }
   560→    }
   561→
   562→    // Apply old zstd-based corrections (legacy, will be removed)
   563→    std::vector<PendingCorrection> cell_corrections;
   564→    for (auto& corr : pending_corrections) {
   565→        if (corr.file_id == (uint32_t)PatchFileId::GEO_CELLS ||
   566→            corr.file_id == (uint32_t)PatchFileId::ADMIN_CELLS) {
   567→            cell_corrections.push_back(std::move(corr));
   568→            continue;
   569→        }
   570→        // Apply entry correction
   571→        std::string derived_path = out_dir + "/" + corr.filename;
   572→        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
   573→        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
   574→        write_file(zst_path, corr.delta);
   575→        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
   576→                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
   577→        if (system(cmd.c_str()) != 0)
   578→            std::cerr << "  " << corr.filename << ": entry correction FAILED" << std::endl;
   579→        else {
   580→            auto corrected = read_file(corrected_path);
   581→            write_file(derived_path, corrected);
   582→            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
   583→        }
   584→        remove(zst_path.c_str()); remove(corrected_path.c_str());
   585→    }
   586→    pending_corrections.clear(); // entry corrections done
   587→
   588→    // Now apply cell corrections (after geo_cells rebuild from corrected entries)
   589→    for (auto& corr : cell_corrections) {
   590→        std::string derived_path = out_dir + "/" + corr.filename;
   591→        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
   592→        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
   593→        write_file(zst_path, corr.delta);
   594→        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
   595→                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
   596→        if (system(cmd.c_str()) != 0) {
   597→            std::cerr << "  " << corr.filename << ": correction FAILED" << std::endl;
   598→        } else {
   599→            auto corrected = read_file(corrected_path);
   600→            write_file(derived_path, corrected);
   601→            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
   602→        }
   603→        remove(zst_path.c_str()); remove(corrected_path.c_str());
   604→    }
   605→
   606→    // Post-correction geo_cells rebuild from corrected entries
   607→    // This produces a nearly-correct geo_cells (>99.99% accurate)
   608→    // which is then fixed by the tiny geo_cells correction delta.
   609→    {
   610→        auto corr_se = read_file(out_dir + "/street_entries.bin");
   611→        auto corr_ae = read_file(out_dir + "/addr_entries.bin");
   612→        auto corr_ie = read_file(out_dir + "/interp_entries.bin");
   613→        auto derived_geo = read_file(out_dir + "/geo_cells.bin");
   614→
   615→        // Rebuild if we have cell flag corrections or cell changes (no full-replacement geo_cells)
   616→        bool need_geo_rebuild = !cell_flag_corrections.empty() || !geo_added.empty() || !geo_removed.empty();
   617→

                if (it != corr_map.end()) {
                    ce.ids = *it->second;
                    applied++;
                }
            }
            // Add cells from corrections that don't exist in derived
            for (auto& c : corrections) {
                if (!existing_cells.count(c.cell_id) && !c.ids.empty()) {
                    cell_entries.push_back({c.cell_id, c.ids});
                    applied++;
                }
            }
            // Re-sort by cell_id to maintain order
            std::sort(cell_entries.begin(), cell_entries.end(),
                [](const CellEntry& a, const CellEntry& b) { return a.cid < b.cid; });

            // Rewrite entry file
            std::vector<char> new_entries;
            std::unordered_map<uint64_t, uint32_t> offsets;
            uint32_t no_data = 0xFFFFFFFF;
            for (auto& ce : cell_entries) {
                if (ce.ids.empty()) continue;
                offsets[ce.cid] = static_cast<uint32_t>(new_entries.size());
                uint16_t count = static_cast<uint16_t>(ce.ids.size());
                new_entries.insert(new_entries.end(), (const char*)&count, (const char*)&count + 2);
                new_entries.insert(new_entries.end(), (const char*)ce.ids.data(),
                                  (const char*)ce.ids.data() + ce.ids.size() * 4);
            }
            write_file(entry_path, new_entries);

            // Rebuild the cells file with corrected offsets
            std::vector<char> new_cells;
            for (auto& ce : cell_entries) {
                new_cells.insert(new_cells.end(), (const char*)&ce.cid, (const char*)&ce.cid + 8);
                if (cell_stride == 20) {
                    // geo_cells: 3 offset columns. Only update the relevant one.
                    // Read old offsets from cells_data for the other columns
                    size_t idx = &ce - &cell_entries[0];
                    for (size_t col_off : {8, 12, 16}) {
                        if (col_off == off_pos) {
                            auto it = offsets.find(ce.cid);
                            uint32_t off = it != offsets.end() ? it->second : no_data;
                            new_cells.insert(new_cells.end(), (const char*)&off, (const char*)&off + 4);
                        } else {
                            // Keep existing offset
                            uint32_t old_off;
                            memcpy(&old_off, cells_data.data() + idx * 20 + col_off, 4);
                            new_cells.insert(new_cells.end(), (const char*)&old_off, (const char*)&old_off + 4);
                        }
                    }
                } else {
                    // admin_cells: single offset
                    auto it = offsets.find(ce.cid);
                    uint32_t off = it != offsets.end() ? it->second : no_data;
                    new_cells.insert(new_cells.end(), (const char*)&off, (const char*)&off + 4);
                }
            }
            write_file(out_dir + "/" + cells_fname, new_cells);

            std::cerr << "  " << fname << ": applied " << applied << " cell corrections ("
                      << corrections.size() << " total)" << std::endl;
        }
    }

    // Apply old zstd-based corrections (legacy, will be removed)
    std::vector<PendingCorrection> cell_corrections;
    for (auto& corr : pending_corrections) {
        if (corr.file_id == (uint32_t)PatchFileId::GEO_CELLS ||
            corr.file_id == (uint32_t)PatchFileId::ADMIN_CELLS) {
            cell_corrections.push_back(std::move(corr));
            continue;
        }
        // Apply entry correction
        std::string derived_path = out_dir + "/" + corr.filename;
        std::string zst_path = tmpdir + "/corr_" + corr.filename + ".zst";
        std::string corrected_path = tmpdir + "/corr_" + corr.filename + ".out";
        write_file(zst_path, corr.delta);
        std::string cmd = "zstd --patch-from='" + derived_path + "' -d '" + zst_path +
                          "' -o '" + corrected_path + "' -f --long=31 --quiet 2>/dev/null";
        if (system(cmd.c_str()) != 0)
            std::cerr << "  " << corr.filename << ": entry correction FAILED" << std::endl;
        else {
            auto corrected = read_file(corrected_path);
            write_file(derived_path, corrected);
            std::cerr << "  " << corr.filename << ": corrected → " << corrected.size() << " bytes" << std::endl;
        }
        remove(zst_path.c_str()); remove(corrected_path.c_str());
    }
    pending_corrections.clear(); // entry corrections done

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

    // Post-correction geo_cells rebuild from corrected entries
    // This produces a nearly-correct geo_cells (>99.99% accurate)
    // which is then fixed by the tiny geo_cells correction delta.
    {
        auto corr_se = read_file(out_dir + "/street_entries.bin");
        auto corr_ae = read_file(out_dir + "/addr_entries.bin");
        auto corr_ie = read_file(out_dir + "/interp_entries.bin");
        auto derived_geo = read_file(out_dir + "/geo_cells.bin");

        // Rebuild if we have cell flag corrections or cell changes (no full-replacement geo_cells)
        bool need_geo_rebuild = !cell_flag_corrections.empty() || !geo_added.empty() || !geo_removed.empty();


   617→
   618→        if (!derived_geo.empty() && need_geo_rebuild) {
   619→            size_t n = derived_geo.size() / 20;
   620→            uint32_t no_data = 0xFFFFFFFF;
   621→
   622→            // Count entries in each corrected file
   623→            auto count_entries = [](const std::vector<char>& ef) -> size_t {
   624→                size_t count = 0, pos = 0;
   625→                while (pos + 2 <= ef.size()) {
   626→                    uint16_t c; memcpy(&c, ef.data() + pos, 2);
   627→                    pos += 2 + c * 4;
   628→                    count++;
   629→                }
   630→                return count;
   631→            };
   632→
   633→            // Build offset arrays by scanning corrected entries
   634→            auto build_offsets = [](const std::vector<char>& ef) -> std::vector<uint32_t> {
   635→                std::vector<uint32_t> offsets;
   636→                uint32_t pos = 0;
   637→                while (pos + 2 <= ef.size()) {
   638→                    offsets.push_back(pos);
   639→                    uint16_t c; memcpy(&c, ef.data() + pos, 2);
   640→                    pos += 2 + c * 4;
   641→                }
   642→                return offsets;
   643→            };
   644→
   645→            auto s_offsets = build_offsets(corr_se);
   646→            auto a_offsets = build_offsets(corr_ae);
   647→            auto i_offsets = build_offsets(corr_ie);
   648→
   649→            // Walk derived geo_cells, replace offsets with corrected values
   650→            size_t si = 0, ai = 0, ii = 0;
   651→            std::vector<char> new_geo(derived_geo.size());
   652→            for (size_t c = 0; c < n; c++) {
   653→                // Copy cell_id
   654→                memcpy(new_geo.data() + c * 20, derived_geo.data() + c * 20, 8);
   655→
   656→                // Determine has/hasn't flags (from derived, with corrections applied)
   657→                uint64_t cid; memcpy(&cid, derived_geo.data() + c * 20, 8);
   658→                uint32_t old_s, old_a, old_i;
   659→                memcpy(&old_s, derived_geo.data() + c * 20 + 8, 4);
   660→                memcpy(&old_a, derived_geo.data() + c * 20 + 12, 4);
   661→                memcpy(&old_i, derived_geo.data() + c * 20 + 16, 4);
   662→                bool has_s = (old_s != no_data), has_a = (old_a != no_data), has_i = (old_i != no_data);
   663→
   664→                // Apply flag correction if this cell has one
   665→                auto fc_it = cell_flag_corrections.find(cid);
   666→                if (fc_it != cell_flag_corrections.end()) {
   667→                    has_s = (fc_it->second & 1) != 0;
   668→                    has_a = (fc_it->second & 2) != 0;
   669→                    has_i = (fc_it->second & 4) != 0;
   670→                }
   671→
   672→                uint32_t new_s = (has_s && si < s_offsets.size()) ? s_offsets[si++] : no_data;
   673→                uint32_t new_a = (has_a && ai < a_offsets.size()) ? a_offsets[ai++] : no_data;
   674→                uint32_t new_i = (has_i && ii < i_offsets.size()) ? i_offsets[ii++] : no_data;
   675→
   676→                memcpy(new_geo.data() + c * 20 + 8, &new_s, 4);

        if (!derived_geo.empty() && need_geo_rebuild) {
            size_t n = derived_geo.size() / 20;
            uint32_t no_data = 0xFFFFFFFF;

            // Count entries in each corrected file
            auto count_entries = [](const std::vector<char>& ef) -> size_t {
                size_t count = 0, pos = 0;
                while (pos + 2 <= ef.size()) {
                    uint16_t c; memcpy(&c, ef.data() + pos, 2);
                    pos += 2 + c * 4;
                    count++;
                }
                return count;
            };

            // Build offset arrays by scanning corrected entries
            auto build_offsets = [](const std::vector<char>& ef) -> std::vector<uint32_t> {
                std::vector<uint32_t> offsets;
                uint32_t pos = 0;
                while (pos + 2 <= ef.size()) {
                    offsets.push_back(pos);
                    uint16_t c; memcpy(&c, ef.data() + pos, 2);
                    pos += 2 + c * 4;
                }
                return offsets;
            };

            auto s_offsets = build_offsets(corr_se);
            auto a_offsets = build_offsets(corr_ae);
            auto i_offsets = build_offsets(corr_ie);

            // Walk derived geo_cells, replace offsets with corrected values
            size_t si = 0, ai = 0, ii = 0;
            std::vector<char> new_geo(derived_geo.size());
            for (size_t c = 0; c < n; c++) {
                // Copy cell_id
                memcpy(new_geo.data() + c * 20, derived_geo.data() + c * 20, 8);

                // Determine has/hasn't flags (from derived, with corrections applied)
                uint64_t cid; memcpy(&cid, derived_geo.data() + c * 20, 8);
                uint32_t old_s, old_a, old_i;
                memcpy(&old_s, derived_geo.data() + c * 20 + 8, 4);
                memcpy(&old_a, derived_geo.data() + c * 20 + 12, 4);
                memcpy(&old_i, derived_geo.data() + c * 20 + 16, 4);
                bool has_s = (old_s != no_data), has_a = (old_a != no_data), has_i = (old_i != no_data);

                // Apply flag correction if this cell has one
                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;
                }

                uint32_t new_s = (has_s && si < s_offsets.size()) ? s_offsets[si++] : no_data;
                uint32_t new_a = (has_a && ai < a_offsets.size()) ? a_offsets[ai++] : no_data;
                uint32_t new_i = (has_i && ii < i_offsets.size()) ? i_offsets[ii++] : no_data;

                memcpy(new_geo.data() + c * 20 + 8, &new_s, 4);


   676→                memcpy(new_geo.data() + c * 20 + 8, &new_s, 4);
   677→                memcpy(new_geo.data() + c * 20 + 12, &new_a, 4);
   678→                memcpy(new_geo.data() + c * 20 + 16, &new_i, 4);
   679→            }
   680→            write_file(out_dir + "/geo_cells.bin", new_geo);
   681→            std::cerr << "  Rebuilt geo_cells from corrected entries" << std::endl;
   682→        }
   683→    }
   684→    if (false) { // REMOVED: legacy rebuild code — see git history
   685→        auto se = read_file(out_dir + "/street_entries.bin");
   686→        auto ae = read_file(out_dir + "/addr_entries.bin");
   687→        auto ie = read_file(out_dir + "/interp_entries.bin");
   688→        auto existing_geo = read_file(out_dir + "/geo_cells.bin");
   689→
   690→        if (!existing_geo.empty()) {
   691→            // Parse the existing (derived) geo_cells for cell IDs, rebuild offsets from corrected entries
   692→            size_t n = existing_geo.size() / 20;
   693→
   694→            // Parse each entry file to build cell→offset maps
   695→            // Walk entries sequentially, matching against geo_cells cell order
   696→            auto build_offsets_from_geo = [](const std::vector<char>& geo, size_t off_pos,
   697→                                              const std::vector<char>& old_entries,
   698→                                              const std::vector<char>& new_entries) -> std::vector<char> {
   699→                // The corrected entries file has the right content.
   700→                // We need to map cell_ids to byte offsets in the corrected entries.
   701→                // The cell order is the same as in the derived geo_cells.
   702→                // We just need to recompute offsets.
   703→                return new_entries; // entries are already correct, just need geo_cells offsets
   704→            };
   705→
   706→            // Actually: re-walk the derived geo_cells, find each cell's entries in the corrected files
   707→            // Since the cell IDs are the same and entries are in the same order, we just recompute offsets.
   708→            // The corrected entry files have entries in cell_id order (from the rebuild).
   709→            // We can walk them to compute offsets.
   710→
   711→            // For each entry type, build cell_id → offset by scanning the entry file
   712→            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
   713→                // Returns a vector of entry start positions
   714→                std::vector<uint32_t> positions;
   715→                uint32_t pos = 0;

                memcpy(new_geo.data() + c * 20 + 8, &new_s, 4);
                memcpy(new_geo.data() + c * 20 + 12, &new_a, 4);
                memcpy(new_geo.data() + c * 20 + 16, &new_i, 4);
            }
            write_file(out_dir + "/geo_cells.bin", new_geo);
            std::cerr << "  Rebuilt geo_cells from corrected entries" << std::endl;
        }
    }
    if (false) { // REMOVED: legacy rebuild code — see git history
        auto se = read_file(out_dir + "/street_entries.bin");
        auto ae = read_file(out_dir + "/addr_entries.bin");
        auto ie = read_file(out_dir + "/interp_entries.bin");
        auto existing_geo = read_file(out_dir + "/geo_cells.bin");

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

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

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

            // For each entry type, build cell_id → offset by scanning the entry file
            auto scan_entries = [](const std::vector<char>& entries) -> std::vector<uint32_t> {
                // Returns a vector of entry start positions
                std::vector<uint32_t> positions;
                uint32_t pos = 0;


Now I need to add the secondary remap handling in the patch tool. The best place is right after the merge replay section builds the `derived_id_remaps`, but before the entry reconstruction. I need to handle the `SECONDARY_REMAP_MARKER` in the section loop:


Edit

if (file_id == ENTRY_CORRECTION_MARKER) {

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