Good point. Let me analyze the time complexity of each step:

1. **String pool sort**: O(N log N) for ~1-3M strings. Each string comparison is fast (first chars often differ). Should be < 1 second.

2. **Addr points reorder**: Single O(N) pass over sorted_addr_cells (already sorted). Essentially a scatter-gather copy. For ~80M Europe / ~1.3B planet addr_points → ~1-3 seconds.

3. **Way sort**: O(N log N) for ~40M Europe / ~300M planet ways. The sort key involves an S2 cell compute (fast - already in memory) + a strcmp. Maybe 5-10 seconds for planet.

4. **Way node copy**: O(total_nodes) sequential copy. For ~1.5B planet nodes × 8 bytes = ~12 GiB memcpy. ~3-5 seconds.

5. **Interp reorder**: Same as ways but much smaller (~160K interps). Negligible.

6. **Sorted pair re-sort**: O(N log N) for sorted_way_cells. These are already nearly sorted (cells unchanged, only item_ids within each cell changed). ~2-3 seconds.

**Total estimate: ~15-25 seconds for planet build.** Against a 12-minute total, that's ~3% overhead. Definitely acceptable.

But there's a memory concern — we temporarily need 2x the ways/nodes arrays during the copy. For planet, `data.street_nodes` is ~12 GiB. Let me check if we have enough headroom on node 3 (174 GiB RAM, peak RSS was ~120 GiB during build).

Actually, looking at the code more carefully, the way reordering creates `new_nodes` of the same size as `data.street_nodes`. That doubles memory usage for that array briefly. On node 3 (174 GiB, ~50 GiB headroom), this should be fine. On node 2b it might be tight.

Let me optimize the memory usage by doing in-place reordering where possible, and make the addr_point reorder more memory-efficient:


  1288→    // --- Stabilize write order for patchability ---
  1289→    // Reorder data so that the same geographic data lands at the same file offset
  1290→    // across builds, making binary delta patches (zstd --patch-from) dramatically smaller.
  1291→    // Without this, PBF processing order + thread merge order creates non-deterministic
  1292→    // record positions, causing ~27% patch ratios. With stable ordering: ~2-5%.
  1293→    {
  1294→        std::cerr << "Stabilizing write order for patchability..." << std::endl;
  1295→        auto _st = std::chrono::steady_clock::now();
  1296→        auto _sc = CpuTicks::now();
  1297→
  1298→        // Step 1: Sort string pool alphabetically.
  1299→        // This ensures the same string always gets the same offset across builds.
  1300→        {
  1301→            auto& pool_data = data.string_pool.mutable_data();
  1302→            if (!pool_data.empty()) {
  1303→                // Extract all strings with current offsets
  1304→                std::vector<std::pair<uint32_t, const char*>> strings;
  1305→                size_t pos = 0;
  1306→                while (pos < pool_data.size()) {
  1307→                    strings.emplace_back(static_cast<uint32_t>(pos), pool_data.data() + pos);
  1308→                    pos += strlen(pool_data.data() + pos) + 1;
  1309→                }
  1310→
  1311→                // Sort by string content
  1312→                std::sort(strings.begin(), strings.end(), [](const auto& a, const auto& b) {
  1313→                    return strcmp(a.second, b.second) < 0;
  1314→                });
  1315→
  1316→                // Build new pool and offset mapping
  1317→                std::vector<uint32_t> remap(pool_data.size(), 0);

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


The remap vector uses `pool_data.size()` as its size — this is the byte length of the string pool, and we index it by byte offset. That's correct and efficient (one uint32_t per byte of pool, so ~82 MB × 4 = ~330 MB for Europe). For planet it could be larger but still manageable.

Actually wait, using a vector indexed by byte offset is wasteful — most offsets aren't string starts. Let me use an unordered_map instead for the remap, which only stores entries for actual string start offsets:


Edit

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

                // Build new pool and offset mapping
                std::unordered_map<uint32_t, uint32_t> remap;
                remap.reserve(strings.size());
                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);
                }