  1600→    }
  1601→
  1602→    // Find where each chunk of sorted_cells starts in sorted_pairs
  1603→    // using binary search (both are sorted by cell_id)
  1604→    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
  1605→    if (nthreads == 0) nthreads = 4;
  1606→    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;
  1607→
  1608→    struct ChunkResult {
  1609→        std::vector<char> buf;
  1610→        size_t cell_start, cell_end; // range in sorted_cells
  1611→        uint32_t local_size; // total bytes in this chunk's buffer
  1612→    };
  1613→    std::vector<ChunkResult> chunks(nthreads);
  1614→
  1615→    // Parallel: each thread processes its range of sorted_cells
  1616→    std::vector<std::thread> threads;
  1617→    for (unsigned int t = 0; t < nthreads; t++) {
  1618→        size_t cs = t * cells_per_chunk;
  1619→        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
  1620→        if (cs >= sorted_cells.size()) break;
  1621→
  1622→        threads.emplace_back([&, t, cs, ce]() {
  1623→            auto& chunk = chunks[t];
  1624→            chunk.cell_start = cs;
  1625→            chunk.cell_end = ce;
  1626→            chunk.local_size = 0;
  1627→
  1628→            // Binary search for where this chunk's first cell appears in sorted_pairs
  1629→            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
  1630→                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
  1631→                    return p.cell_id < id;
  1632→                }) - sorted_pairs.begin();
  1633→
  1634→            // Merge-join for this chunk
  1635→            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
  1636→                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
  1637→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
  1638→                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;
  1639→
  1640→                offsets[si] = chunk.local_size; // local offset, adjusted later
  1641→                size_t start = pi;
  1642→                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
  1643→                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
  1644→                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
  1645→                size_t buf_pos = chunk.buf.size();
  1646→                chunk.buf.resize(buf_pos + entry_size);
  1647→                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
  1648→                for (size_t k = start; k < pi; k++) {
  1649→                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
  1650→                           &sorted_pairs[k].item_id, sizeof(uint32_t));
  1651→                }
  1652→                chunk.local_size += entry_size;
  1653→            }
  1654→        });
  1655→    }
  1656→    for (auto& t : threads) t.join();
  1657→
  1658→    // Prefix-sum: compute global offsets for each chunk
  1659→    uint32_t global_offset = 0;
  1660→    for (auto& chunk : chunks) {
  1661→        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
  1662→            if (offsets[si] != NO_DATA) {
  1663→                offsets[si] += global_offset;
  1664→            }
  1665→        }
  1666→        global_offset += chunk.local_size;
  1667→    }
  1668→
  1669→    // Concatenate buffers and write
  1670→    std::vector<char> buf;
  1671→    buf.reserve(global_offset);
  1672→    for (auto& chunk : chunks) {
  1673→        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
  1674→    }
  1675→
  1676→    std::ofstream f(path, std::ios::binary);
  1677→    f.write(buf.data(), buf.size());
  1678→    return offsets;
  1679→}
  1680→
  1681→static void write_cell_index(
  1682→    const std::string& cells_path,
  1683→    const std::string& entries_path,
  1684→    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
  1685→) {
  1686→    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
  1687→        cell_map.begin(), cell_map.end());
  1688→    std::sort(sorted.begin(), sorted.end());
  1689→
  1690→    {
  1691→        std::ofstream f(cells_path, std::ios::binary);
  1692→        uint32_t current_offset = 0;
  1693→        for (const auto& [cell_id, ids] : sorted) {
  1694→            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
  1695→            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
  1696→            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
  1697→        }
  1698→    }
  1699→
  1700→    {
  1701→        std::ofstream f(entries_path, std::ios::binary);
  1702→        for (const auto& [cell_id, ids] : sorted) {
  1703→            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
  1704→            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
  1705→            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
  1706→        }
  1707→    }
  1708→}
  1709→
  1710→// --- Write all index files ---
  1711→
  1712→static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
  1713→    ensure_dir(output_dir);
  1714→    auto _wt = std::chrono::steady_clock::now();
  1715→
  1716→    bool write_streets = (mode != IndexMode::AdminOnly);
  1717→    bool write_addresses = (mode == IndexMode::Full);
  1718→
  1719→    if (write_streets) {
  1720→        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
  1721→        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
  1722→        std::vector<uint64_t> sorted_geo_cells;
  1723→        {
  1724→            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
  1725→                std::vector<uint64_t> cells;
  1726→                cells.reserve(pairs.size() / 2);
  1727→                for (size_t i = 0; i < pairs.size(); ) {
  1728→                    cells.push_back(pairs[i].cell_id);
  1729→                    uint64_t cur = pairs[i].cell_id;
  1730→                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
  1731→                }
  1732→                return cells;
  1733→            };
  1734→            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
  1735→                std::vector<uint64_t> cells;
  1736→                cells.reserve(m.size());
  1737→                for (auto& [id, _] : m) cells.push_back(id);
  1738→                std::sort(cells.begin(), cells.end());
  1739→                return cells;
  1740→            };
  1741→
  1742→            // Extract unique cell IDs in parallel
  1743→            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
  1744→            {
  1745→                auto f1 = std::async(std::launch::async, [&] {
  1746→                    return !data.sorted_way_cells.empty()
  1747→                        ? extract_unique_cells(data.sorted_way_cells)
  1748→                        : extract_from_map(data.cell_to_ways);
  1749→                });
  1750→                if (write_addresses) {
  1751→                    auto f2 = std::async(std::launch::async, [&] {
  1752→                        return !data.sorted_addr_cells.empty()
  1753→                            ? extract_unique_cells(data.sorted_addr_cells)
  1754→                            : extract_from_map(data.cell_to_addrs);
  1755→                    });
  1756→                    auto f3 = std::async(std::launch::async, [&] {
  1757→                        return extract_from_map(data.cell_to_interps);
  1758→                    });
  1759→                    addr_cells = f2.get();
  1760→                    interp_cells = f3.get();
  1761→                }
  1762→                way_cells = f1.get();
  1763→            }
  1764→
  1765→            // Merge sorted unique cell ID vectors
  1766→            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
  1767→            std::merge(way_cells.begin(), way_cells.end(),
  1768→                       addr_cells.begin(), addr_cells.end(),
  1769→                       std::back_inserter(sorted_geo_cells));
  1770→            if (!interp_cells.empty()) {
  1771→                std::vector<uint64_t> tmp;
  1772→                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
  1773→                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
  1774→                           interp_cells.begin(), interp_cells.end(),
  1775→                           std::back_inserter(tmp));
  1776→                sorted_geo_cells = std::move(tmp);
  1777→            }
  1778→            // Deduplicate (merge may have duplicates from overlapping cells)
  1779→            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
  1780→                                    sorted_geo_cells.end());
  1781→        }
  1782→
  1783→        // Write entry files in parallel (returns positional offset vectors, not hash maps)
  1784→        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
  1785→        {
  1786→            auto f1 = std::async(std::launch::async, [&]() {
  1787→                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
  1788→                if (!data.sorted_way_cells.empty()) {
  1789→                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
  1790→                }
  1791→                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
  1792→            });
  1793→            if (write_addresses) {
  1794→                auto f2 = std::async(std::launch::async, [&]() {
  1795→                    if (!data.sorted_addr_cells.empty()) {
  1796→                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
  1797→                    }
  1798→                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
  1799→                });
  1800→                auto f3 = std::async(std::launch::async, [&]() {
  1801→                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
  1802→                });
  1803→                addr_offsets = f2.get();
  1804→                interp_offsets = f3.get();
  1805→            }
  1806→            street_offsets = f1.get();
  1807→        }
  1808→
  1809→        log_phase("  Write: entry files", _wt);
  1810→        // Build geo_cells.bin — parallel buffer fill + single write
  1811→        {
  1812→            size_t n = sorted_geo_cells.size();
  1813→            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
  1814→            std::vector<char> buf(n * row_size);
  1815→
  1816→            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
  1817→            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);
  1818→
  1819→            // Parallel buffer fill — split into chunks across threads
  1820→            unsigned int nthreads = std::thread::hardware_concurrency();
  1821→            if (nthreads == 0) nthreads = 4;
  1822→            size_t chunk = (n + nthreads - 1) / nthreads;
  1823→            std::vector<std::thread> fill_threads;
  1824→            for (unsigned int t = 0; t < nthreads; t++) {
  1825→                size_t start = t * chunk;
  1826→                size_t end = std::min(start + chunk, n);
  1827→                if (start >= n) break;
  1828→                fill_threads.emplace_back([&, start, end]() {
  1829→                    char* ptr = buf.data() + start * row_size;
  1830→                    for (size_t i = start; i < end; i++) {
  1831→                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
  1832→                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1833→                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1834→                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
  1835→                    }
  1836→                });
  1837→            }
  1838→            for (auto& t : fill_threads) t.join();
  1839→
  1840→            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
  1841→            f.write(buf.data(), buf.size());
  1842→        }
  1843→
  1844→        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
  1845→                  << data.ways.size() << " ways, "
  1846→                  << data.addr_points.size() << " addrs, "
  1847→                  << data.interp_ways.size() << " interps)" << std::endl;
  1848→    }
  1849→
  1850→    log_phase("  Write: geo_cells.bin", _wt);
  1851→    // Write admin cell index, raw data files, and admin polygons in parallel
  1852→    auto admin_future = std::async(std::launch::async, [&] {
  1853→        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
  1854→        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
  1855→    });
  1856→
  1857→    std::vector<std::future<void>> write_futures;
  1858→
  1859→    if (write_streets) {
  1860→        write_futures.push_back(std::async(std::launch::async, [&] {
  1861→            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
  1862→            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
  1863→        }));
  1864→        write_futures.push_back(std::async(std::launch::async, [&] {
  1865→            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
  1866→            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
  1867→        }));
  1868→        if (write_addresses) {
  1869→            write_futures.push_back(std::async(std::launch::async, [&] {
  1870→                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
  1871→                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
  1872→            }));
  1873→            write_futures.push_back(std::async(std::launch::async, [&] {
  1874→                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
  1875→                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
  1876→            }));
  1877→            write_futures.push_back(std::async(std::launch::async, [&] {
  1878→                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
  1879→                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
  1880→            }));
  1881→        }
  1882→    }
  1883→
  1884→    write_futures.push_back(std::async(std::launch::async, [&] {
  1885→        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
  1886→        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
  1887→    }));
  1888→
  1889→    write_futures.push_back(std::async(std::launch::async, [&] {
  1890→        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
  1891→        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
  1892→    }));
  1893→
  1894→    write_futures.push_back(std::async(std::launch::async, [&] {
  1895→        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
  1896→        f.write(data.string_pool.data().data(), data.string_pool.data().size());
  1897→        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
  1898→    }));
  1899→
  1900→    // Wait for all parallel writes
  1901→    log_phase("  Write: parallel data files launched", _wt);
  1902→    admin_future.get();
  1903→    for (auto& f : write_futures) f.get();
  1904→}
  1905→
  1906→// --- Main ---
  1907→
  1908→int main(int argc, char* argv[]) {
  1909→    if (argc < 2) {
  1910→        std::cerr << "Usage: build-index <output-dir> <input.osm.pbf> [options]" << std::endl;
  1911→        std::cerr << "       build-index <output-dir> --load-cache <path> [options]" << std::endl;
  1912→        std::cerr << std::endl;
  1913→        std::cerr << "Options:" << std::endl;
  1914→        std::cerr << "  --street-level N       S2 cell level for streets (default: 17)" << std::endl;
  1915→        std::cerr << "  --admin-level N        S2 cell level for admin (default: 10)" << std::endl;
  1916→        std::cerr << "  --max-admin-level N    Only include admin levels <= N" << std::endl;
  1917→        std::cerr << "  --save-cache <path>    Save parsed data to cache file" << std::endl;
  1918→        std::cerr << "  --load-cache <path>    Load from cache instead of PBF" << std::endl;
  1919→        std::cerr << "  --multi-output         Write full, no-addresses, admin-only indexes" << std::endl;
  1920→        std::cerr << "  --continents           Also generate per-continent indexes" << std::endl;
  1921→        std::cerr << "  --mode <mode>          Index mode: full, no-addresses, admin-only (default: full)" << std::endl;
  1922→        std::cerr << "  --admin-only           Shorthand for --mode admin-only" << std::endl;
  1923→        std::cerr << "  --no-addresses         Shorthand for --mode no-addresses" << std::endl;
  1924→        return 1;
  1925→    }
  1926→
  1927→    std::string output_dir = argv[1];
  1928→    std::vector<std::string> input_files;
  1929→    std::string save_cache_path;
  1930→    std::string load_cache_path;
  1931→    bool multi_output = false;
  1932→    bool generate_continents = false;
  1933→    IndexMode mode = IndexMode::Full;
  1934→
  1935→    for (int i = 2; i < argc; i++) {
  1936→        std::string arg = argv[i];
  1937→        if (arg == "--street-level" && i + 1 < argc) {
  1938→            kStreetCellLevel = std::atoi(argv[++i]);
  1939→        } else if (arg == "--admin-level" && i + 1 < argc) {
  1940→            kAdminCellLevel = std::atoi(argv[++i]);
  1941→        } else if (arg == "--admin-only") {
  1942→            mode = IndexMode::AdminOnly;
  1943→        } else if (arg == "--no-addresses") {
  1944→            mode = IndexMode::NoAddresses;
  1945→        } else if (arg == "--max-admin-level" && i + 1 < argc) {
  1946→            kMaxAdminLevel = std::atoi(argv[++i]);
  1947→        } else if (arg == "--save-cache" && i + 1 < argc) {
  1948→            save_cache_path = argv[++i];
  1949→        } else if (arg == "--load-cache" && i + 1 < argc) {
  1950→            load_cache_path = argv[++i];
  1951→        } else if (arg == "--multi-output") {
  1952→            multi_output = true;
  1953→        } else if (arg == "--continents") {
  1954→            generate_continents = true;
  1955→        } else if (arg == "--mode" && i + 1 < argc) {
  1956→            std::string mode_str = argv[++i];
  1957→            if (mode_str == "full") {
  1958→                mode = IndexMode::Full;
  1959→            } else if (mode_str == "no-addresses") {
  1960→                mode = IndexMode::NoAddresses;
  1961→            } else if (mode_str == "admin-only") {
  1962→                mode = IndexMode::AdminOnly;
  1963→            } else {
  1964→                std::cerr << "Error: unknown mode '" << mode_str << "'" << std::endl;
  1965→                return 1;
  1966→            }
  1967→        } else {
  1968→            input_files.push_back(arg);
  1969→        }
  1970→    }
  1971→
  1972→    ParsedData data;
  1973→    auto _pt = std::chrono::steady_clock::now();
  1974→
  1975→    if (!load_cache_path.empty()) {
  1976→        // Load from cache
  1977→        if (!deserialize_cache(data, load_cache_path)) {
  1978→            std::cerr << "Error: failed to load cache" << std::endl;
  1979→            return 1;
  1980→        }
  1981→    } else {
  1982→        // Parse PBF files (always collect everything)
  1983→        if (input_files.empty()) {
  1984→            std::cerr << "Error: no input files specified and no --load-cache" << std::endl;
  1985→            return 1;
  1986→        }
  1987→
  1988→        // Create thread pool for concurrent admin polygon S2 covering
  1989→        unsigned int num_threads = std::max(1u, std::thread::hardware_concurrency() > 4 ? std::thread::hardware_concurrency() - 4 : 1u);
  1990→        std::cerr << "Using " << num_threads << " worker threads." << std::endl;
  1991→        AdminCoverPool admin_pool(num_threads);
  1992→        // BuildHandler no longer used — parallel processing handles everything
  1993→
  1994→        // Dense array node location index — lockless parallel writes
  1995→        // Planet OSM node IDs max ~12.5 billion. 8 bytes per Location = 100GB virtual.
  1996→        // MAP_NORESERVE means OS only allocates pages on write (~80GB for 10B nodes).
  1997→        static const size_t MAX_NODE_ID = 15000000000ULL;
  1998→        struct DenseIndex {
  1999→            osmium::Location* data;

    }

    // Find where each chunk of sorted_cells starts in sorted_pairs
    // using binary search (both are sorted by cell_id)
    unsigned int nthreads = std::min(std::thread::hardware_concurrency(), 32u);
    if (nthreads == 0) nthreads = 4;
    size_t cells_per_chunk = (sorted_cells.size() + nthreads - 1) / nthreads;

    struct ChunkResult {
        std::vector<char> buf;
        size_t cell_start, cell_end; // range in sorted_cells
        uint32_t local_size; // total bytes in this chunk's buffer
    };
    std::vector<ChunkResult> chunks(nthreads);

    // Parallel: each thread processes its range of sorted_cells
    std::vector<std::thread> threads;
    for (unsigned int t = 0; t < nthreads; t++) {
        size_t cs = t * cells_per_chunk;
        size_t ce = std::min(cs + cells_per_chunk, sorted_cells.size());
        if (cs >= sorted_cells.size()) break;

        threads.emplace_back([&, t, cs, ce]() {
            auto& chunk = chunks[t];
            chunk.cell_start = cs;
            chunk.cell_end = ce;
            chunk.local_size = 0;

            // Binary search for where this chunk's first cell appears in sorted_pairs
            size_t pi = std::lower_bound(sorted_pairs.begin(), sorted_pairs.end(),
                sorted_cells[cs], [](const CellItemPair& p, uint64_t id) {
                    return p.cell_id < id;
                }) - sorted_pairs.begin();

            // Merge-join for this chunk
            for (size_t si = cs; si < ce && pi < sorted_pairs.size(); si++) {
                if (sorted_cells[si] < sorted_pairs[pi].cell_id) continue;
                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id < sorted_cells[si]) pi++;
                if (pi >= sorted_pairs.size() || sorted_pairs[pi].cell_id != sorted_cells[si]) continue;

                offsets[si] = chunk.local_size; // local offset, adjusted later
                size_t start = pi;
                while (pi < sorted_pairs.size() && sorted_pairs[pi].cell_id == sorted_cells[si]) pi++;
                uint16_t count = static_cast<uint16_t>(std::min(pi - start, size_t(65535)));
                size_t entry_size = sizeof(uint16_t) + (pi - start) * sizeof(uint32_t);
                size_t buf_pos = chunk.buf.size();
                chunk.buf.resize(buf_pos + entry_size);
                memcpy(chunk.buf.data() + buf_pos, &count, sizeof(count));
                for (size_t k = start; k < pi; k++) {
                    memcpy(chunk.buf.data() + buf_pos + sizeof(uint16_t) + (k - start) * sizeof(uint32_t),
                           &sorted_pairs[k].item_id, sizeof(uint32_t));
                }
                chunk.local_size += entry_size;
            }
        });
    }
    for (auto& t : threads) t.join();

    // Prefix-sum: compute global offsets for each chunk
    uint32_t global_offset = 0;
    for (auto& chunk : chunks) {
        for (size_t si = chunk.cell_start; si < chunk.cell_end; si++) {
            if (offsets[si] != NO_DATA) {
                offsets[si] += global_offset;
            }
        }
        global_offset += chunk.local_size;
    }

    // Concatenate buffers and write
    std::vector<char> buf;
    buf.reserve(global_offset);
    for (auto& chunk : chunks) {
        buf.insert(buf.end(), chunk.buf.begin(), chunk.buf.end());
    }

    std::ofstream f(path, std::ios::binary);
    f.write(buf.data(), buf.size());
    return offsets;
}

static void write_cell_index(
    const std::string& cells_path,
    const std::string& entries_path,
    const std::unordered_map<uint64_t, std::vector<uint32_t>>& cell_map
) {
    std::vector<std::pair<uint64_t, std::vector<uint32_t>>> sorted(
        cell_map.begin(), cell_map.end());
    std::sort(sorted.begin(), sorted.end());

    {
        std::ofstream f(cells_path, std::ios::binary);
        uint32_t current_offset = 0;
        for (const auto& [cell_id, ids] : sorted) {
            f.write(reinterpret_cast<const char*>(&cell_id), sizeof(cell_id));
            f.write(reinterpret_cast<const char*>(&current_offset), sizeof(current_offset));
            current_offset += sizeof(uint16_t) + ids.size() * sizeof(uint32_t);
        }
    }

    {
        std::ofstream f(entries_path, std::ios::binary);
        for (const auto& [cell_id, ids] : sorted) {
            uint16_t count = static_cast<uint16_t>(std::min(ids.size(), size_t(65535)));
            f.write(reinterpret_cast<const char*>(&count), sizeof(count));
            f.write(reinterpret_cast<const char*>(ids.data()), ids.size() * sizeof(uint32_t));
        }
    }
}

// --- Write all index files ---

static void write_index(const ParsedData& data, const std::string& output_dir, IndexMode mode) {
    ensure_dir(output_dir);
    auto _wt = std::chrono::steady_clock::now();

    bool write_streets = (mode != IndexMode::AdminOnly);
    bool write_addresses = (mode == IndexMode::Full);

    if (write_streets) {
        // Build sorted_geo_cells from sorted pairs where available (avoids hash map iteration).
        // Extract unique cell IDs from each sorted pair vector in parallel, then merge.
        std::vector<uint64_t> sorted_geo_cells;
        {
            auto extract_unique_cells = [](const std::vector<CellItemPair>& pairs) {
                std::vector<uint64_t> cells;
                cells.reserve(pairs.size() / 2);
                for (size_t i = 0; i < pairs.size(); ) {
                    cells.push_back(pairs[i].cell_id);
                    uint64_t cur = pairs[i].cell_id;
                    while (i < pairs.size() && pairs[i].cell_id == cur) i++;
                }
                return cells;
            };
            auto extract_from_map = [](const std::unordered_map<uint64_t, std::vector<uint32_t>>& m) {
                std::vector<uint64_t> cells;
                cells.reserve(m.size());
                for (auto& [id, _] : m) cells.push_back(id);
                std::sort(cells.begin(), cells.end());
                return cells;
            };

            // Extract unique cell IDs in parallel
            std::vector<uint64_t> way_cells, addr_cells, interp_cells;
            {
                auto f1 = std::async(std::launch::async, [&] {
                    return !data.sorted_way_cells.empty()
                        ? extract_unique_cells(data.sorted_way_cells)
                        : extract_from_map(data.cell_to_ways);
                });
                if (write_addresses) {
                    auto f2 = std::async(std::launch::async, [&] {
                        return !data.sorted_addr_cells.empty()
                            ? extract_unique_cells(data.sorted_addr_cells)
                            : extract_from_map(data.cell_to_addrs);
                    });
                    auto f3 = std::async(std::launch::async, [&] {
                        return extract_from_map(data.cell_to_interps);
                    });
                    addr_cells = f2.get();
                    interp_cells = f3.get();
                }
                way_cells = f1.get();
            }

            // Merge sorted unique cell ID vectors
            sorted_geo_cells.reserve(way_cells.size() + addr_cells.size() + interp_cells.size());
            std::merge(way_cells.begin(), way_cells.end(),
                       addr_cells.begin(), addr_cells.end(),
                       std::back_inserter(sorted_geo_cells));
            if (!interp_cells.empty()) {
                std::vector<uint64_t> tmp;
                tmp.reserve(sorted_geo_cells.size() + interp_cells.size());
                std::merge(sorted_geo_cells.begin(), sorted_geo_cells.end(),
                           interp_cells.begin(), interp_cells.end(),
                           std::back_inserter(tmp));
                sorted_geo_cells = std::move(tmp);
            }
            // Deduplicate (merge may have duplicates from overlapping cells)
            sorted_geo_cells.erase(std::unique(sorted_geo_cells.begin(), sorted_geo_cells.end()),
                                    sorted_geo_cells.end());
        }

        // Write entry files in parallel (returns positional offset vectors, not hash maps)
        std::vector<uint32_t> street_offsets, addr_offsets, interp_offsets;
        {
            auto f1 = std::async(std::launch::async, [&]() {
                // Use direct sorted-pair writing for ways (skips hash map extraction + sort)
                if (!data.sorted_way_cells.empty()) {
                    return write_entries_from_sorted(output_dir + "/street_entries.bin", sorted_geo_cells, data.sorted_way_cells);
                }
                return write_entries(output_dir + "/street_entries.bin", sorted_geo_cells, data.cell_to_ways);
            });
            if (write_addresses) {
                auto f2 = std::async(std::launch::async, [&]() {
                    if (!data.sorted_addr_cells.empty()) {
                        return write_entries_from_sorted(output_dir + "/addr_entries.bin", sorted_geo_cells, data.sorted_addr_cells);
                    }
                    return write_entries(output_dir + "/addr_entries.bin", sorted_geo_cells, data.cell_to_addrs);
                });
                auto f3 = std::async(std::launch::async, [&]() {
                    return write_entries(output_dir + "/interp_entries.bin", sorted_geo_cells, data.cell_to_interps);
                });
                addr_offsets = f2.get();
                interp_offsets = f3.get();
            }
            street_offsets = f1.get();
        }

        log_phase("  Write: entry files", _wt);
        // Build geo_cells.bin — parallel buffer fill + single write
        {
            size_t n = sorted_geo_cells.size();
            size_t row_size = sizeof(uint64_t) + 3 * sizeof(uint32_t);
            std::vector<char> buf(n * row_size);

            if (addr_offsets.empty()) addr_offsets.resize(n, NO_DATA);
            if (interp_offsets.empty()) interp_offsets.resize(n, NO_DATA);

            // Parallel buffer fill — split into chunks across threads
            unsigned int nthreads = std::thread::hardware_concurrency();
            if (nthreads == 0) nthreads = 4;
            size_t chunk = (n + nthreads - 1) / nthreads;
            std::vector<std::thread> fill_threads;
            for (unsigned int t = 0; t < nthreads; t++) {
                size_t start = t * chunk;
                size_t end = std::min(start + chunk, n);
                if (start >= n) break;
                fill_threads.emplace_back([&, start, end]() {
                    char* ptr = buf.data() + start * row_size;
                    for (size_t i = start; i < end; i++) {
                        memcpy(ptr, &sorted_geo_cells[i], sizeof(uint64_t)); ptr += sizeof(uint64_t);
                        memcpy(ptr, &street_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                        memcpy(ptr, &addr_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                        memcpy(ptr, &interp_offsets[i], sizeof(uint32_t)); ptr += sizeof(uint32_t);
                    }
                });
            }
            for (auto& t : fill_threads) t.join();

            std::ofstream f(output_dir + "/geo_cells.bin", std::ios::binary);
            f.write(buf.data(), buf.size());
        }

        std::cerr << "geo index: " << sorted_geo_cells.size() << " cells ("
                  << data.ways.size() << " ways, "
                  << data.addr_points.size() << " addrs, "
                  << data.interp_ways.size() << " interps)" << std::endl;
    }

    log_phase("  Write: geo_cells.bin", _wt);
    // Write admin cell index, raw data files, and admin polygons in parallel
    auto admin_future = std::async(std::launch::async, [&] {
        write_cell_index(output_dir + "/admin_cells.bin", output_dir + "/admin_entries.bin", data.cell_to_admin);
        std::cerr << "admin index: " << data.cell_to_admin.size() << " cells, " << data.admin_polygons.size() << " polygons" << std::endl;
    });

    std::vector<std::future<void>> write_futures;

    if (write_streets) {
        write_futures.push_back(std::async(std::launch::async, [&] {
            std::ofstream f(output_dir + "/street_ways.bin", std::ios::binary);
            f.write(reinterpret_cast<const char*>(data.ways.data()), data.ways.size() * sizeof(WayHeader));
        }));
        write_futures.push_back(std::async(std::launch::async, [&] {
            std::ofstream f(output_dir + "/street_nodes.bin", std::ios::binary);
            f.write(reinterpret_cast<const char*>(data.street_nodes.data()), data.street_nodes.size() * sizeof(NodeCoord));
        }));
        if (write_addresses) {
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/addr_points.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.addr_points.data()), data.addr_points.size() * sizeof(AddrPoint));
            }));
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/interp_ways.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.interp_ways.data()), data.interp_ways.size() * sizeof(InterpWay));
            }));
            write_futures.push_back(std::async(std::launch::async, [&] {
                std::ofstream f(output_dir + "/interp_nodes.bin", std::ios::binary);
                f.write(reinterpret_cast<const char*>(data.interp_nodes.data()), data.interp_nodes.size() * sizeof(NodeCoord));
            }));
        }
    }

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/admin_polygons.bin", std::ios::binary);
        f.write(reinterpret_cast<const char*>(data.admin_polygons.data()), data.admin_polygons.size() * sizeof(AdminPolygon));
    }));

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/admin_vertices.bin", std::ios::binary);
        f.write(reinterpret_cast<const char*>(data.admin_vertices.data()), data.admin_vertices.size() * sizeof(NodeCoord));
    }));

    write_futures.push_back(std::async(std::launch::async, [&] {
        std::ofstream f(output_dir + "/strings.bin", std::ios::binary);
        f.write(data.string_pool.data().data(), data.string_pool.data().size());
        std::cerr << "strings.bin: " << data.string_pool.data().size() << " bytes" << std::endl;
    }));

    // Wait for all parallel writes
    log_phase("  Write: parallel data files launched", _wt);
    admin_future.get();
    for (auto& f : write_futures) f.get();
}

// --- 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 = 15000000000ULL;
        struct DenseIndex {
            osmium::Location* data;


  2000→            size_t capacity;
  2001→
  2002→            DenseIndex() : capacity(MAX_NODE_ID) {
  2003→                void* ptr = mmap(nullptr, capacity * sizeof(osmium::Location),
  2004→                    PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
  2005→                    -1, 0);
  2006→                if (ptr == MAP_FAILED) {
  2007→                    std::cerr << "Error: failed to mmap dense node index ("
  2008→                              << (capacity * sizeof(osmium::Location) / (1024*1024*1024)) << "GB virtual)" << std::endl;
  2009→                    std::exit(1);
  2010→                }
  2011→                data = static_cast<osmium::Location*>(ptr);
  2012→                std::cerr << "Allocated dense node index: " << (capacity * sizeof(osmium::Location) / (1024*1024*1024))
  2013→                          << "GB virtual address space" << std::endl;
  2014→            }
  2015→
  2016→            ~DenseIndex() {
  2017→                munmap(data, capacity * sizeof(osmium::Location));
  2018→            }
  2019→
  2020→            // Lockless — each node ID maps to a unique array slot
  2021→            void set(osmium::unsigned_object_id_type id, osmium::Location loc) {
  2022→                if (id >= capacity) {
  2023→                    std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  2024→                    std::exit(1);
  2025→                }
  2026→                data[id] = loc;
  2027→            }
  2028→
  2029→            osmium::Location get(osmium::unsigned_object_id_type id) const {
  2030→                if (id >= capacity) return osmium::Location{};
  2031→                return data[id];
  2032→            }
  2033→
  2034→            void set_batch(const std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>>& batch) {
  2035→                for (const auto& [id, loc] : batch) {
  2036→                    if (id >= capacity) {
  2037→                        std::cerr << "FATAL: node ID " << id << " exceeds dense index capacity " << capacity << std::endl;
  2038→                        std::exit(1);
  2039→                    }
  2040→                    data[id] = loc;
  2041→                }
  2042→            }
  2043→        } index;
  2044→
  2045→        for (const auto& input_file : input_files) {
  2046→            std::cerr << "Processing " << input_file << "..." << std::endl;
  2047→
  2048→            // --- Pass 1: collect relation members for parallel admin assembly ---
  2049→            std::cerr << "  Pass 1: scanning relations..." << std::endl;
  2050→            RelationCollector rel_collector(data.collected_relations);
  2051→
  2052→            {
  2053→                osmium::io::Reader reader1{input_file, osmium::osm_entity_bits::relation};
  2054→                osmium::apply(reader1, rel_collector);
  2055→                reader1.close();
  2056→            }
  2057→            std::cerr << "  Collected " << data.collected_relations.size()
  2058→                      << " admin/postal relations for parallel assembly." << std::endl;
  2059→
  2060→            // --- Combined Pass 2+3: nodes then ways in a single PBF read ---
  2061→            // PBF guarantees ordering: nodes → ways → relations.
  2062→            // We read everything in one pass, processing nodes first, then
  2063→            // transitioning to way processing when the first way block arrives.
  2064→            log_phase("Pass 1: relation scanning", _pt);
  2065→
  2066→            // Build admin_way_ids BEFORE the combined pass (needed during way processing)
  2067→            std::unordered_set<int64_t> admin_way_ids;
  2068→            for (const auto& rel : data.collected_relations) {
  2069→                for (const auto& [way_id, role] : rel.members) {
  2070→                    admin_way_ids.insert(way_id);
  2071→                }
  2072→            }
  2073→            std::cerr << "  Admin assembly needs " << admin_way_ids.size() << " way geometries." << std::endl;
  2074→
  2075→            std::cerr << "  Pass 2+3: processing nodes and ways in single read..." << std::endl;
  2076→
  2077→            {
  2078→                // Shared queue infrastructure for both phases
  2079→                std::mutex queue_mutex;
  2080→                std::condition_variable queue_cv;
  2081→                std::deque<osmium::memory::Buffer> block_queue;
  2082→                bool reader_done = false;
  2083→                const size_t MAX_QUEUE = 64;
  2084→
  2085→                struct NodeThreadLocal {
  2086→                    std::vector<std::pair<double,double>> addr_coords;
  2087→                    std::vector<std::pair<std::string,std::string>> addr_strings;
  2088→                    uint64_t count = 0;
  2089→                };
  2090→                std::vector<NodeThreadLocal> ntld(num_threads);
  2091→                std::atomic<uint64_t> blocks_done{0};
  2092→
  2093→                // Worker threads
  2094→                std::vector<std::thread> node_workers;
  2095→                for (unsigned int t = 0; t < num_threads; t++) {
  2096→                    node_workers.emplace_back([&, t]() {
  2097→                        auto& local = ntld[t];
  2098→                        std::vector<std::pair<osmium::unsigned_object_id_type, osmium::Location>> loc_batch;
  2099→                        loc_batch.reserve(8192);
  2100→
  2101→                        while (true) {
  2102→                            osmium::memory::Buffer block;
  2103→                            {
  2104→                                std::unique_lock<std::mutex> lock(queue_mutex);
  2105→                                queue_cv.wait(lock, [&]{ return !block_queue.empty() || reader_done; });
  2106→                                if (block_queue.empty() && reader_done) break;
  2107→                                block = std::move(block_queue.front());
  2108→                                block_queue.pop_front();
  2109→                            }
  2110→                            queue_cv.notify_one(); // wake reader if it was waiting
  2111→
  2112→                            for (const auto& item : block) {
  2113→                                if (item.type() == osmium::item_type::node) {
  2114→                                    const auto& node = static_cast<const osmium::Node&>(item);
  2115→                                    if (!node.location().valid()) continue;
  2116→                                    loc_batch.push_back({node.positive_id(), node.location()});
  2117→
  2118→                                    const char* housenumber = node.tags()["addr:housenumber"];
  2119→                                    if (housenumber) {
  2120→                                        const char* street = node.tags()["addr:street"];
  2121→                                        if (street) {
  2122→                                            local.addr_coords.push_back({node.location().lat(), node.location().lon()});
  2123→                                            local.addr_strings.push_back({housenumber, street});
  2124→                                            local.count++;
  2125→                                        }
  2126→                                    }
  2127→                                }
  2128→                            }
  2129→
  2130→                            // Flush location batch (sharded index handles its own locking)
  2131→                            if (!loc_batch.empty()) {
  2132→                                index.set_batch(loc_batch);
  2133→                                loc_batch.clear();
  2134→                            }
  2135→
  2136→                            uint64_t n = blocks_done.fetch_add(1) + 1;
  2137→                            if (n % 1000 == 0) {
  2138→                                std::cerr << "  Processed " << n << " node blocks..." << std::endl;
  2139→                            }
  2140→                        }
  2141→
  2142→                        // Flush remaining
  2143→                        if (!loc_batch.empty()) {
  2144→                            index.set_batch(loc_batch);
  2145→                        }
  2146→                    });
  2147→                }
  2148→
  2149→                // Single reader for all entity types — processes nodes first,
  2150→                // then transitions to way processing when first way block arrives.
  2151→                osmium::io::Reader combined_reader{input_file};
  2152→                bool nodes_done = false;
  2153→                osmium::memory::Buffer first_way_buf; // stash the first way block
  2154→
  2155→                // Phase 1: Feed node blocks to node workers
  2156→                while (auto buf = combined_reader.read()) {
  2157→                    // Check if this buffer contains nodes or ways/relations
  2158→                    bool has_node = false;
  2159→                    for (const auto& item : buf) {
  2160→                        if (item.type() == osmium::item_type::node) { has_node = true; break; }
  2161→                    }
  2162→
  2163→                    if (has_node) {
  2164→                        std::unique_lock<std::mutex> lock(queue_mutex);
  2165→                        queue_cv.wait(lock, [&]{ return block_queue.size() < MAX_QUEUE; });
  2166→                        block_queue.push_back(std::move(buf));
  2167→                        queue_cv.notify_one();
  2168→                    } else {
  2169→                        // First non-node block — stash it and transition
  2170→                        first_way_buf = std::move(buf);
  2171→                        nodes_done = true;
  2172→                        break;
  2173→                    }
  2174→                }
  2175→
  2176→                // Signal node workers done
  2177→                {
  2178→                    std::lock_guard<std::mutex> lock(queue_mutex);
  2179→                    reader_done = true;
  2180→                }
  2181→                queue_cv.notify_all();
  2182→                for (auto& w : node_workers) w.join();
  2183→
  2184→                // Merge address points
  2185→                uint64_t total_addrs = 0;
  2186→                for (auto& local : ntld) {
  2187→                    for (size_t j = 0; j < local.addr_coords.size(); j++) {
  2188→                        uint64_t dummy = 0;
  2189→                        add_addr_point(data, local.addr_coords[j].first, local.addr_coords[j].second,
  2190→                                       local.addr_strings[j].first.c_str(),
  2191→                                       local.addr_strings[j].second.c_str(), dummy);
  2192→                    }
  2193→                    total_addrs += local.count;
  2194→                }
  2195→                std::cerr << "  Node processing complete: " << total_addrs
  2196→                          << " address points collected." << std::endl;
  2197→                log_phase("Pass 2: node processing", _pt);
  2198→                std::cerr << "  Processing ways with " << num_threads << " threads..." << std::endl;
  2199→
  2200→                // --- Phase 2: Way processing using same reader ---
  2201→
  2202→                // Thread-local data for parallel way processing
  2203→                struct ThreadLocalData {
  2204→                    std::vector<WayHeader> ways;
  2205→                    std::vector<NodeCoord> street_nodes;
  2206→                    std::vector<DeferredWay> deferred_ways;
  2207→                    std::vector<InterpWay> interp_ways;
  2208→                    std::vector<NodeCoord> interp_nodes;
  2209→                    std::vector<DeferredInterp> deferred_interps;
  2210→                    std::vector<AddrPoint> building_addrs;
  2211→                    std::vector<std::pair<double, double>> building_addr_coords; // lat,lng for S2 cell
  2212→                    std::vector<std::string> way_strings;      // way name strings
  2213→                    std::vector<std::pair<std::string,std::string>> addr_strings; // building addr {hn, street}
  2214→                    std::vector<std::string> interp_strings;   // interp street name strings
  2215→                    uint64_t way_count = 0;
  2216→                    uint64_t building_addr_count = 0;
  2217→                    uint64_t interp_count = 0;
  2218→                    // Way geometries for parallel admin assembly
  2219→                    struct WayGeomEntry {
  2220→                        int64_t way_id;
  2221→                        std::vector<std::pair<double,double>> coords;
  2222→                        int64_t first_node_id;
  2223→                        int64_t last_node_id;
  2224→                    };
  2225→                    std::vector<WayGeomEntry> way_geoms;
  2226→                    // Closed-way admin polygons (ways with boundary=administrative)
  2227→                    struct ClosedWayAdmin {
  2228→                        std::vector<std::pair<double,double>> vertices;
  2229→                        std::string name;
  2230→                        uint8_t admin_level;
  2231→                        std::string country_code;
  2232→                    };
  2233→                    std::vector<ClosedWayAdmin> closed_way_admins;
  2234→                };
  2235→
  2236→                // Read buffers and dispatch to thread pool
  2237→                // Streaming producer-consumer for way blocks
  2238→                std::mutex way_queue_mutex;
  2239→                std::condition_variable way_queue_cv;
  2240→                std::deque<osmium::memory::Buffer> way_block_queue;
  2241→                bool way_reader_done = false;
  2242→                const size_t WAY_MAX_QUEUE = 64;
  2243→
  2244→                std::vector<ThreadLocalData> tld(num_threads);
  2245→                std::atomic<uint64_t> way_blocks_done{0};
  2246→
  2247→                std::vector<std::thread> workers;
  2248→                for (unsigned int t = 0; t < num_threads; t++) {
  2249→                    workers.emplace_back([&, t]() {
  2250→                        auto& local = tld[t];
  2251→                        while (true) {
  2252→                            osmium::memory::Buffer block;
  2253→                            {
  2254→                                std::unique_lock<std::mutex> lock(way_queue_mutex);
  2255→                                way_queue_cv.wait(lock, [&]{ return !way_block_queue.empty() || way_reader_done; });
  2256→                                if (way_block_queue.empty() && way_reader_done) break;
  2257→                                block = std::move(way_block_queue.front());
  2258→                                way_block_queue.pop_front();
  2259→                            }
  2260→                            way_queue_cv.notify_one();
  2261→
  2262→                            for (const auto& item : block) {
  2263→                                if (item.type() == osmium::item_type::way) {
  2264→                                    const auto& way = static_cast<const osmium::Way&>(item);
  2265→
  2266→                                    // Resolve node locations (read-only from shared index)
  2267→                                    // We need to check if nodes have valid locations
  2268→                                    const auto& wnodes = way.nodes();
  2269→
  2270→                                    // Address interpolation
  2271→                                    const char* interpolation = way.tags()["addr:interpolation"];
  2272→                                    if (interpolation) {
  2273→                                        if (wnodes.size() >= 2) {
  2274→                                            bool all_valid = true;
  2275→                                            for (const auto& nr : wnodes) {
  2276→                                                try {
  2277→                                                    auto loc = index.get(nr.positive_ref());
  2278→                                                    if (!loc.valid()) { all_valid = false; break; }
  2279→                                                } catch (...) { all_valid = false; break; }
  2280→                                            }
  2281→                                            if (all_valid) {
  2282→                                                const char* street = way.tags()["addr:street"];
  2283→                                                if (street) {
  2284→                                                    uint32_t interp_id = static_cast<uint32_t>(local.interp_ways.size());
  2285→                                                    uint32_t node_offset = static_cast<uint32_t>(local.interp_nodes.size());
  2286→
  2287→                                                    for (const auto& nr : wnodes) {
  2288→                                                        auto loc = index.get(nr.positive_ref());
  2289→                                                        local.interp_nodes.push_back({
  2290→                                                            static_cast<float>(loc.lat()),
  2291→                                                            static_cast<float>(loc.lon())
  2292→                                                        });
  2293→                                                    }
  2294→
  2295→                                                    uint8_t interp_type = 0;
  2296→                                                    if (std::strcmp(interpolation, "even") == 0) interp_type = 1;
  2297→                                                    else if (std::strcmp(interpolation, "odd") == 0) interp_type = 2;
  2298→
  2299→                                                    InterpWay iw{};
  2300→                                                    iw.node_offset = node_offset;
  2301→                                                    iw.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2302→                                                    iw.street_id = 0; // placeholder, intern later
  2303→                                                    iw.start_number = 0;
  2304→                                                    iw.end_number = 0;
  2305→                                                    iw.interpolation = interp_type;
  2306→                                                    local.interp_ways.push_back(iw);
  2307→                                                    local.interp_strings.push_back(street);
  2308→                                                    local.deferred_interps.push_back({interp_id, node_offset, iw.node_count});
  2309→                                                    local.interp_count++;
  2310→                                                }
  2311→                                            }
  2312→                                        }
  2313→                                        continue;
  2314→                                    }
  2315→
  2316→                                    // Building addresses
  2317→                                    const char* housenumber = way.tags()["addr:housenumber"];
  2318→                                    if (housenumber) {
  2319→                                        const char* street = way.tags()["addr:street"];
  2320→                                        if (street && !wnodes.empty()) {
  2321→                                            double sum_lat = 0, sum_lng = 0;
  2322→                                            int valid = 0;
  2323→                                            for (const auto& nr : wnodes) {
  2324→                                                try {
  2325→                                                    auto loc = index.get(nr.positive_ref());
  2326→                                                    if (loc.valid()) {
  2327→                                                        sum_lat += loc.lat();
  2328→                                                        sum_lng += loc.lon();
  2329→                                                        valid++;
  2330→                                                    }
  2331→                                                } catch (...) {}
  2332→                                            }
  2333→                                            if (valid > 0) {
  2334→                                                // Store coords for later S2 cell computation
  2335→                                                local.building_addr_coords.push_back({sum_lat / valid, sum_lng / valid});
  2336→                                                // Placeholder addr point — string IDs filled in during merge
  2337→                                                local.building_addrs.push_back({
  2338→                                                    static_cast<float>(sum_lat / valid),
  2339→                                                    static_cast<float>(sum_lng / valid),
  2340→                                                    0, 0 // placeholder string IDs
  2341→                                                });
  2342→                                                // Store strings for later interning
  2343→                                                local.addr_strings.push_back({housenumber, street});
  2344→                                                local.building_addr_count++;
  2345→                                            }
  2346→                                        }
  2347→                                    }
  2348→
  2349→                                    // Highway ways
  2350→                                    const char* highway = way.tags()["highway"];
  2351→                                    if (highway && is_included_highway(highway)) {
  2352→                                        const char* name = way.tags()["name"];
  2353→                                        if (name && wnodes.size() >= 2) {
  2354→                                            bool all_valid = true;
  2355→                                            for (const auto& nr : wnodes) {
  2356→                                                try {
  2357→                                                    auto loc = index.get(nr.positive_ref());
  2358→                                                    if (!loc.valid()) { all_valid = false; break; }
  2359→                                                } catch (...) { all_valid = false; break; }
  2360→                                            }
  2361→                                            if (all_valid) {
  2362→                                                uint32_t way_id = static_cast<uint32_t>(local.ways.size());
  2363→                                                uint32_t node_offset = static_cast<uint32_t>(local.street_nodes.size());
  2364→
  2365→                                                for (const auto& nr : wnodes) {
  2366→                                                    auto loc = index.get(nr.positive_ref());
  2367→                                                    local.street_nodes.push_back({
  2368→                                                        static_cast<float>(loc.lat()),
  2369→                                                        static_cast<float>(loc.lon())
  2370→                                                    });
  2371→                                                }
  2372→
  2373→                                                WayHeader header{};
  2374→                                                header.node_offset = node_offset;
  2375→                                                header.node_count = static_cast<uint8_t>(std::min(wnodes.size(), size_t(255)));
  2376→                                                header.name_id = 0; // placeholder, intern later
  2377→                                                local.ways.push_back(header);
  2378→                                                local.way_strings.push_back(name);
  2379→                                                local.deferred_ways.push_back({way_id, node_offset, header.node_count});
  2380→                                                local.way_count++;
  2381→                                            }
  2382→                                        }
  2383→                                    }
  2384→
  2385→                                    // Store way geometry only for admin boundary member ways
  2386→                                    if (!wnodes.empty() && admin_way_ids.count(way.id())) {
  2387→                                        std::vector<std::pair<double,double>> geom;
  2388→                                        for (const auto& nr : wnodes) {
  2389→                                            try {
  2390→                                                auto loc = index.get(nr.positive_ref());
  2391→                                                if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
  2392→                                            } catch (...) {}
  2393→                                        }
  2394→                                        if (!geom.empty()) {
  2395→                                            int64_t first_nid = wnodes.front().positive_ref();
  2396→                                            int64_t last_nid = wnodes.back().positive_ref();
  2397→                                            local.way_geoms.push_back({way.id(), std::move(geom), first_nid, last_nid});
  2398→                                        }
  2399→                                    }

            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 = 64;

                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 = 64;

                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) {
                                                try {
                                                    auto loc = index.get(nr.positive_ref());
                                                    if (!loc.valid()) { all_valid = false; break; }
                                                } catch (...) { 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) {
                                                try {
                                                    auto loc = index.get(nr.positive_ref());
                                                    if (loc.valid()) {
                                                        sum_lat += loc.lat();
                                                        sum_lng += loc.lon();
                                                        valid++;
                                                    }
                                                } catch (...) {}
                                            }
                                            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) {
                                                try {
                                                    auto loc = index.get(nr.positive_ref());
                                                    if (!loc.valid()) { all_valid = false; break; }
                                                } catch (...) { 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) {
                                            try {
                                                auto loc = index.get(nr.positive_ref());
                                                if (loc.valid()) geom.push_back({loc.lat(), loc.lon()});
                                            } catch (...) {}
                                        }
                                        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});
                                        }
                                    }