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

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

    if (multi_output) {

#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <deque>
#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;
        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;

    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 == "--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);
        }
    }

    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_polygon(vertices);
                                    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<uint16_t>(std::min(pp.simplified.size(), size_t(MAX_VERTEX_COUNT)));
                        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);
        }
    }

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

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

    if (generate_continents) {
        // Process continents in parallel (each filters independently from shared data)
        std::vector<std::future<void>> continent_futures;
        std::mutex cerr_mutex;
        for (size_t ci = 0; ci < kContinentCount; ci++) {
            const auto& continent = kContinents[ci];
            continent_futures.push_back(std::async(std::launch::async, [&, continent]() {
                {
                    std::lock_guard<std::mutex> lock(cerr_mutex);
                    std::cerr << "Filtering for continent: " << continent.name << "..." << std::endl;
                }
                auto subset = filter_by_bbox(data, continent);
                std::string base = output_dir + "/" + continent.name;
                ensure_dir(base);
                if (multi_output) {
                    // Write modes in parallel within each continent
                    auto cf1 = std::async(std::launch::async, [&]{ write_index(subset, base + "/full", IndexMode::Full); });
                    auto cf2 = std::async(std::launch::async, [&]{ write_index(subset, base + "/no-addresses", IndexMode::NoAddresses); });
                    auto cf3 = std::async(std::launch::async, [&]{ write_index(subset, base + "/admin-only", IndexMode::AdminOnly); });
                    cf1.get(); cf2.get(); cf3.get();
                } else {
                    write_index(subset, base, mode);
                }
                {
                    std::lock_guard<std::mutex> lock(cerr_mutex);
                    std::cerr << "Done continent: " << continent.name << std::endl;
                }
            }));
        }
        for (auto& f : continent_futures) {
            f.get();
        }
    }

    log_phase("Index file writing", _pt);
    std::cerr << "Done." << std::endl;
    return 0;
}

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

+        log_phase("Rebuild cell maps", _pt);


Bash

cd /root/traccar-geocoder/builder/build && make -j$(nproc) 2>&1 | tail -3

Build with continent fix


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


Now let me send it to node 3 and run the full multi-output + continents build:


scp /root/traccar-geocoder/builder/build/build-index node3:~/build-index-multi 2>&1 && ssh node3 "rm -rf ~/test-multi && mkdir ~/test-multi && time ~/build-index-multi ~/test-multi ~/pbf/planet.osm.pbf --multi-output --continents 2>&1" 2>&1

Run planet multi-output + continents


Command running in background with ID: b6y67lm4h. Output is being written to: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Pass|Processing' | tail -5

Check after 30s


Processing /home/michtest/pbf/planet.osm.pbf...
  Pass 1: scanning relations...


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -5

Check after 60s


[47.7s] Pass 1: relation scanning


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -5

Check after 90s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -5

Check after 120s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -5

Check after 150s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -5

Check after 180s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Node proc' | tail -5

Check after 210s


  [47.7s] Pass 1: relation scanning
  Node processing complete: 78736755 address points collected.
  [186.0s] Pass 2: node processing


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:' | tail -10

Check after 240s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:' | tail -10

Check after 270s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:' | tail -10

Check after 300s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:' | tail -10

Check after 330s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:|S2:|Rebuild' | tail -10

Check after 360s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify
  [2.3s]     Admin: append + submit S2


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:|S2:|Rebuild|Dedup' | tail -10

Check after 390s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Admin:|S2:|Rebuild|Dedup' | tail -10

Check after 420s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify
  [2.3s]     Admin: append + submit S2
  [70.2s]     Admin: S2 covering drain
  [0.6s] Admin assembly


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|S2:|Rebuild|Dedup' | tail -10

Check after 450s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|S2:|Rebuild|Dedup' | tail -10

Check after 480s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|S2:|Rebuild|Dedup|Rebuild' | tail -10

Check after 510s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify
  [2.3s]     Admin: append + submit S2
  [70.2s]     Admin: S2 covering drain
  [0.6s] Admin assembly
  [137.0s]   S2: street ways (parallel)
  [0.0s]   S2: interp ways (parallel)


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|S2:|Rebuild|Dedup|Writing|Filtering|continent' | tail -15

Check after 540s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify
  [2.3s]     Admin: append + submit S2
  [70.2s]     Admin: S2 covering drain
  [0.6s] Admin assembly
  [137.0s]   S2: street ways (parallel)
  [0.0s]   S2: interp ways (parallel)
  [8.8s]   S2: sort + group into cell maps


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[|Rebuild|Dedup|Writing|Filtering|continent|Done' | tail -15

Check after 570s


  [47.7s] Pass 1: relation scanning
  [186.0s] Pass 2: node processing
  [136.9s] Pass 2b: way processing
  [2.7s]     Admin: ring assembly
  [8.7s]     Admin: parallel simplify
  [2.3s]     Admin: append + submit S2
Done reading:
  [70.2s]     Admin: S2 covering drain
  [0.6s] Admin assembly
  [137.0s]   S2: street ways (parallel)
  [0.0s]   S2: interp ways (parallel)
  [8.8s]   S2: sort + group into cell maps


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Rebuild|Dedup|Writing|Filtering|continent|Done\.' | tail -10

Check after 600s


(Bash completed with no output)


cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -15

Check tail of output


  [70.2s]     Admin: S2 covering drain
Admin polygon S2 covering complete (2049797 cells).
  [0.6s] Admin assembly
Computing S2 cells for ways with 28 threads...
  Processing 47826371 street ways...
  [137.0s]   S2: street ways (parallel)
  Processing 72817 interpolation ways...
  [0.0s]   S2: interp ways (parallel)
  Sorting and grouping cell pairs...
  [8.8s]   S2: sort + group into cell maps
S2 cell computation complete.
Resolving interpolation endpoints...
Resolved 41798/72817 interpolation ways
  [253.2s] S2 cell computation
Deduplicating + sorting for write...


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -15

Check after 630s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -15

Check after 660s


Admin polygon S2 covering complete (2049797 cells).
  [0.6s] Admin assembly
Computing S2 cells for ways with 28 threads...
  Processing 47826371 street ways...
  [137.0s]   S2: street ways (parallel)
  Processing 72817 interpolation ways...
  [0.0s]   S2: interp ways (parallel)
  Sorting and grouping cell pairs...
  [8.8s]   S2: sort + group into cell maps
S2 cell computation complete.
Resolving interpolation endpoints...
Resolved 41798/72817 interpolation ways
  [253.2s] S2 cell computation
Deduplicating + sorting for write...
Rebuilding cell maps for continent filtering...


Good — the rebuild step is running. Let me wait for it to progress through writing and continent filtering:


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Rebuild|Dedup|Writing|Filtering|continent|Done' | tail -15

Check after 690s


Done reading:
Deduplicating + sorting for write...
Rebuilding cell maps for continent filtering...


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Rebuild|Dedup|Writing|Filtering|continent|Done|geo index|admin index' | tail -15

Check after 720s


Still rebuilding hash maps. This involves iterating ~300M sorted pairs.


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Rebuild|Dedup|Writing|Filtering|continent|Done|geo index|admin index' | tail -15

Check after 750s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '\[.*Rebuild|Dedup|Writing|Filtering|continent|Done\.|geo index|admin index' | tail -15

Check after 780s


Deduplicating + sorting for write...
Rebuilding cell maps for continent filtering...


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E '^\s+\[' | tail -15

Check phase timings after 810s


  [0.6s] Admin assembly
  [137.0s]   S2: street ways (parallel)
  [0.0s]   S2: interp ways (parallel)
  [8.8s]   S2: sort + group into cell maps
  [253.2s] S2 cell computation
  [232.3s] Rebuild cell maps
  [0.0s] Deduplication
  [0.0s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched
  [5.3s]   Write: entry files
  [7.6s]   Write: entry files
  [5.0s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched
  [3.8s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched


The rebuild took 232s — that's expensive. Writing the 3 planet modes and now starting on continents. Let me wait for completion:


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Filtering|continent|Done\.' | tail -15

Check continent progress after 840s


Rebuilding cell maps for continent filtering...
Filtering for continent: africa...
Filtering for continent: asia...
Filtering for continent: europe...
Filtering for continent: north-america...
Filtering for continent: south-america...
Filtering for continent: central-america...
Filtering for continent: oceania...
Filtering for continent: antarctica...


All 8 continents started. Let me wait for them to finish:


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real' | tail -15

Check after 870s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real' | tail -15

Check after 900s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done|real|Index' | tail -15

Check after 930s


Done reading:


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -10

Check tail after 960s


  [0.0s]   Write: parallel data files launched
strings.bin: 9538440 bytes
strings.bin: 9538440 bytes
admin index: 41256 cells, 32336 polygons
admin index: 41256 cells, 32336 polygons
Done continent: central-america
  [0.0s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched
strings.bin: 17882061 bytes
admin index: 438351 cells, 61191 polygons


Still writing continent indexes. Let me wait longer:


sleep 60 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real|user|sys' | tail -15

Check after 1020s


Done continent: antarctica
Done continent: central-america
Done continent: africa
Done continent: oceania
Done continent: south-america


sleep 60 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real|user|sys' | tail -15

Check after 1080s


sleep 60 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real|user|sys' | tail -15

Check after 1140s


Done continent: antarctica
Done continent: central-america
Done continent: africa
Done continent: oceania
Done continent: south-america
Done continent: asia


sleep 60 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Done continent|Done\.|real|user|sys' | tail -15

Check after 1200s


Done continent: antarctica
Done continent: central-america
Done continent: africa
Done continent: oceania
Done continent: south-america
Done continent: asia
Done continent: europe
Done continent: north-america


sleep 10 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -5

Get final timing


  [1.5s]   Write: geo_cells.bin
  [0.0s]   Write: parallel data files launched
strings.bin: 56226700 bytes
admin index: 372931 cells, 100525 polygons
Done continent: north-america


sleep 10 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -5

Check if done


sleep 10 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'Index file|Done\.|real|user|sys' | tail -5

Check for final Done


(Bash completed with no output)


ssh node3 "ps aux | grep build-index-multi | grep -v grep" 2>&1

Check if still running


michtest   41088  0.0  0.0   7076  2912 ?        Ss   09:51   0:00 bash -c rm -rf ~/test-multi && mkdir ~/test-multi && time ~/build-index-multi ~/test-multi ~/pbf/planet.osm.pbf --multi-output --continents 2>&1
michtest   41091  979 45.7 89370268 83965792 ?   Sl   09:51 240:50 /home/michtest/build-index-multi /home/michtest/test-multi /home/michtest/pbf/planet.osm.pbf --multi-output --continents


Still running — looks like the `Index file writing` log_phase hasn't fired yet. Probably writing the last continent's files. Let me wait:


sleep 30 && ssh node3 "ps aux | grep build-index-multi | grep -v grep | wc -l" 2>&1

Check if done after 30s


sleep 30 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'real|Done\.' | tail -5

Check for completion


Done.


sleep 10 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | grep -E 'real|user|sys'

Get total time


sleep 15 && cat /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output 2>/dev/null | tail -5

Get tail


Done.

real	26m3.755s
user	222m19.888s
sys	19m59.409s


26 minutes total for everything. Now let me generate the size report:


ssh node3 "python3 << 'PYEOF'
import os

base = '/home/michtest/test-multi'
modes = ['full', 'no-addresses', 'admin-only']
continents = ['africa', 'asia', 'europe', 'north-america', 'south-america', 'oceania', 'central-america', 'antarctica']

def dir_size(path):
    total = 0
    if not os.path.exists(path):
        return 0
    for f in os.listdir(path):
        fp = os.path.join(path, f)
        if os.path.isfile(fp):
            total += os.path.getsize(fp)
    return total

def human(b):
    if b == 0: return '0 B'
    for unit in ['B', 'KiB', 'MiB', 'GiB']:
        if b < 1024: return f'{b:.1f} {unit}'
        b /= 1024
    return f'{b:.1f} TiB'

# Planet-level
print('## Planet Index Sizes')
print()
print(f'{\"Region\":<20} {\"Full\":>12} {\"Streets\":>12} {\"Admin Only\":>12}')
print(f'{\"------\":<20} {\"----\":>12} {\"-------\":>12} {\"----------\":>12}')

planet_sizes = {}
for mode in modes:
    path = os.path.join(base, mode)
    sz = dir_size(path)
    planet_sizes[mode] = sz

print(f'{\"Planet\":<20} {human(planet_sizes[\"full\"]):>12} {human(planet_sizes[\"no-addresses\"]):>12} {human(planet_sizes[\"admin-only\"]):>12}')
print()

# Per-continent
print('## Per-Continent Index Sizes')
print()
print(f'{\"Continent\":<20} {\"Full\":>12} {\"Streets\":>12} {\"Admin Only\":>12}')
print(f'{\"------\":<20} {\"----\":>12} {\"-------\":>12} {\"----------\":>12}')

total = {m: 0 for m in modes}
for cont in continents:
    sizes = {}
    for mode in modes:
        path = os.path.join(base, cont, mode)
        sz = dir_size(path)
        sizes[mode] = sz
        total[mode] += sz
    print(f'{cont:<20} {human(sizes[\"full\"]):>12} {human(sizes[\"no-addresses\"]):>12} {human(sizes[\"admin-only\"]):>12}')

print(f'{\"------\":<20} {\"----\":>12} {\"-------\":>12} {\"----------\":>12}')
print(f'{\"All continents\":<20} {human(total[\"full\"]):>12} {human(total[\"no-addresses\"]):>12} {human(total[\"admin-only\"]):>12}')
print()

# Detailed planet file breakdown
print('## Planet File Breakdown')
print()
print(f'{\"File\":<24} {\"Full\":>12} {\"Streets\":>12} {\"Admin Only\":>12}')
print(f'{\"----\":<24} {\"----\":>12} {\"-------\":>12} {\"----------\":>12}')
files = ['admin_cells.bin', 'admin_entries.bin', 'admin_polygons.bin', 'admin_vertices.bin',
         'addr_entries.bin', 'addr_points.bin', 'geo_cells.bin', 'interp_entries.bin',
         'interp_nodes.bin', 'interp_ways.bin', 'street_entries.bin', 'street_nodes.bin',
         'street_ways.bin', 'strings.bin']
for f in files:
    sizes = {}
    for mode in modes:
        fp = os.path.join(base, mode, f)
        sizes[mode] = os.path.getsize(fp) if os.path.exists(fp) else 0
    print(f'{f:<24} {human(sizes[\"full\"]):>12} {human(sizes[\"no-addresses\"]):>12} {human(sizes[\"admin-only\"]):>12}')
PYEOF" 2>&1

Generate comprehensive size report


## Planet Index Sizes

Region                       Full      Streets   Admin Only
------                       ----      -------   ----------
Planet                   17.1 GiB     13.7 GiB      1.7 GiB

## Per-Continent Index Sizes

Continent                    Full      Streets   Admin Only
------                       ----      -------   ----------
africa                  763.3 MiB    696.4 MiB    118.5 MiB
asia                      3.7 GiB      3.1 GiB    501.8 MiB
europe                    6.2 GiB      4.4 GiB    882.8 MiB
north-america             5.9 GiB      4.9 GiB    175.0 MiB
south-america             1.2 GiB      1.1 GiB    143.5 MiB
oceania                 789.6 MiB    649.3 MiB     59.4 MiB
central-america         431.8 MiB    400.4 MiB     50.0 MiB
antarctica                2.3 MiB      2.3 MiB      741.0 B
------                       ----      -------   ----------
All continents           19.0 GiB     15.3 GiB      1.9 GiB

## Planet File Breakdown

File                             Full      Streets   Admin Only
----                             ----      -------   ----------
admin_cells.bin              23.5 MiB     23.5 MiB     23.5 MiB
admin_entries.bin            42.5 MiB     42.5 MiB     42.5 MiB
admin_polygons.bin           21.7 MiB     21.7 MiB     21.7 MiB
admin_vertices.bin            1.4 GiB      1.4 GiB      1.4 GiB
addr_entries.bin            704.0 MiB          0 B          0 B
addr_points.bin               2.4 GiB          0 B          0 B
geo_cells.bin                 6.0 GiB      5.7 GiB          0 B
interp_entries.bin            1.3 MiB          0 B          0 B
interp_nodes.bin              3.3 MiB          0 B          0 B
interp_ways.bin               1.7 MiB          0 B          0 B
street_entries.bin            2.1 GiB      2.1 GiB          0 B
street_nodes.bin              3.8 GiB      3.8 GiB          0 B
street_ways.bin             547.3 MiB    547.3 MiB          0 B
strings.bin                 204.6 MiB    204.6 MiB    204.6 MiB


Now let me commit the fix and present the results:


cd /root/traccar-geocoder && git add builder/src/build_index.cpp && git commit -m "$(cat <<'EOF'
fix: rebuild cell hash maps for continent filtering mode

parallel_sort_and_build skips populating cell_to_ways and cell_to_interps
hash maps (using sorted pairs for direct writing instead). But continent
filtering via filter_by_bbox needs those hash maps to subset the data.

Rebuild them from sorted pairs when --continents or --save-cache is used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EOF
)" && git push origin main 2>&1

Commit and push continent fix


[main b29b951] fix: rebuild cell hash maps for continent filtering mode
 1 file changed, 16 insertions(+)
To github.com:zp-forks/traccar-geocoder.git
   51d3db9..b29b951  main -> main


Here are the results. Single planet run, **26 minutes** total for all configurations:

## Planet Index Sizes

| Region | Full | Streets only | Admin only |
|---|---|---|---|
| **Planet** | **17.1 GiB** | **13.7 GiB** | **1.7 GiB** |

## Per-Continent Breakdown

| Continent | Full | Streets only | Admin only |
|---|---|---|---|
| Europe | 6.2 GiB | 4.4 GiB | 883 MiB |
| North America | 5.9 GiB | 4.9 GiB | 175 MiB |
| Asia | 3.7 GiB | 3.1 GiB | 502 MiB |
| South America | 1.2 GiB | 1.1 GiB | 144 MiB |
| Oceania | 790 MiB | 649 MiB | 59 MiB |
| Africa | 763 MiB | 696 MiB | 119 MiB |
| Central America | 432 MiB | 400 MiB | 50 MiB |
| Antarctica | 2.3 MiB | 2.3 MiB | ~1 KiB |
| **All continents** | **19.0 GiB** | **15.3 GiB** | **1.9 GiB** |

(Continent totals exceed planet because bounding boxes overlap — e.g. Turkey appears in both Europe and Asia.)

## What each mode includes

| Mode | Streets | Addresses | Interpolation | Admin boundaries |
|---|---|---|---|---|
| **Full** | yes | yes | yes | yes |
| **Streets only** | yes | no | no | yes |
| **Admin only** | no | no | no | yes |

Dropping addresses saves ~3.4 GiB (20%) at planet scale. Admin-only is just 1.7 GiB — useful for country/city-level reverse geocoding without street detail.


<task-notification>
<task-id>b6y67lm4h</task-id>
<tool-use-id>toolu_01YRchJFF72T3EnJTRExQCJq</tool-use-id>
<output-file>/tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output</output-file>
<status>completed</status>
<summary>Background command "Run planet multi-output + continents" completed (exit code 0)</summary>
</task-notification>
Read the output file to retrieve the result: /tmp/claude-0/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tasks/b6y67lm4h.output


Already reported — 26 minutes, all 33 index sets (3 modes × (planet + 8 continents) + planet base) generated successfully. Results summarized above.


What exactly does "admin only" include, what level of detail do we get? Right now we use the cities500 extract from geonames for positioning, specifically we use cities500 + admin2codes + admin1codesascii. Is this new implementation of admin only significantly higher detail? Could we easily create a more accurate version of something similar in level to cities500?


Let me check what admin levels we have and compare to cities500.


ssh node3 "python3 << 'PYEOF'
import struct

# Read admin polygons from the admin-only index
data = open('/home/michtest/test-multi/admin-only/admin_polygons.bin', 'rb').read()
strings = open('/home/michtest/test-multi/admin-only/strings.bin', 'rb').read()

# AdminPolygon: 24 bytes
# u32 vertex_offset, u16 vertex_count, u32 name_id, u8 admin_level, f32 area, u16 country_code
# Need to figure out exact layout with padding

count = len(data) // 24
print(f'Total admin polygons: {count:,}')
print()

# Count by admin level
levels = {}
for i in range(count):
    off = i * 24
    # Try reading admin_level at offset 10 (after u32+u16+u32 = 10 bytes)
    level = data[off + 10]
    levels[level] = levels.get(level, 0) + 1

print('Polygons by admin level:')
print(f'  {\"Level\":<8} {\"Count\":>10}  Description')
print(f'  {\"-----\":<8} {\"-----\":>10}  -----------')
descs = {
    2: 'Country',
    3: 'Region/State (large)',
    4: 'State/Province',
    5: 'District/Region',
    6: 'County/District',
    7: 'Municipality',
    8: 'City/Town/Village',
    9: 'City district/Suburb',
    10: 'Neighborhood/Ward',
    11: 'Postal code',
}
for level in sorted(levels.keys()):
    desc = descs.get(level, '???')
    print(f'  {level:<8} {levels[level]:>10,}  {desc}')

print()
total_non_postal = sum(v for k,v in levels.items() if k != 11)
print(f'Total admin (non-postal): {total_non_postal:,}')
print(f'Total postal codes: {levels.get(11, 0):,}')

# Sample some entries at each level
print()
print('Sample names by level:')
for level in sorted(levels.keys()):
    samples = []
    for i in range(count):
        off = i * 24
        if data[off + 10] == level:
            name_id = struct.unpack_from('<I', data, off + 6)[0]
            cc_raw = struct.unpack_from('<H', data, off + 15)[0]
            if name_id < len(strings):
                end = strings.index(b'\x00', name_id)
                name = strings[name_id:end].decode('utf-8', errors='replace')
                cc = ''
                if cc_raw > 0:
                    cc = chr(cc_raw >> 8) + chr(cc_raw & 0xFF)
                samples.append(f'{name} ({cc})' if cc else name)
            if len(samples) >= 5:
                break
    print(f'  Level {level}: {", ".join(samples)}')
PYEOF" 2>&1

Analyze admin polygon levels and detail


Error: Exit code 1
  File "<stdin>", line 66
    print(f'  Level {level}: {, .join(samples)}')
                              ^
SyntaxError: f-string: expecting a valid expression after '{'


ssh node3 "python3 << 'PYEOF'
import struct

data = open('/home/michtest/test-multi/admin-only/admin_polygons.bin', 'rb').read()
strings = open('/home/michtest/test-multi/admin-only/strings.bin', 'rb').read()

count = len(data) // 24
print(f'Total admin polygons: {count:,}')
print()

def get_str(off):
    if off >= len(strings): return '???'
    end = strings.index(b'\\x00', off)
    return strings[off:end].decode('utf-8', errors='replace')

# Count by admin level
levels = {}
level_samples = {}
for i in range(count):
    off = i * 24
    level = data[off + 10]
    levels[level] = levels.get(level, 0) + 1
    if level not in level_samples or len(level_samples[level]) < 5:
        name_id = struct.unpack_from('<I', data, off + 6)[0]
        cc_raw = struct.unpack_from('<H', data, off + 15)[0]
        name = get_str(name_id)
        cc = ''
        if cc_raw > 0:
            cc = chr(cc_raw >> 8) + chr(cc_raw & 0xFF)
        entry = name + ' (' + cc + ')' if cc else name
        if level not in level_samples:
            level_samples[level] = []
        level_samples[level].append(entry)

descs = {
    2: 'Country',
    3: 'Region/State (large)',
    4: 'State/Province',
    5: 'District/Region',
    6: 'County/District',
    7: 'Municipality',
    8: 'City/Town/Village',
    9: 'City district/Suburb',
    10: 'Neighborhood/Ward',
    11: 'Postal code',
}

print('Polygons by admin level:')
for level in sorted(levels.keys()):
    desc = descs.get(level, '???')
    print(f'  Level {level:>2}: {levels[level]:>10,}  {desc}')
    for s in level_samples.get(level, []):
        print(f'           {s}')
    print()

total_non_postal = sum(v for k,v in levels.items() if k != 11)
print(f'Total admin (excl postal): {total_non_postal:,}')
print(f'Total postal codes: {levels.get(11, 0):,}')
PYEOF" 2>&1

Analyze admin levels with samples


<persisted-output>
Output too large (33.7KB). Full output saved to: /root/.claude/projects/-root-traccar-geocoder/af1c4baa-0f9a-4420-a8b3-ce20dc28c8b9/tool-results/bddxrf97v.txt

Preview (first 2KB):
Total admin polygons: 947,358

Polygons by admin level:
  Level  0:      1,026  ???
           ie (&)
           ??? (')
           ??? (Ä)
           os Alazanes ()
           ??? (ê)

  Level  1:        701  ???
           4 ()
           ??? ()
           ??? (v)
           4 ()
           ??? ()

  Level  2:        910  Country
           ??? (¯)
           ??? (§)
           ??? (õ)
           ??? (Ó)
           ??? (:)

  Level  3:      1,659  Region/State (large)
           ??? ()
           ??? (c)
           ??? (e)
           ??? (`)
           ??? ()

  Level  4:      2,076  State/Province
           ??? (h)
           ??? (ñ)
           ??? ()
           ??? (C)
           ??? (Á)

  Level  5:      2,459  District/Region
           ??? (X)
           ??? (þ)
           ??? (À)
           ??? (Ù)
           ??? ()

  Level  6:      1,743  County/District
           Severino Domingos de Castro ()
           ??? (3)
           d 29 ()
           ??? (C)
           ??? ()

  Level  7:        859  Municipality
           ??? ()
           ??? ()
           ??? (W)
           ??? (þ)
           ??? (û)

  Level  8:        894  City/Town/Village
           ??? (Á)
           ??? ())
           ??? (
)
           ??? (ä)
           ??? (¶)

  Level  9:      3,963  City district/Suburb
           ??? (¿)
           ??? (Ù)
           ??? ()
           ??? (á)
           ??? (ø)

  Level 10:      3,962  Neighborhood/Ward
           ue du Clos de l'Écluse (4)
           ??? (Ö)
           ??? (f)
           ??? (+)
           ??? (n)

  Level 11:      1,203  Postal code
           ??? (0)
           ??? ()
           ??? (¸)
           ??? (r)
           ??? (Ð)

  Level 12:        734  ???
           ??? (&)
           ??? ()
           n Poonshof (|)
           9 (±)
           ??? ()

  Level 13:        755  ???
           ??? ()
           ??? (ð)
           ??? (¡)
...
</persisted-output>


The struct layout is wrong — I'm getting garbage. Let me check the actual AdminPolygon layout: