  1630→int main(int argc, char* argv[]) {
  1631→    if (argc < 2) {
  1632→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
  1633→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
  1634→        std::cerr << std::endl;
  1635→        std::cerr << "Options:" << std::endl;
  1636→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
  1637→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
  1638→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
  1639→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
  1640→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
  1641→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
  1642→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
  1643→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
  1644→        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
  1645→        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
  1646→        return 1;
  1647→    }
  1648→
  1649→    std::string output_dir = argv[1];
  1650→    std::vector<std::string> input_files;
  1651→    std::string save_cache_path;
  1652→    std::string load_cache_path;
  1653→    bool multi_output = false;
  1654→    bool generate_continents = false;
  1655→    IndexMode mode = IndexMode::Full;
  1656→
  1657→    for (int i = 2; i < argc; i++) {
  1658→        std::string arg = argv[i];
  1659→        if (arg == "--street-level" && i + 1 < argc) {
  1660→            kStreetCellLevel = std::atoi(argv[++i]);
  1661→        } else if (arg == "--admin-level" && i + 1 < argc) {
  1662→            kAdminCellLevel = std::atoi(argv[++i]);
  1663→        } else if (arg == "--admin-only") {
  1664→            mode = IndexMode::AdminOnly;
  1665→        } else if (arg == "--no-addresses") {
  1666→            mode = IndexMode::NoAddresses;
  1667→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
  1668→            kMaxAdminLevel = std::atoi(argv[++i]);
  1669→        } else if (arg == "--save-cache" && i + 1 < argc) {
  1670→            save_cache_path = argv[++i];
  1671→        } else if (arg == "--load-cache" && i + 1 < argc) {
  1672→            load_cache_path = argv[++i];
  1673→        } else if (arg == "--multi-output") {
  1674→            multi_output = true;
  1675→        } else if (arg == "--continents") {
  1676→            generate_continents = true;
  1677→        } else if (arg == "--mode" && i + 1 < argc) {
  1678→            std::string mode_str = argv[++i];
  1679→            if (mode_str == "full") {
  1680→                mode = IndexMode::Full;
  1681→            } else if (mode_str == "no-addresses") {
  1682→                mode = IndexMode::NoAddresses;
  1683→            } else if (mode_str == "admin-only") {
  1684→                mode = IndexMode::AdminOnly;
  1685→            } else {
  1686→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
  1687→                return 1;
  1688→            }
  1689→        } else {
  1690→            input_files.push_back(arg);
  1691→        }
  1692→    }
  1693→
  1694→    ParsedData data;
  1695→    auto _pt = std::chrono::steady_clock::now();
  1696→
  1697→    if (!load_cache_path.empty()) {
  1698→        // Load from cache
  1699→        if (!deserialize_cache(data, load_cache_path)) {
  1700→            std::cerr << "Error: failed to load cache" << std::endl;
  1701→            return 1;
  1702→        }
  1703→    } else {
  1704→        // Parse PBF files (always collect everything)
  1705→        if (input_files.empty()) {
  1706→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
  1707→            return 1;
  1708→        }
  1709→
  1710→        // Create thread pool for concurrent admin polygon S2 covering
  1711→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
  1712→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
  1713→        AdminCoverPool admin_pool(num_threads);
  1714→        // BuildHandler no longer used — parallel processing handles everything
  1715→
  1716→        // Dense array node location index — lockless parallel writes
  1717→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per Location = 100GB virtual.
  1718→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
  1719→        static const size_t MAX_NODE_ID = MAX_NODE_ID_DEFAULT;
  1720→        struct DenseIndex {
  1721→            osmium::Location* data;
  1722→            size_t capacity;
  1723→
  1724→            DenseIndex() : capacity(MAX_NODE_ID) {
  1725→                void* ptr = mmap(nullptr, capacity * sizeof(osmium::Location),
  1726→                    PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
  1727→                    -1, 0);
  1728→                if (ptr == MAP_FAILED) {
  1729→                    std::cerr << "Error: failed to mmap dense node index ("
  1730→                              << (capacity * sizeof(osmium::Location) / (1024*1024*1024)) << "GB virtual)" << std::endl;
  1731→                    std::exit(1);
  1732→                }
  1733→                data = static_cast<osmium::Location*>(ptr);
  1734→                std::cerr << "Allocated dense node index: " << (capacity * sizeof(osmium::Location) / (1024*1024*1024))
  1735→                          << "GB virtual address space" << std::endl;
  1736→            }
  1737→
  1738→            ~DenseIndex() {
  1739→                munmap(data, capacity * sizeof(osmium::Location));
  1740→            }
  1741→
  1742→            // Lockless — each node ID maps to a unique array slot
  1743→            void set(osmium::unsigned_object_id_type id, osmium::Location loc) {
  1744→                if (id >= capacity) {
  1745→                    std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  1746→                    std::exit(1);
  1747→                }
  1748→                data[id] = loc;
  1749→            }
  1750→
  1751→            osmium::Location get(osmium::unsigned_object_id_type id) const {
  1752→                if (id >= capacity) return osmium::Location{};
  1753→                return data[id];
  1754→            }
  1755→
  1756→            void set_batch(const std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>>& batch) {
  1757→                for (const auto& [id, loc] : batch) {
  1758→                    if (id >= capacity) {
  1759→                        std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  1760→                        std::exit(1);
  1761→                    }
  1762→                    data[id] = loc;
  1763→                }
  1764→            }
  1765→        } index;
  1766→
  1767→        for (const auto& input_file : input_files) {
  1768→            std::cerr << "Processing " << input_file << "..." << std::endl;
  1769→
  1770→            // --- Pass 1: collect relation members for parallel admin assembly ---
  1771→            std::cerr << "  Pass 1: scanning relations..." << std::endl;
  1772→            RelationCollector rel_collector(data.collected_relations);
  1773→
  1774→            {
  1775→                osmium::io::Reader reader1{input_file, osmium::osm_entity_bits::relation};
  1776→                osmium::apply(reader1, rel_collector);
  1777→                reader1.close();
  1778→            }
  1779→            std::cerr << "  Collected " << data.collected_relations.size()
  1780→                      << " admin/postal relations for parallel assembly." << std::endl;
  1781→
  1782→            // --- Combined Pass 2+3: nodes then ways in a single PBF read ---
  1783→            // PBF guarantees ordering: nodes → ways → relations.
  1784→            // We read everything in one pass, processing nodes first, then
  1785→            // transitioning to way processing when the first way block arrives.
  1786→            log_phase("Pass 1: relation scanning", _pt);
  1787→
  1788→            // Build admin_way_ids BEFORE the combined pass (needed during way processing)
  1789→            std::unordered_set<int64_t> admin_way_ids;
  1790→            for (const auto& rel : data.collected_relations) {
  1791→                for (const auto& [way_id, role] : rel.members) {
  1792→                    admin_way_ids.insert(way_id);
  1793→                }
  1794→            }
  1795→            std::cerr << "  Admin assembly needs " << admin_way_ids.size() << " way geometries." << std::endl;
  1796→
  1797→            std::cerr << "  Pass 2+3: processing nodes and ways in single read..." << std::endl;
  1798→
  1799→            {
  1800→                // Shared queue infrastructure for both phases
  1801→                std::mutex queue_mutex;
  1802→                std::condition_variable queue_cv;
  1803→                std::deque<osmium::memory::Buffer> block_queue;
  1804→                bool reader_done = false;
  1805→                const size_t MAX_QUEUE = MAX_BLOCK_QUEUE;
  1806→
  1807→                struct NodeThreadLocal {
  1808→                    std::vector<std::pair<double,double>> addr_coords;
  1809→                    std::vector<std::pair<std::string,std::string>> addr_strings;
  1810→                    uint64_t count = 0;
  1811→                };
  1812→                std::vector<NodeThreadLocal> ntld(num_threads);
  1813→                std::atomic<uint64_t> blocks_done{0};
  1814→
  1815→                // Worker threads
  1816→                std::vector<std::thread> node_workers;
  1817→                for (unsigned int t = 0; t < num_threads; t++) {
  1818→                    node_workers.emplace_back([&, t]() {
  1819→                        auto& local = ntld[t];
  1820→                        std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>> loc_batch;
  1821→                        loc_batch.reserve(8192);
  1822→
  1823→                        while (true) {
  1824→                            osmium::memory::Buffer block;
  1825→                            {
  1826→                                std::unique_lock<std::mutex> lock(queue_mutex);
  1827→                                queue_cv.wait(lock, [&]{ return !block_queue.empty() || reader_done; });
  1828→                                if (block_queue.empty() && reader_done) break;
  1829→                                block = std::move(block_queue.front());
  1830→                                block_queue.pop_front();
  1831→                            }
  1832→                            queue_cv.notify_one(); // wake reader if it was waiting
  1833→
  1834→                            for (const auto& item : block) {
  1835→                                if (item.type() == osmium::item_type::node) {
  1836→                                    const auto& node = static_cast<const osmium::Node&>(item);
  1837→                                    if (!node.location().valid()) continue;
  1838→                                    loc_batch.push_back({node.positive_id(), node.location()});
  1839→
  1840→                                    const char* housenumber = node.tags()["addr:housenumber"];
  1841→                                    if (housenumber) {
  1842→                                        const char* street = node.tags()["addr:street"];
  1843→                                        if (street) {
  1844→                                            local.addr_coords.push_back({node.location().lat(), node.location().lon()});
  1845→                                            local.addr_strings.push_back({housenumber, street});
  1846→                                            local.count++;
  1847→                                        }
  1848→                                    }
  1849→                                }
  1850→                            }
  1851→
  1852→                            // Flush location batch (sharded index handles its own locking)
  1853→                            if (!loc_batch.empty()) {
  1854→                                index.set_batch(loc_batch);
  1855→                                loc_batch.clear();
  1856→                            }
  1857→
  1858→                            uint64_t n = blocks_done.fetch_add(1) + 1;
  1859→                            if (n % 1000 == 0) {
  1860→                                std::cerr << "  Processed " << n << " node blocks..." << std::endl;
  1861→                            }
  1862→                        }
  1863→
  1864→                        // Flush remaining
  1865→                        if (!loc_batch.empty()) {
  1866→                            index.set_batch(loc_batch);
  1867→                        }
  1868→                    });
  1869→                }
  1870→
  1871→                // Single reader for all entity types — processes nodes first,
  1872→                // then transitions to way processing when first way block arrives.
  1873→                osmium::io::Reader combined_reader{input_file};
  1874→                bool nodes_done = false;
  1875→                osmium::memory::Buffer first_way_buf; // stash the first way block
  1876→
  1877→                // Phase 1: Feed node blocks to node workers
  1878→                while (auto buf = combined_reader.read()) {
  1879→                    // Check if this buffer contains nodes or ways/relations
  1880→                    bool has_node = false;
  1881→                    for (const auto& item : buf) {
  1882→                        if (item.type() == osmium::item_type::node) { has_node = true; break; }
  1883→                    }
  1884→
  1885→                    if (has_node) {
  1886→                        std::unique_lock<std::mutex> lock(queue_mutex);
  1887→                        queue_cv.wait(lock, [&]{ return block_queue.size() < MAX_QUEUE; });
  1888→                        block_queue.push_back(std::move(buf));
  1889→                        queue_cv.notify_one();
  1890→                    } else {
  1891→                        // First non-node block — stash it and transition
  1892→                        first_way_buf = std::move(buf);
  1893→                        nodes_done = true;
  1894→                        break;
  1895→                    }
  1896→                }
  1897→
  1898→                // Signal node workers done
  1899→                {
  1900→                    std::lock_guard<std::mutex> lock(queue_mutex);
  1901→                    reader_done = true;
  1902→                }
  1903→                queue_cv.notify_all();
  1904→                for (auto& w : node_workers) w.join();
  1905→
  1906→                // Merge address points
  1907→                uint64_t total_addrs = 0;
  1908→                for (auto& local : ntld) {
  1909→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
  1910→                        uint64_t dummy = 0;
  1911→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
  1912→                                       local.addr_strings[j].first.c_str(),
  1913→                                       local.addr_strings[j].second.c_str(), dummy);
  1914→                    }
  1915→                    total_addrs += local.count;
  1916→                }
  1917→                std::cerr << "  Node processing complete: " << total_addrs
  1918→                          << " address points collected." << std::endl;
  1919→                log_phase("Pass 2: node processing", _pt);
  1920→                std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
  1921→
  1922→                // --- Phase 2: Way processing using same reader ---
  1923→
  1924→                // Thread-local data for parallel way processing
  1925→                struct ThreadLocalData {
  1926→                    std::vector<WayHeader> ways;
  1927→                    std::vector<NodeCoord> street_nodes;
  1928→                    std::vector<DeferredWay> deferred_ways;
  1929→                    std::vector<InterpWay> interp_ways;
  1930→                    std::vector<NodeCoord> interp_nodes;
  1931→                    std::vector<DeferredInterp> deferred_interps;
  1932→                    std::vector<AddrPoint> building_addrs;
  1933→                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
  1934→                    std::vector<std::string> way_strings;      // way name strings
  1935→                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
  1936→                    std::vector<std::string> interp_strings;   // interp street name strings
  1937→                    uint64_t way_count = 0;
  1938→                    uint64_t building_addr_count = 0;
  1939→                    uint64_t interp_count = 0;
  1940→                    // Way geometries for parallel admin assembly
  1941→                    struct WayGeomEntry {
  1942→                        int64_t way_id;
  1943→                        std::vector<std::pair<double,double>> coords;
  1944→                        int64_t first_node_id;
  1945→                        int64_t last_node_id;
  1946→                    };
  1947→                    std::vector<WayGeomEntry> way_geoms;
  1948→                    // Closed-way admin polygons (ways with boundary=administrative)
  1949→                    struct ClosedWayAdmin {
  1950→                        std::vector<std::pair<double,double>> vertices;
  1951→                        std::string name;
  1952→                        uint8_t admin_level;
  1953→                        std::string country_code;
  1954→                    };
  1955→                    std::vector<ClosedWayAdmin> closed_way_admins;
  1956→                };
  1957→
  1958→                // Read buffers and dispatch to thread pool
  1959→                // Streaming producer-consumer for way blocks
  1960→                std::mutex way_queue_mutex;
  1961→                std::condition_variable way_queue_cv;
  1962→                std::deque<osmium::memory::Buffer> way_block_queue;
  1963→                bool way_reader_done = false;
  1964→                const size_t WAY_MAX_QUEUE = MAX_BLOCK_QUEUE;
  1965→
  1966→                std::vector<ThreadLocalData> tld(num_threads);
  1967→                std::atomic<uint64_t> way_blocks_done{0};
  1968→
  1969→                std::vector<std::thread> workers;
  1970→                for (unsigned int t = 0; t < num_threads; t++) {
  1971→                    workers.emplace_back([&, t]() {
  1972→                        auto& local = tld[t];
  1973→                        while (true) {
  1974→                            osmium::memory::Buffer block;
  1975→                            {
  1976→                                std::unique_lock<std::mutex> lock(way_queue_mutex);
  1977→                                way_queue_cv.wait(lock, [&]{ return !way_block_queue.empty() || way_reader_done; });
  1978→                                if (way_block_queue.empty() && way_reader_done) break;
  1979→                                block = std::move(way_block_queue.front());
  1980→                                way_block_queue.pop_front();
  1981→                            }
  1982→                            way_queue_cv.notify_one();
  1983→
  1984→                            for (const auto& item : block) {
  1985→                                if (item.type() == osmium::item_type::way) {
  1986→                                    const auto& way = static_cast<const osmium::Way&>(item);
  1987→
  1988→                                    // Resolve node locations (read-only from shared index)
  1989→                                    // We need to check if nodes have valid locations
  1990→                                    const auto& wnodes = way.nodes();
  1991→
  1992→                                    // Address interpolation
  1993→                                    const char* interpolation = way.tags()["addr:interpolation"];
  1994→                                    if (interpolation) {
  1995→                                        if (wnodes.size() >= 2) {
  1996→                                            bool all_valid = true;
  1997→                                            for (const auto& nr : wnodes) {
  1998→                                                auto loc = index.get(nr.positive_ref());
  1999→                                                if (!loc.valid()) { all_valid = false; break; }
  2000→                                            }
  2001→                                            if (all_valid) {
  2002→                                                const char* street = way.tags()["addr:street"];
  2003→                                                if (street) {
  2004→                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
  2005→                                                    uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
  2006→
  2007→                                                    for (const auto& nr : wnodes) {
  2008→                                                        auto loc = index.get(nr.positive_ref());
  2009→                                                        local.interp_nodes.push_back({
  2010→                                                            static_cast<float>(loc.lat()),
  2011→                                                            static_cast<float>(loc.lon())
  2012→                                                        });
  2013→                                                    }
  2014→
  2015→                                                    uint8_t interp_type = 0;
  2016→                                                    if (std::strcmp(interpolation, "even") == 0) interp_type = 1;
  2017→                                                    else if (std::strcmp(interpolation, "odd") == 0) interp_type = 2;
  2018→
  2019→                                                    InterpWay iw{};
  2020→                                                    iw.node_offset = node_offset;
  2021→                                                    iw.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2022→                                                    iw.street_id = 0; // placeholder, intern later
  2023→                                                    iw.start_number = 0;
  2024→                                                    iw.end_number = 0;
  2025→                                                    iw.interpolation = interp_type;
  2026→                                                    local.interp_ways.push_back(iw);
  2027→                                                    local.interp_strings.push_back(street);
  2028→                                                    local.deferred_interps.push_back({interp_id, node_offset, iw.node_count});
  2029→                                                    local.interp_count++;
  2030→                                                }
  2031→                                            }
  2032→                                        }
  2033→                                        continue;
  2034→                                    }
  2035→
  2036→                                    // Building addresses
  2037→                                    const char* housenumber = way.tags()["addr:housenumber"];
  2038→                                    if (housenumber) {
  2039→                                        const char* street = way.tags()["addr:street"];
  2040→                                        if (street && !wnodes.empty()) {
  2041→                                            double sum_lat = 0, sum_lng = 0;
  2042→                                            int valid = 0;
  2043→                                            for (const auto& nr : wnodes) {
  2044→                                                auto loc = index.get(nr.positive_ref());
  2045→                                                if (loc.valid()) {
  2046→                                                    sum_lat += loc.lat();
  2047→                                                    sum_lng += loc.lon();
  2048→                                                    valid++;
  2049→                                                }
  2050→                                            }
  2051→                                            if (valid > 0) {
  2052→                                                // Store coords for later S2 cell computation
  2053→                                                local.building_addr_coords.push_back({sum_lat / valid, sum_lng / valid});
  2054→                                                // Placeholder addr point — string IDs filled in during merge
  2055→                                                local.building_addrs.push_back({
  2056→                                                    static_cast<float>(sum_lat / valid),
  2057→                                                    static_cast<float>(sum_lng / valid),
  2058→                                                    0, 0 // placeholder string IDs
  2059→                                                });
  2060→                                                // Store strings for later interning
  2061→                                                local.addr_strings.push_back({housenumber, street});
  2062→                                                local.building_addr_count++;
  2063→                                            }
  2064→                                        }
  2065→                                    }
  2066→
  2067→                                    // Highway ways
  2068→                                    const char* highway = way.tags()["highway"];
  2069→                                    if (highway && is_included_highway(highway)) {
  2070→                                        const char* name = way.tags()["name"];
  2071→                                        if (name && wnodes.size() >= 2) {
  2072→                                            bool all_valid = true;
  2073→                                            for (const auto& nr : wnodes) {
  2074→                                                auto loc = index.get(nr.positive_ref());
  2075→                                                if (!loc.valid()) { all_valid = false; break; }
  2076→                                            }
  2077→                                            if (all_valid) {
  2078→                                                uint32_t way_id = static_cast<uint32_t>(local.ways.size());
  2079→                                                uint32_t node_offset = static_cast<uint32_t>(local.street_nodes.size());
  2080→
  2081→                                                for (const auto& nr : wnodes) {
  2082→                                                    auto loc = index.get(nr.positive_ref());
  2083→                                                    local.street_nodes.push_back({
  2084→                                                        static_cast<float>(loc.lat()),
  2085→                                                        static_cast<float>(loc.lon())
  2086→                                                    });
  2087→                                                }
  2088→
  2089→                                                WayHeader header{};
  2090→                                                header.node_offset = node_offset;
  2091→                                                header.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2092→                                                header.name_id = 0; // placeholder, intern later
  2093→                                                local.ways.push_back(header);
  2094→                                                local.way_strings.push_back(name);
  2095→                                                local.deferred_ways.push_back({way_id, node_offset, header.node_count});
  2096→                                                local.way_count++;
  2097→                                            }
  2098→                                        }
  2099→                                    }
  2100→
  2101→                                    // Store way geometry only for admin boundary member ways
  2102→                                    if (!wnodes.empty() && admin_way_ids.count(way.id())) {
  2103→                                        std::vector<std::pair<double,double>> geom;
  2104→                                        for (const auto& nr : wnodes) {
  2105→                                            auto loc = index.get(nr.positive_ref());
  2106→                                            if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
  2107→                                        }
  2108→                                        if (!geom.empty()) {
  2109→                                            int64_t first_nid = wnodes.front().positive_ref();
  2110→                                            int64_t last_nid = wnodes.back().positive_ref();
  2111→                                            local.way_geoms.push_back({way.id(), std::move(geom), first_nid, last_nid});
  2112→                                        }
  2113→                                    }
  2114→
  2115→                                    // Handle closed ways that are admin boundaries themselves
  2116→                                    // Osmium creates areas from closed ways independently of
  2117→                                    // multipolygon relations, even if the way is a relation member.
  2118→                                    {
  2119→                                        const char* boundary = way.tags()["boundary"];
  2120→                                        if (boundary) {
  2121→                                            bool is_admin = (std::strcmp(boundary, "administrative") == 0);
  2122→                                            bool is_postal = (std::strcmp(boundary, "postal_code") == 0);
  2123→                                            if ((is_admin || is_postal) && wnodes.size() >= 4 &&
  2124→                                                wnodes.front().ref() == wnodes.back().ref()) {
  2125→                                                // Closed way forming an admin polygon
  2126→                                                uint8_t al = 0;
  2127→                                                if (is_admin) {
  2128→                                                    const char* level_str = way.tags()["admin_level"];
  2129→                                                    if (level_str) al = static_cast<uint8_t>(std::atoi(level_str));
  2130→                                                } else {
  2131→                                                    al = 11;
  2132→                                                }
  2133→                                                int max_al = is_postal ? 11 : 10;
  2134→                                                if (al >= 2 && al <= max_al && (kMaxAdminLevel == 0 || al <= kMaxAdminLevel)) {
  2135→                                                    const char* aname = way.tags()["name"];
  2136→                                                    if (!aname && is_admin) continue; // admin needs name
  2137→                                                    std::string name_str;
  2138→                                                    if (is_postal) {
  2139→                                                        const char* pc = way.tags()["postal_code"];
  2140→                                                        if (!pc) pc = aname;
  2141→                                                        if (!pc) continue; // postal needs postal_code or name
  2142→                                                        name_str = pc;
  2143→                                                    } else {
  2144→                                                        name_str = aname;
  2145→                                                    }
  2146→                                                    {
  2147→                                                        std::vector<std::pair<double,double>> verts;
  2148→                                                        bool all_valid = true;
  2149→                                                        for (const auto& nr : wnodes) {
  2150→                                                            auto loc = index.get(nr.positive_ref());
  2151→                                                            if (loc.valid()) {
  2152→                                                                verts.push_back({loc.lat(), loc.lon()});
  2153→                                                            } else { all_valid = false; break; }
  2154→                                                        }
  2155→                                                        if (all_valid && verts.size() >= 3) {
  2156→                                                            std::string cc;
  2157→                                                            if (al == 2) {
  2158→                                                                const char* iso = way.tags()["ISO3166-1:alpha2"];
  2159→                                                                if (iso) cc = iso;
  2160→                                                            }
  2161→                                                            local.closed_way_admins.push_back({
  2162→                                                                std::move(verts),
  2163→                                                                std::move(name_str),
  2164→                                                                al, std::move(cc)
  2165→                                                            });
  2166→                                                        }
  2167→                                                    }
  2168→                                                }
  2169→                                            }
  2170→                                        }
  2171→                                    }
  2172→                                }
  2173→                            }
  2174→
  2175→                            uint64_t done = way_blocks_done.fetch_add(1) + 1;
  2176→                            if (done % 1000 == 0) {
  2177→                                std::cerr << "  Processed " << done << " way blocks..." << std::endl;
  2178→                            }
  2179→                        }
  2180→                    });
  2181→                }
  2182→
  2183→                // Way reader: use the combined_reader (continuing from where nodes left off)
  2184→                {
  2185→                    // First, push the stashed way buffer
  2186→                    if (first_way_buf) {
  2187→                        std::unique_lock<std::mutex> lock(way_queue_mutex);
  2188→                        way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2189→                        way_block_queue.push_back(std::move(first_way_buf));
  2190→                        way_queue_cv.notify_one();
  2191→                    }
  2192→                    // Continue reading remaining blocks from the same reader
  2193→                    while (auto buf = combined_reader.read()) {
  2194→                        {
  2195→                            std::unique_lock<std::mutex> lock(way_queue_mutex);
  2196→                            way_queue_cv.wait(lock, [&]{ return way_block_queue.size() < WAY_MAX_QUEUE; });
  2197→                            way_block_queue.push_back(std::move(buf));
  2198→                        }
  2199→                        way_queue_cv.notify_one();
  2200→                    }
  2201→                    combined_reader.close();
  2202→                    {
  2203→                        std::lock_guard<std::mutex> lock(way_queue_mutex);
  2204→                        way_reader_done = true;
  2205→                    }
  2206→                    way_queue_cv.notify_all();
  2207→                }
  2208→
  2209→                for (auto& w : workers) w.join();
  2210→                std::cerr << "  Parallel way processing complete." << std::endl;
  2211→
  2212→                // Process areas/multipolygons — sequential fallback path
  2213→                // Merge thread-local way/interp data into main ParsedData
  2214→                std::cerr << "  Merging thread-local data..." << std::endl;
  2215→                uint64_t total_ways = 0, total_building_addrs = 0, total_interps = 0;
  2216→                for (auto& local : tld) {
  2217→                    uint32_t way_base = static_cast<uint32_t>(data.ways.size());
  2218→                    uint32_t node_base = static_cast<uint32_t>(data.street_nodes.size());
  2219→                    uint32_t interp_base = static_cast<uint32_t>(data.interp_ways.size());
  2220→                    uint32_t interp_node_base = static_cast<uint32_t>(data.interp_nodes.size());
  2221→
  2222→                    // Merge street ways
  2223→                    for (size_t i = 0; i < local.ways.size(); i++) {
  2224→                        auto h = local.ways[i];
  2225→                        h.node_offset += node_base;
  2226→                        h.name_id = data.string_pool.intern(local.way_strings[i]);
  2227→                        data.ways.push_back(h);
  2228→                    }
  2229→                    data.street_nodes.insert(data.street_nodes.end(),

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(),


The main function is very long. Rather than copying everything manually, I'll take a more efficient approach — replace the entire file content, keeping just the includes and main():


Edit

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

#include <osmium/handler.hpp>
#include <osmium/io/pbf_input.hpp>
#include <osmium/visitor.hpp>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

#include <s2/s2cell_id.h>
#include <s2/s2latlng.h>
#include <s2/s2region_coverer.h>
#include <s2/s2polyline.h>
#include <s2/s2polygon.h>
#include <s2/s2loop.h>
#include <s2/s2builder.h>

#include "types.h"
#include "string_pool.h"
#include "geometry.h"

// --- Directory creation ---

static void ensure_dir(const std::string& path) {
    mkdir(path.c_str(), 0755);
}

// Types, constants, and IndexMode are in types.h

// StringPool is in string_pool.h

// --- Phase timer ---
static void log_phase(const char* name, std::chrono::steady_clock::time_point& t) {
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - t).count();
    std::cerr << "  [" << ms/1000 << "." << (ms%1000)/100 << "s] " << name << std::endl;
    t = std::chrono::steady_clock::now();
}

// DeferredWay, DeferredInterp, CollectedRelation are in types.h

// Forward declaration
static std::vector<std::pair<S2CellId, bool>> cover_polygon(const std::vector<std::pair<double,double>>& vertices);

#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"