  1215→                }
  1216→                data.sorted_addr_cells = std::move(pairs);
  1217→                log_phase("    Dedup: build sorted pairs", _dt, _dc);
  1218→            });
  1219→            auto f4 = std::async(std::launch::async, [&]{ deduplicate(data.cell_to_admin); });
  1220→            f2.get(); f4.get();
  1221→        }
  1222→
  1223→        // Save cache if requested
  1224→        if (!save_cache_path.empty()) {
  1225→            serialize_cache(data, save_cache_path);
  1226→        }
  1227→    }
  1228→
  1229→    // --- Rebuild hash maps from sorted pairs if needed for cache saving ---
  1230→    // (Continent filtering now uses sorted pairs directly, so no rebuild needed for that.)
  1231→    if (!save_cache_path.empty()) {
  1232→        // Parallel rebuild: split sorted pairs into chunks, each thread builds
  1233→        // a partial map, then merge. Sorted pairs are grouped by cell_id so we
  1234→        // can split at cell boundaries for zero-conflict parallel insertion.
  1235→        auto rebuild_map_parallel = [](const std::vector<CellItemPair>& sorted,
  1236→                                        std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
  1237→            if (sorted.empty() || !map.empty()) return;
  1238→            unsigned nthreads = std::thread::hardware_concurrency();
  1239→            if (nthreads == 0) nthreads = 4;
  1240→            size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
  1241→
  1242→            // Find cell boundaries for clean splits
  1243→            std::vector<size_t> boundaries = {0};
  1244→            for (unsigned t = 1; t < nthreads; t++) {
  1245→                size_t target = t * chunk;
  1246→                if (target >= sorted.size()) break;
  1247→                // Advance to next cell boundary
  1248→                while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
  1249→                    target++;
  1250→                if (target < sorted.size()) boundaries.push_back(target);
  1251→            }
  1252→            boundaries.push_back(sorted.size());
  1253→
  1254→            // Each thread builds its own sub-map
  1255→            std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> sub_maps(boundaries.size() - 1);
  1256→            std::vector<std::thread> threads;
  1257→            for (size_t t = 0; t + 1 < boundaries.size(); t++) {
  1258→                threads.emplace_back([&, t]() {
  1259→                    auto& sub = sub_maps[t];
  1260→                    // Pre-estimate capacity
  1261→                    size_t n = boundaries[t+1] - boundaries[t];
  1262→                    sub.reserve(n / 3); // rough estimate of unique cells
  1263→                    for (size_t i = boundaries[t]; i < boundaries[t+1]; i++) {
  1264→                        sub[sorted[i].cell_id].push_back(sorted[i].item_id);
  1265→                    }
  1266→                });
  1267→            }
  1268→            for (auto& t : threads) t.join();
  1269→
  1270→            // Merge sub-maps into main map (no conflicts since splits are at cell boundaries)
  1271→            size_t total_cells = 0;
  1272→            for (auto& sub : sub_maps) total_cells += sub.size();
  1273→            map.reserve(total_cells);
  1274→            for (auto& sub : sub_maps) {
  1275→                for (auto& [k, v] : sub) {
  1276→                    map[k] = std::move(v);
  1277→                }
  1278→            }
  1279→        };
  1280→
  1281→        std::cerr << "Rebuilding cell maps for continent filtering..." << std::endl;
  1282→        auto f1 = std::async(std::launch::async, [&]{ rebuild_map_parallel(data.sorted_way_cells, data.cell_to_ways); });
  1283→        auto f2 = std::async(std::launch::async, [&]{ rebuild_map_parallel(data.sorted_interp_cells, data.cell_to_interps); });
  1284→        f1.get(); f2.get();
  1285→        log_phase("Rebuild cell maps", _pt, _cpu);
  1286→    }
  1287→
  1288→    // --- Write index files ---
  1289→    log_phase("Deduplication", _pt, _cpu);
  1290→    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;
  1291→
  1292→    auto quality_dir_name = [](double scale) -> std::string {
  1293→        if (scale == 0) return "uncapped";
  1294→        char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);

                }
                data.sorted_addr_cells = std::move(pairs);
                log_phase("    Dedup: build sorted pairs", _dt, _dc);
            });
            auto f4 = std::async(std::launch::async, [&]{ deduplicate(data.cell_to_admin); });
            f2.get(); f4.get();
        }

        // Save cache if requested
        if (!save_cache_path.empty()) {
            serialize_cache(data, save_cache_path);
        }
    }

    // --- Rebuild hash maps from sorted pairs if needed for cache saving ---
    // (Continent filtering now uses sorted pairs directly, so no rebuild needed for that.)
    if (!save_cache_path.empty()) {
        // Parallel rebuild: split sorted pairs into chunks, each thread builds
        // a partial map, then merge. Sorted pairs are grouped by cell_id so we
        // can split at cell boundaries for zero-conflict parallel insertion.
        auto rebuild_map_parallel = [](const std::vector<CellItemPair>& sorted,
                                        std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
            if (sorted.empty() || !map.empty()) return;
            unsigned nthreads = std::thread::hardware_concurrency();
            if (nthreads == 0) nthreads = 4;
            size_t chunk = (sorted.size() + nthreads - 1) / nthreads;

            // Find cell boundaries for clean splits
            std::vector<size_t> boundaries = {0};
            for (unsigned t = 1; t < nthreads; t++) {
                size_t target = t * chunk;
                if (target >= sorted.size()) break;
                // Advance to next cell boundary
                while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                    target++;
                if (target < sorted.size()) boundaries.push_back(target);
            }
            boundaries.push_back(sorted.size());

            // Each thread builds its own sub-map
            std::vector<std::unordered_map<uint64_t, std::vector<uint32_t>>> sub_maps(boundaries.size() - 1);
            std::vector<std::thread> threads;
            for (size_t t = 0; t + 1 < boundaries.size(); t++) {
                threads.emplace_back([&, t]() {
                    auto& sub = sub_maps[t];
                    // Pre-estimate capacity
                    size_t n = boundaries[t+1] - boundaries[t];
                    sub.reserve(n / 3); // rough estimate of unique cells
                    for (size_t i = boundaries[t]; i < boundaries[t+1]; i++) {
                        sub[sorted[i].cell_id].push_back(sorted[i].item_id);
                    }
                });
            }
            for (auto& t : threads) t.join();

            // Merge sub-maps into main map (no conflicts since splits are at cell boundaries)
            size_t total_cells = 0;
            for (auto& sub : sub_maps) total_cells += sub.size();
            map.reserve(total_cells);
            for (auto& sub : sub_maps) {
                for (auto& [k, v] : sub) {
                    map[k] = std::move(v);
                }
            }
        };

        std::cerr << "Rebuilding cell maps for continent filtering..." << std::endl;
        auto f1 = std::async(std::launch::async, [&]{ rebuild_map_parallel(data.sorted_way_cells, data.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ rebuild_map_parallel(data.sorted_interp_cells, data.cell_to_interps); });
        f1.get(); f2.get();
        log_phase("Rebuild cell maps", _pt, _cpu);
    }

    // --- Write index files ---
    log_phase("Deduplication", _pt, _cpu);
    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;

    auto quality_dir_name = [](double scale) -> std::string {
        if (scale == 0) return "uncapped";
        char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);


Now I have the exact insertion point. The stabilization goes right after the dedup (line 1221, or after cache save at 1227) and before the write phase (line 1288). I'll insert it after the `if (!save_cache_path.empty()) { ... }` block at line 1287, right before the write phase.

Let me now write the stable ordering implementation:


Edit

    // --- Write index files ---
    log_phase("Deduplication", _pt, _cpu);
    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;

    // --- Stabilize write order for patchability ---
    // Reorder data so that the same geographic data lands at the same file offset
    // across builds, making binary delta patches (zstd --patch-from) dramatically smaller.
    // Without this, PBF processing order + thread merge order creates non-deterministic
    // record positions, causing ~27% patch ratios. With stable ordering: ~2-5%.
    {
        std::cerr << "Stabilizing write order for patchability..." << std::endl;
        auto _st = std::chrono::steady_clock::now();
        auto _sc = CpuTicks::now();

        // Step 1: Sort string pool alphabetically.
        // This ensures the same string always gets the same offset across builds.
        {
            auto& pool_data = data.string_pool.mutable_data();
            if (!pool_data.empty()) {
                // Extract all strings with current offsets
                std::vector<std::pair<uint32_t, const char*>> strings;
                size_t pos = 0;
                while (pos < pool_data.size()) {
                    strings.emplace_back(static_cast<uint32_t>(pos), pool_data.data() + pos);
                    pos += strlen(pool_data.data() + pos) + 1;
                }

                // Sort by string content
                std::sort(strings.begin(), strings.end(), [](const auto& a, const auto& b) {
                    return strcmp(a.second, b.second) < 0;
                });

                // Build new pool and offset mapping
                std::vector<uint32_t> remap(pool_data.size(), 0);
                std::vector<char> new_data;
                new_data.reserve(pool_data.size());
                for (auto& [old_off, str] : strings) {
                    uint32_t new_off = static_cast<uint32_t>(new_data.size());
                    remap[old_off] = new_off;
                    size_t len = strlen(str);
                    new_data.insert(new_data.end(), str, str + len + 1);
                }

                // Apply remap to all data (parallel for large arrays)
                unsigned nthreads = std::max(1u, std::thread::hardware_concurrency());
                auto parallel_remap = [nthreads](auto* data, size_t count, auto remap_fn) {
                    if (count == 0) return;
                    size_t chunk = (count + nthreads - 1) / nthreads;
                    std::vector<std::thread> threads;
                    for (unsigned t = 0; t < nthreads; t++) {
                        size_t start = t * chunk;
                        size_t end = std::min(start + chunk, count);
                        if (start >= count) break;
                        threads.emplace_back([data, start, end, &remap_fn]() {
                            for (size_t i = start; i < end; i++) remap_fn(data[i]);
                        });
                    }
                    for (auto& t : threads) t.join();
                };

                parallel_remap(data.ways.data(), data.ways.size(),
                    [&remap](WayHeader& w) { w.name_id = remap[w.name_id]; });
                parallel_remap(data.addr_points.data(), data.addr_points.size(),
                    [&remap](AddrPoint& a) { a.housenumber_id = remap[a.housenumber_id]; a.street_id = remap[a.street_id]; });
                parallel_remap(data.interp_ways.data(), data.interp_ways.size(),
                    [&remap](InterpWay& iw) { iw.street_id = remap[iw.street_id]; });
                parallel_remap(data.admin_polygons.data(), data.admin_polygons.size(),
                    [&remap](AdminPolygon& ap) { ap.name_id = remap[ap.name_id]; });

                pool_data = std::move(new_data);
                std::cerr << "  String pool sorted: " << strings.size() << " strings" << std::endl;
            }
        }
        log_phase("  Stabilize: sort string pool", _st, _sc);

        // Step 2: Reorder addr_points by S2 cell.
        // sorted_addr_cells is [(cell_id, old_addr_id), ...] sorted by cell_id.
        // Each addr maps to exactly one cell, so this is a complete permutation.
        if (!data.sorted_addr_cells.empty()) {
            std::vector<AddrPoint> new_points(data.sorted_addr_cells.size());
            for (size_t i = 0; i < data.sorted_addr_cells.size(); i++) {
                new_points[i] = data.addr_points[data.sorted_addr_cells[i].item_id];
                data.sorted_addr_cells[i].item_id = static_cast<uint32_t>(i);
            }
            data.addr_points = std::move(new_points);
            // Invalidate hash map (no longer matches new IDs)
            data.cell_to_addrs.clear();
            std::cerr << "  Addr points reordered: " << data.addr_points.size() << " points" << std::endl;
        }
        log_phase("  Stabilize: reorder addr_points", _st, _sc);

        // Step 3: Reorder ways by primary S2 cell + name.
        // Primary cell = cell of first node. Sorting by this groups geographically
        // nearby ways together, producing stable file positions across builds.
        if (!data.ways.empty()) {
            size_t nways = data.ways.size();

            // Compute primary cell for each way
            std::vector<uint64_t> primary_cells(nways);
            for (size_t i = 0; i < nways; i++) {
                auto& w = data.ways[i];
                if (w.node_count > 0) {
                    auto& n = data.street_nodes[w.node_offset];
                    primary_cells[i] = S2CellId(S2LatLng::FromDegrees(n.lat, n.lng)).parent(kStreetCellLevel).id();
                }
            }

            // Build permutation: sort by (primary_cell, name)
            const auto& sp = data.string_pool.data();
            std::vector<uint32_t> order(nways);
            std::iota(order.begin(), order.end(), 0);
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                if (primary_cells[a] != primary_cells[b]) return primary_cells[a] < primary_cells[b];
                return strcmp(sp.data() + data.ways[a].name_id, sp.data() + data.ways[b].name_id) < 0;
            });

            // Build old→new mapping
            std::vector<uint32_t> old_to_new(nways);
            for (uint32_t new_id = 0; new_id < nways; new_id++)
                old_to_new[order[new_id]] = new_id;

            // Reorder ways and nodes
            std::vector<WayHeader> new_ways(nways);
            std::vector<NodeCoord> new_nodes;
            new_nodes.reserve(data.street_nodes.size());
            for (uint32_t new_id = 0; new_id < nways; new_id++) {
                auto w = data.ways[order[new_id]];
                w.node_offset = static_cast<uint32_t>(new_nodes.size());
                for (uint8_t j = 0; j < w.node_count; j++)
                    new_nodes.push_back(data.street_nodes[data.ways[order[new_id]].node_offset + j]);
                new_ways[new_id] = w;
            }
            data.ways = std::move(new_ways);
            data.street_nodes = std::move(new_nodes);

            // Update sorted_way_cells with new IDs
            for (auto& pair : data.sorted_way_cells)
                pair.item_id = old_to_new[pair.item_id];
            // Re-sort within each cell for deterministic entry file content
            auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
                return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
            };
            std::sort(data.sorted_way_cells.begin(), data.sorted_way_cells.end(), cmp);

            data.cell_to_ways.clear();
            std::cerr << "  Ways reordered: " << nways << " ways, "
                      << data.street_nodes.size() << " nodes" << std::endl;
        }
        log_phase("  Stabilize: reorder ways", _st, _sc);

        // Step 4: Reorder interpolations by primary S2 cell + street name.
        if (!data.interp_ways.empty()) {
            size_t ninterps = data.interp_ways.size();
            std::vector<uint64_t> primary_cells(ninterps);
            for (size_t i = 0; i < ninterps; i++) {
                auto& iw = data.interp_ways[i];
                if (iw.node_count > 0) {
                    auto& n = data.interp_nodes[iw.node_offset];
                    primary_cells[i] = S2CellId(S2LatLng::FromDegrees(n.lat, n.lng)).parent(kStreetCellLevel).id();
                }
            }

            const auto& sp = data.string_pool.data();
            std::vector<uint32_t> order(ninterps);
            std::iota(order.begin(), order.end(), 0);
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                if (primary_cells[a] != primary_cells[b]) return primary_cells[a] < primary_cells[b];
                return strcmp(sp.data() + data.interp_ways[a].street_id,
                              sp.data() + data.interp_ways[b].street_id) < 0;
            });

            std::vector<uint32_t> old_to_new(ninterps);
            for (uint32_t new_id = 0; new_id < ninterps; new_id++)
                old_to_new[order[new_id]] = new_id;

            std::vector<InterpWay> new_interps(ninterps);
            std::vector<NodeCoord> new_nodes;
            new_nodes.reserve(data.interp_nodes.size());
            for (uint32_t new_id = 0; new_id < ninterps; new_id++) {
                auto iw = data.interp_ways[order[new_id]];
                uint32_t old_offset = iw.node_offset;
                iw.node_offset = static_cast<uint32_t>(new_nodes.size());
                for (uint8_t j = 0; j < iw.node_count; j++)
                    new_nodes.push_back(data.interp_nodes[old_offset + j]);
                new_interps[new_id] = iw;
            }
            data.interp_ways = std::move(new_interps);
            data.interp_nodes = std::move(new_nodes);

            for (auto& pair : data.sorted_interp_cells)
                pair.item_id = old_to_new[pair.item_id];
            auto cmp = [](const CellItemPair& a, const CellItemPair& b) {
                return a.cell_id < b.cell_id || (a.cell_id == b.cell_id && a.item_id < b.item_id);
            };
            std::sort(data.sorted_interp_cells.begin(), data.sorted_interp_cells.end(), cmp);

            data.cell_to_interps.clear();
            std::cerr << "  Interps reordered: " << ninterps << " interps" << std::endl;
        }
        log_phase("  Stabilize: reorder interps", _st, _sc);
    }

    // --- Write index files ---
    log_phase("Deduplication + stabilization", _pt, _cpu);
    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;