The file /root/traccar-geocoder/builder/src/build_index.cpp has been updated successfully.

    // Write continents (parallel — each filters independently from shared data)
    if (generate_continents) {
        std::vector<std::future<void>> continent_futures;
        for (size_t ci = 0; ci < kContinentCount; ci++) {
            continent_futures.push_back(std::async(std::launch::async, [&, ci]() {
                const auto& continent = kContinents[ci];
                std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
                auto subset = filter_by_bbox(data, continent);
                write_region(subset, output_dir + "/" + continent.name);
                std::cerr << "Done continent: " << continent.name << std::endl;
            }));
        }
        for (auto& f : continent_futures) f.get();
    }

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

#include <osmium/io/pbf_input.hpp>
#include <osmium/visitor.hpp>
#include <sys/mman.h>

#include <s2/s2cell_id.h>

#include "types.h"
#include "string_pool.h"
#include "geometry.h"
#include "parsed_data.h"
#include "s2_helpers.h"
#include "ring_assembly.h"
#include "relation_collector.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();

    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() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
        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 Location = 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 DenseIndex {
            osmium::Location* data;
            size_t capacity;

            DenseIndex() : capacity(MAX_NODE_ID) {
                void* ptr = mmap(nullptr, capacity * sizeof(osmium::Location),
                    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 ("
                              << (capacity * sizeof(osmium::Location) / (1024*1024*1024)) << "GB virtual)" << std::endl;
                    std::exit(1);
                }
                data = static_cast<osmium::Location*>(ptr);
                std::cerr << "Allocated dense node index: " << (capacity * sizeof(osmium::Location) / (1024*1024*1024))
                          << "GB virtual address space" << std::endl;
            }

            ~DenseIndex() {
                munmap(data, capacity * sizeof(osmium::Location));
            }

            // Lockless — each node ID maps to a unique array slot
            void set(osmium::unsigned_object_id_type id, osmium::Location loc) {
                if (id >= capacity) {
                    std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
                    std::exit(1);
                }
                data[id] = loc;
            }

            osmium::Location get(osmium::unsigned_object_id_type id) const {
                if (id >= capacity) return osmium::Location{};
                return data[id];
            }

            void set_batch(const std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>>& batch) {
                for (const auto& [id, loc] : batch) {
                    if (id >= capacity) {
                        std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
                        std::exit(1);
                    }
                    data[id] = loc;
                }
            }
        } 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;
            RelationCollector rel_collector(data.collected_relations);

            {
                osmium::io::Reader reader1{input_file, osmium::osm_entity_bits::relation};
                osmium::apply(reader1, rel_collector);
                reader1.close();
            }
            std::cerr << "  Collected " << data.collected_relations.size()
                      << " admin/postal relations for parallel assembly." << std::endl;

            // --- Combined Pass 2+3: nodes then ways in a single PBF read ---
            // PBF guarantees ordering: nodes → ways → relations.
            // We read everything in one pass, processing nodes first, then
            // transitioning to way processing when the first way block arrives.
            log_phase("Pass 1: relation scanning", _pt);

            // Build admin_way_ids BEFORE the combined pass (needed during way processing)
            std::unordered_set<int64_t> admin_way_ids;
            for (const auto& rel : data.collected_relations) {
                for (const auto& [way_id, role] : rel.members) {
                    admin_way_ids.insert(way_id);
                }
            }
            std::cerr << "  Admin assembly needs " << admin_way_ids.size() << " way geometries." << std::endl;

            std::cerr << "  Pass 2+3: processing nodes and ways in single read..." << std::endl;

            {
                // Shared queue infrastructure for both phases
                std::mutex queue_mutex;
                std::condition_variable queue_cv;
                std::deque<osmium::memory::Buffer> block_queue;
                bool reader_done = false;
                const size_t MAX_QUEUE = MAX_BLOCK_QUEUE;

                struct NodeThreadLocal {
                    std::vector<std::pair<double,double>> addr_coords;
                    std::vector<std::pair<std::string,std::string>> addr_strings;
                    uint64_t count = 0;
                };
                std::vector<NodeThreadLocal> ntld(num_threads);
                std::atomic<uint64_t> blocks_done{0};

                // Worker threads
                std::vector<std::thread> node_workers;
                for (unsigned int t = 0; t < num_threads; t++) {
                    node_workers.emplace_back([&, t]() {
                        auto& local = ntld[t];
                        std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>> loc_batch;
                        loc_batch.reserve(8192);

                        while (true) {
                            osmium::memory::Buffer block;
                            {
                                std::unique_lock<std::mutex> lock(queue_mutex);
                                queue_cv.wait(lock, [&]{ return !block_queue.empty() || reader_done; });
                                if (block_queue.empty() && reader_done) break;
                                block = std::move(block_queue.front());
                                block_queue.pop_front();
                            }
                            queue_cv.notify_one(); // wake reader if it was waiting

                            for (const auto& item : block) {
                                if (item.type() == osmium::item_type::node) {
                                    const auto& node = static_cast<const osmium::Node&>(item);
                                    if (!node.location().valid()) continue;
                                    loc_batch.push_back({node.positive_id(), node.location()});

                                    const char* housenumber = node.tags()["addr:housenumber"];
                                    if (housenumber) {
                                        const char* street = node.tags()["addr:street"];
                                        if (street) {
                                            local.addr_coords.push_back({node.location().lat(), node.location().lon()});
                                            local.addr_strings.push_back({housenumber, street});
                                            local.count++;
                                        }
                                    }
                                }
                            }

                            // Flush location batch (sharded index handles its own locking)
                            if (!loc_batch.empty()) {
                                index.set_batch(loc_batch);
                                loc_batch.clear();
                            }

                            uint64_t n = blocks_done.fetch_add(1) + 1;
                            if (n % 1000 == 0) {
                                std::cerr << "  Processed " << n << " node blocks..." << std::endl;
                            }
                        }

                        // Flush remaining
                        if (!loc_batch.empty()) {
                            index.set_batch(loc_batch);
                        }
                    });
                }

                // Single reader for all entity types — processes nodes first,
                // then transitions to way processing when first way block arrives.
                osmium::io::Reader combined_reader{input_file};
                bool nodes_done = false;
                osmium::memory::Buffer first_way_buf; // stash the first way block

                // Phase 1: Feed node blocks to node workers
                while (auto buf = combined_reader.read()) {
                    // Check if this buffer contains nodes or ways/relations
                    bool has_node = false;
                    for (const auto& item : buf) {
                        if (item.type() == osmium::item_type::node) { has_node = true; break; }
                    }

                    if (has_node) {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        queue_cv.wait(lock, [&]{ return block_queue.size() < MAX_QUEUE; });
                        block_queue.push_back(std::move(buf));
                        queue_cv.notify_one();
                    } else {
                        // First non-node block — stash it and transition
                        first_way_buf = std::move(buf);
                        nodes_done = true;
                        break;
                    }
                }

                // Signal node workers done
                {
                    std::lock_guard<std::mutex> lock(queue_mutex);
                    reader_done = true;
                }
                queue_cv.notify_all();
                for (auto& w : node_workers) w.join();

                // 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);
                std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;

                // --- Phase 2: Way processing using same reader ---

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

                // Read buffers and dispatch to thread pool
                // Streaming producer-consumer for way blocks
                std::mutex way_queue_mutex;
                std::condition_variable way_queue_cv;
                std::deque<osmium::memory::Buffer> way_block_queue;
                bool way_reader_done = false;
                const size_t WAY_MAX_QUEUE = MAX_BLOCK_QUEUE;

                std::vector<ThreadLocalData> tld(num_threads);
                std::atomic<uint64_t> way_blocks_done{0};

                std::vector<std::thread> workers;
                for (unsigned int t = 0; t < num_threads; t++) {
                    workers.emplace_back([&, t]() {
                        auto& local = tld[t];
                        while (true) {
                            osmium::memory::Buffer block;
                            {
                                std::unique_lock<std::mutex> lock(way_queue_mutex);
                                way_queue_cv.wait(lock, [&]{ return !way_block_queue.empty() || way_reader_done; });
                                if (way_block_queue.empty() && way_reader_done) break;
                                block = std::move(way_block_queue.front());
                                way_block_queue.pop_front();
                            }
                            way_queue_cv.notify_one();

                            for (const auto& item : block) {
                                if (item.type() == osmium::item_type::way) {
                                    const auto& way = static_cast<const osmium::Way&>(item);

                                    // Resolve node locations (read-only from shared index)
                                    // We need to check if nodes have valid locations
                                    const auto& wnodes = way.nodes();

                                    // Address interpolation
                                    const char* interpolation = way.tags()["addr:interpolation"];
                                    if (interpolation) {
                                        if (wnodes.size() >= 2) {
                                            bool all_valid = true;
                                            for (const auto& nr : wnodes) {
                                                auto loc = index.get(nr.positive_ref());
                                                if (!loc.valid()) { all_valid = false; break; }
                                            }
                                            if (all_valid) {
                                                const char* street = way.tags()["addr: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& nr : wnodes) {
                                                        auto loc = index.get(nr.positive_ref());
                                                        local.interp_nodes.push_back({
                                                            static_cast<float>(loc.lat()),
                                                            static_cast<float>(loc.lon())
                                                        });
                                                    }

                                                    uint8_t interp_type = 0;
                                                    if (std::strcmp(interpolation, "even") == 0) interp_type = 1;
                                                    else if (std::strcmp(interpolation, "odd") == 0) interp_type = 2;

                                                    InterpWay iw{};
                                                    iw.node_offset = node_offset;
                                                    iw.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
                                                    iw.street_id = 0; // placeholder, intern later
                                                    iw.start_number = 0;
                                                    iw.end_number = 0;
                                                    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++;
                                                }
                                            }
                                        }
                                        continue;
                                    }

                                    // Building addresses
                                    const char* housenumber = way.tags()["addr:housenumber"];
                                    if (housenumber) {
                                        const char* street = way.tags()["addr:street"];
                                        if (street && !wnodes.empty()) {
                                            double sum_lat = 0, sum_lng = 0;
                                            int valid = 0;
                                            for (const auto& nr : wnodes) {
                                                auto loc = index.get(nr.positive_ref());
                                                if (loc.valid()) {
                                                    sum_lat += loc.lat();
                                                    sum_lng += loc.lon();
                                                    valid++;
                                                }
                                            }
                                            if (valid > 0) {
                                                // Store coords for later S2 cell computation
                                                local.building_addr_coords.push_back({sum_lat / valid, sum_lng / valid});
                                                // Placeholder addr point — string IDs filled in during merge
                                                local.building_addrs.push_back({
                                                    static_cast<float>(sum_lat / valid),
                                                    static_cast<float>(sum_lng / valid),
                                                    0, 0 // placeholder string IDs
                                                });
                                                // Store strings for later interning
                                                local.addr_strings.push_back({housenumber, street});
                                                local.building_addr_count++;
                                            }
                                        }
                                    }

                                    // Highway ways
                                    const char* highway = way.tags()["highway"];
                                    if (highway && is_included_highway(highway)) {
                                        const char* name = way.tags()["name"];
                                        if (name && wnodes.size() >= 2) {
                                            bool all_valid = true;
                                            for (const auto& nr : wnodes) {
                                                auto loc = index.get(nr.positive_ref());
                                                if (!loc.valid()) { all_valid = false; break; }
                                            }
                                            if (all_valid) {
                                                uint32_t way_id = static_cast<uint32_t>(local.ways.size());
                                                uint32_t node_offset = static_cast<uint32_t>(local.street_nodes.size());

                                                for (const auto& nr : wnodes) {
                                                    auto loc = index.get(nr.positive_ref());
                                                    local.street_nodes.push_back({
                                                        static_cast<float>(loc.lat()),
                                                        static_cast<float>(loc.lon())
                                                    });
                                                }

                                                WayHeader header{};
                                                header.node_offset = node_offset;
                                                header.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
                                                header.name_id = 0; // placeholder, intern later
                                                local.ways.push_back(header);
                                                local.way_strings.push_back(name);
                                                local.deferred_ways.push_back({way_id, node_offset, header.node_count});
                                                local.way_count++;
                                            }
                                        }
                                    }

                                    // Store way geometry only for admin boundary member ways
                                    if (!wnodes.empty() && admin_way_ids.count(way.id())) {
                                        std::vector<std::pair<double,double>> geom;
                                        for (const auto& nr : wnodes) {
                                            auto loc = index.get(nr.positive_ref());
                                            if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
                                        }
                                        if (!geom.empty()) {
                                            int64_t first_nid = wnodes.front().positive_ref();
                                            int64_t last_nid = wnodes.back().positive_ref();
                                            local.way_geoms.push_back({way.id(), std::move(geom), first_nid, last_nid});
                                        }
                                    }

                                    // Handle closed ways that are admin boundaries themselves
                                    // Osmium creates areas from closed ways independently of
                                    // multipolygon relations, even if the way is a relation member.
                                    {
                                        const char* boundary = way.tags()["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) && wnodes.size() >= 4 &&
                                                wnodes.front().ref() == wnodes.back().ref()) {
                                                // Closed way forming an admin polygon
                                                uint8_t al = 0;
                                                if (is_admin) {
                                                    const char* level_str = way.tags()["admin_level"];
                                                    if (level_str) al = static_cast<uint8_t>(std::atoi(level_str));
                                                } else {
                                                    al = 11;
                                                }
                                                int max_al = is_postal ? 11 : 10;
                                                if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
                                                    const char* aname = way.tags()["name"];
                                                    if (!aname && is_admin) continue; // admin needs name
                                                    std::string name_str;
                                                    if (is_postal) {
                                                        const char* pc = way.tags()["postal_code"];
                                                        if (!pc) pc = aname;
                                                        if (!pc) continue; // postal needs postal_code or name
                                                        name_str = pc;
                                                    } else {
                                                        name_str = aname;
                                                    }
                                                    {
                                                        std::vector<std::pair<double,double>> verts;
                                                        bool all_valid = true;
                                                        for (const auto& nr : wnodes) {
                                                            auto loc = index.get(nr.positive_ref());
                                                            if (loc.valid()) {
                                                                verts.push_back({loc.lat(), loc.lon()});
                                                            } else { all_valid = false; break; }
                                                        }
                                                        if (all_valid && verts.size() >= 3) {
                                                            std::string cc;
                                                            if (al == 2) {
                                                                const char* iso = way.tags()["ISO3166-1:alpha2"];
                                                                if (iso) cc = iso;
                                                            }
                                                            local.closed_way_admins.push_back({
                                                                std::move(verts),
                                                                std::move(name_str),
                                                                al, std::move(cc)
                                                            });
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }

                            uint64_t done = way_blocks_done.fetch_add(1) + 1;
                            if (done % 1000 == 0) {
                                std::cerr << "  Processed " << done << " way blocks..." << std::endl;
                            }
                        }
                    });
                }

                // Way reader: use the combined_reader (continuing from where nodes left off)
                {
                    // First, push the stashed way buffer
                    if (first_way_buf) {
                        std::unique_lock<std::mutex> lock(way_queue_mutex);
                        way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
                        way_block_queue.push_back(std::move(first_way_buf));
                        way_queue_cv.notify_one();
                    }
                    // Continue reading remaining blocks from the same reader
                    while (auto buf = combined_reader.read()) {
                        {
                            std::unique_lock<std::mutex> lock(way_queue_mutex);
                            way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
                            way_block_queue.push_back(std::move(buf));
                        }
                        way_queue_cv.notify_one();
                    }
                    combined_reader.close();
                    {
                        std::lock_guard<std::mutex> lock(way_queue_mutex);
                        way_reader_done = true;
                    }
                    way_queue_cv.notify_all();
                }

                for (auto& w : workers) w.join();
                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);
                    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);

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

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

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

        // --- Parallel S2 cell computation for ways and interpolation ---
        {
            log_phase("Admin assembly", _pt);
            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();

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

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

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

            // 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);
        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, [&] {
                // Extract and sort unique cell IDs
                std::vector<uint64_t> sorted_cells;
                sorted_cells.reserve(data.cell_to_addrs.size());
                size_t total_pairs = 0;
                for (auto& [cell_id, ids] : data.cell_to_addrs) {
                    sorted_cells.push_back(cell_id);
                    total_pairs += ids.size();
                }
                std::sort(sorted_cells.begin(), sorted_cells.end());

                // Build sorted pairs by iterating sorted cell IDs
                std::vector<CellItemPair> pairs;
                pairs.reserve(total_pairs);
                for (uint64_t cell_id : sorted_cells) {
                    auto& ids = data.cell_to_addrs[cell_id];
                    std::sort(ids.begin(), ids.end()); // sort within cell
                    ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
                    for (auto id : ids) pairs.push_back({cell_id, id});
                }
                data.sorted_addr_cells = std::move(pairs);
            });
            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 continent filtering ---
    if (generate_continents || !save_cache_path.empty()) {
        auto rebuild_map = [](const std::vector<CellItemPair>& sorted,
                              std::unordered_map<uint64_t, std::vector<uint32_t>>& map) {
            if (sorted.empty() || !map.empty()) return;
            for (const auto& p : sorted) {
                map[p.cell_id].push_back(p.item_id);
            }
        };
        std::cerr << "Rebuilding cell maps for continent filtering..." << std::endl;
        auto f1 = std::async(std::launch::async, [&]{ rebuild_map(data.sorted_way_cells, data.cell_to_ways); });
        auto f2 = std::async(std::launch::async, [&]{ rebuild_map(data.sorted_interp_cells, data.cell_to_interps); });
        f1.get(); f2.get();
        log_phase("Rebuild cell maps", _pt);
    }

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

    // Helper: write quality variants for a dataset into base_dir/admin/qN/
    auto write_qualities = [&](const ParsedData& d, const std::string& base_dir) {
        if (!multi_quality) return;
        std::string admin_dir = base_dir + "/admin";
        for (double scale : quality_scales) {
            std::string qname = (scale == 0) ? "uncapped" : "q" + std::to_string(scale);
            // Clean up trailing zeros: "q1.000000" -> "q1"
            if (scale > 0) {
                char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);
                qname = buf;
            }
            std::string qdir = admin_dir + "/" + qname;
            std::cerr << "    Quality: " << qname << "..." << std::endl;
            write_quality_variant(d, admin_dir, qdir, scale);
        }
    };

    // Helper: write one region (planet or continent subset) in S3-friendly layout
    auto write_region = [&](const ParsedData& d, const std::string& base_dir) {
        ensure_dir(base_dir);

        if (multi_output) {
            // S3-friendly layout: separate directories for each data type
            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_qualities(d, base_dir);
    };

    // Write planet
    write_region(data, output_dir);
    log_phase("Planet index writing", _pt);

    // Write continents
    if (generate_continents) {
        std::mutex cerr_mutex;
        // Process continents sequentially to avoid memory explosion
        // (each filter_by_bbox creates a full copy)
        for (size_t ci = 0; ci < kContinentCount; ci++) {
            const auto& continent = kContinents[ci];
            std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
            auto subset = filter_by_bbox(data, continent);
            write_region(subset, output_dir + "/" + continent.name);
            std::cerr << "Done continent: " << continent.name << std::endl;
        }
    }

    log_phase("Index file writing (total)", _pt);

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

+    // Write continents (parallel — each filters independently from shared data)

-        // Process continents sequentially to avoid memory explosion

-        // (each filter_by_bbox creates a full copy)

+        std::vector<std::future<void>> continent_futures;

-            const auto& continent = kContinents[ci];

-            std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;

-            auto subset = filter_by_bbox(data, continent);

-            write_region(subset, output_dir + "/" + continent.name);

-            std::cerr << "Done continent: " << continent.name << std::endl;

+            continent_futures.push_back(std::async(std::launch::async, [&, ci]() {

+                const auto& continent = kContinents[ci];

+                std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;

+                auto subset = filter_by_bbox(data, continent);

+                write_region(subset, output_dir + "/" + continent.name);

+                std::cerr << "Done continent: " << continent.name << std::endl;

+        for (auto& f : continent_futures) f.get();


Bash

Rebuild with parallel continents


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


Built. Now let me check if the current sequential build finished:


ssh node3 "grep -E 'Done cont|Filtering|manifest|Done\.' /tmp/s3build.log" 2>&1

Check progress


Filtering for continent: africa...
Done continent: africa
Filtering for continent: asia...
Done continent: asia
Filtering for continent: europe...
Done continent: europe
Filtering for continent: north-america...
Done continent: north-america
Filtering for continent: south-america...


Still running South America. Let me wait for it to finish so we can see the full results with timings, then rerun with the parallel version to compare:


sleep 60 && ssh node3 "grep -E 'Done cont|Filtering|manifest|Done\.' /tmp/s3build.log" 2>&1

Check after 60s


sleep 60 && ssh node3 "grep -E 'Done cont|Filtering|manifest|Done\.' /tmp/s3build.log" 2>&1

Check after 60s


sleep 60 && ssh node3 "grep -E 'Done cont|Filtering|manifest|Done\.' /tmp/s3build.log" 2>&1

Check after 60s


Filtering for continent: africa...
Done continent: africa
Filtering for continent: asia...
Done continent: asia
Filtering for continent: europe...
Done continent: europe
Filtering for continent: north-america...
Done continent: north-america
Filtering for continent: south-america...
Done continent: south-america
Filtering for continent: oceania...


sleep 120 && ssh node3 "grep -E 'Done cont|Filtering|manifest|Done\.|^\s+\[.*total\|^\s+\[.*Planet\|^\s+\[.*Quality' /tmp/s3build.log | tail -20" 2>&1

Check after 2 min


sleep 120 && ssh node3 "tail -10 /tmp/s3build.log" 2>&1

Check after 2 min


    Quality: q1...
Quality 1x: 35548 polygons, 3479892 vertices (26 MiB)
    Quality: q1.5...
Quality 1.5x: 34797 polygons, 2783546 vertices (21 MiB)
    Quality: q2...
Quality 2x: 34201 polygons, 2363613 vertices (18 MiB)
    Quality: q2.5...
Quality 2.5x: 33716 polygons, 2076257 vertices (15 MiB)
Done continent: oceania
Filtering for continent: central-america...


sleep 60 && ssh node3 "tail -5 /tmp/s3build.log" 2>&1

Check after 60s


Quality 2x: 34201 polygons, 2363613 vertices (18 MiB)
    Quality: q2.5...
Quality 2.5x: 33716 polygons, 2076257 vertices (15 MiB)
Done continent: oceania
Filtering for continent: central-america...


sleep 60 && ssh node3 "tail -10 /tmp/s3build.log" 2>&1

Check after 60s


    Quality: q1...
Quality 1x: 31813 polygons, 3464869 vertices (26 MiB)
    Quality: q1.5...
Quality 1.5x: 31555 polygons, 2712856 vertices (20 MiB)
    Quality: q2...
Quality 2x: 31323 polygons, 2254624 vertices (17 MiB)
    Quality: q2.5...
Quality 2.5x: 31096 polygons, 1940431 vertices (14 MiB)
Done continent: central-america
Filtering for continent: antarctica...


sleep 30 && ssh node3 "tail -5 /tmp/s3build.log" 2>&1

Check for completion


Quality 2x: 31323 polygons, 2254624 vertices (17 MiB)
    Quality: q2.5...
Quality 2.5x: 31096 polygons, 1940431 vertices (14 MiB)
Done continent: central-america
Filtering for continent: antarctica...


sleep 30 && ssh node3 "tail -5 /tmp/s3build.log" 2>&1

Check for completion


sleep 30 && ssh node3 "tail -5 /tmp/s3build.log && echo '---' && grep -E '^\s+\[' /tmp/s3build.log | tail -5" 2>&1

Check for completion


Quality 2x: 31323 polygons, 2254624 vertices (17 MiB)
    Quality: q2.5...
Quality 2.5x: 31096 polygons, 1940431 vertices (14 MiB)
Done continent: central-america
Filtering for continent: antarctica...
---
  [2.9s]   Write: entry files
  [0.0s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched
  [0.0s]   Write: geo_cells.bin
  [0.1s]   Write: parallel data files launched


sleep 30 && ssh node3 "grep -c 'Done\.' /tmp/s3build.log && grep 'manifest\|Done\.' /tmp/s3build.log" 2>&1

Check for final Done


Exit code 1
0


sleep 30 && ssh node3 "grep -c 'Done\.' /tmp/s3build.log; tail -3 /tmp/s3build.log" 2>&1

Check tail


1
  [2357.1s] Index file writing (total)
Wrote /home/michtest/test-s3/manifest.json
Done.


Complete! Let me get the full timings and check the manifest:


ssh node3 "grep -E '^\s+\[' /tmp/s3build.log | grep -v Write | head -20" 2>&1

Get phase timings


  [34.9s] Pass 1: relation scanning
  [185.3s] Pass 2: node processing
  [134.9s] Pass 2b: way processing
  [2.9s]     Admin: ring assembly
  [1.2s]     Admin: parallel simplify
  [3.6s]     Admin: append + submit S2
  [135.4s]     Admin: S2 covering drain
  [0.6s] Admin assembly
  [137.4s]   S2: street ways (parallel)
  [0.0s]   S2: interp ways (parallel)
  [8.9s]   S2: sort + group into cell maps
  [254.6s] S2 cell computation
  [229.7s] Rebuild cell maps
  [0.0s] Deduplication
  [30.6s] Planet index writing
  [strings.bin: 17884308 bytes
  [2357.1s] Index file writing (total)


ssh node3 "cat /tmp/s3build.log | grep 'EXIT\|real\|user\|sys' ; echo '---'; cat ~/test-s3/manifest.json | head -30" 2>&1

Get total time and manifest sample


No `time` output since it was run via nohup. Let me calculate from the log_phase values and check the manifest:


ssh node3 "cat ~/test-s3/manifest.json" 2>&1

Get full manifest


(Bash completed with no output)


ssh node3 "python3 -c \"import json; print(json.dumps(json.load(open('/home/michtest/test-s3/manifest.json')), indent=2))\" | head -60" 2>&1

Pretty print manifest


Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import json; print(json.dumps(json.load(open('/home/michtest/test-s3/manifest.json')), indent=2))
                                  ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/json/__init__.py", line 293, in load
    return loads(fp.read(),
        cls=cls, object_hook=object_hook,
        parse_float=parse_float, parse_int=parse_int,
        parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
  File "/usr/lib/python3.13/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "/usr/lib/python3.13/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.13/json/decoder.py", line 363, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)


ssh node3 "wc -c ~/test-s3/manifest.json; head -1 ~/test-s3/manifest.json" 2>&1

Check manifest file


0 /home/michtest/test-s3/manifest.json


Manifest is empty — the C++ ofstream didn't write. Likely because the `strings.bin:` line in the log broke the `log_phase` output parsing. Let me check the directory structure at least:


ssh node3 "find ~/test-s3 -maxdepth 3 -type d | sort | head -40" 2>&1

Check directory structure


/home/michtest/test-s3
/home/michtest/test-s3/admin
/home/michtest/test-s3/full
/home/michtest/test-s3/no-addresses


The continent directories and quality subdirectories are missing. The `admin` directory exists but no quality variants. Something went wrong. Let me check:


ssh node3 "find ~/test-s3 -type d | sort && echo '---' && find ~/test-s3 -type f | wc -l && echo '---' && du -sh ~/test-s3/" 2>&1

Full structure check


/home/michtest/test-s3
/home/michtest/test-s3/admin
/home/michtest/test-s3/full
/home/michtest/test-s3/no-addresses
---
29
---
23G	/home/michtest/test-s3/


Only 3 directories, 29 files. The continents and quality variants weren't written. But the log shows them completing. Let me check — the issue might be that `write_region` calls `write_index` which writes to `base/admin` instead of just `base/admin-only`. Let me look at the log more carefully:


ssh node3 "ls -la ~/test-s3/admin/ && echo '---' && ls -la ~/test-s3/full/ | head -10" 2>&1

Check what files exist


total 2953176
drwxr-xr-x 2 michtest michtest       4096 Mar 23 20:28 .
drwxrwxr-x 5 michtest michtest       4096 Mar 23 21:07 ..
-rw-rw-r-- 1 michtest michtest   24677112 Mar 23 20:28 admin_cells.bin
-rw-rw-r-- 1 michtest michtest   47068700 Mar 23 20:28 admin_entries.bin
-rw-rw-r-- 1 michtest michtest   22736592 Mar 23 20:28 admin_polygons.bin
-rw-rw-r-- 1 michtest michtest 2920082568 Mar 23 20:28 admin_vertices.bin
-rw-rw-r-- 1 michtest michtest    9466554 Mar 23 20:28 strings.bin
---
total 9151156
drwxr-xr-x 2 michtest michtest       4096 Mar 23 20:28 .
drwxrwxr-x 5 michtest michtest       4096 Mar 23 21:07 ..
-rw-rw-r-- 1 michtest michtest  738166244 Mar 23 20:28 addr_entries.bin
-rw-rw-r-- 1 michtest michtest          0 Mar 23 20:28 addr_points.bin
-rw-rw-r-- 1 michtest michtest          0 Mar 23 20:28 admin_cells.bin
-rw-rw-r-- 1 michtest michtest          0 Mar 23 20:28 admin_entries.bin
-rw-rw-r-- 1 michtest michtest          0 Mar 23 20:28 admin_polygons.bin
-rw-rw-r-- 1 michtest michtest          0 Mar 23 20:28 admin_vertices.bin
-rw-rw-r-- 1 michtest michtest 6397243392 Mar 23 20:28 geo_cells.bin


The `full/` directory has 0-byte admin files — the write_index for Full mode writes admin to the same dir, which then gets overwritten with the quality process. And the quality subdirectories and continents aren't there at all. The quality variant `write_quality_variant` creates dirs under `base_dir/admin/` but the continents never ran their `write_region`.

The issue is that the build logged "Done continent: X" but the actual continent directories weren't created. The `write_region` lambda likely didn't run correctly. Given this is 2357s (39 minutes) of sequential continent processing, let me fix the bugs and restart with the parallel version. The key issues:

1. `write_index` for Full/NoAddresses mode writes admin files inline — it shouldn't when quality variants handle them separately
2. Quality directories aren't being created under `admin/`
3. Manifest is empty
4. Continents aren't creating directories

This needs a proper fix. Let me stop here, fix the code properly, and restart. The current build produced planet data correctly (23 GiB) but the structure is wrong.


ssh node3 "pkill -9 -f build-index 2>/dev/null; echo 'Cleaned'" 2>&1

Clean up


Exit code 255


The build is already done, no process to kill. The SSH error is from the mux session dying. Let me fix the code issues properly, then we'll rerun with everything working:

The problems are:
1. The `write_region` helper calls `write_index` which writes admin files into the same dir as streets — then `write_qualities` tries to create subdirs under `admin/` but the admin files are in `full/`, `no-addresses/`, etc.
2. The continent `filter_by_bbox` + `write_region` runs but output goes to wrong paths
3. The manifest writer's output file is empty (likely the path doesn't exist when opened)

The root cause is that `write_index` was never designed for the S3 layout — it writes all files for a mode into one flat directory. I need to keep using `write_index` for the actual data writing but reorganize how quality variants overlay on top.

Let me take a step back. The simplest correct approach: keep the existing `write_index` behavior (flat directories per mode), and add quality subdirectories *inside* each mode directory that has admin data:


  1260→    // --- Write index files ---
  1261→    log_phase("Deduplication", _pt);
  1262→    std::cerr << "Writing index files to " << output_dir << "..." << std::endl;
  1263→
  1264→    // Helper: write quality variants for a dataset into base_dir/admin/qN/
  1265→    auto write_qualities = [&](const ParsedData& d, const std::string& base_dir) {
  1266→        if (!multi_quality) return;
  1267→        std::string admin_dir = base_dir + "/admin";
  1268→        for (double scale : quality_scales) {
  1269→            std::string qname = (scale == 0) ? "uncapped" : "q" + std::to_string(scale);
  1270→            // Clean up trailing zeros: "q1.000000" -> "q1"
  1271→            if (scale > 0) {
  1272→                char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);
  1273→                qname = buf;
  1274→            }
  1275→            std::string qdir = admin_dir + "/" + qname;
  1276→            std::cerr << "    Quality: " << qname << "..." << std::endl;
  1277→            write_quality_variant(d, admin_dir, qdir, scale);
  1278→        }
  1279→    };
  1280→
  1281→    // Helper: write one region (planet or continent subset) in S3-friendly layout
  1282→    auto write_region = [&](const ParsedData& d, const std::string& base_dir) {
  1283→        ensure_dir(base_dir);
  1284→
  1285→        if (multi_output) {
  1286→            // S3-friendly layout: separate directories for each data type
  1287→            auto wf1 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/full", IndexMode::Full); });
  1288→            auto wf2 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/no-addresses", IndexMode::NoAddresses); });
  1289→            auto wf3 = std::async(std::launch::async, [&]{ write_index(d, base_dir + "/admin", IndexMode::AdminOnly); });
  1290→            wf1.get(); wf2.get(); wf3.get();
  1291→        } else {
  1292→            write_index(d, base_dir, mode);
  1293→        }
  1294→
  1295→        write_qualities(d, base_dir);
  1296→    };
  1297→
  1298→    // Write planet
  1299→    write_region(data, output_dir);
  1300→    log_phase("Planet index writing", _pt);
  1301→
  1302→    // Write continents (parallel — each filters independently from shared data)
  1303→    if (generate_continents) {
  1304→        std::vector<std::future<void>> continent_futures;
  1305→        for (size_t ci = 0; ci < kContinentCount; ci++) {
  1306→            continent_futures.push_back(std::async(std::launch::async, [&, ci]() {
  1307→                const auto& continent = kContinents[ci];
  1308→                std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
  1309→                auto subset = filter_by_bbox(data, continent);
  1310→                write_region(subset, output_dir + "/" + continent.name);
  1311→                std::cerr << "Done continent: " << continent.name << std::endl;
  1312→            }));
  1313→        }
  1314→        for (auto& f : continent_futures) f.get();

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

    // Helper: write quality variants for a dataset into base_dir/admin/qN/
    auto write_qualities = [&](const ParsedData& d, const std::string& base_dir) {
        if (!multi_quality) return;
        std::string admin_dir = base_dir + "/admin";
        for (double scale : quality_scales) {
            std::string qname = (scale == 0) ? "uncapped" : "q" + std::to_string(scale);
            // Clean up trailing zeros: "q1.000000" -> "q1"
            if (scale > 0) {
                char buf[32]; snprintf(buf, sizeof(buf), "q%.4g", scale);
                qname = buf;
            }
            std::string qdir = admin_dir + "/" + qname;
            std::cerr << "    Quality: " << qname << "..." << std::endl;
            write_quality_variant(d, admin_dir, qdir, scale);
        }
    };

    // Helper: write one region (planet or continent subset) in S3-friendly layout
    auto write_region = [&](const ParsedData& d, const std::string& base_dir) {
        ensure_dir(base_dir);

        if (multi_output) {
            // S3-friendly layout: separate directories for each data type
            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_qualities(d, base_dir);
    };

    // Write planet
    write_region(data, output_dir);
    log_phase("Planet index writing", _pt);

    // Write continents (parallel — each filters independently from shared data)
    if (generate_continents) {
        std::vector<std::future<void>> continent_futures;
        for (size_t ci = 0; ci < kContinentCount; ci++) {
            continent_futures.push_back(std::async(std::launch::async, [&, ci]() {
                const auto& continent = kContinents[ci];
                std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
                auto subset = filter_by_bbox(data, continent);
                write_region(subset, output_dir + "/" + continent.name);
                std::cerr << "Done continent: " << continent.name << std::endl;
            }));
        }
        for (auto& f : continent_futures) f.get();


The problem is clear: the `write_region` lambda captures `&` but the `write_qualities` and `write_index` inside it also capture `&`. When called from `std::async`, the lambda's captures can have lifetime issues. More importantly, `write_index` writes admin files into full/ and no-addresses/ directories which duplicates them.

Let me restructure so quality variants go inside each mode directory that contains admin data: