            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                const auto& pa = data.admin_polygons[a];
                const auto& pb = data.admin_polygons[b];
                if (pa.name_id != pb.name_id) return pa.name_id < pb.name_id;
                if (pa.admin_level != pb.admin_level) return pa.admin_level < pb.admin_level;
                if (pa.country_code != pb.country_code) return pa.country_code < pb.country_code;
                if (pa.vertex_count != pb.vertex_count) return pa.vertex_count < pb.vertex_count;
                // Compare first few vertices for tiebreaking
                uint32_t nc = std::min(pa.vertex_count, pb.vertex_count);
                nc = std::min(nc, 20u); // limit comparison depth
                for (uint32_t j = 0; j < nc; j++) {
                    uint32_t la = float_bits(data.admin_vertices[pa.vertex_offset + j].lat);
                    uint32_t lb = float_bits(data.admin_vertices[pb.vertex_offset + j].lat);
                    if (la != lb) return la < lb;
                    uint32_t ga = float_bits(data.admin_vertices[pa.vertex_offset + j].lng);
                    uint32_t gb = float_bits(data.admin_vertices[pb.vertex_offset + j].lng);
                    if (ga != gb) return ga < gb;
                }
                return false;
            });

#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#include <fstream>
#include <future>
#include <iostream>
#include <mutex>
#include <numeric>
#include <string>
#include <thread>
#include <unordered_set>
#include <vector>

#include <fcntl.h>
#include <sys/mman.h>

#include "pbf_reader.h"

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>

#include "types.h"
#include "string_pool.h"
#include "geometry.h"
#include "parsed_data.h"
#include "s2_helpers.h"
#include "ring_assembly.h"
#include "continent_boundaries.h"
#include "interpolation.h"
#include "cache.h"
#include "continent_filter.h"
#include "cell_index.h"

// --- Main ---

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
        std::cerr << std::endl;
        std::cerr << "Options:" << std::endl;
        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
        std::cerr << "  --simplify-epsilon     Use error-bounded simplification (per-level defaults)" << std::endl;
        std::cerr << "  --simplify-epsilon N   Use fixed epsilon of N meters for all levels" << std::endl;
        std::cerr << "  --epsilon-scale F      Multiply all per-level epsilons by F (e.g. 2.0 = 2x coarser)" << std::endl;
        std::cerr << "  --epsilon-levels L2,L3,...,L8  Set epsilon in meters per level (7 values)" << std::endl;
        std::cerr << "  --multi-quality [S,S,...]  Write quality variants at given scales (default: 0,0.2,0.5,1,1.5,2,2.5)" << std::endl;
        return 1;
    }

    std::string output_dir = argv[1];
    std::vector<std::string> input_files;
    std::string save_cache_path;
    std::string load_cache_path;
    bool multi_output = false;
    bool generate_continents = false;
    IndexMode mode = IndexMode::Full;
    SimplifyMode simplify_mode = SimplifyMode::MaxVertices;
    double simplify_epsilon_override = 0; // 0 = use per-level defaults
    bool multi_quality = false;
    std::vector<double> quality_scales;

    for (int i = 2; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "--street-level" && i + 1 < argc) {
            kStreetCellLevel = std::atoi(argv[++i]);
        } else if (arg == "--admin-level" && i + 1 < argc) {
            kAdminCellLevel = std::atoi(argv[++i]);
        } else if (arg == "--admin-only") {
            mode = IndexMode::AdminOnly;
        } else if (arg == "--no-addresses") {
            mode = IndexMode::NoAddresses;
        } else if (arg == "--max-admin-level" && i + 1 < argc) {
            kMaxAdminLevel = std::atoi(argv[++i]);
        } else if (arg == "--save-cache" && i + 1 < argc) {
            save_cache_path = argv[++i];
        } else if (arg == "--load-cache" && i + 1 < argc) {
            load_cache_path = argv[++i];
        } else if (arg == "--multi-output") {
            multi_output = true;
        } else if (arg == "--continents") {
            generate_continents = true;
        } else if (arg == "--simplify-epsilon") {
            simplify_mode = SimplifyMode::ErrorBounded;
            // Optional: next arg might be a number (fixed epsilon in meters)
            if (i + 1 < argc) {
                char* end;
                double val = std::strtod(argv[i+1], &end);
                if (*end == '\0' && val > 0) {
                    simplify_epsilon_override = val;
                    i++;
                }
            }
        } else if (arg == "--epsilon-scale" && i + 1 < argc) {
            kEpsilonScale = std::strtod(argv[++i], nullptr);
        } else if (arg == "--multi-quality") {
            multi_quality = true;
            // Optional: next arg might be comma-separated scales
            if (i + 1 < argc && argv[i+1][0] != '-') {
                std::string vals = argv[++i];
                size_t pos = 0;
                while (pos < vals.size()) {
                    size_t next = vals.find(',', pos);
                    if (next == std::string::npos) next = vals.size();
                    double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
                    quality_scales.push_back(v);
                    pos = next + 1;
                }
            }
            if (quality_scales.empty()) {
                quality_scales = {0, 0.2, 0.5, 1.0, 1.5, 2.0, 2.5};
            }
            // Build with uncapped polygons — quality variants simplify at write time.
            // We need full vertex detail to produce all quality levels accurately.
            simplify_mode = SimplifyMode::ErrorBounded;
            simplify_epsilon_override = 0.1; // effectively uncapped
        } else if (arg == "--epsilon-levels" && i + 1 < argc) {
            // Parse comma-separated values: L2,L3,L4,L5,L6,L7,L8
            std::string vals = argv[++i];
            int level = 2;
            size_t pos = 0;
            while (pos < vals.size() && level <= 8) {
                size_t next = vals.find(',', pos);
                if (next == std::string::npos) next = vals.size();
                double v = std::strtod(vals.substr(pos, next - pos).c_str(), nullptr);
                if (v > 0) kAdminEpsilonMeters[level] = v;
                level++;
                pos = next + 1;
            }
            // Also set levels 9-11 to same as level 8
            for (int l = level; l <= 11; l++) kAdminEpsilonMeters[l] = kAdminEpsilonMeters[8];
        } else if (arg == "--mode" && i + 1 < argc) {
            std::string mode_str = argv[++i];
            if (mode_str == "full") {
                mode = IndexMode::Full;
            } else if (mode_str == "no-addresses") {
                mode = IndexMode::NoAddresses;
            } else if (mode_str == "admin-only") {
                mode = IndexMode::AdminOnly;
            } else {
                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
                return 1;
            }
        } else {
            input_files.push_back(arg);
        }
    }

    // Set global simplification mode
    kSimplifyMode = simplify_mode;
    kSimplifyEpsilonOverride = simplify_epsilon_override;
    if (simplify_mode == SimplifyMode::ErrorBounded) {
        if (simplify_epsilon_override > 0) {
            std::cerr << "Simplification: error-bounded, fixed " << simplify_epsilon_override << "m epsilon" << std::endl;
        } else {
            std::cerr << "Simplification: error-bounded, scale=" << kEpsilonScale
                      << ", epsilons: L2=" << admin_epsilon_meters(2)
                      << "m L4=" << admin_epsilon_meters(4)
                      << "m L6=" << admin_epsilon_meters(6)
                      << "m L8=" << admin_epsilon_meters(8) << "m" << std::endl;
        }
    }

    ParsedData data;
    auto _pt = std::chrono::steady_clock::now();
    auto _cpu = CpuTicks::now();

    if (!load_cache_path.empty()) {
        // Load from cache
        if (!deserialize_cache(data, load_cache_path)) {
            std::cerr << "Error: failed to load cache" << std::endl;
            return 1;
        }
    } else {
        // Parse PBF files (always collect everything)
        if (input_files.empty()) {
            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
            return 1;
        }

        // Create thread pool for concurrent admin polygon S2 covering
        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() - 1);
        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
        AdminCoverPool admin_pool(num_threads);
        // BuildHandler no longer used — parallel processing handles everything

        // Dense array node location index — lockless parallel writes
        // Planet OSM node IDs max ~12.5 billion. 8 bytes per entry = 100GB virtual.
        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
        static const size_t MAX_NODE_ID = MAX_NODE_ID_DEFAULT;
        struct PackedLocation {
            int32_t lat_e7;  // latitude * 10^7
            int32_t lon_e7;  // longitude * 10^7
            bool valid() const { return lat_e7 != 0 || lon_e7 != 0; }
            double lat() const { return lat_e7 / 10000000.0; }
            double lon() const { return lon_e7 / 10000000.0; }
        };
        struct DenseIndex {
            PackedLocation* data;
            size_t capacity;

            DenseIndex() : capacity(MAX_NODE_ID) {
                size_t byte_size = capacity * sizeof(PackedLocation);
                void* ptr = mmap(nullptr, byte_size,
                    PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
                    -1, 0);
                if (ptr == MAP_FAILED) {
                    std::cerr << "Error: failed to mmap dense node index ("
                              << (byte_size / (1024*1024*1024)) << "GB)" << std::endl;
                    std::exit(1);
                }
                madvise(ptr, byte_size, MADV_HUGEPAGE);
                data = static_cast<PackedLocation*>(ptr);
                std::cerr << "Allocated dense node index: " << (byte_size / (1024*1024*1024))
                          << "GB virtual address space" << std::endl;
            }

            ~DenseIndex() {
                munmap(data, capacity * sizeof(PackedLocation));
            }

            // Lockless — each node ID maps to a unique array slot
            void set(uint64_t id, double lat, double lng) {
                if (id >= capacity) return;
                data[id] = {static_cast<int32_t>(lat * 10000000.0 + (lat >= 0 ? 0.5 : -0.5)),
                            static_cast<int32_t>(lng * 10000000.0 + (lng >= 0 ? 0.5 : -0.5))};
            }

            PackedLocation get(uint64_t id) const {
                if (id >= capacity) return {0, 0};
                return data[id];
            }
        } index;

        for (const auto& input_file : input_files) {
            std::cerr << "Processing " << input_file << "..." << std::endl;

            // --- Pass 1: collect relation members for parallel admin assembly ---
            std::cerr << "  Pass 1: scanning relations..." << std::endl;
            PbfFile pbf(input_file, num_threads);

            {
                std::mutex rel_mutex;
                pbf.read_blocks([&](PbfBlock& block, unsigned) {
                    for (auto& rel : block.relations) {
                        const char* boundary = rel.tag("boundary");
                        if (!boundary) continue;

                        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
                        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
                        if (!is_admin && !is_postal) continue;

                        uint8_t admin_level = 0;
                        if (is_admin) {
                            const char* level_str = rel.tag("admin_level");
                            if (!level_str) continue;
                            admin_level = static_cast<uint8_t>(std::atoi(level_str));
                            if (admin_level < 2 || admin_level > 10) continue;
                        } else {
                            admin_level = 11;
                        }
                        if (kMaxAdminLevel > 0 && admin_level > kMaxAdminLevel) continue;

                        const char* name = rel.tag("name");
                        if (!name && is_admin) continue;

                        std::string name_str;
                        if (is_postal) {
                            const char* postal_code = rel.tag("postal_code");
                            if (!postal_code) postal_code = name;
                            if (!postal_code) continue;
                            name_str = postal_code;
                        } else {
                            name_str = name;
                        }

                        std::string country_code;
                        if (admin_level == 2) {
                            const char* cc = rel.tag("ISO3166-1:alpha2");
                            if (cc) country_code = cc;
                        }

                        CollectedRelation cr;
                        cr.id = rel.id;
                        cr.admin_level = admin_level;
                        cr.name = std::move(name_str);
                        cr.country_code = std::move(country_code);
                        cr.is_postal = is_postal;

                        for (size_t mi = 0; mi < rel.members.size(); mi++) {
                            if (rel.members[mi].type == 'w') {
                                cr.members.emplace_back(rel.members[mi].ref, rel.member_role(mi));
                            }
                        }

                        if (!cr.members.empty()) {
                            std::lock_guard<std::mutex> lock(rel_mutex);
                            data.collected_relations.push_back(std::move(cr));
                        }
                    }
                }, "r");
            }
            std::cerr << "  Collected " << data.collected_relations.size()
                      << " admin/postal relations for parallel assembly." << std::endl;
            log_phase("Pass 1: relation scanning", _pt, _cpu);

            // Build admin_way_ids as a bitset for O(1) lookup
            // Max way ID ~1.3B → ~162 MiB bitset (much faster than unordered_set)
            static constexpr size_t MAX_WAY_ID = 2000000000ULL; // 2B, ~250 MiB
            std::vector<uint8_t> admin_way_bits(MAX_WAY_ID / 8 + 1, 0);
            size_t admin_way_count = 0;
            for (const auto& rel : data.collected_relations) {
                for (const auto& [way_id, role] : rel.members) {
                    if (way_id > 0 && static_cast<size_t>(way_id) < MAX_WAY_ID) {
                        admin_way_bits[way_id / 8] |= (1 << (way_id % 8));
                        admin_way_count++;
                    }
                }
            }
            auto is_admin_way = [&](int64_t id) -> bool {
                if (id <= 0 || static_cast<size_t>(id) >= MAX_WAY_ID) return false;
                return admin_way_bits[id / 8] & (1 << (id % 8));
            };
            std::cerr << "  Admin assembly needs " << admin_way_count << " way geometries." << std::endl;

            // --- Pass 2: Node processing (streaming — no PbfNode objects) ---
            std::cerr << "  Pass 2: processing nodes with " << num_threads << " threads..." << std::endl;
            {
                struct NodeThreadLocal {
                    std::vector<std::pair<double,double>> addr_coords;
                    std::vector<std::pair<std::string,std::string>> addr_strings;
                    uint64_t count = 0;
                };
                // Use thread_local for streaming callback (no thread index available)
                static thread_local NodeThreadLocal* tl_node_data = nullptr;
                std::vector<NodeThreadLocal> ntld(num_threads);
                std::atomic<unsigned> next_tl{0};

                pbf.read_nodes_streaming([&](int64_t id, double lat, double lng,
                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
                    const std::vector<std::string>& st) {

                    // Lazy init thread-local pointer
                    if (!tl_node_data) {
                        unsigned idx = next_tl.fetch_add(1);
                        tl_node_data = &ntld[idx % ntld.size()];
                    }

                    if (id > 0) {
                        index.set(static_cast<uint64_t>(id), lat, lng);
                    }

                    // Check for address tags (fast — no string copies for tagless nodes)
                    if (ntags > 0) {
                        const char* housenumber = nullptr;
                        const char* street = nullptr;
                        for (size_t i = 0; i < ntags; i++) {
                            if (tag_keys[i] < st.size()) {
                                if (st[tag_keys[i]] == "addr:housenumber")
                                    housenumber = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                                else if (st[tag_keys[i]] == "addr:street")
                                    street = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                            }
                        }
                        if (housenumber && street) {
                            tl_node_data->addr_coords.push_back({lat, lng});
                            tl_node_data->addr_strings.push_back({housenumber, street});
                            tl_node_data->count++;
                        }
                    }
                });

                // Reset thread_local for next use
                tl_node_data = nullptr;

                // Merge address points
                uint64_t total_addrs = 0;
                for (auto& local : ntld) {
                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
                        uint64_t dummy = 0;
                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
                                       local.addr_strings[j].first.c_str(),
                                       local.addr_strings[j].second.c_str(), dummy);
                    }
                    total_addrs += local.count;
                }
                std::cerr << "  Node processing complete: " << total_addrs
                          << " address points collected." << std::endl;
            }
            log_phase("Pass 2: node processing", _pt, _cpu);
            pbf.release_pages(); // free PBF mmap pages, will re-fault for way pass

            // --- Pass 2b: Way processing (fully parallel) ---
            std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
            {
                // Thread-local data for parallel way processing
                struct ThreadLocalData {
                    std::vector<WayHeader> ways;
                    std::vector<NodeCoord> street_nodes;
                    std::vector<DeferredWay> deferred_ways;
                    std::vector<InterpWay> interp_ways;
                    std::vector<NodeCoord> interp_nodes;
                    std::vector<DeferredInterp> deferred_interps;
                    std::vector<AddrPoint> building_addrs;
                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
                    std::vector<std::string> way_strings;      // way name strings
                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
                    std::vector<std::string> interp_strings;   // interp street name strings
                    uint64_t way_count = 0;
                    uint64_t building_addr_count = 0;
                    uint64_t interp_count = 0;
                    // Way geometries for parallel admin assembly
                    struct WayGeomEntry {
                        int64_t way_id;
                        std::vector<std::pair<double,double>> coords;
                        int64_t first_node_id;
                        int64_t last_node_id;
                    };
                    std::vector<WayGeomEntry> way_geoms;
                    // Closed-way admin polygons (ways with boundary=administrative)
                    struct ClosedWayAdmin {
                        std::vector<std::pair<double,double>> vertices;
                        std::string name;
                        uint8_t admin_level;
                        std::string country_code;
                    };
                    std::vector<ClosedWayAdmin> closed_way_admins;
                };

                static thread_local ThreadLocalData* tl_way_data = nullptr;
                std::vector<ThreadLocalData> tld(num_threads);
                std::atomic<unsigned> next_tl_way{0};

                pbf.read_ways_streaming([&](int64_t way_id,
                    const int64_t* refs_data, size_t refs_size,
                    const uint32_t* tag_keys, const uint32_t* tag_vals, size_t ntags,
                    const std::vector<std::string>& st) {

                    if (!tl_way_data) {
                        unsigned idx = next_tl_way.fetch_add(1);
                        tl_way_data = &tld[idx % tld.size()];
                    }
                    auto& local = *tl_way_data;

                    // Single-pass tag extraction — avoid repeated linear scans
                    const char* t_interpolation = nullptr;
                    const char* t_housenumber = nullptr;
                    const char* t_street = nullptr;
                    const char* t_highway = nullptr;
                    const char* t_name = nullptr;
                    const char* t_boundary = nullptr;
                    const char* t_admin_level = nullptr;
                    const char* t_postal_code = nullptr;
                    const char* t_iso = nullptr;
                    for (size_t i = 0; i < ntags; i++) {
                        if (tag_keys[i] >= st.size()) continue;
                        const auto& k = st[tag_keys[i]];
                        const char* v = tag_vals[i] < st.size() ? st[tag_vals[i]].c_str() : nullptr;
                        // Fast dispatch by first character to avoid 9 string comparisons
                        switch (k.size() > 0 ? k[0] : 0) {
                            case 'a':
                                if (k == "addr:interpolation") t_interpolation = v;
                                else if (k == "addr:housenumber") t_housenumber = v;
                                else if (k == "addr:street") t_street = v;
                                else if (k == "admin_level") t_admin_level = v;
                                break;
                            case 'h': if (k == "highway") t_highway = v; break;
                            case 'n': if (k == "name") t_name = v; break;
                            case 'b': if (k == "boundary") t_boundary = v; break;
                            case 'p': if (k == "postal_code") t_postal_code = v; break;
                            case 'I': if (k == "ISO3166-1:alpha2") t_iso = v; break;
                        }
                    }

                    // Early exit: if no relevant tags, skip expensive node resolution
                    bool need_nodes = t_interpolation || t_housenumber ||
                        (t_highway && is_included_highway(t_highway) && t_name) ||
                        t_boundary ||
                        (refs_size > 0 && is_admin_way(way_id));
                    if (!need_nodes) return;

                    // Pre-resolve all node locations
                    thread_local std::vector<PackedLocation> resolved_locs;
                    resolved_locs.clear();
                    resolved_locs.reserve(refs_size);
                    bool all_valid = true;
                    for (size_t ri = 0; ri < refs_size; ri++) {
                        auto loc = index.get(static_cast<uint64_t>(refs_data[ri]));
                        resolved_locs.push_back(loc);
                        if (!loc.valid()) all_valid = false;
                    }

                    // Address interpolation
                    if (t_interpolation) {
                        if (refs_size >= 2 && all_valid) {
                            const char* street = t_street;
                            if (street) {
                                uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
                                uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
                                for (const auto& loc : resolved_locs)
                                    local.interp_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
                                uint8_t interp_type = 0;
                                if (std::strcmp(t_interpolation, "even") == 0) interp_type = 1;
                                else if (std::strcmp(t_interpolation, "odd") == 0) interp_type = 2;
                                InterpWay iw{}; iw.node_offset = node_offset;
                                iw.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
                                iw.interpolation = interp_type;
                                local.interp_ways.push_back(iw);
                                local.interp_strings.push_back(street);
                                local.deferred_interps.push_back({interp_id, node_offset, iw.node_count});
                                local.interp_count++;
                            }
                        }
                        return;
                    }

                    // Building addresses
                    const char* housenumber = t_housenumber;
                    if (housenumber) {
                        const char* street = t_street;
                        if (street && refs_size > 0) {
                            double sum_lat = 0, sum_lng = 0; int valid = 0;
                            for (const auto& loc : resolved_locs) {
                                if (loc.valid()) { sum_lat += loc.lat(); sum_lng += loc.lon(); valid++; }
                            }
                            if (valid > 0) {
                                local.building_addr_coords.push_back({sum_lat/valid, sum_lng/valid});
                                local.building_addrs.push_back({static_cast<float>(sum_lat/valid), static_cast<float>(sum_lng/valid), 0, 0});
                                local.addr_strings.push_back({housenumber, street});
                                local.building_addr_count++;
                            }
                        }
                    }

                    // Highway ways
                    if (t_highway && is_included_highway(t_highway)) {
                        if (t_name && refs_size >= 2 && all_valid) {
                            uint32_t wid = static_cast<uint32_t>(local.ways.size());
                            uint32_t noff = static_cast<uint32_t>(local.street_nodes.size());
                            for (const auto& loc : resolved_locs)
                                local.street_nodes.push_back({static_cast<float>(loc.lat()), static_cast<float>(loc.lon())});
                            WayHeader header{}; header.node_offset = noff;
                            header.node_count = static_cast<uint8_t>(std::min(refs_size, size_t(255)));
                            local.ways.push_back(header);
                            local.way_strings.push_back(t_name);
                            local.deferred_ways.push_back({wid, noff, header.node_count});
                            local.way_count++;
                        }
                    }

                    // Admin boundary member ways
                    if (refs_size > 0 && is_admin_way(way_id)) {
                        std::vector<std::pair<double,double>> geom;
                        for (const auto& loc : resolved_locs)
                            if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
                        if (!geom.empty())
                            local.way_geoms.push_back({way_id, std::move(geom), refs_data[0], refs_data[refs_size-1]});
                    }

                    // Closed way admin boundaries
                    const char* boundary = t_boundary;
                    if (boundary) {
                        bool is_admin = (std::strcmp(boundary, "administrative") == 0);
                        bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
                        if ((is_admin || is_postal) && refs_size >= 4 && refs_data[0] == refs_data[refs_size-1]) {
                            uint8_t al = 0;
                            if (is_admin) { const char* ls = t_admin_level; if (ls) al = static_cast<uint8_t>(std::atoi(ls)); }
                            else al = 11;
                            int max_al = is_postal ? 11 : 10;
                            if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
                                const char* aname = t_name;
                                if (aname || !is_admin) {
                                    std::string name_str;
                                    if (is_postal) { const char* pc = t_postal_code; if (!pc) pc = aname; if (pc) name_str = pc; }
                                    else if (aname) name_str = aname;
                                    if (!name_str.empty() && all_valid && resolved_locs.size() >= 3) {
                                        std::vector<std::pair<double,double>> verts;
                                        for (const auto& loc : resolved_locs) verts.push_back({loc.lat(), loc.lon()});
                                        std::string cc; if (al == 2) { const char* iso = t_iso; if (iso) cc = iso; }
                                        local.closed_way_admins.push_back({std::move(verts), std::move(name_str), al, std::move(cc)});
                                    }
                                }
                            }
                        }
                    }
                });
                tl_way_data = nullptr;
                std::cerr << "  Parallel way processing complete." << std::endl;

                // Process areas/multipolygons — sequential fallback path
                // Merge thread-local way/interp data into main ParsedData
                std::cerr << "  Merging thread-local data..." << std::endl;
                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
                for (auto& local : tld) {
                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());

                    // Merge street ways
                    for (size_t i = 0; i < local.ways.size(); i++) {
                        auto h = local.ways[i];
                        h.node_offset += node_base;
                        h.name_id = data.string_pool.intern(local.way_strings[i]);
                        data.ways.push_back(h);
                    }
                    data.street_nodes.insert(data.street_nodes.end(),
                        local.street_nodes.begin(), local.street_nodes.end());

                    // Remap deferred ways
                    for (auto dw : local.deferred_ways) {
                        dw.way_id += way_base;
                        dw.node_offset += node_base;
                        data.deferred_ways.push_back(dw);
                    }

                    // Merge building addresses
                    for (size_t i = 0; i < local.building_addrs.size(); i++) {
                        uint64_t dummy = 0;
                        add_addr_point(data, local.building_addrs[i].lat, local.building_addrs[i].lng,
                                       local.addr_strings[i].first.c_str(),
                                       local.addr_strings[i].second.c_str(), dummy);
                    }

                    // Merge interpolation ways
                    for (size_t i = 0; i < local.interp_ways.size(); i++) {
                        auto iw = local.interp_ways[i];
                        iw.node_offset += interp_node_base;
                        iw.street_id = data.string_pool.intern(local.interp_strings[i]);
                        data.interp_ways.push_back(iw);
                    }
                    data.interp_nodes.insert(data.interp_nodes.end(),
                        local.interp_nodes.begin(), local.interp_nodes.end());

                    // Remap deferred interps
                    for (auto di : local.deferred_interps) {
                        di.interp_id += interp_base;
                        di.node_offset += interp_node_base;
                        data.deferred_interps.push_back(di);
                    }

                    total_ways += local.way_count;
                    total_building_addrs += local.building_addr_count;
                    total_interps += local.interp_count;

                    // Merge way geometries for admin assembly
                    {
                        for (auto& wg : local.way_geoms) {
                            ParsedData::WayGeometry g;
                            g.coords = std::move(wg.coords);
                            g.first_node_id = wg.first_node_id;
                            g.last_node_id = wg.last_node_id;
                            data.way_geometries[wg.way_id] = std::move(g);
                        }
                        local.way_geoms.clear();
                        local.way_geoms.shrink_to_fit();
                    }
                }
                std::cerr << "  Merged: " << total_ways << " ways, "
                          << total_building_addrs << " building addrs, "
                          << total_interps << " interps from parallel processing." << std::endl;

                // --- Merge closed-way admin polygons ---
                {
                    uint64_t closed_way_admin_count = 0;
                    for (auto& local : tld) {
                        for (auto& cwa : local.closed_way_admins) {
                            const char* cc = cwa.country_code.empty() ? nullptr : cwa.country_code.c_str();
                            add_admin_polygon(data, cwa.vertices, cwa.name.c_str(),
                                              cwa.admin_level, cc, &admin_pool);
                            closed_way_admin_count++;
                        }
                        local.closed_way_admins.clear();
                    }
                    if (closed_way_admin_count > 0) {
                        std::cerr << "  Added " << closed_way_admin_count
                                  << " admin polygons from closed ways." << std::endl;
                    }
                }

                // --- Parallel admin boundary assembly ---
                {
                    log_phase("Pass 2b: way processing", _pt, _cpu);
                    pbf.unmap(); // release 86 GiB PBF mmap before admin/S2 phases
                    std::cerr << "  Assembling admin polygons in parallel ("
                              << data.collected_relations.size() << " relations, "
                              << data.way_geometries.size() << " way geometries)..." << std::endl;

                    // Thread-local admin results (to avoid locking add_admin_polygon)
                    struct AdminResult {
                        std::vector<std::pair<double,double>> vertices;
                        std::string name;
                        uint8_t admin_level;
                        std::string country_code;
                    };

                    std::vector<std::vector<AdminResult>> thread_admin_results(num_threads);
                    std::atomic<size_t> rel_idx{0};
                    std::atomic<uint64_t> assembled_count{0};

                    std::vector<std::thread> admin_workers;
                    for (unsigned int t = 0; t < num_threads; t++) {
                        admin_workers.emplace_back([&, t]() {
                            auto& local_results = thread_admin_results[t];
                            while (true) {
                                size_t i = rel_idx.fetch_add(1);
                                if (i >= data.collected_relations.size()) break;

                                const auto& rel = data.collected_relations[i];

                                // Skip level 2 border-line relations — these have names like
                                // "Deutschland - Österreich" (two countries separated by
                                // " - " or " — "). They are boundary lines, not country
                                // polygons, and produce self-intersecting geometry.
                                if (rel.admin_level == 2 && rel.country_code.empty() &&
                                    (rel.name.find(" - ") != std::string::npos ||
                                     rel.name.find(" \xe2\x80\x94 ") != std::string::npos)) { // " — " (em dash)
                                    assembled_count.fetch_add(1);
                                    continue;
                                }

                                // Skip relations with missing member ways —
                                // these cross the extract boundary and would produce
                                // incorrect partial polygons
                                bool has_missing = false;
                                for (const auto& [way_id, role] : rel.members) {
                                    if (data.way_geometries.find(way_id) == data.way_geometries.end()) {
                                        has_missing = true;
                                        break;
                                    }
                                }
                                if (has_missing) {
                                    assembled_count.fetch_add(1);
                                    continue;
                                }

                                auto rings = assemble_outer_rings(rel.members, data.way_geometries);
                                // If outer-only assembly failed, retry with all ways.
                                // Osmium ignores roles during assembly (check_roles=false) —
                                // inner ways sometimes form the boundary or bridge gaps.
                                // Filter retry results: discard rings with duplicate coords
                                // (figure-8 shapes from merging holes with outer boundary).
                                if (rings.empty()) {
                                    auto retry = assemble_outer_rings(rel.members, data.way_geometries, true);
                                    for (auto& ring : retry) {
                                        if (!ring_has_duplicate_coords(ring)) {
                                            rings.push_back(std::move(ring));
                                        }
                                    }
                                }

                                // Diagnostic: log relations with 0 rings produced
                                if (rings.empty() && !rel.members.empty()) {
                                    int total_ways = 0, found = 0, missing = 0;
                                    for (const auto& [way_id, role] : rel.members) {
                                        total_ways++;
                                        auto it = data.way_geometries.find(way_id);
                                        if (it != data.way_geometries.end() && !it->second.coords.empty()) {
                                            found++;
                                        } else {
                                            missing++;
                                        }
                                    }
                                    if (missing == 0 && found > 0) {
                                        std::string detail = "  DEBUG '" + rel.name + "': ways:";
                                        for (const auto& [way_id, role] : rel.members) {
                                            auto it = data.way_geometries.find(way_id);
                                            if (it == data.way_geometries.end()) continue;
                                            detail += " [w" + std::to_string(way_id) +
                                                      " first=" + std::to_string(it->second.first_node_id) +
                                                      " last=" + std::to_string(it->second.last_node_id) +
                                                      " n=" + std::to_string(it->second.coords.size()) + "]";
                                        }
                                        std::cerr << detail << std::endl;
                                    }
                                    std::cerr << "  WARN: relation '" << rel.name
                                              << "' (level " << (int)rel.admin_level
                                              << ", " << rel.members.size() << " members): "
                                              << "0 rings from " << total_ways << " ways ("
                                              << found << " found, " << missing << " missing)" << std::endl;
                                }

                                for (auto& ring : rings) {
                                    if (ring.size() >= 3) {
                                        AdminResult ar;
                                        ar.vertices = std::move(ring);
                                        ar.name = rel.name;
                                        ar.admin_level = rel.admin_level;
                                        ar.country_code = rel.country_code;
                                        local_results.push_back(std::move(ar));
                                    }
                                }

                                uint64_t done = assembled_count.fetch_add(1) + 1;
                                if (done % 10000 == 0) {
                                    std::cerr << "  Assembled " << done / 1000
                                              << "K admin relations..." << std::endl;
                                }
                            }
                        });
                    }
                    for (auto& w : admin_workers) w.join();
                    log_phase("    Admin: ring assembly", _pt, _cpu);

                    // Flatten all admin results into a single vector for parallel processing
                    std::vector<AdminResult> all_admin_results;
                    for (auto& local_results : thread_admin_results) {
                        all_admin_results.insert(all_admin_results.end(),
                            std::make_move_iterator(local_results.begin()),
                            std::make_move_iterator(local_results.end()));
                        local_results.clear();
                    }
                    uint64_t total_admin_rings = all_admin_results.size();

                    // Parallel simplification: the expensive work (normalize, simplify,
                    // compute area) is done per-polygon with no shared state.
                    struct PreparedPolygon {
                        std::vector<std::pair<double,double>> simplified;
                        std::string name;
                        uint8_t admin_level;
                        std::string country_code;
                        float area;
                    };
                    std::vector<PreparedPolygon> prepared(total_admin_rings);
                    {
                        std::atomic<size_t> prep_idx{0};
                        std::vector<std::thread> prep_workers;
                        for (unsigned int t = 0; t < num_threads; t++) {
                            prep_workers.emplace_back([&]() {
                                while (true) {
                                    size_t i = prep_idx.fetch_add(1);
                                    if (i >= total_admin_rings) break;
                                    auto& ar = all_admin_results[i];
                                    auto& pp = prepared[i];

                                    // Normalize ring rotation
                                    auto& vertices = ar.vertices;
                                    if (vertices.size() >= 4 &&
                                        std::fabs(vertices.front().first - vertices.back().first) < 1e-7 &&
                                        std::fabs(vertices.front().second - vertices.back().second) < 1e-7) {
                                        vertices.pop_back();
                                        auto min_it = std::min_element(vertices.begin(), vertices.end());
                                        std::rotate(vertices.begin(), min_it, vertices.end());
                                        vertices.push_back(vertices.front());
                                    }

                                    pp.simplified = simplify_admin_polygon(vertices, ar.admin_level);
                                    if (pp.simplified.size() >= 3) {
                                        pp.area = polygon_area(pp.simplified);
                                    }
                                    pp.name = std::move(ar.name);
                                    pp.admin_level = ar.admin_level;
                                    pp.country_code = std::move(ar.country_code);
                                }
                            });
                        }
                        for (auto& w : prep_workers) w.join();
                    }
                    all_admin_results.clear();
                    log_phase("    Admin: parallel simplify", _pt, _cpu);

                    // Sequential append + submit to S2 pool (cheap: just vector push + string intern)
                    for (auto& pp : prepared) {
                        if (pp.simplified.size() < 3) continue;

                        uint32_t poly_id = static_cast<uint32_t>(data.admin_polygons.size());
                        uint32_t vertex_offset = static_cast<uint32_t>(data.admin_vertices.size());

                        for (const auto& [lat, lng] : pp.simplified) {
                            data.admin_vertices.push_back({static_cast<float>(lat), static_cast<float>(lng)});
                        }

                        AdminPolygon poly{};
                        poly.vertex_offset = vertex_offset;
                        poly.vertex_count = static_cast<uint32_t>(pp.simplified.size());
                        poly.name_id = data.string_pool.intern(pp.name);
                        poly.admin_level = pp.admin_level;
                        poly.area = pp.area;
                        const char* cc = pp.country_code.empty() ? nullptr : pp.country_code.c_str();
                        poly.country_code = (cc && cc[0] && cc[1])
                            ? static_cast<uint16_t>((cc[0] << 8) | cc[1]) : 0;
                        data.admin_polygons.push_back(poly);

                        admin_pool.submit(poly_id, std::move(pp.simplified));
                    }

                    std::cerr << "  Parallel admin assembly complete: "
                              << total_admin_rings << " polygon rings from "
                              << data.collected_relations.size() << " relations." << std::endl;
                    log_phase("    Admin: append + submit S2", _pt, _cpu);

                    // Free collected data
                    data.collected_relations.clear();
                    data.collected_relations.shrink_to_fit();
                    data.way_geometries.clear();
                    std::unordered_map<int64_t, ParsedData::WayGeometry>().swap(data.way_geometries);
                }
            }
        }

        std::cerr << "Done reading:" << std::endl;
        std::cerr << "  " << data.ways.size() << " street ways" << std::endl;
        std::cerr << "  " << data.addr_points.size() << " address points" << std::endl;
        std::cerr << "  " << data.interp_ways.size() << " interpolation ways" << std::endl;
        std::cerr << "  " << data.admin_polygons.size() << " admin polygon rings" << std::endl;

        // Drain admin polygon thread pool (may still be processing)
        std::cerr << "Waiting for admin polygon S2 covering to complete..." << std::endl;
        auto admin_results = admin_pool.drain();
        log_phase("    Admin: S2 covering drain", _pt, _cpu);
        for (auto& [cell_id, ids] : admin_results) {
            auto& target = data.cell_to_admin[cell_id];
            target.insert(target.end(), ids.begin(), ids.end());
        }
        std::cerr << "Admin polygon S2 covering complete (" << data.cell_to_admin.size() << " cells)." << std::endl;

        // Sort admin polygons largest-first (by vertex count descending).
        // This ensures the work-stealing loops in S2 computation and quality
        // variant simplification process expensive polygons first, avoiding
        // stragglers at the end.
        {
            size_t n = data.admin_polygons.size();
            std::vector<uint32_t> order(n);
            std::iota(order.begin(), order.end(), 0u);
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                return data.admin_polygons[a].vertex_count > data.admin_polygons[b].vertex_count;
            });

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

            // Reorder polygons and vertices
            std::vector<AdminPolygon> new_polys(n);
            std::vector<NodeCoord> new_verts;
            new_verts.reserve(data.admin_vertices.size());
            for (uint32_t new_id = 0; new_id < n; new_id++) {
                auto p = data.admin_polygons[order[new_id]];
                uint32_t old_offset = p.vertex_offset;
                p.vertex_offset = static_cast<uint32_t>(new_verts.size());
                new_polys[new_id] = p;
                for (uint32_t v = 0; v < p.vertex_count; v++)
                    new_verts.push_back(data.admin_vertices[old_offset + v]);
            }
            data.admin_polygons = std::move(new_polys);
            data.admin_vertices = std::move(new_verts);

            // Remap poly_ids in cell_to_admin (preserving INTERIOR_FLAG)
            for (auto& [cell_id, ids] : data.cell_to_admin) {
                for (auto& id : ids) {
                    uint32_t flags = id & INTERIOR_FLAG;
                    uint32_t old_id = id & ID_MASK;
                    id = old_to_new[old_id] | flags;
                }
            }
            std::cerr << "Sorted admin polygons largest-first (" << n << " polygons)." << std::endl;
        }

        // --- Parallel S2 cell computation for ways and interpolation ---
        {
            log_phase("Admin assembly", _pt, _cpu);
            std::cerr << "Computing S2 cells for ways with " << num_threads << " threads..." << std::endl;

            // Flat-array approach: threads emit (cell_id, item_id) pairs into
            // thread-local vectors, then we sort globally and group by cell_id.
            // This avoids hash maps entirely — sort is cache-friendly O(n log n).

            struct CellItem { uint64_t cell_id; uint32_t item_id; };

            auto _s2t = std::chrono::steady_clock::now();
            auto _s2cpu = CpuTicks::now();

            // Process streets: emit (cell_id, way_id) pairs
            std::cerr << "  Processing " << data.deferred_ways.size() << " street ways..." << std::endl;
            std::vector<std::vector<CellItem>> way_pairs(num_threads);
            {
                std::atomic<size_t> way_idx{0};
                std::vector<std::thread> threads;
                for (unsigned int t = 0; t < num_threads; t++) {
                    threads.emplace_back([&, t]() {
                        auto& local = way_pairs[t];
                        local.reserve(data.deferred_ways.size() / num_threads * 3);
                        std::vector<S2CellId> edge_cells;
                        std::vector<uint64_t> way_cells;
                        while (true) {
                            size_t i = way_idx.fetch_add(1);
                            if (i >= data.deferred_ways.size()) break;
                            const auto& dw = data.deferred_ways[i];
                            way_cells.clear();
                            for (uint8_t j = 0; j + 1 < dw.node_count; j++) {
                                const auto& n1 = data.street_nodes[dw.node_offset + j];
                                const auto& n2 = data.street_nodes[dw.node_offset + j + 1];
                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
                            }
                            std::sort(way_cells.begin(), way_cells.end());
                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
                            for (uint64_t cell_id : way_cells) {
                                local.push_back({cell_id, dw.way_id});
                            }
                        }
                    });
                }
                for (auto& t : threads) t.join();
            }
            log_phase("  S2: street ways (parallel)", _s2t, _s2cpu);

            // Process interpolations: emit (cell_id, interp_id) pairs
            std::cerr << "  Processing " << data.deferred_interps.size() << " interpolation ways..." << std::endl;
            std::vector<std::vector<CellItem>> interp_pairs(num_threads);
            {
                std::atomic<size_t> interp_idx{0};
                std::vector<std::thread> threads;
                for (unsigned int t = 0; t < num_threads; t++) {
                    threads.emplace_back([&, t]() {
                        auto& local = interp_pairs[t];
                        std::vector<S2CellId> edge_cells;
                        std::vector<uint64_t> way_cells;
                        while (true) {
                            size_t i = interp_idx.fetch_add(1);
                            if (i >= data.deferred_interps.size()) break;
                            const auto& di = data.deferred_interps[i];
                            way_cells.clear();
                            for (uint8_t j = 0; j + 1 < di.node_count; j++) {
                                const auto& n1 = data.interp_nodes[di.node_offset + j];
                                const auto& n2 = data.interp_nodes[di.node_offset + j + 1];
                                cover_edge(n1.lat, n1.lng, n2.lat, n2.lng, edge_cells);
                                for (const auto& c : edge_cells) way_cells.push_back(c.id());
                            }
                            std::sort(way_cells.begin(), way_cells.end());
                            way_cells.erase(std::unique(way_cells.begin(), way_cells.end()), way_cells.end());
                            for (uint64_t cell_id : way_cells) {
                                local.push_back({cell_id, di.interp_id});
                            }
                        }
                    });
                }
                for (auto& t : threads) t.join();
            }
            log_phase("  S2: interp ways (parallel)", _s2t, _s2cpu);

            // Merge thread-local pairs into single vectors, sort, build cell maps
            std::cerr << "  Sorting and grouping cell pairs..." << std::endl;
            // Concatenate + sort helper for flat CellItem pairs
            // Parallel sort each thread's pairs, then k-way merge directly into
            // hash map + sorted output. No intermediate merged vector needed.
            auto parallel_sort_and_build = [](
                std::vector<std::vector<CellItem>>& thread_pairs,
                std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map,
                std::vector<CellItemPair>& sorted_out
            ) {
                // Step 1: Convert + sort each thread's data in parallel
                size_t total = 0;
                for (auto& v : thread_pairs) total += v.size();

                std::vector<std::vector<CellItemPair>> chunks(thread_pairs.size());
                {
                    std::vector<std::thread> sort_threads;
                    for (size_t t = 0; t < thread_pairs.size(); t++) {
                        sort_threads.emplace_back([&, t]() {
                            auto& src = thread_pairs[t];
                            auto& dst = chunks[t];
                            dst.reserve(src.size());
                            for (auto& ci : src) dst.push_back({ci.cell_id, ci.item_id});
                            src.clear(); src.shrink_to_fit();
                            std::sort(dst.begin(), dst.end(), [](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);
                            });
                        });
                    }
                    for (auto& t : sort_threads) t.join();
                }

                // Step 2: Concatenate sorted chunks and merge using parallel tree.
                // Each chunk is already sorted. Record run boundaries, concatenate,
                // then merge adjacent runs pairwise in parallel at each level.
                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);
                };

                // Record run boundaries before concatenation
                std::vector<size_t> run_bounds = {0};
                sorted_out.clear();
                sorted_out.reserve(total);
                for (auto& chunk : chunks) {
                    sorted_out.insert(sorted_out.end(), chunk.begin(), chunk.end());
                    run_bounds.push_back(sorted_out.size());
                    chunk.clear(); chunk.shrink_to_fit();
                }

                // Parallel tree merge: at each level, merge adjacent run pairs in parallel.
                // Level 0: merge runs (0,1), (2,3), ... → N/2 runs
                // Level 1: merge runs (01,23), (45,67), ... → N/4 runs
                // ... until one sorted run remains.
                while (run_bounds.size() > 2) {
                    std::vector<size_t> new_bounds = {0};
                    std::vector<std::thread> merge_threads;
                    for (size_t i = 0; i + 2 < run_bounds.size(); i += 2) {
                        size_t left = run_bounds[i];
                        size_t mid = run_bounds[i + 1];
                        size_t right = run_bounds[i + 2];
                        merge_threads.emplace_back([&, left, mid, right] {
                            std::inplace_merge(sorted_out.begin() + left,
                                               sorted_out.begin() + mid,
                                               sorted_out.begin() + right, cmp);
                        });
                        new_bounds.push_back(right);
                    }
                    // If odd number of runs, carry the last one forward
                    if (run_bounds.size() % 2 == 0) {
                        new_bounds.push_back(run_bounds.back());
                    }
                    for (auto& t : merge_threads) t.join();
                    run_bounds = std::move(new_bounds);
                }

                // Skip building hash map — sorted_out is used directly for writing.
                // cell_map stays empty (only needed for cache/continent modes).
            };

            // Sort each thread's pairs in parallel, then k-way merge directly
            // into hash map + sorted output. No intermediate merged vector.
            auto f_ways = std::async(std::launch::async, [&] {
                parallel_sort_and_build(way_pairs, data.cell_to_ways, data.sorted_way_cells);
            });
            auto f_interps = std::async(std::launch::async, [&] {
                parallel_sort_and_build(interp_pairs, data.cell_to_interps, data.sorted_interp_cells);
            });
            f_ways.get();
            f_interps.get();
            log_phase("  S2: sort + group into cell maps", _s2t, _s2cpu);

            // Free deferred work items
            data.deferred_ways.clear();
            data.deferred_ways.shrink_to_fit();
            data.deferred_interps.clear();
            data.deferred_interps.shrink_to_fit();

            std::cerr << "S2 cell computation complete." << std::endl;
        }

        // Resolve interpolation endpoints
        std::cerr << "Resolving interpolation endpoints..." << std::endl;
        resolve_interpolation_endpoints(data);

        // Deduplicate + convert to sorted pairs for fast writing
        log_phase("S2 cell computation", _pt, _cpu);
        std::cerr << "Deduplicating + sorting for write..." << std::endl;
        {
            // Convert addr hash map to sorted pairs.
            // Sort cell IDs only (30M unique), then build pairs in sorted order.
            // Much faster than sorting all 160M pairs.
            auto f2 = std::async(std::launch::async, [&] {
                auto _dt = std::chrono::steady_clock::now();
                auto _dc = CpuTicks::now();

                // Step 1: Extract cell IDs from hash map
                std::vector<uint64_t> sorted_cells;
                sorted_cells.reserve(data.cell_to_addrs.size());
                for (auto& [cell_id, ids] : data.cell_to_addrs)
                    sorted_cells.push_back(cell_id);
                log_phase("    Dedup: extract cell IDs", _dt, _dc);

                // Step 2: Sort cell IDs
                std::sort(sorted_cells.begin(), sorted_cells.end());
                log_phase("    Dedup: sort cell IDs", _dt, _dc);

                // Step 3: Parallel dedup within each cell
                {
                    unsigned nthreads = std::thread::hardware_concurrency();
                    size_t chunk = (sorted_cells.size() + 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, sorted_cells.size());
                        if (start >= sorted_cells.size()) break;
                        threads.emplace_back([&, start, end]() {
                            for (size_t i = start; i < end; i++) {
                                auto& ids = data.cell_to_addrs[sorted_cells[i]];
                                std::sort(ids.begin(), ids.end());
                                ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
                            }
                        });
                    }
                    for (auto& t : threads) t.join();
                }
                log_phase("    Dedup: per-cell sort+dedup", _dt, _dc);

                // Step 4: Count total pairs
                size_t total_pairs = 0;
                for (auto& [_, ids] : data.cell_to_addrs) total_pairs += ids.size();
                log_phase("    Dedup: count pairs", _dt, _dc);

                // Step 5: Build sorted pairs
                std::vector<CellItemPair> pairs;
                pairs.reserve(total_pairs);
                for (uint64_t cell_id : sorted_cells) {
                    auto& ids = data.cell_to_addrs[cell_id];
                    for (auto id : ids) pairs.push_back({cell_id, id});
                }
                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);
    }

    // --- Deterministic ordering ---
    // Sort all data by canonical keys so that the same PBF input always produces
    // the same binary output, regardless of thread scheduling. This is required
    // for incremental patching — patches between deterministic builds are small
    // because matching records land at the same file offsets.
    {
        std::cerr << "Deterministic ordering..." << std::endl;
        auto _st = std::chrono::steady_clock::now();
        auto _sc = CpuTicks::now();

        // 1. Sort string pool alphabetically, remap all string offsets
        {
            auto& pool_data = data.string_pool.mutable_data();
            if (!pool_data.empty()) {
                // Extract strings
                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;
                }
                std::sort(strings.begin(), strings.end(), [](const auto& a, const auto& b) {
                    return strcmp(a.second, b.second) < 0;
                });

                // Build remap + new pool
                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) {
                    remap[old_off] = static_cast<uint32_t>(new_data.size());
                    size_t len = strlen(str);
                    new_data.insert(new_data.end(), str, str + len + 1);
                }
                pool_data = std::move(new_data);

                // Apply remap to all data records
                const auto& rm = remap;
                for (auto& w : data.ways) w.name_id = rm.at(w.name_id);
                for (auto& a : data.addr_points) {
                    a.housenumber_id = rm.at(a.housenumber_id);
                    a.street_id = rm.at(a.street_id);
                }
                for (auto& iw : data.interp_ways) iw.street_id = rm.at(iw.street_id);
                for (auto& ap : data.admin_polygons) ap.name_id = rm.at(ap.name_id);
                std::cerr << "  String pool sorted: " << strings.size() << " strings" << std::endl;
            }
        }
        log_phase("  Sort strings", _st, _sc);

        // Helper: reinterpret float as uint32 for total ordering (works for IEEE 754)
        auto float_bits = [](float v) -> uint32_t {
            uint32_t bits;
            memcpy(&bits, &v, 4);
            // Handle negative floats: flip all bits if sign bit set, else flip sign bit
            return (bits & 0x80000000) ? ~bits : (bits ^ 0x80000000);
        };

        // 2. Sort addr_points by (street_id, housenumber_id, lat_bits, lng_bits)
        //    Using string offsets directly (already remapped to sorted pool) gives
        //    deterministic order. Raw float bits as final tiebreaker for total order.
        if (!data.sorted_addr_cells.empty()) {
            size_t n = data.addr_points.size();
            std::vector<uint32_t> order(n);
            std::iota(order.begin(), order.end(), 0);
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                const auto& pa = data.addr_points[a];
                const auto& pb = data.addr_points[b];
                // Since strings are sorted alphabetically, comparing offsets
                // gives the same order as strcmp — and it's O(1) not O(n)
                if (pa.street_id != pb.street_id) return pa.street_id < pb.street_id;
                if (pa.housenumber_id != pb.housenumber_id) return pa.housenumber_id < pb.housenumber_id;
                uint32_t la = float_bits(pa.lat), lb = float_bits(pb.lat);
                if (la != lb) return la < lb;
                return float_bits(pa.lng) < float_bits(pb.lng);
            });
            std::vector<uint32_t> old_to_new(n);
            for (uint32_t i = 0; i < n; i++) old_to_new[order[i]] = i;
            std::vector<AddrPoint> sorted(n);
            for (uint32_t i = 0; i < n; i++) sorted[i] = data.addr_points[order[i]];
            data.addr_points = std::move(sorted);
            for (auto& p : data.sorted_addr_cells) p.item_id = old_to_new[p.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_addr_cells.begin(), data.sorted_addr_cells.end(), cmp);
            data.cell_to_addrs.clear();
            std::cerr << "  Addr points sorted: " << n << std::endl;
        }
        log_phase("  Sort addr_points", _st, _sc);

        // 3. Sort ways by (name, node_count, first_node) + reorder nodes
        if (!data.ways.empty()) {
            size_t n = data.ways.size();
            std::vector<uint32_t> order(n);
            std::iota(order.begin(), order.end(), 0);
            const auto& sp = data.string_pool.data();
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                const auto& wa = data.ways[a];
                const auto& wb = data.ways[b];
                if (wa.name_id != wb.name_id) return wa.name_id < wb.name_id;
                if (wa.node_count != wb.node_count) return wa.node_count < wb.node_count;
                // Compare all nodes for total order
                uint8_t nc = std::min(wa.node_count, wb.node_count);
                for (uint8_t j = 0; j < nc; j++) {
                    uint32_t la = float_bits(data.street_nodes[wa.node_offset + j].lat);
                    uint32_t lb = float_bits(data.street_nodes[wb.node_offset + j].lat);
                    if (la != lb) return la < lb;
                    uint32_t ga = float_bits(data.street_nodes[wa.node_offset + j].lng);
                    uint32_t gb = float_bits(data.street_nodes[wb.node_offset + j].lng);
                    if (ga != gb) return ga < gb;
                }
                return false;
            });
            std::vector<uint32_t> old_to_new(n);
            for (uint32_t i = 0; i < n; i++) old_to_new[order[i]] = i;
            std::vector<WayHeader> new_ways(n);
            std::vector<NodeCoord> new_nodes;
            new_nodes.reserve(data.street_nodes.size());
            for (uint32_t i = 0; i < n; i++) {
                auto w = data.ways[order[i]];
                uint32_t old_off = w.node_offset;
                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[old_off + j]);
                new_ways[i] = w;
            }
            data.ways = std::move(new_ways);
            data.street_nodes = std::move(new_nodes);
            for (auto& p : data.sorted_way_cells) p.item_id = old_to_new[p.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_way_cells.begin(), data.sorted_way_cells.end(), cmp);
            data.cell_to_ways.clear();
            std::cerr << "  Ways sorted: " << n << std::endl;
        }
        log_phase("  Sort ways", _st, _sc);

        // 4. Sort interps by (street, start, end, type) + reorder nodes
        if (!data.interp_ways.empty()) {
            size_t n = data.interp_ways.size();
            std::vector<uint32_t> order(n);
            std::iota(order.begin(), order.end(), 0);
            const auto& sp = data.string_pool.data();
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                const auto& ia = data.interp_ways[a];
                const auto& ib = data.interp_ways[b];
                if (ia.street_id != ib.street_id) return ia.street_id < ib.street_id;
                if (ia.start_number != ib.start_number) return ia.start_number < ib.start_number;
                if (ia.end_number != ib.end_number) return ia.end_number < ib.end_number;
                if (ia.interpolation != ib.interpolation) return ia.interpolation < ib.interpolation;
                uint8_t nc = std::min(ia.node_count, ib.node_count);
                for (uint8_t j = 0; j < nc; j++) {
                    uint32_t la = float_bits(data.interp_nodes[ia.node_offset + j].lat);
                    uint32_t lb = float_bits(data.interp_nodes[ib.node_offset + j].lat);
                    if (la != lb) return la < lb;
                    uint32_t ga = float_bits(data.interp_nodes[ia.node_offset + j].lng);
                    uint32_t gb = float_bits(data.interp_nodes[ib.node_offset + j].lng);
                    if (ga != gb) return ga < gb;
                }
                return ia.node_count < ib.node_count;
            });
            std::vector<uint32_t> old_to_new(n);
            for (uint32_t i = 0; i < n; i++) old_to_new[order[i]] = i;
            std::vector<InterpWay> new_interps(n);
            std::vector<NodeCoord> new_nodes;
            new_nodes.reserve(data.interp_nodes.size());
            for (uint32_t i = 0; i < n; i++) {
                auto iw = data.interp_ways[order[i]];
                uint32_t old_off = 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_off + j]);
                new_interps[i] = iw;
            }
            data.interp_ways = std::move(new_interps);
            data.interp_nodes = std::move(new_nodes);
            for (auto& p : data.sorted_interp_cells) p.item_id = old_to_new[p.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 sorted: " << n << std::endl;
        }
        log_phase("  Sort interps", _st, _sc);

        // 5. Sort admin polygons by (name, level, country, vertex_count) + reorder vertices
        if (!data.admin_polygons.empty()) {
            size_t n = data.admin_polygons.size();
            std::vector<uint32_t> order(n);
            std::iota(order.begin(), order.end(), 0);
            const auto& sp = data.string_pool.data();
            std::sort(order.begin(), order.end(), [&](uint32_t a, uint32_t b) {
                const auto& pa = data.admin_polygons[a];
                const auto& pb = data.admin_polygons[b];
                int c = strcmp(sp.data() + pa.name_id, sp.data() + pb.name_id);
                if (c != 0) return c < 0;
                if (pa.admin_level != pb.admin_level) return pa.admin_level < pb.admin_level;
                if (pa.country_code != pb.country_code) return pa.country_code < pb.country_code;
                return pa.vertex_count < pb.vertex_count;
            });
            std::vector<uint32_t> old_to_new(n);
            for (uint32_t i = 0; i < n; i++) old_to_new[order[i]] = i;
            std::vector<AdminPolygon> new_polys(n);
            std::vector<NodeCoord> new_verts;
            new_verts.reserve(data.admin_vertices.size());
            for (uint32_t i = 0; i < n; i++) {
                auto p = data.admin_polygons[order[i]];
                uint32_t old_off = p.vertex_offset;
                p.vertex_offset = static_cast<uint32_t>(new_verts.size());
                for (uint32_t j = 0; j < p.vertex_count; j++)
                    new_verts.push_back(data.admin_vertices[old_off + j]);
                new_polys[i] = p;
            }
            data.admin_polygons = std::move(new_polys);
            data.admin_vertices = std::move(new_verts);
            // Remap admin cell entries
            for (auto& [cell_id, ids] : data.cell_to_admin) {
                for (auto& id : ids) {
                    uint32_t flags = id & 0x80000000u;
                    uint32_t masked = id & 0x7FFFFFFFu;
                    if (masked < old_to_new.size())
                        id = old_to_new[masked] | flags;
                }
                std::sort(ids.begin(), ids.end());
            }
            std::cerr << "  Admin polygons sorted: " << n << std::endl;
        }
        log_phase("  Sort admin", _st, _sc);
    }

    // --- Write index files ---
    log_phase("Deterministic ordering", _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);
        return buf;
    };

    // Write quality variants in parallel (bounded concurrency).
    // Each variant does parallel simplification internally, so limit
    // concurrency to avoid over-subscribing CPU.
    auto write_qualities = [&](const ParsedData& d, const std::string& admin_dir) {
        constexpr unsigned max_concurrent_qualities = 3;
        std::atomic<unsigned> active{0};
        std::mutex qmtx;
        std::condition_variable qcv;
        std::vector<std::future<void>> qfutures;

        for (double scale : quality_scales) {
            {
                std::unique_lock<std::mutex> lock(qmtx);
                qcv.wait(lock, [&]{ return active.load() < max_concurrent_qualities; });
            }
            active.fetch_add(1);

            std::string qname = quality_dir_name(scale);
            std::string qdir = admin_dir + "/" + qname;
            qfutures.push_back(std::async(std::launch::async, [&, qdir, scale]() {
                write_quality_variant(d, admin_dir, qdir, scale);
                active.fetch_sub(1);
                qcv.notify_one();
            }));
        }
        for (auto& f : qfutures) f.get();
    };

    // Write one region: modes + quality variants
    auto write_region = [&](const ParsedData& d, const std::string& base_dir) {
        ensure_dir(base_dir);

        if (multi_output) {
            // Write all 3 modes in parallel (they read shared data, write to separate dirs)
            auto wf1 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/full", IndexMode::Full); });
            auto wf2 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/no-addresses", IndexMode::NoAddresses); });
            auto wf3 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/admin", IndexMode::AdminOnly); });
            wf1.get(); wf2.get(); wf3.get();
        } else {
            write_index(d, base_dir, mode);
        }

        // Write quality variants under admin/ (or base_dir if not multi_output)
        if (multi_quality) {
            std::string admin_dir = multi_output ? base_dir + "/admin" : base_dir;
            std::cerr << "  Writing quality variants for " << base_dir << "..." << std::endl;
            write_qualities(d, admin_dir);
        }
    };

    // Write planet (async — overlaps with continent filtering start)
    auto planet_future = std::async(std::launch::async, [&]() {
        write_region(data, output_dir);
    });

    // Process continents with bounded concurrency, largest first.
    if (generate_continents) {
        // Pre-compute continent membership for each unique cell in sorted pairs.
        // One parallel scan replaces 8 × cell_in_bbox per cell during filtering.
        auto _pct = std::chrono::steady_clock::now();
        auto _pcc = CpuTicks::now();

        // Load continent polygons for boundary-based filtering
        auto continent_polys = get_continent_polygons();
        std::cerr << "  Loaded " << continent_polys.size() << " continent boundary polygons" << std::endl;

        // For each sorted pair array, build a parallel array of continent bitmasks.
        // Since pairs are sorted by cell_id, consecutive entries share the same mask.
        auto precompute_masks = [&continent_polys](const std::vector<CellItemPair>& sorted) -> std::vector<uint8_t> {
            if (sorted.empty()) return {};
            std::vector<uint8_t> masks(sorted.size(), 0);

            unsigned nthreads = std::max(1u, std::thread::hardware_concurrency());
            size_t chunk = (sorted.size() + nthreads - 1) / nthreads;
            std::vector<size_t> bounds = {0};
            for (unsigned t = 1; t < nthreads; t++) {
                size_t target = t * chunk;
                if (target >= sorted.size()) break;
                while (target < sorted.size() && sorted[target].cell_id == sorted[target-1].cell_id)
                    target++;
                if (target < sorted.size()) bounds.push_back(target);
            }
            bounds.push_back(sorted.size());

            std::vector<std::thread> threads;
            for (size_t th = 0; th + 1 < bounds.size(); th++) {
                threads.emplace_back([&, th]() {
                    for (size_t i = bounds[th]; i < bounds[th+1]; ) {
                        uint64_t cell_id = sorted[i].cell_id;
                        S2CellId cell(cell_id);
                        S2LatLng center = cell.ToLatLng();
                        double lat = center.lat().degrees();
                        double lng = center.lng().degrees();
                        uint8_t mask = 0;
                        // Test against continent polygons (not bboxes)
                        for (size_t ci = 0; ci < continent_polys.size() && ci < 8; ci++) {
                            if (point_in_polygon(lat, lng, continent_polys[ci].vertices))
                                mask |= (1u << ci);
                        }
                        while (i < bounds[th+1] && sorted[i].cell_id == cell_id) {
                            masks[i] = mask;
                            i++;
                        }
                    }
                });
            }
            for (auto& t : threads) t.join();
            return masks;
        };

        // Precompute for all three sorted pair arrays
        auto way_masks = std::async(std::launch::async, [&]{ return precompute_masks(data.sorted_way_cells); });
        auto addr_masks = std::async(std::launch::async, [&]{ return precompute_masks(data.sorted_addr_cells); });
        auto interp_masks = std::async(std::launch::async, [&]{ return precompute_masks(data.sorted_interp_cells); });
        auto way_continent_masks = way_masks.get();
        auto addr_continent_masks = addr_masks.get();
        auto interp_continent_masks = interp_masks.get();

        log_phase("Pre-compute continent masks", _pct, _pcc);

        // Sort by bbox area descending — largest continents first
        std::vector<size_t> continent_order(kContinentCount);
        std::iota(continent_order.begin(), continent_order.end(), 0u);
        std::sort(continent_order.begin(), continent_order.end(), [](size_t a, size_t b) {
            auto area = [](const ContinentBBox& c) {
                return (c.max_lat - c.min_lat) * (c.max_lng - c.min_lng);
            };
            return area(kContinents[a]) > area(kContinents[b]);
        });

        unsigned max_concurrent = std::max(1u, std::min(4u,
            std::thread::hardware_concurrency() / 8));
        std::cerr << "Processing " << kContinentCount << " continents ("
                  << max_concurrent << " concurrent, largest first)..." << std::endl;

        std::atomic<unsigned> active{0};
        std::mutex cv_mutex;
        std::condition_variable cv;
        std::vector<std::future<void>> futures;

        for (size_t i = 0; i < kContinentCount; i++) {
            {
                std::unique_lock<std::mutex> lock(cv_mutex);
                cv.wait(lock, [&]{ return active.load() < max_concurrent; });
            }
            active.fetch_add(1);

            futures.push_back(std::async(std::launch::async, [&, i]() {
                const auto& continent = kContinents[continent_order[i]];
                auto _ct = std::chrono::steady_clock::now();
                auto _cc = CpuTicks::now();
                std::cerr << "Continent: " << continent.name << " (start)..." << std::endl;
                uint8_t cbit = 1u << continent_order[i];
                const auto* poly = (continent_order[i] < continent_polys.size() && !continent_polys[continent_order[i]].vertices.empty())
                    ? &continent_polys[continent_order[i]].vertices : nullptr;
                auto subset = filter_by_bbox_masked(data, continent, cbit,
                    way_continent_masks, addr_continent_masks, interp_continent_masks, poly);
                log_phase(("  " + std::string(continent.name) + ": filter").c_str(), _ct, _cc);
                write_region(subset, output_dir + "/" + continent.name);
                log_phase(("  " + std::string(continent.name) + ": total").c_str(), _ct, _cc);

                active.fetch_sub(1);
                cv.notify_one();
            }));
        }
        for (auto& f : futures) f.get();
    }

    // Wait for planet write if not already done
    planet_future.get();
    log_phase("All index writing (total)", _pt, _cpu);

    // --- Generate manifest.json ---
    {
        auto file_size = [](const std::string& path) -> int64_t {
            struct stat st;
            if (stat(path.c_str(), &st) == 0) return st.st_size;
            return -1;
        };

        auto dir_size = [&](const std::string& dir, const std::vector<std::string>& files) -> int64_t {
            int64_t total = 0;
            for (const auto& f : files) {
                int64_t s = file_size(dir + "/" + f);
                if (s > 0) total += s;
            }
            return total;
        };

        std::vector<std::string> admin_base_files = {
            "admin_cells.bin", "admin_entries.bin", "strings.bin"
        };
        std::vector<std::string> admin_quality_files = {
            "admin_polygons.bin", "admin_vertices.bin"
        };
        std::vector<std::string> street_files = {
            "street_ways.bin", "street_nodes.bin", "street_entries.bin"
        };
        std::vector<std::string> address_files = {
            "addr_points.bin", "addr_entries.bin",
            "interp_ways.bin", "interp_nodes.bin", "interp_entries.bin"
        };
        std::vector<std::string> geo_files = {"geo_cells.bin"};

        // Build list of regions
        std::vector<std::pair<std::string, std::string>> regions; // (name, path)
        regions.push_back({"planet", output_dir});
        if (generate_continents) {
            for (size_t ci = 0; ci < kContinentCount; ci++) {
                regions.push_back({kContinents[ci].name,
                    output_dir + "/" + kContinents[ci].name});
            }
        }

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

        std::ofstream mf(output_dir + "/manifest.json");
        mf << "{\n";
        mf << "  \"version\": 1,\n";
        mf << "  \"regions\": [\n";

        for (size_t ri = 0; ri < regions.size(); ri++) {
            const auto& [rname, rpath] = regions[ri];
            mf << "    {\n";
            mf << "      \"name\": \"" << rname << "\",\n";

            // Modes
            mf << "      \"modes\": {\n";
            if (multi_output) {
                // Full
                std::string full_dir = rpath + "/full";
                int64_t full_sz = dir_size(full_dir, geo_files) +
                    dir_size(full_dir, street_files) +
                    dir_size(full_dir, address_files) +
                    dir_size(full_dir, admin_base_files) +
                    dir_size(full_dir, admin_quality_files);
                mf << "        \"full\": {\"size\": " << full_sz << "},\n";

                // Streets (no addresses)
                std::string na_dir = rpath + "/no-addresses";
                int64_t na_sz = dir_size(na_dir, geo_files) +
                    dir_size(na_dir, street_files) +
                    dir_size(na_dir, admin_base_files) +
                    dir_size(na_dir, admin_quality_files);
                mf << "        \"no-addresses\": {\"size\": " << na_sz << "},\n";

                // Admin base (shared files)
                std::string admin_dir = rpath + "/admin";
                int64_t admin_base_sz = dir_size(admin_dir, admin_base_files);
                mf << "        \"admin\": {\"base_size\": " << admin_base_sz << "}\n";
            } else {
                int64_t sz = 0;
                for (const auto& lists : {geo_files, street_files, address_files,
                                          admin_base_files, admin_quality_files}) {
                    sz += dir_size(rpath, lists);
                }
                mf << "        \"" << (mode == IndexMode::Full ? "full" :
                    mode == IndexMode::NoAddresses ? "no-addresses" : "admin") << "\": {\"size\": " << sz << "}\n";
            }
            mf << "      },\n";

            // Quality variants
            if (multi_quality) {
                std::string admin_dir = multi_output ? rpath + "/admin" : rpath;
                mf << "      \"qualities\": [\n";
                for (size_t qi = 0; qi < quality_scales.size(); qi++) {
                    double scale = quality_scales[qi];
                    std::string qname = quality_dir_name(scale);
                    std::string qdir = admin_dir + "/" + qname;
                    int64_t qsz = dir_size(qdir, admin_quality_files);

                    // Compute epsilon values for this scale
                    double eps_l2 = (scale == 0) ? 0 : 500.0 * scale * kEpsilonScale;
                    double eps_l8 = (scale == 0) ? 0 : 15.0 * scale * kEpsilonScale;

                    mf << "        {\"name\": \"" << qname << "\", \"scale\": " << scale
                       << ", \"size\": " << qsz
                       << ", \"epsilon_l2_m\": " << eps_l2
                       << ", \"epsilon_l8_m\": " << eps_l8
                       << "}";
                    if (qi + 1 < quality_scales.size()) mf << ",";
                    mf << "\n";
                }
                mf << "      ]\n";
            } else {
                mf << "      \"qualities\": []\n";
            }

            mf << "    }";
            if (ri + 1 < regions.size()) mf << ",";
            mf << "\n";
        }

        mf << "  ]\n";
        mf << "}\n";

        std::cerr << "Wrote " << output_dir << "/manifest.json" << std::endl;
    }

    std::cerr << "Done." << std::endl;
    return 0;
}

-                int c = strcmp(sp.data() + pa.name_id, sp.data() + pb.name_id);

-                if (c != 0) return c < 0;

+                if (pa.name_id != pb.name_id) return pa.name_id < pb.name_id;

-                return pa.vertex_count < pb.vertex_count;

+                if (pa.vertex_count != pb.vertex_count) return pa.vertex_count < pb.vertex_count;

+                // Compare first few vertices for tiebreaking

+                uint32_t nc = std::min(pa.vertex_count, pb.vertex_count);

+                nc = std::min(nc, 20u); // limit comparison depth

+                    uint32_t la = float_bits(data.admin_vertices[pa.vertex_offset + j].lat);

+                    uint32_t lb = float_bits(data.admin_vertices[pb.vertex_offset + j].lat);

+                    if (la != lb) return la < lb;

+                    uint32_t ga = float_bits(data.admin_vertices[pa.vertex_offset + j].lng);

+                    uint32_t gb = float_bits(data.admin_vertices[pb.vertex_offset + j].lng);

+                    if (ga != gb) return ga < gb;


Bash

cmake --build build -j$(nproc) --target build-index 2>&1 | tail -3

Build with improved sorts


[ 10%] Building CXX object CMakeFiles/build-index.dir/src/build_index.cpp.o
[ 20%] Linking CXX executable build-index
[100%] Built target build-index


default-src 'none'; frame-ancestors 'none'

_cfuvid=xU.h8WaXt7k9ZI1XMmgEjHcr6Vh0Zwhjma6qsBC4Hus-1774606534.9279587-1.0.1.1-RN5qviTemo0YUwck6VG.obQ0PyCcUdaL96Vky_9KnCQ; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.anthropic.com

Overloaded. https://docs.claude.com/en/api/errors


sshpass -p 'elon-is-insane' scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" /root/traccar-geocoder/builder/build/build-index michtest@100.113.88.121:~/build-index-det 2>&1

Deploy improved deterministic build


Warning: Permanently added '100.113.88.121' (ED25519) to the list of known hosts.


sshpass -p 'elon-is-insane' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ProxyCommand="tailscale nc %h %p" michtest@100.113.88.121 '
rm -rf ~/det-A ~/det-B
echo "=== Build A ===" && ~/build-index-det ~/det-A ~/pbf/europe.osm.pbf --simplify-epsilon --epsilon-scale 1 2>&1 | tail -5
echo "=== Build B ===" && ~/build-index-det ~/det-B ~/pbf/europe.osm.pbf --simplify-epsilon --epsilon-scale 1 2>&1 | tail -5
echo "=== Comparing ==="
all_match=true
for f in ~/det-A/*.bin; do
    b=$(basename $f)
    if cmp -s "$f" ~/det-B/$b; then
        echo "  $b: MATCH"
    else
        sa=$(stat -c%s "$f"); sb=$(stat -c%s ~/det-B/$b)
        echo "  $b: MISMATCH (A=$sa B=$sb)"
        all_match=false
    fi
done
if [ -f ~/det-A/manifest.json ] && [ -f ~/det-B/manifest.json ]; then
    if cmp -s ~/det-A/manifest.json ~/det-B/manifest.json; then echo "  manifest.json: MATCH"; fi
fi
$all_match && echo "ALL FILES MATCH" || echo "SOME FILES DIFFER"
' 2>&1

Re-test determinism with improved sorts